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

This commit is contained in:
Joan Sanchez 2023-03-10 13:23:56 +01:00
commit ab113ea908
54 changed files with 2875 additions and 1249 deletions

584
package-lock.json generated
View File

@ -8,10 +8,10 @@
"name": "salix-front",
"version": "0.0.1",
"dependencies": {
"@quasar/extras": "^1.15.8",
"@quasar/extras": "^1.15.11",
"axios": "^1.2.1",
"pinia": "^2.0.28",
"quasar": "^2.11.1",
"quasar": "^2.11.7",
"validator": "^13.7.0",
"vue": "^3.2.45",
"vue-i18n": "^9.2.2",
@ -21,8 +21,8 @@
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^0.8.1",
"@pinia/testing": "^0.0.14",
"@quasar/app-vite": "^1.1.3",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.1.2",
"@quasar/app-vite": "^1.2.1",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.2.1",
"@vue/test-utils": "^2.0.0",
"autoprefixer": "^10.4.13",
"cypress": "^12.2.0",
@ -770,33 +770,33 @@
}
},
"node_modules/@quasar/app-vite": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@quasar/app-vite/-/app-vite-1.1.3.tgz",
"integrity": "sha512-YX6lVkjRFNDbYcORiMtNlDz3jlSf7ldF4zGZk8zaW/Q1CfjaLqpSqCmBP4eta6QXz7To0IdabROYKa55D6IDgA==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@quasar/app-vite/-/app-vite-1.2.1.tgz",
"integrity": "sha512-iKvpucrnt5pQuuQxqkLLWuI7Wm9Xx9fcBK1miMHWBgdcbHOXhc/OrlxeLvRwxm7GMd91cp3nhslo5j3a+zMCOg==",
"dev": true,
"dependencies": {
"@quasar/fastclick": "1.1.5",
"@quasar/vite-plugin": "^1.2.1",
"@quasar/vite-plugin": "^1.3.0",
"@rollup/pluginutils": "^4.1.2",
"@types/chrome": "^0.0.191",
"@types/chrome": "^0.0.208",
"@types/compression": "^1.7.2",
"@types/cordova": "0.0.34",
"@types/express": "^4.17.13",
"@vitejs/plugin-vue": "^2.2.0",
"archiver": "^5.3.0",
"chokidar": "^3.5.3",
"ci-info": "^3.3.0",
"ci-info": "^3.7.1",
"compression": "^1.7.4",
"cross-spawn": "^7.0.3",
"dot-prop": "6.0.1",
"elementtree": "0.1.7",
"esbuild": "0.14.51",
"express": "^4.17.3",
"fast-glob": "3.2.11",
"fs-extra": "^10.0.1",
"fast-glob": "3.2.12",
"fs-extra": "^11.1.0",
"html-minifier": "^4.0.0",
"inquirer": "^8.2.1",
"isbinaryfile": "^4.0.8",
"isbinaryfile": "^5.0.0",
"kolorist": "^1.5.1",
"lodash": "^4.17.21",
"minimist": "^1.2.6",
@ -815,7 +815,7 @@
"quasar": "bin/quasar"
},
"engines": {
"node": "^18 || ^16 || ^14.19",
"node": "^24 || ^22 || ^20 || ^18 || ^16 || ^14.19",
"npm": ">= 6.14.12",
"yarn": ">= 1.17.3"
},
@ -855,26 +855,10 @@
}
}
},
"node_modules/@quasar/app-vite/node_modules/fast-glob": {
"version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.4"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/@quasar/extras": {
"version": "1.15.8",
"resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.15.8.tgz",
"integrity": "sha512-UR6Snu7DSYdDOGqcNjUr0FJtKNfPn2Jc2hKTC+y/Y7Gf+vWu0RYUl49cguD33nn+wpbgs28+cvmjx7u3NNogoQ==",
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.15.11.tgz",
"integrity": "sha512-EEXL10EJQmL9jNs5fp0Kd/nyonG8hTODolj+qpYNPG3qEEoy3txr05Pdmp9qJYaXIzP9rOS9FGhFe9Mnq6MBBg==",
"funding": {
"type": "github",
"url": "https://donate.quasar.dev"
@ -891,12 +875,12 @@
}
},
"node_modules/@quasar/quasar-app-extension-testing-unit-vitest": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@quasar/quasar-app-extension-testing-unit-vitest/-/quasar-app-extension-testing-unit-vitest-0.1.2.tgz",
"integrity": "sha512-yqpXu0J8Gttr00UCzEAMptZX8aggqsiG14cgeKLt0Hm3pU9PtwXqjX/EYifgQyldw0b7LbAzpdHhsXU2BU9vpA==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@quasar/quasar-app-extension-testing-unit-vitest/-/quasar-app-extension-testing-unit-vitest-0.2.1.tgz",
"integrity": "sha512-jR7KgZfujDdfhZe0dcgiA+W2bfm8/NcEbpYuEyr1161Ckn+dL6Y9KZUDEZMUuRYzS+pn1SXsRPMcv8tgXToB4Q==",
"dev": true,
"dependencies": {
"happy-dom": "^5.0.0",
"happy-dom": "^8.9.0",
"lodash-es": "^4.17.21",
"vite-jsconfig-paths": "^2.0.1",
"vite-tsconfig-paths": "^3.5.0"
@ -907,10 +891,10 @@
"yarn": ">= 1.17.3"
},
"peerDependencies": {
"@vitest/ui": "^0.15.0",
"@vitest/ui": "^0.29.1",
"@vue/test-utils": "^2.0.0",
"quasar": "^2.0.0",
"vitest": "^0.15.0",
"quasar": "^2.10.2",
"vitest": "^0.29.1",
"vue": "^3.2.0"
},
"peerDependenciesMeta": {
@ -977,9 +961,9 @@
}
},
"node_modules/@types/chrome": {
"version": "0.0.191",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.191.tgz",
"integrity": "sha512-hXYHJJ1Y265xKCw0o2Kz4CnR8aUhOMdyxK1AinET4EDr3fhpEMvOFDwdqz9LUX4syfTVYWb8w7vfC12s112ehg==",
"version": "0.0.208",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.208.tgz",
"integrity": "sha512-VDU/JnXkF5qaI7WBz14Azpa2VseZTgML0ia/g/B1sr9OfdOnHiH/zZ7P7qCDqxSlkqJh76/bPc8jLFcx8rHJmw==",
"dev": true,
"dependencies": {
"@types/filesystem": "*",
@ -995,15 +979,6 @@
"@types/express": "*"
}
},
"node_modules/@types/concat-stream": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz",
"integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@ -1057,15 +1032,6 @@
"integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==",
"dev": true
},
"node_modules/@types/form-data": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
"integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/har-format": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz",
@ -1509,12 +1475,6 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"dev": true
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
},
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
@ -1997,10 +1957,16 @@
}
},
"node_modules/ci-info": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz",
"integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"engines": {
"node": ">=8"
}
@ -2257,45 +2223,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dev": true,
"engines": [
"node >= 0.8"
],
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"node_modules/concat-stream/node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/concat-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -3823,9 +3750,9 @@
"dev": true
},
"node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz",
"integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
@ -3833,7 +3760,7 @@
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
"node": ">=14.14"
}
},
"node_modules/fs.realpath": {
@ -3894,15 +3821,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-port": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
"integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@ -4023,20 +3941,32 @@
"dev": true
},
"node_modules/happy-dom": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-5.4.0.tgz",
"integrity": "sha512-JxXpBvEUdyCqfRHzHJKtiJ+6+WzTIL6kFCteAOEy13QEnHMD/D5uUIVVw3a4TmQroJriz0gnll4Uv1qZeSz/rA==",
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-8.9.0.tgz",
"integrity": "sha512-JZwJuGdR7ko8L61136YzmrLv7LgTh5b8XaEM3P709mLjyQuXJ3zHTDXvUtBBahRjGlcYW0zGjIiEWizoTUGKfA==",
"dev": true,
"dependencies": {
"css.escape": "^1.5.1",
"he": "^1.2.0",
"iconv-lite": "^0.6.3",
"node-fetch": "^2.x.x",
"sync-request": "^6.1.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0"
}
},
"node_modules/happy-dom/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@ -4100,21 +4030,6 @@
"node": ">=6"
}
},
"node_modules/http-basic": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
"integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==",
"dev": true,
"dependencies": {
"caseless": "^0.12.0",
"concat-stream": "^1.6.2",
"http-response-object": "^3.0.1",
"parse-cache-control": "^1.0.1"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -4131,21 +4046,6 @@
"node": ">= 0.8"
}
},
"node_modules/http-response-object": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
"integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
"dev": true,
"dependencies": {
"@types/node": "^10.0.3"
}
},
"node_modules/http-response-object/node_modules/@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
"dev": true
},
"node_modules/http-signature": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
@ -4498,12 +4398,12 @@
"dev": true
},
"node_modules/isbinaryfile": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
"integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.0.tgz",
"integrity": "sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==",
"dev": true,
"engines": {
"node": ">= 8.0.0"
"node": ">= 14.0.0"
},
"funding": {
"url": "https://github.com/sponsors/gjtorikian/"
@ -5469,12 +5369,6 @@
"node": ">=6"
}
},
"node_modules/parse-cache-control": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
"integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==",
"dev": true
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -5736,15 +5630,6 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
"node_modules/promise": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz",
"integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==",
"dev": true,
"dependencies": {
"asap": "~2.0.6"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -5804,9 +5689,9 @@
}
},
"node_modules/quasar": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.11.1.tgz",
"integrity": "sha512-3whiJaOXbectd2R6Vfjr/vHKeY92zdDpx41zfZ5vrG+Yx16Q76euUQsdKlYr1PD7q33yvHvjegxuILgKJg5GqA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.11.7.tgz",
"integrity": "sha512-OzDwYfoSwZbwqfNWz9Bfn1mhGE8YMxpTLhPvVkQU87ePF6qFj4aWttcTUifXITKldOAZziN1Mmv8VLQyITHwiw==",
"engines": {
"node": ">= 10.18.1",
"npm": ">= 6.13.4",
@ -6608,29 +6493,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sync-request": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz",
"integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==",
"dev": true,
"dependencies": {
"http-response-object": "^3.0.1",
"sync-rpc": "^1.2.1",
"then-request": "^6.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/sync-rpc": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz",
"integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==",
"dev": true,
"dependencies": {
"get-port": "^3.1.0"
}
},
"node_modules/table": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
@ -6691,48 +6553,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/then-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz",
"integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==",
"dev": true,
"dependencies": {
"@types/concat-stream": "^1.6.0",
"@types/form-data": "0.0.33",
"@types/node": "^8.0.0",
"@types/qs": "^6.2.31",
"caseless": "~0.12.0",
"concat-stream": "^1.6.0",
"form-data": "^2.2.0",
"http-basic": "^8.1.1",
"http-response-object": "^3.0.1",
"promise": "^8.0.0",
"qs": "^6.4.0"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/then-request/node_modules/@types/node": {
"version": "8.10.66",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
"integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==",
"dev": true
},
"node_modules/then-request/node_modules/form-data": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -6930,12 +6750,6 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"dev": true
},
"node_modules/ufo": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz",
@ -8076,33 +7890,33 @@
"dev": true
},
"@quasar/app-vite": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@quasar/app-vite/-/app-vite-1.1.3.tgz",
"integrity": "sha512-YX6lVkjRFNDbYcORiMtNlDz3jlSf7ldF4zGZk8zaW/Q1CfjaLqpSqCmBP4eta6QXz7To0IdabROYKa55D6IDgA==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@quasar/app-vite/-/app-vite-1.2.1.tgz",
"integrity": "sha512-iKvpucrnt5pQuuQxqkLLWuI7Wm9Xx9fcBK1miMHWBgdcbHOXhc/OrlxeLvRwxm7GMd91cp3nhslo5j3a+zMCOg==",
"dev": true,
"requires": {
"@quasar/fastclick": "1.1.5",
"@quasar/vite-plugin": "^1.2.1",
"@quasar/vite-plugin": "^1.3.0",
"@rollup/pluginutils": "^4.1.2",
"@types/chrome": "^0.0.191",
"@types/chrome": "^0.0.208",
"@types/compression": "^1.7.2",
"@types/cordova": "0.0.34",
"@types/express": "^4.17.13",
"@vitejs/plugin-vue": "^4.0.0",
"archiver": "^5.3.0",
"chokidar": "^3.5.3",
"ci-info": "^3.3.0",
"ci-info": "^3.7.1",
"compression": "^1.7.4",
"cross-spawn": "^7.0.3",
"dot-prop": "6.0.1",
"elementtree": "0.1.7",
"esbuild": "0.14.51",
"express": "^4.17.3",
"fast-glob": "3.2.11",
"fs-extra": "^10.0.1",
"fast-glob": "3.2.12",
"fs-extra": "^11.1.0",
"html-minifier": "^4.0.0",
"inquirer": "^8.2.1",
"isbinaryfile": "^4.0.8",
"isbinaryfile": "^5.0.0",
"kolorist": "^1.5.1",
"lodash": "^4.17.21",
"minimist": "^1.2.6",
@ -8116,27 +7930,12 @@
"table": "^6.8.0",
"vite": "^4.0.3",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"fast-glob": {
"version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
"dev": true,
"requires": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.4"
}
}
}
},
"@quasar/extras": {
"version": "1.15.8",
"resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.15.8.tgz",
"integrity": "sha512-UR6Snu7DSYdDOGqcNjUr0FJtKNfPn2Jc2hKTC+y/Y7Gf+vWu0RYUl49cguD33nn+wpbgs28+cvmjx7u3NNogoQ=="
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.15.11.tgz",
"integrity": "sha512-EEXL10EJQmL9jNs5fp0Kd/nyonG8hTODolj+qpYNPG3qEEoy3txr05Pdmp9qJYaXIzP9rOS9FGhFe9Mnq6MBBg=="
},
"@quasar/fastclick": {
"version": "1.1.5",
@ -8145,12 +7944,12 @@
"dev": true
},
"@quasar/quasar-app-extension-testing-unit-vitest": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@quasar/quasar-app-extension-testing-unit-vitest/-/quasar-app-extension-testing-unit-vitest-0.1.2.tgz",
"integrity": "sha512-yqpXu0J8Gttr00UCzEAMptZX8aggqsiG14cgeKLt0Hm3pU9PtwXqjX/EYifgQyldw0b7LbAzpdHhsXU2BU9vpA==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@quasar/quasar-app-extension-testing-unit-vitest/-/quasar-app-extension-testing-unit-vitest-0.2.1.tgz",
"integrity": "sha512-jR7KgZfujDdfhZe0dcgiA+W2bfm8/NcEbpYuEyr1161Ckn+dL6Y9KZUDEZMUuRYzS+pn1SXsRPMcv8tgXToB4Q==",
"dev": true,
"requires": {
"happy-dom": "^5.0.0",
"happy-dom": "^8.9.0",
"lodash-es": "^4.17.21",
"vite-jsconfig-paths": "^2.0.1",
"vite-tsconfig-paths": "^3.5.0"
@ -8199,9 +7998,9 @@
}
},
"@types/chrome": {
"version": "0.0.191",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.191.tgz",
"integrity": "sha512-hXYHJJ1Y265xKCw0o2Kz4CnR8aUhOMdyxK1AinET4EDr3fhpEMvOFDwdqz9LUX4syfTVYWb8w7vfC12s112ehg==",
"version": "0.0.208",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.208.tgz",
"integrity": "sha512-VDU/JnXkF5qaI7WBz14Azpa2VseZTgML0ia/g/B1sr9OfdOnHiH/zZ7P7qCDqxSlkqJh76/bPc8jLFcx8rHJmw==",
"dev": true,
"requires": {
"@types/filesystem": "*",
@ -8217,15 +8016,6 @@
"@types/express": "*"
}
},
"@types/concat-stream": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz",
"integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@ -8279,15 +8069,6 @@
"integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==",
"dev": true
},
"@types/form-data": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
"integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/har-format": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz",
@ -8656,12 +8437,6 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"dev": true
},
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
},
"asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
@ -9004,9 +8779,9 @@
}
},
"ci-info": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz",
"integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"dev": true
},
"clean-css": {
@ -9206,44 +8981,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -10302,9 +10039,9 @@
"dev": true
},
"fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz",
"integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
@ -10354,12 +10091,6 @@
"has-symbols": "^1.0.3"
}
},
"get-port": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
"integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==",
"dev": true
},
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@ -10453,18 +10184,29 @@
"dev": true
},
"happy-dom": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-5.4.0.tgz",
"integrity": "sha512-JxXpBvEUdyCqfRHzHJKtiJ+6+WzTIL6kFCteAOEy13QEnHMD/D5uUIVVw3a4TmQroJriz0gnll4Uv1qZeSz/rA==",
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-8.9.0.tgz",
"integrity": "sha512-JZwJuGdR7ko8L61136YzmrLv7LgTh5b8XaEM3P709mLjyQuXJ3zHTDXvUtBBahRjGlcYW0zGjIiEWizoTUGKfA==",
"dev": true,
"requires": {
"css.escape": "^1.5.1",
"he": "^1.2.0",
"iconv-lite": "^0.6.3",
"node-fetch": "^2.x.x",
"sync-request": "^6.1.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0"
},
"dependencies": {
"iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
}
},
"has": {
@ -10509,18 +10251,6 @@
"uglify-js": "^3.5.1"
}
},
"http-basic": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
"integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==",
"dev": true,
"requires": {
"caseless": "^0.12.0",
"concat-stream": "^1.6.2",
"http-response-object": "^3.0.1",
"parse-cache-control": "^1.0.1"
}
},
"http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -10534,23 +10264,6 @@
"toidentifier": "1.0.1"
}
},
"http-response-object": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
"integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
"dev": true,
"requires": {
"@types/node": "^10.0.3"
},
"dependencies": {
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
"dev": true
}
}
},
"http-signature": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
@ -10793,9 +10506,9 @@
"dev": true
},
"isbinaryfile": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
"integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.0.tgz",
"integrity": "sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==",
"dev": true
},
"isexe": {
@ -11540,12 +11253,6 @@
"callsites": "^3.0.0"
}
},
"parse-cache-control": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
"integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==",
"dev": true
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -11715,15 +11422,6 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
"promise": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz",
"integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==",
"dev": true,
"requires": {
"asap": "~2.0.6"
}
},
"proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -11771,9 +11469,9 @@
}
},
"quasar": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.11.1.tgz",
"integrity": "sha512-3whiJaOXbectd2R6Vfjr/vHKeY92zdDpx41zfZ5vrG+Yx16Q76euUQsdKlYr1PD7q33yvHvjegxuILgKJg5GqA=="
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.11.7.tgz",
"integrity": "sha512-OzDwYfoSwZbwqfNWz9Bfn1mhGE8YMxpTLhPvVkQU87ePF6qFj4aWttcTUifXITKldOAZziN1Mmv8VLQyITHwiw=="
},
"queue-microtask": {
"version": "1.2.3",
@ -12355,26 +12053,6 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"sync-request": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz",
"integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==",
"dev": true,
"requires": {
"http-response-object": "^3.0.1",
"sync-rpc": "^1.2.1",
"then-request": "^6.0.0"
}
},
"sync-rpc": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz",
"integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==",
"dev": true,
"requires": {
"get-port": "^3.1.0"
}
},
"table": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
@ -12427,44 +12105,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"then-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz",
"integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==",
"dev": true,
"requires": {
"@types/concat-stream": "^1.6.0",
"@types/form-data": "0.0.33",
"@types/node": "^8.0.0",
"@types/qs": "^6.2.31",
"caseless": "~0.12.0",
"concat-stream": "^1.6.0",
"form-data": "^2.2.0",
"http-basic": "^8.1.1",
"http-response-object": "^3.0.1",
"promise": "^8.0.0",
"qs": "^6.4.0"
},
"dependencies": {
"@types/node": {
"version": "8.10.66",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
"integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==",
"dev": true
},
"form-data": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
}
}
},
"thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -12623,12 +12263,6 @@
"mime-types": "~2.1.24"
}
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"dev": true
},
"ufo": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz",

