Compare commits

...

108 commits

Author SHA1 Message Date
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
119 changed files with 7439 additions and 16930 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 }} REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
CONTAINER_IMAGE_NAME: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_OWNER }}/${{ vars.CONTAINER_IMAGE_NAME }}:latest CONTAINER_IMAGE_NAME: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_OWNER }}/${{ vars.CONTAINER_IMAGE_NAME }}:latest
jobs: jobs:
gitea-release: build-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -51,7 +51,7 @@ jobs:
"description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Deployed by: `${{ github.actor }}`", "description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Deployed by: `${{ github.actor }}`",
"color": 3066993, "color": 3066993,
"footer": { "footer": {
"text": "Gitea Local Release Notification", "text": "Local Release Notification",
"icon_url": "https://example.com/success-icon.png" "icon_url": "https://example.com/success-icon.png"
}, },
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'" "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 }}`", "description": "**Details:**\n- Image: `${{ env.CONTAINER_IMAGE_NAME }}`\n- Attempted by: `${{ github.actor }}`",
"color": 15158332, "color": 15158332,
"footer": { "footer": {
"text": "Gitea Local Release Notification", "text": "Local Release Notification",
"icon_url": "https://example.com/failure-icon.png" "icon_url": "https://example.com/failure-icon.png"
}, },
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'" "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", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"lint": "eslint --ext .js,.ts,.vue ./",
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore", "format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test": "echo \"No test specified\" && exit 0", "test": "echo \"No test specified\" && exit 0",
"dev": "quasar dev", "dev": "quasar dev",
"build": "quasar build", "build": "quasar build",
"postinstall": "quasar prepare",
"changelog:generate": "git-cliff -o CHANGELOG.md" "changelog:generate": "git-cliff -o CHANGELOG.md"
}, },
"dependencies": { "dependencies": {
"@peaceroad/markdown-it-figure-with-p-caption": "^0.11.0", "@peaceroad/markdown-it-figure-with-p-caption": "^0.11.0",
"@quasar/extras": "^1.16.12", "@quasar/extras": "^1.16.17",
"@tato30/vue-pdf": "^1.11.0", "@tato30/vue-pdf": "^1.11.3",
"@vuepic/vue-datepicker": "^8.8.1", "@vuepic/vue-datepicker": "^8.8.1",
"apexcharts": "^4.5.0", "apexcharts": "^4.5.0",
"axios": "^1.7.4", "axios": "^1.8.4",
"cropperjs": "^1.6.2", "cropperjs": "^1.6.2",
"dayjs": "^1.11.13",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"keycloak-js": "^25.0.4", "keycloak-js": "^25.0.6",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0", "markdown-it-anchor": "^9.2.0",
"markdown-it-highlightjs": "^4.2.0", "markdown-it-highlightjs": "^4.2.0",
@ -31,49 +32,44 @@
"markdown-it-html5-media": "^0.7.1", "markdown-it-html5-media": "^0.7.1",
"markdown-it-image-figures": "^2.1.1", "markdown-it-image-figures": "^2.1.1",
"markdown-it-video": "^0.6.3", "markdown-it-video": "^0.6.3",
"mime": "^4.0.4", "mime": "^4.0.6",
"moment": "^2.30.1", "moment": "^2.30.1",
"number-to-words": "^1.2.4", "number-to-words": "^1.2.4",
"open-props": "^1.7.5", "open-props": "^1.7.14",
"pinia": "^2.2.2", "pinia": "^2.3.1",
"quasar": "^2.16.9", "quasar": "^2.18.1",
"signature_pad": "^5.0.2", "signature_pad": "^5.0.7",
"socket.io-client": "^4.7.5",
"tesseract.js": "^5.1.1", "tesseract.js": "^5.1.1",
"thai-baht-text": "^2.0.5", "thai-baht-text": "^2.0.5",
"udsv": "^0.6.0", "udsv": "^0.6.0",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"vue": "^3.4.38", "vue": "^3.5.13",
"vue-dragscroll": "^4.0.6", "vue-dragscroll": "^4.0.6",
"vue-i18n": "^9.14.0", "vue-i18n": "^11.1.2",
"vue-pdf": "^4.3.0", "vue-pdf": "^4.3.0",
"vue-router": "^4.4.3", "vue-router": "^4.5.0",
"vue3-apexcharts": "^1.7.0" "vue-tsc": "^2.2.8",
"vue3-apexcharts": "^1.8.0"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^9.3.0", "@faker-js/faker": "^9.6.0",
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.3.0",
"@intlify/unplugin-vue-i18n": "^4.0.0", "@intlify/unplugin-vue-i18n": "^6.0.5",
"@playwright/test": "^1.46.1", "@playwright/test": "^1.51.1",
"@quasar/app-vite": "2.0.0-beta.19", "@quasar/app-vite": "^2.2.0",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/markdown-it-highlightjs": "^3.3.4", "@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/number-to-words": "^1.2.3",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.18.0", "autoprefixer": "^10.4.21",
"@typescript-eslint/parser": "^7.18.0",
"autoprefixer": "^10.4.20",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"eslint": "^8.57.0", "prettier": "^3.5.3",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.27.0",
"prettier": "^3.3.3",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vue-component-type-helpers": "^2.1.10" "vue-component-type-helpers": "^2.2.8"
}, },
"engines": { "engines": {
"node": "^24 || ^22 || ^20 || ^18", "node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
"npm": ">= 6.13.4", "npm": ">= 6.13.4",
"yarn": ">= 1.21.1" "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',
],
}),
],
};

