0
0
Fork 0

Merge branch 'master' of https://gitea.verdnatura.es/verdnatura/salix-front into hotfix-amountClaimLines

This commit is contained in:
Carlos Satorres 2023-12-14 12:07:57 +01:00
commit 3eb0b38633
126 changed files with 6709 additions and 3679 deletions

View File

@ -64,7 +64,7 @@ module.exports = {
}, },
overrides: [ overrides: [
{ {
files: ['test/cypress/**/*.spec.{js,ts}'], files: ['test/cypress/**/*.*'],
extends: [ extends: [
// Add Cypress-specific lint rules, globals and Cypress plugin // Add Cypress-specific lint rules, globals and Cypress plugin
// See https://github.com/cypress-io/eslint-plugin-cypress#rules // See https://github.com/cypress-io/eslint-plugin-cypress#rules

View File

@ -13,5 +13,6 @@
], ],
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} },
"cSpell.words": ["axios"]
} }

View File

@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2350.01] - 2023-12-14
### Added
- (Carros) => Se añade contador de carros. #6545
- (Reclamaciones) => Se añade la sección para hacer acciones sobre una reclamación. #5654
### Changed
### Fixed
- (Reclamaciones) => Se corrige el color de la barra según el tema y el evento de actualziar cantidades #6334
## [2253.01] - 2023-01-05 ## [2253.01] - 2023-01-05
### Added ### Added

View File

