Merge branch '8087-nuevosCamposTravel' of https://gitea.verdnatura.es/verdnatura/salix-front into 8087-nuevosCamposTravel
gitea/salix-front/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Carlos Andrés 2024-11-12 16:27:41 +01:00
commit b460a0be2f
58 changed files with 1155 additions and 592 deletions

View File

@ -11,12 +11,22 @@ module.exports = defineConfig({
video: false,
specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: true,
watchForFileChanges: true,
reporter: 'cypress-mochawesome-reporter',
reporterOptions: {
charts: true,
reportPageTitle: 'Cypress Inline Reporter',
embeddedScreenshots: true,
reportDir: 'test/cypress/reports',
inlineAssets: true,
},
component: {
componentFolder: 'src',
testFiles: '**/*.spec.js',
supportFile: 'test/cypress/support/unit.js',
},
setupNodeEvents(on, config) {
require('cypress-mochawesome-reporter/plugin')(on);
// implement node event listeners here
},
},

View File

@ -7,10 +7,11 @@
"private": true,
"packageManager": "pnpm@8.15.1",
"scripts": {
"resetDatabase": "cd ../salix && gulp docker",
"lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test:e2e": "cypress open",
"test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run",
"test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest",
"test:unit:ci": "vitest run",
@ -42,6 +43,7 @@
"@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.14",
"cypress": "^13.6.6",
"cypress-mochawesome-reporter": "^3.8.2",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-cypress": "^2.13.3",

View File

@ -70,6 +70,9 @@ devDependencies:
cypress:
specifier: ^13.6.6
version: 13.6.6
cypress-mochawesome-reporter:
specifier: ^3.8.2
version: 3.8.2(cypress@13.6.6)(mocha@10.7.3)
eslint:
specifier: ^8.41.0
version: 8.56.0
@ -829,8 +832,8 @@ packages:
vue-i18n:
optional: true
dependencies:
'@intlify/message-compiler': 10.0.0-beta.5
'@intlify/shared': 10.0.0-beta.5
'@intlify/message-compiler': 10.0.0
'@intlify/shared': 10.0.0
jsonc-eslint-parser: 1.4.1
source-map: 0.6.1
vue-i18n: 9.9.1(vue@3.4.19)
@ -844,11 +847,11 @@ packages:
'@intlify/message-compiler': 9.9.1
'@intlify/shared': 9.9.1
/@intlify/message-compiler@10.0.0-beta.5:
resolution: {integrity: sha512-hLLchnM1dmtSEruerkzvU9vePsLqBXz3RU85SCx/Vd12fFQiymP+/5Rn9MJ8MyfLmIOLDEx4PRh+/GkIQP6oog==}
/@intlify/message-compiler@10.0.0:
resolution: {integrity: sha512-OcaWc63NC/9p1cMdgoNKBj4d61BH8sUW1Hfs6YijTd9656ZR4rNqXAlRnBrfS5ABq0vjQjpa8VnyvH9hK49yBw==}
engines: {node: '>= 16'}
dependencies:
'@intlify/shared': 10.0.0-beta.5
'@intlify/shared': 10.0.0
source-map-js: 1.0.2
dev: true
@ -859,8 +862,8 @@ packages:
'@intlify/shared': 9.9.1
source-map-js: 1.0.2
/@intlify/shared@10.0.0-beta.5:
resolution: {integrity: sha512-g9bq5Y1bOcC9qxtNk4UWtF3sXm6Wh0fGISb7vD5aLyF7yQv7ZFjxQjJzBP2GqG/9+PAGYutqjP1GGadNqFtyAQ==}
/@intlify/shared@10.0.0:
resolution: {integrity: sha512-6ngLfI7DOTew2dcF9WMJx+NnMWghMBhIiHbGg+wRvngpzD5KZJZiJVuzMsUQE1a5YebEmtpTEfUrDp/NqVGdiw==}
engines: {node: '>= 16'}
dev: true
@ -884,7 +887,7 @@ packages:
optional: true
dependencies:
'@intlify/bundle-utils': 4.0.0(vue-i18n@9.9.1)
'@intlify/shared': 10.0.0-beta.5
'@intlify/shared': 10.0.0
'@rollup/pluginutils': 4.2.1
'@vue/compiler-sfc': 3.4.19
debug: 4.3.4(supports-color@8.1.1)
@ -1999,6 +2002,10 @@ packages:
dependencies:
fill-range: 7.0.1
/browser-stdout@1.3.1:
resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
dev: true
/browserslist@4.23.0:
resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@ -2106,6 +2113,16 @@ packages:
upper-case: 1.1.3
dev: true
/camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
dev: true
/camelcase@6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
dev: true
/camelcase@7.0.1:
resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==}
engines: {node: '>=14.16'}
@ -2255,6 +2272,22 @@ packages:
engines: {node: '>= 10'}
dev: true
/cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
dev: true
/cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
dev: true
/cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@ -2558,6 +2591,23 @@ packages:
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
/cypress-mochawesome-reporter@3.8.2(cypress@13.6.6)(mocha@10.7.3):
resolution: {integrity: sha512-oJZkNzhNmN9ZD+LmZyFuPb8aWaIijyHyqYh52YOBvR6B6ckfJNCHP3A98a+/nG0H4t46CKTNwo+wNpMa4d2kjA==}
engines: {node: '>=14'}
hasBin: true
peerDependencies:
cypress: '>=6.2.0'
dependencies:
commander: 10.0.1
cypress: 13.6.6
fs-extra: 10.1.0
mochawesome: 7.1.3(mocha@10.7.3)
mochawesome-merge: 4.3.0
mochawesome-report-generator: 6.2.0
transitivePeerDependencies:
- mocha
dev: true
/cypress@13.6.6:
resolution: {integrity: sha512-S+2S9S94611hXimH9a3EAYt81QM913ZVA03pUmGDfLTFa5gyp85NJ8dJGSlEAEmyRsYkioS1TtnWtbv/Fzt11A==}
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
@ -2627,6 +2677,10 @@ packages:
time-zone: 1.0.0
dev: true
/dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
dev: true
/dayjs@1.11.10:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dev: true
@ -2676,6 +2730,29 @@ packages:
ms: 2.1.2
supports-color: 8.1.1
/debug@4.3.7(supports-color@8.1.1):
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.3
supports-color: 8.1.1
dev: true
/decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
dev: true
/decamelize@4.0.0:
resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
engines: {node: '>=10'}
dev: true
/decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@ -2758,6 +2835,11 @@ packages:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
/diff@5.2.0:
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
engines: {node: '>=0.3.1'}
dev: true
/doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
@ -3550,6 +3632,14 @@ packages:
transitivePeerDependencies:
- supports-color
/find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
dev: true
/find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@ -3646,6 +3736,15 @@ packages:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
dev: true
/fs-extra@10.1.0:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'}
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.1
dev: true
/fs-extra@11.2.0:
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
engines: {node: '>=14.14'}
@ -3654,6 +3753,15 @@ packages:
jsonfile: 6.1.0
universalify: 2.0.1
/fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
dependencies:
graceful-fs: 4.2.11
jsonfile: 4.0.0
universalify: 0.1.2
dev: true
/fs-extra@9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
@ -3675,6 +3783,10 @@ packages:
dev: true
optional: true
/fsu@1.1.1:
resolution: {integrity: sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A==}
dev: true
/function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@ -3775,6 +3887,18 @@ packages:
once: 1.4.0
path-is-absolute: 1.0.1
/glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
deprecated: Glob versions prior to v9 are no longer supported
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 5.1.6
once: 1.4.0
dev: true
/global-directory@4.0.1:
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
engines: {node: '>=18'}
@ -4189,6 +4313,11 @@ packages:
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
engines: {node: '>=8'}
/is-plain-obj@2.1.0:
resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
engines: {node: '>=8'}
dev: true
/is-plain-obj@3.0.0:
resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==}
engines: {node: '>=10'}
@ -4361,6 +4490,12 @@ packages:
resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==}
dev: true
/jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
optionalDependencies:
graceful-fs: 4.2.11
dev: true
/jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies:
@ -4452,6 +4587,13 @@ packages:
engines: {node: '>=14'}
dev: true
/locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
dependencies:
p-locate: 4.1.0
dev: true
/locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@ -4486,10 +4628,26 @@ packages:
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
dev: true
/lodash.isempty@4.4.0:
resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==}
dev: true
/lodash.isfunction@3.0.9:
resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
dev: true
/lodash.isobject@3.0.2:
resolution: {integrity: sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==}
dev: true
/lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
dev: true
/lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
dev: true
/lodash.kebabcase@4.1.1:
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
dev: true
@ -4552,6 +4710,13 @@ packages:
wrap-ansi: 6.2.0
dev: true
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
dependencies:
js-tokens: 4.0.0
dev: true
/loupe@2.3.7:
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
dependencies:
@ -4722,6 +4887,79 @@ packages:
ufo: 1.4.0
dev: true
/mocha@10.7.3:
resolution: {integrity: sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==}
engines: {node: '>= 14.0.0'}
hasBin: true
dependencies:
ansi-colors: 4.1.3
browser-stdout: 1.3.1
chokidar: 3.6.0
debug: 4.3.7(supports-color@8.1.1)
diff: 5.2.0
escape-string-regexp: 4.0.0
find-up: 5.0.0
glob: 8.1.0
he: 1.2.0
js-yaml: 4.1.0
log-symbols: 4.1.0
minimatch: 5.1.6
ms: 2.1.3
serialize-javascript: 6.0.2
strip-json-comments: 3.1.1
supports-color: 8.1.1
workerpool: 6.5.1
yargs: 16.2.0
yargs-parser: 20.2.9
yargs-unparser: 2.0.0
dev: true
/mochawesome-merge@4.3.0:
resolution: {integrity: sha512-1roR6g+VUlfdaRmL8dCiVpKiaUhbPVm1ZQYUM6zHX46mWk+tpsKVZR6ba98k2zc8nlPvYd71yn5gyH970pKBSw==}
engines: {node: '>=10.0.0'}
hasBin: true
dependencies:
fs-extra: 7.0.1
glob: 7.2.3
yargs: 15.4.1
dev: true
/mochawesome-report-generator@6.2.0:
resolution: {integrity: sha512-Ghw8JhQFizF0Vjbtp9B0i//+BOkV5OWcQCPpbO0NGOoxV33o+gKDYU0Pr2pGxkIHnqZ+g5mYiXF7GMNgAcDpSg==}
hasBin: true
dependencies:
chalk: 4.1.2
dateformat: 4.6.3
escape-html: 1.0.3
fs-extra: 10.1.0
fsu: 1.1.1
lodash.isfunction: 3.0.9
opener: 1.5.2
prop-types: 15.8.1
tcomb: 3.2.29
tcomb-validation: 3.4.1
validator: 13.11.0
yargs: 17.7.2
dev: true
/mochawesome@7.1.3(mocha@10.7.3):
resolution: {integrity: sha512-Vkb3jR5GZ1cXohMQQ73H3cZz7RoxGjjUo0G5hu0jLaW+0FdUxUwg3Cj29bqQdh0rFcnyV06pWmqmi5eBPnEuNQ==}
peerDependencies:
mocha: '>=7'
dependencies:
chalk: 4.1.2
diff: 5.2.0
json-stringify-safe: 5.0.1
lodash.isempty: 4.4.0
lodash.isfunction: 3.0.9
lodash.isobject: 3.0.2
lodash.isstring: 4.0.1
mocha: 10.7.3
mochawesome-report-generator: 6.2.0
strip-ansi: 6.0.1
uuid: 8.3.2
dev: true
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@ -4870,6 +5108,11 @@ packages:
is-wsl: 2.2.0
dev: false
/opener@1.5.2:
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
hasBin: true
dev: true
/optionator@0.9.3:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
engines: {node: '>= 0.8.0'}
@ -4915,6 +5158,13 @@ packages:
engines: {node: '>=12.20'}
dev: false
/p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
dependencies:
p-try: 2.2.0
dev: true
/p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@ -4929,6 +5179,13 @@ packages:
yocto-queue: 1.0.0
dev: true
/p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
dependencies:
p-limit: 2.3.0
dev: true
/p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
@ -4950,6 +5207,11 @@ packages:
aggregate-error: 3.1.0
dev: true
/p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
dev: true
/package-json@8.1.1:
resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==}
engines: {node: '>=14.16'}
@ -5139,6 +5401,14 @@ packages:
engines: {node: '>=0.4.0'}
dev: false
/prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
dev: true
/proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
@ -5242,6 +5512,10 @@ packages:
strip-json-comments: 2.0.1
dev: false
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
/react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
dev: true
@ -5328,6 +5602,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
dev: true
/requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
@ -5573,6 +5851,10 @@ packages:
transitivePeerDependencies:
- supports-color
/set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
dev: true
/set-function-length@1.2.1:
resolution: {integrity: sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==}
engines: {node: '>= 0.4'}
@ -5829,6 +6111,16 @@ packages:
readable-stream: 3.6.2
dev: true
/tcomb-validation@3.4.1:
resolution: {integrity: sha512-urVVMQOma4RXwiVCa2nM2eqrAomHROHvWPuj6UkDGz/eb5kcy0x6P0dVt6kzpUZtYMNoAqJLWmz1BPtxrtjtrA==}
dependencies:
tcomb: 3.2.29
dev: true
/tcomb@3.2.29:
resolution: {integrity: sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ==}
dev: true
/text-extensions@2.4.0:
resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
engines: {node: '>=8'}
@ -6048,6 +6340,11 @@ packages:
crypto-random-string: 4.0.0
dev: false
/universalify@0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
dev: true
/universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
@ -6137,7 +6434,6 @@ packages:
/validator@13.11.0:
resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==}
engines: {node: '>= 0.10'}
dev: false
/vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
@ -6484,6 +6780,10 @@ packages:
engines: {node: '>=12'}
dev: true
/which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
dev: true
/which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@ -6511,6 +6811,10 @@ packages:
resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==}
dev: true
/workerpool@6.5.1:
resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==}
dev: true
/wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@ -6559,6 +6863,10 @@ packages:
engines: {node: '>=12'}
dev: true
/y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
dev: true
/y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@ -6584,11 +6892,64 @@ packages:
engines: {node: '>= 6'}
dev: true
/yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
dev: true
/yargs-parser@20.2.9:
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
engines: {node: '>=10'}
dev: true
/yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
dev: true
/yargs-unparser@2.0.0:
resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==}
engines: {node: '>=10'}
dependencies:
camelcase: 6.3.0
decamelize: 4.0.0
flat: 5.0.2
is-plain-obj: 2.1.0
dev: true
/yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
dev: true
/yargs@16.2.0:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
engines: {node: '>=10'}
dependencies:
cliui: 7.0.4
escalade: 3.1.2
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 20.2.9
dev: true
/yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}

