Merge branch 'develop'
This commit is contained in:
commit
35ae5a918b
353 changed files with 46993 additions and 11837 deletions
325
CHANGELOG.md
Normal file
325
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [unreleased]
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(date-picker)* Readonly and disable handle
|
||||
- Add abort upload controller as function args
|
||||
- Add abort upload and upload progress handler
|
||||
- Add workflow template store
|
||||
- Add type
|
||||
- Search and paging workflow template
|
||||
- Add query to store
|
||||
- Add api function invoice
|
||||
- Add status filter
|
||||
- Ad i19n
|
||||
- Add field to type
|
||||
- Add i18n
|
||||
- Update quotation card
|
||||
- Add switch component
|
||||
- Also allow other type (num or enum)
|
||||
- Add icon support for badge component
|
||||
- Detect quotation status
|
||||
- Change view by condition
|
||||
- Add type payment
|
||||
- Add view type
|
||||
- Add dynamic color to quotation form info
|
||||
- Convert back installment no function
|
||||
- Filter by installment no
|
||||
- Send selected installment no to preview
|
||||
- Payment view
|
||||
- Add shared product group
|
||||
- Add params
|
||||
- Remove flow from store
|
||||
- Shared product group input
|
||||
- Add tooltip on hover icon nly button
|
||||
- Don't allow edit after accepted
|
||||
- Paycondition
|
||||
- Also get installment no from preset data
|
||||
- Hide expire when condition met
|
||||
- Add util for array management
|
||||
- Add fetch payment fn
|
||||
- Update payment stores
|
||||
- Add attachment manager for payment store
|
||||
- Add ref type for data
|
||||
- Abstract function
|
||||
- Add utils function for array
|
||||
- Add notifcation store
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- I18n
|
||||
- Receipt dialog
|
||||
- *(05)* Slip file display
|
||||
- Employee customerBranch display (registerName)
|
||||
- *(quotation-preview)* Discount not show
|
||||
- Price calc
|
||||
- Changing language doesn't work
|
||||
- *(05)* Layout space and scroll
|
||||
- *(01)* Search
|
||||
- Quotation attachment
|
||||
- *(04)* Payment type option
|
||||
- *(01)* Search no data
|
||||
- *(05)* Total padi remain display condition
|
||||
- Double upload attachment
|
||||
- *(04)* Search id, flow dialog
|
||||
- Typo
|
||||
- Type
|
||||
- Search no data, data length display
|
||||
- *(02)* Padding
|
||||
- *(04)* Summary
|
||||
- Pagination
|
||||
- Workflow page
|
||||
- Wrong type
|
||||
- Workflow template => flow form data, scroll to last item, sidemenu active
|
||||
- *(03)* Tel
|
||||
- Comma input
|
||||
- Total not update on change tab
|
||||
- Type wf function
|
||||
- Status not update on mount
|
||||
- Assign step id
|
||||
- Error import
|
||||
- *(04)* Service type and store
|
||||
- Main btn & select input component props, etc
|
||||
- Workflow change status
|
||||
- *(04)* Service with workflow
|
||||
- Pagination boundary
|
||||
- Service work product attributes (workflow)
|
||||
- Change workflow template on work, undo issue
|
||||
- Work product installmentNo
|
||||
- Workname
|
||||
- Typos
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- Get customerBranch by id
|
||||
- Handle telephoneNo
|
||||
- Handle show moo
|
||||
- Assign after submit
|
||||
- Format value before peview
|
||||
- Add id
|
||||
- Add query
|
||||
- Search quotation
|
||||
- *(utils)* Always append currency .00
|
||||
- *(form-quotation)* Layout
|
||||
- Add type Payment
|
||||
- Create payment file
|
||||
- Edit btn add
|
||||
- Move remark
|
||||
- Add installments type to service
|
||||
- By dueDate
|
||||
- *(05)* Upload slip
|
||||
- Add type installmentNo
|
||||
- Add input installmentNo
|
||||
- Handle negative value
|
||||
- Handle max-width
|
||||
- Edit name value filName -> nameField
|
||||
- Upload file card component
|
||||
- Payment status i18n, danger color
|
||||
- ReceiptDialog
|
||||
- Flowdialog
|
||||
- Use built in query string instead
|
||||
- I18n
|
||||
- Workflow dialog & mock data
|
||||
- Add installmentNo in node
|
||||
- Add tab status quotation
|
||||
- Delete title product
|
||||
- Add installmentNo
|
||||
- Edit layout input
|
||||
- Edit status
|
||||
- Tel i18n
|
||||
- *(04 flow)* Type and create function
|
||||
- Flow
|
||||
- Add i18n
|
||||
- Show expiration date
|
||||
- Move function to utile
|
||||
- Use i18n
|
||||
- Calculate days expire
|
||||
- Handle show date expire
|
||||
- Create BadgeCompoent
|
||||
- Use BadgeCompoent
|
||||
- Allow non i118n text to be passed
|
||||
- Add type QuotationStatus
|
||||
- Handle QuotationStatus at create
|
||||
- Create function changeStatus
|
||||
- Create function accepted
|
||||
- Create submitAccepted
|
||||
- Test submit
|
||||
- Adjust spacing
|
||||
- Remove param
|
||||
- Use icon on quotation card
|
||||
- Prepare for switch view
|
||||
- Workflow
|
||||
- Handle rules registerName
|
||||
- Convert numbers into thai text
|
||||
- Add agentUserId
|
||||
- Add type agentUserId
|
||||
- Handle field only lagelPersonNo , registerName
|
||||
- Handle type number
|
||||
- Handle codeHome Not required
|
||||
- Use variable for color
|
||||
- Get stats customer
|
||||
- Handle homeCode
|
||||
- Set default
|
||||
- Add payment store skeleton
|
||||
- Use icon instead of character
|
||||
- Handle nrcNO not required
|
||||
- Show namePrefix
|
||||
- Handle employmentOffice
|
||||
- NrcNo can is null
|
||||
- Handle agentUserId
|
||||
- Handle fiel required form
|
||||
- Hide add customer at quotation
|
||||
- Add new column
|
||||
- Edit column
|
||||
- Switch nameEN
|
||||
- Handle data is null
|
||||
- Edit agent -> agentUserId
|
||||
- Remove date from installments
|
||||
- Add selectedAll
|
||||
- Type paySplit add invoice
|
||||
- Add table paySplit
|
||||
- Edit table paySplit
|
||||
- Id is null
|
||||
- Invoice is null
|
||||
- Add page invoice
|
||||
- File name
|
||||
- Project structure (1)
|
||||
- Project structure (2)
|
||||
- Update status
|
||||
- Handle btn save
|
||||
- Project structure (3)
|
||||
- Util fn
|
||||
- Handle peview mod
|
||||
- Delete log
|
||||
- Extract navigator into store instead
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Change variable name
|
||||
- Remove unused
|
||||
- Format
|
||||
- Deprecate function
|
||||
- Add deprecated function
|
||||
|
||||
### Refactro
|
||||
|
||||
- By installmentNo
|
||||
- Add i18n
|
||||
|
||||
## [0.4.2] - 2024-10-21
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- Use session storage instead
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Clean
|
||||
|
||||
## [0.4.1] - 2024-10-18
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Error undefined
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- Final price width
|
||||
|
||||
## [0.4.0] - 2024-10-18
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add vat excluded calc
|
||||
- Store data for preview
|
||||
- Disable view mode
|
||||
- Preview route and trigger preview
|
||||
- Add print button
|
||||
- *(i18n)* Add text
|
||||
- *(doc-preview)* Add toolbar
|
||||
- Add preview footer
|
||||
- Remark
|
||||
- Update button and spacing
|
||||
- Display company name footer
|
||||
- Add additional info to preview
|
||||
- Detect edit mode
|
||||
- Change mode on reset
|
||||
- Detect if closeable
|
||||
- Close button
|
||||
- Add label
|
||||
- Store it full response as source
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(05)* End of month installments
|
||||
- Paysplit assign & info display
|
||||
- Stats not update when change tab with data updated
|
||||
- Home page menu not working
|
||||
- *(05)* Node to selected product
|
||||
- Worker display number, expire date
|
||||
- *(04)* Pay type
|
||||
- Background
|
||||
- Readonly quotation info
|
||||
- *(05)* Display final price on quotation card
|
||||
- PricePerUnit calc
|
||||
- Missing import
|
||||
- Price scope
|
||||
- Name
|
||||
- Wrong calc
|
||||
- *(05)* Watch paysplit
|
||||
- Quotation discount
|
||||
- Remove button
|
||||
- Type error
|
||||
- Disabled / readonly field background
|
||||
- *(05)* Product table
|
||||
- Display name in table
|
||||
- Split date
|
||||
- Hide toggle status
|
||||
- Reset not actually reset
|
||||
- Typo
|
||||
- Delete wrong row
|
||||
- Warning color
|
||||
- I18n
|
||||
- Form info split input
|
||||
- Readonly editor
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- Create fetchOption
|
||||
- Create BankComponents
|
||||
- Use bank
|
||||
- Set Option
|
||||
- Add id
|
||||
- Add i18n
|
||||
- Filter bank
|
||||
- By value
|
||||
- Add closeTab
|
||||
- Use app button
|
||||
- New tab
|
||||
- Calculate value
|
||||
- Add remark
|
||||
- By remark
|
||||
- Set value default
|
||||
- Placeholder
|
||||
- Fetchby id branch
|
||||
- Edit layout bank
|
||||
- By bank
|
||||
- Change button
|
||||
- Change to secondary button
|
||||
- WarningClose
|
||||
- Handle null
|
||||
- Price data product
|
||||
- Receipt dialog & type
|
||||
- Add type
|
||||
- By value at viewHeader
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Clean
|
||||
- Clean log
|
||||
|
||||
<!-- generated by git-cliff -->
|
||||
89
cliff.toml
Normal file
89
cliff.toml
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# git-cliff ~ default configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
#
|
||||
# Lines starting with "#" are comments.
|
||||
# Configuration options are organized into tables and keys.
|
||||
# See documentation for more information on available options.
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="version-") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits | unique(attribute="message") %}
|
||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||
{% if commit.breaking %}💥**breaking**💥 {% endif %}\
|
||||
{{ commit.message | upper_first }}\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# template for the changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
# remove the leading and trailing s
|
||||
trim = true
|
||||
# postprocessors
|
||||
postprocessors = [
|
||||
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
|
||||
]
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
# Replace issue numbers
|
||||
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
||||
# Check spelling of the commit with https://github.com/crate-ci/typos
|
||||
# If the spelling is incorrect, it will be automatically fixed.
|
||||
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(deps.*\\)", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# regex for matching git tags
|
||||
# tag_pattern = "v[0-9].*"
|
||||
# regex for skipping tags
|
||||
# skip_tags = ""
|
||||
# regex for ignoring tags
|
||||
# ignore_tags = ""
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
||||
# limit the number of commits included in the changelog.
|
||||
limit_commits = 300
|
||||
|
|
@ -11,7 +11,8 @@
|
|||
"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"
|
||||
"build": "quasar build",
|
||||
"changelog:generate": "git-cliff -o CHANGELOG.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.16.12",
|
||||
|
|
@ -30,8 +31,10 @@
|
|||
"socket.io-client": "^4.7.5",
|
||||
"tesseract.js": "^5.1.1",
|
||||
"thai-baht-text": "^2.0.5",
|
||||
"udsv": "^0.6.0",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "^3.4.38",
|
||||
"vue-dragscroll": "^4.0.6",
|
||||
"vue-i18n": "^9.14.0",
|
||||
"vue-pdf": "^4.3.0",
|
||||
"vue-router": "^4.4.3"
|
||||
|
|
@ -51,7 +54,8 @@
|
|||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.5.4"
|
||||
"typescript": "^5.5.4",
|
||||
"vue-component-type-helpers": "^2.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24 || ^22 || ^20 || ^18",
|
||||
|
|
|
|||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
|
|
@ -56,12 +56,18 @@ importers:
|
|||
thai-baht-text:
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5
|
||||
udsv:
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0
|
||||
uuid:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
vue:
|
||||
specifier: ^3.4.38
|
||||
version: 3.4.38(typescript@5.5.4)
|
||||
vue-dragscroll:
|
||||
specifier: ^4.0.6
|
||||
version: 4.0.6(typescript@5.5.4)
|
||||
vue-i18n:
|
||||
specifier: ^9.14.0
|
||||
version: 9.14.0(vue@3.4.38(typescript@5.5.4))
|
||||
|
|
@ -117,6 +123,9 @@ importers:
|
|||
typescript:
|
||||
specifier: ^5.5.4
|
||||
version: 5.5.4
|
||||
vue-component-type-helpers:
|
||||
specifier: ^2.1.10
|
||||
version: 2.1.10
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -3395,6 +3404,9 @@ packages:
|
|||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
udsv@0.6.0:
|
||||
resolution: {integrity: sha512-na+0EoqqpDeNKZ0HVTtgYtFP9aQgsMwPM77UEK7g4OX2C42w+Qw7QZs9t1ocDGLidtcKJnsPy+o5XrBYaZfzCA==}
|
||||
|
||||
ufo@1.5.3:
|
||||
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
|
||||
|
||||
|
|
@ -3507,6 +3519,9 @@ packages:
|
|||
vm-browserify@1.1.2:
|
||||
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
|
||||
|
||||
vue-component-type-helpers@2.1.10:
|
||||
resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -3518,6 +3533,9 @@ packages:
|
|||
'@vue/composition-api':
|
||||
optional: true
|
||||
|
||||
vue-dragscroll@4.0.6:
|
||||
resolution: {integrity: sha512-zW1k58p41yhmFhmg/JxfesUM4Srl0JfXg7xSINqffVGpHJKvnEHMK4QgF6mUVkPMTgibn976fhPYkomcXPvvFA==}
|
||||
|
||||
vue-eslint-parser@9.4.3:
|
||||
resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
|
|
@ -7345,6 +7363,8 @@ snapshots:
|
|||
|
||||
typescript@5.5.4: {}
|
||||
|
||||
udsv@0.6.0: {}
|
||||
|
||||
ufo@1.5.3: {}
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
|
|
@ -7431,10 +7451,18 @@ snapshots:
|
|||
|
||||
vm-browserify@1.1.2: {}
|
||||
|
||||
vue-component-type-helpers@2.1.10: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.4.38(typescript@5.5.4)):
|
||||
dependencies:
|
||||
vue: 3.4.38(typescript@5.5.4)
|
||||
|
||||
vue-dragscroll@4.0.6(typescript@5.5.4):
|
||||
dependencies:
|
||||
vue: 3.4.38(typescript@5.5.4)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
vue-eslint-parser@9.4.3(eslint@8.57.0):
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
|
|
|
|||
BIN
public/images/building-banner.png
Normal file
BIN
public/images/building-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
BIN
public/images/quotation-avatar-border.png
Normal file
BIN
public/images/quotation-avatar-border.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
|
@ -1,5 +1,51 @@
|
|||
{
|
||||
"eng": {
|
||||
"workerStatus": [
|
||||
{
|
||||
"label": "Normal",
|
||||
"value": "normal"
|
||||
},
|
||||
{
|
||||
"label": "Service Canceled",
|
||||
"value": "canceled"
|
||||
},
|
||||
{
|
||||
"label": "Resigned",
|
||||
"value": "resigned"
|
||||
},
|
||||
{
|
||||
"label": "Absconded",
|
||||
"value": "absconded"
|
||||
},
|
||||
{
|
||||
"label": "Deceased",
|
||||
"value": "deceased"
|
||||
},
|
||||
{
|
||||
"label": "Repatriated to Home Country",
|
||||
"value": "repatriated"
|
||||
},
|
||||
{
|
||||
"label": "Did Not Enter Thailand",
|
||||
"value": "not_entered"
|
||||
}
|
||||
],
|
||||
|
||||
"workerType": [
|
||||
{
|
||||
"label": "Bangkok Bank",
|
||||
"value": "mou"
|
||||
},
|
||||
{
|
||||
"label": "Nationality Verification Group",
|
||||
"value": "nvg"
|
||||
},
|
||||
{
|
||||
"label": "Border Pass Group",
|
||||
"value": "bp"
|
||||
}
|
||||
],
|
||||
|
||||
"bankBook": [
|
||||
{
|
||||
"label": "Bangkok Bank",
|
||||
|
|
@ -53,131 +99,62 @@
|
|||
}
|
||||
],
|
||||
"typeProduct": [
|
||||
{
|
||||
"label": "AC",
|
||||
"value": "AC"
|
||||
},
|
||||
{
|
||||
"label": "DOE",
|
||||
"value": "DOE"
|
||||
},
|
||||
{
|
||||
"label": "HP",
|
||||
"value": "HP"
|
||||
},
|
||||
{
|
||||
"label": "IMM",
|
||||
"value": "IMM"
|
||||
},
|
||||
{
|
||||
"label": "MOUC",
|
||||
"value": "MOUC"
|
||||
},
|
||||
{
|
||||
"label": "MOUL",
|
||||
"value": "MOUL"
|
||||
},
|
||||
{
|
||||
"label": "TM",
|
||||
"value": "TM"
|
||||
},
|
||||
{
|
||||
"label": "VS",
|
||||
"value": "VS"
|
||||
},
|
||||
{
|
||||
"label": "PPC",
|
||||
"value": "PPC"
|
||||
},
|
||||
{
|
||||
"label": "AB",
|
||||
"value": "AB"
|
||||
},
|
||||
{
|
||||
"label": "PPL",
|
||||
"value": "PPL"
|
||||
},
|
||||
{
|
||||
"label": "PJ",
|
||||
"value": "PJ"
|
||||
},
|
||||
{
|
||||
"label": "EBS",
|
||||
"value": "EBS"
|
||||
},
|
||||
{
|
||||
"label": "CI",
|
||||
"value": "CI"
|
||||
},
|
||||
{
|
||||
"label": "AD",
|
||||
"value": "AD"
|
||||
},
|
||||
{
|
||||
"label": "WO",
|
||||
"value": "WO"
|
||||
},
|
||||
{
|
||||
"label": "AE",
|
||||
"value": "AE"
|
||||
},
|
||||
|
||||
{
|
||||
"label": "AB1",
|
||||
"value": "AB1"
|
||||
},
|
||||
{
|
||||
"label": "VS1",
|
||||
"value": "VS1"
|
||||
},
|
||||
{
|
||||
"label": "PPC1",
|
||||
"value": "PPC1"
|
||||
},
|
||||
{
|
||||
"label": "PPL1",
|
||||
"value": "PPL1"
|
||||
},
|
||||
{
|
||||
"label": "AC1",
|
||||
"value": "AC1"
|
||||
},
|
||||
{
|
||||
"label": "CI1",
|
||||
"value": "CI1"
|
||||
},
|
||||
{
|
||||
"label": "AD1",
|
||||
"value": "AD1"
|
||||
},
|
||||
{
|
||||
"label": "MOUL1",
|
||||
"value": "MOUL1"
|
||||
},
|
||||
{
|
||||
"label": "MOUL3",
|
||||
"value": "MOUL3"
|
||||
},
|
||||
{
|
||||
"label": "MOUC1",
|
||||
"value": "MOUC1"
|
||||
},
|
||||
{
|
||||
"label": "HP1",
|
||||
"value": "HP1"
|
||||
},
|
||||
{
|
||||
"label": "AE3",
|
||||
"value": "AE3"
|
||||
},
|
||||
{
|
||||
"label": "CAR",
|
||||
"value": "CAR"
|
||||
},
|
||||
{
|
||||
"label": "AD3",
|
||||
"value": "AD3"
|
||||
}
|
||||
{ "label": "AB", "value": "AB" },
|
||||
{ "label": "AB1", "value": "AB1" },
|
||||
{ "label": "AC", "value": "AC" },
|
||||
{ "label": "AC1", "value": "AC1" },
|
||||
{ "label": "AC2", "value": "AC2" },
|
||||
{ "label": "AD", "value": "AD" },
|
||||
{ "label": "AD1", "value": "AD1" },
|
||||
{ "label": "AD3", "value": "AD3" },
|
||||
{ "label": "AE", "value": "AE" },
|
||||
{ "label": "AE3", "value": "AE3" },
|
||||
{ "label": "CAR", "value": "CAR" },
|
||||
{ "label": "CI", "value": "CI" },
|
||||
{ "label": "CI1", "value": "CI1" },
|
||||
{ "label": "CI2", "value": "CI2" },
|
||||
{ "label": "DOE", "value": "DOE" },
|
||||
{ "label": "EBS", "value": "EBS" },
|
||||
{ "label": "GI", "value": "GI" },
|
||||
{ "label": "GO", "value": "GO" },
|
||||
{ "label": "HP", "value": "HP" },
|
||||
{ "label": "HP1", "value": "HP1" },
|
||||
{ "label": "HP2", "value": "HP2" },
|
||||
{ "label": "IMM", "value": "IMM" },
|
||||
{ "label": "MOUC", "value": "MOUC" },
|
||||
{ "label": "MOUC1", "value": "MOUC1" },
|
||||
{ "label": "MOUC2", "value": "MOUC2" },
|
||||
{ "label": "MOUL", "value": "MOUL" },
|
||||
{ "label": "MOUL1", "value": "MOUL1" },
|
||||
{ "label": "MOUL3", "value": "MOUL3" },
|
||||
{ "label": "MOUL4", "value": "MOUL4" },
|
||||
{ "label": "OI", "value": "OI" },
|
||||
{ "label": "PJ", "value": "PJ" },
|
||||
{ "label": "PPC", "value": "PPC" },
|
||||
{ "label": "PPC1", "value": "PPC1" },
|
||||
{ "label": "PPC2", "value": "PPC2" },
|
||||
{ "label": "PPL", "value": "PPL" },
|
||||
{ "label": "PPL1", "value": "PPL1" },
|
||||
{ "label": "PPL2", "value": "PPL2" },
|
||||
{ "label": "PREC1", "value": "PREC1" },
|
||||
{ "label": "PREC2", "value": "PREC2" },
|
||||
{ "label": "PREC3", "value": "PREC3" },
|
||||
{ "label": "PREC4", "value": "PREC4" },
|
||||
{ "label": "PREM1", "value": "PREM1" },
|
||||
{ "label": "PREM2", "value": "PREM2" },
|
||||
{ "label": "PREM3", "value": "PREM3" },
|
||||
{ "label": "PREM4", "value": "PREM4" },
|
||||
{ "label": "TB", "value": "TB" },
|
||||
{ "label": "TB1", "value": "TB1" },
|
||||
{ "label": "TB2", "value": "TB2" },
|
||||
{ "label": "TM", "value": "TM" },
|
||||
{ "label": "TM3", "value": "TM3" },
|
||||
{ "label": "VS", "value": "VS" },
|
||||
{ "label": "VS1", "value": "VS1" },
|
||||
{ "label": "VS2", "value": "VS2" },
|
||||
{ "label": "WO", "value": "WO" },
|
||||
{ "label": "WP390", "value": "WP390" },
|
||||
{ "label": "WP44", "value": "WP44" }
|
||||
],
|
||||
|
||||
"prefix": [
|
||||
|
|
@ -513,29 +490,55 @@
|
|||
}
|
||||
],
|
||||
|
||||
"insurancePlace": [
|
||||
"checkupResults": [
|
||||
{
|
||||
"label": "Pacific Cross",
|
||||
"value": "pacificcross"
|
||||
"label": "Normal Results",
|
||||
"value": "normal_results"
|
||||
},
|
||||
{
|
||||
"label": "Dhipaya Insurance",
|
||||
"value": "dhipaya"
|
||||
"label": "Follow-up Treatment",
|
||||
"value": "follow_up_treatment"
|
||||
},
|
||||
{
|
||||
"label": "Lerdsin Hospital",
|
||||
"value": "lerdsin"
|
||||
},
|
||||
{
|
||||
"label": "Ratchapiphat Hospital",
|
||||
"value": "ratchapipat"
|
||||
},
|
||||
{
|
||||
"label": "Charoenkrung Pracharak Hospital",
|
||||
"value": "krungPracharak"
|
||||
"label": "Failed Health Checkup",
|
||||
"value": "failed_checkup"
|
||||
}
|
||||
],
|
||||
|
||||
"insurancePlace": [
|
||||
{
|
||||
"label": "Medical Checkup + Social Insurance",
|
||||
"value": "social_insurance"
|
||||
},
|
||||
{
|
||||
"label": "Medical Checkup + 990 Insurance",
|
||||
"value": "insurance_990"
|
||||
},
|
||||
{
|
||||
"label": "Medical Checkup + Government Hospital 3 Months",
|
||||
"value": "gov_hospital_3m"
|
||||
},
|
||||
{
|
||||
"label": "Medical Checkup + Government Hospital 6 Months",
|
||||
"value": "gov_hospital_6m"
|
||||
},
|
||||
{
|
||||
"label": "Medical Checkup + Government Hospital 1 Year",
|
||||
"value": "gov_hospital_1y"
|
||||
},
|
||||
{
|
||||
"label": "Medical Checkup + Government Hospital 1 Year 3 Months",
|
||||
"value": "gov_hospital_1y_3m"
|
||||
},
|
||||
{
|
||||
"label": "Medical Checkup + Government Hospital 1 Year 6 Months",
|
||||
"value": "gov_hospital_1y_6m"
|
||||
},
|
||||
{
|
||||
"label": "Medical Checkup + Government Hospital 2 Years",
|
||||
"value": "gov_hospital_2y"
|
||||
}
|
||||
],
|
||||
"typeReplace": [
|
||||
{
|
||||
"label": "Work permit, 3 months (325)",
|
||||
|
|
@ -791,7 +794,11 @@
|
|||
"label": "Recording By",
|
||||
"value": "recordBy",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{ "label": "Document Check", "value": "documentCheck", "type": "string" },
|
||||
{ "label": "Duty", "value": "duty", "type": "string" },
|
||||
{ "label": "Messenger", "value": "messenger", "type": "string" },
|
||||
{ "label": "Form", "value": "designForm", "type": "string" }
|
||||
],
|
||||
|
||||
"workPropertiesField": [
|
||||
|
|
@ -958,10 +965,95 @@
|
|||
"label": "Processing Fee",
|
||||
"value": "processingFee"
|
||||
}
|
||||
],
|
||||
|
||||
"agenciesType": [
|
||||
{
|
||||
"label": "Agency",
|
||||
"value": "AGE"
|
||||
},
|
||||
{
|
||||
"label": "Department of Employment",
|
||||
"value": "DOE"
|
||||
},
|
||||
{
|
||||
"label": "Dhipaya Insurance",
|
||||
"value": "INS"
|
||||
},
|
||||
{
|
||||
"label": "Embassy",
|
||||
"value": "EMB"
|
||||
},
|
||||
{
|
||||
"label": "Government Agencies",
|
||||
"value": "GA"
|
||||
},
|
||||
{
|
||||
"label": "Hospital",
|
||||
"value": "HOS"
|
||||
},
|
||||
{
|
||||
"label": "Immigration Bureau",
|
||||
"value": "IMB"
|
||||
},
|
||||
{
|
||||
"label": "Immigration Checkpoint",
|
||||
"value": "IMC"
|
||||
},
|
||||
{
|
||||
"label": "Ministry of Labour",
|
||||
"value": "MOL"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"tha": {
|
||||
"workerStatus": [
|
||||
{
|
||||
"label": "ปกติ",
|
||||
"value": "normal"
|
||||
},
|
||||
{
|
||||
"label": "ยกเลิกบริการ",
|
||||
"value": "canceled"
|
||||
},
|
||||
{
|
||||
"label": "ลาออก",
|
||||
"value": "resigned"
|
||||
},
|
||||
{
|
||||
"label": "หลบหนี",
|
||||
"value": "absconded"
|
||||
},
|
||||
{
|
||||
"label": "เสียชีวิต",
|
||||
"value": "deceased"
|
||||
},
|
||||
{
|
||||
"label": "ส่งกลับประเทศต้นทาง",
|
||||
"value": "repatriated"
|
||||
},
|
||||
{
|
||||
"label": "ไม่ได้เดินทางเข้าประเทศไทย",
|
||||
"value": "not_entered"
|
||||
}
|
||||
],
|
||||
|
||||
"workerType": [
|
||||
{
|
||||
"label": "กลุ่ม MOU",
|
||||
"value": "mou"
|
||||
},
|
||||
{
|
||||
"label": "กลุ่ม พิสูจน์สัญชาติ",
|
||||
"value": "nvg"
|
||||
},
|
||||
{
|
||||
"label": "กลุ่ม Border pass",
|
||||
"value": "bp"
|
||||
}
|
||||
],
|
||||
|
||||
"bankBook": [
|
||||
{
|
||||
"label": "ธนาคารกรุงเทพ",
|
||||
|
|
@ -1015,130 +1107,62 @@
|
|||
}
|
||||
],
|
||||
"typeProduct": [
|
||||
{
|
||||
"label": "AC",
|
||||
"value": "AC"
|
||||
},
|
||||
{
|
||||
"label": "DOE",
|
||||
"value": "DOE"
|
||||
},
|
||||
{
|
||||
"label": "HP",
|
||||
"value": "HP"
|
||||
},
|
||||
{
|
||||
"label": "IMM",
|
||||
"value": "IMM"
|
||||
},
|
||||
{
|
||||
"label": "MOUC",
|
||||
"value": "MOUC"
|
||||
},
|
||||
{
|
||||
"label": "MOUL",
|
||||
"value": "MOUL"
|
||||
},
|
||||
{
|
||||
"label": "TM",
|
||||
"value": "TM"
|
||||
},
|
||||
{
|
||||
"label": "VS",
|
||||
"value": "VS"
|
||||
},
|
||||
{
|
||||
"label": "PPC",
|
||||
"value": "PPC"
|
||||
},
|
||||
{
|
||||
"label": "AB",
|
||||
"value": "AB"
|
||||
},
|
||||
{
|
||||
"label": "PPL",
|
||||
"value": "PPL"
|
||||
},
|
||||
{
|
||||
"label": "PJ",
|
||||
"value": "PJ"
|
||||
},
|
||||
{
|
||||
"label": "EBS",
|
||||
"value": "EBS"
|
||||
},
|
||||
{
|
||||
"label": "CI",
|
||||
"value": "CI"
|
||||
},
|
||||
{
|
||||
"label": "AD",
|
||||
"value": "AD"
|
||||
},
|
||||
{
|
||||
"label": "WO",
|
||||
"value": "WO"
|
||||
},
|
||||
{
|
||||
"label": "AE",
|
||||
"value": "AE"
|
||||
},
|
||||
{
|
||||
"label": "AB1",
|
||||
"value": "AB1"
|
||||
},
|
||||
{
|
||||
"label": "VS1",
|
||||
"value": "VS1"
|
||||
},
|
||||
{
|
||||
"label": "PPC1",
|
||||
"value": "PPC1"
|
||||
},
|
||||
{
|
||||
"label": "PPL1",
|
||||
"value": "PPL1"
|
||||
},
|
||||
{
|
||||
"label": "AC1",
|
||||
"value": "AC1"
|
||||
},
|
||||
{
|
||||
"label": "CI1",
|
||||
"value": "CI1"
|
||||
},
|
||||
{
|
||||
"label": "AD1",
|
||||
"value": "AD1"
|
||||
},
|
||||
{
|
||||
"label": "MOUL1",
|
||||
"value": "MOUL1"
|
||||
},
|
||||
{
|
||||
"label": "MOUL3",
|
||||
"value": "MOUL3"
|
||||
},
|
||||
{
|
||||
"label": "MOUC1",
|
||||
"value": "MOUC1"
|
||||
},
|
||||
{
|
||||
"label": "HP1",
|
||||
"value": "HP1"
|
||||
},
|
||||
{
|
||||
"label": "AE3",
|
||||
"value": "AE3"
|
||||
},
|
||||
{
|
||||
"label": "CAR",
|
||||
"value": "CAR"
|
||||
},
|
||||
{
|
||||
"label": "AD3",
|
||||
"value": "AD3"
|
||||
}
|
||||
{ "label": "AB", "value": "AB" },
|
||||
{ "label": "AB1", "value": "AB1" },
|
||||
{ "label": "AC", "value": "AC" },
|
||||
{ "label": "AC1", "value": "AC1" },
|
||||
{ "label": "AC2", "value": "AC2" },
|
||||
{ "label": "AD", "value": "AD" },
|
||||
{ "label": "AD1", "value": "AD1" },
|
||||
{ "label": "AD3", "value": "AD3" },
|
||||
{ "label": "AE", "value": "AE" },
|
||||
{ "label": "AE3", "value": "AE3" },
|
||||
{ "label": "CAR", "value": "CAR" },
|
||||
{ "label": "CI", "value": "CI" },
|
||||
{ "label": "CI1", "value": "CI1" },
|
||||
{ "label": "CI2", "value": "CI2" },
|
||||
{ "label": "DOE", "value": "DOE" },
|
||||
{ "label": "EBS", "value": "EBS" },
|
||||
{ "label": "GI", "value": "GI" },
|
||||
{ "label": "GO", "value": "GO" },
|
||||
{ "label": "HP", "value": "HP" },
|
||||
{ "label": "HP1", "value": "HP1" },
|
||||
{ "label": "HP2", "value": "HP2" },
|
||||
{ "label": "IMM", "value": "IMM" },
|
||||
{ "label": "MOUC", "value": "MOUC" },
|
||||
{ "label": "MOUC1", "value": "MOUC1" },
|
||||
{ "label": "MOUC2", "value": "MOUC2" },
|
||||
{ "label": "MOUL", "value": "MOUL" },
|
||||
{ "label": "MOUL1", "value": "MOUL1" },
|
||||
{ "label": "MOUL3", "value": "MOUL3" },
|
||||
{ "label": "MOUL4", "value": "MOUL4" },
|
||||
{ "label": "OI", "value": "OI" },
|
||||
{ "label": "PJ", "value": "PJ" },
|
||||
{ "label": "PPC", "value": "PPC" },
|
||||
{ "label": "PPC1", "value": "PPC1" },
|
||||
{ "label": "PPC2", "value": "PPC2" },
|
||||
{ "label": "PPL", "value": "PPL" },
|
||||
{ "label": "PPL1", "value": "PPL1" },
|
||||
{ "label": "PPL2", "value": "PPL2" },
|
||||
{ "label": "PREC1", "value": "PREC1" },
|
||||
{ "label": "PREC2", "value": "PREC2" },
|
||||
{ "label": "PREC3", "value": "PREC3" },
|
||||
{ "label": "PREC4", "value": "PREC4" },
|
||||
{ "label": "PREM1", "value": "PREM1" },
|
||||
{ "label": "PREM2", "value": "PREM2" },
|
||||
{ "label": "PREM3", "value": "PREM3" },
|
||||
{ "label": "PREM4", "value": "PREM4" },
|
||||
{ "label": "TB", "value": "TB" },
|
||||
{ "label": "TB1", "value": "TB1" },
|
||||
{ "label": "TB2", "value": "TB2" },
|
||||
{ "label": "TM", "value": "TM" },
|
||||
{ "label": "TM3", "value": "TM3" },
|
||||
{ "label": "VS", "value": "VS" },
|
||||
{ "label": "VS1", "value": "VS1" },
|
||||
{ "label": "VS2", "value": "VS2" },
|
||||
{ "label": "WO", "value": "WO" },
|
||||
{ "label": "WP390", "value": "WP390" },
|
||||
{ "label": "WP44", "value": "WP44" }
|
||||
],
|
||||
|
||||
"prefix": [
|
||||
|
|
@ -1474,26 +1498,53 @@
|
|||
}
|
||||
],
|
||||
|
||||
"checkupResults": [
|
||||
{
|
||||
"label": "ผลตรวจปกติ",
|
||||
"value": "normal_results"
|
||||
},
|
||||
{
|
||||
"label": "ติดตามการรักษา",
|
||||
"value": "follow_up_treatment"
|
||||
},
|
||||
{
|
||||
"label": "ไม่ผ่านการตรวจสุขภาพ",
|
||||
"value": "failed_checkup"
|
||||
}
|
||||
],
|
||||
|
||||
"insurancePlace": [
|
||||
{
|
||||
"label": "แปซิฟิคครอส",
|
||||
"value": "pacificcross"
|
||||
"label": "ตรวจโรค + ประกันสังคม",
|
||||
"value": "social_insurance"
|
||||
},
|
||||
{
|
||||
"label": "ทิพยประกันภัย",
|
||||
"value": "dhipaya"
|
||||
"label": "ตรวจโรค + ประกัน 990",
|
||||
"value": "insurance_990"
|
||||
},
|
||||
{
|
||||
"label": "รพ.เลิดสิน 4.รพ.ตากสิน",
|
||||
"value": "lerdsin"
|
||||
"label": "ตรวจโรค + ประกัน รพ.รัฐ 3 เดือน",
|
||||
"value": "gov_hospital_3m"
|
||||
},
|
||||
{
|
||||
"label": "รพ.ราชพิพัฒน์",
|
||||
"value": "ratchapipat"
|
||||
"label": "ตรวจโรค + ประกัน รพ.รัฐ 6 เดือน",
|
||||
"value": "gov_hospital_6m"
|
||||
},
|
||||
{
|
||||
"label": "รพ.เจริญกรุงประชารักษ์",
|
||||
"value": "krungPracharak"
|
||||
"label": "ตรวจโรค + ประกัน รพ.รัฐ 1 ปี",
|
||||
"value": "gov_hospital_1y"
|
||||
},
|
||||
{
|
||||
"label": "ตรวจโรค + ประกัน รพ.รัฐ 1 ปี 3 เดือน",
|
||||
"value": "gov_hospital_1y_3m"
|
||||
},
|
||||
{
|
||||
"label": "ตรวจโรค + ประกัน รพ.รัฐ 1 ปี 6 เดือน",
|
||||
"value": "gov_hospital_1y_6m"
|
||||
},
|
||||
{
|
||||
"label": "ตรวจโรค + ประกัน รพ.รัฐ 2 ปี",
|
||||
"value": "gov_hospital_2y"
|
||||
}
|
||||
],
|
||||
|
||||
|
|
@ -1752,7 +1803,11 @@
|
|||
"label": "ลงชื่อผู้บันทึกข้อมูล",
|
||||
"value": "recordBy",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{ "label": "ตรวจสอบเอกสาร", "value": "documentCheck", "type": "string" },
|
||||
{ "label": "อากร", "value": "duty", "type": "string" },
|
||||
{ "label": "พนักงานส่งเอกสาร", "value": "messenger", "type": "string" },
|
||||
{ "label": "ออกแบบฟอร์ม", "value": "designForm", "type": "string" }
|
||||
],
|
||||
|
||||
"workPropertiesField": [
|
||||
|
|
@ -1919,6 +1974,45 @@
|
|||
"label": "ค่าดำเนินงาน",
|
||||
"value": "processingFee"
|
||||
}
|
||||
],
|
||||
|
||||
"agenciesType": [
|
||||
{
|
||||
"label": "เอเจนซี่ / หน่วยงาน",
|
||||
"value": "AGE"
|
||||
},
|
||||
{
|
||||
"label": "จัดหางานพื้นที่",
|
||||
"value": "DOE"
|
||||
},
|
||||
{
|
||||
"label": "ทิพยประกันภัย",
|
||||
"value": "INS"
|
||||
},
|
||||
{
|
||||
"label": "สถานทูต",
|
||||
"value": "EMB"
|
||||
},
|
||||
{
|
||||
"label": "หน่วยงานราชการ",
|
||||
"value": "GA"
|
||||
},
|
||||
{
|
||||
"label": "โรงพยาบาล",
|
||||
"value": "HOS"
|
||||
},
|
||||
{
|
||||
"label": "สำนักงานตรวจคนเข้าเมือง",
|
||||
"value": "IMB"
|
||||
},
|
||||
{
|
||||
"label": "ด่านตรวจคนเข้าเมือง",
|
||||
"value": "IMC"
|
||||
},
|
||||
{
|
||||
"label": "กรมแรงงาน",
|
||||
"value": "MOL"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,11 @@ export default configure((ctx) => {
|
|||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
open: false,
|
||||
port: 5173,
|
||||
},
|
||||
framework: {
|
||||
config: {},
|
||||
plugins: ['Dark', 'Dialog', 'Notify'],
|
||||
plugins: ['Dark', 'Dialog', 'Notify', 'Loading'],
|
||||
iconSet: 'mdi-v7',
|
||||
cssAddon: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import VueDatePicker from '@vuepic/vue-datepicker';
|
|||
import '@vuepic/vue-datepicker/dist/main.css';
|
||||
import GlobalDialog from 'components/GlobalDialog.vue';
|
||||
import GlobalLoading from 'components/GlobalLoading.vue';
|
||||
import VueDragscroll from 'vue-dragscroll';
|
||||
|
||||
export default boot(({ app }) => {
|
||||
app.component('global-dialog', GlobalDialog);
|
||||
app.component('global-loading', GlobalLoading);
|
||||
app.component('VueDatePicker', VueDatePicker);
|
||||
app.use(VueDragscroll);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,13 +21,16 @@ declare module 'vue-i18n' {
|
|||
}
|
||||
/* eslint-enable @typescript-eslint/no-empty-interface */
|
||||
|
||||
export default boot(({ app }) => {
|
||||
const i18n = createI18n({
|
||||
locale: 'tha',
|
||||
legacy: false,
|
||||
messages,
|
||||
});
|
||||
export const i18n = createI18n({
|
||||
locale: 'tha',
|
||||
legacy: false,
|
||||
messages: {
|
||||
'en-US': {},
|
||||
...messages,
|
||||
},
|
||||
});
|
||||
|
||||
export default boot(({ app }) => {
|
||||
// Set i18n instance on app
|
||||
app.use(i18n);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -59,7 +59,10 @@ defineProps<{
|
|||
</q-img>
|
||||
</div>
|
||||
<div class="branch-card__name flex justify-center q-ml-sm">
|
||||
<b>{{ data.branchLabelName }}</b>
|
||||
<b class="ellipsis-2-lines">
|
||||
{{ data.branchLabelName }}
|
||||
<q-tooltip>{{ data.branchLabelName }}</q-tooltip>
|
||||
</b>
|
||||
<small class="branch-card__code" v-if="data.branchLabelCode">
|
||||
{{ data.branchLabelCode }}
|
||||
</small>
|
||||
|
|
@ -98,11 +101,19 @@ defineProps<{
|
|||
<slot name="data"></slot>
|
||||
<template v-if="!$slots.data">
|
||||
<div
|
||||
v-for="key in fieldSelected || [
|
||||
'branchLabelAddress',
|
||||
'branchLabelTel',
|
||||
'branchLabelType',
|
||||
]"
|
||||
v-for="key in (
|
||||
fieldSelected || [
|
||||
'branchLabelAddress',
|
||||
'branchLabelTel',
|
||||
'branchLabelType',
|
||||
]
|
||||
).sort((lhs, rhs) => {
|
||||
let order = Object.keys(data);
|
||||
return (
|
||||
order.findIndex((i) => i === lhs) -
|
||||
order.findIndex((i) => i === rhs)
|
||||
);
|
||||
})"
|
||||
:key="key"
|
||||
class="branch-card__data"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -173,9 +173,10 @@ watch(
|
|||
|
||||
<div
|
||||
class="bordered q-mr-sm rounded"
|
||||
:class="{ 'cursor-pointer': !readonly }"
|
||||
:class="{ 'pointer-none': readonly }"
|
||||
>
|
||||
<ImageHover
|
||||
:readonly="readonly"
|
||||
:img="book.bankUrl"
|
||||
@view="() => $emit('viewQr', i)"
|
||||
@edit="
|
||||
|
|
@ -347,4 +348,8 @@ watch(
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.pointer-none {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { isRoleInclude } from 'src/stores/utils';
|
||||
import DatePicker from '../shared/DatePicker.vue';
|
||||
import { disabledAfterToday } from 'src/utils/datetime';
|
||||
|
||||
const code = defineModel<string>('code');
|
||||
const branchCount = defineModel<number>('branchCount', { default: 0 });
|
||||
|
|
@ -12,8 +13,8 @@ const nameEN = defineModel<string>('nameEn');
|
|||
const typeBranch = defineModel<string>('typeBranch');
|
||||
const virtual = defineModel<boolean>('virtual');
|
||||
|
||||
const permitExpireDate = defineModel<Date>('permitExpireDate');
|
||||
const permitIssueDate = defineModel<Date>('permitIssueDate');
|
||||
const permitExpireDate = defineModel<Date | null>('permitExpireDate');
|
||||
const permitIssueDate = defineModel<Date | null>('permitIssueDate');
|
||||
const permitNo = defineModel<string>('permitNo');
|
||||
|
||||
defineProps<{
|
||||
|
|
@ -123,11 +124,17 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
|
|||
:label="
|
||||
typeBranch === 'headOffice'
|
||||
? $t('branch.form.headofficeName')
|
||||
: $t('branch.form.branchName')
|
||||
: virtual === true
|
||||
? $t('branch.form.servicePointName')
|
||||
: $t('branch.form.branchName')
|
||||
"
|
||||
v-model="name"
|
||||
:rules="[(val) => val && val.length > 0]"
|
||||
:error-message="$t('form.error.required')"
|
||||
:rules="[
|
||||
(val) => !!val || $t('form.error.required'),
|
||||
(val) =>
|
||||
/^[A-Za-z0-9ก-๙\s&.,'-]+$/.test(val) ||
|
||||
$t('form.error.branchNameField'),
|
||||
]"
|
||||
for="input-name"
|
||||
/>
|
||||
<q-input
|
||||
|
|
@ -140,12 +147,15 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
|
|||
:label="
|
||||
typeBranch === 'headOffice'
|
||||
? $t('branch.form.headofficeNameEN')
|
||||
: $t('branch.form.branchNameEN')
|
||||
: virtual === true
|
||||
? $t('branch.form.servicePointNameEN')
|
||||
: $t('branch.form.branchNameEN')
|
||||
"
|
||||
:rules="[
|
||||
(val) => !!val || $t('form.error.required'),
|
||||
(val) =>
|
||||
/^[A-Za-z0-9\s.,]+$/.test(val) || $t('form.error.letterOnly'),
|
||||
/^[A-Za-z0-9\s&.,'-]+$/.test(val) ||
|
||||
$t('form.error.branchNameENField'),
|
||||
]"
|
||||
for="input-name-en"
|
||||
/>
|
||||
|
|
@ -198,7 +208,7 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
|
|||
v-model="permitNo"
|
||||
:rules="[(val) => val && val.length > 0]"
|
||||
:error-message="$t('form.error.required')"
|
||||
for="input-name"
|
||||
for="input-license-number"
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
|
|
@ -216,6 +226,12 @@ function formatCode(input: string | undefined, type: 'code' | 'number') {
|
|||
:readonly="readonly"
|
||||
:label="$t('general.expirationDate')"
|
||||
v-model="permitExpireDate"
|
||||
:disabled-dates="
|
||||
(date: Date) =>
|
||||
date.getTime() <
|
||||
((permitIssueDate && new Date(permitIssueDate).getTime()) ||
|
||||
Date.now())
|
||||
"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { Icon } from '@iconify/vue';
|
|||
import { QSelect } from 'quasar';
|
||||
import DatePicker from '../shared/DatePicker.vue';
|
||||
|
||||
import SelectOffice from 'components/shared/select-muliple/SelectOffice.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const optionStore = useOptionStore();
|
||||
|
|
@ -18,7 +20,7 @@ const userType = defineModel<string>('userType');
|
|||
const registrationNo = defineModel<string | null>('registrationNo');
|
||||
const startDate = defineModel<Date | null | string>('startDate');
|
||||
const retireDate = defineModel<Date | null | string>('retireDate');
|
||||
const responsibleArea = defineModel<string | null | undefined>(
|
||||
const responsibleArea = defineModel<string[] | null | undefined>(
|
||||
'responsibleArea',
|
||||
);
|
||||
const discountCondition = defineModel<string | null | undefined>(
|
||||
|
|
@ -175,44 +177,20 @@ watch(
|
|||
:readonly="readonly"
|
||||
:label="$t('personnel.form.retireDate')"
|
||||
v-model="retireDate"
|
||||
:disabled-dates="
|
||||
(date: Date) =>
|
||||
date.getTime() <
|
||||
((startDate && new Date(startDate).getTime()) || Date.now())
|
||||
"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<q-select
|
||||
<SelectOffice
|
||||
v-model:value="responsibleArea"
|
||||
v-if="userType === 'MESSENGER'"
|
||||
outlined
|
||||
clearable
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
class="col-12"
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
id="input-responsible-area"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:label="$t('personnel.form.responsibleArea')"
|
||||
:options="responsibleAreaOptions"
|
||||
@filter="responsibleAreaFilter"
|
||||
:model-value="readonly ? responsibleArea || '-' : responsibleArea"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (responsibleArea = v) : '')
|
||||
"
|
||||
@clear="responsibleArea = ''"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="userType === 'DELEGATE'"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import useUserStore from 'stores/user';
|
||||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { isRoleInclude } from 'src/stores/utils';
|
||||
import SelectBranch from '../shared/select/SelectBranch.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
|
|
@ -15,7 +16,7 @@ const userRole = defineModel<string>('userRole');
|
|||
const username = defineModel<string | null | undefined>('username');
|
||||
const userCode = defineModel<string>('userCode');
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
title?: string;
|
||||
dense?: boolean;
|
||||
outlined?: boolean;
|
||||
|
|
@ -24,35 +25,6 @@ const props = defineProps<{
|
|||
usernameReadonly?: boolean;
|
||||
}>();
|
||||
|
||||
async function selectHq(id: string) {
|
||||
if (!id) return;
|
||||
brId.value = '';
|
||||
userStore.userOption.brOpts = [];
|
||||
await userStore.fetchBrOption(id);
|
||||
if (userStore.userOption.brOpts.length === 1) {
|
||||
brId.value = userStore.userOption.brOpts[0].value;
|
||||
}
|
||||
brFilter = selectFilterOptionRefMod(
|
||||
ref(userStore.userOption.brOpts),
|
||||
brOptions,
|
||||
'label',
|
||||
);
|
||||
}
|
||||
|
||||
const hqOptions = ref<Record<string, unknown>[]>([]);
|
||||
const hqFilter = selectFilterOptionRefMod(
|
||||
ref(userStore.userOption.hqOpts),
|
||||
hqOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
const brOptions = ref<Record<string, unknown>[]>([]);
|
||||
let brFilter = selectFilterOptionRefMod(
|
||||
ref(userStore.userOption.brOpts),
|
||||
brOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
const userTypeOptions = ref<Record<string, unknown>[]>([]);
|
||||
const userTypeFilter = selectFilterOptionRefMod(
|
||||
ref(
|
||||
|
|
@ -71,35 +43,6 @@ const roleFilter = selectFilterOptionRefMod(
|
|||
roleOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
if (userStore.userOption.hqOpts[0].value && !props.readonly) {
|
||||
await userStore.fetchBrOption(userStore.userOption.hqOpts[0].value);
|
||||
if (userStore.userOption.brOpts.length === 1) {
|
||||
brId.value = userStore.userOption.brOpts[0].value;
|
||||
}
|
||||
brFilter = selectFilterOptionRefMod(
|
||||
ref(userStore.userOption.brOpts),
|
||||
brOptions,
|
||||
'label',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => hqId.value,
|
||||
async (v) => {
|
||||
if (v) {
|
||||
userStore.userOption.brOpts = [];
|
||||
await userStore.fetchBrOption(v);
|
||||
brFilter = selectFilterOptionRefMod(
|
||||
ref(userStore.userOption.brOpts),
|
||||
brOptions,
|
||||
'label',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
|
|
@ -116,81 +59,35 @@ watch(
|
|||
</div>
|
||||
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<q-select
|
||||
outlined
|
||||
clearable
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
:disable="isRoleInclude(['branch_manager']) && !readonly"
|
||||
v-model="hqId"
|
||||
for="select-hq-id"
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
<SelectBranch
|
||||
id="select-hq-id"
|
||||
class="col-md-2 col-12"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:readonly
|
||||
:disabled="isRoleInclude(['branch_manager']) && !readonly"
|
||||
:params="{
|
||||
filter: 'head',
|
||||
}"
|
||||
:label="$t('branch.form.code')"
|
||||
:options="hqOptions"
|
||||
:rules="[
|
||||
(val: string) =>
|
||||
!!val ||
|
||||
$t('form.error.selectField', { field: $t('branch.form.code') }),
|
||||
]"
|
||||
@update:model-value="(val: string) => selectHq(val)"
|
||||
@filter="hqFilter"
|
||||
@clear="
|
||||
() => {
|
||||
(hqId = ''), (brId = '');
|
||||
}
|
||||
"
|
||||
>
|
||||
<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
|
||||
code-only
|
||||
clearable
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
for="select-br-id"
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
v-model:value="hqId"
|
||||
@update:value="() => (brId = '')"
|
||||
required
|
||||
/>
|
||||
<SelectBranch
|
||||
id="select-br-id"
|
||||
class="col-md-2 col-12"
|
||||
:disable="isRoleInclude(['branch_manager']) && !readonly"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:key="hqId ?? undefined"
|
||||
:readonly
|
||||
:disabled="(isRoleInclude(['branch_manager']) && !readonly) || !hqId"
|
||||
:params="{
|
||||
headOfficeId: hqId ?? undefined,
|
||||
}"
|
||||
:label="$t('branch.form.codeBranch')"
|
||||
:options="brOptions"
|
||||
@filter="brFilter"
|
||||
:model-value="readonly ? brId || '-' : brId"
|
||||
@update:model-value="(v) => (typeof v === 'string' ? (brId = v) : '')"
|
||||
@clear="brId = ''"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
code-only
|
||||
clearable
|
||||
v-model:value="brId"
|
||||
/>
|
||||
<q-input
|
||||
for="input-username"
|
||||
:dense="dense"
|
||||
|
|
@ -226,6 +123,7 @@ watch(
|
|||
option-value="value"
|
||||
option-label="label"
|
||||
for="select-user-type"
|
||||
autocomplete="off"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:hide-dropdown-icon="readonly"
|
||||
|
|
@ -259,6 +157,7 @@ watch(
|
|||
hide-bottom-space
|
||||
class="col"
|
||||
input-debounce="0"
|
||||
autocomplete="off"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
for="select-user-role"
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ let nationalityFilter: (
|
|||
|
||||
function matPreFixName() {
|
||||
if (gender.value === 'male') prefixName.value = 'mr';
|
||||
else prefixName.value = 'mrs';
|
||||
if (gender.value === 'female') prefixName.value = 'mrs';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -162,6 +162,7 @@ watch(
|
|||
option-label="label"
|
||||
option-value="value"
|
||||
hide-dropdown-icon
|
||||
autocomplete="off"
|
||||
class="col-md-1 col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
|
|
@ -261,7 +262,7 @@ watch(
|
|||
:rules="[
|
||||
(val: string) => !!val || $t('form.error.required'),
|
||||
(val: string) =>
|
||||
/^[A-Za-z]+$/.test(val) || $t('form.error.letterOnly'),
|
||||
/^[A-Za-z\s]+$/.test(val) || $t('form.error.letterOnly'),
|
||||
]"
|
||||
/>
|
||||
<q-input
|
||||
|
|
@ -290,7 +291,7 @@ watch(
|
|||
:rules="[
|
||||
(val: string) => !!val || $t('form.error.required'),
|
||||
(val: string) =>
|
||||
/^[A-Za-z]+$/.test(val) || $t('form.error.letterOnly'),
|
||||
/^[A-Za-z\s]+$/.test(val) || $t('form.error.letterOnly'),
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -364,6 +365,7 @@ watch(
|
|||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
autocomplete="off"
|
||||
class="col-md-2 col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
|
|
@ -422,7 +424,6 @@ watch(
|
|||
:id="`${prefixId}-input-citizen-issue`"
|
||||
:readonly="readonly"
|
||||
:label="$t('personnel.form.citizenIssue')"
|
||||
:disabled-dates="disabledAfterToday"
|
||||
:rules="[
|
||||
(val: string) =>
|
||||
!!val ||
|
||||
|
|
@ -430,6 +431,17 @@ watch(
|
|||
field: $t('personnel.form.citizenIssue'),
|
||||
}),
|
||||
]"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (!v) return;
|
||||
if (!citizenExpire) return;
|
||||
if (new Date(v).getTime() >= new Date(citizenExpire).getTime()) {
|
||||
const newValue = new Date(v);
|
||||
newValue.setDate(newValue.getDate() + 1);
|
||||
citizenExpire = newValue;
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
|
|
@ -439,6 +451,11 @@ watch(
|
|||
:id="`${prefixId}-input-citizen-expire`"
|
||||
:readonly="readonly"
|
||||
:label="$t('personnel.form.citizenExpire')"
|
||||
:disabled-dates="
|
||||
(date: Date) =>
|
||||
date.getTime() <
|
||||
((citizenIssue && new Date(citizenIssue).getTime()) || Date.now())
|
||||
"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
|
|
@ -450,6 +467,7 @@ watch(
|
|||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
autocomplete="off"
|
||||
hide-bottom-space
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
|
|
@ -483,6 +501,7 @@ watch(
|
|||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
autocomplete="off"
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
prefixId?: string;
|
||||
inactive?: boolean;
|
||||
color?: 'none' | 'pers' | 'corp';
|
||||
data: {
|
||||
|
|
@ -9,6 +10,7 @@ defineProps<{
|
|||
telephone: string;
|
||||
businessTypePure: string;
|
||||
totalEmployee: number;
|
||||
code: string;
|
||||
};
|
||||
metadata?: unknown;
|
||||
badgeField?: string[];
|
||||
|
|
@ -47,11 +49,12 @@ defineProps<{
|
|||
</div>
|
||||
<div class="branch-card__name">
|
||||
<b>{{ data.branchName }}</b>
|
||||
<small class="branch-card__code">{{ data.branchName }}</small>
|
||||
<small class="branch-card__code">{{ data.code }}</small>
|
||||
</div>
|
||||
|
||||
<div class="branch-card__action">
|
||||
<q-btn
|
||||
:id="`${prefixId}-btn-view-detail`"
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
dense
|
||||
|
|
@ -73,18 +76,25 @@ defineProps<{
|
|||
"
|
||||
/>
|
||||
<div
|
||||
v-for="key in fieldSelected?.sort() || [
|
||||
'customerBranchFormTab',
|
||||
'branchName',
|
||||
v-for="key in fieldSelected || [
|
||||
'address',
|
||||
'telephone',
|
||||
'businessTypePure',
|
||||
'totalEmployee',
|
||||
]"
|
||||
:key="key"
|
||||
class="branch-card__data"
|
||||
>
|
||||
<div>{{ $t(key) }}</div>
|
||||
<div>{{ data[key as keyof typeof data] }}</div>
|
||||
<div>
|
||||
{{
|
||||
fieldSelected
|
||||
? $t(key)
|
||||
: key !== 'address' && key !== 'telephone'
|
||||
? $t(`customer.table.${key}`)
|
||||
: $t(`general.${key}`)
|
||||
}}
|
||||
</div>
|
||||
<div>{{ data[key as keyof typeof data] || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -172,7 +182,7 @@ defineProps<{
|
|||
}
|
||||
|
||||
&.branch-card__pers {
|
||||
--_branch-card-bg: var(--pink-6-hsl);
|
||||
--_branch-card-bg: var(--teal-10-hsl);
|
||||
}
|
||||
|
||||
&.branch-card__corp {
|
||||
|
|
|
|||
|
|
@ -1,31 +1,42 @@
|
|||
<script lang="ts" setup>
|
||||
import { calculateDaysUntilExpire } from 'stores/utils';
|
||||
|
||||
defineProps<{
|
||||
expirationDate: Date;
|
||||
showAllDay?: boolean;
|
||||
}>();
|
||||
|
||||
function calculateDaysUntilExpire(expireDate: Date): number {
|
||||
const today = new Date();
|
||||
const expire = new Date(expireDate);
|
||||
const diffInTime = expire.getTime() - today.getTime();
|
||||
const diffInDays = Math.ceil(diffInTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return diffInDays;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="calculateDaysUntilExpire(expirationDate) <= 90">
|
||||
<template
|
||||
v-if="
|
||||
calculateDaysUntilExpire(expirationDate) <= 90 ||
|
||||
(expirationDate !== undefined && !!showAllDay)
|
||||
"
|
||||
>
|
||||
<q-badge
|
||||
:color="calculateDaysUntilExpire(expirationDate) > 0 ? 'orange' : 'red'"
|
||||
:color="
|
||||
calculateDaysUntilExpire(expirationDate) > 90
|
||||
? 'green'
|
||||
: calculateDaysUntilExpire(expirationDate) <= 90 &&
|
||||
calculateDaysUntilExpire(expirationDate) > 0
|
||||
? 'orange'
|
||||
: 'red'
|
||||
"
|
||||
class="text-weight-bold"
|
||||
>
|
||||
{{
|
||||
calculateDaysUntilExpire(expirationDate) > 0
|
||||
? 'จะครบกำหนดในอีก'
|
||||
: calculateDaysUntilExpire(expirationDate) === 0
|
||||
? 'ครบกำหนด'
|
||||
: 'เลยกำหนด'
|
||||
$t(
|
||||
`general.${
|
||||
calculateDaysUntilExpire(expirationDate) > 0
|
||||
? 'beDue'
|
||||
: calculateDaysUntilExpire(expirationDate) === 0
|
||||
? 'due'
|
||||
: 'overDue'
|
||||
}`,
|
||||
)
|
||||
}}
|
||||
|
||||
{{
|
||||
calculateDaysUntilExpire(expirationDate) > 0
|
||||
? calculateDaysUntilExpire(expirationDate)
|
||||
|
|
@ -34,10 +45,11 @@ function calculateDaysUntilExpire(expireDate: Date): number {
|
|||
: calculateDaysUntilExpire(expirationDate) * -1
|
||||
}}
|
||||
<template v-if="calculateDaysUntilExpire(expirationDate) !== 0">
|
||||
วัน
|
||||
{{ $t('general.day') }}
|
||||
</template>
|
||||
</q-badge>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { EmployeeCheckupCreate } from 'stores/employee/types';
|
|||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import { QSelect } from 'quasar';
|
||||
import DatePicker from '../shared/DatePicker.vue';
|
||||
|
||||
import SelectInput from 'components/shared/SelectInput.vue';
|
||||
import {
|
||||
AddButton,
|
||||
EditButton,
|
||||
|
|
@ -31,6 +33,11 @@ const addrOptions = reactive<{
|
|||
|
||||
const currentIndex = defineModel<number>('currentIndex');
|
||||
const employeeCheckup = defineModel<EmployeeCheckupCreate[]>('employeeCheckup');
|
||||
|
||||
const checkupResultsOption = defineModel<{ label: string; value: string }[]>(
|
||||
'checkupResultsOption',
|
||||
{ required: true },
|
||||
);
|
||||
const checkupTypeOption = defineModel<{ label: string; value: string }[]>(
|
||||
'checkupTypeOption',
|
||||
{ required: true },
|
||||
|
|
@ -82,28 +89,6 @@ async function fetchProvince() {
|
|||
);
|
||||
}
|
||||
|
||||
function addCheckup() {
|
||||
// const canAdd = checkTabBeforeAdd(employeeCheckup.value || []);
|
||||
const canAdd = true;
|
||||
if (canAdd) {
|
||||
employeeCheckup.value?.push({
|
||||
coverageExpireDate: null,
|
||||
coverageStartDate: null,
|
||||
insuranceCompany: '',
|
||||
medicalBenefitScheme: '',
|
||||
remark: '',
|
||||
hospitalName: '',
|
||||
provinceId: '',
|
||||
checkupResult: '',
|
||||
checkupType: '',
|
||||
});
|
||||
if (employeeCheckup.value) {
|
||||
tab.value = `tab${employeeCheckup.value.length - 1}`;
|
||||
currentIndex.value = employeeCheckup.value.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeData(index: number) {
|
||||
if (!employeeCheckup.value) return;
|
||||
if (employeeCheckup.value.length === 1) return;
|
||||
|
|
@ -126,6 +111,13 @@ let provinceFilter: (
|
|||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
const checkupResultsOptions = ref<Record<string, unknown>[]>([]);
|
||||
const checkupResultsFilter = selectFilterOptionRefMod(
|
||||
checkupResultsOption,
|
||||
checkupResultsOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
const checkupTypeOptions = ref<Record<string, unknown>[]>([]);
|
||||
const checkupTypeFilter = selectFilterOptionRefMod(
|
||||
checkupTypeOption,
|
||||
|
|
@ -149,28 +141,7 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<div class="col-12 q-pb-sm text-weight-bold text-body1 row items-center">
|
||||
<q-icon
|
||||
flat
|
||||
size="xs"
|
||||
class="q-pa-sm rounded q-mr-sm"
|
||||
color="info"
|
||||
name="mdi-hospital-box-outline"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`customerEmployee.formHealthCheck.title`) }}
|
||||
<AddButton
|
||||
v-if="currentIndex === -1 && !hideAction"
|
||||
id="btn-add-bank"
|
||||
icon-only
|
||||
class="q-ml-sm"
|
||||
type="button"
|
||||
@click="addCheckup"
|
||||
:disabled="!(currentIndex === -1)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
v-for="(checkup, index) in employeeCheckup"
|
||||
v-bind:key="index"
|
||||
|
|
@ -222,15 +193,6 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
|
|||
</div>
|
||||
</span>
|
||||
|
||||
<q-input
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || checkup.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-5"
|
||||
:label="$t('customerEmployee.formHealthCheck.result')"
|
||||
v-model="checkup.checkupResult"
|
||||
/>
|
||||
<q-select
|
||||
outlined
|
||||
clearable
|
||||
|
|
@ -240,7 +202,38 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
|
|||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
class="col-5"
|
||||
class="col-6"
|
||||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
v-model="checkup.checkupResult"
|
||||
:dense="dense"
|
||||
:readonly="readonly || checkup.statusSave"
|
||||
:options="checkupResultsOptions"
|
||||
:hide-dropdown-icon="readonly || checkup.statusSave"
|
||||
:for="`${prefixId}-select-health-checkresult`"
|
||||
:label="$t('customerEmployee.formHealthCheck.result')"
|
||||
@filter="checkupResultsFilter"
|
||||
>
|
||||
<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
|
||||
class="col-6"
|
||||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
|
|
@ -261,47 +254,44 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
|
|||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
<q-select
|
||||
outlined
|
||||
clearable
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
option-value="id"
|
||||
input-debounce="0"
|
||||
option-label="name"
|
||||
class="col-2"
|
||||
v-model="checkup.provinceId"
|
||||
:dense="dense"
|
||||
:readonly="readonly || checkup.statusSave"
|
||||
:label="$t('form.province')"
|
||||
:hide-dropdown-icon="readonly || checkup.statusSave"
|
||||
:for="`${prefixId}-select-province`"
|
||||
:options="provinceOptions"
|
||||
@filter="provinceFilter"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
<!-- @filter="provinceFilter" -->
|
||||
<q-input
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || checkup.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-5"
|
||||
class="col-12"
|
||||
:label="$t('customerEmployee.formHealthCheck.hospital')"
|
||||
v-model="checkup.hospitalName"
|
||||
:for="`${prefixId}-input-hospital`"
|
||||
/>
|
||||
|
||||
<div class="col">
|
||||
<DatePicker
|
||||
v-model="checkup.coverageStartDate"
|
||||
:id="`${prefixId}-input-coverage-start-date`"
|
||||
:readonly="readonly || checkup.statusSave"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<DatePicker
|
||||
:label="$t('customerEmployee.formHealthCheck.coverageExpireDate')"
|
||||
v-model="checkup.coverageExpireDate"
|
||||
:id="`${prefixId}-input-coverage-expire-date`"
|
||||
:readonly="readonly || checkup.statusSave"
|
||||
:disabled-dates="
|
||||
(date: Date) =>
|
||||
date.getTime() <
|
||||
((checkup.coverageStartDate &&
|
||||
new Date(checkup.coverageStartDate).getTime()) ||
|
||||
Date.now())
|
||||
"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-select
|
||||
outlined
|
||||
clearable
|
||||
|
|
@ -311,7 +301,7 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
|
|||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
class="col-5"
|
||||
class="col-6"
|
||||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
|
|
@ -343,53 +333,6 @@ const insuranceCompanyFilter = selectFilterOptionRefMod(
|
|||
type="textarea"
|
||||
:for="`${prefixId}-input-remark`"
|
||||
/>
|
||||
<DatePicker
|
||||
:label="$t('customerEmployee.formHealthCheck.coverageStartDate')"
|
||||
v-model="checkup.coverageStartDate"
|
||||
class="col"
|
||||
:id="`${prefixId}-input-coverage-start-date`"
|
||||
:readonly="readonly || checkup.statusSave"
|
||||
clearable
|
||||
/>
|
||||
<DatePicker
|
||||
:label="$t('customerEmployee.formHealthCheck.coverageExpireDate')"
|
||||
v-model="checkup.coverageExpireDate"
|
||||
class="col"
|
||||
:id="`${prefixId}-input-coverage-expire-date`"
|
||||
:readonly="readonly || checkup.statusSave"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<q-select
|
||||
outlined
|
||||
clearable
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
class="col-7"
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
v-model="checkup.insuranceCompany"
|
||||
:dense="dense"
|
||||
:readonly="readonly || checkup.statusSave"
|
||||
:hide-dropdown-icon="readonly || checkup.statusSave"
|
||||
:options="insuranceCompanyOptions"
|
||||
:for="`${prefixId}-select-province`"
|
||||
:label="$t('customerEmployee.formHealthCheck.insuranceCompany')"
|
||||
@filter="insuranceCompanyFilter"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { EmployeeOtherCreate } from 'stores/employee/types';
|
||||
import {
|
||||
AddButton,
|
||||
EditButton,
|
||||
DeleteButton,
|
||||
SaveButton,
|
||||
|
|
@ -18,6 +17,13 @@ defineProps<{
|
|||
hideAction?: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'save'): void;
|
||||
(e: 'edit'): void;
|
||||
(e: 'delete'): void;
|
||||
(e: 'undo'): void;
|
||||
}>();
|
||||
|
||||
const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
||||
</script>
|
||||
<template>
|
||||
|
|
@ -73,9 +79,20 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
:readonly="readonly || employeeOther.statusSave"
|
||||
hide-bottom-space
|
||||
:label="$t('customerEmployee.formFamily.citizenId')"
|
||||
class="col"
|
||||
class="col-4"
|
||||
v-model="employeeOther.citizenId"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-citizen-id`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || employeeOther.statusSave"
|
||||
hide-bottom-space
|
||||
:label="$t('customerEmployee.formFamily.telephoneNo')"
|
||||
class="col-3"
|
||||
v-model="employeeOther.telephoneNo"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 app-text-muted-2">
|
||||
<q-icon size="xs" class="q-mr-xs" name="mdi-human-male" />
|
||||
|
|
@ -90,7 +107,13 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
hide-bottom-space
|
||||
class="col-md-3 col-6"
|
||||
:label="$t('form.firstName')"
|
||||
v-model="employeeOther.fatherFirstName"
|
||||
:model-value="employeeOther.fatherFirstName"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
typeof v === 'string' && employeeOther
|
||||
? (employeeOther.fatherFirstName = v.trim())
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-father-last-name`"
|
||||
|
|
@ -100,7 +123,13 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
hide-bottom-space
|
||||
class="col-md-3 col-6"
|
||||
:label="$t('form.lastName')"
|
||||
v-model="employeeOther.fatherLastName"
|
||||
:model-value="employeeOther.fatherLastName"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
typeof v === 'string' && employeeOther
|
||||
? (employeeOther.fatherLastName = v.trim())
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-father-first-name-en`"
|
||||
|
|
@ -110,7 +139,13 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
hide-bottom-space
|
||||
class="col-md-3 col-6"
|
||||
:label="$t('form.firstNameEN')"
|
||||
v-model="employeeOther.fatherFirstNameEN"
|
||||
:model-value="employeeOther.fatherFirstNameEN"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
typeof v === 'string' && employeeOther
|
||||
? (employeeOther.fatherFirstNameEN = v.trim())
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-father-last-name-en`"
|
||||
|
|
@ -120,17 +155,13 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
hide-bottom-space
|
||||
class="col-md-3 col-6"
|
||||
:label="$t('form.lastNameEN')"
|
||||
v-model="employeeOther.fatherLastNameEN"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-father-birthplace`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || employeeOther.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-6"
|
||||
:label="$t('customerEmployee.formFamily.fatherBirthPlace')"
|
||||
v-model="employeeOther.fatherBirthPlace"
|
||||
:model-value="employeeOther.fatherLastNameEN"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
typeof v === 'string' && employeeOther
|
||||
? (employeeOther.fatherLastNameEN = v.trim())
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -147,7 +178,13 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
hide-bottom-space
|
||||
class="col-md-3 col-6"
|
||||
:label="$t('form.firstName')"
|
||||
v-model="employeeOther.motherFirstName"
|
||||
:model-value="employeeOther.motherFirstName"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
typeof v === 'string' && employeeOther
|
||||
? (employeeOther.motherFirstName = v.trim())
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-mother-last-name`"
|
||||
|
|
@ -157,7 +194,13 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
hide-bottom-space
|
||||
class="col-md-3 col-6"
|
||||
:label="$t('form.lastName')"
|
||||
v-model="employeeOther.motherLastName"
|
||||
:model-value="employeeOther.motherLastName"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
typeof v === 'string' && employeeOther
|
||||
? (employeeOther.motherLastName = v.trim())
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-mother-first-name-en`"
|
||||
|
|
@ -167,7 +210,13 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
hide-bottom-space
|
||||
class="col-md-3 col-6"
|
||||
:label="$t('form.firstNameEN')"
|
||||
v-model="employeeOther.motherFirstNameEN"
|
||||
:model-value="employeeOther.motherFirstNameEN"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
typeof v === 'string' && employeeOther
|
||||
? (employeeOther.motherFirstNameEN = v.trim())
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-mother-last-name-en`"
|
||||
|
|
@ -177,17 +226,13 @@ const employeeOther = defineModel<EmployeeOtherCreate>('employeeOther');
|
|||
hide-bottom-space
|
||||
class="col-md-3 col-6"
|
||||
:label="$t('form.lastNameEN')"
|
||||
v-model="employeeOther.motherLastNameEN"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-mother-birthplace`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || employeeOther.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-6"
|
||||
:label="$t('customerEmployee.formFamily.motherBirthPlace')"
|
||||
v-model="employeeOther.motherBirthPlace"
|
||||
:model-value="employeeOther.motherLastNameEN"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
typeof v === 'string' && employeeOther
|
||||
? (employeeOther.motherLastNameEN = v.trim())
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,48 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { QSelect } from 'quasar';
|
||||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import { dateFormat, parseAndFormatDate } from 'src/utils/datetime';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import { calculateAge, disabledAfterToday } from 'src/utils/datetime';
|
||||
import useOptionStore from 'stores/options';
|
||||
import { watch } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import DatePicker from '../shared/DatePicker.vue';
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
const { locale } = useI18n();
|
||||
|
||||
const firstName = defineModel<string>('firstName');
|
||||
const lastName = defineModel<string>('lastName');
|
||||
|
||||
const prefixName = defineModel<string>('prefixName');
|
||||
const birthCountry = defineModel<string>('birthCountry');
|
||||
const previousPassportRef = defineModel<string>('previousPassportRef');
|
||||
const issuePlace = defineModel<string>('issuePlace');
|
||||
const issueCountry = defineModel<string>('issueCountry');
|
||||
const issueDate = defineModel<Date | null | string>('issueDate');
|
||||
const type = defineModel<string>('type');
|
||||
const expireDate = defineModel<Date>('expireDate');
|
||||
const birthDate = defineModel<Date>('birthDate');
|
||||
const workerStatus = defineModel<string>('workerStatus');
|
||||
const nationality = defineModel<string>('nationality');
|
||||
const gender = defineModel<string>('gender');
|
||||
|
||||
const passportType = defineModel<string>('passportType');
|
||||
const lastNameEN = defineModel<string>('lastNameEn');
|
||||
const lastName = defineModel<string>('lastName');
|
||||
const middleNameEN = defineModel<string>('middleNameEn');
|
||||
const middleName = defineModel<string>('middleName');
|
||||
const firstNameEN = defineModel<string>('firstNameEn');
|
||||
const firstName = defineModel<string>('firstName');
|
||||
const namePrefix = defineModel<string>('namePrefix');
|
||||
const passportNumber = defineModel<string>('passportNumber');
|
||||
const passportIssueDate = defineModel<Date | null | string>(
|
||||
'passportIssueDate',
|
||||
);
|
||||
const passportExpiryDate = defineModel<Date | null | string>(
|
||||
'passportExpiryDate',
|
||||
);
|
||||
const passportIssuingCountry = defineModel<string>('passportIssuingCountry');
|
||||
const passportIssuingPlace = defineModel<string>('passportIssuingPlace');
|
||||
const previousPassportReference = defineModel<string>(
|
||||
'previousPassportReference',
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
|
|
@ -64,11 +76,17 @@ let prefixNameFilter: (
|
|||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
const workerStatusOptions = ref<Record<string, unknown>[]>([]);
|
||||
let workerStatusFilter: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
function matPreFixName() {
|
||||
if (!gender.value) return;
|
||||
|
||||
if (gender.value === 'male') prefixName.value = 'mr';
|
||||
else prefixName.value = 'mrs';
|
||||
if (gender.value === 'male') namePrefix.value = 'mr';
|
||||
else namePrefix.value = 'mrs';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -93,6 +111,24 @@ onMounted(() => {
|
|||
prefixNameOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
workerStatusFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.workerStatus),
|
||||
workerStatusOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
genderFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.gender),
|
||||
genderOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
nationalityFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.nationality),
|
||||
nationalityOptions,
|
||||
'label',
|
||||
);
|
||||
}
|
||||
|
||||
matPreFixName();
|
||||
|
|
@ -118,11 +154,29 @@ watch(
|
|||
passportIssuingCountryOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
workerStatusFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.workerStatus),
|
||||
workerStatusOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
genderFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.gender),
|
||||
genderOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
nationalityFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.nationality),
|
||||
nationalityOptions,
|
||||
'label',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => prefixName.value,
|
||||
() => namePrefix.value,
|
||||
(v) => {
|
||||
if (v === 'mr') gender.value = 'male';
|
||||
else if (v !== '') gender.value = 'female';
|
||||
|
|
@ -140,10 +194,7 @@ watch(
|
|||
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<div
|
||||
v-if="!ocr && !hideTitle"
|
||||
class="col-12 q-pb-sm text-weight-bold text-body1"
|
||||
>
|
||||
<div v-if="!hideTitle" class="col-12 q-pb-sm text-weight-bold text-body1">
|
||||
<q-icon
|
||||
flat
|
||||
size="xs"
|
||||
|
|
@ -156,7 +207,6 @@ watch(
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="!ocr"
|
||||
class="col-12 row justify-between items-center q-pb-sm text-weight-bold"
|
||||
>
|
||||
<div class="app-text-muted">
|
||||
|
|
@ -170,20 +220,19 @@ watch(
|
|||
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div
|
||||
v-if="!ocr"
|
||||
class="col row justify-center q-col-gutter-sml"
|
||||
style="max-height: 50%"
|
||||
v-if="!ocr"
|
||||
>
|
||||
<div style="border: 1px dashed">
|
||||
<q-avatar
|
||||
square
|
||||
size="100px"
|
||||
font-size="50px"
|
||||
color="grey-4"
|
||||
text-color="grey"
|
||||
icon="mdi-image-outline"
|
||||
/>
|
||||
</div>
|
||||
<q-avatar
|
||||
style="border: 1px dashed; border-color: black"
|
||||
square
|
||||
size="100px"
|
||||
font-size="50px"
|
||||
color="grey-4"
|
||||
text-color="grey"
|
||||
icon="mdi-image-outline"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="row q-col-gutter-sm"
|
||||
|
|
@ -201,22 +250,220 @@ watch(
|
|||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
v-model="passportType"
|
||||
:class="{ 'col-12': ocr, 'col-md-3': !ocr }"
|
||||
class="col-6"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="workerStatusOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:options="passportTypeOptions"
|
||||
:for="`${prefixId}-select-passport-type`"
|
||||
:label="$t('customerEmployee.form.passportType')"
|
||||
:for="`${prefixId}-select-visa-type`"
|
||||
:label="$t('customerEmployee.form.workerType')"
|
||||
@filter="workerStatusFilter"
|
||||
:model-value="readonly ? workerStatus || '-' : workerStatus"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (workerStatus = v) : '')
|
||||
"
|
||||
@clear="workerStatus = ''"
|
||||
:rules="[
|
||||
(val) =>
|
||||
(val && val.length > 0) ||
|
||||
$t('form.error.selectField', {
|
||||
field: $t('customerEmployee.form.passportType'),
|
||||
}),
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
@filter="passportTypeFilter"
|
||||
>
|
||||
<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="`${prefixId}-input-previous-passport-Number`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-6"
|
||||
:label="$t('customerEmployee.form.previousPassportNumber')"
|
||||
v-model="previousPassportRef"
|
||||
:rules="[
|
||||
(v) =>
|
||||
(!!v && v.length === 6) ||
|
||||
$t('form.error.requireLength', { msg: 6 }),
|
||||
]"
|
||||
/>
|
||||
|
||||
<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"
|
||||
hide-dropdown-icon
|
||||
class="col-2"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="prefixNameOptions"
|
||||
:for="`${prefixId}-select-prefix-name`"
|
||||
:label="$t('personnel.form.prefixName')"
|
||||
@filter="prefixNameFilter"
|
||||
:model-value="readonly ? namePrefix || '-' : namePrefix"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (namePrefix = v) : '')
|
||||
"
|
||||
@clear="namePrefix = ''"
|
||||
>
|
||||
<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="`${prefixId}-input-first-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
:label="$t('form.firstName')"
|
||||
:model-value="readonly ? firstName || '-' : firstName"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (firstName = v.trim()) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-middle-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
:label="$t('form.middleName')"
|
||||
:model-value="readonly ? middleName || '-' : middleName"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (middleName = v.trim()) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-last-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
:label="$t('form.lastName')"
|
||||
:model-value="readonly ? lastName || '-' : lastName"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (lastName = v.trim()) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-full-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-12"
|
||||
:disable="!readonly"
|
||||
:label="$t('customer.table.fullname')"
|
||||
:model-value="`${(prefixNameOptions.find((v) => v.value === namePrefix) || {}).label || ''} ${firstName || ''} ${lastName || ''}`"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-first-name-en`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('form.firstNameEN')"
|
||||
:model-value="readonly ? firstNameEN || '-' : firstNameEN"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (firstNameEN = v.trim()) : '')
|
||||
"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-middle-name-en`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('form.middleNameEN')"
|
||||
:model-value="readonly ? middleNameEN || '-' : middleNameEN"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (middleNameEN = v.trim()) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-last-name-en`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('form.lastNameEN')"
|
||||
:model-value="readonly ? lastNameEN || '-' : lastNameEN"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (lastNameEN = v.trim()) : '')
|
||||
"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-full-name-en`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-12"
|
||||
:label="$t('customer.table.fullnameEN')"
|
||||
:disable="!readonly"
|
||||
:model-value="`${(prefixNameOptions.find((v) => v.value === namePrefix) || {}).value || ''} ${firstNameEN || ''} ${lastNameEN || ''}`"
|
||||
/>
|
||||
|
||||
<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"
|
||||
class="col-2"
|
||||
dense
|
||||
:readonly="readonly"
|
||||
:options="genderOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
: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>
|
||||
|
|
@ -233,7 +480,7 @@ watch(
|
|||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
:class="{ 'col-12': ocr, 'col-6': !ocr, 'col-md-3': !ocr }"
|
||||
class="col-6 col-md-3"
|
||||
:label="$t('customerEmployee.form.passportNo')"
|
||||
v-model="passportNumber"
|
||||
:rules="[
|
||||
|
|
@ -241,106 +488,36 @@ watch(
|
|||
]"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="fullName && prefixNameOptions.length !== 0"
|
||||
:for="`${prefixId}-input-last-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
<DatePicker
|
||||
v-model="birthDate"
|
||||
class="col-2"
|
||||
:id="`${prefixId}-input-birth-date`"
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
:label="`${$t('personnel.form.firstName')}- ${$t('personnel.form.lastName')}`"
|
||||
:model-value="`${prefixNameOptions.filter((v) => v.value === prefixName)[0].label || ''} ${firstName || ''} ${lastName || ''}`"
|
||||
/>
|
||||
|
||||
<template v-if="!fullName">
|
||||
<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"
|
||||
hide-dropdown-icon
|
||||
:class="{ 'col-2': ocr, 'col-1': !ocr }"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="prefixNameOptions"
|
||||
: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) : '')
|
||||
"
|
||||
@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>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-first-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
:label="$t('personnel.form.firstName')"
|
||||
v-model="firstName"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-last-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
:label="$t('personnel.form.lastName')"
|
||||
v-model="lastName"
|
||||
/>
|
||||
</template>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-passport-ref`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
:class="{ 'col-12': ocr, 'col-6': !ocr }"
|
||||
:label="$t('customerEmployee.form.passportRef')"
|
||||
:model-value="
|
||||
readonly
|
||||
? previousPassportReference || '-'
|
||||
: previousPassportReference
|
||||
"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
typeof v === 'string' ? (previousPassportReference = v) : ''
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-passport-place`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
:class="{ 'col-12': ocr, 'col-6': !ocr, 'col-md-3': !ocr }"
|
||||
:label="$t('customerEmployee.form.passportPlace')"
|
||||
v-model="passportIssuingPlace"
|
||||
:label="$t('form.birthDate')"
|
||||
:disabled-dates="disabledAfterToday"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
(val: string) =>
|
||||
!!val ||
|
||||
$t('form.error.selectField', { field: $t('form.birthDate') }),
|
||||
]"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-age`"
|
||||
:id="`${prefixId}-input-age`"
|
||||
dense
|
||||
outlined
|
||||
readonly
|
||||
:label="$t('personnel.age')"
|
||||
class="col-2"
|
||||
:model-value="
|
||||
birthDate?.toString() === 'Invalid Date' ||
|
||||
birthDate?.toString() === undefined
|
||||
? ''
|
||||
: calculateAge(birthDate)
|
||||
"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
outlined
|
||||
clearable
|
||||
|
|
@ -353,14 +530,69 @@ watch(
|
|||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
:class="{ 'col-12': ocr, 'col-6': !ocr, 'col-md-3': !ocr }"
|
||||
v-model="passportIssuingCountry"
|
||||
class="col"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="nationalityOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:for="`${prefixId}-select-visa-type`"
|
||||
:label="$t('general.nationality')"
|
||||
@filter="nationalityFilter"
|
||||
:model-value="readonly ? nationality || '-' : nationality"
|
||||
@update:model-value="
|
||||
(v) =>
|
||||
typeof v === 'string' ? (birthCountry = nationality = v) : ''
|
||||
"
|
||||
@clear="nationality = ''"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-place-of-birth`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('customerEmployee.form.placeOfBirth')"
|
||||
:model-value="optionStore.mapOption(birthCountry || '')"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (birthCountry = v) : '')
|
||||
"
|
||||
:rules="[(val) => !!val || $t('form.error.required')]"
|
||||
/>
|
||||
|
||||
<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-6 col-md-4"
|
||||
v-model="issueCountry"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:options="passportIssuingCountryOptions"
|
||||
:for="`${prefixId}-select-passport-country`"
|
||||
:label="$t('customerEmployee.form.passportIssuer')"
|
||||
@update:model-value="(v) => (issuePlace = v)"
|
||||
:rules="[
|
||||
(val) =>
|
||||
(val && val.length > 0) ||
|
||||
|
|
@ -378,12 +610,44 @@ watch(
|
|||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-passport-place`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('customerEmployee.form.passportPlace')"
|
||||
:model-value="
|
||||
readonly
|
||||
? optionStore.mapOption(issuePlace || '') || '-'
|
||||
: optionStore.mapOption(issuePlace || '')
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (issuePlace = v) : '')
|
||||
"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
:id="`${prefixId}-date-picker-passport-issueance`"
|
||||
:label="$t('customerEmployee.form.passportIssueDate')"
|
||||
v-model="passportIssueDate"
|
||||
class="col-6"
|
||||
:class="{ 'col-md-3': !ocr }"
|
||||
v-model="issueDate"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (!v) return;
|
||||
if (!expireDate) return;
|
||||
if (new Date(v).getTime() >= new Date(expireDate).getTime()) {
|
||||
const newValue = new Date(v);
|
||||
newValue.setDate(newValue.getDate() + 1);
|
||||
expireDate = newValue;
|
||||
}
|
||||
}
|
||||
"
|
||||
class="col-6 col-md-3"
|
||||
:readonly="readonly"
|
||||
:rules="[
|
||||
(val) =>
|
||||
|
|
@ -396,9 +660,13 @@ watch(
|
|||
<DatePicker
|
||||
:id="`${prefixId}-date-picker-passport-expire`"
|
||||
:label="$t('customerEmployee.form.passportExpireDate')"
|
||||
v-model="passportExpiryDate"
|
||||
class="col-6"
|
||||
:class="{ 'col-md-3': !ocr }"
|
||||
v-model="expireDate"
|
||||
class="col-6 col-md-3"
|
||||
:disabled-dates="
|
||||
(date: Date) =>
|
||||
date.getTime() <
|
||||
((issueDate && new Date(issueDate).getTime()) || Date.now())
|
||||
"
|
||||
:readonly="readonly"
|
||||
:rules="[
|
||||
(val) =>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { QSelect } from 'quasar';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { dateFormat, parseAndFormatDate } from 'src/utils/datetime';
|
||||
import { onMounted, reactive, ref, computed } from 'vue';
|
||||
import useAddressStore, {
|
||||
District,
|
||||
Province,
|
||||
SubDistrict,
|
||||
} from 'stores/address';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import useOptionStore from 'stores/options';
|
||||
import { watch } from 'vue';
|
||||
import DatePicker from '../shared/DatePicker.vue';
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
const { locale } = useI18n();
|
||||
const adrressStore = useAddressStore();
|
||||
const addressStore = useAddressStore();
|
||||
|
||||
const addrOptions = reactive<{
|
||||
provinceOps: Province[];
|
||||
|
|
@ -26,17 +23,25 @@ const addrOptions = reactive<{
|
|||
districtOps: [],
|
||||
subDistrictOps: [],
|
||||
});
|
||||
const arrivalAt = defineModel<string>('arrivalAt');
|
||||
|
||||
const visaType = defineModel<string>('visaType');
|
||||
const visaNumber = defineModel<string>('visaNumber');
|
||||
const visaIssueDate = defineModel<Date | null | string>('visaIssueDate');
|
||||
const visaExpiryDate = defineModel<Date | null | string>('visaExpiryDate');
|
||||
const visaIssuingPlace = defineModel<string>('visaIssuingPlace');
|
||||
const visaStayUntilDate = defineModel<Date | null | string>(
|
||||
'visaStayUntilDate',
|
||||
);
|
||||
const tm6Number = defineModel<string>('tm6Number');
|
||||
const entryDate = defineModel<Date | null | string>('entryDate');
|
||||
const arrivalTMNo = defineModel<string>('arrivalTmNo');
|
||||
const arrivalTM = defineModel<string>('arrivalTm');
|
||||
const mrz = defineModel<string>('mrz');
|
||||
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 expireDate = defineModel<Date>('expireDate');
|
||||
const remark = defineModel<string>('remark');
|
||||
const workerType = defineModel<string>('workerType');
|
||||
const number = defineModel<string>('visaNumber');
|
||||
|
||||
const calculatedVisaDate = computed(() => {
|
||||
if (!issueDate.value) return undefined;
|
||||
return calculate90DayNext(issueDate.value);
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
title?: string;
|
||||
|
|
@ -46,12 +51,25 @@ defineProps<{
|
|||
separator?: boolean;
|
||||
typeCustomer?: string;
|
||||
prefixId: string;
|
||||
hideTitle?: boolean;
|
||||
|
||||
ocr?: boolean;
|
||||
}>();
|
||||
|
||||
function calculate90DayNext(currentDate: Date | null | string) {
|
||||
if (currentDate === null) return null;
|
||||
|
||||
const date =
|
||||
typeof currentDate === 'string'
|
||||
? new Date(currentDate)
|
||||
: new Date(currentDate.getTime());
|
||||
date.setDate(date.getDate() + 90);
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
async function fetchProvince() {
|
||||
const result = await adrressStore.fetchProvince();
|
||||
const result = await addressStore.fetchProvince();
|
||||
|
||||
if (result) addrOptions.provinceOps = result;
|
||||
}
|
||||
|
|
@ -66,12 +84,24 @@ let visaTypeFilter: (
|
|||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
const workerTypeOptions = ref<Record<string, unknown>[]>([]);
|
||||
let workerTypeFilter: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
onMounted(() => {
|
||||
visaTypeFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.nationality),
|
||||
visaTypeOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
workerTypeFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.workerType),
|
||||
workerTypeOptions,
|
||||
'label',
|
||||
);
|
||||
});
|
||||
|
||||
watch(
|
||||
|
|
@ -82,148 +112,348 @@ watch(
|
|||
visaTypeOptions,
|
||||
'label',
|
||||
);
|
||||
|
||||
workerTypeFilter = selectFilterOptionRefMod(
|
||||
ref(optionStore.globalOption?.workerType),
|
||||
workerTypeOptions,
|
||||
'label',
|
||||
);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div v-if="!ocr" class="col-12 q-pb-sm text-weight-bold text-body1">
|
||||
<div class="row col-12">
|
||||
<div v-if="!hideTitle" class="col-12 q-pb-sm text-weight-bold text-body1">
|
||||
<q-icon
|
||||
flat
|
||||
size="xs"
|
||||
class="q-pa-sm rounded q-mr-xs"
|
||||
color="info"
|
||||
name="mdi-notebook-outline"
|
||||
name="mdi-passport"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`${title}`) }}
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<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-4': !ocr, 'col-6': ocr }"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="visaTypeOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:for="`${prefixId}-select-visa-type`"
|
||||
:label="$t('customerEmployee.form.visaType')"
|
||||
@filter="visaTypeFilter"
|
||||
:model-value="readonly ? visaType || '-' : visaType"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (visaType = v) : '')
|
||||
"
|
||||
@clear="visaType = ''"
|
||||
|
||||
<div
|
||||
class="col-12 row justify-between items-center q-pb-sm text-weight-bold"
|
||||
>
|
||||
<div class="app-text-muted">
|
||||
<slot name="expiryDate" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<slot name="button"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div v-if="!ocr" class="col row justify-center" style="max-height: 50%">
|
||||
<q-avatar
|
||||
style="border: 1px dashed; border-color: black"
|
||||
square
|
||||
size="100px"
|
||||
font-size="50px"
|
||||
color="grey-4"
|
||||
text-color="grey"
|
||||
icon="mdi-image-outline"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="row q-col-gutter-sm"
|
||||
:class="{ 'col-10': !ocr, 'col-12': ocr }"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
<!-- :rules="[
|
||||
(val: string) =>
|
||||
!!val || $t('selectValidate') + $t('formDialogInputVisaType'),
|
||||
]" -->
|
||||
<q-input
|
||||
:for="`${prefixId}-input-visa-no`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
:class="{ 'col-4': !ocr, 'col-6': ocr }"
|
||||
:label="$t('customerEmployee.form.visaNo')"
|
||||
:model-value="readonly ? visaNumber || '-' : visaNumber"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (visaNumber = v) : '')
|
||||
"
|
||||
/>
|
||||
<!-- :rules="[
|
||||
(val: string) =>
|
||||
!!val || $t('inputValidate') + $t('formDialogInputVisaNo'),
|
||||
]" -->
|
||||
<DatePicker
|
||||
:class="{ 'col-2': !ocr, 'col-6': ocr }"
|
||||
:id="`${prefixId}-date-picker-visa-issuance`"
|
||||
:readonly="readonly"
|
||||
:label="$t('customerEmployee.form.visaIssuance')"
|
||||
v-model="visaIssueDate"
|
||||
clearable
|
||||
/>
|
||||
<DatePicker
|
||||
:class="{ 'col-2': !ocr, 'col-6': ocr }"
|
||||
:id="`${prefixId}-date-picker-visa-expire`"
|
||||
:readonly="readonly"
|
||||
:label="$t('customerEmployee.form.visaExpire')"
|
||||
v-model="visaExpiryDate"
|
||||
clearable
|
||||
/>
|
||||
<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-4': !ocr, 'col-6': ocr }"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="workerTypeOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:for="`${prefixId}-select-visa-type`"
|
||||
:label="$t('customerEmployee.form.workerType')"
|
||||
@filter="workerTypeFilter"
|
||||
:model-value="readonly ? workerType || '-' : workerType"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (workerType = v) : '')
|
||||
"
|
||||
@clear="workerType = ''"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-visa-place`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
:class="{ 'col-5': !ocr, 'col-6': ocr }"
|
||||
:label="$t('customerEmployee.form.visaPlace')"
|
||||
:model-value="readonly ? visaIssuingPlace || '-' : visaIssuingPlace"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (visaIssuingPlace = v) : '')
|
||||
"
|
||||
/>
|
||||
<!-- :rules="[
|
||||
(val: string) =>
|
||||
!!val || $t('selectValidate') + $t('formDialogInputVisaPlace'),
|
||||
]" -->
|
||||
<DatePicker
|
||||
:class="{ 'col-4': !ocr, 'col-6': ocr }"
|
||||
:id="`${prefixId}-date-picker-visa-until`"
|
||||
:readonly="readonly"
|
||||
:label="$t('customerEmployee.form.visaStayUntil')"
|
||||
v-model="visaStayUntilDate"
|
||||
clearable
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-visa-no`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
:class="{ 'col-4': !ocr, 'col-6': ocr }"
|
||||
:label="$t('customerEmployee.form.visaNo')"
|
||||
:model-value="readonly ? number || '-' : number"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (number = v) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-tm6`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
:class="{ 'col-5': !ocr, 'col-6': ocr }"
|
||||
:label="$t('customerEmployee.form.visaTM6')"
|
||||
:model-value="readonly ? tm6Number || '-' : tm6Number"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (tm6Number = v) : '')
|
||||
"
|
||||
@clear="tm6Number = ''"
|
||||
/>
|
||||
<!-- :rules="[
|
||||
(val: string) =>
|
||||
!!val || $t('inputValidate') + $t('formDialogInputVisaTM6'),
|
||||
]" -->
|
||||
<DatePicker
|
||||
:class="{ 'col-4': !ocr, 'col-6': ocr }"
|
||||
:id="`${prefixId}-date-picker-visa-enter`"
|
||||
:readonly="readonly"
|
||||
:label="$t('customerEmployee.form.visaEnter')"
|
||||
v-model="entryDate"
|
||||
clearable
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-visa-place`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
:class="{ 'col-4': !ocr, 'col-6': ocr }"
|
||||
:label="$t('customerEmployee.form.visaPlace')"
|
||||
:model-value="readonly ? issuePlace || '-' : issuePlace"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (issuePlace = v) : '')
|
||||
"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="ocr"
|
||||
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-4': !ocr, 'col-6': ocr }"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="visaTypeOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:for="`${prefixId}-select-visa-type`"
|
||||
:label="$t('customerEmployee.form.visaType')"
|
||||
@filter="visaTypeFilter"
|
||||
:model-value="readonly ? type || '-' : type"
|
||||
@update:model-value="(v) => (typeof v === 'string' ? (type = v) : '')"
|
||||
@clear="type = ''"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<div class="row q-col-gutter-sm">
|
||||
<q-select
|
||||
v-if="!ocr"
|
||||
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-4': !ocr, 'col-6': ocr }"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="visaTypeOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:for="`${prefixId}-select-visa-type`"
|
||||
:label="$t('customerEmployee.form.visaType')"
|
||||
@filter="visaTypeFilter"
|
||||
:model-value="readonly ? type || '-' : type"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (type = v) : '')
|
||||
"
|
||||
@clear="type = ''"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<div class="col">
|
||||
<DatePicker
|
||||
:id="`${prefixId}-date-picker-visa-issuance`"
|
||||
:readonly="readonly"
|
||||
:label="$t('customerEmployee.form.visaIssuance')"
|
||||
v-model="issueDate"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (!v) return;
|
||||
if (!expireDate) return;
|
||||
if (new Date(v).getTime() >= new Date(expireDate).getTime()) {
|
||||
const newValue = new Date(v);
|
||||
newValue.setDate(newValue.getDate() + 1);
|
||||
expireDate = newValue;
|
||||
}
|
||||
}
|
||||
"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<DatePicker
|
||||
:id="`${prefixId}-date-picker-visa-expire`"
|
||||
:readonly="readonly"
|
||||
:label="$t('customerEmployee.form.visaExpire')"
|
||||
v-model="expireDate"
|
||||
:disabled-dates="
|
||||
(date: Date) =>
|
||||
date.getTime() <
|
||||
((issueDate && new Date(issueDate).getTime()) || Date.now())
|
||||
"
|
||||
clearable
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<DatePicker
|
||||
:id="`${prefixId}-date-picker-visa-issuance`"
|
||||
:readonly
|
||||
:disabled="!readonly"
|
||||
:label="$t('customerEmployee.form.visa90Day')"
|
||||
:model-value="calculatedVisaDate"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-visa-no`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('customerEmployee.form.arrivalCardNo')"
|
||||
:model-value="readonly ? arrivalTMNo || '-' : arrivalTMNo"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (arrivalTMNo = v) : '')
|
||||
"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
class="col-4"
|
||||
:id="`${prefixId}-date-picker-visa-enter`"
|
||||
:readonly="readonly"
|
||||
:label="$t('customerEmployee.form.visaEnter')"
|
||||
v-model="arrivalTM"
|
||||
clearable
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-visa-no`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('customerEmployee.form.visaCheckpoint')"
|
||||
:model-value="readonly ? arrivalAt || '-' : arrivalAt"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (arrivalAt = v) : '')
|
||||
"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
|
||||
<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-4"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="visaTypeOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:for="`${prefixId}-select-issue-country`"
|
||||
:label="$t('customerEmployee.form.issueCountry')"
|
||||
@filter="visaTypeFilter"
|
||||
:model-value="readonly ? issueCountry || '-' : issueCountry"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (issueCountry = v) : '')
|
||||
"
|
||||
@clear="type = ''"
|
||||
:rules="[
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
]"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-entry-count`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('customerEmployee.form.entryCount')"
|
||||
v-model="entryCount"
|
||||
type="number"
|
||||
min="0"
|
||||
:rules="[(val) => (!!val && val > 0) || $t('form.error.required')]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -45,39 +45,6 @@ defineEmits<{
|
|||
(e: 'undo', index: number): void;
|
||||
}>();
|
||||
|
||||
function addData() {
|
||||
// const canAdd = checkTabBeforeAdd(employeeWork.value || []);
|
||||
const canAdd = true;
|
||||
if (canAdd) {
|
||||
employeeWork.value?.push({
|
||||
workEndDate: null,
|
||||
workPermitExpireDate: null,
|
||||
workPermitIssuDate: null,
|
||||
workPermitNo: '',
|
||||
workplace: '',
|
||||
jobType: '',
|
||||
positionName: '',
|
||||
ownerName: '',
|
||||
remark: '',
|
||||
});
|
||||
if (employeeWork.value) {
|
||||
tab.value = `tab${employeeWork.value.length - 1}`;
|
||||
currentIndex.value = employeeWork.value.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeData(index: number) {
|
||||
if (!employeeWork.value) return;
|
||||
if (employeeWork.value.length === 1) return;
|
||||
|
||||
employeeWork.value.splice(index, 1);
|
||||
|
||||
if (index) if (tab.value === `tab${index}`) tab.value = `tab${index - 1}`;
|
||||
if (tab.value === `tab${employeeWork.value.length}`)
|
||||
tab.value = `tab${employeeWork.value.length - 1}`;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
tab.value = 'tab0';
|
||||
});
|
||||
|
|
@ -106,27 +73,6 @@ const workplaceFilter = selectFilterOptionRefMod(
|
|||
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<div class="col-12 q-pb-sm 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)"
|
||||
/>
|
||||
{{ $t(`customerEmployee.formWorkHistory.title`) }}
|
||||
<AddButton
|
||||
v-if="currentIndex === -1 && !hideAction"
|
||||
id="btn-add-bank"
|
||||
icon-only
|
||||
class="q-ml-sm"
|
||||
type="button"
|
||||
@click="addData"
|
||||
:disabled="!(currentIndex === -1)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(work, index) in employeeWork"
|
||||
v-bind:key="index"
|
||||
|
|
@ -177,36 +123,21 @@ const workplaceFilter = selectFilterOptionRefMod(
|
|||
/>
|
||||
</div>
|
||||
</span>
|
||||
<q-select
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-owner-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
clearable
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
:readonly="readonly || work.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-6"
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
v-model="work.jobType"
|
||||
:dense="dense"
|
||||
:readonly="readonly || work.statusSave"
|
||||
:options="jobTypeOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:for="`${prefixId}-select-job-type`"
|
||||
:label="$t('customerEmployee.formWorkHistory.jobType')"
|
||||
@filter="jobTypeFilter"
|
||||
>
|
||||
<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('customerEmployee.formWorkHistory.employerName')"
|
||||
:model-value="work.ownerName"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (work.ownerName = v.trim()) : '')
|
||||
"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
outlined
|
||||
clearable
|
||||
|
|
@ -237,63 +168,7 @@ const workplaceFilter = selectFilterOptionRefMod(
|
|||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-work-end-date`"
|
||||
:label="$t('general.remark')"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || work.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-12"
|
||||
v-model="work.remark"
|
||||
type="textarea"
|
||||
/>
|
||||
<DatePicker
|
||||
:label="$t('customerEmployee.formWorkHistory.workUntil')"
|
||||
v-model="work.workEndDate"
|
||||
class="col-3"
|
||||
:id="`${prefixId}-input-work-until-date`"
|
||||
:readonly="readonly || work.statusSave"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-work-permit-no`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || work.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-6"
|
||||
:label="$t('customerEmployee.formWorkHistory.permitNo')"
|
||||
v-model="work.workPermitNo"
|
||||
/>
|
||||
<DatePicker
|
||||
:label="$t('customerEmployee.formWorkHistory.permitIssueDate')"
|
||||
v-model="work.workPermitIssuDate"
|
||||
class="col-3"
|
||||
:id="`${prefixId}-date-picker-work-permit-issue-date`"
|
||||
:readonly="readonly || work.statusSave"
|
||||
clearable
|
||||
/>
|
||||
<DatePicker
|
||||
:label="$t('customerEmployee.formWorkHistory.permitExpireDate')"
|
||||
v-model="work.workPermitExpireDate"
|
||||
class="col-3"
|
||||
:id="`${prefixId}-date-picker-work-permit-expire-date`"
|
||||
:readonly="readonly || work.statusSave"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-owner-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || work.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-5"
|
||||
:label="$t('customerEmployee.formWorkHistory.employerName')"
|
||||
v-model="work.ownerName"
|
||||
/>
|
||||
<q-select
|
||||
outlined
|
||||
clearable
|
||||
|
|
@ -303,7 +178,38 @@ const workplaceFilter = selectFilterOptionRefMod(
|
|||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
class="col-6"
|
||||
input-debounce="0"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
v-model="work.jobType"
|
||||
:dense="dense"
|
||||
:readonly="readonly || work.statusSave"
|
||||
:options="jobTypeOptions"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:for="`${prefixId}-select-job-type`"
|
||||
:label="$t('customerEmployee.formWorkHistory.jobType')"
|
||||
@filter="jobTypeFilter"
|
||||
>
|
||||
<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
|
||||
class="col-6"
|
||||
input-debounce="0"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
|
|
@ -324,6 +230,70 @@ const workplaceFilter = selectFilterOptionRefMod(
|
|||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
:for="`${prefixId}-input-owner-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || work.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('customerEmployee.formWorkHistory.identityNo')"
|
||||
v-model="work.identityNo"
|
||||
mask="#############"
|
||||
:rules="[
|
||||
(val) =>
|
||||
!val ||
|
||||
(val.length === 13 && /^[0-9]+$/.test(val)) ||
|
||||
$t('form.error.invalidCustomeMessage', {
|
||||
msg: $t('form.error.requireLength', { msg: 13 }),
|
||||
}),
|
||||
]"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-owner-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || work.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('customerEmployee.formWorkHistory.permitNo')"
|
||||
v-model="work.workPermitNo"
|
||||
/>
|
||||
<q-input
|
||||
:for="`${prefixId}-input-owner-name`"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly || work.statusSave"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('customerEmployee.formWorkHistory.permitIssuedAt')"
|
||||
v-model="work.workPermitIssueAt"
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
:label="$t('customerEmployee.formWorkHistory.permitIssueDate')"
|
||||
v-model="work.workPermitIssueDate"
|
||||
class="col-3"
|
||||
:id="`${prefixId}-date-picker-work-permit-issue-date`"
|
||||
:readonly="readonly || work.statusSave"
|
||||
clearable
|
||||
/>
|
||||
<DatePicker
|
||||
:label="$t('customerEmployee.formWorkHistory.permitExpireDate')"
|
||||
v-model="work.workPermitExpireDate"
|
||||
class="col-3"
|
||||
:id="`${prefixId}-date-picker-work-permit-expire-date`"
|
||||
:readonly="readonly || work.statusSave"
|
||||
:disabled-dates="
|
||||
(date: Date) =>
|
||||
date.getTime() <
|
||||
((work.workPermitIssueDate &&
|
||||
new Date(work.workPermitIssueDate).getTime()) ||
|
||||
Date.now())
|
||||
"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
361
src/components/03_customer-management/TableEmployee.vue
Normal file
361
src/components/03_customer-management/TableEmployee.vue
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
<script setup lang="ts">
|
||||
import { calculateAge, dateFormat } from 'src/utils/datetime';
|
||||
import { calculateDaysUntilExpire } from 'stores/utils';
|
||||
import { baseUrl } from 'src/stores/utils';
|
||||
import useOptionStore from 'stores/options';
|
||||
import { columnsEmployee as columns } from './customer';
|
||||
|
||||
import PersonCard from 'components/shared/PersonCard.vue';
|
||||
import KebabAction from '../shared/KebabAction.vue';
|
||||
import ExpirationDate from 'components/03_customer-management/ExpirationDate.vue';
|
||||
import { AddButton } from 'components/button';
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
const pageSize = defineModel<number>('pageSize', { default: 30 });
|
||||
const currentPage = defineModel<number>('currentPage', { default: 1 });
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
gridView?: boolean;
|
||||
listEmployee: any[];
|
||||
columnsEmployee?: any[];
|
||||
fieldSelected?: string[];
|
||||
inTable?: boolean;
|
||||
addButton?: boolean;
|
||||
prefixId?: string;
|
||||
}>(),
|
||||
{
|
||||
gridView: false,
|
||||
fieldSelected: () => [
|
||||
'orderNumber',
|
||||
'firstName',
|
||||
'general.age',
|
||||
'formDialogInputNationality',
|
||||
'formDialogInputPassportNo',
|
||||
'passportExpiryDate',
|
||||
'visaExpireDate',
|
||||
'beDue',
|
||||
'branchLabel',
|
||||
'action',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
defineEmits<{
|
||||
(e: 'add'): void;
|
||||
(e: 'view', data: any): void;
|
||||
(e: 'edit', data: any): void;
|
||||
(e: 'delete', data: any): void;
|
||||
(e: 'history', data: any): void;
|
||||
(e: 'toggleStatus', data: any): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
:grid="gridView"
|
||||
:rows="listEmployee"
|
||||
:columns="columnsEmployee || columns"
|
||||
class="full-width"
|
||||
card-container-class="q-col-gutter-md"
|
||||
row-key="name"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-pagination
|
||||
:visible-columns="fieldSelected"
|
||||
:no-data-label="$t('general.noDataTable')"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
:style="`background-color: ${inTable ? 'hsla(var(--green-4-hsl) / 0.07)' : 'hsla(var(--info-bg) / 0.07'} `"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span v-if="col.label === 'nameEmployee'">
|
||||
{{ $t('fullname') }}
|
||||
</span>
|
||||
<span v-if="col.label === '' && !!addButton">
|
||||
<AddButton
|
||||
:id="`${prefixId || 'default'}-btn-add-employee`"
|
||||
icon-only
|
||||
@click.stop="() => $emit('add')"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="col.label !== ''">
|
||||
{{ $t(col.label) }}
|
||||
</span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
:class="{
|
||||
'app-text-muted': props.row.status === 'INACTIVE',
|
||||
'status-active': props.row.status !== 'INACTIVE',
|
||||
'status-inactive': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
:props="props"
|
||||
:id="`row-table-${props.row.firstNameEN}`"
|
||||
@click="
|
||||
() => {
|
||||
// employeeFormState.drawerModal = true;
|
||||
// employeeFormStore.assignFormDataEmployee(
|
||||
// props.row.id,
|
||||
// );
|
||||
// $router.push(`/customer-management/${props.row.id}/branch`);
|
||||
// openDialogInputForm('INFO', props.row.id);
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-td class="text-center" v-if="fieldSelected.includes('orderNumber')">
|
||||
{{ (currentPage - 1) * pageSize + props.rowIndex + 1 }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('firstName')">
|
||||
<div class="row items-center" style="flex-wrap: nowrap">
|
||||
<div
|
||||
style="width: 50px; display: flex; margin-bottom: var(--size-2)"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: hsl(var(--pink-6-hsl));
|
||||
padding: 3px;
|
||||
"
|
||||
>
|
||||
<div class="full-width full-height">
|
||||
<q-avatar size="md">
|
||||
<q-img
|
||||
:src="
|
||||
`${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}` ||
|
||||
'/images/employee-avatar.png'
|
||||
"
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
>
|
||||
<template #error>
|
||||
<span>
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
src="/images/employee-avatar.png"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</q-img>
|
||||
|
||||
<q-badge
|
||||
class="absolute-bottom-right no-padding"
|
||||
style="
|
||||
border-radius: 50%;
|
||||
min-width: 8px;
|
||||
min-height: 8px;
|
||||
"
|
||||
:style="{
|
||||
background: `var(--${props.row.status === 'INACTIVE' ? 'stone-5' : 'green-6'})`,
|
||||
}"
|
||||
></q-badge>
|
||||
</q-avatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col text-left">
|
||||
<div class="col">
|
||||
{{ `${props.row.firstNameEN} ${props.row.lastNameEN}` || '-' }}
|
||||
|
||||
<q-icon
|
||||
class="q-pl-xs"
|
||||
:class="{
|
||||
'symbol-gender': props.row.gender,
|
||||
'symbol-gender__male': props.row.gender === 'male',
|
||||
'symbol-gender__female': props.row.gender === 'female',
|
||||
}"
|
||||
:name="`mdi-gender-${props.row.gender === 'male' ? 'male' : 'female'}`"
|
||||
width="24px"
|
||||
/>
|
||||
</div>
|
||||
<div class="col app-text-muted">
|
||||
{{ `${props.row.firstName} ${props.row.lastName}` || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('general.age')">
|
||||
{{ calculateAge(props.row.dateOfBirth) || '-' }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('formDialogInputNationality')">
|
||||
{{ optionStore.mapOption(props.row.nationality || '-') }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('formDialogInputPassportNo')">
|
||||
{{
|
||||
props.row.employeePassport[0]
|
||||
? props.row.employeePassport[0].number
|
||||
: '-'
|
||||
}}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('passportExpiryDate')">
|
||||
{{
|
||||
props.row.employeePassport[0]
|
||||
? dateFormat(props.row.employeePassport[0].expireDate) || '-'
|
||||
: '-'
|
||||
}}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('visaExpireDate')">
|
||||
{{
|
||||
props.row.employeeVisa[0]
|
||||
? dateFormat(props.row.employeeVisa[0].expireDate) || '-'
|
||||
: '-'
|
||||
}}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('beDue')">
|
||||
<ExpirationDate
|
||||
show-all-day
|
||||
:expiration-date="
|
||||
props.row.employeeVisa[0]
|
||||
? props.row.employeeVisa[0].expireDate || undefined
|
||||
: undefined
|
||||
"
|
||||
/>
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('branchLabel')">
|
||||
<div class="row items-center" v-if="props.row.customerBranch">
|
||||
<div class="col text-left">
|
||||
<div class="col">
|
||||
{{ props.row.customerBranch.code || '-' }}
|
||||
</div>
|
||||
<div class="col app-text-muted">
|
||||
{{
|
||||
$i18n.locale === 'eng'
|
||||
? `${props.row.customerBranch.registerNameEN || ''}`
|
||||
: `${props.row.customerBranch.registerName || ''}`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td>
|
||||
<q-btn
|
||||
:id="`btn-eye-${props.row.firstName}`"
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
dense
|
||||
round
|
||||
flat
|
||||
@click.stop="$emit('view', props.row)"
|
||||
/>
|
||||
<KebabAction
|
||||
v-if="!inTable"
|
||||
:id-name="props.row.firstName"
|
||||
:status="props.row.status"
|
||||
@view="$emit('view', props.row)"
|
||||
@edit="$emit('edit', props.row)"
|
||||
@delete="$emit('delete', props.row)"
|
||||
@change-status="$emit('toggleStatus', props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="props">
|
||||
<div class="col-12 col-md-3 col-sm-6">
|
||||
<PersonCard
|
||||
:id="`card-${props.row.firstNameEN}`"
|
||||
:field-selected="fieldSelected"
|
||||
history
|
||||
:prefix-id="props.row.firstNameEN ?? props.rowIndex"
|
||||
:data="{
|
||||
code: props.row.code,
|
||||
name:
|
||||
$i18n.locale === 'eng'
|
||||
? `${props.row.firstNameEN} ${props.row.lastNameEN} `.trim()
|
||||
: `${props.row.firstName} ${props.row.lastName} `.trim(),
|
||||
img:
|
||||
`${baseUrl}/employee/${props.row.id}/image/${props.row.selectedImage}` ||
|
||||
'/images/employee-avatar.png',
|
||||
fallbackImg: '/images/employee-avatar.png',
|
||||
male: props.row.gender === 'male',
|
||||
female: props.row.gender === 'female',
|
||||
detail: [
|
||||
{
|
||||
icon: 'mdi-passport',
|
||||
value: optionStore.mapOption(props.row.nationality),
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'mdi-clock-outline',
|
||||
value: props.row.dateOfBirth
|
||||
? (calculateAge(props.row.dateOfBirth) ?? '')
|
||||
: '',
|
||||
},
|
||||
],
|
||||
}"
|
||||
:tag="[
|
||||
{
|
||||
color: 'pink',
|
||||
value: $t('customer.employee'),
|
||||
},
|
||||
]"
|
||||
:disabled="props.row.status === 'INACTIVE'"
|
||||
@history="() => $emit('history', props.row)"
|
||||
@update-card="() => $emit('edit', props.row)"
|
||||
@enter-card="() => $emit('view', props.row)"
|
||||
@delete-card="() => $emit('delete', props.row)"
|
||||
@toggle-status="() => $emit('toggleStatus', props.row)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.branch-card__icon {
|
||||
background-color: hsla(var(--_branch-card-bg) / 0.15);
|
||||
border-radius: 50%;
|
||||
padding: var(--size-1);
|
||||
position: relative;
|
||||
transform: rotate(45deg);
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
block-size: 0.5rem;
|
||||
aspect-ratio: 1;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
right: -0.1rem;
|
||||
top: calc(50% - 0.25rem);
|
||||
bottom: calc(50% - 0.25rem);
|
||||
background-color: hsla(var(--_branch-status-color) / 1);
|
||||
}
|
||||
|
||||
& :deep(.q-avatar) {
|
||||
transform: rotate(-45deg);
|
||||
color: hsla(var(--_branch-card-bg) / 1);
|
||||
}
|
||||
}
|
||||
|
||||
& .symbol-gender {
|
||||
color: hsla(var(--_fg));
|
||||
|
||||
&.symbol-gender__male {
|
||||
--_fg: var(--gender-male);
|
||||
}
|
||||
|
||||
&.symbol-gender__female {
|
||||
--_fg: var(--gender-female);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { calculateAge, dateFormat } from 'src/utils/datetime';
|
||||
import { calculateDaysUntilExpire } from 'stores/utils';
|
||||
import { baseUrl } from 'src/stores/utils';
|
||||
import useOptionStore from 'stores/options';
|
||||
|
||||
import PersonCard from 'components/shared/PersonCard.vue';
|
||||
import KebabAction from '../shared/KebabAction.vue';
|
||||
import useOptionStore from 'stores/options';
|
||||
import ExpirationDate from 'components/03_customer-management/ExpirationDate.vue';
|
||||
import { AddButton } from 'components/button';
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
const pageSize = defineModel<number>('pageSize', { default: 30 });
|
||||
|
|
@ -17,6 +20,8 @@ const prop = withDefaults(
|
|||
columnsEmployee: any[];
|
||||
fieldSelected?: string[];
|
||||
inTable?: boolean;
|
||||
addButton?: boolean;
|
||||
prefixId?: string;
|
||||
}>(),
|
||||
{
|
||||
gridView: false,
|
||||
|
|
@ -27,7 +32,8 @@ const prop = withDefaults(
|
|||
'formDialogInputNationality',
|
||||
'formDialogInputPassportNo',
|
||||
'passportExpiryDate',
|
||||
'formDialogEmployeeNRCNo',
|
||||
'visaExpireDate',
|
||||
'beDue',
|
||||
'branchLabel',
|
||||
'action',
|
||||
],
|
||||
|
|
@ -35,9 +41,11 @@ const prop = withDefaults(
|
|||
);
|
||||
|
||||
defineEmits<{
|
||||
(e: 'add'): void;
|
||||
(e: 'view', data: any): void;
|
||||
(e: 'edit', data: any): void;
|
||||
(e: 'delete', data: any): void;
|
||||
(e: 'history', data: any): void;
|
||||
(e: 'toggleStatus', data: any): void;
|
||||
}>();
|
||||
</script>
|
||||
|
|
@ -55,17 +63,24 @@ defineEmits<{
|
|||
:rows-per-page-options="[0]"
|
||||
hide-pagination
|
||||
:visible-columns="fieldSelected"
|
||||
:no-data-label="$t('general.noDataTable')"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
:style="`background-color: ${inTable ? '#F0FFF1' : 'hsla(var(--info-bg) / 0.07'} `"
|
||||
:style="`background-color: ${inTable ? 'hsla(var(--green-4-hsl) / 0.07)' : 'hsla(var(--info-bg) / 0.07'} `"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span v-if="col.label === 'nameEmployee'">
|
||||
{{ $t('fullname') }}
|
||||
</span>
|
||||
|
||||
<span v-if="col.label === '' && !!addButton">
|
||||
<AddButton
|
||||
:id="`${prefixId || 'default'}-btn-add-employee`"
|
||||
icon-only
|
||||
@click.stop="() => $emit('add')"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="col.label !== ''">
|
||||
{{ $t(col.label) }}
|
||||
</span>
|
||||
|
|
@ -157,7 +172,7 @@ defineEmits<{
|
|||
|
||||
<div class="col text-left">
|
||||
<div class="col">
|
||||
{{ `${props.row.firstName} ${props.row.lastName}` || '-' }}
|
||||
{{ `${props.row.firstNameEN} ${props.row.lastNameEN}` || '-' }}
|
||||
|
||||
<q-icon
|
||||
class="q-pl-xs"
|
||||
|
|
@ -171,7 +186,7 @@ defineEmits<{
|
|||
/>
|
||||
</div>
|
||||
<div class="col app-text-muted">
|
||||
{{ `${props.row.firstNameEN} ${props.row.lastNameEN}` || '-' }}
|
||||
{{ `${props.row.firstName} ${props.row.lastName}` || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -186,15 +201,38 @@ defineEmits<{
|
|||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('formDialogInputPassportNo')">
|
||||
{{ props.row.passportNumber || '-' }}
|
||||
{{
|
||||
props.row.employeePassport[0]
|
||||
? props.row.employeePassport[0].number
|
||||
: '-'
|
||||
}}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('passportExpiryDate')">
|
||||
{{ dateFormat(props.row.passportExpiryDate) || '-' }}
|
||||
{{
|
||||
props.row.employeePassport[0]
|
||||
? dateFormat(props.row.employeePassport[0].expireDate) || '-'
|
||||
: '-'
|
||||
}}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('formDialogEmployeeNRCNo')">
|
||||
{{ props.row.nrcNo || '-' }}
|
||||
<q-td v-if="fieldSelected.includes('visaExpireDate')">
|
||||
{{
|
||||
props.row.employeeVisa[0]
|
||||
? dateFormat(props.row.employeeVisa[0].expireDate) || '-'
|
||||
: '-'
|
||||
}}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('beDue')">
|
||||
<ExpirationDate
|
||||
show-all-day
|
||||
:expiration-date="
|
||||
props.row.employeeVisa[0]
|
||||
? props.row.employeeVisa[0].expireDate || undefined
|
||||
: undefined
|
||||
"
|
||||
/>
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('branchLabel')">
|
||||
|
|
@ -206,8 +244,8 @@ defineEmits<{
|
|||
<div class="col app-text-muted">
|
||||
{{
|
||||
$i18n.locale === 'eng'
|
||||
? `${props.row.customerBranch.registerNameEN}`
|
||||
: `${props.row.customerBranch.registerName}`
|
||||
? `${props.row.customerBranch.registerNameEN || ''}`
|
||||
: `${props.row.customerBranch.registerName || ''}`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -225,6 +263,7 @@ defineEmits<{
|
|||
@click.stop="$emit('view', props.row)"
|
||||
/>
|
||||
<KebabAction
|
||||
v-if="!inTable"
|
||||
:id-name="props.row.firstName"
|
||||
:status="props.row.status"
|
||||
@view="$emit('view', props.row)"
|
||||
|
|
@ -325,4 +364,15 @@ defineEmits<{
|
|||
--_fg: var(--gender-female);
|
||||
}
|
||||
}
|
||||
|
||||
.status-active {
|
||||
--_branch-status-color: var(--green-6-hsl);
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
--_branch-status-color: var(--stone-5-hsl);
|
||||
--_branch-badge-bg: var(--stone-5-hsl);
|
||||
filter: grayscale(0.5);
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ import {
|
|||
UndoButton,
|
||||
} from 'components/button';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import useOptionStore from 'stores/options';
|
||||
|
||||
const { locale } = useI18n();
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
const optionsBranch = defineModel<{ id: string; name: string }[]>(
|
||||
'optionsBranch',
|
||||
{ default: [] },
|
||||
|
|
@ -109,6 +111,7 @@ defineEmits<{
|
|||
:id="`${prefixId}-select-employer-branch`"
|
||||
:for="`${prefixId}-select-employer-branch`"
|
||||
:use-input="!customerBranch"
|
||||
autocomplete="off"
|
||||
input-debounce="0"
|
||||
:hide-dropdown-icon="readonly"
|
||||
:dense="dense"
|
||||
|
|
@ -162,11 +165,14 @@ defineEmits<{
|
|||
</span>
|
||||
{{
|
||||
scope.opt.customer.customerType === 'CORP'
|
||||
? scope.opt.customerName
|
||||
? $i18n.locale === 'eng'
|
||||
? scope.opt.registerNameEN
|
||||
: scope.opt.registerName
|
||||
: $i18n.locale === 'eng'
|
||||
? `${scope.opt.firstNameEN} ${scope.opt.lastNameEN}` ||
|
||||
? `${optionStore.mapOption(scope.opt.namePrefix)} ${scope.opt.firstNameEN} ${scope.opt.lastNameEN}` ||
|
||||
'-'
|
||||
: `${optionStore.mapOption(scope.opt.namePrefix)} ${scope.opt.firstName} ${scope.opt.lastName}` ||
|
||||
'-'
|
||||
: `${scope.opt.firstName} ${scope.opt.lastName}` || '-'
|
||||
}}
|
||||
({{ scope.opt.code }})
|
||||
</span>
|
||||
|
|
@ -217,10 +223,14 @@ defineEmits<{
|
|||
</span>
|
||||
{{
|
||||
scope.opt.customer.customerType === 'CORP'
|
||||
? scope.opt.customerName
|
||||
? $i18n.locale === 'eng'
|
||||
? scope.opt.registerNameEN
|
||||
: scope.opt.registerName
|
||||
: $i18n.locale === 'eng'
|
||||
? `${scope.opt.firstNameEN} ${scope.opt.lastNameEN}` || '-'
|
||||
: `${scope.opt.firstName} ${scope.opt.lastName}` || '-'
|
||||
? `${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>
|
||||
|
|
@ -293,11 +303,12 @@ defineEmits<{
|
|||
:readonly="readonly"
|
||||
class="col-6"
|
||||
:label="$t('customerEmployee.form.nrcNo')"
|
||||
:model-value="readonly ? nrcNo || '-' : nrcNo"
|
||||
:model-value="nrcNo"
|
||||
@update:model-value="(v) => (typeof v === 'string' ? (nrcNo = v) : '')"
|
||||
:rules="[
|
||||
(val) =>
|
||||
(val && val.length === 13 && /[0-9]+/.test(val)) ||
|
||||
!val ||
|
||||
val.length === 13 ||
|
||||
$t('form.error.invalidCustomeMessage', {
|
||||
msg: $t('form.error.requireLength', { msg: 13 }),
|
||||
}),
|
||||
|
|
|
|||
794
src/components/04_flow-management/FormFlow.vue
Normal file
794
src/components/04_flow-management/FormFlow.vue
Normal file
|
|
@ -0,0 +1,794 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { moveItemUp, moveItemDown, deleteItem } from 'src/stores/utils';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import useUserStore from 'src/stores/user';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import { baseUrl } from 'stores/utils';
|
||||
import { getRole } from 'src/services/keycloak';
|
||||
import {
|
||||
WorkflowUserInTable,
|
||||
WorkflowTemplatePayload,
|
||||
WorkFlowPayloadStep,
|
||||
} from 'src/stores/workflow-template/types';
|
||||
import { User } from 'src/stores/user/types';
|
||||
|
||||
import SelectMenuWithSearch from '../shared/SelectMenuWithSearch.vue';
|
||||
import ToggleButton from 'src/components/button/ToggleButton.vue';
|
||||
import NoData from '../NoData.vue';
|
||||
import SelectBranch from '../shared/select/SelectBranch.vue';
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
onDrawer?: boolean;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const optionStore = useOptionStore();
|
||||
|
||||
const userInTable = defineModel<WorkflowUserInTable[]>('userInTable', {
|
||||
default: [],
|
||||
});
|
||||
const registerBranchId = defineModel('registerBranchId', { default: '' });
|
||||
const flowData = defineModel<WorkflowTemplatePayload>('flowData', {
|
||||
required: true,
|
||||
default: {
|
||||
status: 'CREATED',
|
||||
name: '',
|
||||
step: [],
|
||||
},
|
||||
});
|
||||
|
||||
const objectOptions = [
|
||||
...(optionStore.globalOption?.agenciesType || []),
|
||||
{ label: t('flow.customer'), value: 'customer' },
|
||||
{ label: t('flow.officer'), value: 'officer' },
|
||||
];
|
||||
const options = ref(objectOptions);
|
||||
const role = ref<string[]>([]);
|
||||
const userList = ref<User[]>([]);
|
||||
const responsiblePersonSearch = ref('');
|
||||
|
||||
async function getUserList(opts?: { query: string }) {
|
||||
const resUser = await userStore.fetchList({
|
||||
query: !!opts?.query ? opts.query : undefined,
|
||||
});
|
||||
if (resUser) userList.value = resUser.result;
|
||||
}
|
||||
|
||||
// async function getUserById(responsiblePersonId: string) {
|
||||
// const resUser = await userStore.fetchById(responsiblePersonId);
|
||||
// if (resUser) userInTable.value.push(resUser);
|
||||
// }
|
||||
|
||||
function selectResponsiblePerson(stepIndex: number, responsiblePerson: User) {
|
||||
const currStep = flowData.value.step[stepIndex];
|
||||
const existPersonIndex = currStep.responsiblePersonId?.findIndex(
|
||||
(p) => p === responsiblePerson.id,
|
||||
);
|
||||
|
||||
if (existPersonIndex === -1) {
|
||||
currStep.responsiblePersonId?.push(responsiblePerson.id);
|
||||
|
||||
if (!userInTable.value[stepIndex]) {
|
||||
userInTable.value[stepIndex] = {
|
||||
name: flowData.value.step[stepIndex].name,
|
||||
responsiblePerson: [],
|
||||
};
|
||||
}
|
||||
|
||||
userInTable.value[stepIndex]?.responsiblePerson.push({
|
||||
id: responsiblePerson.id,
|
||||
selectedImage: responsiblePerson.selectedImage,
|
||||
gender: responsiblePerson.gender,
|
||||
namePrefix: responsiblePerson.namePrefix,
|
||||
firstName: responsiblePerson.firstName,
|
||||
lastName: responsiblePerson.lastName,
|
||||
firstNameEN: responsiblePerson.firstNameEN,
|
||||
lastNameEN: responsiblePerson.lastNameEN,
|
||||
code: responsiblePerson.code,
|
||||
});
|
||||
} else {
|
||||
currStep.responsiblePersonId?.splice(Number(existPersonIndex), 1);
|
||||
userInTable.value[stepIndex]?.responsiblePerson.splice(
|
||||
Number(existPersonIndex),
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function selectItem(
|
||||
val: Record<string, unknown>,
|
||||
responsibleInstitution?: string[],
|
||||
) {
|
||||
if (!responsibleInstitution) return;
|
||||
const existIndex = responsibleInstitution.findIndex((p) => p === val.value);
|
||||
if (existIndex === -1) {
|
||||
responsibleInstitution.push(val.value as string);
|
||||
} else {
|
||||
responsibleInstitution.splice(Number(existIndex), 1);
|
||||
}
|
||||
}
|
||||
|
||||
function optionSearch(val: string | null) {
|
||||
if (val === '') {
|
||||
options.value = objectOptions;
|
||||
return;
|
||||
}
|
||||
|
||||
const needle = val ? val.toLowerCase() : '';
|
||||
options.value = objectOptions.filter(
|
||||
(v: { label: string }) => v.label.toLowerCase().indexOf(needle) > -1,
|
||||
);
|
||||
}
|
||||
|
||||
defineEmits<{
|
||||
(e: 'moveUp'): void;
|
||||
(e: 'moveDown'): void;
|
||||
(e: 'changeStatus'): void;
|
||||
(e: 'triggerProperties', step: WorkFlowPayloadStep): void;
|
||||
}>();
|
||||
|
||||
watch(
|
||||
responsiblePersonSearch,
|
||||
async () => await getUserList({ query: responsiblePersonSearch.value }),
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
role.value = getRole() || [];
|
||||
await getUserList();
|
||||
await userStore.fetchHqOption();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<section
|
||||
:id="`form-flow-template-${onDrawer ? 'drawer' : 'dialog'}`"
|
||||
class="col-12 q-pb-sm 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-cogs"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`general.name`, { msg: $t('flow.title') }) }}
|
||||
<span class="row q-ml-lg items-center text-weight-regular text-body2">
|
||||
<ToggleButton
|
||||
class="q-mr-sm"
|
||||
two-way
|
||||
:model-value="flowData.status !== 'INACTIVE'"
|
||||
@click="
|
||||
() => {
|
||||
onDrawer
|
||||
? $emit('changeStatus')
|
||||
: flowData.status !== 'INACTIVE'
|
||||
? (flowData.status = 'INACTIVE')
|
||||
: (flowData.status = 'CREATED');
|
||||
}
|
||||
"
|
||||
/>
|
||||
{{ $t('status.title') }}
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<section class="col-12 row q-col-gutter-sm">
|
||||
<SelectBranch
|
||||
v-if="role.includes('system')"
|
||||
:label="$t('branch.form.code')"
|
||||
:readonly
|
||||
code-only
|
||||
v-model:value="registerBranchId"
|
||||
class="col-md-3 col-12"
|
||||
required
|
||||
/>
|
||||
<q-input
|
||||
:readonly
|
||||
bg-color="transparent"
|
||||
outlined
|
||||
dense
|
||||
class="col"
|
||||
id="input-flow-template-name"
|
||||
v-model="flowData.name"
|
||||
hide-bottom-space
|
||||
:label="$t(`general.name`, { msg: $t('flow.step') })"
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- SEC: Step -->
|
||||
<section
|
||||
:id="`form-flow-step-${onDrawer ? 'drawer' : 'dialog'}`"
|
||||
class="col-12 q-pb-sm q-pt-xl 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-note-edit-outline"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`flow.processStep`) }}
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="flowData.step.length === 0"
|
||||
class="col-12 surface-2 rounded bordered column items-center justify-center q-pa-xl"
|
||||
>
|
||||
<NoData class="col" />
|
||||
</section>
|
||||
|
||||
<section v-else class="col-12 q-gutter-y-md">
|
||||
<template v-for="(step, index) in flowData.step" :key="index">
|
||||
<div class="bordered rounded">
|
||||
<q-expansion-item
|
||||
for="item-up"
|
||||
id="item-up"
|
||||
dense
|
||||
switch-toggle-side
|
||||
default-opened
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="expansion-rounded surface-2"
|
||||
header-style="border-top-left-radius: var(--radius-2); border-top-right-radius: var(--radius-2)"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<div class="column full-width" @keyup.stop @click.stop>
|
||||
<div class="row items-center q-py-sm full-width">
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-work-up-product"
|
||||
for="btn-work-up-product"
|
||||
icon="mdi-arrow-up"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
:disable="index === 0"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
@click.stop="
|
||||
moveItemUp(flowData.step, index);
|
||||
moveItemUp(userInTable, index);
|
||||
"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-work-down-product"
|
||||
for="btn-work-down-product"
|
||||
icon="mdi-arrow-down"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
class="q-mx-sm"
|
||||
:disable="index === flowData.step.length - 1"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
@click.stop="
|
||||
moveItemDown(flowData.step, index);
|
||||
moveItemDown(userInTable, index);
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
:prefix="`${$t('flow.stepNo')} ${index + 1}: `"
|
||||
dense
|
||||
outlined
|
||||
:readonly
|
||||
:id="`input-flow-step-name-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
|
||||
:for="`input-flow-step-name-${index}-${onDrawer ? 'drawer' : 'dialog'}`"
|
||||
class="col q-ml-md"
|
||||
:placeholder="$t('general.no', { msg: $t('flow.step') })"
|
||||
v-model="step.name"
|
||||
hide-bottom-space
|
||||
:rules="[
|
||||
(val: string) => !!val || $t('form.error.required'),
|
||||
]"
|
||||
/>
|
||||
<!-- <div
|
||||
:for="`select-work-name-${index + 1}`"
|
||||
class="col q-py-sm q-px-md"
|
||||
style="background-color: var(--surface-1); z-index: 2"
|
||||
@click="() => (readonly ? '' : fetchListOfWork())"
|
||||
>
|
||||
<span class="text-body2" style="color: var(--foreground)">
|
||||
{{ $t('productService.service.work') }} {{ index + 1 }} :
|
||||
<span class="app-text-muted-2">
|
||||
{{
|
||||
workName
|
||||
? workName
|
||||
: $t('productService.service.workName')
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
<q-menu
|
||||
v-if="!readonly"
|
||||
fit
|
||||
ref="refMenu"
|
||||
self="top left"
|
||||
anchor="bottom left"
|
||||
>
|
||||
<q-item>
|
||||
<div
|
||||
class="full-width flex items-center justify-between"
|
||||
>
|
||||
{{ $t('productService.service.workName') }}
|
||||
<q-btn
|
||||
dense
|
||||
unelevated
|
||||
class="bordered q-px-sm text-capitalize"
|
||||
style="
|
||||
border-radius: var(--radius-2);
|
||||
color: hsl(var(--info-bg));
|
||||
"
|
||||
@click.stop="
|
||||
() => {
|
||||
refMenu.hide();
|
||||
$emit('manageWorkName');
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-icon name="mdi-cog" size="xs" class="q-mr-sm" />
|
||||
{{ $t('general.manage') }}
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-item>
|
||||
<q-item
|
||||
@click="workName = item.name"
|
||||
clickable
|
||||
v-for="(item, index) in workNameItems"
|
||||
:key="index"
|
||||
>
|
||||
<div class="full-width flex items-center">
|
||||
<q-icon
|
||||
v-if="workName === item.name"
|
||||
name="mdi-checkbox-marked"
|
||||
size="xs"
|
||||
color="primary"
|
||||
class="q-mr-sm"
|
||||
/>
|
||||
<q-icon
|
||||
v-else
|
||||
name="mdi-checkbox-blank-outline"
|
||||
size="xs"
|
||||
style="color: hsl(var(--text-mute))"
|
||||
class="q-mr-sm"
|
||||
/>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</q-item>
|
||||
</q-menu>
|
||||
</div> -->
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-delete-work"
|
||||
icon="mdi-trash-can-outline"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
padding="0"
|
||||
class="q-ml-md"
|
||||
color="negative"
|
||||
@click="deleteItem(flowData.step, index)"
|
||||
>
|
||||
<q-tooltip>{{ $t('general.delete') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<section class="q-px-md surface-2 q-py-sm">
|
||||
<div
|
||||
:class="{ 'surface-1 rounded bordered': readonly }"
|
||||
style="border: 1px solid transparent"
|
||||
>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<q-input
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
:readonly
|
||||
class="col-md-6 col-12"
|
||||
type="textarea"
|
||||
dense
|
||||
outlined
|
||||
:label="$t('general.detail')"
|
||||
:model-value="!readonly ? step.detail : step.detail || '-'"
|
||||
@update:model-value="
|
||||
(v) => (step.detail = v?.toString() || '')
|
||||
"
|
||||
/>
|
||||
|
||||
<div class="col-md-6 col-12">
|
||||
<div
|
||||
class="surface-1 rounded bordered full-height"
|
||||
style="padding-inline: 12px"
|
||||
:style="readonly ? 'border: 1px solid transparent;' : ''"
|
||||
>
|
||||
<section
|
||||
class="row items-center q-pt-xs justify-between relative-position"
|
||||
>
|
||||
<div
|
||||
class="app-text-muted-2"
|
||||
:style="
|
||||
step.attributes?.properties &&
|
||||
step.attributes?.properties.length > 0
|
||||
? 'font-size: 10px'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ $t('general.properties') }}
|
||||
</div>
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-add-work-product"
|
||||
class="text-capitalize rounded absolute-top-right"
|
||||
flat
|
||||
dense
|
||||
padding="4px 8px"
|
||||
style="color: hsl(var(--info-bg)); top: 4px"
|
||||
@click.stop="$emit('triggerProperties', step)"
|
||||
>
|
||||
<Icon
|
||||
icon="basil:settings-adjust-solid"
|
||||
width="20.08px"
|
||||
style="color: hsl(var(--info-bg))"
|
||||
/>
|
||||
</q-btn>
|
||||
</section>
|
||||
<section class="row q-gutter-sm q-pb-sm scroll">
|
||||
<span
|
||||
v-for="(att, i) in step.attributes?.properties"
|
||||
:key="i"
|
||||
class="surface-2 bordered rounded q-px-xs"
|
||||
>
|
||||
{{ optionStore.mapOption(att.fieldName ?? '') }}
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RESPONSIBLE-PERSON -->
|
||||
<q-select
|
||||
v-if="step.responsiblePersonId"
|
||||
behavior="menu"
|
||||
: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
|
||||
:label="$t('flow.responsiblePerson')"
|
||||
class="col-md-6 col-12"
|
||||
:hide-dropdown-icon="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)"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:option></template>
|
||||
<q-menu v-if="!readonly" :offset="[0, 4]">
|
||||
<q-list>
|
||||
<q-item>
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="col responsible-search"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="responsiblePersonSearch"
|
||||
debounce="200"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item>
|
||||
<span class="text-caption app-text-muted-2 q-px-md">
|
||||
{{ $t('general.people') }}
|
||||
</span>
|
||||
|
||||
<q-item
|
||||
v-if="userList.length === 0"
|
||||
class="app-text-muted q-px-lg"
|
||||
>
|
||||
{{ $t('general.noData') }}
|
||||
</q-item>
|
||||
<q-item
|
||||
v-for="(person, i) in userList"
|
||||
dense
|
||||
:key="i"
|
||||
clickable
|
||||
class="column"
|
||||
@click.stop="selectResponsiblePerson(index, person)"
|
||||
>
|
||||
<div class="row items-center no-wrap">
|
||||
<q-checkbox
|
||||
size="xs"
|
||||
:model-value="
|
||||
step.responsiblePersonId.includes(person.id)
|
||||
"
|
||||
@click.stop="
|
||||
selectResponsiblePerson(index, person)
|
||||
"
|
||||
/>
|
||||
<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">
|
||||
<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>
|
||||
</q-item>
|
||||
|
||||
<span class="text-caption app-text-muted-2 q-px-md">
|
||||
{{ $t('personnel.MESSENGER') }}
|
||||
</span>
|
||||
<q-item
|
||||
clickable
|
||||
@click="step.messengerByArea = !step.messengerByArea"
|
||||
class="column"
|
||||
>
|
||||
<div class="row items-center">
|
||||
<q-checkbox
|
||||
v-model="step.messengerByArea"
|
||||
size="xs"
|
||||
></q-checkbox>
|
||||
<div class="column q-pl-md">
|
||||
<span>{{ $t('general.byArea') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-select>
|
||||
|
||||
<!-- RESPONSIBLE-AGENCIES, RESPONSIBLE-INSTITUTION -->
|
||||
<q-select
|
||||
behavior="menu"
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
:readonly
|
||||
outlined
|
||||
dense
|
||||
v-model="step.responsibleInstitution"
|
||||
multiple
|
||||
:options="options"
|
||||
hide-bottom-space
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
:label="
|
||||
$t('general.select', { msg: $t('general.agencies') })
|
||||
"
|
||||
class="col-md-6 col-12"
|
||||
:hide-dropdown-icon="readonly"
|
||||
>
|
||||
<template v-slot:selected-item="scope">
|
||||
<q-chip
|
||||
dense
|
||||
:removable="!readonly"
|
||||
@remove="scope.removeAtIndex(scope.index)"
|
||||
>
|
||||
<span class="text-caption">
|
||||
{{
|
||||
scope.opt === 'customer'
|
||||
? $t('flow.customer')
|
||||
: scope.opt === 'officer'
|
||||
? $t('flow.officer')
|
||||
: optionStore.mapOption(
|
||||
scope.opt,
|
||||
'agenciesType',
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</q-chip>
|
||||
</template>
|
||||
<template v-slot:option></template>
|
||||
|
||||
<SelectMenuWithSearch
|
||||
v-if="!readonly"
|
||||
:title="
|
||||
$t('general.select', { msg: $t('general.agencies') })
|
||||
"
|
||||
:option="options"
|
||||
:separator-index="[9]"
|
||||
width="353.66px"
|
||||
@search="(v) => optionSearch(v as string)"
|
||||
@select="
|
||||
(v) => selectItem(v, step.responsibleInstitution)
|
||||
"
|
||||
@before-show="
|
||||
() => {
|
||||
objectOptions = [
|
||||
...(optionStore.globalOption?.agenciesType || []),
|
||||
{ label: t('flow.customer'), value: 'customer' },
|
||||
{ label: t('flow.officer'), value: 'officer' },
|
||||
];
|
||||
options = objectOptions;
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-separator></q-separator>
|
||||
</template>
|
||||
<template
|
||||
#option="{ opt }"
|
||||
v-if="step.responsibleInstitution"
|
||||
>
|
||||
<q-checkbox
|
||||
:model-value="
|
||||
step.responsibleInstitution.some(
|
||||
(v: string) => v === opt.value,
|
||||
)
|
||||
"
|
||||
class="q-pr-sm"
|
||||
size="xs"
|
||||
@click="selectItem(opt, step.responsibleInstitution)"
|
||||
/>
|
||||
<span
|
||||
:class="{
|
||||
'app-text-info': step.responsibleInstitution.some(
|
||||
(v: string) => v === opt.value,
|
||||
),
|
||||
}"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</span>
|
||||
</template>
|
||||
</SelectMenuWithSearch>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</q-expansion-item>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.responsible-search .q-field__control) {
|
||||
height: 36px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(
|
||||
.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;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
:deep(i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon) {
|
||||
color: hsl(var(--text-mute));
|
||||
}
|
||||
|
||||
:deep(
|
||||
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
|
||||
) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
:deep(.q-dialog.fullscreen.no-pointer-events.q-dialog--modal) {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { QSelect } from 'quasar';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import { createEditorImageDrop } from 'src/utils/ui';
|
||||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
|
@ -14,6 +15,7 @@ const process = defineModel<number>('process');
|
|||
const name = defineModel<string>('name');
|
||||
const code = defineModel<string>('code');
|
||||
const expenseType = defineModel<string>('expenseType');
|
||||
const shared = defineModel<boolean>('shared');
|
||||
|
||||
const codeOption = ref<{ id: string; name: string }[]>([]);
|
||||
|
||||
|
|
@ -66,11 +68,13 @@ watch(
|
|||
);
|
||||
},
|
||||
);
|
||||
|
||||
const detailEditorImageDrop = createEditorImageDrop(detail);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<div class="col-12 q-pb-sm text-weight-bold text-body1 row items-center">
|
||||
<div class="col-12 q-pb-sm row items-center">
|
||||
<q-icon
|
||||
flat
|
||||
size="xs"
|
||||
|
|
@ -79,7 +83,21 @@ watch(
|
|||
name="mdi-office-building-outline"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`form.field.basicInformation`) }}
|
||||
<span class="text-body1 text-weight-bold">
|
||||
{{ $t(`form.field.basicInformation`) }}
|
||||
</span>
|
||||
<section class="q-px-md">
|
||||
<label :style="{ opacity: readonly ? '.5' : undefined }">
|
||||
<input
|
||||
id="input-shared"
|
||||
class="q-pr-sm"
|
||||
type="checkbox"
|
||||
v-model="shared"
|
||||
:disabled="readonly"
|
||||
/>
|
||||
{{ $t('general.shared') }}
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
|
|
@ -92,6 +110,7 @@ watch(
|
|||
hide-selected
|
||||
hide-bottom-space
|
||||
input-debounce="0"
|
||||
autocomplete="off"
|
||||
:disable="!readonly && disableCode"
|
||||
class="col-md-3 col-12"
|
||||
v-model="code"
|
||||
|
|
@ -101,7 +120,11 @@ watch(
|
|||
option-value="value"
|
||||
:dense="dense"
|
||||
:readonly="readonly"
|
||||
:options="codeOptions"
|
||||
:options="
|
||||
codeOptions.sort((a, b) => {
|
||||
return String(a.value ?? '').localeCompare(String(b.value ?? ''));
|
||||
})
|
||||
"
|
||||
:label="$t('productService.product.code')"
|
||||
:hide-dropdown-icon="readonly || disableCode"
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
|
|
@ -141,6 +164,7 @@ watch(
|
|||
:label="$t('productService.product.processingTime')"
|
||||
v-model="process"
|
||||
type="number"
|
||||
min="0"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon
|
||||
|
|
@ -160,6 +184,7 @@ watch(
|
|||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
autocomplete="off"
|
||||
input-debounce="0"
|
||||
class="col-md-3 col-6"
|
||||
v-model="expenseType"
|
||||
|
|
@ -202,6 +227,7 @@ watch(
|
|||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (detail = v) : '')
|
||||
"
|
||||
@drop="detailEditorImageDrop"
|
||||
min-height="5rem"
|
||||
class="q-mt-sm q-mb-xs"
|
||||
:flat="!readonly"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { QSelect } from 'quasar';
|
||||
import { getRole } from 'src/services/keycloak';
|
||||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import { watch } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { createEditorImageDrop } from 'src/utils/ui';
|
||||
import SelectBranch from '../shared/select/SelectBranch.vue';
|
||||
|
||||
const remark = defineModel<string>('remark');
|
||||
const detail = defineModel<string>('detail');
|
||||
const name = defineModel<string>('name');
|
||||
const shared = defineModel<boolean>('shared');
|
||||
const code = defineModel<string>('code');
|
||||
|
||||
const serviceCode = defineModel<string>('serviceCode');
|
||||
|
|
@ -15,100 +13,58 @@ const serviceName = defineModel<string>('serviceNameTh');
|
|||
const serviceDescription = defineModel<string>('serviceDescription');
|
||||
const registeredBranchId = defineModel<string>('registeredBranchId');
|
||||
|
||||
const optionsBranch = defineModel<{ id: string; name: string }[]>(
|
||||
'optionsBranch',
|
||||
{ default: [] },
|
||||
);
|
||||
|
||||
defineProps<{
|
||||
dense?: boolean;
|
||||
outlined?: boolean;
|
||||
readonly?: boolean;
|
||||
readOnlybranchOption?: boolean;
|
||||
branchReadonly?: boolean;
|
||||
separator?: boolean;
|
||||
isType?: boolean;
|
||||
disableCode?: boolean;
|
||||
service?: boolean;
|
||||
}>();
|
||||
|
||||
const branchOptions = ref<Record<string, unknown>[]>([]);
|
||||
let branchFilter = selectFilterOptionRefMod(
|
||||
optionsBranch,
|
||||
branchOptions,
|
||||
'name',
|
||||
);
|
||||
|
||||
watch(
|
||||
() => optionsBranch.value,
|
||||
() => {
|
||||
branchFilter = selectFilterOptionRefMod(
|
||||
optionsBranch,
|
||||
branchOptions,
|
||||
'name',
|
||||
);
|
||||
},
|
||||
);
|
||||
const detailEditorImageDrop = createEditorImageDrop(detail);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<div class="col-12 q-pb-sm text-weight-bold text-body1">
|
||||
<div class="col-12 q-pb-sm row items-center">
|
||||
<q-icon
|
||||
flat
|
||||
size="xs"
|
||||
class="q-pa-sm rounded q-mr-xs"
|
||||
class="q-pa-sm rounded q-mr-sm"
|
||||
color="info"
|
||||
name="mdi-office-building-outline"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
{{ $t(`form.field.basicInformation`) }}
|
||||
<span class="text-body1 text-weight-bold">
|
||||
{{ $t(`form.field.basicInformation`) }}
|
||||
</span>
|
||||
<section v-if="!service" class="q-px-md">
|
||||
<label :style="{ opacity: readonly ? '.5' : undefined }">
|
||||
<input
|
||||
id="input-shared"
|
||||
class="q-pr-sm"
|
||||
type="checkbox"
|
||||
v-model="shared"
|
||||
:disabled="readonly"
|
||||
/>
|
||||
{{ $t('general.shared') }}
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div v-if="!service" class="col-12 row q-col-gutter-sm">
|
||||
<q-select
|
||||
outlined
|
||||
use-input
|
||||
fill-input
|
||||
emit-value
|
||||
map-options
|
||||
hide-selected
|
||||
hide-bottom-space
|
||||
<SelectBranch
|
||||
:label="$t('productService.product.registeredBranch')"
|
||||
class="col-md-12 col-12"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
v-model="registeredBranchId"
|
||||
id="input-source-nationality"
|
||||
for="input-source-nationality"
|
||||
:disable="!readonly && readOnlybranchOption"
|
||||
:dense="dense"
|
||||
:readonly="readonly || readOnlybranchOption"
|
||||
:options="branchOptions"
|
||||
:hide-dropdown-icon="readonly || readOnlybranchOption"
|
||||
:label="$t('productService.service.registeredBranch')"
|
||||
:rules="[
|
||||
(val) => {
|
||||
const roles = getRole() || [];
|
||||
const isSpecialRole = ['admin', 'system', 'head_of_admin'].some(
|
||||
(role) => roles.includes(role),
|
||||
);
|
||||
return (
|
||||
isSpecialRole ||
|
||||
!!val ||
|
||||
$t('form.error.selectField', {
|
||||
field: $t('productService.service.registeredBranch'),
|
||||
})
|
||||
);
|
||||
},
|
||||
]"
|
||||
@filter="branchFilter"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
v-model:value="registeredBranchId"
|
||||
:readonly
|
||||
:disabled="!readonly && branchReadonly"
|
||||
clearable
|
||||
required
|
||||
/>
|
||||
<q-input
|
||||
for="input-code"
|
||||
:dense="dense"
|
||||
|
|
@ -135,18 +91,40 @@ watch(
|
|||
v-model="name"
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
/>
|
||||
<q-input
|
||||
for="input-detail"
|
||||
:dense="dense"
|
||||
<q-field
|
||||
class="full-width"
|
||||
outlined
|
||||
for="input-detail"
|
||||
id="input-detail"
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
type="textarea"
|
||||
class="col-12"
|
||||
:borderless="readonly"
|
||||
:label="$t('general.detail')"
|
||||
:model-value="readonly ? detail || '-' : detail"
|
||||
@update:model-value="(v) => (typeof v === 'string' ? (detail = v) : '')"
|
||||
/>
|
||||
stack-label
|
||||
dense
|
||||
>
|
||||
<q-editor
|
||||
dense
|
||||
:model-value="readonly ? detail || '-' : detail || ''"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (detail = v) : '')
|
||||
"
|
||||
@drop="detailEditorImageDrop"
|
||||
min-height="5rem"
|
||||
class="q-mt-sm q-mb-xs"
|
||||
:flat="!readonly"
|
||||
:readonly="readonly"
|
||||
:toolbar-color="
|
||||
readonly ? 'disabled' : $q.dark.isActive ? 'white' : ''
|
||||
"
|
||||
:toolbar-toggle-color="readonly ? 'disabled' : 'primary'"
|
||||
style="
|
||||
cursor: auto;
|
||||
color: var(--foreground);
|
||||
border-color: var(--surface-3);
|
||||
"
|
||||
:style="`width: ${$q.screen.gt.xs ? '100%' : '63vw'}`"
|
||||
/>
|
||||
</q-field>
|
||||
<q-input
|
||||
:dense="dense"
|
||||
outlined
|
||||
|
|
@ -189,21 +167,42 @@ watch(
|
|||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
id="input-service-description"
|
||||
for="input-service-description"
|
||||
:dense="dense"
|
||||
<q-field
|
||||
class="full-width"
|
||||
outlined
|
||||
for="input-service-description"
|
||||
id="input-service-description"
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
type="textarea"
|
||||
class="col-12"
|
||||
:borderless="readonly"
|
||||
:label="$t('general.detail')"
|
||||
:model-value="readonly ? serviceDescription || '-' : serviceDescription"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (serviceDescription = v) : '')
|
||||
"
|
||||
/>
|
||||
stack-label
|
||||
dense
|
||||
>
|
||||
<q-editor
|
||||
dense
|
||||
:model-value="
|
||||
readonly ? serviceDescription || '-' : serviceDescription || ''
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (serviceDescription = v) : '')
|
||||
"
|
||||
@drop="detailEditorImageDrop"
|
||||
min-height="5rem"
|
||||
class="q-mt-sm q-mb-xs"
|
||||
:flat="!readonly"
|
||||
:readonly="readonly"
|
||||
:toolbar-color="
|
||||
readonly ? 'disabled' : $q.dark.isActive ? 'white' : ''
|
||||
"
|
||||
:toolbar-toggle-color="readonly ? 'disabled' : 'primary'"
|
||||
style="
|
||||
cursor: auto;
|
||||
color: var(--foreground);
|
||||
border-color: var(--surface-3);
|
||||
"
|
||||
:style="`width: ${$q.screen.gt.xs ? '100%' : '63vw'}`"
|
||||
/>
|
||||
</q-field>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
192
src/components/04_product-service/FormDocument.vue
Normal file
192
src/components/04_product-service/FormDocument.vue
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import SelectMenuWithSearch from '../shared/SelectMenuWithSearch.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
const attachment = defineModel<string[]>('attachment', { default: [] });
|
||||
|
||||
const mapName = (val: string): string => {
|
||||
const name = objectOptions.find((v) => v.value === val);
|
||||
return name ? name.label : '';
|
||||
};
|
||||
|
||||
const objectOptions = [
|
||||
{
|
||||
label: 'customerEmployee.fileType.passport',
|
||||
value: 'passport',
|
||||
},
|
||||
{
|
||||
label: 'customerEmployee.fileType.visa',
|
||||
value: 'visa',
|
||||
},
|
||||
{
|
||||
label: 'customerEmployee.fileType.tm6',
|
||||
value: 'tm6',
|
||||
},
|
||||
{
|
||||
label: 'customerEmployee.fileType.workPermit',
|
||||
value: 'workPermit',
|
||||
},
|
||||
{
|
||||
label: 'customerEmployee.fileType.noticeJobEmployment',
|
||||
value: 'noticeJobEmployment',
|
||||
},
|
||||
{
|
||||
label: 'customerEmployee.fileType.noticeJobEntry',
|
||||
value: 'noticeJobEntry',
|
||||
},
|
||||
{
|
||||
label: 'customerEmployee.fileType.historyJob',
|
||||
value: 'historyJob',
|
||||
},
|
||||
{
|
||||
label: 'customerEmployee.fileType.acceptJob',
|
||||
value: 'acceptJob',
|
||||
},
|
||||
];
|
||||
|
||||
const options = ref(objectOptions);
|
||||
|
||||
function selectAttachment(val: Record<string, unknown>) {
|
||||
const existIndex = attachment.value.findIndex((p) => p === val.value);
|
||||
|
||||
if (existIndex === -1) {
|
||||
attachment.value.push(val.value as string);
|
||||
} else {
|
||||
attachment.value.splice(Number(existIndex), 1);
|
||||
}
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
if (attachment.value.length === options.value.length) {
|
||||
attachment.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
options.value.forEach((opt) => {
|
||||
const existItem = attachment.value.some((v) => v === opt.value);
|
||||
|
||||
if (!existItem) selectAttachment(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function optionSearch(val: string | null) {
|
||||
if (val === '') {
|
||||
options.value = objectOptions;
|
||||
return;
|
||||
}
|
||||
|
||||
const needle = val ? val.toLowerCase() : '';
|
||||
options.value = objectOptions.filter(
|
||||
(v) => t(v.label).toLowerCase().indexOf(needle) > -1,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<div class="col-12 q-pb-sm row items-center">
|
||||
<q-icon
|
||||
flat
|
||||
size="xs"
|
||||
class="q-pa-sm rounded q-mr-sm"
|
||||
color="info"
|
||||
name="mdi-file-outline"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
<span class="text-body1 text-weight-bold">
|
||||
{{ $t('general.information', { msg: $t('general.attachment') }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<q-select
|
||||
:readonly
|
||||
outlined
|
||||
dense
|
||||
v-model="attachment"
|
||||
multiple
|
||||
:options="options"
|
||||
use-chips
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
:label="$t('general.select', { msg: $t('general.attachment') })"
|
||||
class="col"
|
||||
:hide-dropdown-icon="readonly"
|
||||
>
|
||||
<template v-slot:selected-item="scope">
|
||||
<q-chip
|
||||
:removable="!readonly"
|
||||
@remove="scope.removeAtIndex(scope.index)"
|
||||
>
|
||||
{{ $t(mapName(scope.opt)) }}
|
||||
</q-chip>
|
||||
</template>
|
||||
<template v-slot:option></template>
|
||||
|
||||
<SelectMenuWithSearch
|
||||
v-if="!readonly"
|
||||
:title="$t('general.select', { msg: $t('general.attachment') })"
|
||||
:option="options"
|
||||
width="353.66px"
|
||||
@search="(v) => optionSearch(v as string)"
|
||||
@select="(v) => selectAttachment(v)"
|
||||
>
|
||||
<template #prepend>
|
||||
<!-- <q-item
|
||||
dense
|
||||
clickable
|
||||
class="bordered-t flex items-center app-text-muted"
|
||||
style="padding: 0px 16px"
|
||||
>
|
||||
<q-icon name="mdi-plus" class="q-pa-sm q-mr-sm" />
|
||||
{{ $t('general.add', { text: $t('general.attachment') }) }}
|
||||
</q-item> -->
|
||||
<q-item
|
||||
dense
|
||||
clickable
|
||||
class="flex items-center"
|
||||
style="
|
||||
padding: 0px 16px;
|
||||
background-color: hsla(var(--info-bg) / 0.1);
|
||||
"
|
||||
@click="selectAll()"
|
||||
>
|
||||
<q-checkbox
|
||||
:model-value="
|
||||
options.length > 0 && attachment.length === options.length
|
||||
"
|
||||
class="q-pr-sm"
|
||||
size="xs"
|
||||
@click="selectAll()"
|
||||
/>
|
||||
{{ $t('general.selectAll') }}
|
||||
</q-item>
|
||||
</template>
|
||||
<template #option="{ opt }">
|
||||
<q-checkbox
|
||||
:model-value="attachment.some((v) => v === opt.value)"
|
||||
class="q-pr-sm"
|
||||
size="xs"
|
||||
@click="selectAttachment(opt)"
|
||||
/>
|
||||
<span
|
||||
:class="{
|
||||
'app-text-info': attachment.some((v) => v === opt.value),
|
||||
}"
|
||||
>
|
||||
{{ $t(opt.label as string) }}
|
||||
</span>
|
||||
</template>
|
||||
</SelectMenuWithSearch>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style></style>
|
||||
|
|
@ -1,17 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { moveItemUp, moveItemDown, deleteItem, dialog } from 'stores/utils';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { WorkflowTemplate } from 'src/stores/workflow-template/types';
|
||||
import { ServiceCreate, WorkItems } from 'stores/product-service/types';
|
||||
|
||||
import NoData from 'components/NoData.vue';
|
||||
import WorkManagementComponent from './WorkManagementComponent.vue';
|
||||
import AddButton from '../button/AddButton.vue';
|
||||
import { ServiceCreate, WorkItems } from 'stores/product-service/types';
|
||||
import TreeView from '../shared/TreeView.vue';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const workItems = defineModel<WorkItems[]>('workItems', { default: [] });
|
||||
const workflow = defineModel<WorkflowTemplate>('workflow');
|
||||
|
||||
const props = defineProps<{
|
||||
service?: ServiceCreate;
|
||||
|
|
@ -20,6 +23,7 @@ const props = defineProps<{
|
|||
readonly?: boolean;
|
||||
separator?: boolean;
|
||||
treeView?: boolean;
|
||||
installments?: number;
|
||||
|
||||
priceDisplay?: {
|
||||
price: boolean;
|
||||
|
|
@ -74,7 +78,22 @@ async function addWork() {
|
|||
workItems.value.push({
|
||||
id: '',
|
||||
name: '',
|
||||
attributes: { additional: [], showTotalPrice: false },
|
||||
attributes: {
|
||||
workflowStep: workflow.value?.step
|
||||
? JSON.parse(
|
||||
JSON.stringify(
|
||||
workflow.value.step.map((step) => ({
|
||||
name: step.name,
|
||||
attributes: step.attributes,
|
||||
productsId: [],
|
||||
})),
|
||||
),
|
||||
)
|
||||
: [],
|
||||
additional: [],
|
||||
showTotalPrice: false,
|
||||
workflowId: workflow.value ? workflow.value.id : '',
|
||||
},
|
||||
product: [],
|
||||
});
|
||||
await nextTick();
|
||||
|
|
@ -170,15 +189,24 @@ watch(
|
|||
:key="work.id"
|
||||
:index="index"
|
||||
:length="workItems.length"
|
||||
:workIndex="index"
|
||||
:readonly="readonly"
|
||||
:priceDisplay="priceDisplay"
|
||||
:work-index="index"
|
||||
:readonly
|
||||
:price-display
|
||||
:installments
|
||||
v-model:work-name="workItems[index].name"
|
||||
v-model:product-items="work.product"
|
||||
v-model:attributes="work.attributes"
|
||||
@add-product="$emit('addProduct', index)"
|
||||
@move-work-up="moveItemUp(workItems, index)"
|
||||
@move-work-down="moveItemDown(workItems, index)"
|
||||
@move-work-up="
|
||||
() => {
|
||||
moveItemUp(workItems, index);
|
||||
}
|
||||
"
|
||||
@move-work-down="
|
||||
() => {
|
||||
moveItemDown(workItems, index);
|
||||
}
|
||||
"
|
||||
@delete-work="confirmDelete(workItems, index)"
|
||||
@move-product-up="moveItemUp"
|
||||
@move-product-down="moveItemDown"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { commaInput } from 'stores/utils';
|
||||
import { formatNumberDecimal, commaInput } from 'stores/utils';
|
||||
import { QTableProps } from 'quasar';
|
||||
import { calculatePrice } from 'src/utils/arithmetic';
|
||||
|
||||
const serviceCharge = defineModel<number>('serviceCharge');
|
||||
const agentPrice = defineModel<number>('agentPrice');
|
||||
|
|
@ -8,9 +10,67 @@ const price = defineModel<number>('price');
|
|||
const vatIncluded = defineModel<boolean>('vatIncluded');
|
||||
const calcVat = defineModel<boolean>('calcVat');
|
||||
|
||||
const price4Show = ref('');
|
||||
const agentPrice4Show = ref('');
|
||||
const serviceCharge4Show = ref('');
|
||||
const price4Show = ref<string>(commaInput(price.value?.toString() || '0'));
|
||||
const agentPrice4Show = ref<string>(
|
||||
commaInput(agentPrice.value?.toString() || '0'),
|
||||
);
|
||||
const serviceCharge4Show = ref<string>(
|
||||
commaInput(serviceCharge.value?.toString() || '0'),
|
||||
);
|
||||
|
||||
const column = [
|
||||
{
|
||||
name: 'label',
|
||||
align: 'center',
|
||||
label: 'productService.product.priceInformation',
|
||||
field: 'label',
|
||||
},
|
||||
{
|
||||
name: 'pricePerUnit',
|
||||
align: 'center',
|
||||
label: 'quotation.pricePerUnit',
|
||||
field: 'pricePerUnit',
|
||||
},
|
||||
{
|
||||
name: 'beforeVat',
|
||||
align: 'right',
|
||||
label: 'quotation.priceBeforeVat',
|
||||
field: 'beforeVat',
|
||||
},
|
||||
{
|
||||
name: 'vat',
|
||||
align: 'right',
|
||||
label: 'general.vat',
|
||||
field: 'vat',
|
||||
},
|
||||
{
|
||||
name: 'total',
|
||||
align: 'right',
|
||||
label: 'quotation.sumPrice',
|
||||
field: 'total',
|
||||
},
|
||||
] as const satisfies QTableProps['columns'];
|
||||
|
||||
const row = [
|
||||
{
|
||||
label: 'productService.product.salePrice',
|
||||
beforeVat: 0,
|
||||
vat: 0,
|
||||
total: 0,
|
||||
},
|
||||
{
|
||||
label: 'productService.product.agentPrice',
|
||||
beforeVat: 0,
|
||||
vat: 0,
|
||||
total: 0,
|
||||
},
|
||||
{
|
||||
label: 'productService.product.processingPrice',
|
||||
beforeVat: 0,
|
||||
vat: 0,
|
||||
total: 0,
|
||||
},
|
||||
] as const satisfies QTableProps['rows'];
|
||||
|
||||
watch(calcVat, () => {
|
||||
if (calcVat.value === false) vatIncluded.value = false;
|
||||
|
|
@ -101,81 +161,211 @@ withDefaults(
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<q-input
|
||||
id="input-price"
|
||||
for="input-price"
|
||||
v-if="priceDisplay?.price"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
:borderless="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('productService.product.salePrice')"
|
||||
:model-value="commaInput(price?.toString() || '0')"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (typeof v === 'string') price4Show = commaInput(v);
|
||||
const x = parseFloat(
|
||||
price4Show && typeof price4Show === 'string'
|
||||
? price4Show.replace(/,/g, '')
|
||||
: '',
|
||||
);
|
||||
price = x;
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
id="input-agent-price"
|
||||
for="input-agent-price"
|
||||
v-if="priceDisplay?.agentPrice"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
:borderless="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('productService.product.agentPrice')"
|
||||
:model-value="commaInput(agentPrice?.toString() || '0')"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (typeof v === 'string') agentPrice4Show = commaInput(v);
|
||||
const x = parseFloat(
|
||||
agentPrice4Show && typeof agentPrice4Show === 'string'
|
||||
? agentPrice4Show.replace(/,/g, '')
|
||||
: '',
|
||||
);
|
||||
agentPrice = x;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div class="col-12">
|
||||
<q-table
|
||||
:columns="column"
|
||||
:rows="row"
|
||||
:rows-per-page-options="[0]"
|
||||
bordered
|
||||
flat
|
||||
hide-pagination
|
||||
class="full-width"
|
||||
: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-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label && $t(col.label) }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<q-input
|
||||
id="input-service-charge"
|
||||
for="input-service-charge"
|
||||
v-if="priceDisplay?.serviceCharge"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
:borderless="readonly"
|
||||
hide-bottom-space
|
||||
class="col-4"
|
||||
:label="$t('productService.product.processingPrice')"
|
||||
:model-value="commaInput(serviceCharge?.toString() || '0')"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (typeof v === 'string') serviceCharge4Show = commaInput(v);
|
||||
const x = parseFloat(
|
||||
serviceCharge4Show && typeof serviceCharge4Show === 'string'
|
||||
? serviceCharge4Show.replace(/,/g, '')
|
||||
: '',
|
||||
);
|
||||
serviceCharge = x;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<template v-slot:body="props">
|
||||
<q-tr>
|
||||
<q-td>{{ $t(props.row.label) }}</q-td>
|
||||
<q-td class="text-right" style="width: 15%">
|
||||
<q-input
|
||||
v-if="priceDisplay?.price && props.rowIndex === 0"
|
||||
id="input-price"
|
||||
for="input-price"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
:borderless="readonly"
|
||||
hide-bottom-space
|
||||
input-class="text-right"
|
||||
:model-value="price4Show"
|
||||
@blur="
|
||||
() => {
|
||||
price = Number(price4Show.replace(/,/g, ''));
|
||||
if (price % 1 === 0) {
|
||||
const [, dec] = price4Show.split('.');
|
||||
if (!dec) {
|
||||
price4Show += '.00';
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
price4Show = commaInput(v?.toString() || '0', 'string');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
v-if="priceDisplay?.agentPrice && props.rowIndex === 1"
|
||||
id="input-agent-price"
|
||||
for="input-agent-price"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
:borderless="readonly"
|
||||
hide-bottom-space
|
||||
input-class="text-right"
|
||||
:model-value="agentPrice4Show"
|
||||
@blur="
|
||||
() => {
|
||||
agentPrice = Number(agentPrice4Show.replace(/,/g, ''));
|
||||
if (agentPrice % 1 === 0) {
|
||||
const [, dec] = agentPrice4Show.split('.');
|
||||
if (!dec) {
|
||||
agentPrice4Show += '.00';
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
agentPrice4Show = commaInput(
|
||||
v?.toString() || '0',
|
||||
'string',
|
||||
);
|
||||
}
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
v-if="priceDisplay?.serviceCharge && props.rowIndex === 2"
|
||||
id="input-service-charge"
|
||||
for="input-service-charge"
|
||||
:dense="dense"
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
:borderless="readonly"
|
||||
input-class="text-right"
|
||||
hide-bottom-space
|
||||
:model-value="serviceCharge4Show"
|
||||
@blur="
|
||||
() => {
|
||||
serviceCharge = Number(
|
||||
serviceCharge4Show.replace(/,/g, ''),
|
||||
);
|
||||
if (serviceCharge % 1 === 0) {
|
||||
const [, dec] = serviceCharge4Show.split('.');
|
||||
if (!dec) {
|
||||
serviceCharge4Show += '.00';
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
serviceCharge4Show = commaInput(
|
||||
v?.toString() || '0',
|
||||
'string',
|
||||
);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td class="text-right" style="width: 15%">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'beforeVat',
|
||||
vatIncluded: vatIncluded,
|
||||
price:
|
||||
(props.rowIndex === 0
|
||||
? price
|
||||
: props.rowIndex === 1
|
||||
? agentPrice
|
||||
: serviceCharge) || 0,
|
||||
}),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</q-td>
|
||||
<q-td class="text-right" style="width: 15%">
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'vat',
|
||||
calcVat: calcVat,
|
||||
price: Number(
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'beforeVat',
|
||||
vatIncluded: vatIncluded,
|
||||
price:
|
||||
(props.rowIndex === 0
|
||||
? price
|
||||
: props.rowIndex === 1
|
||||
? agentPrice
|
||||
: serviceCharge) || 0,
|
||||
}),
|
||||
2,
|
||||
).replaceAll(',', ''),
|
||||
),
|
||||
}),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</q-td>
|
||||
<q-td class="text-right" style="width: 15%">
|
||||
<span
|
||||
class="text-weight-bold"
|
||||
:class="{
|
||||
'tags-color-orange': props.rowIndex === 0,
|
||||
'tags-color-purple': props.rowIndex === 1,
|
||||
'tags-color-pink': props.rowIndex === 2,
|
||||
dark: $q.dark.isActive,
|
||||
}"
|
||||
>
|
||||
{{
|
||||
formatNumberDecimal(
|
||||
calculatePrice({
|
||||
output: 'total',
|
||||
vat: 0.03,
|
||||
price:
|
||||
(props.rowIndex === 0
|
||||
? price
|
||||
: props.rowIndex === 1
|
||||
? agentPrice
|
||||
: serviceCharge) || 0,
|
||||
}) +
|
||||
(!vatIncluded
|
||||
? calculatePrice({
|
||||
output: 'vat',
|
||||
calcVat: calcVat,
|
||||
price:
|
||||
(props.rowIndex === 0
|
||||
? price
|
||||
: props.rowIndex === 1
|
||||
? agentPrice
|
||||
: serviceCharge) || 0,
|
||||
})
|
||||
: 0),
|
||||
2,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -190,4 +380,24 @@ withDefaults(
|
|||
background-color: var(--surface-1);
|
||||
}
|
||||
}
|
||||
|
||||
.tags-color-orange {
|
||||
color: var(--orange-5);
|
||||
}
|
||||
|
||||
.dark .tags-color-orange {
|
||||
color: var(--orange-6);
|
||||
}
|
||||
|
||||
.tags-color-purple {
|
||||
color: var(--violet-11);
|
||||
}
|
||||
|
||||
.dark .tags-color-purple {
|
||||
color: var(--violet-10);
|
||||
}
|
||||
|
||||
.tags-color-pink {
|
||||
color: var(--pink-6);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,10 @@ withDefaults(
|
|||
</div>
|
||||
|
||||
<div class="q-px-md" :class="{ 'disabled-card': isDisabled }">
|
||||
<div class="text-subtitle1 text-bold">{{ title }}</div>
|
||||
<div class="text-subtitle1 text-bold ellipsis-2-lines">
|
||||
{{ title }}
|
||||
<q-tooltip>{{ title }}</q-tooltip>
|
||||
</div>
|
||||
<div class="text-subtitle3 app-text-muted">{{ subtitle }}</div>
|
||||
<div class="row q-gutter-md q-mt-sm">
|
||||
<div
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
388
src/components/04_product-service/TableProduct.vue
Normal file
388
src/components/04_product-service/TableProduct.vue
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { QTableProps } from 'quasar';
|
||||
|
||||
import { Product } from 'stores/product-service/types';
|
||||
|
||||
import KebabAction from 'src/components/shared/KebabAction.vue';
|
||||
|
||||
import { MainButton } from 'components/button';
|
||||
|
||||
import useOptionStore from 'stores/options';
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
import { dateFormat } from 'src/utils/datetime';
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
const baseUrl = ref<string>(import.meta.env.VITE_API_BASE_URL);
|
||||
|
||||
const selectedItem = defineModel<Product[]>('selectedItem');
|
||||
const isSort = ref<boolean>(false);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
row: QTableProps['rows'];
|
||||
column: QTableProps['columns'];
|
||||
grid?: boolean;
|
||||
fieldSelected?: string[];
|
||||
currentPage?: number;
|
||||
pageSize?: number;
|
||||
useKebabAction?: boolean;
|
||||
useSortAction?: boolean;
|
||||
}>(),
|
||||
{
|
||||
row: () => [],
|
||||
column: () => [],
|
||||
grid: false,
|
||||
fieldSelected: () => [],
|
||||
currentPage: 1,
|
||||
pageSize: 1,
|
||||
useKebabAction: false,
|
||||
},
|
||||
);
|
||||
|
||||
defineEmits<{
|
||||
(e: 'view'): void;
|
||||
(e: 'edit'): void;
|
||||
(e: 'delete'): void;
|
||||
(e: 'changeStatus'): void;
|
||||
(e: 'select', data: any): void;
|
||||
(e: 'sort', isSort: boolean): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-table
|
||||
bordered
|
||||
:rows="row"
|
||||
:columns="column"
|
||||
:rows-per-page-options="[0]"
|
||||
:grid="grid"
|
||||
v-model:selected="selectedItem"
|
||||
row-key="id"
|
||||
@update:selected="(v) => $emit('select', v)"
|
||||
card-container-class="row full-width q-col-gutter-md"
|
||||
hide-pagination
|
||||
selection="multiple"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th auto-width>
|
||||
<!-- <MainButton
|
||||
icon="mdi-sort"
|
||||
color="0 0% 0%"
|
||||
@click="
|
||||
() => {
|
||||
$emit('sort', (isSort = !isSort));
|
||||
}
|
||||
"
|
||||
/> -->
|
||||
</q-th>
|
||||
<template v-for="col in props.cols" :key="col.name">
|
||||
<q-th>
|
||||
{{ $t(col.label) }}
|
||||
</q-th>
|
||||
</template>
|
||||
<q-th v-if="!!useKebabAction" auto-width />
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
:style="
|
||||
props.rowIndex % 2 !== 0
|
||||
? $q.dark.isActive
|
||||
? 'background: hsl(var(--gray-11-hsl)/0.2)'
|
||||
: `background: #f9fafc`
|
||||
: ''
|
||||
"
|
||||
:class="{
|
||||
'app-text-muted': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
class="cursor-pointer"
|
||||
:props="props"
|
||||
@click.stop="$emit('select', props.row)"
|
||||
>
|
||||
<q-td>
|
||||
<q-checkbox
|
||||
:model-value="props.selected"
|
||||
@click.stop="$emit('select', props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
|
||||
<q-td
|
||||
class="text-center"
|
||||
v-if="fieldSelected.includes('branchLabelNo')"
|
||||
>
|
||||
{{ (currentPage - 1) * pageSize + props.rowIndex + 1 }}
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('productName')">
|
||||
<div class="row items-center no-wrap">
|
||||
<div
|
||||
:class="{
|
||||
'status-active': props.row.status !== 'INACTIVE',
|
||||
'status-inactive': props.row.status === 'INACTIVE',
|
||||
}"
|
||||
style="width: 50px; display: flex; margin-bottom: var(--size-2)"
|
||||
>
|
||||
<div class="table__icon" :class="`icon-color-green`">
|
||||
<q-avatar size="md">
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/product/${props.row.id}/image/${props.row.selectedImage}`"
|
||||
>
|
||||
<template #error>
|
||||
<q-icon
|
||||
size="sm"
|
||||
name="mdi-shopping-outline"
|
||||
style="top: 10%"
|
||||
:style="`color: var(teal-10)`"
|
||||
/>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ellipsis" style="max-width: 20vw">
|
||||
{{ props.row.name }}
|
||||
<q-tooltip anchor="bottom left" self="center left" :delay="300">
|
||||
{{ props.row.name }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<div class="app-text-muted">
|
||||
{{ props.row.code }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td
|
||||
class="ellipsis"
|
||||
style="max-width: 150px"
|
||||
v-if="fieldSelected.includes('productDetail')"
|
||||
>
|
||||
{{ props.row.detail.replace(/<\/?[^>]+(>|$)/g, '') || '-' }}
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('productExpenseType')">
|
||||
{{ optionStore.mapOption(props.row.expenseType) }}
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('productProcessingTime')">
|
||||
{{ props.row.process }}
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('productVat')">
|
||||
{{ $t('productService.product.vatIncluded') }}
|
||||
</q-td>
|
||||
<q-td v-if="fieldSelected.includes('priceInformation')">
|
||||
<div
|
||||
class="row full-width q-gutter-x-md no-wrap items-center text-right"
|
||||
>
|
||||
<div
|
||||
class="tags tags-color-orange col column ellipsis-2-lines"
|
||||
:class="{
|
||||
disable: props.row.status === 'INACTIVE',
|
||||
}"
|
||||
style="min-width: 50px"
|
||||
>
|
||||
<div class="col app-text-muted-2 text-caption">
|
||||
{{ $t('productService.product.salePrice') }}
|
||||
</div>
|
||||
<div class="col text-weight-bold">
|
||||
฿{{ formatNumberDecimal(props.row.price || 0, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tags tags-color-purple col column ellipsis-2-lines"
|
||||
:class="{
|
||||
disable: props.row.status === 'INACTIVE',
|
||||
}"
|
||||
style="min-width: 50px"
|
||||
>
|
||||
<div class="col app-text-muted-2 text-caption">
|
||||
{{ $t('productService.product.agentPrice') }}
|
||||
</div>
|
||||
<div class="col text-weight-bold">
|
||||
฿{{ formatNumberDecimal(props.row.agentPrice || 0, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tags tags-color-pink col column ellipsis-2-lines"
|
||||
:class="{
|
||||
disable: props.row.status === 'INACTIVE',
|
||||
}"
|
||||
style="min-width: 50px"
|
||||
>
|
||||
<div class="col app-text-muted-2 text-caption">
|
||||
{{ $t('productService.product.processingPrice') }}
|
||||
</div>
|
||||
<div class="col">
|
||||
฿{{ formatNumberDecimal(props.row.serviceCharge || 0, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="fieldSelected.includes('createdAt')">
|
||||
{{ dateFormat(props.row.createdAt) }}
|
||||
</q-td>
|
||||
<q-td v-if="!!useKebabAction">
|
||||
<q-btn
|
||||
icon="mdi-eye-outline"
|
||||
:id="`btn-eye-${props.row.name}`"
|
||||
size="sm"
|
||||
dense
|
||||
round
|
||||
flat
|
||||
@click.stop="$emit('view')"
|
||||
/>
|
||||
<KebabAction
|
||||
:status="props.row.status"
|
||||
:id-name="props.row.name"
|
||||
@view="$emit('view')"
|
||||
@edit="$emit('edit')"
|
||||
@delete="$emit('delete')"
|
||||
@change-status="$emit('changeStatus')"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="{ row }">
|
||||
<div class="col-3"><slot name="grid" :row="row" /></div>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.status-active {
|
||||
--_branch-status-color: var(--green-6-hsl);
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
--_branch-status-color: var(--stone-5-hsl);
|
||||
--_branch-badge-bg: var(--stone-5-hsl);
|
||||
filter: grayscale(0.5);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.icon-color-purple {
|
||||
--_color: var(--violet-11-hsl);
|
||||
}
|
||||
|
||||
.icon-color-pink {
|
||||
--_color: var(--pink-6-hsl);
|
||||
}
|
||||
|
||||
.icon-color-orange {
|
||||
--_color: var(--orange-5-hsl);
|
||||
}
|
||||
|
||||
.icon-color-green {
|
||||
--_color: var(--teal-10-hsl);
|
||||
}
|
||||
|
||||
.dark .icon-color-purple {
|
||||
--_color: var(--violet-10-hsl);
|
||||
}
|
||||
|
||||
.dark .icon-color-green {
|
||||
--_color: var(--teal-8-hsl);
|
||||
}
|
||||
|
||||
.dark .icon-color-orange {
|
||||
--_color: var(--orange-6-hsl);
|
||||
}
|
||||
|
||||
.tags-color-green {
|
||||
--_color-tag: var(--teal-10-hsl);
|
||||
}
|
||||
|
||||
.dark .tags-color-green {
|
||||
--_color-tag: var(--teal-8-hsl);
|
||||
}
|
||||
|
||||
.tags-color-orange {
|
||||
--_color-tag: var(--orange-5-hsl);
|
||||
}
|
||||
|
||||
.dark .tags-color-orange {
|
||||
--_color-tag: var(--orange-6-hsl);
|
||||
}
|
||||
|
||||
.tags-color-purple {
|
||||
--_color-tag: var(--violet-11-hsl);
|
||||
}
|
||||
|
||||
.dark .tags-color-purple {
|
||||
--_color-tag: var(--violet-10-hsl);
|
||||
}
|
||||
|
||||
.tags-color-pink {
|
||||
--_color-tag: var(--pink-6-hsl);
|
||||
}
|
||||
|
||||
.table__icon {
|
||||
background-color: hsla(var(--_color) / 0.15);
|
||||
color: hsla(var(--_color) / 1);
|
||||
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
transform: rotate(45deg);
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
block-size: 0.5rem;
|
||||
aspect-ratio: 1;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
right: -0.1rem;
|
||||
top: calc(50% - 0.25rem);
|
||||
bottom: calc(50% - 0.25rem);
|
||||
background-color: hsla(var(--_branch-status-color) / 1);
|
||||
}
|
||||
|
||||
&:deep(.q-icon) {
|
||||
transform: rotate(-45deg);
|
||||
color: hsla(var(--_branch-card-bg) / 1);
|
||||
}
|
||||
|
||||
&:deep(.q-img) {
|
||||
transform: rotate(-45deg);
|
||||
|
||||
&:deep(.q-icon) {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: inline-block;
|
||||
color: hsla(var(--_color-tag) / 1);
|
||||
background: hsla(var(--_color-tag) / 0.075);
|
||||
border-radius: var(--radius-2);
|
||||
padding-inline: var(--size-2);
|
||||
|
||||
&.disable {
|
||||
filter: grayscale(100%);
|
||||
opacity: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
* :deep(.q-icon.mdi-play) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.product-form-active {
|
||||
background-color: hsla(var(--info-bg) / 0.2);
|
||||
color: hsl(var(--info-bg));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.split-pay .q-field__control) {
|
||||
height: 23px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -11,21 +11,16 @@ withDefaults(
|
|||
defineProps<{
|
||||
data?: any;
|
||||
title?: string;
|
||||
|
||||
dense?: boolean;
|
||||
outlined?: boolean;
|
||||
readonly?: boolean;
|
||||
separator?: boolean;
|
||||
typeProduct?: string;
|
||||
|
||||
isAddProduct?: boolean;
|
||||
action?: boolean;
|
||||
|
||||
index?: number;
|
||||
isDisabled?: boolean;
|
||||
|
||||
noTimeImg?: boolean;
|
||||
|
||||
priceDisplay?: {
|
||||
price: boolean;
|
||||
agentPrice: boolean;
|
||||
|
|
@ -36,6 +31,14 @@ withDefaults(
|
|||
action: false,
|
||||
},
|
||||
);
|
||||
|
||||
defineEmits<{
|
||||
(e: 'select', data: any): void;
|
||||
(e: 'menuViewDetail'): void;
|
||||
(e: 'menuEdit'): void;
|
||||
(e: 'menuDelete'): void;
|
||||
(e: 'toggleStatus', id: string): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -97,8 +100,8 @@ withDefaults(
|
|||
|
||||
<div v-if="data?.type === 'product'" class="flex full-width">
|
||||
<div
|
||||
class="row full-width text-right"
|
||||
style="font-size: 10px; color: hsl(var(--stone-4-hsl))"
|
||||
class="row full-width text-right app-text-muted"
|
||||
style="font-size: 10px"
|
||||
>
|
||||
<div class="col" v-if="priceDisplay?.price">
|
||||
{{ $t('productService.product.salePrice') }}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,34 @@
|
|||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
|
||||
import useProductServiceStore from 'stores/product-service';
|
||||
import useOptionStore from 'stores/options';
|
||||
import { Attributes, Product } from 'stores/product-service/types';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref, watch } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import useOptionStore from 'stores/options';
|
||||
import useProductServiceStore from 'stores/product-service';
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
import { useWorkflowTemplate } from 'src/stores/workflow-template';
|
||||
import { Attributes, Product } from 'stores/product-service/types';
|
||||
|
||||
import NoData from '../NoData.vue';
|
||||
import { AddButton } from '../button';
|
||||
|
||||
const baseUrl = ref<string>(import.meta.env.VITE_API_BASE_URL);
|
||||
const productServiceStore = useProductServiceStore();
|
||||
const optionStore = useOptionStore();
|
||||
const workflowStore = useWorkflowTemplate();
|
||||
|
||||
const { fetchListOfWork } = productServiceStore;
|
||||
const { workNameItems } = storeToRefs(productServiceStore);
|
||||
|
||||
withDefaults(
|
||||
const { data: workflowData } = storeToRefs(workflowStore);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
workIndex: number;
|
||||
length: number;
|
||||
index: number;
|
||||
readonly?: boolean;
|
||||
installments?: number;
|
||||
|
||||
priceDisplay?: {
|
||||
price: boolean;
|
||||
|
|
@ -61,14 +69,41 @@ defineEmits<{
|
|||
(e: 'workProperties'): void;
|
||||
}>();
|
||||
|
||||
async function fetchWorkflowOption(val?: string) {
|
||||
const res = await workflowStore.getWorkflowTemplateList({
|
||||
query: val,
|
||||
pageSize: 30,
|
||||
});
|
||||
if (res) workflowData.value = res.result;
|
||||
}
|
||||
|
||||
function toggleCheckProductInStep(id: string, stepIndex: number) {
|
||||
const index = attributes.value.workflowStep[stepIndex].productsId.indexOf(id);
|
||||
|
||||
if (!attributes.value.workflowStep[stepIndex].productsId.includes(id)) {
|
||||
attributes.value.workflowStep[stepIndex].productsId.push(id);
|
||||
} else {
|
||||
attributes.value.workflowStep[stepIndex].productsId.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.workIndex === 0) {
|
||||
await fetchWorkflowOption();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => workNameItems.value,
|
||||
(c, o) => {
|
||||
const list = c.map((v: { name: string }) => v.name);
|
||||
const oldList = o.map((v: { name: string }) => v.name);
|
||||
const index = oldList.indexOf(workName.value);
|
||||
const index = oldList.indexOf(workName.value || '');
|
||||
|
||||
if (list[index] !== oldList[index] && !list.includes(workName.value)) {
|
||||
if (
|
||||
list[index] !== oldList[index] &&
|
||||
!list.includes(workName.value || '')
|
||||
) {
|
||||
if (list.length - 1 === index - 1) workName.value = list[index - 1];
|
||||
else workName.value = list[index];
|
||||
}
|
||||
|
|
@ -84,7 +119,7 @@ watch(
|
|||
switch-toggle-side
|
||||
default-opened
|
||||
expand-icon="mdi-chevron-down-circle"
|
||||
header-class="surface-2 expansion-rounded"
|
||||
header-class="expansion-rounded"
|
||||
header-style="border-top-left-radius: var(--radius-2); border-top-right-radius: var(--radius-2)"
|
||||
>
|
||||
<template v-slot:header>
|
||||
|
|
@ -185,6 +220,27 @@ watch(
|
|||
</q-item>
|
||||
</q-menu>
|
||||
</div>
|
||||
<!-- <SelectInput
|
||||
:readonly
|
||||
incremental
|
||||
:model-value="mapFlowName(attributes.workflowId)"
|
||||
id="select-workflow-name"
|
||||
for="select-workflow-name"
|
||||
class="col"
|
||||
option-label="name"
|
||||
:option="workflowData"
|
||||
:placeholder="$t('productService.service.workName')"
|
||||
@update:model-value="(val: WorkflowTemplate) => selectFlow(val)"
|
||||
@filter="(val: string, update) => filter(val, update)"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="text-body2" style="color: var(--foreground)">
|
||||
{{
|
||||
$t('productService.service.workNo', { msg: workIndex + 1 })
|
||||
}}:
|
||||
</span>
|
||||
</template>
|
||||
</SelectInput> -->
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-delete-work"
|
||||
|
|
@ -203,60 +259,14 @@ watch(
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<div class="surface-2">
|
||||
<!-- properties -->
|
||||
<div class="bordered-t">
|
||||
<div
|
||||
class="q-py-xs text-weight-medium row justify-between items-center q-px-md"
|
||||
style="background-color: hsla(var(--info-bg) / 0.1)"
|
||||
>
|
||||
<span>
|
||||
{{ $t('productService.service.propertiesInWork') }}
|
||||
{{ workIndex + 1 }}
|
||||
</span>
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-add-work-product"
|
||||
class="text-capitalize"
|
||||
flat
|
||||
dense
|
||||
padding="0"
|
||||
style="color: hsl(var(--info-bg))"
|
||||
@click.stop="$emit('workProperties')"
|
||||
>
|
||||
<Icon
|
||||
icon="basil:settings-adjust-solid"
|
||||
width="24px"
|
||||
class="q-mr-sm"
|
||||
style="color: hsl(var(--info-bg))"
|
||||
/>
|
||||
<span v-if="$q.screen.gt.xs">
|
||||
{{ $t('productService.service.properties') }}
|
||||
</span>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<div class="q-py-md q-px-md full-width">
|
||||
<div
|
||||
v-if="attributes.additional.length > 0"
|
||||
class="row items-center full-width surface-1 q-pb-md q-pt-sm q-px-sm q-gutter-sm scroll"
|
||||
:style="$q.screen.xs ? 'max-height: 100px' : ''"
|
||||
>
|
||||
<div
|
||||
v-for="(p, index) in attributes.additional"
|
||||
:key="index"
|
||||
class="bordered q-px-sm surface-3"
|
||||
style="border-radius: 6px"
|
||||
>
|
||||
{{ optionStore.mapOption(p.fieldName ?? '') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="app-text-muted">
|
||||
{{ $t('productService.service.noProperties') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section
|
||||
v-if="!workName && productItems.length === 0"
|
||||
class="surface-2 row items-center justify-center q-py-sm"
|
||||
>
|
||||
<NoData />
|
||||
</section>
|
||||
|
||||
<section v-else class="surface-2">
|
||||
<!-- product -->
|
||||
<div class="bordered-t">
|
||||
<div
|
||||
|
|
@ -276,19 +286,11 @@ watch(
|
|||
:disable="readonly"
|
||||
/>
|
||||
</span>
|
||||
<q-btn
|
||||
<AddButton
|
||||
v-if="!readonly"
|
||||
icon-only
|
||||
id="btn-add-work-product"
|
||||
for="btn-add-work-product"
|
||||
flat
|
||||
dense
|
||||
icon="mdi-plus"
|
||||
class="text-capitalize"
|
||||
:label="
|
||||
$q.screen.gt.xs ? $t('productService.product.addTitle') : ''
|
||||
"
|
||||
padding="0"
|
||||
style="color: hsl(var(--info-bg))"
|
||||
@click.stop="$emit('addProduct')"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -297,15 +299,17 @@ watch(
|
|||
v-if="productItems.length > 0"
|
||||
class="q-py-md q-px-md full-width q-gutter-y-sm"
|
||||
>
|
||||
<div
|
||||
<section
|
||||
v-for="(product, index) in productItems"
|
||||
:key="product.id"
|
||||
class="full-width row items-center justify-between"
|
||||
>
|
||||
<div
|
||||
class="row col items-center justify-between full-width surface-1 q-py-md q-px-sm"
|
||||
class="row col items-center justify-between full-width surface-1 q-px-sm q-py-xs"
|
||||
style="min-height: 70px"
|
||||
>
|
||||
<div
|
||||
<!-- product detail -->
|
||||
<section
|
||||
class="row items-center col-md col-12 no-wrap"
|
||||
v-if="productItems"
|
||||
>
|
||||
|
|
@ -316,6 +320,7 @@ watch(
|
|||
dense
|
||||
flat
|
||||
round
|
||||
size="sm"
|
||||
:disable="index === 0"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
@click.stop="$emit('moveProductUp', productItems, index)"
|
||||
|
|
@ -328,27 +333,27 @@ watch(
|
|||
dense
|
||||
flat
|
||||
round
|
||||
class="q-mx-sm"
|
||||
size="sm"
|
||||
:disable="index === productItems.length - 1"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
@click.stop="$emit('moveProductDown', productItems, index)"
|
||||
/>
|
||||
|
||||
<q-avatar
|
||||
size="md"
|
||||
:class="$q.screen.gt.xs ? 'q-mx-lg' : 'q-mr-lg'"
|
||||
size="sm"
|
||||
class="q-mx-sm"
|
||||
style="background-color: var(--surface-tab)"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</q-avatar>
|
||||
|
||||
<div class="row no-wrap">
|
||||
<div class="col row no-wrap items-center">
|
||||
<div
|
||||
v-if="$q.screen.gt.xs"
|
||||
class="bordered q-mx-md col-3 image-box"
|
||||
>
|
||||
<q-img
|
||||
:src="`${baseUrl}/product/${product?.id}/image`"
|
||||
:src="`${baseUrl}/product/${product.id}/image/${product.selectedImage}`"
|
||||
style="object-fit: cover; width: 100%; height: 100%"
|
||||
>
|
||||
<template #error>
|
||||
|
|
@ -360,35 +365,30 @@ watch(
|
|||
</template>
|
||||
</q-img>
|
||||
</div>
|
||||
<div class="column col justify-between">
|
||||
<article class="column col full-width justify-between">
|
||||
<span
|
||||
class="text-weight-bold ellipsis-2-lines"
|
||||
:style="`max-width: ${$q.screen.gt.sm ? '25vw' : '20vw'}`"
|
||||
class="text-weight-medium ellipsis-2-lines full-width"
|
||||
>
|
||||
{{ product.name }}
|
||||
<q-tooltip>
|
||||
{{ product.name }}
|
||||
</q-tooltip>
|
||||
</span>
|
||||
<div
|
||||
class="bordered q-px-xs ellipsis"
|
||||
style="border-radius: 6px; max-width: 100px"
|
||||
>
|
||||
{{ product.code }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-caption">
|
||||
<span class="bordered q-px-xs rounded q-mr-sm">
|
||||
{{ product.code }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="row justify-end text-right col-md-6 col-12"
|
||||
:class="$q.screen.xs ? 'q-mt-sm text-caption' : 'q-pr-sm'"
|
||||
>
|
||||
<span
|
||||
class="col-12 row"
|
||||
:class="{ 'q-col-gutter-md': $q.screen.gt.xs }"
|
||||
style="color: var(--teal-9)"
|
||||
>
|
||||
<q-icon name="mdi-clock-outline" />
|
||||
{{ product.process }} {{ $t('general.day') }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- product price -->
|
||||
<div class="row justify-end text-right col-md col-12">
|
||||
<span class="col-12 row" style="color: var(--teal-9)">
|
||||
<span
|
||||
v-if="priceDisplay?.price"
|
||||
class="col ellipsis price-orange text-weight-bold"
|
||||
|
|
@ -437,13 +437,27 @@ watch(
|
|||
฿{{ formatNumberDecimal(product.serviceCharge, 2) }}
|
||||
</q-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="col-9 q-mt-sm text-caption app-text-muted-2">
|
||||
{{ $t('productService.product.processingTime') }}
|
||||
</span>
|
||||
<span class="col-3 q-mt-sm text-caption app-text-muted-2">
|
||||
{{ product.process }} {{ $t('general.day') }}
|
||||
<span class="col ellipsis column text-weight-medium">
|
||||
<div class="text-caption app-text-muted-2">
|
||||
{{ $t('productService.service.InstallmentsNo') }}
|
||||
</div>
|
||||
{{ !readonly ? '' : product.installmentNo }}
|
||||
<span class="row justify-end">
|
||||
<q-input
|
||||
v-if="!readonly && $q.screen.gt.xs"
|
||||
outlined
|
||||
:max="installments"
|
||||
input-class="text-right no-padding"
|
||||
for="input-bankbook"
|
||||
hide-bottom-space
|
||||
class="installment-no col-10"
|
||||
dense
|
||||
type="number"
|
||||
v-model="product.installmentNo"
|
||||
min="1"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -463,18 +477,181 @@ watch(
|
|||
>
|
||||
<q-tooltip>{{ $t('general.delete') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div v-else class="app-text-muted q-py-md q-px-lg">
|
||||
<div v-else class="app-text-muted q-py-md q-px-md">
|
||||
{{ $t('productService.product.noProduct') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- properties -->
|
||||
<div class="bordered-t">
|
||||
<q-expansion-item
|
||||
default-opened
|
||||
switch-toggle-side
|
||||
header-style="background-color: hsla(var(--info-bg) / 0.1); padding: 4px 16px; min-height: 36.08px"
|
||||
header-class="row items-center q-px-md"
|
||||
expand-icon-class="no-padding"
|
||||
dense
|
||||
>
|
||||
<template #header>
|
||||
<div
|
||||
class="text-weight-medium row justify-between items-center full-width"
|
||||
>
|
||||
<span>
|
||||
{{ $t('flow.processStep') }}
|
||||
</span>
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
id="btn-add-work-product"
|
||||
class="text-capitalize rounded"
|
||||
flat
|
||||
dense
|
||||
padding="4px 8px"
|
||||
style="color: hsl(var(--info-bg))"
|
||||
@click.stop="$emit('workProperties')"
|
||||
>
|
||||
<Icon
|
||||
icon="basil:settings-adjust-solid"
|
||||
width="20.08px"
|
||||
style="color: hsl(var(--info-bg))"
|
||||
/>
|
||||
</q-btn>
|
||||
</div>
|
||||
</template>
|
||||
<div class="q-py-md q-px-md full-width column">
|
||||
<span class="app-text-muted">
|
||||
{{
|
||||
!attributes.hasOwnProperty('workflowStep')
|
||||
? $t('general.no', { msg: $t('flow.title') })
|
||||
: !attributes.workflowId
|
||||
? $t('general.no', { msg: $t('flow.title') })
|
||||
: attributes.workflowStep?.length === 0
|
||||
? $t('flow.noProcessStep')
|
||||
: attributes.workflowStep?.every(
|
||||
(s) => !s.attributes.properties?.length,
|
||||
)
|
||||
? $t('productService.service.noPropertiesYet')
|
||||
: ''
|
||||
}}
|
||||
</span>
|
||||
|
||||
<template
|
||||
v-for="(step, stepIndex) in attributes.workflowStep"
|
||||
:key="stepIndex"
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
attributes.workflowStep[stepIndex].attributes.properties
|
||||
.length > 0
|
||||
"
|
||||
>
|
||||
<q-icon name="mdi-circle-medium" />
|
||||
{{ $t('flow.stepNo', { msg: stepIndex + 1 }) }}:
|
||||
{{ step.name }}
|
||||
|
||||
<!-- step att -->
|
||||
<section
|
||||
class="col scroll q-pa-sm flex items-center surface-1 rounded"
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
attributes.workflowStep[stepIndex].attributes.properties
|
||||
.length > 0
|
||||
"
|
||||
class="row q-gutter-sm"
|
||||
>
|
||||
<span
|
||||
v-for="(att, i) in step.attributes.properties"
|
||||
:key="i"
|
||||
class="surface-2 bordered rounded q-px-xs"
|
||||
>
|
||||
{{ optionStore.mapOption(att.fieldName ?? '') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="app-text-muted-2">
|
||||
{{ $t('productService.service.noProperties') }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- step product -->
|
||||
<section
|
||||
class="q-pt-sm q-pl-lg column"
|
||||
:class="{
|
||||
'q-pb-sm':
|
||||
stepIndex !== attributes.workflowStep.length - 1,
|
||||
}"
|
||||
>
|
||||
<span class="app-text-muted-2 text-caption">
|
||||
{{
|
||||
$t('general.select', {
|
||||
msg: $t('productService.product.title'),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="productItems.length > 0"
|
||||
class="surface-1 rounded q-pa-xs"
|
||||
>
|
||||
<div v-for="product in productItems" :key="product.id">
|
||||
<q-checkbox
|
||||
v-if="attributes.workflowStep[stepIndex].productsId"
|
||||
:disable="readonly"
|
||||
:model-value="
|
||||
attributes.workflowStep[
|
||||
stepIndex
|
||||
].productsId.includes(product.id)
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
if (readonly) return;
|
||||
toggleCheckProductInStep(product.id, stepIndex);
|
||||
}
|
||||
"
|
||||
size="xs"
|
||||
/>
|
||||
{{ product.name }}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
class="app-text-muted-2 surface-1 rounded q-pa-xs"
|
||||
>
|
||||
{{ $t('productService.product.noProduct') }}
|
||||
</span>
|
||||
</section>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
<!-- <div class="q-py-md q-px-md full-width">
|
||||
<div
|
||||
v-if="attributes.additional.length > 0"
|
||||
class="row items-center full-width surface-1 q-pb-md q-pt-sm q-px-sm q-gutter-sm scroll"
|
||||
:style="$q.screen.xs ? 'max-height: 100px' : ''"
|
||||
>
|
||||
<div
|
||||
v-for="(p, index) in attributes.additional"
|
||||
:key="index"
|
||||
class="bordered q-px-sm surface-3"
|
||||
style="border-radius: 6px"
|
||||
>
|
||||
{{ optionStore.mapOption(p.fieldName ?? '') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="app-text-muted">
|
||||
{{ $t('productService.service.noProperties') }}
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</section>
|
||||
</q-expansion-item>
|
||||
<div class="q-py-sm q-px-md bordered-t row items-center justify-between">
|
||||
<div>
|
||||
{{ $t('productService.service.totalProductWork') }}
|
||||
<span class="app-text-muted-2">
|
||||
<!-- {{ mapFlowName(attributes.workflowId) }} -->
|
||||
{{ workName }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -487,8 +664,8 @@ watch(
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.image-box {
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
border-color: var(--teal-9);
|
||||
border-radius: 10px;
|
||||
background-color: var(--surface-3);
|
||||
|
|
@ -525,4 +702,18 @@ watch(
|
|||
.price-pink {
|
||||
color: var(--pink-6);
|
||||
}
|
||||
|
||||
:deep(i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon) {
|
||||
color: hsl(var(--text-mute));
|
||||
}
|
||||
|
||||
:deep(
|
||||
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
|
||||
) {
|
||||
color: var(--brand-1);
|
||||
}
|
||||
|
||||
:deep(.installment-no .q-field__control) {
|
||||
height: 23px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ defineExpose({
|
|||
defineEmits<{
|
||||
(e: 'delete', id: string, noDialog?: boolean): void;
|
||||
(e: 'edit', id: string, data: { name: string }): void;
|
||||
(e: 'add', data: { name: string; productId: []; order: number }): void;
|
||||
(e: 'add', data: { name: string; order: number }): void;
|
||||
}>();
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
@ -158,8 +158,7 @@ watch(
|
|||
:disable="isWorkNameEdit()"
|
||||
@click="
|
||||
() => {
|
||||
$emit('add', { name: '', productId: [], order: 1 }),
|
||||
(isAdd = true);
|
||||
$emit('add', { name: '', order: 1 }), (isAdd = true);
|
||||
}
|
||||
"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import useBranchStore from 'src/stores/branch';
|
||||
import useCustomerStore from 'src/stores/customer';
|
||||
import SelectInput from '../shared/SelectInput.vue';
|
||||
import { QSelect } from 'quasar';
|
||||
import { Branch } from 'src/stores/branch/types';
|
||||
import { CustomerBranch } from 'src/stores/customer/types';
|
||||
|
||||
const { locale } = useI18n({ useScope: 'global' });
|
||||
const branchStore = useBranchStore();
|
||||
const customerStore = useCustomerStore();
|
||||
import SelectCustomer from '../shared/select/SelectCustomer.vue';
|
||||
import SelectBranch from '../shared/select/SelectBranch.vue';
|
||||
|
||||
const branchId = defineModel<string>('branchId');
|
||||
const customerBranchId = defineModel<string>('customerBranchId');
|
||||
const agentPrice = defineModel<boolean>('agentPrice');
|
||||
const special = defineModel<boolean>('special');
|
||||
|
||||
const branchOption = ref();
|
||||
const customerOption = ref();
|
||||
|
||||
defineProps<{
|
||||
outlined?: boolean;
|
||||
readonly?: boolean;
|
||||
|
|
@ -27,6 +14,7 @@ defineProps<{
|
|||
employee?: boolean;
|
||||
title?: string;
|
||||
inputOnly?: boolean;
|
||||
hideAdd?: boolean;
|
||||
|
||||
onCreate?: boolean;
|
||||
}>();
|
||||
|
|
@ -34,62 +22,6 @@ defineProps<{
|
|||
defineEmits<{
|
||||
(e: 'addCustomer'): void;
|
||||
}>();
|
||||
|
||||
async function filter(
|
||||
val: string,
|
||||
update: (...args: unknown[]) => void,
|
||||
type: 'branch' | 'customer',
|
||||
) {
|
||||
update(
|
||||
async () => {
|
||||
await init(val, type);
|
||||
},
|
||||
|
||||
(ref: QSelect) => {
|
||||
if (val !== '' && ref.options && ref.options?.length > 0) {
|
||||
ref.setOptionIndex(-1);
|
||||
ref.moveOptionSelection(1, true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function init(val: string, type: 'branch' | 'customer') {
|
||||
const res =
|
||||
type === 'branch'
|
||||
? await branchStore.fetchList({
|
||||
query: val,
|
||||
pageSize: 30,
|
||||
})
|
||||
: await customerStore.fetchListCustomerBranch({
|
||||
query: val,
|
||||
pageSize: 30,
|
||||
company: true,
|
||||
});
|
||||
if (res) {
|
||||
if (type === 'branch') {
|
||||
branchOption.value = (res.result as Branch[]).map((v: Branch) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
labelEN: v.nameEN,
|
||||
}));
|
||||
} else if (type === 'customer') {
|
||||
customerOption.value = (res.result as CustomerBranch[]).map(
|
||||
(v: CustomerBranch) => ({
|
||||
value: v.id,
|
||||
label: v.registerName || `${v.firstName} ${v.lastName}` || '-',
|
||||
labelEN:
|
||||
v.registerNameEN || `${v.firstNameEN} ${v.lastNameEN}` || '-',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await init('', 'branch');
|
||||
await init('', 'customer');
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="row">
|
||||
|
|
@ -123,58 +55,28 @@ onMounted(async () => {
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<SelectInput
|
||||
<SelectBranch
|
||||
v-model:value="branchId"
|
||||
:label="$t('quotation.branchVirtual')"
|
||||
class="col-md-6 col-12"
|
||||
simple
|
||||
required
|
||||
:readonly
|
||||
incremental
|
||||
v-model="branchId"
|
||||
id="quotation-branch"
|
||||
class="col-md col-12"
|
||||
:option="branchOption"
|
||||
:label="$t('quotation.branch')"
|
||||
:option-label="locale === 'eng' ? 'labelEN' : 'label'"
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
@filter="(val: string, update) => filter(val, update, 'branch')"
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
:readonly
|
||||
incremental
|
||||
v-model="customerBranchId"
|
||||
class="col-md col-12"
|
||||
id="quotation-customer"
|
||||
:option="customerOption"
|
||||
<SelectCustomer
|
||||
v-model:value="customerBranchId"
|
||||
:label="$t('quotation.customer')"
|
||||
:option-label="locale === 'eng' ? 'labelEN' : 'label'"
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
@filter="(val: string, update) => filter(val, update, 'customer')"
|
||||
>
|
||||
<template #option="{ scope }">
|
||||
<q-item
|
||||
clickable
|
||||
v-if="scope.index === 0"
|
||||
@click.stop="$emit('addCustomer')"
|
||||
>
|
||||
<q-item-section>
|
||||
{{ $t('general.add', { text: $t('quotation.newCustomer') }) }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator v-if="scope.index === 0" />
|
||||
|
||||
<q-item clickable v-bind="scope.itemProps">
|
||||
<q-item-section>
|
||||
{{ locale === 'eng' ? scope.opt.labelEN : scope.opt.label }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<template #noOption>
|
||||
<q-item clickable @click.stop="$emit('addCustomer')">
|
||||
<q-item-section>
|
||||
{{ $t('general.add', { text: $t('quotation.newCustomer') }) }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</SelectInput>
|
||||
:creatable-disabled-text="`(${$t('form.error.selectField', {
|
||||
field: $t('quotation.branchVirtual'),
|
||||
})})`"
|
||||
@create="$emit('addCustomer')"
|
||||
class="col-md-6 col-12"
|
||||
:creatable-disabled="!branchId"
|
||||
:creatable="!inputOnly"
|
||||
simple
|
||||
required
|
||||
:readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,25 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { QTableProps } from 'quasar';
|
||||
import { QTableProps, QTableSlots } from 'quasar';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { baseUrl } from 'stores/utils';
|
||||
|
||||
import WorkerItem from './WorkerItem.vue';
|
||||
import DeleteButton from '../button/DeleteButton.vue';
|
||||
import { precisionRound } from 'src/utils/arithmetic';
|
||||
import { QuotationPayload } from 'stores/quotations/types';
|
||||
import {
|
||||
ProductRelation,
|
||||
ProductServiceList,
|
||||
QuotationPayload,
|
||||
} from 'stores/quotations/types';
|
||||
import { formatNumberDecimal, commaInput } from 'stores/utils';
|
||||
import { useConfigStore } from 'stores/config';
|
||||
|
||||
const props = defineProps<{
|
||||
readonly?: boolean;
|
||||
agentPrice: boolean;
|
||||
installmentInput?: boolean;
|
||||
maxInstallment?: number | null;
|
||||
employeeRows?: {
|
||||
foreignRefNo: string;
|
||||
employeeName: string;
|
||||
|
|
@ -27,7 +34,15 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'viewFile', data: ProductRelation): void;
|
||||
(e: 'delete', index: number): void;
|
||||
(
|
||||
e: 'updateTable',
|
||||
data: QuotationPayload['productServiceList'][number],
|
||||
opt?: {
|
||||
newInstallmentNo: number;
|
||||
},
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
|
@ -45,9 +60,13 @@ function calcPrice(c: (typeof rows.value)[number]) {
|
|||
return precisionRound(
|
||||
c.pricePerUnit * c.amount -
|
||||
c.discount +
|
||||
(c.product.calcVat
|
||||
? (c.pricePerUnit * c.amount - c.discount) * (config.value?.vat || 0.07)
|
||||
: 0),
|
||||
precisionRound(
|
||||
c.product.calcVat
|
||||
? (c.pricePerUnit * (c.discount ? c.amount : 1) - c.discount) *
|
||||
(config.value?.vat || 0.07)
|
||||
: 0,
|
||||
) *
|
||||
(!c.discount ? c.amount : 1),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -63,19 +82,19 @@ const columns = [
|
|||
{
|
||||
name: 'code',
|
||||
align: 'left',
|
||||
label: 'general.code',
|
||||
label: 'productService.product.code',
|
||||
field: (v) => v.product.code,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
align: 'center',
|
||||
label: 'productService.service.list',
|
||||
label: 'quotation.productList',
|
||||
field: (v) => v.product.name,
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'center',
|
||||
label: 'general.amount',
|
||||
label: 'taskOrder.amountOfEmployee',
|
||||
field: 'amount',
|
||||
},
|
||||
{
|
||||
|
|
@ -97,15 +116,15 @@ const columns = [
|
|||
field: 'priceBeforeVat',
|
||||
},
|
||||
{
|
||||
name: 'tax',
|
||||
name: 'vat',
|
||||
align: 'center',
|
||||
label: 'general.vat',
|
||||
field: 'tax',
|
||||
field: 'vat',
|
||||
},
|
||||
{
|
||||
name: 'sumPrice',
|
||||
align: 'right',
|
||||
label: 'quotation.sumPrice',
|
||||
label: 'quotation.totalPriceBaht',
|
||||
field: 'sumPrice',
|
||||
},
|
||||
] satisfies QTableProps['columns'];
|
||||
|
|
@ -193,24 +212,63 @@ function handleCheck(
|
|||
index: number,
|
||||
data: QuotationPayload['productServiceList'][number],
|
||||
) {
|
||||
const equals = data.amount === data.workerIndex.length;
|
||||
const target = data.workerIndex.indexOf(index);
|
||||
if (target > -1) {
|
||||
data.workerIndex.splice(target, 1);
|
||||
if (equals) data.amount -= 1;
|
||||
} else {
|
||||
data.workerIndex.push(index);
|
||||
if (equals) data.amount += 1;
|
||||
}
|
||||
data.amount = Math.max(data.workerIndex.length, data.amount);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.employeeRows,
|
||||
(a, b) => {
|
||||
if (a === undefined || b === undefined) return;
|
||||
if (a.length < b.length) {
|
||||
rows.value.forEach((p) => {
|
||||
const maxValue = Math.max(...p.workerIndex);
|
||||
p.workerIndex = p.workerIndex.filter((i) => i !== maxValue);
|
||||
});
|
||||
}
|
||||
(current, before) => {
|
||||
if (current === undefined || before === undefined) return;
|
||||
|
||||
rows.value.forEach((items) => {
|
||||
const mapping = items.workerIndex.map((v) => before[v]);
|
||||
const incoming = current.filter(
|
||||
(lhs) =>
|
||||
!before.find((rhs) => {
|
||||
console.log(lhs, rhs);
|
||||
return JSON.stringify(lhs) === JSON.stringify(rhs);
|
||||
}),
|
||||
);
|
||||
const selected = mapping.concat(incoming);
|
||||
|
||||
items.workerIndex = selected
|
||||
.map((lhs) =>
|
||||
current.findIndex((rhs) => {
|
||||
return JSON.stringify(lhs) === JSON.stringify(rhs);
|
||||
}),
|
||||
)
|
||||
.filter((v) => v !== -1);
|
||||
items.amount = items.workerIndex.length;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.maxInstallment,
|
||||
() => {
|
||||
if (!props.maxInstallment) return;
|
||||
let test: ProductServiceList[] = [];
|
||||
const items = groupByServiceId(
|
||||
rows.value.map((v, i) => Object.assign(v, { i })),
|
||||
) || [{ title: '', product: [] }];
|
||||
|
||||
items.forEach((p) => {
|
||||
test = test.concat(p.product.flatMap((item) => item));
|
||||
});
|
||||
test.forEach((p) => {
|
||||
if ((props.maxInstallment || 0) < (p.installmentNo || 0)) {
|
||||
p.installmentNo = Number(props.maxInstallment);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
|
@ -224,26 +282,24 @@ watch(
|
|||
:key="i"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<div
|
||||
v-if="item.title && !item.title.includes('_product')"
|
||||
class="q-py-sm q-px-md bordered"
|
||||
style="background: hsla(var(--orange-5-hsl) / 0.1)"
|
||||
>
|
||||
<q-avatar
|
||||
class="q-mr-lg"
|
||||
style="background: var(--orange-5); color: var(--surface-1)"
|
||||
size="sm"
|
||||
>
|
||||
<q-icon size="xs" name="mdi-server-outline" />
|
||||
</q-avatar>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
hide-pagination
|
||||
:columns="columns"
|
||||
:columns="
|
||||
installmentInput
|
||||
? [
|
||||
...columns.slice(0, 1),
|
||||
{
|
||||
name: 'periodNo',
|
||||
align: 'left',
|
||||
label: 'quotation.periodNo',
|
||||
field: (v) => v.product.code,
|
||||
},
|
||||
...columns.slice(1),
|
||||
]
|
||||
: columns
|
||||
"
|
||||
:rows="item.product"
|
||||
class="full-width"
|
||||
:no-data-label="$t('general.noDataTable')"
|
||||
|
|
@ -260,17 +316,51 @@ watch(
|
|||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template #body="props">
|
||||
<template
|
||||
#body="props: {
|
||||
row: Required<QuotationPayload['productServiceList'][number]>;
|
||||
} & Omit<Parameters<QTableSlots['body']>[0], 'row'>"
|
||||
>
|
||||
<q-tr>
|
||||
<q-td class="text-center">{{ props.rowIndex + 1 }}</q-td>
|
||||
<q-td>{{ props.row.product.code }}</q-td>
|
||||
<q-td v-if="installmentInput">
|
||||
<q-input
|
||||
:readonly
|
||||
:bg-color="readonly ? 'transparent' : ''"
|
||||
dense
|
||||
min="1"
|
||||
:max="maxInstallment"
|
||||
outlined
|
||||
input-class="text-right"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
:model-value="props.row.installmentNo"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
$emit('updateTable', props.row, {
|
||||
newInstallmentNo: Number(v),
|
||||
});
|
||||
props.row.installmentNo = Number(v);
|
||||
}
|
||||
"
|
||||
></q-input>
|
||||
</q-td>
|
||||
<q-td class="text-center">{{ props.row.product.code }}</q-td>
|
||||
<q-td style="width: 100%">
|
||||
<q-avatar class="q-mr-sm" size="md">
|
||||
<q-icon
|
||||
class="full-width full-height"
|
||||
name="mdi-shopping-outline"
|
||||
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
|
||||
/>
|
||||
<q-img
|
||||
class="text-center"
|
||||
:ratio="1"
|
||||
:src="`${baseUrl}/product/${props.row.product.id}/image/${props.row.product.selectedImage}`"
|
||||
>
|
||||
<template #error>
|
||||
<q-icon
|
||||
class="full-width full-height"
|
||||
name="mdi-shopping-outline"
|
||||
:style="`color: var(--teal-10); background: hsla(var(--teal-${$q.dark.isActive ? '8' : '10'}-hsl)/0.15)`"
|
||||
/>
|
||||
</template>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
{{ props.row.product.name }}
|
||||
</q-td>
|
||||
|
|
@ -281,10 +371,16 @@ watch(
|
|||
dense
|
||||
outlined
|
||||
:type="readonly ? 'text' : 'number'"
|
||||
input-class="text-center"
|
||||
style="width: 70px"
|
||||
min="0"
|
||||
min="1"
|
||||
debounce="500"
|
||||
v-model="props.row.amount"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
$emit('updateTable', props.row);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td align="right">
|
||||
|
|
@ -307,21 +403,30 @@ watch(
|
|||
outlined
|
||||
input-class="text-right"
|
||||
style="width: 90px"
|
||||
debounce="500"
|
||||
:model-value="
|
||||
commaInput(props.row.discount.toString() || '0')
|
||||
discount4Show[props.rowIndex] ||
|
||||
commaInput(props.row.discount?.toString() || '0')
|
||||
"
|
||||
@blur="
|
||||
() => {
|
||||
props.row.discount = Number(
|
||||
discount4Show[props.rowIndex].replace(/,/g, ''),
|
||||
);
|
||||
if (props.row.discount % 1 === 0) {
|
||||
const [, dec] =
|
||||
discount4Show[props.rowIndex].split('.');
|
||||
if (!dec) {
|
||||
discount4Show[props.rowIndex] += '.00';
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (typeof v === 'string')
|
||||
discount4Show[props.rowIndex] = commaInput(v);
|
||||
const x = parseFloat(
|
||||
discount4Show[props.rowIndex] &&
|
||||
typeof discount4Show[props.rowIndex] === 'string'
|
||||
? discount4Show[props.rowIndex].replace(/,/g, '')
|
||||
: '',
|
||||
discount4Show[props.rowIndex] = commaInput(
|
||||
v?.toString() || '0',
|
||||
'string',
|
||||
);
|
||||
props.row.discount = x;
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
|
@ -347,10 +452,7 @@ watch(
|
|||
{{ formatNumberDecimal(calcPrice(props.row), 2) }}
|
||||
</q-td>
|
||||
<q-td>
|
||||
<div
|
||||
class="row items-center full-width justify-end no-wrap"
|
||||
v-if="!readonly"
|
||||
>
|
||||
<div class="row items-center full-width justify-end no-wrap">
|
||||
<q-btn
|
||||
@click.stop="openEmployeeTable(item.title, props.rowIndex)"
|
||||
dense
|
||||
|
|
@ -377,8 +479,10 @@ watch(
|
|||
background-color: hsla(var(--positive-bg) / 0.1);
|
||||
color: hsl(var(--positive-bg));
|
||||
"
|
||||
@click="$emit('viewFile', props.row.product)"
|
||||
/>
|
||||
<DeleteButton
|
||||
v-if="!readonly"
|
||||
iconOnly
|
||||
@click="$emit('delete', props.row.i)"
|
||||
/>
|
||||
|
|
@ -395,8 +499,9 @@ watch(
|
|||
>
|
||||
<q-td colspan="100%" style="padding: 16px">
|
||||
<WorkerItem
|
||||
:readonly
|
||||
:checkable="!readonly"
|
||||
@check="(wokerIndex) => handleCheck(wokerIndex, props.row)"
|
||||
checkable
|
||||
fallback-img="/images/employee-avatar.png"
|
||||
inTable
|
||||
hideQuantity
|
||||
|
|
|
|||
|
|
@ -1,24 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { formatNumberDecimal } from 'src/stores/utils';
|
||||
import BadgeComponent from 'components/BadgeComponent.vue';
|
||||
import KebabAction from '../shared/KebabAction.vue';
|
||||
import MainButton from '../button/MainButton.vue';
|
||||
|
||||
defineProps<{
|
||||
type?:
|
||||
| 'fullAmountCash'
|
||||
| 'installmentsCash'
|
||||
| 'fullAmountBill'
|
||||
| 'installmentsBill'
|
||||
| string;
|
||||
title?: string;
|
||||
code?: string;
|
||||
amount?: number;
|
||||
date?: string;
|
||||
status?: string;
|
||||
workerCount?: number;
|
||||
workerMax?: number;
|
||||
createdAt?: string;
|
||||
validUntil?: string;
|
||||
customerName?: string;
|
||||
reporter?: string;
|
||||
totalPrice?: number;
|
||||
urgent?: boolean;
|
||||
hidePreview?: boolean;
|
||||
badgeColor?: string;
|
||||
hideKebabView?: boolean;
|
||||
hideKebabEdit?: boolean;
|
||||
hideAction?: boolean;
|
||||
|
||||
customData?: {
|
||||
label: string;
|
||||
value: string | number | unknown;
|
||||
slotName?: string;
|
||||
}[];
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
|
|
@ -30,52 +40,62 @@ defineEmits<{
|
|||
(e: 'example'): void;
|
||||
(e: 'preview'): void;
|
||||
}>();
|
||||
|
||||
const rand = Math.random();
|
||||
</script>
|
||||
<template>
|
||||
<div class="surface-1 rounded bordered q-pa-sm quo-card">
|
||||
<div
|
||||
class="surface-1 rounded q-pa-sm quo-card bordered"
|
||||
:class="{ 'urgent-card': urgent }"
|
||||
:style="{ '--animation-delay': rand + 's' }"
|
||||
>
|
||||
<!-- SEC: header -->
|
||||
<header class="row items-center no-wrap">
|
||||
<div
|
||||
class="badge-card rounded q-pa-xs"
|
||||
:class="{ [`badge-card__${type}`]: true }"
|
||||
>
|
||||
{{ $t(`quotation.type.${type}`) }}
|
||||
<div v-if="urgent" class="q-mr-sm" style="font-size: 90%">
|
||||
<BadgeComponent
|
||||
icon="mdi-fire"
|
||||
:title="$t('general.urgent2')"
|
||||
hsla-color="--gray-1-hsl"
|
||||
hsla-background="--red-8-hsl"
|
||||
solid
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="column q-ml-md relative-position" style="font-size: 12px">
|
||||
<span>
|
||||
{{ $t('general.itemNo', { msg: $t('quotation.title') }) }}
|
||||
</span>
|
||||
<span
|
||||
class="text-caption row items-center"
|
||||
:class="urgent ? 'urgent' : 'app-text-muted'"
|
||||
style="top: 10px"
|
||||
>
|
||||
{{ code }}
|
||||
<q-icon
|
||||
v-if="urgent"
|
||||
name="mdi-fire"
|
||||
size="xs"
|
||||
style="position: absolute; top: 7px"
|
||||
></q-icon>
|
||||
</span>
|
||||
<div class="q-mr-sm" style="font-size: 90%">
|
||||
<BadgeComponent
|
||||
:title="status"
|
||||
:hsla-color="badgeColor || '--blue-6-hsl'"
|
||||
:border="urgent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav class="col text-right">
|
||||
<q-btn
|
||||
v-if="!hidePreview"
|
||||
flat
|
||||
dense
|
||||
rounded
|
||||
icon="mdi-play-box-outline"
|
||||
size="12px"
|
||||
:title="$t('preview.doc')"
|
||||
@click.stop="$emit('preview')"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
rounded
|
||||
icon="mdi-eye-outline"
|
||||
size="12px"
|
||||
:title="$t('general.viewDetail')"
|
||||
@click.stop="$emit('view')"
|
||||
/>
|
||||
<KebabAction
|
||||
v-if="!hideAction"
|
||||
:idName="code"
|
||||
status="ACTIVE"
|
||||
hide-toggle
|
||||
use-link
|
||||
use-upload
|
||||
hide-delete
|
||||
:hide-view="hideKebabView"
|
||||
:hide-edit="hideKebabEdit"
|
||||
@view="$emit('view')"
|
||||
@edit="$emit('edit')"
|
||||
@link="$emit('link')"
|
||||
|
|
@ -85,48 +105,87 @@ defineEmits<{
|
|||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="ellipsis q-px-xs">
|
||||
<b>{{ title || '-' }}</b>
|
||||
<q-tooltip anchor="bottom start" self="top left">
|
||||
{{ title || '-' }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<div class="ellipsis q-px-xs q-mb-sm app-text-muted" style="font-size: 80%">
|
||||
{{ code || '-' }}
|
||||
<q-tooltip anchor="bottom start" self="top left">
|
||||
{{ title || '-' }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- SEC: body -->
|
||||
<section class="row no-wrap q-py-md">
|
||||
<q-img src="/images/quotation-avatar.png" width="4rem" class="q-mr-lg" />
|
||||
<div class="column">
|
||||
<span class="col q-pt-sm">{{ title || '-' }}</span>
|
||||
<span class="app-text-muted">x {{ amount || '0' }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="col text-right app-text-muted q-mr-md self-end"
|
||||
style="font-size: 12px"
|
||||
<section
|
||||
class="rounded q-px-sm"
|
||||
:class="{
|
||||
'surface-1': urgent,
|
||||
'surface-2': !urgent,
|
||||
}"
|
||||
>
|
||||
<article
|
||||
class="q-py-sm"
|
||||
:class="{
|
||||
row: $q.screen.gt.sm,
|
||||
column: $q.screen.lt.sm,
|
||||
}"
|
||||
v-if="customData && customData?.length > 0"
|
||||
>
|
||||
{{ date || '-' }}
|
||||
</div>
|
||||
<template v-for="cData in customData" :key="cData.label">
|
||||
<template v-if="cData.slotName">
|
||||
<slot :name="cData.slotName" :props="cData" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="col-4 app-text-muted q-pr-sm">
|
||||
{{ cData.label || '-' }}
|
||||
</div>
|
||||
<div class="col-8">{{ cData.value || '-' }}</div>
|
||||
</template>
|
||||
</template>
|
||||
</article>
|
||||
|
||||
<article v-else class="row q-py-sm">
|
||||
<div class="col-4 app-text-muted q-pr-sm">
|
||||
{{ $t('quotation.customerName') }}
|
||||
</div>
|
||||
<div class="col-8">{{ customerName || '-' }}</div>
|
||||
<div class="col-4 app-text-muted q-pr-sm">
|
||||
{{ $t('quotation.actor') }}
|
||||
</div>
|
||||
<div class="col-8">{{ reporter || '-' }}</div>
|
||||
<div class="col-4 app-text-muted q-pr-sm">
|
||||
{{ $t('quotation.employee') }}
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<BadgeComponent
|
||||
:hsla-color="badgeColor"
|
||||
icon="mdi-account-multiple-outline"
|
||||
:title="[workerCount, workerMax].join(' / ')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4 app-text-muted q-pr-sm">
|
||||
{{ $t('general.createdAt') }}
|
||||
</div>
|
||||
<div class="col-8">
|
||||
{{ createdAt }}
|
||||
</div>
|
||||
<div class="col-4 app-text-muted q-pr-sm">
|
||||
{{ $t('general.validUntil') }}
|
||||
</div>
|
||||
<div class="col-8">
|
||||
{{ validUntil }}
|
||||
</div>
|
||||
<div class="col-4 app-text-muted q-pr-sm">
|
||||
{{ $t('quotation.totalPrice') }}
|
||||
</div>
|
||||
<div class="col-8">
|
||||
{{ formatNumberDecimal(totalPrice || 0, 2) }}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<q-separator />
|
||||
<section class="row q-py-sm">
|
||||
<div class="col-3 app-text-muted">{{ $t('quotation.customerName') }}</div>
|
||||
<div class="col-9">{{ customerName || '-' }}</div>
|
||||
<div class="col-3 app-text-muted">{{ $t('quotation.actor') }}</div>
|
||||
<div class="col-9">{{ reporter || '-' }}</div>
|
||||
</section>
|
||||
<q-separator />
|
||||
<footer class="row no-wrap items-center q-mt-sm" style="text-wrap: nowrap">
|
||||
<Icon
|
||||
class="q-mr-xs"
|
||||
icon="ph:money-fill"
|
||||
style="font-size: 24px; color: var(--green-9)"
|
||||
/>
|
||||
{{ $t('quotation.totalPriceBaht') }} :
|
||||
<div class="q-ml-xs" style="color: var(--orange-5)">
|
||||
{{ formatNumberDecimal(totalPrice || 0, 2) }}
|
||||
</div>
|
||||
<MainButton
|
||||
outlined
|
||||
icon="mdi-play-box-outline"
|
||||
color="207 96% 32%"
|
||||
class="q-ml-auto"
|
||||
@click="$emit('preview')"
|
||||
>
|
||||
{{ $t('general.view', { msg: $t('general.example') }) }}
|
||||
</MainButton>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -164,7 +223,37 @@ span {
|
|||
height: 12px;
|
||||
}
|
||||
|
||||
.urgent {
|
||||
color: hsl(var(--red-6-hsl));
|
||||
.urgent-card {
|
||||
--_color: var(--red-7-hsl);
|
||||
background-color: hsla(var(--red-7-hsl) / 0.07) !important;
|
||||
border: 0.5px solid var(--red-6) !important;
|
||||
animation: status 1s infinite;
|
||||
animation-delay: var(--animation-delay);
|
||||
|
||||
.code {
|
||||
color: var(--red-6);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 12px;
|
||||
border-radius: var(--radius-2);
|
||||
background: hsl(var(--red-7-hsl));
|
||||
color: white;
|
||||
-webkit-box-shadow: 0px 0px 6px 0px rgba(240, 62, 62, 1);
|
||||
-moz-box-shadow: 0px 0px 6px 0px rgba(240, 62, 62, 1);
|
||||
box-shadow: 0px 0px 6px 0px rgba(240, 62, 62, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes status {
|
||||
0% {
|
||||
box-shadow: 0x 0px 0px hsla(var(--_color) / 1);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0px 0px 1px 4px hsla(var(--_color) / 0.3);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0px 0px 4px 12px hsla(var(--_color) / 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
192
src/components/05_quotation/TableQuotation.vue
Normal file
192
src/components/05_quotation/TableQuotation.vue
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<script lang="ts" setup>
|
||||
import { QTableProps } from 'quasar';
|
||||
import { dateFormat } from 'src/utils/datetime';
|
||||
|
||||
import { formatNumberDecimal } from 'stores/utils';
|
||||
|
||||
import BadgeComponent from 'components/BadgeComponent.vue';
|
||||
import KebabAction from 'components/shared/KebabAction.vue';
|
||||
|
||||
import { hslaColors } from 'src/pages/05_quotation/constants';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
rows: QTableProps['rows'];
|
||||
columns: QTableProps['columns'];
|
||||
grid?: boolean;
|
||||
visibleColumns?: string[];
|
||||
hideEdit?: boolean;
|
||||
}>(),
|
||||
{
|
||||
row: () => [],
|
||||
column: () => [],
|
||||
grid: false,
|
||||
visibleColumns: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
function payCondition(value: string) {
|
||||
if (value === 'Full') return 'quotation.type.fullAmountCash';
|
||||
if (value === 'Split') return 'quotation.type.installmentsCash';
|
||||
if (value === 'SplitCustom') return 'quotation.type.installmentsCustomCash';
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
defineEmits<{
|
||||
(e: 'preview', data: any): void;
|
||||
(e: 'view', data: any): void;
|
||||
(e: 'edit', data: any): void;
|
||||
(e: 'delete', data: any): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-table
|
||||
v-bind="props"
|
||||
flat
|
||||
hide-pagination
|
||||
card-container-class="q-col-gutter-sm"
|
||||
:rows-per-page-options="[0]"
|
||||
class="full-width"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
style="background-color: hsla(var(--info-bg) / 0.07)"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label && $t(col.label) }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr :class="{ urgent: props.row.urgent }">
|
||||
<q-td v-if="visibleColumns.includes('order')">
|
||||
{{ props.rowIndex + 1 }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="visibleColumns.includes('workName')">
|
||||
<div class="column">
|
||||
<div class="col-6">{{ props.row.workName }}</div>
|
||||
<div class="col-6 app-text-muted">{{ props.row.code }}</div>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="visibleColumns.includes('createdAt')">
|
||||
{{ dateFormat(props.row.createdAt) }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="visibleColumns.includes('dueDate')">
|
||||
{{ dateFormat(props.row.dueDate) }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="visibleColumns.includes('contactName')">
|
||||
{{ props.row.contactName }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="visibleColumns.includes('actor')">
|
||||
{{ props.row.createdBy.firstName }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="visibleColumns.includes('summaryPrice')">
|
||||
{{ formatNumberDecimal(props.row.finalPrice, 2) }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="visibleColumns.includes('payCondition')">
|
||||
{{ $t(payCondition(props.row.payCondition)) }}
|
||||
</q-td>
|
||||
|
||||
<q-td v-if="visibleColumns.includes('status')">
|
||||
<div style="min-width: 150px">
|
||||
<BadgeComponent
|
||||
:title="$t(`quotation.status.${props.row.quotationStatus}`)"
|
||||
:hsla-color="hslaColors[props.row.quotationStatus] || ''"
|
||||
/>
|
||||
|
||||
<BadgeComponent
|
||||
class="q-ml-xs"
|
||||
icon="mdi-fire"
|
||||
v-if="props.row.urgent"
|
||||
:title="$t('general.urgent2')"
|
||||
hsla-color="--gray-1-hsl"
|
||||
hsla-background="--red-8-hsl"
|
||||
solid
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td class="text-right">
|
||||
<q-btn
|
||||
:id="`btn-eye-${props.row.firstName}`"
|
||||
icon="mdi-play-box-outline"
|
||||
size="sm"
|
||||
dense
|
||||
round
|
||||
flat
|
||||
@click.stop="$emit('preview', props.row.id)"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
:id="`btn-eye-${props.row.firstName}`"
|
||||
icon="mdi-eye-outline"
|
||||
size="sm"
|
||||
dense
|
||||
round
|
||||
flat
|
||||
@click.stop="$emit('view', props.row)"
|
||||
/>
|
||||
|
||||
<KebabAction
|
||||
:idName="`btn-kebab-${props.row.firstName}`"
|
||||
status="'ACTIVE'"
|
||||
hide-toggle
|
||||
hide-delete
|
||||
:hide-edit="hideEdit"
|
||||
@view="$emit('view', props.row)"
|
||||
@edit="$emit('edit', props.row)"
|
||||
@delete="$emit('delete', props.row.id)"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="props">
|
||||
<slot name="grid" :item="props" />
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.q-table tr.urgent {
|
||||
background: hsla(var(--red-6-hsl) / 0.03);
|
||||
}
|
||||
|
||||
.q-table tr.urgent td:first-child {
|
||||
&::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 15%;
|
||||
bottom: 15%;
|
||||
background: var(--red-8);
|
||||
width: 4px;
|
||||
border-radius: 99rem;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
background: var(--red-8);
|
||||
}
|
||||
50% {
|
||||
background: var(--red-3);
|
||||
}
|
||||
100% {
|
||||
background: var(--red-8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
<script lang="ts" setup>
|
||||
import { QTableProps } from 'quasar';
|
||||
import TableComponents from 'src/components/TableComponents.vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const employeeAmount = defineModel<number>('employeeAmount', { default: 1 });
|
||||
|
||||
defineEmits<{
|
||||
(e: 'delete', index: number): void;
|
||||
(e: 'check', index: number): void;
|
||||
}>();
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
employeeAmount?: number;
|
||||
fallbackImg?: string;
|
||||
hideQuantity?: boolean;
|
||||
checkable?: boolean;
|
||||
|
|
@ -24,13 +25,12 @@ withDefaults(
|
|||
age: string;
|
||||
nationality: string;
|
||||
documentExpireDate: string;
|
||||
imgUrl: string;
|
||||
imgUrl?: string;
|
||||
status: string;
|
||||
}[];
|
||||
}>(),
|
||||
{
|
||||
rows: () => [],
|
||||
employeeAmount: 0,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -86,6 +86,14 @@ const columns = [
|
|||
field: 'action',
|
||||
},
|
||||
] satisfies QTableProps['columns'];
|
||||
|
||||
watch(
|
||||
() => props.rows,
|
||||
() => {
|
||||
if (props.readonly) return;
|
||||
employeeAmount.value = props.rows.length;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
|
|
@ -110,18 +118,27 @@ const columns = [
|
|||
]
|
||||
: columns
|
||||
"
|
||||
:rows
|
||||
:rows="
|
||||
readonly && inTable
|
||||
? rows.filter((_, index) => checkList.includes(index))
|
||||
: rows
|
||||
"
|
||||
hidePagination
|
||||
:customColumn="['check']"
|
||||
@delete="(i) => $emit('delete', i)"
|
||||
>
|
||||
<template v-slot:img-column="{ props }">
|
||||
<q-avatar class="q-mr-sm" size="md">
|
||||
<q-img :src="props.row.imgUrl" class="full-height full-width">
|
||||
<q-img
|
||||
v-if="props.row.imgUrl"
|
||||
:src="props.row.imgUrl"
|
||||
class="full-height full-width"
|
||||
>
|
||||
<template #error>
|
||||
<q-img :src="fallbackImg" :ratio="1" />
|
||||
</template>
|
||||
</q-img>
|
||||
<q-img v-else :src="fallbackImg" :ratio="1" />
|
||||
<div
|
||||
class="absolute-bottom-right"
|
||||
style="
|
||||
|
|
@ -157,15 +174,23 @@ const columns = [
|
|||
</TableComponents>
|
||||
|
||||
<div v-if="!hideQuantity" class="row q-pt-md items-center">
|
||||
<span class="q-ml-auto">
|
||||
<span class="q-ml-auto q-mr-sm">
|
||||
{{ $t('general.numberOf', { msg: $t('quotation.employee') }) }}
|
||||
</span>
|
||||
<div
|
||||
class="surface-tab bordered rounded flex items-center justify-center q-mx-md"
|
||||
style="width: 30px; height: 30px"
|
||||
>
|
||||
{{ employeeAmount || '0' }}
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
for="worker-count"
|
||||
dense
|
||||
outlined
|
||||
style="width: 67px"
|
||||
:type="readonly ? 'text' : 'number'"
|
||||
class="col-1"
|
||||
input-class="text-center"
|
||||
:readonly
|
||||
hide-bottom-space
|
||||
:min="rows.length"
|
||||
v-model="employeeAmount"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts" setup>
|
||||
import SelectInput from '../shared/SelectInput.vue';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
|
||||
defineProps<{
|
||||
dense?: boolean;
|
||||
outlined?: boolean;
|
||||
readonly?: boolean;
|
||||
onDrawer?: boolean;
|
||||
}>();
|
||||
|
||||
const group = defineModel('group', { default: '' });
|
||||
const name = defineModel('name', { default: '' });
|
||||
const nameEn = defineModel('nameEn', { default: '' });
|
||||
|
||||
type Options = { label: string; value: string };
|
||||
</script>
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<div class="col-12 q-pb-sm row items-center">
|
||||
<q-icon
|
||||
flat
|
||||
size="xs"
|
||||
class="q-pa-sm rounded q-mr-sm"
|
||||
color="info"
|
||||
name="mdi-office-building-outline"
|
||||
style="background-color: var(--surface-3)"
|
||||
/>
|
||||
<span class="text-body1 text-weight-bold">
|
||||
{{ $t(`form.field.basicInformation`) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 row q-col-gutter-sm">
|
||||
<SelectInput
|
||||
:disable="!readonly && onDrawer"
|
||||
:readonly="readonly"
|
||||
for="input-agencies-code"
|
||||
:label="$t('agencies.group')"
|
||||
option-label="value"
|
||||
:option="
|
||||
optionStore.globalOption?.agenciesType.sort(
|
||||
(lhs: Options, rhs: Options) => lhs.value.localeCompare(rhs.value),
|
||||
)
|
||||
"
|
||||
v-model="group"
|
||||
>
|
||||
<template v-slot:option="{ scope, opt }">
|
||||
<q-item v-bind="scope.itemProps" clickable>
|
||||
{{ opt.value }} ({{ opt.label }})
|
||||
</q-item>
|
||||
</template>
|
||||
</SelectInput>
|
||||
|
||||
<q-input
|
||||
for="input-agencies-name"
|
||||
dense
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
:label="$t('agencies.name')"
|
||||
v-model="name"
|
||||
:rules="[(val: string) => !!val || $t('form.error.required')]"
|
||||
/>
|
||||
<q-input
|
||||
for="input-agencies-name-en"
|
||||
dense
|
||||
outlined
|
||||
:readonly="readonly"
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
:label="'Agencies Name'"
|
||||
v-model="nameEn"
|
||||
:rules="[
|
||||
(val: string) => !!val || $t('form.error.required'),
|
||||
(val: string) =>
|
||||
/^[A-Za-z0-9.,' -]+$/.test(val) || $t('form.error.letterOnly'),
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
68
src/components/08_request-list/DataDisplay.vue
Normal file
68
src/components/08_request-list/DataDisplay.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
defineEmits<{ (e: 'labelClick', value: string, index: number): void }>();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label: string;
|
||||
value?: string | string[];
|
||||
icon?: string;
|
||||
iconSize?: string;
|
||||
tooltip?: boolean;
|
||||
clickable?: boolean;
|
||||
}>(),
|
||||
{
|
||||
label: '-',
|
||||
value: '-',
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="row items-center">
|
||||
<Icon
|
||||
v-if="icon"
|
||||
:icon
|
||||
class="app-text-muted q-pr-sm"
|
||||
:width="iconSize || '2rem'"
|
||||
/>
|
||||
<span class="row col">
|
||||
<span class="col-12 app-text-muted-2" style="font-size: 10px">
|
||||
{{ label }}
|
||||
</span>
|
||||
<span class="col-12 ellipsis">
|
||||
<slot name="value">
|
||||
<span
|
||||
:class="{ 'link cursor-pointer': clickable }"
|
||||
v-if="typeof value === 'string'"
|
||||
@click="$emit('labelClick', value, null)"
|
||||
>
|
||||
{{ value }}
|
||||
<q-tooltip v-if="tooltip" :delay="500">{{ value }}</q-tooltip>
|
||||
</span>
|
||||
<span v-else :class="{ 'link cursor-pointer': clickable }">
|
||||
<span
|
||||
v-for="(item, index) in value"
|
||||
:key="index"
|
||||
@click="$emit('labelClick', item, index)"
|
||||
class="link cursor-pointer"
|
||||
>
|
||||
{{ item }}
|
||||
<span v-if="index < value.length - 1">, </span>
|
||||
|
||||
<q-tooltip v-if="tooltip" :delay="500">{{ item }}</q-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</slot>
|
||||
</span>
|
||||
</span>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.link {
|
||||
color: hsl(var(--info-bg));
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
120
src/components/08_request-list/PropertiesToInput.vue
Normal file
120
src/components/08_request-list/PropertiesToInput.vue
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
PropDate,
|
||||
PropNumber,
|
||||
PropOptions,
|
||||
PropString,
|
||||
} from 'src/stores/product-service/types';
|
||||
|
||||
import SelectInput from '../shared/SelectInput.vue';
|
||||
import DatePicker from '../shared/DatePicker.vue';
|
||||
|
||||
defineProps<{
|
||||
prop: PropString | PropNumber | PropDate | PropOptions;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
disable?: boolean;
|
||||
}>();
|
||||
|
||||
const model = defineModel<string | number | null | undefined>();
|
||||
|
||||
function numberDisplay(prop: PropNumber, data: number): string {
|
||||
if (isNaN(data)) data = 0;
|
||||
|
||||
let formattedNumber: string;
|
||||
|
||||
if (prop.comma && prop.decimal) {
|
||||
formattedNumber = data.toLocaleString('en-US', {
|
||||
minimumFractionDigits: prop.decimalPlace,
|
||||
maximumFractionDigits: prop.decimalPlace,
|
||||
});
|
||||
} else if (prop.comma && !prop.decimal) {
|
||||
formattedNumber = data.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
} else if (!prop.comma && prop.decimal) {
|
||||
formattedNumber = data.toFixed(prop.decimalPlace);
|
||||
} else {
|
||||
formattedNumber = Math.round(data).toString();
|
||||
}
|
||||
|
||||
return formattedNumber;
|
||||
}
|
||||
|
||||
function numberParse(formatted: string, prop: PropNumber): number {
|
||||
let parsedNumber: number;
|
||||
|
||||
let cleanedFormatted = formatted.replace(/,/g, '');
|
||||
|
||||
if (prop.decimal) {
|
||||
parsedNumber = parseFloat(cleanedFormatted);
|
||||
} else {
|
||||
parsedNumber = parseInt(cleanedFormatted, 10);
|
||||
}
|
||||
|
||||
if (isNaN(parsedNumber)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parsedNumber;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-input
|
||||
v-if="prop.type === 'string'"
|
||||
:readonly
|
||||
:disable
|
||||
class="col-7"
|
||||
:model-value="readonly || disable ? model || '-' : model"
|
||||
dense
|
||||
outlined
|
||||
:placeholder
|
||||
:maxlength="prop.isPhoneNumber ? prop.phoneNumberLength : undefined"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
model = v;
|
||||
}
|
||||
"
|
||||
@focus="(e) => (e.target as HTMLInputElement).select()"
|
||||
/>
|
||||
<q-input
|
||||
v-if="prop.type === 'number'"
|
||||
:readonly
|
||||
:disable
|
||||
class="col-7"
|
||||
debounce="500"
|
||||
dense
|
||||
outlined
|
||||
:model-value="numberDisplay(prop, Number(model))"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
let cleanedFormatted = v?.toString().replace(/,/g, '');
|
||||
|
||||
const x = numberParse(
|
||||
numberDisplay(prop as PropNumber, Number(cleanedFormatted)),
|
||||
prop as PropNumber,
|
||||
);
|
||||
model = x;
|
||||
}
|
||||
"
|
||||
@focus="(e) => (e.target as HTMLInputElement).select()"
|
||||
/>
|
||||
<DatePicker
|
||||
v-if="prop.type === 'date'"
|
||||
:readonly
|
||||
:disabled="disable"
|
||||
class="col-7"
|
||||
v-model="model as string"
|
||||
/>
|
||||
<SelectInput
|
||||
v-if="prop.type === 'array'"
|
||||
:readonly
|
||||
:disable
|
||||
:label="$t('form.selection')"
|
||||
v-model="model as string"
|
||||
class="col-7"
|
||||
:option="prop.options.map((opt) => ({ label: opt, value: opt }))"
|
||||
/>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string;
|
||||
color?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="q-pl-md q-pr-sm row items-center justify-between rounded hover-item"
|
||||
:style="`border:1px solid ${color};color:${color}`"
|
||||
@click.stop="$emit('viewDetail')"
|
||||
>
|
||||
{{ $t(label) }}
|
||||
<q-icon name="mdi-arrow-right" class="q-ml-md" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hover-item:hover {
|
||||
background-color: var(--surface-3);
|
||||
}
|
||||
</style>
|
||||
46
src/components/BadgeComponent.vue
Normal file
46
src/components/BadgeComponent.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
defineProps<{
|
||||
icon?: string;
|
||||
title?: string;
|
||||
titleI18n?: string;
|
||||
hslaColor?: string;
|
||||
hslaBackground?: string;
|
||||
hslaBorder?: string;
|
||||
border?: boolean;
|
||||
solid?: boolean;
|
||||
transparency?: number;
|
||||
hideIcon?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="rounded q-px-sm flex-center"
|
||||
:style="{
|
||||
color: `hsla(var(${hslaColor || '--green-6-hsl'}) / 1)`,
|
||||
background: `hsla(var(${hslaBackground || hslaColor || '--green-6-hsl'}) / ${solid ? 1 : 0.15})`,
|
||||
border: border
|
||||
? `0.5px solid hsla(var(${hslaBorder || hslaBackground || hslaColor || '--green-6-hsl'}) / ${solid ? 1 : 0.15})`
|
||||
: undefined,
|
||||
}"
|
||||
>
|
||||
<Icon
|
||||
v-if="!hideIcon"
|
||||
:icon="icon || 'mdi-circle-medium'"
|
||||
style="margin-right: 0.25rem"
|
||||
/>
|
||||
|
||||
<slot name="label">
|
||||
{{ title || (!!titleI18n ? $t(titleI18n) : '-') }}
|
||||
</slot>
|
||||
|
||||
<slot name="append"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
display: inline-flex;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,8 +6,8 @@ import { ref } from 'vue';
|
|||
const props = defineProps<{
|
||||
action: (...args: unknown[]) => void;
|
||||
checkData: (...args: unknown[]) => {
|
||||
oldData: { filName: string; value: string }[];
|
||||
newData: { filName: string; value: string }[];
|
||||
oldData: { nameField: string; value: string }[];
|
||||
newData: { nameField: string; value: string }[];
|
||||
};
|
||||
cancel?: (...args: unknown[]) => void;
|
||||
}>();
|
||||
|
|
@ -81,7 +81,7 @@ onMounted(() => {
|
|||
|
||||
<div v-for="v in checkData().oldData">
|
||||
<span class="text-regular app-text-muted">
|
||||
{{ v.filName }}
|
||||
{{ v.nameField }}
|
||||
</span>
|
||||
: {{ v.value }}
|
||||
</div>
|
||||
|
|
@ -118,7 +118,7 @@ onMounted(() => {
|
|||
|
||||
<div v-for="v in checkData().newData">
|
||||
<span class="text-regular app-text-muted">
|
||||
{{ v.filName }}
|
||||
{{ v.nameField }}
|
||||
</span>
|
||||
: {{ v.value }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ defineProps<{
|
|||
edit?: boolean;
|
||||
hideFooter?: boolean;
|
||||
hideDelete?: boolean;
|
||||
hideBtn?: boolean;
|
||||
readonly?: boolean;
|
||||
|
||||
saveAmount?: number;
|
||||
|
|
@ -68,44 +69,46 @@ const currentTab = defineModel<string>('currentTab');
|
|||
}"
|
||||
>
|
||||
<div class="row items-center">
|
||||
<div v-if="!readonly && isEdit && edit" class="row">
|
||||
<q-btn
|
||||
round
|
||||
flat
|
||||
id="closeDialog"
|
||||
icon="mdi-arrow-left"
|
||||
padding="xs"
|
||||
class="q-mr-md"
|
||||
:class="{ dark: $q.dark.isActive }"
|
||||
style="color: var(--brand-1)"
|
||||
@click="undo"
|
||||
/>
|
||||
<div style="width: 31.98px"></div>
|
||||
</div>
|
||||
<div v-if="!readonly && !isEdit && edit">
|
||||
<q-btn
|
||||
round
|
||||
flat
|
||||
id="editDialog"
|
||||
icon="mdi-pencil-outline"
|
||||
padding="xs"
|
||||
class="q-mr-md"
|
||||
:class="{ dark: $q.dark.isActive }"
|
||||
style="color: var(--brand-1)"
|
||||
@click="editData"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="edit && !hideDelete"
|
||||
round
|
||||
flat
|
||||
id="deleteDialog"
|
||||
icon="mdi-trash-can-outline"
|
||||
padding="xs"
|
||||
:class="{ dark: $q.dark.isActive }"
|
||||
style="color: hsl(var(--negative-bg))"
|
||||
@click="deleteData"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="!hideBtn">
|
||||
<div v-if="!readonly && isEdit && edit" class="row">
|
||||
<q-btn
|
||||
round
|
||||
flat
|
||||
id="closeDialog"
|
||||
icon="mdi-arrow-left"
|
||||
padding="xs"
|
||||
class="q-mr-md"
|
||||
:class="{ dark: $q.dark.isActive }"
|
||||
style="color: var(--brand-1)"
|
||||
@click="undo"
|
||||
/>
|
||||
<div style="width: 31.98px"></div>
|
||||
</div>
|
||||
<div v-if="!readonly && !isEdit && edit">
|
||||
<q-btn
|
||||
round
|
||||
flat
|
||||
id="editDialog"
|
||||
icon="mdi-pencil-outline"
|
||||
padding="xs"
|
||||
class="q-mr-md"
|
||||
:class="{ dark: $q.dark.isActive }"
|
||||
style="color: var(--brand-1)"
|
||||
@click="editData"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="edit && !hideDelete"
|
||||
round
|
||||
flat
|
||||
id="deleteDialog"
|
||||
icon="mdi-trash-can-outline"
|
||||
padding="xs"
|
||||
:class="{ dark: $q.dark.isActive }"
|
||||
style="color: hsl(var(--negative-bg))"
|
||||
@click="deleteData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div style="width: 31.98px"></div>
|
||||
<div class="col text-subtitle1 text-weight-bold text-center">
|
||||
|
|
@ -140,18 +143,21 @@ const currentTab = defineModel<string>('currentTab');
|
|||
{{ badgeLabel }}
|
||||
</text>
|
||||
</div>
|
||||
<CancelButton
|
||||
icon-only
|
||||
id="btn-form-close"
|
||||
@click="
|
||||
() => {
|
||||
modal = beforeClose ? beforeClose() : !modal;
|
||||
close?.();
|
||||
}
|
||||
"
|
||||
type="reset"
|
||||
resetValidation
|
||||
/>
|
||||
<slot name="top-append" />
|
||||
<div>
|
||||
<CancelButton
|
||||
icon-only
|
||||
id="btn-form-close"
|
||||
@click="
|
||||
() => {
|
||||
modal = beforeClose ? beforeClose() : !modal;
|
||||
close?.();
|
||||
}
|
||||
"
|
||||
type="reset"
|
||||
resetValidation
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,11 @@ defineEmits<{
|
|||
style="z-index: 1; cursor: pointer"
|
||||
@mouseover="showOverlay = true"
|
||||
@mouseleave="showOverlay = false"
|
||||
@click.stop="$emit('view')"
|
||||
@click.stop="
|
||||
() => {
|
||||
if (readonly === false) $emit('view');
|
||||
}
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-if="img"
|
||||
|
|
@ -108,7 +112,11 @@ defineEmits<{
|
|||
>
|
||||
<div
|
||||
class="upload-overlay absolute-bottom flex items-center justify-center"
|
||||
@click.stop="$emit('edit')"
|
||||
@click.stop="
|
||||
() => {
|
||||
if (readonly === false) $emit('edit');
|
||||
}
|
||||
"
|
||||
>
|
||||
{{
|
||||
labelAction === undefined
|
||||
|
|
|
|||
|
|
@ -52,12 +52,14 @@ const onCreateData = defineModel<{
|
|||
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
|
||||
const reader = new FileReader();
|
||||
const inputFile = (() => {
|
||||
const { inputFile, resetInputFile } = (() => {
|
||||
const _form = document.createElement('form');
|
||||
const _element = document.createElement('input');
|
||||
_element.type = 'file';
|
||||
_element.accept = 'image/*';
|
||||
_element.addEventListener('change', change);
|
||||
return _element;
|
||||
_form.appendChild(_element);
|
||||
return { inputFile: _element, resetInputFile: () => _form.reset() };
|
||||
})();
|
||||
|
||||
const selectedImg = ref('');
|
||||
|
|
@ -83,6 +85,7 @@ reader.addEventListener('load', () => {
|
|||
});
|
||||
|
||||
function browse() {
|
||||
resetInputFile();
|
||||
inputFile?.click();
|
||||
}
|
||||
|
||||
|
|
@ -219,11 +222,7 @@ watch(
|
|||
unelevated
|
||||
round
|
||||
v-if="!changeDisabled"
|
||||
@click="
|
||||
() => {
|
||||
inputFile?.click();
|
||||
}
|
||||
"
|
||||
@click="browse"
|
||||
style="color: hsla(var(--stone-0-hsl) / 0.7)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ defineProps<{
|
|||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<q-img
|
||||
src="no-data.png"
|
||||
src="/no-data.png"
|
||||
:style="{
|
||||
height: size ? `${size}px` : '120px',
|
||||
width: size ? `${size + 2}px` : '123px',
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ withDefaults(
|
|||
active-design="outline"
|
||||
gutter="sm"
|
||||
boundary-numbers
|
||||
:max-pages="4"
|
||||
@update:model-value="fetchData"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
21
src/components/PaginationPageSize.vue
Normal file
21
src/components/PaginationPageSize.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
const pageSize = defineModel<number>({ required: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-btn-dropdown dense unelevated :label="pageSize" class="bordered q-pl-md">
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="v in [10, 30, 50, 100, 500, 1000]"
|
||||
:key="v"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="pageSize = v"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ v }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</template>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import ToggleButton from './button/ToggleButton.vue';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
defineProps<{
|
||||
img?: string | null;
|
||||
|
|
@ -12,6 +13,7 @@ defineProps<{
|
|||
toggleTitle?: string;
|
||||
fallbackImg?: string;
|
||||
fallbackCover?: string;
|
||||
prefix: string;
|
||||
|
||||
hideFade?: boolean;
|
||||
hideActive?: boolean;
|
||||
|
|
@ -47,7 +49,7 @@ const smallBanner = ref(false);
|
|||
</script>
|
||||
<template>
|
||||
<q-img
|
||||
v-if="!smallBanner"
|
||||
v-if="!smallBanner && $q.screen.gt.xs"
|
||||
fit="cover"
|
||||
class="cover rounded bordered relative-position"
|
||||
:style="`height: ${tabsList ? '180px' : '10vw'}`"
|
||||
|
|
@ -120,7 +122,7 @@ const smallBanner = ref(false);
|
|||
color: `${color || 'white'}`,
|
||||
}"
|
||||
>
|
||||
<q-icon :name="icon || 'mdi-account'" />
|
||||
<Icon :icon="icon || 'mdi-account'" />
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
|
|
@ -132,7 +134,7 @@ const smallBanner = ref(false);
|
|||
color: `${color || 'white'}`,
|
||||
}"
|
||||
>
|
||||
<q-icon :name="icon || 'mdi-account'" />
|
||||
<Icon :icon="icon || 'mdi-account'" />
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
|
|
@ -145,7 +147,7 @@ const smallBanner = ref(false);
|
|||
color: `${color || 'white'}`,
|
||||
}"
|
||||
>
|
||||
<q-icon :name="icon || 'mdi-account'" />
|
||||
<Icon :icon="icon || 'mdi-account'" />
|
||||
</div>
|
||||
<q-badge
|
||||
v-if="!hideActive"
|
||||
|
|
@ -167,7 +169,7 @@ const smallBanner = ref(false);
|
|||
|
||||
<Transition name="slide-fade">
|
||||
<div
|
||||
v-if="showOverlay && !readonly"
|
||||
v-if="showOverlay && !readonly && !noImageAction"
|
||||
class="absolute text-caption full-width full-height"
|
||||
style="border-radius: 50% 50%; overflow: hidden"
|
||||
:class="{ dark: $q.dark.isActive }"
|
||||
|
|
@ -200,7 +202,12 @@ const smallBanner = ref(false);
|
|||
{{ title }}
|
||||
</q-tooltip>
|
||||
</span>
|
||||
<span v-if="title" class="app-text-muted">{{ caption }}</span>
|
||||
<span
|
||||
v-if="title"
|
||||
:class="$q.dark.isActive ? 'foreground' : 'app-text-muted'"
|
||||
>
|
||||
{{ caption }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- icon -->
|
||||
|
|
@ -211,7 +218,8 @@ const smallBanner = ref(false);
|
|||
>
|
||||
<span
|
||||
v-if="useToggle && toggleTitle"
|
||||
class="q-mr-md app-text-muted-2"
|
||||
class="q-mr-md"
|
||||
:class="$q.dark.isActive ? 'foreground' : 'app-text-muted-2'"
|
||||
>
|
||||
{{ toggleTitle }}
|
||||
</span>
|
||||
|
|
@ -257,7 +265,7 @@ const smallBanner = ref(false);
|
|||
>
|
||||
<q-tab
|
||||
v-for="tab in tabsList"
|
||||
:id="`tab-${tab.label}`"
|
||||
:id="`${prefix}-tab-${tab.label}`"
|
||||
v-bind:key="tab.name"
|
||||
class="content-tab text-capitalize"
|
||||
:name="tab.name"
|
||||
|
|
@ -272,7 +280,7 @@ const smallBanner = ref(false);
|
|||
|
||||
<!-- small -->
|
||||
<q-img
|
||||
v-if="(!$q.screen.gt.sm && smallBanner) || smallBanner"
|
||||
v-if="(!$q.screen.gt.sm && smallBanner) || smallBanner || $q.screen.lt.sm"
|
||||
fit="cover"
|
||||
class="cover rounded bordered relative-position"
|
||||
:style="`height: 45px`"
|
||||
|
|
@ -332,9 +340,9 @@ const smallBanner = ref(false);
|
|||
color: `${color || 'white'}`,
|
||||
}"
|
||||
>
|
||||
<q-icon
|
||||
<Icon
|
||||
class="full-width full-height flex items-center justify-center"
|
||||
:name="icon || 'mdi-account'"
|
||||
:icon="icon || 'mdi-account'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -347,9 +355,9 @@ const smallBanner = ref(false);
|
|||
color: `${color || 'white'}`,
|
||||
}"
|
||||
>
|
||||
<q-icon
|
||||
<Icon
|
||||
class="full-width full-height flex items-center justify-center"
|
||||
:name="icon || 'mdi-account'"
|
||||
:icon="icon || 'mdi-account'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -363,9 +371,9 @@ const smallBanner = ref(false);
|
|||
color: `${color || 'white'}`,
|
||||
}"
|
||||
>
|
||||
<q-icon
|
||||
<Icon
|
||||
class="full-width full-height flex items-center justify-center"
|
||||
:name="icon || 'mdi-account'"
|
||||
:icon="icon || 'mdi-account'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -399,7 +407,8 @@ const smallBanner = ref(false);
|
|||
</span>
|
||||
<span
|
||||
v-if="title"
|
||||
class="app-text-muted absolute"
|
||||
class="absolute"
|
||||
:class="$q.dark.isActive ? 'foreground' : 'app-text-muted'"
|
||||
style="font-size: 10px; bottom: 4px"
|
||||
>
|
||||
{{ caption }}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,14 @@ const file = defineModel<File | null>('file', {
|
|||
});
|
||||
|
||||
const reader = new FileReader();
|
||||
const inputFile = (() => {
|
||||
const { inputFile, resetInputFile } = (() => {
|
||||
const _form = document.createElement('form');
|
||||
const _element = document.createElement('input');
|
||||
_element.type = 'file';
|
||||
_element.accept = 'image/*';
|
||||
_element.addEventListener('change', change);
|
||||
return _element;
|
||||
_form.appendChild(_element);
|
||||
return { inputFile: _element, resetInputFile: () => _form.reset() };
|
||||
})();
|
||||
|
||||
reader.addEventListener('load', () => {
|
||||
|
|
@ -48,6 +50,7 @@ reader.addEventListener('load', () => {
|
|||
});
|
||||
|
||||
function browse() {
|
||||
resetInputFile();
|
||||
inputFile?.click();
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +116,7 @@ async function downloadImage(url: string) {
|
|||
unelevated
|
||||
round
|
||||
v-if="!changeDisabled"
|
||||
@click="inputFile?.click()"
|
||||
@click="browse"
|
||||
style="color: hsla(var(--stone-0-hsl) / 0.7)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
|
|
@ -159,6 +162,7 @@ async function downloadImage(url: string) {
|
|||
v-close-popup
|
||||
/>
|
||||
<SaveButton
|
||||
:label="$t('general.confirm')"
|
||||
outlined
|
||||
@click="$emit('save', inputFile?.files?.[0] || null, imageUrl)"
|
||||
/>
|
||||
|
|
|
|||
7
src/components/SelectWorker.vue
Normal file
7
src/components/SelectWorker.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
defineProps<{}>();
|
||||
|
||||
const emit = defineEmits<{}>();
|
||||
</script>
|
||||
<template></template>
|
||||
<style scoped></style>
|
||||
|
|
@ -8,6 +8,7 @@ type Menu = {
|
|||
anchor: string;
|
||||
name: string;
|
||||
sub?: boolean;
|
||||
hideSubIndex?: boolean;
|
||||
tab?: string;
|
||||
useBtn?: boolean;
|
||||
};
|
||||
|
|
@ -98,10 +99,10 @@ onUnmounted(() => {
|
|||
class="row no-wrap items-center"
|
||||
:class="{ 'app-text-muted': v.sub && activeMenu !== v.anchor }"
|
||||
>
|
||||
<div v-if="v.sub" class="circle-2"></div>
|
||||
<div v-if="v.sub" class="circle-2 q-mr-md"></div>
|
||||
<div
|
||||
v-if="v.sub"
|
||||
class="surface-tab circle flex justify-center q-mx-md"
|
||||
v-if="v.sub && !v.hideSubIndex"
|
||||
class="surface-tab circle flex justify-center q-mr-md"
|
||||
>
|
||||
{{ menu.filter((v) => v.sub === true).indexOf(v) + 1 }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
branch: {
|
||||
|
|
@ -11,13 +13,16 @@ const props = withDefaults(
|
|||
| 'purple'
|
||||
| 'green'
|
||||
| 'orange'
|
||||
| 'dark-orange'
|
||||
| 'cyan'
|
||||
| 'yellow'
|
||||
| 'red'
|
||||
| 'magenta'
|
||||
| 'blue'
|
||||
| 'lime'
|
||||
| 'light-purple';
|
||||
| 'light-purple'
|
||||
| 'light-green'
|
||||
| 'gray';
|
||||
}[];
|
||||
dark?: boolean;
|
||||
textSize?: string;
|
||||
|
|
@ -44,8 +49,9 @@ const props = withDefaults(
|
|||
size="lg"
|
||||
style="background-color: hsla(0 0% 100% /0.2)"
|
||||
text-color="white"
|
||||
:icon="v.icon"
|
||||
/>
|
||||
>
|
||||
<Icon :icon="v.icon" width="24px" />
|
||||
</q-avatar>
|
||||
</div>
|
||||
<div class="col-6 justify-center column">
|
||||
<div
|
||||
|
|
@ -114,6 +120,10 @@ const props = withDefaults(
|
|||
--_color: var(--orange-5-hsl);
|
||||
}
|
||||
|
||||
.stat-card__dark-orange {
|
||||
--_color: var(--orange-10-hsl);
|
||||
}
|
||||
|
||||
.stat-card__magenta {
|
||||
--_color: var(--pink-8-hsl);
|
||||
}
|
||||
|
|
@ -122,6 +132,10 @@ const props = withDefaults(
|
|||
--_color: var(--jungle-8-hsl);
|
||||
}
|
||||
|
||||
.stat-card__light-green {
|
||||
--_color: var(--green-8-hsl);
|
||||
}
|
||||
|
||||
.stat-card__light-purple {
|
||||
--_color: var(--purple-7-hsl);
|
||||
}
|
||||
|
|
@ -130,6 +144,10 @@ const props = withDefaults(
|
|||
--_color: var(--blue-6-hsl);
|
||||
}
|
||||
|
||||
.stat-card__gray {
|
||||
--_color: var(--gray-6-hsl);
|
||||
}
|
||||
|
||||
.dark .stat-card__purple {
|
||||
--_color: var(--violet-10-hsl);
|
||||
}
|
||||
|
|
@ -142,6 +160,10 @@ const props = withDefaults(
|
|||
--_color: var(--orange-6-hsl);
|
||||
}
|
||||
|
||||
.dark.stat-card__dark-orange {
|
||||
--_color: var(--orange-11-hsl);
|
||||
}
|
||||
|
||||
.dark .stat-card__magenta {
|
||||
--_color: var(--pink-7-hsl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,208 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { CustomerBranchCreate } from 'stores/customer/types';
|
||||
import { onMounted } from 'vue';
|
||||
import { checkTabBeforeAdd } from 'stores/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
readonly?: boolean;
|
||||
edit?: boolean;
|
||||
prefixId: string;
|
||||
}>();
|
||||
|
||||
const customerBranch = defineModel<CustomerBranchCreate[]>('customerBranch', {
|
||||
default: [],
|
||||
});
|
||||
|
||||
const statBranchNo = defineModel<number>('statBranchNo', {
|
||||
default: 0,
|
||||
});
|
||||
|
||||
const tab = defineModel<number>('tabIndex', { required: true });
|
||||
|
||||
function addData() {
|
||||
const canAdd = checkTabBeforeAdd(customerBranch.value || [], [
|
||||
'branchNo',
|
||||
'payDate',
|
||||
'registerDate',
|
||||
'status',
|
||||
'wageRate',
|
||||
]);
|
||||
if (canAdd) {
|
||||
const currentNo = (customerBranch.value.at(-1)?.branchNo || 0) + 1;
|
||||
customerBranch.value.push({
|
||||
code: '',
|
||||
branchNo: currentNo,
|
||||
address: '',
|
||||
addressEN: '',
|
||||
provinceId: '',
|
||||
districtId: '',
|
||||
subDistrictId: '',
|
||||
zipCode: '',
|
||||
email: '',
|
||||
telephoneNo: '',
|
||||
name: '',
|
||||
status: 'CREATED',
|
||||
taxNo: '',
|
||||
nameEN: '',
|
||||
legalPersonNo: '',
|
||||
registerName: '',
|
||||
registerDate: new Date(),
|
||||
authorizedCapital: '',
|
||||
employmentOffice: '',
|
||||
bussinessType: '',
|
||||
bussinessTypeEN: '',
|
||||
jobPosition: '',
|
||||
jobPositionEN: '',
|
||||
jobDescription: '',
|
||||
saleEmployee: '',
|
||||
payDate: new Date(),
|
||||
wageRate: 0,
|
||||
});
|
||||
|
||||
tab.value = customerBranch.value.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function close(index: number) {
|
||||
if (customerBranch.value.length < 2) return;
|
||||
|
||||
if (
|
||||
customerBranch.value.length === index + 1 &&
|
||||
tab.value + 1 === customerBranch.value.length
|
||||
) {
|
||||
tab.value = tab.value - 1;
|
||||
} else if (tab.value >= index) {
|
||||
if (tab.value !== 0) {
|
||||
tab.value = tab.value - 1;
|
||||
}
|
||||
}
|
||||
customerBranch.value.splice(index, 1);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
customerBranch.value[0].branchNo =
|
||||
statBranchNo.value !== 0 ? statBranchNo.value : 1;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="row no-wrap full-width">
|
||||
<q-btn
|
||||
:id="`${prefixId}-btn-add`"
|
||||
class="q-px-lg bordered-b bordered-r app-text-muted"
|
||||
flat
|
||||
:color="$q.dark.isActive ? 'primary' : ''"
|
||||
style="background-color: var(--_body-bg)"
|
||||
@click="addData()"
|
||||
icon="mdi-plus"
|
||||
:disable="
|
||||
readonly ||
|
||||
!checkTabBeforeAdd(customerBranch || [], [
|
||||
'branchNo',
|
||||
'payDate',
|
||||
'registerDate',
|
||||
'status',
|
||||
'wageRate',
|
||||
])
|
||||
"
|
||||
></q-btn>
|
||||
<q-tabs
|
||||
:active-bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
:active-color="$q.dark.isActive ? 'white' : 'primary'"
|
||||
indicator-color="transparent"
|
||||
active-class="bordered-r"
|
||||
dense
|
||||
v-model="tab"
|
||||
align="left"
|
||||
inline-label
|
||||
mobile-arrows
|
||||
class="text-grey col"
|
||||
:breakpoint="0"
|
||||
style="background-color: var(--_body-bg); max-width: 55vw"
|
||||
>
|
||||
<q-tab
|
||||
:id="`${prefixId}-tab-branch-${index}`"
|
||||
v-for="(v, index) in customerBranch"
|
||||
:key="index"
|
||||
:name="index"
|
||||
:label="`${customerBranch[index].name !== '' ? customerBranch[index].name : $t('customerBranchFormTab')} `"
|
||||
:disable="tab !== index && edit"
|
||||
@click="tab = index"
|
||||
no-caps
|
||||
:class="tab === index ? '' : 'bordered-b bordered-r'"
|
||||
>
|
||||
<q-btn
|
||||
v-if="!readonly && customerBranch?.length !== 1"
|
||||
round
|
||||
flat
|
||||
:id="`${prefixId}-close-tab-${index}`"
|
||||
icon="mdi-close"
|
||||
size="sm"
|
||||
padding="xs"
|
||||
color="red"
|
||||
class="q-ml-sm"
|
||||
:class="{ dark: $q.dark.isActive }"
|
||||
@click.stop="
|
||||
() => {
|
||||
if (v.statusSave) {
|
||||
$emit('remove', index, v.id);
|
||||
} else {
|
||||
close(index);
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-tab>
|
||||
</q-tabs>
|
||||
</div>
|
||||
<div class="column seprarator-fix">
|
||||
<q-tab-panels v-model="tab" class="rounded-borders">
|
||||
<q-tab-panel
|
||||
:id="`${prefixId}-tab-branch-${index}`"
|
||||
v-for="(v, index) in customerBranch.sort(
|
||||
(a, b) => (a.branchNo ?? 0) - (b.branchNo ?? 0),
|
||||
)"
|
||||
:key="index"
|
||||
:name="index"
|
||||
>
|
||||
<slot name="about"></slot>
|
||||
<slot name="address"></slot>
|
||||
<slot name="businessInformation"></slot>
|
||||
<slot name="contactInformation"></slot>
|
||||
|
||||
<slot name="otherDocuments"></slot>
|
||||
<div class="row col-12 justify-end">
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
dense
|
||||
unelevated
|
||||
id="save-basic-info"
|
||||
color="primary"
|
||||
class="q-px-md"
|
||||
:label="$t('save')"
|
||||
@click="$emit('save')"
|
||||
/>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.active-tab {
|
||||
border: 1px solid #e0dcdc;
|
||||
border-bottom: none;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.tab-style {
|
||||
border: 1px solid #e0dcdc;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
:deep(.q-separator) {
|
||||
padding-block: 0px !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -17,6 +17,7 @@ const props = withDefaults(
|
|||
hidePagination?: boolean;
|
||||
inTable?: boolean;
|
||||
hideView?: boolean;
|
||||
btnSelected?: boolean;
|
||||
|
||||
imgColumn?: string;
|
||||
customColumn?: string[];
|
||||
|
|
@ -55,7 +56,7 @@ defineEmits<{
|
|||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr
|
||||
:style="`background-color: ${inTable ? '#F0FFF1' : 'hsla(var(--info-bg) / 0.07'} `"
|
||||
:style="`background-color: ${inTable ? 'hsla(var(--green-4-hsl) / 0.07)' : 'hsla(var(--info-bg) / 0.07'} `"
|
||||
:props="props"
|
||||
>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { BranchWithChildren } from 'stores/branch/types';
|
||||
import KebabAction from './shared/KebabAction.vue';
|
||||
import { isRoleInclude } from 'stores/utils';
|
||||
|
||||
const nodes = defineModel<(any | BranchWithChildren)[]>('nodes', {
|
||||
default: [],
|
||||
|
|
@ -119,7 +120,11 @@ defineEmits<{
|
|||
/>
|
||||
|
||||
<q-btn
|
||||
v-if="node.isHeadOffice && typeTree === 'branch'"
|
||||
v-if="
|
||||
node.isHeadOffice &&
|
||||
typeTree === 'branch' &&
|
||||
isRoleInclude(['head_of_admin', 'admin', 'system'])
|
||||
"
|
||||
:id="`create-sub-branch-btn-${node.name}`"
|
||||
@click.stop="$emit('create', node)"
|
||||
icon="mdi-file-plus-outline"
|
||||
|
|
|
|||
73
src/components/app/AppDropdown.vue
Normal file
73
src/components/app/AppDropdown.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<script setup lang="ts">
|
||||
import { onUnmounted } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const open = ref(false);
|
||||
const element = ref<HTMLElement>();
|
||||
|
||||
function click({ target }: MouseEvent) {
|
||||
if (!open.value) return;
|
||||
if (target instanceof HTMLElement && element.value?.contains(target)) {
|
||||
return;
|
||||
}
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('mouseup', click);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mouseup', click);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown" ref="element">
|
||||
<button class="dropdown__trigger" @click.stop="open = !open">
|
||||
<slot name="trigger" />
|
||||
</button>
|
||||
<Transition>
|
||||
<div class="dropdown__content" v-if="open">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
|
||||
& .dropdown__trigger {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& .dropdown__content {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 100%;
|
||||
left: 0%;
|
||||
margin-top: 0.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-2, 7px);
|
||||
}
|
||||
}
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
29
src/components/button/ImportButton.vue
Normal file
29
src/components/button/ImportButton.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<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;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MainButton
|
||||
@click="(e) => $emit('click', e)"
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
:icon="icon || 'mdi-import'"
|
||||
color="var(--info-bg)"
|
||||
:title="iconOnly ? $t('general.import') : undefined"
|
||||
>
|
||||
{{ label || $t('general.import') }}
|
||||
</MainButton>
|
||||
</template>
|
||||
|
|
@ -5,11 +5,12 @@ defineEmits<{
|
|||
(e: 'click', v: MouseEvent): void;
|
||||
}>();
|
||||
defineProps<{
|
||||
icon: string;
|
||||
icon?: string;
|
||||
color: string;
|
||||
iconOnly?: boolean;
|
||||
solid?: boolean;
|
||||
outlined?: boolean;
|
||||
pill?: boolean;
|
||||
disabled?: boolean;
|
||||
dark?: boolean;
|
||||
}>();
|
||||
|
|
@ -22,6 +23,7 @@ defineProps<{
|
|||
:class="{
|
||||
'main-btn__solid': solid && !outlined,
|
||||
'main-btn__outline': !solid && outlined,
|
||||
'main-btn__pill': pill,
|
||||
'main-btn__dark': dark,
|
||||
'main-btn__icon-only': iconOnly,
|
||||
}"
|
||||
|
|
@ -30,7 +32,7 @@ defineProps<{
|
|||
type="button"
|
||||
>
|
||||
<slot name="icon">
|
||||
<Icon :icon class="main-btn-icon" />
|
||||
<Icon :icon="icon || ''" class="main-btn-icon" />
|
||||
</slot>
|
||||
<slot v-if="!iconOnly" />
|
||||
</button>
|
||||
|
|
@ -39,7 +41,7 @@ defineProps<{
|
|||
<style scoped>
|
||||
.main-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
|
|
@ -101,16 +103,17 @@ defineProps<{
|
|||
color: hsla(var(--button-main-color) / 1);
|
||||
}
|
||||
|
||||
.main-btn-edit:disabled {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.8;
|
||||
|
||||
&.main-btn__solid {
|
||||
background: hsla(0 0% 0% / 0.08);
|
||||
}
|
||||
.main-btn:disabled {
|
||||
filter: grayscale(0.5);
|
||||
opacity: 0.5 !important;
|
||||
cursor: not-allowed;
|
||||
|
||||
&.main-btn__outline {
|
||||
border: 1px solid hsla(0 0% 0% / 0.3);
|
||||
border: 1px solid hsla(var(--button-main-color) / 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.main-btn__pill {
|
||||
border-radius: 999rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
32
src/components/button/NextButton.vue
Normal file
32
src/components/button/NextButton.vue
Normal 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-arrow-right'"
|
||||
color="207 96% 32%"
|
||||
:title="iconOnly ? $t('general.next') : undefined"
|
||||
>
|
||||
{{ label || $t('general.next') }}
|
||||
{{ amount && amount > 0 ? `(${amount})` : '' }}
|
||||
</MainButton>
|
||||
</template>
|
||||
89
src/components/button/StateButton.vue
Normal file
89
src/components/button/StateButton.vue
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<script setup lang="ts">
|
||||
import { RequestWork, RequestWorkStatus } from 'src/stores/request-list/types';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
statusDone?: boolean;
|
||||
statusActive?: boolean;
|
||||
statusWaiting?: boolean;
|
||||
|
||||
label: string;
|
||||
}>(),
|
||||
{
|
||||
statusDone: false,
|
||||
statusActive: false,
|
||||
label: '',
|
||||
},
|
||||
);
|
||||
|
||||
defineEmits<{
|
||||
(e: 'click'): void;
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
class="status-color q-pa-sm bordered row items-center cursor-pointer no-wrap"
|
||||
style="text-wrap: nowrap"
|
||||
:class="{
|
||||
['status-color-done']: statusDone,
|
||||
['status-color-doing']: true,
|
||||
['status-color-waiting']: statusWaiting,
|
||||
['step-status-active']: statusActive,
|
||||
}"
|
||||
@click="statusWaiting && !statusDone ? undefined : $emit('click')"
|
||||
>
|
||||
<div class="q-px-sm">
|
||||
<q-icon
|
||||
class="icon-color quotation-status"
|
||||
style="border-radius: 50%"
|
||||
:name="`${statusActive ? 'mdi-circle-slice-8' : statusDone ? 'mdi-check-circle' : 'mdi-checkbox-blank-circle-outline'}`"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-left">{{ label }}</div>
|
||||
</button>
|
||||
</template>
|
||||
<style scoped>
|
||||
.status-color {
|
||||
--_color: var(--gray-0);
|
||||
border-color: hsla(var(--_color));
|
||||
background: hsla(var(--_color) / 0.05);
|
||||
border-radius: 4px;
|
||||
|
||||
.icon-color {
|
||||
color: hsla(var(--_color));
|
||||
}
|
||||
|
||||
&.status-color-doing {
|
||||
--_color: var(--blue-5-hsl);
|
||||
color: var(--foreground);
|
||||
}
|
||||
&.status-color-waiting:not(.status-color-done) {
|
||||
--_color: var(--gray-4-hsl);
|
||||
color: var(--foreground);
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
&.status-color-done {
|
||||
--_color: var(--green-5-hsl);
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.step-status-active {
|
||||
opacity: 1;
|
||||
font-weight: 600;
|
||||
transition: 1s box-shadow ease-in-out;
|
||||
animation: status 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes status {
|
||||
0% {
|
||||
box-shadow: 0x 0px 0px hsla(var(--_color) / 0.5);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0px 0px 1px 4px hsla(var(--_color) / 0.2);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0px 0px 4px 7px hsla(var(--_color) / 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -18,7 +18,7 @@ defineProps<{
|
|||
@click="(e) => $emit('click', e)"
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
icon="mdi-arrow-left"
|
||||
color="var(--gray-8-hsl)"
|
||||
:color="`var(--gray-${$q.dark.isActive ? '6' : '8'}-hsl)`"
|
||||
:title="iconOnly ? $t('general.undo') : undefined"
|
||||
>
|
||||
{{ $t('general.undo') }}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ defineProps<{
|
|||
outlined?: boolean;
|
||||
disabled?: boolean;
|
||||
dark?: boolean;
|
||||
size?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,3 +11,6 @@ export { default as ClearButton } from './ClearButton.vue';
|
|||
export { default as CloseButton } from './CloseButton.vue';
|
||||
export { default as ViewButton } from './ViewButton.vue';
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const state = defineModel({ default: false });
|
|||
>
|
||||
<div class="column full-height">
|
||||
<!-- NOTE: DIALOG HEADER -->
|
||||
<div class="form-header q-py-sm q-px-md">
|
||||
<div class="form-header q-py-sm q-px-md" v-if="$slots['header']">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ const state = defineModel({ default: false });
|
|||
|
||||
<!-- NOTE: DIALOG FOOTER -->
|
||||
<div
|
||||
v-if="footer || $slots.footer"
|
||||
v-if="footer || $slots['footer']"
|
||||
class="dialog-footer row items-center full-width justify-between q-px-md q-py-sm surface-1"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const state = defineModel({ default: false });
|
|||
|
||||
<!-- NOTE: DIALOG BODY -->
|
||||
<div
|
||||
class="col full-height column full-width"
|
||||
class="col full-height column full-width surface-0"
|
||||
:class="{ dark: $q.dark.isActive }"
|
||||
>
|
||||
<slot />
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ defineProps<{
|
|||
|
||||
.dialog-header-main {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-weight: bolder;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-header-close {
|
||||
|
|
|
|||
797
src/components/dialog/DialogProperties.vue
Normal file
797
src/components/dialog/DialogProperties.vue
Normal file
|
|
@ -0,0 +1,797 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { moveItemUp, moveItemDown, dialog, deleteItem } from 'stores/utils';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import { useWorkflowTemplate } from 'src/stores/workflow-template';
|
||||
import { Option } from 'stores/options/types';
|
||||
import {
|
||||
WorkFlowPayloadStep,
|
||||
WorkflowTemplate,
|
||||
} from 'src/stores/workflow-template/types';
|
||||
|
||||
import SelectFlow from '../shared/select/SelectFlow.vue';
|
||||
import NoData from '../NoData.vue';
|
||||
import DialogForm from '../DialogForm.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { getWorkflowTemplate } = useWorkflowTemplate();
|
||||
const optionStore = useOptionStore();
|
||||
|
||||
const props = defineProps<{
|
||||
stepIndex?: number;
|
||||
|
||||
onEdit?: boolean;
|
||||
selectFlow?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'submit', currWorkflow: WorkflowTemplate): void;
|
||||
(e: 'show'): void;
|
||||
}>();
|
||||
|
||||
const model = defineModel<boolean>({ required: true, default: false });
|
||||
const workflowId = defineModel<string>('workflowId', { default: '' });
|
||||
const dataStep = defineModel<WorkFlowPayloadStep[]>('dataStep', {
|
||||
default: [],
|
||||
});
|
||||
|
||||
const tempStep = ref<WorkFlowPayloadStep[]>([]);
|
||||
const tempWorkflowId = ref<string>('');
|
||||
const propertiesOption = ref();
|
||||
const currWorkflow = ref<WorkflowTemplate>();
|
||||
const typeOption = [
|
||||
{
|
||||
label: 'Text',
|
||||
value: 'string',
|
||||
color: 'var(--pink-6-hsl)',
|
||||
icon: 'mdi-alpha-t',
|
||||
},
|
||||
{
|
||||
label: 'Number',
|
||||
value: 'number',
|
||||
color: 'var(--purple-11-hsl)',
|
||||
icon: 'mdi-numeric',
|
||||
},
|
||||
{
|
||||
label: 'Date',
|
||||
value: 'date',
|
||||
color: 'var(--green-9-hsl)',
|
||||
icon: 'mdi-calendar-blank-outline',
|
||||
},
|
||||
{
|
||||
label: 'Selection',
|
||||
value: 'array',
|
||||
color: 'var(--indigo-7-hsl)',
|
||||
icon: 'mdi-code-array',
|
||||
},
|
||||
];
|
||||
|
||||
function submit() {
|
||||
workflowId.value = tempWorkflowId.value;
|
||||
dataStep.value = JSON.parse(JSON.stringify(tempStep.value));
|
||||
model.value = false;
|
||||
|
||||
if (props.selectFlow && currWorkflow.value) {
|
||||
emit('submit', currWorkflow.value);
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
tempStep.value = [];
|
||||
model.value = false;
|
||||
}
|
||||
|
||||
function manageProperties(
|
||||
stepIndex: number,
|
||||
property: string,
|
||||
propertyType?: 'date' | 'array' | 'string' | 'number',
|
||||
) {
|
||||
if (property === 'all' && propertiesOption.value) {
|
||||
if (
|
||||
tempStep.value[stepIndex].attributes.properties.length ===
|
||||
propertiesOption.value.length
|
||||
) {
|
||||
tempStep.value[stepIndex].attributes.properties = [];
|
||||
return;
|
||||
}
|
||||
|
||||
for (const ops of propertiesOption.value) {
|
||||
if (
|
||||
tempStep.value[stepIndex].attributes.properties.some(
|
||||
(prop) => prop.fieldName === ops.value,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ops.type === 'date') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: ops.type,
|
||||
fieldName: ops.value,
|
||||
});
|
||||
}
|
||||
|
||||
if (ops.type === 'array') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: ops.type,
|
||||
fieldName: ops.value,
|
||||
options: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (ops.type === 'string') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: ops.type,
|
||||
fieldName: ops.value,
|
||||
isPhoneNumber: false,
|
||||
phoneNumberLength: 10,
|
||||
});
|
||||
}
|
||||
|
||||
if (ops.type === 'number') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: ops.type,
|
||||
fieldName: ops.value,
|
||||
comma: false,
|
||||
decimal: false,
|
||||
decimalPlace: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (tempStep.value[stepIndex].attributes.properties) {
|
||||
const propStep = tempStep.value[stepIndex].attributes.properties.findIndex(
|
||||
(prop) => prop.fieldName === property,
|
||||
);
|
||||
|
||||
if (propStep !== -1) {
|
||||
tempStep.value[stepIndex].attributes.properties.splice(propStep, 1);
|
||||
} else {
|
||||
if (propertyType === 'date') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: propertyType,
|
||||
fieldName: property,
|
||||
});
|
||||
}
|
||||
|
||||
if (propertyType === 'array') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: propertyType,
|
||||
fieldName: property,
|
||||
options: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (propertyType === 'string') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: propertyType,
|
||||
fieldName: property,
|
||||
isPhoneNumber: false,
|
||||
phoneNumberLength: 10,
|
||||
});
|
||||
}
|
||||
|
||||
if (propertyType === 'number') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: propertyType,
|
||||
fieldName: property,
|
||||
comma: false,
|
||||
decimal: false,
|
||||
decimalPlace: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeType(fieldName: string, stepIndex: number) {
|
||||
if (!propertiesOption.value) return;
|
||||
|
||||
const defaultPropType = propertiesOption.value.find(
|
||||
(op: { value: string }) => op.value === fieldName,
|
||||
)?.type;
|
||||
|
||||
if (!defaultPropType) return;
|
||||
|
||||
const idx = tempStep.value[stepIndex].attributes.properties.findIndex(
|
||||
(p) => p.fieldName === fieldName,
|
||||
);
|
||||
|
||||
if (!idx) return;
|
||||
|
||||
if (defaultPropType === 'date') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: defaultPropType,
|
||||
fieldName,
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultPropType === 'array') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: defaultPropType,
|
||||
fieldName,
|
||||
options: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultPropType === 'string') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: defaultPropType,
|
||||
fieldName,
|
||||
isPhoneNumber: false,
|
||||
phoneNumberLength: 10,
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultPropType === 'number') {
|
||||
tempStep.value[stepIndex].attributes.properties.push({
|
||||
type: defaultPropType,
|
||||
fieldName,
|
||||
comma: false,
|
||||
decimal: false,
|
||||
decimalPlace: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function shouldShowItem(opt: Option, stepIndex: number) {
|
||||
if (tempStep.value[stepIndex].attributes.properties) {
|
||||
const properties = new Set(
|
||||
tempStep.value[stepIndex].attributes.properties.map((p) => p.fieldName),
|
||||
);
|
||||
return !!opt && !properties.has(opt.value);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(items: unknown[], index: number) {
|
||||
dialog({
|
||||
color: 'negative',
|
||||
icon: 'mdi-alert',
|
||||
title: t('dialog.title.confirmDelete'),
|
||||
actionText: t('general.delete'),
|
||||
message: t('dialog.message.confirmDelete'),
|
||||
action: async () => {
|
||||
deleteItem(items, index);
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
function assignTemp() {
|
||||
propertiesOption.value = optionStore.globalOption?.servicePropertiesField;
|
||||
|
||||
tempStep.value = JSON.parse(JSON.stringify(dataStep.value));
|
||||
tempWorkflowId.value = workflowId.value;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => tempWorkflowId.value,
|
||||
async (a, b) => {
|
||||
if (props.onEdit && workflowId.value === a) return;
|
||||
|
||||
if (props.selectFlow && a !== b && a) {
|
||||
const ret = await getWorkflowTemplate(a);
|
||||
if (ret) {
|
||||
currWorkflow.value = JSON.parse(JSON.stringify(ret));
|
||||
tempStep.value =
|
||||
ret.step?.length > 0
|
||||
? ret.step.map((s) => ({
|
||||
name: s.name,
|
||||
attributes: s.attributes,
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => model.value,
|
||||
() => {
|
||||
if (model.value) {
|
||||
assignTemp();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<DialogForm
|
||||
no-address
|
||||
no-app-box
|
||||
height="60vh"
|
||||
width="75%"
|
||||
:title="$t('general.properties')"
|
||||
v-model:modal="model"
|
||||
:submit="submit"
|
||||
:close="close"
|
||||
:show="() => $emit('show')"
|
||||
>
|
||||
<div class="column full-height no-wrap">
|
||||
<div
|
||||
v-if="selectFlow"
|
||||
class="bordered-b surface-3 row items-center no-wrap q-py-sm"
|
||||
:class="{
|
||||
'q-px-lg': $q.screen.gt.sm,
|
||||
'q-px-md': !$q.screen.gt.sm,
|
||||
}"
|
||||
>
|
||||
{{ $t('flow.title') }}
|
||||
<SelectFlow
|
||||
style="width: 18vw"
|
||||
class="q-ml-sm"
|
||||
v-model:value="tempWorkflowId"
|
||||
:label="$t('flow.title')"
|
||||
simple
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="$slots.prepend">
|
||||
<slot name="prepend"></slot>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="tempStep?.length === 0"
|
||||
class="row surface-1 rounded bordered items-center justify-center col"
|
||||
:class="{
|
||||
'q-ma-lg': $q.screen.gt.sm,
|
||||
'q-ma-md': !$q.screen.gt.sm,
|
||||
}"
|
||||
>
|
||||
<NoData :text="$t('general.no', { msg: $t('flow.processStep') })" />
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-for="(step, stepIndex) in tempStep"
|
||||
:key="stepIndex"
|
||||
class="column"
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
(props.stepIndex !== undefined && stepIndex === props.stepIndex) ||
|
||||
props.stepIndex === undefined
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="row items-center q-py-sm bordered-b"
|
||||
:class="{
|
||||
'q-px-lg': $q.screen.gt.sm,
|
||||
'q-px-md ': !$q.screen.gt.sm,
|
||||
}"
|
||||
>
|
||||
{{ $t('flow.stepNo', { msg: (props.stepIndex || stepIndex) + 1 }) }}
|
||||
<span class="app-text-muted">: {{ step.name }}</span>
|
||||
<q-btn-dropdown
|
||||
unelevated
|
||||
no-icon-animation
|
||||
size="sm"
|
||||
padding="0 0"
|
||||
class="rounded q-ml-md"
|
||||
dropdown-icon="mdi-plus"
|
||||
style="color: hsl(var(--text-mute))"
|
||||
>
|
||||
<q-list dense v-if="propertiesOption">
|
||||
<q-item
|
||||
for="list-all"
|
||||
id="list-all"
|
||||
clickable
|
||||
@click="manageProperties(stepIndex, 'all')"
|
||||
>
|
||||
<div class="full-width flex items-center">
|
||||
<q-icon
|
||||
v-if="
|
||||
tempStep[stepIndex].attributes.properties?.length ===
|
||||
propertiesOption?.length
|
||||
"
|
||||
name="mdi-checkbox-marked"
|
||||
size="xs"
|
||||
class="q-mr-sm"
|
||||
color="primary"
|
||||
/>
|
||||
<q-icon
|
||||
v-else
|
||||
name="mdi-checkbox-blank-outline"
|
||||
size="xs"
|
||||
class="q-mr-sm"
|
||||
style="color: hsl(var(--text-mute))"
|
||||
/>
|
||||
{{ $t('general.selectAll') }}
|
||||
</div>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item
|
||||
v-for="(ops, index) in propertiesOption"
|
||||
clickable
|
||||
:key="index"
|
||||
@click="manageProperties(stepIndex, ops.value, ops.type)"
|
||||
:for="`list-${ops.value}`"
|
||||
:id="`list-${ops.value}`"
|
||||
>
|
||||
<div class="full-width flex items-center no-wrap">
|
||||
<q-icon
|
||||
v-if="
|
||||
tempStep[stepIndex].attributes.properties.some(
|
||||
(p) => p.fieldName === ops.value,
|
||||
)
|
||||
"
|
||||
name="mdi-checkbox-marked"
|
||||
size="xs"
|
||||
color="primary"
|
||||
class="q-mr-sm"
|
||||
/>
|
||||
<q-icon
|
||||
v-else
|
||||
name="mdi-checkbox-blank-outline"
|
||||
size="xs"
|
||||
style="color: hsl(var(--text-mute))"
|
||||
class="q-mr-sm"
|
||||
/>
|
||||
{{ ops.label }}
|
||||
</div>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="tempStep[stepIndex].attributes?.properties?.length === 0"
|
||||
class="row surface-1 rounded bordered items-center justify-center col"
|
||||
:class="{
|
||||
'q-ma-lg': $q.screen.gt.sm,
|
||||
'q-ma-md': !$q.screen.gt.sm,
|
||||
}"
|
||||
>
|
||||
<NoData useField />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="tempStep[stepIndex].attributes?.properties?.length > 0"
|
||||
class="q-py-sm"
|
||||
:class="{
|
||||
'q-px-lg': $q.screen.gt.sm,
|
||||
'q-px-md': !$q.screen.gt.sm,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(prop, propIndex) in tempStep[stepIndex].attributes
|
||||
.properties"
|
||||
:key="propIndex"
|
||||
class="bordered surface-1 rounded q-py-sm q-px-md row items-start q-my-sm"
|
||||
>
|
||||
<div class="col-md col-12 row items-center">
|
||||
<q-btn
|
||||
id="btn-move-up-product"
|
||||
icon="mdi-arrow-up"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
:disable="propIndex === 0"
|
||||
:size="$q.screen.xs ? 'xs' : ''"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
@click="
|
||||
moveItemUp(
|
||||
tempStep[stepIndex].attributes.properties,
|
||||
propIndex,
|
||||
)
|
||||
"
|
||||
/>
|
||||
<q-btn
|
||||
id="btn-move-down-product"
|
||||
icon="mdi-arrow-down"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
:size="$q.screen.xs ? 'xs' : ''"
|
||||
:disable="
|
||||
propIndex ===
|
||||
tempStep[stepIndex].attributes.properties?.length - 1
|
||||
"
|
||||
style="color: hsl(var(--text-mute-2))"
|
||||
@click="
|
||||
moveItemDown(
|
||||
tempStep[stepIndex].attributes.properties,
|
||||
propIndex,
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<q-avatar
|
||||
:size="$q.screen.xs ? 'sm' : 'md'"
|
||||
class="q-mx-lg"
|
||||
style="background-color: var(--surface-3)"
|
||||
>
|
||||
{{ propIndex + 1 }}
|
||||
</q-avatar>
|
||||
|
||||
<!-- field name -->
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
hide-bottom-space
|
||||
for="input-properties-name"
|
||||
class="col-md col-12 q-mr-md"
|
||||
:class="{ 'q-my-sm': $q.screen.lt.md }"
|
||||
:label="$t('productService.service.propertiesName')"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="propertiesOption"
|
||||
v-model="prop.fieldName"
|
||||
@update:model-value="(v) => changeType(v, stepIndex)"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item
|
||||
v-if="scope.opt && shouldShowItem(scope.opt, stepIndex)"
|
||||
v-bind="scope.itemProps"
|
||||
class="row items-center col-12"
|
||||
>
|
||||
{{ scope.opt.label }}
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<!-- type -->
|
||||
<div
|
||||
v-if="
|
||||
prop.fieldName === 'documentCheck' ||
|
||||
prop.fieldName === 'designForm' ||
|
||||
prop.fieldName === 'messenger' ||
|
||||
prop.fieldName === 'duty'
|
||||
"
|
||||
class="col-md col-12"
|
||||
></div>
|
||||
<div v-else class="col-md col-12">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
hide-bottom-space
|
||||
for="input-properties-type"
|
||||
id="input-properties-type"
|
||||
:label="$t('general.type')"
|
||||
option-value="value"
|
||||
@update:model-value="
|
||||
(t: 'string' | 'number' | 'date' | 'array') => {
|
||||
if (!tempStep) return;
|
||||
if (t === 'date') {
|
||||
tempStep[stepIndex].attributes.properties[propIndex] = {
|
||||
type: t,
|
||||
fieldName: prop.fieldName,
|
||||
};
|
||||
}
|
||||
|
||||
if (t === 'array') {
|
||||
tempStep[stepIndex].attributes.properties[propIndex] = {
|
||||
type: t,
|
||||
fieldName: prop.fieldName,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (t === 'string') {
|
||||
tempStep[stepIndex].attributes.properties[propIndex] = {
|
||||
type: t,
|
||||
fieldName: prop.fieldName,
|
||||
isPhoneNumber: false,
|
||||
phoneNumberLength: 10,
|
||||
};
|
||||
}
|
||||
|
||||
if (t === 'number') {
|
||||
tempStep[stepIndex].attributes.properties[propIndex] = {
|
||||
type: t,
|
||||
fieldName: prop.fieldName,
|
||||
comma: false,
|
||||
decimal: false,
|
||||
decimalPlace: 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
"
|
||||
:options="typeOption"
|
||||
v-model="prop.type"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item
|
||||
v-if="scope.opt"
|
||||
v-bind="scope.itemProps"
|
||||
class="row items-center col-12"
|
||||
:id="`type-${scope.itemProps}`"
|
||||
>
|
||||
<q-avatar
|
||||
size="sm"
|
||||
class="q-mr-md"
|
||||
:style="`background-color: hsla(${scope.opt.color}/0.2)`"
|
||||
>
|
||||
<q-icon
|
||||
size="20px"
|
||||
:name="scope.opt.icon"
|
||||
:style="`color: hsl(${scope.opt.color})`"
|
||||
/>
|
||||
</q-avatar>
|
||||
{{ scope.opt.label }}
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<template v-slot:selected-item="scope">
|
||||
<div v-if="scope.opt" class="row items-center col-12">
|
||||
<q-avatar
|
||||
size="xs"
|
||||
class="q-mr-sm"
|
||||
:style="`background-color: hsla(${scope.opt.color}/0.2)`"
|
||||
>
|
||||
<q-icon
|
||||
size="14px"
|
||||
:name="scope.opt.icon"
|
||||
:style="`color: hsl(${scope.opt.color})`"
|
||||
/>
|
||||
</q-avatar>
|
||||
{{ scope.opt.label }}
|
||||
</div>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<div v-if="prop.type !== 'date' && prop.type">
|
||||
<q-item class="no-padding" style="font-size: 11px">
|
||||
<q-item-section
|
||||
class="column q-py-sm"
|
||||
:style="{ 'padding-left: 12px': $q.screen.gt.xs }"
|
||||
>
|
||||
<span class="app-text-muted-2">
|
||||
{{ $t('general.additional') }}
|
||||
</span>
|
||||
|
||||
<div v-if="prop.type === 'string'" class="q-gutter-y-sm">
|
||||
<div class="row items-center">
|
||||
<div class="col-7 surface-3 rounded q-mr-sm q-py-xs">
|
||||
<q-checkbox
|
||||
:for="`checkbox-is-phone-number-${prop.fieldName}`"
|
||||
v-if="prop.type === 'string'"
|
||||
v-model="prop.isPhoneNumber"
|
||||
size="xs"
|
||||
/>
|
||||
{{ $t('general.telephone') }}
|
||||
</div>
|
||||
<q-input
|
||||
v-if="prop.type === 'string'"
|
||||
:for="`input-max-length-${prop.fieldName}`"
|
||||
v-model="prop.phoneNumberLength"
|
||||
input-class="text-caption"
|
||||
class="col additional-label"
|
||||
dense
|
||||
outlined
|
||||
:label="$t('form.maxLength')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="prop.type === 'number'" class="q-gutter-y-sm">
|
||||
<div class="row items-center">
|
||||
<div class="col-md-4 col-12 surface-3 rounded">
|
||||
<q-checkbox
|
||||
v-model="prop.comma"
|
||||
size="xs"
|
||||
class="q-py-xs"
|
||||
:for="`checkbox-is-comma-${prop.fieldName}`"
|
||||
/>
|
||||
{{ $t('form.useComma') }}
|
||||
</div>
|
||||
<div
|
||||
class="col-md-4 col-7 surface-3 rounded"
|
||||
:class="{
|
||||
'q-mx-sm': $q.screen.gt.sm,
|
||||
'q-mr-sm q-mt-xs': $q.screen.lt.md,
|
||||
}"
|
||||
>
|
||||
<q-checkbox
|
||||
v-model="prop.decimal"
|
||||
size="xs"
|
||||
class="q-py-xs"
|
||||
:for="`checkbox-is-decimal-${prop.fieldName}`"
|
||||
/>
|
||||
{{ $t('form.decimal') }}
|
||||
</div>
|
||||
<q-input
|
||||
:for="`input-decimal-place-${prop.fieldName}`"
|
||||
v-model="prop.decimalPlace"
|
||||
class="col additional-label"
|
||||
:class="{ 'q-mt-xs': $q.screen.lt.md }"
|
||||
input-class="text-caption"
|
||||
dense
|
||||
outlined
|
||||
:label="$t('form.decimalPlace')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="prop.type === 'array'" class="q-gutter-y-sm">
|
||||
<div
|
||||
class="row items-center justify-between"
|
||||
v-for="(_, i) in prop.options"
|
||||
:key="i"
|
||||
>
|
||||
<div class="col rounded">
|
||||
<q-input
|
||||
v-model="prop.options[i]"
|
||||
:for="`input-selection-${prop.fieldName}-${i}`"
|
||||
class="col additional-label"
|
||||
dense
|
||||
outlined
|
||||
input-class="text-caption"
|
||||
:label="$t('form.selection')"
|
||||
:rules="[
|
||||
(val) => !!val || $t('form.error.required'),
|
||||
]"
|
||||
hide-bottom-space
|
||||
/>
|
||||
</div>
|
||||
<div class="col-1 q-pl-sm">
|
||||
<q-btn
|
||||
:id="`btn-delete-selection-${prop.fieldName}-${i}`"
|
||||
:for="`btn-delete-selection-${prop.fieldName}-${i}`"
|
||||
@click="
|
||||
() => {
|
||||
prop.options.splice(i, 1);
|
||||
}
|
||||
"
|
||||
dense
|
||||
flat
|
||||
icon="mdi-trash-can-outline"
|
||||
class="bordered"
|
||||
text-color="negative"
|
||||
style="border-radius: 6px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<q-btn
|
||||
:for="`btn-add-selection-${prop.fieldName}`"
|
||||
:id="`btn-add-selection-${prop.fieldName}`"
|
||||
@click="
|
||||
() => {
|
||||
prop.options.push('');
|
||||
}
|
||||
"
|
||||
dense
|
||||
flat
|
||||
icon="mdi-plus"
|
||||
class="bordered col-11"
|
||||
text-color="grey"
|
||||
style="border-radius: 6px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
id="btn-delete-work-product"
|
||||
icon="mdi-trash-can-outline"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
color="negative"
|
||||
class="q-ml-sm"
|
||||
@click="
|
||||
confirmDelete(
|
||||
tempStep[stepIndex].attributes.properties,
|
||||
propIndex,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</DialogForm>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
191
src/components/dialog/DialogSelect.vue
Normal file
191
src/components/dialog/DialogSelect.vue
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<script setup lang="ts" generic="T extends { id: string }">
|
||||
import { Ref } from 'vue';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
import { CancelButton, MainButton } from 'components/button';
|
||||
import DialogContainer from './DialogContainer.vue';
|
||||
import DialogHeader from './DialogHeader.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
getList: (search?: string) => T[] | Promise<T[]>;
|
||||
|
||||
dialog: {
|
||||
title: string;
|
||||
};
|
||||
orderHidden?: boolean;
|
||||
|
||||
preselectedItem?: T[];
|
||||
disabledItemId?: T['id'][];
|
||||
|
||||
/** @returns true - to auto close dialog */
|
||||
onSubmit: (selected: T[]) => void | boolean | Promise<void | boolean>;
|
||||
}>();
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
const list: Ref<T[]> = ref([]);
|
||||
const selected: Ref<T[]> = ref([]);
|
||||
|
||||
const state = reactive({
|
||||
search: '',
|
||||
});
|
||||
|
||||
function clean() {
|
||||
list.value = [];
|
||||
selected.value = [];
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function selectedIndex(item: T) {
|
||||
return selected.value.findIndex((v) => v.id === item.id);
|
||||
}
|
||||
|
||||
function toggleSelect(item: T) {
|
||||
if (props.disabledItemId?.some((id) => id === item.id)) return;
|
||||
|
||||
const index = selectedIndex(item);
|
||||
|
||||
if (index === -1) {
|
||||
selected.value.push(item);
|
||||
} else {
|
||||
selected.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (props.preselectedItem) {
|
||||
selected.value = JSON.parse(JSON.stringify(props.preselectedItem));
|
||||
}
|
||||
getList();
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
list.value = await props.getList(state.search);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const result = await props.onSubmit(selected.value);
|
||||
if (typeof result === 'boolean') open.value = !result;
|
||||
}
|
||||
|
||||
watch(() => state.search, getList);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogContainer v-model="open" @open="init" @close="clean">
|
||||
<template #header>
|
||||
<DialogHeader :title="dialog.title">
|
||||
<template #title-before>
|
||||
<span class="q-mr-auto"></span>
|
||||
</template>
|
||||
<template #title-after>
|
||||
<span class="q-ml-auto" v-if="!$slots['title-after']" />
|
||||
<div class="q-ml-auto" v-else><slot name="title-after" /></div>
|
||||
</template>
|
||||
</DialogHeader>
|
||||
</template>
|
||||
<div class="column q-pa-md full-height">
|
||||
<section class="row justify-end q-mb-md">
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="state.search"
|
||||
debounce="500"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</section>
|
||||
<!-- NOTE: wrapper -->
|
||||
<div class="col scroll">
|
||||
<section
|
||||
:class="{ ['items-center']: list.length === 0 }"
|
||||
class="row q-col-gutter-md full-height"
|
||||
>
|
||||
<div v-if="list.length === 0" class="inline-block q-mx-auto">
|
||||
<slot name="empty" :data="{ notFound: !!state.search }" />
|
||||
</div>
|
||||
<div
|
||||
:key="item.data.id"
|
||||
v-for="(item, index) in list.map((data) => ({
|
||||
data: data,
|
||||
selectedIndex: selectedIndex(data),
|
||||
}))"
|
||||
class="col-2"
|
||||
>
|
||||
<button
|
||||
class="selectable-item full-width"
|
||||
:class="{
|
||||
['selectable-item__selected']: item.selectedIndex !== -1,
|
||||
['selectable-item__disabled']: disabledItemId?.some(
|
||||
(id) => id === item.data.id,
|
||||
),
|
||||
}"
|
||||
@click="toggleSelect(item.data)"
|
||||
>
|
||||
<span class="selectable-item__pos" v-if="!orderHidden">
|
||||
{{ item.selectedIndex + 1 }}
|
||||
</span>
|
||||
<slot name="card" :data="{ index, item }" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="q-gutter-x-xs q-ml-auto">
|
||||
<CancelButton outlined @click="clean" />
|
||||
<MainButton icon="mdi-check" color="207 96% 32%" solid @click="submit">
|
||||
{{ $t('general.select') }}
|
||||
</MainButton>
|
||||
</div>
|
||||
</template>
|
||||
</DialogContainer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.selectable-item {
|
||||
padding: 0;
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
color: inherit;
|
||||
|
||||
& > .selectable-item__pos {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selectable-item__selected {
|
||||
& > :deep(*) {
|
||||
border: 1px solid var(--_color, var(--brand-1)) !important;
|
||||
}
|
||||
|
||||
& > .selectable-item__pos {
|
||||
display: block;
|
||||
position: absolute;
|
||||
margin: var(--size-2);
|
||||
right: 0;
|
||||
top: 0;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--surface-1);
|
||||
background: var(--brand-1);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.selectable-item__disabled {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.5;
|
||||
|
||||
& :deep(*) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
99
src/components/dialog/DialogViewFile.vue
Normal file
99
src/components/dialog/DialogViewFile.vue
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import DialogFormContainer from './DialogFormContainer.vue';
|
||||
import DialogHeader from './DialogHeader.vue';
|
||||
import MainButton from '../button/MainButton.vue';
|
||||
import NoData from '../NoData.vue';
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
url?: string;
|
||||
}>();
|
||||
|
||||
const open = defineModel<boolean>({ default: false });
|
||||
|
||||
const state = reactive({
|
||||
imageZoom: 100,
|
||||
});
|
||||
|
||||
function openDialog() {
|
||||
state.imageZoom = 100;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<DialogFormContainer v-model="open" v-on:open="openDialog">
|
||||
<template #header>
|
||||
<DialogHeader :title="title" />
|
||||
</template>
|
||||
|
||||
<main class="column full-height">
|
||||
<section
|
||||
style="background: var(--gray-3)"
|
||||
class="q-py-sm row justify-center"
|
||||
>
|
||||
<div class="surface-2 q-px-md q-py-sm rounded row no-wrap items-center">
|
||||
<MainButton
|
||||
icon="mdi-minus"
|
||||
color="0 0% 0%"
|
||||
icon-only
|
||||
@click="
|
||||
() => {
|
||||
if (state.imageZoom > 0) state.imageZoom -= 10;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
class="q-px-sm"
|
||||
input-class="text-center text-caption"
|
||||
:model-value="state.imageZoom"
|
||||
@update:model-value="
|
||||
(val) => {
|
||||
const numVal = Number(val);
|
||||
if (numVal > 500 || numVal < 0) {
|
||||
state.imageZoom = 100;
|
||||
} else {
|
||||
state.imageZoom = numVal || 100;
|
||||
}
|
||||
}
|
||||
"
|
||||
></q-input>
|
||||
<MainButton
|
||||
icon="mdi-plus"
|
||||
color="0 0% 0%"
|
||||
icon-only
|
||||
@click="
|
||||
() => {
|
||||
if (state.imageZoom < 500) state.imageZoom += 10;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
:class="{ 'cursor-pointer': state.imageZoom > 100 }"
|
||||
class="full-height full-width flex justify-center items-center col scroll q-pa-md"
|
||||
v-dragscroll
|
||||
>
|
||||
<q-img
|
||||
v-if="url"
|
||||
class="full-height"
|
||||
:src="url"
|
||||
fit="contain"
|
||||
:style="{ transform: `scale(${state.imageZoom / 100})` }"
|
||||
style="transform-origin: 0 0"
|
||||
/>
|
||||
<NoData v-else />
|
||||
</div>
|
||||
</main>
|
||||
</DialogFormContainer>
|
||||
</template>
|
||||
<style scoped>
|
||||
:deep(.q-field__control.relative-position.row.no-wrap) {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,14 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, watch, reactive, ref, computed } from 'vue';
|
||||
import { onMounted, watch, reactive, ref, computed, watchEffect } from 'vue';
|
||||
import useAddressStore, {
|
||||
District,
|
||||
Province,
|
||||
SubDistrict,
|
||||
Office,
|
||||
} from 'stores/address';
|
||||
import { selectFilterOptionRefMod } from 'stores/utils';
|
||||
import { QSelect } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatAddress } from 'src/utils/address';
|
||||
import useOptionStore from 'stores/options';
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
defineProps<{
|
||||
title?: string;
|
||||
addressTitle?: string;
|
||||
|
|
@ -29,19 +32,27 @@ defineProps<{
|
|||
useWorkPlace?: boolean;
|
||||
}>();
|
||||
|
||||
const adrressStore = useAddressStore();
|
||||
const addressStore = useAddressStore();
|
||||
const workplace = defineModel<string>('workplace', { default: '' });
|
||||
const workplaceEN = defineModel<string>('workplaceEn', { default: '' });
|
||||
const address = defineModel('address', { default: '' });
|
||||
const addressEN = defineModel('addressEn', { default: '' });
|
||||
const street = defineModel('street', { default: '' });
|
||||
const streetEN = defineModel('streetEn', { default: '' });
|
||||
const moo = defineModel('moo', { default: '' });
|
||||
const mooEN = defineModel('mooEn', { default: '' });
|
||||
const soi = defineModel('soi', { default: '' });
|
||||
const soiEN = defineModel('soiEn', { default: '' });
|
||||
const provinceId = defineModel<string | null | undefined>('provinceId');
|
||||
const districtId = defineModel<string | null | undefined>('districtId');
|
||||
const street = defineModel<string | null | undefined>('street', {
|
||||
default: '',
|
||||
});
|
||||
const streetEN = defineModel<string | null | undefined>('streetEn', {
|
||||
default: '',
|
||||
});
|
||||
const moo = defineModel<string | null | undefined>('moo', { default: '' });
|
||||
const mooEN = defineModel<string | null | undefined>('mooEn', { default: '' });
|
||||
const soi = defineModel<string | null | undefined>('soi', { default: '' });
|
||||
const soiEN = defineModel<string | null | undefined>('soiEn', { default: '' });
|
||||
const provinceId = defineModel<string | null | undefined>('provinceId', {
|
||||
default: '',
|
||||
});
|
||||
const districtId = defineModel<string | null | undefined>('districtId', {
|
||||
default: '',
|
||||
});
|
||||
const subDistrictId = defineModel<string | null | undefined>('subDistrictId');
|
||||
const zipCode = defineModel<string | null | undefined>('zipCode');
|
||||
const sameWithEmployer = defineModel<boolean>('sameWithEmployer');
|
||||
|
|
@ -64,82 +75,63 @@ const addrOptions = reactive<{
|
|||
subDistrictOps: [],
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const area = ref<Office[]>([]);
|
||||
|
||||
const fullAddress = computed(() => {
|
||||
const addressParts = [`${address.value},`];
|
||||
const province = provinceOptions.value.find((v) => v.id === provinceId.value);
|
||||
const district = districtOptions.value.find((v) => v.id === districtId.value);
|
||||
const sDistrict = subDistrictOptions.value.find(
|
||||
(v) => v.id === subDistrictId.value,
|
||||
);
|
||||
|
||||
if (moo.value) addressParts.push(`${t('form.moo')} ${moo.value},`);
|
||||
if (soi.value) addressParts.push(`${t('form.soi')} ${soi.value},`);
|
||||
if (street.value) addressParts.push(`${t('form.road')} ${street.value},`);
|
||||
|
||||
if (subDistrictId.value && sDistrict) {
|
||||
addressParts.push(
|
||||
typeof sDistrict.name === 'string' ? `${sDistrict.name},` : '',
|
||||
);
|
||||
if (province && district && sDistrict) {
|
||||
const fullAddress = formatAddress({
|
||||
address: address.value,
|
||||
addressEN: addressEN.value,
|
||||
moo: moo.value ? moo.value : '',
|
||||
mooEN: mooEN.value ? mooEN.value : '',
|
||||
soi: soi.value ? soi.value : '',
|
||||
soiEN: soiEN.value ? soiEN.value : '',
|
||||
street: street.value ? street.value : '',
|
||||
streetEN: streetEN.value ? streetEN.value : '',
|
||||
province: province as unknown as Province,
|
||||
district: district as unknown as District,
|
||||
subDistrict: sDistrict as unknown as SubDistrict,
|
||||
});
|
||||
return fullAddress;
|
||||
}
|
||||
|
||||
if (districtId.value && district)
|
||||
addressParts.push(
|
||||
typeof district.name === 'string' ? `${district.name},` : '',
|
||||
);
|
||||
|
||||
if (provinceId.value && province) {
|
||||
addressParts.push(
|
||||
typeof province.name === 'string' ? `${province.name}` : '',
|
||||
);
|
||||
sDistrict &&
|
||||
addressParts.push(
|
||||
typeof sDistrict.zipCode === 'string' ? `${sDistrict.zipCode}` : '',
|
||||
);
|
||||
}
|
||||
|
||||
return addressParts.join(' ');
|
||||
return '-';
|
||||
});
|
||||
|
||||
const fullAddressEN = computed(() => {
|
||||
const addressParts = [`${addressEN.value},`];
|
||||
const province = provinceOptions.value.find((v) => v.id === provinceId.value);
|
||||
const district = districtOptions.value.find((v) => v.id === districtId.value);
|
||||
const sDistrict = subDistrictOptions.value.find(
|
||||
(v) => v.id === subDistrictId.value,
|
||||
);
|
||||
|
||||
if (mooEN.value) addressParts.push(`Moo ${mooEN.value},`);
|
||||
if (soiEN.value) addressParts.push(`Soi ${soiEN.value},`);
|
||||
if (streetEN.value) addressParts.push(`${streetEN.value} Rd.`);
|
||||
|
||||
if (subDistrictId.value && sDistrict) {
|
||||
addressParts.push(
|
||||
typeof sDistrict.nameEN === 'string' ? `${sDistrict.nameEN},` : '',
|
||||
);
|
||||
if (province && district && sDistrict) {
|
||||
const fullAddress = formatAddress({
|
||||
address: address.value,
|
||||
addressEN: addressEN.value,
|
||||
moo: moo.value ? moo.value : '',
|
||||
mooEN: mooEN.value ? mooEN.value : '',
|
||||
soi: soi.value ? soi.value : '',
|
||||
soiEN: soiEN.value ? soiEN.value : '',
|
||||
street: street.value ? street.value : '',
|
||||
streetEN: streetEN.value ? streetEN.value : '',
|
||||
province: province as unknown as Province,
|
||||
district: district as unknown as District,
|
||||
subDistrict: sDistrict as unknown as SubDistrict,
|
||||
en: true,
|
||||
});
|
||||
return fullAddress;
|
||||
}
|
||||
|
||||
if (districtId.value && district)
|
||||
addressParts.push(
|
||||
typeof district.nameEN === 'string' ? `${district.nameEN},` : '',
|
||||
);
|
||||
|
||||
if (provinceId.value && province) {
|
||||
addressParts.push(
|
||||
typeof province.nameEN === 'string' ? `${province.nameEN}` : '',
|
||||
);
|
||||
sDistrict &&
|
||||
addressParts.push(
|
||||
typeof sDistrict.zipCode === 'string' ? `${sDistrict.zipCode}` : '',
|
||||
);
|
||||
}
|
||||
|
||||
return addressParts.join(' ');
|
||||
return '-';
|
||||
});
|
||||
|
||||
async function fetchProvince() {
|
||||
const result = await adrressStore.fetchProvince();
|
||||
const result = await addressStore.fetchProvince();
|
||||
|
||||
if (result) addrOptions.provinceOps = result;
|
||||
|
||||
|
|
@ -159,7 +151,7 @@ async function fetchProvince() {
|
|||
async function fetchDistrict() {
|
||||
if (!provinceId.value) return;
|
||||
|
||||
const result = await adrressStore.fetchDistrictByProvinceId(provinceId.value);
|
||||
const result = await addressStore.fetchDistrictByProvinceId(provinceId.value);
|
||||
if (result) addrOptions.districtOps = result;
|
||||
|
||||
districtFilter = selectFilterOptionRefMod(
|
||||
|
|
@ -177,7 +169,7 @@ async function fetchDistrict() {
|
|||
|
||||
async function fetchSubDistrict() {
|
||||
if (!districtId.value) return;
|
||||
const result = await adrressStore.fetchSubDistrictByProvinceId(
|
||||
const result = await addressStore.fetchSubDistrictByProvinceId(
|
||||
districtId.value,
|
||||
);
|
||||
if (result) addrOptions.subDistrictOps = result;
|
||||
|
|
@ -203,7 +195,24 @@ async function selectSubDistrict(id: string) {
|
|||
.map((x) => x.zipCode)[0] ?? '';
|
||||
}
|
||||
|
||||
async function getOfficeName(districtId: string) {
|
||||
const result = await addressStore.fetchOffice({
|
||||
districtId,
|
||||
});
|
||||
|
||||
if (result) return result;
|
||||
return [];
|
||||
}
|
||||
|
||||
const office = computed(() => {
|
||||
if (area.value[0]) return area.value[0].name;
|
||||
});
|
||||
const officeEn = computed(() => {
|
||||
if (area.value[0]) return area.value[0].nameEN;
|
||||
});
|
||||
|
||||
const provinceOptions = ref<Record<string, unknown>[]>([]);
|
||||
|
||||
let provinceFilter: (
|
||||
value: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
|
|
@ -233,7 +242,14 @@ let subDistrictEnFilter: (
|
|||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
) => void;
|
||||
|
||||
const rawOption = ref();
|
||||
const areaENOption = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
const resultOption = await fetch('/option/option.json');
|
||||
rawOption.value = await resultOption.json();
|
||||
areaENOption.value = rawOption.value.eng.area;
|
||||
|
||||
await fetchProvince();
|
||||
await fetchDistrict();
|
||||
await fetchSubDistrict();
|
||||
|
|
@ -241,6 +257,15 @@ onMounted(async () => {
|
|||
|
||||
watch(provinceId, fetchDistrict);
|
||||
watch(districtId, fetchSubDistrict);
|
||||
|
||||
watchEffect(async () => {
|
||||
if (!districtId.value) {
|
||||
area.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
area.value = await getOfficeName(districtId.value);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="col-12">
|
||||
|
|
@ -306,8 +331,8 @@ watch(districtId, fetchSubDistrict);
|
|||
disabledRule
|
||||
? []
|
||||
: [
|
||||
(val) => (val && val.length > 0) || $t('form.error.required'),
|
||||
(val) =>
|
||||
!val ||
|
||||
(val && val.length === 11 && /[0-9]+/.test(val)) ||
|
||||
$t('form.error.invalidCustomeMessage', {
|
||||
msg: $t('form.error.requireLength', { msg: 11 }),
|
||||
|
|
@ -315,25 +340,34 @@ watch(districtId, fetchSubDistrict);
|
|||
]
|
||||
"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
outlined
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
v-model="employmentOffice"
|
||||
:model-value="office"
|
||||
:dense="dense"
|
||||
:label="$t('customer.form.employmentOffice')"
|
||||
:readonly="readonly || sameWithEmployer"
|
||||
readonly
|
||||
:disable="!readonly && !sameWithEmployer"
|
||||
:for="`${prefixId}-${indexId !== undefined ? `input-address-${indexId}` : 'input-address'}`"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (employmentOffice = v) : '')
|
||||
"
|
||||
/>
|
||||
<q-input
|
||||
outlined
|
||||
hide-bottom-space
|
||||
class="col"
|
||||
v-model="employmentOfficeEN"
|
||||
:model-value="officeEn"
|
||||
:dense="dense"
|
||||
:label="`${$t('customer.form.employmentOffice')} (EN)`"
|
||||
:readonly="readonly || sameWithEmployer"
|
||||
readonly
|
||||
:disable="!readonly && !sameWithEmployer"
|
||||
:for="`${prefixId}-${indexId !== undefined ? `input-address-${indexId}` : 'input-address'}`"
|
||||
@update:model-value="
|
||||
(v) => (typeof v === 'string' ? (employmentOfficeEN = v) : '')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
export { default as AddButton } from './AddButton.vue';
|
||||
export { default as AllAroundBtn } from './AllAroundBtn.vue';
|
||||
export { default as ButtonAddCompoent } from './ButtonAddCompoent.vue';
|
||||
export { default as FloatingActionButton } from './FloatingActionButton.vue';
|
||||
export { default as CanvasComponent } from './CanvasComponent.vue';
|
||||
export { default as DialogForm } from './DialogForm.vue';
|
||||
export { default as DrawerInfo } from './DrawerInfo.vue';
|
||||
|
|
@ -15,6 +14,5 @@ export { default as ProfileBanner } from './ProfileBanner.vue';
|
|||
export { default as ProfileUpload } from './ProfileUpload.vue';
|
||||
export { default as SideMenu } from './SideMenu.vue';
|
||||
export { default as StatCardComponent } from './StatCardComponent.vue';
|
||||
export { default as TabComponent } from './TabComponent.vue';
|
||||
export { default as TooltipComponent } from './TooltipComponent.vue';
|
||||
export { default as TreeComponent } from './TreeComponent.vue';
|
||||
|
|
|
|||
76
src/components/shared/AvatarGroup.vue
Normal file
76
src/components/shared/AvatarGroup.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
data?: Record<string, unknown>[];
|
||||
dataLabel?: string;
|
||||
dataUrl?: string;
|
||||
}>(),
|
||||
{
|
||||
dataLabel: 'name',
|
||||
dataUrl: 'imgUrl',
|
||||
data: () => [],
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="avatar-group">
|
||||
<div class="avatar" v-for="(person, i) in data.slice(0, 3)" :key="i">
|
||||
<q-tooltip>
|
||||
{{ person[dataLabel] }}
|
||||
</q-tooltip>
|
||||
<img
|
||||
:src="
|
||||
typeof person[dataUrl] === 'string' ? (person[dataUrl] as string) : ''
|
||||
"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="data.length > 3" class="avatar remaining-count">
|
||||
<q-tooltip>
|
||||
<div v-for="(person, i) in data.slice(3)" :key="i + 3">
|
||||
{{ person.name }}
|
||||
</div>
|
||||
</q-tooltip>
|
||||
<span>{{ `+${data.length - 3}` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.avatar {
|
||||
position: relative;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.avatar:not(:first-child) {
|
||||
margin-left: -0.75rem;
|
||||
}
|
||||
.avatar:hover {
|
||||
z-index: 1;
|
||||
transform: translateY(-0.5rem);
|
||||
}
|
||||
.avatar img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
.remaining-count {
|
||||
color: hsl(var(--text-mute-2));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: var(--surface-2);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 50%;
|
||||
font-size: 0.8rem;
|
||||
margin-left: -0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -9,6 +9,7 @@ const model = defineModel<string | Date | null>();
|
|||
const props = defineProps<{
|
||||
id?: string;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
clearable?: boolean;
|
||||
label?: string;
|
||||
bgColor?: string;
|
||||
|
|
@ -24,7 +25,7 @@ function valueUpdate(value: string) {
|
|||
}
|
||||
|
||||
if (value.length === 10) {
|
||||
const _date = parseAndFormatDate(value, i18n.locale.value);
|
||||
const _date = parseAndFormatDate(value);
|
||||
|
||||
if (_date) {
|
||||
if (Array.isArray(props.disabledDates)) {
|
||||
|
|
@ -59,7 +60,7 @@ function valueUpdate(value: string) {
|
|||
auto-apply
|
||||
:id
|
||||
:for="id"
|
||||
:disabled="readonly"
|
||||
:disabled="readonly || disabled"
|
||||
:disabled-dates="disabledDates"
|
||||
:teleport="true"
|
||||
:dark="$q.dark.isActive"
|
||||
|
|
@ -78,7 +79,8 @@ function valueUpdate(value: string) {
|
|||
:rules
|
||||
:label
|
||||
:for="id"
|
||||
:readonly="readonly"
|
||||
:readonly="readonly || disabled"
|
||||
:disable="disabled"
|
||||
:mask="readonly ? '' : '##/##/####'"
|
||||
:model-value="
|
||||
model
|
||||
|
|
|
|||
|
|
@ -6,12 +6,16 @@ import { watch } from 'vue';
|
|||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
idName: string;
|
||||
status: string;
|
||||
idName?: string;
|
||||
status?: string;
|
||||
hideToggle?: boolean;
|
||||
hideEdit?: boolean;
|
||||
hideView?: boolean;
|
||||
hideDelete?: boolean;
|
||||
useLink?: boolean;
|
||||
useUpload?: boolean;
|
||||
|
||||
useCancel?: boolean;
|
||||
disableCancel?: boolean;
|
||||
disableDelete?: boolean;
|
||||
}>(),
|
||||
{
|
||||
|
|
@ -26,6 +30,7 @@ defineEmits<{
|
|||
(e: 'link'): void;
|
||||
(e: 'upload'): void;
|
||||
(e: 'delete'): void;
|
||||
(e: 'cancel'): void;
|
||||
(e: 'changeStatus'): void;
|
||||
}>();
|
||||
|
||||
|
|
@ -53,6 +58,7 @@ watch(
|
|||
<q-menu class="bordered" ref="refMenu" :key="idName">
|
||||
<q-list>
|
||||
<q-item
|
||||
v-if="!hideView"
|
||||
v-close-popup
|
||||
dense
|
||||
clickable
|
||||
|
|
@ -73,7 +79,7 @@ watch(
|
|||
</q-item>
|
||||
|
||||
<q-item
|
||||
v-if="status !== 'INACTIVE'"
|
||||
v-if="status !== 'INACTIVE' && !hideEdit"
|
||||
v-close-popup
|
||||
dense
|
||||
clickable
|
||||
|
|
@ -136,7 +142,7 @@ watch(
|
|||
</q-item>
|
||||
|
||||
<q-item
|
||||
v-if="status !== 'INACTIVE'"
|
||||
v-if="status !== 'INACTIVE' && !hideDelete"
|
||||
v-close-popup
|
||||
dense
|
||||
class="row"
|
||||
|
|
@ -158,7 +164,38 @@ watch(
|
|||
}"
|
||||
/>
|
||||
<span class="col-9 q-px-md flex items-center">
|
||||
{{ $t('general.delete') }}
|
||||
<slot name="labelDelete">
|
||||
{{ $t('general.delete') }}
|
||||
</slot>
|
||||
</span>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
v-if="useCancel"
|
||||
v-close-popup
|
||||
dense
|
||||
class="row"
|
||||
style="white-space: nowrap"
|
||||
:clickable="!disableCancel"
|
||||
:id="`btn-kebab-delete-${idName}`"
|
||||
:class="{
|
||||
'surface-3': disableCancel,
|
||||
'app-text-muted': disableCancel,
|
||||
}"
|
||||
@click.stop="() => $emit('cancel')"
|
||||
>
|
||||
<q-icon
|
||||
size="xs"
|
||||
name="mdi-cancel"
|
||||
class="col-3"
|
||||
:class="{
|
||||
'app-text-negative': !disableCancel,
|
||||
}"
|
||||
/>
|
||||
<span class="col-9 q-px-md flex items-center">
|
||||
<slot name="labelDelete">
|
||||
{{ $t('general.cancel') }}
|
||||
</slot>
|
||||
</span>
|
||||
</q-item>
|
||||
|
||||
|
|
|
|||
264
src/components/shared/NewPersonCard.vue
Normal file
264
src/components/shared/NewPersonCard.vue
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
import AppBox from 'components/app/AppBox.vue';
|
||||
import AppCircle from 'components/app/AppCircle.vue';
|
||||
import KebabAction from './KebabAction.vue';
|
||||
|
||||
defineProps<{
|
||||
data: {
|
||||
name: string;
|
||||
male?: boolean;
|
||||
female?: boolean;
|
||||
img?: string;
|
||||
fallbackImg?: string;
|
||||
detail?: { icon: string; value: string }[];
|
||||
index: number;
|
||||
};
|
||||
tag?: [{ color: string; value: string }];
|
||||
disabled?: boolean;
|
||||
noHover?: boolean;
|
||||
noBg?: boolean;
|
||||
history?: boolean;
|
||||
prefixId?: string;
|
||||
separateEnter?: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'cancel', index: number): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppBox
|
||||
bordered
|
||||
no-padding
|
||||
class="column person-box"
|
||||
:class="{
|
||||
'person-box__disabled': disabled,
|
||||
'person-box__no-hover': noHover,
|
||||
'person-box__no-detail': !data.detail,
|
||||
'person-box__no-bg': noBg,
|
||||
}"
|
||||
>
|
||||
<div class="column">
|
||||
<!-- kebab menu -->
|
||||
<div class="flex">
|
||||
<div class="col-2 flax">
|
||||
<AppCircle
|
||||
bordered
|
||||
class="avatar relative-position"
|
||||
style="border: 2px solid var(--border-color); overflow: visible"
|
||||
>
|
||||
<q-img
|
||||
v-if="!$slots.img"
|
||||
:src="data.img ?? '/no-profile.png'"
|
||||
loading="lazy"
|
||||
fit="cover"
|
||||
style="width: 100%; height: 100%; border-radius: 50%"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
style="background: none"
|
||||
class="no-padding full-width full-height flex items-center justify-center"
|
||||
>
|
||||
<q-img
|
||||
:src="data.fallbackImg || '/no-profile.png'"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</q-img>
|
||||
<slot name="img"></slot>
|
||||
<q-badge
|
||||
class="absolute-bottom-right"
|
||||
style="
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
z-index: 2;
|
||||
background: var(--border-color);
|
||||
"
|
||||
>
|
||||
<q-badge
|
||||
class="absolute-center"
|
||||
style="
|
||||
border-radius: 7px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: hsl(var(--positive-bg));
|
||||
"
|
||||
></q-badge>
|
||||
<!-- :style="`background: hsl(var(${active ? '--positive-bg' : '--text-mute'}))`" -->
|
||||
</q-badge>
|
||||
</AppCircle>
|
||||
</div>
|
||||
<div class="col column q-pl-xs">
|
||||
<div class="col row">
|
||||
<span style="max-width: calc(100% - 35%)">
|
||||
<span class="row items-center ellipsis">
|
||||
<div class="ellipsis" style="max-width: calc(100% - 30%)">
|
||||
{{ data.name }}
|
||||
<q-tooltip>
|
||||
{{ data.name }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<Icon
|
||||
v-if="data.male || data.female"
|
||||
class="q-pl-xs"
|
||||
:class="{
|
||||
'symbol-gender': data.male || data.female,
|
||||
'symbol-gender__male': !disabled && data.male,
|
||||
'symbol-gender__female': !disabled && data.female,
|
||||
'symbol-gender__disable': disabled,
|
||||
}"
|
||||
:icon="`material-symbols:${data.male ? 'male' : 'female'}`"
|
||||
width="24px"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="row items-center q-gutter-x-xs q-ml-auto">
|
||||
<q-btn
|
||||
:id="`btn-history-${prefixId}`"
|
||||
flat
|
||||
round
|
||||
class="app-text-muted-2"
|
||||
padding="xs"
|
||||
icon="mdi-close-circle"
|
||||
size="sm"
|
||||
@click.stop="$emit('cancel', data.index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- profile -->
|
||||
|
||||
<!-- name symbol -->
|
||||
</div>
|
||||
|
||||
<!-- detail -->
|
||||
</AppBox>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.person-box {
|
||||
background-color: var(--surface-1);
|
||||
transition: 100ms ease-in-out;
|
||||
padding: var(--size-2);
|
||||
|
||||
&.person-box__disabled {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.9);
|
||||
|
||||
.status-circle {
|
||||
background-color: var(--surface-1);
|
||||
}
|
||||
}
|
||||
|
||||
&.person-box__no-detail {
|
||||
padding-block: 2rem;
|
||||
}
|
||||
|
||||
&.person-box__no-bg {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
& .detail-icon {
|
||||
color: hsl(var(--text-mute-2));
|
||||
background-color: hsla(var(--stone-6-hsl) / 0.15);
|
||||
border-radius: 50%;
|
||||
scale: 0.8;
|
||||
}
|
||||
|
||||
& .bg-gender {
|
||||
color: hsla(var(--_fg));
|
||||
background-color: hsl(var(--_bg));
|
||||
|
||||
&.bg-gender__disable {
|
||||
--_bg: var(--stone-6-hsl);
|
||||
background-color: hsla(var(--_bg) / 0.3);
|
||||
}
|
||||
|
||||
&.bg-gender__light {
|
||||
color: unset;
|
||||
background-color: hsla(var(--_bg) / 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
& .symbol-gender {
|
||||
color: hsla(var(--_fg));
|
||||
|
||||
&.symbol-gender__disable {
|
||||
--_fg: var(--stone-6-hsl);
|
||||
}
|
||||
|
||||
&.symbol-gender__male {
|
||||
--_fg: var(--gender-male);
|
||||
}
|
||||
|
||||
&.symbol-gender__female {
|
||||
--_fg: var(--gender-female);
|
||||
}
|
||||
}
|
||||
|
||||
& .status-circle {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--teal-6);
|
||||
border: 2px solid var(--border-color);
|
||||
bottom: 0.6rem;
|
||||
right: 0.6rem;
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:not(.person-box__disabled):not(.person-box__no-hover):hover {
|
||||
--_hover: hsl(var(--gender-male));
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
}
|
||||
|
||||
.person-container {
|
||||
display: grid;
|
||||
gap: var(--size-6);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
block-size: 2.5rem;
|
||||
/* transform: rotate(45deg); */
|
||||
|
||||
& .avatar__status {
|
||||
content: ' ';
|
||||
display: block;
|
||||
block-size: 1rem;
|
||||
aspect-ratio: 1;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
right: -0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
top: calc(50% - 0.5rem);
|
||||
bottom: calc(50% - 0.5rem);
|
||||
background-color: hsla(var(--positive-bg) / 1);
|
||||
}
|
||||
|
||||
/* & :deep(.q-img) {
|
||||
transform: rotate(-45deg);
|
||||
} */
|
||||
}
|
||||
|
||||
.edit-profile {
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 80%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -21,7 +21,7 @@ defineProps<{
|
|||
noAction?: boolean;
|
||||
noBg?: boolean;
|
||||
history?: boolean;
|
||||
prefixId: string;
|
||||
prefixId?: string;
|
||||
separateEnter?: boolean;
|
||||
}>();
|
||||
|
||||
|
|
@ -99,6 +99,7 @@ defineEmits<{
|
|||
<q-img
|
||||
v-if="!$slots.img"
|
||||
:src="data.img ?? '/no-profile.png'"
|
||||
loading="lazy"
|
||||
fit="cover"
|
||||
style="width: 100%; height: 100%; border-radius: 50%"
|
||||
>
|
||||
|
|
@ -140,8 +141,11 @@ defineEmits<{
|
|||
<span
|
||||
class="items-center justify-center row text-center ellipsis col-6"
|
||||
>
|
||||
<div class="items-center ellipsis" style="max-width: 140px">
|
||||
<div class="ellipsis" style="max-width: calc(100% - 30%)">
|
||||
{{ data.name }}
|
||||
<q-tooltip>
|
||||
{{ data.name }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<Icon
|
||||
v-if="data.male || data.female"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="T extends Record<string, unknown>">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { selectFilterOptionRefMod } from 'src/stores/utils';
|
||||
import { QSelect } from 'quasar';
|
||||
|
||||
const model = defineModel<string | null>();
|
||||
const model = defineModel<string | string[] | Record<string, unknown> | null>();
|
||||
|
||||
const options = ref<Record<string, unknown>[]>([]);
|
||||
let defaultFilter: (
|
||||
|
|
@ -15,9 +15,9 @@ const props = withDefaults(
|
|||
defineProps<{
|
||||
id?: string;
|
||||
label?: string;
|
||||
option: Record<string, unknown>[];
|
||||
optionLabel?: string;
|
||||
optionValue?: string;
|
||||
option: T[];
|
||||
optionLabel?: keyof T;
|
||||
optionValue?: keyof T;
|
||||
placeholder?: string;
|
||||
|
||||
hideSelected?: boolean;
|
||||
|
|
@ -26,6 +26,7 @@ const props = withDefaults(
|
|||
incremental?: boolean;
|
||||
fillInput?: boolean;
|
||||
disable?: boolean;
|
||||
multiple?: boolean;
|
||||
|
||||
rules?: ((value: string) => string | true)[];
|
||||
}>(),
|
||||
|
|
@ -40,14 +41,18 @@ const props = withDefaults(
|
|||
);
|
||||
|
||||
defineEmits<{
|
||||
(e: 'filter', val: string, update: void): void;
|
||||
(
|
||||
e: 'filter',
|
||||
val: string,
|
||||
update: (callbackFn: () => void, afterFn?: (ref: QSelect) => void) => void,
|
||||
): void;
|
||||
}>();
|
||||
|
||||
onMounted(() => {
|
||||
defaultFilter = selectFilterOptionRefMod(
|
||||
ref(props.option),
|
||||
options,
|
||||
props.optionLabel,
|
||||
typeof props.optionLabel === 'string' ? props.optionLabel : 'label',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -57,7 +62,7 @@ watch(
|
|||
defaultFilter = selectFilterOptionRefMod(
|
||||
ref(props.option),
|
||||
options,
|
||||
props.optionLabel,
|
||||
typeof props.optionLabel === 'string' ? props.optionLabel : 'label',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -71,15 +76,22 @@ watch(
|
|||
use-input
|
||||
emit-value
|
||||
map-options
|
||||
:hideSelected
|
||||
:multiple
|
||||
:use-chips="multiple"
|
||||
:hide-selected
|
||||
hide-bottom-space
|
||||
:fill-input="fillInput && !!model"
|
||||
:hide-dropdown-icon="readonly"
|
||||
input-debounce="0"
|
||||
:option-value="optionValue"
|
||||
:option-label="optionLabel"
|
||||
input-debounce="500"
|
||||
:option-value="
|
||||
typeof props.optionValue === 'string' ? props.optionValue : 'value'
|
||||
"
|
||||
:option-label="
|
||||
typeof props.optionLabel === 'string' ? props.optionLabel : 'label'
|
||||
"
|
||||
v-model="model"
|
||||
dense
|
||||
autocomplete="off"
|
||||
:readonly
|
||||
:label="label"
|
||||
:options="incremental ? option : options"
|
||||
|
|
@ -91,8 +103,15 @@ watch(
|
|||
"
|
||||
:rules
|
||||
>
|
||||
<template v-if="$slots.prepend" v-slot:prepend>
|
||||
<slot name="prepend"></slot>
|
||||
</template>
|
||||
<template v-if="$slots.append" v-slot:append>
|
||||
<slot name="append"></slot>
|
||||
</template>
|
||||
|
||||
<template v-slot:no-option>
|
||||
<slot name="noOption"></slot>
|
||||
<slot name="no-option"></slot>
|
||||
|
||||
<q-item v-if="!$slots.noOption">
|
||||
<q-item-section class="text-grey">
|
||||
|
|
@ -101,12 +120,19 @@ watch(
|
|||
</q-item>
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.selectedItem" v-slot:selected-item="scope">
|
||||
<slot name="selectedItem" :scope="scope"></slot>
|
||||
<template
|
||||
v-if="$slots.selectedItem || $slots['selected-item']"
|
||||
v-slot:selected-item="scope"
|
||||
>
|
||||
<slot name="selected-item" :scope="scope" :opt="scope.opt as T"></slot>
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.option" v-slot:option="scope">
|
||||
<slot name="option" :scope="scope"></slot>
|
||||
<slot name="option" :scope="scope" :opt="scope.opt as T"></slot>
|
||||
</template>
|
||||
|
||||
<template v-if="$slots['before-options']" #before-options>
|
||||
<slot name="before-options"></slot>
|
||||
</template>
|
||||
</q-select>
|
||||
</template>
|
||||
|
|
|
|||
112
src/components/shared/SelectMenuWithSearch.vue
Normal file
112
src/components/shared/SelectMenuWithSearch.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
readonly?: boolean;
|
||||
|
||||
title?: string;
|
||||
width?: string;
|
||||
offset?: number[];
|
||||
|
||||
option: Record<string, unknown>[];
|
||||
optionLabel?: string;
|
||||
separatorIndex?: number[];
|
||||
}>(),
|
||||
{
|
||||
readonly: false,
|
||||
option: () => [],
|
||||
optionLabel: 'label',
|
||||
offset: () => [0, 12],
|
||||
separatorIndex: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
const inputSearch = ref('');
|
||||
|
||||
defineEmits<{
|
||||
(e: 'search', value: string | number | null): void;
|
||||
(e: 'select', value: Record<string, unknown>): void;
|
||||
(e: 'hide'): void;
|
||||
(e: 'show'): void;
|
||||
(e: 'beforeHide'): void;
|
||||
(e: 'beforeShow'): void;
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<q-menu
|
||||
v-if="!readonly"
|
||||
:offset
|
||||
class="bordered"
|
||||
:style="`width: ${width}`"
|
||||
@show="$emit('show')"
|
||||
@hide="$emit('hide')"
|
||||
@before-show="
|
||||
() => {
|
||||
inputSearch = '';
|
||||
$emit('beforeShow');
|
||||
}
|
||||
"
|
||||
@before-hide="$emit('beforeHide')"
|
||||
>
|
||||
<div
|
||||
v-if="title"
|
||||
class="column no-padding"
|
||||
style="padding: 16px !important"
|
||||
>
|
||||
<span class="text-weight-bold q-pb-sm">
|
||||
{{ title }}
|
||||
</span>
|
||||
|
||||
<q-input
|
||||
for="input-search"
|
||||
outlined
|
||||
dense
|
||||
:label="$t('general.search')"
|
||||
class="col responsible-search"
|
||||
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
|
||||
v-model="inputSearch"
|
||||
debounce="200"
|
||||
@update:model-value="(val) => $emit('search', val)"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<template v-if="$slots.prepend">
|
||||
<slot name="prepend"></slot>
|
||||
</template>
|
||||
|
||||
<span v-if="option.length === 0">
|
||||
<q-item dense class="flex items-center app-text-muted">
|
||||
{{ $t('general.noData') }}
|
||||
</q-item>
|
||||
</span>
|
||||
<template v-if="option.length > 0">
|
||||
<q-item
|
||||
v-for="(opt, i) in option"
|
||||
dense
|
||||
:key="i"
|
||||
class="flex items-center"
|
||||
:class="{ 'bordered-t': separatorIndex.includes(i) }"
|
||||
clickable
|
||||
@click.stop="$emit('select', opt)"
|
||||
>
|
||||
<template v-if="$slots.option">
|
||||
<slot name="option" :opt="opt"></slot>
|
||||
</template>
|
||||
|
||||
<span v-if="!$slots.option" class="row items-center">
|
||||
{{ opt.label }}
|
||||
</span>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.append">
|
||||
<slot name="append"></slot>
|
||||
</template>
|
||||
</q-menu>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
9
src/components/shared/SwitchItem.vue
Normal file
9
src/components/shared/SwitchItem.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot :name="value" />
|
||||
</template>
|
||||
104
src/components/shared/select-muliple/SelectOffice.vue
Normal file
104
src/components/shared/select-muliple/SelectOffice.vue
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { createSelect, SelectProps } from './select-multiple';
|
||||
import SelectInput from '../SelectInput.vue';
|
||||
|
||||
import useAddressStore, { Office } from 'stores/address';
|
||||
|
||||
type SelectOption = Office;
|
||||
|
||||
const value = defineModel<string[] | null | undefined>('value', {
|
||||
required: true,
|
||||
});
|
||||
const valueOption = defineModel<SelectOption[]>('valueOption', {
|
||||
required: false,
|
||||
});
|
||||
|
||||
const selectOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const { fetchOffice: getList, fetchOfficeById: getById } = useAddressStore();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {};
|
||||
|
||||
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||
|
||||
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||
createSelect<SelectOption>(
|
||||
{
|
||||
value,
|
||||
valueOption,
|
||||
selectOptions,
|
||||
getList: async (query) => {
|
||||
const ret = await getList({
|
||||
query,
|
||||
...props.params,
|
||||
});
|
||||
if (ret) return ret;
|
||||
},
|
||||
getByValue: async (id) => {
|
||||
const ret = await getById(id);
|
||||
if (ret) return ret;
|
||||
},
|
||||
},
|
||||
{ valueField: 'id' },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await getOptions();
|
||||
|
||||
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||
setFirstValue();
|
||||
}
|
||||
|
||||
await getSelectedOption();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<SelectInput
|
||||
v-model="value"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
incremental
|
||||
multiple
|
||||
:label
|
||||
:placeholder
|
||||
:readonly
|
||||
:disable="disabled"
|
||||
:option="selectOptions"
|
||||
:hide-selected="false"
|
||||
:fill-input="false"
|
||||
:rules="[(v: string) => !!v || $t('form.error.required')]"
|
||||
@filter="filter"
|
||||
>
|
||||
<template #before-options v-if="creatable">
|
||||
<q-item clickable v-close-popup @click.stop="$emit('create')">
|
||||
<q-item-section>
|
||||
<b class="row items-center">
|
||||
<q-icon
|
||||
name="mdi-plus-circle-outline"
|
||||
class="q-mr-sm"
|
||||
style="color: hsl(var(--positive-bg))"
|
||||
/>
|
||||
{{ $t('general.add', { text: $t('quotation.newCustomer') }) }}
|
||||
</b>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator class="q-mx-sm" />
|
||||
</template>
|
||||
|
||||
<template #append v-if="clearable">
|
||||
<q-icon
|
||||
v-if="!readonly && value"
|
||||
name="mdi-close-circle"
|
||||
@click.stop="value = []"
|
||||
class="cursor-pointer clear-btn"
|
||||
/>
|
||||
</template>
|
||||
</SelectInput>
|
||||
</template>
|
||||
110
src/components/shared/select-muliple/select-multiple.ts
Normal file
110
src/components/shared/select-muliple/select-multiple.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { QSelect } from 'quasar';
|
||||
import { ref, Ref, watch } from 'vue';
|
||||
|
||||
export type SelectProps<T extends (...args: any[]) => any> = {
|
||||
params?: Parameters<T>[0];
|
||||
creatable?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
clearable?: boolean;
|
||||
autoSelectOnSingle?: boolean;
|
||||
};
|
||||
|
||||
export const createSelect = <T extends Record<string, any>>(
|
||||
state: {
|
||||
value: Ref<string[] | null | undefined>;
|
||||
valueOption: Ref<T[] | undefined>;
|
||||
selectOptions: Ref<T[]>;
|
||||
getByValue: (id: string) => Promise<T | void> | T | void;
|
||||
getList: (query?: string) => Promise<T[] | void> | T[] | void;
|
||||
},
|
||||
opts?: {
|
||||
valueField?: keyof T;
|
||||
},
|
||||
) => {
|
||||
const { value, valueOption, selectOptions, getList, getByValue } = state;
|
||||
|
||||
const valueField = opts?.valueField || 'value';
|
||||
|
||||
let cache: T[];
|
||||
let previousSearch = '';
|
||||
|
||||
watch(value, (v) => {
|
||||
console.log('UPDATED');
|
||||
if (
|
||||
!v ||
|
||||
(cache && cache.find((opt) => v.find((val) => val === opt[valueField])))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
getSelectedOption();
|
||||
});
|
||||
|
||||
async function getOptions(query?: string) {
|
||||
if (cache && selectOptions.value.length > 0 && previousSearch === query) {
|
||||
selectOptions.value = JSON.parse(JSON.stringify(cache));
|
||||
return;
|
||||
}
|
||||
const ret = await getList(query);
|
||||
if (ret) {
|
||||
cache = ret;
|
||||
selectOptions.value = JSON.parse(JSON.stringify(cache));
|
||||
previousSearch = query || previousSearch;
|
||||
}
|
||||
}
|
||||
|
||||
async function setFirstValue() {
|
||||
if (value.value) return;
|
||||
const first = selectOptions.value.at(0);
|
||||
if (first) value.value = first[valueField];
|
||||
}
|
||||
|
||||
async function getSelectedOption() {
|
||||
const currentValue = value.value;
|
||||
|
||||
if (!currentValue) return;
|
||||
if (selectOptions.value.find((v) => v[valueField] === currentValue)) return;
|
||||
|
||||
const newValueOptions = currentValue.map(async (a) => {
|
||||
const findValue = valueOption.value?.find((b) => b[valueField] === a);
|
||||
|
||||
if (findValue) {
|
||||
selectOptions.value.unshift(findValue);
|
||||
return findValue;
|
||||
}
|
||||
|
||||
const ret = await getByValue(a);
|
||||
|
||||
if (ret) {
|
||||
selectOptions.value.unshift(ret);
|
||||
return ret;
|
||||
}
|
||||
});
|
||||
|
||||
const retValueOptions = await Promise.all(newValueOptions);
|
||||
|
||||
valueOption.value = retValueOptions.flatMap((v) => (!!v ? v : []));
|
||||
}
|
||||
|
||||
type QuasarSelectUpdate = (
|
||||
callback: () => void,
|
||||
afterFn?: ((ref: QSelect) => void) | undefined,
|
||||
) => void;
|
||||
|
||||
function filter(value: string, update: QuasarSelectUpdate) {
|
||||
update(
|
||||
() => getOptions(value),
|
||||
(ref) => {
|
||||
if (!!value && ref.options && ref.options.length > 0) {
|
||||
ref.setOptionIndex(-1);
|
||||
ref.moveOptionSelection(1, true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return { getOptions, setFirstValue, getSelectedOption, filter };
|
||||
};
|
||||
117
src/components/shared/select/SelectBranch.vue
Normal file
117
src/components/shared/select/SelectBranch.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { getRole } from 'src/services/keycloak';
|
||||
|
||||
import { createSelect, SelectProps } from './select';
|
||||
import SelectInput from '../SelectInput.vue';
|
||||
|
||||
import { Branch } from 'src/stores/branch/types';
|
||||
|
||||
import useStore from 'src/stores/branch';
|
||||
|
||||
type SelectOption = Branch;
|
||||
|
||||
const value = defineModel<string | null | undefined>('value', {
|
||||
required: true,
|
||||
});
|
||||
const valueOption = defineModel<SelectOption>('valueOption', {
|
||||
required: false,
|
||||
});
|
||||
|
||||
const selectOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const { fetchList: getList, fetchById: getById } = useStore();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {
|
||||
codeOnly?: boolean;
|
||||
selectFirstValue?: boolean;
|
||||
branchVirtual?: boolean;
|
||||
checkRole?: string[];
|
||||
};
|
||||
|
||||
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||
|
||||
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||
createSelect<SelectOption>(
|
||||
{
|
||||
value,
|
||||
valueOption,
|
||||
selectOptions,
|
||||
getList: async (query) => {
|
||||
const ret = await getList({
|
||||
query,
|
||||
...props.params,
|
||||
activeOnly: true,
|
||||
});
|
||||
if (ret) return ret.result;
|
||||
},
|
||||
getByValue: async (id) => {
|
||||
const ret = await getById(id);
|
||||
if (ret) return ret;
|
||||
},
|
||||
},
|
||||
{ valueField: 'id' },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await getOptions();
|
||||
|
||||
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||
setFirstValue();
|
||||
}
|
||||
|
||||
if (props.selectFirstValue) {
|
||||
setDefaultValue();
|
||||
} else await getSelectedOption();
|
||||
});
|
||||
|
||||
function setDefaultValue() {
|
||||
setFirstValue();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<SelectInput
|
||||
v-model="value"
|
||||
incremental
|
||||
:label
|
||||
:placeholder
|
||||
:readonly
|
||||
:disable="disabled"
|
||||
:option="
|
||||
selectOptions.map((v) => {
|
||||
const ret = {
|
||||
label: codeOnly
|
||||
? v.code
|
||||
: `${branchVirtual ? $t('branch.card.branchVirtual') : ''}` +
|
||||
(
|
||||
{
|
||||
['eng']: [v.nameEN, `(${v.code})`].join(' '),
|
||||
['tha']: [v.name, `(${v.code})`].join(' '),
|
||||
} as const
|
||||
)[$i18n.locale],
|
||||
value: v.id,
|
||||
};
|
||||
return ret;
|
||||
})
|
||||
"
|
||||
hide-selected
|
||||
fill-input
|
||||
:rules="[
|
||||
(v: string) => !props.required || !!v || $t('form.error.required'),
|
||||
]"
|
||||
@filter="filter"
|
||||
>
|
||||
<template #append v-if="clearable">
|
||||
<q-icon
|
||||
v-if="!readonly && value"
|
||||
name="mdi-close-circle"
|
||||
@click.stop="value = ''"
|
||||
class="cursor-pointer clear-btn"
|
||||
/>
|
||||
</template>
|
||||
</SelectInput>
|
||||
</template>
|
||||
174
src/components/shared/select/SelectCustomer.vue
Normal file
174
src/components/shared/select/SelectCustomer.vue
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { createSelect, SelectProps } from './select';
|
||||
import SelectInput from '../SelectInput.vue';
|
||||
import SelectCustomerItem from './SelectCustomerItem.vue';
|
||||
|
||||
import useStore, { Customer, CustomerBranch } from 'src/stores/customer';
|
||||
|
||||
type SelectOption = CustomerBranch & { customer: Customer };
|
||||
|
||||
const value = defineModel<string | null | undefined>('value', {
|
||||
required: true,
|
||||
});
|
||||
const valueOption = defineModel<SelectOption>('valueOption', {
|
||||
required: false,
|
||||
});
|
||||
|
||||
const selectOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const {
|
||||
fetchListCustomerBranch: getList,
|
||||
fetchListCustomerBranchById: getById,
|
||||
} = useStore();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {
|
||||
simple?: boolean;
|
||||
simpleBranchNo?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||
|
||||
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||
createSelect<SelectOption>(
|
||||
{
|
||||
value,
|
||||
valueOption,
|
||||
selectOptions,
|
||||
getList: async (query) => {
|
||||
const ret = await getList({
|
||||
query,
|
||||
...props.params,
|
||||
activeRegisBranchOnly: true,
|
||||
includeCustomer: true,
|
||||
});
|
||||
if (ret) return ret.result;
|
||||
},
|
||||
getByValue: async (id) => {
|
||||
const ret = await getById(id);
|
||||
if (ret) return ret;
|
||||
},
|
||||
},
|
||||
{ valueField: 'id' },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await getOptions();
|
||||
|
||||
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||
setFirstValue();
|
||||
}
|
||||
|
||||
await getSelectedOption();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<SelectInput
|
||||
v-model="value"
|
||||
option-value="id"
|
||||
incremental
|
||||
:label
|
||||
:placeholder
|
||||
:readonly
|
||||
:disable="disabled"
|
||||
:option="selectOptions"
|
||||
:hide-selected="false"
|
||||
:fill-input="false"
|
||||
:rules="[(v: string) => !!v || $t('form.error.required')]"
|
||||
@filter="filter"
|
||||
>
|
||||
<template #selected-item="{ opt }">
|
||||
<SelectCustomerItem
|
||||
v-if="typeof opt === 'object'"
|
||||
:data="opt"
|
||||
:simple
|
||||
:simple-branch-no
|
||||
single-line
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #no-option v-if="creatable">
|
||||
<q-item
|
||||
:disable="creatableDisabled"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click.stop="$emit('create')"
|
||||
>
|
||||
<q-item-section>
|
||||
<span class="row items-center">
|
||||
<q-icon
|
||||
name="mdi-plus-circle-outline"
|
||||
class="q-mr-sm"
|
||||
style="color: hsl(var(--positive-bg))"
|
||||
/>
|
||||
<b>
|
||||
{{ $t('general.add', { text: $t('quotation.newCustomer') }) }}
|
||||
</b>
|
||||
<span
|
||||
v-if="creatableDisabled && creatableDisabledText"
|
||||
class="app-text-muted q-pl-xs"
|
||||
style="font-size: 80%"
|
||||
>
|
||||
{{ creatableDisabledText }}
|
||||
</span>
|
||||
</span>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator class="q-mx-sm" />
|
||||
</template>
|
||||
|
||||
<template #before-options v-if="creatable">
|
||||
<q-item
|
||||
:disable="creatableDisabled"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click.stop="$emit('create')"
|
||||
>
|
||||
<q-item-section>
|
||||
<span class="row items-center">
|
||||
<q-icon
|
||||
name="mdi-plus-circle-outline"
|
||||
class="q-mr-sm"
|
||||
style="color: hsl(var(--positive-bg))"
|
||||
/>
|
||||
<b>
|
||||
{{ $t('general.add', { text: $t('quotation.newCustomer') }) }}
|
||||
</b>
|
||||
<span
|
||||
v-if="creatableDisabled && creatableDisabledText"
|
||||
class="app-text-muted q-pl-xs"
|
||||
style="font-size: 80%"
|
||||
>
|
||||
{{ creatableDisabledText }}
|
||||
</span>
|
||||
</span>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator class="q-mx-sm" />
|
||||
</template>
|
||||
|
||||
<template #option="{ opt, scope }">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<SelectCustomerItem :data="opt" :simple :simple-branch-no />
|
||||
</q-item>
|
||||
|
||||
<q-separator class="q-mx-sm" />
|
||||
</template>
|
||||
|
||||
<template #append v-if="clearable">
|
||||
<q-icon
|
||||
v-if="!readonly && value"
|
||||
name="mdi-close-circle"
|
||||
@click.stop="value = ''"
|
||||
class="cursor-pointer clear-btn"
|
||||
/>
|
||||
</template>
|
||||
</SelectInput>
|
||||
</template>
|
||||
106
src/components/shared/select/SelectCustomerItem.vue
Normal file
106
src/components/shared/select/SelectCustomerItem.vue
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<script lang="ts" setup>
|
||||
import { Customer, CustomerBranch, CustomerType } from 'src/stores/customer';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import { formatAddress } from 'src/utils/address';
|
||||
import { Lang } from 'src/utils/ui';
|
||||
|
||||
defineProps<{
|
||||
data?: CustomerBranch & { customer: Customer };
|
||||
simple?: boolean;
|
||||
simpleBranchNo?: boolean;
|
||||
singleLine?: boolean;
|
||||
}>();
|
||||
|
||||
const optionStore = useOptionStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="data">
|
||||
<div v-if="simple" class="row items-center">
|
||||
{{
|
||||
{
|
||||
[CustomerType.Corporate]:
|
||||
{
|
||||
[1]: data.registerNameEN?.trim(),
|
||||
[0]: data.registerName?.trim(),
|
||||
}[+($i18n.locale === Lang.English)] || '-',
|
||||
[CustomerType.Person]:
|
||||
{
|
||||
[1]: `${optionStore.mapOption(data.namePrefix)} ${data.firstNameEN} ${data.lastNameEN}`.trim(),
|
||||
[0]: `${optionStore.mapOption(data.namePrefix)} ${data.firstName} ${data.lastName}`.trim(),
|
||||
}[+($i18n.locale === Lang.English)] || '-',
|
||||
}[data.customer.customerType]
|
||||
}}
|
||||
|
||||
({{
|
||||
simpleBranchNo
|
||||
? (
|
||||
$t(
|
||||
`branch.form.title.${data.code.endsWith('-00') ? 'branchHQLabel' : 'branchLabel'}`,
|
||||
) +
|
||||
' ' +
|
||||
(!data.code.endsWith('-00')
|
||||
? String(+data.code.split('-')[1])
|
||||
: '')
|
||||
).trim()
|
||||
: data.code
|
||||
}})
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="{
|
||||
['row']: singleLine,
|
||||
['items-center']: singleLine,
|
||||
}"
|
||||
>
|
||||
<div class="q-mr-sm">
|
||||
<span style="font-weight: 600">
|
||||
{{
|
||||
data.customer.customerType === 'CORP'
|
||||
? $t('customer.form.registerName')
|
||||
: $t('customer.form.ownerName')
|
||||
}}:
|
||||
</span>
|
||||
{{
|
||||
{
|
||||
[CustomerType.Corporate]: {
|
||||
[1]: data.registerNameEN,
|
||||
[0]: data.registerName,
|
||||
}[+($i18n.locale === Lang.English)],
|
||||
[CustomerType.Person]:
|
||||
{
|
||||
[1]: `${optionStore.mapOption(data.namePrefix)} ${data.firstNameEN} ${data.lastNameEN}`,
|
||||
[0]: `${optionStore.mapOption(data.namePrefix)} ${data.firstName} ${data.lastName}`,
|
||||
}[+($i18n.locale === Lang.English)] || '-',
|
||||
}[data.customer.customerType]
|
||||
}}
|
||||
({{ data.code }})
|
||||
</div>
|
||||
<div
|
||||
class="text-caption app-text-muted-2"
|
||||
v-if="data.customer && data.province"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`branch.form.title.${data.code.endsWith('-00') ? 'branchHQLabel' : 'branchLabel'}`,
|
||||
)
|
||||
}}
|
||||
|
||||
{{ !data.code.endsWith('-00') ? +data.code.split('-')[1] : '' }}
|
||||
</div>
|
||||
<div
|
||||
class="text-caption app-text-muted-2"
|
||||
v-if="data.customer && data.province"
|
||||
>
|
||||
{{ $t('general.address') }}
|
||||
{{ formatAddress({ ...data, en: $i18n.locale === Lang.English }) }}
|
||||
<q-tooltip v-if="data.customer && data.province">
|
||||
{{ $t('customerBranch.form.title') }}:
|
||||
|
||||
{{ $t('general.address') }}
|
||||
{{ formatAddress({ ...data, en: $i18n.locale === Lang.English }) }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
108
src/components/shared/select/SelectFlow.vue
Normal file
108
src/components/shared/select/SelectFlow.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { createSelect, SelectProps } from './select';
|
||||
import SelectInput from '../SelectInput.vue';
|
||||
|
||||
import { WorkflowTemplate } from 'src/stores/workflow-template/types';
|
||||
|
||||
import { useWorkflowTemplate } from 'src/stores/workflow-template';
|
||||
|
||||
type SelectOption = WorkflowTemplate;
|
||||
|
||||
const value = defineModel<string | null | undefined>('value', {
|
||||
required: true,
|
||||
});
|
||||
const valueOption = defineModel<SelectOption>('valueOption', {
|
||||
required: false,
|
||||
});
|
||||
|
||||
const selectOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const { getWorkflowTemplateList: getList, getWorkflowTemplate: getById } =
|
||||
useWorkflowTemplate();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
(e: 'updateValue', val: string): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {
|
||||
selectFirstValue?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||
|
||||
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||
createSelect<SelectOption>(
|
||||
{
|
||||
value,
|
||||
valueOption,
|
||||
selectOptions,
|
||||
getList: async (query) => {
|
||||
const ret = await getList({
|
||||
query,
|
||||
...props.params,
|
||||
activeOnly: true,
|
||||
});
|
||||
if (ret) return ret.result;
|
||||
},
|
||||
getByValue: async (id) => {
|
||||
const ret = await getById(id);
|
||||
if (ret) return ret;
|
||||
},
|
||||
},
|
||||
{ valueField: 'id' },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await getOptions();
|
||||
|
||||
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||
setFirstValue();
|
||||
}
|
||||
|
||||
if (props.selectFirstValue) {
|
||||
setDefaultValue();
|
||||
} else await getSelectedOption();
|
||||
});
|
||||
|
||||
function setDefaultValue() {
|
||||
setFirstValue();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<SelectInput
|
||||
v-model="value"
|
||||
incremental
|
||||
:label
|
||||
:placeholder
|
||||
:readonly
|
||||
:disable="disabled"
|
||||
:option="
|
||||
selectOptions.map((v) => {
|
||||
const ret = {
|
||||
label: v.name,
|
||||
value: v.id,
|
||||
};
|
||||
return ret;
|
||||
})
|
||||
"
|
||||
:hide-selected="false"
|
||||
:fill-input="false"
|
||||
:rules="
|
||||
required ? [(v: string) => !!v || $t('form.error.required')] : undefined
|
||||
"
|
||||
@filter="filter"
|
||||
@update:model-value="(v) => $emit('updateValue', v as string)"
|
||||
>
|
||||
<template #append v-if="clearable">
|
||||
<q-icon
|
||||
v-if="!readonly && value"
|
||||
name="mdi-close-circle"
|
||||
@click.stop="value = ''"
|
||||
class="cursor-pointer clear-btn"
|
||||
/>
|
||||
</template>
|
||||
</SelectInput>
|
||||
</template>
|
||||
107
src/components/shared/select/SelectInstitution.vue
Normal file
107
src/components/shared/select/SelectInstitution.vue
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { createSelect, SelectProps } from './select';
|
||||
import SelectInput from '../SelectInput.vue';
|
||||
|
||||
import { Institution } from 'src/stores/institution/types';
|
||||
|
||||
import { useInstitution } from 'src/stores/institution';
|
||||
|
||||
type SelectOption = Institution;
|
||||
|
||||
const value = defineModel<string | null | undefined>('value', {
|
||||
required: true,
|
||||
});
|
||||
const valueOption = defineModel<SelectOption>('valueOption', {
|
||||
required: false,
|
||||
});
|
||||
|
||||
const selectOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const { getInstitutionList: getList, getInstitution: getById } =
|
||||
useInstitution();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
(e: 'updateValue', val: string): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {
|
||||
selectFirstValue?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||
|
||||
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||
createSelect<SelectOption>(
|
||||
{
|
||||
value,
|
||||
valueOption,
|
||||
selectOptions,
|
||||
getList: async (query) => {
|
||||
const ret = await getList({
|
||||
query: query === '' ? undefined : query,
|
||||
...props.params,
|
||||
});
|
||||
if (ret) return ret.result;
|
||||
},
|
||||
getByValue: async (id) => {
|
||||
const ret = await getById(id);
|
||||
if (ret) return ret;
|
||||
},
|
||||
},
|
||||
{ valueField: 'id' },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await getOptions();
|
||||
|
||||
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||
setFirstValue();
|
||||
}
|
||||
|
||||
if (props.selectFirstValue) {
|
||||
setDefaultValue();
|
||||
} else await getSelectedOption();
|
||||
});
|
||||
|
||||
function setDefaultValue() {
|
||||
setFirstValue();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<SelectInput
|
||||
v-model="value"
|
||||
incremental
|
||||
:label
|
||||
:placeholder
|
||||
:readonly
|
||||
:disable="disabled"
|
||||
:option="
|
||||
selectOptions.map((v) => {
|
||||
const ret = {
|
||||
label: v.name,
|
||||
value: v.id,
|
||||
};
|
||||
return ret;
|
||||
})
|
||||
"
|
||||
:hide-selected="false"
|
||||
:fill-input="false"
|
||||
:rules="
|
||||
required ? [(v: string) => !!v || $t('form.error.required')] : undefined
|
||||
"
|
||||
@filter="filter"
|
||||
@update:model-value="(v) => $emit('updateValue', v as string)"
|
||||
>
|
||||
<template #append v-if="clearable">
|
||||
<q-icon
|
||||
v-if="!readonly && value"
|
||||
name="mdi-close-circle"
|
||||
@click.stop="value = ''"
|
||||
class="cursor-pointer clear-btn"
|
||||
/>
|
||||
</template>
|
||||
</SelectInput>
|
||||
</template>
|
||||
124
src/components/shared/select/SelectProductGroup.vue
Normal file
124
src/components/shared/select/SelectProductGroup.vue
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { createSelect, SelectProps } from './select';
|
||||
import SelectInput from '../SelectInput.vue';
|
||||
|
||||
import { ProductGroup } from 'stores/product-service/types';
|
||||
|
||||
import useStore from 'stores/product-service';
|
||||
|
||||
type SelectOption = ProductGroup;
|
||||
|
||||
const value = defineModel<string | null | undefined>('value', {
|
||||
required: true,
|
||||
});
|
||||
const valueOption = defineModel<SelectOption>('valueOption', {
|
||||
required: false,
|
||||
});
|
||||
|
||||
const selectOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const { fetchProductGroup: getList, fetchProductGroupById: getById } =
|
||||
useStore();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {
|
||||
selectFirstValue?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||
|
||||
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||
createSelect<SelectOption>(
|
||||
{
|
||||
value,
|
||||
valueOption,
|
||||
selectOptions,
|
||||
getList: async (query) => {
|
||||
const ret = await getList({
|
||||
query,
|
||||
...props.params,
|
||||
activeOnly: true,
|
||||
});
|
||||
if (ret) return ret.result;
|
||||
},
|
||||
getByValue: async (id) => {
|
||||
const ret = await getById(id);
|
||||
if (ret) return ret;
|
||||
},
|
||||
},
|
||||
{ valueField: 'id' },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await getOptions();
|
||||
|
||||
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||
setFirstValue();
|
||||
}
|
||||
|
||||
if (props.selectFirstValue) {
|
||||
setDefaultValue();
|
||||
} else await getSelectedOption();
|
||||
});
|
||||
|
||||
function setDefaultValue() {
|
||||
setFirstValue();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<SelectInput
|
||||
v-model="value"
|
||||
incremental
|
||||
option-label="code"
|
||||
option-value="id"
|
||||
:label
|
||||
:placeholder
|
||||
:readonly
|
||||
:disable="disabled"
|
||||
:option="selectOptions"
|
||||
:hide-selected="false"
|
||||
:fill-input="false"
|
||||
:rules="[
|
||||
(v: string) => !props.required || !!v || $t('form.error.required'),
|
||||
]"
|
||||
@filter="filter"
|
||||
>
|
||||
<template #append v-if="clearable">
|
||||
<q-icon
|
||||
v-if="!readonly && value"
|
||||
name="mdi-close-circle"
|
||||
@click.stop="value = ''"
|
||||
class="cursor-pointer clear-btn"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #option="{ scope }">
|
||||
<q-item
|
||||
v-if="scope.opt"
|
||||
v-bind="scope.itemProps"
|
||||
class="row items-center"
|
||||
>
|
||||
<q-item-section>
|
||||
{{ scope.opt.name }}
|
||||
<span class="app-text-muted text-caption">
|
||||
{{ scope.opt.code }}
|
||||
</span>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<template #selected-item="{ opt }">
|
||||
<q-item-section v-if="opt">
|
||||
{{ opt.name }}
|
||||
<span class="app-text-muted text-caption">
|
||||
{{ opt.code }}
|
||||
</span>
|
||||
</q-item-section>
|
||||
</template>
|
||||
</SelectInput>
|
||||
</template>
|
||||
108
src/components/shared/select/SelectUser.vue
Normal file
108
src/components/shared/select/SelectUser.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { createSelect, SelectProps } from './select';
|
||||
import SelectInput from '../SelectInput.vue';
|
||||
|
||||
import useStore, { User } from 'src/stores/user';
|
||||
import { Lang } from 'src/utils/ui';
|
||||
|
||||
type SelectOption = User;
|
||||
|
||||
const value = defineModel<string | null | undefined>('value', {
|
||||
required: true,
|
||||
});
|
||||
const valueOption = defineModel<SelectOption>('valueOption', {
|
||||
required: false,
|
||||
});
|
||||
|
||||
const selectOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const { fetchList: getList, fetchById: getById } = useStore();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
type ExclusiveProps = {
|
||||
selectFirstValue?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
|
||||
|
||||
const { getOptions, setFirstValue, getSelectedOption, filter } =
|
||||
createSelect<SelectOption>(
|
||||
{
|
||||
value,
|
||||
valueOption,
|
||||
selectOptions,
|
||||
getList: async (query) => {
|
||||
const ret = await getList({
|
||||
query,
|
||||
...props.params,
|
||||
});
|
||||
if (ret) return ret.result;
|
||||
},
|
||||
getByValue: async (id) => {
|
||||
const ret = await getById(id);
|
||||
if (ret) return ret;
|
||||
},
|
||||
},
|
||||
{ valueField: 'id' },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await getOptions();
|
||||
|
||||
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
|
||||
setFirstValue();
|
||||
}
|
||||
|
||||
if (props.selectFirstValue) {
|
||||
setDefaultValue();
|
||||
} else await getSelectedOption();
|
||||
});
|
||||
|
||||
function setDefaultValue() {
|
||||
setFirstValue();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<SelectInput
|
||||
v-model="value"
|
||||
incremental
|
||||
:label
|
||||
:placeholder
|
||||
:readonly
|
||||
:disable="disabled"
|
||||
:option="
|
||||
selectOptions.map((v) => {
|
||||
const ret = {
|
||||
label: (
|
||||
{
|
||||
[Lang.English]: [v.firstNameEN, v.lastNameEN].join(' '),
|
||||
[Lang.Thai]: [v.firstName, v.lastName].join(' '),
|
||||
} as const
|
||||
)[$i18n.locale],
|
||||
value: v.id,
|
||||
};
|
||||
return ret;
|
||||
})
|
||||
"
|
||||
:hide-selected="false"
|
||||
:fill-input="false"
|
||||
:rules="
|
||||
required ? [(v: string) => !!v || $t('form.error.required')] : undefined
|
||||
"
|
||||
@filter="filter"
|
||||
>
|
||||
<template #append v-if="clearable">
|
||||
<q-icon
|
||||
v-if="!readonly && value"
|
||||
name="mdi-close-circle"
|
||||
@click.stop="value = ''"
|
||||
class="cursor-pointer clear-btn"
|
||||
/>
|
||||
</template>
|
||||
</SelectInput>
|
||||
</template>
|
||||
97
src/components/shared/select/select.ts
Normal file
97
src/components/shared/select/select.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { QSelect } from 'quasar';
|
||||
import { Ref, watch } from 'vue';
|
||||
|
||||
export type SelectProps<T extends (...args: any[]) => any> = {
|
||||
params?: Parameters<T>[0];
|
||||
creatable?: boolean;
|
||||
creatableDisabled?: boolean;
|
||||
creatableDisabledText?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
clearable?: boolean;
|
||||
autoSelectOnSingle?: boolean;
|
||||
};
|
||||
|
||||
export const createSelect = <T extends Record<string, any>>(
|
||||
state: {
|
||||
value: Ref<string | null | undefined>;
|
||||
valueOption: Ref<T | undefined>;
|
||||
selectOptions: Ref<T[]>;
|
||||
getByValue: (id: string) => Promise<T | void> | T | void;
|
||||
getList: (query?: string) => Promise<T[] | void> | T[] | void;
|
||||
},
|
||||
opts?: {
|
||||
valueField?: keyof T;
|
||||
},
|
||||
) => {
|
||||
const { value, valueOption, selectOptions, getList, getByValue } = state;
|
||||
|
||||
const valueField = opts?.valueField || 'value';
|
||||
|
||||
let cache: T[];
|
||||
let previousSearch = '';
|
||||
|
||||
watch(value, (v) => {
|
||||
if (!v || (cache && cache.find((opt) => opt[valueField] === v))) return;
|
||||
getSelectedOption();
|
||||
});
|
||||
|
||||
async function getOptions(query?: string) {
|
||||
if (cache && selectOptions.value.length > 0 && previousSearch === query) {
|
||||
selectOptions.value = JSON.parse(JSON.stringify(cache));
|
||||
return;
|
||||
}
|
||||
const ret = await getList(query);
|
||||
if (ret) {
|
||||
cache = ret;
|
||||
selectOptions.value = JSON.parse(JSON.stringify(cache));
|
||||
previousSearch = query || previousSearch;
|
||||
}
|
||||
getSelectedOption();
|
||||
}
|
||||
|
||||
async function setFirstValue() {
|
||||
if (value.value) return;
|
||||
const first = selectOptions.value.at(0);
|
||||
if (first) value.value = first[valueField];
|
||||
}
|
||||
|
||||
async function getSelectedOption() {
|
||||
const currentValue = value.value;
|
||||
|
||||
if (!currentValue) return;
|
||||
if (selectOptions.value.find((v) => v[valueField] === currentValue)) return;
|
||||
if (valueOption.value && valueOption.value[valueField] === currentValue) {
|
||||
return selectOptions.value.unshift(valueOption.value);
|
||||
}
|
||||
|
||||
const ret = await getByValue(currentValue);
|
||||
|
||||
if (ret) {
|
||||
selectOptions.value.unshift(ret);
|
||||
valueOption.value = ret;
|
||||
}
|
||||
}
|
||||
|
||||
type QuasarSelectUpdate = (
|
||||
callback: () => void,
|
||||
afterFn?: ((ref: QSelect) => void) | undefined,
|
||||
) => void;
|
||||
|
||||
function filter(value: string, update: QuasarSelectUpdate) {
|
||||
update(
|
||||
() => getOptions(value),
|
||||
(ref) => {
|
||||
if (!!value && ref.options && ref.options.length > 0) {
|
||||
ref.setOptionIndex(-1);
|
||||
ref.moveOptionSelection(1, true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return { getOptions, setFirstValue, getSelectedOption, filter };
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue