Compare commits

...

364 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
Thanaphon Frappet
ff5767cdd1 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-28 16:07:47 +07:00
Thanaphon Frappet
02c7598aec refactor: edit label
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 12s
2025-04-28 13:57:04 +07:00
Thanaphon Frappet
93700a2b54 refactor: edit i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-04-28 13:49:49 +07:00
Thanaphon Frappet
d67b9b2f02 refactor: edit i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-28 10:32:50 +07:00
Methapon2001
18844c70bc Merge branch 'develop' 2025-04-25 17:30:29 +07:00
Methapon2001
84d3e0d777 fix: dark mode manual table
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-25 17:00:16 +07:00
Methapon2001
a0b7fb3a1b feat: table border and spacing
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-25 16:52:25 +07:00
Methapon2001
21699b14c5 feat: troubleshooting page
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-25 15:15:37 +07:00
puriphatt
8c9e9abc18 fix: update rules for agency prefix name field
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-25 10:59:54 +07:00
puriphatt
ef81522561 feat: add QR code upload functionality and enhance bank management logic
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-24 17:59:11 +07:00
puriphatt
dfc17e9623 feat: update importNationality to support multiple selections and adjust related logic 2025-04-24 17:58:48 +07:00
Thanaphon Frappet
5c12bcbab7 refactor: trim the last 3 characters from the code
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-04-24 15:52:15 +07:00
Thanaphon Frappet
9a8363091d refactor: delete btn paste
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-24 15:45:02 +07:00
puriphatt
aac82ce477 refactor: remove unnecessary console log from responsiblePerson function
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-04-24 15:17:46 +07:00
puriphatt
9f6d972c91 feat: enhance AvatarGroup to display responsible groups alongside users
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-24 15:16:09 +07:00
Methapon2001
92b4db45d2 feat: detect can edit request list
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-04-24 14:32:13 +07:00
puriphatt
56a63185a1 feat: update responsibleGroup type to string array and initialize in workflow steps
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-24 14:31:27 +07:00
Thanaphon Frappet
28395b4f80 refactor: edit name model
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-24 14:25:03 +07:00
Thanaphon Frappet
1d38dbc6cf refactor: copy now
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-24 14:10:11 +07:00
Thanaphon Frappet
a4a101712c fix: incorrectly swapped data 2025-04-24 14:09:17 +07:00
puriphatt
2a2bfa3180 feat: add img-group.png image asset
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-04-24 14:06:13 +07:00
puriphatt
8cf93d0016 feat: add getGroupList function and responsibleGroup to workflow types 2025-04-24 12:52:47 +07:00
puriphatt
63036b03fd feat: add activeOnly parameter to institution queries
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-24 10:22:14 +07:00
Methapon2001
4040da58f9 fix: name repeated 2 times
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-24 09:49:13 +07:00
Methapon2001
cf67ed3d47 feat: product receive code
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 11s
2025-04-23 14:08:49 +07:00
Thanaphon Frappet
8d8ad40de1 feat: import prodect from file
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-04-22 13:58:26 +07:00
puriphatt
74291c0552 refactor: employee => remove validation rules for last name input
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 10s
2025-04-22 09:34:25 +07:00
puriphatt
88f40dcb47 feat: update agencies management to include date range selection and refactor image list handling
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-18 09:29:46 +07:00
puriphatt
03b03b4bc8 feat: implement date range filtering in branch management
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-18 09:24:41 +07:00
puriphatt
285b821c16 feat: add date range search functionality to personnel management
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-17 18:04:12 +07:00
puriphatt
648ed38181 refactor: comment out condition for resetting search date on tab change 2025-04-17 17:45:26 +07:00
puriphatt
ac42ee60d8 feat: add date range selection to credit note, debit note, and receipt management 2025-04-17 17:38:09 +07:00
puriphatt
1e6be274e2 feat: add date range selection to task order filtering 2025-04-17 17:30:06 +07:00
puriphatt
ea21ec4632 feat: add date range selection to request list filtering 2025-04-17 17:25:37 +07:00
puriphatt
73562a59c1 feat: add date range search functionality to invoice management 2025-04-17 17:22:31 +07:00
puriphatt
7897103a1b feat: add date range search functionality to quotation management 2025-04-17 17:13:26 +07:00
puriphatt
36cef7ceb6 feat: add date range selection to customer and employee management 2025-04-17 17:03:16 +07:00
puriphatt
461dd359b1 feat: add date range filtering to product and service lists 2025-04-17 16:40:43 +07:00
puriphatt
efeb1b51eb feat: integrate date range selection in property management and workflow lists
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-17 16:23:52 +07:00
puriphatt
fd5d4b7979 feat: add dayjs library version 1.11.13 to pnpm-lock.yaml
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-17 16:16:22 +07:00
puriphatt
181ddc8f03 feat: add dayjs library for date manipulation 2025-04-17 16:15:33 +07:00
puriphatt
d7e53b764c feat: enhance AdvanceSearch component to support date range selection and workflow template advance search 2025-04-17 16:15:08 +07:00
puriphatt
d95d72806d feat: implement functionality to filter request list by same office area
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 10s
2025-04-17 15:35:22 +07:00
Thanaphon Frappet
550ed55de0 refactor: show all product and add column status and edit format remark
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-17 15:02:22 +07:00
puriphatt
145784ee40 feat: add contact name and contact tel fields to user types and forms
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-17 14:25:46 +07:00
puriphatt
e189b9a880 feat: conditionally render file input based on user type
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 10s
2025-04-17 14:15:04 +07:00
puriphatt
4e86a90b0e feat: add AdvanceSearch component for date range selection
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-17 13:53:36 +07:00
puriphatt
08b0dcbce0 feat: add new translations for date range and document status 2025-04-17 13:53:14 +07:00
Thanaphon Frappet
1d5f77f3a6 feat: copy goodbey
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-11 17:59:40 +07:00
Thanaphon Frappet
82f48a4b80 feat: copy
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-11 17:47:23 +07:00
puriphatt
d909be2fc4 feat: add navigation to customer and employee details from request list
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-11 15:50:31 +07:00
puriphatt
586fbed4e3 refactor: remove console.log from openRequestListDialog function 2025-04-11 15:50:31 +07:00
Thanaphon Frappet
0efe78a37a refactor: visible columns
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-11 13:34:21 +07:00
Thanaphon Frappet
febfbf4828 refactor: add new column
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-11 13:24:08 +07:00
puriphatt
d1bb504174 feat: add VAT parameter to calcPrice function and update related calculations
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-11 11:43:52 +07:00
puriphatt
af37904ce0 refactor: add readonly and disable properties to checkboxes and selects in PriceDataComponent
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-11 11:12:32 +07:00
Thanaphon Frappet
0a5b6af649 refactor: add _ after passport
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 13s
2025-04-11 10:51:57 +07:00
Thanaphon Frappet
71b06c82bd refactor: edit format show
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-11 10:48:17 +07:00
Thanaphon Frappet
b1295d00ff feat: set addr of current customer
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-11 09:45:09 +07:00
puriphatt
d3e5aec842 refactor: rename agencyFile and agencyFileList to userFile and userFileList for clarity
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-10 18:04:37 +07:00
puriphatt
2e813f6e88 refactor: add clear functionality to SelectInput and update Thai translation for agency status 2025-04-10 18:03:53 +07:00
puriphatt
ed5a05709a refactor: implement request list action dialog and enhance messenger functionality
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-10 17:23:27 +07:00
puriphatt
a40f9f9775 refactor: enhance FloatingActionButton to support custom icons 2025-04-10 17:23:27 +07:00
puriphatt
5b1ccadf92 refactor: add incomplete flag and updateMessenger function to request list store 2025-04-10 17:23:27 +07:00
Thanaphon Frappet
a5d73ba1ff refactor: show prefix on banner
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-10 17:21:29 +07:00
Thanaphon Frappet
69f368ede1 refactor: handle show name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-10 10:37:48 +07:00
Methapon2001
bc5097a0a8 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-04-08 16:00:10 +07:00
Thanaphon Frappet
79abde8629 refactor: handle show name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-08 15:29:57 +07:00
Thanaphon Frappet
bd38c008a6 refactor: change form employee
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-04-08 15:04:03 +07:00
puriphatt
7fcb4d7744 refactor: ensure remark and agency status fields default to empty strings in user data
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-08 13:10:30 +07:00
puriphatt
1a8be5ac34 refactor: add contact information fields to agency forms and update data models
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 12s
2025-04-08 12:10:13 +07:00
puriphatt
3efe8e19f4 refactor: add single prop to conditionally render bank form elements 2025-04-08 12:09:56 +07:00
puriphatt
ace3af2a4b refactor: add contact information and bank details to institution types and translations 2025-04-08 12:09:40 +07:00
puriphatt
f22a7e09b3 refactor: add remark and agency status fields to form data
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-08 11:20:27 +07:00
puriphatt
0de6921636 refactor: update training labels and add agency status to user types
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-08 11:12:42 +07:00
Thanaphon Frappet
98ab120e56 fix: i18n tha
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-04 16:07:34 +07:00
Thanaphon Frappet
6f2471c33b refactor: change form like page customer 2025-04-04 15:55:04 +07:00
Thanaphon Frappet
2511690d54 refactor: handle input require name en
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-04 11:28:41 +07:00
Thanaphon Frappet
25b62de139 fix: i18n error
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-04 11:27:51 +07:00
Thanaphon Frappet
68e1abb4cb refactor: edit i18n 2025-04-04 11:08:46 +07:00
Thanaphon Frappet
bc507b7b4c refactor: add option type visa 2025-04-04 11:08:46 +07:00
Thanaphon Frappet
6f16964859 refactor: show option only eng 2025-04-04 11:08:46 +07:00
puriphatt
80f68cd702 feat(address): remove ',' and use addressFormat on BasicInformation(customer employee)
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-04 10:51:26 +07:00
Thanaphon Frappet
174c30875e fix: edit id upload
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-03 18:05:18 +07:00
Thanaphon Frappet
18d5c4ff82 refactor: set default data passport
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-03 18:02:51 +07:00
puriphatt
d5d95648b1 feat(option): add new option "CUST" to option lists in JSON
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-04-03 13:47:07 +07:00
puriphatt
12ec914603 feat(property): assign new property to global option property
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-04-03 13:18:15 +07:00
puriphatt
789502c1b2 feat(i18n): add message for existing property name in English and Thai translations 2025-04-03 13:17:10 +07:00
Thanaphon Frappet
c8b4339cf6 refactor: edit i18n
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-04-03 11:05:31 +07:00
Methapon2001
2d94d163d2 fix: change prefix effect gender and wrong prefix selected
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-04-02 15:35:02 +07:00
puriphatt
f8b56fd37e refactor: remove unnecessary watch on tab value in CanvasComponent
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-03-31 15:42:27 +07:00
Methapon2001
88cabff86e fix: build warn about throw error 2025-03-27 17:20:43 +07:00
Methapon2001
3c85f955c2 chore: update deps 2025-03-27 17:12:35 +07:00
Methapon2001
50bb4638c5 fix: i18n warn locale not found cause of default locale 2025-03-27 17:00:55 +07:00
Methapon2001
0a87843b3b Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-03-27 16:49:25 +07:00
Methapon2001
4fb26bf54b chore: update ci/cd notification text
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-03-27 13:25:15 +07:00
Methapon2001
e2f8f3332a chore: update self-hosted ci/cd jobs name
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-03-27 13:22:37 +07:00
Methapon2001
a24303377f feat: disable issue document when no template is selected
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-03-27 12:42:58 +07:00
Methapon2001
416424b8eb fix: i18n 2025-03-27 10:33:59 +07:00
Methapon2001
71c1f9c770 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-03-27 10:30:06 +07:00
Methapon2001
9312701096 feat: add support for request work form
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-03-27 09:33:57 +07:00
Methapon Metanipat
0e685a99f7
feat: signature (#194)
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
* refactor: enable profile signature option in ProfileMenu

* feat: add signature api function

* refactor: add new translation keys for 'Draw' and 'New Upload' in English and Thai

* refactor: update image URL variable and improve translation keys in CanvasComponent and MainLayout

* refactor: get function

* feat: add delete signature function

* feat: add canvas manipulation functions and integrate signature submission in MainLayout (unfinished)

* chore(deps): update

---------

Co-authored-by: puriphatt <puriphat@frappet.com>
Co-authored-by: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
2025-03-27 09:01:42 +07:00
Methapon2001
3646956038 chore: clean unused
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
chore: clean unused
2025-03-26 15:27:39 +07:00
puriphatt
1e9a5abc1c refactor: add tooltips for item title and detail in MainLayout
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-03-26 14:38:42 +07:00
Methapon2001
af792678dd Merge branch 'update-deps' into develop
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-03-26 14:36:32 +07:00
Methapon2001
90589b3daf chore: update deps
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-03-26 11:51:27 +07:00
puriphatt
fb23ec5fd4 refactor: remove redundant confirmation message for validation
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 11s
2025-03-26 10:30:45 +07:00
Thanaphon Frappet
0dca8a7029 refactor: handle role can approve invoice
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-03-25 14:50:19 +07:00
Thanaphon Frappet
1101fa68d8 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-03-24 10:41:12 +07:00
puriphatt
4e61762130 fix: correct role name from 'account' to 'accountant' and update route syntax
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s
2025-03-24 10:37:00 +07:00
Thanaphon Frappet
4a88a7fc55 refactor: handle role can canceled
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 7s
2025-03-24 10:35:25 +07:00
Methapon2001
3ff6715528 refactor: menu role
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 9s
2025-03-24 10:27:33 +07:00
Methapon2001
6ee9e67633 Merge branch 'develop'
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 11s
2025-03-24 08:48:51 +07:00
Methapon2001
0150de9661 feat: heading count
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
2025-03-20 10:27:02 +07:00
219 changed files with 18527 additions and 23995 deletions

View file

@ -1,9 +0,0 @@
/dist
/src-capacitor
/src-cordova
/.quasar
/node_modules
.eslintrc.cjs
/src-ssr
/quasar.config.*.temporary.compiled*
/tests

View file

@ -1,61 +0,0 @@
module.exports = {
root: true,
parserOptions: {
parser: require.resolve('@typescript-eslint/parser'),
extraFileExtensions: ['.vue'],
},
env: {
browser: true,
es2021: true,
node: true,
'vue/setup-compiler-macros': true,
},
extends: [
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage
// ESLint typescript rules
'plugin:@typescript-eslint/recommended',
// Uncomment any of the lines below to choose desired strictness,
// but leave only one uncommented!
// See https://eslint.vuejs.org/rules/#available-rules
'plugin:vue/vue3-essential',
// https://github.com/prettier/eslint-config-prettier#installation
// usage with Prettier, provided by 'eslint-config-prettier'.
'prettier',
],
plugins: [
// required to apply rules which need type information
'@typescript-eslint',
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
// required to lint *.vue files
'vue',
],
globals: {
ga: 'readonly', // Google Analytics
cordova: 'readonly',
__statics: 'readonly',
__QUASAR_SSR__: 'readonly',
__QUASAR_SSR_SERVER__: 'readonly',
__QUASAR_SSR_CLIENT__: 'readonly',
__QUASAR_SSR_PWA__: 'readonly',
process: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly',
},
// add your custom rules here
rules: {
quotes: ['warn', 'single', { avoidEscape: true }],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'prefer-promise-reject-errors': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
};

View file

@ -9,7 +9,7 @@ env:
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
CONTAINER_IMAGE_NAME: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_OWNER }}/${{ vars.CONTAINER_IMAGE_NAME }}:latest
jobs:
gitea-release:
build-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -51,7 +51,7 @@ jobs:
"description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Deployed by: `${{ github.actor }}`",
"color": 3066993,
"footer": {
"text": "Gitea Local Release Notification",
"text": "Local Release Notification",
"icon_url": "https://example.com/success-icon.png"
},
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
@ -68,7 +68,7 @@ jobs:
"description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Attempted by: `${{ github.actor }}`",
"color": 15158332,
"footer": {
"text": "Gitea Local Release Notification",
"text": "Local Release Notification",
"icon_url": "https://example.com/failure-icon.png"
},
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"

12398
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,23 +7,24 @@
"type": "module",
"private": true,
"scripts": {
"lint": "eslint --ext .js,.ts,.vue ./",
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test": "echo \"No test specified\" && exit 0",
"dev": "quasar dev",
"build": "quasar build",
"postinstall": "quasar prepare",
"changelog:generate": "git-cliff -o CHANGELOG.md"
},
"dependencies": {
"@peaceroad/markdown-it-figure-with-p-caption": "^0.11.0",
"@quasar/extras": "^1.16.12",
"@tato30/vue-pdf": "^1.11.0",
"@quasar/extras": "^1.16.17",
"@tato30/vue-pdf": "^1.11.3",
"@vuepic/vue-datepicker": "^8.8.1",
"apexcharts": "^4.5.0",
"axios": "^1.7.4",
"axios": "^1.8.4",
"cropperjs": "^1.6.2",
"dayjs": "^1.11.13",
"highlight.js": "^11.11.1",
"keycloak-js": "^25.0.4",
"keycloak-js": "^25.0.6",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-highlightjs": "^4.2.0",
@ -31,49 +32,44 @@
"markdown-it-html5-media": "^0.7.1",
"markdown-it-image-figures": "^2.1.1",
"markdown-it-video": "^0.6.3",
"mime": "^4.0.4",
"mime": "^4.0.6",
"moment": "^2.30.1",
"number-to-words": "^1.2.4",
"open-props": "^1.7.5",
"pinia": "^2.2.2",
"quasar": "^2.16.9",
"signature_pad": "^5.0.2",
"socket.io-client": "^4.7.5",
"open-props": "^1.7.14",
"pinia": "^2.3.1",
"quasar": "^2.18.1",
"signature_pad": "^5.0.7",
"tesseract.js": "^5.1.1",
"thai-baht-text": "^2.0.5",
"udsv": "^0.6.0",
"uuid": "^10.0.0",
"vue": "^3.4.38",
"vue": "^3.5.13",
"vue-dragscroll": "^4.0.6",
"vue-i18n": "^9.14.0",
"vue-i18n": "^11.1.2",
"vue-pdf": "^4.3.0",
"vue-router": "^4.4.3",
"vue3-apexcharts": "^1.7.0"
"vue-router": "^4.5.0",
"vue-tsc": "^2.2.8",
"vue3-apexcharts": "^1.8.0"
},
"devDependencies": {
"@faker-js/faker": "^9.3.0",
"@iconify/vue": "^4.1.2",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@playwright/test": "^1.46.1",
"@quasar/app-vite": "2.0.0-beta.19",
"@faker-js/faker": "^9.6.0",
"@iconify/vue": "^4.3.0",
"@intlify/unplugin-vue-i18n": "^6.0.5",
"@playwright/test": "^1.51.1",
"@quasar/app-vite": "^2.2.0",
"@types/markdown-it": "^14.1.2",
"@types/markdown-it-highlightjs": "^3.3.4",
"@types/node": "^20.16.1",
"@types/node": "^20.17.28",
"@types/number-to-words": "^1.2.3",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.21",
"dotenv": "^16.4.7",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.27.0",
"prettier": "^3.3.3",
"prettier": "^3.5.3",
"typescript": "^5.5.4",
"vue-component-type-helpers": "^2.1.10"
"vue-component-type-helpers": "^2.2.8"
},
"engines": {
"node": "^24 || ^22 || ^20 || ^18",
"node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
}

3642
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

18
postcss.config.js Normal file
View file

@ -0,0 +1,18 @@
import autoprefixer from 'autoprefixer';
export default {
plugins: [
autoprefixer({
overrideBrowserslist: [
'last 4 Chrome versions',
'last 4 Firefox versions',
'last 4 Edge versions',
'last 4 Safari versions',
'last 4 Android versions',
'last 4 ChromeAndroid versions',
'last 4 FirefoxAndroid versions',
'last 4 iOS versions',
],
}),
],
};

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

BIN
public/img-group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

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