View File

@ -272,6 +272,7 @@ defineExpose({
hasChanges,
reset,
fetch,
formData,
});
</script>
<template>

View File

@ -1,6 +1,6 @@
<script setup>
import axios from 'axios';
import { onMounted, watch, ref, reactive } from 'vue';
import { onMounted, watch, ref, reactive, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { QSeparator, useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
@ -9,6 +9,7 @@ import { toLowerCamel } from 'src/filters';
import routes from 'src/router/modules';
import LeftMenuItem from './LeftMenuItem.vue';
import LeftMenuItemGroup from './LeftMenuItemGroup.vue';
import VnInput from './common/VnInput.vue';
const { t } = useI18n();
const route = useRoute();
@ -22,7 +23,32 @@ const props = defineProps({
},
});
const items = ref([]);
const expansionItemElements = reactive({});
const pinnedModules = computed(() => {
const map = new Map();
items.value.forEach((item) => item.isPinned && map.set(item.name, item));
return map;
});
const search = ref(null);
const filteredItems = computed(() => {
if (!search.value) return items.value;
return items.value.filter((item) => {
const locale = t(item.title).toLowerCase();
return locale.includes(search.value.toLowerCase());
});
});
const filteredPinnedModules = computed(() => {
if (!search.value) return pinnedModules.value;
const map = new Map();
for (const [key, pinnedModule] of pinnedModules.value) {
const locale = t(pinnedModule.title).toLowerCase();
if (locale.includes(search.value.toLowerCase())) map.set(key, pinnedModule);
}
return map;
});
onMounted(async () => {
await navigation.fetchPinned();
@ -66,8 +92,6 @@ function addChildren(module, route, parent) {
}
}
const items = ref([]);
function getRoutes() {
if (props.source === 'main') {
const modules = Object.assign([], navigation.getModules().value);
@ -129,15 +153,44 @@ const handleItemExpansion = (itemName) => {
<QList padding class="column-max-width">
<template v-if="$props.source === 'main'">
<template v-if="$route?.matched[1]?.name === 'Dashboard'">
<QItem class="header">
<QItemSection avatar>
<QIcon name="view_module" />
</QItemSection>
<QItemSection> {{ t('globals.modules') }}</QItemSection>
<QItem class="q-pb-md">
<VnInput
v-model="search"
:label="t('Search modules')"
class="full-width"
filled
dense
/>
</QItem>
<QSeparator />
<template v-for="item in items" :key="item.name">
<template v-if="item.children">
<template v-if="filteredPinnedModules.size">
<LeftMenuItem
v-for="[key, pinnedModule] of filteredPinnedModules"
:key="key"
:item="pinnedModule"
group="modules"
>
<template #side>
<QBtn
v-if="pinnedModule.isPinned === true"
@click="togglePinned(pinnedModule, $event)"
icon="remove_circle"
size="xs"
flat
round
>
<QTooltip>
{{ t('components.leftMenu.removeFromPinned') }}
</QTooltip>
</QBtn>
</template>
</LeftMenuItem>
<QSeparator />
</template>
<template v-for="item in filteredItems" :key="item.name">
<template
v-if="item.children && !filteredPinnedModules.has(item.name)"
>
<LeftMenuItem :item="item" group="modules">
<template #side>
<QBtn
@ -256,3 +309,7 @@ const handleItemExpansion = (itemName) => {
color: var(--vn-label-color);
}
</style>
<i18n>
es:
Search modules: Buscar módulos
</i18n>

View File

@ -62,7 +62,7 @@ const $props = defineProps({
default: 'flex-one',
},
searchUrl: {
type: String,
type: [String, Boolean],
default: 'table',
},
isEditable: {
@ -331,6 +331,20 @@ function handleScroll() {
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) <= 40;
if (isAtBottom) CrudModelRef.value.vnPaginateRef.paginate();
}
function handleSelection({ evt, added, rows: selectedRows }, rows) {
if (evt?.shiftKey && added) {
const rowIndex = selectedRows[0].$index;
const selectedIndexes = new Set(selected.value.map((row) => row.$index));
for (const row of rows) {
if (row.$index == rowIndex) break;
if (!selectedIndexes.has(row.$index)) {
selected.value.push(row);
selectedIndexes.add(row.$index);
}
}
}
}
</script>
<template>
<QDrawer
@ -431,6 +445,7 @@ function handleScroll() {
@virtual-scroll="handleScroll"
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
@update:selected="emit('update:selected', $event)"
@selection="(details) => handleSelection(details, rows)"
>
<template #top-left v-if="!$props.withoutHeader">
<slot name="top-left"></slot>

View File

@ -31,6 +31,10 @@ const $props = defineProps({
type: String,
default: null,
},
description: {
type: String,
default: null,
},
});
const warehouses = ref();
@ -43,7 +47,8 @@ const dms = ref({});
onMounted(() => {
defaultData();
if (!$props.formInitialData)
dms.value.description = t($props.model + 'Description', dms.value);
dms.value.description =
$props.description ?? t($props.model + 'Description', dms.value);
});
function onFileChange(files) {
dms.value.hasFileAttached = !!files;
@ -54,7 +59,6 @@ function mapperDms(data) {
const formData = new FormData();
const { files } = data;
if (files) formData.append(files?.name, files);
delete data.files;
const dms = {
hasFile: !!data.hasFile,
@ -78,6 +82,7 @@ async function save() {
const body = mapperDms(dms.value);
const response = await axios.post(getUrl(), body[0], body[1]);
emit('onDataSaved', body[1].params, response);
delete dms.value.files;
return response;
}
@ -165,6 +170,7 @@ function addDefaultData(data) {
@update:model-value="onFileChange(dms.files)"
class="required"
:display-value="dms.file"
data-cy="VnDms_inputFile"
>
<template #append>
<QIcon

View File

@ -1,8 +1,11 @@
<script setup>
import { computed, ref } from 'vue';
import { computed, ref, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import { useValidator } from 'src/composables/useValidator';
import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const { t } = useI18n();
const emit = defineEmits([
'update:modelValue',
'update:options',
@ -27,11 +30,11 @@ const $props = defineProps({
type: Boolean,
default: true,
},
emptyToNull: {
type: Boolean,
default: true,
},
});
const { validations } = useValidator();
const { t } = useI18n();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
const vnInputRef = ref(null);
const value = computed({
@ -39,6 +42,7 @@ const value = computed({
return $props.modelValue;
},
set(value) {
if ($props.emptyToNull && value === '') value = null;
emit('update:modelValue', value);
},
});
@ -60,8 +64,6 @@ const focus = () => {
defineExpose({
focus,
});
import { useAttrs } from 'vue';
const $attrs = useAttrs();
const mixinRules = [
requiredFieldRule,
@ -85,7 +87,7 @@ const mixinRules = [
v-model="value"
v-bind="{ ...$attrs, ...styleAttrs }"
:type="$attrs.type"
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
@keyup.enter="emit('keyup.enter')"
:clearable="false"
:rules="mixinRules"

View File

@ -4,8 +4,13 @@ import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useAttrs } from 'vue';
import VnDate from './VnDate.vue';
import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const model = defineModel({ type: [String, Date] });
const { t } = useI18n();
const $props = defineProps({
isOutlined: {
type: Boolean,
@ -16,18 +21,13 @@ const $props = defineProps({
default: true,
},
});
import { useValidator } from 'src/composables/useValidator';
const { validations } = useValidator();
const { t } = useI18n();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
const vnInputDateRef = ref(null);
const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref();
const hover = ref();
const mask = ref();
const $attrs = useAttrs();
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
@ -104,7 +104,7 @@ const manageDate = (date) => {
:mask="mask"
placeholder="dd/mm/aaaa"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
:rules="mixinRules"
:clearable="false"
@click="isPopupOpen = true"

View File

@ -1,8 +1,13 @@
<script setup>
import VnInput from 'src/components/common/VnInput.vue';
import { ref } from 'vue';
import { useAttrs } from 'vue';
const model = defineModel({ type: [Number, String] });
const $attrs = useAttrs();
const step = ref($attrs.step || 0.01);
</script>
<template>
<VnInput v-bind="$attrs" v-model.number="model" type="number" />
<VnInput v-bind="$attrs" v-model.number="model" type="number" :step="step" />
</template>

View File

@ -2,11 +2,12 @@
import { computed, ref, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import { date } from 'quasar';
import { useValidator } from 'src/composables/useValidator';
import VnTime from './VnTime.vue';
import { useRequired } from 'src/composables/useRequired';
const { validations } = useValidator();
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const { t } = useI18n();
const model = defineModel({ type: String });
const props = defineProps({
timeOnly: {
@ -20,8 +21,6 @@ const props = defineProps({
});
const vnInputTimeRef = ref(null);
const initialDate = ref(model.value ?? Date.vnNew());
const { t } = useI18n();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const dateFormat = 'HH:mm';
const isPopupOpen = ref();
@ -78,7 +77,7 @@ function dateToTime(newDate) {
placeholder="--:--"
v-model="formattedTime"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
style="min-width: 100px"
:rules="mixinRules"
@click="isPopupOpen = false"

View File

@ -3,9 +3,12 @@ import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import VnSelectDialog from 'components/common/VnSelectDialog.vue';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { useAttrs } from 'vue';
import { useRequired } from 'src/composables/useRequired';
const { t } = useI18n();
const emit = defineEmits(['update:model-value', 'update:options']);
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const props = defineProps({
location: {
type: Object,
@ -13,6 +16,7 @@ const props = defineProps({
},
});
const mixinRules = [requiredFieldRule];
const locationProperties = [
'postcode',
(obj) =>
@ -69,11 +73,13 @@ const handleModelValue = (data) => {
:label="t('Location')"
:placeholder="t('search_by_postalcode')"
:input-debounce="300"
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
v-bind="$attrs"
clearable
:emit-value="false"
:tooltip="t('Create new location')"
:rules="mixinRules"
:lazy-rules="true"
>
<template #form>
<CreateNewPostcode

View File

@ -2,9 +2,11 @@
import { ref, toRefs, computed, watch, onMounted, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue';
import { useValidator } from 'src/composables/useValidator';
import { useRequired } from 'src/composables/useRequired';
const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
const $attrs = useAttrs();
const { t } = useI18n();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const $props = defineProps({
modelValue: {
type: [String, Number, Object],
@ -87,10 +89,7 @@ const $props = defineProps({
default: false,
},
});
const { validations } = useValidator();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
const $attrs = useAttrs();
const { t } = useI18n();
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const { optionLabel, optionValue, optionFilter, optionFilterValue, options, modelValue } =
toRefs($props);
@ -257,7 +256,7 @@ defineExpose({ opts: myOptions });
:fill-input="nullishToTrue($attrs['fill-input'])"
ref="vnSelectRef"
lazy-rules
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
:rules="mixinRules"
virtual-scroll-slice-size="options.length"
hide-bottom-space

View File

@ -8,7 +8,7 @@ defineProps({
<template>
<div :class="$q.screen.gt.md ? 'q-pb-lg' : 'q-pb-md'">
<div class="header-link" :style="{ cursor: url ? 'pointer' : 'default' }">
<a :href="url" :class="url ? 'link' : 'color-vn-text'">
<a :href="url" :class="url ? 'link' : 'color-vn-text'" v-bind="$attrs">
{{ text }}
<QIcon v-if="url" :name="icon" />
</a>

View File

@ -167,6 +167,7 @@ const toModule = computed(() =>
icon="more_vert"
round
size="md"
data-cy="descriptor-more-opts"
>
<QTooltip>
{{ t('components.cardDescriptor.moreOptions') }}

View File

@ -4,6 +4,7 @@ import { useRoute } from 'vue-router';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import { useArrayData } from 'src/composables/useArrayData';
import { isDialogOpened } from 'src/filters';
const props = defineProps({
url: {
@ -58,22 +59,6 @@ async function fetch() {
emit('onFetch', Array.isArray(data) ? data[0] : data);
isLoading.value = false;
}
const showRedirectToSummaryIcon = computed(() => {
const exist = existSummary(route.matched);
return !isSummary.value && route.meta.moduleName && exist;
});
function existSummary(routes) {
const hasSummary = routes.some((r) => r.name === `${route.meta.moduleName}Summary`);
if (hasSummary) return hasSummary;
for (const current of routes) {
if (current.path != '/' && current.children) {
const exist = existSummary(current.children);
if (exist) return exist;
}
}
}
</script>
<template>
@ -84,7 +69,7 @@ function existSummary(routes) {
<div class="summaryHeader bg-primary q-pa-sm text-weight-bolder">
<slot name="header-left">
<router-link
v-if="showRedirectToSummaryIcon"
v-if="isDialogOpened()"
class="header link"
:to="{
name: `${moduleName ?? route.meta.moduleName}Summary`,
@ -118,6 +103,7 @@ function existSummary(routes) {
.cardSummary {
width: 100%;
max-height: 70vh;
.summaryHeader {
text-align: center;
font-size: 20px;

View File

@ -103,6 +103,7 @@ function cancel() {
@click="confirm()"
unelevated
autofocus
data-cy="VnConfirm_confirm"
/>
</QCardActions>
</QCard>

View File

@ -0,0 +1,16 @@
<script setup>
defineProps({ email: { type: [String], default: null } });
</script>
<template>
<QBtn
v-if="email"
flat
round
icon="email"
size="sm"
color="primary"
padding="none"
:href="`mailto:${email}`"
@click.stop
/>
</template>

View File

@ -3,6 +3,7 @@ import { useRouter, useRoute } from 'vue-router';
import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore';
import { buildFilter } from 'filters/filterPanel';
import { isDialogOpened } from 'src/filters';
const arrayDataStore = useArrayDataStore();
@ -114,8 +115,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
for (const row of response.data) store.data.push(row);
} else {
store.data = response.data;
if (!document.querySelectorAll('[role="dialog"][aria-modal="true"]').length)
updateRouter && updateStateParams();
if (!isDialogOpened()) updateRouter && updateStateParams();
}
store.isLoading = false;
@ -247,9 +247,9 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
}
function updateStateParams() {
if (!route) return;
const newUrl = { path: route.path, query: { ...(route.query ?? {}) } };
newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter);
if (store?.searchUrl)
newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter);
if (store.navigate) {
const { customRouteRedirectName, searchText } = store.navigate;

View File

@ -0,0 +1,13 @@
import { useValidator } from 'src/composables/useValidator';
export function useRequired($attrs) {
const { validations } = useValidator();
const isRequired = Object.keys($attrs).includes('required');
const requiredFieldRule = (val) => validations().required(isRequired, val);
return {
isRequired,
requiredFieldRule,
};
}

View File

@ -12,8 +12,10 @@ import dateRange from './dateRange';
import toHour from './toHour';
import dashOrCurrency from './dashOrCurrency';
import getParamWhere from './getParamWhere';
import isDialogOpened from './isDialogOpened';
export {
isDialogOpened,
toLowerCase,
toLowerCamel,
toDate,

View File

@ -0,0 +1,3 @@
export default function isDialogOpened(query = '[role="dialog"]') {
return document.querySelectorAll(query).length > 0;
}

View File

@ -106,6 +106,9 @@ globals:
weight: Weight
error: Ups! Something went wrong
recalc: Recalculate
small: Small
medium: Medium
big: Big
pageTitles:
logIn: Login
addressEdit: Update address
@ -942,6 +945,7 @@ supplier:
isActive: Active
isPayMethodChecked: PayMethod checked
note: Notes
size: Size
fiscalData:
name: Social name *
nif: Tax number *

View File

@ -108,6 +108,9 @@ globals:
weight: Peso
error: ¡Ups! Algo salió mal
recalc: Recalcular
small: Pequeño/a
medium: Mediano/a
big: Grande
pageTitles:
logIn: Inicio de sesión
addressEdit: Modificar consignatario
@ -940,6 +943,7 @@ supplier:
isActive: Activo
isPayMethodChecked: Método de pago validado
note: Notas
size: Tamaño
fiscalData:
name: Razón social *
nif: NIF/CIF *

View File

@ -27,7 +27,7 @@ const addressFilter = {
'isLogifloraAllowed',
'postalCode',
],
order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'],
order: ['isDefaultAddress DESC', 'isActive DESC', 'id DESC', 'nickname ASC'],
include: [
{
relation: 'observations',

View File

@ -5,7 +5,7 @@ import { useRoute } from 'vue-router';
import { useAcl } from 'src/composables/useAcl';
import axios from 'axios';
import { useQuasar } from 'quasar';
import FetchData from 'components/FetchData.vue';
import { getClientRisk } from '../composables/getClientRisk';
import { toCurrency, toDate, toDateHourMin } from 'src/filters';
import { useState } from 'composables/useState';
@ -16,7 +16,7 @@ import { useVnConfirm } from 'composables/useVnConfirm';
import VnTable from 'components/VnTable/VnTable.vue';
import VnInput from 'components/common/VnInput.vue';
import VnSubToolbar from 'components/ui/VnSubToolbar.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnFilter from 'components/VnTable/VnFilter.vue';
import CustomerNewPayment from 'src/pages/Customer/components/CustomerNewPayment.vue';
import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
@ -25,7 +25,7 @@ const { openConfirmationModal } = useVnConfirm();
const { sendEmail, openReport } = usePrintService();
const { t } = useI18n();
const { hasAny } = useAcl();
const currentBalance = ref({});
const quasar = useQuasar();
const route = useRoute();
const state = useState();
@ -33,28 +33,53 @@ const stateStore = useStateStore();
const user = state.getUser();
const clientRisk = ref([]);
const companies = ref([]);
const tableRef = ref();
const companyId = ref(user.value.companyFk);
const companyId = ref();
const companyUser = ref(user.value.companyFk);
const balances = ref([]);
const vnFilterRef = ref({});
const filter = computed(() => {
return {
clientId: route.params.id,
companyId: companyId.value ?? user.value.companyFk,
companyId: companyId.value ?? companyUser.value,
};
});
const companyFilterColumn = {
align: 'left',
name: 'companyId',
label: t('Company'),
component: 'select',
attrs: {
url: 'Companies',
optionLabel: 'code',
optionValue: 'id',
sortBy: 'code',
},
columnFilter: {
event: {
remove: () => (companyId.value = null),
'update:modelValue': (newCompanyFk) => {
if (!newCompanyFk) return;
vnFilterRef.value.addFilter(newCompanyFk);
companyUser.value = newCompanyFk;
},
blur: () => !companyId.value && (companyId.value = companyUser.value),
},
},
visible: false,
};
const columns = computed(() => [
{
align: 'right',
align: 'left',
name: 'payed',
label: t('Date'),
format: ({ payed }) => toDate(payed),
cardVisible: true,
},
{
align: 'right',
align: 'left',
name: 'created',
label: t('Creation date'),
format: ({ created }) => toDateHourMin(created),
@ -65,7 +90,12 @@ const columns = computed(() => [
label: t('Employee'),
columnField: {
component: 'userLink',
attrs: ({ row }) => ({ workerId: row.workerFk, name: row.userName }),
attrs: ({ row }) => {
return {
workerId: row.workerFk,
name: row.userName,
};
},
},
cardVisible: true,
},
@ -77,13 +107,13 @@ const columns = computed(() => [
class: 'extend',
},
{
align: 'right',
align: 'left',
name: 'bankFk',
label: t('Bank'),
cardVisible: true,
},
{
align: 'right',
align: 'left',
name: 'debit',
label: t('Debit'),
format: ({ debit }) => debit && toCurrency(debit),
@ -136,20 +166,37 @@ const columns = computed(() => [
onBeforeMount(() => {
stateStore.rightDrawer = true;
companyId.value = companyUser.value;
});
async function getCurrentBalance(data) {
currentBalance.value[companyId.value] = {
amount: 0,
code: companies.value.find((c) => c.id === companyId.value)?.code,
async function getClientRisks() {
const filter = {
where: { clientFk: route.params.id, companyFk: companyUser.value },
};
const { data } = await getClientRisk(filter);
clientRisk.value = data;
return clientRisk.value;
}
for (const balance of data) {
currentBalance.value[balance.companyFk] = {
code: balance.company.code,
amount: balance.amount,
};
async function getCurrentBalance() {
const currentBalance = (await getClientRisks()).find((balance) => {
return balance.companyFk === companyId.value;
});
return currentBalance && currentBalance.amount;
}
async function onFetch(data) {
balances.value = [];
for (const [index, balance] of data.entries()) {
if (index === 0) {
balance.balance = await getCurrentBalance();
continue;
}
const previousBalance = data[index - 1];
balance.balance =
previousBalance?.balance - (previousBalance?.debit - previousBalance?.credit);
}
balances.value = data;
}
const showNewPaymentDialog = () => {
@ -169,43 +216,25 @@ const showBalancePdf = ({ id }) => {
</script>
<template>
<FetchData
url="Companies"
auto-load
@on-fetch="(data) => (companies = data)"
></FetchData>
<FetchData
v-if="companies.length > 0"
url="clientRisks"
:filter="{
include: { relation: 'company', scope: { fields: ['code'] } },
where: { clientFk: route.params.id },
}"
auto-load
@on-fetch="getCurrentBalance"
></FetchData>
<VnSubToolbar class="q-mb-md">
<template #st-data>
<div class="column justify-center q-px-md q-py-sm">
<span class="text-bold">{{ t('Total by company') }}</span>
<div class="row justify-center">
{{ currentBalance[companyId]?.code }}:
{{ toCurrency(currentBalance[companyId]?.amount) }}
<div class="row justify-center" v-if="clientRisk?.length">
{{ clientRisk[0].company.code }}:
{{ toCurrency(clientRisk[0].amount) }}
</div>
</div>
</template>
<template #st-actions>
<div>
<VnSelect
:label="t('Company')"
<VnFilter
ref="vnFilterRef"
v-model="companyId"
data-key="CustomerBalance"
:options="companies"
option-label="code"
option-value="id"
></VnSelect>
:column="companyFilterColumn"
search-url="balance"
/>
</div>
</template>
</VnSubToolbar>
@ -219,6 +248,7 @@ const showBalancePdf = ({ id }) => {
:right-search="false"
:is-editable="false"
:column-search="false"
@on-fetch="onFetch"
:disable-option="{ card: true }"
auto-load
>

View File

@ -49,7 +49,9 @@ const columns = computed(() => [
name: 'credit',
create: true,
visible: false,
attrs: {
columnCreate: {
component: 'number',
required: true,
autofocus: true,
},
},

View File

@ -150,7 +150,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
</QCardActions>
</template>
<template #actions="{ entity }">
<QCardActions class="flex justify-center">
<QCardActions class="flex justify-center" style="padding-inline: 0">
<QBtn
:to="{
name: 'TicketList',
@ -168,6 +168,23 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
>
<QTooltip>{{ t('Customer ticket list') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'TicketList',
query: {
table: JSON.stringify({
clientFk: entity.id,
}),
createForm: JSON.stringify({ clientId: entity.id }),
},
}"
size="md"
color="primary"
target="_blank"
icon="vn:ticketAdd"
>
<QTooltip>{{ t('New ticket') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'InvoiceOutList',
@ -179,6 +196,23 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
>
<QTooltip>{{ t('Customer invoice out list') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'OrderList',
query: {
table: JSON.stringify({
clientFk: entity.id,
}),
createForm: JSON.stringify({ clientFk: entity.id }),
},
}"
size="md"
target="_blank"
icon="vn:basketadd"
color="primary"
>
<QTooltip>{{ t('New order') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'AccountSummary',
@ -215,6 +249,8 @@ es:
Go to module index: Ir al índice del módulo
Customer ticket list: Listado de tickets del cliente
Customer invoice out list: Listado de facturas del cliente
New order: Nuevo pedido
New ticket: Nuevo ticket
Go to user: Ir al usuario
Go to supplier: Ir al proveedor
Customer unpaid: Cliente impago

View File

@ -8,9 +8,6 @@ import { useQuasar } from 'quasar';
import useNotify from 'src/composables/useNotify';
import VnSmsDialog from 'src/components/common/VnSmsDialog.vue';
import TicketCreateDialog from 'src/pages/Ticket/TicketCreateDialog.vue';
import OrderCreateDialog from 'src/pages/Order/Card/OrderCreateDialog.vue';
import { ref } from 'vue';
const $props = defineProps({
customer: {
@ -43,34 +40,9 @@ const sendSms = async (payload) => {
notify(error.message, 'positive');
}
};
const ticketCreateFormDialog = ref(null);
const openTicketCreateForm = () => {
ticketCreateFormDialog.value.show();
};
const orderCreateFormDialog = ref(null);
const openOrderCreateForm = () => {
orderCreateFormDialog.value.show();
};
</script>
<template>
<QItem v-ripple clickable @click="openTicketCreateForm()">
<QItemSection>
{{ t('globals.pageTitles.createTicket') }}
<QDialog ref="ticketCreateFormDialog">
<TicketCreateDialog />
</QDialog>
</QItemSection>
</QItem>
<QItem v-ripple clickable @click="openOrderCreateForm()">
<QItemSection>
{{ t('globals.pageTitles.createOrder') }}
<QDialog ref="orderCreateFormDialog">
<OrderCreateDialog :client-fk="customer.id" />
</QDialog>
</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection>
</QItem>

View File

@ -53,11 +53,11 @@ function handleLocation(data, location) {
</QIcon>
</template>
</VnInput>
<VnInput :label="t('Tax number')" clearable v-model="data.fi" />
<VnInput :label="t('Tax number')" clearable v-model="data.fi" required />
</VnRow>
<VnRow>
<VnInput :label="t('Street')" clearable v-model="data.street" />
<VnInput :label="t('Street')" clearable v-model="data.street" required />
</VnRow>
<VnRow>
@ -68,6 +68,7 @@ function handleLocation(data, location) {
option-label="vat"
option-value="id"
v-model="data.sageTaxTypeFk"
:required="data.isTaxDataChecked"
/>
<VnSelect
:label="t('Sage transaction type')"
@ -76,6 +77,7 @@ function handleLocation(data, location) {
option-label="transaction"
option-value="id"
v-model="data.sageTransactionTypeFk"
:required="data.isTaxDataChecked"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -96,6 +98,7 @@ function handleLocation(data, location) {
:roles-allowed-to-create="['deliveryAssistant', 'administrative']"
:acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
:location="data"
:required="true"
@update:model-value="(location) => handleLocation(data, location)"
/>
</VnRow>

View File

@ -80,6 +80,11 @@ const columns = computed(() => [
align: 'left',
name: 'amount',
label: t('Amount'),
columnCreate: {
component: 'number',
autofocus: true,
required: true,
},
format: ({ amount }) => toCurrency(amount),
create: true,
},

View File

@ -56,6 +56,7 @@ const columns = computed(() => [
label: t('Period'),
create: true,
...componentColumn('number'),
required: true,
},
{
align: 'left',

View File

@ -1,13 +1,15 @@
<script setup>
import { computed, ref } from 'vue';
import { computed, ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { toCurrency, toPercentage, toDate } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue';
import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import VnLinkMail from 'src/components/ui/VnLinkMail.vue';
import CustomerSummaryTable from 'src/pages/Customer/components/CustomerSummaryTable.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
import VnRow from 'src/components/ui/VnRow.vue';
@ -25,6 +27,11 @@ const $props = defineProps({
const entityId = computed(() => $props.id || route.params.id);
const customer = computed(() => summary.value.entity);
const summary = ref();
const clientUrl = ref();
onMounted(async () => {
clientUrl.value = (await getUrl('client/')) + entityId.value + '/';
});
const balanceDue = computed(() => {
return (
@ -67,6 +74,7 @@ const sumRisk = ({ clientRisks }) => {
ref="summary"
:url="`Clients/${entityId}/summary`"
data-key="CustomerSummary"
module-name="Customer"
>
<template #body="{ entity }">
<QCard class="vn-one">
@ -89,7 +97,11 @@ const sumRisk = ({ clientRisks }) => {
<VnLinkPhone :phone-number="entity.mobile" />
</template>
</VnLv>
<VnLv :label="t('customer.summary.email')" :value="entity.email" copy />
<VnLv :value="entity.email" copy
><template #label>
{{ t('customer.summary.email') }}
<VnLinkMail email="entity.email"></VnLinkMail> </template
></VnLv>
<VnLv
:label="t('customer.summary.salesPerson')"
:value="entity?.salesPersonUser?.name"
@ -166,7 +178,7 @@ const sumRisk = ({ clientRisks }) => {
<QCard class="vn-one">
<VnTitle
:url="`#/customer/${entityId}/billing-data`"
:text="t('customer.summary.payMethodFk')"
:text="t('customer.summary.billingData')"
/>
<VnLv
:label="t('customer.summary.payMethod')"
@ -222,6 +234,7 @@ const sumRisk = ({ clientRisks }) => {
</QCard>
<QCard class="vn-one" v-if="entity.account">
<VnTitle
target="_blank"
:url="`${grafanaUrl}/d/adjlxzv5yjt34d/analisis-de-clientes-7c-crm?orgId=1&var-clientFk=${entityId}`"
:text="t('customer.summary.businessData')"
icon="vn:grafana"
@ -235,6 +248,7 @@ const sumRisk = ({ clientRisks }) => {
:value="toCurrency(entity?.mana?.mana)"
/>
<VnLv
v-if="entity.claimsRatio"
:label="t('customer.summary.priceIncreasingRate')"
:value="toPercentage(priceIncreasingRate)"
/>
@ -243,12 +257,14 @@ const sumRisk = ({ clientRisks }) => {
:value="toCurrency(entity?.averageInvoiced?.invoiced)"
/>
<VnLv
v-if="entity.claimsRatio"
:label="t('customer.summary.claimRate')"
:value="toPercentage(claimRate)"
/>
</QCard>
<QCard class="vn-one" v-if="entity.account">
<VnTitle
target="_blank"
:url="`${grafanaUrl}/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk=${entityId}`"
:text="t('customer.summary.payMethodFk')"
icon="vn:grafana"
@ -268,10 +284,12 @@ const sumRisk = ({ clientRisks }) => {
/>
<VnLv
v-if="entity.creditInsurance"
:label="t('customer.summary.securedCredit')"
:value="toCurrency(entity.creditInsurance)"
:info="t('customer.summary.securedCreditInfo')"
/>
<VnLv
:label="t('customer.summary.balance')"
:value="toCurrency(sumRisk(entity)) || toCurrency(0)"
@ -301,7 +319,7 @@ const sumRisk = ({ clientRisks }) => {
:value="entity.recommendedCredit"
/>
</QCard>
<QCard>
<QCard class="vn-one">
<VnTitle :text="t('Latest tickets')" />
<CustomerSummaryTable />
</QCard>

View File

@ -68,7 +68,6 @@ const columns = computed(() => [
fields: ['id', 'name'],
where: { role: 'salesPerson' },
optionFilter: 'firstName',
useLike: false,
},
create: false,
columnField: {
@ -429,9 +428,10 @@ function handleLocation(data, location) {
:params="{
departmentCodes: ['VT', 'shopping'],
}"
:fields="['id', 'nickname']"
:fields="['id', 'nickname', 'code']"
sort-by="nickname ASC"
:use-like="false"
option-label="nickname"
option-value="id"
emit-value
auto-load
>

View File

@ -85,15 +85,26 @@ function handleLocation(data, location) {
<QCheckbox :label="t('Default')" v-model="data.isDefaultAddress" />
<VnRow>
<VnInput :label="t('Consignee')" clearable v-model="data.nickname" />
<VnInput
:label="t('Consignee')"
required
clearable
v-model="data.nickname"
/>
<VnInput :label="t('Street address')" clearable v-model="data.street" />
<VnInput
:label="t('Street address')"
clearable
v-model="data.street"
required
/>
</VnRow>
<VnLocation
:rules="validate('Worker.postcode')"
:acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
v-model="data.location"
:required="true"
@update:model-value="(location) => handleLocation(data, location)"
/>

View File

@ -3,7 +3,7 @@ import { onBeforeMount, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
import { getClientRisk } from '../composables/getClientRisk';
import { useDialogPluginComponent } from 'quasar';
import { usePrintService } from 'composables/usePrintService';
@ -158,9 +158,7 @@ async function getAmountPaid() {
},
};
const { data } = await axios(`ClientRisks`, {
params: { filter: JSON.stringify(filter) },
});
const { data } = await getClientRisk(filter);
initialData.amountPaid = (data?.length && data[0].amount) || undefined;
}
</script>
@ -241,7 +239,7 @@ async function getAmountPaid() {
</QItem>
</template>
</VnSelect>
<VnInput
<VnInputNumber
:label="t('Amount')"
:required="true"
@update:model-value="calculateFromAmount($event)"
@ -254,7 +252,7 @@ async function getAmountPaid() {
{{ t('Compensation') }}
</div>
<VnRow>
<VnInput
<VnInputNumber
:label="t('Compensation account')"
clearable
v-model="data.compensationAccount"

View File

@ -63,7 +63,7 @@ const columns = computed(() => [
},
{
align: 'left',
format: (row, dashIfEmpty) => dashIfEmpty(row.agencyMode?.name),
format: (row) => dashIfEmpty(row.agencyMode?.name),
columnClass: 'expand',
label: t('Agency'),
},
@ -111,7 +111,11 @@ const columns = computed(() => [
{
title: t('customer.summary.goToLines'),
icon: 'vn:lines',
action: ({ id }) => router.push({ params: { id }, name: 'TicketSale' }),
action: ({ id }) =>
window.open(
router.resolve({ params: { id }, name: 'TicketSale' }).href,
'_blank'
),
isPrimary: true,
},
{
@ -150,6 +154,8 @@ const setShippedColor = (date) => {
if (difference == 0) return 'warning';
if (difference < 0) return 'success';
};
const rowClick = ({ id }) =>
window.open(router.resolve({ params: { id }, name: 'TicketSummary' }).href, '_blank');
const getItemPackagingType = (ticketSales) => {
if (!ticketSales?.length) return '-';
@ -177,13 +183,14 @@ const getItemPackagingType = (ticketSales) => {
:column-search="false"
url="Tickets"
:columns="columns"
search-url="tickets"
:without-header="true"
auto-load
:row-click="rowClick"
order="shipped DESC, id"
:disable-option="{ card: true, table: true }"
class="full-width"
:disable-infinite-scroll="true"
:search-url="false"
>
<template #column-nickname="{ row }">
<span class="link">

View File

@ -0,0 +1,12 @@
import axios from 'axios';
export async function getClientRisk(_filter) {
const filter = {
..._filter,
include: { relation: 'company', scope: { fields: ['code'] } },
};
return await axios(`ClientRisks`, {
params: { filter: JSON.stringify(filter) },
});
}

View File

@ -3,8 +3,6 @@ import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import { useArrayData } from 'src/composables/useArrayData';
import { downloadFile } from 'src/composables/downloadFile';
import FormModel from 'components/FormModel.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
@ -12,15 +10,15 @@ import FetchData from 'src/components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnDms from 'src/components/common/VnDms.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import axios from 'axios';
const quasar = useQuasar();
const { t } = useI18n();
const dms = ref({});
const route = useRoute();
const quasar = useQuasar();
const editDownloadDisabled = ref(false);
const arrayData = useArrayData();
const invoiceIn = computed(() => arrayData.store.data);
const userConfig = ref(null);
const invoiceId = computed(() => +route.params.id);
@ -36,98 +34,25 @@ const warehousesRef = ref();
const allowTypesRef = ref();
const allowedContentTypes = ref([]);
const sageWithholdings = ref([]);
const inputFileRef = ref();
const editDmsRef = ref();
const createDmsRef = ref();
const documentDialogRef = ref({});
const invoiceInRef = ref({});
async function checkFileExists(dmsId) {
if (!dmsId) return;
try {
await axios.get(`Dms/${dmsId}`, { fields: ['id'] });
editDownloadDisabled.value = false;
} catch (e) {
editDownloadDisabled.value = true;
}
}
async function setEditDms(dmsId) {
const { data } = await axios.get(`Dms/${dmsId}`);
dms.value = {
warehouseId: data.warehouseFk,
companyId: data.companyFk,
dmsTypeId: data.dmsTypeFk,
...data,
};
if (!allowedContentTypes.value.length) await allowTypesRef.value.fetch();
editDmsRef.value.show();
}
async function setCreateDms() {
const { data } = await axios.get('DmsTypes/findOne', {
where: { code: 'invoiceIn' },
});
dms.value = {
reference: invoiceIn.value.supplierRef,
warehouseId: userConfig.value.warehouseFk,
companyId: userConfig.value.companyFk,
dmsTypeId: data.id,
description: invoiceIn.value.supplier.name,
hasFile: true,
hasFileAttached: true,
files: null,
};
createDmsRef.value.show();
}
async function onSubmit() {
try {
const isEdit = !!dms.value.id;
const errors = {
companyId: `The company can't be empty`,
warehouseId: `The warehouse can't be empty`,
dmsTypeId: `The DMS Type can't be empty`,
description: `The description can't be empty`,
};
Object.keys(errors).forEach((key) => {
if (!dms.value[key]) throw Error(t(errors[key]));
function deleteFile(dmsFk) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.confirmDeletion'),
message: t('globals.confirmDeletionMessage'),
},
})
.onOk(async () => {
await axios.post(`dms/${dmsFk}/removeFile`);
invoiceInRef.value.formData.dmsFk = null;
invoiceInRef.value.formData.dms = undefined;
invoiceInRef.value.hasChanges = true;
invoiceInRef.value.save();
});
if (!isEdit && !dms.value.files) throw Error(t(`The files can't be empty`));
const formData = new FormData();
if (dms.value.files) {
for (let i = 0; i < dms.value.files.length; i++)
formData.append(dms.value.files[i].name, dms.value.files[i]);
dms.value.hasFileAttached = true;
}
const url = isEdit ? `dms/${dms.value.id}/updateFile` : 'Dms/uploadFile';
const { data } = await axios.post(url, formData, {
params: dms.value,
});
if (data.length) invoiceIn.value.dmsFk = data[0].id;
if (!isEdit) {
createDmsRef.value.hide();
} else {
editDmsRef.value.hide();
}
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
} catch (error) {
quasar.notify({
message: t(`${error.message}`),
type: 'negative',
});
}
}
</script>
<template>
@ -181,10 +106,12 @@ async function onSubmit() {
@on-fetch="(data) => (sageWithholdings = data)"
/>
<FormModel
ref="invoiceInRef"
model="InvoiceIn"
:go-to="`/invoice-in/${invoiceId}/vat`"
auto-load
:url-update="`InvoiceIns/${invoiceId}/updateInvoiceIn`"
@on-fetch="(data) => (documentDialogRef.supplierName = data.supplier.nickname)"
auto-load
>
<template #form="{ data }">
<VnRow>
@ -242,16 +169,22 @@ async function onSubmit() {
</QItem>
</template>
</VnSelect>
<VnInput
:label="t('Document')"
v-model="data.dmsFk"
clearable
clear-icon="close"
@update:model-value="checkFileExists(data.dmsFk)"
>
<template #prepend>
<div class="row no-wrap">
<VnInput
:label="t('Document')"
v-model="data.dmsFk"
clearable
clear-icon="close"
class="full-width"
:disable="true"
/>
<div
v-if="data.dmsFk"
class="row no-wrap q-pa-xs q-gutter-x-xs"
data-cy="dms-buttons"
>
<QBtn
v-if="data.dmsFk"
:class="{
'no-pointer-events': editDownloadDisabled,
}"
@ -262,33 +195,52 @@ async function onSubmit() {
round
@click="downloadFile(data.dmsFk)"
/>
</template>
<template #append>
<QBtn
:class="{
'no-pointer-events': editDownloadDisabled,
}"
:disable="editDownloadDisabled"
v-if="data.dmsFk"
icon="edit"
round
padding="xs"
@click="setEditDms(data.dmsFk)"
@click="
() => {
documentDialogRef.show = true;
documentDialogRef.dms = data.dms;
}
"
>
<QTooltip>{{ t('Edit document') }}</QTooltip>
</QBtn>
<QBtn
v-else
icon="add_circle"
round
shortcut="+"
:class="{
'no-pointer-events': editDownloadDisabled,
}"
:disable="editDownloadDisabled"
icon="delete"
:title="t('Delete file')"
padding="xs"
@click="setCreateDms()"
>
<QTooltip>{{ t('Create document') }}</QTooltip>
</QBtn>
</template>
</VnInput>
round
@click="deleteFile(data.dmsFk)"
/>
</div>
<QBtn
v-else
icon="add_circle"
round
shortcut="+"
padding="xs"
@click="
() => {
documentDialogRef.show = true;
delete documentDialogRef.dms;
}
"
data-cy="dms-create"
>
<QTooltip>{{ t('Create document') }}</QTooltip>
</QBtn>
</div>
</VnRow>
<VnRow>
<VnSelect
@ -319,237 +271,28 @@ async function onSubmit() {
</VnRow>
</template>
</FormModel>
<QDialog ref="editDmsRef">
<QForm @submit="onSubmit()" class="all-pointer-events">
<QCard class="q-pa-sm">
<QCardSection class="row items-center q-pb-none">
<span class="text-primary text-h6">
<QIcon name="edit" class="q-mr-xs" />
{{ t('Edit document') }}
</span>
<QSpace />
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<QCardSection class="q-py-none">
<QItem>
<VnInput
class="full-width q-pa-xs"
:label="t('Reference')"
v-model="dms.reference"
clearable
clear-icon="close"
/>
<VnSelect
class="full-width q-pa-xs"
:label="t('Company')"
v-model="dms.companyId"
:options="companies"
option-value="id"
option-label="code"
:required="true"
/>
</QItem>
<QItem>
<VnSelect
class="full-width q-pa-xs"
:label="t('Warehouse')"
v-model="dms.warehouseId"
:options="warehouses"
option-value="id"
option-label="name"
:required="true"
/>
<VnSelect
class="full-width q-pa-xs"
:label="t('Type')"
v-model="dms.dmsTypeId"
:options="dmsTypes"
option-value="id"
option-label="name"
:required="true"
/>
</QItem>
<QItem>
<VnInput
:label="t('Description')"
v-model="dms.description"
:required="true"
type="textarea"
class="full-width q-pa-xs"
size="lg"
autogrow
clearable
clear-icon="close"
/>
</QItem>
<QItem>
<QFile
ref="inputFileRef"
class="full-width q-pa-xs"
:label="t('File')"
v-model="dms.files"
multiple
:accept="allowedContentTypes.join(',')"
clearable
clear-icon="close"
>
<template #append>
<QBtn
icon="attach_file_add"
flat
round
padding="xs"
@click="inputFileRef.pickFiles()"
>
<QTooltip>
{{ t('globals.selectFile') }}
</QTooltip>
</QBtn>
<QBtn icon="info" flat round padding="xs">
<QTooltip max-width="30rem">
{{
`${t(
'Allowed content types'
)}: ${allowedContentTypes.join(', ')}`
}}
</QTooltip>
</QBtn>
</template>
</QFile>
</QItem>
<QItem>
<QCheckbox
:label="t('Generate identifier for original file')"
v-model="dms.hasFile"
/>
</QItem>
</QCardSection>
<QCardActions class="justify-end">
<QBtn
flat
:label="t('globals.close')"
color="primary"
v-close-popup
/>
<QBtn :label="t('globals.save')" color="primary" @click="onSubmit" />
</QCardActions>
</QCard>
</QForm>
</QDialog>
<QDialog ref="createDmsRef">
<QForm @submit="onSubmit()" class="all-pointer-events">
<QCard class="q-pa-sm">
<QCardSection class="row items-center q-pb-none">
<span class="text-primary text-h6">
<QIcon name="edit" class="q-mr-xs" />
{{ t('Create document') }}
</span>
<QSpace />
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<QCardSection class="q-pb-none">
<QItem>
<VnInput
class="full-width q-pa-xs"
:label="t('Reference')"
v-model="dms.reference"
/>
<VnSelect
class="full-width q-pa-xs"
:label="`${t('Company')}*`"
v-model="dms.companyId"
:options="companies"
option-value="id"
option-label="code"
:required="true"
/>
</QItem>
<QItem>
<VnSelect
class="full-width q-pa-xs"
:label="`${t('Warehouse')}*`"
v-model="dms.warehouseId"
:options="warehouses"
option-value="id"
option-label="name"
:required="true"
/>
<VnSelect
class="full-width q-pa-xs"
:label="`${t('Type')}*`"
v-model="dms.dmsTypeId"
:options="dmsTypes"
option-value="id"
option-label="name"
:required="true"
/>
</QItem>
<QItem>
<VnInput
class="full-width q-pa-xs"
type="textarea"
size="lg"
autogrow
:label="`${t('Description')}*`"
v-model="dms.description"
clearable
clear-icon="close"
:rules="[(val) => val.length || t('Required field')]"
/>
</QItem>
<QItem>
<QFile
ref="inputFileRef"
class="full-width q-pa-xs"
:label="t('File')"
v-model="dms.files"
multiple
:accept="allowedContentTypes.join(',')"
clearable
clear-icon="close"
>
<template #append>
<QBtn
icon="attach_file_add"
flat
round
padding="xs"
@click="inputFileRef.pickFiles()"
>
<QTooltip>
{{ t('globals.selectFile') }}
</QTooltip>
</QBtn>
<QBtn icon="info" flat round padding="xs">
<QTooltip max-width="30rem">
{{
`${t(
'Allowed content types'
)}: ${allowedContentTypes.join(', ')}`
}}
</QTooltip>
</QBtn>
</template>
</QFile>
</QItem>
<QItem>
<QCheckbox
:label="t('Generate identifier for original file')"
v-model="dms.hasFile"
/>
</QItem>
</QCardSection>
<QCardActions align="right">
<QBtn
flat
:label="t('globals.close')"
color="primary"
v-close-popup
/>
<QBtn :label="t('globals.save')" color="primary" @click="onSubmit" />
</QCardActions>
</QCard>
</QForm>
<QDialog v-model="documentDialogRef.show">
<VnDms
model="dms"
default-dms-code="invoiceIn"
:form-initial-data="documentDialogRef.dms"
:url="
documentDialogRef.dms
? `Dms/${documentDialogRef.dms.id}/updateFile`
: 'Dms/uploadFile'
"
:description="documentDialogRef.supplierName"
@on-data-saved="
(_, { data }) => {
let dmsData = data;
if (Array.isArray(data)) dmsData = data[0];
invoiceInRef.formData.dmsFk = dmsData.id;
invoiceInRef.formData.dms = dmsData;
invoiceInRef.hasChanges = true;
invoiceInRef.save();
}
"
/>
</QDialog>
</template>
<style lang="scss" scoped>

View File

@ -20,6 +20,23 @@ const filter = {
{ relation: 'invoiceInDueDay' },
{ relation: 'company' },
{ relation: 'currency' },
{
relation: 'dms',
scope: {
fields: [
'dmsTypeFk',
'reference',
'hardCopyNumber',
'workerFk',
'description',
'hasFile',
'file',
'created',
'companyFk',
'warehouseFk',
],
},
},
],
};

View File

@ -13,7 +13,7 @@ const { t } = useI18n();
const invoiceOutGlobalStore = useInvoiceOutGlobalStore();
// invoiceOutGlobalStore state and getters
const { initialDataLoading, formInitialData, invoicing, status } =
const { initialDataLoading, formInitialData, status } =
storeToRefs(invoiceOutGlobalStore);
// invoiceOutGlobalStore actions
@ -151,9 +151,8 @@ onMounted(async () => {
rounded
/>
</div>
<QBtn
v-if="!invoicing"
v-if="!getStatus || getStatus === 'stopping'"
:label="t('invoiceOut')"
type="submit"
color="primary"
@ -163,7 +162,7 @@ onMounted(async () => {
dense
/>
<QBtn
v-if="invoicing"
v-else
:label="t('stop')"
color="primary"
class="q-mt-md full-width"

View File

@ -109,7 +109,11 @@ const insertTag = (rows) => {
>
<template #body="{ rows, validate }">
<QCard class="q-px-lg q-pt-md q-pb-sm">
<VnRow v-for="(row, index) in rows" :key="index">
<VnRow
v-for="(row, index) in rows"
:key="index"
class="items-center"
>
<VnSelect
:label="t('itemTags.tag')"
:options="tagOptions"
@ -153,13 +157,14 @@ const insertTag = (rows) => {
:required="true"
:rules="validate('itemTag.priority')"
/>
<div class="row justify-center items-center" style="flex: 0">
<div class="row justify-center" style="flex: 0">
<QIcon
@click="itemTagsRef.remove([row])"
class="fill-icon-on-hover"
color="primary"
name="delete"
size="sm"
dense
>
<QTooltip>
{{ t('itemTags.removeTag') }}
@ -167,22 +172,20 @@ const insertTag = (rows) => {
</QIcon>
</div>
</VnRow>
<VnRow class="justify-center items-center">
<QBtn
@click="insertTag(rows)"
class="cursor-pointer"
color="primary"
flat
icon="add"
shortcut="+"
style="flex: 0"
>
<QTooltip>
{{ t('itemTags.addTag') }}
</QTooltip>
</QBtn>
</VnRow>
</QCard>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
@click="insertTag(rows)"
color="primary"
icon="add"
shortcut="+"
fab
>
<QTooltip>
{{ t('itemTags.addTag') }}
</QTooltip>
</QBtn>
</QPageSticky>
</template>
</CrudModel>
</QPage>

View File

@ -12,6 +12,8 @@ import ItemSummary from '../Item/Card/ItemSummary.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
import { cloneItem } from 'src/pages/Item/composables/cloneItem';
import RightMenu from 'src/components/common/RightMenu.vue';
import ItemListFilter from './ItemListFilter.vue';
const entityId = computed(() => route.params.id);
const { openCloneDialog } = cloneItem();
@ -311,6 +313,11 @@ const columns = computed(() => [
:label="t('item.searchbar.label')"
:info="t('You can search by id')"
/>
<RightMenu>
<template #right-panel>
<ItemListFilter data-key="ItemList" />
</template>
</RightMenu>
<VnTable
ref="tableRef"
data-key="ItemList"
@ -329,6 +336,7 @@ const columns = computed(() => [
auto-load
redirect="Item"
:is-editable="false"
:right-search="false"
:filer="itemFilter"
>
<template #column-id="{ row }">

View File

@ -1,5 +1,4 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
@ -9,8 +8,11 @@ import VnSelect from 'src/components/common/VnSelect.vue';
const route = useRoute();
const { t } = useI18n();
const workersOptions = ref([]);
const companySizes = [
{ id: 'small', name: t('globals.small'), size: '1-5' },
{ id: 'medium', name: t('globals.medium'), size: '6-50' },
{ id: 'big', name: t('globals.big'), size: '>50' },
];
</script>
<template>
<FormModel
@ -31,11 +33,6 @@ const workersOptions = ref([]);
<VnSelect
:label="t('supplier.basicData.workerFk')"
v-model="data.workerFk"
:options="workersOptions"
option-value="id"
option-label="name"
hide-selected
map-options
url="Workers/search"
sort-by="nickname ASC"
:rules="validate('supplier.workerFk')"
@ -58,6 +55,24 @@ const workersOptions = ref([]);
</QItem>
</template>
</VnSelect>
<VnSelect
:label="t('supplier.basicData.size')"
v-model="data.companySize"
:options="companySizes"
sort-by="nickname ASC"
:rules="validate('supplier.workerFk')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt?.size }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow>
<VnRow>
<QCheckbox
@ -88,4 +103,5 @@ const workersOptions = ref([]);
<i18n>
es:
Responsible for approving invoices: Responsable de aprobar las facturas
Small(1-5), Medium(6-50), Big(> 50): Pequeño(1-5), Mediano(6-50), Grande(> 50)
</i18n>

View File

@ -65,6 +65,22 @@ const getUrl = (section) => `#/supplier/${entityId.value}/${section}`;
<span> {{ dashIfEmpty(supplier.note) }} </span>
</template>
</VnLv>
<VnLv
:label="t('supplier.basicData.size')"
:value="supplier.companySize"
class="q-mb-xs"
>
<template #value>
<span>
{{
dashIfEmpty(
supplier.companySize &&
t('globals.' + supplier.companySize)
)
}}
</span>
</template>
</VnLv>
<QCheckbox
:label="t('supplier.summary.verified')"
v-model="supplier.isSerious"

View File

@ -3,11 +3,12 @@ import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnTable from 'components/VnTable/VnTable.vue';
import FetchData from 'src/components/FetchData.vue';
const tableRef = ref();
const payrollComponents = ref([]);
const { t } = useI18n();
const route = useRoute();
const entityId = computed(() => route.params.id);
const columns = computed(() => [
{
align: 'left',
@ -25,8 +26,9 @@ const columns = computed(() => [
create: true,
component: 'select',
attrs: {
url: 'payrollComponents',
fields: ['id', 'name'],
options: payrollComponents,
optionLabel: 'name',
optionValue: 'id',
},
cardVisible: true,
},
@ -73,6 +75,16 @@ const columns = computed(() => [
]);
</script>
<template>
<FetchData
url="PayrollComponents"
:filter="{
fields: ['id', 'name'],
where: { name: { neq: '' } },
order: 'name ASC',
}"
@on-fetch="(data) => (payrollComponents = data)"
auto-load
/>
<VnTable
ref="tableRef"
data-key="WorkerBalance"
@ -94,6 +106,7 @@ const columns = computed(() => [
:is-editable="true"
:use-model="true"
:default-remove="false"
search-url="balance"
/>
</template>
<i18n>

View File

@ -134,6 +134,7 @@ const columns = computed(() => [
:is-editable="true"
:use-model="true"
:default-remove="false"
search-url="formation"
/>
</template>
<style lang="scss" scoped>

View File

@ -100,5 +100,6 @@ const columns = [
:is-editable="true"
:use-model="true"
:default-remove="false"
search-url="medical"
/>
</template>

View File

@ -71,6 +71,7 @@ function setNotifications(data) {
:default-remove="false"
:default-save="false"
@on-fetch="setNotifications"
search-url="notifications"
>
<template #body>
<div

View File

@ -63,6 +63,7 @@ function reloadData() {
url="DeviceProductionUsers"
:filter="{ where: { userFk: routeId } }"
order="id"
search-url="pda"
auto-load
>
<template #body="{ rows }">

View File

@ -93,7 +93,7 @@ export const useInvoiceOutGlobalStore = defineStore({
async makeInvoice(formData, clientsToInvoice) {
this.invoicing = true;
this.status = 'packageInvoicing';
const promises = [];
try {
this.printer = formData.printer;
const params = {
@ -118,10 +118,11 @@ export const useInvoiceOutGlobalStore = defineStore({
);
throw new Error("There aren't addresses to invoice");
}
for (const address of this.addresses) {
await this.invoiceClient(address, formData);
this.status = 'invoicing';
for (let index = 0; index < this.parallelism; index++) {
promises.push(this.invoiceClient(formData, index));
}
await Promise.all(promises);
} catch (err) {
this.handleError(err);
}
@ -171,17 +172,14 @@ export const useInvoiceOutGlobalStore = defineStore({
}
},
async invoiceClient(address, formData) {
async invoiceClient(formData, index) {
const address = this.addresses[index];
if (!address || !this.status || this.status == 'stopping') {
this.status = 'stopping';
this.invoicing = false;
return;
}
try {
if (this.nRequests === this.parallelism || this.isInvoicing) return;
if (this.status === 'stopping') {
if (this.nRequests) return;
this.invoicing = false;
this.status = 'done';
return;
}
const params = {
clientId: address.clientId,
addressId: address.id,
@ -191,13 +189,11 @@ export const useInvoiceOutGlobalStore = defineStore({
serialType: formData.serialType,
};
this.status = 'invoicing';
this.invoicing = true;
const { data } = await axios.post('InvoiceOuts/invoiceClient', params);
if (data) await this.makePdfAndNotify(data, address);
this.addressIndex++;
this.isInvoicing = false;
} catch (err) {
if (err?.response?.status >= 400 && err?.response?.status < 500) {
@ -205,13 +201,16 @@ export const useInvoiceOutGlobalStore = defineStore({
return;
} else {
this.invoicing = false;
this.status = 'done';
notify(
'invoiceOut.globalInvoices.errors.criticalInvoiceError',
'negative'
);
throw new Error('Critical invoicing error, process stopped');
}
} finally {
this.addressIndex++;
if (this.status != 'stopping')
await this.invoiceClient(formData, this.addressIndex);
}
},
@ -234,7 +233,6 @@ export const useInvoiceOutGlobalStore = defineStore({
handleError(err) {
this.invoicing = false;
this.status = null;
throw err;
},
@ -279,7 +277,7 @@ export const useInvoiceOutGlobalStore = defineStore({
return 0;
}
let porcentaje = (state.addressIndex / this.getNAddresses) * 100;
return porcentaje;
return porcentaje?.toFixed(2);
},
getAddressNumber(state) {
return state.addressIndex;

View File

@ -28,7 +28,7 @@ describe('Client list', () => {
cy.get('.q-mt-lg > .q-btn--standard').click();
cy.checkNotification('Data created');
cy.checkNotification('created');
cy.url().should('include', '/summary');
});
it('Client list search client', () => {
@ -43,4 +43,21 @@ describe('Client list', () => {
cy.url().should('include', `/customer/${id}/summary`);
});
});
it('Client founded create ticket', () => {
const search = 'Jessica Jones';
cy.searchByLabel('Name', search);
cy.clickButtonsDescriptor(2);
cy.waitForElement('#formModel');
cy.waitForElement('.q-form');
cy.checkValueForm(1, search);
});
it('Client founded create order', () => {
const search = 'Jessica Jones';
cy.searchByLabel('Name', search);
cy.clickButtonsDescriptor(4);
cy.waitForElement('#formModel');
cy.waitForElement('.q-form');
cy.checkValueForm(2, search);
});
});

View File

@ -2,9 +2,8 @@
describe('InvoiceInBasicData', () => {
const formInputs = '.q-form > .q-card input';
const firstFormSelect = '.q-card > .vn-row:nth-child(1) > .q-select';
const documentBtns = '.q-form .q-field button';
const documentBtns = '[data-cy="dms-buttons"] button';
const dialogInputs = '.q-dialog input';
const dialogActionBtns = '.q-card__actions button';
beforeEach(() => {
cy.login('developer');
@ -21,27 +20,35 @@ describe('InvoiceInBasicData', () => {
cy.get(formInputs).eq(1).invoke('val').should('eq', '4739');
});
it('should edit the dms data', () => {
it('should edit, remove and create the dms data', () => {
const firtsInput = 'Ticket:65';
const secondInput = "I don't know what posting here!";
//edit
cy.get(documentBtns).eq(1).click();
cy.get(dialogInputs).eq(0).type(`{selectall}${firtsInput}`);
cy.get('textarea').type(`{selectall}${secondInput}`);
cy.get(dialogActionBtns).eq(1).click();
cy.get('[data-cy="FormModelPopup_save"]').click();
cy.get(documentBtns).eq(1).click();
cy.get(dialogInputs).eq(0).invoke('val').should('eq', firtsInput);
cy.get('textarea').invoke('val').should('eq', secondInput);
});
cy.get('[data-cy="FormModelPopup_save"]').click();
cy.checkNotification('Data saved');
it('should throw an error creating a new dms if a file is not attached', () => {
cy.get(formInputs).eq(7).type('{selectall}{backspace}');
cy.get(documentBtns).eq(0).click();
cy.get(dialogActionBtns).eq(1).click();
cy.get('.q-notification__message').should(
'have.text',
"The files can't be empty"
//remove
cy.get(documentBtns).eq(2).click();
cy.get('[data-cy="VnConfirm_confirm"]').click();
cy.checkNotification('Data saved');
//create
cy.get('[data-cy="dms-create"]').eq(0).click();
cy.get('[data-cy="VnDms_inputFile"').selectFile(
'test/cypress/fixtures/image.jpg',
{
force: true,
}
);
cy.get('[data-cy="FormModelPopup_save"]').click();
cy.checkNotification('Data saved');
});
});

View File

@ -14,6 +14,8 @@ describe('Ticket descriptor', () => {
it('should clone the ticket without warehouse', () => {
cy.visit('/#/ticket/1/summary');
cy.intercept('GET', /\/api\/Tickets\/\d/).as('ticket');
cy.wait('@ticket');
cy.openActionsDescriptor();
cy.contains(listItem, toCloneOpt).click();
cy.clickConfirm();
@ -28,6 +30,8 @@ describe('Ticket descriptor', () => {
it('should set the weight of the ticket', () => {
cy.visit('/#/ticket/10/summary');
cy.intercept('GET', /\/api\/Tickets\/\d/).as('ticket');
cy.wait('@ticket');
cy.openActionsDescriptor();
cy.contains(listItem, setWeightOpt).click();
cy.intercept('POST', /\/api\/Tickets\/\d+\/setWeight/).as('weight');

View File

@ -12,7 +12,7 @@ describe('VnLocation', () => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('/#/supplier/567/fiscal-data', { timeout: 7000 });
cy.waitForElement('.q-card');
cy.domContentLoad();
cy.get(createLocationButton).click();
});
it('should filter provinces based on selected country', () => {

View File

@ -27,6 +27,10 @@
// DO NOT REMOVE
// Imports Quasar Cypress AE predefined commands
// import { registerCommands } from '@quasar/quasar-app-extension-testing-e2e-cypress';
Cypress.Commands.add('waitUntil', { prevSubject: 'optional' }, require('./waitUntil'));
Cypress.Commands.add('resetDB', () => {
cy.exec('pnpm run resetDatabase');
});
Cypress.Commands.add('login', (user) => {
//cy.visit('/#/login');
cy.request({
@ -50,10 +54,12 @@ Cypress.Commands.add('login', (user) => {
});
});
Cypress.Commands.add('waitForElement', (element, timeout = 5000) => {
cy.get(element, { timeout }).should('be.visible');
Cypress.Commands.add('domContentLoad', (element, timeout = 5000) => {
cy.waitUntil(() => cy.document().then((doc) => doc.readyState === 'complete'));
});
Cypress.Commands.add('waitForElement', (element, timeout = 5000) => {
cy.waitUntil(() => cy.get(element).then(($el) => $el.is(':visible')));
});
Cypress.Commands.add('getValue', (selector) => {
cy.get(selector).then(($el) => {
if ($el.find('.q-checkbox__inner').length > 0) {
@ -261,7 +267,13 @@ Cypress.Commands.add('openActionDescriptor', (opt) => {
});
Cypress.Commands.add('openActionsDescriptor', () => {
cy.get('.header > :nth-child(3) > .q-btn__content > .q-icon').click();
cy.get('[data-cy="descriptor-more-opts"]').click();
});
Cypress.Commands.add('clickButtonsDescriptor', (id) => {
cy.get(`.actions > .q-card__actions> .q-btn:nth-child(${id})`)
.invoke('removeAttr', 'target')
.click();
});
Cypress.Commands.add('openUserPanel', () => {
@ -284,6 +296,18 @@ Cypress.Commands.add('checkNotification', (text) => {
});
});
Cypress.Commands.add('checkValueForm', (id, search) => {
cy.get(
`.grid-create > :nth-child(${id}) > .q-field__inner>.q-field__control> .q-field__control-container>.q-field__native >.q-field__input`
).should('have.value', search);
});
Cypress.Commands.add('checkValueSelectForm', (id, search) => {
cy.get(
`.grid-create > :nth-child(${id}) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container>.q-field__native>.q-field__input`
).should('have.value', search);
});
Cypress.Commands.add('searchByLabel', (label, value) => {
cy.get(`[label="${label}"] > .q-field > .q-field__inner`).type(`${value}{enter}`);
});

View File

@ -0,0 +1,59 @@
const waitUntil = (subject, checkFunction, originalOptions = {}) => {
if (!(checkFunction instanceof Function)) {
throw new Error(
'`checkFunction` parameter should be a function. Found: ' + checkFunction
);
}
const defaultOptions = {
// base options
interval: 200,
timeout: 5000,
errorMsg: 'Timed out retrying',
// log options
description: 'waitUntil',
log: true,
customMessage: undefined,
logger: Cypress.log,
verbose: false,
customCheckMessage: undefined,
};
const options = { ...defaultOptions, ...originalOptions };
// filter out a falsy passed "customMessage" value
options.customMessage = [options.customMessage, originalOptions].filter(Boolean);
const endTime = Date.now() + options.timeout;
const check = (result) => {
if (result) {
return result;
}
if (Date.now() >= endTime) {
const msg =
options.errorMsg instanceof Function
? options.errorMsg(result, options)
: options.errorMsg;
throw new Error(msg);
}
cy.wait(options.interval, { log: false }).then(() => {
return resolveValue();
});
};
const resolveValue = () => {
const result = checkFunction(subject);
const isAPromise = Boolean(result && result.then);
if (isAPromise) {
return result.then(check);
} else {
return check(result);
}
};
return resolveValue();
};
export default waitUntil;

View File

@ -0,0 +1,47 @@
import { describe, expect, it, beforeAll, beforeEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnTable from 'src/components/VnTable/VnTable.vue';
describe('VnTable', () => {
let wrapper;
let vm;
beforeAll(() => {
wrapper = createWrapper(VnTable, {
propsData: {
columns: [],
},
});
vm = wrapper.vm;
});
beforeEach(() => (vm.selected = []));
describe('handleSelection()', () => {
const rows = [{ $index: 0 }, { $index: 1 }, { $index: 2 }];
const selectedRows = [{ $index: 1 }];
it('should add rows to selected when shift key is pressed and rows are added except last one', () => {
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows
);
expect(vm.selected).toEqual([{ $index: 0 }]);
});
it('should not add rows to selected when shift key is not pressed', () => {
vm.handleSelection(
{ evt: { shiftKey: false }, added: true, rows: selectedRows },
rows
);
expect(vm.selected).toEqual([]);
});
it('should not add rows to selected when rows are not added', () => {
vm.handleSelection(
{ evt: { shiftKey: true }, added: false, rows: selectedRows },
rows
);
expect(vm.selected).toEqual([]);
});
});
});