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

12398
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

3642
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

18
postcss.config.js Normal file
View file

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

BIN
public/img-group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 NextButton } from './NextButton.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"
/>
</div>
<div v-if="data.length > 3" class="avatar remaining-count">
<div
v-if="data.length > 3"
class="avatar remaining-count"
style="cursor: default"
>
<q-tooltip>
<div v-for="(person, i) in data.slice(3)" :key="i + 3">
{{ person.name }}

View file

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

View file

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

View file

@ -65,6 +65,8 @@ onMounted(async () => {
}
await getSelectedOption();
valueOption.value = selectOptions.value.find((v) => v.id === value.value);
});
</script>
<template>
@ -158,7 +160,7 @@ onMounted(async () => {
</template>
<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 />
</q-item>

View file

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

View file

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

2
src/env.d.ts vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,9 +5,7 @@ import hljs from 'highlight.js';
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
// @ts-expect-error
import mditFigureWithPCaption from 'markdown-it-image-figures';
// @ts-expect-error
import mditMedia from 'markdown-it-html5-media';
import mditAnchor from 'markdown-it-anchor';
import mditHighlight from 'markdown-it-highlightjs';
@ -58,14 +56,28 @@ onUnmounted(() => {
async function getContent() {
if (!category.value || !page.value) return;
const res = await manualStore.getManualByPage({
category: category.value,
pageName: page.value,
});
if (res && res.ok) {
const text = await res.text();
content.value = text;
contentParsed.value = md.parse(text, {});
if (ROUTE.name === 'ManualView') {
const res = await manualStore.getManualByPage({
category: category.value,
pageName: page.value,
});
if (res && res.ok) {
const text = await res.text();
content.value = text;
contentParsed.value = md.parse(text, {});
}
}
if (ROUTE.name === 'TroubleshootingView') {
const res = await manualStore.getTroubleshootingByPage({
category: category.value,
pageName: page.value,
});
if (res && res.ok) {
const text = await res.text();
content.value = text;
contentParsed.value = md.parse(text, {});
}
}
}
@ -186,7 +198,9 @@ async function scrollTo(id: string) {
md.render(
content.replaceAll(
'assets/',
`${baseUrl}/manual/${category}/assets/`,
$route.name === 'ManualView'
? `${baseUrl}/manual/${category}/assets/`
: `${baseUrl}/troubleshooting/${category}/assets/`,
),
)
"
@ -317,4 +331,20 @@ async function scrollTo(id: string) {
.markdown :deep(video) {
width: 100%;
}
.markdown :deep(table) {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5rem;
}
.markdown :deep(:where(table th)) {
background: var(--surface-2);
border: 1px solid var(--border-color);
}
.markdown :deep(:where(table td, table th)) {
border: 1px solid var(--border-color);
padding: 0.25rem 1rem;
}
</style>

View file

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

View file

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

View file

@ -1,10 +1,10 @@
<script setup lang="ts">
import { ref, watch, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { QSelect, useQuasar } from 'quasar';
import { useQuasar } from 'quasar';
import { useRoute, useRouter } from 'vue-router';
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 { dialogCheckData } from 'stores/utils';
@ -86,6 +86,7 @@ import { nextTick } from 'vue';
import FormEmployeeVisa from 'components/03_customer-management/FormEmployeeVisa.vue';
import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import { AddButton } from 'components/button';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { t, locale } = useI18n();
const $q = useQuasar();
@ -101,28 +102,33 @@ const employeeFormStore = useEmployeeForm();
const optionStore = useOptionStore();
const ocrStore = useOcrStore();
const refFilter = ref<InstanceType<typeof QSelect>>();
const statusEmployeeCreate = ref<boolean>(false);
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 } =
storeToRefs(customerFormStore);
const { state: employeeFormState, currentFromDataEmployee } =
storeToRefs(employeeFormStore);
const {
fetchListOfOptionBranch,
customerFormUndo,
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() {
navigatorStore.current.title = 'menu.customer';
@ -136,6 +142,14 @@ async function init() {
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 (
@ -175,6 +189,7 @@ const statsCustomerType = ref<CustomerStats>({
});
// NOTE: Page State
const searchDate = ref<string[]>([]);
const currentTab = ref<'employer' | 'employee'>('employer');
const inputSearch = ref('');
const currentStatus = ref<Status | 'All'>('All');
@ -209,17 +224,19 @@ const dialogCustomerImageUpload = ref<InstanceType<typeof ImageUploadDialog>>();
const dialogEmployeeImageUpload = ref<InstanceType<typeof ImageUploadDialog>>();
// image
const refreshImageState = ref(false);
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(
[currentTab, currentStatus, inputSearch, customerTypeSelected, pageSize],
async ([tabName]) => {
[
currentTab,
currentStatus,
inputSearch,
customerTypeSelected,
pageSize,
searchDate,
],
async ([tabName], [oldTabName]) => {
// if (tabName !== oldTabName) searchDate.value = [];
if (tabName === 'employer') {
currentPageCustomer.value = 1;
currentBtnOpen.value = [];
@ -276,8 +293,6 @@ const fieldSelected = ref<string[]>(
].filter((v, index, self) => self.indexOf(v) === index),
);
const registerAbleBranchOption = ref<{ id: string; name: string }[]>();
const branch = ref<CustomerBranch[]>();
const customerStats = [
@ -294,81 +309,6 @@ const fieldCustomer = [
const employeeHistoryDialog = ref(false);
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) {
const total = statsCustomerType.value.PERS + statsCustomerType.value.CORP;
@ -386,6 +326,8 @@ async function fetchListCustomer(fetchStats = false, mobileFetch?: boolean) {
? 'ACTIVE'
: 'INACTIVE',
query: inputSearch.value,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
customerType: (
{
all: undefined,
@ -439,6 +381,8 @@ async function fetchListEmployee(opt?: {
query: inputSearch.value,
passport: true,
visa: true,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
});
if (resultListEmployee) {
maxPageEmployee.value = Math.ceil(
@ -503,64 +447,6 @@ async function toggleStatusCustomer(id: string, status: boolean) {
await fetchListCustomer(false, $q.screen.xs);
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) {
const res = await employeeStore.getEditHistory(id);
employeeHistory.value = res.reverse();
@ -594,64 +480,6 @@ async function editEmployeeFormPersonal(id: string) {
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') {
customerFormState.value.dialogModal = true;
customerFormState.value.dialogType = 'create';
@ -683,10 +511,34 @@ async function fetchImageList(
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
watch(
() => employeeFormState.value.formDataEmployeeOwner,
() => employeeFormState.value.currentCustomerBranch,
(e) => {
if (!e) return;
if (employeeFormState.value.formDataEmployeeSameAddr) {
@ -703,21 +555,21 @@ watch(
watch(
() => employeeFormState.value.formDataEmployeeSameAddr,
(isSame) => {
if (!employeeFormState.value.formDataEmployeeOwner) return;
if (!employeeFormState.value.currentCustomerBranch) return;
if (isSame) {
currentFromDataEmployee.value.address =
employeeFormState.value.formDataEmployeeOwner.address;
employeeFormState.value.currentCustomerBranch.address;
currentFromDataEmployee.value.addressEN =
employeeFormState.value.formDataEmployeeOwner.addressEN;
employeeFormState.value.currentCustomerBranch.addressEN;
currentFromDataEmployee.value.provinceId =
employeeFormState.value.formDataEmployeeOwner.provinceId;
employeeFormState.value.currentCustomerBranch.provinceId;
currentFromDataEmployee.value.districtId =
employeeFormState.value.formDataEmployeeOwner.districtId;
employeeFormState.value.currentCustomerBranch.districtId;
currentFromDataEmployee.value.subDistrictId =
employeeFormState.value.formDataEmployeeOwner.subDistrictId;
employeeFormState.value.currentCustomerBranch.subDistrictId;
}
currentFromDataEmployee.value.customerBranchId =
employeeFormState.value.formDataEmployeeOwner.id;
employeeFormState.value.currentCustomerBranch.id;
},
);
@ -936,26 +788,43 @@ const emptyCreateDialog = ref(false);
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-if="$q.screen.lt.md" v-slot:append>
<span class="row">
<q-separator vertical />
<q-btn
icon="mdi-filter-variant"
unelevated
class="q-ml-sm"
padding="4px"
size="sm"
rounded
@click="refFilter?.showPopup"
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch
v-model="searchDate"
:active="$q.screen.lt.md && currentStatus !== 'All'"
>
<div
v-if="$q.screen.lt.md"
class="q-mt-sm text-weight-medium"
>
{{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
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>
</q-input>
<div class="row col-md-5" style="white-space: nowrap">
<q-select
v-show="$q.screen.gt.sm"
ref="refFilter"
v-if="$q.screen.gt.sm"
id="select-status"
for="select-status"
v-model="currentStatus"
@ -1540,7 +1409,13 @@ const emptyCreateDialog = ref(false);
customerFormState.branchIndex = 0;
}
"
@delete="deleteCustomerById(props.row.id)"
@delete="
deleteCustomerById(
props.row.id,
async () =>
await fetchListCustomer(true, $q.screen.xs),
)
"
@change-status="
async () => {
triggerChangeStatus(
@ -1588,7 +1463,23 @@ const emptyCreateDialog = ref(false);
"
@delete="
(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="
@ -1763,7 +1654,16 @@ const emptyCreateDialog = ref(false);
customerFormState.branchIndex = 0;
}
"
@delete="deleteCustomerById(props.row.id)"
@delete="
deleteCustomerById(
props.row.id,
async () =>
await fetchListCustomer(
true,
$q.screen.xs,
),
)
"
@change-status="
triggerChangeStatus(
props.row.id,
@ -1898,7 +1798,23 @@ const emptyCreateDialog = ref(false);
@edit="(item: any) => editEmployeeFormPersonal(item.id)"
@delete="
(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="
@ -2036,7 +1952,7 @@ const emptyCreateDialog = ref(false);
async (currentBranch) => {
createEmployeeForm();
await nextTick();
employeeFormState.formDataEmployeeOwner = { ...currentBranch };
employeeFormState.currentBranchId = currentBranch.id;
}
"
v-model:branch="branch"
@ -2150,7 +2066,16 @@ const emptyCreateDialog = ref(false);
"
:title="
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
"
:caption="
@ -2259,13 +2184,16 @@ const emptyCreateDialog = ref(false);
id="form-basic-info-customer"
:onCreate="customerFormState.dialogType === 'create'"
@edit="
(customerFormState.dialogType = 'edit'),
(customerFormState.readonly = false)
((customerFormState.dialogType = 'edit'),
(customerFormState.readonly = false))
"
@cancel="() => customerFormUndo(false)"
@delete="
customerFormState.editCustomerId &&
deleteCustomerById(customerFormState.editCustomerId)
deleteCustomerById(
customerFormState.editCustomerId,
async () => await fetchListCustomer(true, $q.screen.xs),
)
"
:customer-type="customerFormData.customerType"
v-model:registered-branch-id="customerFormData.registeredBranchId"
@ -2435,7 +2363,11 @@ const emptyCreateDialog = ref(false);
if (!customerFormState.editCustomerId) return;
if (idx === 0) {
deleteCustomerById(customerFormState.editCustomerId);
deleteCustomerById(
customerFormState.editCustomerId,
async () =>
await fetchListCustomer(true, $q.screen.xs),
);
return;
}
if (!!customerFormData.customerBranch?.[idx].id) {
@ -2591,6 +2523,20 @@ const emptyCreateDialog = ref(false);
"
:toggleTitle="$t('status.title')"
hideFade
:title="
currentFromDataEmployee
? setPrefixName(
{
namePrefix: currentFromDataEmployee.namePrefix,
firstName: currentFromDataEmployee.firstName,
lastName: currentFromDataEmployee.lastName,
firstNameEN: currentFromDataEmployee.firstNameEN,
lastNameEN: currentFromDataEmployee.lastNameEN,
},
{ locale },
)
: '-'
"
@view="
() => {
employeeFormState.imageDialog = true;
@ -2870,12 +2816,30 @@ const emptyCreateDialog = ref(false);
id="btn-info-basic-delete"
icon-only
@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"
/>
</div>
</div>
<BasicInformation
no-action
id="form-information"
@ -2888,11 +2852,13 @@ const emptyCreateDialog = ref(false);
title="form.field.basicInformation"
:readonly="!employeeFormState.isEmployeeEdit"
: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:nrc-no="currentFromDataEmployee.nrcNo"
v-model:code="currentFromDataEmployee.code"
@filter-owner-branch="employeeFormStore.employeeFilterOwnerBranch"
class="q-mb-xl"
/>
<FormPerson
@ -2918,6 +2884,7 @@ const emptyCreateDialog = ref(false);
class="q-mb-xl"
/>
<AddressForm
disabledRule
id="form-personal-address"
prefix-id="form-employee"
:readonly="!employeeFormState.isEmployeeEdit"
@ -2940,7 +2907,7 @@ const emptyCreateDialog = ref(false);
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">
<q-icon
flat
@ -3448,7 +3415,24 @@ const emptyCreateDialog = ref(false);
@click.stop="
() => {
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"
@ -3599,7 +3583,24 @@ const emptyCreateDialog = ref(false);
@click.stop="
() => {
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"
@ -3671,7 +3672,23 @@ const emptyCreateDialog = ref(false);
@delete="
(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="
@ -3757,7 +3774,23 @@ const emptyCreateDialog = ref(false);
@delete="
(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="
@ -4104,7 +4137,17 @@ const emptyCreateDialog = ref(false);
"
:title="
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
"
:caption="
@ -4218,13 +4261,16 @@ const emptyCreateDialog = ref(false);
id="form-basic-info-customer"
:onCreate="customerFormState.dialogType === 'create'"
@edit="
(customerFormState.dialogType = 'edit'),
(customerFormState.readonly = false)
((customerFormState.dialogType = 'edit'),
(customerFormState.readonly = false))
"
@cancel="() => customerFormUndo(false)"
@delete="
customerFormState.editCustomerId &&
deleteCustomerById(customerFormState.editCustomerId)
deleteCustomerById(
customerFormState.editCustomerId,
async () => await fetchListCustomer(true, $q.screen.xs),
)
"
:customer-type="customerFormData.customerType"
v-model:registered-branch-id="customerFormData.registeredBranchId"
@ -4524,9 +4570,16 @@ const emptyCreateDialog = ref(false);
fallback-cover="/images/employee-banner.png"
:title="
employeeFormState.currentEmployee
? $i18n.locale === 'eng'
? `${employeeFormState.currentEmployee.firstNameEN} ${employeeFormState.currentEmployee.lastNameEN}`
: `${employeeFormState.currentEmployee.firstName} ${employeeFormState.currentEmployee.lastName}`
? setPrefixName(
{
namePrefix: employeeFormState.currentEmployee.namePrefix,
firstName: employeeFormState.currentEmployee.firstName,
lastName: employeeFormState.currentEmployee.lastName,
firstNameEN: employeeFormState.currentEmployee.firstNameEN,
lastNameEN: employeeFormState.currentEmployee.lastNameEN,
},
{ locale },
)
: '-'
"
:caption="currentFromDataEmployee.code"
@ -4821,7 +4874,23 @@ const emptyCreateDialog = ref(false);
icon-only
@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"
/>
@ -4835,16 +4904,13 @@ const emptyCreateDialog = ref(false);
outlined
title="form.field.basicInformation"
:readonly="!employeeFormState.isEmployeeEdit"
: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:nrc-no="currentFromDataEmployee.nrcNo"
v-model:code="currentFromDataEmployee.code"
@filter-owner-branch="
employeeFormStore.employeeFilterOwnerBranch
"
class="q-mb-xl"
/>
<FormPerson
@ -4871,6 +4937,7 @@ const emptyCreateDialog = ref(false);
<AddressForm
id="drawer-form-personal-address"
employee
disabledRule
v-model:address="currentFromDataEmployee.address"
v-model:address-en="currentFromDataEmployee.addressEN"
v-model:moo="currentFromDataEmployee.moo"
@ -5450,6 +5517,20 @@ const emptyCreateDialog = ref(false);
deleteEmployeeById({
type: 'passport',
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
@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"
@ -5742,7 +5840,24 @@ const emptyCreateDialog = ref(false);
@delete="
(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="
(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="

View file

@ -1,22 +1,50 @@
import { ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useI18n } from 'vue-i18n';
import {
CustomerBranch,
CustomerBranchCreate,
CustomerCreate,
CustomerType,
} from 'stores/customer/types';
import { Employee, EmployeeCreate } from 'stores/employee/types';
import { dialog } from 'stores/utils';
import useMyBranch from 'stores/my-branch';
import useCustomerStore from 'stores/customer';
import useEmployeeStore from 'stores/employee';
import useFlowStore from 'stores/flow';
import useMyBranchStore from 'stores/my-branch';
import { baseUrl } from 'src/stores/utils';
import { getRole, getUserId } from 'src/services/keycloak';
import { useRoute } from 'vue-router';
export const useCustomerForm = defineStore('form-customer', () => {
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 = {
// code: '',
// 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 {
tabFieldRequired,
registerAbleBranchOption,
state,
resetFormData,
currentFormData,
@ -370,13 +509,18 @@ export const useCustomerForm = defineStore('form-customer', () => {
submitFormCustomer,
addCurrentCustomerBranch,
deleteAttachment,
fetchListOfOptionBranch,
customerFormUndo,
customerConfirmUnsave,
deleteCustomerById,
validateTabField,
deleteCustomerBranchById,
};
});
export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
const customerStore = useCustomerStore();
const customerFormStore = useCustomerForm();
const defaultFormData: CustomerBranchCreate & {
id?: string;
codeCustomer?: string;
@ -581,11 +725,23 @@ export const useCustomerBranchForm = defineStore('form-customer-branch', () => {
});
export const useEmployeeForm = defineStore('form-employee', () => {
const { t } = useI18n();
const customerStore = useCustomerStore();
const employeeStore = useEmployeeStore();
const flowStore = useFlowStore();
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<{
dialogType: 'info' | 'create' | 'edit';
imageDialog: boolean;
@ -594,6 +750,8 @@ export const useEmployeeForm = defineStore('form-employee', () => {
drawerModal: boolean;
isImageEdit: boolean;
currentBranchId: string;
currentCustomerBranch?: CustomerBranch;
currentEmployeeCode: string;
currentEmployee: Employee | null;
currentIndexPassport: number;
@ -627,6 +785,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
| undefined;
ocr: boolean;
}>({
currentBranchId: '',
isImageEdit: false,
currentIndexPassport: -1,
currentIndexVisa: -1,
@ -1089,6 +1248,8 @@ export const useEmployeeForm = defineStore('form-employee', () => {
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}) {
let employeeId: string | undefined = undefined;
currentFromDataEmployee.value.firstName =
currentFromDataEmployee.value.firstName.trim();
currentFromDataEmployee.value.middleName =
@ -1107,7 +1268,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
const res = await employeeStore.create(
{
...currentFromDataEmployee.value,
customerBranchId: state.value.formDataEmployeeOwner?.id || '',
customerBranchId: state.value.currentBranchId || '',
employeeWork: [],
employeeCheckup: [],
@ -1117,6 +1278,7 @@ export const useEmployeeForm = defineStore('form-employee', () => {
);
if (res) {
employeeId = res.id;
await assignFormDataEmployee(res.id);
currentFromDataEmployee.value.id = res.id;
state.value.statusSavePersonal = true;
@ -1138,10 +1300,12 @@ export const useEmployeeForm = defineStore('form-employee', () => {
},
);
if (res) {
employeeId = res.id;
await assignFormDataEmployee(res.id);
state.value.statusSavePersonal = true;
}
}
return employeeId;
}
async function assignFormDataEmployee(id?: string) {
@ -1183,7 +1347,19 @@ export const useEmployeeForm = defineStore('form-employee', () => {
employeePassport: structuredClone(
payload.employeePassport?.length === 0
? 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,
),
@ -1270,6 +1446,8 @@ export const useEmployeeForm = defineStore('form-employee', () => {
state.value.currentIndexVisa = -1;
}
state.value.currentBranchId = payload.customerBranchId;
const foundBranch = await customerStore.fetchListCustomerBranchById(
payload.customerBranchId,
);
@ -1325,17 +1503,17 @@ export const useEmployeeForm = defineStore('form-employee', () => {
issueDate: new Date(),
type: '',
expireDate: new Date(),
birthDate: new Date(),
birthDate: currentFromDataEmployee.value.dateOfBirth,
workerStatus: '',
nationality: '',
gender: '',
lastNameEN: '',
lastName: '',
middleNameEN: '',
middleName: '',
firstNameEN: '',
firstName: '',
namePrefix: '',
nationality: currentFromDataEmployee.value.nationality,
gender: currentFromDataEmployee.value.gender,
lastNameEN: currentFromDataEmployee.value.lastNameEN,
lastName: currentFromDataEmployee.value.lastName,
middleNameEN: currentFromDataEmployee.value.middleNameEN,
middleName: currentFromDataEmployee.value.middleName,
firstNameEN: currentFromDataEmployee.value.firstNameEN,
firstName: currentFromDataEmployee.value.firstName,
namePrefix: currentFromDataEmployee.value.namePrefix,
number: '',
});
@ -1396,7 +1574,88 @@ export const useEmployeeForm = defineStore('form-employee', () => {
(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 {
refreshImageState,
statusEmployeeCreate,
onCreateImageList,
state,
currentFromDataEmployee,
resetEmployeeData,
@ -1423,5 +1682,9 @@ export const useEmployeeForm = defineStore('form-employee', () => {
employeeFilterOwnerBranch,
isFormDataDifferent,
employeeFormUndo,
employeeConfirmUnsave,
deleteEmployeeById,
};
});

View file

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

View file

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue';
import { QSelect, QTableProps } from 'quasar';
import { QTableProps } from 'quasar';
import { storeToRefs } from 'pinia';
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 PaginationPageSize from 'src/components/PaginationPageSize.vue';
import { useQuasar } from 'quasar';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const { t } = useI18n();
const workflowStore = useWorkflowTemplate();
@ -45,6 +46,7 @@ const pageState = reactive({
addModal: false,
viewDrawer: false,
isDrawerEdit: true,
searchDate: [],
});
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 formDataWorkflow = ref<WorkflowTemplatePayload>({
status: 'CREATED',
@ -102,6 +103,7 @@ const columns = [
function triggerDialog(type: 'add' | 'edit' | 'view') {
if (type === 'add') {
registeredBranchId.value = '';
userInTable.value = [];
formDataWorkflow.value = {
status: 'CREATED',
name: '',
@ -206,7 +208,7 @@ async function submit() {
...formDataWorkflow.value,
});
} else {
await workflowStore.creatWorkflowTemplate({
await workflowStore.createWorkflowTemplate({
registeredBranchId: registeredBranchId.value,
...formDataWorkflow.value,
});
@ -222,7 +224,11 @@ function assignFormData(workflowData: WorkflowTemplate) {
status: workflowData.status,
name: workflowData.name,
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) => {
userInTable.value[i].responsiblePerson.push({
id: p.user.id,
@ -236,12 +242,16 @@ function assignFormData(workflowData: WorkflowTemplate) {
code: p.user.code,
});
});
s.responsibleGroup.forEach((g) => {
userInTable.value[i].responsibleGroup.push(g);
});
return {
id: s.id,
name: s.name,
detail: s.detail,
messengerByArea: s.messengerByArea || false,
value: s.value.length > 0 ? JSON.parse(JSON.stringify(s.value)) : [],
responsibleGroup: s.responsibleGroup.map((g) => g),
responsiblePersonId: s.responsiblePerson.map((p) => p.userId),
responsibleInstitution: JSON.parse(
JSON.stringify(s.responsibleInstitution),
@ -282,6 +292,8 @@ async function fetchWorkflowList(mobileFetch?: boolean) {
: statusFilter.value === 'statusACTIVE'
? 'ACTIVE'
: 'INACTIVE',
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
});
if (res) {
workflowData.value =
@ -311,11 +323,14 @@ watch(
fetchWorkflowList();
},
);
watch([() => pageState.inputSearch, workflowPageSize], () => {
workflowData.value = [];
workflowPage.value = 1;
fetchWorkflowList();
});
watch(
[() => pageState.inputSearch, workflowPageSize, () => pageState.searchDate],
() => {
workflowData.value = [];
workflowPage.value = 1;
fetchWorkflowList();
},
);
</script>
<template>
<FloatingActionButton
@ -392,26 +407,44 @@ watch([() => pageState.inputSearch, workflowPageSize], () => {
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-if="$q.screen.lt.md" v-slot:append>
<span class="row">
<q-separator vertical />
<q-btn
icon="mdi-filter-variant"
unelevated
class="q-ml-sm"
padding="4px"
size="sm"
rounded
@click="refFilter?.showPopup"
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch
v-model="pageState.searchDate"
:active="$q.screen.lt.md && statusFilter !== 'all'"
>
<div
v-if="$q.screen.lt.md"
class="q-mt-sm text-weight-medium"
>
{{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
v-model="statusFilter"
outlined
dense
option-value="value"
option-label="label"
map-options
emit-value
:for="'field-select-status'"
:options="[
{ label: $t('general.all'), value: 'all' },
{ label: $t('general.active'), value: 'statusACTIVE' },
{
label: $t('general.inactive'),
value: 'statusINACTIVE',
},
]"
/>
</span>
</AdvanceSearch>
</template>
</q-input>
<div class="row col-md-5" style="white-space: nowrap">
<q-select
v-show="$q.screen.gt.sm"
ref="refFilter"
v-if="$q.screen.gt.sm"
v-model="statusFilter"
outlined
dense
@ -509,12 +542,12 @@ watch([() => pageState.inputSearch, workflowPageSize], () => {
class="col surface-2 flex items-center justify-center"
>
<NoData
v-if="pageState.total !== 0"
v-if="pageState.total !== 0 || pageState.searchDate.length > 0"
:not-found="!!pageState.inputSearch"
/>
<CreateButton
v-if="pageState.total === 0"
v-if="pageState.total === 0 && pageState.searchDate.length === 0"
@click="triggerDialog('add')"
label="general.add"
: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 { onMounted } from 'vue';
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 ProductCardComponent from 'components/04_product-service/ProductCardComponent.vue';
@ -33,6 +33,7 @@ import {
SaveButton,
UndoButton,
ToggleButton,
ImportButton,
} from 'components/button';
import TableProduct from 'src/components/04_product-service/TableProduct.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 { dateFormat } from 'src/utils/datetime';
import { formatNumberDecimal, isRoleInclude } from 'stores/utils';
import { formatNumberDecimal, isRoleInclude, notify } from 'stores/utils';
const { getWorkflowTemplate } = useWorkflowTemplate();
import { Status } from 'stores/types';
@ -67,6 +68,8 @@ import {
} from 'src/stores/workflow-template/types';
import { useWorkflowTemplate } from 'src/stores/workflow-template';
import { deepEquals } from 'src/utils/arr';
import { toRaw } from 'vue';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const flowStore = useFlowStore();
const navigatorStore = useNavigator();
@ -96,10 +99,13 @@ const {
createWork,
editWork,
deleteWork,
importProduct,
} = productServiceStore;
const { workNameItems } = storeToRefs(productServiceStore);
const allStat = ref<{ mode: string; count: number }[]>([]);
const stat = ref<
{
icon: string;
@ -161,8 +167,6 @@ const splitterModel = computed(() =>
$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 imageDialog = ref(false);
const currentNode = ref<ProductGroup & { type: string }>();
@ -520,6 +524,7 @@ const currentStatusGroupType = ref<Status>('CREATED');
const currentIdGroupType = ref('');
const currentStatus = ref<Status | 'All'>('All');
const searchDate = ref<string[]>([]);
// img
const isImageEdit = ref<boolean>(false);
@ -615,6 +620,8 @@ async function fetchListGroups(mobileFetch?: boolean) {
: currentStatus.value === 'ACTIVE'
? 'ACTIVE'
: 'INACTIVE',
startDate: searchDate.value[0],
endDate: searchDate.value[1],
});
if (res) {
@ -675,6 +682,8 @@ async function fetchListOfProduct(mobileFetch?: boolean) {
? 'ACTIVE'
: undefined,
productGroupId: currentIdGroup.value,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
});
if (res) {
@ -720,6 +729,8 @@ async function fetchListOfService(mobileFetch?: boolean) {
? 'ACTIVE'
: undefined,
productGroupId: currentIdGroup.value,
startDate: searchDate.value[0],
endDate: searchDate.value[1],
});
if (res) {
@ -1590,6 +1601,7 @@ async function enterNext(type: 'service' | 'product') {
inputSearchProductAndService.value = '';
currentStatus.value = 'All';
filterStat.value = [];
searchDate.value = [];
if (
expandedTree.value.length > 1 &&
@ -1745,7 +1757,7 @@ watch(currentStatus, async () => {
flowStore.rotate();
});
watch(inputSearch, async () => {
watch([inputSearch, () => searchDate.value], async () => {
if (productMode.value === 'group') {
productGroup.value = [];
currentPageGroup.value = 1;
@ -1754,7 +1766,7 @@ watch(inputSearch, async () => {
}
});
watch(inputSearchProductAndService, async () => {
watch([inputSearchProductAndService, () => searchDate.value], async () => {
product.value = [];
service.value = [];
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(
() => formService.value.attributes.workflowId,
async (a, b) => {
@ -1948,19 +2005,34 @@ watch(
<template v-slot:prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-if="$q.screen.lt.md" v-slot:append>
<span class="row">
<q-separator vertical />
<q-btn
icon="mdi-filter-variant"
unelevated
class="q-ml-sm"
padding="4px"
size="sm"
rounded
@click="refFilterGroup?.showPopup"
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch
v-model="searchDate"
:active="$q.screen.lt.md && currentStatus !== 'All'"
>
<div class="q-mt-sm text-weight-medium">
{{ $t('general.status') }}
</div>
<q-select
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>
</q-input>
</div>
@ -2115,26 +2187,44 @@ watch(
<template v-slot:prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-if="$q.screen.lt.md" v-slot:append>
<span class="row">
<q-separator vertical />
<q-btn
icon="mdi-filter-variant"
unelevated
class="q-ml-sm"
padding="4px"
size="sm"
rounded
@click="refFilterGroup?.showPopup"
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch
v-model="searchDate"
:active="$q.screen.lt.md && currentStatus !== 'All'"
>
<div
v-if="$q.screen.lt.md"
class="q-mt-sm text-weight-medium"
>
{{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
v-model="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>
</q-input>
<div class="row col-md-6" style="white-space: nowrap">
<q-select
v-show="$q.screen.gt.sm"
ref="refFilterGroup"
v-model="currentStatus"
for="select-status"
outlined
@ -2155,7 +2245,6 @@ watch(
},
]"
></q-select>
<q-select
v-if="modeView === false"
id="select-field"
@ -2178,7 +2267,6 @@ watch(
multiple
dense
/>
<q-btn-toggle
v-model="modeView"
id="btn-mode"
@ -2618,26 +2706,65 @@ watch(
<template v-slot:prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-if="$q.screen.lt.md" v-slot:append>
<span class="row">
<q-separator vertical />
<q-btn
icon="mdi-filter-variant"
unelevated
class="q-ml-sm"
padding="4px"
size="sm"
rounded
@click="refFilterProductService?.showPopup"
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch
v-model="searchDate"
:active="$q.screen.lt.md && currentStatus !== 'All'"
>
<div
v-if="$q.screen.lt.md"
class="q-mt-sm text-weight-medium"
>
{{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
: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>
</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">
<q-select
v-show="$q.screen.gt.sm"
ref="refFilterProductService"
:for="'field-select-status'"
v-model="currentStatus"
outlined
@ -2659,7 +2786,6 @@ watch(
]"
@update:model-value="fetchStatus()"
></q-select>
<q-select
v-if="modeView === false"
:hide-dropdown-icon="$q.screen.lt.sm"
@ -2694,7 +2820,6 @@ watch(
multiple
dense
/>
<q-btn-toggle
v-model="modeView"
id="btn-mode"
@ -3127,8 +3252,14 @@ watch(
"
/>
<KebabAction
:use-copy="productAndServiceTab === 'service'"
:status="props.row.status"
:id-name="props.row.name"
@copy="
() => {
copy(props.row.id);
}
"
@view="
async () => {
if (props.row.type === 'product') {
@ -4422,6 +4553,7 @@ watch(
@click="serviceTreeView = false"
/>
</div>
<SaveButton id="btn-info-basic-save" icon-only type="submit" />
</div>

View file

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

View file

@ -2,10 +2,12 @@
import { onMounted, reactive, ref, watch, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
// NOTE: Import stores
import useCustomerStore from 'stores/customer';
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 useFlowStore from 'src/stores/flow';
import useMyBranch from 'stores/my-branch';
@ -38,26 +40,44 @@ import { AddressForm } from 'components/form';
import {
EmployerFormBusiness,
EmployerFormAbout,
EmployerFormBasicInfo,
EmployerFormBranch,
} from 'src/pages/03_customer-management/components';
import { useCustomerForm } from 'src/pages/03_customer-management/form';
import { Quotation } from 'src/stores/quotations/types';
import TableQuotation from 'src/components/05_quotation/TableQuotation.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 quotationFormStore = useQuotationForm();
const customerFormStore = useCustomerForm();
const flowStore = useFlowStore();
const userBranch = useMyBranch();
const navigatorStore = useNavigator();
const customerStore = useCustomerStore();
const {
fetchListOfOptionBranch,
customerFormUndo,
deleteCustomerById,
validateTabField,
deleteCustomerBranchById,
} = customerFormStore;
const {
currentFormData: quotationFormData,
currentFormState: quotationFormState,
} = storeToRefs(quotationFormStore);
const { state: customerFormState, currentFormData: customerFormData } =
storeToRefs(customerFormStore);
const {
state: customerFormState,
currentFormData: customerFormData,
registerAbleBranchOption,
tabFieldRequired,
} = storeToRefs(customerFormStore);
const { currentMyBranch } = storeToRefs(userBranch);
const fieldSelectedOption = computed(() => {
@ -90,6 +110,7 @@ const pageState = reactive({
quotationModal: false,
employeeModal: false,
receiptModal: false,
searchDate: [],
});
pageState.fieldSelected = [...columnQuotation.map((v) => v.name)];
@ -170,8 +191,7 @@ async function submitCustomer() {
customerFormData.value.registeredBranchId = isRoleInclude(['system'])
? branchId.value
: currentMyBranch.value.id;
await customerFormStore.submitFormCustomer();
await customerFormStore.addCurrentCustomerBranch();
customerFormState.value.dialogModal = false;
// customerFormState.value.dialogType = 'info';
}
@ -241,6 +261,16 @@ const {
stats: quotationStats,
} = 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 () => {
pageState.gridView = $q.screen.lt.md ? true : false;
navigatorStore.current.title = 'quotation.title';
@ -299,6 +329,8 @@ async function fetchQuotationList(mobileFetch?: boolean) {
: 'Issued',
query: pageState.inputSearch,
urgentFirst: true,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
});
if (ret) {
@ -322,7 +354,12 @@ async function fetchQuotationList(mobileFetch?: boolean) {
}
watch(
[() => pageState.currentTab, () => pageState.inputSearch, quotationPageSize],
[
() => pageState.currentTab,
() => pageState.inputSearch,
() => pageState.searchDate,
quotationPageSize,
],
() => {
quotationPage.value = 1;
quotationData.value = [];
@ -489,6 +526,10 @@ async function storeDataLocal(id: string) {
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch v-model="pageState.searchDate" />
</template>
</q-input>
<div class="row col-md-5 justify-end" style="white-space: nowrap">
@ -911,30 +952,49 @@ async function storeDataLocal(id: string) {
<!-- NOTE: START - Customer / Employer Form -->
<DialogForm
hide-footer
ref="formDialogRef"
v-model:modal="customerFormState.dialogModal"
:title="
customerFormState.dialogType === 'create'
? $t(`general.add`, {
text: `${$t('customer.employer')} `,
})
: `${$t('customer.employer')} `
"
:submit="
() => {
submitCustomer();
<DialogContainer
:model-value="customerFormState.dialogModal"
:on-open="
async () => {
customerFormStore.resetForm(customerFormState.dialogType === 'create');
onCreateImageList = { selectedImage: '', list: [] };
customerFormState.customerImageUrl = '';
await fetchListOfOptionBranch();
await customerFormStore.addCurrentCustomerBranch();
}
"
:close="
:on-close="
() => {
customerFormState.dialogModal = false;
customerFormStore.resetForm(true);
setDefaultCustomer();
onCreateImageList = { selectedImage: '', list: [] };
}
"
>
<template #header>
<DialogHeader
:title="
customerFormState.dialogType === 'create'
? $t(`general.add`, {
text: `${$t('customer.employer')} `,
})
: `${$t('customer.employer')} `
"
>
<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
:class="{
'q-mx-lg q-my-md': $q.screen.gt.sm,
@ -942,10 +1002,10 @@ async function storeDataLocal(id: string) {
}"
>
<ProfileBanner
prefix="dialog"
v-if="customerFormData.customerBranch !== undefined"
active
hide-fade
prefix="dialog"
:fallback-cover="`/images/customer-${customerFormData.customerType}-banner-bg.jpg`"
:img="
customerFormState.customerImageUrl ||
@ -961,13 +1021,22 @@ async function storeDataLocal(id: string) {
"
:title="
customerFormData.customerType === 'PERS'
? `${customerFormData.customerBranch[0]?.firstName || ''} ${customerFormData.customerBranch[0]?.lastName || ''}`
: customerFormData.customerBranch[0]?.registerName || ''
? 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
"
:caption="
customerFormData.customerType === 'PERS'
? `${customerFormData.customerBranch[0]?.firstNameEN || ''} ${customerFormData.customerBranch[0]?.lastNameEN || ''}`
: customerFormData.customerBranch[0]?.registerNameEN || ''
? `${customerFormData.customerBranch[0]?.firstNameEN} ${customerFormData.customerBranch[0]?.lastNameEN}`
: customerFormData.customerBranch[0]?.registerNameEN
"
@view="
() => {
@ -981,143 +1050,295 @@ async function storeDataLocal(id: string) {
/>
</div>
<div
class="col"
style="flex: 1; width: 100%; overflow-y: auto"
id="customer-form"
:class="{
'q-px-lg q-pb-lg': $q.screen.gt.sm,
'q-px-md q-pb-sm': !$q.screen.gt.sm,
}"
>
<div
style="overflow-y: auto"
class="row full-width full-height surface-1 rounded bordered relative-position"
>
<div class="col surface-1 full-height rounded bordered scroll row">
<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"
class="col"
style="height: 100%; max-height: 100; overflow-y: auto"
v-if="$q.screen.gt.sm"
>
<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 class="q-py-md q-pl-md q-pr-sm">
<SideMenu
:menu="[
{
name: $t('form.field.basicInformation'),
anchor: 'form-basic-info-customer',
},
{
name: $t('customer.form.group.branch'),
anchor: 'form-branch-customer-branch',
useBtn: true,
},
...(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"
:active="{
background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)',
}"
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
class="col full-height rounded scroll row q-py-md q-pl-md q-pr-sm"
v-if="$q.screen.gt.sm"
>
<SideMenu
:menu="[
{
name: $t('form.customerInformation'),
anchor: 'form-information',
},
{
name: $t('customerBranch.tab.business'),
anchor: 'form-business',
},
{
name: $t('form.address'),
anchor: 'form-address',
},
]"
background="transparent"
:active="{
background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)',
}"
scroll-element="#branch-form"
/>
</div>
<div
class="col-12 col-md-10 full-height q-col-gutter-sm"
class="col-12 col-md-10"
:class="{
'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"
style="overflow-y: auto"
id="customer-form-content"
style="height: 100%; max-height: 100%; overflow-y: auto"
>
<EmployerFormAbout
id="form-information"
show-title
:index="'0'"
:customerType="customerFormData.customerType"
: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
<EmployerFormBasicInfo
prefixId="form"
v-if="
customerFormData.customerBranch !== undefined &&
customerFormData.customerBranch.length > 0
"
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
id="form-business"
show-title
prefix-id="dialog"
dense
outlined
:readonly="customerFormState.dialogType === 'info'"
v-model:bussiness-type="formDataCustomerBranch.businessType"
v-model:job-position="formDataCustomerBranch.jobPosition"
v-model:job-description="formDataCustomerBranch.jobDescription"
v-model:pay-date="formDataCustomerBranch.payDate"
v-model:pay-date-en="formDataCustomerBranch.payDateEN"
v-model:wage-rate="formDataCustomerBranch.wageRate"
v-model:wage-rate-text="formDataCustomerBranch.wageRateText"
/>
<AddressForm
id="form-address"
prefix-id="employer"
dense
outlined
use-employment
:title="$t('form.address')"
:addressTitle="$t('form.address')"
:addressTitleEN="$t('form.address', { suffix: '(EN)' })"
:readonly="customerFormState.dialogType === 'info'"
v-model:bussiness-type="formDataCustomerBranch.businessType"
v-model:address="formDataCustomerBranch.address"
v-model:address-en="formDataCustomerBranch.addressEN"
v-model:street="formDataCustomerBranch.street"
v-model:street-en="formDataCustomerBranch.streetEN"
v-model:moo="formDataCustomerBranch.moo"
v-model:moo-en="formDataCustomerBranch.mooEN"
v-model:soi="formDataCustomerBranch.soi"
v-model:soi-en="formDataCustomerBranch.soiEN"
v-model:province-id="formDataCustomerBranch.provinceId"
v-model:district-id="formDataCustomerBranch.districtId"
v-model:sub-district-id="formDataCustomerBranch.subDistrictId"
v-model:home-code="formDataCustomerBranch.homeCode"
/>
<template
v-for="(_, idx) in customerFormData.customerBranch"
:key="idx"
>
<!-- v-if="customerFormData.customerBranch" -->
<q-form
class="full-width"
greedy
@submit.prevent="
async () => {
if (!customerFormData.customerBranch) return;
if (customerFormData.customerType === 'PERS') {
tabFieldRequired.main = [
'citizenId',
'namePrefix',
'firstName',
'firstNameEN',
'lastName',
'lastNameEN',
'gender',
'birthDate',
];
}
if (customerFormData.customerType === 'CORP') {
tabFieldRequired.main = ['legalPersonNo', 'registerName'];
}
let tapIsUndefined = validateTabField(
customerFormData.customerBranch?.[idx],
tabFieldRequired,
);
if (tapIsUndefined.length > 0) {
return dialog({
color: 'warning',
icon: 'mdi-alert',
title: t('dialog.title.incompleteDataEntry'),
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>
</DialogForm>
</DialogContainer>
</template>
<style scoped></style>

View file

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

View file

@ -3,7 +3,7 @@ import { Icon } from '@iconify/vue';
import { ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { View } from './types.ts';
import { View } from './types';
import { formatNumberDecimal, commaInput } from 'stores/utils';
@ -14,7 +14,7 @@ import SelectInput from 'src/components/shared/SelectInput.vue';
import { storeToRefs } from 'pinia';
import { precisionRound } from 'src/utils/arithmetic';
import { PayCondition } from 'src/stores/quotations/types.ts';
import { PayCondition } from 'src/stores/quotations/types';
defineEmits<{
(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({
//passportNo: obj.data.passportNo,
//documentExpireDate: obj.data.documentExpireDate,
id: obj.data.id,
lastNameEN: obj.data.lastNameEN,
lastName: obj.data.lastName,
middleNameEN: obj.data.middleNameEN,

View file

@ -600,7 +600,7 @@ function print() {
details?.worker.map(
(v, i) =>
`${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 ProfileBanner from 'src/components/ProfileBanner.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 {
UndoButton,
@ -19,6 +20,8 @@ import {
} from 'src/components/button';
import AddressForm from 'src/components/form/AddressForm.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();
@ -27,10 +30,14 @@ const drawerModel = defineModel<boolean>('drawerModel', {
required: true,
default: false,
});
const onCreateImageList = defineModel<{
const imageListOnCreate = defineModel<{
selectedImage: 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({
imageDialog: false,
@ -43,6 +50,14 @@ const imageState = reactive({
const imageFile = ref<File | null>(null);
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(
defineProps<{
readonly?: boolean;
@ -73,6 +88,9 @@ const data = defineModel<InstitutionPayload>('data', {
group: '',
name: '',
nameEN: '',
contactName: '',
contactEmail: '',
contactTel: '',
code: '',
addressEN: '',
address: '',
@ -88,6 +106,19 @@ const data = defineModel<InstitutionPayload>('data', {
selectedImage: '',
},
});
const formBankBook = defineModel<BankBook[]>('formBankBook', {
default: [
{
bankName: '',
accountNumber: '',
bankBranch: '',
accountName: '',
accountType: '',
currentlyUse: true,
bankUrl: '',
},
],
});
function viewImage() {
imageState.imageDialog = true;
@ -141,10 +172,39 @@ async function submitImage(name: string) {
function clearImageState() {
imageState.imageDialog = false;
imageFile.value = null;
onCreateImageList.value = { selectedImage: '', list: [] };
imageListOnCreate.value = { selectedImage: '', list: [] };
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(
() => imageFile.value,
() => {
@ -159,7 +219,6 @@ watch(
imageList.value
? (imageList.value.selectedImage = data.value.selectedImage || '')
: '';
console.log(imageState.imageUrl);
imageState.refreshImageState = false;
},
);
@ -237,11 +296,15 @@ watch(
:menu="[
{
name: $t('form.field.basicInformation'),
anchor: 'agencies-basic-info',
anchor: 'agencies-form-basic-info',
},
{
name: $t('general.address'),
anchor: 'agencies-address-info',
anchor: 'agencies-form-address-info',
},
{
name: $t('agencies.bankInfo'),
anchor: 'agencies-form-bank-info',
},
]"
background="transparent"
@ -275,14 +338,17 @@ watch(
</div>
</div>
<FormBasicInfoAgencies
id="agencies-basic-info"
id="agencies-form-basic-info"
class="q-mb-xl"
v-model:group="data.group"
v-model:name="data.name"
v-model:name-en="data.nameEN"
v-model:contact-name="data.contactName"
v-model:email="data.contactEmail"
v-model:contact-tel="data.contactTel"
/>
<AddressForm
id="agencies-address-info"
id="agencies-form-address-info"
dense
:prefix-id="''"
v-model:address="data.address"
@ -297,6 +363,25 @@ watch(
v-model:district-id="data.districtId"
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>
</DialogForm>
@ -427,13 +512,17 @@ watch(
name: $t('general.address'),
anchor: 'agencies-address-info',
},
{
name: $t('agencies.bankInfo'),
anchor: 'agencies-bank-info',
},
]"
background="transparent"
:active="{
background: 'hsla(var(--blue-6-hsl) / .2)',
foreground: 'var(--blue-6)',
}"
scroll-element="#agencies-form-content"
scroll-element="#agencies-view-content"
/>
</div>
</div>
@ -444,7 +533,7 @@ watch(
'q-py-md q-pr-md ': $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"
>
<FormBasicInfoAgencies
@ -455,6 +544,9 @@ watch(
v-model:group="data.group"
v-model:name="data.name"
v-model:name-en="data.nameEN"
v-model:contact-name="data.contactName"
v-model:email="data.contactEmail"
v-model:contact-tel="data.contactTel"
/>
<AddressForm
id="agencies-address-info"
@ -473,6 +565,26 @@ watch(
v-model:district-id="data.districtId"
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>
@ -482,7 +594,7 @@ watch(
<ImageUploadDialog
v-model:dialog-state="imageState.imageDialog"
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:data-list="imageList"
:on-create="model"
@ -512,5 +624,31 @@ watch(
</div>
</template>
</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>
<style scoped></style>

View file

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

View file

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

View file

@ -2,14 +2,49 @@
import { ref } from 'vue';
import { MainButton } from 'components/button';
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;
requestWorkId: string;
}>();
const templateForm = defineModel<string>();
const templateFormOption = ref<
{ 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>
<template>
@ -24,7 +59,6 @@ const templateFormOption = ref<
<SelectInput
id="quotation-branch"
style="grid-column: span 2"
incremental
v-model="templateForm"
class="full-width"
:readonly
@ -32,21 +66,14 @@ const templateFormOption = ref<
:label="$t('quotation.templateForm')"
: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
solid
icon="mdi-pencil-outline"
color="207 96% 32%"
class="full-width"
style="grid-column: span 1"
:disabled="!templateForm"
@click="formDownload"
>
{{ $t('general.designForm') }}
</MainButton>

View file

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

View file

@ -13,6 +13,7 @@ const props = defineProps<{
readonly?: boolean;
step: Step;
responsibleAreaDistrictId?: string;
defaultMessenger?: string;
}>();
const emit = defineEmits<{
@ -85,7 +86,8 @@ function assignToForm() {
companyDuty: attributesForm.value.companyDuty ?? false,
companyDutyCost: attributesForm.value.companyDutyCost ?? 30,
responsibleUserLocal: attributesForm.value.responsibleUserLocal ?? true,
responsibleUserId: attributesForm.value.responsibleUserId ?? '',
responsibleUserId:
attributesForm.value.responsibleUserId || props.defaultMessenger,
individualDuty: attributesForm.value.individualDuty ?? false,
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 { getUserId } from 'src/services/keycloak';
import { QuotationFull } from 'src/stores/quotations/types';
import useUserStore from 'src/stores/user';
const { locale, t } = useI18n();
@ -62,7 +63,9 @@ const route = useRoute();
const optionStore = useOptionStore();
const requestListStore = useRequestList();
const flowTemplateStore = useWorkflowTemplate();
const userStore = useUserStore();
const currentUserGroup = ref<string[]>([]);
const workList = ref<RequestWork[]>([]);
const statusFile = ref<Attributes>({
customer: {},
@ -158,6 +161,10 @@ onMounted(async () => {
initTheme();
initLang();
const result = await userStore.fetchUserGroup();
currentUserGroup.value = result.map((v) => v.name);
// get data
await getData();
});
@ -283,26 +290,38 @@ async function triggerViewFile(opt: {
if (!opt.download) window.open(url, '_blank');
}
const responsiblePersonList = computed(() => {
const temp = workList.value?.reduce<Record<string, CreatedBy[]>>(
(acc, curr: RequestWork) => {
curr.productService.service?.workflow?.step.forEach((v) => {
const key = v.order.toString();
const responsibleList = computed(() => {
const temp = workList.value?.reduce<
Record<string, { user: CreatedBy[]; group: string[] }>
>((acc, curr: RequestWork) => {
curr.productService.service?.workflow?.step.forEach((v) => {
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) => {
if (current.find((rhs) => rhs.id === lhs.userId)) return;
current.push(lhs.user);
});
v.responsiblePerson.forEach((lhs) => {
if (current.user.find((rhs) => rhs.id === lhs.userId)) return;
current.user.push(lhs.user);
});
return acc;
},
{},
);
responsibleGroup.forEach((lhs) => {
if (current.group.find((rhs) => rhs === lhs)) return;
current.group.push(lhs);
});
});
return acc;
}, {});
return temp || {};
});
@ -438,6 +457,24 @@ async function submitRejectCancel() {
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>
<template>
<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>
<template
v-if="responsiblePersonList[pageState.currentStep]?.length"
v-if="responsibleList[pageState.currentStep]?.user.length"
>
<AvatarGroup
:data="
(responsiblePersonList[pageState.currentStep] || []).map(
:data="[
...(responsibleList[pageState.currentStep].user || []).map(
(v) => ({
name:
$i18n.locale === 'eng'
@ -494,8 +531,12 @@ async function submitRejectCancel() {
: `/no-img-female.png`
: `${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 v-else>-</template>
@ -701,6 +742,7 @@ async function submitRejectCancel() {
}"
>
<DataDisplay
clickable
class="col"
icon="mdi-account-settings-outline"
:label="$t('customer.employer')"
@ -710,8 +752,10 @@ async function submitRejectCancel() {
noCode: true,
}) || '-'
"
@label-click="toCustomer(data.quotation.customerBranch)"
/>
<DataDisplay
clickable
class="col"
icon="mdi-account-settings-outline"
:label="$t('customer.employee')"
@ -720,6 +764,7 @@ async function submitRejectCancel() {
locale: $i18n.locale,
}) || '-'
"
@label-click="toEmployee(data.employee)"
/>
<DataDisplay
class="col"
@ -776,11 +821,15 @@ async function submitRejectCancel() {
:cancel="data.requestDataStatus === RequestDataStatus.Canceled"
:readonly="
data.requestDataStatus === RequestDataStatus.Canceled ||
(responsiblePersonList &&
!!responsiblePersonList[pageState.currentStep]?.length &&
!responsiblePersonList[pageState.currentStep]?.find(
(responsibleList &&
!responsibleList[pageState.currentStep]?.user.find(
(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"
:installment-info="getInstallmentInfo()"
@ -873,6 +922,11 @@ async function submitRejectCancel() {
:readonly="
data.requestDataStatus === RequestDataStatus.Canceled
"
:default-messenger="
value.stepStatus[pageState.currentStep - 1]
? undefined
: data.defaultMessengerId
"
:step="{
step: pageState.currentStep,
requestWorkId: value.id || '',
@ -906,6 +960,7 @@ async function submitRejectCancel() {
/>
<FormExpansion
v-if="value._formExpansion"
:request-work-id="value.id"
:readonly="
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 { CreatedBy } from 'src/stores/types';
import { dateFormatJS } from 'src/utils/datetime';
const props = withDefaults(
defineProps<{
@ -21,6 +22,9 @@ const props = withDefaults(
grid?: boolean;
visibleColumns?: string[];
hideAction?: boolean;
hideView?: boolean;
checkable?: boolean;
listSameArea?: string[];
}>(),
{
row: () => [],
@ -36,9 +40,16 @@ defineEmits<{
(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 tempPerson: CreatedBy[] = [];
const tempGroup: {
group: string;
id: string;
workflowTemplateStepId: string;
}[] = [];
for (const v of productServiceList) {
const tempStep = v.service?.workflow?.step;
@ -49,7 +60,17 @@ function responsiblePerson(quotation: QuotationFull): CreatedBy[] | undefined {
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 (
{
['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'] || '-'
);
}
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>
<template>
<q-table
@ -106,12 +156,36 @@ function getEmployeeName(
card-container-class="q-col-gutter-sm"
:rows-per-page-options="[0]"
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">
<q-tr
style="background-color: hsla(var(--info-bg) / 0.07)"
: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">
{{ col.label && $t(col.label) }}
</q-th>
@ -125,9 +199,30 @@ function getEmployeeName(
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
>
<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"
>
<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')">
{{ props.rowIndex + 1 }}
</q-td>
@ -138,21 +233,50 @@ function getEmployeeName(
</div>
</q-td>
<q-td v-if="visibleColumns.includes('employer')" class="text-left">
{{
getCustomerName(props.row, {
noCode: true,
locale: $i18n.locale,
}) || '-'
}}
<span
class="link"
@click="toCustomer(props.row.quotation.customerBranch)"
>
{{
getCustomerName(props.row, {
noCode: true,
locale: $i18n.locale,
}) || '-'
}}
</span>
</q-td>
<q-td v-if="visibleColumns.includes('employee')" class="text-left">
{{ getEmployeeName(props.row, { locale: $i18n.locale }) || '-' }}
<span class="link" @click="toEmployee(props.row.employee)">
{{ getEmployeeName(props.row, { locale: $i18n.locale }) || '-' }}
</span>
</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')">
{{ props.row.quotation.code || '-' }}
</q-td>
<q-td v-if="visibleColumns.includes('responsiblePerson')">
<AvatarGroup
<!-- <AvatarGroup
:data="
responsiblePerson(props.row.quotation)?.map((v) => {
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 v-if="visibleColumns.includes('status')">
<BadgeComponent
@ -215,6 +358,7 @@ function getEmployeeName(
</q-td>
<q-td class="text-right">
<q-btn
v-if="!hideView"
:id="`btn-eye-${props.row.code}`"
icon="mdi-eye-outline"
size="sm"
@ -313,22 +457,29 @@ function getEmployeeName(
</div>
<div class="col-8">
<AvatarGroup
v-if="(responsiblePerson(props.row.quotation) ?? []).length > 0"
:data="
responsiblePerson(props.row.quotation)?.map((v) => {
return {
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}`,
};
})
v-if="
(responsiblePerson(props.row.quotation).user ?? []).length >
0 ||
(responsiblePerson(props.row.quotation).group ?? []).length >
0
"
: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',
})),
]"
/>
<span v-else>-</span>
</div>
@ -406,4 +557,15 @@ function getEmployeeName(
background: var(--red-8);
}
}
.link {
color: hsl(var(--info-bg));
text-decoration: underline;
cursor: pointer;
}
.disabled-row {
opacity: 0.3;
filter: grayscale(1);
}
</style>

View file

@ -28,6 +28,24 @@ export const column = [
label: 'customer.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',

View file

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

View file

@ -160,7 +160,12 @@ const emit = defineEmits<{
</q-tooltip>
</div>
<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>
</q-td>
<q-td v-if="visibleColumns.includes('issueBranch')">

View file

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

View file

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

View file

@ -11,6 +11,8 @@ import { baseUrl, formatNumberDecimal, commaInput } from 'src/stores/utils';
import { precisionRound } from 'src/utils/arithmetic';
import { useConfigStore } from 'stores/config';
import { storeToRefs } from 'pinia';
import BadgeComponent from 'src/components/BadgeComponent.vue';
import { TaskStatus } from 'src/stores/task-order/types';
const currentBtnOpen = ref<boolean[]>([]);
const configStore = useConfigStore();
@ -30,7 +32,10 @@ const props = defineProps<{
readonly?: boolean;
agentPrice?: boolean;
taskList: {
product: RequestWork['productService']['product'];
product: RequestWork['productService']['product'] & {
taskStatus?: TaskStatus;
totalNotStatusComplete?: number;
};
list: RequestWork[];
}[];
creditNote?: boolean;
@ -111,6 +116,26 @@ function calcPrice(
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>
<template>
<q-expansion-item
@ -144,7 +169,8 @@ function calcPrice(
(v) =>
v.name !== 'discount' &&
v.name !== 'priceBeforeVat' &&
v.name !== 'vat',
v.name !== 'vat' &&
v.name !== 'status',
)
: productColumn
"
@ -173,7 +199,10 @@ function calcPrice(
<template
v-slot:body="props: {
row: {
product: RequestWork['productService']['product'];
product: RequestWork['productService']['product'] & {
taskStatus?: TaskStatus;
totalNotStatusComplete?: number;
};
list: RequestWork[];
};
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
@ -203,6 +232,14 @@ function calcPrice(
</q-avatar>
{{ props.row.product.name }}
</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>
{{ props.row.list.length }}
</q-td>

View file

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

View file

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

View file

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

View file

@ -248,6 +248,7 @@ function calcPricePerUnit(product: RequestWork['productService']['product']) {
function calcPrice(
product: RequestWork['productService']['product'],
amount: number,
vat: number = 0,
) {
const pricePerUnit = agentPrice.value ? product.agentPrice : product.price;
@ -256,7 +257,8 @@ function calcPrice(
: pricePerUnit;
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);
}
@ -346,7 +348,7 @@ function closeAble() {
<td style="text-align: center">
{{
formatNumberDecimal(
calcPrice(v.product.product, v.list.length),
calcPrice(v.product.product, v.list.length, v.product.vat),
2,
)
}}
@ -431,7 +433,7 @@ function closeAble() {
class="column set-width bg-color full-height"
style="padding: 12px"
>
({{ ThaiBahtText(summaryPrice.finalPrice) }})
({{ ThaiBahtText(precisionRound(summaryPrice.finalPrice)) }})
</div>
<div
class="row text-right border-5 items-center"
@ -494,7 +496,7 @@ function closeAble() {
details?.worker.map(
(v, i) =>
`${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 { dialogWarningClose } from 'src/stores/utils';
import { useQuasar } from 'quasar';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const $q = useQuasar();
const { t } = useI18n();
@ -46,6 +47,7 @@ const pageState = reactive({
total: 0,
debitDialog: false,
searchDate: [],
});
const fieldSelectedOption = computed(() => {
@ -68,6 +70,8 @@ async function getList(opts?: { page?: number; pageSize?: number }) {
? undefined
: pageState.currentTab) as DebitNoteStatus,
includeRegisteredBranch: true,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
});
if (res) {
@ -149,6 +153,7 @@ watch(
() => pageState.inputSearch,
() => pageSize.value,
() => pageState.statusFilter,
() => pageState.searchDate,
],
() => getList(),
);
@ -256,6 +261,10 @@ watch(
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch v-model="pageState.searchDate" />
</template>
</q-input>
<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 { DebitNote, useDebitNote } from 'src/stores/debit-note';
import { columns } from './constants.ts';
import { columns } from './constants';
import KebabAction from 'src/components/shared/KebabAction.vue';
const debitNote = useDebitNote();

View file

@ -501,7 +501,7 @@ function print() {
details?.worker.map(
(v, i) =>
`${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 { usePayment, useReceipt } from 'src/stores/payment';
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 navigatorStore = useNavigator();
@ -26,7 +27,6 @@ const receiptStore = useReceipt();
const { data, page, pageMax, pageSize } = storeToRefs(receiptStore);
// NOTE: Variable
const refFilter = ref<InstanceType<typeof QSelect>>();
const pageState = reactive({
hideStat: false,
@ -35,6 +35,7 @@ const pageState = reactive({
fieldSelected: [...columns.map((v) => v.name)],
gridView: false,
total: 0,
searchDate: [],
});
const fieldSelectedOption = computed(() => {
@ -49,6 +50,8 @@ async function fetchList(opts?: { rotateFlowId?: boolean }) {
page: page.value,
pageSize: pageSize.value,
query: pageState.inputSearch,
startDate: pageState.searchDate[0],
endDate: pageState.searchDate[1],
});
if (ret) {
data.value = $q.screen.xs ? [...data.value, ...ret.result] : ret.result;
@ -95,6 +98,7 @@ watch(
() => pageState.inputSearch,
() => pageState.statusFilter,
() => pageSize.value,
() => pageState.searchDate,
],
() => {
page.value = 1;
@ -172,25 +176,43 @@ watch(
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
<template v-if="$q.screen.lt.md" v-slot:append>
<span class="row">
<q-separator vertical />
<q-btn
icon="mdi-filter-variant"
unelevated
class="q-ml-sm"
padding="4px"
size="sm"
rounded
@click="refFilter?.showPopup"
<template v-slot:append>
<q-separator vertical inset class="q-mr-xs" />
<AdvanceSearch
v-model="pageState.searchDate"
:active="$q.screen.lt.md && pageState.statusFilter !== 'None'"
>
<div
v-if="$q.screen.lt.md"
class="q-mt-sm text-weight-medium"
>
{{ $t('general.status') }}
</div>
<q-select
v-if="$q.screen.lt.md"
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>
</q-input>
<div class="row col-md-5" style="white-space: nowrap">
<q-select
v-show="$q.screen.gt.sm"
v-if="$q.screen.gt.sm"
ref="refFilter"
v-model="pageState.statusFilter"
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 {
createMemoryHistory,
createRouter,
@ -17,7 +17,7 @@ import routes from './routes';
* with the Router instance.
*/
export default route(function (/* { store, ssrContext } */) {
export default defineRouter(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'

View file

@ -155,6 +155,16 @@ const routes: RouteRecordRaw[] = [
name: 'ManualView',
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];
}
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 {
fetchOffice,
fetchOfficeById,
fetchProvince,
fetchDistrictByProvinceId,
fetchSubDistrictByProvinceId,
listSameOfficeArea,
};
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,9 @@ export const useInstitution = defineStore('institution-store', () => {
group?: string;
status?: Status;
payload?: { group?: string[] };
startDate?: string;
endDate?: string;
activeOnly?: boolean;
}) {
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) {
return res.data;
}
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}`, {
...data,
id: 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) {
return res.data;
}

View file

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

View file

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

View file

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

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