Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 4797-worker-notification-selector
gitea/salix-front/pipeline/head This commit looks good
Details
gitea/salix-front/pipeline/head This commit looks good
Details
This commit is contained in:
commit
9dfcaaf056
19
.babelrc
19
.babelrc
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"plugins": ["@babel/plugin-syntax-dynamic-import"],
|
|
||||||
"env": {
|
|
||||||
"test": {
|
|
||||||
"plugins": ["dynamic-import-node"],
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"@babel/preset-env",
|
|
||||||
{
|
|
||||||
"modules": "commonjs",
|
|
||||||
"targets": {
|
|
||||||
"node": "current"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,6 @@
|
||||||
/dist
|
/dist
|
||||||
/src-bex/www
|
|
||||||
/src-capacitor
|
/src-capacitor
|
||||||
/src-cordova
|
/src-cordova
|
||||||
/.quasar
|
/.quasar
|
||||||
/node_modules
|
/node_modules
|
||||||
.eslintrc.js
|
.eslintrc.js
|
||||||
babel.config.js
|
|
||||||
|
|
13
.eslintrc.js
13
.eslintrc.js
|
@ -5,13 +5,13 @@ module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: '@babel/eslint-parser',
|
ecmaVersion: '2021', // Allows for the parsing of modern ECMAScript features
|
||||||
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
|
|
||||||
sourceType: 'module', // Allows for the use of imports
|
|
||||||
},
|
},
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
|
node: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
|
'vue/setup-compiler-macros': true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Rules order is important, please avoid shuffling them
|
// Rules order is important, please avoid shuffling them
|
||||||
|
@ -52,22 +52,19 @@ module.exports = {
|
||||||
process: 'readonly',
|
process: 'readonly',
|
||||||
Capacitor: 'readonly',
|
Capacitor: 'readonly',
|
||||||
chrome: 'readonly',
|
chrome: 'readonly',
|
||||||
defineProps: 'readonly', // Vue SFC setup compiler macro
|
|
||||||
defineEmits: 'readonly', // Vue SFC setup compiler macro
|
|
||||||
defineExpose: 'readonly', // Vue SFC setup compiler macro
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// add your custom rules here
|
// add your custom rules here
|
||||||
rules: {
|
rules: {
|
||||||
'prefer-promise-reject-errors': 'off',
|
'prefer-promise-reject-errors': 'off',
|
||||||
|
'no-unused-vars': 'warn',
|
||||||
|
|
||||||
// allow debugger during development only
|
// allow debugger during development only
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
},
|
},
|
||||||
|
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['**/*.spec.{js,ts}'],
|
files: ['test/cypress/**/*.spec.{js,ts}'],
|
||||||
extends: [
|
extends: [
|
||||||
// Add Cypress-specific lint rules, globals and Cypress plugin
|
// Add Cypress-specific lint rules, globals and Cypress plugin
|
||||||
// See https://github.com/cypress-io/eslint-plugin-cypress#rules
|
// See https://github.com/cypress-io/eslint-plugin-cypress#rules
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.thumbs.db
|
.thumbs.db
|
||||||
node_modules
|
node_modules
|
||||||
junit.xml
|
|
||||||
|
|
||||||
# Quasar core related directories
|
# Quasar core related directories
|
||||||
.quasar
|
.quasar
|
||||||
|
@ -17,10 +16,6 @@ junit.xml
|
||||||
/src-capacitor/www
|
/src-capacitor/www
|
||||||
/src-capacitor/node_modules
|
/src-capacitor/node_modules
|
||||||
|
|
||||||
# BEX related directories and files
|
|
||||||
/src-bex/www
|
|
||||||
/src-bex/js/core
|
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
@ -32,3 +27,7 @@ yarn-error.log*
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
|
|
||||||
|
# Cypress directories and files
|
||||||
|
/tests/cypress/videos
|
||||||
|
/tests/cypress/screenshots
|
|
@ -0,0 +1,3 @@
|
||||||
|
# pnpm-related options
|
||||||
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
|
@ -1,9 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
// https://github.com/michael-ciniawsky/postcss-load-config
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
plugins: [
|
|
||||||
// to edit target browsers: use "browserslist" field in package.json
|
|
||||||
require('autoprefixer'),
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,6 +1,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
printWidth: 120,
|
printWidth: 90,
|
||||||
tabWidth: 4,
|
tabWidth: 4,
|
||||||
semi: true,
|
semi: true,
|
||||||
endOfLine: 'auto',
|
endOfLine: 'auto',
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"editorconfig.editorconfig",
|
"editorconfig.editorconfig",
|
||||||
"Vue.volar",
|
"vue.volar",
|
||||||
"wayou.vscode-todo-highlight"
|
"wayou.vscode-todo-highlight"
|
||||||
],
|
],
|
||||||
"unwantedRecommendations": [
|
"unwantedRecommendations": [
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2253.01] - 2023-01-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- (Clientes) => Añadida nueva sección "Pagos Web" para gestionar los pagos de todos los clientes
|
||||||
|
- (Tickets) => Añadida opción en el menú desplegable del ticket para enviar SMS al cliente
|
||||||
|
- (Reclamaciones) => Añadida nueva sección "Registros de auditoría"
|
||||||
|
- (Trabajadores) => Añadido módulo de trabajadores
|
||||||
|
- (General) => Añadida barra de búsqueda general en los listados principales
|
||||||
|
- (Vagones) => Añadido módulo de vagones
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed...
|
|
@ -31,8 +31,7 @@ pipeline {
|
||||||
NODE_ENV = ""
|
NODE_ENV = ""
|
||||||
}
|
}
|
||||||
steps {
|
steps {
|
||||||
nodejs('node-v14') {
|
nodejs('node-v18') {
|
||||||
sh 'npm install -g @quasar/cli'
|
|
||||||
sh 'npm install --no-audit --prefer-offline'
|
sh 'npm install --no-audit --prefer-offline'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +47,7 @@ pipeline {
|
||||||
parallel {
|
parallel {
|
||||||
stage('Frontend') {
|
stage('Frontend') {
|
||||||
steps {
|
steps {
|
||||||
nodejs('node-v14') {
|
nodejs('node-v18') {
|
||||||
sh 'npm run test:unit:ci'
|
sh 'npm run test:unit:ci'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +63,7 @@ pipeline {
|
||||||
CREDENTIALS = credentials('docker-registry')
|
CREDENTIALS = credentials('docker-registry')
|
||||||
}
|
}
|
||||||
steps {
|
steps {
|
||||||
nodejs('node-v14') {
|
nodejs('node-v18') {
|
||||||
sh 'quasar build'
|
sh 'quasar build'
|
||||||
}
|
}
|
||||||
dockerBuild()
|
dockerBuild()
|
||||||
|
|
22
README.md
22
README.md
|
@ -1,12 +1,10 @@
|
||||||
# Salix (salix-front)
|
# Lilium (lilium-front)
|
||||||
|
|
||||||
Salix front-end
|
Lilium frontend
|
||||||
|
|
||||||
## Install the dependencies
|
## Install the dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn
|
|
||||||
# or
|
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -22,20 +20,16 @@ sudo npm install -g @quasar/cli
|
||||||
quasar dev
|
quasar dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lint the files
|
### Run unit tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn lint
|
npm run test:unit
|
||||||
# or
|
|
||||||
npm run lint
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Format the files
|
### Run e2e tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn format
|
npm run test:e2e
|
||||||
# or
|
|
||||||
npm run format
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build the app for production
|
### Build the app for production
|
||||||
|
@ -43,7 +37,3 @@ npm run format
|
||||||
```bash
|
```bash
|
||||||
quasar build
|
quasar build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Customize the configuration
|
|
||||||
|
|
||||||
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js).
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
/* eslint-env node */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
let extend = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The .babelrc file has been created to assist Jest for transpiling.
|
|
||||||
* You should keep your application's babel rules in this file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (fs.existsSync('./.babelrc')) {
|
|
||||||
extend = './.babelrc';
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
presets: ['@quasar/babel-preset-app'],
|
|
||||||
extends: extend,
|
|
||||||
};
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
const { defineConfig } = require('cypress');
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:9000/',
|
||||||
|
fixturesFolder: 'test/cypress/fixtures',
|
||||||
|
screenshotsFolder: 'test/cypress/screenshots',
|
||||||
|
supportFile: 'test/cypress/support/index.js',
|
||||||
|
videosFolder: 'test/cypress/videos',
|
||||||
|
video: true,
|
||||||
|
specPattern: 'test/cypress/integration/*.spec.js',
|
||||||
|
experimentalRunAllSpecs: true,
|
||||||
|
component: {
|
||||||
|
componentFolder: 'src',
|
||||||
|
testFiles: '**/*.spec.js',
|
||||||
|
supportFile: 'test/cypress/support/unit.js',
|
||||||
|
},
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
// implement node event listeners here
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
15
cypress.json
15
cypress.json
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"baseUrl": "http://localhost:8080/",
|
|
||||||
"fixturesFolder": "tests/cypress/fixtures",
|
|
||||||
"integrationFolder": "tests/cypress/integration",
|
|
||||||
"pluginsFile": "tests/cypress/plugins/index.js",
|
|
||||||
"screenshotsFolder": "tests/cypress/screenshots",
|
|
||||||
"supportFile": "tests/cypress/support/index.js",
|
|
||||||
"videosFolder": "tests/cypress/videos",
|
|
||||||
"video": true,
|
|
||||||
"component": {
|
|
||||||
"componentFolder": "src",
|
|
||||||
"testFiles": "**/*.spec.js",
|
|
||||||
"supportFile": "tests/cypress/support/unit.js"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,7 +19,6 @@
|
||||||
<link rel="icon" type="image/ico" href="favicon.ico" />
|
<link rel="icon" type="image/ico" href="favicon.ico" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- DO NOT touch the following DIV -->
|
<!-- quasar:entry-point -->
|
||||||
<div id="q-app"></div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,59 +0,0 @@
|
||||||
const esModules = ['quasar', 'quasar/lang', 'lodash-es'].join('|');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
globals: {
|
|
||||||
__DEV__: true,
|
|
||||||
// TODO: Remove if resolved natively
|
|
||||||
// See https://github.com/vuejs/vue-jest/issues/175
|
|
||||||
'vue-jest': {
|
|
||||||
pug: { doctype: 'html' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Jest assumes we are testing in node environment, specify jsdom environment instead
|
|
||||||
testEnvironment: 'jsdom',
|
|
||||||
// noStackTrace: true,
|
|
||||||
// bail: true,
|
|
||||||
// cache: false,
|
|
||||||
// verbose: true,
|
|
||||||
// watch: true,
|
|
||||||
reporters: ['default', 'jest-junit'],
|
|
||||||
collectCoverage: false,
|
|
||||||
coverageDirectory: '<rootDir>/tests/jest/coverage',
|
|
||||||
collectCoverageFrom: ['<rootDir>/src/**/*.vue', '<rootDir>/src/**/*.js', '<rootDir>/src/**/*.jsx'],
|
|
||||||
// Needed in JS codebases too because of feature flags
|
|
||||||
coveragePathIgnorePatterns: ['/node_modules/', '.d.ts$'],
|
|
||||||
coverageThreshold: {
|
|
||||||
global: {
|
|
||||||
// branches: 50,
|
|
||||||
// functions: 50,
|
|
||||||
// lines: 50,
|
|
||||||
// statements: 50
|
|
||||||
},
|
|
||||||
},
|
|
||||||
testMatch: ['<rootDir>/src/**/__tests__/*.(spec|test).+(ts|js)?(x)'],
|
|
||||||
moduleFileExtensions: ['vue', 'js', 'jsx', 'json'],
|
|
||||||
moduleNameMapper: {
|
|
||||||
'^quasar$': 'quasar/dist/quasar.esm.prod.js',
|
|
||||||
'^~/(.*)$': '<rootDir>/$1',
|
|
||||||
'^src/(.*)$': '<rootDir>/src/$1',
|
|
||||||
'^app/(.*)$': '<rootDir>/$1',
|
|
||||||
'^components/(.*)$': '<rootDir>/src/components/$1',
|
|
||||||
'^composables/(.*)$': '<rootDir>/src/composables/$1',
|
|
||||||
'^filters/(.*)$': '<rootDir>/src/filters/$1',
|
|
||||||
'^layouts/(.*)$': '<rootDir>/src/layouts/$1',
|
|
||||||
'^pages/(.*)$': '<rootDir>/src/pages/$1',
|
|
||||||
'^assets/(.*)$': '<rootDir>/src/assets/$1',
|
|
||||||
'^boot/(.*)$': '<rootDir>/src/boot/$1',
|
|
||||||
'.*css$': '@quasar/quasar-app-extension-testing-unit-jest/stub.css',
|
|
||||||
},
|
|
||||||
transform: {
|
|
||||||
'.*\\.vue$': 'vue-jest',
|
|
||||||
'.*\\.js$': 'babel-jest',
|
|
||||||
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
|
|
||||||
// use these if NPM is being flaky, care as hosting could interfere with these
|
|
||||||
// '.*\\.vue$': '@quasar/quasar-app-extension-testing-unit-jest/node_modules/vue-jest',
|
|
||||||
// '.*\\.js$': '@quasar/quasar-app-extension-testing-unit-jest/node_modules/babel-jest'
|
|
||||||
},
|
|
||||||
transformIgnorePatterns: [`node_modules/(?!(${esModules}))`],
|
|
||||||
snapshotSerializers: ['jest-serializer-vue'],
|
|
||||||
};
|
|
|
@ -11,11 +11,9 @@
|
||||||
"assets/*": ["src/assets/*"],
|
"assets/*": ["src/assets/*"],
|
||||||
"boot/*": ["src/boot/*"],
|
"boot/*": ["src/boot/*"],
|
||||||
"stores/*": ["src/stores/*"],
|
"stores/*": ["src/stores/*"],
|
||||||
|
"filters/*": ["src/filters/*"],
|
||||||
"vue$": ["node_modules/vue/dist/vue.runtime.esm-bundler.js"]
|
"vue$": ["node_modules/vue/dist/vue.runtime.esm-bundler.js"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["dist", ".quasar", "node_modules"],
|
"exclude": ["dist", ".quasar", "node_modules"]
|
||||||
"vueCompilerOptions": {
|
|
||||||
"experimentalDisableTemplateSupport": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
|
@ -1,63 +1,54 @@
|
||||||
{
|
{
|
||||||
"name": "salix-front",
|
"name": "salix-front",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Salix front-end",
|
"description": "Salix frontend",
|
||||||
"productName": "Salix",
|
"productName": "Salix",
|
||||||
"author": "Verdnatura",
|
"author": "Verdnatura",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint --ext .js,.vue ./",
|
"lint": "eslint --ext .js,.vue ./",
|
||||||
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
||||||
|
"test:e2e": "cypress open",
|
||||||
|
"test:e2e:ci": "cypress run --browser chromium",
|
||||||
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
|
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
|
||||||
"test:unit": "jest --reporters=default --watchAll",
|
"test:unit": "vitest",
|
||||||
"test:unit:ci": "jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2",
|
"test:unit:ci": "vitest run"
|
||||||
"test:unit:coverage": "jest --coverage",
|
|
||||||
"serve:test:coverage": "quasar serve test/jest/coverage/lcov-report/ --port 8788",
|
|
||||||
"concurrently:dev:jest": "concurrently \"quasar dev\" \"jest --watch\"",
|
|
||||||
"test:e2e": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress open\"",
|
|
||||||
"test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.15.8",
|
"@quasar/extras": "^1.15.11",
|
||||||
"axios": "^1.2.1",
|
"axios": "^1.2.1",
|
||||||
"core-js": "^3.6.5",
|
|
||||||
"pinia": "^2.0.28",
|
"pinia": "^2.0.28",
|
||||||
"quasar": "^2.11.1",
|
"quasar": "^2.11.7",
|
||||||
"validator": "^13.7.0",
|
"validator": "^13.7.0",
|
||||||
"vue": "^3.2.45",
|
"vue": "^3.2.45",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-router": "^4.1.6"
|
"vue-router": "^4.1.6",
|
||||||
|
"vue-router-mock": "^0.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.13.14",
|
"@intlify/unplugin-vue-i18n": "^0.8.1",
|
||||||
"@intlify/vue-i18n-loader": "^4.1.0",
|
|
||||||
"@pinia/testing": "^0.0.14",
|
"@pinia/testing": "^0.0.14",
|
||||||
"@quasar/app-webpack": "^3.6.2",
|
"@quasar/app-vite": "^1.2.1",
|
||||||
"@quasar/quasar-app-extension-testing-e2e-cypress": "^4.2.2",
|
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.2.1",
|
||||||
"@quasar/quasar-app-extension-testing-unit-jest": "^3.0.0-beta.5",
|
"@vue/test-utils": "^2.0.0",
|
||||||
|
"autoprefixer": "^10.4.13",
|
||||||
|
"cypress": "^12.2.0",
|
||||||
"eslint": "^8.30.0",
|
"eslint": "^8.30.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-cypress": "^2.12.1",
|
"eslint-plugin-cypress": "^2.12.1",
|
||||||
"eslint-plugin-jest": "^27.1.7",
|
"eslint-plugin-vue": "^9.8.0",
|
||||||
"eslint-plugin-vue": "^8.7.1",
|
"postcss": "^8.4.20",
|
||||||
"eslint-webpack-plugin": "^3.2.0",
|
"prettier": "^2.8.1",
|
||||||
"jest-junit": "^13.0.0",
|
"vitest": "^0.26.3"
|
||||||
"prettier": "^2.5.1"
|
|
||||||
},
|
},
|
||||||
"browserslist": [
|
|
||||||
"last 10 Chrome versions",
|
|
||||||
"last 10 Firefox versions",
|
|
||||||
"last 4 Edge versions",
|
|
||||||
"last 7 Safari versions",
|
|
||||||
"last 8 Android versions",
|
|
||||||
"last 8 ChromeAndroid versions",
|
|
||||||
"last 8 FirefoxAndroid versions",
|
|
||||||
"last 10 iOS versions",
|
|
||||||
"last 5 Opera versions"
|
|
||||||
],
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.22.1",
|
"node": "^18 || ^16 || ^14.19",
|
||||||
"npm": ">= 6.13.4",
|
"npm": ">= 6.13.4",
|
||||||
"yarn": ">= 1.21.1"
|
"yarn": ">= 1.21.1"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"vite": "^4.0.3",
|
||||||
|
"vitest": "^0.26.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
// https://github.com/postcss/autoprefixer
|
||||||
|
require('autoprefixer')({
|
||||||
|
overrideBrowserslist: [
|
||||||
|
'last 4 Chrome versions',
|
||||||
|
'last 4 Firefox versions',
|
||||||
|
'last 4 Edge versions',
|
||||||
|
'last 4 Safari versions',
|
||||||
|
'last 4 Android versions',
|
||||||
|
'last 4 ChromeAndroid versions',
|
||||||
|
'last 4 FirefoxAndroid versions',
|
||||||
|
'last 4 iOS versions',
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// https://github.com/elchininet/postcss-rtlcss
|
||||||
|
// If you want to support RTL css, then
|
||||||
|
// 1. yarn/npm install postcss-rtlcss
|
||||||
|
// 2. optionally set quasar.config.js > framework > lang to an RTL language
|
||||||
|
// 3. uncomment the following line:
|
||||||
|
// require('postcss-rtlcss')
|
||||||
|
],
|
||||||
|
};
|
234
quasar.config.js
234
quasar.config.js
|
@ -6,26 +6,32 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Configuration for your app
|
// Configuration for your app
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||||
|
|
||||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
|
||||||
const { configure } = require('quasar/wrappers');
|
const { configure } = require('quasar/wrappers');
|
||||||
|
const VueI18nPlugin = require('@intlify/unplugin-vue-i18n/vite');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
module.exports = configure(function (ctx) {
|
module.exports = configure(function (/* ctx */) {
|
||||||
return {
|
return {
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/supporting-ts
|
eslint: {
|
||||||
supportTS: false,
|
// fix: true,
|
||||||
|
// include = [],
|
||||||
|
// exclude = [],
|
||||||
|
// rawOptions = {},
|
||||||
|
warnings: true,
|
||||||
|
errors: true,
|
||||||
|
},
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/prefetch-feature
|
// https://v2.quasar.dev/quasar-cli/prefetch-feature
|
||||||
// preFetch: true,
|
// preFetch: true,
|
||||||
|
|
||||||
// app boot file (/src/boot)
|
// app boot file (/src/boot)
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/boot-files
|
// https://v2.quasar.dev/quasar-cli/boot-files
|
||||||
boot: ['i18n', 'axios', 'pinia'],
|
boot: ['i18n', 'axios', 'vnDate'],
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||||
css: ['app.scss'],
|
css: ['app.scss'],
|
||||||
|
|
||||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||||
|
@ -43,58 +49,63 @@ module.exports = configure(function (ctx) {
|
||||||
'material-symbols-outlined',
|
'material-symbols-outlined',
|
||||||
],
|
],
|
||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-build
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||||
build: {
|
build: {
|
||||||
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
target: {
|
||||||
|
browser: ['es2022', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||||
// transpile: false,
|
node: 'node18',
|
||||||
// publicPath: '/',
|
|
||||||
|
|
||||||
// Add dependencies for transpiling with Babel (Array of string/regex)
|
|
||||||
// (from node_modules, which are by default not transpiled).
|
|
||||||
// Applies only if "transpile" is set to true.
|
|
||||||
// transpileDependencies: [],
|
|
||||||
|
|
||||||
// rtl: true, // https://quasar.dev/options/rtl-support
|
|
||||||
// preloadChunks: true,
|
|
||||||
// showProgress: false,
|
|
||||||
// gzip: true,
|
|
||||||
// analyze: true,
|
|
||||||
|
|
||||||
// Options below are automatically set depending on the env, set them if you want to override
|
|
||||||
// extractCSS: false,
|
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/handling-webpack
|
|
||||||
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
|
||||||
|
|
||||||
chainWebpack(chain) {
|
|
||||||
chain.module
|
|
||||||
.rule('i18n')
|
|
||||||
.resourceQuery(/blockType=i18n/)
|
|
||||||
.type('javascript/auto')
|
|
||||||
.use('i18n')
|
|
||||||
.loader('@intlify/vue-i18n-loader')
|
|
||||||
.end();
|
|
||||||
|
|
||||||
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]);
|
|
||||||
},
|
},
|
||||||
extendWebpack(cfg) {
|
|
||||||
cfg.resolve.alias = {
|
|
||||||
...cfg.resolve.alias, // This adds the existing alias
|
|
||||||
|
|
||||||
// Add your own alias like this
|
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
||||||
composables: path.resolve(__dirname, './src/composables'),
|
// vueRouterBase,
|
||||||
filters: path.resolve(__dirname, './src/filters'),
|
// vueDevtools,
|
||||||
|
// vueOptionsAPI: false,
|
||||||
|
|
||||||
|
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
|
||||||
|
|
||||||
|
// publicPath: '/',
|
||||||
|
// analyze: true,
|
||||||
|
// env: {},
|
||||||
|
// rawDefine: {}
|
||||||
|
// ignorePublicFolder: true,
|
||||||
|
// minify: false,
|
||||||
|
// polyfillModulePreload: true,
|
||||||
|
// distDir
|
||||||
|
|
||||||
|
extendViteConf(viteConf) {
|
||||||
|
// FIXME: Delete deprecated property polyfillModulePreload
|
||||||
|
// that is set by Quasar by default
|
||||||
|
delete viteConf.build.polyfillModulePreload;
|
||||||
|
viteConf.build.modulePreload = {
|
||||||
|
polyfill: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
// viteVuePluginOptions: {},
|
||||||
|
|
||||||
|
alias: {
|
||||||
|
composables: path.join(__dirname, './src/composables'),
|
||||||
|
filters: path.join(__dirname, './src/filters'),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-devServer
|
vitePlugins: [
|
||||||
|
[
|
||||||
|
VueI18nPlugin,
|
||||||
|
{
|
||||||
|
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
|
||||||
|
// compositionOnly: false,
|
||||||
|
|
||||||
|
// you need to set i18n resource including paths !
|
||||||
|
include: path.resolve(__dirname, './src/i18n/**'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||||
devServer: {
|
devServer: {
|
||||||
server: {
|
server: {
|
||||||
type: 'http',
|
type: 'http',
|
||||||
},
|
},
|
||||||
port: 8080,
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://0.0.0.0:3000',
|
target: 'http://0.0.0.0:3000',
|
||||||
|
@ -105,15 +116,17 @@ module.exports = configure(function (ctx) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
|
||||||
framework: {
|
framework: {
|
||||||
|
config: {
|
||||||
config: {
|
config: {
|
||||||
brand: {
|
brand: {
|
||||||
primary: 'orange',
|
primary: 'orange',
|
||||||
},
|
},
|
||||||
dark: 'auto',
|
dark: 'auto',
|
||||||
},
|
},
|
||||||
lang: 'es',
|
},
|
||||||
|
lang: 'en-GB',
|
||||||
|
|
||||||
// iconSet: 'material-icons', // Quasar icon set
|
// iconSet: 'material-icons', // Quasar icon set
|
||||||
// lang: 'en-US', // Quasar language pack
|
// lang: 'en-US', // Quasar language pack
|
||||||
|
@ -127,14 +140,34 @@ module.exports = configure(function (ctx) {
|
||||||
|
|
||||||
// Quasar plugins
|
// Quasar plugins
|
||||||
plugins: ['Notify', 'Dialog'],
|
plugins: ['Notify', 'Dialog'],
|
||||||
|
all: 'auto',
|
||||||
|
autoImportComponentCase: 'pascal',
|
||||||
},
|
},
|
||||||
|
|
||||||
// animations: 'all', // --- includes all animations
|
// animations: 'all', // --- includes all animations
|
||||||
// https://quasar.dev/options/animations
|
// https://v2.quasar.dev/options/animations
|
||||||
animations: [],
|
animations: [],
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/developing-ssr/configuring-ssr
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
|
||||||
|
// sourceFiles: {
|
||||||
|
// rootComponent: 'src/App.vue',
|
||||||
|
// router: 'src/router/index',
|
||||||
|
// store: 'src/store/index',
|
||||||
|
// registerServiceWorker: 'src-pwa/register-service-worker',
|
||||||
|
// serviceWorker: 'src-pwa/custom-service-worker',
|
||||||
|
// pwaManifestFile: 'src-pwa/manifest.json',
|
||||||
|
// electronMain: 'src-electron/electron-main',
|
||||||
|
// electronPreload: 'src-electron/electron-preload'
|
||||||
|
// },
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr
|
||||||
ssr: {
|
ssr: {
|
||||||
|
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||||
|
// will mess up SSR
|
||||||
|
|
||||||
|
// extendSSRWebserverConf (esbuildConf) {},
|
||||||
|
// extendPackageJson (json) {},
|
||||||
|
|
||||||
pwa: false,
|
pwa: false,
|
||||||
|
|
||||||
// manualStoreHydration: true,
|
// manualStoreHydration: true,
|
||||||
|
@ -143,81 +176,42 @@ module.exports = configure(function (ctx) {
|
||||||
prodPort: 3000, // The default port that the production server should use
|
prodPort: 3000, // The default port that the production server should use
|
||||||
// (gets superseded if process.env.PORT is specified at runtime)
|
// (gets superseded if process.env.PORT is specified at runtime)
|
||||||
|
|
||||||
maxAge: 1000 * 60 * 60 * 24 * 30,
|
|
||||||
// Tell browser when a file from the server should expire from cache (in ms)
|
|
||||||
|
|
||||||
chainWebpackWebserver(chain) {
|
|
||||||
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [{ extensions: ['js'] }]);
|
|
||||||
},
|
|
||||||
|
|
||||||
middlewares: [
|
middlewares: [
|
||||||
ctx.prod ? 'compression' : '',
|
|
||||||
'render', // keep this as last one
|
'render', // keep this as last one
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/developing-pwa/configuring-pwa
|
// https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa
|
||||||
pwa: {
|
pwa: {
|
||||||
workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
|
workboxMode: 'generateSW', // or 'injectManifest'
|
||||||
workboxOptions: {}, // only for GenerateSW
|
injectPwaMetaTags: true,
|
||||||
|
swFilename: 'sw.js',
|
||||||
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
|
manifestFilename: 'manifest.json',
|
||||||
// if using workbox in InjectManifest mode
|
useCredentialsForManifestTag: false,
|
||||||
|
// useFilenameHashes: true,
|
||||||
chainWebpackCustomSW(chain) {
|
// extendGenerateSWOptions (cfg) {}
|
||||||
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [{ extensions: ['js'] }]);
|
// extendInjectManifestOptions (cfg) {},
|
||||||
|
// extendManifestJson (json) {}
|
||||||
|
// extendPWACustomSWConf (esbuildConf) {}
|
||||||
},
|
},
|
||||||
|
|
||||||
manifest: {
|
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
|
||||||
name: `Salix`,
|
|
||||||
short_name: `Salix`,
|
|
||||||
description: `Salix front-end`,
|
|
||||||
display: 'standalone',
|
|
||||||
orientation: 'portrait',
|
|
||||||
background_color: '#ffffff',
|
|
||||||
theme_color: '#027be3',
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: 'icons/icon-128x128.png',
|
|
||||||
sizes: '128x128',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'icons/icon-192x192.png',
|
|
||||||
sizes: '192x192',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'icons/icon-256x256.png',
|
|
||||||
sizes: '256x256',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'icons/icon-384x384.png',
|
|
||||||
sizes: '384x384',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'icons/icon-512x512.png',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-cordova-apps/configuring-cordova
|
|
||||||
cordova: {
|
cordova: {
|
||||||
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
|
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-capacitor-apps/configuring-capacitor
|
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
|
||||||
capacitor: {
|
capacitor: {
|
||||||
hideSplashscreen: true,
|
hideSplashscreen: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-electron-apps/configuring-electron
|
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
|
||||||
electron: {
|
electron: {
|
||||||
|
// extendElectronMainConf (esbuildConf)
|
||||||
|
// extendElectronPreloadConf (esbuildConf)
|
||||||
|
|
||||||
|
inspectPort: 5858,
|
||||||
|
|
||||||
bundler: 'packager', // 'packager' or 'builder'
|
bundler: 'packager', // 'packager' or 'builder'
|
||||||
|
|
||||||
packager: {
|
packager: {
|
||||||
|
@ -234,18 +228,16 @@ module.exports = configure(function (ctx) {
|
||||||
builder: {
|
builder: {
|
||||||
// https://www.electron.build/configuration/configuration
|
// https://www.electron.build/configuration/configuration
|
||||||
|
|
||||||
appId: 'salix-front',
|
appId: 'salix-frontend',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
|
||||||
|
bex: {
|
||||||
|
contentScripts: ['my-content-script'],
|
||||||
|
|
||||||
chainWebpackMain(chain) {
|
// extendBexScriptsConf (esbuildConf) {}
|
||||||
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [{ extensions: ['js'] }]);
|
// extendBexManifestJson (json) {}
|
||||||
},
|
|
||||||
|
|
||||||
chainWebpackPreload(chain) {
|
|
||||||
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [{ extensions: ['js'] }]);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
{
|
{
|
||||||
"@quasar/testing-unit-jest": {
|
"@quasar/testing-unit-vitest": {
|
||||||
"babel": "babelrc",
|
"options": [
|
||||||
"options": ["scripts"]
|
"scripts"
|
||||||
},
|
]
|
||||||
"@quasar/testing-e2e-cypress": {
|
|
||||||
"options": ["scripts"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,5 @@
|
||||||
{
|
{
|
||||||
"unit-jest": {
|
"unit-vitest": {
|
||||||
"runnerCommand": "jest --ci"
|
"runnerCommand": "vitest run"
|
||||||
},
|
|
||||||
"e2e-cypress": {
|
|
||||||
"runnerCommand": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
|
|
||||||
},
|
|
||||||
"unit-cypress": {
|
|
||||||
"runnerCommand": "cypress run-ct"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
71
src/App.vue
71
src/App.vue
|
@ -1,16 +1,10 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import axios from 'axios';
|
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useSession } from 'src/composables/useSession';
|
|
||||||
|
|
||||||
const quasar = useQuasar();
|
const quasar = useQuasar();
|
||||||
const router = useRouter();
|
const { availableLocales, locale, fallbackLocale } = useI18n();
|
||||||
const session = useSession();
|
|
||||||
const { t, availableLocales, locale, fallbackLocale } = useI18n();
|
|
||||||
const { isLoggedIn } = session;
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let userLang = window.navigator.language;
|
let userLang = window.navigator.language;
|
||||||
|
@ -30,76 +24,19 @@ quasar.iconMapFn = (iconName) => {
|
||||||
const name = iconName.substring(3);
|
const name = iconName.substring(3);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cls: `icon-${name}`,
|
cls: `icon-${name} notranslate`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cls: 'material-symbols-outlined',
|
cls: 'material-symbols-outlined notranslate',
|
||||||
content: iconName,
|
content: iconName,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function responseError(error) {
|
|
||||||
let message = error.message;
|
|
||||||
let logOut = false;
|
|
||||||
|
|
||||||
switch (error.response?.status) {
|
|
||||||
case 401:
|
|
||||||
message = 'login.loginError';
|
|
||||||
|
|
||||||
if (isLoggedIn()) {
|
|
||||||
message = 'errors.statusUnauthorized';
|
|
||||||
logOut = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 403:
|
|
||||||
message = 'errors.statusUnauthorized';
|
|
||||||
break;
|
|
||||||
case 500:
|
|
||||||
message = 'errors.statusInternalServerError';
|
|
||||||
break;
|
|
||||||
case 502:
|
|
||||||
message = 'errors.statusBadGateway';
|
|
||||||
break;
|
|
||||||
case 504:
|
|
||||||
message = 'errors.statusGatewayTimeout';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let translatedMessage = t(message);
|
|
||||||
if (!translatedMessage) translatedMessage = message;
|
|
||||||
|
|
||||||
quasar.notify({
|
|
||||||
message: translatedMessage,
|
|
||||||
type: 'negative',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (logOut) {
|
|
||||||
session.destroy();
|
|
||||||
router.push({ path: '/login' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
axios.interceptors.response.use((response) => {
|
|
||||||
const { method } = response.config;
|
|
||||||
|
|
||||||
const isSaveRequest = method === 'patch';
|
|
||||||
if (isSaveRequest) {
|
|
||||||
quasar.notify({
|
|
||||||
message: t('globals.dataSaved'),
|
|
||||||
type: 'positive',
|
|
||||||
icon: 'check',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}, responseError);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-view />
|
<RouterView />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
|
||||||
import { createWrapper } from 'app/tests/jest/jestHelpers';
|
|
||||||
import App from '../App.vue';
|
|
||||||
import { useSession } from 'src/composables/useSession';
|
|
||||||
|
|
||||||
const mockPush = jest.fn();
|
|
||||||
const mockLoggedIn = jest.fn();
|
|
||||||
const mockDestroy = jest.fn();
|
|
||||||
const session = useSession();
|
|
||||||
|
|
||||||
jest.mock('vue-router', () => ({
|
|
||||||
useRouter: () => ({
|
|
||||||
push: mockPush,
|
|
||||||
currentRoute: { value: 'myCurrentRoute' },
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('src/composables/useSession', () => ({
|
|
||||||
useSession: () => ({
|
|
||||||
isLoggedIn: mockLoggedIn,
|
|
||||||
destroy: mockDestroy,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('App', () => {
|
|
||||||
let vm;
|
|
||||||
beforeAll(() => {
|
|
||||||
const options = {
|
|
||||||
global: {
|
|
||||||
stubs: ['router-view'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
vm = createWrapper(App, options).vm;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a login error message', async () => {
|
|
||||||
jest.spyOn(vm.quasar, 'notify');
|
|
||||||
|
|
||||||
session.isLoggedIn.mockReturnValue(false);
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
response: {
|
|
||||||
status: 401,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
|
|
||||||
expect(vm.quasar.notify).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
message: 'Invalid username or password',
|
|
||||||
type: 'negative',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an unauthorized error message', async () => {
|
|
||||||
jest.spyOn(vm.quasar, 'notify');
|
|
||||||
|
|
||||||
session.isLoggedIn.mockReturnValue(true);
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
response: {
|
|
||||||
status: 401,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
|
|
||||||
expect(vm.quasar.notify).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
message: 'Access denied',
|
|
||||||
type: 'negative',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(session.destroy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,21 +1,83 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { Notify } from 'quasar';
|
||||||
import { useSession } from 'src/composables/useSession';
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
import { Router } from 'src/router';
|
||||||
|
import { i18n } from './i18n';
|
||||||
|
|
||||||
const { getToken } = useSession();
|
const session = useSession();
|
||||||
|
const { t } = i18n.global;
|
||||||
|
|
||||||
axios.defaults.baseURL = '/api/';
|
axios.defaults.baseURL = '/api/';
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
const onRequest = (config) => {
|
||||||
function (context) {
|
const token = session.getToken();
|
||||||
const token = getToken();
|
if (token.length && config.headers) {
|
||||||
|
config.headers.Authorization = token;
|
||||||
if (token.length && context.headers) {
|
|
||||||
context.headers.Authorization = token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return config;
|
||||||
},
|
};
|
||||||
function (error) {
|
|
||||||
|
const onRequestError = (error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResponse = (response) => {
|
||||||
|
const { method } = response.config;
|
||||||
|
|
||||||
|
const isSaveRequest = method === 'patch';
|
||||||
|
if (isSaveRequest) {
|
||||||
|
Notify.create({
|
||||||
|
message: t('globals.dataSaved'),
|
||||||
|
type: 'positive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResponseError = (error) => {
|
||||||
|
let message = '';
|
||||||
|
|
||||||
|
const response = error.response;
|
||||||
|
const responseData = response && response.data;
|
||||||
|
const responseError = responseData && response.data.error;
|
||||||
|
if (responseError) {
|
||||||
|
message = responseError.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (response.status) {
|
||||||
|
case 500:
|
||||||
|
message = 'errors.statusInternalServerError';
|
||||||
|
break;
|
||||||
|
case 502:
|
||||||
|
message = 'errors.statusBadGateway';
|
||||||
|
break;
|
||||||
|
case 504:
|
||||||
|
message = 'errors.statusGatewayTimeout';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.isLoggedIn() && response.status === 401) {
|
||||||
|
session.destroy();
|
||||||
|
Router.push({ path: '/login' });
|
||||||
|
} else if(!session.isLoggedIn())
|
||||||
|
{
|
||||||
|
message = 'login.loginError';
|
||||||
|
}
|
||||||
|
|
||||||
|
Notify.create({
|
||||||
|
message: t(message),
|
||||||
|
type: 'negative',
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.interceptors.request.use(onRequest, onRequestError);
|
||||||
|
axios.interceptors.response.use(onResponse, onResponseError);
|
||||||
|
|
||||||
|
export {
|
||||||
|
onRequest,
|
||||||
|
onResponseError
|
||||||
}
|
}
|
||||||
);
|
|
|
@ -3,11 +3,13 @@ import { createI18n } from 'vue-i18n';
|
||||||
import messages from 'src/i18n';
|
import messages from 'src/i18n';
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
locale: 'en',
|
locale: navigator.language || navigator.userLanguage,
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
|
globalInjection: true,
|
||||||
messages,
|
messages,
|
||||||
|
missingWarn: false,
|
||||||
|
fallbackWarn: false,
|
||||||
legacy: false,
|
legacy: false,
|
||||||
missingWarn: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
export default boot(({ app }) => {
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { boot } from 'quasar/wrappers';
|
|
||||||
import { createPinia } from 'pinia';
|
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
|
||||||
const pinia = createPinia();
|
|
||||||
|
|
||||||
app.use(pinia);
|
|
||||||
});
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
|
||||||
|
export default boot(() => {
|
||||||
|
Date.vnUTC = () => {
|
||||||
|
const env = process.env.NODE_ENV;
|
||||||
|
if (!env || env === 'development') return new Date(Date.UTC(2001, 0, 1, 11));
|
||||||
|
|
||||||
|
return new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
Date.vnNew = () => {
|
||||||
|
return new Date(Date.vnUTC());
|
||||||
|
};
|
||||||
|
|
||||||
|
Date.vnNow = () => {
|
||||||
|
return new Date(Date.vnUTC()).getTime();
|
||||||
|
};
|
||||||
|
});
|
|
@ -1,9 +1,8 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import axios from 'axios';
|
||||||
import { onMounted, onUnmounted, computed, ref, watch } from 'vue';
|
import { onMounted, onUnmounted, computed, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import { useState } from 'src/composables/useState';
|
import { useState } from 'src/composables/useState';
|
||||||
import { useValidator } from 'src/composables/useValidator';
|
import { useValidator } from 'src/composables/useValidator';
|
||||||
import SkeletonForm from 'components/ui/SkeletonForm.vue';
|
import SkeletonForm from 'components/ui/SkeletonForm.vue';
|
||||||
|
@ -44,6 +43,7 @@ const isLoading = ref(false);
|
||||||
const hasChanges = ref(false);
|
const hasChanges = ref(false);
|
||||||
const formData = computed(() => state.get($props.model));
|
const formData = computed(() => state.get($props.model));
|
||||||
const originalData = ref();
|
const originalData = ref();
|
||||||
|
const formUrl = computed(() => $props.url);
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
const { data } = await axios.get($props.url, {
|
const { data } = await axios.get($props.url, {
|
||||||
|
@ -91,18 +91,24 @@ function filter(value, update, filterOptions) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(formUrl, async () => {
|
||||||
|
originalData.value = null;
|
||||||
|
reset();
|
||||||
|
fetch();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-banner v-if="hasChanges" class="text-white bg-warning">
|
<QBanner v-if="hasChanges" class="text-white bg-warning">
|
||||||
<q-icon name="warning" size="md" class="q-mr-md" />
|
<QIcon name="warning" size="md" class="q-mr-md" />
|
||||||
<span>{{ t('globals.changesToSave') }}</span>
|
<span>{{ t('globals.changesToSave') }}</span>
|
||||||
</q-banner>
|
</QBanner>
|
||||||
<q-form v-if="formData" @submit="save" @reset="reset" class="q-pa-md">
|
<QForm v-if="formData" @submit="save" @reset="reset" class="q-pa-md">
|
||||||
<slot name="form" :data="formData" :validate="validate" :filter="filter"></slot>
|
<slot name="form" :data="formData" :validate="validate" :filter="filter"></slot>
|
||||||
<div class="q-mt-lg">
|
<div class="q-mt-lg">
|
||||||
<slot name="actions">
|
<slot name="actions">
|
||||||
<q-btn :label="t('globals.save')" type="submit" color="primary" />
|
<QBtn :label="t('globals.save')" type="submit" color="primary" />
|
||||||
<q-btn
|
<QBtn
|
||||||
:label="t('globals.reset')"
|
:label="t('globals.reset')"
|
||||||
type="reset"
|
type="reset"
|
||||||
class="q-ml-sm"
|
class="q-ml-sm"
|
||||||
|
@ -112,7 +118,11 @@ function filter(value, update, filterOptions) {
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</QForm>
|
||||||
<skeleton-form v-if="!formData" />
|
<SkeletonForm v-if="!formData" />
|
||||||
<q-inner-loading :showing="isLoading" :label="t('globals.pleaseWait')" color="primary" />
|
<QInnerLoading
|
||||||
|
:showing="isLoading"
|
||||||
|
:label="t('globals.pleaseWait')"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { onMounted, ref, computed } from 'vue';
|
import { onMounted, ref, computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useQuasar } from 'quasar';
|
import { QSeparator, useQuasar } from 'quasar';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useNavigationStore } from 'src/stores/useNavigationStore';
|
import { useNavigationStore } from 'src/stores/useNavigationStore';
|
||||||
import { toLowerCamel } from 'src/filters';
|
import { toLowerCamel } from 'src/filters';
|
||||||
|
@ -65,7 +65,9 @@ function getRoutes() {
|
||||||
const modules = Object.assign([], navigation.getModules().value);
|
const modules = Object.assign([], navigation.getModules().value);
|
||||||
|
|
||||||
for (const item of modules) {
|
for (const item of modules) {
|
||||||
const moduleDef = routes.find((route) => toLowerCamel(route.name) === item.module);
|
const moduleDef = routes.find(
|
||||||
|
(route) => toLowerCamel(route.name) === item.module
|
||||||
|
);
|
||||||
item.children = [];
|
item.children = [];
|
||||||
|
|
||||||
if (!moduleDef) continue;
|
if (!moduleDef) continue;
|
||||||
|
@ -79,7 +81,9 @@ function getRoutes() {
|
||||||
if (props.source === 'card') {
|
if (props.source === 'card') {
|
||||||
const currentRoute = route.matched[1];
|
const currentRoute = route.matched[1];
|
||||||
const currentModule = toLowerCamel(currentRoute.name);
|
const currentModule = toLowerCamel(currentRoute.name);
|
||||||
const moduleDef = routes.find((route) => toLowerCamel(route.name) === currentModule);
|
const moduleDef = routes.find(
|
||||||
|
(route) => toLowerCamel(route.name) === currentModule
|
||||||
|
);
|
||||||
|
|
||||||
if (!moduleDef) return;
|
if (!moduleDef) return;
|
||||||
|
|
||||||
|
@ -111,16 +115,16 @@ async function togglePinned(item, event) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-list padding>
|
<QList padding>
|
||||||
<template v-if="$props.source === 'main'">
|
<template v-if="$props.source === 'main'">
|
||||||
<q-item-label header>
|
<QItemLabel header>
|
||||||
{{ t('globals.pinnedModules') }}
|
{{ t('globals.pinnedModules') }}
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
<template v-for="item in pinnedItems" :key="item.name">
|
<template v-for="item in pinnedItems" :key="item.name">
|
||||||
<template v-if="item.children">
|
<template v-if="item.children">
|
||||||
<left-menu-item-group :item="item" group="pinnedModules" class="pinned">
|
<LeftMenuItemGroup :item="item" group="pinnedModules" class="pinned">
|
||||||
<template #side>
|
<template #side>
|
||||||
<q-btn
|
<QBtn
|
||||||
v-if="item.isPinned === true"
|
v-if="item.isPinned === true"
|
||||||
@click="togglePinned(item, $event)"
|
@click="togglePinned(item, $event)"
|
||||||
icon="vn:pin_off"
|
icon="vn:pin_off"
|
||||||
|
@ -128,9 +132,11 @@ async function togglePinned(item, event) {
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
>
|
>
|
||||||
<q-tooltip>{{ t('components.leftMenu.removeFromPinned') }}</q-tooltip>
|
<QTooltip>{{
|
||||||
</q-btn>
|
t('components.leftMenu.removeFromPinned')
|
||||||
<q-btn
|
}}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QBtn
|
||||||
v-if="item.isPinned === false"
|
v-if="item.isPinned === false"
|
||||||
@click="togglePinned(item, $event)"
|
@click="togglePinned(item, $event)"
|
||||||
icon="vn:pin"
|
icon="vn:pin"
|
||||||
|
@ -138,21 +144,23 @@ async function togglePinned(item, event) {
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
>
|
>
|
||||||
<q-tooltip>{{ t('components.leftMenu.addToPinned') }}</q-tooltip>
|
<QTooltip>{{
|
||||||
</q-btn>
|
t('components.leftMenu.addToPinned')
|
||||||
|
}}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
</template>
|
</template>
|
||||||
</left-menu-item-group>
|
</LeftMenuItemGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<left-menu-item v-if="!item.children" :item="item" />
|
<LeftMenuItem v-if="!item.children" :item="item" />
|
||||||
</template>
|
</template>
|
||||||
<q-separator />
|
<QSeparator />
|
||||||
<q-expansion-item :label="t('moduleIndex.allModules')">
|
<QExpansionItem :label="t('moduleIndex.allModules')">
|
||||||
<template v-for="item in items" :key="item.name">
|
<template v-for="item in items" :key="item.name">
|
||||||
<template v-if="item.children">
|
<template v-if="item.children">
|
||||||
<left-menu-item-group :item="item" group="modules">
|
<LeftMenuItemGroup :item="item" group="modules">
|
||||||
<template #side>
|
<template #side>
|
||||||
<q-btn
|
<QBtn
|
||||||
v-if="item.isPinned === true"
|
v-if="item.isPinned === true"
|
||||||
@click="togglePinned(item, $event)"
|
@click="togglePinned(item, $event)"
|
||||||
icon="vn:pin_off"
|
icon="vn:pin_off"
|
||||||
|
@ -160,9 +168,11 @@ async function togglePinned(item, event) {
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
>
|
>
|
||||||
<q-tooltip>{{ t('components.leftMenu.removeFromPinned') }}</q-tooltip>
|
<QTooltip>
|
||||||
</q-btn>
|
{{ t('components.leftMenu.removeFromPinned') }}
|
||||||
<q-btn
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QBtn
|
||||||
v-if="item.isPinned === false"
|
v-if="item.isPinned === false"
|
||||||
@click="togglePinned(item, $event)"
|
@click="togglePinned(item, $event)"
|
||||||
icon="vn:pin"
|
icon="vn:pin"
|
||||||
|
@ -170,21 +180,23 @@ async function togglePinned(item, event) {
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
>
|
>
|
||||||
<q-tooltip>{{ t('components.leftMenu.addToPinned') }}</q-tooltip>
|
<QTooltip>
|
||||||
</q-btn>
|
{{ t('components.leftMenu.addToPinned') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
</template>
|
</template>
|
||||||
</left-menu-item-group>
|
</LeftMenuItemGroup>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</q-expansion-item>
|
</QExpansionItem>
|
||||||
<q-separator />
|
<QSeparator />
|
||||||
</template>
|
</template>
|
||||||
<template v-if="$props.source === 'card'">
|
<template v-if="$props.source === 'card'">
|
||||||
<template v-for="item in items" :key="item.name">
|
<template v-for="item in items" :key="item.name">
|
||||||
<left-menu-item v-if="!item.children" :item="item" />
|
<LeftMenuItem v-if="!item.children" :item="item" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</q-list>
|
</QList>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -14,13 +14,13 @@ const props = defineProps({
|
||||||
const item = computed(() => props.item);
|
const item = computed(() => props.item);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-item active-class="text-primary" :to="{ name: item.name }" clickable v-ripple>
|
<QItem active-class="text-primary" :to="{ name: item.name }" clickable v-ripple>
|
||||||
<q-item-section avatar v-if="item.icon">
|
<QItemSection avatar v-if="item.icon">
|
||||||
<q-icon :name="item.icon" />
|
<QIcon :name="item.icon" />
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
<q-item-section avatar v-if="!item.icon">
|
<QItemSection avatar v-if="!item.icon">
|
||||||
<q-icon name="disabled_by_default" />
|
<QIcon name="disabled_by_default" />
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
<q-item-section>{{ t(item.title) }}</q-item-section>
|
<QItemSection>{{ t(item.title) }}</QItemSection>
|
||||||
</q-item>
|
</QItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -27,7 +27,7 @@ const isOpened = computed(() => {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-expansion-item
|
<QExpansionItem
|
||||||
:group="props.group"
|
:group="props.group"
|
||||||
active-class="text-primary"
|
active-class="text-primary"
|
||||||
:label="item.title"
|
:label="item.title"
|
||||||
|
@ -36,16 +36,16 @@ const isOpened = computed(() => {
|
||||||
:default-opened="isOpened"
|
:default-opened="isOpened"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<q-item-section avatar>
|
<QItemSection avatar>
|
||||||
<q-icon :name="item.icon"></q-icon>
|
<QIcon :name="item.icon"></QIcon>
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
<q-item-section>{{ t(item.title) }}</q-item-section>
|
<QItemSection>{{ t(item.title) }}</QItemSection>
|
||||||
<q-item-section side>
|
<QItemSection side>
|
||||||
<slot name="side" :item="item" />
|
<slot name="side" :item="item" />
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
</template>
|
</template>
|
||||||
<template v-for="section in item.children" :key="section.name">
|
<template v-for="section in item.children" :key="section.name">
|
||||||
<left-menu-item :item="section" />
|
<LeftMenuItem :item="section" />
|
||||||
</template>
|
</template>
|
||||||
</q-expansion-item>
|
</QExpansionItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,68 +1,82 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useState } from 'src/composables/useState';
|
|
||||||
import { useSession } from 'src/composables/useSession';
|
import { useSession } from 'src/composables/useSession';
|
||||||
import UserPanel from 'components/UserPanel.vue';
|
import UserPanel from 'components/UserPanel.vue';
|
||||||
|
import { useState } from 'src/composables/useState';
|
||||||
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
import PinnedModules from './PinnedModules.vue';
|
import PinnedModules from './PinnedModules.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
const stateStore = useStateStore();
|
||||||
const state = useState();
|
const state = useState();
|
||||||
const user = state.getUser();
|
const user = state.getUser();
|
||||||
const token = session.getToken();
|
const token = session.getToken();
|
||||||
|
const appName = 'Lilium';
|
||||||
|
|
||||||
onMounted(() => (state.headerMounted.value = true));
|
onMounted(() => stateStore.setMounted());
|
||||||
|
|
||||||
function onToggleDrawer() {
|
|
||||||
state.drawer.value = !state.drawer.value;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-header class="bg-dark" color="white" elevated>
|
<QHeader class="bg-dark" color="white" elevated>
|
||||||
<q-toolbar class="q-py-sm q-px-md">
|
<QToolbar class="q-py-sm q-px-md">
|
||||||
<q-btn flat @click="onToggleDrawer()" round dense icon="menu">
|
<QBtn
|
||||||
<q-tooltip bottom anchor="bottom right">
|
@click="stateStore.toggleLeftDrawer()"
|
||||||
{{ t('globals.collapseMenu') }}
|
icon="menu"
|
||||||
</q-tooltip>
|
class="q-mr-sm"
|
||||||
</q-btn>
|
round
|
||||||
<router-link to="/">
|
dense
|
||||||
<q-btn flat round class="q-ml-xs" v-if="$q.screen.gt.xs">
|
flat
|
||||||
<q-avatar square size="md">
|
|
||||||
<q-img src="~/assets/logo_icon.svg" alt="Logo" />
|
|
||||||
</q-avatar>
|
|
||||||
<q-tooltip bottom>
|
|
||||||
{{ t('globals.backToDashboard') }}
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</router-link>
|
|
||||||
<q-toolbar-title shrink class="text-weight-bold">Salix</q-toolbar-title>
|
|
||||||
<q-space></q-space>
|
|
||||||
<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>
|
|
||||||
<q-btn id="pinnedModules" icon="apps" flat dense rounded>
|
|
||||||
<q-tooltip bottom>
|
|
||||||
{{ t('globals.pinnedModules') }}
|
|
||||||
</q-tooltip>
|
|
||||||
<PinnedModules />
|
|
||||||
</q-btn>
|
|
||||||
<q-btn rounded dense flat no-wrap id="user">
|
|
||||||
<q-avatar size="lg">
|
|
||||||
<q-img
|
|
||||||
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
|
|
||||||
spinner-color="white"
|
|
||||||
>
|
>
|
||||||
</q-img>
|
<QTooltip bottom anchor="bottom right">
|
||||||
</q-avatar>
|
{{ t('globals.collapseMenu') }}
|
||||||
<q-tooltip bottom>
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<RouterLink to="/">
|
||||||
|
<QBtn class="q-ml-xs" color="primary" flat round>
|
||||||
|
<QAvatar square size="md">
|
||||||
|
<QImg
|
||||||
|
src="~/assets/logo_icon.svg"
|
||||||
|
:alt="appName"
|
||||||
|
spinner-color="primary"
|
||||||
|
/>
|
||||||
|
</QAvatar>
|
||||||
|
<QTooltip bottom>
|
||||||
|
{{ t('globals.backToDashboard') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</RouterLink>
|
||||||
|
<QToolbarTitle shrink class="text-weight-bold" v-if="$q.screen.gt.sm">
|
||||||
|
{{ appName }}
|
||||||
|
<QBadge label="Beta" align="top" />
|
||||||
|
</QToolbarTitle>
|
||||||
|
<QSpace />
|
||||||
|
<div id="searchbar"></div>
|
||||||
|
<QSpace />
|
||||||
|
<div class="q-pl-sm q-gutter-sm row items-center no-wrap">
|
||||||
|
<div id="actions-prepend"></div>
|
||||||
|
<QBtn id="pinnedModules" icon="apps" flat dense rounded>
|
||||||
|
<QTooltip bottom>
|
||||||
|
{{ t('globals.pinnedModules') }}
|
||||||
|
</QTooltip>
|
||||||
|
<PinnedModules />
|
||||||
|
</QBtn>
|
||||||
|
<QBtn rounded dense flat no-wrap id="user">
|
||||||
|
<QAvatar size="lg">
|
||||||
|
<QImg
|
||||||
|
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
|
||||||
|
spinner-color="primary"
|
||||||
|
>
|
||||||
|
</QImg>
|
||||||
|
</QAvatar>
|
||||||
|
<QTooltip bottom>
|
||||||
{{ t('globals.userPanel') }}
|
{{ t('globals.userPanel') }}
|
||||||
</q-tooltip>
|
</QTooltip>
|
||||||
<UserPanel />
|
<UserPanel />
|
||||||
</q-btn>
|
</QBtn>
|
||||||
|
<div id="actions-append"></div>
|
||||||
</div>
|
</div>
|
||||||
</q-toolbar>
|
</QToolbar>
|
||||||
</q-header>
|
</QHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,205 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import axios from 'axios';
|
|
||||||
import { onMounted, ref, watch } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const $props = defineProps({
|
|
||||||
autoLoad: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: Array,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
sortBy: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
limit: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
rowsPerPage: {
|
|
||||||
type: Number,
|
|
||||||
default: 10,
|
|
||||||
},
|
|
||||||
offset: {
|
|
||||||
type: Number,
|
|
||||||
default: 500,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['onFetch', 'onPaginate']);
|
|
||||||
defineExpose({ refresh });
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if ($props.autoLoad) paginate();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => $props.data,
|
|
||||||
() => {
|
|
||||||
rows.value = $props.data;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLoading = ref(false);
|
|
||||||
const hasMoreData = ref(false);
|
|
||||||
const pagination = ref({
|
|
||||||
sortBy: $props.sortBy,
|
|
||||||
rowsPerPage: $props.rowsPerPage,
|
|
||||||
page: 1,
|
|
||||||
});
|
|
||||||
const rows = ref(null);
|
|
||||||
|
|
||||||
async function fetch() {
|
|
||||||
const { page, rowsPerPage, sortBy } = pagination.value;
|
|
||||||
|
|
||||||
if (!$props.url) return;
|
|
||||||
|
|
||||||
const filter = {
|
|
||||||
limit: rowsPerPage,
|
|
||||||
skip: rowsPerPage * (page - 1),
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(filter, $props.filter);
|
|
||||||
|
|
||||||
if ($props.where) filter.where = $props.where;
|
|
||||||
if ($props.sortBy) filter.order = $props.sortBy;
|
|
||||||
if ($props.limit) filter.limit = $props.limit;
|
|
||||||
|
|
||||||
if (sortBy) filter.order = sortBy;
|
|
||||||
|
|
||||||
const { data } = await axios.get($props.url, {
|
|
||||||
params: { filter },
|
|
||||||
});
|
|
||||||
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function paginate() {
|
|
||||||
const { page, rowsPerPage, sortBy, descending } = pagination.value;
|
|
||||||
|
|
||||||
const data = await fetch();
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
isLoading.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMoreData.value = data.length === rowsPerPage;
|
|
||||||
|
|
||||||
if (!rows.value) rows.value = [];
|
|
||||||
for (const row of data) rows.value.push(row);
|
|
||||||
|
|
||||||
pagination.value.rowsNumber = rows.value.length;
|
|
||||||
pagination.value.page = page;
|
|
||||||
pagination.value.rowsPerPage = rowsPerPage;
|
|
||||||
pagination.value.sortBy = sortBy;
|
|
||||||
pagination.value.descending = descending;
|
|
||||||
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
emit('onFetch', rows);
|
|
||||||
emit('onPaginate', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
const { rowsPerPage } = pagination.value;
|
|
||||||
|
|
||||||
const data = await fetch();
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
isLoading.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMoreData.value = data.length === rowsPerPage;
|
|
||||||
|
|
||||||
if (!rows.value) rows.value = [];
|
|
||||||
rows.value = data;
|
|
||||||
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
emit('onFetch', rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onLoad(...params) {
|
|
||||||
const done = params[1];
|
|
||||||
if (!rows.value || rows.value.length === 0 || !$props.url) return done(false);
|
|
||||||
|
|
||||||
pagination.value.page = pagination.value.page + 1;
|
|
||||||
|
|
||||||
await paginate();
|
|
||||||
|
|
||||||
const endOfPages = !hasMoreData.value;
|
|
||||||
done(endOfPages);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-infinite-scroll @load="onLoad" :offset="offset" class="column items-center">
|
|
||||||
<div v-if="rows" class="card-list q-gutter-y-md">
|
|
||||||
<slot name="body" :rows="rows"></slot>
|
|
||||||
<div v-if="!rows.length && !isLoading" class="info-row q-pa-md text-center">
|
|
||||||
<h5>
|
|
||||||
{{ t('components.smartCard.noData') }}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div v-if="isLoading" class="info-row q-pa-md text-center">
|
|
||||||
<q-spinner color="orange" size="md" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!rows" class="card-list q-gutter-y-md">
|
|
||||||
<q-card class="card" v-for="$index in $props.rowsPerPage" :key="$index">
|
|
||||||
<q-item v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
|
|
||||||
<q-item-section class="q-pa-md">
|
|
||||||
<q-skeleton type="rect" class="q-mb-md" square />
|
|
||||||
<q-skeleton type="text" square />
|
|
||||||
<q-skeleton type="text" class="q-mb-md" square />
|
|
||||||
<q-skeleton type="text" square />
|
|
||||||
<q-skeleton type="text" square />
|
|
||||||
</q-item-section>
|
|
||||||
<q-separator vertical />
|
|
||||||
<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-skeleton type="circle" class="q-mb-md" size="40px" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-item>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</q-infinite-scroll>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.card-list {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 60em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -14,15 +14,19 @@ const pinnedModules = computed(() => navigation.getPinnedModules());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-menu
|
<QMenu
|
||||||
anchor="bottom left"
|
anchor="bottom left"
|
||||||
class="row q-pa-md q-col-gutter-lg"
|
class="row q-pa-md q-col-gutter-lg"
|
||||||
max-width="350px"
|
max-width="350px"
|
||||||
max-height="400px"
|
max-height="400px"
|
||||||
v-if="pinnedModules.length"
|
|
||||||
>
|
>
|
||||||
<div v-for="item of pinnedModules" :key="item.title" class="row no-wrap q-pa-xs flex-item">
|
<template v-if="pinnedModules.length">
|
||||||
<q-btn
|
<div
|
||||||
|
v-for="item of pinnedModules"
|
||||||
|
:key="item.title"
|
||||||
|
class="row no-wrap q-pa-xs flex-item"
|
||||||
|
>
|
||||||
|
<QBtn
|
||||||
align="evenly"
|
align="evenly"
|
||||||
padding="16px"
|
padding="16px"
|
||||||
flat
|
flat
|
||||||
|
@ -36,9 +40,18 @@ const pinnedModules = computed(() => navigation.getPinnedModules());
|
||||||
<div class="text-center text-primary button-text">
|
<div class="text-center text-primary button-text">
|
||||||
{{ t(item.title) }}
|
{{ t(item.title) }}
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
</q-menu>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
class="row no-wrap q-pa-xs flex-item text-center text-grey-5"
|
||||||
|
style="min-width: 200px"
|
||||||
|
>
|
||||||
|
{{ t('globals.noPinnedModules') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</QMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -22,9 +22,16 @@ const userLocale = computed({
|
||||||
|
|
||||||
if (value === 'en') value = 'en-GB';
|
if (value === 'en') value = 'en-GB';
|
||||||
|
|
||||||
import(`quasar/lang/${value}`).then((language) => {
|
// FIXME: Dynamic imports from absolute paths are not compatible with vite:
|
||||||
Quasar.lang.set(language.default);
|
// https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
|
||||||
|
try {
|
||||||
|
const langList = import.meta.glob('../../node_modules/quasar/lang/*.mjs');
|
||||||
|
langList[`../../node_modules/quasar/lang/${value}.mjs`]().then((lang) => {
|
||||||
|
Quasar.lang.set(lang.default);
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,7 +70,7 @@ async function saveDarkMode(value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveLanguage(value) {
|
async function saveLanguage(value) {
|
||||||
const query = `/Accounts/${user.value.id}`;
|
const query = `/VnUsers/${user.value.id}`;
|
||||||
await axios.patch(query, {
|
await axios.patch(query, {
|
||||||
lang: value,
|
lang: value,
|
||||||
});
|
});
|
||||||
|
@ -77,11 +84,13 @@ function logout() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-menu anchor="bottom left">
|
<QMenu anchor="bottom left">
|
||||||
<div class="row no-wrap q-pa-md">
|
<div class="row no-wrap q-pa-md">
|
||||||
<div class="column panel">
|
<div class="column panel">
|
||||||
<div class="text-h6 q-mb-md">{{ t('components.userPanel.settings') }}</div>
|
<div class="text-h6 q-mb-md">
|
||||||
<q-toggle
|
{{ t('components.userPanel.settings') }}
|
||||||
|
</div>
|
||||||
|
<QToggle
|
||||||
v-model="userLocale"
|
v-model="userLocale"
|
||||||
@update:model-value="saveLanguage"
|
@update:model-value="saveLanguage"
|
||||||
:label="t(`globals.lang['${userLocale}']`)"
|
:label="t(`globals.lang['${userLocale}']`)"
|
||||||
|
@ -90,7 +99,7 @@ function logout() {
|
||||||
false-value="es"
|
false-value="es"
|
||||||
true-value="en"
|
true-value="en"
|
||||||
/>
|
/>
|
||||||
<q-toggle
|
<QToggle
|
||||||
v-model="darkMode"
|
v-model="darkMode"
|
||||||
@update:model-value="saveDarkMode"
|
@update:model-value="saveDarkMode"
|
||||||
:label="t(`globals.darkMode`)"
|
:label="t(`globals.darkMode`)"
|
||||||
|
@ -100,22 +109,22 @@ function logout() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator vertical inset class="q-mx-lg" />
|
<QSeparator vertical inset class="q-mx-lg" />
|
||||||
|
|
||||||
<div class="column items-center panel">
|
<div class="column items-center panel">
|
||||||
<q-avatar size="80px">
|
<QAvatar size="80px">
|
||||||
<q-img
|
<QImg
|
||||||
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
|
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
|
||||||
spinner-color="white"
|
spinner-color="white"
|
||||||
/>
|
/>
|
||||||
</q-avatar>
|
</QAvatar>
|
||||||
|
|
||||||
<div class="text-subtitle1 q-mt-md">
|
<div class="text-subtitle1 q-mt-md">
|
||||||
<strong>{{ user.nickname }}</strong>
|
<strong>{{ user.nickname }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-subtitle3 text-grey-7 q-mb-xs">@{{ user.name }}</div>
|
<div class="text-subtitle3 text-grey-7 q-mb-xs">@{{ user.name }}</div>
|
||||||
|
|
||||||
<q-btn
|
<QBtn
|
||||||
id="logout"
|
id="logout"
|
||||||
color="orange"
|
color="orange"
|
||||||
flat
|
flat
|
||||||
|
@ -127,7 +136,7 @@ function logout() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-menu>
|
</QMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
|
||||||
import { createWrapper } from 'app/tests/jest/jestHelpers';
|
|
||||||
import Leftmenu from '../LeftMenu.vue';
|
|
||||||
|
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
|
||||||
import { useNavigationStore } from 'src/stores/useNavigationStore';
|
|
||||||
|
|
||||||
const mockPush = jest.fn();
|
|
||||||
|
|
||||||
jest.mock('vue-router', () => ({
|
|
||||||
useRouter: () => ({
|
|
||||||
push: mockPush,
|
|
||||||
currentRoute: { value: 'myCurrentRoute' },
|
|
||||||
}),
|
|
||||||
useRoute: () => ({
|
|
||||||
matched: [],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('src/router/modules', () => [
|
|
||||||
{
|
|
||||||
path: '/customer',
|
|
||||||
name: 'Customer',
|
|
||||||
meta: {
|
|
||||||
title: 'customers',
|
|
||||||
icon: 'vn:client',
|
|
||||||
},
|
|
||||||
menus: {
|
|
||||||
main: ['CustomerList', 'CustomerCreate'],
|
|
||||||
card: ['CustomerBasicData'],
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'CustomerMain',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'list',
|
|
||||||
name: 'CustomerList',
|
|
||||||
meta: {
|
|
||||||
title: 'list',
|
|
||||||
icon: 'view_list',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'create',
|
|
||||||
name: 'CustomerCreate',
|
|
||||||
meta: {
|
|
||||||
title: 'createCustomer',
|
|
||||||
icon: 'vn:addperson',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
describe('Leftmenu', () => {
|
|
||||||
let vm;
|
|
||||||
let navigation;
|
|
||||||
beforeAll(async () => {
|
|
||||||
vm = createWrapper(Leftmenu, {
|
|
||||||
propsData: {
|
|
||||||
source: 'main',
|
|
||||||
},
|
|
||||||
global: {
|
|
||||||
plugins: [createTestingPinia({ stubActions: false })],
|
|
||||||
},
|
|
||||||
}).vm;
|
|
||||||
|
|
||||||
navigation = useNavigationStore();
|
|
||||||
navigation.modules = ['customer']; // I should mock to have just one module but isn´t working
|
|
||||||
navigation.fetchPinned = jest.fn().mockReturnValue(Promise.resolve(true));
|
|
||||||
navigation.getModules = jest.fn().mockReturnValue({
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
name: 'customer',
|
|
||||||
title: 'customer.pageTitles.customers',
|
|
||||||
icon: 'vn:customer',
|
|
||||||
module: 'customer',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a proper formated object with two child items', async () => {
|
|
||||||
const expectedMenuItem = [
|
|
||||||
{
|
|
||||||
name: 'CustomerList',
|
|
||||||
title: 'customer.pageTitles.list',
|
|
||||||
icon: 'view_list',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CustomerCreate',
|
|
||||||
title: 'customer.pageTitles.createCustomer',
|
|
||||||
icon: 'vn:addperson',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const firstMenuItem = vm.items[0];
|
|
||||||
expect(firstMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem));
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -3,12 +3,13 @@ import { ref } from 'vue';
|
||||||
import { useDialogPluginComponent } from 'quasar';
|
import { useDialogPluginComponent } from 'quasar';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const $props = defineProps({
|
const props = defineProps({
|
||||||
address: {
|
data: {
|
||||||
type: String,
|
type: Object,
|
||||||
default: '',
|
requied: true,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
send: {
|
promise: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
@ -19,37 +20,51 @@ defineEmits(['confirm', ...useDialogPluginComponent.emits]);
|
||||||
const { dialogRef, onDialogOK } = useDialogPluginComponent();
|
const { dialogRef, onDialogOK } = useDialogPluginComponent();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const address = ref($props.address);
|
const address = ref(props.data.address);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
async function confirm() {
|
async function confirm() {
|
||||||
isLoading.value = true;
|
const response = { address };
|
||||||
await $props.send(address.value);
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
onDialogOK();
|
if (props.promise) {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
Object.assign(response, props.data);
|
||||||
|
await props.promise(response);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDialogOK(response);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-dialog ref="dialogRef" persistent>
|
<QDialog ref="dialogRef" persistent>
|
||||||
<q-card class="q-pa-sm">
|
<QCard class="q-pa-sm">
|
||||||
<q-card-section class="row items-center q-pb-none">
|
<QCardSection class="row items-center q-pb-none">
|
||||||
<span class="text-h6 text-grey">{{ t('sendEmailNotification') }}</span>
|
<span class="text-h6 text-grey">{{ t('Send email notification') }}</span>
|
||||||
<q-space />
|
<QSpace />
|
||||||
<q-btn icon="close" flat round dense v-close-popup />
|
<QBtn icon="close" flat round dense v-close-popup />
|
||||||
</q-card-section>
|
</QCardSection>
|
||||||
<q-card-section class="row items-center">
|
<QCardSection class="row items-center">
|
||||||
{{ t('notifyAddress') }}
|
{{ t('The notification will be sent to the following address') }}
|
||||||
</q-card-section>
|
</QCardSection>
|
||||||
<q-card-section class="q-pt-none">
|
<QCardSection class="q-pt-none">
|
||||||
<q-input dense v-model="address" rounded outlined autofocus />
|
<QInput dense v-model="address" rounded outlined autofocus />
|
||||||
</q-card-section>
|
</QCardSection>
|
||||||
<q-card-actions align="right">
|
<QCardActions align="right">
|
||||||
<q-btn :label="t('globals.cancel')" color="primary" flat v-close-popup />
|
<QBtn :label="t('globals.cancel')" color="primary" flat v-close-popup />
|
||||||
<q-btn :label="t('globals.confirm')" color="primary" :loading="isLoading" @click="confirm" />
|
<QBtn
|
||||||
</q-card-actions>
|
:label="t('globals.confirm')"
|
||||||
</q-card>
|
color="primary"
|
||||||
</q-dialog>
|
:loading="isLoading"
|
||||||
|
@click="confirm"
|
||||||
|
unelevated
|
||||||
|
/>
|
||||||
|
</QCardActions>
|
||||||
|
</QCard>
|
||||||
|
</QDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -59,14 +74,7 @@ async function confirm() {
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
{
|
es:
|
||||||
"en": {
|
Send email notification: Enviar notificación por correo
|
||||||
"sendEmailNotification": "Send email notification",
|
The notification will be sent to the following address: La notificación se enviará a la siguiente dirección
|
||||||
"notifyAddress": "The notification will be sent to the following address"
|
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"sendEmailNotification": "Enviar notificación por correo",
|
|
||||||
"notifyAddress": "La notificación se enviará a la siguiente dirección"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|
|
@ -0,0 +1,261 @@
|
||||||
|
<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>
|
||||||
|
<QDialog ref="dialogRef" persistent>
|
||||||
|
<QCard class="q-pa-sm">
|
||||||
|
<QCardSection class="row items-center q-pb-none">
|
||||||
|
<span class="text-h6 text-grey">
|
||||||
|
{{ t('Send SMS') }}
|
||||||
|
</span>
|
||||||
|
<QSpace />
|
||||||
|
<QBtn icon="close" :disable="isLoading" flat round dense v-close-popup />
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection v-if="props.locale">
|
||||||
|
<QBanner class="bg-amber text-white" rounded dense>
|
||||||
|
<template #avatar>
|
||||||
|
<QIcon name="warning" />
|
||||||
|
</template>
|
||||||
|
<span
|
||||||
|
v-html="t('CustomerDefaultLanguage', { locale: t(props.locale) })"
|
||||||
|
></span>
|
||||||
|
</QBanner>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection class="q-pb-xs">
|
||||||
|
<QSelect
|
||||||
|
:label="t('Language')"
|
||||||
|
:options="languages"
|
||||||
|
v-model="locale"
|
||||||
|
@update:model-value="updateMessage()"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
:input-debounce="0"
|
||||||
|
rounded
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection class="q-pb-xs">
|
||||||
|
<QInput
|
||||||
|
:label="t('Phone')"
|
||||||
|
v-model="phone"
|
||||||
|
rounded
|
||||||
|
outlined
|
||||||
|
autofocus
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection class="q-pb-xs">
|
||||||
|
<QInput
|
||||||
|
:label="t('Subject')"
|
||||||
|
v-model="subject"
|
||||||
|
rounded
|
||||||
|
outlined
|
||||||
|
autofocus
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection class="q-mb-md" q-input>
|
||||||
|
<QInput
|
||||||
|
: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>
|
||||||
|
<QIcon
|
||||||
|
v-if="message !== ''"
|
||||||
|
name="close"
|
||||||
|
@click="message = ''"
|
||||||
|
class="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #counter>
|
||||||
|
<QChip :color="color" dense>
|
||||||
|
{{ totalLength }}/{{ maxLength }}
|
||||||
|
</QChip>
|
||||||
|
</template>
|
||||||
|
</QInput>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardActions align="right">
|
||||||
|
<QBtn
|
||||||
|
:label="t('globals.cancel')"
|
||||||
|
color="primary"
|
||||||
|
:disable="isLoading"
|
||||||
|
flat
|
||||||
|
v-close-popup
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
:label="t('globals.confirm')"
|
||||||
|
@click="send()"
|
||||||
|
:loading="isLoading"
|
||||||
|
color="primary"
|
||||||
|
unelevated
|
||||||
|
/>
|
||||||
|
</QCardActions>
|
||||||
|
</QCard>
|
||||||
|
</QDialog>
|
||||||
|
</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
|
||||||
|
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 }: { changes }'
|
||||||
|
en: English
|
||||||
|
es: Spanish
|
||||||
|
fr: French
|
||||||
|
pt: Portuguese
|
||||||
|
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
|
||||||
|
pt: Portugué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 }: { changes }'
|
||||||
|
en: Anglais
|
||||||
|
es: Espagnol
|
||||||
|
fr: Français
|
||||||
|
pt: Portugais
|
||||||
|
pt:
|
||||||
|
Send SMS: Enviar SMS
|
||||||
|
CustomerDefaultLanguage: Este cliente utiliza o <strong>{locale}</strong> como seu idioma padrão
|
||||||
|
Language: Linguagem
|
||||||
|
Phone: Móvel
|
||||||
|
Subject: Assunto
|
||||||
|
Message: Mensagem
|
||||||
|
templates:
|
||||||
|
pendingPayment: 'Seu pedido está pendente de pagamento.
|
||||||
|
Por favor, acesse o site e faça o pagamento com cartão. Muito obrigado.'
|
||||||
|
minAmount: 'É necessário um valor mínimo de 50€ (sem IVA) em seu pedido
|
||||||
|
{ orderId } do dia { shipped } para recebê-lo sem custos de envio adicionais.'
|
||||||
|
orderChanges: 'Pedido { orderId } dia { shipped }: { changes }'
|
||||||
|
en: Inglês
|
||||||
|
es: Espanhol
|
||||||
|
fr: Francês
|
||||||
|
pt: Português
|
||||||
|
</i18n>
|
|
@ -0,0 +1,139 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { toCurrency } from 'filters/index';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
quantity: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
discount: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
mana: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
promise: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['onUpdate']);
|
||||||
|
|
||||||
|
const discount = ref(0);
|
||||||
|
let canceller;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.discount) {
|
||||||
|
discount.value = props.discount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = computed(() => {
|
||||||
|
const amount = props.price * props.quantity;
|
||||||
|
const appliedDiscount = (discount.value * amount) / 100;
|
||||||
|
|
||||||
|
return amount - appliedDiscount;
|
||||||
|
});
|
||||||
|
|
||||||
|
const newAmount = computed(() => `${t('New amount')}: ${toCurrency(total.value)}`);
|
||||||
|
|
||||||
|
const isLoading = ref(false);
|
||||||
|
async function save({ set }) {
|
||||||
|
isLoading.value = true;
|
||||||
|
const response = {
|
||||||
|
...props.data,
|
||||||
|
...{
|
||||||
|
discount: parseInt(discount.value),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (props.promise) {
|
||||||
|
canceller = new AbortController();
|
||||||
|
Object.assign(response, { canceller });
|
||||||
|
|
||||||
|
await props.promise(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set) set();
|
||||||
|
emit('onUpdate', response);
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel({ cancel }) {
|
||||||
|
if (canceller) {
|
||||||
|
canceller.abort();
|
||||||
|
canceller = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
discount.value = props.discount;
|
||||||
|
if (isLoading.value === true) isLoading.value = false;
|
||||||
|
if (cancel) cancel();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<QPopupEdit
|
||||||
|
v-model="discount"
|
||||||
|
v-slot="scope"
|
||||||
|
:title="t('Update discount')"
|
||||||
|
@cancel="cancel"
|
||||||
|
>
|
||||||
|
<QBanner class="bg-primary text-center q-mb-md" rounded dense>
|
||||||
|
{{ t('Mana') }} {{ toCurrency(props.mana) }}
|
||||||
|
</QBanner>
|
||||||
|
<QInput
|
||||||
|
v-model="scope.value"
|
||||||
|
type="number"
|
||||||
|
dense
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="save(scope)"
|
||||||
|
@update:model-value="discount = scope.value"
|
||||||
|
@focus="($event) => $event.target.select()"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
:disable="isLoading"
|
||||||
|
:hint="newAmount"
|
||||||
|
:rules="[
|
||||||
|
(value) => value.length > 0 || t('Enter a value'),
|
||||||
|
(value) => (value >= 0 && value <= 100) || t('Invalid discount amount'),
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<QCardActions class="q-px-none q-mt-sm" align="right">
|
||||||
|
<QBtn :label="t('Cancel')" color="primary" flat @click="cancel(scope)" />
|
||||||
|
<QBtn
|
||||||
|
:label="t('Update')"
|
||||||
|
color="primary"
|
||||||
|
:loading="isLoading"
|
||||||
|
unelevated
|
||||||
|
@click="save(scope)"
|
||||||
|
/>
|
||||||
|
</QCardActions>
|
||||||
|
</QPopupEdit>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
New amount: Nuevo importe
|
||||||
|
Update discount: Actualizar descuento
|
||||||
|
Mana: Maná
|
||||||
|
Enter a value: Introduce un valor
|
||||||
|
Invalid discount amount: Cantidad de descuento incorrecta
|
||||||
|
Cancel: Cancelar
|
||||||
|
Update: Actualizar
|
||||||
|
</i18n>
|
|
@ -1,81 +1,110 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useSlots } from 'vue';
|
import { onMounted, useSlots, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import axios from 'axios';
|
||||||
|
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
onMounted(() => fetch());
|
||||||
|
|
||||||
|
const emit = defineEmits(['onFetch']);
|
||||||
|
|
||||||
|
const entity = ref();
|
||||||
|
async function fetch() {
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
entity.value = null;
|
||||||
|
await fetch();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="descriptor">
|
<div class="descriptor">
|
||||||
|
<template v-if="entity">
|
||||||
<div class="header bg-primary q-pa-sm">
|
<div class="header bg-primary q-pa-sm">
|
||||||
<router-link :to="{ name: `${module}List` }">
|
<RouterLink :to="{ name: `${module}List` }">
|
||||||
<q-btn round flat dense size="md" icon="view_list" color="white">
|
<QBtn round flat dense size="md" icon="view_list" color="white">
|
||||||
<q-tooltip>{{ t('components.cardDescriptor.mainList') }}</q-tooltip>
|
<QTooltip>
|
||||||
</q-btn>
|
{{ t('components.cardDescriptor.mainList') }}
|
||||||
</router-link>
|
</QTooltip>
|
||||||
<router-link :to="{ name: `${module}Summary`, params: { id: data.id } }">
|
</QBtn>
|
||||||
<q-btn round flat dense size="md" icon="launch" color="white">
|
</RouterLink>
|
||||||
<q-tooltip>{{ t('components.cardDescriptor.summary') }}</q-tooltip>
|
<RouterLink :to="{ name: `${module}Summary`, params: { id: entity.id } }">
|
||||||
</q-btn>
|
<QBtn round flat dense size="md" icon="launch" color="white">
|
||||||
</router-link>
|
<QTooltip>
|
||||||
|
{{ t('components.cardDescriptor.summary') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
<q-btn v-if="slots.menu" size="md" icon="more_vert" color="white" round flat dense>
|
<QBtn
|
||||||
<q-tooltip>{{ t('components.cardDescriptor.moreOptions') }}</q-tooltip>
|
v-if="slots.menu"
|
||||||
<q-menu>
|
size="md"
|
||||||
<q-list>
|
icon="more_vert"
|
||||||
<slot name="menu" />
|
color="white"
|
||||||
</q-list>
|
round
|
||||||
</q-menu>
|
flat
|
||||||
</q-btn>
|
dense
|
||||||
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('components.cardDescriptor.moreOptions') }}
|
||||||
|
</QTooltip>
|
||||||
|
<QMenu>
|
||||||
|
<QList>
|
||||||
|
<slot name="menu" :entity="entity" />
|
||||||
|
</QList>
|
||||||
|
</QMenu>
|
||||||
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="$props.data" class="body q-py-sm">
|
|
||||||
<slot name="before" />
|
<slot name="before" />
|
||||||
<q-list>
|
<div class="body q-py-sm">
|
||||||
<q-item-label header class="ellipsis text-h5" :lines="1">
|
<QList dense>
|
||||||
{{ $props.description }}
|
<QItemLabel header class="ellipsis text-h5" :lines="1">
|
||||||
<q-tooltip>{{ $props.description }}</q-tooltip>
|
<slot name="description" :entity="entity">
|
||||||
</q-item-label>
|
<span>
|
||||||
<q-item dense>
|
{{ entity.name }}
|
||||||
<q-item-label class="text-subtitle2" caption>#{{ data.id }}</q-item-label>
|
<QTooltip>{{ entity.name }}</QTooltip>
|
||||||
</q-item>
|
</span>
|
||||||
</q-list>
|
</slot>
|
||||||
<slot name="body" />
|
</QItemLabel>
|
||||||
|
<QItem dense>
|
||||||
|
<QItemLabel class="text-subtitle2" caption>
|
||||||
|
#{{ entity.id }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
<slot name="body" :entity="entity" />
|
||||||
|
</div>
|
||||||
<slot name="after" />
|
<slot name="after" />
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<!-- Skeleton -->
|
<!-- Skeleton -->
|
||||||
<div id="descriptor-skeleton" v-if="!$props.data">
|
<SkeletonDescriptor v-if="!entity" />
|
||||||
<div class="col q-pl-sm q-pa-sm">
|
|
||||||
<q-skeleton type="text" square height="45px" />
|
|
||||||
<q-skeleton type="text" square height="18px" />
|
|
||||||
<q-skeleton type="text" square height="18px" />
|
|
||||||
<q-skeleton type="text" square height="18px" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-card-actions>
|
|
||||||
<q-skeleton size="40px" />
|
|
||||||
<q-skeleton size="40px" />
|
|
||||||
<q-skeleton size="40px" />
|
|
||||||
<q-skeleton size="40px" />
|
|
||||||
<q-skeleton size="40px" />
|
|
||||||
</q-card-actions>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
|
||||||
|
onMounted(() => fetch());
|
||||||
|
|
||||||
|
const entity = ref();
|
||||||
|
const props = defineProps({
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['onFetch']);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
entity,
|
||||||
|
fetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
if (props.filter) params.filter = props.filter;
|
||||||
|
|
||||||
|
const { data } = await axios.get(props.url, { params });
|
||||||
|
entity.value = data;
|
||||||
|
|
||||||
|
emit('onFetch', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(props, async () => {
|
||||||
|
entity.value = null;
|
||||||
|
fetch();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="summary container">
|
||||||
|
<QCard>
|
||||||
|
<SkeletonSummary v-if="!entity" />
|
||||||
|
<template v-if="entity">
|
||||||
|
<div class="header bg-primary q-pa-sm q-mb-md">
|
||||||
|
<slot name="header" :entity="entity">
|
||||||
|
{{ entity.id }} - {{ entity.name }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="body q-pa-md q-mb-md">
|
||||||
|
<slot name="body" :entity="entity" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</QCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.summary.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
.q-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.negative {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.q-list {
|
||||||
|
.q-item__label--header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body > .q-card__section.row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& > .col {
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
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>
|
|
@ -1,19 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="descriptor-skeleton">
|
<div id="descriptor-skeleton">
|
||||||
<div class="col q-pl-sm q-pa-sm">
|
<div class="col q-pl-sm q-pa-sm">
|
||||||
<q-skeleton type="text" square height="45px" />
|
<QSkeleton type="text" square height="45px" />
|
||||||
<q-skeleton type="text" square height="18px" />
|
<QSkeleton type="text" square height="18px" />
|
||||||
<q-skeleton type="text" square height="18px" />
|
<QSkeleton type="text" square height="18px" />
|
||||||
<q-skeleton type="text" square height="18px" />
|
<QSkeleton type="text" square height="18px" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-card-actions>
|
<QCardActions>
|
||||||
<q-skeleton size="40px" />
|
<QSkeleton size="40px" />
|
||||||
<q-skeleton size="40px" />
|
<QSkeleton size="40px" />
|
||||||
<q-skeleton size="40px" />
|
<QSkeleton size="40px" />
|
||||||
<q-skeleton size="40px" />
|
<QSkeleton size="40px" />
|
||||||
<q-skeleton size="40px" />
|
<QSkeleton size="40px" />
|
||||||
</q-card-actions>
|
</QCardActions>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -2,31 +2,31 @@
|
||||||
<div class="q-pa-md">
|
<div class="q-pa-md">
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="QInput" square />
|
<QSkeleton type="QInput" square />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="QInput" square />
|
<QSkeleton type="QInput" square />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="QInput" square />
|
<QSkeleton type="QInput" square />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="QInput" square />
|
<QSkeleton type="QInput" square />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="QInput" square />
|
<QSkeleton type="QInput" square />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="QInput" square />
|
<QSkeleton type="QInput" square />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-gutter-md">
|
<div class="row q-gutter-md">
|
||||||
<q-skeleton type="QBtn" />
|
<QSkeleton type="QBtn" />
|
||||||
<q-skeleton type="QBtn" />
|
<QSkeleton type="QBtn" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,47 +1,47 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="header bg-primary q-pa-sm q-mb-md">
|
<div class="header bg-primary q-pa-sm q-mb-md">
|
||||||
<q-skeleton type="rect" square />
|
<QSkeleton type="rect" square />
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-pa-md q-col-gutter-md q-mb-md">
|
<div class="row q-pa-md q-col-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="rect" class="q-mb-md" square />
|
<QSkeleton type="rect" class="q-mb-md" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="rect" class="q-mb-md" square />
|
<QSkeleton type="rect" class="q-mb-md" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="rect" class="q-mb-md" square />
|
<QSkeleton type="rect" class="q-mb-md" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="rect" class="q-mb-md" square />
|
<QSkeleton type="rect" class="q-mb-md" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-skeleton type="rect" class="q-mb-md" square />
|
<QSkeleton type="rect" class="q-mb-md" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
<q-skeleton type="text" square />
|
<QSkeleton type="text" square />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -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>
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useDialogPluginComponent } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
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 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>
|
||||||
|
<QDialog ref="dialogRef" persistent>
|
||||||
|
<QCard class="q-pa-sm">
|
||||||
|
<QCardSection class="row items-center q-pb-none">
|
||||||
|
<QAvatar
|
||||||
|
:icon="icon"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
size="xl"
|
||||||
|
v-if="icon"
|
||||||
|
/>
|
||||||
|
<span class="text-h6 text-grey">{{ title }}</span>
|
||||||
|
<QSpace />
|
||||||
|
<QBtn icon="close" :disable="isLoading" flat round dense v-close-popup />
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection class="row items-center">
|
||||||
|
<span v-html="message"></span>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardActions align="right">
|
||||||
|
<QBtn
|
||||||
|
:label="t('globals.cancel')"
|
||||||
|
color="primary"
|
||||||
|
:disable="isLoading"
|
||||||
|
flat
|
||||||
|
v-close-popup
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
:label="t('globals.confirm')"
|
||||||
|
color="primary"
|
||||||
|
:loading="isLoading"
|
||||||
|
@click="confirm()"
|
||||||
|
unelevated
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</QCardActions>
|
||||||
|
</QCard>
|
||||||
|
</QDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.q-card {
|
||||||
|
min-width: 350px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
Confirm: Confirmar
|
||||||
|
Are you sure you want to continue?: ¿Seguro que quieres continuar?
|
||||||
|
</i18n>
|
|
@ -0,0 +1,203 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useArrayData } from 'composables/useArrayData';
|
||||||
|
import toDate from 'filters/toDate';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const props = defineProps({
|
||||||
|
dataKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
searchButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh', 'clear']);
|
||||||
|
|
||||||
|
const arrayData = useArrayData(props.dataKey);
|
||||||
|
const store = arrayData.store;
|
||||||
|
const userParams = ref({});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.params) userParams.value = props.params;
|
||||||
|
const params = store.userParams;
|
||||||
|
if (Object.keys(params).length > 0) {
|
||||||
|
userParams.value = Object.assign({}, params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoading = ref(false);
|
||||||
|
async function search() {
|
||||||
|
const params = userParams.value;
|
||||||
|
for (const param in params) {
|
||||||
|
if (params[param] === '' || params[param] === null) {
|
||||||
|
delete userParams.value[param];
|
||||||
|
delete store.userParams[param];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
await arrayData.addFilter({ params });
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reload() {
|
||||||
|
isLoading.value = true;
|
||||||
|
await arrayData.fetch({ append: false });
|
||||||
|
isLoading.value = false;
|
||||||
|
emit('refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearFilters() {
|
||||||
|
userParams.value = {};
|
||||||
|
isLoading.value = true;
|
||||||
|
await arrayData.applyFilter({ params: {} });
|
||||||
|
isLoading.value = false;
|
||||||
|
|
||||||
|
emit('clear');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = computed(() => {
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
for (const param in store.userParams) {
|
||||||
|
params.push({
|
||||||
|
label: param,
|
||||||
|
value: store.userParams[param],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function remove(key) {
|
||||||
|
delete userParams.value[key];
|
||||||
|
delete store.userParams[key];
|
||||||
|
await search();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatValue(value) {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value ? t('Yes') : t('No');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(value) && !isNaN(Date.parse(value))) {
|
||||||
|
return toDate(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `"${value}"`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<QForm @submit="search">
|
||||||
|
<QList dense>
|
||||||
|
<QItem class="q-mt-xs">
|
||||||
|
<QItemSection top>
|
||||||
|
<QItemLabel header lines="1" class="text-uppercase q-py-xs q-px-none">
|
||||||
|
{{ t('Applied filters') }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection top side>
|
||||||
|
<div class="q-gutter-xs">
|
||||||
|
<QBtn
|
||||||
|
@click="clearFilters"
|
||||||
|
icon="filter_list_off"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
padding="none"
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('Remove filters') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QBtn
|
||||||
|
@click="reload"
|
||||||
|
icon="refresh"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
padding="none"
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('Refresh') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</div>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<div
|
||||||
|
v-if="tags.length === 0"
|
||||||
|
class="text-grey font-xs text-center full-width"
|
||||||
|
>
|
||||||
|
{{ t(`No filters applied`) }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<QChip
|
||||||
|
v-for="chip of tags"
|
||||||
|
:key="chip.label"
|
||||||
|
@remove="remove(chip.label)"
|
||||||
|
icon="label"
|
||||||
|
color="primary"
|
||||||
|
class="text-dark"
|
||||||
|
size="sm"
|
||||||
|
removable
|
||||||
|
>
|
||||||
|
<slot name="tags" :tag="chip" :format-fn="formatValue">
|
||||||
|
<div class="q-gutter-x-xs">
|
||||||
|
<strong>{{ chip.label }}:</strong>
|
||||||
|
<span>"{{ chip.value }}"</span>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</QChip>
|
||||||
|
</div>
|
||||||
|
</QItem>
|
||||||
|
<QSeparator />
|
||||||
|
<template v-if="props.searchButton">
|
||||||
|
<QItem>
|
||||||
|
<QItemSection class="q-py-sm">
|
||||||
|
<QBtn
|
||||||
|
:label="t('Search')"
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
class="full-width"
|
||||||
|
icon="search"
|
||||||
|
unelevated
|
||||||
|
rounded
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QSeparator />
|
||||||
|
</template>
|
||||||
|
</QList>
|
||||||
|
<slot name="body" :params="userParams" :search-fn="search"></slot>
|
||||||
|
</QForm>
|
||||||
|
<QInnerLoading
|
||||||
|
:showing="isLoading"
|
||||||
|
:label="t('globals.pleaseWait')"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
No filters applied: No se han aplicado filtros
|
||||||
|
Applied filters: Filtros aplicados
|
||||||
|
Remove filters: Eliminar filtros
|
||||||
|
Refresh: Refrescar
|
||||||
|
Search: Buscar
|
||||||
|
Yes: Si
|
||||||
|
No: No
|
||||||
|
</i18n>
|
|
@ -0,0 +1,189 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useArrayData } from 'composables/useArrayData';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dataKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
autoLoad: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
userParams: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
type: Number,
|
||||||
|
default: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['onFetch', 'onPaginate']);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const pagination = ref({
|
||||||
|
sortBy: props.order,
|
||||||
|
rowsPerPage: props.limit,
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrayData = useArrayData(props.dataKey, {
|
||||||
|
url: props.url,
|
||||||
|
filter: props.filter,
|
||||||
|
where: props.where,
|
||||||
|
limit: props.limit,
|
||||||
|
order: props.order,
|
||||||
|
userParams: props.userParams,
|
||||||
|
});
|
||||||
|
const store = arrayData.store;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.autoLoad) fetch();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
store.data = props.data;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
await arrayData.fetch({ append: false });
|
||||||
|
if (!arrayData.hasMoreData.value) {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('onFetch', store.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function paginate() {
|
||||||
|
const { page, rowsPerPage, sortBy, descending } = pagination.value;
|
||||||
|
|
||||||
|
if (!props.url) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
await arrayData.loadMore();
|
||||||
|
|
||||||
|
if (!arrayData.hasMoreData.value) {
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pagination.value.rowsNumber = store.data.length;
|
||||||
|
pagination.value.page = page;
|
||||||
|
pagination.value.rowsPerPage = rowsPerPage;
|
||||||
|
pagination.value.sortBy = sortBy;
|
||||||
|
pagination.value.descending = descending;
|
||||||
|
|
||||||
|
isLoading.value = false;
|
||||||
|
|
||||||
|
emit('onFetch', store.data);
|
||||||
|
emit('onPaginate');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onLoad(...params) {
|
||||||
|
if (!store.data) return;
|
||||||
|
|
||||||
|
const done = params[1];
|
||||||
|
if (store.data.length === 0 || !props.url) return done(false);
|
||||||
|
|
||||||
|
pagination.value.page = pagination.value.page + 1;
|
||||||
|
|
||||||
|
await paginate();
|
||||||
|
|
||||||
|
const endOfPages = !arrayData.hasMoreData.value;
|
||||||
|
done(endOfPages);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="!props.autoLoad && !store.data && !isLoading"
|
||||||
|
class="info-row q-pa-md text-center"
|
||||||
|
>
|
||||||
|
<h5>
|
||||||
|
{{ t('No data to display') }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="store.data && store.data.length === 0 && !isLoading"
|
||||||
|
class="info-row q-pa-md text-center"
|
||||||
|
>
|
||||||
|
<h5>
|
||||||
|
{{ t('No results found') }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div v-if="props.autoLoad && !store.data" class="card-list q-gutter-y-md">
|
||||||
|
<QCard class="card" v-for="$index in $props.limit" :key="$index">
|
||||||
|
<QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
|
||||||
|
<QItemSection class="q-pa-md">
|
||||||
|
<QSkeleton type="rect" class="q-mb-md" square />
|
||||||
|
<QSkeleton type="text" square />
|
||||||
|
<QSkeleton type="text" class="q-mb-md" square />
|
||||||
|
<QSkeleton type="text" square />
|
||||||
|
<QSkeleton type="text" square />
|
||||||
|
</QItemSection>
|
||||||
|
<QSeparator vertical />
|
||||||
|
<QCardActions vertical class="justify-between">
|
||||||
|
<QSkeleton type="circle" class="q-mb-md" size="40px" />
|
||||||
|
<QSkeleton type="circle" class="q-mb-md" size="40px" />
|
||||||
|
<QSkeleton type="circle" class="q-mb-md" size="40px" />
|
||||||
|
</QCardActions>
|
||||||
|
</QItem>
|
||||||
|
</QCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<QInfiniteScroll 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">
|
||||||
|
<QSpinner color="orange" size="md" />
|
||||||
|
</div>
|
||||||
|
</QInfiniteScroll>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.info-row {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
No data to display: Sin datos que mostrar
|
||||||
|
No results found: No se han encontrado resultados
|
||||||
|
</i18n>
|
|
@ -0,0 +1,141 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { useArrayData } from 'composables/useArrayData';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dataKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'Search',
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
redirect: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
userParams: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const arrayData = useArrayData(props.dataKey, { ...props });
|
||||||
|
const store = arrayData.store;
|
||||||
|
const searchText = ref('');
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const params = store.userParams;
|
||||||
|
if (params && params.search) {
|
||||||
|
searchText.value = params.search;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function search() {
|
||||||
|
await arrayData.applyFilter({
|
||||||
|
params: {
|
||||||
|
search: searchText.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!props.redirect) return;
|
||||||
|
|
||||||
|
const rows = store.data;
|
||||||
|
const module = route.matched[1];
|
||||||
|
if (rows.length === 1) {
|
||||||
|
const [firstRow] = rows;
|
||||||
|
await router.push({ path: `/${module.name}/${firstRow.id}` });
|
||||||
|
} else if (route.matched.length > 3) {
|
||||||
|
await router.push({ path: `/${module.name}` });
|
||||||
|
arrayData.updateStateParams();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<QForm @submit="search">
|
||||||
|
<QInput
|
||||||
|
id="searchbar"
|
||||||
|
v-model="searchText"
|
||||||
|
:placeholder="props.label"
|
||||||
|
dense
|
||||||
|
standout
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="search" />
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<QIcon
|
||||||
|
v-if="searchText !== ''"
|
||||||
|
name="close"
|
||||||
|
@click="searchText = ''"
|
||||||
|
class="cursor-pointer"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QIcon v-if="props.info" name="info" class="cursor-info">
|
||||||
|
<QTooltip>{{ props.info }}</QTooltip>
|
||||||
|
</QIcon>
|
||||||
|
</template>
|
||||||
|
</QInput>
|
||||||
|
</QForm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.q-field {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $breakpoint-sm-max) {
|
||||||
|
.q-field {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-field {
|
||||||
|
transition: width 0.36s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.cursor-info {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body--light #searchbar {
|
||||||
|
.q-field--standout.q-field--highlighted .q-field__control {
|
||||||
|
background-color: $grey-7;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,19 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export async function getUrl(route, appName = 'salix') {
|
||||||
|
let url;
|
||||||
|
const env = process.env.NODE_ENV === 'development' ? 'dev' : process.env.NODE_ENV;
|
||||||
|
const filter = {
|
||||||
|
where: {and: [
|
||||||
|
{appName: appName},
|
||||||
|
{environment: env}
|
||||||
|
]}
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.get('Urls/findOne', {params: {filter}})
|
||||||
|
.then(res => {
|
||||||
|
url = res.data.url + route;
|
||||||
|
});
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
import { onMounted, ref, computed } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useArrayDataStore } from 'stores/useArrayDataStore';
|
||||||
|
|
||||||
|
const arrayDataStore = useArrayDataStore();
|
||||||
|
|
||||||
|
export function useArrayData(key, userOptions) {
|
||||||
|
if (!key) throw new Error('ArrayData: A key is required to use this composable');
|
||||||
|
|
||||||
|
if (!arrayDataStore.get(key)) {
|
||||||
|
arrayDataStore.set(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = arrayDataStore.get(key);
|
||||||
|
const hasMoreData = ref(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
let canceller = null;
|
||||||
|
|
||||||
|
const page = ref(1);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setOptions();
|
||||||
|
|
||||||
|
const query = route.query;
|
||||||
|
if (query.params) {
|
||||||
|
store.userParams = JSON.parse(query.params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setOptions() {
|
||||||
|
const allowedOptions = [
|
||||||
|
'url',
|
||||||
|
'filter',
|
||||||
|
'where',
|
||||||
|
'order',
|
||||||
|
'limit',
|
||||||
|
'skip',
|
||||||
|
'userParams',
|
||||||
|
'userFilter'
|
||||||
|
];
|
||||||
|
if (typeof userOptions === 'object') {
|
||||||
|
for (const option in userOptions) {
|
||||||
|
const isEmpty = userOptions[option] == null || userOptions[option] == ''
|
||||||
|
if (isEmpty || !allowedOptions.includes(option)) continue;
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(store, option)) {
|
||||||
|
store[option] = userOptions[option];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetch({ append = false }) {
|
||||||
|
if (!store.url) return;
|
||||||
|
|
||||||
|
cancelRequest();
|
||||||
|
canceller = new AbortController();
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
order: store.order,
|
||||||
|
limit: store.limit,
|
||||||
|
skip: store.skip,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(filter, store.userFilter);
|
||||||
|
Object.assign(store.filter, filter);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
filter: JSON.stringify(store.filter),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(params, store.userParams);
|
||||||
|
|
||||||
|
store.isLoading = true
|
||||||
|
const response = await axios.get(store.url, {
|
||||||
|
signal: canceller.signal,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { limit } = filter;
|
||||||
|
|
||||||
|
hasMoreData.value = response.data.length === limit;
|
||||||
|
|
||||||
|
if (append === true) {
|
||||||
|
if (!store.data) store.data = [];
|
||||||
|
for (const row of response.data) store.data.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (append === false) {
|
||||||
|
store.data = response.data;
|
||||||
|
|
||||||
|
updateStateParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
store.isLoading = false
|
||||||
|
|
||||||
|
canceller = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy() {
|
||||||
|
if (arrayDataStore.get(key)) {
|
||||||
|
arrayDataStore.clear(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelRequest() {
|
||||||
|
if (canceller) {
|
||||||
|
canceller.abort();
|
||||||
|
canceller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyFilter({ filter, params }) {
|
||||||
|
if (filter) store.userFilter = filter;
|
||||||
|
if (params) store.userParams = Object.assign({}, params);
|
||||||
|
|
||||||
|
await fetch({ append: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addFilter({ filter, params }) {
|
||||||
|
if (filter) store.userFilter = Object.assign(store.userFilter, filter);
|
||||||
|
if (params) store.userParams = Object.assign(store.userParams, params);
|
||||||
|
|
||||||
|
await fetch({ append: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMore() {
|
||||||
|
if (!hasMoreData.value) return;
|
||||||
|
|
||||||
|
store.skip = store.limit * page.value;
|
||||||
|
page.value += 1;
|
||||||
|
|
||||||
|
await fetch({ append: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
await fetch({ append: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStateParams() {
|
||||||
|
const query = {};
|
||||||
|
if (store.order) query.order = store.order;
|
||||||
|
if (store.limit) query.limit = store.limit;
|
||||||
|
if (store.skip) query.skip = store.skip;
|
||||||
|
if (store.userParams && Object.keys(store.userParams).length !== 0)
|
||||||
|
query.params = JSON.stringify(store.userParams);
|
||||||
|
|
||||||
|
router.replace({
|
||||||
|
path: route.path,
|
||||||
|
query: query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalRows = computed(() => store.data && store.data.length || 0);
|
||||||
|
const isLoading = computed(() => store.isLoading || false)
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetch,
|
||||||
|
applyFilter,
|
||||||
|
addFilter,
|
||||||
|
refresh,
|
||||||
|
destroy,
|
||||||
|
loadMore,
|
||||||
|
store,
|
||||||
|
hasMoreData,
|
||||||
|
totalRows,
|
||||||
|
updateStateParams,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ export function useRole() {
|
||||||
const state = useState();
|
const state = useState();
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
const { data } = await axios.get('Accounts/acl');
|
const { data } = await axios.get('VnUsers/acl');
|
||||||
const roles = data.roles.map((userRoles) => userRoles.role.name);
|
const roles = data.roles.map((userRoles) => userRoles.role.name);
|
||||||
|
|
||||||
const userData = {
|
const userData = {
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
import * as validator from 'validator';
|
|
||||||
|
|
||||||
export const validators = {
|
|
||||||
presence: ($translate, value) => {
|
|
||||||
if (validator.isEmpty(value ? String(value) : ''))
|
|
||||||
throw new Error(_($translate, `Value can't be empty`));
|
|
||||||
},
|
|
||||||
absence: ($translate, value) => {
|
|
||||||
if (!validator.isEmpty(value))
|
|
||||||
throw new Error(_($translate, `Value should be empty`));
|
|
||||||
},
|
|
||||||
length: ($translate, value, conf) => {
|
|
||||||
let options = {
|
|
||||||
min: conf.min || conf.is,
|
|
||||||
max: conf.max || conf.is
|
|
||||||
};
|
|
||||||
let val = value ? String(value) : '';
|
|
||||||
if (!validator.isLength(val, options)) {
|
|
||||||
if (conf.is) {
|
|
||||||
throw new Error(_($translate,
|
|
||||||
`Value should be %s characters long`, [conf.is]));
|
|
||||||
} else if (conf.min && conf.max) {
|
|
||||||
throw new Error(_($translate,
|
|
||||||
`Value should have a length between %s and %s`, [conf.min, conf.max]));
|
|
||||||
} else if (conf.min) {
|
|
||||||
throw new Error(_($translate,
|
|
||||||
`Value should have at least %s characters`, [conf.min]));
|
|
||||||
} else {
|
|
||||||
throw new Error(_($translate,
|
|
||||||
`Value should have at most %s characters`, [conf.max]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
numericality: ($translate, value, conf) => {
|
|
||||||
if (conf.int) {
|
|
||||||
if (!validator.isInt(value))
|
|
||||||
throw new Error(_($translate, `Value should be integer`));
|
|
||||||
} else if (!validator.isNumeric(value))
|
|
||||||
throw new Error(_($translate, `Value should be a number`));
|
|
||||||
},
|
|
||||||
inclusion: ($translate, value, conf) => {
|
|
||||||
if (!validator.isIn(value, conf.in))
|
|
||||||
throw new Error(_($translate, `Invalid value`));
|
|
||||||
},
|
|
||||||
exclusion: ($translate, value, conf) => {
|
|
||||||
if (validator.isIn(value, conf.in))
|
|
||||||
throw new Error(_($translate, `Invalid value`));
|
|
||||||
},
|
|
||||||
format: ($translate, value, conf) => {
|
|
||||||
if (!validator.matches(value, conf.with))
|
|
||||||
throw new Error(_($translate, `Invalid value`));
|
|
||||||
},
|
|
||||||
custom: ($translate, value, conf) => {
|
|
||||||
if (!conf.bindedFunction(value))
|
|
||||||
throw new Error(_($translate, `Invalid value`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if value satisfies a set of validations.
|
|
||||||
*
|
|
||||||
* @param {*} value The value
|
|
||||||
* @param {Array} validations Array with validations
|
|
||||||
*/
|
|
||||||
export function validateAll($translate, value, validations) {
|
|
||||||
for (let conf of validations)
|
|
||||||
validate($translate, value, conf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if value satisfies a validation.
|
|
||||||
*
|
|
||||||
* @param {*} value The value
|
|
||||||
* @param {Object} conf The validation configuration
|
|
||||||
*/
|
|
||||||
export function validate($translate, value, conf) {
|
|
||||||
let validator = validators[conf.validation];
|
|
||||||
try {
|
|
||||||
let isEmpty = value == null || value === '';
|
|
||||||
|
|
||||||
if (isEmpty)
|
|
||||||
checkNull($translate, value, conf);
|
|
||||||
if (validator && (!isEmpty || conf.validation == 'presence'))
|
|
||||||
validator($translate, value, conf);
|
|
||||||
} catch (e) {
|
|
||||||
let message = conf.message ? conf.message : e.message;
|
|
||||||
throw new Error(_($translate, message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if value satisfies a blank or not null validation.
|
|
||||||
*
|
|
||||||
* @param {*} value The value
|
|
||||||
* @param {Object} conf The validation configuration
|
|
||||||
*/
|
|
||||||
export function checkNull($translate, value, conf) {
|
|
||||||
if (conf.allowBlank === false && value === '')
|
|
||||||
throw new Error(_($translate, `Value can't be blank`));
|
|
||||||
else if (conf.allowNull === false && value == null)
|
|
||||||
throw new Error(_($translate, `Value can't be null`));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function _($translate, text, params = []) {
|
|
||||||
text = $translate.instant(text);
|
|
||||||
|
|
||||||
for (let i = 0; i < params.length; i++) {
|
|
||||||
text = text.replace('%s', params[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
|
@ -1,14 +1,8 @@
|
||||||
// app global css in SCSS form
|
// app global css in SCSS form
|
||||||
@import './icons.scss';
|
@import './icons.scss';
|
||||||
|
|
||||||
.body--dark {
|
a {
|
||||||
.q-card--dark {
|
text-decoration: none;
|
||||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-layout__shadow::after {
|
|
||||||
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.24) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
|
@ -19,3 +13,20 @@
|
||||||
.link:hover {
|
.link:hover {
|
||||||
color: $orange-4;
|
color: $orange-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Removes chrome autofill background
|
||||||
|
input:-webkit-autofill,
|
||||||
|
select:-webkit-autofill {
|
||||||
|
color: $input-text-color !important;
|
||||||
|
font-family: $typography-font-family;
|
||||||
|
-webkit-text-fill-color: $input-text-color !important;
|
||||||
|
-webkit-background-clip: text !important;
|
||||||
|
background-clip: text !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.body--light {
|
||||||
|
.q-header .q-toolbar {
|
||||||
|
background-color: white;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 162 KiB |
Binary file not shown.
Binary file not shown.
|
@ -10,7 +10,8 @@
|
||||||
font-display: block;
|
font-display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class^="icon-"], [class*=" icon-"] {
|
[class^='icon-'],
|
||||||
|
[class*=' icon-'] {
|
||||||
/* use !important to prevent issues with browser extensions that change fonts */
|
/* use !important to prevent issues with browser extensions that change fonts */
|
||||||
font-family: 'icomoon' !important;
|
font-family: 'icomoon' !important;
|
||||||
speak: never;
|
speak: never;
|
||||||
|
@ -26,374 +27,377 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-pin:before {
|
.icon-pin:before {
|
||||||
content: "\e950";
|
content: '\e950';
|
||||||
}
|
}
|
||||||
.icon-pin_off:before {
|
.icon-pin_off:before {
|
||||||
content: "\e95b";
|
content: '\e95b';
|
||||||
}
|
}
|
||||||
.icon-frozen:before {
|
.icon-frozen:before {
|
||||||
content: "\e900";
|
content: '\e900';
|
||||||
}
|
}
|
||||||
.icon-Person:before {
|
.icon-Person:before {
|
||||||
content: "\e901";
|
content: '\e901';
|
||||||
}
|
}
|
||||||
.icon-handmadeArtificial:before {
|
.icon-handmadeArtificial:before {
|
||||||
content: "\e902";
|
content: '\e902';
|
||||||
}
|
}
|
||||||
.icon-fruit:before {
|
.icon-fruit:before {
|
||||||
content: "\e903";
|
content: '\e903';
|
||||||
}
|
}
|
||||||
.icon-funeral:before {
|
.icon-funeral:before {
|
||||||
content: "\e904";
|
content: '\e904';
|
||||||
}
|
}
|
||||||
.icon-noPayMethod:before {
|
.icon-noPayMethod:before {
|
||||||
content: "\e905";
|
content: '\e905';
|
||||||
}
|
}
|
||||||
.icon-preserved:before {
|
.icon-preserved:before {
|
||||||
content: "\e906";
|
content: '\e906';
|
||||||
}
|
}
|
||||||
.icon-greenery:before {
|
.icon-greenery:before {
|
||||||
content: "\e907";
|
content: '\e907';
|
||||||
}
|
}
|
||||||
.icon-planta:before {
|
.icon-planta:before {
|
||||||
content: "\e908";
|
content: '\e908';
|
||||||
}
|
}
|
||||||
.icon-handmade:before {
|
.icon-handmade:before {
|
||||||
content: "\e909";
|
content: '\e909';
|
||||||
}
|
}
|
||||||
.icon-accessory:before {
|
.icon-accessory:before {
|
||||||
content: "\e90a";
|
content: '\e90a';
|
||||||
}
|
}
|
||||||
.icon-artificial:before {
|
.icon-artificial:before {
|
||||||
content: "\e90b";
|
content: '\e90b';
|
||||||
}
|
}
|
||||||
.icon-flower:before {
|
.icon-flower:before {
|
||||||
content: "\e90c";
|
content: '\e90c';
|
||||||
}
|
}
|
||||||
.icon-fixedPrice:before {
|
.icon-fixedPrice:before {
|
||||||
content: "\e90d";
|
content: '\e90d';
|
||||||
}
|
}
|
||||||
.icon-addperson:before {
|
.icon-addperson:before {
|
||||||
content: "\e90e";
|
content: '\e90e';
|
||||||
}
|
}
|
||||||
.icon-supplierfalse:before {
|
.icon-supplierfalse:before {
|
||||||
content: "\e90f";
|
content: '\e90f';
|
||||||
}
|
}
|
||||||
.icon-invoice-out:before {
|
.icon-invoice-out:before {
|
||||||
content: "\e910";
|
content: '\e910';
|
||||||
}
|
}
|
||||||
.icon-invoice-in:before {
|
.icon-invoice-in:before {
|
||||||
content: "\e911";
|
content: '\e911';
|
||||||
}
|
}
|
||||||
.icon-invoice-in-create:before {
|
.icon-invoice-in-create:before {
|
||||||
content: "\e912";
|
content: '\e912';
|
||||||
}
|
}
|
||||||
.icon-basketadd:before {
|
.icon-basketadd:before {
|
||||||
content: "\e913";
|
content: '\e913';
|
||||||
}
|
}
|
||||||
.icon-basket:before {
|
.icon-basket:before {
|
||||||
content: "\e914";
|
content: '\e914';
|
||||||
}
|
}
|
||||||
.icon-uniE915:before {
|
.icon-uniE915:before {
|
||||||
content: "\e915";
|
content: '\e915';
|
||||||
}
|
}
|
||||||
.icon-uniE916:before {
|
.icon-uniE916:before {
|
||||||
content: "\e916";
|
content: '\e916';
|
||||||
}
|
}
|
||||||
.icon-uniE917:before {
|
.icon-uniE917:before {
|
||||||
content: "\e917";
|
content: '\e917';
|
||||||
}
|
}
|
||||||
.icon-uniE918:before {
|
.icon-uniE918:before {
|
||||||
content: "\e918";
|
content: '\e918';
|
||||||
}
|
}
|
||||||
.icon-uniE919:before {
|
.icon-uniE919:before {
|
||||||
content: "\e919";
|
content: '\e919';
|
||||||
}
|
}
|
||||||
.icon-uniE91A:before {
|
.icon-uniE91A:before {
|
||||||
content: "\e91a";
|
content: '\e91a';
|
||||||
}
|
}
|
||||||
.icon-isTooLittle:before {
|
.icon-isTooLittle:before {
|
||||||
content: "\e91b";
|
content: '\e91b';
|
||||||
}
|
}
|
||||||
.icon-deliveryprices:before {
|
.icon-deliveryprices:before {
|
||||||
content: "\e91c";
|
content: '\e91c';
|
||||||
}
|
}
|
||||||
.icon-onlinepayment:before {
|
.icon-onlinepayment:before {
|
||||||
content: "\e91d";
|
content: '\e91d';
|
||||||
}
|
}
|
||||||
.icon-risk:before {
|
.icon-risk:before {
|
||||||
content: "\e91e";
|
content: '\e91e';
|
||||||
}
|
}
|
||||||
.icon-noweb:before {
|
.icon-noweb:before {
|
||||||
content: "\e91f";
|
content: '\e91f';
|
||||||
}
|
}
|
||||||
.icon-no036:before {
|
.icon-no036:before {
|
||||||
content: "\e920";
|
content: '\e920';
|
||||||
}
|
}
|
||||||
.icon-disabled:before {
|
.icon-disabled:before {
|
||||||
content: "\e921";
|
content: '\e921';
|
||||||
}
|
}
|
||||||
.icon-treatments:before {
|
.icon-treatments:before {
|
||||||
content: "\e922";
|
content: '\e922';
|
||||||
}
|
}
|
||||||
.icon-invoice:before {
|
.icon-invoice:before {
|
||||||
content: "\e923";
|
content: '\e923';
|
||||||
}
|
}
|
||||||
.icon-photo:before {
|
.icon-photo:before {
|
||||||
content: "\e924";
|
content: '\e924';
|
||||||
}
|
}
|
||||||
.icon-supplier:before {
|
.icon-supplier:before {
|
||||||
content: "\e925";
|
content: '\e925';
|
||||||
}
|
}
|
||||||
.icon-languaje:before {
|
.icon-languaje:before {
|
||||||
content: "\e926";
|
content: '\e926';
|
||||||
}
|
}
|
||||||
.icon-credit:before {
|
.icon-credit:before {
|
||||||
content: "\e927";
|
content: '\e927';
|
||||||
}
|
}
|
||||||
.icon-client:before {
|
.icon-client:before {
|
||||||
content: "\e928";
|
content: '\e928';
|
||||||
}
|
}
|
||||||
.icon-shipment-01:before {
|
.icon-shipment-01:before {
|
||||||
content: "\e929";
|
content: '\e929';
|
||||||
}
|
}
|
||||||
.icon-account:before {
|
.icon-account:before {
|
||||||
content: "\e92a";
|
content: '\e92a';
|
||||||
}
|
}
|
||||||
.icon-inventory:before {
|
.icon-inventory:before {
|
||||||
content: "\e92b";
|
content: '\e92b';
|
||||||
}
|
}
|
||||||
.icon-unavailable:before {
|
.icon-unavailable:before {
|
||||||
content: "\e92c";
|
content: '\e92c';
|
||||||
}
|
}
|
||||||
.icon-wiki:before {
|
.icon-wiki:before {
|
||||||
content: "\e92d";
|
content: '\e92d';
|
||||||
}
|
}
|
||||||
.icon-attach:before {
|
.icon-attach:before {
|
||||||
content: "\e92e";
|
content: '\e92e';
|
||||||
}
|
}
|
||||||
.icon-exit:before {
|
.icon-exit:before {
|
||||||
content: "\e92f";
|
content: '\e92f';
|
||||||
}
|
}
|
||||||
.icon-anonymous:before {
|
.icon-anonymous:before {
|
||||||
content: "\e930";
|
content: '\e930';
|
||||||
}
|
}
|
||||||
.icon-net:before {
|
.icon-net:before {
|
||||||
content: "\e931";
|
content: '\e931';
|
||||||
}
|
}
|
||||||
.icon-buyrequest:before {
|
.icon-buyrequest:before {
|
||||||
content: "\e932";
|
content: '\e932';
|
||||||
}
|
}
|
||||||
.icon-thermometer:before {
|
.icon-thermometer:before {
|
||||||
content: "\e933";
|
content: '\e933';
|
||||||
}
|
}
|
||||||
.icon-entry:before {
|
.icon-entry:before {
|
||||||
content: "\e934";
|
content: '\e934';
|
||||||
}
|
}
|
||||||
.icon-deletedTicket:before {
|
.icon-deletedTicket:before {
|
||||||
content: "\e935";
|
content: '\e935';
|
||||||
}
|
}
|
||||||
.icon-logout:before {
|
.icon-logout:before {
|
||||||
content: "\e936";
|
content: '\e936';
|
||||||
}
|
}
|
||||||
.icon-catalog:before {
|
.icon-catalog:before {
|
||||||
content: "\e937";
|
content: '\e937';
|
||||||
}
|
}
|
||||||
.icon-agency:before {
|
.icon-agency:before {
|
||||||
content: "\e938";
|
content: '\e938';
|
||||||
}
|
}
|
||||||
.icon-delivery:before {
|
.icon-delivery:before {
|
||||||
content: "\e939";
|
content: '\e939';
|
||||||
}
|
}
|
||||||
.icon-wand:before {
|
.icon-wand:before {
|
||||||
content: "\e93a";
|
content: '\e93a';
|
||||||
}
|
}
|
||||||
.icon-buscaman:before {
|
.icon-buscaman:before {
|
||||||
content: "\e93b";
|
content: '\e93b';
|
||||||
}
|
}
|
||||||
.icon-pbx:before {
|
.icon-pbx:before {
|
||||||
content: "\e93c";
|
content: '\e93c';
|
||||||
}
|
}
|
||||||
.icon-calendar:before {
|
.icon-calendar:before {
|
||||||
content: "\e93d";
|
content: '\e93d';
|
||||||
}
|
}
|
||||||
.icon-splitline:before {
|
.icon-splitline:before {
|
||||||
content: "\e93e";
|
content: '\e93e';
|
||||||
}
|
}
|
||||||
.icon-consignatarios:before {
|
.icon-consignatarios:before {
|
||||||
content: "\e93f";
|
content: '\e93f';
|
||||||
}
|
}
|
||||||
.icon-tax:before {
|
.icon-tax:before {
|
||||||
content: "\e940";
|
content: '\e940';
|
||||||
}
|
}
|
||||||
.icon-notes:before {
|
.icon-notes:before {
|
||||||
content: "\e941";
|
content: '\e941';
|
||||||
}
|
}
|
||||||
.icon-lines:before {
|
.icon-lines:before {
|
||||||
content: "\e942";
|
content: '\e942';
|
||||||
}
|
}
|
||||||
.icon-zone:before {
|
.icon-zone:before {
|
||||||
content: "\e943";
|
content: '\e943';
|
||||||
}
|
}
|
||||||
.icon-greuge:before {
|
.icon-greuge:before {
|
||||||
content: "\e944";
|
content: '\e944';
|
||||||
}
|
}
|
||||||
.icon-ticketAdd:before {
|
.icon-ticketAdd:before {
|
||||||
content: "\e945";
|
content: '\e945';
|
||||||
}
|
}
|
||||||
.icon-components:before {
|
.icon-components:before {
|
||||||
content: "\e946";
|
content: '\e946';
|
||||||
}
|
}
|
||||||
.icon-pets:before {
|
.icon-pets:before {
|
||||||
content: "\e947";
|
content: '\e947';
|
||||||
}
|
}
|
||||||
.icon-linesprepaired:before {
|
.icon-linesprepaired:before {
|
||||||
content: "\e948";
|
content: '\e948';
|
||||||
}
|
}
|
||||||
.icon-control:before {
|
.icon-control:before {
|
||||||
content: "\e949";
|
content: '\e949';
|
||||||
}
|
}
|
||||||
.icon-revision:before {
|
.icon-revision:before {
|
||||||
content: "\e94a";
|
content: '\e94a';
|
||||||
}
|
}
|
||||||
.icon-deaulter:before {
|
.icon-deaulter:before {
|
||||||
content: "\e94b";
|
content: '\e94b';
|
||||||
}
|
}
|
||||||
.icon-services:before {
|
.icon-services:before {
|
||||||
content: "\e94c";
|
content: '\e94c';
|
||||||
}
|
}
|
||||||
.icon-albaran:before {
|
.icon-albaran:before {
|
||||||
content: "\e94d";
|
content: '\e94d';
|
||||||
}
|
}
|
||||||
.icon-solunion:before {
|
.icon-solunion:before {
|
||||||
content: "\e94e";
|
content: '\e94e';
|
||||||
}
|
}
|
||||||
.icon-stowaway:before {
|
.icon-stowaway:before {
|
||||||
content: "\e94f";
|
content: '\e94f';
|
||||||
}
|
}
|
||||||
.icon-apps:before {
|
.icon-apps:before {
|
||||||
content: "\e951";
|
content: '\e951';
|
||||||
}
|
}
|
||||||
.icon-info:before {
|
.icon-info:before {
|
||||||
content: "\e952";
|
content: '\e952';
|
||||||
}
|
}
|
||||||
.icon-columndelete:before {
|
.icon-columndelete:before {
|
||||||
content: "\e953";
|
content: '\e953';
|
||||||
}
|
}
|
||||||
.icon-columnadd:before {
|
.icon-columnadd:before {
|
||||||
content: "\e954";
|
content: '\e954';
|
||||||
}
|
}
|
||||||
.icon-deleteline:before {
|
.icon-deleteline:before {
|
||||||
content: "\e955";
|
content: '\e955';
|
||||||
}
|
}
|
||||||
.icon-item:before {
|
.icon-item:before {
|
||||||
content: "\e956";
|
content: '\e956';
|
||||||
}
|
}
|
||||||
.icon-worker:before {
|
.icon-worker:before {
|
||||||
content: "\e957";
|
content: '\e957';
|
||||||
}
|
}
|
||||||
.icon-headercol:before {
|
.icon-headercol:before {
|
||||||
content: "\e958";
|
content: '\e958';
|
||||||
}
|
}
|
||||||
.icon-reserva:before {
|
.icon-reserva:before {
|
||||||
content: "\e959";
|
content: '\e959';
|
||||||
}
|
}
|
||||||
.icon-100:before {
|
.icon-100:before {
|
||||||
content: "\e95a";
|
content: '\e95a';
|
||||||
}
|
}
|
||||||
.icon-sign:before {
|
.icon-sign:before {
|
||||||
content: "\e95d";
|
content: '\e95d';
|
||||||
}
|
}
|
||||||
.icon-polizon:before {
|
.icon-polizon:before {
|
||||||
content: "\e95e";
|
content: '\e95e';
|
||||||
}
|
}
|
||||||
.icon-solclaim:before {
|
.icon-solclaim:before {
|
||||||
content: "\e95f";
|
content: '\e95f';
|
||||||
}
|
}
|
||||||
.icon-actions:before {
|
.icon-actions:before {
|
||||||
content: "\e960";
|
content: '\e960';
|
||||||
}
|
}
|
||||||
.icon-details:before {
|
.icon-details:before {
|
||||||
content: "\e961";
|
content: '\e961';
|
||||||
}
|
}
|
||||||
.icon-traceability:before {
|
.icon-traceability:before {
|
||||||
content: "\e962";
|
content: '\e962';
|
||||||
}
|
}
|
||||||
.icon-claims:before {
|
.icon-claims:before {
|
||||||
content: "\e963";
|
content: '\e963';
|
||||||
}
|
}
|
||||||
.icon-regentry:before {
|
.icon-regentry:before {
|
||||||
content: "\e964";
|
content: '\e964';
|
||||||
}
|
}
|
||||||
.icon-transaction:before {
|
.icon-transaction:before {
|
||||||
content: "\e966";
|
content: '\e966';
|
||||||
}
|
}
|
||||||
.icon-History:before {
|
.icon-History:before {
|
||||||
content: "\e968";
|
content: '\e968';
|
||||||
}
|
}
|
||||||
.icon-mana:before {
|
.icon-mana:before {
|
||||||
content: "\e96a";
|
content: '\e96a';
|
||||||
}
|
}
|
||||||
.icon-ticket:before {
|
.icon-ticket:before {
|
||||||
content: "\e96b";
|
content: '\e96b';
|
||||||
}
|
}
|
||||||
.icon-niche:before {
|
.icon-niche:before {
|
||||||
content: "\e96c";
|
content: '\e96c';
|
||||||
}
|
}
|
||||||
.icon-tags:before {
|
.icon-tags:before {
|
||||||
content: "\e96d";
|
content: '\e96d';
|
||||||
}
|
}
|
||||||
.icon-volume:before {
|
.icon-volume:before {
|
||||||
content: "\e96e";
|
content: '\e96e';
|
||||||
}
|
}
|
||||||
.icon-bin:before {
|
.icon-bin:before {
|
||||||
content: "\e96f";
|
content: '\e96f';
|
||||||
}
|
}
|
||||||
.icon-splur:before {
|
.icon-splur:before {
|
||||||
content: "\e970";
|
content: '\e970';
|
||||||
}
|
}
|
||||||
.icon-barcode:before {
|
.icon-barcode:before {
|
||||||
content: "\e971";
|
content: '\e971';
|
||||||
}
|
}
|
||||||
.icon-botanical:before {
|
.icon-botanical:before {
|
||||||
content: "\e972";
|
content: '\e972';
|
||||||
}
|
}
|
||||||
.icon-clone:before {
|
.icon-clone:before {
|
||||||
content: "\e973";
|
content: '\e973';
|
||||||
}
|
}
|
||||||
.icon-sms:before {
|
.icon-sms:before {
|
||||||
content: "\e975";
|
content: '\e975';
|
||||||
}
|
}
|
||||||
.icon-eye:before {
|
.icon-eye:before {
|
||||||
content: "\e976";
|
content: '\e976';
|
||||||
}
|
}
|
||||||
.icon-doc:before {
|
.icon-doc:before {
|
||||||
content: "\e977";
|
content: '\e977';
|
||||||
}
|
}
|
||||||
.icon-package:before {
|
.icon-package:before {
|
||||||
content: "\e978";
|
content: '\e978';
|
||||||
}
|
}
|
||||||
.icon-settings:before {
|
.icon-settings:before {
|
||||||
content: "\e979";
|
content: '\e979';
|
||||||
}
|
}
|
||||||
.icon-bucket:before {
|
.icon-bucket:before {
|
||||||
content: "\e97a";
|
content: '\e97a';
|
||||||
}
|
}
|
||||||
.icon-mandatory:before {
|
.icon-mandatory:before {
|
||||||
content: "\e97b";
|
content: '\e97b';
|
||||||
}
|
}
|
||||||
.icon-recovery:before {
|
.icon-recovery:before {
|
||||||
content: "\e97c";
|
content: '\e97c';
|
||||||
}
|
}
|
||||||
.icon-payment:before {
|
.icon-payment:before {
|
||||||
content: "\e97e";
|
content: '\e97e';
|
||||||
}
|
}
|
||||||
.icon-grid:before {
|
.icon-grid:before {
|
||||||
content: "\e980";
|
content: '\e980';
|
||||||
}
|
}
|
||||||
.icon-web:before {
|
.icon-web:before {
|
||||||
content: "\e982";
|
content: '\e982';
|
||||||
}
|
}
|
||||||
.icon-dfiscales:before {
|
.icon-dfiscales:before {
|
||||||
content: "\e984";
|
content: '\e984';
|
||||||
|
}
|
||||||
|
.icon-trolley:before {
|
||||||
|
content: '\e95c';
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,15 +16,16 @@ $primary: #ff9800;
|
||||||
$secondary: #26a69a;
|
$secondary: #26a69a;
|
||||||
$accent: #9c27b0;
|
$accent: #9c27b0;
|
||||||
|
|
||||||
$dark: #1d1d1d;
|
|
||||||
|
|
||||||
$positive: #21ba45;
|
$positive: #21ba45;
|
||||||
$negative: #c10015;
|
$negative: #c10015;
|
||||||
$info: #31ccec;
|
$info: #31ccec;
|
||||||
$warning: #f2c037;
|
$warning: #f2c037;
|
||||||
|
|
||||||
$color-spacer-light: rgba(255, 255, 255, .12);
|
$color-spacer-light: rgba(255, 255, 255, 0.12);
|
||||||
$color-spacer:rgba(255, 255, 255, .3);
|
$color-spacer: rgba(255, 255, 255, 0.3);
|
||||||
$border-thin-light: 1px solid $color-spacer-light;
|
$border-thin-light: 1px solid $color-spacer-light;
|
||||||
|
|
||||||
|
$dark-shadow-color: #000;
|
||||||
|
$dark: #292929;
|
||||||
|
$layout-shadow-dark: 0 0 10px 2px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.24);
|
||||||
$spacing-md: 16px;
|
$spacing-md: 16px;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import toLowerCase from './toLowerCase';
|
import toLowerCase from './toLowerCase';
|
||||||
import toDate from './toDate';
|
import toDate from './toDate';
|
||||||
|
import toDateString from './toDateString';
|
||||||
import toCurrency from './toCurrency';
|
import toCurrency from './toCurrency';
|
||||||
import toPercentage from './toPercentage';
|
import toPercentage from './toPercentage';
|
||||||
import toLowerCamel from './toLowerCamel';
|
import toLowerCamel from './toLowerCamel';
|
||||||
|
@ -9,6 +10,7 @@ export {
|
||||||
toLowerCase,
|
toLowerCase,
|
||||||
toLowerCamel,
|
toLowerCamel,
|
||||||
toDate,
|
toDate,
|
||||||
|
toDateString,
|
||||||
toCurrency,
|
toCurrency,
|
||||||
toPercentage,
|
toPercentage,
|
||||||
dashIfEmpty,
|
dashIfEmpty,
|
||||||
|
|
|
@ -3,10 +3,14 @@ import { useI18n } from 'vue-i18n';
|
||||||
export default function (value, options = {}) {
|
export default function (value, options = {}) {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
|
|
||||||
if (!options.dateStyle) options.dateStyle = 'short';
|
if (!options.dateStyle && !options.timeStyle) {
|
||||||
|
options.day = '2-digit';
|
||||||
|
options.month = '2-digit';
|
||||||
|
options.year = 'numeric';
|
||||||
|
}
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
|
|
||||||
return new Intl.DateTimeFormat(locale.value, options).format(date)
|
return new Intl.DateTimeFormat(locale.value, options).format(date);
|
||||||
}
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default function toDateString(date) {
|
||||||
|
let day = date.getDate();
|
||||||
|
let month = date.getMonth() + 1;
|
||||||
|
let year = date.getFullYear();
|
||||||
|
|
||||||
|
if (day < 10) day = `0${day}`;
|
||||||
|
if (month < 10) month = `0${month}`;
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ export default {
|
||||||
rowAdded: 'Row added',
|
rowAdded: 'Row added',
|
||||||
rowRemoved: 'Row removed',
|
rowRemoved: 'Row removed',
|
||||||
pleaseWait: 'Please wait...',
|
pleaseWait: 'Please wait...',
|
||||||
|
noPinnedModules: 'You have dont have any pinned modules',
|
||||||
},
|
},
|
||||||
moduleIndex: {
|
moduleIndex: {
|
||||||
allModules: 'All modules',
|
allModules: 'All modules',
|
||||||
|
@ -59,6 +60,7 @@ export default {
|
||||||
pageTitles: {
|
pageTitles: {
|
||||||
customers: 'Customers',
|
customers: 'Customers',
|
||||||
list: 'List',
|
list: 'List',
|
||||||
|
webPayments: 'Web Payments',
|
||||||
createCustomer: 'Create customer',
|
createCustomer: 'Create customer',
|
||||||
summary: 'Summary',
|
summary: 'Summary',
|
||||||
basicData: 'Basic Data',
|
basicData: 'Basic Data',
|
||||||
|
@ -239,7 +241,10 @@ export default {
|
||||||
rmaList: 'RMA',
|
rmaList: 'RMA',
|
||||||
summary: 'Summary',
|
summary: 'Summary',
|
||||||
basicData: 'Basic Data',
|
basicData: 'Basic Data',
|
||||||
|
lines: 'Lines',
|
||||||
rma: 'RMA',
|
rma: 'RMA',
|
||||||
|
photos: 'Photos',
|
||||||
|
log: 'Audit logs',
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
customer: 'Customer',
|
customer: 'Customer',
|
||||||
|
@ -283,6 +288,14 @@ export default {
|
||||||
responsibility: 'Responsibility',
|
responsibility: 'Responsibility',
|
||||||
company: 'Company',
|
company: 'Company',
|
||||||
person: 'Employee/Customer',
|
person: 'Employee/Customer',
|
||||||
|
notes: 'Notes',
|
||||||
|
photos: 'Photos',
|
||||||
|
development: 'Development',
|
||||||
|
reason: 'Reason',
|
||||||
|
result: 'Result',
|
||||||
|
responsible: 'Responsible',
|
||||||
|
worker: 'Worker',
|
||||||
|
redelivery: 'Redelivery'
|
||||||
},
|
},
|
||||||
basicData: {
|
basicData: {
|
||||||
customer: 'Customer',
|
customer: 'Customer',
|
||||||
|
@ -293,10 +306,15 @@ export default {
|
||||||
picked: 'Picked',
|
picked: 'Picked',
|
||||||
returnOfMaterial: 'Return of material authorization (RMA)',
|
returnOfMaterial: 'Return of material authorization (RMA)',
|
||||||
},
|
},
|
||||||
|
photo: {
|
||||||
|
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}',
|
||||||
|
noData: 'There are no images/videos, click here or drag and drop the file',
|
||||||
|
dragDrop: 'Drag and drop it here',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
invoiceOut: {
|
invoiceOut: {
|
||||||
pageTitles: {
|
pageTitles: {
|
||||||
invoiceOuts: 'InvoiceOuts',
|
invoiceOuts: 'Invoices Out',
|
||||||
list: 'List',
|
list: 'List',
|
||||||
createInvoiceOut: 'Create invoice out',
|
createInvoiceOut: 'Create invoice out',
|
||||||
summary: 'Summary',
|
summary: 'Summary',
|
||||||
|
@ -385,6 +403,49 @@ export default {
|
||||||
},
|
},
|
||||||
imageNotFound: 'Image not found',
|
imageNotFound: 'Image not found',
|
||||||
},
|
},
|
||||||
|
wagon: {
|
||||||
|
pageTitles: {
|
||||||
|
wagons: 'Wagons',
|
||||||
|
wagonsList: 'Wagons List',
|
||||||
|
wagonCreate: 'Create wagon',
|
||||||
|
wagonEdit: 'Edit wagon',
|
||||||
|
typesList: 'Types List',
|
||||||
|
typeCreate: 'Create type',
|
||||||
|
typeEdit: 'Edit type'
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
name: 'Name',
|
||||||
|
submit: 'Submit',
|
||||||
|
reset: 'Reset',
|
||||||
|
trayColor: 'Tray color',
|
||||||
|
removeItem: 'Wagon type removed successfully',
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
plate: 'Plate',
|
||||||
|
volume: 'Volume',
|
||||||
|
type: 'Type',
|
||||||
|
remove: 'Remove',
|
||||||
|
removeItem: 'Wagon removed successfully',
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
plate: 'Plate',
|
||||||
|
volume: 'Volume',
|
||||||
|
type: 'Type',
|
||||||
|
label: 'Label'
|
||||||
|
},
|
||||||
|
warnings: {
|
||||||
|
noData: 'No data available',
|
||||||
|
nameNotEmpty: 'Name can not be empty',
|
||||||
|
labelNotEmpty: 'Label can not be empty',
|
||||||
|
plateNotEmpty: 'Plate can not be empty',
|
||||||
|
volumeNotEmpty: 'Volume can not be empty',
|
||||||
|
typeNotEmpty: 'Type can not be empty',
|
||||||
|
maxTrays: 'You have reached the max number of trays',
|
||||||
|
minHeightBetweenTrays: 'The minimum height between trays is ',
|
||||||
|
maxWagonHeight: 'The maximum height of the wagon is ',
|
||||||
|
uncompleteTrays: 'There are incomplete trays',
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
topbar: {},
|
topbar: {},
|
||||||
userPanel: {
|
userPanel: {
|
||||||
|
@ -392,7 +453,6 @@ export default {
|
||||||
logOut: 'Log Out',
|
logOut: 'Log Out',
|
||||||
},
|
},
|
||||||
smartCard: {
|
smartCard: {
|
||||||
noData: 'No data to display',
|
|
||||||
openCard: 'View card',
|
openCard: 'View card',
|
||||||
openSummary: 'Open summary',
|
openSummary: 'Open summary',
|
||||||
viewDescription: 'View description',
|
viewDescription: 'View description',
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default {
|
||||||
darkMode: 'Modo oscuro',
|
darkMode: 'Modo oscuro',
|
||||||
logOut: 'Cerrar sesión',
|
logOut: 'Cerrar sesión',
|
||||||
dataSaved: 'Datos guardados',
|
dataSaved: 'Datos guardados',
|
||||||
dataDeleted: 'Data deleted',
|
dataDeleted: 'Datos eliminados',
|
||||||
add: 'Añadir',
|
add: 'Añadir',
|
||||||
create: 'Crear',
|
create: 'Crear',
|
||||||
save: 'Guardar',
|
save: 'Guardar',
|
||||||
|
@ -30,6 +30,7 @@ export default {
|
||||||
rowAdded: 'Fila añadida',
|
rowAdded: 'Fila añadida',
|
||||||
rowRemoved: 'Fila eliminada',
|
rowRemoved: 'Fila eliminada',
|
||||||
pleaseWait: 'Por favor, espera...',
|
pleaseWait: 'Por favor, espera...',
|
||||||
|
noPinnedModules: 'No has fijado ningún módulo',
|
||||||
},
|
},
|
||||||
moduleIndex: {
|
moduleIndex: {
|
||||||
allModules: 'Todos los módulos',
|
allModules: 'Todos los módulos',
|
||||||
|
@ -59,9 +60,10 @@ export default {
|
||||||
pageTitles: {
|
pageTitles: {
|
||||||
customers: 'Clientes',
|
customers: 'Clientes',
|
||||||
list: 'Listado',
|
list: 'Listado',
|
||||||
|
webPayments: 'Pagos Web',
|
||||||
createCustomer: 'Crear cliente',
|
createCustomer: 'Crear cliente',
|
||||||
summary: 'Resumen',
|
|
||||||
basicData: 'Datos básicos',
|
basicData: 'Datos básicos',
|
||||||
|
summary: 'Resumen'
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
phone: 'Teléfono',
|
phone: 'Teléfono',
|
||||||
|
@ -238,7 +240,10 @@ export default {
|
||||||
rmaList: 'RMA',
|
rmaList: 'RMA',
|
||||||
summary: 'Resumen',
|
summary: 'Resumen',
|
||||||
basicData: 'Datos básicos',
|
basicData: 'Datos básicos',
|
||||||
|
lines: 'Líneas',
|
||||||
rma: 'RMA',
|
rma: 'RMA',
|
||||||
|
photos: 'Fotos',
|
||||||
|
log: 'Registros de auditoría',
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
customer: 'Cliente',
|
customer: 'Cliente',
|
||||||
|
@ -282,6 +287,14 @@ export default {
|
||||||
responsibility: 'Responsabilidad',
|
responsibility: 'Responsabilidad',
|
||||||
company: 'Empresa',
|
company: 'Empresa',
|
||||||
person: 'Comercial/Cliente',
|
person: 'Comercial/Cliente',
|
||||||
|
notes: 'Observaciones',
|
||||||
|
photos: 'Fotos',
|
||||||
|
development: 'Trazabilidad',
|
||||||
|
reason: 'Motivo',
|
||||||
|
result: 'Consecuencias',
|
||||||
|
responsible: 'Responsable',
|
||||||
|
worker: 'Trabajador',
|
||||||
|
redelivery: 'Devolución'
|
||||||
},
|
},
|
||||||
basicData: {
|
basicData: {
|
||||||
customer: 'Cliente',
|
customer: 'Cliente',
|
||||||
|
@ -292,6 +305,12 @@ export default {
|
||||||
picked: 'Recogida',
|
picked: 'Recogida',
|
||||||
returnOfMaterial: 'Autorización de retorno de materiales (RMA)',
|
returnOfMaterial: 'Autorización de retorno de materiales (RMA)',
|
||||||
},
|
},
|
||||||
|
photo: {
|
||||||
|
fileDescription:
|
||||||
|
'Reclamacion ID {claimId} del cliente {clientName} id {clientId}',
|
||||||
|
noData: 'No hay imágenes/videos, haz click aquí o arrastra y suelta el archivo',
|
||||||
|
dragDrop: 'Arrástralo y sueltalo aquí',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
invoiceOut: {
|
invoiceOut: {
|
||||||
pageTitles: {
|
pageTitles: {
|
||||||
|
@ -384,6 +403,49 @@ export default {
|
||||||
},
|
},
|
||||||
imageNotFound: 'No se ha encontrado la imagen',
|
imageNotFound: 'No se ha encontrado la imagen',
|
||||||
},
|
},
|
||||||
|
wagon: {
|
||||||
|
pageTitles: {
|
||||||
|
wagons: 'Vagones',
|
||||||
|
wagonsList: 'Listado vagones',
|
||||||
|
wagonCreate: 'Crear tipo',
|
||||||
|
wagonEdit: 'Editar tipo',
|
||||||
|
typesList: 'Listado tipos',
|
||||||
|
typeCreate: 'Crear tipo',
|
||||||
|
typeEdit: 'Editar tipo'
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
name: 'Nombre',
|
||||||
|
submit: 'Guardar',
|
||||||
|
reset: 'Deshacer cambios',
|
||||||
|
trayColor: 'Color de la bandeja',
|
||||||
|
removeItem: 'Tipo de vagón borrado correctamente',
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
plate: 'Matrícula',
|
||||||
|
volume: 'Volumen',
|
||||||
|
type: 'Tipo',
|
||||||
|
remove: 'Borrar',
|
||||||
|
removeItem: 'Vagón borrado correctamente',
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
plate: 'Matrícula',
|
||||||
|
volume: 'Volumen',
|
||||||
|
type: 'Tipo',
|
||||||
|
label: 'Etiqueta',
|
||||||
|
},
|
||||||
|
warnings: {
|
||||||
|
noData: 'Sin datos disponibles',
|
||||||
|
nameNotEmpty: 'El nombre no puede estar vacío',
|
||||||
|
labelNotEmpty: 'La etiqueta no puede estar vacía',
|
||||||
|
plateNotEmpty: 'La matrícula no puede estar vacía',
|
||||||
|
volumeNotEmpty: 'El volumen no puede estar vacío',
|
||||||
|
typeNotEmpty: 'El tipo no puede estar vacío',
|
||||||
|
maxTrays: 'Has alcanzado el número máximo de bandejas',
|
||||||
|
minHeightBetweenTrays: 'La distancia mínima entre bandejas es ',
|
||||||
|
maxWagonHeight: 'La altura máxima del vagón es ',
|
||||||
|
uncompleteTrays: 'Hay bandejas sin completar',
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
topbar: {},
|
topbar: {},
|
||||||
userPanel: {
|
userPanel: {
|
||||||
|
@ -391,7 +453,6 @@ export default {
|
||||||
logOut: 'Cerrar sesión',
|
logOut: 'Cerrar sesión',
|
||||||
},
|
},
|
||||||
smartCard: {
|
smartCard: {
|
||||||
noData: 'Sin datos que mostrar',
|
|
||||||
openCard: 'Ver ficha',
|
openCard: 'Ver ficha',
|
||||||
openSummary: 'Abrir detalles',
|
openSummary: 'Abrir detalles',
|
||||||
viewDescription: 'Ver descripción',
|
viewDescription: 'Ver descripción',
|
||||||
|
|
|
@ -6,11 +6,11 @@ const quasar = useQuasar();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-layout view="hHh LpR fFf">
|
<QLayout view="hHh LpR fFf">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<router-view></router-view>
|
<RouterView></RouterView>
|
||||||
<q-footer v-if="quasar.platform.is.mobile"></q-footer>
|
<QFooter v-if="quasar.platform.is.mobile"></QFooter>
|
||||||
</q-layout>
|
</QLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|
|
@ -73,41 +73,66 @@ const statesFilter = {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<fetch-data
|
<FetchData
|
||||||
url="Workers/activeWithInheritedRole"
|
url="Workers/activeWithInheritedRole"
|
||||||
:filter="{ where: { role: 'salesPerson' } }"
|
:filter="{ where: { role: 'salesPerson' } }"
|
||||||
@on-fetch="setWorkers"
|
@on-fetch="setWorkers"
|
||||||
auto-load
|
auto-load
|
||||||
/>
|
/>
|
||||||
<fetch-data url="ClaimStates" @on-fetch="setClaimStates" auto-load />
|
<FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load />
|
||||||
|
|
||||||
<div class="container">
|
<div class="column items-center">
|
||||||
<q-card>
|
<QCard>
|
||||||
<form-model :url="`Claims/${route.params.id}`" :filter="claimFilter" model="claim">
|
<FormModel
|
||||||
|
:url="`Claims/${route.params.id}`"
|
||||||
|
:filter="claimFilter"
|
||||||
|
model="claim"
|
||||||
|
>
|
||||||
<template #form="{ data, validate, filter }">
|
<template #form="{ data, validate, filter }">
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-input v-model="data.client.name" :label="t('claim.basicData.customer')" disable />
|
<QInput
|
||||||
|
v-model="data.client.name"
|
||||||
|
:label="t('claim.basicData.customer')"
|
||||||
|
disable
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-input v-model="data.created" mask="####-##-##" fill-mask="_" autofocus>
|
<QInput
|
||||||
|
v-model="data.created"
|
||||||
|
mask="####-##-##"
|
||||||
|
fill-mask="_"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<q-icon name="event" class="cursor-pointer">
|
<QIcon name="event" class="cursor-pointer">
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
<QPopupProxy
|
||||||
<q-date v-model="data.created" mask="YYYY-MM-DD">
|
cover
|
||||||
|
transition-show="scale"
|
||||||
|
transition-hide="scale"
|
||||||
|
>
|
||||||
|
<QDate
|
||||||
|
v-model="data.created"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
>
|
||||||
<div class="row items-center justify-end">
|
<div class="row items-center justify-end">
|
||||||
<q-btn v-close-popup label="Close" color="primary" flat />
|
<QBtn
|
||||||
|
v-close-popup
|
||||||
|
label="Close"
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-date>
|
</QDate>
|
||||||
</q-popup-proxy>
|
</QPopupProxy>
|
||||||
</q-icon>
|
</QIcon>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</QInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-select
|
<QSelect
|
||||||
v-model="data.workerFk"
|
v-model="data.workerFk"
|
||||||
:options="workers"
|
:options="workers"
|
||||||
option-value="id"
|
option-value="id"
|
||||||
|
@ -116,23 +141,25 @@ const statesFilter = {
|
||||||
:label="t('claim.basicData.assignedTo')"
|
:label="t('claim.basicData.assignedTo')"
|
||||||
map-options
|
map-options
|
||||||
use-input
|
use-input
|
||||||
@filter="(value, update) => filter(value, update, workerFilter)"
|
@filter="
|
||||||
|
(value, update) => filter(value, update, workerFilter)
|
||||||
|
"
|
||||||
:rules="validate('claim.claimStateFk')"
|
:rules="validate('claim.claimStateFk')"
|
||||||
:input-debounce="0"
|
:input-debounce="0"
|
||||||
>
|
>
|
||||||
<template #before>
|
<template #before>
|
||||||
<q-avatar color="orange">
|
<QAvatar color="orange">
|
||||||
<q-img
|
<QImg
|
||||||
v-if="data.workerFk"
|
v-if="data.workerFk"
|
||||||
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`"
|
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`"
|
||||||
spinner-color="white"
|
spinner-color="white"
|
||||||
/>
|
/>
|
||||||
</q-avatar>
|
</QAvatar>
|
||||||
</template>
|
</template>
|
||||||
</q-select>
|
</QSelect>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-select
|
<QSelect
|
||||||
v-model="data.claimStateFk"
|
v-model="data.claimStateFk"
|
||||||
:options="claimStates"
|
:options="claimStates"
|
||||||
option-value="id"
|
option-value="id"
|
||||||
|
@ -141,23 +168,25 @@ const statesFilter = {
|
||||||
:label="t('claim.basicData.state')"
|
:label="t('claim.basicData.state')"
|
||||||
map-options
|
map-options
|
||||||
use-input
|
use-input
|
||||||
@filter="(value, update) => filter(value, update, statesFilter)"
|
@filter="
|
||||||
|
(value, update) => filter(value, update, statesFilter)
|
||||||
|
"
|
||||||
:rules="validate('claim.claimStateFk')"
|
:rules="validate('claim.claimStateFk')"
|
||||||
:input-debounce="0"
|
:input-debounce="0"
|
||||||
>
|
>
|
||||||
</q-select>
|
</QSelect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-input
|
<QInput
|
||||||
v-model="data.packages"
|
v-model="data.packages"
|
||||||
:label="t('claim.basicData.packages')"
|
:label="t('claim.basicData.packages')"
|
||||||
:rules="validate('claim.packages')"
|
:rules="validate('claim.packages')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-input
|
<QInput
|
||||||
v-model="data.rma"
|
v-model="data.rma"
|
||||||
:label="t('claim.basicData.returnOfMaterial')"
|
:label="t('claim.basicData.returnOfMaterial')"
|
||||||
:rules="validate('claim.rma')"
|
:rules="validate('claim.rma')"
|
||||||
|
@ -166,22 +195,21 @@ const statesFilter = {
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-checkbox v-model="data.hasToPickUp" :label="t('claim.basicData.picked')" />
|
<QCheckbox
|
||||||
|
v-model="data.hasToPickUp"
|
||||||
|
:label="t('claim.basicData.picked')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</form-model>
|
</FormModel>
|
||||||
</q-card>
|
</QCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-card {
|
.q-card {
|
||||||
width: 800px;
|
width: 100%;
|
||||||
|
max-width: 60em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,50 +1,82 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useState } from 'composables/useState';
|
|
||||||
import ClaimDescriptor from './ClaimDescriptor.vue';
|
|
||||||
import LeftMenu from 'components/LeftMenu.vue';
|
import LeftMenu from 'components/LeftMenu.vue';
|
||||||
|
import { getUrl } from 'composables/getUrl';
|
||||||
|
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
|
||||||
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
|
import { computed, onMounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import ClaimDescriptor from './ClaimDescriptor.vue';
|
||||||
|
|
||||||
const state = useState();
|
const stateStore = useStateStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const $props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const entityId = computed(() => {
|
||||||
|
return $props.id || route.params.id;
|
||||||
|
});
|
||||||
|
const claimSections = [
|
||||||
|
{ name: 'Notes', url: '/note/index', icon: 'draft' },
|
||||||
|
{ name: 'Development', url: '/development', icon: 'vn:traceability' },
|
||||||
|
{ name: 'Action', url: '/action', icon: 'vn:actions' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let salixUrl;
|
||||||
|
onMounted(async () => {
|
||||||
|
salixUrl = await getUrl(`claim/${entityId.value}`);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
|
||||||
<q-scroll-area class="fit">
|
<VnSearchbar
|
||||||
<claim-descriptor />
|
data-key="ClaimList"
|
||||||
<q-separator />
|
url="Claims/filter"
|
||||||
<left-menu source="card" />
|
:label="t('Search claim')"
|
||||||
</q-scroll-area>
|
:info="t('You can search by claim id or customer name')"
|
||||||
</q-drawer>
|
/>
|
||||||
<q-page-container>
|
</Teleport>
|
||||||
<q-page class="q-pa-md">
|
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||||
<router-view></router-view>
|
<QScrollArea class="fit">
|
||||||
</q-page>
|
<ClaimDescriptor />
|
||||||
</q-page-container>
|
<QSeparator />
|
||||||
|
<LeftMenu source="card" />
|
||||||
|
<QSeparator />
|
||||||
|
<QList>
|
||||||
|
<QItem
|
||||||
|
v-for="section in claimSections"
|
||||||
|
:key="section.name"
|
||||||
|
active-class="text-primary"
|
||||||
|
:href="salixUrl + section.url"
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<QItemSection avatar>
|
||||||
|
<QIcon :name="section.icon" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection> {{ t(section.name) }} </QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</QScrollArea>
|
||||||
|
</QDrawer>
|
||||||
|
<QPageContainer>
|
||||||
|
<QPage class="q-pa-md">
|
||||||
|
<RouterView></RouterView>
|
||||||
|
</QPage>
|
||||||
|
</QPageContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<i18n>
|
||||||
.q-scrollarea__content {
|
es:
|
||||||
max-width: 100%;
|
Search claim: Buscar reclamación
|
||||||
}
|
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
|
||||||
</style>
|
Details: Detalles
|
||||||
|
Notes: Notas
|
||||||
<style lang="scss" scoped>
|
Development: Trazabilidad
|
||||||
.descriptor {
|
Action: Acción
|
||||||
max-width: 256px;
|
</i18n>
|
||||||
|
|
||||||
h5 {
|
|
||||||
margin: 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-card__actions {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#descriptor-skeleton .q-card__actions {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, computed, ref } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { toDate } from 'src/filters';
|
import { toDate } from 'src/filters';
|
||||||
import axios from 'axios';
|
|
||||||
import TicketDescriptorPopover from 'pages/Ticket/Card/TicketDescriptorPopover.vue';
|
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
|
||||||
import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue';
|
import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue';
|
||||||
import CardDescriptor from 'components/ui/CardDescriptor.vue';
|
import CardDescriptor from 'components/ui/CardDescriptor.vue';
|
||||||
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
|
|
||||||
|
|
||||||
const $props = defineProps({
|
const $props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
|
@ -17,10 +16,6 @@ const $props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetch();
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -28,8 +23,6 @@ const entityId = computed(() => {
|
||||||
return $props.id || route.params.id;
|
return $props.id || route.params.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const claim = ref();
|
|
||||||
async function fetch() {
|
|
||||||
const filter = {
|
const filter = {
|
||||||
include: [
|
include: [
|
||||||
{ relation: 'client' },
|
{ relation: 'client' },
|
||||||
|
@ -45,11 +38,6 @@ async function fetch() {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const options = { params: { filter } };
|
|
||||||
const { data } = await axios.get(`Claims/${entityId.value}`, options);
|
|
||||||
|
|
||||||
if (data) claim.value = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stateColor(code) {
|
function stateColor(code) {
|
||||||
if (code === 'pending') return 'green';
|
if (code === 'pending') return 'green';
|
||||||
|
@ -59,62 +47,77 @@ function stateColor(code) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<skeleton-descriptor v-if="!claim" />
|
<CardDescriptor
|
||||||
<card-descriptor v-if="claim" module="Claim" :data="claim" :description="claim.client.name">
|
ref="descriptor"
|
||||||
<template #menu>
|
:url="`Claims/${entityId}`"
|
||||||
<claim-descriptor-menu v-if="claim" :claim="claim" />
|
:filter="filter"
|
||||||
|
module="Claim"
|
||||||
|
>
|
||||||
|
<template #menu="{ entity }">
|
||||||
|
<ClaimDescriptorMenu :claim="entity" />
|
||||||
</template>
|
</template>
|
||||||
<template #body>
|
<template #description="{ entity }">
|
||||||
<q-list>
|
<span>
|
||||||
<q-item>
|
{{ entity.client.name }}
|
||||||
<q-item-section>
|
<QTooltip>{{ entity.client.name }}</QTooltip>
|
||||||
<q-item-label caption>{{ t('claim.card.created') }}</q-item-label>
|
</span>
|
||||||
<q-item-label>{{ toDate(claim.created) }}</q-item-label>
|
</template>
|
||||||
</q-item-section>
|
<template #body="{ entity }">
|
||||||
<q-item-section>
|
<QList>
|
||||||
<q-item-label caption>{{ t('claim.card.state') }}</q-item-label>
|
<QItem>
|
||||||
<q-item-label>
|
<QItemSection>
|
||||||
<q-chip :color="stateColor(claim.claimState.code)" dense>
|
<QItemLabel caption>{{ t('claim.card.created') }}</QItemLabel>
|
||||||
{{ claim.claimState.description }}
|
<QItemLabel>{{ toDate(entity.created) }}</QItemLabel>
|
||||||
</q-chip>
|
</QItemSection>
|
||||||
</q-item-label>
|
<QItemSection v-if="entity.claimState">
|
||||||
</q-item-section>
|
<QItemLabel caption>{{ t('claim.card.state') }}</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel>
|
||||||
<q-item>
|
<QBadge :color="stateColor(entity.claimState.code)" dense>
|
||||||
<q-item-section>
|
{{ entity.claimState.description }}
|
||||||
<q-item-label caption>{{ t('claim.card.ticketId') }}</q-item-label>
|
</QBadge>
|
||||||
<q-item-label class="link">
|
</QItemLabel>
|
||||||
{{ claim.ticketFk }}
|
</QItemSection>
|
||||||
<q-popup-proxy>
|
</QItem>
|
||||||
<ticket-descriptor-popover :id="claim.ticketFk" />
|
<QItem>
|
||||||
</q-popup-proxy>
|
<QItemSection>
|
||||||
</q-item-label>
|
<QItemLabel caption>
|
||||||
</q-item-section>
|
{{ t('claim.card.ticketId') }}
|
||||||
<q-item-section>
|
</QItemLabel>
|
||||||
<q-item-label caption>{{ t('claim.card.assignedTo') }}</q-item-label>
|
<QItemLabel>
|
||||||
<q-item-label>{{ claim.worker.user.name }}</q-item-label>
|
<span class="link">
|
||||||
</q-item-section>
|
{{ entity.ticketFk }}
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
|
|
||||||
<q-card-actions>
|
<TicketDescriptorProxy :id="entity.ticketFk" />
|
||||||
<q-btn
|
</span>
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="entity.worker">
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('claim.card.assignedTo') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>{{ entity.worker.user.name }}</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
|
||||||
|
<QCardActions>
|
||||||
|
<QBtn
|
||||||
size="md"
|
size="md"
|
||||||
icon="vn:client"
|
icon="vn:client"
|
||||||
color="primary"
|
color="primary"
|
||||||
:to="{ name: 'CustomerCard', params: { id: claim.clientFk } }"
|
:to="{ name: 'CustomerCard', params: { id: entity.clientFk } }"
|
||||||
>
|
>
|
||||||
<q-tooltip>{{ t('claim.card.customerSummary') }}</q-tooltip>
|
<QTooltip>{{ t('claim.card.customerSummary') }}</QTooltip>
|
||||||
</q-btn>
|
</QBtn>
|
||||||
<q-btn
|
<QBtn
|
||||||
size="md"
|
size="md"
|
||||||
icon="vn:ticket"
|
icon="vn:ticket"
|
||||||
color="primary"
|
color="primary"
|
||||||
:to="{ name: 'TicketCard', params: { id: claim.ticketFk } }"
|
:to="{ name: 'TicketCard', params: { id: entity.ticketFk } }"
|
||||||
>
|
>
|
||||||
<q-tooltip>{{ t('claim.card.claimedTicket') }}</q-tooltip>
|
<QTooltip>{{ t('claim.card.claimedTicket') }}</QTooltip>
|
||||||
</q-btn>
|
</QBtn>
|
||||||
</q-card-actions>
|
</QCardActions>
|
||||||
</template>
|
</template>
|
||||||
</card-descriptor>
|
</CardDescriptor>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { usePrintService } from 'composables/usePrintService';
|
import { usePrintService } from 'composables/usePrintService';
|
||||||
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
|
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
|
||||||
|
import VnConfirm from 'components/ui/VnConfirm.vue';
|
||||||
|
|
||||||
const $props = defineProps({
|
const $props = defineProps({
|
||||||
claim: {
|
claim: {
|
||||||
|
@ -33,13 +34,15 @@ function confirmPickupOrder() {
|
||||||
quasar.dialog({
|
quasar.dialog({
|
||||||
component: SendEmailDialog,
|
component: SendEmailDialog,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
|
data: {
|
||||||
address: customer.email,
|
address: customer.email,
|
||||||
|
},
|
||||||
send: sendPickupOrder,
|
send: sendPickupOrder,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendPickupOrder(address) {
|
function sendPickupOrder({ address }) {
|
||||||
const id = claim.value.id;
|
const id = claim.value.id;
|
||||||
const customer = claim.value.client;
|
const customer = claim.value.client;
|
||||||
return sendEmail(`Claims/${id}/claim-pickup-email`, {
|
return sendEmail(`Claims/${id}/claim-pickup-email`, {
|
||||||
|
@ -48,66 +51,61 @@ function sendPickupOrder(address) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const showConfirmDialog = ref(false);
|
function confirmRemove() {
|
||||||
async function deleteClaim() {
|
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;
|
const id = claim.value.id;
|
||||||
await axios.delete(`Claims/${id}`);
|
await axios.delete(`Claims/${id}`);
|
||||||
quasar.notify({
|
quasar.notify({
|
||||||
message: t('globals.dataDeleted'),
|
message: t('globals.dataDeleted'),
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
icon: 'check',
|
|
||||||
});
|
});
|
||||||
await router.push({ name: 'ClaimList' });
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-item v-ripple clickable>
|
<QItem v-ripple clickable>
|
||||||
<q-item-section avatar>
|
<QItemSection avatar>
|
||||||
<q-icon name="summarize" />
|
<QIcon name="summarize" />
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
<q-item-section>{{ t('pickupOrder') }}</q-item-section>
|
<QItemSection>{{ t('pickupOrder') }}</QItemSection>
|
||||||
<q-item-section side>
|
<QItemSection side>
|
||||||
<q-icon name="keyboard_arrow_right" />
|
<QIcon name="keyboard_arrow_right" />
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
<q-menu anchor="top end" self="top start" auto-close>
|
<QMenu anchor="top end" self="top start" auto-close>
|
||||||
<q-list>
|
<QList>
|
||||||
<q-item @click="openPickupOrder" v-ripple clickable>
|
<QItem @click="openPickupOrder" v-ripple clickable>
|
||||||
<q-item-section avatar>
|
<QItemSection avatar>
|
||||||
<q-icon name="picture_as_pdf" />
|
<QIcon name="picture_as_pdf" />
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
<q-item-section>{{ t('openPickupOrder') }}</q-item-section>
|
<QItemSection>{{ t('openPickupOrder') }}</QItemSection>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item @click="confirmPickupOrder" v-ripple clickable>
|
<QItem @click="confirmPickupOrder" v-ripple clickable>
|
||||||
<q-item-section avatar>
|
<QItemSection avatar>
|
||||||
<q-icon name="send" />
|
<QIcon name="send" />
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
<q-item-section>{{ t('sendPickupOrder') }}</q-item-section>
|
<QItemSection>{{ t('sendPickupOrder') }}</QItemSection>
|
||||||
</q-item>
|
</QItem>
|
||||||
</q-list>
|
</QList>
|
||||||
</q-menu>
|
</QMenu>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-separator />
|
<QSeparator />
|
||||||
<q-item @click="showConfirmDialog = true" v-ripple clickable>
|
<QItem @click="confirmRemove()" v-ripple clickable>
|
||||||
<q-item-section avatar>
|
<QItemSection avatar>
|
||||||
<q-icon name="delete" />
|
<QIcon name="delete" />
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
<q-item-section>{{ t('deleteClaim') }}</q-item-section>
|
<QItemSection>{{ t('deleteClaim') }}</QItemSection>
|
||||||
</q-item>
|
</QItem>
|
||||||
|
|
||||||
<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>
|
</template>
|
||||||
|
|
||||||
<i18n>
|
<i18n>
|
||||||
|
|
|
@ -0,0 +1,440 @@
|
||||||
|
<script setup>
|
||||||
|
import axios from 'axios';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useArrayData } from 'composables/useArrayData';
|
||||||
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
|
import VnPaginate from 'components/ui/VnPaginate.vue';
|
||||||
|
import FetchData from 'components/FetchData.vue';
|
||||||
|
import VnConfirm from 'components/ui/VnConfirm.vue';
|
||||||
|
|
||||||
|
import { toDate, toCurrency, toPercentage } from 'filters/index';
|
||||||
|
import VnDiscount from 'components/common/vnDiscount.vue';
|
||||||
|
import ClaimLinesImport from './ClaimLinesImport.vue';
|
||||||
|
|
||||||
|
const quasar = useQuasar();
|
||||||
|
const route = useRoute();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const stateStore = useStateStore();
|
||||||
|
const arrayData = useArrayData('ClaimLines');
|
||||||
|
const store = arrayData.store;
|
||||||
|
|
||||||
|
const claimFilter = {
|
||||||
|
fields: ['ticketFk'],
|
||||||
|
};
|
||||||
|
const linesFilter = {
|
||||||
|
include: {
|
||||||
|
relation: 'sale',
|
||||||
|
scope: {
|
||||||
|
fields: ['concept', 'ticketFk', 'price', 'quantity', 'discount', 'itemFk'],
|
||||||
|
include: {
|
||||||
|
relation: 'ticket',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const claim = ref(null);
|
||||||
|
async function onFetchClaim(data) {
|
||||||
|
claim.value = data;
|
||||||
|
|
||||||
|
fetchMana();
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = ref(0);
|
||||||
|
const amountClaimed = ref(0);
|
||||||
|
async function onFetch(rows) {
|
||||||
|
amount.value = rows.reduce(
|
||||||
|
(acumulator, { sale }) => acumulator + sale.price * sale.quantity,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
amountClaimed.value = rows.reduce(
|
||||||
|
(acumulator, line) => acumulator + line.sale.price * line.quantity,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = computed(() => [
|
||||||
|
{
|
||||||
|
name: 'dated',
|
||||||
|
label: t('Delivered'),
|
||||||
|
field: ({ sale: { ticket } }) => toDate(ticket.landed),
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quantity',
|
||||||
|
label: t('Quantity'),
|
||||||
|
field: ({ sale }) => sale.quantity,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'claimed',
|
||||||
|
label: t('Claimed'),
|
||||||
|
field: (row) => row.quantity,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
label: t('Description'),
|
||||||
|
field: ({ sale }) => sale.concept,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'price',
|
||||||
|
label: t('Price'),
|
||||||
|
field: ({ sale }) => sale.price,
|
||||||
|
format: (value) => toCurrency(value),
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'discount',
|
||||||
|
label: t('Discount'),
|
||||||
|
field: ({ sale }) => sale.discount,
|
||||||
|
format: (value) => toPercentage(value / 100),
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'total',
|
||||||
|
label: t('Total'),
|
||||||
|
field: ({ sale }) => {
|
||||||
|
const amount = sale.price * sale.quantity;
|
||||||
|
const appliedDiscount = (sale.discount * amount) / 100;
|
||||||
|
|
||||||
|
return amount - appliedDiscount;
|
||||||
|
},
|
||||||
|
format: (value) => toCurrency(value),
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selected = ref([]);
|
||||||
|
const mana = ref(0);
|
||||||
|
async function fetchMana() {
|
||||||
|
const ticketId = claim.value.ticketFk;
|
||||||
|
const response = await axios.get(`Tickets/${ticketId}/getSalesPersonMana`);
|
||||||
|
mana.value = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateQuantity({ id, quantity }) {
|
||||||
|
if (!id) return;
|
||||||
|
await axios.patch(`ClaimBeginnings/${id}`, { quantity });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDiscount({ saleFk, discount, canceller }) {
|
||||||
|
const body = { salesIds: [saleFk], newDiscount: discount };
|
||||||
|
const claimId = claim.value.ticketFk;
|
||||||
|
const query = `Tickets/${claimId}/updateDiscount`;
|
||||||
|
|
||||||
|
await axios.post(query, body, {
|
||||||
|
signal: canceller.signal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpdateDiscount(response) {
|
||||||
|
const row = store.data[response.rowIndex];
|
||||||
|
row.sale.discount = response.discount;
|
||||||
|
quasar.notify({
|
||||||
|
message: t('Discount updated'),
|
||||||
|
type: 'positive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmRemove() {
|
||||||
|
const rows = selected.value;
|
||||||
|
const count = rows.length;
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
return quasar.notify({
|
||||||
|
message: 'You must select at least one row',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
quasar
|
||||||
|
.dialog({
|
||||||
|
component: VnConfirm,
|
||||||
|
componentProps: {
|
||||||
|
title: t('Delete claimed sales'),
|
||||||
|
message: t('You are about to remove {count} rows', count, { count }),
|
||||||
|
data: { rows },
|
||||||
|
promise: remove,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.onOk(() => {
|
||||||
|
for (const row of rows) {
|
||||||
|
const orgData = store.data;
|
||||||
|
const index = orgData.findIndex((item) => item.id === row.id);
|
||||||
|
store.data.splice(index, 1);
|
||||||
|
selected.value = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove({ rows }) {
|
||||||
|
if (!rows.length) return;
|
||||||
|
const body = { deletes: rows.map((row) => row.id) };
|
||||||
|
await axios.post(`ClaimBeginnings/crud`, body);
|
||||||
|
quasar.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: t('globals.rowRemoved'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showImportDialog() {
|
||||||
|
quasar
|
||||||
|
.dialog({
|
||||||
|
component: ClaimLinesImport,
|
||||||
|
})
|
||||||
|
.onOk(() => arrayData.refresh());
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<QPageSticky position="top" :offset="[0, 0]" expand>
|
||||||
|
<QToolbar class="bg-dark text-white">
|
||||||
|
<QToolbarTitle> {{ t('Claimed lines') }} </QToolbarTitle>
|
||||||
|
<QSpace />
|
||||||
|
<div class="row q-gutter-md">
|
||||||
|
<div>
|
||||||
|
{{ t('Amount') }}
|
||||||
|
<QChip :dense="$q.screen.lt.sm">
|
||||||
|
{{ toCurrency(amount) }}
|
||||||
|
</QChip>
|
||||||
|
</div>
|
||||||
|
<QSeparator dark vertical />
|
||||||
|
<div>
|
||||||
|
{{ t('Amount Claimed') }}
|
||||||
|
<QChip color="positive" :dense="$q.screen.lt.sm">
|
||||||
|
{{ toCurrency(amountClaimed) }}
|
||||||
|
</QChip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</QToolbar>
|
||||||
|
</QPageSticky>
|
||||||
|
|
||||||
|
<FetchData
|
||||||
|
:url="`Claims/${route.params.id}`"
|
||||||
|
:filter="claimFilter"
|
||||||
|
@on-fetch="onFetchClaim"
|
||||||
|
auto-load
|
||||||
|
/>
|
||||||
|
<div class="column items-center">
|
||||||
|
<div class="list">
|
||||||
|
<VnPaginate
|
||||||
|
data-key="ClaimLines"
|
||||||
|
:url="`Claims/${route.params.id}/lines`"
|
||||||
|
:filter="linesFilter"
|
||||||
|
@on-fetch="onFetch"
|
||||||
|
auto-load
|
||||||
|
>
|
||||||
|
<template #body="{ rows }">
|
||||||
|
<QTable
|
||||||
|
:columns="columns"
|
||||||
|
:rows="rows"
|
||||||
|
:dense="$q.screen.lt.md"
|
||||||
|
:pagination="{ rowsPerPage: 0 }"
|
||||||
|
row-key="id"
|
||||||
|
selection="multiple"
|
||||||
|
v-model:selected="selected"
|
||||||
|
hide-pagination
|
||||||
|
:grid="$q.screen.lt.md"
|
||||||
|
>
|
||||||
|
<template #body-cell-claimed="{ row, value }">
|
||||||
|
<QTd auto-width align="right" class="text-primary">
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
|
||||||
|
<QPopupEdit
|
||||||
|
v-model="row.quantity"
|
||||||
|
v-slot="scope"
|
||||||
|
:title="t('Claimed quantity')"
|
||||||
|
@update:model-value="updateQuantity(row)"
|
||||||
|
buttons
|
||||||
|
>
|
||||||
|
<QInput
|
||||||
|
v-model="scope.value"
|
||||||
|
type="number"
|
||||||
|
dense
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="scope.set"
|
||||||
|
@focus="($event) => $event.target.select()"
|
||||||
|
/>
|
||||||
|
</QPopupEdit>
|
||||||
|
</QTd>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell-discount="{ row, value, rowIndex }">
|
||||||
|
<QTd auto-width align="right" class="text-primary">
|
||||||
|
{{ value }}
|
||||||
|
<VnDiscount
|
||||||
|
:quantity="row.quantity"
|
||||||
|
:price="row.sale.price"
|
||||||
|
:discount="row.sale.discount"
|
||||||
|
:mana="mana"
|
||||||
|
:promise="updateDiscount"
|
||||||
|
:data="{ saleFk: row.sale.id, rowIndex: rowIndex }"
|
||||||
|
@on-update="onUpdateDiscount"
|
||||||
|
/>
|
||||||
|
</QTd>
|
||||||
|
</template>
|
||||||
|
<!-- View for grid mode -->
|
||||||
|
<template #item="props">
|
||||||
|
<div
|
||||||
|
class="q-mb-md col-12 grid-style-transition"
|
||||||
|
:style="props.selected ? 'transform: scale(0.95);' : ''"
|
||||||
|
>
|
||||||
|
<QCard>
|
||||||
|
<QCardSection>
|
||||||
|
<QCheckbox v-model="props.selected" />
|
||||||
|
</QCardSection>
|
||||||
|
<QSeparator inset />
|
||||||
|
<QList dense>
|
||||||
|
<QItem
|
||||||
|
v-for="column of props.cols"
|
||||||
|
:key="column.name"
|
||||||
|
>
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ column.label }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection side>
|
||||||
|
<template
|
||||||
|
v-if="column.name === 'claimed'"
|
||||||
|
>
|
||||||
|
<QItemLabel class="text-primary">
|
||||||
|
{{ column.value }}
|
||||||
|
<QPopupEdit
|
||||||
|
v-model="props.row.quantity"
|
||||||
|
v-slot="scope"
|
||||||
|
:title="t('Claimed quantity')"
|
||||||
|
@update:model-value="
|
||||||
|
updateQuantity(props.row)
|
||||||
|
"
|
||||||
|
buttons
|
||||||
|
>
|
||||||
|
<QInput
|
||||||
|
v-model="scope.value"
|
||||||
|
type="number"
|
||||||
|
dense
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="scope.set"
|
||||||
|
@focus="
|
||||||
|
($event) =>
|
||||||
|
$event.target.select()
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</QPopupEdit>
|
||||||
|
</QItemLabel>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-else-if="column.name === 'discount'"
|
||||||
|
>
|
||||||
|
<QItemLabel class="text-primary">
|
||||||
|
{{ column.value }}
|
||||||
|
<VnDiscount
|
||||||
|
:quantity="props.row.quantity"
|
||||||
|
:price="props.row.sale.price"
|
||||||
|
:discount="
|
||||||
|
props.row.sale.discount
|
||||||
|
"
|
||||||
|
:mana="mana"
|
||||||
|
:promise="updateDiscount"
|
||||||
|
:data="{
|
||||||
|
saleFk: props.row.sale.id,
|
||||||
|
rowIndex: props.rowIndex,
|
||||||
|
}"
|
||||||
|
@on-update="onUpdateDiscount"
|
||||||
|
/>
|
||||||
|
</QItemLabel>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<QItemLabel>
|
||||||
|
{{ column.value }}
|
||||||
|
</QItemLabel>
|
||||||
|
</template>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</QCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</QTable>
|
||||||
|
</template>
|
||||||
|
</VnPaginate>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport
|
||||||
|
v-if="stateStore.isHeaderMounted() && !$q.screen.lt.sm"
|
||||||
|
to="#actions-prepend"
|
||||||
|
>
|
||||||
|
<div class="row q-gutter-x-sm">
|
||||||
|
<QBtn
|
||||||
|
v-if="selected.length > 0"
|
||||||
|
@click="confirmRemove"
|
||||||
|
icon="delete"
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
>
|
||||||
|
<QTooltip bottom> {{ t('globals.remove') }} </QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QBtn @click="showImportDialog" icon="add" color="primary" flat dense rounded>
|
||||||
|
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QSeparator vertical />
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
<!-- v-if="quasar.platform.is.mobile" -->
|
||||||
|
<QPageSticky v-if="$q.screen.lt.sm" position="bottom" :offset="[0, 0]" expand>
|
||||||
|
<QToolbar class="bg-primary text-white q-pa-none">
|
||||||
|
<QTabs class="full-width" align="justify" inline-label narrow-indicator>
|
||||||
|
<QTab @click="showImportDialog" icon="add" :label="t('globals.add')" />
|
||||||
|
<QSeparator vertical inset />
|
||||||
|
<QTab
|
||||||
|
@click="confirmRemove"
|
||||||
|
icon="delete"
|
||||||
|
:label="t('globals.remove')"
|
||||||
|
:disable="selected.length === 0"
|
||||||
|
/>
|
||||||
|
</QTabs>
|
||||||
|
</QToolbar>
|
||||||
|
</QPageSticky>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.list {
|
||||||
|
padding-top: 50px;
|
||||||
|
max-width: 900px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.grid-style-transition {
|
||||||
|
transition: transform 0.28s, background-color 0.28s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
en:
|
||||||
|
You are about to remove {count} rows: '
|
||||||
|
You are about to remove <strong>{count}</strong> row |
|
||||||
|
You are about to remove <strong>{count}</strong> rows'
|
||||||
|
es:
|
||||||
|
Claimed lines: Líneas reclamadas
|
||||||
|
Delivered: Entregado
|
||||||
|
Quantity: Cantidad
|
||||||
|
Claimed: Reclamada
|
||||||
|
Description: Descripción
|
||||||
|
Price: Precio
|
||||||
|
Discount: Descuento
|
||||||
|
Actions: Acciones
|
||||||
|
Amount: Total
|
||||||
|
Amount Claimed: Cantidad reclamada
|
||||||
|
Delete claimed sales: Eliminar ventas reclamadas
|
||||||
|
Discount updated: Descuento actualizado
|
||||||
|
Claimed quantity: Cantidad reclamada
|
||||||
|
You are about to remove {count} rows: '
|
||||||
|
Vas a eliminar <strong>{count}</strong> línea |
|
||||||
|
Vas a eliminar <strong>{count}</strong> líneas'
|
||||||
|
</i18n>
|
|
@ -0,0 +1,182 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useQuasar, useDialogPluginComponent } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import FetchData from 'components/FetchData.vue';
|
||||||
|
import { toDate, toCurrency, toPercentage } from 'filters/index';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
defineEmits([...useDialogPluginComponent.emits]);
|
||||||
|
|
||||||
|
const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
||||||
|
const route = useRoute();
|
||||||
|
const quasar = useQuasar();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const columns = computed(() => [
|
||||||
|
{
|
||||||
|
name: 'delivered',
|
||||||
|
label: t('Delivered'),
|
||||||
|
field: (row) => row.landed,
|
||||||
|
format: (value) => toDate(value),
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quantity',
|
||||||
|
label: t('Quantity'),
|
||||||
|
field: (row) => row.quantity,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
label: t('Description'),
|
||||||
|
field: (row) => row.concept,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'price',
|
||||||
|
label: t('Price'),
|
||||||
|
field: (row) => row.price,
|
||||||
|
format: (value) => toCurrency(value),
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'discount',
|
||||||
|
label: t('Discount'),
|
||||||
|
field: (row) => row.discount,
|
||||||
|
format: (value) => toPercentage(value),
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selected = ref([]);
|
||||||
|
const claimableSales = ref([]);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
let canceller;
|
||||||
|
|
||||||
|
async function importLines() {
|
||||||
|
const sales = selected.value;
|
||||||
|
|
||||||
|
if (!sales.length) {
|
||||||
|
return quasar.notify({
|
||||||
|
message: 'You must select at least one',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = sales.map((row) => ({
|
||||||
|
claimFk: route.params.id,
|
||||||
|
saleFk: row.saleFk,
|
||||||
|
quantity: row.quantity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
canceller = new AbortController();
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
const { data } = await axios.post('ClaimBeginnings', body, {
|
||||||
|
signal: canceller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
quasar.notify({
|
||||||
|
message: 'Lines added to claim',
|
||||||
|
type: 'positive',
|
||||||
|
});
|
||||||
|
|
||||||
|
onDialogOK(data);
|
||||||
|
|
||||||
|
canceller = null;
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
if (canceller) {
|
||||||
|
canceller.abort();
|
||||||
|
canceller = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDialogCancel();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<FetchData
|
||||||
|
url="Sales/getClaimableFromTicket?ticketFk=16"
|
||||||
|
@on-fetch="(data) => (claimableSales = data)"
|
||||||
|
auto-load
|
||||||
|
/>
|
||||||
|
<QDialog ref="dialogRef" persistent>
|
||||||
|
<QCard>
|
||||||
|
<QCardSection class="row items-center">
|
||||||
|
<span class="text-h6 text-grey">{{ t('Available sales lines') }}</span>
|
||||||
|
<QSpace />
|
||||||
|
<QBtn icon="close" flat round dense v-close-popup />
|
||||||
|
</QCardSection>
|
||||||
|
<QTable
|
||||||
|
class="my-sticky-header-table"
|
||||||
|
:columns="columns"
|
||||||
|
:rows="claimableSales"
|
||||||
|
:pagination="{ rowsPerPage: 10 }"
|
||||||
|
row-key="saleFk"
|
||||||
|
selection="multiple"
|
||||||
|
v-model:selected="selected"
|
||||||
|
square
|
||||||
|
flat
|
||||||
|
/>
|
||||||
|
<QSeparator />
|
||||||
|
<QCardActions align="right">
|
||||||
|
<QBtn :label="t('globals.cancel')" color="primary" flat @click="cancel" />
|
||||||
|
<QBtn
|
||||||
|
:label="t('globals.confirm')"
|
||||||
|
color="primary"
|
||||||
|
:loading="isLoading"
|
||||||
|
@click="importLines"
|
||||||
|
unelevated
|
||||||
|
/>
|
||||||
|
</QCardActions>
|
||||||
|
</QCard>
|
||||||
|
</QDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.q-card {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.my-sticky-header-table {
|
||||||
|
height: 400px;
|
||||||
|
|
||||||
|
thead tr th {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead tr:first-child th {
|
||||||
|
/* this is when the loading indicator appears */
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.q-table--loading thead tr:last-child th {
|
||||||
|
/* height of all previous header rows */
|
||||||
|
top: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// /* prevent scrolling behind sticky top row on focus */
|
||||||
|
tbody {
|
||||||
|
/* height of all previous header rows */
|
||||||
|
scroll-margin-top: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
Available sales lines: Líneas de venta disponibles
|
||||||
|
Delivered: Entrega
|
||||||
|
Quantity: Cantidad
|
||||||
|
Description: Descripción
|
||||||
|
Price: Precio
|
||||||
|
Discount: Descuento
|
||||||
|
Lines added to claim: Lineas añadidas a la reclamación
|
||||||
|
You must select at least one: Debes seleccionar al menos una
|
||||||
|
</i18n>
|
|
@ -0,0 +1,201 @@
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
|
import VnPaginate from 'src/components/ui/VnPaginate.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">
|
||||||
|
<QTimeline class="q-pa-md">
|
||||||
|
<QTimelineEntry heading tag="h4"> {{ t('Audit logs') }} </QTimelineEntry>
|
||||||
|
<VnPaginate
|
||||||
|
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">
|
||||||
|
<QTimelineEntry
|
||||||
|
: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>
|
||||||
|
<QChip :color="actionColor(log.action)">
|
||||||
|
{{ t(`actions.${log.action}`) }}
|
||||||
|
</QChip>
|
||||||
|
{{ t(`models.${log.model}`) }}
|
||||||
|
</template>
|
||||||
|
<QTable
|
||||||
|
:rows="log.changes"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="property"
|
||||||
|
hide-pagination
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<template #header="props">
|
||||||
|
<QTr :props="props">
|
||||||
|
<QTh
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
{{ t(col.label) }}
|
||||||
|
</QTh>
|
||||||
|
</QTr>
|
||||||
|
</template>
|
||||||
|
</QTable>
|
||||||
|
</QTimelineEntry>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</VnPaginate>
|
||||||
|
</QTimeline>
|
||||||
|
</div>
|
||||||
|
<Teleport v-if="stateStore.isHeaderMounted()" to="#actions-append">
|
||||||
|
<div class="row q-gutter-x-sm">
|
||||||
|
<QBtn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
|
||||||
|
<QTooltip bottom anchor="bottom right">
|
||||||
|
{{ t('globals.collapseMenu') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
<QDrawer v-model="stateStore.rightDrawer" show-if-above side="right" :width="300">
|
||||||
|
<QScrollArea class="fit text-grey-8">
|
||||||
|
<ClaimLogFilter data-key="ClaimLogs" />
|
||||||
|
</QScrollArea>
|
||||||
|
</QDrawer>
|
||||||
|
</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
|
||||||
|
claimStateFk: Claim State
|
||||||
|
workerFk: Worker
|
||||||
|
clientFk: Customer
|
||||||
|
rma: RMA
|
||||||
|
responsibility: Responsibility
|
||||||
|
packages: Packages
|
||||||
|
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
|
||||||
|
claimStateFk: Estado de la reclamación
|
||||||
|
workerFk: Trabajador
|
||||||
|
clientFk: Cliente
|
||||||
|
rma: RMA
|
||||||
|
responsibility: Responsabilidad
|
||||||
|
packages: Bultos
|
||||||
|
</i18n>
|
|
@ -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>
|
||||||
|
<FetchData
|
||||||
|
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 }">
|
||||||
|
<QDate
|
||||||
|
v-model="params.created"
|
||||||
|
@update:model-value="searchFn()"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
minimal
|
||||||
|
>
|
||||||
|
</QDate>
|
||||||
|
<QList dense>
|
||||||
|
<QSeparator />
|
||||||
|
<QItem>
|
||||||
|
<QItemSection v-if="!workers">
|
||||||
|
<QSkeleton type="QInput" class="full-width" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="workers">
|
||||||
|
<QSelect
|
||||||
|
: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"
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</template>
|
||||||
|
</VnFilterPanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
en:
|
||||||
|
params:
|
||||||
|
search: Contains
|
||||||
|
userFk: User
|
||||||
|
created: Created
|
||||||
|
es:
|
||||||
|
params:
|
||||||
|
search: Contiene
|
||||||
|
userFk: Usuario
|
||||||
|
created: Creada
|
||||||
|
User: Usuario
|
||||||
|
</i18n>
|
|
@ -0,0 +1,384 @@
|
||||||
|
<script setup>
|
||||||
|
import axios from 'axios';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
|
import { useSession } from 'composables/useSession';
|
||||||
|
import VnConfirm from 'components/ui/VnConfirm.vue';
|
||||||
|
import FetchData from 'components/FetchData.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const quasar = useQuasar();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const stateStore = useStateStore();
|
||||||
|
const session = useSession();
|
||||||
|
const token = session.getToken();
|
||||||
|
|
||||||
|
const claimId = computed(() => router.currentRoute.value.params.id);
|
||||||
|
|
||||||
|
const claimDms = ref([
|
||||||
|
{
|
||||||
|
dmsFk: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const client = ref({});
|
||||||
|
|
||||||
|
const inputFile = ref();
|
||||||
|
const files = ref({});
|
||||||
|
|
||||||
|
const claimDmsRef = ref();
|
||||||
|
const dmsType = ref({});
|
||||||
|
const config = ref({});
|
||||||
|
const dragFile = ref(false);
|
||||||
|
const dragFileTimeout = ref();
|
||||||
|
const claimDmsFilter = ref({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
relation: 'client',
|
||||||
|
scope: {
|
||||||
|
fields: ['id', 'name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relation: 'claimDms',
|
||||||
|
scope: {
|
||||||
|
include: {
|
||||||
|
relation: 'dms',
|
||||||
|
scope: {
|
||||||
|
fields: ['contentType'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
where: { id: claimId.value },
|
||||||
|
});
|
||||||
|
|
||||||
|
const multimediaDialog = ref();
|
||||||
|
const multimediaSlide = ref();
|
||||||
|
|
||||||
|
function openDialog(dmsId) {
|
||||||
|
multimediaSlide.value = dmsId;
|
||||||
|
multimediaDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewDeleteDms(index) {
|
||||||
|
quasar
|
||||||
|
.dialog({
|
||||||
|
component: VnConfirm,
|
||||||
|
componentProps: {
|
||||||
|
title: t('This file will be deleted'),
|
||||||
|
icon: 'delete',
|
||||||
|
data: { index },
|
||||||
|
promise: deleteDms,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.onOk(() => claimDms.value.splice(index, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDms({ index }) {
|
||||||
|
const dmsId = claimDms.value[index].dmsFk;
|
||||||
|
await axios.post(`ClaimDms/${dmsId}/removeFile`);
|
||||||
|
quasar.notify({
|
||||||
|
message: t('globals.dataDeleted'),
|
||||||
|
type: 'positive',
|
||||||
|
icon: 'delete',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setClaimDms(data) {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
claimDms.value = data.claimDms.map((media) => {
|
||||||
|
media.isVideo = media.dms.contentType == 'video/mp4';
|
||||||
|
media.url = `/api/Claims/${media.dmsFk}/downloadFile?access_token=${token}`;
|
||||||
|
return media;
|
||||||
|
});
|
||||||
|
client.value = data.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create() {
|
||||||
|
const formData = new FormData();
|
||||||
|
const inputFiles = files.value;
|
||||||
|
for (let i = 0; i < inputFiles.length; i++)
|
||||||
|
formData.append(inputFiles[i].name, inputFiles[i]);
|
||||||
|
|
||||||
|
const query = `claims/${claimId.value}/uploadFile`;
|
||||||
|
|
||||||
|
const dms = {
|
||||||
|
hasFile: false,
|
||||||
|
hasFileAttached: false,
|
||||||
|
reference: claimId.value,
|
||||||
|
warehouseId: config.value.warehouseFk,
|
||||||
|
companyId: config.value.companyFk,
|
||||||
|
dmsTypeId: dmsType.value.id,
|
||||||
|
description: t('claim.photo.fileDescription', {
|
||||||
|
claimId: claimId.value,
|
||||||
|
clientName: client.value.name,
|
||||||
|
clientId: client.value.id,
|
||||||
|
}).toUpperCase(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(query, formData, {
|
||||||
|
params: dms,
|
||||||
|
});
|
||||||
|
|
||||||
|
quasar.notify({
|
||||||
|
message: t('globals.dataSaved'),
|
||||||
|
type: 'positive',
|
||||||
|
icon: 'check',
|
||||||
|
});
|
||||||
|
|
||||||
|
claimDmsRef.value.fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop($data) {
|
||||||
|
dragFile.value = false;
|
||||||
|
files.value = $data.dataTransfer.files;
|
||||||
|
create();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrag() {
|
||||||
|
clearTimeout(dragFileTimeout.value);
|
||||||
|
dragFileTimeout.value = setTimeout(() => (dragFile.value = false), 500);
|
||||||
|
dragFile.value = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<FetchData
|
||||||
|
url="Claims"
|
||||||
|
:filter="claimDmsFilter"
|
||||||
|
@on-fetch="([data]) => setClaimDms(data)"
|
||||||
|
limit="20"
|
||||||
|
auto-load
|
||||||
|
ref="claimDmsRef"
|
||||||
|
/>
|
||||||
|
<FetchData
|
||||||
|
url="DmsTypes/findOne"
|
||||||
|
:filter="{ where: { code: 'claim' } }"
|
||||||
|
@on-fetch="(data) => (dmsType = data)"
|
||||||
|
auto-load
|
||||||
|
/>
|
||||||
|
<FetchData
|
||||||
|
url="UserConfigs/getUserConfig"
|
||||||
|
@on-fetch="(data) => (config = data)"
|
||||||
|
auto-load
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
:class="['container', { dragFile }]"
|
||||||
|
@drop.prevent="onDrop"
|
||||||
|
@dragenter.prevent
|
||||||
|
@dragover.prevent="onDrag"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-center items-center text-grey q-mt-md column"
|
||||||
|
v-if="dragFile"
|
||||||
|
>
|
||||||
|
<QIcon size="xl" name="file_download" />
|
||||||
|
<h5>
|
||||||
|
{{ t('claim.photo.dragDrop') }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-center text-grey q-mt-md cursor-pointer"
|
||||||
|
v-if="!claimDms?.length && !dragFile"
|
||||||
|
@click="inputFile.nativeEl.click()"
|
||||||
|
>
|
||||||
|
<QIcon size="xl" name="image"></QIcon>
|
||||||
|
<QIcon size="xl" name="movie"></QIcon>
|
||||||
|
<h5>
|
||||||
|
{{ t('claim.photo.noData') }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="multimediaParent bg-transparent" v-if="claimDms?.length && !dragFile">
|
||||||
|
<div
|
||||||
|
v-for="(media, index) of claimDms"
|
||||||
|
:key="index"
|
||||||
|
class="relative-position"
|
||||||
|
>
|
||||||
|
<QBtn
|
||||||
|
icon="delete"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
size="md"
|
||||||
|
class="all-pointer-events absolute delete-button zindex"
|
||||||
|
@click.stop="viewDeleteDms(index)"
|
||||||
|
round
|
||||||
|
/>
|
||||||
|
<QIcon
|
||||||
|
name="play_circle"
|
||||||
|
color="primary"
|
||||||
|
size="xl"
|
||||||
|
class="absolute-center zindex"
|
||||||
|
v-if="media.isVideo"
|
||||||
|
@click.stop="openDialog(media.dmsFk)"
|
||||||
|
>
|
||||||
|
<QTooltip>Video</QTooltip>
|
||||||
|
</QIcon>
|
||||||
|
<QCard class="multimedia relative-position">
|
||||||
|
<QImg
|
||||||
|
:src="media.url"
|
||||||
|
class="rounded-borders cursor-pointer fit"
|
||||||
|
@click="openDialog(media.dmsFk)"
|
||||||
|
v-if="!media.isVideo"
|
||||||
|
>
|
||||||
|
</QImg>
|
||||||
|
<video
|
||||||
|
:src="media.url"
|
||||||
|
class="rounded-borders cursor-pointer fit"
|
||||||
|
muted="muted"
|
||||||
|
v-if="media.isVideo"
|
||||||
|
@click="openDialog(media.dmsFk)"
|
||||||
|
/>
|
||||||
|
</QCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport
|
||||||
|
v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile"
|
||||||
|
to="#actions-prepend"
|
||||||
|
>
|
||||||
|
<div class="row q-gutter-x-sm">
|
||||||
|
<label for="fileInput">
|
||||||
|
<QBtn
|
||||||
|
@click="inputFile.nativeEl.click()"
|
||||||
|
icon="add"
|
||||||
|
color="primary"
|
||||||
|
dense
|
||||||
|
rounded
|
||||||
|
>
|
||||||
|
<QInput
|
||||||
|
ref="inputFile"
|
||||||
|
type="file"
|
||||||
|
style="display: none"
|
||||||
|
multiple
|
||||||
|
v-model="files"
|
||||||
|
@update:model-value="create()"
|
||||||
|
/>
|
||||||
|
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</label>
|
||||||
|
<QSeparator vertical />
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<QPageSticky
|
||||||
|
v-if="quasar.platform.is.mobile"
|
||||||
|
position="bottom"
|
||||||
|
:offset="[0, 0]"
|
||||||
|
expand
|
||||||
|
>
|
||||||
|
<QToolbar class="bg-primary text-white q-pa-none">
|
||||||
|
<QTabs class="full-width" align="justify" inline-label narrow-indicator>
|
||||||
|
<QTab
|
||||||
|
@click="inputFile.nativeEl.click()"
|
||||||
|
icon="add_circle"
|
||||||
|
:label="t('globals.add')"
|
||||||
|
>
|
||||||
|
<QInput
|
||||||
|
ref="inputFile"
|
||||||
|
type="file"
|
||||||
|
style="display: none"
|
||||||
|
multiple
|
||||||
|
v-model="files"
|
||||||
|
@update:model-value="create()"
|
||||||
|
/>
|
||||||
|
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
|
||||||
|
</QTab>
|
||||||
|
</QTabs>
|
||||||
|
</QToolbar>
|
||||||
|
</QPageSticky>
|
||||||
|
|
||||||
|
<!-- MULTIMEDIA DIALOG START-->
|
||||||
|
<QDialog
|
||||||
|
v-model="multimediaDialog"
|
||||||
|
transition-show="slide-up"
|
||||||
|
transition-hide="slide-down"
|
||||||
|
>
|
||||||
|
<QToolbar class="absolute zindex close-button">
|
||||||
|
<QSpace />
|
||||||
|
<QBtn icon="close" color="primary" round dense v-close-popup />
|
||||||
|
</QToolbar>
|
||||||
|
<QCarousel swipeable animated v-model="multimediaSlide" arrows class="fit">
|
||||||
|
<QCarouselSlide
|
||||||
|
v-for="media of claimDms"
|
||||||
|
:key="media.dmsFk"
|
||||||
|
:name="media.dmsFk"
|
||||||
|
>
|
||||||
|
<QImg
|
||||||
|
:src="media.url"
|
||||||
|
class="fit"
|
||||||
|
fit="scale-down"
|
||||||
|
v-if="!media.isVideo"
|
||||||
|
/>
|
||||||
|
<video class="q-ma-none fit" v-if="media.isVideo" controls muted autoplay>
|
||||||
|
<source :src="media.url" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
</QCarouselSlide>
|
||||||
|
</QCarousel>
|
||||||
|
</QDialog>
|
||||||
|
<!-- MULTIMEDIA DIALOG END-->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 80vh;
|
||||||
|
min-width: 80%;
|
||||||
|
}
|
||||||
|
.q-dialog__inner--minimized > div {
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
.multimediaParent {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
|
||||||
|
grid-gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multimedia {
|
||||||
|
transition: all 0.5s;
|
||||||
|
opacity: 1;
|
||||||
|
height: 250px;
|
||||||
|
|
||||||
|
.q-img {
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
video {
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multimedia:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
top: 1%;
|
||||||
|
right: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zindex {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragFile {
|
||||||
|
border: 2px dashed $color-spacer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
This file will be deleted: Este archivo va a ser borrado
|
||||||
|
</i18n>
|
|
@ -1,25 +1,32 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import axios from 'axios';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import axios from 'axios';
|
import { useArrayData } from 'src/composables/useArrayData';
|
||||||
import Paginate from 'src/components/PaginateData.vue';
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
|
import VnPaginate from 'src/components/ui/VnPaginate.vue';
|
||||||
import FetchData from 'components/FetchData.vue';
|
import FetchData from 'components/FetchData.vue';
|
||||||
import TeleportSlot from 'components/ui/TeleportSlot';
|
import VnConfirm from 'src/components/ui/VnConfirm.vue';
|
||||||
|
|
||||||
import { toDate } from 'src/filters';
|
import { toDate } from 'src/filters';
|
||||||
|
|
||||||
const quasar = useQuasar();
|
const quasar = useQuasar();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const stateStore = useStateStore();
|
||||||
|
const arrayData = useArrayData('ClaimRma');
|
||||||
|
|
||||||
const claim = ref([]);
|
const claim = ref();
|
||||||
const fetcher = ref();
|
const claimFilter = {
|
||||||
|
fields: ['rma'],
|
||||||
|
};
|
||||||
|
|
||||||
|
async function onFetch(data) {
|
||||||
|
claim.value = data;
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
include: {
|
|
||||||
relation: 'rmas',
|
|
||||||
scope: {
|
|
||||||
include: {
|
include: {
|
||||||
relation: 'worker',
|
relation: 'worker',
|
||||||
scope: {
|
scope: {
|
||||||
|
@ -29,17 +36,27 @@ const filter = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
order: 'created DESC',
|
order: 'created DESC',
|
||||||
},
|
where: {
|
||||||
|
code: claim.value.rma,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
arrayData.applyFilter({ filter });
|
||||||
|
}
|
||||||
|
|
||||||
async function addRow() {
|
async function addRow() {
|
||||||
|
if (!claim.value.rma) {
|
||||||
|
return quasar.notify({
|
||||||
|
message: `This claim is not associated to any RMA`,
|
||||||
|
type: 'negative',
|
||||||
|
});
|
||||||
|
}
|
||||||
const formData = {
|
const formData = {
|
||||||
code: claim.value.rma,
|
code: claim.value.rma,
|
||||||
};
|
};
|
||||||
|
|
||||||
await axios.post(`ClaimRmas`, formData);
|
await axios.post(`ClaimRmas`, formData);
|
||||||
await fetcher.value.fetch();
|
await arrayData.refresh();
|
||||||
|
|
||||||
quasar.notify({
|
quasar.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
|
@ -48,104 +65,119 @@ async function addRow() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmShown = ref(false);
|
|
||||||
const rmaId = ref(null);
|
|
||||||
function confirmRemove(id) {
|
function confirmRemove(id) {
|
||||||
confirmShown.value = true;
|
quasar
|
||||||
rmaId.value = id;
|
.dialog({
|
||||||
|
component: VnConfirm,
|
||||||
|
componentProps: {
|
||||||
|
data: { id },
|
||||||
|
promise: remove,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.onOk(async () => await arrayData.refresh());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove() {
|
async function remove({ id }) {
|
||||||
const id = rmaId.value;
|
|
||||||
|
|
||||||
await axios.delete(`ClaimRmas/${id}`);
|
await axios.delete(`ClaimRmas/${id}`);
|
||||||
await fetcher.value.fetch();
|
|
||||||
confirmShown.value = false;
|
|
||||||
|
|
||||||
quasar.notify({
|
quasar.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: t('globals.rowRemoved'),
|
message: t('globals.rowRemoved'),
|
||||||
icon: 'check',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
|
||||||
rmaId.value = null;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<fetch-data
|
<FetchData
|
||||||
ref="fetcher"
|
|
||||||
:url="`Claims/${route.params.id}`"
|
:url="`Claims/${route.params.id}`"
|
||||||
:filter="filter"
|
:filter="claimFilter"
|
||||||
@on-fetch="(data) => (claim = data)"
|
@on-fetch="onFetch"
|
||||||
auto-load
|
auto-load
|
||||||
/>
|
/>
|
||||||
<paginate :data="claim.rmas">
|
<div class="column items-center">
|
||||||
|
<div class="list">
|
||||||
|
<VnPaginate data-key="ClaimRma" url="ClaimRmas">
|
||||||
<template #body="{ rows }">
|
<template #body="{ rows }">
|
||||||
<q-card class="card">
|
<QCard class="card">
|
||||||
<template v-for="row of rows" :key="row.id">
|
<template v-for="(row, index) of rows" :key="row.id">
|
||||||
<q-item class="q-pa-none items-start">
|
<QItem class="q-pa-none items-start">
|
||||||
<q-item-section class="q-pa-md">
|
<QItemSection class="q-pa-md">
|
||||||
<q-list>
|
<QList>
|
||||||
<q-item class="q-pa-none">
|
<QItem class="q-pa-none">
|
||||||
<q-item-section>
|
<QItemSection>
|
||||||
<q-item-label caption>{{ t('claim.rma.user') }}</q-item-label>
|
<QItemLabel caption>
|
||||||
<q-item-label>{{ row.worker.user.name }}</q-item-label>
|
{{ t('claim.rma.user') }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel>
|
||||||
<q-item class="q-pa-none">
|
{{ row.worker.user.name }}
|
||||||
<q-item-section>
|
</QItemLabel>
|
||||||
<q-item-label caption>{{ t('claim.rma.created') }}</q-item-label>
|
</QItemSection>
|
||||||
<q-item-label>
|
</QItem>
|
||||||
{{ toDate(row.created, { timeStyle: 'medium' }) }}
|
<QItem class="q-pa-none">
|
||||||
</q-item-label>
|
<QItemSection>
|
||||||
</q-item-section>
|
<QItemLabel caption>
|
||||||
</q-item>
|
{{ t('claim.rma.created') }}
|
||||||
</q-list>
|
</QItemLabel>
|
||||||
</q-item-section>
|
<QItemLabel>
|
||||||
<q-card-actions vertical class="justify-between">
|
{{
|
||||||
<q-btn flat round color="orange" icon="vn:bin" @click="confirmRemove(row.id)">
|
toDate(row.created, {
|
||||||
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
|
timeStyle: 'medium',
|
||||||
</q-btn>
|
})
|
||||||
</q-card-actions>
|
}}
|
||||||
</q-item>
|
</QItemLabel>
|
||||||
<q-separator />
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</QItemSection>
|
||||||
|
<QCardActions vertical class="justify-between">
|
||||||
|
<QBtn
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
color="orange"
|
||||||
|
icon="vn:bin"
|
||||||
|
@click="confirmRemove(row.id)"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('globals.remove') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</QCardActions>
|
||||||
|
</QItem>
|
||||||
|
<QSeparator v-if="index !== rows.length - 1" />
|
||||||
</template>
|
</template>
|
||||||
</q-card>
|
</QCard>
|
||||||
</template>
|
</template>
|
||||||
</paginate>
|
</VnPaginate>
|
||||||
|
</div>
|
||||||
<q-dialog v-model="confirmShown" persistent @hide="hide">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section class="row items-center">
|
|
||||||
<q-avatar icon="warning" color="primary" text-color="white" />
|
|
||||||
<span class="q-ml-sm">{{ t('globals.confirmRemove') }}</span>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn flat :label="t('globals.no')" color="primary" v-close-popup autofocus />
|
|
||||||
<q-btn flat :label="t('globals.yes')" color="primary" @click="remove()" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<teleport-slot v-if="!quasar.platform.is.mobile" to="#header-actions">
|
|
||||||
<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>
|
</div>
|
||||||
</teleport-slot>
|
|
||||||
|
|
||||||
<teleport-slot to=".q-footer">
|
<Teleport
|
||||||
<q-tabs align="justify" inline-label narrow-indicator>
|
v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile"
|
||||||
<q-tab @click="addRow()" icon="add_circle" :label="t('globals.add')" />
|
to="#actions-prepend"
|
||||||
</q-tabs>
|
>
|
||||||
</teleport-slot>
|
<div class="row q-gutter-x-sm">
|
||||||
|
<QBtn @click="addRow()" icon="add" color="primary" dense rounded>
|
||||||
|
<QTooltip bottom> {{ t('globals.add') }} </QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QSeparator vertical />
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<QPageSticky
|
||||||
|
v-if="quasar.platform.is.mobile"
|
||||||
|
position="bottom"
|
||||||
|
:offset="[0, 0]"
|
||||||
|
expand
|
||||||
|
>
|
||||||
|
<QToolbar class="bg-primary text-white q-pa-none">
|
||||||
|
<QTabs class="full-width" align="justify" inline-label narrow-indicator>
|
||||||
|
<QTab @click="addRow()" icon="add_circle" :label="t('globals.add')" />
|
||||||
|
</QTabs>
|
||||||
|
</QToolbar>
|
||||||
|
</QPageSticky>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.list {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60em;
|
||||||
|
}
|
||||||
.q-toolbar {
|
.q-toolbar {
|
||||||
background-color: $grey-9;
|
background-color: $grey-9;
|
||||||
}
|
}
|
||||||
|
@ -157,3 +189,8 @@ function hide() {
|
||||||
z-index: 2998;
|
z-index: 2998;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
This claim is not associated to any RMA: Esta reclamación no está asociada a ninguna ARM
|
||||||
|
</i18n>
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import axios from 'axios';
|
|
||||||
import { toDate, toCurrency } from 'src/filters';
|
import { toDate, toCurrency } from 'src/filters';
|
||||||
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
|
import CardSummary from 'components/ui/CardSummary.vue';
|
||||||
|
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
|
||||||
onMounted(() => fetch());
|
import FetchData from 'components/FetchData.vue';
|
||||||
|
import { useSession } from 'src/composables/useSession';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const session = useSession();
|
||||||
|
const token = session.getToken();
|
||||||
|
|
||||||
const $props = defineProps({
|
const $props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
|
@ -20,16 +22,6 @@ const $props = defineProps({
|
||||||
|
|
||||||
const entityId = computed(() => $props.id || route.params.id);
|
const entityId = computed(() => $props.id || route.params.id);
|
||||||
|
|
||||||
const claim = ref(null);
|
|
||||||
const salesClaimed = ref(null);
|
|
||||||
function fetch() {
|
|
||||||
const id = entityId.value;
|
|
||||||
axios.get(`Claims/${id}/getSummary`).then(({ data }) => {
|
|
||||||
claim.value = data.claim;
|
|
||||||
salesClaimed.value = data.salesClaimed;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const detailsColumns = ref([
|
const detailsColumns = ref([
|
||||||
{
|
{
|
||||||
name: 'item',
|
name: 'item',
|
||||||
|
@ -77,7 +69,8 @@ const detailsColumns = ref([
|
||||||
{
|
{
|
||||||
name: 'total',
|
name: 'total',
|
||||||
label: 'claim.summary.total',
|
label: 'claim.summary.total',
|
||||||
field: ({ sale }) => toCurrency(sale.quantity * sale.price * ((100 - sale.discount) / 100)),
|
field: ({ sale }) =>
|
||||||
|
toCurrency(sale.quantity * sale.price * ((100 - sale.discount) / 100)),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -87,57 +80,219 @@ function stateColor(code) {
|
||||||
if (code === 'managed') return 'orange';
|
if (code === 'managed') return 'orange';
|
||||||
if (code === 'resolved') return 'red';
|
if (code === 'resolved') return 'red';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const developmentColumns = ref([
|
||||||
|
{
|
||||||
|
name: 'claimReason',
|
||||||
|
label: 'claim.summary.reason',
|
||||||
|
field: (row) => row.claimReason.description,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'claimResult',
|
||||||
|
label: 'claim.summary.result',
|
||||||
|
field: (row) => row.claimResult.description,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'claimResponsible',
|
||||||
|
label: 'claim.summary.responsible',
|
||||||
|
field: (row) => row.claimResponsible.description,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'worker',
|
||||||
|
label: 'claim.summary.worker',
|
||||||
|
field: (row) => row.worker.user.nickname,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'claimRedelivery',
|
||||||
|
label: 'claim.summary.redelivery',
|
||||||
|
field: (row) => row.claimRedelivery.description,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const claimDms = ref([]);
|
||||||
|
const multimediaDialog = ref();
|
||||||
|
const multimediaSlide = ref();
|
||||||
|
const claimDmsFilter = ref({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
relation: 'dms',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
where: { claimFk: entityId.value },
|
||||||
|
});
|
||||||
|
|
||||||
|
function setClaimDms(data) {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
data.forEach((media) => {
|
||||||
|
claimDms.value.push({
|
||||||
|
isVideo: media.dms.contentType == 'video/mp4',
|
||||||
|
url: `/api/Claims/${media.dmsFk}/downloadFile?access_token=${token}`,
|
||||||
|
dmsFk: media.dmsFk,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDialog(dmsId) {
|
||||||
|
multimediaSlide.value = dmsId;
|
||||||
|
multimediaDialog.value = true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="summary container">
|
<FetchData
|
||||||
<q-card>
|
url="ClaimDms"
|
||||||
<skeleton-summary v-if="!claim" />
|
:filter="claimDmsFilter"
|
||||||
<template v-if="claim">
|
@on-fetch="(data) => setClaimDms(data)"
|
||||||
<div class="header bg-primary q-pa-sm q-mb-md">{{ claim.id }} - {{ claim.client.name }}</div>
|
limit="20"
|
||||||
<q-list>
|
auto-load
|
||||||
<q-item>
|
/>
|
||||||
<q-item-section>
|
<CardSummary ref="summary" :url="`Claims/${entityId}/getSummary`">
|
||||||
<q-item-label caption>{{ t('claim.summary.created') }}</q-item-label>
|
<template #header="{ entity: { claim } }">
|
||||||
<q-item-label>{{ toDate(claim.created) }}</q-item-label>
|
{{ claim.id }} - {{ claim.client.name }}
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label caption>{{ t('claim.summary.state') }}</q-item-label>
|
|
||||||
<q-item-label>
|
|
||||||
<q-chip :color="stateColor(claim.claimState.code)" dense>
|
|
||||||
{{ claim.claimState.description }}
|
|
||||||
</q-chip>
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label caption>{{ t('claim.summary.assignedTo') }}</q-item-label>
|
|
||||||
<q-item-label>{{ claim.worker.user.nickname }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label caption>{{ t('claim.summary.attendedBy') }}</q-item-label>
|
|
||||||
<q-item-label>{{ claim.client.salesPersonUser.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
<q-card-section class="q-pa-md">
|
|
||||||
<h6>{{ t('claim.summary.details') }}</h6>
|
|
||||||
<q-table :columns="detailsColumns" :rows="salesClaimed" 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>
|
</template>
|
||||||
</q-table>
|
<template #body="{ entity: { developments, observations, claim, salesClaimed } }">
|
||||||
</q-card-section>
|
<QCardSection class="row q-pa-none q-col-gutter-md">
|
||||||
<q-card-section class="q-pa-md">
|
<div class="col">
|
||||||
|
<QList>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('claim.summary.created') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>{{ toDate(claim.created) }}</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="claim.claimState">
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('claim.summary.state') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>
|
||||||
|
<QChip
|
||||||
|
:color="stateColor(claim.claimState.code)"
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
{{ claim.claimState.description }}
|
||||||
|
</QChip>
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection v-if="claim.worker && claim.worker.user">
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('claim.summary.assignedTo') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>
|
||||||
|
<span class="link">
|
||||||
|
{{ claim.worker.user.nickname }}
|
||||||
|
<WorkerDescriptorProxy :id="claim.workerFk" />
|
||||||
|
</span>
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection
|
||||||
|
v-if="claim.client && claim.client.salesPersonUser"
|
||||||
|
>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('claim.summary.attendedBy') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>
|
||||||
|
<span class="link">
|
||||||
|
{{ claim.client.salesPersonUser.name }}
|
||||||
|
<WorkerDescriptorProxy
|
||||||
|
:id="claim.client.salesPersonFk"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</div>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection class="q-pa-md" v-if="observations.length > 0">
|
||||||
|
<h6>{{ t('claim.summary.notes') }}</h6>
|
||||||
|
<div class="note-list" v-for="note in observations" :key="note.id">
|
||||||
|
<div class="note-caption">
|
||||||
|
<span
|
||||||
|
>{{ note.worker.firstName }} {{ note.worker.lastName }}
|
||||||
|
</span>
|
||||||
|
<span>{{ toDate(note.created) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="note-text">
|
||||||
|
<span>{{ note.text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection class="q-pa-md" v-if="salesClaimed.length > 0">
|
||||||
|
<h6>{{ t('claim.summary.details') }}</h6>
|
||||||
|
<QTable :columns="detailsColumns" :rows="salesClaimed" flat>
|
||||||
|
<template #header="props">
|
||||||
|
<QTr :props="props">
|
||||||
|
<QTh v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ t(col.label) }}
|
||||||
|
</QTh>
|
||||||
|
</QTr>
|
||||||
|
</template>
|
||||||
|
</QTable>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection class="q-pa-md" v-if="claimDms.length > 0">
|
||||||
|
<h6>{{ t('claim.summary.photos') }}</h6>
|
||||||
|
<div class="container">
|
||||||
|
<div class="multimediaParent bg-transparent">
|
||||||
|
<div
|
||||||
|
v-for="(media, index) of claimDms"
|
||||||
|
:key="index"
|
||||||
|
class="relative-position"
|
||||||
|
>
|
||||||
|
<QIcon
|
||||||
|
name="play_circle"
|
||||||
|
color="primary"
|
||||||
|
size="xl"
|
||||||
|
class="absolute-center zindex"
|
||||||
|
v-if="media.isVideo"
|
||||||
|
@click.stop="openDialog(media.dmsFk)"
|
||||||
|
>
|
||||||
|
<QTooltip>Video</QTooltip>
|
||||||
|
</QIcon>
|
||||||
|
<QCard class="multimedia relative-position">
|
||||||
|
<QImg
|
||||||
|
:src="media.url"
|
||||||
|
class="rounded-borders cursor-pointer fit"
|
||||||
|
@click="openDialog(media.dmsFk)"
|
||||||
|
v-if="!media.isVideo"
|
||||||
|
>
|
||||||
|
</QImg>
|
||||||
|
<video
|
||||||
|
:src="media.url"
|
||||||
|
class="rounded-borders cursor-pointer fit"
|
||||||
|
muted="muted"
|
||||||
|
v-if="media.isVideo"
|
||||||
|
@click="openDialog(media.dmsFk)"
|
||||||
|
/>
|
||||||
|
</QCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection class="q-pa-md" v-if="developments.length > 0">
|
||||||
|
<h6>{{ t('claim.summary.development') }}</h6>
|
||||||
|
<QTable :columns="developmentColumns" :rows="developments" flat>
|
||||||
|
<template #header="props">
|
||||||
|
<QTr :props="props">
|
||||||
|
<QTh v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ t(col.label) }}
|
||||||
|
</QTh>
|
||||||
|
</QTr>
|
||||||
|
</template>
|
||||||
|
</QTable>
|
||||||
|
</QCardSection>
|
||||||
|
<QCardSection class="q-pa-md">
|
||||||
<h6>{{ t('claim.summary.actions') }}</h6>
|
<h6>{{ t('claim.summary.actions') }}</h6>
|
||||||
<q-separator />
|
<QSeparator />
|
||||||
<div id="slider-container">
|
<div id="slider-container">
|
||||||
<q-slider
|
<QSlider
|
||||||
v-model="claim.responsibility"
|
v-model="claim.responsibility"
|
||||||
label
|
label
|
||||||
:label-value="t('claim.summary.responsibility')"
|
:label-value="t('claim.summary.responsibility')"
|
||||||
|
@ -153,47 +308,105 @@ function stateColor(code) {
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</QCardSection>
|
||||||
|
<QDialog
|
||||||
|
v-model="multimediaDialog"
|
||||||
|
transition-show="slide-up"
|
||||||
|
transition-hide="slide-down"
|
||||||
|
>
|
||||||
|
<QToolbar class="absolute zindex close-button">
|
||||||
|
<QSpace />
|
||||||
|
<QBtn icon="close" color="primary" round dense v-close-popup />
|
||||||
|
</QToolbar>
|
||||||
|
<QCarousel
|
||||||
|
swipeable
|
||||||
|
animated
|
||||||
|
v-model="multimediaSlide"
|
||||||
|
arrows
|
||||||
|
class="fit"
|
||||||
|
>
|
||||||
|
<QCarouselSlide
|
||||||
|
v-for="media of claimDms"
|
||||||
|
:key="media.dmsFk"
|
||||||
|
:name="media.dmsFk"
|
||||||
|
>
|
||||||
|
<QImg
|
||||||
|
:src="media.url"
|
||||||
|
class="fit"
|
||||||
|
fit="scale-down"
|
||||||
|
v-if="!media.isVideo"
|
||||||
|
/>
|
||||||
|
<video
|
||||||
|
class="q-ma-none fit"
|
||||||
|
v-if="media.isVideo"
|
||||||
|
controls
|
||||||
|
muted
|
||||||
|
autoplay
|
||||||
|
>
|
||||||
|
<source :src="media.url" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
</QCarouselSlide>
|
||||||
|
</QCarousel>
|
||||||
|
</QDialog>
|
||||||
</template>
|
</template>
|
||||||
</q-card>
|
</CardSummary>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
min-width: 80%;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
.q-dialog__inner--minimized > div {
|
||||||
.q-card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 950px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary {
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#slider-container {
|
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
margin: 0 auto;
|
}
|
||||||
|
|
||||||
.q-slider {
|
.multimediaParent {
|
||||||
.q-slider__marker-labels:nth-child(1) {
|
display: grid;
|
||||||
transform: none;
|
grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
|
||||||
}
|
|
||||||
.q-slider__marker-labels:nth-child(2) {
|
grid-auto-rows: auto;
|
||||||
transform: none;
|
|
||||||
left: auto !important;
|
grid-gap: 1rem;
|
||||||
right: 0%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multimedia {
|
||||||
|
transition: all 0.5s;
|
||||||
|
opacity: 1;
|
||||||
|
height: 250px;
|
||||||
|
|
||||||
|
.q-img {
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
video {
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-dialog .summary {
|
.multimedia:hover {
|
||||||
max-width: 1200px;
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
top: 1%;
|
||||||
|
right: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zindex {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-list {
|
||||||
|
width: 100%;
|
||||||
|
border: 0.1rem solid $grey-7;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-caption {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: $grey-7;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -15,9 +15,9 @@ const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
<QDialog ref="dialogRef" @hide="onDialogHide">
|
||||||
<claim-summary v-if="$props.id" :id="$props.id" />
|
<ClaimSummary v-if="$props.id" :id="$props.id" />
|
||||||
</q-dialog>
|
</QDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -0,0 +1,217 @@
|
||||||
|
<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();
|
||||||
|
const states = ref();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FetchData url="ClaimStates" @on-fetch="(data) => (states = data)" auto-load />
|
||||||
|
<FetchData
|
||||||
|
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 }">
|
||||||
|
<QList dense>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput
|
||||||
|
:label="t('Customer ID')"
|
||||||
|
v-model="params.clientFk"
|
||||||
|
lazy-rules
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="badge" size="sm"></QIcon>
|
||||||
|
</template>
|
||||||
|
</QInput>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput
|
||||||
|
:label="t('Client Name')"
|
||||||
|
v-model="params.clientName"
|
||||||
|
lazy-rules
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection v-if="!workers">
|
||||||
|
<QSkeleton type="QInput" class="full-width" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="workers">
|
||||||
|
<QSelect
|
||||||
|
:label="t('Salesperson')"
|
||||||
|
v-model="params.salesPersonFk"
|
||||||
|
@update:model-value="searchFn()"
|
||||||
|
:options="workers"
|
||||||
|
option-value="id"
|
||||||
|
option-label="name"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-input
|
||||||
|
:input-debounce="0"
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection v-if="!workers">
|
||||||
|
<QSkeleton type="QInput" class="full-width" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="workers">
|
||||||
|
<QSelect
|
||||||
|
:label="t('Attender')"
|
||||||
|
v-model="params.attenderFk"
|
||||||
|
@update:model-value="searchFn()"
|
||||||
|
:options="workers"
|
||||||
|
option-value="id"
|
||||||
|
option-label="name"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-input
|
||||||
|
:input-debounce="0"
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection v-if="!workers">
|
||||||
|
<QSkeleton type="QInput" class="full-width" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="workers">
|
||||||
|
<QSelect
|
||||||
|
:label="t('Responsible')"
|
||||||
|
v-model="params.claimResponsibleFk"
|
||||||
|
@update:model-value="searchFn()"
|
||||||
|
:options="workers"
|
||||||
|
option-value="id"
|
||||||
|
option-label="name"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-input
|
||||||
|
:input-debounce="0"
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem class="q-mb-md">
|
||||||
|
<QItemSection v-if="!states">
|
||||||
|
<QSkeleton type="QInput" class="full-width" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="states">
|
||||||
|
<QSelect
|
||||||
|
:label="t('State')"
|
||||||
|
v-model="params.claimStateFk"
|
||||||
|
@update:model-value="searchFn()"
|
||||||
|
:options="states"
|
||||||
|
option-value="id"
|
||||||
|
option-label="description"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QSeparator />
|
||||||
|
<QExpansionItem :label="t('More options')" expand-separator>
|
||||||
|
<!-- <QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<qSelect
|
||||||
|
:label="t('Item')"
|
||||||
|
v-model="params.itemFk"
|
||||||
|
:options="items"
|
||||||
|
:loading="loading"
|
||||||
|
@filter="filterFn"
|
||||||
|
@virtual-scroll="onScroll"
|
||||||
|
option-value="id"
|
||||||
|
option-label="name"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem> -->
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput
|
||||||
|
v-model="params.created"
|
||||||
|
:label="t('Created')"
|
||||||
|
autofocus
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<QIcon name="event" class="cursor-pointer">
|
||||||
|
<QPopupProxy
|
||||||
|
cover
|
||||||
|
transition-show="scale"
|
||||||
|
transition-hide="scale"
|
||||||
|
>
|
||||||
|
<QDate v-model="params.created">
|
||||||
|
<div class="row items-center justify-end">
|
||||||
|
<QBtn
|
||||||
|
v-close-popup
|
||||||
|
label="Close"
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</QDate>
|
||||||
|
</QPopupProxy>
|
||||||
|
</QIcon>
|
||||||
|
</template>
|
||||||
|
</QInput>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QExpansionItem>
|
||||||
|
</QList>
|
||||||
|
</template>
|
||||||
|
</VnFilterPanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
en:
|
||||||
|
params:
|
||||||
|
search: Contains
|
||||||
|
clientFk: Customer
|
||||||
|
clientName: Customer
|
||||||
|
salesPersonFk: Salesperson
|
||||||
|
attenderFk: Attender
|
||||||
|
claimResponsibleFk: Responsible
|
||||||
|
claimStateFk: State
|
||||||
|
created: Created
|
||||||
|
es:
|
||||||
|
params:
|
||||||
|
search: Contiene
|
||||||
|
clientFk: Cliente
|
||||||
|
clientName: Cliente
|
||||||
|
salesPersonFk: Comercial
|
||||||
|
attenderFk: Asistente
|
||||||
|
claimResponsibleFk: Responsable
|
||||||
|
claimStateFk: Estado
|
||||||
|
created: Creada
|
||||||
|
Customer ID: ID cliente
|
||||||
|
Client Name: Nombre del cliente
|
||||||
|
Salesperson: Comercial
|
||||||
|
Attender: Asistente
|
||||||
|
Responsible: Responsable
|
||||||
|
State: Estado
|
||||||
|
Item: Artículo
|
||||||
|
Created: Creada
|
||||||
|
More options: Más opciones
|
||||||
|
</i18n>
|
|
@ -2,32 +2,19 @@
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import Paginate from 'src/components/PaginateData.vue';
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
import { toDate } from 'src/filters/index';
|
import { toDate } from 'filters/index';
|
||||||
|
import VnPaginate from 'src/components/ui/VnPaginate.vue';
|
||||||
import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue';
|
import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue';
|
||||||
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
|
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
|
||||||
|
import VnSearchbar from 'components/ui/VnSearchbar.vue';
|
||||||
|
import ClaimFilter from './ClaimFilter.vue';
|
||||||
|
|
||||||
|
const stateStore = useStateStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const quasar = useQuasar();
|
const quasar = useQuasar();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const filter = {
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
relation: 'client',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
relation: 'claimState',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
relation: 'worker',
|
|
||||||
scope: {
|
|
||||||
include: { relation: 'user' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
function stateColor(code) {
|
function stateColor(code) {
|
||||||
if (code === 'pending') return 'green';
|
if (code === 'pending') return 'green';
|
||||||
if (code === 'managed') return 'orange';
|
if (code === 'managed') return 'orange';
|
||||||
|
@ -49,84 +36,169 @@ function viewSummary(id) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-page class="q-pa-md">
|
<template v-if="stateStore.isHeaderMounted()">
|
||||||
<paginate url="/Claims" :filter="filter" sort-by="id DESC" auto-load>
|
<Teleport to="#searchbar">
|
||||||
<template #body="{ rows }">
|
<VnSearchbar
|
||||||
<q-card class="card" v-for="row of rows" :key="row.id">
|
data-key="ClaimList"
|
||||||
<q-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
|
:label="t('Search claim')"
|
||||||
<q-item-section class="q-pa-md" @click="navigate(row.id)">
|
:info="t('You can search by claim id or customer name')"
|
||||||
<div class="text-h6 link">
|
/>
|
||||||
{{ row.client.name }}
|
</Teleport>
|
||||||
<q-popup-proxy>
|
<Teleport to="#actions-append">
|
||||||
<customer-descriptor-popover :customer="row.client" />
|
<div class="row q-gutter-x-sm">
|
||||||
</q-popup-proxy>
|
<QBtn
|
||||||
|
flat
|
||||||
|
@click="stateStore.toggleRightDrawer()"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
icon="menu"
|
||||||
|
>
|
||||||
|
<QTooltip bottom anchor="bottom right">
|
||||||
|
{{ t('globals.collapseMenu') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
<q-item-label caption>#{{ row.id }}</q-item-label>
|
</Teleport>
|
||||||
<q-list>
|
</template>
|
||||||
<q-item class="q-pa-none">
|
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
|
||||||
<q-item-section>
|
<QScrollArea class="fit text-grey-8">
|
||||||
<q-item-label caption>{{ t('claim.list.customer') }}</q-item-label>
|
<ClaimFilter data-key="ClaimList" />
|
||||||
<q-item-label>{{ row.client.name }}</q-item-label>
|
</QScrollArea>
|
||||||
</q-item-section>
|
</QDrawer>
|
||||||
<q-item-section>
|
<QPage class="column items-center q-pa-md">
|
||||||
<q-item-label caption>{{ t('claim.list.assignedTo') }}</q-item-label>
|
<div class="card-list">
|
||||||
<q-item-label>{{ row.worker.user.name }}</q-item-label>
|
<VnPaginate
|
||||||
</q-item-section>
|
data-key="ClaimList"
|
||||||
</q-item>
|
url="Claims/filter"
|
||||||
<q-item class="q-pa-none">
|
order="id DESC"
|
||||||
<q-item-section>
|
auto-load
|
||||||
<q-item-label caption>{{ t('claim.list.created') }}</q-item-label>
|
>
|
||||||
<q-item-label>{{ toDate(row.created) }}</q-item-label>
|
<template #body="{ rows }">
|
||||||
</q-item-section>
|
<QCard class="card q-mb-md" v-for="row of rows" :key="row.id">
|
||||||
<q-item-section>
|
<QItem
|
||||||
<q-item-label caption>{{ t('claim.list.state') }}</q-item-label>
|
class="q-pa-none items-start cursor-pointer q-hoverable"
|
||||||
<q-item-label>
|
v-ripple
|
||||||
<q-chip :color="stateColor(row.claimState.code)" dense>
|
clickable
|
||||||
{{ row.claimState.description }}
|
>
|
||||||
</q-chip>
|
<QItemSection class="q-pa-md" @click="navigate(row.id)">
|
||||||
</q-item-label>
|
<div class="text-h6 link">
|
||||||
</q-item-section>
|
{{ row.clientName }}
|
||||||
</q-item>
|
</div>
|
||||||
</q-list>
|
<QItemLabel caption>#{{ row.id }}</QItemLabel>
|
||||||
</q-item-section>
|
<QList>
|
||||||
<q-separator vertical />
|
<QItem class="q-pa-none">
|
||||||
<q-card-actions vertical class="justify-between">
|
<QItemSection>
|
||||||
<!-- <q-btn color="grey-7" round flat icon="more_vert">
|
<QItemLabel caption>
|
||||||
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
|
{{ t('claim.list.customer') }}
|
||||||
<q-menu cover auto-close>
|
</QItemLabel>
|
||||||
<q-list>
|
<QItemLabel>
|
||||||
<q-item clickable>
|
{{ row.clientName }}
|
||||||
<q-item-section avatar>
|
</QItemLabel>
|
||||||
<q-icon name="add" />
|
</QItemSection>
|
||||||
</q-item-section>
|
<QItemSection>
|
||||||
<q-item-section>Add a note</q-item-section>
|
<QItemLabel caption>
|
||||||
</q-item>
|
{{ t('claim.list.assignedTo') }}
|
||||||
<q-item clickable>
|
</QItemLabel>
|
||||||
<q-item-section avatar>
|
<QItemLabel>
|
||||||
<q-icon name="logs" />
|
{{ row.workerName }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
<q-item-section>Display claim logs</q-item-section>
|
</QItemSection>
|
||||||
</q-item>
|
</QItem>
|
||||||
</q-list>
|
<QItem class="q-pa-none">
|
||||||
</q-menu>
|
<QItemSection>
|
||||||
</q-btn> -->
|
<QItemLabel caption>
|
||||||
|
{{ t('claim.list.created') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>
|
||||||
|
{{ toDate(row.created) }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('claim.list.state') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>
|
||||||
|
<QBadge
|
||||||
|
:color="stateColor(row.stateCode)"
|
||||||
|
class="q-ma-none"
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
{{ row.stateDescription }}
|
||||||
|
</QBadge>
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</QItemSection>
|
||||||
|
<QSeparator vertical />
|
||||||
|
<QCardActions vertical class="justify-between">
|
||||||
|
<!-- <QBtn color="grey-7" round flat icon="more_vert">
|
||||||
|
<QTooltip>{{ t('customer.list.moreOptions') }}</QTooltip>
|
||||||
|
<QMenu cover auto-close>
|
||||||
|
<QList>
|
||||||
|
<QItem clickable>
|
||||||
|
<QItemSection avatar>
|
||||||
|
<QIcon name="add" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection>Add a note</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem clickable>
|
||||||
|
<QItemSection avatar>
|
||||||
|
<QIcon name="logs" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection>Display claim logs</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</QMenu>
|
||||||
|
</QBtn> -->
|
||||||
|
|
||||||
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
|
<QBtn
|
||||||
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
|
flat
|
||||||
</q-btn>
|
round
|
||||||
<q-btn flat round color="grey-7" icon="preview" @click="viewSummary(row.id)">
|
color="orange"
|
||||||
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
|
icon="arrow_circle_right"
|
||||||
</q-btn>
|
@click="navigate(row.id)"
|
||||||
<q-btn flat round color="grey-7" icon="vn:client">
|
>
|
||||||
<q-tooltip>{{ t('components.smartCard.viewDescription') }}</q-tooltip>
|
<QTooltip>
|
||||||
<q-popup-proxy>
|
{{ t('components.smartCard.openCard') }}
|
||||||
<customer-descriptor-popover :customer="row.client" />
|
</QTooltip>
|
||||||
</q-popup-proxy>
|
</QBtn>
|
||||||
</q-btn>
|
<QBtn
|
||||||
</q-card-actions>
|
flat
|
||||||
</q-item>
|
round
|
||||||
</q-card>
|
color="grey-7"
|
||||||
|
icon="preview"
|
||||||
|
@click="viewSummary(row.id)"
|
||||||
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('components.smartCard.openSummary') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QBtn flat round color="grey-7" icon="vn:client">
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('components.smartCard.viewDescription') }}
|
||||||
|
</QTooltip>
|
||||||
|
|
||||||
|
<CustomerDescriptorProxy :id="row.clientFk" />
|
||||||
|
</QBtn>
|
||||||
|
</QCardActions>
|
||||||
|
</QItem>
|
||||||
|
</QCard>
|
||||||
</template>
|
</template>
|
||||||
</paginate>
|
</VnPaginate>
|
||||||
</q-page>
|
</div>
|
||||||
|
</QPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-list {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
Search claim: Buscar reclamación
|
||||||
|
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
|
||||||
|
</i18n>
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useState } from 'src/composables/useState';
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
import LeftMenu from 'components/LeftMenu.vue';
|
import LeftMenu from 'components/LeftMenu.vue';
|
||||||
|
|
||||||
const state = useState();
|
const stateStore = useStateStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||||
<q-scroll-area class="fit text-grey-8">
|
<QScrollArea class="fit text-grey-8">
|
||||||
<LeftMenu />
|
<LeftMenu />
|
||||||
</q-scroll-area>
|
</QScrollArea>
|
||||||
</q-drawer>
|
</QDrawer>
|
||||||
<q-page-container>
|
<QPageContainer>
|
||||||
<router-view></router-view>
|
<RouterView></RouterView>
|
||||||
</q-page-container>
|
</QPageContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -3,128 +3,153 @@ import { ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Paginate from 'src/components/PaginateData.vue';
|
import VnPaginate from 'src/components/ui/VnPaginate.vue';
|
||||||
|
import { useArrayData } from 'src/composables/useArrayData';
|
||||||
|
import VnConfirm from 'src/components/ui/VnConfirm.vue';
|
||||||
|
|
||||||
const quasar = useQuasar();
|
const quasar = useQuasar();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const rmas = ref([]);
|
const arrayData = useArrayData('ClaimRmaList');
|
||||||
const card = ref(null);
|
const isLoading = ref(false);
|
||||||
|
const input = ref();
|
||||||
function onFetch(data) {
|
|
||||||
rmas.value = data.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newRma = ref({
|
const newRma = ref({
|
||||||
code: '',
|
code: '',
|
||||||
crated: new Date(),
|
crated: Date.vnNew(),
|
||||||
});
|
});
|
||||||
|
|
||||||
function onInputUpdate(value) {
|
function onInputUpdate(value) {
|
||||||
newRma.value.code = value.toUpperCase();
|
newRma.value.code = value.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
async function submit() {
|
||||||
const formData = newRma.value;
|
const formData = newRma.value;
|
||||||
if (formData.code === '') return;
|
if (formData.code === '') return;
|
||||||
|
|
||||||
axios
|
isLoading.value = true;
|
||||||
.post('ClaimRmas', formData)
|
await axios.post('ClaimRmas', formData);
|
||||||
.then(() => {
|
await arrayData.refresh();
|
||||||
|
isLoading.value = false;
|
||||||
|
input.value.$el.focus();
|
||||||
|
|
||||||
newRma.value = {
|
newRma.value = {
|
||||||
code: '',
|
code: '',
|
||||||
crated: new Date(),
|
created: Date.vnNew(),
|
||||||
};
|
};
|
||||||
})
|
|
||||||
.then(() => card.value.refresh());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmShown = ref(false);
|
|
||||||
const rmaId = ref(null);
|
|
||||||
function confirm(id) {
|
function confirm(id) {
|
||||||
confirmShown.value = true;
|
quasar
|
||||||
rmaId.value = id;
|
.dialog({
|
||||||
|
component: VnConfirm,
|
||||||
|
componentProps: {
|
||||||
|
data: { id },
|
||||||
|
promise: remove,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.onOk(async () => await arrayData.refresh());
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove() {
|
async function remove({ id }) {
|
||||||
const id = rmaId.value;
|
await axios.delete(`ClaimRmas/${id}`);
|
||||||
axios
|
|
||||||
.delete(`ClaimRmas/${id}`)
|
|
||||||
.then(() => {
|
|
||||||
confirmShown.value = false;
|
|
||||||
|
|
||||||
quasar.notify({
|
quasar.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Entry deleted',
|
message: t('globals.rowRemoved'),
|
||||||
icon: 'check',
|
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.then(() => card.value.refresh());
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
rmaId.value = null;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-page class="q-pa-md sticky">
|
<QPage class="column items-center q-pa-md sticky">
|
||||||
<q-page-sticky expand position="top" :offset="[16, 16]">
|
<QPageSticky expand position="top" :offset="[16, 16]">
|
||||||
<q-card class="card q-pa-md">
|
<QCard class="card q-pa-md">
|
||||||
<q-form @submit="submit">
|
<QForm @submit="submit">
|
||||||
<q-input
|
<QInput
|
||||||
|
ref="input"
|
||||||
v-model="newRma.code"
|
v-model="newRma.code"
|
||||||
:label="t('claim.rmaList.code')"
|
:label="t('claim.rmaList.code')"
|
||||||
@update:model-value="onInputUpdate"
|
@update:model-value="onInputUpdate"
|
||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
|
:readonly="isLoading"
|
||||||
|
:loading="isLoading"
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
<div class="text-caption">{{ rmas.length }} {{ t('claim.rmaList.records') }}</div>
|
<div class="text-caption">
|
||||||
</q-form>
|
{{ arrayData.totalRows }} {{ t('claim.rmaList.records') }}
|
||||||
</q-card>
|
</div>
|
||||||
</q-page-sticky>
|
</QForm>
|
||||||
|
</QCard>
|
||||||
<paginate ref="card" url="/ClaimRmas" @on-fetch="onFetch" sort-by="id DESC" auto-load>
|
</QPageSticky>
|
||||||
|
<div class="card-list">
|
||||||
|
<VnPaginate
|
||||||
|
data-key="ClaimRmaList"
|
||||||
|
url="ClaimRmas"
|
||||||
|
order="id DESC"
|
||||||
|
:offset="50"
|
||||||
|
auto-load
|
||||||
|
>
|
||||||
<template #body="{ rows }">
|
<template #body="{ rows }">
|
||||||
<q-card class="card">
|
<QCard class="card">
|
||||||
<template v-for="row of rows" :key="row.code">
|
<template v-if="isLoading">
|
||||||
<q-item class="q-pa-none items-start">
|
<QItem class="q-pa-none items-start">
|
||||||
<q-item-section class="q-pa-md">
|
<QItemSection class="q-pa-md">
|
||||||
<q-list>
|
<QList>
|
||||||
<q-item class="q-pa-none">
|
<QItem class="q-pa-none">
|
||||||
<q-item-section>
|
<QItemSection>
|
||||||
<q-item-label caption>{{ t('claim.rmaList.code') }}</q-item-label>
|
<QItemLabel caption>
|
||||||
<q-item-label>{{ row.code }}</q-item-label>
|
<QSkeleton />
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel>
|
||||||
</q-list>
|
<QSkeleton type="text" />
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
<q-card-actions vertical class="justify-between">
|
</QItemSection>
|
||||||
<q-btn flat round color="primary" icon="vn:bin" @click="confirm(row.id)">
|
</QItem>
|
||||||
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
|
</QList>
|
||||||
</q-btn>
|
</QItemSection>
|
||||||
</q-card-actions>
|
<QCardActions vertical class="justify-between">
|
||||||
</q-item>
|
<QSkeleton
|
||||||
<q-separator />
|
type="circle"
|
||||||
|
class="q-mb-md"
|
||||||
|
size="40px"
|
||||||
|
/>
|
||||||
|
</QCardActions>
|
||||||
|
</QItem>
|
||||||
|
<QSeparator />
|
||||||
</template>
|
</template>
|
||||||
</q-card>
|
<template v-for="row of rows" :key="row.id">
|
||||||
|
<QItem class="q-pa-none items-start">
|
||||||
|
<QItemSection class="q-pa-md">
|
||||||
|
<QList>
|
||||||
|
<QItem class="q-pa-none">
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>{{
|
||||||
|
t('claim.rmaList.code')
|
||||||
|
}}</QItemLabel>
|
||||||
|
<QItemLabel>{{ row.code }}</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</QItemSection>
|
||||||
|
<QCardActions vertical class="justify-between">
|
||||||
|
<QBtn
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
color="primary"
|
||||||
|
icon="vn:bin"
|
||||||
|
@click="confirm(row.id)"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('globals.remove') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</QCardActions>
|
||||||
|
</QItem>
|
||||||
|
<QSeparator />
|
||||||
</template>
|
</template>
|
||||||
</paginate>
|
</QCard>
|
||||||
</q-page>
|
</template>
|
||||||
|
</VnPaginate>
|
||||||
<q-dialog v-model="confirmShown" persistent @hide="hide">
|
</div>
|
||||||
<q-card>
|
</QPage>
|
||||||
<q-card-section class="row items-center">
|
|
||||||
<q-avatar icon="warning" color="primary" text-color="white" />
|
|
||||||
<span class="q-ml-sm">{{ t('globals.confirmRemove') }}</span>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn flat :label="t('globals.no')" color="primary" v-close-popup autofocus />
|
|
||||||
<q-btn flat :label="t('globals.yes')" color="primary" @click="remove()" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -132,6 +157,7 @@ function hide() {
|
||||||
padding-top: 156px;
|
padding-top: 156px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-list,
|
||||||
.card {
|
.card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 60em;
|
max-width: 60em;
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import { jest, describe, expect, it, beforeAll } from '@jest/globals';
|
|
||||||
import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
|
|
||||||
import ClaimDescriptorMenu from '../Card/ClaimDescriptorMenu.vue';
|
|
||||||
|
|
||||||
const mockPush = jest.fn();
|
|
||||||
|
|
||||||
jest.mock('vue-router', () => ({
|
|
||||||
useRouter: () => ({
|
|
||||||
push: mockPush,
|
|
||||||
currentRoute: {
|
|
||||||
value: {
|
|
||||||
params: {
|
|
||||||
id: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('ClaimDescriptorMenu', () => {
|
|
||||||
let vm;
|
|
||||||
beforeAll(() => {
|
|
||||||
vm = createWrapper(ClaimDescriptorMenu, {
|
|
||||||
propsData: {
|
|
||||||
claim: {
|
|
||||||
id: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).vm;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteClaim()', () => {
|
|
||||||
it('should delete the claim', async () => {
|
|
||||||
jest.spyOn(axios, 'delete').mockResolvedValue({ data: true });
|
|
||||||
jest.spyOn(vm.quasar, 'notify');
|
|
||||||
|
|
||||||
await vm.deleteClaim();
|
|
||||||
|
|
||||||
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
|
|
||||||
{ 'type': 'positive' }
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -48,15 +48,23 @@ const filterOptions = {
|
||||||
@on-fetch="setWorkers"
|
@on-fetch="setWorkers"
|
||||||
auto-load
|
auto-load
|
||||||
/>
|
/>
|
||||||
<fetch-data url="ContactChannels" @on-fetch="(data) => contactChannels = data" auto-load />
|
<fetch-data
|
||||||
<fetch-data url="BusinessTypes" @on-fetch="(data) => businessTypes = data" auto-load />
|
url="ContactChannels"
|
||||||
<div class="container">
|
@on-fetch="(data) => (contactChannels = data)"
|
||||||
<q-card>
|
auto-load
|
||||||
<form-model :url="`Clients/${route.params.id}`" model="customer">
|
/>
|
||||||
|
<fetch-data
|
||||||
|
url="BusinessTypes"
|
||||||
|
@on-fetch="(data) => (businessTypes = data)"
|
||||||
|
auto-load
|
||||||
|
/>
|
||||||
|
<div class="column items-center">
|
||||||
|
<QCard>
|
||||||
|
<FormModel :url="`Clients/${route.params.id}`" model="customer">
|
||||||
<template #form="{ data, validate, filter }">
|
<template #form="{ data, validate, filter }">
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-input
|
<QInput
|
||||||
v-model="data.socialName"
|
v-model="data.socialName"
|
||||||
:label="t('customer.basicData.socialName')"
|
:label="t('customer.basicData.socialName')"
|
||||||
:rules="validate('client.socialName')"
|
:rules="validate('client.socialName')"
|
||||||
|
@ -64,7 +72,7 @@ const filterOptions = {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-select
|
<QSelect
|
||||||
v-model="data.businessTypeFk"
|
v-model="data.businessTypeFk"
|
||||||
:options="businessTypes"
|
:options="businessTypes"
|
||||||
option-value="code"
|
option-value="code"
|
||||||
|
@ -79,7 +87,7 @@ const filterOptions = {
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-input
|
<QInput
|
||||||
v-model="data.contact"
|
v-model="data.contact"
|
||||||
:label="t('customer.basicData.contact')"
|
:label="t('customer.basicData.contact')"
|
||||||
:rules="validate('client.contact')"
|
:rules="validate('client.contact')"
|
||||||
|
@ -87,7 +95,7 @@ const filterOptions = {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-input
|
<QInput
|
||||||
v-model="data.email"
|
v-model="data.email"
|
||||||
type="email"
|
type="email"
|
||||||
:label="t('customer.basicData.email')"
|
:label="t('customer.basicData.email')"
|
||||||
|
@ -98,7 +106,7 @@ const filterOptions = {
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-input
|
<QInput
|
||||||
v-model="data.phone"
|
v-model="data.phone"
|
||||||
:label="t('customer.basicData.phone')"
|
:label="t('customer.basicData.phone')"
|
||||||
:rules="validate('client.phone')"
|
:rules="validate('client.phone')"
|
||||||
|
@ -106,7 +114,7 @@ const filterOptions = {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-input
|
<QInput
|
||||||
v-model="data.mobile"
|
v-model="data.mobile"
|
||||||
:label="t('customer.basicData.mobile')"
|
:label="t('customer.basicData.mobile')"
|
||||||
:rules="validate('client.mobile')"
|
:rules="validate('client.mobile')"
|
||||||
|
@ -116,7 +124,7 @@ const filterOptions = {
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-gutter-md q-mb-md">
|
<div class="row q-gutter-md q-mb-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-select
|
<QSelect
|
||||||
v-model="data.salesPersonFk"
|
v-model="data.salesPersonFk"
|
||||||
:options="workers"
|
:options="workers"
|
||||||
option-value="id"
|
option-value="id"
|
||||||
|
@ -125,23 +133,26 @@ const filterOptions = {
|
||||||
:label="t('customer.basicData.salesPerson')"
|
:label="t('customer.basicData.salesPerson')"
|
||||||
map-options
|
map-options
|
||||||
use-input
|
use-input
|
||||||
@filter="(value, update) => filter(value, update, filterOptions)"
|
@filter="
|
||||||
|
(value, update) =>
|
||||||
|
filter(value, update, filterOptions)
|
||||||
|
"
|
||||||
:rules="validate('client.salesPersonFk')"
|
:rules="validate('client.salesPersonFk')"
|
||||||
:input-debounce="0"
|
:input-debounce="0"
|
||||||
>
|
>
|
||||||
<template #before>
|
<template #prepend>
|
||||||
<q-avatar color="orange">
|
<QAvatar color="orange">
|
||||||
<q-img
|
<QImg
|
||||||
v-if="data.salesPersonFk"
|
v-if="data.salesPersonFk"
|
||||||
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
|
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
|
||||||
spinner-color="white"
|
spinner-color="white"
|
||||||
/>
|
/>
|
||||||
</q-avatar>
|
</QAvatar>
|
||||||
</template>
|
</template>
|
||||||
</q-select>
|
</QSelect>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-select
|
<QSelect
|
||||||
v-model="data.contactChannelFk"
|
v-model="data.contactChannelFk"
|
||||||
:options="contactChannels"
|
:options="contactChannels"
|
||||||
option-value="id"
|
option-value="id"
|
||||||
|
@ -155,17 +166,12 @@ const filterOptions = {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</form-model>
|
</FormModel>
|
||||||
</q-card>
|
</QCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-card {
|
.q-card {
|
||||||
width: 800px;
|
width: 800px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,38 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useState } from 'src/composables/useState';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
import CustomerDescriptor from './CustomerDescriptor.vue';
|
import CustomerDescriptor from './CustomerDescriptor.vue';
|
||||||
import LeftMenu from 'components/LeftMenu.vue';
|
import LeftMenu from 'components/LeftMenu.vue';
|
||||||
|
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
|
||||||
|
|
||||||
const state = useState();
|
const stateStore = useStateStore();
|
||||||
|
const { t } = useI18n();
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
|
||||||
<q-scroll-area class="fit">
|
<VnSearchbar
|
||||||
<customer-descriptor />
|
data-key="CustomerList"
|
||||||
<q-separator />
|
url="Clients/filter"
|
||||||
<left-menu source="card" />
|
:label="t('Search customer')"
|
||||||
</q-scroll-area>
|
:info="t('You can search by customer id or name')"
|
||||||
</q-drawer>
|
/>
|
||||||
<q-page-container>
|
</Teleport>
|
||||||
<q-page class="q-pa-md">
|
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||||
<router-view></router-view>
|
<QScrollArea class="fit">
|
||||||
</q-page>
|
<CustomerDescriptor />
|
||||||
</q-page-container>
|
<QSeparator />
|
||||||
|
<LeftMenu source="card" />
|
||||||
|
</QScrollArea>
|
||||||
|
</QDrawer>
|
||||||
|
<QPageContainer>
|
||||||
|
<QPage class="q-pa-md">
|
||||||
|
<RouterView></RouterView>
|
||||||
|
</QPage>
|
||||||
|
</QPageContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
Search customer: Buscar cliente
|
||||||
|
You can search by customer id or name: Puedes buscar por id o nombre del cliente
|
||||||
|
</i18n>
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { toCurrency } from 'src/filters';
|
import { toCurrency } from 'src/filters';
|
||||||
import axios from 'axios';
|
|
||||||
import CardDescriptor from 'components/ui/CardDescriptor.vue';
|
import CardDescriptor from 'components/ui/CardDescriptor.vue';
|
||||||
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
|
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
|
||||||
|
|
||||||
const $props = defineProps({
|
const $props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
|
@ -15,99 +14,153 @@ const $props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetch();
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const entityId = computed(() => {
|
const entityId = computed(() => {
|
||||||
return $props.id || route.params.id;
|
return $props.id || route.params.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const customer = ref();
|
|
||||||
async function fetch() {
|
|
||||||
const { data } = await axios.get(`Clients/${entityId.value}/getCard`);
|
|
||||||
|
|
||||||
if (data) customer.value = data;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<skeleton-descriptor v-if="!customer" />
|
<CardDescriptor module="Customer" :url="`Clients/${entityId}/getCard`">
|
||||||
<card-descriptor v-if="customer" module="Customer" :data="customer" :description="customer.name">
|
<template #body="{ entity }">
|
||||||
<!-- <template #menu>
|
<QList dense>
|
||||||
<q-item clickable v-ripple>Option 1</q-item>
|
<QItem v-if="entity.salesPersonUser" class="row">
|
||||||
<q-item clickable v-ripple>Option 2</q-item>
|
<QItemLabel class="col" caption>
|
||||||
</template> -->
|
{{ t('customer.card.salesPerson') }}
|
||||||
<template #body>
|
</QItemLabel>
|
||||||
<q-list>
|
<QItemLabel class="col q-ma-none">
|
||||||
<q-item v-if="customer.salesPersonUser">
|
<span class="link">
|
||||||
<q-item-section>
|
{{ entity.salesPersonUser.name }}
|
||||||
<q-item-label caption>{{ t('customer.card.salesPerson') }}</q-item-label>
|
<WorkerDescriptorProxy :id="entity.salesPersonFk" />
|
||||||
<q-item-label>{{ customer.salesPersonUser.name }}</q-item-label>
|
</span>
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item>
|
<QItem class="row">
|
||||||
<q-item-section>
|
<QItemLabel class="col" caption>
|
||||||
<q-item-label caption>{{ t('customer.card.credit') }}</q-item-label>
|
{{ t('customer.card.credit') }}
|
||||||
<q-item-label>{{ toCurrency(customer.credit) }}</q-item-label>
|
</QItemLabel>
|
||||||
</q-item-section>
|
<QItemLabel class="col q-ma-none">
|
||||||
<q-item-section>
|
{{ toCurrency(entity.credit) }}
|
||||||
<q-item-label caption>{{ t('customer.card.securedCredit') }}</q-item-label>
|
</QItemLabel>
|
||||||
<q-item-label>{{ toCurrency(customer.creditInsurance) }}</q-item-label>
|
</QItem>
|
||||||
</q-item-section>
|
<QItem class="row">
|
||||||
</q-item>
|
<QItemLabel class="col" caption>
|
||||||
<q-item>
|
{{ t('customer.card.securedCredit') }}
|
||||||
<q-item-section v-if="customer.payMethod">
|
</QItemLabel>
|
||||||
<q-item-label caption>{{ t('customer.card.payMethod') }}</q-item-label>
|
<QItemLabel class="col q-ma-none">
|
||||||
<q-item-label>{{ customer.payMethod.name }}</q-item-label>
|
{{ toCurrency(entity.creditInsurance) }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
<q-item-section>
|
</QItem>
|
||||||
<q-item-label caption>{{ t('customer.card.debt') }}</q-item-label>
|
<QItem v-if="entity.payMethod" class="row">
|
||||||
<q-item-label>{{ toCurrency(customer.debt) }}</q-item-label>
|
<QItemLabel class="col" caption>
|
||||||
</q-item-section>
|
{{ t('customer.card.payMethod') }}
|
||||||
</q-item>
|
</QItemLabel>
|
||||||
</q-list>
|
<QItemLabel class="col q-ma-none">
|
||||||
<q-card-actions class="q-gutter-md">
|
{{ entity.payMethod.name }}
|
||||||
<q-icon v-if="customer.isActive == false" name="vn:disabled" size="xs" color="primary">
|
</QItemLabel>
|
||||||
<q-tooltip>{{ t('customer.card.isDisabled') }}</q-tooltip>
|
</QItem>
|
||||||
</q-icon>
|
<QItem class="row">
|
||||||
<q-icon v-if="customer.isFreezed == true" name="vn:frozen" size="xs" color="primary">
|
<QItemLabel class="col" caption>
|
||||||
<q-tooltip>{{ t('customer.card.isFrozen') }}</q-tooltip>
|
{{ t('customer.card.debt') }}
|
||||||
</q-icon>
|
</QItemLabel>
|
||||||
<q-icon v-if="customer.debt > customer.credit" name="vn:risk" size="xs" color="primary">
|
<QItemLabel class="col q-ma-none">
|
||||||
<q-tooltip>{{ t('customer.card.hasDebt') }}</q-tooltip>
|
{{ toCurrency(entity.debt) }}
|
||||||
</q-icon>
|
</QItemLabel>
|
||||||
<q-icon v-if="customer.isTaxDataChecked == false" name="vn:no036" size="xs" color="primary">
|
</QItem>
|
||||||
<q-tooltip>{{ t('customer.card.notChecked') }}</q-tooltip>
|
</QList>
|
||||||
</q-icon>
|
<QCardActions class="q-gutter-md">
|
||||||
<q-icon v-if="customer.account.active == false" name="vn:noweb" size="xs" color="primary">
|
<QIcon
|
||||||
<q-tooltip>{{ t('customer.card.noWebAccess') }}</q-tooltip>
|
v-if="entity.isActive == false"
|
||||||
</q-icon>
|
name="vn:disabled"
|
||||||
</q-card-actions>
|
size="xs"
|
||||||
<!-- <q-card-actions>
|
color="primary"
|
||||||
<q-btn size="md" icon="vn:ticket" color="primary">
|
>
|
||||||
<q-tooltip>Ticket list</q-tooltip>
|
<QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip>
|
||||||
</q-btn>
|
</QIcon>
|
||||||
|
<QIcon
|
||||||
|
v-if="entity.isFreezed == true"
|
||||||
|
name="vn:frozen"
|
||||||
|
size="xs"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip>
|
||||||
|
</QIcon>
|
||||||
|
<QIcon
|
||||||
|
v-if="entity.debt > entity.credit"
|
||||||
|
name="vn:risk"
|
||||||
|
size="xs"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('customer.card.hasDebt') }}</QTooltip>
|
||||||
|
</QIcon>
|
||||||
|
<QIcon
|
||||||
|
v-if="entity.isTaxDataChecked == false"
|
||||||
|
name="vn:no036"
|
||||||
|
size="xs"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('customer.card.notChecked') }}</QTooltip>
|
||||||
|
</QIcon>
|
||||||
|
<QIcon
|
||||||
|
v-if="entity.account && entity.account.active == false"
|
||||||
|
name="vn:noweb"
|
||||||
|
size="xs"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('customer.card.noWebAccess') }}</QTooltip>
|
||||||
|
</QIcon>
|
||||||
|
</QCardActions>
|
||||||
|
<QCardActions>
|
||||||
|
<QBtn
|
||||||
|
:to="{
|
||||||
|
name: 'TicketList',
|
||||||
|
query: { params: JSON.stringify({ clientFk: entity.id }) },
|
||||||
|
}"
|
||||||
|
size="md"
|
||||||
|
icon="vn:ticket"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('ticketList') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QBtn
|
||||||
|
:to="{
|
||||||
|
name: 'InvoiceOutList',
|
||||||
|
query: { params: JSON.stringify({ clientFk: entity.id }) },
|
||||||
|
}"
|
||||||
|
size="md"
|
||||||
|
icon="vn:invoice-out"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('invoiceOutList') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<!--
|
||||||
|
<QBtn size="md" icon="vn:basketadd" color="primary">
|
||||||
|
<QTooltip>Order list</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
|
||||||
<q-btn size="md" icon="vn:invoice-out" color="primary">
|
<QBtn size="md" icon="face" color="primary">
|
||||||
<q-tooltip>Invoice Out list</q-tooltip>
|
<QTooltip>View user</QTooltip>
|
||||||
</q-btn>
|
</QBtn>
|
||||||
|
|
||||||
<q-btn size="md" icon="vn:basketadd" color="primary">
|
<QBtn size="md" icon="expand_more" color="primary">
|
||||||
<q-tooltip>Order list</q-tooltip>
|
<QTooltip>More options</QTooltip>
|
||||||
</q-btn>
|
</QBtn> -->
|
||||||
|
</QCardActions>
|
||||||
<q-btn size="md" icon="face" color="primary">
|
|
||||||
<q-tooltip>View user</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn size="md" icon="expand_more" color="primary">
|
|
||||||
<q-tooltip>More options</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-card-actions> -->
|
|
||||||
</template>
|
</template>
|
||||||
</card-descriptor>
|
</CardDescriptor>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
{
|
||||||
|
"en": {
|
||||||
|
"ticketList": "Customer ticket list",
|
||||||
|
"invoiceOutList": "Customer invoice out list"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"ticketList": "Listado de tickets del cliente",
|
||||||
|
"invoiceOutList": "Listado de facturas del cliente"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</i18n>
|
||||||
|
|
|
@ -9,7 +9,7 @@ const $props = defineProps({
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-card>
|
<QPopupProxy>
|
||||||
<customer-descriptor v-if="$props.id" :id="$props.id" />
|
<CustomerDescriptor v-if="$props.id" :id="$props.id" />
|
||||||
</q-card>
|
</QPopupProxy>
|
||||||
</template>
|
</template>
|
|
@ -1,12 +1,9 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import axios from 'axios';
|
|
||||||
import { toCurrency, toPercentage, toDate } from 'src/filters';
|
import { toCurrency, toPercentage, toDate } from 'src/filters';
|
||||||
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
|
import CardSummary from 'components/ui/CardSummary.vue';
|
||||||
|
|
||||||
onMounted(() => fetch());
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -19,37 +16,29 @@ const $props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
const entityId = computed(() => $props.id || route.params.id);
|
const entityId = computed(() => $props.id || route.params.id);
|
||||||
|
const summary = ref();
|
||||||
const customer = ref(null);
|
const customer = computed(() => summary.value.entity);
|
||||||
function fetch() {
|
|
||||||
const id = entityId.value;
|
|
||||||
axios.get(`Clients/${id}/summary`).then(({ data }) => {
|
|
||||||
customer.value = data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const balanceDue = computed(() => {
|
const balanceDue = computed(() => {
|
||||||
return customer.value.defaulters.length && customer.value.defaulters[0].amount;
|
return (
|
||||||
|
customer.value &&
|
||||||
|
customer.value.defaulters.length &&
|
||||||
|
customer.value.defaulters[0].amount
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const balanceDueWarning = computed(() => (balanceDue.value ? 'negative' : ''));
|
const balanceDueWarning = computed(() => (balanceDue.value ? 'negative' : ''));
|
||||||
|
|
||||||
const claimRate = computed(() => {
|
const claimRate = computed(() => {
|
||||||
const data = customer.value;
|
return customer.value.claimsRatio.claimingRate * 100;
|
||||||
|
|
||||||
return data.claimsRatio.claimingRate * 100;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const priceIncreasingRate = computed(() => {
|
const priceIncreasingRate = computed(() => {
|
||||||
const data = customer.value;
|
return customer.value.claimsRatio.priceIncreasing / 100;
|
||||||
|
|
||||||
return data.claimsRatio.priceIncreasing / 100;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const debtWarning = computed(() => {
|
const debtWarning = computed(() => {
|
||||||
const data = customer.value;
|
return customer.value.debt.debt > customer.value.credit ? 'negative' : '';
|
||||||
|
|
||||||
return data.debt.debt > data.credit ? 'negative' : '';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const creditWarning = computed(() => {
|
const creditWarning = computed(() => {
|
||||||
|
@ -62,431 +51,480 @@ const creditWarning = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="summary container">
|
<CardSummary ref="summary" :url="`Clients/${entityId}/summary`">
|
||||||
<q-card>
|
<template #body="{ entity }">
|
||||||
<skeleton-summary v-if="!customer" />
|
<QCardSection class="row q-pa-none QCol-gutter-md">
|
||||||
<template v-if="customer">
|
|
||||||
<div class="header bg-primary q-pa-sm q-mb-md">{{ customer.id }} - {{ customer.name }}</div>
|
|
||||||
<div class="row q-pa-md q-col-gutter-md q-mb-md">
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-list>
|
<QList dense>
|
||||||
<q-item-label header class="text-h6">
|
<QItemLabel header class="text-h6">
|
||||||
{{ t('customer.summary.basicData') }}
|
{{ t('customer.summary.basicData') }}
|
||||||
<router-link
|
<RouterLink
|
||||||
:to="{ name: 'CustomerBasicData', params: { id: entityId } }"
|
:to="{
|
||||||
|
name: 'CustomerBasicData',
|
||||||
|
params: { id: entity.id },
|
||||||
|
}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<q-icon name="open_in_new" />
|
<QIcon name="open_in_new" />
|
||||||
</router-link>
|
</RouterLink>
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
|
<QSeparator class="q-mb-md" />
|
||||||
|
|
||||||
<q-item>
|
<QItem class="row col">
|
||||||
<q-item-section>
|
<QItemLabel class="col" caption>
|
||||||
<q-item-label caption>{{ t('customer.summary.customerId') }}</q-item-label>
|
{{ t('customer.summary.customerId') }}
|
||||||
<q-item-label>{{ customer.id }}</q-item-label>
|
</QItemLabel>
|
||||||
</q-item-section>
|
<QItemLabel class="col q-ma-none">
|
||||||
</q-item>
|
{{ entity.id }}
|
||||||
<q-item>
|
</QItemLabel>
|
||||||
<q-item-section>
|
</QItem>
|
||||||
<q-item-label caption>{{ t('customer.summary.name') }}</q-item-label>
|
<QItem class="row col">
|
||||||
<q-item-label>{{ customer.name }}</q-item-label>
|
<QItemLabel class="col" caption>
|
||||||
</q-item-section>
|
{{ t('customer.summary.name') }}
|
||||||
</q-item>
|
</QItemLabel>
|
||||||
<q-item>
|
<QItemLabel class="col q-ma-none">
|
||||||
<q-item-section>
|
{{ entity.name }}
|
||||||
<q-item-label caption>{{ t('customer.summary.contact') }}</q-item-label>
|
</QItemLabel>
|
||||||
<q-item-label>{{ customer.contact }}</q-item-label>
|
</QItem>
|
||||||
</q-item-section>
|
<QItem class="row col">
|
||||||
</q-item>
|
<QItemLabel class="col" caption>
|
||||||
<q-item v-if="customer.salesPersonUser">
|
{{ t('customer.summary.contact') }}
|
||||||
<q-item-section>
|
</QItemLabel>
|
||||||
<q-item-label caption>{{ t('customer.summary.salesPerson') }}</q-item-label>
|
<QItemLabel class="col q-ma-none">
|
||||||
<q-item-label>{{ customer.salesPersonUser.name }}</q-item-label>
|
{{ entity.contact }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
<QItem v-if="entity.salesPersonUser" class="row col">
|
||||||
<q-item-label caption>{{ t('customer.summary.phone') }}</q-item-label>
|
<QItemLabel class="col" caption>
|
||||||
<q-item-label>{{ customer.phone }}</q-item-label>
|
{{ t('customer.summary.salesPerson') }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel class="col q-ma-none">
|
||||||
<q-item>
|
{{ entity.salesPersonUser.name }}
|
||||||
<q-item-section>
|
</QItemLabel>
|
||||||
<q-item-label caption>{{ t('customer.summary.mobile') }}</q-item-label>
|
</QItem>
|
||||||
<q-item-label>{{ customer.mobile }}</q-item-label>
|
|
||||||
</q-item-section>
|
<QItem class="row col">
|
||||||
</q-item>
|
<QItemLabel class="col" caption>
|
||||||
<q-item>
|
{{ t('customer.summary.phone') }}
|
||||||
<q-item-section>
|
</QItemLabel>
|
||||||
<q-item-label caption>{{ t('customer.summary.email') }}</q-item-label>
|
<QItemLabel class="col q-ma-none">
|
||||||
<q-item-label>{{ customer.email }}</q-item-label>
|
{{ entity.phone }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item v-if="customer.contactChannel">
|
|
||||||
<q-item-section>
|
<QItem class="row col">
|
||||||
<q-item-label caption>{{ t('customer.summary.contactChannel') }}</q-item-label>
|
<QItemLabel class="col" caption>
|
||||||
<q-item-label>{{ customer.contactChannel.name }}</q-item-label>
|
{{ t('customer.summary.mobile') }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel class="col q-ma-none">{{
|
||||||
</q-list>
|
entity.mobile
|
||||||
|
}}</QItemLabel>
|
||||||
|
</QItem>
|
||||||
|
|
||||||
|
<QItem v-if="entity.contactChannel" class="row col">
|
||||||
|
<QItemLabel class="col" caption>
|
||||||
|
{{ t('customer.summary.contactChannel') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel class="col q-ma-none">
|
||||||
|
{{ entity.contactChannel.name }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItem>
|
||||||
|
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('customer.summary.email') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>{{ entity.email }}</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-list>
|
<QList>
|
||||||
<q-item-label header class="text-h6">
|
<QItemLabel header class="text-h6">
|
||||||
{{ t('customer.summary.fiscalAddress') }}
|
{{ t('customer.summary.fiscalAddress') }}
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
<q-item>
|
<QItem>
|
||||||
<q-item-section>
|
<QItemSection>
|
||||||
<q-item-label caption>{{ t('customer.summary.socialName') }}</q-item-label>
|
<QItemLabel caption>
|
||||||
<q-item-label>{{ customer.socialName }}</q-item-label>
|
{{ t('customer.summary.socialName') }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel>{{ entity.socialName }}</QItemLabel>
|
||||||
<q-item>
|
</QItemSection>
|
||||||
<q-item-section>
|
</QItem>
|
||||||
<q-item-label caption>{{ t('customer.summary.fiscalId') }}</q-item-label>
|
<QItem>
|
||||||
<q-item-label>{{ customer.fi }}</q-item-label>
|
<QItemSection>
|
||||||
</q-item-section>
|
<QItemLabel caption>
|
||||||
</q-item>
|
{{ t('customer.summary.fiscalId') }}
|
||||||
<q-item>
|
</QItemLabel>
|
||||||
<q-item-section>
|
<QItemLabel>{{ entity.fi }}</QItemLabel>
|
||||||
<q-item-label caption>{{ t('customer.summary.postcode') }}</q-item-label>
|
</QItemSection>
|
||||||
<q-item-label>{{ customer.postcode }}</q-item-label>
|
</QItem>
|
||||||
</q-item-section>
|
<QItem>
|
||||||
</q-item>
|
<QItemSection>
|
||||||
<q-item v-if="customer.province">
|
<QItemLabel caption>
|
||||||
<q-item-section>
|
{{ t('customer.summary.postcode') }}
|
||||||
<q-item-label caption>{{ t('customer.summary.province') }}</q-item-label>
|
</QItemLabel>
|
||||||
<q-item-label>{{ customer.province.name }}</q-item-label>
|
<QItemLabel>{{ entity.postcode }}</QItemLabel>
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item v-if="customer.country">
|
<QItem v-if="entity.province">
|
||||||
<q-item-section>
|
<QItemSection>
|
||||||
<q-item-label caption>{{ t('customer.summary.country') }}</q-item-label>
|
<QItemLabel caption>
|
||||||
<q-item-label>{{ customer.country.country }}</q-item-label>
|
{{ t('customer.summary.province') }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel>{{ entity.province.name }}</QItemLabel>
|
||||||
<q-item>
|
</QItemSection>
|
||||||
<q-item-section>
|
</QItem>
|
||||||
<q-item-label caption>{{ t('customer.summary.street') }}</q-item-label>
|
<QItem v-if="entity.country">
|
||||||
<q-item-label>{{ customer.street }}</q-item-label>
|
<QItemSection>
|
||||||
</q-item-section>
|
<QItemLabel caption>
|
||||||
</q-item>
|
{{ t('customer.summary.country') }}
|
||||||
</q-list>
|
</QItemLabel>
|
||||||
|
<QItemLabel>{{ entity.country.country }}</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('customer.summary.street') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>{{ entity.street }}</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-list>
|
<QList>
|
||||||
<q-item-label header class="text-h6">
|
<QItemLabel header class="text-h6">
|
||||||
{{ t('customer.summary.fiscalData') }}
|
{{ t('customer.summary.fiscalData') }}
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
<q-item dense>
|
<QItem dense>
|
||||||
<q-checkbox
|
<QCheckbox
|
||||||
v-model="customer.isEqualizated"
|
v-model="entity.isEqualizated"
|
||||||
:label="t('customer.summary.isEqualizated')"
|
:label="t('customer.summary.isEqualizated')"
|
||||||
disable
|
disable
|
||||||
/>
|
/>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item dense>
|
<QItem dense>
|
||||||
<q-checkbox
|
<QCheckbox
|
||||||
v-model="customer.isActive"
|
v-model="entity.isActive"
|
||||||
:label="t('customer.summary.isActive')"
|
:label="t('customer.summary.isActive')"
|
||||||
disable
|
disable
|
||||||
/>
|
/>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item dense>
|
<QItem dense>
|
||||||
<q-checkbox
|
<QCheckbox
|
||||||
v-model="customer.hasToInvoiceByAddress"
|
v-model="entity.hasToInvoiceByAddress"
|
||||||
:label="t('customer.summary.invoiceByAddress')"
|
:label="t('customer.summary.invoiceByAddress')"
|
||||||
disable
|
disable
|
||||||
/>
|
/>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item dense>
|
<QItem dense>
|
||||||
<q-checkbox
|
<QCheckbox
|
||||||
v-model="customer.isTaxDataChecked"
|
v-model="entity.isTaxDataChecked"
|
||||||
:label="t('customer.summary.verifiedData')"
|
:label="t('customer.summary.verifiedData')"
|
||||||
disable
|
disable
|
||||||
/>
|
/>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item dense>
|
<QItem dense>
|
||||||
<q-checkbox
|
<QCheckbox
|
||||||
v-model="customer.hasToInvoice"
|
v-model="entity.hasToInvoice"
|
||||||
:label="t('customer.summary.hasToInvoice')"
|
:label="t('customer.summary.hasToInvoice')"
|
||||||
disable
|
disable
|
||||||
/>
|
/>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item dense>
|
<QItem dense>
|
||||||
<q-checkbox
|
<QCheckbox
|
||||||
v-model="customer.isToBeMailed"
|
v-model="entity.isToBeMailed"
|
||||||
:label="t('customer.summary.notifyByEmail')"
|
:label="t('customer.summary.notifyByEmail')"
|
||||||
disable
|
disable
|
||||||
/>
|
/>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item dense>
|
<QItem dense>
|
||||||
<q-checkbox v-model="customer.isVies" :label="t('customer.summary.vies')" disable />
|
<QCheckbox
|
||||||
</q-item>
|
v-model="entity.isVies"
|
||||||
</q-list>
|
:label="t('customer.summary.vies')"
|
||||||
|
disable
|
||||||
|
/>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-list>
|
<QList>
|
||||||
<q-item-label header class="text-h6">
|
<QItemLabel header class="text-h6">
|
||||||
{{ t('customer.summary.billingData') }}
|
{{ t('customer.summary.billingData') }}
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
<q-item>
|
<QItem>
|
||||||
<q-item-section>
|
<QItemSection>
|
||||||
<q-item-label caption>{{ t('customer.summary.payMethod') }}</q-item-label>
|
<QItemLabel caption>
|
||||||
<q-item-label>{{ customer.payMethod.name }}</q-item-label>
|
{{ t('customer.summary.payMethod') }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel>{{ entity.payMethod.name }}</QItemLabel>
|
||||||
<q-item>
|
</QItemSection>
|
||||||
<q-item-section>
|
</QItem>
|
||||||
<q-item-label caption>{{ t('customer.summary.bankAccount') }}</q-item-label>
|
<QItem>
|
||||||
<q-item-label>{{ customer.iban }}</q-item-label>
|
<QItemSection>
|
||||||
</q-item-section>
|
<QItemLabel caption>
|
||||||
</q-item>
|
{{ t('customer.summary.bankAccount') }}
|
||||||
<q-item>
|
</QItemLabel>
|
||||||
<q-item-section>
|
<QItemLabel>{{ entity.iban }}</QItemLabel>
|
||||||
<q-item-label caption>{{ t('customer.summary.dueDay') }}</q-item-label>
|
</QItemSection>
|
||||||
<q-item-label>{{ customer.dueDay }}</q-item-label>
|
</QItem>
|
||||||
</q-item-section>
|
<QItem>
|
||||||
</q-item>
|
<QItemSection>
|
||||||
<q-item dense>
|
<QItemLabel caption>
|
||||||
<q-checkbox v-model="customer.hasLcr" :label="t('customer.summary.hasLcr')" disable />
|
{{ t('customer.summary.dueDay') }}
|
||||||
</q-item>
|
</QItemLabel>
|
||||||
<q-item dense>
|
<QItemLabel>{{ entity.dueDay }}</QItemLabel>
|
||||||
<q-checkbox
|
</QItemSection>
|
||||||
v-model="customer.hasCoreVnl"
|
</QItem>
|
||||||
|
<QItem dense>
|
||||||
|
<QCheckbox
|
||||||
|
v-model="entity.hasLcr"
|
||||||
|
:label="t('customer.summary.hasLcr')"
|
||||||
|
disable
|
||||||
|
/>
|
||||||
|
</QItem>
|
||||||
|
<QItem dense>
|
||||||
|
<QCheckbox
|
||||||
|
v-model="entity.hasCoreVnl"
|
||||||
:label="t('customer.summary.hasCoreVnl')"
|
:label="t('customer.summary.hasCoreVnl')"
|
||||||
disable
|
disable
|
||||||
/>
|
/>
|
||||||
</q-item>
|
</QItem>
|
||||||
<q-item dense>
|
<QItem dense>
|
||||||
<q-checkbox
|
<QCheckbox
|
||||||
v-model="customer.hasSepaVnl"
|
v-model="entity.hasSepaVnl"
|
||||||
:label="t('customer.summary.hasB2BVnl')"
|
:label="t('customer.summary.hasB2BVnl')"
|
||||||
disable
|
disable
|
||||||
/>
|
/>
|
||||||
</q-item>
|
</QItem>
|
||||||
</q-list>
|
</QList>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" v-if="customer.defaultAddress">
|
<div class="col" v-if="entity.defaultAddress">
|
||||||
<q-list>
|
<QList>
|
||||||
<q-item-label header class="text-h6">
|
<QItemLabel header class="text-h6">
|
||||||
{{ t('customer.summary.consignee') }}
|
{{ t('customer.summary.consignee') }}
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
<q-item>
|
<QItem>
|
||||||
<q-item-section>
|
<QItemSection>
|
||||||
<q-item-label caption>{{ t('customer.summary.addressName') }}</q-item-label>
|
<QItemLabel caption>
|
||||||
<q-item-label>{{ customer.defaultAddress.nickname }}</q-item-label>
|
{{ t('customer.summary.addressName') }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel>
|
||||||
<q-item>
|
{{ entity.defaultAddress.nickname }}
|
||||||
<q-item-section>
|
</QItemLabel>
|
||||||
<q-item-label caption>{{ t('customer.summary.addressCity') }}</q-item-label>
|
</QItemSection>
|
||||||
<q-item-label>{{ customer.defaultAddress.city }}</q-item-label>
|
</QItem>
|
||||||
</q-item-section>
|
<QItem>
|
||||||
</q-item>
|
<QItemSection>
|
||||||
<q-item>
|
<QItemLabel caption>
|
||||||
<q-item-section>
|
{{ t('customer.summary.addressCity') }}
|
||||||
<q-item-label caption>{{ t('customer.summary.addressStreet') }}</q-item-label>
|
</QItemLabel>
|
||||||
<q-item-label>{{ customer.defaultAddress.street }}</q-item-label>
|
<QItemLabel>{{ entity.defaultAddress.city }}</QItemLabel>
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
</q-item>
|
</QItem>
|
||||||
</q-list>
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('customer.summary.addressStreet') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>
|
||||||
|
{{ entity.defaultAddress.street }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" v-if="customer.account">
|
<div class="col" v-if="entity.account">
|
||||||
<q-list>
|
<QList>
|
||||||
<q-item-label header class="text-h6">
|
<QItemLabel header class="text-h6">
|
||||||
{{ t('customer.summary.webAccess') }}
|
{{ t('customer.summary.webAccess') }}
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
<q-item>
|
<QItem>
|
||||||
<q-item-section>
|
<QItemSection>
|
||||||
<q-item-label caption>{{ t('customer.summary.username') }}</q-item-label>
|
<QItemLabel caption>
|
||||||
<q-item-label>{{ customer.account.name }}</q-item-label>
|
{{ t('customer.summary.username') }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel>{{ entity.account.name }}</QItemLabel>
|
||||||
<q-item dense>
|
</QItemSection>
|
||||||
<q-checkbox
|
</QItem>
|
||||||
v-model="customer.account.active"
|
<QItem dense>
|
||||||
|
<QCheckbox
|
||||||
|
v-model="entity.account.active"
|
||||||
:label="t('customer.summary.webAccess')"
|
:label="t('customer.summary.webAccess')"
|
||||||
disable
|
disable
|
||||||
/>
|
/>
|
||||||
</q-item>
|
</QItem>
|
||||||
</q-list>
|
</QList>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-list>
|
<QList>
|
||||||
<q-item-label header class="text-h6">
|
<QItemLabel header class="text-h6">
|
||||||
{{ t('customer.summary.businessData') }}
|
{{ t('customer.summary.businessData') }}
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
<q-item>
|
<QItem>
|
||||||
<q-item-section>
|
<QItemSection>
|
||||||
<q-item-label caption>{{ t('customer.summary.totalGreuge') }}</q-item-label>
|
<QItemLabel caption>
|
||||||
<q-item-label>{{ toCurrency(customer.totalGreuge) }}</q-item-label>
|
{{ t('customer.summary.totalGreuge') }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
<QItemLabel>
|
||||||
<q-item v-if="customer.mana">
|
{{ toCurrency(entity.totalGreuge) }}
|
||||||
<q-item-section>
|
</QItemLabel>
|
||||||
<q-item-label caption>{{ t('customer.summary.mana') }}</q-item-label>
|
</QItemSection>
|
||||||
<q-item-label>{{ toCurrency(customer.mana.mana) }}</q-item-label>
|
</QItem>
|
||||||
</q-item-section>
|
<QItem v-if="entity.mana">
|
||||||
</q-item>
|
<QItemSection>
|
||||||
<q-item v-if="customer.claimsRatio">
|
<QItemLabel caption>
|
||||||
<q-item-section>
|
{{ t('customer.summary.mana') }}
|
||||||
<q-item-label caption>
|
</QItemLabel>
|
||||||
|
<QItemLabel>
|
||||||
|
{{ toCurrency(entity.mana.mana) }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem v-if="entity.claimsRatio">
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
{{ t('customer.summary.priceIncreasingRate') }}
|
{{ t('customer.summary.priceIncreasingRate') }}
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
<q-item-label>{{ toPercentage(priceIncreasingRate) }}</q-item-label>
|
<QItemLabel>
|
||||||
</q-item-section>
|
{{ toPercentage(priceIncreasingRate) }}
|
||||||
</q-item>
|
</QItemLabel>
|
||||||
<q-item v-if="customer.averageInvoiced">
|
</QItemSection>
|
||||||
<q-item-section>
|
</QItem>
|
||||||
<q-item-label caption>{{ t('customer.summary.averageInvoiced') }}</q-item-label>
|
<QItem v-if="entity.averageInvoiced">
|
||||||
<q-item-label>{{ toCurrency(customer.averageInvoiced.invoiced) }}</q-item-label>
|
<QItemSection>
|
||||||
</q-item-section>
|
<QItemLabel caption>
|
||||||
</q-item>
|
{{ t('customer.summary.averageInvoiced') }}
|
||||||
<q-item v-if="customer.claimsRatio">
|
</QItemLabel>
|
||||||
<q-item-section>
|
<QItemLabel>
|
||||||
<q-item-label caption>{{ t('customer.summary.claimRate') }}</q-item-label>
|
{{ toCurrency(entity.averageInvoiced.invoiced) }}
|
||||||
<q-item-label>{{ toPercentage(claimRate) }}</q-item-label>
|
</QItemLabel>
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
</q-item>
|
</QItem>
|
||||||
</q-list>
|
<QItem v-if="entity.claimsRatio">
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('customer.summary.claimRate') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>{{ toPercentage(claimRate) }}</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-list>
|
<QList>
|
||||||
<q-item-label header class="text-h6">
|
<QItemLabel header class="text-h6">
|
||||||
{{ t('customer.summary.financialData') }}
|
{{ t('customer.summary.financialData') }}
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
<q-item v-if="customer.debt">
|
<QItem v-if="entity.debt">
|
||||||
<q-item-section>
|
<QItemSection>
|
||||||
<q-item-label caption>{{ t('customer.summary.risk') }}</q-item-label>
|
<QItemLabel caption>
|
||||||
<q-item-label :class="debtWarning">
|
{{ t('customer.summary.risk') }}
|
||||||
{{ toCurrency(customer.debt.debt) }}
|
</QItemLabel>
|
||||||
</q-item-label>
|
<QItemLabel :class="debtWarning">
|
||||||
</q-item-section>
|
{{ toCurrency(entity.debt.debt) }}
|
||||||
<q-item-section side>
|
</QItemLabel>
|
||||||
<q-icon name="vn:info">
|
</QItemSection>
|
||||||
<q-tooltip>{{ t('customer.summary.riskInfo') }}</q-tooltip>
|
<QItemSection side>
|
||||||
</q-icon>
|
<QIcon name="vn:info">
|
||||||
</q-item-section>
|
<QTooltip>
|
||||||
</q-item>
|
{{ t('customer.summary.riskInfo') }}
|
||||||
<q-item>
|
</QTooltip>
|
||||||
<q-item-section>
|
</QIcon>
|
||||||
<q-item-label caption>{{ t('customer.summary.credit') }}</q-item-label>
|
</QItemSection>
|
||||||
<q-item-label :class="creditWarning">
|
</QItem>
|
||||||
{{ toCurrency(customer.credit) }}
|
<QItem>
|
||||||
</q-item-label>
|
<QItemSection>
|
||||||
</q-item-section>
|
<QItemLabel caption>
|
||||||
<q-item-section side>
|
{{ t('customer.summary.credit') }}
|
||||||
<q-icon name="vn:info">
|
</QItemLabel>
|
||||||
<q-tooltip>{{ t('customer.summary.creditInfo') }}</q-tooltip>
|
<QItemLabel :class="creditWarning">
|
||||||
</q-icon>
|
{{ toCurrency(entity.credit) }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
</q-item>
|
</QItemSection>
|
||||||
<q-item v-if="customer.creditInsurance">
|
<QItemSection side>
|
||||||
<q-item-section>
|
<QIcon name="vn:info">
|
||||||
<q-item-label caption>{{ t('customer.summary.securedCredit') }}</q-item-label>
|
<QTooltip>
|
||||||
<q-item-label>{{ toCurrency(customer.creditInsurance) }}</q-item-label>
|
{{ t('customer.summary.creditInfo') }}
|
||||||
</q-item-section>
|
</QTooltip>
|
||||||
<q-item-section side>
|
</QIcon>
|
||||||
<q-icon name="vn:info">
|
</QItemSection>
|
||||||
<q-tooltip>{{ t('customer.summary.securedCreditInfo') }}</q-tooltip>
|
</QItem>
|
||||||
</q-icon>
|
<QItem v-if="entity.creditInsurance">
|
||||||
</q-item-section>
|
<QItemSection>
|
||||||
</q-item>
|
<QItemLabel caption>
|
||||||
<q-item>
|
{{ t('customer.summary.securedCredit') }}
|
||||||
<q-item-section>
|
</QItemLabel>
|
||||||
<q-item-label caption>{{ t('customer.summary.balance') }}</q-item-label>
|
<QItemLabel>
|
||||||
<q-item-label>{{ toCurrency(customer.sumRisk) || toCurrency(0) }}</q-item-label>
|
{{ toCurrency(entity.creditInsurance) }}
|
||||||
</q-item-section>
|
</QItemLabel>
|
||||||
<q-item-section side>
|
</QItemSection>
|
||||||
<q-icon name="vn:info">
|
<QItemSection side>
|
||||||
<q-tooltip>{{ t('customer.summary.balanceInfo') }}</q-tooltip>
|
<QIcon name="vn:info">
|
||||||
</q-icon>
|
<QTooltip>
|
||||||
</q-item-section>
|
{{ t('customer.summary.securedCreditInfo') }}
|
||||||
</q-item>
|
</QTooltip>
|
||||||
<q-item v-if="customer.defaulters">
|
</QIcon>
|
||||||
<q-item-section>
|
</QItemSection>
|
||||||
<q-item-label caption>{{ t('customer.summary.balanceDue') }}</q-item-label>
|
</QItem>
|
||||||
<q-item-label :class="balanceDueWarning">
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('customer.summary.balance') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>
|
||||||
|
{{ toCurrency(entity.sumRisk) || toCurrency(0) }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection side>
|
||||||
|
<QIcon name="vn:info">
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('customer.summary.balanceInfo') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QIcon>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem v-if="entity.defaulters">
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ t('customer.summary.balanceDue') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel :class="balanceDueWarning">
|
||||||
{{ toCurrency(balanceDue) }}
|
{{ toCurrency(balanceDue) }}
|
||||||
</q-item-label>
|
</QItemLabel>
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
<q-item-section side>
|
<QItemSection side>
|
||||||
<q-icon name="vn:info">
|
<QIcon name="vn:info">
|
||||||
<q-tooltip>{{ t('customer.summary.balanceDueInfo') }}</q-tooltip>
|
<QTooltip>
|
||||||
</q-icon>
|
{{ t('customer.summary.balanceDueInfo') }}
|
||||||
</q-item-section>
|
</QTooltip>
|
||||||
</q-item>
|
</QIcon>
|
||||||
<q-item v-if="customer.recovery">
|
</QItemSection>
|
||||||
<q-item-section>
|
</QItem>
|
||||||
<q-item-label caption>{{ t('customer.summary.recoverySince') }}</q-item-label>
|
<QItem v-if="entity.recovery">
|
||||||
<q-item-label>{{ toDate(customer.recovery.started) }}</q-item-label>
|
<QItemSection>
|
||||||
</q-item-section>
|
<QItemLabel caption>
|
||||||
</q-item>
|
{{ t('customer.summary.recoverySince') }}
|
||||||
</q-list>
|
</QItemLabel>
|
||||||
</div>
|
<QItemLabel>
|
||||||
|
{{ toDate(entity.recovery.started) }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
</div>
|
</div>
|
||||||
|
</QCardSection>
|
||||||
</template>
|
</template>
|
||||||
</q-card>
|
</CardSummary>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-card {
|
<style lang="scss">
|
||||||
width: 100%;
|
.q-item__label + .q-item__label {
|
||||||
max-width: 1200px;
|
margin: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.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>
|
</style>
|
||||||
|
|
|
@ -15,9 +15,9 @@ const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
<QDialog ref="dialogRef" @hide="onDialogHide">
|
||||||
<customer-summary v-if="$props.id" :id="$props.id" />
|
<CustomerSummary v-if="$props.id" :id="$props.id" />
|
||||||
</q-dialog>
|
</QDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -1,47 +1,56 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, watch } from 'vue'
|
import { reactive, watch } from 'vue';
|
||||||
|
|
||||||
const customer = reactive({
|
const customer = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => customer.name, () => {
|
watch(
|
||||||
|
() => customer.name,
|
||||||
|
() => {
|
||||||
console.log('customer.name changed');
|
console.log('customer.name changed');
|
||||||
});
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-page class="q-pa-md">
|
<QPage class="q-pa-md">
|
||||||
<q-card class="q-pa-md">
|
<QCard class="q-pa-md">
|
||||||
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-md">
|
<QForm @submit="onSubmit" @reset="onReset" class="q-gutter-md">
|
||||||
<q-input
|
<QInput
|
||||||
filled
|
filled
|
||||||
v-model="customer.name"
|
v-model="customer.name"
|
||||||
label="Your name *"
|
label="Your name *"
|
||||||
hint="Name and surname"
|
hint="Name and surname"
|
||||||
lazy-rules
|
lazy-rules
|
||||||
:rules="[val => val && val.length > 0 || 'Please type something']"
|
:rules="[(val) => (val && val.length > 0) || 'Please type something']"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<QInput
|
||||||
filled
|
filled
|
||||||
type="number"
|
type="number"
|
||||||
v-model="age"
|
v-model="age"
|
||||||
label="Your age *"
|
label="Your age *"
|
||||||
lazy-rules
|
lazy-rules
|
||||||
:rules="[
|
:rules="[
|
||||||
val => val !== null && val !== '' || 'Please type your age',
|
(val) => (val !== null && val !== '') || 'Please type your age',
|
||||||
val => val > 0 && val < 100 || 'Please type a real age'
|
(val) => (val > 0 && val < 100) || 'Please type a real age',
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<q-btn label="Submit" type="submit" color="primary" />
|
<QBtn label="Submit" type="submit" color="primary" />
|
||||||
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
|
<QBtn
|
||||||
|
label="Reset"
|
||||||
|
type="reset"
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
class="q-ml-sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</QForm>
|
||||||
</q-card>
|
</QCard>
|
||||||
</q-page>
|
</QPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
<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 provinces = ref();
|
||||||
|
const workers = ref();
|
||||||
|
const zones = ref();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FetchData url="Provinces" @on-fetch="(data) => (provinces = data)" auto-load />
|
||||||
|
<FetchData url="Zones" @on-fetch="(data) => (zones = data)" auto-load />
|
||||||
|
<FetchData
|
||||||
|
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 }">
|
||||||
|
<QList dense>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput :label="t('FI')" v-model="params.fi" lazy-rules>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="badge" size="sm"></QIcon>
|
||||||
|
</template>
|
||||||
|
</QInput>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput :label="t('Name')" v-model="params.name" lazy-rules />
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput
|
||||||
|
:label="t('Social Name')"
|
||||||
|
v-model="params.socialName"
|
||||||
|
lazy-rules
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection v-if="!workers">
|
||||||
|
<QSkeleton type="QInput" class="full-width" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="workers">
|
||||||
|
<QSelect
|
||||||
|
:label="t('Salesperson')"
|
||||||
|
v-model="params.salesPersonFk"
|
||||||
|
@update:model-value="searchFn()"
|
||||||
|
:options="workers"
|
||||||
|
option-value="id"
|
||||||
|
option-label="name"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-input
|
||||||
|
:input-debounce="0"
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection v-if="!provinces">
|
||||||
|
<QSkeleton type="QInput" class="full-width" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="provinces">
|
||||||
|
<QSelect
|
||||||
|
:label="t('Province')"
|
||||||
|
v-model="params.provinceFk"
|
||||||
|
@update:model-value="searchFn()"
|
||||||
|
:options="provinces"
|
||||||
|
option-value="id"
|
||||||
|
option-label="name"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem class="q-mb-md">
|
||||||
|
<QItemSection>
|
||||||
|
<QInput :label="t('City')" v-model="params.city" lazy-rules />
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QSeparator />
|
||||||
|
<QExpansionItem :label="t('More options')" expand-separator>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput :label="t('Phone')" v-model="params.phone" lazy-rules>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="phone" size="sm"></QIcon>
|
||||||
|
</template>
|
||||||
|
</QInput>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput :label="t('Email')" v-model="params.email" lazy-rules>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="email" size="sm"></QIcon>
|
||||||
|
</template>
|
||||||
|
</QInput>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection v-if="!zones">
|
||||||
|
<QSkeleton type="QInput" class="full-width" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="zones">
|
||||||
|
<QSelect
|
||||||
|
:label="t('Zone')"
|
||||||
|
v-model="params.zoneFk"
|
||||||
|
@update:model-value="searchFn()"
|
||||||
|
:options="zones"
|
||||||
|
option-value="id"
|
||||||
|
option-label="name"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput
|
||||||
|
:label="t('Postcode')"
|
||||||
|
v-model="params.postcode"
|
||||||
|
lazy-rules
|
||||||
|
/>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QExpansionItem>
|
||||||
|
</QList>
|
||||||
|
</template>
|
||||||
|
</VnFilterPanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
en:
|
||||||
|
params:
|
||||||
|
search: Contains
|
||||||
|
fi: FI
|
||||||
|
name: Name
|
||||||
|
socialName: Social Name
|
||||||
|
salesPersonFk: Salesperson
|
||||||
|
provinceFk: Province
|
||||||
|
city: City
|
||||||
|
phone: Phone
|
||||||
|
email: Email
|
||||||
|
zoneFk: Zone
|
||||||
|
postcode: Postcode
|
||||||
|
es:
|
||||||
|
params:
|
||||||
|
search: Contiene
|
||||||
|
fi: NIF
|
||||||
|
name: Nombre
|
||||||
|
socialName: Razón Social
|
||||||
|
salesPersonFk: Comercial
|
||||||
|
provinceFk: Provincia
|
||||||
|
city: Ciudad
|
||||||
|
phone: Teléfono
|
||||||
|
email: Email
|
||||||
|
zoneFk: Zona
|
||||||
|
postcode: CP
|
||||||
|
FI: NIF
|
||||||
|
Name: Nombre
|
||||||
|
Social Name: Razón social
|
||||||
|
Salesperson: Comercial
|
||||||
|
Province: Provincia
|
||||||
|
City: Ciudad
|
||||||
|
More options: Más opciones
|
||||||
|
Phone: Teléfono
|
||||||
|
Email: Email
|
||||||
|
Zone: Zona
|
||||||
|
Postcode: Código postal
|
||||||
|
</i18n>
|
|
@ -2,9 +2,13 @@
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import Paginate from 'src/components/PaginateData.vue';
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
|
import VnPaginate from 'src/components/ui/VnPaginate.vue';
|
||||||
import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue';
|
import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue';
|
||||||
|
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
|
||||||
|
import CustomerFilter from './CustomerFilter.vue';
|
||||||
|
|
||||||
|
const stateStore = useStateStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const quasar = useQuasar();
|
const quasar = useQuasar();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -24,64 +28,138 @@ function viewSummary(id) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-page class="q-pa-md">
|
<template v-if="stateStore.isHeaderMounted()">
|
||||||
<paginate url="/Clients" sort-by="id DESC" auto-load>
|
<Teleport to="#searchbar">
|
||||||
|
<VnSearchbar
|
||||||
|
data-key="CustomerList"
|
||||||
|
:label="t('Search customer')"
|
||||||
|
:info="t('You can search by customer id or name')"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport to="#actions-append">
|
||||||
|
<div class="row q-gutter-x-sm">
|
||||||
|
<QBtn
|
||||||
|
flat
|
||||||
|
@click="stateStore.toggleRightDrawer()"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
icon="menu"
|
||||||
|
>
|
||||||
|
<QTooltip bottom anchor="bottom right">
|
||||||
|
{{ t('globals.collapseMenu') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
|
||||||
|
<QScrollArea class="fit text-grey-8">
|
||||||
|
<CustomerFilter data-key="CustomerList" />
|
||||||
|
</QScrollArea>
|
||||||
|
</QDrawer>
|
||||||
|
<QPage class="column items-center q-pa-md">
|
||||||
|
<div class="card-list">
|
||||||
|
<VnPaginate
|
||||||
|
data-key="CustomerList"
|
||||||
|
url="/Clients/filter"
|
||||||
|
order="id DESC"
|
||||||
|
auto-load
|
||||||
|
>
|
||||||
<template #body="{ rows }">
|
<template #body="{ rows }">
|
||||||
<q-card class="card" v-for="row of rows" :key="row.id">
|
<QCard 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>
|
<QItem
|
||||||
<q-item-section class="q-pa-md" @click="navigate(row.id)">
|
class="q-pa-none items-start cursor-pointer q-hoverable"
|
||||||
|
v-ripple
|
||||||
|
clickable
|
||||||
|
>
|
||||||
|
<QItemSection class="q-pa-md" @click="navigate(row.id)">
|
||||||
<div class="text-h6">{{ row.name }}</div>
|
<div class="text-h6">{{ row.name }}</div>
|
||||||
<q-item-label caption>#{{ row.id }}</q-item-label>
|
<QItemLabel caption>#{{ row.id }}</QItemLabel>
|
||||||
<q-list>
|
|
||||||
<q-item class="q-pa-none">
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label caption>{{ t('customer.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('customer.list.phone') }}</q-item-label>
|
|
||||||
<q-item-label>{{ row.phone }}</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 color="grey-7" round flat icon="more_vert">
|
|
||||||
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
|
|
||||||
<q-menu cover auto-close>
|
|
||||||
<q-list>
|
|
||||||
<q-item clickable>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="add" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>Add a note</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="history" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>Display customer history</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
</q-btn> -->
|
|
||||||
|
|
||||||
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
|
<QList>
|
||||||
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
|
<QItem class="q-pa-none">
|
||||||
</q-btn>
|
<QItemSection>
|
||||||
<q-btn flat round color="grey-7" icon="preview" @click="viewSummary(row.id)">
|
<QItemLabel caption>
|
||||||
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
|
{{ t('customer.list.email') }}
|
||||||
</q-btn>
|
</QItemLabel>
|
||||||
<!-- <q-btn flat round color="grey-7" icon="vn:ticket">
|
<QItemLabel>{{ row.email }}</QItemLabel>
|
||||||
<q-tooltip>{{ t('customer.list.customerOrders') }}</q-tooltip>
|
</QItemSection>
|
||||||
</q-btn> -->
|
</QItem>
|
||||||
</q-card-actions>
|
<QItem class="q-pa-none">
|
||||||
</q-item>
|
<QItemSection>
|
||||||
</q-card>
|
<QItemLabel caption>
|
||||||
|
{{ t('customer.list.phone') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel>{{ row.phone }}</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</QItemSection>
|
||||||
|
<QSeparator vertical />
|
||||||
|
<QCardActions vertical class="justify-between">
|
||||||
|
<!-- <QBtn color="grey-7" round flat icon="more_vert">
|
||||||
|
<QTooltip>{{ t('customer.list.moreOptions') }}</QTooltip>
|
||||||
|
<QMenu cover auto-close>
|
||||||
|
<QList>
|
||||||
|
<QItem clickable>
|
||||||
|
<QItemSection avatar>
|
||||||
|
<QIcon name="add" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection>Add a note</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem clickable>
|
||||||
|
<QItemSection avatar>
|
||||||
|
<QIcon name="history" />
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection>Display customer history</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</QMenu>
|
||||||
|
</QBtn> -->
|
||||||
|
|
||||||
|
<QBtn
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
color="primary"
|
||||||
|
icon="arrow_circle_right"
|
||||||
|
@click="navigate(row.id)"
|
||||||
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('components.smartCard.openCard') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QBtn
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
color="grey-7"
|
||||||
|
icon="preview"
|
||||||
|
@click="viewSummary(row.id)"
|
||||||
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('components.smartCard.openSummary') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<!-- <QBtn flat round color="grey-7" icon="vn:ticket">
|
||||||
|
<QTooltip>{{ t('customer.list.customerOrders') }}</QTooltip>
|
||||||
|
</QBtn> -->
|
||||||
|
</QCardActions>
|
||||||
|
</QItem>
|
||||||
|
</QCard>
|
||||||
</template>
|
</template>
|
||||||
</paginate>
|
</VnPaginate>
|
||||||
</q-page>
|
</div>
|
||||||
|
</QPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-list {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
Search customer: Buscar cliente
|
||||||
|
You can search by customer id or name: Puedes buscar por id o nombre del cliente
|
||||||
|
</i18n>
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useState } from 'src/composables/useState';
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
import LeftMenu from 'components/LeftMenu.vue';
|
import LeftMenu from 'components/LeftMenu.vue';
|
||||||
|
|
||||||
const state = useState();
|
const stateStore = useStateStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||||
<q-scroll-area class="fit text-grey-8">
|
<QScrollArea class="fit text-grey-8">
|
||||||
<LeftMenu />
|
<LeftMenu />
|
||||||
</q-scroll-area>
|
</QScrollArea>
|
||||||
</q-drawer>
|
</QDrawer>
|
||||||
<q-page-container>
|
<QPageContainer>
|
||||||
<router-view></router-view>
|
<RouterView></RouterView>
|
||||||
</q-page-container>
|
</QPageContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#searchbar,
|
||||||
|
.search-panel {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,282 @@
|
||||||
|
<script setup>
|
||||||
|
import axios from 'axios';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
|
import { useArrayData } from 'composables/useArrayData';
|
||||||
|
import VnPaginate from 'components/ui/VnPaginate.vue';
|
||||||
|
import VnConfirm from 'components/ui/VnConfirm.vue';
|
||||||
|
import CustomerDescriptorProxy from './Card/CustomerDescriptorProxy.vue';
|
||||||
|
import { toDate, toCurrency } from 'filters/index';
|
||||||
|
import CustomerPaymentsFilter from './CustomerPaymentsFilter.vue';
|
||||||
|
|
||||||
|
const stateStore = useStateStore();
|
||||||
|
const quasar = useQuasar();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const arrayData = useArrayData('CustomerTransactions');
|
||||||
|
|
||||||
|
async function confirm(transaction) {
|
||||||
|
quasar
|
||||||
|
.dialog({
|
||||||
|
component: VnConfirm,
|
||||||
|
componentProps: {
|
||||||
|
data: transaction,
|
||||||
|
title: t('Confirm transaction'),
|
||||||
|
promise: confirmTransaction,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.onOk((row) => (row.isConfirmed = true));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmTransaction({ id }) {
|
||||||
|
await axios.post('Clients/confirmTransaction', { id });
|
||||||
|
quasar.notify({
|
||||||
|
message: t('Payment confirmed'),
|
||||||
|
type: 'positive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const grid = ref(false);
|
||||||
|
const columns = computed(() => [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
label: t('Transaction ID'),
|
||||||
|
field: (row) => row.id,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customerId',
|
||||||
|
label: t('Customer ID'),
|
||||||
|
field: (row) => row.clientFk,
|
||||||
|
align: 'right',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customer',
|
||||||
|
label: t('Customer Name'),
|
||||||
|
field: (row) => row.customerName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'state',
|
||||||
|
label: t('State'),
|
||||||
|
field: (row) => row.isConfirmed,
|
||||||
|
format: (value) => (value ? t('Confirmed') : t('Unconfirmed')),
|
||||||
|
align: 'left',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dated',
|
||||||
|
label: t('Dated'),
|
||||||
|
field: (row) => toDate(row.created),
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'amount',
|
||||||
|
label: t('Amount'),
|
||||||
|
field: (row) => row.amount,
|
||||||
|
format: (value) => toCurrency(value),
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: t('Actions'),
|
||||||
|
grid: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const isLoading = computed(() => arrayData.isLoading.value);
|
||||||
|
|
||||||
|
function stateColor(row) {
|
||||||
|
if (row.isConfirmed) return 'positive';
|
||||||
|
return 'primary';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="stateStore.isHeaderMounted()">
|
||||||
|
<Teleport to="#actions-append">
|
||||||
|
<div class="row q-gutter-x-sm">
|
||||||
|
<QBtn
|
||||||
|
flat
|
||||||
|
@click="stateStore.toggleRightDrawer()"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
icon="menu"
|
||||||
|
>
|
||||||
|
<QTooltip bottom anchor="bottom right">
|
||||||
|
{{ t('globals.collapseMenu') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
|
||||||
|
<QScrollArea class="fit text-grey-8">
|
||||||
|
<CustomerPaymentsFilter data-key="CustomerTransactions" />
|
||||||
|
</QScrollArea>
|
||||||
|
</QDrawer>
|
||||||
|
<QPage class="column items-center q-pa-md">
|
||||||
|
<div class="card-list">
|
||||||
|
<QToolbar class="q-pa-none">
|
||||||
|
<QToolbarTitle>{{ t('Web Payments') }}</QToolbarTitle>
|
||||||
|
<QBtn
|
||||||
|
@click="arrayData.refresh()"
|
||||||
|
:loading="isLoading"
|
||||||
|
icon="refresh"
|
||||||
|
color="primary"
|
||||||
|
class="q-mr-sm"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
></QBtn>
|
||||||
|
<QBtn @click="grid = !grid" icon="list" color="primary" round dense>
|
||||||
|
<QTooltip>{{ t('Change view') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</QToolbar>
|
||||||
|
<VnPaginate
|
||||||
|
data-key="CustomerTransactions"
|
||||||
|
url="Clients/transactions"
|
||||||
|
order="created DESC"
|
||||||
|
:limit="20"
|
||||||
|
:offset="50"
|
||||||
|
auto-load
|
||||||
|
>
|
||||||
|
<template #body="{ rows }">
|
||||||
|
<QTable
|
||||||
|
:dense="$q.screen.lt.md"
|
||||||
|
:columns="columns"
|
||||||
|
:rows="rows"
|
||||||
|
row-key="id"
|
||||||
|
:pagination="{ rowsPerPage: 0 }"
|
||||||
|
:grid="grid || $q.screen.lt.sm"
|
||||||
|
class="q-mt-xs"
|
||||||
|
hide-pagination
|
||||||
|
>
|
||||||
|
<template #body-cell-actions="{ row }">
|
||||||
|
<QTd auto-width class="text-center">
|
||||||
|
<QBtn
|
||||||
|
v-if="!row.isConfirmed"
|
||||||
|
icon="check"
|
||||||
|
@click="confirm(row)"
|
||||||
|
color="primary"
|
||||||
|
size="md"
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('Confirm transaction') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</QTd>
|
||||||
|
</template>
|
||||||
|
<template #body-cell-customerId="{ row }">
|
||||||
|
<QTd align="right">
|
||||||
|
<span class="link">
|
||||||
|
{{ row.clientFk }}
|
||||||
|
<CustomerDescriptorProxy :id="row.clientFk" />
|
||||||
|
</span>
|
||||||
|
</QTd>
|
||||||
|
</template>
|
||||||
|
<template #body-cell-state="{ row }">
|
||||||
|
<QTd auto-width class="text-center">
|
||||||
|
<QBadge :color="stateColor(row)">
|
||||||
|
{{
|
||||||
|
row.isConfirmed
|
||||||
|
? t('Confirmed')
|
||||||
|
: t('Unconfirmed')
|
||||||
|
}}
|
||||||
|
</QBadge>
|
||||||
|
</QTd>
|
||||||
|
</template>
|
||||||
|
<template #item="{ cols, row }">
|
||||||
|
<div class="q-mb-md col-12">
|
||||||
|
<QCard>
|
||||||
|
<QItem class="q-pa-none items-start">
|
||||||
|
<QItemSection class="q-pa-md">
|
||||||
|
<QList>
|
||||||
|
<template
|
||||||
|
v-for="col of cols"
|
||||||
|
:key="col.name"
|
||||||
|
>
|
||||||
|
<QItem
|
||||||
|
v-if="col.grid !== false"
|
||||||
|
class="q-pa-none"
|
||||||
|
>
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ col.label }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel
|
||||||
|
v-if="col.name == 'state'"
|
||||||
|
>
|
||||||
|
<QBadge
|
||||||
|
:color="
|
||||||
|
stateColor(row)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ col.value }}
|
||||||
|
</QBadge>
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel
|
||||||
|
v-if="col.name != 'state'"
|
||||||
|
>
|
||||||
|
{{ col.value }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</template>
|
||||||
|
</QList>
|
||||||
|
</QItemSection>
|
||||||
|
<template v-if="!row.isConfirmed">
|
||||||
|
<QSeparator vertical />
|
||||||
|
<QCardActions
|
||||||
|
vertical
|
||||||
|
class="justify-between"
|
||||||
|
>
|
||||||
|
<QBtn
|
||||||
|
icon="check"
|
||||||
|
@click="confirm(row)"
|
||||||
|
color="primary"
|
||||||
|
size="md"
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('Confirm transaction') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</QCardActions>
|
||||||
|
</template>
|
||||||
|
</QItem>
|
||||||
|
</QCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</QTable>
|
||||||
|
</template>
|
||||||
|
</VnPaginate>
|
||||||
|
</div>
|
||||||
|
</QPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-list {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
Web Payments: Pagos Web
|
||||||
|
Confirm transaction: Confirmar transacción
|
||||||
|
Transaction ID: ID transacción
|
||||||
|
Customer ID: ID cliente
|
||||||
|
Customer Name: Nombre cliente
|
||||||
|
State: Estado
|
||||||
|
Dated: Fecha
|
||||||
|
Amount: Importe
|
||||||
|
Actions: Acciones
|
||||||
|
Confirmed: Confirmada
|
||||||
|
Unconfirmed: Sin confirmar
|
||||||
|
Change view: Cambiar vista
|
||||||
|
Payment confirmed: Pago confirmado
|
||||||
|
</i18n>
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const props = defineProps({
|
||||||
|
dataKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
|
||||||
|
<template #tags="{ tag, formatFn }">
|
||||||
|
<div class="q-gutter-x-xs">
|
||||||
|
<strong>{{ t(`params.${tag.label}`) }}: </strong>
|
||||||
|
<span>{{ formatFn(tag.value) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body="{ params }">
|
||||||
|
<QList dense>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput
|
||||||
|
:label="t('Order ID')"
|
||||||
|
v-model="params.orderFk"
|
||||||
|
lazy-rules
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="vn:basket" size="sm"></QIcon>
|
||||||
|
</template>
|
||||||
|
</QInput>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput
|
||||||
|
:label="t('Customer ID')"
|
||||||
|
v-model="params.clientFk"
|
||||||
|
lazy-rules
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="vn:client" size="sm"></QIcon>
|
||||||
|
</template>
|
||||||
|
</QInput>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
<QItem>
|
||||||
|
<QItemSection>
|
||||||
|
<QInput :label="t('Amount')" v-model="params.amount" lazy-rules>
|
||||||
|
<template #prepend>
|
||||||
|
<QIcon name="euro" size="sm"></QIcon>
|
||||||
|
</template>
|
||||||
|
</QInput>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</template>
|
||||||
|
</VnFilterPanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
en:
|
||||||
|
params:
|
||||||
|
orderFk: Order
|
||||||
|
clientFk: Customer
|
||||||
|
amount: Amount
|
||||||
|
es:
|
||||||
|
params:
|
||||||
|
orderFk: Pedido
|
||||||
|
clientFk: Cliente
|
||||||
|
amount: Importe
|
||||||
|
Order ID: ID pedido
|
||||||
|
Customer ID: ID cliente
|
||||||
|
Amount: Importe
|
||||||
|
</i18n>
|
|
@ -1,39 +1,48 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, computed } from 'vue';
|
import { onMounted, computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useState } from 'src/composables/useState';
|
|
||||||
import LeftMenu from 'components/LeftMenu.vue';
|
import LeftMenu from 'components/LeftMenu.vue';
|
||||||
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
import { useNavigationStore } from 'src/stores/useNavigationStore';
|
import { useNavigationStore } from 'src/stores/useNavigationStore';
|
||||||
|
|
||||||
const state = useState();
|
const stateStore = useStateStore();
|
||||||
const navigation = useNavigationStore();
|
const navigation = useNavigationStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => navigation.fetchPinned());
|
||||||
navigation.fetchPinned();
|
|
||||||
});
|
|
||||||
|
|
||||||
const pinnedModules = computed(() => navigation.getPinnedModules());
|
const pinnedModules = computed(() => navigation.getPinnedModules());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
<QDrawer
|
||||||
<q-scroll-area class="fit text-grey-8">
|
v-model="stateStore.leftDrawer"
|
||||||
|
show-if-above
|
||||||
|
:width="256"
|
||||||
|
:breakpoint="1000"
|
||||||
|
>
|
||||||
|
<QScrollArea class="fit text-grey-8">
|
||||||
<LeftMenu />
|
<LeftMenu />
|
||||||
</q-scroll-area>
|
</QScrollArea>
|
||||||
</q-drawer>
|
</QDrawer>
|
||||||
<q-page-container>
|
<QPageContainer>
|
||||||
<q-page class="q-pa-md">
|
<QPage class="q-pa-md">
|
||||||
<div class="row items-start wrap q-col-gutter-md q-mb-lg">
|
<div class="row items-start wrap q-col-gutter-md q-mb-lg">
|
||||||
<div class="col-12 col-md">
|
<div class="col-12 col-md">
|
||||||
<div class="text-h6 text-grey-8 q-mb-sm">{{ t('globals.pinnedModules') }}</div>
|
<div class="text-h6 text-grey-8 q-mb-sm">
|
||||||
<q-card class="row flex-container q-pa-md">
|
{{ t('globals.pinnedModules') }}
|
||||||
|
</div>
|
||||||
|
<QCard class="row flex-container q-pa-md">
|
||||||
<div class="text-grey-5" v-if="pinnedModules.length === 0">
|
<div class="text-grey-5" v-if="pinnedModules.length === 0">
|
||||||
{{ t('pinnedInfo') }}
|
{{ t('pinnedInfo') }}
|
||||||
</div>
|
</div>
|
||||||
<template v-if="pinnedModules.length">
|
<template v-if="pinnedModules.length">
|
||||||
<div v-for="item of pinnedModules" :key="item.title" class="row no-wrap q-pa-xs flex-item">
|
<div
|
||||||
<q-btn
|
v-for="item of pinnedModules"
|
||||||
|
:key="item.title"
|
||||||
|
class="row no-wrap q-pa-xs flex-item"
|
||||||
|
>
|
||||||
|
<QBtn
|
||||||
align="evenly"
|
align="evenly"
|
||||||
padding="16px"
|
padding="16px"
|
||||||
flat
|
flat
|
||||||
|
@ -47,14 +56,14 @@ const pinnedModules = computed(() => navigation.getPinnedModules());
|
||||||
<div class="text-center text-primary button-text">
|
<div class="text-center text-primary button-text">
|
||||||
{{ t(item.title) }}
|
{{ t(item.title) }}
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-card>
|
</QCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-page>
|
</QPage>
|
||||||
</q-page-container>
|
</QPageContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -1,18 +1,38 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useState } from 'src/composables/useState';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useStateStore } from 'stores/useStateStore';
|
||||||
import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue';
|
import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue';
|
||||||
|
import LeftMenu from 'components/LeftMenu.vue';
|
||||||
|
import VnSearchbar from 'components/ui/VnSearchbar.vue';
|
||||||
|
|
||||||
const state = useState();
|
const stateStore = useStateStore();
|
||||||
|
const { t } = useI18n();
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
|
||||||
<q-scroll-area class="fit">
|
<VnSearchbar
|
||||||
|
data-key="InvoiceOutList"
|
||||||
|
url="InvoiceOuts/filter"
|
||||||
|
:label="t('Search invoice')"
|
||||||
|
:info="t('You can search by invoice reference')"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||||
|
<QScrollArea class="fit">
|
||||||
<InvoiceOutDescriptor />
|
<InvoiceOutDescriptor />
|
||||||
</q-scroll-area>
|
<QSeparator />
|
||||||
</q-drawer>
|
<LeftMenu source="card" />
|
||||||
<q-page-container>
|
</QScrollArea>
|
||||||
<q-page class="q-pa-md">
|
</QDrawer>
|
||||||
<router-view></router-view>
|
<QPageContainer>
|
||||||
</q-page>
|
<QPage class="q-pa-md">
|
||||||
</q-page-container>
|
<RouterView></RouterView>
|
||||||
|
</QPage>
|
||||||
|
</QPageContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<i18n>
|
||||||
|
es:
|
||||||
|
Search invoice: Buscar factura emitida
|
||||||
|
You can search by invoice reference: Puedes buscar por referencia de la factura
|
||||||
|
</i18n>
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { toCurrency, toDate } from 'src/filters';
|
import { toCurrency, toDate } from 'src/filters';
|
||||||
import axios from 'axios';
|
import CardDescriptor from 'components/ui/CardDescriptor.vue';
|
||||||
import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
|
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
|
||||||
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
|
|
||||||
|
|
||||||
const $props = defineProps({
|
const $props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
|
@ -15,19 +14,14 @@ const $props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetch();
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const entityId = computed(() => {
|
const entityId = computed(() => {
|
||||||
return $props.id || route.params.id;
|
return $props.id || route.params.id;
|
||||||
});
|
});
|
||||||
|
const descriptor = ref();
|
||||||
|
|
||||||
const invoiceOut = ref();
|
|
||||||
async function fetch() {
|
|
||||||
const filter = {
|
const filter = {
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
|
@ -45,59 +39,80 @@ async function fetch() {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = { params: { filter } };
|
function ticketFilter(invoice) {
|
||||||
const { data } = await axios.get(`InvoiceOuts/${entityId.value}`, options);
|
return JSON.stringify({ refFk: invoice.ref });
|
||||||
if (data) invoiceOut.value = data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = computed(() => {
|
|
||||||
return invoiceOut.value ? JSON.stringify({ refFk: invoiceOut.value.ref }) : null;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<card-descriptor v-if="invoiceOut" module="InvoiceOut" :data="invoiceOut" :description="invoiceOut.ref">
|
<CardDescriptor
|
||||||
<template #body>
|
ref="descriptor"
|
||||||
<q-list>
|
module="InvoiceOut"
|
||||||
<q-item>
|
:url="`InvoiceOuts/${entityId}`"
|
||||||
<q-item-section>
|
:filter="filter"
|
||||||
<q-item-label caption>{{ t('invoiceOut.card.issued') }}</q-item-label>
|
>
|
||||||
<q-item-label>{{ toDate(invoiceOut.issued) }}</q-item-label>
|
<template #description="{ entity }">
|
||||||
</q-item-section>
|
<span>
|
||||||
<q-item-section>
|
{{ entity.ref }}
|
||||||
<q-item-label caption>{{ t('invoiceOut.card.amount') }}</q-item-label>
|
<QTooltip>{{ entity.ref }}</QTooltip>
|
||||||
<q-item-label>{{ toCurrency(invoiceOut.amount) }}</q-item-label>
|
</span>
|
||||||
</q-item-section>
|
</template>
|
||||||
</q-item>
|
<template #body="{ entity }">
|
||||||
<q-item>
|
<QList>
|
||||||
<q-item-section v-if="invoiceOut.company">
|
<QItem>
|
||||||
<q-item-label caption>{{ t('invoiceOut.card.client') }}</q-item-label>
|
<QItemSection>
|
||||||
<q-item-label class="link">
|
<QItemLabel caption>
|
||||||
{{ invoiceOut.client.name }}
|
{{ t('invoiceOut.card.issued') }}
|
||||||
<q-popup-proxy>
|
</QItemLabel>
|
||||||
<customer-descriptor-popover :id="invoiceOut.client.id" />
|
<QItemLabel>{{ toDate(entity.issued) }}</QItemLabel>
|
||||||
</q-popup-proxy>
|
</QItemSection>
|
||||||
</q-item-label>
|
<QItemSection>
|
||||||
</q-item-section>
|
<QItemLabel caption>
|
||||||
<q-item-section v-if="invoiceOut.company">
|
{{ t('invoiceOut.card.amount') }}
|
||||||
<q-item-label caption>{{ t('invoiceOut.card.company') }}</q-item-label>
|
</QItemLabel>
|
||||||
<q-item-label>{{ invoiceOut.company.code }}</q-item-label>
|
<QItemLabel>{{ toCurrency(entity.amount) }}</QItemLabel>
|
||||||
</q-item-section>
|
</QItemSection>
|
||||||
</q-item>
|
</QItem>
|
||||||
</q-list>
|
<QItem>
|
||||||
<q-card-actions>
|
<QItemSection v-if="entity.client">
|
||||||
<q-btn
|
<QItemLabel caption>
|
||||||
|
{{ t('invoiceOut.card.client') }}
|
||||||
|
</QItemLabel>
|
||||||
|
<QItemLabel class="link">
|
||||||
|
{{ entity.client.name }}
|
||||||
|
<CustomerDescriptorProxy :id="entity.client.id" />
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
<QItemSection v-if="entity.company">
|
||||||
|
<QItemLabel caption>{{
|
||||||
|
t('invoiceOut.card.company')
|
||||||
|
}}</QItemLabel>
|
||||||
|
<QItemLabel>{{ entity.company.code }}</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
<QCardActions>
|
||||||
|
<QBtn
|
||||||
|
v-if="entity.client"
|
||||||
size="md"
|
size="md"
|
||||||
icon="vn:client"
|
icon="vn:client"
|
||||||
color="primary"
|
color="primary"
|
||||||
:to="{ name: 'CustomerCard', params: { id: invoiceOut.client.id } }"
|
:to="{ name: 'CustomerCard', params: { id: entity.client.id } }"
|
||||||
>
|
>
|
||||||
<q-tooltip>{{ t('invoiceOut.card.customerCard') }}</q-tooltip>
|
<QTooltip>{{ t('invoiceOut.card.customerCard') }}</QTooltip>
|
||||||
</q-btn>
|
</QBtn>
|
||||||
<q-btn size="md" icon="vn:ticket" color="primary" :to="{ name: 'TicketList', params: { q: filter } }">
|
<QBtn
|
||||||
<q-tooltip>{{ t('invoiceOut.card.ticketList') }}</q-tooltip>
|
size="md"
|
||||||
</q-btn>
|
icon="vn:ticket"
|
||||||
</q-card-actions>
|
color="primary"
|
||||||
|
:to="{
|
||||||
|
name: 'TicketList',
|
||||||
|
query: { q: ticketFilter(entity) },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('invoiceOut.card.ticketList') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
</QCardActions>
|
||||||
</template>
|
</template>
|
||||||
</card-descriptor>
|
</CardDescriptor>
|
||||||
</template>
|
</template>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue