#7793 - sortByWeight #763

Closed
jsegarra wants to merge 11 commits from 7793_sortByWeight into dev
775 changed files with 34926 additions and 21422 deletions
Showing only changes of commit 022ced3116 - 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',
},

8
.gitignore vendored
View File

@ -29,5 +29,9 @@ yarn-error.log*
*.sln
# Cypress directories and files
/tests/cypress/videos
/tests/cypress/screenshots
/test/cypress/videos
/test/cypress/screenshots
# VitePress directories and files
/docs/.vitepress/cache
/docs/.vuepress

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,670 @@
# Version 25.04 - 2025-01-28
### Added 🆕
- chore: add task comment by:jorgep
- chore: refs #8198 rollback by:jorgep
- chore: refs #8322 unnecessary prop by:alexm
- feat: refs #7055 added new test case by:provira
- feat: refs #7055 created FilterItemForm test by:provira
- feat: refs #7077 created test for VnInputTime by:provira
- feat: refs #7078 created test for VnJsonValue by:provira
- feat: refs #7087 added more test cases by:provira
- feat: refs #7087 added new test by:provira
- feat: refs #7087 created CardSummary test by:provira
- feat: refs #7088 created test for FetchedTags by:provira
- feat: refs #7202 added new field by:Jon
- feat: refs #7882 Added coords to create a address by:guillermo
- feat: refs #7957 add tooltip and i18n support for search link in VnSearchbar component by:jorgep
- feat: refs #7957 enhance search functionality and improve data filtering logic by:jorgep
- feat: refs #7957 open in new tab by:jorgep
- feat: refs #7957 simplify fn to by:jorgep
- feat: refs #7957 update VnSearchbar component with improved search URL handling and styling enhancements by:jorgep
- feat: refs #8117 filters and values added as needed by:jtubau
- feat: refs #8197 useHasContent and use in VnSection and RightMenu by:alexm
- feat: refs #8219 added invoice out e2e tests by:Jon
- feat: refs #8219 global invoicing e2e by:Jon
- feat: refs #8220 added barcodes e2e test by:Jon
- feat: refs #8220 created items e2e by:Jon
- feat: refs #8220 modified create item form and added respective e2e by:Jon
- feat: refs #8225 added account and invoiceOut modules by:Jon
- feat: refs #8225 added entry module and fixed translations by:Jon
- feat: refs #8225 added invoiceIn and travel module by:Jon
- feat: refs #8225 added moreOptions and use it in customer and ticket summary by:Jon
- feat: refs #8225 added route and shelving module by:Jon
- feat: refs #8225 added worker and zone modules by:Jon
- feat: refs #8225 use it in claim, item and order modules by:Jon
- feat: refs #8258 added button to pass to uppercase by:provira
- feat: refs #8258 added uppercase option to VnInput by:provira
- feat: refs #8258 added uppercase validation on supplier create by:provira
- feat: refs #8298 add price optimum input and update translations for bonus and price optimum by:jgallego
- feat: refs #8316 add entryFilter prop to VnTable component in EntryList by:jtubau
- feat: refs #8322 added department changes by:provira
- feat: refs #8372 workerPBX by:robert
- feat: refs #8381 add initial and final temperature fields to entry forms and summaries by:jgallego
- feat: refs #8381 add initial and final temperature labels in English and Spanish locales by:jgallego
- feat: refs #8381 add toCelsius filter and update temperature fields in entry forms and summaries by:jgallego
- feat: skip tests by:jorgep
- style: refs #7957 update VnSearchbar padding for improved layout by:jorgep
### Changed 📦
- perf: refs #8219 #8219 minor change by:Javier Segarra
- perf: refs #8220 on-fetch and added missing translations by:Jon
- perf: refs #8220 on-fetch by:Jon
- perf: refs #8220 translations by:Jon
- perf: refs #8220 use searchbar selector in e2e tests by:Jon
- perf: remove warning default value by:Javier Segarra
- refactor: redirect using params by:Jon
- refactor: refs #7077 removed some comments by:provira
- refactor: refs #7087 removed unused imports by:provira
- refactor: refs #7100 added const mockData by:jtubau
- refactor: refs #7100 delete unnecesary set prop by:jtubau
- refactor: refs #7100 refactorized with methods by:jtubau
- refactor: refs #7957 remove blank by:jorgep
- refactor: refs #8198 simplify data fetching and filtering logic by:jorgep
- refactor: refs #8198 simplify state management and data fetching in ItemDiary component by:jorgep
- refactor: refs #8219 modified e2e tests and fixed some translations by:Jon
- refactor: refs #8219 modified list test, created cypress download folder and added to gitignore by:Jon
- refactor: refs #8219 requested changes by:Jon
- refactor: refs #8219 use checkNotification command by:Jon
- refactor: refs #8220 added data-cy for e2e tests by:Jon
- refactor: refs #8220 requested changes by:Jon
- refactor: refs #8220 skip failling test and modifed tag test by:Jon
- refactor: refs #8225 requested changes by:Jon
- refactor: refs #8247 use new acl for sysadmin by:Jon
- refactor: refs #8316 added claimFilter by:jtubau
- refactor: refs #8316 added entryFilter by:jtubau
- refactor: refs #8316 add new localization keys and update existing ones for entry components by:jtubau
- refactor: refs #8316 moved localizations to local locale by:jtubau
- refactor: refs #8316 move order localization by:jtubau
- refactor: refs #8316 remove unused OrderSearchbar component by:jtubau
- refactor: refs #8316 update EntryCard to use user-filter prop and remove exprBuilder from EntryList by:jtubau
- refactor: refs #8316 used VnSection and VnCardBeta by:jtubau
- refactor: refs #8322 changed translations by:provira
- refactor: refs #8322 changed Worker component to use VnSection/VnCardBeta by:provira
- refactor: refs #8322 set department inside worker by:alexm
- refactor: skip intermitent failing test by:Jon
### Fixed 🛠️
- feat: refs #8225 added entry module and fixed translations by:Jon
- fix: added missing translations in InvoiceIn by:provira
- fix: changed invoiceIn for InvoiceIn by:provira
- fix: changed translations to only use "invoicein" by:provira
- fix: department descriptor link by:Jon
- fix: e2e tests by:Jon
- fix: entry summary view and build warnings by:Jon
- fix: fixed InvoiceIn filter translations by:provira
- fix: modified setData in customerDescriptor to show the icons by:Jon
- fix: redirect to TicketSale from OrderLines (origin/Fix-RedirectToTicketSale) by:Jon
- fix: redirect when confirming lines by:Jon
- fix: refs #7055 #7055 #7055 fixed some tests by:provira
- fix: refs #7077 removed unused imports by:provira
- fix: refs #7078 added missing case with array by:provira
- fix: refs #7087 fixed some tests by:provira
- fix: refs #7088 changed "vm.vm" to "vm" by:provira
- fix: refs #7088 changed wrapper to vm by:provira
- fix: refs #7699 add icons and hint by:carlossa
- fix: refs #7699 add pwd vnInput by:carlossa
- fix: refs #7699 fix component by:carlossa
- fix: refs #7699 fix password visibility by:carlossa
- fix: refs #7699 fix tfront clean code by:carlossa
- fix: refs #7699 fix vnChangePassword, clean VnInput by:carlossa
- fix: refs #7699 fix vnInputPassword by:carlossa
- fix: refs #7957 add missing closing brace by:jorgep
- fix: refs #7957 css by:jorgep
- fix: refs #7957 rollback by:jorgep
- fix: refs #7957 update data-cy by:jorgep
- fix: refs #7957 update visibility handling for clear icon in VnInput component by:jorgep
- fix: refs #7957 vn-searchbar test by:jorgep
- fix: refs #8117 update salesPersonFk filter options and URL for improved data retrieval by:jtubau
- fix: refs #8197 not use yet by:alexm
- fix: refs #8198 update query param by:jorgep
- fix: refs #8219 fixed e2e tests by:Jon
- fix: refs #8219 fixed summary and global tests by:Jon
- fix: refs #8219 forgotten dataCy by:Jon
- fix: refs #8219 global e2e by:Jon
- fix: refs #8219 requested changes by:Jon
- fix: refs #8220 itemTag test by:Javier Segarra
- fix: refs #8225 invoice in translations by:Jon
- fix: refs #8243 fixed SkeletonSummary by:provira
- fix: refs #8247 conflicts by:Jon
- fix: refs #8247 fixed acls and added lost options by:Jon
- fix: refs #8316 ref="claimFilterRef" by:alexm
- fix: refs #8316 userFilter by:alexm
- fix: refs #8316 use rightMenu by:alexm
- fix: refs #8316 use section-searchbar by:alexm
- fix: refs #8317 disable action buttons when no rows are selected in ItemFixedPrice by:jtubau
- fix: refs #8322 unnecessary section by:alexm
- fix: refs #8338 fixed VnTable translations by:provira
- fix: refs #8338 removed chipLocale property/added more translations by:provira
- fix: refs #8448 e2e by:Jon
- fix: refs #8448 not use croppie by:alexm
- fix: remove departmentCode by:Javier Segarra
- fix: removed unused searchbar by:PAU ROVIRA ROSALENY
- fix: skip failling e2e by:Jon
- fix: sort by name in description by:Jon
- fix: translations by:Jon
- fix: use entryFilter by:alexm
- fix(VnCardBeta): add userFilter by:alexm
- refactor: refs #8219 modified e2e tests and fixed some translations by:Jon
- revert: revert header by:alexm
- test: fix expedition e2e by:alexm
# 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 🆕
- feat: add reportFileName option by:Javier Segarra
- feat: all clients just with global series by:jgallego
- feat: improve Merge branch 'test' into dev by:Javier Segarra
- feat: manual invoice in two lines by:jgallego
- feat: manualInvoice with address by:jgallego
- feat: randomize functions and example by:Javier Segarra
- feat: refs #6999 added search when user tabs on a filter with value by:Jon
- feat: refs #6999 added tab to search in VnTable filter by:Jon
- feat: refs #7346 #7346 improve form by:Javier Segarra
- feat: refs #7346 address ordered by:jgallego
- feat: refs #7346 radioButton by:jgallego
- feat: refs #7346 style radioButton by:jgallego
- feat: refs #7346 traducciones en cammelCase (7346-manualInvoice) by:jgallego
- feat: refs #8038 added new functionality in VnSelect and refactor styles by:Jon
- feat: refs #8061 #8061 updates by:Javier Segarra
- feat: refs #8087 reactive data by:jorgep
- feat: refs #8087 refs#8087 Redadas en travel by:Carlos Andrés
- feat: refs #8138 add component ticket problems by:pablone
- feat: refs #8163 add max length and more tests by:wbuezas
- feat: refs #8163 add prop by:wbuezas
- feat: refs #8163 add VnInput insert functionality and e2e test by:wbuezas
- feat: refs #8163 limit with maxLength by:Javier Segarra
- feat: refs #8163 maxLength SupplierFD account by:Javier Segarra
- feat: refs #8163 maxLengthVnInput by:Javier Segarra
- feat: refs #8163 use VnAccountNumber in VnAccountNumber by:Javier Segarra
- feat: refs #8166 show notification by:jorgep
### Changed 📦
- feat: refs #8038 added new functionality in VnSelect and refactor styles by:Jon
- perf: add dataCy by:Javier Segarra
- perf: refs #7346 #7346 Imrpove interface dialog by:Javier Segarra
- perf: refs #7346 #7346 use v-show instead v-if by:Javier Segarra
- perf: refs #8036 currentFilter by:alexm
- perf: refs #8061 filter autonomy by:Javier Segarra
- perf: refs #8061 solve conflicts and random posCode it by:Javier Segarra
- perf: refs #8061 use opts from VnSelect by:Javier Segarra
- perf: refs #8163 #8061 createNewPostCodeForm by:Javier Segarra
- perf: remove console by:Javier Segarra
- perf: remove timeout by:Javier Segarra
- perf: test command fillInForm by:Javier Segarra
- refactor: refs #8162 remove comment by:wbuezas
- refactor: remove unnecesary things by:wbuezas
### Fixed 🛠️
- fix: #8016 fetching data by:Javier Segarra
- fix: icons by:jgallego
- fix: refs #7229 download file by:jorgep
- fix: refs #7229 remove catch by:jorgep
- fix: refs #7229 set url by:jorgep
- fix: refs #7229 test by:jorgep
- fix: refs #7229 url by:jorgep
- fix: refs #7229 url + test by:jorgep
- fix: refs #7304 7304 clean warning by:carlossa
- fix: refs #7304 fix list by:carlossa
- fix: refs #7304 fix warning by:carlossa
- fix: refs #7346 traslations by:jgallego
- fix: refs #7529 add save by:carlossa
- fix: refs #7529 fix e2e by:carlossa
- fix: refs #7529 fix front by:carlossa
- fix: refs #7529 fix scss by:carlossa
- fix: refs #7529 fix te2e by:carlossa
- fix: refs #7529 fix workerPit e2e by:carlossa
- fix: refs #7529 front by:carlossa
- fix: refs #8036 apply exprBuilder after save filters by:alexm
- fix: refs #8036 only add where when required by:alexm
- fix: refs #8038 solve conflicts by:Jon
- fix: refs #8061 improve code dependencies (origin/8061_improve_newCP) by:Javier Segarra
- fix: refs #8138 move component from ui folder by:pablone
- fix: refs #8138 sme minor issues by:pablone
- fix: refs #8163 #8061 createNewPostCodeForm by:Javier Segarra
- fix: refs #8163 minor problem when keypress by:Javier Segarra
- fix: refs #8166 show zone error by:jorgep
- fix: removed selectedClient by:jgallego
- refs #7529 fix workerPit by:carlossa
- revert: refs #8061 test #8061 updates by:Javier Segarra
- test: fix own test by:Javier Segarra
- test: refs #8162 #8162 fix TicketList spec by:Javier Segarra
# Version 24.48 - 2024-11-25
### Added 🆕
- chore: correct checkNotification (fix_customer_issues) by:alexm
- chore: perf (warmFix_order_equalSalix) by:alexm
- chore: refs #6818 add spaces by:jorgep
- chore: refs #6818 drop useless code & comment by:jorgep
- chore: refs #7273 sticky add btn & refactor by:jorgep
- chore: refs #7524 fix test by:jorgep
- chore: refs #8039 not required by:alexm
- chore: refs #8078 fiz tests by:jorgep
- chore: refs #8078 rollback ref by:jorgep
- chore: remove console.log (warmFix_invoiceOut_Global) by:alexm
- chore: typo (fix_itemType-redirection) by:alexm
- feat: #6943 use openURL quasar by:Javier Segarra
- feat: #7782 add cypress report by:Javier Segarra
- feat: #7782 cypress.config watchForFileChanges by:Javier Segarra
- feat: #7782 npm run resetDatabase by:Javier Segarra
- feat: #7782 waitUntil domContentLoad by:Javier Segarra
- feat: added composable to confirm orders by:Jon
- feat: add /reports in gitignore (warmFix_reports_in_gitignore) by:alexm
- feat: apply changes for customerModule by:Javier Segarra
- feat: disabled buttons by:Javier Segarra
- feat: move buttons to DescriptorMenu by:Javier Segarra
- feat: refs #6818 add icon by:jorgep
- feat: refs #6818 fetch url & default channel by:jorgep
- feat: refs #6818 saysimple integration by:jorgep
- feat: refs #6839 module searching (6839-addSearchMenu) by:jorgep
- feat: refs #6839 normalize search by:jorgep
- feat: refs #6919 sync entry data by:jorgep
- feat: refs #7006 itemType basic data new inputs by:guillermo
- feat: refs #7006 itemTypeLog added by:guillermo
- feat: refs #7193 modified parking to use the scope and corrected small errors by:Jon
- feat: refs #7206 added inactive label and corrected minor errors by:Jon
- feat: refs #7308 #7308 remove warnings related to useSession by:Javier Segarra
- feat: refs #7349 usa back con permisos by:jgallego
- feat: refs #7524 add front test by:jorgep
- feat: refs #7874 improve vn-notes ui by:jorgep
- feat: refs #7970 notify changes by:Jon
- feat(): refs #8039 canceledError not notify by:alexm
- feat: refs #8039 notify error unify by:alexm
- feat: refs #8039 show duplicate request in local by:alexm
- feat: refs #8078 add shortcut multi selection by:jorgep
- feat: refs #8078 add tests by:jorgep
- feat: refs#8087 Redadas en travel by:Carlos Andrés
- feat: refs #8087 Traspasar redadas a travels by:Carlos Andrés
- feat: remove comments by:Javier Segarra
- feat(Supplier): add companySize by:alexm
- feat: use composable to unify logic by:Javier Segarra
- feat(VnInput): empty to null by:alexm
- feat(VnSelect): order data equal salix by:alexm
- feat(VnSelect): refs #7136 add scroll (7136-vnSelect_paginate_simplify_2) by:alexm
### Changed 📦
- chore: perf (warmFix_order_equalSalix) by:alexm
- chore: refs #7273 sticky add btn & refactor by:jorgep
- fix: better performance (warmFix_accountAcls) by:alexm
- perf: minor bugs detected by:Javier Segarra
- perf: refs #6943 #6943 merge command by:Javier Segarra
- perf: refs #7283 #7283 declare composable inst4ead code duplicated by:Javier Segarra
- perf: refs #7283 #7283 handle composable i18n by:Javier Segarra
- perf: refs #7283 #7283 handle i18n by:Javier Segarra
- perf: refs #7283 #7283 i18n params by:Javier Segarra
- perf: refs #7308 #7308 remove comments by:Javier Segarra
- perf: remove appendParams by:Javier Segarra
- perf: use const in VnLocation by:Javier Segarra
- perf: use required instead :required="true" by:Javier Segarra
- refactor: apply QPopupProxy by:wbuezas
- refactor: changed confirmOrder directory by:Jon
- refactor: change keyup.enter for update:model-value by:wbuezas
- refactor(InvoiceInBasicData): use VnDms by:alexm
- refactor: modified composable by:Jon
- refactor: refs #6818 change channel source by:jorgep
- refactor: refs #6818 channel logic by:jorgep
- refactor: refs #6919 export filter by:jorgep
- refactor: refs #7132 1st wave of changes in global translations files by:Jon
- refactor: refs #7132 account's module translations by:Jon
- refactor: refs #7132 customer's module translations by:Jon
- refactor: refs #7132 deleted pageTitles repeated by:Jon
- refactor: refs #7132 delete duplicate translations' keys by:Jon
- refactor: refs #7132 deleted useless code by:Jon
- refactor: refs #7132 global translations files changed by:Jon
- refactor: refs #7266 Changed method name by:guillermo
- refactor: refs #7950 Created cmr model by:guillermo
- refactor: refs #7970 added emit by:Jon
- refactor: refs #7970 refactored VnConfirm to emit events by:Jon
- refactor: refs #8185 modified LeftMenu to avoid duplicates by:Jon
- refactor: remove unused variable by:wbuezas
- refactor: revert catalog changes by:Jon
- refactor: small change by:wbuezas
- test: refactor e2e by:alexm
- test: refs #8039 add hasNotify and, refactor: agencyWorkCenter test by:alexm
### Fixed 🛠️
- chore: refs #7524 fix test by:jorgep
- fix: better performance (warmFix_accountAcls) by:alexm
- fix: catalog view category and type filter by:wbuezas
- fix: category and tags filters by:Jon
- fix: changed route.query by:Jon
- fix: change type vnput by:Javier Segarra
- fix(ClaimList): stateCode orderBy priority by:alexm
- fix: entryFilters by:carlossa
- fix: filter panel by:Jon
- fix(InvoiceOutGlobal): parallelism by:alexm
- fix: itemBotanical by:Javier Segarra
- fix: itemType redirection and fix filters by:alexm
- fix: logout spec (warmFix_logout.spec) by:alexm
- fix: merge errors by:alexm
- fix: order catalog by:wbuezas
- fix: order catalog fixes by:wbuezas
- fix: refs #6818 use right icon by:jorgep
- fix: refs #6896 fixed module problems by:Jon
- fix: refs #7193 fixed e2e test by:Jon
- fix: refs #7206 deleted duplicate code by:Jon
- fix: refs #7273 use same filter by:jorgep
- fix: refs #7283 #7283 bugs by:Javier Segarra
- fix: refs #7283 #7283 ItemDiary subToolbar by:Javier Segarra
- fix: refs #7283 #7283 ItemSummary bugs by:Javier Segarra
- fix: refs #7283 Account image resolution by:guillermo
- fix: refs #7283 css by:jorgep
- fix: refs #7283 filter by:carlossa
- fix: refs #7283 fix image by:carlossa
- fix: refs #7283 fix pr by:carlossa
- fix: refs #7283 fix preview by:carlossa
- fix: refs #7283 fix required by:carlossa
- fix: refs #7283 item filters by:carlossa
- fix: refs #7283 itemtype fix by:carlossa
- fix: refs #7283 order translation by:carlossa
- fix: refs #7283 preview by:carlossa
- fix: refs #7283 tooltips !Item by:Javier Segarra
- fix: refs #7306 clean warning by:carlossa
- fix: refs #7310 clean warning by:carlossa
- fix: refs #7323 locale #7396 by:jorgep
- fix: refs #7323 show advanced fields by:jorgep
- fix: refs #7349 dependencia no usada by:jgallego
- fix: refs #7524 e2e & worker module by:jorgep
- fix: refs #7874 add title by:jorgep
- fix: refs #7874 show name by:jorgep
- fix: refs #7943 use correct data-key by:jorgep
- fix: refs #7943 use summary by:jorgep
- fix: refs #8039 bad tests by:alexm
- fix: refs #8039 o not handle unnecessary errors by:alexm
- fix: refs #8078 e2e #7970 by:jorgep
- fix: refs #8078 handleSelection by:jorgep
- fix: refs #8078 improve cy command (8078-enableMultiSelection) by:jorgep
- fix: refs #8078 improve handleSelection by:jorgep
- fix: reset category by:wbuezas
- fix: tag chips by:Jon
- fix: vnSearchbar spec (warmFix_vnSearchBar.spec) by:alexm
- fix(VnSelect): setOptions when applyFilter by:alexm
- fix: worker test e2e by:Jon
- Merge branch 'dev' into fix_customer_issues by:Javier Segarra
- refactor: revert catalog changes by:Jon
- refs #7283 fix conflicts by:carlossa
- refs #7283 fix descriptorproxy by:carlossa
- refs #7283 fixedPrice by:carlossa
- refs #7283 fixedPrices by:carlossa
- refs #7283 fix itemFixed by:carlossa
- refs #7283 fix itemFixedPrice by:carlossa
- refs #7283 fix itemMigration by:carlossa
- refs #7283 fix itemMigration list filters by:carlossa
- refs #7283 fix items by:carlossa
- refs #7283 fix items error get images by:carlossa
- refs #7283 fix items images by:carlossa
- refs #7283 fix request by:carlossa
- refs #7283 fix searchbar by:carlossa
- refs #7283 fix viewSummary by:carlossa
- refs #7283 fix yml list basicData by:carlossa
- refs #7283 itemRequest fix by:carlossa
- refs #7283 itemRequest fix deny by:carlossa
- refs #7283 itemRequest fix reload by:carlossa
- refs #72983 fix filters by:carlossa
- revert: commit by:Javier Segarra
- revert e57a253c6f649382da187d1129449d265fb26d3b by:Javier Segarra
- test: #8162 fix clientList spec by:Javier Segarra
- test: #8162 fix vnLocation spec by:Javier Segarra
- test: fix arrayData by:Javier Segarra
- test: fix e2e by:alexm
- test: fix e2e by:Javier Segarra
- test: refs #8039 fix WorkerNotification e2e by:alexm
- test: refs #8039 fix ZoneWarehouse e2e by:alexm
- warmfix: ItemLastEntries to date (origin/warmfix_itemLastEntriesFilter) by:Javier Segarra
# Version 24.40 - 2024-10-02
### Added 🆕