BIN
public/img-group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -1,5 +1,28 @@
{ {
"eng": { "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": [ "workerStatus": [
{ {
"label": "Normal", "label": "Normal",
@ -154,7 +177,8 @@
{ "label": "VS2", "value": "VS2" }, { "label": "VS2", "value": "VS2" },
{ "label": "WO", "value": "WO" }, { "label": "WO", "value": "WO" },
{ "label": "WP390", "value": "WP390" }, { "label": "WP390", "value": "WP390" },
{ "label": "WP44", "value": "WP44" } { "label": "WP44", "value": "WP44" },
{ "label": "CUST", "value": "CUST" }
], ],
"prefix": [ "prefix": [
@ -183,29 +207,44 @@
} }
], ],
"training": [ "border": [
{ {
"label": "Myanmar Labor Training Center - Mae Sot, Tak Province", "label": "Mae Sot, Tak Province",
"value": "trainingTak" "value": "trainingTak"
}, },
{ {
"label": "Myanmar Labor Training Center - Kawthoung, Ranong Province", "label": "Koh Song, Ranong province",
"value": "trainingRanong" "value": "trainingRanong"
}, },
{ {
"label": "Laos Labor Training Center - Nong Khai, Nong Khai Province", "label": "Nong Khai, Nong Khai Province",
"value": "trainingNongKhai" "value": "trainingNongKhai"
}, },
{ {
"label": "Cambodian Labor Training Center - Aranyaprathet, Sa Kaeo Province", "label": "Aranyaprathet, Sa Kaeo Province",
"value": "trainingSaKaeo" "value": "trainingSaKaeo"
}, },
{ {
"label": "Cambodian Labor Training Center - Ban Laem, Chanthaburi Province", "label": "Ban Laem, Chanthaburi Province",
"value": "trainingChanthaburi" "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": [ "nationality": [
{ {
"label": "Thai", "label": "Thai",
@ -1050,6 +1089,29 @@
}, },
"tha": { "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": [ "workerStatus": [
{ {
"label": "ปกติ", "label": "ปกติ",
@ -1204,7 +1266,8 @@
{ "label": "VS2", "value": "VS2" }, { "label": "VS2", "value": "VS2" },
{ "label": "WO", "value": "WO" }, { "label": "WO", "value": "WO" },
{ "label": "WP390", "value": "WP390" }, { "label": "WP390", "value": "WP390" },
{ "label": "WP44", "value": "WP44" } { "label": "WP44", "value": "WP44" },
{ "label": "CUST", "value": "CUST" }
], ],
"prefix": [ "prefix": [
@ -1233,29 +1296,44 @@
} }
], ],
"training": [ "border": [
{ {
"label": "สถานที่อบรมแรงงานเมียนมา-แม่สอด จ.ตาก", "label": "แม่สอด จ.ตาก",
"value": "trainingTak" "value": "trainingTak"
}, },
{ {
"label": "สถานที่อบรมแรงงานเมียนมา-เกาะสอง จ.ระนอง", "label": "เกาะสอง จ.ระนอง",
"value": "trainingRanong" "value": "trainingRanong"
}, },
{ {
"label": "สถานที่อบรมแรงงานลาว-หนองคาย จ.หนองคาย", "label": "หนองคาย จ.หนองคาย",
"value": "trainingNongKhai" "value": "trainingNongKhai"
}, },
{ {
"label": "สถานที่อบรมแรงงานกัมพูชา-อรัญประเทศ จ.สระแก้ว", "label": "อรัญประเทศ จ.สระแก้ว",
"value": "trainingSaKaeo" "value": "trainingSaKaeo"
}, },
{ {
"label": "สถานที่อบรมแรงงานกัมพูชา-บ้านแหลม จ.จันทบุรี", "label": "บ้านแหลม จ.จันทบุรี",
"value": "trainingChanthaburi" "value": "trainingChanthaburi"
} }
], ],
"training": [
{
"label": "ศูนย์แรกรับเข้าทำงาน และสิ้นสุดการจ้าง จังหวัดตาก",
"value": "trainingTak"
},
{
"label": "ศูนย์แรกรับเข้าทำงาน และสิ้นสุดการจ้าง จังหวัดหนองคาย",
"value": "trainingNongKhai"
},
{
"label": "ศูนย์แรกรับเข้าทำงาน และสิ้นสุดการจ้าง จังหวัดสระแก้ว",
"value": "trainingSaKaeo"
}
],
"nationality": [ "nationality": [
{ {
"label": "ไทย", "label": "ไทย",

View file

@ -1,26 +1,22 @@
/* eslint-env node */
// Configuration for your app // Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js // 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'; import { fileURLToPath } from 'node:url';
export default configure((ctx) => { export default defineConfig((ctx) => {
return { return {
eslint: {
fix: true,
warnings: true,
errors: true,
},
boot: ['i18n', 'axios', 'components'], boot: ['i18n', 'axios', 'components'],
css: ['app.scss'], css: ['app.scss'],
extras: ['mdi-v7'], extras: ['mdi-v7'],
build: { build: {
target: { target: {
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'], browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
node: 'node20', node: 'node20',
}, },
typescript: {
vueShim: true,
},
vueRouterMode: 'history', vueRouterMode: 'history',
vitePlugins: [ vitePlugins: [
[ [
@ -35,7 +31,7 @@ export default configure((ctx) => {
devServer: { devServer: {
host: '0.0.0.0', host: '0.0.0.0',
open: false, open: false,
port: 5173, port: 5174,
}, },
framework: { framework: {
config: {}, 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 axios, { AxiosInstance } from 'axios';
import { boot } from 'quasar/wrappers'; import { defineBoot } from '#q-app/wrappers';
import { getToken } from 'src/services/keycloak'; import { getToken } from 'src/services/keycloak';
import { dialog } from 'stores/utils'; import { dialog } from 'stores/utils';
import useLoader from 'stores/loader'; import useLoader from 'stores/loader';
import useFlowStore from 'src/stores/flow'; import useFlowStore from 'src/stores/flow';
declare module '@vue/runtime-core' { declare module 'vue' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
$axios: AxiosInstance; $axios: AxiosInstance;
$api: AxiosInstance; $api: AxiosInstance;
@ -24,10 +24,10 @@ function parseError(
status: number, status: number,
body?: { status: number; message: string; code: string }, body?: { status: number; message: string; code: string },
) { ) {
if (status === 422) return 'invalideData'; if (status === 422) return 'invalidData';
if (body && body.code) return body.code; if (body && body.code) return body.code;
return 'errorOccure'; return 'errorOccurred';
} }
api.interceptors.request.use(async (config) => { 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 // for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios; 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 VueDatePicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css'; import '@vuepic/vue-datepicker/dist/main.css';
import GlobalDialog from 'components/GlobalDialog.vue'; import GlobalDialog from 'components/GlobalDialog.vue';
@ -6,7 +6,7 @@ import GlobalLoading from 'components/GlobalLoading.vue';
import VueDragscroll from 'vue-dragscroll'; import VueDragscroll from 'vue-dragscroll';
import VueApexCharts from 'vue3-apexcharts'; import VueApexCharts from 'vue3-apexcharts';
export default boot(({ app }) => { export default defineBoot(({ app }) => {
app.component('global-dialog', GlobalDialog); app.component('global-dialog', GlobalDialog);
app.component('global-loading', GlobalLoading); app.component('global-loading', GlobalLoading);
app.component('VueDatePicker', VueDatePicker); 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 { createI18n } from 'vue-i18n';
import messages from 'src/i18n'; import messages from 'src/i18n';
import { Lang } from 'src/utils/ui';
export type MessageLanguages = keyof typeof messages; export type MessageLanguages = keyof typeof messages;
// Type-define 'eng' as the master schema for the resource // 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 */ /* eslint-enable @typescript-eslint/no-empty-interface */
export const i18n = createI18n({ export const i18n = createI18n<
{ message: MessageSchema },
MessageLanguages,
false
>({
locale: 'tha', locale: 'tha',
legacy: false, legacy: false,
messages: { messages,
'en-US': {},
...messages,
},
}); });
export default boot(({ app }) => { export default defineBoot(({ app }) => {
// Set i18n instance on app // Set i18n instance on app
app.use(i18n); app.use(i18n);
}); });

View file

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

View file

@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import useUserStore from 'stores/user'; import useUserStore from 'stores/user';
import useOptionStore from 'stores/options'; import useOptionStore from 'stores/options';
import { UserAttachmentDelete } from 'stores/user/types'; import { UserAttachmentDelete, AgencyStatus } from 'stores/user/types';
import { dialog, selectFilterOptionRefMod } from 'stores/utils'; import { dialog } from 'stores/utils';
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Icon } from '@iconify/vue'; import { Icon } from '@iconify/vue';
import { QSelect } from 'quasar';
import DatePicker from '../shared/DatePicker.vue'; import DatePicker from '../shared/DatePicker.vue';
import SelectInput from 'src/components/shared/SelectInput.vue';
import SelectOffice from 'components/shared/select-muliple/SelectOffice.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>( const sourceNationality = defineModel<string | null | undefined>(
'sourceNationality', 'sourceNationality',
); );
const importNationality = defineModel<string | null | undefined>( const importNationality = defineModel<string[] | null | undefined>(
'importNationality', 'importNationality',
); );
const trainingPlace = defineModel<string | null | undefined>('trainingPlace'); const trainingPlace = defineModel<string | null | undefined>('trainingPlace');
const checkpoint = defineModel<string | null | undefined>('checkPoint'); const checkpoint = defineModel<string | null | undefined>('checkpoint');
const checkpointEN = defineModel<string | null | undefined>('checkPointEn'); const userFile = defineModel<File[]>('userFile');
const agencyFile = defineModel<File[]>('agencyFile'); const userFileList =
const agencyFileList = defineModel<{ name: string; url: string }[]>('userFileList');
defineModel<{ name: string; url: string }[]>('agencyFileList'); const remark = defineModel<string | null | undefined>('remark');
const agencyStatus = defineModel<string | null | undefined>('agencyStatus');
const attachmentRef = ref(); const attachmentRef = ref();
@ -69,66 +70,12 @@ function deleteFile(name: string) {
userStore.deleteAttachment(userId.value, payload); userStore.deleteAttachment(userId.value, payload);
const result = await userStore.fetchAttachment(userId.value); const result = await userStore.fetchAttachment(userId.value);
if (result) { if (result) {
agencyFileList.value = result; userFileList.value = result;
} }
}, },
cancel: () => {}, 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> </script>
<template> <template>
<div class="row col-12"> <div class="row col-12">
@ -186,11 +133,12 @@ watch(
/> />
<SelectOffice <SelectOffice
v-if="userType === 'MESSENGER'"
for="input-responsible-area" for="input-responsible-area"
v-model:value="responsibleArea" v-model:value="responsibleArea"
v-if="userType === 'MESSENGER'"
:readonly="readonly" :readonly="readonly"
:label="$t('personnel.form.responsibleArea')" :label="$t('personnel.form.responsibleArea')"
class="col"
/> />
</div> </div>
<div <div
@ -218,138 +166,103 @@ watch(
class="row col-12 q-col-gutter-sm" class="row col-12 q-col-gutter-sm"
style="margin-left: 0px; padding-left: 0px" style="margin-left: 0px; padding-left: 0px"
> >
<q-select <SelectInput
outlined :model-value="readonly ? sourceNationality || '-' : sourceNationality"
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"
id="input-source-nationality" id="input-source-nationality"
for="input-source-nationality" for="input-source-nationality"
:dense="dense" :option="optionStore.globalOption.nationality"
:readonly="readonly" class="col-md-3 col-6"
:hide-dropdown-icon="readonly" :readonly
clearable
:label="$t('personnel.form.sourceNationality')" :label="$t('personnel.form.sourceNationality')"
:options="nationalityOptions"
@filter="nationalityFilter"
:model-value="readonly ? sourceNationality || '-' : sourceNationality"
@update:model-value=" @update:model-value="
(v) => (typeof v === 'string' ? (sourceNationality = v) : '') (v) => (typeof v === 'string' ? (sourceNationality = v) : '')
" "
@clear="sourceNationality = ''" />
>
<template v-slot:no-option> <SelectInput
<q-item> v-model="importNationality"
<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"
id="input-import-nationality" id="input-import-nationality"
for="input-import-nationality" for="input-import-nationality"
:dense="dense" :option="optionStore.globalOption.nationality"
:readonly="readonly" class="col-md-3 col-6"
:hide-dropdown-icon="readonly" :readonly
:label="$t('personnel.form.importNationality')" multiple
:options="nationalityOptions" :hideSelected="false"
@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
clearable clearable
use-input fillInput
fill-input :label="$t('personnel.form.importNationality')"
emit-value />
map-options
hide-selected <SelectInput
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"
:model-value="readonly ? checkpoint || '-' : checkpoint" :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=" @update:model-value="
(v) => (typeof v === 'string' ? (checkpoint = v) : '') (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 <q-input
for="input-checkpoint-en" for="input-discount-condition"
:dense="dense" :dense="dense"
outlined outlined
:readonly="readonly" :readonly
:label="$t('personnel.form.checkpointEN')" :label="$t('general.remark')"
class="col-6" class="col-12"
:model-value="readonly ? checkpointEN || '-' : checkpointEN" type="textarea"
:model-value="readonly ? remark || '-' : remark"
@update:model-value=" @update:model-value="
(v) => (typeof v === 'string' ? (checkpointEN = v) : '') (v) => (typeof v === 'string' ? (remark = v) : '')
" "
@clear="checkpointEN = ''" @clear="remark = ''"
/> />
</div>
<q-file <q-file
v-if="userType"
ref="attachmentRef" ref="attachmentRef"
for="input-attachment" for="input-attachment"
:dense="dense" :dense="dense"
@ -358,8 +271,8 @@ watch(
multiple multiple
append append
:label="$t('personnel.form.attachment')" :label="$t('personnel.form.attachment')"
class="col-12" class="col"
v-model="agencyFile" v-model="userFile"
> >
<template v-slot:prepend> <template v-slot:prepend>
<Icon <Icon
@ -386,12 +299,12 @@ watch(
</template> </template>
</q-file> </q-file>
<div v-if="agencyFileList && agencyFileList?.length > 0" class="col-12"> <div v-if="userFileList && userFileList?.length > 0" class="col-12">
<q-list bordered separator class="rounded" style="padding: 0"> <q-list bordered separator class="rounded" style="padding: 0">
<q-item <q-item
id="attachment-file" id="attachment-file"
for="attachment-file" for="attachment-file"
v-for="item in agencyFileList" v-for="item in userFileList"
clickable clickable
:key="item.url" :key="item.url"
class="items-center row" class="items-center row"
@ -421,5 +334,4 @@ watch(
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>

View file

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

View file

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

View file

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

View file

@ -32,11 +32,11 @@ const entryCount = defineModel<number>('entryCount');
const issuePlace = defineModel<string>('issuePlace'); const issuePlace = defineModel<string>('issuePlace');
const issueCountry = defineModel<string>('issueCountry'); const issueCountry = defineModel<string>('issueCountry');
const issueDate = defineModel<Date | null | string>('visaIssueDate'); const issueDate = defineModel<Date | null | string>('visaIssueDate');
const type = defineModel<string>('visaType'); const type = defineModel<string>('type');
const expireDate = defineModel<Date>('expireDate'); const expireDate = defineModel<Date>('expireDate');
const remark = defineModel<string>('remark'); const remark = defineModel<string>('remark');
const workerType = defineModel<string>('workerType'); const workerType = defineModel<string>('workerType');
const number = defineModel<string>('visaNumber'); const number = defineModel<string>('number');
const calculatedVisaDate = computed(() => { const calculatedVisaDate = computed(() => {
if (!issueDate.value) return undefined; if (!issueDate.value) return undefined;
@ -78,6 +78,12 @@ onMounted(async () => {
await fetchProvince(); 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>[]>([]); const visaTypeOptions = ref<Record<string, unknown>[]>([]);
let visaTypeFilter: ( let visaTypeFilter: (
value: string, value: string,
@ -92,11 +98,17 @@ let workerTypeFilter: (
onMounted(() => { onMounted(() => {
visaTypeFilter = selectFilterOptionRefMod( visaTypeFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.nationality), ref(optionStore.globalOption?.visaType),
visaTypeOptions, visaTypeOptions,
'label', 'label',
); );
visaIssueCountryFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.nationality),
visaIssueCountryOptions,
'label',
);
workerTypeFilter = selectFilterOptionRefMod( workerTypeFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.workerType), ref(optionStore.globalOption?.workerType),
workerTypeOptions, workerTypeOptions,
@ -107,8 +119,14 @@ onMounted(() => {
watch( watch(
() => optionStore.globalOption, () => optionStore.globalOption,
() => { () => {
visaIssueCountryFilter = selectFilterOptionRefMod(
ref(optionStore.globalOption?.nationality),
visaIssueCountryOptions,
'label',
);
visaTypeFilter = selectFilterOptionRefMod( visaTypeFilter = selectFilterOptionRefMod(
optionStore.globalOption.nationality, optionStore.globalOption.visaType,
visaTypeOptions, visaTypeOptions,
'label', 'label',
); );
@ -422,11 +440,11 @@ watch(
class="col-md-4 col-6" class="col-md-4 col-6"
:dense="dense" :dense="dense"
:readonly="readonly" :readonly="readonly"
:options="visaTypeOptions" :options="visaIssueCountryOptions"
:hide-dropdown-icon="readonly" :hide-dropdown-icon="readonly"
:for="`${prefixId}-select-issue-country`" :for="`${prefixId}-select-issue-country`"
:label="$t('customerEmployee.form.issueCountry')" :label="$t('customerEmployee.form.issueCountry')"
@filter="visaTypeFilter" @filter="visaIssueCountryFilter"
:model-value="readonly ? issueCountry || '-' : issueCountry" :model-value="readonly ? issueCountry || '-' : issueCountry"
@update:model-value=" @update:model-value="
(v) => (typeof v === 'string' ? (issueCountry = v) : '') (v) => (typeof v === 'string' ? (issueCountry = v) : '')

View file

@ -3,6 +3,7 @@ import { QSelect } from 'quasar';
import { CustomerBranch } from 'stores/customer/types'; import { CustomerBranch } from 'stores/customer/types';
import { selectFilterOptionRefMod } from 'stores/utils'; import { selectFilterOptionRefMod } from 'stores/utils';
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import SelectCustomer from 'components/shared/select/SelectCustomer.vue';
import { import {
EditButton, EditButton,
DeleteButton, DeleteButton,
@ -11,6 +12,7 @@ import {
} from 'components/button'; } from 'components/button';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import useOptionStore from 'stores/options'; import useOptionStore from 'stores/options';
import { formatAddress } from 'src/utils/address';
const { locale } = useI18n(); const { locale } = useI18n();
@ -21,6 +23,13 @@ const optionsBranch = defineModel<{ id: string; name: string }[]>(
); );
// employee // employee
const customerBranchId = defineModel<string>('customerBranchId');
const currentCustomerBranch = defineModel<CustomerBranch>(
'currentCustomerBranch',
);
const customerBranch = defineModel<{ const customerBranch = defineModel<{
id: string; id: string;
address: string; address: string;
@ -107,180 +116,14 @@ defineEmits<{
</div> </div>
<div class="col-12 row" style="gap: var(--size-2)"> <div class="col-12 row" style="gap: var(--size-2)">
<q-select <SelectCustomer
:id="`${prefixId}-select-employer-branch`" v-model:value="customerBranchId"
:for="`${prefixId}-select-employer-branch`" v-model:value-option="currentCustomerBranch"
:use-input="!customerBranch"
autocomplete="off"
input-debounce="0"
:hide-dropdown-icon="readonly"
:dense="dense"
outlined
:readonly="readonly"
hide-bottom-space
class="col-12"
:label="$t('customer.form.branchCode')" :label="$t('customer.form.branchCode')"
v-model="customerBranch" class="col-12 field-two"
:option-value=" required
(v) => ({ :readonly
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>
<q-input <q-input
:for="`${prefixId}-input-code`" :for="`${prefixId}-input-code`"

View file

@ -6,12 +6,14 @@ import { useI18n } from 'vue-i18n';
import useUserStore from 'src/stores/user'; import useUserStore from 'src/stores/user';
import useOptionStore from 'src/stores/options'; import useOptionStore from 'src/stores/options';
import { useWorkflowTemplate } from 'src/stores/workflow-template';
import { baseUrl } from 'stores/utils'; import { baseUrl } from 'stores/utils';
import { getRole } from 'src/services/keycloak'; import { getRole } from 'src/services/keycloak';
import { import {
WorkflowUserInTable, WorkflowUserInTable,
WorkflowTemplatePayload, WorkflowTemplatePayload,
WorkFlowPayloadStep, WorkFlowPayloadStep,
Group,
} from 'src/stores/workflow-template/types'; } from 'src/stores/workflow-template/types';
import { User } from 'src/stores/user/types'; import { User } from 'src/stores/user/types';
@ -20,6 +22,7 @@ import ToggleButton from 'src/components/button/ToggleButton.vue';
import NoData from '../NoData.vue'; import NoData from '../NoData.vue';
import SelectBranch from '../shared/select/SelectBranch.vue'; import SelectBranch from '../shared/select/SelectBranch.vue';
import AddButton from '../button/AddButton.vue'; import AddButton from '../button/AddButton.vue';
import { QField } from 'quasar';
defineProps<{ defineProps<{
readonly?: boolean; readonly?: boolean;
@ -29,6 +32,7 @@ defineProps<{
const { t } = useI18n(); const { t } = useI18n();
const userStore = useUserStore(); const userStore = useUserStore();
const optionStore = useOptionStore(); const optionStore = useOptionStore();
const workflowStore = useWorkflowTemplate();
const userInTable = defineModel<WorkflowUserInTable[]>('userInTable', { const userInTable = defineModel<WorkflowUserInTable[]>('userInTable', {
default: [], default: [],
@ -43,7 +47,7 @@ const flowData = defineModel<WorkflowTemplatePayload>('flowData', {
}, },
}); });
const objectOptions = [ let objectOptions = [
...(optionStore.globalOption?.agenciesType || []), ...(optionStore.globalOption?.agenciesType || []),
{ label: t('flow.customer'), value: 'customer' }, { label: t('flow.customer'), value: 'customer' },
{ label: t('flow.officer'), value: 'officer' }, { label: t('flow.officer'), value: 'officer' },
@ -51,7 +55,9 @@ const objectOptions = [
const options = ref(objectOptions); const options = ref(objectOptions);
const role = ref<string[]>([]); const role = ref<string[]>([]);
const userList = ref<User[]>([]); const userList = ref<User[]>([]);
const groupList = ref<Group[]>([]);
const responsiblePersonSearch = ref(''); const responsiblePersonSearch = ref('');
const responsibleMenu = ref(false);
async function getUserList(opts?: { query: string }) { async function getUserList(opts?: { query: string }) {
const resUser = await userStore.fetchList({ const resUser = await userStore.fetchList({
@ -60,10 +66,10 @@ async function getUserList(opts?: { query: string }) {
if (resUser) userList.value = resUser.result; if (resUser) userList.value = resUser.result;
} }
// async function getUserById(responsiblePersonId: string) { async function getGroupList() {
// const resUser = await userStore.fetchById(responsiblePersonId); const resGroup = await workflowStore.getGroupList();
// if (resUser) userInTable.value.push(resUser); if (resGroup) groupList.value = resGroup;
// } }
function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) { function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
const currStep = flowData.value.step[stepIndex]; const currStep = flowData.value.step[stepIndex];
@ -78,6 +84,7 @@ function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
userInTable.value[stepIndex] = { userInTable.value[stepIndex] = {
name: flowData.value.step[stepIndex].name, name: flowData.value.step[stepIndex].name,
responsiblePerson: [], responsiblePerson: [],
responsibleGroup: [],
}; };
} }
@ -101,6 +108,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( function selectItem(
val: Record<string, unknown>, val: Record<string, unknown>,
responsibleInstitution?: string[], responsibleInstitution?: string[],
@ -142,6 +176,7 @@ watch(
onMounted(async () => { onMounted(async () => {
role.value = getRole() || []; role.value = getRole() || [];
await getUserList(); await getUserList();
await getGroupList();
await userStore.fetchHqOption(); await userStore.fetchHqOption();
}); });
</script> </script>
@ -467,33 +502,29 @@ onMounted(async () => {
</div> </div>
<!-- RESPONSIBLE-PERSON --> <!-- RESPONSIBLE-PERSON -->
<q-select <q-field
v-if="step.responsiblePersonId" v-if="step.responsiblePersonId"
behavior="menu"
:for="`select-responsible-person-${index}-${onDrawer ? 'drawer' : 'dialog'}`" :for="`select-responsible-person-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
:bg-color="readonly ? 'transparent' : ''" :bg-color="readonly ? 'transparent' : ''"
:readonly :readonly
outlined outlined
dense :stack-label="
v-model="step.responsiblePersonId" userInTable[index]?.responsiblePerson.length > 0 ||
multiple userInTable[index]?.responsibleGroup.length > 0
:options="[1, 2, 3]" "
hide-bottom-space
option-label="label"
option-value="value"
emit-value
:label="$t('flow.responsiblePerson')" :label="$t('flow.responsiblePerson')"
dense
class="col-md-6 col-12" class="col-md-6 col-12"
:hide-dropdown-icon="readonly" :class="{ 'cursor-pointer': !readonly }"
> >
<template v-slot:selected-item="scope"> <template #control>
<div class="column full-width"> <q-item
<div dense
class="row items-center no-wrap" class="items-center full-width no-padding"
v-for="person in userInTable[ v-for="person in userInTable[
index index
]?.responsiblePerson.filter( ]?.responsiblePerson.filter((p) =>
(p) => p.id === scope.opt, step.responsiblePersonId.includes(p.id),
)" )"
:key="person.id" :key="person.id"
> >
@ -547,12 +578,52 @@ onMounted(async () => {
{{ person.code }} {{ person.code }}
</span> </span>
</div> </div>
</div> </q-item>
</div>
</template>
<template v-slot:option></template> <div
<q-menu v-if="!readonly" :offset="[0, 4]"> 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 #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-list>
<q-item> <q-item>
<q-input <q-input
@ -581,6 +652,7 @@ onMounted(async () => {
{{ $t('general.noData') }} {{ $t('general.noData') }}
</q-item> </q-item>
<q-item <q-item
v-else
v-for="(person, i) in userList" v-for="(person, i) in userList"
dense dense
:key="i" :key="i"
@ -655,6 +727,7 @@ onMounted(async () => {
{{ $t('personnel.MESSENGER') }} {{ $t('personnel.MESSENGER') }}
</span> </span>
<q-item <q-item
dense
clickable clickable
@click="step.messengerByArea = !step.messengerByArea" @click="step.messengerByArea = !step.messengerByArea"
class="column" class="column"
@ -670,9 +743,49 @@ onMounted(async () => {
</div> </div>
</div> </div>
</q-item> </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-list>
</q-menu> </q-menu>
</q-select> </q-field>
<!-- RESPONSIBLE-AGENCIES, RESPONSIBLE-INSTITUTION --> <!-- RESPONSIBLE-AGENCIES, RESPONSIBLE-INSTITUTION -->
<q-select <q-select
@ -815,4 +928,11 @@ onMounted(async () => {
:deep(.q-dialog.fullscreen.no-pointer-events.q-dialog--modal) { :deep(.q-dialog.fullscreen.no-pointer-events.q-dialog--modal) {
visibility: hidden; visibility: hidden;
} }
.transition-rotate {
transition: transform 0.3s ease;
}
.rotated {
transform: rotate(180deg);
}
</style> </style>

View file

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

View file

@ -14,6 +14,9 @@ defineProps<{
const group = defineModel('group', { default: '' }); const group = defineModel('group', { default: '' });
const name = defineModel('name', { default: '' }); const name = defineModel('name', { default: '' });
const nameEn = defineModel('nameEn', { default: '' }); const nameEn = defineModel('nameEn', { default: '' });
const contactName = defineModel('contactName', { default: '' });
const email = defineModel('email', { default: '' });
const contactTel = defineModel('contactTel', { default: '' });
type Options = { label: string; value: string }; type Options = { label: string; value: string };
</script> </script>
@ -35,7 +38,7 @@ type Options = { label: string; value: string };
<div class="col-12 row q-col-gutter-sm"> <div class="col-12 row q-col-gutter-sm">
<SelectInput <SelectInput
:class="{ col: $q.screen.lt.md }" class="col"
:disable="!readonly && onDrawer" :disable="!readonly && onDrawer"
:readonly="readonly" :readonly="readonly"
for="input-agencies-code" for="input-agencies-code"
@ -62,7 +65,7 @@ type Options = { label: string; value: string };
outlined outlined
:readonly="readonly" :readonly="readonly"
hide-bottom-space hide-bottom-space
class="col-md col-12" class="col-md-4 col-12"
:label="$t('agencies.name')" :label="$t('agencies.name')"
v-model="name" v-model="name"
:rules="[(val: string) => !!val || $t('form.error.required')]" :rules="[(val: string) => !!val || $t('form.error.required')]"
@ -73,10 +76,78 @@ type Options = { label: string; value: string };
outlined outlined
:readonly="readonly" :readonly="readonly"
hide-bottom-space hide-bottom-space
class="col-md col-12" class="col-md-4 col-12"
:label="'Agencies Name'" :label="'Agencies Name'"
v-model="nameEn" v-model="nameEn"
/> />
<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>
</div> </div>
</div> </div>
</template> </template>

View file

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

View file

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

View file

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

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

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

View file

@ -0,0 +1,189 @@
<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">
<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" alt="Image"
/> />
</div> </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> <q-tooltip>
<div v-for="(person, i) in data.slice(3)" :key="i + 3"> <div v-for="(person, i) in data.slice(3)" :key="i + 3">
{{ person.name }} {{ person.name }}

View file

@ -16,6 +16,7 @@ const props = withDefaults(
useUpload?: boolean; useUpload?: boolean;
useCancel?: boolean; useCancel?: boolean;
useRejectCancel?: boolean; useRejectCancel?: boolean;
useCopy?: boolean;
disableCancel?: boolean; disableCancel?: boolean;
disableDelete?: boolean; disableDelete?: boolean;
}>(), }>(),
@ -31,6 +32,7 @@ defineEmits<{
(e: 'link'): void; (e: 'link'): void;
(e: 'upload'): void; (e: 'upload'): void;
(e: 'delete'): void; (e: 'delete'): void;
(e: 'copy'): void;
(e: 'cancel'): void; (e: 'cancel'): void;
(e: 'rejectCancel'): void; (e: 'rejectCancel'): void;
(e: 'changeStatus'): void; (e: 'changeStatus'): void;
@ -172,6 +174,27 @@ watch(
</span> </span>
</q-item> </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 <q-item
v-if="useCancel" v-if="useCancel"
v-close-popup v-close-popup

View file

@ -28,6 +28,7 @@ const props = withDefaults(
disable?: boolean; disable?: boolean;
multiple?: boolean; multiple?: boolean;
hideInput?: boolean; hideInput?: boolean;
hideDropdownIcon?: boolean;
rules?: ((value: string) => string | true)[]; rules?: ((value: string) => string | true)[];
}>(), }>(),
@ -82,7 +83,7 @@ watch(
:hide-selected :hide-selected
hide-bottom-space hide-bottom-space
:fill-input="fillInput && !!model" :fill-input="fillInput && !!model"
:hide-dropdown-icon="readonly" :hide-dropdown-icon="readonly || hideDropdownIcon"
input-debounce="500" input-debounce="500"
:option-value=" :option-value="
typeof props.optionValue === 'string' ? props.optionValue : 'value' typeof props.optionValue === 'string' ? props.optionValue : 'value'
@ -103,6 +104,11 @@ watch(
} }
" "
:rules :rules
@clear="
() => {
multiple ? (model = []) : (model = '');
}
"
> >
<template v-if="$slots.prepend" v-slot:prepend> <template v-if="$slots.prepend" v-slot:prepend>
<slot name="prepend"></slot> <slot name="prepend"></slot>

View file

@ -65,6 +65,8 @@ onMounted(async () => {
} }
await getSelectedOption(); await getSelectedOption();
valueOption.value = selectOptions.value.find((v) => v.id === value.value);
}); });
</script> </script>
<template> <template>
@ -158,7 +160,7 @@ onMounted(async () => {
</template> </template>
<template #option="{ opt, scope }"> <template #option="{ opt, scope }">
<q-item v-bind="scope.itemProps"> <q-item @click="valueOption = opt" v-bind="scope.itemProps">
<SelectCustomerItem :data="opt" :simple :simple-branch-no /> <SelectCustomerItem :data="opt" :simple :simple-branch-no />
</q-item> </q-item>

View file

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

View file

@ -54,7 +54,9 @@ const columns = [
field: (v: Employee) => field: (v: Employee) =>
locale.value === Lang.English locale.value === Lang.English
? `${v.firstNameEN} ${v.lastNameEN}` ? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName} ${v.lastName}`, : v.firstName
? `${v.firstName} ${v.lastName}`
: `${v.firstNameEN} ${v.lastNameEN}`,
}, },
{ {
name: 'birthDate', name: 'birthDate',

2
src/env.d.ts vendored
View file

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

View file

@ -60,7 +60,7 @@ export default {
branchStatus: 'Branch Status', branchStatus: 'Branch Status',
success: 'Success', success: 'Success',
taxNo: 'Legal Person', taxNo: 'Legal Person',
contactName: 'Contact Name', contactName: 'Contact Person',
image: 'Image of ', image: 'Image of ',
apply: 'Apply', apply: 'Apply',
licenseNumber: 'License number', licenseNumber: 'License number',
@ -151,6 +151,15 @@ export default {
dueDate: 'Due date', dueDate: 'Due date',
year: 'year', year: 'year',
tableOfContent: 'Table of Contents', 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',
}, },
menu: { menu: {
@ -245,7 +254,8 @@ export default {
manual: { manual: {
title: 'Manual', title: 'Manual',
usage: 'การใช้งาน', usage: 'Usage',
troubleshooting: 'Troubleshooting',
}, },
}, },
@ -374,7 +384,7 @@ export default {
branchLabel: 'Branch', branchLabel: 'Branch',
branchHQLabel: 'Headoffice', branchHQLabel: 'Headoffice',
taxNo: 'Legal Person', taxNo: 'Legal Person',
contactName: 'Contact Name', contactName: 'Contact Person',
}, },
page: { page: {
captionManage: 'Manage', captionManage: 'Manage',
@ -395,8 +405,8 @@ export default {
code: 'Headoffice Code', code: 'Headoffice Code',
codeBranch: 'Branch Code', codeBranch: 'Branch Code',
taxNo: 'Tax Identification Number', taxNo: 'Tax Identification Number',
contactName: 'Contact Name', contactName: 'Contact Person',
contactTelephone: 'Contact Telephone', contactTelephone: 'Contact Number',
branchName: 'Branch Name', branchName: 'Branch Name',
branchNameEN: 'Branch Name (EN)', branchNameEN: 'Branch Name (EN)',
servicePointName: 'Service Point Name', servicePointName: 'Service Point Name',
@ -449,7 +459,7 @@ export default {
responsibleArea: 'Responsibel Area', responsibleArea: 'Responsibel Area',
discount: 'Discount Condition', discount: 'Discount Condition',
sourceNationality: 'Source Nationality', sourceNationality: 'Source Nationality',
importNationality: 'import Nationality', importNationality: 'Import Nationality',
trainingPlace: 'Training Place', trainingPlace: 'Training Place',
checkpoint: 'Checkpoint', checkpoint: 'Checkpoint',
checkpointEN: 'Checkpoint (EN)', checkpointEN: 'Checkpoint (EN)',
@ -457,6 +467,12 @@ export default {
citizenId: 'Citizen ID', citizenId: 'Citizen ID',
citizenIssue: 'Citizen Issue', citizenIssue: 'Citizen Issue',
citizenExpire: 'Citizen Expire', citizenExpire: 'Citizen Expire',
agencyStatus: 'Agency Status',
normal: 'Normal',
canceled: 'Canceled',
blacklist: 'Black list',
contactName: 'Contact Person',
contactTel: 'Contact Number',
}, },
}, },
customer: { customer: {
@ -470,7 +486,6 @@ export default {
powerOfAttorney: 'Power of Attorney', powerOfAttorney: 'Power of Attorney',
others: 'Others', others: 'Others',
}, },
employer: 'Employer', employer: 'Employer',
employerLegalEntity: 'Legal Entity', employerLegalEntity: 'Legal Entity',
employerNaturalPerson: 'Natrual Person', employerNaturalPerson: 'Natrual Person',
@ -492,15 +507,12 @@ export default {
religion: 'Religion', religion: 'Religion',
issueDate: 'Issue Date', issueDate: 'Issue Date',
passportExpiryDate: 'Passport Expiry Date', passportExpiryDate: 'Passport Expiry Date',
ownerName: 'Customer Name', ownerName: 'Customer Name',
firstName: 'First Name ', firstName: 'First Name ',
lastName: 'Last Name ', lastName: 'Last Name ',
firstNameEN: 'First Name in English', firstNameEN: 'First Name in English',
lastNameEN: 'Last Name in English', lastNameEN: 'Last Name in English',
cardNumber: 'ID Card Number', cardNumber: 'ID Card Number',
prefixName: 'Prefix', prefixName: 'Prefix',
legalPersonNo: 'Legal Entity Registration Number', legalPersonNo: 'Legal Entity Registration Number',
registerName: 'Company Name', registerName: 'Company Name',
@ -508,7 +520,6 @@ export default {
registerDate: 'Registered On', registerDate: 'Registered On',
registerCompanyName: 'Registered Name', registerCompanyName: 'Registered Name',
authorizedCapital: 'Authorized Capital', authorizedCapital: 'Authorized Capital',
workplace: 'Workplace', workplace: 'Workplace',
workplaceEN: 'Workplace (EN)', workplaceEN: 'Workplace (EN)',
address: 'Address', address: 'Address',
@ -516,7 +527,6 @@ export default {
branchCode: 'Branch Code', branchCode: 'Branch Code',
customerCode: 'Employer Code', customerCode: 'Employer Code',
legalPersonCode: 'Legal Entity Code', legalPersonCode: 'Legal Entity Code',
codeAbbrev: 'Company Abbreviation', codeAbbrev: 'Company Abbreviation',
codeNumber: 'Company Number', codeNumber: 'Company Number',
registeredBranch: 'Registered Branch', registeredBranch: 'Registered Branch',
@ -554,7 +564,7 @@ export default {
jobPosition: 'Job Position', jobPosition: 'Job Position',
address: 'Address', address: 'Address',
workPlace: 'Workplace', workPlace: 'Workplace',
contactName: 'Contact Name', contactName: 'Contact Person',
contactPhone: 'Contact Phone', contactPhone: 'Contact Phone',
totalEmployee: 'Total Employee', totalEmployee: 'Total Employee',
officeTel: 'Headoffice Telephone', officeTel: 'Headoffice Telephone',
@ -792,7 +802,7 @@ export default {
employee: 'Employee', employee: 'Employee',
employeeName: 'Full Name', employeeName: 'Full Name',
workName: 'Work Name', workName: 'Work Name',
contactName: 'Contact Name', contactName: 'Contact Person',
documentReceivePoint: 'Document Drop-Off Point"', documentReceivePoint: 'Document Drop-Off Point"',
dueDate: 'Quotation Due Date', dueDate: 'Quotation Due Date',
specialCondition: 'Special Conditions', specialCondition: 'Special Conditions',
@ -874,7 +884,7 @@ export default {
SplitCustom: 'Custom Installments Bill', SplitCustom: 'Custom Installments Bill',
BillFull: 'Full Amount Bill', BillFull: 'Full Amount Bill',
BillSplit: 'Installments Bill', BillSplit: 'Installments Bill',
BillCustomSplit: 'Custom Installments Bill', BillSplitCustom: 'Custom Installments Bill',
}, },
status: { status: {
@ -910,6 +920,9 @@ export default {
code: 'Agencies Code', code: 'Agencies Code',
group: 'Agencies Group', group: 'Agencies Group',
name: 'Agencies Name', name: 'Agencies Name',
contactName: 'Contact Person',
contactTel: 'Contact Number',
bankInfo: 'Bank Information',
}, },
requestList: { requestList: {
@ -931,8 +944,9 @@ export default {
localEmployee: 'Local Employee', localEmployee: 'Local Employee',
nonLocalEmployee: 'Non Local Employee', nonLocalEmployee: 'Non Local Employee',
noWorkflowTemplate: 'A workflow template has not been selected.', noWorkflowTemplate: 'A workflow template has not been selected.',
salesRepresentative: 'Sales Representative', salesRepresentative: 'Sales Representative',
dataOffice: 'Employment Office District',
ref: 'Reference', ref: 'Reference',
action: { action: {
title: 'Action', title: 'Action',
@ -996,7 +1010,7 @@ export default {
issueBranch: 'Issue Branch', issueBranch: 'Issue Branch',
issueDate: 'Issue Date', issueDate: 'Issue Date',
madeBy: 'Made By', madeBy: 'Made By',
contactName: 'Contact Name', contactName: 'Contact Person',
workOrderCode: 'Work Order Code', workOrderCode: 'Work Order Code',
workOrderName: 'Work Order Name', workOrderName: 'Work Order Name',
telephone: 'Telephone', telephone: 'Telephone',
@ -1055,6 +1069,10 @@ export default {
confirmDebitNoteAccept: 'Confirm acceptance of the debit note.', confirmDebitNoteAccept: 'Confirm acceptance of the debit note.',
}, },
message: { 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', quotationAccept: 'Once accepted, no further modifications can be made',
beingUse: '"{msg}" is being used.', beingUse: '"{msg}" is being used.',
incompleteDataEntry: 'Incomplete data entry on {tap} page', incompleteDataEntry: 'Incomplete data entry on {tap} page',
@ -1072,10 +1090,8 @@ export default {
confirmSavingStatus: confirmSavingStatus:
'Do you want to confirm the saving of the status change data?', 'Do you want to confirm the saving of the status change data?',
confirmSending: 'Do you confirm the submission of the task?', confirmSending: 'Do you confirm the submission of the task?',
confirmValidate: 'Do you confirm the validation?',
warningSelectDeliveryStaff: warningSelectDeliveryStaff:
'You have not yet selected a document delivery staff.', 'You have not yet selected a document delivery staff.',
confirmEndWorkWarning: confirmEndWorkWarning:
"Do you want to end the work now? The current statuses 'Pending', 'In Progress', 'To Be Reprocessed' will be changed to 'Redo All'.", "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?', confirmEndWork: 'Do you want to end the work?',
@ -1183,13 +1199,14 @@ export default {
'Product with the same name already exists. If you want to create with this name please select another code.', 'Product with the same name already exists. If you want to create with this name please select another code.',
userExists: 'User already exits.', userExists: 'User already exits.',
sameNameExists: 'Same name exists.', sameNameExists: 'Same name exists.',
samePropertyNameExists: 'Same property name exists.',
validateError: 'Validate Error', validateError: 'Validate Error',
codeMisMatch: 'Code Mismatch', codeMisMatch: 'Code Mismatch',
crossCompanyNotPermit: 'Cannot move between different headoffice', crossCompanyNotPermit: 'Cannot move between different headoffice',
errorOccure: errorOccurred:
'An error has occurred, causing the system to be unable to function. Please try again later.', '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. ', authFailed: 'Authentication Failed. Please try again later. ',
installmentsValidateFailed: installmentsValidateFailed:
'Validation failed. Each installment must include at least one product. Please review and update the installments accordingly.', 'Validation failed. Each installment must include at least one product. Please review and update the installments accordingly.',
@ -1476,4 +1493,19 @@ export default {
type: 'Type', 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',
},
}; };

View file

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

View file

@ -151,6 +151,15 @@ export default {
dueDate: 'วันครบกำหนด', dueDate: 'วันครบกำหนด',
year: 'ปี', year: 'ปี',
tableOfContent: 'สารบัญ', tableOfContent: 'สารบัญ',
draw: 'วาด',
newUpload: 'อัปโหลดใหม่',
nativeLanguage: '{msg} ภาษาต้นทาง',
copy: 'คัดลอก',
paste: 'วาง',
period: 'ช่วงเวลา',
documentStatus: 'สถานะเอกสาร',
advanceSearch: 'ค้นหาขั้นสูง',
totalPeople: '{meg} คน',
}, },
menu: { menu: {
@ -246,6 +255,7 @@ export default {
manual: { manual: {
title: 'คู่มือ', title: 'คู่มือ',
usage: 'การใช้งาน', usage: 'การใช้งาน',
troubleshooting: 'การแก้ปัญหา',
}, },
}, },
@ -453,6 +463,12 @@ export default {
citizenId: 'เลขที่บัตรประชาชน', citizenId: 'เลขที่บัตรประชาชน',
citizenIssue: 'วันที่ออกบัตร', citizenIssue: 'วันที่ออกบัตร',
citizenExpire: 'วันที่หมดอายุ', citizenExpire: 'วันที่หมดอายุ',
agencyStatus: 'สถานะเอเจนซี่',
normal: 'ปกติ',
canceled: 'ยกเลิก',
blacklist: 'แบล็คลิสต์',
contactName: 'ชื่อผู้ติดต่อ',
contactTel: 'เบอร์โทรศัพท์ผู้ติดต่อ',
}, },
}, },
customer: { customer: {
@ -571,7 +587,7 @@ export default {
family: 'ข้อมูลครอบครัว', family: 'ข้อมูลครอบครัว',
}, },
workerStatus: 'สถานะคนงาน', workerStatus: 'สถานะคนงาน',
previousPassportNumber: 'หมายเลขอันเก่าหนังสือเดินทาง', previousPassportNumber: 'หมายเลขหนังสือเดินทางเล่มเก่า',
employerBranch: 'สาขานายจ้าง', employerBranch: 'สาขานายจ้าง',
employeeCode: 'รหัสลูกจ้าง', employeeCode: 'รหัสลูกจ้าง',
nrcNo: 'เลขบัตรประจำตัวคนซึ่งไม่มีสัญชาติไทย (N.R.C No.)', nrcNo: 'เลขบัตรประจำตัวคนซึ่งไม่มีสัญชาติไทย (N.R.C No.)',
@ -778,7 +794,7 @@ export default {
branch: 'สาขาที่ออกใบเสนอราคา', branch: 'สาขาที่ออกใบเสนอราคา',
branchVirtual: 'จุดรับบริการที่ออกใบเสนอราคา', branchVirtual: 'จุดรับบริการที่ออกใบเสนอราคา',
customer: 'ลูกค้า', customer: 'ลูกค้า',
newCustomer: 'แรงงานใหม่', newCustomer: 'ลูกค้าใหม่',
employeeList: 'รายชื่อแรงงาน', employeeList: 'รายชื่อแรงงาน',
employee: 'แรงงาน', employee: 'แรงงาน',
employeeName: 'ชื่อ-นามสกุล แรงงาน', employeeName: 'ชื่อ-นามสกุล แรงงาน',
@ -901,6 +917,9 @@ export default {
code: 'รหัสหน่วยงาน', code: 'รหัสหน่วยงาน',
group: 'กลุ่มหน่วยงาน', group: 'กลุ่มหน่วยงาน',
name: 'ชื่อหน่วยงาน', name: 'ชื่อหน่วยงาน',
contactName: 'ชื่อผู้ติดต่อ',
contactTel: 'เบอร์โทรผู้ติดต่อ',
bankInfo: 'ข้อมูลธนาคาร',
}, },
requestList: { requestList: {
@ -922,6 +941,7 @@ export default {
nonLocalEmployee: 'พนักงานนอกพื้นที่', nonLocalEmployee: 'พนักงานนอกพื้นที่',
noWorkflowTemplate: 'คุณไม่ได้เลือกแม่แบบขั้นตอนการทำงาน', noWorkflowTemplate: 'คุณไม่ได้เลือกแม่แบบขั้นตอนการทำงาน',
salesRepresentative: 'พนักงานขาย', salesRepresentative: 'พนักงานขาย',
dataOffice: 'สำนักงานเขตจัดหางาน',
ref: 'อ้างอิง', ref: 'อ้างอิง',
action: { action: {
title: 'จัดการ', title: 'จัดการ',
@ -1040,6 +1060,9 @@ export default {
confirmDebitNoteAccept: 'ยืนยันการตอบรับใบเพิ่มหนี้', confirmDebitNoteAccept: 'ยืนยันการตอบรับใบเพิ่มหนี้',
}, },
message: { message: {
copy: 'คัดลอก',
warningPaste: 'คุณต้องการที่จะเเทนที่ข้อมูลที่คัดลอกมาใหม่ใช่หรือไม่',
warningCopyEmpty: 'คุณยังไม่ได้คัดลอกข้อมูล',
quotationAccept: 'เมื่อตอบรับเเล้วจะไม่สามารถแก้ไขได้อีก', quotationAccept: 'เมื่อตอบรับเเล้วจะไม่สามารถแก้ไขได้อีก',
beingUse: '"{msg}" มีการใช้งานอยู่', beingUse: '"{msg}" มีการใช้งานอยู่',
incompleteDataEntry: 'กรอกข้อมูลไม่ครบในหน้า {tap}', incompleteDataEntry: 'กรอกข้อมูลไม่ครบในหน้า {tap}',
@ -1161,13 +1184,14 @@ export default {
'สินค้าที่มีชื่อเดียวกันมีในระบบแล้ว หากคุณต้องการสร้างด้วยชื่อนี้โปรดเลือกรหัสอื่น', 'สินค้าที่มีชื่อเดียวกันมีในระบบแล้ว หากคุณต้องการสร้างด้วยชื่อนี้โปรดเลือกรหัสอื่น',
userExists: 'ชื่อผู้ใช้นี้มีอยู่ในระบบอยู่แล้ว', userExists: 'ชื่อผู้ใช้นี้มีอยู่ในระบบอยู่แล้ว',
sameNameExists: 'ชื่อนี้ถูกใช้ไปแล้ว', sameNameExists: 'ชื่อนี้ถูกใช้ไปแล้ว',
samePropertyNameExists: 'คุณสมบัตินี้มีอยู่ในระบบอยู่แล้ว',
validateError: 'เกิดข้อผิดพลาดจากการตรวจสอบ', validateError: 'เกิดข้อผิดพลาดจากการตรวจสอบ',
codeMisMatch: 'รหัสไม่ตรงกัน', codeMisMatch: 'รหัสไม่ตรงกัน',
crossCompanyNotPermit: 'ไม่สามารถดำเนินการระหว่างสำนักงานใหญ่อื่นได้', crossCompanyNotPermit: 'ไม่สามารถดำเนินการระหว่างสำนักงานใหญ่อื่นได้',
errorOccure: errorOccurred:
'เกิดข้อผิดพลาดทำให้ระบบไม่สามารถทำงานได้ กรุณาลองใหม่ในภายหลัง', 'เกิดข้อผิดพลาดทำให้ระบบไม่สามารถทำงานได้ กรุณาลองใหม่ในภายหลัง',
invalideData: 'ข้อมูลไม่ถูกต้อง กรุณาตรวจสอบใหม่อีกครั้ง', invalidData: 'ข้อมูลไม่ถูกต้อง กรุณาตรวจสอบใหม่อีกครั้ง',
authFailed: 'การยืนยันตัวตนล้มเหลว กรุณาลองใหม่ในภายหลัง', authFailed: 'การยืนยันตัวตนล้มเหลว กรุณาลองใหม่ในภายหลัง',
installmentsValidateFailed: installmentsValidateFailed:
'ข้อมูลงวดไม่ถูกต้อง กรุณาตรวจสอบและยืนยันว่าแต่ละงวดมีสินค้าอย่างน้อยหนึ่งรายการ', 'ข้อมูลงวดไม่ถูกต้อง กรุณาตรวจสอบและยืนยันว่าแต่ละงวดมีสินค้าอย่างน้อยหนึ่งรายการ',
@ -1457,4 +1481,19 @@ export default {
type: 'ประเภท', type: 'ประเภท',
}, },
}, },
dateRange: {
today: 'วันนี้',
yesterday: 'เมื่อวานนี้',
thisWeek: 'สัปดาห์นี้',
lastWeek: 'สัปดาห์ที่แล้ว',
thisMonth: 'เดือนนี้',
lastMonth: 'เดือนที่แล้ว',
thisYear: 'ปีนี้',
lastYear: 'ปีที่แล้ว',
last7Days: '7 วันที่ผ่านมา',
last30Days: '30 วันที่ผ่านมา',
last90Days: '90 วันที่ผ่านมา',
customDateRange: 'กำหนดช่วงวันที่เอง',
},
}; };

View file

@ -215,6 +215,10 @@ function initMenu() {
label: 'usage', label: 'usage',
route: '/manual', route: '/manual',
}, },
{
label: 'troubleshooting',
route: '/troubleshooting',
},
], ],
}, },
]; ];

View file

@ -50,7 +50,6 @@ const leftDrawerMini = ref(false);
const unread = computed<number>( const unread = computed<number>(
() => notificationData.value.filter((v) => !v.read).length || 0, () => notificationData.value.filter((v) => !v.read).length || 0,
); );
// const filterRole = ref<string[]>();
const userImage = ref<string>(); const userImage = ref<string>();
const userGender = ref(''); const userGender = ref('');
const canvasRef = ref(); const canvasRef = ref();
@ -124,6 +123,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 () => { onMounted(async () => {
initTheme(); initTheme();
initLang(); initLang();
@ -367,12 +377,28 @@ onMounted(async () => {
<div class="col column text-caption q-pl-md ellipsis"> <div class="col column text-caption q-pl-md ellipsis">
<span class="block ellipsis full-width text-weight-bold"> <span class="block ellipsis full-width text-weight-bold">
{{ item.title }} {{ item.title }}
<q-tooltip
anchor="top middle"
self="bottom middle"
:delay="300"
:offset="[10, 10]"
>
{{ item.title }}
</q-tooltip>
</span> </span>
<span <span
class="block ellipsis full-width text-stone" class="block ellipsis full-width text-stone"
:class="{ 'text-weight-medium': !item.read }" :class="{ 'text-weight-medium': !item.read }"
> >
{{ item.detail }} {{ item.detail }}
<q-tooltip
anchor="top middle"
self="bottom middle"
:delay="300"
:offset="[10, 10]"
>
{{ item.detail }}
</q-tooltip>
</span> </span>
</div> </div>
<span <span
@ -382,15 +408,6 @@ onMounted(async () => {
> >
{{ moment(item.createdAt).fromNow() }} {{ moment(item.createdAt).fromNow() }}
</span> </span>
<q-tooltip
anchor="top middle"
self="bottom middle"
:delay="1000"
:offset="[10, 10]"
>
{{ item.title }}
{{ item.detail }}
</q-tooltip>
</q-item> </q-item>
</section> </section>
<section <section
@ -495,13 +512,15 @@ onMounted(async () => {
no-app-box no-app-box
:title="$t('menu.profile.addSignature')" :title="$t('menu.profile.addSignature')"
:close="() => (canvasModal = false)" :close="() => (canvasModal = false)"
:submit="signatureSubmit"
:show="signatureFetch"
> >
<CanvasComponent ref="canvasRef" v-model:modal="canvasModal" /> <CanvasComponent ref="canvasRef" v-model:modal="canvasModal" />
<template #footer> <template #footer>
<q-btn <q-btn
flat flat
dense dense
:label="$t('clear')" :label="$t('general.clear')"
@click=" @click="
() => { () => {
canvasRef.clearCanvas(), canvasRef.clearUpload(); canvasRef.clearCanvas(), canvasRef.clearUpload();

View file

@ -31,7 +31,7 @@ const options = [
label: 'menu.profile.signature', label: 'menu.profile.signature',
value: 'signature', value: 'signature',
color: 'grey', color: 'grey',
disabled: true, disabled: false,
}, },
{ {
icon: 'mdi-brightness-6', icon: 'mdi-brightness-6',

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

View file

@ -5,9 +5,7 @@ import hljs from 'highlight.js';
import { nextTick, onMounted, onUnmounted, ref } from 'vue'; import { nextTick, onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
// @ts-expect-error
import mditFigureWithPCaption from 'markdown-it-image-figures'; import mditFigureWithPCaption from 'markdown-it-image-figures';
// @ts-expect-error
import mditMedia from 'markdown-it-html5-media'; import mditMedia from 'markdown-it-html5-media';
import mditAnchor from 'markdown-it-anchor'; import mditAnchor from 'markdown-it-anchor';
import mditHighlight from 'markdown-it-highlightjs'; import mditHighlight from 'markdown-it-highlightjs';
@ -58,6 +56,8 @@ onUnmounted(() => {
async function getContent() { async function getContent() {
if (!category.value || !page.value) return; if (!category.value || !page.value) return;
if (ROUTE.name === 'ManualView') {
const res = await manualStore.getManualByPage({ const res = await manualStore.getManualByPage({
category: category.value, category: category.value,
pageName: page.value, pageName: page.value,
@ -68,6 +68,18 @@ async function getContent() {
contentParsed.value = md.parse(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, {});
}
}
}
function onScroll() { function onScroll() {
let current = ''; let current = '';
@ -186,7 +198,9 @@ async function scrollTo(id: string) {
md.render( md.render(
content.replaceAll( content.replaceAll(
'assets/', 'assets/',
`${baseUrl}/manual/${category}/assets/`, $route.name === 'ManualView'
? `${baseUrl}/manual/${category}/assets/`
: `${baseUrl}/troubleshooting/${category}/assets/`,
), ),
) )
" "
@ -317,4 +331,20 @@ async function scrollTo(id: string) {
.markdown :deep(video) { .markdown :deep(video) {
width: 100%; 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> </style>

View file

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

View file

@ -10,8 +10,8 @@ import useOptionStore from 'stores/options';
import useAddressStore from 'stores/address'; import useAddressStore from 'stores/address';
import useMyBranch from 'src/stores/my-branch'; import useMyBranch from 'src/stores/my-branch';
import { calculateAge } from 'src/utils/datetime'; import { calculateAge } from 'src/utils/datetime';
import { QSelect, useQuasar, type QTableProps } from 'quasar'; import { useQuasar, type QTableProps } from 'quasar';
import { dialog, baseUrl } from 'stores/utils'; import { dialog, baseUrl, setPrefixName } from 'stores/utils';
import { useNavigator } from 'src/stores/navigator'; import { useNavigator } from 'src/stores/navigator';
import { isRoleInclude, resetScrollBar } from 'src/stores/utils'; import { isRoleInclude, resetScrollBar } from 'src/stores/utils';
import { BranchUserStats } from 'stores/branch/types'; 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 FormByType from 'components/02_personnel-management/FormByType.vue';
import FormInformation from 'components/02_personnel-management/FormInformation.vue'; import FormInformation from 'components/02_personnel-management/FormInformation.vue';
import PaginationPageSize from 'src/components/PaginationPageSize.vue'; import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { locale, t } = useI18n(); const { locale, t } = useI18n();
const $q = useQuasar(); const $q = useQuasar();
@ -73,7 +74,6 @@ const isImageEdit = ref(false);
const imageDialog = ref(false); const imageDialog = ref(false);
const infoDrawerEdit = ref(false); const infoDrawerEdit = ref(false);
const refreshImageState = ref(false); const refreshImageState = ref(false);
const refFilter = ref<InstanceType<typeof QSelect>>();
const firstScroll = ref(false); const firstScroll = ref(false);
const inputSearch = ref(''); const inputSearch = ref('');
@ -93,12 +93,14 @@ const currentUser = ref<User>();
const userCode = ref<string>(); const userCode = ref<string>();
const statusToggle = ref(true); const statusToggle = ref(true);
const agencyFile = ref<File[]>([]); const userFile = ref<File[]>([]);
const agencyFileList = ref<{ name: string; url: string }[]>([]); const userFileList = ref<{ name: string; url: string }[]>([]);
const typeStats = ref<UserTypeStats>(); const typeStats = ref<UserTypeStats>();
const userStats = ref<BranchUserStats[]>(); const userStats = ref<BranchUserStats[]>();
const searchDate = ref<[]>([]);
const urlProfile = ref<string>(); const urlProfile = ref<string>();
const profileFileImg = ref<File | null>(null); const profileFileImg = ref<File | null>(null);
const imageList = ref<{ selectedImage: string; list: string[] }>(); const imageList = ref<{ selectedImage: string; list: string[] }>();
@ -124,7 +126,7 @@ const defaultFormData = {
streetEN: '', streetEN: '',
street: '', street: '',
trainingPlace: null, trainingPlace: null,
importNationality: null, importNationality: [],
sourceNationality: null, sourceNationality: null,
licenseExpireDate: null, licenseExpireDate: null,
licenseIssueDate: null, licenseIssueDate: null,
@ -151,6 +153,10 @@ const defaultFormData = {
citizenExpire: null, citizenExpire: null,
citizenIssue: null, citizenIssue: null,
citizenId: '', citizenId: '',
contactName: '',
contactTel: '',
remark: '',
agencyStatus: '',
}; };
const formData = ref<UserCreate>({ const formData = ref<UserCreate>({
@ -172,7 +178,7 @@ const formData = ref<UserCreate>({
streetEN: '', streetEN: '',
street: '', street: '',
trainingPlace: null, trainingPlace: null,
importNationality: null, importNationality: [],
sourceNationality: null, sourceNationality: null,
licenseExpireDate: null, licenseExpireDate: null,
licenseIssueDate: null, licenseIssueDate: null,
@ -199,6 +205,10 @@ const formData = ref<UserCreate>({
citizenExpire: null, citizenExpire: null,
citizenIssue: null, citizenIssue: null,
citizenId: '', citizenId: '',
contactName: '',
contactTel: '',
remark: '',
agencyStatus: '',
}); });
const fieldSelectedOption = ref<{ label: string; value: string }[]>([ const fieldSelectedOption = ref<{ label: string; value: string }[]>([
@ -327,7 +337,7 @@ function onClose(excludeDialog?: boolean) {
urlProfile.value = ''; urlProfile.value = '';
profileFileImg.value = null; profileFileImg.value = null;
infoDrawerEdit.value = false; infoDrawerEdit.value = false;
agencyFile.value = []; userFile.value = [];
isEdit.value = false; isEdit.value = false;
statusToggle.value = true; statusToggle.value = true;
isImageEdit.value = false; isImageEdit.value = false;
@ -336,6 +346,8 @@ function onClose(excludeDialog?: boolean) {
mapUserType(currentTab.value); mapUserType(currentTab.value);
imageList.value = { selectedImage: '', list: [] }; imageList.value = { selectedImage: '', list: [] };
onCreateImageList.value = { selectedImage: '', list: [] }; onCreateImageList.value = { selectedImage: '', list: [] };
userFileList.value = [];
userFile.value = [];
flowStore.rotate(); flowStore.rotate();
} }
@ -356,12 +368,10 @@ async function openDialog(
isEdit.value = true; isEdit.value = true;
await assignFormData(id); await assignFormData(id);
if (formData.value.userType === 'AGENCY') {
const result = await userStore.fetchAttachment(id); const result = await userStore.fetchAttachment(id);
if (result) { if (result) {
agencyFileList.value = result; userFileList.value = result;
}
} }
} }
if (userStore.userOption.hqOpts.length !== 0 && !id) { if (userStore.userOption.hqOpts.length !== 0 && !id) {
@ -419,15 +429,15 @@ async function onSubmit(excludeDialog?: boolean) {
: ''; : '';
const formDataEdit = { const formDataEdit = {
...formData.value, ...formData.value,
checkpointEN: formData.value.checkpoint,
status: !statusToggle.value ? 'INACTIVE' : 'ACTIVE', status: !statusToggle.value ? 'INACTIVE' : 'ACTIVE',
} as const; } as const;
await userStore.editById(currentUser.value.id, formDataEdit); await userStore.editById(currentUser.value.id, formDataEdit);
if (currentUser.value.id && formDataEdit.userType === 'AGENCY') { if (userFile.value) {
if (!agencyFile.value) return;
const payload: UserAttachmentCreate = { const payload: UserAttachmentCreate = {
file: agencyFile.value, file: userFile.value,
}; };
if (payload?.file) { if (payload?.file) {
@ -450,16 +460,15 @@ async function onSubmit(excludeDialog?: boolean) {
: hqId.value : hqId.value
? hqId.value ? hqId.value
: ''; : '';
formData.value.checkpointEN = formData.value.checkpoint;
const result = await userStore.create( const result = await userStore.create(
formData.value, formData.value,
onCreateImageList.value, onCreateImageList.value,
); );
if (result && formData.value.userType === 'AGENCY') { if (userFile.value && result) {
if (!agencyFile.value) return;
const payload: UserAttachmentCreate = { const payload: UserAttachmentCreate = {
file: agencyFile.value, file: userFile.value,
}; };
if (payload?.file) { if (payload?.file) {
@ -546,6 +555,7 @@ async function triggerChangeStatus(id: string, status: string) {
async function assignFormData(idEdit: string) { async function assignFormData(idEdit: string) {
if (!userData.value) return; if (!userData.value) return;
const foundUser = userData.value.result.find((user) => user.id === idEdit); const foundUser = userData.value.result.find((user) => user.id === idEdit);
console.log(foundUser);
if (foundUser) { if (foundUser) {
currentUser.value = foundUser; currentUser.value = foundUser;
@ -567,7 +577,10 @@ async function assignFormData(idEdit: string) {
street: foundUser.street, street: foundUser.street,
streetEN: foundUser.streetEN, streetEN: foundUser.streetEN,
trainingPlace: foundUser.trainingPlace, trainingPlace: foundUser.trainingPlace,
importNationality: foundUser.importNationality, importNationality:
typeof foundUser.importNationality === 'string'
? [foundUser.importNationality]
: foundUser.importNationality,
sourceNationality: foundUser.sourceNationality, sourceNationality: foundUser.sourceNationality,
licenseNo: foundUser.licenseNo, licenseNo: foundUser.licenseNo,
discountCondition: foundUser.discountCondition, discountCondition: foundUser.discountCondition,
@ -587,6 +600,8 @@ async function assignFormData(idEdit: string) {
responsibleArea: foundUser.responsibleArea, responsibleArea: foundUser.responsibleArea,
status: foundUser.status, status: foundUser.status,
selectedImage: foundUser.selectedImage, selectedImage: foundUser.selectedImage,
contactName: foundUser.contactName,
contactTel: foundUser.contactTel,
licenseExpireDate: licenseExpireDate:
(foundUser.licenseExpireDate && (foundUser.licenseExpireDate &&
new Date(foundUser.licenseExpireDate)) || new Date(foundUser.licenseExpireDate)) ||
@ -603,6 +618,8 @@ async function assignFormData(idEdit: string) {
(foundUser.citizenIssue && new Date(foundUser.citizenIssue)) || null, (foundUser.citizenIssue && new Date(foundUser.citizenIssue)) || null,
citizenExpire: citizenExpire:
(foundUser.citizenExpire && new Date(foundUser.citizenExpire)) || null, (foundUser.citizenExpire && new Date(foundUser.citizenExpire)) || null,
remark: foundUser.remark || '',
agencyStatus: foundUser.agencyStatus || '',
}; };
formData.value.status === 'ACTIVE' || 'CREATED' formData.value.status === 'ACTIVE' || 'CREATED'
@ -661,6 +678,8 @@ async function fetchUserList(mobileFetch?: boolean) {
: statusFilter.value === 'statusACTIVE' : statusFilter.value === 'statusACTIVE'
? 'ACTIVE' ? 'ACTIVE'
: 'INACTIVE', : 'INACTIVE',
startDate: searchDate.value[0],
endDate: searchDate.value[1],
}); });
if (ret) { if (ret) {
@ -735,11 +754,11 @@ watch(
formData.value.responsibleArea = null; formData.value.responsibleArea = null;
formData.value.discountCondition = null; formData.value.discountCondition = null;
formData.value.sourceNationality = null; formData.value.sourceNationality = null;
formData.value.importNationality = null; formData.value.importNationality = [];
formData.value.trainingPlace = null; formData.value.trainingPlace = null;
formData.value.checkpoint = null; formData.value.checkpoint = null;
formData.value.checkpointEN = null; formData.value.checkpointEN = null;
agencyFile.value = []; userFile.value = [];
}, },
); );
@ -750,7 +769,7 @@ watch(
}, },
); );
watch([inputSearch, statusFilter, pageSize], async () => { watch([inputSearch, statusFilter, pageSize, searchDate], async () => {
if (userData.value) userData.value.result = []; if (userData.value) userData.value.result = [];
currentPage.value = 1; currentPage.value = 1;
@ -872,26 +891,45 @@ watch(
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && statusFilter !== 'all'"
unelevated >
class="q-ml-sm" <div
padding="4px" v-if="$q.screen.lt.md"
size="sm" class="q-mt-sm text-weight-medium"
rounded >
@click="refFilter?.showPopup" {{ $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> </template>
</q-input> </q-input>
<div class="row col-md-5" style="white-space: nowrap"> <div class="row col-md-5" style="white-space: nowrap">
<q-select <q-select
v-show="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
ref="refFilter"
v-model="statusFilter" v-model="statusFilter"
outlined outlined
dense dense
@ -1551,7 +1589,18 @@ watch(
v-model:toggle-status="formData.status" v-model:toggle-status="formData.status"
hideFade hideFade
:toggle-title="$t('status.title')" :toggle-title="$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 },
)
"
:caption="userCode" :caption="userCode"
:img=" :img="
`${baseUrl}/user/${currentUser.id}/profile-image/${formData.selectedImage}`.concat( `${baseUrl}/user/${currentUser.id}/profile-image/${formData.selectedImage}`.concat(
@ -1736,12 +1785,15 @@ watch(
v-model:citizen-id="formData.citizenId" v-model:citizen-id="formData.citizenId"
v-model:citizen-issue="formData.citizenIssue" v-model:citizen-issue="formData.citizenIssue"
v-model:citizen-expire="formData.citizenExpire" v-model:citizen-expire="formData.citizenExpire"
v-model:contact-name="formData.contactName"
v-model:contact-tel="formData.contactTel"
:title="'personnel.form.personalInformation'" :title="'personnel.form.personalInformation'"
prefix-id="drawer-info-personnel" prefix-id="drawer-info-personnel"
dense dense
outlined outlined
separator separator
:readonly="!infoDrawerEdit" :readonly="!infoDrawerEdit"
:agency="formData.userType === 'AGENCY'"
class="q-mb-xl" class="q-mb-xl"
/> />
@ -1781,10 +1833,11 @@ watch(
v-model:import-nationality="formData.importNationality" v-model:import-nationality="formData.importNationality"
v-model:training-place="formData.trainingPlace" v-model:training-place="formData.trainingPlace"
v-model:checkpoint="formData.checkpoint" v-model:checkpoint="formData.checkpoint"
v-model:checkpoint-en="formData.checkpointEN" v-model:user-file="userFile"
v-model:agency-file="agencyFile" v-model:user-file-list="userFileList"
v-model:agency-file-list="agencyFileList"
v-model:user-id="currentUser.id" v-model:user-id="currentUser.id"
v-model:remark="formData.remark"
v-model:agency-status="formData.agencyStatus"
/> />
</div> </div>
</div> </div>
@ -1828,7 +1881,18 @@ watch(
}[formData.gender] }[formData.gender]
" "
:toggleTitle="$t('status.title')" :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=" :fallbackImg="
{ {
male: '/no-img-man.png', male: '/no-img-man.png',
@ -1854,7 +1918,6 @@ watch(
<div <div
class="col" class="col"
id="personnel-form"
:class="{ :class="{
'q-px-lg q-pb-lg': $q.screen.gt.sm, 'q-px-lg q-pb-lg': $q.screen.gt.sm,
'q-px-md q-pb-sm': !$q.screen.gt.sm, 'q-px-md q-pb-sm': !$q.screen.gt.sm,
@ -1898,7 +1961,7 @@ watch(
? [ ? [
{ {
name: $t('personnel.form.workInformation'), name: $t('personnel.form.workInformation'),
anchor: 'dialog-info-work', anchor: 'dialog-form-work',
}, },
] ]
: [], : [],
@ -1914,6 +1977,7 @@ watch(
</div> </div>
</div> </div>
<div <div
id="personnel-form"
class="col-md-10 col-12 full-height scroll" class="col-md-10 col-12 full-height scroll"
:class="{ :class="{
'q-py-md q-pr-md ': $q.screen.gt.sm, 'q-py-md q-pr-md ': $q.screen.gt.sm,
@ -1939,6 +2003,7 @@ watch(
id="dialog-form-personal" id="dialog-form-personal"
prefix-id="form-dialog-personnel" prefix-id="form-dialog-personnel"
dense dense
:agency="formData.userType === 'AGENCY'"
outlined outlined
separator separator
:title="'personnel.form.personalInformation'" :title="'personnel.form.personalInformation'"
@ -1957,6 +2022,8 @@ watch(
v-model:citizen-id="formData.citizenId" v-model:citizen-id="formData.citizenId"
v-model:citizen-issue="formData.citizenIssue" v-model:citizen-issue="formData.citizenIssue"
v-model:citizen-expire="formData.citizenExpire" v-model:citizen-expire="formData.citizenExpire"
v-model:contact-name="formData.contactName"
v-model:contact-tel="formData.contactTel"
class="q-mb-xl" class="q-mb-xl"
/> />
<AddressForm <AddressForm
@ -1992,8 +2059,10 @@ watch(
v-model:import-nationality="formData.importNationality" v-model:import-nationality="formData.importNationality"
v-model:training-place="formData.trainingPlace" v-model:training-place="formData.trainingPlace"
v-model:checkpoint="formData.checkpoint" v-model:checkpoint="formData.checkpoint"
v-model:checkpoint-en="formData.checkpointEN" v-model:agency-status="formData.agencyStatus"
v-model:agency-file="agencyFile" v-model:remark="formData.remark"
v-model:user-file="userFile"
v-model:user-file-list="userFileList"
/> />
</div> </div>
</div> </div>

View file

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted, computed } from 'vue'; import { ref, watch, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QSelect, useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { getUserId, getRole } from 'src/services/keycloak'; import { getUserId, getRole } from 'src/services/keycloak';
import { baseUrl, waitAll } from 'src/stores/utils'; import { baseUrl, setPrefixName, waitAll } from 'src/stores/utils';
import { dateFormat } from 'src/utils/datetime'; import { dateFormat } from 'src/utils/datetime';
import { dialogCheckData } from 'stores/utils'; import { dialogCheckData } from 'stores/utils';
@ -86,6 +86,7 @@ import { nextTick } from 'vue';
import FormEmployeeVisa from 'components/03_customer-management/FormEmployeeVisa.vue'; import FormEmployeeVisa from 'components/03_customer-management/FormEmployeeVisa.vue';
import PaginationPageSize from 'src/components/PaginationPageSize.vue'; import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import { AddButton } from 'components/button'; import { AddButton } from 'components/button';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const $q = useQuasar(); const $q = useQuasar();
@ -101,28 +102,33 @@ const employeeFormStore = useEmployeeForm();
const optionStore = useOptionStore(); const optionStore = useOptionStore();
const ocrStore = useOcrStore(); const ocrStore = useOcrStore();
const refFilter = ref<InstanceType<typeof QSelect>>();
const statusEmployeeCreate = ref<boolean>(false);
const mrz = ref<Awaited<ReturnType<typeof parseResultMRZ>>>(); const mrz = ref<Awaited<ReturnType<typeof parseResultMRZ>>>();
const tabFieldRequired = ref<{ [key: string]: (keyof CustomerBranchCreate)[] }>(
{
main: [],
business: ['businessType', 'jobPosition'],
address: [
'address',
'addressEN',
'provinceId',
'districtId',
'subDistrictId',
],
contact: [],
},
);
const { state: customerFormState, currentFormData: customerFormData } = const {
storeToRefs(customerFormStore); fetchListOfOptionBranch,
const { state: employeeFormState, currentFromDataEmployee } = customerFormUndo,
storeToRefs(employeeFormStore); customerConfirmUnsave,
deleteCustomerById,
validateTabField,
deleteCustomerBranchById,
} = customerFormStore;
const { employeeFormUndo, employeeConfirmUnsave, deleteEmployeeById } =
employeeFormStore;
const {
state: customerFormState,
currentFormData: customerFormData,
registerAbleBranchOption,
tabFieldRequired,
} = storeToRefs(customerFormStore);
const {
state: employeeFormState,
currentFromDataEmployee,
onCreateImageList,
statusEmployeeCreate,
refreshImageState,
} = storeToRefs(employeeFormStore);
async function init() { async function init() {
navigatorStore.current.title = 'menu.customer'; navigatorStore.current.title = 'menu.customer';
@ -136,6 +142,14 @@ async function init() {
gridView.value = $q.screen.lt.md ? true : false; gridView.value = $q.screen.lt.md ? true : false;
if (route.query.tab === 'customer') {
currentTab.value = 'employer';
if (route.query.id) openSpecificCustomer(route.query.id as string);
} else if (route.query.tab === 'employee') {
currentTab.value = 'employee';
if (route.query.id) openSpecificEmployee(route.query.id as string);
}
if (route.name === 'CustomerManagement') await fetchListCustomer(true); if (route.name === 'CustomerManagement') await fetchListCustomer(true);
if ( if (
@ -175,6 +189,7 @@ const statsCustomerType = ref<CustomerStats>({
}); });
// NOTE: Page State // NOTE: Page State
const searchDate = ref<string[]>([]);
const currentTab = ref<'employer' | 'employee'>('employer'); const currentTab = ref<'employer' | 'employee'>('employer');
const inputSearch = ref(''); const inputSearch = ref('');
const currentStatus = ref<Status | 'All'>('All'); const currentStatus = ref<Status | 'All'>('All');
@ -209,17 +224,19 @@ const dialogCustomerImageUpload = ref<InstanceType<typeof ImageUploadDialog>>();
const dialogEmployeeImageUpload = ref<InstanceType<typeof ImageUploadDialog>>(); const dialogEmployeeImageUpload = ref<InstanceType<typeof ImageUploadDialog>>();
// image // image
const refreshImageState = ref(false);
const imageList = ref<{ selectedImage: string; list: string[] }>(); const imageList = ref<{ selectedImage: string; list: string[] }>();
const onCreateImageList = ref<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>({ selectedImage: '', list: [] });
watch(() => route.name, init); watch(() => route.name, init);
watch( watch(
[currentTab, currentStatus, inputSearch, customerTypeSelected, pageSize], [
async ([tabName]) => { currentTab,
currentStatus,
inputSearch,
customerTypeSelected,
pageSize,
searchDate,
],
async ([tabName], [oldTabName]) => {
// if (tabName !== oldTabName) searchDate.value = [];
if (tabName === 'employer') { if (tabName === 'employer') {
currentPageCustomer.value = 1; currentPageCustomer.value = 1;
currentBtnOpen.value = []; currentBtnOpen.value = [];
@ -276,8 +293,6 @@ const fieldSelected = ref<string[]>(
].filter((v, index, self) => self.indexOf(v) === index), ].filter((v, index, self) => self.indexOf(v) === index),
); );
const registerAbleBranchOption = ref<{ id: string; name: string }[]>();
const branch = ref<CustomerBranch[]>(); const branch = ref<CustomerBranch[]>();
const customerStats = [ const customerStats = [
@ -294,81 +309,6 @@ const fieldCustomer = [
const employeeHistoryDialog = ref(false); const employeeHistoryDialog = ref(false);
const employeeHistory = ref<EmployeeHistory[]>(); const employeeHistory = ref<EmployeeHistory[]>();
function validateTabField<T = CustomerBranchCreate>(
value: T,
fieldRequired: { [key: string]: (keyof T)[] },
) {
const list: string[] = [];
for (const tab in fieldRequired) {
for (const field of fieldRequired[tab]) {
if (!value[field] && !list.includes(tab)) list.push(tab);
}
}
return list;
}
function deleteCustomerById(id: string) {
dialog({
color: 'negative',
icon: 'mdi-alert',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
persistent: true,
message: t('dialog.message.confirmDelete'),
action: async () => {
await customerStore.deleteById(id);
await fetchListCustomer(true, $q.screen.xs);
customerFormState.value.dialogModal = false;
flowStore.rotate();
},
cancel: () => {},
});
}
async function deleteCustomerBranchById(id: string) {
return await new Promise((resolve) => {
dialog({
color: 'negative',
icon: 'mdi-alert',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
persistent: true,
message: t('dialog.message.confirmDelete'),
action: async () => {
await customerStore.deleteBranchById(id);
flowStore.rotate();
resolve(true);
},
cancel: () => {
resolve(false);
},
});
});
}
async function fetchListOfOptionBranch() {
if (registerAbleBranchOption.value) return;
const uid = getUserId();
const role = getRole();
if (!uid) return; // should not possible as the system require login to be able to access resource.
if (role?.includes('system')) {
const result = await userBranchStore.fetchListOptionBranch();
if (result && result.total > 0)
registerAbleBranchOption.value = result.result;
} else {
const result = await userBranchStore.fetchListMyBranch(uid);
if (result && result.total > 0)
registerAbleBranchOption.value = result.result;
}
// TODO: Assign (first) branch of the user as register branch of the data
}
async function fetchListCustomer(fetchStats = false, mobileFetch?: boolean) { async function fetchListCustomer(fetchStats = false, mobileFetch?: boolean) {
const total = statsCustomerType.value.PERS + statsCustomerType.value.CORP; const total = statsCustomerType.value.PERS + statsCustomerType.value.CORP;
@ -386,6 +326,8 @@ async function fetchListCustomer(fetchStats = false, mobileFetch?: boolean) {
? 'ACTIVE' ? 'ACTIVE'
: 'INACTIVE', : 'INACTIVE',
query: inputSearch.value, query: inputSearch.value,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
customerType: ( customerType: (
{ {
all: undefined, all: undefined,
@ -439,6 +381,8 @@ async function fetchListEmployee(opt?: {
query: inputSearch.value, query: inputSearch.value,
passport: true, passport: true,
visa: true, visa: true,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
}); });
if (resultListEmployee) { if (resultListEmployee) {
maxPageEmployee.value = Math.ceil( maxPageEmployee.value = Math.ceil(
@ -503,64 +447,6 @@ async function toggleStatusCustomer(id: string, status: boolean) {
await fetchListCustomer(false, $q.screen.xs); await fetchListCustomer(false, $q.screen.xs);
flowStore.rotate(); flowStore.rotate();
} }
async function deleteEmployeeById(opts: {
id?: string;
type?: 'passport' | 'visa' | 'healthCheck' | 'work';
index?: number;
}) {
dialog({
color: 'negative',
icon: 'mdi-alert',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
persistent: true,
message: t('dialog.message.confirmDelete'),
action: async () => {
if (opts.type === 'passport' && opts.index !== undefined) {
await employeeFormStore.deletePassport(opts.index);
}
if (opts.type === 'visa' && opts.index !== undefined) {
await employeeFormStore.deleteVisa(opts.index);
}
if (opts.type === 'healthCheck' && opts.index !== undefined) {
await employeeFormStore.deleteHealthCheck(opts.index);
}
if (opts.type === 'work' && opts.index !== undefined) {
await employeeFormStore.deleteWorkHistory(opts.index);
} else {
if (!!opts.id) {
const result = await employeeStore.deleteById(opts.id);
if (result) {
employeeFormState.value.drawerModal = false;
employeeFormState.value.dialogModal = false;
}
}
}
if (route.name !== 'CustomerBranchManagement') {
await fetchListEmployee(
currentTab.value === 'employer'
? {
page: 1,
pageSize: 999,
customerId: customerFormState.value.currentCustomerId,
}
: { fetchStats: true, mobileFetch: $q.screen.xs },
);
flowStore.rotate();
}
},
cancel: () => {},
});
}
async function openHistory(id: string) { async function openHistory(id: string) {
const res = await employeeStore.getEditHistory(id); const res = await employeeStore.getEditHistory(id);
employeeHistory.value = res.reverse(); employeeHistory.value = res.reverse();
@ -594,64 +480,6 @@ async function editEmployeeFormPersonal(id: string) {
employeeFormState.value.drawerModal = true; employeeFormState.value.drawerModal = true;
} }
function employeeConfirmUnsave(close = true) {
dialog({
color: 'warning',
icon: 'mdi-alert',
title: t('form.warning.title'),
actionText: t('general.ok'),
persistent: true,
message: t('form.warning.unsave'),
action: () => {
employeeFormStore.resetFormDataEmployee();
onCreateImageList.value = { selectedImage: '', list: [] };
employeeFormState.value.editReadonly = true;
employeeFormState.value.dialogModal = !close;
employeeFormState.value.drawerModal = !close;
},
cancel: () => {},
});
}
function employeeFormUndo(close = true) {
if (employeeFormStore.isFormDataDifferent()) {
return employeeConfirmUnsave(close);
}
employeeFormStore.resetFormDataEmployee();
employeeFormState.value.editReadonly = true;
}
function customerConfirmUnsave(close = true) {
dialog({
color: 'warning',
icon: 'mdi-alert',
title: t('form.warning.title'),
actionText: t('general.ok'),
persistent: true,
message: t('form.warning.unsave'),
action: () => {
customerFormStore.resetForm();
customerFormState.value.readonly = true;
if (!customerFormState.value.drawerModal) {
customerFormState.value.dialogModal = !close;
} else {
customerFormState.value.drawerModal = !close;
}
},
cancel: () => {},
});
}
function customerFormUndo(close = true) {
if (customerFormStore.isFormDataDifferent()) {
return customerConfirmUnsave(close);
}
customerFormStore.resetForm();
customerFormState.value.readonly = true;
}
async function createCustomerForm(customerType: 'CORP' | 'PERS') { async function createCustomerForm(customerType: 'CORP' | 'PERS') {
customerFormState.value.dialogModal = true; customerFormState.value.dialogModal = true;
customerFormState.value.dialogType = 'create'; customerFormState.value.dialogType = 'create';
@ -683,10 +511,34 @@ async function fetchImageList(
return res; return res;
} }
async function openSpecificCustomer(id: string) {
await customerFormStore.assignFormData(id);
await fetchImageList(
id,
customerFormData.value.selectedImage || '',
'customer',
);
customerFormState.value.branchIndex = -1;
customerFormState.value.drawerModal = true;
customerFormState.value.editCustomerId = id;
customerFormState.value.dialogType = 'info';
}
async function openSpecificEmployee(id: string) {
await employeeFormStore.assignFormDataEmployee(id);
await fetchImageList(
id,
currentFromDataEmployee.value.selectedImage || '',
'employee',
);
employeeFormState.value.dialogType = 'info';
employeeFormState.value.drawerModal = true;
}
// TODO: When in employee form, if select address same as customer then auto fill // TODO: When in employee form, if select address same as customer then auto fill
watch( watch(
() => employeeFormState.value.formDataEmployeeOwner, () => employeeFormState.value.currentCustomerBranch,
(e) => { (e) => {
if (!e) return; if (!e) return;
if (employeeFormState.value.formDataEmployeeSameAddr) { if (employeeFormState.value.formDataEmployeeSameAddr) {
@ -703,21 +555,21 @@ watch(
watch( watch(
() => employeeFormState.value.formDataEmployeeSameAddr, () => employeeFormState.value.formDataEmployeeSameAddr,
(isSame) => { (isSame) => {
if (!employeeFormState.value.formDataEmployeeOwner) return; if (!employeeFormState.value.currentCustomerBranch) return;
if (isSame) { if (isSame) {
currentFromDataEmployee.value.address = currentFromDataEmployee.value.address =
employeeFormState.value.formDataEmployeeOwner.address; employeeFormState.value.currentCustomerBranch.address;
currentFromDataEmployee.value.addressEN = currentFromDataEmployee.value.addressEN =
employeeFormState.value.formDataEmployeeOwner.addressEN; employeeFormState.value.currentCustomerBranch.addressEN;
currentFromDataEmployee.value.provinceId = currentFromDataEmployee.value.provinceId =
employeeFormState.value.formDataEmployeeOwner.provinceId; employeeFormState.value.currentCustomerBranch.provinceId;
currentFromDataEmployee.value.districtId = currentFromDataEmployee.value.districtId =
employeeFormState.value.formDataEmployeeOwner.districtId; employeeFormState.value.currentCustomerBranch.districtId;
currentFromDataEmployee.value.subDistrictId = currentFromDataEmployee.value.subDistrictId =
employeeFormState.value.formDataEmployeeOwner.subDistrictId; employeeFormState.value.currentCustomerBranch.subDistrictId;
} }
currentFromDataEmployee.value.customerBranchId = currentFromDataEmployee.value.customerBranchId =
employeeFormState.value.formDataEmployeeOwner.id; employeeFormState.value.currentCustomerBranch.id;
}, },
); );
@ -936,26 +788,43 @@ const emptyCreateDialog = ref(false);
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && currentStatus !== 'All'"
unelevated >
class="q-ml-sm" <div
padding="4px" v-if="$q.screen.lt.md"
size="sm" class="q-mt-sm text-weight-medium"
rounded >
@click="refFilter?.showPopup" {{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
id="select-status"
for="select-status"
v-model="currentStatus"
outlined
dense
autocomplete="off"
option-value="value"
option-label="label"
map-options
emit-value
:options="[
{ label: $t('general.all'), value: 'All' },
{ label: $t('status.ACTIVE'), value: 'ACTIVE' },
{ label: $t('status.INACTIVE'), value: 'INACTIVE' },
]"
/> />
</span> </AdvanceSearch>
</template> </template>
</q-input> </q-input>
<div class="row col-md-5" style="white-space: nowrap"> <div class="row col-md-5" style="white-space: nowrap">
<q-select <q-select
v-show="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
ref="refFilter"
id="select-status" id="select-status"
for="select-status" for="select-status"
v-model="currentStatus" v-model="currentStatus"
@ -1540,7 +1409,13 @@ const emptyCreateDialog = ref(false);
customerFormState.branchIndex = 0; customerFormState.branchIndex = 0;
} }
" "
@delete="deleteCustomerById(props.row.id)" @delete="
deleteCustomerById(
props.row.id,
async () =>
await fetchListCustomer(true, $q.screen.xs),
)
"
@change-status=" @change-status="
async () => { async () => {
triggerChangeStatus( triggerChangeStatus(
@ -1588,7 +1463,23 @@ const emptyCreateDialog = ref(false);
" "
@delete=" @delete="
(item: any) => { (item: any) => {
deleteEmployeeById({ id: item.id }); deleteEmployeeById({
id: item.id,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId:
customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
} }
" "
@toggle-status=" @toggle-status="
@ -1763,7 +1654,16 @@ const emptyCreateDialog = ref(false);
customerFormState.branchIndex = 0; customerFormState.branchIndex = 0;
} }
" "
@delete="deleteCustomerById(props.row.id)" @delete="
deleteCustomerById(
props.row.id,
async () =>
await fetchListCustomer(
true,
$q.screen.xs,
),
)
"
@change-status=" @change-status="
triggerChangeStatus( triggerChangeStatus(
props.row.id, props.row.id,
@ -1898,7 +1798,23 @@ const emptyCreateDialog = ref(false);
@edit="(item: any) => editEmployeeFormPersonal(item.id)" @edit="(item: any) => editEmployeeFormPersonal(item.id)"
@delete=" @delete="
(item: any) => { (item: any) => {
deleteEmployeeById({ id: item.id }); deleteEmployeeById({
id: item.id,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId:
customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
} }
" "
@toggle-status=" @toggle-status="
@ -2036,7 +1952,7 @@ const emptyCreateDialog = ref(false);
async (currentBranch) => { async (currentBranch) => {
createEmployeeForm(); createEmployeeForm();
await nextTick(); await nextTick();
employeeFormState.formDataEmployeeOwner = { ...currentBranch }; employeeFormState.currentBranchId = currentBranch.id;
} }
" "
v-model:branch="branch" v-model:branch="branch"
@ -2150,7 +2066,16 @@ const emptyCreateDialog = ref(false);
" "
:title=" :title="
customerFormData.customerType === 'PERS' customerFormData.customerType === 'PERS'
? `${customerFormData.customerBranch[0]?.firstName} ${customerFormData.customerBranch[0]?.lastName}` ? setPrefixName(
{
namePrefix: customerFormData.customerBranch[0]?.namePrefix,
firstName: customerFormData.customerBranch[0]?.firstName,
lastName: customerFormData.customerBranch[0]?.lastName,
firstNameEN: customerFormData.customerBranch[0]?.firstNameEN,
lastNameEN: customerFormData.customerBranch[0]?.lastNameEN,
},
{ locale },
)
: customerFormData.customerBranch[0]?.registerName : customerFormData.customerBranch[0]?.registerName
" "
:caption=" :caption="
@ -2259,13 +2184,16 @@ const emptyCreateDialog = ref(false);
id="form-basic-info-customer" id="form-basic-info-customer"
:onCreate="customerFormState.dialogType === 'create'" :onCreate="customerFormState.dialogType === 'create'"
@edit=" @edit="
(customerFormState.dialogType = 'edit'), ((customerFormState.dialogType = 'edit'),
(customerFormState.readonly = false) (customerFormState.readonly = false))
" "
@cancel="() => customerFormUndo(false)" @cancel="() => customerFormUndo(false)"
@delete=" @delete="
customerFormState.editCustomerId && customerFormState.editCustomerId &&
deleteCustomerById(customerFormState.editCustomerId) deleteCustomerById(
customerFormState.editCustomerId,
async () => await fetchListCustomer(true, $q.screen.xs),
)
" "
:customer-type="customerFormData.customerType" :customer-type="customerFormData.customerType"
v-model:registered-branch-id="customerFormData.registeredBranchId" v-model:registered-branch-id="customerFormData.registeredBranchId"
@ -2435,7 +2363,11 @@ const emptyCreateDialog = ref(false);
if (!customerFormState.editCustomerId) return; if (!customerFormState.editCustomerId) return;
if (idx === 0) { if (idx === 0) {
deleteCustomerById(customerFormState.editCustomerId); deleteCustomerById(
customerFormState.editCustomerId,
async () =>
await fetchListCustomer(true, $q.screen.xs),
);
return; return;
} }
if (!!customerFormData.customerBranch?.[idx].id) { if (!!customerFormData.customerBranch?.[idx].id) {
@ -2591,6 +2523,20 @@ const emptyCreateDialog = ref(false);
" "
:toggleTitle="$t('status.title')" :toggleTitle="$t('status.title')"
hideFade hideFade
:title="
currentFromDataEmployee
? setPrefixName(
{
namePrefix: currentFromDataEmployee.namePrefix,
firstName: currentFromDataEmployee.firstName,
lastName: currentFromDataEmployee.lastName,
firstNameEN: currentFromDataEmployee.firstNameEN,
lastNameEN: currentFromDataEmployee.lastNameEN,
},
{ locale },
)
: '-'
"
@view=" @view="
() => { () => {
employeeFormState.imageDialog = true; employeeFormState.imageDialog = true;
@ -2870,12 +2816,30 @@ const emptyCreateDialog = ref(false);
id="btn-info-basic-delete" id="btn-info-basic-delete"
icon-only icon-only
@click=" @click="
() => deleteEmployeeById({ id: currentFromDataEmployee.id }) () =>
deleteEmployeeById({
id: currentFromDataEmployee.id,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId:
customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
})
" "
type="button" type="button"
/> />
</div> </div>
</div> </div>
<BasicInformation <BasicInformation
no-action no-action
id="form-information" id="form-information"
@ -2888,11 +2852,13 @@ const emptyCreateDialog = ref(false);
title="form.field.basicInformation" title="form.field.basicInformation"
:readonly="!employeeFormState.isEmployeeEdit" :readonly="!employeeFormState.isEmployeeEdit"
:employee-owner-option="employeeStore.ownerOption || []" :employee-owner-option="employeeStore.ownerOption || []"
v-model:customer-branch="employeeFormState.formDataEmployeeOwner" v-model:customer-branch-id="employeeFormState.currentBranchId"
v-model:current-customer-branch="
employeeFormState.currentCustomerBranch
"
v-model:employee-id="employeeFormState.currentEmployeeCode" v-model:employee-id="employeeFormState.currentEmployeeCode"
v-model:nrc-no="currentFromDataEmployee.nrcNo" v-model:nrc-no="currentFromDataEmployee.nrcNo"
v-model:code="currentFromDataEmployee.code" v-model:code="currentFromDataEmployee.code"
@filter-owner-branch="employeeFormStore.employeeFilterOwnerBranch"
class="q-mb-xl" class="q-mb-xl"
/> />
<FormPerson <FormPerson
@ -2918,6 +2884,7 @@ const emptyCreateDialog = ref(false);
class="q-mb-xl" class="q-mb-xl"
/> />
<AddressForm <AddressForm
disabledRule
id="form-personal-address" id="form-personal-address"
prefix-id="form-employee" prefix-id="form-employee"
:readonly="!employeeFormState.isEmployeeEdit" :readonly="!employeeFormState.isEmployeeEdit"
@ -2940,7 +2907,7 @@ const emptyCreateDialog = ref(false);
class="q-mb-xl" class="q-mb-xl"
/> />
<div class="row q-mb-md" id="drawer-info-file-upload"> <div class="row q-mb-md" id="form-info-file-upload">
<div class="col-12 q-pb-sm text-weight-bold text-body1"> <div class="col-12 q-pb-sm text-weight-bold text-body1">
<q-icon <q-icon
flat flat
@ -3448,7 +3415,24 @@ const emptyCreateDialog = ref(false);
@click.stop=" @click.stop="
() => { () => {
employeeFormState.currentIndexPassport = index; employeeFormState.currentIndexPassport = index;
deleteEmployeeById({ type: 'passport', index }); deleteEmployeeById({
type: 'passport',
index,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId:
customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
} }
" "
type="button" type="button"
@ -3599,7 +3583,24 @@ const emptyCreateDialog = ref(false);
@click.stop=" @click.stop="
() => { () => {
employeeFormState.currentIndexVisa = index; employeeFormState.currentIndexVisa = index;
deleteEmployeeById({ type: 'visa', index }); deleteEmployeeById({
type: 'visa',
index,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId:
customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
} }
" "
type="button" type="button"
@ -3671,7 +3672,23 @@ const emptyCreateDialog = ref(false);
@delete=" @delete="
(index) => { (index) => {
employeeFormState.currentIndexCheckup = index; employeeFormState.currentIndexCheckup = index;
deleteEmployeeById({ type: 'healthCheck', index }); deleteEmployeeById({
type: 'healthCheck',
index,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId: customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
} }
" "
@save=" @save="
@ -3757,7 +3774,23 @@ const emptyCreateDialog = ref(false);
@delete=" @delete="
(index) => { (index) => {
employeeFormState.currentIndexWorkHistory = index; employeeFormState.currentIndexWorkHistory = index;
deleteEmployeeById({ type: 'work', index }); deleteEmployeeById({
type: 'work',
index,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId: customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
} }
" "
@save=" @save="
@ -4104,7 +4137,17 @@ const emptyCreateDialog = ref(false);
" "
:title=" :title="
customerFormData.customerType === 'PERS' customerFormData.customerType === 'PERS'
? `${customerFormData.customerBranch[0]?.firstName} ${customerFormData.customerBranch[0]?.lastName}` ? setPrefixName(
{
namePrefix: customerFormData.customerBranch[0]?.namePrefix,
firstName: customerFormData.customerBranch[0]?.firstName,
lastName: customerFormData.customerBranch[0]?.lastName,
firstNameEN:
customerFormData.customerBranch[0]?.firstNameEN,
lastNameEN: customerFormData.customerBranch[0]?.lastNameEN,
},
{ locale },
)
: customerFormData.customerBranch[0]?.registerName : customerFormData.customerBranch[0]?.registerName
" "
:caption=" :caption="
@ -4218,13 +4261,16 @@ const emptyCreateDialog = ref(false);
id="form-basic-info-customer" id="form-basic-info-customer"
:onCreate="customerFormState.dialogType === 'create'" :onCreate="customerFormState.dialogType === 'create'"
@edit=" @edit="
(customerFormState.dialogType = 'edit'), ((customerFormState.dialogType = 'edit'),
(customerFormState.readonly = false) (customerFormState.readonly = false))
" "
@cancel="() => customerFormUndo(false)" @cancel="() => customerFormUndo(false)"
@delete=" @delete="
customerFormState.editCustomerId && customerFormState.editCustomerId &&
deleteCustomerById(customerFormState.editCustomerId) deleteCustomerById(
customerFormState.editCustomerId,
async () => await fetchListCustomer(true, $q.screen.xs),
)
" "
:customer-type="customerFormData.customerType" :customer-type="customerFormData.customerType"
v-model:registered-branch-id="customerFormData.registeredBranchId" v-model:registered-branch-id="customerFormData.registeredBranchId"
@ -4524,9 +4570,16 @@ const emptyCreateDialog = ref(false);
fallback-cover="/images/employee-banner.png" fallback-cover="/images/employee-banner.png"
:title=" :title="
employeeFormState.currentEmployee employeeFormState.currentEmployee
? $i18n.locale === 'eng' ? setPrefixName(
? `${employeeFormState.currentEmployee.firstNameEN} ${employeeFormState.currentEmployee.lastNameEN}` {
: `${employeeFormState.currentEmployee.firstName} ${employeeFormState.currentEmployee.lastName}` namePrefix: employeeFormState.currentEmployee.namePrefix,
firstName: employeeFormState.currentEmployee.firstName,
lastName: employeeFormState.currentEmployee.lastName,
firstNameEN: employeeFormState.currentEmployee.firstNameEN,
lastNameEN: employeeFormState.currentEmployee.lastNameEN,
},
{ locale },
)
: '-' : '-'
" "
:caption="currentFromDataEmployee.code" :caption="currentFromDataEmployee.code"
@ -4821,7 +4874,23 @@ const emptyCreateDialog = ref(false);
icon-only icon-only
@click=" @click="
() => () =>
deleteEmployeeById({ id: currentFromDataEmployee.id }) deleteEmployeeById({
id: currentFromDataEmployee.id,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId:
customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
})
" "
type="button" type="button"
/> />
@ -4835,16 +4904,13 @@ const emptyCreateDialog = ref(false);
outlined outlined
title="form.field.basicInformation" title="form.field.basicInformation"
:readonly="!employeeFormState.isEmployeeEdit" :readonly="!employeeFormState.isEmployeeEdit"
:employee-owner-option="employeeStore.ownerOption" v-model:customer-branch-id="employeeFormState.currentBranchId"
v-model:customer-branch=" v-model:current-customer-branch="
employeeFormState.formDataEmployeeOwner employeeFormState.currentCustomerBranch
" "
v-model:employee-id="employeeFormState.currentEmployeeCode" v-model:employee-id="employeeFormState.currentEmployeeCode"
v-model:nrc-no="currentFromDataEmployee.nrcNo" v-model:nrc-no="currentFromDataEmployee.nrcNo"
v-model:code="currentFromDataEmployee.code" v-model:code="currentFromDataEmployee.code"
@filter-owner-branch="
employeeFormStore.employeeFilterOwnerBranch
"
class="q-mb-xl" class="q-mb-xl"
/> />
<FormPerson <FormPerson
@ -4871,6 +4937,7 @@ const emptyCreateDialog = ref(false);
<AddressForm <AddressForm
id="drawer-form-personal-address" id="drawer-form-personal-address"
employee employee
disabledRule
v-model:address="currentFromDataEmployee.address" v-model:address="currentFromDataEmployee.address"
v-model:address-en="currentFromDataEmployee.addressEN" v-model:address-en="currentFromDataEmployee.addressEN"
v-model:moo="currentFromDataEmployee.moo" v-model:moo="currentFromDataEmployee.moo"
@ -5450,6 +5517,20 @@ const emptyCreateDialog = ref(false);
deleteEmployeeById({ deleteEmployeeById({
type: 'passport', type: 'passport',
index, index,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId:
customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
}); });
} }
" "
@ -5621,7 +5702,24 @@ const emptyCreateDialog = ref(false);
icon-only icon-only
@click.stop=" @click.stop="
() => { () => {
deleteEmployeeById({ type: 'visa', index }); deleteEmployeeById({
type: 'visa',
index,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId:
customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
} }
" "
type="button" type="button"
@ -5742,7 +5840,24 @@ const emptyCreateDialog = ref(false);
@delete=" @delete="
(index) => { (index) => {
employeeFormState.currentIndexCheckup = index; employeeFormState.currentIndexCheckup = index;
deleteEmployeeById({ type: 'healthCheck', index }); deleteEmployeeById({
type: 'healthCheck',
index,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId:
customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
} }
" "
/> />
@ -5812,7 +5927,24 @@ const emptyCreateDialog = ref(false);
@delete=" @delete="
(index) => { (index) => {
employeeFormState.currentIndexWorkHistory = index; employeeFormState.currentIndexWorkHistory = index;
deleteEmployeeById({ type: 'work', index }); deleteEmployeeById({
type: 'work',
index,
fetch: async () =>
await fetchListEmployee(
currentTab === 'employer'
? {
page: 1,
pageSize: 999,
customerId:
customerFormState.currentCustomerId,
}
: {
fetchStats: true,
mobileFetch: $q.screen.xs,
},
),
});
} }
" "
@save=" @save="

View file

@ -1,22 +1,50 @@
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useI18n } from 'vue-i18n';
import { import {
CustomerBranch,
CustomerBranchCreate, CustomerBranchCreate,
CustomerCreate, CustomerCreate,
CustomerType, CustomerType,
} from 'stores/customer/types'; } from 'stores/customer/types';
import { Employee, EmployeeCreate } from 'stores/employee/types'; import { Employee, EmployeeCreate } from 'stores/employee/types';
import { dialog } from 'stores/utils';
import useMyBranch from 'stores/my-branch'; import useMyBranch from 'stores/my-branch';
import useCustomerStore from 'stores/customer'; import useCustomerStore from 'stores/customer';
import useEmployeeStore from 'stores/employee'; import useEmployeeStore from 'stores/employee';
import useFlowStore from 'stores/flow'; import useFlowStore from 'stores/flow';
import useMyBranchStore from 'stores/my-branch';
import { baseUrl } from 'src/stores/utils'; import { baseUrl } from 'src/stores/utils';
import { getRole, getUserId } from 'src/services/keycloak';
import { useRoute } from 'vue-router';
export const useCustomerForm = defineStore('form-customer', () => { export const useCustomerForm = defineStore('form-customer', () => {
const customerStore = useCustomerStore(); const customerStore = useCustomerStore();
const { t } = useI18n();
const flowStore = useFlowStore();
const userBranchStore = useMyBranchStore();
const registerAbleBranchOption = ref<{ id: string; name: string }[]>();
const tabFieldRequired = ref<{
[key: string]: (keyof CustomerBranchCreate)[];
}>({
main: [],
business: ['businessType', 'jobPosition'],
address: [
'address',
'addressEN',
'provinceId',
'districtId',
'subDistrictId',
],
contact: [],
});
const defaultFormData: CustomerCreate = { const defaultFormData: CustomerCreate = {
// code: '', // code: '',
// namePrefix: '', // namePrefix: '',
@ -360,7 +388,118 @@ export const useCustomerForm = defineStore('form-customer', () => {
} }
} }
async function fetchListOfOptionBranch() {
if (registerAbleBranchOption.value) return;
const uid = getUserId();
const role = getRole();
if (!uid) return; // should not possible as the system require login to be able to access resource.
if (role?.includes('system')) {
const result = await userBranchStore.fetchListOptionBranch();
if (result && result.total > 0)
registerAbleBranchOption.value = result.result;
} else {
const result = await userBranchStore.fetchListMyBranch(uid);
if (result && result.total > 0)
registerAbleBranchOption.value = result.result;
}
// TODO: Assign (first) branch of the user as register branch of the data
}
function customerFormUndo(close = true) {
if (isFormDataDifferent()) {
return customerConfirmUnsave(close);
}
resetForm();
state.value.readonly = true;
}
function customerConfirmUnsave(close = true) {
dialog({
color: 'warning',
icon: 'mdi-alert',
title: t('form.warning.title'),
actionText: t('general.ok'),
persistent: true,
message: t('form.warning.unsave'),
action: () => {
resetForm();
state.value.readonly = true;
if (!state.value.drawerModal) {
state.value.dialogModal = !close;
} else {
state.value.drawerModal = !close;
}
},
cancel: () => {},
});
}
function deleteCustomerById(
id: string,
fetch?: (...args: unknown[]) => unknown,
) {
dialog({
color: 'negative',
icon: 'mdi-alert',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
persistent: true,
message: t('dialog.message.confirmDelete'),
action: async () => {
await customerStore.deleteById(id);
await fetch();
state.value.dialogModal = false;
flowStore.rotate();
},
cancel: () => {},
});
}
function validateTabField<T = CustomerBranchCreate>(
value: T,
fieldRequired: { [key: string]: (keyof T)[] },
) {
const list: string[] = [];
for (const tab in fieldRequired) {
for (const field of fieldRequired[tab]) {
if (!value[field] && !list.includes(tab)) list.push(tab);
}
}
return list;
}
async function deleteCustomerBranchById(id: string) {
return await new Promise((resolve) => {
dialog({
color: 'negative',
icon: 'mdi-alert',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
persistent: true,
message: t('dialog.message.confirmDelete'),
action: async () => {
await customerStore.deleteBranchById(id);
flowStore.rotate();
resolve(true);
},
cancel: () => {
resolve(false);
},
});
});
}
return { return {
tabFieldRequired,
registerAbleBranchOption,
state, state,
resetFormData, resetFormData,
currentFormData, currentFormData,
@ -370,13 +509,18 @@ export const useCustomerForm = defineStore('form-customer', () => {
submitFormCustomer, submitFormCustomer,
addCurrentCustomerBranch, addCurrentCustomerBranch,
deleteAttachment, deleteAttachment,
fetchListOfOptionBranch,
customerFormUndo,
customerConfirmUnsave,
deleteCustomerById,
validateTabField,
deleteCustomerBranchById,
}; };
}); });
export const useCustomerBranchForm = defineStore('form-customer-branch', () => { export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
const customerStore = useCustomerStore(); const customerStore = useCustomerStore();
const customerFormStore = useCustomerForm(); const customerFormStore = useCustomerForm();
const defaultFormData: CustomerBranchCreate & { const defaultFormData: CustomerBranchCreate & {
id?: string; id?: string;
codeCustomer?: string; codeCustomer?: string;
@ -581,11 +725,23 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
}); });
export const useEmployeeForm = defineStore('form-employee', () => { export const useEmployeeForm = defineStore('form-employee', () => {
const { t } = useI18n();
const customerStore = useCustomerStore(); const customerStore = useCustomerStore();
const employeeStore = useEmployeeStore(); const employeeStore = useEmployeeStore();
const flowStore = useFlowStore(); const flowStore = useFlowStore();
const branchStore = useMyBranch(); const branchStore = useMyBranch();
const route = useRoute();
const refreshImageState = ref(false);
const onCreateImageList = ref<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>({ selectedImage: '', list: [] });
const statusEmployeeCreate = ref<boolean>(false);
const state = ref<{ const state = ref<{
dialogType: 'info' | 'create' | 'edit'; dialogType: 'info' | 'create' | 'edit';
imageDialog: boolean; imageDialog: boolean;
@ -594,6 +750,8 @@ export const useEmployeeForm = defineStore('form-employee', () => {
drawerModal: boolean; drawerModal: boolean;
isImageEdit: boolean; isImageEdit: boolean;
currentBranchId: string;
currentCustomerBranch?: CustomerBranch;
currentEmployeeCode: string; currentEmployeeCode: string;
currentEmployee: Employee | null; currentEmployee: Employee | null;
currentIndexPassport: number; currentIndexPassport: number;
@ -627,6 +785,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
| undefined; | undefined;
ocr: boolean; ocr: boolean;
}>({ }>({
currentBranchId: '',
isImageEdit: false, isImageEdit: false,
currentIndexPassport: -1, currentIndexPassport: -1,
currentIndexVisa: -1, currentIndexVisa: -1,
@ -1089,6 +1248,8 @@ export const useEmployeeForm = defineStore('form-employee', () => {
selectedImage: string; selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[]; list: { url: string; imgFile: File | null; name: string }[];
}) { }) {
let employeeId: string | undefined = undefined;
currentFromDataEmployee.value.firstName = currentFromDataEmployee.value.firstName =
currentFromDataEmployee.value.firstName.trim(); currentFromDataEmployee.value.firstName.trim();
currentFromDataEmployee.value.middleName = currentFromDataEmployee.value.middleName =
@ -1107,7 +1268,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
const res = await employeeStore.create( const res = await employeeStore.create(
{ {
...currentFromDataEmployee.value, ...currentFromDataEmployee.value,
customerBranchId: state.value.formDataEmployeeOwner?.id || '', customerBranchId: state.value.currentBranchId || '',
employeeWork: [], employeeWork: [],
employeeCheckup: [], employeeCheckup: [],
@ -1117,6 +1278,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
); );
if (res) { if (res) {
employeeId = res.id;
await assignFormDataEmployee(res.id); await assignFormDataEmployee(res.id);
currentFromDataEmployee.value.id = res.id; currentFromDataEmployee.value.id = res.id;
state.value.statusSavePersonal = true; state.value.statusSavePersonal = true;
@ -1138,10 +1300,12 @@ export const useEmployeeForm = defineStore('form-employee', () => {
}, },
); );
if (res) { if (res) {
employeeId = res.id;
await assignFormDataEmployee(res.id); await assignFormDataEmployee(res.id);
state.value.statusSavePersonal = true; state.value.statusSavePersonal = true;
} }
} }
return employeeId;
} }
async function assignFormDataEmployee(id?: string) { async function assignFormDataEmployee(id?: string) {
@ -1183,7 +1347,19 @@ export const useEmployeeForm = defineStore('form-employee', () => {
employeePassport: structuredClone( employeePassport: structuredClone(
payload.employeePassport?.length === 0 payload.employeePassport?.length === 0
? state.value.dialogModal ? state.value.dialogModal
? defaultFormData.employeePassport ? defaultFormData.employeePassport.map((v) => ({
...v,
namePrefix: payload.namePrefix,
firstName: payload.firstName,
firstNameEN: payload.firstNameEN,
middleName: payload.middleName,
middleNameEN: payload.middleNameEN,
lastName: payload.lastName,
lastNameEN: payload.lastNameEN,
gender: payload.gender,
nationality: payload.nationality,
birthDate: payload.dateOfBirth,
}))
: [] : []
: payload.employeePassport, : payload.employeePassport,
), ),
@ -1270,6 +1446,8 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state.value.currentIndexVisa = -1; state.value.currentIndexVisa = -1;
} }
state.value.currentBranchId = payload.customerBranchId;
const foundBranch = await customerStore.fetchListCustomerBranchById( const foundBranch = await customerStore.fetchListCustomerBranchById(
payload.customerBranchId, payload.customerBranchId,
); );
@ -1325,17 +1503,17 @@ export const useEmployeeForm = defineStore('form-employee', () => {
issueDate: new Date(), issueDate: new Date(),
type: '', type: '',
expireDate: new Date(), expireDate: new Date(),
birthDate: new Date(), birthDate: currentFromDataEmployee.value.dateOfBirth,
workerStatus: '', workerStatus: '',
nationality: '', nationality: currentFromDataEmployee.value.nationality,
gender: '', gender: currentFromDataEmployee.value.gender,
lastNameEN: '', lastNameEN: currentFromDataEmployee.value.lastNameEN,
lastName: '', lastName: currentFromDataEmployee.value.lastName,
middleNameEN: '', middleNameEN: currentFromDataEmployee.value.middleNameEN,
middleName: '', middleName: currentFromDataEmployee.value.middleName,
firstNameEN: '', firstNameEN: currentFromDataEmployee.value.firstNameEN,
firstName: '', firstName: currentFromDataEmployee.value.firstName,
namePrefix: '', namePrefix: currentFromDataEmployee.value.namePrefix,
number: '', number: '',
}); });
@ -1396,7 +1574,88 @@ export const useEmployeeForm = defineStore('form-employee', () => {
(currentFromDataEmployee.value.employeeWork?.length || 0) - 1; (currentFromDataEmployee.value.employeeWork?.length || 0) - 1;
} }
function employeeFormUndo(close = true) {
if (isFormDataDifferent()) {
return employeeConfirmUnsave(close);
}
resetFormDataEmployee();
state.value.editReadonly = true;
}
function employeeConfirmUnsave(close = true) {
dialog({
color: 'warning',
icon: 'mdi-alert',
title: t('form.warning.title'),
actionText: t('general.ok'),
persistent: true,
message: t('form.warning.unsave'),
action: () => {
resetFormDataEmployee();
onCreateImageList.value = { selectedImage: '', list: [] };
state.value.editReadonly = true;
state.value.dialogModal = !close;
state.value.drawerModal = !close;
},
cancel: () => {},
});
}
async function deleteEmployeeById(opts: {
id?: string;
type?: 'passport' | 'visa' | 'healthCheck' | 'work';
index?: number;
fetch?: (...args: unknown[]) => unknown;
removeArray?: (...args: unknown[]) => unknown;
}) {
dialog({
color: 'negative',
icon: 'mdi-alert',
title: t('dialog.title.confirmDelete'),
actionText: t('general.delete'),
persistent: true,
message: t('dialog.message.confirmDelete'),
action: async () => {
if (opts.type === 'passport' && opts.index !== undefined) {
await deletePassport(opts.index);
}
if (opts.type === 'visa' && opts.index !== undefined) {
await deleteVisa(opts.index);
}
if (opts.type === 'healthCheck' && opts.index !== undefined) {
await deleteHealthCheck(opts.index);
}
if (opts.type === 'work' && opts.index !== undefined) {
await deleteWorkHistory(opts.index);
} else {
if (!!opts.id) {
const result = await employeeStore.deleteById(opts.id);
if (result) {
state.value.drawerModal = false;
state.value.dialogModal = false;
}
}
}
if (route.name !== 'CustomerBranchManagement') {
await opts.fetch?.();
flowStore.rotate();
}
opts.removeArray?.();
},
cancel: () => {},
});
}
return { return {
refreshImageState,
statusEmployeeCreate,
onCreateImageList,
state, state,
currentFromDataEmployee, currentFromDataEmployee,
resetEmployeeData, resetEmployeeData,
@ -1423,5 +1682,9 @@ export const useEmployeeForm = defineStore('form-employee', () => {
employeeFilterOwnerBranch, employeeFilterOwnerBranch,
isFormDataDifferent, isFormDataDifferent,
employeeFormUndo,
employeeConfirmUnsave,
deleteEmployeeById,
}; };
}); });

View file

@ -60,6 +60,7 @@ async function addStep() {
flowData.value.step.push({ flowData.value.step.push({
responsibleInstitution: [], responsibleInstitution: [],
responsiblePersonId: [], responsiblePersonId: [],
responsibleGroup: [],
value: [], value: [],
detail: '', detail: '',
name: '', name: '',
@ -166,6 +167,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
id="flow-form-dialog" id="flow-form-dialog"
> >
<FormFlow <FormFlow
v-model:user-in-table="userInTable"
v-model:flow-data="flowData" v-model:flow-data="flowData"
v-model:register-branch-id="registerBranchId" v-model:register-branch-id="registerBranchId"
@trigger-properties="triggerPropertiesDialog" @trigger-properties="triggerPropertiesDialog"

View file

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue'; import { onMounted, reactive, ref, watch } from 'vue';
import { QSelect, QTableProps } from 'quasar'; import { QTableProps } from 'quasar';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -22,6 +22,7 @@ import NoData from 'src/components/NoData.vue';
import KebabAction from 'src/components/shared/KebabAction.vue'; import KebabAction from 'src/components/shared/KebabAction.vue';
import PaginationPageSize from 'src/components/PaginationPageSize.vue'; import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { t } = useI18n(); const { t } = useI18n();
const workflowStore = useWorkflowTemplate(); const workflowStore = useWorkflowTemplate();
@ -45,6 +46,7 @@ const pageState = reactive({
addModal: false, addModal: false,
viewDrawer: false, viewDrawer: false,
isDrawerEdit: true, isDrawerEdit: true,
searchDate: [],
}); });
const fieldSelected = ref<('order' | 'name' | 'step')[]>([ const fieldSelected = ref<('order' | 'name' | 'step')[]>([
@ -68,7 +70,6 @@ const fieldSelectedOption = ref<{ label: string; value: string }[]>([
}, },
]); ]);
const refFilter = ref<InstanceType<typeof QSelect>>();
const currWorkflowData = ref<WorkflowTemplate>(); const currWorkflowData = ref<WorkflowTemplate>();
const formDataWorkflow = ref<WorkflowTemplatePayload>({ const formDataWorkflow = ref<WorkflowTemplatePayload>({
status: 'CREATED', status: 'CREATED',
@ -102,6 +103,7 @@ const columns = [
function triggerDialog(type: 'add' | 'edit' | 'view') { function triggerDialog(type: 'add' | 'edit' | 'view') {
if (type === 'add') { if (type === 'add') {
registeredBranchId.value = ''; registeredBranchId.value = '';
userInTable.value = [];
formDataWorkflow.value = { formDataWorkflow.value = {
status: 'CREATED', status: 'CREATED',
name: '', name: '',
@ -206,7 +208,7 @@ async function submit() {
...formDataWorkflow.value, ...formDataWorkflow.value,
}); });
} else { } else {
await workflowStore.creatWorkflowTemplate({ await workflowStore.createWorkflowTemplate({
registeredBranchId: registeredBranchId.value, registeredBranchId: registeredBranchId.value,
...formDataWorkflow.value, ...formDataWorkflow.value,
}); });
@ -222,7 +224,11 @@ function assignFormData(workflowData: WorkflowTemplate) {
status: workflowData.status, status: workflowData.status,
name: workflowData.name, name: workflowData.name,
step: workflowData.step.map((s, i) => { step: workflowData.step.map((s, i) => {
userInTable.value[i] = { name: s.name, responsiblePerson: [] }; userInTable.value[i] = {
name: s.name,
responsiblePerson: [],
responsibleGroup: [],
};
s.responsiblePerson.forEach((p) => { s.responsiblePerson.forEach((p) => {
userInTable.value[i].responsiblePerson.push({ userInTable.value[i].responsiblePerson.push({
id: p.user.id, id: p.user.id,
@ -236,12 +242,16 @@ function assignFormData(workflowData: WorkflowTemplate) {
code: p.user.code, code: p.user.code,
}); });
}); });
s.responsibleGroup.forEach((g) => {
userInTable.value[i].responsibleGroup.push(g);
});
return { return {
id: s.id, id: s.id,
name: s.name, name: s.name,
detail: s.detail, detail: s.detail,
messengerByArea: s.messengerByArea || false, messengerByArea: s.messengerByArea || false,
value: s.value.length > 0 ? JSON.parse(JSON.stringify(s.value)) : [], value: s.value.length > 0 ? JSON.parse(JSON.stringify(s.value)) : [],
responsibleGroup: s.responsibleGroup.map((g) => g),
responsiblePersonId: s.responsiblePerson.map((p) => p.userId), responsiblePersonId: s.responsiblePerson.map((p) => p.userId),
responsibleInstitution: JSON.parse( responsibleInstitution: JSON.parse(
JSON.stringify(s.responsibleInstitution), JSON.stringify(s.responsibleInstitution),
@ -282,6 +292,8 @@ async function fetchWorkflowList(mobileFetch?: boolean) {
: statusFilter.value === 'statusACTIVE' : statusFilter.value === 'statusACTIVE'
? 'ACTIVE' ? 'ACTIVE'
: 'INACTIVE', : 'INACTIVE',
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
}); });
if (res) { if (res) {
workflowData.value = workflowData.value =
@ -311,11 +323,14 @@ watch(
fetchWorkflowList(); fetchWorkflowList();
}, },
); );
watch([() => pageState.inputSearch, workflowPageSize], () => { watch(
[() => pageState.inputSearch, workflowPageSize, () => pageState.searchDate],
() => {
workflowData.value = []; workflowData.value = [];
workflowPage.value = 1; workflowPage.value = 1;
fetchWorkflowList(); fetchWorkflowList();
}); },
);
</script> </script>
<template> <template>
<FloatingActionButton <FloatingActionButton
@ -392,26 +407,44 @@ watch([() => pageState.inputSearch, workflowPageSize], () => {
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="pageState.searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && statusFilter !== 'all'"
unelevated >
class="q-ml-sm" <div
padding="4px" v-if="$q.screen.lt.md"
size="sm" class="q-mt-sm text-weight-medium"
rounded >
@click="refFilter?.showPopup" {{ $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
: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> </template>
</q-input> </q-input>
<div class="row col-md-5" style="white-space: nowrap"> <div class="row col-md-5" style="white-space: nowrap">
<q-select <q-select
v-show="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
ref="refFilter"
v-model="statusFilter" v-model="statusFilter"
outlined outlined
dense dense
@ -509,12 +542,12 @@ watch([() => pageState.inputSearch, workflowPageSize], () => {
class="col surface-2 flex items-center justify-center" class="col surface-2 flex items-center justify-center"
> >
<NoData <NoData
v-if="pageState.total !== 0" v-if="pageState.total !== 0 || pageState.searchDate.length > 0"
:not-found="!!pageState.inputSearch" :not-found="!!pageState.inputSearch"
/> />
<CreateButton <CreateButton
v-if="pageState.total === 0" v-if="pageState.total === 0 && pageState.searchDate.length === 0"
@click="triggerDialog('add')" @click="triggerDialog('add')"
label="general.add" label="general.add"
:i18n-args="{ text: $t('flow.title') }" :i18n-args="{ text: $t('flow.title') }"

View file

@ -3,7 +3,7 @@ import { nextTick, ref, watch, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { QSelect, useQuasar, type QTableProps } from 'quasar'; import { useQuasar, type QTableProps } from 'quasar';
import DialogProperties from 'src/components/dialog/DialogProperties.vue'; import DialogProperties from 'src/components/dialog/DialogProperties.vue';
import ProductCardComponent from 'components/04_product-service/ProductCardComponent.vue'; import ProductCardComponent from 'components/04_product-service/ProductCardComponent.vue';
@ -33,6 +33,7 @@ import {
SaveButton, SaveButton,
UndoButton, UndoButton,
ToggleButton, ToggleButton,
ImportButton,
} from 'components/button'; } from 'components/button';
import TableProduct from 'src/components/04_product-service/TableProduct.vue'; import TableProduct from 'src/components/04_product-service/TableProduct.vue';
import PaginationPageSize from 'src/components/PaginationPageSize.vue'; import PaginationPageSize from 'src/components/PaginationPageSize.vue';
@ -40,7 +41,7 @@ import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import useFlowStore from 'stores/flow'; import useFlowStore from 'stores/flow';
import { dateFormat } from 'src/utils/datetime'; import { dateFormat } from 'src/utils/datetime';
import { formatNumberDecimal, isRoleInclude } from 'stores/utils'; import { formatNumberDecimal, isRoleInclude, notify } from 'stores/utils';
const { getWorkflowTemplate } = useWorkflowTemplate(); const { getWorkflowTemplate } = useWorkflowTemplate();
import { Status } from 'stores/types'; import { Status } from 'stores/types';
@ -67,6 +68,8 @@ import {
} from 'src/stores/workflow-template/types'; } from 'src/stores/workflow-template/types';
import { useWorkflowTemplate } from 'src/stores/workflow-template'; import { useWorkflowTemplate } from 'src/stores/workflow-template';
import { deepEquals } from 'src/utils/arr'; import { deepEquals } from 'src/utils/arr';
import { toRaw } from 'vue';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const flowStore = useFlowStore(); const flowStore = useFlowStore();
const navigatorStore = useNavigator(); const navigatorStore = useNavigator();
@ -96,10 +99,13 @@ const {
createWork, createWork,
editWork, editWork,
deleteWork, deleteWork,
importProduct,
} = productServiceStore; } = productServiceStore;
const { workNameItems } = storeToRefs(productServiceStore); const { workNameItems } = storeToRefs(productServiceStore);
const allStat = ref<{ mode: string; count: number }[]>([]); const allStat = ref<{ mode: string; count: number }[]>([]);
const stat = ref< const stat = ref<
{ {
icon: string; icon: string;
@ -161,8 +167,6 @@ const splitterModel = computed(() =>
$q.screen.lt.md ? (productMode.value !== 'group' ? 0 : 100) : 25, $q.screen.lt.md ? (productMode.value !== 'group' ? 0 : 100) : 25,
); );
const refFilterGroup = ref<InstanceType<typeof QSelect>>();
const refFilterProductService = ref<InstanceType<typeof QSelect>>();
const holdDialog = ref(false); const holdDialog = ref(false);
const imageDialog = ref(false); const imageDialog = ref(false);
const currentNode = ref<ProductGroup & { type: string }>(); const currentNode = ref<ProductGroup & { type: string }>();
@ -520,6 +524,7 @@ const currentStatusGroupType = ref<Status>('CREATED');
const currentIdGroupType = ref(''); const currentIdGroupType = ref('');
const currentStatus = ref<Status | 'All'>('All'); const currentStatus = ref<Status | 'All'>('All');
const searchDate = ref<string[]>([]);
// img // img
const isImageEdit = ref<boolean>(false); const isImageEdit = ref<boolean>(false);
@ -615,6 +620,8 @@ async function fetchListGroups(mobileFetch?: boolean) {
: currentStatus.value === 'ACTIVE' : currentStatus.value === 'ACTIVE'
? 'ACTIVE' ? 'ACTIVE'
: 'INACTIVE', : 'INACTIVE',
startDate: searchDate.value[0],
endDate: searchDate.value[1],
}); });
if (res) { if (res) {
@ -675,6 +682,8 @@ async function fetchListOfProduct(mobileFetch?: boolean) {
? 'ACTIVE' ? 'ACTIVE'
: undefined, : undefined,
productGroupId: currentIdGroup.value, productGroupId: currentIdGroup.value,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
}); });
if (res) { if (res) {
@ -720,6 +729,8 @@ async function fetchListOfService(mobileFetch?: boolean) {
? 'ACTIVE' ? 'ACTIVE'
: undefined, : undefined,
productGroupId: currentIdGroup.value, productGroupId: currentIdGroup.value,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
}); });
if (res) { if (res) {
@ -1590,6 +1601,7 @@ async function enterNext(type: 'service' | 'product') {
inputSearchProductAndService.value = ''; inputSearchProductAndService.value = '';
currentStatus.value = 'All'; currentStatus.value = 'All';
filterStat.value = []; filterStat.value = [];
searchDate.value = [];
if ( if (
expandedTree.value.length > 1 && expandedTree.value.length > 1 &&
@ -1745,7 +1757,7 @@ watch(currentStatus, async () => {
flowStore.rotate(); flowStore.rotate();
}); });
watch(inputSearch, async () => { watch([inputSearch, () => searchDate.value], async () => {
if (productMode.value === 'group') { if (productMode.value === 'group') {
productGroup.value = []; productGroup.value = [];
currentPageGroup.value = 1; currentPageGroup.value = 1;
@ -1754,7 +1766,7 @@ watch(inputSearch, async () => {
} }
}); });
watch(inputSearchProductAndService, async () => { watch([inputSearchProductAndService, () => searchDate.value], async () => {
product.value = []; product.value = [];
service.value = []; service.value = [];
currentPageServiceAndProduct.value = 1; currentPageServiceAndProduct.value = 1;
@ -1831,6 +1843,51 @@ function handleSubmitSameWorkflow() {
); );
} }
async function copy(id: string) {
{
const res = await fetchListServiceById(id);
if (res) {
formService.value = {
code: res.code.slice(0, -3),
name: res.name,
detail: res.detail,
attributes: res.attributes,
work: res.work.map((v) => ({
id: v.id,
name: v.name,
attributes: v.attributes,
product: v.productOnWork.map((productOnWorkItem) => ({
id: productOnWorkItem.product.id,
installmentNo: productOnWorkItem.installmentNo,
stepCount: productOnWorkItem.stepCount,
})),
})),
status: res.status,
productGroupId: res.productGroupId,
selectedImage: res.selectedImage,
installments: res.installments,
};
workItems.value = res.work.map((item) => {
return {
id: item.id,
name: item.name,
attributes: item.attributes,
product: item.productOnWork.map((productOnWorkItem) => {
return {
...productOnWorkItem.product,
nameEn: productOnWorkItem.product.name,
installmentNo: productOnWorkItem.installmentNo,
};
}),
};
});
}
}
dialogService.value = true;
}
watch( watch(
() => formService.value.attributes.workflowId, () => formService.value.attributes.workflowId,
async (a, b) => { async (a, b) => {
@ -1948,19 +2005,34 @@ watch(
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && currentStatus !== 'All'"
unelevated >
class="q-ml-sm" <div class="q-mt-sm text-weight-medium">
padding="4px" {{ $t('general.status') }}
size="sm" </div>
rounded <q-select
@click="refFilterGroup?.showPopup" v-model="currentStatus"
for="select-status"
outlined
dense
option-value="value"
option-label="label"
map-options
emit-value
:options="[
{ label: $t('general.all'), value: 'All' },
{ label: $t('general.active'), value: 'ACTIVE' },
{
label: $t('general.inactive'),
value: 'INACTIVE',
},
]"
/> />
</span> </AdvanceSearch>
</template> </template>
</q-input> </q-input>
</div> </div>
@ -2115,26 +2187,44 @@ watch(
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && currentStatus !== 'All'"
unelevated >
class="q-ml-sm" <div
padding="4px" v-if="$q.screen.lt.md"
size="sm" class="q-mt-sm text-weight-medium"
rounded >
@click="refFilterGroup?.showPopup" {{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
v-model="currentStatus"
for="select-status"
outlined
dense
option-value="value"
option-label="label"
map-options
emit-value
:options="[
{ label: $t('general.all'), value: 'All' },
{ label: $t('general.active'), value: 'ACTIVE' },
{
label: $t('general.inactive'),
value: 'INACTIVE',
},
]"
/> />
</span> </AdvanceSearch>
</template> </template>
</q-input> </q-input>
<div class="row col-md-6" style="white-space: nowrap"> <div class="row col-md-6" style="white-space: nowrap">
<q-select <q-select
v-show="$q.screen.gt.sm" v-show="$q.screen.gt.sm"
ref="refFilterGroup"
v-model="currentStatus" v-model="currentStatus"
for="select-status" for="select-status"
outlined outlined
@ -2155,7 +2245,6 @@ watch(
}, },
]" ]"
></q-select> ></q-select>
<q-select <q-select
v-if="modeView === false" v-if="modeView === false"
id="select-field" id="select-field"
@ -2178,7 +2267,6 @@ watch(
multiple multiple
dense dense
/> />
<q-btn-toggle <q-btn-toggle
v-model="modeView" v-model="modeView"
id="btn-mode" id="btn-mode"
@ -2618,26 +2706,65 @@ watch(
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && currentStatus !== 'All'"
unelevated >
class="q-ml-sm" <div
padding="4px" v-if="$q.screen.lt.md"
size="sm" class="q-mt-sm text-weight-medium"
rounded >
@click="refFilterProductService?.showPopup" {{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
:for="'field-select-status'"
v-model="currentStatus"
outlined
dense
option-value="value"
option-label="label"
map-options
emit-value
:options="[
{ label: $t('general.all'), value: 'All' },
{ label: $t('general.active'), value: 'ACTIVE' },
{
label: $t('general.inactive'),
value: 'INACTIVE',
},
]"
@update:model-value="fetchStatus()"
/> />
</span> </AdvanceSearch>
</template> </template>
</q-input> </q-input>
<div
class="flex q-mr-auto q-pl-sm"
v-if="productAndServiceTab === 'product'"
>
<input ref="fileImport" type="file" hidden />
<ImportButton
type="file"
import-file
icon-only
@file-selected="
(file) => {
importProduct(
currentIdGroup,
file,
async () => await fetchListOfProduct(),
);
}
"
/>
</div>
<div class="row col-md-6" style="white-space: nowrap"> <div class="row col-md-6" style="white-space: nowrap">
<q-select <q-select
v-show="$q.screen.gt.sm" v-show="$q.screen.gt.sm"
ref="refFilterProductService"
:for="'field-select-status'" :for="'field-select-status'"
v-model="currentStatus" v-model="currentStatus"
outlined outlined
@ -2659,7 +2786,6 @@ watch(
]" ]"
@update:model-value="fetchStatus()" @update:model-value="fetchStatus()"
></q-select> ></q-select>
<q-select <q-select
v-if="modeView === false" v-if="modeView === false"
:hide-dropdown-icon="$q.screen.lt.sm" :hide-dropdown-icon="$q.screen.lt.sm"
@ -2694,7 +2820,6 @@ watch(
multiple multiple
dense dense
/> />
<q-btn-toggle <q-btn-toggle
v-model="modeView" v-model="modeView"
id="btn-mode" id="btn-mode"
@ -3127,8 +3252,14 @@ watch(
" "
/> />
<KebabAction <KebabAction
:use-copy="productAndServiceTab === 'service'"
:status="props.row.status" :status="props.row.status"
:id-name="props.row.name" :id-name="props.row.name"
@copy="
() => {
copy(props.row.id);
}
"
@view=" @view="
async () => { async () => {
if (props.row.type === 'product') { if (props.row.type === 'product') {
@ -4422,6 +4553,7 @@ watch(
@click="serviceTreeView = false" @click="serviceTreeView = false"
/> />
</div> </div>
<SaveButton id="btn-info-basic-save" icon-only type="submit" /> <SaveButton id="btn-info-basic-save" icon-only type="submit" />
</div> </div>

View file

@ -2,7 +2,7 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { onMounted, reactive, ref } from 'vue'; import { onMounted, reactive, ref } from 'vue';
import { QSelect, QTableProps } from 'quasar'; import { QTableProps } from 'quasar';
import { useNavigator } from 'src/stores/navigator'; import { useNavigator } from 'src/stores/navigator';
import { useProperty } from 'src/stores/property'; import { useProperty } from 'src/stores/property';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@ -14,19 +14,23 @@ import { FloatingActionButton, PaginationComponent } from 'src/components';
import PaginationPageSize from 'src/components/PaginationPageSize.vue'; import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import PropertyDialog from './PropertyDialog.vue'; import PropertyDialog from './PropertyDialog.vue';
import { Property } from 'src/stores/property/types'; import { Property } from 'src/stores/property/types';
import { dialog } from 'src/stores/utils'; import { dialog, toCamelCase } from 'src/stores/utils';
import CreateButton from 'src/components/AddButton.vue'; import CreateButton from 'src/components/AddButton.vue';
import useOptionStore from 'stores/options';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { t } = useI18n(); const { t, locale } = useI18n();
const $q = useQuasar(); const $q = useQuasar();
const navigatorStore = useNavigator(); const navigatorStore = useNavigator();
const propertyStore = useProperty(); const propertyStore = useProperty();
const optionStore = useOptionStore();
const { const {
data: propertyData, data: propertyData,
page: propertyPage, page: propertyPage,
pageSize: propertyPageSize, pageSize: propertyPageSize,
pageMax: propertyPageMax, pageMax: propertyPageMax,
} = storeToRefs(propertyStore); } = storeToRefs(propertyStore);
const { globalOption } = storeToRefs(optionStore);
const currPropertyData = ref<Property>(); const currPropertyData = ref<Property>();
const formProperty = ref<Property>({ const formProperty = ref<Property>({
@ -35,7 +39,6 @@ const formProperty = ref<Property>({
type: {}, type: {},
}); });
const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all'); const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all');
const refFilter = ref<InstanceType<typeof QSelect>>();
const fieldSelected = ref<('order' | 'name' | 'type')[]>([ const fieldSelected = ref<('order' | 'name' | 'type')[]>([
'order', 'order',
'name', 'name',
@ -115,6 +118,7 @@ const pageState = reactive({
addModal: false, addModal: false,
viewDrawer: false, viewDrawer: false,
isDrawerEdit: true, isDrawerEdit: true,
searchDate: [],
}); });
async function fetchPropertyList(mobileFetch?: boolean) { async function fetchPropertyList(mobileFetch?: boolean) {
@ -131,6 +135,8 @@ async function fetchPropertyList(mobileFetch?: boolean) {
: statusFilter.value === 'statusACTIVE' : statusFilter.value === 'statusACTIVE'
? 'ACTIVE' ? 'ACTIVE'
: 'INACTIVE', : 'INACTIVE',
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
}); });
if (res) { if (res) {
propertyData.value = propertyData.value =
@ -146,6 +152,7 @@ async function fetchPropertyList(mobileFetch?: boolean) {
function triggerDialog(type: 'add' | 'edit' | 'view') { function triggerDialog(type: 'add' | 'edit' | 'view') {
if (type === 'add') { if (type === 'add') {
resetForm();
pageState.addModal = true; pageState.addModal = true;
pageState.isDrawerEdit = true; pageState.isDrawerEdit = true;
} }
@ -271,6 +278,31 @@ async function submit() {
}); });
} }
await fetchPropertyList($q.screen.xs); await fetchPropertyList($q.screen.xs);
// assign new property to global option
const propList = propertyData.value.map((v) => {
return {
label: locale.value === 'eng' ? v.nameEN : v.name,
value: toCamelCase(v.nameEN),
type: v.type.type,
};
});
const existingValues = new Set(
globalOption.value.propertiesField.map(
(item: { value: string }) => item.value,
),
);
const newProps = propList.filter((prop) => !existingValues.has(prop.value));
if (newProps) {
globalOption.value.propertiesField.splice(
globalOption.value.propertiesField.length - 4,
0,
...newProps,
);
}
currPropertyData.value;
resetForm(); resetForm();
} }
@ -288,11 +320,14 @@ watch(
fetchPropertyList(); fetchPropertyList();
}, },
); );
watch([() => pageState.inputSearch, propertyPageSize], () => { watch(
[() => pageState.inputSearch, propertyPageSize, () => pageState.searchDate],
() => {
propertyData.value = []; propertyData.value = [];
propertyPage.value = 1; propertyPage.value = 1;
fetchPropertyList(); fetchPropertyList();
}); },
);
</script> </script>
<template> <template>
<FloatingActionButton <FloatingActionButton
@ -369,26 +404,44 @@ watch([() => pageState.inputSearch, propertyPageSize], () => {
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="pageState.searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && statusFilter !== 'all'"
unelevated >
class="q-ml-sm" <div
padding="4px" v-if="$q.screen.lt.md"
size="sm" class="q-mt-sm text-weight-medium"
rounded >
@click="refFilter?.showPopup" {{ $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
: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> </template>
</q-input> </q-input>
<div class="row col-md-5" style="white-space: nowrap"> <div class="row col-md-5" style="white-space: nowrap">
<q-select <q-select
v-show="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
ref="refFilter"
v-model="statusFilter" v-model="statusFilter"
outlined outlined
dense dense
@ -483,11 +536,11 @@ watch([() => pageState.inputSearch, propertyPageSize], () => {
class="col surface-2 flex items-center justify-center" class="col surface-2 flex items-center justify-center"
> >
<NoData <NoData
v-if="pageState.total !== 0" v-if="pageState.total !== 0 || pageState.searchDate.length > 0"
:not-found="!!pageState.inputSearch" :not-found="!!pageState.inputSearch"
/> />
<CreateButton <CreateButton
v-if="pageState.total === 0" v-if="pageState.total === 0 && pageState.searchDate.length === 0"
@click="triggerDialog('add')" @click="triggerDialog('add')"
label="general.add" label="general.add"
:i18n-args="{ text: $t('flow.title') }" :i18n-args="{ text: $t('flow.title') }"

View file

@ -2,10 +2,12 @@
import { onMounted, reactive, ref, watch, computed } from 'vue'; import { onMounted, reactive, ref, watch, computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
// NOTE: Import stores // NOTE: Import stores
import useCustomerStore from 'stores/customer';
import { useQuotationStore } from 'src/stores/quotations'; import { useQuotationStore } from 'src/stores/quotations';
import { isRoleInclude } from 'stores/utils'; import { dialog, isRoleInclude, notify, setPrefixName } from 'stores/utils';
import { useNavigator } from 'src/stores/navigator'; import { useNavigator } from 'src/stores/navigator';
import useFlowStore from 'src/stores/flow'; import useFlowStore from 'src/stores/flow';
import useMyBranch from 'stores/my-branch'; import useMyBranch from 'stores/my-branch';
@ -38,26 +40,44 @@ import { AddressForm } from 'components/form';
import { import {
EmployerFormBusiness, EmployerFormBusiness,
EmployerFormAbout, EmployerFormAbout,
EmployerFormBasicInfo,
EmployerFormBranch,
} from 'src/pages/03_customer-management/components'; } from 'src/pages/03_customer-management/components';
import { useCustomerForm } from 'src/pages/03_customer-management/form'; import { useCustomerForm } from 'src/pages/03_customer-management/form';
import { Quotation } from 'src/stores/quotations/types'; import { Quotation } from 'src/stores/quotations/types';
import TableQuotation from 'src/components/05_quotation/TableQuotation.vue'; import TableQuotation from 'src/components/05_quotation/TableQuotation.vue';
import PaginationPageSize from 'src/components/PaginationPageSize.vue'; import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import { DialogContainer, DialogHeader } from 'src/components/dialog';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { t, locale } = useI18n();
const $q = useQuasar(); const $q = useQuasar();
const quotationFormStore = useQuotationForm(); const quotationFormStore = useQuotationForm();
const customerFormStore = useCustomerForm(); const customerFormStore = useCustomerForm();
const flowStore = useFlowStore(); const flowStore = useFlowStore();
const userBranch = useMyBranch(); const userBranch = useMyBranch();
const navigatorStore = useNavigator(); const navigatorStore = useNavigator();
const customerStore = useCustomerStore();
const {
fetchListOfOptionBranch,
customerFormUndo,
deleteCustomerById,
validateTabField,
deleteCustomerBranchById,
} = customerFormStore;
const { const {
currentFormData: quotationFormData, currentFormData: quotationFormData,
currentFormState: quotationFormState, currentFormState: quotationFormState,
} = storeToRefs(quotationFormStore); } = storeToRefs(quotationFormStore);
const { state: customerFormState, currentFormData: customerFormData } = const {
storeToRefs(customerFormStore); state: customerFormState,
currentFormData: customerFormData,
registerAbleBranchOption,
tabFieldRequired,
} = storeToRefs(customerFormStore);
const { currentMyBranch } = storeToRefs(userBranch); const { currentMyBranch } = storeToRefs(userBranch);
const fieldSelectedOption = computed(() => { const fieldSelectedOption = computed(() => {
@ -90,6 +110,7 @@ const pageState = reactive({
quotationModal: false, quotationModal: false,
employeeModal: false, employeeModal: false,
receiptModal: false, receiptModal: false,
searchDate: [],
}); });
pageState.fieldSelected = [...columnQuotation.map((v) => v.name)]; pageState.fieldSelected = [...columnQuotation.map((v) => v.name)];
@ -170,8 +191,7 @@ async function submitCustomer() {
customerFormData.value.registeredBranchId = isRoleInclude(['system']) customerFormData.value.registeredBranchId = isRoleInclude(['system'])
? branchId.value ? branchId.value
: currentMyBranch.value.id; : currentMyBranch.value.id;
await customerFormStore.submitFormCustomer(); await customerFormStore.addCurrentCustomerBranch();
customerFormState.value.dialogModal = false; customerFormState.value.dialogModal = false;
// customerFormState.value.dialogType = 'info'; // customerFormState.value.dialogType = 'info';
} }
@ -241,6 +261,16 @@ const {
stats: quotationStats, stats: quotationStats,
} = storeToRefs(quotationStore); } = storeToRefs(quotationStore);
const customerNameInfo = computed(() => {
if (customerFormData.value.customerBranch === undefined) return;
const name =
locale.value === 'eng'
? `${customerFormData.value.customerBranch[0]?.firstNameEN} ${customerFormData.value.customerBranch[0]?.lastNameEN}`
: `${customerFormData.value.customerBranch[0]?.firstName} ${customerFormData.value.customerBranch[0]?.lastName}`;
return name || '-';
});
onMounted(async () => { onMounted(async () => {
pageState.gridView = $q.screen.lt.md ? true : false; pageState.gridView = $q.screen.lt.md ? true : false;
navigatorStore.current.title = 'quotation.title'; navigatorStore.current.title = 'quotation.title';
@ -299,6 +329,8 @@ async function fetchQuotationList(mobileFetch?: boolean) {
: 'Issued', : 'Issued',
query: pageState.inputSearch, query: pageState.inputSearch,
urgentFirst: true, urgentFirst: true,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
}); });
if (ret) { if (ret) {
@ -322,7 +354,12 @@ async function fetchQuotationList(mobileFetch?: boolean) {
} }
watch( watch(
[() => pageState.currentTab, () => pageState.inputSearch, quotationPageSize], [
() => pageState.currentTab,
() => pageState.inputSearch,
() => pageState.searchDate,
quotationPageSize,
],
() => { () => {
quotationPage.value = 1; quotationPage.value = 1;
quotationData.value = []; quotationData.value = [];
@ -489,6 +526,10 @@ async function storeDataLocal(id: string) {
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch v-model="pageState.searchDate" />
</template>
</q-input> </q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap"> <div class="row col-md-5 justify-end" style="white-space: nowrap">
@ -911,10 +952,26 @@ async function storeDataLocal(id: string) {
<!-- NOTE: START - Customer / Employer Form --> <!-- NOTE: START - Customer / Employer Form -->
<DialogForm <DialogContainer
hide-footer :model-value="customerFormState.dialogModal"
ref="formDialogRef" :on-open="
v-model:modal="customerFormState.dialogModal" async () => {
customerFormStore.resetForm(customerFormState.dialogType === 'create');
onCreateImageList = { selectedImage: '', list: [] };
customerFormState.customerImageUrl = '';
await fetchListOfOptionBranch();
await customerFormStore.addCurrentCustomerBranch();
}
"
:on-close="
() => {
customerFormState.dialogModal = false;
onCreateImageList = { selectedImage: '', list: [] };
}
"
>
<template #header>
<DialogHeader
:title=" :title="
customerFormState.dialogType === 'create' customerFormState.dialogType === 'create'
? $t(`general.add`, { ? $t(`general.add`, {
@ -922,19 +979,22 @@ async function storeDataLocal(id: string) {
}) })
: `${$t('customer.employer')} ` : `${$t('customer.employer')} `
" "
:submit="
() => {
submitCustomer();
}
"
:close="
() => {
customerFormState.dialogModal = false;
customerFormStore.resetForm(true);
setDefaultCustomer();
}
"
> >
<template #title-after>
<span
:style="`color: hsla(var(--${customerFormData.customerType === 'PERS' ? 'teal-10-hsl' : 'violet-11-hsl'})/1)`"
>
:
{{
customerFormData.customerType === 'CORP'
? $t('customer.employerLegalEntity')
: $t('customer.employerNaturalPerson')
}}
</span>
</template>
</DialogHeader>
</template>
<div <div
:class="{ :class="{
'q-mx-lg q-my-md': $q.screen.gt.sm, 'q-mx-lg q-my-md': $q.screen.gt.sm,
@ -942,10 +1002,10 @@ async function storeDataLocal(id: string) {
}" }"
> >
<ProfileBanner <ProfileBanner
prefix="dialog"
v-if="customerFormData.customerBranch !== undefined" v-if="customerFormData.customerBranch !== undefined"
active active
hide-fade hide-fade
prefix="dialog"
:fallback-cover="`/images/customer-${customerFormData.customerType}-banner-bg.jpg`" :fallback-cover="`/images/customer-${customerFormData.customerType}-banner-bg.jpg`"
:img=" :img="
customerFormState.customerImageUrl || customerFormState.customerImageUrl ||
@ -961,13 +1021,22 @@ async function storeDataLocal(id: string) {
" "
:title=" :title="
customerFormData.customerType === 'PERS' customerFormData.customerType === 'PERS'
? `${customerFormData.customerBranch[0]?.firstName || ''} ${customerFormData.customerBranch[0]?.lastName || ''}` ? setPrefixName(
: customerFormData.customerBranch[0]?.registerName || '' {
namePrefix: customerFormData.customerBranch[0]?.namePrefix,
firstName: customerFormData.customerBranch[0]?.firstName,
lastName: customerFormData.customerBranch[0]?.lastName,
firstNameEN: customerFormData.customerBranch[0]?.firstNameEN,
lastNameEN: customerFormData.customerBranch[0]?.lastNameEN,
},
{ locale },
)
: customerFormData.customerBranch[0]?.registerName
" "
:caption=" :caption="
customerFormData.customerType === 'PERS' customerFormData.customerType === 'PERS'
? `${customerFormData.customerBranch[0]?.firstNameEN || ''} ${customerFormData.customerBranch[0]?.lastNameEN || ''}` ? `${customerFormData.customerBranch[0]?.firstNameEN} ${customerFormData.customerBranch[0]?.lastNameEN}`
: customerFormData.customerBranch[0]?.registerNameEN || '' : customerFormData.customerBranch[0]?.registerNameEN
" "
@view=" @view="
() => { () => {
@ -981,143 +1050,295 @@ async function storeDataLocal(id: string) {
/> />
</div> </div>
<div <div
class="col" style="flex: 1; width: 100%; overflow-y: auto"
id="customer-form"
:class="{ :class="{
'q-px-lg q-pb-lg': $q.screen.gt.sm, 'q-px-lg q-pb-lg': $q.screen.gt.sm,
'q-px-md q-pb-sm': !$q.screen.gt.sm, 'q-px-md q-pb-sm': !$q.screen.gt.sm,
}" }"
> >
<div class="col surface-1 full-height rounded bordered scroll row">
<div <div
style="overflow-y: auto" class="col"
class="row full-width full-height surface-1 rounded bordered relative-position" style="height: 100%; max-height: 100; overflow-y: auto"
>
<div
:class="{
'q-py-md q-px-lg': $q.screen.gt.sm,
'q-py-sm q-px-lg': !$q.screen.gt.sm,
}"
style="position: absolute; z-index: 99999; top: 0; right: 0"
>
<div
v-if="customerFormData.status !== 'INACTIVE'"
class="surface-1 row rounded"
>
<SaveButton
v-if="
customerFormState.dialogType === 'edit' ||
customerFormState.dialogType === 'create'
"
id="btn-info-basic-save"
icon-only
type="submit"
/>
</div>
</div>
<div
class="col full-height rounded scroll row q-py-md q-pl-md q-pr-sm"
v-if="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
> >
<div class="q-py-md q-pl-md q-pr-sm">
<SideMenu <SideMenu
:menu="[ :menu="[
{ {
name: $t('form.customerInformation'), name: $t('form.field.basicInformation'),
anchor: 'form-information', anchor: 'form-basic-info-customer',
}, },
{ {
name: $t('customerBranch.tab.business'), name: $t('customer.form.group.branch'),
anchor: 'form-business', anchor: 'form-branch-customer-branch',
}, useBtn: true,
{
name: $t('form.address'),
anchor: 'form-address',
}, },
...(customerFormData.customerBranch?.map((v, i) => ({
name:
i === 0
? $t('customer.form.headQuarters.title')
: $t('customer.form.branch.title', {
name: i,
}),
anchor: `form-branch-customer-no-${i}`,
sub: true,
})) || []),
]" ]"
background="transparent" background="transparent"
:active="{ :active="{
background: 'hsla(var(--blue-6-hsl) / .2)', background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)', foreground: 'var(--blue-6)',
}" }"
scroll-element="#branch-form" scroll-element="#customer-form-content"
>
<template v-slot:btn-form-branch-customer-branch>
<q-btn
dense
flat
icon="mdi-plus"
size="sm"
rounded
padding="0px 0px"
style="color: var(--stone-9)"
@click.stop="submitCustomer()"
v-if="
customerFormState.branchIndex === -1 &&
!!customerFormState.editCustomerId &&
customerFormState.dialogType !== 'create'
"
:disabled="!customerFormState.readonly"
/> />
</template>
</SideMenu>
</div>
</div> </div>
<div <div
class="col-12 col-md-10 full-height q-col-gutter-sm" class="col-12 col-md-10"
:class="{ :class="{
'q-py-md q-pr-md ': $q.screen.gt.sm, 'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-py-md q-px-lg': !$q.screen.gt.sm, 'q-pa-sm': !$q.screen.gt.sm,
}" }"
id="branch-form" id="customer-form-content"
style="overflow-y: auto" style="height: 100%; max-height: 100%; overflow-y: auto"
> >
<EmployerFormAbout <EmployerFormBasicInfo
id="form-information" prefixId="form"
show-title v-if="
:index="'0'" customerFormData.customerBranch !== undefined &&
:customerType="customerFormData.customerType" customerFormData.customerBranch.length > 0
:readonly="customerFormState.dialogType === 'info'"
v-model:citizen-id="formDataCustomerBranch.citizenId"
v-model:prefix-name="formDataCustomerBranch.namePrefix"
v-model:first-name="formDataCustomerBranch.firstName"
v-model:last-name="formDataCustomerBranch.lastName"
v-model:first-name-en="formDataCustomerBranch.firstNameEN"
v-model:last-name-en="formDataCustomerBranch.lastNameEN"
v-model:gender="formDataCustomerBranch.gender"
v-model:birth-date="formDataCustomerBranch.birthDate"
v-model:customer-name="formDataCustomerBranch.customerName"
v-model:legal-person-no="formDataCustomerBranch.legalPersonNo"
v-model:register-name="formDataCustomerBranch.registerName"
v-model:register-name-en="formDataCustomerBranch.registerNameEN"
v-model:register-date="formDataCustomerBranch.registerDate"
v-model:authorized-capital="
formDataCustomerBranch.authorizedCapital
" "
v-model:telephone-no="formDataCustomerBranch.telephoneNo" class="q-mb-xl"
:readonly="
(customerFormState.dialogType === 'edit' &&
customerFormState.readonly === true) ||
customerFormState.dialogType === 'info'
"
:action-disabled="customerFormState.branchIndex !== -1"
id="form-basic-info-customer"
:onCreate="customerFormState.dialogType === 'create'"
@edit="
((customerFormState.dialogType = 'edit'),
(customerFormState.readonly = false))
"
@cancel="() => customerFormUndo(false)"
@delete="
customerFormState.editCustomerId &&
deleteCustomerById(customerFormState.editCustomerId)
"
:customer-type="customerFormData.customerType"
v-model:registered-branch-id="customerFormData.registeredBranchId"
v-model:customer-name="customerNameInfo"
v-model:register-name="
customerFormData.customerBranch[0].registerName
"
v-model:citizen-id="customerFormData.customerBranch[0].citizenId"
v-model:legal-person-no="
customerFormData.customerBranch[0].legalPersonNo
"
v-model:business-type="
customerFormData.customerBranch[0].businessType
"
v-model:job-position="
customerFormData.customerBranch[0].jobPosition
"
v-model:telephone-no="
customerFormData.customerBranch[0].telephoneNo
"
v-model:branch-options="registerAbleBranchOption"
/> />
<div class="row q-col-gutter-sm" id="form-branch-customer-branch">
<div class="col-12 text-weight-bold text-body1 row items-center">
<q-icon
flat
size="xs"
class="q-pa-sm rounded q-mr-xs"
color="info"
name="mdi-briefcase-outline"
style="background-color: var(--surface-3)"
/>
<span>{{ $t('customer.form.group.branch') }}</span>
</div>
<EmployerFormBusiness <template
id="form-business" v-for="(_, idx) in customerFormData.customerBranch"
show-title :key="idx"
prefix-id="dialog" >
dense <!-- v-if="customerFormData.customerBranch" -->
outlined <q-form
:readonly="customerFormState.dialogType === 'info'" class="full-width"
v-model:bussiness-type="formDataCustomerBranch.businessType" greedy
v-model:job-position="formDataCustomerBranch.jobPosition" @submit.prevent="
v-model:job-description="formDataCustomerBranch.jobDescription" async () => {
v-model:pay-date="formDataCustomerBranch.payDate" if (!customerFormData.customerBranch) return;
v-model:pay-date-en="formDataCustomerBranch.payDateEN"
v-model:wage-rate="formDataCustomerBranch.wageRate" if (customerFormData.customerType === 'PERS') {
v-model:wage-rate-text="formDataCustomerBranch.wageRateText" tabFieldRequired.main = [
/> 'citizenId',
<AddressForm 'namePrefix',
id="form-address" 'firstName',
prefix-id="employer" 'firstNameEN',
dense 'lastName',
outlined 'lastNameEN',
use-employment 'gender',
:title="$t('form.address')" 'birthDate',
:addressTitle="$t('form.address')" ];
:addressTitleEN="$t('form.address', { suffix: '(EN)' })" }
:readonly="customerFormState.dialogType === 'info'" if (customerFormData.customerType === 'CORP') {
v-model:bussiness-type="formDataCustomerBranch.businessType" tabFieldRequired.main = ['legalPersonNo', 'registerName'];
v-model:address="formDataCustomerBranch.address" }
v-model:address-en="formDataCustomerBranch.addressEN" let tapIsUndefined = validateTabField(
v-model:street="formDataCustomerBranch.street" customerFormData.customerBranch?.[idx],
v-model:street-en="formDataCustomerBranch.streetEN" tabFieldRequired,
v-model:moo="formDataCustomerBranch.moo" );
v-model:moo-en="formDataCustomerBranch.mooEN"
v-model:soi="formDataCustomerBranch.soi" if (tapIsUndefined.length > 0) {
v-model:soi-en="formDataCustomerBranch.soiEN" return dialog({
v-model:province-id="formDataCustomerBranch.provinceId" color: 'warning',
v-model:district-id="formDataCustomerBranch.districtId" icon: 'mdi-alert',
v-model:sub-district-id="formDataCustomerBranch.subDistrictId" title: t('dialog.title.incompleteDataEntry'),
v-model:home-code="formDataCustomerBranch.homeCode" actionText: t('general.ok'),
persistent: true,
message: t('dialog.message.incompleteDataEntry', {
tap: `${tapIsUndefined.map((v: string) => t(`customerBranch.tab.${v}`)).join(', ')}`,
}),
action: async () => {
return;
},
});
}
if (!customerFormData.customerBranch[idx].id) {
let res: any;
if (idx === 0) {
res =
await customerFormStore.submitFormCustomer(
onCreateImageList,
);
} else {
res = await customerStore.createBranch({
...customerFormData.customerBranch[idx],
customerId: customerFormState.editCustomerId || '',
id: undefined,
});
}
if (res) {
customerFormState.readonly = true;
notify('create', $t('general.success'));
}
} else {
if (!customerFormState.editCustomerId) return;
await customerStore.editBranchById(
customerFormData.customerBranch[idx].id || '',
{
...customerFormData.customerBranch[idx],
id: undefined,
},
);
}
const uploadResult =
customerFormData.customerBranch[idx].file?.map(
async (v) => {
if (!v.file) return;
const ext = v.file.name.split('.').at(-1);
let filename = v.group + '-' + new Date().getTime();
if (ext) filename += `.${ext}`;
const res = await customerStore.putAttachment({
branchId:
customerFormData.customerBranch?.[idx].id || '',
file: v.file,
filename,
});
if (res) {
await customerFormStore.assignFormData(
customerFormState.editCustomerId,
);
}
},
) || [];
for (const r of uploadResult) await r;
await customerFormStore.assignFormData(
customerFormState.editCustomerId,
);
customerFormStore.resetForm();
}
"
>
<!-- v-if="!!customerFormState.editCustomerId" -->
<EmployerFormBranch
:index="idx"
prefixId="form"
v-if="customerFormData.customerBranch"
v-model:customer="customerFormData"
v-model:customer-branch="customerFormData.customerBranch[idx]"
:onCreate="customerFormState.dialogType === 'create'"
:customer-type="customerFormData.customerType"
:readonly="customerFormState.branchIndex !== idx"
:action-disabled="
!customerFormState.readonly ||
(customerFormState.branchIndex !== -1 &&
customerFormState.branchIndex !== idx)
"
@edit="() => (customerFormState.branchIndex = idx)"
@cancel="() => customerFormUndo(false)"
@delete="
async () => {
if (!customerFormState.editCustomerId) return;
if (idx === 0) {
deleteCustomerById(customerFormState.editCustomerId);
return;
}
if (!!customerFormData.customerBranch?.[idx].id) {
const action = await deleteCustomerBranchById(
customerFormData.customerBranch[idx].id || '',
);
if (action) {
await customerFormStore.assignFormData(
customerFormState.editCustomerId,
);
}
customerFormStore.resetForm();
}
}
"
@save="() => {}"
/> />
</q-form>
</template>
</div> </div>
</div> </div>
</div> </div>
</DialogForm> </div>
</DialogContainer>
</template> </template>
<style scoped></style> <style scoped></style>

View file

@ -25,7 +25,7 @@ import { deleteItem } from 'stores/utils';
// NOTE Import Types // NOTE Import Types
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types'; import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
import { View } from './types.ts'; import { View } from './types';
import { import {
PayCondition, PayCondition,
ProductRelation, ProductRelation,
@ -76,6 +76,7 @@ import { api } from 'src/boot/axios';
import { RouterLink, useRoute } from 'vue-router'; import { RouterLink, useRoute } from 'vue-router';
import { initLang, initTheme, Lang } from 'src/utils/ui'; import { initLang, initTheme, Lang } from 'src/utils/ui';
import { convertTemplate } from 'src/utils/string-template'; import { convertTemplate } from 'src/utils/string-template';
import { getRole } from 'src/services/keycloak';
type Node = { type Node = {
[key: string]: any; [key: string]: any;
@ -163,7 +164,9 @@ const selectedWorkerItem = computed(() => {
employeeName: employeeName:
locale.value === Lang.English locale.value === Lang.English
? `${e.firstNameEN} ${e.lastNameEN}` ? `${e.firstNameEN} ${e.lastNameEN}`
: `${e.firstName} ${e.lastName}`, : e.firstName
? `${e.firstName} ${e.lastName}`
: `${e.firstNameEN} ${e.lastNameEN}`,
birthDate: dateFormatJS({ date: e.dateOfBirth }), birthDate: dateFormatJS({ date: e.dateOfBirth }),
gender: e.gender, gender: e.gender,
age: calculateAge(e.dateOfBirth), age: calculateAge(e.dateOfBirth),
@ -211,6 +214,17 @@ const attachmentData = ref<
url?: string; url?: string;
}[] }[]
>([]); >([]);
const hideBtnApproveInvoice = computed(() => {
const role = getRole();
const allowedRoles = [
'system',
'head_of_admin',
'admin',
'head_of_accountant',
'accountant',
];
return !role || !role.some((r) => allowedRoles.includes(r));
});
const getToolbarConfig = computed(() => { const getToolbarConfig = computed(() => {
const toolbar = [['left', 'center', 'justify'], ['toggle'], ['clip']]; const toolbar = [['left', 'center', 'justify'], ['toggle'], ['clip']];
@ -578,8 +592,9 @@ async function convertDataToFormSubmit() {
} }
}), }),
...newWorkerList.value.map((v) => { ...newWorkerList.value.map((v) => {
const { attachment, ...payload } = v; {
return payload; return v.id;
}
}), }),
]), ]),
); );
@ -1850,7 +1865,7 @@ function covertToNode() {
name: selectedWorker.map( name: selectedWorker.map(
(v, i) => (v, i) =>
`${i + 1}. ` + `${i + 1}. ` +
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(), `${v.employeePassport.length !== 0 ? v.employeePassport[0].number + '_' : ''} ${v.namePrefix}.${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
), ),
}, },
}) })
@ -2317,6 +2332,7 @@ function covertToNode() {
" "
> >
<MainButton <MainButton
v-if="!hideBtnApproveInvoice"
solid solid
icon="mdi-account-multiple-check-outline" icon="mdi-account-multiple-check-outline"
color="207 96% 32%" color="207 96% 32%"

View file

@ -3,7 +3,7 @@ import { Icon } from '@iconify/vue';
import { ref, watch, computed } from 'vue'; import { ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { View } from './types.ts'; import { View } from './types';
import { formatNumberDecimal, commaInput } from 'stores/utils'; import { formatNumberDecimal, commaInput } from 'stores/utils';
@ -14,7 +14,7 @@ import SelectInput from 'src/components/shared/SelectInput.vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { precisionRound } from 'src/utils/arithmetic'; import { precisionRound } from 'src/utils/arithmetic';
import { PayCondition } from 'src/stores/quotations/types.ts'; import { PayCondition } from 'src/stores/quotations/types';
defineEmits<{ defineEmits<{
(e: 'changePayType', type: PayCondition): void; (e: 'changePayType', type: PayCondition): void;

File diff suppressed because it is too large Load diff

View file

@ -221,6 +221,7 @@ export const useQuotationForm = defineStore('form-quotation', () => {
newWorkerList.value.push({ newWorkerList.value.push({
//passportNo: obj.data.passportNo, //passportNo: obj.data.passportNo,
//documentExpireDate: obj.data.documentExpireDate, //documentExpireDate: obj.data.documentExpireDate,
id: obj.data.id,
lastNameEN: obj.data.lastNameEN, lastNameEN: obj.data.lastNameEN,
lastName: obj.data.lastName, lastName: obj.data.lastName,
middleNameEN: obj.data.middleNameEN, middleNameEN: obj.data.middleNameEN,

View file

@ -600,7 +600,7 @@ function print() {
details?.worker.map( details?.worker.map(
(v, i) => (v, i) =>
`${i + 1}. ` + `${i + 1}. ` +
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(), `${v.namePrefix}. ${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
) || [], ) || [],
}, },
}) || '-' }) || '-'

View file

@ -10,6 +10,7 @@ import DrawerInfo from 'src/components/DrawerInfo.vue';
import DialogForm from 'src/components/DialogForm.vue'; import DialogForm from 'src/components/DialogForm.vue';
import ProfileBanner from 'src/components/ProfileBanner.vue'; import ProfileBanner from 'src/components/ProfileBanner.vue';
import SideMenu from 'src/components/SideMenu.vue'; import SideMenu from 'src/components/SideMenu.vue';
import FormBank from 'src/components/01_branch-management/FormBank.vue';
import FormBasicInfoAgencies from 'src/components/07_agencies-management/FormBasicInfoAgencies.vue'; import FormBasicInfoAgencies from 'src/components/07_agencies-management/FormBasicInfoAgencies.vue';
import { import {
UndoButton, UndoButton,
@ -19,6 +20,8 @@ import {
} from 'src/components/button'; } from 'src/components/button';
import AddressForm from 'src/components/form/AddressForm.vue'; import AddressForm from 'src/components/form/AddressForm.vue';
import ImageUploadDialog from 'src/components/ImageUploadDialog.vue'; import ImageUploadDialog from 'src/components/ImageUploadDialog.vue';
import { BankBook } from 'src/stores/branch/types';
import QrCodeUploadDialog from 'src/components/QrCodeUploadDialog.vue';
const institutionStore = useInstitution(); const institutionStore = useInstitution();
@ -27,10 +30,14 @@ const drawerModel = defineModel<boolean>('drawerModel', {
required: true, required: true,
default: false, default: false,
}); });
const onCreateImageList = defineModel<{ const imageListOnCreate = defineModel<{
selectedImage: string; selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[]; list: { url: string; imgFile: File | null; name: string }[];
}>('onCreateImageList', { default: { selectedImage: '', list: [] } }); }>('imageListOnCreate', { default: { selectedImage: '', list: [] } });
const deletesStatusQrCodeBankImag = defineModel<number[]>(
'deletesStatusQrCodeBankImag',
{ default: [] },
);
const imageState = reactive({ const imageState = reactive({
imageDialog: false, imageDialog: false,
@ -43,6 +50,14 @@ const imageState = reactive({
const imageFile = ref<File | null>(null); const imageFile = ref<File | null>(null);
const imageList = ref<{ selectedImage: string; list: string[] }>(); const imageList = ref<{ selectedImage: string; list: string[] }>();
const qrCodeDialog = ref(false);
const qrCodeImageUrl = ref<string>('');
const currentIndexQrCodeBank = ref<number>(-1);
const statusQrCodeFile = ref<File | undefined>(undefined);
const refQrCodeUpload = ref();
const statusQrCodeUrl = ref<string>('');
const statusDeletesQrCode = ref<boolean>(false);
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
readonly?: boolean; readonly?: boolean;
@ -73,6 +88,9 @@ const data = defineModel<InstitutionPayload>('data', {
group: '', group: '',
name: '', name: '',
nameEN: '', nameEN: '',
contactName: '',
contactEmail: '',
contactTel: '',
code: '', code: '',
addressEN: '', addressEN: '',
address: '', address: '',
@ -88,6 +106,19 @@ const data = defineModel<InstitutionPayload>('data', {
selectedImage: '', selectedImage: '',
}, },
}); });
const formBankBook = defineModel<BankBook[]>('formBankBook', {
default: [
{
bankName: '',
accountNumber: '',
bankBranch: '',
accountName: '',
accountType: '',
currentlyUse: true,
bankUrl: '',
},
],
});
function viewImage() { function viewImage() {
imageState.imageDialog = true; imageState.imageDialog = true;
@ -141,10 +172,39 @@ async function submitImage(name: string) {
function clearImageState() { function clearImageState() {
imageState.imageDialog = false; imageState.imageDialog = false;
imageFile.value = null; imageFile.value = null;
onCreateImageList.value = { selectedImage: '', list: [] }; imageListOnCreate.value = { selectedImage: '', list: [] };
imageState.refreshImageState = false; imageState.refreshImageState = false;
} }
function triggerEditQrCodeBank(opts?: { save?: boolean }) {
if (opts?.save === undefined) {
qrCodeDialog.value = true;
statusDeletesQrCode.value = false;
statusQrCodeUrl.value =
formBankBook.value[currentIndexQrCodeBank.value].bankUrl || '';
statusDeletesQrCode.value = false;
} else {
formBankBook.value[currentIndexQrCodeBank.value].bankUrl =
statusQrCodeUrl.value;
formBankBook.value[currentIndexQrCodeBank.value].bankQr =
statusQrCodeFile.value;
if (statusDeletesQrCode.value === true) {
deletesStatusQrCodeBankImag.value.push(currentIndexQrCodeBank.value);
}
if (statusDeletesQrCode.value === false) {
deletesStatusQrCodeBankImag.value =
deletesStatusQrCodeBankImag.value.filter(
(item) => item !== currentIndexQrCodeBank.value,
);
}
currentIndexQrCodeBank.value = -1;
statusDeletesQrCode.value = false;
}
}
watch( watch(
() => imageFile.value, () => imageFile.value,
() => { () => {
@ -159,7 +219,6 @@ watch(
imageList.value imageList.value
? (imageList.value.selectedImage = data.value.selectedImage || '') ? (imageList.value.selectedImage = data.value.selectedImage || '')
: ''; : '';
console.log(imageState.imageUrl);
imageState.refreshImageState = false; imageState.refreshImageState = false;
}, },
); );
@ -237,11 +296,15 @@ watch(
:menu="[ :menu="[
{ {
name: $t('form.field.basicInformation'), name: $t('form.field.basicInformation'),
anchor: 'agencies-basic-info', anchor: 'agencies-form-basic-info',
}, },
{ {
name: $t('general.address'), name: $t('general.address'),
anchor: 'agencies-address-info', anchor: 'agencies-form-address-info',
},
{
name: $t('agencies.bankInfo'),
anchor: 'agencies-form-bank-info',
}, },
]" ]"
background="transparent" background="transparent"
@ -275,14 +338,17 @@ watch(
</div> </div>
</div> </div>
<FormBasicInfoAgencies <FormBasicInfoAgencies
id="agencies-basic-info" id="agencies-form-basic-info"
class="q-mb-xl" class="q-mb-xl"
v-model:group="data.group" v-model:group="data.group"
v-model:name="data.name" v-model:name="data.name"
v-model:name-en="data.nameEN" v-model:name-en="data.nameEN"
v-model:contact-name="data.contactName"
v-model:email="data.contactEmail"
v-model:contact-tel="data.contactTel"
/> />
<AddressForm <AddressForm
id="agencies-address-info" id="agencies-form-address-info"
dense dense
:prefix-id="''" :prefix-id="''"
v-model:address="data.address" v-model:address="data.address"
@ -297,6 +363,25 @@ watch(
v-model:district-id="data.districtId" v-model:district-id="data.districtId"
v-model:sub-district-id="data.subDistrictId" v-model:sub-district-id="data.subDistrictId"
/> />
<FormBank
id="agencies-form-bank-info"
title="agencies.bankInfo"
class="q-pt-xl"
dense
v-model:bank-book-list="formBankBook"
@view-qr="
(i) => {
currentIndexQrCodeBank = i;
triggerEditQrCodeBank();
}
"
@edit-qr="
(i) => {
currentIndexQrCodeBank = i;
refQrCodeUpload && refQrCodeUpload.browse();
}
"
/>
</div> </div>
</div> </div>
</DialogForm> </DialogForm>
@ -427,13 +512,17 @@ watch(
name: $t('general.address'), name: $t('general.address'),
anchor: 'agencies-address-info', anchor: 'agencies-address-info',
}, },
{
name: $t('agencies.bankInfo'),
anchor: 'agencies-bank-info',
},
]" ]"
background="transparent" background="transparent"
:active="{ :active="{
background: 'hsla(var(--blue-6-hsl) / .2)', background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)', foreground: 'var(--blue-6)',
}" }"
scroll-element="#agencies-form-content" scroll-element="#agencies-view-content"
/> />
</div> </div>
</div> </div>
@ -444,7 +533,7 @@ watch(
'q-py-md q-pr-md ': $q.screen.gt.sm, 'q-py-md q-pr-md ': $q.screen.gt.sm,
'q-pa-sm': !$q.screen.gt.sm, 'q-pa-sm': !$q.screen.gt.sm,
}" }"
id="user-form-content" id="agencies-view-content"
style="height: 100%; max-height: 100; overflow-y: auto" style="height: 100%; max-height: 100; overflow-y: auto"
> >
<FormBasicInfoAgencies <FormBasicInfoAgencies
@ -455,6 +544,9 @@ watch(
v-model:group="data.group" v-model:group="data.group"
v-model:name="data.name" v-model:name="data.name"
v-model:name-en="data.nameEN" v-model:name-en="data.nameEN"
v-model:contact-name="data.contactName"
v-model:email="data.contactEmail"
v-model:contact-tel="data.contactTel"
/> />
<AddressForm <AddressForm
id="agencies-address-info" id="agencies-address-info"
@ -473,6 +565,26 @@ watch(
v-model:district-id="data.districtId" v-model:district-id="data.districtId"
v-model:sub-district-id="data.subDistrictId" v-model:sub-district-id="data.subDistrictId"
/> />
<FormBank
id="agencies-bank-info"
title="agencies.bankInfo"
class="q-pt-xl"
dense
:readonly
v-model:bank-book-list="formBankBook"
@view-qr="
(i) => {
currentIndexQrCodeBank = i;
triggerEditQrCodeBank();
}
"
@edit-qr="
(i) => {
currentIndexQrCodeBank = i;
refQrCodeUpload && refQrCodeUpload.browse();
}
"
/>
</div> </div>
</div> </div>
</div> </div>
@ -482,7 +594,7 @@ watch(
<ImageUploadDialog <ImageUploadDialog
v-model:dialog-state="imageState.imageDialog" v-model:dialog-state="imageState.imageDialog"
v-model:file="imageFile" v-model:file="imageFile"
v-model:on-create-data-list="onCreateImageList" v-model:on-create-data-list="imageListOnCreate"
v-model:image-url="imageState.imageUrl" v-model:image-url="imageState.imageUrl"
v-model:data-list="imageList" v-model:data-list="imageList"
:on-create="model" :on-create="model"
@ -512,5 +624,31 @@ watch(
</div> </div>
</template> </template>
</ImageUploadDialog> </ImageUploadDialog>
<QrCodeUploadDialog
ref="refQrCodeUpload"
v-model:dialog-state="qrCodeDialog"
v-model:file="statusQrCodeFile as File"
v-model:image-url="statusQrCodeUrl"
@save="
(_file) => {
qrCodeDialog = false;
if (currentIndexQrCodeBank !== -1) {
triggerEditQrCodeBank({ save: true });
}
}
"
@clear="statusDeletesQrCode = true"
clearButton
>
<template #error>
<div
class="full-width full-height flex items-center justify-center"
style="color: gray"
>
<q-icon size="15rem" name="mdi-qrcode" />
</div>
</template>
</QrCodeUploadDialog>
</template> </template>
<style scoped></style> <style scoped></style>

View file

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { QSelect, QTableProps } from 'quasar'; import { QTableProps } from 'quasar';
import { dialog } from 'src/stores/utils'; import { dialog } from 'src/stores/utils';
import { onMounted, reactive, ref, watch } from 'vue'; import { onMounted, reactive, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@ -21,6 +21,7 @@ import FloatingActionButton from 'src/components/FloatingActionButton.vue';
import CreateButton from 'src/components/AddButton.vue'; import CreateButton from 'src/components/AddButton.vue';
import NoData from 'src/components/NoData.vue'; import NoData from 'src/components/NoData.vue';
import AgenciesDialog from './AgenciesDialog.vue'; import AgenciesDialog from './AgenciesDialog.vue';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { t } = useI18n(); const { t } = useI18n();
const $q = useQuasar(); const $q = useQuasar();
@ -78,13 +79,17 @@ const pageState = reactive({
addModal: false, addModal: false,
viewDrawer: false, viewDrawer: false,
isDrawerEdit: true, isDrawerEdit: true,
searchDate: [],
}); });
const deletesStatusQrCodeBankImag = ref<number[]>([]);
const blankFormData: InstitutionPayload = { const blankFormData: InstitutionPayload = {
group: '', group: '',
code: '', code: '',
name: '', name: '',
nameEN: '', nameEN: '',
contactName: '',
contactEmail: '',
contactTel: '',
addressEN: '', addressEN: '',
address: '', address: '',
soi: '', soi: '',
@ -98,14 +103,23 @@ const blankFormData: InstitutionPayload = {
provinceId: '', provinceId: '',
selectedImage: '', selectedImage: '',
status: 'CREATED', status: 'CREATED',
bank: [
{
bankName: '',
accountNumber: '',
bankBranch: '',
accountName: '',
accountType: '',
currentlyUse: true,
},
],
}; };
const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all'); const statusFilter = ref<'all' | 'statusACTIVE' | 'statusINACTIVE'>('all');
const refFilter = ref<InstanceType<typeof QSelect>>();
const refAgenciesDialog = ref(); const refAgenciesDialog = ref();
const formData = ref<InstitutionPayload>(structuredClone(blankFormData)); const formData = ref<InstitutionPayload>(structuredClone(blankFormData));
const currAgenciesData = ref<Institution>(); const currAgenciesData = ref<Institution>();
const onCreateImageList = ref<{ const imageListOnCreate = ref<{
selectedImage: string; selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[]; list: { url: string; imgFile: File | null; name: string }[];
}>({ selectedImage: '', list: [] }); }>({ selectedImage: '', list: [] });
@ -160,6 +174,18 @@ function assignFormData(data: Institution) {
provinceId: data.provinceId, provinceId: data.provinceId,
selectedImage: data.selectedImage, selectedImage: data.selectedImage,
status: data.status, status: data.status,
contactEmail: data.contactEmail,
contactName: data.contactName,
contactTel: data.contactTel,
bank: data.bank.map((v) => ({
bankName: v.bankName,
accountNumber: v.accountNumber,
bankBranch: v.bankBranch,
accountName: v.accountName,
accountType: v.accountType,
currentlyUse: v.currentlyUse,
bankUrl: `${baseUrl}/institution/${data.id}/bank-qr/${v.id}?ts=${Date.now()}`,
})),
}; };
} }
@ -169,6 +195,9 @@ async function submit(opt?: { selectedImage: string }) {
code: formData.value.code, code: formData.value.code,
name: formData.value.name, name: formData.value.name,
nameEN: formData.value.nameEN, nameEN: formData.value.nameEN,
contactName: formData.value.contactName,
contactEmail: formData.value.contactEmail,
contactTel: formData.value.contactTel,
addressEN: formData.value.addressEN, addressEN: formData.value.addressEN,
address: formData.value.address, address: formData.value.address,
soi: formData.value.soi, soi: formData.value.soi,
@ -181,7 +210,11 @@ async function submit(opt?: { selectedImage: string }) {
districtId: formData.value.districtId, districtId: formData.value.districtId,
provinceId: formData.value.provinceId, provinceId: formData.value.provinceId,
status: formData.value.status, status: formData.value.status,
bank: formData.value.bank.map((v) => ({
...v,
})),
}; };
console.log('payload', payload);
if ( if (
(pageState.isDrawerEdit && currAgenciesData.value?.id) || (pageState.isDrawerEdit && currAgenciesData.value?.id) ||
(opt?.selectedImage && currAgenciesData.value?.id) (opt?.selectedImage && currAgenciesData.value?.id)
@ -192,6 +225,7 @@ async function submit(opt?: { selectedImage: string }) {
id: currAgenciesData.value.id, id: currAgenciesData.value.id,
selectedImage: opt?.selectedImage || undefined, selectedImage: opt?.selectedImage || undefined,
}), }),
{ indexDeleteQrCodeBank: deletesStatusQrCodeBankImag.value },
); );
if (ret) { if (ret) {
@ -211,7 +245,7 @@ async function submit(opt?: { selectedImage: string }) {
...payload, ...payload,
code: formData.value.group || '', code: formData.value.group || '',
}, },
onCreateImageList.value, imageListOnCreate.value,
); );
await fetchData($q.screen.xs); await fetchData($q.screen.xs);
@ -251,6 +285,8 @@ async function fetchData(mobileFetch?: boolean) {
: statusFilter.value === 'statusACTIVE' : statusFilter.value === 'statusACTIVE'
? 'ACTIVE' ? 'ACTIVE'
: 'INACTIVE', : 'INACTIVE',
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
}); });
if (ret) { if (ret) {
@ -320,7 +356,7 @@ onMounted(async () => {
}); });
watch( watch(
() => [pageState.inputSearch, statusFilter.value], () => [pageState.inputSearch, statusFilter.value, pageState.searchDate],
() => { () => {
page.value = 1; page.value = 1;
data.value = []; data.value = [];
@ -403,26 +439,44 @@ watch(
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="pageState.searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && statusFilter !== 'all'"
unelevated >
class="q-ml-sm" <div
padding="4px" v-if="$q.screen.lt.md"
size="sm" class="q-mt-sm text-weight-medium"
rounded >
@click="refFilter?.showPopup" {{ $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
: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> </template>
</q-input> </q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap"> <div class="row col-md-5 justify-end" style="white-space: nowrap">
<q-select <q-select
v-show="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
ref="refFilter"
v-model="statusFilter" v-model="statusFilter"
outlined outlined
dense dense
@ -922,7 +976,9 @@ watch(
v-model="pageState.addModal" v-model="pageState.addModal"
v-model:drawer-model="pageState.viewDrawer" v-model:drawer-model="pageState.viewDrawer"
v-model:data="formData" v-model:data="formData"
v-model:on-create-image-list="onCreateImageList" v-model:form-bank-book="formData.bank"
v-model:image-list-on-create="imageListOnCreate"
v-model:deletes-status-qr-code-bank-imag="deletesStatusQrCodeBankImag"
/> />
</template> </template>
<style scoped> <style scoped>

View file

@ -11,6 +11,7 @@ import { useRequestList } from 'src/stores/request-list';
const props = defineProps<{ const props = defineProps<{
readonly?: boolean; readonly?: boolean;
step: Step; step: Step;
requestWorkId: string;
}>(); }>();
const requestListStore = useRequestList(); const requestListStore = useRequestList();
@ -116,7 +117,7 @@ function assignToForm() {
<FormGroupHead> <FormGroupHead>
{{ $t('quotation.templateForm') }} {{ $t('quotation.templateForm') }}
</FormGroupHead> </FormGroupHead>
<FormIssue :readonly="!state.isEdit" /> <FormIssue :request-work-id="requestWorkId" :readonly="!state.isEdit" />
</section> </section>
</main> </main>
</q-expansion-item> </q-expansion-item>

View file

@ -2,14 +2,49 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { MainButton } from 'components/button'; import { MainButton } from 'components/button';
import SelectInput from 'src/components/shared/SelectInput.vue'; import SelectInput from 'src/components/shared/SelectInput.vue';
import { onMounted } from 'vue';
import { api } from 'src/boot/axios';
import { baseUrl } from 'src/stores/utils';
defineProps<{ const prop = defineProps<{
readonly?: boolean; readonly?: boolean;
requestWorkId: string;
}>(); }>();
const templateForm = defineModel<string>(); const templateForm = defineModel<string>();
const templateFormOption = ref< const templateFormOption = ref<
{ label: string; labelEN: string; value: string }[] { label: string; labelEN: string; value: string }[]
>([]); >([]);
onMounted(async () => {
const { data: docTemplate, status } = await api.get<string[]>(
'/doc-template',
{ params: { templateGroup: 'Request' } },
);
if (status < 400) {
templateFormOption.value = docTemplate.map((v) => ({
label: v,
labelEN: v,
value: v,
}));
}
});
async function formDownload() {
const res = await fetch(
baseUrl +
'/doc-template/' +
templateForm.value +
`?templateGroup=Request&data=request-work&dataId=${prop.requestWorkId}`,
);
const blob = await res.blob();
const a = document.createElement('a');
a.download = templateForm.value;
a.href = window.URL.createObjectURL(blob);
a.click();
a.remove();
}
</script> </script>
<template> <template>
@ -24,7 +59,6 @@ const templateFormOption = ref<
<SelectInput <SelectInput
id="quotation-branch" id="quotation-branch"
style="grid-column: span 2" style="grid-column: span 2"
incremental
v-model="templateForm" v-model="templateForm"
class="full-width" class="full-width"
:readonly :readonly
@ -32,21 +66,14 @@ const templateFormOption = ref<
:label="$t('quotation.templateForm')" :label="$t('quotation.templateForm')"
:option-label="$i18n.locale === 'eng' ? 'labelEN' : 'label'" :option-label="$i18n.locale === 'eng' ? 'labelEN' : 'label'"
/> />
<MainButton
outlined
icon="mdi-play-box-outline"
color="207 96% 32%"
class="full-width"
style="grid-column: span 1"
>
{{ $t('general.view', { msg: $t('general.example') }) }}
</MainButton>
<MainButton <MainButton
solid solid
icon="mdi-pencil-outline" icon="mdi-pencil-outline"
color="207 96% 32%" color="207 96% 32%"
class="full-width" class="full-width"
style="grid-column: span 1" style="grid-column: span 1"
:disabled="!templateForm"
@click="formDownload"
> >
{{ $t('general.designForm') }} {{ $t('general.designForm') }}
</MainButton> </MainButton>

View file

@ -3,7 +3,7 @@
import { computed, onMounted, reactive, ref, watch } from 'vue'; import { computed, onMounted, reactive, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QSelect, useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
// NOTE: Components // NOTE: Components
import StatCardComponent from 'src/components/StatCardComponent.vue'; import StatCardComponent from 'src/components/StatCardComponent.vue';
@ -24,6 +24,9 @@ import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
import { dialogWarningClose } from 'src/stores/utils'; import { dialogWarningClose } from 'src/stores/utils';
import { CancelButton, SaveButton } from 'src/components/button'; import { CancelButton, SaveButton } from 'src/components/button';
import { getRole } from 'src/services/keycloak'; import { getRole } from 'src/services/keycloak';
import FloatingActionButton from 'src/components/FloatingActionButton.vue';
import RequestListAction from './RequestListAction .vue';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const $q = useQuasar(); const $q = useQuasar();
const navigatorStore = useNavigator(); const navigatorStore = useNavigator();
@ -32,7 +35,7 @@ const requestListStore = useRequestList();
const { t } = useI18n(); const { t } = useI18n();
const { data, stats, page, pageMax, pageSize } = storeToRefs(requestListStore); const { data, stats, page, pageMax, pageSize } = storeToRefs(requestListStore);
const refFilter = ref<InstanceType<typeof QSelect>>(); const requestListActionData = ref<RequestData[]>();
// NOTE: Variable // NOTE: Variable
const pageState = reactive({ const pageState = reactive({
@ -45,6 +48,8 @@ const pageState = reactive({
rejectCancelDialog: false, rejectCancelDialog: false,
rejectCancelReason: '', rejectCancelReason: '',
requestId: '', requestId: '',
requestListActionDialog: false,
searchDate: [],
}); });
const fieldSelectedOption = computed(() => { const fieldSelectedOption = computed(() => {
@ -60,6 +65,8 @@ async function fetchList(opts?: { rotateFlowId?: boolean }) {
query: pageState.inputSearch, query: pageState.inputSearch,
page: page.value, page: page.value,
pageSize: pageSize.value, pageSize: pageSize.value,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
requestDataStatus: requestDataStatus:
pageState.statusFilter === 'None' ? undefined : pageState.statusFilter, pageState.statusFilter === 'None' ? undefined : pageState.statusFilter,
// responsibleOnly: true, // responsibleOnly: true,
@ -131,6 +138,32 @@ async function submitRejectCancel() {
} }
} }
async function openRequestListDialog() {
const ret = await requestListStore.getRequestDataList({
page: 1,
pageSize: 999,
incomplete: true,
});
if (ret) {
requestListActionData.value = ret.result;
}
pageState.requestListActionDialog = true;
}
async function submitRequestListAction(data: {
form: { responsibleUserLocal: boolean; responsibleUserId: string };
selected: RequestData[];
}) {
const res = await requestListStore.updateMessenger(
data.selected.map((v) => v.id),
data.form.responsibleUserId,
);
if (res) pageState.requestListActionDialog = false;
}
onMounted(async () => { onMounted(async () => {
pageState.gridView = $q.screen.lt.md ? true : false; pageState.gridView = $q.screen.lt.md ? true : false;
navigatorStore.current.title = 'requestList.title'; navigatorStore.current.title = 'requestList.title';
@ -140,13 +173,27 @@ onMounted(async () => {
await fetchList({ rotateFlowId: true }); await fetchList({ rotateFlowId: true });
}); });
watch([() => pageState.inputSearch, () => pageState.statusFilter], () => { watch(
[
() => pageState.inputSearch,
() => pageState.statusFilter,
() => pageState.searchDate,
],
() => {
page.value = 1; page.value = 1;
data.value = []; data.value = [];
fetchList({ rotateFlowId: true }); fetchList({ rotateFlowId: true });
}); },
);
</script> </script>
<template> <template>
<FloatingActionButton
hide-icon
style="z-index: 999"
icon="mdi-account-outline"
@click.stop="openRequestListDialog"
></FloatingActionButton>
<div class="column full-height no-wrap"> <div class="column full-height no-wrap">
<!-- SEC: stat --> <!-- SEC: stat -->
<section class="text-body-2 q-mb-xs flex items-center"> <section class="text-body-2 q-mb-xs flex items-center">
@ -239,26 +286,62 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () => {
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="pageState.searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && pageState.statusFilter !== 'None'"
unelevated >
class="q-ml-sm" <div
padding="4px" v-if="$q.screen.lt.md"
size="sm" class="q-mt-sm text-weight-medium"
rounded >
@click="refFilter?.showPopup" {{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
v-model="pageState.statusFilter"
outlined
dense
option-value="value"
option-label="label"
map-options
emit-value
:for="'field-select-status'"
:options="[
{
label: $t('general.all'),
value: 'None',
},
{
label: $t('requestList.status.Pending'),
value: RequestDataStatus.Pending,
},
{
label: $t('requestList.status.Ready'),
value: RequestDataStatus.Ready,
},
{
label: $t('requestList.status.InProgress'),
value: RequestDataStatus.InProgress,
},
{
label: $t('requestList.status.Completed'),
value: RequestDataStatus.Completed,
},
{
label: $t('requestList.status.Canceled'),
value: RequestDataStatus.Canceled,
},
]"
/> />
</span> </AdvanceSearch>
</template> </template>
</q-input> </q-input>
<div class="row col-md-5" style="white-space: nowrap"> <div class="row col-md-5" style="white-space: nowrap">
<q-select <q-select
v-show="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
ref="refFilter"
v-model="pageState.statusFilter" v-model="pageState.statusFilter"
outlined outlined
dense dense
@ -477,7 +560,7 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () => {
@click="() => (pageState.rejectCancelDialog = false)" @click="() => (pageState.rejectCancelDialog = false)"
/> />
<SaveButton <SaveButton
label="ยืนยัน" :label="$t('general.confirm')"
class="q-ml-sm" class="q-ml-sm"
icon="mdi-check" icon="mdi-check"
solid solid
@ -485,6 +568,13 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () => {
/> />
</template> </template>
</DialogFormContainer> </DialogFormContainer>
<RequestListAction
v-if="requestListActionData"
v-model="pageState.requestListActionDialog"
:request-list="requestListActionData"
@submit="submitRequestListAction"
/>
</div> </div>
</template> </template>
<style></style> <style></style>

View file

@ -13,6 +13,7 @@ const props = defineProps<{
readonly?: boolean; readonly?: boolean;
step: Step; step: Step;
responsibleAreaDistrictId?: string; responsibleAreaDistrictId?: string;
defaultMessenger?: string;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -85,7 +86,8 @@ function assignToForm() {
companyDuty: attributesForm.value.companyDuty ?? false, companyDuty: attributesForm.value.companyDuty ?? false,
companyDutyCost: attributesForm.value.companyDutyCost ?? 30, companyDutyCost: attributesForm.value.companyDutyCost ?? 30,
responsibleUserLocal: attributesForm.value.responsibleUserLocal ?? true, responsibleUserLocal: attributesForm.value.responsibleUserLocal ?? true,
responsibleUserId: attributesForm.value.responsibleUserId ?? '', responsibleUserId:
attributesForm.value.responsibleUserId || props.defaultMessenger,
individualDuty: attributesForm.value.individualDuty ?? false, individualDuty: attributesForm.value.individualDuty ?? false,
individualDutyCost: attributesForm.value.individualDutyCost ?? 10, individualDutyCost: attributesForm.value.individualDutyCost ?? 10,
}), }),

View file

@ -0,0 +1,238 @@
<script setup lang="ts">
import { reactive, ref, watch } from 'vue';
import { RequestData } from 'src/stores/request-list';
import { DialogContainer, DialogHeader } from 'src/components/dialog';
import {
BackButton,
CancelButton,
MainButton,
SaveButton,
} from 'src/components/button';
import FormResponsibleUser from './FormResponsibleUser.vue';
import FormGroupHead from './FormGroupHead.vue';
import TableRequestList from './TableRequestList.vue';
import { column } from './constants';
import useAddressStore from 'src/stores/address';
defineProps<{
requestList: RequestData[];
}>();
defineEmits<{
(
e: 'submit',
data: {
form: { responsibleUserLocal: boolean; responsibleUserId: string };
selected: RequestData[];
},
): void;
}>();
enum Step {
RequestList = 1,
Configure = 2,
}
const open = defineModel<boolean>({ default: false });
const step = ref<Step>(Step.RequestList);
const selected = ref<RequestData[]>([]);
const listSameArea = ref<string[]>([]);
const form = reactive({
responsibleUserLocal: false,
responsibleUserId: '',
});
function reset() {
step.value = Step.RequestList;
selected.value = [];
form.responsibleUserLocal = false;
form.responsibleUserId = '';
}
function prev() {
step.value = Step.RequestList;
}
watch(
() => selected.value,
async () => {
if (selected.value.length === 1) {
const districtId = selected.value[0].quotation.customerBranch.districtId;
const ret = await useAddressStore().listSameOfficeArea(districtId);
if (ret) listSameArea.value = ret;
}
},
);
</script>
<template>
<DialogContainer v-model="open" :onOpen="reset">
<template #header>
<DialogHeader :title="$t('requestList.action.title')" />
</template>
<div class="surface-0 q-pa-md">
<div class="stepper-wrapper">
<div class="stepper">
<template
v-for="(label, i) in [
$t('menu.product'),
$t('requestList.action.configure'),
]"
:key="i"
>
<span class="step" :class="{ ['step__active']: step > i }">
<span class="step-outer"><span class="step-inner" /></span>
<span class="step-label">{{ label }}</span>
</span>
<span
class="step-connector"
:class="{ ['step-connector__active']: step > i + 1 }"
/>
</template>
</div>
</div>
</div>
<div class="surface-1 q-pa-md col full-width scroll">
<TableRequestList
v-if="step === Step.RequestList"
v-model:selected="selected"
hide-action
hide-view
checkable
:list-same-area="listSameArea"
:columns="column"
:rows="requestList"
:visible-columns="[...column.map((col) => col.name)]"
/>
<template v-if="step === Step.Configure">
<q-expansion-item
dense
class="overflow-hidden bordered full-width"
switch-toggle-side
style="border-radius: var(--radius-2)"
expand-icon="mdi-chevron-down-circle"
header-class="surface-1 q-py-sm text-medium text-body1"
default-opened
>
<template #header>
<span>
{{ $t('requestList.employeeMessenger') }}
</span>
</template>
<FormGroupHead>
{{
$t('general.select', { msg: $t('requestList.employeeMessenger') })
}}
</FormGroupHead>
<FormResponsibleUser
:district-id="listSameArea[0]"
v-model:responsible-user-id="form.responsibleUserId"
v-model:responsible-user-local="form.responsibleUserLocal"
/>
</q-expansion-item>
</template>
</div>
<template #footer>
<div class="q-gutter-x-xs q-ml-auto">
<CancelButton
v-if="step === Step.RequestList"
id="btn-cancel"
outlined
@click="
reset();
open = false;
"
/>
<BackButton
v-if="step === Step.Configure"
id="btn-back"
outlined
@click="prev"
/>
<MainButton
icon="mdi-check"
color="207 96% 32%"
solid
id="btn-next"
v-if="step === Step.RequestList"
@click="step = Step.Configure"
>
{{ $t('general.next') }}
</MainButton>
<SaveButton
v-if="step === Step.Configure"
id="btn-save"
solid
@click="$emit('submit', { form, selected })"
/>
</div>
</template>
</DialogContainer>
</template>
<style lang="scss" scoped>
.stepper {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
margin-inline: 25%;
& > .step {
--__color: var(--gray-5);
display: flex;
flex-direction: column;
align-items: center;
position: relative;
gap: 0.25rem;
&.step__active {
--__color: var(--brand-1);
}
& > .step-label {
position: absolute;
font-weight: 600;
color: var(--__color);
white-space: nowrap;
top: 2rem;
}
& > .step-outer {
display: inline-flex;
border: 2px solid var(--__color);
border-radius: 50%;
width: 1.5rem;
height: 1.5rem;
align-items: center;
justify-content: center;
& > .step-inner {
display: inline-block;
border-radius: 50%;
background-color: var(--__color);
width: 0.7rem;
height: 0.7rem;
}
}
}
& > .step-connector {
display: block;
border-bottom: 2px solid var(--gray-5);
flex-grow: 1;
&.step-connector__active {
border-color: var(--brand-1);
}
&:last-child {
display: none;
}
}
}
</style>

View file

@ -54,6 +54,7 @@ import { Invoice } from 'src/stores/payment/types';
import { CreatedBy } from 'src/stores/types'; import { CreatedBy } from 'src/stores/types';
import { getUserId } from 'src/services/keycloak'; import { getUserId } from 'src/services/keycloak';
import { QuotationFull } from 'src/stores/quotations/types'; import { QuotationFull } from 'src/stores/quotations/types';
import useUserStore from 'src/stores/user';
const { locale, t } = useI18n(); const { locale, t } = useI18n();
@ -62,7 +63,9 @@ const route = useRoute();
const optionStore = useOptionStore(); const optionStore = useOptionStore();
const requestListStore = useRequestList(); const requestListStore = useRequestList();
const flowTemplateStore = useWorkflowTemplate(); const flowTemplateStore = useWorkflowTemplate();
const userStore = useUserStore();
const currentUserGroup = ref<string[]>([]);
const workList = ref<RequestWork[]>([]); const workList = ref<RequestWork[]>([]);
const statusFile = ref<Attributes>({ const statusFile = ref<Attributes>({
customer: {}, customer: {},
@ -158,6 +161,10 @@ onMounted(async () => {
initTheme(); initTheme();
initLang(); initLang();
const result = await userStore.fetchUserGroup();
currentUserGroup.value = result.map((v) => v.name);
// get data // get data
await getData(); await getData();
}); });
@ -283,26 +290,38 @@ async function triggerViewFile(opt: {
if (!opt.download) window.open(url, '_blank'); if (!opt.download) window.open(url, '_blank');
} }
const responsiblePersonList = computed(() => { const responsibleList = computed(() => {
const temp = workList.value?.reduce<Record<string, CreatedBy[]>>( const temp = workList.value?.reduce<
(acc, curr: RequestWork) => { Record<string, { user: CreatedBy[]; group: string[] }>
>((acc, curr: RequestWork) => {
curr.productService.service?.workflow?.step.forEach((v) => { curr.productService.service?.workflow?.step.forEach((v) => {
const key = v.order.toString(); const key = v.order.toString();
const responsibleGroup = (
v.responsibleGroup as unknown as { group: string }[]
).map((v) => v.group);
if (!acc[key]) acc[key] = v.responsiblePerson.map((v) => v.user); if (!acc[key]) {
acc[key] = {
user: v.responsiblePerson.map((v) => v.user),
group: responsibleGroup,
};
}
const current = acc[key]; const current = acc[key];
v.responsiblePerson.forEach((lhs) => { v.responsiblePerson.forEach((lhs) => {
if (current.find((rhs) => rhs.id === lhs.userId)) return; if (current.user.find((rhs) => rhs.id === lhs.userId)) return;
current.push(lhs.user); current.user.push(lhs.user);
});
responsibleGroup.forEach((lhs) => {
if (current.group.find((rhs) => rhs === lhs)) return;
current.group.push(lhs);
}); });
}); });
return acc; return acc;
}, }, {});
{},
);
return temp || {}; return temp || {};
}); });
@ -438,6 +457,24 @@ async function submitRejectCancel() {
pageState.rejectCancelDialog = false; pageState.rejectCancelDialog = false;
} }
} }
function toCustomer(customer: RequestData['quotation']['customerBranch']) {
const url = new URL(
`/customer-management?tab=customer&id=${customer.customerId}`,
window.location.origin,
);
window.open(url.toString(), '_blank');
}
function toEmployee(employee: RequestData['employee']) {
const url = new URL(
`/customer-management?tab=employee&id=${employee.id}`,
window.location.origin,
);
window.open(url.toString(), '_blank');
}
</script> </script>
<template> <template>
<div class="column surface-0 fullscreen" v-if="data"> <div class="column surface-0 fullscreen" v-if="data">
@ -478,11 +515,11 @@ async function submitRejectCancel() {
<span class="app-text-muted">{{ $t('flow.responsiblePerson') }}</span> <span class="app-text-muted">{{ $t('flow.responsiblePerson') }}</span>
<span> <span>
<template <template
v-if="responsiblePersonList[pageState.currentStep]?.length" v-if="responsibleList[pageState.currentStep]?.user.length"
> >
<AvatarGroup <AvatarGroup
:data=" :data="[
(responsiblePersonList[pageState.currentStep] || []).map( ...(responsibleList[pageState.currentStep].user || []).map(
(v) => ({ (v) => ({
name: name:
$i18n.locale === 'eng' $i18n.locale === 'eng'
@ -494,8 +531,12 @@ async function submitRejectCancel() {
: `/no-img-female.png` : `/no-img-female.png`
: `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`, : `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`,
}), }),
) ),
" ...responsibleList[pageState.currentStep].group.map((g) => ({
name: `${$t('general.group')} ${g}`,
imgUrl: '/img-group.png',
})),
]"
/> />
</template> </template>
<template v-else>-</template> <template v-else>-</template>
@ -701,6 +742,7 @@ async function submitRejectCancel() {
}" }"
> >
<DataDisplay <DataDisplay
clickable
class="col" class="col"
icon="mdi-account-settings-outline" icon="mdi-account-settings-outline"
:label="$t('customer.employer')" :label="$t('customer.employer')"
@ -710,8 +752,10 @@ async function submitRejectCancel() {
noCode: true, noCode: true,
}) || '-' }) || '-'
" "
@label-click="toCustomer(data.quotation.customerBranch)"
/> />
<DataDisplay <DataDisplay
clickable
class="col" class="col"
icon="mdi-account-settings-outline" icon="mdi-account-settings-outline"
:label="$t('customer.employee')" :label="$t('customer.employee')"
@ -720,6 +764,7 @@ async function submitRejectCancel() {
locale: $i18n.locale, locale: $i18n.locale,
}) || '-' }) || '-'
" "
@label-click="toEmployee(data.employee)"
/> />
<DataDisplay <DataDisplay
class="col" class="col"
@ -776,11 +821,15 @@ async function submitRejectCancel() {
:cancel="data.requestDataStatus === RequestDataStatus.Canceled" :cancel="data.requestDataStatus === RequestDataStatus.Canceled"
:readonly=" :readonly="
data.requestDataStatus === RequestDataStatus.Canceled || data.requestDataStatus === RequestDataStatus.Canceled ||
(responsiblePersonList && (responsibleList &&
!!responsiblePersonList[pageState.currentStep]?.length && !responsibleList[pageState.currentStep]?.user.find(
!responsiblePersonList[pageState.currentStep]?.find(
(v) => v.id === getUserId(), (v) => v.id === getUserId(),
)) ) &&
!responsibleList[pageState.currentStep]?.group.some((v) =>
currentUserGroup.includes(v),
)) ||
(!!responsibleList[pageState.currentStep]?.user?.length &&
!!responsibleList[pageState.currentStep]?.user?.length)
" "
:order-able="value._messengerExpansion" :order-able="value._messengerExpansion"
:installment-info="getInstallmentInfo()" :installment-info="getInstallmentInfo()"
@ -873,6 +922,11 @@ async function submitRejectCancel() {
:readonly=" :readonly="
data.requestDataStatus === RequestDataStatus.Canceled data.requestDataStatus === RequestDataStatus.Canceled
" "
:default-messenger="
value.stepStatus[pageState.currentStep - 1]
? undefined
: data.defaultMessengerId
"
:step="{ :step="{
step: pageState.currentStep, step: pageState.currentStep,
requestWorkId: value.id || '', requestWorkId: value.id || '',
@ -906,6 +960,7 @@ async function submitRejectCancel() {
/> />
<FormExpansion <FormExpansion
v-if="value._formExpansion" v-if="value._formExpansion"
:request-work-id="value.id"
:readonly=" :readonly="
data.requestDataStatus === RequestDataStatus.Canceled data.requestDataStatus === RequestDataStatus.Canceled
" "

View file

@ -13,6 +13,7 @@ import useOptionStore from 'src/stores/options';
import KebabAction from 'src/components/shared/KebabAction.vue'; import KebabAction from 'src/components/shared/KebabAction.vue';
import { CreatedBy } from 'src/stores/types'; import { CreatedBy } from 'src/stores/types';
import { dateFormatJS } from 'src/utils/datetime';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -21,6 +22,9 @@ const props = withDefaults(
grid?: boolean; grid?: boolean;
visibleColumns?: string[]; visibleColumns?: string[];
hideAction?: boolean; hideAction?: boolean;
hideView?: boolean;
checkable?: boolean;
listSameArea?: string[];
}>(), }>(),
{ {
row: () => [], row: () => [],
@ -36,9 +40,16 @@ defineEmits<{
(e: 'rejectCancel', data: RequestData): void; (e: 'rejectCancel', data: RequestData): void;
}>(); }>();
function responsiblePerson(quotation: QuotationFull): CreatedBy[] | undefined { const selected = defineModel<RequestData[]>('selected');
function responsiblePerson(quotation: QuotationFull) {
const productServiceList = quotation.productServiceList; const productServiceList = quotation.productServiceList;
const tempPerson: CreatedBy[] = []; const tempPerson: CreatedBy[] = [];
const tempGroup: {
group: string;
id: string;
workflowTemplateStepId: string;
}[] = [];
for (const v of productServiceList) { for (const v of productServiceList) {
const tempStep = v.service?.workflow?.step; const tempStep = v.service?.workflow?.step;
@ -49,7 +60,17 @@ function responsiblePerson(quotation: QuotationFull): CreatedBy[] | undefined {
tempPerson.push(rhs.user); tempPerson.push(rhs.user);
} }
}); });
return tempPerson; tempStep.forEach((lhs) => {
const newGroup = lhs.responsibleGroup as unknown as {
group: string;
id: string;
workflowTemplateStepId: string;
}[];
for (const rhs of newGroup) {
tempGroup.push(rhs);
}
});
return { user: tempPerson, group: tempGroup };
} }
} }
@ -92,10 +113,39 @@ function getEmployeeName(
return ( return (
{ {
['eng']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstNameEN} ${employee?.lastNameEN}`, ['eng']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstNameEN} ${employee?.lastNameEN}`,
['tha']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstName} ${employee?.lastName}`, ['tha']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstName || employee?.firstNameEN} ${employee?.lastName || employee?.lastNameEN}`,
}[opts?.locale || 'eng'] || '-' }[opts?.locale || 'eng'] || '-'
); );
} }
function toCustomer(customer: RequestData['quotation']['customerBranch']) {
const url = new URL(
`/customer-management?tab=customer&id=${customer.customerId}`,
window.location.origin,
);
window.open(url.toString(), '_blank');
}
function toEmployee(employee: RequestData['employee']) {
const url = new URL(
`/customer-management?tab=employee&id=${employee.id}`,
window.location.origin,
);
window.open(url.toString(), '_blank');
}
function handleCheckAll() {
const filteredRows = props.rows.filter((row) =>
props.listSameArea?.includes(row.quotation.customerBranch.districtId),
);
if (selected.value.length === filteredRows.length) {
selected.value = [];
} else {
selected.value = filteredRows;
}
}
</script> </script>
<template> <template>
<q-table <q-table
@ -106,12 +156,36 @@ function getEmployeeName(
card-container-class="q-col-gutter-sm" card-container-class="q-col-gutter-sm"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
class="full-width" class="full-width"
selection="multiple"
v-model:selected="selected"
:selected-rows-label="
(n) =>
$t('general.selected', {
number: n,
msg: $t('general.list'),
})
"
:no-data-label="$t('general.noDataTable')"
> >
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr <q-tr
style="background-color: hsla(var(--info-bg) / 0.07)" style="background-color: hsla(var(--info-bg) / 0.07)"
:props="props" :props="props"
> >
<q-th v-if="checkable">
<q-checkbox
v-if="selected.length > 0"
:model-value="
selected.length ===
rows.filter((row) =>
listSameArea?.includes(row.quotation.customerBranch.districtId),
).length
"
size="sm"
@click="handleCheckAll"
/>
<div v-else style="width: 35px; height: 35px"></div>
</q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label && $t(col.label) }} {{ col.label && $t(col.label) }}
</q-th> </q-th>
@ -125,9 +199,30 @@ function getEmployeeName(
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>" } & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
> >
<q-tr <q-tr
:class="{ urgent: props.row.quotation.urgent, dark: $q.dark.isActive }" :class="{
urgent: props.row.quotation.urgent,
dark: $q.dark.isActive,
'disabled-row':
selected &&
selected.length > 0 &&
!listSameArea.includes(
props.row.quotation.customerBranch.districtId,
),
}"
class="text-center" class="text-center"
> >
<q-td v-if="checkable">
<q-checkbox
:disable="
selected.length > 0 &&
!listSameArea.includes(
props.row.quotation.customerBranch.districtId,
)
"
v-model="props.selected"
size="sm"
/>
</q-td>
<q-td v-if="visibleColumns.includes('order')"> <q-td v-if="visibleColumns.includes('order')">
{{ props.rowIndex + 1 }} {{ props.rowIndex + 1 }}
</q-td> </q-td>
@ -138,21 +233,50 @@ function getEmployeeName(
</div> </div>
</q-td> </q-td>
<q-td v-if="visibleColumns.includes('employer')" class="text-left"> <q-td v-if="visibleColumns.includes('employer')" class="text-left">
<span
class="link"
@click="toCustomer(props.row.quotation.customerBranch)"
>
{{ {{
getCustomerName(props.row, { getCustomerName(props.row, {
noCode: true, noCode: true,
locale: $i18n.locale, locale: $i18n.locale,
}) || '-' }) || '-'
}} }}
</span>
</q-td> </q-td>
<q-td v-if="visibleColumns.includes('employee')" class="text-left"> <q-td v-if="visibleColumns.includes('employee')" class="text-left">
<span class="link" @click="toEmployee(props.row.employee)">
{{ getEmployeeName(props.row, { locale: $i18n.locale }) || '-' }} {{ getEmployeeName(props.row, { locale: $i18n.locale }) || '-' }}
</span>
</q-td> </q-td>
<q-td
v-if="visibleColumns.includes('employeePassport')"
class="text-left"
>
{{
props.row.employee.employeePassport.length !== 0
? props.row.employee.employeePassport[0].number
: '-'
}}
</q-td>
<q-td v-if="visibleColumns.includes('dataOffice')" class="text-left">
{{
$i18n.locale === 'eng'
? props.row.dataOffice.nameEN
: props.row.dataOffice.name
}}
</q-td>
<q-td v-if="visibleColumns.includes('createdAt')" class="text-left">
{{ dateFormatJS({ date: props.row.createdAt }) }}
</q-td>
<q-td v-if="visibleColumns.includes('quotationCode')"> <q-td v-if="visibleColumns.includes('quotationCode')">
{{ props.row.quotation.code || '-' }} {{ props.row.quotation.code || '-' }}
</q-td> </q-td>
<q-td v-if="visibleColumns.includes('responsiblePerson')"> <q-td v-if="visibleColumns.includes('responsiblePerson')">
<AvatarGroup <!-- <AvatarGroup
:data=" :data="
responsiblePerson(props.row.quotation)?.map((v) => { responsiblePerson(props.row.quotation)?.map((v) => {
return { return {
@ -168,7 +292,26 @@ function getEmployeeName(
}; };
}) })
" "
/> /> -->
<AvatarGroup
:data="[
...responsiblePerson(props.row.quotation).user.map((v) => ({
name:
$i18n.locale === 'eng'
? `${v.firstNameEN} ${v.lastNameEN}`
: `${v.firstName} ${v.lastName}`,
imgUrl: !v.selectedImage
? v.gender === 'male'
? `/no-img-man.png`
: `/no-img-female.png`
: `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`,
})),
...responsiblePerson(props.row.quotation).group.map((g) => ({
name: `${$t('general.group')} ${g.group}`,
imgUrl: '/img-group.png',
})),
]"
></AvatarGroup>
</q-td> </q-td>
<q-td v-if="visibleColumns.includes('status')"> <q-td v-if="visibleColumns.includes('status')">
<BadgeComponent <BadgeComponent
@ -215,6 +358,7 @@ function getEmployeeName(
</q-td> </q-td>
<q-td class="text-right"> <q-td class="text-right">
<q-btn <q-btn
v-if="!hideView"
:id="`btn-eye-${props.row.code}`" :id="`btn-eye-${props.row.code}`"
icon="mdi-eye-outline" icon="mdi-eye-outline"
size="sm" size="sm"
@ -313,10 +457,14 @@ function getEmployeeName(
</div> </div>
<div class="col-8"> <div class="col-8">
<AvatarGroup <AvatarGroup
v-if="(responsiblePerson(props.row.quotation) ?? []).length > 0" v-if="
:data=" (responsiblePerson(props.row.quotation).user ?? []).length >
responsiblePerson(props.row.quotation)?.map((v) => { 0 ||
return { (responsiblePerson(props.row.quotation).group ?? []).length >
0
"
:data="[
...responsiblePerson(props.row.quotation).user.map((v) => ({
name: name:
$i18n.locale === 'eng' $i18n.locale === 'eng'
? `${v.firstNameEN} ${v.lastNameEN}` ? `${v.firstNameEN} ${v.lastNameEN}`
@ -326,9 +474,12 @@ function getEmployeeName(
? `/no-img-man.png` ? `/no-img-man.png`
: `/no-img-female.png` : `/no-img-female.png`
: `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`, : `${baseUrl}/user/${v.id}/profile-image/${v.selectedImage}`,
}; })),
}) ...responsiblePerson(props.row.quotation).group.map((g) => ({
" name: `${$t('general.group')} ${g.group}`,
imgUrl: '/img-group.png',
})),
]"
/> />
<span v-else>-</span> <span v-else>-</span>
</div> </div>
@ -406,4 +557,15 @@ function getEmployeeName(
background: var(--red-8); background: var(--red-8);
} }
} }
.link {
color: hsl(var(--info-bg));
text-decoration: underline;
cursor: pointer;
}
.disabled-row {
opacity: 0.3;
filter: grayscale(1);
}
</style> </style>

View file

@ -28,6 +28,24 @@ export const column = [
label: 'customer.employee', label: 'customer.employee',
field: 'employee', field: 'employee',
}, },
{
name: 'employeePassport',
align: 'center',
label: 'customerEmployee.form.passportNo',
field: 'employeePassport',
},
{
name: 'dataOffice',
align: 'center',
label: 'requestList.dataOffice',
field: 'dataOffice',
},
{
name: 'createdAt',
align: 'center',
label: 'general.createdAt',
field: 'createdAt',
},
{ {
name: 'quotationCode', name: 'quotationCode',

View file

@ -27,6 +27,7 @@ import useFlowStore from 'src/stores/flow';
import { pageTabs, column, pageTabsReceive } from './constants'; import { pageTabs, column, pageTabsReceive } from './constants';
import { dialogWarningClose, isRoleInclude } from 'src/stores/utils'; import { dialogWarningClose, isRoleInclude } from 'src/stores/utils';
import { PaginationResult } from 'src/types'; import { PaginationResult } from 'src/types';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { t } = useI18n(); const { t } = useI18n();
const $q = useQuasar(); const $q = useQuasar();
@ -48,6 +49,7 @@ const pageState = reactive({
isMessenger: isRoleInclude(['messenger']), isMessenger: isRoleInclude(['messenger']),
receiveDialog: false, receiveDialog: false,
isReceiveScan: false, isReceiveScan: false,
searchDate: [],
}); });
const taskOrderList = ref<TaskOrder[]>([]); const taskOrderList = ref<TaskOrder[]>([]);
@ -69,6 +71,8 @@ async function fetchTaskOrderList(opts?: { page?: number; pageSize?: number }) {
pageSize: opts?.pageSize || pageSize.value, pageSize: opts?.pageSize || pageSize.value,
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch, query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
userTaskStatus: pageState.currentTab as UserTaskStatus, userTaskStatus: pageState.currentTab as UserTaskStatus,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
}); });
} else { } else {
res = await taskOrderStore.getTaskOrderList({ res = await taskOrderStore.getTaskOrderList({
@ -76,6 +80,8 @@ async function fetchTaskOrderList(opts?: { page?: number; pageSize?: number }) {
pageSize: opts?.pageSize || pageSize.value, pageSize: opts?.pageSize || pageSize.value,
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch, query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
taskOrderStatus: pageState.currentTab as TaskOrderStatus | undefined, taskOrderStatus: pageState.currentTab as TaskOrderStatus | undefined,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
}); });
} }
if (res) { if (res) {
@ -157,6 +163,7 @@ watch(
() => pageState.inputSearch, () => pageState.inputSearch,
() => pageSize.value, () => pageSize.value,
() => pageState.statusFilter, () => pageState.statusFilter,
() => pageState.searchDate,
], ],
() => { () => {
fetchTaskOrderList(); fetchTaskOrderList();
@ -299,6 +306,10 @@ watch(
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch v-model="pageState.searchDate" />
</template>
</q-input> </q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap"> <div class="row col-md-5 justify-end" style="white-space: nowrap">

View file

@ -160,7 +160,12 @@ const emit = defineEmits<{
</q-tooltip> </q-tooltip>
</div> </div>
<div class="text-caption app-text-muted"> <div class="text-caption app-text-muted">
{{ props.row.code || '-' }} {{
(props.row.taskOrderStatus === TaskOrderStatus.Complete &&
props.row.codeProductReceived
? props.row.codeProductReceived
: props.row.code) || '-'
}}
</div> </div>
</q-td> </q-td>
<q-td v-if="visibleColumns.includes('issueBranch')"> <q-td v-if="visibleColumns.includes('issueBranch')">

View file

@ -227,6 +227,12 @@ export const productColumn = [
label: 'taskOrder.productList', label: 'taskOrder.productList',
field: 'productList', field: 'productList',
}, },
{
name: 'status',
align: 'center',
label: 'general.status',
field: 'status',
},
{ {
name: 'amountOfEmployee', name: 'amountOfEmployee',
align: 'center', align: 'center',

View file

@ -294,7 +294,7 @@ function closeAble() {
:branch="branch" :branch="branch"
:institution="data.institution" :institution="data.institution"
:details="{ :details="{
code: data.code, code: data.codeProductReceived ?? data.code,
name: data.taskName, name: data.taskName,
contactName: data.contactName, contactName: data.contactName,
contactTel: data.contactTel, contactTel: data.contactTel,

View file

@ -11,6 +11,8 @@ import { baseUrl, formatNumberDecimal, commaInput } from 'src/stores/utils';
import { precisionRound } from 'src/utils/arithmetic'; import { precisionRound } from 'src/utils/arithmetic';
import { useConfigStore } from 'stores/config'; import { useConfigStore } from 'stores/config';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import BadgeComponent from 'src/components/BadgeComponent.vue';
import { TaskStatus } from 'src/stores/task-order/types';
const currentBtnOpen = ref<boolean[]>([]); const currentBtnOpen = ref<boolean[]>([]);
const configStore = useConfigStore(); const configStore = useConfigStore();
@ -30,7 +32,10 @@ const props = defineProps<{
readonly?: boolean; readonly?: boolean;
agentPrice?: boolean; agentPrice?: boolean;
taskList: { taskList: {
product: RequestWork['productService']['product']; product: RequestWork['productService']['product'] & {
taskStatus?: TaskStatus;
totalNotStatusComplete?: number;
};
list: RequestWork[]; list: RequestWork[];
}[]; }[];
creditNote?: boolean; creditNote?: boolean;
@ -111,6 +116,26 @@ function calcPrice(
return precisionRound(priceNoVat * amount + rawVatTotal); return precisionRound(priceNoVat * amount + rawVatTotal);
} }
function taskOrderStatus(value: TaskStatus) {
if ([TaskStatus.Pending].includes(value)) {
return '--blue-6-hsl';
}
if ([TaskStatus.InProgress, TaskStatus.Validate].includes(value)) {
return '--orange-5-hsl';
}
if (
[
TaskStatus.Canceled,
TaskStatus.Restart,
TaskStatus.Redo,
TaskStatus.Failed,
].includes(value)
) {
return '--red-5-hsl';
}
return '--green-8-hsl';
}
</script> </script>
<template> <template>
<q-expansion-item <q-expansion-item
@ -144,7 +169,8 @@ function calcPrice(
(v) => (v) =>
v.name !== 'discount' && v.name !== 'discount' &&
v.name !== 'priceBeforeVat' && v.name !== 'priceBeforeVat' &&
v.name !== 'vat', v.name !== 'vat' &&
v.name !== 'status',
) )
: productColumn : productColumn
" "
@ -173,7 +199,10 @@ function calcPrice(
<template <template
v-slot:body="props: { v-slot:body="props: {
row: { row: {
product: RequestWork['productService']['product']; product: RequestWork['productService']['product'] & {
taskStatus?: TaskStatus;
totalNotStatusComplete?: number;
};
list: RequestWork[]; list: RequestWork[];
}; };
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>" } & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
@ -203,6 +232,14 @@ function calcPrice(
</q-avatar> </q-avatar>
{{ props.row.product.name }} {{ props.row.product.name }}
</q-td> </q-td>
<q-td class="text-left" v-if="!creditNote">
<BadgeComponent
hide-icon
:hsla-color="taskOrderStatus(props.row.product.taskStatus)"
:title="`${$t(`taskOrder.status.${props.row.product.taskStatus}`)} ${!!props.row.product.totalNotStatusComplete ? $t('general.totalPeople', { meg: props.row.product.totalNotStatusComplete }) : ''}`"
/>
</q-td>
<q-td> <q-td>
{{ props.row.list.length }} {{ props.row.list.length }}
</q-td> </q-td>

View file

@ -280,7 +280,10 @@ let taskListGroup = computed(() => {
const cacheData = currentFormData.value.taskList.reduce< const cacheData = currentFormData.value.taskList.reduce<
{ {
product: RequestWork['productService']['product']; product: RequestWork['productService']['product'] & {
taskStatus?: TaskStatus;
totalNotStatusComplete?: number;
};
list: (RequestWork & { list: (RequestWork & {
_template?: { _template?: {
id: string; id: string;
@ -289,15 +292,15 @@ let taskListGroup = computed(() => {
step: number; step: number;
responsibleInstitution: (string | { group: string })[]; responsibleInstitution: (string | { group: string })[];
} | null; } | null;
taskStatus?: TaskStatus;
failedComment?: string;
failedType?: string;
})[]; })[];
}[] }[]
>((acc, curr) => { >((acc, curr) => {
if ( const isNotComplete =
fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.Complete && fullTaskOrder.value?.taskOrderStatus === TaskOrderStatus.Complete &&
curr.taskStatus !== TaskStatus.Complete curr.taskStatus !== TaskStatus.Complete;
) {
return acc;
}
const task = curr.requestWorkStep; const task = curr.requestWorkStep;
const step = curr.step; const step = curr.step;
@ -308,9 +311,18 @@ let taskListGroup = computed(() => {
let exist = acc.find( let exist = acc.find(
(item) => task.requestWork.productService.productId == item.product.id, (item) => task.requestWork.productService.productId == item.product.id,
); );
const record = Object.assign(task.requestWork, {
const record = Object.assign(
{
...task.requestWork,
taskStatus: curr.taskStatus,
failedComment: curr.failedComment || '',
failedType: curr.failedType || '',
},
{
_template: getTemplateData(task.requestWork, step), _template: getTemplateData(task.requestWork, step),
}); },
);
const template = getTemplateData(task.requestWork, step); const template = getTemplateData(task.requestWork, step);
@ -323,10 +335,18 @@ let taskListGroup = computed(() => {
} }
if (exist) { if (exist) {
exist.list.push(task.requestWork); exist.list.push(record);
if (isNotComplete) {
exist.product.totalNotStatusComplete =
(exist.product.totalNotStatusComplete || undefined) + 1;
}
} else { } else {
acc.push({ acc.push({
product: task.requestWork.productService.product, product: {
...task.requestWork.productService.product,
taskStatus: curr.taskStatus || TaskStatus.Pending,
totalNotStatusComplete: isNotComplete ? 1 : undefined,
},
list: [record], list: [record],
}); });
} }
@ -897,9 +917,14 @@ watch(
v-model:registered-branch-id="currentFormData.registeredBranchId" v-model:registered-branch-id="currentFormData.registeredBranchId"
v-model:institution-id="currentFormData.institutionId" v-model:institution-id="currentFormData.institutionId"
v-model:task-name="currentFormData.taskName" v-model:task-name="currentFormData.taskName"
v-model:code="currentFormData.code"
v-model:contact-name="currentFormData.contactName" v-model:contact-name="currentFormData.contactName"
v-model:contact-tel="currentFormData.contactTel" v-model:contact-tel="currentFormData.contactTel"
:code="
view === TaskOrderStatus.Complete &&
currentFormData.codeProductReceived
? currentFormData.codeProductReceived
: currentFormData.code
"
:task-list-group=" :task-list-group="
taskListGroup.length === 0 && state.mode === 'create' taskListGroup.length === 0 && state.mode === 'create'
" "
@ -985,6 +1010,7 @@ watch(
" "
/> />
<!-- TODO: blind remark, urgent --> <!-- TODO: blind remark, urgent -->
{{ console.log(taskListGroup) }}
<RemarkExpansion <RemarkExpansion
v-if=" v-if="
view === TaskOrderStatus.Pending || view === TaskOrderStatus.Pending ||

View file

@ -2,7 +2,7 @@
// NOTE: Library // NOTE: Library
import { computed, onMounted, reactive, ref, watch } from 'vue'; import { computed, onMounted, reactive, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { QSelect, useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
// NOTE: Components // NOTE: Components
import StatCardComponent from 'src/components/StatCardComponent.vue'; import StatCardComponent from 'src/components/StatCardComponent.vue';
@ -18,13 +18,13 @@ import { columns, hslaColors } from './constants';
import useFlowStore from 'src/stores/flow'; import useFlowStore from 'src/stores/flow';
import { useInvoice } from 'src/stores/payment'; import { useInvoice } from 'src/stores/payment';
import { Invoice, PaymentDataStatus } from 'src/stores/payment/types'; import { Invoice, PaymentDataStatus } from 'src/stores/payment/types';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const $q = useQuasar(); const $q = useQuasar();
const navigatorStore = useNavigator(); const navigatorStore = useNavigator();
const flowStore = useFlowStore(); const flowStore = useFlowStore();
const invoiceStore = useInvoice(); const invoiceStore = useInvoice();
const { data, stats, page, pageMax, pageSize } = storeToRefs(invoiceStore); const { data, stats, page, pageMax, pageSize } = storeToRefs(invoiceStore);
const refFilter = ref<InstanceType<typeof QSelect>>();
// NOTE: Variable // NOTE: Variable
const pageState = reactive({ const pageState = reactive({
@ -34,6 +34,7 @@ const pageState = reactive({
fieldSelected: [...columns.map((v) => v.name)], fieldSelected: [...columns.map((v) => v.name)],
gridView: false, gridView: false,
total: 0, total: 0,
searchDate: [],
}); });
const fieldSelectedOption = computed(() => { const fieldSelectedOption = computed(() => {
@ -56,6 +57,8 @@ async function fetchList(opts?: { rotateFlowId?: boolean }) {
: undefined, : undefined,
quotationOnly: true, quotationOnly: true,
debitNoteOnly: false, debitNoteOnly: false,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
}); });
if (ret) { if (ret) {
data.value = $q.screen.xs ? [...data.value, ...ret.result] : ret.result; data.value = $q.screen.xs ? [...data.value, ...ret.result] : ret.result;
@ -89,8 +92,6 @@ function triggerView(opts: { quotationId: string }) {
} }
function viewDocExample(quotationId: string, codeInvoice: string) { function viewDocExample(quotationId: string, codeInvoice: string) {
console.log(codeInvoice);
localStorage.setItem( localStorage.setItem(
'quotation-preview', 'quotation-preview',
JSON.stringify({ JSON.stringify({
@ -124,6 +125,7 @@ watch(
() => pageState.inputSearch, () => pageState.inputSearch,
() => pageState.statusFilter, () => pageState.statusFilter,
() => pageSize.value, () => pageSize.value,
() => pageState.searchDate,
], ],
() => { () => {
page.value = 1; page.value = 1;
@ -207,26 +209,50 @@ watch(
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="pageState.searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && pageState.statusFilter !== 'None'"
unelevated >
class="q-ml-sm" <div
padding="4px" v-if="$q.screen.lt.md"
size="sm" class="q-mt-sm text-weight-medium"
rounded >
@click="refFilter?.showPopup" {{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
v-model="pageState.statusFilter"
outlined
dense
option-value="value"
option-label="label"
map-options
emit-value
:for="'field-select-status'"
:options="[
{
label: $t('general.all'),
value: 'None',
},
{
label: $t('invoice.status.PaymentWait'),
value: PaymentDataStatus.Wait,
},
{
label: $t('invoice.status.PaymentSuccess'),
value: PaymentDataStatus.Success,
},
]"
/> />
</span> </AdvanceSearch>
</template> </template>
</q-input> </q-input>
<div class="row col-md-5" style="white-space: nowrap"> <div class="row col-md-5" style="white-space: nowrap">
<q-select <q-select
v-show="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
ref="refFilter"
v-model="pageState.statusFilter" v-model="pageState.statusFilter"
outlined outlined
dense dense

View file

@ -24,6 +24,7 @@ import { pageTabs, columns, hslaColors } from './constants';
import { CreditNoteStatus, useCreditNote } from 'src/stores/credit-note'; import { CreditNoteStatus, useCreditNote } from 'src/stores/credit-note';
import TableCreditNote from './TableCreditNote.vue'; import TableCreditNote from './TableCreditNote.vue';
import { dialogWarningClose } from 'src/stores/utils'; import { dialogWarningClose } from 'src/stores/utils';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const $q = useQuasar(); const $q = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
@ -46,6 +47,7 @@ const pageState = reactive({
total: 0, total: 0,
creditDialog: false, creditDialog: false,
searchDate: [],
}); });
const fieldSelectedOption = computed(() => { const fieldSelectedOption = computed(() => {
@ -64,6 +66,8 @@ async function getList(opts?: { page?: number; pageSize?: number }) {
pageSize: opts?.pageSize || pageSize.value, pageSize: opts?.pageSize || pageSize.value,
query: pageState.inputSearch === '' ? undefined : pageState.inputSearch, query: pageState.inputSearch === '' ? undefined : pageState.inputSearch,
creditNoteStatus: pageState.currentTab as CreditNoteStatus | undefined, creditNoteStatus: pageState.currentTab as CreditNoteStatus | undefined,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
}); });
if (res) { if (res) {
@ -133,6 +137,7 @@ watch(
() => pageState.inputSearch, () => pageState.inputSearch,
() => pageSize.value, () => pageSize.value,
() => pageState.statusFilter, () => pageState.statusFilter,
() => pageState.searchDate,
], ],
() => { () => {
getList(); getList();
@ -228,6 +233,10 @@ watch(
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch v-model="pageState.searchDate" />
</template>
</q-input> </q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap"> <div class="row col-md-5 justify-end" style="white-space: nowrap">

View file

@ -4,7 +4,7 @@ import { storeToRefs } from 'pinia';
import { QTableSlots } from 'quasar'; import { QTableSlots } from 'quasar';
import { CreditNote, useCreditNote } from 'src/stores/credit-note'; import { CreditNote, useCreditNote } from 'src/stores/credit-note';
import { columns } from './constants.ts'; import { columns } from './constants';
import KebabAction from 'src/components/shared/KebabAction.vue'; import KebabAction from 'src/components/shared/KebabAction.vue';
const creditNote = useCreditNote(); const creditNote = useCreditNote();

View file

@ -248,6 +248,7 @@ function calcPricePerUnit(product: RequestWork['productService']['product']) {
function calcPrice( function calcPrice(
product: RequestWork['productService']['product'], product: RequestWork['productService']['product'],
amount: number, amount: number,
vat: number = 0,
) { ) {
const pricePerUnit = agentPrice.value ? product.agentPrice : product.price; const pricePerUnit = agentPrice.value ? product.agentPrice : product.price;
@ -256,7 +257,8 @@ function calcPrice(
: pricePerUnit; : pricePerUnit;
const priceDiscountNoVat = priceNoVat * amount - 0; const priceDiscountNoVat = priceNoVat * amount - 0;
const rawVatTotal = priceDiscountNoVat * (config.value?.vat || 0.07); const rawVatTotal =
vat === 0 ? 0 : priceDiscountNoVat * (config.value?.vat || 0.07);
return precisionRound(priceNoVat * amount + rawVatTotal); return precisionRound(priceNoVat * amount + rawVatTotal);
} }
@ -346,7 +348,7 @@ function closeAble() {
<td style="text-align: center"> <td style="text-align: center">
{{ {{
formatNumberDecimal( formatNumberDecimal(
calcPrice(v.product.product, v.list.length), calcPrice(v.product.product, v.list.length, v.product.vat),
2, 2,
) )
}} }}
@ -431,7 +433,7 @@ function closeAble() {
class="column set-width bg-color full-height" class="column set-width bg-color full-height"
style="padding: 12px" style="padding: 12px"
> >
({{ ThaiBahtText(summaryPrice.finalPrice) }}) ({{ ThaiBahtText(precisionRound(summaryPrice.finalPrice)) }})
</div> </div>
<div <div
class="row text-right border-5 items-center" class="row text-right border-5 items-center"
@ -494,7 +496,7 @@ function closeAble() {
details?.worker.map( details?.worker.map(
(v, i) => (v, i) =>
`${i + 1}. ` + `${i + 1}. ` +
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(), `${v.namePrefix}. ${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
) || [], ) || [],
}, },
}) || '-' }) || '-'

View file

@ -24,6 +24,7 @@ import { pageTabs, columns, hslaColors } from './constants';
import { DebitNoteStatus, useDebitNote } from 'src/stores/debit-note'; import { DebitNoteStatus, useDebitNote } from 'src/stores/debit-note';
import { dialogWarningClose } from 'src/stores/utils'; import { dialogWarningClose } from 'src/stores/utils';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const $q = useQuasar(); const $q = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
@ -46,6 +47,7 @@ const pageState = reactive({
total: 0, total: 0,
debitDialog: false, debitDialog: false,
searchDate: [],
}); });
const fieldSelectedOption = computed(() => { const fieldSelectedOption = computed(() => {
@ -68,6 +70,8 @@ async function getList(opts?: { page?: number; pageSize?: number }) {
? undefined ? undefined
: pageState.currentTab) as DebitNoteStatus, : pageState.currentTab) as DebitNoteStatus,
includeRegisteredBranch: true, includeRegisteredBranch: true,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
}); });
if (res) { if (res) {
@ -149,6 +153,7 @@ watch(
() => pageState.inputSearch, () => pageState.inputSearch,
() => pageSize.value, () => pageSize.value,
() => pageState.statusFilter, () => pageState.statusFilter,
() => pageState.searchDate,
], ],
() => getList(), () => getList(),
); );
@ -256,6 +261,10 @@ watch(
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch v-model="pageState.searchDate" />
</template>
</q-input> </q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap"> <div class="row col-md-5 justify-end" style="white-space: nowrap">

View file

@ -4,7 +4,7 @@ import { storeToRefs } from 'pinia';
import { QTableSlots } from 'quasar'; import { QTableSlots } from 'quasar';
import { DebitNote, useDebitNote } from 'src/stores/debit-note'; import { DebitNote, useDebitNote } from 'src/stores/debit-note';
import { columns } from './constants.ts'; import { columns } from './constants';
import KebabAction from 'src/components/shared/KebabAction.vue'; import KebabAction from 'src/components/shared/KebabAction.vue';
const debitNote = useDebitNote(); const debitNote = useDebitNote();

View file

@ -501,7 +501,7 @@ function print() {
details?.worker.map( details?.worker.map(
(v, i) => (v, i) =>
`${i + 1}. ` + `${i + 1}. ` +
`${v.namePrefix}. ${v.firstNameEN} ${v.lastNameEN}`.toUpperCase(), `${v.namePrefix}. ${v.firstNameEN ? `${v.firstNameEN} ${v.lastNameEN}` : `${v.firstName} ${v.lastName}`} `.toUpperCase(),
) || [], ) || [],
}, },
}) || '-' }) || '-'

View file

@ -17,7 +17,8 @@ import { columns, hslaColors } from './constants';
import useFlowStore from 'src/stores/flow'; import useFlowStore from 'src/stores/flow';
import { usePayment, useReceipt } from 'src/stores/payment'; import { usePayment, useReceipt } from 'src/stores/payment';
import { PaymentDataStatus } from 'src/stores/payment/types'; import { PaymentDataStatus } from 'src/stores/payment/types';
import { QSelect, useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const $q = useQuasar(); const $q = useQuasar();
const navigatorStore = useNavigator(); const navigatorStore = useNavigator();
@ -26,7 +27,6 @@ const receiptStore = useReceipt();
const { data, page, pageMax, pageSize } = storeToRefs(receiptStore); const { data, page, pageMax, pageSize } = storeToRefs(receiptStore);
// NOTE: Variable // NOTE: Variable
const refFilter = ref<InstanceType<typeof QSelect>>();
const pageState = reactive({ const pageState = reactive({
hideStat: false, hideStat: false,
@ -35,6 +35,7 @@ const pageState = reactive({
fieldSelected: [...columns.map((v) => v.name)], fieldSelected: [...columns.map((v) => v.name)],
gridView: false, gridView: false,
total: 0, total: 0,
searchDate: [],
}); });
const fieldSelectedOption = computed(() => { const fieldSelectedOption = computed(() => {
@ -49,6 +50,8 @@ async function fetchList(opts?: { rotateFlowId?: boolean }) {
page: page.value, page: page.value,
pageSize: pageSize.value, pageSize: pageSize.value,
query: pageState.inputSearch, query: pageState.inputSearch,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
}); });
if (ret) { if (ret) {
data.value = $q.screen.xs ? [...data.value, ...ret.result] : ret.result; data.value = $q.screen.xs ? [...data.value, ...ret.result] : ret.result;
@ -95,6 +98,7 @@ watch(
() => pageState.inputSearch, () => pageState.inputSearch,
() => pageState.statusFilter, () => pageState.statusFilter,
() => pageSize.value, () => pageSize.value,
() => pageState.searchDate,
], ],
() => { () => {
page.value = 1; page.value = 1;
@ -172,25 +176,43 @@ watch(
<template #prepend> <template #prepend>
<q-icon name="mdi-magnify" /> <q-icon name="mdi-magnify" />
</template> </template>
<template v-if="$q.screen.lt.md" v-slot:append> <template v-slot:append>
<span class="row"> <q-separator vertical inset class="q-mr-xs" />
<q-separator vertical /> <AdvanceSearch
<q-btn v-model="pageState.searchDate"
icon="mdi-filter-variant" :active="$q.screen.lt.md && pageState.statusFilter !== 'None'"
unelevated >
class="q-ml-sm" <div
padding="4px" v-if="$q.screen.lt.md"
size="sm" class="q-mt-sm text-weight-medium"
rounded >
@click="refFilter?.showPopup" {{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
ref="refFilter"
v-model="pageState.statusFilter"
outlined
dense
option-value="value"
option-label="label"
map-options
emit-value
:for="'field-select-status'"
:options="[
{
label: $t('general.all'),
value: 'None',
},
]"
/> />
</span> </AdvanceSearch>
</template> </template>
</q-input> </q-input>
<div class="row col-md-5" style="white-space: nowrap"> <div class="row col-md-5" style="white-space: nowrap">
<q-select <q-select
v-show="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
ref="refFilter" ref="refFilter"
v-model="pageState.statusFilter" v-model="pageState.statusFilter"
outlined outlined

9
src/quasar.d.ts vendored
View file

@ -1,9 +0,0 @@
/* eslint-disable */
// Forces TS to apply `@quasar/app-vite` augmentations of `quasar` package
// Removing this would break `quasar/wrappers` imports as those typings are declared
// into `@quasar/app-vite`
// As a side effect, since `@quasar/app-vite` reference `quasar` to augment it,
// this declaration also apply `quasar` own
// augmentations (eg. adds `$q` into Vue component context)
/// <reference types="@quasar/app-vite" />

View file

@ -1,4 +1,4 @@
import { route } from 'quasar/wrappers'; import { defineRouter } from '#q-app/wrappers';
import { import {
createMemoryHistory, createMemoryHistory,
createRouter, createRouter,
@ -17,7 +17,7 @@ import routes from './routes';
* with the Router instance. * with the Router instance.
*/ */
export default route(function (/* { store, ssrContext } */) { export default defineRouter(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER const createHistory = process.env.SERVER
? createMemoryHistory ? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history' : process.env.VUE_ROUTER_MODE === 'history'

View file

@ -155,6 +155,16 @@ const routes: RouteRecordRaw[] = [
name: 'ManualView', name: 'ManualView',
component: () => import('pages/00_manual/ViewPage.vue'), component: () => import('pages/00_manual/ViewPage.vue'),
}, },
{
path: '/troubleshooting',
name: 'Troubleshooting',
component: () => import('pages/00_manual/MainPage.vue'),
},
{
path: '/troubleshooting/:category/:page',
name: 'TroubleshootingView',
component: () => import('pages/00_manual/ViewPage.vue'),
},
], ],
}, },

10
src/shims-vue.d.ts vendored
View file

@ -1,10 +0,0 @@
/* eslint-disable */
/// <reference types="vite/client" />
// Mocks all files ending in `.vue` showing them as plain Vue instances
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

View file

@ -102,12 +102,25 @@ const useAddressStore = defineStore('api-address', () => {
return subDistrict.value[districtId]; return subDistrict.value[districtId];
} }
async function listSameOfficeArea(districtId: string) {
const res = await api.post<string[]>(
`/employment-office/list-same-office-area`,
{ districtId: districtId },
);
if (!res) return false;
return res.data;
}
return { return {
fetchOffice, fetchOffice,
fetchOfficeById, fetchOfficeById,
fetchProvince, fetchProvince,
fetchDistrictByProvinceId, fetchDistrictByProvinceId,
fetchSubDistrictByProvinceId, fetchSubDistrictByProvinceId,
listSameOfficeArea,
}; };
}); });

View file

@ -39,6 +39,8 @@ const useBranchStore = defineStore('api-branch', () => {
withHead?: boolean; withHead?: boolean;
activeOnly?: boolean; activeOnly?: boolean;
headOfficeId?: string; headOfficeId?: string;
startDate?: string;
endDate?: string;
}, },
Data extends Pagination<Branch[]>, Data extends Pagination<Branch[]>,
>(opts?: Options): Promise<Data | false> { >(opts?: Options): Promise<Data | false> {

View file

@ -1,6 +1,7 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { api } from 'src/boot/axios'; import { api } from 'src/boot/axios';
import { ref } from 'vue'; import { ref } from 'vue';
import type { AppConfig } from './types';
export const useConfigStore = defineStore('config-store', () => { export const useConfigStore = defineStore('config-store', () => {
const data = ref<AppConfig>(); const data = ref<AppConfig>();

View file

@ -1,3 +1,3 @@
type AppConfig = { export type AppConfig = {
vat: number; vat: number;
}; };

View file

@ -3,17 +3,17 @@ import {
CreditNoteStatus as Status, CreditNoteStatus as Status,
CreditNotePayload as Payload, CreditNotePayload as Payload,
CreditNotePaybackStatus, CreditNotePaybackStatus,
} from './types.ts'; } from './types';
import { ref } from 'vue'; import { ref } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { api } from 'src/boot/axios.ts'; import { api } from 'src/boot/axios';
import { PaginationResult } from 'src/types.ts'; import { PaginationResult } from 'src/types';
import { manageAttachment, manageFile } from '../utils/index.ts'; import { manageAttachment, manageFile } from '../utils';
const ENDPOINT = 'credit-note'; const ENDPOINT = 'credit-note';
export * from './types.ts'; export * from './types';
export async function getCreditNoteStats() { export async function getCreditNoteStats() {
const res = await api.get<Record<Status, number>>(`/${ENDPOINT}/stats`); const res = await api.get<Record<Status, number>>(`/${ENDPOINT}/stats`);
@ -28,6 +28,8 @@ export async function getCreditNoteList(params?: {
pageSize?: number; pageSize?: number;
query?: string; query?: string;
creditNoteStatus?: Status; creditNoteStatus?: Status;
startDate?: string;
endDate?: string;
}) { }) {
const res = await api.get<PaginationResult<Data>>(`/${ENDPOINT}`, { const res = await api.get<PaginationResult<Data>>(`/${ENDPOINT}`, {
params, params,

View file

@ -113,6 +113,8 @@ const useCustomerStore = defineStore('api-customer', () => {
includeBranch?: boolean; includeBranch?: boolean;
status?: 'CREATED' | 'ACTIVE' | 'INACTIVE'; status?: 'CREATED' | 'ACTIVE' | 'INACTIVE';
customerType?: CustomerType; customerType?: CustomerType;
startDate?: string;
endDate?: string;
}, },
Data extends Pagination< Data extends Pagination<
(Customer & (Customer &
@ -500,6 +502,6 @@ const useCustomerStore = defineStore('api-customer', () => {
}; };
}); });
export * from './types.ts'; export * from './types';
export default useCustomerStore; export default useCustomerStore;

View file

@ -2,17 +2,17 @@ import {
DebitNote as Data, DebitNote as Data,
DebitNoteStatus as Status, DebitNoteStatus as Status,
DebitNotePayload as Payload, DebitNotePayload as Payload,
} from './types.ts'; } from './types';
import { ref } from 'vue'; import { ref } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { api } from 'src/boot/axios.ts'; import { api } from 'src/boot/axios';
import { PaginationResult } from 'src/types.ts'; import { PaginationResult } from 'src/types';
import { manageAttachment, manageFile } from '../utils/index.ts'; import { manageAttachment, manageFile } from '../utils';
const ENDPOINT = 'debit-note'; const ENDPOINT = 'debit-note';
export * from './types.ts'; export * from './types';
export async function getDebitNoteStats() { export async function getDebitNoteStats() {
const res = await api.get<Record<Status, number>>(`/${ENDPOINT}/stats`); const res = await api.get<Record<Status, number>>(`/${ENDPOINT}/stats`);
@ -28,6 +28,8 @@ export async function getDebitNoteList(params?: {
query?: string; query?: string;
status?: Status; status?: Status;
includeRegisteredBranch?: boolean; includeRegisteredBranch?: boolean;
startDate?: string;
endDate?: string;
}) { }) {
const res = await api.get<PaginationResult<Data>>(`/${ENDPOINT}`, { const res = await api.get<PaginationResult<Data>>(`/${ENDPOINT}`, {
params, params,

View file

@ -45,6 +45,8 @@ const useEmployeeStore = defineStore('api-employee', () => {
customerId?: string; customerId?: string;
customerBranchId?: string; customerBranchId?: string;
activeOnly?: boolean; activeOnly?: boolean;
startDate?: string;
endDate?: string;
payload?: { passport?: string[] }; payload?: { passport?: string[] };
}) { }) {
const { payload, ...params } = opts || {}; const { payload, ...params } = opts || {};

View file

@ -29,6 +29,9 @@ export const useInstitution = defineStore('institution-store', () => {
group?: string; group?: string;
status?: Status; status?: Status;
payload?: { group?: string[] }; payload?: { group?: string[] };
startDate?: string;
endDate?: string;
activeOnly?: boolean;
}) { }) {
const { payload, ...params } = opts || {}; const { payload, ...params } = opts || {};
@ -72,18 +75,67 @@ export const useInstitution = defineStore('institution-store', () => {
} }
} }
if (res.data.bank && data.bank.length > 0) {
for (let i = 0; i < data.bank?.length; i++) {
if (data.bank[i].bankQr) {
await api
.put(
`/institution/${res.data.id}/bank-qr/${res.data.bank[i].id}`,
data.bank[i].bankQr,
{
headers: { 'Content-Type': data.bank[i].bankQr?.type },
onUploadProgress: (e) => console.log(e),
},
)
.catch((e) => console.error(e));
}
}
}
if (res.status < 400) { if (res.status < 400) {
return res.data; return res.data;
} }
return null; return null;
} }
async function editInstitution(data: InstitutionPayload & { id: string }) { async function editInstitution(
data: InstitutionPayload & { id: string },
opts?: { indexDeleteQrCodeBank?: number[] },
) {
const res = await api.put(`/institution/${data.id}`, { const res = await api.put(`/institution/${data.id}`, {
...data, ...data,
id: undefined, id: undefined,
group: undefined, group: undefined,
}); });
if (!!res.data.bank && !!data.bank.length) {
for (let i = 0; i < data.bank?.length; i++) {
if (data.bank[i].bankQr) {
console.log(i);
console.log(data.bank[i].bankQr);
await api
.put(
`/institution/${res.data.id}/bank-qr/${res.data.bank[i].id}`,
data.bank[i].bankQr,
{
headers: { 'Content-Type': data.bank[i].bankQr?.type },
onUploadProgress: (e) => console.log(e),
},
)
.catch((e) => console.error(e));
}
}
}
if (opts.indexDeleteQrCodeBank && opts.indexDeleteQrCodeBank.length > 0) {
console.log('delete');
opts.indexDeleteQrCodeBank.forEach(async (i) => {
await api
.delete(`/institution/${res.data.id}/bank-qr/${res.data.bank[i].id}`)
.catch((e) => console.error(e));
});
}
if (res.status < 400) { if (res.status < 400) {
return res.data; return res.data;
} }

View file

@ -1,4 +1,5 @@
import { District, Province, SubDistrict } from '../address'; import { District, Province, SubDistrict } from '../address';
import { BankBook } from '../branch/types';
import { Status } from '../types'; import { Status } from '../types';
export type Institution = { export type Institution = {
@ -25,6 +26,11 @@ export type Institution = {
districtId: string; districtId: string;
provinceId: string; provinceId: string;
status: Status; status: Status;
contactName?: string | null;
contactEmail?: string | null;
contactTel?: string | null;
bank: BankBook[];
}; };
export type InstitutionPayload = { export type InstitutionPayload = {
@ -34,6 +40,11 @@ export type InstitutionPayload = {
group?: string; group?: string;
selectedImage?: string | null; selectedImage?: string | null;
contactName?: string | null;
contactEmail?: string | null;
contactTel?: string | null;
bank: BankBook[];
addressEN: string; addressEN: string;
address: string; address: string;
soi?: string | null; soi?: string | null;

View file

@ -5,10 +5,11 @@ import { getToken } from 'src/services/keycloak';
import { Manual } from './types'; import { Manual } from './types';
import { baseUrl } from '../utils'; import { baseUrl } from '../utils';
const ENDPOINT = 'manual'; const MANUAL_ENDPOINT = 'manual';
const TROUBLESHOOTING_ENDPOINT = 'troubleshooting';
export async function getManual() { export async function getManual() {
const res = await api.get<Manual[]>(`/${ENDPOINT}`); const res = await api.get<Manual[]>(`/${MANUAL_ENDPOINT}`);
if (res.status < 400) { if (res.status < 400) {
return res.data; return res.data;
} }
@ -20,7 +21,28 @@ export async function getManualByPage(opt: {
pageName: string; pageName: string;
}) { }) {
const res = await fetch( const res = await fetch(
`${baseUrl}/${ENDPOINT}/${opt.category}/page/${opt.pageName}`, `${baseUrl}/${MANUAL_ENDPOINT}/${opt.category}/page/${opt.pageName}`,
);
if (res.status < 400) {
return res;
}
return null;
}
export async function getTroubleshooting() {
const res = await api.get<Manual[]>(`/${TROUBLESHOOTING_ENDPOINT}`);
if (res.status < 400) {
return res.data;
}
return null;
}
export async function getTroubleshootingByPage(opt: {
category: string;
pageName: string;
}) {
const res = await fetch(
`${baseUrl}/${TROUBLESHOOTING_ENDPOINT}/${opt.category}/page/${opt.pageName}`,
); );
if (res.status < 400) { if (res.status < 400) {
return res; return res;
@ -30,11 +52,15 @@ export async function getManualByPage(opt: {
export const useManualStore = defineStore('manual-store', () => { export const useManualStore = defineStore('manual-store', () => {
const dataManual = ref<Manual[]>([]); const dataManual = ref<Manual[]>([]);
const dataTroubleshooting = ref<Manual[]>([]);
return { return {
getManual, getManual,
getManualByPage, getManualByPage,
getTroubleshooting,
getTroubleshootingByPage,
dataManual, dataManual,
dataTroubleshooting,
}; };
}); });

View file

@ -75,6 +75,7 @@ const useOptionStore = defineStore('optionStore', () => {
} }
return { return {
rawOption,
globalOption, globalOption,
mapOption, mapOption,
}; };

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