@ -1,5 +1,28 @@
{
"eng": {
"visaType": [
{
"label": "Non-LA",
"value": "nla"
},
{
"label": "Non-B",
"value": "nb"
},
{
"label": "TV.60",
"value": "tv60"
},
{
"label": "Non-TR",
"value": "ntr"
},
{
"label": "TV.30",
"value": "tv30"
}
],
"workerStatus": [
{
"label": "Normal",
@ -154,20 +177,21 @@
{ "label": "VS2", "value": "VS2" },
{ "label": "WO", "value": "WO" },
{ "label": "WP390", "value": "WP390" },
{ "label": "WP44", "value": "WP44" }
{ "label": "WP44", "value": "WP44" },
{ "label": "CUST", "value": "CUST" }
],
"prefix": [
{
"label": "Mr",
"label": "MR",
"value": "mr"
},
{
"label": "Mrs",
"label": "MRS",
"value": "mrs"
},
{
"label": "Miss",
"label": "MISS",
"value": "miss"
}
],
@ -183,29 +207,44 @@
}
],
"training": [
"border": [
{
"label": "Myanmar Labor Training Center - Mae Sot, Tak Province",
"label": "Mae Sot, Tak Province",
"value": "trainingTak"
},
{
"label": "Myanmar Labor Training Center - Kawthoung, Ranong Province",
"label": "Koh Song, Ranong province",
"value": "trainingRanong"
},
{
"label": "Laos Labor Training Center - Nong Khai, Nong Khai Province",
"label": "Nong Khai, Nong Khai Province",
"value": "trainingNongKhai"
},
{
"label": "Cambodian Labor Training Center - Aranyaprathet, Sa Kaeo Province",
"label": "Aranyaprathet, Sa Kaeo Province",
"value": "trainingSaKaeo"
},
{
"label": "Cambodian Labor Training Center - Ban Laem, Chanthaburi Province",
"label": "Ban Laem, Chanthaburi Province",
"value": "trainingChanthaburi"
}
],
"training": [
{
"label": "The first center accepts work. and end of employment Tak Province",
"value": "trainingTak"
},
{
"label": "The first center accepts work. and end of employment Nong Khai Province",
"value": "trainingNongKhai"
},
{
"label": "The first center accepts work. and end of employment Sa Kaeo Province",
"value": "trainingSaKaeo"
}
],
"nationality": [
{
"label": "Thai",
@ -1050,6 +1089,29 @@
},
"tha": {
"visaType": [
{
"label": "Non-LA",
"value": "nla"
},
{
"label": "Non-B",
"value": "nb"
},
{
"label": "ผผ.60",
"value": "tv60"
},
{
"label": "Non-TR",
"value": "ntr"
},
{
"label": "ผผ.30",
"value": "tv30"
}
],
"workerStatus": [
{
"label": "ปกติ",
@ -1204,7 +1266,8 @@
{ "label": "VS2", "value": "VS2" },
{ "label": "WO", "value": "WO" },
{ "label": "WP390", "value": "WP390" },
{ "label": "WP44", "value": "WP44" }
{ "label": "WP44", "value": "WP44" },
{ "label": "CUST", "value": "CUST" }
],
"prefix": [
@ -1233,29 +1296,44 @@
}
],
"training": [
"border": [
{
"label": "สถานที่อบรมแรงงานเมียนมา-แม่สอด จ.ตาก",
"label": "แม่สอด จ.ตาก",
"value": "trainingTak"
},
{
"label": "สถานที่อบรมแรงงานเมียนมา-เกาะสอง จ.ระนอง",
"label": "เกาะสอง จ.ระนอง",
"value": "trainingRanong"
},
{
"label": "สถานที่อบรมแรงงานลาว-หนองคาย จ.หนองคาย",
"label": "หนองคาย จ.หนองคาย",
"value": "trainingNongKhai"
},
{
"label": "สถานที่อบรมแรงงานกัมพูชา-อรัญประเทศ จ.สระแก้ว",
"label": "อรัญประเทศ จ.สระแก้ว",
"value": "trainingSaKaeo"
},
{
"label": "สถานที่อบรมแรงงานกัมพูชา-บ้านแหลม จ.จันทบุรี",
"label": "บ้านแหลม จ.จันทบุรี",
"value": "trainingChanthaburi"
}
],
"training": [
{
"label": "ศูนย์แรกรับเข้าทำงาน และสิ้นสุดการจ้าง จังหวัดตาก",
"value": "trainingTak"
},
{
"label": "ศูนย์แรกรับเข้าทำงาน และสิ้นสุดการจ้าง จังหวัดหนองคาย",
"value": "trainingNongKhai"
},
{
"label": "ศูนย์แรกรับเข้าทำงาน และสิ้นสุดการจ้าง จังหวัดสระแก้ว",
"value": "trainingSaKaeo"
}
],
"nationality": [
{
"label": "ไทย",

View file

@ -1,26 +1,22 @@
/* eslint-env node */
// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
import { configure } from 'quasar/wrappers';
import { defineConfig } from '#q-app/wrappers';
import { fileURLToPath } from 'node:url';
export default configure((ctx) => {
export default defineConfig((ctx) => {
return {
eslint: {
fix: true,
warnings: true,
errors: true,
},
boot: ['i18n', 'axios', 'components'],
css: ['app.scss'],
extras: ['mdi-v7'],
build: {
target: {
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
node: 'node20',
},
typescript: {
vueShim: true,
},
vueRouterMode: 'history',
vitePlugins: [
[
@ -35,7 +31,7 @@ export default configure((ctx) => {
devServer: {
host: '0.0.0.0',
open: false,
port: 5173,
port: 5174,
},
framework: {
config: {},

View file

@ -1,167 +0,0 @@
{
"config": {
"configFile": "/Users/linping/Desktop/Chamomind&FrappeT/JWS_TestScript/playwright.config.ts",
"rootDir": "/Users/linping/Desktop/Chamomind&FrappeT/JWS_TestScript/tests",
"forbidOnly": false,
"fullyParallel": true,
"globalSetup": null,
"globalTeardown": null,
"globalTimeout": 0,
"grep": {},
"grepInvert": null,
"maxFailures": 0,
"metadata": {
"actualWorkers": 1
},
"preserveOutput": "always",
"reporter": [
[
"json",
{
"outputFile": "reports.json"
}
]
],
"reportSlowTests": {
"max": 5,
"threshold": 15000
},
"quiet": false,
"projects": [
{
"outputDir": "/Users/linping/Desktop/Chamomind&FrappeT/JWS_TestScript/test-results",
"repeatEach": 1,
"retries": 0,
"metadata": {},
"id": "chromium",
"name": "chromium",
"testDir": "/Users/linping/Desktop/Chamomind&FrappeT/JWS_TestScript/tests",
"testIgnore": [],
"testMatch": [
"**/*.@(spec|test).?(c|m)[jt]s?(x)"
],
"timeout": 30000
}
],
"shard": null,
"updateSnapshots": "missing",
"version": "1.44.1",
"workers": 1,
"webServer": null
},
"suites": [
{
"title": "01-Admin-BranchManagement/JWS_BM_001_CreateHeadquarters.spec.ts",
"file": "01-Admin-BranchManagement/JWS_BM_001_CreateHeadquarters.spec.ts",
"column": 0,
"line": 0,
"specs": [
{
"title": "Login",
"ok": true,
"tags": [],
"tests": [
{
"timeout": 30000,
"annotations": [],
"expectedStatus": "passed",
"projectId": "chromium",
"projectName": "chromium",
"results": [
{
"workerIndex": 4,
"status": "passed",
"duration": 3024,
"errors": [],
"stdout": [],
"stderr": [],
"retry": 0,
"startTime": "2024-07-30T02:59:00.817Z",
"attachments": []
}
],
"status": "expected"
}
],
"id": "8c5091bd59605f227965-8109f0f4a59e27330a76",
"file": "01-Admin-BranchManagement/JWS_BM_001_CreateHeadquarters.spec.ts",
"line": 16,
"column": 1
},
{
"title": "Create Branch Managenment",
"ok": true,
"tags": [],
"tests": [
{
"timeout": 30000,
"annotations": [],
"expectedStatus": "passed",
"projectId": "chromium",
"projectName": "chromium",
"results": [
{
"workerIndex": 4,
"status": "passed",
"duration": 5091,
"errors": [],
"stdout": [],
"stderr": [],
"retry": 0,
"startTime": "2024-07-30T02:59:05.659Z",
"attachments": []
}
],
"status": "expected"
}
],
"id": "8c5091bd59605f227965-5a0d70f27623401a3479",
"file": "01-Admin-BranchManagement/JWS_BM_001_CreateHeadquarters.spec.ts",
"line": 27,
"column": 1
},
{
"title": "Create Branch Managenment Second",
"ok": true,
"tags": [],
"tests": [
{
"timeout": 30000,
"annotations": [],
"expectedStatus": "passed",
"projectId": "chromium",
"projectName": "chromium",
"results": [
{
"workerIndex": 4,
"status": "passed",
"duration": 5029,
"errors": [],
"stdout": [],
"stderr": [],
"retry": 0,
"startTime": "2024-07-30T02:59:10.755Z",
"attachments": []
}
],
"status": "expected"
}
],
"id": "8c5091bd59605f227965-d619bd2184e7f07d4970",
"file": "01-Admin-BranchManagement/JWS_BM_001_CreateHeadquarters.spec.ts",
"line": 52,
"column": 1
}
]
}
],
"errors": [],
"stats": {
"startTime": "2024-07-30T02:59:00.334Z",
"duration": 15556.794999999925,
"expected": 3,
"skipped": 0,
"unexpected": 0,
"flaky": 0
}
}

View file

@ -1,11 +1,11 @@
import axios, { AxiosInstance } from 'axios';
import { boot } from 'quasar/wrappers';
import { defineBoot } from '#q-app/wrappers';
import { getToken } from 'src/services/keycloak';
import { dialog } from 'stores/utils';
import useLoader from 'stores/loader';
import useFlowStore from 'src/stores/flow';
declare module '@vue/runtime-core' {
declare module 'vue' {
interface ComponentCustomProperties {
$axios: AxiosInstance;
$api: AxiosInstance;
@ -24,10 +24,10 @@ function parseError(
status: number,
body?: { status: number; message: string; code: string },
) {
if (status === 422) return 'invalideData';
if (status === 422) return 'invalidData';
if (body && body.code) return body.code;
return 'errorOccure';
return 'errorOccurred';
}
api.interceptors.request.use(async (config) => {
@ -64,7 +64,7 @@ api.interceptors.response.use(
},
);
export default boot(({ app }) => {
export default defineBoot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios;

View file

@ -1,4 +1,4 @@
import { boot } from 'quasar/wrappers';
import { defineBoot } from '#q-app/wrappers';
import VueDatePicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
import GlobalDialog from 'components/GlobalDialog.vue';
@ -6,7 +6,7 @@ import GlobalLoading from 'components/GlobalLoading.vue';
import VueDragscroll from 'vue-dragscroll';
import VueApexCharts from 'vue3-apexcharts';
export default boot(({ app }) => {
export default defineBoot(({ app }) => {
app.component('global-dialog', GlobalDialog);
app.component('global-loading', GlobalLoading);
app.component('VueDatePicker', VueDatePicker);

View file

@ -1,7 +1,8 @@
import { boot } from 'quasar/wrappers';
import { defineBoot } from '#q-app/wrappers';
import { createI18n } from 'vue-i18n';
import messages from 'src/i18n';
import { Lang } from 'src/utils/ui';
export type MessageLanguages = keyof typeof messages;
// Type-define 'eng' as the master schema for the resource
@ -21,16 +22,17 @@ declare module 'vue-i18n' {
}
/* eslint-enable @typescript-eslint/no-empty-interface */
export const i18n = createI18n({
export const i18n = createI18n<
{ message: MessageSchema },
MessageLanguages,
false
>({
locale: 'tha',
legacy: false,
messages: {
'en-US': {},
...messages,
},
messages,
});
export default boot(({ app }) => {
export default defineBoot(({ app }) => {
// Set i18n instance on app
app.use(i18n);
});

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

@ -36,6 +36,7 @@ defineProps<{
outlined?: boolean;
readonly?: boolean;
view?: boolean;
single?: boolean;
}>();
defineEmits<{
@ -121,7 +122,7 @@ watch(
/>
{{ $t(`${title}`) }}
<AddButton
v-if="!readonly"
v-if="!readonly && !single"
id="btn-add-bank"
icon-only
class="q-ml-sm"
@ -141,7 +142,10 @@ watch(
style="padding-block: 0.01px"
spaced="lg"
/>
<span class="col-12 app-text-muted-2 flex justify-between items-center">
<span
v-if="!single"
class="col-12 app-text-muted-2 flex justify-between items-center"
>
{{ `${$t('branch.form.bankAccountNo')} ${i + 1}` }}
<div class="row items-center">
<div style="height: 30.8px" />
@ -172,7 +176,8 @@ watch(
</span>
<div
class="bordered q-mr-sm rounded col text-center overflow-hidden"
v-if="!single"
class="bordered q-mr-sm rounded col-4 text-center overflow-hidden"
:class="{ 'pointer-none': readonly, 'q-my-sm': $q.screen.lt.md }"
>
<ImageHover

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

@ -1,13 +1,13 @@
<script setup lang="ts">
import useUserStore from 'stores/user';
import useOptionStore from 'stores/options';
import { UserAttachmentDelete } from 'stores/user/types';
import { dialog, selectFilterOptionRefMod } from 'stores/utils';
import { onMounted, ref, watch } from 'vue';
import { UserAttachmentDelete, AgencyStatus } from 'stores/user/types';
import { dialog } from 'stores/utils';
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { Icon } from '@iconify/vue';
import { QSelect } from 'quasar';
import DatePicker from '../shared/DatePicker.vue';
import SelectInput from 'src/components/shared/SelectInput.vue';
import SelectOffice from 'components/shared/select-muliple/SelectOffice.vue';
@ -29,15 +29,16 @@ const discountCondition = defineModel<string | null | undefined>(
const sourceNationality = defineModel<string | null | undefined>(
'sourceNationality',
);
const importNationality = defineModel<string | null | undefined>(
const importNationality = defineModel<string[] | null | undefined>(
'importNationality',
);
const trainingPlace = defineModel<string | null | undefined>('trainingPlace');
const checkpoint = defineModel<string | null | undefined>('checkPoint');
const checkpointEN = defineModel<string | null | undefined>('checkPointEn');
const agencyFile = defineModel<File[]>('agencyFile');
const agencyFileList =
defineModel<{ name: string; url: string }[]>('agencyFileList');
const checkpoint = defineModel<string | null | undefined>('checkpoint');
const userFile = defineModel<File[]>('userFile');
const userFileList =
defineModel<{ name: string; url: string }[]>('userFileList');
const remark = defineModel<string | null | undefined>('remark');
const agencyStatus = defineModel<string | null | undefined>('agencyStatus');
const attachmentRef = ref();
@ -69,66 +70,12 @@ function deleteFile(name: string) {
userStore.deleteAttachment(userId.value, payload);
const result = await userStore.fetchAttachment(userId.value);
if (result) {
agencyFileList.value = result;
userFileList.value = result;
}
},
cancel: () => {},
});
}
const nationalityOptions = ref<Record<string, unknown>[]>([]);
let nationalityFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const trainingPlaceOptions = ref<Record<string, unknown>[]>([]);
let trainingPlaceFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const responsibleAreaOptions = ref<Record<string, unknown>[]>([]);
let responsibleAreaFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
onMounted(() => {
if (optionStore.globalOption?.nationality) {
nationalityFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.nationality),
nationalityOptions,
'label',
);
trainingPlaceFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.training),
trainingPlaceOptions,
'label',
);
responsibleAreaFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.area),
responsibleAreaOptions,
'label',
);
}
});
watch(
() => optionStore.globalOption,
() => {
nationalityFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.nationality),
nationalityOptions,
'label',
);
trainingPlaceFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.training),
trainingPlaceOptions,
'label',
);
},
);
</script>
<template>
<div class="row col-12">
@ -186,11 +133,12 @@ watch(
/>
<SelectOffice
v-if="userType === 'MESSENGER'"
for="input-responsible-area"
v-model:value="responsibleArea"
v-if="userType === 'MESSENGER'"
:readonly="readonly"
:label="$t('personnel.form.responsibleArea')"
class="col"
/>
</div>
<div
@ -218,207 +166,171 @@ watch(
class="row col-12 q-col-gutter-sm"
style="margin-left: 0px; padding-left: 0px"
>
<q-select
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-value="value"
option-label="label"
class="col-md-3 col-6"
<SelectInput
:model-value="readonly ? sourceNationality || '-' : sourceNationality"
id="input-source-nationality"
for="input-source-nationality"
:dense="dense"
:readonly="readonly"
:hide-dropdown-icon="readonly"
:option="optionStore.globalOption.nationality"
class="col-md-3 col-6"
:readonly
clearable
:label="$t('personnel.form.sourceNationality')"
:options="nationalityOptions"
@filter="nationalityFilter"
:model-value="readonly ? sourceNationality || '-' : sourceNationality"
@update:model-value="
(v) => (typeof v === 'string' ? (sourceNationality = v) : '')
"
@clear="sourceNationality = ''"
>
<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
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-value="value"
option-label="label"
class="col-md-3 col-6"
/>
<SelectInput
v-model="importNationality"
id="input-import-nationality"
for="input-import-nationality"
:dense="dense"
:readonly="readonly"
:hide-dropdown-icon="readonly"
:label="$t('personnel.form.importNationality')"
:options="nationalityOptions"
@filter="nationalityFilter"
:model-value="readonly ? importNationality || '-' : importNationality"
@update:model-value="
(v) => (typeof v === 'string' ? (importNationality = v) : '')
"
@clear="importNationality = ''"
>
<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
outlined
:option="optionStore.globalOption.nationality"
class="col-md-3 col-6"
:readonly
multiple
:hideSelected="false"
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="label"
class="col-md-6 col-12"
id="select-trainig-place"
for="select-trainig-place"
:dense="dense"
:readonly="readonly"
:hide-dropdown-icon="readonly"
:label="$t('personnel.form.trainingPlace')"
:options="trainingPlaceOptions"
@filter="trainingPlaceFilter"
:model-value="readonly ? trainingPlace || '-' : trainingPlace"
@update:model-value="
(v) => (typeof v === 'string' ? (trainingPlace = v) : '')
"
@clear="trainingPlace = ''"
>
<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-input
for="input-checkpoint"
:dense="dense"
outlined
:readonly="readonly"
:label="$t('personnel.form.checkpoint')"
class="col-6"
fillInput
:label="$t('personnel.form.importNationality')"
/>
<SelectInput
:model-value="readonly ? checkpoint || '-' : checkpoint"
id="select-checkpoint"
for="select-checkpoint"
:option="optionStore.globalOption.border"
class="col-md-6 col-12"
:readonly
:label="$t('personnel.form.checkpoint')"
clearable
@update:model-value="
(v) => (typeof v === 'string' ? (checkpoint = v) : '')
"
@clear="checkpoint = ''"
/>
<SelectInput
:model-value="readonly ? trainingPlace || '-' : trainingPlace"
id="select-trainig-place"
for="select-trainig-place"
:option="optionStore.globalOption.training"
class="col-md-8 col-12"
:readonly
:label="$t('personnel.form.trainingPlace')"
clearable
@update:model-value="
(v) => (typeof v === 'string' ? (trainingPlace = v) : '')
"
/>
<SelectInput
:model-value="readonly ? agencyStatus || '-' : agencyStatus"
id="select-checkpoint-en"
for="select-checkpoint-en"
:option="[
{ label: $t('personnel.form.normal'), value: AgencyStatus.Normal },
{
label: $t('personnel.form.canceled'),
value: AgencyStatus.Canceled,
},
{
label: $t('personnel.form.blacklist'),
value: AgencyStatus.Blacklist,
},
]"
class="col-md-4 col-12"
:readonly
:label="$t('personnel.form.agencyStatus')"
clearable
@update:model-value="
(v) => (typeof v === 'string' ? (agencyStatus = v) : '')
"
/>
<q-input
for="input-checkpoint-en"
for="input-discount-condition"
:dense="dense"
outlined
:readonly="readonly"
:label="$t('personnel.form.checkpointEN')"
class="col-6"
:model-value="readonly ? checkpointEN || '-' : checkpointEN"
@update:model-value="
(v) => (typeof v === 'string' ? (checkpointEN = v) : '')
"
@clear="checkpointEN = ''"
/>
<q-file
ref="attachmentRef"
for="input-attachment"
:dense="dense"
outlined
:readonly="readonly"
multiple
append
:label="$t('personnel.form.attachment')"
:readonly
:label="$t('general.remark')"
class="col-12"
v-model="agencyFile"
>
<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>
type="textarea"
:model-value="readonly ? remark || '-' : remark"
@update:model-value="
(v) => (typeof v === 'string' ? (remark = v) : '')
"
@clear="remark = ''"
/>
</div>
<div v-if="agencyFileList && agencyFileList?.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 agencyFileList"
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
id="delete-file"
v-if="!readonly && userId"
rounded
flat
dense
unelevated
size="md"
icon="mdi-trash-can-outline"
class="app-text-negative"
@click.stop="deleteFile(item.name)"
/>
<q-file
v-if="userType && !readonly"
ref="attachmentRef"
for="input-attachment"
:dense="dense"
outlined
:readonly="readonly"
multiple
append
:label="$t('personnel.form.attachment')"
class="col"
v-model="userFile"
>
<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="userFileList && userFileList?.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 userFileList"
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-item-section>
</q-item>
</q-list>
</div>
<q-btn
id="delete-file"
v-if="!readonly && userId"
rounded
flat
dense
unelevated
size="md"
icon="mdi-trash-can-outline"
class="app-text-negative"
@click.stop="deleteFile(item.name)"
/>
</div>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</div>

View file

@ -1,10 +1,8 @@
<script setup lang="ts">
import { QSelect } from 'quasar';
import useOptionStore from 'stores/options';
import { selectFilterOptionRefMod } from 'stores/utils';
import { calculateAge, disabledAfterToday } from 'src/utils/datetime';
import { ref, onMounted, watch } from 'vue';
import { capitalize } from 'vue';
import { watch } from 'vue';
import SelectInput from '../shared/SelectInput.vue';
import DatePicker from '../shared/DatePicker.vue';
const optionStore = useOptionStore();
@ -23,6 +21,8 @@ const midNameEN = defineModel<string | null>('midNameEn');
const citizenId = defineModel<string>('citizenId');
const citizenIssue = defineModel<Date | null>('citizenIssue');
const citizenExpire = defineModel<Date | null>('citizenExpire');
const contactName = defineModel<string>('contactName');
const contactTel = defineModel<string>('contactTel');
const props = defineProps<{
dense?: boolean;
@ -30,73 +30,19 @@ const props = defineProps<{
readonly?: boolean;
separator?: boolean;
employee?: boolean;
agency?: boolean;
title?: string;
prefixId: string;
hideNameEn?: boolean;
}>();
const prefixNameOptions = ref<Record<string, unknown>[]>([]);
let prefixNameFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const genderOptions = ref<Record<string, unknown>[]>([]);
let genderFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const nationalityOptions = ref<Record<string, unknown>[]>([]);
let nationalityFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
function matPreFixName() {
function matchPreFixName() {
if (gender.value === 'male') prefixName.value = 'mr';
if (gender.value === 'female') prefixName.value = 'mrs';
if (gender.value === 'female' && prefixName.value === 'mr') {
prefixName.value = 'mrs';
}
}
onMounted(() => {
prefixNameFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.prefix),
prefixNameOptions,
'label',
);
genderFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.gender),
genderOptions,
'label',
);
nationalityFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.nationality),
nationalityOptions,
'label',
);
});
watch(
() => optionStore.globalOption,
() => {
prefixNameFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.prefix),
prefixNameOptions,
'label',
);
genderFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.gender),
genderOptions,
'label',
);
nationalityFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.nationality),
nationalityOptions,
'label',
);
},
);
watch(
() => prefixName.value,
(v) => {
@ -110,7 +56,7 @@ watch(
() => gender.value,
() => {
if (props.readonly) return;
matPreFixName();
matchPreFixName();
},
);
</script>
@ -150,40 +96,19 @@ watch(
for="input-citizen-id"
/>
<div class="col-12 row" style="display: flex; gap: var(--size-2)">
<q-select
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
<SelectInput
hide-dropdown-icon
autocomplete="off"
class="col-md-1 col-6"
:dense="dense"
:readonly="readonly"
:options="prefixNameOptions"
:readonly
:option="optionStore.globalOption?.prefix"
:id="`${prefixId}-select-prefix-name`"
:for="`${prefixId}-select-prefix-name`"
:label="$t('personnel.form.prefixName')"
@filter="prefixNameFilter"
:model-value="readonly ? prefixName || '-' : prefixName"
@update:model-value="
(v) => (typeof v === 'string' ? (prefixName = v) : '')
:rules="
agency ? [] : [(val: string) => !!val || $t('form.error.required')]
"
@clear="prefixName = ''"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
:label="$t('personnel.form.prefixName')"
class="col-md-1 col-6"
v-model="prefixName"
/>
<q-input
:for="`${prefixId}-input-first-name`"
@ -194,7 +119,11 @@ watch(
class="col"
:label="$t('personnel.form.firstName')"
v-model="firstName"
:rules="[(val: string) => !!val || $t('form.error.required')]"
:rules="
employee || agency
? []
: [(val: string) => !!val || $t('form.error.required')]
"
/>
<q-input
@ -229,24 +158,16 @@ watch(
class="col-12 row"
style="display: flex; gap: var(--size-2)"
>
<q-input
:for="`${prefixId}-input-first-name`"
:dense="dense"
outlined
hide-bottom-space
:readonly="readonly"
:disable="!readonly"
<SelectInput
hide-dropdown-icon
:readonly
:option="optionStore.rawOption?.eng.prefix"
:id="`${prefixId}-select-prefix-name-en`"
:for="`${prefixId}-select-prefix-name-en`"
:rules="[(val: string) => !!val || $t('form.error.required')]"
label="Prefix"
class="col-md-1 col-6"
label="Title"
:model-value="
readonly
? capitalize(prefixName || '') || '-'
: capitalize(prefixName || '')
"
@update:model-value="
(v) => (typeof v === 'string' ? (prefixName = v) : '')
"
@clear="prefixName = ''"
v-model="prefixName"
/>
<q-input
@ -287,10 +208,16 @@ watch(
class="col"
label="Surname"
v-model="lastNameEN"
:rules="[
(val: string) =>
!val || /^[A-Za-z\s]+$/.test(val) || $t('form.error.letterOnly'),
]"
:rules="
employee
? []
: [
(val: string) =>
!val ||
/^[A-Za-z\s]+$/.test(val) ||
$t('form.error.letterOnly'),
]
"
/>
</div>
@ -326,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) : '')"
@ -351,39 +275,16 @@ watch(
</template>
</q-input>
<q-select
<SelectInput
v-if="!employee"
outlined
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
autocomplete="off"
class="col-md-2 col-6"
:dense="dense"
:readonly="readonly"
:options="genderOptions"
:hide-dropdown-icon="readonly"
:readonly
:option="optionStore.globalOption?.gender"
:id="`${prefixId}-select-gender`"
:for="`${prefixId}-select-gender`"
:label="$t('form.gender')"
@filter="genderFilter"
:model-value="readonly ? gender || '-' : gender"
@update:model-value="(v) => (typeof v === 'string' ? (gender = v) : '')"
@clear="gender = ''"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
</template>
</q-select>
class="col-md-2 col-6"
v-model="gender"
/>
<DatePicker
v-model="birthDate"
@ -456,72 +357,67 @@ watch(
"
/>
<q-select
<SelectInput
v-if="employee"
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
autocomplete="off"
hide-bottom-space
input-debounce="0"
option-label="label"
option-value="value"
class="col-md-2 col-6"
:dense="dense"
v-model="gender"
:readonly="readonly"
:options="genderOptions"
:hide-dropdown-icon="readonly"
:readonly
:option="optionStore.globalOption?.gender"
:id="`${prefixId}-select-gender`"
:for="`${prefixId}-select-gender`"
:label="$t('form.gender')"
:rules="[(val: string) => !!val || $t('form.error.required')]"
@filter="genderFilter"
>
<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
v-if="employee"
outlined
clearable
use-input
fill-input
emit-value
map-options
hide-selected
hide-bottom-space
autocomplete="off"
input-debounce="0"
option-label="label"
option-value="value"
v-model="nationality"
class="col-md-2 col-6"
:dense="dense"
:readonly="readonly"
:options="nationalityOptions"
:hide-dropdown-icon="readonly"
v-model="gender"
/>
<SelectInput
v-if="employee"
:readonly
:option="optionStore.globalOption.nationality"
:id="`${prefixId}-select-nationality`"
:for="`${prefixId}-select-nationality`"
:label="$t('general.nationality')"
:rules="[(val: string) => !!val || $t('form.error.required')]"
@filter="nationalityFilter"
class="col-md-2 col-6"
v-model="nationality"
clearable
/>
<q-input
v-if="agency"
for="input-agencies-contact-name"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-4 col-12"
:label="$t('personnel.form.contactName')"
:model-value="readonly ? contactName || '-' : contactName"
@update:model-value="
(v) => (typeof v === 'string' ? (contactName = v) : '')
"
/>
<q-input
v-if="agency"
for="input-agencies-contact-tel"
id="input-agencies-contact-tel"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-4 col-12"
:label="$t('personnel.form.contactTel')"
:model-value="readonly ? contactTel || '-' : contactTel"
@update:model-value="
(v) => (typeof v === 'string' ? (contactTel = v) : '')
"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
{{ $t('general.noData') }}
</q-item-section>
</q-item>
<template #prepend>
<q-icon
size="xs"
name="mdi-phone-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-select>
</q-input>
</div>
</div>
</template>

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

@ -106,7 +106,7 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
:readonly="readonly || employeeOther.statusSave"
hide-bottom-space
class="col-md-3 col-6"
:label="$t('form.firstName')"
:label="$t('general.nativeLanguage', { msg: $t('form.firstName') })"
:model-value="employeeOther.fatherFirstName"
@update:model-value="
(v) =>
@ -122,7 +122,7 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
:readonly="readonly || employeeOther.statusSave"
hide-bottom-space
class="col-md-3 col-6"
:label="$t('form.lastName')"
:label="$t('general.nativeLanguage', { msg: $t('form.lastName') })"
:model-value="employeeOther.fatherLastName"
@update:model-value="
(v) =>
@ -177,7 +177,7 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
:readonly="readonly || employeeOther.statusSave"
hide-bottom-space
class="col-md-3 col-6"
:label="$t('form.firstName')"
:label="$t('general.nativeLanguage', { msg: $t('form.firstName') })"
:model-value="employeeOther.motherFirstName"
@update:model-value="
(v) =>
@ -193,7 +193,7 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
:readonly="readonly || employeeOther.statusSave"
hide-bottom-space
class="col-md-3 col-6"
:label="$t('form.lastName')"
:label="$t('general.nativeLanguage', { msg: $t('form.lastName') })"
:model-value="employeeOther.motherLastName"
@update:model-value="
(v) =>

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>[]>([]);
@ -103,7 +107,7 @@ onMounted(() => {
'label',
);
passportIssuingCountryFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.nationality),
ref(optionStore.rawOption?.eng.nationality),
passportIssuingCountryOptions,
'label',
);
@ -121,13 +125,13 @@ onMounted(() => {
);
genderFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.gender),
ref(optionStore.rawOption?.eng.gender),
genderOptions,
'label',
);
nationalityFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.nationality),
ref(optionStore.rawOption?.eng.nationality),
nationalityOptions,
'label',
);
@ -152,7 +156,7 @@ watch(
);
passportIssuingCountryFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption.nationality),
ref(optionStore.rawOption?.eng.nationality),
passportIssuingCountryOptions,
'label',
);
@ -164,19 +168,43 @@ watch(
);
genderFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.gender),
ref(optionStore.rawOption?.eng.gender),
genderOptions,
'label',
);
nationalityFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.nationality),
ref(optionStore.rawOption?.eng.nationality),
nationalityOptions,
'label',
);
},
);
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"
@ -258,7 +280,7 @@ watch(
:options="workerStatusOptions"
:hide-dropdown-icon="readonly"
:for="`${prefixId}-select-visa-type`"
:label="$t('customerEmployee.form.workerType')"
:label="$t('customerEmployee.form.workerStatus')"
@filter="workerStatusFilter"
:model-value="readonly ? workerStatus || '-' : workerStatus"
@update:model-value="

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>('visaType');
const expireDate = defineModel<Date>('expireDate');
const type = defineModel<string>('type');
const expireDate = defineModel<Date | string>('expireDate');
const remark = defineModel<string>('remark');
const workerType = defineModel<string>('workerType');
const number = defineModel<string>('visaNumber');
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;
@ -78,6 +80,12 @@ onMounted(async () => {
await fetchProvince();
});
const visaIssueCountryOptions = ref<Record<string, unknown>[]>([]);
let visaIssueCountryFilter: (
value: string,
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
) => void;
const visaTypeOptions = ref<Record<string, unknown>[]>([]);
let visaTypeFilter: (
value: string,
@ -92,11 +100,17 @@ let workerTypeFilter: (
onMounted(() => {
visaTypeFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.nationality),
ref(optionStore.globalOption?.visaType),
visaTypeOptions,
'label',
);
visaIssueCountryFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.nationality),
visaIssueCountryOptions,
'label',
);
workerTypeFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.workerType),
workerTypeOptions,
@ -107,8 +121,14 @@ onMounted(() => {
watch(
() => optionStore.globalOption,
() => {
visaIssueCountryFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.nationality),
visaIssueCountryOptions,
'label',
);
visaTypeFilter = selectFilterOptionRefMod(
optionStore.globalOption.nationality,
optionStore.globalOption.visaType,
visaTypeOptions,
'label',
);
@ -120,6 +140,10 @@ watch(
);
},
);
//
// watch([() => issueDate.value], () => {
// reportDate.value = calculate90DayNext(issueDate.value);
// });
</script>
<template>
@ -133,7 +157,7 @@ watch(
name="mdi-passport"
style="background-color: var(--surface-3)"
/>
{{ title }}
{{ $t(title) }}
</div>
<div
@ -353,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>
@ -422,11 +448,11 @@ watch(
class="col-md-4 col-6"
:dense="dense"
:readonly="readonly"
:options="visaTypeOptions"
:options="visaIssueCountryOptions"
:hide-dropdown-icon="readonly"
:for="`${prefixId}-select-issue-country`"
:label="$t('customerEmployee.form.issueCountry')"
@filter="visaTypeFilter"
@filter="visaIssueCountryFilter"
:model-value="readonly ? issueCountry || '-' : issueCountry"
@update:model-value="
(v) => (typeof v === 'string' ? (issueCountry = v) : '')

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

@ -3,6 +3,7 @@ import { QSelect } from 'quasar';
import { CustomerBranch } from 'stores/customer/types';
import { selectFilterOptionRefMod } from 'stores/utils';
import { onMounted, ref, watch } from 'vue';
import SelectCustomer from 'components/shared/select/SelectCustomer.vue';
import {
EditButton,
DeleteButton,
@ -11,6 +12,7 @@ import {
} from 'components/button';
import { useI18n } from 'vue-i18n';
import useOptionStore from 'stores/options';
import { formatAddress } from 'src/utils/address';
const { locale } = useI18n();
@ -21,6 +23,13 @@ const optionsBranch = defineModel<{ id: string; name: string }[]>(
);
// employee
const customerBranchId = defineModel<string>('customerBranchId');
const currentCustomerBranch = defineModel<CustomerBranch>(
'currentCustomerBranch',
);
const customerBranch = defineModel<{
id: string;
address: string;
@ -45,6 +54,7 @@ defineProps<{
employeeOwnerOption?: CustomerBranch[];
prefixId: string;
showBtnSave?: boolean;
disableCustomerSelect?: boolean;
}>();
defineEmits<{
@ -107,180 +117,18 @@ defineEmits<{
</div>
<div class="col-12 row" style="gap: var(--size-2)">
<q-select
:id="`${prefixId}-select-employer-branch`"
:for="`${prefixId}-select-employer-branch`"
:use-input="!customerBranch"
autocomplete="off"
input-debounce="0"
:hide-dropdown-icon="readonly"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-12"
<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')"
v-model="customerBranch"
:option-value="
(v) => ({
id: v.id,
address: v.address,
addressEN: v.addressEN,
provinceId: v.provinceId,
districtId: v.districtId,
subDistrictId: v.subDistrictId,
zipCode: v.zipCode,
})
"
emit-value
map-options
:options="employeeOwnerOption"
@filter="(val, update) => $emit('filterOwnerBranch', val, update)"
:rules="[
(val: string) =>
!!val ||
$t('form.error.selectField', {
field: $t('customerEmployee.branch'),
}),
]"
>
<template v-slot:option="scope">
<q-item
v-if="scope.opt"
v-bind="scope.itemProps"
class="row items-start col-12 no-padding"
>
<div class="q-ma-sm">
<i class="isax isax-frame5" style="color: var(--brand-1)" />
</div>
<div class="q-mt-sm">
<div>
<span v-if="scope.opt.customer.customerType">
<span style="font-weight: 600">
{{
scope.opt.customer.customerType === 'CORP'
? $t('customer.form.registerName')
: $t('customer.form.ownerName')
}}:
</span>
{{
scope.opt.customer.customerType === 'CORP'
? $i18n.locale === 'eng'
? scope.opt.registerNameEN
: scope.opt.registerName
: $i18n.locale === 'eng'
? `${optionStore.mapOption(scope.opt.namePrefix)} ${scope.opt.firstNameEN} ${scope.opt.lastNameEN}` ||
'-'
: `${optionStore.mapOption(scope.opt.namePrefix)} ${scope.opt.firstName} ${scope.opt.lastName}` ||
'-'
}}
({{ scope.opt.code }})
</span>
</div>
<div class="text-caption app-text-muted-2 q-mb-xs">
<span v-if="scope.opt.customer" class="col column">
{{
$t(
`branch.form.title.${scope.opt.code.endsWith('-00') ? 'branchHQLabel' : 'branchLabel'}`,
)
}}
{{
!scope.opt.code.endsWith('-00')
? +scope.opt.code.split('-')[1]
: ''
}}
</span>
<span v-if="scope.opt.province" class="col">
{{ $t('general.address') }}
{{
$i18n.locale === 'eng'
? `${scope.opt.addressEN || ''}, ${scope.opt.mooEN && `${$t('form.moo')} ${scope.opt.mooEN},`} ${scope.opt.soiEN && `${$t('form.soi')} ${scope.opt.soiEN},`} ${scope.opt.streetEN && `${scope.opt.streetEN} Rd,`} ${scope.opt.subDistrict.nameEN || ''}, ${scope.opt.district.nameEN || ''}, ${scope.opt.province.nameEN || ''}`
: `${scope.opt.address || ''}, ${scope.opt.moo && `${$t('form.moo')} ${scope.opt.moo},`} ${scope.opt.soi && `${$t('form.soi')} ${scope.opt.soi},`} ${scope.opt.street && `${$t('form.road')} ${scope.opt.street},`} ${scope.opt.subDistrict.name || ''}, ${scope.opt.district.name || ''}, ${scope.opt.province.name || ''}`
}}
{{ scope.opt.subDistrict?.zipCode || '' }}
</span>
</div>
</div>
</q-item>
<q-separator class="q-mx-sm" />
</template>
<template v-slot:selected-item="scope">
<div
v-if="scope.opt"
class="row items-center no-wrap"
style="width: 1px"
>
<div class="q-mr-sm">
<span style="font-weight: 600">
{{
scope.opt.customer.customerType === 'CORP'
? $t('customer.form.registerName')
: $t('customer.form.ownerName')
}}:
</span>
{{
scope.opt.customer.customerType === 'CORP'
? $i18n.locale === 'eng'
? scope.opt.registerNameEN
: scope.opt.registerName
: $i18n.locale === 'eng'
? `${optionStore.mapOption(scope.opt.namePrefix)} ${scope.opt.firstNameEN} ${scope.opt.lastNameEN}` ||
'-'
: `${optionStore.mapOption(scope.opt.namePrefix)} ${scope.opt.firstName} ${scope.opt.lastName}` ||
'-'
}}
({{ scope.opt.code }})
</div>
<div
class="text-caption app-text-muted-2"
v-if="scope.opt.customer && scope.opt.province"
>
{{
$t(
`branch.form.title.${scope.opt.code.endsWith('-00') ? 'branchHQLabel' : 'branchLabel'}`,
)
}}
{{
!scope.opt.code.endsWith('-00')
? +scope.opt.code.split('-')[1]
: ''
}}
{{ $t('general.address') }}
{{
$i18n.locale === 'eng'
? `${scope.opt.addressEN || ''}, ${scope.opt.mooEN && `${$t('form.moo')} ${scope.opt.mooEN},`} ${scope.opt.soiEN && `${$t('form.soi')} ${scope.opt.soiEN},`} ${scope.opt.streetEN && `${scope.opt.streetEN} Rd,`} ${scope.opt.subDistrict.nameEN || ''}, ${scope.opt.district.nameEN || ''}, ${scope.opt.province.nameEN || ''}`
: `${scope.opt.address || ''}, ${scope.opt.moo && `${$t('form.moo')} ${scope.opt.moo},`} ${scope.opt.soi && `${$t('form.soi')} ${scope.opt.soi},`} ${scope.opt.street && `${$t('form.road')} ${scope.opt.street},`} ${scope.opt.subDistrict.name || ''}, ${scope.opt.district.name || ''}, ${scope.opt.province.name || ''}`
}}
{{ scope.opt.subDistrict?.zipCode || '' }}
<q-tooltip v-if="scope.opt.customer && scope.opt.province">
{{ $t('customerBranch.form.title') }}:
{{ $t('general.address') }}
{{
$i18n.locale === 'eng'
? `${scope.opt.addressEN || ''}, ${scope.opt.mooEN && `${$t('form.moo')} ${scope.opt.mooEN},`} ${scope.opt.soiEN && `${$t('form.soi')} ${scope.opt.soiEN},`} ${scope.opt.streetEN && `${scope.opt.streetEN} Rd,`} ${scope.opt.subDistrict.nameEN || ''}, ${scope.opt.district.nameEN || ''}, ${scope.opt.province.nameEN || ''}`
: `${scope.opt.address || ''}, ${scope.opt.moo && `${$t('form.moo')} ${scope.opt.moo},`} ${scope.opt.soi && `${$t('form.soi')} ${scope.opt.soi},`} ${scope.opt.street && `${$t('form.road')} ${scope.opt.street},`} ${scope.opt.subDistrict.name || ''}, ${scope.opt.district.name || ''}, ${scope.opt.province.name || ''}`
}}
{{ scope.opt.subDistrict?.zipCode || '' }}
</q-tooltip>
</div>
</div>
</template>
<template v-slot:append>
<q-icon
v-if="!readonly && customerBranch"
name="mdi-close-circle"
@click.stop="customerBranch = undefined"
class="cursor-pointer clear-btn"
/>
</template>
</q-select>
class="col-12 field-two"
simple
required
:readonly
:disabled="disableCustomerSelect && !readonly"
/>
<q-input
:for="`${prefixId}-input-code`"

View file

@ -6,12 +6,14 @@ import { useI18n } from 'vue-i18n';
import useUserStore from 'src/stores/user';
import useOptionStore from 'src/stores/options';
import { useWorkflowTemplate } from 'src/stores/workflow-template';
import { baseUrl } from 'stores/utils';
import { getRole } from 'src/services/keycloak';
import {
WorkflowUserInTable,
WorkflowTemplatePayload,
WorkFlowPayloadStep,
Group,
} from 'src/stores/workflow-template/types';
import { User } from 'src/stores/user/types';
@ -20,15 +22,18 @@ import ToggleButton from 'src/components/button/ToggleButton.vue';
import NoData from '../NoData.vue';
import SelectBranch from '../shared/select/SelectBranch.vue';
import AddButton from '../button/AddButton.vue';
import { QField } from 'quasar';
defineProps<{
readonly?: boolean;
onDrawer?: boolean;
hideAction?: boolean;
}>();
const { t } = useI18n();
const userStore = useUserStore();
const optionStore = useOptionStore();
const workflowStore = useWorkflowTemplate();
const userInTable = defineModel<WorkflowUserInTable[]>('userInTable', {
default: [],
@ -43,7 +48,7 @@ const flowData = defineModel<WorkflowTemplatePayload>('flowData', {
},
});
const objectOptions = [
let objectOptions = [
...(optionStore.globalOption?.agenciesType || []),
{ label: t('flow.customer'), value: 'customer' },
{ label: t('flow.officer'), value: 'officer' },
@ -51,7 +56,9 @@ const objectOptions = [
const options = ref(objectOptions);
const role = ref<string[]>([]);
const userList = ref<User[]>([]);
const groupList = ref<Group[]>([]);
const responsiblePersonSearch = ref('');
const responsibleMenu = ref(false);
async function getUserList(opts?: { query: string }) {
const resUser = await userStore.fetchList({
@ -60,10 +67,10 @@ async function getUserList(opts?: { query: string }) {
if (resUser) userList.value = resUser.result;
}
// async function getUserById(responsiblePersonId: string) {
// const resUser = await userStore.fetchById(responsiblePersonId);
// if (resUser) userInTable.value.push(resUser);
// }
async function getGroupList() {
const resGroup = await workflowStore.getGroupList();
if (resGroup) groupList.value = resGroup;
}
function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
const currStep = flowData.value.step[stepIndex];
@ -78,6 +85,7 @@ function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
userInTable.value[stepIndex] = {
name: flowData.value.step[stepIndex].name,
responsiblePerson: [],
responsibleGroup: [],
};
}
@ -101,6 +109,33 @@ function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
}
}
function selectResponsibleGroup(stepIndex: number, responsibleGroup: string) {
const currStep = flowData.value.step[stepIndex];
const existGroupIndex = currStep.responsibleGroup?.findIndex(
(p) => p === responsibleGroup,
);
if (existGroupIndex === -1) {
currStep.responsibleGroup?.push(responsibleGroup);
if (!userInTable.value[stepIndex]) {
userInTable.value[stepIndex] = {
name: flowData.value.step[stepIndex].name,
responsiblePerson: [],
responsibleGroup: [],
};
}
userInTable.value[stepIndex]?.responsibleGroup.push(responsibleGroup);
} else {
currStep.responsibleGroup?.splice(Number(existGroupIndex), 1);
userInTable.value[stepIndex]?.responsibleGroup.splice(
Number(existGroupIndex),
1,
);
}
}
function selectItem(
val: Record<string, unknown>,
responsibleInstitution?: string[],
@ -142,6 +177,7 @@ watch(
onMounted(async () => {
role.value = getRole() || [];
await getUserList();
await getGroupList();
await userStore.fetchHqOption();
});
</script>
@ -166,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'"
@ -467,92 +504,128 @@ onMounted(async () => {
</div>
<!-- RESPONSIBLE-PERSON -->
<q-select
<q-field
v-if="step.responsiblePersonId"
behavior="menu"
:for="`select-responsible-person-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
:bg-color="readonly ? 'transparent' : ''"
:readonly
outlined
dense
v-model="step.responsiblePersonId"
multiple
:options="[1, 2, 3]"
hide-bottom-space
option-label="label"
option-value="value"
emit-value
:stack-label="
userInTable[index]?.responsiblePerson.length > 0 ||
userInTable[index]?.responsibleGroup.length > 0
"
:label="$t('flow.responsiblePerson')"
dense
class="col-md-6 col-12"
:hide-dropdown-icon="readonly"
:class="{ 'cursor-pointer': !readonly }"
>
<template v-slot:selected-item="scope">
<div class="column full-width">
<div
class="row items-center no-wrap"
v-for="person in userInTable[
index
]?.responsiblePerson.filter(
(p) => p.id === scope.opt,
)"
:key="person.id"
>
<q-avatar class="q-ml-sm" size="md">
<q-img
class="text-center"
:ratio="1"
:src="`${baseUrl}/user/${person.id}/profile-image/${person.selectedImage}`"
>
<template #error>
<div
class="no-padding full-width full-height flex items-center justify-center"
:style="`${person.gender ? 'background: white' : 'background: linear-gradient(135deg,rgba(43, 137, 223, 1) 0%, rgba(230, 51, 81, 1) 100%);'}`"
>
<q-img
v-if="person.gender"
:src="
person.gender === 'male'
? '/no-img-man.png'
: '/no-img-female.png'
"
/>
<q-icon
v-else
size="sm"
name="mdi-account-outline"
style="color: white"
/>
</div>
</template>
</q-img>
</q-avatar>
<div
class="column q-pl-md"
style="color: var(--foreground)"
<template #control>
<q-item
dense
class="items-center full-width no-padding"
v-for="person in userInTable[
index
]?.responsiblePerson.filter((p) =>
step.responsiblePersonId.includes(p.id),
)"
:key="person.id"
>
<q-avatar class="q-ml-sm" size="md">
<q-img
class="text-center"
:ratio="1"
:src="`${baseUrl}/user/${person.id}/profile-image/${person.selectedImage}`"
>
<span>
{{
`${optionStore.mapOption(person.namePrefix || '')} ${
$i18n.locale === 'eng'
? person.firstNameEN
: person.firstName
} ${
$i18n.locale === 'eng'
? person.lastNameEN
: person.lastName
}`
}}
</span>
<span class="text-caption app-text-muted">
{{ person.code }}
</span>
</div>
<template #error>
<div
class="no-padding full-width full-height flex items-center justify-center"
:style="`${person.gender ? 'background: white' : 'background: linear-gradient(135deg,rgba(43, 137, 223, 1) 0%, rgba(230, 51, 81, 1) 100%);'}`"
>
<q-img
v-if="person.gender"
:src="
person.gender === 'male'
? '/no-img-man.png'
: '/no-img-female.png'
"
/>
<q-icon
v-else
size="sm"
name="mdi-account-outline"
style="color: white"
/>
</div>
</template>
</q-img>
</q-avatar>
<div
class="column q-pl-md"
style="color: var(--foreground)"
>
<span>
{{
`${optionStore.mapOption(person.namePrefix || '')} ${
$i18n.locale === 'eng'
? person.firstNameEN
: person.firstName
} ${
$i18n.locale === 'eng'
? person.lastNameEN
: person.lastName
}`
}}
</span>
<span class="text-caption app-text-muted">
{{ person.code }}
</span>
</div>
</div>
</template>
</q-item>
<template v-slot:option></template>
<q-menu v-if="!readonly" :offset="[0, 4]">
<div
v-if="step.responsibleGroup.length > 0"
class="full-width app-text-muted text-weight-medium"
style="font-size: 10px"
>
{{ $t('general.group') }}
</div>
<q-item
class="items-center full-width no-padding"
v-for="group in userInTable[
index
]?.responsibleGroup.filter((g) =>
step.responsibleGroup.includes(g),
)"
:key="group"
dense
>
<q-avatar class="q-ml-sm" size="md">
<q-img
class="text-center"
:ratio="1"
:src="`/img-group.png`"
/>
</q-avatar>
<span class="q-pl-md">
{{ group }}
</span>
</q-item>
</template>
<template v-if="!readonly" #append>
<q-icon
name="mdi-menu-down"
:class="{ rotated: responsibleMenu }"
class="transition-rotate"
/>
</template>
<q-menu
v-if="!readonly"
no-focus
no-refocus
:offset="[0, 4]"
@before-show="() => (responsibleMenu = true)"
@before-hide="() => (responsibleMenu = false)"
>
<q-list>
<q-item>
<q-input
@ -581,6 +654,7 @@ onMounted(async () => {
{{ $t('general.noData') }}
</q-item>
<q-item
v-else
v-for="(person, i) in userList"
dense
:key="i"
@ -655,6 +729,7 @@ onMounted(async () => {
{{ $t('personnel.MESSENGER') }}
</span>
<q-item
dense
clickable
@click="step.messengerByArea = !step.messengerByArea"
class="column"
@ -670,9 +745,49 @@ onMounted(async () => {
</div>
</div>
</q-item>
<span class="text-caption app-text-muted-2 q-px-md">
{{ $t('general.group') }}
</span>
<q-item
v-if="groupList.length === 0"
class="app-text-muted q-px-lg"
>
{{ $t('general.noData') }}
</q-item>
<q-item
v-else
v-for="(group, i) in groupList"
dense
clickable
@click="selectResponsibleGroup(index, group.name)"
class="column"
>
<div class="row items-center">
<q-checkbox
size="xs"
:model-value="
step.responsibleGroup.includes(group.name)
"
@click.stop="
selectResponsibleGroup(index, group.name)
"
/>
<q-avatar class="q-ml-sm" size="md">
<q-img
class="text-center"
:ratio="1"
:src="`/img-group.png`"
/>
</q-avatar>
<div class="column q-pl-md">
<span>{{ group.name }}</span>
</div>
</div>
</q-item>
</q-list>
</q-menu>
</q-select>
</q-field>
<!-- RESPONSIBLE-AGENCIES, RESPONSIBLE-INSTITUTION -->
<q-select
@ -787,8 +902,8 @@ onMounted(async () => {
}
: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;
@ -800,19 +915,26 @@ onMounted(async () => {
}
: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;
}
:deep(.q-dialog.fullscreen.no-pointer-events.q-dialog--modal) {
visibility: hidden;
}
.transition-rotate {
transition: transform 0.3s ease;
}
.rotated {
transform: rotate(180deg);
}
</style>

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
@ -244,16 +246,19 @@ withDefaults(
<template v-if="col.name === '#calcVat'">
<q-checkbox
v-if="priceDisplay?.price && props.rowIndex === 0"
:disable="readonly"
v-model="calcVat"
size="xs"
/>
<q-checkbox
v-if="priceDisplay?.agentPrice && props.rowIndex === 1"
:disable="readonly"
v-model="agentPriceCalcVat"
size="xs"
/>
<q-checkbox
v-if="priceDisplay?.serviceCharge && props.rowIndex === 2"
:disable="readonly"
v-model="serviceChargeCalcVat"
size="xs"
/>
@ -271,6 +276,8 @@ withDefaults(
flat
outlined
dense
:readonly
:hide-dropdown-icon="readonly"
v-model="vatIncluded"
></q-select>
<q-select
@ -285,6 +292,8 @@ withDefaults(
flat
outlined
dense
:readonly
:hide-dropdown-icon="readonly"
v-model="agentPriceVatIncluded"
></q-select>
<q-select
@ -299,6 +308,8 @@ withDefaults(
flat
outlined
dense
:readonly
:hide-dropdown-icon="readonly"
v-model="serviceChargeVatIncluded"
></q-select>
</template>

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,12 +14,31 @@ 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: '' });
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">
@ -35,7 +58,7 @@ type Options = { label: string; value: string };
<div class="col-12 row q-col-gutter-sm">
<SelectInput
:class="{ col: $q.screen.lt.md }"
class="col"
:disable="!readonly && onDrawer"
:readonly="readonly"
for="input-agencies-code"
@ -62,10 +85,15 @@ type Options = { label: string; value: string };
outlined
:readonly="readonly"
hide-bottom-space
class="col-md col-12"
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"
@ -73,10 +101,159 @@ type Options = { label: string; value: string };
outlined
:readonly="readonly"
hide-bottom-space
class="col-md col-12"
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
for="input-agencies-contact-name"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-4 col-12"
:label="$t('agencies.contactName')"
:model-value="readonly ? contactName || '-' : contactName"
@update:model-value="
(v) => (typeof v === 'string' ? (contactName = v) : '')
"
/>
<q-input
for="input-agencies-email"
dense
outlined
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'),
]
"
class="col-md-4 col-12"
:model-value="readonly ? email || '-' : email"
@update:model-value="(v) => (typeof v === 'string' ? (email = v) : '')"
@clear="email = ''"
>
<template #prepend>
<q-icon
size="xs"
name="mdi-email-outline"
class="cursor-pointer"
color="primary"
/>
</template>
</q-input>
<q-input
for="input-agencies-contact-tel"
id="input-agencies-contact-tel"
dense
outlined
:readonly="readonly"
hide-bottom-space
class="col-md-4 col-12"
:label="$t('agencies.contactTel')"
:model-value="readonly ? contactTel || '-' : contactTel"
@update:model-value="
(v) => (typeof v === 'string' ? (contactTel = v) : '')
"
>
<template #prepend>
<q-icon
size="xs"
name="mdi-phone-outline"
class="cursor-pointer"
color="primary"
/>
</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

@ -4,7 +4,7 @@ import { useQuasar } from 'quasar';
import SignaturePad from 'signature_pad';
import Cropper from 'cropperjs';
defineExpose({ clearCanvas, clearUpload });
defineExpose({ setCanvas, getCanvas, clearCanvas, clearUpload });
const $q = useQuasar();
const isDarkActive = computed(() => $q.dark.isActive);
@ -18,7 +18,7 @@ const cropper = ref();
const tab = ref('draw');
const uploadFile = ref<File | undefined>(undefined);
const profileUrl = ref<string | null>('');
const imgUrl = ref<string | null>('');
const inputFile = (() => {
const element = document.createElement('input');
element.type = 'file';
@ -26,7 +26,7 @@ const inputFile = (() => {
const reader = new FileReader();
reader.addEventListener('load', () => {
if (typeof reader.result === 'string') profileUrl.value = reader.result;
if (typeof reader.result === 'string') imgUrl.value = reader.result;
});
element.addEventListener('change', () => {
@ -39,12 +39,11 @@ const inputFile = (() => {
return element;
})();
async function initializeSignaturePad(canva?: HTMLCanvasElement) {
if (canva) {
signaturePad.value = new SignaturePad(canva, {
backgroundColor: isDarkActive.value
? 'rgb(21,25,29)'
: 'rgb(248,249,250)',
async function initializeSignaturePad() {
const canvas = canvasRef.value;
if (canvas) {
signaturePad.value = new SignaturePad(canvas, {
penColor: 'blue',
});
} else {
@ -77,34 +76,34 @@ function changeColor(color: string) {
currentColor.value = color;
}
function setCanvas() {
const data = signaturePad.value.toDataURL('image/png');
return data;
}
function getCanvas(signature: string) {
signaturePad.value.fromDataURL(signature);
}
function clearCanvas() {
signaturePad.value.clear();
}
function clearUpload() {
profileUrl.value = '';
imgUrl.value = '';
}
watch(
() => tab.value,
async () => {
await initializeSignaturePad(canvasRef.value);
await initializeCropper(imageRef.value);
},
);
onMounted(async () => {
await initializeSignaturePad(canvasRef.value);
await initializeCropper(imageRef.value);
await initializeSignaturePad();
});
</script>
<template>
<div class="surface-1 bordered rounded full-width">
<div class="surface-1 column full-width full-height">
<q-tabs
v-model="tab"
dense
align="left"
class="text-grey"
class="text-grey surface-2"
active-color="primary"
indicator-color="primary"
>
@ -112,18 +111,18 @@ onMounted(async () => {
<div class="row">
<q-tab
name="draw"
label="Draw"
:label="$t('general.draw')"
style="border-top-left-radius: var(--radius-2)"
/>
<q-tab name="upload" label="Upload" />
<q-tab name="upload" :label="$t('general.upload')" />
</div>
<div class="q-pr-md">
<q-btn
v-if="tab === 'upload'"
dense
flat
v-if="tab === 'upload'"
:label="$t('newUpload')"
:label="$t('general.newUpload')"
color="info"
@click="inputFile.click()"
/>
@ -132,89 +131,66 @@ onMounted(async () => {
</q-tabs>
<q-separator />
<div v-show="tab === 'draw'" class="q-pa-md">
<section v-show="tab === 'draw'" class="q-pa-md col">
<div class="column relative-position">
<div class="absolute-top-right q-ma-md q-gutter-x-md row items-center">
<article
class="absolute-top-right q-ma-md q-gutter-x-md row items-center"
>
<span
v-for="color in ['black', 'red', 'blue']"
:key="color"
:class="{ active: currentColor === color }"
class="dot"
:class="{ active: currentColor === 'black' }"
style="background-color: black"
@click="changeColor('black')"
:style="`background-color: ${color}`"
@click="changeColor(color)"
>
<q-icon
v-if="currentColor === 'black'"
v-if="currentColor === color"
name="mdi-check"
color="white"
size="sm"
/>
</span>
<span
:class="{ active: currentColor === 'red' }"
class="dot"
style="background-color: red"
@click="changeColor('red')"
>
<q-icon
v-if="currentColor === 'red'"
name="mdi-check"
color="white"
size="sm"
/>
</span>
<span
:class="{ active: currentColor === 'blue' }"
class="dot"
style="background-color: blue"
@click="changeColor('blue')"
>
<q-icon
v-if="currentColor === 'blue'"
name="mdi-check"
color="white"
size="sm"
/>
</span>
</div>
</article>
<canvas
class="signature-canvas"
ref="canvasRef"
id="signature-pad"
width="700"
height="310"
width="766"
height="364"
></canvas>
</div>
</div>
</section>
<div v-show="tab === 'upload'" class="q-pa-md">
<section v-show="tab === 'upload'" class="q-pa-md col">
<div
class="bordered upload-border rounded column items-center justify-center"
style="height: 312px"
class="bordered upload-border rounded column items-center justify-center full-height"
>
<q-img
v-show="profileUrl"
v-show="imgUrl"
ref="imageRef"
:src="profileUrl ?? ''"
:src="imgUrl ?? ''"
style="object-fit: cover; width: 100%; height: 100%"
/>
<div v-if="!profileUrl">
<div v-if="!imgUrl">
<q-icon
name="mdi-cloud-upload"
size="10rem"
style="color: hsla(var(--text-mute) / 0.2)"
/>
<div>
<div class="text-center">
<q-btn
unelevated
color="info"
:label="$t('uploadFile')"
:label="$t('general.upload')"
icon="mdi-plus"
@click="inputFile.click()"
/>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<style scoped lang="scss">

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

@ -1,6 +1,9 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
defineProps<{
hideIcon?: boolean;
icon?: string;
}>();
</script>
@ -10,10 +13,13 @@ defineProps<{
v-if="!hideIcon"
id="btn-add"
padding="sm"
icon="mdi-plus"
:icon="icon ? undefined : 'mdi-plus'"
direction="up"
class="color-btn"
>
<template #icon v-if="icon">
<Icon :icon width="24" />
</template>
<slot>
<q-fab-action
padding="xs"
@ -29,10 +35,12 @@ defineProps<{
fab
id="btn-add"
padding="sm"
icon="mdi-plus"
:icon="icon ? undefined : 'mdi-plus'"
direction="up"
class="color-btn"
/>
>
<Icon v-if="icon" :icon width="24" />
</q-btn>
</q-page-sticky>
</template>

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

@ -1,8 +1,10 @@
<script lang="ts" setup>
import { ref } from 'vue';
import MainButton from './MainButton.vue';
defineEmits<{
const emit = defineEmits<{
(e: 'click', v: MouseEvent): void;
(e: 'fileSelected', v: File[]): void;
}>();
defineProps<{
iconOnly?: boolean;
@ -10,15 +12,29 @@ defineProps<{
outlined?: boolean;
disabled?: boolean;
dark?: boolean;
importFile?: boolean;
label?: string;
icon?: string;
}>();
const inputRef = ref<HTMLInputElement | null>(null);
function triggerFileInput() {
inputRef.value?.click();
}
function handleFileChange(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
emit('fileSelected', Array.from(files));
}
}
</script>
<template>
<MainButton
@click="(e) => $emit('click', e)"
@click="(e) => (importFile ? triggerFileInput() : $emit('click', e))"
v-bind="{ ...$props, ...$attrs }"
:icon="icon || 'mdi-import'"
color="var(--info-bg)"
@ -26,4 +42,13 @@ defineProps<{
>
{{ label || $t('general.import') }}
</MainButton>
<input
ref="inputRef"
type="file"
@change="(e) => handleFileChange(e)"
hidden
accept=".xls, .xlsx , .csv"
multiple
/>
</template>

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

@ -0,0 +1,32 @@
<script lang="ts" setup>
import MainButton from './MainButton.vue';
defineEmits<{
(e: 'click', v: MouseEvent): void;
}>();
defineProps<{
iconOnly?: boolean;
solid?: boolean;
outlined?: boolean;
disabled?: boolean;
dark?: boolean;
label?: string;
icon?: string;
amount?: number;
}>();
</script>
<template>
<MainButton
@click="(e) => $emit('click', e)"
v-bind="{ ...$props, ...$attrs }"
:icon="icon || 'mdi-file-replace'"
color="207 96% 32%"
:title="iconOnly ? $t('general.paste') : undefined"
>
{{ label || $t('general.paste') }}
{{ amount && amount > 0 ? `(${amount})` : '' }}
</MainButton>
</template>

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

@ -14,3 +14,4 @@ export { default as PrintButton } from './PrintButton.vue';
export { default as StateButton } from './StateButton.vue';
export { default as NextButton } from './NextButton.vue';
export { default as ImportButton } from './ImportButton.vue';
export { default as PasteButton } from './PasteButton.vue';

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

@ -0,0 +1,190 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { dateFormatJS } from 'src/utils/datetime';
import SelectInput from './SelectInput.vue';
import VueDatePicker from '@vuepic/vue-datepicker';
import dayjs from 'dayjs';
defineProps<{
active?: boolean;
}>();
const date = defineModel<string[]>();
const dateRange = ref<string>('');
const isDateSelect = ref(false);
function mapDateRange(val: string) {
const today = dayjs();
let start: dayjs.Dayjs, end: dayjs.Dayjs;
switch (val) {
case 'toDay':
start = today.startOf('day');
end = today.endOf('day');
break;
case 'yesterday':
start = today.subtract(1, 'day').startOf('day');
end = today.subtract(1, 'day').endOf('day');
break;
case 'thisWeek':
start = today.startOf('week');
end = today.endOf('week');
break;
case 'lastWeek':
start = today.subtract(1, 'week').startOf('week');
end = today.subtract(1, 'week').endOf('week');
break;
case 'thisMonth':
start = today.startOf('month');
end = today.endOf('month');
break;
case 'lastMonth':
start = today.subtract(1, 'month').startOf('month');
end = today.subtract(1, 'month').endOf('month');
break;
case 'thisYear':
start = today.startOf('year');
end = today.endOf('year');
break;
case 'lastYear':
start = today.subtract(1, 'year').startOf('year');
end = today.subtract(1, 'year').endOf('year');
break;
case 'last7Days':
start = today.subtract(6, 'day').startOf('day');
end = today.endOf('day');
break;
case 'last30Days':
start = today.subtract(29, 'day').startOf('day');
end = today.endOf('day');
break;
case 'last90Days':
start = today.subtract(89, 'day').startOf('day');
end = today.endOf('day');
break;
case 'customDateRange':
start = today.startOf('day');
end = today.endOf('day');
break;
default:
return;
}
return [start.toDate().toISOString(), end.toDate().toISOString()];
}
watch(
() => dateRange.value,
() => {
if (!dateRange.value) return;
date.value = mapDateRange(dateRange.value);
},
);
watch(
() => date.value,
() => {
if (date.value && date.value.length === 0) dateRange.value = '';
},
);
</script>
<template>
<q-btn
size="xs"
round
dense
unelevated
icon="mdi-tune-variant"
:flat="active ? false : !dateRange"
:color="active || dateRange ? 'info' : undefined"
>
<q-menu
:offset="[5, 10]"
max-width="300px"
class="bordered"
:persistent="isDateSelect"
>
<div class="q-pa-sm">
<slot name="prepend"></slot>
<div class="text-weight-medium">
{{ $t('general.advanceSearch') }}
</div>
<SelectInput
v-model="dateRange"
:label="$t('general.period')"
:option="[
{ label: $t('dateRange.today'), value: 'toDay' },
{ label: $t('dateRange.yesterday'), value: 'yesterday' },
{ label: $t('dateRange.thisWeek'), value: 'thisWeek' },
{ label: $t('dateRange.lastWeek'), value: 'lastWeek' },
{ label: $t('dateRange.thisMonth'), value: 'thisMonth' },
{ label: $t('dateRange.lastMonth'), value: 'lastMonth' },
{ label: $t('dateRange.thisYear'), value: 'thisYear' },
{ label: $t('dateRange.lastYear'), value: 'lastYear' },
{ label: $t('dateRange.last7Days'), value: 'last7Days' },
{ label: $t('dateRange.last30Days'), value: 'last30Days' },
{ label: $t('dateRange.last90Days'), value: 'last90Days' },
{
label: $t('dateRange.customDateRange'),
value: 'customDateRange',
},
]"
clearable
@clear="() => (date = [])"
/>
<VueDatePicker
v-if="dateRange === 'customDateRange'"
utc
range
teleport
auto-apply
for="select-date-range"
class="q-mt-sm"
v-model="date"
:dark="$q.dark.isActive"
:locale="$i18n.locale === 'tha' ? 'th' : 'en'"
@open="() => (isDateSelect = true)"
@closed="() => (isDateSelect = false)"
>
<template #trigger>
<q-input
placeholder="DD/MM/YYYY"
hide-bottom-space
dense
outlined
for="select-date-range"
:model-value="
date
? dateFormatJS({ date: date[0] }) +
' - ' +
dateFormatJS({ date: date[1] })
: ''
"
>
<template #prepend>
<q-icon name="mdi-calendar-outline" class="app-text-muted" />
</template>
<q-tooltip>
{{
date
? dateFormatJS({ date: date[0] }) +
' - ' +
dateFormatJS({ date: date[1] })
: ''
}}
</q-tooltip>
</q-input>
</template>
</VueDatePicker>
<slot></slot>
<!-- <SelectInput :label="$t('general.documentStatus')" :option="[]" /> -->
</div>
</q-menu>
<q-tooltip v-if="$q.screen.gt.sm">
{{ $t('general.advanceSearch') }}
</q-tooltip>
</q-btn>
</template>

View file

@ -25,7 +25,11 @@ withDefaults(
alt="Image"
/>
</div>
<div v-if="data.length > 3" class="avatar remaining-count">
<div
v-if="data.length > 3"
class="avatar remaining-count"
style="cursor: default"
>
<q-tooltip>
<div v-for="(person, i) in data.slice(3)" :key="i + 3">
{{ person.name }}

View file

@ -16,6 +16,7 @@ const props = withDefaults(
useUpload?: boolean;
useCancel?: boolean;
useRejectCancel?: boolean;
useCopy?: boolean;
disableCancel?: boolean;
disableDelete?: boolean;
}>(),
@ -31,6 +32,7 @@ defineEmits<{
(e: 'link'): void;
(e: 'upload'): void;
(e: 'delete'): void;
(e: 'copy'): void;
(e: 'cancel'): void;
(e: 'rejectCancel'): void;
(e: 'changeStatus'): void;
@ -172,6 +174,27 @@ watch(
</span>
</q-item>
<q-item
v-if="useCopy"
v-close-popup
dense
clickable
class="row q-py-sm"
style="white-space: nowrap"
:id="`btn-kebab-copy-${idName}`"
@click.stop="() => $emit('copy')"
>
<q-icon
size="xs"
class="col-3"
name="mdi-content-copy"
style="color: hsl(var(--teal-5-hsl))"
/>
<span class="col-9 q-px-md flex items-center">
{{ $t('general.copy') }}
</span>
</q-item>
<q-item
v-if="useCancel"
v-close-popup

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[];
@ -28,6 +29,7 @@ const props = withDefaults(
disable?: boolean;
multiple?: boolean;
hideInput?: boolean;
hideDropdownIcon?: boolean;
rules?: ((value: string) => string | true)[];
}>(),
@ -70,6 +72,7 @@ watch(
</script>
<template>
<q-select
:id="id"
:placeholder="placeholder"
outlined
:clearable
@ -82,7 +85,7 @@ watch(
:hide-selected
hide-bottom-space
:fill-input="fillInput && !!model"
:hide-dropdown-icon="readonly"
:hide-dropdown-icon="readonly || hideDropdownIcon"
input-debounce="500"
:option-value="
typeof props.optionValue === 'string' ? props.optionValue : 'value'
@ -103,6 +106,11 @@ watch(
}
"
:rules
@clear="
() => {
multiple ? (model = []) : (model = '');
}
"
>
<template v-if="$slots.prepend" v-slot:prepend>
<slot name="prepend"></slot>

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,8 +65,14 @@ onMounted(async () => {
setFirstValue();
}
await getSelectedOption();
if (props.selectFirstValue) {
setDefaultValue();
} else await getSelectedOption();
});
function setDefaultValue() {
setFirstValue();
}
</script>
<template>
<SelectInput
@ -158,11 +165,9 @@ onMounted(async () => {
</template>
<template #option="{ opt, scope }">
<q-item 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">
@ -175,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

@ -43,6 +43,7 @@ const { getOptions, setFirstValue, getSelectedOption, filter } =
const ret = await getList({
query: query === '' ? undefined : query,
...props.params,
activeOnly: true,
});
if (ret) return ret.result;
},

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

@ -54,7 +54,9 @@ const columns = [
field: (v: Employee) =>
locale.value === Lang.English
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName} ${v.lastName}`,
: v.firstName
? `${v.firstName} ${v.lastName}`
: `${v.firstNameEN} ${v.lastNameEN}`,
},
{
name: 'birthDate',
@ -143,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"
@ -198,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;
}
}

2
src/env.d.ts vendored
View file

@ -1,5 +1,3 @@
/* eslint-disable */
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: string;

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',
@ -60,7 +61,7 @@ export default {
branchStatus: 'Branch Status',
success: 'Success',
taxNo: 'Legal Person',
contactName: 'Contact Name',
contactName: 'Contact Person',
image: 'Image of ',
apply: 'Apply',
licenseNumber: 'License number',
@ -151,6 +152,16 @@ export default {
dueDate: 'Due date',
year: 'year',
tableOfContent: 'Table of Contents',
draw: 'Draw',
newUpload: 'New Upload',
nativeLanguage: '{msg} Native Language',
copy: 'Copy',
paste: 'Paste',
period: 'Period',
documentStatus: 'Document Status',
advanceSearch: 'Advance Search',
totalPeople: '{meg} people',
price: 'Price {price} Baht',
},
menu: {
@ -193,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: {
@ -245,7 +258,8 @@ export default {
manual: {
title: 'Manual',
usage: 'การใช้งาน',
usage: 'Usage',
troubleshooting: 'Troubleshooting',
},
},
@ -327,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: {
@ -374,7 +388,7 @@ export default {
branchLabel: 'Branch',
branchHQLabel: 'Headoffice',
taxNo: 'Legal Person',
contactName: 'Contact Name',
contactName: 'Contact Person',
},
page: {
captionManage: 'Manage',
@ -395,8 +409,8 @@ export default {
code: 'Headoffice Code',
codeBranch: 'Branch Code',
taxNo: 'Tax Identification Number',
contactName: 'Contact Name',
contactTelephone: 'Contact Telephone',
contactName: 'Contact Person',
contactTelephone: 'Contact Number',
branchName: 'Branch Name',
branchNameEN: 'Branch Name (EN)',
servicePointName: 'Service Point Name',
@ -446,10 +460,10 @@ 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',
importNationality: 'Import Nationality',
trainingPlace: 'Training Place',
checkpoint: 'Checkpoint',
checkpointEN: 'Checkpoint (EN)',
@ -457,6 +471,13 @@ export default {
citizenId: 'Citizen ID',
citizenIssue: 'Citizen Issue',
citizenExpire: 'Citizen Expire',
agencyStatus: 'Agency Status',
normal: 'Normal',
canceled: 'Canceled',
blacklist: 'Black list',
contactName: 'Contact Person',
contactTel: 'Contact Number',
addressForeign: 'Use foreign address',
},
},
customer: {
@ -470,10 +491,9 @@ export default {
powerOfAttorney: 'Power of Attorney',
others: 'Others',
},
employer: 'Employer',
employerLegalEntity: 'Legal Entity',
employerNaturalPerson: 'Natrual Person',
employerNaturalPerson: 'Natural Person',
employerType: 'Employer Type',
employee: 'Employee',
form: {
@ -483,24 +503,22 @@ 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',
passportExpiryDate: 'Passport Expiry Date',
ownerName: 'Customer Name',
firstName: 'First Name ',
lastName: 'Last Name ',
firstNameEN: 'First Name in English',
lastNameEN: 'Last Name in English',
cardNumber: 'ID Card Number',
prefixName: 'Prefix',
legalPersonNo: 'Legal Entity Registration Number',
registerName: 'Company Name',
@ -508,7 +526,6 @@ export default {
registerDate: 'Registered On',
registerCompanyName: 'Registered Name',
authorizedCapital: 'Authorized Capital',
workplace: 'Workplace',
workplaceEN: 'Workplace (EN)',
address: 'Address',
@ -516,7 +533,6 @@ export default {
branchCode: 'Branch Code',
customerCode: 'Employer Code',
legalPersonCode: 'Legal Entity Code',
codeAbbrev: 'Company Abbreviation',
codeNumber: 'Company Number',
registeredBranch: 'Registered Branch',
@ -554,7 +570,7 @@ export default {
jobPosition: 'Job Position',
address: 'Address',
workPlace: 'Workplace',
contactName: 'Contact Name',
contactName: 'Contact Person',
contactPhone: 'Contact Phone',
totalEmployee: 'Total Employee',
officeTel: 'Headoffice Telephone',
@ -608,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',
@ -758,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',
@ -792,7 +811,7 @@ export default {
employee: 'Employee',
employeeName: 'Full Name',
workName: 'Work Name',
contactName: 'Contact Name',
contactName: 'Contact Person',
documentReceivePoint: 'Document Drop-Off Point"',
dueDate: 'Quotation Due Date',
specialCondition: 'Special Conditions',
@ -874,7 +893,7 @@ export default {
SplitCustom: 'Custom Installments Bill',
BillFull: 'Full Amount Bill',
BillSplit: 'Installments Bill',
BillCustomSplit: 'Custom Installments Bill',
BillSplitCustom: 'Custom Installments Bill',
},
status: {
@ -910,6 +929,10 @@ export default {
code: 'Agencies Code',
group: 'Agencies Group',
name: 'Agencies Name',
contactName: 'Contact Person',
contactTel: 'Contact Number',
bankInfo: 'Bank Information',
attachment: 'Attachment',
},
requestList: {
@ -931,8 +954,9 @@ export default {
localEmployee: 'Local Employee',
nonLocalEmployee: 'Non Local Employee',
noWorkflowTemplate: 'A workflow template has not been selected.',
salesRepresentative: 'Sales Representative',
dataOffice: 'Employment Office District',
ref: 'Reference',
action: {
title: 'Action',
@ -996,7 +1020,7 @@ export default {
issueBranch: 'Issue Branch',
issueDate: 'Issue Date',
madeBy: 'Made By',
contactName: 'Contact Name',
contactName: 'Contact Person',
workOrderCode: 'Work Order Code',
workOrderName: 'Work Order Name',
telephone: 'Telephone',
@ -1055,6 +1079,10 @@ export default {
confirmDebitNoteAccept: 'Confirm acceptance of the debit note.',
},
message: {
copy: 'Copy',
warningPaste:
'Do you want to replace the data with the newly copied information?',
warningCopyEmpty: 'You have not copied any data yet',
quotationAccept: 'Once accepted, no further modifications can be made',
beingUse: '"{msg}" is being used.',
incompleteDataEntry: 'Incomplete data entry on {tap} page',
@ -1072,10 +1100,8 @@ export default {
confirmSavingStatus:
'Do you want to confirm the saving of the status change data?',
confirmSending: 'Do you confirm the submission of the task?',
confirmValidate: 'Do you confirm the validation?',
warningSelectDeliveryStaff:
'You have not yet selected a document delivery staff.',
confirmEndWorkWarning:
"Do you want to end the work now? The current statuses 'Pending', 'In Progress', 'To Be Reprocessed' will be changed to 'Redo All'.",
confirmEndWork: 'Do you want to end the work?',
@ -1101,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.',
@ -1183,13 +1209,14 @@ export default {
'Product with the same name already exists. If you want to create with this name please select another code.',
userExists: 'User already exits.',
sameNameExists: 'Same name exists.',
samePropertyNameExists: 'Same property name exists.',
validateError: 'Validate Error',
codeMisMatch: 'Code Mismatch',
crossCompanyNotPermit: 'Cannot move between different headoffice',
errorOccure:
errorOccurred:
'An error has occurred, causing the system to be unable to function. Please try again later.',
invalideData: 'The information is incorrect. Please try again later.',
invalidData: 'The information is incorrect. Please try again later.',
authFailed: 'Authentication Failed. Please try again later. ',
installmentsValidateFailed:
'Validation failed. Each installment must include at least one product. Please review and update the installments accordingly.',
@ -1207,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',
},
},
@ -1476,4 +1506,26 @@ export default {
type: 'Type',
},
},
dateRange: {
today: 'Today',
yesterday: 'Yesterday',
thisWeek: 'This Week',
lastWeek: 'Last Week',
thisMonth: 'This Month',
lastMonth: 'Last Month',
thisYear: 'This Year',
lastYear: 'Last Year',
last7Days: 'Last 7 Days',
last30Days: 'Last 30 Days',
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

@ -1,7 +1,7 @@
import eng from './eng';
import tha from './tha';
import tha from './tha'; // spellchecker:disable-line
export default {
eng,
tha,
tha, // spellchecker:disable-line
};

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}',
@ -151,6 +152,16 @@ export default {
dueDate: 'วันครบกำหนด',
year: 'ปี',
tableOfContent: 'สารบัญ',
draw: 'วาด',
newUpload: 'อัปโหลดใหม่',
nativeLanguage: '{msg} ภาษาต้นทาง',
copy: 'คัดลอก',
paste: 'วาง',
period: 'ช่วงเวลา',
documentStatus: 'สถานะเอกสาร',
advanceSearch: 'ค้นหาขั้นสูง',
totalPeople: '{meg} คน',
price: 'ราคา {price} บาท',
},
menu: {
@ -193,12 +204,14 @@ export default {
title: 'จัดการ',
branch: 'สาขา',
personnel: 'บุคลากร',
group: 'กลุ่ม',
productService: 'สินค้าและบริการ',
workflow: 'ขั้นตอนการทำงาน',
property: 'คุณสมบัติ',
customer: 'ลูกค้า',
mainData: 'ข้อมูลหลัก',
agencies: 'หน่วยงาน',
businessType: 'ประเภทกิจการ',
},
sales: {
@ -246,6 +259,7 @@ export default {
manual: {
title: 'คู่มือ',
usage: 'การใช้งาน',
troubleshooting: 'การแก้ปัญหา',
},
},
@ -324,7 +338,7 @@ export default {
letterAndNumOnly: 'โปรดใช้เฉพาะ _ ตัวอักษรภาษาอังกฤษและตัวเลขเท่านั้น',
numOnly: 'โปรดใช้เฉพาะตัวเลขเท่านั้น',
requireLength: 'กรุณากรอกให้ครบ {msg} หลัก',
branchNameField: "โปรดใช้ตัวอักษร ตัวเลข หรือ . , - ' & เท่านั้น",
branchNameField: "โปรดใช้ตัวอักษร ตัวเลข หรือ . , - ' ( ) & เท่านั้น",
branchNameENField:
"โปรดใช้ตัวอักษรภาษาอังกฤษ ตัวเลข หรือ . , - ' & เท่านั้น",
passportFormat: 'กรุณากรอกหมายเลขพาสปอร์ตให้ถูกต้องตามรูปแบบ',
@ -453,6 +467,13 @@ export default {
citizenId: 'เลขที่บัตรประชาชน',
citizenIssue: 'วันที่ออกบัตร',
citizenExpire: 'วันที่หมดอายุ',
agencyStatus: 'สถานะเอเจนซี่',
normal: 'ปกติ',
canceled: 'ยกเลิก',
blacklist: 'แบล็คลิสต์',
contactName: 'ชื่อผู้ติดต่อ',
contactTel: 'เบอร์โทรศัพท์ผู้ติดต่อ',
addressForeign: 'ใช้ที่อยู่ต่างประเทศ',
},
},
customer: {
@ -479,15 +500,16 @@ export default {
},
prefix: {
mr: 'Mr.',
mrs: 'Mrs.',
miss: 'Miss.',
mr: 'นาย',
mrs: 'นาง',
miss: 'นางสาว',
},
citizenId: 'บัตรประจำตัวประชาชน',
religion: 'ศาสนา',
issueDate: 'วันที่ออกหนังสือ',
passportExpiryDate: 'วันหiมดอายุหนังสือเดินทาง',
taxpayyerNo: 'เลขที่ประจำตัวผู้เสียภาษี',
ownerName: 'ชื่อนายจ้าง',
firstName: 'ชื่อ ',
@ -571,7 +593,7 @@ export default {
family: 'ข้อมูลครอบครัว',
},
workerStatus: 'สถานะคนงาน',
previousPassportNumber: 'หมายเลขอันเก่าหนังสือเดินทาง',
previousPassportNumber: 'หมายเลขหนังสือเดินทางเล่มเก่า',
employerBranch: 'สาขานายจ้าง',
employeeCode: 'รหัสลูกจ้าง',
nrcNo: 'เลขบัตรประจำตัวคนซึ่งไม่มีสัญชาติไทย (N.R.C No.)',
@ -604,7 +626,7 @@ export default {
placeOfBirth: 'สถานที่เกิด',
countryOfbirth: 'ประเทศที่เกิด',
issueCountry: 'ประเทศที่ออก',
entryCount: 'จำนวนที่เข้าประเทศ',
entryCount: 'จำนวนวันที่เข้าประเทศ',
employerSelect: {
branchName: 'ชื่อสาขา',
customerName: 'ชื่อนายจ้าง',
@ -632,7 +654,7 @@ export default {
permitIssuedAt: 'สถานที่ออกใบอนุญาต',
permitIssueDate: 'วันที่ออกใบอนุญาตทำงาน',
permitExpireDate: 'วันที่หมดอายุใบอนุญาตทำงาน',
identityNo: 'เลขประจำตัวคนต่างด้าว (13หลัก)',
identityNo: 'เลขประจำตัวคนต่างด้าว (13 หลัก)',
},
formFamily: {
citizenId:
@ -750,10 +772,13 @@ export default {
},
quotation: {
ownOnly: 'เห็นเฉพาะใบเสนอราคาของตัวเอง',
quotationDate: 'วันที่ใบเสนอราคา',
seller: 'ผู้ขาย',
paymentChannels: 'ช่องทางชำระเงิน',
channelsThat: 'ช่องทางที่',
refNo: 'เลขที่อ้างอิง',
bankAccount: 'บัญชีธนาคาร',
bankAccountNumber: 'เลขบัญชีธนาคาร',
bankAccountName: 'ชื่อบัญชี',
inTheNameOf: 'ในนาม',
@ -901,6 +926,10 @@ export default {
code: 'รหัสหน่วยงาน',
group: 'กลุ่มหน่วยงาน',
name: 'ชื่อหน่วยงาน',
contactName: 'ชื่อผู้ติดต่อ',
contactTel: 'เบอร์โทรผู้ติดต่อ',
bankInfo: 'ข้อมูลธนาคาร',
attachment: 'เอกสารเพิ่มเติม',
},
requestList: {
@ -922,6 +951,7 @@ export default {
nonLocalEmployee: 'พนักงานนอกพื้นที่',
noWorkflowTemplate: 'คุณไม่ได้เลือกแม่แบบขั้นตอนการทำงาน',
salesRepresentative: 'พนักงานขาย',
dataOffice: 'สำนักงานเขตจัดหางาน',
ref: 'อ้างอิง',
action: {
title: 'จัดการ',
@ -1040,6 +1070,9 @@ export default {
confirmDebitNoteAccept: 'ยืนยันการตอบรับใบเพิ่มหนี้',
},
message: {
copy: 'คัดลอก',
warningPaste: 'คุณต้องการที่จะเเทนที่ข้อมูลที่คัดลอกมาใหม่ใช่หรือไม่',
warningCopyEmpty: 'คุณยังไม่ได้คัดลอกข้อมูล',
quotationAccept: 'เมื่อตอบรับเเล้วจะไม่สามารถแก้ไขได้อีก',
beingUse: '"{msg}" มีการใช้งานอยู่',
incompleteDataEntry: 'กรอกข้อมูลไม่ครบในหน้า {tap}',
@ -1161,13 +1194,14 @@ export default {
'สินค้าที่มีชื่อเดียวกันมีในระบบแล้ว หากคุณต้องการสร้างด้วยชื่อนี้โปรดเลือกรหัสอื่น',
userExists: 'ชื่อผู้ใช้นี้มีอยู่ในระบบอยู่แล้ว',
sameNameExists: 'ชื่อนี้ถูกใช้ไปแล้ว',
samePropertyNameExists: 'คุณสมบัตินี้มีอยู่ในระบบอยู่แล้ว',
validateError: 'เกิดข้อผิดพลาดจากการตรวจสอบ',
codeMisMatch: 'รหัสไม่ตรงกัน',
crossCompanyNotPermit: 'ไม่สามารถดำเนินการระหว่างสำนักงานใหญ่อื่นได้',
errorOccure:
errorOccurred:
'เกิดข้อผิดพลาดทำให้ระบบไม่สามารถทำงานได้ กรุณาลองใหม่ในภายหลัง',
invalideData: 'ข้อมูลไม่ถูกต้อง กรุณาตรวจสอบใหม่อีกครั้ง',
invalidData: 'ข้อมูลไม่ถูกต้อง กรุณาตรวจสอบใหม่อีกครั้ง',
authFailed: 'การยืนยันตัวตนล้มเหลว กรุณาลองใหม่ในภายหลัง',
installmentsValidateFailed:
'ข้อมูลงวดไม่ถูกต้อง กรุณาตรวจสอบและยืนยันว่าแต่ละงวดมีสินค้าอย่างน้อยหนึ่งรายการ',
@ -1186,6 +1220,8 @@ export default {
'มีงานหนึ่งงานหรือมากกว่าที่ไม่อยู่ในสถานะรอดำเนินการ',
reqNotMet: 'ไม่ตรงกัน',
systemError: 'ระบบเกิดข้อผิดพลาด',
taskOrderInvalid: 'โปรดเลือก สินค้าเเละสาขา',
flowAccountProductIdNotFound: 'ไม่พบสินค้าใน flow account',
},
},
@ -1457,4 +1493,26 @@ export default {
type: 'ประเภท',
},
},
dateRange: {
today: 'วันนี้',
yesterday: 'เมื่อวานนี้',
thisWeek: 'สัปดาห์นี้',
lastWeek: 'สัปดาห์ที่แล้ว',
thisMonth: 'เดือนนี้',
lastMonth: 'เดือนที่แล้ว',
thisYear: 'ปีนี้',
lastYear: 'ปีที่แล้ว',
last7Days: '7 วันที่ผ่านมา',
last30Days: '30 วันที่ผ่านมา',
last90Days: '90 วันที่ผ่านมา',
customDateRange: 'กำหนดช่วงวันที่เอง',
},
businessType: {
title: 'ประเภทกิจการ',
caption: 'จัดการประเภทกิจการ',
name: 'ชื่อประเภทกิจการ',
nameEn: 'ชื่อประเภทกิจการ (ภาษาอังกฤษ)',
},
};

View file

@ -7,6 +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 { canAccess } from 'src/stores/utils';
type Menu = {
label: string;
@ -70,39 +71,45 @@ function initMenu() {
{
label: 'branch',
route: '/branch-management',
hidden: !(
role.value.includes('admin') ||
role.value.includes('branch_manager') ||
role.value.includes('head_of_admin') ||
role.value.includes('system') ||
role.value.includes('owner') ||
role.value.includes('head_of_account')
),
hidden: !canAccess('branch'),
},
{
label: 'personnel',
route: '/personnel-management',
hidden: !(
role.value.includes('admin') ||
role.value.includes('head_of_admin') ||
role.value.includes('system') ||
role.value.includes('owner') ||
role.value.includes('branch_manager')
),
hidden: !canAccess('personnel'),
},
{
label: 'group',
route: '/group-management',
hidden: !canAccess('personnel'),
},
{
label: 'workflow',
route: '/workflow',
hidden: !canAccess('workflow'),
},
{ label: 'workflow', route: '/workflow' },
{
label: 'property',
route: '/property',
hidden: !(
role.value.includes('admin') ||
role.value.includes('head_of_admin') ||
role.value.includes('system')
),
hidden: !canAccess('workflow'),
},
{
label: 'businessType',
route: '/business-type',
},
{
label: 'productService',
route: '/product-service',
},
{
label: 'customer',
route: '/customer-management',
hidden: !canAccess('customer'),
},
{
label: 'agencies',
route: '/agencies-management',
},
{ label: 'productService', route: '/product-service' },
{ label: 'customer', route: '/customer-management' },
{ label: 'agencies', route: '/agencies-management' },
],
},
{
@ -153,7 +160,11 @@ function initMenu() {
icon: 'mdi-monitor-dashboard',
children: [
{ label: 'report', route: '/report' },
{ label: 'dashboard', route: '/dash-board' },
{
label: 'dashboard',
route: '/dash-board',
hidden: !canAccess('dashBoard'),
},
],
},
@ -163,7 +174,11 @@ function initMenu() {
children: [
{
label: 'usage',
route: `/manual`,
route: '/manual',
},
{
label: 'troubleshooting',
route: '/troubleshooting',
},
],
},
@ -250,7 +265,13 @@ onMounted(async () => {
</header>
<div id="drawer-menu" class="q-pl-md q-mr-xs q-gutter-y-sm">
<template v-for="(menu, i) in menuData" :key="i">
<template
v-for="(menu, i) in menuData.filter(
(v) =>
!(v.children?.length === 0 || v.children?.every((i) => i.hidden)),
)"
:key="i"
>
<q-expansion-item
v-if="!menu.hidden"
:id="menu.label"
@ -444,8 +465,9 @@ onMounted(async () => {
</span>
</div>
<!-- v-if="!mini" -->
<q-btn
v-if="!mini"
v-if="false"
dense
flat
rounded

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);
@ -50,11 +50,16 @@ const leftDrawerMini = ref(false);
const unread = computed<number>(
() => notificationData.value.filter((v) => !v.read).length || 0,
);
// const filterRole = ref<string[]>();
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;
@ -124,6 +129,17 @@ function readNoti(id: string) {
}
}
function signatureSubmit() {
const signature = canvasRef.value.setCanvas();
userStore.setSignature(signature);
canvasModal.value = false;
}
async function signatureFetch() {
const ret = await userStore.getSignature();
if (ret) canvasRef.value.getCanvas(ret);
}
onMounted(async () => {
initTheme();
initLang();
@ -151,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();
}
}
});
@ -367,12 +388,28 @@ onMounted(async () => {
<div class="col column text-caption q-pl-md ellipsis">
<span class="block ellipsis full-width text-weight-bold">
{{ item.title }}
<q-tooltip
anchor="top middle"
self="bottom middle"
:delay="300"
:offset="[10, 10]"
>
{{ item.title }}
</q-tooltip>
</span>
<span
class="block ellipsis full-width text-stone"
:class="{ 'text-weight-medium': !item.read }"
>
{{ item.detail }}
<q-tooltip
anchor="top middle"
self="bottom middle"
:delay="300"
:offset="[10, 10]"
>
{{ item.detail }}
</q-tooltip>
</span>
</div>
<span
@ -382,15 +419,6 @@ onMounted(async () => {
>
{{ moment(item.createdAt).fromNow() }}
</span>
<q-tooltip
anchor="top middle"
self="bottom middle"
:delay="1000"
:offset="[10, 10]"
>
{{ item.title }}
{{ item.detail }}
</q-tooltip>
</q-item>
</section>
<section
@ -467,6 +495,7 @@ onMounted(async () => {
<!-- User -->
<ProfileMenu
id="btn-profile-menu"
:user-name="displayName"
@logout="doLogout"
@edit-personal-info="console.log('edit')"
@signature="
@ -495,13 +524,15 @@ onMounted(async () => {
no-app-box
:title="$t('menu.profile.addSignature')"
:close="() => (canvasModal = false)"
:submit="signatureSubmit"
:show="signatureFetch"
>
<CanvasComponent ref="canvasRef" v-model:modal="canvasModal" />
<template #footer>
<q-btn
flat
dense
:label="$t('clear')"
:label="$t('general.clear')"
@click="
() => {
canvasRef.clearCanvas(), canvasRef.clearUpload();

View file

@ -12,6 +12,7 @@ const filterRole = ref<string[]>();
defineProps<{
userImage?: string;
gender?: string;
userName?: string;
}>();
const inputFile = document.createElement('input');
@ -31,7 +32,7 @@ const options = [
label: 'menu.profile.signature',
value: 'signature',
color: 'grey',
disabled: true,
disabled: false,
},
{
icon: 'mdi-brightness-6',
@ -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>

2
src/markdown-it.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
declare module 'markdown-it-image-figures';
declare module 'markdown-it-html5-media';

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
// NOTE: Library
import { storeToRefs } from 'pinia';
import { onMounted } from 'vue';
import { onMounted, watch } from 'vue';
// NOTE: Components
@ -10,22 +10,44 @@ import { onMounted } from 'vue';
import { useManualStore } from 'src/stores/manual';
import { useNavigator } from 'src/stores/navigator';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { useRoute, useRouter } from 'vue-router';
// NOTE: Variable
const route = useRoute();
const router = useRouter();
const manualStore = useManualStore();
const navigatorStore = useNavigator();
const { dataManual } = storeToRefs(manualStore);
async function fetchManual() {
const res = await manualStore.getManual();
dataManual.value = res ? res : [];
}
const { dataManual, dataTroubleshooting } = storeToRefs(manualStore);
onMounted(async () => {
navigatorStore.current.title = 'menu.manual.title';
navigatorStore.current.path = [{ text: '' }];
await fetchManual();
});
watch(
() => route.name,
async () => {
if (route.name === 'Manual') {
const res = await manualStore.getManual();
dataManual.value = res ? res : [];
}
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 },
);
</script>
<template>
@ -34,7 +56,7 @@ onMounted(async () => {
>
<section class="scroll q-gutter-y-sm">
<q-expansion-item
v-for="v in dataManual"
v-for="v in $route.name === 'Manual' ? dataManual : dataTroubleshooting"
:key="v.labelEN"
:content-inset-level="0.5"
class="rounded overflow-hidden bordered"
@ -58,7 +80,11 @@ onMounted(async () => {
clickable
dense
class="dot items-center rounded q-my-xs"
:to="`/manual/${v.category}/${x.name}`"
:to="
$route.name === 'Manual'
? `/manual/${v.category}/${x.name}`
: `/troubleshooting/${v.category}/${x.name}`
"
>
<Icon
v-if="!!x.icon"

View file

@ -5,9 +5,7 @@ import hljs from 'highlight.js';
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
// @ts-expect-error
import mditFigureWithPCaption from 'markdown-it-image-figures';
// @ts-expect-error
import mditMedia from 'markdown-it-html5-media';
import mditAnchor from 'markdown-it-anchor';
import mditHighlight from 'markdown-it-highlightjs';
@ -58,14 +56,28 @@ onUnmounted(() => {
async function getContent() {
if (!category.value || !page.value) return;
const res = await manualStore.getManualByPage({
category: category.value,
pageName: page.value,
});
if (res && res.ok) {
const text = await res.text();
content.value = text;
contentParsed.value = md.parse(text, {});
if (ROUTE.name === 'ManualView') {
const res = await manualStore.getManualByPage({
category: category.value,
pageName: page.value,
});
if (res && res.ok) {
const text = await res.text();
content.value = text;
contentParsed.value = md.parse(text, {});
}
}
if (ROUTE.name === 'TroubleshootingView') {
const res = await manualStore.getTroubleshootingByPage({
category: category.value,
pageName: page.value,
});
if (res && res.ok) {
const text = await res.text();
content.value = text;
contentParsed.value = md.parse(text, {});
}
}
}
@ -104,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"
@ -156,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"
@ -169,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"
>
@ -186,7 +198,9 @@ async function scrollTo(id: string) {
md.render(
content.replaceAll(
'assets/',
`${baseUrl}/manual/${category}/assets/`,
$route.name === 'ManualView'
? `${baseUrl}/manual/${category}/assets/`
: `${baseUrl}/troubleshooting/${category}/assets/`,
),
)
"
@ -209,6 +223,38 @@ async function scrollTo(id: string) {
padding-block: 1rem !important;
}
.markdown {
counter-set: h1 0;
counter-reset: h1;
}
.markdown :deep(h1) {
counter-reset: h2;
}
.markdown :deep(h2) {
counter-reset: h3;
}
.markdown :deep(h3) {
counter-reset: h4;
}
.markdown :deep(h2:before) {
counter-increment: h2;
content: counter(h2) '. ';
}
.markdown :deep(h3:before) {
counter-increment: h3;
content: counter(h2) '.' counter(h3) ' ';
}
.markdown :deep(h4:before) {
counter-increment: h4;
content: counter(h2) '.' counter(h3) '.' counter(h4) ' ';
}
.markdown :deep(blockquote) {
background-color: var(--surface-2);
border-radius: 8px;
@ -282,7 +328,31 @@ 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%;
}
.markdown :deep(table) {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5rem;
}
.markdown :deep(:where(table th)) {
background: var(--surface-2);
border: 1px solid var(--border-color);
}
.markdown :deep(:where(table td, table th)) {
border: 1px solid var(--border-color);
padding: 0.25rem 1rem;
}
</style>

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

@ -6,11 +6,11 @@ import { Icon } from '@iconify/vue';
import { BranchContact } from 'stores/branch-contact/types';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import type { QSelect, QTableProps, QTableSlots } from 'quasar';
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,
@ -52,6 +52,7 @@ import {
UndoButton,
} from 'components/button';
import { useNavigator } from 'src/stores/navigator';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const $q = useQuasar();
const { t } = useI18n();
@ -72,7 +73,6 @@ const typeBranchItem = [
color: 'var(--blue-6-hsl)',
},
];
const refFilter = ref<InstanceType<typeof QSelect>>();
const holdDialog = ref(false);
const isSubCreate = ref(false);
const columns = [
@ -175,6 +175,8 @@ const qrCodeDialog = ref(false);
const qrCodeimageUrl = ref<string>('');
const formLastSubBranch = ref<number>(0);
const searchDate = ref<string[]>([]);
const branchStore = useBranchStore();
const flowStore = useFlowStore();
const { locale } = useI18n();
@ -715,12 +717,20 @@ async function fetchList(opts: {
tree?: boolean;
withHead?: boolean;
filter?: 'head' | 'sub';
startDate?: string;
endDate?: string;
}) {
await branchStore.fetchList(opts);
}
watch(inputSearch, () => {
fetchList({ tree: true, query: inputSearch.value, withHead: true });
watch([inputSearch, searchDate], () => {
fetchList({
tree: true,
query: inputSearch.value,
withHead: true,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
});
currentSubBranch.value = undefined;
});
@ -781,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}`;
@ -855,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();
},
@ -1036,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"
@ -1060,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"
@ -1170,26 +1185,49 @@ watch(currentHq, () => {
<template v-slot:prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-if="$q.screen.lt.md" v-slot:append>
<span class="row">
<q-separator vertical />
<q-btn
icon="mdi-filter-variant"
unelevated
class="q-ml-sm"
padding="4px"
size="sm"
rounded
@click="refFilter?.showPopup"
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch
v-model="searchDate"
:active="$q.screen.lt.md && statusFilter !== 'all'"
>
<div
v-if="$q.screen.lt.md"
class="q-mt-sm text-weight-medium"
>
{{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
v-model="statusFilter"
outlined
dense
autocomplete="off"
option-value="value"
option-label="label"
map-options
emit-value
:for="'field-select-status'"
:options="[
{ label: $t('general.all'), value: 'all' },
{
label: $t('status.ACTIVE'),
value: 'statusACTIVE',
},
{
label: $t('status.INACTIVE'),
value: 'statusINACTIVE',
},
]"
/>
</span>
</AdvanceSearch>
</template>
</q-input>
<div class="row col-md-6 justify-end">
<q-select
v-show="$q.screen.gt.sm"
ref="refFilter"
v-if="$q.screen.gt.sm"
v-model="statusFilter"
outlined
dense
@ -1524,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(
@ -1665,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(
@ -2214,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

@ -10,8 +10,8 @@ import useOptionStore from 'stores/options';
import useAddressStore from 'stores/address';
import useMyBranch from 'src/stores/my-branch';
import { calculateAge } from 'src/utils/datetime';
import { QSelect, useQuasar, type QTableProps } from 'quasar';
import { dialog, baseUrl } from 'stores/utils';
import { useQuasar, type QTableProps } from 'quasar';
import { dialog, baseUrl, setPrefixName } from 'stores/utils';
import { useNavigator } from 'src/stores/navigator';
import { isRoleInclude, resetScrollBar } from 'src/stores/utils';
import { BranchUserStats } from 'stores/branch/types';
@ -49,6 +49,7 @@ import FormPerson from 'components/02_personnel-management/FormPerson.vue';
import FormByType from 'components/02_personnel-management/FormByType.vue';
import FormInformation from 'components/02_personnel-management/FormInformation.vue';
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { locale, t } = useI18n();
const $q = useQuasar();
@ -73,7 +74,6 @@ const isImageEdit = ref(false);
const imageDialog = ref(false);
const infoDrawerEdit = ref(false);
const refreshImageState = ref(false);
const refFilter = ref<InstanceType<typeof QSelect>>();
const firstScroll = ref(false);
const inputSearch = ref('');
@ -93,12 +93,14 @@ const currentUser = ref<User>();
const userCode = ref<string>();
const statusToggle = ref(true);
const agencyFile = ref<File[]>([]);
const agencyFileList = ref<{ name: string; url: string }[]>([]);
const userFile = ref<File[]>([]);
const userFileList = ref<{ name: string; url: string }[]>([]);
const typeStats = ref<UserTypeStats>();
const userStats = ref<BranchUserStats[]>();
const searchDate = ref<[]>([]);
const urlProfile = ref<string>();
const profileFileImg = ref<File | null>(null);
const imageList = ref<{ selectedImage: string; list: string[] }>();
@ -124,7 +126,7 @@ const defaultFormData = {
streetEN: '',
street: '',
trainingPlace: null,
importNationality: null,
importNationality: [],
sourceNationality: null,
licenseExpireDate: null,
licenseIssueDate: null,
@ -151,6 +153,18 @@ const defaultFormData = {
citizenExpire: null,
citizenIssue: null,
citizenId: '',
contactName: '',
contactTel: '',
remark: '',
agencyStatus: '',
addressForeign: false,
provinceText: null,
districtText: null,
subDistrictText: null,
provinceTextEN: null,
districtTextEN: null,
subDistrictTextEN: null,
zipCodeText: null,
};
const formData = ref<UserCreate>({
@ -172,7 +186,7 @@ const formData = ref<UserCreate>({
streetEN: '',
street: '',
trainingPlace: null,
importNationality: null,
importNationality: [],
sourceNationality: null,
licenseExpireDate: null,
licenseIssueDate: null,
@ -199,6 +213,18 @@ const formData = ref<UserCreate>({
citizenExpire: null,
citizenIssue: null,
citizenId: '',
contactName: '',
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 }[]>([
@ -327,7 +353,7 @@ function onClose(excludeDialog?: boolean) {
urlProfile.value = '';
profileFileImg.value = null;
infoDrawerEdit.value = false;
agencyFile.value = [];
userFile.value = [];
isEdit.value = false;
statusToggle.value = true;
isImageEdit.value = false;
@ -336,6 +362,8 @@ function onClose(excludeDialog?: boolean) {
mapUserType(currentTab.value);
imageList.value = { selectedImage: '', list: [] };
onCreateImageList.value = { selectedImage: '', list: [] };
userFileList.value = [];
userFile.value = [];
flowStore.rotate();
}
@ -356,12 +384,10 @@ async function openDialog(
isEdit.value = true;
await assignFormData(id);
if (formData.value.userType === 'AGENCY') {
const result = await userStore.fetchAttachment(id);
const result = await userStore.fetchAttachment(id);
if (result) {
agencyFileList.value = result;
}
if (result) {
userFileList.value = result;
}
}
if (userStore.userOption.hqOpts.length !== 0 && !id) {
@ -419,15 +445,37 @@ async function onSubmit(excludeDialog?: boolean) {
: '';
const formDataEdit = {
...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);
if (currentUser.value.id && formDataEdit.userType === 'AGENCY') {
if (!agencyFile.value) return;
if (userFile.value) {
const payload: UserAttachmentCreate = {
file: agencyFile.value,
file: userFile.value,
};
if (payload?.file) {
@ -450,16 +498,39 @@ async function onSubmit(excludeDialog?: boolean) {
: hqId.value
? hqId.value
: '';
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,
);
if (result && formData.value.userType === 'AGENCY') {
if (!agencyFile.value) return;
if (userFile.value && result) {
const payload: UserAttachmentCreate = {
file: agencyFile.value,
file: userFile.value,
};
if (payload?.file) {
@ -551,12 +622,20 @@ async function assignFormData(idEdit: string) {
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,
@ -567,7 +646,10 @@ async function assignFormData(idEdit: string) {
street: foundUser.street,
streetEN: foundUser.streetEN,
trainingPlace: foundUser.trainingPlace,
importNationality: foundUser.importNationality,
importNationality:
typeof foundUser.importNationality === 'string'
? [foundUser.importNationality]
: foundUser.importNationality,
sourceNationality: foundUser.sourceNationality,
licenseNo: foundUser.licenseNo,
discountCondition: foundUser.discountCondition,
@ -587,6 +669,8 @@ async function assignFormData(idEdit: string) {
responsibleArea: foundUser.responsibleArea,
status: foundUser.status,
selectedImage: foundUser.selectedImage,
contactName: foundUser.contactName || '',
contactTel: foundUser.contactTel || '',
licenseExpireDate:
(foundUser.licenseExpireDate &&
new Date(foundUser.licenseExpireDate)) ||
@ -603,6 +687,12 @@ async function assignFormData(idEdit: string) {
(foundUser.citizenIssue && new Date(foundUser.citizenIssue)) || null,
citizenExpire:
(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'
@ -661,6 +751,8 @@ async function fetchUserList(mobileFetch?: boolean) {
: statusFilter.value === 'statusACTIVE'
? 'ACTIVE'
: 'INACTIVE',
startDate: searchDate.value[0],
endDate: searchDate.value[1],
});
if (ret) {
@ -727,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;
@ -735,11 +837,11 @@ watch(
formData.value.responsibleArea = null;
formData.value.discountCondition = null;
formData.value.sourceNationality = null;
formData.value.importNationality = null;
formData.value.importNationality = [];
formData.value.trainingPlace = null;
formData.value.checkpoint = null;
formData.value.checkpointEN = null;
agencyFile.value = [];
userFile.value = [];
},
);
@ -750,7 +852,7 @@ watch(
},
);
watch([inputSearch, statusFilter, pageSize], async () => {
watch([inputSearch, statusFilter, pageSize, searchDate], async () => {
if (userData.value) userData.value.result = [];
currentPage.value = 1;
@ -872,26 +974,45 @@ watch(
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-if="$q.screen.lt.md" v-slot:append>
<span class="row">
<q-separator vertical />
<q-btn
icon="mdi-filter-variant"
unelevated
class="q-ml-sm"
padding="4px"
size="sm"
rounded
@click="refFilter?.showPopup"
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch
v-model="searchDate"
:active="$q.screen.lt.md && statusFilter !== 'all'"
>
<div
v-if="$q.screen.lt.md"
class="q-mt-sm text-weight-medium"
>
{{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
v-model="statusFilter"
outlined
dense
option-value="value"
option-label="label"
map-options
emit-value
autocomplete="off"
:for="'field-select-status'"
:options="[
{ label: $t('general.all'), value: 'all' },
{ label: $t('general.active'), value: 'statusACTIVE' },
{
label: $t('general.inactive'),
value: 'statusINACTIVE',
},
]"
/>
</span>
</AdvanceSearch>
</template>
</q-input>
<div class="row col-md-5" style="white-space: nowrap">
<q-select
v-show="$q.screen.gt.sm"
ref="refFilter"
v-if="$q.screen.gt.sm"
v-model="statusFilter"
outlined
dense
@ -1241,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"
@ -1251,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>
@ -1517,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()"
@ -1551,7 +1675,18 @@ watch(
v-model:toggle-status="formData.status"
hideFade
:toggle-title="$t('status.title')"
:title="`${locale === 'eng' ? `${formData.firstNameEN} ${formData.lastNameEN}` : `${formData.firstName} ${formData.lastName}`}`"
:title="
setPrefixName(
{
namePrefix: formData.namePrefix,
firstName: formData.firstName || formData.firstNameEN,
lastName: formData.lastName || formData.lastNameEN,
firstNameEN: formData.firstNameEN,
lastNameEN: formData.lastNameEN,
},
{ locale },
)
"
:caption="userCode"
:img="
`${baseUrl}/user/${currentUser.id}/profile-image/${formData.selectedImage}`.concat(
@ -1736,12 +1871,15 @@ watch(
v-model:citizen-id="formData.citizenId"
v-model:citizen-issue="formData.citizenIssue"
v-model:citizen-expire="formData.citizenExpire"
v-model:contact-name="formData.contactName"
v-model:contact-tel="formData.contactTel"
:title="'personnel.form.personalInformation'"
prefix-id="drawer-info-personnel"
dense
outlined
separator
:readonly="!infoDrawerEdit"
:agency="formData.userType === 'AGENCY'"
class="q-mb-xl"
/>
@ -1759,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
@ -1781,10 +1924,11 @@ watch(
v-model:import-nationality="formData.importNationality"
v-model:training-place="formData.trainingPlace"
v-model:checkpoint="formData.checkpoint"
v-model:checkpoint-en="formData.checkpointEN"
v-model:agency-file="agencyFile"
v-model:agency-file-list="agencyFileList"
v-model:user-file="userFile"
v-model:user-file-list="userFileList"
v-model:user-id="currentUser.id"
v-model:remark="formData.remark"
v-model:agency-status="formData.agencyStatus"
/>
</div>
</div>
@ -1828,7 +1972,18 @@ watch(
}[formData.gender]
"
:toggleTitle="$t('status.title')"
:title="`${locale === 'eng' ? `${formData.firstNameEN} ${formData.lastNameEN}` : `${formData.firstName} ${formData.lastName}`}`"
:title="
setPrefixName(
{
namePrefix: formData.namePrefix,
firstName: formData.firstName,
lastName: formData.lastName,
firstNameEN: formData.firstNameEN,
lastNameEN: formData.lastNameEN,
},
{ locale },
)
"
:fallbackImg="
{
male: '/no-img-man.png',
@ -1854,7 +2009,6 @@ watch(
<div
class="col"
id="personnel-form"
:class="{
'q-px-lg q-pb-lg': $q.screen.gt.sm,
'q-px-md q-pb-sm': !$q.screen.gt.sm,
@ -1898,7 +2052,7 @@ watch(
? [
{
name: $t('personnel.form.workInformation'),
anchor: 'dialog-info-work',
anchor: 'dialog-form-work',
},
]
: [],
@ -1914,6 +2068,7 @@ watch(
</div>
</div>
<div
id="personnel-form"
class="col-md-10 col-12 full-height scroll"
:class="{
'q-py-md q-pr-md ': $q.screen.gt.sm,
@ -1939,6 +2094,7 @@ watch(
id="dialog-form-personal"
prefix-id="form-dialog-personnel"
dense
:agency="formData.userType === 'AGENCY'"
outlined
separator
:title="'personnel.form.personalInformation'"
@ -1957,6 +2113,8 @@ watch(
v-model:citizen-id="formData.citizenId"
v-model:citizen-issue="formData.citizenIssue"
v-model:citizen-expire="formData.citizenExpire"
v-model:contact-name="formData.contactName"
v-model:contact-tel="formData.contactTel"
class="q-mb-xl"
/>
<AddressForm
@ -1973,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
@ -1992,8 +2155,10 @@ watch(
v-model:import-nationality="formData.importNationality"
v-model:training-place="formData.trainingPlace"
v-model:checkpoint="formData.checkpoint"
v-model:checkpoint-en="formData.checkpointEN"
v-model:agency-file="agencyFile"
v-model:agency-status="formData.agencyStatus"
v-model:remark="formData.remark"
v-model:user-file="userFile"
v-model:user-file-list="userFileList"
/>
</div>
</div>

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

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