@ -7,7 +7,7 @@ module.exports = defineConfig({
screenshotsFolder: 'test/cypress/screenshots', screenshotsFolder: 'test/cypress/screenshots',
supportFile: 'test/cypress/support/index.js', supportFile: 'test/cypress/support/index.js',
videosFolder: 'test/cypress/videos', videosFolder: 'test/cypress/videos',
video: true, video: false,
specPattern: 'test/cypress/integration/*.spec.js', specPattern: 'test/cypress/integration/*.spec.js',
experimentalRunAllSpecs: true, experimentalRunAllSpecs: true,
component: { component: {

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "0.0.1", "version": "23.40.01",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "0.0.1", "version": "23.50.01",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -9,13 +9,13 @@
"lint": "eslint --ext .js,.vue ./", "lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test:e2e": "cypress open", "test:e2e": "cypress open",
"test:e2e:ci": "cypress run --browser chromium", "test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run --browser chromium",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest", "test:unit": "vitest",
"test:unit:ci": "vitest run" "test:unit:ci": "vitest run"
}, },
"dependencies": { "dependencies": {
"@quasar/cli": "^2.2.1", "@quasar/cli": "^2.3.0",
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"axios": "^1.4.0", "axios": "^1.4.0",
"chromium": "^3.0.3", "chromium": "^3.0.3",

View File

@ -66,7 +66,9 @@ module.exports = configure(function (/* ctx */) {
// publicPath: '/', // publicPath: '/',
// analyze: true, // analyze: true,
// env: {}, // env: {},
// rawDefine: {} rawDefine: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
},
// ignorePublicFolder: true, // ignorePublicFolder: true,
// minify: false, // minify: false,
// polyfillModulePreload: true, // polyfillModulePreload: true,
@ -89,11 +91,12 @@ module.exports = configure(function (/* ctx */) {
vitePlugins: [ vitePlugins: [
[ [
VueI18nPlugin, VueI18nPlugin({
runtimeOnly: false
}),
{ {
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false` // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false, // compositionOnly: false,
// you need to set i18n resource including paths ! // you need to set i18n resource including paths !
include: path.resolve(__dirname, './src/i18n/**'), include: path.resolve(__dirname, './src/i18n/**'),
}, },

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

32
src/assets/salix_dark.svg Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 400 168.6" style="enable-background:new 0 0 400 168.6;" xmlns="http://www.w3.org/2000/svg">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#3D3D3F;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#8EBB27;}
.st2{fill:#8EBB27;}
.st3{fill:#F19300;}
</style>
<g>
<g>
<path class="st0" d="M106.1,40L92.3,0h10.9l5.6,20.6l0.5,1.7c0.7,2.5,1.2,4.5,1.6,6.2c0.2-0.8,0.4-1.8,0.7-2.9 c0.3-1.1,0.7-2.6,1.2-4.3L118.7,0h10.8l-13.9,40H106.1z" style="fill: rgb(255, 255, 255);"/>
<path class="st1" d="M386.1,40h-9.8c0-0.5,0.1-1,0.1-1.5l0.2-1.6c-1.7,1.4-3.5,2.4-5.2,3c-1.7,0.6-3.5,1-5.3,1 c-2.8,0-4.9-0.8-6.1-2.3c-1.2-1.6-1.5-3.7-0.7-6.3c0.7-2.4,1.9-4.4,3.6-6c1.7-1.5,4-2.6,6.8-3.2c1.5-0.3,3.5-0.7,5.8-1.1 c3.5-0.5,5.4-1.3,5.7-2.4l0.2-0.7c0.2-0.9,0.1-1.5-0.4-2c-0.5-0.4-1.4-0.7-2.7-0.7c-1.4,0-2.6,0.3-3.5,0.8c-1,0.6-1.7,1.4-2.2,2.5 h-8.9c1.4-3.3,3.5-5.8,6.2-7.5c2.7-1.6,6.2-2.4,10.5-2.4c2.6,0,4.7,0.3,6.4,1c1.6,0.6,2.8,1.6,3.4,2.9c0.4,0.9,0.6,2,0.6,3.3 c-0.1,1.3-0.5,3.3-1.3,6.2l-3.1,11.2c-0.4,1.3-0.5,2.4-0.5,3.2c0,0.8,0.2,1.3,0.7,1.5L386.1,40z M379.4,26.1 c-0.9,0.5-2.3,0.9-4.3,1.3c-1,0.2-1.7,0.3-2.2,0.5c-1.3,0.3-2.2,0.7-2.8,1.2c-0.6,0.5-1.1,1.2-1.3,2c-0.3,1.1-0.2,1.9,0.3,2.5 c0.5,0.6,1.2,1,2.3,1c1.7,0,3.1-0.5,4.4-1.4c1.3-1,2.2-2.2,2.6-3.7L379.4,26.1z"/>
<path class="st1" d="M337.3,40l8.3-29.5h9.3l-1.4,5.2c1.6-2,3.3-3.5,5.1-4.4c1.8-0.9,3.9-1.4,6.3-1.5l-2.7,9.6 c-0.4-0.1-0.8-0.1-1.2-0.1c-0.4,0-0.8,0-1.1,0c-1.5,0-2.8,0.2-3.9,0.7c-1.1,0.4-2.1,1.1-2.9,2.1c-0.5,0.6-1,1.5-1.5,2.6 c-0.5,1.1-1.1,3-1.8,5.6l-2.8,9.9H337.3z"/>
<path class="st1" d="M340.8,10.5L332.5,40h-9.5l1.1-4.1c-1.6,1.6-3.3,2.9-4.9,3.6c-1.7,0.8-3.5,1.2-5.4,1.2 c-3.3,0-5.5-0.8-6.7-2.5c-1.2-1.7-1.3-4.2-0.4-7.4l5.7-20.3h9.7L317.6,27c-0.7,2.4-0.8,4.1-0.5,5c0.4,0.9,1.3,1.4,2.8,1.4 c1.7,0,3.1-0.6,4.1-1.7c1.1-1.1,2-2.9,2.7-5.5l4.4-15.8H340.8z"/>
<path class="st1" d="M290.1,16.3l1.6-5.8h4l2.3-8.3h9.7l-2.3,8.3h5l-1.6,5.8h-5l-3.6,12.8c-0.5,2-0.7,3.3-0.3,3.9 c0.3,0.6,1.2,1,2.6,1l0.7,0l0.5,0l-1.7,6.2c-1.1,0.2-2.1,0.3-3.1,0.5c-1,0.1-2,0.2-2.9,0.2c-3.4,0-5.4-0.8-6.2-2.5 c-0.8-1.6-0.4-5.1,1.1-10.5l3.2-11.4H290.1z"/>
<path class="st1" d="M283.5,40h-9.8c0-0.5,0.1-1,0.1-1.5L274,37c-1.7,1.4-3.5,2.4-5.2,3c-1.7,0.6-3.5,1-5.3,1 c-2.8,0-4.9-0.8-6.1-2.3c-1.2-1.6-1.5-3.7-0.7-6.3c0.7-2.4,1.9-4.4,3.6-6c1.7-1.5,4-2.6,6.8-3.2c1.5-0.3,3.5-0.7,5.8-1.1 c3.5-0.5,5.4-1.3,5.7-2.4l0.2-0.7c0.2-0.9,0.1-1.5-0.4-2c-0.5-0.4-1.4-0.7-2.7-0.7c-1.4,0-2.6,0.3-3.5,0.8c-1,0.6-1.7,1.4-2.2,2.5 H261c1.4-3.3,3.5-5.8,6.2-7.5c2.7-1.6,6.2-2.4,10.5-2.4c2.6,0,4.7,0.3,6.4,1c1.6,0.6,2.8,1.6,3.4,2.9c0.4,0.9,0.6,2,0.6,3.3 c-0.1,1.3-0.5,3.3-1.3,6.2l-3.1,11.2c-0.4,1.3-0.5,2.4-0.5,3.2c0,0.8,0.2,1.3,0.7,1.5L283.5,40z M276.7,26.1 c-0.9,0.5-2.3,0.9-4.3,1.3c-1,0.2-1.7,0.3-2.2,0.5c-1.3,0.3-2.2,0.7-2.8,1.2c-0.6,0.5-1.1,1.2-1.3,2c-0.3,1.1-0.2,1.9,0.3,2.5 c0.5,0.6,1.2,1,2.3,1c1.7,0,3.1-0.5,4.4-1.4c1.3-1,2.2-2.2,2.6-3.7L276.7,26.1z"/>
<path class="st0" d="M219.6,0l-11.2,40h-9.7l1.1-3.9c-1.5,1.6-3.1,2.8-4.8,3.6c-1.6,0.8-3.4,1.2-5.3,1.2c-3.7,0-6.3-1.4-7.8-4.3 c-1.5-2.9-1.6-6.6-0.3-11.2c1.3-4.7,3.5-8.4,6.7-11.4c3.1-2.9,6.5-4.4,10.1-4.4c1.9,0,3.6,0.4,4.8,1.2c1.3,0.8,2.2,1.9,2.8,3.5 L210,0H219.6z M189.8,24.9c-0.7,2.6-0.8,4.7-0.2,6.1c0.6,1.4,1.8,2.1,3.7,2.1c1.8,0,3.4-0.7,4.8-2.1c1.3-1.4,2.4-3.4,3.1-6.1 c0.7-2.5,0.7-4.4,0.1-5.8c-0.6-1.4-1.8-2-3.7-2c-1.7,0-3.3,0.7-4.7,2.1C191.5,20.6,190.4,22.5,189.8,24.9z" style="fill: rgb(255, 255, 255);"/>
<path class="st0" d="M153.6,40l8.3-29.5h9.3l-1.4,5.2c1.6-2,3.3-3.5,5.1-4.4c1.8-0.9,7.9-1.4,10.3-1.5l-2.7,9.6 c-0.4-0.1-0.8-0.1-1.2-0.1c-0.4,0-0.8,0-1.1,0c-1.5,0-6.8,0.2-7.9,0.7c-1.1,0.4-2.1,1.1-2.9,2.1c-0.5,0.6-1,1.5-1.5,2.6 c-0.5,1.1-1.1,3-1.8,5.6l-2.8,9.9H153.6z" style="fill: rgb(255, 255, 255);"/>
<path class="st0" d="M143.5,30.7h9.3c-1.8,3.2-4.2,5.7-7.2,7.5c-3,1.8-6.4,2.7-10.2,2.7c-4.6,0-7.8-1.4-9.7-4.2 c-1.9-2.8-2.2-6.6-0.8-11.4c1.4-4.9,3.8-8.8,7.3-11.6c3.5-2.9,7.5-4.3,12-4.3c4.7,0,8,1.5,9.8,4.3c1.9,2.9,2.1,6.9,0.7,12 l-0.3,1.1l-0.2,0.6h-20c-0.6,2.1-0.6,3.7,0,4.8c0.6,1.1,1.8,1.6,3.5,1.6c1.3,0,2.4-0.3,3.4-0.8C142.1,32.6,142.9,31.8,143.5,30.7z M135.4,22.1l11,0c0.5-1.9,0.4-3.4-0.3-4.4c-0.7-1.1-1.8-1.6-3.5-1.6c-1.6,0-3,0.5-4.3,1.6C137.1,18.6,136.1,20.1,135.4,22.1z" style="fill: rgb(255, 255, 255);"/>
<path class="st2" d="M241.2,40.4l-8.4-24.6l-8.5,24.6h-9.6l12.6-40h10.8L244,21l0.5,1.7c0.7,2.5,1.2,4.5,1.6,6.2l0.7-2.9 c0.3-1.1,0.7-2.6,1.2-4.3l5.9-21.2h10.8l-13.9,40H241.2z"/>
</g>
<g>
<path class="st3" d="M106.1,54.4h4.8l48.9,113.9h-5.9L137,129H79.9l-16.8,39.3H57L106.1,54.4z M135.3,124.2l-26.8-62.7l-26.9,62.7 H135.3z"/>
<path class="st3" d="M178.1,168.3V54.4h5.6v108.7h69.8v5.1H178.1z"/>
<path class="st3" d="M271.1,168.3V54.4h5.6v113.9H271.1z"/>
<path class="st3" d="M300.2,54.4l42,53.6l42-53.6h6.4l-45.4,57.7l44.1,56.1H383l-40.7-52l-40.7,52h-6.7l44.1-56.1l-45.4-57.7 H300.2z"/>
<g>
<path class="st3" d="M5.8,168.3L5.3,163l0.2,2.7L5.3,163c0.4,0,10.4-1.1,18.9-11.8c10.5-13.1,14.1-35.2,10.5-63.9 C31,57.7,35.4,34.8,47.6,19.1C60.3,3,76.6,0.9,77.3,0.8l0.6,5.3c-0.1,0-11.9,1.6-22.4,12.1c-14,14-19.3,37.7-15.5,68.4 c3.8,30.7-0.1,53.6-11.8,68.1C18.3,167.1,6.3,168.2,5.8,168.3z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

158
src/assets/vn.svg Normal file
View File

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="226.229px"
height="31.038px"
viewBox="0 0 226.229 31.038"
enable-background="new 0 0 226.229 31.038"
xml:space="preserve"
id="svg2"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
sodipodi:docname="logo.svg"><metadata
id="metadata61"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs59">
</defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview57"
showgrid="false"
inkscape:zoom="4.8159974"
inkscape:cx="90.91814"
inkscape:cy="16.509992"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:document-rotation="0" />
<g
id="Background">
</g>
<g
id="Guides">
</g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 10.417,30.321 0,0 h 8.233 l 4.26,15.582 0.349,1.276 c 0.521,1.866 0.918,3.431 1.191,4.693 0.15,-0.618 0.335,-1.345 0.555,-2.182 0.219,-0.837 0.528,-1.935 0.925,-3.293 L 19.981,0 h 8.19 l -10.5,30.321 z"
id="path11"
style="fill:#1a1a1a;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A0CE67"
d="m 139.809,19.787 c -0.665,0.357 -1.748,0.686 -3.25,0.988 -0.727,0.137 -1.283,0.254 -1.667,0.35 -0.95,0.247 -1.661,0.563 -2.134,0.947 -0.472,0.384 -0.799,0.899 -0.979,1.544 -0.223,0.796 -0.155,1.438 0.204,1.925 0.359,0.488 0.945,0.731 1.757,0.731 1.252,0 2.375,-0.36 3.369,-1.081 0.994,-0.721 1.653,-1.665 1.98,-2.831 z m 5.106,10.534 h -7.458 c 0.017,-0.356 0.048,-0.726 0.094,-1.11 l 0.159,-1.192 c -1.318,1.026 -2.627,1.786 -3.927,2.279 -1.299,0.493 -2.643,0.739 -4.031,0.739 -2.158,0 -3.7,-0.593 -4.625,-1.779 -0.925,-1.187 -1.106,-2.788 -0.542,-4.804 0.519,-1.851 1.431,-3.356 2.737,-4.515 1.307,-1.159 3.021,-1.972 5.142,-2.438 1.169,-0.247 2.641,-0.515 4.413,-0.803 2.646,-0.412 4.082,-1.016 4.304,-1.812 l 0.151,-0.539 c 0.182,-0.65 0.076,-1.145 -0.317,-1.483 -0.393,-0.339 -1.071,-0.508 -2.033,-0.508 -1.045,0 -1.934,0.214 -2.666,0.643 -0.731,0.428 -1.289,1.058 -1.673,1.887 h -6.748 c 1.065,-2.53 2.64,-4.413 4.723,-5.65 2.083,-1.237 4.724,-1.856 7.923,-1.856 1.991,0 3.602,0.241 4.833,0.722 1.231,0.481 2.095,1.209 2.59,2.185 0.339,0.701 0.483,1.536 0.432,2.504 -0.052,0.969 -0.377,2.525 -0.978,4.669 l -2.375,8.483 c -0.284,1.014 -0.416,1.812 -0.396,2.395 0.02,0.583 0.188,0.962 0.503,1.141 z"
id="path15"
style="fill:#97d700;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A0CE67"
d="m 185.7,30.321 6.27,-22.393 h 7.049 l -1.097,3.918 c 1.213,-1.537 2.502,-2.659 3.867,-3.366 1.365,-0.707 2.951,-1.074 4.758,-1.101 l -2.03,7.25 c -0.304,-0.042 -0.608,-0.072 -0.912,-0.093 -0.303,-0.02 -0.592,-0.03 -0.867,-0.03 -1.126,0 -2.104,0.168 -2.932,0.504 -0.829,0.336 -1.561,0.854 -2.197,1.555 -0.406,0.467 -0.789,1.136 -1.149,2.007 -0.361,0.872 -0.814,2.282 -1.359,4.232 l -2.104,7.516 H 185.7 Z"
id="path19"
style="fill:#97d700;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A0CE67"
d="m 217.631,19.787 c -0.664,0.357 -1.748,0.686 -3.25,0.988 -0.727,0.137 -1.282,0.254 -1.667,0.35 -0.95,0.247 -1.661,0.563 -2.134,0.947 -0.472,0.384 -0.799,0.899 -0.979,1.544 -0.223,0.796 -0.155,1.438 0.205,1.925 0.359,0.488 0.945,0.731 1.757,0.731 1.252,0 2.375,-0.36 3.369,-1.081 0.994,-0.721 1.654,-1.665 1.98,-2.831 z m 5.106,10.534 h -7.458 c 0.017,-0.356 0.048,-0.726 0.094,-1.11 l 0.159,-1.192 c -1.318,1.026 -2.627,1.786 -3.927,2.279 -1.299,0.493 -2.643,0.739 -4.031,0.739 -2.158,0 -3.7,-0.593 -4.625,-1.779 -0.926,-1.187 -1.106,-2.788 -0.542,-4.804 0.519,-1.851 1.431,-3.356 2.737,-4.515 1.306,-1.159 3.02,-1.972 5.142,-2.438 1.169,-0.247 2.641,-0.515 4.413,-0.803 2.647,-0.412 4.082,-1.016 4.304,-1.812 l 0.151,-0.539 c 0.182,-0.65 0.077,-1.145 -0.317,-1.483 -0.393,-0.339 -1.071,-0.508 -2.033,-0.508 -1.045,0 -1.934,0.214 -2.666,0.643 -0.731,0.428 -1.289,1.058 -1.672,1.887 h -6.748 c 1.065,-2.53 2.64,-4.413 4.723,-5.65 2.083,-1.237 4.724,-1.856 7.923,-1.856 1.99,0 3.601,0.241 4.833,0.722 1.232,0.481 2.095,1.209 2.591,2.185 0.339,0.701 0.483,1.536 0.431,2.504 -0.051,0.969 -0.377,2.525 -0.978,4.669 l -2.375,8.483 c -0.284,1.014 -0.416,1.812 -0.396,2.395 0.02,0.583 0.188,0.962 0.503,1.141 z"
id="path23"
style="fill:#97d700;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A0CE67"
d="m 188.386,7.928 -6.269,22.393 h -7.174 l 0.864,-3.085 c -1.227,1.246 -2.476,2.163 -3.746,2.751 -1.27,0.588 -2.625,0.882 -4.067,0.882 -2.471,0 -4.154,-0.634 -5.048,-1.901 -0.895,-1.268 -0.993,-3.149 -0.294,-5.644 l 4.31,-15.396 h 7.338 l -3.508,12.53 c -0.516,1.842 -0.641,3.109 -0.375,3.803 0.266,0.694 0.967,1.041 2.105,1.041 1.275,0 2.323,-0.422 3.142,-1.267 0.819,-0.845 1.497,-2.223 2.031,-4.133 l 3.353,-11.974 z"
id="path27"
style="fill:#97d700;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A0CE67"
d="m 149.937,12.356 1.239,-4.428 h 2.995 l 1.771,-6.326 h 7.338 l -1.771,6.326 h 3.753 l -1.24,4.428 h -3.753 l -2.716,9.702 c -0.416,1.483 -0.498,2.465 -0.247,2.946 0.25,0.48 0.905,0.721 1.964,0.721 l 0.549,-0.011 0.39,-0.031 -1.31,4.678 c -0.811,0.148 -1.596,0.263 -2.354,0.344 -0.758,0.081 -1.48,0.122 -2.167,0.122 -2.543,0 -4.108,-0.621 -4.695,-1.863 -0.587,-1.242 -0.313,-3.887 0.82,-7.936 l 2.428,-8.672 z"
id="path31"
style="fill:#97d700;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#ffffff"
d="m 73.875,18.896 c -0.561,2.004 -0.616,3.537 -0.167,4.601 0.449,1.064 1.375,1.595 2.774,1.595 1.399,0 2.605,-0.524 3.62,-1.574 1.015,-1.05 1.806,-2.59 2.375,-4.622 0.526,-1.879 0.556,-3.334 0.09,-4.363 -0.466,-1.029 -1.393,-1.543 -2.778,-1.543 -1.304,0 -2.487,0.528 -3.551,1.585 -1.064,1.057 -1.852,2.496 -2.363,4.321 z M 96.513,0 88.024,30.321 h -7.337 l 0.824,-2.944 c -1.166,1.22 -2.369,2.121 -3.61,2.703 -1.241,0.582 -2.583,0.874 -4.025,0.874 -2.802,0 -4.772,-1.081 -5.912,-3.243 -1.139,-2.162 -1.218,-4.993 -0.238,-8.493 0.988,-3.528 2.668,-6.404 5.042,-8.627 2.374,-2.224 4.927,-3.336 7.661,-3.336 1.47,0 2.695,0.296 3.676,0.887 0.981,0.591 1.681,1.465 2.099,2.62 L 89.217,0 Z"
id="path35" /><g
id="g37">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 73.875,18.896 c -0.561,2.004 -0.616,3.537 -0.167,4.601 0.449,1.064 1.375,1.595 2.774,1.595 1.399,0 2.605,-0.524 3.62,-1.574 1.015,-1.05 1.806,-2.59 2.375,-4.622 0.526,-1.879 0.556,-3.334 0.09,-4.363 -0.466,-1.029 -1.393,-1.543 -2.778,-1.543 -1.304,0 -2.487,0.528 -3.551,1.585 -1.064,1.057 -1.852,2.496 -2.363,4.321 z M 96.513,0 88.024,30.321 h -7.337 l 0.824,-2.944 c -1.166,1.22 -2.369,2.121 -3.61,2.703 -1.241,0.582 -2.583,0.874 -4.025,0.874 -2.802,0 -4.772,-1.081 -5.912,-3.243 -1.139,-2.162 -1.218,-4.993 -0.238,-8.493 0.988,-3.528 2.668,-6.404 5.042,-8.627 2.374,-2.224 4.927,-3.336 7.661,-3.336 1.47,0 2.695,0.296 3.676,0.887 0.981,0.591 1.681,1.465 2.099,2.62 L 89.217,0 Z"
id="path39"
style="fill:#1a1a1a;fill-opacity:1" />
</g><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 46.488,30.321 52.757,7.928 h 7.049 l -1.098,3.918 C 59.921,10.309 61.21,9.187 62.576,8.48 63.942,7.773 68.591,7.406 70.398,7.379 l -2.03,7.25 c -0.304,-0.042 -0.608,-0.072 -0.911,-0.093 -0.304,-0.02 -0.592,-0.03 -0.867,-0.03 -1.126,0 -5.167,0.168 -5.997,0.504 -0.829,0.336 -1.561,0.854 -2.196,1.555 -0.406,0.467 -0.789,1.136 -1.149,2.007 -0.361,0.872 -0.814,2.282 -1.36,4.232 l -2.104,7.516 h -7.296 z"
id="path43"
style="fill:#1a1a1a;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#ffffff"
d="m 32.673,16.742 8.351,-0.021 c 0.375,-1.436 0.308,-2.558 -0.201,-3.365 -0.509,-0.807 -1.402,-1.211 -2.68,-1.211 -1.209,0 -2.285,0.397 -3.229,1.19 -0.944,0.793 -1.69,1.93 -2.241,3.407 z m 6.144,6.536 h 7.043 c -1.347,2.456 -3.172,4.356 -5.477,5.7 -2.305,1.345 -4.885,2.017 -7.74,2.017 -3.473,0 -5.923,-1.054 -7.351,-3.161 -1.427,-2.107 -1.632,-4.98 -0.613,-8.618 1.038,-3.707 2.875,-6.641 5.512,-8.803 2.637,-2.163 5.678,-3.244 9.123,-3.244 3.555,0 6.04,1.099 7.456,3.298 1.417,2.198 1.582,5.234 0.498,9.109 l -0.239,0.814 -0.167,0.484 H 31.721 c -0.441,1.575 -0.438,2.777 0.01,3.606 0.448,0.829 1.332,1.244 2.65,1.244 0.975,0 1.836,-0.206 2.583,-0.617 0.747,-0.411 1.366,-1.021 1.853,-1.829 z"
id="path47" /><g
id="g49"
style="fill:#1a1a1a;fill-opacity:1">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 32.673,16.742 8.351,-0.021 c 0.375,-1.436 0.308,-2.558 -0.201,-3.365 -0.509,-0.807 -1.402,-1.211 -2.68,-1.211 -1.209,0 -2.285,0.397 -3.229,1.19 -0.944,0.793 -1.69,1.93 -2.241,3.407 z m 6.144,6.536 h 7.043 c -1.347,2.456 -3.172,4.356 -5.477,5.7 -2.305,1.345 -4.885,2.017 -7.74,2.017 -3.473,0 -5.923,-1.054 -7.351,-3.161 -1.427,-2.107 -1.632,-4.98 -0.613,-8.618 1.038,-3.707 2.875,-6.641 5.512,-8.803 2.637,-2.163 5.678,-3.244 9.123,-3.244 3.555,0 6.04,1.099 7.456,3.298 1.417,2.198 1.582,5.234 0.498,9.109 l -0.239,0.814 -0.167,0.484 H 31.721 c -0.441,1.575 -0.438,2.777 0.01,3.606 0.448,0.829 1.332,1.244 2.65,1.244 0.975,0 1.836,-0.206 2.583,-0.617 0.747,-0.411 1.366,-1.021 1.853,-1.829 z"
id="path51"
style="fill:#1a1a1a;fill-opacity:1" />
</g><path
fill="#A0CE67"
d="m 112.881,30.643 -6.404,-18.639 -6.455,18.639 h -7.254 l 9.565,-30.321 h 8.19 l 4.434,15.582 0.35,1.276 c 0.521,1.866 0.917,3.431 1.191,4.693 l 0.555,-2.182 c 0.219,-0.837 0.528,-1.935 0.925,-3.293 l 4.468,-16.076 h 8.19 l -10.501,30.321 z"
id="path55"
style="fill:#97d700;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

161
src/assets/vn_dark.svg Normal file
View File

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="226.229px"
height="31.038px"
viewBox="0 0 226.229 31.038"
enable-background="new 0 0 226.229 31.038"
xml:space="preserve"
id="svg2"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
sodipodi:docname="logo-dark.svg"><metadata
id="metadata61"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs59">
</defs><sodipodi:namedview
pagecolor="#1a1a1a"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview57"
showgrid="false"
inkscape:zoom="3.4054244"
inkscape:cx="112.21891"
inkscape:cy="27.15689"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:document-rotation="0" />
<g
id="Background">
</g>
<g
id="Guides">
</g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 10.417,30.321 0,0 h 8.233 l 4.26,15.582 0.349,1.276 c 0.521,1.866 0.918,3.431 1.191,4.693 0.15,-0.618 0.335,-1.345 0.555,-2.182 0.219,-0.837 0.528,-1.935 0.925,-3.293 L 19.981,0 h 8.19 l -10.5,30.321 z"
id="path11"
style="fill:#ffffff;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A0CE67"
d="m 139.809,19.787 c -0.665,0.357 -1.748,0.686 -3.25,0.988 -0.727,0.137 -1.283,0.254 -1.667,0.35 -0.95,0.247 -1.661,0.563 -2.134,0.947 -0.472,0.384 -0.799,0.899 -0.979,1.544 -0.223,0.796 -0.155,1.438 0.204,1.925 0.359,0.488 0.945,0.731 1.757,0.731 1.252,0 2.375,-0.36 3.369,-1.081 0.994,-0.721 1.653,-1.665 1.98,-2.831 z m 5.106,10.534 h -7.458 c 0.017,-0.356 0.048,-0.726 0.094,-1.11 l 0.159,-1.192 c -1.318,1.026 -2.627,1.786 -3.927,2.279 -1.299,0.493 -2.643,0.739 -4.031,0.739 -2.158,0 -3.7,-0.593 -4.625,-1.779 -0.925,-1.187 -1.106,-2.788 -0.542,-4.804 0.519,-1.851 1.431,-3.356 2.737,-4.515 1.307,-1.159 3.021,-1.972 5.142,-2.438 1.169,-0.247 2.641,-0.515 4.413,-0.803 2.646,-0.412 4.082,-1.016 4.304,-1.812 l 0.151,-0.539 c 0.182,-0.65 0.076,-1.145 -0.317,-1.483 -0.393,-0.339 -1.071,-0.508 -2.033,-0.508 -1.045,0 -1.934,0.214 -2.666,0.643 -0.731,0.428 -1.289,1.058 -1.673,1.887 h -6.748 c 1.065,-2.53 2.64,-4.413 4.723,-5.65 2.083,-1.237 4.724,-1.856 7.923,-1.856 1.991,0 3.602,0.241 4.833,0.722 1.231,0.481 2.095,1.209 2.59,2.185 0.339,0.701 0.483,1.536 0.432,2.504 -0.052,0.969 -0.377,2.525 -0.978,4.669 l -2.375,8.483 c -0.284,1.014 -0.416,1.812 -0.396,2.395 0.02,0.583 0.188,0.962 0.503,1.141 z"
id="path15"
style="fill:#97d700;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A0CE67"
d="m 185.7,30.321 6.27,-22.393 h 7.049 l -1.097,3.918 c 1.213,-1.537 2.502,-2.659 3.867,-3.366 1.365,-0.707 2.951,-1.074 4.758,-1.101 l -2.03,7.25 c -0.304,-0.042 -0.608,-0.072 -0.912,-0.093 -0.303,-0.02 -0.592,-0.03 -0.867,-0.03 -1.126,0 -2.104,0.168 -2.932,0.504 -0.829,0.336 -1.561,0.854 -2.197,1.555 -0.406,0.467 -0.789,1.136 -1.149,2.007 -0.361,0.872 -0.814,2.282 -1.359,4.232 l -2.104,7.516 H 185.7 Z"
id="path19"
style="fill:#97d700;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A0CE67"
d="m 217.631,19.787 c -0.664,0.357 -1.748,0.686 -3.25,0.988 -0.727,0.137 -1.282,0.254 -1.667,0.35 -0.95,0.247 -1.661,0.563 -2.134,0.947 -0.472,0.384 -0.799,0.899 -0.979,1.544 -0.223,0.796 -0.155,1.438 0.205,1.925 0.359,0.488 0.945,0.731 1.757,0.731 1.252,0 2.375,-0.36 3.369,-1.081 0.994,-0.721 1.654,-1.665 1.98,-2.831 z m 5.106,10.534 h -7.458 c 0.017,-0.356 0.048,-0.726 0.094,-1.11 l 0.159,-1.192 c -1.318,1.026 -2.627,1.786 -3.927,2.279 -1.299,0.493 -2.643,0.739 -4.031,0.739 -2.158,0 -3.7,-0.593 -4.625,-1.779 -0.926,-1.187 -1.106,-2.788 -0.542,-4.804 0.519,-1.851 1.431,-3.356 2.737,-4.515 1.306,-1.159 3.02,-1.972 5.142,-2.438 1.169,-0.247 2.641,-0.515 4.413,-0.803 2.647,-0.412 4.082,-1.016 4.304,-1.812 l 0.151,-0.539 c 0.182,-0.65 0.077,-1.145 -0.317,-1.483 -0.393,-0.339 -1.071,-0.508 -2.033,-0.508 -1.045,0 -1.934,0.214 -2.666,0.643 -0.731,0.428 -1.289,1.058 -1.672,1.887 h -6.748 c 1.065,-2.53 2.64,-4.413 4.723,-5.65 2.083,-1.237 4.724,-1.856 7.923,-1.856 1.99,0 3.601,0.241 4.833,0.722 1.232,0.481 2.095,1.209 2.591,2.185 0.339,0.701 0.483,1.536 0.431,2.504 -0.051,0.969 -0.377,2.525 -0.978,4.669 l -2.375,8.483 c -0.284,1.014 -0.416,1.812 -0.396,2.395 0.02,0.583 0.188,0.962 0.503,1.141 z"
id="path23"
style="fill:#97d700;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A0CE67"
d="m 188.386,7.928 -6.269,22.393 h -7.174 l 0.864,-3.085 c -1.227,1.246 -2.476,2.163 -3.746,2.751 -1.27,0.588 -2.625,0.882 -4.067,0.882 -2.471,0 -4.154,-0.634 -5.048,-1.901 -0.895,-1.268 -0.993,-3.149 -0.294,-5.644 l 4.31,-15.396 h 7.338 l -3.508,12.53 c -0.516,1.842 -0.641,3.109 -0.375,3.803 0.266,0.694 0.967,1.041 2.105,1.041 1.275,0 2.323,-0.422 3.142,-1.267 0.819,-0.845 1.497,-2.223 2.031,-4.133 l 3.353,-11.974 z"
id="path27"
style="fill:#97d700;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A0CE67"
d="m 149.937,12.356 1.239,-4.428 h 2.995 l 1.771,-6.326 h 7.338 l -1.771,6.326 h 3.753 l -1.24,4.428 h -3.753 l -2.716,9.702 c -0.416,1.483 -0.498,2.465 -0.247,2.946 0.25,0.48 0.905,0.721 1.964,0.721 l 0.549,-0.011 0.39,-0.031 -1.31,4.678 c -0.811,0.148 -1.596,0.263 -2.354,0.344 -0.758,0.081 -1.48,0.122 -2.167,0.122 -2.543,0 -4.108,-0.621 -4.695,-1.863 -0.587,-1.242 -0.313,-3.887 0.82,-7.936 l 2.428,-8.672 z"
id="path31"
style="fill:#97d700;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#ffffff"
d="m 73.875,18.896 c -0.561,2.004 -0.616,3.537 -0.167,4.601 0.449,1.064 1.375,1.595 2.774,1.595 1.399,0 2.605,-0.524 3.62,-1.574 1.015,-1.05 1.806,-2.59 2.375,-4.622 0.526,-1.879 0.556,-3.334 0.09,-4.363 -0.466,-1.029 -1.393,-1.543 -2.778,-1.543 -1.304,0 -2.487,0.528 -3.551,1.585 -1.064,1.057 -1.852,2.496 -2.363,4.321 z M 96.513,0 88.024,30.321 h -7.337 l 0.824,-2.944 c -1.166,1.22 -2.369,2.121 -3.61,2.703 -1.241,0.582 -2.583,0.874 -4.025,0.874 -2.802,0 -4.772,-1.081 -5.912,-3.243 -1.139,-2.162 -1.218,-4.993 -0.238,-8.493 0.988,-3.528 2.668,-6.404 5.042,-8.627 2.374,-2.224 4.927,-3.336 7.661,-3.336 1.47,0 2.695,0.296 3.676,0.887 0.981,0.591 1.681,1.465 2.099,2.62 L 89.217,0 Z"
id="path35"
style="fill:#ffffff;fill-opacity:1" /><g
id="g37"
style="fill:#ffffff;fill-opacity:1">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 73.875,18.896 c -0.561,2.004 -0.616,3.537 -0.167,4.601 0.449,1.064 1.375,1.595 2.774,1.595 1.399,0 2.605,-0.524 3.62,-1.574 1.015,-1.05 1.806,-2.59 2.375,-4.622 0.526,-1.879 0.556,-3.334 0.09,-4.363 -0.466,-1.029 -1.393,-1.543 -2.778,-1.543 -1.304,0 -2.487,0.528 -3.551,1.585 -1.064,1.057 -1.852,2.496 -2.363,4.321 z M 96.513,0 88.024,30.321 h -7.337 l 0.824,-2.944 c -1.166,1.22 -2.369,2.121 -3.61,2.703 -1.241,0.582 -2.583,0.874 -4.025,0.874 -2.802,0 -4.772,-1.081 -5.912,-3.243 -1.139,-2.162 -1.218,-4.993 -0.238,-8.493 0.988,-3.528 2.668,-6.404 5.042,-8.627 2.374,-2.224 4.927,-3.336 7.661,-3.336 1.47,0 2.695,0.296 3.676,0.887 0.981,0.591 1.681,1.465 2.099,2.62 L 89.217,0 Z"
id="path39"
style="fill:#ffffff;fill-opacity:1" />
</g><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 46.488,30.321 52.757,7.928 h 7.049 l -1.098,3.918 C 59.921,10.309 61.21,9.187 62.576,8.48 63.942,7.773 68.591,7.406 70.398,7.379 l -2.03,7.25 c -0.304,-0.042 -0.608,-0.072 -0.911,-0.093 -0.304,-0.02 -0.592,-0.03 -0.867,-0.03 -1.126,0 -5.167,0.168 -5.997,0.504 -0.829,0.336 -1.561,0.854 -2.196,1.555 -0.406,0.467 -0.789,1.136 -1.149,2.007 -0.361,0.872 -0.814,2.282 -1.36,4.232 l -2.104,7.516 h -7.296 z"
id="path43"
style="fill:#ffffff;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#ffffff"
d="m 32.673,16.742 8.351,-0.021 c 0.375,-1.436 0.308,-2.558 -0.201,-3.365 -0.509,-0.807 -1.402,-1.211 -2.68,-1.211 -1.209,0 -2.285,0.397 -3.229,1.19 -0.944,0.793 -1.69,1.93 -2.241,3.407 z m 6.144,6.536 h 7.043 c -1.347,2.456 -3.172,4.356 -5.477,5.7 -2.305,1.345 -4.885,2.017 -7.74,2.017 -3.473,0 -5.923,-1.054 -7.351,-3.161 -1.427,-2.107 -1.632,-4.98 -0.613,-8.618 1.038,-3.707 2.875,-6.641 5.512,-8.803 2.637,-2.163 5.678,-3.244 9.123,-3.244 3.555,0 6.04,1.099 7.456,3.298 1.417,2.198 1.582,5.234 0.498,9.109 l -0.239,0.814 -0.167,0.484 H 31.721 c -0.441,1.575 -0.438,2.777 0.01,3.606 0.448,0.829 1.332,1.244 2.65,1.244 0.975,0 1.836,-0.206 2.583,-0.617 0.747,-0.411 1.366,-1.021 1.853,-1.829 z"
id="path47"
style="fill:#ffffff;fill-opacity:1" /><g
id="g49"
style="fill:#ffffff;fill-opacity:1">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 32.673,16.742 8.351,-0.021 c 0.375,-1.436 0.308,-2.558 -0.201,-3.365 -0.509,-0.807 -1.402,-1.211 -2.68,-1.211 -1.209,0 -2.285,0.397 -3.229,1.19 -0.944,0.793 -1.69,1.93 -2.241,3.407 z m 6.144,6.536 h 7.043 c -1.347,2.456 -3.172,4.356 -5.477,5.7 -2.305,1.345 -4.885,2.017 -7.74,2.017 -3.473,0 -5.923,-1.054 -7.351,-3.161 -1.427,-2.107 -1.632,-4.98 -0.613,-8.618 1.038,-3.707 2.875,-6.641 5.512,-8.803 2.637,-2.163 5.678,-3.244 9.123,-3.244 3.555,0 6.04,1.099 7.456,3.298 1.417,2.198 1.582,5.234 0.498,9.109 l -0.239,0.814 -0.167,0.484 H 31.721 c -0.441,1.575 -0.438,2.777 0.01,3.606 0.448,0.829 1.332,1.244 2.65,1.244 0.975,0 1.836,-0.206 2.583,-0.617 0.747,-0.411 1.366,-1.021 1.853,-1.829 z"
id="path51"
style="fill:#ffffff;fill-opacity:1" />
</g><path
fill="#A0CE67"
d="m 112.881,30.643 -6.404,-18.639 -6.455,18.639 h -7.254 l 9.565,-30.321 h 8.19 l 4.434,15.582 0.35,1.276 c 0.521,1.866 0.917,3.431 1.191,4.693 l 0.555,-2.182 c 0.219,-0.837 0.528,-1.935 0.925,-3.293 l 4.468,-16.076 h 8.19 l -10.501,30.321 z"
id="path55"
style="fill:#97d700;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

72
src/assets/vn_icon.svg Normal file
View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="187"
height="187"
viewBox="0 0 187 187"
enable-background="new 0 0 595 842"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="logo.svg"><metadata
id="metadata21"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs19" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview17"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="4.6900433"
inkscape:cx="83.335292"
inkscape:cy="99.203526"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" /><g
id="Background"
transform="translate(-211.7456,-282.24899)" /><g
id="Guides"
transform="translate(-211.7456,-282.24899)" /><g
id="g7"
transform="matrix(1.0030446,0,0,1.0030446,-212.39029,-288.74375)"
style="fill:#8ed300;fill-opacity:1"><path
style="fill:#8ed300;fill-opacity:1"
inkscape:connector-curvature="0"
id="path9"
d="m 339.611,301.147 c 1.324,-0.375 2.663,-0.656 4.017,-0.838 l 54.55,-7.391 -1.039,30.924 c -0.862,25.488 -15.732,48.394 -34.025,53.571 -1.319,0.374 -2.654,0.654 -4.003,0.837 l -54.551,7.379 1.038,-30.923 c 0.864,-25.481 15.725,-48.38 34.013,-53.559 z" /><path
style="fill:#8ed300;fill-opacity:1"
inkscape:connector-curvature="0"
id="path11"
d="m 304.353,399.358 27.265,-3.692 c 10.052,-1.368 17.809,8.612 17.351,22.267 l -0.523,15.469 -27.265,3.692 c -10.041,1.366 -17.811,-8.612 -17.354,-22.279 l 0.526,-15.457 z" /><path
style="fill:#8ed300;fill-opacity:1"
inkscape:connector-curvature="0"
id="path13"
d="m 212.72,326.05 49.089,-6.647 c 18.083,-2.444 32.068,15.502 31.238,40.103 l -0.941,27.826 -49.09,6.647 c -18.081,2.456 -32.057,-15.506 -31.236,-40.09 l 0.94,-27.839 z" /><path
style="fill:#8ed300;fill-opacity:1"
inkscape:connector-curvature="0"
id="path15"
d="m 248.296,407.657 c 0.966,-0.272 1.943,-0.478 2.93,-0.611 l 39.76,-5.383 -0.75,22.539 c -0.624,18.584 -11.458,35.275 -24.792,39.049 -0.966,0.272 -1.943,0.48 -2.931,0.613 l -39.772,5.385 0.761,-22.542 c 0.625,-18.573 11.461,-35.274 24.794,-39.05 z" /></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -46,7 +46,7 @@ const onResponseError = (error) => {
message = responseError.message; message = responseError.message;
} }
switch (response.status) { switch (response?.status) {
case 500: case 500:
message = 'errors.statusInternalServerError'; message = 'errors.statusInternalServerError';
break; break;
@ -58,12 +58,13 @@ const onResponseError = (error) => {
break; break;
} }
if (session.isLoggedIn() && response.status === 401) { if (session.isLoggedIn() && response?.status === 401) {
session.destroy(); session.destroy();
Router.push({ path: '/login' }); const hash = window.location.hash;
} else if(!session.isLoggedIn()) const url = hash.slice(1);
{ Router.push({ path: url });
message = 'login.loginError'; } else if (!session.isLoggedIn()) {
return Promise.reject(error);
} }
Notify.create({ Notify.create({
@ -77,7 +78,4 @@ const onResponseError = (error) => {
axios.interceptors.request.use(onRequest, onRequestError); axios.interceptors.request.use(onRequest, onRequestError);
axios.interceptors.response.use(onResponse, onResponseError); axios.interceptors.response.use(onResponse, onResponseError);
export { export { onRequest, onResponseError };
onRequest,
onResponseError
}

View File

@ -0,0 +1,325 @@
<script setup>
import axios from 'axios';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useValidator } from 'src/composables/useValidator';
import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import SkeletonTable from 'components/ui/SkeletonTable.vue';
import { tMobile } from 'src/composables/tMobile';
const quasar = useQuasar();
const stateStore = useStateStore();
const { t } = useI18n();
const { validate } = useValidator();
const $props = defineProps({
model: {
type: String,
default: '',
},
url: {
type: String,
default: '',
},
saveUrl: {
type: String,
default: null,
},
primaryKey: {
type: String,
default: 'id',
},
dataRequired: {
type: Object,
default: () => {},
},
defaultSave: {
type: Boolean,
default: true,
},
defaultReset: {
type: Boolean,
default: true,
},
defaultRemove: {
type: Boolean,
default: true,
},
selected: {
type: Object,
default: null,
},
saveFn: {
type: Function,
default: null,
},
});
const isLoading = ref(false);
const hasChanges = ref(false);
const originalData = ref();
const vnPaginateRef = ref();
const formData = ref();
const saveButtonRef = ref(null);
const formUrl = computed(() => $props.url);
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
defineExpose({
reload,
insert,
remove,
onSubmit,
reset,
hasChanges,
saveChanges,
});
async function fetch(data) {
if (data && Array.isArray(data)) {
let $index = 0;
data.map((d) => (d.$index = $index++));
}
originalData.value = data && JSON.parse(JSON.stringify(data));
formData.value = data && JSON.parse(JSON.stringify(data));
watch(formData, () => (hasChanges.value = true), { deep: true });
emit('onFetch', data);
return data;
}
async function reset() {
await fetch(originalData.value);
hasChanges.value = false;
}
// eslint-disable-next-line vue/no-dupe-keys
function filter(value, update, filterOptions) {
update(
() => {
const { options, filterFn, field } = filterOptions;
options.value = filterFn(options, value, field);
},
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
);
}
async function onSubmit() {
if (!hasChanges.value) {
return quasar.notify({
type: 'negative',
message: t('globals.noChanges'),
});
}
isLoading.value = true;
await saveChanges();
}
async function saveChanges(data) {
if ($props.saveFn) return $props.saveFn(data, getChanges);
const changes = data || getChanges();
try {
await axios.post($props.saveUrl || $props.url + '/crud', changes);
} catch (e) {
return (isLoading.value = false);
}
originalData.value = JSON.parse(JSON.stringify(formData.value));
if (changes.creates?.length) await vnPaginateRef.value.fetch();
hasChanges.value = false;
isLoading.value = false;
emit('saveChanges', data);
}
async function insert() {
const $index = formData.value.length
? formData.value[formData.value.length - 1].$index + 1
: 0;
formData.value.push(Object.assign({ $index }, $props.dataRequired));
hasChanges.value = true;
}
async function remove(data) {
if (!data.length)
return quasar.notify({
type: 'warning',
message: t('globals.noChanges'),
});
const pk = $props.primaryKey;
let ids = data.map((d) => d[pk]).filter(Boolean);
let preRemove = data.map((d) => (d[pk] ? null : d.$index)).filter(Boolean);
let newData = formData.value;
if (preRemove.length) {
newData = newData.filter(
(form) => !preRemove.some((index) => index == form.$index)
);
const changes = getChanges();
if (!changes.creates?.length && !changes.updates?.length)
hasChanges.value = false;
fetch(newData);
}
if (ids.length) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('confirmDeletion'),
message: t('confirmDeletionMessage'),
newData,
ids,
},
})
.onOk(async () => {
await saveChanges({ deletes: ids });
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
fetch(newData);
});
}
emit('update:selected', []);
}
function getChanges() {
const updates = [];
const creates = [];
const pk = $props.primaryKey;
for (const [i, row] of formData.value.entries()) {
if (!row[pk]) {
creates.push(row);
} else if (originalData.value) {
const data = getDifferences(originalData.value[i], row);
if (!isEmpty(data)) {
updates.push({
data,
where: { [pk]: row[pk] },
});
}
}
}
const changes = { updates, creates };
for (let prop in changes) {
if (changes[prop].length === 0) changes[prop] = undefined;
}
return changes;
}
function getDifferences(obj1, obj2) {
let diff = {};
delete obj1.$index;
delete obj2.$index;
for (let key in obj1) {
if (obj2[key] && obj1[key] !== obj2[key]) {
diff[key] = obj2[key];
}
}
for (let key in obj2) {
if (obj1[key] === undefined || obj1[key] !== obj2[key]) {
diff[key] = obj2[key];
}
}
return diff;
}
function isEmpty(obj) {
if (obj == null) return true;
if (obj === undefined) return true;
if (Object.keys(obj).length === 0) return true;
if (obj.length > 0) return false;
}
async function reload() {
vnPaginateRef.value.fetch();
}
watch(formUrl, async () => {
originalData.value = null;
reset();
});
</script>
<template>
<VnPaginate
:url="url"
v-bind="$attrs"
@on-fetch="fetch"
:skeleton="false"
ref="vnPaginateRef"
>
<template #body v-if="formData">
<slot
name="body"
:rows="formData"
:validate="validate"
:filter="filter"
></slot>
</template>
</VnPaginate>
<SkeletonTable v-if="!formData" />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" />
<QBtn
:label="tMobile('globals.remove')"
color="primary"
icon="delete"
flat
@click="remove(selected)"
:disable="!selected?.length"
:title="t('globals.remove')"
v-if="$props.defaultRemove"
/>
<QBtn
:label="tMobile('globals.reset')"
color="primary"
icon="restart_alt"
flat
@click="reset"
:disable="!hasChanges"
:title="t('globals.reset')"
v-if="$props.defaultReset"
/>
<QBtn
:label="tMobile('globals.save')"
ref="saveButtonRef"
color="primary"
icon="save"
@click="onSubmit"
:disable="!hasChanges"
:title="t('globals.save')"
v-if="$props.defaultSave"
/>
<slot name="moreAfterActions" />
</QBtnGroup>
</Teleport>
<QInnerLoading
:showing="isLoading"
:label="t && t('globals.pleaseWait')"
color="primary"
/>
</template>
<i18n>
{
"en": {
"confirmDeletion": "Confirm deletion",
"confirmDeletionMessage": "Are you sure you want to delete this?"
},
"es": {
"confirmDeletion": "Confirmar eliminación",
"confirmDeletionMessage": "Seguro que quieres eliminar?"
}
}
</i18n>

View File

@ -4,12 +4,14 @@ import { onMounted, onUnmounted, computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import SkeletonForm from 'components/ui/SkeletonForm.vue'; import SkeletonForm from 'components/ui/SkeletonForm.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n();
const state = useState(); const state = useState();
const stateStore = useStateStore();
const { t } = useI18n();
const { validate } = useValidator(); const { validate } = useValidator();
const $props = defineProps({ const $props = defineProps({
@ -25,6 +27,14 @@ const $props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
urlUpdate: {
type: String,
default: null,
},
defaultActions: {
type: Boolean,
default: true,
},
}); });
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
@ -41,17 +51,21 @@ onUnmounted(() => {
const isLoading = ref(false); const isLoading = ref(false);
const hasChanges = ref(false); const hasChanges = ref(false);
const formData = computed(() => state.get($props.model));
const originalData = ref(); const originalData = ref();
const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
function tMobile(...args) {
if (!quasar.platform.is.mobile) return t(...args);
}
async function fetch() { async function fetch() {
const { data } = await axios.get($props.url, { const { data } = await axios.get($props.url, {
params: { filter: $props.filter }, params: { filter: $props.filter },
}); });
state.set($props.model, data); state.set($props.model, data);
originalData.value = Object.assign({}, data); originalData.value = data && JSON.parse(JSON.stringify(data));
watch(formData.value, () => (hasChanges.value = true)); watch(formData.value, () => (hasChanges.value = true));
@ -66,15 +80,20 @@ async function save() {
}); });
} }
isLoading.value = true; isLoading.value = true;
await axios.patch($props.url, formData.value); await axios.patch($props.urlUpdate || $props.url, formData.value);
originalData.value = formData.value; originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false; hasChanges.value = false;
isLoading.value = false; isLoading.value = false;
} }
function reset() { function reset() {
state.set($props.model, originalData.value); state.set($props.model, originalData.value);
originalData.value = JSON.parse(JSON.stringify(originalData.value));
watch(formData.value, () => (hasChanges.value = true));
emit('onFetch', state.get($props.model));
hasChanges.value = false; hasChanges.value = false;
} }
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
@ -103,22 +122,48 @@ watch(formUrl, async () => {
<QIcon name="warning" size="md" class="q-mr-md" /> <QIcon name="warning" size="md" class="q-mr-md" />
<span>{{ t('globals.changesToSave') }}</span> <span>{{ t('globals.changesToSave') }}</span>
</QBanner> </QBanner>
<QForm v-if="formData" @submit="save" @reset="reset" class="q-pa-md"> <div class="column items-center">
<slot name="form" :data="formData" :validate="validate" :filter="filter"></slot> <QForm
<div class="q-mt-lg"> v-if="formData"
<slot name="actions"> @submit="save"
<QBtn :label="t('globals.save')" type="submit" color="primary" /> @reset="reset"
<QBtn class="q-pa-md"
:label="t('globals.reset')" id="formModel"
type="reset" >
class="q-ml-sm" <QCard>
color="primary" <slot
flat name="form"
:disable="!hasChanges" :data="formData"
:validate="validate"
:filter="filter"
/> />
</slot> </QCard>
</QForm>
</div>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<div v-if="$props.defaultActions">
<QBtnGroup push class="q-gutter-x-sm">
<slot name="moreActions" />
<QBtn
:label="tMobile('globals.reset')"
color="primary"
icon="restart_alt"
flat
@click="reset"
:disable="!hasChanges"
:title="t('globals.reset')"
/>
<QBtn
:label="tMobile('globals.save')"
color="primary"
icon="save"
@click="save"
:disable="!hasChanges"
:title="t('globals.save')"
/>
</QBtnGroup>
</div> </div>
</QForm> </Teleport>
<SkeletonForm v-if="!formData" /> <SkeletonForm v-if="!formData" />
<QInnerLoading <QInnerLoading
:showing="isLoading" :showing="isLoading"
@ -126,3 +171,12 @@ watch(formUrl, async () => {
color="primary" color="primary"
/> />
</template> </template>
<style lang="scss" scoped>
#formModel {
max-width: 800px;
width: 100%;
}
.q-card {
padding: 32px;
}
</style>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QSeparator, useQuasar } from 'quasar'; import { QSeparator, useQuasar } from 'quasar';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
@ -31,7 +31,7 @@ function findMatches(search, item) {
const matches = []; const matches = [];
function findRoute(search, item) { function findRoute(search, item) {
for (const child of item.children) { for (const child of item.children) {
if (search.indexOf(child.name) > -1) { if (search?.indexOf(child.name) > -1) {
matches.push(child); matches.push(child);
} else if (child.children) { } else if (child.children) {
findRoute(search, child); findRoute(search, child);
@ -55,10 +55,6 @@ function addChildren(module, route, parent) {
} }
} }
const pinnedItems = computed(() => {
return items.value.filter((item) => item.isPinned);
});
const items = ref([]); const items = ref([]);
function getRoutes() { function getRoutes() {
if (props.source === 'main') { if (props.source === 'main') {
@ -115,48 +111,65 @@ async function togglePinned(item, event) {
</script> </script>
<template> <template>
<QList padding> <QList padding class="column-max-width">
<template v-if="$props.source === 'main'"> <template v-if="$props.source === 'main'">
<QItemLabel header> <template v-if="$route?.matched[1]?.name === 'Dashboard'">
{{ t('globals.pinnedModules') }} <QItem class="header">
</QItemLabel> <QItemSection avatar>
<template v-for="item in pinnedItems" :key="item.name"> <QIcon name="view_module" />
<template v-if="item.children"> </QItemSection>
<LeftMenuItemGroup :item="item" group="pinnedModules" class="pinned"> <QItemSection> {{ t('globals.modules') }}</QItemSection>
<template #side> </QItem>
<QBtn <QSeparator />
v-if="item.isPinned === true"
@click="togglePinned(item, $event)"
icon="remove_circle"
size="xs"
flat
round
>
<QTooltip>{{
t('components.leftMenu.removeFromPinned')
}}</QTooltip>
</QBtn>
<QBtn
v-if="item.isPinned === false"
@click="togglePinned(item, $event)"
icon="push_pin"
size="xs"
flat
round
>
<QTooltip>{{
t('components.leftMenu.addToPinned')
}}</QTooltip>
</QBtn>
</template>
</LeftMenuItemGroup>
</template>
<LeftMenuItem v-if="!item.children" :item="item" />
</template>
<QSeparator />
<QExpansionItem :label="t('moduleIndex.allModules')">
<template v-for="item in items" :key="item.name"> <template v-for="item in items" :key="item.name">
<template v-if="item.children">
<LeftMenuItem :item="item" group="modules">
<template #side>
<QBtn
v-if="item.isPinned === true"
@click="togglePinned(item, $event)"
icon="remove_circle"
size="xs"
flat
round
>
<QTooltip>
{{ t('components.leftMenu.removeFromPinned') }}
</QTooltip>
</QBtn>
<QBtn
v-if="item.isPinned === false"
@click="togglePinned(item, $event)"
icon="push_pin"
size="xs"
flat
round
>
<QTooltip>
{{ t('components.leftMenu.addToPinned') }}
</QTooltip>
</QBtn>
</template>
</LeftMenuItem>
</template>
</template>
</template>
<template v-for="item in items" :key="item.name">
<template v-if="item.name === $route?.matched[1]?.name">
<QItem class="header">
<QItemSection avatar v-if="item.icon">
<QIcon :name="item.icon" />
</QItemSection>
<QItemSection avatar v-if="!item.icon">
<QIcon name="disabled_by_default" />
</QItemSection>
<QItemSection>{{ t(item.title) }}</QItemSection>
<QItemSection side>
<slot name="side" :item="item" />
</QItemSection>
</QItem>
<QSeparator />
<template v-if="item.children"> <template v-if="item.children">
<LeftMenuItemGroup :item="item" group="modules"> <LeftMenuItemGroup :item="item" group="modules">
<template #side> <template #side>
@ -188,8 +201,7 @@ async function togglePinned(item, event) {
</LeftMenuItemGroup> </LeftMenuItemGroup>
</template> </template>
</template> </template>
</QExpansionItem> </template>
<QSeparator />
</template> </template>
<template v-if="$props.source === 'card'"> <template v-if="$props.source === 'card'">
<template v-for="item in items" :key="item.name"> <template v-for="item in items" :key="item.name">
@ -199,7 +211,7 @@ async function togglePinned(item, event) {
</QList> </QList>
</template> </template>
<style> <style scoped>
.pinned .q-btn { .pinned .q-btn {
visibility: hidden; visibility: hidden;
} }
@ -207,4 +219,10 @@ async function togglePinned(item, event) {
.pinned:hover .q-btn { .pinned:hover .q-btn {
visibility: visible; visibility: visible;
} }
.column-max-width {
max-width: 256px;
}
.header {
color: #999999;
}
</style> </style>

View File

@ -22,5 +22,8 @@ const item = computed(() => props.item); // eslint-disable-line vue/no-dupe-keys
<QIcon name="disabled_by_default" /> <QIcon name="disabled_by_default" />
</QItemSection> </QItemSection>
<QItemSection>{{ t(item.title) }}</QItemSection> <QItemSection>{{ t(item.title) }}</QItemSection>
<QItemSection side>
<slot name="side" :item="item" />
</QItemSection>
</QItem> </QItem>
</template> </template>

View File

@ -1,12 +1,7 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import LeftMenuItem from './LeftMenuItem.vue'; import LeftMenuItem from './LeftMenuItem.vue';
const route = useRoute();
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
item: { item: {
type: Object, type: Object,
@ -19,33 +14,9 @@ const props = defineProps({
}); });
const item = computed(() => props.item); // eslint-disable-line vue/no-dupe-keys const item = computed(() => props.item); // eslint-disable-line vue/no-dupe-keys
const isOpened = computed(() => {
const { matched } = route;
const { name } = item.value;
return matched.some((item) => item.name === name);
});
</script> </script>
<template> <template>
<QExpansionItem <template v-for="section in item.children" :key="section.name">
:group="props.group" <LeftMenuItem :item="section" />
active-class="text-primary" </template>
:label="item.title"
:to="{ name: item.name }"
expand-separator
:default-opened="isOpened"
>
<template #header>
<QItemSection avatar>
<QIcon :name="item.icon"></QIcon>
</QItemSection>
<QItemSection>{{ t(item.title) }}</QItemSection>
<QItemSection side>
<slot name="side" :item="item" />
</QItemSection>
</template>
<template v-for="section in item.children" :key="section.name">
<LeftMenuItem :item="section" />
</template>
</QExpansionItem>
</template> </template>

View File

@ -1,43 +1,44 @@
<script setup> <script setup>
import { onMounted } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import UserPanel from 'components/UserPanel.vue';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useQuasar } from 'quasar';
import PinnedModules from './PinnedModules.vue'; import PinnedModules from './PinnedModules.vue';
import UserPanel from 'components/UserPanel.vue';
import VnBreadcrumbs from './common/VnBreadcrumbs.vue';
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const session = useSession();
const stateStore = useStateStore(); const stateStore = useStateStore();
const quasar = useQuasar();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const token = session.getToken(); const token = session.getToken();
const appName = 'Lilium'; const appName = 'Lilium';
onMounted(() => stateStore.setMounted()); onMounted(() => stateStore.setMounted());
const pinnedModulesRef = ref();
</script> </script>
<template> <template>
<QHeader class="bg-dark" color="white" elevated> <QHeader class="bg-dark" color="white" elevated>
<QToolbar class="q-py-sm q-px-md"> <QToolbar
<QBtn class="q-py-sm q-px-md"
@click="stateStore.toggleLeftDrawer()" :class="{ 'q-gutter-x-sm': !quasar.platform.is.mobile }"
icon="menu" >
class="q-mr-sm" <QBtn @click="stateStore.toggleLeftDrawer()" icon="menu" round dense flat>
round
dense
flat
>
<QTooltip bottom anchor="bottom right"> <QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }} {{ t('globals.collapseMenu') }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<RouterLink to="/"> <RouterLink to="/">
<QBtn class="q-ml-xs" color="primary" flat round> <QBtn color="primary" flat round v-if="!quasar.platform.is.mobile">
<QAvatar square size="md"> <QAvatar square size="md">
<QImg <QImg
src="~/assets/logo_icon.svg" src="~/assets/salix_icon.svg"
:alt="appName" :alt="appName"
spinner-color="primary" spinner-color="primary"
/> />
@ -47,22 +48,43 @@ onMounted(() => stateStore.setMounted());
</QTooltip> </QTooltip>
</QBtn> </QBtn>
</RouterLink> </RouterLink>
<QToolbarTitle shrink class="text-weight-bold" v-if="$q.screen.gt.sm"> <VnBreadcrumbs v-if="$q.screen.gt.sm" />
{{ appName }}
<QBadge label="Beta" align="top" />
</QToolbarTitle>
<QSpace /> <QSpace />
<div id="searchbar"></div> <div id="searchbar" class="searchbar"></div>
<QSpace /> <QSpace />
<div class="q-pl-sm q-gutter-sm row items-center no-wrap"> <div class="q-pl-sm q-gutter-sm row items-center no-wrap">
<div id="actions-prepend"></div> <div id="actions-prepend"></div>
<QBtn id="pinnedModules" icon="apps" flat dense rounded> <QBtn
flat
v-if="!quasar.platform.is.mobile"
@click="pinnedModulesRef.redirect($route.params.id)"
icon="more_up"
>
<QTooltip>
{{ t('Go to Salix') }}
</QTooltip>
</QBtn>
<QBtn
:class="{ 'q-pa-none': quasar.platform.is.mobile }"
id="pinnedModules"
icon="apps"
flat
dense
rounded
>
<QTooltip bottom> <QTooltip bottom>
{{ t('globals.pinnedModules') }} {{ t('globals.pinnedModules') }}
</QTooltip> </QTooltip>
<PinnedModules /> <PinnedModules ref="pinnedModulesRef" />
</QBtn> </QBtn>
<QBtn rounded dense flat no-wrap id="user"> <QBtn
:class="{ 'q-pa-none': quasar.platform.is.mobile }"
rounded
dense
flat
no-wrap
id="user"
>
<QAvatar size="lg"> <QAvatar size="lg">
<QImg <QImg
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`" :src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
@ -78,5 +100,18 @@ onMounted(() => stateStore.setMounted());
<div id="actions-append"></div> <div id="actions-append"></div>
</div> </div>
</QToolbar> </QToolbar>
<VnBreadcrumbs v-if="$q.screen.lt.md" class="q-ml-md" />
</QHeader> </QHeader>
</template> </template>
<style lang="scss" scoped>
.searchbar {
width: max-content;
}
</style>
<i18n>
en:
Go to Salix: Go to Salix
es:
Go to Salix: Ir a Salix
</i18n>

View File

@ -2,69 +2,85 @@
import { onMounted, computed } from 'vue'; import { onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useNavigationStore } from 'src/stores/useNavigationStore'; import { useNavigationStore } from 'src/stores/useNavigationStore';
import { getUrl } from 'src/composables/getUrl';
import { useRoute } from 'vue-router';
const navigation = useNavigationStore(); const navigation = useNavigationStore();
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute();
onMounted(() => { onMounted(() => {
navigation.fetchPinned(); navigation.fetchPinned();
}); });
defineExpose({
redirect,
});
const pinnedModules = computed(() => navigation.getPinnedModules()); const pinnedModules = computed(() => navigation.getPinnedModules());
async function redirect() {
if (route.path == '/dashboard') return (window.location.href = await getUrl(''));
let section = route.path.substring(1);
section = section.substring(0, section.indexOf('/'));
if (route?.params?.id)
return (window.location.href = await getUrl(
`${section}/${route.params.id}/summary`
));
return (window.location.href = await getUrl(section + '/index'));
}
</script> </script>
<template> <template>
<QMenu <QMenu anchor="bottom left" max-width="300px" max-height="400px">
anchor="bottom left" <div v-if="pinnedModules.length >= 0" class="row justify-around q-pa-md">
class="row q-pa-md q-col-gutter-lg" <QBtn flat stack size="lg" icon="more_up" @click="redirect($route.params.id)">
max-width="350px" <div class="button-text">Salix</div>
max-height="400px" </QBtn>
> <QBtn flat stack size="lg" icon="home" to="/">
<template v-if="pinnedModules.length"> <div class="button-text">{{ t('Home') }}</div>
<div </QBtn>
v-for="item of pinnedModules"
:key="item.title" <div class="row col-12 justify-around q-mt-md">
class="row no-wrap q-pa-xs flex-item"
>
<QBtn <QBtn
align="evenly"
padding="16px"
flat flat
stack stack
size="lg" size="lg"
:icon="item.icon" :icon="item.icon"
color="primary" color="primary"
class="col-4 button" class="col-5"
:to="{ name: item.name }" :to="{ name: item.name }"
v-for="item of pinnedModules"
:key="item.title"
> >
<div class="text-center text-primary button-text"> <div class="text-center text-primary button-text">
{{ t(item.title) }} {{ t(item.title) }}
</div> </div>
</QBtn> </QBtn>
</div> </div>
</template> </div>
<template v-else> <div v-else>
<div <div
class="row no-wrap q-pa-xs flex-item text-center text-grey-5" class="row no-wrap q-pa-xs flex-item text-center text-grey-5"
style="min-width: 200px" style="min-width: 200px"
> >
{{ t('globals.noPinnedModules') }} {{ t('globals.noPinnedModules') }}
</div> </div>
</template> </div>
</QMenu> </QMenu>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.flex-item {
width: 100px;
}
.button {
width: 100%;
line-height: normal;
align-items: center;
}
.button-text { .button-text {
font-size: 10px; font-size: 10px;
margin-top: 5px; margin-top: 5px;
} }
</style> </style>
<i18n>
en:
Home: Home
es:
Home: Inicio
</i18n>

View File

@ -40,7 +40,7 @@ async function confirm() {
} }
</script> </script>
<template> <template>
<QDialog ref="dialogRef" persistent> <QDialog ref="dialogRef">
<QCard class="q-pa-sm"> <QCard class="q-pa-sm">
<QCardSection class="row items-center q-pb-none"> <QCardSection class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{ t('Send email notification') }}</span> <span class="text-h6 text-grey">{{ t('Send email notification') }}</span>

View File

@ -0,0 +1,82 @@
<script setup>
import { useRouter } from 'vue-router';
import { ref, watchEffect } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useCamelCase } from 'src/composables/useCamelCase';
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
let matched = ref([]);
let breadcrumbs = ref([]);
let root = ref(null);
watchEffect(() => {
matched.value = router.currentRoute.value.matched.filter(
(matched) => Object.keys(matched.meta).length
);
breadcrumbs.value.length = 0;
if (matched.value[0].name != 'Dashboard') {
root.value = useCamelCase(matched.value[0].path.substring(1).toLowerCase());
for (let index in matched.value)
breadcrumbs.value.push(getBreadcrumb(matched.value[index]));
breadcrumbs.value[breadcrumbs.value.length - 1].path = undefined;
}
});
function getBreadcrumb(param) {
const breadcrumb = {
icon: param.meta.icon,
path: param.path,
root: root.value,
};
if (quasar.screen.gt.sm) {
breadcrumb.name = param.name;
breadcrumb.title = useCamelCase(param.meta.title);
}
return breadcrumb;
}
</script>
<template>
<QBreadcrumbs v-if="breadcrumbs.length && $q.screen.gt.sm" class="q-pa-xs">
<QBreadcrumbsEl
v-for="(breadcrumb, index) of breadcrumbs"
:key="index"
:icon="breadcrumb.icon"
:label="t(`${breadcrumb.root}.pageTitles.${breadcrumb.title}`)"
:to="breadcrumb.path"
/>
</QBreadcrumbs>
<QBreadcrumbs v-else class="q-pa-xs">
<QBreadcrumbsEl
v-for="(breadcrumb, index) of breadcrumbs"
:key="index"
:icon="breadcrumb.icon"
:to="breadcrumb.path"
/>
</QBreadcrumbs>
</template>
<style lang="scss">
.q-breadcrumbs {
&__el,
> div {
flex-wrap: nowrap;
}
}
@media (max-width: $breakpoint-md) {
.q-breadcrumbs {
overflow: hidden;
&__el:not(:first-child):not(:last-child) {
display: none !important;
}
}
}
</style>

View File

@ -0,0 +1,98 @@
<script setup>
import { ref, toRefs, watch, computed } from 'vue';
const emit = defineEmits(['update:modelValue', 'update:options']);
const $props = defineProps({
modelValue: {
type: [String, Number, Object],
default: null,
},
options: {
type: Array,
default: () => [],
},
optionLabel: {
type: String,
default: '',
},
isClearable: {
type: Boolean,
default: true,
},
});
const { optionLabel, options } = toRefs($props);
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const vnSelectRef = ref(null);
function setOptions(data) {
myOptions.value = JSON.parse(JSON.stringify(data));
myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
}
setOptions(options.value);
const filter = (val, options) => {
const search = val.toLowerCase();
if (val === '') return options;
return options.filter((row) => {
const id = row.id;
const name = row[$props.optionLabel].toLowerCase();
const idMatches = id == search;
const nameMatches = name.indexOf(search) > -1;
return idMatches || nameMatches;
});
};
const filterHandler = (val, update) => {
update(
() => {
myOptions.value = filter(val, myOptionsOriginal.value);
},
(ref) => {
if (val !== '' && ref.options.length > 0) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
}
);
};
watch(options, (newValue) => {
setOptions(newValue);
});
const value = computed({
get() {
return $props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
</script>
<template>
<QSelect
v-model="value"
:options="myOptions"
:option-label="optionLabel"
v-bind="$attrs"
emit-value
map-options
use-input
@filter="filterHandler"
fill-input
ref="vnSelectRef"
>
<template v-if="isClearable" #append>
<QIcon name="close" @click.stop="value = null" class="cursor-pointer" />
</template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData">
<slot :name="slotName" v-bind="slotData" />
</template>
</QSelect>
</template>

View File

@ -83,7 +83,7 @@ async function send() {
</script> </script>
<template> <template>
<QDialog ref="dialogRef" persistent> <QDialog ref="dialogRef">
<QCard class="q-pa-sm"> <QCard class="q-pa-sm">
<QCardSection class="row items-center q-pb-none"> <QCardSection class="row items-center q-pb-none">
<span class="text-h6 text-grey"> <span class="text-h6 text-grey">

View File

@ -1,10 +1,11 @@
<script setup> <script setup>
import { onMounted, useSlots, ref, watch } from 'vue'; import { onMounted, useSlots, ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue'; import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData';
const props = defineProps({ const $props = defineProps({
url: { url: {
type: String, type: String,
default: '', default: '',
@ -17,31 +18,45 @@ const props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
title: {
type: String,
default: '',
},
subtitle: {
type: Number,
default: 0,
},
dataKey: {
type: String,
default: '',
},
}); });
const slots = useSlots(); const slots = useSlots();
const { t } = useI18n(); const { t } = useI18n();
const entity = computed(() => useArrayData($props.dataKey).store.data);
onMounted(async () => {
await getData();
watch(
() => $props.url,
async (newUrl, lastUrl) => {
if (newUrl == lastUrl) return;
entity.value = null;
await getData();
}
);
});
onMounted(() => fetch()); async function getData() {
const arrayData = useArrayData($props.dataKey, {
const emit = defineEmits(['onFetch']); url: $props.url,
filter: $props.filter,
const entity = ref(); skip: 0,
async function fetch() { });
const params = {}; const { data } = await arrayData.fetch({ append: false });
if (props.filter) params.filter = JSON.stringify(props.filter);
const { data } = await axios.get(props.url, { params });
entity.value = data;
emit('onFetch', data); emit('onFetch', data);
} }
const emit = defineEmits(['onFetch']);
watch(props, async () => {
entity.value = null;
await fetch();
});
</script> </script>
<template> <template>
@ -49,14 +64,30 @@ watch(props, async () => {
<template v-if="entity"> <template v-if="entity">
<div class="header bg-primary q-pa-sm"> <div class="header bg-primary q-pa-sm">
<RouterLink :to="{ name: `${module}List` }"> <RouterLink :to="{ name: `${module}List` }">
<QBtn round flat dense size="md" icon="view_list" color="white"> <QBtn
round
flat
dense
size="md"
icon="view_list"
color="white"
class="link"
>
<QTooltip> <QTooltip>
{{ t('components.cardDescriptor.mainList') }} {{ t('components.cardDescriptor.mainList') }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
</RouterLink> </RouterLink>
<RouterLink :to="{ name: `${module}Summary`, params: { id: entity.id } }"> <RouterLink :to="{ name: `${module}Summary`, params: { id: entity.id } }">
<QBtn round flat dense size="md" icon="launch" color="white"> <QBtn
round
flat
dense
size="md"
icon="launch"
color="white"
class="link"
>
<QTooltip> <QTooltip>
{{ t('components.cardDescriptor.summary') }} {{ t('components.cardDescriptor.summary') }}
</QTooltip> </QTooltip>
@ -86,20 +117,32 @@ watch(props, async () => {
<div class="body q-py-sm"> <div class="body q-py-sm">
<QList dense> <QList dense>
<QItemLabel header class="ellipsis text-h5" :lines="1"> <QItemLabel header class="ellipsis text-h5" :lines="1">
<slot name="description" :entity="entity"> <div class="title">
<span> <span v-if="$props.title" :title="$props.title">
{{ entity.name }} {{ $props.title }}
<QTooltip>{{ entity.name }}</QTooltip>
</span> </span>
</slot> <slot v-else name="description" :entity="entity">
<span :title="entity.name">
{{ entity.name }}
</span>
</slot>
</div>
</QItemLabel> </QItemLabel>
<QItem dense> <QItem dense>
<QItemLabel class="text-subtitle2" caption> <QItemLabel class="subtitle" caption>
#{{ entity.id }} #{{ $props.subtitle ?? entity.id }}
</QItemLabel> </QItemLabel>
</QItem> </QItem>
</QList> </QList>
<slot name="body" :entity="entity" /> <div class="list-box q-mt-xs">
<slot name="body" :entity="entity" />
</div>
</div>
<div class="icons">
<slot name="icons" :entity="entity" />
</div>
<div class="actions">
<slot name="actions" :entity="entity" />
</div> </div>
<slot name="after" /> <slot name="after" />
</template> </template>
@ -110,24 +153,82 @@ watch(props, async () => {
<style lang="scss"> <style lang="scss">
.body { .body {
.q-card__actions {
justify-content: center;
}
.text-h5 { .text-h5 {
padding-top: 5px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
} }
.q-item {
min-height: 20px;
.link {
margin-left: 5px;
}
}
.vn-label-value {
display: flex;
padding: 2px 16px;
.label {
color: var(--vn-label);
font-size: 12px;
width: 47%;
}
.value {
color: var(--vn-text);
font-size: 14px;
margin-left: 12px;
width: 47%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.info {
margin-left: 5px;
}
}
} }
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
.title {
overflow: hidden;
text-overflow: ellipsis;
span {
color: $primary;
font-weight: bold;
}
}
.subtitle {
color: var(--vn-text);
font-size: 16px;
margin-bottom: 15px;
}
.list-box {
width: 90%;
background-color: var(--vn-gray);
margin: 10px auto;
padding: 10px 5px 10px 0px;
border-radius: 8px;
.q-item__label {
color: var(--vn-label);
}
}
.descriptor { .descriptor {
width: 256px; width: 256px;
.header { .header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: stretch; align-items: stretch;
} }
.icons {
margin: 0 10px;
display: flex;
justify-content: center;
.q-icon {
margin-right: 5px;
}
}
.actions {
margin: 0 5px;
}
} }
</style> </style>

View File

@ -0,0 +1,83 @@
<script setup>
const $props = defineProps({
id: { type: Number, default: null },
title: { type: String, default: null },
});
</script>
<template>
<QCard class="card q-mb-md cursor-pointer q-hoverable bg-white-7 q-pa-lg">
<div>
<slot name="title">
<div class="title text-primary text-weight-bold text-h5">
{{ $props.title ?? `#${$props.id}` }}
</div>
</slot>
<div class="card-list-body row">
<div class="list-items row flex-wrap-wrap q-mt-md">
<slot name="list-items" />
</div>
<div class="actions column justify-center">
<slot name="actions" />
</div>
</div>
</div>
</QCard>
</template>
<style lang="scss">
.card-list-body {
.vn-label-value {
display: flex;
justify-content: flex-start;
gap: 2%;
width: 50%;
.label {
width: 30%;
color: var(--vn-label);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.value {
width: 60%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.actions {
.q-btn {
width: 30px;
}
.q-icon {
color: $primary;
font-size: 25px;
}
}
}
@media (max-width: $breakpoint-xs) {
.card-list-body {
.vn-label-value {
width: 100%;
}
}
}
</style>
<style lang="scss" scoped>
.card {
transition: background-color 0.2s;
}
.card:hover {
background-color: var(--vn-gray);
}
.list-items {
width: 90%;
}
@media (max-width: $breakpoint-xs) {
.list-items {
width: 85%;
}
}
</style>

View File

@ -2,6 +2,8 @@
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import axios from 'axios'; import axios from 'axios';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue'; import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
onMounted(() => fetch()); onMounted(() => fetch());
const entity = ref(); const entity = ref();
@ -41,15 +43,21 @@ watch(props, async () => {
<template> <template>
<div class="summary container"> <div class="summary container">
<QCard> <QCard class="cardSummary">
<SkeletonSummary v-if="!entity" /> <SkeletonSummary v-if="!entity" />
<template v-if="entity"> <template v-if="entity">
<div class="header bg-primary q-pa-sm q-mb-md"> <div class="summaryHeader bg-primary q-pa-md text-weight-bolder">
<slot name="header-left">
<span></span>
</slot>
<slot name="header" :entity="entity"> <slot name="header" :entity="entity">
{{ entity.id }} - {{ entity.name }} <VnLv :label="`${entity.id} -`" :value="entity.name" />
</slot>
<slot name="header-right">
<span></span>
</slot> </slot>
</div> </div>
<div class="body q-pa-md q-mb-md"> <div class="summaryBody row q-mb-md">
<slot name="body" :entity="entity" /> <slot name="body" :entity="entity" />
</div> </div>
</template> </template>
@ -63,57 +71,87 @@ watch(props, async () => {
justify-content: center; justify-content: center;
} }
.summary { .cardSummary {
.q-card { width: 100%;
width: 100%; .summaryHeader {
max-width: 1200px;
}
.negative {
color: red;
}
.q-list {
.q-item__label--header {
display: flex;
justify-content: space-between;
a {
color: $primary;
}
}
}
.body > .q-card__section.row {
flex-wrap: wrap;
& > .col {
min-width: 250px;
}
}
.header {
text-align: center; text-align: center;
font-size: 18px; font-size: 20px;
display: flex;
justify-content: space-between;
} }
.summaryBody {
display: flex;
flex-direction: row;
justify-content: space-evenly;
gap: 15px;
padding: 15px;
#slider-container { > .q-card.vn-one {
max-width: 80%; flex: 1;
margin: 0 auto; }
> .q-card.vn-two {
flex: 2;
}
> .q-card.vn-three {
flex: 3;
}
> .q-card.vn-max {
width: 100%;
}
.q-slider { > .q-card {
.q-slider__marker-labels:nth-child(1) { width: 100%;
transform: none; background-color: var(--vn-gray);
padding: 15px;
font-size: 16px;
min-width: 275px;
.vn-label-value {
display: flex;
flex-direction: row;
margin-top: 5px;
.label {
color: var(--vn-label);
width: 10em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 10px;
}
.value {
color: var(--vn-text);
width: max-content;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
} }
.q-slider__marker-labels:nth-child(2) { .header {
transform: none; color: $primary;
left: auto !important; font-weight: bold;
right: 0%; margin-bottom: 25px;
font-size: 20px;
display: inline-block;
}
.header.link:hover {
color: lighten($primary, 20%);
} }
} }
} }
}
.q-dialog .summary { @media (max-width: $breakpoint-xs) {
max-width: 1200px; .summaryBody {
padding: 0;
}
}
}
</style>
<style lang="scss" scoped>
.summaryHeader .vn-label-value {
display: flex;
flex-direction: row;
}
.summaryHeader {
color: $white;
} }
</style> </style>

View File

@ -0,0 +1,50 @@
<template>
<div class="q-pa-md w">
<div class="row q-gutter-md q-mb-md">
<div class="col-1">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
</div>
<div class="row q-gutter-md q-mb-md" v-for="n in 5" :key="n">
<div class="col-1">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.w {
width: 80vw;
}
</style>

View File

@ -0,0 +1,26 @@
<script setup>
import { useSession } from 'src/composables/useSession';
const $props = defineProps({
worker: { type: Number, required: true },
description: { type: String, default: null },
});
const session = useSession();
const token = session.getToken();
</script>
<template>
<div class="avatar-picture column items-center">
<QAvatar color="orange">
<QImg
:src="`/api/Images/user/160x160/${$props.worker}/download?access_token=${token}`"
spinner-color="white"
/>
</QAvatar>
<div class="description">
<slot name="description" v-if="$props.description">
<p>
{{ $props.description }}
</p>
</slot>
</div>
</div>
</template>

View File

@ -51,7 +51,7 @@ async function confirm() {
} }
</script> </script>
<template> <template>
<QDialog ref="dialogRef" persistent> <QDialog ref="dialogRef">
<QCard class="q-pa-sm"> <QCard class="q-pa-sm">
<QCardSection class="row items-center q-pb-none"> <QCardSection class="row items-center q-pb-none">
<QAvatar <QAvatar

View File

@ -20,6 +20,10 @@ const props = defineProps({
required: false, required: false,
default: null, default: null,
}, },
showAll: {
type: Boolean,
default: true,
},
}); });
const emit = defineEmits(['refresh', 'clear']); const emit = defineEmits(['refresh', 'clear']);
@ -29,31 +33,30 @@ const store = arrayData.store;
const userParams = ref({}); const userParams = ref({});
onMounted(() => { onMounted(() => {
if (props.params) userParams.value = props.params; if (props.params) userParams.value = JSON.parse(JSON.stringify(props.params));
const params = store.userParams; if (Object.keys(store.userParams).length > 0) {
if (Object.keys(params).length > 0) { userParams.value = JSON.parse(JSON.stringify(store.userParams));
userParams.value = Object.assign({}, params);
} }
}); });
const isLoading = ref(false); const isLoading = ref(false);
async function search() { async function search() {
const params = userParams.value;
for (const param in params) {
if (params[param] === '' || params[param] === null) {
delete userParams.value[param];
delete store.userParams[param];
}
}
isLoading.value = true; isLoading.value = true;
await arrayData.addFilter({ params }); const params = { ...userParams.value };
const { params: newParams } = await arrayData.addFilter({ params });
userParams.value = newParams;
if (!props.showAll && !Object.values(params).length) store.data = [];
isLoading.value = false; isLoading.value = false;
} }
async function reload() { async function reload() {
isLoading.value = true; isLoading.value = true;
const params = Object.values(userParams.value).filter((param) => param);
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!props.showAll && !params.length) store.data = [];
isLoading.value = false; isLoading.value = false;
emit('refresh'); emit('refresh');
} }
@ -62,6 +65,7 @@ async function clearFilters() {
userParams.value = {}; userParams.value = {};
isLoading.value = true; isLoading.value = true;
await arrayData.applyFilter({ params: {} }); await arrayData.applyFilter({ params: {} });
if (!props.showAll) store.data = [];
isLoading.value = false; isLoading.value = false;
emit('clear'); emit('clear');
@ -70,10 +74,11 @@ async function clearFilters() {
const tags = computed(() => { const tags = computed(() => {
const params = []; const params = [];
for (const param in store.userParams) { for (const param in userParams.value) {
if (!userParams.value[param]) continue;
params.push({ params.push({
label: param, label: param,
value: store.userParams[param], value: userParams.value[param],
}); });
} }
@ -81,8 +86,7 @@ const tags = computed(() => {
}); });
async function remove(key) { async function remove(key) {
delete userParams.value[key]; userParams.value[key] = null;
delete store.userParams[key];
await search(); await search();
} }
@ -192,7 +196,7 @@ function formatValue(value) {
</template> </template>
<i18n> <i18n>
es: es:
No filters applied: No se han aplicado filtros No filters applied: No se han aplicado filtros
Applied filters: Filtros aplicados Applied filters: Filtros aplicados
Remove filters: Eliminar filtros Remove filters: Eliminar filtros

View File

@ -0,0 +1,24 @@
<script setup>
import { Dark } from 'quasar';
import { computed } from 'vue';
const $props = defineProps({
logo: {
type: String,
default: 'salix',
},
});
const src = computed({
get() {
return new URL(
`../../assets/${$props.logo}${Dark.isActive ? '_dark' : ''}.svg`,
import.meta.url
).href;
},
});
</script>
<template>
<QImg :src="src" v-bind="$attrs" />
</template>

View File

@ -0,0 +1,44 @@
<script setup>
import { computed } from 'vue';
import { dashIfEmpty } from 'src/filters';
const $props = defineProps({
label: { type: String, default: null },
value: { type: [Number, String, Boolean], default: null },
titleLabel: { type: String, default: null },
titleValue: { type: [Number, String, Boolean], default: null },
info: { type: String, default: null },
dash: { type: Boolean, default: true },
});
const isBooleanValue = computed(() => typeof $props.value === 'boolean');
</script>
<template>
<div class="vn-label-value">
<div v-if="$props.label || $slots.label" class="label">
<slot name="label">
<span :title="$props.titleLabel ?? $props.label">{{ $props.label }}</span>
</slot>
</div>
<div class="value">
<span v-if="isBooleanValue">
<QIcon
:name="$props.value ? `check` : `close`"
:color="$props.value ? `positive` : `negative`"
size="sm"
/>
</span>
<slot v-else name="value">
<span :title="$props.value">
{{ $props.dash ? dashIfEmpty($props.value) : $props.value }}
</span>
</slot>
</div>
<div class="info" v-if="$props.info">
<QIcon name="info">
<QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]">
{{ $props.info }}
</QTooltip>
</QIcon>
</div>
</div>
</template>

View File

@ -0,0 +1,127 @@
<script setup>
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnAvatar from 'src/components/ui/VnAvatar.vue';
import { toDateHour } from 'src/filters';
import { ref } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n';
import VnPaginate from './VnPaginate.vue';
const $props = defineProps({
id: { type: String, required: true },
url: { type: String, default: null },
filter: { type: Object, default: () => {} },
body: { type: Object, default: () => {} },
addNote: { type: Boolean, default: false },
});
const { t } = useI18n();
const noteModal = ref(false);
const newNote = ref('');
const vnPaginateRef = ref();
async function insert() {
const body = $props.body;
Object.assign(body, { text: newNote.value });
await axios.post($props.url, body);
vnPaginateRef.value.fetch();
newNote.value = '';
}
</script>
<template>
<div class="column items-center">
<VnPaginate
:data-key="$props.url"
:url="$props.url"
order="created DESC"
:limit="20"
:filter="$props.filter"
auto-load
ref="vnPaginateRef"
>
<template #body="{ rows }">
<QCard class="q-pa-md q-mb-md" v-for="(note, index) in rows" :key="index">
<QCardSection horizontal>
<slot name="picture">
<VnAvatar :worker="note.workerFk" />
</slot>
<QItem class="full-width justify-between items-start">
<span class="link">
{{ `${note.worker.firstName} ${note.worker.lastName}` }}
<WorkerDescriptorProxy :id="note.worker.id" />
</span>
<slot name="actions">
{{ toDateHour(note.created) }}
</slot>
</QItem>
</QCardSection>
<QCardSection>
<slot name="text">
{{ note.text }}
</slot>
</QCardSection>
</QCard>
</template>
</VnPaginate>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
v-if="addNote"
color="primary"
icon="add"
size="lg"
round
@click="noteModal = true"
/>
</QPageSticky>
<QDialog v-model="noteModal" @hide="newNote = ''">
<QCard>
<QCardSection>
<QItem class="q-px-none">
<span class="text-primary text-h6 full-width">
<QIcon name="draft" class="q-mr-xs" />
{{ t('Add note') }}
</span>
<QBtn icon="close" flat round dense v-close-popup />
</QItem>
</QCardSection>
<QCardSection>
<QInput
autofocus
type="textarea"
:label="t('Add note here...')"
filled
size="lg"
autogrow
v-model="newNote"
></QInput>
</QCardSection>
<QCardActions class="justify-end q-mr-sm">
<QBtn
flat
:label="t('globals.close')"
color="primary"
v-close-popup
/>
<QBtn
:label="t('globals.save')"
color="primary"
v-close-popup
@click="insert"
/>
</QCardActions>
</QCard>
</QDialog>
</div>
</template>
<style lang="scss" scoped>
.q-card {
max-width: 80em;
}
.q-dialog .q-card {
width: 400px;
}
</style>
<i18n>
es:
Add note here...: Añadir nota aquí...
Add note: Añadir nota
</i18n>

View File

@ -46,9 +46,18 @@ const props = defineProps({
type: Number, type: Number,
default: 500, default: 500,
}, },
skeleton: {
type: Boolean,
default: true,
},
exprBuilder: {
type: Function,
default: null,
},
}); });
const emit = defineEmits(['onFetch', 'onPaginate']); const emit = defineEmits(['onFetch', 'onPaginate']);
defineExpose({ fetch });
const isLoading = ref(false); const isLoading = ref(false);
const pagination = ref({ const pagination = ref({
sortBy: props.order, sortBy: props.order,
@ -63,6 +72,7 @@ const arrayData = useArrayData(props.dataKey, {
limit: props.limit, limit: props.limit,
order: props.order, order: props.order,
userParams: props.userParams, userParams: props.userParams,
exprBuilder: props.exprBuilder,
}); });
const store = arrayData.store; const store = arrayData.store;
@ -82,7 +92,6 @@ async function fetch() {
if (!arrayData.hasMoreData.value) { if (!arrayData.hasMoreData.value) {
isLoading.value = false; isLoading.value = false;
} }
emit('onFetch', store.data); emit('onFetch', store.data);
} }
@ -137,14 +146,9 @@ async function onLoad(...params) {
</h5> </h5>
</div> </div>
<div <div
v-if="store.data && store.data.length === 0 && !isLoading" v-if="props.skeleton && props.autoLoad && !store.data"
class="info-row q-pa-md text-center" class="card-list q-gutter-y-md"
> >
<h5>
{{ t('No results found') }}
</h5>
</div>
<div v-if="props.autoLoad && !store.data" class="card-list q-gutter-y-md">
<QCard class="card" v-for="$index in $props.limit" :key="$index"> <QCard class="card" v-for="$index in $props.limit" :key="$index">
<QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable"> <QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<QItemSection class="q-pa-md"> <QItemSection class="q-pa-md">
@ -164,7 +168,7 @@ async function onLoad(...params) {
</QCard> </QCard>
</div> </div>
</div> </div>
<QInfiniteScroll v-if="store.data" @load="onLoad" :offset="offset"> <QInfiniteScroll v-if="store.data" @load="onLoad" :offset="offset" class="full-width">
<slot name="body" :rows="store.data"></slot> <slot name="body" :rows="store.data"></slot>
<div v-if="isLoading" class="info-row q-pa-md text-center"> <div v-if="isLoading" class="info-row q-pa-md text-center">
<QSpinner color="orange" size="md" /> <QSpinner color="orange" size="md" />

View File

@ -0,0 +1,12 @@
<template>
<div id="row">
<slot></slot>
</div>
</template>
<style lang="scss" scopped>
@media screen and (max-width: 800px) {
#row {
flex-direction: column;
}
}
</style>

View File

@ -2,6 +2,8 @@
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useQuasar } from 'quasar';
const quasar = useQuasar();
const props = defineProps({ const props = defineProps({
dataKey: { dataKey: {
@ -93,7 +95,7 @@ async function search() {
autofocus autofocus
> >
<template #prepend> <template #prepend>
<QIcon name="search" /> <QIcon name="search" v-if="!quasar.platform.is.mobile" />
</template> </template>
<template #append> <template #append>
<QIcon <QIcon
@ -103,7 +105,11 @@ async function search() {
class="cursor-pointer" class="cursor-pointer"
/> />
<QIcon v-if="props.info" name="info" class="cursor-info"> <QIcon
v-if="props.info && $q.screen.gt.xs"
name="info"
class="cursor-info"
>
<QTooltip>{{ props.info }}</QTooltip> <QTooltip>{{ props.info }}</QTooltip>
</QIcon> </QIcon>
</template> </template>
@ -112,13 +118,9 @@ async function search() {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-field {
width: 250px;
}
@media screen and (min-width: $breakpoint-sm-max) { @media screen and (min-width: $breakpoint-sm-max) {
.q-field { .q-field {
width: 400px; width: 450px;
} }
} }

View File

@ -1,19 +1,11 @@
import axios from 'axios'; import axios from 'axios';
export async function getUrl(route, appName = 'salix') { export async function getUrl(route, appName = 'salix') {
let url;
const env = process.env.NODE_ENV === 'development' ? 'dev' : process.env.NODE_ENV;
const filter = { const filter = {
where: {and: [ where: { and: [{ appName: appName }, { environment: process.env.NODE_ENV }] },
{appName: appName},
{environment: env}
]}
}; };
await axios.get('Urls/findOne', {params: {filter}}) const { data } = await axios.get('Urls/findOne', { params: { filter } });
.then(res => { const url = data.url;
url = res.data.url + route; return route ? url + route : url;
});
return url;
} }

View File

@ -0,0 +1,8 @@
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
export function tMobile(...args) {
const quasar = useQuasar();
const { t } = useI18n();
if (!quasar.platform.is.mobile) return t(...args);
}

View File

@ -2,6 +2,7 @@ import { onMounted, ref, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore'; import { useArrayDataStore } from 'stores/useArrayDataStore';
import { buildFilter } from 'filters/filterPanel';
const arrayDataStore = useArrayDataStore(); const arrayDataStore = useArrayDataStore();
@ -29,6 +30,10 @@ export function useArrayData(key, userOptions) {
} }
}); });
if (key && userOptions) {
setOptions();
}
function setOptions() { function setOptions() {
const allowedOptions = [ const allowedOptions = [
'url', 'url',
@ -38,11 +43,12 @@ export function useArrayData(key, userOptions) {
'limit', 'limit',
'skip', 'skip',
'userParams', 'userParams',
'userFilter' 'userFilter',
'exprBuilder',
]; ];
if (typeof userOptions === 'object') { if (typeof userOptions === 'object') {
for (const option in userOptions) { for (const option in userOptions) {
const isEmpty = userOptions[option] == null || userOptions[option] == '' const isEmpty = userOptions[option] == null || userOptions[option] === '';
if (isEmpty || !allowedOptions.includes(option)) continue; if (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) { if (Object.prototype.hasOwnProperty.call(store, option)) {
@ -64,16 +70,27 @@ export function useArrayData(key, userOptions) {
skip: store.skip, skip: store.skip,
}; };
Object.assign(filter, store.userFilter); let exprFilter;
Object.assign(store.filter, filter); let userParams = { ...store.userParams };
if (store?.exprBuilder) {
const where = buildFilter(userParams, (param, value) => {
const res = store.exprBuilder(param, value);
if (res) delete userParams[param];
return res;
});
exprFilter = where ? { where } : null;
}
Object.assign(filter, store.userFilter, exprFilter);
Object.assign(store.filter, filter);
const params = { const params = {
filter: JSON.stringify(store.filter), filter: JSON.stringify(store.filter),
}; };
Object.assign(params, store.userParams); Object.assign(params, userParams);
store.isLoading = true;
store.isLoading = true
const response = await axios.get(store.url, { const response = await axios.get(store.url, {
signal: canceller.signal, signal: canceller.signal,
params, params,
@ -94,9 +111,10 @@ export function useArrayData(key, userOptions) {
updateStateParams(); updateStateParams();
} }
store.isLoading = false store.isLoading = false;
canceller = null; canceller = null;
return response;
} }
function destroy() { function destroy() {
@ -121,9 +139,30 @@ export function useArrayData(key, userOptions) {
async function addFilter({ filter, params }) { async function addFilter({ filter, params }) {
if (filter) store.userFilter = Object.assign(store.userFilter, filter); if (filter) store.userFilter = Object.assign(store.userFilter, filter);
if (params) store.userParams = Object.assign(store.userParams, params);
let userParams = Object.assign({}, store.userParams, params);
userParams = sanitizerParams(userParams, store?.exprBuilder);
store.userParams = userParams;
await fetch({ append: false }); await fetch({ append: false });
return { filter, params };
}
function sanitizerParams(params) {
for (const param in params) {
if (params[param] === '' || params[param] === null) {
delete store.userParams[param];
delete params[param];
if (store.filter?.where) {
delete store.filter.where[Object.keys(store?.exprBuilder(param))[0]];
if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where;
}
}
}
}
return params;
} }
async function loadMore() { async function loadMore() {
@ -136,7 +175,7 @@ export function useArrayData(key, userOptions) {
} }
async function refresh() { async function refresh() {
await fetch({ append: false }); if (Object.values(store.userParams).length) await fetch({ append: false });
} }
function updateStateParams() { function updateStateParams() {
@ -147,14 +186,15 @@ export function useArrayData(key, userOptions) {
if (store.userParams && Object.keys(store.userParams).length !== 0) if (store.userParams && Object.keys(store.userParams).length !== 0)
query.params = JSON.stringify(store.userParams); query.params = JSON.stringify(store.userParams);
router.replace({ if (router)
path: route.path, router.replace({
query: query, path: route.path,
}); query: query,
});
} }
const totalRows = computed(() => store.data && store.data.length || 0); const totalRows = computed(() => (store.data && store.data.length) || 0);
const isLoading = computed(() => store.isLoading || false) const isLoading = computed(() => store.isLoading || false);
return { return {
fetch, fetch,
@ -167,6 +207,6 @@ export function useArrayData(key, userOptions) {
hasMoreData, hasMoreData,
totalRows, totalRows,
updateStateParams, updateStateParams,
isLoading isLoading,
}; };
} }

View File

@ -0,0 +1,3 @@
export function useCamelCase(value) {
return value.replace(/[-_](.)/g, (_, char) => char.toUpperCase());
}

View File

@ -0,0 +1,8 @@
export default function useCardDescription(title, subtitle) {
const getTitle = (title) => title;
const getSubtitle = (subtitle) => subtitle;
return {
title: getTitle(title),
subtitle: getSubtitle(subtitle),
};
}

View File

@ -0,0 +1,3 @@
export function useFirstUpper(str) {
return str && str.charAt(0).toUpperCase() + str.substr(1);
}

View File

@ -0,0 +1,30 @@
import { ref, computed } from 'vue';
const user = ref({});
export function useLogin() {
function getUser() {
const userData = user.value;
user.value = {};
return computed(() => {
return {
user: userData.user,
password: userData.password,
keepLogin: userData.keepLogin,
};
});
}
function setUser(data) {
user.value = {
user: data.user,
password: data.password,
keepLogin: data.keepLogin,
};
}
return {
getUser,
setUser,
};
}

View File

@ -3,15 +3,13 @@ import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import validator from 'validator'; import validator from 'validator';
const models = ref(null); const models = ref(null);
export function useValidator() { export function useValidator() {
if (!models.value) fetch(); if (!models.value) fetch();
function fetch() { function fetch() {
axios.get('Schemas/ModelInfo') axios.get('Schemas/ModelInfo').then((response) => (models.value = response.data));
.then(response => models.value = response.data)
} }
function validate(propertyRule) { function validate(propertyRule) {
@ -38,19 +36,18 @@ export function useValidator() {
const { t } = useI18n(); const { t } = useI18n();
const validations = function (validation) { const validations = function (validation) {
return { return {
presence: (value) => { presence: (value) => {
let message = `Value can't be empty`; let message = `Value can't be empty`;
if (validation.message) if (validation.message)
message = t(validation.message) || validation.message message = t(validation.message) || validation.message;
return !validator.isEmpty(value ? String(value) : '') || message return !validator.isEmpty(value ? String(value) : '') || message;
}, },
length: (value) => { length: (value) => {
const options = { const options = {
min: validation.min || validation.is, min: validation.min || validation.is,
max: validation.max || validation.is max: validation.max || validation.is,
}; };
value = String(value); value = String(value);
@ -69,14 +66,14 @@ export function useValidator() {
}, },
numericality: (value) => { numericality: (value) => {
if (validation.int) if (validation.int)
return validator.isInt(value) || 'Value should be integer' return validator.isInt(value) || 'Value should be integer';
return validator.isNumeric(value) || 'Value should be a number' return validator.isNumeric(value) || 'Value should be a number';
}, },
custom: (value) => validation.bindedFunction(value) || 'Invalid value' custom: (value) => validation.bindedFunction(value) || 'Invalid value',
}; };
}; };
return { return {
validate validate,
}; };
} }

View File

@ -26,7 +26,28 @@ select:-webkit-autofill {
body.body--light { body.body--light {
.q-header .q-toolbar { .q-header .q-toolbar {
background-color: white; background-color: $white;
color: #555; color: #555;
} }
--vn-text: #000000;
--vn-gray: #f5f5f5;
--vn-label: #5f5f5f;
--vn-dark: white;
}
body.body--dark {
--vn-text: #ffffff;
--vn-gray: #313131;
--vn-label: #a8a8a8;
--vn-dark: #292929;
}
.bg-vn-dark {
background-color: var(--vn-dark);
}
.vn-card {
background-color: var(--vn-gray);
color: var(--vn-text);
border-radius: 8px;
} }

View File

@ -12,14 +12,33 @@
// to match your app's branding. // to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website. // Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary: #ff9800; $primary: #ec8916;
$secondary: #26a69a; $secondary: #26a69a;
$accent: #9c27b0; $accent: #9c27b0;
$white: #fff;
$positive: #21ba45; $positive: #21ba45;
$negative: #c10015; $negative: #c10015;
$info: #31ccec; $info: #31ccec;
$warning: #f2c037; $warning: #f2c037;
$vnColor: #8ebb27;
// Pendiente de cuadrar con la base de datos
$success: $positive;
$alert: $negative;
.bg-success {
background-color: $positive;
}
.bg-notice {
background-color: $info;
}
.text-notice {
color: $info;
}
.bg-alert {
background-color: $negative;
}
$color-spacer-light: rgba(255, 255, 255, 0.12); $color-spacer-light: rgba(255, 255, 255, 0.12);
$color-spacer: rgba(255, 255, 255, 0.3); $color-spacer: rgba(255, 255, 255, 0.3);

View File

@ -0,0 +1,94 @@
/**
* Passes a loopback fields filter to an object.
*
* @param {Object} fields The fields object or array
* @return {Object} The fields as object
*/
function fieldsToObject(fields) {
let fieldsObj = {};
if (Array.isArray(fields)) {
for (let field of fields) fieldsObj[field] = true;
} else if (typeof fields == 'object') {
for (let field in fields) {
if (fields[field]) fieldsObj[field] = true;
}
}
return fieldsObj;
}
/**
* Merges two loopback fields filters.
*
* @param {Object|Array} src The source fields
* @param {Object|Array} dst The destination fields
* @return {Array} The merged fields as an array
*/
function mergeFields(src, dst) {
let fields = {};
Object.assign(fields, fieldsToObject(src), fieldsToObject(dst));
return Object.keys(fields);
}
/**
* Merges two loopback where filters.
*
* @param {Object|Array} src The source where
* @param {Object|Array} dst The destination where
* @return {Array} The merged wheres
*/
function mergeWhere(src, dst) {
let and = [];
if (src) and.push(src);
if (dst) and.push(dst);
return simplifyOperation(and, 'and');
}
/**
* Merges two loopback filters returning the merged filter.
*
* @param {Object} src The source filter
* @param {Object} dst The destination filter
* @return {Object} The result filter
*/
function mergeFilters(src, dst) {
let res = Object.assign({}, dst);
if (!src) return res;
if (src.fields) res.fields = mergeFields(src.fields, res.fields);
if (src.where) res.where = mergeWhere(res.where, src.where);
if (src.include) res.include = src.include;
if (src.order) res.order = src.order;
if (src.limit) res.limit = src.limit;
if (src.offset) res.offset = src.offset;
if (src.skip) res.skip = src.skip;
return res;
}
function simplifyOperation(operation, operator) {
switch (operation.length) {
case 0:
return undefined;
case 1:
return operation[0];
default:
return { [operator]: operation };
}
}
function buildFilter(params, builderFunc) {
let and = [];
for (let param in params) {
let value = params[param];
if (value == null) continue;
let expr = builderFunc(param, value);
if (expr) and.push(expr);
}
return simplifyOperation(and, 'and');
}
export { fieldsToObject, mergeFields, mergeWhere, mergeFilters, buildFilter };

View File

@ -1,6 +1,7 @@
import toLowerCase from './toLowerCase'; import toLowerCase from './toLowerCase';
import toDate from './toDate'; import toDate from './toDate';
import toDateString from './toDateString'; import toDateString from './toDateString';
import toDateHour from './toDateHour';
import toCurrency from './toCurrency'; import toCurrency from './toCurrency';
import toPercentage from './toPercentage'; import toPercentage from './toPercentage';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
@ -11,6 +12,7 @@ export {
toLowerCamel, toLowerCamel,
toDate, toDate,
toDateString, toDateString,
toDateHour,
toCurrency, toCurrency,
toPercentage, toPercentage,
dashIfEmpty, dashIfEmpty,

12
src/filters/toDateHour.js Normal file
View File

@ -0,0 +1,12 @@
export default function toDateHour(date) {
const dateHour = new Date(date).toLocaleDateString('es-ES', {
timeZone: 'Europe/Madrid',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
return dateHour;
}

View File

@ -9,6 +9,7 @@ export default {
backToDashboard: 'Return to dashboard', backToDashboard: 'Return to dashboard',
notifications: 'Notifications', notifications: 'Notifications',
userPanel: 'User panel', userPanel: 'User panel',
modules: 'Modules',
pinnedModules: 'Pinned modules', pinnedModules: 'Pinned modules',
darkMode: 'Dark mode', darkMode: 'Dark mode',
logOut: 'Log out', logOut: 'Log out',
@ -19,6 +20,7 @@ export default {
save: 'Save', save: 'Save',
remove: 'Remove', remove: 'Remove',
reset: 'Reset', reset: 'Reset',
close: 'Close',
cancel: 'Cancel', cancel: 'Cancel',
confirm: 'Confirm', confirm: 'Confirm',
back: 'Back', back: 'Back',
@ -30,10 +32,11 @@ export default {
rowAdded: 'Row added', rowAdded: 'Row added',
rowRemoved: 'Row removed', rowRemoved: 'Row removed',
pleaseWait: 'Please wait...', pleaseWait: 'Please wait...',
noPinnedModules: 'You have dont have any pinned modules', noPinnedModules: `You don't have any pinned modules`,
}, summary: {
moduleIndex: { basicData: 'Basic data',
allModules: 'All modules', },
noSelectedRows: `You don't have any line selected`,
}, },
errors: { errors: {
statusUnauthorized: 'Access denied', statusUnauthorized: 'Access denied',
@ -50,6 +53,25 @@ export default {
loginSuccess: 'You have successfully logged in', loginSuccess: 'You have successfully logged in',
loginError: 'Invalid username or password', loginError: 'Invalid username or password',
fieldRequired: 'This field is required', fieldRequired: 'This field is required',
twoFactorRequired: 'Two-factor verification required',
pageTitles: {
logIn: 'Login',
},
},
twoFactor: {
code: 'Code',
validate: 'Validate',
insert: 'Enter the verification code',
explanation:
'Please, enter the verification code that we have sent to your email in the next 5 minutes',
pageTitles: {
twoFactor: 'Two-Factor',
},
},
verifyEmail: {
pageTitles: {
verifyEmail: 'Email verification',
},
}, },
dashboard: { dashboard: {
pageTitles: { pageTitles: {
@ -141,6 +163,8 @@ export default {
balanceDue: 'Balance due', balanceDue: 'Balance due',
balanceDueInfo: 'Deviated invoices minus payments', balanceDueInfo: 'Deviated invoices minus payments',
recoverySince: 'Recovery since', recoverySince: 'Recovery since',
businessType: 'Business Type',
city: 'City',
}, },
basicData: { basicData: {
socialName: 'Fiscal name', socialName: 'Fiscal name',
@ -161,6 +185,8 @@ export default {
summary: 'Summary', summary: 'Summary',
basicData: 'Basic Data', basicData: 'Basic Data',
boxing: 'Boxing', boxing: 'Boxing',
sms: 'Sms',
notes: 'Notes',
}, },
list: { list: {
nickname: 'Nickname', nickname: 'Nickname',
@ -230,6 +256,7 @@ export default {
requester: 'Requester', requester: 'Requester',
atender: 'Atender', atender: 'Atender',
request: 'Request', request: 'Request',
weight: 'Weight',
goTo: 'Go to', goTo: 'Go to',
}, },
}, },
@ -244,7 +271,10 @@ export default {
lines: 'Lines', lines: 'Lines',
rma: 'RMA', rma: 'RMA',
photos: 'Photos', photos: 'Photos',
development: 'Development',
log: 'Audit logs', log: 'Audit logs',
notes: 'Notes',
action: 'Action',
}, },
list: { list: {
customer: 'Customer', customer: 'Customer',
@ -268,6 +298,9 @@ export default {
ticketId: 'Ticket ID', ticketId: 'Ticket ID',
customerSummary: 'Customer summary', customerSummary: 'Customer summary',
claimedTicket: 'Claimed ticket', claimedTicket: 'Claimed ticket',
commercial: 'Commercial',
province: 'Province',
zone: 'Zone',
}, },
summary: { summary: {
customer: 'Customer', customer: 'Customer',
@ -295,7 +328,7 @@ export default {
result: 'Result', result: 'Result',
responsible: 'Responsible', responsible: 'Responsible',
worker: 'Worker', worker: 'Worker',
redelivery: 'Redelivery' redelivery: 'Redelivery',
}, },
basicData: { basicData: {
customer: 'Customer', customer: 'Customer',
@ -323,11 +356,14 @@ export default {
list: { list: {
ref: 'Reference', ref: 'Reference',
issued: 'Issued', issued: 'Issued',
shortIssued: 'Issued',
amount: 'Amount', amount: 'Amount',
client: 'Client', client: 'Client',
created: 'Created', created: 'Created',
shortCreated: 'Created',
company: 'Company', company: 'Company',
dued: 'Due date', dued: 'Due date',
shortDued: 'Due date',
}, },
card: { card: {
issued: 'Issued', issued: 'Issued',
@ -394,6 +430,7 @@ export default {
userId: 'User ID', userId: 'User ID',
role: 'Role', role: 'Role',
sipExtension: 'Extension', sipExtension: 'Extension',
locker: 'Locker',
}, },
notificationsManager: { notificationsManager: {
activeNotifications: 'Active notifications', activeNotifications: 'Active notifications',
@ -411,7 +448,8 @@ export default {
wagonEdit: 'Edit wagon', wagonEdit: 'Edit wagon',
typesList: 'Types List', typesList: 'Types List',
typeCreate: 'Create type', typeCreate: 'Create type',
typeEdit: 'Edit type' typeEdit: 'Edit type',
wagonCounter: 'Trolley counter',
}, },
type: { type: {
name: 'Name', name: 'Name',
@ -431,7 +469,7 @@ export default {
plate: 'Plate', plate: 'Plate',
volume: 'Volume', volume: 'Volume',
type: 'Type', type: 'Type',
label: 'Label' label: 'Label',
}, },
warnings: { warnings: {
noData: 'No data available', noData: 'No data available',
@ -444,7 +482,29 @@ export default {
minHeightBetweenTrays: 'The minimum height between trays is ', minHeightBetweenTrays: 'The minimum height between trays is ',
maxWagonHeight: 'The maximum height of the wagon is ', maxWagonHeight: 'The maximum height of the wagon is ',
uncompleteTrays: 'There are incomplete trays', uncompleteTrays: 'There are incomplete trays',
} },
},
route: {
pageTitles: {
routes: 'Routes',
cmrsList: 'External CMRs list',
},
cmr: {
list: {
results: 'results',
cmrFk: 'CMR id',
hasCmrDms: `Attached in gestdoc`,
true: 'Yes',
false: 'No',
ticketFk: 'Ticketd id',
routeFk: 'Route id',
country: 'Country',
clientFk: 'Client id',
shipped: 'Preparation date',
viewCmr: 'View CMR',
downloadCmrs: 'Download CMRs',
},
},
}, },
components: { components: {
topbar: {}, topbar: {},

View File

@ -9,6 +9,7 @@ export default {
backToDashboard: 'Volver al tablón', backToDashboard: 'Volver al tablón',
notifications: 'Notificaciones', notifications: 'Notificaciones',
userPanel: 'Panel de usuario', userPanel: 'Panel de usuario',
modules: 'Módulos',
pinnedModules: 'Módulos fijados', pinnedModules: 'Módulos fijados',
darkMode: 'Modo oscuro', darkMode: 'Modo oscuro',
logOut: 'Cerrar sesión', logOut: 'Cerrar sesión',
@ -19,6 +20,7 @@ export default {
save: 'Guardar', save: 'Guardar',
remove: 'Eliminar', remove: 'Eliminar',
reset: 'Restaurar', reset: 'Restaurar',
close: 'Cerrar',
cancel: 'Cancelar', cancel: 'Cancelar',
confirm: 'Confirmar', confirm: 'Confirmar',
back: 'Volver', back: 'Volver',
@ -31,9 +33,10 @@ export default {
rowRemoved: 'Fila eliminada', rowRemoved: 'Fila eliminada',
pleaseWait: 'Por favor, espera...', pleaseWait: 'Por favor, espera...',
noPinnedModules: 'No has fijado ningún módulo', noPinnedModules: 'No has fijado ningún módulo',
}, summary: {
moduleIndex: { basicData: 'Datos básicos',
allModules: 'Todos los módulos', },
noSelectedRows: `No tienes ninguna línea seleccionada`,
}, },
errors: { errors: {
statusUnauthorized: 'Acceso denegado', statusUnauthorized: 'Acceso denegado',
@ -50,6 +53,25 @@ export default {
loginSuccess: 'Inicio de sesión correcto', loginSuccess: 'Inicio de sesión correcto',
loginError: 'Nombre de usuario o contraseña incorrectos', loginError: 'Nombre de usuario o contraseña incorrectos',
fieldRequired: 'Este campo es obligatorio', fieldRequired: 'Este campo es obligatorio',
twoFactorRequired: 'Verificación de doble factor requerida',
pageTitles: {
logIn: 'Inicio de sesión',
},
},
twoFactor: {
code: 'Código',
validate: 'Validar',
insert: 'Introduce el código de verificación',
explanation:
'Por favor, introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos',
pageTitles: {
twoFactor: 'Doble factor',
},
},
verifyEmail: {
pageTitles: {
verifyEmail: 'Verificación de correo',
},
}, },
dashboard: { dashboard: {
pageTitles: { pageTitles: {
@ -63,7 +85,7 @@ export default {
webPayments: 'Pagos Web', webPayments: 'Pagos Web',
createCustomer: 'Crear cliente', createCustomer: 'Crear cliente',
basicData: 'Datos básicos', basicData: 'Datos básicos',
summary: 'Resumen' summary: 'Resumen',
}, },
list: { list: {
phone: 'Teléfono', phone: 'Teléfono',
@ -106,7 +128,7 @@ export default {
province: 'Provincia', province: 'Provincia',
country: 'País', country: 'País',
street: 'Calle', street: 'Calle',
isEqualizated: 'Equalizado', isEqualizated: 'Recargo de equivalencia',
isActive: 'Activo', isActive: 'Activo',
invoiceByAddress: 'Facturar por consignatario', invoiceByAddress: 'Facturar por consignatario',
verifiedData: 'Datos verificados', verifiedData: 'Datos verificados',
@ -140,6 +162,8 @@ export default {
balanceDue: 'Saldo vencido', balanceDue: 'Saldo vencido',
balanceDueInfo: 'Facturas fuera de plazo menos recibos', balanceDueInfo: 'Facturas fuera de plazo menos recibos',
recoverySince: 'Recobro desde', recoverySince: 'Recobro desde',
businessType: 'Tipo de negocio',
city: 'Población',
}, },
basicData: { basicData: {
socialName: 'Nombre fiscal', socialName: 'Nombre fiscal',
@ -160,6 +184,8 @@ export default {
summary: 'Resumen', summary: 'Resumen',
basicData: 'Datos básicos', basicData: 'Datos básicos',
boxing: 'Encajado', boxing: 'Encajado',
sms: 'Sms',
notes: 'Notas',
}, },
list: { list: {
nickname: 'Alias', nickname: 'Alias',
@ -229,6 +255,7 @@ export default {
requester: 'Solicitante', requester: 'Solicitante',
atender: 'Comprador', atender: 'Comprador',
request: 'Petición de compra', request: 'Petición de compra',
weight: 'Peso',
goTo: 'Ir a', goTo: 'Ir a',
}, },
}, },
@ -242,8 +269,11 @@ export default {
basicData: 'Datos básicos', basicData: 'Datos básicos',
lines: 'Líneas', lines: 'Líneas',
rma: 'RMA', rma: 'RMA',
development: 'Trazabilidad',
photos: 'Fotos', photos: 'Fotos',
log: 'Registros de auditoría', log: 'Registros de auditoría',
notes: 'Notas',
action: 'Acción',
}, },
list: { list: {
customer: 'Cliente', customer: 'Cliente',
@ -267,6 +297,9 @@ export default {
ticketId: 'ID ticket', ticketId: 'ID ticket',
customerSummary: 'Resumen del cliente', customerSummary: 'Resumen del cliente',
claimedTicket: 'Ticket reclamado', claimedTicket: 'Ticket reclamado',
commercial: 'Comercial',
province: 'Provincia',
zone: 'Zona',
}, },
summary: { summary: {
customer: 'Cliente', customer: 'Cliente',
@ -294,7 +327,7 @@ export default {
result: 'Consecuencias', result: 'Consecuencias',
responsible: 'Responsable', responsible: 'Responsable',
worker: 'Trabajador', worker: 'Trabajador',
redelivery: 'Devolución' redelivery: 'Devolución',
}, },
basicData: { basicData: {
customer: 'Cliente', customer: 'Cliente',
@ -323,11 +356,14 @@ export default {
list: { list: {
ref: 'Referencia', ref: 'Referencia',
issued: 'Fecha emisión', issued: 'Fecha emisión',
shortIssued: 'F. emisión',
amount: 'Importe', amount: 'Importe',
client: 'Cliente', client: 'Cliente',
created: 'Fecha creación', created: 'Fecha creación',
shortCreated: 'F. creación',
company: 'Empresa', company: 'Empresa',
dued: 'Fecha vencimineto', dued: 'Fecha vencimineto',
shortDued: 'F. vencimiento',
}, },
card: { card: {
issued: 'Fecha emisión', issued: 'Fecha emisión',
@ -394,6 +430,7 @@ export default {
userId: 'ID del usuario', userId: 'ID del usuario',
role: 'Rol', role: 'Rol',
sipExtension: 'Extensión', sipExtension: 'Extensión',
locker: 'Taquilla',
}, },
notificationsManager: { notificationsManager: {
activeNotifications: 'Notificaciones activas', activeNotifications: 'Notificaciones activas',
@ -411,7 +448,8 @@ export default {
wagonEdit: 'Editar tipo', wagonEdit: 'Editar tipo',
typesList: 'Listado tipos', typesList: 'Listado tipos',
typeCreate: 'Crear tipo', typeCreate: 'Crear tipo',
typeEdit: 'Editar tipo' typeEdit: 'Editar tipo',
wagonCounter: 'Contador de carros',
}, },
type: { type: {
name: 'Nombre', name: 'Nombre',
@ -444,7 +482,29 @@ export default {
minHeightBetweenTrays: 'La distancia mínima entre bandejas es ', minHeightBetweenTrays: 'La distancia mínima entre bandejas es ',
maxWagonHeight: 'La altura máxima del vagón es ', maxWagonHeight: 'La altura máxima del vagón es ',
uncompleteTrays: 'Hay bandejas sin completar', uncompleteTrays: 'Hay bandejas sin completar',
} },
},
route: {
pageTitles: {
routes: 'Rutas',
cmrsList: 'Listado de CMRs externos',
},
cmr: {
list: {
results: 'resultados',
cmrFk: 'Id CMR',
hasCmrDms: 'Adjuntado en gestdoc',
true: 'Sí',
false: 'No',
ticketFk: 'Id ticket',
routeFk: 'Id ruta',
country: 'País',
clientFk: 'Id cliente',
shipped: 'Fecha preparación',
viewCmr: 'Ver CMR',
downloadCmrs: 'Descargar CMRs',
},
},
}, },
components: { components: {
topbar: {}, topbar: {},

100
src/layouts/OutLayout.vue Normal file
View File

@ -0,0 +1,100 @@
<script setup>
import { Dark, Quasar } from 'quasar';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
const userLocale = computed({
get() {
return locale.value;
},
set(value) {
locale.value = value;
if (value === 'en') value = 'en-GB';
// FIXME: Dynamic imports from absolute paths are not compatible with vite:
// https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
try {
const langList = import.meta.glob('../../node_modules/quasar/lang/*.mjs');
langList[`../../node_modules/quasar/lang/${value}.mjs`]().then((lang) => {
Quasar.lang.set(lang.default);
});
} catch (error) {
//
}
},
});
const darkMode = computed({
get() {
return Dark.isActive;
},
set(value) {
Dark.set(value);
},
});
const langs = ['en', 'es'];
</script>
<template>
<QLayout view="hHh LpR fFf">
<QHeader reveal class="bg-dark">
<QToolbar class="justify-end">
<QBtn
id="switchLanguage"
:label="t('globals.language')"
icon="translate"
color="primary"
size="sm"
flat
rounded
>
<QMenu auto-close>
<QList dense v-for="lang in langs" :key="lang">
<QItem
@click="userLocale = lang"
:active="userLocale == lang"
v-ripple
clickable
>
{{ t(`globals.lang.${lang}`) }}
</QItem>
</QList>
</QMenu>
</QBtn>
<QList>
<QItem>
<QItemSection>
<QToggle
v-model="darkMode"
checked-icon="dark_mode"
unchecked-icon="light_mode"
:label="t(`globals.darkMode`)"
/>
</QItemSection>
</QItem>
</QList>
</QToolbar>
</QHeader>
<QPageContainer>
<QPage>
<div class="form">
<QCard class="flex flex-center">
<RouterView></RouterView>
</QCard>
</div>
</QPage>
</QPageContainer>
</QLayout>
</template>
<style lang="scss" scoped>
.form {
display: flex;
align-items: center;
justify-content: center;
min-height: inherit;
flex-direction: column;
}
</style>

View File

@ -0,0 +1,518 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { useStateStore } from 'src/stores/useStateStore';
import { toDate, toPercentage, toCurrency } from 'filters/index';
import { tMobile } from 'src/composables/tMobile';
import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import { useArrayData } from 'composables/useArrayData';
const { t } = useI18n();
const quasar = useQuasar();
const route = useRoute();
const router = useRouter();
const stateStore = computed(() => useStateStore());
const claim = ref(null);
const claimRef = ref();
const claimId = route.params.id;
const dialogDestination = ref(false);
const claimDestinationFk = ref(null);
const resolvedStateId = ref(null);
const claimActionsForm = ref();
const rows = ref([]);
const selectedRows = ref([]);
const destinationTypes = ref([]);
const totalClaimed = ref(null);
const DEFAULT_MAX_RESPONSABILITY = 5;
const DEFAULT_MIN_RESPONSABILITY = 1;
const arrayData = useArrayData('claimData');
const marker_labels = [
{ value: DEFAULT_MIN_RESPONSABILITY, label: t('claim.summary.company') },
{ value: DEFAULT_MAX_RESPONSABILITY, label: t('claim.summary.person') },
];
const columns = computed(() => [
{
name: 'Id',
label: t('Id item'),
field: (row) => row.itemFk,
},
{
name: 'ticket',
label: t('Ticket'),
field: (row) => row.ticketFk,
align: 'center',
},
{
name: 'destination',
label: t('Destination'),
field: (row) => row.claimDestinationFk,
align: 'left',
},
{
name: 'Landed',
label: t('Landed'),
field: (row) => toDate(row.landed),
},
{
name: 'quantity',
label: t('Quantity'),
field: (row) => row.quantity,
},
{
name: 'concept',
label: t('Description'),
field: (row) => row.concept,
align: 'left',
},
{
name: 'price',
label: t('Price'),
field: (row) => row.price,
format: (value) => value,
align: 'center',
},
{
name: 'discount',
label: t('Discount'),
field: (row) => row.discount,
format: (value) => toPercentage(value / 100),
align: 'left',
},
{
name: 'total',
label: t('Total'),
field: (row) => row.total,
format: (value) => value,
align: 'center',
},
{
name: 'delete',
},
]);
onMounted(() => {
getTotal();
});
function setData(data) {
rows.value = data;
getTotal();
}
function getTotal() {
if (rows.value.length) {
totalClaimed.value = rows.value.reduce((total, row) => total + row.total, 0);
}
}
async function updateDestinations(claimDestinationFk) {
await updateDestination(claimDestinationFk, selectedRows.value, { reload: true });
}
async function updateDestination(claimDestinationFk, row, options = {}) {
if (claimDestinationFk) {
await axios.post('Claims/updateClaimDestination', {
claimDestinationFk,
rows: Array.isArray(row) ? row : [row],
});
options.reload && claimActionsForm.value.reload();
}
}
async function regularizeClaim() {
const query = `Claims/${claimId}/regularizeClaim`;
await axios.post(query);
if (claim.value.responsibility >= Math.ceil(DEFAULT_MAX_RESPONSABILITY) / 2) {
await claimRef.value.fetch();
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('confirmGreuges'),
message: t('confirmGreugesMessage'),
},
})
.onOk(async () => await onUpdateGreugeAccept());
} else {
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
}
await arrayData.fetch({ append: false });
}
async function updateGreuge(greuges) {
const { data } = await axios.post(`Greuges`, greuges);
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
return data;
}
async function onUpdateGreugeAccept() {
const greugeTypeFreightId = await getGreugeTypeId();
const freightPickUpPrice = await getGreugeConfig();
await updateGreuge({
clientFk: claim.value.clientFk,
description: `${t('ClaimGreugeDescription')} ${claimId}`.toUpperCase(),
amount: freightPickUpPrice,
greugeTypeFk: greugeTypeFreightId,
ticketFk: claim.value.ticketFk,
});
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
}
async function getGreugeTypeId() {
const params = { filter: { where: { code: 'freightPickUp' } } };
const query = `GreugeTypes/findOne`;
const { data } = await axios.get(query, { params });
return data.id;
}
async function getGreugeConfig() {
const query = `GreugeConfigs/findOne`;
const { data } = await axios.get(query);
return data.freightPickUpPrice;
}
async function save(data) {
const query = `Claims/${claimId}/updateClaimAction`;
await axios.patch(query, data);
}
async function importToNewRefundTicket() {
const query = `ClaimBeginnings/${claimId}/importToNewRefundTicket`;
await axios.post(query);
claimActionsForm.value.reload();
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
}
</script>
<template>
<FetchData
ref="claimRef"
:url="`Claims/${claimId}`"
@on-fetch="(data) => (claim = data)"
auto-load
/>
<FetchData
url="ClaimStates/findOne"
@on-fetch="(data) => (resolvedStateId = data.id)"
auto-load
:where="{ code: 'resolved' }"
/>
<FetchData
url="ClaimDestinations"
auto-load
@on-fetch="(data) => (destinationTypes = data)"
/>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer
v-model="stateStore.rightDrawer"
side="right"
:width="300"
show-if-above
v-if="claim"
>
<QCard class="totalClaim vn-card q-my-md q-pa-sm">
{{ `${t('Total claimed')}: ${toCurrency(totalClaimed)}` }}
</QCard>
<QCard class="vn-card q-mb-md q-pa-sm">
<QItem class="justify-between">
<QItemLabel class="slider-container">
<p class="text-primary">
{{ t('claim.summary.actions') }}
</p>
<QSlider
class="responsibility { 'background-color:primary': quasar.platform.is.mobile }"
v-model="claim.responsibility"
:label-value="t('claim.summary.responsibility')"
@change="(value) => save({ responsibility: value })"
label-always
color="primary"
markers
:marker-labels="marker_labels"
:min="DEFAULT_MIN_RESPONSABILITY"
:max="DEFAULT_MAX_RESPONSABILITY"
/>
</QItemLabel>
</QItem>
</QCard>
<QItemLabel class="mana q-mb-md">
<QCheckbox
v-model="claim.isChargedToMana"
@update:model-value="(value) => save({ isChargedToMana: value })"
/>
<span>{{ t('mana') }}</span>
</QItemLabel>
</QDrawer>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()"> </Teleport>
<CrudModel
v-if="claim"
data-key="ClaimEnds"
url="ClaimEnds/filter"
save-url="ClaimEnds/crud"
ref="claimActionsForm"
v-model:selected="selectedRows"
:filter="{ where: { claimFk: claimId } }"
:default-remove="true"
:default-save="false"
:default-reset="false"
@on-fetch="setData"
auto-load
>
<template #body>
<QTable
:columns="columns"
:rows="rows"
:dense="$q.screen.lt.md"
row-key="id"
selection="multiple"
v-model:selected="selectedRows"
:grid="$q.screen.lt.md"
>
<template #body-cell-ticket="{ value }">
<QTd align="center">
<span class="link">
{{ value }}
<TicketDescriptorProxy :id="value" />
</span>
</QTd>
</template>
<template #body-cell-destination="{ row }">
<QTd>
<VnSelectFilter
v-model="row.claimDestinationFk"
:options="destinationTypes"
option-label="description"
option-value="id"
:autofocus="true"
dense
input-debounce="0"
hide-selected
@update:model-value="(value) => updateDestination(value, row)"
/>
</QTd>
</template>
<template #body-cell-price="{ value }">
<QTd align="center">
{{ toCurrency(value) }}
</QTd>
</template>
<template #body-cell-total="{ value }">
<QTd align="center">
{{ toCurrency(value) }}
</QTd>
</template>
<!-- View for grid mode -->
<template #item="props">
<div class="q-mb-md col-12 grid-style-transition">
<QCard>
<QCardSection class="row justify-between">
<QCheckbox v-model="props.selected" />
<QBtn color="primary" icon="delete" flat round />
</QCardSection>
<QSeparator inset />
<QList dense>
<QItem v-for="column of props.cols" :key="column.name">
<QItemSection>
<QItemLabel caption>
{{ column.label }}
</QItemLabel>
</QItemSection>
<QItemSection side>
<QItemLabel v-if="column.name === 'destination'">
{{ column.value.description }}
</QItemLabel>
<QItemLabel v-else>
{{ column.value }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
<template #moreBeforeActions>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:label="tMobile('Regularize')"
:title="t('Regularize')"
icon="check"
@click="regularizeClaim"
:disable="claim.claimStateFk == resolvedStateId"
/>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:disable="!selectedRows.length"
:label="tMobile('Change destination')"
:title="t('Change destination')"
icon="swap_horiz"
@click="dialogDestination = !dialogDestination"
/>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:label="tMobile('Import claim')"
:title="t('Import claim')"
icon="Upload"
@click="importToNewRefundTicket"
:disable="claim.claimStateFk == resolvedStateId"
/>
</template>
</CrudModel>
<QDialog v-model="dialogDestination">
<QCard>
<QCardSection>
<QItem class="q-pa-sm">
<span class="q-dialog__title text-white">
{{ t('dialog title') }}
</span>
<QBtn icon="close" flat round dense v-close-popup />
</QItem>
</QCardSection>
<QItemSection>
<VnSelectFilter
class="q-pa-sm"
v-model="claimDestinationFk"
:options="destinationTypes"
option-label="description"
option-value="id"
:autofocus="true"
dense
input-debounce="0"
hide-selected
/>
</QItemSection>
<QCardActions class="justify-end q-mr-sm">
<QBtn flat :label="t('globals.close')" color="primary" v-close-popup />
<QBtn
:disable="!claimDestinationFk"
:label="t('globals.save')"
color="primary"
v-close-popup
@click="updateDestinations(claimDestinationFk)"
/>
</QCardActions>
</QCard>
</QDialog>
<!-- <QDialog v-model="dialogGreuge">
<QCardSection>
<QItem class="q-pa-sm">
<span class="q-pa-sm q-dialog__title text-white">
{{ t('dialogGreuge title') }}
</span>
<QBtn class="q-pa-sm" icon="close" flat round dense v-close-popup />
</QItem>
<QCardActions class="justify-end q-mr-sm">
<QBtn flat :label="t('globals.close')" color="primary" v-close-popup />
<QBtn
:label="t('globals.save')"
color="primary"
v-close-popup
@click="onUpdateGreugeAccept"
/>
</QCardActions>
</QCardSection>
</QDialog> -->
</template>
<style lang="scss" scoped>
.slider-container {
width: 50%;
}
@media (max-width: $breakpoint-xs) {
.slider-container {
width: 90%;
}
}
.q-table {
.q-item {
min-height: min-content;
height: 0;
}
}
.q-dialog {
.q-btn {
height: min-content;
}
}
.responsibility {
max-width: 100%;
margin-left: 40px;
}
.mana {
float: inline-start;
}
</style>
<i18n>
en:
mana: Is paid with mana
dialog title: Change destination to all selected rows
confirmGreuges: Do you want to insert complaints?
confirmGreugesMessage: Insert complaints into the client's record
es:
mana: Cargado al maná
Delivered: Descripción
Quantity: Cantidad
Claimed: Rec
Description: Descripción
Price: Precio
Discount: Dto.
Destination: Destino
Landed: F.entrega
Remove line: Eliminar línea
Total claimed: Total reclamado
Regularize: Regularizar
Change destination: Cambiar destino
Import claim: Importar reclamación
dialog title: Cambiar destino en todas las filas seleccionadas
Remove: Eliminar
dialogGreuge title: Insertar greuges en la ficha del cliente
ClaimGreugeDescription: Id reclamación
Id item: Id artículo
confirmGreuges: ¿Desea insertar greuges?
confirmGreugesMessage: Insertar greuges en la ficha del cliente
</i18n>

View File

@ -6,6 +6,7 @@ import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -13,6 +14,16 @@ const session = useSession();
const token = session.getToken(); const token = session.getToken();
const claimFilter = { const claimFilter = {
fields: [
'id',
'clientFk',
'created',
'workerFk',
'claimStateFk',
'packages',
'rma',
'hasToPickUp',
],
include: [ include: [
{ {
relation: 'client', relation: 'client',
@ -80,136 +91,119 @@ const statesFilter = {
auto-load auto-load
/> />
<FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load /> <FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load />
<FormModel
<div class="column items-center"> :url="`Claims/${route.params.id}`"
<QCard> :url-update="`Claims/updateClaim/${route.params.id}`"
<FormModel :filter="claimFilter"
:url="`Claims/${route.params.id}`" model="claim"
:filter="claimFilter" >
model="claim" <template #form="{ data, validate, filter }">
> <VnRow class="row q-gutter-md q-mb-md">
<template #form="{ data, validate, filter }"> <div class="col">
<div class="row q-gutter-md q-mb-md"> <QInput
<div class="col"> v-model="data.client.name"
<QInput :label="t('claim.basicData.customer')"
v-model="data.client.name" disable
:label="t('claim.basicData.customer')" />
disable </div>
/> <div class="col">
</div> <QInput
<div class="col"> v-model="data.created"
<QInput mask="####-##-##"
v-model="data.created" fill-mask="_"
mask="####-##-##" autofocus
fill-mask="_" >
autofocus <template #append>
> <QIcon name="event" class="cursor-pointer">
<template #append> <QPopupProxy
<QIcon name="event" class="cursor-pointer"> cover
<QPopupProxy transition-show="scale"
cover transition-hide="scale"
transition-show="scale" >
transition-hide="scale" <QDate v-model="data.created" mask="YYYY-MM-DD">
> <div class="row items-center justify-end">
<QDate <QBtn
v-model="data.created" v-close-popup
mask="YYYY-MM-DD" label="Close"
> color="primary"
<div class="row items-center justify-end"> flat
<QBtn />
v-close-popup </div>
label="Close" </QDate>
color="primary" </QPopupProxy>
flat </QIcon>
/> </template>
</div> </QInput>
</QDate> </div>
</QPopupProxy> </VnRow>
</QIcon> <VnRow class="row q-gutter-md q-mb-md">
</template> <div class="col">
</QInput> <QSelect
</div> v-model="data.workerFk"
</div> :options="workers"
<div class="row q-gutter-md q-mb-md"> option-value="id"
<div class="col"> option-label="name"
<QSelect emit-value
v-model="data.workerFk" :label="t('claim.basicData.assignedTo')"
:options="workers" map-options
option-value="id" use-input
option-label="name" @filter="(value, update) => filter(value, update, workerFilter)"
emit-value :rules="validate('claim.claimStateFk')"
:label="t('claim.basicData.assignedTo')" :input-debounce="0"
map-options >
use-input <template #before>
@filter=" <QAvatar color="orange">
(value, update) => filter(value, update, workerFilter) <QImg
" v-if="data.workerFk"
:rules="validate('claim.claimStateFk')" :src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`"
:input-debounce="0" spinner-color="white"
> />
<template #before> </QAvatar>
<QAvatar color="orange"> </template>
<QImg </QSelect>
v-if="data.workerFk" </div>
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`" <div class="col">
spinner-color="white" <QSelect
/> v-model="data.claimStateFk"
</QAvatar> :options="claimStates"
</template> option-value="id"
</QSelect> option-label="description"
</div> emit-value
<div class="col"> :label="t('claim.basicData.state')"
<QSelect map-options
v-model="data.claimStateFk" use-input
:options="claimStates" @filter="(value, update) => filter(value, update, statesFilter)"
option-value="id" :rules="validate('claim.claimStateFk')"
option-label="description" :input-debounce="0"
emit-value >
:label="t('claim.basicData.state')" </QSelect>
map-options </div>
use-input </VnRow>
@filter=" <VnRow class="row q-gutter-md q-mb-md">
(value, update) => filter(value, update, statesFilter) <div class="col">
" <QInput
:rules="validate('claim.claimStateFk')" v-model.number="data.packages"
:input-debounce="0" :label="t('claim.basicData.packages')"
> :rules="validate('claim.packages')"
</QSelect> type="number"
</div> />
</div> </div>
<div class="row q-gutter-md q-mb-md"> <div class="col">
<div class="col"> <QInput
<QInput v-model="data.rma"
v-model="data.packages" :label="t('claim.basicData.returnOfMaterial')"
:label="t('claim.basicData.packages')" :rules="validate('claim.rma')"
:rules="validate('claim.packages')" />
/> </div>
</div> </VnRow>
<div class="col"> <VnRow class="row q-gutter-md q-mb-md">
<QInput <div class="col">
v-model="data.rma" <QCheckbox
:label="t('claim.basicData.returnOfMaterial')" v-model="data.hasToPickUp"
:rules="validate('claim.rma')" :label="t('claim.basicData.picked')"
/> />
</div> </div>
</div> </VnRow>
<div class="row q-gutter-md q-mb-md"> </template>
<div class="col"> </FormModel>
<QCheckbox
v-model="data.hasToPickUp"
:label="t('claim.basicData.picked')"
/>
</div>
</div>
</template>
</FormModel>
</QCard>
</div>
</template> </template>
<style lang="scss" scoped>
.q-card {
width: 100%;
max-width: 60em;
}
</style>

View File

@ -3,11 +3,12 @@ import LeftMenu from 'components/LeftMenu.vue';
import { getUrl } from 'composables/getUrl'; import { getUrl } from 'composables/getUrl';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { computed, onMounted } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import ClaimDescriptor from './ClaimDescriptor.vue'; import ClaimDescriptor from './ClaimDescriptor.vue';
import { onMounted } from 'vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -21,16 +22,6 @@ const $props = defineProps({
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const claimSections = [
{ name: 'Notes', url: '/note/index', icon: 'draft' },
{ name: 'Development', url: '/development', icon: 'vn:traceability' },
{ name: 'Action', url: '/action', icon: 'vn:actions' },
];
let salixUrl;
onMounted(async () => {
salixUrl = await getUrl(`claim/${entityId.value}`);
});
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
@ -46,27 +37,16 @@ onMounted(async () => {
<ClaimDescriptor /> <ClaimDescriptor />
<QSeparator /> <QSeparator />
<LeftMenu source="card" /> <LeftMenu source="card" />
<QSeparator />
<QList>
<QItem
v-for="section in claimSections"
:key="section.name"
active-class="text-primary"
:href="salixUrl + section.url"
clickable
v-ripple
>
<QItemSection avatar>
<QIcon :name="section.icon" />
</QItemSection>
<QItemSection> {{ t(section.name) }} </QItemSection>
</QItem>
</QList>
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
<QPage class="q-pa-md"> <QPage>
<RouterView></RouterView> <QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>
@ -77,6 +57,5 @@ es:
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
Details: Detalles Details: Detalles
Notes: Notas Notes: Notas
Development: Trazabilidad
Action: Acción Action: Acción
</i18n> </i18n>

View File

@ -1,12 +1,16 @@
<script setup> <script setup>
import { computed } from 'vue'; import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import { useState } from 'src/composables/useState';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue'; import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -17,6 +21,7 @@ const $props = defineProps({
}); });
const route = useRoute(); const route = useRoute();
const state = useState();
const { t } = useI18n(); const { t } = useI18n();
const entityId = computed(() => { const entityId = computed(() => {
@ -25,11 +30,29 @@ const entityId = computed(() => {
const filter = { const filter = {
include: [ include: [
{ relation: 'client' }, {
{ relation: 'claimState' }, relation: 'client',
scope: {
include: { relation: 'salesPersonUser' },
},
},
{ {
relation: 'claimState', relation: 'claimState',
}, },
{
relation: 'ticket',
scope: {
include: [
{ relation: 'zone' },
{
relation: 'address',
scope: {
include: { relation: 'province' },
},
},
],
},
},
{ {
relation: 'worker', relation: 'worker',
scope: { scope: {
@ -39,11 +62,21 @@ const filter = {
], ],
}; };
const STATE_COLOR = {
pending: 'positive',
managed: 'warning',
resolved: 'negative',
};
function stateColor(code) { function stateColor(code) {
if (code === 'pending') return 'green'; return STATE_COLOR[code];
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
} }
const data = ref(useCardDescription());
const setData = (entity) => {
if (!entity) return;
data.value = useCardDescription(entity.client.name, entity.id);
state.set('ClaimDescriptor', entity);
};
</script> </script>
<template> <template>
@ -52,54 +85,59 @@ function stateColor(code) {
:url="`Claims/${entityId}`" :url="`Claims/${entityId}`"
:filter="filter" :filter="filter"
module="Claim" module="Claim"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="claimData"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">
<ClaimDescriptorMenu :claim="entity" /> <ClaimDescriptorMenu :claim="entity" />
</template> </template>
<template #description="{ entity }">
<span>
{{ entity.client.name }}
<QTooltip>{{ entity.client.name }}</QTooltip>
</span>
</template>
<template #body="{ entity }"> <template #body="{ entity }">
<QList> <VnLv :label="t('claim.card.created')" :value="toDate(entity.created)" />
<QItem> <VnLv v-if="entity.claimState" :label="t('claim.card.state')">
<QItemSection> <template #value>
<QItemLabel caption>{{ t('claim.card.created') }}</QItemLabel> <QBadge :color="stateColor(entity.claimState.code)" dense>
<QItemLabel>{{ toDate(entity.created) }}</QItemLabel> {{ entity.claimState.description }}
</QItemSection> </QBadge>
<QItemSection v-if="entity.claimState"> </template>
<QItemLabel caption>{{ t('claim.card.state') }}</QItemLabel> </VnLv>
<QItemLabel> <VnLv :label="t('claim.card.ticketId')">
<QBadge :color="stateColor(entity.claimState.code)" dense> <template #value>
{{ entity.claimState.description }} <span class="link">
</QBadge> {{ entity.ticketFk }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('claim.card.ticketId') }}
</QItemLabel>
<QItemLabel>
<span class="link">
{{ entity.ticketFk }}
<TicketDescriptorProxy :id="entity.ticketFk" />
</span>
</QItemLabel>
</QItemSection>
<QItemSection v-if="entity.worker">
<QItemLabel caption>
{{ t('claim.card.assignedTo') }}
</QItemLabel>
<QItemLabel>{{ entity.worker.user.name }}</QItemLabel>
</QItemSection>
</QItem>
</QList>
<TicketDescriptorProxy :id="entity.ticketFk" />
</span>
</template>
</VnLv>
<VnLv
v-if="entity.worker"
:label="t('claim.card.assignedTo')"
:value="entity.worker.user.name"
>
<template #value>
<span class="link">
{{ entity.worker.user.name }}
<WorkerDescriptorProxy :id="entity.worker.userFk" />
</span>
</template>
</VnLv>
<VnLv :label="t('claim.card.commercial')">
<template #value>
<span class="link">
{{ entity.client?.salesPersonUser?.name }}
<WorkerDescriptorProxy :id="entity.client?.salesPersonFk" />
</span>
</template>
</VnLv>
<VnLv
:label="t('claim.card.province')"
:value="entity.ticket?.address?.province?.name"
/>
<VnLv :label="t('claim.card.zone')" :value="entity.ticket?.zone?.name" />
</template>
<template #actions="{ entity }">
<QCardActions> <QCardActions>
<QBtn <QBtn
size="md" size="md"
@ -121,3 +159,8 @@ function stateColor(code) {
</template> </template>
</CardDescriptor> </CardDescriptor>
</template> </template>
<style scoped>
.q-item__label {
margin-top: 0;
}
</style>

View File

@ -0,0 +1,260 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import CrudModel from 'components/CrudModel.vue';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import { getUrl } from 'composables/getUrl';
import { tMobile } from 'composables/tMobile';
import router from 'src/router';
const route = useRoute();
const { t } = useI18n();
const claimDevelopmentForm = ref();
const claimReasons = ref([]);
const claimResults = ref([]);
const claimResponsibles = ref([]);
const claimRedeliveries = ref([]);
const workers = ref([]);
const selected = ref([]);
const saveButtonRef = ref();
let salixUrl;
onMounted(async () => {
salixUrl = await getUrl(`claim/${route.params.id}`);
});
const developmentsFilter = {
fields: [
'id',
'claimFk',
'claimReasonFk',
'claimResultFk',
'claimResponsibleFk',
'workerFk',
'claimRedeliveryFk',
],
where: {
claimFk: route.params.id,
},
};
const columns = computed(() => [
{
name: 'claimReason',
label: t('Reason'),
field: (row) => row.claimReasonFk,
sortable: true,
options: claimReasons.value,
required: true,
model: 'claimReasonFk',
optionValue: 'id',
optionLabel: 'description',
tabIndex: 1,
},
{
name: 'claimResult',
label: t('Result'),
field: (row) => row.claimResultFk,
sortable: true,
options: claimResults.value,
required: true,
model: 'claimResultFk',
optionValue: 'id',
optionLabel: 'description',
tabIndex: 2,
},
{
name: 'claimResponsible',
label: t('Responsible'),
field: (row) => row.claimResponsibleFk,
sortable: true,
options: claimResponsibles.value,
required: true,
model: 'claimResponsibleFk',
optionValue: 'id',
optionLabel: 'description',
tabIndex: 3,
},
{
name: 'worker',
label: t('Worker'),
field: (row) => row.workerFk,
sortable: true,
options: workers.value,
model: 'workerFk',
optionValue: 'id',
optionLabel: 'nickname',
tabIndex: 4,
},
{
name: 'claimRedelivery',
label: t('Redelivery'),
field: (row) => row.claimRedeliveryFk,
sortable: true,
options: claimRedeliveries.value,
required: true,
model: 'claimRedeliveryFk',
optionValue: 'id',
optionLabel: 'description',
tabIndex: 5,
},
]);
</script>
<template>
<FetchData
url="ClaimReasons"
order="description"
@on-fetch="(data) => (claimReasons = data)"
auto-load
/>
<FetchData
url="ClaimResults"
order="description"
@on-fetch="(data) => (claimResults = data)"
auto-load
/>
<FetchData
url="ClaimResponsibles"
order="description"
@on-fetch="(data) => (claimResponsibles = data)"
auto-load
/>
<FetchData
url="ClaimRedeliveries"
order="description"
@on-fetch="(data) => (claimRedeliveries = data)"
auto-load
/>
<FetchData
url="Workers/search"
:where="{ active: 1 }"
order="name ASC"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<CrudModel
data-key="ClaimDevelopments"
url="ClaimDevelopments"
model="claimDevelopment"
:filter="developmentsFilter"
ref="claimDevelopmentForm"
:data-required="{ claimFk: route.params.id }"
v-model:selected="selected"
auto-load
@save-changes="$router.push(`/claim/${route.params.id}/action`)"
:default-save="false"
>
<template #body="{ rows }">
<QTable
:columns="columns"
:rows="rows"
:pagination="{ rowsPerPage: 0 }"
row-key="$index"
selection="multiple"
hide-pagination
v-model:selected="selected"
:grid="$q.screen.lt.md"
>
<template #body-cell="{ row, col }">
<QTd
auto-width
@keyup.ctrl.enter.stop="claimDevelopmentForm.saveChanges()"
>
<VnSelectFilter
:label="col.label"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:autofocus="col.tabIndex == 1"
input-debounce="0"
hide-selected
>
<template #option="scope" v-if="col.name == 'worker'">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt?.nickname }}
{{ scope.opt?.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QTd>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard
bordered
flat
@keyup.ctrl.enter.stop="claimDevelopmentForm?.saveChanges()"
>
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList dense>
<QItem v-for="col in props.cols" :key="col.name">
<QItemSection>
<VnSelectFilter
:label="col.label"
v-model="props.row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
dense
input-debounce="0"
:autofocus="col.tabIndex == 1"
hide-selected
/>
</QItemSection>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
<template #moreAfterActions>
<QBtn
:label="tMobile('globals.save')"
ref="saveButtonRef"
color="primary"
icon="save"
:disable="!claimDevelopmentForm?.hasChanges"
@click="claimDevelopmentForm?.onSubmit"
:title="t('globals.save')"
/>
</template>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
fab
color="primary"
icon="add"
@keydown.tab.prevent="saveButtonRef.$el.focus()"
@click="claimDevelopmentForm.insert()"
/>
</QPageSticky>
</template>
<style lang="scss" scoped>
.grid-style-transition {
transition: transform 0.28s, background-color 0.28s;
}
</style>
<i18n>
es:
Reason: Motivo
Result: Consecuencia
Responsible: Responsable
Worker: Trabajador
Redelivery: Devolución
</i18n>

View File

@ -14,6 +14,13 @@ const route = useRoute();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const $props = defineProps({
ticketId: {
type: Number,
required: true,
},
});
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'delivered', name: 'delivered',
@ -99,11 +106,11 @@ function cancel() {
</script> </script>
<template> <template>
<FetchData <FetchData
url="Sales/getClaimableFromTicket?ticketFk=16" :url="`Sales/getClaimableFromTicket?ticketFk=${$props.ticketId}`"
@on-fetch="(data) => (claimableSales = data)" @on-fetch="(data) => (claimableSales = data)"
auto-load auto-load
/> />
<QDialog ref="dialogRef" persistent> <QDialog ref="dialogRef">
<QCard> <QCard>
<QCardSection class="row items-center"> <QCardSection class="row items-center">
<span class="text-h6 text-grey">{{ t('Available sales lines') }}</span> <span class="text-h6 text-grey">{{ t('Available sales lines') }}</span>

View File

@ -0,0 +1,37 @@
<script setup>
import { useRoute } from 'vue-router';
import { useState } from 'src/composables/useState';
import VnNotes from 'src/components/ui/VnNotes.vue';
const route = useRoute();
const state = useState();
const user = state.getUser();
const id = route.params.id;
const claimFilter = {
where: { claimFk: id },
fields: ['created', 'workerFk', 'text'],
include: {
relation: 'worker',
scope: {
fields: ['id', 'firstName', 'lastName'],
},
},
};
const body = {
claimFk: id,
workerFk: user.value.id,
};
</script>
<template>
<div class="column items-center">
<VnNotes
:add-note="true"
:id="id"
url="claimObservations"
:filter="claimFilter"
:body="body"
/>
</div>
</template>

View File

@ -4,7 +4,6 @@ import { ref, computed } from 'vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useSession } from 'composables/useSession'; import { useSession } from 'composables/useSession';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
@ -12,7 +11,6 @@ import FetchData from 'components/FetchData.vue';
const router = useRouter(); const router = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore();
const session = useSession(); const session = useSession();
const token = session.getToken(); const token = session.getToken();
@ -237,59 +235,20 @@ function onDrag() {
</div> </div>
</div> </div>
<Teleport <QPageSticky position="bottom-right" :offset="[25, 25]">
v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile" <label for="fileInput">
to="#actions-prepend" <QBtn fab @click="inputFile.nativeEl.click()" icon="add" color="primary">
> <QInput
<div class="row q-gutter-x-sm"> ref="inputFile"
<label for="fileInput"> type="file"
<QBtn style="display: none"
@click="inputFile.nativeEl.click()" multiple
icon="add" v-model="files"
color="primary" @update:model-value="create()"
dense />
rounded <QTooltip bottom> {{ t('globals.add') }} </QTooltip>
> </QBtn>
<QInput </label>
ref="inputFile"
type="file"
style="display: none"
multiple
v-model="files"
@update:model-value="create()"
/>
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
</QBtn>
</label>
<QSeparator vertical />
</div>
</Teleport>
<QPageSticky
v-if="quasar.platform.is.mobile"
position="bottom"
:offset="[0, 0]"
expand
>
<QToolbar class="bg-primary text-white q-pa-none">
<QTabs class="full-width" align="justify" inline-label narrow-indicator>
<QTab
@click="inputFile.nativeEl.click()"
icon="add_circle"
:label="t('globals.add')"
>
<QInput
ref="inputFile"
type="file"
style="display: none"
multiple
v-model="files"
@update:model-value="create()"
/>
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
</QTab>
</QTabs>
</QToolbar>
</QPageSticky> </QPageSticky>
<!-- MULTIMEDIA DIALOG START--> <!-- MULTIMEDIA DIALOG START-->

View File

@ -1,48 +1,34 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { ref } from 'vue'; import { watch, ref, computed, onUnmounted, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router'; import CrudModel from 'components/CrudModel.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import FetchData from 'components/FetchData.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute(); const state = useState();
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const selected = ref([]);
const arrayData = useArrayData('ClaimRma'); const claimRmaRef = ref();
const claim = computed(() => state.get('ClaimDescriptor'));
const claim = ref(); const claimRmaFilter = {
const claimFilter = { include: {
fields: ['rma'], relation: 'worker',
}; scope: {
include: {
async function onFetch(data) { relation: 'user',
claim.value = data;
const filter = {
include: {
relation: 'worker',
scope: {
include: {
relation: 'user',
},
}, },
}, },
order: 'created DESC', },
where: { order: 'created DESC',
code: claim.value.rma, where: {
}, code: claim.value?.rma,
}; },
};
arrayData.applyFilter({ filter });
}
async function addRow() { async function addRow() {
if (!claim.value.rma) { if (!claim.value.rma) {
@ -56,7 +42,7 @@ async function addRow() {
}; };
await axios.post(`ClaimRmas`, formData); await axios.post(`ClaimRmas`, formData);
await arrayData.refresh(); await claimRmaRef.value.reload();
quasar.notify({ quasar.notify({
type: 'positive', type: 'positive',
@ -65,38 +51,33 @@ async function addRow() {
}); });
} }
function confirmRemove(id) { onMounted(() => {
quasar if (claim.value) claimRmaRef.value.reload();
.dialog({ });
component: VnConfirm, watch(
componentProps: { claim,
data: { id }, () => {
promise: remove, claimRmaRef.value.reload();
}, },
}) { deep: true }
.onOk(async () => await arrayData.refresh()); );
}
async function remove({ id }) {
await axios.delete(`ClaimRmas/${id}`);
quasar.notify({
type: 'positive',
message: t('globals.rowRemoved'),
});
}
</script> </script>
<template> <template>
<FetchData
:url="`Claims/${route.params.id}`"
:filter="claimFilter"
@on-fetch="onFetch"
auto-load
/>
<div class="column items-center"> <div class="column items-center">
<div class="list"> <div class="list">
<VnPaginate data-key="ClaimRma" url="ClaimRmas"> <CrudModel
data-key="ClaimRma"
url="ClaimRmas"
model="ClaimRma"
:filter="claimRmaFilter"
v-model:selected="selected"
ref="claimRmaRef"
:default-save="false"
:default-reset="false"
:default-remove="false"
>
<template #body="{ rows }"> <template #body="{ rows }">
<QCard class="card"> <QCard>
<template v-for="(row, index) of rows" :key="row.id"> <template v-for="(row, index) of rows" :key="row.id">
<QItem class="q-pa-none items-start"> <QItem class="q-pa-none items-start">
<QItemSection class="q-pa-md"> <QItemSection class="q-pa-md">
@ -107,7 +88,7 @@ async function remove({ id }) {
{{ t('claim.rma.user') }} {{ t('claim.rma.user') }}
</QItemLabel> </QItemLabel>
<QItemLabel> <QItemLabel>
{{ row.worker.user.name }} {{ row?.worker?.user?.name }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -133,7 +114,7 @@ async function remove({ id }) {
round round
color="orange" color="orange"
icon="vn:bin" icon="vn:bin"
@click="confirmRemove(row.id)" @click="claimRmaRef.remove([row])"
> >
<QTooltip>{{ t('globals.remove') }}</QTooltip> <QTooltip>{{ t('globals.remove') }}</QTooltip>
</QBtn> </QBtn>
@ -143,33 +124,11 @@ async function remove({ id }) {
</template> </template>
</QCard> </QCard>
</template> </template>
</VnPaginate> </CrudModel>
</div> </div>
</div> </div>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<Teleport <QBtn fab color="primary" icon="add" @click="addRow()" />
v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile"
to="#actions-prepend"
>
<div class="row q-gutter-x-sm">
<QBtn @click="addRow()" icon="add" color="primary" dense rounded>
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
</QBtn>
<QSeparator vertical />
</div>
</Teleport>
<QPageSticky
v-if="quasar.platform.is.mobile"
position="bottom"
:offset="[0, 0]"
expand
>
<QToolbar class="bg-primary text-white q-pa-none">
<QTabs class="full-width" align="justify" inline-label narrow-indicator>
<QTab @click="addRow()" icon="add_circle" :label="t('globals.add')" />
</QTabs>
</QToolbar>
</QPageSticky> </QPageSticky>
</template> </template>
@ -178,16 +137,6 @@ async function remove({ id }) {
width: 100%; width: 100%;
max-width: 60em; max-width: 60em;
} }
.q-toolbar {
background-color: $grey-9;
}
.sticky-page {
padding-top: 66px;
}
.q-page-sticky {
z-index: 2998;
}
</style> </style>
<i18n> <i18n>

View File

@ -1,12 +1,14 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { getUrl } from 'src/composables/getUrl';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnLv from 'src/components/ui/VnLv.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -22,6 +24,14 @@ const $props = defineProps({
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
const claimUrl = ref();
const salixUrl = ref();
onMounted(async () => {
salixUrl.value = await getUrl('');
claimUrl.value = salixUrl.value + `claim/${entityId.value}/`;
});
const detailsColumns = ref([ const detailsColumns = ref([
{ {
name: 'item', name: 'item',
@ -75,10 +85,15 @@ const detailsColumns = ref([
}, },
]); ]);
const STATE_COLOR = {
pending: 'positive',
managed: 'warning',
resolved: 'negative',
};
function stateColor(code) { function stateColor(code) {
if (code === 'pending') return 'green'; return STATE_COLOR[code];
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
} }
const developmentColumns = ref([ const developmentColumns = ref([
@ -155,78 +170,52 @@ function openDialog(dmsId) {
<template #header="{ entity: { claim } }"> <template #header="{ entity: { claim } }">
{{ claim.id }} - {{ claim.client.name }} {{ claim.id }} - {{ claim.client.name }}
</template> </template>
<template #body="{ entity: { developments, observations, claim, salesClaimed } }"> <template #body="{ entity: { claim, salesClaimed, developments } }">
<QCardSection class="row q-pa-none q-col-gutter-md"> <QCard class="vn-one">
<div class="col"> <a class="header" :href="`#/claim/${entityId}/basic-data`">
<QList> {{ t('claim.pageTitles.basicData') }}
<QItem> <QIcon name="open_in_new" color="primary" />
<QItemSection> </a>
<QItemLabel caption> <VnLv
{{ t('claim.summary.created') }} :label="t('claim.summary.created')"
</QItemLabel> :value="toDate(claim.created)"
<QItemLabel>{{ toDate(claim.created) }}</QItemLabel> />
</QItemSection> <VnLv :label="t('claim.summary.state')">
<QItemSection v-if="claim.claimState"> <template #value>
<QItemLabel caption> <QChip :color="stateColor(claim.claimState.code)" dense>
{{ t('claim.summary.state') }} {{ claim.claimState.description }}
</QItemLabel> </QChip>
<QItemLabel> </template>
<QChip </VnLv>
:color="stateColor(claim.claimState.code)" <VnLv :label="t('claim.summary.assignedTo')">
dense <template #value>
> <span class="link">
{{ claim.claimState.description }} {{ claim.worker.user.nickname }}
</QChip> <WorkerDescriptorProxy :id="claim.workerFk" />
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection v-if="claim.worker && claim.worker.user">
<QItemLabel caption>
{{ t('claim.summary.assignedTo') }}
</QItemLabel>
<QItemLabel>
<span class="link">
{{ claim.worker.user.nickname }}
<WorkerDescriptorProxy :id="claim.workerFk" />
</span>
</QItemLabel>
</QItemSection>
<QItemSection
v-if="claim.client && claim.client.salesPersonUser"
>
<QItemLabel caption>
{{ t('claim.summary.attendedBy') }}
</QItemLabel>
<QItemLabel>
<span class="link">
{{ claim.client.salesPersonUser.name }}
<WorkerDescriptorProxy
:id="claim.client.salesPersonFk"
/>
</span>
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</div>
</QCardSection>
<QCardSection class="q-pa-md" v-if="observations.length > 0">
<h6>{{ t('claim.summary.notes') }}</h6>
<div class="note-list" v-for="note in observations" :key="note.id">
<div class="note-caption">
<span
>{{ note.worker.firstName }} {{ note.worker.lastName }}
</span> </span>
<span>{{ toDate(note.created) }}</span> </template>
</div> </VnLv>
<div class="note-text"> <VnLv :label="t('claim.summary.attendedBy')">
<span>{{ note.text }}</span> <template #value>
</div> <span class="link">
</div> {{ claim.client.salesPersonUser.name }}
</QCardSection> <WorkerDescriptorProxy :id="claim.client.salesPersonFk" />
<QCardSection class="q-pa-md" v-if="salesClaimed.length > 0"> </span>
<h6>{{ t('claim.summary.details') }}</h6> </template>
</VnLv>
</QCard>
<QCard class="vn-one">
<a class="header" :href="claimUrl + 'note/index'">
{{ t('claim.summary.notes') }}
<QIcon name="open_in_new" color="primary" />
</a>
<!-- Use VnNotes and maybe VirtualScroll-->
</QCard>
<QCard class="vn-max" v-if="salesClaimed.length > 0">
<a class="header" :href="claimUrl + 'note/index'">
{{ t('claim.summary.details') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QTable :columns="detailsColumns" :rows="salesClaimed" flat> <QTable :columns="detailsColumns" :rows="salesClaimed" flat>
<template #header="props"> <template #header="props">
<QTr :props="props"> <QTr :props="props">
@ -236,16 +225,19 @@ function openDialog(dmsId) {
</QTr> </QTr>
</template> </template>
</QTable> </QTable>
</QCardSection> </QCard>
<QCardSection class="q-pa-md" v-if="claimDms.length > 0"> <QCard class="vn-max" v-if="claimDms.length > 0">
<h6>{{ t('claim.summary.photos') }}</h6> <a class="header" :href="`#/claim/${entityId}/photos`">
{{ t('claim.summary.photos') }}
<QIcon name="open_in_new" color="primary" />
</a>
<div class="container"> <div class="container">
<div class="multimediaParent bg-transparent"> <div
<div class="multimedia-container"
v-for="(media, index) of claimDms" v-for="(media, index) of claimDms"
:key="index" :key="index"
class="relative-position" >
> <div class="relative-position">
<QIcon <QIcon
name="play_circle" name="play_circle"
color="primary" color="primary"
@ -275,9 +267,12 @@ function openDialog(dmsId) {
</div> </div>
</div> </div>
</div> </div>
</QCardSection> </QCard>
<QCardSection class="q-pa-md" v-if="developments.length > 0"> <QCard class="vn-two" v-if="developments.length > 0">
<h6>{{ t('claim.summary.development') }}</h6> <a class="header" :href="claimUrl + 'development'">
{{ t('claim.summary.development') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QTable :columns="developmentColumns" :rows="developments" flat> <QTable :columns="developmentColumns" :rows="developments" flat>
<template #header="props"> <template #header="props">
<QTr :props="props"> <QTr :props="props">
@ -287,11 +282,13 @@ function openDialog(dmsId) {
</QTr> </QTr>
</template> </template>
</QTable> </QTable>
</QCardSection> </QCard>
<QCardSection class="q-pa-md"> <QCard class="vn-max" v-if="developments.length > 0">
<h6>{{ t('claim.summary.actions') }}</h6> <a class="header" :href="claimUrl + 'action'">
<QSeparator /> {{ t('claim.summary.actions') }}
<div id="slider-container"> <QIcon name="open_in_new" color="primary" />
</a>
<div id="slider-container" class="q-px-xl q-py-md">
<QSlider <QSlider
v-model="claim.responsibility" v-model="claim.responsibility"
label label
@ -308,7 +305,21 @@ function openDialog(dmsId) {
readonly readonly
/> />
</div> </div>
</QCardSection> </QCard>
<!-- <QCardSection class="q-pa-md" v-if="observations.length > 0">
<h6>{{ t('claim.summary.notes') }}</h6>
<div class="note-list" v-for="note in observations" :key="note.id">
<div class="note-caption">
<span
>{{ note.worker.firstName }} {{ note.worker.lastName }}
</span>
<span>{{ toDate(note.created) }}</span>
</div>
<div class="note-text">
<span>{{ note.text }}</span>
</div>
</div>
</QCardSection> -->
<QDialog <QDialog
v-model="multimediaDialog" v-model="multimediaDialog"
transition-show="slide-up" transition-show="slide-up"
@ -352,22 +363,19 @@ function openDialog(dmsId) {
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.container {
min-width: 80%;
}
.q-dialog__inner--minimized > div { .q-dialog__inner--minimized > div {
max-width: 80%; max-width: 80%;
} }
.container {
.multimediaParent { display: flex;
display: grid; flex-direction: row;
grid-template-columns: repeat(auto-fill, minmax(500px, 1fr)); flex-wrap: wrap;
gap: 15px;
grid-auto-rows: auto; flex-basis: 30%;
}
grid-gap: 1rem; .multimedia-container {
flex: 1 0 21%;
} }
.multimedia { .multimedia {
transition: all 0.5s; transition: all 0.5s;
opacity: 1; opacity: 1;
@ -395,18 +403,4 @@ function openDialog(dmsId) {
.zindex { .zindex {
z-index: 1; z-index: 1;
} }
.note-list {
width: 100%;
border: 0.1rem solid $grey-7;
padding: 0.5rem;
margin-bottom: 0.5rem;
}
.note-caption {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
color: $grey-7;
}
</style> </style>

View File

@ -3,6 +3,7 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -60,7 +61,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="workers"> <QItemSection v-if="workers">
<QSelect <VnSelectFilter
:label="t('Salesperson')" :label="t('Salesperson')"
v-model="params.salesPersonFk" v-model="params.salesPersonFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -79,7 +80,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="workers"> <QItemSection v-if="workers">
<QSelect <VnSelectFilter
:label="t('Attender')" :label="t('Attender')"
v-model="params.attenderFk" v-model="params.attenderFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -98,7 +99,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="workers"> <QItemSection v-if="workers">
<QSelect <VnSelectFilter
:label="t('Responsible')" :label="t('Responsible')"
v-model="params.claimResponsibleFk" v-model="params.claimResponsibleFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -117,7 +118,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="states"> <QItemSection v-if="states">
<QSelect <VnSelectFilter
:label="t('State')" :label="t('State')"
v-model="params.claimStateFk" v-model="params.claimStateFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"

View File

@ -9,18 +9,22 @@ import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import ClaimFilter from './ClaimFilter.vue'; import ClaimFilter from './ClaimFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const router = useRouter(); const router = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const STATE_COLOR = {
pending: 'positive',
managed: 'warning',
resolved: 'negative',
};
function stateColor(code) { function stateColor(code) {
if (code === 'pending') return 'green'; return STATE_COLOR[code];
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
} }
function navigate(id) { function navigate(id) {
router.push({ path: `/claim/${id}` }); router.push({ path: `/claim/${id}` });
} }
@ -74,116 +78,62 @@ function viewSummary(id) {
auto-load auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QCard class="card q-mb-md" v-for="row of rows" :key="row.id"> <CardList
<QItem v-for="row of rows"
class="q-pa-none items-start cursor-pointer q-hoverable" :key="row.id"
v-ripple :title="row.clientName"
clickable @click="navigate(row.id)"
> >
<QItemSection class="q-pa-md" @click="navigate(row.id)"> <template #list-items>
<div class="text-h6 link"> <VnLv label="ID" :value="row.id" />
{{ row.clientName }} <VnLv
</div> :label="t('claim.list.customer')"
<QItemLabel caption>#{{ row.id }}</QItemLabel> :value="row.clientName"
<QList> />
<QItem class="q-pa-none"> <VnLv
<QItemSection> :label="t('claim.list.assignedTo')"
<QItemLabel caption> :value="row.workerName"
{{ t('claim.list.customer') }} />
</QItemLabel> <VnLv
<QItemLabel> :label="t('claim.list.created')"
{{ row.clientName }} :value="toDate(row.created)"
</QItemLabel> />
</QItemSection> <VnLv :label="t('claim.list.state')">
<QItemSection> <template #value>
<QItemLabel caption> <QBadge
{{ t('claim.list.assignedTo') }} :color="stateColor(row.stateCode)"
</QItemLabel> class="q-ma-none"
<QItemLabel> dense
{{ row.workerName }} >
</QItemLabel> {{ row.stateDescription }}
</QItemSection> </QBadge>
</QItem> </template>
<QItem class="q-pa-none"> </VnLv>
<QItemSection> </template>
<QItemLabel caption> <template #actions>
{{ t('claim.list.created') }} <QBtn
</QItemLabel> flat
<QItemLabel> icon="arrow_circle_right"
{{ toDate(row.created) }} @click.stop="navigate(row.id)"
</QItemLabel> >
</QItemSection> <QTooltip>
<QItemSection> {{ t('components.smartCard.openCard') }}
<QItemLabel caption> </QTooltip>
{{ t('claim.list.state') }} </QBtn>
</QItemLabel> <QBtn flat icon="preview" @click.stop="viewSummary(row.id)">
<QItemLabel> <QTooltip>
<QBadge {{ t('components.smartCard.openSummary') }}
:color="stateColor(row.stateCode)" </QTooltip>
class="q-ma-none" </QBtn>
dense <QBtn flat icon="vn:client" @click.stop>
> <QTooltip>
{{ row.stateDescription }} {{ t('components.smartCard.viewDescription') }}
</QBadge> </QTooltip>
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QItemSection>
<QSeparator vertical />
<QCardActions vertical class="justify-between">
<!-- <QBtn color="grey-7" round flat icon="more_vert">
<QTooltip>{{ t('customer.list.moreOptions') }}</QTooltip>
<QMenu cover auto-close>
<QList>
<QItem clickable>
<QItemSection avatar>
<QIcon name="add" />
</QItemSection>
<QItemSection>Add a note</QItemSection>
</QItem>
<QItem clickable>
<QItemSection avatar>
<QIcon name="logs" />
</QItemSection>
<QItemSection>Display claim logs</QItemSection>
</QItem>
</QList>
</QMenu>
</QBtn> -->
<QBtn <CustomerDescriptorProxy :id="row.clientFk" />
flat </QBtn>
round </template>
color="orange" </CardList>
icon="arrow_circle_right"
@click="navigate(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openCard') }}
</QTooltip>
</QBtn>
<QBtn
flat
round
color="grey-7"
icon="preview"
@click="viewSummary(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
<QBtn flat round color="grey-7" icon="vn:client">
<QTooltip>
{{ t('components.smartCard.viewDescription') }}
</QTooltip>
<CustomerDescriptorProxy :id="row.clientFk" />
</QBtn>
</QCardActions>
</QItem>
</QCard>
</template> </template>
</VnPaginate> </VnPaginate>
</div> </div>

View File

@ -6,6 +6,7 @@ import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -58,121 +59,109 @@ const filterOptions = {
@on-fetch="(data) => (businessTypes = data)" @on-fetch="(data) => (businessTypes = data)"
auto-load auto-load
/> />
<div class="column items-center">
<QCard>
<FormModel :url="`Clients/${route.params.id}`" model="customer">
<template #form="{ data, validate, filter }">
<div class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
v-model="data.socialName"
:label="t('customer.basicData.socialName')"
:rules="validate('client.socialName')"
autofocus
/>
</div>
<div class="col">
<QSelect
v-model="data.businessTypeFk"
:options="businessTypes"
option-value="code"
option-label="description"
emit-value
:label="t('customer.basicData.businessType')"
map-options
:rules="validate('client.businessTypeFk')"
:input-debounce="0"
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
v-model="data.contact"
:label="t('customer.basicData.contact')"
:rules="validate('client.contact')"
clearable
/>
</div>
<div class="col">
<QInput
v-model="data.email"
type="email"
:label="t('customer.basicData.email')"
:rules="validate('client.email')"
clearable
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
v-model="data.phone"
:label="t('customer.basicData.phone')"
:rules="validate('client.phone')"
clearable
/>
</div>
<div class="col">
<QInput
v-model="data.mobile"
:label="t('customer.basicData.mobile')"
:rules="validate('client.mobile')"
clearable
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<QSelect
v-model="data.salesPersonFk"
:options="workers"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.salesPerson')"
map-options
use-input
@filter="
(value, update) =>
filter(value, update, filterOptions)
"
:rules="validate('client.salesPersonFk')"
:input-debounce="0"
>
<template #prepend>
<QAvatar color="orange">
<QImg
v-if="data.salesPersonFk"
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
/>
</QAvatar>
</template>
</QSelect>
</div>
<div class="col">
<QSelect
v-model="data.contactChannelFk"
:options="contactChannels"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.contactChannel')"
map-options
:rules="validate('client.contactChannelFk')"
:input-debounce="0"
/>
</div>
</div>
</template>
</FormModel>
</QCard>
</div>
</template>
<style lang="scss" scoped> <FormModel :url="`Clients/${route.params.id}`" model="customer">
.q-card { <template #form="{ data, validate, filter }">
width: 800px; <VnRow class="row q-gutter-md q-mb-md">
} <div class="col">
</style> <QInput
v-model="data.socialName"
:label="t('customer.basicData.socialName')"
:rules="validate('client.socialName')"
autofocus
/>
</div>
<div class="col">
<QSelect
v-model="data.businessTypeFk"
:options="businessTypes"
option-value="code"
option-label="description"
emit-value
:label="t('customer.basicData.businessType')"
map-options
:rules="validate('client.businessTypeFk')"
:input-debounce="0"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
v-model="data.contact"
:label="t('customer.basicData.contact')"
:rules="validate('client.contact')"
clearable
/>
</div>
<div class="col">
<QInput
v-model="data.email"
type="email"
:label="t('customer.basicData.email')"
:rules="validate('client.email')"
clearable
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
v-model="data.phone"
:label="t('customer.basicData.phone')"
:rules="validate('client.phone')"
clearable
/>
</div>
<div class="col">
<QInput
v-model="data.mobile"
:label="t('customer.basicData.mobile')"
:rules="validate('client.mobile')"
clearable
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QSelect
v-model="data.salesPersonFk"
:options="workers"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.salesPerson')"
map-options
use-input
@filter="(value, update) => filter(value, update, filterOptions)"
:rules="validate('client.salesPersonFk')"
:input-debounce="0"
>
<template #prepend>
<QAvatar color="orange">
<QImg
v-if="data.salesPersonFk"
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
/>
</QAvatar>
</template>
</QSelect>
</div>
<div class="col">
<QSelect
v-model="data.contactChannelFk"
:options="contactChannels"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.contactChannel')"
map-options
:rules="validate('client.contactChannelFk')"
:input-debounce="0"
/>
</div>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -1,11 +1,13 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
import CustomerDescriptor from './CustomerDescriptor.vue'; import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue'; import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
</script> </script>
<template> <template>
@ -25,8 +27,13 @@ const { t } = useI18n();
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
<QPage class="q-pa-md"> <QPage>
<RouterView></RouterView> <QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>

View File

@ -1,10 +1,12 @@
<script setup> <script setup>
import { computed } from 'vue'; import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -20,57 +22,39 @@ const { t } = useI18n();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.name, entity.id));
</script> </script>
<template> <template>
<CardDescriptor module="Customer" :url="`Clients/${entityId}/getCard`"> <CardDescriptor
module="Customer"
:url="`Clients/${entityId}/getCard`"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="customerData"
>
<template #body="{ entity }"> <template #body="{ entity }">
<QList dense> <VnLv v-if="entity.salesPersonUser" :label="t('customer.card.salesPerson')">
<QItem v-if="entity.salesPersonUser" class="row"> <template #value>
<QItemLabel class="col" caption> <span class="link">
{{ t('customer.card.salesPerson') }} {{ entity.salesPersonUser.name }}
</QItemLabel> <WorkerDescriptorProxy :id="entity.salesPersonFk" />
<QItemLabel class="col q-ma-none"> </span>
<span class="link"> </template>
{{ entity.salesPersonUser.name }} </VnLv>
<WorkerDescriptorProxy :id="entity.salesPersonFk" /> <VnLv :label="t('customer.card.credit')" :value="toCurrency(entity.credit)" />
</span> <VnLv
</QItemLabel> :label="t('customer.card.securedCredit')"
</QItem> :value="toCurrency(entity.creditInsurance)"
<QItem class="row"> />
<QItemLabel class="col" caption> <VnLv :label="t('customer.card.payMethod')" :value="entity.payMethod.name" />
{{ t('customer.card.credit') }} <VnLv :label="t('customer.card.debt')" :value="toCurrency(entity.debt)" />
</QItemLabel> </template>
<QItemLabel class="col q-ma-none"> <template #icons="{ entity }">
{{ toCurrency(entity.credit) }} <QCardActions>
</QItemLabel>
</QItem>
<QItem class="row">
<QItemLabel class="col" caption>
{{ t('customer.card.securedCredit') }}
</QItemLabel>
<QItemLabel class="col q-ma-none">
{{ toCurrency(entity.creditInsurance) }}
</QItemLabel>
</QItem>
<QItem v-if="entity.payMethod" class="row">
<QItemLabel class="col" caption>
{{ t('customer.card.payMethod') }}
</QItemLabel>
<QItemLabel class="col q-ma-none">
{{ entity.payMethod.name }}
</QItemLabel>
</QItem>
<QItem class="row">
<QItemLabel class="col" caption>
{{ t('customer.card.debt') }}
</QItemLabel>
<QItemLabel class="col q-ma-none">
{{ toCurrency(entity.debt) }}
</QItemLabel>
</QItem>
</QList>
<QCardActions class="q-gutter-md">
<QIcon <QIcon
v-if="entity.isActive == false" v-if="entity.isActive == false"
name="vn:disabled" name="vn:disabled"
@ -103,15 +87,9 @@ const entityId = computed(() => {
> >
<QTooltip>{{ t('customer.card.notChecked') }}</QTooltip> <QTooltip>{{ t('customer.card.notChecked') }}</QTooltip>
</QIcon> </QIcon>
<QIcon
v-if="entity.account && entity.account.active == false"
name="vn:noweb"
size="xs"
color="primary"
>
<QTooltip>{{ t('customer.card.noWebAccess') }}</QTooltip>
</QIcon>
</QCardActions> </QCardActions>
</template>
<template #actions="{ entity }">
<QCardActions> <QCardActions>
<QBtn <QBtn
:to="{ :to="{
@ -135,23 +113,10 @@ const entityId = computed(() => {
> >
<QTooltip>{{ t('invoiceOutList') }}</QTooltip> <QTooltip>{{ t('invoiceOutList') }}</QTooltip>
</QBtn> </QBtn>
<!--
<QBtn size="md" icon="vn:basketadd" color="primary">
<QTooltip>Order list</QTooltip>
</QBtn>
<QBtn size="md" icon="face" color="primary">
<QTooltip>View user</QTooltip>
</QBtn>
<QBtn size="md" icon="expand_more" color="primary">
<QTooltip>More options</QTooltip>
</QBtn> -->
</QCardActions> </QCardActions>
</template> </template>
</CardDescriptor> </CardDescriptor>
</template> </template>
<i18n> <i18n>
{ {
"en": { "en": {

View File

@ -1,9 +1,11 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toCurrency, toPercentage, toDate } from 'src/filters'; import { toCurrency, toPercentage, toDate } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -16,8 +18,13 @@ const $props = defineProps({
}); });
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
const summary = ref();
const customer = computed(() => summary.value.entity); const customer = computed(() => summary.value.entity);
const summary = ref();
const clientUrl = ref();
onMounted(async () => {
clientUrl.value = (await getUrl('client/')) + entityId.value + '/';
});
const balanceDue = computed(() => { const balanceDue = computed(() => {
return ( return (
@ -38,7 +45,7 @@ const priceIncreasingRate = computed(() => {
}); });
const debtWarning = computed(() => { const debtWarning = computed(() => {
return customer.value.debt.debt > customer.value.credit ? 'negative' : ''; return customer.value?.debt?.debt > customer.value.credit ? 'negative' : '';
}); });
const creditWarning = computed(() => { const creditWarning = computed(() => {
@ -53,478 +60,212 @@ const creditWarning = computed(() => {
<template> <template>
<CardSummary ref="summary" :url="`Clients/${entityId}/summary`"> <CardSummary ref="summary" :url="`Clients/${entityId}/summary`">
<template #body="{ entity }"> <template #body="{ entity }">
<QCardSection class="row q-pa-none QCol-gutter-md"> <QCard class="vn-one">
<div class="col"> <a class="header" :href="clientUrl + `basic-data`">
<QList dense> {{ t('customer.summary.basicData') }}
<QItemLabel header class="text-h6"> <QIcon name="open_in_new" color="primary" />
{{ t('customer.summary.basicData') }} </a>
<RouterLink <VnLv :label="t('customer.summary.customerId')" :value="entity.id" />
:to="{ <VnLv :label="t('customer.summary.name')" :value="entity.name" />
name: 'CustomerBasicData', <VnLv :label="t('customer.summary.contact')" :value="entity.contact" />
params: { id: entity.id }, <VnLv :label="t('customer.summary.phone')" :value="entity.phone" />
}" <VnLv :label="t('customer.summary.mobile')" :value="entity.mobile" />
target="_blank" <VnLv :label="t('customer.summary.email')" :value="entity.email" />
> <VnLv
<QIcon name="open_in_new" /> :label="t('customer.summary.salesPerson')"
</RouterLink> :value="entity?.salesPersonUser?.name"
</QItemLabel> />
<QSeparator class="q-mb-md" /> <VnLv
:label="t('customer.summary.contactChannel')"
:value="entity?.contactChannel?.name"
/>
<VnLv
:label="t('customer.summary.businessType')"
:value="entity.businessType.description"
/>
</QCard>
<QCard class="vn-one">
<a class="header" :href="clientUrl + `fiscal-data`">
{{ t('customer.summary.fiscalAddress') }}
<QIcon name="open_in_new" color="primary" />
</a>
<VnLv
:label="t('customer.summary.socialName')"
:value="entity.socialName"
/>
<VnLv :label="t('customer.summary.fiscalId')" :value="entity.fi" />
<VnLv :label="t('customer.summary.city')" :value="entity.city" />
<VnLv :label="t('customer.summary.postcode')" :value="entity.postcode" />
<QItem class="row col"> <VnLv
<QItemLabel class="col" caption> v-if="entity.province"
{{ t('customer.summary.customerId') }} :label="t('customer.summary.province')"
</QItemLabel> :value="entity.province.name"
<QItemLabel class="col q-ma-none"> />
{{ entity.id }} <VnLv
</QItemLabel> v-if="entity.country"
</QItem> :label="t('customer.summary.country')"
<QItem class="row col"> :value="entity.country.country"
<QItemLabel class="col" caption> />
{{ t('customer.summary.name') }} <VnLv :label="t('customer.summary.street')" :value="entity.street" />
</QItemLabel> </QCard>
<QItemLabel class="col q-ma-none"> <QCard class="vn-one">
{{ entity.name }} <a class="header link" :href="clientUrl + `fiscal-data`" link>
</QItemLabel> {{ t('customer.summary.fiscalAddress') }}
</QItem> <QIcon name="open_in_new" color="primary" />
<QItem class="row col"> </a>
<QItemLabel class="col" caption> <VnLv
{{ t('customer.summary.contact') }} :label="t('customer.summary.isEqualizated')"
</QItemLabel> :value="entity.isEqualizated"
<QItemLabel class="col q-ma-none"> />
{{ entity.contact }} <VnLv :label="t('customer.summary.isActive')" :value="entity.isActive" />
</QItemLabel> <VnLv
</QItem> :label="t('customer.summary.invoiceByAddress')"
:value="entity.hasToInvoiceByAddress"
/>
<VnLv
:label="t('customer.summary.verifiedData')"
:value="entity.isTaxDataChecked"
/>
<VnLv
:label="t('customer.summary.hasToInvoice')"
:value="entity.hasToInvoice"
/>
<VnLv
:label="t('customer.summary.notifyByEmail')"
:value="entity.isToBeMailed"
/>
<VnLv :label="t('customer.summary.vies')" :value="entity.isVies" />
</QCard>
<QCard class="vn-one">
<a class="header link" :href="clientUrl + `billing-data`" link>
{{ t('customer.summary.billingData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<VnLv
:label="t('customer.summary.payMethod')"
:value="entity.payMethod.name"
/>
<VnLv :label="t('customer.summary.bankAccount')" :value="entity.iban" />
<VnLv :label="t('customer.summary.dueDay')" :value="entity.dueDay" />
<VnLv :label="t('customer.summary.hasLcr')" :value="entity.hasLcr" />
<VnLv
:label="t('customer.summary.hasCoreVnl')"
:value="entity.hasCoreVnl"
/>
<VnLv
:label="t('customer.summary.hasB2BVnl')"
:value="entity.hasSepaVnl"
/>
</QCard>
<QCard class="vn-one" v-if="entity.defaultAddress">
<a class="header link" :href="clientUrl + `address/index`" link>
{{ t('customer.summary.consignee') }}
<QIcon name="open_in_new" color="primary" />
</a>
<VnLv
:label="t('customer.summary.addressName')"
:value="entity.defaultAddress.nickname"
/>
<VnLv
:label="t('customer.summary.addressCity')"
:value="entity.defaultAddress.city"
/>
<VnLv
:label="t('customer.summary.addressStreet')"
:value="entity.defaultAddress.street"
/>
</QCard>
<QCard class="vn-one" v-if="entity.account">
<a class="header link" :href="clientUrl + `web-access`">
{{ t('customer.summary.webAccess') }}
<QIcon name="open_in_new" color="primary" />
</a>
<VnLv
:label="t('customer.summary.username')"
:value="entity.account.name"
/>
<VnLv
:label="t('customer.summary.webAccess')"
:value="entity.account.active"
/>
</QCard>
<QCard class="vn-one" v-if="entity.account">
<div class="header">
{{ t('customer.summary.businessData') }}
</div>
<VnLv
:label="t('customer.summary.totalGreuge')"
:value="toCurrency(entity.totalGreuge)"
/>
<VnLv
:label="t('customer.summary.mana')"
:value="toCurrency(entity?.mana?.mana)"
/>
<VnLv
v-if="entity.claimsRatio"
:label="t('customer.summary.priceIncreasingRate')"
:value="toPercentage(priceIncreasingRate)"
/>
<VnLv
:label="t('customer.summary.averageInvoiced')"
: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">
<a
class="header link"
:href="`https://grafana.verdnatura.es/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk=${entityId}`"
link
>
{{ t('customer.summary.financialData') }}
<QIcon name="vn:grafana" color="primary" />
</a>
<VnLv
:label="t('customer.summary.risk')"
:value="toCurrency(entity?.debt?.debt)"
:class="debtWarning"
:info="t('customer.summary.riskInfo')"
/>
<QItem v-if="entity.salesPersonUser" class="row col"> <VnLv
<QItemLabel class="col" caption> :label="t('customer.summary.credit')"
{{ t('customer.summary.salesPerson') }} :value="toCurrency(entity.credit)"
</QItemLabel> :class="creditWarning"
<QItemLabel class="col q-ma-none"> :info="t('customer.summary.creditInfo')"
{{ entity.salesPersonUser.name }} />
</QItemLabel>
</QItem>
<QItem class="row col"> <VnLv
<QItemLabel class="col" caption> v-if="entity.creditInsurance"
{{ t('customer.summary.phone') }} :label="t('customer.summary.securedCredit')"
</QItemLabel> :value="toCurrency(entity.creditInsurance)"
<QItemLabel class="col q-ma-none"> :info="t('customer.summary.securedCreditInfo')"
{{ entity.phone }} />
</QItemLabel>
</QItem>
<QItem class="row col"> <VnLv
<QItemLabel class="col" caption> :label="t('customer.summary.balance')"
{{ t('customer.summary.mobile') }} :value="toCurrency(entity.sumRisk) || toCurrency(0)"
</QItemLabel> :info="t('customer.summary.balanceInfo')"
<QItemLabel class="col q-ma-none">{{ />
entity.mobile
}}</QItemLabel>
</QItem>
<QItem v-if="entity.contactChannel" class="row col"> <VnLv
<QItemLabel class="col" caption> v-if="entity.defaulters"
{{ t('customer.summary.contactChannel') }} :label="t('customer.summary.balanceDue')"
</QItemLabel> :value="toCurrency(balanceDue)"
<QItemLabel class="col q-ma-none"> :class="balanceDueWarning"
{{ entity.contactChannel.name }} :info="t('customer.summary.balanceDueInfo')"
</QItemLabel> />
</QItem> <VnLv
v-if="entity.recovery"
<QItem> :label="t('customer.summary.recoverySince')"
<QItemSection> :value="toDate(entity.recovery.started)"
<QItemLabel caption> />
{{ t('customer.summary.email') }} </QCard>
</QItemLabel>
<QItemLabel>{{ entity.email }}</QItemLabel>
</QItemSection>
</QItem>
</QList>
</div>
<div class="col">
<QList>
<QItemLabel header class="text-h6">
{{ t('customer.summary.fiscalAddress') }}
</QItemLabel>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.socialName') }}
</QItemLabel>
<QItemLabel>{{ entity.socialName }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.fiscalId') }}
</QItemLabel>
<QItemLabel>{{ entity.fi }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.postcode') }}
</QItemLabel>
<QItemLabel>{{ entity.postcode }}</QItemLabel>
</QItemSection>
</QItem>
<QItem v-if="entity.province">
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.province') }}
</QItemLabel>
<QItemLabel>{{ entity.province.name }}</QItemLabel>
</QItemSection>
</QItem>
<QItem v-if="entity.country">
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.country') }}
</QItemLabel>
<QItemLabel>{{ entity.country.country }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.street') }}
</QItemLabel>
<QItemLabel>{{ entity.street }}</QItemLabel>
</QItemSection>
</QItem>
</QList>
</div>
<div class="col">
<QList>
<QItemLabel header class="text-h6">
{{ t('customer.summary.fiscalData') }}
</QItemLabel>
<QItem dense>
<QCheckbox
v-model="entity.isEqualizated"
:label="t('customer.summary.isEqualizated')"
disable
/>
</QItem>
<QItem dense>
<QCheckbox
v-model="entity.isActive"
:label="t('customer.summary.isActive')"
disable
/>
</QItem>
<QItem dense>
<QCheckbox
v-model="entity.hasToInvoiceByAddress"
:label="t('customer.summary.invoiceByAddress')"
disable
/>
</QItem>
<QItem dense>
<QCheckbox
v-model="entity.isTaxDataChecked"
:label="t('customer.summary.verifiedData')"
disable
/>
</QItem>
<QItem dense>
<QCheckbox
v-model="entity.hasToInvoice"
:label="t('customer.summary.hasToInvoice')"
disable
/>
</QItem>
<QItem dense>
<QCheckbox
v-model="entity.isToBeMailed"
:label="t('customer.summary.notifyByEmail')"
disable
/>
</QItem>
<QItem dense>
<QCheckbox
v-model="entity.isVies"
:label="t('customer.summary.vies')"
disable
/>
</QItem>
</QList>
</div>
<div class="col">
<QList>
<QItemLabel header class="text-h6">
{{ t('customer.summary.billingData') }}
</QItemLabel>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.payMethod') }}
</QItemLabel>
<QItemLabel>{{ entity.payMethod.name }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.bankAccount') }}
</QItemLabel>
<QItemLabel>{{ entity.iban }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.dueDay') }}
</QItemLabel>
<QItemLabel>{{ entity.dueDay }}</QItemLabel>
</QItemSection>
</QItem>
<QItem dense>
<QCheckbox
v-model="entity.hasLcr"
:label="t('customer.summary.hasLcr')"
disable
/>
</QItem>
<QItem dense>
<QCheckbox
v-model="entity.hasCoreVnl"
:label="t('customer.summary.hasCoreVnl')"
disable
/>
</QItem>
<QItem dense>
<QCheckbox
v-model="entity.hasSepaVnl"
:label="t('customer.summary.hasB2BVnl')"
disable
/>
</QItem>
</QList>
</div>
<div class="col" v-if="entity.defaultAddress">
<QList>
<QItemLabel header class="text-h6">
{{ t('customer.summary.consignee') }}
</QItemLabel>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.addressName') }}
</QItemLabel>
<QItemLabel>
{{ entity.defaultAddress.nickname }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.addressCity') }}
</QItemLabel>
<QItemLabel>{{ entity.defaultAddress.city }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.addressStreet') }}
</QItemLabel>
<QItemLabel>
{{ entity.defaultAddress.street }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</div>
<div class="col" v-if="entity.account">
<QList>
<QItemLabel header class="text-h6">
{{ t('customer.summary.webAccess') }}
</QItemLabel>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.username') }}
</QItemLabel>
<QItemLabel>{{ entity.account.name }}</QItemLabel>
</QItemSection>
</QItem>
<QItem dense>
<QCheckbox
v-model="entity.account.active"
:label="t('customer.summary.webAccess')"
disable
/>
</QItem>
</QList>
</div>
<div class="col">
<QList>
<QItemLabel header class="text-h6">
{{ t('customer.summary.businessData') }}
</QItemLabel>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.totalGreuge') }}
</QItemLabel>
<QItemLabel>
{{ toCurrency(entity.totalGreuge) }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem v-if="entity.mana">
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.mana') }}
</QItemLabel>
<QItemLabel>
{{ toCurrency(entity.mana.mana) }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem v-if="entity.claimsRatio">
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.priceIncreasingRate') }}
</QItemLabel>
<QItemLabel>
{{ toPercentage(priceIncreasingRate) }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem v-if="entity.averageInvoiced">
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.averageInvoiced') }}
</QItemLabel>
<QItemLabel>
{{ toCurrency(entity.averageInvoiced.invoiced) }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem v-if="entity.claimsRatio">
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.claimRate') }}
</QItemLabel>
<QItemLabel>{{ toPercentage(claimRate) }}</QItemLabel>
</QItemSection>
</QItem>
</QList>
</div>
<div class="col">
<QList>
<QItemLabel header class="text-h6">
{{ t('customer.summary.financialData') }}
</QItemLabel>
<QItem v-if="entity.debt">
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.risk') }}
</QItemLabel>
<QItemLabel :class="debtWarning">
{{ toCurrency(entity.debt.debt) }}
</QItemLabel>
</QItemSection>
<QItemSection side>
<QIcon name="vn:info">
<QTooltip>
{{ t('customer.summary.riskInfo') }}
</QTooltip>
</QIcon>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.credit') }}
</QItemLabel>
<QItemLabel :class="creditWarning">
{{ toCurrency(entity.credit) }}
</QItemLabel>
</QItemSection>
<QItemSection side>
<QIcon name="vn:info">
<QTooltip>
{{ t('customer.summary.creditInfo') }}
</QTooltip>
</QIcon>
</QItemSection>
</QItem>
<QItem v-if="entity.creditInsurance">
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.securedCredit') }}
</QItemLabel>
<QItemLabel>
{{ toCurrency(entity.creditInsurance) }}
</QItemLabel>
</QItemSection>
<QItemSection side>
<QIcon name="vn:info">
<QTooltip>
{{ t('customer.summary.securedCreditInfo') }}
</QTooltip>
</QIcon>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.balance') }}
</QItemLabel>
<QItemLabel>
{{ toCurrency(entity.sumRisk) || toCurrency(0) }}
</QItemLabel>
</QItemSection>
<QItemSection side>
<QIcon name="vn:info">
<QTooltip>
{{ t('customer.summary.balanceInfo') }}
</QTooltip>
</QIcon>
</QItemSection>
</QItem>
<QItem v-if="entity.defaulters">
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.balanceDue') }}
</QItemLabel>
<QItemLabel :class="balanceDueWarning">
{{ toCurrency(balanceDue) }}
</QItemLabel>
</QItemSection>
<QItemSection side>
<QIcon name="vn:info">
<QTooltip>
{{ t('customer.summary.balanceDueInfo') }}
</QTooltip>
</QIcon>
</QItemSection>
</QItem>
<QItem v-if="entity.recovery">
<QItemSection>
<QItemLabel caption>
{{ t('customer.summary.recoverySince') }}
</QItemLabel>
<QItemLabel>
{{ toDate(entity.recovery.started) }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</div>
</QCardSection>
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss">
.q-item__label + .q-item__label {
margin: 0;
}
</style>

View File

@ -1,61 +0,0 @@
<script setup>
import { reactive, watch } from 'vue';
const customer = reactive({
name: '',
});
watch(
() => customer.name,
() => {
console.log('customer.name changed');
}
);
</script>
<template>
<QPage class="q-pa-md">
<QCard class="q-pa-md">
<QForm @submit="onSubmit" @reset="onReset" class="q-gutter-md">
<QInput
filled
v-model="customer.name"
label="Your name *"
hint="Name and surname"
lazy-rules
:rules="[(val) => (val && val.length > 0) || 'Please type something']"
/>
<QInput
filled
type="number"
v-model="age"
label="Your age *"
lazy-rules
:rules="[
(val) => (val !== null && val !== '') || 'Please type your age',
(val) => (val > 0 && val < 100) || 'Please type a real age',
]"
/>
<div>
<QBtn label="Submit" type="submit" color="primary" />
<QBtn
label="Reset"
type="reset"
color="primary"
flat
class="q-ml-sm"
/>
</div>
</QForm>
</QCard>
</QPage>
</template>
<style lang="scss" scoped>
.card {
width: 100%;
max-width: 60em;
}
</style>

View File

@ -3,6 +3,7 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -63,7 +64,7 @@ const zones = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="workers"> <QItemSection v-if="workers">
<QSelect <VnSelectFilter
:label="t('Salesperson')" :label="t('Salesperson')"
v-model="params.salesPersonFk" v-model="params.salesPersonFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -82,7 +83,7 @@ const zones = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="provinces"> <QItemSection v-if="provinces">
<QSelect <VnSelectFilter
:label="t('Province')" :label="t('Province')"
v-model="params.provinceFk" v-model="params.provinceFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -91,6 +92,7 @@ const zones = ref();
option-label="name" option-label="name"
emit-value emit-value
map-options map-options
:input-debounce="0"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -124,7 +126,7 @@ const zones = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="zones"> <QItemSection v-if="zones">
<QSelect <VnSelectFilter
:label="t('Zone')" :label="t('Zone')"
v-model="params.zoneFk" v-model="params.zoneFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"

View File

@ -7,6 +7,8 @@ import VnPaginate from 'src/components/ui/VnPaginate.vue';
import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue'; import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import CustomerFilter from './CustomerFilter.vue'; import CustomerFilter from './CustomerFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const router = useRouter(); const router = useRouter();
@ -66,85 +68,40 @@ function viewSummary(id) {
auto-load auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QCard class="card q-mb-md" v-for="row of rows" :key="row.id"> <CardList
<QItem v-for="row of rows"
class="q-pa-none items-start cursor-pointer q-hoverable" :key="row.id"
v-ripple :title="row.name"
clickable @click="navigate(row.id)"
> >
<QItemSection class="q-pa-md" @click="navigate(row.id)"> <template #list-items>
<div class="text-h6">{{ row.name }}</div> <VnLv label="ID" :value="row.id" />
<QItemLabel caption>#{{ row.id }}</QItemLabel> <VnLv :label="t('customer.list.email')" :value="row.email" />
<VnLv :label="t('customer.list.phone')" :value="row.phone" />
<QList> </template>
<QItem class="q-pa-none"> <template #actions>
<QItemSection> <QBtn
<QItemLabel caption> flat
{{ t('customer.list.email') }} color="primary"
</QItemLabel> icon="arrow_circle_right"
<QItemLabel>{{ row.email }}</QItemLabel> @click.stop="navigate(row.id)"
</QItemSection> >
</QItem> <QTooltip>
<QItem class="q-pa-none"> {{ t('components.smartCard.openCard') }}
<QItemSection> </QTooltip>
<QItemLabel caption> </QBtn>
{{ t('customer.list.phone') }} <QBtn
</QItemLabel> flat
<QItemLabel>{{ row.phone }}</QItemLabel> color="grey-7"
</QItemSection> icon="preview"
</QItem> @click.stop="viewSummary(row.id)"
</QList> >
</QItemSection> <QTooltip>
<QSeparator vertical /> {{ t('components.smartCard.openSummary') }}
<QCardActions vertical class="justify-between"> </QTooltip>
<!-- <QBtn color="grey-7" round flat icon="more_vert"> </QBtn>
<QTooltip>{{ t('customer.list.moreOptions') }}</QTooltip> </template>
<QMenu cover auto-close> </CardList>
<QList>
<QItem clickable>
<QItemSection avatar>
<QIcon name="add" />
</QItemSection>
<QItemSection>Add a note</QItemSection>
</QItem>
<QItem clickable>
<QItemSection avatar>
<QIcon name="history" />
</QItemSection>
<QItemSection>Display customer history</QItemSection>
</QItem>
</QList>
</QMenu>
</QBtn> -->
<QBtn
flat
round
color="primary"
icon="arrow_circle_right"
@click="navigate(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openCard') }}
</QTooltip>
</QBtn>
<QBtn
flat
round
color="grey-7"
icon="preview"
@click="viewSummary(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
<!-- <QBtn flat round color="grey-7" icon="vn:ticket">
<QTooltip>{{ t('customer.list.customerOrders') }}</QTooltip>
</QBtn> -->
</QCardActions>
</QItem>
</QCard>
</template> </template>
</VnPaginate> </VnPaginate>
</div> </div>

View File

@ -110,12 +110,18 @@ function stateColor(row) {
</div> </div>
</Teleport> </Teleport>
</template> </template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer
v-model="stateStore.rightDrawer"
side="right"
:width="256"
show-if-above
:breakpoint="1600"
>
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit text-grey-8">
<CustomerPaymentsFilter data-key="CustomerTransactions" /> <CustomerPaymentsFilter data-key="CustomerTransactions" />
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md customer-payments">
<div class="card-list"> <div class="card-list">
<QToolbar class="q-pa-none"> <QToolbar class="q-pa-none">
<QToolbarTitle>{{ t('Web Payments') }}</QToolbarTitle> <QToolbarTitle>{{ t('Web Payments') }}</QToolbarTitle>
@ -138,7 +144,7 @@ function stateColor(row) {
order="created DESC" order="created DESC"
:limit="20" :limit="20"
:offset="50" :offset="50"
auto-load :auto-load="!!$route?.query.params"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QTable <QTable
@ -148,7 +154,7 @@ function stateColor(row) {
row-key="id" row-key="id"
:pagination="{ rowsPerPage: 0 }" :pagination="{ rowsPerPage: 0 }"
:grid="grid || $q.screen.lt.sm" :grid="grid || $q.screen.lt.sm"
class="q-mt-xs" class="q-mt-xs custom-table"
hide-pagination hide-pagination
> >
<template #body-cell-actions="{ row }"> <template #body-cell-actions="{ row }">
@ -167,6 +173,13 @@ function stateColor(row) {
</QBtn> </QBtn>
</QTd> </QTd>
</template> </template>
<template #body-cell-id="{ row }">
<QTd auto-width align="right">
<span>
{{ row.id }}
</span>
</QTd>
</template>
<template #body-cell-customerId="{ row }"> <template #body-cell-customerId="{ row }">
<QTd align="right"> <QTd align="right">
<span class="link"> <span class="link">
@ -175,6 +188,13 @@ function stateColor(row) {
</span> </span>
</QTd> </QTd>
</template> </template>
<template #body-cell-customer="{ row }">
<QTd auto-width align="left" :title="row.customerName">
<span>
{{ row.customerName }}
</span>
</QTd>
</template>
<template #body-cell-state="{ row }"> <template #body-cell-state="{ row }">
<QTd auto-width class="text-center"> <QTd auto-width class="text-center">
<QBadge :color="stateColor(row)"> <QBadge :color="stateColor(row)">
@ -188,9 +208,9 @@ function stateColor(row) {
</template> </template>
<template #item="{ cols, row }"> <template #item="{ cols, row }">
<div class="q-mb-md col-12"> <div class="q-mb-md col-12">
<QCard> <QCard class="q-pa-none">
<QItem class="q-pa-none items-start"> <QItem class="q-pa-none items-start">
<QItemSection class="q-pa-md"> <QItemSection class="q-pa-none">
<QList> <QList>
<template <template
v-for="col of cols" v-for="col of cols"
@ -257,10 +277,21 @@ function stateColor(row) {
</QPage> </QPage>
</template> </template>
<style lang="scss" scoped> <style lang="scss">
.card-list { .customer-payments {
width: 100%; .card-list {
max-width: 60em; width: 100%;
max-width: 60em;
.q-table--dense .q-table th:first-child {
padding-left: 0;
}
td {
max-width: 130px;
overflow: hidden;
text-overflow: ellipsis;
}
}
} }
</style> </style>

View File

@ -9,10 +9,14 @@ const props = defineProps({
required: true, required: true,
}, },
}); });
function isValidNumber(value) {
return /^(\d|\d+(\.|,)?\d+)$/.test(value);
}
</script> </script>
<template> <template>
<VnFilterPanel :data-key="props.dataKey" :search-button="true"> <VnFilterPanel :data-key="props.dataKey" :search-button="true" :show-all="false">
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong> <strong>{{ t(`params.${tag.label}`) }}: </strong>
@ -49,9 +53,99 @@ const props = defineProps({
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<QInput :label="t('Amount')" v-model="params.amount" lazy-rules> <QInput
:label="t('Amount')"
v-model="params.amount"
lazy-rules
@update:model-value="
(value) => {
if (value.includes(','))
params.amount = params.amount.replace(',', '.');
}
"
:rules="[
(val) =>
isValidNumber(val) || !val || 'Please type a number',
]"
>
<template #prepend> <template #prepend>
<QIcon name="euro" size="sm"></QIcon> <QIcon name="euro" size="sm" />
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
v-model="params.from"
:label="t('From')"
mask="date"
placeholder="yyyy/mm/dd"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="params.from" landscape>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="t('globals.confirm')"
color="primary"
flat
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QItemSection>
<QItemSection>
<QInput
v-model="params.to"
:label="t('To')"
mask="date"
placeholder="yyyy/mm/dd"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="params.to" landscape>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="t('globals.confirm')"
color="primary"
flat
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template> </template>
</QInput> </QInput>
</QItemSection> </QItemSection>
@ -67,12 +161,19 @@ en:
orderFk: Order orderFk: Order
clientFk: Customer clientFk: Customer
amount: Amount amount: Amount
from: From
to: To
es: es:
params: params:
orderFk: Pedido orderFk: Pedido
clientFk: Cliente clientFk: Cliente
amount: Importe amount: Importe
from: Desde
to: Hasta
Order ID: ID pedido Order ID: ID pedido
Customer ID: ID cliente Customer ID: ID cliente
Amount: Importe Amount: Importe
Please type a number: Por favor, escriba un número
From: Desde
To: Hasta
</i18n> </i18n>

View File

@ -90,7 +90,7 @@ const pinnedModules = computed(() => navigation.getPinnedModules());
<i18n> <i18n>
{ {
"en": { "en": {
"pinnedInfo": "Your pinned modules will be shown here..." "pinnedInfo": "Your pinned modules will be shown here..."
}, },
"es": { "es": {

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
<QPage class="q-pa-md"> <QPage>
<RouterView></RouterView> <QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>

View File

@ -5,6 +5,8 @@ import { useI18n } from 'vue-i18n';
import { toCurrency, toDate } from 'src/filters'; import { toCurrency, toDate } from 'src/filters';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -42,6 +44,8 @@ const filter = {
function ticketFilter(invoice) { function ticketFilter(invoice) {
return JSON.stringify({ refFk: invoice.ref }); return JSON.stringify({ refFk: invoice.ref });
} }
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.ref, entity.id));
</script> </script>
<template> <template>
@ -50,47 +54,32 @@ function ticketFilter(invoice) {
module="InvoiceOut" module="InvoiceOut"
:url="`InvoiceOuts/${entityId}`" :url="`InvoiceOuts/${entityId}`"
:filter="filter" :filter="filter"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="invoiceOutData"
> >
<template #description="{ entity }">
<span>
{{ entity.ref }}
<QTooltip>{{ entity.ref }}</QTooltip>
</span>
</template>
<template #body="{ entity }"> <template #body="{ entity }">
<QList> <VnLv :label="t('invoiceOut.card.issued')" :value="toDate(entity.issued)" />
<QItem> <VnLv
<QItemSection> :label="t('invoiceOut.card.amount')"
<QItemLabel caption> :value="toCurrency(entity.amount)"
{{ t('invoiceOut.card.issued') }} />
</QItemLabel> <VnLv v-if="entity.client" :label="t('invoiceOut.card.client')">
<QItemLabel>{{ toDate(entity.issued) }}</QItemLabel> <template #value>
</QItemSection> <span class="link">
<QItemSection> {{ entity.client.name }}
<QItemLabel caption> <CustomerDescriptorProxy :id="entity.client.id" />
{{ t('invoiceOut.card.amount') }} </span>
</QItemLabel> </template>
<QItemLabel>{{ toCurrency(entity.amount) }}</QItemLabel> </VnLv>
</QItemSection> <VnLv
</QItem> v-if="entity.company"
<QItem> :label="t('invoiceOut.card.company')"
<QItemSection v-if="entity.client"> :value="entity.company.code"
<QItemLabel caption> />
{{ t('invoiceOut.card.client') }} </template>
</QItemLabel> <template #actions="{ entity }">
<QItemLabel class="link">
{{ entity.client.name }}
<CustomerDescriptorProxy :id="entity.client.id" />
</QItemLabel>
</QItemSection>
<QItemSection v-if="entity.company">
<QItemLabel caption>{{
t('invoiceOut.card.company')
}}</QItemLabel>
<QItemLabel>{{ entity.company.code }}</QItemLabel>
</QItemSection>
</QItem>
</QList>
<QCardActions> <QCardActions>
<QBtn <QBtn
v-if="entity.client" v-if="entity.client"

View File

@ -4,8 +4,15 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import { toCurrency, toDate } from 'src/filters'; import { toCurrency, toDate } from 'src/filters';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
onMounted(() => fetch()); import VnLv from 'src/components/ui/VnLv.vue';
import { getUrl } from 'src/composables/getUrl';
onMounted(async () => {
fetch();
salixUrl.value = await getUrl('');
invoiceOutUrl.value = salixUrl.value + `invoiceOut/${entityId.value}/`;
});
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -19,20 +26,13 @@ const $props = defineProps({
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
const invoiceOut = ref(null); const salixUrl = ref();
const tax = ref(null); const invoiceOutUrl = ref();
const tikets = ref(null); const tickets = ref(null);
function fetch() { function fetch() {
const id = entityId.value; axios.get(`InvoiceOuts/${entityId.value}/getTickets`).then(({ data }) => {
tickets.value = data;
axios.get(`InvoiceOuts/${id}/summary`).then(({ data }) => {
invoiceOut.value = data.invoiceOut;
tax.value = data.invoiceOut.taxesBreakdown;
});
axios.get(`InvoiceOuts/${id}/getTickets`).then(({ data }) => {
tikets.value = data;
}); });
} }
@ -95,124 +95,64 @@ const ticketsColumns = ref([
</script> </script>
<template> <template>
<div class="summary container"> <CardSummary ref="summary" :url="`InvoiceOuts/${entityId}/summary`">
<QCard> <template #header="{ entity: { invoiceOut } }">
<SkeletonSummary v-if="!invoiceOut" /> <div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div>
<template v-if="invoiceOut"> </template>
<div class="header bg-primary q-pa-sm q-mb-md"> <template #body="{ entity: { invoiceOut } }">
{{ invoiceOut.ref }} - {{ invoiceOut.client.socialName }} <QCard class="vn-one">
<div class="header">
{{ t('invoiceOut.pageTitles.basicData') }}
</div> </div>
<QList> <VnLv
<QItem> :label="t('invoiceOut.summary.issued')"
<QItemSection> :value="toDate(invoiceOut.issued)"
<QItemLabel caption>{{ />
t('invoiceOut.summary.issued') <VnLv
}}</QItemLabel> :label="t('invoiceOut.summary.dued')"
<QItemLabel>{{ toDate(invoiceOut.issued) }}</QItemLabel> :value="toDate(invoiceOut.dued)"
</QItemSection> />
<QItemSection> <VnLv
<QItemLabel caption>{{ :label="t('invoiceOut.summary.created')"
t('invoiceOut.summary.dued') :value="toDate(invoiceOut.created)"
}}</QItemLabel> />
<QItemLabel>{{ toDate(invoiceOut.dued) }}</QItemLabel> <VnLv
</QItemSection> :label="t('invoiceOut.summary.booked')"
</QItem> :value="toDate(invoiceOut.booked)"
<QItem> />
<QItemSection> <VnLv
<QItemLabel caption>{{ :label="t('invoiceOut.summary.company')"
t('invoiceOut.summary.created') :value="invoiceOut.company.code"
}}</QItemLabel> />
<QItemLabel>{{ toDate(invoiceOut.created) }}</QItemLabel> </QCard>
</QItemSection> <QCard class="vn-three">
<QItemSection> <div class="header">
<QItemLabel caption>{{ {{ t('invoiceOut.summary.taxBreakdown') }}
t('invoiceOut.summary.booked') </div>
}}</QItemLabel> <QTable :columns="taxColumns" :rows="invoiceOut.taxesBreakdown" flat>
<QItemLabel>{{ toDate(invoiceOut.booked) }}</QItemLabel> <template #header="props">
</QItemSection> <QTr :props="props">
</QItem> <QTh v-for="col in props.cols" :key="col.name" :props="props">
<QItem> {{ t(col.label) }}
<QItemSection> </QTh>
<QItemLabel caption>{{ </QTr>
t('invoiceOut.summary.company') </template>
}}</QItemLabel> </QTable>
<QItemLabel>{{ invoiceOut.company.code }}</QItemLabel> </QCard>
</QItemSection> <QCard class="vn-three">
</QItem> <div class="header">
</QList> {{ t('invoiceOut.summary.tickets') }}
<QCardSection class="q-pa-md"> </div>
<h6>{{ t('invoiceOut.summary.taxBreakdown') }}</h6> <QTable v-if="tickets" :columns="ticketsColumns" :rows="tickets" flat>
<QTable :columns="taxColumns" :rows="tax" flat> <template #header="props">
<template #header="props"> <QTr :props="props">
<QTr :props="props"> <QTh v-for="col in props.cols" :key="col.name" :props="props">
<QTh {{ t(col.label) }}
v-for="col in props.cols" </QTh>
:key="col.name" </QTr>
:props="props" </template>
> </QTable>
{{ t(col.label) }} </QCard>
</QTh> </template>
</QTr> </CardSummary>
</template>
</QTable>
</QCardSection>
<QCardSection class="q-pa-md">
<h6>{{ t('invoiceOut.summary.tickets') }}</h6>
<QTable :columns="ticketsColumns" :rows="tikets" flat>
<template #header="props">
<QTr :props="props">
<QTh
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ t(col.label) }}
</QTh>
</QTr>
</template>
</QTable>
</QCardSection>
</template>
</QCard>
</div>
</template> </template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
min-width: 950px;
max-width: 950px;
}
.summary {
.header {
text-align: center;
font-size: 18px;
}
#slider-container {
max-width: 80%;
margin: 0 auto;
.q-slider {
.q-slider__marker-labels:nth-child(1) {
transform: none;
}
.q-slider__marker-labels:nth-child(2) {
transform: none;
left: auto !important;
right: 0%;
}
}
}
}
.q-dialog .summary {
max-width: 1200px;
}
</style>

View File

@ -9,6 +9,8 @@ import InvoiceOutSummaryDialog from './Card/InvoiceOutSummaryDialog.vue';
import { toDate, toCurrency } from 'src/filters/index'; import { toDate, toCurrency } from 'src/filters/index';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import InvoiceOutFilter from './InvoiceOutFilter.vue'; import InvoiceOutFilter from './InvoiceOutFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const router = useRouter(); const router = useRouter();
@ -71,97 +73,59 @@ function viewSummary(id) {
auto-load auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QCard class="card q-mb-md" v-for="row of rows" :key="row.id"> <CardList
<QItem v-for="row of rows"
class="q-pa-none items-start cursor-pointer q-hoverable" :key="row.id"
v-ripple :title="row.ref"
clickable @click="navigate(row.id)"
> >
<QItemSection class="q-pa-md" @click="navigate(row.id)"> <template #list-items>
<div class="text-h6">{{ row.ref }}</div> <VnLv label="ID" :value="row.id" />
<QItemLabel caption>#{{ row.id }}</QItemLabel> <VnLv
<QList> :label="t('invoiceOut.list.shortIssued')"
<QItem class="q-pa-none"> :title-label="t('invoiceOut.list.issued')"
<QItemSection> :value="toDate(row.issued)"
<QItemLabel caption> />
{{ t('invoiceOut.list.issued') }} <VnLv
</QItemLabel> :label="t('invoiceOut.list.amount')"
<QItemLabel> :value="toCurrency(row.amount)"
{{ toDate(row.issued) }} />
</QItemLabel> <VnLv
</QItemSection> :label="t('invoiceOut.list.client')"
<QItemSection> :value="row.clientSocialName"
<QItemLabel caption> />
{{ t('invoiceOut.list.amount') }} <VnLv
</QItemLabel> :label="t('invoiceOut.list.shortCreated')"
<QItemLabel> :title-label="t('invoiceOut.list.created')"
{{ toCurrency(row.amount) }} :value="toDate(row.created)"
</QItemLabel> />
</QItemSection> <VnLv
</QItem> :label="t('invoiceOut.list.company')"
<QItem class="q-pa-none"> :value="row.companyCode"
<QItemSection> />
<QItemLabel caption> <VnLv
{{ t('invoiceOut.list.client') }} :label="t('invoiceOut.list.shortDued')"
</QItemLabel> :title-label="t('invoiceOut.list.dued')"
<QItemLabel> :value="toDate(row.dued)"
{{ row.clientSocialName }} />
</QItemLabel> </template>
</QItemSection> <template #actions>
<QItemSection> <QBtn
<QItemLabel caption> flat
{{ t('invoiceOut.list.created') }} icon="arrow_circle_right"
</QItemLabel> @click.stop="navigate(row.id)"
<QItemLabel> >
{{ toDate(row.created) }} <QTooltip>
</QItemLabel> {{ t('components.smartCard.openCard') }}
</QItemSection> </QTooltip>
</QItem> </QBtn>
<QItem class="q-pa-none"> <QBtn flat icon="preview" @click.stop="viewSummary(row.id)">
<QItemSection> <QTooltip>
<QItemLabel caption> {{ t('components.smartCard.openSummary') }}
{{ t('invoiceOut.list.company') }} </QTooltip>
</QItemLabel> </QBtn>
<QItemLabel>{{ row.companyCode }}</QItemLabel> </template>
</QItemSection> </CardList>
<QItemSection>
<QItemLabel caption>
{{ t('invoiceOut.list.dued') }}
</QItemLabel>
<QItemLabel>
{{ toDate(row.dued) }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QItemSection>
<QSeparator vertical />
<QCardActions vertical class="justify-between">
<QBtn
flat
round
color="orange"
icon="arrow_circle_right"
@click="navigate(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openCard') }}
</QTooltip>
</QBtn>
<QBtn
flat
round
color="grey-7"
icon="preview"
@click="viewSummary(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
</QCardActions>
</QItem>
</QCard>
</template> </template>
</VnPaginate> </VnPaginate>
</div> </div>

View File

@ -1,58 +1,32 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref } from 'vue';
import { Dark, Quasar, useQuasar } from 'quasar'; import { Notify, useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { useLogin } from 'src/composables/useLogin';
import VnLogo from 'components/ui/VnLogo.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const session = useSession(); const session = useSession();
const loginCache = useLogin();
const router = useRouter(); const router = useRouter();
const { t, locale } = useI18n(); const { t } = useI18n();
const userLocale = computed({
get() {
return locale.value;
},
set(value) {
locale.value = value;
if (value === 'en') value = 'en-GB';
// FIXME: Dynamic imports from absolute paths are not compatible with vite:
// https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
try {
const langList = import.meta.glob('../../node_modules/quasar/lang/*.mjs');
langList[`../../node_modules/quasar/lang/${value}.mjs`]().then((lang) => {
Quasar.lang.set(lang.default);
});
} catch (error) {
//
}
},
});
const darkMode = computed({
get() {
return Dark.isActive;
},
set(value) {
Dark.set(value);
},
});
const username = ref(''); const username = ref('');
const password = ref(''); const password = ref('');
const keepLogin = ref(true); const keepLogin = ref(true);
async function onSubmit() { async function onSubmit() {
const params = {
user: username.value,
password: password.value,
};
try { try {
const { data } = await axios.post('Accounts/login', { const { data } = await axios.post('Accounts/login', params);
user: username.value,
password: password.value,
});
if (!data) return; if (!data) return;
@ -69,122 +43,68 @@ async function onSubmit() {
} else { } else {
router.push({ name: 'Dashboard' }); router.push({ name: 'Dashboard' });
} }
} catch (e) { } catch (res) {
// if (res.response?.data?.error?.code === 'REQUIRES_2FA') {
Notify.create({
message: t('login.twoFactorRequired'),
icon: 'phonelink_lock',
type: 'warning',
});
params.keepLogin = keepLogin.value;
loginCache.setUser(params);
return router.push({
name: 'TwoFactor',
query: router.currentRoute.value?.query,
});
}
Notify.create({
message: t('login.loginError'),
type: 'negative',
});
} }
} }
</script> </script>
<template> <template>
<QLayout> <QForm @submit="onSubmit" class="q-gutter-y-md q-pa-lg formCard">
<QPageContainer> <VnLogo alt="Logo" fit="contain" :ratio="16 / 9" class="q-mb-md" />
<QPage id="login"> <QInput
<QPageSticky position="top-right"> v-model="username"
<QToolbar> :label="t('login.username')"
<QBtn lazy-rules
id="switchLanguage" :rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]"
:label="t('globals.language')" />
icon="translate" <QInput
color="primary" type="password"
size="sm" v-model="password"
flat :label="t('login.password')"
rounded lazy-rules
> :rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]"
<QMenu auto-close> />
<QList dense> <QToggle v-model="keepLogin" :label="t('login.keepLogin')" />
<QItem
@click="userLocale = 'en'"
:active="userLocale == 'en'"
v-ripple
clickable
>
{{ t('globals.lang.en') }}
</QItem>
<QItem
@click="userLocale = 'es'"
:active="userLocale == 'es'"
v-ripple
clickable
>
{{ t('globals.lang.es') }}
</QItem>
</QList>
</QMenu>
</QBtn>
<QList>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t(`globals.darkMode`) }}
</QItemLabel>
</QItemSection>
<QItemSection side>
<QToggle
v-model="darkMode"
checked-icon="dark_mode"
unchecked-icon="light_mode"
/>
</QItemSection>
</QItem>
</QList>
</QToolbar>
</QPageSticky>
<div class="login-form q-pa-xl">
<QImg
src="~/assets/logo.svg"
alt="Logo"
fit="contain"
:ratio="16 / 9"
class="q-mb-md"
/>
<QForm @submit="onSubmit" class="q-gutter-md">
<QInput
v-model="username"
:label="t('login.username')"
lazy-rules
:rules="[
(val) =>
(val && val.length > 0) || t('login.fieldRequired'),
]"
/>
<QInput
type="password"
v-model="password"
:label="t('login.password')"
lazy-rules
:rules="[
(val) =>
(val && val.length > 0) || t('login.fieldRequired'),
]"
/>
<QToggle v-model="keepLogin" :label="t('login.keepLogin')" />
<div> <div>
<QBtn <QBtn
:label="t('login.submit')" :label="t('login.submit')"
type="submit" type="submit"
color="primary" color="primary"
class="full-width" class="full-width"
rounded rounded
unelevated unelevated
/> />
</div> </div>
</QForm> </QForm>
</div>
</QPage>
</QPageContainer>
</QLayout>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
#login { .formCard {
display: flex; max-width: 350px;
align-items: center; min-width: 300px;
justify-content: center;
min-height: inherit;
} }
.login-form { @media (max-width: $breakpoint-xs-max) {
width: 400px; .formCard {
min-width: 100%;
}
} }
</style> </style>

View File

@ -0,0 +1,91 @@
<script setup>
import { ref } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import axios from 'axios';
import { useSession } from 'src/composables/useSession';
import { useLogin } from 'src/composables/useLogin';
const quasar = useQuasar();
const session = useSession();
const router = useRouter();
const loginCache = useLogin();
const { t } = useI18n();
const code = ref('');
const params = loginCache.getUser().value;
if (!params.user) {
router.push({ name: 'Login' });
}
async function onSubmit() {
try {
params.code = code.value;
const { data } = await axios.post('VnUsers/validate-auth', params);
if (!data) return;
await session.login(data.token, params.keepLogin);
quasar.notify({
message: t('login.loginSuccess'),
type: 'positive',
});
const currentRoute = router.currentRoute.value;
if (currentRoute.query && currentRoute.query.redirect) {
router.push(currentRoute.query.redirect);
} else {
router.push({ name: 'Dashboard' });
}
} catch (e) {
quasar.notify({
message: e.response?.data?.error.message,
type: 'negative',
});
}
}
</script>
<template>
<QForm @submit="onSubmit" class="q-gutter-y-md q-pa-lg formCard">
<div class="column items-center">
<QIcon name="phonelink_lock" size="xl" color="primary" />
<h5 class="text-center q-my-md">{{ t('twoFactor.insert') }}</h5>
</div>
<QInput
v-model="code"
:hint="t('twoFactor.explanation')"
mask="# # # # # #"
fill-mask
unmasked-value
autofocus
>
<template #prepend>
<QIcon name="lock" />
</template>
</QInput>
<div class="q-mt-xl">
<QBtn
:label="t('twoFactor.validate')"
type="submit"
color="primary"
class="full-width q-mt-md"
rounded
unelevated
/>
</div>
</QForm>
</template>
<style lang="scss" scoped>
.formCard {
max-width: 350px;
min-width: 300px;
}
@media (max-width: $breakpoint-xs-max) {
.formCard {
min-width: 100%;
}
}
</style>

View File

@ -0,0 +1,124 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnLogo from 'components/ui/VnLogo.vue';
import axios from 'axios';
const route = useRoute();
const { t } = useI18n();
const redirectButtons = ref([]);
onMounted(async () => {
const params = route?.query;
const { data } = await axios.get(`Urls/${params.userId}/get-by-user`);
redirectButtons.value.push({
color: 'bg-vnColor',
icon: new URL(`../../assets/vn_icon.svg`, import.meta.url).href,
text: 'goToShop',
url: data.find((url) => url.appName == 'hedera').url,
});
const urls = data.filter((url) => url.appName != 'hedera');
if (urls.length) {
for (const url of urls) {
redirectButtons.value.push({
color: 'bg-primary',
icon: new URL(`../../assets/${url.appName}_icon.svg`, import.meta.url)
.href,
text: 'logIn',
url: url.url,
});
}
}
});
</script>
<template>
<div class="q-gutter-y-xl q-pa-lg formCard flex flex-center">
<VnLogo
logo="vn"
alt="Logo"
fit="fill"
class="q-px-xl q-py-md q-mb-xl logoWidth"
/>
<div class="text-h4 vnColor text-center text-weight-medium">
{{ t('verifyEmail') }}
</div>
<div class="q-gutter-md flex flex-center">
<QBtn
:class="button.color"
v-for="button of redirectButtons"
:key="button.id"
:href="button.url"
>
<div class="row items-center no-wrap q-gutter-md">
<div class="circle q-pa-sm" style="background-color: var(--vn-gray)">
<QImg :src="button.icon" class="q-pa-md" />
</div>
<div class="text-h5" style="color: var(--vn-gray)">
{{ t(button.text) }}
</div>
</div>
</QBtn>
</div>
</div>
</template>
<i18n>
{
"en": {
"verifyEmail": "Your email has been successfully verified. You can now log in to enjoy all the features of our platform.",
"goToShop": "Go to the shop",
"logIn": "Log In"
},
"es": {
"verifyEmail": "Su correo electrónico ha sido verificado con éxito. Ahora puede iniciar sesión para disfrutar de todas las funcionalidades de nuestra plataforma.",
"goToShop": "Ir a la tienda",
"logIn": "Iniciar sesión"
},
"fr": {
"verifyEmail": "Votre courrier électronique a été vérifié avec succès. Vous pouvez maintenant vous connecter pour profiter de toutes les fonctionnalités de notre plateforme.",
"goToShop": "Aller à la boutique",
"logIn": "Se connecter"
},
"pt": {
"verifyEmail": "Seu e-mail foi verificado com sucesso. Agora você pode fazer o login para aproveitar todas as funcionalidades da nossa plataforma.",
"goToShop": "Ir para a loja",
"logIn": "Fazer login"
},
"it": {
"verifyEmail": "La tua email è stata verificata con successo. Ora puoi accedere per godere di tutte le funzionalità della nostra piattaforma.",
"goToShop": "Vai al negozio",
"logIn": "Accedi"
}
}
</i18n>
<style lang="scss">
.formCard {
max-width: 1500px;
min-width: 700px;
}
@media (max-width: $breakpoint-xs-max) {
.formCard {
min-width: 100%;
}
}
.vnColor {
color: $vnColor;
}
.bg-vnColor {
background-color: $vnColor;
}
.circle {
border-radius: 50%;
}
.logoWidth {
width: 50%;
}
</style>

View File

@ -0,0 +1,168 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const countries = ref();
</script>
<template>
<FetchData url="Countries" @on-fetch="(data) => (countries = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params }">
<QList dense>
<QItem>
<QItemSection>
<QInput
:label="t('route.cmr.list.cmrFk')"
v-model="params.cmrFk"
lazy-rules
>
<template #prepend>
<QIcon name="article" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('route.cmr.list.hasCmrDms')"
v-model="params.hasCmrDms"
lazy-rules
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('route.cmr.list.ticketFk')"
v-model="params.ticketFk"
lazy-rules
>
<template #prepend>
<QIcon name="vn:ticket" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('route.cmr.list.routeFk')"
v-model="params.routeFk"
lazy-rules
>
<template #prepend>
<QIcon name="vn:delivery" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('route.cmr.list.clientFk')"
v-model="params.clientFk"
lazy-rules
>
<template #prepend>
<QIcon name="vn:client" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection v-if="!countries">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="countries">
<QSelect
:label="t('route.cmr.list.country')"
v-model="params.country"
:options="countries"
option-value="country"
option-label="country"
transition-show="jump-down"
transition-hide="jump-up"
emit-value
map-options
>
<template #prepend>
<QIcon name="flag" size="sm"></QIcon>
</template>
</QSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('route.cmr.list.shipped')"
v-model="params.shipped"
mask="date"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="rotate"
transition-hide="rotate"
>
<QDate v-model="params.shipped" minimal>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="t('globals.close')"
color="primary"
flat
@click="save"
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
</QList>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
cmrFk: Cmr id
hasCmrDms: Attached in gestdoc
ticketFk: Ticketd id
country: Country
clientFk: Client id
shipped: Preparation date
es:
params:
cmrFk: Id cmr
hasCmrDms: Adjuntado en gestdoc
ticketFk: Id ticket
country: País
clientFk: Id cliente
shipped: Fecha preparación
</i18n>

View File

@ -0,0 +1,196 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { Notify } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useSession } from 'src/composables/useSession';
import { toDate } from 'filters/index';
import CmrFilter from './CmrFilter.vue';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
const stateStore = useStateStore();
const { t } = useI18n();
const session = useSession();
const token = session.getToken();
const selected = ref([]);
const columns = computed(() => [
{
name: 'cmrFk',
label: t('route.cmr.list.cmrFk'),
field: (row) => row.cmrFk,
sortable: true,
},
{
name: 'hasCmrDms',
label: t('route.cmr.list.hasCmrDms'),
field: (row) => row.hasCmrDms,
align: 'center',
sortable: true,
headerStyle: 'padding-left: 35px',
},
{
name: 'ticketFk',
label: t('route.cmr.list.ticketFk'),
field: (row) => row.ticketFk,
sortable: true,
},
{
name: 'routeFkFk',
label: t('route.cmr.list.routeFk'),
field: (row) => row.routeFk,
sortable: true,
},
{
name: 'clientFk',
label: t('route.cmr.list.clientFk'),
field: (row) => row.clientFk,
sortable: true,
},
{
name: 'country',
label: t('route.cmr.list.country'),
field: (row) => row.country,
align: 'left',
sortable: true,
},
{
name: 'shipped',
label: t('route.cmr.list.shipped'),
field: (row) => toDate(row.shipped),
align: 'center',
sortable: true,
headerStyle: 'padding-left: 33px',
},
{
name: 'icons',
align: 'center',
field: (row) => row.cmrFk,
},
]);
function getApiUrl() {
return new URL(window.location).origin;
}
function getCmrUrl(value) {
return `${getApiUrl()}/api/Routes/${value}/cmr?access_token=${token}`;
}
function downloadPdfs() {
if (!selected.value.length) {
Notify.create({
message: t('globals.noSelectedRows'),
type: 'warning',
});
return;
}
let cmrs = [];
for (let value of selected.value) cmrs.push(value.cmrFk);
// prettier-ignore
return window.open(`${getApiUrl()}/api/Routes/downloadCmrsZip?ids=${cmrs.join(',')}&access_token=${token}`);
}
</script>
<template>
<div class="column items-center">
<div class="list">
<VnPaginate
data-key="CmrList"
:url="`Routes/getExternalCmrs`"
order="cmrFk DESC"
limit="null"
auto-load
>
<template #body="{ rows }">
<QTable
:columns="columns"
:rows="rows"
:dense="$q.screen.lt.md"
:pagination="{ rowsPerPage: null }"
hide-pagination
row-key="cmrFk"
selection="multiple"
v-model:selected="selected"
:grid="$q.screen.lt.md"
auto-load
>
<template #top>
<div style="width: 100%; display: table">
<div style="float: right; color: lightgray">
{{ `${rows.length} ${t('route.cmr.list.results')}` }}
</div>
</div>
</template>
<template #body-cell-hasCmrDms="{ value }">
<QTd align="center">
<QBadge
:id="value ? 'true' : 'false'"
:label="
value
? t('route.cmr.list.true')
: t('route.cmr.list.false')
"
/>
</QTd>
</template>
<template #body-cell-ticketFk="{ value }">
<QTd align="right" class="text-primary">
<span class="text-primary link">{{ value }}</span>
<TicketDescriptorProxy :id="value" />
</QTd>
</template>
<template #body-cell-clientFk="{ value }">
<QTd align="right" class="text-primary">
<span class="text-primary link">{{ value }}</span>
<CustomerDescriptorProxy :id="value" />
</QTd>
</template>
<template #body-cell-icons="{ value }">
<QTd align="center">
<a :href="getCmrUrl(value)" target="_blank">
<QIcon
name="visibility"
color="primary"
size="2em"
class="q-mr-sm q-ml-sm"
/>
<QTooltip>
{{ t('route.cmr.list.viewCmr') }}
</QTooltip>
</a>
</QTd>
</template>
</QTable>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]">
<QBtn @click="downloadPdfs" fab icon="cloud_download" color="primary" />
</QPageSticky>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<CmrFilter data-key="CmrList" />
<QTooltip>
{{ t('route.cmr.list.downloadCmrs') }}
</QTooltip>
</QScrollArea>
</QDrawer>
</div>
</template>
<style lang="scss" scoped>
.list {
padding-top: 15px;
padding-bottom: 15px;
max-width: 1000px;
width: 100%;
}
.grid-style-transition {
transition: transform 0.28s, background-color 0.28s;
}
#true {
background-color: $positive;
}
#false {
background-color: $negative;
}
</style>

View File

@ -0,0 +1,17 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'src/components/LeftMenu.vue';
const stateStore = useStateStore();
</script>
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit text-grey-8">
<LeftMenu />
</QScrollArea>
</QDrawer>
<QPageContainer>
<RouterView></RouterView>
</QPageContainer>
</template>

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
<QPage class="q-pa-md"> <QPage>
<RouterView></RouterView> <QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>

View File

@ -1,11 +1,14 @@
<script setup> <script setup>
import { computed } from 'vue'; import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import TicketDescriptorMenu from './TicketDescriptorMenu.vue'; import TicketDescriptorMenu from './TicketDescriptorMenu.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -66,78 +69,61 @@ const filter = {
], ],
}; };
function stateColor(state) { const data = ref(useCardDescription());
if (state.code === 'OK') return 'text-green'; const setData = (entity) =>
if (state.code === 'FREE') return 'text-blue-3'; (data.value = useCardDescription(entity.client.name, entity.id));
if (state.alertLevel === 1) return 'text-primary';
if (state.alertLevel === 0) return 'text-red';
}
</script> </script>
<template> <template>
<CardDescriptor module="Ticket" :url="`Tickets/${entityId}`" :filter="filter"> <CardDescriptor
module="Ticket"
:url="`Tickets/${entityId}`"
:filter="filter"
:title="data.title"
:subtitle="data.subtitle"
data-key="ticketData"
@on-fetch="setData"
>
<template #menu="{ entity }"> <template #menu="{ entity }">
<TicketDescriptorMenu :ticket="entity" /> <TicketDescriptorMenu :ticket="entity" />
</template> </template>
<template #description="{ entity }">
<span>
{{ entity.client.name }}
<QTooltip>{{ entity.client.name }}</QTooltip>
</span>
</template>
<template #body="{ entity }"> <template #body="{ entity }">
<QList> <VnLv v-if="entity.ticketState" :label="t('ticket.card.state')">
<QItem> <template #value>
<QItemSection v-if="entity.ticketState"> <QBadge :color="entity.ticketState.state.classColor">
<QItemLabel caption>{{ t('ticket.card.state') }}</QItemLabel> {{ entity.ticketState.state.name }}
<QItemLabel :class="stateColor(entity.ticketState.state)"> </QBadge>
{{ entity.ticketState.state.name }} </template>
</QItemLabel> </VnLv>
</QItemSection> <VnLv :label="t('ticket.card.shipped')" :value="toDate(entity.shipped)" />
<QItemSection> <VnLv :label="t('ticket.card.customerId')">
<QItemLabel caption> <template #value>
{{ t('ticket.card.shipped') }} <span class="link">
</QItemLabel> {{ entity.clientFk }}
<QItemLabel>{{ toDate(entity.shipped) }}</QItemLabel> <CustomerDescriptorProxy :id="entity.client.id" />
</QItemSection> </span>
</QItem> </template>
<QItem> </VnLv>
<QItemSection> <VnLv :label="t('ticket.summary.salesPerson')">
<QItemLabel caption> <template #value>
{{ t('ticket.card.customerId') }} <span class="link">
</QItemLabel> {{ entity.client.salesPersonUser.name }}
<QItemLabel> <WorkerDescriptorProxy
<span class="link"> :id="entity.client.salesPersonFk"
{{ entity.clientFk }} v-if="entity.client.salesPersonFk"
<CustomerDescriptorProxy :id="entity.client.id" /> />
</span> </span>
</QItemLabel> </template>
</QItemSection> </VnLv>
<QItemSection v-if="entity.client && entity.client.salesPersonUser"> <VnLv :label="t('ticket.card.warehouse')" :value="entity.warehouse?.name" />
<QItemLabel caption> <VnLv
{{ t('ticket.card.salesPerson') }} v-if="entity.agencyMode"
</QItemLabel> :label="t('ticket.card.agency')"
<QItemLabel> :value="entity.agencyMode.name"
{{ entity.client.salesPersonUser.name }} />
</QItemLabel> </template>
</QItemSection> <template #icons="{ entity }">
</QItem> <QCardActions>
<QItem>
<QItemSection v-if="entity.warehouse">
<QItemLabel caption>
{{ t('ticket.card.warehouse') }}
</QItemLabel>
<QItemLabel>{{ entity.warehouse.name }}</QItemLabel>
</QItemSection>
</QItem>
<QItem v-if="entity.agencyMode">
<QItemSection>
<QItemLabel caption>{{ t('ticket.card.agency') }}</QItemLabel>
<QItemLabel>{{ entity.agencyMode.name }}</QItemLabel>
</QItemSection>
</QItem>
</QList>
<QCardActions class="q-gutter-md">
<QIcon <QIcon
v-if="entity.isDeleted == true" v-if="entity.isDeleted == true"
name="vn:deletedTicket" name="vn:deletedTicket"
@ -147,7 +133,8 @@ function stateColor(state) {
<QTooltip>{{ t('This ticket is deleted') }}</QTooltip> <QTooltip>{{ t('This ticket is deleted') }}</QTooltip>
</QIcon> </QIcon>
</QCardActions> </QCardActions>
</template>
<template #actions="{ entity }">
<QCardActions> <QCardActions>
<QBtn <QBtn
size="md" size="md"

View File

@ -87,7 +87,7 @@ function showSmsDialog(template, customData) {
componentProps: { componentProps: {
phone: phone, phone: phone,
template: template, template: template,
locale: client.user.lang, locale: client?.user?.lang ?? 'default_locale',
data: data, data: data,
promise: sendSms, promise: sendSms,
}, },

View File

@ -0,0 +1,118 @@
<script setup>
import { date } from 'quasar';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useSession } from 'src/composables/useSession';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
const router = useRouter();
const session = useSession();
const token = session.getToken();
const entityId = computed(function () {
return router.currentRoute.value.params.id;
});
const filter = {
fields: ['ticketFk', 'smsFk'],
include: {
relation: 'sms',
scope: {
fields: [
'senderFk',
'sender',
'destination',
'message',
'statusCode',
'status',
'created',
],
include: {
relation: 'sender',
scope: {
fields: ['name'],
},
},
},
},
where: {
ticketFk: entityId,
},
};
function formatNumer(number) {
if (number.length <= 10) return number;
return number.slice(0, 4) + ' ' + number.slice(4);
}
</script>
<template>
<div class="column items-center">
<div class="list">
<VnPaginate
data-key="TicketSms"
url="TicketSms"
:filter="filter"
order="smsFk DESC"
:offset="100"
:limit="5"
auto-load
>
<template #body="{ rows }">
<QCard
flat
bordered
class="card q-pa-md"
v-for="row of rows"
:key="row.smsFk"
>
<QItem>
<QItmSection top avatar>
<QItemLabel class="column items-center">
<QAvatar>
<QImg
:src="`/api/Images/user/160x160/${row.sms.senderFk}/download?access_token=${token}`"
spinner-color="white"
/>
</QAvatar>
<span class="link">
{{ row.sms.sender.name }}
<WorkerDescriptorProxy :id="row.sms.senderFk" />
</span>
</QItemLabel>
</QItmSection>
<QSeparator spaced inset="item" />
<QItemSection>
<QItemLabel caption>{{
formatNumer(row.sms.destination)
}}</QItemLabel>
<QItemLabel>{{ row.sms.message }}</QItemLabel>
</QItemSection>
<QItemSection side top>
<QItemLabel caption>{{
date.formatDate(
row.sms.created,
'YYYY-MM-DD HH:mm:ss'
)
}}</QItemLabel>
<QItemLabel>
<QChip
:color="
row.sms.status == 'OK'
? 'positive'
: 'negative'
"
>
{{ row.sms.status }}
</QChip>
</QItemLabel>
</QItemSection>
</QItem>
</QCard>
</template>
</VnPaginate>
</div>
</div>
</template>

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,8 @@ import { toDate, toDateString, toCurrency } from 'src/filters/index';
import TicketSummaryDialog from './Card/TicketSummaryDialog.vue'; import TicketSummaryDialog from './Card/TicketSummaryDialog.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import TicketFilter from './TicketFilter.vue'; import TicketFilter from './TicketFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
const router = useRouter(); const router = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
@ -18,34 +20,6 @@ const stateStore = useStateStore();
onMounted(() => (stateStore.rightDrawer = true)); onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false)); onUnmounted(() => (stateStore.rightDrawer = false));
const filter = {
include: [
{
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['name'],
},
},
},
},
{
relation: 'ticketState',
scope: {
fields: ['stateFk', 'code', 'alertLevel'],
include: {
relation: 'state',
scope: {
fields: ['name'],
},
},
},
},
],
};
const from = Date.vnNew(); const from = Date.vnNew();
const to = Date.vnNew(); const to = Date.vnNew();
to.setDate(to.getDate() + 1); to.setDate(to.getDate() + 1);
@ -55,14 +29,6 @@ const userParams = {
to: toDateString(to), to: toDateString(to),
}; };
function stateColor(row) {
if (row.alertLevelCode === 'OK') return 'green';
if (row.alertLevelCode === 'FREE') return 'blue-3';
if (row.alertLevel === 1) return 'orange';
if (row.alertLevel === 0) return 'red';
return 'red';
}
function navigate(id) { function navigate(id) {
router.push({ path: `/ticket/${id}` }); router.push({ path: `/ticket/${id}` });
} }
@ -102,7 +68,7 @@ function viewSummary(id) {
</div> </div>
</Teleport> </Teleport>
</template> </template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256">
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit text-grey-8">
<TicketFilter data-key="TicketList" /> <TicketFilter data-key="TicketList" />
</QScrollArea> </QScrollArea>
@ -112,111 +78,55 @@ function viewSummary(id) {
<VnPaginate <VnPaginate
data-key="TicketList" data-key="TicketList"
url="Tickets/filter" url="Tickets/filter"
:filter="filter"
:user-params="userParams" :user-params="userParams"
order="id DESC" order="id DESC"
auto-load auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QCard class="card q-mb-md" v-for="row of rows" :key="row.id"> <CardList
<QItem v-for="row of rows"
class="q-pa-none items-start cursor-pointer q-hoverable" :key="row.id"
v-ripple :id="row.id"
clickable @click="navigate(row.id)"
> >
<QItemSection class="q-pa-md" @click="navigate(row.id)"> <template #list-items>
<div class="text-h6">{{ row.name }}</div> <VnLv
<QItemLabel caption>#{{ row.id }}</QItemLabel> :label="t('ticket.list.nickname')"
<QList> :value="row.nickname"
<QItem class="q-pa-none"> />
<QItemSection> <VnLv :label="t('ticket.list.state')">
<QItemLabel caption> <template #value>
{{ t('ticket.list.nickname') }} <QBadge
</QItemLabel> :color="row.classColor ?? 'orange'"
<QItemLabel> class="q-ma-none"
{{ row.nickname }} dense
</QItemLabel> >
</QItemSection> {{ row.state }}
<QItemSection> </QBadge>
<QItemLabel caption> </template>
{{ t('ticket.list.state') }} </VnLv>
</QItemLabel> <VnLv
<QItemLabel> :label="t('ticket.list.shipped')"
<QBadge :value="toDate(row.shipped)"
:color="stateColor(row)" />
class="q-ma-none" <VnLv :label="t('Zone')" :value="row.zoneName" />
dense <VnLv
> :label="t('ticket.list.salesPerson')"
{{ row.state }} :value="row.salesPerson"
</QBadge> />
</QItemLabel> <VnLv
</QItemSection> :label="t('ticket.list.total')"
</QItem> :value="toCurrency(row.totalWithVat)"
<QItem class="q-pa-none"> />
<QItemSection> </template>
<QItemLabel caption> <template #actions>
{{ t('ticket.list.shipped') }} <QBtn flat icon="preview" @click.stop="viewSummary(row.id)">
</QItemLabel> <QTooltip>
<QItemLabel> {{ t('components.smartCard.openSummary') }}
{{ toDate(row.shipped) }} </QTooltip>
</QItemLabel> </QBtn>
</QItemSection> </template>
<QItemSection> </CardList>
<QItemLabel caption>
{{ t('Zone') }}
</QItemLabel>
<QItemLabel>
{{ row.zoneName }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem class="q-pa-none">
<QItemSection>
<QItemLabel caption>
{{ t('ticket.list.salesPerson') }}
</QItemLabel>
<QItemLabel>
{{ row.salesPerson }}
</QItemLabel>
</QItemSection>
<QItemSection>
<QItemLabel caption>
{{ t('ticket.list.total') }}
</QItemLabel>
<QItemLabel>
{{ toCurrency(row.totalWithVat) }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QItemSection>
<QSeparator vertical />
<QCardActions vertical class="justify-between">
<QBtn
flat
round
color="orange"
icon="arrow_circle_right"
@click="navigate(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openCard') }}
</QTooltip>
</QBtn>
<QBtn
flat
round
color="grey-7"
icon="preview"
@click="viewSummary(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
</QCardActions>
</QItem>
</QCard>
</template> </template>
</VnPaginate> </VnPaginate>
</div> </div>

View File

@ -0,0 +1,29 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
import LeftMenu from 'components/LeftMenu.vue';
const stateStore = useStateStore();
const route = useRoute();
const { t } = useI18n();
</script>
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage>
</QPageContainer>
</template>
<i18n>
es:
Search customer: Buscar cliente
You can search by customer id or name: Puedes buscar por id o nombre del cliente
</i18n>

View File

@ -0,0 +1,154 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useSession } from 'src/composables/useSession';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import VnConfirm from 'components/ui/VnConfirm.vue';
const quasar = useQuasar();
const { t } = useI18n();
const session = useSession();
const token = session.getToken();
const counters = ref({
alquilerBandeja: { count: 0, id: 96001, title: 'CC Bandeja', isTray: true },
bandejaRota: { count: 0, id: 88381, title: 'CC Bandeja Rota', isTray: true },
carryOficial: { count: 0, id: 96000, title: 'CC Carry OFICIAL TAG5' },
candadoRojo: { count: 0, id: 96002, title: 'CC Carry NO OFICIAL' },
sacadores: { count: 0, id: 142260, title: 'CC Sacadores' },
sinChapa: { count: 0, id: 2214, title: 'DC Carry Sin Placa CC' },
carroRoto: { count: 0, id: 142251, title: 'Carro Roto' },
});
const actions = {
add: (counter) => counter + 1,
subtract: (counter) => (counter ? counter - 1 : 0),
flush: () => 0,
addSpecific: (counter, amount) => counter + amount,
};
onMounted(() => {
const types = Object.keys(counters.value);
for (let type of types) {
const counter = localStorage.getItem(type);
counters.value[type].count = counter ? parseInt(counter) : 0;
}
});
function getUrl(id) {
return `/api/Images/catalog/200x200/${id}/download?access_token=${token}`;
}
async function handleEvent(type, action, amount) {
const counter = counters.value[type].count;
let isOk = true;
if (action == 'flush') isOk = await confirm();
if (isOk) {
counters.value[type].count = actions[action](counter, amount);
localStorage.setItem(type, counters.value[type].count);
}
}
function confirm() {
return new Promise((resolve) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Are you sure?'),
message: t('The counter will be reset to zero'),
},
})
.onOk(() => resolve(true))
.onCancel(() => resolve(false));
});
}
</script>
<template>
<QList class="row q-mx-auto q-mt-xl">
<QItem v-for="(props, name) in counters" :key="name" class="col-6">
<QItemSection>
<QImg
:src="getUrl(props.id)"
width="130px"
@click="handleEvent(name, 'add')"
/>
<p class="title">{{ props.title }}</p>
</QItemSection>
<QItemSection class="q-ma-none">
<QItemLabel class="text-h4">
{{ props.count }}
</QItemLabel>
<QItemLabel>
<QBtn
class="text-center q-mr-xs"
color="warning"
dense
size="sm"
v-if="props.isTray"
@click="handleEvent(name, 'addSpecific', 30)"
>
{{ t('Add 30') }}
</QBtn>
<QBtn
class="text-center q-mr-xs"
color="warning"
dense
size="sm"
v-else
@click="handleEvent(name, 'addSpecific', 10)"
>
{{ t('Add 10') }}
</QBtn>
</QItemLabel>
<QItemLabel>
<QBtn
class="text-center q-mr-xs"
color="warning"
dense
size="sm"
@click="handleEvent(name, 'subtract')"
>
{{ t('Subtract 1') }}
</QBtn>
<QBtn
class="text-center q-ml-xs"
color="red"
dense
size="sm"
@click="handleEvent(name, 'flush')"
>
{{ t('Flush') }}
</QBtn>
</QItemLabel>
</QItemSection>
<QSeparator class="q-mt-sm q-mx-none" color="primary" />
</QItem>
</QList>
</template>
<style lang="scss" scoped>
.q-list {
max-width: 50em;
}
@media (max-width: $breakpoint-sm) {
.q-item {
display: flex;
flex-direction: column;
}
.title {
min-height: 3em;
}
}
</style>
<i18n>
es:
Subtract 1: Quitar 1
Add 30: Añadir 30
Add 10: Añadir 10
Flush: Vaciar
Are you sure?: ¿Estás seguro?
It will set to 0: Se pondrá a 0
The counter will be reset to zero: Se pondrá el contador a cero
</i18n>

View File

@ -20,7 +20,7 @@ const $props = defineProps({
}); });
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
let wagonTypes; let wagonTypes = [];
let originalData = {}; let originalData = {};
const wagon = ref({}); const wagon = ref({});
const filteredWagonTypes = ref(wagonTypes); const filteredWagonTypes = ref(wagonTypes);

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
<QPage class="q-pa-md"> <QPage>
<RouterView></RouterView> <QToolbar class="bg-vn-dark justify-end">
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<div class="q-pa-md"><RouterView></RouterView></div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>
</template> </template>

View File

@ -4,7 +4,8 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
const $props = defineProps({ const $props = defineProps({
id: { id: {
type: Number, type: Number,
@ -46,25 +47,38 @@ const filter = {
], ],
}; };
const sip = computed(() => worker.value.sip && worker.value.sip.extension); const sip = computed(() => worker.value?.sip && worker.value.sip.extension);
function getWorkerAvatar() { function getWorkerAvatar() {
const token = getToken(); const token = getToken();
return `/api/Images/user/160x160/${route.params.id}/download?access_token=${token}`; return `/api/Images/user/160x160/${route.params.id}/download?access_token=${token}`;
} }
const data = ref(useCardDescription());
const setData = (entity) => {
if (!entity) return;
data.value = useCardDescription(entity.user.nickname, entity.id);
};
</script> </script>
<template> <template>
<CardDescriptor <CardDescriptor
module="Worker" module="Worker"
data-key="workerData"
:url="`Workers/${entityId}`" :url="`Workers/${entityId}`"
:filter="filter" :filter="filter"
@on-fetch="(data) => (worker = data)" :title="data.title"
:subtitle="data.subtitle"
@on-fetch="
(data) => {
worker = data;
setData(data);
}
"
> >
<template #before> <template #before>
<QImg :src="getWorkerAvatar()" class="photo"> <QImg :src="getWorkerAvatar()" class="photo">
<template #error> <template #error>
<div <div
class="absolute-full bg-grey-10 text-center q-pa-md flex flex-center" class="absolute-full picture text-center q-pa-md flex flex-center"
> >
<div> <div>
<div class="text-grey-5" style="opacity: 0.4; font-size: 5vh"> <div class="text-grey-5" style="opacity: 0.4; font-size: 5vh">
@ -78,55 +92,15 @@ function getWorkerAvatar() {
</template> </template>
</QImg> </QImg>
</template> </template>
<template #description="{ entity }">
<span>
{{ entity.user.nickname }}
<QTooltip>{{ entity.user.nickname }}</QTooltip>
</span>
</template>
<template #body="{ entity }"> <template #body="{ entity }">
<QList> <VnLv :label="t('worker.card.name')" :value="entity.user?.nickname" />
<QItem> <VnLv :label="t('worker.card.email')" :value="entity.user?.email"> </VnLv>
<QItemSection> <VnLv
<QItemLabel caption> {{ t('worker.card.name') }} </QItemLabel> :label="t('worker.list.department')"
<QItemLabel>{{ entity.user.nickname }}</QItemLabel> :value="entity.department ? entity.department.department.name : null"
</QItemSection> />
</QItem> <VnLv :label="t('worker.card.phone')" :value="entity.phone" />
<QItem> <VnLv :label="t('worker.summary.sipExtension')" :value="sip" />
<QItemSection>
<QItemLabel caption>
{{ t('worker.card.email') }}
</QItemLabel>
<QItemLabel>{{ entity.user.email }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('worker.list.department') }}
</QItemLabel>
<QItemLabel>
{{ entity.department.department.name }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('worker.card.phone') }}
</QItemLabel>
<QItemLabel>{{ entity.phone }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption
>{{ t('worker.summary.sipExtension') }}
</QItemLabel>
<QItemLabel>{{ sip }}</QItemLabel>
</QItemSection>
</QItem>
</QList>
</template> </template>
</CardDescriptor> </CardDescriptor>
</template> </template>

View File

@ -1,10 +1,12 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { computed, onMounted, onUpdated, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import CrudModel from 'components/CrudModel.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
type: Number, type: Number,
@ -12,131 +14,139 @@ const $props = defineProps({
default: null, default: null,
}, },
}); });
const entityId = computed(() => $props.id || route.params.id);
onMounted(() => fetch());
onUpdated(() => fetch());
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const entityId = computed(() => $props.id || route.params.id);
const notifications = ref([]); const URL_KEY = 'NotificationSubscriptions';
const active = ref();
async function fetch() { const available = ref();
try {
await axios
.get(`NotificationSubscriptions/${entityId.value}/getList`)
.then(async (res) => {
if (res.data) {
notifications.value = res.data;
}
});
} catch (e) {
//
}
}
async function disableNotification(notification) {
await axios
.delete(`NotificationSubscriptions/${notification.id}`)
.catch(() => (notification.active = true))
.then((res) => {
if (res.data) {
notification.id = null;
notification.active = false;
quasar.notify({
type: 'positive',
message: t('worker.notificationsManager.unsubscribed'),
});
}
});
}
async function toggleNotification(notification) { async function toggleNotification(notification) {
if (!notification.active) { try {
await disableNotification(notification); if (!notification.active) {
} else { await axios.delete(`${URL_KEY}/${notification.id}`);
await axios swapEntry(active.value, available.value, notification.notificationFk);
.post(`NotificationSubscriptions`, { } else {
const { data } = await axios.post(URL_KEY, {
notificationFk: notification.notificationFk, notificationFk: notification.notificationFk,
userFk: entityId.value, userFk: entityId.value,
})
.catch(() => (notification.active = false))
.then((res) => {
if (res.data) {
notification.id = res.data.id;
quasar.notify({
type: 'positive',
message: t('worker.notificationsManager.subscribed'),
});
}
}); });
notification.id = data.id;
swapEntry(available.value, active.value, notification.notificationFk);
}
quasar.notify({
type: 'positive',
message: t(
`worker.notificationsManager.${notification.active ? '' : 'un'}subscribed`
),
});
} catch {
notification.active = !notification.active;
} }
} }
const swapEntry = (from, to, key) => {
const element = from.get(key);
to.set(key, element);
from.delete(key);
};
function setNotifications(data) {
active.value = new Map(data.active);
available.value = new Map(data.available);
}
</script> </script>
<template> <template>
<QPage> <CrudModel
<QCard class="q-pa-md"> auto-load
<QList> :data-key="URL_KEY"
<div :url="`${URL_KEY}/${entityId}/getList`"
v-show=" :default-reset="false"
notifications.filter( :default-remove="false"
(notification) => notification.active == true :default-save="false"
).length @on-fetch="setNotifications"
" >
> <template #body>
<QItemLabel header class="text-h6"> <div
{{ t('worker.notificationsManager.activeNotifications') }} v-for="(notifications, index) in [
</QItemLabel> [...active.values()],
<QItem> [...available.values()],
<div ]"
v-for="notification in notifications.filter( :key="notifications"
(notification) => notification.active == true >
)" <QList class="notificationList">
:key="notification.id" <TransitionGroup>
> <QCard
<QChip
:key="notification.id"
:label="notification.name"
text-color="white"
color="primary"
class="q-mr-sm"
removable
@remove="disableNotification(notification)"
/>
</div>
</QItem>
</div>
<div v-show="notifications.length">
<QItemLabel header class="text-h6">
{{ t('worker.notificationsManager.availableNotifications') }}
</QItemLabel>
<div class="row">
<QItem
class="col-3"
:key="notification.notificationFk"
v-for="notification in notifications" v-for="notification in notifications"
:key="notification.notificationFk"
class="q-pa-md"
> >
<QItemSection> <QItem>
<QItemLabel>{{ notification.name }}</QItemLabel> <QItemSection avatar>
<QItemLabel caption>{{ <QBtn
notification.description round
}}</QItemLabel> icon="mail"
</QItemSection> :color="notification.active ? 'green' : 'grey'"
<QItemSection side top> />
</QItemSection>
<QItemSection>
<QItemLabel>{{ notification.name }}</QItemLabel>
<QItemLabel caption>
{{ notification.description }}
</QItemLabel>
</QItemSection>
<QToggle <QToggle
checked-icon="check" checked-icon="check"
unchecked-icon="close" unchecked-icon="close"
indeterminate-icon="block"
v-model="notification.active" v-model="notification.active"
color="green"
@update:model-value="toggleNotification(notification)" @update:model-value="toggleNotification(notification)"
/> />
</QItemSection> </QItem>
</QItem> </QCard>
</div> </TransitionGroup>
</div> </QList>
</QList> <QSeparator
</QCard> color="primary"
</QPage> class="q-my-lg"
v-if="!index && available.size && active.size"
/>
</div>
</template>
</CrudModel>
</template> </template>
<style lang="scss" scoped>
.notificationList {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 10px;
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
}
@media (max-width: $breakpoint-md) {
.notificationList {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: $breakpoint-xs) {
.notificationList {
grid-template-columns: repeat(1, 1fr);
}
}
</style>

View File

@ -1,13 +1,12 @@
<script setup> <script setup>
import axios from 'axios'; import { ref, onMounted, computed } from 'vue';
import { ref, onMounted, computed, onUpdated } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue';
import WorkerDescriptorProxy from './WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from './WorkerDescriptorProxy.vue';
import { dashIfEmpty } from 'src/filters';
onMounted(() => fetch());
onUpdated(() => fetch());
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -20,8 +19,11 @@ const $props = defineProps({
}); });
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
const workerUrl = ref();
const worker = ref(null); onMounted(async () => {
workerUrl.value = (await getUrl('')) + `worker/${entityId.value}/`;
});
const filter = { const filter = {
include: [ include: [
@ -59,230 +61,59 @@ const filter = {
}, },
], ],
}; };
function fetch() {
const id = entityId.value;
axios.get(`/Workers/${id}`, { params: { filter } }).then((response) => {
worker.value = response.data;
});
}
function sipExtension() {
if (worker.value.sip) return worker.value.sip.extension;
return '-';
}
</script> </script>
<template> <template>
<div class="summary container"> <CardSummary ref="summary" :url="`Workers/${entityId}`" :filter="filter">
<QCard> <template #header="{ entity }">
<SkeletonSummary v-if="!worker" /> <div>{{ entity.id }} - {{ entity.firstName }} {{ entity.lastName }}</div>
<template v-if="worker"> </template>
<div class="header bg-primary q-pa-sm q-mb-md"> <template #body="{ entity: worker }">
{{ worker.id }} - {{ worker.firstName }} {{ worker.lastName }} <QCard class="vn-one">
</div> <a class="header" :href="workerUrl + `basic-data`">
<div class="row q-pa-md q-col-gutter-md q-mb-md"> {{ t('worker.summary.basicData') }}
<div class="col"> <QIcon name="open_in_new" color="primary" />
<QList> </a>
<QItemLabel header class="text-h6"> <VnLv :label="t('worker.card.name')" :value="worker.user.nickname" />
{{ t('worker.summary.basicData') }} <VnLv
</QItemLabel> :label="t('worker.list.department')"
<QItem> :value="worker.department.department.name"
<QItemSection> />
<QItemLabel caption> ID </QItemLabel> <VnLv :label="t('worker.list.email')" :value="worker.user.email" />
<QItemLabel>{{ worker.id }}</QItemLabel> <VnLv :label="t('worker.summary.boss')" link>
</QItemSection> <template #value>
</QItem> <span class="link">
<QItem> {{ dashIfEmpty(worker.boss?.name) }}
<QItemSection> <WorkerDescriptorProxy
<QItemLabel caption :id="worker.bossFk"
>{{ t('worker.card.name') }}
</QItemLabel>
<QItemLabel>
{{ worker.user.nickname }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption
>{{ t('worker.list.department') }}
</QItemLabel>
<QItemLabel>{{
worker.department.department.name
}}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption
>{{ t('worker.list.email') }}
</QItemLabel>
<QItemLabel>{{ worker.user.email }}</QItemLabel>
</QItemSection>
</QItem>
<QItem
class="items-start cursor-pointer q-hoverable"
v-if="worker.boss" v-if="worker.boss"
> />
<QItemSection> </span>
<QItemLabel caption> </template>
{{ t('worker.summary.boss') }} </VnLv>
</QItemLabel> <VnLv
<QItemLabel> :label="t('worker.summary.phoneExtension')"
<span class="link"> :value="worker.mobileExtension"
{{ worker.boss.name }} />
<WorkerDescriptorProxy :id="worker.bossFk" /> <VnLv :label="t('worker.summary.entPhone')" :value="worker.phone" />
</span> <VnLv
</QItemLabel> :label="t('worker.summary.personalPhone')"
</QItemSection> :value="worker.client.phone"
</QItem> />
<QItem> <VnLv :label="t('worker.summary.locker')" :value="worker.locker" />
<QItemSection> </QCard>
<QItemLabel caption <QCard class="vn-one">
>{{ t('worker.summary.phoneExtension') }} <div class="header">
</QItemLabel> {{ t('worker.summary.userData') }}
<QItemLabel>
{{
worker.mobileExtension == ''
? worker.mobileExtension
: '-'
}}
</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption
>{{ t('worker.summary.entPhone') }}
</QItemLabel>
<QItemLabel>{{
worker.phone == '' ? worker.phone : '-'
}}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption
>{{ t('worker.summary.personalPhone') }}
</QItemLabel>
<QItemLabel>{{
worker.client.phone == ''
? worker.client.phone
: '-'
}}</QItemLabel>
</QItemSection>
</QItem>
</QList>
</div>
<div class="col">
<QList>
<QItemLabel header class="text-h6">
{{ t('worker.summary.userData') }}
</QItemLabel>
<QItem>
<QItemSection>
<QItemLabel caption>
{{ t('worker.summary.userId') }}
</QItemLabel>
<QItemLabel>{{ worker.user.id }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption
>{{ t('worker.card.name') }}
</QItemLabel>
<QItemLabel>{{ worker.user.nickname }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption
>{{ t('worker.summary.role') }}
</QItemLabel>
<QItemLabel>{{ worker.user.role.name }}</QItemLabel>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QItemLabel caption
>{{ t('worker.summary.sipExtension') }}
</QItemLabel>
<QItemLabel>{{ sipExtension() }}</QItemLabel>
</QItemSection>
</QItem>
</QList>
</div>
</div> </div>
</template> <VnLv :label="t('worker.summary.userId')" :value="worker.user.id" />
</QCard> <VnLv :label="t('worker.card.name')" :value="worker.user.nickname" />
</div> <VnLv :label="t('worker.summary.role')" :value="worker.user.role.name" />
<VnLv
:label="t('worker.summary.sipExtension')"
:value="worker?.sip?.extension"
/>
</QCard>
</template>
</CardSummary>
</template> </template>
<style lang="scss" scoped>
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
max-width: 1200px;
}
.negative {
color: red;
}
.summary {
.q-list {
.q-item__label--header {
display: flex;
justify-content: space-between;
a {
color: $primary;
}
}
}
.row {
flex-wrap: wrap;
.col {
min-width: 250px;
}
}
.header {
text-align: center;
font-size: 18px;
}
#slider-container {
max-width: 80%;
margin: 0 auto;
.q-slider {
.q-slider__marker-labels:nth-child(1) {
transform: none;
}
.q-slider__marker-labels:nth-child(2) {
transform: none;
left: auto !important;
right: 0%;
}
}
}
}
.q-dialog .summary {
max-width: 1200px;
}
</style>

View File

@ -7,6 +7,8 @@ import VnPaginate from 'src/components/ui/VnPaginate.vue';
import WorkerSummaryDialog from './Card/WorkerSummaryDialog.vue'; import WorkerSummaryDialog from './Card/WorkerSummaryDialog.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import WorkerFilter from './WorkerFilter.vue'; import WorkerFilter from './WorkerFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const router = useRouter(); const router = useRouter();
@ -66,73 +68,38 @@ function viewSummary(id) {
auto-load auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QCard class="card q-mb-md" v-for="row of rows" :key="row.id"> <CardList
<QItem v-for="row of rows"
class="q-pa-none items-start cursor-pointer q-hoverable" :key="row.id"
v-ripple @click="navigate(row.id)"
clickable :title="row.nickname"
> >
<QItemSection class="q-pa-md" @click="navigate(row.id)"> <template #list-items>
<QItemLabel class="text-h6"> <VnLv label="ID" :value="row.id" />
{{ row.nickname }} <VnLv :label="t('worker.list.name')" :value="row.userName" />
</QItemLabel> <VnLv :label="t('worker.list.email')" :value="row.email" />
<QItemLabel caption>#{{ row.id }}</QItemLabel> <VnLv
<QList> :label="t('worker.list.department')"
<QItem class="q-pa-none"> :value="row.department"
<QItemSection> />
<QItemLabel caption> </template>
{{ t('worker.list.name') }} <template #actions>
</QItemLabel> <QBtn
<QItemLabel>{{ row.userName }}</QItemLabel> flat
</QItemSection> icon="arrow_circle_right"
</QItem> @click.stop="navigate(row.id)"
<QItem class="q-pa-none"> >
<QItemSection> <QTooltip>
<QItemLabel caption> {{ t('components.smartCard.openCard') }}
{{ t('worker.list.email') }} </QTooltip>
</QItemLabel> </QBtn>
<QItemLabel>{{ row.email }}</QItemLabel> <QBtn flat icon="preview" @click.stop="viewSummary(row.id)">
</QItemSection> <QTooltip>
</QItem> {{ t('components.smartCard.openSummary') }}
<QItem class="q-pa-none"> </QTooltip>
<QItemSection> </QBtn>
<QItemLabel caption>{{ </template>
t('worker.list.department') </CardList>
}}</QItemLabel>
<QItemLabel>
{{ row.department }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QItemSection>
<QSeparator vertical />
<QCardActions vertical class="justify-between">
<QBtn
flat
round
color="primary"
icon="arrow_circle_right"
@click="navigate(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openCard') }}
</QTooltip>
</QBtn>
<QBtn
flat
round
color="grey-7"
icon="preview"
@click="viewSummary(row.id)"
>
<QTooltip>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
</QCardActions>
</QItem>
</QCard>
</template> </template>
</VnPaginate> </VnPaginate>
</div> </div>

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