View File

@ -15,10 +15,10 @@
"test:unit:ci": "vitest run"
},
"dependencies": {
"@quasar/extras": "^1.15.8",
"@quasar/extras": "^1.15.11",
"axios": "^1.2.1",
"pinia": "^2.0.28",
"quasar": "^2.11.1",
"quasar": "^2.11.7",
"validator": "^13.7.0",
"vue": "^3.2.45",
"vue-i18n": "^9.2.2",
@ -28,8 +28,8 @@
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^0.8.1",
"@pinia/testing": "^0.0.14",
"@quasar/app-vite": "^1.1.3",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.1.2",
"@quasar/app-vite": "^1.2.1",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.2.1",
"@vue/test-utils": "^2.0.0",
"autoprefixer": "^10.4.13",
"cypress": "^12.2.0",

View File

@ -9,7 +9,7 @@
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
const { configure } = require('quasar/wrappers');
const VueI18nPlugin = require('@intlify/unplugin-vue-i18n/vite')
const VueI18nPlugin = require('@intlify/unplugin-vue-i18n/vite');
const path = require('path');
module.exports = configure(function (/* ctx */) {
@ -140,6 +140,8 @@ module.exports = configure(function (/* ctx */) {
// Quasar plugins
plugins: ['Notify', 'Dialog'],
//all: 'auto',
//autoImportComponentCase: 'pascal',
},
// animations: 'all', // --- includes all animations

View File

@ -44,6 +44,11 @@ function responseError(error) {
let message = error.message;
let logOut = false;
const response = error.response;
if (response && response.data.error) {
message = response.data.error.message;
}
switch (error.response?.status) {
case 401:
message = 'login.loginError';

View File

@ -21,7 +21,14 @@ onMounted(() => stateStore.setMounted());
<template>
<q-header class="bg-dark" color="white" elevated>
<q-toolbar class="q-py-sm q-px-md">
<q-btn flat @click="stateStore.toggleLeftDrawer()" round dense icon="menu">
<q-btn
@click="stateStore.toggleLeftDrawer()"
icon="menu"
class="q-mr-sm"
round
dense
flat
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
@ -48,24 +55,13 @@ onMounted(() => stateStore.setMounted());
<div id="searchbar"></div>
<q-space></q-space>
<div class="q-pl-sm q-gutter-sm row items-center no-wrap">
<div id="header-actions"></div>
<div id="actions-prepend"></div>
<q-btn id="pinnedModules" icon="apps" flat dense rounded>
<q-tooltip bottom>
{{ t('globals.pinnedModules') }}
</q-tooltip>
<PinnedModules />
</q-btn>
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
<q-btn rounded dense flat no-wrap id="user">
<q-avatar size="lg">
<q-img
@ -79,6 +75,7 @@ onMounted(() => stateStore.setMounted());
</q-tooltip>
<UserPanel />
</q-btn>
<div id="actions-append"></div>
</div>
</q-toolbar>
</q-header>

View File

@ -86,6 +86,7 @@ async function paginate() {
if (!props.url) return;
isLoading.value = true;
await arrayData.loadMore();
if (!arrayData.hasMoreData.value) {
@ -121,7 +122,7 @@ async function onLoad(...params) {
</script>
<template>
<div class="column items-center">
<div>
<div
v-if="store.data && store.data.length === 0 && !isLoading"
class="info-row q-pa-md text-center"
@ -150,27 +151,18 @@ async function onLoad(...params) {
</q-card>
</div>
</div>
<q-infinite-scroll
v-if="store.data"
@load="onLoad"
:offset="offset"
class="column items-center"
>
<div v-if="store" class="card-list q-gutter-y-md">
<q-infinite-scroll v-if="store.data" @load="onLoad" :offset="offset">
<slot name="body" :rows="store.data"></slot>
<div v-if="isLoading" class="info-row q-pa-md text-center">
<q-spinner color="orange" size="md" />
</div>
</div>
</q-infinite-scroll>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
// .q-infinite-scroll {
// width: 100%;
// }
.info-row {
width: 100%;

View File

@ -3,12 +3,13 @@ import { ref } from 'vue';
import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n';
const $props = defineProps({
address: {
type: String,
default: '',
const props = defineProps({
data: {
type: Object,
requied: true,
default: null,
},
send: {
promise: {
type: Function,
required: true,
},
@ -19,24 +20,30 @@ defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const { dialogRef, onDialogOK } = useDialogPluginComponent();
const { t } = useI18n();
const address = ref($props.address);
const address = ref(props.data.address);
const isLoading = ref(false);
async function confirm() {
isLoading.value = true;
await $props.send(address.value);
isLoading.value = false;
const response = { address };
onDialogOK();
if (props.promise) {
isLoading.value = true;
try {
Object.assign(response, props.data);
await props.promise(response);
} finally {
isLoading.value = false;
}
}
onDialogOK(response);
}
</script>
<template>
<q-dialog ref="dialogRef" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{
t('Send email notification: Send email notification')
}}</span>
<span class="text-h6 text-grey">{{ t('Send email notification') }}</span>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
@ -53,6 +60,7 @@ async function confirm() {
color="primary"
:loading="isLoading"
@click="confirm"
unelevated
/>
</q-card-actions>
</q-card>
@ -67,6 +75,6 @@ async function confirm() {
<i18n>
es:
Send email notification: Enviar notificación por correo,
Send email notification: Enviar notificación por correo
The notification will be sent to the following address: La notificación se enviará a la siguiente dirección
</i18n>

View File

@ -0,0 +1,243 @@
<script setup>
import { ref, computed } from 'vue';
import { useDialogPluginComponent } from 'quasar';
import { useI18n } from 'vue-i18n';
const { dialogRef, onDialogOK } = useDialogPluginComponent();
const { t, availableLocales } = useI18n();
defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const props = defineProps({
subject: {
type: String,
required: false,
default: 'Verdnatura',
},
phone: {
type: String,
required: true,
},
template: {
type: String,
required: true,
},
locale: {
type: String,
required: false,
default: 'es',
},
data: {
type: Object,
required: false,
default: null,
},
promise: {
type: Function,
required: true,
},
});
const maxLength = 160;
const locale = ref(props.locale);
const subject = ref(props.subject);
const phone = ref(props.phone);
const message = ref('');
updateMessage();
function updateMessage() {
const params = props.data;
const key = `templates['${props.template}']`;
message.value = t(key, params, { locale: locale.value });
}
const totalLength = computed(() => message.value.length);
const color = computed(() => {
if (totalLength.value == maxLength) return 'negative';
if ((totalLength.value / maxLength) * 100 > 90) return 'warning';
return 'positive';
});
const languages = availableLocales.map((locale) => ({ label: t(locale), value: locale }));
const isLoading = ref(false);
async function send() {
const response = {
destination: phone.value,
message: message.value,
};
if (props.promise) {
isLoading.value = true;
try {
Object.assign(response, props.data);
await props.promise(response);
} finally {
isLoading.value = false;
}
}
onDialogOK(response);
}
</script>
<template>
<q-dialog ref="dialogRef" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row items-center q-pb-none">
<span class="text-h6 text-grey">
{{ t('Send SMS') }}
</span>
<q-space />
<q-btn icon="close" :disable="isLoading" flat round dense v-close-popup />
</q-card-section>
<q-card-section>
<q-banner class="bg-amber text-white" rounded dense>
<template #avatar>
<q-icon name="warning" />
</template>
<span
v-html="t('CustomerDefaultLanguage', { locale: t(props.locale) })"
></span>
</q-banner>
</q-card-section>
<q-card-section class="q-pb-xs">
<q-select
:label="t('Language')"
:options="languages"
v-model="locale"
@update:model-value="updateMessage()"
emit-value
map-options
:input-debounce="0"
rounded
outlined
dense
/>
</q-card-section>
<q-card-section class="q-pb-xs">
<q-input
:label="t('Phone')"
v-model="phone"
rounded
outlined
autofocus
dense
/>
</q-card-section>
<q-card-section class="q-pb-xs">
<q-input
:label="t('Subject')"
v-model="subject"
rounded
outlined
autofocus
dense
/>
</q-card-section>
<q-card-section class="q-mb-md" q-input>
<q-input
:label="t('Message')"
v-model="message"
type="textarea"
:maxlength="maxLength"
:counter="true"
:autogrow="true"
:bottom-slots="true"
:rules="[(value) => value.length < maxLength || 'Error!']"
stack-label
outlined
autofocus
>
<template #append>
<q-icon
v-if="message !== ''"
name="close"
@click="message = ''"
class="cursor-pointer"
/>
</template>
<template #counter>
<q-chip :color="color" dense>
{{ totalLength }}/{{ maxLength }}
</q-chip>
</template>
</q-input>
</q-card-section>
<q-card-actions align="right">
<q-btn
:label="t('globals.cancel')"
color="primary"
:disable="isLoading"
flat
v-close-popup
/>
<q-btn
:label="t('globals.confirm')"
@click="send()"
:loading="isLoading"
color="primary"
unelevated
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<style lang="scss" scoped>
.q-chip {
transition: background 0.36s;
}
.q-card {
width: 500px;
}
</style>
<i18n>
en:
CustomerDefaultLanguage: This customer uses <strong>{locale}</strong> as their default language
en: English
es: Spanish
fr: French
templates:
pendingPayment: 'Your order is pending of payment.
Please, enter the website and make the payment with a credit card. Thank you.'
minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order
{ orderId } of { shipped } to receive it without additional shipping costs.'
orderChanges: 'Order {orderId} of { shipped }\r
{ changes }'
es:
Send SMS: Enviar SMS
CustomerDefaultLanguage: Este cliente utiliza <strong>{locale}</strong> como idioma por defecto
Language: Idioma
Phone: Móvil
Subject: Asunto
Message: Mensaje
templates:
pendingPayment: 'Su pedido está pendiente de pago.
Por favor, entre en la página web y efectue el pago con tarjeta. Muchas gracias.'
minAmount: 'Es necesario un importe mínimo de 50 (Sin IVA) en su pedido
{ orderId } del día { shipped } para recibirlo sin portes adicionales.'
orderChanges: 'Pedido {orderId} día { shipped }
{ changes }'
en: Inglés
es: Español
fr: Francés
fr:
Send SMS: Envoyer SMS
CustomerDefaultLanguage: Ce client utilise l'{locale} comme langue par défaut
Language: Langage
Phone: Mobile
Subject: Affaire
Message: Message
templates:
pendingPayment: 'Votre commande est en attente de paiement.
Veuillez vous connecter sur le site web et effectuer le paiement par carte. Merci beaucoup.'
minAmount: 'Un montant minimum de 50 (TVA non incluse) est requis pour votre commande
{ orderId } du { shipped } afin de la recevoir sans frais de port supplémentaires.'
orderChanges: 'Commande { orderId } du { shipped }\r { changes }'
en: Anglais
es: Espagnol
fr: Français
</i18n>

View File

@ -16,7 +16,7 @@ const props = defineProps({
module: {
type: String,
required: true,
}
},
});
const slots = useSlots();
@ -24,14 +24,18 @@ const { t } = useI18n();
onMounted(() => fetch());
const emit = defineEmits(['onFetch']);
const entity = ref();
async function fetch() {
const params = {};
if (props.filter) params.filter = props.filter;
if (props.filter) params.filter = JSON.stringify(props.filter);
const { data } = await axios.get(props.url, { params });
entity.value = data;
emit('onFetch', data);
}
watch(props, async () => {
@ -46,18 +50,18 @@ watch(props, async () => {
<div class="header bg-primary q-pa-sm">
<router-link :to="{ name: `${module}List` }">
<q-btn round flat dense size="md" icon="view_list" color="white">
<q-tooltip>{{
t('components.cardDescriptor.mainList')
}}</q-tooltip>
<q-tooltip>
{{ t('components.cardDescriptor.mainList') }}
</q-tooltip>
</q-btn>
</router-link>
<router-link
:to="{ name: `${module}Summary`, params: { id: entity.id } }"
>
<q-btn round flat dense size="md" icon="launch" color="white">
<q-tooltip>{{
t('components.cardDescriptor.summary')
}}</q-tooltip>
<q-tooltip>
{{ t('components.cardDescriptor.summary') }}
</q-tooltip>
</q-btn>
</router-link>
@ -80,8 +84,9 @@ watch(props, async () => {
</q-menu>
</q-btn>
</div>
<slot name="before" />
<div class="body q-py-sm">
<q-list>
<q-list dense>
<q-item-label header class="ellipsis text-h5" :lines="1">
<slot name="description" :entity="entity">
<span>
@ -98,6 +103,7 @@ watch(props, async () => {
</q-list>
<slot name="body" :entity="entity" />
</div>
<slot name="after" />
</template>
<!-- Skeleton -->
<skeleton-descriptor v-if="!entity" />

View File

@ -1,20 +0,0 @@
<script setup>
import { nextTick, ref } from 'vue';
const $props = defineProps({
to: {
type: String,
required: true,
},
});
const isHeaderMounted = ref(false);
nextTick(() => {
isHeaderMounted.value = document.querySelector($props.to) !== null;
});
</script>
<template>
<teleport v-if="isHeaderMounted" :to="$props.to">
<slot />
</teleport>
</template>

View File

@ -10,7 +10,7 @@ const props = defineProps({
type: String,
default: null,
},
question: {
title: {
type: String,
default: null,
},
@ -18,15 +18,37 @@ const props = defineProps({
type: String,
default: null,
},
data: {
type: Object,
required: false,
default: null,
},
promise: {
type: Function,
required: false,
default: null,
},
});
defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const { dialogRef, onDialogOK } = useDialogPluginComponent();
const question = props.question || t('question');
const message = props.message || t('message');
const title = props.title || t('Confirm');
const message = props.message || t('Are you sure you want to continue?');
const isLoading = ref(false);
async function confirm() {
isLoading.value = true;
if (props.promise) {
try {
await props.promise(props.data);
} finally {
isLoading.value = false;
}
}
onDialogOK(props.data);
}
</script>
<template>
<q-dialog ref="dialogRef" persistent>
@ -39,20 +61,28 @@ const isLoading = ref(false);
size="xl"
v-if="icon"
/>
<span class="text-h6 text-grey">{{ message }}</span>
<span class="text-h6 text-grey">{{ title }}</span>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<q-btn icon="close" :disable="isLoading" flat round dense v-close-popup />
</q-card-section>
<q-card-section class="row items-center">
{{ question }}
{{ message }}
</q-card-section>
<q-card-actions align="right">
<q-btn :label="t('globals.cancel')" color="primary" flat v-close-popup />
<q-btn
:label="t('globals.cancel')"
color="primary"
:disable="isLoading"
flat
v-close-popup
/>
<q-btn
:label="t('globals.confirm')"
color="primary"
:loading="isLoading"
@click="onDialogOK()"
@click="confirm()"
unelevated
autofocus
/>
</q-card-actions>
</q-card>
@ -66,13 +96,7 @@ const isLoading = ref(false);
</style>
<i18n>
"en": {
"question": "Are you sure you want to continue?",
"message": "Confirm"
}
"es": {
"question": "¿Seguro que quieres continuar?",
"message": "Confirmar"
}
es:
Confirm: Confirmar
Are you sure you want to continue?: ¿Seguro que quieres continuar?
</i18n>

View File

@ -87,15 +87,29 @@ async function search() {
</q-form>
</template>
<style lang="scss" scoped>
@media screen and (max-width: $breakpoint-xs-max) {
.q-field {
width: 250px;
}
}
@media screen and (min-width: $breakpoint-xs-max) {
.q-field {
width: 400px;
}
}
.q-field {
transition: width 0.36s;
}
</style>
<style lang="scss">
.cursor-info {
cursor: help;
}
#searchbar .q-field {
min-width: 350px;
}
.body--light #searchbar {
.q-field--standout.q-field--highlighted .q-field__control {
background-color: $grey-7;

View File

@ -50,7 +50,7 @@ export function useArrayData(key, userOptions) {
Object.assign(store.filter, filter);
const params = {
filter: JSON.stringify(filter),
filter: JSON.stringify(store.filter),
};
Object.assign(params, store.userParams);

View File

@ -242,6 +242,7 @@ export default {
basicData: 'Basic Data',
rma: 'RMA',
photos: 'Photos',
log: 'Audit logs',
},
list: {
customer: 'Customer',
@ -344,6 +345,47 @@ export default {
totalWithVat: 'Amount',
},
},
worker: {
pageTitles: {
workers: 'Workers',
list: 'List',
basicData: 'Basic data',
summary: 'Summary',
},
list: {
name: 'Name',
email: 'Email',
phone: 'Phone',
mobile: 'Mobile',
active: 'Active',
department: 'Department',
schedule: 'Schedule',
},
card: {
workerId: 'Worker ID',
name: 'Name',
email: 'Email',
phone: 'Phone',
mobile: 'Mobile',
active: 'Active',
warehouse: 'Warehouse',
agency: 'Agency',
salesPerson: 'Sales person',
},
summary: {
basicData: 'Basic data',
boss: 'Boss',
phoneExtension: 'Phone extension',
entPhone: 'Enterprise phone',
personalPhone: 'Personal phone',
noBoss: 'No boss',
userData: 'User data',
userId: 'User ID',
role: 'Role',
sipExtension: 'Extension',
},
imageNotFound: 'Image not found',
},
components: {
topbar: {},
userPanel: {

View File

@ -241,6 +241,7 @@ export default {
basicData: 'Datos básicos',
rma: 'RMA',
photos: 'Fotos',
log: 'Registros de auditoría',
},
list: {
customer: 'Cliente',
@ -344,6 +345,47 @@ export default {
totalWithVat: 'Importe',
},
},
worker: {
pageTitles: {
workers: 'Trabajadores',
list: 'Listado',
basicData: 'Datos básicos',
summary: 'Resumen',
},
list: {
name: 'Nombre',
email: 'Email',
phone: 'Teléfono',
mobile: 'Móvil',
active: 'Activo',
department: 'Departamento',
schedule: 'Horario',
},
card: {
workerId: 'ID Trabajador',
name: 'Nombre',
email: 'Email',
phone: 'Teléfono',
mobile: 'Móvil',
active: 'Activo',
warehouse: 'Almacén',
agency: 'Empresa',
salesPerson: 'Comercial',
},
summary: {
basicData: 'Datos básicos',
boss: 'Jefe',
phoneExtension: 'Extensión de teléfono',
entPhone: 'Teléfono de empresa',
personalPhone: 'Teléfono personal',
noBoss: 'Sin jefe',
userData: 'Datos de usuario',
userId: 'ID del usuario',
role: 'Rol',
sipExtension: 'Extensión',
},
imageNotFound: 'No se ha encontrado la imagen',
},
components: {
topbar: {},
userPanel: {

View File

@ -2,26 +2,13 @@
import { useQuasar } from 'quasar';
import Navbar from 'src/components/NavBar.vue';
import { useStateStore } from 'stores/useStateStore';
const quasar = useQuasar();
const stateStore = useStateStore();
</script>
<template>
<q-layout view="hHh LpR fFf">
<Navbar />
<router-view></router-view>
<q-drawer
v-model="stateStore.rightDrawer"
side="right"
:width="256"
:persistent="false"
>
<q-scroll-area class="fit text-grey-8">
<div id="rightPanel"></div>
</q-scroll-area>
</q-drawer>
<q-footer v-if="quasar.platform.is.mobile"></q-footer>
</q-layout>
</template>

View File

@ -81,22 +81,47 @@ const statesFilter = {
/>
<fetch-data url="ClaimStates" @on-fetch="setClaimStates" auto-load />
<div class="container">
<div class="column items-center">
<q-card>
<form-model :url="`Claims/${route.params.id}`" :filter="claimFilter" model="claim">
<form-model
:url="`Claims/${route.params.id}`"
:filter="claimFilter"
model="claim"
>
<template #form="{ data, validate, filter }">
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input v-model="data.client.name" :label="t('claim.basicData.customer')" disable />
<q-input
v-model="data.client.name"
:label="t('claim.basicData.customer')"
disable
/>
</div>
<div class="col">
<q-input v-model="data.created" mask="####-##-##" fill-mask="_" autofocus>
<q-input
v-model="data.created"
mask="####-##-##"
fill-mask="_"
autofocus
>
<template #append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="data.created" mask="YYYY-MM-DD">
<q-popup-proxy
cover
transition-show="scale"
transition-hide="scale"
>
<q-date
v-model="data.created"
mask="YYYY-MM-DD"
>
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
<q-btn
v-close-popup
label="Close"
color="primary"
flat
/>
</div>
</q-date>
</q-popup-proxy>
@ -116,7 +141,9 @@ const statesFilter = {
:label="t('claim.basicData.assignedTo')"
map-options
use-input
@filter="(value, update) => filter(value, update, workerFilter)"
@filter="
(value, update) => filter(value, update, workerFilter)
"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
@ -141,7 +168,9 @@ const statesFilter = {
:label="t('claim.basicData.state')"
map-options
use-input
@filter="(value, update) => filter(value, update, statesFilter)"
@filter="
(value, update) => filter(value, update, statesFilter)
"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
@ -166,7 +195,10 @@ const statesFilter = {
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-checkbox v-model="data.hasToPickUp" :label="t('claim.basicData.picked')" />
<q-checkbox
v-model="data.hasToPickUp"
:label="t('claim.basicData.picked')"
/>
</div>
</div>
</template>
@ -176,12 +208,8 @@ const statesFilter = {
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 800px;
width: 100%;
max-width: 60em;
}
</style>

View File

@ -3,20 +3,19 @@ import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import ClaimDescriptor from './ClaimDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const { t } = useI18n();
</script>
<template>
<teleport-slot to="#searchbar">
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="ClaimList"
:label="t('Search claim')"
:info="t('You can search by claim id or customer name')"
/>
</teleport-slot>
</Teleport>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit">
<claim-descriptor />

View File

@ -6,6 +6,7 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { usePrintService } from 'composables/usePrintService';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
const $props = defineProps({
claim: {
@ -33,13 +34,15 @@ function confirmPickupOrder() {
quasar.dialog({
component: SendEmailDialog,
componentProps: {
data: {
address: customer.email,
},
send: sendPickupOrder,
},
});
}
function sendPickupOrder(address) {
function sendPickupOrder({ address }) {
const id = claim.value.id;
const customer = claim.value.client;
return sendEmail(`Claims/${id}/claim-pickup-email`, {
@ -48,16 +51,26 @@ function sendPickupOrder(address) {
});
}
const showConfirmDialog = ref(false);
async function deleteClaim() {
function confirmRemove() {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('confirmDeletion'),
message: t('confirmDeletionMessage'),
promise: remove,
},
})
.onOk(async () => await router.push({ name: 'ClaimList' }));
}
async function remove() {
const id = claim.value.id;
await axios.delete(`Claims/${id}`);
quasar.notify({
message: t('globals.dataDeleted'),
type: 'positive',
icon: 'check',
type: 'positive'
});
await router.push({ name: 'ClaimList' });
}
</script>
<template>
@ -87,27 +100,12 @@ async function deleteClaim() {
</q-menu>
</q-item>
<q-separator />
<q-item @click="showConfirmDialog = true" v-ripple clickable>
<q-item @click="confirmRemove()" v-ripple clickable>
<q-item-section avatar>
<q-icon name="delete" />
</q-item-section>
<q-item-section>{{ t('deleteClaim') }}</q-item-section>
</q-item>
<q-dialog v-model="showConfirmDialog">
<q-card class="q-pa-sm">
<q-card-section class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{ t('confirmDeletion') }}</span>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-card-section class="row items-center">{{ t('confirmDeletionMessage') }}</q-card-section>
<q-card-actions align="right">
<q-btn :label="t('globals.cancel')" color="primary" flat v-close-popup />
<q-btn :label="t('globals.confirm')" color="primary" @click="deleteClaim" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<i18n>

View File

@ -0,0 +1,189 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useSession } from 'src/composables/useSession';
import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue';
import ClaimLogFilter from './ClaimLogFilter.vue';
import { toDate } from 'src/filters';
const stateStore = useStateStore();
const route = useRoute();
const session = useSession();
const token = session.getToken();
const { t } = useI18n();
const columns = [
{
name: 'property',
label: 'Property',
field: (row) => t(`properties.${row.property}`),
align: 'left',
},
{
name: 'before',
label: 'Before',
field: (row) => formatValue(row.before),
},
{
name: 'after',
label: 'After',
field: (row) => formatValue(row.after),
},
];
function formatValue(value) {
if (typeof value === 'boolean') {
return value ? t('Yes') : t('No');
}
if (isNaN(value) && !isNaN(Date.parse(value))) {
return toDate(value);
}
if (value === undefined) {
return t('Nothing');
}
return `"${value}"`;
}
function actionColor(action) {
if (action === 'insert') return 'positive';
if (action === 'update') return 'positive';
if (action === 'delete') return 'negative';
}
</script>
<template>
<div class="column items-center">
<q-timeline class="q-pa-md">
<q-timeline-entry heading tag="h4"> {{ t('Audit logs') }} </q-timeline-entry>
<Paginate
data-key="ClaimLogs"
:url="`Claims/${route.params.id}/logs`"
order="id DESC"
:offset="100"
:limit="5"
auto-load
>
<template #body="{ rows }">
<template v-for="log of rows" :key="log.id">
<q-timeline-entry
:avatar="`/api/Images/user/160x160/${log.userFk}/download?access_token=${token}`"
>
<template #subtitle>
{{ log.userName }} -
{{
toDate(log.created, {
dateStyle: 'medium',
timeStyle: 'short',
})
}}
</template>
<template #title>
<q-chip :color="actionColor(log.action)">
{{ t(`actions.${log.action}`) }}
</q-chip>
{{ t(`models.${log.model}`) }}
</template>
<q-table
:rows="log.changes"
:columns="columns"
row-key="property"
hide-pagination
dense
flat
>
<template #header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ t(col.label) }}
</q-th>
</q-tr>
</template>
</q-table>
</q-timeline-entry>
</template>
</template>
</Paginate>
</q-timeline>
</div>
<Teleport v-if="stateStore.isHeaderMounted()" to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
<q-drawer v-model="stateStore.rightDrawer" show-if-above side="right" :width="300">
<q-scroll-area class="fit text-grey-8">
<ClaimLogFilter data-key="ClaimLogs" />
</q-scroll-area>
</q-drawer>
</template>
<style lang="scss" scoped>
.q-timeline {
width: 100%;
max-width: 80em;
}
</style>
<i18n>
en:
actions:
insert: Creates
update: Updates
delete: Deletes
models:
Claim: Claim
ClaimDms: Document
ClaimBeginning: Claimed Sales
ClaimObservation: Observation
properties:
id: ID
claimFk: Claim ID
saleFk: Sale ID
quantity: Quantity
observation: Observation
ticketCreated: Created
created: Created
isChargedToMana: Charged to mana
hasToPickUp: Has to pick Up
dmsFk: Document ID
text: Description
es:
Audit logs: Registros de auditoría
Property: Propiedad
Before: Antes
After: Después
Yes: Si
Nothing: Nada
actions:
insert: Crea
update: Actualiza
delete: Elimina
models:
Claim: Reclamación
ClaimDms: Documento
ClaimBeginning: Línea reclamada
ClaimObservation: Observación
properties:
id: ID
claimFk: ID reclamación
saleFk: ID linea de venta
quantity: Cantidad
observation: Observación
ticketCreated: Creado
created: Creado
isChargedToMana: Cargado a maná
hasToPickUp: Se debe recoger
dmsFk: ID documento
text: Descripción
</i18n>

View File

@ -0,0 +1,79 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const workers = ref();
</script>
<template>
<fetch-data
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<q-date
v-model="params.created"
@update:model-value="searchFn()"
dense
flat
minimal
>
</q-date>
<q-list dense>
<q-separator />
<q-item>
<q-item-section v-if="!workers">
<q-skeleton type="QInput" class="full-width" />
</q-item-section>
<q-item-section v-if="workers">
<q-select
:label="t('User')"
v-model="params.userFk"
@update:model-value="searchFn()"
:options="workers"
option-value="id"
option-label="name"
emit-value
map-options
use-input
:input-debounce="0"
/>
</q-item-section>
</q-item>
</q-list>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
search: Contains
userFk: User
created: Created
es:
params:
search: Contiene
userFk: Usuario
created: Creada
User: Usuario
</i18n>

View File

@ -1,22 +1,20 @@
<script setup>
import { ref, computed } from 'vue';
import axios from 'axios';
import { ref, computed } from 'vue';
import { useQuasar } from 'quasar';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import TeleportSlot from 'src/components/ui/TeleportSlot.vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import { useStateStore } from 'stores/useStateStore';
import { useSession } from 'composables/useSession';
import VnConfirm from 'components/ui/VnConfirm.vue';
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();
const quasar = useQuasar();
const claimId = computed(() => router.currentRoute.value.params.id);
@ -66,23 +64,23 @@ function openDialog(dmsId) {
multimediaDialog.value = true;
}
function viewDeleteDms(dmsId) {
function viewDeleteDms(index) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
message: t('This file will be deleted'),
title: t('This file will be deleted'),
icon: 'delete',
data: { index },
promise: deleteDms,
},
})
.onOk(() => deleteDms(dmsId));
.onOk(() => claimDms.value.splice(index, 1));
}
async function deleteDms(index) {
async function deleteDms({ index }) {
const dmsId = claimDms.value[index].dmsFk;
await axios.post(`ClaimDms/${dmsId}/removeFile`);
claimDms.value.splice(index, 1);
quasar.notify({
message: t('globals.dataDeleted'),
type: 'positive',
@ -95,7 +93,7 @@ function setClaimDms(data) {
claimDms.value = data.claimDms.map((media) => {
media.isVideo = media.dms.contentType == 'video/mp4';
media.url = `${window.location.origin}/api/Claims/${media.dmsFk}/downloadFile?access_token=${token}`;
media.url = `/api/Claims/${media.dmsFk}/downloadFile?access_token=${token}`;
return media;
});
client.value = data.client;
@ -239,7 +237,10 @@ function onDrag() {
</div>
</div>
<teleport-slot v-if="!quasar.platform.is.mobile" to="#header-actions">
<Teleport
v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile"
to="#actions-prepend"
>
<div class="row q-gutter-x-sm">
<label for="fileInput">
<q-btn
@ -262,10 +263,16 @@ function onDrag() {
</label>
<q-separator vertical />
</div>
</teleport-slot>
</Teleport>
<teleport-slot to=".q-footer">
<q-tabs align="justify" inline-label narrow-indicator>
<q-page-sticky
v-if="quasar.platform.is.mobile"
position="bottom"
:offset="[0, 0]"
expand
>
<q-toolbar class="bg-primary text-white q-pa-none">
<q-tabs class="full-width" align="justify" inline-label narrow-indicator>
<q-tab
@click="inputFile.nativeEl.click()"
icon="add_circle"
@ -282,7 +289,8 @@ function onDrag() {
<q-tooltip bottom> {{ t('globals.add') }} </q-tooltip>
</q-tab>
</q-tabs>
</teleport-slot>
</q-toolbar>
</q-page-sticky>
<!-- MULTIMEDIA DIALOG START-->
<q-dialog

View File

@ -5,9 +5,9 @@ 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 Paginate from 'src/components/PaginateData.vue';
import FetchData from 'components/FetchData.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import { toDate } from 'src/filters';
@ -15,6 +15,7 @@ import { toDate } from 'src/filters';
const quasar = useQuasar();
const route = useRoute();
const { t } = useI18n();
const stateStore = useStateStore();
const arrayData = useArrayData('ClaimRma');
const claim = ref();
@ -44,6 +45,12 @@ async function onFetch(data) {
}
async function addRow() {
if (!claim.value.rma) {
return quasar.notify({
message: `This claim is not associated to any RMA`,
type: 'negative',
});
}
const formData = {
code: claim.value.rma,
};
@ -62,18 +69,19 @@ function confirmRemove(id) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
data: { id },
promise: remove,
},
})
.onOk(() => remove(id));
.onOk(async () => await arrayData.refresh());
}
async function remove(id) {
async function remove({ id }) {
await axios.delete(`ClaimRmas/${id}`);
await arrayData.refresh();
quasar.notify({
type: 'positive',
message: t('globals.rowRemoved'),
icon: 'check',
});
}
</script>
@ -84,6 +92,8 @@ async function remove(id) {
@on-fetch="onFetch"
auto-load
/>
<div class="column items-center">
<div class="list">
<paginate data-key="ClaimRma" url="ClaimRmas">
<template #body="{ rows }">
<q-card class="card">
@ -134,24 +144,40 @@ async function remove(id) {
</q-card>
</template>
</paginate>
</div>
</div>
<teleport-slot v-if="!quasar.platform.is.mobile" to="#header-actions">
<Teleport
v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile"
to="#actions-prepend"
>
<div class="row q-gutter-x-sm">
<q-btn @click="addRow()" icon="add" color="primary" dense rounded>
<q-tooltip bottom> {{ t('globals.add') }} </q-tooltip>
</q-btn>
<q-separator vertical />
</div>
</teleport-slot>
</Teleport>
<teleport-slot to=".q-footer">
<q-tabs align="justify" inline-label narrow-indicator>
<q-page-sticky
v-if="quasar.platform.is.mobile"
position="bottom"
:offset="[0, 0]"
expand
>
<q-toolbar class="bg-primary text-white q-pa-none">
<q-tabs class="full-width" align="justify" inline-label narrow-indicator>
<q-tab @click="addRow()" icon="add_circle" :label="t('globals.add')" />
</q-tabs>
</teleport-slot>
</q-toolbar>
</q-page-sticky>
</template>
<style lang="scss" scoped>
.list {
width: 100%;
max-width: 60em;
}
.q-toolbar {
background-color: $grey-9;
}
@ -163,3 +189,8 @@ async function remove(id) {
z-index: 2998;
}
</style>
<i18n>
es:
This claim is not associated to any RMA: Esta reclamación no está asociada a ninguna ARM
</i18n>

View File

@ -1,15 +1,13 @@
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters/index';
import Paginate from 'src/components/PaginateData.vue';
import { toDate } from 'filters/index';
import Paginate from 'components/PaginateData.vue';
import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue';
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import CustomerDescriptorPopover from 'pages/Customer/Card/CustomerDescriptorPopover.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import ClaimFilter from './ClaimFilter.vue';
const stateStore = useStateStore();
@ -17,9 +15,6 @@ const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false));
function stateColor(code) {
if (code === 'pending') return 'green';
if (code === 'managed') return 'orange';
@ -41,20 +36,40 @@ function viewSummary(id) {
</script>
<template>
<teleport-slot to="#searchbar">
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="ClaimList"
:label="t('Search claim')"
:info="t('You can search by claim id or customer name')"
/>
</teleport-slot>
<teleport-slot to="#rightPanel">
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
</template>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<ClaimFilter data-key="ClaimList" />
</teleport-slot>
<q-page class="q-pa-md">
</q-scroll-area>
</q-drawer>
<q-page class="column items-center q-pa-md">
<div class="card-list">
<paginate data-key="ClaimList" url="Claims/filter" order="id DESC" auto-load>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-card class="card q-mb-md" v-for="row of rows" :key="row.id">
<q-item
class="q-pa-none items-start cursor-pointer q-hoverable"
v-ripple
@ -71,13 +86,17 @@ function viewSummary(id) {
<q-item-label caption>
{{ t('claim.list.customer') }}
</q-item-label>
<q-item-label>{{ row.clientName }}</q-item-label>
<q-item-label>
{{ row.clientName }}
</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>
{{ t('claim.list.assignedTo') }}
</q-item-label>
<q-item-label>{{ row.workerName }}</q-item-label>
<q-item-label>
{{ row.workerName }}
</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
@ -163,9 +182,17 @@ function viewSummary(id) {
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search claim: Buscar reclamación

View File

@ -43,23 +43,25 @@ function confirm(id) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
data: { id },
promise: remove,
},
})
.onOk(() => remove(id));
.onOk(async () => await arrayData.refresh());
}
async function remove(id) {
async function remove({ id }) {
await axios.delete(`ClaimRmas/${id}`);
await arrayData.refresh();
quasar.notify({
type: 'positive',
message: t('globals.rowRemoved'),
icon: 'check',
});
}
</script>
<template>
<q-page class="q-pa-md sticky">
<q-page class="column items-center q-pa-md sticky">
<q-page-sticky expand position="top" :offset="[16, 16]">
<q-card class="card q-pa-md">
<q-form @submit="submit">
@ -79,6 +81,7 @@ async function remove(id) {
</q-form>
</q-card>
</q-page-sticky>
<div class="card-list">
<paginate
data-key="ClaimRmaList"
url="ClaimRmas"
@ -105,7 +108,11 @@ async function remove(id) {
</q-list>
</q-item-section>
<q-card-actions vertical class="justify-between">
<q-skeleton type="circle" class="q-mb-md" size="40px" />
<q-skeleton
type="circle"
class="q-mb-md"
size="40px"
/>
</q-card-actions>
</q-item>
<q-separator />
@ -119,7 +126,9 @@ async function remove(id) {
<q-item-label caption>{{
t('claim.rmaList.code')
}}</q-item-label>
<q-item-label>{{ row.code }}</q-item-label>
<q-item-label>{{
row.code
}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
@ -141,6 +150,7 @@ async function remove(id) {
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
@ -149,7 +159,7 @@ async function remove(id) {
padding-top: 156px;
}
.card {
.card-list, .card {
width: 100%;
max-width: 60em;
}

View File

@ -58,7 +58,7 @@ const filterOptions = {
@on-fetch="(data) => (businessTypes = data)"
auto-load
/>
<div class="container">
<div class="column items-center">
<q-card>
<form-model :url="`Clients/${route.params.id}`" model="customer">
<template #form="{ data, validate, filter }">
@ -172,11 +172,6 @@ const filterOptions = {
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 800px;
}

View File

@ -3,20 +3,19 @@ import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const { t } = useI18n();
</script>
<template>
<teleport-slot to="#searchbar">
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="CustomerList"
:label="t('Search customer')"
:info="t('You can search by customer id or name')"
/>
</teleport-slot>
</Teleport>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit">
<CustomerDescriptor />

View File

@ -4,6 +4,7 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const $props = defineProps({
id: {
@ -30,7 +31,10 @@ const entityId = computed(() => {
{{ t('customer.card.salesPerson') }}
</q-item-label>
<q-item-label class="col q-ma-none">
<span class="link">
{{ entity.salesPersonUser.name }}
<WorkerDescriptorProxy :id="entity.salesPersonFk" />
</span>
</q-item-label>
</q-item>
<q-item class="row">

View File

@ -1,12 +1,10 @@
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue';
import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import CustomerFilter from './CustomerFilter.vue';
@ -15,9 +13,6 @@ const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false));
function navigate(id) {
router.push({ path: `/customer/${id}` });
}
@ -33,20 +28,45 @@ function viewSummary(id) {
</script>
<template>
<teleport-slot to="#searchbar">
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="CustomerList"
:label="t('Search customer')"
:info="t('You can search by customer id or name')"
/>
</teleport-slot>
<teleport-slot to="#rightPanel">
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
</template>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<CustomerFilter data-key="CustomerList" />
</teleport-slot>
<q-page class="q-pa-md">
<paginate data-key="CustomerList" url="/Clients/filter" order="id DESC" auto-load>
</q-scroll-area>
</q-drawer>
<q-page class="column items-center q-pa-md">
<div class="card-list">
<paginate
data-key="CustomerList"
url="/Clients/filter"
order="id DESC"
auto-load
>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-card class="card q-mb-md" v-for="row of rows" :key="row.id">
<q-item
class="q-pa-none items-start cursor-pointer q-hoverable"
v-ripple
@ -100,7 +120,7 @@ function viewSummary(id) {
<q-btn
flat
round
color="orange"
color="primary"
icon="arrow_circle_right"
@click="navigate(row.id)"
>
@ -127,9 +147,17 @@ function viewSummary(id) {
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search customer: Buscar cliente

View File

@ -3,20 +3,19 @@ import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const { t } = useI18n();
</script>
<template>
<teleport-slot to="#searchbar">
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="InvoiceOutList"
:label="t('Search invoice')"
:info="t('You can search by invoice reference')"
/>
</teleport-slot>
</Teleport>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit">
<InvoiceOutDescriptor />

View File

@ -7,7 +7,6 @@ import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue';
import InvoiceOutSummaryDialog from './Card/InvoiceOutSummaryDialog.vue';
import { toDate, toCurrency } from 'src/filters/index';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import InvoiceOutFilter from './InvoiceOutFilter.vue';
@ -34,17 +33,37 @@ function viewSummary(id) {
</script>
<template>
<teleport-slot to="#searchbar">
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="InvoiceOutList"
:label="t('Search invoice')"
:info="t('You can search by invoice reference')"
/>
</teleport-slot>
<teleport-slot to="#rightPanel">
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
</template>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<InvoiceOutFilter data-key="InvoiceOutList" />
</teleport-slot>
<q-page class="q-pa-md">
</q-scroll-area>
</q-drawer>
<q-page class="column items-center q-pa-md">
<div class="card-list">
<paginate
data-key="InvoiceOutList"
url="InvoiceOuts/filter"
@ -52,7 +71,7 @@ function viewSummary(id) {
auto-load
>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-card class="card q-mb-md" v-for="row of rows" :key="row.id">
<q-item
class="q-pa-none items-start cursor-pointer q-hoverable"
v-ripple
@ -103,7 +122,9 @@ function viewSummary(id) {
<q-item-label caption>
{{ t('invoiceOut.list.company') }}
</q-item-label>
<q-item-label>{{ row.companyCode }}</q-item-label>
<q-item-label>{{
row.companyCode
}}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>
@ -145,9 +166,17 @@ function viewSummary(id) {
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search invoice: Buscar factura emitida

View File

@ -3,20 +3,19 @@ import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import TicketDescriptor from './TicketDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const { t } = useI18n();
</script>
<template>
<teleport-slot to="#searchbar">
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="TicketList"
:label="t('Search ticket')"
:info="t('You can search by ticket id or alias')"
/>
</teleport-slot>
</Teleport>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit">
<TicketDescriptor />

View File

@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters';
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import TicketDescriptorMenu from './TicketDescriptorMenu.vue';
const $props = defineProps({
id: {
@ -23,11 +24,25 @@ const entityId = computed(() => {
const filter = {
include: [
{
relation: 'address',
scope: {
fields: ['id', 'name', 'mobile', 'phone'],
},
},
{
relation: 'client',
scope: {
fields: ['id', 'name', 'salesPersonFk'],
include: { relation: 'salesPersonUser' },
fields: ['id', 'name', 'salesPersonFk', 'phone', 'mobile', 'email'],
include: [
{
relation: 'user',
scope: {
fields: ['id', 'lang'],
},
},
{ relation: 'salesPersonUser' },
],
},
},
{
@ -61,6 +76,9 @@ function stateColor(state) {
<template>
<card-descriptor module="Ticket" :url="`Tickets/${entityId}`" :filter="filter">
<template #menu="{ entity }">
<TicketDescriptorMenu :ticket="entity" />
</template>
<template #description="{ entity }">
<span>
{{ entity.client.name }}
@ -121,6 +139,16 @@ function stateColor(state) {
</q-item-section>
</q-item>
</q-list>
<q-card-actions class="q-gutter-md">
<q-icon
v-if="entity.isDeleted == true"
name="vn:deletedTicket"
size="xs"
color="primary"
>
<q-tooltip>{{ t('This ticket is deleted') }}</q-tooltip>
</q-icon>
</q-card-actions>
<q-card-actions>
<q-btn
@ -135,3 +163,8 @@ function stateColor(state) {
</template>
</card-descriptor>
</template>
<i18n>
es:
This ticket is deleted: Este ticket está eliminado
</i18n>

View File

@ -0,0 +1,256 @@
<script setup>
import axios from 'axios';
import { ref } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import { usePrintService } from 'composables/usePrintService';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnSmsDialog from 'components/common/VnSmsDialog.vue';
import toDate from 'filters/toDate';
const props = defineProps({
ticket: {
type: Object,
required: true,
},
});
const router = useRouter();
const route = useRoute();
const quasar = useQuasar();
const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const ticket = ref(props.ticket);
function openDeliveryNote(type = 'deliveryNote', documentType = 'pdf') {
const path = `Tickets/${ticket.value.id}/delivery-note-${documentType}`;
openReport(path, {
recipientId: ticket.value.clientFk,
type: type,
});
}
function sendDeliveryNoteConfirmation(type = 'deliveryNote', documentType = 'pdf') {
const customer = ticket.value.client;
quasar.dialog({
component: SendEmailDialog,
componentProps: {
data: {
address: customer.email,
type: type,
documentType: documentType,
},
promise: sendDeliveryNote,
},
});
}
async function sendDeliveryNote({ address, type, documentType }) {
const id = ticket.value.id;
const customer = ticket.value.client;
let pathName = 'delivery-note-email';
if (documentType == 'csv') pathName = 'delivery-note-csv-email';
const path = `Tickets/${id}/${pathName}`;
return sendEmail(path, {
recipientId: customer.id,
recipient: address,
type: type,
});
}
const shipped = toDate(ticket.value.shipped);
function showSmsDialog(template, customData) {
const address = ticket.value.address;
const client = ticket.value.client;
const phone =
route.params.phone ||
address.mobile ||
address.phone ||
client.mobile ||
client.phone;
const data = {
orderId: ticket.value.id,
shipped: shipped,
};
if (typeof customData === 'object') {
Object.assign(data, customData);
}
quasar.dialog({
component: VnSmsDialog,
componentProps: {
phone: phone,
template: template,
locale: client.user.lang,
data: data,
promise: sendSms,
},
});
}
async function showSmsDialogWithChanges() {
const query = `TicketLogs/${route.params.id}/getChanges`;
const response = await axios.get(query);
showSmsDialog('orderChanges', { changes: response.data });
}
async function sendSms(body) {
await axios.post(`Tickets/${route.params.id}/sendSms`, body);
quasar.notify({
message: 'Notification sent',
type: 'positive',
});
}
function confirmDelete() {
quasar
.dialog({
component: VnConfirm,
componentProps: {
promise: remove,
},
})
.onOk(async () => await router.push({ name: 'TicketList' }));
}
async function remove() {
const id = route.params.id;
await axios.post(`Tickets/${id}/setDeleted`);
quasar.notify({
message: t('Ticket deleted'),
type: 'positive',
});
quasar.notify({
message: t('You can undo this action within the first hour'),
icon: 'info',
});
}
</script>
<template>
<q-item v-ripple clickable>
<q-item-section avatar>
<q-icon name="picture_as_pdf" />
</q-item-section>
<q-item-section>{{ t('Open Delivery Note...') }}</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top end" self="top start" auto-close bordered>
<q-list>
<q-item @click="openDeliveryNote('deliveryNote')" v-ripple clickable>
<q-item-section>{{ t('With prices') }}</q-item-section>
</q-item>
<q-item @click="openDeliveryNote('withoutPrices')" v-ripple clickable>
<q-item-section>{{ t('Without prices') }}</q-item-section>
</q-item>
<q-item
@click="openDeliveryNote('deliveryNote', 'csv')"
v-ripple
clickable
>
<q-item-section>{{ t('As CSV') }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item v-ripple clickable>
<q-item-section avatar>
<q-icon name="send" />
</q-item-section>
<q-item-section>{{ t('Send Delivery Note...') }}</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top end" self="top start" auto-close>
<q-list>
<q-item
@click="sendDeliveryNoteConfirmation('deliveryNote')"
v-ripple
clickable
>
<q-item-section>{{ t('With prices') }}</q-item-section>
</q-item>
<q-item
@click="sendDeliveryNoteConfirmation('withoutPrices')"
v-ripple
clickable
>
<q-item-section>{{ t('Without prices') }}</q-item-section>
</q-item>
<q-item
@click="sendDeliveryNoteConfirmation('deliveryNote', 'csv')"
v-ripple
clickable
>
<q-item-section>{{ t('As CSV') }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item @click="openDeliveryNote('proforma')" v-ripple clickable>
<q-item-section avatar>
<q-icon name="receipt" />
</q-item-section>
<q-item-section>{{ t('Open Proforma Invoice') }}</q-item-section>
</q-item>
<q-item v-ripple clickable>
<q-item-section avatar>
<q-icon name="sms" />
</q-item-section>
<q-item-section>{{ t('Send SMS...') }}</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top end" self="top start" auto-close>
<q-list>
<q-item @click="showSmsDialog('pendingPayment')" v-ripple clickable>
<q-item-section>{{ t('Pending payment') }}</q-item-section>
</q-item>
<q-item @click="showSmsDialog('minAmount')" v-ripple clickable>
<q-item-section>{{ t('Minimum amount') }}</q-item-section>
</q-item>
<q-item
@click="showSmsDialogWithChanges('orderChanges')"
v-ripple
clickable
>
<q-item-section>{{ t('Order changes') }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<template v-if="!ticket.isDeleted">
<q-separator />
<q-item @click="confirmDelete()" v-ripple clickable>
<q-item-section avatar>
<q-icon name="delete" />
</q-item-section>
<q-item-section>{{ t('Delete ticket') }}</q-item-section>
</q-item>
</template>
</template>
<i18n>
es:
Open Delivery Note...: Abrir albarán...
Send Delivery Note...: Enviar albarán...
With prices: Con precios
Without prices: Sin precios
As CSV: Como CSV
Open Proforma Invoice: Abrir factura proforma
Delete ticket: Eliminar ticket
Send SMS...: Enviar SMS
Pending payment: Pago pendiente
Minimum amount: Importe mínimo
Order changes: Cambios del pedido
Ticket deleted: Ticket eliminado
You can undo this action within the first hour: Puedes deshacer esta acción dentro de la primera hora
</i18n>

View File

@ -7,8 +7,6 @@ import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue';
import { toDate, toCurrency } from 'src/filters/index';
import TicketSummaryDialog from './Card/TicketSummaryDialog.vue';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import TicketFilter from './TicketFilter.vue';
@ -71,17 +69,37 @@ function viewSummary(id) {
</script>
<template>
<teleport-slot to="#searchbar">
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="TicketList"
:label="t('Search ticket')"
:info="t('You can search by ticket id or alias')"
/>
</teleport-slot>
<teleport-slot to="#rightPanel">
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
</template>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<TicketFilter data-key="TicketList" />
</teleport-slot>
<q-page class="q-pa-md">
</q-scroll-area>
</q-drawer>
<q-page class="column items-center q-pa-md">
<div class="card-list">
<paginate
data-key="TicketList"
url="Tickets/filter"
@ -90,7 +108,7 @@ function viewSummary(id) {
auto-load
>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-card class="card q-mb-md" v-for="row of rows" :key="row.id">
<q-item
class="q-pa-none items-start cursor-pointer q-hoverable"
v-ripple
@ -105,7 +123,9 @@ function viewSummary(id) {
<q-item-label caption>
{{ t('ticket.list.nickname') }}
</q-item-label>
<q-item-label>{{ row.nickname }}</q-item-label>
<q-item-label>{{
row.nickname
}}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>
@ -189,9 +209,17 @@ function viewSummary(id) {
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search ticket: Buscar ticket

View File

@ -0,0 +1,37 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import WorkerDescriptor from './WorkerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const stateStore = useStateStore();
const { t } = useI18n();
</script>
<template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="WorkerList"
:label="t('Search worker')"
:info="t('You can search by worker id or name')"
/>
</Teleport>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit">
<WorkerDescriptor />
<q-separator />
<left-menu source="card" />
</q-scroll-area>
</q-drawer>
<q-page-container>
<q-page class="q-pa-md">
<router-view></router-view>
</q-page>
</q-page-container>
</template>
<i18n>
es:
Search worker: Buscar trabajador
You can search by worker id or name: Puedes buscar por id o nombre del trabajador
</i18n>

View File

@ -0,0 +1,138 @@
<script setup>
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const route = useRoute();
const { t } = useI18n();
const { getToken } = useSession();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const worker = ref();
const filter = {
include: [
{
relation: 'user',
scope: {
fields: ['email', 'name', 'nickname'],
},
},
{
relation: 'department',
scope: {
include: [
{
relation: 'department',
},
],
},
},
{
relation: 'sip',
},
],
};
const sip = computed(() => worker.value.sip && worker.value.sip.extension);
function getWorkerAvatar() {
const token = getToken();
return `/api/Images/user/160x160/${route.params.id}/download?access_token=${token}`;
}
</script>
<template>
<card-descriptor
module="Worker"
:url="`Workers/${entityId}`"
:filter="filter"
@on-fetch="(data) => (worker = data)"
>
<template #before>
<q-img :src="getWorkerAvatar()" class="photo">
<template #error>
<div
class="absolute-full bg-grey-10 text-center q-pa-md flex flex-center"
>
<div>
<div class="text-grey-5" style="opacity: 0.4; font-size: 5vh">
<q-icon name="vn:claims" />
</div>
<div class="text-grey-5" style="opacity: 0.4">
{{ t('worker.imageNotFound') }}
</div>
</div>
</div>
</template>
</q-img>
</template>
<template #description="{ entity }">
<span>
{{ entity.user.nickname }}
<q-tooltip>{{ entity.user.nickname }}</q-tooltip>
</span>
</template>
<template #body="{ entity }">
<q-list>
<q-item>
<q-item-section>
<q-item-label caption> {{ t('worker.card.name') }} </q-item-label>
<q-item-label>{{ entity.user.nickname }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>
{{ t('worker.card.email') }}
</q-item-label>
<q-item-label>{{ entity.user.email }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>
{{ t('worker.list.department') }}
</q-item-label>
<q-item-label>
{{ entity.department.department.name }}
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>
{{ t('worker.card.phone') }}
</q-item-label>
<q-item-label>{{ entity.phone }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.sipExtension') }}
</q-item-label>
<q-item-label>{{ sip }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</template>
</card-descriptor>
</template>
<style lang="scss" scoped>
.photo {
height: 256px;
}
</style>

View File

@ -0,0 +1,16 @@
<script setup>
import WorkerDescriptor from './WorkerDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<q-popup-proxy>
<WorkerDescriptor v-if="$props.id" :id="$props.id" />
</q-popup-proxy>
</template>

View File

@ -0,0 +1,292 @@
<script setup>
import axios from 'axios';
import { ref, onMounted, computed, onUpdated } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import WorkerDescriptorProxy from './WorkerDescriptorProxy.vue';
onMounted(() => fetch());
onUpdated(() => fetch());
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const entityId = computed(() => $props.id || route.params.id);
const worker = ref(null);
const filter = {
include: [
{
relation: 'user',
scope: {
fields: ['email', 'name', 'nickname', 'roleFk'],
include: {
relation: 'role',
scope: {
fields: ['name'],
},
},
},
},
{
relation: 'department',
scope: {
include: {
relation: 'department',
scope: {
fields: ['name'],
},
},
},
},
{
relation: 'boss',
},
{
relation: 'client',
},
{
relation: 'sip',
},
],
};
function fetch() {
const id = entityId.value;
axios.get(`/Workers/${id}`, { params: { filter } }).then((response) => {
worker.value = response.data;
});
}
function sipExtension() {
if (worker.value.sip) return worker.value.sip.extension;
return '-';
}
</script>
<template>
<div class="summary container">
<q-card>
<SkeletonSummary v-if="!worker" />
<template v-if="worker">
<div class="header bg-primary q-pa-sm q-mb-md">
{{ worker.id }} - {{ worker.firstName }} {{ worker.lastName }}
</div>
<div class="row q-pa-md q-col-gutter-md q-mb-md">
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('worker.summary.basicData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption> ID </q-item-label>
<q-item-label>{{ worker.id }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.card.name') }}
</q-item-label>
<q-item-label>
{{ worker.user.nickname }}
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.list.department') }}
</q-item-label>
<q-item-label>{{
worker.department.department.name
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.list.email') }}
</q-item-label>
<q-item-label>{{ worker.user.email }}</q-item-label>
</q-item-section>
</q-item>
<q-item
class="items-start cursor-pointer q-hoverable"
v-if="worker.boss"
>
<q-item-section>
<q-item-label caption>
{{ t('worker.summary.boss') }}
</q-item-label>
<q-item-label>
<span class="link">
{{ worker.boss.name }}
<WorkerDescriptorProxy :id="worker.bossFk" />
</span>
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.phoneExtension') }}
</q-item-label>
<q-item-label>
{{
worker.mobileExtension == ''
? worker.mobileExtension
: '-'
}}
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.entPhone') }}
</q-item-label>
<q-item-label>{{
worker.phone == '' ? worker.phone : '-'
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.personalPhone') }}
</q-item-label>
<q-item-label>{{
worker.client.phone == ''
? worker.client.phone
: '-'
}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('worker.summary.userData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>
{{ t('worker.summary.userId') }}
</q-item-label>
<q-item-label>{{ worker.user.id }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.card.name') }}
</q-item-label>
<q-item-label>{{
worker.user.nickname
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.role') }}
</q-item-label>
<q-item-label>{{
worker.user.role.name
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption
>{{ t('worker.summary.sipExtension') }}
</q-item-label>
<q-item-label>{{ sipExtension() }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</template>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
max-width: 1200px;
}
.negative {
color: red;
}
.summary {
.q-list {
.q-item__label--header {
display: flex;
justify-content: space-between;
a {
color: $primary;
}
}
}
.row {
flex-wrap: wrap;
.col {
min-width: 250px;
}
}
.header {
text-align: center;
font-size: 18px;
}
#slider-container {
max-width: 80%;
margin: 0 auto;
.q-slider {
.q-slider__marker-labels:nth-child(1) {
transform: none;
}
.q-slider__marker-labels:nth-child(2) {
transform: none;
left: auto !important;
right: 0%;
}
}
}
}
.q-dialog .summary {
max-width: 1200px;
}
</style>

View File

@ -0,0 +1,21 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import WorkerSummary from './WorkerSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<worker-summary v-if="$props.id" :id="$props.id" />
</q-dialog>
</template>

View File

@ -0,0 +1,121 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const departments = ref();
</script>
<template>
<fetch-data url="Departments" @on-fetch="(data) => (departments = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<q-list dense>
<q-item>
<q-item-section>
<q-input :label="t('FI')" v-model="params.fi" lazy-rules>
<template #prepend>
<q-icon name="badge" size="sm"></q-icon>
</template>
</q-input>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-input
:label="t('First Name')"
v-model="params.firstName"
lazy-rules
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-input
:label="t('Last Name')"
v-model="params.lastName"
lazy-rules
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-input
:label="t('User Name')"
v-model="params.userName"
lazy-rules
/>
</q-item-section>
</q-item>
<q-item>
<q-item-section v-if="!departments">
<q-skeleton type="QInput" class="full-width" />
</q-item-section>
<q-item-section v-if="departments">
<q-select
:label="t('Department')"
v-model="params.departmentFk"
@update:model-value="searchFn()"
:options="departments"
option-value="id"
option-label="name"
emit-value
map-options
use-input
:input-debounce="0"
/>
</q-item-section>
</q-item>
<q-item class="q-mb-md">
<q-item-section>
<q-input
:label="t('Extension')"
v-model="params.extension"
lazy-rules
/>
</q-item-section>
</q-item>
</q-list>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
search: Contains
fi: FI
firstName: First name
lastName: Last name
userName: User
extension: Extension
es:
params:
search: Contiene
fi: NIF
firstName: Nombre
lastName: Apellidos
userName: Usuario
extension: Extensión
FI: NIF
First Name: Nombre
Last Name: Apellidos
User Name: Usuario
Department: Departamento
Extension: Extensión
</i18n>

View File

@ -0,0 +1,155 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import Paginate from 'src/components/PaginateData.vue';
import WorkerSummaryDialog from './Card/WorkerSummaryDialog.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import WorkerFilter from './WorkerFilter.vue';
const stateStore = useStateStore();
const router = useRouter();
const { t } = useI18n();
const quasar = useQuasar();
function navigate(id) {
router.push({ path: `/worker/${id}` });
}
function viewSummary(id) {
quasar.dialog({
component: WorkerSummaryDialog,
componentProps: {
id,
},
});
}
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="WorkerList"
:label="t('Search worker')"
:info="t('You can search by worker id or name')"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</Teleport>
</template>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<WorkerFilter data-key="WorkerList" />
</q-scroll-area>
</q-drawer>
<q-page class="column items-center q-pa-md">
<div class="card-list">
<paginate
data-key="WorkerList"
url="Workers/filter"
order="id DESC"
auto-load
>
<template #body="{ rows }">
<q-card class="card q-mb-md" v-for="row of rows" :key="row.id">
<q-item
class="q-pa-none items-start cursor-pointer q-hoverable"
v-ripple
clickable
>
<q-item-section class="q-pa-md" @click="navigate(row.id)">
<q-item-label class="text-h6">
{{ row.nickname }}
</q-item-label>
<q-item-label caption>#{{ row.id }}</q-item-label>
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>
{{ t('worker.list.name') }}
</q-item-label>
<q-item-label>{{
row.userName
}}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>
{{ t('worker.list.email') }}
</q-item-label>
<q-item-label>{{ row.email }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{
t('worker.list.department')
}}</q-item-label>
<q-item-label>
{{ row.department }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-item-section>
<q-separator vertical />
<q-card-actions vertical class="justify-between">
<q-btn
flat
round
color="primary"
icon="arrow_circle_right"
@click="navigate(row.id)"
>
<q-tooltip>
{{ t('components.smartCard.openCard') }}
</q-tooltip>
</q-btn>
<q-btn
flat
round
color="grey-7"
icon="preview"
@click="viewSummary(row.id)"
>
<q-tooltip>
{{ t('components.smartCard.openSummary') }}
</q-tooltip>
</q-btn>
</q-card-actions>
</q-item>
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search worker: Buscar trabajador
You can search by worker id or name: Puedes buscar por id o nombre del trabajador
</i18n>

View File

@ -0,0 +1,17 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'src/components/LeftMenu.vue';
const stateStore = useStateStore();
</script>
<template>
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<q-scroll-area class="fit text-grey-8">
<LeftMenu />
</q-scroll-area>
</q-drawer>
<q-page-container>
<router-view></router-view>
</q-page-container>
</template>

View File

@ -11,7 +11,7 @@ export default {
redirect: { name: 'ClaimMain' },
menus: {
main: ['ClaimList', 'ClaimRmaList'],
card: ['ClaimBasicData', 'ClaimRma', 'ClaimPhotos'],
card: ['ClaimBasicData', 'ClaimRma', 'ClaimPhotos', 'ClaimLog'],
},
children: [
{
@ -85,6 +85,15 @@ export default {
},
component: () => import('src/pages/Claim/Card/ClaimPhoto.vue'),
},
{
name: 'ClaimLog',
path: 'log',
meta: {
title: 'log',
icon: 'history',
},
component: () => import('src/pages/Claim/Card/ClaimLog.vue'),
},
],
},
],

View File

@ -2,10 +2,12 @@ import Customer from './customer';
import Ticket from './ticket';
import Claim from './claim';
import InvoiceOut from './invoiceOut';
import Worker from './worker';
export default [
Customer,
Ticket,
Claim,
InvoiceOut
InvoiceOut,
Worker
]

View File

@ -0,0 +1,51 @@
import { RouterView } from 'vue-router';
export default {
path: '/worker',
name: 'Worker',
meta: {
title: 'workers',
icon: 'vn:worker',
},
component: RouterView,
redirect: { name: 'WorkerMain' },
menus: {
main: ['WorkerList'],
card: [],
},
children: [
{
path: '',
name: 'WorkerMain',
component: () => import('src/pages/Worker/WorkerMain.vue'),
redirect: { name: 'WorkerList' },
children: [
{
path: 'list',
name: 'WorkerList',
meta: {
title: 'list',
icon: 'view_list',
},
component: () => import('src/pages/Worker/WorkerList.vue'),
},
],
},
{
name: 'WorkerCard',
path: ':id',
component: () => import('src/pages/Worker/Card/WorkerCard.vue'),
redirect: { name: 'WorkerSummary' },
children: [
{
name: 'WorkerSummary',
path: 'summary',
meta: {
title: 'summary',
},
component: () => import('src/pages/Worker/Card/WorkerSummary.vue'),
},
],
},
],
};

View File

@ -1,6 +1,7 @@
import customer from './modules/customer';
import ticket from './modules/ticket';
import claim from './modules/claim';
import worker from './modules/worker';
import invoiceOut from './modules/invoiceOut';
const routes = [
@ -26,6 +27,7 @@ const routes = [
customer,
ticket,
claim,
worker,
invoiceOut,
{
path: '/:catchAll(.*)*',

View File

@ -6,7 +6,7 @@ import { useRole } from 'src/composables/useRole';
import routes from 'src/router/modules';
export const useNavigationStore = defineStore('navigationStore', () => {
const modules = ['customer', 'claim', 'ticket', 'invoiceOut'];
const modules = ['customer', 'claim', 'ticket', 'invoiceOut', 'worker'];
const pinnedModules = ref([]);
const role = useRole();

View File

@ -0,0 +1,20 @@
describe('WorkerList', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('/#/worker/list');
});
it('should load workers', () => {
cy.get('div[class="q-item__label text-h6"]').eq(0).should('have.text', 'Jessica Jones');
cy.get('div[class="q-item__label text-h6"]').eq(1).should('have.text', 'Bruce Banner');
cy.get('div[class="q-item__label text-h6"]').eq(2).should('have.text', 'Charles Xavier');
});
it('should open the worker summary', () => {
cy.get('div[class="q-item__section column q-item__section--side justify-center q-pa-md"]').eq(0).click();
cy.get('div[class="header bg-primary q-pa-sm q-mb-md"').should('have.text', '1110 - Jessica Jones');
cy.get('div[class="q-item__label q-item__label--header text-h6"]').eq(0).should('have.text', 'Basic data');
cy.get('div[class="q-item__label q-item__label--header text-h6"]').eq(1).should('have.text', 'User data');
});
});

View File

@ -0,0 +1,15 @@
describe('WorkerSummary', () => {
beforeEach(() => {
cy.viewport(1280, 720)
cy.login('developer')
cy.visit('/#/worker/19/summary');
});
it('should load worker summary', () => {
cy.get('div[class="header bg-primary q-pa-sm q-mb-md"').should('have.text', '19 - salesBoss');
cy.get('div[class="q-item__label q-item__label--header text-h6"]').eq(0).should('have.text', 'Basic data');
cy.get('div[class="q-item__label q-item__label--header text-h6"]').eq(1).should('have.text', 'User data');
cy.get('div[class="q-item__section column q-item__section--main justify-center"]').eq(0).should('have.text', 'NamesalesBossNick');
});
});

View File

@ -34,10 +34,17 @@ describe('App', () => {
const response = {
response: {
status: 401,
data: {
error: {
message: 'Invalid username or password',
},
},
},
};
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
expect(vm.responseError(response)).rejects.toEqual(
expect.objectContaining(response)
);
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Invalid username or password',
@ -54,10 +61,17 @@ describe('App', () => {
const response = {
response: {
status: 401,
data: {
error: {
message: 'Access denied',
},
},
},
};
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
expect(vm.responseError(response)).rejects.toEqual(
expect.objectContaining(response)
);
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Access denied',

View File

@ -18,12 +18,12 @@ describe('ClaimDescriptorMenu', () => {
vi.clearAllMocks();
});
describe('deleteClaim()', () => {
describe('remove()', () => {
it('should delete the claim', async () => {
vi.spyOn(axios, 'delete').mockResolvedValue({ data: true });
vi.spyOn(vm.quasar, 'notify');
await vm.deleteClaim();
await vm.remove();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'positive' })

View File

@ -21,7 +21,7 @@ describe('ClaimPhoto', () => {
beforeAll(() => {
vm = createWrapper(ClaimPhoto, {
global: {
stubs: ['FetchData', 'TeleportSlot', 'vue-i18n'],
stubs: ['FetchData', 'vue-i18n'],
mocks: {
fetch: vi.fn(),
},
@ -38,7 +38,7 @@ describe('ClaimPhoto', () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
vi.spyOn(vm.quasar, 'notify');
await vm.deleteDms(0);
await vm.deleteDms({ index: 0 });
expect(axios.post).toHaveBeenCalledWith(
`ClaimDms/${claimMock.claimDms[0].dmsFk}/removeFile`
@ -46,7 +46,6 @@ describe('ClaimPhoto', () => {
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'positive' })
);
expect(vm.claimDms).toEqual([]);
});
});
@ -59,8 +58,10 @@ describe('ClaimPhoto', () => {
expect(vm.quasar.dialog).toHaveBeenCalledWith(
expect.objectContaining({
componentProps: {
message: 'This file will be deleted',
title: 'This file will be deleted',
icon: 'delete',
data: { index: 1 },
promise: vm.deleteDms
},
})
);
@ -78,7 +79,7 @@ describe('ClaimPhoto', () => {
contentType: 'contentType',
},
isVideo: false,
url: '///api/Claims/1/downloadFile?access_token=',
url: '/api/Claims/1/downloadFile?access_token=',
},
]);

View File

@ -1,4 +1,4 @@
import { installQuasar } from '@quasar/quasar-app-extension-testing-unit-vitest';
import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import { vi } from 'vitest';
@ -6,7 +6,7 @@ import { i18n } from 'src/boot/i18n';
import { Notify, Dialog } from 'quasar';
import axios from 'axios';
installQuasar({
installQuasarPlugin({
plugins: {
Notify,
Dialog,