4774-traducciones #853

Open
carlossa wants to merge 34 commits from 4774-traducciones into dev
244 changed files with 8151 additions and 6685 deletions
Showing only changes of commit b1faf14f18 - Show all commits

View File

@ -1,4 +1,4 @@
module.exports = { export default {
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
// This option interrupts the configuration hierarchy at this file // 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) // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
@ -58,7 +58,7 @@ module.exports = {
rules: { rules: {
'prefer-promise-reject-errors': 'off', 'prefer-promise-reject-errors': 'off',
'no-unused-vars': 'warn', 'no-unused-vars': 'warn',
"vue/no-multiple-template-root": "off" , 'vue/no-multiple-template-root': 'off',
// allow debugger during development only // allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
}, },

4
.gitignore vendored
View File

@ -29,5 +29,5 @@ yarn-error.log*
*.sln *.sln
# Cypress directories and files # Cypress directories and files
/tests/cypress/videos /test/cypress/videos
/tests/cypress/screenshots /test/cypress/screenshots

View File

@ -1,23 +1,24 @@
const fs = require('fs'); import { existsSync, readFileSync, writeFileSync } from 'fs';
const path = require('path'); import { join, resolve } from 'path';
function getCurrentBranchName(p = process.cwd()) { function getCurrentBranchName(p = process.cwd()) {
if (!fs.existsSync(p)) return false; if (!existsSync(p)) return false;
const gitHeadPath = path.join(p, '.git', 'HEAD'); const gitHeadPath = join(p, '.git', 'HEAD');
if (!fs.existsSync(gitHeadPath)) if (!existsSync(gitHeadPath)) {
return getCurrentBranchName(path.resolve(p, '..')); return getCurrentBranchName(resolve(p, '..'));
}
const headContent = fs.readFileSync(gitHeadPath, 'utf-8'); const headContent = readFileSync(gitHeadPath, 'utf-8');
return headContent.trim().split('/')[2]; return headContent.trim().split('/')[2];
} }
const branchName = getCurrentBranchName(); const branchName = getCurrentBranchName();
if (branchName) { if (branchName) {
const msgPath = `.git/COMMIT_EDITMSG`; const msgPath = '.git/COMMIT_EDITMSG';
const msg = fs.readFileSync(msgPath, 'utf-8'); const msg = readFileSync(msgPath, 'utf-8');
const reference = branchName.match(/^\d+/); const reference = branchName.match(/^\d+/);
const referenceTag = `refs #${reference}`; const referenceTag = `refs #${reference}`;
@ -26,8 +27,7 @@ if (branchName) {
if (splitedMsg.length > 1) { if (splitedMsg.length > 1) {
const finalMsg = splitedMsg[0] + ': ' + referenceTag + splitedMsg.slice(1).join(':'); const finalMsg = splitedMsg[0] + ': ' + referenceTag + splitedMsg.slice(1).join(':');
fs.writeFileSync(msgPath, finalMsg); writeFileSync(msgPath, finalMsg);
} }
} }
} }

View File

@ -1,4 +1,4 @@
module.exports = { export default {
singleQuote: true, singleQuote: true,
printWidth: 90, printWidth: 90,
tabWidth: 4, tabWidth: 4,

View File

@ -1,3 +1,82 @@
# Version 25.00 - 2025-01-14
### Added 🆕
- chore: refs #7056 move test by:jorgep
- feat: refs #7050 7050 add object check by:Jtubau
- feat: refs #7050 7050 add test to isEmpty() by:Jtubau
- feat: refs #7056 add tests in FormModel by:jorgep
- feat: refs #7056 update route meta information and add FormModel tests by:jorgep
- feat: refs #7074 tests for fns setData(), parseDms() and showFormDialog() (7074-makeFrontTestToVnDmsList) by:Jtubau
- feat: refs #7079 created VnLocation front test by:provira
- feat: refs #7189 add Accept-Language header to axios requests by:jorgep
- feat: refs #7924 add custom inspection checkbox and localization support by:jgallego
- feat: refs #7924 update custom inspection label for clarity in English and Spanish locales by:jgallego
- feat: refs #8004 enhance FetchedTags component with column support and styling updates by:pablone
- feat: refs #8004 hide rightFilter by:pablone
- feat: refs #8246 added new field in list by:Jon
- feat: refs #8266 added descriptor to item name by:jtubau
- feat: refs #8293 add zone filter by:Jtubau
- feat: refs #8293 include zone data in each record by:Jtubau
- fix: refs #8004 more list style issues by:pablone
- fix: refs #8004 some style issues on all list by:pablone
- style: refs #8004 update layout and styling in FetchedTags and ItemList components by:pablone
- style: update CustomerBalance.vue to set label color by:jgallego
### Changed 📦
- perf: order by:alexm
- perf: redirect transition list to card by:alexm
- perf: refs #8201 onDataSaved fetch by:Jon
- perf: revert processData by:alexm
- perf: simplify if by:alexm
- perf: simplify if (perf_redirectTransition) by:alexm
- refactor: item fixedPrice by:Jon
- refactor: refs #7050 refactorize by:jtubau
- refactor: refs #7050 removed blank spaces by:jtubau
- refactor: refs #7052 move EditTableCellValueForm tests to a new location and enhance test coverage by:jgallego
- refactor: refs #7052 remove unnecessary console logs from EditTableCellValueForm tests by:jgallego
- refactor: refs #7074 move dms constant to global scope by:Jtubau
- refactor: refs #7079 removed useless code by:provira
- refactor: refs #7924 simplify custom inspection icon rendering in ExtraCommunity.vue by:jgallego
- refactor: refs #8004 remove console log from CardSummary component on mount by:pablone
- refactor: refs #8004 remove consoleLogs by:pablone
- refactor: refs #8004 remove unused stateStore import in InvoiceInList.vue by:pablone
- refactor: refs #8004 remove unused travelFilterRef and chip definition in TravelList.vue by:pablone
- refactor: refs #8004 replace VnSelect with VnSelectWorker in CustomerList component by:pablone
- refactor: refs #8201 added onMounted to stablish the value to show icons by:Jon
- refactor: refs #8201 deleted condition by:Jon
- refactor: refs #8201 deleted log by:Jon
- refactor: refs #8201 deleted logs by:Jon
- refactor: refs #8266 8266 change expedition item name by:Jtubau
- refactor: refs #8266 change expedition label by:Jtubau
- refactor: refs #8293 remove redundant attributes by:Jtubau
- refactor: refs #8320 changed folder names from "specs" to "**tests**" by:provira
- refactor: refs #8320 moved front tests to their respective sections by:provira
- refactor: refs #8813 removed unused class property by:provira
### Fixed 🛠️
- fix: discount class by:PAU ROVIRA ROSALENY
- fix: duplicate transalation after test to dev by:alexm
- fix: fix translations by:carlossa
- fix: redirect to sales when confirming lines by:Jon
- fix: refs #7050 delete import added by mistake by:Jtubau
- fix: refs #7133 handleSalesModelValue function to handle empty input by:jorgep
- fix: refs #7189 update user language on sessionStorage by:jorgep
- fix: refs #7935 remove unused 'companyFk' column from InvoiceInList component by:jorgep
- fix: refs #8004 more list style issues by:pablone
- fix: refs #8004 some style issues on all list by:pablone
- fix: refs #8004 update label for daysOnward in TravelFilter component and add translations by:pablone
- fix: refs #8004 vnTable card with and add permanent labels by:pablone
- fix: refs #8201 added onDataSaved emi to refetch when cahnges are made by:Jon
- fix: refs #8201 use arrayData to fix the error by:Jon
- fix: refs #8314 space between label and value by:jtubau
- fix: refs #8813 fixed ClaimLines format by:provira
- fix: update button sizes in ExtraCommunity.vue for better visibility by:jgallego
- perf: revert processData by:alexm
- refactor: item fixedPrice by:Jon
# Version 24.52 - 2024-01-07 # Version 24.52 - 2024-01-07
### Added 🆕 ### Added 🆕

View File

@ -1 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] }; export default { extends: ['@commitlint/config-conventional'] };

View File

@ -1,9 +1,9 @@
const { defineConfig } = require('cypress'); import { defineConfig } from 'cypress';
// https://docs.cypress.io/app/tooling/reporters // https://docs.cypress.io/app/tooling/reporters
// https://docs.cypress.io/app/references/configuration // https://docs.cypress.io/app/references/configuration
// https://www.npmjs.com/package/cypress-mochawesome-reporter // https://www.npmjs.com/package/cypress-mochawesome-reporter
module.exports = defineConfig({ export default defineConfig({
e2e: { e2e: {
baseUrl: 'http://localhost:9000/', baseUrl: 'http://localhost:9000/',
experimentalStudio: true, experimentalStudio: true,
@ -31,8 +31,10 @@ module.exports = defineConfig({
supportFile: 'test/cypress/support/unit.js', supportFile: 'test/cypress/support/unit.js',
}, },
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
require('cypress-mochawesome-reporter/plugin')(on); import('cypress-mochawesome-reporter/plugin').then((plugin) => plugin.default(on));
// implement node event listeners here // implement node event listeners here
}, },
viewportWidth: 1280,
viewportHeight: 720,
}, },
}); });

View File

