Compare commits

..

No commits in common. "dev" and "8449-fixes_2" have entirely different histories.

230 changed files with 3987 additions and 4137 deletions

6
.eslintignore Normal file
View File

@ -0,0 +1,6 @@
/dist
/src-capacitor
/src-cordova
/.quasar
/node_modules
.eslintrc.js

75
.eslintrc.js Normal file
View File

@ -0,0 +1,75 @@
export default {
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
// This option interrupts the configuration hierarchy at this file
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
root: true,
parserOptions: {
ecmaVersion: '2021', // Allows for the parsing of modern ECMAScript features
},
env: {
node: true,
browser: true,
'vue/setup-compiler-macros': true,
},
// Rules order is important, please avoid shuffling them
extends: [
// Base ESLint recommended rules
'eslint:recommended',
// Uncomment any of the lines below to choose desired strictness,
// but leave only one uncommented!
// See https://eslint.vuejs.org/rules/#available-rules
// 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
// https://github.com/prettier/eslint-config-prettier#installation
// usage with Prettier, provided by 'eslint-config-prettier'.
'prettier',
],
plugins: [
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
// required to lint *.vue files
'vue',
// https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
// Prettier has not been included as plugin to avoid performance impact
// add it as an extension for your IDE
],
globals: {
ga: 'readonly', // Google Analytics
cordova: 'readonly',
__statics: 'readonly',
__QUASAR_SSR__: 'readonly',
__QUASAR_SSR_SERVER__: 'readonly',
__QUASAR_SSR_CLIENT__: 'readonly',
__QUASAR_SSR_PWA__: 'readonly',
process: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly',
},
// add your custom rules here
rules: {
'prefer-promise-reject-errors': 'off',
'no-unused-vars': 'warn',
'vue/no-multiple-template-root': 'off',
// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
overrides: [
{
files: ['test/cypress/**/*.*'],
extends: [
// Add Cypress-specific lint rules, globals and Cypress plugin
// See https://github.com/cypress-io/eslint-plugin-cypress#rules
'plugin:cypress/recommended',
],
},
],
};

View File

@ -1,3 +0,0 @@
{
"extends": ["plugin:cypress/recommended"]
}

5
Jenkinsfile vendored
View File

@ -125,10 +125,8 @@ pipeline {
sh "docker-compose ${env.COMPOSE_PARAMS} pull db" sh "docker-compose ${env.COMPOSE_PARAMS} pull db"
sh "docker-compose ${env.COMPOSE_PARAMS} up -d" sh "docker-compose ${env.COMPOSE_PARAMS} up -d"
def modules = sh(script: 'node test/cypress/docker/find/find.js', returnStdout: true).trim()
echo "E2E MODULES: ${modules}"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") { image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") {
sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'" sh 'sh test/cypress/cypressParallel.sh 1'
} }
} }
} }
@ -185,4 +183,3 @@ pipeline {
} }
} }
} }

View File

