8627-devToTest #1421

Merged
alexm merged 768 commits from 8627-devToTest into test 2025-02-18 12:37:37 +00:00
341 changed files with 10579 additions and 7566 deletions
Showing only changes of commit ece705b0ae - Show all commits

View File

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

4
.gitignore vendored
View File

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

View File

@ -1,23 +1,24 @@
const fs = require('fs');
const path = require('path');
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { join, resolve } from 'path';
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))
return getCurrentBranchName(path.resolve(p, '..'));
if (!existsSync(gitHeadPath)) {
return getCurrentBranchName(resolve(p, '..'));
}
const headContent = fs.readFileSync(gitHeadPath, 'utf-8');
const headContent = readFileSync(gitHeadPath, 'utf-8');
return headContent.trim().split('/')[2];
}
const branchName = getCurrentBranchName();
if (branchName) {
const msgPath = `.git/COMMIT_EDITMSG`;
const msg = fs.readFileSync(msgPath, 'utf-8');
const msgPath = '.git/COMMIT_EDITMSG';
const msg = readFileSync(msgPath, 'utf-8');
const reference = branchName.match(/^\d+/);
const referenceTag = `refs #${reference}`;
@ -26,8 +27,7 @@ if (branchName) {
if (splitedMsg.length > 1) {
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,
printWidth: 90,
tabWidth: 4,

View File

@ -1,3 +1,246 @@
# 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
### Added 🆕
- chore: refs #8197 remove console log by:alexm
- chore: refs #8197 replace name by:alexm
- chore: refs #8197 unnecessary file by:alexm
- feat: #8110 apply mixin in quasar components by:Javier Segarra
- feat(Account & AccountRole): refs #8197 add VnCardMain by:alexm
- feat: addDptoLink by:Jtubau
- feat: added restore ticket function in ticket descriptor menu by:Jon
- feat: add support service wip by:jorgep
- feat: focus menu searchbar by:jorgep
- feat: make additional data object by:jorgep
- feat: message to grant access by:jorgep
- feat: refs #6583 add default param by:jorgep
- feat: refs #6583 add destination opt filter by:jorgep
- feat: refs #6583 add icon by:jorgep
- feat: refs #6583 add locale by:jorgep
- feat: refs #7072 added test to computed fn total by:Jtubau
- feat: refs #7235 update invoice out global form to fetch config based on serial type by:jgallego
- feat: refs #7301 add exclude inventory supplier from list by:pablone
- feat: refs #7301 enhance VnDateBadge styling and improve ItemLastEntries component by:pablone
- feat: refs #7882 Added distribution point by:guillermo
- feat: refs #7882 Added longitude & latitude by:guillermo
- feat: refs #7936 add autocomplete on tab fn by:jorgep
- feat: refs #7936 add company filter by:jorgep
- feat: refs #7936 add currency check before fetching by:jorgep
- feat: refs #7936 add dueDated field by:jorgep
- feat: refs #7936 add number validation to VnInputNumber & new daysAgo filter in InvoiceInFilter by:jorgep
- feat: refs #7936 add optionCaption by:jorgep
- feat: refs #7936 add row click navigation to InvoiceInSerial by:jorgep
- feat: refs #7936 add unit tests by:jorgep
- feat: refs #7936 add useAccountShortToStandard composable by:jorgep
- feat: refs #7936 calculate exchange & update taxable base by:jorgep
- feat: refs #7936 enhance downloadFile function to support opening in a new tab by:jorgep
- feat: refs #7936 enhance getTotal fn & add unit tests by:jorgep
- feat: refs #7936 enhance vn-select by:jorgep
- feat: refs #7936 improve optionLabel logic in InvoiceInVat component for better handling of numeric values by:jorgep
- feat: refs #7936 limit decimal places by:jorgep
- feat: refs #7936 make fields required by:jorgep
- feat: refs #7936 show country code & isVies fields by:jorgep
- feat: refs #7936 show id & value by:jorgep
- feat: refs #7936 simplify optionLabel wip by:jorgep
- feat: refs #7936 update 'isVies' label to use global translation key by:jorgep
- feat: refs #7936 update option labels in InvoiceIn components for better clarity by:jorgep
- feat: refs #7936 use default invoice data by:jorgep
- feat: refs #8001 change request by:robert
- feat: refs #8001 ticketExpeditionGrafana by:robert
- feat: refs #8194 created VnSelectWorker component and use it in Lilium by:Jon
- feat: refs #8197 better leftMenu and VnCardMain improvements by:alexm
- feat: refs #8197 default leftMenu by:alexm
- feat: refs #8197 default sectionName by:alexm
- feat: refs #8197 keepData in VnSection by:alexm
- feat: refs #8197 vnTableFilter by:alexm
- feat: refs #8197 working rightMenu by:alexm
- feat: remove re-fetch when add element by:Javier Segarra
- feat: remove search after category by:Javier Segarra
- feat: requested changes in item module by:Jon
- feat: update quantity by:Javier Segarra
- feat(VnPaginate): refs #8197 hold data when change to Card by:alexm
### Changed 📦
- perf: #6896 REMOVE COMMENTS by:Javier Segarra
- perf: qFormMixin by:Javier Segarra
- perf: qFormMixin improvement by:Javier Segarra
- perf: refs #8194 select worker component by:Jon
- perf: refs #8197 perf by:alexm
- perf: remove comments by:Javier Segarra
- perf: remove unused variables (origin/warmfix_noUsedVars) by:Javier Segarra
- refactor: added again search emit by:Jon
- refactor: add useCau composable by:jorgep
- refactor: deleted log by:Jon
- refactor: deleted onUnmounted code by:Jon
- refactor: deleted useless hidden tag by:Jon
- refactor: deleted warnings and corrected itemTag by:Jon
- refactor: drop logic by:jorgep
- refactor: ignore params when searching by id on searchbar (origin/VnSearchbar-SearchRemoveParams) by:Jon
- refactor: log error by:Jon
- refactor: refs #7936 locale by:jorgep
- refactor: refs #7936 simplify getTotal fn by:jorgep
- refactor: refs #7936 update label capitalization and replace invoice type options by:jorgep
- refactor: refs #8194 deleted unnecessary label by:Jon
- refactor: refs #8194 modified select worker template by:Jon
- refactor: refs #8194 modified select worker to allow no one filter from monitor ticket by:Jon
- refactor: refs #8194 moved translation to the correct place by:Jon
- refactor: refs #8194 requested changes by:Jon
- refactor: refs #8194 structure changes in component and related files by:Jon
- refactor: refs #8197 adapt AccountAcls to VnCardMain by:alexm
- refactor: refs #8197 adapt AccountAlias by:alexm
- refactor: refs #8197 adapt Ticket to VnCardMain by:alexm
- refactor: refs #8197 backward compatible (8197-VnCardMain_backwardCompatibility) by:alexm
- refactor: refs #8197 rename VnSectionMain to VnModule and VnCardMain to VnSection by:alexm
- refactor: refs #8288 changed invoice out spanish translation by:provira
- refactor: use locale keys by:jorgep
- refactor: use teleport to avoid qdrawer overlapping by:Jon
- refactor: use VnSelectWorker by:Jon
### Fixed 🛠️
- fix: account by:carlossa
- fix: account create by:carlossa
- fix: accountList create by:carlossa
- fix(AccountList): use $refs by:alexm
- fix: add data-key by:alexm
- fix: addLocales by:Jtubau
- fix: dated field by:Jon
- fix: e2e by:jorgep
- fix: fix department filter by:carlossa
- fix: fixed translations by:Javier Segarra
- fix: fixed translations by:provira
- fix: get total from api by:Javier Segarra
- fix: handle non-object options by:jorgep
- fix: monitorPayMethodFilter by:carlossa
- fix: orderBy priority by:Javier Segarra
- fix: prevent null by:jorgep
- fix: redirection vnTable VnTableFilter by:alexm
- fix: refs #6389 fix filter trad by:carlossa
- fix: refs #6389 fix front, filters, itp by:carlossa
- fix: refs #6389 front add packing filter by:carlossa
- fix: refs #6389 front by:carlossa
- fix: refs #6389 front filters by:carlossa
- fix: refs #6389 ipt by:carlossa
- fix: refs #6389 packing by:carlossa
- fix: refs #6583 update checkbox for filtering by destination in TicketAdvanceFilter by:jorgep
- fix: refs #7031 add test e2e by:carlossa
- fix: refs #7031 fix zoneTest by:carlossa
- fix: refs #7301 unnecessary console logs from ItemLastEntries.vue by:pablone
- fix: refs #7936 changes by:jorgep
- fix: refs #7936 decimal places & locale by:jorgep
- fix: refs #7936 descriptor & dueday by:jorgep
- fix: refs #7936 exclude disabled els on tab by:jorgep
- fix: refs #7936 format tax calculation to two decimal places by:jorgep
- fix: refs #7936 improve error handling by:jorgep
- fix: refs #7936 redirection by:jorgep
- fix: refs #7936 rollback by:jorgep
- fix: refs #7936 serial by:jorgep
- fix: refs #7936 tabulation wip by:jorgep
- fix: refs #7936 test by:jorgep
- fix: refs #8114 clean by:carlossa
- fix: refs #8114 fix agencyList by:carlossa
- fix: refs #8114 fix lifeCycle hooks by:carlossa
- fix: refs #8114 fix pr by:carlossa
- fix: refs #8114 fix removeAddress by:carlossa
- fix: refs #8114 orderList by:carlossa
- fix: refs #8114 remove logs by:carlossa
- fix: refs #8197 mapKey (origin/8197-perf_vnTableInside, 8197-perf_vnTableInside) by:alexm
- fix: refs #8197 redirection (8197-perf_redirection) by:alexm
- fix: refs #8197 staticParams and redirect by:alexm
- fix: refs #8197 vnPaginate onFetch emit by:alexm
- fix: refs #8197 vnPaginate when change :id by:alexm
- fix: refs #8197 vnTableFilter in vnTable by:alexm
- fix: refs #8315 ticketBoxing test by:alexm
- fix: remove url by:carlossa
- fix: rollback by:jorgep
- fix: test by:jorgep
- fix(VnDmsList): refs #8197 add mapKey by:alexm
- revert: refs #8197 arrayData changes by:alexm
- test: refs #8197 fix e2e by:alexm
- test: refs #8315 fix claimDevelopment fixtures by:alexm
- test: refs #8315 fix clientList by:alexm
- test: refs #8315 fix VnSelect in e2e by:alexm
# Version 24.50 - 2024-12-10
### 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/references/configuration
// https://www.npmjs.com/package/cypress-mochawesome-reporter
module.exports = defineConfig({
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:9000/',
experimentalStudio: true,
@ -11,6 +11,7 @@ module.exports = defineConfig({
screenshotsFolder: 'test/cypress/screenshots',
supportFile: 'test/cypress/support/index.js',
videosFolder: 'test/cypress/videos',
downloadsFolder: 'test/cypress/downloads',
video: false,
specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: true,
@ -30,8 +31,10 @@ module.exports = defineConfig({
supportFile: 'test/cypress/support/unit.js',
},
setupNodeEvents(on, config) {
require('cypress-mochawesome-reporter/plugin')(on);
import('cypress-mochawesome-reporter/plugin').then((plugin) => plugin.default(on));
// implement node event listeners here
},
viewportWidth: 1280,
viewportHeight: 720,
},
});

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,13 +55,6 @@ async function setCountry(countryFk, data) {
}
// Province
async function handleProvinces(data) {
provincesOptions.value = data;
if (postcodeFormData.countryFk) {
await fetchTowns();
}
}
async function setProvince(id, data) {
if (data.provinceFk === id) return;
const newProvince = provincesOptions.value.find((province) => province.id == id);
@ -69,6 +62,7 @@ async function setProvince(id, data) {
postcodeFormData.provinceFk = id;
await fetchTowns();
}
async function onProvinceCreated(data) {
postcodeFormData.provinceFk = data.id;
}

View File

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

View File

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

View File

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

View File

@ -44,7 +44,7 @@ const onDataSaved = (data) => {
<FormModelPopup
url-create="Items/regularize"
model="Items"
:title="t('Regularize stock')"
:title="t('item.regularizeStock')"
:form-initial-data="regularizeFormData"
@on-data-saved="onDataSaved($event)"
>
@ -55,6 +55,7 @@ const onDataSaved = (data) => {
v-model.number="data.quantity"
type="number"
autofocus
data-cy="regularizeStockInput"
/>
</VnRow>
<VnRow>

View File

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

View File

@ -73,8 +73,8 @@ const $props = defineProps({
default: true,
},
bottom: {
type: Object,
default: null,
type: Boolean,
default: false,
},
cardClass: {
type: String,
@ -203,7 +203,7 @@ onMounted(() => {
watch(
() => $props.columns,
(value) => splitColumns(value),
{ immediate: true }
{ immediate: true },
);
function splitColumns(columns) {
@ -231,7 +231,7 @@ function splitColumns(columns) {
// Status column
if (splittedColumns.value.chips.length) {
splittedColumns.value.columnChips = splittedColumns.value.chips.filter(
(c) => !c.isId
(c) => !c.isId,
);
if (splittedColumns.value.columnChips.length)
splittedColumns.value.columns.unshift({
@ -575,7 +575,19 @@ function getCheckboxIcon(value) {
show-if-above
>
<QScrollArea class="fit">
<VnTableFilter :data-key="$attrs['data-key']" :columns="columns" />
<VnTableFilter
:data-key="$attrs['data-key']"
:columns="columns"
:redirect="redirect"
>
<template
v-for="(_, slotName) in $slots"
#[slotName]="slotData"
:key="slotName"
>
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
</VnTableFilter>
</QScrollArea>
</QDrawer>
<CrudModel
@ -760,35 +772,14 @@ function getCheckboxIcon(value) {
btn.isPrimary ? 'text-primary-light' : 'color-vn-text '
"
:style="`visibility: ${
(btn.show && btn.show(row)) ?? true ? 'visible' : 'hidden'
((btn.show && btn.show(row)) ?? true)
? 'visible'
: 'hidden'
}`"
@click="btn.action(row)"
/>
</QTd>
</template>
<template #bottom v-if="bottom">
<slot name="bottom-table">
<QBtn
@click="
() =>
createAsDialog
? (showForm = !showForm)
: handleOnDataSaved(create)
"
class="cursor-pointer fill-icon"
color="primary"
icon="add_circle"
size="md"
round
flat
shortcut="+"
:disabled="!disabledAttr"
/>
<QTooltip>
{{ createForm.title }}
</QTooltip>
</slot>
</template>
<template #item="{ row, colsMap }">
<component
:is="$props.redirect ? 'router-link' : 'span'"
@ -914,6 +905,27 @@ function getCheckboxIcon(value) {
</QTr>
</template>
</QTable>
<div class="full-width bottomButton" v-if="bottom">
<QBtn
@click="
() =>
createAsDialog
? (showForm = !showForm)
: handleOnDataSaved(create)
"
class="cursor-pointer fill-icon"
color="primary"
icon="add_circle"
size="md"
round
flat
shortcut="+"
:disabled="!disabledAttr"
/>
<QTooltip>
{{ createForm.title }}
</QTooltip>
</div>
</template>
</CrudModel>
<QPageSticky v-if="$props.create" :offset="[20, 20]" style="z-index: 2">

View File

@ -1,6 +1,5 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnFilter from 'components/VnTable/VnFilter.vue';
@ -11,16 +10,11 @@ defineProps({
type: Array,
required: true,
},
chipLocale: {
type: String,
default: null,
},
searchUrl: {
type: [String, Boolean],
default: 'table',
},
});
const { t } = useI18n();
const tableFilterRef = ref([]);
@ -62,11 +56,14 @@ function columnName(col) {
:columns="columns"
/>
</template>
<template #tags="{ tag, formatFn }" v-if="chipLocale">
<template #tags="{ tag, formatFn, getLocale }">
<div class="q-gutter-x-xs">
<strong>{{ t(`${chipLocale}.${tag.label}`) }}: </strong>
<strong>{{ getLocale(`${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
</VnFilterPanel>
</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,56 @@
import { createWrapper, axios } from 'app/test/vitest/helper';
import EditForm from 'components/EditTableCellValueForm.vue';
import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest';
const fieldA = 'fieldA';
const fieldB = 'fieldB';
describe('EditForm', () => {
let vm;
const mockRows = [
{ id: 1, itemFk: 101 },
{ id: 2, itemFk: 102 },
];
const mockFieldsOptions = [
{ label: 'Field A', field: fieldA, component: 'input', attrs: {} },
{ label: 'Field B', field: fieldB, component: 'date', attrs: {} },
];
const editUrl = '/api/edit';
beforeAll(() => {
vi.spyOn(axios, 'post').mockResolvedValue({ status: 200 });
vm = createWrapper(EditForm, {
props: {
rows: mockRows,
fieldsOptions: mockFieldsOptions,
editUrl,
},
}).vm;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('onSubmit()', () => {
it('should call axios.post with the correct parameters in the payload', async () => {
const selectedField = { field: fieldA, component: 'input', attrs: {} };
const newValue = 'Test Value';
vm.selectedField = selectedField;
vm.newValue = newValue;
await vm.onSubmit();
const payload = axios.post.mock.calls[0][1];
expect(axios.post).toHaveBeenCalledWith(editUrl, expect.any(Object));
expect(payload.field).toEqual(fieldA);
expect(payload.newValue).toEqual(newValue);
expect(payload.lines).toEqual(expect.arrayContaining(mockRows));
expect(vm.isLoading).toEqual(false);
});
});
});

View File

@ -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>
import { ref, onMounted, useSlots } from 'vue';
import { onMounted, useSlots } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useQuasar } from 'quasar';
import { useHasContent } from 'src/composables/useHasContent';
const { t } = useI18n();
const quasar = useQuasar();
const stateStore = useStateStore();
const slots = useSlots();
const hasContent = ref(false);
const rightPanel = ref(null);
const hasContent = useHasContent('#right-panel');
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)
stateStore.rightDrawer = false;
});
</script>
<template>
<Teleport to="#actions-append" v-if="stateStore.isHeaderMounted()">
<Teleport to="#actions-prepend" v-if="stateStore.isHeaderMounted()">
<div class="row q-gutter-x-sm">
<QBtn
v-if="hasContent || $slots['right-panel']"

View File

@ -14,6 +14,7 @@ defineProps({
hide-dropdown-icon
focus-on-mount
@update:model-value="promise"
data-cy="vnBtnSelect_select"
/>
</QBtnDropdown>
</template>

View File

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

View File

@ -2,9 +2,9 @@
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnRow from '../ui/VnRow.vue';
import VnInput from './VnInput.vue';
import FetchData from '../FetchData.vue';
import useNotify from 'src/composables/useNotify';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
const props = defineProps({
submitFn: { type: Function, default: () => {} },
@ -70,19 +70,19 @@ defineExpose({ show: () => changePassDialog.value.show() });
</QCardSection>
<QForm ref="form">
<QCardSection>
<VnInput
<VnInputPassword
v-if="props.askOldPass"
:label="t('Old password')"
v-model="passwords.oldPassword"
type="password"
:required="true"
:toggle-visibility="true"
autofocus
/>
<VnInput
<VnInputPassword
:label="t('New password')"
v-model="passwords.newPassword"
type="password"
:required="true"
:toggle-visibility="true"
:info="
t('passwordRequirements', {
length: requirements.length,
@ -95,10 +95,10 @@ defineExpose({ show: () => changePassDialog.value.show() });
autofocus
/>
<VnInput
<VnInputPassword
:label="t('Repeat password')"
v-model="passwords.repeatPassword"
type="password"
:toggle-visibility="true"
/>
</QCardSection>
</QForm>

View File

@ -11,9 +11,9 @@ function getBadgeAttrs(date) {
let timeDiff = today - timeTicket;
if (timeDiff == 0) return { color: 'warning', 'text-color': 'black' };
if (timeDiff < 0) return { color: 'success', 'text-color': 'black' };
return { color: 'transparent', 'text-color': 'white' };
if (timeDiff == 0) return { color: 'warning', class: 'black-text-color' };
if (timeDiff < 0) return { color: 'success', class: 'black-text-color' };
return { color: 'transparent', class: 'normal-text-color' };
}
function formatShippedDate(date) {
@ -29,3 +29,11 @@ function formatShippedDate(date) {
{{ formatShippedDate(date) }}
</QBadge>
</template>
<style lang="scss">
.black-text-color {
color: var(--vn-black-text-color);
}
.normal-text-color {
color: var(--vn-text-color);
}
</style>

View File

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

View File

@ -43,6 +43,10 @@ const $props = defineProps({
type: Number,
default: null,
},
uppercase: {
type: Boolean,
default: false,
},
});
const vnInputRef = ref(null);
@ -114,9 +118,9 @@ const handleInsertMode = (e) => {
});
};
defineExpose({
focus,
});
const handleUppercase = () => {
value.value = value.value?.toUpperCase() || '';
};
</script>
<template>
@ -136,20 +140,23 @@ defineExpose({
hide-bottom-space
:data-cy="$attrs.dataCy ?? $attrs.label + '_input'"
>
<template v-if="$slots.prepend" #prepend>
<template #prepend>
<slot name="prepend" />
</template>
<template #append>
<QIcon
name="close"
size="xs"
v-if="
hover &&
value &&
!$attrs.disabled &&
!$attrs.readonly &&
$props.clearable
"
:style="{
visibility:
hover &&
value &&
!$attrs.disabled &&
!$attrs.readonly &&
$props.clearable
? 'visible'
: 'hidden',
}"
@click="
() => {
value = null;
@ -158,6 +165,15 @@ defineExpose({
}
"
></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" />
<QIcon v-if="info" name="info">
<QTooltip max-width="350px">
@ -168,6 +184,7 @@ defineExpose({
</QInput>
</div>
</template>
<i18n>
en:
inputMin: Must be more than {value}
@ -177,9 +194,4 @@ defineExpose({
inputMin: Debe ser mayor a {value}
maxLength: El valor excede los {value} carácteres
inputMax: Debe ser menor a {value}
</i18n>
<style lang="scss">
.q-field__append {
padding-inline: 0;
}
</style>
</i18n>

View File

@ -1,14 +1,12 @@
<script setup>
import { onMounted, watch, computed, ref, useAttrs } from 'vue';
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
import VnDate from './VnDate.vue';
import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const model = defineModel({ type: [String, Date] });
const { t } = useI18n();
const $props = defineProps({
isOutlined: {
@ -108,7 +106,7 @@ const manageDate = (date) => {
:rules="mixinRules"
:clearable="false"
@click="isPopupOpen = !isPopupOpen"
@blur="emit('blur')"
@keydown="isPopupOpen = false"
hide-bottom-space
>
<template #append>

View File

@ -0,0 +1,31 @@
<script setup>
import VnInput from 'src/components/common/VnInput.vue';
import { ref } from 'vue';
const model = defineModel({ type: [Number, String] });
const $props = defineProps({
toggleVisibility: {
type: Boolean,
default: false,
},
});
const showPassword = ref(false);
</script>
<template>
<VnInput
v-bind="{ ...$attrs }"
v-model="model"
:type="
$props.toggleVisibility ? (showPassword ? 'text' : 'password') : $attrs.type
"
>
<template #append v-if="toggleVisibility">
<QIcon
:name="showPassword ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showPassword = !showPassword"
/>
</template>
</VnInput>
</template>

View File

@ -1,13 +1,11 @@
<script setup>
import { computed, ref, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import { date } from 'quasar';
import VnTime from './VnTime.vue';
import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const { t } = useI18n();
const model = defineModel({ type: String });
const props = defineProps({
timeOnly: {
@ -82,7 +80,7 @@ function dateToTime(newDate) {
style="min-width: 100px"
:rules="mixinRules"
@click="isPopupOpen = !isPopupOpen"
@blur="emit('blur')"
@keydown="isPopupOpen = false"
type="time"
hide-bottom-space
>

View File

@ -2,7 +2,7 @@
import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import VnSelectDialog from 'components/common/VnSelectDialog.vue';
import { useI18n } from 'vue-i18n';
import { computed } from 'vue';
import { ref, watch } from 'vue';
import { useAttrs } from 'vue';
import { useRequired } from 'src/composables/useRequired';
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 locationProperties = [
'postcode',
@ -43,9 +51,7 @@ const formatLocation = (obj, properties = locationProperties) => {
return filteredParts.join(', ');
};
const modelValue = computed(() =>
props.location ? formatLocation(props.location, locationProperties) : null
);
const modelValue = ref(props.location ? formatLocation(props.location) : null);
function showLabel(data) {
const dataProperties = [

View File

@ -15,6 +15,7 @@ import FetchData from '../FetchData.vue';
import VnSelect from './VnSelect.vue';
import VnUserLink from '../ui/VnUserLink.vue';
import VnPaginate from '../ui/VnPaginate.vue';
import RightMenu from './RightMenu.vue';
const stateStore = useStateStore();
const validationsStore = useValidator();
@ -130,7 +131,7 @@ const actionsIcon = {
};
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 +
/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) {
@ -192,7 +193,7 @@ function getLogTree(data) {
user: log.user,
userFk: log.userFk,
logs: [],
})
}),
);
}
// Model
@ -210,7 +211,7 @@ function getLogTree(data) {
id: log.changedModelId,
showValue: log.changedModelValue,
logs: [],
})
}),
);
nLogs = 0;
}
@ -282,7 +283,7 @@ function setDate(type) {
to = date.adjustDate(
to,
{ hour: 21, minute: 59, second: 59, millisecond: 999 },
true
true,
);
switch (type) {
@ -365,7 +366,7 @@ async function clearFilter() {
dateTo.value = undefined;
userRadio.value = undefined;
Object.keys(checkboxOptions.value).forEach(
(opt) => (checkboxOptions.value[opt].selected = false)
(opt) => (checkboxOptions.value[opt].selected = false),
);
await applyFilter();
}
@ -378,7 +379,7 @@ watch(
() => router.currentRoute.value.params.id,
() => {
applyFilter();
}
},
);
</script>
<template>
@ -391,7 +392,7 @@ watch(
const changedModel = item.changedModel;
return {
locale: useCapitalize(
validations[changedModel]?.locale?.name ?? changedModel
validations[changedModel]?.locale?.name ?? changedModel,
),
value: changedModel,
};
@ -507,7 +508,7 @@ watch(
:title="
date.formatDate(
log.creationDate,
'DD/MM/YYYY hh:mm:ss'
'DD/MM/YYYY hh:mm:ss',
) ?? `date:'dd/MM/yyyy HH:mm:ss'`
"
>
@ -577,7 +578,7 @@ watch(
t(
`actions.${
actionsText[log.action]
}`
}`,
)
"
/>
@ -677,139 +678,144 @@ watch(
</div>
</template>
</VnPaginate>
<Teleport to="#right-panel" v-if="stateStore.isHeaderMounted()">
<QList dense>
<QSeparator />
<QItem class="q-mt-sm">
<QInput
:label="t('globals.search')"
v-model="searchInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="() => selectFilter('search')"
@focusout="() => selectFilter('search')"
@clear="() => selectFilter('search')"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{ t('tooltips.search') }}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
<QItem>
<VnSelect
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">
<RightMenu>
<template #right-panel>
<QList dense>
<QSeparator />
<QItem class="q-mt-sm">
<QInput
:label="t('globals.search')"
v-model="searchInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="() => selectFilter('search')"
@focusout="() => selectFilter('search')"
@clear="() => selectFilter('search')"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{ t('tooltips.search') }}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
<QItem>
<VnSelect
class="full-width"
:label="t('globals.user')"
v-model="userSelect"
option-label="name"
option-value="id"
:url="`${model}Logs/${$route.params.id}/editors`"
:fields="['id', 'nickname', 'name', 'image']"
sort-by="nickname"
@update:model-value="selectFilter('userSelect')"
: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 #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 #label="{ label }">
{{ t(`Users.${label}`) }}
</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')"
</QOptionGroup>
</QItem>
<QItem class="q-mt-sm">
<QItemSection v-if="userRadio !== null">
<VnSelect
class="full-width"
:label="t('globals.user')"
v-model="userSelect"
option-label="name"
option-value="id"
:url="`${model}Logs/${route.params.id}/editors`"
:fields="['id', 'nickname', 'name', 'image']"
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>
<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"
>
<QCheckbox
size="sm"
v-model="checkboxOption.selected"
:label="t(`actions.${checkboxOption.label}`)"
@update:model-value="selectFilter"
/>
</QItem>
<QItem class="q-mt-sm">
<QInput
class="full-width"
:label="t('globals.date')"
@click="dateFromDialog = true"
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'to')"
v-model="dateFrom"
clearable
clear-icon="close"
/>
</QItem>
<QItem class="q-mt-sm">
<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>
<QCheckbox
size="sm"
v-model="checkboxOption.selected"
:label="t(`actions.${checkboxOption.label}`)"
@update:model-value="selectFilter"
/>
</QItem>
<QItem class="q-mt-sm">
<QInput
class="full-width"
:label="t('globals.date')"
@click="dateFromDialog = true"
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'to')"
v-model="dateFrom"
clearable
clear-icon="close"
/>
</QItem>
<QItem class="q-mt-sm">
<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>
</template>
</RightMenu>
<QDialog v-model="dateFromDialog">
<QDate
:years-in-month-view="false"

View File

@ -20,16 +20,15 @@ const hasContent = ref();
let observer;
onMounted(() => {
if (teleportRef.value) {
const checkContent = () => {
hasContent.value = teleportRef.value.innerHTML.trim() !== '';
};
if (!teleportRef.value) return;
const checkContent = () => {
hasContent.value = teleportRef.value?.innerHTML?.trim() !== '';
};
observer = new MutationObserver(checkContent);
observer.observe(teleportRef.value, { childList: true, subtree: true });
observer = new MutationObserver(checkContent);
observer.observe(teleportRef.value, { childList: true, subtree: true });
checkContent();
}
checkContent();
});
</script>

View File

@ -1,14 +1,16 @@
<script setup>
import RightMenu from './RightMenu.vue';
import RightAdvancedMenu from './RightAdvancedMenu.vue';
import VnSearchbar from 'components/ui/VnSearchbar.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 { useRoute, useRouter } from 'vue-router';
import { useHasContent } from 'src/composables/useHasContent';
const $props = defineProps({
section: {
type: String,
required: true,
default: null,
},
dataKey: {
type: String,
@ -38,33 +40,63 @@ const $props = defineProps({
type: Boolean,
default: true,
},
keepData: {
type: Boolean,
default: true,
},
});
const sectionValue = computed(() => $props.section ?? $props.dataKey);
const route = useRoute();
const router = useRouter();
let arrayData;
const sectionValue = computed(() => $props.section ?? $props.dataKey);
const isMainSection = ref(false);
const searchbarId = 'section-searchbar';
const advancedMenuSlot = 'advanced-menu';
const hasContent = useHasContent(`#${searchbarId}`);
onBeforeMount(() => {
if ($props.dataKey)
arrayData = useArrayData($props.dataKey, {
searchUrl: 'table',
keepData: $props.keepData,
...$props.arrayDataProps,
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>
<template>
<slot name="searchbar">
<VnSearchbar
v-if="searchBar"
v-if="searchBar && !hasContent"
v-bind="arrayDataProps"
:data-key="dataKey"
:label="$t(`${prefix}.search`)"
:info="$t(`${prefix}.searchInfo`)"
/>
<div :id="searchbarId"></div>
</slot>
<RightMenu>
<template #right-panel v-if="$slots['rightMenu'] || rightFilter">
<slot name="rightMenu">
<RightAdvancedMenu :is-main-section="isMainSection">
<template #advanced-menu v-if="$slots[advancedMenuSlot] || rightFilter">
<slot :name="advancedMenuSlot">
<VnTableFilter
v-if="rightFilter && columns"
:data-key="dataKey"
@ -73,7 +105,7 @@ onBeforeMount(() => {
/>
</slot>
</template>
</RightMenu>
<slot name="body" v-if="sectionValue == $route.name" />
</RightAdvancedMenu>
<slot name="body" v-if="isMainSection" />
<RouterView v-else />
</template>

View File

@ -27,7 +27,7 @@ const $props = defineProps({
default: () => [],
},
optionLabel: {
type: [String],
type: [String, Function],
default: 'name',
},
optionValue: {
@ -113,8 +113,15 @@ const $props = defineProps({
});
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const { optionLabel, optionValue, optionFilter, optionFilterValue, options, modelValue } =
toRefs($props);
const {
optionLabel,
optionValue,
optionCaption,
optionFilter,
optionFilterValue,
options,
modelValue,
} = toRefs($props);
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const vnSelectRef = ref();
@ -166,7 +173,11 @@ onMounted(() => {
const arrayDataKey =
$props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label);
const arrayData = useArrayData(arrayDataKey, { url: $props.url, searchUrl: false });
const arrayData = useArrayData(arrayDataKey, {
url: $props.url,
searchUrl: false,
mapKey: $attrs['map-key'],
});
function findKeyInOptions() {
if (!$props.options) return;
@ -194,10 +205,10 @@ function filter(val, options) {
}
if (!row) return;
const id = row[$props.optionValue];
const id = String(row[$props.optionValue]);
const optionLabel = String(row[$props.optionLabel]).toLowerCase();
return id == search || optionLabel.includes(search);
return id.includes(search) || optionLabel.includes(search);
});
}
@ -221,12 +232,15 @@ async function fetchFilter(val) {
} else defaultWhere = { [key]: getVal(val) };
const where = { ...(val ? defaultWhere : {}), ...$props.where };
$props.exprBuilder && Object.assign(where, $props.exprBuilder(key, val));
const fetchOptions = { where, include, limit };
if (fields) fetchOptions.fields = fields;
if (sortBy) fetchOptions.order = sortBy;
const filterOptions = { where, include, limit };
if (fields) filterOptions.fields = fields;
if (sortBy) filterOptions.order = sortBy;
arrayData.resetPagination();
const { data } = await arrayData.applyFilter({ filter: fetchOptions });
const { data } = await arrayData.applyFilter(
{ filter: filterOptions },
{ updateRouter: false }
);
setOptions(data);
return data;
}
@ -283,7 +297,7 @@ async function onScroll({ to, direction, from, index }) {
}
}
defineExpose({ opts: myOptions });
defineExpose({ opts: myOptions, vnSelectRef });
function handleKeyDown(event) {
if (event.key === 'Tab' && !event.shiftKey) {
@ -317,6 +331,11 @@ function handleKeyDown(event) {
}
}
}
function getCaption(opt) {
if (optionCaption.value === false) return;
return opt[optionCaption.value] || opt[optionValue.value];
}
</script>
<template>
@ -387,8 +406,8 @@ function handleKeyDown(event) {
<QItemLabel>
{{ opt[optionLabel] }}
</QItemLabel>
<QItemLabel caption v-if="optionCaption !== false">
{{ `#${opt[optionCaption] || opt[optionValue]}` }}
<QItemLabel caption v-if="getCaption(opt)">
{{ `#${getCaption(opt)}` }}
</QItemLabel>
</QItemSection>
</QItem>

View File

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

View File

@ -51,10 +51,11 @@ const url = computed(() => {
option-value="id"
option-label="nickname"
:fields="['id', 'name', 'nickname', 'code']"
:filter-options="['id', 'name', 'nickname', 'code']"
sort-by="nickname ASC"
>
<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 #append v-if="$props.hasInfo">
<QIcon name="info" class="cursor-pointer">
@ -71,7 +72,8 @@ const url = computed(() => {
{{ scope.opt.nickname }}
</QItemLabel>
<QItemLabel caption v-else>
{{ scope.opt.nickname }}, {{ scope.opt.code }}
#{{ scope.opt.id }}, {{ scope.opt.nickname }},
{{ scope.opt.code }}
</QItemLabel>
</QItemSection>
</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

@ -0,0 +1,95 @@
import { describe, it, expect } from 'vitest';
import VnJsonValue from 'src/components/common/VnJsonValue.vue';
import { createWrapper } from 'app/test/vitest/helper';
const buildComponent = (props) => {
return createWrapper(VnJsonValue, {
props,
}).wrapper;
};
describe('VnJsonValue', () => {
it('renders null value correctly', async () => {
const wrapper = buildComponent({ value: null });
const span = wrapper.find('span');
expect(span.text()).toBe('∅');
expect(span.classes()).toContain('json-null');
});
it('renders boolean true correctly', async () => {
const wrapper = buildComponent({ value: true });
const span = wrapper.find('span');
expect(span.text()).toBe('✓');
expect(span.classes()).toContain('json-true');
});
it('renders boolean false correctly', async () => {
const wrapper = buildComponent({ value: false });
const span = wrapper.find('span');
expect(span.text()).toBe('✗');
expect(span.classes()).toContain('json-false');
});
it('renders a short string correctly', async () => {
const wrapper = buildComponent({ value: 'Hello' });
const span = wrapper.find('span');
expect(span.text()).toBe('Hello');
expect(span.classes()).toContain('json-string');
});
it('renders a long string correctly with ellipsis', async () => {
const longString = 'a'.repeat(600);
const wrapper = buildComponent({ value: longString });
const span = wrapper.find('span');
expect(span.text()).toContain('...');
expect(span.text().length).toBeLessThanOrEqual(515);
expect(span.attributes('title')).toBe(longString);
expect(span.classes()).toContain('json-string');
});
it('renders a number correctly', async () => {
const wrapper = buildComponent({ value: 123.4567 });
const span = wrapper.find('span');
expect(span.text()).toBe('123.457');
expect(span.classes()).toContain('json-number');
});
it('renders an integer correctly', async () => {
const wrapper = buildComponent({ value: 42 });
const span = wrapper.find('span');
expect(span.text()).toBe('42');
expect(span.classes()).toContain('json-number');
});
it('renders a date correctly', async () => {
const date = new Date('2023-01-01');
const wrapper = buildComponent({ value: date });
const span = wrapper.find('span');
expect(span.text()).toBe('2023-01-01');
expect(span.classes()).toContain('json-object');
});
it('renders an object correctly', async () => {
const obj = { key: 'value' };
const wrapper = buildComponent({ value: obj });
const span = wrapper.find('span');
expect(span.text()).toBe(obj.toString());
expect(span.classes()).toContain('json-object');
});
it('renders an array correctly', async () => {
const arr = [1, 2, 3];
const wrapper = buildComponent({ value: arr });
const span = wrapper.find('span');
expect(span.text()).toBe(arr.toString());
expect(span.classes()).toContain('json-object');
});
it('updates value when prop changes', async () => {
const wrapper = buildComponent({ value: true });
await wrapper.setProps({ value: 123 });
const span = wrapper.find('span');
expect(span.text()).toBe('123');
expect(span.classes()).toContain('json-number');
});
});

View File

@ -0,0 +1,107 @@
import { describe, it, expect, vi, beforeAll, afterEach, beforeEach } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import VnNotes from 'src/components/ui/VnNotes.vue';
describe('VnNotes', () => {
let vm;
let wrapper;
let spyFetch;
let postMock;
let expectedBody;
const mockData= {name: 'Tony', lastName: 'Stark', text: 'Test Note', observationTypeFk: 1};
function generateExpectedBody() {
expectedBody = {...vm.$props.body, ...{ text: vm.newNote.text, observationTypeFk: vm.newNote.observationTypeFk }};
}
async function setTestParams(text, observationType, type){
vm.newNote.text = text;
vm.newNote.observationTypeFk = observationType;
wrapper.setProps({ selectType: type });
}
beforeAll(async () => {
vi.spyOn(axios, 'get').mockReturnValue({ data: [] });
wrapper = createWrapper(VnNotes, {
propsData: {
url: '/test',
body: { name: 'Tony', lastName: 'Stark' },
}
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
});
beforeEach(() => {
postMock = vi.spyOn(axios, 'post').mockResolvedValue(mockData);
spyFetch = vi.spyOn(vm.vnPaginateRef, 'fetch').mockImplementation(() => vi.fn());
});
afterEach(() => {
vi.clearAllMocks();
expectedBody = {};
});
describe('insert', () => {
it('should not call axios.post and vnPaginateRef.fetch if newNote.text is null', async () => {
await setTestParams( null, null, true );
await vm.insert();
expect(postMock).not.toHaveBeenCalled();
expect(spyFetch).not.toHaveBeenCalled();
});
it('should not call axios.post and vnPaginateRef.fetch if newNote.text is empty', async () => {
await setTestParams( "", null, false );
await vm.insert();
expect(postMock).not.toHaveBeenCalled();
expect(spyFetch).not.toHaveBeenCalled();
});
it('should not call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is true', async () => {
await setTestParams( "Test Note", null, true );
await vm.insert();
expect(postMock).not.toHaveBeenCalled();
expect(spyFetch).not.toHaveBeenCalled();
});
it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is false', async () => {
await setTestParams( "Test Note", null, false );
generateExpectedBody();
await vm.insert();
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody);
expect(spyFetch).toHaveBeenCalled();
});
it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is setted and selectType is false', async () => {
await setTestParams( "Test Note", 1, false );
generateExpectedBody();
await vm.insert();
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody);
expect(spyFetch).toHaveBeenCalled();
});
it('should call axios.post and vnPaginateRef.fetch when newNote is valid', async () => {
await setTestParams( "Test Note", 1, true );
generateExpectedBody();
await vm.insert();
expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody);
expect(spyFetch).toHaveBeenCalled();
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -190,7 +190,10 @@ const getLocale = (label) => {
const globalLocale = `globals.params.${param}`;
if (te(globalLocale)) return t(globalLocale);
else if (te(t(`params.${param}`)));
else return t(`${route.meta.moduleName}.params.${param}`);
else {
const camelCaseModuleName = route.meta.moduleName.charAt(0).toLowerCase() + route.meta.moduleName.slice(1);
return t(`${camelCaseModuleName}.params.${param}`);
}
};
</script>
@ -202,7 +205,11 @@ const getLocale = (label) => {
style="position: fixed; z-index: 1; right: 0; bottom: 0"
icon="search"
@click="search()"
></QBtn>
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.search') }}
</QTooltip>
</QBtn>
<QForm @submit="search" id="filterPanelForm" @keyup.enter="search()">
<QList dense>
<QItem class="q-mt-xs">

View File

@ -30,7 +30,7 @@ onBeforeMount(async () => {
.data;
if (!channel) channel = defaultChannel;
phone.value = await parsePhone(props.phoneNumber, props.country.toLowerCase());
phone.value = await parsePhone(props.phoneNumber, props.country?.toLowerCase());
config[
type
].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

@ -110,7 +110,7 @@ onBeforeRouteLeave((to, from, next) => {
:url="$props.url"
order="created DESC"
:limit="0"
:filter="$props.filter"
:user-filter="$props.filter"
auto-load
ref="vnPaginateRef"
class="show"

View File

@ -78,6 +78,10 @@ const props = defineProps({
type: String,
default: '',
},
keyData: {
type: String,
default: undefined,
},
});
const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']);
@ -111,6 +115,7 @@ onMounted(async () => {
});
onBeforeUnmount(() => {
if (!store.keepData) arrayData.reset(['data']);
arrayData.resetPagination();
});
@ -254,7 +259,7 @@ defineExpose({
:disable="disableInfiniteScroll || !store.hasMoreData"
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">
<QSpinner color="primary" size="md" />
</div>

View File

@ -1,14 +1,16 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import { onMounted, ref, computed, watch } from 'vue';
import { useQuasar } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
import VnInput from 'src/components/common/VnInput.vue';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'src/stores/useStateStore';
import { useRoute } from 'vue-router';
const quasar = useQuasar();
const { t } = useI18n();
const state = useStateStore();
const route = useRoute();
const props = defineProps({
dataKey: {
@ -83,6 +85,17 @@ if (props.redirect)
};
let arrayData = useArrayData(props.dataKey, arrayDataProps);
let store = arrayData.store;
const to = computed(() => {
const url = { path: route.path, query: { ...(route.query ?? {}) } };
const searchUrl = arrayData.store.searchUrl;
const currentFilter = {
...arrayData.store.currentFilter,
search: searchText.value || undefined,
};
if (searchUrl) url.query[searchUrl] = JSON.stringify(currentFilter);
return url;
});
watch(
() => props.dataKey,
@ -100,23 +113,20 @@ onMounted(() => {
});
async function search() {
const staticParams = Object.keys(store.userParams ?? {}).length
? store.userParams
: store.defaultParams;
arrayData.resetPagination();
const filter = {
params: {
search: searchText.value,
},
filter: props.filter,
};
let filter = { params: { search: searchText.value } };
if (!props.searchRemoveParams || !searchText.value) {
filter.params = {
...staticParams,
search: searchText.value,
filter = {
params: {
...store.userParams,
search: searchText.value,
},
filter: store.filter,
};
} else {
arrayData.reset(['currentFilter', 'userParams']);
}
if (props.whereFilter) {
@ -126,28 +136,38 @@ async function search() {
delete filter.params.search;
}
await arrayData.applyFilter(filter);
searchText.value = undefined;
}
</script>
<template>
<Teleport to="#searchbar" v-if="state.isHeaderMounted()">
<QForm @submit="search" id="searchbarForm">
<RouterLink
:to="to"
@click="
!$event.shiftKey && !$event.ctrlKey && search();
$refs.input.focus();
"
>
<QIcon
v-if="!quasar.platform.is.mobile"
class="cursor-pointer"
name="search"
size="sm"
>
<QTooltip>{{ t('link') }}</QTooltip>
</QIcon>
</RouterLink>
<VnInput
id="searchbar"
ref="input"
v-model.trim="searchText"
:placeholder="t(props.label)"
dense
standout
autofocus
data-cy="vnSearchBar"
data-cy="vn-searchbar"
borderless
>
<template #prepend>
<QIcon
v-if="!quasar.platform.is.mobile"
class="cursor-pointer"
name="search"
@click="search"
/>
</template>
<template #append>
<QIcon
v-if="props.info && $q.screen.gt.xs"
@ -172,20 +192,52 @@ async function search() {
.q-field {
transition: width 0.36s;
}
</style>
<style lang="scss">
:deep(.q-field__native) {
padding-top: 10px;
padding-left: 5px;
}
:deep(.q-field--dark .q-field__native:focus) {
color: black;
}
:deep(.q-field--focused) {
.q-icon {
color: black;
}
}
.cursor-info {
cursor: help;
}
#searchbar {
.q-field--standout.q-field--highlighted .q-field__control {
.q-form {
display: flex;
align-items: center;
border-radius: 4px;
padding: 0 5px;
background-color: var(--vn-search-color);
&:hover {
background-color: var(--vn-search-color-hover);
}
&:focus-within {
background-color: white;
color: black;
.q-field__native,
.q-icon {
color: black !important;
color: black;
}
}
}
.q-icon {
color: var(--vn-label-color);
}
</style>
<i18n>
en:
link: click to search, ctrl + click to open in a new tab, shift + click to open in a new window
es:
link: clic para buscar, ctrl + clic para abrir en una nueva pestaña, shift + clic para abrir en una nueva ventana
</i18n>

View File

@ -54,6 +54,7 @@ function formatNumber(number) {
:offset="100"
:limit="5"
auto-load
map-key="smsFk"
>
<template #body="{ rows }">
<QCard

View File

@ -1,5 +1,5 @@
<script setup>
import { defineProps, ref } from 'vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
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,81 @@
import { describe, expect, it } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import FetchedTags from 'src/components/ui/FetchedTags.vue';
describe('tags computed property', () => {
it('returns an object with the correct keys and values', () => {
const vm = createWrapper(FetchedTags, {
props: {
item: {
tag1: 'JavaScript',
value1: 'Programming Language',
tag2: 'Vue',
value2: 'Framework',
tag3: 'EmptyTag',
},
tag: 'tag',
value: 'value',
columns: 2,
},
}).vm;
expect(vm.tags).toEqual({
JavaScript: 'Programming Language',
Vue: 'Framework',
EmptyTag: '',
});
});
it('returns an empty object if the item prop is an empty object', () => {
const vm = createWrapper(FetchedTags, {
props: {
item: {},
tag: 'tag',
value: 'value',
},
}).vm;
expect(vm.tags).toEqual({});
});
it('should calculate the correct columnStyle when columns prop is defined', () => {
const vm = createWrapper(FetchedTags, {
props: {
item: {
tag1: 'JavaScript',
value1: 'Programming Language',
tag2: 'Vue',
value2: 'Framework',
tag3: 'EmptyTag',
},
tag: 'tag',
value: 'value',
columns: 2,
},
}).vm;
const expectedStyle = {
'grid-template-columns': 'repeat(2, 1fr)',
'max-width': '8rem',
};
expect(vm.columnStyle).toEqual(expectedStyle);
});
it('should return an empty object for columnStyle when columns prop is not defined', () => {
const vm = createWrapper(FetchedTags, {
props: {
item: {
tag1: 'JavaScript',
value1: 'Programming Language',
tag2: 'Vue',
value2: 'Framework',
tag3: 'EmptyTag',
},
tag: 'tag',
value: 'value',
columns: null,
},
}).vm;
expect(vm.columnStyle).toEqual({});
});
});

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();
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 (!arrayDataStore.get(key)) arrayDataStore.set(key);
@ -31,10 +33,11 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
: JSON.parse(params?.filter ?? '{}');
delete params.filter;
store.userParams = { ...store.userParams, ...params };
store.userParams = params;
store.filter = { ...filter, ...store.userFilter };
if (filter?.order) store.order = filter.order;
}
setCurrentFilter();
});
if (key && userOptions) setOptions();
@ -53,6 +56,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
'searchUrl',
'navigate',
'mapKey',
'keepData',
];
if (typeof userOptions === 'object') {
for (const option in userOptions) {
@ -75,30 +79,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
cancelRequest();
canceller = new AbortController();
const filter = {
limit: store.limit,
};
let userParams = { ...store.userParams };
Object.assign(filter, store.userFilter);
let where;
if (filter?.where || store.filter?.where)
where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {});
Object.assign(filter, store.filter);
filter.where = where;
const params = { filter };
Object.assign(params, userParams);
if (params.filter) params.filter.skip = store.skip;
if (store?.order && typeof store?.order == 'string') store.order = [store.order];
if (store.order?.length) params.filter.order = [...store.order];
else delete params.filter.order;
store.currentFilter = JSON.parse(JSON.stringify(params));
delete store.currentFilter.filter.include;
store.currentFilter.filter = JSON.stringify(store.currentFilter.filter);
const { params, limit } = setCurrentFilter();
let exprFilter;
if (store?.exprBuilder) {
@ -120,15 +101,16 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
params,
});
const { limit } = filter;
store.hasMoreData = limit && response.data.length >= limit;
processData(response.data, { map: !!store.mapKey, append });
if (!append && !isDialogOpened()) updateRouter && updateStateParams();
if (!append && !isDialogOpened() && updateRouter) {
if (updateStateParams(response.data)?.redirect && !store.keepData) return;
}
store.isLoading = false;
canceller = null;
processData(response.data, { map: !!store.mapKey, append });
return response;
}
@ -157,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;
store.filter = {};
if (params) store.userParams = { ...params };
const response = await fetch({});
const response = await fetch(fetchOptions);
return response;
}
@ -188,10 +170,9 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function addOrder(field, direction = 'ASC') {
const newOrder = field + ' ' + direction;
let order = store.order || [];
if (typeof order == 'string') order = [order];
const order = toArray(store.order);
let index = order.findIndex((o) => o.split(' ')[0] === field);
let index = getOrderIndex(order, field);
if (index > -1) {
order[index] = newOrder;
} else {
@ -208,16 +189,24 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
}
async function deleteOrder(field) {
let order = store.order ?? [];
if (typeof order == 'string') order = [order];
const index = order.findIndex((o) => o.split(' ')[0] === field);
const order = toArray(store.order);
const index = getOrderIndex(order, field);
if (index > -1) order.splice(index, 1);
store.order = order;
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) {
for (const param in params) {
if (params[param] === '' || params[param] === null) {
@ -258,7 +247,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
if (Object.values(store.userParams).length) await fetch({});
}
function updateStateParams() {
function updateStateParams(data) {
if (!route?.path) return;
const newUrl = { path: route.path, query: { ...(route.query ?? {}) } };
if (store?.searchUrl)
@ -275,21 +264,53 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const { path } = matches.at(-1);
const to =
store?.data?.length === 1
? path.replace(/\/(list|:id)|-list/, `/${store.data[0].id}`)
data?.length === 1
? path.replace(/\/(list|:id)|-list/, `/${data[0].id}`)
: path.replace(/:id.*/, '');
if (route.path != to) {
const pushUrl = { path: to };
if (to.endsWith('/list') || to.endsWith('/'))
pushUrl.query = newUrl.query;
return router.push(pushUrl);
return router.push(pushUrl) && { redirect: true };
}
}
router.replace(newUrl);
}
function getCurrentFilter() {
if (!Object.keys(store.userParams).length)
store.userParams = store.defaultParams ?? {};
const filter = {
limit: store.limit,
...store.userFilter,
};
let where;
if (filter?.where || store.filter?.where)
where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {});
Object.assign(filter, store.filter);
filter.where = where;
const params = { filter };
Object.assign(params, store.userParams);
if (params.filter) params.filter.skip = store.skip;
if (store.order) params.filter.order = toArray(store.order);
else delete params.filter.order;
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 }) {
if (!append) {
store.data = [];
@ -302,7 +323,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
for (const row of data) {
const key = row[store.mapKey];
const val = { ...row, key };
if (store.map.has(key)) {
if (key && store.map.has(key)) {
const { position } = store.map.get(key);
val.position = position;
store.map.set(key, val);
@ -322,6 +343,8 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
fetch,
applyFilter,
addFilter,
getCurrentFilter,
setCurrentFilter,
addFilterWhere,
addOrder,
deleteOrder,

View File

@ -29,8 +29,12 @@ export function useFilterParams(key) {
orders.value = orderObject;
}
function setUserParams(watchedParams) {
if (!watchedParams || Object.keys(watchedParams).length == 0) return;
function setUserParams(watchedParams = {}) {
if (Object.keys(watchedParams).length == 0) {
params.value = {};
orders.value = {};
return;
}
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
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 useNotify from './useNotify';
import { useTokenConfig } from './useTokenConfig';
import { getToken, getTokenMultimedia } from 'src/utils/session';
const TOKEN_MULTIMEDIA = 'tokenMultimedia';
const TOKEN = 'token';
@ -15,19 +16,6 @@ export function useSession() {
let isCheckingToken = false;
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) {
let keepLogin = data.keepLogin;
const storage = keepLogin ? localStorage : sessionStorage;

View File

@ -1,26 +1,29 @@
// app global css in SCSS form
@import './icons.scss';
@import '@quasar/quasar-ui-qcalendar/src/QCalendarMonth.sass';
@import '@quasar/quasar-ui-qcalendar/src/QCalendarMonth.scss';
.tbody {
--vn-color-negative: $negative;
}
body.body--light {
--font-color: black;
--vn-header-color: #cecece;
--vn-page-color: #ffffff;
--vn-section-color: #e0e0e0;
--vn-section-hover-color: #b9b9b9;
--vn-text-color: var(--font-color);
--vn-text-color: black;
--vn-label-color: #5f5f5f;
--vn-accent-color: #e7e3e3;
--vn-search-color: #d4d4d4;
--vn-search-color-hover: #cfcfcf;
--vn-empty-tag: #acacac;
--vn-black-text-color: black;
--vn-text-color-contrast: white;
background-color: var(--vn-page-color);
.q-header .q-toolbar {
color: var(--font-color);
color: var(--vn-text-color);
}
--vn-color-negative: $negative;
@ -33,7 +36,11 @@ body.body--dark {
--vn-text-color: white;
--vn-label-color: #a8a8a8;
--vn-accent-color: #424242;
--vn-search-color: #4b4b4b;
--vn-search-color-hover: #5b5b5b;
--vn-empty-tag: #2d2d2d;
--vn-black-text-color: black;
--vn-text-color-contrast: black;
background-color: var(--vn-page-color);
@ -93,6 +100,10 @@ select:-webkit-autofill {
background-color: var(--vn-section-hover-color);
}
.bg-vn-page {
background-color: var(--vn-page-color);
}
.color-vn-label {
color: var(--vn-label-color);
}
@ -153,7 +164,8 @@ select:-webkit-autofill {
.q-card,
.q-table,
.q-table__bottom,
.q-drawer {
.q-drawer,
.bottomButton {
background-color: var(--vn-section-color);
}
@ -190,7 +202,7 @@ select:-webkit-autofill {
.q-tooltip {
background-color: var(--vn-page-color);
color: var(--font-color);
color: var(--vn-text-color);
font-size: medium;
}
@ -298,6 +310,14 @@ input::-webkit-inner-spin-button {
.no-visible {
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__control {
min-height: auto !important;

View File

@ -16,6 +16,7 @@ import getUpdatedValues from './getUpdatedValues';
import getParamWhere from './getParamWhere';
import parsePhone from './parsePhone';
import isDialogOpened from './isDialogOpened';
import toCelsius from './toCelsius';
export {
getUpdatedValues,
@ -36,4 +37,5 @@ export {
dashIfEmpty,
dateRange,
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,5 +1,6 @@
export default function toDateHourMin(date) {
const dateHour = new Date(date).toLocaleDateString('es-ES', {
if (!date) return date;
return new Date(date).toLocaleDateString('es-ES', {
timeZone: 'Europe/Madrid',
year: 'numeric',
month: '2-digit',
@ -7,5 +8,4 @@ export default function toDateHourMin(date) {
hour: '2-digit',
minute: '2-digit',
});
return dateHour;
}

View File

@ -1,5 +1,6 @@
export default function toDateHourMinSec(date) {
const dateHour = new Date(date).toLocaleDateString('es-ES', {
if (!date) return date;
return new Date(date).toLocaleDateString('es-ES', {
timeZone: 'Europe/Madrid',
year: 'numeric',
month: '2-digit',
@ -8,5 +9,4 @@ export default function toDateHourMinSec(date) {
minute: '2-digit',
second: '2-digit',
});
return dateHour;
}

View File

@ -1,13 +1,15 @@
globals:
lang:
es: Spanish
en: English
quantity: Quantity
en: English
language: Language
quantity: Quantity
entity: Entity
preview: Preview
user: User
details: Details
collapseMenu: Collapse left menu
collapseMenu: Collapse lateral menu
advancedMenu: Advanced menu
backToDashboard: Return to dashboard
notifications: Notifications
userPanel: User panel
@ -35,7 +37,6 @@ globals:
confirm: Confirm
assign: Assign
back: Back
downloadPdf: Download PDF
yes: 'Yes'
no: 'No'
noChanges: No changes to save
@ -59,6 +60,7 @@ globals:
downloadCSVSuccess: CSV downloaded successfully
reference: Reference
agency: Agency
entry: Entry
warehouseOut: Warehouse Out
warehouseIn: Warehouse In
landed: Landed
@ -67,11 +69,11 @@ globals:
amount: Amount
packages: Packages
download: Download
downloadPdf: Download PDF
selectRows: 'Select all { numberRows } row(s)'
allRows: 'All { numberRows } row(s)'
markAll: Mark all
requiredField: Required field
valueCantBeEmpty: Value cannot be empty
class: clase
type: Type
reason: reason
@ -81,6 +83,9 @@ globals:
warehouse: Warehouse
company: Company
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 }'
smsSent: SMS sent
confirmDeletion: Confirm deletion
@ -130,6 +135,26 @@ globals:
medium: Medium
big: Big
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:
logIn: Login
addressEdit: Update address
@ -151,13 +176,14 @@ globals:
subRoles: Subroles
inheritedRoles: Inherited Roles
customers: Customers
customerCreate: New customer
createCustomer: Create customer
createOrder: New order
list: List
webPayments: Web Payments
extendedList: Extended list
notifications: Notifications
defaulter: Defaulter
customerCreate: New customer
createOrder: New order
fiscalData: Fiscal data
billingData: Billing data
consignees: Consignees
@ -193,27 +219,28 @@ globals:
claims: Claims
claimCreate: New claim
lines: Lines
photos: Photos
development: Development
photos: Photos
action: Action
invoiceOuts: Invoice out
negativeBases: Negative Bases
globalInvoicing: Global invoicing
invoiceOutCreate: Create invoice out
order: Orders
orderList: List
orderCreate: New order
catalog: Catalog
volume: Volume
shelving: Shelving
shelvingList: Shelving List
shelvingCreate: New shelving
invoiceIns: Invoices In
invoiceInCreate: Create invoice in
vat: VAT
labeler: Labeler
dueDay: Due day
intrastat: Intrastat
corrective: Corrective
order: Orders
orderList: List
orderCreate: New order
catalog: Catalog
volume: Volume
workers: Workers
workerCreate: New worker
department: Department
@ -226,10 +253,10 @@ globals:
wagonsList: Wagons List
wagonCreate: Create wagon
wagonEdit: Edit wagon
wagonCounter: Trolley counter
typesList: Types List
typeCreate: Create type
typeEdit: Edit type
wagonCounter: Trolley counter
roadmap: Roadmap
stops: Stops
routes: Routes
@ -238,21 +265,16 @@ globals:
routeCreate: New route
RouteRoadmap: Roadmaps
RouteRoadmapCreate: Create roadmap
RouteExtendedList: Router
autonomous: Autonomous
suppliers: Suppliers
supplier: Supplier
expedition: Expedition
services: Service
components: Components
pictures: Pictures
packages: Packages
tracking: Tracking
labeler: Labeler
supplierCreate: New supplier
accounts: Accounts
addresses: Addresses
agencyTerm: Agency agreement
travel: Travels
create: Create
extraCommunity: Extra community
travelCreate: New travel
history: Log
@ -260,14 +282,13 @@ globals:
items: Items
diary: Diary
tags: Tags
create: Create
buyRequest: Buy requests
fixedPrice: Fixed prices
buyRequest: Buy requests
wasteBreakdown: Waste breakdown
itemCreate: New item
barcode: Barcodes
tax: Tax
botanical: Botanical
barcode: Barcodes
itemTypeCreate: New item type
family: Item Type
lastEntries: Last entries
@ -283,13 +304,20 @@ globals:
formation: Formation
locations: Locations
warehouses: Warehouses
saleTracking: Sale tracking
roles: Roles
connections: Connections
acls: ACLs
mailForwarding: Mail forwarding
mailAlias: Mail alias
privileges: Privileges
observation: Notes
expedition: Expedition
saleTracking: Sale tracking
services: Service
tracking: Tracking
components: Components
pictures: Pictures
packages: Packages
ldap: LDAP
samba: Samba
twoFactor: Two factor
@ -300,27 +328,12 @@ globals:
serial: Serial
medical: Mutual
pit: IRPF
RouteExtendedList: Router
wasteRecalc: Waste recaclulate
operator: Operator
parking: Parking
supplier: Supplier
created: Created
worker: Worker
now: Now
name: Name
new: New
comment: Comment
observations: Observations
goToModuleIndex: Go to module index
unsavedPopup:
title: Unsaved changes will be lost
subtitle: Are you sure exit without saving?
createInvoiceIn: Create invoice in
myAccount: My account
noOne: No one
maxTemperature: Max
minTemperature: Min
params:
clientFk: Client id
salesPersonFk: Sales person
@ -338,18 +351,13 @@ globals:
supplierFk: Supplier
supplierRef: Supplier ref
serial: Serial
amount: Importe
amount: Amount
awbCode: AWB
correctedFk: Rectified
correctingFk: Rectificative
daysOnward: Days onward
countryFk: Country
companyFk: Company
changePass: Change password
deleteConfirmTitle: Delete selected elements
changeState: Change state
raid: 'Raid {daysInForward} days'
isVies: Vies
errors:
statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred
@ -469,6 +477,18 @@ entry:
invoiceNumber: Invoice
reference: Ref/Alb/Guide
ticket:
params:
ticketFk: Ticket ID
weekDay: Weekday
agencyModeFk: Agency
id: Worker
state: State
created: Created
externalId: External ID
counter: Counter
freightItemName: Freight item name
packageItemName: Package item name
longName: Long name
card:
customerId: Customer ID
customerCard: Customer card
@ -519,6 +539,7 @@ invoiceOut:
card:
issued: Issued
customerCard: Customer card
ticketList: Ticket List
summary:
issued: Issued
dued: Due
@ -556,42 +577,6 @@ invoiceOut:
comercial: Comercial
errors:
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:
chat: Chat
bossDepartment: Boss Department
@ -624,7 +609,7 @@ worker:
fi: DNI/NIE/NIF
birth: Birth
isFreelance: Freelance
isSsDiscounted: Bonificación SS
isSsDiscounted: SS Bonification
hasMachineryAuthorized: Machinery authorized
isDisable: Disable
notificationsManager:
@ -710,6 +695,11 @@ wagon:
minHeightBetweenTrays: 'The minimum height between trays is '
maxWagonHeight: 'The maximum height of the wagon is '
uncompleteTrays: There are incomplete trays
params:
label: Label
plate: Plate
volume: Volume
name: Name
supplier:
list:
@ -778,6 +768,9 @@ supplier:
consumption:
entry: Entry
travel:
search: Search travel
searchInfo: You can search by travel id or name
id: Id
travelList:
tableVisibleColumns:
ref: Reference
@ -786,6 +779,7 @@ travel:
totalEntries: Total entries
totalEntriesTooltip: Total entries
daysOnward: Landed days onwards
awb: AWB
summary:
entryId: Entry Id
freight: Freight
@ -807,62 +801,6 @@ travel:
destination: Destination
thermograph: Thermograph
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:
topbar: {}
itemsFilterPanel:
@ -870,14 +808,17 @@ components:
value: Value
# ItemFixedPriceFilter
buyerFk: Buyer
warehouseFk: Warehouse
started: From
ended: To
mine: For me
hasMinPrice: Minimum price
warehouseFk: Warehouse
# LatestBuysFilter
salesPersonFk: Buyer
supplierFk: Supplier
from: From
to: To
visible: Is visible
active: Is active
floramondo: Is floramondo
showBadDates: Show future items

View File

@ -5,9 +5,11 @@ globals:
language: Idioma
quantity: Cantidad
entity: Entidad
preview: Vista previa
user: Usuario
details: Detalles
collapseMenu: Contraer menú lateral
advancedMenu: Menú avanzado
backToDashboard: Volver al tablón
notifications: Notificaciones
userPanel: Panel de usuario
@ -58,6 +60,7 @@ globals:
downloadCSVSuccess: Descarga de CSV exitosa
reference: Referencia
agency: Agencia
entry: Entrada
warehouseOut: Alm. salida
warehouseIn: Alm. entrada
landed: F. entrega
@ -132,6 +135,26 @@ globals:
medium: Mediano/a
big: Grande
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:
logIn: Inicio de sesión
addressEdit: Modificar consignatario
@ -154,17 +177,17 @@ globals:
inheritedRoles: Roles heredados
customers: Clientes
customerCreate: Nuevo cliente
createCustomer: Crear cliente
createOrder: Nuevo pedido
list: Listado
webPayments: Pagos Web
extendedList: Listado extendido
notifications: Notificaciones
defaulter: Morosos
createCustomer: Crear cliente
fiscalData: Datos fiscales
billingData: Forma de pago
consignees: Consignatarios
'address-create': Nuevo consignatario
address-create: Nuevo consignatario
notes: Notas
credits: Créditos
greuges: Greuges
@ -230,10 +253,10 @@ globals:
wagonsList: Listado vagones
wagonCreate: Crear tipo
wagonEdit: Editar tipo
wagonCounter: Contador de carros
typesList: Listado tipos
typeCreate: Crear tipo
typeEdit: Editar tipo
wagonCounter: Contador de carros
roadmap: Troncales
stops: Paradas
routes: Rutas
@ -242,8 +265,8 @@ globals:
routeCreate: Nueva ruta
RouteRoadmap: Troncales
RouteRoadmapCreate: Crear troncal
autonomous: Autónomos
RouteExtendedList: Enrutador
autonomous: Autónomos
suppliers: Proveedores
supplier: Proveedor
supplierCreate: Nuevo proveedor
@ -308,23 +331,9 @@ globals:
wasteRecalc: Recalcular mermas
operator: Operario
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:
title: Los cambios que no haya guardado se perderán
subtitle: ¿Seguro que quiere salir sin guardar?
createInvoiceIn: Crear factura recibida
myAccount: Mi cuenta
noOne: Nadie
maxTemperature: Máx
minTemperature: Mín
params:
clientFk: Id cliente
salesPersonFk: Comercial
@ -347,11 +356,6 @@ globals:
packing: ITP
countryFk: País
companyFk: Empresa
changePass: Cambiar contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado
raid: 'Redada {daysInForward} días'
isVies: Vies
errors:
statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor
@ -470,6 +474,18 @@ entry:
invoiceNumber: Nº Factura
reference: Ref/Alb/Guía
ticket:
params:
ticketFk: ID de ticket
weekDay: Salida
agencyModeFk: Agencia
id: Comercial
created: Creado
state: Estado
externalId: ID externo
counter: Contador
freightItemName: Nombre
packageItemName: Embalaje
longName: Descripción
card:
customerId: ID cliente
customerCard: Ficha del cliente
@ -621,6 +637,15 @@ worker:
role: Rol
sipExtension: Extensión
locker: Taquilla
fiDueDate: F. caducidad DNI
sex: Sexo
seniority: Antigüedad
fi: DNI/NIE/NIF
birth: F. nacimiento
isFreelance: Autónomo
isSsDiscounted: Bonificación SS
hasMachineryAuthorized: Autorizado para maquinaria
isDisable: Deshabilitado
notificationsManager:
activeNotifications: Notificaciones activas
availableNotifications: Notificaciones disponibles
@ -704,6 +729,11 @@ wagon:
minHeightBetweenTrays: 'La distancia mínima entre bandejas es '
maxWagonHeight: 'La altura máxima del vagón es '
uncompleteTrays: Hay bandejas sin completar
params:
label: Etiqueta
plate: Matrícula
volume: Volumen
name: Nombre
supplier:
list:
payMethod: Método de pago
@ -770,6 +800,9 @@ supplier:
consumption:
entry: Entrada
travel:
search: Buscar envío
searchInfo: Buscar envío por id o nombre
id: Id
travelList:
tableVisibleColumns:
ref: Referencia
@ -778,6 +811,7 @@ travel:
totalEntries:
totalEntriesTooltip: Entradas totales
daysOnward: Días de llegada en adelante
awb: AWB
summary:
entryId: Id entrada
freight: Porte
@ -799,62 +833,6 @@ travel:
destination: Destino
thermograph: Termógrafo
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:
topbar: {}
itemsFilterPanel:
@ -862,6 +840,7 @@ components:
value: Valor
# ItemFixedPriceFilter
buyerFk: Comprador
warehouseFk: Almacen
started: Desde
ended: Hasta
mine: Para mi
@ -869,7 +848,11 @@ components:
wareHouseFk: Almacén
# LatestBuysFilter
salesPersonFk: Comprador
supplierFk: Proveedor
visible: Visible
active: Activo
from: Desde
to: Hasta
floramondo: Floramondo
showBadDates: Ver items a futuro
userPanel:

View File

@ -2,6 +2,8 @@
import { Dark, Quasar } from 'quasar';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { localeEquivalence } from 'src/i18n/index';
import quasarLang from 'src/utils/quasarLang';
const { t, locale } = useI18n();
@ -12,18 +14,9 @@ const userLocale = computed({
set(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:
// 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) {
//
}
quasarLang(value);
},
});

View File

@ -6,6 +6,7 @@ import FormModelPopup from 'components/FormModelPopup.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
const { t } = useI18n();
const router = useRouter();
@ -61,10 +62,10 @@ const redirectToAccountBasicData = (_, { id }) => {
hide-selected
:rules="validate('VnUser.roleFk')"
/>
<VnInput
<VnInputPassword
v-model="data.password"
:label="t('ldap.password')"
type="password"
:toggle-visibility="true"
:rules="validate('VnUser.password')"
/>
<QCheckbox

View File

@ -8,6 +8,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
const { t } = useI18n();
const { notify } = useNotify();
@ -128,10 +129,9 @@ onMounted(async () => await getInitialLdapConfig());
:required="true"
:rules="validate('LdapConfig.rdn')"
/>
<VnInput
<VnInputPassword
:label="t('ldap.password')"
clearable
type="password"
v-model="data.password"
:required="true"
:rules="validate('LdapConfig.password')"

View File

@ -4,9 +4,9 @@ import { computed, ref } from 'vue';
import VnTable from 'components/VnTable/VnTable.vue';
import AccountSummary from './Card/AccountSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnInput from 'src/components/common/VnInput.vue';
import VnSection from 'src/components/common/VnSection.vue';
import FetchData from 'src/components/FetchData.vue';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
@ -168,10 +168,9 @@ function exprBuilder(param, value) {
>
<template #more-create-dialog="{ data }">
<QCardSection>
<VnInput
<VnInputPassword
:label="t('Password')"
v-model="data.password"
type="password"
:required="true"
autocomplete="new-password"
/>

View File

@ -8,6 +8,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
import VnInputPassword from 'src/components/common/VnInputPassword.vue';
const { t } = useI18n();
const { notify } = useNotify();
@ -143,10 +144,9 @@ onMounted(async () => await getInitialSambaConfig());
v-model="data.adUser"
:rules="validate('SambaConfigs.adUser')"
/>
<VnInput
<VnInputPassword
:label="t('samba.passwordAD')"
clearable
type="password"
v-model="data.adPassword"
/>
<VnInput

View File

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

View File

@ -1,14 +1,17 @@
<script setup>
import axios from 'axios';
import { computed, ref, toRefs } from 'vue';
import { computed, onMounted, ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useRoute } from 'vue-router';
import { useAcl } from 'src/composables/useAcl';
import { useArrayData } from 'src/composables/useArrayData';
import { useState } from 'src/composables/useState';
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 { useQuasar } from 'quasar';
import { useRouter } from 'vue-router';
const $props = defineProps({
hasAccount: {
@ -17,14 +20,20 @@ const $props = defineProps({
required: true,
},
});
const { t } = useI18n();
const { hasAccount } = toRefs($props);
const { openConfirmationModal } = useVnConfirm();
const route = useRoute();
const router = useRouter();
const state = useState();
const user = state.getUser();
const { notify } = useQuasar();
const account = computed(() => useArrayData('AccountId').store.data[0]);
account.value.hasAccount = hasAccount.value;
const entityId = computed(() => +route.params.id);
const hasitManagementAccess = ref();
const hasSysadminAccess = ref();
async function updateStatusAccount(active) {
if (active) {
@ -36,7 +45,7 @@ async function updateStatusAccount(active) {
account.value.hasAccount = active;
const status = active ? 'enable' : 'disable';
notify({
message: t(`account.card.${status}Account.success`),
message: t(`account.card.actions.${status}Account.success`),
type: 'positive',
});
}
@ -49,6 +58,17 @@ async function updateStatusUser(active) {
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 syncPassword = ref(null);
const shouldSyncPassword = ref(false);
@ -63,11 +83,27 @@ async function sync() {
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>
<template>
<VnChangePassword
ref="changePassRef"
:ask-old-pass="true"
:ask-old-pass="askOldPass"
:submit-fn="
async (newPassword, oldPassword) => {
await axios.patch(`Accounts/change-password`, {
@ -97,7 +133,7 @@ async function sync() {
<QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip>
</QIcon></QCheckbox
>
<QInput
<VnInputPassword
v-if="shouldSyncPassword"
:label="t('login.password')"
v-model="syncPassword"
@ -109,18 +145,46 @@ async function sync() {
</template>
</VnConfirm>
<QItem
v-if="
entityId == account.id &&
useAcl().hasAny([{ model: 'Account', props: '*', accessType: 'WRITE' }])
"
v-if="hasitManagementAccess"
v-ripple
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
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
clickable
@click="
@ -135,7 +199,7 @@ async function sync() {
</QItem>
<QItem
v-if="!account.active"
v-if="!account.active && hasitManagementAccess"
v-ripple
clickable
@click="
@ -149,7 +213,7 @@ async function sync() {
<QItemSection>{{ t('account.card.actions.activateUser.name') }}</QItemSection>
</QItem>
<QItem
v-if="account.active"
v-if="account.active && hasitManagementAccess"
v-ripple
clickable
@click="
@ -162,7 +226,12 @@ async function sync() {
>
<QItemSection>{{ t('account.card.actions.deactivateUser.name') }}</QItemSection>
</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>
</QItem>
<QSeparator />

View File

@ -9,6 +9,7 @@ import AccountMailAliasCreateForm from './AccountMailAliasCreateForm.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import useHasAccount from 'src/composables/useHasAccount.js';
import axios from 'axios';
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) => {
await axios.delete(`${urlPath}/${row.id}`);
fetchMailAliases();
@ -79,7 +70,7 @@ const fetchMailAliases = async () => {
const getAccountData = async (reload = true) => {
loading.value = true;
hasAccount.value = await fetchAccountExistence();
hasAccount.value = await useHasAccount(route.params.id);
if (!hasAccount.value) {
loading.value = false;
store.data = [];

View File

@ -9,6 +9,7 @@ import VnRow from 'components/ui/VnRow.vue';
import axios from 'axios';
import { useStateStore } from 'stores/useStateStore';
import useNotify from 'src/composables/useNotify.js';
import useHasAccount from 'src/composables/useHasAccount';
const { t } = useI18n();
const route = useRoute();
@ -30,23 +31,9 @@ const hasDataChanged = computed(
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 () => {
try {
const response = await axios.get(`MailForwards/${route.params.id}`);
return response.data;
} catch {
return null;
}
const response = await axios.get(`MailForwards/${route.params.id}`);
return response.data;
};
const deleteMailForward = async () => {
@ -72,7 +59,7 @@ const setInitialData = async () => {
loading.value = true;
initialData.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) {
loading.value = false;
return;

View File

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

View File

@ -50,7 +50,7 @@ const removeRole = async () => {
>
<template #menu>
<QItem v-ripple clickable @click="removeRole()">
<QItemSection>{{ t('Delete') }}</QItemSection>
<QItemSection>{{ t('globals.delete') }}</QItemSection>
</QItem>
</template>
<template #body="{ entity }">

View File

@ -1,4 +1,18 @@
account:
params:
id: Id
name: Name
roleFk: Role
nickname: Nickname
password: Password
active: Active
search: Id
description: Description
alias: Alias
model: Model
principalId: Role
property: Property
accessType: Access
card:
nickname: User
role: Role

View File

@ -1,4 +1,20 @@
accessType: Acceso
property: Propiedad
account:
params:
id: Id
name: Nombre
roleFk: Rol
nickname: Nickname
password: Contraseña
active: Activo
search: Id
description: Descripción
alias: Alias
model: Modelo
principalId: Rol
property: Propiedad
accessType: Acceso
card:
nickname: Usuario
role: Rol

View File

@ -4,7 +4,6 @@ import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
import { useStateStore } from 'src/stores/useStateStore';
import { toDate, toPercentage, toCurrency } from 'filters/index';
import { tMobile } from 'src/composables/tMobile';
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 TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue';
const { t } = useI18n();
const quasar = useQuasar();
const route = useRoute();
const stateStore = computed(() => useStateStore());
const claim = ref(null);
const claimRef = ref();
const claimId = route.params.id;
@ -201,58 +200,62 @@ async function post(query, params) {
auto-load
@on-fetch="(data) => (destinationTypes = data)"
/>
<Teleport to="#right-panel" v-if="stateStore.isHeaderMounted() && claim">
<QCard class="totalClaim q-my-md q-pa-sm no-box-shadow">
{{ `${t('Total claimed')}: ${toCurrency(totalClaimed)}` }}
</QCard>
<QCard class="q-mb-md q-pa-sm no-box-shadow">
<QItem class="justify-between">
<QItemLabel class="slider-container">
<p class="text-primary">
{{ t('claim.actions') }}
</p>
<QSlider
class="responsibility { 'background-color:primary': quasar.platform.is.mobile }"
v-model="claim.responsibility"
:label-value="t('claim.responsibility')"
@change="(value) => save({ responsibility: value })"
label-always
color="primary"
markers
:marker-labels="marker_labels"
:min="DEFAULT_MIN_RESPONSABILITY"
:max="DEFAULT_MAX_RESPONSABILITY"
<RightMenu v-if="claim">
<template #right-panel>
<QCard class="totalClaim q-my-md q-pa-sm no-box-shadow">
{{ `${t('Total claimed')}: ${toCurrency(totalClaimed)}` }}
</QCard>
<QCard class="q-mb-md q-pa-sm no-box-shadow">
<QItem class="justify-between">
<QItemLabel class="slider-container">
<p class="text-primary">
{{ t('claim.actions') }}
</p>
<QSlider
class="responsibility { 'background-color:primary': quasar.platform.is.mobile }"
v-model="claim.responsibility"
:label-value="t('claim.responsibility')"
@change="(value) => save({ responsibility: value })"
label-always
color="primary"
markers
:marker-labels="marker_labels"
: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>
</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 })"
</QCard>
<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"
/>
<span>{{ t('mana') }}</span>
</QItemLabel>
</QCard>
<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>
</QCard>
</template>
</RightMenu>
<CrudModel
v-if="claim"
data-key="ClaimEnds"

View File

@ -1,21 +1,13 @@
<script setup>
import VnCard from 'components/common/VnCard.vue';
import VnCardBeta from 'components/common/VnCardBeta.vue';
import ClaimDescriptor from './ClaimDescriptor.vue';
import ClaimFilter from '../ClaimFilter.vue';
import filter from './ClaimFilter.js';
</script>
<template>
<VnCard
data-key="Claim"
base-url="Claims"
:descriptor="ClaimDescriptor"
:filter-panel="ClaimFilter"
search-data-key="ClaimList"
<VnCardBeta
data-key="Claim"
base-url="Claims"
:descriptor="ClaimDescriptor"
:filter="filter"
:searchbar-props="{
url: 'Claims/filter',
label: 'Search claim',
info: 'You can search by claim id or customer name',
}"
/>
</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 CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ClaimDescriptorMenu from './ClaimDescriptorMenu.vue';
const route = useRoute();
const router = useRouter();
@ -228,6 +229,9 @@ function claimUrl(section) {
</QList>
</QBtnDropdown>
</template>
<template #menu="{ entity }">
<ClaimDescriptorMenu :claim="entity.claim" />
</template>
<template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one" v-if="$route.name != 'ClaimSummary'">
<VnTitle

View File

@ -93,16 +93,7 @@ defineExpose({ states });
outlined
rounded
dense
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.name }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
/>
<VnSelect
:label="t('claim.responsible')"
v-model="params.claimResponsibleFk"

View File

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

View File

@ -44,3 +44,5 @@ claim:
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}'
noData: 'There are no images/videos, click here or drag and drop the file'
dragDrop: Drag and drop it here
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:
customer: Cliente
code: Código
@ -46,3 +44,5 @@ claim:
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'
dragDrop: Arrastra y suelta aquí
search: Buscar reclamación
searchInfo: Puedes buscar por ID de la reclamación o nombre del cliente

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