6
Jenkinsfile vendored
View File

@ -4,7 +4,8 @@ def PROTECTED_BRANCH
def BRANCH_ENV = [
test: 'test',
master: 'production'
master: 'production',
beta: 'production'
]
node {
@ -15,7 +16,8 @@ node {
PROTECTED_BRANCH = [
'dev',
'test',
'master'
'master',
'beta'
].contains(env.BRANCH_NAME)
// https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables

View File

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

View File

@ -1,6 +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,
@ -8,16 +11,32 @@ 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,
experimentalRunAllSpecs: false,
watchForFileChanges: false,
reporter: 'cypress-mochawesome-reporter',
reporterOptions: {
charts: true,
reportPageTitle: 'Cypress Inline Reporter',
reportFilename: '[status]_[datetime]-report',
embeddedScreenshots: true,
reportDir: 'test/cypress/reports',
inlineAssets: true,
},
component: {
componentFolder: 'src',
testFiles: '**/*.spec.js',
supportFile: 'test/cypress/support/unit.js',
},
setupNodeEvents(on, config) {
// implement node event listeners here
setupNodeEvents: async (on, config) => {
const plugin = await import('cypress-mochawesome-reporter/plugin');
plugin.default(on);
return config;
},
viewportWidth: 1280,
viewportHeight: 720,
},
});

38
docs/.vitepress/config.js Normal file
View File

@ -0,0 +1,38 @@
import { defineConfig } from 'vitepress';
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: 'Lilium',
description: 'Lilium docs',
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
{ text: 'Components', link: '/components/vnInput' },
{ text: 'Composables', link: '/composables/useArrayData' },
],
sidebar: [
{
items: [
{
text: 'Components',
collapsible: true,
collapsed: true,
items: [{ text: 'VnInput', link: '/components/vnInput' }],
},
{
text: 'Composables',
collapsible: true,
collapsed: true,
items: [
{ text: 'useArrayData', link: '/composables/useArrayData' },
],
},
],
},
],
socialLinks: [{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }],
},
});

136
docs/components/vnInput.md Normal file
View File

@ -0,0 +1,136 @@
# VnInput
`VnInput` is a custom input component that provides various useful features such as validation, input clearing, and more.
## Props
### `modelValue`
- **Type:** `String | Number`
- **Default:** `null`
- **Description:** The value of the model bound to the component.
### `isOutlined`
- **Type:** `Boolean`
- **Default:** `false`
- **Description:** If `true`, the component is rendered with an outlined style.
### `info`
- **Type:** `String`
- **Default:** `''`
- **Description:** Additional information displayed alongside the component.
### `clearable`
- **Type:** `Boolean`
- **Default:** `true`
- **Description:** If `true`, the component shows a button to clear the input.
### `emptyToNull`
- **Type:** `Boolean`
- **Default:** `true`
- **Description:** If `true`, converts empty inputs to `null`.
### `insertable`
- **Type:** `Boolean`
- **Default:** `false`
- **Description:** If `true`, allows the insertion of new values.
### `maxlength`
- **Type:** `Number`
- **Default:** `null`
- **Description:** The maximum number of characters allowed in the input.
### `uppercase`
- **Type:** `Boolean`
- **Default:** `false`
- **Description:** If `true`, converts the input text to uppercase.
## Emits
### `update:modelValue`
- **Description:** Emits the updated model value.
- **Behavior:** This event is emitted whenever the input value changes. It is used to update the model value bound to the component.
### `update:options`
- **Description:** Emits the updated options.
- **Behavior:** This event is emitted when the component's options change. It is useful for components with dynamic options.
### `keyup.enter`
- **Description:** Emits an event when the Enter key is pressed.
- **Behavior:** This event is emitted whenever the Enter key is pressed while the input is focused. It can be used to handle specific actions when the input is confirmed.
### `remove`
- **Description:** Emits an event to remove the current value.
- **Behavior:** This event is emitted when the clear button (close icon) is clicked. It is used to handle the removal of the current input value.
## Functions
### `focus`
- **Description:** Focuses the input.
- **Behavior:** This function is exposed so it can be called from outside the component. It uses `vnInputRef.value.focus()` to focus the input.
### `handleKeydown`
- **Description:** Handles the `keydown` event of the input.
- **Behavior:** This function is called whenever a key is pressed while the input is focused. If the pressed key is `Backspace`, it does nothing. If `insertable` is `true` and the pressed key is a number, it calls `handleInsertMode`.
### `handleInsertMode`
- **Description:** Handles the insertion mode of values.
- **Behavior:** This function is called when `insertable` is `true` and a numeric key is pressed. It inserts the value at the cursor position and updates the input value. Then, it moves the cursor to the correct position.
### `handleUppercase`
- **Description:** Converts the input value to uppercase.
- **Behavior:** This function is called when the uppercase icon is clicked. It converts the current input value to uppercase.
## Usage
```vue
<template>
<VnInput
v-model="inputValue"
:isOutlined="true"
info="Additional information"
:clearable="true"
:emptyToNull="true"
:insertable="false"
:maxlength="50"
:uppercase="true"
@update:modelValue="handleUpdate"
@keyup.enter="handleEnter"
@remove="handleRemove"
/>
</template>
<script setup>
import { ref } from 'vue';
import VnInput from 'src/components/common/VnInput.vue';
const inputValue = ref('');
const handleUpdate = (value) => {
console.log('Updated value:', value);
};
const handleEnter = () => {
console.log('Enter pressed');
};
const handleRemove = () => {
console.log('Value removed');
};
</script>
```

View File

@ -0,0 +1,215 @@
# useArrayData
`useArrayData` is a composable function that provides a set of utilities for managing array data in a Vue component. It leverages Pinia for state management and provides various methods for fetching, filtering, and manipulating data.
## Usage
```javascript
import { useArrayData } from 'src/composables/useArrayData';
const {
fetch,
applyFilter,
addFilter,
getCurrentFilter,
setCurrentFilter,
addFilterWhere,
addOrder,
deleteOrder,
refresh,
destroy,
loadMore,
store,
totalRows,
updateStateParams,
isLoading,
deleteOption,
reset,
resetPagination,
} = useArrayData('myKey', userOptions);
```
## Parameters
### `key`
- **Type:** `String`
- **Description:** A unique key to identify the data store.
### `userOptions`
- **Type:** `Object`
- **Description:** An object containing user-defined options for configuring the data store.
## Methods
### `fetch`
Fetches data from the server.
#### Parameters
- **`options`** : An object with the following properties:
- `append` (Boolean): Whether to append the fetched data to the existing data.
- `updateRouter` (Boolean): Whether to update the router with the current filter.
#### Returns
- **`Promise`** : A promise that resolves with the fetched data.
### `applyFilter`
Applies a filter to the data.
#### Parameters
- **`filter`** : An object containing the filter criteria.
- **`params`** : Additional parameters for the filter.
- **`fetchOptions`** : Options for the fetch method.
#### Returns
- **`Promise`** : A promise that resolves with the filtered data.
### `addFilter`
Adds a filter to the existing filters.
#### Parameters
- **`filter`** : An object containing the filter criteria.
- **`params`** : Additional parameters for the filter.
#### Returns
- **`Promise`** : A promise that resolves with the updated filter and parameters.
### `getCurrentFilter`
Gets the current filter applied to the data.
#### Returns
- **`Object`** : The current filter and parameters.
### `setCurrentFilter`
Sets the current filter for the data.
#### Returns
- **`Object`** : The current filter and parameters.
### `addFilterWhere`
Adds a `where` clause to the existing filters.
#### Parameters
- **`where`** : An object containing the `where` clause.
#### Returns
- **`Promise`** : A promise that resolves when the filter is applied.
### `addOrder`
Adds an order to the existing orders.
#### Parameters
- **`field`** : The field to order by.
- **`direction`** : The direction of the order (`ASC` or `DESC`).
#### Returns
- **`Promise`** : A promise that resolves with the updated order.
### `deleteOrder`
Deletes an order from the existing orders.
#### Parameters
- **`field`** : The field to delete the order for.
#### Returns
- **`Promise`** : A promise that resolves when the order is deleted.
### `refresh`
Refreshes the data by re-fetching it from the server.
#### Returns
- **`Promise`** : A promise that resolves with the refreshed data.
### `destroy`
Destroys the data store for the given key.
### `loadMore`
Loads more data by incrementing the pagination.
#### Returns
- **`Promise`** : A promise that resolves with the additional data.
### `updateStateParams`
Updates the state parameters with the given data.
#### Parameters
- **`data`** : The data to update the state parameters with.
### `deleteOption`
Deletes an option from the store.
#### Parameters
- **`option`** : The option to delete.
### `reset`
Resets the store to its default state.
#### Parameters
- **`opts`** : An array of options to reset.
### `resetPagination`
Resets the pagination for the store.
## Computed Properties
### `totalRows`
- **Description:** The total number of rows in the data.
- **Type:** `Number`
### `isLoading`
- **Description:** Whether the data is currently being loaded.
- **Type:** `Boolean`
```vue
<script setup>
import { useArrayData } from 'src/composables/useArrayData';
const userOptions = {
url: '/api/data',
limit: 10,
};
const arrayData = useArrayData('myKey', userOptions);
</script>
```
```
```

13
docs/index.md Normal file
View File

@ -0,0 +1,13 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: 'Lilium'
text: 'Lilium docs'
tagline: Powered by Verdnatura
actions:
- theme: brand
text: Docs
link: /components/vnInput
---

View File

@ -1,55 +1,64 @@
{
"name": "salix-front",
"version": "24.42.0",
"version": "25.08.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 ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test:e2e": "cypress open",
"test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run",
"test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest",
"test:unit:ci": "vitest run",
"commitlint": "commitlint --edit",
"prepare": "npx husky install",
"addReferenceTag": "node .husky/addReferenceTag.js"
"addReferenceTag": "node .husky/addReferenceTag.js",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"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",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-cypress": "^2.13.3",
"eslint-plugin-vue": "^9.14.1",
"cypress-mochawesome-reporter": "^3.8.2",
"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",
"vitepress": "^1.6.3",
"vitest": "^0.34.0"
},
"engines": {
"node": "^20 || ^18 || ^16",
@ -58,8 +67,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(),
],
};

6
proxy-serve.js Normal file
View File

@ -0,0 +1,6 @@
export default [
{
path: '/api',
rule: { target: 'http://0.0.0.0:3000' },
},
];

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,
@ -30,7 +30,6 @@ module.exports = configure(function (/* ctx */) {
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: ['i18n', 'axios', 'vnDate', 'validations', 'quasar', 'quasar.defaults'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'],
@ -179,7 +178,6 @@ module.exports = configure(function (/* ctx */) {
'render', // keep this as last one
],
},
// https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa
pwa: {
workboxMode: 'generateSW', // or 'injectManifest'

View File

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

View File

@ -1,4 +1,3 @@
import { Notify } from 'quasar';
import { onRequest, onResponseError } from 'src/boot/axios';
import { describe, expect, it, vi } from 'vitest';
@ -7,75 +6,60 @@ vi.mock('src/composables/useSession', () => ({
getToken: () => 'DEFAULT_TOKEN',
isLoggedIn: () => vi.fn(),
destroy: () => vi.fn(),
})
}),
}));
vi.mock('src/stores/useStateQueryStore', () => ({
useStateQueryStore: () => ({
add: () => vi.fn(),
remove: () => vi.fn(),
}),
}));
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(expect.objectContaining({
headers: {
Authorization: 'DEFAULT_TOKEN'
}
}));
});
})
describe('onResponseError()', async () => {
it('should call to the Notify plugin with a message error for an status code "500"', async () => {
Notify.create = vi.fn()
const error = {
response: {
status: 500
}
};
const result = onResponseError(error);
expect(result).rejects.toEqual(
expect.objectContaining(error)
);
expect(Notify.create).toHaveBeenCalledWith(
expect(resultConfig).toEqual(
expect.objectContaining({
message: 'An internal server error has ocurred',
type: 'negative',
headers: {
'Accept-Language': 'en-US',
Authorization: 'DEFAULT_TOKEN',
},
})
);
});
});
describe('onResponseError()', async () => {
it('should call to the Notify plugin with a message error for an status code "500"', async () => {
const error = {
response: {
status: 500,
},
};
const result = onResponseError(error);
expect(result).rejects.toEqual(expect.objectContaining(error));
});
it('should call to the Notify plugin with a message from the response property', async () => {
Notify.create = vi.fn()
const error = {
response: {
status: 401,
data: {
error: {
message: 'Invalid user or password'
}
}
}
message: 'Invalid user or password',
},
},
},
};
const result = onResponseError(error);
expect(result).rejects.toEqual(
expect.objectContaining(error)
);
expect(Notify.create).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Invalid user or password',
type: 'negative',
})
);
expect(result).rejects.toEqual(expect.objectContaining(error));
});
})
});
});

View File

@ -2,20 +2,23 @@ import axios from 'axios';
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/';
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;
}
stateQuery.add(config);
return config;
};
@ -24,58 +27,28 @@ const onRequestError = (error) => {
};
const onResponse = (response) => {
const { method } = response.config;
const config = response.config;
stateQuery.remove(config);
const isSaveRequest = method === 'patch';
if (isSaveRequest) {
if (config.method === 'patch') {
notify('globals.dataSaved', 'positive');
}
return response;
};
const onResponseError = (error) => {
let message = '';
const onResponseError = async (error) => {
stateQuery.remove(error.config);
const response = error.response;
const responseData = response && response.data;
const responseError = responseData && response.data.error;
if (responseError) {
message = responseError.message;
}
switch (response?.status) {
case 422:
if (error.name == 'ValidationError')
message +=
' "' +
responseError.details.context +
'.' +
Object.keys(responseError.details.codes).join(',') +
'"';
break;
case 500:
message = 'errors.statusInternalServerError';
break;
case 502:
message = 'errors.statusBadGateway';
break;
case 504:
message = 'errors.statusGatewayTimeout';
break;
}
if (session.isLoggedIn() && response?.status === 401) {
session.destroy(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);
}
notify(message, 'negative');
return Promise.reject(error);
};

View File

@ -0,0 +1,2 @@
export const langs = ['en', 'es'];
export const decimalPlaces = 2;

View File

@ -1,9 +1,11 @@
import { boot } from 'quasar/wrappers';
import { createI18n } from 'vue-i18n';
import messages from 'src/i18n';
import { useState } from 'src/composables/useState';
const user = useState().getUser();
const i18n = createI18n({
locale: navigator.language || navigator.userLanguage,
locale: user.value.lang || navigator.language || navigator.userLanguage,
fallbackLocale: 'en',
globalInjection: true,
messages,

View File

@ -1,6 +1,6 @@
export default {
mounted: function (el, binding) {
const shortcut = binding.value ?? '+';
mounted(el, binding) {
const shortcut = binding.value || '+';
const { key, ctrl, alt, callback } =
typeof shortcut === 'string'
@ -8,25 +8,24 @@ export default {
key: shortcut,
ctrl: true,
alt: true,
callback: () =>
document
.querySelector(`button[shortcut="${shortcut}"]`)
?.click(),
callback: () => el?.click(),
}
: binding.value;
if (!el.hasAttribute('shortcut')) {
el.setAttribute('shortcut', key);
}
const handleKeydown = (event) => {
if (event.key === key && (!ctrl || event.ctrlKey) && (!alt || event.altKey)) {
callback();
}
};
// Attach the event listener to the window
window.addEventListener('keydown', handleKeydown);
el._handleKeydown = handleKeydown;
},
unmounted: function (el) {
unmounted(el) {
if (el._handleKeydown) {
window.removeEventListener('keydown', el._handleKeydown);
}

View File

@ -25,8 +25,6 @@ export default {
const handleKeyUp = (event) => {
const { ctrlKey, altKey } = event;
// Resetea la bandera cuando se sueltan las teclas ctrl o alt
if (!ctrlKey || !altKey) {
isNotified = false;
}

View File

@ -1,30 +1,51 @@
import { getCurrentInstance } from 'vue';
function focusFirstInput(input) {
input.focus();
}
export default {
mounted: function () {
const vm = getCurrentInstance();
if (vm.type.name === 'QForm') {
if (!['searchbarForm', 'filterPanelForm'].includes(this.$el?.id)) {
// TODO: AUTOFOCUS IS NOT FOCUSING
const that = this;
this.$el.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter') {
const input = evt.target;
if (input.type == 'textarea' && evt.shiftKey) {
evt.preventDefault();
let { selectionStart, selectionEnd } = input;
input.value =
input.value.substring(0, selectionStart) +
'\n' +
input.value.substring(selectionEnd);
selectionStart = selectionEnd = selectionStart + 1;
return;
}
evt.preventDefault();
that.onSubmit();
}
});
const that = this;
const form = document.querySelector('.q-form#formModel');
if (!form) return;
try {
const inputsFormCard = form.querySelectorAll(
`input:not([disabled]):not([type="checkbox"])`
);
if (inputsFormCard.length) {
focusFirstInput(inputsFormCard[0]);
}
const textareas = document.querySelectorAll(
'textarea:not([disabled]), [contenteditable]:not([disabled])'
);
if (textareas.length) {
focusFirstInput(textareas[textareas.length - 1]);
}
const inputs = document.querySelectorAll(
'form#formModel input:not([disabled]):not([type="checkbox"])'
);
const input = inputs[0];
if (!input) return;
focusFirstInput(input);
} catch (error) {
console.error(error);
}
form.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter' && !that.$attrs['prevent-submit']) {
const input = evt.target;
if (input.type == 'textarea' && evt.shiftKey) {
evt.preventDefault();
let { selectionStart, selectionEnd } = input;
input.value =
input.value.substring(0, selectionStart) +
'\n' +
input.value.substring(selectionEnd);
selectionStart = selectionEnd = selectionStart + 1;
return;
}
evt.preventDefault();
that.onSubmit();
}
});
},
};

View File

@ -1,16 +1,55 @@
import axios from 'axios';
import { boot } from 'quasar/wrappers';
import qFormMixin from './qformMixin';
import mainShortcutMixin from './mainShortcutMixin';
import keyShortcut from './keyShortcut';
import useNotify from 'src/composables/useNotify.js';
const { notify } = useNotify();
import { QForm } from 'quasar';
import { QLayout } from 'quasar';
import mainShortcutMixin from './mainShortcutMixin';
import { useCau } from 'src/composables/useCau';
export default boot(({ app }) => {
app.mixin(qFormMixin);
app.mixin(mainShortcutMixin);
QForm.mixins = [qFormMixin];
QLayout.mixins = [mainShortcutMixin];
app.directive('shortcut', keyShortcut);
app.config.errorHandler = function (err) {
console.error(err);
notify('globals.error', 'negative', 'error');
app.config.errorHandler = async (error) => {
let message;
const response = error.response;
const responseData = response?.data;
const responseError = responseData && response.data.error;
if (responseError) {
message = responseError.message;
}
switch (response?.status) {
case 422:
if (error.name == 'ValidationError')
message +=
' "' +
responseError.details.context +
'.' +
Object.keys(responseError.details.codes).join(',') +
'"';
break;
case 500:
message = 'errors.statusInternalServerError';
break;
case 502:
message = 'errors.statusBadGateway';
break;
case 504:
message = 'errors.statusGatewayTimeout';
break;
}
console.error(error);
if (error instanceof axios.CanceledError) {
const env = process.env.NODE_ENV;
if (env && env !== 'development') return;
message = 'Duplicate request';
}
await useCau(response, message);
};
app.provide('app', app);
});

View File

@ -9,30 +9,27 @@ import VnRow from 'components/ui/VnRow.vue';
import FormModelPopup from './FormModelPopup.vue';
import { useState } from 'src/composables/useState';
defineProps({ showEntityField: { type: Boolean, default: true } });
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const bicInputRef = ref(null);
const state = useState();
const customer = computed(() => state.get('customer'));
const bankEntityFormData = reactive({
name: null,
bic: null,
countryFk: customer.value?.countryFk,
id: null,
});
const customer = computed(() => state.get('Customer'));
const countriesFilter = {
fields: ['id', 'name', 'code'],
};
const bankEntityFormData = reactive({
name: null,
bic: null,
countryFk: customer.value?.countryFk,
});
const countriesOptions = ref([]);
const onDataSaved = (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
const onDataSaved = (...args) => {
emit('onDataSaved', ...args);
};
onMounted(async () => {
@ -44,7 +41,6 @@ onMounted(async () => {
<template>
<FetchData
url="Countries"
:filter="countriesFilter"
auto-load
@on-fetch="(data) => (countriesOptions = data)"
/>
@ -54,6 +50,7 @@ onMounted(async () => {
:title="t('title')"
:subtitle="t('subtitle')"
:form-initial-data="bankEntityFormData"
:filter="countriesFilter"
@on-data-saved="onDataSaved"
>
<template #form-inputs="{ data, validate }">
@ -85,7 +82,13 @@ onMounted(async () => {
:rules="validate('bankEntity.countryFk')"
/>
</div>
<div v-if="showEntityField" class="col">
<div
v-if="
countriesOptions.find((c) => c.id === data.countryFk)?.code ==
'ES'
"
class="col"
>
<VnInput
:label="t('id')"
v-model="data.id"

View File

@ -1,155 +0,0 @@
<script setup>
import { reactive, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from './FormModelPopup.vue';
import VnInputDate from './common/VnInputDate.vue';
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const router = useRouter();
const manualInvoiceFormData = reactive({
maxShipped: Date.vnNew(),
});
const formModelPopupRef = ref();
const invoiceOutSerialsOptions = ref([]);
const taxAreasOptions = ref([]);
const ticketsOptions = ref([]);
const clientsOptions = ref([]);
const isLoading = computed(() => formModelPopupRef.value?.isLoading);
const onDataSaved = async (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
if (requestResponse && requestResponse.id)
router.push({ name: 'InvoiceOutSummary', params: { id: requestResponse.id } });
};
</script>
<template>
<FetchData
url="InvoiceOutSerials"
:filter="{ where: { code: { neq: 'R' } }, order: ['code'] }"
@on-fetch="(data) => (invoiceOutSerialsOptions = data)"
auto-load
/>
<FetchData
url="TaxAreas"
:filter="{ order: ['code'] }"
@on-fetch="(data) => (taxAreasOptions = data)"
auto-load
/>
<FormModelPopup
ref="formModelPopupRef"
:title="t('Create manual invoice')"
url-create="InvoiceOuts/createManualInvoice"
model="invoiceOut"
:form-initial-data="manualInvoiceFormData"
@on-data-saved="onDataSaved"
>
<template #form-inputs="{ data }">
<span v-if="isLoading" class="text-primary invoicing-text">
<QIcon name="warning" class="fill-icon q-mr-sm" size="md" />
{{ t('Invoicing in progress...') }}
</span>
<VnRow>
<VnSelect
:label="t('Ticket')"
:options="ticketsOptions"
hide-selected
option-label="id"
option-value="id"
v-model="data.ticketFk"
@update:model-value="data.clientFk = null"
url="Tickets"
:where="{ refFk: null }"
:fields="['id', 'nickname']"
:filter-options="{ order: 'shipped DESC' }"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.nickname }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<span class="row items-center" style="max-width: max-content">{{
t('Or')
}}</span>
<VnSelect
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.clientFk"
@update:model-value="data.ticketFk = null"
url="Clients"
:fields="['id', 'name']"
:filter-options="{ order: 'name ASC' }"
/>
<VnInputDate :label="t('Max date')" v-model="data.maxShipped" />
</VnRow>
<VnRow>
<VnSelect
:label="t('Serial')"
:options="invoiceOutSerialsOptions"
hide-selected
option-label="description"
option-value="code"
v-model="data.serial"
/>
<VnSelect
:label="t('Area')"
:options="taxAreasOptions"
hide-selected
option-label="code"
option-value="code"
v-model="data.taxArea"
/>
</VnRow>
<VnRow>
<VnInput
:label="t('Reference')"
type="textarea"
v-model="data.reference"
fill-input
autogrow
/>
</VnRow>
</template>
</FormModelPopup>
</template>
<style lang="scss" scoped>
.invoicing-text {
display: flex;
justify-content: center;
align-items: center;
color: $primary;
font-size: 24px;
margin-bottom: 8px;
}
</style>
<i18n>
es:
Create manual invoice: Crear factura manual
Ticket: Ticket
Client: Cliente
Max date: Fecha límite
Serial: Serie
Area: Area
Reference: Referencia
Or: O
Invoicing in progress...: Facturación en progreso...
</i18n>

View File

@ -17,10 +17,6 @@ const $props = defineProps({
type: Number,
default: null,
},
provinces: {
type: Array,
default: () => [],
},
});
const { t } = useI18n();
@ -44,19 +40,23 @@ const onDataSaved = (...args) => {
url-create="towns"
model="city"
@on-data-saved="onDataSaved"
data-cy="newCityForm"
>
<template #form-inputs="{ data, validate }">
<VnRow>
<VnInput
:label="t('Names')"
:label="t('Name')"
v-model="data.name"
:rules="validate('city.name')"
required
data-cy="cityName"
/>
<VnSelectProvince
:province-selected="$props.provinceSelected"
:country-fk="$props.countryFk"
v-model="data.provinceFk"
:provinces="$props.provinces"
required
data-cy="provinceCity"
/>
</VnRow>
</template>

View File

@ -1,5 +1,5 @@
<script setup>
import { reactive, ref, watch } from 'vue';
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
@ -21,13 +21,14 @@ const postcodeFormData = reactive({
provinceFk: null,
townFk: null,
});
const townsFetchDataRef = ref(false);
const townFilter = ref({});
const townsFetchDataRef = ref(null);
const provincesFetchDataRef = ref(null);
const countriesOptions = ref([]);
const countriesRef = ref(false);
const provincesOptions = ref([]);
const townsOptions = ref([]);
const town = ref({});
const countryFilter = ref({});
function onDataSaved(formData) {
const newPostcode = {
@ -39,102 +40,90 @@ function onDataSaved(formData) {
({ id }) => id === formData.provinceFk
);
newPostcode.province = provinceObject?.name;
const countryObject = countriesOptions.value.find(
const countryObject = countriesRef.value.opts.find(
({ id }) => id === formData.countryFk
);
newPostcode.country = countryObject?.name;
emit('onDataSaved', newPostcode);
}
async function setCountry(countryFk, data) {
data.townFk = null;
data.provinceFk = null;
data.countryFk = countryFk;
await fetchTowns();
}
// Province
async function setProvince(id, data) {
if (data.provinceFk === id) return;
const newProvince = provincesOptions.value.find((province) => province.id == id);
if (newProvince) data.countryFk = newProvince.countryFk;
postcodeFormData.provinceFk = id;
await fetchTowns();
}
async function onProvinceCreated(data) {
postcodeFormData.provinceFk = data.id;
}
function provinceByCountry(countryFk = postcodeFormData.countryFk) {
return provincesOptions.value
.filter((province) => province.countryFk === countryFk)
.map(({ id }) => id);
}
// Town
async function handleTowns(data) {
townsOptions.value = data;
}
function setTown(newTown, data) {
town.value = newTown;
data.provinceFk = newTown?.provinceFk ?? newTown;
data.countryFk = newTown?.province?.countryFk ?? newTown;
}
async function onCityCreated(newTown, formData) {
await provincesFetchDataRef.value.fetch();
newTown.province = provincesOptions.value.find(
(province) => province.id === newTown.provinceFk
);
formData.townFk = newTown;
setTown(newTown, formData);
}
function setTown(newTown, data) {
if (!newTown) return;
town.value = newTown;
data.provinceFk = newTown.provinceFk;
data.countryFk = newTown.province.countryFk;
async function fetchTowns(countryFk = postcodeFormData.countryFk) {
if (!countryFk) return;
const provinces = postcodeFormData.provinceFk
? [postcodeFormData.provinceFk]
: provinceByCountry();
townFilter.value.where = {
provinceFk: {
inq: provinces,
},
};
await townsFetchDataRef.value?.fetch();
}
async function setProvince(id, data) {
const newProvince = provincesOptions.value.find((province) => province.id == id);
if (!newProvince) return;
data.countryFk = newProvince.countryFk;
}
async function onProvinceCreated(data) {
await provincesFetchDataRef.value.fetch({
where: { countryFk: postcodeFormData.countryFk },
});
postcodeFormData.provinceFk.value = data.id;
}
watch(
() => [postcodeFormData.countryFk],
async (newCountryFk, oldValueFk) => {
if (!!oldValueFk[0] && newCountryFk[0] !== oldValueFk[0]) {
postcodeFormData.provinceFk = null;
postcodeFormData.townFk = null;
}
if ((newCountryFk, newCountryFk !== postcodeFormData.countryFk)) {
await provincesFetchDataRef.value.fetch({
where: {
countryFk: newCountryFk[0],
},
});
await townsFetchDataRef.value.fetch({
where: {
provinceFk: {
inq: provincesOptions.value.map(({ id }) => id),
},
},
});
}
async function filterTowns(name) {
if (name !== '') {
townFilter.value.where = {
name: {
like: `%${name}%`,
},
};
await townsFetchDataRef.value?.fetch();
}
);
watch(
() => postcodeFormData.provinceFk,
async (newProvinceFk) => {
if (newProvinceFk[0] && newProvinceFk[0] !== postcodeFormData.provinceFk) {
await townsFetchDataRef.value.fetch({
where: { provinceFk: newProvinceFk[0] },
});
}
}
);
async function handleProvinces(data) {
provincesOptions.value = data;
}
async function handleTowns(data) {
townsOptions.value = data;
}
async function handleCountries(data) {
countriesOptions.value = data;
}
</script>
<template>
<FetchData
ref="provincesFetchDataRef"
@on-fetch="handleProvinces"
auto-load
url="Provinces/location"
/>
<FetchData
ref="townsFetchDataRef"
:sort-by="['name ASC']"
:limit="30"
:filter="townFilter"
@on-fetch="handleTowns"
auto-load
url="Towns/location"
/>
<FetchData @on-fetch="handleCountries" auto-load url="Countries" />
<FormModelPopup
url-create="postcodes"
model="postcode"
@ -151,10 +140,13 @@ async function handleCountries(data) {
v-model="data.code"
:rules="validate('postcode.code')"
clearable
required
data-cy="locationPostcode"
/>
<VnSelectDialog
:label="t('City')"
@update:model-value="(value) => setTown(value, data)"
@filter="filterTowns"
:tooltip="t('Create city')"
v-model="data.townFk"
:options="townsOptions"
@ -163,7 +155,8 @@ async function handleCountries(data) {
:rules="validate('postcode.city')"
:acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
:emit-value="false"
:clearable="true"
required
data-cy="locationTown"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
@ -180,7 +173,6 @@ async function handleCountries(data) {
<CreateNewCityForm
:country-fk="data.countryFk"
:province-selected="data.provinceFk"
:provinces="provincesOptions"
@on-data-saved="
(_, requestResponse) =>
onCityCreated(requestResponse, data)
@ -194,19 +186,31 @@ async function handleCountries(data) {
:country-fk="data.countryFk"
:province-selected="data.provinceFk"
@update:model-value="(value) => setProvince(value, data)"
@update:options="
(data) => {
provincesOptions = data;
}
"
v-model="data.provinceFk"
:clearable="true"
:provinces="provincesOptions"
@on-province-created="onProvinceCreated"
required
/>
<VnSelect
ref="countriesRef"
:limit="30"
:filter="countryFilter"
:sort-by="['name ASC']"
auto-load
url="Countries"
required
:label="t('Country')"
:options="countriesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.countryFk"
:rules="validate('postcode.countryFk')"
@update:model-value="(value) => setCountry(value, data)"
data-cy="locationCountry"
/>
</VnRow>
</template>

View File

@ -1,8 +1,7 @@
<script setup>
import { reactive, ref } from 'vue';
import { computed, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
@ -21,32 +20,24 @@ const $props = defineProps({
type: Number,
default: null,
},
provinces: {
type: Array,
default: () => [],
},
});
const autonomiesOptions = ref([]);
const autonomiesRef = ref([]);
const onDataSaved = (dataSaved, requestResponse) => {
requestResponse.autonomy = autonomiesOptions.value.find(
requestResponse.autonomy = autonomiesRef.value.opts.find(
(autonomy) => autonomy.id == requestResponse.autonomyFk
);
emit('onDataSaved', dataSaved, requestResponse);
};
const where = computed(() => {
if (!$props.countryFk) {
return {};
}
return { countryFk: $props.countryFk };
});
</script>
<template>
<FetchData
@on-fetch="(data) => (autonomiesOptions = data)"
auto-load
:filter="{
where: {
countryFk: $props.countryFk,
},
}"
url="Autonomies/location"
/>
<FormModelPopup
:title="t('New province')"
:subtitle="t('Please, ensure you put the correct data!')"
@ -61,10 +52,19 @@ const onDataSaved = (dataSaved, requestResponse) => {
:label="t('Name')"
v-model="data.name"
:rules="validate('province.name')"
required
data-cy="provinceName"
/>
<VnSelect
data-cy="autonomyProvince"
required
ref="autonomiesRef"
auto-load
:where="where"
url="Autonomies/location"
:sort-by="['name ASC']"
:limit="30"
:label="t('Autonomy')"
:options="autonomiesOptions"
hide-selected
option-label="name"
option-value="id"

View File

@ -38,7 +38,7 @@ const onDataSaved = (dataSaved) => {
@on-fetch="(data) => (warehousesOptions = data)"
auto-load
url="Warehouses"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
/>
<FetchData
@on-fetch="(data) => (temperaturesOptions = data)"

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';
@ -10,12 +10,14 @@ import VnPaginate from 'components/ui/VnPaginate.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import SkeletonTable from 'components/ui/SkeletonTable.vue';
import { tMobile } from 'src/composables/tMobile';
import getDifferences from 'src/filters/getDifferences';
const { push } = useRouter();
const quasar = useQuasar();
const stateStore = useStateStore();
const { t } = useI18n();
const { validate } = useValidator();
const $attrs = useAttrs();
const $props = defineProps({
model: {
@ -62,6 +64,10 @@ const $props = defineProps({
type: Function,
default: null,
},
beforeSaveFn: {
type: Function,
default: null,
},
goTo: {
type: String,
default: '',
@ -77,7 +83,7 @@ const isLoading = ref(false);
const hasChanges = ref(false);
const originalData = ref();
const vnPaginateRef = ref();
const formData = ref();
const formData = ref([]);
const saveButtonRef = ref(null);
const watchChanges = ref();
const formUrl = computed(() => $props.url);
@ -94,6 +100,7 @@ defineExpose({
saveChanges,
getChanges,
formData,
originalData,
vnPaginateRef,
});
@ -111,9 +118,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) {
@ -125,7 +134,7 @@ function resetData(data) {
originalData.value = JSON.parse(JSON.stringify(data));
formData.value = JSON.parse(JSON.stringify(data));
if (watchChanges.value) watchChanges.value(); //destoy watcher
if (watchChanges.value) watchChanges.value(); //destroy watcher
watchChanges.value = watch(formData, () => (hasChanges.value = true), { deep: true });
}
@ -144,7 +153,7 @@ function filter(value, update, filterOptions) {
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
},
);
}
@ -171,17 +180,20 @@ async function saveChanges(data) {
hasChanges.value = false;
return;
}
const changes = data || getChanges();
let changes = data || getChanges();
if ($props.beforeSaveFn) {
changes = await $props.beforeSaveFn(changes, getChanges);
}
try {
await axios.post($props.saveUrl || $props.url + '/crud', changes);
} catch (e) {
return (isLoading.value = false);
} finally {
isLoading.value = false;
}
originalData.value = JSON.parse(JSON.stringify(formData.value));
if (changes.creates?.length) await vnPaginateRef.value.fetch();
hasChanges.value = false;
isLoading.value = false;
emit('saveChanges', data);
quasar.notify({
type: 'positive',
@ -211,7 +223,7 @@ async function remove(data) {
if (preRemove.length) {
newData = newData.filter(
(form) => !preRemove.some((index) => index == form.$index)
(form) => !preRemove.some((index) => index == form.$index),
);
const changes = getChanges();
if (!changes.creates?.length && !changes.updates?.length)
@ -225,12 +237,12 @@ async function remove(data) {
componentProps: {
title: t('globals.confirmDeletion'),
message: t('globals.confirmDeletionMessage'),
newData,
data: { deletes: ids },
ids,
promise: saveChanges,
},
})
.onOk(async () => {
await saveChanges({ deletes: ids });
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
fetch(newData);
});
@ -248,7 +260,7 @@ function getChanges() {
for (const [i, row] of formData.value.entries()) {
if (!row[pk]) {
creates.push(row);
} else if (originalData.value) {
} else if (originalData.value[i]) {
const data = getDifferences(originalData.value[i], row);
if (!isEmpty(data)) {
updates.push({
@ -267,34 +279,10 @@ function getChanges() {
return changes;
}
function getDifferences(obj1, obj2) {
let diff = {};
delete obj1.$index;
delete obj2.$index;
for (let key in obj1) {
if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
diff[key] = obj2[key];
}
}
for (let key in obj2) {
if (
obj1[key] === undefined ||
JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])
) {
diff[key] = obj2[key];
}
}
return diff;
}
function isEmpty(obj) {
if (obj == null) return true;
if (obj === undefined) return true;
if (Object.keys(obj).length === 0) return true;
if (obj.length > 0) return false;
if (Array.isArray(obj)) return !obj.length;
return !Object.keys(obj).length;
}
async function reload(params) {
@ -394,6 +382,9 @@ watch(formUrl, async () => {
@click="onSubmit"
:disable="!hasChanges"
:title="t('globals.save')"
v-shortcut="'s'"
shortcut="s"
data-cy="crudModelDefaultSaveBtn"
/>
<slot name="moreAfterActions" />
</QBtnGroup>

View File

@ -156,26 +156,22 @@ const rotateRight = () => {
};
const onSubmit = () => {
try {
if (!newPhoto.files && !newPhoto.url) {
notify(t('Select an image'), 'negative');
return;
}
const options = {
type: 'blob',
};
editor.value
.result(options)
.then((result) => {
const file = new File([result], newPhoto.files?.name || '');
newPhoto.blob = file;
})
.then(() => makeRequest());
} catch (err) {
console.error('Error uploading image');
if (!newPhoto.files && !newPhoto.url) {
notify(t('Select an image'), 'negative');
return;
}
const options = {
type: 'blob',
};
editor.value
.result(options)
.then((result) => {
const file = new File([result], newPhoto.files?.name || '');
newPhoto.blob = file;
})
.then(() => makeRequest());
};
const makeRequest = async () => {

View File

@ -51,21 +51,17 @@ const onDataSaved = () => {
};
const onSubmit = async () => {
try {
isLoading.value = true;
const rowsToEdit = $props.rows.map((row) => ({ id: row.id, itemFk: row.itemFk }));
const payload = {
field: selectedField.value.field,
newValue: newValue.value,
lines: rowsToEdit,
};
isLoading.value = true;
const rowsToEdit = $props.rows.map((row) => ({ id: row.id, itemFk: row.itemFk }));
const payload = {
field: selectedField.value.field,
newValue: newValue.value,
lines: rowsToEdit,
};
await axios.post($props.editUrl, payload);
onDataSaved();
isLoading.value = false;
} catch (err) {
console.error('Error submitting table cell edit');
}
await axios.post($props.editUrl, payload);
onDataSaved();
isLoading.value = false;
};
const closeForm = () => {
@ -89,12 +85,14 @@ const closeForm = () => {
hide-selected
option-label="label"
v-model="selectedField"
data-cy="field-to-edit"
/>
<component
:is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}"
v-model="newValue"
:label="t('Value')"
data-cy="value-to-edit"
style="width: 200px"
/>
</VnRow>

View File

@ -50,25 +50,25 @@ const loading = ref(false);
const tableColumns = computed(() => [
{
label: t('entry.buys.id'),
label: t('globals.id'),
name: 'id',
field: 'id',
align: 'left',
},
{
label: t('entry.buys.name'),
label: t('globals.name'),
name: 'name',
field: 'name',
align: 'left',
},
{
label: t('entry.buys.size'),
label: t('globals.size'),
name: 'size',
field: 'size',
align: 'left',
},
{
label: t('entry.buys.producer'),
label: t('globals.producer'),
name: 'producerName',
field: 'producer',
align: 'left',
@ -84,34 +84,30 @@ const tableColumns = computed(() => [
]);
const onSubmit = async () => {
try {
let filter = itemFilter;
const params = itemFilterParams;
const where = {};
for (let key in params) {
const value = params[key];
if (!value) continue;
let filter = itemFilter;
const params = itemFilterParams;
const where = {};
for (let key in params) {
const value = params[key];
if (!value) continue;
switch (key) {
case 'name':
where[key] = { like: `%${value}%` };
break;
case 'producerFk':
case 'typeFk':
case 'size':
case 'inkFk':
where[key] = value;
}
switch (key) {
case 'name':
where[key] = { like: `%${value}%` };
break;
case 'producerFk':
case 'typeFk':
case 'size':
case 'inkFk':
where[key] = value;
}
filter.where = where;
const { data } = await axios.get(props.url, {
params: { filter: JSON.stringify(filter) },
});
tableRows.value = data;
} catch (err) {
console.error('Error fetching entries items');
}
filter.where = where;
const { data } = await axios.get(props.url, {
params: { filter: JSON.stringify(filter) },
});
tableRows.value = data;
};
const closeForm = () => {
@ -152,10 +148,10 @@ const selectItem = ({ id }) => {
</span>
<h1 class="title">{{ t('Filter item') }}</h1>
<VnRow>
<VnInput :label="t('entry.buys.name')" v-model="itemFilterParams.name" />
<VnInput :label="t('globals.name')" v-model="itemFilterParams.name" />
<VnInput :label="t('entry.buys.size')" v-model="itemFilterParams.size" />
<VnSelect
:label="t('entry.buys.producer')"
:label="t('globals.producer')"
:options="producersOptions"
hide-selected
option-label="name"
@ -163,7 +159,7 @@ const selectItem = ({ id }) => {
v-model="itemFilterParams.producerFk"
/>
<VnSelect
:label="t('entry.buys.type')"
:label="t('globals.type')"
:options="ItemTypesOptions"
hide-selected
option-label="name"

View File

@ -48,13 +48,13 @@ const loading = ref(false);
const tableColumns = computed(() => [
{
label: t('entry.basicData.id'),
label: t('globals.id'),
name: 'id',
field: 'id',
align: 'left',
},
{
label: t('entry.basicData.warehouseOut'),
label: t('globals.warehouseOut'),
name: 'warehouseOutFk',
field: 'warehouseOutFk',
align: 'left',
@ -62,7 +62,7 @@ const tableColumns = computed(() => [
warehousesOptions.value.find((warehouse) => warehouse.id === val).name,
},
{
label: t('entry.basicData.warehouseIn'),
label: t('globals.warehouseIn'),
name: 'warehouseInFk',
field: 'warehouseInFk',
align: 'left',
@ -70,14 +70,14 @@ const tableColumns = computed(() => [
warehousesOptions.value.find((warehouse) => warehouse.id === val).name,
},
{
label: t('entry.basicData.shipped'),
label: t('globals.shipped'),
name: 'shipped',
field: 'shipped',
align: 'left',
format: (val) => toDate(val),
},
{
label: t('entry.basicData.landed'),
label: t('globals.landed'),
name: 'landed',
field: 'landed',
align: 'left',
@ -86,32 +86,28 @@ const tableColumns = computed(() => [
]);
const onSubmit = async () => {
try {
let filter = travelFilter;
const params = travelFilterParams;
const where = {};
for (let key in params) {
const value = params[key];
if (!value) continue;
let filter = travelFilter;
const params = travelFilterParams;
const where = {};
for (let key in params) {
const value = params[key];
if (!value) continue;
switch (key) {
case 'agencyModeFk':
case 'warehouseInFk':
case 'warehouseOutFk':
case 'shipped':
case 'landed':
where[key] = value;
}
switch (key) {
case 'agencyModeFk':
case 'warehouseInFk':
case 'warehouseOutFk':
case 'shipped':
case 'landed':
where[key] = value;
}
filter.where = where;
const { data } = await axios.get('Travels', {
params: { filter: JSON.stringify(filter) },
});
tableRows.value = data;
} catch (err) {
console.error('Error fetching travels');
}
filter.where = where;
const { data } = await axios.get('Travels', {
params: { filter: JSON.stringify(filter) },
});
tableRows.value = data;
};
const closeForm = () => {
@ -146,7 +142,7 @@ const selectTravel = ({ id }) => {
<h1 class="title">{{ t('Filter travels') }}</h1>
<VnRow>
<VnSelect
:label="t('entry.basicData.agency')"
:label="t('globals.agency')"
:options="agenciesOptions"
hide-selected
option-label="name"
@ -154,7 +150,7 @@ const selectTravel = ({ id }) => {
v-model="travelFilterParams.agencyModeFk"
/>
<VnSelect
:label="t('entry.basicData.warehouseOut')"
:label="t('globals.warehouseOut')"
:options="warehousesOptions"
hide-selected
option-label="name"
@ -162,7 +158,7 @@ const selectTravel = ({ id }) => {
v-model="travelFilterParams.warehouseOutFk"
/>
<VnSelect
:label="t('entry.basicData.warehouseIn')"
:label="t('globals.warehouseIn')"
:options="warehousesOptions"
hide-selected
option-label="name"
@ -170,11 +166,11 @@ const selectTravel = ({ id }) => {
v-model="travelFilterParams.warehouseInFk"
/>
<VnInputDate
:label="t('entry.basicData.shipped')"
:label="t('globals.shipped')"
v-model="travelFilterParams.shipped"
/>
<VnInputDate
:label="t('entry.basicData.landed')"
:label="t('globals.landed')"
v-model="travelFilterParams.landed"
/>
</VnRow>
@ -185,6 +181,7 @@ const selectTravel = ({ id }) => {
color="primary"
:disabled="isLoading"
:loading="isLoading"
data-cy="save-filter-travel-form"
/>
</div>
<QTable
@ -195,9 +192,10 @@ const selectTravel = ({ id }) => {
:no-data-label="t('Enter a new search')"
class="q-mt-lg"
@row-click="(_, row) => selectTravel(row)"
data-cy="table-filter-travel-form"
>
<template #body-cell-id="{ row }">
<QTd auto-width @click.stop>
<QTd auto-width @click.stop data-cy="travelFk-travel-form">
<QBtn flat color="blue">{{ row.id }}</QBtn>
<TravelDescriptorProxy :id="row.id" />
</QTd>

View File

@ -1,7 +1,7 @@
<script setup>
import axios from 'axios';
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
import { onBeforeRouteLeave, useRouter } from 'vue-router';
import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState';
@ -12,7 +12,6 @@ import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData';
import { useRoute } from 'vue-router';
const { push } = useRouter();
const quasar = useQuasar();
@ -85,16 +84,20 @@ const $props = defineProps({
},
reload: {
type: Boolean,
default: false,
default: true,
},
defaultTrim: {
type: Boolean,
default: true,
},
maxWidth: {
type: [String, Boolean],
default: '800px',
},
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`,
).value;
const componentIsRendered = ref(false);
const arrayData = useArrayData(modelValue);
@ -102,10 +105,11 @@ const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({});
const formData = computed(() => state.get(modelValue));
const originalData = computed(() => state.get(modelValue));
const formData = ref({});
const defaultButtons = computed(() => ({
save: {
dataCy: 'saveDefaultBtn',
color: 'primary',
icon: 'save',
label: 'globals.save',
@ -113,6 +117,7 @@ const defaultButtons = computed(() => ({
type: 'submit',
},
reset: {
dataCy: 'resetDefaultBtn',
color: 'primary',
icon: 'restart_alt',
label: 'globals.reset',
@ -122,8 +127,6 @@ const defaultButtons = computed(() => ({
}));
onMounted(async () => {
originalData.value = JSON.parse(JSON.stringify($props.formInitialData ?? {}));
nextTick(() => (componentIsRendered.value = true));
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
@ -143,7 +146,7 @@ onMounted(async () => {
JSON.stringify(newVal) !== JSON.stringify(originalData.value);
isResetting.value = false;
},
{ deep: true }
{ deep: true },
);
}
});
@ -151,16 +154,24 @@ onMounted(async () => {
if (!$props.url)
watch(
() => arrayData.store.data,
(val) => updateAndEmit('onFetch', val)
(val) => updateAndEmit('onFetch', val),
);
watch(
originalData,
(val) => {
if (val) formData.value = JSON.parse(JSON.stringify(val));
},
{ immediate: true },
);
watch(
() => [$props.url, $props.filter],
async () => {
originalData.value = null;
state.set(modelValue, null);
reset();
await fetch();
}
},
);
onBeforeRouteLeave((to, from, next) => {
@ -192,7 +203,7 @@ async function fetch() {
updateAndEmit('onFetch', data);
} catch (e) {
state.set(modelValue, {});
originalData.value = {};
throw e;
}
}
@ -203,7 +214,9 @@ async function save() {
isLoading.value = true;
try {
formData.value = trimData(formData.value);
const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
const body = $props.mapper
? $props.mapper(formData.value, originalData.value)
: formData.value;
const method = $props.urlCreate ? 'post' : 'patch';
const url =
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
@ -217,9 +230,6 @@ async function save() {
updateAndEmit('onDataSaved', formData.value, response?.data);
if ($props.reload) await arrayData.fetch({});
hasChanges.value = false;
} catch (err) {
console.error(err);
notify('errors.writeRequest', 'negative');
} finally {
isLoading.value = false;
}
@ -231,6 +241,7 @@ async function saveAndGo() {
}
function reset() {
formData.value = JSON.parse(JSON.stringify(originalData.value));
updateAndEmit('onFetch', originalData.value);
if ($props.observeFormChanges) {
hasChanges.value = false;
@ -249,13 +260,12 @@ function filter(value, update, filterOptions) {
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
},
);
}
function updateAndEmit(evt, val, res) {
state.set(modelValue, val);
originalData.value = val && JSON.parse(JSON.stringify(val));
if (!$props.url) arrayData.store.data = val;
emit(evt, state.get(modelValue), res);
@ -275,6 +285,7 @@ defineExpose({
hasChanges,
reset,
fetch,
formData,
});
</script>
<template>
@ -285,7 +296,9 @@ defineExpose({
@submit="save"
@reset="reset"
class="q-pa-md"
:style="maxWidth ? 'max-width: ' + maxWidth : ''"
id="formModel"
:prevent-submit="$attrs['prevent-submit']"
>
<QCard>
<slot
@ -319,6 +332,7 @@ defineExpose({
:title="t(defaultButtons.reset.label)"
/>
<QBtnDropdown
data-cy="saveAndContinueDefaultBtn"
v-if="$props.goTo"
@click="saveAndGo"
:label="tMobile('globals.saveAndContinue')"
@ -373,7 +387,6 @@ defineExpose({
color: black;
}
#formModel {
max-width: 800px;
width: 100%;
}

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
@ -15,23 +15,30 @@ defineProps({
type: String,
default: '',
},
showSaveAndContinueBtn: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
const formModelRef = ref(null);
const closeButton = ref(null);
const isSaveAndContinue = ref(false);
const onDataSaved = (formData, requestResponse) => {
if (closeButton.value) closeButton.value.click();
if (closeButton.value && isSaveAndContinue) closeButton.value.click();
emit('onDataSaved', formData, requestResponse);
};
const isLoading = computed(() => formModelRef.value?.isLoading);
const reset = computed(() => formModelRef.value?.reset);
defineExpose({
isLoading,
onDataSaved,
isSaveAndContinue,
reset,
});
</script>
@ -51,6 +58,19 @@ defineExpose({
<p>{{ subtitle }}</p>
<slot name="form-inputs" :data="data" :validate="validate" />
<div class="q-mt-lg row justify-end">
<QBtn
v-if="showSaveAndContinueBtn"
:label="t('globals.isSaveAndContinue')"
:title="t('globals.isSaveAndContinue')"
type="submit"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
data-cy="FormModelPopup_isSaveAndContinue"
z-max
@click="() => (isSaveAndContinue = true)"
/>
<QBtn
:label="t('globals.cancel')"
:title="t('globals.cancel')"
@ -59,8 +79,15 @@ defineExpose({
flat
:disabled="isLoading"
:loading="isLoading"
@click="emit('onDataCanceled')"
data-cy="FormModelPopup_cancel"
v-close-popup
z-max
@click="
() => {
isSaveAndContinue = false;
emit('onDataCanceled');
}
"
/>
<QBtn
:label="t('globals.save')"
@ -70,6 +97,9 @@ defineExpose({
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
data-cy="FormModelPopup_save"
z-max
@click="() => (isSaveAndContinue = false)"
/>
</div>
</template>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
const emit = defineEmits(['onSubmit']);
defineProps({
const $props = defineProps({
title: {
type: String,
default: '',
@ -25,16 +25,21 @@ defineProps({
type: String,
default: '',
},
submitOnEnter: {
type: Boolean,
default: true,
},
});
const { t } = useI18n();
const closeButton = ref(null);
const isLoading = ref(false);
const onSubmit = () => {
emit('onSubmit');
closeForm();
if ($props.submitOnEnter) {
emit('onSubmit');
closeForm();
}
};
const closeForm = () => {

View File

@ -9,6 +9,8 @@ import VnSelect from 'components/common/VnSelect.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import axios from 'axios';
import { getParamWhere } from 'src/filters';
import { useRoute } from 'vue-router';
const { t } = useI18n();
const props = defineProps({
@ -26,32 +28,24 @@ const props = defineProps({
},
});
const itemCategories = ref([]);
const selectedCategoryFk = ref(null);
const selectedTypeFk = ref(null);
const route = useRoute();
const itemTypesOptions = ref([]);
const suppliersOptions = ref([]);
const tagOptions = ref([]);
const tagValues = ref([]);
const categoryList = ref(null);
const selectedCategoryFk = ref(getParamWhere(route.query.table, 'categoryFk', false));
const selectedTypeFk = ref(getParamWhere(route.query.table, 'typeFk', false));
const categoryList = computed(() => {
return (itemCategories.value || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
const selectedCategory = computed(() => {
return (categoryList.value || []).find(
(category) => category?.id === selectedCategoryFk.value,
);
});
const selectedCategory = computed(() =>
(itemCategories.value || []).find(
(category) => category?.id === selectedCategoryFk.value
)
);
const selectedType = computed(() => {
return (itemTypesOptions.value || []).find(
(type) => type?.id === selectedTypeFk.value
(type) => type?.id === selectedTypeFk.value,
);
});
@ -87,21 +81,17 @@ const applyTags = (params, search) => {
search();
};
const fetchItemTypes = async (id) => {
try {
const filter = {
fields: ['id', 'name', 'categoryFk'],
where: { categoryFk: id },
include: 'category',
order: 'name ASC',
};
const { data } = await axios.get('ItemTypes', {
params: { filter: JSON.stringify(filter) },
});
itemTypesOptions.value = data;
} catch (err) {
console.error('Error fetching item types', err);
}
const fetchItemTypes = async (id = selectedCategoryFk.value) => {
const filter = {
fields: ['id', 'name', 'categoryFk'],
where: { categoryFk: id },
include: 'category',
order: 'name ASC',
};
const { data } = await axios.get('ItemTypes', {
params: { filter: JSON.stringify(filter) },
});
itemTypesOptions.value = data;
};
const getCategoryClass = (category, params) => {
@ -111,45 +101,38 @@ const getCategoryClass = (category, params) => {
};
const getSelectedTagValues = async (tag) => {
try {
if (!tag?.selectedTag?.id) return;
tag.value = null;
const filter = {
fields: ['value'],
order: 'value ASC',
limit: 30,
};
if (!tag?.selectedTag?.id) return;
tag.value = null;
const filter = {
fields: ['value'],
order: 'value ASC',
limit: 30,
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`Tags/${tag.selectedTag.id}/filterValue`, {
params,
});
tag.valueOptions = data;
} catch (err) {
console.error('Error getting selected tag values');
}
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`Tags/${tag.selectedTag.id}/filterValue`, {
params,
});
tag.valueOptions = data;
};
const removeTag = (index, params, search) => {
(tagValues.value || []).splice(index, 1);
applyTags(params, search);
};
const setCategoryList = (data) => {
categoryList.value = (data || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
fetchItemTypes();
};
</script>
<template>
<FetchData
url="ItemCategories"
limit="30"
auto-load
@on-fetch="(data) => (itemCategories = data)"
/>
<FetchData
url="Suppliers"
limit="30"
auto-load
:filter="{ fields: ['id', 'name', 'nickname'], order: 'name ASC', limit: 30 }"
@on-fetch="(data) => (suppliersOptions = data)"
/>
<FetchData url="ItemCategories" limit="30" auto-load @on-fetch="setCategoryList" />
<FetchData
url="Tags"
:filter="{ fields: ['id', 'name', 'isFree'] }"
@ -248,7 +231,7 @@ const removeTag = (index, params, search) => {
>
<QItemSection class="col">
<VnSelect
:label="t('components.itemsFilterPanel.tag')"
:label="t('globals.tag')"
v-model="value.selectedTag"
:options="tagOptions"
option-label="name"
@ -299,7 +282,7 @@ const removeTag = (index, params, search) => {
<QItem class="q-mt-lg">
<QBtn
icon="add_circle"
shortcut="+"
v-shortcut="'+'"
flat
class="fill-icon-on-hover q-px-xs"
color="primary"
@ -345,7 +328,6 @@ en:
active: Is active
visible: Is visible
floramondo: Is floramondo
salesPersonFk: Buyer
categoryFk: Category
es:
@ -356,6 +338,12 @@ es:
active: Activo
visible: Visible
floramondo: Floramondo
salesPersonFk: Comprador
categoryFk: Categoría
Plant: Planta natural
Flower: Flor fresca
Handmade: Hecho a mano
Artificial: Artificial
Green: Verdes frescos
Accessories: Complementos florales
Fruit: Fruta
</i18n>

View File

@ -1,6 +1,6 @@
<script setup>
import axios from 'axios';
import { onMounted, watch, ref, reactive } from 'vue';
import { onMounted, watch, ref, reactive, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { QSeparator, useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
@ -9,33 +9,69 @@ import { toLowerCamel } from 'src/filters';
import routes from 'src/router/modules';
import LeftMenuItem from './LeftMenuItem.vue';
import LeftMenuItemGroup from './LeftMenuItemGroup.vue';
import VnInput from './common/VnInput.vue';
import { useRouter } from 'vue-router';
const { t } = useI18n();
const route = useRoute();
const quasar = useQuasar();
const navigation = useNavigationStore();
const router = useRouter();
const props = defineProps({
source: {
type: String,
default: 'main',
},
});
const initialized = ref(false);
const items = ref([]);
const expansionItemElements = reactive({});
const pinnedModules = computed(() => {
const map = new Map();
items.value.forEach((item) => item.isPinned && map.set(item.name, item));
return map;
});
const search = ref(null);
const filteredItems = computed(() => {
if (!search.value) return items.value;
const normalizedSearch = normalize(search.value);
return items.value.filter((item) => {
const locale = normalize(t(item.title));
return locale.includes(normalizedSearch);
});
});
const filteredPinnedModules = computed(() => {
if (!search.value) return pinnedModules.value;
const normalizedSearch = search.value
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
const map = new Map();
for (const [key, pinnedModule] of pinnedModules.value) {
const locale = t(pinnedModule.title)
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
if (locale.includes(normalizedSearch)) map.set(key, pinnedModule);
}
return map;
});
onMounted(async () => {
await navigation.fetchPinned();
getRoutes();
initialized.value = true;
});
watch(
() => route.matched,
() => {
if (!initialized.value) return;
items.value = [];
getRoutes();
},
{ deep: true }
{ deep: true },
);
function findMatches(search, item) {
@ -56,46 +92,61 @@ function findMatches(search, item) {
}
function addChildren(module, route, parent) {
if (route.menus) {
const mainMenus = route.menus[props.source];
const matches = findMatches(mainMenus, route);
const menus = route?.meta?.menu ?? route?.menus?.[props.source]; //backwards compatible
if (!menus) return;
for (const child of matches) {
navigation.addMenuItem(module, child, parent);
}
const matches = findMatches(menus, route);
for (const child of matches) {
navigation.addMenuItem(module, child, parent);
}
}
const items = ref([]);
function getRoutes() {
if (props.source === 'main') {
const modules = Object.assign([], navigation.getModules().value);
for (const item of modules) {
const moduleDef = routes.find(
(route) => toLowerCamel(route.name) === item.module
);
if (!moduleDef) continue;
item.children = [];
addChildren(item.module, moduleDef, item.children);
}
items.value = modules;
const handleRoutes = {
main: getMainRoutes,
card: getCardRoutes,
};
try {
handleRoutes[props.source]();
} catch (error) {
throw new Error(`Method is not defined`);
}
}
function getMainRoutes() {
const modules = Object.assign([], navigation.getModules().value);
if (props.source === 'card') {
const currentRoute = route.matched[1];
const currentModule = toLowerCamel(currentRoute.name);
for (const item of modules) {
const moduleDef = routes.find(
(route) => toLowerCamel(route.name) === currentModule
(route) => toLowerCamel(route.name) === item.module,
);
if (!moduleDef) continue;
item.children = [];
if (!moduleDef) return;
addChildren(currentModule, moduleDef, items.value);
addChildren(item.module, moduleDef, item.children);
}
items.value = modules;
}
function getCardRoutes() {
const currentRoute = route.matched[1];
const currentModule = toLowerCamel(currentRoute.name);
let moduleDef = routes.find((route) => toLowerCamel(route.name) === currentModule);
if (!moduleDef) return;
if (!moduleDef?.menus) moduleDef = betaGetRoutes();
addChildren(currentModule, moduleDef, items.value);
}
function betaGetRoutes() {
let menuRoute;
let index = route.matched.length - 1;
while (!menuRoute && index > 0) {
if (route.matched[index]?.meta?.menu) menuRoute = route.matched[index];
index--;
}
return menuRoute;
}
async function togglePinned(item, event) {
@ -123,22 +174,71 @@ async function togglePinned(item, event) {
const handleItemExpansion = (itemName) => {
expansionItemElements[itemName].scrollToLastElement();
};
function normalize(text) {
return text
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
}
const searchModule = () => {
const [item] = filteredItems.value;
if (item) router.push({ name: item.name });
};
</script>
<template>
<QList padding class="column-max-width">
<template v-if="$props.source === 'main'">
<template v-if="$route?.matched[1]?.name === 'Dashboard'">
<QItem class="header">
<QItemSection avatar>
<QIcon name="view_module" />
</QItemSection>
<QItemSection> {{ t('globals.modules') }}</QItemSection>
<QItem class="q-pb-md">
<VnInput
v-model="search"
:label="t('Search modules')"
class="full-width"
filled
dense
autofocus
@keyup.enter.stop="searchModule()"
/>
</QItem>
<QSeparator />
<template v-for="item in items" :key="item.name">
<template v-if="item.children">
<LeftMenuItem :item="item" group="modules">
<template v-if="filteredPinnedModules.size && !search">
<LeftMenuItem
v-for="[key, pinnedModule] of filteredPinnedModules"
:key="key"
:item="pinnedModule"
group="modules"
>
<template #side>
<QBtn
v-if="pinnedModule.isPinned === true"
@click="togglePinned(pinnedModule, $event)"
icon="remove_circle"
size="xs"
flat
round
>
<QTooltip>
{{ t('components.leftMenu.removeFromPinned') }}
</QTooltip>
</QBtn>
</template>
</LeftMenuItem>
<QSeparator />
</template>
<template v-for="(item, index) in filteredItems" :key="item.name">
<template
v-if="
search ||
(item.children && !filteredPinnedModules.has(item.name))
"
>
<LeftMenuItem
:item="item"
group="modules"
:class="search && index === 0 ? 'searched' : ''"
>
<template #side>
<QBtn
v-if="item.isPinned === true"
@ -255,4 +355,11 @@ const handleItemExpansion = (itemName) => {
.header {
color: var(--vn-label-color);
}
.searched {
background-color: var(--vn-section-hover-color);
}
</style>
<i18n>
es:
Search modules: Buscar módulos
</i18n>

View File

@ -26,6 +26,7 @@ const itemComputed = computed(() => {
:to="{ name: itemComputed.name }"
clickable
v-ripple
:data-cy="`${itemComputed.name}-menu-item`"
>
<QItemSection avatar v-if="itemComputed.icon">
<QIcon :name="itemComputed.icon" />

View File

@ -3,6 +3,7 @@ import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore';
import { useStateQueryStore } from 'src/stores/useStateQueryStore';
import { useQuasar } from 'quasar';
import PinnedModules from './PinnedModules.vue';
import UserPanel from 'components/UserPanel.vue';
@ -12,15 +13,15 @@ import VnAvatar from './ui/VnAvatar.vue';
const { t } = useI18n();
const stateStore = useStateStore();
const quasar = useQuasar();
const stateQuery = useStateQueryStore();
const state = useState();
const user = state.getUser();
const appName = 'Lilium';
const pinnedModulesRef = ref();
onMounted(() => stateStore.setMounted());
const pinnedModulesRef = ref();
const refresh = () => window.location.reload();
</script>
<template>
<QHeader color="white" elevated>
<QToolbar class="q-py-sm q-px-md">
@ -50,21 +51,27 @@ const pinnedModulesRef = ref();
</QBtn>
</RouterLink>
<VnBreadcrumbs v-if="$q.screen.gt.sm" />
<QSpinner
color="primary"
class="q-ml-md"
:class="{
'no-visible': !stateQuery.isLoading().value,
}"
size="xs"
data-cy="loading-spinner"
/>
<QSpace />
<div id="searchbar" class="searchbar"></div>
<QSpace />
<div class="q-pl-sm q-gutter-sm row items-center no-wrap">
<div id="actions-prepend"></div>
<QBtn
flat
v-if="!quasar.platform.is.mobile"
@click="pinnedModulesRef.redirect($route.params.id)"
icon="more_up"
>
<QTooltip>
{{ t('Go to Salix') }}
</QTooltip>
</QBtn>
<QIcon
name="refresh"
size="md"
color="red"
v-if="state.get('error')"
@click="refresh"
/>
<QBtn
:class="{ 'q-pa-none': quasar.platform.is.mobile }"
id="pinnedModules"
@ -96,7 +103,6 @@ const pinnedModulesRef = ref();
<VnBreadcrumbs v-if="$q.screen.lt.md" class="q-ml-md" />
</QHeader>
</template>
<style lang="scss" scoped>
.searchbar {
width: max-content;
@ -105,9 +111,3 @@ const pinnedModulesRef = ref();
background-color: var(--vn-section-color);
}
</style>
<i18n>
en:
Go to Salix: Go to Salix
es:
Go to Salix: Ir a Salix
</i18n>

View File

@ -9,6 +9,7 @@ import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
const $props = defineProps({
invoiceOutData: {
@ -39,14 +40,10 @@ const refund = async () => {
invoiceCorrectionTypeFk: invoiceParams.invoiceCorrectionTypeFk,
};
try {
const { data } = await axios.post('InvoiceOuts/refundAndInvoice', params);
notify(t('Refunded invoice'), 'positive');
const [id] = data?.refundId || [];
if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
} catch (err) {
console.error('Error refunding invoice', err);
}
const { data } = await axios.post('InvoiceOuts/refundAndInvoice', params);
notify(t('Refunded invoice'), 'positive');
const [id] = data?.refundId || [];
if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
};
</script>
@ -135,15 +132,11 @@ const refund = async () => {
:required="true"
/> </VnRow
><VnRow>
<div>
<QCheckbox
:label="t('Inherit warehouse')"
v-model="invoiceParams.inheritWarehouse"
/>
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('Inherit warehouse tooltip') }}</QTooltip>
</QIcon>
</div>
<VnCheckbox
v-model="invoiceParams.inheritWarehouse"
:label="t('Inherit warehouse')"
:info="t('Inherit warehouse tooltip')"
/>
</VnRow>
</template>
</FormPopup>

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

@ -0,0 +1,70 @@
<script setup>
defineProps({ row: { type: Object, required: true } });
</script>
<template>
<span class="q-gutter-x-xs">
<QIcon
v-if="row?.risk"
name="vn:risk"
:color="row.hasHighRisk ? 'negative' : 'primary'"
size="xs"
>
<QTooltip>
{{ $t('salesTicketsTable.risk') }}: {{ row.risk - row.credit }}
</QTooltip>
</QIcon>
<QIcon
v-if="row?.hasComponentLack"
name="vn:components"
color="primary"
size="xs"
>
<QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip>
</QIcon>
<QIcon v-if="row?.hasItemDelay" color="primary" size="xs" name="vn:hasItemDelay">
<QTooltip>
{{ $t('ticket.summary.hasItemDelay') }}
</QTooltip>
</QIcon>
<QIcon v-if="row?.hasItemLost" color="primary" size="xs" name="vn:hasItemLost">
<QTooltip>
{{ $t('salesTicketsTable.hasItemLost') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row?.hasItemShortage"
name="vn:unavailable"
color="primary"
size="xs"
>
<QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip>
</QIcon>
<QIcon v-if="row?.hasRounding" color="primary" name="sync_problem" size="xs">
<QTooltip>
{{ $t('ticketList.rounding') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row?.hasTicketRequest"
name="vn:buyrequest"
color="primary"
size="xs"
>
<QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip>
</QIcon>
<QIcon
v-if="!row?.isTaxDataChecked === 0"
name="vn:no036"
color="primary"
size="xs"
>
<QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip>
</QIcon>
<QIcon v-if="row?.isFreezed" name="vn:frozen" color="primary" size="xs">
<QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip>
</QIcon>
<QIcon v-if="row?.isTooLittle" name="vn:isTooLittle" color="primary" size="xs">
<QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip>
</QIcon>
</span>
</template>

View File

@ -10,6 +10,7 @@ import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import VnCheckbox from './common/VnCheckbox.vue';
const $props = defineProps({
invoiceOutData: {
@ -49,36 +50,32 @@ const makeInvoice = async () => {
makeInvoice: checked.value,
};
try {
if (checked.value && hasToInvoiceByAddress) {
const response = await new Promise((resolve) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Bill destination client'),
message: t('transferInvoiceInfo'),
},
})
.onOk(() => {
resolve(true);
})
.onCancel(() => {
resolve(false);
});
});
if (!response) {
return;
}
if (checked.value && hasToInvoiceByAddress) {
const response = await new Promise((resolve) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Bill destination client'),
message: t('transferInvoiceInfo'),
},
})
.onOk(() => {
resolve(true);
})
.onCancel(() => {
resolve(false);
});
});
if (!response) {
return;
}
const { data } = await axios.post('InvoiceOuts/transfer', params);
notify(t('Transferred invoice'), 'positive');
const id = data?.[0];
if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
} catch (err) {
console.error('Error transfering invoice', err);
}
const { data } = await axios.post('InvoiceOuts/transfer', params);
notify(t('Transferred invoice'), 'positive');
const id = data?.[0];
if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
};
</script>
@ -190,15 +187,11 @@ const makeInvoice = async () => {
/>
</VnRow>
<VnRow>
<div>
<QCheckbox
:label="t('Bill destination client')"
v-model="checked"
/>
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('transferInvoiceInfo') }}</QTooltip>
</QIcon>
</div>
<VnCheckbox
v-model="checked"
:label="t('Bill destination client')"
:info="t('transferInvoiceInfo')"
/>
</VnRow>
</template>
</FormPopup>

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);
},
});
@ -87,10 +83,10 @@ async function saveDarkMode(value) {
async function saveLanguage(value) {
const query = `/VnUsers/${user.value.id}`;
try {
await axios.patch(query, {
lang: value,
});
await axios.patch(query, { lang: value });
user.value.lang = value;
useState().setUser(user.value);
onDataSaved();
} catch (error) {
onDataError();

View File

@ -7,7 +7,7 @@ import VnSelectDialog from 'components/common/VnSelectDialog.vue';
import FetchData from 'components/FetchData.vue';
import CreateNewProvinceForm from './CreateNewProvinceForm.vue';
const emit = defineEmits(['onProvinceCreated']);
const emit = defineEmits(['onProvinceCreated', 'onProvinceFetched', 'update:options']);
const $props = defineProps({
countryFk: {
type: Number,
@ -17,20 +17,23 @@ const $props = defineProps({
type: Number,
default: null,
},
provinces: {
type: Array,
default: () => [],
},
});
const provinceFk = defineModel({ type: Number, default: null });
const { validate } = useValidator();
const { t } = useI18n();
const filter = ref({
include: { relation: 'country' },
where: {
countryFk: $props.countryFk,
},
});
const provincesOptions = ref($props.provinces);
provinceFk.value = $props.provinceSelected;
const provincesFetchDataRef = ref();
provinceFk.value = $props.provinceSelected;
if (!$props.countryFk) {
filter.value.where = {};
}
async function onProvinceCreated(_, data) {
await provincesFetchDataRef.value.fetch({ where: { countryFk: $props.countryFk } });
provinceFk.value = data.id;
@ -38,24 +41,33 @@ async function onProvinceCreated(_, data) {
}
async function handleProvinces(data) {
provincesOptions.value = data;
emit('update:options', data);
}
watch(
() => $props.countryFk,
async () => {
if ($props.countryFk) {
filter.value.where.countryFk = $props.countryFk;
} else filter.value.where = {};
await provincesFetchDataRef.value.fetch({});
emit('onProvinceFetched', provincesOptions.value);
}
);
</script>
<template>
<FetchData
ref="provincesFetchDataRef"
:filter="{
include: { relation: 'country' },
where: {
countryFk: $props.countryFk,
},
}"
:filter="filter"
@on-fetch="handleProvinces"
url="Provinces"
auto-load
/>
<VnSelectDialog
data-cy="locationProvince"
:label="t('Province')"
:options="$props.provinces"
:options="provincesOptions"
:tooltip="t('Create province')"
hide-selected
v-model="provinceFk"

View File

@ -1,9 +1,8 @@
<script setup>
import { markRaw, computed, defineModel } from 'vue';
import { QIcon, QCheckbox } from 'quasar';
import { markRaw, computed } from 'vue';
import { QIcon, QCheckbox, QToggle } from 'quasar';
import { dashIfEmpty } from 'src/filters';
/* basic input */
import VnSelect from 'components/common/VnSelect.vue';
import VnSelectCache from 'components/common/VnSelectCache.vue';
import VnInput from 'components/common/VnInput.vue';
@ -12,8 +11,11 @@ import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnComponent from 'components/common/VnComponent.vue';
import VnUserLink from 'components/ui/VnUserLink.vue';
import VnSelectEnum from '../common/VnSelectEnum.vue';
import VnCheckbox from '../common/VnCheckbox.vue';
const model = defineModel(undefined, { required: true });
const emit = defineEmits(['blur']);
const $props = defineProps({
column: {
type: Object,
@ -39,10 +41,18 @@ const $props = defineProps({
type: Object,
default: null,
},
autofocus: {
type: Boolean,
default: false,
},
showLabel: {
type: Boolean,
default: null,
},
eventHandlers: {
type: Object,
default: null,
},
});
const defaultSelect = {
@ -99,7 +109,8 @@ const defaultComponents = {
},
},
checkbox: {
component: markRaw(QCheckbox),
ref: 'checkbox',
component: markRaw(VnCheckbox),
attrs: ({ model }) => {
const defaultAttrs = {
disable: !$props.isEditable,
@ -115,6 +126,10 @@ const defaultComponents = {
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
autofocus: true,
},
events: {
blur: () => emit('blur'),
},
},
select: {
@ -125,12 +140,19 @@ const defaultComponents = {
component: markRaw(VnSelect),
...defaultSelect,
},
selectEnum: {
component: markRaw(VnSelectEnum),
...defaultSelect,
},
icon: {
component: markRaw(QIcon),
},
userLink: {
component: markRaw(VnUserLink),
},
toggle: {
component: markRaw(QToggle),
},
};
const value = computed(() => {
@ -160,7 +182,28 @@ const col = computed(() => {
return newColumn;
});
const components = computed(() => $props.components ?? defaultComponents);
const components = computed(() => {
const sourceComponents = $props.components ?? defaultComponents;
return Object.keys(sourceComponents).reduce((acc, key) => {
const component = sourceComponents[key];
if (!component || typeof component !== 'object') {
acc[key] = component;
return acc;
}
acc[key] = {
...component,
attrs: {
...(component.attrs || {}),
autofocus: $props.autofocus,
},
event: { ...component?.event, ...$props?.eventHandlers },
};
return acc;
}, {});
});
</script>
<template>
<div class="row no-wrap">

View File

@ -1,14 +1,12 @@
<script setup>
import { markRaw, computed, defineModel } from 'vue';
import { QCheckbox } from 'quasar';
import { markRaw, computed } from 'vue';
import { QCheckbox, QToggle } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
/* basic input */
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue';
import VnColumn from 'components/VnTable/VnColumn.vue';
const $props = defineProps({
column: {
@ -25,14 +23,21 @@ const $props = defineProps({
},
searchUrl: {
type: String,
default: 'params',
default: 'table',
},
customClass: {
type: String,
default: '',
},
});
defineExpose({ addFilter, props: $props });
const model = defineModel(undefined, { required: true });
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
const arrayData = useArrayData(
$props.dataKey,
$props.searchUrl ? { searchUrl: $props.searchUrl } : null,
);
const columnFilter = computed(() => $props.column?.columnFilter);
const updateEvent = { 'update:modelValue': addFilter };
@ -43,19 +48,18 @@ const enterEvent = {
const defaultAttrs = {
filled: !$props.showTitle,
class: 'q-px-xs q-pb-xs q-pt-none fit',
dense: true,
};
const forceAttrs = {
label: $props.showTitle ? '' : columnFilter.value?.label ?? $props.column.label,
label: $props.showTitle ? '' : (columnFilter.value?.label ?? $props.column.label),
};
const selectComponent = {
component: markRaw(VnSelect),
event: updateEvent,
attrs: {
class: 'q-px-sm q-pb-xs q-pt-none fit',
class: `q-pt-none fit ${$props.customClass}`,
dense: true,
filled: !$props.showTitle,
},
@ -106,14 +110,24 @@ const components = {
component: markRaw(QCheckbox),
event: updateEvent,
attrs: {
dense: true,
class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs fit',
class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit',
'toggle-indeterminate': true,
size: 'sm',
},
forceAttrs,
},
select: selectComponent,
rawSelect: selectComponent,
toggle: {
component: markRaw(QToggle),
event: updateEvent,
attrs: {
class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit',
'toggle-indeterminate': true,
size: 'sm',
},
forceAttrs,
},
};
async function addFilter(value, name) {
@ -129,34 +143,28 @@ async function addFilter(value, name) {
await arrayData.addFilter({ params: { [field]: value } });
}
function alignRow() {
switch ($props.column.align) {
case 'left':
return 'justify-start items-start';
case 'right':
return 'justify-end items-end';
default:
return 'flex-center';
}
}
const showFilter = computed(
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions'
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions',
);
const onTabPressed = async () => {
if (model.value) enterEvent['keyup.enter']();
};
</script>
<template>
<div
v-if="showFilter"
class="full-width"
:class="alignRow()"
style="max-height: 45px; overflow: hidden"
>
<VnTableColumn
<div v-if="showFilter" class="full-width flex-center" style="overflow: hidden">
<VnColumn
:column="$props.column"
default="input"
v-model="model"
:components="components"
component-prop="columnFilter"
@keydown.tab="onTabPressed"
/>
</div>
</template>
<style lang="scss" scoped>
label.vn-label-padding > .q-field__inner > .q-field__control {
padding: inherit !important;
}
</style>

View File

@ -17,7 +17,7 @@ const $props = defineProps({
},
searchUrl: {
type: String,
default: 'params',
default: 'table',
},
vertical: {
type: Boolean,
@ -41,6 +41,7 @@ async function orderBy(name, direction) {
break;
}
if (!direction) return await arrayData.deleteOrder(name);
await arrayData.addOrder(name, direction);
}
@ -51,45 +52,60 @@ defineExpose({ orderBy });
@mouseenter="hover = true"
@mouseleave="hover = false"
@click="orderBy(name, model?.direction)"
class="row items-center no-wrap cursor-pointer"
class="row items-center no-wrap cursor-pointer title"
>
<span :title="label">{{ label }}</span>
<QChip
v-if="name"
:label="!vertical ? model?.index : ''"
:icon="
(model?.index || hover) && !vertical
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: undefined
"
:size="vertical ? '' : 'sm'"
:class="[
model?.index ? 'color-vn-text' : 'bg-transparent',
vertical ? 'q-px-none' : '',
]"
class="no-box-shadow"
:clickable="true"
style="min-width: 40px"
>
<div
class="column flex-center"
v-if="vertical"
:style="!model?.index && 'color: #5d5d5d'"
<sup v-if="name && model?.index">
<QChip
:label="!vertical ? model?.index : ''"
:icon="
(model?.index || hover) && !vertical
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: undefined
"
:size="vertical ? '' : 'sm'"
:class="[
model?.index ? 'color-vn-text' : 'bg-transparent',
vertical ? 'q-px-none' : '',
]"
class="no-box-shadow"
:clickable="true"
style="min-width: 40px; max-height: 30px"
>
{{ model?.index }}
<QIcon
:name="
model?.index
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: 'swap_vert'
"
size="xs"
/>
</div>
</QChip>
<div
class="column flex-center"
v-if="vertical"
:style="!model?.index && 'color: #5d5d5d'"
>
{{ model?.index }}
<QIcon
:name="
model?.index
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: 'swap_vert'
"
size="xs"
/>
</div>
</QChip>
</sup>
</div>
</template>
<style lang="scss" scoped>
.title {
display: flex;
justify-content: center;
align-items: center;
height: 30px;
width: 100%;
color: var(--vn-label-color);
}
sup {
vertical-align: super; /* Valor predeterminado */
/* También puedes usar otros valores como "baseline", "top", "text-top", etc. */
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,92 @@
<script setup>
import { ref } from 'vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnFilter from 'components/VnTable/VnFilter.vue';
import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
defineProps({
columns: {
type: Array,
required: true,
},
searchUrl: {
type: [String, Boolean],
default: 'table',
},
});
const tableFilterRef = ref([]);
function columnName(col) {
const column = { ...col, ...col.columnFilter };
let name = column.name;
if (column.alias) name = column.alias + '.' + name;
return name;
}
</script>
<template>
<VnFilterPanel v-bind="$attrs" :search-button="true" :disable-submit-event="true">
<template #body="{ params, orders, searchFn }">
<div
class="container"
v-for="col of columns.filter((c) => c.columnFilter ?? true)"
:key="col.id"
>
<div class="filter">
<VnFilter
ref="tableFilterRef"
:column="col"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</div>
<div class="order">
<VnTableOrder
v-if="col?.columnFilter !== false && col?.name !== 'tableActions'"
v-model="orders[col.orderBy ?? col.name]"
:name="col.orderBy ?? col.name"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
:vertical="true"
/>
</div>
</div>
<slot
name="moreFilterPanel"
:params="params"
:search-fn="searchFn"
:orders="orders"
:columns="columns"
/>
</template>
<template #tags="{ tag, formatFn, getLocale }">
<div class="q-gutter-x-xs">
<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>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
align-items: center;
height: 45px;
gap: 10px;
}
.filter {
width: 70%;
height: 40px;
text-align: center;
}
.order {
width: 10%;
}
</style>

View File

@ -32,16 +32,21 @@ const areAllChecksMarked = computed(() => {
function setUserConfigViewData(data, isLocal) {
if (!data) return;
// Importante: El name de las columnas de la tabla debe conincidir con el name de las variables que devuelve la view config
if (!isLocal) localColumns.value = [];
// Array to Object
const skippeds = $props.skip.reduce((a, v) => ({ ...a, [v]: v }), {});
for (let column of columns.value) {
const { label, name } = column;
const { label, name, labelAbbreviation } = column;
if (skippeds[name]) continue;
column.visible = data[name] ?? true;
if (!isLocal) localColumns.value.push({ name, label, visible: column.visible });
if (!isLocal)
localColumns.value.push({
name,
label,
labelAbbreviation,
visible: column.visible,
});
}
}
@ -152,7 +157,11 @@ onMounted(async () => {
<QCheckbox
v-for="col in localColumns"
:key="col.name"
:label="col.label"
:label="
col?.labelAbbreviation
? col.labelAbbreviation + ` (${col.label ?? col.name})`
: (col.label ?? col.name)
"
v-model="col.visible"
/>
</div>

View File

@ -0,0 +1,56 @@
import { describe, expect, it, beforeAll, beforeEach, vi } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnTable from 'src/components/VnTable/VnTable.vue';
describe('VnTable', () => {
let wrapper;
let vm;
beforeAll(() => {
wrapper = createWrapper(VnTable, {
propsData: {
columns: [],
},
});
vm = wrapper.vm;
vi.mock('src/composables/useFilterParams', () => {
return {
useFilterParams: vi.fn(() => ({
params: {},
orders: {},
})),
};
});
});
beforeEach(() => (vm.selected = []));
describe('handleSelection()', () => {
const rows = [{ $index: 0 }, { $index: 1 }, { $index: 2 }];
const selectedRows = [{ $index: 1 }];
it('should add rows to selected when shift key is pressed and rows are added except last one', () => {
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows
);
expect(vm.selected).toEqual([{ $index: 0 }]);
});
it('should not add rows to selected when shift key is not pressed', () => {
vm.handleSelection(
{ evt: { shiftKey: false }, added: true, rows: selectedRows },
rows
);
expect(vm.selected).toEqual([]);
});
it('should not add rows to selected when rows are not added', () => {
vm.handleSelection(
{ evt: { shiftKey: true }, added: false, rows: selectedRows },
rows
);
expect(vm.selected).toEqual([]);
});
});
});

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,12 @@
export default function (initialFooter, data) {
const footer = data.reduce(
(acc, row) => {
Object.entries(initialFooter).forEach(([key, initialValue]) => {
acc[key] += row?.[key] !== undefined ? row[key] : initialValue;
});
return acc;
},
{ ...initialFooter }
);
return footer;
}

View File

@ -0,0 +1,248 @@
import { createWrapper, axios } from 'app/test/vitest/helper';
import CrudModel from 'components/CrudModel.vue';
import { vi, afterEach, beforeEach, beforeAll, describe, expect, it } from 'vitest';
describe('CrudModel', () => {
let wrapper;
let vm;
let data;
beforeAll(() => {
wrapper = createWrapper(CrudModel, {
global: {
stubs: [
'vnPaginate',
'useState',
'arrayData',
'useStateStore',
'vue-i18n',
],
mocks: {
validate: vi.fn(),
},
},
propsData: {
dataRequired: {
fk: 1,
},
dataKey: 'crudModelKey',
model: 'crudModel',
url: 'crudModelUrl',
saveFn: '',
},
});
wrapper=wrapper.wrapper;
vm=wrapper.vm;
});
beforeEach(() => {
vm.fetch([]);
vm.watchChanges = null;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('insert()', () => {
it('should new element in list with index 0 if formData not has data', () => {
vm.insert();
expect(vm.formData.length).toEqual(1);
expect(vm.formData[0].fk).toEqual(1);
expect(vm.formData[0].$index).toEqual(0);
});
});
describe('getChanges()', () => {
it('should return correct updates and creates', async () => {
vm.fetch([
{ id: 1, name: 'New name one' },
{ id: 2, name: 'New name two' },
{ id: 3, name: 'Bruce Wayne' },
]);
vm.originalData = [
{ id: 1, name: 'Tony Starks' },
{ id: 2, name: 'Jessica Jones' },
{ id: 3, name: 'Bruce Wayne' },
];
vm.insert();
const result = vm.getChanges();
const expected = {
creates: [
{
$index: 3,
fk: 1,
},
],
updates: [
{
data: {
name: 'New name one',
},
where: {
id: 1,
},
},
{
data: {
name: 'New name two',
},
where: {
id: 2,
},
},
],
};
expect(result).toEqual(expected);
});
});
describe('getDifferences()', () => {
it('should return the differences between two objects', async () => {
const obj1 = {
a: 1,
b: 2,
c: 3,
};
const obj2 = {
a: null,
b: 4,
d: 5,
};
const result = vm.getDifferences(obj1, obj2);
expect(result).toEqual({
a: null,
b: 4,
d: 5,
});
});
});
describe('isEmpty()', () => {
let dummyObj;
let dummyArray;
let result;
it('should return true if object si null', async () => {
dummyObj = null;
result = vm.isEmpty(dummyObj);
expect(result).toBe(true);
});
it('should return true if object si undefined', async () => {
dummyObj = undefined;
result = vm.isEmpty(dummyObj);
expect(result).toBe(true);
});
it('should return true if object is empty', async () => {
dummyObj ={};
result = vm.isEmpty(dummyObj);
expect(result).toBe(true);
});
it('should return false if object is not empty', async () => {
dummyObj = {a:1, b:2, c:3};
result = vm.isEmpty(dummyObj);
expect(result).toBe(false);
});
it('should return true if array is empty', async () => {
dummyArray = [];
result = vm.isEmpty(dummyArray);
expect(result).toBe(true);
});
it('should return false if array is not empty', async () => {
dummyArray = [1,2,3];
result = vm.isEmpty(dummyArray);
expect(result).toBe(false);
})
});
describe('resetData()', () => {
it('should add $index to elements in data[] and sets originalData and formData with data', async () => {
data = [{
name: 'Tony',
lastName: 'Stark',
age: 42,
}];
vm.resetData(data);
expect(vm.originalData).toEqual(data);
expect(vm.originalData[0].$index).toEqual(0);
expect(vm.formData).toEqual(data);
expect(vm.formData[0].$index).toEqual(0);
expect(vm.watchChanges).not.toBeNull();
});
it('should dont do nothing if data is null', async () => {
vm.resetData(null);
expect(vm.watchChanges).toBeNull();
});
it('should set originalData and formatData with data and generate watchChanges', async () => {
data = {
name: 'Tony',
lastName: 'Stark',
age: 42,
};
vm.resetData(data);
expect(vm.originalData).toEqual(data);
expect(vm.formData).toEqual(data);
expect(vm.watchChanges).not.toBeNull();
});
});
describe('saveChanges()', () => {
data = [{
name: 'Tony',
lastName: 'Stark',
age: 42,
}];
it('should call saveFn if exists', async () => {
await wrapper.setProps({ saveFn: vi.fn() });
vm.saveChanges(data);
expect(vm.saveFn).toHaveBeenCalledOnce();
expect(vm.isLoading).toBe(false);
expect(vm.hasChanges).toBe(false);
await wrapper.setProps({ saveFn: '' });
});
it("should use default url if there's not saveFn", async () => {
const postMock =vi.spyOn(axios, 'post');
vm.formData = [{
name: 'Bruce',
lastName: 'Wayne',
age: 45,
}]
await vm.saveChanges(data);
expect(postMock).toHaveBeenCalledWith(vm.url + '/crud', data);
expect(vm.isLoading).toBe(false);
expect(vm.hasChanges).toBe(false);
expect(vm.originalData).toEqual(JSON.parse(JSON.stringify(vm.formData)));
});
});
});

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,150 @@
import { describe, expect, it, beforeAll, vi, afterAll } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import FormModel from 'src/components/FormModel.vue';
describe('FormModel', () => {
const model = 'mockModel';
const url = 'mockUrl';
const formInitialData = { mockKey: 'mockVal' };
describe('modelValue', () => {
it('should use the provided model', () => {
const { vm } = mount({ propsData: { model } });
expect(vm.modelValue).toBe(model);
});
it('should use the route meta title when model is not provided', () => {
const { vm } = mount({});
expect(vm.modelValue).toBe('formModel_mockTitle');
});
});
describe('onMounted()', () => {
let mockGet;
beforeAll(() => {
mockGet = vi.spyOn(axios, 'get').mockResolvedValue({ data: {} });
});
afterAll(() => {
mockGet.mockRestore();
});
it('should not fetch when has formInitialData', () => {
mount({ propsData: { url, model, autoLoad: true, formInitialData } });
expect(mockGet).not.toHaveBeenCalled();
});
it('should fetch when there is url and auto-load', () => {
mount({ propsData: { url, model, autoLoad: true } });
expect(mockGet).toHaveBeenCalled();
});
it('should not observe changes', () => {
const { vm } = mount({
propsData: { url, model, observeFormChanges: false, formInitialData },
});
expect(vm.hasChanges).toBe(true);
vm.reset();
expect(vm.hasChanges).toBe(true);
});
it('should observe changes', async () => {
const { vm } = mount({
propsData: { url, model, formInitialData },
});
vm.state.set(model, formInitialData);
expect(vm.hasChanges).toBe(false);
vm.formData.mockKey = 'newVal';
await vm.$nextTick();
expect(vm.hasChanges).toBe(true);
vm.formData.mockKey = 'mockVal';
});
});
describe('trimData()', () => {
let vm;
beforeAll(() => {
vm = mount({}).vm;
});
it('should trim whitespace from string values', () => {
const data = { key1: ' value1 ', key2: ' value2 ' };
const trimmedData = vm.trimData(data);
expect(trimmedData).toEqual({ key1: 'value1', key2: 'value2' });
});
it('should not modify non-string values', () => {
const data = { key1: 123, key2: true, key3: null, key4: undefined };
const trimmedData = vm.trimData(data);
expect(trimmedData).toEqual(data);
});
});
describe('save()', async () => {
it('should not call if there are not changes', async () => {
const { vm } = mount({ propsData: { url, model } });
await vm.save();
expect(vm.hasChanges).toBe(false);
});
it('should call axios.patch with the right data', async () => {
const spy = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} });
const { vm } = mount({ propsData: { url, model } });
vm.formData.mockKey = 'newVal';
await vm.$nextTick();
await vm.save();
expect(spy).toHaveBeenCalled();
vm.formData.mockKey = 'mockVal';
});
it('should call axios.post with the right data', async () => {
const spy = vi.spyOn(axios, 'post').mockResolvedValue({ data: {} });
const { vm } = mount({
propsData: { url, model, formInitialData, urlCreate: 'mockUrlCreate' },
});
await vm.$nextTick();
vm.formData.mockKey = 'newVal';
await vm.$nextTick();
await vm.save();
expect(spy).toHaveBeenCalled();
vm.formData.mockKey = 'mockVal';
});
it('should use the saveFn', async () => {
const { vm } = mount({
propsData: { url, model, formInitialData, saveFn: () => {} },
});
const spyPatch = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} });
const spySaveFn = vi.spyOn(vm.$props, 'saveFn');
await vm.$nextTick();
vm.formData.mockKey = 'newVal';
await vm.$nextTick();
await vm.save();
expect(spyPatch).not.toHaveBeenCalled();
expect(spySaveFn).toHaveBeenCalled();
vm.formData.mockKey = 'mockVal';
});
it('should reload the data after save', async () => {
const { vm } = mount({
propsData: { url, model, formInitialData, reload: true },
});
vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} });
vm.formData.mockKey = 'newVal';
await vm.$nextTick();
await vm.save();
vm.formData.mockKey = 'mockVal';
});
});
});
function mount({ propsData = {} }) {
return createWrapper(FormModel, {
propsData,
});
}

View File

@ -0,0 +1,388 @@
import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import Leftmenu from 'components/LeftMenu.vue';
import * as vueRouter from 'vue-router';
import { useNavigationStore } from 'src/stores/useNavigationStore';
let vm;
let navigation;
vi.mock('src/router/modules', () => ({
default: [
{
path: '/customer',
name: 'Customer',
meta: {
title: 'customers',
icon: 'vn:client',
},
menus: {
main: ['CustomerList', 'CustomerCreate'],
card: ['CustomerBasicData'],
},
children: [
{
path: '',
name: 'CustomerMain',
meta: {
menu: 'Customer',
menuChildren: [
{
name: 'CustomerCreditContracts',
title: 'creditContracts',
icon: 'vn:solunion',
},
],
},
children: [
{
path: 'list',
name: 'CustomerList',
meta: {
title: 'list',
icon: 'view_list',
menuChildren: [
{
name: 'CustomerCreditContracts',
title: 'creditContracts',
icon: 'vn:solunion',
},
],
},
},
{
path: 'create',
name: 'CustomerCreate',
meta: {
title: 'createCustomer',
icon: 'vn:addperson',
},
},
],
},
],
},
],
}));
vi.spyOn(vueRouter, 'useRoute').mockReturnValue({
matched: [
{
path: '/',
redirect: {
name: 'Dashboard',
},
name: 'Main',
meta: {},
props: {
default: false,
},
children: [
{
path: '/dashboard',
name: 'Dashboard',
meta: {
title: 'dashboard',
icon: 'dashboard',
},
},
],
},
{
path: '/customer',
redirect: {
name: 'CustomerMain',
},
name: 'Customer',
meta: {
title: 'customers',
icon: 'vn:client',
moduleName: 'Customer',
keyBinding: 'c',
menu: 'customer',
},
},
],
query: {},
params: {},
meta: { moduleName: 'mockName' },
path: 'mockName/1',
name: 'Customer',
});
function mount(source = 'main') {
vi.spyOn(axios, 'get').mockResolvedValue({
data: [],
});
const wrapper = createWrapper(Leftmenu, {
propsData: {
source,
},
});
navigation = useNavigationStore();
navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true));
navigation.getModules = vi.fn().mockReturnValue({
value: [
{
name: 'customer',
title: 'customer.pageTitles.customers',
icon: 'vn:customer',
module: 'customer',
},
],
});
return wrapper;
}
describe('getRoutes', () => {
afterEach(() => vi.clearAllMocks());
const getRoutes = vi.fn().mockImplementation((props, getMethodA, getMethodB) => {
const handleRoutes = {
methodA: getMethodA,
methodB: getMethodB,
};
try {
handleRoutes[props.source]();
} catch (error) {
throw Error('Method not defined');
}
});
const getMethodA = vi.fn();
const getMethodB = vi.fn();
const fn = (props) => getRoutes(props, getMethodA, getMethodB);
it('should call getMethodB when source is card', () => {
let props = { source: 'methodB' };
fn(props);
expect(getMethodB).toHaveBeenCalled();
expect(getMethodA).not.toHaveBeenCalled();
});
it('should call getMethodA when source is main', () => {
let props = { source: 'methodA' };
fn(props);
expect(getMethodA).toHaveBeenCalled();
expect(getMethodB).not.toHaveBeenCalled();
});
it('should call getMethodA when source is not exists or undefined', () => {
let props = { source: 'methodC' };
expect(() => fn(props)).toThrowError('Method not defined');
expect(getMethodA).not.toHaveBeenCalled();
expect(getMethodB).not.toHaveBeenCalled();
});
});
describe('Leftmenu as card', () => {
beforeAll(() => {
vm = mount('card').vm;
});
it('should get routes for card source', async () => {
vm.getRoutes();
});
});
describe('Leftmenu as main', () => {
beforeEach(() => {
vm = mount().vm;
});
it('should initialize with default props', () => {
expect(vm.source).toBe('main');
});
it('should filter items based on search input', async () => {
vm.search = 'cust';
await vm.$nextTick();
expect(vm.filteredItems[0].name).toEqual('customer');
expect(vm.filteredItems[0].module).toEqual('customer');
});
it('should filter items based on search input', async () => {
vm.search = 'Rou';
await vm.$nextTick();
expect(vm.filteredItems).toEqual([]);
});
it('should return pinned items', () => {
vm.items = [
{ name: 'Item 1', isPinned: false },
{ name: 'Item 2', isPinned: true },
];
expect(vm.pinnedModules).toEqual(
new Map([['Item 2', { name: 'Item 2', isPinned: true }]]),
);
});
it('should find matches in routes', () => {
const search = 'child1';
const item = {
children: [
{ name: 'child1', children: [] },
{ name: 'child2', children: [] },
],
};
const matches = vm.findMatches(search, item);
expect(matches).toEqual([{ name: 'child1', children: [] }]);
});
it('should not proceed if event is already prevented', async () => {
const item = { module: 'testModule', isPinned: false };
const event = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
defaultPrevented: true,
};
await vm.togglePinned(item, event);
expect(event.preventDefault).not.toHaveBeenCalled();
expect(event.stopPropagation).not.toHaveBeenCalled();
});
it('should call quasar.notify with success message', async () => {
const item = { module: 'testModule', isPinned: false };
const event = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
defaultPrevented: false,
};
const response = { data: { id: 1 } };
vi.spyOn(axios, 'post').mockResolvedValue(response);
vi.spyOn(vm.quasar, 'notify');
await vm.togglePinned(item, event);
expect(vm.quasar.notify).toHaveBeenCalledWith({
message: 'Data saved',
type: 'positive',
});
});
it('should handle a single matched route with a menu', () => {
const route = {
matched: [{ meta: { menu: 'customer' } }],
};
const result = vm.betaGetRoutes();
expect(result.meta.menu).toEqual(route.matched[0].meta.menu);
});
it('should get routes for main source', () => {
vm.props.source = 'main';
vm.getRoutes();
expect(navigation.getModules).toHaveBeenCalled();
});
it('should find direct child matches', () => {
const search = 'child1';
const item = {
children: [{ name: 'child1' }, { name: 'child2' }],
};
const result = vm.findMatches(search, item);
expect(result).toEqual([{ name: 'child1' }]);
});
it('should find nested child matches', () => {
const search = 'child3';
const item = {
children: [
{ name: 'child1' },
{
name: 'child2',
children: [{ name: 'child3' }],
},
],
};
const result = vm.findMatches(search, item);
expect(result).toEqual([{ name: 'child3' }]);
});
});
describe('normalize', () => {
beforeAll(() => {
vm = mount('card').vm;
});
it('should normalize and lowercase text', () => {
const input = 'ÁÉÍÓÚáéíóú';
const expected = 'aeiouaeiou';
expect(vm.normalize(input)).toBe(expected);
});
it('should handle empty string', () => {
const input = '';
const expected = '';
expect(vm.normalize(input)).toBe(expected);
});
it('should handle text without diacritics', () => {
const input = 'hello';
const expected = 'hello';
expect(vm.normalize(input)).toBe(expected);
});
it('should handle mixed text', () => {
const input = 'Héllo Wórld!';
const expected = 'hello world!';
expect(vm.normalize(input)).toBe(expected);
});
});
describe('addChildren', () => {
const module = 'testModule';
beforeEach(() => {
vm = mount().vm;
vi.clearAllMocks();
});
it('should add menu items to parent if matches are found', () => {
const parent = 'testParent';
const route = {
meta: {
menu: 'testMenu',
},
children: [{ name: 'child1' }, { name: 'child2' }],
};
vm.addChildren(module, route, parent);
expect(navigation.addMenuItem).toHaveBeenCalled();
});
it('should handle routes with no meta menu', () => {
const route = {
meta: {},
menus: {},
};
const parent = [];
vm.addChildren(module, route, parent);
expect(navigation.addMenuItem).toHaveBeenCalled();
});
it('should handle empty parent array', () => {
const parent = [];
const route = {
meta: {
menu: 'child11',
},
children: [
{
name: 'child1',
meta: {
menuChildren: [
{
name: 'CustomerCreditContracts',
title: 'creditContracts',
icon: 'vn:solunion',
},
],
},
},
],
};
vm.addChildren(module, route, parent);
expect(navigation.addMenuItem).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,65 @@
import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import UserPanel from 'src/components/UserPanel.vue';
import axios from 'axios';
import { useState } from 'src/composables/useState';
vi.mock('src/utils/quasarLang', () => ({
default: vi.fn(),
}));
describe('UserPanel', () => {
let wrapper;
let vm;
let state;
beforeEach(() => {
wrapper = createWrapper(UserPanel, {});
state = useState();
state.setUser({
id: 115,
name: 'itmanagement',
nickname: 'itManagementNick',
lang: 'en',
darkMode: false,
companyFk: 442,
warehouseFk: 1,
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should fetch warehouses data on mounted', async () => {
const fetchData = wrapper.findComponent({ name: 'FetchData' });
expect(fetchData.props('url')).toBe('Warehouses');
expect(fetchData.props('autoLoad')).toBe(true);
});
it('should toggle dark mode correctly and update preferences', async () => {
await vm.saveDarkMode(true);
expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true });
expect(vm.user.darkMode).toBe(true);
await vm.updatePreferences();
expect(vm.darkMode).toBe(true);
});
it('should change user language and update preferences', async () => {
const userLanguage = 'es';
await vm.saveLanguage(userLanguage);
expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage });
expect(vm.user.lang).toBe(userLanguage);
await vm.updatePreferences();
expect(vm.locale).toBe(userLanguage);
});
it('should update user data', async () => {
const key = 'name';
const value = 'itboss';
await vm.saveUserData(key, value);
expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value });
});
});

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
icon="tune"
color="white"
>
<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';
const slots = useSlots();
const hasContent = ref(false);
const rightPanel = ref(null);
onMounted(() => {
rightPanel.value = document.querySelector('#right-panel');
if (!rightPanel.value) return;
// Check if there's content to display
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) stateStore.rightDrawer = false;
});
import { useQuasar } from 'quasar';
import { useHasContent } from 'src/composables/useHasContent';
const { t } = useI18n();
const quasar = useQuasar();
const stateStore = useStateStore();
const slots = useSlots();
const hasContent = useHasContent('#right-panel');
onMounted(() => {
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']"
@ -38,6 +26,7 @@ const stateStore = useStateStore();
round
dense
icon="dock_to_left"
data-cy="toggle-right-drawer"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
@ -45,7 +34,7 @@ const stateStore = useStateStore();
</QBtn>
</div>
</Teleport>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256">
<QScrollArea class="fit">
<div id="right-panel"></div>
<slot v-if="!hasContent" name="right-panel" />

View File

@ -58,79 +58,71 @@ const getConfig = async (url, filter) => {
};
const fetchViewConfigData = async () => {
try {
const userConfigFilter = {
where: { tableCode: $props.tableCode, userFk: user.value.id },
};
const userConfig = await getConfig('UserConfigViews', userConfigFilter);
const userConfigFilter = {
where: { tableCode: $props.tableCode, userFk: user.value.id },
};
const userConfig = await getConfig('UserConfigViews', userConfigFilter);
if (userConfig) {
initialUserConfigViewData.value = userConfig;
setUserConfigViewData(userConfig.configuration);
return;
}
if (userConfig) {
initialUserConfigViewData.value = userConfig;
setUserConfigViewData(userConfig.configuration);
return;
}
const defaultConfigFilter = { where: { tableCode: $props.tableCode } };
const defaultConfig = await getConfig('DefaultViewConfigs', defaultConfigFilter);
const defaultConfigFilter = { where: { tableCode: $props.tableCode } };
const defaultConfig = await getConfig('DefaultViewConfigs', defaultConfigFilter);
if (defaultConfig) {
// Si el backend devuelve una configuración por defecto la usamos
setUserConfigViewData(defaultConfig.columns);
return;
} else {
// Si no hay configuración por defecto mostramos todas las columnas
const defaultColumns = {};
$props.allColumns.forEach((col) => (defaultColumns[col] = true));
setUserConfigViewData(defaultColumns);
}
} catch (err) {
console.error('Error fetching config view data', err);
if (defaultConfig) {
// Si el backend devuelve una configuración por defecto la usamos
setUserConfigViewData(defaultConfig.columns);
return;
} else {
// Si no hay configuración por defecto mostramos todas las columnas
const defaultColumns = {};
$props.allColumns.forEach((col) => (defaultColumns[col] = true));
setUserConfigViewData(defaultColumns);
}
};
const saveConfig = async () => {
try {
const params = {};
const configuration = {};
const params = {};
const configuration = {};
formattedCols.value.forEach((col) => {
const { name, active } = col;
configuration[name] = active;
});
formattedCols.value.forEach((col) => {
const { name, active } = col;
configuration[name] = active;
});
// Si existe una view config del usuario hacemos un update si no la creamos
if (initialUserConfigViewData.value) {
params.updates = [
{
data: {
configuration: configuration,
},
where: {
id: initialUserConfigViewData.value.id,
},
},
];
} else {
params.creates = [
{
userFk: user.value.id,
tableCode: $props.tableCode,
tableConfig: $props.tableCode,
// Si existe una view config del usuario hacemos un update si no la creamos
if (initialUserConfigViewData.value) {
params.updates = [
{
data: {
configuration: configuration,
},
];
}
const response = await axios.post('UserConfigViews/crud', params);
if (response.data && response.data[0]) {
initialUserConfigViewData.value = response.data[0];
}
emitSavedConfig();
notify('globals.dataSaved', 'positive');
popupProxyRef.value.hide();
} catch (err) {
console.error('Error saving user view config', err);
where: {
id: initialUserConfigViewData.value.id,
},
},
];
} else {
params.creates = [
{
userFk: user.value.id,
tableCode: $props.tableCode,
tableConfig: $props.tableCode,
configuration: configuration,
},
];
}
const response = await axios.post('UserConfigViews/crud', params);
if (response.data && response.data[0]) {
initialUserConfigViewData.value = response.data[0];
}
emitSavedConfig();
notify('globals.dataSaved', 'positive');
popupProxyRef.value.hide();
};
const emitSavedConfig = () => {

View File

@ -1,20 +1,24 @@
<script setup>
import { ref, watch } from 'vue';
import { nextTick, ref, watch } from 'vue';
import { QInput } from 'quasar';
const props = defineProps({
const $props = defineProps({
modelValue: {
type: String,
default: '',
},
insertable: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:modelValue', 'accountShortToStandard']);
let internalValue = ref(props.modelValue);
let internalValue = ref($props.modelValue);
watch(
() => props.modelValue,
() => $props.modelValue,
(newVal) => {
internalValue.value = newVal;
}
@ -28,8 +32,46 @@ watch(
}
);
const handleKeydown = (e) => {
if (e.key === 'Backspace') return;
if (e.key === '.') {
accountShortToStandard();
// TODO: Fix this setTimeout, with nextTick doesn't work
setTimeout(() => {
setCursorPosition(0, e.target);
}, 1);
return;
}
if ($props.insertable && e.key.match(/[0-9]/)) {
handleInsertMode(e);
}
};
function setCursorPosition(pos, el = vnInputRef.value) {
el.focus();
el.setSelectionRange(pos, pos);
}
const vnInputRef = ref(false);
const handleInsertMode = (e) => {
e.preventDefault();
const input = e.target;
const cursorPos = input.selectionStart;
const { maxlength } = vnInputRef.value;
let currentValue = internalValue.value;
if (!currentValue) currentValue = e.key;
const newValue = e.key;
if (newValue && !isNaN(newValue) && cursorPos < maxlength) {
internalValue.value =
currentValue.substring(0, cursorPos) +
newValue +
currentValue.substring(cursorPos + 1);
}
nextTick(() => {
input.setSelectionRange(cursorPos + 1, cursorPos + 1);
});
};
function accountShortToStandard() {
internalValue.value = internalValue.value.replace(
internalValue.value = internalValue.value?.replace(
'.',
'0'.repeat(11 - internalValue.value.length)
);
@ -37,5 +79,5 @@ function accountShortToStandard() {
</script>
<template>
<q-input v-model="internalValue" />
<QInput @keydown="handleKeydown" ref="vnInputRef" v-model="internalValue" />
</template>

View File

@ -15,7 +15,7 @@ let root = ref(null);
watchEffect(() => {
matched.value = currentRoute.value.matched.filter(
(matched) => Object.keys(matched.meta).length
(matched) => !!matched?.meta?.title || !!matched?.meta?.icon
);
breadcrumbs.value.length = 0;
if (!matched.value[0]) return;

View File

@ -0,0 +1,20 @@
<script setup>
import VnSelect from './VnSelect.vue';
defineProps({
selectProps: { type: Object, required: true },
promise: { type: Function, default: () => {} },
});
</script>
<template>
<QBtnDropdown v-bind="$attrs" color="primary">
<VnSelect
v-bind="selectProps"
hide-selected
hide-dropdown-icon
focus-on-mount
@update:model-value="promise"
data-cy="vnBtnSelect_select"
/>
</QBtnDropdown>
</template>

View File

@ -10,11 +10,11 @@ import LeftMenu from 'components/LeftMenu.vue';
import RightMenu from 'components/common/RightMenu.vue';
const props = defineProps({
dataKey: { type: String, required: true },
baseUrl: { type: String, default: undefined },
customUrl: { type: String, default: undefined },
url: { type: String, default: undefined },
filter: { type: Object, default: () => {} },
descriptor: { type: Object, required: true },
filterPanel: { type: Object, default: undefined },
idInWhere: { type: Boolean, default: false },
searchDataKey: { type: String, default: undefined },
searchbarProps: { type: Object, default: undefined },
redirectOnError: { type: Boolean, default: false },
@ -23,25 +23,20 @@ const props = defineProps({
const stateStore = useStateStore();
const route = useRoute();
const router = useRouter();
const url = computed(() => {
if (props.baseUrl) {
return `${props.baseUrl}/${route.params.id}`;
}
return props.customUrl;
});
const searchRightDataKey = computed(() => {
if (!props.searchDataKey) return route.name;
return props.searchDataKey;
});
const arrayData = useArrayData(props.dataKey, {
url: url.value,
filter: props.filter,
url: props.url,
userFilter: props.filter,
oneRecord: true,
});
onBeforeMount(async () => {
try {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: false, updateRouter: false });
await fetch(route.params.id);
} catch {
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
@ -49,13 +44,17 @@ onBeforeMount(async () => {
}
});
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${to.params.id}`;
await arrayData.fetch({ append: false, updateRouter: false });
}
});
onBeforeRouteUpdate(async (to, from) => {
const id = to.params.id;
if (id !== from.params.id) await fetch(id, true);
});
async function fetch(id, append = false) {
const regex = /\/(\d+)/;
if (props.idInWhere) arrayData.store.filter.where = { id };
else if (!regex.test(props.url)) arrayData.store.url = `${props.url}/${id}`;
else arrayData.store.url = props.url.replace(regex, `/${id}`);
await arrayData.fetch({ append, updateRouter: false });
}
</script>
<template>
@ -83,7 +82,7 @@ if (props.baseUrl) {
<QPage>
<VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]">
<RouterView :key="route.path" />
<RouterView :key="$route.path" />
</div>
</QPage>
</QPageContainer>

View File

@ -0,0 +1,64 @@
<script setup>
import { onBeforeMount } from 'vue';
import { useRouter, onBeforeRouteUpdate } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
import LeftMenu from 'components/LeftMenu.vue';
import VnSubToolbar from '../ui/VnSubToolbar.vue';
const props = defineProps({
dataKey: { type: String, required: true },
url: { type: String, default: undefined },
idInWhere: { type: Boolean, default: false },
filter: { type: Object, default: () => {} },
descriptor: { type: Object, required: true },
filterPanel: { type: Object, default: undefined },
searchDataKey: { type: String, default: undefined },
searchbarProps: { type: Object, default: undefined },
redirectOnError: { type: Boolean, default: false },
});
const stateStore = useStateStore();
const router = useRouter();
const arrayData = useArrayData(props.dataKey, {
url: props.url,
userFilter: props.filter,
oneRecord: true,
});
onBeforeMount(async () => {
const route = router.currentRoute.value;
try {
await fetch(route.params.id);
} catch {
const { matched: matches } = route;
const { path } = matches.at(-1);
router.push({ path: path.replace(/:id.*/, '') });
}
});
onBeforeRouteUpdate(async (to, from) => {
const id = to.params.id;
if (id !== from.params.id) await fetch(id, true);
});
async function fetch(id, append = false) {
const regex = /\/(\d+)/;
if (props.idInWhere) arrayData.store.filter.where = { id };
else if (!regex.test(props.url)) arrayData.store.url = `${props.url}/${id}`;
else arrayData.store.url = props.url.replace(regex, `/${id}`);
await arrayData.fetch({ append, updateRouter: false });
}
</script>
<template>
<Teleport to="#left-panel" v-if="stateStore.isHeaderMounted()">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</Teleport>
<VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]">
<RouterView :key="$route.path" />
</div>
</template>

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

@ -0,0 +1,43 @@
<script setup>
import { computed } from 'vue';
const model = defineModel({ type: [Number, Boolean] });
const $props = defineProps({
info: {
type: String,
default: null,
},
});
const checkboxModel = computed({
get() {
if (typeof model.value === 'number') {
return model.value !== 0;
}
return model.value;
},
set(value) {
if (typeof model.value === 'number') {
model.value = value ? 1 : 0;
} else {
model.value = value;
}
},
});
</script>
<template>
<div>
<QCheckbox v-bind="$attrs" v-on="$attrs" v-model="checkboxModel" />
<QIcon
v-if="info"
v-bind="$attrs"
class="cursor-info q-ml-sm"
name="info"
size="sm"
>
<QTooltip>
{{ info }}
</QTooltip>
</QIcon>
</div>
</template>

View File

@ -0,0 +1,32 @@
<script setup>
const $props = defineProps({
colors: {
type: String,
default: '{"value":[]}',
},
});
const colorArray = JSON.parse($props.colors)?.value;
const maxHeight = 30;
const colorHeight = maxHeight / colorArray?.length;
</script>
<template>
<div class="color-div" :style="{ height: `${maxHeight}px` }">
<div
v-for="(color, index) in colorArray"
:key="index"
:style="{
backgroundColor: `#${color}`,
height: `${colorHeight}px`,
}"
>
&nbsp;
</div>
</div>
</template>
<style scoped>
.color-div {
display: flex;
flex-direction: column;
}
</style>

View File

@ -1,5 +1,5 @@
<script setup>
import { computed, defineModel } from 'vue';
import { computed } from 'vue';
const model = defineModel(undefined, { required: true });
const $props = defineProps({
@ -17,6 +17,8 @@ const $props = defineProps({
},
});
const emit = defineEmits(['blur']);
const componentArray = computed(() => {
if (typeof $props.prop === 'object') return [$props.prop];
return $props.prop;
@ -54,6 +56,7 @@ function toValueAttrs(attrs) {
v-bind="mix(toComponent).attrs"
v-on="mix(toComponent).event ?? {}"
v-model="model"
@blur="emit('blur')"
/>
</span>
</template>

View File

@ -0,0 +1,29 @@
<script setup>
const model = defineModel({ type: [String, Number], required: true });
</script>
<template>
<QDate v-model="model" :today-btn="true" :options="$attrs.options" />
</template>
<style lang="scss" scoped>
.q-date {
width: 245px;
min-width: unset;
:deep(.q-date__calendar) {
padding-bottom: 0;
}
:deep(.q-date__view) {
min-height: 245px;
padding: 8px;
}
:deep(.q-date__calendar-days-container) {
min-height: 160px;
height: unset;
}
:deep(.q-date__header) {
padding: 2px 2px 5px 12px;
height: 60px;
}
}
</style>

View File

@ -0,0 +1,39 @@
<script setup>
import { toDateFormat } from 'src/filters/date.js';
defineProps({ date: { type: [Date, String], required: true } });
function getBadgeAttrs(date) {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0);
let timeDiff = today - timeTicket;
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) {
if (!date) return '-';
const dateSplit = date.split('T');
const [year, month, day] = dateSplit[0].split('-');
const newDate = new Date(year, month - 1, day);
return toDateFormat(newDate);
}
</script>
<template>
<QBadge v-bind="getBadgeAttrs(date)" class="q-pa-sm" style="font-size: 14px">
{{ 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

@ -31,6 +31,10 @@ const $props = defineProps({
type: String,
default: null,
},
description: {
type: String,
default: null,
},
});
const warehouses = ref();
@ -43,7 +47,8 @@ const dms = ref({});
onMounted(() => {
defaultData();
if (!$props.formInitialData)
dms.value.description = t($props.model + 'Description', dms.value);
dms.value.description =
$props.description ?? t($props.model + 'Description', dms.value);
});
function onFileChange(files) {
dms.value.hasFileAttached = !!files;
@ -54,7 +59,6 @@ function mapperDms(data) {
const formData = new FormData();
const { files } = data;
if (files) formData.append(files?.name, files);
delete data.files;
const dms = {
hasFile: !!data.hasFile,
@ -78,6 +82,7 @@ async function save() {
const body = mapperDms(dms.value);
const response = await axios.post(getUrl(), body[0], body[1]);
emit('onDataSaved', body[1].params, response);
delete dms.value.files;
return response;
}
@ -158,13 +163,14 @@ function addDefaultData(data) {
/>
<QFile
ref="inputFileRef"
:label="t('entry.buys.file')"
:label="t('globals.file')"
v-model="dms.files"
:multiple="false"
:accept="allowedContentTypes"
@update:model-value="onFileChange(dms.files)"
class="required"
:display-value="dms.file"
data-cy="VnDms_inputFile"
>
<template #append>
<QIcon

View File

@ -17,7 +17,7 @@ import { useSession } from 'src/composables/useSession';
const route = useRoute();
const quasar = useQuasar();
const { t } = useI18n();
const rows = ref();
const rows = ref([]);
const dmsRef = ref();
const formDialog = ref({});
const token = useSession().getTokenMultimedia();
@ -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,
@ -202,7 +202,7 @@ const columns = computed(() => [
prop.row.id,
$props.downloadModel,
undefined,
prop.row.download
prop.row.download,
),
},
{
@ -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"
@ -373,7 +374,7 @@ defineExpose({
v-if="
shouldRenderButton(
button.name,
props.row.isDocuware
props.row.isDocuware,
)
"
:is="button.component"
@ -388,6 +389,14 @@ defineExpose({
</div>
</template>
</QTable>
<div
v-else
class="info-row q-pa-md text-center"
>
<h5>
{{ t('No data to display') }}
</h5>
</div>
</template>
</VnPaginate>
<QDialog v-model="formDialog.show">
@ -404,7 +413,7 @@ defineExpose({
fab
color="primary"
icon="add"
shortcut="+"
v-shortcut
@click="showFormDialog()"
class="fill-icon"
>

View File

@ -1,13 +1,17 @@
<script setup>
import { computed, ref } from 'vue';
import { computed, ref, useAttrs, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useValidator } from 'src/composables/useValidator';
import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const { t } = useI18n();
const emit = defineEmits([
'update:modelValue',
'update:options',
'keyup.enter',
'remove',
'blur',
]);
const $props = defineProps({
@ -27,11 +31,23 @@ const $props = defineProps({
type: Boolean,
default: true,
},
emptyToNull: {
type: Boolean,
default: true,
},
insertable: {
type: Boolean,
default: false,
},
maxlength: {
type: Number,
default: null,
},
uppercase: {
type: Boolean,
default: false,
},
});
const { validations } = useValidator();
const { t } = useI18n();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
const vnInputRef = ref(null);
const value = computed({
@ -39,6 +55,7 @@ const value = computed({
return $props.modelValue;
},
set(value) {
if ($props.emptyToNull && value === '') value = null;
emit('update:modelValue', value);
},
});
@ -59,14 +76,16 @@ const focus = () => {
defineExpose({
focus,
vnInputRef,
});
import { useAttrs } from 'vue';
const $attrs = useAttrs();
const mixinRules = [
requiredFieldRule,
...($attrs.rules ?? []),
(val) => {
const { maxlength } = vnInputRef.value;
if (maxlength && +val.length > maxlength)
return t(`maxLength`, { value: maxlength });
const { min, max } = vnInputRef.value.$attrs;
if (!min) return null;
if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min });
@ -76,6 +95,37 @@ const mixinRules = [
}
},
];
const handleKeydown = (e) => {
if (e.key === 'Backspace') return;
if ($props.insertable && e.key.match(/[0-9]/)) {
handleInsertMode(e);
}
};
const handleInsertMode = (e) => {
e.preventDefault();
const input = e.target;
const cursorPos = input.selectionStart;
const { maxlength } = vnInputRef.value;
let currentValue = value.value;
if (!currentValue) currentValue = e.key;
const newValue = e.key;
if (newValue && !isNaN(newValue) && cursorPos < maxlength) {
value.value =
currentValue.substring(0, cursorPos) +
newValue +
currentValue.substring(cursorPos + 1);
}
nextTick(() => {
input.setSelectionRange(cursorPos + 1, cursorPos + 1);
});
};
const handleUppercase = () => {
value.value = value.value?.toUpperCase() || '';
};
</script>
<template>
@ -85,21 +135,33 @@ const mixinRules = [
v-model="value"
v-bind="{ ...$attrs, ...styleAttrs }"
:type="$attrs.type"
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
@keyup.enter="emit('keyup.enter')"
@blur="emit('blur')"
@keydown="handleKeydown"
:clearable="false"
:rules="mixinRules"
:lazy-rules="true"
hide-bottom-space
:data-cy="$attrs.dataCy ?? $attrs.label + '_input'"
>
<template v-if="$slots.prepend" #prepend>
<template #prepend v-if="$slots.prepend">
<slot name="prepend" />
</template>
<template #append>
<QIcon
name="close"
size="xs"
v-if="hover && value && !$attrs.disabled && $props.clearable"
:style="{
visibility:
hover &&
value &&
!$attrs.disabled &&
!$attrs.readonly &&
$props.clearable
? 'visible'
: 'hidden',
}"
@click="
() => {
value = null;
@ -108,6 +170,19 @@ const mixinRules = [
}
"
></QIcon>
<QIcon
name="match_case"
size="xs"
v-if="!$attrs.disabled && !$attrs.readonly && $props.uppercase"
@click="handleUppercase"
class="uppercase-icon"
>
<QTooltip>
{{ t('Convert to uppercase') }}
</QTooltip>
</QIcon>
<slot name="append" v-if="$slots.append && !$attrs.disabled" />
<QIcon v-if="info" name="info">
<QTooltip max-width="350px">
@ -118,16 +193,29 @@ const mixinRules = [
</QInput>
</div>
</template>
<style>
.uppercase-icon {
transition:
color 0.3s,
transform 0.2s;
cursor: pointer;
}
.uppercase-icon:hover {
color: #ed9937;
transform: scale(1.2);
}
</style>
<i18n>
en:
inputMin: Must be more than {value}
maxLength: The value exceeds {value} characters
inputMax: Must be less than {value}
es:
inputMin: Debe ser mayor a {value}
maxLength: El valor excede los {value} carácteres
inputMax: Debe ser menor a {value}
Convert to uppercase: Convertir a mayúsculas
</i18n>
<style lang="scss">
.q-field__append {
padding-inline: 0;
}
</style>

View File

@ -1,10 +1,13 @@
<script setup>
import { onMounted, watch, computed, ref } from 'vue';
import { onMounted, watch, computed, ref, useAttrs } from 'vue';
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useAttrs } from 'vue';
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 $props = defineProps({
isOutlined: {
type: Boolean,
@ -15,18 +18,13 @@ const $props = defineProps({
default: true,
},
});
import { useValidator } from 'src/composables/useValidator';
const { validations } = useValidator();
const { t } = useI18n();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
const vnInputDateRef = ref(null);
const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref();
const hover = ref();
const mask = ref();
const $attrs = useAttrs();
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
@ -44,7 +42,7 @@ const formattedDate = computed({
if (value.at(2) == '/') value = value.split('/').reverse().join('/');
value = date.formatDate(
new Date(value).toISOString(),
'YYYY-MM-DDTHH:mm:ss.SSSZ'
'YYYY-MM-DDTHH:mm:ss.SSSZ',
);
}
const [year, month, day] = value.split('-').map((e) => parseInt(e));
@ -57,7 +55,7 @@ const formattedDate = computed({
orgDate.getHours(),
orgDate.getMinutes(),
orgDate.getSeconds(),
orgDate.getMilliseconds()
orgDate.getMilliseconds(),
);
}
}
@ -66,7 +64,7 @@ const formattedDate = computed({
});
const popupDate = computed(() =>
model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value
model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value,
);
onMounted(() => {
// fix quasar bug
@ -75,7 +73,7 @@ onMounted(() => {
watch(
() => model.value,
(val) => (formattedDate.value = val),
{ immediate: true }
{ immediate: true },
);
const styleAttrs = computed(() => {
@ -87,6 +85,11 @@ const styleAttrs = computed(() => {
}
: {};
});
const manageDate = (date) => {
formattedDate.value = date;
isPopupOpen.value = false;
};
</script>
<template>
@ -98,10 +101,11 @@ const styleAttrs = computed(() => {
:mask="mask"
placeholder="dd/mm/aaaa"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
:rules="mixinRules"
:clearable="false"
@click="isPopupOpen = true"
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
hide-bottom-space
>
<template #append>
@ -120,15 +124,9 @@ const styleAttrs = computed(() => {
isPopupOpen = false;
"
/>
<QIcon
v-if="showEvent"
name="event"
class="cursor-pointer"
@click="isPopupOpen = !isPopupOpen"
:title="t('Open date')"
/>
</template>
<QMenu
v-if="$q.screen.gt.xs"
transition-show="scale"
transition-hide="scale"
v-model="isPopupOpen"
@ -137,31 +135,14 @@ const styleAttrs = computed(() => {
:no-focus="true"
:no-parent-event="true"
>
<QDate
v-model="popupDate"
:landscape="true"
:today-btn="true"
:options="$attrs.options"
@update:model-value="
(date) => {
formattedDate = date;
isPopupOpen = false;
}
"
/>
<VnDate v-model="popupDate" @update:model-value="manageDate" />
</QMenu>
<QDialog v-else v-model="isPopupOpen">
<VnDate v-model="popupDate" @update:model-value="manageDate" />
</QDialog>
</QInput>
</div>
</template>
<style lang="scss">
.vn-input-date.q-field--standard.q-field--readonly .q-field__control:before {
border-bottom-style: solid;
}
.vn-input-date.q-field--outlined.q-field--readonly .q-field__control:before {
border-style: solid;
}
</style>
<i18n>
es:
Open date: Abrir fecha

View File

@ -1,8 +1,30 @@
<script setup>
import VnInput from 'src/components/common/VnInput.vue';
const model = defineModel({ type: [Number, String] });
</script>
defineProps({
step: { type: Number, default: 0.01 },
decimalPlaces: { type: Number, default: 2 },
positive: { type: Boolean, default: true },
});
const model = defineModel({ type: [Number, String] });
const emit = defineEmits(['blur']);
</script>
<template>
<VnInput v-bind="$attrs" v-model.number="model" type="number" />
<VnInput
v-bind="$attrs"
v-model.number="model"
type="number"
:step="step"
@input="
(evt) => {
const val = evt.target.value;
if (positive && val < 0) return (model = 0);
const [, decimal] = val.split('.');
if (val && decimal?.length > decimalPlaces)
model = parseFloat(val).toFixed(decimalPlaces);
}
"
@blur="emit('blur')"
/>
</template>

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,10 +1,11 @@
<script setup>
import { computed, ref, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import { date } from 'quasar';
import { useValidator } from 'src/composables/useValidator';
const { validations } = useValidator();
import VnTime from './VnTime.vue';
import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const model = defineModel({ type: String });
const props = defineProps({
timeOnly: {
@ -18,8 +19,6 @@ const props = defineProps({
});
const vnInputTimeRef = ref(null);
const initialDate = ref(model.value ?? Date.vnNew());
const { t } = useI18n();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const dateFormat = 'HH:mm';
const isPopupOpen = ref();
@ -76,10 +75,11 @@ function dateToTime(newDate) {
placeholder="--:--"
v-model="formattedTime"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
style="min-width: 100px"
:rules="mixinRules"
@click="isPopupOpen = false"
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
type="time"
hide-bottom-space
>
@ -99,14 +99,9 @@ function dateToTime(newDate) {
isPopupOpen = false;
"
/>
<QIcon
name="Schedule"
class="cursor-pointer"
@click="isPopupOpen = !isPopupOpen"
:title="t('Open time')"
/>
</template>
<QMenu
v-if="$q.screen.gt.xs"
transition-show="scale"
transition-hide="scale"
v-model="isPopupOpen"
@ -115,8 +110,11 @@ function dateToTime(newDate) {
:no-focus="true"
:no-parent-event="true"
>
<QTime v-model="formattedTime" mask="HH:mm" landscape now-btn />
<VnTime v-model="formattedTime" />
</QMenu>
<QDialog v-else v-model="isPopupOpen">
<VnTime v-model="formattedTime" />
</QDialog>
</QInput>
</div>
</template>

View File

@ -2,10 +2,13 @@
import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import VnSelectDialog from 'components/common/VnSelectDialog.vue';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { ref, watch } from 'vue';
import { useAttrs } from 'vue';
import { useRequired } from 'src/composables/useRequired';
const { t } = useI18n();
const emit = defineEmits(['update:model-value', 'update:options']);
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const props = defineProps({
location: {
type: Object,
@ -13,6 +16,15 @@ const props = defineProps({
},
});
watch(
() => props.location,
(newValue) => {
if (!modelValue.value) return;
modelValue.value = formatLocation(newValue) ?? null;
}
);
const mixinRules = [requiredFieldRule];
const locationProperties = [
'postcode',
(obj) =>
@ -22,7 +34,7 @@ const locationProperties = [
(obj) => obj.country?.name,
];
const formatLocation = (obj, properties) => {
const formatLocation = (obj, properties = locationProperties) => {
const parts = properties.map((prop) => {
if (typeof prop === 'string') {
return obj[prop];
@ -39,9 +51,7 @@ const formatLocation = (obj, properties) => {
return filteredParts.join(', ');
};
const modelValue = ref(
props.location ? formatLocation(props.location, locationProperties) : null
);
const modelValue = ref(props.location ? formatLocation(props.location) : null);
function showLabel(data) {
const dataProperties = [
@ -69,11 +79,12 @@ const handleModelValue = (data) => {
:label="t('Location')"
:placeholder="t('search_by_postalcode')"
:input-debounce="300"
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
v-bind="$attrs"
clearable
:emit-value="false"
:tooltip="t('Create new location')"
:rules="mixinRules"
:lazy-rules="true"
>
<template #form>
<CreateNewPostcode

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;
}
@ -238,6 +239,7 @@ async function openPointRecord(id, modelLog) {
pointRecord.value = parseProps(propNames, locale, data);
}
async function setLogTree(data) {
if (!data) return;
logTree.value = getLogTree(data);
}
@ -266,7 +268,7 @@ async function applyFilter() {
filter.where.and.push(selectedFilters.value);
}
paginate.value.fetch(filter);
paginate.value.fetch({ filter });
}
function setDate(type) {
@ -281,7 +283,7 @@ function setDate(type) {
to = date.adjustDate(
to,
{ hour: 21, minute: 59, second: 59, millisecond: 999 },
true
true,
);
switch (type) {
@ -364,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();
}
@ -377,7 +379,7 @@ watch(
() => router.currentRoute.value.params.id,
() => {
applyFilter();
}
},
);
</script>
<template>
@ -390,7 +392,7 @@ watch(
const changedModel = item.changedModel;
return {
locale: useCapitalize(
validations[changedModel]?.locale?.name ?? changedModel
validations[changedModel]?.locale?.name ?? changedModel,
),
value: changedModel,
};
@ -402,7 +404,7 @@ watch(
ref="paginate"
:data-key="`${model}Log`"
:url="`${model}Logs`"
:filter="filter"
:user-filter="filter"
:skeleton="false"
auto-load
@on-fetch="setLogTree"
@ -506,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'`
"
>
@ -576,7 +578,7 @@ watch(
t(
`actions.${
actionsText[log.action]
}`
}`,
)
"
/>
@ -676,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('globals.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"
@ -1052,9 +1059,9 @@ en:
Deletes: Deletes
Accesses: Accesses
Users:
User: Usuario
All: Todo
System: Sistema
User: User
All: All
System: System
properties:
id: ID
claimFk: Claim ID

View File

@ -1,8 +1,8 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'components/LeftMenu.vue';
import { onMounted } from 'vue';
import { onMounted, ref } from 'vue';
import { useQuasar } from 'quasar';
import LeftMenu from '../LeftMenu.vue';
const stateStore = useStateStore();
const $props = defineProps({
@ -14,12 +14,29 @@ const $props = defineProps({
onMounted(
() => (stateStore.leftDrawer = useQuasar().screen.gt.xs ? $props.leftDrawer : false)
);
const teleportRef = ref({});
const hasContent = ref();
let observer;
onMounted(() => {
if (!teleportRef.value) return;
const checkContent = () => {
hasContent.value = teleportRef.value?.innerHTML?.trim() !== '';
};
observer = new MutationObserver(checkContent);
observer.observe(teleportRef.value, { childList: true, subtree: true });
checkContent();
});
</script>
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit text-grey-8">
<LeftMenu />
<div id="left-panel" ref="teleportRef"></div>
<LeftMenu v-if="!hasContent" />
</QScrollArea>
</QDrawer>
<QPageContainer>

View File

@ -0,0 +1,38 @@
<script setup>
import { ref } from 'vue';
defineProps({
label: {
type: String,
default: '',
},
icon: {
type: String,
required: true,
default: null,
},
color: {
type: String,
default: 'primary',
},
tooltip: {
type: String,
default: null,
},
});
const popupProxyRef = ref(null);
</script>
<template>
<QBtn :color="$props.color" :icon="$props.icon" :label="$t($props.label)">
<template #default>
<slot name="extraIcon"></slot>
<QPopupProxy ref="popupProxyRef" style="max-width: none">
<QCard>
<slot :popup="popupProxyRef"></slot>
</QCard>
</QPopupProxy>
<QTooltip>{{ $t($props.tooltip) }}</QTooltip>
</template>
</QBtn>
</template>

View File

@ -9,10 +9,6 @@ const $props = defineProps({
type: Number, //Progress value (1.0 > x > 0.0)
required: true,
},
showDialog: {
type: Boolean,
required: true,
},
cancelled: {
type: Boolean,
required: false,
@ -24,30 +20,22 @@ const emit = defineEmits(['cancel', 'close']);
const dialogRef = ref(null);
const _showDialog = computed({
get: () => $props.showDialog,
set: (value) => {
if (value) dialogRef.value.show();
},
const showDialog = defineModel('showDialog', {
type: Boolean,
default: false,
});
const _progress = computed(() => $props.progress);
const progressLabel = computed(() => `${Math.round($props.progress * 100)}%`);
const cancel = () => {
dialogRef.value.hide();
emit('cancel');
};
</script>
<template>
<QDialog ref="dialogRef" v-model="_showDialog" @hide="onDialogHide">
<QDialog ref="dialogRef" v-model="showDialog" @hide="emit('close')">
<QCard class="full-width dialog">
<QCardSection class="row">
<span class="text-h6">{{ t('Progress') }}</span>
<QSpace />
<QBtn icon="close" flat round dense @click="emit('close')" />
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<QCardSection>
<div class="column">
@ -80,7 +68,7 @@ const cancel = () => {
type="button"
flat
class="text-primary"
@click="cancel()"
v-close-popup
>
{{ t('globals.cancel') }}
</QBtn>

View File

@ -2,5 +2,12 @@
const model = defineModel({ type: Boolean, required: true });
</script>
<template>
<QRadio v-model="model" v-bind="$attrs" dense :dark="true" class="q-mr-sm" />
<QRadio
v-model="model"
v-bind="$attrs"
dense
:dark="true"
class="q-mr-sm"
size="xs"
/>
</template>

View File

@ -0,0 +1,122 @@
<script setup>
import RightAdvancedMenu from './RightAdvancedMenu.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnTableFilter from '../VnTable/VnTableFilter.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,
default: null,
},
dataKey: {
type: String,
default: null,
},
searchBar: {
type: Boolean,
default: true,
},
prefix: {
type: String,
default: null,
},
rightFilter: {
type: Boolean,
default: true,
},
columns: {
type: Array,
default: null,
},
arrayDataProps: {
type: Object,
default: null,
},
redirect: {
type: Boolean,
default: true,
},
keepData: {
type: Boolean,
default: true,
},
});
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);
});
onUnmounted(() => {
if (arrayData) arrayData.destroy();
});
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 && !hasContent"
v-bind="arrayDataProps"
:data-key="dataKey"
:label="$t(`${prefix}.search`)"
:info="$t(`${prefix}.searchInfo`)"
/>
<div :id="searchbarId"></div>
</slot>
<RightAdvancedMenu :is-main-section="isMainSection">
<template #advanced-menu v-if="$slots[advancedMenuSlot] || rightFilter">
<slot :name="advancedMenuSlot">
<VnTableFilter
v-if="rightFilter && columns"
:data-key="dataKey"
:array-data="arrayData"
:columns="columns"
>
<template #moreFilterPanel="{ params, orders, searchFn }">
<slot
name="moreFilterPanel"
v-bind="{ params, orders, searchFn }"
/>
</template>
</VnTableFilter>
</slot>
</template>
</RightAdvancedMenu>
<slot name="body" v-if="isMainSection" />
<RouterView v-else />
</template>

View File

@ -1,9 +1,21 @@
<script setup>
import { ref, toRefs, computed, watch, onMounted, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue';
import { useValidator } from 'src/composables/useValidator';
import { useArrayData } from 'src/composables/useArrayData';
import { useRequired } from 'src/composables/useRequired';
import dataByOrder from 'src/utils/dataByOrder';
import { QItemLabel } from 'quasar';
const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
const $attrs = useAttrs();
const { t } = useI18n();
const isRequired = computed(() => {
return useRequired($attrs).isRequired;
});
const requiredFieldRule = computed(() => {
return useRequired($attrs).requiredFieldRule;
});
const $props = defineProps({
modelValue: {
@ -15,13 +27,17 @@ const $props = defineProps({
default: () => [],
},
optionLabel: {
type: [String],
type: [String, Function],
default: 'name',
},
optionValue: {
type: String,
default: 'id',
},
optionCaption: {
type: String,
default: null,
},
optionFilter: {
type: String,
default: null,
@ -86,26 +102,46 @@ const $props = defineProps({
type: Boolean,
default: false,
},
dataKey: {
type: String,
default: null,
},
isOutlined: {
type: Boolean,
default: false,
},
});
const { validations } = useValidator();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
const $attrs = useAttrs();
const { t } = useI18n();
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();
const dataRef = ref();
const lastVal = ref();
const noOneText = t('globals.noOne');
const noOneOpt = ref({
[optionValue.value]: false,
[optionLabel.value]: noOneText,
});
const sort = computed(() => [...($props.sortBy ?? []), `${$props.optionLabel} DESC`]);
const styleAttrs = computed(() => {
return $props.isOutlined
? {
dense: true,
outlined: true,
rounded: true,
}
: {};
});
Outdated
Review

Para esto no seria mejor ya pedir los datos ordenadors por optionLabel?

Para esto no seria mejor ya pedir los datos ordenadors por optionLabel?

De esta manera nos aseguramos que se ordenan por optionLabel, porque que pasa con esos VnSelect que no tienen definidos los sortBy? Tendríamos que ir uno a uno indicándoles la ordenación.
De esta manera nos aseguramos que, definido o no, se ordenan por label.
También nos aseguramos de aplicar el criterio de ordenar según la posición del texto buscado dentro del valor

De esta manera nos aseguramos que se ordenan por optionLabel, porque que pasa con esos VnSelect que no tienen definidos los sortBy? Tendríamos que ir uno a uno indicándoles la ordenación. De esta manera nos aseguramos que, definido o no, se ordenan por label. También nos aseguramos de aplicar el criterio de ordenar según la posición del texto buscado dentro del valor
const isLoading = ref(false);
const useURL = computed(() => $props.url);
const value = computed({
get() {
return $props.modelValue;
@ -121,7 +157,7 @@ watch(options, (newValue) => {
});
watch(modelValue, async (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
if (!myOptions?.value?.some((option) => option[optionValue.value] == newValue))
await fetchFilter(newValue);
if ($props.noOne) myOptions.value.unshift(noOneOpt.value);
@ -129,11 +165,21 @@ watch(modelValue, async (newValue) => {
onMounted(() => {
setOptions(options.value);
if ($props.url && $props.modelValue && !findKeyInOptions())
if (useURL.value && $props.modelValue && !findKeyInOptions())
fetchFilter($props.modelValue);
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
const arrayDataKey =
$props.dataKey ??
($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label));
const arrayData = useArrayData(arrayDataKey, {
url: $props.url,
searchUrl: false,
mapKey: $attrs['map-key'],
});
function findKeyInOptions() {
if (!$props.options) return;
return filter($props.modelValue, $props.options)?.length;
@ -158,9 +204,10 @@ function sortOptions(data) {
});
}
function setOptions(data) {
if (lastVal.value) data = sortOptions(data);
data = dataByOrder(data, $props.sortBy);
myOptions.value = JSON.parse(JSON.stringify(data));
myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
emit('update:options', data);
}
function filter(val, options) {
@ -177,24 +224,23 @@ function filter(val, options) {
});
}
if (!row) return;
const id = row[$props.optionValue];
const optionLabel = String(row[$props.optionLabel]).toLowerCase();
if (!row) return;
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);
});
}
async function fetchFilter(val) {
if (!$props.url || !dataRef.value) return;
if (!$props.url) return;
const { fields, include, limit } = $props;
const key =
optionFilterValue.value ??
(new RegExp(/\d/g).test(val)
? optionValue.value
: optionFilter.value ?? optionLabel.value);
: (optionFilter.value ?? optionLabel.value));
let defaultWhere = {};
if ($props.filterOptions.length) {
@ -206,10 +252,17 @@ 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;
const filterOptions = { where, include, limit };
if (fields) filterOptions.fields = fields;
if (sortBy) filterOptions.order = sortBy;
arrayData.resetPagination();
return dataRef.value.fetch(fetchOptions);
const { data } = await arrayData.applyFilter(
{ filter: filterOptions },
{ updateRouter: false },
);
setOptions(data);
return data;
}
async function filterHandler(val, update) {
@ -239,7 +292,7 @@ async function filterHandler(val, update) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
}
},
);
}
@ -248,25 +301,70 @@ function nullishToTrue(value) {
}
const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
async function onScroll({ to, direction, from, index }) {
const lastIndex = myOptions.value.length - 1;
if (from === 0 && index === 0) return;
if (!useURL.value && !$props.fetchRef) return;
if (direction === 'decrease') return;
if (to === lastIndex && arrayData.store.hasMoreData && !isLoading.value) {
isLoading.value = true;
await arrayData.loadMore();
setOptions(arrayData.store.data);
vnSelectRef.value.scrollTo(lastIndex);
isLoading.value = false;
}
}
defineExpose({ opts: myOptions, vnSelectRef });
function handleKeyDown(event) {
if (event.key === 'Tab' && !event.shiftKey) {
event.preventDefault();
const inputValue = vnSelectRef.value?.inputValue;
if (inputValue) {
const matchingOption = myOptions.value.find(
(option) =>
option[optionLabel.value].toLowerCase() === inputValue.toLowerCase(),
);
if (matchingOption) {
emit('update:modelValue', matchingOption[optionValue.value]);
} else {
emit('update:modelValue', inputValue);
}
vnSelectRef.value?.hidePopup();
}
const focusableElements = document.querySelectorAll(
'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])',
);
const currentIndex = Array.prototype.indexOf.call(
focusableElements,
event.target,
);
if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) {
focusableElements[currentIndex + 1].focus();
}
}
}
function getCaption(opt) {
if (optionCaption.value === false) return;
return opt[optionCaption.value] || opt[optionValue.value];
}
</script>
<template>
<FetchData
ref="dataRef"
:url="$props.url"
@on-fetch="(data) => setOptions(data)"
:where="where || { [optionValue]: value }"
:limit="limit"
:sort-by="sort"
:fields="fields"
:params="params"
/>
<QSelect
v-model="value"
:options="myOptions"
:option-label="optionLabel"
:option-value="optionValue"
v-bind="$attrs"
v-bind="{ ...$attrs, ...styleAttrs }"
@filter="filterHandler"
:emit-value="nullishToTrue($attrs['emit-value'])"
:map-options="nullishToTrue($attrs['map-options'])"
@ -275,16 +373,22 @@ const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
:fill-input="nullishToTrue($attrs['fill-input'])"
ref="vnSelectRef"
lazy-rules
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
:rules="mixinRules"
virtual-scroll-slice-size="options.length"
hide-bottom-space
:input-debounce="useURL ? '300' : '0'"
:loading="isLoading"
@virtual-scroll="onScroll"
@keydown="handleKeyDown"
:data-cy="$attrs.dataCy ?? $attrs.label + '_select'"
:data-url="url"
>
<template v-if="isClearable" #append>
<template #append>
<QIcon
v-show="value"
v-show="isClearable && value"
name="close"
@click.stop="
@click="
() => {
value = null;
emit('remove');
@ -295,7 +399,38 @@ const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
/>
</template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
<div v-if="slotName == 'append'">
<QIcon
v-show="isClearable && value"
name="close"
@click.stop="
() => {
value = null;
emit('remove');
}
"
class="cursor-pointer"
size="xs"
/>
<slot name="append" v-if="$slots.append" v-bind="slotData ?? {}" />
</div>
<slot v-else :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps">
<QItemSection v-if="typeof opt !== 'object'"> {{ opt }}</QItemSection>
<QItemSection v-else-if="opt[optionValue] == opt[optionLabel]">
<QItemLabel>{{ opt[optionLabel] }}</QItemLabel>
</QItemSection>
<QItemSection v-else>
<QItemLabel>
{{ opt[optionLabel] }}
</QItemLabel>
<QItemLabel caption v-if="getCaption(opt)">
{{ `#${getCaption(opt)}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</QSelect>
</template>

View File

@ -14,7 +14,7 @@ const $props = defineProps({
},
});
const options = ref([]);
const emit = defineEmits(['blur']);
onBeforeMount(async () => {
const { url, optionValue, optionLabel } = useAttrs();
const findBy = $props.find ?? url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1);
@ -35,5 +35,5 @@ onBeforeMount(async () => {
});
</script>
<template>
<VnSelect v-bind="$attrs" :options="$attrs.options ?? options" />
<VnSelect v-bind="$attrs" :options="$attrs.options ?? options" @blur="emit('blur')" />
</template>

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,16 +34,19 @@ const isAllowedToCreate = computed(() => {
if ($props.acls.length) return acl.hasAny($props.acls);
return role.hasAny($props.rolesAllowedToCreate);
});
</script>
defineExpose({ vnSelectDialogRef: select });
</script>
<template>
<VnSelect
ref="select"
v-model="value"
v-bind="$attrs"
@update:model-value="(...args) => emit('update:modelValue', ...args)"
>
<template v-if="isAllowedToCreate" #append>
<QIcon
:data-cy="$attrs.dataCy ?? $attrs.label + '_icon'"
@click.stop.prevent="$refs.dialog.show()"
:name="actionIcon"
:size="actionIcon === 'add' ? 'xs' : 'sm'"
@ -62,7 +66,6 @@ const isAllowedToCreate = computed(() => {
</template>
</VnSelect>
</template>
<style lang="scss" scoped>
.default-icon {
cursor: pointer;

View File

@ -0,0 +1,35 @@
<script setup>
import VnSelect from 'components/common/VnSelect.vue';
const model = defineModel({ type: [String, Number, Object] });
</script>
<template>
<VnSelect
:label="$t('globals.supplier')"
v-bind="$attrs"
v-model="model"
url="Suppliers"
option-value="id"
option-label="nickname"
:fields="['id', 'name', 'nickname', 'nif']"
:filter-options="['id', 'name', 'nickname', 'nif']"
sort-by="name ASC"
data-cy="vnSupplierSelect"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.name }}
</QItemLabel>
<QItemLabel caption>
{{
`#${scope.opt?.id} , ${scope.opt?.nickname} (${scope.opt?.nif})`
}}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template>

View File

@ -0,0 +1,50 @@
<script setup>
import VnSelectDialog from './VnSelectDialog.vue';
import FilterTravelForm from 'src/components/FilterTravelForm.vue';
import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters';
const { t } = useI18n();
const $props = defineProps({
data: {
type: Object,
required: true,
},
onFilterTravelSelected: {
type: Function,
required: true,
},
});
</script>
<template>
<VnSelectDialog
:label="t('entry.basicData.travel')"
v-bind="$attrs"
url="Travels/filter"
:fields="['id', 'warehouseInName']"
option-value="id"
option-label="warehouseInName"
map-options
hide-selected
:required="true"
action-icon="filter_alt"
:roles-allowed-to-create="['buyer']"
>
<template #form>
<FilterTravelForm @travel-selected="onFilterTravelSelected(data, $event)" />
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.agencyModeName }} -
{{ scope.opt?.warehouseInName }}
({{ toDate(scope.opt?.shipped) }})
{{ scope.opt?.warehouseOutName }}
({{ toDate(scope.opt?.landed) }})
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectDialog>
</template>

View File

@ -0,0 +1,88 @@
<script setup>
import { computed, useAttrs } from 'vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnAvatar from 'src/components/ui/VnAvatar.vue';
const emit = defineEmits(['update:modelValue']);
const $props = defineProps({
hasAvatar: {
type: Boolean,
default: false,
},
info: {
type: String,
default: undefined,
},
modelValue: {
type: [String, Number, Object],
default: null,
},
});
const $attrs = useAttrs();
const value = computed({
get() {
return $props.modelValue;
},
set(val) {
emit('update:modelValue', val);
},
});
const url = computed(() => {
let url = 'Workers/search';
const { departmentCodes } = $attrs.params ?? {};
if (!departmentCodes) return url;
const params = new URLSearchParams({
departmentCodes: JSON.stringify(departmentCodes),
});
return url.concat(`?${params.toString()}`);
});
</script>
<template>
<VnSelect
:label="$t('globals.worker')"
v-bind="$attrs"
v-model="value"
:url="url"
option-value="id"
option-label="nickname"
:fields="['id', 'name', 'nickname', 'code']"
:filter-options="['id', 'name', 'nickname', 'code']"
sort-by="nickname ASC"
data-cy="vnWorkerSelect"
>
<template #prepend v-if="$props.hasAvatar">
<VnAvatar :worker-id="value" color="primary" v-bind="$attrs" />
</template>
<template #append v-if="$props.info">
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{ $t($props.info) }}</QTooltip>
</QIcon>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt.name }}
</QItemLabel>
<QItemLabel v-if="!scope.opt.id">
{{ scope.opt.nickname }}
</QItemLabel>
<QItemLabel caption v-else>
#{{ scope.opt.id }}, {{ scope.opt.nickname }},
{{ scope.opt.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template>
<i18n>
es:
Responsible for approving invoices: Responsable de aprobar las facturas
</i18n>

View File

@ -86,7 +86,7 @@ async function send() {
</script>
<template>
<QDialog ref="dialogRef">
<QDialog ref="dialogRef" data-cy="vnSmsDialog">
<QCard class="q-pa-sm">
<QCardSection class="row items-center q-pb-none">
<span class="text-h6 text-grey">
@ -161,6 +161,7 @@ async function send() {
:loading="isLoading"
color="primary"
unelevated
data-cy="sendSmsBtn"
/>
</QCardActions>
</QCard>

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