@ -1,87 +0,0 @@
import cypress from 'eslint-plugin-cypress';
import eslint from 'eslint-plugin-import';
import globals from 'globals';
import js from '@eslint/js';
import vue from 'eslint-plugin-vue';
export default {
plugins: { vue, eslint, cypress },
languageOptions: {
globals: {
...globals.node,
...globals.browser,
...vue.configs['vue3-strongly-recommended'].globals,
...cypress.environments.globals.globals,
ga: 'readonly',
cordova: 'readonly',
__statics: 'readonly',
__QUASAR_SSR__: 'readonly',
__QUASAR_SSR_SERVER__: 'readonly',
__QUASAR_SSR_CLIENT__: 'readonly',
__QUASAR_SSR_PWA__: 'readonly',
process: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly',
},
ecmaVersion: 2020,
sourceType: 'module',
parserOptions: {
parser: '@babel/eslint-parser',
},
},
rules: {
...vue.rules['flat/strongly-recommended'],
...js.configs.recommended.rules,
semi: 'off',
'generator-star-spacing': 'warn',
'arrow-parens': 'warn',
'no-var': 'error',
'prefer-const': 'error',
'prefer-template': 'warn',
'prefer-destructuring': 'off',
'prefer-spread': 'warn',
'prefer-rest-params': 'warn',
'prefer-object-spread': 'warn',
'prefer-arrow-callback': 'warn',
'prefer-numeric-literals': 'warn',
'prefer-exponentiation-operator': 'warn',
'prefer-regex-literals': 'warn',
'one-var': [
'error',
{
let: 'never',
const: 'never',
},
],
'no-void': 'off',
'prefer-promise-reject-errors': 'error',
'multiline-ternary': 'warn',
'no-restricted-imports': 'warn',
'no-import-assign': 'warn',
'no-duplicate-imports': 'warn',
'no-useless-rename': 'warn',
'eslint/no-named-as-default': 'warn',
'eslint/no-named-as-default-member': 'warn',
'no-unsafe-optional-chaining': 'warn',
'no-undef': 'error',
'no-unused-vars': 'error',
'no-console': 'error',
'no-debugger': 'error',
'no-useless-escape': 'error',
'no-prototype-builtins': 'error',
'no-async-promise-executor': 'error',
'no-irregular-whitespace': 'error',
'no-constant-condition': 'error',
'no-unsafe-finally': 'error',
'no-extend-native': 'error',
},
ignores: [
'/dist',
'/src-capacitor',
'/src-cordova',
'/.quasar',
'/node_modules',
'.eslintrc.js',
],
};

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "25.16.0", "version": "25.14.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -9,8 +9,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"resetDatabase": "cd ../salix && gulp docker", "resetDatabase": "cd ../salix && gulp docker",
"lint": "eslint \"**/*.{vue,js}\" ", "lint": "eslint --ext .js,.vue ./",
"lint:fix": "eslint \"**/*.{vue,js}\" --fix ",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test:e2e": "cypress open", "test:e2e": "cypress open",
"test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run", "test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run",
@ -18,8 +17,6 @@
"test:e2e:summary": "bash ./test/cypress/summary.sh", "test:e2e:summary": "bash ./test/cypress/summary.sh",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:front": "vitest", "test:front": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:front:ci": "vitest run", "test:front:ci": "vitest run",
"commitlint": "commitlint --edit", "commitlint": "commitlint --edit",
"prepare": "npx husky install", "prepare": "npx husky install",
@ -29,53 +26,43 @@
"docs:preview": "vitepress preview docs" "docs:preview": "vitepress preview docs"
}, },
"dependencies": { "dependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.20.0",
"@quasar/cli": "^2.4.1", "@quasar/cli": "^2.4.1",
"@quasar/extras": "^1.16.16", "@quasar/extras": "^1.16.16",
"axios": "^1.4.0", "axios": "^1.4.0",
"chromium": "^3.0.3", "chromium": "^3.0.3",
"croppie": "^2.6.5", "croppie": "^2.6.5",
"es-module-lexer": "^1.6.0",
"fast-glob": "^3.3.3",
"moment": "^2.30.1", "moment": "^2.30.1",
"pinia": "^2.1.3", "pinia": "^2.1.3",
"quasar": "^2.17.7", "quasar": "^2.17.7",
"validator": "^13.9.0", "validator": "^13.9.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^9.4.0", "vue-i18n": "^9.3.0",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.2.1", "@commitlint/cli": "^19.2.1",
"@commitlint/config-conventional": "^19.1.0", "@commitlint/config-conventional": "^19.1.0",
"@eslint/eslintrc": "^3.2.0", "@intlify/unplugin-vue-i18n": "^0.8.2",
"@eslint/js": "^9.20.0",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@pinia/testing": "^0.1.2", "@pinia/testing": "^0.1.2",
"@quasar/app-vite": "^2.0.8", "@quasar/app-vite": "^2.0.8",
"@quasar/quasar-app-extension-qcalendar": "^4.0.2", "@quasar/quasar-app-extension-qcalendar": "^4.0.2",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0", "@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0",
"@vitest/ui": "3.1.1",
"@vue/compiler-sfc": "^3.5.13",
"@vue/test-utils": "^2.4.4", "@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"cypress": "^14.1.0", "cypress": "^14.1.0",
"cypress-mochawesome-reporter": "^3.8.2", "cypress-mochawesome-reporter": "^3.8.2",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-cypress": "^4.1.0", "eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-vue": "^9.32.0", "eslint-plugin-vue": "^9.32.0",
"globals": "^16.0.0",
"husky": "^8.0.0", "husky": "^8.0.0",
"junit-merge": "^2.0.0", "junit-merge": "^2.0.0",
"mocha": "^11.1.0", "mocha": "^11.1.0",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"sass": "^1.83.4", "sass": "^1.83.4",
"vitest": "^3.0.3", "vitepress": "^1.6.3",
"vitest": "^0.34.0",
"xunit-viewer": "^10.6.1" "xunit-viewer": "^10.6.1"
}, },
"engines": { "engines": {

File diff suppressed because it is too large Load Diff

View File

@ -53,7 +53,7 @@ export default configure(function (/* ctx */) {
build: { build: {
target: { target: {
browser: ['es2022', 'edge88', 'firefox78', 'chrome87', 'safari13.1'], browser: ['es2022', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
node: 'node20', node: 'node18',
}, },
vueRouterMode: 'hash', // available values: 'hash', 'history' vueRouterMode: 'hash', // available values: 'hash', 'history'
@ -92,7 +92,6 @@ export default configure(function (/* ctx */) {
vitePlugins: [ vitePlugins: [
[ [
VueI18nPlugin({ VueI18nPlugin({
strictMessage: false,
runtimeOnly: false, runtimeOnly: false,
include: [ include: [
path.resolve(__dirname, './src/i18n/locale/**'), path.resolve(__dirname, './src/i18n/locale/**'),

View File

@ -1,227 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* 1. DO NOT edit this file directly as it won't do anything.
* 2. EDIT the original quasar.config file INSTEAD.
* 3. DO NOT git commit this file. It should be ignored.
*
* This file is still here because there was an error in
* the original quasar.config file and this allows you to
* investigate the Node.js stack error.
*
* After you fix the original file, this file will be
* deleted automatically.
**/
// quasar.config.js
import { configure } from "quasar/wrappers";
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
import path from "path";
var __quasar_inject_dirname__ = "/home/jsegarra/Projects/salix-front";
var target = `http://${process.env.CI ? "back" : "localhost"}:3000`;
var quasar_config_default = configure(function() {
return {
eslint: {
// fix: true,
// include = [],
// exclude = [],
// rawOptions = {},
warnings: true,
errors: true
},
// https://v2.quasar.dev/quasar-cli/prefetch-feature
// preFetch: true,
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: ["i18n", "axios", "vnDate", "validations", "quasar", "quasar.defaults"],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ["app.scss"],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
// 'mdi-v5',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
"roboto-font",
"material-icons-outlined",
"material-symbols-outlined"
],
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: {
target: {
browser: ["es2022", "edge88", "firefox78", "chrome87", "safari13.1"],
node: "node20"
},
vueRouterMode: "hash",
// available values: 'hash', 'history'
// vueRouterBase,
// vueDevtools,
// vueOptionsAPI: false,
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
// publicPath: '/',
// analyze: true,
// env: {},
rawDefine: {
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV)
},
// ignorePublicFolder: true,
// minify: false,
// polyfillModulePreload: true,
// distDir
extendViteConf(viteConf) {
delete viteConf.build.polyfillModulePreload;
viteConf.build.modulePreload = {
polyfill: false
};
},
// viteVuePluginOptions: {},
alias: {
composables: path.join(__quasar_inject_dirname__, "./src/composables"),
filters: path.join(__quasar_inject_dirname__, "./src/filters")
},
vitePlugins: [
[
VueI18nPlugin({
strictMessage: false,
runtimeOnly: false,
include: [
path.resolve(__quasar_inject_dirname__, "./src/i18n/locale/**"),
path.resolve(__quasar_inject_dirname__, "./src/pages/**/locale/**")
]
})
]
]
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
server: {
type: "http"
},
proxy: {
"/api": {
target,
logLevel: "debug",
changeOrigin: true,
secure: false
}
},
open: false,
allowedHosts: [
"front",
// Agrega este nombre de host
"localhost"
// Opcional, para pruebas locales
]
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: {
config: {
config: {
dark: "auto"
}
},
lang: "en-GB",
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],
// Quasar plugins
plugins: ["Notify", "Dialog"],
all: "auto",
autoImportComponentCase: "pascal"
},
// animations: 'all', // --- includes all animations
// https://v2.quasar.dev/options/animations
animations: [],
// 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: {
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
// will mess up SSR
// extendSSRWebserverConf (esbuildConf) {},
// extendPackageJson (json) {},
pwa: false,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
prodPort: 3e3,
// The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
middlewares: [
"render"
// keep this as last one
]
},
// https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa
pwa: {
workboxMode: "generateSW",
// or 'injectManifest'
injectPwaMetaTags: true,
swFilename: "sw.js",
manifestFilename: "manifest.json",
useCredentialsForManifestTag: false
// useFilenameHashes: true,
// extendGenerateSWOptions (cfg) {}
// extendInjectManifestOptions (cfg) {},
// extendManifestJson (json) {}
// extendPWACustomSWConf (esbuildConf) {}
},
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
electron: {
// extendElectronMainConf (esbuildConf)
// extendElectronPreloadConf (esbuildConf)
inspectPort: 5858,
bundler: "packager",
// 'packager' or 'builder'
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
builder: {
// https://www.electron.build/configuration/configuration
appId: "salix-frontend"
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
bex: {
contentScripts: ["my-content-script"]
// extendBexScriptsConf (esbuildConf) {}
// extendBexManifestJson (json) {}
}
};
});
export {
quasar_config_default as default
};

View File

@ -9,30 +9,6 @@ vi.mock('src/composables/useSession', () => ({
}), }),
})); }));
// Mock axios
vi.mock('axios', () => ({
default: {
create: vi.fn(() => ({
interceptors: {
request: { use: vi.fn() },
response: { use: vi.fn() },
},
})),
interceptors: {
request: { use: vi.fn() },
response: { use: vi.fn() },
},
defaults: {
baseURL: '',
},
},
}));
vi.mock('src/router', () => ({
Router: {
push: vi.fn(),
},
}));
vi.mock('src/stores/useStateQueryStore', () => ({ vi.mock('src/stores/useStateQueryStore', () => ({
useStateQueryStore: () => ({ useStateQueryStore: () => ({
add: () => vi.fn(), add: () => vi.fn(),
@ -53,7 +29,7 @@ describe('Axios boot', () => {
'Accept-Language': 'en-US', 'Accept-Language': 'en-US',
Authorization: 'DEFAULT_TOKEN', Authorization: 'DEFAULT_TOKEN',
}, },
}), })
); );
}); });
}); });

View File

@ -1,4 +1,3 @@
/* eslint-disable eslint/export */
export * from './defaults/qTable'; export * from './defaults/qTable';
export * from './defaults/qInput'; export * from './defaults/qInput';
export * from './defaults/qSelect'; export * from './defaults/qSelect';

View File

@ -332,7 +332,6 @@ watch(formUrl, async () => {
:disable="!selected?.length" :disable="!selected?.length"
:title="t('globals.remove')" :title="t('globals.remove')"
v-if="$props.defaultRemove" v-if="$props.defaultRemove"
data-cy="crudModelDefaultRemoveBtn"
/> />
<QBtn <QBtn
:label="tMobile('globals.reset')" :label="tMobile('globals.reset')"

View File

@ -8,6 +8,11 @@ import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import { QCheckbox } from 'quasar'; import { QCheckbox } from 'quasar';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const emit = defineEmits(['onDataSaved']);
const $props = defineProps({ const $props = defineProps({
rows: { rows: {
type: Array, type: Array,
@ -21,14 +26,10 @@ const $props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
beforeSave: {
type: Function,
default: () => {},
},
}); });
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits(['onDataSaved']); const { notify } = useNotify();
const inputs = { const inputs = {
input: markRaw(VnInput), input: markRaw(VnInput),
@ -43,13 +44,24 @@ const selectedField = ref(null);
const closeButton = ref(null); const closeButton = ref(null);
const isLoading = ref(false); const isLoading = ref(false);
const onDataSaved = () => {
notify('globals.dataSaved', 'positive');
emit('onDataSaved');
closeForm();
};
const onSubmit = async () => { const onSubmit = async () => {
isLoading.value = true; isLoading.value = true;
$props.rows.forEach((row) => { const rowsToEdit = $props.rows.map((row) => ({ id: row.id, itemFk: row.itemFk }));
row[selectedField.value.name] = newValue.value; const payload = {
}); field: selectedField.value.field,
emit('onDataSaved', $props.rows); newValue: newValue.value,
closeForm(); lines: rowsToEdit,
};
await axios.post($props.editUrl, payload);
onDataSaved();
isLoading.value = false;
}; };
const closeForm = () => { const closeForm = () => {
@ -66,24 +78,21 @@ const closeForm = () => {
<span class="title">{{ t('Edit') }}</span> <span class="title">{{ t('Edit') }}</span>
<span class="countLines">{{ ` ${rows.length} ` }}</span> <span class="countLines">{{ ` ${rows.length} ` }}</span>
<span class="title">{{ t('buy(s)') }}</span> <span class="title">{{ t('buy(s)') }}</span>
<VnRow class="q-mt-md"> <VnRow>
<VnSelect <VnSelect
class="editOption"
:label="t('Field to edit')" :label="t('Field to edit')"
:options="fieldsOptions" :options="fieldsOptions"
hide-selected hide-selected
option-label="label" option-label="label"
v-model="selectedField" v-model="selectedField"
data-cy="EditFixedPriceSelectOption" data-cy="field-to-edit"
@update:model-value="newValue = null"
:class="{ 'is-select': selectedField?.component === 'select' }"
/> />
<component <component
:is="inputs[selectedField?.component || 'input']" :is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}" v-bind="selectedField?.attrs || {}"
v-model="newValue" v-model="newValue"
:label="t('Value')" :label="t('Value')"
data-cy="EditFixedPriceValueOption" data-cy="value-to-edit"
style="width: 200px" style="width: 200px"
/> />
</VnRow> </VnRow>
@ -131,15 +140,6 @@ const closeForm = () => {
} }
</style> </style>
<style lang="scss">
.editOption .q-field__inner .q-field__control {
padding: 0 !important;
}
.editOption.is-select .q-field__inner .q-field__control {
padding: 0 !important;
}
</style>
<i18n> <i18n>
es: es:
Edit: Editar Edit: Editar

View File

@ -13,12 +13,13 @@ import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile'; import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { getDifferences, getUpdatedValues } from 'src/filters'; import { getDifferences, getUpdatedValues } from 'src/filters';
const { push } = useRouter(); const { push } = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const state = useState(); const state = useState();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const { validate, validations } = useValidator(); const { validate } = useValidator();
const { notify } = useNotify(); const { notify } = useNotify();
const route = useRoute(); const route = useRoute();
const myForm = ref(null); const myForm = ref(null);
@ -118,7 +119,7 @@ const defaultButtons = computed(() => ({
color: 'primary', color: 'primary',
icon: 'save', icon: 'save',
label: 'globals.save', label: 'globals.save',
click: async (evt) => submitForm(evt), click: async () => await save(),
type: 'submit', type: 'submit',
}, },
reset: { reset: {
@ -131,13 +132,6 @@ const defaultButtons = computed(() => ({
...$props.defaultButtons, ...$props.defaultButtons,
})); }));
const submitForm = async (evt) => {
const isFormValid = await myForm.value.validate();
if (isFormValid) {
await save(evt);
}
};
onMounted(async () => { onMounted(async () => {
nextTick(() => (componentIsRendered.value = true)); nextTick(() => (componentIsRendered.value = true));
@ -233,9 +227,10 @@ async function save() {
const method = $props.urlCreate ? 'post' : 'patch'; const method = $props.urlCreate ? 'post' : 'patch';
const url = const url =
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url; $props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
const response = await Promise.resolve( let response;
$props.saveFn ? $props.saveFn(body) : axios[method](url, body),
); if ($props.saveFn) response = await $props.saveFn(body);
else response = await axios[method](url, body);
if ($props.urlCreate) notify('globals.dataCreated', 'positive'); if ($props.urlCreate) notify('globals.dataCreated', 'positive');
@ -312,13 +307,11 @@ async function onKeyup(evt) {
selectionStart = selectionEnd = selectionStart + 1; selectionStart = selectionEnd = selectionStart + 1;
return; return;
} }
await myForm.value.submit(evt); await save();
} }
} }
defineExpose({ defineExpose({
submitForm,
myForm,
save, save,
isLoading, isLoading,
hasChanges, hasChanges,
@ -332,7 +325,7 @@ defineExpose({
<QForm <QForm
ref="myForm" ref="myForm"
v-if="formData" v-if="formData"
@submit.prevent="save" @submit.prevent
@keyup.prevent="onKeyup" @keyup.prevent="onKeyup"
@reset="reset" @reset="reset"
class="q-pa-md" class="q-pa-md"
@ -346,7 +339,6 @@ defineExpose({
name="form" name="form"
:data="formData" :data="formData"
:validate="validate" :validate="validate"
:validations="validations()"
:filter="filter" :filter="filter"
/> />
<SkeletonForm v-else /> <SkeletonForm v-else />

View File

@ -41,12 +41,9 @@ const onDataSaved = async (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse); emit('onDataSaved', formData, requestResponse);
}; };
const onClick = async (saveAndContinue = showSaveAndContinueBtn) => { const onClick = async (saveAndContinue) => {
await formModelRef.value.myForm.validate(true);
isSaveAndContinue.value = saveAndContinue; isSaveAndContinue.value = saveAndContinue;
if (formModelRef.value) { await formModelRef.value.save();
await formModelRef.value.submitForm();
}
}; };
defineExpose({ defineExpose({
@ -62,23 +59,16 @@ defineExpose({
ref="formModelRef" ref="formModelRef"
:observe-form-changes="false" :observe-form-changes="false"
:default-actions="false" :default-actions="false"
@submit="onClick"
v-bind="$attrs" v-bind="$attrs"
@on-data-saved="onDataSaved" @on-data-saved="onDataSaved"
:prevent-submit="false"
> >
<template #form="{ data, validate, validations }"> <template #form="{ data, validate }">
<span ref="closeButton" class="close-icon" v-close-popup> <span ref="closeButton" class="close-icon" v-close-popup>
<QIcon name="close" size="sm" /> <QIcon name="close" size="sm" />
</span> </span>
<h1 class="title">{{ title }}</h1> <h1 class="title">{{ title }}</h1>
<p>{{ subtitle }}</p> <p>{{ subtitle }}</p>
<slot <slot name="form-inputs" :data="data" :validate="validate" />
name="form-inputs"
:data="data"
:validate="validate"
:validations="validations"
/>
<div class="q-mt-lg row justify-end"> <div class="q-mt-lg row justify-end">
<QBtn <QBtn
:label="t('globals.cancel')" :label="t('globals.cancel')"
@ -97,13 +87,12 @@ defineExpose({
:flat="showSaveAndContinueBtn" :flat="showSaveAndContinueBtn"
:label="t('globals.save')" :label="t('globals.save')"
:title="t('globals.save')" :title="t('globals.save')"
:type="!showSaveAndContinueBtn ? 'submit' : 'button'" @click="onClick(false)"
color="primary" color="primary"
class="q-ml-sm" class="q-ml-sm"
:disabled="isLoading" :disabled="isLoading"
:loading="isLoading" :loading="isLoading"
data-cy="FormModelPopup_save" data-cy="FormModelPopup_save"
@click="showSaveAndContinueBtn ? onClick(false) : null"
z-max z-max
/> />
<QBtn <QBtn
@ -111,13 +100,12 @@ defineExpose({
:label="t('globals.isSaveAndContinue')" :label="t('globals.isSaveAndContinue')"
:title="t('globals.isSaveAndContinue')" :title="t('globals.isSaveAndContinue')"
color="primary" color="primary"
:type="showSaveAndContinueBtn ? 'submit' : 'button'"
class="q-ml-sm" class="q-ml-sm"
:disabled="isLoading" :disabled="isLoading"
:loading="isLoading" :loading="isLoading"
data-cy="FormModelPopup_isSaveAndContinue" data-cy="FormModelPopup_isSaveAndContinue"
@click="showSaveAndContinueBtn ? onClick(true) : null"
z-max z-max
@click="onClick(true)"
/> />
</div> </div>
</template> </template>

View File

@ -1,22 +1,12 @@
<script setup> <script setup>
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import { getValueFromPath } from 'src/composables/getValueFromPath';
const { row, visibleProblems = null } = defineProps({ defineProps({ row: { type: Object, required: true } });
row: { type: Object, required: true },
visibleProblems: { type: Array },
});
function showProblem(problem) {
const val = getValueFromPath(row, problem);
if (!visibleProblems) return val;
return !!(visibleProblems?.includes(problem) && val);
}
</script> </script>
<template> <template>
<span class="q-gutter-x-xs"> <span class="q-gutter-x-xs">
<router-link <router-link
v-if="showProblem('claim.claimFk')" v-if="row.claim?.claimFk"
:to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }" :to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }"
class="link" class="link"
> >
@ -28,7 +18,7 @@ function showProblem(problem) {
</QIcon> </QIcon>
</router-link> </router-link>
<QIcon <QIcon
v-if="showProblem('isDeleted')" v-if="row?.isDeleted"
color="primary" color="primary"
name="vn:deletedTicket" name="vn:deletedTicket"
size="xs" size="xs"
@ -39,7 +29,7 @@ function showProblem(problem) {
</QTooltip> </QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
v-if="showProblem('hasRisk')" v-if="row?.hasRisk"
name="vn:risk" name="vn:risk"
:color="row.hasHighRisk ? 'negative' : 'primary'" :color="row.hasHighRisk ? 'negative' : 'primary'"
size="xs" size="xs"
@ -50,76 +40,51 @@ function showProblem(problem) {
</QTooltip> </QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
v-if="showProblem('hasComponentLack')" v-if="row?.hasComponentLack"
name="vn:components" name="vn:components"
color="primary" color="primary"
size="xs" size="xs"
> >
<QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip> <QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon v-if="row?.hasItemDelay" color="primary" size="xs" name="vn:hasItemDelay">
v-if="showProblem('hasItemDelay')"
color="primary"
size="xs"
name="vn:hasItemDelay"
>
<QTooltip> <QTooltip>
{{ $t('ticket.summary.hasItemDelay') }} {{ $t('ticket.summary.hasItemDelay') }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon v-if="row?.hasItemLost" color="primary" size="xs" name="vn:hasItemLost">
v-if="showProblem('hasItemLost')"
color="primary"
size="xs"
name="vn:hasItemLost"
>
<QTooltip> <QTooltip>
{{ $t('salesTicketsTable.hasItemLost') }} {{ $t('salesTicketsTable.hasItemLost') }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
v-if="showProblem('hasItemShortage')" v-if="row?.hasItemShortage"
name="vn:unavailable" name="vn:unavailable"
color="primary" color="primary"
size="xs" size="xs"
> >
<QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon v-if="row?.hasRounding" color="primary" name="sync_problem" size="xs">
v-if="showProblem('hasRounding')"
color="primary"
name="sync_problem"
size="xs"
>
<QTooltip> <QTooltip>
{{ $t('ticketList.rounding') }} {{ $t('ticketList.rounding') }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
v-if="showProblem('hasTicketRequest')" v-if="row?.hasTicketRequest"
name="vn:buyrequest" name="vn:buyrequest"
color="primary" color="primary"
size="xs" size="xs"
> >
<QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon v-if="row?.isTaxDataChecked" name="vn:no036" color="primary" size="xs">
v-if="showProblem('isTaxDataChecked')"
name="vn:no036"
color="primary"
size="xs"
>
<QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip>
</QIcon> </QIcon>
<QIcon v-if="showProblem('isFreezed')" name="vn:frozen" color="primary" size="xs"> <QIcon v-if="row?.isFreezed" name="vn:frozen" color="primary" size="xs">
<QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon v-if="row?.isTooLittle" name="vn:isTooLittle" color="primary" size="xs">
v-if="showProblem('isTooLittle')"
name="vn:isTooLittle"
color="primary"
size="xs"
>
<QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip> <QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip>
</QIcon> </QIcon>
</span> </span>

View File

@ -19,7 +19,6 @@ import { useQuasar, date } from 'quasar';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useFilterParams } from 'src/composables/useFilterParams'; import { useFilterParams } from 'src/composables/useFilterParams';
import { dashIfEmpty, toDate } from 'src/filters'; import { dashIfEmpty, toDate } from 'src/filters';
import { useTableHeight } from './filters/useTableHeight';
import CrudModel from 'src/components/CrudModel.vue'; import CrudModel from 'src/components/CrudModel.vue';
import FormModelPopup from 'components/FormModelPopup.vue'; import FormModelPopup from 'components/FormModelPopup.vue';
@ -118,7 +117,7 @@ const $props = defineProps({
}, },
tableHeight: { tableHeight: {
type: String, type: String,
default: undefined, default: '90vh',
}, },
footer: { footer: {
type: Boolean, type: Boolean,
@ -167,7 +166,6 @@ const tableRef = ref();
const params = ref(useFilterParams($attrs['data-key']).params); const params = ref(useFilterParams($attrs['data-key']).params);
const orders = ref(useFilterParams($attrs['data-key']).orders); const orders = ref(useFilterParams($attrs['data-key']).orders);
const app = inject('app'); const app = inject('app');
const tableHeight = useTableHeight();
const editingRow = ref(null); const editingRow = ref(null);
const editingField = ref(null); const editingField = ref(null);
@ -229,7 +227,6 @@ watch(
defineExpose({ defineExpose({
create: createForm, create: createForm,
showForm,
reload, reload,
redirect: redirectFn, redirect: redirectFn,
selected, selected,
@ -681,7 +678,7 @@ const rowCtrlClickFunction = computed(() => {
table-header-class="bg-header" table-header-class="bg-header"
card-container-class="grid-three" card-container-class="grid-three"
flat flat
:style="isTableMode && `max-height: ${$props.tableHeight || tableHeight}`" :style="isTableMode && `max-height: ${tableHeight}`"
:virtual-scroll="isTableMode" :virtual-scroll="isTableMode"
@virtual-scroll="handleScroll" @virtual-scroll="handleScroll"
@row-click="(event, row) => handleRowClick(event, row)" @row-click="(event, row) => handleRowClick(event, row)"
@ -1045,7 +1042,7 @@ const rowCtrlClickFunction = computed(() => {
:model="$attrs['data-key'] + 'Create'" :model="$attrs['data-key'] + 'Create'"
@on-data-saved="(_, res) => createForm.onDataSaved(res)" @on-data-saved="(_, res) => createForm.onDataSaved(res)"
> >
<template #form-inputs="{ data, validations }"> <template #form-inputs="{ data }">
<slot name="alter-create" :data="data"> <slot name="alter-create" :data="data">
<div :style="createComplement?.containerStyle"> <div :style="createComplement?.containerStyle">
<div <div
@ -1063,7 +1060,6 @@ const rowCtrlClickFunction = computed(() => {
:key="column.name" :key="column.name"
:name="`column-create-${column.name}`" :name="`column-create-${column.name}`"
:data="data" :data="data"
:validations="validations"
:column-name="column.name" :column-name="column.name"
:label="column.label" :label="column.label"
> >

View File

@ -1,7 +1,8 @@
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'; import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper } from 'app/test/vitest/helper';
import VnVisibleColumn from '../VnVisibleColumn.vue'; import VnVisibleColumn from '../VnVisibleColumn.vue';
import { default as axios } from 'axios'; import { axios } from 'app/test/vitest/helper';
describe('VnVisibleColumns', () => { describe('VnVisibleColumns', () => {
let wrapper; let wrapper;
let vm; let vm;

View File

@ -1,18 +0,0 @@
import { onMounted, nextTick, ref } from 'vue';
export function useTableHeight() {
const tableHeight = ref('90vh');
onMounted(async () => {
await nextTick();
let height = 100;
Array.from(document.querySelectorAll('[role="toolbar"]'))
.filter((element) => window.getComputedStyle(element).display !== 'none')
.forEach(() => {
height -= 10;
});
tableHeight.value = `${height}vh`;
});
return tableHeight;
}

View File

@ -1,6 +1,4 @@
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { default as axios } from 'axios';
import CrudModel from 'components/CrudModel.vue'; import CrudModel from 'components/CrudModel.vue';
import { vi, afterEach, beforeEach, beforeAll, describe, expect, it } from 'vitest'; import { vi, afterEach, beforeEach, beforeAll, describe, expect, it } from 'vitest';

View File

@ -0,0 +1,56 @@
import { createWrapper, axios } from 'app/test/vitest/helper';
import EditForm from 'components/EditTableCellValueForm.vue';
import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest';
const fieldA = 'fieldA';
const fieldB = 'fieldB';
describe('EditForm', () => {
let vm;
const mockRows = [
{ id: 1, itemFk: 101 },
{ id: 2, itemFk: 102 },
];
const mockFieldsOptions = [
{ label: 'Field A', field: fieldA, component: 'input', attrs: {} },
{ label: 'Field B', field: fieldB, component: 'date', attrs: {} },
];
const editUrl = '/api/edit';
beforeAll(() => {
vi.spyOn(axios, 'post').mockResolvedValue({ status: 200 });
vm = createWrapper(EditForm, {
props: {
rows: mockRows,
fieldsOptions: mockFieldsOptions,
editUrl,
},
}).vm;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('onSubmit()', () => {
it('should call axios.post with the correct parameters in the payload', async () => {
const selectedField = { field: fieldA, component: 'input', attrs: {} };
const newValue = 'Test Value';
vm.selectedField = selectedField;
vm.newValue = newValue;
await vm.onSubmit();
const payload = axios.post.mock.calls[0][1];
expect(axios.post).toHaveBeenCalledWith(editUrl, expect.any(Object));
expect(payload.field).toEqual(fieldA);
expect(payload.newValue).toEqual(newValue);
expect(payload.lines).toEqual(expect.arrayContaining(mockRows));
expect(vm.isLoading).toEqual(false);
});
});
});

View File

@ -1,6 +1,4 @@
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { default as axios } from 'axios';
import FilterItemForm from 'src/components/FilterItemForm.vue'; import FilterItemForm from 'src/components/FilterItemForm.vue';
import { vi, beforeAll, describe, expect, it } from 'vitest'; import { vi, beforeAll, describe, expect, it } from 'vitest';
@ -40,7 +38,7 @@ describe('FilterItemForm', () => {
{ relation: 'producer', scope: { fields: ['name'] } }, { relation: 'producer', scope: { fields: ['name'] } },
{ relation: 'ink', scope: { fields: ['name'] } }, { relation: 'ink', scope: { fields: ['name'] } },
], ],
where: { name: { like: '%bolas de madera%' } }, where: {"name":{"like":"%bolas de madera%"}},
}; };
expect(axios.get).toHaveBeenCalledWith('Items/withName', { expect(axios.get).toHaveBeenCalledWith('Items/withName', {

View File

@ -1,7 +1,5 @@
import { describe, expect, it, beforeAll, vi, afterAll } from 'vitest'; import { describe, expect, it, beforeAll, vi, afterAll } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { default as axios } from 'axios';
import FormModel from 'src/components/FormModel.vue'; import FormModel from 'src/components/FormModel.vue';
describe('FormModel', () => { describe('FormModel', () => {

View File

@ -1,7 +1,6 @@
import { vi, describe, expect, it, beforeAll, afterEach, beforeEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest';
import { default as axios } from 'axios'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper'; import Leftmenu from 'components/LeftMenu.vue';
import LeftMenu from 'components/LeftMenu.vue';
import * as vueRouter from 'vue-router'; import * as vueRouter from 'vue-router';
import { useNavigationStore } from 'src/stores/useNavigationStore'; import { useNavigationStore } from 'src/stores/useNavigationStore';
@ -102,7 +101,7 @@ function mount(source = 'main') {
vi.spyOn(axios, 'get').mockResolvedValue({ vi.spyOn(axios, 'get').mockResolvedValue({
data: [], data: [],
}); });
const wrapper = createWrapper(LeftMenu, { const wrapper = createWrapper(Leftmenu, {
propsData: { propsData: {
source, source,
}, },
@ -165,7 +164,7 @@ describe('getRoutes', () => {
}); });
}); });
describe('LeftMenu as card', () => { describe('Leftmenu as card', () => {
beforeAll(() => { beforeAll(() => {
vm = mount('card').vm; vm = mount('card').vm;
}); });
@ -174,7 +173,7 @@ describe('LeftMenu as card', () => {
vm.getRoutes(); vm.getRoutes();
}); });
}); });
describe('LeftMenu as main', () => { describe('Leftmenu as main', () => {
beforeEach(() => { beforeEach(() => {
vm = mount().vm; vm = mount().vm;
}); });

View File

@ -1,9 +1,13 @@
import { vi, describe, expect, it, beforeEach, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper } from 'app/test/vitest/helper';
import UserPanel from 'src/components/UserPanel.vue'; import UserPanel from 'src/components/UserPanel.vue';
import axios from 'axios'; import axios from 'axios';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
vi.mock('src/utils/quasarLang', () => ({
default: vi.fn(),
}));
describe('UserPanel', () => { describe('UserPanel', () => {
let wrapper; let wrapper;
let vm; let vm;
@ -39,7 +43,7 @@ describe('UserPanel', () => {
await vm.saveDarkMode(true); await vm.saveDarkMode(true);
expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true }); expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true });
expect(vm.user.darkMode).toBe(true); expect(vm.user.darkMode).toBe(true);
vm.updatePreferences(); await vm.updatePreferences();
expect(vm.darkMode).toBe(true); expect(vm.darkMode).toBe(true);
}); });
@ -48,7 +52,7 @@ describe('UserPanel', () => {
await vm.saveLanguage(userLanguage); await vm.saveLanguage(userLanguage);
expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage }); expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage });
expect(vm.user.lang).toBe(userLanguage); expect(vm.user.lang).toBe(userLanguage);
vm.updatePreferences(); await vm.updatePreferences();
expect(vm.locale).toBe(userLanguage); expect(vm.locale).toBe(userLanguage);
}); });
@ -56,8 +60,6 @@ describe('UserPanel', () => {
const key = 'name'; const key = 'name';
const value = 'itboss'; const value = 'itboss';
await vm.saveUserData(key, value); await vm.saveUserData(key, value);
expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value });
[key]: value,
});
}); });
}); });

View File

@ -60,7 +60,7 @@ async function confirm() {
v-model="address" v-model="address"
is-outlined is-outlined
autofocus autofocus
data-cy="SendEmailNotificationDialogInput" data-cy="SendEmailNotifiactionDialogInput"
/> />
</QCardSection> </QCardSection>
<QCardActions align="right"> <QCardActions align="right">

View File

@ -1,6 +1,4 @@
<script setup> <script setup>
import { computed } from 'vue';
const $props = defineProps({ const $props = defineProps({
colors: { colors: {
type: String, type: String,
@ -8,9 +6,9 @@ const $props = defineProps({
}, },
}); });
const colorArray = computed(() => JSON.parse($props.colors)?.value); const colorArray = JSON.parse($props.colors)?.value;
const maxHeight = 30; const maxHeight = 30;
const colorHeight = maxHeight / colorArray.value?.length; const colorHeight = maxHeight / colorArray?.length;
</script> </script>
<template> <template>
<div v-if="colors" class="color-div" :style="{ height: `${maxHeight}px` }"> <div v-if="colors" class="color-div" :style="{ height: `${maxHeight}px` }">

View File

@ -1,44 +0,0 @@
<script setup>
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
const model = defineModel({ type: [Number, String] });
const emit = defineEmits(['updateBic']);
const getIbanCountry = (bank) => {
return bank.substr(0, 2);
};
const autofillBic = async (iban) => {
if (!iban) return;
const bankEntityId = parseInt(iban.substr(4, 4));
const ibanCountry = getIbanCountry(iban);
if (ibanCountry != 'ES') return;
const filter = { where: { id: bankEntityId } };
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`BankEntities`, { params });
emit('updateBic', data[0]?.id);
};
</script>
<template>
<VnInput
:label="t('IBAN')"
clearable
v-model="model"
@update:model-value="autofillBic($event)"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{ t('components.iban_tooltip') }}</QTooltip>
</QIcon>
</template>
</VnInput>
</template>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { watch } from 'vue'; import { watch } from 'vue';
import { toDateHourMinSec } from 'src/filters'; import { toDateString } from 'src/filters';
const props = defineProps({ const props = defineProps({
value: { type: [String, Number, Boolean, Object], default: undefined }, value: { type: [String, Number, Boolean, Object], default: undefined },
@ -40,7 +40,7 @@ const updateValue = () => {
break; break;
case 'object': case 'object':
if (props.value instanceof Date) { if (props.value instanceof Date) {
t = toDateHourMinSec(props.value); t = toDateString(props.value);
} else { } else {
t = props.value.toString(); t = props.value.toString();
} }

View File

@ -10,7 +10,6 @@ import { useFilterParams } from 'src/composables/useFilterParams';
import FetchData from '../FetchData.vue'; import FetchData from '../FetchData.vue';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import { useCapitalize } from 'src/composables/useCapitalize'; import { useCapitalize } from 'src/composables/useCapitalize';
import VnAvatar from '../ui/VnAvatar.vue';
const $props = defineProps({ const $props = defineProps({
dataKey: { dataKey: {
@ -100,6 +99,7 @@ function getActions() {
:columns="columns" :columns="columns"
:redirect="false" :redirect="false"
:hiddenTags="['originFk', 'creationDate']" :hiddenTags="['originFk', 'creationDate']"
:exprBuilder
search-url="logs" search-url="logs"
:showTagChips="false" :showTagChips="false"
> >

View File

@ -1,5 +1,4 @@
import axios from 'axios'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper';
import VnChangePassword from 'src/components/common/VnChangePassword.vue'; import VnChangePassword from 'src/components/common/VnChangePassword.vue';
import { vi, beforeEach, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { vi, beforeEach, afterEach, beforeAll, describe, expect, it } from 'vitest';
import { Notify } from 'quasar'; import { Notify } from 'quasar';

View File

@ -1,5 +1,4 @@
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { default as axios } from 'axios';
import { vi, afterEach, beforeEach, beforeAll, describe, expect, it } from 'vitest'; import { vi, afterEach, beforeEach, beforeAll, describe, expect, it } from 'vitest';
import VnDms from 'src/components/common/VnDms.vue'; import VnDms from 'src/components/common/VnDms.vue';
@ -41,10 +40,7 @@ describe('VnDms', () => {
companyFk: 2, companyFk: 2,
dmsTypeFk: 3, dmsTypeFk: 3,
description: 'This is a test description', description: 'This is a test description',
files: { files: { name: 'example.txt', content: new Blob(['file content'], { type: 'text/plain' })},
name: 'example.txt',
content: new Blob(['file content'], { type: 'text/plain' }),
},
}; };
const expectedBody = { const expectedBody = {
@ -63,7 +59,7 @@ describe('VnDms', () => {
url: '/test', url: '/test',
formInitialData: { id: 1, reference: 'test' }, formInitialData: { id: 1, reference: 'test' },
model: 'Worker', model: 'Worker',
}, }
}); });
wrapper = wrapper.wrapper; wrapper = wrapper.wrapper;
vm = wrapper.vm; vm = wrapper.vm;
@ -117,9 +113,7 @@ describe('VnDms', () => {
describe('save', () => { describe('save', () => {
it('should save data correctly', async () => { it('should save data correctly', async () => {
await vm.save(); await vm.save();
expect(postMock).toHaveBeenCalledWith(vm.getUrl(), expect.any(FormData), { expect(postMock).toHaveBeenCalledWith(vm.getUrl(), expect.any(FormData), { params: expectedBody });
params: expectedBody,
});
expect(wrapper.emitted('onDataSaved')).toBeTruthy(); expect(wrapper.emitted('onDataSaved')).toBeTruthy();
}); });
}); });
@ -133,8 +127,8 @@ describe('VnDms', () => {
warehouseFk: 2, warehouseFk: 2,
companyFk: 3, companyFk: 3,
dmsTypeFk: 2, dmsTypeFk: 2,
description: 'This is a test description', description: 'This is a test description'
}; }
await wrapper.setProps({ formInitialData: testData }); await wrapper.setProps({ formInitialData: testData });
vm.defaultData(); vm.defaultData();

View File

@ -1,6 +1,4 @@
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { default as axios } from 'axios';
import VnDmsList from 'src/components/common/VnDmsList.vue'; import VnDmsList from 'src/components/common/VnDmsList.vue';
import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest';

View File

@ -65,7 +65,7 @@ describe('VnJsonValue', () => {
const date = new Date('2023-01-01'); const date = new Date('2023-01-01');
const wrapper = buildComponent({ value: date }); const wrapper = buildComponent({ value: date });
const span = wrapper.find('span'); const span = wrapper.find('span');
expect(span.text()).toBe('01/01/2023, 01:00:00'); expect(span.text()).toBe('2023-01-01');
expect(span.classes()).toContain('json-object'); expect(span.classes()).toContain('json-object');
}); });

View File

@ -1,6 +1,5 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import axios from 'axios'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper';
import VnLog from 'src/components/common/VnLog.vue'; import VnLog from 'src/components/common/VnLog.vue';
describe('VnLog', () => { describe('VnLog', () => {

View File

@ -1,6 +1,5 @@
import { describe, it, expect, vi, afterEach, beforeEach, afterAll } from 'vitest'; import { describe, it, expect, vi, afterEach, beforeEach, afterAll } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { default as axios } from 'axios';
import VnNotes from 'src/components/ui/VnNotes.vue'; import VnNotes from 'src/components/ui/VnNotes.vue';
describe('VnNotes', () => { describe('VnNotes', () => {

View File

@ -159,7 +159,6 @@ async function fetch() {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-top: 2px; margin-top: 2px;
align-items: start;
.label { .label {
color: var(--vn-label-color); color: var(--vn-label-color);
width: 9em; width: 9em;
@ -170,15 +169,9 @@ async function fetch() {
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
} }
&.ellipsis > .value {
text-overflow: ellipsis;
white-space: pre;
}
.value { .value {
color: var(--vn-text-color); color: var(--vn-text-color);
overflow: hidden; overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
} }
.header { .header {
@ -210,21 +203,27 @@ async function fetch() {
} }
.vn-card-group { .vn-card-group {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); flex-direction: column;
gap: 16px;
} }
.vn-card-content { .vn-card-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
> div { > div {
max-height: 70px; max-height: 70px;
} }
} }
@media (min-width: 1010px) {
.vn-card-group {
flex-direction: row;
}
.vn-card-content {
flex: 1;
}
}
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
.summaryHeader .vn-label-value { .summaryHeader .vn-label-value {

View File

@ -6,7 +6,6 @@ import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useClipboard } from 'src/composables/useClipboard'; import { useClipboard } from 'src/composables/useClipboard';
import VnMoreOptions from './VnMoreOptions.vue'; import VnMoreOptions from './VnMoreOptions.vue';
import { getValueFromPath } from 'src/composables/getValueFromPath';
const entity = defineModel({ type: Object, default: null }); const entity = defineModel({ type: Object, default: null });
const $props = defineProps({ const $props = defineProps({
@ -57,6 +56,18 @@ const routeName = computed(() => {
return `${routeName}Summary`; return `${routeName}Summary`;
}); });
function getValueFromPath(path) {
if (!path) return;
const keys = path.toString().split('.');
let current = entity.value;
for (const key of keys) {
if (current[key] === undefined) return undefined;
else current = current[key];
}
return current;
}
function copyIdText(id) { function copyIdText(id) {
copyText(id, { copyText(id, {
component: { component: {
@ -159,10 +170,10 @@ const toModule = computed(() => {
<div class="title"> <div class="title">
<span <span
v-if="title" v-if="title"
:title="getValueFromPath(entity, title)" :title="getValueFromPath(title)"
:data-cy="`${$attrs['data-cy'] ?? 'vnDescriptor'}_title`" :data-cy="`${$attrs['data-cy'] ?? 'vnDescriptor'}_title`"
> >
{{ getValueFromPath(entity, title) ?? title }} {{ getValueFromPath(title) ?? title }}
</span> </span>
<slot v-else name="description" :entity="entity"> <slot v-else name="description" :entity="entity">
<span <span
@ -178,7 +189,7 @@ const toModule = computed(() => {
class="subtitle" class="subtitle"
:data-cy="`${$attrs['data-cy'] ?? 'vnDescriptor'}_subtitle`" :data-cy="`${$attrs['data-cy'] ?? 'vnDescriptor'}_subtitle`"
> >
#{{ getValueFromPath(entity, subtitle) ?? entity.id }} #{{ getValueFromPath(subtitle) ?? entity.id }}
</QItemLabel> </QItemLabel>
<QBtn <QBtn
round round
@ -241,10 +252,6 @@ const toModule = computed(() => {
content: ':'; content: ':';
} }
} }
&.ellipsis > .value {
text-overflow: ellipsis;
white-space: pre;
}
.value { .value {
color: var(--vn-text-color); color: var(--vn-text-color);
font-size: 14px; font-size: 14px;

View File

@ -1,11 +1,8 @@
<script setup> <script setup>
import { dashIfEmpty } from 'src/filters';
defineProps({ email: { type: [String], default: null } }); defineProps({ email: { type: [String], default: null } });
</script> </script>
<template> <template>
<QBtn <QBtn
class="q-pr-xs"
v-if="email" v-if="email"
flat flat
round round
@ -16,5 +13,4 @@ defineProps({ email: { type: [String], default: null } });
:href="`mailto:${email}`" :href="`mailto:${email}`"
@click.stop @click.stop
/> />
<span>{{ dashIfEmpty(email) }}</span>
</template> </template>

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, reactive, useAttrs, onBeforeMount, capitalize } from 'vue'; import { ref, reactive, useAttrs, onBeforeMount, capitalize } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { dashIfEmpty, parsePhone } from 'src/filters'; import { parsePhone } from 'src/filters';
import useOpenURL from 'src/composables/useOpenURL'; import useOpenURL from 'src/composables/useOpenURL';
const props = defineProps({ const props = defineProps({
@ -12,51 +12,38 @@ const props = defineProps({
const phone = ref(props.phoneNumber); const phone = ref(props.phoneNumber);
const config = reactive({ const config = reactive({
sip: { icon: 'phone', href: `sip:${props.phoneNumber}` },
'say-simple': { 'say-simple': {
icon: 'vn:saysimple', icon: 'vn:saysimple',
url: null, url: null,
channel: props.channel, channel: props.channel,
}, },
sip: { icon: 'phone', href: `sip:${props.phoneNumber}` },
}); });
const type = Object.keys(config).find((key) => key in useAttrs()) || 'sip';
const attrs = useAttrs();
const types = Object.keys(config)
.filter((key) => key in attrs)
.sort();
const activeTypes = types.length ? types : ['sip'];
onBeforeMount(async () => { onBeforeMount(async () => {
if (!phone.value) return; if (!phone.value) return;
for (const type of activeTypes) {
if (type === 'say-simple') {
let { channel } = config[type]; let { channel } = config[type];
if (type === 'say-simple') {
const { url, defaultChannel } = (await axios.get('SaySimpleConfigs/findOne')) const { url, defaultChannel } = (await axios.get('SaySimpleConfigs/findOne'))
.data; .data;
if (!channel) channel = defaultChannel; if (!channel) channel = defaultChannel;
phone.value = await parsePhone( phone.value = await parsePhone(props.phoneNumber, props.country?.toLowerCase());
props.phoneNumber, config[
props.country?.toLowerCase(), type
); ].url = `${url}?customerIdentity=%2B${phone.value}&channelId=${channel}`;
config[type].url =
`${url}?customerIdentity=%2B${phone.value}&channelId=${channel}`;
}
} }
}); });
function handleClick(type) { function handleClick() {
if (config[type].url) useOpenURL(config[type].url); if (config[type].url) useOpenURL(config[type].url);
else if (config[type].href) window.location.href = config[type].href; else if (config[type].href) window.location.href = config[type].href;
} }
</script> </script>
<template> <template>
<div class="flex items-center gap-2">
<template v-for="type in activeTypes">
<QBtn <QBtn
:key="type"
v-if="phone" v-if="phone"
flat flat
round round
@ -64,13 +51,11 @@ function handleClick(type) {
size="sm" size="sm"
color="primary" color="primary"
padding="none" padding="none"
@click.stop="() => handleClick(type)" @click.stop="handleClick"
> >
<QTooltip> <QTooltip>
{{ capitalize(type).replace('-', '') }} {{ capitalize(type).replace('-', '') }}
</QTooltip> </QTooltip>
</QBtn></template </QBtn>
> {{ phoneNumber }}
<span>{{ dashIfEmpty(phone) }}</span>
</div>
</template> </template>

View File

@ -0,0 +1,60 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
usesMana: {
type: Boolean,
required: true,
},
manaCode: {
type: String,
required: true,
},
manaVal: {
type: String,
default: 'mana',
},
manaLabel: {
type: String,
default: 'Promotion mana',
},
manaClaimVal: {
type: String,
default: 'manaClaim',
},
claimLabel: {
type: String,
default: 'Claim mana',
},
});
const manaCode = ref(props.manaCode);
</script>
<template>
<div class="column q-gutter-y-sm q-mt-sm">
<QRadio
v-model="manaCode"
dense
:val="manaVal"
:label="t(manaLabel)"
:dark="true"
class="q-mb-sm"
/>
<QRadio
v-model="manaCode"
dense
:val="manaClaimVal"
:label="t(claimLabel)"
:dark="true"
class="q-mb-sm"
/>
</div>
</template>
<i18n>
es:
Promotion mana: Maná promoción
Claim mana: Maná reclamación
</i18n>

View File

@ -1,7 +1,5 @@
import { vi, describe, expect, it, beforeAll, afterEach, beforeEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach, beforeEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { default as axios } from 'axios';
import CardSummary from 'src/components/ui/CardSummary.vue'; import CardSummary from 'src/components/ui/CardSummary.vue';
import * as vueRouter from 'vue-router'; import * as vueRouter from 'vue-router';

View File

@ -1,6 +1,5 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import axios from 'axios'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
describe('VnPaginate', () => { describe('VnPaginate', () => {

View File

@ -1,5 +1,5 @@
import { describe, it, expect, beforeAll, vi } from 'vitest'; import { describe, it, expect, beforeAll, vi } from 'vitest';
import axios from 'axios'; import { axios } from 'app/test/vitest/helper';
import parsePhone from 'src/filters/parsePhone'; import parsePhone from 'src/filters/parsePhone';
describe('parsePhone filter', () => { describe('parsePhone filter', () => {

View File

@ -1,6 +1,5 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import axios from 'axios'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper';
import VnSms from 'src/components/ui/VnSms.vue'; import VnSms from 'src/components/ui/VnSms.vue';
describe('VnSms', () => { describe('VnSms', () => {

View File

@ -1,5 +1,5 @@
import { vi, describe, expect, it, beforeAll, afterAll } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterAll } from 'vitest';
import axios from 'axios'; import { axios } from 'app/test/vitest/helper';
import { downloadFile } from 'src/composables/downloadFile'; import { downloadFile } from 'src/composables/downloadFile';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
const session = useSession(); const session = useSession();

View File

@ -1,7 +1,5 @@
import { vi, describe, expect, it, beforeAll, afterAll } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterAll } from 'vitest';
import axios from 'axios'; import { axios, flushPromises } from 'app/test/vitest/helper';
import { flushPromises } from '@vue/test-utils';
import { useAcl } from 'src/composables/useAcl'; import { useAcl } from 'src/composables/useAcl';
describe('useAcl', () => { describe('useAcl', () => {

View File

@ -1,39 +1,15 @@
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'; import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
import { default as axios } from 'axios'; import { axios, flushPromises } from 'app/test/vitest/helper';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import * as vueRouter from 'vue-router'; import * as vueRouter from 'vue-router';
import { setActivePinia, createPinia } from 'pinia';
describe('useArrayData', () => { describe('useArrayData', () => {
const filter = '{"limit":20,"skip":0}'; const filter = '{"limit":20,"skip":0}';
const params = { supplierFk: 2 }; const params = { supplierFk: 2 };
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()); vi.spyOn(useRouter(), 'replace');
vi.spyOn(useRouter(), 'push');
// Mock route
vi.spyOn(vueRouter, 'useRoute').mockReturnValue({
path: 'mockSection/list',
matched: [],
query: {},
params: {},
meta: { moduleName: 'mockName' },
});
// Mock router
vi.spyOn(vueRouter, 'useRouter').mockReturnValue({
push: vi.fn(),
replace: vi.fn(),
currentRoute: {
value: {
path: 'mockSection/list',
params: { id: 1 },
meta: { moduleName: 'mockName' },
matched: [{ path: 'mockName/:id' }],
},
},
});
}); });
afterEach(() => { afterEach(() => {
@ -41,69 +17,103 @@ describe('useArrayData', () => {
}); });
it('should fetch and replace url with new params', async () => { it('should fetch and replace url with new params', async () => {
vi.spyOn(axios, 'get').mockResolvedValueOnce({ data: [] }); vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] });
const arrayData = useArrayData('ArrayData', { const arrayData = useArrayData('ArrayData', { url: 'mockUrl' });
url: 'mockUrl',
searchUrl: 'params',
});
arrayData.store.userParams = params; arrayData.store.userParams = params;
await arrayData.fetch({}); arrayData.fetch({});
await flushPromises();
const routerReplace = useRouter().replace.mock.calls[0][0]; const routerReplace = useRouter().replace.mock.calls[0][0];
expect(axios.get).toHaveBeenCalledWith('mockUrl', { expect(axios.get.mock.calls[0][1].params).toEqual({
signal: expect.any(Object),
params: {
filter, filter,
supplierFk: 2, supplierFk: 2,
},
}); });
expect(routerReplace.path).toEqual('mockSection/list');
expect(routerReplace.path).toBe('mockSection/list');
expect(JSON.parse(routerReplace.query.params)).toEqual( expect(JSON.parse(routerReplace.query.params)).toEqual(
expect.objectContaining(params), expect.objectContaining(params),
); );
}); });
it('should redirect to detail when single record is returned with navigation', async () => { it('should get data and send new URL without keeping parameters, if there is only one record', async () => {
vi.spyOn(axios, 'get').mockResolvedValueOnce({ vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }] });
data: [{ id: 1 }],
});
const arrayData = useArrayData('ArrayData', { const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} });
url: 'mockUrl',
navigate: {},
});
arrayData.store.userParams = params; arrayData.store.userParams = params;
await arrayData.fetch({}); arrayData.fetch({});
await flushPromises();
const routerPush = useRouter().push.mock.calls[0][0]; const routerPush = useRouter().push.mock.calls[0][0];
expect(routerPush.path).toBe('mockName/1'); expect(axios.get.mock.calls[0][1].params).toEqual({
filter,
supplierFk: 2,
});
expect(routerPush.path).toEqual('mockName/1');
expect(routerPush.query).toBeUndefined(); expect(routerPush.query).toBeUndefined();
}); });
it('should return one record when oneRecord is true', async () => { it('should get data and send new URL keeping parameters, if you have more than one record', async () => {
vi.spyOn(axios, 'get').mockResolvedValueOnce({ vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }] });
vi.spyOn(vueRouter, 'useRoute').mockReturnValue({
matched: [],
query: {},
params: {},
meta: { moduleName: 'mockName' },
path: 'mockName/1',
});
vi.spyOn(vueRouter, 'useRouter').mockReturnValue({
push: vi.fn(),
replace: vi.fn(),
currentRoute: {
value: {
params: {
id: 1,
},
meta: { moduleName: 'mockName' },
matched: [{ path: 'mockName/:id' }],
},
},
});
const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} });
arrayData.store.userParams = params;
arrayData.fetch({});
await flushPromises();
const routerPush = useRouter().push.mock.calls[0][0];
expect(axios.get.mock.calls[0][1].params).toEqual({
filter,
supplierFk: 2,
});
expect(routerPush.path).toEqual('mockName/');
expect(routerPush.query.params).toBeDefined();
});
it('should return one record', async () => {
vi.spyOn(axios, 'get').mockReturnValueOnce({
data: [ data: [
{ id: 1, name: 'Entity 1' }, { id: 1, name: 'Entity 1' },
{ id: 2, name: 'Entity 2' }, { id: 2, name: 'Entity 2' },
], ],
}); });
const arrayData = useArrayData('ArrayData', { url: 'mockUrl', oneRecord: true });
const arrayData = useArrayData('ArrayData', {
url: 'mockUrl',
oneRecord: true,
});
await arrayData.fetch({}); await arrayData.fetch({});
expect(arrayData.store.data).toEqual({ expect(arrayData.store.data).toEqual({ id: 1, name: 'Entity 1' });
id: 1, });
name: 'Entity 1',
}); it('should handle empty data gracefully if has to return one record', async () => {
vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] });
const arrayData = useArrayData('ArrayData', { url: 'mockUrl', oneRecord: true });
await arrayData.fetch({});
expect(arrayData.store.data).toBeUndefined();
}); });
}); });

View File

@ -1,6 +1,5 @@
import { vi, describe, expect, it } from 'vitest'; import { vi, describe, expect, it } from 'vitest';
import axios from 'axios'; import { axios, flushPromises } from 'app/test/vitest/helper';
import { flushPromises } from '@vue/test-utils';
import { useRole } from 'composables/useRole'; import { useRole } from 'composables/useRole';
const role = useRole(); const role = useRole();

View File

@ -1,5 +1,5 @@
import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest';
import axios from 'axios'; import { axios } from 'app/test/vitest/helper';
import { useSession } from 'composables/useSession'; import { useSession } from 'composables/useSession';
import { useState } from 'composables/useState'; import { useState } from 'composables/useState';

View File

@ -1,6 +1,5 @@
import { vi, describe, expect, it } from 'vitest'; import { vi, describe, expect, it } from 'vitest';
import axios from 'axios'; import { axios, flushPromises } from 'app/test/vitest/helper';
import { flushPromises } from '@vue/test-utils';
import { useTokenConfig } from 'composables/useTokenConfig'; import { useTokenConfig } from 'composables/useTokenConfig';
const tokenConfig = useTokenConfig(); const tokenConfig = useTokenConfig();

View File

@ -1,11 +0,0 @@
export function getValueFromPath(root, path) {
if (!root || !path) return;
const keys = path.toString().split('.');
let current = root;
for (const key of keys) {
if (current[key] === undefined) return undefined;
else current = current[key];
}
return current;
}

View File

@ -1,51 +0,0 @@
import axios from 'axios';
export async function beforeSave(data, getChanges, modelOrigin) {
try {
const changes = data.updates;
if (!changes) return data;
const patchPromises = [];
for (const change of changes) {
let patchData = {};
if ('hasMinPrice' in change.data) {
patchData.hasMinPrice = change.data?.hasMinPrice;
delete change.data.hasMinPrice;
}
if ('minPrice' in change.data) {
patchData.minPrice = change.data?.minPrice;
delete change.data.minPrice;
}
if (Object.keys(patchData).length > 0) {
const promise = axios
.get(`${modelOrigin}/findOne`, {
params: {
filter: {
fields: ['itemFk'],
where: { id: change.where.id },
},
},
})
.then((row) => {
return axios.patch(`Items/${row.data.itemFk}`, patchData);
})
.catch((error) => {
console.error('Error processing change: ', change, error);
});
patchPromises.push(promise);
}
}
await Promise.all(patchPromises);
data.updates = changes.filter((change) => Object.keys(change.data).length > 0);
return data;
} catch (error) {
console.error('Error in beforeSave:', error);
throw error;
}
}

View File

@ -30,16 +30,9 @@ export function useAcl() {
return false; return false;
} }
function hasAcl(model, prop, accessType) {
const modelAcl = state.getAcls().value[model];
const propAcl = modelAcl?.[prop] || modelAcl?.['*'];
return !!(propAcl?.[accessType] || propAcl?.['*']);
}
return { return {
fetch, fetch,
hasAny, hasAny,
state, state,
hasAcl,
}; };
} }

View File

@ -5,11 +5,12 @@ import { useArrayDataStore } from 'stores/useArrayDataStore';
import { buildFilter } from 'filters/filterPanel'; import { buildFilter } from 'filters/filterPanel';
import { isDialogOpened } from 'src/filters'; import { isDialogOpened } from 'src/filters';
const arrayDataStore = useArrayDataStore();
export function useArrayData(key, userOptions) { export function useArrayData(key, userOptions) {
key ??= useRoute().meta.moduleName; key ??= useRoute().meta.moduleName;
if (!key) throw new Error('ArrayData: A key is required to use this composable'); if (!key) throw new Error('ArrayData: A key is required to use this composable');
const arrayDataStore = useArrayDataStore(); // Move inside function
if (!arrayDataStore.get(key)) arrayDataStore.set(key); if (!arrayDataStore.get(key)) arrayDataStore.set(key);

View File

@ -13,7 +13,7 @@ export function useRole() {
name: data.user.name, name: data.user.name,
nickname: data.user.nickname, nickname: data.user.nickname,
lang: data.user.lang || 'es', lang: data.user.lang || 'es',
departmentFk: data.user?.worker?.department?.departmentFk, departmentFk: data.user.worker.department.departmentFk,
}; };
state.setUser(userData); state.setUser(userData);
state.setRoles(roles); state.setRoles(roles);

View File

@ -60,7 +60,7 @@ export function useSession() {
const { data: isValidToken } = await axios.get('VnUsers/validateToken'); const { data: isValidToken } = await axios.get('VnUsers/validateToken');
if (isValidToken) if (isValidToken)
destroyTokenPromises = Object.entries(tokens).map(([key, url]) => destroyTokenPromises = Object.entries(tokens).map(([key, url]) =>
destroyToken(url, storage, key), destroyToken(url, storage, key)
); );
} }
} finally { } finally {

View File

@ -78,8 +78,7 @@ export function useValidator() {
if (min >= 0) if (min >= 0)
if (Math.floor(value) < min) return t('inputMin', { value: min }); if (Math.floor(value) < min) return t('inputMin', { value: min });
}, },
custom: (value) => custom: (value) => validation.bindedFunction(value) || 'Invalid value',
eval(`(${validation.bindedFunction})`)(value) || 'Invalid value',
}; };
}; };

View File

@ -340,6 +340,3 @@ input::-webkit-inner-spin-button {
.containerShrinked { .containerShrinked {
width: 70%; width: 70%;
} }
.q-item__section--main ~ .q-item__section--side {
padding-inline: 0;
}

View File

@ -18,7 +18,6 @@ $positive: #c8e484;
$negative: #fb5252; $negative: #fb5252;
$info: #84d0e2; $info: #84d0e2;
$warning: #f4b974; $warning: #f4b974;
$neutral: #b0b0b0;
// Pendiente de cuadrar con la base de datos // Pendiente de cuadrar con la base de datos
$success: $positive; $success: $positive;
$alert: $negative; $alert: $negative;
@ -52,6 +51,3 @@ $width-xl: 1600px;
.bg-alert { .bg-alert {
background-color: $negative; background-color: $negative;
} }
.bg-neutral {
background-color: $neutral;
}

View File

@ -877,11 +877,6 @@ components:
active: Is active active: Is active
floramondo: Is floramondo floramondo: Is floramondo
showBadDates: Show future items showBadDates: Show future items
name: Nombre
rate2: Grouping price
rate3: Packing price
minPrice: Min. Price
itemFk: Item id
userPanel: userPanel:
copyToken: Token copied to clipboard copyToken: Token copied to clipboard
settings: Settings settings: Settings

View File

@ -961,11 +961,6 @@ components:
to: Hasta to: Hasta
floramondo: Floramondo floramondo: Floramondo
showBadDates: Ver items a futuro showBadDates: Ver items a futuro
name: Nombre
rate2: Precio grouping
rate3: Precio packing
minPrice: Precio mínimo
itemFk: Id item
userPanel: userPanel:
copyToken: Token copiado al portapapeles copyToken: Token copiado al portapapeles
settings: Configuración settings: Configuración

View File

@ -100,8 +100,12 @@ const onChangePass = (oldPass) => {
}; };
onMounted(() => { onMounted(() => {
hasitManagementAccess.value = useAcl().hasAcl('VnUser', 'higherPrivileges', 'WRITE'); hasitManagementAccess.value = useAcl().hasAny([
hasSysadminAccess.value = useAcl().hasAcl('VnUser', 'adminUser', 'WRITE'); { model: 'VnUser', props: 'higherPrivileges', accessType: 'WRITE' },
]);
hasSysadminAccess.value = useAcl().hasAny([
{ model: 'VnUser', props: 'adminUser', accessType: 'WRITE' },
]);
}); });
</script> </script>
<template> <template>
@ -223,7 +227,7 @@ onMounted(() => {
<QItemSection>{{ t('account.card.actions.deactivateUser.name') }}</QItemSection> <QItemSection>{{ t('account.card.actions.deactivateUser.name') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem
v-if="useAcl().hasAcl('VnRole', '*', 'WRITE')" v-if="useAcl().hasAny([{ model: 'VnRole', props: '*', accessType: 'WRITE' }])"
v-ripple v-ripple
clickable clickable
@click="showSyncDialog = true" @click="showSyncDialog = true"

View File

@ -28,8 +28,14 @@ const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
function stateColor(entity) { const STATE_COLOR = {
return entity?.claimState?.classColor; pending: 'warning',
incomplete: 'info',
resolved: 'positive',
canceled: 'negative',
};
function stateColor(code) {
return STATE_COLOR[code];
} }
onMounted(async () => { onMounted(async () => {
@ -51,8 +57,9 @@ onMounted(async () => {
<VnLv v-if="entity.claimState" :label="t('claim.state')"> <VnLv v-if="entity.claimState" :label="t('claim.state')">
<template #value> <template #value>
<QBadge <QBadge
:color="stateColor(entity)" :color="stateColor(entity.claimState.code)"
style="color: var(--vn-black-text-color)" text-color="black"
dense
> >
{{ entity.claimState.description }} {{ entity.claimState.description }}
</QBadge> </QBadge>

View File

@ -123,8 +123,8 @@ async function fetchMana() {
async function updateDiscount({ saleFk, discount, canceller }) { async function updateDiscount({ saleFk, discount, canceller }) {
const body = { salesIds: [saleFk], newDiscount: discount }; const body = { salesIds: [saleFk], newDiscount: discount };
const ticketFk = claim.value.ticketFk; const claimId = claim.value.ticketFk;
const query = `Tickets/${ticketFk}/updateDiscount`; const query = `Tickets/${claimId}/updateDiscount`;
await axios.post(query, body, { await axios.post(query, body, {
signal: canceller.signal, signal: canceller.signal,

View File

@ -108,9 +108,15 @@ const markerLabels = [
{ value: 5, label: t('claim.person') }, { value: 5, label: t('claim.person') },
]; ];
const STATE_COLOR = {
pending: 'warning',
incomplete: 'info',
resolved: 'positive',
canceled: 'negative',
};
function stateColor(code) { function stateColor(code) {
const claimState = claimStates.value.find((state) => state.code === code); return STATE_COLOR[code];
return claimState?.classColor;
} }
const developmentColumns = ref([ const developmentColumns = ref([
@ -182,7 +188,7 @@ function claimUrl(section) {
<template> <template>
<FetchData <FetchData
url="ClaimStates" url="ClaimStates"
:filter="{ fields: ['id', 'description', 'code', 'classColor'] }" :filter="{ fields: ['id', 'description'] }"
@on-fetch="(data) => (claimStates = data)" @on-fetch="(data) => (claimStates = data)"
auto-load auto-load
/> />
@ -340,18 +346,17 @@ function claimUrl(section) {
<template #body="props"> <template #body="props">
<QTr :props="props"> <QTr :props="props">
<QTd v-for="col in props.cols" :key="col.name" :props="props"> <QTd v-for="col in props.cols" :key="col.name" :props="props">
<template v-if="col.name === 'description'"> <span v-if="col.name != 'description'">{{
<span class="link">{{ t(col.value)
dashIfEmpty(col.field(props.row)) }}</span>
<span class="link" v-if="col.name === 'description'">{{
t(col.value)
}}</span> }}</span>
<ItemDescriptorProxy <ItemDescriptorProxy
v-if="col.name == 'description'"
:id="props.row.sale.itemFk" :id="props.row.sale.itemFk"
:sale-fk="props.row.saleFk" :sale-fk="props.row.saleFk"
/> ></ItemDescriptorProxy>
</template>
<template v-else>
{{ dashIfEmpty(col.field(props.row)) }}
</template>
</QTd> </QTd>
</QTr> </QTr>
</template> </template>

View File

@ -88,13 +88,13 @@ const columns = [
auto-load auto-load
> >
<template #column-itemFk="{ row }"> <template #column-itemFk="{ row }">
<span class="link" @click.stop> <span class="link">
{{ row.itemFk }} {{ row.itemFk }}
<ItemDescriptorProxy :id="row.itemFk" /> <ItemDescriptorProxy :id="row.itemFk" />
</span> </span>
</template> </template>
<template #column-ticketFk="{ row }"> <template #column-ticketFk="{ row }">
<span class="link" @click.stop> <span class="link">
{{ row.ticketFk }} {{ row.ticketFk }}
<TicketDescriptorProxy :id="row.ticketFk" /> <TicketDescriptorProxy :id="row.ticketFk" />
</span> </span>

View File

@ -1,6 +1,5 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import axios from 'axios'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper';
import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue'; import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue';
describe('ClaimDescriptorMenu', () => { describe('ClaimDescriptorMenu', () => {

View File

@ -1,6 +1,5 @@
import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest';
import axios from 'axios'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper';
import ClaimLines from '/src/pages/Claim/Card/ClaimLines.vue'; import ClaimLines from '/src/pages/Claim/Card/ClaimLines.vue';
describe('ClaimLines', () => { describe('ClaimLines', () => {

View File

@ -1,6 +1,5 @@
import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest';
import axios from 'axios'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper';
import ClaimLinesImport from 'pages/Claim/Card/ClaimLinesImport.vue'; import ClaimLinesImport from 'pages/Claim/Card/ClaimLinesImport.vue';
describe('ClaimLinesImport', () => { describe('ClaimLinesImport', () => {

View File

@ -1,7 +1,7 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import axios from 'axios'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper';
import ClaimPhoto from 'pages/Claim/Card/ClaimPhoto.vue'; import ClaimPhoto from 'pages/Claim/Card/ClaimPhoto.vue';
describe('ClaimPhoto', () => { describe('ClaimPhoto', () => {
let vm; let vm;
@ -61,7 +61,7 @@ describe('ClaimPhoto', () => {
title: 'This file will be deleted', title: 'This file will be deleted',
icon: 'delete', icon: 'delete',
data: { index: 1 }, data: { index: 1 },
promise: vm.deleteDms, promise: vm.deleteDms
}, },
}) })
); );

View File

@ -101,10 +101,7 @@ const columns = computed(() => [
name: 'stateCode', name: 'stateCode',
chip: { chip: {
condition: () => true, condition: () => true,
color: ({ stateCode }) => { color: ({ stateCode }) => STATE_COLOR[stateCode] ?? 'bg-grey',
const state = states.value?.find(({ code }) => code === stateCode);
return `bg-${state.classColor}`;
},
}, },
columnFilter: { columnFilter: {
name: 'claimStateFk', name: 'claimStateFk',
@ -134,6 +131,12 @@ const columns = computed(() => [
], ],
}, },
]); ]);
const STATE_COLOR = {
pending: 'bg-warning',
loses: 'bg-negative',
resolved: 'bg-positive',
};
</script> </script>
<template> <template>

View File

@ -25,7 +25,7 @@ import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.v
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const { sendEmail, openReport } = usePrintService(); const { sendEmail, openReport } = usePrintService();
const { t } = useI18n(); const { t } = useI18n();
const { hasAcl } = useAcl(); const { hasAny } = useAcl();
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
@ -276,7 +276,9 @@ const showBalancePdf = ({ id }) => {
> >
<VnInput <VnInput
v-model="scope.value" v-model="scope.value"
:disable="!hasAcl('Receipt', '*', 'WRITE')" :disable="
!hasAny([{ model: 'Receipt', props: '*', accessType: 'WRITE' }])
"
@keypress.enter="scope.set" @keypress.enter="scope.set"
autofocus autofocus
/> />

View File

@ -9,7 +9,6 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue'; import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue';
import VnInputBic from 'src/components/common/VnInputBic.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -18,7 +17,7 @@ const bankEntitiesRef = ref(null);
const filter = { const filter = {
fields: ['id', 'bic', 'name'], fields: ['id', 'bic', 'name'],
order: 'bic ASC', order: 'bic ASC'
}; };
const getBankEntities = (data, formData) => { const getBankEntities = (data, formData) => {
@ -44,11 +43,13 @@ const getBankEntities = (data, formData) => {
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnInputBic <VnInput :label="t('IBAN')" clearable v-model="data.iban">
:label="t('IBAN')" <template #append>
v-model="data.iban" <QIcon name="info" class="cursor-info">
@update-bic="(bankEntityFk) => (data.bankEntityFk = bankEntityFk)" <QTooltip>{{ t('components.iban_tooltip') }}</QTooltip>
/> </QIcon>
</template>
</VnInput>
<VnSelectDialog <VnSelectDialog
:label="t('Swift / BIC')" :label="t('Swift / BIC')"
ref="bankEntitiesRef" ref="bankEntitiesRef"

View File

@ -79,7 +79,7 @@ async function acceptPropagate({ isEqualizated }) {
observe-form-changes observe-form-changes
@on-data-saved="checkEtChanges" @on-data-saved="checkEtChanges"
> >
<template #form="{ data, validate, validations }"> <template #form="{ data, validate }">
<VnRow> <VnRow>
<VnInput <VnInput
:label="t('Social name')" :label="t('Social name')"
@ -112,7 +112,6 @@ async function acceptPropagate({ isEqualizated }) {
v-model="data.sageTaxTypeFk" v-model="data.sageTaxTypeFk"
data-cy="sageTaxTypeFk" data-cy="sageTaxTypeFk"
:required="data.isTaxDataChecked" :required="data.isTaxDataChecked"
:rules="[(val) => validations.required(data.isTaxDataChecked, val)]"
/> />
<VnSelect <VnSelect
:label="t('Sage transaction type')" :label="t('Sage transaction type')"
@ -123,9 +122,6 @@ async function acceptPropagate({ isEqualizated }) {
data-cy="sageTransactionTypeFk" data-cy="sageTransactionTypeFk"
v-model="data.sageTransactionTypeFk" v-model="data.sageTransactionTypeFk"
:required="data.isTaxDataChecked" :required="data.isTaxDataChecked"
:rules="[
(val) => validations.required(data.sageTransactionTypeFk, val),
]"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">

View File

@ -84,31 +84,29 @@ const sumRisk = ({ clientRisks }) => {
<VnLv :label="t('customer.summary.customerId')" :value="entity.id" /> <VnLv :label="t('customer.summary.customerId')" :value="entity.id" />
<VnLv :label="t('globals.name')" :value="entity.name" /> <VnLv :label="t('globals.name')" :value="entity.name" />
<VnLv :label="t('customer.summary.contact')" :value="entity.contact" /> <VnLv :label="t('customer.summary.contact')" :value="entity.contact" />
<VnLv :label="t('customer.extendedList.tableVisibleColumns.phone')"> <VnLv :value="entity.phone">
<template #value> <template #label>
{{ t('customer.extendedList.tableVisibleColumns.phone') }}
<VnLinkPhone :phone-number="entity.phone" /> <VnLinkPhone :phone-number="entity.phone" />
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('customer.summary.mobile')"> <VnLv :value="entity.mobile">
<template #value> <template #label>
{{ t('customer.summary.mobile') }}
<VnLinkPhone :phone-number="entity.mobile" />
<VnLinkPhone <VnLinkPhone
sip
say-simple say-simple
:phone-number="entity.mobile" :phone-number="entity.mobile"
:channel="entity.country?.saySimpleCountry?.channel" :channel="entity.country?.saySimpleCountry?.channel"
class="q-ml-xs"
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv :value="entity.email" copy
:label="t('globals.params.email')" ><template #label>
:value="entity.email" {{ t('globals.params.email') }}
class="ellipsis" <VnLinkMail email="entity.email"></VnLinkMail> </template
copy ></VnLv>
>
<template #value>
<VnLinkMail :email="entity.email" />
</template>
</VnLv>
<VnLv :label="t('globals.department')"> <VnLv :label="t('globals.department')">
<template #value> <template #value>
<span class="link" v-text="entity.department?.name" /> <span class="link" v-text="entity.department?.name" />
@ -181,11 +179,11 @@ const sumRisk = ({ clientRisks }) => {
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle <VnTitle
:url="`#/customer/${entityId}/billing-data`" :url="`#/customer/${entityId}/billing-data`"
:text="t('customer.summary.payMethodFk')" :text="t('customer.summary.billingData')"
/> />
<VnLv <VnLv
:label="t('customer.summary.payMethod')" :label="t('customer.summary.payMethod')"
:value="entity.payMethod?.name" :value="entity.payMethod.name"
/> />
<VnLv :label="t('customer.summary.bankAccount')" :value="entity.iban" /> <VnLv :label="t('customer.summary.bankAccount')" :value="entity.iban" />
<VnLv :label="t('customer.summary.dueDay')" :value="entity.dueDay" /> <VnLv :label="t('customer.summary.dueDay')" :value="entity.dueDay" />
@ -289,7 +287,7 @@ const sumRisk = ({ clientRisks }) => {
<VnLv <VnLv
v-if="entity.creditInsurance" v-if="entity.creditInsurance"
:label="t('customer.summary.securedCredit')" :label="t('customer.summary.securedCredit')"
:value="`${toCurrency(entity.creditInsurance)} (${entity.classifications[0]?.insurances[0]?.grade || ''})`" :value="toCurrency(entity.creditInsurance)"
:info="t('customer.summary.securedCreditInfo')" :info="t('customer.summary.securedCreditInfo')"
/> />

View File

@ -1,6 +1,5 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import axios from 'axios'; import { createWrapper, axios } from 'app/test/vitest/helper';
import { createWrapper } from 'app/test/vitest/helper';
import CustomerPayments from 'src/pages/Customer/Payments/CustomerPayments.vue'; import CustomerPayments from 'src/pages/Customer/Payments/CustomerPayments.vue';
describe('CustomerPayments', () => { describe('CustomerPayments', () => {

View File

@ -19,7 +19,6 @@ import { checkEntryLock } from 'src/composables/checkEntryLock';
import VnRow from 'src/components/ui/VnRow.vue'; import VnRow from 'src/components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { beforeSave } from 'src/composables/updateMinPriceBeforeSave';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -416,6 +415,56 @@ function getAmountStyle(row) {
return { color: 'var(--vn-label-color)' }; return { color: 'var(--vn-label-color)' };
} }
async function beforeSave(data, getChanges) {
try {
const changes = data.updates;
if (!changes) return data;
const patchPromises = [];
for (const change of changes) {
let patchData = {};
if ('hasMinPrice' in change.data) {
patchData.hasMinPrice = change.data?.hasMinPrice;
delete change.data.hasMinPrice;
}
if ('minPrice' in change.data) {
patchData.minPrice = change.data?.minPrice;
delete change.data.minPrice;
}
if (Object.keys(patchData).length > 0) {
const promise = axios
.get('Buys/findOne', {
params: {
filter: {
fields: ['itemFk'],
where: { id: change.where.id },
},
},
})
.then((buy) => {
return axios.patch(`Items/${buy.data.itemFk}`, patchData);
})
.catch((error) => {
console.error('Error processing change: ', change, error);
});
patchPromises.push(promise);
}
}
await Promise.all(patchPromises);
data.updates = changes.filter((change) => Object.keys(change.data).length > 0);
return data;
} catch (error) {
console.error('Error in beforeSave:', error);
throw error;
}
}
function invertQuantitySign(rows, sign) { function invertQuantitySign(rows, sign) {
for (const row of rows) { for (const row of rows) {
if (sign > 0) row.quantity = Math.abs(row.quantity); if (sign > 0) row.quantity = Math.abs(row.quantity);
@ -648,7 +697,7 @@ onMounted(() => {
:right-search="false" :right-search="false"
:row-click="false" :row-click="false"
:columns="columns" :columns="columns"
:beforeSaveFn="(data, getChanges) => beforeSave(data, getChanges, 'Buys')" :beforeSaveFn="beforeSave"
class="buyList" class="buyList"
:table-height="$props.tableHeight ?? '84vh'" :table-height="$props.tableHeight ?? '84vh'"
auto-load auto-load
@ -738,7 +787,7 @@ onMounted(() => {
<span data-cy="footer-amount">{{ footer?.amount }} / </span> <span data-cy="footer-amount">{{ footer?.amount }} / </span>
<span style="color: var(--q-positive)">{{ footer?.checkedAmount }}</span> <span style="color: var(--q-positive)">{{ footer?.checkedAmount }}</span>
</template> </template>
<template #column-create-itemFk="{ data, validations }"> <template #column-create-itemFk="{ data }">
<VnSelect <VnSelect
url="Items/search" url="Items/search"
v-model="data.itemFk" v-model="data.itemFk"
@ -752,8 +801,7 @@ onMounted(() => {
await setBuyUltimate(value, data); await setBuyUltimate(value, data);
} }
" "
required :required="true"
:rules="[(val) => validations.required(true, val)]"
data-cy="itemFk-create-popup" data-cy="itemFk-create-popup"
sort-by="nickname DESC" sort-by="nickname DESC"
> >

View File

@ -70,8 +70,8 @@ onMounted(async () => {
:url="`#/entry/${entityId}/basic-data`" :url="`#/entry/${entityId}/basic-data`"
:text="t('globals.summary.basicData')" :text="t('globals.summary.basicData')"
/> />
<div class="vn-card-group"> <div class="card-group">
<div class="vn-card-content"> <div class="card-content">
<VnLv <VnLv
:label="t('entry.summary.commission')" :label="t('entry.summary.commission')"
:value="entry?.commission" :value="entry?.commission"
@ -93,7 +93,7 @@ onMounted(async () => {
:value="entry?.invoiceNumber" :value="entry?.invoiceNumber"
/> />
</div> </div>
<div class="vn-card-content"> <div class="card-content">
<VnCheckbox <VnCheckbox
:label="t('entry.list.tableVisibleColumns.isOrdered')" :label="t('entry.list.tableVisibleColumns.isOrdered')"
v-model="entry.isOrdered" v-model="entry.isOrdered"
@ -130,8 +130,8 @@ onMounted(async () => {
:url="`#/travel/${entry.travel.id}/summary`" :url="`#/travel/${entry.travel.id}/summary`"
:text="t('Travel')" :text="t('Travel')"
/> />
<div class="vn-card-group"> <div class="card-group">
<div class="vn-card-content"> <div class="card-content">
<VnLv :label="t('entry.summary.travelReference')"> <VnLv :label="t('entry.summary.travelReference')">
<template #value> <template #value>
<span class="link"> <span class="link">
@ -161,7 +161,7 @@ onMounted(async () => {
:value="entry.travel.warehouseIn?.name" :value="entry.travel.warehouseIn?.name"
/> />
</div> </div>
<div class="vn-card-content"> <div class="card-content">
<VnLv :label="t('travel.awbFk')" :value="entry.travel.awbFk" /> <VnLv :label="t('travel.awbFk')" :value="entry.travel.awbFk" />
<VnCheckbox <VnCheckbox
:label="t('entry.summary.travelDelivered')" :label="t('entry.summary.travelDelivered')"
@ -193,6 +193,31 @@ onMounted(async () => {
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss" scoped>
.card-group {
display: flex;
flex-direction: column;
}
.card-content {
display: flex;
flex-direction: column;
text-overflow: ellipsis;
> div {
max-height: 24px;
}
}
@media (min-width: 1010px) {
.card-group {
flex-direction: row;
}
.card-content {
flex: 1;
margin-right: 16px;
}
}
</style>
<i18n> <i18n>
es: es:
Travel: Envío Travel: Envío

View File

@ -22,7 +22,7 @@ const routes = reactive({
getSupplier: (id) => { getSupplier: (id) => {
return { name: 'SupplierCard', params: { id } }; return { name: 'SupplierCard', params: { id } };
}, },
getInvoices: (id) => { getTickets: (id) => {
return { return {
name: 'InvoiceInList', name: 'InvoiceInList',
query: { query: {
@ -131,11 +131,11 @@ async function setInvoiceCorrection(id) {
</QBtn> </QBtn>
<QBtn <QBtn
size="md" size="md"
icon="vn:invoice-in" icon="vn:ticket"
color="primary" color="primary"
:to="routes.getInvoices(entity.supplierFk)" :to="routes.getTickets(entity.supplierFk)"
> >
<QTooltip>{{ t('invoiceIn.descriptor.invoices') }}</QTooltip> <QTooltip>{{ t('globals.ticketList') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
v-if=" v-if="

View File

@ -15,7 +15,6 @@ invoiceIn:
amount: Amount amount: Amount
descriptor: descriptor:
ticketList: Ticket list ticketList: Ticket list
invoices: Supplier invoices
descriptorMenu: descriptorMenu:
book: Book book: Book
unbook: Unbook unbook: Unbook

View File

@ -14,7 +14,7 @@ invoiceIn:
awb: AWB awb: AWB
amount: Importe amount: Importe
descriptor: descriptor:
invoices: Facturas de proveedor ticketList: Listado de tickets
descriptorMenu: descriptorMenu:
book: Contabilizar book: Contabilizar
unbook: Descontabilizar unbook: Descontabilizar

View File

@ -79,30 +79,6 @@ const columns = computed(() => [
inWhere: true, inWhere: true,
}, },
}, },
{
align: 'left',
name: 'issued',
label: t('invoiceOut.summary.issued'),
component: 'date',
format: (row) => toDate(row.issued),
columnField: { component: null },
},
{
align: 'left',
name: 'created',
label: t('globals.created'),
component: 'date',
columnField: { component: null },
format: (row) => toDate(row.created),
},
{
align: 'left',
name: 'dued',
label: t('invoiceOut.summary.expirationDate'),
component: 'date',
columnField: { component: null },
format: (row) => toDate(row.dued),
},
{ {
align: 'left', align: 'left',
name: 'clientFk', name: 'clientFk',
@ -156,6 +132,22 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
format: (row) => toCurrency(row.amount), format: (row) => toCurrency(row.amount),
}, },
{
align: 'left',
name: 'created',
label: t('globals.created'),
component: 'date',
columnField: { component: null },
format: (row) => toDate(row.created),
},
{
align: 'left',
name: 'dued',
label: t('invoiceOut.summary.dued'),
component: 'date',
columnField: { component: null },
format: (row) => toDate(row.dued),
},
{ {
align: 'left', align: 'left',
name: 'customsAgentFk', name: 'customsAgentFk',

View File

@ -22,7 +22,7 @@ const $props = defineProps({
}); });
</script> </script>
<template> <template>
<QPopupProxy style="max-width: 10px" data-cy="ItemDescriptor"> <QPopupProxy style="max-width: 10px">
<ItemDescriptor <ItemDescriptor
v-if="$props.id" v-if="$props.id"
:id="$props.id" :id="$props.id"

View File

@ -1,386 +1,574 @@
<script setup> <script setup>
import { onMounted, ref, onUnmounted, computed, watch } from 'vue'; import { onMounted, ref, onUnmounted, nextTick, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState';
import { beforeSave } from 'src/composables/updateMinPriceBeforeSave';
import FetchedTags from 'components/ui/FetchedTags.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import EditFixedPriceForm from 'src/pages/Item/components/EditFixedPriceForm.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import EditTableCellValueForm from 'src/components/EditTableCellValueForm.vue';
import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue';
import { useQuasar } from 'quasar';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
import { tMobile } from 'src/composables/tMobile';
import VnConfirm from 'components/ui/VnConfirm.vue';
import FetchData from 'src/components/FetchData.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useState } from 'src/composables/useState';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
import { isLower, isBigger } from 'src/filters/date.js';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import VnColor from 'src/components/common/VnColor.vue'; import { QCheckbox } from 'quasar';
import { toDate } from 'src/filters';
import { isLower, isBigger } from 'src/filters/date.js';
import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
import { toCurrency } from 'src/filters';
const quasar = useQuasar();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref(); const { openConfirmationModal } = useVnConfirm();
const editFixedPriceForm = ref(null);
const selectedRows = ref([]);
const hasSelectedRows = computed(() => selectedRows.value.length > 0);
const isToClone = ref(false);
const dateColor = 'var(--vn-label-text-color)';
const state = useState(); const state = useState();
const user = state.getUser().fn(); const { notify } = useNotify();
const warehouse = computed(() => user.warehouseFk); const tableRef = ref();
const editTableCellDialogRef = ref(null);
const user = state.getUser();
const fixedPrices = ref([]);
const warehousesOptions = ref([]);
const hasSelectedRows = computed(() => rowsSelected.value.length > 0);
const rowsSelected = ref([]);
const itemFixedPriceFilterRef = ref();
onMounted(async () => { onMounted(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
}); });
onUnmounted(() => (stateStore.rightDrawer = false)); onUnmounted(() => (stateStore.rightDrawer = false));
const defaultColumnAttrs = {
align: 'left',
sortable: true,
};
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'itemFk',
label: t('item.fixedPrice.itemFk'), label: t('item.fixedPrice.itemFk'),
labelAbbreviation: 'Id', name: 'itemFk',
toolTip: t('item.fixedPrice.itemFk'), ...defaultColumnAttrs,
isId: true,
columnField: {
component: 'input', component: 'input',
type: 'number',
},
columnClass: 'shrink',
},
{
label: t('globals.name'),
name: 'name',
...defaultColumnAttrs,
create: true,
columnFilter: { columnFilter: {
attrs: {
component: 'select', component: 'select',
attrs: {
url: 'Items', url: 'Items',
fields: ['id', 'name', 'subName'], fields: ['id', 'name', 'subName'],
optionLabel: 'name', optionLabel: 'name',
optionValue: 'id', optionValue: 'name',
uppercase: false, uppercase: false,
}, },
inWhere: true,
}, },
width: '55px',
isEditable: false,
},
{
labelAbbreviation: '',
name: 'hex',
columnSearch: false,
isEditable: false,
width: '9px',
component: 'select',
attrs: {
url: 'Inks',
fields: ['id', 'name'],
},
},
{
align: 'left',
label: t('globals.name'),
name: 'name',
create: true,
component: 'input',
isEditable: false,
}, },
{ {
label: t('item.fixedPrice.groupingPrice'), label: t('item.fixedPrice.groupingPrice'),
labelAbbreviation: 'P. Group',
toolTip: t('item.fixedPrice.groupingPrice'),
name: 'rate2', name: 'rate2',
component: 'number', ...defaultColumnAttrs,
create: true, component: 'input',
createOrder: 3, type: 'number',
createAttrs: {
required: true,
},
width: '50px',
format: (row) => toCurrency(row.rate2),
}, },
{ {
label: t('item.fixedPrice.packingPrice'), label: t('item.fixedPrice.packingPrice'),
labelAbbreviation: 'P. Pack',
toolTip: t('item.fixedPrice.packingPrice'),
name: 'rate3', name: 'rate3',
component: 'number', ...defaultColumnAttrs,
create: true, component: 'input',
createOrder: 4, type: 'number',
createAttrs: {
required: true,
},
width: '50px',
format: (row) => toCurrency(row.rate3),
},
{
name: 'hasMinPrice',
label: t('item.fixedPrice.hasMinPrice'),
labelAbbreviation: t('item.fixedPrice.MP'),
toolTip: t('item.fixedPrice.hasMinPrice'),
label: t('item.fixedPrice.hasMinPrice'),
component: 'checkbox',
attrs: {
toggleIndeterminate: false,
},
width: '50px',
}, },
{ {
label: t('item.fixedPrice.minPrice'), label: t('item.fixedPrice.minPrice'),
labelAbbreviation: 'Min.P',
toolTip: t('item.fixedPrice.minPrice'),
name: 'minPrice', name: 'minPrice',
component: 'number', ...defaultColumnAttrs,
width: '50px', component: 'input',
style: (row) => { type: 'number',
if (!row?.hasMinPrice) return { color: 'var(--vn-label-color)' };
},
format: (row) => toCurrency(row.minPrice),
}, },
{ {
label: t('item.fixedPrice.started'), label: t('item.fixedPrice.started'),
field: 'started',
name: 'started', name: 'started',
format: ({ started }) => toDate(started),
...defaultColumnAttrs,
columnField: {
component: 'date', component: 'date',
class: 'shrink',
},
columnFilter: { columnFilter: {
component: 'date', component: 'date',
}, },
createAttrs: { columnClass: 'expand',
required: true,
},
create: true,
createOrder: 5,
width: '65px',
}, },
{ {
label: t('item.fixedPrice.ended'), label: t('item.fixedPrice.ended'),
name: 'ended', name: 'ended',
...defaultColumnAttrs,
columnField: {
component: 'date', component: 'date',
class: 'shrink',
},
columnFilter: { columnFilter: {
component: 'date', component: 'date',
}, },
createAttrs: { columnClass: 'expand',
required: true, format: (row) => toDate(row.ended),
},
create: true,
createOrder: 6,
width: '65px',
}, },
{ {
align: 'center',
label: t('globals.warehouse'), label: t('globals.warehouse'),
name: 'warehouseFk', name: 'warehouseFk',
...defaultColumnAttrs,
columnClass: 'shrink',
component: 'select',
options: warehousesOptions,
columnFilter: {
name: 'warehouseFk',
inWhere: true,
component: 'select', component: 'select',
attrs: { attrs: {
url: 'Warehouses', options: warehousesOptions,
fields: ['id', 'name'], 'option-label': 'name',
optionLabel: 'name', 'option-value': 'id',
optionValue: 'id', },
}, },
create: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.warehouseName),
width: '80px',
}, },
{ {
align: 'right', align: 'right',
name: 'tableActions', name: 'tableActions',
actions: [ actions: [
{ {
title: t('globals.clone'), title: t('delete'),
icon: 'vn:clone', icon: 'delete',
action: (row) => openCloneFixedPriceForm(row), action: (row) => confirmRemove(row),
isPrimary: true, isPrimary: true,
}, },
], ],
}, },
]); ]);
const openEditFixedPriceForm = () => { const editTableFieldsOptions = [
editFixedPriceForm.value.show(); {
}; field: 'rate2',
label: t('item.fixedPrice.groupingPrice'),
const openCloneFixedPriceForm = (row) => { component: 'input',
tableRef.value.showForm = true; attrs: {
isToClone.value = true; type: 'number',
tableRef.value.create.title = t('Clone fixed price'); },
tableRef.value.create.formInitialData = (({ },
itemFk, {
rate2, field: 'rate3',
rate3, label: t('item.fixedPrice.packingPrice'),
started, component: 'input',
ended, attrs: {
warehouseFk, type: 'number',
}) => ({ },
itemFk, },
rate2, {
rate3, field: 'minPrice',
started, label: t('item.fixedPrice.minPrice'),
ended, component: 'input',
warehouseFk, attrs: {
}))(JSON.parse(JSON.stringify(row))); type: 'number',
}; },
},
const dateStyle = (date) => {
date field: 'started',
label: t('item.fixedPrice.started'),
component: 'date',
},
{
field: 'ended',
label: t('item.fixedPrice.ended'),
component: 'date',
},
{
field: 'warehouseFk',
label: t('globals.warehouse'),
component: 'select',
attrs: {
options: [],
'option-label': 'name',
'option-value': 'id',
},
},
];
const getRowUpdateInputEvents = (props, resetMinPrice, inputType = 'text') => {
return inputType === 'text'
? { ? {
color: 'var(--vn-black-text-color)', 'keyup.enter': () => upsertPrice(props, resetMinPrice),
blur: () => upsertPrice(props, resetMinPrice),
} }
: { color: dateColor, 'background-color': 'transparent' }; : { 'update:modelValue': () => upsertPrice(props, resetMinPrice) };
const onDataSaved = () => {
tableRef.value.CrudModelRef.saveChanges();
selectedRows.value = [];
}; };
onMounted(() => { const updateMinPrice = async (value, props) => {
if (tableRef.value) { props.row.hasMinPrice = value;
tableRef.value.showForm = false; await upsertPrice({
row: props.row,
col: { field: 'hasMinPrice' },
rowIndex: props.rowIndex,
});
};
const validations = ({ row }) => {
const requiredFields = [
'itemFk',
'started',
'ended',
'rate2',
'rate3',
'warehouseFk',
];
const isValid = requiredFields.every(
(field) => row[field] !== null && row[field] !== undefined
);
return isValid;
};
const upsertPrice = async (props, resetMinPrice = false) => {
const isValid = validations({ ...props });
if (!isValid) {
return;
}
const { row } = props;
const changes = tableRef.value.CrudModelRef.getChanges();
if (changes?.updates?.length > 0) {
if (resetMinPrice) row.hasMinPrice = 0;
}
if (!changes.updates && !changes.creates) return;
const data = await upsertFixedPrice(row);
Object.assign(tableRef.value.CrudModelRef.formData[props.rowIndex], data);
notify(t('globals.dataSaved'), 'positive');
tableRef.value.reload();
};
async function upsertFixedPrice(row) {
const { data } = await axios.patch('FixedPrices/upsertFixedPrice', row);
data.hasMinPrice = data.hasMinPrice ? 1 : 0;
return data;
}
function checkLastVisibleRow() {
let lastVisibleRow = null;
getTableRows().forEach((row, index) => {
const rect = row.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
lastVisibleRow = index;
} }
}); });
watch( return lastVisibleRow;
() => tableRef.value?.showForm, }
(newVal) => {
if (!newVal) { const addRow = (original = null) => {
tableRef.value.create.title = ''; let copy = null;
tableRef.value.create.formInitialData = { warehouseFk: warehouse }; const today = Date.vnNew();
if (tableRef.value) { const millisecsInDay = 86400000;
isToClone.value = false; const daysInWeek = 7;
tableRef.value.create.title = t('Create fixed price'); const nextWeek = new Date(today.getTime() + daysInWeek * millisecsInDay);
copy = {
id: 0,
started: today,
ended: nextWeek,
hasMinPrice: 0,
$index: 0,
};
return { original, copy };
};
const getTableRows = () =>
document.getElementsByClassName('q-table')[0].querySelectorAll('tr.cursor-pointer');
function highlightNewRow({ $index: index }) {
const row = getTableRows()[index];
if (row) {
row.classList.add('highlight');
setTimeout(() => {
row.classList.remove('highlight');
}, 3000);
} }
} }
const openEditTableCellDialog = () => {
editTableCellDialogRef.value.show();
};
const onEditCellDataSaved = async () => {
rowsSelected.value = [];
tableRef.value.reload();
};
const removeFuturePrice = async () => {
rowsSelected.value.forEach(({ id }) => {
const rowIndex = fixedPrices.value.findIndex(({ id }) => id === id);
removePrice(id, rowIndex);
});
};
function confirmRemove(item, isFuture) {
const promise = async () =>
isFuture ? removeFuturePrice(item.id) : removePrice(item.id);
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.rowWillBeRemoved'),
message: t('globals.confirmDeletion'),
promise,
}, },
); });
}
const removePrice = async (id) => {
await axios.delete(`FixedPrices/${id}`);
notify(t('globals.dataSaved'), 'positive');
tableRef.value.reload({});
};
const dateStyle = (date) =>
date
? {
'bg-color': 'warning',
'is-outlined': true,
}
: {};
function handleOnDataSave({ CrudModelRef }) {
const { original, copy } = addRow(CrudModelRef.formData[checkLastVisibleRow()]);
if (original) {
CrudModelRef.formData.splice(original?.$index ?? 0, 0, copy);
} else {
CrudModelRef.insert(copy);
}
nextTick(() => {
highlightNewRow(original ?? { $index: 0 });
});
}
</script> </script>
<template> <template>
<FetchData
@on-fetch="(data) => (warehousesOptions = data)"
auto-load
url="Warehouses"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
/>
<RightMenu> <RightMenu>
<template #right-panel> <template #right-panel>
<ItemFixedPriceFilter data-key="ItemFixedPrices" /> <ItemFixedPriceFilter
data-key="ItemFixedPrices"
ref="itemFixedPriceFilterRef"
/>
</template> </template>
</RightMenu> </RightMenu>
<VnSubToolbar /> <VnSubToolbar>
<Teleport to="#st-data" v-if="stateStore?.isSubToolbarShown()"> <template #st-actions>
<QBtnGroup push style="column-gap: 10px">
<QBtn <QBtn
:disable="!hasSelectedRows" :disable="!hasSelectedRows"
@click="openEditFixedPriceForm()" @click="openEditTableCellDialog()"
color="primary" color="primary"
icon="edit" icon="edit"
flat
:label="t('globals.edit')"
data-cy="FixedPriceToolbarEditBtn"
> >
<QTooltip> <QTooltip>
{{ t('Edit fixed price(s)') }} {{ t('Edit fixed price(s)') }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
</QBtnGroup> <QBtn
</Teleport> :disable="!hasSelectedRows"
:label="tMobile('globals.remove')"
color="primary"
icon="delete"
flat
@click="(row) => confirmRemove(row, true)"
:title="t('globals.remove')"
/>
</template>
</VnSubToolbar>
<VnTable <VnTable
ref="tableRef" :default-remove="false"
:default-reset="false"
:default-save="false"
data-key="ItemFixedPrices" data-key="ItemFixedPrices"
url="FixedPrices/filter" url="FixedPrices/filter"
:order="'name DESC'" :order="['name DESC', 'itemFk DESC']"
save-url="FixedPrices/crud" save-url="FixedPrices/crud"
ref="tableRef"
dense
:filter="{
where: {
warehouseFk: user.warehouseFk,
},
}"
:columns="columns" :columns="columns"
default-mode="table"
auto-load
:is-editable="true" :is-editable="true"
:right-search="false" :right-search="false"
:table="{ :table="{
'row-key': 'id', 'row-key': 'id',
selection: 'multiple', selection: 'multiple',
}" }"
v-model:selected="selectedRows" v-model:selected="rowsSelected"
:create-as-dialog="false"
:create="{ :create="{
urlCreate: 'FixedPrices', onDataSaved: handleOnDataSave,
title: t('Create fixed price'),
formInitialData: { warehouseFk: warehouse },
onDataSaved: () => tableRef.reload(),
showSaveAndContinueBtn: true,
}" }"
:disable-option="{ card: true }" :disable-option="{ card: true }"
auto-load :has-sub-toolbar="false"
:beforeSaveFn="(data, getChanges) => beforeSave(data, getChanges, 'FixedPrices')"
> >
<template #column-hex="{ row }"> <template #header-selection="scope">
<VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" /> <QCheckbox v-model="scope.selected" />
</template>
<template #body-selection="scope">
{{ scope }}
<QCheckbox flat v-model="scope.selected" />
</template>
<template #column-itemFk="props">
<VnSelect
style="max-width: 100px"
url="Items/withName"
hide-selected
option-label="id"
option-value="id"
v-model="props.row.itemFk"
v-on="getRowUpdateInputEvents(props, true, 'select')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.name }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template> </template>
<template #column-name="{ row }"> <template #column-name="{ row }">
<span class="link"> <span class="link">
{{ row.name }} {{ row.name }}
<ItemDescriptorProxy :id="row.itemFk" />
</span> </span>
<span class="subName">{{ row.subName }}</span> <span class="subName">{{ row.subName }}</span>
<FetchedTags :item="row" :columns="6" /> <ItemDescriptorProxy :id="row.itemFk" />
<FetchedTags :item="row" :columns="3" />
</template> </template>
<template #column-started="{ row }"> <template #column-rate2="props">
<div class="editable-text q-pb-xxs"> <QTd class="col">
<QBadge class="badge" :style="dateStyle(isLower(row?.ended))"> <VnInput
{{ toDate(row?.started) }} type="currency"
</QBadge> style="width: 75px"
</div> v-model.number="props.row.rate2"
</template> v-on="getRowUpdateInputEvents(props)"
<template #column-ended="{ row }">
<div class="editable-text q-pb-xxs">
<QBadge class="badge" :style="dateStyle(isBigger(row?.ended))">
{{ toDate(row?.ended) }}
</QBadge>
</div>
</template>
<template #column-create-name="{ data }">
<VnSelect
url="Items/search"
v-model="data.itemFk"
:label="t('item.fixedPrice.itemName')"
:fields="['id', 'name']"
:filter-options="['id', 'name']"
option-label="name"
option-value="id"
:required="true"
sort-by="name ASC"
data-cy="FixedPriceCreateNameSelect"
> >
<template #option="scope"> <template #append></template>
<QItem v-bind="scope.itemProps"> </VnInput>
<QItemSection> </QTd>
<QItemLabel>
{{ scope.opt.name }}
</QItemLabel>
<QItemLabel caption> #{{ scope.opt.id }} </QItemLabel>
</QItemSection>
</QItem>
</template> </template>
</VnSelect> <template #column-rate3="props">
<QTd class="col">
<VnInput
style="width: 75px"
type="currency"
v-model.number="props.row.rate3"
v-on="getRowUpdateInputEvents(props)"
>
<template #append></template>
</VnInput>
</QTd>
</template> </template>
<template #column-create-warehouseFk="{ data }"> <template #column-minPrice="props">
<QTd class="col">
<div class="row" style="align-items: center">
<QCheckbox
:model-value="props.row.hasMinPrice"
@update:model-value="updateMinPrice($event, props)"
:false-value="0"
:true-value="1"
:toggle-indeterminate="false"
/>
<VnInput
class="col"
type="currency"
mask="###.##"
:disable="props.row.hasMinPrice === 0"
v-model.number="props.row.minPrice"
v-on="getRowUpdateInputEvents(props)"
>
<template #append></template>
</VnInput>
</div>
</QTd>
</template>
<template #column-started="props">
<VnInputDate
class="vnInputDate"
:show-event="true"
v-model="props.row.started"
v-on="getRowUpdateInputEvents(props, false, 'date')"
v-bind="dateStyle(isBigger(props.row.started))"
/>
</template>
<template #column-ended="props">
<VnInputDate
class="vnInputDate"
:show-event="true"
v-model="props.row.ended"
v-on="getRowUpdateInputEvents(props, false, 'date')"
v-bind="dateStyle(isLower(props.row.ended))"
/>
</template>
<template #column-warehouseFk="props">
<QTd class="col">
<VnSelect <VnSelect
:label="t('globals.warehouse')" style="max-width: 150px"
url="Warehouses" :options="warehousesOptions"
v-model="data.warehouseFk"
:fields="['id', 'name']"
option-label="name"
option-value="id"
hide-selected hide-selected
:required="true" option-label="name"
sort-by="name ASC" option-value="id"
data-cy="FixedPriceCreateWarehouseSelect" v-model="props.row.warehouseFk"
> v-on="getRowUpdateInputEvents(props, false, 'select')"
<template #option="scope"> />
<QItem v-bind="scope.itemProps"> </QTd>
<QItemSection>
<QItemLabel>
{{ scope.opt.name }}
</QItemLabel>
<QItemLabel caption> #{{ scope.opt.id }} </QItemLabel>
</QItemSection>
</QItem>
</template> </template>
</VnSelect> <template #column-deleteAction="{ row, rowIndex }">
</template> <QIcon
</VnTable> name="delete"
<QDialog ref="editFixedPriceForm"> size="sm"
<EditFixedPriceForm class="cursor-pointer fill-icon-on-hover"
edit-url="FixedPrices/editFixedPrice" color="primary"
:rows="selectedRows" @click.stop="
:fields-options=" openConfirmationModal(
columns.filter( t('globals.rowWillBeRemoved'),
({ isEditable, component, name }) => t('Do you want to clone this item?'),
isEditable !== false && component && name !== 'itemFk', () => removePrice(row.id, rowIndex)
) )
" "
:beforeSave="beforeSave" >
@on-data-saved="onDataSaved" <QTooltip class="text-no-wrap">
{{ t('globals.delete') }}
</QTooltip>
</QIcon>
</template>
</VnTable>
<QDialog ref="editTableCellDialogRef">
<EditTableCellValueForm
edit-url="FixedPrices/editFixedPrice"
:rows="rowsSelected"
:fields-options="editTableFieldsOptions"
@on-data-saved="onEditCellDataSaved()"
/> />
</QDialog> </QDialog>
</template> </template>
@ -435,17 +623,8 @@ tbody tr.highlight .q-td {
color: var(--vn-label-color); color: var(--vn-label-color);
} }
</style> </style>
<style lang="scss" scoped>
.badge {
background-color: $warning;
}
</style>
<i18n> <i18n>
es: es:
Add fixed price: Añadir precio fijado Add fixed price: Añadir precio fijado
Edit fixed price(s): Editar precio(s) fijado(s) Edit fixed price(s): Editar precio(s) fijado(s)
Create fixed price: Crear precio fijado
Clone fixed price: Clonar precio fijado
</i18n> </i18n>

View File

@ -29,7 +29,6 @@ const props = defineProps({
dense dense
filled filled
use-input use-input
:use-like="false"
@update:model-value="searchFn()" @update:model-value="searchFn()"
sort-by="nickname ASC" sort-by="nickname ASC"
/> />
@ -51,19 +50,21 @@ const props = defineProps({
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem class="q-my-md">
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
v-model="params.started"
:label="t('params.started')" :label="t('params.started')"
v-model="params.started"
filled filled
@update:model-value="searchFn()" @update:model-value="searchFn()"
/> />
</QItemSection> </QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
v-model="params.ended"
:label="t('params.ended')" :label="t('params.ended')"
v-model="params.ended"
filled filled
@update:model-value="searchFn()" @update:model-value="searchFn()"
/> />

View File

@ -7,7 +7,7 @@ import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue'; import { QCheckbox } from 'quasar';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
@ -250,9 +250,10 @@ onMounted(async () => {
</QItemSection> </QItemSection>
</QItem> </QItem>
<!-- Tags filter --> <!-- Tags filter -->
<QItemLabel header> <QItem class="row items-center">
<QItemLabel>
{{ t('params.tags') }} {{ t('params.tags') }}
</QItemLabel>
<QIcon <QIcon
name="add_circle" name="add_circle"
class="fill-icon-on-hover q-ml-md" class="fill-icon-on-hover q-ml-md"
@ -260,7 +261,7 @@ onMounted(async () => {
color="primary" color="primary"
@click="tagValues.push({})" @click="tagValues.push({})"
/> />
</QItemLabel> </QItem>
<QItem <QItem
v-for="(tag, index) in tagValues" v-for="(tag, index) in tagValues"
:key="index" :key="index"
@ -268,7 +269,6 @@ onMounted(async () => {
> >
<QItemSection class="col"> <QItemSection class="col">
<VnSelect <VnSelect
class="full-width"
:label="t('params.tag')" :label="t('params.tag')"
v-model="tag.selectedTag" v-model="tag.selectedTag"
:options="tagOptions" :options="tagOptions"
@ -316,19 +316,25 @@ onMounted(async () => {
/> />
</QItem> </QItem>
<!-- Filter fields --> <!-- Filter fields -->
<QItemLabel header <QItem class="row items-center">
>{{ t('More fields') }} <QItemLabel>
{{ t('More fields') }}
</QItemLabel>
<QIcon <QIcon
name="add_circle" name="add_circle"
class="fill-icon-on-hover q-ml-md" class="fill-icon-on-hover q-ml-md"
size="sm" size="sm"
color="primary" color="primary"
@click="fieldFiltersValues.push({})" @click="fieldFiltersValues.push({})"
/></QItemLabel> />
<QItem v-for="(fieldFilter, index) in fieldFiltersValues" :key="index"> </QItem>
<QItem
v-for="(fieldFilter, index) in fieldFiltersValues"
:key="index"
class="row items-center"
>
<QItemSection class="col"> <QItemSection class="col">
<VnSelect <VnSelect
class="full-width"
:label="t('params.tag')" :label="t('params.tag')"
:model-value="fieldFilter.selectedField" :model-value="fieldFilter.selectedField"
:options="moreFields" :options="moreFields"
@ -349,7 +355,7 @@ onMounted(async () => {
/> />
</QItemSection> </QItemSection>
<QItemSection class="col"> <QItemSection class="col">
<VnCheckbox <QCheckbox
v-if="fieldFilter.selectedField?.type === 'boolean'" v-if="fieldFilter.selectedField?.type === 'boolean'"
v-model="fieldFilter.value" v-model="fieldFilter.value"
:label="t('params.value')" :label="t('params.value')"
@ -364,14 +370,13 @@ onMounted(async () => {
@keydown.enter="applyFieldFilters(params, searchFn)" @keydown.enter="applyFieldFilters(params, searchFn)"
/> />
</QItemSection> </QItemSection>
<QItemSection side <QIcon
><QIcon
name="delete" name="delete"
class="fill-icon-on-hover q-ml-xs" class="fill-icon-on-hover q-ml-xs"
size="sm" size="sm"
color="primary" color="primary"
@click="removeFieldFilter(index, params, searchFn)" @click="removeFieldFilter(index, params, searchFn)"
/></QItemSection> />
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>

View File

@ -1,46 +0,0 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import EditFixedPriceForm from 'src/pages/Item/components/EditFixedPriceForm.vue';
describe('EditFixedPriceForm.vue', () => {
let wrapper;
let vm;
const mockRows = [
{ id: 1, itemFk: 101 },
{ id: 2, itemFk: 102 },
];
const mockFieldsOptions = [
{
name: 'price',
label: 'Price',
component: 'input',
attrs: { type: 'number' },
},
];
beforeEach(() => {
wrapper = createWrapper(EditFixedPriceForm, {
props: {
rows: JSON.parse(JSON.stringify(mockRows)),
fieldsOptions: mockFieldsOptions,
},
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should emit "onDataSaved" with updated rows on submit', async () => {
vm.selectedField = mockFieldsOptions[0];
vm.newValue = 199.99;
await vm.onSubmit();
expect(wrapper.emitted('onDataSaved')).toBeTruthy();
});
});

View File

@ -167,8 +167,6 @@ item:
started: Started started: Started
ended: Ended ended: Ended
warehouse: Warehouse warehouse: Warehouse
MP: MP
itemName: Item
create: create:
name: Name name: Name
tag: Tag tag: Tag

View File

@ -173,8 +173,6 @@ item:
started: Inicio started: Inicio
ended: Fin ended: Fin
warehouse: Almacén warehouse: Almacén
MP: PM
itemName: Nombre
create: create:
name: Nombre name: Nombre
tag: Etiqueta tag: Etiqueta

View File

@ -1,7 +1,6 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper, axios } from 'app/test/vitest/helper';
import Login from 'pages/Login/LoginMain.vue'; import Login from 'pages/Login/LoginMain.vue';
import axios from 'axios';
describe('Login', () => { describe('Login', () => {
let vm; let vm;

View File

@ -295,11 +295,13 @@ watch(
:user-filter="lineFilter" :user-filter="lineFilter"
> >
<template #column-image="{ row }"> <template #column-image="{ row }">
<div class="image-wrapper">
<VnImg <VnImg
:id="parseInt(row?.item?.image)" :id="parseInt(row?.item?.image)"
class="rounded" class="rounded"
zoom-resolution="1600x900" zoom-resolution="1600x900"
/> />
</div>
</template> </template>
<template #column-id="{ row }"> <template #column-id="{ row }">
<span class="link" @click.stop> <span class="link" @click.stop>
@ -359,6 +361,12 @@ watch(
} }
} }
.image-wrapper {
height: 50px;
width: 50px;
margin-left: 30%;
}
.header { .header {
color: $primary; color: $primary;
font-weight: bold; font-weight: bold;

View File

@ -112,9 +112,12 @@ const filter = {
:label="t('Trailer Plate')" :label="t('Trailer Plate')"
:value="dashIfEmpty(entity?.trailerPlate)" :value="dashIfEmpty(entity?.trailerPlate)"
/> />
<VnLv :label="t('Phone')"> <VnLv :label="t('Phone')" :value="dashIfEmpty(entity?.phone)">
<template #value> <template #value>
<span>
{{ dashIfEmpty(entity?.phone) }}
<VnLinkPhone :phone-number="entity?.phone" /> <VnLinkPhone :phone-number="entity?.phone" />
</span>
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv

View File

@ -46,6 +46,7 @@ const columns = computed(() => [
}, },
isId: true, isId: true,
columnFilter: false, columnFilter: false,
width: '25px',
}, },
{ {
name: 'workerFk', name: 'workerFk',
@ -141,6 +142,7 @@ const columns = computed(() => [
label: 'm3', label: 'm3',
cardVisible: true, cardVisible: true,
columnClass: 'shrink', columnClass: 'shrink',
width: '50px',
}, },
{ {
name: 'started', name: 'started',
@ -148,6 +150,7 @@ const columns = computed(() => [
component: 'time', component: 'time',
columnFilter: false, columnFilter: false,
format: ({ started }) => toHour(started), format: ({ started }) => toHour(started),
width: '50px',
}, },
{ {
name: 'finished', name: 'finished',
@ -155,6 +158,7 @@ const columns = computed(() => [
component: 'time', component: 'time',
columnFilter: false, columnFilter: false,
format: ({ finished }) => toHour(finished), format: ({ finished }) => toHour(finished),
width: '50px',
}, },
{ {
align: 'right', align: 'right',

View File

@ -46,6 +46,7 @@ const columns = computed(() => [
condition: () => true, condition: () => true,
}, },
columnFilter: false, columnFilter: false,
width: '25px',
}, },
{ {
align: 'left', align: 'left',
@ -56,6 +57,7 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef),
columnFilter: false, columnFilter: false,
width: '100px',
}, },
{ {
label: t('globals.agency'), label: t('globals.agency'),
@ -98,6 +100,7 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
columnFilter: false, columnFilter: false,
format: ({ started }) => toHour(started), format: ({ started }) => toHour(started),
width: '50px',
}, },
{ {
align: 'center', align: 'center',
@ -106,6 +109,7 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
columnFilter: false, columnFilter: false,
format: ({ finished }) => toHour(finished), format: ({ finished }) => toHour(finished),
width: '50px',
}, },
{ {
align: 'left', align: 'left',

View File

@ -91,7 +91,7 @@ const totalPrice = computed(() => {
const totalNewPrice = computed(() => { const totalNewPrice = computed(() => {
return rows.value.reduce( return rows.value.reduce(
(acc, item) => acc + item.component.newPrice * item.quantity, (acc, item) => acc + item.component.newPrice * item.quantity,
0, 0
); );
}); });
@ -210,18 +210,18 @@ onMounted(async () => {
flat flat
> >
<template #body-cell-item="{ row }"> <template #body-cell-item="{ row }">
<QTd align="center"> <QTd @click.stop class="link">
<span @click.stop class="link"> <QBtn flat>
{{ row.itemFk }} {{ row.itemFk }}
<ItemDescriptorProxy :id="row.itemFk" /> <ItemDescriptorProxy :id="row.itemFk" />
</span> </QBtn>
</QTd> </QTd>
</template> </template>
<template #body-cell-description="{ row }"> <template #body-cell-description="{ row }">
<QTd style="min-width: 120px; max-width: 120px"> <QTd style="min-width: 120px; max-width: 120px">
<div class="column q-pb-xs" style="min-width: 120px"> <div class="column q-pb-xs" style="min-width: 120px">
<span>{{ row.item.name }}</span> <span>{{ row.item.name }}</span>
<FetchedTags :item="row.item" class="full-width" :columns="6" /> <FetchedTags :item="row.item" class="full-width" />
</div> </div>
</QTd> </QTd>
</template> </template>

View File

@ -25,7 +25,9 @@ const { validate } = useValidator();
const { notify } = useNotify(); const { notify } = useNotify();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const canEditZone = useAcl().hasAcl('Ticket', 'editZone', 'WRITE'); const canEditZone = useAcl().hasAny([
{ model: 'Ticket', props: 'editZone', accessType: 'WRITE' },
]);
const agencyFetchRef = ref(); const agencyFetchRef = ref();
const warehousesOptions = ref([]); const warehousesOptions = ref([]);

View File

@ -0,0 +1,59 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import { useState } from 'src/composables/useState';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
const emit = defineEmits(['onRequestCreated']);
const route = useRoute();
const { t } = useI18n();
const state = useState();
const user = state.getUser();
const stateFetchDataRef = ref(null);
const statesOptions = ref([]);
const onStateFkChange = (formData) => (formData.userFk = user.value.id);
</script>
<template>
<FetchData
ref="stateFetchDataRef"
url="States"
auto-load
@on-fetch="(data) => (statesOptions = data)"
/>
<FormModelPopup
:title="t('Create tracking')"
url-create="Tickets/state"
model="CreateTicketTracking"
:form-initial-data="{ ticketFk: route.params.id }"
@on-data-saved="() => emit('onRequestCreated')"
>
<template #form-inputs="{ data }">
<VnRow>
<VnSelect
v-model="data.stateFk"
:label="t('ticketList.state')"
:options="statesOptions"
@update:model-value="onStateFkChange(data)"
hide-selected
option-label="name"
option-value="id"
/>
<VnSelectWorker v-model="data.userFk" :fields="['id', 'name']" />
</VnRow>
</template>
</FormModelPopup>
</template>
<i18n>
es:
Create tracking: Crear estado
</i18n>

View File

@ -46,7 +46,7 @@ async function getClaims() {
originalTicket.value = data[0]?.originalTicketFk; originalTicket.value = data[0]?.originalTicketFk;
} }
async function getProblems() { async function getProblems() {
const { data } = await axios.get(`Tickets/getTicketProblems`, {params: { ids: [entityId.value] }}); const { data } = await axios.get(`Tickets/${entityId.value}/getTicketProblems`);
if (!data) return; if (!data) return;
problems.value = data[0]; problems.value = data[0];
} }

View File

@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import VnUsesMana from 'components/ui/VnUsesMana.vue';
const $props = defineProps({ const $props = defineProps({
newPrice: { newPrice: {
@ -14,36 +15,23 @@ const $props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
componentId: {
type: Number,
default: null,
},
}); });
const emit = defineEmits(['save', 'cancel', 'update:componentId']);
const route = useRoute(); const route = useRoute();
const mana = ref(null); const mana = ref(null);
const usesMana = ref([]); const usesMana = ref(false);
const emit = defineEmits(['save', 'cancel']);
const { t } = useI18n(); const { t } = useI18n();
const QPopupProxyRef = ref(null); const QPopupProxyRef = ref(null);
const manaCode = ref($props.manaCode);
const componentId = computed({
get: () => $props.componentId,
set: (val) => emit('update:componentId', val),
});
const save = (sale = $props.sale) => { const save = (sale = $props.sale) => {
emit('save', sale); emit('save', sale);
QPopupProxyRef.value.hide(); QPopupProxyRef.value.hide();
}; };
const cancel = () => {
emit('cancel');
QPopupProxyRef.value.hide();
};
const getMana = async () => { const getMana = async () => {
const { data } = await axios.get(`Tickets/${route.params.id}/getDepartmentMana`); const { data } = await axios.get(`Tickets/${route.params.id}/getDepartmentMana`);
mana.value = data; mana.value = data;
@ -51,12 +39,15 @@ const getMana = async () => {
}; };
const getUsesMana = async () => { const getUsesMana = async () => {
const { data } = await axios.get('Sales/getComponents'); const { data } = await axios.get('Sales/usesMana');
usesMana.value = data; usesMana.value = data;
}; };
const cancel = () => {
emit('cancel');
QPopupProxyRef.value.hide();
};
const hasMana = computed(() => typeof mana.value === 'number'); const hasMana = computed(() => typeof mana.value === 'number');
defineExpose({ save }); defineExpose({ save });
</script> </script>
@ -68,29 +59,17 @@ defineExpose({ save });
> >
<div class="container"> <div class="container">
<div class="header">Mana: {{ toCurrency(mana) }}</div> <div class="header">Mana: {{ toCurrency(mana) }}</div>
<QSpinner v-if="!hasMana" color="primary" size="md" />
<QSpinner v-if="!usesMana.length" color="primary" />
<div class="q-pa-md" v-else> <div class="q-pa-md" v-else>
<slot :popup="QPopupProxyRef" /> <slot :popup="QPopupProxyRef" />
<div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm">
<div v-if="usesMana.length" class="column q-gutter-y-sm q-mt-sm"> <VnUsesMana :mana-code="manaCode" />
<QRadio
v-for="(item, index) in usesMana"
:key="index"
v-model="componentId"
:val="item.id"
:label="item.name"
dense
:dark="true"
class="q-mb-sm"
:data-cy="`componentOption-${item.id}`"
/>
</div> </div>
<div v-if="newPrice" class="column items-center q-mt-lg"> <div v-if="newPrice" class="column items-center q-mt-lg">
<span class="text-primary">{{ t('New price') }}</span> <span class="text-primary">{{ t('New price') }}</span>
<span class="text-subtitle1">{{ toCurrency(newPrice) }}</span> <span class="text-subtitle1">
{{ toCurrency($props.newPrice) }}
</span>
</div> </div>
</div> </div>
@ -98,6 +77,7 @@ defineExpose({ save });
<QBtn <QBtn
color="primary" color="primary"
class="no-border-radius" class="no-border-radius"
dense
style="width: 50%" style="width: 50%"
@click="cancel()" @click="cancel()"
> >
@ -106,6 +86,7 @@ defineExpose({ save });
<QBtn <QBtn
color="primary" color="primary"
class="no-border-radius" class="no-border-radius"
dense
style="width: 50%" style="width: 50%"
@click="save()" @click="save()"
data-cy="saveManaBtn" data-cy="saveManaBtn"
@ -135,3 +116,8 @@ defineExpose({ save });
min-width: 230px; min-width: 230px;
} }
</style> </style>
<i18n>
es:
New price: Nuevo precio
</i18n>

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