Compare commits

...

250 commits

Author SHA1 Message Date
HAM
65dcd138db fix: creditNote column number wrong running
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2026-01-12 15:25:32 +07:00
net
e6d06b39da refactor: id missing test
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-12-16 17:07:28 +07:00
net
3cab6cc0e5 refactor: handle btn save
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-12-16 10:28:24 +07:00
JakkrapartXD
9994366c74 feat: Add multi-language user name display to the profile menu.
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-12-04 16:44:09 +07:00
Aif
f4db5ad855 feat: required branch
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-11-12 11:34:21 +07:00
Aif
15a812b50e feat: Refresh quotation list on window focus
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-11-12 11:27:55 +07:00
Aif
f7a8416e7a feat: Refresh task order list on window focus
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-11-12 11:10:21 +07:00
Aif
79d6482caa Add unique id and for attributes to status elements
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
Introduces dynamic id and for attributes to readonly status, dropdown, menu items, and failed remark button for improved accessibility and testability in TaskStatusComponent.vue.
2025-11-11 15:02:36 +07:00
Aif
75d5c7dfe8 feat: unique id attributes to UI components
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-11-11 11:01:36 +07:00
Aif
637eeab3c2 feat: unique IDs to UI components for testing
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-11-11 10:02:13 +07:00
net
2b1e3b12a4 refacotr: add id
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-11-07 16:24:10 +07:00
Aif
a1ed625d32 feat: for
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-20 11:24:28 +07:00
Aif
59a3f964c4 fix: Adjust the badge count to reflect the current tab
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-10-20 10:11:24 +07:00
Aif
2afb5ea7e9 feat: fetch data
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-17 10:11:25 +07:00
net
aaf776639d refactor: hide btn
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-16 14:17:58 +07:00
net
04c463a717 refactor: handle i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-15 10:24:07 +07:00
net
8e65a1c5a2 refactor: handle i18n #236
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-10-15 10:04:56 +07:00
net
21fc2d5d96 refacotr: bind value
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 18:03:59 +07:00
net
73f43c2a29 refactor: date en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 17:57:05 +07:00
net
16ea66484d refactor: add i18n #229
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 17:51:05 +07:00
net
90f31a0c87 refactor: update stats #234
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 17:25:51 +07:00
net
d1785faed2 refactor: update stats
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 17:20:10 +07:00
net
f68e8cf675 refactor: add i18n error
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 17:16:35 +07:00
net
cd4b087fec refactor: hide filter
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 16:50:14 +07:00
Aif
8a0340f588 feat: i18n payment condition en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 16:04:33 +07:00
net
2b9c8aa613 refacotr: clear value #230
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 15:47:53 +07:00
Aif
eebd585554 feat: i18n branch name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 15:28:17 +07:00
Aif
db5262da42 feat: i18n register name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 15:17:13 +07:00
Aif
72b0e89642 feat: i18n register name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 14:57:58 +07:00
Aif
2320883cb6 feat: i18n register name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 14:50:18 +07:00
Aif
0e6bee7b62 feat: i18n registerNameEN
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 14:46:12 +07:00
net
5c867a496d fix: #242
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 14:45:41 +07:00
Aif
d06c26c3c8 feat: i18n registerName
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 14:40:21 +07:00
Aif
c4f088c5cb feat: i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-10-14 14:30:08 +07:00
net
6cf8cf28aa refactor: #245 handle i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-10-14 13:32:30 +07:00
puriphatt
1249f67a0f Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-25 10:18:48 +07:00
puriphatt
05d38b1ab3 chore: remove duplicate 2025-09-25 10:18:31 +07:00
Thanaphon Frappet
d09484a52a refactor: get contact number and name 2025-09-25 10:15:12 +07:00
puriphatt
d8d02a679d fix: quotation payment date
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-25 10:14:22 +07:00
Methapon2001
11047e569d Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-09-22 08:56:46 +07:00
puriphatt
f3b5b25bf3 refactor: quotation payment method select bank from register branch
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-19 14:43:15 +07:00
Methapon2001
e817e8fd05 Merge branch 'develop' 2025-09-19 10:02:10 +07:00
puriphatt
18e5517325 fix: submit payment method
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-09-19 09:55:21 +07:00
Methapon2001
5e13864d4a Merge branch 'develop' 2025-09-19 09:13:06 +07:00
net
61dca12e5a fix: no data
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 17:34:23 +07:00
net
aa908f0c3d fix: show summary price
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 16:41:41 +07:00
net
80056f8e0b fix: search date product
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 15:39:25 +07:00
Methapon2001
02bb682150 Merge branch 'develop' 2025-09-18 14:27:56 +07:00
net
d2acd6ba4c refactor: export product
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-09-18 13:21:11 +07:00
net
d53eb15a88 fix: new worker
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 11:13:35 +07:00
Methapon2001
35e23aa291 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-09-18 10:29:45 +07:00
puriphatt
00f9b5f4c4 fix: customer filter and export
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 10:25:23 +07:00
Methapon2001
7846950802 Merge branch 'develop' 2025-09-18 10:09:37 +07:00
puriphatt
ef8e294ae4 Merge remote-tracking branch 'forgejo/refactor/customer' into develop
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-09-18 09:59:59 +07:00
net
016a54e45e fix: label 2025-09-18 09:59:59 +07:00
puriphatt
4e887fdff8 refactor: customer, employee upload profile after created 2025-09-18 09:59:59 +07:00
net
ded56d103b refactor: handle show discount 2025-09-18 09:59:59 +07:00
puriphatt
0d708405f6 refactor: drawer employee name 2025-09-18 09:59:59 +07:00
net
2099031fa8 refactor: add icon 2025-09-18 09:59:59 +07:00
puriphatt
fd12f32ab0 fix: customer & employee image list 2025-09-18 09:59:59 +07:00
net
f10103b5d0 fix: label 2025-09-18 09:59:59 +07:00
puriphatt
d6e366f788 feat: add customer export by status 2025-09-18 09:59:59 +07:00
net
52c384f0fb fix: create sub branch 2025-09-18 09:59:59 +07:00
puriphatt
fcafaeebc0 fix: change employee status with drawer alert 2025-09-18 09:59:59 +07:00
Methapon2001
0e57a3daf6 fix: .01 error 2025-09-18 09:59:59 +07:00
puriphatt
0ad017309f fix: drawer before close and employee edit state 2025-09-18 09:59:59 +07:00
net
b9cfb6274b fix: label 2025-09-18 09:59:59 +07:00
puriphatt
5becbae369 feat: filter customer by business type, address 2025-09-18 09:59:59 +07:00
net
7c3a9818c2 fix: calc price order 2025-09-18 09:59:59 +07:00
net
56f0a86845 fix: create sub branch 2025-09-18 09:59:59 +07:00
net
492f341e68 fix: current branch missing 2025-09-18 09:59:59 +07:00
puriphatt
49897ff007 fix: image selection 2025-09-18 09:59:59 +07:00
net
c430b6082e fix: calc price 2025-09-18 09:59:59 +07:00
puriphatt
e6f8870cdf refactor: drawer employee 2025-09-18 09:59:59 +07:00
Methapon2001
67cde37e34 fix: price calc on update installments 2025-09-18 09:59:59 +07:00
puriphatt
763ac07be7 refactor: customer & employee 2025-09-18 09:59:59 +07:00
Methapon2001
c07efa7318 fix: type error 2025-09-18 09:59:59 +07:00
puriphatt
507141dca5 fix: component 2025-09-18 09:59:59 +07:00
net
73b2d52fb0 fix: price calc 2025-09-18 09:59:59 +07:00
puriphatt
e6ecd39d24 feat: resize img 2025-09-18 09:59:59 +07:00
Methapon2001
c0a2d3769d fix: margin error .01 2025-09-18 09:59:59 +07:00
Methapon2001
05f7c886d6 refactor: use switch case instead 2025-09-18 09:59:59 +07:00
Methapon2001
93c54c0dd1 fix: remark 2025-09-18 09:59:59 +07:00
Methapon2001
ef4c84341c fix: price calc 2025-09-18 09:58:29 +07:00
net
09b51d601e refactor: can create new type 2025-09-18 09:58:29 +07:00
net
c29e1d4ec5 fix: select customer 2025-09-18 09:58:29 +07:00
puriphatt
d89925dee9 feat: quotation payment method 2025-09-18 09:58:29 +07:00
Thanaphon Frappet
3e24a46f66 feat: add customer export 2025-09-18 09:58:29 +07:00
Thanaphon Frappet
764d9bab3f fix: get contact number and name 2025-09-18 09:58:29 +07:00
Thanaphon Frappet
e5b2114984 fix: show remark
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-22 11:45:25 +07:00
Thanaphon Frappet
37c9f5fcd5 fix: edit label
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-22 11:25:51 +07:00
Thanaphon Frappet
67a69b85e0 refactor: add ( ) ,
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-21 15:54:30 +07:00
Methapon2001
eb88cc4269 fix: duplicate value 2025-08-21 15:54:30 +07:00
Methapon2001
ec780f2018 chore: clean 2025-08-21 15:54:30 +07:00
Methapon2001
31b4daf42b feat(biz-type): add create button on empty 2025-08-21 15:54:30 +07:00
Methapon2001
5fad663a6e feat(biz-type): add response type and response data 2025-08-21 15:54:30 +07:00
Methapon2001
d4a9be9236 feat: add create slot for business type select 2025-08-21 15:54:30 +07:00
puriphatt
e33191dcd4 fix: business type display 2025-08-21 15:54:30 +07:00
Thanaphon Frappet
fd28f36876 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-21 10:07:56 +07:00
Thanaphon Frappet
a05c1e7004 fix: i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-21 10:07:33 +07:00
Thanaphon Frappet
db2a094471 fix: name
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-21 10:04:23 +07:00
puriphatt
9aba48401a Merge remote-tracking branch 'origin/develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-20 17:52:48 +07:00
puriphatt
a3c51f5f52 fix: request list hide btn manage messenger condition
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-20 17:48:32 +07:00
Thanaphon Frappet
d44850a9ae Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-20 16:28:33 +07:00
Thanaphon Frappet
b86891c8c2 refactor: remove btn
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-20 15:43:55 +07:00
Methapon2001
473e272328 fix: error undefined
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-08-19 11:45:24 +07:00
Thanaphon Frappet
02d02cf3a1 refactor: handle btn
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-18 09:24:49 +07:00
Thanaphon Frappet
044a530b8d refactor: edit i18n 2025-08-18 09:24:11 +07:00
Thanaphon Frappet
b21949712b fix: handle show group name
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-08-14 11:07:25 +07:00
Thanaphon Frappet
42e545dd66 fix: value option no set 2025-08-14 11:07:01 +07:00
Methapon2001
263c703e69 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-08-04 09:01:06 +07:00
Methapon2001
968aa04aa9 fix: remark handling
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-25 16:19:36 +07:00
puriphatt
8ca3f784f1 fix: handle customer business type in table
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-21 09:53:31 +07:00
puriphatt
d60f858582 fix: quotation disable selectInstallment invoice btn when not selected
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-17 11:12:27 +07:00
Methapon2001
4ec3506e62 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-17 08:58:25 +07:00
puriphatt
f3342dfbda fix: i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-16 15:29:11 +07:00
puriphatt
9b56896695 fix: q-file visibility
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-16 15:25:47 +07:00
puriphatt
7d4b38369c fix: agency zipcode rules
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-16 15:00:36 +07:00
puriphatt
b977f86de9 feat: institution attachment
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-16 14:39:41 +07:00
Methapon2001
da52bfbcbd feat: remove employer prefix from string remark
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-16 13:36:12 +07:00
puriphatt
cdb38e301e feat: agency personnel foreign address
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-16 12:55:51 +07:00
puriphatt
7f56a6219a fix: edit debit note with new employee
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-15 14:31:53 +07:00
puriphatt
642dec8de9 refactor: debit note pay type
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-15 13:57:26 +07:00
Thanaphon Frappet
1360aca7e9 refactor: set label
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-15 13:53:45 +07:00
Thanaphon Frappet
dbca22f639 fix: show name anmd name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-15 13:44:30 +07:00
Thanaphon Frappet
b5abf693c2 fix: name
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-15 13:42:29 +07:00
puriphatt
8ef2ca2e96 fix: fix quotation customer branch id when create new employee
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-15 11:57:15 +07:00
puriphatt
ad715b20a2 fix: document view customer name new line
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-14 13:35:57 +07:00
puriphatt
915ce6f70b fix: pay condition type
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-14 11:40:43 +07:00
puriphatt
12b49a2a07 fix: quotation invoice btn handle
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-14 10:53:55 +07:00
puriphatt
40e6d1ba1c refactor: view document customer name
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-14 09:57:01 +07:00
puriphatt
22e11cf699 fix/chore: agencies indexDeleteQrCodeBank undefined handle / remove log
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 13:54:28 +07:00
puriphatt
af1f74bdda fix: business type icon and color
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 11:16:00 +07:00
Thanaphon Frappet
ed55d07e38 refactor: remove can access
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 11:11:20 +07:00
puriphatt
ac50ef1c7c fix: folder name
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 11:10:09 +07:00
Thanaphon Frappet
d4f021d0e6 refactor: add page size
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 11:09:42 +07:00
Methapon2001
3ddea74b73 fix: card business type not show
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-11 11:05:55 +07:00
puriphatt
2ac31c2e4c fix: can't create employee with img
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 11:01:38 +07:00
Thanaphon Frappet
e3f86136e7 refactor: bind crud business type
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 10:49:01 +07:00
Methapon2001
577da39cf0 feat: business type is now from api
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 10:45:35 +07:00
puriphatt
30d2126161 fix: customer label and i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-11 10:44:04 +07:00
puriphatt
b86c0a1e7a Merge branch 'fix/package-behavior' into develop
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 10:37:40 +07:00
puriphatt
d59642bcb3 refactor: package behavior 2025-07-11 10:37:10 +07:00
Methapon2001
0e5378455b fix: wrong lang
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 10:17:18 +07:00
puriphatt
0c59ef89ca fix: i18n จำนวนที่เข้า => จำนวนวันที่เข้า
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-11 09:45:32 +07:00
puriphatt
052722eb14 fix: product service form id 2025-07-11 09:30:34 +07:00
Thanaphon Frappet
db7f96fc02 refactor: remove user
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 09:19:49 +07:00
Thanaphon Frappet
04765a656a refactor: add visa report date
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-11 09:12:55 +07:00
Thanaphon Frappet
c3989768ed feat: add crud business type
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-07-11 09:12:17 +07:00
Thanaphon Frappet
e19d3f05f1 refactor: remove customer name
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 15:06:30 +07:00
puriphatt
fe2860d818 fix: can access cancel request list
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 15:04:18 +07:00
puriphatt
25216de820 fix: can access task order & group i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-09 14:35:38 +07:00
Methapon2001
76cfb5fcec fix: missing prefix in dialog header
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 14:24:15 +07:00
Methapon2001
7d1a32efb4 feat: adjust prefix display
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 14:20:08 +07:00
puriphatt
9999a49fa0 fix: can access quotation
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 14:19:32 +07:00
Methapon2001
6b55701afb feat: make prefix uppercase
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-09 13:52:44 +07:00
Methapon2001
fc7a94d7a2 feat: make prefix uppercase
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 13:38:01 +07:00
Methapon2001
2ba4758e50 fix: typos 2025-07-09 13:37:30 +07:00
puriphatt
5bed71053e fix: can access task order kebab
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 13:36:02 +07:00
puriphatt
bc53399153 fix: update access control checks for delete actions in TableTaskOrder component
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 13:33:57 +07:00
puriphatt
059c6d3afc feat: update task order permissions to include create access and adjust edit logic
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 13:17:06 +07:00
puriphatt
6117867aba feat: ensure contact fields are initialized to empty strings in agencies management form
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-09 11:24:09 +07:00
puriphatt
7d425332c3 feat: agencies hide kebab
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 11:06:14 +07:00
puriphatt
9539adee36 feat: enhance email validation and improve name display logic in personnel management forms
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-09 10:55:02 +07:00
puriphatt
14487ed849 feat: refactor delete button rendering logic in MainPage component
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-09 10:33:56 +07:00
puriphatt
4a195494d6 feat: add create permission handling in TreeComponent and MainPage
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-09 10:13:56 +07:00
puriphatt
5e155cfb0c feat: sanitize service detail display in TableProductAndService component
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-08 15:31:41 +07:00
puriphatt
78a59e277f feat: sanitize HTML content in MainPage detail display
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-08 15:04:41 +07:00
puriphatt
844cf176df feat: update hide-action logic in request list
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-07-08 14:24:48 +07:00
puriphatt
a89de83fe9 feat: conditionally display price rows in PriceDataComponent based on availability
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-08 13:39:53 +07:00
puriphatt
2b310c667d feat: add support for debit notes in credit note forms and related components
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-08 13:28:27 +07:00
puriphatt
7679c076a7 feat: add disabled submit functionality to OcrDialog and enhance request list handling
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-08 11:39:57 +07:00
puriphatt
088f829146 feat: add access control to group management route
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-08 09:18:33 +07:00
puriphatt
b9f1d04105 feat: enhance employee form to support image uploads and track image edits
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-07 17:54:03 +07:00
puriphatt
34f5b6474b fix: update status handling in debit note list retrieval and remove unused issued stats calculation
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-07 16:22:21 +07:00
puriphatt
84591ae719 fix: correct iframe source URL format in group management page
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-07 16:17:54 +07:00
puriphatt
8b2e3f76c4 feat: add group management page and update translations
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-07 15:29:18 +07:00
puriphatt
1e34f18366 feat: add seller filtering and enhance quotation forms with sellerId
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-07 14:34:39 +07:00
puriphatt
f08c83c98b Merge branch 'feat/handle-role' into develop
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-07 12:44:10 +07:00
puriphatt
c481266654 refactor: enhance access control and visibility logic across various components
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-07 12:42:52 +07:00
puriphatt
a59e0c5157 fix: change status employee 2025-07-07 11:19:57 +07:00
puriphatt
a5791a1b54 refactor: add hideDelete prop to various components and update delete button visibility logic
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-07 10:40:35 +07:00
puriphatt
f646b3c9ba refactor: credit note role check
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-04 17:13:07 +07:00
puriphatt
9dcec6b4c6 refactor: streamline access control for task order actions in the table component
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-07-04 16:31:17 +07:00
Thanaphon Frappet
03adabeabd refactor: add btn uploand file passport
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-04 16:24:55 +07:00
puriphatt
942449e373 refactor: enhance access control for task order editing and employee name display
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-04 16:20:44 +07:00
Thanaphon Frappet
dd09a8cb23 fix: uploand error
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-04 15:15:06 +07:00
puriphatt
33e040c21a refactor: update responsible user logic and permissions for task orders
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-04 15:06:44 +07:00
puriphatt
ca57f4790c refactor: update customer and quotation permissions to include additional roles
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-04 14:08:11 +07:00
puriphatt
e957672c91 refactor: update access control checks for customer view permissions 2025-07-04 14:08:04 +07:00
puriphatt
436bfa41bb refactor: update access control checks for quotation edit permissions
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-04 13:52:53 +07:00
puriphatt
d4cab27aaf refactor: add conditional delete button visibility in agencies management
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-04 13:17:52 +07:00
puriphatt
f286f6a16e refactor: add access control for add btn (workflow)
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-04 12:53:17 +07:00
puriphatt
f516212b6c fix: workflow hide dropdown icon on responsible person 2025-07-04 12:53:17 +07:00
puriphatt
011c65dcf8 fix: workflow hide dropdown icon on responsible person
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-04 11:00:27 +07:00
Thanaphon Frappet
3c4573192f refactor: update role task order can edit
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-03 16:19:33 +07:00
puriphatt
48a1800b54 refactor: request list role check
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-03 16:14:16 +07:00
Methapon2001
963ed11073 refactor: debit note role check
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-03 16:09:06 +07:00
puriphatt
8b933455d1 refactor: remove account permissions
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-03 15:57:41 +07:00
puriphatt
e9d995fa3e refactor: update role checks and access control in MainPage and QuotationFormProductSelect components
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-03 15:19:45 +07:00
puriphatt
2269038e11 refactor: quotation role check 2025-07-03 15:19:45 +07:00
Methapon2001
d880e1a1c5 refactor: move variable out of function
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 5s
2025-07-03 14:53:33 +07:00
puriphatt
5e76c4d50d refactor: agencies role check
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-03 14:10:32 +07:00
puriphatt
2d7b0189ee refactor: employee role check
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-03 13:54:10 +07:00
puriphatt
c991e9e03f refactor: implement role-based access control for customer edit actions in BranchPage and MainPage components
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-03 13:32:21 +07:00
puriphatt
aaa448fa0c fix: disable toggle button when in readonly mode in ProfileBanner component 2025-07-03 13:31:22 +07:00
puriphatt
58231aa936 feat: workflow role
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-03 10:07:00 +07:00
puriphatt
701b90d89a refactor: enhance role-based access checks in TreeComponent and MainPage components
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-07-02 15:53:20 +07:00
puriphatt
8799799214 refactor: replace role-based access checks with canAccess utility in menu components
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-07-02 14:58:44 +07:00
puriphatt
57660d7991 fix: make treeFile prop optional in UploadFile component 2025-07-02 14:07:52 +07:00
Thanaphon Frappet
9ab61d8ded refactor: set formtype view
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-06-30 13:56:44 +07:00
Thanaphon Frappet
09f368f516 fix: btn save hide
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-06-27 14:24:10 +07:00
puriphatt
d831cd0799 fix: update status label from Pending to Waiting in MainPage component
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-06-18 14:32:14 +07:00
Methapon2001
860b0b8f47 Merge branch 'fix/responsible-only-editable' into develop
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-06-17 11:58:35 +07:00
Methapon2001
41d02273ee fix: responsible only edit only
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-06-17 11:55:00 +07:00
Methapon2001
19ee1040d4 fix: validation error when mount
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-06-13 16:29:13 +07:00
Methapon2001
5c01882a34 fix: validation not reset
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-06-13 16:06:19 +07:00
Methapon2001
5edff6a5a8 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-06-13 15:08:07 +07:00
Methapon2001
1b475933da fix: incorrect condition
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-06-13 15:08:03 +07:00
Thanaphon Frappet
40942fc420 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-06-11 10:24:48 +07:00
Thanaphon Frappet
060b5980dd fix: can't change branch
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 10s
2025-06-11 10:00:08 +07:00
Methapon2001
04c47b2700 fix: quotation created at time not display correctly
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-05-30 09:41:19 +07:00
Methapon2001
df9430af24 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-05-28 14:24:56 +07:00
Methapon2001
ccf1a55052 fix: h4 size
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-05-28 14:24:42 +07:00
puriphatt
a74c4648c6 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-05-23 17:27:01 +07:00
puriphatt
62ac7503a1 fix: update tab management and reset logic in ProfileBanner and MainPage components
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-05-23 17:26:40 +07:00
puriphatt
3f15ce3ca6 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-05-23 11:41:09 +07:00
puriphatt
0916ce7af2 fix: enhance validation rules for agency and employer name fields
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-05-23 11:40:22 +07:00
puriphatt
105e91a655 Merge commit 'de281ea79f' 2025-05-22 17:41:55 +07:00
puriphatt
de281ea79f fix: improve work name management and validation logic
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-05-22 16:35:58 +07:00
Methapon2001
cd86e7718a fix: id
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-05-22 11:52:33 +07:00
puriphatt
0f252b3080 fix: update label from 'คำนำหน้า' to 'Prefix' in forms (english)
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-05-20 11:48:05 +07:00
Methapon2001
80c8a0d8b4 fix: form not clear 2025-05-20 11:48:05 +07:00
puriphatt
0150f80ba2 fix: update label from 'คำนำหน้า' to 'Prefix' in forms (english)
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-05-20 11:43:07 +07:00
Methapon2001
1efa90816a fix: form not clear
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-05-19 16:00:21 +07:00
Thanaphon Frappet
543c28e162 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 12s
2025-05-02 10:25:03 +07:00
Methapon2001
bcd54813d1 fix: select multiple when remove
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-30 17:03:07 +07:00
puriphatt
2b78abcd3b fix: adjust layout classes for responsive design on ViewPage
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-30 16:58:40 +07:00
puriphatt
4d023a7c7c feat: add smooth scrolling on validation error for forms
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-30 16:15:08 +07:00
puriphatt
70e9755952 fix: ensure contactName and contactTel default to empty string if undefined
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-30 15:34:30 +07:00
puriphatt
9a57056ffa refactor: remove unused workerList and reset selectedWorker on branch change
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-30 15:11:12 +07:00
Methapon2001
8354b6b40a Merge branch 'develop' 2025-04-30 12:42:24 +07:00
puriphatt
4d8eebdd04 feat: enable responsible user filtering and enhance button states for improved UX
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 10s
2025-04-30 12:04:18 +07:00
Methapon2001
5ec924fe14 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-30 12:02:12 +07:00
Methapon2001
0871a8b899 feat: only allow current responsible user and group 2025-04-30 12:02:05 +07:00
puriphatt
7a2be56ef4 refactor: improve checkbox and badge behavior based on notification state
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-30 11:13:45 +07:00
puriphatt
c4c4b76973 refactor: adjust button sizes and padding for improved layout consistency
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-30 10:23:47 +07:00
Methapon2001
2192041e35 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-29 13:08:13 +07:00
Methapon2001
c1ffbef565 feat: auto go into first entry if only one
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-29 13:08:02 +07:00
Thanaphon Frappet
46de2412df refactor: handle role
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-29 11:22:10 +07:00
167 changed files with 11475 additions and 7573 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before After
Before After

View file

@ -183,15 +183,15 @@
"prefix": [
{
"label": "Mr",
"label": "MR",
"value": "mr"
},
{
"label": "Mrs",
"label": "MRS",
"value": "mrs"
},
{
"label": "Miss",
"label": "MISS",
"value": "miss"
}
],

View file

@ -89,15 +89,7 @@ defineProps<{
</div>
</div>
</div>
<div
style="
display: block;
width: 100%;
height: 1px;
background: hsla(0 0% 0% / 0.1);
margin-bottom: var(--size-2);
"
/>
<q-separator />
<slot name="data"></slot>
<template v-if="!$slots.data">
<div

View file

@ -159,42 +159,6 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
]"
for="input-name-en"
/>
<q-select
v-if="
typeBranch !== 'headOffice' &&
isRoleInclude(['head_of_admin', 'head_of_account'])
"
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
class="col-2"
dense
for="input-branch-status"
:readonly="readonly || isRoleInclude(['head_of_account'])"
:options="['Virtual', 'Branch']"
:hide-dropdown-icon="readonly"
:label="$t('general.branchStatus')"
:model-value="virtual ? 'Virtual' : 'Branch'"
@update:model-value="(v) => (virtual = v === 'Virtual')"
:rules="[(val) => val && val.length > 0]"
:error-message="$t('form.error.required')"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 row q-col-gutter-sm">

View file

@ -262,7 +262,7 @@ function deleteFile(name: string) {
</div>
<q-file
v-if="userType"
v-if="userType && !readonly"
ref="attachmentRef"
for="input-attachment"
:dense="dense"

View file

@ -165,7 +165,7 @@ watch(
:id="`${prefixId}-select-prefix-name-en`"
:for="`${prefixId}-select-prefix-name-en`"
:rules="[(val: string) => !!val || $t('form.error.required')]"
:label="$t('personnel.form.prefixName')"
label="Prefix"
class="col-md-1 col-6"
v-model="prefixName"
/>
@ -253,16 +253,13 @@ watch(
hide-bottom-space
:readonly="readonly"
:label="$t('form.email')"
:rules="
readonly
? undefined
: [
(v: string) =>
!v ||
/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g.test(v) ||
$t('form.error.invalid'),
]
"
:rules="[
(val) => (val && val.length > 0) || $t('form.error.required'),
(v: string) =>
!v ||
/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g.test(v) ||
$t('form.error.invalid'),
]"
class="col-md-3 col-6"
:model-value="readonly ? email || '-' : email"
@update:model-value="(v) => (typeof v === 'string' ? (email = v) : '')"
@ -296,15 +293,11 @@ watch(
:readonly="readonly"
:label="$t('form.birthDate')"
:disabled-dates="disabledAfterToday"
:rules="
employee
? []
: [
(val: string) =>
!!val ||
$t('form.error.selectField', { field: $t('form.birthDate') }),
]
"
:rules="[
(val: string) =>
!!val ||
$t('form.error.selectField', { field: $t('form.birthDate') }),
]"
/>
<q-input

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -268,6 +268,7 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
<div class="col-md col-6">
<DatePicker
:label="$t('customerEmployee.formHealthCheck.coverageStartDate')"
v-model="checkup.coverageStartDate"
:id="`${prefixId}-input-coverage-start-date`"
:readonly="readonly || checkup.statusSave"

View file

@ -9,6 +9,8 @@ import useOptionStore from 'stores/options';
import DatePicker from '../shared/DatePicker.vue';
import { dateFormat } from 'src/utils/datetime';
const optionStore = useOptionStore();
const { locale } = useI18n();
@ -18,8 +20,8 @@ const issuePlace = defineModel<string>('issuePlace');
const issueCountry = defineModel<string>('issueCountry');
const issueDate = defineModel<Date | null | string>('issueDate');
const type = defineModel<string>('type');
const expireDate = defineModel<Date>('expireDate');
const birthDate = defineModel<Date>('birthDate');
const expireDate = defineModel<Date | string>('expireDate');
const birthDate = defineModel<Date | string>('birthDate');
const workerStatus = defineModel<string>('workerStatus');
const nationality = defineModel<string>('nationality');
const gender = defineModel<string>('gender');
@ -32,6 +34,8 @@ const firstName = defineModel<string>('firstName');
const namePrefix = defineModel<string>('namePrefix');
const passportNumber = defineModel<string>('passportNumber');
const file = defineModel<File>('file');
const passportValidator = /[a-zA-Z]{1}[a-zA-Z0-9]{1}[0-9]{5,7}$/;
const genderOptions = ref<Record<string, unknown>[]>([]);
@ -177,6 +181,30 @@ watch(
},
);
function browse() {
inputFile?.click();
}
const inputFile = (() => {
const _element = document.createElement('input');
_element.type = 'file';
_element.accept = 'image/jpeg,image/png';
_element.addEventListener('change', change);
return _element;
})();
async function change(e: Event) {
const _element = e.target as HTMLInputElement | null;
const _file = _element?.files?.[0];
if (_file) {
const newFileName = `passport-${dateFormat(new Date().toISOString())}-${_file.name}`;
const renamedFile = new File([_file], newFileName, { type: _file.type });
file.value = renamedFile;
}
}
watch(
() => namePrefix.value,
(v) => {
@ -221,20 +249,14 @@ watch(
</div>
<div class="q-col-gutter-sm" :class="{ row: $q.screen.gt.sm }">
<div
class="col row justify-center q-col-gutter-sml"
style="max-height: 50%"
v-if="!ocr"
>
<q-avatar
style="border: 1px dashed; border-color: black"
square
size="100px"
font-size="50px"
color="grey-4"
text-color="grey"
icon="mdi-image-outline"
/>
<div v-if="!ocr">
<q-btn
flat
color="primary"
icon="mdi-upload-box-outline"
@click="() => browse()"
:disable="readonly"
></q-btn>
</div>
<div
class="row q-col-gutter-sm"

View file

@ -28,20 +28,22 @@ const arrivalAt = defineModel<string>('arrivalAt');
const arrivalTMNo = defineModel<string>('arrivalTmNo');
const arrivalTM = defineModel<string>('arrivalTm');
const mrz = defineModel<string>('mrz');
const entryCount = defineModel<number>('entryCount');
const entryCount = defineModel<number | string>('entryCount');
const issuePlace = defineModel<string>('issuePlace');
const issueCountry = defineModel<string>('issueCountry');
const issueDate = defineModel<Date | null | string>('visaIssueDate');
const type = defineModel<string>('type');
const expireDate = defineModel<Date>('expireDate');
const expireDate = defineModel<Date | string>('expireDate');
const remark = defineModel<string>('remark');
const workerType = defineModel<string>('workerType');
const number = defineModel<string>('number');
const reportDate = defineModel<Date | null | string>('reportDate');
const calculatedVisaDate = computed(() => {
if (!issueDate.value) return undefined;
return calculate90DayNext(issueDate.value);
});
//
// const calculatedVisaDate = computed(() => {
// if (!issueDate.value) return undefined;
// return calculate90DayNext(issueDate.value);
// });
defineProps<{
title?: string;
@ -138,6 +140,10 @@ watch(
);
},
);
//
// watch([() => issueDate.value], () => {
// reportDate.value = calculate90DayNext(issueDate.value);
// });
</script>
<template>
@ -151,7 +157,7 @@ watch(
name="mdi-passport"
style="background-color: var(--surface-3)"
/>
{{ title }}
{{ $t(title) }}
</div>
<div
@ -371,10 +377,12 @@ watch(
<DatePicker
:id="`${prefixId}-date-picker-visa-issuance`"
:readonly
:disabled="!readonly"
:label="$t('customerEmployee.form.visa90Day')"
:model-value="calculatedVisaDate"
v-model="reportDate"
clearable
:rules="[
(val) => (val && val.length > 0) || $t('form.error.required'),
]"
/>
</div>
</div>

View file

@ -22,6 +22,8 @@ const prop = withDefaults(
inTable?: boolean;
addButton?: boolean;
prefixId?: string;
hideAction?: boolean;
hideDelete?: boolean;
}>(),
{
gridView: false,
@ -139,8 +141,9 @@ defineEmits<{
<q-avatar size="md">
<q-img
:src="
`${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}` ||
`/images/employee-avatar-${props.row.gender}.png`
props.row.selectedImage
? `${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}`
: `/images/employee-avatar-${props.row.gender}.png`
"
class="text-center"
:ratio="1"
@ -265,9 +268,10 @@ defineEmits<{
@click.stop="$emit('view', props.row)"
/>
<KebabAction
v-if="!inTable"
v-if="!inTable && !hideAction"
:id-name="props.row.firstName"
:status="props.row.status"
:hide-delete="hideDelete"
@view="$emit('view', props.row)"
@edit="$emit('edit', props.row)"
@delete="$emit('delete', props.row)"
@ -280,9 +284,11 @@ defineEmits<{
<template v-slot:item="props">
<div class="col-12 col-md-3 col-sm-6">
<PersonCard
history
:hide-delete="hideDelete"
:hide-action="hideAction"
:id="`card-${props.row.firstNameEN}`"
:field-selected="fieldSelected"
history
:prefix-id="props.row.firstNameEN ?? props.rowIndex"
:data="{
code: props.row.code,
@ -290,9 +296,9 @@ defineEmits<{
$i18n.locale === 'eng'
? `${props.row.firstNameEN} ${props.row.lastNameEN} `.trim()
: `${props.row.firstName} ${props.row.lastName} `.trim(),
img:
`${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}` ||
`/images/employee-avatar-${props.row.gender}.png`,
img: props.row.selectedImage
? `${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}`
: `/images/employee-avatar-${props.row.gender}.png`,
fallbackImg: `/images/employee-avatar-${props.row.gender}.png`,
male: props.row.gender === 'male',
female: props.row.gender === 'female',

View file

@ -54,6 +54,7 @@ defineProps<{
employeeOwnerOption?: CustomerBranch[];
prefixId: string;
showBtnSave?: boolean;
disableCustomerSelect?: boolean;
}>();
defineEmits<{
@ -117,12 +118,16 @@ defineEmits<{
<div class="col-12 row" style="gap: var(--size-2)">
<SelectCustomer
id="form-select-customer-branch-id"
for="form-select-customer-branch-id"
v-model:value="customerBranchId"
v-model:value-option="currentCustomerBranch"
:label="$t('customer.form.branchCode')"
class="col-12 field-two"
simple
required
:readonly
:disabled="disableCustomerSelect && !readonly"
/>
<q-input

View file

@ -27,6 +27,7 @@ import { QField } from 'quasar';
defineProps<{
readonly?: boolean;
onDrawer?: boolean;
hideAction?: boolean;
}>();
const { t } = useI18n();
@ -201,6 +202,7 @@ onMounted(async () => {
:class="{ 'q-ml-lg': $q.screen.gt.xs, 'q-mt-sm': $q.screen.lt.sm }"
>
<ToggleButton
:disable="hideAction"
class="q-mr-sm"
two-way
:model-value="flowData.status !== 'INACTIVE'"
@ -609,7 +611,7 @@ onMounted(async () => {
</span>
</q-item>
</template>
<template #append>
<template v-if="!readonly" #append>
<q-icon
name="mdi-menu-down"
:class="{ rotated: responsibleMenu }"

View file

@ -167,26 +167,28 @@ withDefaults(
<div class="col-12 full-width">
<q-table
:rows-per-page-options="[0]"
:rows="[
{
label: $t('productService.product.salePrice'),
pricePerUnit: price,
calcVat,
vatIncluded,
},
{
label: $t('productService.product.agentPrice'),
calcVat: agentPriceCalcVat,
vatIncluded: agentPriceVatIncluded,
pricePerUnit: agentPrice,
},
{
label: $t('productService.product.processingPrice'),
calcVat: serviceChargeCalcVat,
vatIncluded: serviceChargeVatIncluded,
pricePerUnit: serviceCharge,
},
]"
:rows="
[
priceDisplay.price && {
label: $t('productService.product.salePrice'),
pricePerUnit: price,
calcVat,
vatIncluded,
},
priceDisplay.agentPrice && {
label: $t('productService.product.agentPrice'),
calcVat: agentPriceCalcVat,
vatIncluded: agentPriceVatIncluded,
pricePerUnit: agentPrice,
},
priceDisplay.serviceCharge && {
label: $t('productService.product.processingPrice'),
calcVat: serviceChargeCalcVat,
vatIncluded: serviceChargeVatIncluded,
pricePerUnit: serviceCharge,
},
].filter(Boolean)
"
:columns
hide-bottom
bordered

View file

@ -98,7 +98,8 @@ watch(
(c, o) => {
const list = c.map((v: { name: string }) => v.name);
const oldList = o.map((v: { name: string }) => v.name);
const index = oldList.indexOf(workName.value || '');
const index = workName.value ? oldList.indexOf(workName.value) : -1;
if (index === -1) return;
if (
list[index] !== oldList[index] &&
@ -704,8 +705,8 @@ watch(
}
:deep(
.q-item__section.column.q-item__section--side.justify-center.q-item__section--avatar.q-focusable.relative-position.cursor-pointer
) {
.q-item__section.column.q-item__section--side.justify-center.q-item__section--avatar.q-focusable.relative-position.cursor-pointer
) {
justify-content: start !important;
padding-right: 8px !important;
padding-top: 16px;
@ -735,8 +736,8 @@ watch(
}
:deep(
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
color: var(--brand-1);
}

View file

@ -1,7 +1,6 @@
<script lang="ts" setup>
import { nextTick, onMounted, ref, watch } from 'vue';
import { DeleteButton, EditButton, SaveButton, UndoButton } from '../button';
import { scrollToElement } from 'stores/utils';
// import { useI18n } from 'vue-i18n';
// import { storeToRefs } from 'pinia';
@ -138,11 +137,8 @@ watch(
@click="
() => {
assignClone();
if (nameList[nameList.length - 1].name === '') {
$emit('delete', cloneList[cloneList.length - 1].id, true);
cloneList = cloneList.filter((item) => item.name !== '');
nameList = nameList.filter((item) => item.name !== '');
}
cloneList = cloneList.filter((item) => item.name !== '');
nameList = nameList.filter((item) => item.name !== '');
}
"
/>

View file

@ -48,6 +48,7 @@ defineProps<{
readonly?: boolean;
onDrawer?: boolean;
inputOnly?: boolean;
disableToggle?: boolean;
}>();
defineEmits<{
@ -76,6 +77,7 @@ defineEmits<{
<ToggleButton
class="q-mr-sm"
two-way
:disable="disableToggle"
:model-value="status !== 'INACTIVE'"
@click="
() => {
@ -195,8 +197,8 @@ defineEmits<{
}
:deep(
.q-item__section.column.q-item__section--side.justify-center.q-item__section--avatar.q-focusable.relative-position.cursor-pointer
) {
.q-item__section.column.q-item__section--side.justify-center.q-item__section--avatar.q-focusable.relative-position.cursor-pointer
) {
justify-content: start !important;
padding-right: 8px !important;
padding-top: 16px;
@ -208,15 +210,15 @@ defineEmits<{
}
:deep(
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
color: var(--brand-1);
}
:deep(
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.expansion-rounded.surface-2
.q-focus-helper
) {
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.expansion-rounded.surface-2
.q-focus-helper
) {
visibility: hidden;
}

View file

@ -2,11 +2,18 @@
import SelectCustomer from '../shared/select/SelectCustomer.vue';
import SelectBranch from '../shared/select/SelectBranch.vue';
import { CustomerBranch } from 'src/stores/customer';
import { ref } from 'vue';
const branchId = defineModel<string>('branchId');
const customerBranchId = defineModel<string>('customerBranchId');
const agentPrice = defineModel<boolean>('agentPrice');
const special = defineModel<boolean>('special');
const customerBranchOption = defineModel<CustomerBranch>(
'customerBranchOption',
);
defineProps<{
outlined?: boolean;
readonly?: boolean;
@ -67,8 +74,12 @@ defineEmits<{
required
:readonly
/>
<SelectCustomer
id="about-select-customer-branch-id"
for="about-select-customer-branch-id"
v-model:value="customerBranchId"
v-model:value-option="customerBranchOption"
:label="$t('quotation.customer')"
:creatable-disabled-text="`(${$t('form.error.selectField', {
field: $t('quotation.branchVirtual'),
@ -88,14 +99,14 @@ defineEmits<{
<style scoped>
:deep(
label.q-field.row.no-wrap.items-start.q-field--outlined.q-select.field-one
) {
label.q-field.row.no-wrap.items-start.q-field--outlined.q-select.field-one
) {
padding-right: 4px;
}
:deep(
label.q-field.row.no-wrap.items-start.q-field--outlined.q-select.field-two
) {
label.q-field.row.no-wrap.items-start.q-field--outlined.q-select.field-two
) {
padding-left: 4px;
}
</style>

View file

@ -58,16 +58,15 @@ const currentBtnOpen = ref<{ title: string; opened: boolean[] }[]>([
function calcPrice(c: (typeof rows.value)[number]) {
const originalPrice = c.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice + originalPrice * (config.value?.vat || 0.07),
const finalPricePerUnit = precisionRound(
originalPrice +
(c.product[props.agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? originalPrice * (config.value?.vat || 0.07)
: 0),
);
const finalPriceNoVat = finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPricePerUnit * c.amount - c.discount;
const price = finalPriceNoVat * c.amount - c.discount;
const vat = c.product[props.agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? (finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07)
: 0;
return precisionRound(price + vat);
return precisionRound(price);
}
const discount4Show = ref<string[]>([]);
@ -435,8 +434,20 @@ watch(
<q-td align="right">
{{
formatNumberDecimal(
props.row.pricePerUnit * props.row.amount -
props.row.discount,
props.row.product[
agentPrice ? 'agentPriceCalcVat' : 'calcVat'
]
? precisionRound(
(props.row.pricePerUnit *
(1 + (config?.vat || 0.07)) *
props.row.amount -
props.row.discount) /
(1 + (config?.vat || 0.07)),
)
: precisionRound(
props.row.pricePerUnit * props.row.amount -
props.row.discount,
),
2,
)
}}
@ -448,9 +459,12 @@ watch(
agentPrice ? 'agentPriceCalcVat' : 'calcVat'
]
? precisionRound(
(props.row.pricePerUnit * props.row.amount -
props.row.discount) *
(config?.vat || 0.07),
((props.row.pricePerUnit *
(1 + (config?.vat || 0.07)) *
props.row.amount -
props.row.discount) /
(1 + (config?.vat || 0.07))) *
0.07,
)
: 0,
2,

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { QTableProps } from 'quasar';
import { dateFormat } from 'src/utils/datetime';
import { dateFormat, dateFormatJS } from 'src/utils/datetime';
import { formatNumberDecimal } from 'stores/utils';
@ -19,6 +19,8 @@ const props = withDefaults(
page?: number;
pageSize?: number;
hideBtnPreview?: boolean;
hideAction?: boolean;
hideDelete?: boolean;
}>(),
{
row: () => [],
@ -84,11 +86,11 @@ defineEmits<{
</q-td>
<q-td v-if="visibleColumns.includes('createdAt')">
{{ dateFormat(props.row.createdAt) }}
{{ dateFormatJS({ date: props.row.createdAt }) }}
</q-td>
<q-td v-if="visibleColumns.includes('dueDate')">
{{ dateFormat(props.row.dueDate) }}
{{ dateFormatJS({ date: props.row.dueDate }) }}
</q-td>
<q-td v-if="visibleColumns.includes('contactName')">
@ -147,12 +149,12 @@ defineEmits<{
flat
@click.stop="$emit('view', props.row)"
/>
<KebabAction
v-if="!hideAction"
:idName="`btn-kebab-${props.row.workName}`"
status="'ACTIVE'"
hide-toggle
hide-delete
:hide-delete
:hide-edit="hideEdit"
@view="$emit('view', props.row)"
@edit="$emit('edit', props.row)"

View file

@ -1,6 +1,10 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import SelectInput from '../shared/SelectInput.vue';
import useOptionStore from 'src/stores/options';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { useInstitution } from 'src/stores/institution';
const optionStore = useOptionStore();
@ -10,6 +14,11 @@ defineProps<{
readonly?: boolean;
onDrawer?: boolean;
}>();
const emit = defineEmits<{
(e: 'deleteAttachment', name: string): void;
}>();
const attachmentRef = ref();
const group = defineModel('group', { default: '' });
const name = defineModel('name', { default: '' });
@ -17,8 +26,19 @@ const nameEn = defineModel('nameEn', { default: '' });
const contactName = defineModel('contactName', { default: '' });
const email = defineModel('email', { default: '' });
const contactTel = defineModel('contactTel', { default: '' });
const attachment = defineModel<File[]>('attachment');
const attachmentList =
defineModel<{ name: string; url: string }[]>('attachmentList');
type Options = { label: string; value: string };
function openNewTab(url: string) {
window.open(url, '_blank');
}
function deleteAttachment(name: string) {
emit('deleteAttachment', name);
}
</script>
<template>
<div class="row col-12">
@ -68,7 +88,12 @@ type Options = { label: string; value: string };
class="col-md-4 col-12"
:label="$t('agencies.name')"
v-model="name"
:rules="[(val: string) => !!val || $t('form.error.required')]"
:rules="[
(val) => !!val || $t('form.error.required'),
(val) =>
/^[A-Za-z0-9ก-๙\s&.,'-]+$/.test(val) ||
$t('form.error.branchNameField'),
]"
/>
<q-input
for="input-agencies-name-en"
@ -79,6 +104,15 @@ type Options = { label: string; value: string };
class="col-md-4 col-12"
:label="'Agencies Name'"
v-model="nameEn"
:rules="
nameEn
? [
(val) =>
/^[A-Za-z0-9ก-๙\s&.,'-]+$/.test(val) ||
$t('form.error.branchNameENField'),
]
: []
"
/>
<q-input
@ -148,6 +182,78 @@ type Options = { label: string; value: string };
/>
</template>
</q-input>
<q-file
v-if="!readonly"
ref="attachmentRef"
for="input-attachment"
dense
outlined
multiple
append
:readonly
:label="$t('personnel.form.attachment')"
class="col"
v-model="attachment"
>
<template v-slot:prepend>
<Icon
icon="material-symbols:attach-file"
width="20px"
style="color: var(--brand-1)"
/>
</template>
<template v-slot:file="file">
<div class="row full-width items-center">
<span class="col ellipsis">
{{ file.file.name }}
</span>
<q-btn
dense
rounded
flat
padding="2 2"
class="app-text-muted"
icon="mdi-close-circle"
@click.stop="attachmentRef.removeAtIndex(file.index)"
/>
</div>
</template>
</q-file>
<div v-if="attachmentList && attachmentList?.length > 0" class="col-12">
<q-list bordered separator class="rounded" style="padding: 0">
<q-item
id="attachment-file"
for="attachment-file"
v-for="item in attachmentList"
clickable
:key="item.url"
class="items-center row"
@click="() => openNewTab(item.url)"
>
<q-item-section>
<div class="row items-center justify-between">
<div class="col">
{{ item.name }}
</div>
<q-btn
v-if="!readonly"
id="delete-file"
rounded
flat
dense
unelevated
size="md"
icon="mdi-trash-can-outline"
class="app-text-negative"
@click.stop="deleteAttachment(item.name)"
/>
</div>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</div>
</template>

View file

@ -27,26 +27,38 @@ withDefaults(
class="app-text-muted q-pr-sm"
:width="iconSize || '2rem'"
/>
<span class="row col">
<span class="col-12 app-text-muted-2" style="font-size: 10px">
<span :id="`dd-wrapper-${label}`" class="row col">
<span
:id="`dd-label-${label}`"
class="col-12 app-text-muted-2"
style="font-size: 10px"
>
{{ label }}
</span>
<span class="col-12 ellipsis">
<span :id="`dd-value-wrapper-${label}`" class="col-12 ellipsis">
<slot name="value">
<span
:class="{ 'link cursor-pointer': clickable }"
v-if="typeof value === 'string'"
@click="$emit('labelClick', value, null)"
@click="clickable ? $emit('labelClick', value, null) : undefined"
:id="`link-${value}`"
:for="`link-${value}`"
>
{{ value }}
<q-tooltip v-if="tooltip" :delay="500">{{ value }}</q-tooltip>
</span>
<span v-else :class="{ 'link cursor-pointer': clickable }">
<span
:id="`dd-value-${label}`"
v-else
:class="{ 'link cursor-pointer': clickable }"
>
<span
v-for="(item, index) in value"
:key="index"
@click="$emit('labelClick', item, index)"
class="link cursor-pointer"
:id="`link-${item}`"
:for="`link-${item}`"
>
{{ item }}
<span v-if="index < value.length - 1">,&nbsp;</span>

View file

@ -8,6 +8,10 @@ defineProps<{
const quotationId = defineModel<string>('quotationId', {
required: true,
});
const isDebitNote = defineModel<boolean>('isDebitNote', {
required: false,
default: false,
});
</script>
<template>
<div class="row col-12">
@ -37,6 +41,7 @@ const quotationId = defineModel<string>('quotationId', {
cancelIncludeDebitNote: true,
hasCancel: true,
}"
@selected="(v) => (isDebitNote = v.isDebitNote)"
/>
</section>
</div>

View file

@ -42,6 +42,15 @@ defineProps<{
const modal = defineModel('modal', { default: false });
const currentTab = defineModel<string>('currentTab');
async function onValidationError(ref: any) {
const el = ref.$el as Element;
el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
}
</script>
<template>
<q-dialog
@ -60,6 +69,7 @@ const currentTab = defineModel<string>('currentTab');
@submit.prevent
@validation-success="submit"
class="column full-height"
@validation-error="onValidationError"
>
<!-- header -->
<div

View file

@ -8,7 +8,7 @@ import {
UndoButton,
} from 'components/button';
withDefaults(
const props = withDefaults(
defineProps<{
title: string;
category?: string;
@ -42,10 +42,24 @@ const drawerOpen = defineModel<boolean>('drawerOpen', {
const myForm = ref();
function reset() {
if (props.beforeClose) {
drawerOpen.value = props.beforeClose
? props.beforeClose()
: !drawerOpen.value;
}
if (myForm.value) {
myForm.value.resetValidation();
}
}
async function onValidationError(ref: any) {
const el = ref.$el as Element;
el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
}
</script>
<template>
<q-drawer
@ -53,7 +67,6 @@ function reset() {
@show="show"
@before-hide="reset"
@hide="close"
@update:model-value="(v) => (drawerOpen = beforeClose ? beforeClose() : v)"
:width="$q.screen.gt.xs ? windowSize * 0.85 : windowSize"
v-model="drawerOpen"
behavior="mobile"
@ -66,6 +79,7 @@ function reset() {
greedy
@submit.prevent
@validation-success="submit"
@validation-error="onValidationError"
>
<div
class="column justify-between full-height"

View file

@ -46,6 +46,7 @@ defineProps<{
color="grey"
icon="mdi-close"
v-close-popup
@click="cancel"
/>
</div>

View file

@ -1,5 +1,12 @@
<script setup lang="ts">
const pageSize = defineModel<number>({ required: true });
withDefaults(
defineProps<{
fetchData?: (...args: unknown[]) => void;
}>(),
{},
);
</script>
<template>
@ -10,7 +17,12 @@ const pageSize = defineModel<number>({ required: true });
:key="v"
clickable
v-close-popup
@click="pageSize = v"
@click="
() => {
pageSize = v;
fetchData();
}
"
>
<q-item-section>
<q-item-label>{{ v }}</q-item-label>

View file

@ -229,6 +229,7 @@ const smallBanner = ref(false);
<ToggleButton
v-if="useToggle"
:disable="readonly"
two-way
:model-value="toggleStatus !== 'INACTIVE'"
@click="$emit('update:toggleStatus', toggleStatus)"
@ -265,6 +266,7 @@ const smallBanner = ref(false);
class="app-text-muted full-width"
align="left"
v-if="typeof tabsList === 'object'"
@update:model-value="(v) => $emit('update:currentTab', v)"
>
<q-tab
v-for="tab in tabsList"

View file

@ -1,7 +1,6 @@
<script setup lang="ts">
import { BranchWithChildren } from 'stores/branch/types';
import KebabAction from './shared/KebabAction.vue';
import { isRoleInclude } from 'stores/utils';
const nodes = defineModel<(any | BranchWithChildren)[]>('nodes', {
default: [],
@ -17,6 +16,7 @@ withDefaults(
labelKey?: string;
childrenKey: string;
action?: boolean;
hideCreate?: boolean;
}>(),
{
color: 'transparent',
@ -96,7 +96,9 @@ defineEmits<{
expandedTree[expandedTree.length - 1] === node.id,
}"
>
{{ node.name }}
{{
$i18n.locale === 'eng' ? node.nameEN || node.name : node.name
}}
</span>
<span class="app-text-muted text-caption ellipsis">
{{ node.code }}
@ -120,11 +122,7 @@ defineEmits<{
/>
<q-btn
v-if="
node.isHeadOffice &&
typeTree === 'branch' &&
isRoleInclude(['head_of_admin', 'admin', 'system'])
"
v-if="node.isHeadOffice && typeTree === 'branch' && !hideCreate"
:id="`create-sub-branch-btn-${node.name}`"
@click.stop="$emit('create', node)"
icon="mdi-file-plus-outline"

View file

@ -5,6 +5,7 @@ defineEmits<{
(e: 'click', v: MouseEvent): void;
}>();
defineProps<{
id?: string;
icon?: string;
color: string;
iconOnly?: boolean;
@ -18,6 +19,7 @@ defineProps<{
<template>
<button
:id="id"
@click="(e) => $emit('click', e)"
class="main-btn"
:class="{

View file

@ -10,6 +10,7 @@ defineProps<{
outlined?: boolean;
disabled?: boolean;
dark?: boolean;
color?: string;
label?: string;
icon?: string;
@ -23,7 +24,7 @@ defineProps<{
@click="(e) => $emit('click', e)"
v-bind="{ ...$props, ...$attrs }"
:icon="icon || 'mdi-content-save-outline'"
color="207 96% 32%"
:color="color || '207 96% 32%'"
:title="iconOnly ? $t('general.save') : undefined"
>
{{ label || $t('general.save') }}

View file

@ -23,6 +23,15 @@ function update(value: boolean) {
}
}
async function onValidationError(ref: any) {
const el = ref.$el as Element;
el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
}
const state = defineModel({ default: false });
</script>
@ -41,6 +50,7 @@ const state = defineModel({ default: false });
}"
>
<q-form
@validation-error="onValidationError"
@submit.prevent="(e) => $emit('submit', e)"
@reset="$emit('reset')"
greedy

View file

@ -12,7 +12,7 @@ import { formatAddress } from 'src/utils/address';
import useOptionStore from 'stores/options';
const optionStore = useOptionStore();
defineProps<{
const props = defineProps<{
title?: string;
addressTitle?: string;
addressTitleEN?: string;
@ -30,6 +30,7 @@ defineProps<{
useEmployment?: boolean;
useWorkPlace?: boolean;
useForeignAddress?: boolean;
}>();
const addressStore = useAddressStore();
@ -57,6 +58,25 @@ const subDistrictId = defineModel<string | null | undefined>('subDistrictId');
const zipCode = defineModel<string | null | undefined>('zipCode');
const sameWithEmployer = defineModel<boolean>('sameWithEmployer');
const provinceTextEN = defineModel<string | null | undefined>(
'provinceTextEn',
{
default: '',
},
);
const districtTextEN = defineModel<string | null | undefined>(
'districtTextEn',
{
default: '',
},
);
const subDistrictTextEN = defineModel<string | null | undefined>(
'subDistrictTextEn',
{
default: '',
},
);
const homeCode = defineModel<string | null | undefined>('homeCode');
const employmentOffice = defineModel<string | null | undefined>(
'employmentOffice',
@ -64,6 +84,7 @@ const employmentOffice = defineModel<string | null | undefined>(
const employmentOfficeEN = defineModel<string | null | undefined>(
'employmentOfficeEn',
);
const addressForeign = defineModel<boolean>('addressForeign');
const addrOptions = reactive<{
provinceOps: Province[];
@ -78,14 +99,18 @@ const addrOptions = reactive<{
const area = ref<Office[]>([]);
const fullAddress = computed(() => {
const province = provinceOptions.value.find((v) => v.id === provinceId.value);
const district = districtOptions.value.find((v) => v.id === districtId.value);
const sDistrict = subDistrictOptions.value.find(
(v) => v.id === subDistrictId.value,
);
const province = addressForeign.value
? { id: '1', name: provinceId.value }
: provinceOptions.value.find((v) => v.id === provinceId.value);
const district = addressForeign.value
? { id: '1', name: districtId.value }
: districtOptions.value.find((v) => v.id === districtId.value);
const sDistrict = addressForeign.value
? { id: '1', name: subDistrictId.value }
: subDistrictOptions.value.find((v) => v.id === subDistrictId.value);
if (province && district && sDistrict) {
const fullAddress = formatAddress({
if (province?.name && district?.name && sDistrict?.name) {
const fullAddressText = formatAddress({
address: address.value,
addressEN: addressEN.value,
moo: moo.value ? moo.value : '',
@ -97,21 +122,26 @@ const fullAddress = computed(() => {
province: province as unknown as Province,
district: district as unknown as District,
subDistrict: sDistrict as unknown as SubDistrict,
zipCode: addressForeign.value ? zipCode.value || ' ' : undefined,
});
return fullAddress;
return fullAddressText;
}
return '-';
});
const fullAddressEN = computed(() => {
const province = provinceOptions.value.find((v) => v.id === provinceId.value);
const district = districtOptions.value.find((v) => v.id === districtId.value);
const sDistrict = subDistrictOptions.value.find(
(v) => v.id === subDistrictId.value,
);
const province = addressForeign.value
? { nameEN: provinceTextEN.value }
: provinceOptions.value.find((v) => v.id === provinceId.value);
const district = addressForeign.value
? { nameEN: districtTextEN.value }
: districtOptions.value.find((v) => v.id === districtId.value);
const sDistrict = addressForeign.value
? { nameEN: subDistrictTextEN.value }
: subDistrictOptions.value.find((v) => v.id === subDistrictId.value);
if (province && district && sDistrict) {
const fullAddress = formatAddress({
if (province?.nameEN && district?.nameEN && sDistrict?.nameEN) {
const fullAddressText = formatAddress({
address: address.value,
addressEN: addressEN.value,
moo: moo.value ? moo.value : '',
@ -124,8 +154,9 @@ const fullAddressEN = computed(() => {
district: district as unknown as District,
subDistrict: sDistrict as unknown as SubDistrict,
en: true,
zipCode: addressForeign.value ? zipCode.value || ' ' : undefined,
});
return fullAddress;
return fullAddressText;
}
return '-';
});
@ -149,7 +180,7 @@ async function fetchProvince() {
}
async function fetchDistrict() {
if (!provinceId.value) return;
if (!provinceId.value || addressForeign.value) return;
const result = await addressStore.fetchDistrictByProvinceId(provinceId.value);
if (result) addrOptions.districtOps = result;
@ -168,7 +199,7 @@ async function fetchDistrict() {
}
async function fetchSubDistrict() {
if (!districtId.value) return;
if (!districtId.value || addressForeign.value) return;
const result = await addressStore.fetchSubDistrictByProvinceId(
districtId.value,
);
@ -255,6 +286,16 @@ onMounted(async () => {
await fetchSubDistrict();
});
function clearAddress() {
provinceId.value = null;
districtId.value = null;
subDistrictId.value = null;
provinceTextEN.value = null;
districtTextEN.value = null;
subDistrictTextEN.value = null;
zipCode.value = null;
}
watch(provinceId, fetchDistrict);
watch(districtId, fetchSubDistrict);
@ -313,6 +354,15 @@ watchEffect(async () => {
{{ $t('customerEmployee.form.addressCustom') }}
</span>
</div>
<div v-if="useForeignAddress" class="text-caption q-ml-md app-text-muted">
<q-checkbox
size="xs"
v-model="addressForeign"
@update:model-value="clearAddress"
/>
{{ $t('personnel.form.addressForeign') }}
</div>
</div>
<div class="col-12 row q-col-gutter-y-md">
@ -449,7 +499,24 @@ watchEffect(async () => {
(v) => (typeof v === 'string' ? (street = v) : '')
"
/>
<q-input
v-if="addressForeign"
outlined
hide-bottom-space
class="col-md-3 col-6"
v-model="provinceId"
:dense="dense"
:label="$t('form.province')"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-province-${indexId}` : 'input-province'}`"
:rules="
disabledRule
? []
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-select
v-else
autocomplete="off"
outlined
clearable
@ -493,7 +560,24 @@ watchEffect(async () => {
</template>
</q-select>
<q-input
v-if="addressForeign"
outlined
hide-bottom-space
class="col-md-3 col-6"
v-model="districtId"
:dense="dense"
:label="$t('form.district')"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-district-${indexId}` : 'input-district'}`"
:rules="
disabledRule
? []
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-select
v-else
autocomplete="off"
outlined
clearable
@ -536,7 +620,25 @@ watchEffect(async () => {
</q-item>
</template>
</q-select>
<q-input
v-if="addressForeign"
outlined
hide-bottom-space
class="col-md-3 col-6"
v-model="subDistrictId"
:dense="dense"
:label="$t('form.district')"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-sub-district-${indexId}` : 'input-sub-district'}`"
:rules="
disabledRule
? []
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-select
v-else
autocomplete="off"
outlined
clearable
@ -580,17 +682,27 @@ watchEffect(async () => {
</template>
</q-select>
<q-input
:key="Number(addressForeign)"
hide-bottom-space
:for="`${prefixId}-${indexId !== undefined ? `input-zip-code-${indexId}` : 'input-zip-code'}`"
:dense="dense"
outlined
:disable="!readonly && !sameWithEmployer"
readonly
:disable="!addressForeign && !readonly && !sameWithEmployer"
:readonly="!addressForeign || readonly"
:label="$t('form.zipCode')"
class="col-md-3 col-6"
:model-value="
addrOptions.subDistrictOps
?.filter((x) => x.id === subDistrictId)
.map((x) => x.zipCode)[0] ?? ''
!addressForeign
? (addrOptions.subDistrictOps
?.filter((x) => x.id === subDistrictId)
.map((x) => x.zipCode)[0] ?? '')
: zipCode
"
@update:model-value="(v) => (zipCode = v.toString())"
:rules="
!addressForeign
? []
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-input
@ -689,7 +801,24 @@ watchEffect(async () => {
(v) => (typeof v === 'string' ? (streetEN = v) : '')
"
/>
<q-input
v-if="addressForeign"
outlined
hide-bottom-space
class="col-md-3 col-6"
v-model="provinceTextEN"
:dense="dense"
label="Province"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-province-en-${indexId}` : 'input-province-en'}`"
:rules="
disabledRule
? []
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-select
v-else
autocomplete="off"
outlined
clearable
@ -732,7 +861,25 @@ watchEffect(async () => {
</q-item>
</template>
</q-select>
<q-input
v-if="addressForeign"
outlined
hide-bottom-space
class="col-md-3 col-6"
v-model="districtTextEN"
:dense="dense"
label="District"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-district-en-${indexId}` : 'input-district-en'}`"
:rules="
disabledRule
? []
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-select
v-else
autocomplete="off"
outlined
clearable
@ -775,7 +922,25 @@ watchEffect(async () => {
</q-item>
</template>
</q-select>
<q-input
v-if="addressForeign"
outlined
hide-bottom-space
class="col-md-3 col-6"
v-model="subDistrictTextEN"
:dense="dense"
label="Sub-District"
:readonly="readonly || sameWithEmployer"
:for="`${prefixId}-${indexId !== undefined ? `input-sub-district-en-${indexId}` : 'input-sub-district-en'}`"
:rules="
disabledRule
? []
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-select
v-else
autocomplete="off"
outlined
clearable
@ -819,19 +984,28 @@ watchEffect(async () => {
</template>
</q-select>
<q-input
:key="Number(addressForeign)"
hide-bottom-space
:for="`${prefixId}-${indexId !== undefined ? `input-zip-code-${indexId}` : 'input-zip-code'}`"
:dense="dense"
outlined
readonly
:disable="!readonly && !sameWithEmployer"
:readonly="!addressForeign || readonly"
:disable="!addressForeign && !readonly && !sameWithEmployer"
zip="zip-en"
label="Zip Code"
class="col-md-3 col-6"
:model-value="
addrOptions.subDistrictOps
?.filter((x) => x.id === subDistrictId)
.map((x) => x.zipCode)[0] ?? ''
!addressForeign
? (addrOptions.subDistrictOps
?.filter((x) => x.id === subDistrictId)
.map((x) => x.zipCode)[0] ?? '')
: zipCode
"
@update:model-value="(v) => (zipCode = v.toString())"
:rules="
!addressForeign
? []
: [(val) => (val && val.length > 0) || $t('form.error.required')]
"
/>
<q-input

View file

@ -16,3 +16,4 @@ export { default as SideMenu } from './SideMenu.vue';
export { default as StatCardComponent } from './StatCardComponent.vue';
export { default as TooltipComponent } from './TooltipComponent.vue';
export { default as TreeComponent } from './TreeComponent.vue';
export { default as PaginationPageSize } from './PaginationPageSize.vue';

View file

@ -106,6 +106,7 @@ watch(
:persistent="isDateSelect"
>
<div class="q-pa-sm">
<slot name="prepend"></slot>
<div class="text-weight-medium">
{{ $t('general.advanceSearch') }}
</div>

View file

@ -23,6 +23,8 @@ defineProps<{
history?: boolean;
prefixId?: string;
separateEnter?: boolean;
hideAction?: boolean;
hideDelete?: boolean;
}>();
defineEmits<{
@ -76,8 +78,10 @@ defineEmits<{
/>
<KebabAction
v-if="!hideAction"
:id-name="prefixId"
:status="disabled ? 'INACTIVE' : 'ACTIVE'"
:hide-delete="hideDelete"
@view="
separateEnter
? $emit('viewCard', 'INFO')

View file

@ -13,6 +13,7 @@ let defaultFilter: (
const props = withDefaults(
defineProps<{
prefix?: string;
id?: string;
label?: string;
option: T[];
@ -71,6 +72,7 @@ watch(
</script>
<template>
<q-select
:id="id"
:placeholder="placeholder"
outlined
:clearable

View file

@ -72,7 +72,11 @@ onMounted(async () => {
:option="selectOptions"
:hide-selected="false"
:fill-input="false"
:rules="[(v: string) => !!v || $t('form.error.required')]"
:rules="[
(v: string) => {
return !!v?.length || $t('form.error.required');
},
]"
@filter="filter"
>
<template #before-options v-if="creatable">

View file

@ -75,9 +75,9 @@ function setDefaultValue() {
</script>
<template>
<SelectInput
for="select-hq-id"
v-model="value"
incremental
id="select-hq-id"
:label
:placeholder
:readonly

View file

@ -0,0 +1,213 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { createSelect, SelectProps } from './select';
import SelectInput from '../SelectInput.vue';
import { BusinessType } from 'src/stores/business-type/types';
import useStore from 'src/stores/business-type';
type SelectOption = BusinessType;
const value = defineModel<string | null | undefined>('value', {
required: true,
});
const valueOption = defineModel<SelectOption>('valueOption', {
required: false,
});
const selectOptions = ref<SelectOption[]>([]);
const { fetchList: getList, fetchById: getById } = useStore();
defineEmits<{
(e: 'create'): void;
}>();
type ExclusiveProps = {
lang?: string;
codeOnly?: boolean;
selectFirstValue?: boolean;
branchVirtual?: boolean;
checkRole?: string[];
};
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
const { getOptions, setFirstValue, getSelectedOption, filter } =
createSelect<SelectOption>(
{
value,
valueOption,
selectOptions,
getList: async (query) => {
const ret = await getList({
query,
...props.params,
pageSize: 99999,
});
if (ret) return ret.result;
},
getByValue: async (id) => {
const ret = await getById(id);
if (ret) return ret;
},
},
{ valueField: 'id' },
);
onMounted(async () => {
await getOptions();
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
setFirstValue();
}
if (props.selectFirstValue) {
setDefaultValue();
} else await getSelectedOption();
});
function setDefaultValue() {
setFirstValue();
}
</script>
<template>
<SelectInput
v-model="value"
incremental
option-value="id"
:label="label || $t('menu.manage.businessType')"
:placeholder
:readonly
:disable="disabled"
:option="selectOptions"
:hide-selected="false"
:fill-input="false"
:rules="[
(v: string) => !props.required || !!v || $t('form.error.required'),
]"
@filter="filter"
>
<template #selected-item="{ opt }">
{{ (lang ?? $i18n.locale) !== 'eng' ? opt.name : opt.nameEN }}
</template>
<template #no-option v-if="creatable">
<q-item
:disable="creatableDisabled"
clickable
v-close-popup
@click.stop="$emit('create')"
>
<q-item-section>
<span class="row items-center">
<q-icon
name="mdi-plus-circle-outline"
class="q-mr-sm"
style="color: hsl(var(--positive-bg))"
/>
<b>
{{ $t('general.add', { text: $t('businessType.title') }) }}
</b>
<span
v-if="creatableDisabled && creatableDisabledText"
class="app-text-muted q-pl-xs"
style="font-size: 80%"
>
{{ creatableDisabledText }}
</span>
</span>
</q-item-section>
</q-item>
<q-separator class="q-mx-sm" />
</template>
<template #before-options v-if="creatable">
<q-item
:disable="creatableDisabled"
clickable
v-close-popup
@click.stop="$emit('create')"
for="select-biz-type-add-new"
id="select-biz-type-add-new"
>
<q-item-section>
<span class="row items-center">
<q-icon
name="mdi-plus-circle-outline"
class="q-mr-sm"
style="color: hsl(var(--positive-bg))"
/>
<b>
{{ $t('general.add', { text: $t('businessType.title') }) }}
</b>
<span
v-if="creatableDisabled && creatableDisabledText"
class="app-text-muted q-pl-xs"
style="font-size: 80%"
>
{{ creatableDisabledText }}
</span>
</span>
</q-item-section>
</q-item>
<q-separator class="q-mx-sm" />
</template>
<template #before-options v-if="creatable">
<q-item
:disable="creatableDisabled"
clickable
v-close-popup
@click.stop="$emit('create')"
for="select-business-type-add-new"
id="select-business-type-add-new"
>
<q-item-section>
<span class="row items-center">
<q-icon
name="mdi-plus-circle-outline"
class="q-mr-sm"
style="color: hsl(var(--positive-bg))"
/>
<b>
{{ $t('general.add', { text: $t('menu.manage.businessType') }) }}
</b>
<span
v-if="creatableDisabled && creatableDisabledText"
class="app-text-muted q-pl-xs"
style="font-size: 80%"
>
{{ creatableDisabledText }}
</span>
</span>
</q-item-section>
</q-item>
<q-separator class="q-mx-sm" />
</template>
<template #option="{ opt, scope }">
<q-item v-bind="scope.itemProps">
<span class="row items-center">
{{ (lang ?? $i18n.locale) !== 'eng' ? opt.name : opt.nameEN }}
</span>
</q-item>
<q-separator class="q-mx-sm" />
</template>
<template #append v-if="clearable">
<q-icon
v-if="!readonly && value"
name="mdi-close-circle"
@click.stop="value = ''"
class="cursor-pointer clear-btn"
/>
</template>
</SelectInput>
</template>

View file

@ -30,6 +30,7 @@ defineEmits<{
type ExclusiveProps = {
simple?: boolean;
simpleBranchNo?: boolean;
selectFirstValue?: boolean;
};
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
@ -64,10 +65,14 @@ onMounted(async () => {
setFirstValue();
}
await getSelectedOption();
valueOption.value = selectOptions.value.find((v) => v.id === value.value);
if (props.selectFirstValue) {
setDefaultValue();
} else await getSelectedOption();
});
function setDefaultValue() {
setFirstValue();
}
</script>
<template>
<SelectInput
@ -160,11 +165,9 @@ onMounted(async () => {
</template>
<template #option="{ opt, scope }">
<q-item @click="valueOption = opt" v-bind="scope.itemProps">
<q-item v-bind="scope.itemProps" class="q-mx-sm bodrder">
<SelectCustomerItem :data="opt" :simple :simple-branch-no />
</q-item>
<q-separator class="q-mx-sm" />
</template>
<template #append v-if="clearable">
@ -177,3 +180,11 @@ onMounted(async () => {
</template>
</SelectInput>
</template>
<style scoped>
.bodrder {
border-bottom: solid;
border-bottom-width: 1px;
border-color: var(--border-color);
}
</style>

View file

@ -25,6 +25,7 @@ const { getQuotationList: getList, getQuotation: getById } = useStore();
defineEmits<{
(e: 'create'): void;
(e: 'selected', value: SelectOption): void;
}>();
type ExclusiveProps = {
@ -117,6 +118,14 @@ function setDefaultValue() {
(v: string) => !props.required || !!v || $t('form.error.required'),
]"
@filter="filter"
@update:model-value="
(v) => {
$emit(
'selected',
selectOptions.find((opt) => opt.id === v),
);
}
"
>
<template #append v-if="clearable">
<q-icon

View file

@ -26,6 +26,7 @@ defineEmits<{
type ExclusiveProps = {
selectFirstValue?: boolean;
prefix?: string;
};
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
@ -71,6 +72,7 @@ function setDefaultValue() {
<SelectInput
v-model="value"
incremental
:id="`${prefix || 'nome'}-select-user`"
:label
:placeholder
:readonly
@ -92,7 +94,9 @@ function setDefaultValue() {
:hide-selected="false"
:fill-input="false"
:rules="
required ? [(v: string) => !!v || $t('form.error.required')] : undefined
required && !readonly
? [(v: string) => !!v || $t('form.error.required')]
: undefined
"
@filter="filter"
>

View file

@ -35,7 +35,13 @@ export const createSelect = <T extends Record<string, any>>(
let previousSearch = '';
watch(value, (v) => {
if (!v || (cache && cache.find((opt) => opt[valueField] === v))) return;
if (!v) return;
if (cache && cache.find((opt) => opt[valueField] === v)) {
valueOption.value = cache.find((opt) => opt[valueField] === v);
return;
}
getSelectedOption();
});
@ -63,15 +69,26 @@ export const createSelect = <T extends Record<string, any>>(
const currentValue = value.value;
if (!currentValue) return;
if (selectOptions.value.find((v) => v[valueField] === currentValue)) return;
if (valueOption.value && valueOption.value[valueField] === currentValue) {
return selectOptions.value.unshift(valueOption.value);
selectOptions.value.unshift(valueOption.value);
selectOptions.value = selectOptions.value.filter((curr, idx, arr) => {
return (
arr.findIndex((item) => item[valueField] === curr[valueField]) === idx
);
});
return;
}
const ret = await getByValue(currentValue);
if (ret) {
selectOptions.value.unshift(ret);
selectOptions.value = selectOptions.value.filter((curr, idx, arr) => {
return (
arr.findIndex((item) => item[valueField] === curr[valueField]) === idx
);
});
valueOption.value = ret;
}
}

View file

@ -251,7 +251,10 @@ function selectedIndex(item: any) {
>
<!-- NOTE: custom column will starts with # -->
<template v-if="!col.name.startsWith('#')">
<span>
<span v-if="col.name === 'serviceDetail'">
{{ props.row.detail.replace(/<\/?[^>]+(>|$)/g, '') || '-' }}
</span>
<span v-else>
{{
typeof col.field === 'string'
? props.row[col.field as keyof (Product | Service)]

View file

@ -145,6 +145,7 @@ function selectedIndex(item: Employee) {
<template v-if="col.name === '#check'">
<q-checkbox
id="select-worker-all"
for="select-worker-all"
v-model="props.selected"
@update:model-value="(v) => handleUpdate()"
size="sm"
@ -200,6 +201,7 @@ function selectedIndex(item: Employee) {
v-model="props.selected"
size="sm"
:id="`select-worker-${props.row.firstName}`"
:for="`select-worker-${props.row.firstName}`"
/>
</template>
</q-td>

View file

@ -188,6 +188,7 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
:label="$t('customer.form.citizenId')"
for="input-citizen-id"
v-model="citizenId"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<DatePicker
@ -221,6 +222,7 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
:label="$t('customer.form.religion')"
for="input-religion"
v-model="religion"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-select
outlined
@ -305,6 +307,7 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
:label="$t('customer.form.firstName')"
for="input-first-name"
v-model="firstName"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input
@ -316,6 +319,7 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
:label="$t('customer.form.lastName')"
for="input-last-name"
v-model="lastName"
:rules="[(val: string) => !!val || $t('form.error.required')]"
/>
<q-input

View file

@ -22,6 +22,7 @@ type Props = {
autoSave?: boolean;
data?: Data;
hideBtn?: boolean;
disabledSubmit?: boolean;
};
type HandleProps = {
@ -109,6 +110,7 @@ async function change(e: Event) {
hide-delete
hide-btn
edit
:disabledSubmit
:title
:is-edit
:readonly

View file

@ -31,7 +31,7 @@ const currentIndexDropdownList = ref(0);
const props = withDefaults(
defineProps<{
treeFile: { label: string; file: { label: string }[] }[];
treeFile?: { label: string; file: { label: string }[] }[];
readonly?: boolean;
dropdownList?: { label: string; value: string }[];
hideAction?: boolean;

View file

@ -54,10 +54,11 @@ onMounted(() => {
@click="$emit('click')"
>
<q-icon :name="icon" size="lg" :style="`color: ${color}`" />
<article class="col column q-pl-md">
<span class="ellipsis full-width">
<div class="col column q-pl-md">
<div class="ellipsis full-width" style="max-width: 65vw !important">
{{ name }}
</span>
</div>
<span class="text-caption app-text-muted-2">
{{
uploading.loaded
@ -79,7 +80,7 @@ onMounted(() => {
/>
{{ idle ? `Pending` : progress !== 1 ? `Uploading...` : 'Completed' }}
</span>
</article>
</div>
<q-btn
v-if="closeable"
icon="mdi-close"

View file

@ -45,9 +45,9 @@ const props = withDefaults(
readonly?: boolean;
showTitle?: boolean;
ocr?: (
group: any,
group: string,
file: File,
) => void | Promise<{
) => Promise<{
status: boolean;
group: string;
meta: { name: string; value: string }[];
@ -123,7 +123,7 @@ async function change(e: Event) {
...obj.value,
{
_meta: structuredClone(toRaw(selectedMenu.value)._meta || {}),
group: selectedMenu.value?.value,
group: selectedMenu.value?.group,
file: renamedFile,
},
];
@ -168,8 +168,8 @@ async function change(e: Event) {
type: map['doc_type'],
number: map['doc_number'],
gender: map['sex'],
firstName: map['first_name'],
lastName: map['last_name'],
firstName: map['last_name'],
lastName: map['first_name'],
issueDate: map['issue_date'],
expireDate: map['expire_date'],
issuePlace: map['nationality'],
@ -327,7 +327,7 @@ defineEmits<{
:rows="
obj
.filter((v) => {
if (!autoSave && v.group !== selectedMenu?.value) {
if (!autoSave && v.group !== selectedMenu?.group) {
return false;
}
return true;

View file

@ -198,3 +198,10 @@ i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-i
.q-focus-helper {
visibility: hidden;
}
.clear-btn {
opacity: 0.6;
&:hover {
opacity: 1;
}
}

View file

@ -4,7 +4,7 @@ export default {
save: 'Save',
open: 'Open',
close: 'Close',
edit: 'Edit',
edit: 'Edit{text}',
cancel: 'Cancel',
back: 'Back',
undo: 'Undo',
@ -31,6 +31,7 @@ export default {
displayField: 'Display Fields',
order: 'Order',
name: '{msg} Name',
nameEN: 'Name (English)',
fullName: 'Full Name',
detail: '{msg} Detail',
remark: '{msg} Remark',
@ -160,6 +161,7 @@ export default {
documentStatus: 'Document Status',
advanceSearch: 'Advance Search',
totalPeople: '{meg} people',
price: 'Price {price} Baht',
},
menu: {
@ -202,12 +204,14 @@ export default {
title: 'Manage',
branch: 'Branch',
personnel: 'Personnel',
group: 'Group',
productService: 'Product and Service',
workflow: 'Workflow',
property: 'Property',
customer: 'Customer',
mainData: 'Main Data',
agencies: 'Agencies',
businessType: 'Business Type',
},
sales: {
@ -337,7 +341,7 @@ export default {
requireLength: 'Please enter {msg} character',
branchNameField: "Only letters, numbers, or the characters . , - ' &.",
branchNameENField:
"Only English letters, numbers, or the characters . , - ' &.",
"Only English letters, numbers, or the characters . , - ' &. ( )",
passportFormat: 'Please enter the passport number in the correct format.',
},
warning: {
@ -456,7 +460,7 @@ export default {
regisNo: 'Registration Number',
startDate: 'Start Date',
retireDate: 'Retire Date',
responsibleArea: 'Responsibel Area',
responsibleArea: 'Responsible Area',
discount: 'Discount Condition',
sourceNationality: 'Source Nationality',
importNationality: 'Import Nationality',
@ -473,6 +477,7 @@ export default {
blacklist: 'Black list',
contactName: 'Contact Person',
contactTel: 'Contact Number',
addressForeign: 'Use foreign address',
},
},
customer: {
@ -488,7 +493,7 @@ export default {
},
employer: 'Employer',
employerLegalEntity: 'Legal Entity',
employerNaturalPerson: 'Natrual Person',
employerNaturalPerson: 'Natural Person',
employerType: 'Employer Type',
employee: 'Employee',
form: {
@ -498,11 +503,12 @@ export default {
},
prefix: {
mr: 'Mr.',
mrs: 'Mrs.',
miss: 'Miss.',
mr: 'MR.',
mrs: 'MRS.',
miss: 'MISS.',
},
taxpayyerNo: 'Taxpayer Identification Number',
citizenId: 'Citizen ID',
religion: 'Religion',
issueDate: 'Issue Date',
@ -618,7 +624,7 @@ export default {
placeOfBirth: 'Place of Birth',
countryOfbirth: 'Country of Birth',
issueCountry: 'Issue Country',
entryCount: 'Entry Count',
entryCount: 'Number of Days in the Country',
employerSelect: {
branchName: 'Branch Name',
customerName: 'Employer Name',
@ -768,10 +774,13 @@ export default {
},
quotation: {
ownOnly: 'View Own Quotation Only',
quotationDate: 'Quotation Date',
seller: 'Seller',
paymentChannels: 'Payment Channels',
channelsThat: 'Channels That',
refNo: 'Reference Number',
bankAccount: 'Bank Account',
bankAccountNumber: 'Bank Account Number',
bankAccountName: 'Bank Account Name',
inTheNameOf: 'In The Name Of',
@ -923,6 +932,7 @@ export default {
contactName: 'Contact Person',
contactTel: 'Contact Number',
bankInfo: 'Bank Information',
attachment: 'Attachment',
},
requestList: {
@ -1117,7 +1127,7 @@ export default {
oneOrMoreBranchMissing:
'One or more branch cannot be delete and is missing.',
cantMakeHQAndBranchSameTime:
'Cannot make this as headquaters and branch at the same time.',
'Cannot make this as headquarters and branch at the same time.',
unknowHowToVerify: 'Unknown how to verify identity.',
noPermission:
'You do not have permission to access or perform with this resource.',
@ -1224,6 +1234,9 @@ export default {
taskListNotPending: 'One or more task is not pending.',
reqNotMet: 'Not Match',
systemError: 'A system error occurred.',
taskOrderInvalid: 'Please select the product and the organization.',
flowAccountProductIdNotFound: 'Product not found in flow account',
},
},
@ -1508,4 +1521,11 @@ export default {
last90Days: 'Last 90 Days',
customDateRange: 'Custom Date Range',
},
businessType: {
title: 'Business Type',
caption: 'Manage Business Type',
name: 'Business Type Name',
nameEn: 'Business Type Name (English)',
},
};

View file

@ -4,7 +4,7 @@ export default {
save: 'บันทึก',
open: 'เปิด',
close: 'ปิด',
edit: 'แก้ไข',
edit: 'แก้ไข{text}',
cancel: 'ยกเลิก',
back: 'ย้อนกลับ',
undo: 'ย้อนกลับ',
@ -31,6 +31,7 @@ export default {
displayField: 'ฟิลด์แสดงผล',
order: 'ลำดับ',
name: 'ชื่อ{msg}',
nameEN: 'ชื่อ (ภาษาอังกฤษ)',
fullName: 'ชื่อ-สกุล',
detail: 'รายละเอียด{msg}',
remark: 'หมายเหตุ{msg}',
@ -160,6 +161,7 @@ export default {
documentStatus: 'สถานะเอกสาร',
advanceSearch: 'ค้นหาขั้นสูง',
totalPeople: '{meg} คน',
price: 'ราคา {price} บาท',
},
menu: {
@ -202,12 +204,14 @@ export default {
title: 'จัดการ',
branch: 'สาขา',
personnel: 'บุคลากร',
group: 'กลุ่ม',
productService: 'สินค้าและบริการ',
workflow: 'ขั้นตอนการทำงาน',
property: 'คุณสมบัติ',
customer: 'ลูกค้า',
mainData: 'ข้อมูลหลัก',
agencies: 'หน่วยงาน',
businessType: 'ประเภทกิจการ',
},
sales: {
@ -334,7 +338,7 @@ export default {
letterAndNumOnly: 'โปรดใช้เฉพาะ _ ตัวอักษรภาษาอังกฤษและตัวเลขเท่านั้น',
numOnly: 'โปรดใช้เฉพาะตัวเลขเท่านั้น',
requireLength: 'กรุณากรอกให้ครบ {msg} หลัก',
branchNameField: "โปรดใช้ตัวอักษร ตัวเลข หรือ . , - ' & เท่านั้น",
branchNameField: "โปรดใช้ตัวอักษร ตัวเลข หรือ . , - ' ( ) & เท่านั้น",
branchNameENField:
"โปรดใช้ตัวอักษรภาษาอังกฤษ ตัวเลข หรือ . , - ' & เท่านั้น",
passportFormat: 'กรุณากรอกหมายเลขพาสปอร์ตให้ถูกต้องตามรูปแบบ',
@ -469,6 +473,7 @@ export default {
blacklist: 'แบล็คลิสต์',
contactName: 'ชื่อผู้ติดต่อ',
contactTel: 'เบอร์โทรศัพท์ผู้ติดต่อ',
addressForeign: 'ใช้ที่อยู่ต่างประเทศ',
},
},
customer: {
@ -495,15 +500,16 @@ export default {
},
prefix: {
mr: 'Mr.',
mrs: 'Mrs.',
miss: 'Miss.',
mr: 'นาย',
mrs: 'นาง',
miss: 'นางสาว',
},
citizenId: 'บัตรประจำตัวประชาชน',
religion: 'ศาสนา',
issueDate: 'วันที่ออกหนังสือ',
passportExpiryDate: 'วันหiมดอายุหนังสือเดินทาง',
taxpayyerNo: 'เลขที่ประจำตัวผู้เสียภาษี',
ownerName: 'ชื่อนายจ้าง',
firstName: 'ชื่อ ',
@ -620,7 +626,7 @@ export default {
placeOfBirth: 'สถานที่เกิด',
countryOfbirth: 'ประเทศที่เกิด',
issueCountry: 'ประเทศที่ออก',
entryCount: 'จำนวนที่เข้าประเทศ',
entryCount: 'จำนวนวันที่เข้าประเทศ',
employerSelect: {
branchName: 'ชื่อสาขา',
customerName: 'ชื่อนายจ้าง',
@ -648,7 +654,7 @@ export default {
permitIssuedAt: 'สถานที่ออกใบอนุญาต',
permitIssueDate: 'วันที่ออกใบอนุญาตทำงาน',
permitExpireDate: 'วันที่หมดอายุใบอนุญาตทำงาน',
identityNo: 'เลขประจำตัวคนต่างด้าว (13หลัก)',
identityNo: 'เลขประจำตัวคนต่างด้าว (13 หลัก)',
},
formFamily: {
citizenId:
@ -766,10 +772,13 @@ export default {
},
quotation: {
ownOnly: 'เห็นเฉพาะใบเสนอราคาของตัวเอง',
quotationDate: 'วันที่ใบเสนอราคา',
seller: 'ผู้ขาย',
paymentChannels: 'ช่องทางชำระเงิน',
channelsThat: 'ช่องทางที่',
refNo: 'เลขที่อ้างอิง',
bankAccount: 'บัญชีธนาคาร',
bankAccountNumber: 'เลขบัญชีธนาคาร',
bankAccountName: 'ชื่อบัญชี',
inTheNameOf: 'ในนาม',
@ -794,7 +803,7 @@ export default {
branch: 'สาขาที่ออกใบเสนอราคา',
branchVirtual: 'จุดรับบริการที่ออกใบเสนอราคา',
customer: 'ลูกค้า',
newCustomer: 'ลูกค้าใหม่',
newCustomer: 'แรงงานใหม่',
employeeList: 'รายชื่อแรงงาน',
employee: 'แรงงาน',
employeeName: 'ชื่อ-นามสกุล แรงงาน',
@ -920,6 +929,7 @@ export default {
contactName: 'ชื่อผู้ติดต่อ',
contactTel: 'เบอร์โทรผู้ติดต่อ',
bankInfo: 'ข้อมูลธนาคาร',
attachment: 'เอกสารเพิ่มเติม',
},
requestList: {
@ -1210,6 +1220,8 @@ export default {
'มีงานหนึ่งงานหรือมากกว่าที่ไม่อยู่ในสถานะรอดำเนินการ',
reqNotMet: 'ไม่ตรงกัน',
systemError: 'ระบบเกิดข้อผิดพลาด',
taskOrderInvalid: 'โปรดเลือก สินค้าเเละสาขา',
flowAccountProductIdNotFound: 'ไม่พบสินค้าใน flow account',
},
},
@ -1496,4 +1508,11 @@ export default {
last90Days: '90 วันที่ผ่านมา',
customDateRange: 'กำหนดช่วงวันที่เอง',
},
businessType: {
title: 'ประเภทกิจการ',
caption: 'จัดการประเภทกิจการ',
name: 'ชื่อประเภทกิจการ',
nameEn: 'ชื่อประเภทกิจการ (ภาษาอังกฤษ)',
},
};

View file

@ -7,7 +7,7 @@ import useMyBranch from 'stores/my-branch';
import { getUserId, getRole } from 'src/services/keycloak';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { isRoleInclude } from 'src/stores/utils';
import { canAccess } from 'src/stores/utils';
type Menu = {
label: string;
@ -71,82 +71,50 @@ function initMenu() {
{
label: 'branch',
route: '/branch-management',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
]),
hidden: !canAccess('branch'),
},
{
label: 'personnel',
route: '/personnel-management',
hidden: !isRoleInclude([
'owner',
'system',
'head_of_admin',
'admin',
'branch_manager',
]),
hidden: !canAccess('personnel'),
},
{
label: 'group',
route: '/group-management',
hidden: !canAccess('personnel'),
},
{
label: 'workflow',
route: '/workflow',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']),
hidden: !canAccess('workflow'),
},
{
label: 'property',
route: '/property',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']),
hidden: !canAccess('workflow'),
},
{
label: 'businessType',
route: '/business-type',
},
{
label: 'productService',
route: '/product-service',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'head_of_sale',
'sale',
]),
},
{
label: 'customer',
route: '/customer-management',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
hidden: !canAccess('customer'),
},
{
label: 'agencies',
route: '/agencies-management',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']),
},
],
},
{
label: 'menu.sales',
icon: 'mdi-store-settings-outline',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
children: [
{ label: 'quotation', route: '/quotation' },
{ label: 'invoice', route: '/invoice' },
@ -169,16 +137,6 @@ function initMenu() {
label: 'menu.account',
icon: 'mdi-bank-outline',
disabled: false,
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
children: [
{ label: 'receipt', route: '/receipt' },
{ label: 'creditNote', route: '/credit-note' },
@ -200,10 +158,13 @@ function initMenu() {
{
label: 'menu.overall',
icon: 'mdi-monitor-dashboard',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin', 'executive']),
children: [
{ label: 'report', route: '/report' },
{ label: 'dashboard', route: '/dash-board' },
{
label: 'dashboard',
route: '/dash-board',
hidden: !canAccess('dashBoard'),
},
],
},

View file

@ -2,7 +2,7 @@
import { ref, onMounted, computed, reactive } from 'vue';
import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar';
import { getUserId, getUsername, logout, getRole } from 'src/services/keycloak';
import { getUserId, getUsername, getName, logout, getRole } from 'src/services/keycloak';
import { Icon } from '@iconify/vue';
import { useI18n } from 'vue-i18n';
import moment from 'moment';
@ -39,7 +39,7 @@ const configStore = useConfigStore();
const { data: notificationData } = storeToRefs(notificationStore);
const { visible } = storeToRefs(loaderStore);
const { t } = useI18n({ useScope: 'global' });
const { t, locale } = useI18n({ useScope: 'global' });
const userStore = useUserStore();
const canvasModal = ref(false);
@ -52,8 +52,14 @@ const unread = computed<number>(
);
const userImage = ref<string>();
const userGender = ref('');
const userName = ref({ th: '', en: '' });
const canvasRef = ref();
const displayName = computed(() => {
if (!userName.value.th && !userName.value.en) return getName() || 'Guest';
return locale.value === 'eng' ? userName.value.en : userName.value.th;
});
const language: {
value: Lang;
label: string;
@ -161,9 +167,14 @@ onMounted(async () => {
if (user === 'admin') return;
if (uid) {
const res = await userStore.fetchById(uid);
if (res && res.gender) {
userGender.value = res.gender;
userImage.value = `${baseUrl}/user/${uid}/profile-image/${res.selectedImage}`;
if (res) {
if (res.gender) {
userGender.value = res.gender;
userImage.value = `${baseUrl}/user/${uid}/profile-image/${res.selectedImage}`;
}
//
userName.value.th = `${res.firstName || ''} ${res.lastName || ''}`.trim();
userName.value.en = `${res.firstNameEN || ''} ${res.lastNameEN || ''}`.trim();
}
}
});
@ -484,6 +495,7 @@ onMounted(async () => {
<!-- User -->
<ProfileMenu
id="btn-profile-menu"
:user-name="displayName"
@logout="doLogout"
@edit-personal-info="console.log('edit')"
@signature="

View file

@ -12,6 +12,7 @@ const filterRole = ref<string[]>();
defineProps<{
userImage?: string;
gender?: string;
userName?: string;
}>();
const inputFile = document.createElement('input');
@ -147,9 +148,9 @@ onMounted(async () => {
class="text-weight-bold ellipsis"
style="max-width: 9vw"
>
{{ getName() }}
{{ userName || getName() }}
<q-tooltip>
{{ getName() }}
{{ userName || getName() }}
</q-tooltip>
</span>
<span
@ -234,12 +235,12 @@ onMounted(async () => {
style="margin-top: 58px"
>
<span v-if="isLoggedIn()">
{{ getName() }}
{{ userName || getName() }}
</span>
<span v-else>{{ 'Guest' }}</span>
<q-tooltip>
<span v-if="isLoggedIn()">
{{ getName() }}
{{ userName || getName() }}
</span>
<span v-else>{{ 'Guest' }}</span>
</q-tooltip>

View file

@ -10,10 +10,11 @@ import { onMounted, watch } from 'vue';
import { useManualStore } from 'src/stores/manual';
import { useNavigator } from 'src/stores/navigator';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
// NOTE: Variable
const route = useRoute();
const router = useRouter();
const manualStore = useManualStore();
const navigatorStore = useNavigator();
const { dataManual, dataTroubleshooting } = storeToRefs(manualStore);
@ -33,6 +34,16 @@ watch(
if (route.name === 'Troubleshooting') {
const res = await manualStore.getTroubleshooting();
dataTroubleshooting.value = res ? res : [];
if (
res.length &&
res.length === 1 &&
res[0].page &&
res[0].page.length === 1
) {
router.replace(
`/troubleshooting/${res[0].category}/${res[0].page[0].name}`,
);
}
}
},
{ immediate: true },

View file

@ -116,8 +116,8 @@ async function scrollTo(id: string) {
<template>
<main
class="full-height q-gutter-sm"
:class="{ 'row reverse': $q.screen.gt.xs, column: $q.screen.xs }"
class="full-height q-gutter-sm no-wrap"
:class="{ column: !toc && $q.screen.lt.md, 'row reverse': $q.screen.gt.sm }"
>
<section
v-if="toc"
@ -168,7 +168,7 @@ async function scrollTo(id: string) {
</q-list>
</section>
<section v-if="!toc && $q.screen.xs">
<section v-if="!toc && $q.screen.lt.md">
<q-btn
dense
class="full-width text-capitalize"
@ -181,7 +181,7 @@ async function scrollTo(id: string) {
</section>
<section
v-if="content || (!toc && $q.screen.xs)"
v-if="$q.screen.gt.xs || (!toc && $q.screen.xs)"
ref="wrapper"
class="markdown col scroll full-height rounded"
>
@ -328,6 +328,14 @@ async function scrollTo(id: string) {
padding: 0px 16px;
}
.markdown :deep(h4) {
text-align: left;
margin-block: 0;
font-size: 16px;
font-weight: 600;
padding: 0px 16px;
}
.markdown :deep(video) {
width: 100%;
}

View file

@ -94,21 +94,22 @@ onMounted(async () => {
<template>
<main class="column full-height no-wrap">
<div class="surface-1 col bordered rounded column">
<div class="q-px-lg q-py-xs row items-center">
<div class="q-py-xs row items-center" style="padding-inline: 38px">
<q-checkbox
size="xs"
:model-value="selectedNoti.length === noti.length"
class="q-px-sm"
:model-value="noti.length > 0 && selectedNoti.length === noti.length"
:disable="noti.length === 0"
@click="toggleSelection('', true)"
/>
<q-separator vertical inset spaced="md" />
<q-btn
v-if="selectedNoti.length === 0"
icon="mdi-refresh"
rounded
flat
dense
size="xs"
class="app-text-muted-2 q-ml-sm q-mt-xs"
size="sm"
class="app-text-muted-2 q-mt-xs"
@click="async () => await fetchNoti()"
>
<q-tooltip>Refresh</q-tooltip>
@ -119,8 +120,8 @@ onMounted(async () => {
rounded
flat
dense
size="xs"
class="app-text-muted-2 q-ml-sm"
size="sm"
class="app-text-muted-2"
@click="async () => await deleteNoti()"
>
<q-tooltip>{{ $t('general.delete') }}</q-tooltip>
@ -131,7 +132,7 @@ onMounted(async () => {
rounded
flat
dense
size="xs"
size="sm"
class="app-text-muted-2 q-mx-sm"
@click="async () => await markAsRead()"
>
@ -177,7 +178,13 @@ onMounted(async () => {
<q-badge
rounded
class="q-ml-md"
style="background: hsl(var(--info-bg))"
:color="
(tab.value === 'all'
? noti.length
: noti.filter((v) => !v.read).length) > 0
? 'info'
: 'grey'
"
>
{{
tab.value === 'all'

View file

@ -10,7 +10,7 @@ import type { QTableProps, QTableSlots } from 'quasar';
import { resetScrollBar } from 'src/stores/utils';
import useBranchStore from 'stores/branch';
import useFlowStore from 'stores/flow';
import { isRoleInclude } from 'stores/utils';
import { isRoleInclude, canAccess } from 'stores/utils';
import {
BranchWithChildren,
BranchCreate,
@ -791,6 +791,8 @@ async function onSubmit(submitSelectedItem?: boolean) {
);
if (!res) return;
formType.value = 'view';
formData.value.codeHeadOffice = formData.value.code = res.code;
imageUrl.value = `${baseUrl}/branch/${res.id}/image/${res.selectedImage}`;
@ -865,7 +867,9 @@ async function onSubmit(submitSelectedItem?: boolean) {
actionText: t('dialog.action.ok'),
persistent: true,
title: t('form.warning.title'),
cancel: () => {},
cancel: () => {
formType.value = 'create';
},
action: async () => {
await createBranch();
},
@ -1046,7 +1050,7 @@ watch(currentHq, () => {
{{ $t('branch.allBranch') }}
</div>
<q-btn
v-if="isRoleInclude(['head_of_admin', 'admin', 'system'])"
v-if="isRoleInclude(['system'])"
round
flat
size="md"
@ -1070,6 +1074,7 @@ watch(currentHq, () => {
<div class="col full-width scroll">
<div class="q-pa-md">
<TreeComponent
:hide-create="!canAccess('branch', 'create')"
v-model:nodes="treeData"
v-model:expanded-tree="expandedTree"
node-key="id"
@ -1557,8 +1562,18 @@ watch(currentHq, () => {
</q-td>
<q-td>
<KebabAction
v-if="
!currentHq.id
? canAccess('branch', 'create')
: true
"
:status="props.row.status"
:idName="props.row.name"
:hide-delete="
!currentHq.id
? !isRoleInclude(['system'])
: !canAccess('branch', 'create')
"
@view="
if (props.row.isHeadOffice) {
triggerEdit(
@ -1698,8 +1713,18 @@ watch(currentHq, () => {
>
<template v-slot:action>
<KebabAction
v-if="
!currentHq.id
? canAccess('branch', 'create')
: true
"
:status="props.row.status"
:idName="props.row.name"
:hide-delete="
!currentHq.id
? !isRoleInclude(['system'])
: !canAccess('branch', 'create')
"
@view="
if (props.row.isHeadOffice) {
triggerEdit(
@ -2247,13 +2272,22 @@ watch(currentHq, () => {
@click="drawerEdit()"
type="button"
/>
<DeleteButton
v-if="formType !== 'edit'"
id="btn-info-basic-delete"
icon-only
@click="triggerDelete(currentEdit.id)"
type="button"
/>
<template
v-if="
formType !== 'edit' && formTypeBranch === 'headOffice'
? isRoleInclude(['system'])
: canAccess('branch', 'create')
"
>
<DeleteButton
v-if="formType !== 'edit'"
id="btn-info-basic-delete"
icon-only
@click="triggerDelete(currentEdit.id)"
type="button"
/>
</template>
</div>
</div>
<div

View file

@ -0,0 +1,67 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { getInstance } from 'src/services/keycloak';
import { useNavigator } from 'src/stores/navigator';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { initLang } from 'src/utils/ui';
const $q = useQuasar();
const { locale } = useI18n();
const navigatorStore = useNavigator();
const EDM_SERVICE = import.meta.env.VITE_EDM_MICRO_FRONTEND_URL;
const kc = getInstance();
const at = ref(kc.token);
const rt = ref(kc.refreshToken);
const iframe = ref<InstanceType<typeof HTMLIFrameElement>>();
function sendMessage() {
iframe?.value?.contentWindow?.postMessage(
{
i18n: locale.value,
darkMode: $q.dark.isActive,
},
'*',
);
}
onMounted(() => {
initLang();
currentLocale.value = locale.value;
navigatorStore.current.title = 'menu.dms';
navigatorStore.current.path = [
{
text: '',
i18n: true,
handler: () => {},
},
];
sendMessage();
});
watch(
() => kc.token,
() => {
at.value = kc.token;
rt.value = kc.refreshToken;
},
);
watch([locale, $q.dark], () => {
sendMessage();
});
const currentLocale = ref(locale.value);
</script>
<template>
<iframe
ref="iframe"
:src="`${EDM_SERVICE}/user?at=${at}&rt=${rt}&lang=${currentLocale}`"
frameborder="0"
class="full-width full-height rounded"
allowtransparency="true"
></iframe>
</template>
<style scoped></style>

View file

@ -157,6 +157,14 @@ const defaultFormData = {
contactTel: '',
remark: '',
agencyStatus: '',
addressForeign: false,
provinceText: null,
districtText: null,
subDistrictText: null,
provinceTextEN: null,
districtTextEN: null,
subDistrictTextEN: null,
zipCodeText: null,
};
const formData = ref<UserCreate>({
@ -209,6 +217,14 @@ const formData = ref<UserCreate>({
contactTel: '',
remark: '',
agencyStatus: '',
addressForeign: false,
provinceText: null,
districtText: null,
subDistrictText: null,
provinceTextEN: null,
districtTextEN: null,
subDistrictTextEN: null,
zipCodeText: null,
});
const fieldSelectedOption = ref<{ label: string; value: string }[]>([
@ -431,6 +447,28 @@ async function onSubmit(excludeDialog?: boolean) {
...formData.value,
checkpointEN: formData.value.checkpoint,
status: !statusToggle.value ? 'INACTIVE' : 'ACTIVE',
provinceId: formData.value.addressForeign
? null
: formData.value.provinceId,
districtId: formData.value.addressForeign
? null
: formData.value.districtId,
subDistrictId: formData.value.addressForeign
? null
: formData.value.subDistrictId,
provinceText: formData.value.addressForeign
? formData.value.provinceId
: null,
districtText: formData.value.addressForeign
? formData.value.districtId
: null,
subDistrictText: formData.value.addressForeign
? formData.value.subDistrictId
: null,
zipCodeText: formData.value.addressForeign
? formData.value.zipCode
: null,
} as const;
await userStore.editById(currentUser.value.id, formDataEdit);
@ -462,7 +500,31 @@ async function onSubmit(excludeDialog?: boolean) {
: '';
formData.value.checkpointEN = formData.value.checkpoint;
const result = await userStore.create(
formData.value,
{
...formData.value,
provinceId: formData.value.addressForeign
? null
: formData.value.provinceId,
districtId: formData.value.addressForeign
? null
: formData.value.districtId,
subDistrictId: formData.value.addressForeign
? null
: formData.value.subDistrictId,
provinceText: formData.value.addressForeign
? formData.value.provinceId
: null,
districtText: formData.value.addressForeign
? formData.value.districtId
: null,
subDistrictText: formData.value.addressForeign
? formData.value.subDistrictId
: null,
zipCodeText: formData.value.addressForeign
? formData.value.zipCode
: null,
},
onCreateImageList.value,
);
@ -555,18 +617,25 @@ async function triggerChangeStatus(id: string, status: string) {
async function assignFormData(idEdit: string) {
if (!userData.value) return;
const foundUser = userData.value.result.find((user) => user.id === idEdit);
console.log(foundUser);
if (foundUser) {
currentUser.value = foundUser;
formData.value = {
branchId: foundUser.branch[0]?.id,
provinceId: foundUser.provinceId,
districtId: foundUser.districtId,
subDistrictId: foundUser.subDistrictId,
provinceId: foundUser.addressForeign
? foundUser.provinceText
: foundUser.provinceId,
districtId: foundUser.addressForeign
? foundUser.districtText
: foundUser.districtId,
subDistrictId: foundUser.addressForeign
? foundUser.subDistrictText
: foundUser.subDistrictId,
telephoneNo: foundUser.telephoneNo,
email: foundUser.email,
zipCode: foundUser.zipCode,
zipCode: foundUser.addressForeign
? foundUser.zipCodeText
: foundUser.zipCode,
gender: foundUser.gender,
addressEN: foundUser.addressEN,
address: foundUser.address,
@ -600,8 +669,8 @@ async function assignFormData(idEdit: string) {
responsibleArea: foundUser.responsibleArea,
status: foundUser.status,
selectedImage: foundUser.selectedImage,
contactName: foundUser.contactName,
contactTel: foundUser.contactTel,
contactName: foundUser.contactName || '',
contactTel: foundUser.contactTel || '',
licenseExpireDate:
(foundUser.licenseExpireDate &&
new Date(foundUser.licenseExpireDate)) ||
@ -620,6 +689,10 @@ async function assignFormData(idEdit: string) {
(foundUser.citizenExpire && new Date(foundUser.citizenExpire)) || null,
remark: foundUser.remark || '',
agencyStatus: foundUser.agencyStatus || '',
addressForeign: foundUser.addressForeign || false,
provinceTextEN: foundUser.provinceTextEN,
districtTextEN: foundUser.districtTextEN,
subDistrictTextEN: foundUser.subDistrictTextEN,
};
formData.value.status === 'ACTIVE' || 'CREATED'
@ -746,7 +819,17 @@ watch(
watch(
() => formData.value.userType,
async () => {
async (type) => {
if (type !== 'AGENCY') {
formData.value.addressForeign = false;
formData.value.provinceId = null;
formData.value.districtId = null;
formData.value.subDistrictId = null;
formData.value.provinceTextEN = null;
formData.value.districtTextEN = null;
formData.value.subDistrictTextEN = null;
formData.value.zipCodeText = null;
}
if (!infoDrawerEdit.value) return;
formData.value.registrationNo = null;
formData.value.startDate = null;
@ -1279,7 +1362,7 @@ watch(
{{
locale === 'eng'
? `${props.row.firstNameEN} ${props.row.lastNameEN}`.trim()
: `${props.row.firstName} ${props.row.lastName}`.trim()
: `${props.row.firstName || props.row.firstNameEN} ${props.row.lastName || props.row.lastNameEN}`.trim()
}}
<q-tooltip
anchor="bottom left"
@ -1289,7 +1372,7 @@ watch(
{{
locale === 'eng'
? `${props.row.firstNameEN} ${props.row.lastNameEN}`.trim()
: `${props.row.firstName} ${props.row.lastName}`.trim()
: `${props.row.firstName || props.row.firstNameEN} ${props.row.lastName || props.row.lastNameEN}`.trim()
}}
</q-tooltip>
@ -1555,9 +1638,12 @@ watch(
hide-action
:is-edit="infoDrawerEdit"
:title="
locale === 'eng'
(currentUser.namePrefix
? $t('customer.form.prefix.' + currentUser.namePrefix) + ' '
: '') +
(locale === 'eng'
? `${currentUser.firstNameEN} ${currentUser.lastNameEN}`
: `${currentUser.firstName} ${currentUser.lastName}`
: `${currentUser.firstName || currentUser.firstNameEN} ${currentUser.lastName || currentUser.lastNameEN}`)
"
v-model:drawerOpen="infoDrawer"
:submit="() => onSubmit()"
@ -1593,8 +1679,8 @@ watch(
setPrefixName(
{
namePrefix: formData.namePrefix,
firstName: formData.firstName,
lastName: formData.lastName,
firstName: formData.firstName || formData.firstNameEN,
lastName: formData.lastName || formData.lastNameEN,
firstNameEN: formData.firstNameEN,
lastNameEN: formData.lastNameEN,
},
@ -1811,10 +1897,15 @@ watch(
v-model:district-id="formData.districtId"
v-model:sub-district-id="formData.subDistrictId"
v-model:zip-code="formData.zipCode"
v-model:address-foreign="formData.addressForeign"
v-model:province-text-en="formData.provinceTextEN"
v-model:district-text-en="formData.districtTextEN"
v-model:sub-district-text-en="formData.subDistrictTextEN"
:readonly="!infoDrawerEdit"
prefix-id="drawer-info-personnel"
:title="'personnel.form.addressInformation'"
dense
:use-foreign-address="formData.userType === 'AGENCY'"
class="q-mb-xl"
/>
<FormByType
@ -2040,8 +2131,13 @@ watch(
v-model:district-id="formData.districtId"
v-model:sub-district-id="formData.subDistrictId"
v-model:zip-code="formData.zipCode"
v-model:address-foreign="formData.addressForeign"
v-model:province-text-en="formData.provinceTextEN"
v-model:district-text-en="formData.districtTextEN"
v-model:sub-district-text-en="formData.subDistrictTextEN"
prefix-id="drawer-info-personnel"
dense
:use-foreign-address="formData.userType === 'AGENCY'"
class="q-mb-xl"
/>
<FormByType

View file

@ -9,7 +9,7 @@ import { baseUrl } from 'src/stores/utils';
import useCustomerStore from 'stores/customer';
import useFlowStore from 'stores/flow';
import useOptionStore from 'stores/options';
import { dialog } from 'stores/utils';
import { dialog, canAccess } from 'stores/utils';
import { Status } from 'stores/types';
import { Employee } from 'stores/employee/types';
@ -18,6 +18,8 @@ import { CustomerBranch, CustomerType } from 'stores/customer/types';
import { columnsEmployee } from './constant';
import { useCustomerBranchForm, useEmployeeForm } from './form';
import DialogEmployee from 'src/components/03_customer-management/DialogEmployee.vue';
import DrawerEmployee from 'src/components/03_customer-management/DrawerEmployee.vue';
import EmployerFormAuthorized from './components/employer/EmployerFormAuthorized.vue';
import FloatingActionButton from 'components/FloatingActionButton.vue';
import SideMenu from 'components/SideMenu.vue';
@ -89,6 +91,11 @@ const prop = withDefaults(
currentCitizenId?: string;
gender: string;
selectedImage: string;
fetchImageList: (
id: string,
selectedName: string,
type: 'customer' | 'employee',
) => Promise<void>;
}>(),
{
color: 'green',
@ -96,7 +103,6 @@ const prop = withDefaults(
);
const currentBranchEmployee = ref<string>('');
const listEmployee = ref<Employee[]>([]);
const customerId = defineModel<string>('customerId', { required: true });
defineEmits<{
@ -106,16 +112,6 @@ defineEmits<{
(e: 'dialog'): void;
}>();
onMounted(async () => {
customerBranchFormState.value.currentCustomerId = route.params
.customerId as string;
await fetchList();
branch.value?.forEach((v) => {
currentBtnOpen.value.push(false);
});
});
const columns = [
{
name: 'branchName',
@ -257,10 +253,6 @@ async function fetchEmployee(opts: { branchId: string; pageSize?: number }) {
}
}
onMounted(async () => {
await fetchList();
});
watch([customerId, inputSearch, currentStatus, pageSizeBranch], async () => {
await fetchList();
});
@ -280,12 +272,22 @@ watch(
}
},
);
onMounted(async () => {
customerBranchFormState.value.currentCustomerId = route.params
.customerId as string;
await fetchList();
branch.value?.forEach((v) => {
currentBtnOpen.value.push(false);
});
});
</script>
<template>
<FloatingActionButton
style="z-index: 999"
v-if="$route.name !== 'CustomerManagement'"
v-if="$route.name !== 'CustomerManagement' && canAccess('customer', 'edit')"
@click="openEmployerBranchForm('create')"
hide-icon
></FloatingActionButton>
@ -473,7 +475,6 @@ watch(
<q-tr
:class="{
'app-text-muted': props.row.status === 'INACTIVE',
'cursor-pointer': props.row._count?.branch !== 0,
}"
:props="props"
@click="$emit('viewDetail', props.row, props.rowIndex)"
@ -547,7 +548,13 @@ watch(
v-if="branchFieldSelected.includes('businessTypePure')"
class="text-left"
>
{{ useOptionStore().mapOption(props.row.businessType) || '-' }}
{{
props.row.businessType
? props.row.businessType[
$i18n.locale === 'eng' ? 'nameEN' : 'name'
]
: '-'
}}
</q-td>
<q-td
v-if="branchFieldSelected.includes('totalEmployee')"
@ -566,8 +573,6 @@ watch(
await fetchEmployee({
branchId: currentBranchEmployee,
pageSize: 999,
passport: true,
visa: true,
});
currentBtnOpen.map((v, i) => {
@ -615,7 +620,7 @@ watch(
<div class="text-center">
<TableEmpoloyee
:prefix-id="props.row.registerName || props.row.firstName"
add-button
:add-button="canAccess('customer', 'edit')"
in-table
:list-employee="listEmployee"
:columns-employee="columnsEmployee"
@ -638,10 +643,15 @@ watch(
"
@history="(item) => {}"
@view="
(item) => {
async (item) => {
employeeFormState.drawerModal = true;
//employeeFormState.isEmployeeEdit = true;
employeeFormStore.assignFormDataEmployee(item.id);
await fetchImageList(
item.id,
item.selectedImage || '',
'employee',
);
}
"
/>
@ -668,9 +678,11 @@ watch(
? `${props.row.addressEN || ''} ${props.row.subDistrict?.nameEN || ''} ${props.row.district?.nameEN || ''} ${props.row.province?.nameEN || ''}`
: `${props.row.address || ''} ${props.row.subDistrict?.name || ''} ${props.row.district?.name || ''} ${props.row.province?.name || ''}`,
telephone: props.row.telephoneNo,
businessTypePure: useOptionStore().mapOption(
props.row.businessType,
),
businessTypePure: props.row.businessType
? props.row.businessType[
$i18n.locale === 'eng' ? 'nameEN' : 'name'
]
: '-',
totalEmployee: props.row._count?.employee,
}"
:visible-columns="branchFieldSelected"
@ -759,7 +771,10 @@ watch(
/>
<DeleteButton
icon-only
v-if="customerBranchFormState.dialogType === 'info'"
v-if="
customerBranchFormState.dialogType === 'info' &&
canAccess('customer', 'edit')
"
@click="
() => {
deleteBranchById(customerBranchFormData.id || '');
@ -853,7 +868,6 @@ watch(
v-model:last-name-en="customerBranchFormData.lastNameEN"
v-model:gender="customerBranchFormData.gender"
v-model:birth-date="customerBranchFormData.birthDate"
v-model:customer-name="customerBranchFormData.customerName"
v-model:legal-person-no="customerBranchFormData.legalPersonNo"
v-model:branch-code="customerBranchFormData.code"
v-model:register-name="customerBranchFormData.registerName"
@ -883,15 +897,14 @@ watch(
</div>
<EmployerFormBusiness
dense
class="q-mb-xl"
outlined
prefix-id="employer-branch"
:readonly="customerBranchFormState.dialogType === 'info'"
v-model:bussiness-type="customerBranchFormData.businessType"
v-model:business-type-id="customerBranchFormData.businessTypeId"
v-model:job-position="customerBranchFormData.jobPosition"
v-model:job-description="customerBranchFormData.jobDescription"
v-model:pay-date="customerBranchFormData.payDate"
v-model:pay-date-e-n="customerBranchFormData.payDateEN"
v-model:pay-date-en="customerBranchFormData.payDateEN"
v-model:wage-rate="customerBranchFormData.wageRate"
v-model:wage-rate-text="customerBranchFormData.wageRateText"
/>
@ -982,7 +995,7 @@ watch(
v-model:email="customerBranchFormData.email"
v-model:contact-tel="customerBranchFormData.contactTel"
v-model:office-tel="customerBranchFormData.officeTel"
v-model:agent="customerBranchFormData.agent"
v-model:agent-user-id="customerBranchFormData.agentUserId"
/>
</div>
</div>
@ -998,6 +1011,18 @@ watch(
/>
</template>
</DialogFormContainer>
<DialogEmployee
:fetch-list-employee="fetchEmployee"
:fetch-image-list="fetchImageList"
current-tab="employer"
/>
<DrawerEmployee
:fetch-list-employee="fetchEmployee"
:fetch-image-list="fetchImageList"
current-tab="employer"
/>
</template>
<style scoped>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,514 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import {
PaginationComponent,
PaginationPageSize,
ImageUploadDialog,
DialogForm,
NoData,
} from 'src/components';
import DialogEmployee from 'src/components/03_customer-management/DialogEmployee.vue';
import HistoryEditComponent from 'src/components/03_customer-management/HistoryEditComponent.vue';
import TableEmpoloyee from 'src/components/03_customer-management/TableEmpoloyee.vue';
import DrawerEmployee from 'src/components/03_customer-management/DrawerEmployee.vue';
import useFlowStore from 'src/stores/flow';
import useEmployeeStore from 'src/stores/employee';
import { Status } from 'src/stores/types';
import { Employee, EmployeeHistory } from 'src/stores/employee/types';
import { baseUrl, dialog, canAccess } from 'src/stores/utils';
import { calculateAge, toISOStringWithTimezone } from 'src/utils/datetime';
import { useCustomerForm, useEmployeeForm } from './form';
import { columnsEmployee } from './constant';
const $q = useQuasar();
const flowStore = useFlowStore();
const employeeStore = useEmployeeStore();
const employeeFormStore = useEmployeeForm();
const customerFormStore = useCustomerForm();
const { t } = useI18n();
const { state: employeeFormState, currentFromDataEmployee } =
storeToRefs(employeeFormStore);
const { deleteEmployeeById } = employeeFormStore;
const { state: customerFormState } = storeToRefs(customerFormStore);
const employeeStats = defineModel('employeeStats', { default: 0 });
const props = defineProps<{
currentTab: 'employee' | 'employer';
currentStatus: Status | 'All';
gridView: boolean;
inputSearch: string;
searchDate: string[];
fieldSelected: string[];
fetchImageList: (
id: string,
selectedName: string,
type: 'customer' | 'employee',
) => Promise<void>;
triggerChangeStatus: (
id: string,
status: string,
employeeName?: string,
) => void;
}>();
defineExpose({
openSpecificEmployee,
fetchListEmployee,
toggleStatusEmployee,
});
const listEmployee = ref<Employee[]>([]);
const currentPageEmployee = ref<number>(1);
const maxPageEmployee = ref<number>(1);
const pageSize = ref<number>(30);
const employeeHistoryDialog = ref(false);
const employeeHistory = ref<EmployeeHistory[]>();
const splitPercent = computed(() => ($q.screen.lt.md ? 0 : 15));
async function fetchListEmployee(opt?: {
fetchStats?: boolean;
page?: number;
pageSize?: number;
customerId?: string;
mobileFetch?: boolean;
}) {
const resultListEmployee = await employeeStore.fetchList({
customerId: opt?.customerId,
page: opt
? opt.mobileFetch
? 1
: opt.page || currentPageEmployee.value
: currentPageEmployee.value,
pageSize: opt
? opt.mobileFetch
? listEmployee.value.length +
(employeeStats.value === listEmployee.value.length ? 1 : 0)
: opt.pageSize || pageSize.value
: pageSize.value,
status:
props.currentStatus === 'All'
? undefined
: props.currentStatus === 'ACTIVE'
? 'ACTIVE'
: 'INACTIVE',
query: props.inputSearch,
passport: true,
visa: true,
startDate: props.searchDate[0],
endDate: props.searchDate[1],
});
if (resultListEmployee) {
maxPageEmployee.value = Math.ceil(
resultListEmployee.total / pageSize.value,
);
$q.screen.xs && !(opt && opt.mobileFetch)
? listEmployee.value.push(...resultListEmployee.result)
: (listEmployee.value = resultListEmployee.result);
}
if (opt && opt.fetchStats)
employeeStats.value = await employeeStore.getStatsEmployee();
}
async function openHistory(id: string) {
const res = await employeeStore.getEditHistory(id);
employeeHistory.value = res.reverse();
employeeHistoryDialog.value = true;
}
async function editEmployeeFormPersonal(id: string) {
await employeeFormStore.assignFormDataEmployee(id);
await props.fetchImageList(
id,
currentFromDataEmployee.value.selectedImage || '',
'employee',
);
employeeFormState.value.isEmployeeEdit = true;
employeeFormState.value.dialogType = 'edit';
employeeFormState.value.drawerModal = true;
}
async function openSpecificEmployee(id: string) {
await employeeFormStore.assignFormDataEmployee(id);
await props.fetchImageList(
id,
currentFromDataEmployee.value.selectedImage || '',
'employee',
);
employeeFormState.value.dialogType = 'info';
employeeFormState.value.drawerModal = true;
}
async function toggleStatusEmployee(
id: string,
status: boolean,
employeeName: string,
) {
const res = await employeeStore.editById(id, {
status: !status ? 'ACTIVE' : 'INACTIVE',
firstNameEN: employeeName,
});
if (res && employeeFormState.value.drawerModal) {
currentFromDataEmployee.value.status = res.status;
}
await employeeFormStore.assignFormDataEmployee(id);
await fetchListEmployee({ mobileFetch: $q.screen.xs });
flowStore.rotate();
}
watch(
() => [
props.inputSearch,
props.searchDate,
props.currentStatus,
pageSize.value,
],
async () => {
currentPageEmployee.value = 1;
listEmployee.value = [];
await fetchListEmployee({ fetchStats: true });
customerFormState.value.currentCustomerId = undefined;
flowStore.rotate();
},
);
watch(
() => employeeFormState.value.currentCustomerBranch,
(e) => {
if (!e) return;
if (employeeFormState.value.formDataEmployeeSameAddr) {
currentFromDataEmployee.value.address = e.address;
currentFromDataEmployee.value.addressEN = e.addressEN;
currentFromDataEmployee.value.provinceId = e.provinceId;
currentFromDataEmployee.value.districtId = e.districtId;
currentFromDataEmployee.value.subDistrictId = e.subDistrictId;
}
currentFromDataEmployee.value.customerBranchId = e.id;
},
);
watch(
() => employeeFormState.value.formDataEmployeeSameAddr,
(isSame) => {
if (!employeeFormState.value.currentCustomerBranch) return;
if (isSame) {
currentFromDataEmployee.value.address =
employeeFormState.value.currentCustomerBranch.address;
currentFromDataEmployee.value.addressEN =
employeeFormState.value.currentCustomerBranch.addressEN;
currentFromDataEmployee.value.provinceId =
employeeFormState.value.currentCustomerBranch.provinceId;
currentFromDataEmployee.value.districtId =
employeeFormState.value.currentCustomerBranch.districtId;
currentFromDataEmployee.value.subDistrictId =
employeeFormState.value.currentCustomerBranch.subDistrictId;
}
currentFromDataEmployee.value.customerBranchId =
employeeFormState.value.currentCustomerBranch.id;
},
);
watch(
() => currentFromDataEmployee.value.dateOfBirth,
(v) => {
const isEdit =
employeeFormState.value.drawerModal &&
employeeFormState.value.isEmployeeEdit;
let currentFormDate = v && toISOStringWithTimezone(new Date(v));
let currentDate: string = '';
if (isEdit && employeeFormState.value.currentEmployee) {
currentDate = toISOStringWithTimezone(
new Date(employeeFormState.value.currentEmployee.dateOfBirth),
);
}
if (
employeeFormState.value.dialogModal ||
(isEdit && currentFormDate !== currentDate)
) {
const age = calculateAge(
currentFromDataEmployee.value.dateOfBirth,
'year',
);
if (currentFromDataEmployee.value.dateOfBirth && Number(age) < 15) {
dialog({
color: 'warning',
icon: 'mdi-alert',
title: t('dialog.title.youngWorker15'),
cancelText: t('general.edit'),
persistent: true,
message: t('dialog.message.youngWorker15'),
cancel: async () => {
currentFromDataEmployee.value.dateOfBirth = null;
return;
},
});
}
if (
currentFromDataEmployee.value.dateOfBirth &&
Number(age) > 15 &&
Number(age) <= 18
) {
dialog({
color: 'warning',
icon: 'mdi-alert',
title: t('dialog.title.youngWorker18'),
cancelText: t('general.cancel'),
actionText: t('general.confirm'),
persistent: true,
message: t('dialog.message.youngWorker18'),
action: () => {},
cancel: async () => {
currentFromDataEmployee.value.dateOfBirth = null;
return;
},
});
}
}
},
);
watch(
() => currentFromDataEmployee.value.image,
() => {
if (currentFromDataEmployee.value.image !== null)
employeeFormState.value.isImageEdit = true;
},
);
onMounted(async () => {
currentPageEmployee.value = 1;
listEmployee.value = [];
await fetchListEmployee({ fetchStats: true });
});
</script>
<template>
<q-splitter
v-model="splitPercent"
:limits="[0, 100]"
class="col full-width"
before-class="overflow-hidden"
after-class="overflow-hidden"
:disable="$q.screen.lt.sm"
>
<template v-slot:before>
<div
class="column q-pa-md surface-1 full-height full-width"
style="gap: var(--size-1)"
>
<q-item
active
dense
active-class="employer-active"
class="no-padding items-center rounded full-width"
v-close-popup
clickable
>
<span class="q-px-md ellipsis">
{{ $t('general.all') }}
</span>
</q-item>
</div>
</template>
<template v-slot:after>
<div class="column full-height no-wrap">
<!-- employee -->
<template
v-if="listEmployee && employeeStats > 0 && currentTab === 'employee'"
>
<div
v-if="listEmployee.length === 0"
class="row col full-width items-center justify-center"
style="min-height: 250px"
>
<NoData :not-found="!!inputSearch" />
</div>
<article
v-if="listEmployee.length !== 0"
class="column scroll q-pa-md col"
>
<q-infinite-scroll
:offset="10"
@load="
(_, done) => {
if (
$q.screen.gt.xs ||
currentPageEmployee === maxPageEmployee
)
return;
currentPageEmployee = currentPageEmployee + 1;
fetchListEmployee().then(() =>
done(currentPageEmployee >= maxPageEmployee),
);
}
"
>
<TableEmpoloyee
:hide-delete="!canAccess('customer', 'edit')"
v-model:page-size="pageSize"
v-model:current-page="currentPageEmployee"
:grid-view="gridView"
:list-employee="listEmployee"
:columns-employee="columnsEmployee"
:field-selected="fieldSelected"
@history="
(item: any) => {
openHistory(item.id);
}
"
@view="
async (item: any) => {
employeeFormState.drawerModal = true;
employeeFormState.isEmployeeEdit = false;
employeeFormStore.assignFormDataEmployee(item.id);
await fetchImageList(
item.id,
item.selectedImage || '',
'employee',
);
}
"
@edit="(item: any) => editEmployeeFormPersonal(item.id)"
@delete="
(item: any) => {
deleteEmployeeById({
id: item.id,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId: customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
}
"
@toggle-status="
async (item: any) => {
triggerChangeStatus(item.id, item.status, item.firstNameEN);
}
"
/>
<template v-slot:loading>
<div
v-if="
$q.screen.lt.sm && currentPageEmployee !== maxPageEmployee
"
class="row justify-center"
>
<q-spinner-dots color="primary" size="40px" />
</div>
</template>
</q-infinite-scroll>
</article>
<footer
v-if="listEmployee.length !== 0 && $q.screen.gt.xs"
class="row justify-between items-center q-px-md q-py-sm"
>
<div class="row col-4 items-center">
<div
class="app-text-muted"
style="width: 80px"
v-if="$q.screen.gt.sm"
>
{{ $t('general.recordPerPage') }}
</div>
<div><PaginationPageSize v-model="pageSize" /></div>
</div>
<div class="col-4 flex justify-center app-text-muted">
{{
$q.screen.gt.sm
? $t('general.recordsPage', {
resultcurrentPage: listEmployee.length,
total: employeeStats,
})
: $t('general.ofPage', {
current: listEmployee.length,
total: employeeStats,
})
}}
</div>
<div class="col-4 flex justify-end">
<PaginationComponent
v-model:current-page="currentPageEmployee"
v-model:max-page="maxPageEmployee"
:fetch-data="
async () => {
await fetchListEmployee();
flowStore.rotate();
}
"
/>
</div>
</footer>
</template>
</div>
</template>
</q-splitter>
<!-- add employee -->
<DialogEmployee
:fetch-list-employee="fetchListEmployee"
:fetch-image-list="fetchImageList"
:current-tab="currentTab"
/>
<!-- กจาง edit employee -->
<DrawerEmployee
:fetch-list-employee="fetchListEmployee"
:fetch-image-list="fetchImageList"
:current-tab="currentTab"
@change-status="
(s) =>
triggerChangeStatus(
currentFromDataEmployee.id,
s,
currentFromDataEmployee.firstNameEN,
)
"
/>
<DialogForm
:title="$t('general.historyEdit')"
hide-footer
v-model:modal="employeeHistoryDialog"
>
<div class="q-pa-md">
<HistoryEditComponent
v-if="employeeHistory"
v-model:history-list="employeeHistory"
/>
</div>
</DialogForm>
</template>
<style scoped>
.employer-active {
background-color: hsla(var(--info-bg) / 0.1);
color: hsl(var(--info-bg));
}
</style>

View file

@ -137,7 +137,12 @@ watch(
@update:model-value="
(v) => (typeof v === 'string' ? (registerName = v.trim()) : '')
"
:rules="[(val: string) => !!val || $t('form.error.required')]"
:rules="[
(val) => !!val || $t('form.error.required'),
(val) =>
/^[A-Za-z0-9ก-๙\s&.,'()-]+$/.test(val) ||
$t('form.error.branchNameField'),
]"
/>
<q-input
@ -153,29 +158,15 @@ watch(
(v) => (typeof v === 'string' ? (registerNameEN = v.trim()) : '')
"
:rules="[
(val: string) =>
val === '' ||
/^[0-9A-Za-z\s.,]+$/.test(val) ||
$t('form.error.letterOnly'),
(val) => !!val || $t('form.error.required'),
(val) =>
/^[A-Za-z0-9\s&.,'-]+$/.test(val) ||
$t('form.error.branchNameENField'),
]"
/>
</div>
<div class="col-12 row q-col-gutter-sm">
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-12 col-md-5"
:label="$t('customer.form.employerName')"
for="input-legal-person-no"
:model-value="customerName"
@update:model-value="
(v) => (typeof v === 'string' ? (customerName = v.trim()) : '')
"
/>
<q-input
dense
outlined
@ -345,6 +336,7 @@ watch(
(v) => (typeof v === 'string' ? (prefixName = v) : '')
"
@clear="prefixName = ''"
:rules="[(val: string) => !!val || $t('form.error.required')]"
>
<template v-slot:no-option>
<q-item>
@ -393,11 +385,11 @@ watch(
:readonly="readonly"
:disable="!readonly"
class="col-md-2 col-6"
label="Title"
label="Prefix"
:model-value="
readonly
? capitalize(prefixName || '') || '-'
: capitalize(prefixName || '')
? prefixName.toUpperCase() || '-'
: prefixName.toUpperCase() || ''
"
@update:model-value="
(v) => (typeof v === 'string' ? (prefixName = v) : '')

View file

@ -2,6 +2,7 @@
import useOptionStore from 'stores/options';
import SelectBranch from 'src/components/shared/select/SelectBranch.vue';
import { isRoleInclude } from 'src/stores/utils';
import SelectBusinessType from 'src/components/shared/select/SelectBusinessType.vue';
withDefaults(
defineProps<{
@ -102,7 +103,13 @@ const telephoneNo = defineModel<string>('telephoneNo', { default: '' });
class="col-md-6"
:readonly
:disabled="
!isRoleInclude(['admin', 'system', 'head_of_admin']) && !readonly
!isRoleInclude([
'admin',
'system',
'head_of_admin',
'executive',
'accountant',
]) && !readonly
"
:label="$t('customer.form.registeredBranch')"
select-first-value
@ -136,15 +143,10 @@ const telephoneNo = defineModel<string>('telephoneNo', { default: '' });
for="input-tax"
v-model="legalPersonNo"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
<SelectBusinessType
class="col-6 col-md-3"
:label="$t('customer.table.businessTypePure')"
for="input-business-type"
:model-value="optionStore.mapOption(businessType)"
v-model:value="businessType"
:readonly
/>
</div>
@ -173,15 +175,10 @@ const telephoneNo = defineModel<string>('telephoneNo', { default: '' });
:label="$t('personnel.form.citizenId')"
for="input-citizen-id"
/>
<q-input
dense
outlined
:readonly="readonly"
hide-bottom-space
<SelectBusinessType
class="col-6 col-md-3"
:label="$t('customer.table.businessTypePure')"
for="input-first-name-en"
:model-value="optionStore.mapOption(businessType)"
v-model:value="businessType"
:readonly
/>
</div>

View file

@ -7,6 +7,8 @@ import { CustomerCreate } from 'stores/customer/types';
import EmployerFormAbout from './EmployerFormAbout.vue';
import EmployerFormAuthorized from './EmployerFormAuthorized.vue';
import { waitAll } from 'src/stores/utils';
import FormEmployeePassport from 'src/components/03_customer-management/FormEmployeePassport.vue';
import FormEmployeeVisa from 'src/components/03_customer-management/FormEmployeeVisa.vue';
import {
FormCitizen,
CorpFormBusinessRegistration,
@ -51,6 +53,7 @@ withDefaults(
actionDisabled?: boolean;
customerType?: 'CORP' | 'PERS';
hideAction?: boolean;
hideDelete?: boolean;
}>(),
{
hideAction: false,
@ -81,7 +84,7 @@ withDefaults(
/>
<DeleteButton
icon-only
v-if="readonly"
v-if="readonly && !hideDelete"
@click="$emit('delete')"
type="button"
:disabled="actionDisabled"
@ -141,7 +144,6 @@ withDefaults(
v-model:last-name-en="item.lastNameEN"
v-model:gender="item.gender"
v-model:birth-date="item.birthDate"
v-model:customer-name="item.customerName"
v-model:legal-person-no="item.legalPersonNo"
v-model:branch-code="item.code"
v-model:register-name="item.registerName"
@ -158,7 +160,7 @@ withDefaults(
outlined
:prefix-id="prefixId || 'employer'"
:readonly="readonly"
v-model:bussiness-type="item.businessType"
v-model:business-type-id="item.businessTypeId"
v-model:job-position="item.jobPosition"
v-model:job-description="item.jobDescription"
v-model:pay-date="item.payDate"
@ -220,7 +222,6 @@ withDefaults(
hide-action
:ocr="
async (group, file) => {
console.log(group);
if (group !== 'attachment') {
const res = await ocrStore.sendOcr({
file: file,
@ -245,12 +246,20 @@ withDefaults(
:auto-save="item.id !== ''"
:download="
(obj) => {
customerStore.getFile({
parentId: item.id || '',
group: obj.group,
fileId: obj._meta.id,
download: true,
});
if (obj.group === 'citizen') {
customerStore.getFile({
parentId: item.id || '',
group: obj.group,
fileId: obj._meta.id,
download: true,
});
} else {
customerStore.getAttachment({
parentId: item.id || '',
name: obj._meta.id,
download: true,
});
}
}
"
:delete-item="
@ -279,7 +288,7 @@ withDefaults(
_meta: any,
file: File | undefined,
) => {
if (group !== 'attachment') {
if (group === 'citizen') {
if (file !== undefined && item.id) {
const res = await customerStore.postMeta({
parentId: item.id || '',
@ -347,7 +356,6 @@ withDefaults(
});
const tempValue = resMeta.map(async (v: any) => {
console.log(v.expireDate);
return {
_meta: { ...v },
name: `${group}-${dateFormat(v.expireDate)}`,
@ -368,7 +376,6 @@ withDefaults(
});
const tempValue = (res as string[]).map(async (i: any) => {
console.log(i);
return {
_meta: { id: i, name: i },
name: i || '',

View file

@ -8,12 +8,28 @@ import { onMounted, watch } from 'vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import ThaiBahtText from 'thai-baht-text';
import SelectBusinessType from 'src/components/shared/select/SelectBusinessType.vue';
import BusinessTypeDialog from 'src/pages/16_business-type-management/BusinessTypeDialog.vue';
import useBusinessTypeStore from 'src/stores/business-type';
const { locale } = useI18n({ useScope: 'global' });
const rawOption = ref();
const bussinessType = defineModel<string>('bussinessType');
const businessTypeDialog = ref<boolean>(false);
const formDataBusinessType = ref<{
name: string;
nameEN: string;
}>({
name: '',
nameEN: '',
});
const useBusinessType = useBusinessTypeStore();
const businessTypeId = defineModel<string>('businessTypeId');
const jobPosition = defineModel<string>('jobPosition');
const jobDescription = defineModel<string>('jobDescription');
const payDate = defineModel<string>('payDate');
@ -28,6 +44,8 @@ const typeBusinessENOption = ref([]);
const jobPositionOption = ref([]);
const jobPositionENOption = ref([]);
const keySelect = ref<number>(0);
defineProps<{
title?: string;
dense?: boolean;
@ -37,35 +55,33 @@ defineProps<{
showTitle?: boolean;
}>();
function resetFormBusinessType() {
businessTypeDialog.value = false;
formDataBusinessType.value = { name: '', nameEN: '' };
}
async function submitBusinessType() {
const res = await useBusinessType.create(formDataBusinessType.value);
if (res) {
businessTypeId.value = res.id;
resetFormBusinessType();
keySelect.value++;
}
}
onMounted(async () => {
const resultOption = await fetch('/option/option.json');
rawOption.value = await resultOption.json();
typeBusinessENOption.value = rawOption.value.eng.businessType;
jobPositionENOption.value = rawOption.value.eng.position;
if (locale.value === 'eng') {
typeBusinessOption.value = rawOption.value.eng.businessType;
jobPositionOption.value = rawOption.value.eng.position;
}
if (locale.value === 'tha') {
typeBusinessOption.value = rawOption.value.tha.businessType;
jobPositionOption.value = rawOption.value.tha.position;
}
});
watch([typeBusinessOption, typeBusinessENOption], () => {
typeBusinessFilter = selectFilterOptionRefMod(
typeBusinessOption,
typeBusinessOptions,
'label',
);
typeBusinessENFilter = selectFilterOptionRefMod(
typeBusinessENOption,
typeBusinessENOptions,
'label',
);
});
watch(
() => rate.value,
(newValue) => {
@ -135,69 +151,26 @@ let jobPositionENFilter = selectFilterOptionRefMod(
<span>{{ $t('customerBranch.tab.business') }}</span>
</div>
<q-select
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
:hide-dropdown-icon="readonly"
input-debounce="0"
option-value="value"
option-label="label"
v-model="bussinessType"
<SelectBusinessType
:key="keySelect"
class="col-md-6 col-12"
:dense="dense"
:readonly="readonly"
:label="$t('customer.form.businessType')"
:options="typeBusinessOptions"
:for="`${prefixId}-select-business-type`"
@filter="typeBusinessFilter"
v-model:value="businessTypeId"
:readonly
creatable
lang="tha"
:rules="[(val: string) => !!val || $t('form.error.required')]"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
<q-select
:for="`${prefixId}-input-bussiness-type-en`"
:id="`${prefixId}-input-bussiness-type-en`"
:label="`${$t('customer.form.businessType')} (EN)`"
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
:hide-dropdown-icon="readonly"
input-debounce="0"
option-value="value"
option-label="label"
v-model="bussinessType"
@create="() => (businessTypeDialog = true)"
/>
<SelectBusinessType
:key="keySelect"
class="col-md-6 col-12"
:dense="dense"
:readonly="readonly"
:options="typeBusinessENOptions"
@filter="typeBusinessENFilter"
v-model:value="businessTypeId"
:readonly
creatable
lang="eng"
:rules="[(val: string) => !!val || $t('form.error.required')]"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
@create="() => (businessTypeDialog = true)"
/>
<q-select
outlined
@ -348,4 +321,12 @@ let jobPositionENFilter = selectFilterOptionRefMod(
"
/>
</div>
<BusinessTypeDialog
ref="refBusinessTypeDialog"
@close="resetFormBusinessType()"
@submit="submitBusinessType()"
v-model="businessTypeDialog"
v-model:data="formDataBusinessType"
/>
</template>

View file

@ -9,8 +9,6 @@ const contactName = defineModel<string>('contactName');
const email = defineModel<string>('email');
const contactTel = defineModel<string>('contactTel');
const officeTel = defineModel<string>('officeTel');
const agent = defineModel<string>('agent');
const agentUserId = defineModel<string>('agentUserId');
</script>
@ -109,7 +107,6 @@ const agentUserId = defineModel<string>('agentUserId');
/>
</template>
</q-input>
<SelectAgent
:label="$t('customer.form.agent')"
v-model:value="agentUserId"

View file

@ -69,19 +69,19 @@ export const uploadFileListCustomer: {
}[] = [
{
label: 'customer.typeFile.citizenId',
value: 'citizen',
group: 'citizen',
value: 'citizen',
},
{
label: 'customer.typeFile.registrationBook',
value: 'attachment',
group: 'houseRegistration',
value: 'attachment',
},
{
label: 'customer.typeFile.houseMap',
value: 'attachment',
group: 'vatRegistration',
value: 'attachment',
},
{
@ -92,7 +92,7 @@ export const uploadFileListCustomer: {
{
label: 'customer.typeFile.dbdCertificate',
group: 'powerOfAttorney',
group: 'dbdCertificate',
value: 'attachment',
},
@ -144,43 +144,43 @@ export const uploadFileListEmployee: {
},
{
label: 'customerEmployee.fileType.tm6',
value: 'attachment',
group: 'tm6',
value: 'attachment',
},
{
label: 'customerEmployee.fileType.workPermit',
value: 'attachment',
group: 'workPermit',
value: 'attachment',
},
{
label: 'customerEmployee.fileType.noticeJobEmployment',
value: 'attachment',
group: 'noticeJobEmployment',
value: 'attachment',
},
{
label: 'customerEmployee.fileType.noticeJobEntry',
value: 'attachment',
group: 'noticeJobEntry',
value: 'attachment',
},
{
label: 'customerEmployee.fileType.historyJob',
value: 'attachment',
group: 'historyJob',
value: 'attachment',
},
{
label: 'customerEmployee.fileType.acceptJob',
value: 'attachment',
group: 'acceptJob',
value: 'attachment',
},
{
label: 'customerEmployee.fileType.receipt',
value: 'attachment',
group: 'receipt',
value: 'attachment',
},
{
label: 'customerEmployee.fileType.other',
value: 'attachment',
group: 'other',
value: 'attachment',
},
];

View file

@ -22,6 +22,10 @@ import { useRoute } from 'vue-router';
export const useCustomerForm = defineStore('form-customer', () => {
const customerStore = useCustomerStore();
const onCreateImageList = ref<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>({ selectedImage: '', list: [] });
const { t } = useI18n();
const flowStore = useFlowStore();
@ -30,11 +34,13 @@ export const useCustomerForm = defineStore('form-customer', () => {
const registerAbleBranchOption = ref<{ id: string; name: string }[]>();
const currentBranchRootId = ref<string>('');
const tabFieldRequired = ref<{
[key: string]: (keyof CustomerBranchCreate)[];
}>({
main: [],
business: ['businessType', 'jobPosition'],
business: ['businessTypeId', 'jobPosition'],
address: [
'address',
'addressEN',
@ -82,6 +88,7 @@ export const useCustomerForm = defineStore('form-customer', () => {
formDataOcr: Record<string, any>;
isImageEdit: boolean;
currentCustomerId?: string;
imageList: { list: string[]; selectedImage: string };
}>({
dialogType: 'info',
dialogOpen: false,
@ -98,6 +105,7 @@ export const useCustomerForm = defineStore('form-customer', () => {
treeFile: [],
formDataOcr: {},
isImageEdit: false,
imageList: { list: [], selectedImage: '' },
});
watch(
@ -160,6 +168,7 @@ export const useCustomerForm = defineStore('form-customer', () => {
state.value.editCustomerCode = data.code;
state.value.customerImageUrl = `${baseUrl}/customer/${id}/image/${data.selectedImage}`;
state.value.defaultCustomerImageUrl = `${baseUrl}/customer/${id}/image/${data.selectedImage}`;
currentBranchRootId.value = data.branch[0].id || '';
resetFormData.registeredBranchId = data.registeredBranchId;
resetFormData.status = data.status;
@ -181,7 +190,7 @@ export const useCustomerForm = defineStore('form-customer', () => {
payDate: v.payDate,
jobDescription: v.jobDescription,
jobPosition: v.jobPosition,
businessType: v.businessType,
businessTypeId: v.businessTypeId,
employmentOffice: v.employmentOffice,
employmentOfficeEN: v.employmentOfficeEN,
telephoneNo: v.telephoneNo,
@ -218,7 +227,6 @@ export const useCustomerForm = defineStore('form-customer', () => {
contactTel: v.contactTel,
officeTel: v.officeTel,
agentUserId: v.agentUserId || undefined,
customerName: v.customerName,
authorizedName: v.authorizedName,
authorizedNameEN: v.authorizedNameEN,
@ -256,7 +264,6 @@ export const useCustomerForm = defineStore('form-customer', () => {
async function addCurrentCustomerBranch() {
if (currentFormData.value.customerBranch?.some((v) => !v.id)) return;
currentFormData.value.customerBranch?.push({
id: '',
customerId: '',
branchCode:
currentFormData.value.customerBranch.length !== 0
@ -290,8 +297,8 @@ export const useCustomerForm = defineStore('form-customer', () => {
birthDate:
currentFormData.value.customerBranch?.at(0)?.birthDate || undefined,
businessType:
currentFormData.value.customerBranch?.at(0)?.businessType || '',
businessTypeId:
currentFormData.value.customerBranch?.at(0)?.businessTypeId || '',
jobPosition:
currentFormData.value.customerBranch?.at(0)?.jobPosition || '',
jobDescription:
@ -328,8 +335,6 @@ export const useCustomerForm = defineStore('form-customer', () => {
currentFormData.value.customerBranch?.at(0)?.agentUserId || undefined,
status: 'CREATED',
customerName:
currentFormData.value.customerBranch?.at(0)?.customerName || '',
registerName:
currentFormData.value.customerBranch?.at(0)?.registerName || '',
registerNameEN:
@ -498,11 +503,14 @@ export const useCustomerForm = defineStore('form-customer', () => {
}
return {
onCreateImageList,
tabFieldRequired,
registerAbleBranchOption,
state,
resetFormData,
currentFormData,
currentBranchRootId,
isFormDataDifferent,
resetForm,
assignFormData,
@ -541,7 +549,7 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
gender: '',
birthDate: undefined,
businessType: '',
businessTypeId: '',
jobPosition: '',
jobDescription: '',
payDate: '',
@ -571,7 +579,6 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
agentUserId: undefined,
status: 'CREATED',
customerName: '',
registerName: '',
registerNameEN: '',
registerDate: undefined,
@ -623,7 +630,7 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
payDateEN: _data.payDateEN,
jobDescription: _data.jobDescription,
jobPosition: _data.jobPosition,
businessType: _data.businessType,
businessTypeId: _data.businessTypeId,
employmentOffice: _data.employmentOffice,
employmentOfficeEN: _data.employmentOfficeEN,
telephoneNo: _data.telephoneNo,
@ -657,7 +664,6 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
officeTel: _data.officeTel,
agentUserId: _data.agentUserId || undefined,
codeCustomer: _data.codeCustomer,
customerName: _data.customerName,
homeCode: _data.homeCode,
authorizedName: _data.authorizedName,
authorizedNameEN: _data.authorizedNameEN,
@ -784,6 +790,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
}
| undefined;
ocr: boolean;
imageList: { list: string[]; selectedImage: string };
}>({
currentBranchId: '',
isImageEdit: false,
@ -808,9 +815,10 @@ export const useEmployeeForm = defineStore('form-employee', () => {
infoEmployeePersonCard: [],
formDataEmployeeOwner: undefined,
ocr: false,
imageList: { list: [], selectedImage: '' },
});
const defaultFormData: EmployeeCreate = {
const defaultFormData: EmployeeCreate & { image?: File } = {
id: '',
code: '',
customerBranchId: '',
@ -892,6 +900,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
expireDate: new Date(),
remark: undefined,
workerType: '',
reportDate: null,
number: '',
},
],
@ -937,7 +946,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
};
let resetEmployeeData = structuredClone(defaultFormData);
const currentFromDataEmployee = ref<EmployeeCreate>(
const currentFromDataEmployee = ref<EmployeeCreate & { image?: File }>(
structuredClone(defaultFormData),
);
@ -959,12 +968,14 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state.value.currentIndexVisa = -1;
state.value.currentIndexCheckup = -1;
state.value.currentIndexWorkHistory = -1;
state.value.currentTab = 'personalInfo';
state.value.imageList = { list: [], selectedImage: '' };
// state.value.currentTab = 'personalInfo';
if (clean) {
state.value.formDataEmployeeOwner = undefined;
resetEmployeeData = structuredClone(defaultFormData);
state.value.statusSavePersonal = false;
state.value.profileUrl = '';
state.value.currentBranchId = '';
} else {
resetEmployeeData.selectedImage =
currentFromDataEmployee.value.selectedImage;
@ -985,12 +996,16 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state.value.currentIndexPassport
].id === undefined
) {
const { id, employeeId, updatedAt, createdAt, file, ...payload } =
currentFromDataEmployee.value.employeePassport?.[
state.value.currentIndexPassport
];
const res = await employeeStore.postMeta({
parentId: currentFromDataEmployee.value.id || '',
group: 'passport',
meta: currentFromDataEmployee.value.employeePassport?.[
state.value.currentIndexPassport
],
meta: payload,
file: file,
});
if (res) {
@ -1004,7 +1019,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state.value.currentIndexPassport
].id !== undefined
) {
const { id, employeeId, updatedAt, createdAt, ...payload } =
const { id, employeeId, updatedAt, createdAt, file, ...payload } =
currentFromDataEmployee.value.employeePassport?.[
state.value.currentIndexPassport
];
@ -1017,6 +1032,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state.value.currentIndexPassport
].id || '',
meta: payload,
file: file || undefined,
});
}
@ -1244,7 +1260,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
await assignFormDataEmployee(currentFromDataEmployee.value.id);
}
async function submitPersonal(imgList: {
async function submitPersonal(imgList?: {
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}) {
@ -1265,6 +1281,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
currentFromDataEmployee.value.lastNameEN.trim();
if (state.value.dialogType === 'create') {
delete currentFromDataEmployee.value.image;
const res = await employeeStore.create(
{
...currentFromDataEmployee.value,
@ -1293,7 +1310,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state.value.currentEmployee?.status === 'CREATED'
? 'ACTIVE'
: state.value.currentEmployee?.status,
customerBranchId: state.value.formDataEmployeeOwner?.id || '',
customerBranchId: state.value.currentBranchId || '',
employeeWork: [],
employeeCheckup: [],
employeeOtherInfo: undefined,
@ -1381,12 +1398,10 @@ export const useEmployeeForm = defineStore('form-employee', () => {
statusSave: true,
})),
),
employeeOtherInfo: structuredClone(
{
...payload.employeeOtherInfo,
statusSave: !!payload.employeeOtherInfo?.id ? true : false,
} || {},
),
employeeOtherInfo: structuredClone({
...(payload.employeeOtherInfo ?? {}),
statusSave: true,
}),
employeeWork: structuredClone(
payload.employeeWork?.length === 0
? state.value.dialogModal
@ -1535,6 +1550,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
expireDate: new Date(),
remark: undefined,
workerType: '',
reportDate: null,
number: '',
});
@ -1659,6 +1675,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state,
currentFromDataEmployee,
resetEmployeeData,
addPassport,
addVisa,
addCheckup,

View file

@ -43,6 +43,7 @@ withDefaults(
defineProps<{
readonly?: boolean;
isEdit?: boolean;
hideAction?: boolean;
}>(),
{ readonly: false, isEdit: false },
);
@ -207,7 +208,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
style="position: absolute; z-index: 999; top: 0; right: 0"
>
<div
v-if="flowData.status !== 'INACTIVE'"
v-if="flowData.status !== 'INACTIVE' && !hideAction"
class="surface-1 row rounded"
>
<UndoButton
@ -287,6 +288,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
>
<template v-slot:btn-form-flow-step-drawer>
<q-btn
v-if="!hideAction"
dense
flat
icon="mdi-plus"
@ -315,6 +317,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
<FormFlow
:readonly
onDrawer
:hide-action="hideAction"
v-model:user-in-table="userInTable"
v-model:flow-data="flowData"
v-model:register-branch-id="registerBranchId"

View file

@ -11,7 +11,7 @@ import {
} from 'src/stores/workflow-template/types';
import { useWorkflowTemplate } from 'src/stores/workflow-template';
import { useNavigator } from 'src/stores/navigator';
import { dialog } from 'src/stores/utils';
import { dialog, canAccess } from 'src/stores/utils';
import FloatingActionButton from 'components/FloatingActionButton.vue';
import StatCardComponent from 'src/components/StatCardComponent.vue';
@ -334,6 +334,7 @@ watch(
</script>
<template>
<FloatingActionButton
v-if="canAccess('workflow', 'edit')"
style="z-index: 999"
hide-icon
@click="triggerDialog('add')"
@ -682,6 +683,7 @@ watch(
"
/>
<KebabAction
v-if="canAccess('workflow', 'edit')"
:id-name="props.row.name"
:status="props.row.status"
@view="
@ -763,7 +765,8 @@ watch(
"
/>
<KebabAction
:id-name="props.row.id"
v-if="canAccess('workflow', 'edit')"
:id-name="props.row.name"
:status="props.row.status"
@view="
() => {
@ -846,6 +849,7 @@ watch(
@drawer-undo="undo"
@close="resetForm"
@submit="submit"
:hide-action="!canAccess('workflow', 'edit')"
:readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal"

View file

@ -41,7 +41,7 @@ import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import useFlowStore from 'stores/flow';
import { dateFormat } from 'src/utils/datetime';
import { formatNumberDecimal, isRoleInclude, notify } from 'stores/utils';
import { formatNumberDecimal, isRoleInclude, canAccess } from 'stores/utils';
const { getWorkflowTemplate } = useWorkflowTemplate();
import { Status } from 'stores/types';
@ -60,6 +60,7 @@ import {
ServiceById,
WorkItems,
Attributes,
WorkCreate,
} from 'stores/product-service/types';
import { computed } from 'vue';
import {
@ -101,6 +102,8 @@ const {
deleteWork,
importProduct,
productExport,
} = productServiceStore;
const { workNameItems } = storeToRefs(productServiceStore);
@ -142,27 +145,25 @@ const { t } = useI18n();
const baseUrl = ref<string>(import.meta.env.VITE_API_BASE_URL);
const priceDisplay = computed(() => ({
price: !isRoleInclude(['sale_agent']),
// price: !isRoleInclude(['sale_agent']),
price: true,
agentPrice: isRoleInclude([
'admin',
'head_of_admin',
'head_of_sale',
'system',
'owner',
'head_of_admin',
'admin',
'executive',
'accountant',
'sale_agent',
'head_of_sale',
]),
serviceCharge: isRoleInclude([
'admin',
'head_of_admin',
'system',
'owner',
'head_of_admin',
'admin',
'executive',
'accountant',
]),
}));
const actionDisplay = computed(() =>
isRoleInclude(['admin', 'head_of_admin', 'system', 'owner', 'accountant']),
);
const actionDisplay = computed(() => canAccess('product', 'edit'));
const splitterModel = computed(() =>
$q.screen.lt.md ? (productMode.value !== 'group' ? 0 : 100) : 25,
);
@ -1170,6 +1171,7 @@ function clearFormService() {
profileSubmit.value = false;
imageProduct.value = undefined;
profileFileImg.value = null;
serviceTab.value = 1;
}
function sameFormService() {
@ -1396,6 +1398,7 @@ function submitAddWorkProduct() {
if (!s.hasOwnProperty('productsId')) {
s.productsId = [];
}
if (s.productsId.length === 0) return;
s.productsId.push(i.id);
},
);
@ -1448,17 +1451,11 @@ function confirmDeleteWork(id: string, noDialog?: boolean) {
}
}
function triggerConfirmCloseWork() {
function triggerConfirmCloseWorkName() {
dialogWarningClose(t, {
message: t('dialog.message.warningClose'),
action: () => {
manageWorkNameDialog.value = false;
if (workNameItems.value[workNameItems.value.length - 1].name === '') {
confirmDeleteWork(
workNameItems.value[workNameItems.value.length - 1].id,
true,
);
}
},
cancel: () => {},
});
@ -1888,6 +1885,39 @@ async function copy(id: string) {
dialogService.value = true;
}
function addWorkName(data: { name: string; order: number }) {
workNameItems.value.push({ id: '', name: data.name, isEdit: true });
}
async function submitWorkName(
workId: string,
data: Partial<WorkCreate & { status: string }>,
) {
if (workNameItems.value.length === 0) return;
if (!workId) await createWork({ ...data, order: 1 });
else await editWork(workId, data);
}
async function triggerExport() {
productExport({
pageSize: 100_000,
productGroupId: currentIdGroup.value,
query: !!inputSearchProductAndService.value
? inputSearchProductAndService.value
: undefined,
status:
currentStatus.value === 'INACTIVE'
? 'INACTIVE'
: currentStatus.value === 'ACTIVE'
? 'ACTIVE'
: undefined,
startDate: searchDate.value[0] ? new Date(searchDate.value[0]) : undefined,
endDate: searchDate.value[1] ? new Date(searchDate.value[1]) : undefined,
});
}
watch(
() => formService.value.attributes.workflowId,
async (a, b) => {
@ -2221,7 +2251,6 @@ watch(
</AdvanceSearch>
</template>
</q-input>
<div class="row col-md-6" style="white-space: nowrap">
<q-select
v-show="$q.screen.gt.sm"
@ -2456,7 +2485,10 @@ watch(
)
"
>
{{ props.row.detail || '-' }}
{{
props.row.detail.replace(/<\/?[^>]+(>|$)/g, '') ||
'-'
}}
</q-td>
<q-td
@ -2506,6 +2538,7 @@ watch(
/>
<KebabAction
v-if="actionDisplay"
:disable-delete="props.row.status !== 'CREATED'"
:status="props.row.status"
:id-name="props.row.name"
@ -2648,7 +2681,15 @@ watch(
{{ $t('general.recordPerPage') }}
</div>
<div>
<PaginationPageSize v-model="pageSizeGroup" />
<PaginationPageSize
v-model="pageSizeGroup"
:fetch-data="
async () => {
await fetchListGroups();
flowStore.rotate();
}
"
/>
</div>
</div>
</div>
@ -2760,6 +2801,13 @@ watch(
}
"
/>
<SaveButton
icon-only
:icon="'material-symbols:download'"
color="var(--info-bg)"
@click.stop="triggerExport()"
/>
</div>
<div class="row col-md-6" style="white-space: nowrap">
@ -3252,6 +3300,7 @@ watch(
"
/>
<KebabAction
v-if="actionDisplay"
:use-copy="productAndServiceTab === 'service'"
:status="props.row.status"
:id-name="props.row.name"
@ -3406,7 +3455,15 @@ watch(
{{ $t('general.recordPerPage') }}
</div>
<div>
<PaginationPageSize v-model="pageSizeServiceAndProduct" />
<PaginationPageSize
v-model="pageSizeServiceAndProduct"
:fetch-data="
async () => {
await alternativeFetch();
flowStore.rotate();
}
"
/>
</div>
</div>
</div>
@ -3538,7 +3595,7 @@ watch(
</div>
<div
class="col-12 col-md-10"
id="customer-form-content"
id="group-create"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-py-md q-px-lg': !$q.screen.gt.sm,
@ -4057,7 +4114,7 @@ watch(
</div>
<div
class="col-12 col-md-10"
id="customer-form-content"
id="product-create"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
@ -4283,7 +4340,7 @@ watch(
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
}"
id="customer-form-content"
id="product-info"
style="height: 100%; max-height: 100%; overflow-y: auto"
>
<BasicInfoProduct
@ -4328,6 +4385,7 @@ watch(
<!-- add service -->
<DialogForm
v-if="dialogService"
hide-footer
no-address
no-app-box
@ -4464,6 +4522,11 @@ watch(
sub: true,
}))
"
:active="{
background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)',
}"
scroll-element="#service-create"
/>
</div>
<span
@ -4486,7 +4549,7 @@ watch(
</div>
<div
class="col-12 col-md-10"
id="customer-form-content"
id="service-create"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,
@ -4667,7 +4730,7 @@ watch(
? workNameRef.isWorkNameEdit()
: false;
if (isWorkNameEdit) {
triggerConfirmCloseWork();
triggerConfirmCloseWorkName();
return true;
}
return false;
@ -4679,15 +4742,16 @@ watch(
ref="workNameRef"
v-model:name-list="workNameItems"
@delete="confirmDeleteWork"
@edit="editWork"
@add="createWork"
@edit="submitWorkName"
@add="addWorkName"
/>
</div>
</DialogForm>
<!-- edit service -->
<!-- edit service edit package-->
<!-- :edit="!(formDataProductService.status === 'INACTIVE')" -->
<DialogForm
v-if="dialogServiceEdit"
hide-footer
no-address
height="95vh"
@ -4940,6 +5004,7 @@ watch(
background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)',
}"
scroll-element="#service-info"
/>
</div>
<span
@ -4962,7 +5027,7 @@ watch(
</div>
<div
class="col-12 col-md-10"
id="customer-form-content"
id="service-info"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm,

View file

@ -14,7 +14,7 @@ import { FloatingActionButton, PaginationComponent } from 'src/components';
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import PropertyDialog from './PropertyDialog.vue';
import { Property } from 'src/stores/property/types';
import { dialog, toCamelCase } from 'src/stores/utils';
import { dialog, toCamelCase, canAccess } from 'src/stores/utils';
import CreateButton from 'src/components/AddButton.vue';
import useOptionStore from 'stores/options';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
@ -331,6 +331,7 @@ watch(
</script>
<template>
<FloatingActionButton
v-if="canAccess('workflow', 'edit')"
style="z-index: 999"
hide-icon
@click="triggerDialog('add')"
@ -536,11 +537,19 @@ watch(
class="col surface-2 flex items-center justify-center"
>
<NoData
v-if="pageState.total !== 0 || pageState.searchDate.length > 0"
v-if="
pageState.total !== 0 ||
pageState.searchDate.length > 0 ||
!canAccess('workflow', 'edit')
"
:not-found="!!pageState.inputSearch"
/>
<CreateButton
v-if="pageState.total === 0 && pageState.searchDate.length === 0"
v-if="
pageState.total === 0 &&
pageState.searchDate.length === 0 &&
canAccess('workflow', 'edit')
"
@click="triggerDialog('add')"
label="general.add"
:i18n-args="{ text: $t('flow.title') }"
@ -698,6 +707,7 @@ watch(
"
/>
<KebabAction
v-if="canAccess('workflow', 'edit')"
:id-name="props.row.name"
:status="props.row.status"
@view="
@ -815,6 +825,7 @@ watch(
"
/>
<KebabAction
v-if="canAccess('workflow', 'edit')"
:id-name="props.row.id"
:status="props.row.status"
@view="
@ -906,6 +917,7 @@ watch(
@drawer-undo="() => undo()"
@close="() => resetForm()"
@submit="() => submit()"
:hide-action="!canAccess('workflow', 'edit')"
:readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal"

View file

@ -30,6 +30,7 @@ withDefaults(
defineProps<{
readonly?: boolean;
isEdit?: boolean;
hideAction?: boolean;
}>(),
{ readonly: false, isEdit: false },
);
@ -151,7 +152,7 @@ defineEmits<{
style="position: absolute; z-index: 999; top: 0; right: 0"
>
<div
v-if="propertyData.status !== 'INACTIVE'"
v-if="propertyData.status !== 'INACTIVE' && !hideAction"
class="surface-1 row rounded"
>
<UndoButton
@ -236,6 +237,7 @@ defineEmits<{
<FormProperty
onDrawer
:readonly="!isEdit"
:disable-toggle="hideAction"
v-model:name="formProperty.name"
v-model:name-en="formProperty.nameEN"
v-model:type="formProperty.type"

View file

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { onMounted, reactive, ref, watch, computed } from 'vue';
import { onMounted, onUnmounted, reactive, ref, watch, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
@ -14,7 +14,8 @@ import useMyBranch from 'stores/my-branch';
import { useQuotationForm } from './form';
import { hslaColors } from './constants';
import { pageTabs, columnQuotation } from './constants';
import { toCamelCase } from 'stores/utils';
import { toCamelCase, canAccess } from 'stores/utils';
import { getUserId } from 'src/services/keycloak';
// NOTE Import Types
import { CustomerBranchCreate, CustomerType } from 'stores/customer/types';
@ -59,7 +60,6 @@ const flowStore = useFlowStore();
const userBranch = useMyBranch();
const navigatorStore = useNavigator();
const customerStore = useCustomerStore();
const {
fetchListOfOptionBranch,
customerFormUndo,
@ -75,6 +75,7 @@ const {
const {
state: customerFormState,
currentFormData: customerFormData,
currentBranchRootId,
registerAbleBranchOption,
tabFieldRequired,
} = storeToRefs(customerFormStore);
@ -88,6 +89,8 @@ const fieldSelectedOption = computed(() => {
value: v.name,
}));
});
const keyAddDialog = ref<number>(0);
const special = ref(false);
const branchId = ref('');
const agentPrice = ref<boolean>(false);
@ -104,6 +107,7 @@ const pageState = reactive({
fieldSelected: [''],
gridView: false,
total: 0,
sellerId: '',
currentTab: 'Issued',
addModal: false,
@ -271,6 +275,10 @@ const customerNameInfo = computed(() => {
return name || '-';
});
function handleWindowFocus() {
fetchQuotationList();
}
onMounted(async () => {
pageState.gridView = $q.screen.lt.md ? true : false;
navigatorStore.current.title = 'quotation.title';
@ -297,6 +305,7 @@ onMounted(async () => {
pageSize: quotationPageSize.value,
status: 'Issued',
urgentFirst: true,
sellerId: pageState.sellerId || undefined,
});
if (ret) {
@ -307,6 +316,12 @@ onMounted(async () => {
}
flowStore.rotate();
window.addEventListener('focus', handleWindowFocus);
});
onUnmounted(() => {
window.removeEventListener('focus', handleWindowFocus);
});
async function fetchQuotationList(mobileFetch?: boolean) {
@ -331,6 +346,7 @@ async function fetchQuotationList(mobileFetch?: boolean) {
urgentFirst: true,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
sellerId: pageState.sellerId || undefined,
});
if (ret) {
@ -406,6 +422,11 @@ async function storeDataLocal(id: string) {
window.open(url, '_blank');
}
async function filterBySellerId() {
pageState.sellerId = pageState.sellerId ? '' : getUserId();
await fetchQuotationList();
}
</script>
<template>
@ -413,6 +434,7 @@ async function storeDataLocal(id: string) {
hide-icon
style="z-index: 999"
@click.stop="triggerAddQuotationDialog"
v-if="canAccess('quotation', 'create')"
/>
<div class="column full-height no-wrap">
@ -528,7 +550,18 @@ async function storeDataLocal(id: string) {
</template>
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch v-model="pageState.searchDate" />
<AdvanceSearch v-model="pageState.searchDate">
<template #prepend>
<div class="text-weight-medium q-mb-sm">
<q-checkbox
size="xs"
:model-value="!!pageState.sellerId"
@click="filterBySellerId"
/>
{{ $t('quotation.ownOnly') }}
</div>
</template>
</AdvanceSearch>
</template>
</q-input>
@ -647,12 +680,20 @@ async function storeDataLocal(id: string) {
class="col surface-2 flex items-center justify-center"
>
<NoData
v-if="pageState.inputSearch || pageState.currentTab !== 'Issued'"
v-if="
pageState.inputSearch ||
!canAccess('quotation', 'create') ||
pageState.currentTab !== 'Issued'
"
:not-found="!!pageState.inputSearch"
/>
<CreateButton
v-if="!pageState.inputSearch && pageState.currentTab === 'Issued'"
v-if="
!pageState.inputSearch &&
pageState.currentTab === 'Issued' &&
canAccess('quotation', 'create')
"
@click="triggerAddQuotationDialog"
label="general.add"
:i18n-args="{ text: $t('quotation.title') }"
@ -682,6 +723,8 @@ async function storeDataLocal(id: string) {
:visible-columns="pageState.fieldSelected"
:grid="pageState.gridView"
:hide-edit="pageState.currentTab !== 'Issued'"
:hide-action="!canAccess('quotation', 'edit')"
:hide-delete="!canAccess('quotation', 'delete')"
:hide-btn-preview="pageState.currentTab === 'PaymentSuccess'"
@preview="(id: any) => storeDataLocal(id)"
@view="
@ -707,7 +750,8 @@ async function storeDataLocal(id: string) {
<div class="col-md-4 col-sm-6 col-12 column">
<QuotationCard
class="col"
hide-kebab-delete
:hide-action="!canAccess('quotation', 'edit')"
:hide-kebab-delete="!canAccess('quotation', 'delete')"
:hide-kebab-edit="!(pageState.currentTab === 'Issued')"
:hide-preview="pageState.currentTab === 'PaymentSuccess'"
:urgent="item.row.urgent"
@ -733,8 +777,13 @@ async function storeDataLocal(id: string) {
:worker-count="item.row._count.worker"
:worker-max="item.row.workerMax || item.row._count.worker"
:customer-name="
item.row.customerBranch.registerName ||
`${item.row.customerBranch.firstName || '-'} ${item.row.customerBranch.lastName || ''}`
item.row.customerBranch.customer.customerType === 'CORP'
? $i18n.locale === 'tha'
? item.row.customerBranch.registerName
: item.row.customerBranch.registerNameEN
: $i18n.locale === 'tha'
? `${item.row.customerBranch.firstName || '-'} ${item.row.customerBranch.lastName || ''}`
: `${item.row.customerBranch.firstNameEN || '-'} ${item.row.customerBranch.lastNameEN || ''}`
"
:reporter="
$i18n.locale === 'eng'
@ -833,6 +882,7 @@ async function storeDataLocal(id: string) {
<!-- NOTE: START - Quotation Form, Add Quotation -->
<DialogForm
:key="keyAddDialog"
:title="$t('general.add', { text: $t('quotation.title') })"
v-model:modal="pageState.addModal"
:submit-label="$t('general.add', { text: $t('quotation.title') })"
@ -842,13 +892,14 @@ async function storeDataLocal(id: string) {
:submit="
() => {
quotationFormState.mode = 'create';
quotationFormData.customerBranchId = currentBranchRootId;
triggerQuotationDialog({ statusDialog: 'create' });
}
"
:close="
() => {
branchId = '';
quotationFormData.customerBranchId = '';
currentBranchRootId = '';
}
"
:beforeClose="
@ -898,7 +949,7 @@ async function storeDataLocal(id: string) {
v-model:agent-price="agentPrice"
v-model:branch-id="branchId"
v-model:special="special"
v-model:customer-branch-id="quotationFormData.customerBranchId"
v-model:customer-branch-id="currentBranchRootId"
@add-customer="triggerSelectTypeCustomerd()"
/>
</div>
@ -967,6 +1018,7 @@ async function storeDataLocal(id: string) {
() => {
customerFormState.dialogModal = false;
onCreateImageList = { selectedImage: '', list: [] };
keyAddDialog++;
}
"
>
@ -1158,7 +1210,7 @@ async function storeDataLocal(id: string) {
customerFormData.customerBranch[0].legalPersonNo
"
v-model:business-type="
customerFormData.customerBranch[0].businessType
customerFormData.customerBranch[0].businessTypeId
"
v-model:job-position="
customerFormData.customerBranch[0].jobPosition
@ -1239,7 +1291,6 @@ async function storeDataLocal(id: string) {
res = await customerStore.createBranch({
...customerFormData.customerBranch[idx],
customerId: customerFormState.editCustomerId || '',
id: undefined,
});
}
if (res) {

File diff suppressed because it is too large Load diff

View file

@ -2,12 +2,15 @@
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { QSelect, useQuasar } from 'quasar';
import { getUserId } from 'src/services/keycloak';
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
import {
baseUrl,
dialogCheckData,
dialogWarningClose,
formatNumberDecimal,
canAccess,
isRoleInclude,
} from 'stores/utils';
import { ProductTree, quotationProductTree } from './utils';
@ -76,7 +79,8 @@ import { api } from 'src/boot/axios';
import { RouterLink, useRoute } from 'vue-router';
import { initLang, initTheme, Lang } from 'src/utils/ui';
import { convertTemplate } from 'src/utils/string-template';
import { getRole } from 'src/services/keycloak';
import { CustomerBranch } from 'src/stores/customer';
type Node = {
[key: string]: any;
@ -92,6 +96,8 @@ type ProductGroupId = string;
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
const customerBranchOption = ref<CustomerBranch>();
const employeeStore = useEmployeeStore();
const route = useRoute();
const useReceiptStore = useReceipt();
@ -155,50 +161,7 @@ const selectedWorker = ref<
}[];
})[]
>([]);
const selectedWorkerItem = computed(() => {
return [
...selectedWorker.value.map((e) => ({
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: e.firstName
? `${e.firstName} ${e.lastName}`
: `${e.firstNameEN} ${e.lastNameEN}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
status: e.status,
})),
...newWorkerList.value.map((v: any) => ({
foreignRefNo: v.passportNo,
employeeName:
locale.value === Lang.English
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName} ${v.lastName}`,
birthDate: dateFormatJS({ date: v.dateOfBirth }),
gender: v.gender,
age: calculateAge(v.dateOfBirth),
nationality: optionStore.mapOption(v.nationality),
documentExpireDate: '-',
imgUrl: '',
status: 'CREATED',
})),
];
});
const workerList = ref<Employee[]>([]);
const selectedWorkerItem = ref([]);
const firstCodePayment = ref('');
const selectedProductGroup = ref('');
const selectedInstallmentNo = ref<number[]>([]);
@ -214,17 +177,6 @@ const attachmentData = ref<
url?: string;
}[]
>([]);
const hideBtnApproveInvoice = computed(() => {
const role = getRole();
const allowedRoles = [
'system',
'head_of_admin',
'admin',
'head_of_accountant',
'accountant',
];
return !role || !role.some((r) => allowedRoles.includes(r));
});
const getToolbarConfig = computed(() => {
const toolbar = [['left', 'center', 'justify'], ['toggle'], ['clip']];
@ -244,7 +196,7 @@ function getPrice(
) {
if (filterHook) list = list.filter(filterHook);
return list.reduce(
const value = list.reduce(
(a, c) => {
if (
selectedInstallmentNo.value.length > 0 &&
@ -254,32 +206,25 @@ function getPrice(
return a;
}
const originalPrice = c.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice * (1 + (config.value?.vat || 0.07)),
);
const finalPriceNoVat =
finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPriceNoVat * c.amount;
const vat =
(finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07);
const calcVat =
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
a.totalPrice = precisionRound(a.totalPrice + price);
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
a.vat = calcVat ? precisionRound(a.vat + vat) : a.vat;
const pricePerUnit =
precisionRound(c.pricePerUnit * (1 + vatFactor)) / (1 + vatFactor);
const price =
(pricePerUnit * c.amount * (1 + vatFactor) - c.discount) /
(1 + vatFactor);
const vat = price * vatFactor;
a.totalPrice = precisionRound(a.totalPrice + price + c.discount);
a.totalDiscount = precisionRound(a.totalDiscount + c.discount);
a.vat = precisionRound(a.vat + vat);
a.vatExcluded = calcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + price);
a.finalPrice = precisionRound(
a.totalPrice -
a.totalDiscount +
a.vat -
Number(quotationFormData.value.discount || 0),
);
a.finalPrice = precisionRound(a.totalPrice - a.totalDiscount + a.vat);
return a;
},
@ -291,6 +236,8 @@ function getPrice(
finalPrice: 0,
},
);
return value;
}
const summaryPrice = computed(() => getPrice(productServiceList.value));
@ -569,7 +516,7 @@ async function convertDataToFormSubmit() {
),
);
selectedWorker.value.forEach((v, i) => {
selectedWorkerItem.value.forEach((v, i) => {
if (v.attachment !== undefined) {
v.attachment.forEach((value) => {
fileItemNewWorker.value.push({
@ -586,7 +533,7 @@ async function convertDataToFormSubmit() {
quotationFormData.value.worker = JSON.parse(
JSON.stringify([
...selectedWorker.value.map((v) => {
...selectedWorkerItem.value.map((v) => {
{
return v.id;
}
@ -626,6 +573,7 @@ async function convertDataToFormSubmit() {
discount: quotationFormData.value.discount,
remark: quotationFormData.value.remark || '',
agentPrice: agentPrice.value,
sellerId: quotationFormData.value.sellerId,
};
newWorkerList.value = [];
@ -728,19 +676,13 @@ function handleUpdateProductTable(
// handleChangePayType(quotationFormData.value.payCondition);
// calc price
const calc = (c: QuotationPayload['productServiceList'][number]) => {
const originalPrice = c.pricePerUnit || 0;
const finalPriceWithVat = precisionRound(
originalPrice * (1 + (config.value?.vat || 0.07)),
);
const finalPriceNoVat =
finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPriceNoVat * c.amount;
const vat = c.product.calcVat
? (finalPriceNoVat * c.amount - (c.discount || 0)) *
(config.value?.vat || 0.07)
: 0;
return precisionRound(price + vat);
const calcVat =
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
const pricePerUnit =
precisionRound(c.pricePerUnit * (1 + vatFactor)) / (1 + vatFactor);
const price = pricePerUnit * c.amount * (1 + vatFactor) - c.discount;
return precisionRound(price);
};
// installment
@ -793,21 +735,16 @@ function toggleDeleteProduct(index: number) {
// cal curr amount
if (currPaySplit && currTempPaySplit) {
const price = agentPrice.value
? currProduct.product.agentPrice
: currProduct.product.price;
const pricePerUnit = currProduct.product.vatIncluded
? price / (1 + (config.value?.vat || 0.07))
: price;
const vat =
(pricePerUnit * currProduct.amount - currProduct.discount) *
(config.value?.vat || 0.07);
const finalPrice =
pricePerUnit * currProduct.amount +
vat -
Number(currProduct.discount || 0);
const calcVat =
currProduct.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
currTempPaySplit.amount = currPaySplit.amount - finalPrice;
const price = precisionRound(
currProduct.pricePerUnit * currProduct.amount * (1 + vatFactor) -
currProduct.discount,
);
currTempPaySplit.amount = currPaySplit.amount - price;
currPaySplit.amount = currTempPaySplit.amount;
}
@ -829,7 +766,40 @@ function toggleDeleteProduct(index: number) {
}
async function assignWorkerToSelectedWorker() {
selectedWorker.value = quotationFormData.value.worker;
selectedWorkerItem.value = quotationFormData.value.worker.map((e) => {
return {
id: e.id,
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: `${e.firstName || e.firstNameEN} ${e.lastName || e.lastNameEN}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
employeePassport: e.employeePassport,
status: e.status,
workerNew: false,
lastNameEN: e.lastNameEN,
lastName: e.lastName,
middleNameEN: e.middleNameEN,
middleName: e.middleName,
firstNameEN: e.firstNameEN,
firstName: e.firstName,
namePrefix: e.namePrefix,
};
});
}
function convertToTable(nodes: Node[]) {
@ -862,8 +832,7 @@ function convertToTable(nodes: Node[]) {
tempTableProduct.value = JSON.parse(JSON.stringify(list));
if (nodes.length > 0) {
quotationFormData.value.paySplit = Array.apply(
null,
quotationFormData.value.paySplit = Array.from(
new Array(quotationFormData.value.paySplitCount),
).map((_, i) => ({
no: i + 1,
@ -889,21 +858,21 @@ function convertToTable(nodes: Node[]) {
function convertEmployeeToTable(selected: Employee[]) {
productServiceList.value.forEach((v) => {
if (selectedWorker.value.length === 0 && v.amount === 1) v.amount -= 1;
if (selectedWorkerItem.value.length === 0 && v.amount === 1) v.amount -= 1;
v.amount = Math.max(
v.amount + selected.length - selectedWorker.value.length,
v.amount + selected.length - selectedWorkerItem.value.length,
1,
);
const oldWorkerId: string[] = [];
const newWorkerIndex: number[] = [];
selectedWorker.value.forEach((item, i) => {
selectedWorkerItem.value.forEach((item, i) => {
if (v.workerIndex.includes(i)) oldWorkerId.push(item.id);
});
selected.forEach((item, i) => {
if (selectedWorker.value.find((n) => item.id === n.id)) return;
if (selectedWorkerItem.value.find((n) => item.id === n.id)) return;
newWorkerIndex.push(i);
});
@ -916,7 +885,7 @@ function convertEmployeeToTable(selected: Employee[]) {
pageState.employeeModal = false;
quotationFormData.value.workerMax = Math.max(
quotationFormData.value.workerMax || 1,
selectedWorker.value.length,
selectedWorkerItem.value.length,
);
}
@ -989,6 +958,71 @@ function viewProductFile(data: ProductRelation) {
pageState.imageDialogUrl = base64 ? base64[1] : '';
}
function combineWorker(newWorker: any, oldWorker: any) {
selectedWorkerItem.value = [
...oldWorker.map((e) => ({
id: e.id,
foreignRefNo: e.employeePassport
? e.employeePassport[0]?.number || '-'
: '-',
employeeName:
locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}`
: `${e.firstName || e.firstNameEN} ${e.lastName || e.lastNameEN}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender,
age: calculateAge(e.dateOfBirth),
nationality: optionStore.mapOption(e.nationality),
documentExpireDate:
e.employeePassport !== undefined &&
e.employeePassport[0]?.expireDate !== undefined
? dateFormatJS({ date: e.employeePassport[0]?.expireDate })
: '-',
imgUrl: e.selectedImage
? `${API_BASE_URL}/employee/${e.id}/image/${e.selectedImage}`
: '',
employeePassport: e.employeePassport,
status: e.status,
workerNew: false,
lastNameEN: e.lastNameEN,
lastName: e.lastName,
middleNameEN: e.middleNameEN,
middleName: e.middleName,
firstNameEN: e.firstNameEN,
firstName: e.firstName,
namePrefix: e.namePrefix,
})),
...newWorker.map((v: any) => ({
id: v.id,
foreignRefNo: v.passportNo || '-',
employeeName:
locale.value === Lang.English
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName || v.firstNameEN} ${v.lastName || v.lastNameEN}`,
birthDate: dateFormatJS({ date: v.dateOfBirth }),
gender: v.gender,
age: calculateAge(v.dateOfBirth),
nationality: optionStore.mapOption(v.nationality),
documentExpireDate: '-',
imgUrl: '',
status: 'CREATED',
lastNameEN: v.lastNameEN,
lastName: v.lastName,
middleNameEN: v.middleNameEN,
middleName: v.middleName,
firstNameEN: v.firstNameEN,
firstName: v.firstName,
namePrefix: v.namePrefix,
dateOfBirth: v.dateOfBirth,
workerNew: true,
})),
];
}
const sessionData = ref<Record<string, any>>();
onMounted(async () => {
@ -1025,6 +1059,7 @@ onMounted(async () => {
quotationFormData.value.customerBranchId = parsed.customerBranchId;
currentQuotationId.value = parsed.quotationId;
agentPrice.value = parsed.agentPrice;
quotationFormData.value.sellerId = getUserId();
await fetchQuotation();
await assignWorkerToSelectedWorker();
sessionData.value = parsed;
@ -1059,12 +1094,7 @@ watch(
() => quotationFormData.value.customerBranchId,
async (v) => {
if (!v) return;
const retEmp = await customerStore.fetchBranchEmployee(v, {
passport: true,
});
if (retEmp) workerList.value = retEmp.data.result;
selectedWorkerItem.value = [];
},
);
@ -1080,6 +1110,15 @@ watch(
const productServiceNodes = ref<ProductTree>([]);
watch(customerBranchOption, () => {
if (!customerBranchOption.value) return;
quotationFormData.value.contactName =
customerBranchOption.value.contactName || '';
quotationFormData.value.contactTel =
customerBranchOption.value.contactTel || '';
});
watch(
() => productServiceList.value,
() => {
@ -1087,6 +1126,13 @@ watch(
},
);
watch(customerBranchOption, () => {
if (!customerBranchOption.value) return;
quotationFormData.value.contactName = customerBranchOption.value.contactName;
quotationFormData.value.contactTel = customerBranchOption.value.contactTel;
});
// async function searchEmployee(text: string) {
// let query: string | undefined = text;
// let pageSize = 50;
@ -1108,7 +1154,19 @@ watch(
// }
function storeDataLocal() {
quotationFormData.value.productServiceList = productService.value;
const tempProductService = productService.value.map((v) => {
return {
...v,
vat: v.product[agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? precisionRound(
((v.pricePerUnit * (1 + (config?.value.vat || 0.07)) * v.amount -
v.discount) /
(1 + (config?.value.vat || 0.07))) *
0.07,
)
: 0,
};
});
localStorage.setItem(
'quotation-preview',
@ -1117,7 +1175,7 @@ function storeDataLocal() {
codeInvoice: code.value,
codePayment: firstCodePayment.value,
...quotationFormData.value,
productServiceList: productService.value,
productServiceList: tempProductService,
},
meta: {
source: {
@ -1137,7 +1195,7 @@ function storeDataLocal() {
workName: quotationFormData.value.workName,
dueDate: quotationFormData.value.dueDate,
},
selectedWorker: selectedWorker.value,
selectedWorker: selectedWorkerItem.value,
createdBy: quotationFormState.value.createdBy('tha'),
agentPrice: agentPrice.value,
},
@ -1222,10 +1280,10 @@ async function getWorkerFromCriteria(
if (!ret) return false; // error, do not close dialog
const deduplicate = ret.result.filter(
(a) => !selectedWorker.value.find((b) => a.id === b.id),
(a) => !selectedWorkerItem.value.find((b) => a.id === b.id),
);
convertEmployeeToTable([...deduplicate, ...selectedWorker.value]);
convertEmployeeToTable([...deduplicate, ...selectedWorkerItem.value]);
return true;
}
@ -1515,11 +1573,13 @@ function covertToNode() {
:quotation-status="
quotationFormState.source?.quotationStatus === 'Expired'
"
:created-at="quotationFormState.createdAt"
v-model:urgent="quotationFormData.urgent"
v-model:work-name="quotationFormData.workName"
v-model:contactor="quotationFormData.contactName"
v-model:telephone="quotationFormData.contactTel"
v-model:due-date="quotationFormData.dueDate"
v-model:seller-id="quotationFormData.sellerId"
>
<template #issue-info>
<FormAbout
@ -1530,6 +1590,7 @@ function covertToNode() {
v-model:customer-branch-id="
quotationFormData.customerBranchId
"
v-model:customer-branch-option="customerBranchOption"
:readonly="readonly"
/>
</template>
@ -1574,7 +1635,7 @@ function covertToNode() {
}}
</template>
</div>
<nav class="q-ml-auto">
<nav v-if="canAccess('quotation', 'edit')" class="q-ml-auto">
<AddButton
id="btn-add-worker"
for="btn-add-worker"
@ -1612,15 +1673,15 @@ function covertToNode() {
(v) =>
(quotationFormData.workerMax = Math.max(
v,
selectedWorker.length,
selectedWorkerItem.length,
))
"
:employee-amount="
quotationFormData.workerMax || selectedWorker.length
quotationFormData.workerMax || selectedWorkerItem.length
"
:readonly="readonly"
:rows="selectedWorkerItem"
@delete="(i) => deleteItem(selectedWorker, i)"
@delete="(i) => deleteItem(selectedWorkerItem, i)"
/>
</div>
</q-expansion-item>
@ -1751,7 +1812,9 @@ function covertToNode() {
:readonly="
{
quotation: quotationFormState.mode !== 'edit',
invoice: false,
invoice:
isRoleInclude(['sale', 'head_of_sale']) ||
!canAccess('quotation', 'edit'),
accepted: true,
}[view]
"
@ -1862,10 +1925,10 @@ function covertToNode() {
installments: quotationFormData.paySplit,
},
'quotation-labor': {
name: selectedWorker.map(
name: selectedWorkerItem.map(
(v, i) =>
`${i + 1}. ` +
`${v.employeePassport.length !== 0 ? v.employeePassport[0].number + '_' : ''} ${v.namePrefix}.${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
`${v.employeePassport.length !== 0 ? v.employeePassport[0].number + '_' : ''}${v.namePrefix}.${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
),
},
})
@ -1955,6 +2018,11 @@ function covertToNode() {
view !== View.Receipt &&
view !== View.Complete
"
:branch-id="quotationFull.registeredBranchId"
:readonly="
isRoleInclude(['sale', 'head_of_sale']) ||
!canAccess('quotation', 'edit')
"
:data="quotationFormState.source"
v-model:first-code-payment="firstCodePayment"
@fetch-status="
@ -2093,7 +2161,14 @@ function covertToNode() {
:style="`background-color:hsla(var(--info-bg) / 0.07)`"
>
<q-th auto-width>
<q-checkbox v-model="props.selected" />
<q-checkbox
v-if="
!quotationFormData.paySplit.every(
(p) => p.invoiceId,
)
"
v-model="props.selected"
/>
</q-th>
<q-th
v-for="col in props.cols"
@ -2123,8 +2198,6 @@ function covertToNode() {
installmentAmount = props.row.amount;
view = View.Invoice;
console.log(code);
}
}
"
@ -2286,6 +2359,7 @@ function covertToNode() {
class="q-ml-sm"
v-if="
view === View.Accepted &&
canAccess('quotation', 'edit') &&
quotationFormData.quotationStatus === 'Issued'
"
>
@ -2304,9 +2378,15 @@ function covertToNode() {
</MainButton>
</div>
<template v-if="view === View.InvoicePre">
<template
v-if="
view === View.InvoicePre &&
!quotationFormData.paySplit.every((p) => p.invoiceId)
"
>
<MainButton
solid
:disabled="selectedInstallment.length === 0"
icon="mdi-account-multiple-check-outline"
class="q-ml-sm"
color="207 96% 32%"
@ -2325,6 +2405,7 @@ function covertToNode() {
class="q-ml-sm"
v-if="
view === View.Invoice &&
canAccess('quotation', 'edit') &&
((quotationFormData.quotationStatus !== 'PaymentPending' &&
quotationFormData.payCondition !== 'Full') ||
quotationFormData.quotationStatus === 'Accepted') &&
@ -2332,7 +2413,6 @@ function covertToNode() {
"
>
<MainButton
v-if="!hideBtnApproveInvoice"
solid
icon="mdi-account-multiple-check-outline"
color="207 96% 32%"
@ -2352,6 +2432,7 @@ function covertToNode() {
style="gap: var(--size-2)"
v-if="
(view === View.Quotation &&
canAccess('quotation', 'edit') &&
(quotationFormData.quotationStatus === 'Issued' ||
quotationFormData.quotationStatus === 'Expired')) ||
!quotationFormData.quotationStatus
@ -2388,13 +2469,12 @@ function covertToNode() {
<!-- add employee quotation -->
<QuotationFormWorkerSelect
:preselect-worker="selectedWorker"
:preselect-worker="selectedWorkerItem"
:customerBranchId="quotationFormData.customerBranchId"
v-model:open="pageState.employeeModal"
v-model:new-worker-list="newWorkerList"
@success="
(v) => {
selectedWorker = v.worker;
combineWorker(v.newWorker, v.worker);
}
"
/>
@ -2437,7 +2517,7 @@ function covertToNode() {
<!-- add Worker -->
<QuotationFormWorkerAddDialog
v-if="quotationFormState.source"
:disabled-worker-id="selectedWorker.map((v) => v.id)"
:disabled-worker-id="selectedWorkerItem.map((v) => v.id)"
:product-service-list="quotationFormState.source.productServiceList"
:quotation-id="quotationFormState.source.id"
:customer-branch-id="quotationFormState.source.customerBranchId"
@ -2531,8 +2611,8 @@ function covertToNode() {
}
:deep(
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
color: var(--brand-1);
}
@ -2549,9 +2629,9 @@ function covertToNode() {
}
:deep(
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.surface-1
.q-focus-helper
) {
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.surface-1
.q-focus-helper
) {
visibility: hidden;
}

View file

@ -234,14 +234,14 @@ watch(
<section class="row q-col-gutter-sm col-12 items-center">
<SelectInput
class="col-md-6 col-12"
id="select-pay-type"
:label="$t('quotation.payType')"
:option="
taskOrder
? payTypeOption.filter((v) => v.value === 'Full')
: payTypeOption
"
:readonly
id="pay-type"
:readonly="readonly || debitNote"
:model-value="payType"
@update:model-value="
(v) => {
@ -275,6 +275,7 @@ watch(
</div>
<q-input
v-model="paySplitCount"
id="input-pay-split-count"
:readonly="readonly || payType === 'Split'"
class="col-3"
type="number"
@ -311,6 +312,7 @@ watch(
<q-input
:readonly="readonly"
:label="$t('general.name')"
:id="`input-period-name-${i}`"
v-if="payType === 'SplitCustom'"
v-model="period.name"
class="col q-mx-sm"
@ -320,6 +322,7 @@ watch(
<q-input
:readonly="readonly || payType === 'Split'"
class="col q-mx-sm"
:id="`input-period-amount-${i}`"
:label="$t('quotation.amount')"
:model-value="
amount4Show[i] || commaInput(period.amount.toString())
@ -377,6 +380,7 @@ watch(
<DatePicker
v-if="payType === 'BillFull'"
id="datepicker-bill-date"
:readonly
class="col-12"
:label="$t('quotation.callDueDate')"
@ -446,7 +450,9 @@ watch(
<span class="q-ml-auto">
{{
formatNumberDecimal(
summaryPrice.totalPrice - summaryPrice.totalDiscount,
summaryPrice.totalPrice -
summaryPrice.totalDiscount -
summaryPrice.vatExcluded,
2,
)
}}
@ -482,7 +488,11 @@ watch(
<div class="q-pa-sm row surface-2 items-center text-weight-bold">
{{ $t('quotation.totalPriceBaht') }}
<span class="q-ml-auto" style="color: var(--brand-1)">
<span
class="q-ml-auto"
style="color: var(--brand-1)"
id="value-final-price"
>
{{
payType === 'SplitCustom' && view === View.Invoice
? formatNumberDecimal(Math.max(installmentAmount || 0, 0), 2) || 0

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import DatePicker from 'src/components/shared/DatePicker.vue';
import SelectUser from 'src/components/shared/select/SelectUser.vue';
defineProps<{
readonly: boolean;
@ -13,6 +14,7 @@ const contactor = defineModel<string>('contactor', { required: true });
const telephone = defineModel<string>('telephone', { required: true });
const dueDate = defineModel<Date | string>('dueDate', { required: true });
const createdAt = defineModel<Date | string>('createdAt');
const sellerId = defineModel<string>('sellerId', { required: true });
</script>
<template>
@ -95,5 +97,11 @@ const createdAt = defineModel<Date | string>('createdAt');
dense
outlined
/>
<SelectUser
:label="$t('preview.seller')"
v-model:value="sellerId"
:readonly
class="col-12 col-md-2"
/>
</div>
</template>

View file

@ -51,6 +51,8 @@ const emit = defineEmits<{
const selectedProductGroup = defineModel<string>('selectedProductGroup', {
default: '',
});
const selectedProductGroupOption = ref<ProductGroup | undefined>();
const model = defineModel<boolean>();
const inputSearch = defineModel<string>('inputSearch');
const productGroup = defineModel<ProductGroup[]>('productGroup', {
@ -66,21 +68,21 @@ const serviceList = defineModel<Partial<Record<ProductGroupId, Service[]>>>(
);
const priceDisplay = computed(() => ({
price: !isRoleInclude(['sale_agent']),
// price: !isRoleInclude(['sale_agent']),
price: true,
agentPrice: isRoleInclude([
'admin',
'head_of_admin',
'head_of_sale',
'system',
'owner',
'head_of_admin',
'admin',
'executive',
'accountant',
'sale_agent',
'head_of_sale',
]),
serviceCharge: isRoleInclude([
'admin',
'head_of_admin',
'system',
'owner',
'head_of_admin',
'admin',
'executive',
'accountant',
]),
}));
@ -569,14 +571,18 @@ watch(
{{
productGroup.find(
(g) => g.id === selectedProductGroup,
)?.name || '-'
)?.name ||
selectedProductGroupOption?.name ||
'-'
}}
</span>
<span class="text-caption app-text-muted">
{{
productGroup.find(
(g) => g.id === selectedProductGroup,
)?.code || '-'
)?.code ||
selectedProductGroupOption?.code ||
'-'
}}
</span>
</div>
@ -862,13 +868,13 @@ watch(
<span class="q-pr-sm">
{{ $t('productService.group.title') }}
</span>
<SelectProductGroup
class="col-md-4 col-12"
:class="{ 'q-mb-sm': $q.screen.lt.md }"
id="product-group-select"
clearable
v-model:value="selectedProductGroup"
v-model:value-option="selectedProductGroupOption"
:placeholder="
!selectedProductGroup
? $t('general.select', {

View file

@ -341,12 +341,13 @@ watch(() => state.search, getWorkerList);
>
<div
style="display: inline-block; margin-inline: auto"
v-if="workerList.length === 0"
v-if="workerList.length === 0 && state.search"
>
<NoData :not-found="!!state.search" />
</div>
<TableWorker
v-else
v-model:selected="workerSelected"
:rows="workerList"
:disabledWorkerId

View file

@ -21,6 +21,7 @@ import useOcrStore from 'stores/ocr';
// NOTE: Import Components
import {
AddButton,
SaveButton,
EditButton,
UndoButton,
@ -53,6 +54,9 @@ import { SideMenu } from 'src/components';
import BasicInformation from 'components/03_customer-management/employee/BasicInformation.vue';
import { AddressForm } from 'src/components/form';
import ExpirationDate from 'src/components/03_customer-management/ExpirationDate.vue';
import FormEmployeeHealthCheck from 'src/components/03_customer-management/FormEmployeeHealthCheck.vue';
import FormEmployeeWorkHistory from 'src/components/03_customer-management/FormEmployeeWorkHistory.vue';
import FormEmployeeOther from 'src/components/03_customer-management/FormEmployeeOther.vue';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
@ -109,7 +113,7 @@ const props = withDefaults(
defineProps<{
customerBranchId?: string;
disabledWorkerId?: string[];
preselectWorker?: Employee[];
preselectWorker?: (Employee & { workerNew: boolean })[];
}>(),
{},
);
@ -129,7 +133,7 @@ const optionStore = useOptionStore();
const employeeStore = useEmployeeStore();
const open = defineModel<boolean>('open', { default: false });
const newWorkerList = defineModel<
const newWorkerList = ref<
(EmployeeWorker & {
attachment?: {
name?: string;
@ -139,7 +143,7 @@ const newWorkerList = defineModel<
_meta?: Record<string, any>;
}[];
})[]
>('newWorkerList', { default: [] });
>([]);
const workerSelected = ref<Employee[]>([]);
const workerList = ref<Employee[]>([]);
const importWorkerCriteria = ref<{
@ -204,7 +208,13 @@ function getEmployeeImageUrl(item: Employee) {
function init() {
if (props.preselectWorker) {
workerSelected.value = JSON.parse(JSON.stringify(props.preselectWorker));
workerSelected.value = JSON.parse(
JSON.stringify(props.preselectWorker.filter((v) => !v.workerNew)),
);
newWorkerList.value = JSON.parse(
JSON.stringify(props.preselectWorker.filter((v) => v.workerNew)),
);
}
getWorkerList();
}
@ -604,11 +614,14 @@ watch(
solid
id="btn-success"
@click="
emits('success', {
worker: workerSelected,
newWorker: newWorkerList,
}),
(open = false)
() => {
$emit('success', {
worker: workerSelected,
newWorker: newWorkerList,
});
open = false;
}
"
>
{{ $t('general.select', { msg: $t('quotation.employeeList') }) }}
@ -630,9 +643,11 @@ watch(
if (employeeFormState.currentTab === 'personalInfo') {
const currentEmployeeId =
await employeeFormStore.submitPersonal(onCreateImageList);
quotationForm.injectNewEmployee({
data: { ...currentFromDataEmployee, id: currentEmployeeId },
});
newWorkerList.push(
quotationForm.injectNewEmployee({
data: { ...currentFromDataEmployee, id: currentEmployeeId },
}),
);
employeeFormState.isEmployeeEdit = false;
employeeFormState.dialogType = 'info';
}
@ -663,6 +678,7 @@ watch(
:show="
() => {
employeeFormStore.resetFormDataEmployee(true);
setCurrentBranchId();
}
"
:before-close="
@ -1035,6 +1051,7 @@ watch(
</div>
<BasicInformation
disable-customer-select
no-action
id="form-information"
prefix-id="form-employee"
@ -1455,6 +1472,7 @@ watch(
v-model:remark="meta.remark"
v-model:worker-type="meta.workerType"
v-model:number="meta.number"
v-model:report-date="meta.reportDate"
/>
<NoticeJobEmployment v-if="mode === 'noticeJobEmployment'" />
@ -1681,6 +1699,7 @@ watch(
v-model:remark="value.remark"
v-model:worker-type="value.workerType"
v-model:number="value.number"
v-model:report-date="value.reportDate"
>
<template v-slot:expiryDate>
{{ $t('general.expirationDate') }} :
@ -1991,8 +2010,8 @@ watch(
}
:deep(
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) {
color: var(--brand-1);
}
@ -2009,9 +2028,9 @@ watch(
}
:deep(
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.surface-1
.q-focus-helper
) {
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.surface-1
.q-focus-helper
) {
visibility: hidden;
}

View file

@ -8,6 +8,7 @@ import {
QuotationPayload,
QuotationFull,
EmployeeWorker,
PayCondition,
} from 'src/stores/quotations/types';
import { Employee } from 'src/stores/employee/types';
@ -29,7 +30,7 @@ export const DEFAULT_DATA: QuotationPayload = {
payBillDate: new Date(),
paySplit: [],
paySplitCount: 0,
payCondition: 'Full',
payCondition: PayCondition.Full,
dueDate: new Date(Date.now() + 86400000),
discount: 0,
contactTel: '',
@ -40,6 +41,7 @@ export const DEFAULT_DATA: QuotationPayload = {
status: 'CREATED',
remark: '#[quotation-labor]<br/><br/>#[quotation-payment]',
agentPrice: false,
sellerId: '',
};
const DEFAULT_DATA_INVOICE: InvoicePayload = {
@ -67,6 +69,7 @@ export const useQuotationForm = defineStore('form-quotation', () => {
file?: File;
_meta?: Record<string, any>;
}[];
workerNew: boolean;
})[]
>([]);
@ -218,7 +221,7 @@ export const useQuotationForm = defineStore('form-quotation', () => {
},
callback?: () => void,
) {
newWorkerList.value.push({
const temp = {
//passportNo: obj.data.passportNo,
//documentExpireDate: obj.data.documentExpireDate,
id: obj.data.id,
@ -233,9 +236,12 @@ export const useQuotationForm = defineStore('form-quotation', () => {
gender: obj.data.gender,
dateOfBirth: obj.data.dateOfBirth,
attachment: obj.data.attachment,
});
workerNew: true,
};
callback?.();
return temp;
}
function dialogDelete(callback: () => void) {

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { onMounted, nextTick, ref, watch } from 'vue';
import { onMounted, nextTick, ref, watch, toRaw } from 'vue';
import { precisionRound } from 'src/utils/arithmetic';
import { useI18n } from 'vue-i18n';
import ThaiBahtText from 'thai-baht-text';
@ -175,6 +175,8 @@ enum View {
const view = ref<View>(View.Quotation);
onMounted(async () => {
await configStore.getConfig();
const currentDocumentType = new URL(window.location.href).searchParams.get(
'type',
);
@ -259,18 +261,6 @@ onMounted(async () => {
productList.value =
(data?.value?.productServiceList ?? data.value?.productServiceList).map(
(v) => {
const originalPrice = v.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice * (1 + (config.value?.vat || 0.07)),
);
const finalPriceNoVat =
finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPriceNoVat * v.amount - v.discount;
const vat =
(finalPriceNoVat * v.amount - v.discount) *
(config.value?.vat || 0.07);
return {
id: v.product.id,
code: v.product.code,
@ -279,8 +269,8 @@ onMounted(async () => {
pricePerUnit: v.pricePerUnit || 0,
discount: v.discount || 0,
vat: v.vat || 0,
value: precisionRound(price + (v.product.calcVat ? vat : 0)),
calcVat: v.product.calcVat,
value: 0,
calcVat: v.vat > 0,
product: v.product,
};
},
@ -292,23 +282,17 @@ onMounted(async () => {
[]
).reduce(
(a, c) => {
const originalPrice = c.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice * (1 + (config.value?.vat || 0.07)),
);
const finalPriceNoVat =
finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const calcVat = c.vat > 0;
const vatFactor = calcVat ? (config.value?.vat ?? 0.07) : 0;
const pricePerUnit =
precisionRound(c.pricePerUnit * (1 + vatFactor)) / (1 + vatFactor);
const price =
(pricePerUnit * c.amount * (1 + vatFactor) - c.discount) /
(1 + vatFactor);
const price = finalPriceNoVat * c.amount;
const vat =
(finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07);
const calcVat =
c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat'];
a.totalPrice = precisionRound(a.totalPrice + price);
a.totalPrice = precisionRound(a.totalPrice + price + c.discount);
a.totalDiscount = precisionRound(a.totalDiscount + Number(c.discount));
a.vat = calcVat ? precisionRound(a.vat + vat) : a.vat;
a.vat = calcVat ? precisionRound(a.vat + c.vat) : a.vat;
a.vatExcluded = calcVat
? a.vatExcluded
: precisionRound(a.vatExcluded + price);
@ -334,16 +318,12 @@ onMounted(async () => {
function calcPrice(c: Product) {
const originalPrice = c.pricePerUnit;
const finalPriceWithVat = precisionRound(
originalPrice + originalPrice * (config.value?.vat || 0.07),
const finalPricePerUnit = precisionRound(
originalPrice +
(c.calcVat ? originalPrice * (config.value?.vat || 0.07) : 0),
);
const finalPriceNoVat = finalPriceWithVat / (1 + (config.value?.vat || 0.07));
const price = finalPriceNoVat * c.amount - c.discount;
const vat = c.product[agentPrice.value ? 'agentPriceCalcVat' : 'calcVat']
? (finalPriceNoVat * c.amount - c.discount) * (config.value?.vat || 0.07)
: 0;
return precisionRound(price + vat);
const price = finalPricePerUnit * c.amount - c.discount;
return precisionRound(price);
}
async function closeTab() {
@ -427,31 +407,15 @@ function print() {
<td>{{ v.detail }}</td>
<td style="text-align: right">{{ v.amount }}</td>
<td style="text-align: right">
{{
formatNumberDecimal(
v.pricePerUnit +
(v.product[agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? v.pricePerUnit * (config?.vat || 0.07)
: 0),
2,
)
}}
{{ formatNumberDecimal(v.pricePerUnit, 2) }}
</td>
<td style="text-align: right">
{{ formatNumberDecimal(v.discount, 2) }}
<template v-if="v.discount !== 0">
{{ formatNumberDecimal(v.discount, 2) }} ฿
</template>
</td>
<td style="text-align: right">
{{
formatNumberDecimal(
v.product[agentPrice ? 'agentPriceCalcVat' : 'calcVat']
? precisionRound(
(v.pricePerUnit * v.amount - v.discount) *
(config?.vat || 0.07),
)
: 0,
2,
)
}}
{{ Math.round((v.vat > 0 ? config?.vat || 0.07 : 0) * 100) }}%
</td>
<td style="text-align: right">
{{ formatNumberDecimal(calcPrice(v), 2) }}
@ -511,7 +475,9 @@ function print() {
<td class="text-right">
{{
formatNumberDecimal(
summaryPrice.totalPrice - summaryPrice.totalDiscount,
summaryPrice.totalPrice -
summaryPrice.totalDiscount -
summaryPrice.vatExcluded,
2,
)
}}
@ -600,7 +566,7 @@ function print() {
details?.worker.map(
(v, i) =>
`${i + 1}. ` +
`${v.namePrefix}. ${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
`${v.employeePassport.length !== 0 ? v.employeePassport[0].number + '_' : ''}${v.namePrefix}. ${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
) || [],
},
}) || '-'

View file

@ -3,6 +3,7 @@ import { dateFormat } from 'src/utils/datetime';
// NOTE: Import stores
import { formatAddress } from 'src/utils/address';
import { getCustomerName } from 'src/stores/utils';
// NOTE Import Types
import { Branch } from 'src/stores/branch/types';
@ -78,12 +79,15 @@ function titleMode(mode: View): string {
})
}}
</span>
<span>เลขประจำตวผเสยภาษ {{ branch.taxNo }}</span>
<span>เบอรโทร {{ branch.telephoneNo }}</span>
<span>{{ $t('branch.form.taxNo') }} {{ branch.taxNo }}</span>
<span>{{ $t('taskOrder.telephone') }} {{ branch.telephoneNo }}</span>
<span>{{ branch.webUrl }}</span>
</article>
<article>
<b>{{ $t('quotation.customer') }}</b>
<div>
{{ getCustomerName(customer, { noCode: true, locale: 'tha' }) }}
</div>
<span>
{{
formatAddress({
@ -101,8 +105,18 @@ function titleMode(mode: View): string {
})
}}
</span>
<span>เลขประจำตวผเสยภาษ {{ customer.citizenId }}</span>
<span>เบอรโทร {{ customer.telephoneNo }}</span>
<span>
{{
customer.customer.customerType === 'CORP'
? `${$t('customer.form.legalPersonNo')} `
: `${$t('customer.form.taxpayyerNo')} `
}}{{
customer.customer.customerType === 'CORP'
? customer.codeCustomer
: customer.citizenId
}}
</span>
<span>{{ $t('taskOrder.telephone') }} {{ customer.telephoneNo }}</span>
</article>
</section>
<section class="detail-quotation-info">

View file

@ -62,6 +62,8 @@ const props = withDefaults(
defineProps<{
readonly?: boolean;
isEdit?: boolean;
hideAction?: boolean;
hideDelete?: boolean;
dataId?: string;
}>(),
@ -80,6 +82,7 @@ const emit = defineEmits<{
(e: 'addImage'): void;
(e: 'removeImage'): void;
(e: 'submitImage', name: string): void;
(e: 'deleteAttachment', name: string): void;
}>();
const data = defineModel<InstitutionPayload>('data', {
@ -119,6 +122,9 @@ const formBankBook = defineModel<BankBook[]>('formBankBook', {
},
],
});
const attachment = defineModel<File[]>('attachment');
const attachmentList =
defineModel<{ name: string; url: string }[]>('attachmentList');
function viewImage() {
imageState.imageDialog = true;
@ -346,6 +352,7 @@ watch(
v-model:contact-name="data.contactName"
v-model:email="data.contactEmail"
v-model:contact-tel="data.contactTel"
v-model:attachment="attachment"
/>
<AddressForm
id="agencies-form-address-info"
@ -411,6 +418,7 @@ watch(
:prefix="data.name"
hide-fade
use-toggle
:readonly="hideAction"
:active="data.status !== 'INACTIVE'"
:toggle-title="$t('status.title')"
:icon="'ph-building-office'"
@ -450,7 +458,7 @@ watch(
style="position: absolute; z-index: 999; top: 0; right: 0"
>
<div
v-if="data.status !== 'INACTIVE'"
v-if="data.status !== 'INACTIVE' && !hideAction"
class="surface-1 row rounded"
>
<UndoButton
@ -484,7 +492,7 @@ watch(
type="button"
/>
<DeleteButton
v-if="!isEdit"
v-if="!isEdit && !hideDelete"
id="btn-info-basic-delete"
icon-only
@click="
@ -547,6 +555,9 @@ watch(
v-model:contact-name="data.contactName"
v-model:email="data.contactEmail"
v-model:contact-tel="data.contactTel"
v-model:attachment="attachment"
:attachment-list="attachmentList"
@delete-attachment="(name) => $emit('deleteAttachment', name)"
/>
<AddressForm
id="agencies-address-info"
@ -597,6 +608,7 @@ watch(
v-model:on-create-data-list="imageListOnCreate"
v-model:image-url="imageState.imageUrl"
v-model:data-list="imageList"
:changeDisabled="hideAction"
:on-create="model"
:hiddenFooter="!imageState.isImageEdit"
@add-image="addImage"

View file

@ -7,7 +7,7 @@ import { Icon } from '@iconify/vue/dist/iconify.js';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { baseUrl } from 'src/stores/utils';
import { baseUrl, canAccess } from 'src/stores/utils';
import { useNavigator } from 'src/stores/navigator';
import { useInstitution } from 'src/stores/institution';
import { Institution, InstitutionPayload } from 'src/stores/institution/types';
@ -115,6 +115,8 @@ const blankFormData: InstitutionPayload = {
],
};
const attachment = ref<File[]>([]);
const attachmentList = ref<{ name: string; url: string }[]>([]);
const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all');
const refAgenciesDialog = ref();
const formData = ref<InstitutionPayload>(structuredClone(blankFormData));
@ -145,6 +147,8 @@ function resetForm() {
pageState.addModal = false;
pageState.viewDrawer = false;
currAgenciesData.value = undefined;
attachment.value = [];
attachmentList.value = [];
formData.value = structuredClone(blankFormData);
}
@ -154,7 +158,7 @@ function undo() {
pageState.isDrawerEdit = false;
}
function assignFormData(data: Institution) {
async function assignFormData(data: Institution) {
currAgenciesData.value = data;
formData.value = {
group: data.group,
@ -174,9 +178,9 @@ function assignFormData(data: Institution) {
provinceId: data.provinceId,
selectedImage: data.selectedImage,
status: data.status,
contactEmail: data.contactEmail,
contactName: data.contactName,
contactTel: data.contactTel,
contactEmail: data.contactEmail || '',
contactName: data.contactName || '',
contactTel: data.contactTel || '',
bank: data.bank.map((v) => ({
bankName: v.bankName,
accountNumber: v.accountNumber,
@ -187,6 +191,8 @@ function assignFormData(data: Institution) {
bankUrl: `${baseUrl}/institution/${data.id}/bank-qr/${v.id}?ts=${Date.now()}`,
})),
};
await fetchAttachment();
}
async function submit(opt?: { selectedImage: string }) {
@ -214,7 +220,6 @@ async function submit(opt?: { selectedImage: string }) {
...v,
})),
};
console.log('payload', payload);
if (
(pageState.isDrawerEdit && currAgenciesData.value?.id) ||
(opt?.selectedImage && currAgenciesData.value?.id)
@ -229,18 +234,29 @@ async function submit(opt?: { selectedImage: string }) {
);
if (ret) {
attachment.value.forEach(async (file) => {
await institutionStore.putAttachment({
parentId: ret.id || '',
name: file.name,
file: file,
});
});
pageState.isDrawerEdit = false;
currAgenciesData.value = ret;
formData.value.selectedImage = ret.selectedImage;
await fetchData($q.screen.xs);
attachment.value = [];
if (refAgenciesDialog.value && opt?.selectedImage) {
refAgenciesDialog.value.clearImageState();
}
setTimeout(async () => {
await fetchAttachment();
}, 300);
return;
}
} else {
await institutionStore.createInstitution(
const res = await institutionStore.createInstitution(
{
...payload,
code: formData.value.group || '',
@ -248,6 +264,16 @@ async function submit(opt?: { selectedImage: string }) {
imageListOnCreate.value,
);
if (!res) return;
attachment.value.forEach(async (file) => {
await institutionStore.putAttachment({
parentId: res.id || '',
name: file.name,
file: file,
});
});
await fetchData($q.screen.xs);
pageState.addModal = false;
return;
@ -347,6 +373,49 @@ async function changeStatus(id?: string) {
}
}
async function deleteAttachment(attachmentName: string) {
dialog({
color: 'negative',
icon: 'mdi-trash-can-outline',
title: t('dialog.title.confirmDelete', {
msg: t('personnel.form.attachment'),
}),
actionText: t('general.delete'),
persistent: true,
message: t('dialog.message.confirmDelete'),
action: async () => {
if (!currAgenciesData.value?.id) return;
institutionStore.delAttachment({
parentId: currAgenciesData.value?.id,
name: attachmentName,
});
setTimeout(async () => {
await fetchAttachment();
}, 300);
},
cancel: () => {},
});
}
async function fetchAttachment() {
const resAttachment = await institutionStore.listAttachment({
parentId: currAgenciesData.value.id,
});
if (!resAttachment) return;
attachmentList.value = await Promise.all(
resAttachment.map(async (f) => ({
name: f,
url: await institutionStore.getAttachment({
parentId: currAgenciesData.value.id,
name: f,
download: false,
}),
})),
);
}
onMounted(async () => {
navigatorStore.current.title = 'agencies.title';
navigatorStore.current.path = [{ text: 'agencies.caption', i18n: true }];
@ -366,6 +435,7 @@ watch(
</script>
<template>
<FloatingActionButton
v-if="canAccess('agencies', 'edit')"
style="z-index: 999"
hide-icon
@click="triggerDialog('add')"
@ -750,6 +820,7 @@ watch(
"
/>
<KebabAction
v-if="canAccess('agencies', 'edit')"
:id-name="props.row.name"
:status="props.row.status"
@view="
@ -833,6 +904,7 @@ watch(
"
/>
<KebabAction
v-if="canAccess('agencies', 'edit')"
:id-name="props.row.id"
:status="props.row.status"
@view="
@ -920,7 +992,10 @@ watch(
{{ $t('general.recordPerPage') }}
</div>
<div>
<PaginationPageSize v-model="pageSize" />
<PaginationPageSize
v-model="pageSize"
:fetch-data="() => fetchData()"
/>
</div>
</div>
</div>
@ -971,14 +1046,18 @@ watch(
}
"
@change-status="triggerChangeStatus"
@delete-attachment="deleteAttachment"
:readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit"
:hide-action="!canAccess('agencies', 'edit')"
v-model="pageState.addModal"
v-model:drawer-model="pageState.viewDrawer"
v-model:data="formData"
v-model:form-bank-book="formData.bank"
v-model:image-list-on-create="imageListOnCreate"
v-model:deletes-status-qr-code-bank-imag="deletesStatusQrCodeBankImag"
v-model:attachment="attachment"
:attachment-list="attachmentList"
/>
</template>
<style scoped>

Some files were not shown because too many files have changed in this diff Show more