0
0
Fork 0

Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 6553-workerBusiness

This commit is contained in:
Carlos Satorres 2024-09-03 08:11:01 +02:00
commit 60e9accd99
499 changed files with 28012 additions and 21444 deletions

33
.husky/addReferenceTag.js Normal file
View File

@ -0,0 +1,33 @@
const fs = require('fs');
const path = require('path');
function getCurrentBranchName(p = process.cwd()) {
if (!fs.existsSync(p)) return false;
const gitHeadPath = path.join(p, '.git', 'HEAD');
if (!fs.existsSync(gitHeadPath))
return getCurrentBranchName(path.resolve(p, '..'));
const headContent = fs.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 reference = branchName.match(/^\d+/);
const referenceTag = `refs #${reference}`;
if (!msg.includes(referenceTag) && reference) {
const splitedMsg = msg.split(':');
if (splitedMsg.length > 1) {
const finalMsg = splitedMsg[0] + ': ' + referenceTag + splitedMsg.slice(1).join(':');
fs.writeFileSync(msgPath, finalMsg);
}
}
}

8
.husky/commit-msg Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "Running husky commit-msg hook"
npx --no-install commitlint --edit
echo "Adding reference tag to commit message"
node .husky/addReferenceTag.js

View File

@ -14,5 +14,5 @@
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"cSpell.words": ["axios"] "cSpell.words": ["axios", "composables"]
} }

View File