@ -1,11 +1,12 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "25.04.0", "version": "25.06.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
"private": true, "private": true,
"packageManager": "pnpm@8.15.1", "packageManager": "pnpm@8.15.1",
"type": "module",
"scripts": { "scripts": {
"resetDatabase": "cd ../salix && gulp docker", "resetDatabase": "cd ../salix && gulp docker",
"lint": "eslint --ext .js,.vue ./", "lint": "eslint --ext .js,.vue ./",
@ -20,39 +21,40 @@
"addReferenceTag": "node .husky/addReferenceTag.js" "addReferenceTag": "node .husky/addReferenceTag.js"
}, },
"dependencies": { "dependencies": {
"@quasar/cli": "^2.3.0", "@quasar/cli": "^2.4.1",
"@quasar/extras": "^1.16.9", "@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",
"moment": "^2.30.1", "moment": "^2.30.1",
"pinia": "^2.1.3", "pinia": "^2.1.3",
"quasar": "^2.14.5", "quasar": "^2.17.7",
"validator": "^13.9.0", "validator": "^13.9.0",
"vue": "^3.3.4", "vue": "^3.5.13",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.3.0",
"vue-router": "^4.2.1" "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",
"@intlify/unplugin-vue-i18n": "^0.8.1", "@intlify/unplugin-vue-i18n": "^0.8.2",
"@pinia/testing": "^0.1.2", "@pinia/testing": "^0.1.2",
"@quasar/app-vite": "^1.7.3", "@quasar/app-vite": "^2.0.8",
"@quasar/quasar-app-extension-qcalendar": "4.0.0-beta.15", "@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",
"@vue/test-utils": "^2.4.4", "@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"cypress": "^13.6.6", "cypress": "^13.6.6",
"cypress-mochawesome-reporter": "^3.8.2", "cypress-mochawesome-reporter": "^3.8.2",
"eslint": "^8.41.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-cypress": "^2.13.3", "eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-vue": "^9.14.1", "eslint-plugin-vue": "^9.32.0",
"husky": "^8.0.0", "husky": "^8.0.0",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"prettier": "^2.8.8", "prettier": "^3.4.2",
"vitest": "^0.31.1" "sass": "^1.83.4",
"vitest": "^0.34.0"
}, },
"engines": { "engines": {
"node": "^20 || ^18 || ^16", "node": "^20 || ^18 || ^16",
@ -61,8 +63,8 @@
"bun": ">= 1.0.25" "bun": ">= 1.0.25"
}, },
"overrides": { "overrides": {
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.2.1",
"vite": "^5.1.4", "vite": "^6.0.11",
"vitest": "^0.31.1" "vitest": "^0.31.1"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,14 @@
/* eslint-disable */ /* eslint-disable */
// https://github.com/michael-ciniawsky/postcss-load-config // https://github.com/michael-ciniawsky/postcss-load-config
module.exports = { import autoprefixer from 'autoprefixer';
// Uncomment the following line if you want to support RTL CSS
// import rtlcss from 'postcss-rtlcss';
export default {
plugins: [ plugins: [
// https://github.com/postcss/autoprefixer // https://github.com/postcss/autoprefixer
require('autoprefixer')({ autoprefixer({
overrideBrowserslist: [ overrideBrowserslist: [
'last 4 Chrome versions', 'last 4 Chrome versions',
'last 4 Firefox versions', 'last 4 Firefox versions',
@ -18,10 +22,7 @@ module.exports = {
}), }),
// https://github.com/elchininet/postcss-rtlcss // https://github.com/elchininet/postcss-rtlcss
// If you want to support RTL css, then // If you want to support RTL CSS, uncomment the following line:
// 1. yarn/npm install postcss-rtlcss // rtlcss(),
// 2. optionally set quasar.config.js > framework > lang to an RTL language
// 3. uncomment the following line:
// require('postcss-rtlcss')
], ],
}; };

View File

@ -8,11 +8,11 @@
// Configuration for your app // Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
const { configure } = require('quasar/wrappers'); import { configure } from 'quasar/wrappers';
const VueI18nPlugin = require('@intlify/unplugin-vue-i18n/vite'); import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
const path = require('path'); import path from 'path';
module.exports = configure(function (/* ctx */) { export default configure(function (/* ctx */) {
return { return {
eslint: { eslint: {
// fix: true, // fix: true,

View File

@ -1,6 +1,8 @@
{ {
"@quasar/testing-unit-vitest": { "@quasar/testing-unit-vitest": {
"options": ["scripts"] "options": [
"scripts"
]
}, },
"@quasar/qcalendar": {} "@quasar/qcalendar": {}
} }

View File

@ -20,7 +20,7 @@ describe('Axios boot', () => {
describe('onRequest()', async () => { describe('onRequest()', async () => {
it('should set the "Authorization" property on the headers', async () => { it('should set the "Authorization" property on the headers', async () => {
const config = { headers: {} }; const config = { headers: {} };
localStorage.setItem('token', 'DEFAULT_TOKEN');
const resultConfig = onRequest(config); const resultConfig = onRequest(config);
expect(resultConfig).toEqual( expect(resultConfig).toEqual(

View File

@ -3,9 +3,9 @@ import { useSession } from 'src/composables/useSession';
import { Router } from 'src/router'; import { Router } from 'src/router';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useStateQueryStore } from 'src/stores/useStateQueryStore'; import { useStateQueryStore } from 'src/stores/useStateQueryStore';
import { getToken, isLoggedIn } from 'src/utils/session';
import { i18n } from 'src/boot/i18n'; import { i18n } from 'src/boot/i18n';
const session = useSession();
const { notify } = useNotify(); const { notify } = useNotify();
const stateQuery = useStateQueryStore(); const stateQuery = useStateQueryStore();
const baseUrl = '/api/'; const baseUrl = '/api/';
@ -13,7 +13,7 @@ axios.defaults.baseURL = baseUrl;
const axiosNoError = axios.create({ baseURL: baseUrl }); const axiosNoError = axios.create({ baseURL: baseUrl });
const onRequest = (config) => { const onRequest = (config) => {
const token = session.getToken(); const token = getToken();
if (token.length && !config.headers.Authorization) { if (token.length && !config.headers.Authorization) {
config.headers.Authorization = token; config.headers.Authorization = token;
config.headers['Accept-Language'] = i18n.global.locale.value; config.headers['Accept-Language'] = i18n.global.locale.value;
@ -37,15 +37,15 @@ const onResponse = (response) => {
return response; return response;
}; };
const onResponseError = (error) => { const onResponseError = async (error) => {
stateQuery.remove(error.config); stateQuery.remove(error.config);
if (session.isLoggedIn() && error.response?.status === 401) { if (isLoggedIn() && error.response?.status === 401) {
session.destroy(false); await useSession().destroy(false);
const hash = window.location.hash; const hash = window.location.hash;
const url = hash.slice(1); const url = hash.slice(1);
Router.push(`/login?redirect=${url}`); Router.push(`/login?redirect=${url}`);
} else if (!session.isLoggedIn()) { } else if (!isLoggedIn()) {
return Promise.reject(error); return Promise.reject(error);
} }

View File

@ -31,7 +31,7 @@ export default {
console.error(error); console.error(error);
} }
form.addEventListener('keyup', function (evt) { form.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter') { if (evt.key === 'Enter' && !that.$attrs['prevent-submit']) {
const input = evt.target; const input = evt.target;
if (input.type == 'textarea' && evt.shiftKey) { if (input.type == 'textarea' && evt.shiftKey) {
evt.preventDefault(); evt.preventDefault();

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { computed, nextTick, ref, watch } from 'vue'; import { computed, ref, nextTick, ref, useAttrs, watch } from 'vue';
import { useRouter, onBeforeRouteLeave } from 'vue-router'; import { useRouter, onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@ -17,6 +17,7 @@ const quasar = useQuasar();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator(); const { validate } = useValidator();
const $attrs = useAttrs();
const $props = defineProps({ const $props = defineProps({
model: { model: {
@ -114,9 +115,11 @@ onBeforeRouteLeave((to, from, next) => {
}); });
async function fetch(data) { async function fetch(data) {
resetData(data); const keyData = $attrs['key-data'];
emit('onFetch', data); const rows = keyData ? data[keyData] : data;
return data; resetData(rows);
emit('onFetch', rows);
return rows;
} }
function resetData(data) { function resetData(data) {

View File

@ -198,6 +198,7 @@ async function fetch() {
} catch (e) { } catch (e) {
state.set(modelValue, {}); state.set(modelValue, {});
originalData.value = {}; originalData.value = {};
throw e;
} }
} }

View File

@ -61,6 +61,7 @@ onMounted(() => stateStore.setMounted());
/> />
<QSpace /> <QSpace />
<div id="searchbar" class="searchbar"></div> <div id="searchbar" class="searchbar"></div>
<div id="searchbar-after"></div>
<QSpace /> <QSpace />
<div class="q-pl-sm q-gutter-sm row items-center no-wrap"> <div class="q-pl-sm q-gutter-sm row items-center no-wrap">
<div id="actions-prepend"></div> <div id="actions-prepend"></div>

View File

@ -44,7 +44,7 @@ const onDataSaved = (data) => {
<FormModelPopup <FormModelPopup
url-create="Items/regularize" url-create="Items/regularize"
model="Items" model="Items"
:title="t('Regularize stock')" :title="t('item.regularizeStock')"
:form-initial-data="regularizeFormData" :form-initial-data="regularizeFormData"
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved($event)"
> >

View File

@ -1,6 +1,9 @@
<script setup> <script setup>
import quasarLang from 'src/utils/quasarLang';
import { onMounted, computed, ref } from 'vue'; import { onMounted, computed, ref } from 'vue';
import { Dark, Quasar } from 'quasar';
import { Dark } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
@ -31,14 +34,7 @@ const userLocale = computed({
value = localeEquivalence[value] ?? value; value = localeEquivalence[value] ?? value;
try { quasarLang(value);
/* @vite-ignore */
import(`../../node_modules/quasar/lang/${value}.mjs`).then((lang) => {
Quasar.lang.set(lang.default);
});
} catch (error) {
//
}
}, },
}); });

View File

@ -181,7 +181,7 @@ onMounted(() => {
watch( watch(
() => $props.columns, () => $props.columns,
(value) => splitColumns(value), (value) => splitColumns(value),
{ immediate: true } { immediate: true },
); );
watch( watch(
@ -218,7 +218,7 @@ function splitColumns(columns) {
// Status column // Status column
if (splittedColumns.value.chips.length) { if (splittedColumns.value.chips.length) {
splittedColumns.value.columnChips = splittedColumns.value.chips.filter( splittedColumns.value.columnChips = splittedColumns.value.chips.filter(
(c) => !c.isId (c) => !c.isId,
); );
if (splittedColumns.value.columnChips.length) if (splittedColumns.value.columnChips.length)
splittedColumns.value.columns.unshift({ splittedColumns.value.columns.unshift({
@ -324,7 +324,15 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
:columns="columns" :columns="columns"
:redirect="redirect" :redirect="redirect"
/> >
<template
v-for="(_, slotName) in $slots"
#[slotName]="slotData"
:key="slotName"
>
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
</VnTableFilter>
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<CrudModel <CrudModel
@ -483,7 +491,9 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
btn.isPrimary ? 'text-primary-light' : 'color-vn-text ' btn.isPrimary ? 'text-primary-light' : 'color-vn-text '
" "
:style="`visibility: ${ :style="`visibility: ${
(btn.show && btn.show(row)) ?? true ? 'visible' : 'hidden' ((btn.show && btn.show(row)) ?? true)
? 'visible'
: 'hidden'
}`" }`"
@click="btn.action(row)" @click="btn.action(row)"
:disable="btn.disable ? btn.disable(row) : true" :disable="btn.disable ? btn.disable(row) : true"

View File

@ -62,5 +62,8 @@ function columnName(col) {
<span>{{ formatFn(tag.value) }}</span> <span>{{ formatFn(tag.value) }}</span>
</div> </div>
</template> </template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
</VnFilterPanel> </VnFilterPanel>
</template> </template>

View File

@ -0,0 +1,121 @@
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnVisibleColumn from '../VnVisibleColumn.vue';
import { axios } from 'app/test/vitest/helper';
describe('VnVisibleColumns', () => {
let wrapper;
let vm;
beforeEach(() => {
wrapper = createWrapper(VnVisibleColumn, {
propsData: {
tableCode: 'testTable',
skip: ['skippedColumn'],
},
});
vm = wrapper.vm;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('setUserConfigViewData()', () => {
it('should initialize localColumns with visible configuration', () => {
vm.columns = [
{ name: 'columnMockName', label: undefined },
{ name: 'columnMockAddress', label: undefined },
{ name: 'columnMockId', label: undefined },
];
const configuration = {
columnMockName: true,
columnMockAddress: false,
columnMockId: true,
};
const expectedColumns = [
{ name: 'columnMockName', label: undefined, visible: true },
{ name: 'columnMockAddress', label: undefined, visible: false },
{ name: 'columnMockId', label: undefined, visible: true },
];
vm.setUserConfigViewData(configuration, false);
expect(vm.localColumns).toEqual(expectedColumns);
});
it('should skip columns based on props', () => {
vm.columns = [
{ name: 'columnMockName', label: undefined },
{ name: 'columnMockId', label: undefined },
{ name: 'skippedColumn', label: 'Skipped Column' },
];
const configuration = {
columnMockName: true,
skippedColumn: false,
columnMockId: true,
};
const expectedColumns = [
{ name: 'columnMockName', label: undefined, visible: true },
{ name: 'columnMockId', label: undefined, visible: true },
];
vm.setUserConfigViewData(configuration, false);
expect(vm.localColumns).toEqual(expectedColumns);
});
});
describe('toggleMarkAll()', () => {
it('should set all localColumns to visible=true', () => {
vm.localColumns = [
{ name: 'columnMockName', visible: false },
{ name: 'columnMockId', visible: false },
];
vm.toggleMarkAll(true);
expect(vm.localColumns.every((col) => col.visible)).toBe(true);
});
it('should set all localColumns to visible=false', () => {
vm.localColumns = [
{ name: 'columnMockName', visible: true },
{ name: 'columnMockId', visible: true },
];
vm.toggleMarkAll(false);
expect(vm.localColumns.every((col) => col.visible)).toBe(false);
});
});
describe('saveConfig()', () => {
it('should call setUserConfigViewData and axios.post with correct params', async () => {
const mockAxiosPost = vi.spyOn(axios, 'post').mockResolvedValue({
data: [{ id: 1 }],
});
vm.localColumns = [
{ name: 'columnMockName', visible: true },
{ name: 'columnMockId', visible: false },
];
await vm.saveConfig();
expect(mockAxiosPost).toHaveBeenCalledWith('UserConfigViews/crud', {
creates: [
{
userFk: vm.user.id,
tableCode: vm.tableCode,
tableConfig: vm.tableCode,
configuration: {
columnMockName: true,
columnMockId: false,
},
},
],
});
});
});
});

View File

@ -0,0 +1,82 @@
import { createWrapper, axios } from 'app/test/vitest/helper';
import FilterItemForm from 'src/components/FilterItemForm.vue';
import { vi, beforeAll, describe, expect, it } from 'vitest';
describe('FilterItemForm', () => {
let vm;
let wrapper;
beforeAll(() => {
wrapper = createWrapper(FilterItemForm, {
props: {
url: 'Items/withName',
},
});
vm = wrapper.vm;
wrapper = wrapper.wrapper;
vi.spyOn(axios, 'get').mockResolvedValue({
data: [
{
id: 999996,
name: 'bolas de madera',
size: 2,
inkFk: null,
producerFk: null,
},
],
});
});
it('should filter data and populate tableRows for table display', async () => {
vm.itemFilterParams.name = 'bolas de madera';
await vm.onSubmit();
const expectedFilter = {
include: [
{ relation: 'producer', scope: { fields: ['name'] } },
{ relation: 'ink', scope: { fields: ['name'] } },
],
where: {"name":{"like":"%bolas de madera%"}},
};
expect(axios.get).toHaveBeenCalledWith('Items/withName', {
params: { filter: JSON.stringify(expectedFilter) },
});
expect(vm.tableRows).toEqual([
{
id: 999996,
name: 'bolas de madera',
size: 2,
inkFk: null,
producerFk: null,
},
]);
});
it('should handle an empty itemFilterParams correctly', async () => {
vm.itemFilterParams.name = null;
vm.itemFilterParams = {};
await vm.onSubmit();
const expectedFilter = {
include: [
{ relation: 'producer', scope: { fields: ['name'] } },
{ relation: 'ink', scope: { fields: ['name'] } },
],
where: {},
};
expect(axios.get).toHaveBeenCalledWith('Items/withName', {
params: { filter: JSON.stringify(expectedFilter) },
});
});
it('should emit "itemSelected" with the correct id and close the form', () => {
vm.selectItem({ id: 12345 });
expect(wrapper.emitted('itemSelected')[0]).toEqual([12345]);
});
});

View File

@ -0,0 +1,53 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useHasContent } from 'src/composables/useHasContent';
import { watch } from 'vue';
const { t } = useI18n();
const stateStore = useStateStore();
const hasContent = useHasContent('#advanced-menu');
const $props = defineProps({
isMainSection: {
type: Boolean,
default: false,
},
});
watch(
() => $props.isMainSection,
(val) => {
if (stateStore) stateStore.rightAdvancedDrawer = val;
},
{ immediate: true }
);
</script>
<template>
<Teleport to="#searchbar-after" v-if="stateStore.isHeaderMounted()">
<QBtn
v-if="hasContent || $slots['advanced-menu']"
flat
@click="stateStore.toggleRightAdvancedDrawer()"
round
dense
icon="tune"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.advancedMenu') }}
</QTooltip>
</QBtn>
</Teleport>
<QDrawer
v-model="stateStore.rightAdvancedDrawer"
side="right"
:width="256"
:overlay="!isMainSection"
v-bind="$attrs"
>
<QScrollArea class="fit">
<div id="advanced-menu"></div>
<slot v-if="!hasContent" name="advanced-menu" />
</QScrollArea>
</QDrawer>
</template>

View File

@ -1,35 +1,23 @@
<script setup> <script setup>
import { ref, onMounted, useSlots } from 'vue'; import { onMounted, useSlots } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useHasContent } from 'src/composables/useHasContent';
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const stateStore = useStateStore(); const stateStore = useStateStore();
const slots = useSlots(); const slots = useSlots();
const hasContent = ref(false); const hasContent = useHasContent('#right-panel');
const rightPanel = ref(null);
onMounted(() => { onMounted(() => {
rightPanel.value = document.querySelector('#right-panel');
if (!rightPanel.value) return;
const observer = new MutationObserver(() => {
hasContent.value = rightPanel.value.childNodes.length;
});
observer.observe(rightPanel.value, {
subtree: true,
childList: true,
attributes: true,
});
if ((!slots['right-panel'] && !hasContent.value) || quasar.platform.is.mobile) if ((!slots['right-panel'] && !hasContent.value) || quasar.platform.is.mobile)
stateStore.rightDrawer = false; stateStore.rightDrawer = false;
}); });
</script> </script>
<template> <template>
<Teleport to="#actions-append" v-if="stateStore.isHeaderMounted()"> <Teleport to="#actions-prepend" v-if="stateStore.isHeaderMounted()">
<div class="row q-gutter-x-sm"> <div class="row q-gutter-x-sm">
<QBtn <QBtn
v-if="hasContent || $slots['right-panel']" v-if="hasContent || $slots['right-panel']"

View File

@ -12,6 +12,7 @@ const props = defineProps({
baseUrl: { type: String, default: undefined }, baseUrl: { type: String, default: undefined },
customUrl: { type: String, default: undefined }, customUrl: { type: String, default: undefined },
filter: { type: Object, default: () => {} }, filter: { type: Object, default: () => {} },
userFilter: { type: Object, default: () => {} },
descriptor: { type: Object, required: true }, descriptor: { type: Object, required: true },
filterPanel: { type: Object, default: undefined }, filterPanel: { type: Object, default: undefined },
searchDataKey: { type: String, default: undefined }, searchDataKey: { type: String, default: undefined },
@ -32,6 +33,7 @@ const url = computed(() => {
const arrayData = useArrayData(props.dataKey, { const arrayData = useArrayData(props.dataKey, {
url: url.value, url: url.value,
filter: props.filter, filter: props.filter,
userFilter: props.userFilter,
}); });
onBeforeMount(async () => { onBeforeMount(async () => {

View File

@ -102,7 +102,7 @@ const columns = computed(() => [
storage: 'dms', storage: 'dms',
collection: null, collection: null,
resolution: null, resolution: null,
id: prop.row.file.split('.')[0], id: Number(prop.row.file.split('.')[0]),
token: token, token: token,
class: 'rounded', class: 'rounded',
ratio: 1, ratio: 1,
@ -299,11 +299,12 @@ defineExpose({
:url="$props.model" :url="$props.model"
:user-filter="dmsFilter" :user-filter="dmsFilter"
:order="['dmsFk DESC']" :order="['dmsFk DESC']"
:auto-load="true" auto-load
@on-fetch="setData" @on-fetch="setData"
> >
<template #body> <template #body>
<QTable <QTable
v-if="rows"
:columns="columns" :columns="columns"
:rows="rows" :rows="rows"
class="full-width q-mt-md" class="full-width q-mt-md"

View File

@ -42,10 +42,13 @@ const $props = defineProps({
type: Number, type: Number,
default: null, default: null,
}, },
uppercase: {
type: Boolean,
default: false,
},
}); });
const vnInputRef = ref(null); const vnInputRef = ref(null);
const showPassword = ref(false);
const value = computed({ const value = computed({
get() { get() {
return $props.modelValue; return $props.modelValue;
@ -117,6 +120,10 @@ const handleInsertMode = (e) => {
input.setSelectionRange(cursorPos + 1, cursorPos + 1); input.setSelectionRange(cursorPos + 1, cursorPos + 1);
}); });
}; };
const handleUppercase = () => {
value.value = value.value?.toUpperCase() || '';
};
</script> </script>
<template> <template>
@ -159,7 +166,16 @@ const handleInsertMode = (e) => {
emit('remove'); emit('remove');
} }
" "
></QIcon>
<QIcon
name="match_case"
size="xs"
v-if="!$attrs.disabled && !($attrs.readonly) && $props.uppercase"
@click="handleUppercase"
class="uppercase-icon"
/> />
<slot name="append" v-if="$slots.append && !$attrs.disabled" /> <slot name="append" v-if="$slots.append && !$attrs.disabled" />
<QIcon v-if="info" name="info"> <QIcon v-if="info" name="info">
<QTooltip max-width="350px"> <QTooltip max-width="350px">
@ -170,3 +186,14 @@ const handleInsertMode = (e) => {
</QInput> </QInput>
</div> </div>
</template> </template>
<i18n>
en:
inputMin: Must be more than {value}
maxLength: The value exceeds {value} characters
inputMax: Must be less than {value}
es:
inputMin: Debe ser mayor a {value}
maxLength: El valor excede los {value} carácteres
inputMax: Debe ser menor a {value}
</i18n>

View File

@ -105,6 +105,7 @@ const manageDate = (date) => {
:rules="mixinRules" :rules="mixinRules"
:clearable="false" :clearable="false"
@click="isPopupOpen = !isPopupOpen" @click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
hide-bottom-space hide-bottom-space
> >
<template #append> <template #append>

View File

@ -79,6 +79,7 @@ function dateToTime(newDate) {
style="min-width: 100px" style="min-width: 100px"
:rules="mixinRules" :rules="mixinRules"
@click="isPopupOpen = !isPopupOpen" @click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
type="time" type="time"
hide-bottom-space hide-bottom-space
> >

View File

@ -2,7 +2,7 @@
import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue'; import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import VnSelectDialog from 'components/common/VnSelectDialog.vue'; import VnSelectDialog from 'components/common/VnSelectDialog.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed } from 'vue'; import { ref, watch } from 'vue';
import { useAttrs } from 'vue'; import { useAttrs } from 'vue';
import { useRequired } from 'src/composables/useRequired'; import { useRequired } from 'src/composables/useRequired';
const { t } = useI18n(); const { t } = useI18n();
@ -16,6 +16,14 @@ const props = defineProps({
}, },
}); });
watch(
() => props.location,
(newValue) => {
if (!modelValue.value) return;
modelValue.value = formatLocation(newValue) ?? null;
}
);
const mixinRules = [requiredFieldRule]; const mixinRules = [requiredFieldRule];
const locationProperties = [ const locationProperties = [
'postcode', 'postcode',
@ -43,9 +51,7 @@ const formatLocation = (obj, properties = locationProperties) => {
return filteredParts.join(', '); return filteredParts.join(', ');
}; };
const modelValue = computed(() => const modelValue = ref(props.location ? formatLocation(props.location) : null);
props.location ? formatLocation(props.location, locationProperties) : null
);
function showLabel(data) { function showLabel(data) {
const dataProperties = [ const dataProperties = [

View File

@ -15,6 +15,7 @@ import FetchData from '../FetchData.vue';
import VnSelect from './VnSelect.vue'; import VnSelect from './VnSelect.vue';
import VnUserLink from '../ui/VnUserLink.vue'; import VnUserLink from '../ui/VnUserLink.vue';
import VnPaginate from '../ui/VnPaginate.vue'; import VnPaginate from '../ui/VnPaginate.vue';
import RightMenu from './RightMenu.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const validationsStore = useValidator(); const validationsStore = useValidator();
@ -130,7 +131,7 @@ const actionsIcon = {
}; };
const validDate = new RegExp( const validDate = new RegExp(
/^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])/.source + /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])/.source +
/T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/.source /T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/.source,
); );
function castJsonValue(value) { function castJsonValue(value) {
@ -192,7 +193,7 @@ function getLogTree(data) {
user: log.user, user: log.user,
userFk: log.userFk, userFk: log.userFk,
logs: [], logs: [],
}) }),
); );
} }
// Model // Model
@ -210,7 +211,7 @@ function getLogTree(data) {
id: log.changedModelId, id: log.changedModelId,
showValue: log.changedModelValue, showValue: log.changedModelValue,
logs: [], logs: [],
}) }),
); );
nLogs = 0; nLogs = 0;
} }
@ -282,7 +283,7 @@ function setDate(type) {
to = date.adjustDate( to = date.adjustDate(
to, to,
{ hour: 21, minute: 59, second: 59, millisecond: 999 }, { hour: 21, minute: 59, second: 59, millisecond: 999 },
true true,
); );
switch (type) { switch (type) {
@ -365,7 +366,7 @@ async function clearFilter() {
dateTo.value = undefined; dateTo.value = undefined;
userRadio.value = undefined; userRadio.value = undefined;
Object.keys(checkboxOptions.value).forEach( Object.keys(checkboxOptions.value).forEach(
(opt) => (checkboxOptions.value[opt].selected = false) (opt) => (checkboxOptions.value[opt].selected = false),
); );
await applyFilter(); await applyFilter();
} }
@ -378,7 +379,7 @@ watch(
() => router.currentRoute.value.params.id, () => router.currentRoute.value.params.id,
() => { () => {
applyFilter(); applyFilter();
} },
); );
</script> </script>
<template> <template>
@ -391,7 +392,7 @@ watch(
const changedModel = item.changedModel; const changedModel = item.changedModel;
return { return {
locale: useCapitalize( locale: useCapitalize(
validations[changedModel]?.locale?.name ?? changedModel validations[changedModel]?.locale?.name ?? changedModel,
), ),
value: changedModel, value: changedModel,
}; };
@ -507,7 +508,7 @@ watch(
:title=" :title="
date.formatDate( date.formatDate(
log.creationDate, log.creationDate,
'DD/MM/YYYY hh:mm:ss' 'DD/MM/YYYY hh:mm:ss',
) ?? `date:'dd/MM/yyyy HH:mm:ss'` ) ?? `date:'dd/MM/yyyy HH:mm:ss'`
" "
> >
@ -577,7 +578,7 @@ watch(
t( t(
`actions.${ `actions.${
actionsText[log.action] actionsText[log.action]
}` }`,
) )
" "
/> />
@ -677,139 +678,144 @@ watch(
</div> </div>
</template> </template>
</VnPaginate> </VnPaginate>
<Teleport to="#right-panel" v-if="stateStore.isHeaderMounted()"> <RightMenu>
<QList dense> <template #right-panel>
<QSeparator /> <QList dense>
<QItem class="q-mt-sm"> <QSeparator />
<QInput <QItem class="q-mt-sm">
:label="t('globals.search')" <QInput
v-model="searchInput" :label="t('globals.search')"
class="full-width" v-model="searchInput"
clearable class="full-width"
clear-icon="close" clearable
@keyup.enter="() => selectFilter('search')" clear-icon="close"
@focusout="() => selectFilter('search')" @keyup.enter="() => selectFilter('search')"
@clear="() => selectFilter('search')" @focusout="() => selectFilter('search')"
> @clear="() => selectFilter('search')"
<template #append> >
<QIcon name="info" class="cursor-pointer"> <template #append>
<QTooltip>{{ t('tooltips.search') }}</QTooltip> <QIcon name="info" class="cursor-pointer">
</QIcon> <QTooltip>{{ t('tooltips.search') }}</QTooltip>
</template> </QIcon>
</QInput> </template>
</QItem> </QInput>
<QItem> </QItem>
<VnSelect <QItem>
class="full-width"
:label="t('globals.entity')"
v-model="selectedFilters.changedModel"
option-label="locale"
option-value="value"
:options="actions"
@update:model-value="selectFilter('action')"
hide-selected
/>
</QItem>
<QItem class="q-mt-sm">
<QOptionGroup
size="sm"
v-model="userRadio"
:options="userTypes"
color="primary"
@update:model-value="selectFilter('userRadio')"
right-label
>
<template #label="{ label }">
{{ t(`Users.${label}`) }}
</template>
</QOptionGroup>
</QItem>
<QItem class="q-mt-sm">
<QItemSection v-if="userRadio !== null">
<VnSelect <VnSelect
class="full-width" class="full-width"
:label="t('globals.user')" :label="t('globals.entity')"
v-model="userSelect" v-model="selectedFilters.changedModel"
option-label="name" option-label="locale"
option-value="id" option-value="value"
:url="`${model}Logs/${$route.params.id}/editors`" :options="actions"
:fields="['id', 'nickname', 'name', 'image']" @update:model-value="selectFilter('action')"
sort-by="nickname"
@update:model-value="selectFilter('userSelect')"
hide-selected hide-selected
/>
</QItem>
<QItem class="q-mt-sm">
<QOptionGroup
size="sm"
v-model="userRadio"
:options="userTypes"
color="primary"
@update:model-value="selectFilter('userRadio')"
right-label
> >
<template #option="{ opt, itemProps }"> <template #label="{ label }">
<QItem v-bind="itemProps" class="q-pa-xs row items-center"> {{ t(`Users.${label}`) }}
<QItemSection class="col-3 items-center">
<VnAvatar :worker-id="opt.id" />
</QItemSection>
<QItemSection class="col-9 justify-center">
<span>{{ opt.name }}</span>
<span class="text-grey">{{ opt.nickname }}</span>
</QItemSection>
</QItem>
</template> </template>
</VnSelect> </QOptionGroup>
</QItemSection> </QItem>
</QItem> <QItem class="q-mt-sm">
<QItem class="q-mt-sm"> <QItemSection v-if="userRadio !== null">
<QInput <VnSelect
:label="t('globals.changes')" class="full-width"
v-model="changeInput" :label="t('globals.user')"
class="full-width" v-model="userSelect"
clearable option-label="name"
clear-icon="close" option-value="id"
@keyup.enter="selectFilter('change')" :url="`${model}Logs/${route.params.id}/editors`"
@focusout="selectFilter('change')" :fields="['id', 'nickname', 'name', 'image']"
@clear="selectFilter('change')" sort-by="nickname"
@update:model-value="selectFilter('userSelect')"
hide-selected
>
<template #option="{ opt, itemProps }">
<QItem
v-bind="itemProps"
class="q-pa-xs row items-center"
>
<QItemSection class="col-3 items-center">
<VnAvatar :worker-id="opt.id" />
</QItemSection>
<QItemSection class="col-9 justify-center">
<span>{{ opt.name }}</span>
<span class="text-grey">{{ opt.nickname }}</span>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem class="q-mt-sm">
<QInput
:label="t('globals.changes')"
v-model="changeInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="selectFilter('change')"
@focusout="selectFilter('change')"
@clear="selectFilter('change')"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip max-width="250px">{{
t('tooltips.changes')
}}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
<QItem
:class="index == 'create' ? 'q-mt-md' : 'q-mt-xs'"
v-for="(checkboxOption, index) in checkboxOptions"
:key="index"
> >
<template #append> <QCheckbox
<QIcon name="info" class="cursor-pointer"> size="sm"
<QTooltip max-width="250px">{{ v-model="checkboxOption.selected"
t('tooltips.changes') :label="t(`actions.${checkboxOption.label}`)"
}}</QTooltip> @update:model-value="selectFilter"
</QIcon> />
</template> </QItem>
</QInput> <QItem class="q-mt-sm">
</QItem> <QInput
<QItem class="full-width"
:class="index == 'create' ? 'q-mt-md' : 'q-mt-xs'" :label="t('globals.date')"
v-for="(checkboxOption, index) in checkboxOptions" @click="dateFromDialog = true"
:key="index" @focus="(evt) => evt.target.blur()"
> @clear="selectFilter('date', 'to')"
<QCheckbox v-model="dateFrom"
size="sm" clearable
v-model="checkboxOption.selected" clear-icon="close"
:label="t(`actions.${checkboxOption.label}`)" />
@update:model-value="selectFilter" </QItem>
/> <QItem class="q-mt-sm">
</QItem> <QInput
<QItem class="q-mt-sm"> class="full-width"
<QInput :label="t('to')"
class="full-width" @click="dateToDialog = true"
:label="t('globals.date')" @focus="(evt) => evt.target.blur()"
@click="dateFromDialog = true" @clear="selectFilter('date', 'from')"
@focus="(evt) => evt.target.blur()" v-model="dateTo"
@clear="selectFilter('date', 'to')" clearable
v-model="dateFrom" clear-icon="close"
clearable />
clear-icon="close" </QItem>
/> </QList>
</QItem> </template>
<QItem class="q-mt-sm"> </RightMenu>
<QInput
class="full-width"
:label="t('to')"
@click="dateToDialog = true"
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'from')"
v-model="dateTo"
clearable
clear-icon="close"
/>
</QItem>
</QList>
</Teleport>
<QDialog v-model="dateFromDialog"> <QDialog v-model="dateFromDialog">
<QDate <QDate
:years-in-month-view="false" :years-in-month-view="false"

View File

@ -1,10 +1,11 @@
<script setup> <script setup>
import RightMenu from './RightMenu.vue'; import RightAdvancedMenu from './RightAdvancedMenu.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnTableFilter from '../VnTable/VnTableFilter.vue'; import VnTableFilter from '../VnTable/VnTableFilter.vue';
import { onBeforeMount, computed } from 'vue'; import { onBeforeMount, onMounted, onUnmounted, computed, ref } from 'vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useHasContent } from 'src/composables/useHasContent';
const $props = defineProps({ const $props = defineProps({
section: { section: {
@ -46,15 +47,15 @@ const $props = defineProps({
}); });
const route = useRoute(); const route = useRoute();
const router = useRouter();
let arrayData; let arrayData;
const sectionValue = computed(() => $props.section ?? $props.dataKey); const sectionValue = computed(() => $props.section ?? $props.dataKey);
const isMainSection = computed(() => { const isMainSection = ref(false);
const isSame = sectionValue.value == route.name;
if (!isSame && arrayData) { const searchbarId = 'section-searchbar';
arrayData.reset(['userParams', 'userFilter']); const advancedMenuSlot = 'advanced-menu';
} const hasContent = useHasContent(`#${searchbarId}`);
return isSame;
});
onBeforeMount(() => { onBeforeMount(() => {
if ($props.dataKey) if ($props.dataKey)
@ -64,22 +65,38 @@ onBeforeMount(() => {
...$props.arrayDataProps, ...$props.arrayDataProps,
navigate: $props.redirect, navigate: $props.redirect,
}); });
checkIsMain();
}); });
onMounted(() => {
const unsubscribe = router.afterEach(() => {
checkIsMain();
});
onUnmounted(unsubscribe);
});
function checkIsMain() {
isMainSection.value = sectionValue.value == route.name;
if (!isMainSection.value && arrayData) {
arrayData.reset(['userParams', 'filter']);
arrayData.setCurrentFilter();
}
}
</script> </script>
<template> <template>
<slot name="searchbar"> <slot name="searchbar">
<VnSearchbar <VnSearchbar
v-if="searchBar" v-if="searchBar && !hasContent"
v-bind="arrayDataProps" v-bind="arrayDataProps"
:data-key="dataKey" :data-key="dataKey"
:label="$t(`${prefix}.search`)" :label="$t(`${prefix}.search`)"
:info="$t(`${prefix}.searchInfo`)" :info="$t(`${prefix}.searchInfo`)"
/> />
<div :id="searchbarId"></div>
</slot> </slot>
<RightAdvancedMenu :is-main-section="isMainSection">
<RightMenu> <template #advanced-menu v-if="$slots[advancedMenuSlot] || rightFilter">
<template #right-panel v-if="$slots['rightMenu'] || rightFilter"> <slot :name="advancedMenuSlot">
<slot name="rightMenu">
<VnTableFilter <VnTableFilter
v-if="rightFilter && columns" v-if="rightFilter && columns"
:data-key="dataKey" :data-key="dataKey"
@ -88,7 +105,7 @@ onBeforeMount(() => {
/> />
</slot> </slot>
</template> </template>
</RightMenu> </RightAdvancedMenu>
<slot name="body" v-if="isMainSection" /> <slot name="body" v-if="isMainSection" />
<RouterView v-else /> <RouterView v-else />
</template> </template>

View File

@ -27,7 +27,7 @@ const $props = defineProps({
default: () => [], default: () => [],
}, },
optionLabel: { optionLabel: {
type: [String], type: [String, Function],
default: 'name', default: 'name',
}, },
optionValue: { optionValue: {
@ -232,12 +232,15 @@ async function fetchFilter(val) {
} else defaultWhere = { [key]: getVal(val) }; } else defaultWhere = { [key]: getVal(val) };
const where = { ...(val ? defaultWhere : {}), ...$props.where }; const where = { ...(val ? defaultWhere : {}), ...$props.where };
$props.exprBuilder && Object.assign(where, $props.exprBuilder(key, val)); $props.exprBuilder && Object.assign(where, $props.exprBuilder(key, val));
const fetchOptions = { where, include, limit }; const filterOptions = { where, include, limit };
if (fields) fetchOptions.fields = fields; if (fields) filterOptions.fields = fields;
if (sortBy) fetchOptions.order = sortBy; if (sortBy) filterOptions.order = sortBy;
arrayData.resetPagination(); arrayData.resetPagination();
const { data } = await arrayData.applyFilter({ filter: fetchOptions }); const { data } = await arrayData.applyFilter(
{ filter: filterOptions },
{ updateRouter: false }
);
setOptions(data); setOptions(data);
return data; return data;
} }
@ -294,7 +297,7 @@ async function onScroll({ to, direction, from, index }) {
} }
} }
defineExpose({ opts: myOptions }); defineExpose({ opts: myOptions, vnSelectRef });
function handleKeyDown(event) { function handleKeyDown(event) {
if (event.key === 'Tab' && !event.shiftKey) { if (event.key === 'Tab' && !event.shiftKey) {

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed } from 'vue'; import { ref, computed } from 'vue';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import { useAcl } from 'src/composables/useAcl'; import { useAcl } from 'src/composables/useAcl';
@ -7,6 +7,7 @@ import VnSelect from 'src/components/common/VnSelect.vue';
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const value = defineModel({ type: [String, Number, Object] }); const value = defineModel({ type: [String, Number, Object] });
const select = ref(null);
const $props = defineProps({ const $props = defineProps({
rolesAllowedToCreate: { rolesAllowedToCreate: {
type: Array, type: Array,
@ -33,10 +34,13 @@ const isAllowedToCreate = computed(() => {
if ($props.acls.length) return acl.hasAny($props.acls); if ($props.acls.length) return acl.hasAny($props.acls);
return role.hasAny($props.rolesAllowedToCreate); return role.hasAny($props.rolesAllowedToCreate);
}); });
defineExpose({ vnSelectDialogRef: select });
</script> </script>
<template> <template>
<VnSelect <VnSelect
ref="select"
v-model="value" v-model="value"
v-bind="$attrs" v-bind="$attrs"
@update:model-value="(...args) => emit('update:modelValue', ...args)" @update:model-value="(...args) => emit('update:modelValue', ...args)"

View File

@ -55,7 +55,7 @@ const url = computed(() => {
sort-by="nickname ASC" sort-by="nickname ASC"
> >
<template #prepend v-if="$props.hasAvatar"> <template #prepend v-if="$props.hasAvatar">
<VnAvatar :worker-id="value" color="primary" :title="title" /> <VnAvatar :worker-id="value" color="primary" v-bind="$attrs" />
</template> </template>
<template #append v-if="$props.hasInfo"> <template #append v-if="$props.hasInfo">
<QIcon name="info" class="cursor-pointer"> <QIcon name="info" class="cursor-pointer">
@ -72,7 +72,8 @@ const url = computed(() => {
{{ scope.opt.nickname }} {{ scope.opt.nickname }}
</QItemLabel> </QItemLabel>
<QItemLabel caption v-else> <QItemLabel caption v-else>
#{{ scope.opt.id }}, {{ scope.opt.nickname }}, {{ scope.opt.code }} #{{ scope.opt.id }}, {{ scope.opt.nickname }},
{{ scope.opt.code }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>

View File

@ -0,0 +1,146 @@
import { createWrapper, axios } from 'app/test/vitest/helper';
import { vi, afterEach, beforeEach, beforeAll, describe, expect, it } from 'vitest';
import VnDms from 'src/components/common/VnDms.vue';
class MockFormData {
constructor() {
this.entries = {};
}
append(key, value) {
if (!key) {
throw new Error('Key is required for FormData.append');
}
this.entries[key] = value;
}
get(key) {
return this.entries[key] || null;
}
getAll() {
return this.entries;
}
}
global.FormData = MockFormData;
describe('VnDms', () => {
let wrapper;
let vm;
let postMock;
const postResponseMock = { data: { success: true } };
const data = {
hasFile: true,
hasFileAttached: true,
reference: 'DMS-test',
warehouseFk: 1,
companyFk: 2,
dmsTypeFk: 3,
description: 'This is a test description',
files: { name: 'example.txt', content: new Blob(['file content'], { type: 'text/plain' })},
};
const expectedBody = {
hasFile: true,
hasFileAttached: true,
reference: 'DMS-test',
warehouseId: 1,
companyId: 2,
dmsTypeId: 3,
description: 'This is a test description',
};
beforeAll(() => {
wrapper = createWrapper(VnDms, {
propsData: {
url: '/test',
formInitialData: { id: 1, reference: 'test' },
model: 'Worker',
}
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
vi.spyOn(vm, '$emit');
});
beforeEach(() => {
postMock = vi.spyOn(axios, 'post').mockResolvedValue(postResponseMock);
vm.dms = data;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('mapperDms', () => {
it('should map DMS data correctly and add file to FormData', () => {
const [formData, params] = vm.mapperDms(data);
expect(formData.get('example.txt')).toBe(data.files);
expect(expectedBody).toEqual(params.params);
});
it('should map DMS data correctly without file', () => {
delete data.files;
const [formData, params] = vm.mapperDms(data);
expect(formData.getAll()).toEqual({});
expect(expectedBody).toEqual(params.params);
});
});
describe('getUrl', () => {
it('should returns prop url when is set', async () => {
expect(vm.getUrl()).toBe('/test');
});
it('should returns url dms/"props.formInitialData.id"/updateFile when prop url is null', async () => {
await wrapper.setProps({ url: null });
expect(vm.getUrl()).toBe('dms/1/updateFile');
});
it('should returns url "props.model"/"route.params.id"/uploadFile when formInitialData is null', async () => {
await wrapper.setProps({ formInitialData: null });
vm.route.params.id = '123';
expect(vm.getUrl()).toBe('Worker/123/uploadFile');
});
});
describe('save', () => {
it('should save data correctly', async () => {
await vm.save();
expect(postMock).toHaveBeenCalledWith(vm.getUrl(), expect.any(FormData), { params: expectedBody });
expect(wrapper.emitted('onDataSaved')).toBeTruthy();
});
});
describe('defaultData', () => {
it('should set dms with formInitialData', async () => {
const testData = {
hasFile: false,
hasFileAttached: false,
reference: 'defaultData-test',
warehouseFk: 2,
companyFk: 3,
dmsTypeFk: 2,
description: 'This is a test description'
}
await wrapper.setProps({ formInitialData: testData });
vm.defaultData();
expect(vm.dms).toEqual(testData);
});
it('should add reference with "route.params.id" to dms if formInitialData is null', async () => {
await wrapper.setProps({ formInitialData: null });
vm.route.params.id= '111';
vm.defaultData();
expect(vm.dms.reference).toBe('111');
});
});
});

View File

@ -0,0 +1,91 @@
import { createWrapper } from 'app/test/vitest/helper';
import { vi, describe, expect, it } from 'vitest';
import VnInput from 'src/components/common/VnInput.vue';
describe('VnInput', () => {
let vm;
let wrapper;
let input;
function generateWrapper(value, isOutlined, emptyToNull, insertable) {
wrapper = createWrapper(VnInput, {
props: {
modelValue: value,
isOutlined, emptyToNull, insertable,
maxlength: 101
},
attrs: {
label: 'test',
required: true,
maxlength: 101,
maxLength: 10,
'max-length':20
},
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
input = wrapper.find('[data-cy="test_input"]');
};
describe('value', () => {
it('should emit update:modelValue when value changes', async () => {
generateWrapper('12345', false, false, true)
await input.setValue('123');
expect(wrapper.emitted('update:modelValue')).toBeTruthy();
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['123']);
});
it('should emit update:modelValue with null when input is empty', async () => {
generateWrapper('12345', false, true, true);
await input.setValue('');
expect(wrapper.emitted('update:modelValue')[0]).toEqual([null]);
});
});
describe('styleAttrs', () => {
it('should return empty styleAttrs when isOutlined is false', async () => {
generateWrapper('123', false, false, false);
expect(vm.styleAttrs).toEqual({});
});
it('should set styleAttrs when isOutlined is true', async () => {
generateWrapper('123', true, false, false);
expect(vm.styleAttrs.outlined).toBe(true);
});
});
describe('handleKeydown', () => {
it('should do nothing when "Backspace" key is pressed', async () => {
generateWrapper('12345', false, false, true);
await input.trigger('keydown', { key: 'Backspace' });
expect(wrapper.emitted('update:modelValue')).toBeUndefined();
const spyhandler = vi.spyOn(vm, 'handleInsertMode');
expect(spyhandler).not.toHaveBeenCalled();
});
/*
TODO: #8399 REDMINE
*/
it.skip('handleKeydown respects insertable behavior', async () => {
const expectedValue = '12345';
generateWrapper('1234', false, false, true);
vm.focus()
await input.trigger('keydown', { key: '5' });
await vm.$nextTick();
expect(wrapper.emitted('update:modelValue')).toBeTruthy();
expect(wrapper.emitted('update:modelValue')[0]).toEqual([expectedValue ]);
expect(vm.value).toBe( expectedValue);
});
});
describe('focus', () => {
it('should call focus method when input is focused', async () => {
generateWrapper('123', false, false, true);
const focusSpy = vi.spyOn(input.element, 'focus');
vm.focus();
expect(focusSpy).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,72 @@
import { createWrapper } from 'app/test/vitest/helper.js';
import { describe, it, expect } from 'vitest';
import VnInputDate from 'components/common/VnInputDate.vue';
let vm;
let wrapper;
function generateWrapper(date, outlined, required) {
wrapper = createWrapper(VnInputDate, {
props: {
modelValue: date,
},
attrs: {
isOutlined: outlined,
required: required
},
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
};
describe('VnInputDate', () => {
describe('formattedDate', () => {
it('formats a valid date correctly', async () => {
generateWrapper('2023-12-25', false, false);
await vm.$nextTick();
expect(vm.formattedDate).toBe('25/12/2023');
});
it('updates the model value when a new date is set', async () => {
const input = wrapper.find('input');
await input.setValue('31/12/2023');
expect(wrapper.emitted()['update:modelValue']).toBeTruthy();
expect(wrapper.emitted()['update:modelValue'][0][0]).toBe('2023-12-31T00:00:00.000Z');
});
it('should not update the model value when an invalid date is set', async () => {
const input = wrapper.find('input');
await input.setValue('invalid-date');
expect(wrapper.emitted()['update:modelValue'][0][0]).toBe('2023-12-31T00:00:00.000Z');
});
});
describe('styleAttrs', () => {
it('should return empty styleAttrs when isOutlined is false', async () => {
generateWrapper('2023-12-25', false, false);
await vm.$nextTick();
expect(vm.styleAttrs).toEqual({});
});
it('should set styleAttrs when isOutlined is true', async () => {
generateWrapper('2023-12-25', true, false);
await vm.$nextTick();
expect(vm.styleAttrs.outlined).toBe(true);
});
});
describe('required', () => {
it('should not applies required class when isRequired is false', async () => {
generateWrapper('2023-12-25', false, false);
await vm.$nextTick();
expect(wrapper.find('.vn-input-date').classes()).not.toContain('required');
});
it('should applies required class when isRequired is true', async () => {
generateWrapper('2023-12-25', false, true);
await vm.$nextTick();
expect(wrapper.find('.vn-input-date').classes()).toContain('required');
});
});
});

View File

@ -0,0 +1,63 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnInputTime from 'components/common/VnInputTime.vue';
describe('VnInputTime', () => {
let wrapper;
let vm;
beforeAll(() => {
wrapper = createWrapper(VnInputTime, {
props: {
isOutlined: true,
timeOnly: false,
},
});
vm = wrapper.vm;
wrapper = wrapper.wrapper;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should return the correct data if isOutlined is true', () => {
expect(vm.isOutlined).toBe(true);
expect(vm.styleAttrs).toEqual({ dense: true, outlined: true, rounded: true });
});
it('should return the formatted data', () => {
expect(vm.dateToTime('2022-01-01T03:23:43')).toBe('03:23');
});
describe('formattedTime', () => {
it('should return the formatted time for a valid ISO date', () => {
vm.model = '2025-01-02T15:45:00';
expect(vm.formattedTime).toBe('15:45');
});
it('should handle null model value gracefully', () => {
vm.model = null;
expect(vm.formattedTime).toBe(null);
});
it('should handle time-only input correctly', async () => {
await wrapper.setProps({ timeOnly: true });
vm.formattedTime = '14:30';
expect(vm.model).toBe('14:30');
});
it('should pad short time values correctly', async () => {
await wrapper.setProps({ timeOnly: true });
vm.formattedTime = '9';
expect(vm.model).toBe('09:00');
});
it('should not update the model if the value is unchanged', () => {
vm.model = '14:30';
const previousModel = vm.model;
vm.formattedTime = '14:30';
expect(vm.model).toBe(previousModel);
});
});
});

View File

@ -6,6 +6,7 @@ import { useArrayData } from 'composables/useArrayData';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnMoreOptions from './VnMoreOptions.vue';
const $props = defineProps({ const $props = defineProps({
url: { url: {
@ -47,7 +48,6 @@ let store;
let entity; let entity;
const isLoading = ref(false); const isLoading = ref(false);
const isSameDataKey = computed(() => $props.dataKey === route.meta.moduleName); const isSameDataKey = computed(() => $props.dataKey === route.meta.moduleName);
const menuRef = ref();
defineExpose({ getData }); defineExpose({ getData });
onBeforeMount(async () => { onBeforeMount(async () => {
@ -159,25 +159,11 @@ const toModule = computed(() =>
</QTooltip> </QTooltip>
</QBtn> </QBtn>
</RouterLink> </RouterLink>
<QBtn <VnMoreOptions v-if="$slots.menu">
v-if="$slots.menu" <template #menu="{ menuRef }">
color="white" <slot name="menu" :entity="entity" :menu-ref="menuRef" />
dense </template>
flat </VnMoreOptions>
icon="more_vert"
round
size="md"
data-cy="descriptor-more-opts"
>
<QTooltip>
{{ t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu :ref="menuRef">
<QList>
<slot name="menu" :entity="entity" :menu-ref="menuRef" />
</QList>
</QMenu>
</QBtn>
</div> </div>
<slot name="before" /> <slot name="before" />
<div class="body q-py-sm"> <div class="body q-py-sm">
@ -222,8 +208,8 @@ const toModule = computed(() =>
/> />
</template> </template>
<style lang="scss" scoped> <style lang="scss">
:deep(.body) { .body {
background-color: var(--vn-section-color); background-color: var(--vn-section-color);
.text-h5 { .text-h5 {
font-size: 20px; font-size: 20px;
@ -262,7 +248,9 @@ const toModule = computed(() =>
} }
} }
} }
</style>
<style lang="scss" scoped>
.title { .title {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -1,10 +1,10 @@
<script setup> <script setup>
import { ref, computed, watch, onBeforeMount, onMounted } from 'vue'; import { ref, computed, watch, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue'; import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { isDialogOpened } from 'src/filters'; import { isDialogOpened } from 'src/filters';
import { useStateStore } from 'src/stores/useStateStore'; import VnMoreOptions from './VnMoreOptions.vue';
const props = defineProps({ const props = defineProps({
url: { url: {
@ -40,7 +40,6 @@ const { store } = arrayData;
const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data)); const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
const isLoading = ref(false); const isLoading = ref(false);
const stateStore = useStateStore();
defineExpose({ defineExpose({
entity, entity,
fetch, fetch,
@ -52,9 +51,6 @@ onBeforeMount(async () => {
watch(props, async () => await fetch()); watch(props, async () => await fetch());
}); });
onMounted(() => {
stateStore.rightDrawerChangeValue(false);
});
async function fetch() { async function fetch() {
store.url = props.url; store.url = props.url;
store.filter = props.filter ?? {}; store.filter = props.filter ?? {};
@ -64,6 +60,7 @@ async function fetch() {
isLoading.value = false; isLoading.value = false;
} }
</script> </script>
<template> <template>
<div class="summary container"> <div class="summary container">
<QCard class="cardSummary"> <QCard class="cardSummary">
@ -86,9 +83,14 @@ async function fetch() {
<slot name="header" :entity="entity" dense> <slot name="header" :entity="entity" dense>
{{ entity.id + ' - ' + entity.name }} {{ entity.id + ' - ' + entity.name }}
</slot> </slot>
<slot name="header-right" :entity="entity"> <span class="row no-wrap">
<span></span> <slot name="header-right" :entity="entity" />
</slot> <VnMoreOptions v-if="$slots.menu && isDialogOpened()">
<template #menu="{ menuRef }">
<slot name="menu" :entity="entity" :menu-ref="menuRef" />
</template>
</VnMoreOptions>
</span>
</div> </div>
<div class="summaryBody row q-mb-md"> <div class="summaryBody row q-mb-md">
<slot name="body" :entity="entity" /> <slot name="body" :entity="entity" />
@ -97,6 +99,7 @@ async function fetch() {
</QCard> </QCard>
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
.summary.container { .summary.container {
display: flex; display: flex;
@ -172,7 +175,7 @@ async function fetch() {
display: inline-block; display: inline-block;
} }
.header.link:hover { .header.link:hover {
color: lighten($primary, 20%); color: rgba(var(--q-primary), 0.8);
} }
.q-checkbox { .q-checkbox {
& .q-checkbox__label { & .q-checkbox__label {

View File

@ -41,7 +41,7 @@ const card = toRef(props, 'item');
</div> </div>
</div> </div>
<div class="content"> <div class="content">
<span class="link"> <span class="link" @click.stop>
{{ card.name }} {{ card.name }}
<ItemDescriptorProxy :id="card.id" /> <ItemDescriptorProxy :id="card.id" />
</span> </span>

View File

@ -18,8 +18,7 @@ const $props = defineProps({
}, },
columns: { columns: {
type: Number, type: Number,
required: false, default: 3,
default: null,
}, },
}); });

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import '@quasar/quasar-ui-qcalendar/src/QCalendarVariables.sass'; import '@quasar/quasar-ui-qcalendar/src/QCalendarVariables.scss';
const $props = defineProps({ const $props = defineProps({
bordered: { bordered: {

View File

@ -1,38 +1,49 @@
<template> <template>
<div class="header bg-primary q-pa-sm q-mb-md"> <div class="header bg-primary q-pa-sm q-mb-md">
<QSkeleton type="rect" square /> <QSkeleton type="rect" square />
<QSkeleton type="rect" square />
</div> </div>
<div class="row q-pa-md q-col-gutter-md q-mb-md"> <div class="row q-pa-md q-col-gutter-md q-mb-md">
<QSkeleton type="rect" class="q-mb-md" square /> <div class="col">
<QSkeleton type="text" square /> <QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="rect" class="q-mb-md" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> </div>
<QSkeleton type="text" square /> <div class="col">
<QSkeleton type="text" square /> <QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="rect" class="q-mb-md" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> </div>
<QSkeleton type="text" square /> <div class="col">
<QSkeleton type="text" square /> <QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="rect" class="q-mb-md" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> </div>
<QSkeleton type="rect" class="q-mb-md" square /> <div class="col">
<QSkeleton type="text" square /> <QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square /> <QSkeleton type="text" square />
<QSkeleton type="text" square />
</div>
<div class="col">
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
</div>
</div> </div>
</template> </template>

View File

@ -190,7 +190,10 @@ const getLocale = (label) => {
const globalLocale = `globals.params.${param}`; const globalLocale = `globals.params.${param}`;
if (te(globalLocale)) return t(globalLocale); if (te(globalLocale)) return t(globalLocale);
else if (te(t(`params.${param}`))); else if (te(t(`params.${param}`)));
else return t(`${route.meta.moduleName.toLowerCase()}.params.${param}`); else {
const camelCaseModuleName = route.meta.moduleName.charAt(0).toLowerCase() + route.meta.moduleName.slice(1);
return t(`${camelCaseModuleName}.params.${param}`);
}
}; };
</script> </script>

View File

@ -30,7 +30,7 @@ onBeforeMount(async () => {
.data; .data;
if (!channel) channel = defaultChannel; if (!channel) channel = defaultChannel;
phone.value = await parsePhone(props.phoneNumber, props.country.toLowerCase()); phone.value = await parsePhone(props.phoneNumber, props.country?.toLowerCase());
config[ config[
type type
].url = `${url}?customerIdentity=%2B${phone.value}&channelId=${channel}`; ].url = `${url}?customerIdentity=%2B${phone.value}&channelId=${channel}`;

View File

@ -0,0 +1,20 @@
<template>
<QBtn
color="white"
dense
flat
icon="more_vert"
round
size="md"
data-cy="descriptor-more-opts"
>
<QTooltip>
{{ $t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu ref="menuRef">
<QList>
<slot name="menu" :menu-ref="$refs.menuRef" />
</QList>
</QMenu>
</QBtn>
</template>

View File

@ -78,6 +78,10 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
keyData: {
type: String,
default: undefined,
},
}); });
const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']); const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']);
@ -255,7 +259,7 @@ defineExpose({
:disable="disableInfiniteScroll || !store.hasMoreData" :disable="disableInfiniteScroll || !store.hasMoreData"
v-bind="$attrs" v-bind="$attrs"
> >
<slot name="body" :rows="store.data"></slot> <slot name="body" :rows="keyData ? store.data[keyData] : store.data"></slot>
<div v-if="isLoading" class="spinner info-row q-pa-md text-center"> <div v-if="isLoading" class="spinner info-row q-pa-md text-center">
<QSpinner color="primary" size="md" /> <QSpinner color="primary" size="md" />
</div> </div>

View File

@ -113,23 +113,20 @@ onMounted(() => {
}); });
async function search() { async function search() {
const staticParams = Object.keys(store.userParams ?? {}).length
? store.userParams
: store.defaultParams;
arrayData.resetPagination(); arrayData.resetPagination();
const filter = { let filter = { params: { search: searchText.value } };
params: {
search: searchText.value,
},
filter: props.filter,
};
if (!props.searchRemoveParams || !searchText.value) { if (!props.searchRemoveParams || !searchText.value) {
filter.params = { filter = {
...staticParams, params: {
search: searchText.value, ...store.userParams,
search: searchText.value,
},
filter: store.filter,
}; };
} else {
arrayData.reset(['currentFilter', 'userParams']);
} }
if (props.whereFilter) { if (props.whereFilter) {

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { defineProps, ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();

View File

@ -0,0 +1,78 @@
import { vi, describe, expect, it, beforeAll, afterEach, beforeEach } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import CardSummary from 'src/components/ui/CardSummary.vue';
import * as vueRouter from 'vue-router';
describe('CardSummary', () => {
let vm;
let wrapper;
beforeAll(() => {
vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
});
vi.spyOn(vueRouter, 'useRoute').mockReturnValue({
query: {},
params: {},
meta: { moduleName: 'mockName' },
path: 'mockName/1/summary',
name: 'CardSummary',
});
beforeEach(() => {
wrapper = createWrapper(CardSummary, {
propsData: {
dataKey: 'cardSummaryKey',
url: 'cardSummaryUrl',
filter: 'cardFilter',
},
});
vm = wrapper.vm;
wrapper = wrapper.wrapper;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should fetch data correctly', async () => {
const fetchSpy = vi
.spyOn(vm.arrayData, 'fetch')
.mockResolvedValue({ data: [{ id: 1, name: 'Test Entity' }] });
await vm.fetch();
expect(fetchSpy).toHaveBeenCalledWith({ append: false, updateRouter: false });
expect(wrapper.emitted('onFetch')).toBeTruthy();
expect(vm.isLoading).toBe(false);
});
it('should set correct props to the store', () => {
expect(vm.store.url).toEqual('cardSummaryUrl');
expect(vm.store.filter).toEqual('cardFilter');
});
it('should compute entity correctly from store data', () => {
vm.store.data = [{ id: 1, name: 'Entity 1' }];
expect(vm.entity).toEqual({ id: 1, name: 'Entity 1' });
});
it('should handle empty data gracefully', () => {
vm.store.data = [];
expect(vm.entity).toBeUndefined();
});
it('should respond to prop changes and refetch data', async () => {
const newUrl = 'CardSummary/35';
const newKey = 'cardSummaryKey/35';
const fetchSpy = vi.spyOn(vm.arrayData, 'fetch');
await wrapper.setProps({ url: newUrl, filter: { key: newKey } });
expect(fetchSpy).toHaveBeenCalled();
expect(vm.store.url).toBe(newUrl);
expect(vm.store.filter).toEqual({ key: newKey });
});
it('should return true if route path ends with /summary' , () => {
expect(vm.isSummary).toBe(true);
});
});

View File

@ -0,0 +1,89 @@
import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnImg from 'src/components/ui/VnImg.vue';
let wrapper;
let vm;
const isEmployeeMock = vi.fn();
function generateWrapper(storage = 'images') {
wrapper = createWrapper(VnImg, {
props: {
id: 123,
zoomResolution: '400x400',
storage,
}
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
vm.timeStamp = 'timestamp';
};
vi.mock('src/composables/useSession', () => ({
useSession: () => ({
getTokenMultimedia: () => 'token',
}),
}));
vi.mock('src/composables/useRole', () => ({
useRole: () => ({
isEmployee: isEmployeeMock,
}),
}));
describe('VnImg', () => {
beforeEach(() => {
isEmployeeMock.mockReset();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('getUrl', () => {
it('should return /api/{storage}/{id}/downloadFile?access_token={token} when storage is dms', async () => {
isEmployeeMock.mockReturnValue(false);
generateWrapper('dms');
await vm.$nextTick();
const url = vm.getUrl();
expect(url).toBe('/api/dms/123/downloadFile?access_token=token');
});
it('should return /no-user.png when role is not employee and storage is not dms', async () => {
isEmployeeMock.mockReturnValue(false);
generateWrapper();
await vm.$nextTick();
const url = vm.getUrl();
expect(url).toBe('/no-user.png');
});
it('should return /api/{storage}/{collection}/{curResolution}/{id}/download?access_token={token}&{timeStamp} when zoom is false and role is employee and storage is not dms', async () => {
isEmployeeMock.mockReturnValue(true);
generateWrapper();
await vm.$nextTick();
const url = vm.getUrl();
expect(url).toBe('/api/images/catalog/200x200/123/download?access_token=token&timestamp');
});
it('should return /api/{storage}/{collection}/{curResolution}/{id}/download?access_token={token}&{timeStamp} when zoom is true and role is employee and storage is not dms', async () => {
isEmployeeMock.mockReturnValue(true);
generateWrapper();
await vm.$nextTick();
const url = vm.getUrl(true);
expect(url).toBe('/api/images/catalog/400x400/123/download?access_token=token&timestamp');
});
});
describe('reload', () => {
it('should update the timestamp', async () => {
generateWrapper();
const initialTimestamp = wrapper.vm.timeStamp;
wrapper.vm.reload();
const newTimestamp = wrapper.vm.timeStamp;
expect(initialTimestamp).not.toEqual(newTimestamp);
});
});
});

View File

@ -0,0 +1,71 @@
import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
describe('VnSearchbar', () => {
let vm;
let wrapper;
let applyFilterSpy;
const searchText = 'Bolas de madera';
const userParams = {staticKey: 'staticValue'};
beforeEach(async () => {
wrapper = createWrapper(VnSearchbar, {
propsData: {
dataKey: 'testKey',
filter: null,
whereFilter: null,
searchRemoveParams: true,
},
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
vm.searchText = searchText;
vm.arrayData.store.userParams = userParams;
applyFilterSpy = vi.spyOn(vm.arrayData, 'applyFilter').mockImplementation(() => {});
});
afterEach(() => {
vi.clearAllMocks();
});
it('search resets pagination and applies filter', async () => {
const resetPaginationSpy = vi.spyOn(vm.arrayData, 'resetPagination').mockImplementation(() => {});
await vm.search();
expect(resetPaginationSpy).toHaveBeenCalled();
expect(applyFilterSpy).toHaveBeenCalledWith({
params: { search: searchText },
});
});
it('search includes static params if searchRemoveParams is false', async () => {
wrapper.setProps({ searchRemoveParams: false });
await vm.$nextTick();
await vm.search();
expect(applyFilterSpy).toHaveBeenCalledWith({
params: { staticKey: 'staticValue', search: searchText },
filter: {skip: 0},
});
});
it('updates store when dataKey changes', async () => {
expect(vm.store.userParams).toEqual(userParams);
wrapper.setProps({ dataKey: 'newTestKey' });
await vm.$nextTick();
expect(vm.store.userParams).toEqual({});
});
it('computes the "to" property correctly for redirection', () => {
vm.arrayData.store.searchUrl = 'searchParam';
vm.arrayData.store.currentFilter = { category: 'plants' };
const expectedQuery = JSON.stringify({
...vm.arrayData.store.currentFilter,
search: searchText,
});
expect(vm.to.query.searchParam).toBe(expectedQuery);
});
});

View File

@ -7,7 +7,9 @@ import { isDialogOpened } from 'src/filters';
const arrayDataStore = useArrayDataStore(); const arrayDataStore = useArrayDataStore();
export function useArrayData(key = useRoute().meta.moduleName, userOptions) { export function useArrayData(key, userOptions) {
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');
if (!arrayDataStore.get(key)) arrayDataStore.set(key); if (!arrayDataStore.get(key)) arrayDataStore.set(key);
@ -31,10 +33,11 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
: JSON.parse(params?.filter ?? '{}'); : JSON.parse(params?.filter ?? '{}');
delete params.filter; delete params.filter;
store.userParams = { ...store.userParams, ...params }; store.userParams = params;
store.filter = { ...filter, ...store.userFilter }; store.filter = { ...filter, ...store.userFilter };
if (filter?.order) store.order = filter.order; if (filter?.order) store.order = filter.order;
} }
setCurrentFilter();
}); });
if (key && userOptions) setOptions(); if (key && userOptions) setOptions();
@ -76,11 +79,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
cancelRequest(); cancelRequest();
canceller = new AbortController(); canceller = new AbortController();
const { params, limit } = getCurrentFilter(); const { params, limit } = setCurrentFilter();
store.currentFilter = JSON.parse(JSON.stringify(params));
delete store.currentFilter.filter.include;
store.currentFilter.filter = JSON.stringify(store.currentFilter.filter);
let exprFilter; let exprFilter;
if (store?.exprBuilder) { if (store?.exprBuilder) {
@ -105,7 +104,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
store.hasMoreData = limit && response.data.length >= limit; store.hasMoreData = limit && response.data.length >= limit;
if (!append && !isDialogOpened() && updateRouter) { if (!append && !isDialogOpened() && updateRouter) {
if (updateStateParams(response.data)?.redirect) return; if (updateStateParams(response.data)?.redirect && !store.keepData) return;
} }
store.isLoading = false; store.isLoading = false;
canceller = null; canceller = null;
@ -140,12 +139,12 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
} }
} }
async function applyFilter({ filter, params }) { async function applyFilter({ filter, params }, fetchOptions = {}) {
if (filter) store.userFilter = filter; if (filter) store.userFilter = filter;
store.filter = {}; store.filter = {};
if (params) store.userParams = { ...params }; if (params) store.userParams = { ...params };
const response = await fetch({}); const response = await fetch(fetchOptions);
return response; return response;
} }
@ -171,10 +170,9 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function addOrder(field, direction = 'ASC') { async function addOrder(field, direction = 'ASC') {
const newOrder = field + ' ' + direction; const newOrder = field + ' ' + direction;
let order = store.order || []; const order = toArray(store.order);
if (typeof order == 'string') order = [order];
let index = order.findIndex((o) => o.split(' ')[0] === field); let index = getOrderIndex(order, field);
if (index > -1) { if (index > -1) {
order[index] = newOrder; order[index] = newOrder;
} else { } else {
@ -191,16 +189,24 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
} }
async function deleteOrder(field) { async function deleteOrder(field) {
let order = store.order ?? []; const order = toArray(store.order);
if (typeof order == 'string') order = [order]; const index = getOrderIndex(order, field);
const index = order.findIndex((o) => o.split(' ')[0] === field);
if (index > -1) order.splice(index, 1); if (index > -1) order.splice(index, 1);
store.order = order; store.order = order;
fetch({}); fetch({});
} }
function getOrderIndex(order, field) {
return order.findIndex((o) => o.split(' ')[0] === field);
}
function toArray(str = []) {
if (!str) return [];
if (Array.isArray(str)) return str;
if (typeof str === 'string') return str.split(',').map((item) => item.trim());
}
function sanitizerParams(params, exprBuilder) { function sanitizerParams(params, exprBuilder) {
for (const param in params) { for (const param in params) {
if (params[param] === '' || params[param] === null) { if (params[param] === '' || params[param] === null) {
@ -274,14 +280,14 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
} }
function getCurrentFilter() { function getCurrentFilter() {
if (!Object.keys(store.userParams).length)
store.userParams = store.defaultParams ?? {};
const filter = { const filter = {
limit: store.limit, limit: store.limit,
...store.userFilter,
}; };
let userParams = { ...store.userParams };
Object.assign(filter, store.userFilter);
let where; let where;
if (filter?.where || store.filter?.where) if (filter?.where || store.filter?.where)
where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {}); where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {});
@ -289,15 +295,22 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
filter.where = where; filter.where = where;
const params = { filter }; const params = { filter };
Object.assign(params, userParams); Object.assign(params, store.userParams);
if (params.filter) params.filter.skip = store.skip; if (params.filter) params.filter.skip = store.skip;
if (store?.order && typeof store?.order == 'string') store.order = [store.order]; if (store.order) params.filter.order = toArray(store.order);
if (store.order?.length) params.filter.order = [...store.order];
else delete params.filter.order; else delete params.filter.order;
return { filter, params, limit: filter.limit }; return { filter, params, limit: filter.limit };
} }
function setCurrentFilter() {
const { params, limit } = getCurrentFilter();
store.currentFilter = JSON.parse(JSON.stringify(params));
delete store.currentFilter.filter.include;
store.currentFilter.filter = JSON.stringify(store.currentFilter.filter);
return { params, limit };
}
function processData(data, { map = true, append = true }) { function processData(data, { map = true, append = true }) {
if (!append) { if (!append) {
store.data = []; store.data = [];
@ -331,6 +344,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
applyFilter, applyFilter,
addFilter, addFilter,
getCurrentFilter, getCurrentFilter,
setCurrentFilter,
addFilterWhere, addFilterWhere,
addOrder, addOrder,
deleteOrder, deleteOrder,

View File

@ -29,8 +29,12 @@ export function useFilterParams(key) {
orders.value = orderObject; orders.value = orderObject;
} }
function setUserParams(watchedParams) { function setUserParams(watchedParams = {}) {
if (!watchedParams || Object.keys(watchedParams).length == 0) return; if (Object.keys(watchedParams).length == 0) {
params.value = {};
orders.value = {};
return;
}
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
if (typeof watchedParams?.filter == 'string') if (typeof watchedParams?.filter == 'string')

View File

@ -0,0 +1,6 @@
import axios from 'axios';
export default async (id) => {
const { data } = await axios.get(`Accounts/${id}/exists`);
return data.exists;
};

View File

@ -0,0 +1,24 @@
import { onMounted, ref } from 'vue';
export function useHasContent(selector) {
const container = ref({});
const hasContent = ref();
onMounted(() => {
container.value = document?.querySelector(selector);
if (!container.value) return;
const observer = new MutationObserver(() => {
if (document.querySelector(selector))
hasContent.value = !!container.value.childNodes.length;
});
observer.observe(container.value, {
subtree: true,
childList: true,
attributes: true,
});
});
return hasContent;
}

View File

@ -6,6 +6,7 @@ import axios from 'axios';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import useNotify from './useNotify'; import useNotify from './useNotify';
import { useTokenConfig } from './useTokenConfig'; import { useTokenConfig } from './useTokenConfig';
import { getToken, getTokenMultimedia } from 'src/utils/session';
const TOKEN_MULTIMEDIA = 'tokenMultimedia'; const TOKEN_MULTIMEDIA = 'tokenMultimedia';
const TOKEN = 'token'; const TOKEN = 'token';
@ -15,19 +16,6 @@ export function useSession() {
let isCheckingToken = false; let isCheckingToken = false;
let intervalId = null; let intervalId = null;
function getToken() {
const localToken = localStorage.getItem(TOKEN);
const sessionToken = sessionStorage.getItem(TOKEN);
return localToken || sessionToken || '';
}
function getTokenMultimedia() {
const localTokenMultimedia = localStorage.getItem(TOKEN_MULTIMEDIA);
const sessionTokenMultimedia = sessionStorage.getItem(TOKEN_MULTIMEDIA);
return localTokenMultimedia || sessionTokenMultimedia || '';
}
function setSession(data) { function setSession(data) {
let keepLogin = data.keepLogin; let keepLogin = data.keepLogin;
const storage = keepLogin ? localStorage : sessionStorage; const storage = keepLogin ? localStorage : sessionStorage;

View File

@ -1,6 +1,6 @@
// app global css in SCSS form // app global css in SCSS form
@import './icons.scss'; @import './icons.scss';
@import '@quasar/quasar-ui-qcalendar/src/QCalendarMonth.sass'; @import '@quasar/quasar-ui-qcalendar/src/QCalendarMonth.scss';
body.body--light { body.body--light {
--vn-header-color: #cecece; --vn-header-color: #cecece;
@ -310,6 +310,14 @@ input::-webkit-inner-spin-button {
.no-visible { .no-visible {
visibility: hidden; visibility: hidden;
} }
.q-item > .q-item__section:has(.q-checkbox) {
max-width: fit-content;
}
.row > .column:has(.q-checkbox) {
max-width: fit-content;
}
.q-field__inner { .q-field__inner {
.q-field__control { .q-field__control {
min-height: auto !important; min-height: auto !important;

View File

@ -16,6 +16,7 @@ import getUpdatedValues from './getUpdatedValues';
import getParamWhere from './getParamWhere'; import getParamWhere from './getParamWhere';
import parsePhone from './parsePhone'; import parsePhone from './parsePhone';
import isDialogOpened from './isDialogOpened'; import isDialogOpened from './isDialogOpened';
import toCelsius from './toCelsius';
export { export {
getUpdatedValues, getUpdatedValues,
@ -36,4 +37,5 @@ export {
dashIfEmpty, dashIfEmpty,
dateRange, dateRange,
getParamWhere, getParamWhere,
toCelsius,
}; };

3
src/filters/toCelsius.js Normal file
View File

@ -0,0 +1,3 @@
export default function toCelsius(value) {
return value ? `${value}°C` : '';
}

View File

@ -1,13 +1,15 @@
globals: globals:
lang: lang:
es: Spanish es: Spanish
en: English en: English
quantity: Quantity
language: Language language: Language
quantity: Quantity
entity: Entity entity: Entity
preview: Preview
user: User user: User
details: Details details: Details
collapseMenu: Collapse left menu collapseMenu: Collapse lateral menu
advancedMenu: Advanced menu
backToDashboard: Return to dashboard backToDashboard: Return to dashboard
notifications: Notifications notifications: Notifications
userPanel: User panel userPanel: User panel
@ -35,7 +37,6 @@ globals:
confirm: Confirm confirm: Confirm
assign: Assign assign: Assign
back: Back back: Back
downloadPdf: Download PDF
yes: 'Yes' yes: 'Yes'
no: 'No' no: 'No'
noChanges: No changes to save noChanges: No changes to save
@ -59,6 +60,7 @@ globals:
downloadCSVSuccess: CSV downloaded successfully downloadCSVSuccess: CSV downloaded successfully
reference: Reference reference: Reference
agency: Agency agency: Agency
entry: Entry
warehouseOut: Warehouse Out warehouseOut: Warehouse Out
warehouseIn: Warehouse In warehouseIn: Warehouse In
landed: Landed landed: Landed
@ -67,11 +69,11 @@ globals:
amount: Amount amount: Amount
packages: Packages packages: Packages
download: Download download: Download
downloadPdf: Download PDF
selectRows: 'Select all { numberRows } row(s)' selectRows: 'Select all { numberRows } row(s)'
allRows: 'All { numberRows } row(s)' allRows: 'All { numberRows } row(s)'
markAll: Mark all markAll: Mark all
requiredField: Required field requiredField: Required field
valueCantBeEmpty: Value cannot be empty
class: clase class: clase
type: Type type: Type
reason: reason reason: reason
@ -81,6 +83,9 @@ globals:
warehouse: Warehouse warehouse: Warehouse
company: Company company: Company
fieldRequired: Field required fieldRequired: Field required
valueCantBeEmpty: Value cannot be empty
Value can't be blank: Value cannot be blank
Value can't be null: Value cannot be null
allowedFilesText: 'Allowed file types: { allowedContentTypes }' allowedFilesText: 'Allowed file types: { allowedContentTypes }'
smsSent: SMS sent smsSent: SMS sent
confirmDeletion: Confirm deletion confirmDeletion: Confirm deletion
@ -130,6 +135,26 @@ globals:
medium: Medium medium: Medium
big: Big big: Big
email: Email email: Email
supplier: Supplier
ticketList: Ticket List
created: Created
worker: Worker
now: Now
name: Name
new: New
comment: Comment
observations: Observations
goToModuleIndex: Go to module index
createInvoiceIn: Create invoice in
myAccount: My account
noOne: No one
maxTemperature: Max
minTemperature: Min
changePass: Change password
deleteConfirmTitle: Delete selected elements
changeState: Change state
raid: 'Raid {daysInForward} days'
isVies: Vies
pageTitles: pageTitles:
logIn: Login logIn: Login
addressEdit: Update address addressEdit: Update address
@ -151,13 +176,14 @@ globals:
subRoles: Subroles subRoles: Subroles
inheritedRoles: Inherited Roles inheritedRoles: Inherited Roles
customers: Customers customers: Customers
customerCreate: New customer
createCustomer: Create customer
createOrder: New order
list: List list: List
webPayments: Web Payments webPayments: Web Payments
extendedList: Extended list extendedList: Extended list
notifications: Notifications notifications: Notifications
defaulter: Defaulter defaulter: Defaulter
customerCreate: New customer
createOrder: New order
fiscalData: Fiscal data fiscalData: Fiscal data
billingData: Billing data billingData: Billing data
consignees: Consignees consignees: Consignees
@ -193,27 +219,28 @@ globals:
claims: Claims claims: Claims
claimCreate: New claim claimCreate: New claim
lines: Lines lines: Lines
photos: Photos
development: Development development: Development
photos: Photos
action: Action action: Action
invoiceOuts: Invoice out invoiceOuts: Invoice out
negativeBases: Negative Bases negativeBases: Negative Bases
globalInvoicing: Global invoicing globalInvoicing: Global invoicing
invoiceOutCreate: Create invoice out invoiceOutCreate: Create invoice out
order: Orders
orderList: List
orderCreate: New order
catalog: Catalog
volume: Volume
shelving: Shelving shelving: Shelving
shelvingList: Shelving List shelvingList: Shelving List
shelvingCreate: New shelving shelvingCreate: New shelving
invoiceIns: Invoices In invoiceIns: Invoices In
invoiceInCreate: Create invoice in invoiceInCreate: Create invoice in
vat: VAT vat: VAT
labeler: Labeler
dueDay: Due day dueDay: Due day
intrastat: Intrastat intrastat: Intrastat
corrective: Corrective corrective: Corrective
order: Orders
orderList: List
orderCreate: New order
catalog: Catalog
volume: Volume
workers: Workers workers: Workers
workerCreate: New worker workerCreate: New worker
department: Department department: Department
@ -226,10 +253,10 @@ globals:
wagonsList: Wagons List wagonsList: Wagons List
wagonCreate: Create wagon wagonCreate: Create wagon
wagonEdit: Edit wagon wagonEdit: Edit wagon
wagonCounter: Trolley counter
typesList: Types List typesList: Types List
typeCreate: Create type typeCreate: Create type
typeEdit: Edit type typeEdit: Edit type
wagonCounter: Trolley counter
roadmap: Roadmap roadmap: Roadmap
stops: Stops stops: Stops
routes: Routes routes: Routes
@ -238,21 +265,16 @@ globals:
routeCreate: New route routeCreate: New route
RouteRoadmap: Roadmaps RouteRoadmap: Roadmaps
RouteRoadmapCreate: Create roadmap RouteRoadmapCreate: Create roadmap
RouteExtendedList: Router
autonomous: Autonomous autonomous: Autonomous
suppliers: Suppliers suppliers: Suppliers
supplier: Supplier supplier: Supplier
expedition: Expedition
services: Service
components: Components
pictures: Pictures
packages: Packages
tracking: Tracking
labeler: Labeler
supplierCreate: New supplier supplierCreate: New supplier
accounts: Accounts accounts: Accounts
addresses: Addresses addresses: Addresses
agencyTerm: Agency agreement agencyTerm: Agency agreement
travel: Travels travel: Travels
create: Create
extraCommunity: Extra community extraCommunity: Extra community
travelCreate: New travel travelCreate: New travel
history: Log history: Log
@ -260,14 +282,13 @@ globals:
items: Items items: Items
diary: Diary diary: Diary
tags: Tags tags: Tags
create: Create
buyRequest: Buy requests
fixedPrice: Fixed prices fixedPrice: Fixed prices
buyRequest: Buy requests
wasteBreakdown: Waste breakdown wasteBreakdown: Waste breakdown
itemCreate: New item itemCreate: New item
barcode: Barcodes
tax: Tax tax: Tax
botanical: Botanical botanical: Botanical
barcode: Barcodes
itemTypeCreate: New item type itemTypeCreate: New item type
family: Item Type family: Item Type
lastEntries: Last entries lastEntries: Last entries
@ -283,13 +304,20 @@ globals:
formation: Formation formation: Formation
locations: Locations locations: Locations
warehouses: Warehouses warehouses: Warehouses
saleTracking: Sale tracking
roles: Roles roles: Roles
connections: Connections connections: Connections
acls: ACLs acls: ACLs
mailForwarding: Mail forwarding mailForwarding: Mail forwarding
mailAlias: Mail alias mailAlias: Mail alias
privileges: Privileges privileges: Privileges
observation: Notes
expedition: Expedition
saleTracking: Sale tracking
services: Service
tracking: Tracking
components: Components
pictures: Pictures
packages: Packages
ldap: LDAP ldap: LDAP
samba: Samba samba: Samba
twoFactor: Two factor twoFactor: Two factor
@ -300,28 +328,13 @@ globals:
serial: Serial serial: Serial
medical: Mutual medical: Mutual
pit: IRPF pit: IRPF
RouteExtendedList: Router
wasteRecalc: Waste recaclulate wasteRecalc: Waste recaclulate
translations: Translations translations: Translations
operator: Operator operator: Operator
parking: Parking parking: Parking
supplier: Supplier
created: Created
worker: Worker
now: Now
name: Name
new: New
comment: Comment
observations: Observations
goToModuleIndex: Go to module index
unsavedPopup: unsavedPopup:
title: Unsaved changes will be lost title: Unsaved changes will be lost
subtitle: Are you sure exit without saving? subtitle: Are you sure exit without saving?
createInvoiceIn: Create invoice in
myAccount: My account
noOne: No one
maxTemperature: Max
minTemperature: Min
params: params:
clientFk: Client id clientFk: Client id
salesPersonFk: Sales person salesPersonFk: Sales person
@ -339,18 +352,13 @@ globals:
supplierFk: Supplier supplierFk: Supplier
supplierRef: Supplier ref supplierRef: Supplier ref
serial: Serial serial: Serial
amount: Importe amount: Amount
awbCode: AWB awbCode: AWB
correctedFk: Rectified correctedFk: Rectified
correctingFk: Rectificative correctingFk: Rectificative
daysOnward: Days onward daysOnward: Days onward
countryFk: Country countryFk: Country
companyFk: Company companyFk: Company
changePass: Change password
deleteConfirmTitle: Delete selected elements
changeState: Change state
raid: 'Raid {daysInForward} days'
isVies: Vies
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred statusInternalServerError: An internal server error has ocurred
@ -389,80 +397,6 @@ cau:
subtitle: By sending this ticket, all the data related to the error, the section, the user, etc., are already sent. subtitle: By sending this ticket, all the data related to the error, the section, the user, etc., are already sent.
inputLabel: Explain why this error should not appear inputLabel: Explain why this error should not appear
askPrivileges: Ask for privileges askPrivileges: Ask for privileges
entry:
list:
newEntry: New entry
tableVisibleColumns:
created: Creation
supplierFk: Supplier
isBooked: Booked
isConfirmed: Confirmed
isOrdered: Ordered
companyFk: Company
travelFk: Travel
isExcludedFromAvailable: Inventory
invoiceAmount: Import
summary:
commission: Commission
currency: Currency
invoiceNumber: Invoice number
ordered: Ordered
booked: Booked
excludedFromAvailable: Inventory
travelReference: Reference
travelAgency: Agency
travelShipped: Shipped
travelDelivered: Delivered
travelLanded: Landed
travelReceived: Received
buys: Buys
stickers: Stickers
package: Package
packing: Pack.
grouping: Group.
buyingValue: Buying value
import: Import
pvp: PVP
basicData:
travel: Travel
currency: Currency
commission: Commission
observation: Observation
booked: Booked
excludedFromAvailable: Inventory
buys:
observations: Observations
packagingFk: Box
color: Color
printedStickers: Printed stickers
notes:
observationType: Observation type
latestBuys:
tableVisibleColumns:
image: Picture
itemFk: Item ID
weightByPiece: Weight/Piece
isActive: Active
family: Family
entryFk: Entry
freightValue: Freight value
comissionValue: Commission value
packageValue: Package value
isIgnored: Is ignored
price2: Grouping
price3: Packing
minPrice: Min
ektFk: Ekt
packingOut: Package out
landing: Landing
isExcludedFromAvailable: Es inventory
params:
toShipped: To
fromShipped: From
warehouseiNFk: Warehouse
daysOnward: Days onward
daysAgo: Days ago
warehouseInFk: Warehouse in
ticket: ticket:
params: params:
ticketFk: Ticket ID ticketFk: Ticket ID
@ -564,42 +498,6 @@ invoiceOut:
comercial: Comercial comercial: Comercial
errors: errors:
downloadCsvFailed: CSV download failed downloadCsvFailed: CSV download failed
shelving:
list:
parking: Parking
priority: Priority
newShelving: New Shelving
summary:
recyclable: Recyclable
parking:
pickingOrder: Picking order
sector: Sector
row: Row
column: Column
searchBar:
info: You can search by parking code
label: Search parking...
order:
field:
salesPersonFk: Sales Person
form:
clientFk: Client
addressFk: Address
agencyModeFk: Agency
list:
newOrder: New Order
summary:
basket: Basket
notConfirmed: Not confirmed
created: Created
createdFrom: Created From
address: Address
total: Total
items: Items
orderTicketList: Order Ticket List
amount: Amount
confirm: Confirm
confirmLines: Confirm lines
department: department:
chat: Chat chat: Chat
bossDepartment: Boss Department bossDepartment: Boss Department
@ -791,6 +689,9 @@ supplier:
consumption: consumption:
entry: Entry entry: Entry
travel: travel:
search: Search travel
searchInfo: You can search by travel id or name
id: Id
travelList: travelList:
tableVisibleColumns: tableVisibleColumns:
ref: Reference ref: Reference
@ -799,6 +700,7 @@ travel:
totalEntries: Total entries totalEntries: Total entries
totalEntriesTooltip: Total entries totalEntriesTooltip: Total entries
daysOnward: Landed days onwards daysOnward: Landed days onwards
awb: AWB
summary: summary:
entryId: Entry Id entryId: Entry Id
freight: Freight freight: Freight
@ -820,62 +722,6 @@ travel:
destination: Destination destination: Destination
thermograph: Thermograph thermograph: Thermograph
travelFileDescription: 'Travel id { travelId }' travelFileDescription: 'Travel id { travelId }'
item:
descriptor:
buyer: Buyer
color: Color
category: Category
available: Available
warehouseText: 'Calculated on the warehouse of { warehouseName }'
itemDiary: Item diary
list:
id: Identifier
stems: Stems
category: Category
typeName: Type
isActive: Active
userName: Buyer
weightByPiece: Weight/Piece
stemMultiplier: Multiplier
fixedPrice:
itemFk: Item ID
groupingPrice: Grouping price
packingPrice: Packing price
hasMinPrice: Has min price
minPrice: Min price
started: Started
ended: Ended
create:
priority: Priority
buyRequest:
requester: Requester
requested: Requested
attender: Atender
achieved: Achieved
concept: Concept
summary:
otherData: Other data
tax: Tax
botanical: Botanical
barcode: Barcode
completeName: Complete name
family: Familiy
stems: Stems
multiplier: Multiplier
buyer: Buyer
doPhoto: Do photo
intrastatCode: Intrastat code
ref: Reference
relevance: Relevance
weight: Weight (gram)/stem
units: Units/box
expense: Expense
generic: Generic
recycledPlastic: Recycled plastic
nonRecycledPlastic: Non recycled plastic
minSalesQuantity: Min sales quantity
genus: Genus
specie: Specie
components: components:
topbar: {} topbar: {}
itemsFilterPanel: itemsFilterPanel:
@ -890,7 +736,10 @@ components:
hasMinPrice: Minimum price hasMinPrice: Minimum price
# LatestBuysFilter # LatestBuysFilter
salesPersonFk: Buyer salesPersonFk: Buyer
supplierFk: Supplier
from: From from: From
to: To
visible: Is visible
active: Is active active: Is active
floramondo: Is floramondo floramondo: Is floramondo
showBadDates: Show future items showBadDates: Show future items

View File

@ -5,9 +5,11 @@ globals:
language: Idioma language: Idioma
quantity: Cantidad quantity: Cantidad
entity: Entidad entity: Entidad
preview: Vista previa
user: Usuario user: Usuario
details: Detalles details: Detalles
collapseMenu: Contraer menú lateral collapseMenu: Contraer menú lateral
advancedMenu: Menú avanzado
backToDashboard: Volver al tablón backToDashboard: Volver al tablón
notifications: Notificaciones notifications: Notificaciones
userPanel: Panel de usuario userPanel: Panel de usuario
@ -53,11 +55,12 @@ globals:
today: Hoy today: Hoy
yesterday: Ayer yesterday: Ayer
dateFormat: es-ES dateFormat: es-ES
noSelectedRows: No tienes ninguna línea seleccionada
microsip: Abrir en MicroSIP microsip: Abrir en MicroSIP
noSelectedRows: No tienes ninguna línea seleccionada
downloadCSVSuccess: Descarga de CSV exitosa downloadCSVSuccess: Descarga de CSV exitosa
reference: Referencia reference: Referencia
agency: Agencia agency: Agencia
entry: Entrada
warehouseOut: Alm. salida warehouseOut: Alm. salida
warehouseIn: Alm. entrada warehouseIn: Alm. entrada
landed: F. entrega landed: F. entrega
@ -132,6 +135,26 @@ globals:
medium: Mediano/a medium: Mediano/a
big: Grande big: Grande
email: Correo email: Correo
supplier: Proveedor
ticketList: Listado de tickets
created: Fecha creación
worker: Trabajador
now: Ahora
name: Nombre
new: Nuevo
comment: Comentario
observations: Observaciones
goToModuleIndex: Ir al índice del módulo
createInvoiceIn: Crear factura recibida
myAccount: Mi cuenta
noOne: Nadie
maxTemperature: Máx
minTemperature: Mín
changePass: Cambiar contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado
raid: 'Redada {daysInForward} días'
isVies: Vies
pageTitles: pageTitles:
logIn: Inicio de sesión logIn: Inicio de sesión
addressEdit: Modificar consignatario addressEdit: Modificar consignatario
@ -154,17 +177,17 @@ globals:
inheritedRoles: Roles heredados inheritedRoles: Roles heredados
customers: Clientes customers: Clientes
customerCreate: Nuevo cliente customerCreate: Nuevo cliente
createCustomer: Crear cliente
createOrder: Nuevo pedido createOrder: Nuevo pedido
list: Listado list: Listado
webPayments: Pagos Web webPayments: Pagos Web
extendedList: Listado extendido extendedList: Listado extendido
notifications: Notificaciones notifications: Notificaciones
defaulter: Morosos defaulter: Morosos
createCustomer: Crear cliente
fiscalData: Datos fiscales fiscalData: Datos fiscales
billingData: Forma de pago billingData: Forma de pago
consignees: Consignatarios consignees: Consignatarios
'address-create': Nuevo consignatario address-create: Nuevo consignatario
notes: Notas notes: Notas
credits: Créditos credits: Créditos
greuges: Greuges greuges: Greuges
@ -230,10 +253,10 @@ globals:
wagonsList: Listado vagones wagonsList: Listado vagones
wagonCreate: Crear tipo wagonCreate: Crear tipo
wagonEdit: Editar tipo wagonEdit: Editar tipo
wagonCounter: Contador de carros
typesList: Listado tipos typesList: Listado tipos
typeCreate: Crear tipo typeCreate: Crear tipo
typeEdit: Editar tipo typeEdit: Editar tipo
wagonCounter: Contador de carros
roadmap: Troncales roadmap: Troncales
stops: Paradas stops: Paradas
routes: Rutas routes: Rutas
@ -242,8 +265,8 @@ globals:
routeCreate: Nueva ruta routeCreate: Nueva ruta
RouteRoadmap: Troncales RouteRoadmap: Troncales
RouteRoadmapCreate: Crear troncal RouteRoadmapCreate: Crear troncal
autonomous: Autónomos
RouteExtendedList: Enrutador RouteExtendedList: Enrutador
autonomous: Autónomos
suppliers: Proveedores suppliers: Proveedores
supplier: Proveedor supplier: Proveedor
supplierCreate: Nuevo proveedor supplierCreate: Nuevo proveedor
@ -309,23 +332,9 @@ globals:
translations: Traducciones translations: Traducciones
operator: Operario operator: Operario
parking: Parking parking: Parking
supplier: Proveedor
created: Fecha creación
worker: Trabajador
now: Ahora
name: Nombre
new: Nuevo
comment: Comentario
observations: Observaciones
goToModuleIndex: Ir al índice del módulo
unsavedPopup: unsavedPopup:
title: Los cambios que no haya guardado se perderán title: Los cambios que no haya guardado se perderán
subtitle: ¿Seguro que quiere salir sin guardar? subtitle: ¿Seguro que quiere salir sin guardar?
createInvoiceIn: Crear factura recibida
myAccount: Mi cuenta
noOne: Nadie
maxTemperature: Máx
minTemperature: Mín
params: params:
clientFk: Id cliente clientFk: Id cliente
salesPersonFk: Comercial salesPersonFk: Comercial
@ -348,11 +357,6 @@ globals:
packing: ITP packing: ITP
countryFk: País countryFk: País
companyFk: Empresa companyFk: Empresa
changePass: Cambiar contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado
raid: 'Redada {daysInForward} días'
isVies: Vies
errors: errors:
statusUnauthorized: Acceso denegado statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor statusInternalServerError: Ha ocurrido un error interno del servidor
@ -389,80 +393,6 @@ cau:
subtitle: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc subtitle: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc
inputLabel: Explique el motivo por el que no deberia aparecer este fallo inputLabel: Explique el motivo por el que no deberia aparecer este fallo
askPrivileges: Solicitar permisos askPrivileges: Solicitar permisos
entry:
list:
newEntry: Nueva entrada
tableVisibleColumns:
created: Creación
supplierFk: Proveedor
isBooked: Asentado
isConfirmed: Confirmado
isOrdered: Pedida
companyFk: Empresa
travelFk: Envio
isExcludedFromAvailable: Inventario
invoiceAmount: Importe
summary:
commission: Comisión
currency: Moneda
invoiceNumber: Núm. factura
ordered: Pedida
booked: Contabilizada
excludedFromAvailable: Inventario
travelReference: Referencia
travelAgency: Agencia
travelShipped: F. envio
travelWarehouseOut: Alm. salida
travelDelivered: Enviada
travelLanded: F. entrega
travelReceived: Recibida
buys: Compras
stickers: Etiquetas
package: Embalaje
packing: Pack.
grouping: Group.
buyingValue: Coste
import: Importe
pvp: PVP
basicData:
travel: Envío
currency: Moneda
observation: Observación
commission: Comisión
booked: Asentado
excludedFromAvailable: Inventario
buys:
observations: Observaciónes
packagingFk: Embalaje
color: Color
printedStickers: Etiquetas impresas
notes:
observationType: Tipo de observación
latestBuys:
tableVisibleColumns:
image: Foto
itemFk: Id Artículo
weightByPiece: Peso (gramos)/tallo
isActive: Activo
family: Familia
entryFk: Entrada
freightValue: Porte
comissionValue: Comisión
packageValue: Embalaje
isIgnored: Ignorado
price2: Grouping
price3: Packing
minPrice: Min
ektFk: Ekt
packingOut: Embalaje envíos
landing: Llegada
isExcludedFromAvailable: Es inventario
params:
toShipped: Hasta
fromShipped: Desde
warehouseInFk: Alm. entrada
daysOnward: Días adelante
daysAgo: Días atras
ticket: ticket:
params: params:
ticketFk: ID de ticket ticketFk: ID de ticket
@ -521,48 +451,6 @@ ticket:
attender: Consignatario attender: Consignatario
create: create:
address: Dirección address: Dirección
invoiceOut:
card:
issued: Fecha emisión
customerCard: Ficha del cliente
ticketList: Listado de tickets
summary:
issued: Fecha
dued: Fecha límite
booked: Contabilizada
taxBreakdown: Desglose impositivo
taxableBase: Base imp.
rate: Tarifa
fee: Cuota
tickets: Tickets
totalWithVat: Importe
globalInvoices:
errors:
chooseValidClient: Selecciona un cliente válido
chooseValidCompany: Selecciona una empresa válida
chooseValidPrinter: Selecciona una impresora válida
chooseValidSerialType: Selecciona una tipo de serie válida
fillDates: La fecha de la factura y la fecha máxima deben estar completas
invoiceDateLessThanMaxDate: La fecha de la factura no puede ser menor que la fecha máxima
invoiceWithFutureDate: Existe una factura con una fecha futura
noTicketsToInvoice: No existen tickets para facturar
criticalInvoiceError: Error crítico en la facturación proceso detenido
invalidSerialTypeForAll: El tipo de serie debe ser global cuando se facturan todos los clientes
table:
addressId: Id dirección
streetAddress: Dirección fiscal
statusCard:
percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}'
pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs'
negativeBases:
clientId: Id cliente
base: Base
active: Activo
hasToInvoice: Facturar
verifiedData: Datos comprobados
comercial: Comercial
errors:
downloadCsvFailed: Error al descargar CSV
order: order:
field: field:
salesPersonFk: Comercial salesPersonFk: Comercial
@ -573,34 +461,15 @@ order:
list: list:
newOrder: Nuevo Pedido newOrder: Nuevo Pedido
summary: summary:
basket: Cesta issued: Fecha
notConfirmed: No confirmada dued: Fecha límite
created: Creado booked: Contabilizada
createdFrom: Creado desde taxBreakdown: Desglose impositivo
address: Dirección taxableBase: Base imp.
total: Total rate: Tarifa
vat: IVA fee: Cuota
state: Estado tickets: Tickets
alias: Alias totalWithVat: Importe
items: Artículos
orderTicketList: Tickets del pedido
amount: Monto
confirm: Confirmar
confirmLines: Confirmar lineas
shelving:
list:
parking: Parking
priority: Prioridad
newShelving: Nuevo Carro
summary:
recyclable: Reciclable
parking:
pickingOrder: Orden de recogida
row: Fila
column: Columna
searchBar:
info: Puedes buscar por código de parking
label: Buscar parking...
department: department:
chat: Chat chat: Chat
bossDepartment: Jefe de departamento bossDepartment: Jefe de departamento
@ -790,6 +659,9 @@ supplier:
consumption: consumption:
entry: Entrada entry: Entrada
travel: travel:
search: Buscar envío
searchInfo: Buscar envío por id o nombre
id: Id
travelList: travelList:
tableVisibleColumns: tableVisibleColumns:
ref: Referencia ref: Referencia
@ -798,6 +670,7 @@ travel:
totalEntries: totalEntries:
totalEntriesTooltip: Entradas totales totalEntriesTooltip: Entradas totales
daysOnward: Días de llegada en adelante daysOnward: Días de llegada en adelante
awb: AWB
summary: summary:
entryId: Id entrada entryId: Id entrada
freight: Porte freight: Porte
@ -819,62 +692,6 @@ travel:
destination: Destino destination: Destino
thermograph: Termógrafo thermograph: Termógrafo
travelFileDescription: 'Id envío { travelId }' travelFileDescription: 'Id envío { travelId }'
item:
descriptor:
buyer: Comprador
color: Color
category: Categoría
available: Disponible
warehouseText: 'Calculado sobre el almacén de { warehouseName }'
itemDiary: Registro de compra-venta
list:
id: Identificador
stems: Tallos
category: Reino
typeName: Tipo
isActive: Activo
weightByPiece: Peso (gramos)/tallo
userName: Comprador
stemMultiplier: Multiplicador
fixedPrice:
itemFk: ID Artículo
groupingPrice: Precio grouping
packingPrice: Precio packing
hasMinPrice: Tiene precio mínimo
minPrice: Precio min
started: Inicio
ended: Fin
create:
priority: Prioridad
summary:
otherData: Otros datos
tax: IVA
botanical: Botánico
barcode: Código de barras
completeName: Nombre completo
family: Familia
stems: Tallos
multiplier: Multiplicador
buyer: Comprador
doPhoto: Hacer foto
intrastatCode: Código intrastat
ref: Referencia
relevance: Relevancia
weight: Peso (gramos)/tallo
units: Unidades/caja
expense: Gasto
generic: Genérico
recycledPlastic: Plástico reciclado
nonRecycledPlastic: Plástico no reciclado
minSalesQuantity: Cantidad mínima de venta
genus: Genus
specie: Specie
buyRequest:
requester: Solicitante
requested: Solicitado
attender: Comprador
achieved: Conseguido
concept: Concepto
components: components:
topbar: {} topbar: {}
itemsFilterPanel: itemsFilterPanel:
@ -890,7 +707,11 @@ components:
wareHouseFk: Almacén wareHouseFk: Almacén
# LatestBuysFilter # LatestBuysFilter
salesPersonFk: Comprador salesPersonFk: Comprador
supplierFk: Proveedor
visible: Visible
active: Activo active: Activo
from: Desde
to: Hasta
floramondo: Floramondo floramondo: Floramondo
showBadDates: Ver items a futuro showBadDates: Ver items a futuro
userPanel: userPanel:

View File

@ -2,6 +2,8 @@
import { Dark, Quasar } from 'quasar'; import { Dark, Quasar } from 'quasar';
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { localeEquivalence } from 'src/i18n/index';
import quasarLang from 'src/utils/quasarLang';
const { t, locale } = useI18n(); const { t, locale } = useI18n();
@ -12,18 +14,9 @@ const userLocale = computed({
set(value) { set(value) {
locale.value = value; locale.value = value;
if (value === 'en') value = 'en-GB'; value = localeEquivalence[value] ?? value;
// FIXME: Dynamic imports from absolute paths are not compatible with vite: quasarLang(value);
// https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
try {
const langList = import.meta.glob('../../node_modules/quasar/lang/*.mjs');
langList[`../../node_modules/quasar/lang/${value}.mjs`]().then((lang) => {
Quasar.lang.set(lang.default);
});
} catch (error) {
//
}
}, },
}); });

View File

@ -1,13 +1,13 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue';
import FetchData from 'src/components/FetchData.vue';
import VnImg from 'src/components/ui/VnImg.vue'; import VnImg from 'src/components/ui/VnImg.vue';
import useHasAccount from 'src/composables/useHasAccount.js';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -23,6 +23,7 @@ const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const hasAccount = ref();
const setData = (entity) => (data.value = useCardDescription(entity.nickname, entity.id)); const setData = (entity) => (data.value = useCardDescription(entity.nickname, entity.id));
const filter = { const filter = {
@ -30,18 +31,16 @@ const filter = {
fields: ['id', 'nickname', 'name', 'role'], fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } }, include: { relation: 'role', scope: { fields: ['id', 'name'] } },
}; };
const hasAccount = ref(false);
onMounted(async () => {
hasAccount.value = await useHasAccount(entityId.value);
});
</script> </script>
<template> <template>
<FetchData
:url="`Accounts/${entityId}/exists`"
auto-load
@on-fetch="(data) => (hasAccount = data.exists)"
/>
<CardDescriptor <CardDescriptor
ref="descriptor" ref="descriptor"
url="VnUsers/preview" :url="`VnUsers/preview`"
:filter="filter" :filter="filter"
module="Account" module="Account"
@on-fetch="setData" @on-fetch="setData"
@ -50,7 +49,7 @@ const hasAccount = ref(false);
:subtitle="data.subtitle" :subtitle="data.subtitle"
> >
<template #menu> <template #menu>
<AccountDescriptorMenu :has-account="hasAccount" /> <AccountDescriptorMenu :entity-id="entityId" />
</template> </template>
<template #before> <template #before>
<VnImg :id="entityId" collection="user" resolution="520x520" class="photo"> <VnImg :id="entityId" collection="user" resolution="520x520" class="photo">
@ -74,7 +73,7 @@ const hasAccount = ref(false);
<VnLv :label="t('account.card.nickname')" :value="entity.name" /> <VnLv :label="t('account.card.nickname')" :value="entity.name" />
<VnLv :label="t('account.card.role')" :value="entity.role.name" /> <VnLv :label="t('account.card.role')" :value="entity.role.name" />
</template> </template>
<template #icons="{ entity }"> <template #actions="{ entity }">
<QCardActions class="q-gutter-x-md"> <QCardActions class="q-gutter-x-md">
<QIcon <QIcon
v-if="!entity.active" v-if="!entity.active"
@ -82,7 +81,7 @@ const hasAccount = ref(false);
name="vn:disabled" name="vn:disabled"
flat flat
round round
size="xs" size="sm"
class="fill-icon" class="fill-icon"
> >
<QTooltip>{{ t('account.card.deactivated') }}</QTooltip> <QTooltip>{{ t('account.card.deactivated') }}</QTooltip>
@ -93,7 +92,7 @@ const hasAccount = ref(false);
v-if="hasAccount" v-if="hasAccount"
flat flat
round round
size="xs" size="sm"
class="fill-icon" class="fill-icon"
> >
<QTooltip>{{ t('account.card.enabled') }}</QTooltip> <QTooltip>{{ t('account.card.enabled') }}</QTooltip>

View File

@ -1,15 +1,17 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { computed, ref, toRefs } from 'vue'; import { computed, onMounted, ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useAcl } from 'src/composables/useAcl'; import { useAcl } from 'src/composables/useAcl';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { useState } from 'src/composables/useState';
import VnConfirm from 'src/components/ui/VnConfirm.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
import VnChangePassword from 'src/components/common/VnChangePassword.vue'; import VnChangePassword from 'src/components/common/VnChangePassword.vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import VnInputPassword from 'src/components/common/VnInputPassword.vue'; import { useRouter } from 'vue-router';
const $props = defineProps({ const $props = defineProps({
hasAccount: { hasAccount: {
@ -18,14 +20,20 @@ const $props = defineProps({
required: true, required: true,
}, },
}); });
const { t } = useI18n(); const { t } = useI18n();
const { hasAccount } = toRefs($props); const { hasAccount } = toRefs($props);
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const state = useState();
const user = state.getUser();
const { notify } = useQuasar(); const { notify } = useQuasar();
const account = computed(() => useArrayData('AccountId').store.data[0]); const account = computed(() => useArrayData('AccountId').store.data[0]);
account.value.hasAccount = hasAccount.value; account.value.hasAccount = hasAccount.value;
const entityId = computed(() => +route.params.id); const entityId = computed(() => +route.params.id);
const hasitManagementAccess = ref();
const hasSysadminAccess = ref();
async function updateStatusAccount(active) { async function updateStatusAccount(active) {
if (active) { if (active) {
@ -37,7 +45,7 @@ async function updateStatusAccount(active) {
account.value.hasAccount = active; account.value.hasAccount = active;
const status = active ? 'enable' : 'disable'; const status = active ? 'enable' : 'disable';
notify({ notify({
message: t(`account.card.${status}Account.success`), message: t(`account.card.actions.${status}Account.success`),
type: 'positive', type: 'positive',
}); });
} }
@ -50,6 +58,17 @@ async function updateStatusUser(active) {
type: 'positive', type: 'positive',
}); });
} }
async function deleteAccount() {
const { data } = await axios.delete(`VnUsers/${entityId.value}`);
if (data) {
notify({
message: t('account.card.actions.delete.success'),
type: 'positive',
});
router.push({ name: 'AccountList' });
}
}
const showSyncDialog = ref(false); const showSyncDialog = ref(false);
const syncPassword = ref(null); const syncPassword = ref(null);
const shouldSyncPassword = ref(false); const shouldSyncPassword = ref(false);
@ -64,11 +83,27 @@ async function sync() {
type: 'positive', type: 'positive',
}); });
} }
const askOldPass = ref(false);
const changePassRef = ref();
const onChangePass = (oldPass) => {
askOldPass.value = oldPass;
changePassRef.value.show();
};
onMounted(() => {
hasitManagementAccess.value = useAcl().hasAny([
{ model: 'VnUser', props: 'higherPrivileges', accessType: 'WRITE' },
]);
hasSysadminAccess.value = useAcl().hasAny([
{ model: 'VnUser', props: 'adminUser', accessType: 'WRITE' },
]);
});
</script> </script>
<template> <template>
<VnChangePassword <VnChangePassword
ref="changePassRef" ref="changePassRef"
:ask-old-pass="true" :ask-old-pass="askOldPass"
:submit-fn=" :submit-fn="
async (newPassword, oldPassword) => { async (newPassword, oldPassword) => {
await axios.patch(`Accounts/change-password`, { await axios.patch(`Accounts/change-password`, {
@ -105,22 +140,51 @@ async function sync() {
class="full-width" class="full-width"
clearable clearable
clear-icon="close" clear-icon="close"
type="password"
/> />
</template> </template>
</VnConfirm> </VnConfirm>
<QItem <QItem
v-if=" v-if="hasitManagementAccess"
entityId == account.id &&
useAcl().hasAny([{ model: 'Account', props: '*', accessType: 'WRITE' }])
"
v-ripple v-ripple
clickable clickable
@click="$refs.changePassRef.show()" @click="
openConfirmationModal(
t('account.card.actions.disableAccount.title'),
t('account.card.actions.disableAccount.subtitle'),
() => deleteAccount()
)
"
> >
<QItemSection>{{ t('globals.changePass') }}</QItemSection> <QItemSection>{{ t('globals.delete') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem
v-if="account.hasAccount" v-if="hasSysadminAccess"
v-ripple
clickable
@click="user.id === account.id ? onChangePass(true) : onChangePass(false)"
>
<QItemSection v-if="user.id === account.id">
{{ t('globals.changePass') }}
</QItemSection>
<QItemSection v-else>{{ t('globals.setPass') }}</QItemSection>
</QItem>
<QItem
v-if="!account.hasAccount && hasSysadminAccess"
v-ripple
clickable
@click="
openConfirmationModal(
t('account.card.actions.enableAccount.title'),
t('account.card.actions.enableAccount.subtitle'),
() => updateStatusAccount(true)
)
"
>
<QItemSection>{{ t('account.card.actions.enableAccount.name') }}</QItemSection>
</QItem>
<QItem
v-if="account.hasAccount && hasSysadminAccess"
v-ripple v-ripple
clickable clickable
@click=" @click="
@ -135,7 +199,7 @@ async function sync() {
</QItem> </QItem>
<QItem <QItem
v-if="!account.active" v-if="!account.active && hasitManagementAccess"
v-ripple v-ripple
clickable clickable
@click=" @click="
@ -149,7 +213,7 @@ async function sync() {
<QItemSection>{{ t('account.card.actions.activateUser.name') }}</QItemSection> <QItemSection>{{ t('account.card.actions.activateUser.name') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem
v-if="account.active" v-if="account.active && hasitManagementAccess"
v-ripple v-ripple
clickable clickable
@click=" @click="
@ -162,7 +226,12 @@ async function sync() {
> >
<QItemSection>{{ t('account.card.actions.deactivateUser.name') }}</QItemSection> <QItemSection>{{ t('account.card.actions.deactivateUser.name') }}</QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable @click="showSyncDialog = true"> <QItem
v-if="useAcl().hasAny([{ model: 'VnRole', props: '*', accessType: 'WRITE' }])"
v-ripple
clickable
@click="showSyncDialog = true"
>
<QItemSection>{{ t('account.card.actions.sync.name') }}</QItemSection> <QItemSection>{{ t('account.card.actions.sync.name') }}</QItemSection>
</QItem> </QItem>
<QSeparator /> <QSeparator />

View File

@ -9,6 +9,7 @@ import AccountMailAliasCreateForm from './AccountMailAliasCreateForm.vue';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import useHasAccount from 'src/composables/useHasAccount.js';
import axios from 'axios'; import axios from 'axios';
const { t } = useI18n(); const { t } = useI18n();
@ -50,16 +51,6 @@ const columns = computed(() => [
}, },
]); ]);
const fetchAccountExistence = async () => {
try {
const { data } = await axios.get(`Accounts/${route.params.id}/exists`);
return data.exists;
} catch (error) {
console.error('Error fetching account existence', error);
return false;
}
};
const deleteMailAlias = async (row) => { const deleteMailAlias = async (row) => {
await axios.delete(`${urlPath}/${row.id}`); await axios.delete(`${urlPath}/${row.id}`);
fetchMailAliases(); fetchMailAliases();
@ -79,7 +70,7 @@ const fetchMailAliases = async () => {
const getAccountData = async (reload = true) => { const getAccountData = async (reload = true) => {
loading.value = true; loading.value = true;
hasAccount.value = await fetchAccountExistence(); hasAccount.value = await useHasAccount(route.params.id);
if (!hasAccount.value) { if (!hasAccount.value) {
loading.value = false; loading.value = false;
store.data = []; store.data = [];

View File

@ -9,6 +9,7 @@ import VnRow from 'components/ui/VnRow.vue';
import axios from 'axios'; import axios from 'axios';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import useHasAccount from 'src/composables/useHasAccount';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -30,23 +31,9 @@ const hasDataChanged = computed(
initialData.value.hasData !== hasData.value initialData.value.hasData !== hasData.value
); );
const fetchAccountExistence = async () => {
try {
const { data } = await axios.get(`Accounts/${route.params.id}/exists`);
return data.exists;
} catch (error) {
console.error('Error fetching account existence', error);
return false;
}
};
const fetchMailForwards = async () => { const fetchMailForwards = async () => {
try { const response = await axios.get(`MailForwards/${route.params.id}`);
const response = await axios.get(`MailForwards/${route.params.id}`); return response.data;
return response.data;
} catch {
return null;
}
}; };
const deleteMailForward = async () => { const deleteMailForward = async () => {
@ -72,7 +59,7 @@ const setInitialData = async () => {
loading.value = true; loading.value = true;
initialData.value.account = route.params.id; initialData.value.account = route.params.id;
formData.value.account = route.params.id; formData.value.account = route.params.id;
hasAccount.value = await fetchAccountExistence(route.params.id); hasAccount.value = await useHasAccount(route.params.id);
if (!hasAccount.value) { if (!hasAccount.value) {
loading.value = false; loading.value = false;
return; return;

View File

@ -7,6 +7,7 @@ import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import AccountDescriptorMenu from './AccountDescriptorMenu.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -31,12 +32,14 @@ const filter = {
<template> <template>
<CardSummary <CardSummary
data-key="AccountId" data-key="AccountId"
ref="AccountSummary"
url="VnUsers/preview" url="VnUsers/preview"
:filter="filter" :filter="filter"
@on-fetch="(data) => (account = data)" @on-fetch="(data) => (account = data)"
> >
<template #header>{{ account.id }} - {{ account.nickname }}</template> <template #header>{{ account.id }} - {{ account.nickname }}</template>
<template #menu="">
<AccountDescriptorMenu :entity-id="entityId" />
</template>
<template #body> <template #body>
<QCard class="vn-one"> <QCard class="vn-one">
<QCardSection class="q-pa-none"> <QCardSection class="q-pa-none">

View File

@ -4,7 +4,6 @@ import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useStateStore } from 'src/stores/useStateStore';
import { toDate, toPercentage, toCurrency } from 'filters/index'; import { toDate, toPercentage, toCurrency } from 'filters/index';
import { tMobile } from 'src/composables/tMobile'; import { tMobile } from 'src/composables/tMobile';
import CrudModel from 'src/components/CrudModel.vue'; import CrudModel from 'src/components/CrudModel.vue';
@ -13,11 +12,11 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue';
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const stateStore = computed(() => useStateStore());
const claim = ref(null); const claim = ref(null);
const claimRef = ref(); const claimRef = ref();
const claimId = route.params.id; const claimId = route.params.id;
@ -201,58 +200,62 @@ async function post(query, params) {
auto-load auto-load
@on-fetch="(data) => (destinationTypes = data)" @on-fetch="(data) => (destinationTypes = data)"
/> />
<Teleport to="#right-panel" v-if="stateStore.isHeaderMounted() && claim"> <RightMenu v-if="claim">
<QCard class="totalClaim q-my-md q-pa-sm no-box-shadow"> <template #right-panel>
{{ `${t('Total claimed')}: ${toCurrency(totalClaimed)}` }} <QCard class="totalClaim q-my-md q-pa-sm no-box-shadow">
</QCard> {{ `${t('Total claimed')}: ${toCurrency(totalClaimed)}` }}
<QCard class="q-mb-md q-pa-sm no-box-shadow"> </QCard>
<QItem class="justify-between"> <QCard class="q-mb-md q-pa-sm no-box-shadow">
<QItemLabel class="slider-container"> <QItem class="justify-between">
<p class="text-primary"> <QItemLabel class="slider-container">
{{ t('claim.actions') }} <p class="text-primary">
</p> {{ t('claim.actions') }}
<QSlider </p>
class="responsibility { 'background-color:primary': quasar.platform.is.mobile }" <QSlider
v-model="claim.responsibility" class="responsibility { 'background-color:primary': quasar.platform.is.mobile }"
:label-value="t('claim.responsibility')" v-model="claim.responsibility"
@change="(value) => save({ responsibility: value })" :label-value="t('claim.responsibility')"
label-always @change="(value) => save({ responsibility: value })"
color="primary" label-always
markers color="primary"
:marker-labels="marker_labels" markers
:min="DEFAULT_MIN_RESPONSABILITY" :marker-labels="marker_labels"
:max="DEFAULT_MAX_RESPONSABILITY" :min="DEFAULT_MIN_RESPONSABILITY"
:max="DEFAULT_MAX_RESPONSABILITY"
/>
</QItemLabel>
</QItem>
</QCard>
<QCard class="q-mb-md q-pa-sm no-box-shadow" style="margin-bottom: 1em">
<QItemLabel class="mana q-mb-md">
<QCheckbox
v-model="claim.isChargedToMana"
@update:model-value="(value) => save({ isChargedToMana: value })"
/> />
<span>{{ t('mana') }}</span>
</QItemLabel> </QItemLabel>
</QItem> </QCard>
</QCard> <QCard class="q-mb-md q-pa-sm no-box-shadow" style="position: static">
<QCard class="q-mb-md q-pa-sm no-box-shadow" style="margin-bottom: 1em"> <QInput
<QItemLabel class="mana q-mb-md"> :disable="
<QCheckbox !(
v-model="claim.isChargedToMana" claim.responsibility >=
@update:model-value="(value) => save({ isChargedToMana: value })" Math.ceil(DEFAULT_MAX_RESPONSABILITY) / 2
)
"
:label="t('confirmGreuges')"
class="q-field__native text-grey-2"
type="number"
placeholder="0"
id="multiplicatorValue"
name="multiplicatorValue"
min="0"
max="50"
v-model="multiplicatorValue"
/> />
<span>{{ t('mana') }}</span> </QCard>
</QItemLabel> </template>
</QCard> </RightMenu>
<QCard class="q-mb-md q-pa-sm no-box-shadow" style="position: static">
<QInput
:disable="
!(claim.responsibility >= Math.ceil(DEFAULT_MAX_RESPONSABILITY) / 2)
"
:label="t('confirmGreuges')"
class="q-field__native text-grey-2"
type="number"
placeholder="0"
id="multiplicatorValue"
name="multiplicatorValue"
min="0"
max="50"
v-model="multiplicatorValue"
/>
</QCard>
</Teleport>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()"> </Teleport>
<CrudModel <CrudModel
v-if="claim" v-if="claim"
data-key="ClaimEnds" data-key="ClaimEnds"

View File

@ -1,21 +1,13 @@
<script setup> <script setup>
import VnCard from 'components/common/VnCard.vue'; import VnCardBeta from 'components/common/VnCardBeta.vue';
import ClaimDescriptor from './ClaimDescriptor.vue'; import ClaimDescriptor from './ClaimDescriptor.vue';
import ClaimFilter from '../ClaimFilter.vue';
import filter from './ClaimFilter.js'; import filter from './ClaimFilter.js';
</script> </script>
<template> <template>
<VnCard <VnCardBeta
data-key="Claim" data-key="Claim"
base-url="Claims" base-url="Claims"
:descriptor="ClaimDescriptor" :descriptor="ClaimDescriptor"
:filter-panel="ClaimFilter"
search-data-key="ClaimList"
:filter="filter" :filter="filter"
:searchbar-props="{
url: 'Claims/filter',
label: 'Search claim',
info: 'You can search by claim id or customer name',
}"
/> />
</template> </template>

View File

@ -19,6 +19,7 @@ import ClaimNotes from 'src/pages/Claim/Card/ClaimNotes.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ClaimDescriptorMenu from './ClaimDescriptorMenu.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -228,6 +229,9 @@ function claimUrl(section) {
</QList> </QList>
</QBtnDropdown> </QBtnDropdown>
</template> </template>
<template #menu="{ entity }">
<ClaimDescriptorMenu :claim="entity.claim" />
</template>
<template #body="{ entity: { claim, salesClaimed, developments } }"> <template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one" v-if="$route.name != 'ClaimSummary'"> <QCard class="vn-one" v-if="$route.name != 'ClaimSummary'">
<VnTitle <VnTitle

View File

@ -2,18 +2,18 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate } from 'filters/index'; import { toDate } from 'filters/index';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import ClaimFilter from './ClaimFilter.vue'; import ClaimFilter from './ClaimFilter.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ClaimSummary from './Card/ClaimSummary.vue'; import ClaimSummary from './Card/ClaimSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
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 ZoneDescriptorProxy from '../Zone/Card/ZoneDescriptorProxy.vue'; import ZoneDescriptorProxy from '../Zone/Card/ZoneDescriptorProxy.vue';
import VnSection from 'src/components/common/VnSection.vue';
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const dataKey = 'ClaimList';
const claimFilterRef = ref(); const claimFilterRef = ref();
const columns = computed(() => [ const columns = computed(() => [
@ -125,48 +125,50 @@ const STATE_COLOR = {
</script> </script>
<template> <template>
<VnSearchbar <VnSection
data-key="ClaimList" :data-key="dataKey"
:label="t('Search claim')" :columns="columns"
:info="t('You can search by claim id or customer name')" prefix="claim"
/> :array-data-props="{
<RightMenu> url: 'Claims/filter',
<template #right-panel> order: ['cs.priority ASC', 'created ASC'],
}"
>
<template #advanced-menu>
<ClaimFilter data-key="ClaimList" ref="claimFilterRef" /> <ClaimFilter data-key="ClaimList" ref="claimFilterRef" />
</template> </template>
</RightMenu> <template #body>
<VnTable <VnTable
data-key="ClaimList" :data-key="dataKey"
url="Claims/filter" :columns="columns"
:order="['cs.priority ASC', 'created ASC']" redirect="claim"
:columns="columns" :right-search="false"
redirect="claim" auto-load
:right-search="false" >
> <template #column-clientFk="{ row }">
<template #column-clientFk="{ row }"> <span class="link" @click.stop>
<span class="link" @click.stop> {{ row.clientName }}
{{ row.clientName }} <CustomerDescriptorProxy :id="row.clientFk" />
<CustomerDescriptorProxy :id="row.clientFk" /> </span>
</span> </template>
<template #column-attendedBy="{ row }">
<span @click.stop>
<VnUserLink :name="row.workerName" :worker-id="row.workerFk" />
</span>
</template>
<template #column-zoneFk="{ row }">
<span class="link" @click.stop>
{{ row.zoneName }}
<ZoneDescriptorProxy :id="row.zoneId" />
</span>
</template>
</VnTable>
</template> </template>
<template #column-attendedBy="{ row }"> </VnSection>
<span @click.stop>
<VnUserLink :name="row.workerName" :worker-id="row.workerFk" />
</span>
</template>
<template #column-zoneFk="{ row }">
<span class="link" @click.stop>
{{ row.zoneName }}
<ZoneDescriptorProxy :id="row.zoneId" />
</span>
</template>
</VnTable>
</template> </template>
<i18n> <i18n>
es: es:
Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
params: params:
stateCode: Estado stateCode: Estado
en: en:

View File

@ -44,3 +44,5 @@ claim:
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}' fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}'
noData: 'There are no images/videos, click here or drag and drop the file' noData: 'There are no images/videos, click here or drag and drop the file'
dragDrop: Drag and drop it here dragDrop: Drag and drop it here
search: Search claims
searchInfo: You can search by claim id or customer name

View File

@ -1,5 +1,3 @@
Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
claim: claim:
customer: Cliente customer: Cliente
code: Código code: Código
@ -46,3 +44,5 @@ claim:
fileDescription: 'ID de reclamación {claimId} del cliente {clientName} con ID {clientId}' fileDescription: 'ID de reclamación {claimId} del cliente {clientName} con ID {clientId}'
noData: 'No hay imágenes/videos, haz clic aquí o arrastra y suelta el archivo' noData: 'No hay imágenes/videos, haz clic aquí o arrastra y suelta el archivo'
dragDrop: Arrastra y suelta aquí dragDrop: Arrastra y suelta aquí
search: Buscar reclamación
searchInfo: Puedes buscar por ID de la reclamación o nombre del cliente

View File

@ -38,7 +38,7 @@ const getBankEntities = (data, formData) => {
hide-selected hide-selected
option-label="name" option-label="name"
option-value="id" option-value="id"
v-model="data.payMethod" v-model="data.payMethodFk"
/> />
<VnInput :label="t('Due day')" clearable v-model="data.dueDay" /> <VnInput :label="t('Due day')" clearable v-model="data.dueDay" />
</VnRow> </VnRow>

View File

@ -1,25 +1,12 @@
<script setup> <script setup>
import { computed } from 'vue'; import VnCardBeta from 'components/common/VnCardBeta.vue';
import { useRoute } from 'vue-router';
import VnCard from 'components/common/VnCard.vue';
import CustomerDescriptor from './CustomerDescriptor.vue'; import CustomerDescriptor from './CustomerDescriptor.vue';
import CustomerFilter from '../CustomerFilter.vue';
const route = useRoute();
const routeName = computed(() => route.name);
</script> </script>
<template> <template>
<VnCard <VnCardBeta
data-key="Client" data-key="Client"
base-url="Clients" base-url="Clients"
:descriptor="CustomerDescriptor" :descriptor="CustomerDescriptor"
:filter-panel="routeName != 'CustomerConsumption' && CustomerFilter"
search-data-key="CustomerList"
:searchbar-props="{
url: 'Clients/filter',
label: 'Search customer',
info: 'You can search by customer id or name',
}"
/> />
</template> </template>

View File

@ -152,7 +152,8 @@ const updateDateParams = (value, params) => {
v-if="campaignList" v-if="campaignList"
data-key="CustomerConsumption" data-key="CustomerConsumption"
url="Clients/consumption" url="Clients/consumption"
:order="['itemTypeFk', 'itemName', 'itemSize', 'description']" :order="['itemTypeFk', 'itemName', 'itemSize', 'description']"
:filter="{ where: { clientFk: route.params.id } }"
:columns="columns" :columns="columns"
search-url="consumption" search-url="consumption"
:user-params="userParams" :user-params="userParams"

View File

@ -1,177 +0,0 @@
<script setup>
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { QItem } from 'quasar';
import VnSelect from 'src/components/common/VnSelect.vue';
import { QItemSection } from 'quasar';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import { toDate } from 'src/filters';
const { t } = useI18n();
defineProps({ dataKey: { type: String, required: true } });
</script>
<template>
<VnFilterPanel :data-key="dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params }">
<QItem>
<QItemSection>
<VnInput
:label="t('params.item')"
v-model="params.itemId"
is-outlined
lazy-rules
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
v-model="params.buyerId"
url="TicketRequests/getItemTypeWorker"
:fields="['id', 'nickname']"
sort-by="nickname ASC"
:label="t('params.buyer')"
option-value="id"
option-label="nickname"
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
v-model="params.typeId"
url="ItemTypes"
:include="['category']"
:fields="['id', 'name', 'categoryFk']"
sort-by="name ASC"
:label="t('params.typeId')"
option-label="name"
option-value="id"
dense
outlined
rounded
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>{{
scope.opt?.category?.name
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
v-model="params.categoryId"
url="ItemCategories"
:fields="['id', 'name']"
sort-by="name ASC"
:label="t('params.categoryId')"
option-label="name"
option-value="id"
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
v-model="params.campaignId"
url="Campaigns/latest"
sort-by="dated DESC"
:label="t('params.campaignId')"
option-label="code"
option-value="id"
dense
outlined
rounded
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
t(`params.${scope.opt?.code}`)
}}</QItemLabel>
<QItemLabel caption>{{
toDate(scope.opt.dated)
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('params.from')"
v-model="params.from"
@update:model-value="searchFn()"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('params.to')"
v-model="params.to"
@update:model-value="searchFn()"
is-outlined
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
item: Item id
buyer: Buyer
type: Type
category: Category
itemId: Item id
buyerId: Buyer
typeId: Type
categoryId: Category
from: From
to: To
campaignId: Campaña
valentinesDay: Valentine's Day
mothersDay: Mother's Day
allSaints: All Saints' Day
es:
params:
item: Id artículo
buyer: Comprador
type: Tipo
category: Categoría
itemId: Id Artículo
buyerId: Comprador
typeId: Tipo
categoryId: Reino
from: Desde
to: Hasta
campaignId: Campaña
valentinesDay: Día de San Valentín
mothersDay: Día de la Madre
allSaints: Día de Todos los Santos
</i18n>

View File

@ -59,6 +59,7 @@ const columns = computed(() => [
</script> </script>
<template> <template>
<VnTable <VnTable
:user-filter="{ include: filter.include }"
ref="tableRef" ref="tableRef"
data-key="ClientCredit" data-key="ClientCredit"
url="ClientCredits" url="ClientCredits"

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -11,16 +11,9 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue'; import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue';
import { useState } from 'src/composables/useState';
const state = useState();
const customer = ref();
onMounted(async () => {
customer.value = state.get('customer');
if (customer.value) customer.value.webAccess = data.value?.account?.isActive;
});
const customerDebt = ref();
const customerCredit = ref();
const $props = defineProps({ const $props = defineProps({
id: { id: {
type: Number, type: Number,
@ -42,10 +35,12 @@ const entityId = computed(() => {
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => { const setData = (entity) => {
customerDebt.value = entity?.debt;
customerCredit.value = entity?.credit;
data.value = useCardDescription(entity?.name, entity?.id); data.value = useCardDescription(entity?.name, entity?.id);
}; };
const debtWarning = computed(() => { const debtWarning = computed(() => {
return customer.value?.debt > customer.value?.credit ? 'negative' : 'primary'; return customerDebt.value > customerCredit.value ? 'negative' : 'primary';
}); });
</script> </script>
@ -97,26 +92,21 @@ const debtWarning = computed(() => {
:value="entity.businessType.description" :value="entity.businessType.description"
/> />
</template> </template>
<template #icons> <template #icons="{ entity }">
<QCardActions v-if="customer" class="q-gutter-x-md"> <QCardActions class="q-gutter-x-md">
<QIcon <QIcon
v-if="!customer.isActive" v-if="!entity.isActive"
name="vn:disabled" name="vn:disabled"
size="xs" size="xs"
color="primary" color="primary"
> >
<QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip> <QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon v-if="entity.isFreezed" name="vn:frozen" size="xs" color="primary">
v-if="customer.isFreezed"
name="vn:frozen"
size="xs"
color="primary"
>
<QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip> <QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
v-if="!customer.account?.active" v-if="!entity.account?.active"
color="primary" color="primary"
name="vn:noweb" name="vn:noweb"
size="xs" size="xs"
@ -124,7 +114,7 @@ const debtWarning = computed(() => {
<QTooltip>{{ t('customer.card.webAccountInactive') }}</QTooltip> <QTooltip>{{ t('customer.card.webAccountInactive') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
v-if="customer.debt > customer.credit" v-if="entity.debt > entity.credit"
name="vn:risk" name="vn:risk"
size="xs" size="xs"
:color="debtWarning" :color="debtWarning"
@ -132,7 +122,7 @@ const debtWarning = computed(() => {
<QTooltip>{{ t('customer.card.hasDebt') }}</QTooltip> <QTooltip>{{ t('customer.card.hasDebt') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
v-if="!customer.isTaxDataChecked" v-if="!entity.isTaxDataChecked"
name="vn:no036" name="vn:no036"
size="xs" size="xs"
color="primary" color="primary"
@ -140,7 +130,7 @@ const debtWarning = computed(() => {
<QTooltip>{{ t('customer.card.notChecked') }}</QTooltip> <QTooltip>{{ t('customer.card.notChecked') }}</QTooltip>
</QIcon> </QIcon>
<QBtn <QBtn
v-if="customer.unpaid" v-if="entity.unpaid"
flat flat
size="sm" size="sm"
icon="vn:Client_unpaid" icon="vn:Client_unpaid"
@ -197,14 +187,18 @@ const debtWarning = computed(() => {
</QBtn> </QBtn>
<QBtn <QBtn
:to="{ :to="{
name: 'AccountSummary', name: 'OrderList',
params: { id: entity.id }, query: {
createForm: JSON.stringify({
clientFk: entity.id,
}),
},
}" }"
size="md" size="md"
icon="face" icon="vn:basketadd"
color="primary" color="primary"
> >
<QTooltip>{{ t('Go to user') }}</QTooltip> <QTooltip>{{ t('globals.pageTitles.createOrder') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
v-if="entity.supplier" v-if="entity.supplier"
@ -228,14 +222,9 @@ en:
unpaidDated: 'Date {dated}' unpaidDated: 'Date {dated}'
unpaidAmount: 'Amount {amount}' unpaidAmount: 'Amount {amount}'
es: es:
Go to module index: Ir al índice del módulo
Customer ticket list: Listado de tickets del cliente Customer ticket list: Listado de tickets del cliente
Customer invoice out list: Listado de facturas del cliente Customer invoice out list: Listado de facturas del cliente
New order: Nuevo pedido
New ticket: Nuevo ticket
Go to user: Ir al usuario
Go to supplier: Ir al proveedor Go to supplier: Ir al proveedor
Customer unpaid: Cliente impago
Unpaid: Impagado Unpaid: Impagado
unpaidDated: 'Fecha {dated}' unpaidDated: 'Fecha {dated}'
unpaidAmount: 'Importe {amount}' unpaidAmount: 'Importe {amount}'

View File

@ -51,7 +51,6 @@ const openCreateForm = (type) => {
}; };
const clientFk = { const clientFk = {
ticket: 'clientId', ticket: 'clientId',
order: 'clientFk',
}; };
const key = clientFk[type]; const key = clientFk[type];
if (!key) return; if (!key) return;
@ -70,11 +69,6 @@ const openCreateForm = (type) => {
{{ t('globals.pageTitles.createTicket') }} {{ t('globals.pageTitles.createTicket') }}
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable @click="openCreateForm('order')">
<QItemSection>
{{ t('globals.pageTitles.createOrder') }}
</QItemSection>
</QItem>
<QItem v-ripple clickable> <QItem v-ripple clickable>
<QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection> <QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection>
</QItem> </QItem>

View File

@ -44,6 +44,7 @@ function handleLocation(data, location) {
:required="true" :required="true"
:rules="validate('client.socialName')" :rules="validate('client.socialName')"
clearable clearable
uppercase="true"
v-model="data.socialName" v-model="data.socialName"
> >
<template #append> <template #append>

View File

@ -84,6 +84,7 @@ const columns = computed(() => [
component: 'number', component: 'number',
autofocus: true, autofocus: true,
required: true, required: true,
positive: false,
}, },
format: ({ amount }) => toCurrency(amount), format: ({ amount }) => toCurrency(amount),
create: true, create: true,

View File

@ -12,6 +12,7 @@ import VnLinkMail from 'src/components/ui/VnLinkMail.vue';
import CustomerSummaryTable from 'src/pages/Customer/components/CustomerSummaryTable.vue'; import CustomerSummaryTable from 'src/pages/Customer/components/CustomerSummaryTable.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import VnRow from 'src/components/ui/VnRow.vue'; import VnRow from 'src/components/ui/VnRow.vue';
import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const grafanaUrl = 'https://grafana.verdnatura.es'; const grafanaUrl = 'https://grafana.verdnatura.es';
@ -70,6 +71,9 @@ const sumRisk = ({ clientRisks }) => {
data-key="CustomerSummary" data-key="CustomerSummary"
module-name="Customer" module-name="Customer"
> >
<template #menu="{ entity }">
<CustomerDescriptorMenu :customer="entity" />
</template>
<template #body="{ entity }"> <template #body="{ entity }">
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle <VnTitle
@ -94,14 +98,13 @@ const sumRisk = ({ clientRisks }) => {
:phone-number="entity.mobile" :phone-number="entity.mobile"
:channel="entity.country?.saySimpleCountry?.channel" :channel="entity.country?.saySimpleCountry?.channel"
class="q-ml-xs" class="q-ml-xs"
:country="entity.country?.code"
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :value="entity.email" copy <VnLv :value="entity.email" copy
><template #label> ><template #label>
{{ t('globals.params.email') }} {{ t('globals.params.email') }}
<VnLinkMail :email="entity.email"></VnLinkMail> </template <VnLinkMail email="entity.email"></VnLinkMail> </template
></VnLv> ></VnLv>
<VnLv <VnLv
:label="t('customer.summary.salesPerson')" :label="t('customer.summary.salesPerson')"
@ -173,7 +176,7 @@ const sumRisk = ({ clientRisks }) => {
:label="t('customer.summary.notifyByEmail')" :label="t('customer.summary.notifyByEmail')"
:value="entity.isToBeMailed" :value="entity.isToBeMailed"
/> />
<VnLv :label="t('globals.isVies')" :value="entity.isVies" /> <VnLv :label="t('customer.summary.vies')" :value="entity.isVies" />
</VnRow> </VnRow>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">

View File

@ -50,7 +50,8 @@ const filterClientFindOne = {
> >
<template #form="{ data }"> <template #form="{ data }">
<VnRow> <VnRow>
<QCheckbox :label="t('Unpaid client')" v-model="data.unpaid" /> <QCheckbox :label="t('Unpaid client')" v-model="data.unpaid"
data-cy="UnpaidCheckBox" />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md" v-show="data.unpaid"> <VnRow class="row q-gutter-md q-mb-md" v-show="data.unpaid">

View File

@ -5,18 +5,19 @@ import { useRouter } from 'vue-router';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import RightMenu from 'src/components/common/RightMenu.vue';
import CustomerSummary from './Card/CustomerSummary.vue'; import CustomerSummary from './Card/CustomerSummary.vue';
import CustomerFilter from './CustomerFilter.vue'; import CustomerFilter from './CustomerFilter.vue';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import VnLocation from 'src/components/common/VnLocation.vue'; import VnLocation from 'src/components/common/VnLocation.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
import VnSection from 'src/components/common/VnSection.vue';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const tableRef = ref(); const tableRef = ref();
const dataKey = 'CustomerList';
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
@ -50,6 +51,14 @@ const columns = computed(() => [
isTitle: true, isTitle: true,
create: true, create: true,
columnClass: 'expand', columnClass: 'expand',
attrs: {
uppercase: true,
},
columnFilter: {
attrs: {
uppercase: false,
},
},
}, },
{ {
align: 'left', align: 'left',
@ -390,82 +399,91 @@ function handleLocation(data, location) {
</script> </script>
<template> <template>
<VnSearchbar <VnSection
:info="t('You can search by customer id or name')" :data-key="dataKey"
:label="t('Search customer')" :columns="columns"
data-key="CustomerList" prefix="customer"
/> :array-data-props="{
<RightMenu> url: 'Clients/filter',
<template #right-panel> order: ['id DESC'],
}"
>
<template #advanced-menu>
<CustomerFilter data-key="CustomerList" /> <CustomerFilter data-key="CustomerList" />
</template> </template>
</RightMenu> <template #body>
<VnTable <VnTable
ref="tableRef" ref="tableRef"
data-key="CustomerList" :data-key="dataKey"
url="Clients/filter" url="Clients/filter"
order="id DESC" :create="{
:create="{ urlCreate: 'Clients/createWithUser',
urlCreate: 'Clients/createWithUser', title: t('globals.pageTitles.customerCreate'),
title: t('globals.pageTitles.customerCreate'), onDataSaved: ({ id }) => tableRef.redirect(id),
onDataSaved: ({ id }) => tableRef.redirect(id), formInitialData: {
formInitialData: { active: true,
active: true, isEqualizated: false,
isEqualizated: false, },
},
}"
:columns="columns"
:right-search="false"
redirect="customer"
>
<template #more-create-dialog="{ data }">
<VnSelectWorker
:label="t('customer.summary.salesPerson')"
v-model="data.salesPersonFk"
:params="{
departmentCodes: ['VT', 'shopping'],
}" }"
:has-avatar="true" :columns="columns"
:id-value="data.salesPersonFk" :right-search="false"
emit-value redirect="customer"
auto-load
> >
<template #prepend> <template #more-create-dialog="{ data }">
<VnAvatar <VnSelectWorker
:worker-id="data.salesPersonFk" :label="t('customer.summary.salesPerson')"
color="primary" v-model="data.salesPersonFk"
:title="title" :params="{
departmentCodes: ['VT', 'shopping'],
}"
:has-avatar="true"
:id-value="data.salesPersonFk"
emit-value
auto-load
>
<template #prepend>
<VnAvatar
:worker-id="data.salesPersonFk"
color="primary"
:title="title"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt?.nickname }},
{{ scope.opt?.code }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectWorker>
<VnLocation
:acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
/> />
<QInput v-model="data.userName" :label="t('Web user')" />
<QInput
:label="t('Email')"
clearable
type="email"
v-model="data.email"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip max-width="400px">{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</QInput>
</template> </template>
<template #option="scope"> </VnTable>
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt?.nickname }},
{{ scope.opt?.code }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectWorker>
<VnLocation
:acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
/>
<QInput v-model="data.userName" :label="t('Web user')" />
<QInput :label="t('Email')" clearable type="email" v-model="data.email">
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip max-width="400px">{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</QInput>
</template> </template>
</VnTable> </VnSection>
</template> </template>
<i18n> <i18n>
es: es:

View File

@ -11,14 +11,14 @@ import VnInput from 'src/components/common/VnInput.vue';
import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue'; import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import { useArrayData } from 'src/composables/useArrayData';
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const dataRef = ref(null); const dataRef = ref(null);
const balanceDueTotal = ref(0);
const selected = ref([]); const selected = ref([]);
const arrayData = useArrayData('CustomerDefaulter');
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
@ -165,26 +165,37 @@ const viewAddObservation = (rowsSelected) => {
}); });
}; };
const onFetch = async (data) => {
balanceDueTotal.value = data.reduce((acc, { amount = 0 }) => acc + amount, 0);
};
function exprBuilder(param, value) { function exprBuilder(param, value) {
switch (param) { switch (param) {
case 'clientFk':
return { [`d.${param}`]: value };
case 'creditInsurance':
case 'amount':
case 'workerFk':
case 'departmentFk':
case 'countryFk':
case 'payMethod':
case 'salesPersonFk': case 'salesPersonFk':
case 'creditInsurance':
case 'countryFk':
return { [`c.${param}`]: value };
case 'payMethod':
return { [`c.payMethodFk`]: value };
case 'workerFk':
return { [`co.${param}`]: value };
case 'departmentFk':
return { [`wd.${param}`]: value };
case 'amount':
case 'clientFk':
return { [`d.${param}`]: value }; return { [`d.${param}`]: value };
case 'created': case 'created':
return { 'd.created': { between: dateRange(value) } }; return { 'd.created': { between: dateRange(value) } };
case 'defaulterSinced': case 'defaulterSinced':
return { 'd.defaulterSinced': { between: dateRange(value) } }; return { 'd.defaulterSinced': { between: dateRange(value) } };
case 'isWorker': {
if (value == undefined) return;
const search = value ? 'worker' : { neq: 'worker' };
return { 'c.businessTypeFk': search };
}
case 'hasRecovery': {
if (value == undefined) return;
const search = value ? null : { neq: null };
return { 'r.finished': search };
}
case 'observation':
return { 'co.text': { like: `%${value}%` } };
} }
} }
</script> </script>
@ -192,7 +203,7 @@ function exprBuilder(param, value) {
<template> <template>
<VnSubToolbar> <VnSubToolbar>
<template #st-data> <template #st-data>
<CustomerBalanceDueTotal :amount="balanceDueTotal" /> <CustomerBalanceDueTotal :amount="arrayData.store.data?.amount" />
</template> </template>
<template #st-actions> <template #st-actions>
<QBtn <QBtn
@ -211,8 +222,6 @@ function exprBuilder(param, value) {
url="Defaulters/filter" url="Defaulters/filter"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
:columns="columns" :columns="columns"
@on-fetch="onFetch"
:use-model="true"
:table="{ :table="{
'row-key': 'clientFk', 'row-key': 'clientFk',
selection: 'multiple', selection: 'multiple',
@ -221,6 +230,7 @@ function exprBuilder(param, value) {
:disable-option="{ card: true }" :disable-option="{ card: true }"
auto-load auto-load
:order="['amount DESC']" :order="['amount DESC']"
key-data="defaulters"
> >
<template #column-clientFk="{ row }"> <template #column-clientFk="{ row }">
<span class="link" @click.stop> <span class="link" @click.stop>

View File

@ -11,6 +11,7 @@ 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 CustomerNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue'; import CustomerNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -150,6 +151,22 @@ function onAgentCreated({ id, fiscalName }, data) {
</template> </template>
</VnSelectDialog> </VnSelectDialog>
</VnRow> </VnRow>
<VnRow>
<VnInputNumber
:label="t('Longitude')"
clearable
v-model="data.longitude"
:decimal-places="7"
:positive="false"
/>
<VnInputNumber
:label="t('Latitude')"
clearable
v-model="data.latitude"
:decimal-places="7"
:positive="false"
/>
</VnRow>
</template> </template>
</FormModel> </FormModel>
</template> </template>
@ -175,4 +192,6 @@ es:
Mobile: Movíl Mobile: Movíl
Incoterms: Incoterms Incoterms: Incoterms
Customs agent: Agente de aduanas Customs agent: Agente de aduanas
Longitude: Longitud
Latitude: Latitud
</i18n> </i18n>

View File

@ -247,8 +247,16 @@ function handleLocation(data, location) {
:label="t('Longitude')" :label="t('Longitude')"
clearable clearable
v-model="data.longitude" v-model="data.longitude"
:decimal-places="7"
:positive="false"
/>
<VnInputNumber
:label="t('Latitude')"
clearable
v-model="data.latitude"
:decimal-places="7"
:positive="false"
/> />
<VnInputNumber :label="t('Latitude')" clearable v-model="data.latitude" />
</VnRow> </VnRow>
<h4 class="q-mb-xs">{{ t('Notes') }}</h4> <h4 class="q-mb-xs">{{ t('Notes') }}</h4>
<VnRow <VnRow

View File

@ -214,7 +214,7 @@ const toCustomerSamples = () => {
<template #custom-buttons> <template #custom-buttons>
<QBtn <QBtn
:disabled="isLoading || !sampleType?.hasPreview" :disabled="isLoading || !sampleType?.hasPreview"
:label="t('Preview')" :label="t('globals.preview')"
:loading="isLoading" :loading="isLoading"
@click.stop="getPreview()" @click.stop="getPreview()"
color="primary" color="primary"
@ -353,7 +353,6 @@ es:
Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla
To who should the recipient replay?: ¿A quien debería responder el destinatario? To who should the recipient replay?: ¿A quien debería responder el destinatario?
Edit address: Editar dirección Edit address: Editar dirección
Preview: Vista previa
Email cannot be blank: Debes introducir un email Email cannot be blank: Debes introducir un email
Choose a sample: Selecciona una plantilla Choose a sample: Selecciona una plantilla
Choose a company: Selecciona una empresa Choose a company: Selecciona una empresa

View File

@ -94,6 +94,8 @@ customer:
hasToInvoiceByAddress: Invoice by address hasToInvoiceByAddress: Invoice by address
isToBeMailed: Mailing isToBeMailed: Mailing
hasSepaVnl: VNL B2B received hasSepaVnl: VNL B2B received
search: Search customer
searchInfo: You can search by customer ID
params: params:
id: Id id: Id
isWorker: Is Worker isWorker: Is Worker

View File

@ -1,5 +1,3 @@
Search customer: Buscar cliente
You can search by customer id or name: Puedes buscar por id o nombre del cliente
customer: customer:
card: card:
debt: Riesgo debt: Riesgo
@ -96,6 +94,8 @@ customer:
hasToInvoiceByAddress: Factura por consigna hasToInvoiceByAddress: Factura por consigna
isToBeMailed: Env. emails isToBeMailed: Env. emails
hasSepaVnl: Recibido B2B VNL hasSepaVnl: Recibido B2B VNL
search: Buscar cliente
searchInfo: Puedes buscar por id o nombre del cliente
params: params:
id: ID id: ID
isWorker: Es trabajador isWorker: Es trabajador

View File

@ -106,7 +106,7 @@ const { openConfirmationModal } = useVnConfirm();
:to="{ :to="{
name: 'WorkerList', name: 'WorkerList',
query: { query: {
params: JSON.stringify({ departmentFk: entityId }), table: JSON.stringify({ departmentFk: entityId }),
}, },
}" }"
> >

View File

@ -3,7 +3,6 @@ import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
@ -11,7 +10,7 @@ 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 FilterTravelForm from 'src/components/FilterTravelForm.vue'; import FilterTravelForm from 'src/components/FilterTravelForm.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
const route = useRoute(); const route = useRoute();
@ -26,6 +25,7 @@ const onFilterTravelSelected = (formData, id) => {
formData.travelFk = id; formData.travelFk = id;
}; };
</script> </script>
<template> <template>
<FetchData <FetchData
ref="companiesRef" ref="companiesRef"
@ -93,14 +93,13 @@ const onFilterTravelSelected = (formData, id) => {
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
<QItemSection> <QItemSection>
<QItemLabel <QItemLabel>
>{{ scope.opt?.agencyModeName }} - {{ scope.opt?.agencyModeName }} -
{{ scope.opt?.warehouseInName }} ({{ {{ scope.opt?.warehouseInName }}
toDate(scope.opt?.shipped) ({{ toDate(scope.opt?.shipped) }})
}}) &#x2192; {{ scope.opt?.warehouseOutName }} ({{ {{ scope.opt?.warehouseOutName }}
toDate(scope.opt?.landed) ({{ toDate(scope.opt?.landed) }})
}})</QItemLabel </QItemLabel>
>
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
@ -126,6 +125,13 @@ const onFilterTravelSelected = (formData, id) => {
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnInputNumber
:label="t('entry.summary.commission')"
v-model="data.commission"
step="1"
autofocus
:positive="false"
/>
<VnSelect <VnSelect
:label="t('entry.summary.currency')" :label="t('entry.summary.currency')"
v-model="data.currencyFk" v-model="data.currencyFk"
@ -133,12 +139,23 @@ const onFilterTravelSelected = (formData, id) => {
option-value="id" option-value="id"
option-label="code" option-label="code"
/> />
<QInput </VnRow>
:label="t('entry.summary.commission')" <VnRow>
v-model="data.commission" <VnInputNumber
type="number" v-model="data.initialTemperature"
autofocus name="initialTemperature"
min="0" :label="t('entry.basicData.initialTemperature')"
:step="0.5"
:decimal-places="2"
:positive="false"
/>
<VnInputNumber
v-model="data.finalTemperature"
name="finalTemperature"
:label="t('entry.basicData.finalTemperature')"
:step="0.5"
:decimal-places="2"
:positive="false"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>

View File

@ -1,21 +1,13 @@
<script setup> <script setup>
import VnCard from 'components/common/VnCard.vue'; import VnCardBeta from 'components/common/VnCardBeta.vue';
import EntryDescriptor from './EntryDescriptor.vue'; import EntryDescriptor from './EntryDescriptor.vue';
import EntryFilter from '../EntryFilter.vue'; import filter from './EntryFilter.js'
import filter from './EntryFilter.js';
</script> </script>
<template> <template>
<VnCard <VnCardBeta
data-key="Entry" data-key="Entry"
base-url="Entries" base-url="Entries"
:filter="filter"
:descriptor="EntryDescriptor" :descriptor="EntryDescriptor"
:filter-panel="EntryFilter" :user-filter="filter"
search-data-key="EntryList"
:searchbar-props="{
url: 'Entries/filter',
label: 'Search entries',
info: 'You can search by entry reference',
}"
/> />
</template> </template>

View File

@ -7,9 +7,9 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import { usePrintService } from 'composables/usePrintService';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import filter from './EntryFilter.js'; import filter from './EntryFilter.js';
import EntryDescriptorMenu from './EntryDescriptorMenu.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -21,7 +21,6 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { openReport } = usePrintService();
const entryDescriptorRef = ref(null); const entryDescriptorRef = ref(null);
const url = ref(); const url = ref();
@ -52,10 +51,6 @@ const getEntryRedirectionFilter = (entry) => {
to, to,
}); });
}; };
const showEntryReport = () => {
openReport(`Entries/${route.params.id}/entry-order-pdf`);
};
</script> </script>
<template> <template>
@ -68,14 +63,12 @@ const showEntryReport = () => {
data-key="Entry" data-key="Entry"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">
<QItem v-ripple clickable @click="showEntryReport(entity)"> <EntryDescriptorMenu :id="entity.id" />
<QItemSection>{{ t('Show entry report') }}</QItemSection>
</QItem>
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('globals.agency')" :value="entity.travel?.agency?.name" /> <VnLv :label="t('globals.agency')" :value="entity.travel?.agency?.name" />
<VnLv :label="t('globals.shipped')" :value="toDate(entity.travel?.shipped)" /> <VnLv :label="t('shipped')" :value="toDate(entity.travel?.shipped)" />
<VnLv :label="t('globals.landed')" :value="toDate(entity.travel?.landed)" /> <VnLv :label="t('landed')" :value="toDate(entity.travel?.landed)" />
<VnLv <VnLv
:label="t('globals.warehouseOut')" :label="t('globals.warehouseOut')"
:value="entity.travel?.warehouseOut?.name" :value="entity.travel?.warehouseOut?.name"
@ -155,7 +148,6 @@ es:
All travels with current agency: Todos los envíos con la agencia actual All travels with current agency: Todos los envíos con la agencia actual
All entries with current supplier: Todas las entradas con el proveedor actual All entries with current supplier: Todas las entradas con el proveedor actual
Show entry report: Ver informe del pedido Show entry report: Ver informe del pedido
Go to module index: Ir al índice del modulo
Inventory entry: Es inventario Inventory entry: Es inventario
Virtual entry: Es una redada Virtual entry: Es una redada
</i18n> </i18n>

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