refs #6118 importSalixVariables #93

Merged
pablone merged 19 commits from 6118-responsiveFixes into dev 2023-10-30 10:32:15 +00:00
61 changed files with 2196 additions and 468 deletions
Showing only changes of commit 91a88d148d - Show all commits

View File

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

View File

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

2
package-lock.json generated
View File

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

View File

@ -1,6 +1,6 @@
{
"name": "salix-front",
"version": "23.36.01",
"version": "23.40.01",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",
@ -9,7 +9,7 @@
"lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"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:unit": "vitest",
"test:unit:ci": "vitest run"

View File

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>

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

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>

Before

Width:  |  Height:  |  Size: 10 KiB

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>

Before

Width:  |  Height:  |  Size: 10 KiB

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>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -46,7 +46,7 @@ const onResponseError = (error) => {
message = responseError.message;
}
switch (response.status) {
switch (response?.status) {
case 500:
message = 'errors.statusInternalServerError';
break;
@ -58,7 +58,7 @@ const onResponseError = (error) => {
break;
}
if (session.isLoggedIn() && response.status === 401) {
if (session.isLoggedIn() && response?.status === 401) {
session.destroy();
const hash = window.location.hash;
const url = hash.slice(1);

View File

@ -0,0 +1,324 @@
<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);
}
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 class="q-gutter-x-sm">
<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 { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore';
import { useValidator } from 'src/composables/useValidator';
import SkeletonForm from 'components/ui/SkeletonForm.vue';
const quasar = useQuasar();
const { t } = useI18n();
const state = useState();
const stateStore = useStateStore();
const { t } = useI18n();
const { validate } = useValidator();
const $props = defineProps({
@ -29,6 +31,10 @@ const $props = defineProps({
type: String,
default: null,
},
defaultActions: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['onFetch']);
@ -45,17 +51,21 @@ onUnmounted(() => {
const isLoading = ref(false);
const hasChanges = ref(false);
const formData = computed(() => state.get($props.model));
const originalData = ref();
const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url);
function tMobile(...args) {
if (!quasar.platform.is.mobile) return t(...args);
}
async function fetch() {
const { data } = await axios.get($props.url, {
params: { filter: $props.filter },
});
state.set($props.model, data);
originalData.value = Object.assign({}, data);
originalData.value = data && JSON.parse(JSON.stringify(data));
watch(formData.value, () => (hasChanges.value = true));
@ -72,13 +82,18 @@ async function save() {
isLoading.value = true;
await axios.patch($props.urlUpdate || $props.url, formData.value);
originalData.value = formData.value;
originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false;
isLoading.value = false;
}
function reset() {
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;
}
// eslint-disable-next-line vue/no-dupe-keys
@ -107,29 +122,33 @@ watch(formUrl, async () => {
<QIcon name="warning" size="md" class="q-mr-md" />
<span>{{ t('globals.changesToSave') }}</span>
</QBanner>
<QForm v-if="formData" @submit="save" @reset="reset" class="q-pa-md formModel">
<QCard class="q-pa-lg">
<slot
name="form"
:data="formData"
:validate="validate"
:filter="filter"
></slot>
<div class="q-mt-lg">
<slot name="actions">
<QBtn :label="t('globals.save')" type="submit" color="primary" />
<QBtn
:label="t('globals.reset')"
type="reset"
class="q-ml-sm"
color="primary"
flat
:disable="!hasChanges"
/>
</slot>
</div>
</QCard>
<QForm v-if="formData" @submit="save" @reset="reset" class="q-pa-md">
<slot name="form" :data="formData" :validate="validate" :filter="filter"></slot>
</QForm>
<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
pablone marked this conversation as resolved Outdated
Outdated
Review

Declarar como scoped y usar un id

Declarar como scoped y usar un id
:label="tMobile('globals.save')"
color="primary"
icon="save"
@click="save"
:disable="!hasChanges"
:title="t('globals.save')"
/>
</QBtnGroup>
</div>
</Teleport>
<SkeletonForm v-if="!formData" />
<QInnerLoading
:showing="isLoading"

View File

@ -47,7 +47,7 @@ const pinnedModulesRef = ref();
>
<QAvatar square size="md">
<QImg
src="~/assets/logo_icon.svg"
src="~/assets/salix_icon.svg"
:alt="appName"
spinner-color="primary"
/>

View File

@ -0,0 +1,95 @@
<script setup>
import { ref, toRefs, watch, computed } from 'vue';
const emit = defineEmits(['update:modelValue', 'update:options']);
const $props = defineProps({
modelValue: {
type: [String, Number],
default: null,
},
options: {
type: Array,
default: () => [],
},
optionLabel: {
type: String,
default: '',
},
});
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"
hide-selected
fill-input
ref="vnSelectRef"
>
<template #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

@ -29,12 +29,14 @@ const $props = defineProps({
const slots = useSlots();
const { t } = useI18n();
const entity = ref();
onMounted(() => fetch());
onMounted(async () => {
await fetch();
});
const emit = defineEmits(['onFetch']);
const entity = ref();
async function fetch() {
const params = {};

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

@ -20,6 +20,10 @@ const props = defineProps({
required: false,
default: null,
},
showAll: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['refresh', 'clear']);
@ -29,31 +33,34 @@ const store = arrayData.store;
const userParams = ref({});
onMounted(() => {
if (props.params) userParams.value = props.params;
const params = store.userParams;
if (Object.keys(params).length > 0) {
userParams.value = Object.assign({}, params);
if (props.params) userParams.value = JSON.parse(JSON.stringify(props.params));
if (Object.keys(store.userParams).length > 0) {
userParams.value = JSON.parse(JSON.stringify(store.userParams));
}
});
const isLoading = ref(false);
async function search() {
const params = userParams.value;
for (const param in params) {
if (params[param] === '' || params[param] === null) {
for (const param in userParams.value) {
if (userParams.value[param] === '' || userParams.value[param] === null) {
delete userParams.value[param];
delete store.userParams[param];
}
}
const params = { ...userParams.value };
isLoading.value = true;
await arrayData.addFilter({ params });
if (!props.showAll && !Object.values(params).length) store.data = [];
isLoading.value = false;
}
async function reload() {
isLoading.value = true;
const params = Object.values(userParams.value).filter((param) => param);
await arrayData.fetch({ append: false });
if (!props.showAll && !params.length) store.data = [];
isLoading.value = false;
emit('refresh');
}
@ -62,6 +69,7 @@ async function clearFilters() {
userParams.value = {};
isLoading.value = true;
await arrayData.applyFilter({ params: {} });
if (!props.showAll) store.data = [];
isLoading.value = false;
emit('clear');

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

@ -46,6 +46,10 @@ const props = defineProps({
type: Number,
default: 500,
},
skeleton: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['onFetch', 'onPaginate']);
@ -137,14 +141,9 @@ async function onLoad(...params) {
</h5>
</div>
<div
v-if="store.data && store.data.length === 0 && !isLoading"
class="info-row q-pa-md text-center"
v-if="props.skeleton && props.autoLoad && !store.data"
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">
<QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<QItemSection class="q-pa-md">
@ -164,7 +163,7 @@ async function onLoad(...params) {
</QCard>
</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>
<div v-if="isLoading" class="info-row q-pa-md text-center">
<QSpinner color="orange" size="md" />

View File

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

@ -38,11 +38,11 @@ export function useArrayData(key, userOptions) {
'limit',
'skip',
'userParams',
'userFilter'
'userFilter',
];
if (typeof userOptions === 'object') {
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 (Object.prototype.hasOwnProperty.call(store, option)) {
@ -73,7 +73,7 @@ export function useArrayData(key, userOptions) {
Object.assign(params, store.userParams);
store.isLoading = true
store.isLoading = true;
const response = await axios.get(store.url, {
signal: canceller.signal,
params,
@ -94,7 +94,7 @@ export function useArrayData(key, userOptions) {
updateStateParams();
}
store.isLoading = false
store.isLoading = false;
canceller = null;
}
@ -136,7 +136,7 @@ export function useArrayData(key, userOptions) {
}
async function refresh() {
await fetch({ append: false });
if (Object.values(store.userParams).length) await fetch({ append: false });
}
function updateStateParams() {
@ -153,8 +153,8 @@ export function useArrayData(key, userOptions) {
});
}
const totalRows = computed(() => store.data && store.data.length || 0);
const isLoading = computed(() => store.isLoading || false)
const totalRows = computed(() => (store.data && store.data.length) || 0);
const isLoading = computed(() => store.isLoading || false);
return {
fetch,
@ -167,6 +167,6 @@ export function useArrayData(key, userOptions) {
hasMoreData,
totalRows,
updateStateParams,
isLoading
isLoading,
};
}

View File

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

View File

@ -32,12 +32,18 @@ body.body--light {
--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);
}
html [vnRow],
pablone marked this conversation as resolved Outdated
Outdated
Review

VnRow no se pot?

VnRow no se pot?

View File

@ -21,6 +21,7 @@ $positive: #21ba45;
$negative: #c10015;
$info: #31ccec;
$warning: #f2c037;
$vnColor: #8ebb27;
// Pendiente de cuadrar con la base de datos
$success: $positive;

View File

@ -32,10 +32,11 @@ export default {
rowAdded: 'Row added',
rowRemoved: 'Row removed',
pleaseWait: 'Please wait...',
noPinnedModules: 'You have dont have any pinned modules',
noPinnedModules: `You don't have any pinned modules`,
summary: {
basicData: 'Basic data',
},
noSelectedRows: `You don't have any line selected`,
},
errors: {
statusUnauthorized: 'Access denied',
@ -67,6 +68,11 @@ export default {
twoFactor: 'Two-Factor',
},
},
verifyEmail: {
pageTitles: {
verifyEmail: 'Email verification',
},
},
dashboard: {
pageTitles: {
dashboard: 'Dashboard',
@ -265,6 +271,7 @@ export default {
lines: 'Lines',
rma: 'RMA',
photos: 'Photos',
development: 'Development',
log: 'Audit logs',
notes: 'Notes',
},
@ -482,15 +489,18 @@ export default {
},
cmr: {
list: {
total: 'Total records',
cmrFk: 'Cmr id',
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',
},
},
},

View File

@ -36,6 +36,7 @@ export default {
summary: {
basicData: 'Datos básicos',
},
noSelectedRows: `No tienes ninguna línea seleccionada`,
},
errors: {
statusUnauthorized: 'Acceso denegado',
@ -67,6 +68,11 @@ export default {
twoFactor: 'Doble factor',
},
},
verifyEmail: {
pageTitles: {
verifyEmail: 'Verificación de correo',
},
},
dashboard: {
pageTitles: {
dashboard: 'Tablón',
@ -263,6 +269,7 @@ export default {
basicData: 'Datos básicos',
lines: 'Líneas',
rma: 'RMA',
development: 'Trazabilidad',
photos: 'Fotos',
log: 'Registros de auditoría',
notes: 'Notas',
@ -482,15 +489,18 @@ export default {
},
cmr: {
list: {
total: 'Total registros',
cmrFk: 'Id cmr',
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',
},
},
},

View File

@ -80,7 +80,7 @@ const langs = ['en', 'es'];
<QPageContainer>
<QPage>
<div class="form">
<QCard class="q-pa-lg formCard">
<QCard class="flex flex-center">
<RouterView></RouterView>
</QCard>
</div>
@ -97,15 +97,4 @@ const langs = ['en', 'es'];
min-height: inherit;
flex-direction: column;
}
.formCard {
max-width: 350px;
min-width: 300px;
}
@media (max-width: $breakpoint-xs-max) {
.formCard {
min-width: 100%;
}
}
</style>

View File

@ -44,17 +44,6 @@ onMounted(async () => {
<LeftMenu source="card" />
<QSeparator />
<QList>
<QItem
active-class="text-primary"
clickable
v-ripple
:href="`${salixUrl}/development`"
>
<QItemSection avatar>
<QIcon name="vn:traceability"></QIcon>
</QItemSection>
<QItemSection>{{ t('Development') }}</QItemSection>
</QItem>
<QItem
active-class="text-primary"
clickable
@ -68,8 +57,13 @@ onMounted(async () => {
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage class="q-pa-md">
<RouterView></RouterView>
<QPage>
<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>
</QPageContainer>
</template>
@ -80,6 +74,5 @@ es:
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
Details: Detalles
Notes: Notas
Development: Trazabilidad
Action: Acción
</i18n>

View File

@ -3,6 +3,8 @@ import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters';
import { useState } from 'src/composables/useState';
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';
@ -19,6 +21,7 @@ const $props = defineProps({
});
const route = useRoute();
const state = useState();
const { t } = useI18n();
const entityId = computed(() => {
@ -67,6 +70,7 @@ function stateColor(code) {
const data = ref(useCardDescription());
const setData = (entity) => {
data.value = useCardDescription(entity.client.name, entity.id);
state.set('ClaimDescriptor', entity);
};
</script>

View File

@ -0,0 +1,266 @@
<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';
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 insertButtonRef = 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,
},
]);
function goToAction() {
location.href = `${salixUrl}/action`;
}
</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="goToAction"
: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"
>
<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"
/>
</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
ref="insertButtonRef"
fab
color="primary"
icon="add"
@click="claimDevelopmentForm.insert()"
@keydown.ctrl.enter.stop="claimDevelopmentForm.saveChanges()"
@keydown.enter.stop
/>
</QPageSticky>
</template>
<style lang="scss" scoped>
.grid-style-transition {
transition: transform 0.28s, background-color 0.28s;
}
.maxwidth {
width: 100%;
}
</style>
<i18n>
es:
Reason: Motivo
Result: Consecuencia
Responsible: Responsable
Worker: Trabajador
Redelivery: Devolución
</i18n>

View File

@ -6,9 +6,8 @@ import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'components/ui/VnPaginate.vue';
import CrudModel from 'components/CrudModel.vue';
import FetchData from 'components/FetchData.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import { toDate, toCurrency, toPercentage } from 'filters/index';
import VnDiscount from 'components/common/vnDiscount.vue';
@ -17,6 +16,7 @@ import ClaimLinesImport from './ClaimLinesImport.vue';
const quasar = useQuasar();
const route = useRoute();
const { t } = useI18n();
const stateStore = useStateStore();
const arrayData = useArrayData('ClaimLines');
const store = arrayData.store;
@ -36,16 +36,17 @@ const linesFilter = {
},
};
const claimLinesForm = ref();
const claim = ref(null);
async function onFetchClaim(data) {
claim.value = data;
fetchMana();
}
const amount = ref(0);
const amountClaimed = ref(0);
async function onFetch(rows) {
if (!rows || rows.length) return;
amount.value = rows.reduce(
(acumulator, { sale }) => acumulator + sale.price * sale.quantity,
0
@ -141,60 +142,20 @@ function onUpdateDiscount(response) {
});
}
async function confirmRemove() {
const rows = selected.value;
const count = rows.length;
if (count === 0) {
return quasar.notify({
message: 'You must select at least one row',
type: 'warning',
});
}
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Delete claimed sales'),
message: t('You are about to remove {count} rows', count, { count }),
data: { rows },
promise: remove,
},
})
.onOk(() => {
for (const row of rows) {
const orgData = store.data;
const index = orgData.findIndex((item) => item.id === row.id);
store.data.splice(index, 1);
selected.value = [];
}
});
}
async function remove({ rows }) {
if (!rows.length) return;
const body = { deletes: rows.map((row) => row.id) };
await axios.post(`ClaimBeginnings/crud`, body);
quasar.notify({
type: 'positive',
message: t('globals.rowRemoved'),
});
}
function showImportDialog() {
quasar
.dialog({
component: ClaimLinesImport,
componentProps: {
ticketId: claim.value.ticketFk,
},
})
.onOk(() => arrayData.refresh());
.onOk(() => claimLinesForm.value.reload());
}
</script>
<template>
<QPageSticky position="top" :offset="[0, 0]" expand>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()">
<QToolbar class="bg-dark text-white">
<QToolbarTitle> {{ t('Claimed lines') }} </QToolbarTitle>
<QSpace />
<div class="row q-gutter-md">
<div>
{{ t('Amount') }}
@ -211,7 +172,7 @@ function showImportDialog() {
</div>
</div>
</QToolbar>
</QPageSticky>
</Teleport>
<FetchData
:url="`Claims/${route.params.id}`"
@ -221,11 +182,16 @@ function showImportDialog() {
/>
<div class="column items-center">
<div class="list">
<VnPaginate
<CrudModel
data-key="ClaimLines"
ref="claimLinesForm"
:url="`Claims/${route.params.id}/lines`"
save-url="ClaimBeginnings/crud"
:filter="linesFilter"
@on-fetch="onFetch"
v-model:selected="selected"
:default-save="false"
:default-reset="false"
auto-load
>
<template #body="{ rows }">
@ -361,46 +327,12 @@ function showImportDialog() {
</template>
</QTable>
</template>
</VnPaginate>
</CrudModel>
</div>
</div>
<Teleport
v-if="stateStore.isHeaderMounted() && !$q.screen.lt.sm"
to="#actions-prepend"
>
<div class="row q-gutter-x-sm">
<QBtn
v-if="selected.length > 0"
@click="confirmRemove"
icon="delete"
color="primary"
flat
dense
rounded
>
<QTooltip bottom> {{ t('globals.remove') }} </QTooltip>
</QBtn>
<QBtn @click="showImportDialog" icon="add" color="primary" flat dense rounded>
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
</QBtn>
<QSeparator vertical />
</div>
</Teleport>
<!-- v-if="quasar.platform.is.mobile" -->
<QPageSticky v-if="$q.screen.lt.sm" 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="showImportDialog" icon="add" :label="t('globals.add')" />
<QSeparator vertical inset />
<QTab
@click="confirmRemove"
icon="delete"
:label="t('globals.remove')"
:disable="selected.length === 0"
/>
</QTabs>
</QToolbar>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="showImportDialog()" />
</QPageSticky>
</template>
@ -421,7 +353,6 @@ en:
You are about to remove <strong>{count}</strong> row |
You are about to remove <strong>{count}</strong> rows'
es:
Claimed lines: Líneas reclamadas
Delivered: Entregado
Quantity: Cantidad
Claimed: Reclamada

View File

@ -14,6 +14,13 @@ const route = useRoute();
const quasar = useQuasar();
const { t } = useI18n();
const $props = defineProps({
ticketId: {
type: Number,
required: true,
},
});
const columns = computed(() => [
{
name: 'delivered',
@ -99,7 +106,7 @@ function cancel() {
</script>
<template>
<FetchData
url="Sales/getClaimableFromTicket?ticketFk=16"
:url="`Sales/getClaimableFromTicket?ticketFk=${$props.ticketId}`"
@on-fetch="(data) => (claimableSales = data)"
auto-load
/>

View File

@ -25,7 +25,7 @@ const body = {
};
</script>
<template>
<div class="col items-center">
<div class="column items-center">
<VnNotes
:add-note="true"
:id="id"

View File

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

View File

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

View File

@ -1,11 +1,13 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const route = useRoute();
const { t } = useI18n();
</script>
<template>
@ -25,8 +27,13 @@ const { t } = useI18n();
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage class="q-pa-md">
<RouterView></RouterView>
<QPage>
<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>
</QPageContainer>
</template>

View File

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

View File

@ -9,10 +9,14 @@ const props = defineProps({
required: true,
},
});
function isValidNumber(value) {
return /^(\d|\d+(\.|,)?\d+)$/.test(value);
}
</script>
<template>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<VnFilterPanel :data-key="props.dataKey" :search-button="true" :show-all="false">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
@ -49,9 +53,99 @@ const props = defineProps({
</QItem>
<QItem>
<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>
<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>
</QInput>
</QItemSection>
@ -67,12 +161,19 @@ en:
orderFk: Order
clientFk: Customer
amount: Amount
from: From
to: To
es:
params:
orderFk: Pedido
clientFk: Cliente
amount: Importe
from: Desde
to: Hasta
Order ID: ID pedido
Customer ID: ID cliente
Amount: Importe
Please type a number: Por favor, escriba un número
From: Desde
To: Hasta
</i18n>

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage class="q-pa-md">
<RouterView></RouterView>
<QPage>
<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>
</QPageContainer>
</template>

View File

@ -8,6 +8,8 @@ import axios from 'axios';
import { useSession } from 'src/composables/useSession';
import { useLogin } from 'src/composables/useLogin';
import VnLogo from 'components/ui/VnLogo.vue';
const quasar = useQuasar();
const session = useSession();
const loginCache = useLogin();
@ -64,14 +66,8 @@ async function onSubmit() {
</script>
<template>
<QImg
src="~/assets/logo.svg"
alt="Logo"
fit="contain"
:ratio="16 / 9"
class="q-mb-md"
/>
<QForm @submit="onSubmit" class="q-gutter-md q-pa-lg">
<QForm @submit="onSubmit" class="q-gutter-y-md q-pa-lg formCard">
<VnLogo alt="Logo" fit="contain" :ratio="16 / 9" class="q-mb-md" />
<QInput
v-model="username"
:label="t('login.username')"
@ -100,4 +96,15 @@ async function onSubmit() {
</QForm>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.formCard {
max-width: 350px;
min-width: 300px;
}
@media (max-width: $breakpoint-xs-max) {
.formCard {
min-width: 100%;
}
}
</style>

View File

@ -48,7 +48,7 @@ async function onSubmit() {
}
</script>
<template>
<QForm @submit="onSubmit" class="q-gutter-md q-pa-lg">
<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>
@ -77,4 +77,15 @@ async function onSubmit() {
</div>
</QForm>
</template>
<style lang="scss" scoped></style>
<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

@ -28,7 +28,11 @@ const countries = ref();
<QList dense>
<QItem>
<QItemSection>
<QInput :label="t('route.cmr.list.cmrFk')" v-model="params.cmrFk" lazy-rules>
<QInput
:label="t('route.cmr.list.cmrFk')"
v-model="params.cmrFk"
lazy-rules
>
<template #prepend>
<QIcon name="article" size="sm"></QIcon>
</template>
@ -46,7 +50,11 @@ const countries = ref();
</QItem>
<QItem>
<QItemSection>
<QInput :label="t('route.cmr.list.ticketFk')" v-model="params.ticketFk" lazy-rules>
<QInput
:label="t('route.cmr.list.ticketFk')"
v-model="params.ticketFk"
lazy-rules
>
<template #prepend>
<QIcon name="vn:ticket" size="sm"></QIcon>
</template>
@ -55,7 +63,24 @@ const countries = ref();
</QItem>
<QItem>
<QItemSection>
<QInput :label="t('route.cmr.list.clientFk')" v-model="params.clientFk" lazy-rules>
<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>
@ -78,9 +103,9 @@ const countries = ref();
emit-value
map-options
>
<template #prepend>
<QIcon name="flag" size="sm"></QIcon>
</template>
<template #prepend>
<QIcon name="flag" size="sm"></QIcon>
</template>
</QSelect>
</QItemSection>
</QItem>
@ -99,7 +124,9 @@ const countries = ref();
transition-hide="rotate"
>
<QDate v-model="params.shipped" minimal>
<div class="row items-center justify-end q-gutter-sm">
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="t('globals.close')"
color="primary"

View File

@ -1,16 +1,20 @@
<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(() => [
{
@ -33,6 +37,12 @@ const columns = computed(() => [
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'),
@ -54,10 +64,31 @@ const columns = computed(() => [
sortable: true,
headerStyle: 'padding-left: 33px',
},
{
name: 'icons',
align: 'center',
field: (row) => row.cmrFk,
},
]);
function getProjectUrl() {
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">
@ -76,21 +107,18 @@ function getProjectUrl() {
: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>
{{ `${t('route.cmr.list.total')}: ${rows.length}` }}
</template>
<template #body-cell-cmrFk="{ value }">
<QTd align="right" class="text-primary">
<a
:href="`${getProjectUrl()}/api/Routes/${value}/cmr?access_token=${token}`"
target="_blank"
>
<span class="text-primary">{{ value }}</span>
</a>
</QTd>
<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">
@ -104,13 +132,46 @@ function getProjectUrl() {
/>
</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>
@ -120,7 +181,7 @@ function getProjectUrl() {
.list {
padding-top: 15px;
padding-bottom: 15px;
max-width: 900px;
max-width: 1000px;
width: 100%;
}
.grid-style-transition {

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage class="q-pa-md">
<RouterView></RouterView>
<QPage>
<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>
</QPageContainer>
</template>

View File

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

View File

@ -25,8 +25,13 @@ const { t } = useI18n();
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage class="q-pa-md">
<RouterView></RouterView>
<QPage>
<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>
</QPageContainer>
</template>

View File

@ -1,6 +1,5 @@
<script setup>
import axios from 'axios';
import { ref, onMounted, computed, onUpdated } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardSummary from 'components/ui/CardSummary.vue';

View File

@ -45,7 +45,7 @@ export { Router };
export default route(function (/* { store, ssrContext } */) {
Router.beforeEach(async (to, from, next) => {
const { isLoggedIn } = session;
const outLayout = ['Login', 'TwoFactor'];
const outLayout = ['Login', 'TwoFactor', 'VerifyEmail'];
if (!isLoggedIn() && !outLayout.includes(to.name)) {
return next({ name: 'Login', query: { redirect: to.fullPath } });
}

View File

@ -18,6 +18,7 @@ export default {
'ClaimPhotos',
'ClaimLog',
'ClaimNotes',
'ClaimDevelopment',
],
},
children: [
@ -101,6 +102,16 @@ export default {
},
component: () => import('src/pages/Claim/Card/ClaimPhoto.vue'),
},
{
name: 'ClaimDevelopment',
path: 'development',
meta: {
title: 'development',
icon: 'vn:traceability',
roles: ['claimManager'],
},
component: () => import('src/pages/Claim/Card/ClaimDevelopment.vue'),
},
{
name: 'ClaimLog',
path: 'log',

View File

@ -23,6 +23,12 @@ const routes = [
meta: { title: 'twoFactor' },
component: () => import('../pages/Login/TwoFactor.vue'),
},
{
path: '/verifyEmail',
name: 'VerifyEmail',
meta: { title: 'verifyEmail' },
component: () => import('../pages/Login/VerifyEmail.vue'),
},
],
},
{

View File

@ -30,6 +30,13 @@ export const useStateStore = defineStore('stateStore', () => {
return rightDrawer.value;
}
function isSubToolbarShown() {
return (
!!document.querySelector('#st-data') &&
!!document.querySelector('#st-actions')
);
}
return {
leftDrawer,
rightDrawer,
@ -39,5 +46,6 @@ export const useStateStore = defineStore('stateStore', () => {
toggleRightDrawer,
isLeftDrawerShown,
isRightDrawerShown,
isSubToolbarShown,
};
});

View File

@ -0,0 +1,58 @@
/// <reference types="cypress" />
describe('ClaimDevelopment', () => {
const claimId = 1;
const firstLineReason = 'tbody > :nth-child(1) > :nth-child(2)';
const thirdRow = 'tbody > :nth-child(3)';
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/claim/${claimId}/development`);
});
it('should reset line', () => {
cy.selectOption(firstLineReason, 'Novato');
cy.resetCard();
cy.getValue(firstLineReason).should('have.value', 'Prisas');
});
it('should edit line', () => {
cy.selectOption(firstLineReason, 'Novato');
cy.saveCard();
cy.login('developer');
cy.visit(`/#/claim/${claimId}/development`);
cy.getValue(firstLineReason).should('have.value', 'Novato');
//Restart data
cy.selectOption(firstLineReason, 'Prisas');
cy.saveCard();
});
it('should add and remove new line', () => {
cy.addCard();
cy.get(thirdRow).should('exist');
const rowData = [false, 'Novato', 'Roces', 'Compradores', 'employeeNick', 'Tour'];
cy.fillRow(thirdRow, rowData);
cy.saveCard();
cy.login('developer');
cy.visit(`/#/claim/${claimId}/development`);
cy.validateRow(thirdRow, rowData);
cy.reload();
cy.validateRow(thirdRow, rowData);
//remove row
cy.fillRow(thirdRow, [true]);
cy.removeCard();
cy.clickConfirm();
cy.get(thirdRow).should('not.exist');
cy.reload();
cy.get(thirdRow).should('not.exist');
});
});

View File

@ -8,18 +8,18 @@ describe('WorkerList', () => {
it('should load workers', () => {
cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span')
.eq(0)
.should('have.text', 'victorvd');
cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span')
.eq(1)
.should('have.text', 'JessicaJones');
cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span')
.eq(2)
.eq(1)
.should('have.text', 'BruceBanner');
cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span')
.eq(2)
.should('have.text', 'CharlesXavier');
});
it('should open the worker summary', () => {
cy.get('.card-list-body .actions .q-btn:nth-child(2)').eq(1).click();
cy.get('.summaryHeader div').should('have.text', '1110 - Jessica Jones');
cy.get('.summaryHeader div').should('have.text', '1109 - Bruce Banner');
cy.get('.summary .header').eq(0).invoke('text').should('include', 'Basic data');
cy.get('.summary .header').eq(1).should('have.text', 'User data');
});

View File

@ -40,4 +40,90 @@ Cypress.Commands.add('login', (user) => {
window.localStorage.setItem('token', response.body.token);
});
});
Cypress.Commands.add('waitForElement', (element) => {
cy.get(element, { timeout: 2000 }).should('be.visible');
});
Cypress.Commands.add('getValue', (selector) => {
cy.get(selector).then(($el) => {
if ($el.find('.q-checkbox__inner').length > 0) {
return cy.get(selector + '.q-checkbox__inner');
}
// Si es un QSelect
else if ($el.find('.q-select__dropdown-icon').length) {
return cy.get(
selector +
'> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input'
);
} else {
// Puedes añadir un log o lanzar un error si el elemento no es reconocido
cy.log('Elemento no soportado');
}
});
});
// Fill Inputs
Cypress.Commands.add('selectOption', (selector, option) => {
cy.get(selector).find('.q-select__dropdown-icon').click();
cy.get('.q-menu .q-item').contains(option).click();
});
Cypress.Commands.add('checkOption', (selector) => {
cy.wrap(selector).find('.q-checkbox__inner').click();
});
// Global buttons
Cypress.Commands.add('saveCard', () => {
cy.get('[title="Save"]').click();
});
Cypress.Commands.add('resetCard', () => {
cy.get('[title="Reset"]').click();
});
Cypress.Commands.add('removeCard', () => {
cy.get('[title="Remove"]').click();
});
Cypress.Commands.add('addCard', () => {
cy.waitForElement('tbody');
cy.get('.q-page-sticky > div > .q-btn').click();
});
Cypress.Commands.add('clickConfirm', () => {
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
});
Cypress.Commands.add('fillRow', (rowSelector, data) => {
// Usar el selector proporcionado para obtener la fila deseada
cy.waitForElement('tbody');
cy.get(rowSelector).as('currentRow');
data.forEach((value, index) => {
if (value === null) return;
cy.get('@currentRow')
.find('td')
.eq(index)
.then((td) => {
if (td.find('.q-select__dropdown-icon').length) {
cy.selectOption(td, value);
}
if (td.find('.q-checkbox__inner').length && value) {
cy.checkOption(td);
}
});
});
});
Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
cy.waitForElement('tbody');
cy.get(rowSelector).within(() => {
for (const [index, value] of expectedValues.entries()) {
cy.log('CHECKING ', index, value);
if (typeof value == 'boolean') {
const prefix = value ? '' : 'not.';
cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`);
continue;
}
cy.getValue(`:nth-child(${index + 1})`).should('have.value', value);
}
});
});
// registerCommands();

View File

@ -0,0 +1,120 @@
import { createWrapper } from 'app/test/vitest/helper';
import CrudModel from 'components/CrudModel.vue';
import { vi, afterEach, beforeEach, beforeAll, describe, expect, it } from 'vitest';
describe('CrudModel', () => {
let vm;
beforeAll(() => {
vm = createWrapper(CrudModel, {
global: {
stubs: [
'vnPaginate',
'useState',
'arrayData',
'useStateStore',
'vue-i18n',
],
mocks: {
validate: vi.fn(),
},
},
propsData: {
dataRequired: {
fk: 1,
},
dataKey: 'crudModelKey',
model: 'crudModel',
url: 'crudModelUrl',
},
}).vm;
});
beforeEach(() => {
vm.fetch([]);
});
afterEach(() => {
vi.clearAllMocks();
});
describe('insert()', () => {
it('should new element in list with index 0 if formData not has data', () => {
vm.insert();
expect(vm.formData.length).toEqual(1);
expect(vm.formData[0].fk).toEqual(1);
expect(vm.formData[0].$index).toEqual(0);
});
});
describe('getChanges()', () => {
it('should return correct updates and creates', async () => {
vm.fetch([
{ id: 1, name: 'New name one' },
{ id: 2, name: 'New name two' },
{ id: 3, name: 'Bruce Wayne' },
]);
vm.originalData = [
{ id: 1, name: 'Tony Starks' },
{ id: 2, name: 'Jessica Jones' },
{ id: 3, name: 'Bruce Wayne' },
];
vm.insert();
const result = vm.getChanges();
const expected = {
creates: [
{
$index: 3,
fk: 1,
},
],
updates: [
{
data: {
name: 'New name one',
},
where: {
id: 1,
},
},
{
data: {
name: 'New name two',
},
where: {
id: 2,
},
},
],
};
expect(result).toEqual(expected);
});
});
describe('getDifferences()', () => {
it('should return the differences between two objects', async () => {
const obj1 = {
a: 1,
b: 2,
c: 3,
};
const obj2 = {
a: null,
b: 4,
d: 5,
};
const result = vm.getDifferences(obj1, obj2);
expect(result).toEqual({
a: null,
b: 4,
d: 5,
});
});
});
});

View File

@ -5,7 +5,6 @@ import ClaimLines from 'pages/Claim/Card/ClaimLines.vue';
describe('ClaimLines', () => {
let vm;
beforeAll(() => {
vm = createWrapper(ClaimLines, {
global: {
@ -13,25 +12,26 @@ describe('ClaimLines', () => {
mocks: {
fetch: vi.fn(),
},
}
},
}).vm;
});
beforeEach(() => {
vm.claim = {
id: 1,
ticketFk: 1
}
ticketFk: 1,
};
vm.store.data = [
{
id: 1,
quantity: 10,
sale: {
id: 1, discount: 0
}
}
]
})
id: 1,
discount: 0,
},
},
];
});
afterEach(() => {
vi.clearAllMocks();
@ -42,13 +42,17 @@ describe('ClaimLines', () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
vi.spyOn(vm.quasar, 'notify');
const canceller = new AbortController()
const canceller = new AbortController();
await vm.updateDiscount({ saleFk: 1, discount: 5, canceller });
const expectedData = { salesIds: [1], newDiscount: 5 }
expect(axios.post).toHaveBeenCalledWith('Tickets/1/updateDiscount', expectedData, {
signal: canceller.signal
})
const expectedData = { salesIds: [1], newDiscount: 5 };
expect(axios.post).toHaveBeenCalledWith(
'Tickets/1/updateDiscount',
expectedData,
{
signal: canceller.signal,
}
);
});
});
@ -56,37 +60,14 @@ describe('ClaimLines', () => {
it('should make a POST request and then set the discount on the original row', async () => {
vi.spyOn(vm.quasar, 'notify');
vm.onUpdateDiscount({ discount: 5, rowIndex: 0 });
const firstRow = vm.store.data[0]
const firstRow = vm.store.data[0];
expect(firstRow.sale.discount).toEqual(5)
expect(firstRow.sale.discount).toEqual(5);
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Discount updated',
type: 'positive'
})
);
});
});
describe('remove()', () => {
it('should make a POST request and then call to the quasar notify() method', async () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
vi.spyOn(vm.quasar, 'notify');
await vm.remove({
rows: [
{ id: 1 }
]
});
const expectedData = { deletes: [1] }
expect(axios.post).toHaveBeenCalledWith('ClaimBeginnings/crud', expectedData)
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Row removed',
type: 'positive'
type: 'positive',
})
);
});

View File

@ -7,9 +7,11 @@ describe('WagonCreate', () => {
const entityId = 1;
beforeAll(() => {
vmEdit = createWrapper(WagonCreate, {propsData: {
vmEdit = createWrapper(WagonCreate, {
propsData: {
id: entityId,
}}).vm;
},
}).vm;
vmCreate = createWrapper(WagonCreate).vm;
});
@ -29,9 +31,7 @@ describe('WagonCreate', () => {
await vmCreate.onSubmit();
expect(axios.patch).toHaveBeenCalledWith(
`Wagons`, vmCreate.wagon
);
expect(axios.patch).toHaveBeenCalledWith(`Wagons`, vmCreate.wagon);
});
it('should update a wagon', async () => {
@ -46,9 +46,7 @@ describe('WagonCreate', () => {
await vmEdit.onSubmit();
expect(axios.patch).toHaveBeenCalledWith(
`Wagons`, vmEdit.wagon
);
expect(axios.patch).toHaveBeenCalledWith(`Wagons`, vmEdit.wagon);
});
});
@ -88,16 +86,12 @@ describe('WagonCreate', () => {
describe('fetch()', () => {
it('should fetch data', async () => {
vi.spyOn(axios, 'get').mockResolvedValue({ data: true });
vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
await vmEdit.fetch();
expect(axios.get).toHaveBeenCalledWith(
`WagonTypes`
);
expect(axios.get).toHaveBeenCalledWith(
`Wagons/${entityId}`
);
expect(axios.get).toHaveBeenCalledWith(`WagonTypes`);
expect(axios.get).toHaveBeenCalledWith(`Wagons/${entityId}`);
});
});
});

View File

@ -5,6 +5,7 @@ import { vi } from 'vitest';
import { i18n } from 'src/boot/i18n';
import { Notify, Dialog } from 'quasar';
import axios from 'axios';
import * as useValidator from 'src/composables/useValidator';
installQuasarPlugin({
plugins: {
@ -34,6 +35,10 @@ vi.mock('vue-router', () => ({
}),
}));
vi.spyOn(useValidator, 'useValidator').mockImplementation(() => {
return { validate: vi.fn(), fetch: vi.fn() };
});
class FormDataMock {
append() {
vi.fn();
@ -58,12 +63,17 @@ class FormDataMock {
}
}
global.FormData = FormDataMock;
global.URL = class URL {};
export function createWrapper(component, options) {
const defaultOptions = {
global: {
plugins: [i18n, pinia],
},
mocks: {
t: (tKey) => tKey,
$t: (tKey) => tKey,
},
};
const mountOptions = Object.assign({}, defaultOptions);