@ -1,3 +1,395 @@
# Version 24.36 - 2024-08-27
### Added 🆕
- feat(FormModel): trim data by default by:alexm
- feat(orderBasicData): add notes by:alexm
- feat(orderList): correct create order by:alexm
- feat(orderList): use orderFilter and fixed this by:alexm
- feat: #7323 handle workerPhoto (origin/7323_workerPhoto, 7323_workerPhoto) by:Javier Segarra
- feat: add recover password and reset password by:alexm
- feat: refs #7346 add seriaType option by:jgallego
- feat: refs #7346 elimino === by:jgallego
- feat: refs #7346 formdata uses serialType by:jgallego
- feat: refs #7346 refactor by:jgallego
- feat: refs #7346 sonarLint warnings (origin/7346-invoiceOutMultilple, 7346-invoiceOutMultilple) by:jgallego
- feat: refs #7710 uses cloneAll by:jgallego
- fix: refs #7717 fix OrderList table filters' and summary table style by:Jon
### Changed 📦
- feat: refs #7346 refactor by:jgallego
- perf: date fields (mindshore/feature/TicketFutureFilter, feature/TicketFutureFilter) by:Javier Segarra
- perf: refs #7717 right menu filter by:Jon
- perf: use ref at component start by:Javier Segarra
- refactor: refs #7717 delete useless function and import by:Jon
- refactor: refs #7717 deleted useless code by:Jon
### Fixed 🛠️
- feat(orderList): use orderFilter and fixed this by:alexm
- fix(VnTable): orderBy v-model by:alexm
- fix(account_card): redirection by:carlossa
- fix(orderLines): reload when delete and redirect when confirm by:alexm
- fix: #6336 ClaimListStates by:Javier Segarra
- fix: account subsections cards by:carlossa
- fix: duplicate key by:Jon
- fix: order description to vnTable by:alexm
- fix: orderCatalogFilter order by:alexm
- fix: quasar build warnings (6336_claim_fix_states) by:Javier Segarra
- fix: refs #7717 fix OrderList table filters' and summary table style by:Jon
- fix: refs #7717 fix basic data form & minor errors by:Jon
- fix: refs #7717 fix catalog filter, searchbar redirect and search by:Jon
- fix: refs #7717 fix catalog searchbar and worker tests(refs #7323) by:Jon
- fix: refs #7717 fix order sections by:Jon
- fix: refs #7717 fix volume and lines redirect by:Jon
- fix: refs #7717 fixed searchbar filter with rightmenu filters' applied by:Jon
- fix: test by:alexm
- fix: ticketDescriptorMenu by:Javier Segarra
- refs #7355 account fixes by:carlossa
# Version 24.34 - 2024-08-20
### Added 🆕
- chore: #6900 order params by:jorgep
- chore: refs #6900 drop console log by:jorgep
- chore: refs #6900 drop vnCurrency by:jorgep
- chore: refs #6900 fix e2e tests by:jorgep
- chore: refs #6900 mv rectificative logic by:jorgep
- chore: refs #6900 responsive code by:jorgep
- chore: refs #7283 drop array types by:jorgep
- chore: refs #7283 drop import by:jorgep
- chore: refs #7283 fix e2e logout by:jorgep
- chore: refs #7283 update VnAvatar title handling by:jorgep
- chore: refs #7323 fix test by:jorgep
- chore: refs #7323 remove unused import by:jorgep
- chore: refs #7323drop commented code by:jorgep
- feat(VnCard): use props searchbar by:alexm
- feat(customer): improve basicData to balance by:alexm
- feat(customer_balance): refs #6943 add functionality from salix by:alexm
- feat(customer_balance): refs #6943 translations by:alexm
- feat: refs #6130 husky commitLint config by:pablone
- feat: refs #6130 husky hooks by:pablone
- feat: refs #6900 add InvoiceInSerial by:jorgep
- feat: refs #6900 add locale by:jorgep
- feat: refs #6900 use VnTable & sort filter fields by:jorgep
- feat: refs #7323 add flex-wrap by:jorgep
- feat: refs #7323 add my account" btn & fix models log selectable by:jorgep
- feat: refs #7323 improve test by:jorgep
### Changed 📦
- refactor(customer_log: use VnLog by:alexm
- refactor(customer_recovery): to vnTable by:alexm
- refactor(customer_webAccess): FormModel by:alexm
- refactor: refs #7283 update avatar size and color by:jorgep
### Fixed 🛠️
- chore: refs #6900 fix e2e tests by:jorgep
- chore: refs #7283 fix e2e logout by:jorgep
- chore: refs #7323 fix test by:jorgep
- feat: refs #7323 add my account" btn & fix models log selectable by:jorgep
- fix #7355 fix acls list by:carlossa
- fix(VnFilterPanel): emit userParams better by:alexm
- fix(claim_summary): url links (HEAD -> 7864_testToMaster_2434, origin/test, origin/7864_testToMaster_2434, test) by:alexm
- fix(customer_sms: fix reload by:alexm
- fix(twoFactor): unify code login and twoFactor by:alexm
- fix: VnCard VnSearchbar props by:alexm
- fix: accountMailAlias by:alexm
- fix: refs #6130 add commit lint modules by:pablone
- fix: refs #6130 pnpm-lock.yml by:pablone
- fix: refs #6900 improve loading by:jorgep
- fix: refs #6900 improve logic (origin/6900-addSerial) by:jorgep
- fix: refs #6900 improve logic by:jorgep
- fix: refs #6900 rectificative btn reactivity by:jorgep
- fix: refs #6900 use type number by:jorgep
- fix: refs #6900 vat & dueday by:jorgep
- fix: refs #6900 vat, dueday & intrastat by:jorgep
- fix: refs #6989 show entity name & default time from config table by:jorgep
- fix: refs #7283 basicData locale by:jorgep
- fix: refs #7283 itemLastEntries filter by:jorgep
- fix: refs #7283 itemTags & VnImg by:jorgep
- fix: refs #7283 locale by:jorgep
- fix: refs #7283 min-width vnImg by:jorgep
- fix: refs #7283 use vnAvatar & add optional zoom by:jorgep
- fix: refs #7283 userPanel pic by:jorgep
- fix: refs #7323 add department popup by:jorgep
- fix: refs #7323 add locale by:jorgep
- fix: refs #7323 css righ menu by:jorgep
- fix: refs #7323 data-key & add select by:jorgep
- fix: refs #7323 load all opts by:jorgep
- fix: refs #7323 righ menu bug by:jorgep
- fix: refs #7323 use global locale by:jorgep
- fix: refs #7323 use workerFilter (origin/7323-warmfix-fixErrors) by:jorgep
- fix: refs #7323 vnsubtoolbar css by:jorgep
- fix: refs #7323 wrong css by:jorgep
- refs #7355 fix Rol, alias by:carlossa
- refs #7355 fix accountAlias by:carlossa
- refs #7355 fix alias summary by:carlossa
- refs #7355 fix conflicts by:carlossa
- refs #7355 fix create Rol by:carlossa
- refs #7355 fix list by:carlossa
- refs #7355 fix lists redirects summary by:carlossa
- refs #7355 fix roles by:carlossa
- refs #7355 fix search exprBuilder by:carlossa
- refs #7355 fix vnTable by:carlossa
# Version 24.32 - 2024-08-06
### Added 🆕
- chore: refs #7197 drop space by:jorgep
- chore: refs #7197 drop useless attr by:jorgep
- chore: refs #7197 fix test by:jorgep
- chore: refs #7197 fix tests by:jorgep
- chore: refs #7197 fix unit tests by:jorgep
- chore: refs #7197 idrop useless class by:jorgep
- chore: refs #7197 improve form filling in Cypress tests by:jorgep
- chore: refs #7197 remove unused import by:jorgep
- feat: customerPayments card view by:alexm
- feat: refs #6943 lock grid mode by:jorgep
- feat: refs #6943 wip consumption filter by:jorgep
- feat: refs #7197 add correcting filter by:jorgep
- feat: refs #7197 add supplier activities filter option by:jorgep
- feat: refs #7197 summary responsive by:jorgep
- feat: refs #7323 fix descriptors, added VnTable and minor changes by:Jon
- feat: refs #7323 fixed tests, changed calendar styles and fix workerCreate by:Jon
- feat: refs #7356 list & weekly to VnTable and style fixes by:Jon
- feat: refs #7401 add menu options by:pablone
- feat: SalesClientTable by:Javier Segarra
- feat: salesOrderTable by:Javier Segarra
- feat: salesTicketTable by:Javier Segarra
- feat: VnTable SalesTicketTable by:Javier Segarra
- fix: columns style by:alexm
### Changed 📦
- perf: LeftMenu show/hide by:Javier Segarra
- perf: refs #7356 TicketList state column by:Jon
- perf: VnFilterPanel (origin/7323_WorkerMigration_End) by:Javier Segarra
- perf: width SalesTicketsTable by:Javier Segarra
- refactor: #6943 wip use vnTable CustomerCredits by:jorgep
- refactor: CustomerNotifications use VnTable by:alexm
- refactor: CustomerPayments use VnTable by:alexm
- refactor: refs #7014 deleted main files and changed route files by:Jon
- refactor: refs #7014 improved route.js & deleted RouteMain by:Jon
- refactor: refs #7014 refactor <module>Main.vue by:Jon
- refactor: refs #7014 refactor ZoneCard, deleted ZoneMain & created basic tests for functionality by:Jon
- refactor: refs #7197 use invoiceInSearchbar & queryParams by:jorgep
- refactor: refs #7323 hidden column filter proposal by:Jon
- refactor: refs #7356 fixed VnTable filters by:Jon
- refactor: refs #7356 requested changes by:Jon
- refactor: wip use vnTable CustomerCredits by:jorgep
### Fixed 🛠️
- chore: refs #7197 fix test by:jorgep
- chore: refs #7197 fix tests by:jorgep
- chore: refs #7197 fix unit tests by:jorgep
- feat: refs #7323 fix descriptors, added VnTable and minor changes by:Jon
- feat: refs #7323 fixed tests, changed calendar styles and fix workerCreate by:Jon
- feat: refs #7356 list & weekly to VnTable and style fixes by:Jon
- fix(claim): small details (6336-claim-v6) by:alexm
- fix: columns style by:alexm
- fix: customer defaulter add amount order (6943-fixCustomer) by:alexm
- fix: customerDefaulter correct functionality by:alexm
- fix: customerNotifications filter by:alexm
- fix: fix conflicts by:Jon
- fix: refs #6101 fix TicketList by:Jon
- fix: refs #6891 worker tests by:jorgep
- fix: refs #6943 drop padding-left checkbox & create wrap mode vnRow by:jorgep
- fix: refs #6943 prevent undefined by:jorgep
- fix: refs #7014 fix tests by:Jon
- fix: refs #7014 fix wagon module by:Jon
- fix: refs #7197 add url InvoiceInSearchbar by:jorgep
- fix: refs #7197 amount reactivity by:jorgep
- fix: refs #7197 drop character by:jorgep
- fix: refs #7197 reactivity invoiceCorrection by:jorgep
- fix: refs #7197 responsive summary layout by:jorgep
- fix: refs #7197 rollback by:jorgep
- fix: refs #7197 rollback crudModel by:jorgep
- fix: refs #7197 setInvoiceInCorrecition by:jorgep
- fix: refs #7197 vat, intrastat, filter and list sections by:jorgep
- fix: refs #7323 fix department & email table filter by:Jon
- fix: refs #7323 fixed left filter by:Jon
- fix: refs #7323 fix workerTimeControl form by:Jon
- fix: refs #7401 fix routeForm by:pablone
- fix: refs #7401 remove console.log by:pablone
- fix: refs CAU 207504 fix itemDiary and logs by:Jon
- fix: workerCreate form street field to be always upperCase by:Jon
- hotfix: refs CAU #207614 fix sale.concept field by:Jon
- refactor: refs #7356 fixed VnTable filters by:Jon
- refs #6898 fix by:carlossa
- Ticket expedition initial load fix by:wbuezas
# Version 24.28 - 2024-07-09
### Added 🆕
- Change header titles style by:wbuezas
- chore: refs #7436 fix e2e (origin/7436-showQCheckbox) by:jorgep
- feat: #7196 eslint (origin/7196-cjsToEsm) by:jgallego
- feat: adapt tu VnTable → CrudModel by:alexm
- feat(CustomerFIlter): use correct table by:alexm
- feat(customerList): add searchbar by:alexm
- feat: customerList is customerExtendedList by:alexm
- feat: fixes #7196 by:jgallego
- feat: refs #6739 transferInvoice new checkbox and functionality by:Jon
- feat: refs #6825 create vnTable and add in CustomerExtendedList by:alexm
- feat: refs #6825 create vnTableColumn, cardActions by:alexm
- feat: refs #6825 fix modes by:alexm
- feat: refs #6825 qchip color by:alexm
- feat: refs #6825 right filter panel (6825-vnTable) by:alexm
- feat: refs #6825 scroll for table mode by:alexm
- feat: refs #6825 share filters, create popup by:alexm
- feat: refs #6825 VnComponent mix component and attrs Form to create new row by:alexm
- feat: refs #6825 VnTableFilter and VnPanelFilter init by:alexm
- feat: refs #6826 added rol summary link by:Jon
- feat: refs #6896 created VnImg and added to order module by:Jon
- feat: refs #6896 new filters by:Jon
- feat: refs #7129 fix some code and add order by:pablone
- feat: refs #7436 show checkbox by:jorgep
- feat: refs #7545 Deleted hasIncoterms client column (origin/7545-hasIncoterms) by:guillermo
- feat(TicketService): use correct format by:alexm
- feat(url): sepate filters by:alexm
- feat(VnFilter): merge objects by:alexm
- feat(VnTable): is-editable and use-model. fix: checkbox by:alexm
- feat(VnTable): refs #6825 actions sticky by:alexm
- feat(VnTable): refs #6825 addInWhere by:alexm
- feat(VnTable): refs #6825 dinamic columns by:alexm
- feat(VnTable): refs #6825 execute function when create by:alexm
- feat(VnTable): refs #6825 fix ellipsis and add titles by:alexm
- feat(VnTable): refs #6825 merge where's by:alexm
- feat(VnTable): refs #6825 move to folder. fix checkboxs by:alexm
- feat(VnTable): refs #6825 remove field prop. Add actions in table by:alexm
- feat(VnTable): refs #6825 use checkbox if startsWith 'is' or 'has' by:alexm
- feat(VnTable): refs #6825 VnTableChip component by:alexm
- feat(vnTable): reload data when change url by:alexm
- feat(WorkerFormation): add columnFilter by:alexm
- feat(WorkerFormation): is-editable and use-model by:alexm
- fix: notify icon style by:Javier Segarra
- refactor: refs #6896 fixed styles by:Jon
- Revert "feat: fixes #7196" by:alexm
- style: refs #6464 changed checkbox and qbtn styles by:Jon
### Changed 📦
- perf: Remove div.col by:Javier Segarra
- perf: remove ItemPicture by:Javier Segarra
- perf: replace ItemPicture in favour of VnImg by:Javier Segarra
- refactor by:wbuezas
- refactor: refs #5447 changed warehouse filter by:Jon
- refactor: refs #5447 changed warehouse out filter behavior by:Jon
- refactor: refs #5447 fixed filter if continent not selected by:Jon
- refactor: refs #5447 fix request by:Jon
- refactor: refs #5447 refactor filters by:Jon
- refactor: refs #6739 changed invoice functions' name by:Jon
- refactor: refs #6739 changed router.push by:Jon
- refactor: refs #6739 deleted useless const by:Jon
- refactor: refs #6739 fix redirect transferInvoice by:Jon
- refactor: refs #6739 new confirmation window by:Jon
- refactor: refs #6739 requested changes by:Jon
- refactor: refs #6739 updated transferInvoice function by:Jon
- refactor: refs #6896 changes requested in PR by:Jon
- refactor: refs #6896 end migration orders by:Jon
- refactor: refs #6896 fixed styles by:Jon
- refactor: refs #6896 fix qdrawer by:Jon
- refactor: refs #6896 refactor VnImg by:Jon
- refactor: refs #6896 requested changes by:Jon
- refactor: refs #6977 fix VnImg props (origin/6977-ClonedURL) by:Jon
- refactor: refs #6977 refactor VnImg by:Jon
- refactor: refs #6977 use VnImg by:Jon
- refactors by:alexm
### Fixed 🛠️
- chore: refs #7436 fix e2e (origin/7436-showQCheckbox) by:jorgep
- feat: fixes #7196 by:jgallego
- feat: refs #6825 fix modes by:alexm
- feat: refs #7129 fix some code and add order by:pablone
- feat(VnTable): is-editable and use-model. fix: checkbox by:alexm
- feat(VnTable): refs #6825 fix ellipsis and add titles by:alexm
- feat(VnTable): refs #6825 move to folder. fix checkboxs by:alexm
- fix(ArrayData): refs #6825 router.replace and use filter.where by:alexm
- fix: bug replace by:alexm
- fix: column hidden v-if by:Javier Segarra
- fix: comment 4 by:Javier Segarra
- fix: comments by:Javier Segarra
- fix: cypress.config to mjs by:alexm
- fix(EntryBuys): fix VnSubtoolbar by:alexm
- fixes: fix vnFilter params and redirect by:alexm
- fix: fix warnings by:alexm
- fix: invoiceDueDay test by:alexm
- fix log view not refreshing when changing id param by:wbuezas
- fix: map selected by:Javier Segarra
- fix: merge dev by:Javier Segarra
- fix: notify icon style by:Javier Segarra
- fix: point 1 by:Javier Segarra
- fix: point 3 by:Javier Segarra
- fix: refs #5447 deleted console.log by:Jon
- fix: refs 6464 deleted useless class in checkbox by:Jon
- fix: refs #6464 fix error isLoading by:Jon
- fix: refs #6739 changed checkbox field by:Jon
- fix: refs #6825 css by:carlossa
- fix: refs #6826 fix redirect by:Jon
- fix: refs #6826 fix roleDescriptor by:Jon
- fix: refs #7129 fix e2e by:pablone
- fix: refs #7129 fix module routes by:pablone
- fix: refs #7129 fix some issues on load and tools by:pablone
- fix: refs #7129 remove consoleLog by:pablone
- fix: refs #7129 remove fix from claim lines by:pablone
- fix: refs #7274 fix duplicate rows by:jorgep
- fix: refs #7433 skeleton by:jorgep
- fix: refs #7623 bugs & tests by:jorgep
- fix: refs #7623 disable router update by:jorgep
- fix: refs #7623 redirect by:jorgep
- fix: refs #7623 test by:jorgep
- fix: refs #7623 update add updateRoute prop in VnPaginate by:jorgep
- fix: refs #7623 updating skip param by:jorgep
- fix: revert cypress mjs by:alexm
- fix: SkeletonTable by:alexm
- fix: state translations by:Javier Segarra
- fix: ticket order by:Javier Segarra
- fix(ticket router): typo by:alexm
- fix(TicketService): pay use selected by:alexm
- fix: TravelLog by:Javier Segarra
- fix(url): filter by:alexm
- fix(url): redirect by:alexm
- fix(VnFilter): filter with params by:alexm
- fix(VnFilterPanel): remove key by:alexm
- fix(VnTable): create scss by:alexm
- fix(VnTable): duplicate fetch by:alexm
- fix(VnTable): Qtable v-bind by:alexm
- fix(VnTable): refs #6825 checkbox align and color by:alexm
- fix(VnTable): refs #6825 fix click sticky column by:alexm
- fix(VnTable): refs #6825 fix events and css by:alexm
- fix(VnTable): refs #6825 VnInputDate by:alexm
- fix(VnTable): showLabel by:alexm
- fix(VnTable): warns by:alexm
- fix: WorkerNotificationsManager test by:alexm
- fix: WorkerSelect option format by:Javier Segarra
- refactor: refs #5447 fixed filter if continent not selected by:Jon
- refactor: refs #5447 fix request by:Jon
- refactor: refs #6739 fix redirect transferInvoice by:Jon
- refactor: refs #6896 fixed styles by:Jon
- refactor: refs #6896 fix qdrawer by:Jon
- refactor: refs #6977 fix VnImg props (origin/6977-ClonedURL) by:Jon
- refs #6504 fix formModel claimFilter claimCard (origin/6504-fixCardClaim) by:carlossa
- refs #7406 fix components by:carlossa
- refs #7406 fix pr by:carlossa
- refs #7406 fix props by:carlossa
- refs #7406 fix Tb components create by:carlossa
- refs #7406 fix trad by:carlossa
- refs #7406 fix url by:carlossa
- refs #7406 fix VnTable columns by:carlossa
- refs #7409 fix balance and formation by:carlossa
- refs #7409 fix trad by:carlossa
- Revert "feat: fixes #7196" by:alexm
- test: fix intermitent e2e by:alexm
- test: fix vnSearchbar adapt to vnTable (origin/7648_dev_customerEntries) by:alexm
# Version 24.24 - 2024-06-11 # Version 24.24 - 2024-06-11
### Added 🆕 ### Added 🆕

1
commitlint.config.js Normal file
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.28.1", "version": "24.36.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -13,7 +13,10 @@
"test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run", "test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest", "test:unit": "vitest",
"test:unit:ci": "vitest run" "test:unit:ci": "vitest run",
"commitlint": "commitlint --edit",
"prepare": "npx husky install",
"addReferenceTag": "node .husky/addReferenceTag.js"
}, },
"dependencies": { "dependencies": {
"@quasar/cli": "^2.3.0", "@quasar/cli": "^2.3.0",
@ -29,6 +32,8 @@
"vue-router": "^4.2.1" "vue-router": "^4.2.1"
}, },
"devDependencies": { "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.1",
"@pinia/testing": "^0.1.2", "@pinia/testing": "^0.1.2",
"@quasar/app-vite": "^1.7.3", "@quasar/app-vite": "^1.7.3",
@ -41,6 +46,7 @@
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-cypress": "^2.13.3", "eslint-plugin-cypress": "^2.13.3",
"eslint-plugin-vue": "^9.14.1", "eslint-plugin-vue": "^9.14.1",
"husky": "^8.0.0",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"vitest": "^0.31.1" "vitest": "^0.31.1"

File diff suppressed because it is too large Load Diff

BIN
public/no-image-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
public/no-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/no-user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -43,6 +43,15 @@ const onResponseError = (error) => {
} }
switch (response?.status) { switch (response?.status) {
case 422:
if (error.name == 'ValidationError')
message +=
' "' +
responseError.details.context +
'.' +
Object.keys(responseError.details.codes).join(',') +
'"';
break;
case 500: case 500:
message = 'errors.statusInternalServerError'; message = 'errors.statusInternalServerError';
break; break;
@ -55,10 +64,10 @@ const onResponseError = (error) => {
} }
if (session.isLoggedIn() && response?.status === 401) { if (session.isLoggedIn() && response?.status === 401) {
session.destroy(); session.destroy(false);
const hash = window.location.hash; const hash = window.location.hash;
const url = hash.slice(1); const url = hash.slice(1);
Router.push({ path: url }); Router.push(`/login?redirect=${url}`);
} else if (!session.isLoggedIn()) { } else if (!session.isLoggedIn()) {
return Promise.reject(error); return Promise.reject(error);
} }

View File

@ -0,0 +1,23 @@
import routes from 'src/router/modules';
import { useRouter } from 'vue-router';
export default {
mounted: function () {
const router = useRouter();
const keyBindingMap = routes
.filter((route) => route.meta.keyBinding)
.reduce((map, route) => {
map[route.meta.keyBinding.toLowerCase()] = route.path;
return map;
}, {});
const handleKeyDown = (event) => {
const { ctrlKey, altKey, key } = event;
event.preventDefault();
if (ctrlKey && altKey && keyBindingMap[key]) {
router.push(keyBindingMap[key]);
}
};
window.addEventListener('keydown', handleKeyDown);
},
};

View File

@ -1,28 +1,11 @@
import { getCurrentInstance } from 'vue'; import { getCurrentInstance } from 'vue';
const filterAvailableInput = (element) => {
return element.classList.contains('q-field__native') && !element.disabled;
};
const filterAvailableText = (element) => {
return (
element.__vueParentComponent.type.name === 'QInput' &&
element.__vueParentComponent?.attrs?.class !== 'vn-input-date'
);
};
export default { export default {
mounted: function () { mounted: function () {
const vm = getCurrentInstance(); const vm = getCurrentInstance();
if (vm.type.name === 'QForm') { if (vm.type.name === 'QForm') {
if (!['searchbarForm', 'filterPanelForm'].includes(this.$el?.id)) { if (!['searchbarForm', 'filterPanelForm'].includes(this.$el?.id)) {
// AUTOFOCUS // TODO: AUTOFOCUS IS NOT FOCUSING
const elementsArray = Array.from(this.$el.elements);
const availableInputs = elementsArray.filter(filterAvailableInput);
const firstInputElement = availableInputs.find(filterAvailableText);
if (firstInputElement) {
firstInputElement.focus();
}
const that = this; const that = this;
this.$el.addEventListener('keyup', function (evt) { this.$el.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter') { if (evt.key === 'Enter') {

View File

@ -1,6 +1,8 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import qFormMixin from './qformMixin'; import qFormMixin from './qformMixin';
import mainShortcutMixin from './mainShortcutMixin';
export default boot(({ app }) => { export default boot(({ app }) => {
app.mixin(qFormMixin); app.mixin(qFormMixin);
app.mixin(mainShortcutMixin);
}); });

View File

@ -52,7 +52,7 @@ onMounted(async () => {
@on-data-saved="onDataSaved" @on-data-saved="onDataSaved"
> >
<template #form-inputs="{ data, validate }"> <template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnInput <VnInput
:label="t('name')" :label="t('name')"
v-model="data.name" v-model="data.name"
@ -67,7 +67,7 @@ onMounted(async () => {
:rules="validate('bankEntity.bic')" :rules="validate('bankEntity.bic')"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<div class="col"> <div class="col">
<VnSelect <VnSelect
:label="t('country')" :label="t('country')"

View File

@ -46,22 +46,6 @@ const onDataSaved = async (formData, requestResponse) => {
@on-fetch="(data) => (taxAreasOptions = data)" @on-fetch="(data) => (taxAreasOptions = data)"
auto-load auto-load
/> />
<FetchData
url="Tickets"
:filter="{
fields: ['id', 'nickname'],
where: { refFk: null },
order: 'shipped DESC',
}"
@on-fetch="(data) => (ticketsOptions = data)"
auto-load
/>
<FetchData
url="Clients"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
@on-fetch="(data) => (clientsOptions = data)"
auto-load
/>
<FormModelPopup <FormModelPopup
ref="formModelPopupRef" ref="formModelPopupRef"
:title="t('Create manual invoice')" :title="t('Create manual invoice')"
@ -75,7 +59,7 @@ const onDataSaved = async (formData, requestResponse) => {
<QIcon name="warning" class="fill-icon q-mr-sm" size="md" /> <QIcon name="warning" class="fill-icon q-mr-sm" size="md" />
{{ t('Invoicing in progress...') }} {{ t('Invoicing in progress...') }}
</span> </span>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnSelect <VnSelect
:label="t('Ticket')" :label="t('Ticket')"
:options="ticketsOptions" :options="ticketsOptions"
@ -84,6 +68,10 @@ const onDataSaved = async (formData, requestResponse) => {
option-value="id" option-value="id"
v-model="data.ticketFk" v-model="data.ticketFk"
@update:model-value="data.clientFk = null" @update:model-value="data.clientFk = null"
url="Tickets"
:where="{ refFk: null }"
:fields="['id', 'nickname']"
:filter-options="{ order: 'shipped DESC' }"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -105,10 +93,13 @@ const onDataSaved = async (formData, requestResponse) => {
option-value="id" option-value="id"
v-model="data.clientFk" v-model="data.clientFk"
@update:model-value="data.ticketFk = null" @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" /> <VnInputDate :label="t('Max date')" v-model="data.maxShipped" />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnSelect <VnSelect
:label="t('Serial')" :label="t('Serial')"
:options="invoiceOutSerialsOptions" :options="invoiceOutSerialsOptions"
@ -116,7 +107,6 @@ const onDataSaved = async (formData, requestResponse) => {
option-label="description" option-label="description"
option-value="code" option-value="code"
v-model="data.serial" v-model="data.serial"
:required="true"
/> />
<VnSelect <VnSelect
:label="t('Area')" :label="t('Area')"
@ -125,10 +115,9 @@ const onDataSaved = async (formData, requestResponse) => {
option-label="code" option-label="code"
option-value="code" option-value="code"
v-model="data.taxArea" v-model="data.taxArea"
:required="true"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnInput <VnInput
:label="t('Reference')" :label="t('Reference')"
type="textarea" type="textarea"

View File

@ -4,8 +4,8 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectProvince from 'components/VnSelectProvince.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import FormModelPopup from './FormModelPopup.vue'; import FormModelPopup from './FormModelPopup.vue';
const emit = defineEmits(['onDataSaved']); const emit = defineEmits(['onDataSaved']);
@ -19,8 +19,8 @@ const cityFormData = reactive({
const provincesOptions = ref([]); const provincesOptions = ref([]);
const onDataSaved = (dataSaved) => { const onDataSaved = (...args) => {
emit('onDataSaved', dataSaved); emit('onDataSaved', ...args);
}; };
</script> </script>
@ -36,24 +36,16 @@ const onDataSaved = (dataSaved) => {
:form-initial-data="cityFormData" :form-initial-data="cityFormData"
url-create="towns" url-create="towns"
model="city" model="city"
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved"
> >
<template #form-inputs="{ data, validate }"> <template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnInput <VnInput
:label="t('Name')" :label="t('Name')"
v-model="data.name" v-model="data.name"
:rules="validate('city.name')" :rules="validate('city.name')"
/> />
<VnSelect <VnSelectProvince v-model="data.provinceFk" />
:label="t('Province')"
:options="provincesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
:rules="validate('city.provinceFk')"
/>
</VnRow> </VnRow>
</template> </template>
</FormModelPopup> </FormModelPopup>

View File

@ -5,9 +5,9 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectProvince from 'src/components/VnSelectProvince.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import CreateNewCityForm from './CreateNewCityForm.vue'; import CreateNewCityForm from './CreateNewCityForm.vue';
import CreateNewProvinceForm from './CreateNewProvinceForm.vue';
import VnSelectDialog from 'components/common/VnSelectDialog.vue'; import VnSelectDialog from 'components/common/VnSelectDialog.vue';
import FormModelPopup from './FormModelPopup.vue'; import FormModelPopup from './FormModelPopup.vue';
@ -22,20 +22,17 @@ const postcodeFormData = reactive({
townFk: null, townFk: null,
}); });
const townsFetchDataRef = ref(null);
const provincesFetchDataRef = ref(null); const provincesFetchDataRef = ref(null);
const countriesOptions = ref([]); const countriesOptions = ref([]);
const provincesOptions = ref([]); const provincesOptions = ref([]);
const townsLocationOptions = ref([]); const town = ref({});
const onDataSaved = (formData) => { function onDataSaved(formData) {
const newPostcode = { const newPostcode = {
...formData, ...formData,
}; };
const townObject = townsLocationOptions.value.find( newPostcode.town = town.value.name;
({ id }) => id === formData.townFk newPostcode.townFk = town.value.id;
);
newPostcode.town = townObject?.name;
const provinceObject = provincesOptions.value.find( const provinceObject = provincesOptions.value.find(
({ id }) => id === formData.provinceFk ({ id }) => id === formData.provinceFk
); );
@ -43,39 +40,41 @@ const onDataSaved = (formData) => {
const countryObject = countriesOptions.value.find( const countryObject = countriesOptions.value.find(
({ id }) => id === formData.countryFk ({ id }) => id === formData.countryFk
); );
newPostcode.country = countryObject?.country; newPostcode.country = countryObject?.name;
emit('onDataSaved', newPostcode); emit('onDataSaved', newPostcode);
}; }
const onCityCreated = async ({ name, provinceFk }, formData) => { async function onCityCreated(newTown, formData) {
await townsFetchDataRef.value.fetch();
formData.townFk = townsLocationOptions.value.find((town) => town.name === name).id;
formData.provinceFk = provinceFk;
formData.countryFk = provincesOptions.value.find(
(province) => province.id === provinceFk
).countryFk;
};
const onProvinceCreated = async ({ name }, formData) => {
await provincesFetchDataRef.value.fetch(); await provincesFetchDataRef.value.fetch();
formData.provinceFk = provincesOptions.value.find( newTown.province = provincesOptions.value.find(
(province) => province.name === name (province) => province.id === newTown.provinceFk
).id; );
}; 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 setProvince(id, data) {
await provincesFetchDataRef.value.fetch();
const newProvince = provincesOptions.value.find((province) => province.id == id);
if (!newProvince) return;
data.countryFk = newProvince.countryFk;
}
</script> </script>
<template> <template>
<FetchData
ref="townsFetchDataRef"
@on-fetch="(data) => (townsLocationOptions = data)"
auto-load
url="Towns/location"
/>
<FetchData <FetchData
ref="provincesFetchDataRef" ref="provincesFetchDataRef"
@on-fetch="(data) => (provincesOptions = data)" @on-fetch="(data) => (provincesOptions = data)"
auto-load auto-load
url="Provinces" url="Provinces/location"
/> />
<FetchData <FetchData
@on-fetch="(data) => (countriesOptions = data)" @on-fetch="(data) => (countriesOptions = data)"
@ -88,10 +87,11 @@ const onProvinceCreated = async ({ name }, formData) => {
:title="t('New postcode')" :title="t('New postcode')"
:subtitle="t('Please, ensure you put the correct data!')" :subtitle="t('Please, ensure you put the correct data!')"
:form-initial-data="postcodeFormData" :form-initial-data="postcodeFormData"
:mapper="(data) => (data.townFk = data.townFk.id) && data"
@on-data-saved="onDataSaved" @on-data-saved="onDataSaved"
> >
<template #form-inputs="{ data, validate }"> <template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnInput <VnInput
:label="t('Postcode')" :label="t('Postcode')"
v-model="data.code" v-model="data.code"
@ -99,38 +99,43 @@ const onProvinceCreated = async ({ name }, formData) => {
/> />
<VnSelectDialog <VnSelectDialog
:label="t('City')" :label="t('City')"
:options="townsLocationOptions" url="Towns/location"
@update:model-value="(value) => setTown(value, data)"
v-model="data.townFk" v-model="data.townFk"
hide-selected
option-label="name" option-label="name"
option-value="id" option-value="id"
:rules="validate('postcode.city')" :rules="validate('postcode.city')"
:roles-allowed-to-create="['deliveryAssistant']" :roles-allowed-to-create="['deliveryAssistant']"
:emit-value="false"
clearable
> >
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.province.name }},
{{ opt.province.country.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
<template #form> <template #form>
<CreateNewCityForm @on-data-saved="onCityCreated($event, data)" /> <CreateNewCityForm
@on-data-saved="
(_, requestResponse) =>
onCityCreated(requestResponse, data)
"
/>
</template> </template>
</VnSelectDialog> </VnSelectDialog>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-xl"> <VnRow>
<VnSelectDialog <VnSelectProvince
:label="t('Province')" @update:model-value="(value) => setProvince(value, data)"
:options="provincesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk" v-model="data.provinceFk"
:rules="validate('postcode.provinceFk')"
:roles-allowed-to-create="['deliveryAssistant']"
>
<template #form>
<CreateNewProvinceForm
@on-data-saved="onProvinceCreated($event, data)"
/> />
</template> </VnSelectDialog <VnSelect
></VnRow>
<VnRow class="row q-gutter-md q-mb-xl"
><VnSelect
:label="t('Country')" :label="t('Country')"
:options="countriesOptions" :options="countriesOptions"
hide-selected hide-selected

View File

@ -19,8 +19,11 @@ const provinceFormData = reactive({
const autonomiesOptions = ref([]); const autonomiesOptions = ref([]);
const onDataSaved = (dataSaved) => { const onDataSaved = (dataSaved, requestResponse) => {
emit('onDataSaved', dataSaved); requestResponse.autonomy = autonomiesOptions.value.find(
(autonomy) => autonomy.id == requestResponse.autonomyFk
);
emit('onDataSaved', dataSaved, requestResponse);
}; };
</script> </script>
@ -28,7 +31,7 @@ const onDataSaved = (dataSaved) => {
<FetchData <FetchData
@on-fetch="(data) => (autonomiesOptions = data)" @on-fetch="(data) => (autonomiesOptions = data)"
auto-load auto-load
url="Autonomies" url="Autonomies/location"
/> />
<FormModelPopup <FormModelPopup
:title="t('New province')" :title="t('New province')"
@ -36,10 +39,10 @@ const onDataSaved = (dataSaved) => {
url-create="provinces" url-create="provinces"
model="province" model="province"
:form-initial-data="provinceFormData" :form-initial-data="provinceFormData"
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved"
> >
<template #form-inputs="{ data, validate }"> <template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnInput <VnInput
:label="t('Name')" :label="t('Name')"
v-model="data.name" v-model="data.name"
@ -53,7 +56,16 @@ const onDataSaved = (dataSaved) => {
option-value="id" option-value="id"
v-model="data.autonomyFk" v-model="data.autonomyFk"
:rules="validate('province.autonomyFk')" :rules="validate('province.autonomyFk')"
/> >
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption> {{ opt.country.name }} </QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow> </VnRow>
</template> </template>
</FormModelPopup> </FormModelPopup>

View File

@ -53,7 +53,7 @@ const onDataSaved = (dataSaved) => {
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved($event)"
> >
<template #form-inputs="{ data, validate }"> <template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnInput <VnInput
:label="t('Identifier')" :label="t('Identifier')"
v-model="data.thermographId" v-model="data.thermographId"

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
@ -67,7 +67,7 @@ const $props = defineProps({
default: '', default: '',
description: 'It is used for redirect on click "save and continue"', description: 'It is used for redirect on click "save and continue"',
}, },
hasSubtoolbar: { hasSubToolbar: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
@ -97,20 +97,31 @@ defineExpose({
vnPaginateRef, vnPaginateRef,
}); });
onBeforeRouteLeave((to, from, next) => {
if (hasChanges.value)
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.unsavedPopup.title'),
message: t('globals.unsavedPopup.subtitle'),
promise: () => next(),
},
});
else next();
});
async function fetch(data) { async function fetch(data) {
if (data && Array.isArray(data)) {
let $index = 0;
data.map((d) => (d.$index = $index++));
}
resetData(data); resetData(data);
emit('onFetch', data); emit('onFetch', data);
return data; return data;
} }
function resetData(data) { function resetData(data) {
if (!data) return; if (!data) return;
if (data && Array.isArray(data)) {
let $index = 0;
data.map((d) => (d.$index = $index++));
}
originalData.value = JSON.parse(JSON.stringify(data)); originalData.value = JSON.parse(JSON.stringify(data));
formData.value = JSON.parse(JSON.stringify(data)); formData.value = JSON.parse(JSON.stringify(data));
@ -299,7 +310,7 @@ watch(formUrl, async () => {
:url="url" :url="url"
:limit="limit" :limit="limit"
@on-fetch="fetch" @on-fetch="fetch"
@on-change="resetData" @on-change="fetch"
:skeleton="false" :skeleton="false"
ref="vnPaginateRef" ref="vnPaginateRef"
v-bind="$attrs" v-bind="$attrs"
@ -313,8 +324,11 @@ watch(formUrl, async () => {
></slot> ></slot>
</template> </template>
</VnPaginate> </VnPaginate>
<SkeletonTable v-if="!formData" :columns="$attrs.columns?.length" /> <SkeletonTable
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubtoolbar"> v-if="!formData && $attrs.autoLoad"
:columns="$attrs.columns?.length"
/>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubToolbar">
<QBtnGroup push style="column-gap: 10px"> <QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" /> <slot name="moreBeforeActions" />
<QBtn <QBtn

View File

@ -245,14 +245,14 @@ const makeRequest = async () => {
</div> </div>
<div class="column"> <div class="column">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<QOptionGroup <QOptionGroup
:options="uploadMethodsOptions" :options="uploadMethodsOptions"
type="radio" type="radio"
v-model="uploadMethodSelected" v-model="uploadMethodSelected"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<QFile <QFile
v-if="uploadMethodSelected === 'computer'" v-if="uploadMethodSelected === 'computer'"
ref="inputFileRef" ref="inputFileRef"
@ -287,7 +287,7 @@ const makeRequest = async () => {
placeholder="https://" placeholder="https://"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnSelect <VnSelect
:label="t('Orientation')" :label="t('Orientation')"
:options="viewportTypes" :options="viewportTypes"

View File

@ -82,7 +82,7 @@ const closeForm = () => {
<span class="title">{{ t('Edit') }}</span> <span class="title">{{ t('Edit') }}</span>
<span class="countLines">{{ ` ${rows.length} ` }}</span> <span class="countLines">{{ ` ${rows.length} ` }}</span>
<span class="title">{{ t('buy(s)') }}</span> <span class="title">{{ t('buy(s)') }}</span>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnSelect <VnSelect
:label="t('Field to edit')" :label="t('Field to edit')"
:options="fieldsOptions" :options="fieldsOptions"

View File

@ -151,7 +151,7 @@ const selectItem = ({ id }) => {
<QIcon name="close" size="sm" /> <QIcon name="close" size="sm" />
</span> </span>
<h1 class="title">{{ t('Filter item') }}</h1> <h1 class="title">{{ t('Filter item') }}</h1>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnInput :label="t('entry.buys.name')" v-model="itemFilterParams.name" /> <VnInput :label="t('entry.buys.name')" v-model="itemFilterParams.name" />
<VnInput :label="t('entry.buys.size')" v-model="itemFilterParams.size" /> <VnInput :label="t('entry.buys.size')" v-model="itemFilterParams.size" />
<VnSelect <VnSelect

View File

@ -144,7 +144,7 @@ const selectTravel = ({ id }) => {
<QIcon name="close" size="sm" /> <QIcon name="close" size="sm" />
</span> </span>
<h1 class="title">{{ t('Filter travels') }}</h1> <h1 class="title">{{ t('Filter travels') }}</h1>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnSelect <VnSelect
:label="t('entry.basicData.agency')" :label="t('entry.basicData.agency')"
:options="agenciesOptions" :options="agenciesOptions"

View File

@ -22,7 +22,7 @@ const { t } = useI18n();
const { validate } = useValidator(); const { validate } = useValidator();
const { notify } = useNotify(); const { notify } = useNotify();
const route = useRoute(); const route = useRoute();
const myForm = ref(null);
const $props = defineProps({ const $props = defineProps({
url: { url: {
type: String, type: String,
@ -87,6 +87,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
defaultTrim: {
type: Boolean,
default: true,
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed( const modelValue = computed(
@ -100,17 +104,19 @@ const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges); const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({}); const originalData = ref({});
const formData = computed(() => state.get(modelValue)); const formData = computed(() => state.get(modelValue));
const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({ const defaultButtons = computed(() => ({
save: { save: {
color: 'primary', color: 'primary',
icon: 'save', icon: 'save',
label: 'globals.save', label: 'globals.save',
click: () => myForm.value.submit(),
type: 'submit',
}, },
reset: { reset: {
color: 'primary', color: 'primary',
icon: 'restart_alt', icon: 'restart_alt',
label: 'globals.reset', label: 'globals.reset',
click: () => reset(),
}, },
...$props.defaultButtons, ...$props.defaultButtons,
})); }));
@ -148,19 +154,22 @@ if (!$props.url)
(val) => updateAndEmit('onFetch', val) (val) => updateAndEmit('onFetch', val)
); );
watch(formUrl, async () => { watch(
() => [$props.url, $props.filter],
async () => {
originalData.value = null; originalData.value = null;
reset(); reset();
await fetch(); await fetch();
}); }
);
onBeforeRouteLeave((to, from, next) => { onBeforeRouteLeave((to, from, next) => {
if (hasChanges.value && $props.observeFormChanges) if (hasChanges.value && $props.observeFormChanges)
quasar.dialog({ quasar.dialog({
component: VnConfirm, component: VnConfirm,
componentProps: { componentProps: {
title: t('Unsaved changes will be lost'), title: t('globals.unsavedPopup.title'),
message: t('Are you sure exit without saving?'), message: t('globals.unsavedPopup.subtitle'),
promise: () => next(), promise: () => next(),
}, },
}); });
@ -193,6 +202,7 @@ async function save() {
isLoading.value = true; isLoading.value = true;
try { try {
formData.value = trimData(formData.value);
const body = $props.mapper ? $props.mapper(formData.value) : formData.value; const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
const method = $props.urlCreate ? 'post' : 'patch'; const method = $props.urlCreate ? 'post' : 'patch';
const url = const url =
@ -206,11 +216,11 @@ async function save() {
updateAndEmit('onDataSaved', formData.value, response?.data); updateAndEmit('onDataSaved', formData.value, response?.data);
if ($props.reload) await arrayData.fetch({}); if ($props.reload) await arrayData.fetch({});
hasChanges.value = false;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
notify('errors.writeRequest', 'negative'); notify('errors.writeRequest', 'negative');
} finally { } finally {
hasChanges.value = false;
isLoading.value = false; isLoading.value = false;
} }
} }
@ -251,6 +261,14 @@ function updateAndEmit(evt, val, res) {
emit(evt, state.get(modelValue), res); emit(evt, state.get(modelValue), res);
} }
function trimData(data) {
if (!$props.defaultTrim) return data;
for (const key in data) {
if (typeof data[key] == 'string') data[key] = data[key].trim();
}
return data;
}
defineExpose({ defineExpose({
save, save,
isLoading, isLoading,
@ -262,6 +280,7 @@ defineExpose({
<template> <template>
<div class="column items-center full-width"> <div class="column items-center full-width">
<QForm <QForm
ref="myForm"
v-if="formData" v-if="formData"
@submit="save" @submit="save"
@reset="reset" @reset="reset"
@ -270,19 +289,24 @@ defineExpose({
> >
<QCard> <QCard>
<slot <slot
v-if="formData"
name="form" name="form"
:data="formData" :data="formData"
:validate="validate" :validate="validate"
:filter="filter" :filter="filter"
/> />
<SkeletonForm v-else />
</QCard> </QCard>
</QForm> </QForm>
</div> </div>
<Teleport <Teleport
to="#st-actions" to="#st-actions"
v-if="stateStore?.isSubToolbarShown() && componentIsRendered" v-if="
$props.defaultActions &&
stateStore?.isSubToolbarShown() &&
componentIsRendered
"
> >
<div v-if="$props.defaultActions">
<QBtnGroup push class="q-gutter-x-sm"> <QBtnGroup push class="q-gutter-x-sm">
<slot name="moreActions" /> <slot name="moreActions" />
<QBtn <QBtn
@ -290,7 +314,7 @@ defineExpose({
:color="defaultButtons.reset.color" :color="defaultButtons.reset.color"
:icon="defaultButtons.reset.icon" :icon="defaultButtons.reset.icon"
flat flat
@click="reset" @click="defaultButtons.reset.click"
:disable="!hasChanges" :disable="!hasChanges"
:title="t(defaultButtons.reset.label)" :title="t(defaultButtons.reset.label)"
/> />
@ -330,14 +354,13 @@ defineExpose({
:label="tMobile('globals.save')" :label="tMobile('globals.save')"
color="primary" color="primary"
icon="save" icon="save"
@click="save" @click="defaultButtons.save.click"
:disable="!hasChanges" :disable="!hasChanges"
:title="t(defaultButtons.save.label)" :title="t(defaultButtons.save.label)"
/> />
</QBtnGroup> </QBtnGroup>
</div>
</Teleport> </Teleport>
<SkeletonForm v-if="!formData" />
<QInnerLoading <QInnerLoading
:showing="isLoading" :showing="isLoading"
:label="t('globals.pleaseWait')" :label="t('globals.pleaseWait')"
@ -358,8 +381,3 @@ defineExpose({
padding: 32px; padding: 32px;
} }
</style> </style>
<i18n>
es:
Unsaved changes will be lost: Los cambios que no haya guardado se perderán
Are you sure exit without saving?: ¿Seguro que quiere salir sin guardar?
</i18n>

View File

@ -23,18 +23,15 @@ const formModelRef = ref(null);
const closeButton = ref(null); const closeButton = ref(null);
const onDataSaved = (formData, requestResponse) => { const onDataSaved = (formData, requestResponse) => {
closeForm(); if (closeButton.value) closeButton.value.click();
emit('onDataSaved', formData, requestResponse); emit('onDataSaved', formData, requestResponse);
}; };
const isLoading = computed(() => formModelRef.value?.isLoading); const isLoading = computed(() => formModelRef.value?.isLoading);
const closeForm = async () => {
if (closeButton.value) closeButton.value.click();
};
defineExpose({ defineExpose({
isLoading, isLoading,
onDataSaved,
}); });
</script> </script>

View File

@ -112,6 +112,7 @@ const getCategoryClass = (category, params) => {
const getSelectedTagValues = async (tag) => { const getSelectedTagValues = async (tag) => {
try { try {
if (!tag?.selectedTag?.id) return;
tag.value = null; tag.value = null;
const filter = { const filter = {
fields: ['value'], fields: ['value'],

View File

@ -58,6 +58,7 @@ function addChildren(module, route, parent) {
} }
const items = ref([]); const items = ref([]);
function getRoutes() { function getRoutes() {
if (props.source === 'main') { if (props.source === 'main') {
const modules = Object.assign([], navigation.getModules().value); const modules = Object.assign([], navigation.getModules().value);
@ -66,9 +67,8 @@ function getRoutes() {
const moduleDef = routes.find( const moduleDef = routes.find(
(route) => toLowerCamel(route.name) === item.module (route) => toLowerCamel(route.name) === item.module
); );
item.children = [];
if (!moduleDef) continue; if (!moduleDef) continue;
item.children = [];
addChildren(item.module, moduleDef, item.children); addChildren(item.module, moduleDef, item.children);
} }

View File

@ -21,7 +21,7 @@ const itemComputed = computed(() => {
</script> </script>
<template> <template>
<QItem <QItem
active-class="bg-hover" active-class="bg-vn-hover"
class="min-height" class="min-height"
:to="{ name: itemComputed.name }" :to="{ name: itemComputed.name }"
clickable clickable

View File

@ -1,21 +1,19 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import PinnedModules from './PinnedModules.vue'; import PinnedModules from './PinnedModules.vue';
import UserPanel from 'components/UserPanel.vue'; import UserPanel from 'components/UserPanel.vue';
import VnBreadcrumbs from './common/VnBreadcrumbs.vue'; import VnBreadcrumbs from './common/VnBreadcrumbs.vue';
import VnAvatar from './ui/VnAvatar.vue';
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
const quasar = useQuasar(); const quasar = useQuasar();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const appName = 'Lilium'; const appName = 'Lilium';
onMounted(() => stateStore.setMounted()); onMounted(() => stateStore.setMounted());
@ -74,21 +72,13 @@ const pinnedModulesRef = ref();
</QTooltip> </QTooltip>
<PinnedModules ref="pinnedModulesRef" /> <PinnedModules ref="pinnedModulesRef" />
</QBtn> </QBtn>
<QBtn <QBtn class="q-pa-none" rounded dense flat no-wrap id="user">
:class="{ 'q-pa-none': quasar.platform.is.mobile }" <VnAvatar
rounded :worker-id="user.id"
dense :title="user.name"
flat size="lg"
no-wrap color="transparent"
id="user" />
>
<QAvatar size="lg">
<QImg
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="primary"
>
</QImg>
</QAvatar>
<QTooltip bottom> <QTooltip bottom>
{{ t('globals.userPanel') }} {{ t('globals.userPanel') }}
</QTooltip> </QTooltip>

View File

@ -15,7 +15,7 @@ const props = defineProps({
default: null, default: null,
}, },
warehouseFk: { warehouseFk: {
type: Boolean, type: Number,
default: null, default: null,
}, },
}); });
@ -23,7 +23,7 @@ const props = defineProps({
const { t } = useI18n(); const { t } = useI18n();
const regularizeFormData = reactive({ const regularizeFormData = reactive({
itemFk: props.itemFk, itemFk: Number(props.itemFk),
warehouseFk: props.warehouseFk, warehouseFk: props.warehouseFk,
quantity: null, quantity: null,
}); });
@ -49,18 +49,19 @@ const onDataSaved = (data) => {
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved($event)"
> >
<template #form-inputs="{ data }"> <template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<QInput <QInput
:label="t('Type the visible quantity')" :label="t('Type the visible quantity')"
v-model.number="data.quantity" v-model.number="data.quantity"
type="number"
autofocus autofocus
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<div class="col"> <div class="col">
<VnSelect <VnSelect
:label="t('Warehouse')" :label="t('Warehouse')"
v-model="data.warehouseFk" v-model.number="data.warehouseFk"
:options="warehousesOptions" :options="warehousesOptions"
option-value="id" option-value="id"
option-label="name" option-label="name"

View File

@ -2,12 +2,13 @@
import { ref, reactive } from 'vue'; import { ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue'; import FormPopup from './FormPopup.vue';
import { useDialogPluginComponent } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
@ -17,34 +18,66 @@ const $props = defineProps({
default: () => {}, default: () => {},
}, },
}); });
const { dialogRef } = useDialogPluginComponent();
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
const checked = ref(true);
const transferInvoiceParams = reactive({ const transferInvoiceParams = reactive({
id: $props.invoiceOutData?.id, id: $props.invoiceOutData?.id,
refFk: $props.invoiceOutData?.ref, refFk: $props.invoiceOutData?.ref,
}); });
const closeButton = ref(null);
const clientsOptions = ref([]);
const rectificativeTypeOptions = ref([]); const rectificativeTypeOptions = ref([]);
const siiTypeInvoiceOutsOptions = ref([]); const siiTypeInvoiceOutsOptions = ref([]);
const invoiceCorrectionTypesOptions = ref([]); const invoiceCorrectionTypesOptions = ref([]);
const closeForm = () => { const selectedClient = (client) => {
if (closeButton.value) closeButton.value.click(); transferInvoiceParams.selectedClientData = client;
};
const makeInvoice = async () => {
const hasToInvoiceByAddress =
transferInvoiceParams.selectedClientData.hasToInvoiceByAddress;
const params = {
id: transferInvoiceParams.id,
cplusRectificationTypeFk: transferInvoiceParams.cplusRectificationTypeFk,
invoiceCorrectionTypeFk: transferInvoiceParams.invoiceCorrectionTypeFk,
newClientFk: transferInvoiceParams.newClientFk,
refFk: transferInvoiceParams.refFk,
siiTypeInvoiceOutFk: transferInvoiceParams.siiTypeInvoiceOutFk,
makeInvoice: checked.value,
}; };
const transferInvoice = async () => {
try { try {
const { data } = await axios.post( if (checked.value && hasToInvoiceByAddress) {
'InvoiceOuts/transferInvoice', const response = await new Promise((resolve) => {
transferInvoiceParams 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/transferInvoice', params);
notify(t('Transferred invoice'), 'positive'); notify(t('Transferred invoice'), 'positive');
closeForm(); const id = data?.[0];
router.push('InvoiceOutSummary', { id: data.id }); if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
} catch (err) { } catch (err) {
console.error('Error transfering invoice', err); console.error('Error transfering invoice', err);
} }
@ -52,22 +85,30 @@ const transferInvoice = async () => {
</script> </script>
<template> <template>
<FetchData
url="Clients"
@on-fetch="(data) => (clientsOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'id', limit: 30 }"
auto-load
/>
<FetchData <FetchData
url="CplusRectificationTypes" url="CplusRectificationTypes"
:filter="{ order: 'description' }" :filter="{ order: 'description' }"
@on-fetch="(data) => (rectificativeTypeOptions = data)" @on-fetch="
(data) => (
(rectificativeTypeOptions = data),
(transferInvoiceParams.cplusRectificationTypeFk = data.filter(
(type) => type.description == 'I Por diferencias'
)[0].id)
)
"
auto-load auto-load
/> />
<FetchData <FetchData
url="SiiTypeInvoiceOuts" url="SiiTypeInvoiceOuts"
:filter="{ where: { code: { like: 'R%' } } }" :filter="{ where: { code: { like: 'R%' } } }"
@on-fetch="(data) => (siiTypeInvoiceOutsOptions = data)" @on-fetch="
(data) => (
(siiTypeInvoiceOutsOptions = data),
(transferInvoiceParams.siiTypeInvoiceOutFk = data.filter(
(type) => type.code == 'R4'
)[0].id)
)
"
auto-load auto-load
/> />
<FetchData <FetchData
@ -75,14 +116,15 @@ const transferInvoice = async () => {
@on-fetch="(data) => (invoiceCorrectionTypesOptions = data)" @on-fetch="(data) => (invoiceCorrectionTypesOptions = data)"
auto-load auto-load
/> />
<QDialog ref="dialogRef">
<FormPopup <FormPopup
@on-submit="transferInvoice()" @on-submit="makeInvoice()"
:title="t('Transfer invoice')" :title="t('Transfer invoice')"
:custom-submit-button-label="t('Transfer client')" :custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false" :default-cancel-button="false"
> >
<template #form-inputs> <template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnSelect <VnSelect
:label="t('Client')" :label="t('Client')"
:options="clientsOptions" :options="clientsOptions"
@ -91,13 +133,18 @@ const transferInvoice = async () => {
option-value="id" option-value="id"
v-model="transferInvoiceParams.newClientFk" v-model="transferInvoiceParams.newClientFk"
:required="true" :required="true"
url="Clients"
:fields="['id', 'name', 'hasToInvoiceByAddress']"
auto-load
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem
v-bind="scope.itemProps"
@click="selectedClient(scope.opt)"
>
<QItemSection> <QItemSection>
<QItemLabel> <QItemLabel>
#{{ scope.opt?.id }} - #{{ scope.opt?.id }} - {{ scope.opt?.name }}
{{ scope.opt?.name }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -113,7 +160,7 @@ const transferInvoice = async () => {
:required="true" :required="true"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow>
<VnSelect <VnSelect
:label="t('Class')" :label="t('Class')"
:options="siiTypeInvoiceOutsOptions" :options="siiTypeInvoiceOutsOptions"
@ -144,11 +191,27 @@ const transferInvoice = async () => {
:required="true" :required="true"
/> />
</VnRow> </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>
</VnRow>
</template> </template>
</FormPopup> </FormPopup>
</QDialog>
</template> </template>
<i18n> <i18n>
en:
checkInfo: New tickets from the destination customer will be generated in the consignee by default.
transferInvoiceInfo: Destination customer is marked to bill in the consignee
confirmTransferInvoice: The destination customer has selected to bill in the consignee, do you want to continue?
es: es:
Transfer invoice: Transferir factura Transfer invoice: Transferir factura
Transfer client: Transferir cliente Transfer client: Transferir cliente
@ -157,4 +220,7 @@ es:
Class: Clase Class: Clase
Type: Tipo Type: Tipo
Transferred invoice: Factura transferida Transferred invoice: Factura transferida
Bill destination client: Facturar cliente destino
transferInvoiceInfo: Los nuevos tickets del cliente destino, serán generados en el consignatario por defecto.
confirmTransferInvoice: El cliente destino tiene marcado facturar por consignatario, desea continuar?
</i18n> </i18n>

View File

@ -11,12 +11,15 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { useClipboard } from 'src/composables/useClipboard'; import { useClipboard } from 'src/composables/useClipboard';
import { useRole } from 'src/composables/useRole';
import VnAvatar from './ui/VnAvatar.vue';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
const router = useRouter(); const router = useRouter();
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const { copyText } = useClipboard(); const { copyText } = useClipboard();
const userLocale = computed({ const userLocale = computed({
get() { get() {
return locale.value; return locale.value;
@ -47,7 +50,6 @@ const darkMode = computed({
}); });
const user = state.getUser(); const user = state.getUser();
const token = session.getTokenMultimedia();
const warehousesData = ref(); const warehousesData = ref();
const companiesData = ref(); const companiesData = ref();
const accountBankData = ref(); const accountBankData = ref();
@ -99,6 +101,7 @@ function saveUserData(param, value) {
axios.post('UserConfigs/setUserConfig', { [param]: value }); axios.post('UserConfigs/setUserConfig', { [param]: value });
localUserData(); localUserData();
} }
const isEmployee = computed(() => useRole().isEmployee());
</script> </script>
<template> <template>
@ -109,12 +112,14 @@ function saveUserData(param, value) {
auto-load auto-load
/> />
<FetchData <FetchData
v-if="isEmployee"
url="Companies" url="Companies"
order="name" order="name"
@on-fetch="(data) => (companiesData = data)" @on-fetch="(data) => (companiesData = data)"
auto-load auto-load
/> />
<FetchData <FetchData
v-if="isEmployee"
url="Accountings" url="Accountings"
order="name" order="name"
@on-fetch="(data) => (accountBankData = data)" @on-fetch="(data) => (accountBankData = data)"
@ -131,7 +136,7 @@ function saveUserData(param, value) {
@update:model-value="saveLanguage" @update:model-value="saveLanguage"
:label="t(`globals.lang['${userLocale}']`)" :label="t(`globals.lang['${userLocale}']`)"
icon="public" icon="public"
color="orange" color="primary"
false-value="es" false-value="es"
true-value="en" true-value="en"
/> />
@ -140,7 +145,7 @@ function saveUserData(param, value) {
@update:model-value="saveDarkMode" @update:model-value="saveDarkMode"
:label="t(`globals.darkMode`)" :label="t(`globals.darkMode`)"
checked-icon="dark_mode" checked-icon="dark_mode"
color="orange" color="primary"
unchecked-icon="light_mode" unchecked-icon="light_mode"
/> />
</div> </div>
@ -148,13 +153,20 @@ function saveUserData(param, value) {
<QSeparator vertical inset class="q-mx-lg" /> <QSeparator vertical inset class="q-mx-lg" />
<div class="col column items-center q-mb-sm"> <div class="col column items-center q-mb-sm">
<QAvatar size="80px"> <VnAvatar
<QImg :worker-id="user.id"
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`" :title="user.name"
spinner-color="white" size="xxl"
color="transparent"
/>
<QBtn
v-if="isEmployee"
class="q-mt-sm q-px-md"
:to="`/worker/${user.id}`"
color="primary"
:label="t('globals.myAccount')"
dense
/> />
</QAvatar>
<div class="text-subtitle1 q-mt-md"> <div class="text-subtitle1 q-mt-md">
<strong>{{ user.nickname }}</strong> <strong>{{ user.nickname }}</strong>
</div> </div>
@ -166,7 +178,7 @@ function saveUserData(param, value) {
</div> </div>
<QBtn <QBtn
id="logout" id="logout"
color="orange" color="primary"
flat flat
:label="t('globals.logOut')" :label="t('globals.logOut')"
size="sm" size="sm"

View File

@ -0,0 +1,59 @@
<script setup>
import { ref, watch } from 'vue';
import { useValidator } from 'src/composables/useValidator';
import { useI18n } from 'vue-i18n';
import VnSelectDialog from 'components/common/VnSelectDialog.vue';
import FetchData from 'components/FetchData.vue';
import CreateNewProvinceForm from './CreateNewProvinceForm.vue';
const emit = defineEmits(['onProvinceCreated']);
const provinceFk = defineModel({ type: Number });
watch(provinceFk, async () => await provincesFetchDataRef.value.fetch());
const { validate } = useValidator();
const { t } = useI18n();
const provincesOptions = ref();
const provincesFetchDataRef = ref();
async function onProvinceCreated(_, data) {
await provincesFetchDataRef.value.fetch();
provinceFk.value = data.id;
emit('onProvinceCreated', data);
}
</script>
<template>
<FetchData
ref="provincesFetchDataRef"
:filter="{ include: { relation: 'country' } }"
@on-fetch="(data) => (provincesOptions = data)"
auto-load
url="Provinces"
/>
<VnSelectDialog
:label="t('Province')"
:options="provincesOptions"
hide-selected
v-model="provinceFk"
:rules="validate && validate('postcode.provinceFk')"
:roles-allowed-to-create="['deliveryAssistant']"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption> {{ opt.country.name }} </QItemLabel>
</QItemSection>
</QItem>
</template>
<template #form>
<CreateNewProvinceForm @on-data-saved="onProvinceCreated" />
</template>
</VnSelectDialog>
</template>
<i18n>
es:
Province: Provincia
</i18n>

View File

@ -35,7 +35,9 @@ function stopEventPropagation(event) {
dense dense
square square
> >
<span v-if="!col.chip.icon">{{ row[col.name] }}</span> <span v-if="!col.chip.icon">
{{ col.format ? col.format(row) : row[col.name] }}
</span>
<QIcon v-else :name="col.chip.icon" color="primary-light" /> <QIcon v-else :name="col.chip.icon" color="primary-light" />
</QChip> </QChip>
</span> </span>

View File

@ -5,9 +5,13 @@ import { dashIfEmpty } from 'src/filters';
/* basic input */ /* basic input */
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnSelectCache from 'components/common/VnSelectCache.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import VnInputNumber from 'components/common/VnInputNumber.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnComponent from 'components/common/VnComponent.vue'; import VnComponent from 'components/common/VnComponent.vue';
import VnUserLink from 'components/ui/VnUserLink.vue';
const model = defineModel(undefined, { required: true }); const model = defineModel(undefined, { required: true });
const $props = defineProps({ const $props = defineProps({
@ -41,20 +45,33 @@ const $props = defineProps({
}, },
}); });
const defaultSelect = {
attrs: {
row: $props.row,
disable: !$props.isEditable,
class: 'fit',
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
};
const defaultComponents = { const defaultComponents = {
input: { input: {
component: markRaw(VnInput), component: markRaw(VnInput),
attrs: { attrs: {
disable: !$props.isEditable, disable: !$props.isEditable,
class: 'fit',
}, },
forceAttrs: { forceAttrs: {
label: $props.showLabel && $props.column.label, label: $props.showLabel && $props.column.label,
}, },
}, },
number: { number: {
component: markRaw(VnInput), component: markRaw(VnInputNumber),
attrs: { attrs: {
disable: !$props.isEditable, disable: !$props.isEditable,
class: 'fit',
}, },
forceAttrs: { forceAttrs: {
label: $props.showLabel && $props.column.label, label: $props.showLabel && $props.column.label,
@ -63,9 +80,19 @@ const defaultComponents = {
date: { date: {
component: markRaw(VnInputDate), component: markRaw(VnInputDate),
attrs: { attrs: {
readonly: true, readonly: !$props.isEditable,
disable: !$props.isEditable, disable: !$props.isEditable,
style: 'min-width: 125px', style: 'min-width: 125px',
class: 'fit',
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
time: {
component: markRaw(VnInputTime),
attrs: {
disable: !$props.isEditable,
}, },
forceAttrs: { forceAttrs: {
label: $props.showLabel && $props.column.label, label: $props.showLabel && $props.column.label,
@ -73,14 +100,14 @@ const defaultComponents = {
}, },
checkbox: { checkbox: {
component: markRaw(QCheckbox), component: markRaw(QCheckbox),
attrs: (prop) => { attrs: ({ model }) => {
const defaultAttrs = { const defaultAttrs = {
disable: !$props.isEditable, disable: !$props.isEditable,
'model-value': Boolean(prop), 'model-value': Boolean(model),
class: 'no-padding', class: 'no-padding fit',
}; };
if (typeof prop == 'number') { if (typeof model == 'number') {
defaultAttrs['true-value'] = 1; defaultAttrs['true-value'] = 1;
defaultAttrs['false-value'] = 0; defaultAttrs['false-value'] = 0;
} }
@ -91,17 +118,19 @@ const defaultComponents = {
}, },
}, },
select: { select: {
component: markRaw(VnSelectCache),
...defaultSelect,
},
rawSelect: {
component: markRaw(VnSelect), component: markRaw(VnSelect),
attrs: { ...defaultSelect,
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
}, },
icon: { icon: {
component: markRaw(QIcon), component: markRaw(QIcon),
}, },
userLink: {
component: markRaw(VnUserLink),
},
}; };
const value = computed(() => { const value = computed(() => {
@ -122,8 +151,8 @@ const col = computed(() => {
}; };
} }
if ( if (
(newColumn.name.startsWith('is') || newColumn.name.startsWith('has')) && (/^is[A-Z]/.test(newColumn.name) || /^has[A-Z]/.test(newColumn.name)) &&
!newColumn.component newColumn.component == null
) )
newColumn.component = 'checkbox'; newColumn.component = 'checkbox';
if ($props.default && !newColumn.component) newColumn.component = $props.default; if ($props.default && !newColumn.component) newColumn.component = $props.default;
@ -134,19 +163,19 @@ const col = computed(() => {
const components = computed(() => $props.components ?? defaultComponents); const components = computed(() => $props.components ?? defaultComponents);
</script> </script>
<template> <template>
<div class="row no-wrap fit"> <div class="row no-wrap">
<VnComponent <VnComponent
v-if="col.before" v-if="col.before"
:prop="col.before" :prop="col.before"
:components="components" :components="components"
:value="model" :value="{ row, model }"
v-model="model" v-model="model"
/> />
<VnComponent <VnComponent
v-if="col.component" v-if="col.component"
:prop="col" :prop="col"
:components="components" :components="components"
:value="model" :value="{ row, model }"
v-model="model" v-model="model"
/> />
<span :title="value" v-else>{{ value }}</span> <span :title="value" v-else>{{ value }}</span>
@ -154,7 +183,7 @@ const components = computed(() => $props.components ?? defaultComponents);
v-if="col.after" v-if="col.after"
:prop="col.after" :prop="col.after"
:components="components" :components="components"
:value="model" :value="{ row, model }"
v-model="model" v-model="model"
/> />
</div> </div>

View File

@ -7,8 +7,11 @@ import { useArrayData } from 'composables/useArrayData';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue'; import VnTableColumn from 'components/VnTable/VnColumn.vue';
defineExpose({ addFilter });
const $props = defineProps({ const $props = defineProps({
column: { column: {
type: Object, type: Object,
@ -31,7 +34,7 @@ const model = defineModel(undefined, { required: true });
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl }); const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
const columnFilter = computed(() => $props.column?.columnFilter); const columnFilter = computed(() => $props.column?.columnFilter);
const updateEvent = { 'update:modelValue': addFilter }; const updateEvent = { 'update:modelValue': addFilter, remove: () => addFilter(null) };
const enterEvent = { const enterEvent = {
'keyup.enter': () => addFilter(model.value), 'keyup.enter': () => addFilter(model.value),
remove: () => addFilter(null), remove: () => addFilter(null),
@ -39,12 +42,23 @@ const enterEvent = {
const defaultAttrs = { const defaultAttrs = {
filled: !$props.showTitle, filled: !$props.showTitle,
class: 'q-px-sm q-pb-xs q-pt-none', class: 'q-px-xs q-pb-xs q-pt-none fit',
dense: true, dense: true,
}; };
const forceAttrs = { const forceAttrs = {
label: $props.showTitle ? '' : $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',
dense: true,
filled: !$props.showTitle,
},
forceAttrs,
}; };
const components = { const components = {
@ -63,6 +77,7 @@ const components = {
attrs: { attrs: {
...defaultAttrs, ...defaultAttrs,
clearable: true, clearable: true,
type: 'number',
}, },
forceAttrs, forceAttrs,
}, },
@ -75,26 +90,29 @@ const components = {
}, },
forceAttrs, forceAttrs,
}, },
time: {
component: markRaw(VnInputTime),
event: updateEvent,
attrs: {
...defaultAttrs,
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
checkbox: { checkbox: {
component: markRaw(QCheckbox), component: markRaw(QCheckbox),
event: updateEvent, event: updateEvent,
attrs: { attrs: {
dense: true, dense: true,
class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs', class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs fit',
'toggle-indeterminate': true, 'toggle-indeterminate': true,
}, },
forceAttrs, forceAttrs,
}, },
select: { select: selectComponent,
component: markRaw(VnSelect), rawSelect: selectComponent,
event: updateEvent,
attrs: {
class: 'q-px-md q-pb-xs q-pt-none',
dense: true,
filled: !$props.showTitle,
},
forceAttrs,
},
}; };
async function addFilter(value) { async function addFilter(value) {
@ -127,14 +145,11 @@ const showFilter = computed(
</script> </script>
<template> <template>
<div <div
v-if="showTitle" v-if="showFilter"
class="q-pt-sm q-px-sm ellipsis" class="full-width"
:class="`text-${column?.align ?? 'left'}`" :class="alignRow()"
:style="!showFilter ? { 'min-height': 72 + 'px' } : ''" style="max-height: 45px; overflow: hidden"
> >
{{ column?.label }}
</div>
<div v-if="showFilter" class="full-width" :class="alignRow()">
<VnTableColumn <VnTableColumn
:column="$props.column" :column="$props.column"
default="input" default="input"

View File

@ -0,0 +1,95 @@
<script setup>
import { ref } from 'vue';
import { useArrayData } from 'composables/useArrayData';
const model = defineModel({ type: Object });
const $props = defineProps({
name: {
type: String,
default: '',
},
label: {
type: String,
default: undefined,
},
dataKey: {
type: String,
required: true,
},
searchUrl: {
type: String,
default: 'params',
},
vertical: {
type: Boolean,
default: false,
},
});
const hover = ref();
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
async function orderBy(name, direction) {
if (!name) return;
switch (direction) {
case 'DESC':
direction = undefined;
break;
case undefined:
direction = 'ASC';
break;
case 'ASC':
direction = 'DESC';
break;
}
if (!direction) return await arrayData.deleteOrder(name);
await arrayData.addOrder(name, direction);
}
defineExpose({ orderBy });
</script>
<template>
<div
@mouseenter="hover = true"
@mouseleave="hover = false"
@click="orderBy(name, model?.direction)"
class="row items-center no-wrap cursor-pointer"
>
<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'"
>
{{ model?.index }}
<QIcon
:name="
model?.index
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: 'swap_vert'
"
size="xs"
/>
</div>
</QChip>
</div>
</template>

View File

@ -1,18 +1,20 @@
<script setup> <script setup>
import { ref, onMounted, computed, watch } from 'vue'; import { ref, onBeforeMount, onMounted, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import FormModelPopup from 'components/FormModelPopup.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnLv from 'components/ui/VnLv.vue';
import CrudModel from 'src/components/CrudModel.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue'; import VnTableColumn from 'components/VnTable/VnColumn.vue';
import VnTableFilter from 'components/VnTable/VnFilter.vue'; import VnTableFilter from 'components/VnTable/VnFilter.vue';
import VnTableChip from 'components/VnTable/VnChip.vue'; import VnTableChip from 'components/VnTable/VnChip.vue';
import VnVisibleColumn from 'src/components/VnTable/VnVisibleColumn.vue';
import VnLv from 'components/ui/VnLv.vue';
import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
const $props = defineProps({ const $props = defineProps({
columns: { columns: {
@ -21,7 +23,7 @@ const $props = defineProps({
}, },
defaultMode: { defaultMode: {
type: String, type: String,
default: 'card', // 'table', 'card' default: 'table', // 'table', 'card'
}, },
columnSearch: { columnSearch: {
type: Boolean, type: Boolean,
@ -32,7 +34,11 @@ const $props = defineProps({
default: true, default: true,
}, },
rowClick: { rowClick: {
type: Function, type: [Function, Boolean],
default: null,
},
rowCtrlClick: {
type: [Function, Boolean],
default: null, default: null,
}, },
redirect: { redirect: {
@ -59,6 +65,30 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
hasSubToolbar: {
type: Boolean,
default: true,
},
disableOption: {
type: Object,
default: () => ({ card: false, table: false }),
},
withoutHeader: {
type: Boolean,
default: false,
},
tableCode: {
type: String,
default: null,
},
table: {
type: Object,
default: () => ({}),
},
tableHeight: {
type: String,
default: '90vh',
},
}); });
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -66,32 +96,59 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const DEFAULT_MODE = 'card'; const CARD_MODE = 'card';
const TABLE_MODE = 'table'; const TABLE_MODE = 'table';
const mode = ref(DEFAULT_MODE); const mode = ref(CARD_MODE);
const selected = ref([]); const selected = ref([]);
const hasParams = ref(false);
const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}'); const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}');
const params = ref({ ...routeQuery, ...routeQuery.filter?.where }); const params = ref({ ...routeQuery, ...routeQuery.filter?.where });
const orders = ref(parseOrder(routeQuery.filter?.order));
const CrudModelRef = ref({}); const CrudModelRef = ref({});
const showForm = ref(false); const showForm = ref(false);
const splittedColumns = ref({ columns: [] }); const splittedColumns = ref({ columns: [] });
const columnsVisibilitySkipped = ref();
const createForm = ref();
const tableModes = [ const tableModes = [
{ {
icon: 'view_column', icon: 'view_column',
title: t('table view'), title: t('table view'),
value: TABLE_MODE, value: TABLE_MODE,
disable: $props.disableOption?.table,
}, },
{ {
icon: 'grid_view', icon: 'grid_view',
title: t('grid view'), title: t('grid view'),
value: DEFAULT_MODE, value: CARD_MODE,
disable: $props.disableOption?.card,
}, },
]; ];
onBeforeMount(() => {
setUserParams(route.query[$props.searchUrl]);
hasParams.value = params.value && Object.keys(params.value).length !== 0;
});
onMounted(() => { onMounted(() => {
mode.value = quasar.platform.is.mobile ? DEFAULT_MODE : $props.defaultMode; mode.value =
quasar.platform.is.mobile && !$props.disableOption?.card
? CARD_MODE
: $props.defaultMode;
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
setUserParams(route.query[$props.searchUrl]); columnsVisibilitySkipped.value = [
...splittedColumns.value.columns
.filter((c) => c.visible == false)
.map((c) => c.name),
...['tableActions'],
];
createForm.value = $props.create;
if ($props.create && route?.query?.createForm) {
showForm.value = true;
createForm.value = {
...createForm.value,
...{ formInitialData: JSON.parse(route?.query?.createForm) },
};
}
}); });
watch( watch(
@ -105,14 +162,34 @@ watch(
(val) => setUserParams(val) (val) => setUserParams(val)
); );
function setUserParams(watchedParams) { const isTableMode = computed(() => mode.value == TABLE_MODE);
function setUserParams(watchedParams, watchedOrder) {
if (!watchedParams) return; if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
const where = JSON.parse(watchedParams?.filter)?.where; const filter =
typeof watchedParams?.filter == 'string'
? JSON.parse(watchedParams?.filter ?? '{}')
: watchedParams?.filter;
const where = filter?.where;
const order = watchedOrder ?? filter?.order;
watchedParams = { ...watchedParams, ...where }; watchedParams = { ...watchedParams, ...where };
delete watchedParams.filter; delete watchedParams.filter;
params.value = { ...params.value, ...watchedParams }; delete params.value?.filter;
params.value = { ...params.value, ...sanitizer(watchedParams) };
orders.value = parseOrder(order);
}
function sanitizer(params) {
for (const [key, value] of Object.entries(params)) {
if (typeof value == 'object') {
const param = Object.values(value)[0];
if (typeof param == 'string') params[key] = param.replaceAll('%', '');
}
}
return params;
} }
function splitColumns(columns) { function splitColumns(columns) {
@ -120,17 +197,21 @@ function splitColumns(columns) {
columns: [], columns: [],
chips: [], chips: [],
create: [], create: [],
visible: [], cardVisible: [],
}; };
for (const col of columns) { for (const col of columns) {
if (col.name == 'tableActions') splittedColumns.value.actions = col; if (col.name == 'tableActions') {
col.orderBy = false;
splittedColumns.value.actions = col;
}
if (col.chip) splittedColumns.value.chips.push(col); if (col.chip) splittedColumns.value.chips.push(col);
if (col.isTitle) splittedColumns.value.title = col; if (col.isTitle) splittedColumns.value.title = col;
if (col.create) splittedColumns.value.create.push(col); if (col.create) splittedColumns.value.create.push(col);
if (col.cardVisible) splittedColumns.value.visible.push(col); if (col.cardVisible) splittedColumns.value.cardVisible.push(col);
if ($props.isEditable && col.disable == null) col.disable = false; if ($props.isEditable && col.disable == null) col.disable = false;
if ($props.useModel) col.columnFilter = { ...col.columnFilter, inWhere: true }; if ($props.useModel && col.columnFilter != false)
col.columnFilter = { ...col.columnFilter, inWhere: true };
splittedColumns.value.columns.push(col); splittedColumns.value.columns.push(col);
} }
// Status column // Status column
@ -144,16 +225,27 @@ function splitColumns(columns) {
label: t('status'), label: t('status'),
name: 'tableStatus', name: 'tableStatus',
columnFilter: false, columnFilter: false,
orderBy: false,
}); });
} }
} }
const rowClickFunction = computed(() => { const rowClickFunction = computed(() => {
if ($props.rowClick) return $props.rowClick; if ($props.rowClick != undefined) return $props.rowClick;
if ($props.redirect) return ({ id }) => redirectFn(id); if ($props.redirect) return ({ id }) => redirectFn(id);
return () => {}; return () => {};
}); });
const rowCtrlClickFunction = computed(() => {
if ($props.rowCtrlClick != undefined) return $props.rowCtrlClick;
if ($props.redirect)
return (evt, { id }) => {
stopEventPropagation(evt);
window.open(`/#/${$props.redirect}/${id}`, '_blank');
};
return () => {};
});
function redirectFn(id) { function redirectFn(id) {
router.push({ path: `/${$props.redirect}/${id}` }); router.push({ path: `/${$props.redirect}/${id}` });
} }
@ -164,6 +256,7 @@ function stopEventPropagation(event) {
} }
function reload(params) { function reload(params) {
selected.value = [];
CrudModelRef.value.reload(params); CrudModelRef.value.reload(params);
} }
@ -175,11 +268,25 @@ function columnName(col) {
} }
function getColAlign(col) { function getColAlign(col) {
return 'text-' + (col.align ?? 'left') return 'text-' + (col.align ?? 'left');
} }
function parseOrder(urlOrders) {
const orderObject = {};
if (!urlOrders) return orderObject;
if (typeof urlOrders == 'string') urlOrders = [urlOrders];
for (const [index, orders] of urlOrders.entries()) {
const [name, direction] = orders.split(' ');
orderObject[name] = { direction, index: index + 1 };
}
return orderObject;
}
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
defineExpose({ defineExpose({
reload, reload,
redirect: redirectFn, redirect: redirectFn,
selected,
}); });
</script> </script>
<template> <template>
@ -195,97 +302,135 @@ defineExpose({
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
:search-button="true" :search-button="true"
v-model="params" v-model="params"
:disable-submit-event="true"
:search-url="searchUrl" :search-url="searchUrl"
:redirect="!!redirect"
@set-user-params="setUserParams"
> >
<template #body> <template #body>
<div
class="row no-wrap flex-center"
v-for="col of splittedColumns.columns.filter(
(c) => c.columnFilter ?? true
)"
:key="col.id"
>
<VnTableFilter <VnTableFilter
:column="col" :column="col"
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
v-for="col of splittedColumns.columns"
:key="col.id"
v-model="params[columnName(col)]" v-model="params[columnName(col)]"
:search-url="searchUrl" :search-url="searchUrl"
/> />
</template> <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>
<slot <slot
name="moreFilterPanel" name="moreFilterPanel"
:params="params" :params="params"
:columns="splittedColumns.columns" :columns="splittedColumns.columns"
/> />
</template>
</VnFilterPanel> </VnFilterPanel>
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<!-- class in div to fix warn--> <!-- class in div to fix warn-->
<div class="q-px-md">
<CrudModel <CrudModel
v-bind="$attrs" v-bind="$attrs"
:class="$attrs['class'] ?? 'q-px-md'"
:limit="20" :limit="20"
ref="CrudModelRef" ref="CrudModelRef"
@on-fetch="(...args) => emit('onFetch', ...args)"
:search-url="searchUrl" :search-url="searchUrl"
:disable-infinite-scroll="mode == TABLE_MODE" :disable-infinite-scroll="isTableMode"
@save-changes="reload" @save-changes="reload"
:has-subtoolbar="isEditable" :has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable"
:auto-load="hasParams || $attrs['auto-load']"
> >
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
<template #body="{ rows }"> <template #body="{ rows }">
<QTable <QTable
v-bind="$attrs['QTable']" v-bind="table"
class="vnTable" class="vnTable"
:columns="splittedColumns.columns" :columns="splittedColumns.columns"
:rows="rows" :rows="rows"
v-model:selected="selected" v-model:selected="selected"
:grid="mode != TABLE_MODE" :grid="!isTableMode"
table-header-class="bg-header" table-header-class="bg-header"
card-container-class="grid-three" card-container-class="grid-three"
flat flat
:style="mode == TABLE_MODE && 'max-height: 90vh'" :style="isTableMode && `max-height: ${tableHeight}`"
virtual-scroll virtual-scroll
@virtual-scroll=" @virtual-scroll="
(event) => (event) =>
event.index > rows.length - 2 && event.index > rows.length - 2 &&
CrudModelRef.vnPaginateRef.paginate() CrudModelRef.vnPaginateRef.paginate()
" "
@row-click="(_, row) => rowClickFunction(row)" @row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
@update:selected="emit('update:selected', $event)"
> >
<template #top-left> <template #top-left v-if="!$props.withoutHeader">
<slot name="top-left"></slot> <slot name="top-left"></slot>
</template> </template>
<template #top-right> <template #top-right v-if="!$props.withoutHeader">
<!-- <QBtn <VnVisibleColumn
icon="visibility" v-if="isTableMode"
title="asd" v-model="splittedColumns.columns"
class="bg-vn-section-color q-mr-md" :table-code="tableCode ?? route.name"
dense :skip="columnsVisibilitySkipped"
v-if="mode == 'table'" />
/> -->
<QBtnToggle <QBtnToggle
v-model="mode" v-model="mode"
toggle-color="primary" toggle-color="primary"
class="bg-vn-section-color" class="bg-vn-section-color"
dense dense
:options="tableModes" :options="tableModes.filter((mode) => !mode.disable)"
/> />
<QBtn <QBtn
v-if="$props.rightSearch"
icon="filter_alt" icon="filter_alt"
title="asd"
class="bg-vn-section-color q-ml-md" class="bg-vn-section-color q-ml-md"
dense dense
@click="stateStore.toggleRightDrawer()" @click="stateStore.toggleRightDrawer()"
/> />
</template> </template>
<template #header-cell="{ col }"> <template #header-cell="{ col }">
<QTh <QTh v-if="col.visible ?? true">
auto-width <div
style="min-width: 100px" class="column self-start q-ml-xs ellipsis"
v-if="$props.columnSearch" :class="`text-${col?.align ?? 'left'}`"
:style="$props.columnSearch ? 'height: 75px' : ''"
> >
<div class="row items-center no-wrap" style="height: 30px">
<QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip>
<VnTableOrder
v-model="orders[col.orderBy ?? col.name]"
:name="col.orderBy ?? col.name"
:label="col?.label"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
/>
</div>
<VnTableFilter <VnTableFilter
v-if="$props.columnSearch"
:column="col" :column="col"
:show-title="true" :show-title="true"
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
v-model="params[columnName(col)]" v-model="params[columnName(col)]"
:search-url="searchUrl" :search-url="searchUrl"
class="full-width"
/> />
</div>
</QTh> </QTh>
</template> </template>
<template #header-cell-tableActions> <template #header-cell-tableActions>
@ -293,30 +438,39 @@ defineExpose({
</template> </template>
<template #body-cell-tableStatus="{ col, row }"> <template #body-cell-tableStatus="{ col, row }">
<QTd auto-width :class="getColAlign(col)"> <QTd auto-width :class="getColAlign(col)">
<VnTableChip <VnTableChip :columns="splittedColumns.columnChips" :row="row">
:columns="splittedColumns.columnChips"
:row="row"
>
<template #afterChip> <template #afterChip>
<slot name="afterChip" :row="row"></slot> <slot name="afterChip" :row="row"></slot>
</template> </template>
</VnTableChip> </VnTableChip>
</QTd> </QTd>
</template> </template>
<template #body-cell="{ col, row }"> <template #body-cell="{ col, row, rowIndex }">
<!-- Columns --> <!-- Columns -->
<QTd <QTd
auto-width auto-width
class="no-margin q-px-xs" class="no-margin q-px-xs"
:class="getColAlign(col)" :class="[getColAlign(col), col.columnClass]"
v-if="col.visible ?? true"
@click.ctrl="
($event) =>
rowCtrlClickFunction && rowCtrlClickFunction($event, row)
"
>
<slot
:name="`column-${col.name}`"
:col="col"
:row="row"
:row-index="rowIndex"
> >
<VnTableColumn <VnTableColumn
:column="col" :column="col"
:row="row" :row="row"
:is-editable="false" :is-editable="col.isEditable ?? isEditable"
v-model="row[col.name]" v-model="row[col.name]"
component-prop="columnField" component-prop="columnField"
/> />
</slot>
</QTd> </QTd>
</template> </template>
<template #body-cell-tableActions="{ col, row }"> <template #body-cell-tableActions="{ col, row }">
@ -334,10 +488,11 @@ defineExpose({
class="q-px-sm" class="q-px-sm"
flat flat
:class=" :class="
btn.isPrimary btn.isPrimary ? 'text-primary-light' : 'color-vn-text '
? 'text-primary-light'
: 'color-vn-text '
" "
:style="`visibility: ${
(btn.show && btn.show(row)) ?? true ? 'visible' : 'hidden'
}`"
@click="btn.action(row)" @click="btn.action(row)"
/> />
</QTd> </QTd>
@ -395,22 +550,28 @@ defineExpose({
:class="$props.cardClass" :class="$props.cardClass"
> >
<div <div
v-for="col of splittedColumns.visible" v-for="(
col, index
) of splittedColumns.cardVisible"
:key="col.name" :key="col.name"
class="fields" class="fields"
> >
<VnLv <VnLv
:label=" :label="
!col.component && !col.component && col.label
col.label && ? `${col.label}:`
`${col.label}:` : ''
" "
> >
<template #value> <template #value>
<span <span
@click=" @click="stopEventPropagation($event)"
stopEventPropagation($event) >
" <slot
:name="`column-${col.name}`"
:col="col"
:row="row"
:row-index="index"
> >
<VnTableColumn <VnTableColumn
:column="col" :column="col"
@ -420,6 +581,7 @@ defineExpose({
component-prop="columnField" component-prop="columnField"
:show-label="true" :show-label="true"
/> />
</slot>
</span> </span>
</template> </template>
</VnLv> </VnLv>
@ -454,30 +616,37 @@ defineExpose({
</QTable> </QTable>
</template> </template>
</CrudModel> </CrudModel>
</div>
<QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2"> <QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2">
<QBtn @click="showForm = !showForm" color="primary" fab icon="add" /> <QBtn @click="showForm = !showForm" color="primary" fab icon="add" />
<QTooltip> <QTooltip>
{{ create.title }} {{ createForm.title }}
</QTooltip> </QTooltip>
</QPageSticky> </QPageSticky>
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> <QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
<FormModelPopup <FormModelPopup
v-bind="create" v-bind="createForm"
:model="$attrs['data-key'] + 'Create'" :model="$attrs['data-key'] + 'Create'"
@on-data-saved="(_, res) => create.onDataSaved(res)" @on-data-saved="(_, res) => createForm.onDataSaved(res)"
> >
<template #form-inputs="{ data }"> <template #form-inputs="{ data }">
<div class="grid-create"> <div class="grid-create">
<VnTableColumn <slot
v-for="column of splittedColumns.create" v-for="column of splittedColumns.create"
:key="column.name" :key="column.name"
:name="`column-create-${column.name}`"
:data="data"
:column-name="column.name"
:label="column.label"
>
<VnTableColumn
:column="column" :column="column"
:row="{}" :row="{}"
default="input" default="input"
v-model="data[column.name]" v-model="data[column.name]"
:show-label="true" :show-label="true"
component-prop="columnCreate"
/> />
</slot>
<slot name="more-create-dialog" :data="data" /> <slot name="more-create-dialog" :data="data" />
</div> </div>
</template> </template>
@ -487,8 +656,12 @@ defineExpose({
<i18n> <i18n>
en: en:
status: Status status: Status
table view: Table view
grid view: Grid view
es: es:
status: Estados status: Estados
table view: Vista en tabla
grid view: Vista en cuadrícula
</i18n> </i18n>
<style lang="scss"> <style lang="scss">
@ -498,7 +671,11 @@ es:
} }
.bg-header { .bg-header {
background-color: #5d5d5d; background-color: var(--vn-header-color);
color: var(--vn-text-color);
}
.color-vn-text {
color: var(--vn-text-color); color: var(--vn-text-color);
} }
@ -507,7 +684,7 @@ es:
.q-table--dark tr, .q-table--dark tr,
.q-table--dark th, .q-table--dark th,
.q-table--dark td { .q-table--dark td {
border-color: #222222; border-color: var(--vn-section-color);
} }
.q-table__container > div:first-child { .q-table__container > div:first-child {
@ -577,7 +754,7 @@ es:
right: 0; right: 0;
} }
td.sticky { td.sticky {
background-color: var(--q-dark); background-color: var(--vn-section-color);
z-index: 1; z-index: 1;
} }
} }

View File

@ -0,0 +1,189 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref, computed, onMounted } from 'vue';
import { useState } from 'src/composables/useState';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const columns = defineModel({ type: Object, default: [] });
const $props = defineProps({
tableCode: {
type: String,
default: '',
},
skip: {
type: Array,
default: () => [],
},
});
const { notify } = useNotify();
const { t } = useI18n();
const state = useState();
const user = state.getUser();
const popupProxyRef = ref();
const initialUserConfigViewData = ref();
const localColumns = ref([]);
const areAllChecksMarked = computed(() => {
return localColumns.value.every((col) => col.visible);
});
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;
if (skippeds[name]) continue;
column.visible = data[name] ?? true;
if (!isLocal) localColumns.value.push({ name, label, visible: column.visible });
}
}
function toggleMarkAll(val) {
localColumns.value.forEach((col) => (col.visible = val));
}
async function getConfig(url, filter) {
const response = await axios.get(url, {
params: { filter: filter },
});
return response.data && response.data.length > 0 ? response.data[0] : null;
}
async function fetchViewConfigData() {
try {
const defaultFilter = {
where: { tableCode: $props.tableCode },
};
const userConfig = await getConfig('UserConfigViews', {
where: {
...defaultFilter.where,
...{ userFk: user.value.id },
},
});
if (userConfig) {
initialUserConfigViewData.value = userConfig;
setUserConfigViewData(userConfig.configuration);
return;
}
const defaultConfig = await getConfig('DefaultViewConfigs', defaultFilter);
if (defaultConfig) {
setUserConfigViewData(defaultConfig.columns);
return;
}
} catch (err) {
console.error('Error fetching config view data', err);
}
}
async function saveConfig() {
const configuration = {};
for (const { name, visible } of localColumns.value)
configuration[name] = visible ?? true;
setUserConfigViewData(configuration, true);
if (!$props.tableCode) return popupProxyRef.value.hide();
try {
const params = {};
// Si existe una view config del usuario hacemos un update si no la creamos
if (initialUserConfigViewData.value) {
params.updates = [
{
data: {
configuration,
},
where: {
id: initialUserConfigViewData.value.id,
},
},
];
} else {
params.creates = [
{
userFk: user.value.id,
tableCode: $props.tableCode,
tableConfig: $props.tableCode,
configuration,
},
];
}
const response = await axios.post('UserConfigViews/crud', params);
if (response.data && response.data[0]) {
initialUserConfigViewData.value = response.data[0];
}
notify('globals.dataSaved', 'positive');
popupProxyRef.value.hide();
} catch (err) {
console.error('Error saving user view config', err);
notify('errors.writeRequest', 'negative');
}
}
onMounted(async () => {
setUserConfigViewData({});
await fetchViewConfigData();
});
</script>
<template>
<QBtn icon="vn:visible_columns" class="bg-vn-section-color q-mr-md q-px-sm" dense>
<QPopupProxy ref="popupProxyRef">
<QCard class="column q-pa-md">
<QIcon name="info" size="sm" class="info-icon">
<QTooltip>{{ t('Check the columns you want to see') }}</QTooltip>
</QIcon>
<span class="text-body1 q-mb-sm">{{ t('Visible columns') }}</span>
<QCheckbox
:label="t('Tick all')"
:model-value="areAllChecksMarked"
@update:model-value="toggleMarkAll($event)"
class="q-mb-sm"
/>
<div v-if="columns.length > 0" class="checks-layout">
<QCheckbox
v-for="col in localColumns"
:key="col.name"
:label="col.label"
v-model="col.visible"
/>
</div>
<QBtn
class="full-width q-mt-md"
color="primary"
@click="saveConfig()"
:label="t('globals.save')"
/>
</QCard>
</QPopupProxy>
<QTooltip>{{ t('Visible columns') }}</QTooltip>
</QBtn>
</template>
<style lang="scss" scoped>
.info-icon {
position: absolute;
top: 20px;
right: 20px;
}
.checks-layout {
display: grid;
grid-template-columns: repeat(3, 200px);
}
</style>
<i18n>
es:
Check the columns you want to see: Marca las columnas que quieres ver
Visible columns: Columnas visibles
Tick all: Marcar todas
</i18n>

View File

@ -46,7 +46,7 @@ const stateStore = useStateStore();
</div> </div>
</Teleport> </Teleport>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit">
<div id="right-panel"></div> <div id="right-panel"></div>
<slot v-if="!hasContent" name="right-panel" /> <slot v-if="!hasContent" name="right-panel" />
</QScrollArea> </QScrollArea>

View File

@ -52,7 +52,7 @@ const toggleMarkAll = (val) => {
const getConfig = async (url, filter) => { const getConfig = async (url, filter) => {
const response = await axios.get(url, { const response = await axios.get(url, {
params: { filter: filter }, params: { filter: JSON.stringify(filter) },
}); });
return response.data && response.data.length > 0 ? response.data[0] : null; return response.data && response.data.length > 0 ? response.data[0] : null;
}; };
@ -60,7 +60,7 @@ const getConfig = async (url, filter) => {
const fetchViewConfigData = async () => { const fetchViewConfigData = async () => {
try { try {
const userConfigFilter = { const userConfigFilter = {
where: { tableCode: $props.tableCode, userFk: user.id }, where: { tableCode: $props.tableCode, userFk: user.value.id },
}; };
const userConfig = await getConfig('UserConfigViews', userConfigFilter); const userConfig = await getConfig('UserConfigViews', userConfigFilter);
@ -74,11 +74,17 @@ const fetchViewConfigData = async () => {
const defaultConfig = await getConfig('DefaultViewConfigs', defaultConfigFilter); const defaultConfig = await getConfig('DefaultViewConfigs', defaultConfigFilter);
if (defaultConfig) { if (defaultConfig) {
// Si el backend devuelve una configuración por defecto la usamos
setUserConfigViewData(defaultConfig.columns); setUserConfigViewData(defaultConfig.columns);
return; 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) { } catch (err) {
console.err('Error fetching config view data', err); console.error('Error fetching config view data', err);
} }
}; };

View File

@ -18,7 +18,7 @@ watchEffect(() => {
(matched) => Object.keys(matched.meta).length (matched) => Object.keys(matched.meta).length
); );
breadcrumbs.value.length = 0; breadcrumbs.value.length = 0;
if (!matched.value[0]) return;
if (matched.value[0].name != 'Dashboard') { if (matched.value[0].name != 'Dashboard') {
root.value = useCamelCase(matched.value[0].path.substring(1).toLowerCase()); root.value = useCamelCase(matched.value[0].path.substring(1).toLowerCase());

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { onBeforeMount, computed } from 'vue'; import { onBeforeMount, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize'; import useCardSize from 'src/composables/useCardSize';
@ -8,7 +8,6 @@ import VnSubToolbar from '../ui/VnSubToolbar.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import LeftMenu from 'components/LeftMenu.vue'; import LeftMenu from 'components/LeftMenu.vue';
import RightMenu from 'components/common/RightMenu.vue'; import RightMenu from 'components/common/RightMenu.vue';
const props = defineProps({ const props = defineProps({
dataKey: { type: String, required: true }, dataKey: { type: String, required: true },
baseUrl: { type: String, default: undefined }, baseUrl: { type: String, default: undefined },
@ -17,12 +16,7 @@ const props = defineProps({
descriptor: { type: Object, required: true }, descriptor: { type: Object, required: true },
filterPanel: { type: Object, default: undefined }, filterPanel: { type: Object, default: undefined },
searchDataKey: { type: String, default: undefined }, searchDataKey: { type: String, default: undefined },
searchUrl: { type: String, default: undefined }, searchbarProps: { type: Object, default: undefined },
searchbarLabel: { type: String, default: '' },
searchbarInfo: { type: String, default: '' },
searchCustomRouteRedirect: { type: String, default: undefined },
searchRedirect: { type: Boolean, default: true },
searchMakeFetch: { type: Boolean, default: true },
}); });
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -31,7 +25,10 @@ const url = computed(() => {
if (props.baseUrl) return `${props.baseUrl}/${route.params.id}`; if (props.baseUrl) return `${props.baseUrl}/${route.params.id}`;
return props.customUrl; return props.customUrl;
}); });
const searchRightDataKey = computed(() => {
if (!props.searchDataKey) return route.name;
return props.searchDataKey;
});
const arrayData = useArrayData(props.dataKey, { const arrayData = useArrayData(props.dataKey, {
url: url.value, url: url.value,
filter: props.filter, filter: props.filter,
@ -39,8 +36,17 @@ const arrayData = useArrayData(props.dataKey, {
onBeforeMount(async () => { onBeforeMount(async () => {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false, updateRouter: false });
}); });
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 });
}
});
}
</script> </script>
<template> <template>
<QDrawer <QDrawer
@ -56,26 +62,18 @@ onBeforeMount(async () => {
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<slot name="searchbar" v-if="props.searchDataKey"> <slot name="searchbar" v-if="props.searchDataKey">
<VnSearchbar <VnSearchbar :data-key="props.searchDataKey" v-bind="props.searchbarProps" />
:data-key="props.searchDataKey"
:url="props.searchUrl"
:label="props.searchbarLabel"
:info="props.searchbarInfo"
:custom-route-redirect-name="searchCustomRouteRedirect"
:redirect="searchRedirect"
/>
</slot> </slot>
<slot v-else name="searchbar" />
<RightMenu> <RightMenu>
<template #right-panel v-if="props.filterPanel"> <template #right-panel v-if="props.filterPanel">
<component :is="props.filterPanel" :data-key="props.searchDataKey" /> <component :is="props.filterPanel" :data-key="searchRightDataKey" />
</template> </template>
</RightMenu> </RightMenu>
<QPageContainer> <QPageContainer>
<QPage> <QPage>
<VnSubToolbar /> <VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]"> <div :class="[useCardSize(), $attrs.class]">
<RouterView /> <RouterView :key="route.fullPath" />
</div> </div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>

View File

@ -12,20 +12,22 @@ const $props = defineProps({
default: () => {}, default: () => {},
}, },
value: { value: {
type: [Object, Number, String], type: [Object, Number, String, Boolean],
default: () => {}, default: () => {},
}, },
}); });
let mixed;
const componentArray = computed(() => { const componentArray = computed(() => {
if (typeof $props.prop === 'object') return [$props.prop]; if (typeof $props.prop === 'object') return [$props.prop];
return $props.prop; return $props.prop;
}); });
function mix(toComponent) { function mix(toComponent) {
if (mixed) return mixed;
const { component, attrs, event } = toComponent; const { component, attrs, event } = toComponent;
const customComponent = $props.components[component]; const customComponent = $props.components[component];
return { mixed = {
component: customComponent?.component ?? component, component: customComponent?.component ?? component,
attrs: { attrs: {
...toValueAttrs(attrs), ...toValueAttrs(attrs),
@ -33,8 +35,9 @@ function mix(toComponent) {
...toComponent, ...toComponent,
...toValueAttrs(customComponent?.forceAttrs), ...toValueAttrs(customComponent?.forceAttrs),
}, },
event: event ?? customComponent?.event, event: { ...customComponent?.event, ...event },
}; };
return mixed;
} }
function toValueAttrs(attrs) { function toValueAttrs(attrs) {
@ -54,7 +57,6 @@ function toValueAttrs(attrs) {
v-bind="mix(toComponent).attrs" v-bind="mix(toComponent).attrs"
v-on="mix(toComponent).event ?? {}" v-on="mix(toComponent).event ?? {}"
v-model="model" v-model="model"
class="fit"
/> />
</span> </span>
</template> </template>

View File

@ -1,34 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useCapitalize } from 'src/composables/useCapitalize';
import VnInput from 'src/components/common/VnInput.vue';
const props = defineProps({
modelValue: { type: [String, Number], default: '' },
});
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']);
const amount = computed({
get() {
return props.modelValue;
},
set(val) {
emit('update:modelValue', val);
},
});
</script>
<template>
<VnInput
v-model="amount"
type="number"
step="any"
:label="useCapitalize(t('amount'))"
/>
</template>
<i18n>
es:
amount: importe
</i18n>

View File

@ -273,6 +273,10 @@ function shouldRenderButton(button, isExternal = false) {
if (button.name == 'download') return true; if (button.name == 'download') return true;
return button.external === isExternal; return button.external === isExternal;
} }
defineExpose({
dmsRef,
});
</script> </script>
<template> <template>
<VnPaginate <VnPaginate
@ -374,7 +378,11 @@ function shouldRenderButton(button, isExternal = false) {
/> />
</QDialog> </QDialog>
<QPageSticky position="bottom-right" :offset="[25, 25]"> <QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="showFormDialog()" /> <QBtn fab color="primary" icon="add" @click="showFormDialog()" class="fill-icon">
<QTooltip>
{{ t('Upload file') }}
</QTooltip>
</QBtn>
</QPageSticky> </QPageSticky>
</template> </template>
<style scoped> <style scoped>
@ -392,4 +400,5 @@ en:
es: es:
contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes} contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes}
Generate identifier for original file: Generar identificador para archivo original Generate identifier for original file: Generar identificador para archivo original
Upload file: Subir fichero
</i18n> </i18n>

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useValidator } from 'src/composables/useValidator';
const emit = defineEmits([ const emit = defineEmits([
'update:modelValue', 'update:modelValue',
@ -22,10 +23,16 @@ const $props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
clearable: {
type: Boolean,
default: true,
},
}); });
const { validations } = useValidator();
const { t } = useI18n(); const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired'); const requiredFieldRule = (val) => validations().required($attrs.required, val);
const vnInputRef = ref(null); const vnInputRef = ref(null);
const value = computed({ const value = computed({
get() { get() {
@ -53,21 +60,22 @@ const focus = () => {
defineExpose({ defineExpose({
focus, focus,
}); });
import { useAttrs } from 'vue';
const $attrs = useAttrs();
const inputRules = [ const mixinRules = [
requiredFieldRule,
...($attrs.rules ?? []),
(val) => { (val) => {
const { min } = vnInputRef.value.$attrs; const { min } = vnInputRef.value.$attrs;
if (!min) return null;
if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min }); if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min });
}, },
]; ];
</script> </script>
<template> <template>
<div <div @mouseover="hover = true" @mouseleave="hover = false">
@mouseover="hover = true"
@mouseleave="hover = false"
:rules="$attrs.required ? [requiredFieldRule] : null"
>
<QInput <QInput
ref="vnInputRef" ref="vnInputRef"
v-model="value" v-model="value"
@ -76,7 +84,7 @@ const inputRules = [
:class="{ required: $attrs.required }" :class="{ required: $attrs.required }"
@keyup.enter="emit('keyup.enter')" @keyup.enter="emit('keyup.enter')"
:clearable="false" :clearable="false"
:rules="inputRules" :rules="mixinRules"
:lazy-rules="true" :lazy-rules="true"
hide-bottom-space hide-bottom-space
> >
@ -88,8 +96,13 @@ const inputRules = [
<QIcon <QIcon
name="close" name="close"
size="xs" size="xs"
v-if="hover && value && !$attrs.disabled" v-if="hover && value && !$attrs.disabled && $props.clearable"
@click="value = null" @click="
() => {
value = null;
emit('remove');
}
"
></QIcon> ></QIcon>
<QIcon v-if="info" name="info"> <QIcon v-if="info" name="info">
<QTooltip max-width="350px"> <QTooltip max-width="350px">

View File

@ -1,80 +1,31 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { onMounted, watch, computed, ref } from 'vue';
import isValidDate from 'filters/isValidDate'; import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
const props = defineProps({ const model = defineModel({ type: [String, Date] });
modelValue: { const $props = defineProps({
type: String,
default: null,
},
readonly: {
type: Boolean,
default: false,
},
isOutlined: { isOutlined: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
emitDateFormat: {
type: Boolean,
default: false,
},
});
const hover = ref(false);
const emit = defineEmits(['update:modelValue']);
const joinDateAndTime = (date, time) => {
if (!date) {
return null;
}
if (!time) {
return new Date(date).toISOString();
}
const [year, month, day] = date.split('/');
return new Date(`${year}-${month}-${day}T${time}`).toISOString();
};
const time = computed(() => (props.modelValue ? props.modelValue.split('T')?.[1] : null));
const value = computed({
get() {
return props.modelValue;
},
set(value) {
emit(
'update:modelValue',
props.emitDateFormat ? new Date(value) : joinDateAndTime(value, time.value)
);
},
}); });
const isPopupOpen = ref(false); const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const onDateUpdate = (date) => { const dateFormat = 'DD/MM/YYYY';
value.value = date; const isPopupOpen = ref();
isPopupOpen.value = false; const hover = ref();
}; const mask = ref();
const padDate = (value) => value.toString().padStart(2, '0'); onMounted(() => {
const formatDate = (dateString) => { // fix quasar bug
const date = new Date(dateString || ''); mask.value = '##/##/####';
return `${date.getFullYear()}/${padDate(date.getMonth() + 1)}/${padDate(
date.getDate()
)}`;
};
const displayDate = (dateString) => {
if (!dateString || !isValidDate(dateString)) {
return '';
}
return new Date(dateString).toLocaleDateString([], {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}); });
};
const styleAttrs = computed(() => { const styleAttrs = computed(() => {
return props.isOutlined return $props.isOutlined
? { ? {
dense: true, dense: true,
outlined: true, outlined: true,
@ -82,44 +33,114 @@ const styleAttrs = computed(() => {
} }
: {}; : {};
}); });
const formattedDate = computed({
get() {
if (!model.value) return model.value;
return date.formatDate(new Date(model.value), dateFormat);
},
set(value) {
if (value == model.value) return;
let newDate;
if (value) {
// parse input
if (value.includes('/')) {
if (value.length == 6) value = value + new Date().getFullYear();
if (value.length >= 10) {
if (value.at(2) == '/') value = value.split('/').reverse().join('/');
value = date.formatDate(
new Date(value).toISOString(),
'YYYY-MM-DDTHH:mm:ss.SSSZ'
);
}
}
const [year, month, day] = value.split('-').map((e) => parseInt(e));
newDate = new Date(year, month - 1, day);
if (model.value) {
const orgDate =
model.value instanceof Date ? model.value : new Date(model.value);
newDate.setHours(
orgDate.getHours(),
orgDate.getMinutes(),
orgDate.getSeconds(),
orgDate.getMilliseconds()
);
}
}
if (!isNaN(newDate)) model.value = newDate.toISOString();
},
});
const popupDate = computed(() =>
model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value
);
watch(
() => model.value,
(val) => (formattedDate.value = val),
{ immediate: true }
);
</script> </script>
<template> <template>
<div @mouseover="hover = true" @mouseleave="hover = false"> <div @mouseover="hover = true" @mouseleave="hover = false">
<QInput <QInput
v-model="formattedDate"
class="vn-input-date" class="vn-input-date"
readonly :mask="mask"
:model-value="displayDate(value)" placeholder="dd/mm/aaaa"
v-bind="{ ...$attrs, ...styleAttrs }" v-bind="{ ...$attrs, ...styleAttrs }"
@click="isPopupOpen = true" :class="{ required: $attrs.required }"
:rules="$attrs.required ? [requiredFieldRule] : null"
:clearable="false"
> >
<template #append> <template #append>
<QIcon <QIcon
name="close" name="close"
size="xs" size="xs"
v-if="hover && value && !readonly" v-if="
@click="onDateUpdate(null)" ($attrs.clearable == undefined || $attrs.clearable) &&
></QIcon> hover &&
<QIcon name="event" class="cursor-pointer"> model &&
<QPopupProxy !$attrs.disable
v-model="isPopupOpen" "
cover @click="
model = null;
isPopupOpen = false;
"
/>
<QIcon
name="event"
class="cursor-pointer"
@click="isPopupOpen = !isPopupOpen"
:title="t('Open date')"
/>
</template>
<QMenu
transition-show="scale" transition-show="scale"
transition-hide="scale" transition-hide="scale"
:no-parent-event="props.readonly" v-model="isPopupOpen"
anchor="bottom left"
self="top start"
:no-focus="true"
:no-parent-event="true"
> >
<QDate <QDate
v-model="popupDate"
:landscape="true"
:today-btn="true" :today-btn="true"
:model-value="formatDate(value)" @update:model-value="
@update:model-value="onDateUpdate" (date) => {
formattedDate = date;
isPopupOpen = false;
}
"
/> />
</QPopupProxy> </QMenu>
</QIcon>
</template>
</QInput> </QInput>
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
.vn-input-date.q-field--standard.q-field--readonly .q-field__control:before { .vn-input-date.q-field--standard.q-field--readonly .q-field__control:before {
border-bottom-style: solid; border-bottom-style: solid;
@ -129,3 +150,7 @@ const styleAttrs = computed(() => {
border-style: solid; border-style: solid;
} }
</style> </style>
<i18n>
es:
Open date: Abrir fecha
</i18n>

View File

@ -0,0 +1,8 @@
<script setup>
import VnInput from 'src/components/common/VnInput.vue';
const model = defineModel({ type: [Number, String] });
</script>
<template>
<VnInput v-bind="$attrs" v-model.number="model" type="number" />
</template>

View File

@ -1,14 +1,11 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import isValidDate from 'filters/isValidDate'; import { date } from 'quasar';
const model = defineModel({ type: String });
const props = defineProps({ const props = defineProps({
modelValue: { timeOnly: {
type: String,
default: null,
},
readonly: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
@ -17,42 +14,13 @@ const props = defineProps({
default: false, default: false,
}, },
}); });
const initialDate = ref(model.value ?? Date.vnNew());
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits(['update:modelValue']); const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const value = computed({ const dateFormat = 'HH:mm';
get() { const isPopupOpen = ref();
return props.modelValue; const hover = ref();
},
set(value) {
const [hours, minutes] = value.split(':');
const date = new Date(props.modelValue);
date.setHours(Number.parseInt(hours) || 0, Number.parseInt(minutes) || 0, 0, 0);
emit('update:modelValue', value ? date.toISOString() : null);
},
});
const isPopupOpen = ref(false);
const onDateUpdate = (date) => {
internalValue.value = date;
};
const save = () => {
value.value = internalValue.value;
};
const formatTime = (dateString) => {
if (!dateString || !isValidDate(dateString)) {
return '';
}
const date = new Date(dateString || '');
return date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
};
const internalValue = ref(formatTime(value));
const styleAttrs = computed(() => { const styleAttrs = computed(() => {
return props.isOutlined return props.isOutlined
@ -63,52 +31,89 @@ const styleAttrs = computed(() => {
} }
: {}; : {};
}); });
</script>
const formattedTime = computed({
get() {
if (!model.value || model.value?.length <= 5) return model.value;
return dateToTime(model.value);
},
set(value) {
if (value == model.value) return;
let time = value;
if (time) {
if (time?.length > 5) time = dateToTime(time);
else {
if (time.length == 1 && parseInt(time) > 2) time = time.padStart(2, '0');
time = time.padEnd(5, '0');
if (!time.includes(':'))
time = time.substring(0, 2) + ':' + time.substring(3, 5);
}
if (!props.timeOnly) {
const [hh, mm] = time.split(':');
const date = new Date(model.value ? model.value : initialDate.value);
date.setHours(hh, mm, 0);
time = date?.toISOString();
}
}
model.value = time;
},
});
function dateToTime(newDate) {
return date.formatDate(new Date(newDate), dateFormat);
}
</script>
<template> <template>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput <QInput
class="vn-input-time" class="vn-input-time"
readonly mask="##:##"
:model-value="formatTime(value)" placeholder="--:--"
v-model="formattedTime"
v-bind="{ ...$attrs, ...styleAttrs }" v-bind="{ ...$attrs, ...styleAttrs }"
@click="isPopupOpen = true" :class="{ required: $attrs.required }"
style="min-width: 100px"
:rules="$attrs.required ? [requiredFieldRule] : null"
@click="isPopupOpen = false"
type="time"
> >
<template #append> <template #append>
<QIcon name="Schedule" class="cursor-pointer"> <QIcon
<QPopupProxy name="close"
v-model="isPopupOpen" size="xs"
cover v-if="
($attrs.clearable == undefined || $attrs.clearable) &&
hover &&
model &&
!$attrs.disable
"
@click="
model = null;
isPopupOpen = false;
"
/>
<QIcon
name="Schedule"
class="cursor-pointer"
@click="isPopupOpen = !isPopupOpen"
:title="t('Open time')"
/>
</template>
<QMenu
transition-show="scale" transition-show="scale"
transition-hide="scale" transition-hide="scale"
:no-parent-event="props.readonly" v-model="isPopupOpen"
anchor="bottom left"
self="top start"
:no-focus="true"
:no-parent-event="true"
> >
<QTime <QTime v-model="formattedTime" mask="HH:mm" landscape now-btn />
:format24h="false" </QMenu>
:model-value="formatTime(value)"
@update:model-value="onDateUpdate"
>
<div class="row items-center justify-end q-gutter-sm">
<QBtn
:label="t('Cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
label="Ok"
color="primary"
flat
@click="save"
v-close-popup
/>
</div>
</QTime>
</QPopupProxy>
</QIcon>
</template>
</QInput> </QInput>
</div>
</template> </template>
<style lang="scss"> <style lang="scss">
.vn-input-time.q-field--standard.q-field--readonly .q-field__control:before { .vn-input-time.q-field--standard.q-field--readonly .q-field__control:before {
border-bottom-style: solid; border-bottom-style: solid;
@ -118,8 +123,12 @@ const styleAttrs = computed(() => {
border-style: solid; border-style: solid;
} }
</style> </style>
<style lang="scss" scoped>
:deep(input[type='time']::-webkit-calendar-picker-indicator) {
display: none;
}
</style>
<i18n> <i18n>
es: es:
Cancel: Cancelar Open time: Abrir tiempo
</i18n> </i18n>

View File

@ -1,123 +1,33 @@
<script setup> <script setup>
import { ref, toRefs, computed, watch, onMounted } from 'vue';
import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue'; import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import VnSelectDialog from 'components/common/VnSelectDialog.vue'; import VnSelectDialog from 'components/common/VnSelectDialog.vue';
import FetchData from 'components/FetchData.vue';
const emit = defineEmits(['update:modelValue', 'update:options']);
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
const postcodesOptions = ref([]); const value = defineModel({ type: [String, Number, Object] });
const postcodesRef = ref(null);
const $props = defineProps({
modelValue: {
type: [String, Number, Object],
default: null,
},
options: {
type: Array,
default: () => [],
},
optionLabel: {
type: String,
default: '',
},
optionValue: {
type: String,
default: '',
},
filterOptions: {
type: Array,
default: () => [],
},
isClearable: {
type: Boolean,
default: true,
},
defaultFilter: {
type: Boolean,
default: true,
},
});
const { options } = toRefs($props);
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const value = computed({
get() {
return $props.modelValue;
},
set(value) {
emit(
'update:modelValue',
postcodesOptions.value.find((p) => p.code === value)
);
},
});
onMounted(() => {
locationFilter($props.modelValue);
});
function setOptions(data) {
myOptions.value = JSON.parse(JSON.stringify(data));
myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
}
setOptions(options.value);
watch(options, (newValue) => {
setOptions(newValue);
});
function showLabel(data) { function showLabel(data) {
return `${data.code} - ${data.town}(${data.province}), ${data.country}`; return `${data.code} - ${data.town}(${data.province}), ${data.country}`;
} }
function locationFilter(search = '') {
if (
search &&
(search.includes('undefined') || search.startsWith(`${$props.modelValue} - `))
)
return;
let where = { search };
postcodesRef.value.fetch({ filter: { where }, limit: 30 });
}
function handleFetch(data) {
postcodesOptions.value = data;
}
function onDataSaved(newPostcode) {
postcodesOptions.value.push(newPostcode);
value.value = newPostcode.code;
}
</script> </script>
<template> <template>
<FetchData
ref="postcodesRef"
url="Postcodes/filter"
@on-fetch="(data) => handleFetch(data)"
/>
<VnSelectDialog <VnSelectDialog
v-if="postcodesRef"
:option-label="(opt) => showLabel(opt) ?? 'code'"
:option-value="(opt) => opt.code"
v-model="value" v-model="value"
:options="postcodesOptions" option-value="code"
option-filter-value="search"
:option-label="(opt) => showLabel(opt)"
url="Postcodes/filter"
:use-like="false"
:label="t('Location')" :label="t('Location')"
:placeholder="t('search_by_postalcode')" :placeholder="t('search_by_postalcode')"
@input-value="locationFilter"
:default-filter="false"
:input-debounce="300" :input-debounce="300"
:class="{ required: $attrs.required }" :class="{ required: $attrs.required }"
v-bind="$attrs" v-bind="$attrs"
clearable clearable
:emit-value="false"
> >
<template #form> <template #form>
<CreateNewPostcode <CreateNewPostcode @on-data-saved="(newValue) => (value = newValue)" />
@on-data-saved="onDataSaved"
/>
</template> </template>
<template #option="{ itemProps, opt }"> <template #option="{ itemProps, opt }">
<QItem v-bind="itemProps"> <QItem v-bind="itemProps">

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, onUnmounted } from 'vue'; import { ref, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { date } from 'quasar'; import { date } from 'quasar';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
@ -19,6 +19,7 @@ const stateStore = useStateStore();
const validationsStore = useValidator(); const validationsStore = useValidator();
const { models } = validationsStore; const { models } = validationsStore;
const route = useRoute(); const route = useRoute();
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
model: { model: {
@ -49,6 +50,7 @@ const filter = {
'changedModelId', 'changedModelId',
'changedModelValue', 'changedModelValue',
'description', 'description',
'summaryId',
], ],
include: [ include: [
{ {
@ -212,7 +214,7 @@ function getLogTree(data) {
} }
nLogs++; nLogs++;
modelLog.logs.push(log); modelLog.logs.push(log);
modelLog.summaryId = modelLog.logs[0].summaryId;
// Changes // Changes
const notDelete = log.action != 'delete'; const notDelete = log.action != 'delete';
const olds = (notDelete ? log.oldInstance : null) || {}; const olds = (notDelete ? log.oldInstance : null) || {};
@ -380,6 +382,13 @@ setLogTree();
onUnmounted(() => { onUnmounted(() => {
stateStore.rightDrawer = false; stateStore.rightDrawer = false;
}); });
watch(
() => router.currentRoute.value.params.id,
() => {
applyFilter();
}
);
</script> </script>
<template> <template>
<FetchData <FetchData
@ -398,9 +407,12 @@ onUnmounted(() => {
@on-fetch=" @on-fetch="
(data) => (data) =>
(actions = data.map((item) => { (actions = data.map((item) => {
const changedModel = item.changedModel;
return { return {
locale: useCapitalize(validations[item.changedModel].locale.name), locale: useCapitalize(
value: item.changedModel, validations[changedModel]?.locale?.name ?? changedModel
),
value: changedModel,
}; };
})) }))
" "
@ -459,16 +471,21 @@ onUnmounted(() => {
:style="{ :style="{
backgroundColor: useColor(modelLog.model), backgroundColor: useColor(modelLog.model),
}" }"
:title="modelLog.model" :title="`${modelLog.model} #${modelLog.id}`"
> >
{{ t(modelLog.modelI18n) }} {{ t(modelLog.modelI18n) }}
</QChip> </QChip>
<span class="model-id" v-if="modelLog.id"
>#{{ modelLog.id }}</span <span
> class="model-id q-mr-xs"
<span class="model-value" :title="modelLog.showValue"> v-if="modelLog.summaryId"
{{ modelLog.showValue }} v-text="`#${modelLog.summaryId}`"
</span> />
<span
class="model-value"
:title="modelLog.showValue"
v-text="modelLog.showValue"
/>
<QBtn <QBtn
flat flat
round round

View File

@ -0,0 +1,97 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
const { t } = useI18n();
const $props = defineProps({
progress: {
type: Number, //Progress value (1.0 > x > 0.0)
required: true,
},
showDialog: {
type: Boolean,
required: true,
},
cancelled: {
type: Boolean,
required: false,
default: false,
},
});
const emit = defineEmits(['cancel', 'close']);
const dialogRef = ref(null);
const _showDialog = computed({
get: () => $props.showDialog,
set: (value) => {
if (value) dialogRef.value.show();
},
});
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">
<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')" />
</QCardSection>
<QCardSection>
<div class="column">
<span>{{ t('Total progress') }}:</span>
<QLinearProgress
size="30px"
:value="_progress"
color="primary"
stripe
class="q-mt-sm q-mb-md"
>
<div class="absolute-full flex flex-center">
<QBadge
v-if="cancelled"
text-color="white"
color="negative"
:label="t('Cancelled')"
/>
<span v-else class="text-white text-subtitle1">
{{ progressLabel }}
</span>
</div>
</QLinearProgress>
<slot />
</div>
</QCardSection>
<QCardActions align="right">
<QBtn
v-if="!cancelled && progress < 1"
type="button"
flat
class="text-primary"
@click="cancel()"
>
{{ t('globals.cancel') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
</template>
<i18n>
es:
Progress: Progreso
Total progress: Progreso total
Cancelled: Cancelado
</i18n>

View File

@ -1,8 +1,16 @@
<script setup> <script setup>
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'components/LeftMenu.vue'; import LeftMenu from 'components/LeftMenu.vue';
import { onMounted } from 'vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const $props = defineProps({
leftDrawer: {
type: Boolean,
default: true,
},
});
onMounted(() => (stateStore.leftDrawer = $props.leftDrawer));
</script> </script>
<template> <template>

View File

@ -1,9 +1,8 @@
<script setup> <script setup>
import { ref, toRefs, computed, watch } from 'vue'; import { ref, toRefs, computed, watch, onMounted } from 'vue';
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
const emit = defineEmits(['update:modelValue', 'update:options']); const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
const $props = defineProps({ const $props = defineProps({
modelValue: { modelValue: {
@ -26,9 +25,13 @@ const $props = defineProps({
type: String, type: String,
default: null, default: null,
}, },
optionFilterValue: {
type: String,
default: null,
},
url: { url: {
type: String, type: String,
default: '', default: null,
}, },
filterOptions: { filterOptions: {
type: [Array], type: [Array],
@ -46,6 +49,10 @@ const $props = defineProps({
type: Array, type: Array,
default: null, default: null,
}, },
include: {
type: [Object, Array],
default: null,
},
where: { where: {
type: Object, type: Object,
default: null, default: null,
@ -58,37 +65,80 @@ const $props = defineProps({
type: [Number, String], type: [Number, String],
default: '30', default: '30',
}, },
focusOnMount: {
type: Boolean,
default: false,
},
useLike: {
type: Boolean,
default: true,
},
params: {
type: Object,
default: null,
},
noOne: {
type: Boolean,
default: false,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
const requiredFieldRule = (val) => val ?? t('globals.fieldRequired'); const requiredFieldRule = (val) => val ?? t('globals.fieldRequired');
const { optionLabel, optionValue, optionFilter, options, modelValue } = toRefs($props); const { optionLabel, optionValue, optionFilter, optionFilterValue, options, modelValue } =
toRefs($props);
const myOptions = ref([]); const myOptions = ref([]);
const myOptionsOriginal = ref([]); const myOptionsOriginal = ref([]);
const vnSelectRef = ref(); const vnSelectRef = ref();
const dataRef = ref(); const dataRef = ref();
const lastVal = ref();
const noOneText = t('globals.noOne');
const noOneOpt = ref({
[optionValue.value]: false,
[optionLabel.value]: noOneText,
});
const value = computed({ const value = computed({
get() { get() {
return $props.modelValue; return $props.modelValue;
}, },
set(value) { set(value) {
setOptions(myOptionsOriginal.value);
emit('update:modelValue', value); emit('update:modelValue', value);
}, },
}); });
watch(options, (newValue) => {
setOptions(newValue);
});
watch(modelValue, async (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
await fetchFilter(newValue);
if ($props.noOne) myOptions.value.unshift(noOneOpt.value);
});
onMounted(() => {
setOptions(options.value);
if ($props.url && $props.modelValue && !findKeyInOptions())
fetchFilter($props.modelValue);
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
function findKeyInOptions() {
if (!$props.options) return;
return filter($props.modelValue, $props.options)?.length;
}
function setOptions(data) { function setOptions(data) {
myOptions.value = JSON.parse(JSON.stringify(data)); myOptions.value = JSON.parse(JSON.stringify(data));
myOptionsOriginal.value = JSON.parse(JSON.stringify(data)); myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
} }
onMounted(() => {
setOptions(options.value);
if ($props.url && $props.modelValue) fetchFilter($props.modelValue);
});
function filter(val, options) { function filter(val, options) {
const search = val.toString().toLowerCase(); const search = val?.toString()?.toLowerCase();
if (!search) return options; if (!search) return options;
@ -100,7 +150,8 @@ function filter(val, options) {
}); });
} }
const id = row.id; if (!row) return;
const id = row[$props.optionValue];
const optionLabel = String(row[$props.optionLabel]).toLowerCase(); const optionLabel = String(row[$props.optionLabel]).toLowerCase();
return id == search || optionLabel.includes(search); return id == search || optionLabel.includes(search);
@ -110,25 +161,49 @@ function filter(val, options) {
async function fetchFilter(val) { async function fetchFilter(val) {
if (!$props.url || !dataRef.value) return; if (!$props.url || !dataRef.value) return;
const { fields, sortBy, limit } = $props; const { fields, include, sortBy, limit } = $props;
let key = optionLabel.value; const key =
optionFilterValue.value ??
(new RegExp(/\d/g).test(val)
? optionValue.value
: optionFilter.value ?? optionLabel.value);
if (new RegExp(/\d/g).test(val)) key = optionFilter.value ?? optionValue.value; let defaultWhere = {};
if ($props.filterOptions.length) {
const where = { ...{ [key]: { like: `%${val}%` } }, ...$props.where }; defaultWhere = $props.filterOptions.reduce((obj, prop) => {
const fetchOptions = { where, order: sortBy, limit }; if (!obj.or) obj.or = [];
obj.or.push({ [prop]: getVal(val) });
return obj;
}, {});
} else defaultWhere = { [key]: getVal(val) };
const where = { ...(val ? defaultWhere : {}), ...$props.where };
const fetchOptions = { where, include, limit };
if (fields) fetchOptions.fields = fields; if (fields) fetchOptions.fields = fields;
if (sortBy) fetchOptions.order = sortBy;
return dataRef.value.fetch(fetchOptions); return dataRef.value.fetch(fetchOptions);
} }
async function filterHandler(val, update) { async function filterHandler(val, update) {
if (!$props.defaultFilter) return update(); if (!val && lastVal.value === val) {
lastVal.value = val;
return update();
}
lastVal.value = val;
let newOptions; let newOptions;
if ($props.url) {
if (!$props.defaultFilter) return update();
if (
$props.url &&
($props.limit || (!$props.limit && Object.keys(myOptions.value).length === 0))
) {
newOptions = await fetchFilter(val); newOptions = await fetchFilter(val);
} else newOptions = filter(val, myOptionsOriginal.value); } else newOptions = filter(val, myOptionsOriginal.value);
update( update(
() => { () => {
if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase()))
newOptions.unshift(noOneOpt.value);
myOptions.value = newOptions; myOptions.value = newOptions;
}, },
(ref) => { (ref) => {
@ -140,14 +215,11 @@ async function filterHandler(val, update) {
); );
} }
watch(options, (newValue) => { function nullishToTrue(value) {
setOptions(newValue); return value ?? true;
}); }
watch(modelValue, (newValue) => { const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue);
});
</script> </script>
<template> <template>
@ -159,6 +231,7 @@ watch(modelValue, (newValue) => {
:limit="limit" :limit="limit"
:sort-by="sortBy" :sort-by="sortBy"
:fields="fields" :fields="fields"
:params="params"
/> />
<QSelect <QSelect
v-model="value" v-model="value"
@ -166,12 +239,12 @@ watch(modelValue, (newValue) => {
:option-label="optionLabel" :option-label="optionLabel"
:option-value="optionValue" :option-value="optionValue"
v-bind="$attrs" v-bind="$attrs"
emit-value
map-options
use-input
@filter="filterHandler" @filter="filterHandler"
hide-selected :emit-value="nullishToTrue($attrs['emit-value'])"
fill-input :map-options="nullishToTrue($attrs['map-options'])"
:use-input="nullishToTrue($attrs['use-input'])"
:hide-selected="nullishToTrue($attrs['hide-selected'])"
:fill-input="nullishToTrue($attrs['fill-input'])"
ref="vnSelectRef" ref="vnSelectRef"
lazy-rules lazy-rules
:class="{ required: $attrs.required }" :class="{ required: $attrs.required }"
@ -182,7 +255,12 @@ watch(modelValue, (newValue) => {
<QIcon <QIcon
v-show="value" v-show="value"
name="close" name="close"
@click.stop="value = null" @click.stop="
() => {
value = null;
emit('remove');
}
"
class="cursor-pointer" class="cursor-pointer"
size="xs" size="xs"
/> />

View File

@ -0,0 +1,39 @@
<script setup>
import { ref, onBeforeMount, useAttrs } from 'vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const $props = defineProps({
row: {
type: [Object],
default: null,
},
find: {
type: [String, Object],
default: null,
description: 'search in row to add default options',
},
});
const options = ref([]);
onBeforeMount(async () => {
const { url, optionValue, optionLabel } = useAttrs();
const findBy = $props.find ?? url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1);
if (!findBy || !$props.row) return;
// is object
if (typeof findBy == 'object') {
const { value, label } = findBy;
if (!$props.row[value] || !$props.row[label]) return;
return (options.value = [
{
[optionValue ?? 'id']: $props.row[value],
[optionLabel ?? 'name']: $props.row[label],
},
]);
}
// is string
if ($props.row[findBy]) options.value = [$props.row[findBy]];
});
</script>
<template>
<VnSelect v-bind="$attrs" :options="$attrs.options ?? options" />
</template>

View File

@ -1,21 +1,12 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import VnSelect from 'src/components/common/VnSelect.vue';
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const value = defineModel({ type: [String, Number, Object] });
const $props = defineProps({ const $props = defineProps({
modelValue: {
type: [String, Number, Object],
default: null,
},
options: {
type: Array,
default: () => [],
},
rolesAllowedToCreate: { rolesAllowedToCreate: {
type: Array, type: Array,
default: () => ['developer'], default: () => ['developer'],
@ -33,15 +24,6 @@ const $props = defineProps({
const role = useRole(); const role = useRole();
const showForm = ref(false); const showForm = ref(false);
const value = computed({
get() {
return $props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
const isAllowedToCreate = computed(() => { const isAllowedToCreate = computed(() => {
return role.hasAny($props.rolesAllowedToCreate); return role.hasAny($props.rolesAllowedToCreate);
}); });
@ -52,7 +34,11 @@ const toggleForm = () => {
</script> </script>
<template> <template>
<VnSelect v-model="value" :options="options" v-bind="$attrs"> <VnSelect
v-model="value"
v-bind="$attrs"
@update:model-value="(...args) => emit('update:modelValue', ...args)"
>
<template v-if="isAllowedToCreate" #append> <template v-if="isAllowedToCreate" #append>
<QIcon <QIcon
@click.stop.prevent="toggleForm()" @click.stop.prevent="toggleForm()"

View File

@ -184,6 +184,7 @@ en:
minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order
{ orderId } of { shipped } to receive it without additional shipping costs.' { orderId } of { shipped } to receive it without additional shipping costs.'
orderChanges: 'Order {orderId} of { shipped }: { changes }' orderChanges: 'Order {orderId} of { shipped }: { changes }'
productNotAvailable: 'Verdnatura communicates: Your order {ticketFk} with reception date on {landed}. {notAvailables} not available. Sorry for the inconvenience.'
en: English en: English
es: Spanish es: Spanish
fr: French fr: French
@ -203,6 +204,7 @@ es:
Te recomendamos amplíes para no generar costes extra, provocarán un incremento de tu tarifa. Te recomendamos amplíes para no generar costes extra, provocarán un incremento de tu tarifa.
¡Un saludo!' ¡Un saludo!'
orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }' orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }'
productNotAvailable: 'Verdnatura le comunica: Pedido {ticketFk} con fecha de recepción {landed}. {notAvailables} no disponible/s. Disculpe las molestias.'
en: Inglés en: Inglés
es: Español es: Español
fr: Francés fr: Francés
@ -222,6 +224,7 @@ fr:
Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }. Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }.
Merci.' Merci.'
orderChanges: 'Commande {orderId} livraison {landing} indisponible/s. Désolés pour le dérangement.' orderChanges: 'Commande {orderId} livraison {landing} indisponible/s. Désolés pour le dérangement.'
productNotAvailable: 'Verdnatura communique : Votre commande {ticketFk} avec date de réception le {landed}. {notAvailables} non disponible. Nous sommes désolés pour les inconvénients.'
en: Anglais en: Anglais
es: Espagnol es: Espagnol
fr: Français fr: Français
@ -240,6 +243,7 @@ pt:
minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido
{ orderId } do dia { landing } para recebê-lo sem custos de envio adicionais.' { orderId } do dia { landing } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } com chegada dia { landing }: { changes }' orderChanges: 'Pedido { orderId } com chegada dia { landing }: { changes }'
productNotAvailable: 'Verdnatura comunica: Seu pedido {ticketFk} com data de recepção em {landed}. {notAvailables} não disponível/eis. Desculpe pelo transtorno.'
en: Inglês en: Inglês
es: Espanhol es: Espanhol
fr: Francês fr: Francês

View File

@ -7,7 +7,7 @@ defineProps({
</script> </script>
<template> <template>
<div :class="$q.screen.gt.md ? 'q-pb-lg' : 'q-pb-md'"> <div :class="$q.screen.gt.md ? 'q-pb-lg' : 'q-pb-md'">
<div class="header-link"> <div class="header-link" :style="{ cursor: url ? 'pointer' : 'default' }">
<a :href="url" :class="url ? 'link' : 'color-vn-text'"> <a :href="url" :class="url ? 'link' : 'color-vn-text'">
{{ text }} {{ text }}
<QIcon v-if="url" :name="icon" /> <QIcon v-if="url" :name="icon" />

View File

@ -46,7 +46,7 @@ let arrayData;
let store; let store;
let entity; let entity;
const isLoading = ref(false); const isLoading = ref(false);
const isSameDataKey = computed(() => $props.dataKey === route.meta.moduleName);
defineExpose({ getData }); defineExpose({ getData });
onBeforeMount(async () => { onBeforeMount(async () => {
@ -56,12 +56,19 @@ onBeforeMount(async () => {
skip: 0, skip: 0,
}); });
store = arrayData.store; store = arrayData.store;
entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data)); entity = computed(() => {
const data = (Array.isArray(store.data) ? store.data[0] : store.data) ?? {};
if (data) emit('onFetch', data);
return data;
});
// It enables to load data only once if the module is the same as the dataKey // It enables to load data only once if the module is the same as the dataKey
if ($props.dataKey !== route.meta.moduleName || !route.params.id) await getData(); if (!isSameDataKey.value || !route.params.id) await getData();
watch( watch(
() => [$props.url, $props.filter], () => [$props.url, $props.filter],
async () => await getData() async () => {
if (!isSameDataKey.value) await getData();
}
); );
}); });
@ -77,14 +84,50 @@ async function getData() {
isLoading.value = false; isLoading.value = false;
} }
} }
function getValueFromPath(path) {
if (!path) return;
const keys = path.toString().split('.');
let current = entity.value;
for (const key of keys) {
if (current[key] === undefined) return undefined;
else current = current[key];
}
return current;
}
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
const iconModule = computed(() => route.matched[1].meta.icon);
const toModule = computed(() =>
route.matched[1].path.split('/').length > 2
? route.matched[1].redirect
: route.matched[1].children[0].redirect
);
</script> </script>
<template> <template>
<div class="descriptor"> <div class="descriptor">
<template v-if="entity && !isLoading"> <template v-if="entity && !isLoading">
<div class="header bg-primary q-pa-sm justify-between"> <div class="header bg-primary q-pa-sm justify-between">
<slot name="header-extra-action" /> <slot name="header-extra-action"
><QBtn
round
flat
dense
size="md"
:icon="iconModule"
color="white"
class="link"
:to="$attrs['to-module'] ?? toModule"
>
<QTooltip>
{{ t('globals.goToModuleIndex') }}
</QTooltip>
</QBtn></slot
>
<QBtn <QBtn
@click.stop="viewSummary(entity.id, $props.summary)" @click.stop="viewSummary(entity.id, $props.summary)"
round round
@ -127,9 +170,9 @@ const emit = defineEmits(['onFetch']);
<QTooltip> <QTooltip>
{{ t('components.cardDescriptor.moreOptions') }} {{ t('components.cardDescriptor.moreOptions') }}
</QTooltip> </QTooltip>
<QMenu> <QMenu ref="menuRef">
<QList> <QList>
<slot name="menu" :entity="entity" /> <slot name="menu" :entity="entity" :menu-ref="menuRef" />
</QList> </QList>
</QMenu> </QMenu>
</QBtn> </QBtn>
@ -139,8 +182,8 @@ const emit = defineEmits(['onFetch']);
<QList dense> <QList dense>
<QItemLabel header class="ellipsis text-h5" :lines="1"> <QItemLabel header class="ellipsis text-h5" :lines="1">
<div class="title"> <div class="title">
<span v-if="$props.title" :title="$props.title"> <span v-if="$props.title" :title="getValueFromPath(title)">
{{ entity[title] ?? $props.title }} {{ getValueFromPath(title) ?? $props.title }}
</span> </span>
<slot v-else name="description" :entity="entity"> <slot v-else name="description" :entity="entity">
<span :title="entity.name"> <span :title="entity.name">
@ -151,7 +194,7 @@ const emit = defineEmits(['onFetch']);
</QItemLabel> </QItemLabel>
<QItem dense> <QItem dense>
<QItemLabel class="subtitle" caption> <QItemLabel class="subtitle" caption>
#{{ $props.subtitle ?? entity.id }} #{{ getValueFromPath(subtitle) ?? entity.id }}
</QItemLabel> </QItemLabel>
</QItem> </QItem>
</QList> </QList>

View File

@ -30,7 +30,7 @@ const props = defineProps({
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
const route = useRoute(); const route = useRoute();
const isSummary = ref(); const isSummary = ref();
const arrayData = useArrayData(props.dataKey || route.meta.moduleName, { const arrayData = useArrayData(props.dataKey, {
url: props.url, url: props.url,
filter: props.filter, filter: props.filter,
skip: 0, skip: 0,
@ -187,15 +187,10 @@ function existSummary(routes) {
color: lighten($primary, 20%); color: lighten($primary, 20%);
} }
.q-checkbox { .q-checkbox {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label { & .q-checkbox__label {
margin-left: 31px;
color: var(--vn-text-color); color: var(--vn-text-color);
} }
& .q-checkbox__inner { & .q-checkbox__inner {
position: absolute;
left: 0;
color: var(--vn-label-color); color: var(--vn-label-color);
} }
} }

View File

@ -7,7 +7,7 @@ import VnImg from 'src/components/ui/VnImg.vue';
import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue'; import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import toCurrency from '../../../filters/toCurrency'; import { toCurrency } from 'filters/index';
const DEFAULT_PRICE_KG = 0; const DEFAULT_PRICE_KG = 0;
@ -18,6 +18,10 @@ defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
isCatalog: {
type: Boolean,
default: false,
},
}); });
const dialog = ref(null); const dialog = ref(null);
@ -28,7 +32,7 @@ const dialog = ref(null);
<QCard class="card shadow-6"> <QCard class="card shadow-6">
<div class="img-wrapper"> <div class="img-wrapper">
<VnImg :id="item.id" class="image" /> <VnImg :id="item.id" class="image" />
<div v-if="item.hex" class="item-color-container"> <div v-if="item.hex && isCatalog" class="item-color-container">
<div <div
class="item-color" class="item-color"
:style="{ backgroundColor: `#${item.hex}` }" :style="{ backgroundColor: `#${item.hex}` }"
@ -48,13 +52,18 @@ const dialog = ref(null);
:value="item?.[`value${index + 4}`]" :value="item?.[`value${index + 4}`]"
/> />
</template> </template>
<div v-if="item.minQuantity" class="min-quantity">
<QIcon name="production_quantity_limits" size="xs" />
{{ item.minQuantity }}
</div>
<div class="footer"> <div class="footer">
<div class="price"> <div class="price">
<p> <p v-if="isCatalog">
{{ item.available }} {{ t('to') }} {{ item.available }} {{ t('to') }}
{{ toCurrency(item.price) }} {{ toCurrency(item.price) }}
</p> </p>
<QIcon name="add_circle" class="icon"> <slot name="price" />
<QIcon v-if="isCatalog" name="add_circle" class="icon">
<QTooltip>{{ t('globals.add') }}</QTooltip> <QTooltip>{{ t('globals.add') }}</QTooltip>
<QPopupProxy ref="dialog"> <QPopupProxy ref="dialog">
<OrderCatalogItemDialog <OrderCatalogItemDialog
@ -128,6 +137,11 @@ const dialog = ref(null);
} }
} }
.min-quantity {
text-align: right;
color: $negative !important;
}
.footer { .footer {
.price { .price {
overflow: hidden; overflow: hidden;

View File

@ -52,8 +52,8 @@ const containerClasses = computed(() => {
--calendar-border-current: #84d0e2 2px solid; --calendar-border-current: #84d0e2 2px solid;
--calendar-current-color-dark: #84d0e2; --calendar-current-color-dark: #84d0e2;
// Colores de fondo del calendario en dark mode // Colores de fondo del calendario en dark mode
--calendar-outside-background-dark: #222; --calendar-outside-background-dark: var(--vn-section-color);
--calendar-background-dark: #222; --calendar-background-dark: var(--vn-section-color);
} }
// Clases para modificar el color de fecha seleccionada en componente QCalendarMonth // Clases para modificar el color de fecha seleccionada en componente QCalendarMonth
@ -70,8 +70,26 @@ const containerClasses = computed(() => {
text-transform: capitalize; text-transform: capitalize;
} }
.q-calendar-month__head--workweek,
.q-calendar-month__head--weekday,
// .q-calendar-month__workweek.q-past-day,
.q-calendar-month__week :nth-child(n+6):nth-child(-n+7) {
color: var(--vn-label-color);
}
.q-calendar-month__head--weekdays > div[aria-label='miércoles'] > span {
/* color: transparent; */
visibility: hidden;
// position: absolute;
}
.q-calendar-month__head--weekdays > div[aria-label='miércoles'] > span:after {
content: 'X';
visibility: visible;
left: 33%;
position: absolute;
}
.transparent-background { .transparent-background {
--calendar-background-dark: transparent; // --calendar-background-dark: transparent;
--calendar-background: transparent; --calendar-background: transparent;
--calendar-outside-background-dark: transparent; --calendar-outside-background-dark: transparent;
} }
@ -110,11 +128,6 @@ const containerClasses = computed(() => {
cursor: pointer; cursor: pointer;
} }
} }
.q-calendar-month__week--days > div:nth-child(6),
.q-calendar-month__week--days > div:nth-child(7) {
// Cambia el color de los días sábado y domingo
color: #777777;
}
.q-calendar-month__week--wrapper { .q-calendar-month__week--wrapper {
margin-bottom: 4px; margin-bottom: 4px;
@ -124,6 +137,7 @@ const containerClasses = computed(() => {
height: 32px; height: 32px;
display: flex; display: flex;
justify-content: center; justify-content: center;
color: var(--vn-label-color);
} }
.q-calendar__button--bordered { .q-calendar__button--bordered {
@ -147,7 +161,7 @@ const containerClasses = computed(() => {
.q-calendar-month__head--workweek, .q-calendar-month__head--workweek,
.q-calendar-month__head--weekday.q-calendar__center.q-calendar__ellipsis { .q-calendar-month__head--weekday.q-calendar__center.q-calendar__ellipsis {
text-transform: capitalize; text-transform: capitalize;
color: $color-font-secondary; color: var(--vn-label-color);
font-weight: bold; font-weight: bold;
font-size: 0.8rem; font-size: 0.8rem;
text-align: center; text-align: center;

View File

@ -1,20 +1,14 @@
<template> <template>
<div class="q-pa-md">
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" square /> <QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" square /> <QSkeleton type="QInput" class="col" square />
</div> </div>
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" square /> <QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" square /> <QSkeleton type="QInput" class="col" square />
</div> </div>
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" square /> <QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" square /> <QSkeleton type="QInput" class="col" square />
</div>
<div class="row q-gutter-md">
<QSkeleton type="QBtn" />
<QSkeleton type="QBtn" />
</div>
</div> </div>
</template> </template>

View File

@ -1,45 +1,62 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { useColor } from 'src/composables/useColor'; import { useColor } from 'src/composables/useColor';
import { getCssVar } from 'quasar';
const $props = defineProps({ const $props = defineProps({
workerId: { type: Number, required: true }, workerId: { type: Number, required: true },
description: { type: String, default: null }, description: { type: String, default: null },
size: { type: String, default: null },
title: { type: String, default: null }, title: { type: String, default: null },
color: { type: String, default: null },
}); });
const { getTokenMultimedia } = useSession(); const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); const token = getTokenMultimedia();
const { t } = useI18n(); const { t } = useI18n();
const title = computed(() => $props.title ?? t('globals.system')); const src = computed(
() => `/api/Images/user/160x160/${$props.workerId}/download?access_token=${token}`
);
const title = computed(() => $props.title?.toUpperCase() || t('globals.system'));
const showLetter = ref(false); const showLetter = ref(false);
const backgroundColor = computed(() => {
const color = $props.color || useColor(title.value);
return getCssVar(color) || color;
});
watch(src, () => (showLetter.value = false));
</script> </script>
<template> <template>
<div class="avatar-picture column items-center"> <div class="column items-center">
<QAvatar <QAvatar
:style="{ :style="{ backgroundColor }"
backgroundColor: useColor(title), v-bind="$attrs"
}" :title="title || t('globals.system')"
:size="$props.size"
:title="title"
> >
<template v-if="showLetter">{{ title.charAt(0) }}</template> <template v-if="showLetter">
<QImg {{ title.charAt(0) }}
v-else </template>
:src="`/api/Images/user/160x160/${$props.workerId}/download?access_token=${token}`" <QImg v-else :src="src" spinner-color="white" @error="showLetter = true" />
spinner-color="white"
@error="showLetter = true"
/>
</QAvatar> </QAvatar>
<div class="description"> <div class="description">
<slot name="description" v-if="$props.description"> <slot name="description" v-if="description">
<p> <p v-text="description" />
{{ $props.description }}
</p>
</slot> </slot>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped>
[size='xxl'] {
.q-avatar,
.q-img {
width: 80px;
height: 80px;
}
.q-img {
object-fit: cover;
}
}
</style>

View File

@ -15,7 +15,7 @@ const props = defineProps({
default: null, default: null,
}, },
message: { message: {
type: String, type: [String, Boolean],
default: null, default: null,
}, },
data: { data: {
@ -35,7 +35,10 @@ defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const { dialogRef, onDialogOK } = useDialogPluginComponent(); const { dialogRef, onDialogOK } = useDialogPluginComponent();
const title = props.title || t('Confirm'); const title = props.title || t('Confirm');
const message = props.message || t('Are you sure you want to continue?'); const message =
props.message ||
(props.message !== false ? t('Are you sure you want to continue?') : false);
const isLoading = ref(false); const isLoading = ref(false);
async function confirm() { async function confirm() {
@ -61,12 +64,12 @@ async function confirm() {
size="xl" size="xl"
v-if="icon" v-if="icon"
/> />
<span class="text-h6 text-grey">{{ title }}</span> <span class="text-h6">{{ title }}</span>
<QSpace /> <QSpace />
<QBtn icon="close" :disable="isLoading" flat round dense v-close-popup /> <QBtn icon="close" :disable="isLoading" flat round dense v-close-popup />
</QCardSection> </QCardSection>
<QCardSection class="row items-center"> <QCardSection class="row items-center">
<span v-html="message"></span> <span v-if="message !== false" v-html="message" />
<slot name="customHTML"></slot> <slot name="customHTML"></slot>
</QCardSection> </QCardSection>
<QCardActions align="right"> <QCardActions align="right">

View File

@ -7,15 +7,17 @@ import toDate from 'filters/toDate';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
const { t } = useI18n(); const { t } = useI18n();
const params = defineModel({ default: {}, required: true, type: Object });
const $props = defineProps({ const $props = defineProps({
modelValue: {
type: Object,
default: () => {},
},
dataKey: { dataKey: {
type: String, type: String,
required: true, required: true,
}, },
searchButton: { searchButton: {
type: Boolean, type: Boolean,
required: false,
default: false, default: false,
}, },
showAll: { showAll: {
@ -26,8 +28,8 @@ const $props = defineProps({
type: Array, type: Array,
required: false, required: false,
default: () => [], default: () => [],
description: description: `Some filters come with default search parameters and require a value.
'Algunos filtros vienen con parametros de búsqueda por default y necesitan tener si o si un valor, por eso de ser necesario, esta prop nos sirve para saber que filtros podemos remover y cuales no', This prop helps us determine which filters can be removed and which cannot.`,
}, },
exprBuilder: { exprBuilder: {
type: Function, type: Function,
@ -35,7 +37,7 @@ const $props = defineProps({
}, },
hiddenTags: { hiddenTags: {
type: Array, type: Array,
default: () => ['filter'], default: () => ['filter', 'search', 'or', 'and'],
}, },
customTags: { customTags: {
type: Array, type: Array,
@ -55,62 +57,86 @@ const $props = defineProps({
}, },
}); });
const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']); defineExpose({ search, sanitizer });
const emit = defineEmits([
'update:modelValue',
'refresh',
'clear',
'search',
'init',
'remove',
'setUserParams',
]);
const arrayData = useArrayData($props.dataKey, { const arrayData = useArrayData($props.dataKey, {
exprBuilder: $props.exprBuilder, exprBuilder: $props.exprBuilder,
searchUrl: $props.searchUrl, searchUrl: $props.searchUrl,
navigate: {}, navigate: $props.redirect ? {} : null,
}); });
const route = useRoute(); const route = useRoute();
const store = arrayData.store; const store = arrayData.store;
const userParams = ref({});
onMounted(() => { onMounted(() => {
emit('init', { params: params.value }); userParams.value = $props.modelValue ?? {};
emit('init', { params: userParams.value });
}); });
function setUserParams(watchedParams) { function setUserParams(watchedParams) {
if (!watchedParams) return; if (!watchedParams || Object.keys(watchedParams).length == 0) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
if (typeof watchedParams?.filter == 'string')
watchedParams.filter = JSON.parse(watchedParams.filter);
watchedParams = { ...watchedParams, ...watchedParams.filter?.where }; watchedParams = { ...watchedParams, ...watchedParams.filter?.where };
const order = watchedParams.filter?.order;
delete watchedParams.filter; delete watchedParams.filter;
params.value = { ...params.value, ...watchedParams }; userParams.value = sanitizer(watchedParams);
emit('setUserParams', userParams.value, order);
} }
watch( watch(
() => route.query[$props.searchUrl], () => route.query[$props.searchUrl],
(val) => setUserParams(val) (val, oldValue) => (val || oldValue) && setUserParams(val)
); );
watch( watch(
() => arrayData.store.userParams, () => arrayData.store.userParams,
(val) => setUserParams(val) (val, oldValue) => (val || oldValue) && setUserParams(val)
);
watch(
() => $props.modelValue,
(val) => (userParams.value = val ?? {})
); );
const isLoading = ref(false); const isLoading = ref(false);
async function search(evt) { async function search(evt) {
try {
if (evt && $props.disableSubmitEvent) return; if (evt && $props.disableSubmitEvent) return;
store.filter.where = {}; store.filter.where = {};
isLoading.value = true; isLoading.value = true;
const filter = { ...params.value }; const filter = { ...userParams.value, ...$props.modelValue };
store.userParamsChanged = true; store.userParamsChanged = true;
store.filter.skip = 0; const { params: newParams } = await arrayData.addFilter({
store.skip = 0; params: filter,
const { params: newParams } = await arrayData.addFilter({ params: params.value }); });
params.value = newParams; userParams.value = newParams;
if (!$props.showAll && !Object.values(filter).length) store.data = []; if (!$props.showAll && !Object.values(filter).length) store.data = [];
isLoading.value = false;
emit('search'); emit('search');
} finally {
isLoading.value = false;
}
} }
async function reload() { async function reload() {
isLoading.value = true; isLoading.value = true;
const params = Object.values(params.value).filter((param) => param); const params = Object.values(userParams.value).filter((param) => param);
store.skip = 0;
store.page = 1;
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!$props.showAll && !params.length) store.data = []; if (!$props.showAll && !params.length) store.data = [];
isLoading.value = false; isLoading.value = false;
@ -118,35 +144,37 @@ async function reload() {
} }
async function clearFilters() { async function clearFilters() {
try {
isLoading.value = true; isLoading.value = true;
store.userParamsChanged = true; store.userParamsChanged = true;
store.filter.skip = 0; arrayData.reset(['skip', 'filter.skip', 'page']);
store.skip = 0;
// Filtrar los params no removibles // Filtrar los params no removibles
const removableFilters = Object.keys(params.value).filter((param) => const removableFilters = Object.keys(userParams.value).filter((param) =>
$props.unremovableParams.includes(param) $props.unremovableParams.includes(param)
); );
const newParams = {}; const newParams = {};
// Conservar solo los params que no son removibles // Conservar solo los params que no son removibles
for (const key of removableFilters) { for (const key of removableFilters) {
newParams[key] = params.value[key]; newParams[key] = userParams.value[key];
} }
params.value = {}; userParams.value = {};
params.value = { ...newParams }; // Actualizar los params con los removibles userParams.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: params.value }); await arrayData.applyFilter({ params: userParams.value });
if (!$props.showAll) { if (!$props.showAll) {
store.data = []; store.data = [];
} }
isLoading.value = false;
emit('clear'); emit('clear');
emit('update:modelValue', userParams.value);
} finally {
isLoading.value = false;
}
} }
const tagsList = computed(() => { const tagsList = computed(() => {
const tagList = []; const tagList = [];
for (const key of Object.keys(params.value)) { for (const key of Object.keys(userParams.value)) {
const value = params.value[key]; const value = userParams.value[key];
if (value == null || ($props.hiddenTags || []).includes(key)) continue; if (value == null || ($props.hiddenTags || []).includes(key)) continue;
tagList.push({ label: key, value }); tagList.push({ label: key, value });
} }
@ -154,16 +182,17 @@ const tagsList = computed(() => {
}); });
const tags = computed(() => { const tags = computed(() => {
return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key)); return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.label));
}); });
const customTags = computed(() => const customTags = computed(() =>
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key)) tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label))
); );
async function remove(key) { async function remove(key) {
params.value[key] = undefined; userParams.value[key] = undefined;
search(); search();
emit('remove', key); emit('remove', key);
emit('update:modelValue', userParams.value);
} }
function formatValue(value) { function formatValue(value) {
@ -172,9 +201,27 @@ function formatValue(value) {
return `"${value}"`; return `"${value}"`;
} }
function sanitizer(params) {
for (const [key, value] of Object.entries(params)) {
if (value && typeof value === 'object') {
const param = Object.values(value)[0];
if (typeof param == 'string') params[key] = param.replaceAll('%', '');
}
}
return params;
}
</script> </script>
<template> <template>
<QBtn
class="q-mt-lg q-mr-xs q-mb-lg"
round
color="primary"
style="position: fixed; z-index: 1; right: 0; bottom: 0"
icon="search"
@click="search()"
></QBtn>
<QForm @submit="search" id="filterPanelForm"> <QForm @submit="search" id="filterPanelForm">
<QList dense> <QList dense>
<QItem class="q-mt-xs"> <QItem class="q-mt-xs">
@ -223,7 +270,7 @@ function formatValue(value) {
<VnFilterPanelChip <VnFilterPanelChip
v-for="chip of tags" v-for="chip of tags"
:key="chip.label" :key="chip.label"
:removable="!unremovableParams.includes(chip.label)" :removable="!unremovableParams?.includes(chip.label)"
@remove="remove(chip.label)" @remove="remove(chip.label)"
> >
<slot name="tags" :tag="chip" :format-fn="formatValue"> <slot name="tags" :tag="chip" :format-fn="formatValue">
@ -236,7 +283,7 @@ function formatValue(value) {
<slot <slot
v-if="$slots.customTags" v-if="$slots.customTags"
name="customTags" name="customTags"
:params="params" :params="userParams"
:tags="customTags" :tags="customTags"
:format-fn="formatValue" :format-fn="formatValue"
:search-fn="search" :search-fn="search"
@ -246,25 +293,8 @@ function formatValue(value) {
<QSeparator /> <QSeparator />
</QList> </QList>
<QList dense class="list q-gutter-y-sm q-mt-sm"> <QList dense class="list q-gutter-y-sm q-mt-sm">
<slot name="body" :params="params" :search-fn="search"></slot> <slot name="body" :params="sanitizer(userParams)" :search-fn="search"></slot>
</QList> </QList>
<template v-if="$props.searchButton">
<QItem>
<QItemSection class="q-py-sm">
<QBtn
:label="t('Search')"
class="full-width"
color="primary"
dense
icon="search"
rounded
:type="disableSubmitEvent ? 'button' : 'submit'"
unelevated
/>
</QItemSection>
</QItem>
<QSeparator />
</template>
</QForm> </QForm>
<QInnerLoading <QInnerLoading
:label="t('globals.pleaseWait')" :label="t('globals.pleaseWait')"

View File

@ -1,60 +1,83 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref } from 'vue';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import noImage from '/no-user.png';
import { useRole } from 'src/composables/useRole';
const $props = defineProps({ const $props = defineProps({
collection: { storage: {
type: [String, Number], type: [String, Number],
default: 'Images', default: 'Images',
}, },
size: { collection: {
type: String,
default: 'catalog',
},
resolution: {
type: String, type: String,
default: '200x200', default: '200x200',
}, },
zoomSize: { zoomResolution: {
type: String, type: String,
required: true, default: null,
default: 'lg', },
zoom: {
type: Boolean,
default: true,
}, },
id: { id: {
type: Boolean, type: Number,
default: false, required: true,
}, },
}); });
const show = ref(false); const show = ref(false);
const token = useSession().getTokenMultimedia(); const token = useSession().getTokenMultimedia();
const timeStamp = ref(`timestamp=${Date.now()}`); const timeStamp = ref(`timestamp=${Date.now()}`);
const url = computed( const isEmployee = useRole().isEmployee();
() =>
`/api/${$props.collection}/catalog/${$props.size}/${$props.id}/download?access_token=${token}&${timeStamp.value}` const getUrl = (zoom = false) => {
); const curResolution = zoom
const emits = defineEmits(['refresh']); ? $props.zoomResolution || $props.resolution
const reload = (emit = false) => { : $props.resolution;
return isEmployee
? `/api/${$props.storage}/${$props.collection}/${curResolution}/${$props.id}/download?access_token=${token}&${timeStamp.value}`
: noImage;
};
const reload = () => {
timeStamp.value = `timestamp=${Date.now()}`; timeStamp.value = `timestamp=${Date.now()}`;
}; };
defineExpose({ defineExpose({
reload, reload,
}); });
onMounted(() => {});
</script> </script>
<template> <template>
<QImg :src="url" v-bind="$attrs" @click="show = !show" spinner-color="primary" /> <QImg
<QDialog v-model="show" v-if="$props.zoomSize"> :class="{ zoomIn: zoom }"
<QImg :src="url" class="img_zoom" v-bind="$attrs" spinner-color="primary" /> :src="getUrl()"
v-bind="$attrs"
@click.stop="show = $props.zoom ? true : false"
spinner-color="primary"
/>
<QDialog v-if="$props.zoom" v-model="show">
<QImg
:src="getUrl(true)"
v-bind="$attrs"
spinner-color="primary"
class="img_zoom"
/>
</QDialog> </QDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-img { .q-img {
&.zoomIn {
cursor: zoom-in; cursor: zoom-in;
} }
min-width: 50px;
}
.rounded { .rounded {
border-radius: 50%; border-radius: 50%;
} }
.img_zoom { .img_zoom {
width: 100%;
height: auto;
border-radius: 0%; border-radius: 0%;
} }
</style> </style>

View File

@ -2,6 +2,7 @@
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useClipboard } from 'src/composables/useClipboard'; import { useClipboard } from 'src/composables/useClipboard';
import { computed } from 'vue';
const $props = defineProps({ const $props = defineProps({
label: { type: String, default: null }, label: { type: String, default: null },
@ -24,52 +25,67 @@ function copyValueText() {
}, },
}); });
} }
const val = computed(() => $props.value);
</script> </script>
<style scoped> <template>
<div class="vn-label-value">
<QCheckbox
v-if="typeof value === 'boolean'"
v-model="val"
:label="label"
disable
dense
/>
<template v-else>
<div v-if="label || $slots.label" class="label">
<slot name="label">
<span>{{ label }}</span>
</slot>
</div>
<div class="value">
<slot name="value">
<span :title="value">
{{ dash ? dashIfEmpty(value) : value }}
</span>
</slot>
</div>
<div class="info" v-if="info">
<QIcon name="info" class="cursor-pointer" size="xs" color="grey">
<QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]">
{{ info }}
</QTooltip>
</QIcon>
</div>
<div class="copy" v-if="copy && value" @click="copyValueText()">
<QIcon name="Content_Copy" color="primary">
<QTooltip>{{ t('globals.copyClipboard') }}</QTooltip>
</QIcon>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.vn-label-value {
&:hover .copy {
visibility: visible;
cursor: pointer;
}
.label, .label,
.value { .value {
white-space: pre-line; white-space: pre-line;
word-wrap: break-word; word-wrap: break-word;
} }
</style>
<template>
<div class="vn-label-value">
<div v-if="$props.label || $slots.label" class="label">
<slot name="label">
<span>{{ $props.label }}</span>
</slot>
</div>
<div class="value">
<slot name="value">
<span :title="$props.value">
{{ $props.dash ? dashIfEmpty($props.value) : $props.value }}
</span>
</slot>
</div>
<div class="info" v-if="$props.info">
<QIcon name="info" class="cursor-pointer" size="xs" color="grey">
<QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]">
{{ $props.info }}
</QTooltip>
</QIcon>
</div>
<div class="copy" v-if="$props.copy && $props.value" @click="copyValueText()">
<QIcon name="Content_Copy" color="primary">
<QTooltip>{{ t('globals.copyClipboard') }}</QTooltip>
</QIcon>
</div>
</div>
</template>
<style lang="scss" scoped>
.vn-label-value:hover .copy {
visibility: visible;
cursor: pointer;
}
.copy { .copy {
visibility: hidden; visibility: hidden;
} }
.info { .info {
margin-left: 5px; margin-left: 5px;
} }
}
:deep(.q-checkbox.disabled) {
opacity: 1 !important;
}
</style> </style>

View File

@ -1,13 +1,18 @@
<script setup> <script setup>
import VnAvatar from 'src/components/ui/VnAvatar.vue';
import { toDateHourMin } from 'src/filters';
import { ref } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { ref } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnPaginate from './VnPaginate.vue'; import { useQuasar } from 'quasar';
import VnUserLink from '../ui/VnUserLink.vue';
import { toDateHourMin } from 'src/filters';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnUserLink from 'components/ui/VnUserLink.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnAvatar from 'components/ui/VnAvatar.vue';
const $props = defineProps({ const $props = defineProps({
url: { type: String, default: null }, url: { type: String, default: null },
filter: { type: Object, default: () => {} }, filter: { type: Object, default: () => {} },
@ -17,6 +22,7 @@ const $props = defineProps({
const { t } = useI18n(); const { t } = useI18n();
const state = useState(); const state = useState();
const quasar = useQuasar();
const currentUser = ref(state.getUser()); const currentUser = ref(state.getUser());
const newNote = ref(''); const newNote = ref('');
const vnPaginateRef = ref(); const vnPaginateRef = ref();
@ -28,11 +34,24 @@ function handleKeyUp(event) {
} }
async function insert() { async function insert() {
const body = $props.body; const body = $props.body;
Object.assign(body, { text: newNote.value }); const newBody = { ...body, ...{ text: newNote.value } };
await axios.post($props.url, body); await axios.post($props.url, newBody);
await vnPaginateRef.value.fetch(); await vnPaginateRef.value.fetch();
newNote.value = ''; newNote.value = '';
} }
onBeforeRouteLeave((to, from, next) => {
if (newNote.value)
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.unsavedPopup.title'),
message: t('globals.unsavedPopup.subtitle'),
promise: () => next(),
},
});
else next();
});
</script> </script>
<template> <template>
<QCard class="q-pa-xs q-mb-xl full-width" v-if="$props.addNote"> <QCard class="q-pa-xs q-mb-xl full-width" v-if="$props.addNote">
@ -78,6 +97,7 @@ async function insert() {
ref="vnPaginateRef" ref="vnPaginateRef"
class="show" class="show"
v-bind="$attrs" v-bind="$attrs"
search-url="notes"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width"> <TransitionGroup name="list" tag="div" class="column items-center full-width">

View File

@ -0,0 +1,32 @@
<script setup>
const emit = defineEmits(['submit']);
defineProps({
icon: { type: String, required: false, default: 'phonelink_lock' },
title: { type: String, required: true },
});
</script>
<template>
<QForm @submit="emit('submit')" class="q-gutter-y-md q-pa-lg formCard">
<div class="column items-center">
<QIcon v-if="icon != false" :name="icon" size="xl" color="primary" />
<h5 class="text-center q-my-md">
{{ title }}
</h5>
</div>
<slot></slot>
<div class="q-mt-lg">
<slot name="buttons"></slot>
</div>
</QForm>
</template>
<style lang="scss" scoped>
.formCard {
max-width: 350px;
min-width: 300px;
}
@media (max-width: $breakpoint-xs-max) {
.formCard {
min-width: 100%;
}
}
</style>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, ref, watch } from 'vue'; import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
@ -10,6 +10,10 @@ const props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
class: {
type: String,
default: '',
},
autoLoad: { autoLoad: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -26,6 +30,10 @@ const props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
userFilter: {
type: Object,
default: null,
},
where: { where: {
type: Object, type: Object,
default: null, default: null,
@ -80,6 +88,7 @@ const pagination = ref({
const arrayData = useArrayData(props.dataKey, { const arrayData = useArrayData(props.dataKey, {
url: props.url, url: props.url,
filter: props.filter, filter: props.filter,
userFilter: props.userFilter,
where: props.where, where: props.where,
limit: props.limit, limit: props.limit,
order: props.order, order: props.order,
@ -95,6 +104,8 @@ onMounted(async () => {
mounted.value = true; mounted.value = true;
}); });
onBeforeUnmount(() => arrayData.reset());
watch( watch(
() => props.data, () => props.data,
() => { () => {
@ -108,8 +119,8 @@ watch(
); );
watch( watch(
() => props.url, () => [props.url, props.filter],
(url) => fetch({ url }) ([url, filter]) => mounted.value && fetch({ url, filter })
); );
const addFilter = async (filter, params) => { const addFilter = async (filter, params) => {
@ -118,8 +129,7 @@ const addFilter = async (filter, params) => {
async function fetch(params) { async function fetch(params) {
useArrayData(props.dataKey, params); useArrayData(props.dataKey, params);
store.filter.skip = 0; arrayData.reset(['filter.skip', 'skip']);
store.skip = 0;
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!store.hasMoreData) { if (!store.hasMoreData) {
isLoading.value = false; isLoading.value = false;
@ -209,13 +219,13 @@ defineExpose({ fetch, addFilter, paginate });
v-if="store.data" v-if="store.data"
@load="onLoad" @load="onLoad"
:offset="offset" :offset="offset"
class="full-width" :class="['full-width', props.class]"
:disable="disableInfiniteScroll || !store.hasMoreData" :disable="disableInfiniteScroll || !store.hasMoreData"
v-bind="$attrs" v-bind="$attrs"
> >
<slot name="body" :rows="store.data"></slot> <slot name="body" :rows="store.data"></slot>
<div v-if="isLoading" class="info-row q-pa-md text-center"> <div v-if="isLoading" class="info-row q-pa-md text-center">
<QSpinner color="orange" size="md" /> <QSpinner color="primary" size="md" />
</div> </div>
</QInfiniteScroll> </QInfiniteScroll>
</template> </template>

View File

@ -1,14 +1,17 @@
<template> <template>
<div class="vn-row q-gutter-md q-mb-md"> <div class="vn-row q-gutter-md q-mb-md">
<slot></slot> <slot />
</div> </div>
</template> </template>
<style lang="scss" scopped> <style lang="scss" scoped>
.vn-row { .vn-row {
display: flex; display: flex;
> * { > :deep(*) {
flex: 1; flex: 1;
} }
&[wrap] {
flex-wrap: wrap;
}
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
.vn-row { .vn-row {

View File

@ -63,13 +63,13 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
makeFetch: { whereFilter: {
type: Boolean, type: Function,
default: true, default: undefined,
}, },
}); });
const searchText = ref(''); const searchText = ref();
let arrayDataProps = { ...props }; let arrayDataProps = { ...props };
if (props.redirect) if (props.redirect)
arrayDataProps = { arrayDataProps = {
@ -100,18 +100,23 @@ onMounted(() => {
}); });
async function search() { async function search() {
const staticParams = Object.entries(store.userParams).filter( const staticParams = Object.entries(store.userParams);
([key, value]) => value && (props.staticParams || []).includes(key) arrayData.reset(['skip', 'page']);
);
store.skip = 0;
if (props.makeFetch) const filter = {
await arrayData.applyFilter({
params: { params: {
...Object.fromEntries(staticParams), ...Object.fromEntries(staticParams),
search: searchText.value, search: searchText.value,
}, },
}); };
if (props.whereFilter) {
filter.filter = {
where: props.whereFilter(searchText.value),
};
delete filter.params.search;
}
await arrayData.applyFilter(filter);
} }
</script> </script>
<template> <template>
@ -119,7 +124,7 @@ async function search() {
<QForm @submit="search" id="searchbarForm"> <QForm @submit="search" id="searchbarForm">
<VnInput <VnInput
id="searchbar" id="searchbar"
v-model="searchText" v-model.trim="searchText"
:placeholder="t(props.label)" :placeholder="t(props.label)"
dense dense
standout standout

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onBeforeMount } from 'vue'; import { computed } from 'vue';
import { date } from 'quasar'; import { date } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnAvatar from '../ui/VnAvatar.vue'; import VnAvatar from '../ui/VnAvatar.vue';
@ -10,7 +10,8 @@ const $props = defineProps({
where: { type: Object, default: () => {} }, where: { type: Object, default: () => {} },
}); });
const filter = { const filter = computed(() => {
return {
fields: ['smsFk'], fields: ['smsFk'],
include: { include: {
relation: 'sms', relation: 'sms',
@ -32,9 +33,9 @@ const filter = {
}, },
}, },
}, },
...{ where: $props.where },
}; };
});
onBeforeMount(() => (filter.where = $props.where));
function formatNumber(number) { function formatNumber(number) {
if (number.length <= 10) return number; if (number.length <= 10) return number;

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onBeforeUnmount, ref } from 'vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
const stateStore = useStateStore(); const stateStore = useStateStore();
const actions = ref(null); const actions = ref(null);
const data = ref(null); const data = ref(null);
@ -24,13 +25,12 @@ onMounted(() => {
if (data.value) observer.observe(data.value, opts); if (data.value) observer.observe(data.value, opts);
}); });
onUnmounted(() => { onBeforeUnmount(() => stateStore.toggleSubToolbar());
stateStore.toggleSubToolbar();
});
</script> </script>
<template> <template>
<QToolbar <QToolbar
id="subToolbar"
class="justify-end sticky" class="justify-end sticky"
v-show="hasContent || $slots['st-actions'] || $slots['st-data']" v-show="hasContent || $slots['st-actions'] || $slots['st-data']"
> >
@ -43,20 +43,9 @@ onUnmounted(() => {
</slot> </slot>
</QToolbar> </QToolbar>
</template> </template>
<style lang="scss">
.q-toolbar {
background: var(--vn-section-color);
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
.sticky { .sticky {
position: sticky; position: sticky;
top: 61px;
z-index: 1; z-index: 1;
} }
@media (max-width: $breakpoint-sm) {
.sticky {
top: 90px;
}
}
</style> </style>

View File

@ -18,4 +18,3 @@ const { t } = useI18n();
</slot> </slot>
<WorkerDescriptorProxy v-if="$props.workerId" :id="$props.workerId" /> <WorkerDescriptorProxy v-if="$props.workerId" :id="$props.workerId" />
</template> </template>
<style scoped></style>

View File

@ -7,5 +7,5 @@ export function getDateQBadgeColor(date) {
let comparation = today - timeTicket; let comparation = today - timeTicket;
if (comparation == 0) return 'warning'; if (comparation == 0) return 'warning';
if (comparation < 0) return 'negative'; if (comparation < 0) return 'success';
} }

View File

@ -1,4 +1,4 @@
import { onMounted, ref, computed } from 'vue'; import { onMounted, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore'; import { useArrayDataStore } from 'stores/useArrayDataStore';
@ -16,20 +16,19 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const router = useRouter(); const router = useRouter();
let canceller = null; let canceller = null;
const page = ref(1);
onMounted(() => { onMounted(() => {
setOptions(); setOptions();
store.skip = 0; reset(['skip']);
const query = route.query; const query = route.query;
const searchUrl = store.searchUrl; const searchUrl = store.searchUrl;
if (query[searchUrl]) { if (query[searchUrl]) {
const params = JSON.parse(query[searchUrl]); const params = JSON.parse(query[searchUrl]);
const filter = params?.filter; const filter = params?.filter && JSON.parse(params?.filter ?? '{}');
delete params.filter; delete params.filter;
store.userParams = { ...params, ...store.userParams }; store.userParams = { ...params, ...store.userParams };
store.userFilter = { ...JSON.parse(filter), ...store.userFilter }; store.userFilter = { ...filter, ...store.userFilter };
if (filter?.order) store.order = filter.order;
} }
}); });
@ -71,9 +70,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
canceller = new AbortController(); canceller = new AbortController();
const filter = { const filter = {
order: store.order,
limit: store.limit, limit: store.limit,
skip: store.skip,
}; };
let exprFilter; let exprFilter;
@ -88,13 +85,19 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
} }
Object.assign(filter, store.userFilter, exprFilter); Object.assign(filter, store.userFilter, exprFilter);
Object.assign(store.filter, filter); let where;
const params = { if (filter?.where || store.filter?.where)
filter: JSON.stringify(store.filter), where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {});
}; Object.assign(filter, store.filter);
filter.where = where;
const params = { filter };
Object.assign(params, userParams); Object.assign(params, userParams);
params.filter.skip = store.skip;
if (store.order && store.order.length) params.filter.order = store.order;
else delete params.filter.order;
params.filter = JSON.stringify(params.filter);
store.currentFilter = params; store.currentFilter = params;
store.isLoading = true; store.isLoading = true;
const response = await axios.get(store.url, { const response = await axios.get(store.url, {
@ -130,6 +133,10 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
delete store[option]; delete store[option];
} }
function reset(opts = []) {
if (arrayDataStore.get(key)) arrayDataStore.reset(key, opts);
}
function cancelRequest() { function cancelRequest() {
if (canceller) { if (canceller) {
canceller.abort(); canceller.abort();
@ -142,60 +149,100 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
store.filter = {}; store.filter = {};
if (params) store.userParams = { ...params }; if (params) store.userParams = { ...params };
const response = await fetch({ append: false }); const response = await fetch({});
return response; return response;
} }
async function addFilter({ filter, params }) { async function addFilter({ filter, params }) {
if (filter) store.userFilter = Object.assign(store.userFilter, filter); if (filter) store.filter = filter;
let userParams = { ...store.userParams, ...params }; let userParams = { ...store.userParams, ...params };
userParams = sanitizerParams(userParams, store?.exprBuilder); userParams = sanitizerParams(userParams, store?.exprBuilder);
store.userParams = userParams; store.userParams = userParams;
store.skip = 0; reset(['skip', 'filter.skip', 'page']);
store.filter.skip = 0;
page.value = 1;
await fetch({ append: false }); await fetch({});
return { filter, params }; return { filter, params };
} }
async function addFilterWhere(where) { async function addFilterWhere(where) {
const storedFilter = { ...store.userFilter }; const storedFilter = { ...store.filter };
if (!storedFilter?.where) storedFilter.where = {}; if (!storedFilter?.where) storedFilter.where = {};
where = { ...storedFilter.where, ...where }; where = { ...storedFilter.where, ...where };
await addFilter({ filter: { where } }); await addFilter({ filter: { where } });
} }
async function addOrder(field, direction = 'ASC') {
const newOrder = field + ' ' + direction;
let order = store.order || [];
if (typeof order == 'string') order = [order];
let index = order.findIndex((o) => o.split(' ')[0] === field);
if (index > -1) {
order[index] = newOrder;
} else {
index = order.length;
order.push(newOrder);
}
store.order = order;
reset(['skip', 'filter.skip', 'page']);
fetch({});
index++;
return { index, order };
}
async function deleteOrder(field) {
let order = store.order ?? [];
if (typeof order == 'string') order = [order];
const index = order.findIndex((o) => o.split(' ')[0] === field);
if (index > -1) order.splice(index, 1);
store.order = order;
fetch({});
}
function sanitizerParams(params, exprBuilder) { function sanitizerParams(params, exprBuilder) {
for (const param in params) { for (const param in params) {
if (params[param] === '' || params[param] === null) { if (params[param] === '' || params[param] === null) {
delete store.userParams[param]; delete store.userParams[param];
delete params[param]; delete params[param];
if (store.filter?.where) { if (store.filter?.where) {
const key = Object.keys(exprBuilder ? exprBuilder(param) : param); let key;
if (key[0]) delete store.filter.where[key[0]]; if (exprBuilder) {
const result = exprBuilder(param);
if (result !== undefined && result !== null)
key = Object.keys(result);
} else {
if (typeof param === 'object' && param !== null)
key = Object.keys(param);
}
if (key && key[0]) {
delete store.filter.where[key[0]];
if (Object.keys(store.filter.where).length === 0) { if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where; delete store.filter.where;
} }
} }
} }
} }
}
return params; return params;
} }
async function loadMore() { async function loadMore() {
if (!store.hasMoreData) return; if (!store.hasMoreData) return;
store.skip = store.limit * page.value; store.skip = store.limit * store.page;
page.value += 1; store.page += 1;
await fetch({ append: true }); await fetch({ append: true });
} }
async function refresh() { async function refresh() {
if (Object.values(store.userParams).length) await fetch({ append: false }); if (Object.values(store.userParams).length) await fetch({});
} }
function updateStateParams() { function updateStateParams() {
@ -237,6 +284,8 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
applyFilter, applyFilter,
addFilter, addFilter,
addFilterWhere, addFilterWhere,
addOrder,
deleteOrder,
refresh, refresh,
destroy, destroy,
loadMore, loadMore,
@ -245,5 +294,6 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
updateStateParams, updateStateParams,
isLoading, isLoading,
deleteOption, deleteOption,
reset,
}; };
} }

View File

@ -27,8 +27,12 @@ export function useRole() {
return false; return false;
} }
function isEmployee() {
return hasAny(['employee']);
}
return { return {
isEmployee,
fetch, fetch,
hasAny, hasAny,
state, state,

View File

@ -3,12 +3,14 @@ import { useRole } from './useRole';
import { useAcl } from './useAcl'; import { useAcl } from './useAcl';
import { useUserConfig } from './useUserConfig'; import { useUserConfig } from './useUserConfig';
import axios from 'axios'; import axios from 'axios';
import { useRouter } from 'vue-router';
import useNotify from './useNotify'; import useNotify from './useNotify';
import { useTokenConfig } from './useTokenConfig'; import { useTokenConfig } from './useTokenConfig';
const TOKEN_MULTIMEDIA = 'tokenMultimedia'; const TOKEN_MULTIMEDIA = 'tokenMultimedia';
const TOKEN = 'token'; const TOKEN = 'token';
export function useSession() { export function useSession() {
const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
let isCheckingToken = false; let isCheckingToken = false;
let intervalId = null; let intervalId = null;
@ -58,20 +60,25 @@ export function useSession() {
} }
} }
} }
async function destroy() { async function destroy(destroyTokens = true) {
const tokens = { const tokens = {
tokenMultimedia: 'Accounts/logout', tokenMultimedia: 'Accounts/logout',
token: 'VnUsers/logout', token: 'VnUsers/logout',
}; };
const storage = keepLogin() ? localStorage : sessionStorage; const storage = keepLogin() ? localStorage : sessionStorage;
let destroyTokenPromises = [];
for (const [key, url] of Object.entries(tokens)) { try {
await destroyToken(url, storage, key); if (destroyTokens) {
const { data: isValidToken } = await axios.get('VnUsers/validateToken');
if (isValidToken)
destroyTokenPromises = Object.entries(tokens).map(([key, url]) =>
destroyToken(url, storage, key)
);
} }
} finally {
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
await Promise.allSettled(destroyTokenPromises);
const { setUser } = useState(); const { setUser } = useState();
setUser({ setUser({
@ -84,6 +91,7 @@ export function useSession() {
stopRenewer(); stopRenewer();
} }
}
async function login(data) { async function login(data) {
setSession(data); setSession(data);
@ -96,6 +104,31 @@ export function useSession() {
startInterval(); startInterval();
} }
async function setLogin(data) {
const {
data: { multimediaToken },
} = await axios.get('VnUsers/ShareToken', {
headers: { Authorization: data.token },
});
if (!multimediaToken) return;
await login({
...data,
created: Date.now(),
tokenMultimedia: multimediaToken.id,
});
notify('login.loginSuccess', 'positive');
const currentRoute = router.currentRoute.value;
if (currentRoute.query?.redirect) {
router.push(currentRoute.query.redirect);
} else {
router.push({ name: 'Dashboard' });
}
}
function isLoggedIn() { function isLoggedIn() {
const localToken = localStorage.getItem(TOKEN); const localToken = localStorage.getItem(TOKEN);
const sessionToken = sessionStorage.getItem(TOKEN); const sessionToken = sessionStorage.getItem(TOKEN);
@ -157,6 +190,7 @@ export function useSession() {
setToken, setToken,
destroy, destroy,
login, login,
setLogin,
isLoggedIn, isLoggedIn,
checkValidity, checkValidity,
setSession, setSession,

View File

@ -28,7 +28,7 @@ export function useValidator() {
} }
const { t } = useI18n(); const { t } = useI18n();
const validations = function (validation) { const validations = function (validation = {}) {
return { return {
format: (value) => { format: (value) => {
const { allowNull, with: format, allowBlank } = validation; const { allowNull, with: format, allowBlank } = validation;
@ -40,12 +40,15 @@ export function useValidator() {
if (!isValid) return message; if (!isValid) return message;
}, },
presence: (value) => { presence: (value) => {
let message = `Value can't be empty`; let message = t(`globals.valueCantBeEmpty`);
if (validation.message) if (validation.message)
message = t(validation.message) || validation.message; message = t(validation.message) || validation.message;
return !validator.isEmpty(value ? String(value) : '') || message; return !validator.isEmpty(value ? String(value) : '') || message;
}, },
required: (required, value) => {
return required ? !!value || t('globals.fieldRequired') : null;
},
length: (value) => { length: (value) => {
const options = { const options = {
min: validation.min || validation.is, min: validation.min || validation.is,
@ -71,12 +74,17 @@ export function useValidator() {
return validator.isInt(value) || 'Value should be integer'; return validator.isInt(value) || 'Value should be integer';
return validator.isNumeric(value) || 'Value should be a number'; return validator.isNumeric(value) || 'Value should be a number';
}, },
min: (value, min) => {
if (min >= 0)
if (Math.floor(value) < min) return t('inputMin', { value: min });
},
custom: (value) => validation.bindedFunction(value) || 'Invalid value', custom: (value) => validation.bindedFunction(value) || 'Invalid value',
}; };
}; };
return { return {
validate, validate,
validations,
models, models,
}; };
} }

View File

@ -4,8 +4,10 @@
body.body--light { body.body--light {
--font-color: black; --font-color: black;
--vn-section-color: #e0e0e0; --vn-header-color: #cecece;
--vn-page-color: #ffffff; --vn-page-color: #ffffff;
--vn-section-color: #e0e0e0;
--vn-section-hover-color: #b9b9b9;
--vn-text-color: var(--font-color); --vn-text-color: var(--font-color);
--vn-label-color: #5f5f5f; --vn-label-color: #5f5f5f;
--vn-accent-color: #e7e3e3; --vn-accent-color: #e7e3e3;
@ -17,8 +19,10 @@ body.body--light {
} }
} }
body.body--dark { body.body--dark {
--vn-header-color: #5d5d5d;
--vn-page-color: #222; --vn-page-color: #222;
--vn-section-color: #3d3d3d; --vn-section-color: #3d3d3d;
--vn-section-hover-color: #747474;
--vn-text-color: white; --vn-text-color: white;
--vn-label-color: #a8a8a8; --vn-label-color: #a8a8a8;
--vn-accent-color: #424242; --vn-accent-color: #424242;
@ -71,8 +75,9 @@ select:-webkit-autofill {
.bg-vn-section-color { .bg-vn-section-color {
background-color: var(--vn-section-color); background-color: var(--vn-section-color);
} }
.bg-hover {
background-color: #666666; .bg-vn-hover {
background-color: var(--vn-section-hover-color);
} }
.color-vn-label { .color-vn-label {
@ -98,10 +103,6 @@ select:-webkit-autofill {
border-radius: 8px; border-radius: 8px;
} }
.card-width {
width: 770px;
}
.vn-card-list { .vn-card-list {
width: 100%; width: 100%;
max-width: 60em; max-width: 60em;
@ -148,10 +149,25 @@ select:-webkit-autofill {
background-color: var(--vn-section-color); background-color: var(--vn-section-color);
} }
.q-table td[shrink] {
text-overflow: ellipsis;
overflow: hidden;
max-width: 80px;
}
.tr-header { .tr-header {
color: var(--vn-label-color); color: var(--vn-label-color);
} }
.disabled {
& .q-checkbox__label {
color: var(--vn-text-color);
}
& .q-checkbox__inner {
color: var(--vn-label-color);
}
}
.q-chip, .q-chip,
.q-notification__message, .q-notification__message,
.q-notification__icon { .q-notification__icon {
@ -170,17 +186,12 @@ select:-webkit-autofill {
font-size: medium; font-size: medium;
} }
.q-card__actions { .q-toolbar {
justify-content: center; background: var(--vn-section-color);
} }
/* q-notification row items-stretch q-notification--standard bg-negative text-white */ .q-card__actions {
justify-content: center;
.q-card,
.q-table,
.q-table__bottom,
.q-drawer {
background-color: var(--vn-section-color);
} }
input[type='number'] { input[type='number'] {
@ -197,6 +208,7 @@ input::-webkit-inner-spin-button {
max-width: 100%; max-width: 100%;
} }
.q-table__container {
/* ===== Scrollbar CSS ===== / /* ===== Scrollbar CSS ===== /
/ Firefox */ / Firefox */
@ -219,3 +231,42 @@ input::-webkit-inner-spin-button {
*::-webkit-scrollbar-track { *::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
}
.q-table {
th,
td {
padding: 1px 10px 1px 10px;
max-width: 100px;
div span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
tr {
th {
font-size: 11pt;
}
td {
font-size: 11pt;
border-collapse: collapse;
}
}
.shrink {
max-width: 75px;
}
.expand {
max-width: 400px;
}
}
.edit-photo-btn {
position: absolute;
right: 12px;
bottom: 12px;
z-index: 1;
cursor: pointer;
}

View File

@ -20,21 +20,21 @@ export function isValidDate(date) {
* Converts a given date to a specific format. * Converts a given date to a specific format.
* *
* @param {number|string|Date} date - The date to be formatted. * @param {number|string|Date} date - The date to be formatted.
* @param {Object} opts - Optional parameters to customize the output format.
* @returns {string} The formatted date as a string in 'dd/mm/yyyy' format. If the provided date is not valid, an empty string is returned. * @returns {string} The formatted date as a string in 'dd/mm/yyyy' format. If the provided date is not valid, an empty string is returned.
* *
* @example * @example
* // returns "02/12/2022" * // returns "02/12/2022"
* toDateFormat(new Date(2022, 11, 2)); * toDateFormat(new Date(2022, 11, 2));
*/ */
export function toDateFormat(date, locale = 'es-ES') { export function toDateFormat(date, locale = 'es-ES', opts = {}) {
if (!isValidDate(date)) { if (!isValidDate(date)) return '';
return '';
} const format = Object.assign(
return new Date(date).toLocaleDateString(locale, { { year: 'numeric', month: '2-digit', day: '2-digit' },
year: 'numeric', opts
month: '2-digit', );
day: '2-digit', return new Date(date).toLocaleDateString(locale, format);
});
} }
/** /**

View File

@ -1,7 +1,7 @@
export default function dateRange(value) { export default function dateRange(value) {
const minHour = new Date(value); const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0); minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(); const maxHour = new Date(value);
maxHour.setHours(23, 59, 59, 59); maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour]; return [minHour, maxHour];

View File

@ -0,0 +1,21 @@
// parsing JSON safely
function parseJSON(str, fallback) {
try {
return JSON.parse(str ?? '{}');
} catch (e) {
console.error('Error parsing JSON:', e);
return fallback;
}
}
export default function (route, param) {
// catch route query params
const params = parseJSON(route?.query?.params, {});
// extract and parse filter from params
const { filter: filterStr = '{}' } = params;
const where = parseJSON(filterStr, {})?.where;
if (where && where[param] !== undefined) {
return where[param];
}
return null;
}

View File

@ -11,6 +11,7 @@ import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange'; import dateRange from './dateRange';
import toHour from './toHour'; import toHour from './toHour';
import dashOrCurrency from './dashOrCurrency'; import dashOrCurrency from './dashOrCurrency';
import getParamWhere from './getParamWhere';
export { export {
toLowerCase, toLowerCase,
@ -26,4 +27,5 @@ export {
toPercentage, toPercentage,
dashIfEmpty, dashIfEmpty,
dateRange, dateRange,
getParamWhere,
}; };

View File

@ -8,11 +8,8 @@ export default function (value, fractionSize = 2) {
const options = { const options = {
style: 'percent', style: 'percent',
minimumFractionDigits: fractionSize, minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize maximumFractionDigits: fractionSize,
}; };
return new Intl.NumberFormat(locale, options) return new Intl.NumberFormat(locale, options).format(parseFloat(value));
.format(parseFloat(value));
} }

View File

@ -46,6 +46,7 @@ globals:
noPinnedModules: You don't have any pinned modules noPinnedModules: You don't have any pinned modules
summary: summary:
basicData: Basic data basicData: Basic data
daysOnward: Days onward
today: Today today: Today
yesterday: Yesterday yesterday: Yesterday
dateFormat: en-GB dateFormat: en-GB
@ -66,6 +67,7 @@ globals:
allRows: 'All { numberRows } row(s)' allRows: 'All { numberRows } row(s)'
markAll: Mark all markAll: Mark all
requiredField: Required field requiredField: Required field
valueCantBeEmpty: Value cannot be empty
class: clase class: clase
type: Type type: Type
reason: reason reason: reason
@ -89,7 +91,13 @@ globals:
salesPerson: SalesPerson salesPerson: SalesPerson
send: Send send: Send
code: Code code: Code
since: Since
from: From
to: To
notes: Notes
refresh: Refresh
pageTitles: pageTitles:
logIn: Login
summary: Summary summary: Summary
basicData: Basic data basicData: Basic data
log: Logs log: Logs
@ -100,13 +108,158 @@ globals:
modes: Modes modes: Modes
zones: Zones zones: Zones
zonesList: Zones zonesList: Zones
deliveryList: Delivery days deliveryDays: Delivery days
upcomingList: Upcoming deliveries upcomingDeliveries: Upcoming deliveries
role: Role role: Role
alias: Alias alias: Alias
aliasUsers: Users aliasUsers: Users
subRoles: Subroles subRoles: Subroles
inheritedRoles: Inherited Roles inheritedRoles: Inherited Roles
customers: Customers
list: List
webPayments: Web Payments
extendedList: Extended list
notifications: Notifications
defaulter: Defaulter
customerCreate: New customer
fiscalData: Fiscal data
billingData: Billing data
consignees: Consignees
notes: Notes
credits: Credits
greuges: Greuges
balance: Balance
recoveries: Recoveries
webAccess: Web access
sms: Sms
creditManagement: Credit management
creditContracts: Credit contracts
creditOpinion: Credit opinion
others: Others
samples: Samples
consumption: Consumption
mandates: Mandates
contacts: Contacts
webPayment: Web payment
fileManagement: File management
unpaid: Unpaid
entries: Entries
buys: Buys
dms: File management
entryCreate: New entry
latestBuys: Latest buys
tickets: Tickets
ticketCreate: New Tickets
boxing: Boxing
sale: Sale
claims: Claims
claimCreate: New claim
lines: Lines
photos: Photos
development: Development
action: Action
invoiceOuts: Invoice out
negativeBases: Negative Bases
globalInvoicing: Global invoicing
invoiceOutCreate: Create invoice out
shelving: Shelving
shelvingList: Shelving List
shelvingCreate: New shelving
invoiceIns: Invoices In
invoiceInCreate: Create invoice in
vat: VAT
dueDay: Due day
intrastat: Intrastat
corrective: Corrective
order: Orders
orderList: List
orderCreate: New order
catalog: Catalog
volume: Volume
workers: Workers
workerCreate: New worker
department: Department
pda: PDA
pbx: Private Branch Exchange
calendar: Calendar
timeControl: Time control
locker: Locker
wagons: Wagons
wagonsList: Wagons List
wagonCreate: Create wagon
wagonEdit: Edit wagon
typesList: Types List
typeCreate: Create type
typeEdit: Edit type
wagonCounter: Trolley counter
roadmap: Roadmap
stops: Stops
routes: Routes
cmrsList: CMRs list
RouteList: List
routeCreate: New route
RouteRoadmap: Roadmaps
RouteRoadmapCreate: Create roadmap
autonomous: Autonomous
suppliers: Suppliers
supplier: Supplier
expedition: Expedition
services: Service
components: Components
pictures: Pictures
packages: Packages
tracking: Tracking
labeler: Labeler
supplierCreate: New supplier
accounts: Accounts
addresses: Addresses
agencyTerm: Agency agreement
travel: Travels
extraCommunity: Extra community
travelCreate: New travel
history: Log
thermographs: Thermograph
items: Items
diary: Diary
tags: Tags
create: Create
buyRequest: Buy requests
fixedPrice: Fixed prices
wasteBreakdown: Waste breakdown
itemCreate: New item
barcode: Barcodes
tax: Tax
botanical: Botanical
itemTypeCreate: New item type
family: Item Type
lastEntries: Last entries
itemType: Item type
monitors: Monitors
dashboard: Dashboard
users: Users
createTicket: Create ticket
ticketAdvance: Advance tickets
futureTickets: Future tickets
purchaseRequest: Purchase request
weeklyTickets: Weekly tickets
formation: Formation
locations: Locations
warehouses: Warehouses
saleTracking: Sale tracking
roles: Roles
connections: Connections
acls: ACLs
mailForwarding: Mail forwarding
mailAlias: Mail alias
privileges: Privileges
ldap: LDAP
samba: Samba
twoFactor: Two factor
recoverPassword: Recover password
resetPassword: Reset password
ticketsMonitor: Tickets monitor
clientsActionsMonitor: Clients and actions
serial: Serial
created: Created created: Created
worker: Worker worker: Worker
now: Now now: Now
@ -114,6 +267,13 @@ globals:
new: New new: New
comment: Comment comment: Comment
observations: Observations observations: Observations
goToModuleIndex: Go to module index
unsavedPopup:
title: Unsaved changes will be lost
subtitle: Are you sure exit without saving?
createInvoiceIn: Create invoice in
myAccount: My account
noOne: No one
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred statusInternalServerError: An internal server error has ocurred
@ -132,8 +292,6 @@ login:
loginError: Invalid username or password loginError: Invalid username or password
fieldRequired: This field is required fieldRequired: This field is required
twoFactorRequired: Two-factor verification required twoFactorRequired: Two-factor verification required
pageTitles:
logIn: Login
twoFactor: twoFactor:
code: Code code: Code
validate: Validate validate: Validate
@ -141,47 +299,18 @@ twoFactor:
explanation: >- explanation: >-
Please, enter the verification code that we have sent to your email in the Please, enter the verification code that we have sent to your email in the
next 5 minutes next 5 minutes
pageTitles:
twoFactor: Two-Factor
verifyEmail: verifyEmail:
pageTitles: pageTitles:
verifyEmail: Email verification verifyEmail: Email verification
dashboard: recoverPassword:
pageTitles: userOrEmail: User or recovery email
dashboard: Dashboard explanation: >-
We will sent you an email to recover your password
resetPassword:
repeatPassword: Repeat password
passwordNotMatch: Passwords don't match
passwordChanged: Password changed
customer: customer:
pageTitles:
customers: Customers
list: List
webPayments: Web Payments
extendedList: Extended list
notifications: Notifications
defaulter: Defaulter
customerCreate: New customer
summary: Summary
basicData: Basic data
fiscalData: Fiscal data
billingData: Billing data
consignees: Consignees
notes: Notes
credits: Credits
greuges: Greuges
balance: Balance
recoveries: Recoveries
webAccess: Web access
log: Log
sms: Sms
creditManagement: Credit management
creditContracts: Credit contracts
creditOpinion: Credit opinion
others: Others
samples: Samples
consumption: Consumption
mandates: Mandates
contacts: Contacts
webPayment: Web payment
fileManagement: File management
unpaid: Unpaid
list: list:
phone: Phone phone: Phone
email: Email email: Email
@ -280,8 +409,8 @@ customer:
extendedList: extendedList:
tableVisibleColumns: tableVisibleColumns:
id: Identifier id: Identifier
name: Comercial name name: Name
socialName: Business name socialName: Social name
fi: Tax number fi: Tax number
salesPersonFk: Salesperson salesPersonFk: Salesperson
credit: Credit credit: Credit
@ -311,17 +440,6 @@ customer:
hasCoreVnl: VNL core received hasCoreVnl: VNL core received
hasSepaVnl: VNL B2B received hasSepaVnl: VNL B2B received
entry: entry:
pageTitles:
entries: Entries
list: List
summary: Summary
basicData: Basic data
buys: Buys
notes: Notes
dms: File management
log: Log
entryCreate: New entry
latestBuys: Latest buys
list: list:
newEntry: New entry newEntry: New entry
landed: Landed landed: Landed
@ -330,6 +448,19 @@ entry:
booked: Booked booked: Booked
confirmed: Confirmed confirmed: Confirmed
ordered: Ordered ordered: Ordered
tableVisibleColumns:
id: Id
reference: Reference
created: Creation
supplierFk: Supplier
isBooked: Booked
isConfirmed: Confirmed
isOrdered: Ordered
companyFk: Company
travelFk: Travel
isExcludedFromAvailable: Inventory
isRaid: Raid
invoiceAmount: Import
summary: summary:
commission: Commission commission: Commission
currency: Currency currency: Currency
@ -405,7 +536,8 @@ entry:
landed: Landed landed: Landed
warehouseOut: Warehouse Out warehouseOut: Warehouse Out
latestBuys: latestBuys:
picture: Picture tableVisibleColumns:
image: Picture
itemFk: Item ID itemFk: Item ID
packing: Packing packing: Packing
grouping: Grouping grouping: Grouping
@ -433,6 +565,8 @@ entry:
packagingFk: Package packagingFk: Package
packingOut: Package out packingOut: Package out
landing: Landing landing: Landing
isExcludedFromAvailable: Es inventory
isRaid: Raid
ticket: ticket:
pageTitles: pageTitles:
tickets: Tickets tickets: Tickets
@ -444,6 +578,13 @@ ticket:
sms: Sms sms: Sms
notes: Notes notes: Notes
sale: Sale sale: Sale
dms: File management
volume: Volume
observation: Notes
ticketAdvance: Advance tickets
futureTickets: Future tickets
purchaseRequest: Purchase request
weeklyTickets: Weekly tickets
list: list:
nickname: Nickname nickname: Nickname
state: State state: State
@ -517,90 +658,7 @@ ticket:
landed: Landed landed: Landed
warehouse: Warehouse warehouse: Warehouse
agency: Agency agency: Agency
claim:
pageTitles:
claims: Claims
list: List
claimCreate: New claim
summary: Summary
basicData: Basic Data
lines: Lines
photos: Photos
development: Development
log: Audit logs
notes: Notes
action: Action
list:
customer: Customer
assignedTo: Assigned
created: Created
state: State
rmaList:
code: Code
records: records
card:
claimId: Claim ID
attendedBy: Attended by
created: Created
state: State
ticketId: Ticket ID
customerSummary: Customer summary
claimedTicket: Claimed ticket
saleTracking: Sale tracking
ticketTracking: Ticket tracking
commercial: Commercial
province: Province
zone: Zone
customerId: client ID
summary:
customer: Customer
assignedTo: Assigned
attendedBy: Attended by
created: Created
state: State
details: Details
item: Item
landed: Landed
quantity: Quantity
claimed: Claimed
price: Price
discount: Discount
total: Total
actions: Actions
responsibility: Responsibility
company: Company
person: Employee/Customer
notes: Notes
photos: Photos
development: Development
reason: Reason
result: Result
responsible: Responsible
worker: Worker
redelivery: Redelivery
changeState: Change state
basicData:
customer: Customer
assignedTo: Assigned
created: Created
state: State
pickup: Pick up
null: No
agency: Agency
delivery: Delivery
photo:
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}'
noData: 'There are no images/videos, click here or drag and drop the file'
dragDrop: Drag and drop it here
invoiceOut: invoiceOut:
pageTitles:
invoiceOuts: Invoice out
list: List
negativeBases: Negative Bases
globalInvoicing: Global invoicing
invoiceOutCreate: Create invoice out
summary: Summary
basicData: Basic Data
list: list:
ref: Reference ref: Reference
issued: Issued issued: Issued
@ -639,6 +697,7 @@ invoiceOut:
chooseValidClient: Choose a valid client chooseValidClient: Choose a valid client
chooseValidCompany: Choose a valid company chooseValidCompany: Choose a valid company
chooseValidPrinter: Choose a valid printer chooseValidPrinter: Choose a valid printer
chooseValidSerialType: Choose a serial type
fillDates: Invoice date and the max date should be filled fillDates: Invoice date and the max date should be filled
invoiceDateLessThanMaxDate: Invoice date can not be less than max date invoiceDateLessThanMaxDate: Invoice date can not be less than max date
invoiceWithFutureDate: Exists an invoice with a future date invoiceWithFutureDate: Exists an invoice with a future date
@ -668,13 +727,6 @@ invoiceOut:
errors: errors:
downloadCsvFailed: CSV download failed downloadCsvFailed: CSV download failed
shelving: shelving:
pageTitles:
shelving: Shelving
shelvingList: Shelving List
shelvingCreate: New shelving
summary: Summary
basicData: Basic Data
log: Logs
list: list:
parking: Parking parking: Parking
priority: Priority priority: Priority
@ -701,17 +753,6 @@ parking:
info: You can search by parking code info: You can search by parking code
label: Search parking... label: Search parking...
invoiceIn: invoiceIn:
pageTitles:
invoiceIns: Invoices In
list: List
invoiceInCreate: Create invoice in
summary: Summary
basicData: Basic data
vat: VAT
dueDay: Due day
intrastat: Intrastat
corrective: Corrective
log: Logs
list: list:
ref: Reference ref: Reference
supplier: Supplier supplier: Supplier
@ -762,15 +803,6 @@ invoiceIn:
stems: Stems stems: Stems
country: Country country: Country
order: order:
pageTitles:
order: Orders
orderList: List
orderCreate: New order
summary: Summary
basicData: Basic Data
catalog: Catalog
volume: Volume
lines: Lines
field: field:
salesPersonFk: Sales Person salesPersonFk: Sales Person
clientFk: Client clientFk: Client
@ -845,6 +877,7 @@ worker:
calendar: Calendar calendar: Calendar
timeControl: Time control timeControl: Time control
locker: Locker locker: Locker
balance: Balance
business: Business business: Business
list: list:
name: Name name: Name
@ -922,7 +955,24 @@ worker:
payMethods: Pay method payMethods: Pay method
iban: IBAN iban: IBAN
bankEntity: Swift / BIC bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: Image not found imageNotFound: Image not found
balance:
tableVisibleColumns:
paymentDate: Date
incomeType: Type
debit: Debt
credit: Have
concept: Concept
business: business:
tableVisibleColumns: tableVisibleColumns:
started: Start Date started: Start Date
@ -942,15 +992,6 @@ worker:
notes: Notes notes: Notes
wagon: wagon:
pageTitles:
wagons: Wagons
wagonsList: Wagons List
wagonCreate: Create wagon
wagonEdit: Edit wagon
typesList: Types List
typeCreate: Create type
typeEdit: Edit type
wagonCounter: Trolley counter
type: type:
name: Name name: Name
submit: Submit submit: Submit
@ -979,20 +1020,9 @@ wagon:
minHeightBetweenTrays: 'The minimum height between trays is ' minHeightBetweenTrays: 'The minimum height between trays is '
maxWagonHeight: 'The maximum height of the wagon is ' maxWagonHeight: 'The maximum height of the wagon is '
uncompleteTrays: There are incomplete trays uncompleteTrays: There are incomplete trays
route/roadmap:
pageTitles:
roadmap: Roadmap
summary: Summary
basicData: Basic Data
stops: Stops
roadmap:
pageTitles:
roadmap: Roadmap
summary: Summary
basicData: Basic Data
stops: Stops
route: route:
pageTitles: pageTitles:
agency: Agency List
routes: Routes routes: Routes
cmrsList: CMRs list cmrsList: CMRs list
RouteList: List RouteList: List
@ -1018,41 +1048,21 @@ route:
shipped: Preparation date shipped: Preparation date
viewCmr: View CMR viewCmr: View CMR
downloadCmrs: Download CMRs downloadCmrs: Download CMRs
columnLabels:
Id: Id
vehicle: Vehicle
description: Description
isServed: Served
worker: Worker
date: Date
started: Started
actions: Actions
agency: Agency
volume: Volume
finished: Finished
supplier: supplier:
pageTitles:
suppliers: Suppliers
supplier: Supplier
list: List
supplierCreate: New supplier
summary: Summary
basicData: Basic data
fiscalData: Fiscal data
billingData: Billing data
log: Log
accounts: Accounts
contacts: Contacts
addresses: Addresses
consumption: Consumption
agencyTerm: Agency agreement
dms: File management
list: list:
payMethod: Pay method payMethod: Pay method
payDeadline: Pay deadline payDeadline: Pay deadline
payDay: Pay day payDay: Pay day
account: Account account: Account
newSupplier: New supplier newSupplier: New supplier
tableVisibleColumns:
id: Id
name: Name
nif: NIF/CIF
nickname: Alias
account: Account
payMethod: Pay Method
payDay: Pay Day
summary: summary:
responsible: Responsible responsible: Responsible
notes: Notes notes: Notes
@ -1138,15 +1148,19 @@ supplier:
date: Date date: Date
reference: Reference reference: Reference
travel: travel:
pageTitles: travelList:
travel: Travels tableVisibleColumns:
list: List id: Id
summary: Summary ref: Reference
extraCommunity: Extra community agency: Agency
travelCreate: New travel shipped: Shipped
basicData: Basic data landed: Landed
history: Log shipHour: Shipment Hour
thermographs: Thermograph landHour: Landing Hour
warehouseIn: Warehouse in
warehouseOut: Warehouse out
totalEntries: Total entries
totalEntriesTooltip: Total entries
summary: summary:
confirmed: Confirmed confirmed: Confirmed
entryId: Entry Id entryId: Entry Id
@ -1193,24 +1207,6 @@ travel:
travelFileDescription: 'Travel id { travelId }' travelFileDescription: 'Travel id { travelId }'
file: File file: File
item: item:
pageTitles:
items: Items
list: List
diary: Diary
tags: Tags
create: Create
buyRequest: Buy requests
fixedPrice: Fixed prices
wasteBreakdown: Waste breakdown
itemCreate: New item
barcode: Barcodes
tax: Tax
log: Log
botanical: Botanical
shelving: Shelving
itemTypeCreate: New item type
family: Item Type
lastEntries: Last entries
descriptor: descriptor:
item: Item item: Item
buyer: Buyer buyer: Buyer
@ -1296,22 +1292,6 @@ item:
minSalesQuantity: 'Cantidad mínima de venta' minSalesQuantity: 'Cantidad mínima de venta'
genus: 'Genus' genus: 'Genus'
specie: 'Specie' specie: 'Specie'
item/itemType:
pageTitles:
itemType: Item type
basicData: Basic data
summary: Summary
monitor:
pageTitles:
monitors: Monitors
list: List
zone:
pageTitles:
zones: Zones
zonesList: Zones
deliveryList: Delivery days
upcomingList: Upcoming deliveries
components: components:
topbar: {} topbar: {}
itemsFilterPanel: itemsFilterPanel:

View File

@ -45,6 +45,7 @@ globals:
noPinnedModules: No has fijado ningún módulo noPinnedModules: No has fijado ningún módulo
summary: summary:
basicData: Datos básicos basicData: Datos básicos
daysOnward: Días adelante
today: Hoy today: Hoy
yesterday: Ayer yesterday: Ayer
dateFormat: es-ES dateFormat: es-ES
@ -75,6 +76,9 @@ globals:
warehouse: Almacén warehouse: Almacén
company: Empresa company: Empresa
fieldRequired: Campo requerido fieldRequired: Campo requerido
valueCantBeEmpty: El valor no puede estar vacío
Value can't be blank: El valor no puede estar en blanco
Value can't be null: El valor no puede ser nulo
allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }' allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }'
smsSent: SMS enviado smsSent: SMS enviado
confirmDeletion: Confirmar eliminación confirmDeletion: Confirmar eliminación
@ -89,7 +93,13 @@ globals:
salesPerson: Comercial salesPerson: Comercial
send: Enviar send: Enviar
code: Código code: Código
since: Desde
from: Desde
to: Hasta
notes: Notas
refresh: Actualizar
pageTitles: pageTitles:
logIn: Inicio de sesión
summary: Resumen summary: Resumen
basicData: Datos básicos basicData: Datos básicos
log: Historial log: Historial
@ -100,14 +110,160 @@ globals:
modes: Modos modes: Modos
zones: Zonas zones: Zonas
zonesList: Zonas zonesList: Zonas
deliveryList: Días de entrega deliveryDays: Días de entrega
upcomingList: Próximos repartos upcomingDeliveries: Próximos repartos
role: Role role: Role
alias: Alias alias: Alias
aliasUsers: Usuarios aliasUsers: Usuarios
subRoles: Subroles subRoles: Subroles
inheritedRoles: Roles heredados inheritedRoles: Roles heredados
customers: Clientes
customerCreate: Nuevo cliente
list: Listado
webPayments: Pagos Web
extendedList: Listado extendido
notifications: Notificaciones
defaulter: Morosos
createCustomer: Crear cliente
fiscalData: Datos fiscales
billingData: Forma de pago
consignees: Consignatarios
notes: Notas
credits: Créditos
greuges: Greuges
balance: Balance
recoveries: Recobros
webAccess: Acceso web
sms: Sms
creditManagement: Gestión de crédito
creditContracts: Contratos de crédito
creditOpinion: Opinión de crédito
others: Otros
samples: Plantillas
consumption: Consumo
mandates: Mandatos
contacts: Contactos
webPayment: Pago web
fileManagement: Gestión documental
unpaid: Impago
entries: Entradas
buys: Compras
dms: Gestión documental
entryCreate: Nueva entrada
latestBuys: Últimas compras
tickets: Tickets
ticketCreate: Nuevo ticket
boxing: Encajado
sale: Lineas del pedido
claims: Reclamaciones
claimCreate: Crear reclamación
lines: Líneas
development: Trazabilidad
photos: Fotos
action: Acción
invoiceOuts: Fact. emitidas
negativeBases: Bases Negativas
globalInvoicing: Facturación global
invoiceOutCreate: Crear fact. emitida
order: Cesta
orderList: Listado
orderCreate: Nueva orden
catalog: Catálogo
volume: Volumen
shelving: Carros
shelvingList: Listado de carros
shelvingCreate: Nuevo carro
invoiceIns: Fact. recibidas
invoiceInCreate: Crear fact. recibida
vat: IVA
labeler: Etiquetas
dueDay: Vencimiento
intrastat: Intrastat
corrective: Rectificativa
workers: Trabajadores workers: Trabajadores
workerCreate: Nuevo trabajador
department: Departamentos
pda: PDA
pbx: Centralita
calendar: Calendario
timeControl: Control de horario
locker: Taquilla
wagons: Vagones
wagonsList: Listado vagones
wagonCreate: Crear tipo
wagonEdit: Editar tipo
typesList: Listado tipos
typeCreate: Crear tipo
typeEdit: Editar tipo
wagonCounter: Contador de carros
roadmap: Troncales
stops: Paradas
routes: Rutas
cmrsList: Listado de CMRs
RouteList: Listado
routeCreate: Nueva ruta
RouteRoadmap: Troncales
RouteRoadmapCreate: Crear troncal
autonomous: Autónomos
suppliers: Proveedores
supplier: Proveedor
supplierCreate: Nuevo proveedor
accounts: Cuentas
addresses: Direcciones
agencyTerm: Acuerdo agencia
travel: Envíos
create: Crear
extraCommunity: Extra comunitarios
travelCreate: Nuevo envío
history: Historial
thermographs: Termógrafos
items: Artículos
diary: Histórico
tags: Etiquetas
fixedPrice: Precios fijados
buyRequest: Peticiones de compra
wasteBreakdown: Deglose de mermas
itemCreate: Nuevo artículo
tax: 'IVA'
botanical: 'Botánico'
barcode: 'Código de barras'
itemTypeCreate: Nueva familia
family: Familia
lastEntries: Últimas entradas
itemType: Familia
monitors: Monitores
dashboard: Tablón
users: Usuarios
createTicket: Crear ticket
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
purchaseRequest: Petición de compra
weeklyTickets: Tickets programados
formation: Formación
locations: Localizaciones
warehouses: Almacenes
roles: Roles
connections: Conexiones
acls: ACLs
mailForwarding: Reenvío de correo
mailAlias: Alias de correo
privileges: Privilegios
observation: Notas
expedition: Expedición
saleTracking: Líneas preparadas
services: Servicios
tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
ldap: LDAP
samba: Samba
twoFactor: Doble factor
recoverPassword: Recuperar contraseña
resetPassword: Restablecer contraseña
ticketsMonitor: Monitor de tickets
clientsActionsMonitor: Clientes y acciones
serial: Facturas por serie
created: Fecha creación created: Fecha creación
worker: Trabajador worker: Trabajador
now: Ahora now: Ahora
@ -115,6 +271,13 @@ globals:
new: Nuevo new: Nuevo
comment: Comentario comment: Comentario
observations: Observaciones observations: Observaciones
goToModuleIndex: Ir al índice del módulo
unsavedPopup:
title: Los cambios que no haya guardado se perderán
subtitle: ¿Seguro que quiere salir sin guardar?
createInvoiceIn: Crear factura recibida
myAccount: Mi cuenta
noOne: Nadie
errors: errors:
statusUnauthorized: Acceso denegado statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor statusInternalServerError: Ha ocurrido un error interno del servidor
@ -133,55 +296,23 @@ login:
loginError: Nombre de usuario o contraseña incorrectos loginError: Nombre de usuario o contraseña incorrectos
fieldRequired: Este campo es obligatorio fieldRequired: Este campo es obligatorio
twoFactorRequired: Verificación de doble factor requerida twoFactorRequired: Verificación de doble factor requerida
pageTitles:
logIn: Inicio de sesión
twoFactor: twoFactor:
code: Código code: Código
validate: Validar validate: Validar
insert: Introduce el código de verificación insert: Introduce el código de verificación
explanation: Por favor introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos explanation: Por favor introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos
pageTitles:
twoFactor: Doble factor
verifyEmail: verifyEmail:
pageTitles: pageTitles:
verifyEmail: Verificación de correo verifyEmail: Verificación de correo
dashboard: recoverPassword:
pageTitles: userOrEmail: Usuario o correo de recuperación
dashboard: Tablón explanation: >-
Te enviaremos un correo para restablecer tu contraseña
resetPassword:
repeatPassword: Repetir contraseña
passwordNotMatch: Las contraseñas no coinciden
passwordChanged: Contraseña cambiada
customer: customer:
pageTitles:
customers: Clientes
customerCreate: Nuevo cliente
list: Listado
webPayments: Pagos Web
extendedList: Listado extendido
notifications: Notificaciones
defaulter: Morosos
createCustomer: Crear cliente
summary: Resumen
basicData: Datos básicos
fiscalData: Datos fiscales
billingData: Forma de pago
consignees: Consignatarios
notes: Notas
credits: Créditos
greuges: Greuges
balance: Balance
recoveries: Recobros
webAccess: Acceso web
log: Historial
sms: Sms
creditManagement: Gestión de crédito
creditContracts: Contratos de crédito
creditOpinion: Opinión de crédito
others: Otros
samples: Plantillas
consumption: Consumo
mandates: Mandatos
contacts: Contactos
webPayment: Pago web
fileManagement: Gestión documental
unpaid: Impago
list: list:
phone: Teléfono phone: Teléfono
email: Email email: Email
@ -279,7 +410,7 @@ customer:
extendedList: extendedList:
tableVisibleColumns: tableVisibleColumns:
id: Identificador id: Identificador
name: Nombre Comercial name: Nombre
socialName: Razón social socialName: Razón social
fi: NIF / CIF fi: NIF / CIF
salesPersonFk: Comercial salesPersonFk: Comercial
@ -310,17 +441,6 @@ customer:
hasCoreVnl: Recibido core VNL hasCoreVnl: Recibido core VNL
hasSepaVnl: Recibido B2B VNL hasSepaVnl: Recibido B2B VNL
entry: entry:
pageTitles:
entries: Entradas
list: Listado
summary: Resumen
basicData: Datos básicos
buys: Compras
notes: Notas
dms: Gestión documental
log: Historial
entryCreate: Nueva entrada
latestBuys: Últimas compras
list: list:
newEntry: Nueva entrada newEntry: Nueva entrada
landed: F. entrega landed: F. entrega
@ -329,6 +449,19 @@ entry:
booked: Asentado booked: Asentado
confirmed: Confirmado confirmed: Confirmado
ordered: Pedida ordered: Pedida
tableVisibleColumns:
id: Id
reference: Referencia
created: Creación
supplierFk: Proveedor
isBooked: Asentado
isConfirmed: Confirmado
isOrdered: Pedida
companyFk: Empresa
travelFk: Envio
isExcludedFromAvailable: Inventario
isRaid: Redada
invoiceAmount: Importe
summary: summary:
commission: Comisión commission: Comisión
currency: Moneda currency: Moneda
@ -404,9 +537,10 @@ entry:
landed: F. entrega landed: F. entrega
warehouseOut: Alm. salida warehouseOut: Alm. salida
latestBuys: latestBuys:
picture: Foto tableVisibleColumns:
itemFk: ID Artículo image: Foto
packing: Packing itemFk: Id Artículo
packing: packing
grouping: Grouping grouping: Grouping
quantity: Cantidad quantity: Cantidad
size: Medida size: Medida
@ -432,6 +566,8 @@ entry:
packagingFk: Embalaje packagingFk: Embalaje
packingOut: Embalaje envíos packingOut: Embalaje envíos
landing: Llegada landing: Llegada
isExcludedFromAvailable: Es inventario
isRaid: Redada
ticket: ticket:
pageTitles: pageTitles:
tickets: Tickets tickets: Tickets
@ -443,6 +579,20 @@ ticket:
sms: Sms sms: Sms
notes: Notas notes: Notas
sale: Lineas del pedido sale: Lineas del pedido
dms: Gestión documental
volume: Volumen
observation: Notas
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
expedition: Expedición
purchaseRequest: Petición de compra
weeklyTickets: Tickets programados
saleTracking: Líneas preparadas
services: Servicios
tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
list: list:
nickname: Alias nickname: Alias
state: Estado state: Estado
@ -516,90 +666,7 @@ ticket:
landed: F. entrega landed: F. entrega
warehouse: Almacén warehouse: Almacén
agency: Agencia agency: Agencia
claim:
pageTitles:
claims: Reclamaciones
list: Listado
claimCreate: Crear reclamación
summary: Resumen
basicData: Datos básicos
lines: Líneas
development: Trazabilidad
photos: Fotos
log: Historial
notes: Notas
action: Acción
list:
customer: Cliente
assignedTo: Asignada a
created: Creada
state: Estado
rmaList:
code: Código
records: registros
card:
claimId: ID reclamación
attendedBy: Atendida por
created: Creada
state: Estado
ticketId: ID ticket
customerSummary: Resumen del cliente
claimedTicket: Ticket reclamado
saleTracking: Líneas preparadas
ticketTracking: Estados del ticket
commercial: Comercial
province: Provincia
zone: Zona
customerId: ID del cliente
summary:
customer: Cliente
assignedTo: Asignada a
attendedBy: Atendida por
created: Creada
state: Estado
details: Detalles
item: Artículo
landed: Entregado
quantity: Cantidad
claimed: Reclamado
price: Precio
discount: Descuento
total: Total
actions: Acciones
responsibility: Responsabilidad
company: Empresa
person: Comercial/Cliente
notes: Observaciones
photos: Fotos
development: Trazabilidad
reason: Motivo
result: Consecuencias
responsible: Responsable
worker: Trabajador
redelivery: Devolución
changeState: Cambiar estado
basicData:
customer: Cliente
assignedTo: Asignada a
created: Creada
state: Estado
pickup: Recogida
null: No
agency: Agencia
delivery: Reparto
photo:
fileDescription: 'Reclamacion ID {claimId} del cliente {clientName} id {clientId}'
noData: No hay imágenes/videos haz click aquí o arrastra y suelta el archivo
dragDrop: Arrástralo y sueltalo aquí
invoiceOut: invoiceOut:
pageTitles:
invoiceOuts: Fact. emitidas
list: Listado
negativeBases: Bases Negativas
globalInvoicing: Facturación global
invoiceOutCreate: Crear fact. emitida
summary: Resumen
basicData: Datos básicos
list: list:
ref: Referencia ref: Referencia
issued: Fecha emisión issued: Fecha emisión
@ -638,6 +705,7 @@ invoiceOut:
chooseValidClient: Selecciona un cliente válido chooseValidClient: Selecciona un cliente válido
chooseValidCompany: Selecciona una empresa válida chooseValidCompany: Selecciona una empresa válida
chooseValidPrinter: Selecciona una impresora válida chooseValidPrinter: Selecciona una impresora válida
chooseValidSerialType: Selecciona una tipo de serie válida
fillDates: La fecha de la factura y la fecha máxima deben estar completas fillDates: La fecha de la factura y la fecha máxima deben estar completas
invoiceDateLessThanMaxDate: La fecha de la factura no puede ser menor que la fecha máxima invoiceDateLessThanMaxDate: La fecha de la factura no puede ser menor que la fecha máxima
invoiceWithFutureDate: Existe una factura con una fecha futura invoiceWithFutureDate: Existe una factura con una fecha futura
@ -651,8 +719,6 @@ invoiceOut:
percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}' percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}'
pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs' pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs'
negativeBases: negativeBases:
from: Desde
to: Hasta
company: Empresa company: Empresa
country: País country: País
clientId: Id cliente clientId: Id cliente
@ -667,15 +733,6 @@ invoiceOut:
errors: errors:
downloadCsvFailed: Error al descargar CSV downloadCsvFailed: Error al descargar CSV
order: order:
pageTitles:
order: Cesta
orderList: Listado
orderCreate: Nueva orden
summary: Resumen
basicData: Datos básicos
catalog: Catálogo
volume: Volumen
lines: Líneas
field: field:
salesPersonFk: Comercial salesPersonFk: Comercial
clientFk: Cliente clientFk: Cliente
@ -717,13 +774,6 @@ order:
price: Precio price: Precio
amount: Monto amount: Monto
shelving: shelving:
pageTitles:
shelving: Carros
shelvingList: Listado de carros
shelvingCreate: Nuevo carro
summary: Resumen
basicData: Datos básicos
log: Historial
list: list:
parking: Parking parking: Parking
priority: Prioridad priority: Prioridad
@ -749,17 +799,6 @@ parking:
info: Puedes buscar por código de parking info: Puedes buscar por código de parking
label: Buscar parking... label: Buscar parking...
invoiceIn: invoiceIn:
pageTitles:
invoiceIns: Fact. recibidas
list: Listado
invoiceInCreate: Crear fact. recibida
summary: Resumen
basicData: Datos básicos
vat: IVA
dueDay: Vencimiento
intrastat: Intrastat
corrective: Rectificativa
log: Historial
list: list:
ref: Referencia ref: Referencia
supplier: Proveedor supplier: Proveedor
@ -841,6 +880,7 @@ worker:
calendar: Calendario calendar: Calendario
timeControl: Control de horario timeControl: Control de horario
locker: Taquilla locker: Taquilla
balance: Balance
business: Contrato business: Contrato
list: list:
name: Nombre name: Nombre
@ -854,7 +894,7 @@ worker:
card: card:
workerId: ID Trabajador workerId: ID Trabajador
name: Nombre name: Nombre
email: Email email: Correo personal
phone: Teléfono phone: Teléfono
mobile: Móvil mobile: Móvil
active: Activo active: Activo
@ -918,7 +958,24 @@ worker:
payMethods: Método de pago payMethods: Método de pago
iban: IBAN iban: IBAN
bankEntity: Swift / BIC bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: No se ha encontrado la imagen imageNotFound: No se ha encontrado la imagen
balance:
tableVisibleColumns:
paymentDate: Fecha
incomeType: Tipo
debit: Debe
credit: Haber
concept: Concepto
business: business:
tableVisibleColumns: tableVisibleColumns:
started: Fecha inicio started: Fecha inicio
@ -937,15 +994,6 @@ worker:
basicSalary: Salario transportistas basicSalary: Salario transportistas
notes: Notas notes: Notas
wagon: wagon:
pageTitles:
wagons: Vagones
wagonsList: Listado vagones
wagonCreate: Crear tipo
wagonEdit: Editar tipo
typesList: Listado tipos
typeCreate: Crear tipo
typeEdit: Editar tipo
wagonCounter: Contador de carros
type: type:
name: Nombre name: Nombre
submit: Guardar submit: Guardar
@ -974,36 +1022,12 @@ wagon:
minHeightBetweenTrays: 'La distancia mínima entre bandejas es ' minHeightBetweenTrays: 'La distancia mínima entre bandejas es '
maxWagonHeight: 'La altura máxima del vagón es ' maxWagonHeight: 'La altura máxima del vagón es '
uncompleteTrays: Hay bandejas sin completar uncompleteTrays: Hay bandejas sin completar
route/roadmap:
pageTitles:
roadmap: Troncales
summary: Resumen
basicData: Datos básicos
stops: Paradas
roadmap:
pageTitles:
roadmap: Troncales
summary: Resumen
basicData: Datos básicos
stops: Paradas
route: route:
pageTitles:
routes: Rutas
cmrsList: Listado de CMRs
RouteList: Listado
routeCreate: Nueva ruta
basicData: Datos básicos
summary: Resumen
RouteRoadmap: Troncales
RouteRoadmapCreate: Crear troncal
tickets: Tickets
log: Historial
autonomous: Autónomos
cmr: cmr:
list: list:
results: resultados results: resultados
cmrFk: Id CMR cmrFk: Id CMR
hasCmrDms: Adjuntado en gestdoc hasCmrDms: Gestdoc
'true': 'true':
'false': 'No' 'false': 'No'
ticketFk: Id ticket ticketFk: Id ticket
@ -1013,41 +1037,21 @@ route:
shipped: Fecha preparación shipped: Fecha preparación
viewCmr: Ver CMR viewCmr: Ver CMR
downloadCmrs: Descargar CMRs downloadCmrs: Descargar CMRs
columnLabels:
Id: Id
vehicle: Vehículo
description: Descripción
isServed: Servida
worker: Trabajador
date: Fecha
started: Iniciada
actions: Acciones
agency: Agencia
volume: Volumen
finished: Finalizada
supplier: supplier:
pageTitles:
suppliers: Proveedores
supplier: Proveedor
list: Listado
supplierCreate: Nuevo proveedor
summary: Resumen
basicData: Datos básicos
fiscalData: Datos fiscales
billingData: Forma de pago
log: Historial
accounts: Cuentas
contacts: Contactos
addresses: Direcciones
consumption: Consumo
agencyTerm: Acuerdo agencia
dms: Gestión documental
list: list:
payMethod: Método de pago payMethod: Método de pago
payDeadline: Plazo de pago payDeadline: Plazo de pago
payDay: Día de pago payDay: Día de pago
account: Cuenta account: Cuenta
newSupplier: Nuevo proveedor newSupplier: Nuevo proveedor
tableVisibleColumns:
id: Id
name: Nombre
nif: NIF/CIF
nickname: Alias
account: Cuenta
payMethod: Método de pago
payDay: Dia de pago
summary: summary:
responsible: Responsable responsible: Responsable
notes: Notas notes: Notas
@ -1133,16 +1137,19 @@ supplier:
date: Fecha date: Fecha
reference: Referencia reference: Referencia
travel: travel:
pageTitles: travelList:
travel: Envíos tableVisibleColumns:
list: Listado id: Id
create: Crear ref: Referencia
summary: Resumen agency: Agencia
extraCommunity: Extra comunitarios shipped: F.envío
travelCreate: Nuevo envío shipHour: Hora de envío
basicData: Datos básicos landHour: Hora de llegada
history: Historial landed: F.entrega
thermographs: Termógrafos warehouseIn: Alm.salida
warehouseOut: Alm.entrada
totalEntries:
totalEntriesTooltip: Entradas totales
summary: summary:
confirmed: Confirmado confirmed: Confirmado
entryId: Id entrada entryId: Id entrada
@ -1189,24 +1196,6 @@ travel:
travelFileDescription: 'Id envío { travelId }' travelFileDescription: 'Id envío { travelId }'
file: Fichero file: Fichero
item: item:
pageTitles:
items: Artículos
list: Listado
diary: Histórico
tags: Etiquetas
fixedPrice: Precios fijados
buyRequest: Peticiones de compra
wasteBreakdown: Deglose de mermas
itemCreate: Nuevo artículo
basicData: 'Datos básicos'
tax: 'IVA'
botanical: 'Botánico'
barcode: 'Código de barras'
log: Historial
shelving: Carros
itemTypeCreate: Nueva familia
family: Familia
lastEntries: Últimas entradas
descriptor: descriptor:
item: Artículo item: Artículo
buyer: Comprador buyer: Comprador
@ -1292,27 +1281,6 @@ item:
achieved: 'Conseguido' achieved: 'Conseguido'
concept: 'Concepto' concept: 'Concepto'
state: 'Estado' state: 'Estado'
item/itemType:
pageTitles:
itemType: Familia
basicData: Datos básicos
summary: Resumen
zone:
pageTitles:
zones: Zonas
list: Zonas
deliveryList: Días de entrega
upcomingList: Próximos repartos
role:
pageTitles:
zones: Zonas
list: Zonas
deliveryList: Días de entrega
upcomingList: Próximos repartos
monitor:
pageTitles:
monitors: Monitores
list: Listado
components: components:
topbar: {} topbar: {}
itemsFilterPanel: itemsFilterPanel:
@ -1329,8 +1297,6 @@ components:
# LatestBuysFilter # LatestBuysFilter
salesPersonFk: Comprador salesPersonFk: Comprador
supplierFk: Proveedor supplierFk: Proveedor
from: Desde
to: Hasta
active: Activo active: Activo
visible: Visible visible: Visible
floramondo: Floramondo floramondo: Floramondo
@ -1348,6 +1314,7 @@ components:
clone: Clonar clone: Clonar
openCard: Ficha openCard: Ficha
openSummary: Detalles openSummary: Detalles
viewSummary: Vista previa
cardDescriptor: cardDescriptor:
mainList: Listado principal mainList: Listado principal
summary: Resumen summary: Resumen

View File

@ -1,11 +1,9 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';

View File

@ -1,19 +1,14 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ref } from 'vue'; import { ref, computed } from 'vue';
import FetchData from 'components/FetchData.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import AclFilter from './Acls/AclFilter.vue';
import AclFormView from './Acls/AclFormView.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useQuasar } from 'quasar';
import VnTable from 'components/VnTable/VnTable.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
defineProps({ defineProps({
id: { id: {
@ -25,11 +20,9 @@ defineProps({
const { notify } = useNotify(); const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { openConfirmationModal } = useVnConfirm(); const quasar = useQuasar();
const paginateRef = ref(); const tableRef = ref();
const formDialog = ref(false);
const rolesOptions = ref([]);
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
switch (param) { switch (param) {
@ -40,32 +33,94 @@ const exprBuilder = (param, value) => {
} }
}; };
const deleteAcl = async (id) => { const columns = computed(() => [
{
align: 'left',
name: 'id',
label: t('id'),
isId: true,
cardVisible: true,
},
{
align: 'left',
name: 'model',
label: t('model'),
cardVisible: true,
create: true,
},
{
align: 'left',
name: 'principalId',
label: t('principalId'),
cardVisible: true,
component: 'select',
attrs: {
url: 'VnRoles',
optionLabel: 'name',
optionValue: 'name',
},
create: true,
},
{
align: 'left',
name: 'property',
label: t('property'),
cardVisible: true,
create: true,
},
{
align: 'left',
name: 'accessType',
label: t('accessType'),
component: 'select',
attrs: {
options: ['READ', 'WRITE', '*'],
},
cardVisible: true,
create: true,
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('Delete'),
icon: 'delete',
action: deleteAcl,
isPrimary: true,
},
],
},
]);
const deleteAcl = async ({ id }) => {
try { try {
await new Promise((resolve) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Remove ACL'),
message: t('Do you want to remove this ACL?'),
},
})
.onOk(() => {
resolve(true);
})
.onCancel(() => {
resolve(false);
});
});
await axios.delete(`ACLs/${id}`); await axios.delete(`ACLs/${id}`);
paginateRef.value.fetch(); tableRef.value.reload();
notify('ACL removed', 'positive'); notify('ACL removed', 'positive');
} catch (error) { } catch (error) {
console.error('Error deleting Acl: ', error); console.error('Error deleting Acl: ', error);
} }
}; };
function showFormDialog(data) {
formDialog.value = {
show: true,
formInitialData: { ...data },
};
}
</script> </script>
<template> <template>
<FetchData
url="VnRoles"
:filter="{ fields: ['name'], order: 'name ASC' }"
@on-fetch="(data) => (rolesOptions = data)"
auto-load
/>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar <VnSearchbar
data-key="AccountAcls" data-key="AccountAcls"
url="ACLs" url="ACLs"
@ -73,74 +128,27 @@ function showFormDialog(data) {
:label="t('acls.search')" :label="t('acls.search')"
:info="t('acls.searchInfo')" :info="t('acls.searchInfo')"
/> />
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<AclFilter data-key="AccountAcls" />
</QScrollArea>
</QDrawer> </QDrawer>
<VnTable
<QPage class="flex justify-center q-pa-md"> ref="tableRef"
<div class="vn-card-list">
<VnPaginate
ref="paginateRef"
data-key="AccountAcls" data-key="AccountAcls"
url="ACLs" :url="`ACLs`"
:expr-builder="exprBuilder" :create="{
> urlCreate: 'ACLs',
<template #body="{ rows }"> title: 'Create ACL',
<CardList onDataSaved: () => tableRef.reload(),
v-for="row of rows" formInitialData: {},
:id="row.id" }"
:key="row.id" order="id DESC"
:title="`${row.model}.${row.property}`" :disable-option="{ card: true }"
@click="showFormDialog(row)" :columns="columns"
> default-mode="table"
<template #list-items> :right-search="true"
<VnLv :label="t('acls.role')" :value="row.principalId" /> :is-editable="true"
<VnLv :label="t('acls.accessType')" :value="row.accessType" /> :use-model="true"
<VnLv
:label="t('acls.permissions')"
:value="row.permission"
/> />
</template> </template>
<template #actions>
<QBtn
:label="t('globals.delete')"
@click.stop="
openConfirmationModal(
t('ACL will be removed'),
t('Are you sure you want to continue?'),
() => deleteAcl(row.id)
)
"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
<QDialog
v-model="formDialog.show"
transition-show="scale"
transition-hide="scale"
>
<AclFormView
:form-initial-data="formDialog.formInitialData"
@on-data-change="paginateRef.fetch()"
:roles-options="rolesOptions"
/>
</QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="showFormDialog()">
<QTooltip class="text-no-wrap">{{ t('New ACL') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>
<i18n> <i18n>
es: es:
@ -148,4 +156,17 @@ es:
ACL removed: ACL eliminado ACL removed: ACL eliminado
ACL will be removed: El ACL será eliminado ACL will be removed: El ACL será eliminado
Are you sure you want to continue?: ¿Seguro que quieres continuar? Are you sure you want to continue?: ¿Seguro que quieres continuar?
Remove ACL: Eliminar Acl
Do you want to remove this ACL?: ¿Quieres eliminar este ACL?
principalId: Rol
model: Modelo
en:
New ACL: New ACL
ACL removed: ACL removed
ACL will be removed: ACL will be removed
Are you sure you want to continue?: Are you sure you want to continue?
Remove ACL: Remove ACL
Do you want to remove this ACL?: Do you want to remove this ACL?
principalId: Rol
model: Models
</i18n> </i18n>

View File

@ -1,30 +1,13 @@
<script setup> <script setup>
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ref } from 'vue'; import { ref, computed } from 'vue';
import VnTable from 'components/VnTable/VnTable.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import AliasSummary from './Alias/Card/AliasSummary.vue';
import AliasCreateForm from './Alias/AliasCreateForm.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
defineProps({ const tableRef = ref();
id: {
type: Number,
default: 0,
},
});
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const router = useRouter();
const stateStore = useStateStore(); const stateStore = useStateStore();
const aliasCreateDialogRef = ref(null);
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
switch (param) { switch (param) {
@ -34,10 +17,29 @@ const exprBuilder = (param, value) => {
: { alias: { like: `%${value}%` } }; : { alias: { like: `%${value}%` } };
} }
}; };
const columns = computed(() => [
const navigate = (id) => router.push({ name: 'AliasSummary', params: { id } }); {
align: 'left',
const openCreateModal = () => aliasCreateDialogRef.value.show(); name: 'id',
label: t('Id'),
isId: true,
cardVisible: true,
},
{
align: 'left',
name: 'alias',
label: t('Alias'),
cardVisible: true,
create: true,
},
{
align: 'left',
name: 'description',
label: t('Description'),
cardVisible: true,
create: true,
},
]);
</script> </script>
<template> <template>
@ -52,54 +54,29 @@ const openCreateModal = () => aliasCreateDialogRef.value.show();
/> />
</Teleport> </Teleport>
</template> </template>
<QPage class="flex justify-center q-pa-md"> <VnTable
<div class="vn-card-list"> ref="tableRef"
<VnPaginate
ref="paginateRef"
data-key="AccountAliasList" data-key="AccountAliasList"
url="MailAliases" url="MailAliases"
:expr-builder="exprBuilder" :create="{
> urlCreate: 'MailAliases',
<template #body="{ rows }"> title: 'Create MailAlias',
<CardList onDataSaved: ({ id }) => tableRef.redirect(id),
v-for="row of rows" formInitialData: {},
:id="row.id" }"
:key="row.id" order="id DESC"
:title="row.alias" :columns="columns"
@click="navigate(row.id)" :disable-option="{ card: true }"
> default-mode="table"
<template #list-items> redirect="account/alias"
<VnLv :label="t('mailAlias.alias')" :value="row.alias"> :is-editable="true"
</VnLv> :use-model="true"
<VnLv
:label="t('mailAlias.description')"
:value="row.description"
>
</VnLv>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, AliasSummary)"
color="primary"
style="margin-top: 15px"
/> />
</template> </template>
</CardList>
</template> <i18n>
</VnPaginate> es:
</div> Id: Id
<QDialog Alias: Alias
ref="aliasCreateDialogRef" Description: Descripción
transition-show="scale" </i18n>
transition-hide="scale"
>
<AliasCreateForm />
</QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="openCreateModal()">
<QTooltip class="text-no-wrap">{{ t('mailAlias.newAlias') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>

View File

@ -2,11 +2,9 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import CardList from 'src/components/ui/CardList.vue'; import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import { toDateTimeFormat } from 'src/filters/date.js'; import { toDateTimeFormat } from 'src/filters/date.js';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';

View File

@ -2,7 +2,6 @@
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import FormModelPopup from 'components/FormModelPopup.vue'; import FormModelPopup from 'components/FormModelPopup.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
@ -15,7 +14,6 @@ const newAccountForm = reactive({
active: true, active: true,
}); });
const rolesOptions = ref([]); const rolesOptions = ref([]);
const redirectToAccountBasicData = (_, { id }) => { const redirectToAccountBasicData = (_, { id }) => {
router.push({ name: 'AccountBasicData', params: { id } }); router.push({ name: 'AccountBasicData', params: { id } });
}; };

View File

@ -1,7 +1,6 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';

View File

@ -1,12 +1,10 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import axios from 'axios'; import axios from 'axios';
@ -15,7 +13,6 @@ const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
const arrayData = useArrayData('AccountLdap'); const arrayData = useArrayData('AccountLdap');
const URL_UPDATE = `LdapConfigs/${1}`; const URL_UPDATE = `LdapConfigs/${1}`;
const URL_CREATE = `LdapConfigs`; const URL_CREATE = `LdapConfigs`;
const DEFAULT_DATA = { const DEFAULT_DATA = {
@ -27,11 +24,9 @@ const DEFAULT_DATA = {
server: null, server: null,
userDn: null, userDn: null,
}; };
const initialData = ref({ const initialData = ref({
...DEFAULT_DATA, ...DEFAULT_DATA,
}); });
const hasData = computed({ const hasData = computed({
get: () => initialData.value.hasData, get: () => initialData.value.hasData,
set: (val) => { set: (val) => {
@ -40,12 +35,10 @@ const hasData = computed({
else formCustomFn.value = null; else formCustomFn.value = null;
}, },
}); });
const initialDataLoaded = ref(false); const initialDataLoaded = ref(false);
const formUrlCreate = ref(null); const formUrlCreate = ref(null);
const formUrlUpdate = ref(null); const formUrlUpdate = ref(null);
const formCustomFn = ref(null); const formCustomFn = ref(null);
const onTestConection = async () => { const onTestConection = async () => {
try { try {
await axios.get(`LdapConfigs/test`); await axios.get(`LdapConfigs/test`);
@ -54,7 +47,6 @@ const onTestConection = async () => {
console.error('Error testing connection', error); console.error('Error testing connection', error);
} }
}; };
const getInitialLdapConfig = async () => { const getInitialLdapConfig = async () => {
try { try {
initialDataLoaded.value = false; initialDataLoaded.value = false;
@ -79,7 +71,6 @@ const getInitialLdapConfig = async () => {
initialDataLoaded.value = true; initialDataLoaded.value = true;
} }
}; };
const deleteMailForward = async () => { const deleteMailForward = async () => {
try { try {
await axios.delete(URL_UPDATE); await axios.delete(URL_UPDATE);

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