From 91b08334607e1e8a820253ad97ffc609a1f9c80a Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Thu, 20 Feb 2025 10:17:59 +0100 Subject: [PATCH 01/28] fix: refs #8198 handle potential null values in itemBalances computation --- src/pages/Item/Card/ItemDiary.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/Item/Card/ItemDiary.vue b/src/pages/Item/Card/ItemDiary.vue index 4b6775183..31b3c328e 100644 --- a/src/pages/Item/Card/ItemDiary.vue +++ b/src/pages/Item/Card/ItemDiary.vue @@ -27,7 +27,7 @@ const user = state.getUser(); const today = Date.vnNew(); today.setHours(0, 0, 0, 0); const warehousesOptions = ref([]); -const itemBalances = computed(() => arrayDataItemBalances.store.data); +const itemBalances = computed(() => arrayDataItemBalances.store.data || []); const where = computed(() => arrayDataItemBalances.store.filter.where || {}); const showWhatsBeforeInventory = ref(false); const inventoriedDate = ref(null); @@ -313,8 +313,8 @@ async function updateWarehouse(warehouseFk) { row.lineFk == row.lastPreparedLineFk ? 'black' : row.balance < 0 - ? 'negative' - : '' + ? 'negative' + : '' " dense style="font-size: 14px" From 64f29e0696b465feee45da34da5954520af0238b Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Thu, 20 Feb 2025 13:37:17 +0100 Subject: [PATCH 02/28] fix: address --- src/pages/Ticket/Card/TicketSummary.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index 999240b7c..8cb518823 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -45,6 +45,15 @@ const descriptorData = useArrayData('ticketData'); onMounted(async () => { ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/'; }); +const formattedAddress = computed(() => { + if (!ticket.value) return ''; + + const address = ticket.value.address; + const postcode = address.postalCode; + const province = address.province ? `(${address.province.name})` : ''; + + return `${address.street} - ${postcode} - ${address.city} ${province}`; +}); function isEditable() { try { @@ -237,7 +246,7 @@ onMounted(async () => { /> <VnLv :label="t('ticket.summary.consigneeStreet')" - :value="entity.address?.street" + :value="formattedAddress" /> </QCard> <QCard class="vn-one" v-if="entity.notes.length"> From a3828ab8692a0aee5f8890348b29dc8d233f325e Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Thu, 20 Feb 2025 21:16:05 +0100 Subject: [PATCH 03/28] fix: handle multiple changes --- src/pages/Ticket/Card/TicketSale.vue | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index 92936b26a..a083ed316 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -174,11 +174,19 @@ const getSaleTotal = (sale) => { return price - discount; }; +const getRowUpdateInputEvents = (sale) => ({ + 'keyup.enter': () => { + changeQuantity(sale); + }, + blur: () => { + changeQuantity(sale); + }, +}); + const resetChanges = async () => { arrayData.fetch({ append: false }); tableRef.value.reload(); }; -const rowToUpdate = ref(null); const changeQuantity = async (sale) => { if ( !sale.itemFk || @@ -196,11 +204,8 @@ const changeQuantity = async (sale) => { const updateQuantity = async (sale) => { try { let { quantity, id } = sale; - if (!rowToUpdate.value) return; - rowToUpdate.value = null; sale.isNew = false; - const params = { quantity: quantity }; - await axios.post(`Sales/${id}/updateQuantity`, params); + await axios.post(`Sales/${id}/updateQuantity`, { quantity }); notify('globals.dataSaved', 'positive'); tableRef.value.reload(); } catch (e) { @@ -816,9 +821,7 @@ watch( v-if="row.isNew || isTicketEditable" type="number" v-model.number="row.quantity" - @blur="changeQuantity(row)" - @keyup.enter.stop="changeQuantity(row)" - @update:model-value="() => (rowToUpdate = row)" + v-on="getRowUpdateInputEvents(row)" @focus="edit.oldQuantity = row.quantity" /> <span v-else>{{ row.quantity }}</span> From e82dc90ff9a91b26d48fb882a7f351dd5d3923f3 Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Fri, 21 Feb 2025 09:01:28 +0100 Subject: [PATCH 04/28] fix: ticketSale --- src/pages/Ticket/Card/TicketSale.vue | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index a083ed316..f5fb50ecf 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -188,11 +188,7 @@ const resetChanges = async () => { tableRef.value.reload(); }; const changeQuantity = async (sale) => { - if ( - !sale.itemFk || - sale.quantity == null || - edit.value?.oldQuantity === sale.quantity - ) + if (!sale.itemFk || sale.quantity == null || sale?.originalQuantity === sale.quantity) return; if (!sale.id) return addSale(sale); From 8536ade5b7e13ec4e1d962863ca49fbce218a501 Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Fri, 21 Feb 2025 09:01:40 +0100 Subject: [PATCH 05/28] feat: add keyup.enter --- src/pages/Ticket/Card/TicketSaleMoreActions.vue | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pages/Ticket/Card/TicketSaleMoreActions.vue b/src/pages/Ticket/Card/TicketSaleMoreActions.vue index 4cc96e9e2..8b5537edc 100644 --- a/src/pages/Ticket/Card/TicketSaleMoreActions.vue +++ b/src/pages/Ticket/Card/TicketSaleMoreActions.vue @@ -50,6 +50,7 @@ const { dialog } = useQuasar(); const { notify } = useNotify(); const acl = useAcl(); const btnDropdownRef = ref(null); +const editManaProxyRef = ref(null); const { openConfirmationModal } = useVnConfirm(); const newDiscount = ref(null); @@ -131,13 +132,13 @@ const createClaim = () => { openConfirmationModal( t('Claim out of time'), t('Do you want to continue?'), - onCreateClaimAccepted + onCreateClaimAccepted, ); else openConfirmationModal( t('Do you want to create a claim?'), false, - onCreateClaimAccepted + onCreateClaimAccepted, ); }; @@ -216,8 +217,15 @@ const createRefund = async (withWarehouse) => { <QItemSection> <QItemLabel>{{ t('Update discount') }}</QItemLabel> </QItemSection> - <TicketEditManaProxy :mana="props.mana" @save="changeMultipleDiscount()"> + <TicketEditManaProxy + ref="editManaProxyRef" + :sale="row" + :mana="props.mana" + @save="changeMultipleDiscount" + > <VnInput + autofocus + @keyup.enter.stop="() => editManaProxyRef.save(row)" v-model.number="newDiscount" :label="t('ticketSale.discount')" type="number" From 81a33dd2fc4910c2a5107af7ad14463cd2da83c2 Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Fri, 21 Feb 2025 11:38:32 +0100 Subject: [PATCH 06/28] fix: shipped columnFilter --- src/pages/Ticket/TicketList.vue | 44 +++++++++++++++++---------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/pages/Ticket/TicketList.vue b/src/pages/Ticket/TicketList.vue index 8df19c0d9..dfa1a4e14 100644 --- a/src/pages/Ticket/TicketList.vue +++ b/src/pages/Ticket/TicketList.vue @@ -108,13 +108,11 @@ const columns = computed(() => [ }, { align: 'left', - name: 'shippedDate', + name: 'shipped', cardVisible: true, label: t('ticketList.shipped'), columnFilter: { component: 'date', - alias: 't', - inWhere: true, }, format: ({ shippedDate }) => toDate(shippedDate), }, @@ -122,6 +120,12 @@ const columns = computed(() => [ align: 'left', name: 'shipped', label: t('ticketList.hour'), + columnFilter: { + component: 'time', + attrs: { + timeOnly: true, + }, + }, format: (row) => toTimeFormat(row.shipped), }, { @@ -232,7 +236,7 @@ const columns = computed(() => [ function resetAgenciesSelector(formData) { agenciesOptions.value = []; - if(formData) formData.agencyModeId = null; + if (formData) formData.agencyModeId = null; } function redirectToLines(id) { @@ -240,7 +244,7 @@ function redirectToLines(id) { window.open(url, '_blank'); } -const onClientSelected = async (formData) => { +const onClientSelected = async (formData) => { resetAgenciesSelector(formData); await fetchClient(formData); await fetchAddresses(formData); @@ -248,14 +252,12 @@ const onClientSelected = async (formData) => { const fetchAvailableAgencies = async (formData) => { resetAgenciesSelector(formData); - const response= await getAgencies(formData, selectedClient.value); + const response = await getAgencies(formData, selectedClient.value); if (!response) return; - - const { options, agency } = response - if(options) - agenciesOptions.value = options; - if(agency) - formData.agencyModeId = agency; + + const { options, agency } = response; + if (options) agenciesOptions.value = options; + if (agency) formData.agencyModeId = agency; }; const fetchClient = async (formData) => { @@ -330,7 +332,7 @@ function openBalanceDialog(ticket) { const description = ref([]); const firstTicketClientId = checkedTickets[0].clientFk; const isSameClient = checkedTickets.every( - (ticket) => ticket.clientFk === firstTicketClientId + (ticket) => ticket.clientFk === firstTicketClientId, ); if (!isSameClient) { @@ -369,7 +371,7 @@ async function onSubmit() { description: dialogData.value.value.description, clientFk: dialogData.value.value.clientFk, email: email[0].email, - } + }, ); if (data) notify('globals.dataSaved', 'positive'); @@ -388,32 +390,32 @@ function setReference(data) { switch (data) { case 1: newDescription = `${t( - 'ticketList.creditCard' + 'ticketList.creditCard', )}, ${dialogData.value.value.description.replace( /^(Credit Card, |Cash, |Transfers, )/, - '' + '', )}`; break; case 2: newDescription = `${t( - 'ticketList.cash' + 'ticketList.cash', )}, ${dialogData.value.value.description.replace( /^(Credit Card, |Cash, |Transfers, )/, - '' + '', )}`; break; case 3: newDescription = `${newDescription.replace( /^(Credit Card, |Cash, |Transfers, )/, - '' + '', )}`; break; case 4: newDescription = `${t( - 'ticketList.transfers' + 'ticketList.transfers', )}, ${dialogData.value.value.description.replace( /^(Credit Card, |Cash, |Transfers, )/, - '' + '', )}`; break; case 3317: From ef624af3f8ae3c664b79bbce5483396f0c945d36 Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Fri, 21 Feb 2025 11:44:07 +0100 Subject: [PATCH 07/28] revert: column time --- src/pages/Ticket/TicketList.vue | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/pages/Ticket/TicketList.vue b/src/pages/Ticket/TicketList.vue index dfa1a4e14..78bebc297 100644 --- a/src/pages/Ticket/TicketList.vue +++ b/src/pages/Ticket/TicketList.vue @@ -120,12 +120,6 @@ const columns = computed(() => [ align: 'left', name: 'shipped', label: t('ticketList.hour'), - columnFilter: { - component: 'time', - attrs: { - timeOnly: true, - }, - }, format: (row) => toTimeFormat(row.shipped), }, { From 680c1f9d9ca1c573f3d41c5446d01130e5c3accc Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Fri, 21 Feb 2025 11:45:41 +0100 Subject: [PATCH 08/28] perf: i18n --- src/pages/Ticket/TicketFilter.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/Ticket/TicketFilter.vue b/src/pages/Ticket/TicketFilter.vue index 4b50892b0..c82c0067f 100644 --- a/src/pages/Ticket/TicketFilter.vue +++ b/src/pages/Ticket/TicketFilter.vue @@ -293,6 +293,7 @@ en: clientFk: Customer orderFk: Order from: From + shipped: Shipped to: To salesPersonFk: Salesperson stateFk: State @@ -320,6 +321,7 @@ es: clientFk: Cliente orderFk: Pedido from: Desde + shipped: F. envío to: Hasta salesPersonFk: Comercial stateFk: Estado From 5481ad6478b7924e773083c58f8e4cedca03e1a9 Mon Sep 17 00:00:00 2001 From: carlossa <carlossa@verdnatura.es> Date: Fri, 21 Feb 2025 15:23:44 +0100 Subject: [PATCH 09/28] fix: refs #6553 workerBusiness --- src/pages/Worker/Card/WorkerBusiness.vue | 41 +++++++++++++++------- src/pages/Worker/Card/WorkerDescriptor.vue | 7 ++++ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/pages/Worker/Card/WorkerBusiness.vue b/src/pages/Worker/Card/WorkerBusiness.vue index 6025ae289..e3582a2d5 100644 --- a/src/pages/Worker/Card/WorkerBusiness.vue +++ b/src/pages/Worker/Card/WorkerBusiness.vue @@ -3,7 +3,7 @@ import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import VnTable from 'components/VnTable/VnTable.vue'; -import { toDate } from 'src/filters'; +import { dashIfEmpty, toDate } from 'src/filters'; import { useQuasar } from 'quasar'; import axios from 'axios'; @@ -15,7 +15,7 @@ const quasar = useQuasar(); async function reactivateWorker() { const hasToReactive = tableRef.value.CrudModelRef.formData.find( - (data) => !data.ended + (data) => !data.ended, ); if (hasToReactive) { quasar @@ -38,25 +38,25 @@ const columns = computed(() => [ { name: 'started', label: t('worker.business.tableVisibleColumns.started'), - align: 'left', format: ({ started }) => toDate(started), component: 'date', cardVisible: true, create: true, + width: '90px', }, { name: 'ended', label: t('worker.business.tableVisibleColumns.ended'), - align: 'left', format: ({ ended }) => toDate(ended), component: 'date', cardVisible: true, create: true, + width: '90px', }, { label: t('worker.business.tableVisibleColumns.company'), - align: 'left', + toolTip: t('worker.business.tableVisibleColumns.company'), name: 'companyCodeFk', component: 'select', attrs: { @@ -65,23 +65,23 @@ const columns = computed(() => [ optionLabel: 'code', optionValue: 'code', }, - cardVisible: true, create: true, + width: '60px', }, { - align: 'left', name: 'reasonEndFk', component: 'select', label: t('worker.business.tableVisibleColumns.reasonEnd'), + toolTip: t('worker.business.tableVisibleColumns.reasonEnd'), attrs: { url: 'BusinessReasonEnds', fields: ['id', 'reason'], optionLabel: 'reason', }, cardVisible: true, + format: ({ reason }, dashIfEmpty) => dashIfEmpty(reason), }, { - align: 'left', name: 'departmentFk', component: 'select', label: t('worker.business.tableVisibleColumns.department'), @@ -89,15 +89,19 @@ const columns = computed(() => [ url: 'Departments', fields: ['id', 'name'], optionLabel: 'name', + optionValue: 'id', }, cardVisible: true, create: true, + width: '80px', + format: ({ departmentName }, dashIfEmpty) => dashIfEmpty(departmentName), }, { align: 'left', name: 'workerBusinessProfessionalCategoryFk', component: 'select', label: t('worker.business.tableVisibleColumns.professionalCategory'), + toolTip: t('worker.business.tableVisibleColumns.professionalCategory'), attrs: { url: 'WorkerBusinessProfessionalCategories', fields: ['id', 'description', 'code'], @@ -105,6 +109,9 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + width: '100px', + format: ({ professionalDescription }, dashIfEmpty) => + dashIfEmpty(professionalDescription), }, { align: 'left', @@ -118,6 +125,8 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + format: ({ calendarTypeDescription }, dashIfEmpty) => + dashIfEmpty(calendarTypeDescription), }, { align: 'left', @@ -131,6 +140,8 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + width: '100px', + format: ({ workCenterName }, dashIfEmpty) => dashIfEmpty(workCenterName), }, { align: 'left', @@ -144,6 +155,7 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + format: ({ payrollDescription }, dashIfEmpty) => dashIfEmpty(payrollDescription), }, { align: 'left', @@ -157,6 +169,7 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + format: ({ occupationName }, dashIfEmpty) => dashIfEmpty(occupationName), }, { align: 'left', @@ -165,6 +178,7 @@ const columns = computed(() => [ component: 'input', cardVisible: true, create: true, + width: '50px', }, { align: 'left', @@ -177,6 +191,8 @@ const columns = computed(() => [ }, cardVisible: true, create: true, + format: ({ workerBusinessTypeName }, dashIfEmpty) => + dashIfEmpty(workerBusinessTypeName), }, { align: 'left', @@ -185,6 +201,7 @@ const columns = computed(() => [ component: 'input', cardVisible: true, create: true, + width: '70px', }, { align: 'left', @@ -193,6 +210,7 @@ const columns = computed(() => [ component: 'input', cardVisible: true, create: true, + width: '70px', }, { name: 'notes', @@ -208,7 +226,7 @@ const columns = computed(() => [ <VnTable ref="tableRef" data-key="WorkerBusiness" - :url="`Workers/${entityId}/Business`" + :url="`Workers/${entityId}/getWorkerBusiness`" save-url="/Businesses/crud" :create="{ urlCreate: `Workers/${entityId}/Business`, @@ -218,13 +236,12 @@ const columns = computed(() => [ }" order="id DESC" :columns="columns" - default-mode="card" auto-load - :disable-option="{ table: true }" + :disable-option="{ card: true }" :right-search="false" - card-class="grid-two q-gutter-x-xl q-gutter-y-md q-pr-lg q-py-lg" :is-editable="true" :use-model="true" + :right-search-icon="false" @save-changes="(data) => reactivateWorker(data)" /> </template> diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index de3f634e2..0e946f1dd 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -111,6 +111,7 @@ const handlePhotoUpdated = (evt = false) => { <template #body="{ entity }"> <VnLv :label="t('globals.user')" :value="entity.user?.name" /> <VnLv + class="ellipsis-text" :label="t('globals.params.email')" :value="entity.user?.emailUser?.email" copy @@ -177,6 +178,12 @@ const handlePhotoUpdated = (evt = false) => { .photo { height: 256px; } +.ellipsis-text { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} </style> <i18n> From 24b4d0071a2b9535d1cf9e6985dff50245bf98fc Mon Sep 17 00:00:00 2001 From: pablone <pablone@verdnatura.es> Date: Sun, 23 Feb 2025 12:57:57 +0100 Subject: [PATCH 10/28] feat: enhance item tags with data attributes for improved testing --- src/pages/Item/Card/ItemTags.vue | 6 ++- test/cypress/integration/item/itemTag.spec.js | 41 ++++++++++--------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue index 5876cf8dc..5a7d7f818 100644 --- a/src/pages/Item/Card/ItemTags.vue +++ b/src/pages/Item/Card/ItemTags.vue @@ -87,7 +87,7 @@ const insertTag = (rows) => { tagFk: undefined, }" :default-remove="false" - :filter="{ + :user-filter="{ fields: ['id', 'itemFk', 'tagFk', 'value', 'priority'], where: { itemFk: route.params.id }, include: { @@ -119,6 +119,7 @@ const insertTag = (rows) => { " :required="true" :rules="validate('itemTag.tagFk')" + :data-cy="`tag${row?.tag?.name}`" /> <VnSelect v-if="row.tag?.isFree === false" @@ -145,6 +146,7 @@ const insertTag = (rows) => { :label="t('itemTags.value')" :is-clearable="false" @keyup.enter.stop="(data) => itemTagsRef.onSubmit(data)" + :data-cy="`tag${row?.tag?.name}Value`" /> <VnInput :label="t('itemBasicData.relevancy')" @@ -162,6 +164,7 @@ const insertTag = (rows) => { name="delete" size="sm" dense + :data-cy="`deleteTag${row?.tag?.name}`" > <QTooltip> {{ t('itemTags.removeTag') }} @@ -177,6 +180,7 @@ const insertTag = (rows) => { icon="add" shortcut="+" fab + data-cy="createNewTag" > <QTooltip> {{ t('itemTags.addTag') }} diff --git a/test/cypress/integration/item/itemTag.spec.js b/test/cypress/integration/item/itemTag.spec.js index 10d68d08a..17423bc51 100644 --- a/test/cypress/integration/item/itemTag.spec.js +++ b/test/cypress/integration/item/itemTag.spec.js @@ -1,33 +1,36 @@ -/// <reference types="cypress" /> describe('Item tag', () => { beforeEach(() => { cy.viewport(1920, 1080); cy.login('developer'); cy.visit(`/#/item/1/tags`); + cy.get('.q-page').should('be.visible'); + cy.waitForElement('[data-cy="itemTags"]'); }); + const createNewTag = 'createNewTag'; + const saveBtn = 'crudModelDefaultSaveBtn'; + const newTag = 'tagundefined'; + it('should throw an error adding an existent tag', () => { - cy.get('.q-page').should('be.visible'); - cy.get('.q-page-sticky > div').click(); - cy.get('.q-page-sticky > div').click(); - cy.dataCy('Tag_select').eq(7).type('Tallos'); - cy.get('.q-menu .q-item').contains('Tallos').click(); - cy.get(':nth-child(8) > [label="Value"]').type('1'); - +cy.dataCy('crudModelDefaultSaveBtn').click(); - cy.checkNotification("The tag or priority can't be repeated for an item"); + cy.dataCy(createNewTag).click(); + cy.dataCy(newTag).should('be.visible').click().type('Genero{enter}'); + cy.dataCy('tagGeneroValue').eq(1).should('be.visible'); + cy.dataCy(saveBtn).click(); + cy.get('.q-notification__message').should( + 'have.text', + "The tag or priority can't be repeated for an item", + ); }); it('should add a new tag', () => { - cy.get('.q-page').should('be.visible'); - cy.get('.q-page-sticky > div').click(); - cy.get('.q-page-sticky > div').click(); - cy.dataCy('Tag_select').eq(7).click(); - cy.get('.q-menu .q-item').contains('Ancho de la base').type('{enter}'); - cy.get(':nth-child(8) > [label="Value"]').type('50'); - cy.dataCy('crudModelDefaultSaveBtn').click(); + cy.dataCy(createNewTag).click(); + cy.dataCy(newTag).should('be.visible').click().type('Forma{enter}'); + cy.dataCy('tagFormaValue').should('be.visible').type('50'); + cy.dataCy(saveBtn).click(); + cy.checkNotification('Data saved'); - cy.dataCy('itemTags').children(':nth-child(8)').find('.justify-center > .q-icon').click(); - cy.dataCy('VnConfirm_confirm').click(); + cy.dataCy('deleteTagForma').should('be.visible').click(); + cy.dataCy('VnConfirm_confirm').should('be.visible').click(); cy.checkNotification('Data saved'); }); -}); \ No newline at end of file +}); From 3b5c4731f09e16d916f3b4878698c5457d7d45db Mon Sep 17 00:00:00 2001 From: carlossa <carlossa@verdnatura.es> Date: Mon, 24 Feb 2025 08:20:04 +0100 Subject: [PATCH 11/28] fix: hotfix filters --- src/pages/Customer/CustomerFilter.vue | 2 +- src/pages/Customer/CustomerList.vue | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index eae97d1be..9b883daad 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -1,4 +1,3 @@ - <script setup> import { useI18n } from 'vue-i18n'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; @@ -148,6 +147,7 @@ const exprBuilder = (param, value) => { outlined rounded auto-load + sortBy="name ASC" /></QItemSection> </QItem> <QItem class="q-mb-sm"> diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index 3c638b612..2f2dd5978 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -78,10 +78,20 @@ const columns = computed(() => [ component: 'select', attrs: { url: 'Workers/activeWithInheritedRole', - fields: ['id', 'name'], + fields: ['id', 'name', 'firstName'], where: { role: 'salesPerson' }, optionFilter: 'firstName', }, + columnFilter: { + component: 'select', + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name', 'firstName'], + where: { role: 'salesPerson' }, + optionLabel: 'firstName', + optionValue: 'id', + }, + }, create: false, columnField: { component: null, From 1e9e5e647d5bb0af8ea8b89ffd502754cb9ab027 Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Mon, 24 Feb 2025 10:05:13 +0100 Subject: [PATCH 12/28] fix: refs #6919 update customer data retrieval to use useArrayData for improved reactivity --- src/pages/Customer/components/CustomerSamplesCreate.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Customer/components/CustomerSamplesCreate.vue b/src/pages/Customer/components/CustomerSamplesCreate.vue index 8d241441d..7624a3f98 100644 --- a/src/pages/Customer/components/CustomerSamplesCreate.vue +++ b/src/pages/Customer/components/CustomerSamplesCreate.vue @@ -39,7 +39,7 @@ const optionsSamplesVisible = ref([]); const sampleType = ref({ hasPreview: false }); const initialData = reactive({}); const entityId = computed(() => route.params.id); -const customer = computed(() => state.get('Customer')); +const customer = computed(() => useArrayData('Customer').store?.data); const filterEmailUsers = { where: { userFk: user.value.id } }; const filterClientsAddresses = { include: [ @@ -65,9 +65,9 @@ const filterSamplesVisible = { defineEmits(['confirm', ...useDialogPluginComponent.emits]); onBeforeMount(async () => { - initialData.clientFk = customer.value.id; - initialData.recipient = customer.value.email; - initialData.recipientId = customer.value.id; + initialData.clientFk = customer.value?.id; + initialData.recipient = customer.value?.email; + initialData.recipientId = customer.value?.id; }); const setEmailUser = (data) => { From 5e8ad9df11e5550ec4c4345021a374a8ce0409bf Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Mon, 24 Feb 2025 10:13:09 +0100 Subject: [PATCH 13/28] feat: refs #6919 add useArrayData import to CustomerSamplesCreate for improved data handling --- src/pages/Customer/components/CustomerSamplesCreate.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Customer/components/CustomerSamplesCreate.vue b/src/pages/Customer/components/CustomerSamplesCreate.vue index 7624a3f98..1294a5d25 100644 --- a/src/pages/Customer/components/CustomerSamplesCreate.vue +++ b/src/pages/Customer/components/CustomerSamplesCreate.vue @@ -18,6 +18,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import CustomerSamplesPreview from 'src/pages/Customer/components/CustomerSamplesPreview.vue'; import FormPopup from 'src/components/FormPopup.vue'; +import { useArrayData } from 'src/composables/useArrayData'; const { dialogRef, onDialogOK } = useDialogPluginComponent(); From 09b613cac19ed233ecfeb3a2012f53bcc52488aa Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Mon, 24 Feb 2025 11:03:14 +0100 Subject: [PATCH 14/28] refactor: refs #8372 update FormModelPopup to use props for save and continue logic --- src/components/FormModelPopup.vue | 13 +++++++------ src/components/VnTable/VnTable.vue | 8 -------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/components/FormModelPopup.vue b/src/components/FormModelPopup.vue index 672eeff7a..db9974422 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -6,7 +6,7 @@ import FormModel from 'components/FormModel.vue'; const emit = defineEmits(['onDataSaved', 'onDataCanceled']); -defineProps({ +const props = defineProps({ title: { type: String, default: '', @@ -25,10 +25,14 @@ const { t } = useI18n(); const formModelRef = ref(null); const closeButton = ref(null); -const isSaveAndContinue = ref(false); +const isSaveAndContinue = ref(props.showSaveAndContinueBtn); +const isLoading = computed(() => formModelRef.value?.isLoading); +const reset = computed(() => formModelRef.value?.reset); + const onDataSaved = (formData, requestResponse) => { - if (closeButton.value && !isSaveAndContinue.value) closeButton.value.click(); + if (!isSaveAndContinue.value) closeButton.value?.click(); emit('onDataSaved', formData, requestResponse); + isSaveAndContinue.value = props.showSaveAndContinueBtn; }; const onClick = async (saveAndContinue) => { @@ -36,9 +40,6 @@ const onClick = async (saveAndContinue) => { await formModelRef.value.save(); }; -const isLoading = computed(() => formModelRef.value?.isLoading); -const reset = computed(() => formModelRef.value?.reset); - defineExpose({ isLoading, onDataSaved, diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 105010140..7ff56860f 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -961,14 +961,6 @@ function cardClick(_, row) { transition-show="scale" transition-hide="scale" :full-width="createComplement?.isFullWidth ?? false" - @before-hide=" - () => { - if (createRef.isSaveAndContinue) { - showForm = true; - createForm.formInitialData = { ...create.formInitialData }; - } - } - " data-cy="vn-table-create-dialog" > <FormModelPopup From f551e1a14a71456c0bcff98fb175354e1cc89a3f Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Mon, 24 Feb 2025 11:06:20 +0100 Subject: [PATCH 15/28] refactor: refs #8372 simplify cancel btn click --- src/components/FormModelPopup.vue | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/FormModelPopup.vue b/src/components/FormModelPopup.vue index db9974422..e87de5c65 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -75,10 +75,7 @@ defineExpose({ data-cy="FormModelPopup_cancel" v-close-popup z-max - @click=" - isSaveAndContinue = false; - emit('onDataCanceled'); - " + @click="emit('onDataCanceled')" /> <QBtn :flat="showSaveAndContinueBtn" From b26db960be4218056d70e015e7b8d6a5758270ba Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Mon, 24 Feb 2025 11:43:29 +0100 Subject: [PATCH 16/28] refactor: refs #8372 prueba --- src/components/FormModelPopup.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/FormModelPopup.vue b/src/components/FormModelPopup.vue index e87de5c65..33041d29a 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -63,6 +63,7 @@ defineExpose({ <h1 class="title">{{ title }}</h1> <p>{{ subtitle }}</p> <slot name="form-inputs" :data="data" :validate="validate" /> + <div class="q-mt-lg row justify-end"> <QBtn :label="t('globals.cancel')" From fbce97dd01049d7817bc6b7d5282f8fe9793f8e3 Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Mon, 24 Feb 2025 11:44:00 +0100 Subject: [PATCH 17/28] chore: refs #8372 rollback --- src/components/FormModelPopup.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/FormModelPopup.vue b/src/components/FormModelPopup.vue index 33041d29a..e87de5c65 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -63,7 +63,6 @@ defineExpose({ <h1 class="title">{{ title }}</h1> <p>{{ subtitle }}</p> <slot name="form-inputs" :data="data" :validate="validate" /> - <div class="q-mt-lg row justify-end"> <QBtn :label="t('globals.cancel')" From 659d87020f3eaea4a86ef0310bda1d4ef78e262d Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Mon, 24 Feb 2025 13:20:59 +0100 Subject: [PATCH 18/28] refactor: refs #8372 update FormModelPopup to enhance save and continue logic with state management --- src/components/FormModelPopup.vue | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/FormModelPopup.vue b/src/components/FormModelPopup.vue index e87de5c65..85943e91e 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -1,6 +1,7 @@ <script setup> -import { ref, computed } from 'vue'; +import { ref, computed, useAttrs, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useState } from 'src/composables/useState'; import FormModel from 'components/FormModel.vue'; @@ -22,17 +23,22 @@ const props = defineProps({ }); const { t } = useI18n(); - +const attrs = useAttrs(); +const state = useState(); const formModelRef = ref(null); const closeButton = ref(null); const isSaveAndContinue = ref(props.showSaveAndContinueBtn); const isLoading = computed(() => formModelRef.value?.isLoading); const reset = computed(() => formModelRef.value?.reset); -const onDataSaved = (formData, requestResponse) => { +const onDataSaved = async (formData, requestResponse) => { if (!isSaveAndContinue.value) closeButton.value?.click(); - emit('onDataSaved', formData, requestResponse); + if (isSaveAndContinue.value) { + await nextTick(); + state.set(attrs.model, attrs.formInitialData); + } isSaveAndContinue.value = props.showSaveAndContinueBtn; + emit('onDataSaved', formData, requestResponse); }; const onClick = async (saveAndContinue) => { From 3a9a9bd517f15f2d2f59e4a71351cfb247297b78 Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Mon, 24 Feb 2025 15:30:30 +0100 Subject: [PATCH 19/28] fix: check type variable --- src/pages/Ticket/Card/TicketEditMana.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Ticket/Card/TicketEditMana.vue b/src/pages/Ticket/Card/TicketEditMana.vue index 14eec9db9..c1bc2639b 100644 --- a/src/pages/Ticket/Card/TicketEditMana.vue +++ b/src/pages/Ticket/Card/TicketEditMana.vue @@ -48,7 +48,7 @@ defineExpose({ save }); <template> <QPopupProxy ref="QPopupProxyRef" data-cy="ticketEditManaProxy"> <div class="container"> - <QSpinner v-if="!mana" color="primary" size="md" /> + <QSpinner v-if="typeof mana === 'number' && mana" color="primary" size="md" /> <div v-else> <div class="header">Mana: {{ toCurrency(mana) }}</div> <div class="q-pa-md"> From ab5ae580b3bfbfb2291b492d25317864c52bdb2c Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Mon, 24 Feb 2025 16:08:40 +0100 Subject: [PATCH 20/28] fix: check type variable --- src/pages/Ticket/Card/TicketEditMana.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/Ticket/Card/TicketEditMana.vue b/src/pages/Ticket/Card/TicketEditMana.vue index 14eec9db9..b3ba870fb 100644 --- a/src/pages/Ticket/Card/TicketEditMana.vue +++ b/src/pages/Ticket/Card/TicketEditMana.vue @@ -48,7 +48,11 @@ defineExpose({ save }); <template> <QPopupProxy ref="QPopupProxyRef" data-cy="ticketEditManaProxy"> <div class="container"> - <QSpinner v-if="!mana" color="primary" size="md" /> + <QSpinner + v-if="!(typeof mana === 'number' && mana >= 0)" + color="primary" + size="md" + /> <div v-else> <div class="header">Mana: {{ toCurrency(mana) }}</div> <div class="q-pa-md"> From 223a1ea4490ea6ad2a00c60297fd3c74cd713338 Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Mon, 24 Feb 2025 15:52:21 +0000 Subject: [PATCH 21/28] revert 1015acefb7e400be2d8b5958dba69b4d98276b34 revert Merge branch 'test' into master --- cypress.config.js | 4 +- package.json | 144 +- quasar.config.js | 1 + src/boot/defaults/constants.js | 2 - src/boot/keyShortcut.js | 17 +- src/boot/qformMixin.js | 23 +- src/boot/quasar.js | 1 - src/components/CreateBankEntityForm.vue | 2 +- src/components/CrudModel.vue | 16 +- src/components/FilterTravelForm.vue | 4 +- src/components/FormModel.vue | 46 +- src/components/FormModelPopup.vue | 52 +- src/components/ItemsFilterPanel.vue | 4 +- src/components/LeftMenu.vue | 69 +- src/components/LeftMenuItem.vue | 1 - src/components/RefundInvoiceForm.vue | 15 +- src/components/TicketProblems.vue | 84 +- src/components/TransferInvoiceForm.vue | 15 +- src/components/VnTable/VnColumn.vue | 51 +- src/components/VnTable/VnFilter.vue | 58 +- src/components/VnTable/VnOrder.vue | 101 +- src/components/VnTable/VnTable.vue | 573 ++------ src/components/VnTable/VnTableFilter.vue | 57 +- src/components/VnTable/VnVisibleColumn.vue | 19 +- src/components/__tests__/FormModel.spec.js | 12 +- src/components/__tests__/Leftmenu.spec.js | 372 +---- src/components/__tests__/UserPanel.spec.js | 100 +- src/components/common/VnCard.vue | 39 +- src/components/common/VnCardBeta.vue | 61 +- src/components/common/VnCheckbox.vue | 43 - src/components/common/VnColor.vue | 32 - src/components/common/VnComponent.vue | 6 +- src/components/common/VnDmsList.vue | 12 +- src/components/common/VnInput.vue | 22 +- src/components/common/VnInputDate.vue | 8 +- src/components/common/VnInputNumber.vue | 2 - src/components/common/VnPopupProxy.vue | 38 - src/components/common/VnSection.vue | 9 +- src/components/common/VnSelect.vue | 22 +- src/components/common/VnSelectCache.vue | 4 +- src/components/common/VnSelectDialog.vue | 2 + src/components/common/VnSelectSupplier.vue | 6 +- .../common/VnSelectTravelExtended.vue | 50 - .../common/__tests__/VnNotes.spec.js | 151 +-- src/components/ui/CardDescriptor.vue | 52 +- src/components/ui/CardSummary.vue | 14 +- src/components/ui/SkeletonDescriptor.vue | 65 +- src/components/ui/VnConfirm.vue | 3 +- src/components/ui/VnFilterPanel.vue | 16 +- src/components/ui/VnMoreOptions.vue | 2 +- src/components/ui/VnNotes.vue | 94 +- src/components/ui/VnStockValueDisplay.vue | 41 - src/components/ui/VnSubToolbar.vue | 11 +- .../ui/__tests__/CardSummary.spec.js | 14 +- .../__tests__/useArrayData.spec.js | 29 +- src/composables/checkEntryLock.js | 65 - src/composables/getColAlign.js | 22 - src/composables/useArrayData.js | 13 +- src/composables/useRole.js | 10 - src/css/app.scss | 28 +- src/css/quasar.variables.scss | 6 +- src/filters/toDate.js | 11 +- src/i18n/locale/en.yml | 117 -- src/i18n/locale/es.yml | 225 +--- src/layouts/MainLayout.vue | 2 +- src/layouts/OutLayout.vue | 5 +- src/pages/Account/AccountAliasList.vue | 10 +- src/pages/Account/AccountExprBuilder.js | 18 - src/pages/Account/AccountList.vue | 26 +- src/pages/Account/Alias/AliasExprBuilder.js | 8 - src/pages/Account/Alias/Card/AliasCard.vue | 10 +- .../Account/Alias/Card/AliasDescriptor.vue | 11 +- src/pages/Account/Alias/Card/AliasSummary.vue | 19 +- src/pages/Account/Card/AccountBasicData.vue | 38 +- src/pages/Account/Card/AccountCard.vue | 10 +- src/pages/Account/Card/AccountDescriptor.vue | 43 +- .../Account/Card/AccountDescriptorMenu.vue | 27 +- src/pages/Account/Card/AccountFilter.js | 3 - src/pages/Account/Card/AccountMailAlias.vue | 7 +- src/pages/Account/Card/AccountSummary.vue | 41 +- src/pages/Account/Role/AccountRoles.vue | 18 +- src/pages/Account/Role/Card/RoleBasicData.vue | 14 +- src/pages/Account/Role/Card/RoleCard.vue | 7 +- .../Account/Role/Card/RoleDescriptor.vue | 16 +- src/pages/Account/Role/Card/RoleSummary.vue | 23 +- src/pages/Account/Role/Card/SubRoles.vue | 6 +- src/pages/Account/Role/RoleExprBuilder.js | 16 - src/pages/Claim/Card/ClaimBasicData.vue | 1 + src/pages/Claim/Card/ClaimCard.vue | 9 +- src/pages/Claim/Card/ClaimDescriptor.vue | 17 +- src/pages/Claim/Card/ClaimLines.vue | 8 +- src/pages/Claim/Card/ClaimNotes.vue | 3 +- src/pages/Claim/Card/ClaimPhoto.vue | 4 +- src/pages/Claim/ClaimList.vue | 2 +- src/pages/Customer/Card/CustomerAddress.vue | 8 +- src/pages/Customer/Card/CustomerBalance.vue | 4 +- src/pages/Customer/Card/CustomerBasicData.vue | 4 +- .../Customer/Card/CustomerBillingData.vue | 2 +- src/pages/Customer/Card/CustomerCard.vue | 4 +- .../Customer/Card/CustomerConsumption.vue | 95 +- src/pages/Customer/Card/CustomerContacts.vue | 2 +- .../Customer/Card/CustomerCreditContracts.vue | 2 +- .../Customer/Card/CustomerDescriptor.vue | 42 +- .../Customer/Card/CustomerDescriptorMenu.vue | 17 - .../Customer/Card/CustomerFileManagement.vue | 2 +- .../Customer/Card/CustomerFiscalData.vue | 32 +- src/pages/Customer/Card/CustomerNotes.vue | 1 - src/pages/Customer/Card/CustomerSamples.vue | 2 +- src/pages/Customer/Card/CustomerWebAccess.vue | 2 +- src/pages/Customer/CustomerFilter.vue | 6 +- src/pages/Customer/CustomerList.vue | 4 +- .../Customer/Defaulter/CustomerDefaulter.vue | 2 +- .../components/CustomerAddressEdit.vue | 4 +- .../components/CustomerNewPayment.vue | 6 +- .../components/CustomerSamplesCreate.vue | 9 +- src/pages/Customer/locale/en.yml | 3 - src/pages/Customer/locale/es.yml | 3 - .../Department/Card/DepartmentBasicData.vue | 35 +- .../Department/Card/DepartmentCard.vue | 4 +- .../Department/Card/DepartmentDescriptor.vue | 23 +- .../Card/DepartmentDescriptorProxy.vue | 0 .../Department/Card/DepartmentSummary.vue | 2 +- .../Card/DepartmentSummaryDialog.vue | 0 src/pages/Entry/Card/EntryBasicData.vue | 63 +- src/pages/Entry/Card/EntryBuys.vue | 1196 ++++++----------- src/pages/Entry/Card/EntryCard.vue | 6 +- src/pages/Entry/Card/EntryDescriptor.vue | 158 +-- src/pages/Entry/Card/EntryFilter.js | 17 +- src/pages/Entry/Card/EntryNotes.vue | 4 +- src/pages/Entry/Card/EntrySummary.vue | 392 ++++-- src/pages/Entry/EntryFilter.vue | 277 ++-- src/pages/Entry/EntryList.vue | 372 ++--- src/pages/Entry/EntryStockBought.vue | 18 +- src/pages/Entry/EntryStockBoughtDetail.vue | 22 +- src/pages/Entry/locale/en.yml | 82 +- src/pages/Entry/locale/es.yml | 105 +- .../InvoiceIn/Card/InvoiceInBasicData.vue | 6 +- src/pages/InvoiceIn/Card/InvoiceInCard.vue | 41 +- .../InvoiceIn/Card/InvoiceInDescriptor.vue | 33 +- .../Card/InvoiceInDescriptorMenu.vue | 4 +- src/pages/InvoiceIn/Card/InvoiceInDueDay.vue | 26 +- src/pages/InvoiceIn/Card/InvoiceInFilter.js | 33 - .../InvoiceIn/Card/InvoiceInIntrastat.vue | 2 +- src/pages/InvoiceIn/Card/InvoiceInSummary.vue | 13 +- src/pages/InvoiceIn/Card/InvoiceInVat.vue | 78 +- src/pages/InvoiceIn/InvoiceInList.vue | 5 +- src/pages/InvoiceIn/InvoiceInToBook.vue | 56 +- src/pages/InvoiceIn/locale/en.yml | 5 +- src/pages/InvoiceIn/locale/es.yml | 9 +- src/pages/InvoiceOut/Card/InvoiceOutCard.vue | 4 +- .../InvoiceOut/Card/InvoiceOutDescriptor.vue | 28 +- src/pages/InvoiceOut/Card/InvoiceOutFilter.js | 16 - .../{components => Card}/CreateGenusForm.vue | 0 .../{components => Card}/CreateSpecieForm.vue | 0 src/pages/Item/Card/ItemBarcode.vue | 2 +- src/pages/Item/Card/ItemBasicData.vue | 42 +- src/pages/Item/Card/ItemBotanical.vue | 4 +- src/pages/Item/Card/ItemCard.vue | 2 +- src/pages/Item/Card/ItemDescriptor.vue | 26 +- src/pages/Item/Card/ItemDescriptorProxy.vue | 6 +- src/pages/Item/Card/ItemShelving.vue | 10 +- src/pages/Item/Card/ItemTags.vue | 2 +- src/pages/Item/ItemFixedPrice.vue | 16 +- .../Item/ItemType/Card/ItemTypeBasicData.vue | 7 +- src/pages/Item/ItemType/Card/ItemTypeCard.vue | 6 +- .../Item/ItemType/Card/ItemTypeDescriptor.vue | 40 +- .../Item/ItemType/Card/ItemTypeFilter.js | 8 - .../Item/ItemType/Card/ItemTypeSummary.vue | 15 +- src/pages/Item/components/ItemProposal.vue | 332 ----- .../Item/components/ItemProposalProxy.vue | 56 - src/pages/Item/locale/en.yml | 24 +- src/pages/Item/locale/es.yml | 31 +- src/pages/Monitor/MonitorOrders.vue | 2 +- src/pages/Monitor/locale/en.yml | 1 - src/pages/Monitor/locale/es.yml | 1 - .../Order/Card/CatalogFilterValueDialog.vue | 2 +- src/pages/Order/Card/OrderBasicData.vue | 6 +- src/pages/Order/Card/OrderCard.vue | 4 +- src/pages/Order/Card/OrderCatalogFilter.vue | 4 +- .../Order/Card/OrderCatalogItemDialog.vue | 8 +- src/pages/Order/Card/OrderDescriptor.vue | 38 +- src/pages/Order/Card/OrderFilter.js | 26 - src/pages/Order/Card/OrderLines.vue | 4 +- src/pages/Order/Card/OrderSummary.vue | 2 +- src/pages/Order/OrderList.vue | 7 +- .../Parking/Card/ParkingBasicData.vue | 18 +- .../Parking/Card/ParkingCard.vue | 6 +- .../Parking/Card/ParkingDescriptor.vue | 16 +- .../Parking/Card/ParkingLog.vue | 0 .../Parking/Card/ParkingSummary.vue | 0 .../{Shelving => }/Parking/ParkingFilter.vue | 0 .../{Shelving => }/Parking/ParkingList.vue | 13 +- .../{Shelving => }/Parking/locale/en.yml | 0 .../{Shelving => }/Parking/locale/es.yml | 0 src/pages/Route/Agency/AgencyList.vue | 4 +- .../Route/Agency/Card/AgencyBasicData.vue | 2 +- src/pages/Route/Agency/Card/AgencyCard.vue | 2 +- .../Route/Agency/Card/AgencyDescriptor.vue | 1 + .../Route/Agency/Card/AgencyWorkcenter.vue | 2 +- src/pages/Route/Card/RouteCard.vue | 5 +- src/pages/Route/Card/RouteDescriptor.vue | 70 +- src/pages/Route/Card/RouteFilter.js | 39 - src/pages/Route/Card/RouteFilter.vue | 2 +- src/pages/Route/Card/RouteForm.vue | 54 +- src/pages/Route/Roadmap/RoadmapBasicData.vue | 5 +- src/pages/Route/Roadmap/RoadmapCard.vue | 2 +- src/pages/Route/Roadmap/RoadmapDescriptor.vue | 18 +- src/pages/Route/Roadmap/RoadmapFilter.js | 3 - src/pages/Route/Roadmap/RoadmapStops.vue | 2 +- src/pages/Route/Roadmap/RoadmapSummary.vue | 3 +- src/pages/Route/RouteExtendedList.vue | 152 +-- src/pages/Route/RouteList.vue | 31 - src/pages/Route/RouteTickets.vue | 18 +- .../Route/Vehicle/Card/VehicleBasicData.vue | 162 --- src/pages/Route/Vehicle/Card/VehicleCard.vue | 13 - .../Route/Vehicle/Card/VehicleDescriptor.vue | 49 - .../Route/Vehicle/Card/VehicleSummary.vue | 127 -- src/pages/Route/Vehicle/VehicleFilter.js | 76 -- src/pages/Route/Vehicle/VehicleList.vue | 224 --- src/pages/Route/Vehicle/locale/en.yml | 20 - src/pages/Route/Vehicle/locale/es.yml | 20 - src/pages/Shelving/Card/ShelvingCard.vue | 4 +- .../Shelving/Card/ShelvingDescriptor.vue | 30 +- src/pages/Shelving/Card/ShelvingFilter.js | 15 - src/pages/Shelving/Card/ShelvingForm.vue | 32 +- src/pages/Shelving/Card/ShelvingSearchbar.vue | 8 +- src/pages/Shelving/Card/ShelvingSummary.vue | 37 +- .../Shelving/Parking/Card/ParkingFilter.js | 4 - .../Shelving/Parking/ParkingExprBuilder.js | 10 - src/pages/Shelving/ShelvingExprBuilder.js | 10 - src/pages/Shelving/ShelvingList.vue | 26 +- src/pages/Supplier/Card/SupplierAccounts.vue | 6 +- src/pages/Supplier/Card/SupplierAddresses.vue | 2 +- .../Supplier/Card/SupplierAgencyTerm.vue | 2 +- src/pages/Supplier/Card/SupplierBasicData.vue | 3 +- src/pages/Supplier/Card/SupplierCard.vue | 16 +- .../Supplier/Card/SupplierConsumption.vue | 103 +- src/pages/Supplier/Card/SupplierContacts.vue | 2 +- .../Supplier/Card/SupplierDescriptor.vue | 49 +- src/pages/Supplier/Card/SupplierFilter.js | 35 - .../Supplier/Card/SupplierFiscalData.vue | 22 +- src/pages/Supplier/SupplierList.vue | 91 +- src/pages/Supplier/SupplierListFilter.vue | 122 ++ .../Ticket/Card/BasicData/TicketBasicData.vue | 16 +- .../Card/BasicData/TicketBasicDataForm.vue | 4 +- .../Card/BasicData/TicketBasicDataView.vue | 116 +- src/pages/Ticket/Card/TicketCard.vue | 8 +- src/pages/Ticket/Card/TicketComponents.vue | 2 +- src/pages/Ticket/Card/TicketDescriptor.vue | 139 +- src/pages/Ticket/Card/TicketExpedition.vue | 2 +- src/pages/Ticket/Card/TicketFilter.js | 72 - src/pages/Ticket/Card/TicketNotes.vue | 4 +- src/pages/Ticket/Card/TicketPackage.vue | 4 +- src/pages/Ticket/Card/TicketSale.vue | 60 +- src/pages/Ticket/Card/TicketService.vue | 6 +- src/pages/Ticket/Card/TicketSplit.vue | 37 - src/pages/Ticket/Card/TicketSummary.vue | 81 +- src/pages/Ticket/Card/TicketTracking.vue | 4 +- src/pages/Ticket/Card/TicketTransfer.vue | 131 +- src/pages/Ticket/Card/TicketTransferProxy.vue | 54 - src/pages/Ticket/Card/components/split.js | 22 - .../Ticket/Negative/TicketLackDetail.vue | 198 --- .../Ticket/Negative/TicketLackFilter.vue | 175 --- src/pages/Ticket/Negative/TicketLackList.vue | 227 ---- src/pages/Ticket/Negative/TicketLackTable.vue | 356 ----- .../Negative/components/ChangeItemDialog.vue | 90 -- .../components/ChangeQuantityDialog.vue | 84 -- .../Negative/components/ChangeStateDialog.vue | 91 -- src/pages/Ticket/TicketFuture.vue | 561 +++++--- src/pages/Ticket/TicketFutureFilter.vue | 4 +- src/pages/Ticket/locale/en.yml | 87 +- src/pages/Ticket/locale/es.yml | 83 -- src/pages/Travel/Card/TravelBasicData.vue | 19 +- src/pages/Travel/Card/TravelCard.vue | 36 +- src/pages/Travel/Card/TravelDescriptor.vue | 1 + src/pages/Travel/Card/TravelFilter.js | 1 - src/pages/Travel/Card/TravelSummary.vue | 8 - src/pages/Travel/Card/TravelThermographs.vue | 2 +- src/pages/Travel/ExtraCommunityFilter.vue | 2 +- src/pages/Travel/TravelList.vue | 24 - src/pages/Wagon/Card/WagonCard.vue | 2 +- src/pages/Wagon/Type/WagonTypeList.vue | 8 +- src/pages/Worker/Card/WorkerBasicData.vue | 17 +- src/pages/Worker/Card/WorkerCalendar.vue | 32 +- .../Worker/Card/WorkerCalendarFilter.vue | 2 + src/pages/Worker/Card/WorkerCard.vue | 7 +- src/pages/Worker/Card/WorkerDescriptor.vue | 9 +- .../Worker/Card/WorkerDescriptorProxy.vue | 7 +- src/pages/Worker/Card/WorkerFormation.vue | 3 +- src/pages/Worker/Card/WorkerMedical.vue | 16 - src/pages/Worker/Card/WorkerOperator.vue | 19 +- src/pages/Worker/Card/WorkerPda.vue | 10 +- src/pages/Worker/Card/WorkerPit.vue | 2 +- src/pages/Worker/Card/WorkerSummary.vue | 2 +- src/pages/Worker/Card/WorkerTimeControl.vue | 16 +- src/pages/Worker/WorkerDepartmentTree.vue | 4 +- src/pages/Zone/Card/ZoneBasicData.vue | 33 +- src/pages/Zone/Card/ZoneCard.vue | 12 +- src/pages/Zone/Card/ZoneDescriptor.vue | 44 +- src/pages/Zone/Card/ZoneEvents.vue | 4 +- src/pages/Zone/Card/ZoneFilter.js | 10 - src/pages/Zone/Card/ZoneSearchbar.vue | 41 +- src/pages/Zone/Card/ZoneSummary.vue | 18 +- src/pages/Zone/Card/ZoneWarehouses.vue | 2 +- src/pages/Zone/Delivery/ZoneDeliveryList.vue | 2 +- src/pages/Zone/Upcoming/ZoneUpcomingList.vue | 2 +- src/pages/Zone/ZoneList.vue | 29 +- src/router/modules/account/aliasCard.js | 2 +- src/router/modules/account/roleCard.js | 1 - src/router/modules/entry.js | 17 +- src/router/modules/route.js | 52 - src/router/modules/shelving.js | 11 +- src/router/modules/supplier.js | 315 ++--- src/router/modules/ticket.js | 34 +- src/router/modules/worker.js | 9 +- .../__tests__/useNavigationStore.spec.js | 153 --- src/stores/useArrayDataStore.js | 1 - src/utils/notifyResults.js | 19 - .../integration/Order/orderCatalog.spec.js | 1 + .../integration/entry/entryList.spec.js | 224 --- .../integration/entry/stockBought.spec.js | 37 +- .../invoiceIn/invoiceInBasicData.spec.js | 27 +- .../invoiceIn/invoiceInVat.spec.js | 2 +- .../invoiceOutNegativeBases.spec.js | 4 +- .../integration/item/ItemProposal.spec.js | 11 - test/cypress/integration/item/itemTag.spec.js | 5 +- .../parking/parkingBasicData.spec.js | 4 +- .../route/agency/agencyWorkCenter.spec.js | 1 - .../integration/route/routeList.spec.js | 19 +- .../route/vehicle/vehicleDescriptor.spec.js | 13 - .../ticket/negative/TicketLackDetail.spec.js | 147 -- .../ticket/negative/TicketLackList.spec.js | 36 - .../integration/ticket/ticketList.spec.js | 25 - .../vnComponent/VnShortcut.spec.js | 11 - .../wagon/wagonType/wagonTypeCreate.spec.js | 2 +- .../integration/zone/zoneBasicData.spec.js | 16 +- test/cypress/support/commands.js | 71 +- test/cypress/support/waitUntil.js | 2 +- 338 files changed, 4377 insertions(+), 9582 deletions(-) delete mode 100644 src/boot/defaults/constants.js delete mode 100644 src/components/common/VnCheckbox.vue delete mode 100644 src/components/common/VnColor.vue delete mode 100644 src/components/common/VnPopupProxy.vue delete mode 100644 src/components/common/VnSelectTravelExtended.vue delete mode 100644 src/components/ui/VnStockValueDisplay.vue delete mode 100644 src/composables/checkEntryLock.js delete mode 100644 src/composables/getColAlign.js delete mode 100644 src/pages/Account/AccountExprBuilder.js delete mode 100644 src/pages/Account/Alias/AliasExprBuilder.js delete mode 100644 src/pages/Account/Card/AccountFilter.js delete mode 100644 src/pages/Account/Role/RoleExprBuilder.js rename src/pages/{Worker => }/Department/Card/DepartmentBasicData.vue (73%) rename src/pages/{Worker => }/Department/Card/DepartmentCard.vue (70%) rename src/pages/{Worker => }/Department/Card/DepartmentDescriptor.vue (84%) rename src/pages/{Worker => }/Department/Card/DepartmentDescriptorProxy.vue (100%) rename src/pages/{Worker => }/Department/Card/DepartmentSummary.vue (99%) rename src/pages/{Worker => }/Department/Card/DepartmentSummaryDialog.vue (100%) delete mode 100644 src/pages/InvoiceIn/Card/InvoiceInFilter.js delete mode 100644 src/pages/InvoiceOut/Card/InvoiceOutFilter.js rename src/pages/Item/{components => Card}/CreateGenusForm.vue (100%) rename src/pages/Item/{components => Card}/CreateSpecieForm.vue (100%) delete mode 100644 src/pages/Item/ItemType/Card/ItemTypeFilter.js delete mode 100644 src/pages/Item/components/ItemProposal.vue delete mode 100644 src/pages/Item/components/ItemProposalProxy.vue delete mode 100644 src/pages/Order/Card/OrderFilter.js rename src/pages/{Shelving => }/Parking/Card/ParkingBasicData.vue (68%) rename src/pages/{Shelving => }/Parking/Card/ParkingCard.vue (53%) rename src/pages/{Shelving => }/Parking/Card/ParkingDescriptor.vue (58%) rename src/pages/{Shelving => }/Parking/Card/ParkingLog.vue (100%) rename src/pages/{Shelving => }/Parking/Card/ParkingSummary.vue (100%) rename src/pages/{Shelving => }/Parking/ParkingFilter.vue (100%) rename src/pages/{Shelving => }/Parking/ParkingList.vue (90%) rename src/pages/{Shelving => }/Parking/locale/en.yml (100%) rename src/pages/{Shelving => }/Parking/locale/es.yml (100%) delete mode 100644 src/pages/Route/Card/RouteFilter.js delete mode 100644 src/pages/Route/Roadmap/RoadmapFilter.js delete mode 100644 src/pages/Route/Vehicle/Card/VehicleBasicData.vue delete mode 100644 src/pages/Route/Vehicle/Card/VehicleCard.vue delete mode 100644 src/pages/Route/Vehicle/Card/VehicleDescriptor.vue delete mode 100644 src/pages/Route/Vehicle/Card/VehicleSummary.vue delete mode 100644 src/pages/Route/Vehicle/VehicleFilter.js delete mode 100644 src/pages/Route/Vehicle/VehicleList.vue delete mode 100644 src/pages/Route/Vehicle/locale/en.yml delete mode 100644 src/pages/Route/Vehicle/locale/es.yml delete mode 100644 src/pages/Shelving/Card/ShelvingFilter.js delete mode 100644 src/pages/Shelving/Parking/Card/ParkingFilter.js delete mode 100644 src/pages/Shelving/Parking/ParkingExprBuilder.js delete mode 100644 src/pages/Shelving/ShelvingExprBuilder.js delete mode 100644 src/pages/Supplier/Card/SupplierFilter.js create mode 100644 src/pages/Supplier/SupplierListFilter.vue delete mode 100644 src/pages/Ticket/Card/TicketFilter.js delete mode 100644 src/pages/Ticket/Card/TicketSplit.vue delete mode 100644 src/pages/Ticket/Card/TicketTransferProxy.vue delete mode 100644 src/pages/Ticket/Card/components/split.js delete mode 100644 src/pages/Ticket/Negative/TicketLackDetail.vue delete mode 100644 src/pages/Ticket/Negative/TicketLackFilter.vue delete mode 100644 src/pages/Ticket/Negative/TicketLackList.vue delete mode 100644 src/pages/Ticket/Negative/TicketLackTable.vue delete mode 100644 src/pages/Ticket/Negative/components/ChangeItemDialog.vue delete mode 100644 src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue delete mode 100644 src/pages/Ticket/Negative/components/ChangeStateDialog.vue delete mode 100644 src/pages/Zone/Card/ZoneFilter.js delete mode 100644 src/stores/__tests__/useNavigationStore.spec.js delete mode 100644 src/utils/notifyResults.js delete mode 100644 test/cypress/integration/entry/entryList.spec.js delete mode 100644 test/cypress/integration/item/ItemProposal.spec.js delete mode 100644 test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js delete mode 100644 test/cypress/integration/ticket/negative/TicketLackDetail.spec.js delete mode 100644 test/cypress/integration/ticket/negative/TicketLackList.spec.js diff --git a/cypress.config.js b/cypress.config.js index a9e27fcfd..1924144f6 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -14,8 +14,8 @@ export default defineConfig({ downloadsFolder: 'test/cypress/downloads', video: false, specPattern: 'test/cypress/integration/**/*.spec.js', - experimentalRunAllSpecs: false, - watchForFileChanges: false, + experimentalRunAllSpecs: true, + watchForFileChanges: true, reporter: 'cypress-mochawesome-reporter', reporterOptions: { charts: true, diff --git a/package.json b/package.json index d23ed0ced..17f39cad7 100644 --- a/package.json +++ b/package.json @@ -1,74 +1,74 @@ { - "name": "salix-front", - "version": "25.08.0", - "description": "Salix frontend", - "productName": "Salix", - "author": "Verdnatura", - "private": true, - "packageManager": "pnpm@8.15.1", - "type": "module", - "scripts": { - "resetDatabase": "cd ../salix && gulp docker", - "lint": "eslint --ext .js,.vue ./", - "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", - "test:e2e": "cypress open", - "test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run", - "test": "echo \"See package.json => scripts for available tests.\" && exit 0", - "test:unit": "vitest", - "test:unit:ci": "vitest run", - "commitlint": "commitlint --edit", - "prepare": "npx husky install", - "addReferenceTag": "node .husky/addReferenceTag.js", - "docs:dev": "vitepress dev docs", - "docs:build": "vitepress build docs", - "docs:preview": "vitepress preview docs" - }, - "dependencies": { - "@quasar/cli": "^2.4.1", - "@quasar/extras": "^1.16.16", - "axios": "^1.4.0", - "chromium": "^3.0.3", - "croppie": "^2.6.5", - "moment": "^2.30.1", - "pinia": "^2.1.3", - "quasar": "^2.17.7", - "validator": "^13.9.0", - "vue": "^3.5.13", - "vue-i18n": "^9.3.0", - "vue-router": "^4.2.5" - }, - "devDependencies": { - "@commitlint/cli": "^19.2.1", - "@commitlint/config-conventional": "^19.1.0", - "@intlify/unplugin-vue-i18n": "^0.8.2", - "@pinia/testing": "^0.1.2", - "@quasar/app-vite": "^2.0.8", - "@quasar/quasar-app-extension-qcalendar": "^4.0.2", - "@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0", - "@vue/test-utils": "^2.4.4", - "autoprefixer": "^10.4.14", - "cypress": "^13.6.6", - "cypress-mochawesome-reporter": "^3.8.2", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-cypress": "^4.1.0", - "eslint-plugin-vue": "^9.32.0", - "husky": "^8.0.0", - "postcss": "^8.4.23", - "prettier": "^3.4.2", - "sass": "^1.83.4", - "vitepress": "^1.6.3", - "vitest": "^0.34.0" - }, - "engines": { - "node": "^20 || ^18 || ^16", - "npm": ">= 8.1.2", - "yarn": ">= 1.21.1", - "bun": ">= 1.0.25" - }, - "overrides": { - "@vitejs/plugin-vue": "^5.2.1", - "vite": "^6.0.11", - "vitest": "^0.31.1" - } + "name": "salix-front", + "version": "25.06.0", + "description": "Salix frontend", + "productName": "Salix", + "author": "Verdnatura", + "private": true, + "packageManager": "pnpm@8.15.1", + "type": "module", + "scripts": { + "resetDatabase": "cd ../salix && gulp docker", + "lint": "eslint --ext .js,.vue ./", + "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", + "test:e2e": "cypress open", + "test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run", + "test": "echo \"See package.json => scripts for available tests.\" && exit 0", + "test:unit": "vitest", + "test:unit:ci": "vitest run", + "commitlint": "commitlint --edit", + "prepare": "npx husky install", + "addReferenceTag": "node .husky/addReferenceTag.js", + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "dependencies": { + "@quasar/cli": "^2.4.1", + "@quasar/extras": "^1.16.16", + "axios": "^1.4.0", + "chromium": "^3.0.3", + "croppie": "^2.6.5", + "moment": "^2.30.1", + "pinia": "^2.1.3", + "quasar": "^2.17.7", + "validator": "^13.9.0", + "vue": "^3.5.13", + "vue-i18n": "^9.3.0", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@commitlint/cli": "^19.2.1", + "@commitlint/config-conventional": "^19.1.0", + "@intlify/unplugin-vue-i18n": "^0.8.2", + "@pinia/testing": "^0.1.2", + "@quasar/app-vite": "^2.0.8", + "@quasar/quasar-app-extension-qcalendar": "^4.0.2", + "@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0", + "@vue/test-utils": "^2.4.4", + "autoprefixer": "^10.4.14", + "cypress": "^13.6.6", + "cypress-mochawesome-reporter": "^3.8.2", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-cypress": "^4.1.0", + "eslint-plugin-vue": "^9.32.0", + "husky": "^8.0.0", + "postcss": "^8.4.23", + "prettier": "^3.4.2", + "sass": "^1.83.4", + "vitepress": "^1.6.3", + "vitest": "^0.34.0" + }, + "engines": { + "node": "^20 || ^18 || ^16", + "npm": ">= 8.1.2", + "yarn": ">= 1.21.1", + "bun": ">= 1.0.25" + }, + "overrides": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.0.11", + "vitest": "^0.31.1" + } } \ No newline at end of file diff --git a/quasar.config.js b/quasar.config.js index 9467c92af..6d545c026 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -30,6 +30,7 @@ export default configure(function (/* ctx */) { // --> boot files are part of "main.js" // https://v2.quasar.dev/quasar-cli/boot-files boot: ['i18n', 'axios', 'vnDate', 'validations', 'quasar', 'quasar.defaults'], + // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css css: ['app.scss'], diff --git a/src/boot/defaults/constants.js b/src/boot/defaults/constants.js deleted file mode 100644 index c96ceb2d1..000000000 --- a/src/boot/defaults/constants.js +++ /dev/null @@ -1,2 +0,0 @@ -export const langs = ['en', 'es']; -export const decimalPlaces = 2; diff --git a/src/boot/keyShortcut.js b/src/boot/keyShortcut.js index 6da06c8bf..5afb5b74a 100644 --- a/src/boot/keyShortcut.js +++ b/src/boot/keyShortcut.js @@ -1,6 +1,6 @@ export default { - mounted(el, binding) { - const shortcut = binding.value || '+'; + mounted: function (el, binding) { + const shortcut = binding.value ?? '+'; const { key, ctrl, alt, callback } = typeof shortcut === 'string' @@ -8,24 +8,25 @@ export default { key: shortcut, ctrl: true, alt: true, - callback: () => el?.click(), + callback: () => + document + .querySelector(`button[shortcut="${shortcut}"]`) + ?.click(), } : binding.value; - if (!el.hasAttribute('shortcut')) { - el.setAttribute('shortcut', key); - } - const handleKeydown = (event) => { if (event.key === key && (!ctrl || event.ctrlKey) && (!alt || event.altKey)) { callback(); } }; + // Attach the event listener to the window window.addEventListener('keydown', handleKeydown); + el._handleKeydown = handleKeydown; }, - unmounted(el) { + unmounted: function (el) { if (el._handleKeydown) { window.removeEventListener('keydown', el._handleKeydown); } diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js index 182c51e47..97d80c670 100644 --- a/src/boot/qformMixin.js +++ b/src/boot/qformMixin.js @@ -9,19 +9,19 @@ export default { if (!form) return; try { const inputsFormCard = form.querySelectorAll( - `input:not([disabled]):not([type="checkbox"])`, + `input:not([disabled]):not([type="checkbox"])` ); if (inputsFormCard.length) { focusFirstInput(inputsFormCard[0]); } const textareas = document.querySelectorAll( - 'textarea:not([disabled]), [contenteditable]:not([disabled])', + 'textarea:not([disabled]), [contenteditable]:not([disabled])' ); if (textareas.length) { focusFirstInput(textareas[textareas.length - 1]); } const inputs = document.querySelectorAll( - 'form#formModel input:not([disabled]):not([type="checkbox"])', + 'form#formModel input:not([disabled]):not([type="checkbox"])' ); const input = inputs[0]; if (!input) return; @@ -30,5 +30,22 @@ export default { } catch (error) { console.error(error); } + form.addEventListener('keyup', function (evt) { + if (evt.key === 'Enter' && !that.$attrs['prevent-submit']) { + const input = evt.target; + if (input.type == 'textarea' && evt.shiftKey) { + evt.preventDefault(); + let { selectionStart, selectionEnd } = input; + input.value = + input.value.substring(0, selectionStart) + + '\n' + + input.value.substring(selectionEnd); + selectionStart = selectionEnd = selectionStart + 1; + return; + } + evt.preventDefault(); + that.onSubmit(); + } + }); }, }; diff --git a/src/boot/quasar.js b/src/boot/quasar.js index a8c397b83..547517682 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -51,5 +51,4 @@ export default boot(({ app }) => { await useCau(response, message); }; - app.provide('app', app); }); diff --git a/src/components/CreateBankEntityForm.vue b/src/components/CreateBankEntityForm.vue index 7c4b94a6a..2da3aa994 100644 --- a/src/components/CreateBankEntityForm.vue +++ b/src/components/CreateBankEntityForm.vue @@ -14,7 +14,7 @@ const { t } = useI18n(); const bicInputRef = ref(null); const state = useState(); -const customer = computed(() => state.get('Customer')); +const customer = computed(() => state.get('customer')); const countriesFilter = { fields: ['id', 'name', 'code'], diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index 93a2ac96a..d569dfda1 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -64,10 +64,6 @@ const $props = defineProps({ type: Function, default: null, }, - beforeSaveFn: { - type: Function, - default: null, - }, goTo: { type: String, default: '', @@ -180,11 +176,7 @@ async function saveChanges(data) { hasChanges.value = false; return; } - let changes = data || getChanges(); - if ($props.beforeSaveFn) { - changes = await $props.beforeSaveFn(changes, getChanges); - } - + const changes = data || getChanges(); try { await axios.post($props.saveUrl || $props.url + '/crud', changes); } finally { @@ -237,12 +229,12 @@ async function remove(data) { componentProps: { title: t('globals.confirmDeletion'), message: t('globals.confirmDeletionMessage'), - data: { deletes: ids }, + newData, ids, - promise: saveChanges, }, }) .onOk(async () => { + await saveChanges({ deletes: ids }); newData = newData.filter((form) => !ids.some((id) => id == form[pk])); fetch(newData); }); @@ -382,8 +374,6 @@ watch(formUrl, async () => { @click="onSubmit" :disable="!hasChanges" :title="t('globals.save')" - v-shortcut="'s'" - shortcut="s" data-cy="crudModelDefaultSaveBtn" /> <slot name="moreAfterActions" /> diff --git a/src/components/FilterTravelForm.vue b/src/components/FilterTravelForm.vue index 765d97763..4d43c3810 100644 --- a/src/components/FilterTravelForm.vue +++ b/src/components/FilterTravelForm.vue @@ -181,7 +181,6 @@ const selectTravel = ({ id }) => { color="primary" :disabled="isLoading" :loading="isLoading" - data-cy="save-filter-travel-form" /> </div> <QTable @@ -192,10 +191,9 @@ const selectTravel = ({ id }) => { :no-data-label="t('Enter a new search')" class="q-mt-lg" @row-click="(_, row) => selectTravel(row)" - data-cy="table-filter-travel-form" > <template #body-cell-id="{ row }"> - <QTd auto-width @click.stop data-cy="travelFk-travel-form"> + <QTd auto-width @click.stop> <QBtn flat color="blue">{{ row.id }}</QBtn> <TravelDescriptorProxy :id="row.id" /> </QTd> diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 04ef13d45..3842ff947 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -1,6 +1,6 @@ <script setup> import axios from 'axios'; -import { onMounted, onUnmounted, computed, ref, watch, nextTick, useAttrs } from 'vue'; +import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue'; import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; @@ -22,7 +22,6 @@ const { validate } = useValidator(); const { notify } = useNotify(); const route = useRoute(); const myForm = ref(null); -const attrs = useAttrs(); const $props = defineProps({ url: { type: String, @@ -85,7 +84,7 @@ const $props = defineProps({ }, reload: { type: Boolean, - default: true, + default: false, }, defaultTrim: { type: Boolean, @@ -106,15 +105,15 @@ const isLoading = ref(false); // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas const isResetting = ref(false); const hasChanges = ref(!$props.observeFormChanges); -const originalData = computed(() => state.get(modelValue)); -const formData = ref(); +const originalData = ref({}); +const formData = computed(() => state.get(modelValue)); const defaultButtons = computed(() => ({ save: { dataCy: 'saveDefaultBtn', color: 'primary', icon: 'save', label: 'globals.save', - click: async () => await save(), + click: () => myForm.value.submit(), type: 'submit', }, reset: { @@ -128,6 +127,8 @@ const defaultButtons = computed(() => ({ })); onMounted(async () => { + originalData.value = JSON.parse(JSON.stringify($props.formInitialData ?? {})); + nextTick(() => (componentIsRendered.value = true)); // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla @@ -159,18 +160,10 @@ if (!$props.url) (val) => updateAndEmit('onFetch', { val }), ); -watch( - originalData, - (val) => { - if (val) formData.value = JSON.parse(JSON.stringify(val)); - }, - { immediate: true }, -); - watch( () => [$props.url, $props.filter], async () => { - state.set(modelValue, null); + originalData.value = null; reset(); await fetch(); }, @@ -205,6 +198,7 @@ async function fetch() { updateAndEmit('onFetch', { val: data }); } catch (e) { state.set(modelValue, {}); + originalData.value = {}; throw e; } } @@ -247,7 +241,6 @@ async function saveAndGo() { } function reset() { - formData.value = JSON.parse(JSON.stringify(originalData.value)); updateAndEmit('onFetch', { val: originalData.value }); if ($props.observeFormChanges) { hasChanges.value = false; @@ -272,6 +265,7 @@ function filter(value, update, filterOptions) { function updateAndEmit(evt, { val, res, old } = { val: null, res: null, old: null }) { state.set(modelValue, val); + originalData.value = val && JSON.parse(JSON.stringify(val)); if (!$props.url) arrayData.store.data = val; emit(evt, state.get(modelValue), res, old); @@ -285,22 +279,6 @@ function trimData(data) { return data; } -async function onKeyup(evt) { - if (evt.key === 'Enter' && !('prevent-submit' in attrs)) { - const input = evt.target; - if (input.type == 'textarea' && evt.shiftKey) { - let { selectionStart, selectionEnd } = input; - input.value = - input.value.substring(0, selectionStart) + - '\n' + - input.value.substring(selectionEnd); - selectionStart = selectionEnd = selectionStart + 1; - return; - } - await save(); - } -} - defineExpose({ save, isLoading, @@ -315,12 +293,12 @@ defineExpose({ <QForm ref="myForm" v-if="formData" - @submit.prevent - @keyup.prevent="onKeyup" + @submit="save" @reset="reset" class="q-pa-md" :style="maxWidth ? 'max-width: ' + maxWidth : ''" id="formModel" + :prevent-submit="$attrs['prevent-submit']" > <QCard> <slot diff --git a/src/components/FormModelPopup.vue b/src/components/FormModelPopup.vue index 85943e91e..afdc6efca 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -1,13 +1,12 @@ <script setup> -import { ref, computed, useAttrs, nextTick } from 'vue'; +import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; -import { useState } from 'src/composables/useState'; import FormModel from 'components/FormModel.vue'; const emit = defineEmits(['onDataSaved', 'onDataCanceled']); -const props = defineProps({ +defineProps({ title: { type: String, default: '', @@ -16,41 +15,23 @@ const props = defineProps({ type: String, default: '', }, - showSaveAndContinueBtn: { - type: Boolean, - default: false, - }, }); const { t } = useI18n(); -const attrs = useAttrs(); -const state = useState(); + const formModelRef = ref(null); const closeButton = ref(null); -const isSaveAndContinue = ref(props.showSaveAndContinueBtn); -const isLoading = computed(() => formModelRef.value?.isLoading); -const reset = computed(() => formModelRef.value?.reset); -const onDataSaved = async (formData, requestResponse) => { - if (!isSaveAndContinue.value) closeButton.value?.click(); - if (isSaveAndContinue.value) { - await nextTick(); - state.set(attrs.model, attrs.formInitialData); - } - isSaveAndContinue.value = props.showSaveAndContinueBtn; +const onDataSaved = (formData, requestResponse) => { + if (closeButton.value) closeButton.value.click(); emit('onDataSaved', formData, requestResponse); }; -const onClick = async (saveAndContinue) => { - isSaveAndContinue.value = saveAndContinue; - await formModelRef.value.save(); -}; +const isLoading = computed(() => formModelRef.value?.isLoading); defineExpose({ isLoading, onDataSaved, - isSaveAndContinue, - reset, }); </script> @@ -78,16 +59,15 @@ defineExpose({ flat :disabled="isLoading" :loading="isLoading" - data-cy="FormModelPopup_cancel" - v-close-popup - z-max @click="emit('onDataCanceled')" + v-close-popup + data-cy="FormModelPopup_cancel" + z-max /> <QBtn - :flat="showSaveAndContinueBtn" :label="t('globals.save')" :title="t('globals.save')" - @click="onClick(false)" + type="submit" color="primary" class="q-ml-sm" :disabled="isLoading" @@ -95,18 +75,6 @@ defineExpose({ data-cy="FormModelPopup_save" z-max /> - <QBtn - v-if="showSaveAndContinueBtn" - :label="t('globals.isSaveAndContinue')" - :title="t('globals.isSaveAndContinue')" - color="primary" - class="q-ml-sm" - :disabled="isLoading" - :loading="isLoading" - data-cy="FormModelPopup_isSaveAndContinue" - z-max - @click="onClick(true)" - /> </div> </template> </FormModel> diff --git a/src/components/ItemsFilterPanel.vue b/src/components/ItemsFilterPanel.vue index f73753a6b..36123b834 100644 --- a/src/components/ItemsFilterPanel.vue +++ b/src/components/ItemsFilterPanel.vue @@ -281,7 +281,7 @@ const setCategoryList = (data) => { <QItem class="q-mt-lg"> <QBtn icon="add_circle" - v-shortcut="'+'" + shortcut="+" flat class="fill-icon-on-hover q-px-xs" color="primary" @@ -327,6 +327,7 @@ en: active: Is active visible: Is visible floramondo: Is floramondo + salesPersonFk: Buyer categoryFk: Category es: @@ -337,6 +338,7 @@ es: active: Activo visible: Visible floramondo: Floramondo + salesPersonFk: Comprador categoryFk: Categoría Plant: Planta natural Flower: Flor fresca diff --git a/src/components/LeftMenu.vue b/src/components/LeftMenu.vue index 9a9949499..644f831d4 100644 --- a/src/components/LeftMenu.vue +++ b/src/components/LeftMenu.vue @@ -41,6 +41,7 @@ const filteredItems = computed(() => { return locale.includes(normalizedSearch); }); }); + const filteredPinnedModules = computed(() => { if (!search.value) return pinnedModules.value; const normalizedSearch = search.value @@ -71,7 +72,7 @@ watch( items.value = []; getRoutes(); }, - { deep: true }, + { deep: true } ); function findMatches(search, item) { @@ -103,40 +104,33 @@ function addChildren(module, route, parent) { } function getRoutes() { - const handleRoutes = { - main: getMainRoutes, - card: getCardRoutes, - }; - try { - handleRoutes[props.source](); - } catch (error) { - throw new Error(`Method is not defined`); - } -} -function getMainRoutes() { - const modules = Object.assign([], navigation.getModules().value); + if (props.source === 'main') { + const modules = Object.assign([], navigation.getModules().value); - for (const item of modules) { - const moduleDef = routes.find( - (route) => toLowerCamel(route.name) === item.module, + for (const item of modules) { + const moduleDef = routes.find( + (route) => toLowerCamel(route.name) === item.module + ); + if (!moduleDef) continue; + item.children = []; + + addChildren(item.module, moduleDef, item.children); + } + + items.value = modules; + } + + if (props.source === 'card') { + const currentRoute = route.matched[1]; + const currentModule = toLowerCamel(currentRoute.name); + let moduleDef = routes.find( + (route) => toLowerCamel(route.name) === currentModule ); - if (!moduleDef) continue; - item.children = []; - addChildren(item.module, moduleDef, item.children); + if (!moduleDef) return; + if (!moduleDef?.menus) moduleDef = betaGetRoutes(); + addChildren(currentModule, moduleDef, items.value); } - - items.value = modules; -} - -function getCardRoutes() { - const currentRoute = route.matched[1]; - const currentModule = toLowerCamel(currentRoute.name); - let moduleDef = routes.find((route) => toLowerCamel(route.name) === currentModule); - - if (!moduleDef) return; - if (!moduleDef?.menus) moduleDef = betaGetRoutes(); - addChildren(currentModule, moduleDef, items.value); } function betaGetRoutes() { @@ -229,16 +223,9 @@ const searchModule = () => { </template> <template v-for="(item, index) in filteredItems" :key="item.name"> <template - v-if=" - search || - (item.children && !filteredPinnedModules.has(item.name)) - " + v-if="search ||item.children && !filteredPinnedModules.has(item.name)" > - <LeftMenuItem - :item="item" - group="modules" - :class="search && index === 0 ? 'searched' : ''" - > + <LeftMenuItem :item="item" group="modules" :class="search && index === 0 ? 'searched' : ''"> <template #side> <QBtn v-if="item.isPinned === true" @@ -355,7 +342,7 @@ const searchModule = () => { .header { color: var(--vn-label-color); } -.searched { +.searched{ background-color: var(--vn-section-hover-color); } </style> diff --git a/src/components/LeftMenuItem.vue b/src/components/LeftMenuItem.vue index c0cee44fe..a3112b17f 100644 --- a/src/components/LeftMenuItem.vue +++ b/src/components/LeftMenuItem.vue @@ -26,7 +26,6 @@ const itemComputed = computed(() => { :to="{ name: itemComputed.name }" clickable v-ripple - :data-cy="`${itemComputed.name}-menu-item`" > <QItemSection avatar v-if="itemComputed.icon"> <QIcon :name="itemComputed.icon" /> diff --git a/src/components/RefundInvoiceForm.vue b/src/components/RefundInvoiceForm.vue index 6dcb8b390..590acede0 100644 --- a/src/components/RefundInvoiceForm.vue +++ b/src/components/RefundInvoiceForm.vue @@ -9,7 +9,6 @@ import VnSelect from 'components/common/VnSelect.vue'; import FormPopup from './FormPopup.vue'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; -import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const $props = defineProps({ invoiceOutData: { @@ -132,11 +131,15 @@ const refund = async () => { :required="true" /> </VnRow ><VnRow> - <VnCheckbox - v-model="invoiceParams.inheritWarehouse" - :label="t('Inherit warehouse')" - :info="t('Inherit warehouse tooltip')" - /> + <div> + <QCheckbox + :label="t('Inherit warehouse')" + v-model="invoiceParams.inheritWarehouse" + /> + <QIcon name="info" class="cursor-info q-ml-sm" size="sm"> + <QTooltip>{{ t('Inherit warehouse tooltip') }}</QTooltip> + </QIcon> + </div> </VnRow> </template> </FormPopup> diff --git a/src/components/TicketProblems.vue b/src/components/TicketProblems.vue index 783f2556f..934b13a1c 100644 --- a/src/components/TicketProblems.vue +++ b/src/components/TicketProblems.vue @@ -4,21 +4,26 @@ import { toCurrency } from 'src/filters'; defineProps({ row: { type: Object, required: true } }); </script> <template> - <span class="q-gutter-x-xs"> - <router-link - v-if="row.claim?.claimFk" - :to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }" - class="link" - > - <QIcon name="vn:claims" size="xs"> - <QTooltip> - {{ t('ticketSale.claim') }}: - {{ row.claim?.claimFk }} - </QTooltip> - </QIcon> - </router-link> + <span> <QIcon - v-if="row?.risk" + v-if="row.isTaxDataChecked === 0" + name="vn:no036" + color="primary" + size="xs" + > + <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> + </QIcon> + <QIcon v-if="row.hasTicketRequest" name="vn:buyrequest" color="primary" size="xs"> + <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> + </QIcon> + <QIcon v-if="row.itemShortage" name="vn:unavailable" color="primary" size="xs"> + <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> + </QIcon> + <QIcon v-if="row.isFreezed" name="vn:frozen" color="primary" size="xs"> + <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> + </QIcon> + <QIcon + v-if="row.risk" name="vn:risk" :color="row.hasHighRisk ? 'negative' : 'primary'" size="xs" @@ -28,57 +33,10 @@ defineProps({ row: { type: Object, required: true } }); {{ toCurrency(row.risk - row.credit) }} </QTooltip> </QIcon> - <QIcon - v-if="row?.hasComponentLack" - name="vn:components" - color="primary" - size="xs" - > + <QIcon v-if="row.hasComponentLack" name="vn:components" color="primary" size="xs"> <QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip> </QIcon> - <QIcon v-if="row?.hasItemDelay" color="primary" size="xs" name="vn:hasItemDelay"> - <QTooltip> - {{ $t('ticket.summary.hasItemDelay') }} - </QTooltip> - </QIcon> - <QIcon v-if="row?.hasItemLost" color="primary" size="xs" name="vn:hasItemLost"> - <QTooltip> - {{ $t('salesTicketsTable.hasItemLost') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row?.hasItemShortage" - name="vn:unavailable" - color="primary" - size="xs" - > - <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> - </QIcon> - <QIcon v-if="row?.hasRounding" color="primary" name="sync_problem" size="xs"> - <QTooltip> - {{ $t('ticketList.rounding') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row?.hasTicketRequest" - name="vn:buyrequest" - color="primary" - size="xs" - > - <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> - </QIcon> - <QIcon - v-if="row?.isTaxDataChecked !== 0" - name="vn:no036" - color="primary" - size="xs" - > - <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> - </QIcon> - <QIcon v-if="row?.isFreezed" name="vn:frozen" color="primary" size="xs"> - <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> - </QIcon> - <QIcon v-if="row?.isTooLittle" name="vn:isTooLittle" color="primary" size="xs"> + <QIcon v-if="row.isTooLittle" name="vn:isTooLittle" color="primary" size="xs"> <QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip> </QIcon> </span> diff --git a/src/components/TransferInvoiceForm.vue b/src/components/TransferInvoiceForm.vue index c4ef1454a..aa71070d6 100644 --- a/src/components/TransferInvoiceForm.vue +++ b/src/components/TransferInvoiceForm.vue @@ -10,7 +10,6 @@ import VnSelect from 'components/common/VnSelect.vue'; import FormPopup from './FormPopup.vue'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; -import VnCheckbox from './common/VnCheckbox.vue'; const $props = defineProps({ invoiceOutData: { @@ -187,11 +186,15 @@ const makeInvoice = async () => { /> </VnRow> <VnRow> - <VnCheckbox - v-model="checked" - :label="t('Bill destination client')" - :info="t('transferInvoiceInfo')" - /> + <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> </FormPopup> diff --git a/src/components/VnTable/VnColumn.vue b/src/components/VnTable/VnColumn.vue index d0e245388..9e9bfad69 100644 --- a/src/components/VnTable/VnColumn.vue +++ b/src/components/VnTable/VnColumn.vue @@ -1,8 +1,9 @@ <script setup> import { markRaw, computed } from 'vue'; -import { QIcon, QToggle } from 'quasar'; +import { QIcon, QCheckbox } from 'quasar'; import { dashIfEmpty } from 'src/filters'; +/* basic input */ import VnSelect from 'components/common/VnSelect.vue'; import VnSelectCache from 'components/common/VnSelectCache.vue'; import VnInput from 'components/common/VnInput.vue'; @@ -11,11 +12,8 @@ import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputTime from 'components/common/VnInputTime.vue'; import VnComponent from 'components/common/VnComponent.vue'; import VnUserLink from 'components/ui/VnUserLink.vue'; -import VnSelectEnum from '../common/VnSelectEnum.vue'; -import VnCheckbox from '../common/VnCheckbox.vue'; const model = defineModel(undefined, { required: true }); -const emit = defineEmits(['blur']); const $props = defineProps({ column: { type: Object, @@ -41,18 +39,10 @@ const $props = defineProps({ type: Object, default: null, }, - autofocus: { - type: Boolean, - default: false, - }, showLabel: { type: Boolean, default: null, }, - eventHandlers: { - type: Object, - default: null, - }, }); const defaultSelect = { @@ -109,8 +99,7 @@ const defaultComponents = { }, }, checkbox: { - ref: 'checkbox', - component: markRaw(VnCheckbox), + component: markRaw(QCheckbox), attrs: ({ model }) => { const defaultAttrs = { disable: !$props.isEditable, @@ -126,10 +115,6 @@ const defaultComponents = { }, forceAttrs: { label: $props.showLabel && $props.column.label, - autofocus: true, - }, - events: { - blur: () => emit('blur'), }, }, select: { @@ -140,19 +125,12 @@ const defaultComponents = { component: markRaw(VnSelect), ...defaultSelect, }, - selectEnum: { - component: markRaw(VnSelectEnum), - ...defaultSelect, - }, icon: { component: markRaw(QIcon), }, userLink: { component: markRaw(VnUserLink), }, - toggle: { - component: markRaw(QToggle), - }, }; const value = computed(() => { @@ -182,28 +160,7 @@ const col = computed(() => { return newColumn; }); -const components = computed(() => { - const sourceComponents = $props.components ?? defaultComponents; - - return Object.keys(sourceComponents).reduce((acc, key) => { - const component = sourceComponents[key]; - - if (!component || typeof component !== 'object') { - acc[key] = component; - return acc; - } - - acc[key] = { - ...component, - attrs: { - ...(component.attrs || {}), - autofocus: $props.autofocus, - }, - event: { ...component?.event, ...$props?.eventHandlers }, - }; - return acc; - }, {}); -}); +const components = computed(() => $props.components ?? defaultComponents); </script> <template> <div class="row no-wrap"> diff --git a/src/components/VnTable/VnFilter.vue b/src/components/VnTable/VnFilter.vue index 0de3834ea..426f5c716 100644 --- a/src/components/VnTable/VnFilter.vue +++ b/src/components/VnTable/VnFilter.vue @@ -1,12 +1,14 @@ <script setup> import { markRaw, computed } from 'vue'; -import { QCheckbox, QToggle } from 'quasar'; +import { QCheckbox } from 'quasar'; import { useArrayData } from 'composables/useArrayData'; + +/* basic input */ import VnSelect from 'components/common/VnSelect.vue'; import VnInput from 'components/common/VnInput.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputTime from 'components/common/VnInputTime.vue'; -import VnColumn from 'components/VnTable/VnColumn.vue'; +import VnTableColumn from 'components/VnTable/VnColumn.vue'; const $props = defineProps({ column: { @@ -25,10 +27,6 @@ const $props = defineProps({ type: String, default: 'table', }, - customClass: { - type: String, - default: '', - }, }); defineExpose({ addFilter, props: $props }); @@ -36,7 +34,7 @@ defineExpose({ addFilter, props: $props }); const model = defineModel(undefined, { required: true }); const arrayData = useArrayData( $props.dataKey, - $props.searchUrl ? { searchUrl: $props.searchUrl } : null, + $props.searchUrl ? { searchUrl: $props.searchUrl } : null ); const columnFilter = computed(() => $props.column?.columnFilter); @@ -48,18 +46,19 @@ const enterEvent = { const defaultAttrs = { filled: !$props.showTitle, + class: 'q-px-xs q-pb-xs q-pt-none fit', dense: true, }; const forceAttrs = { - label: $props.showTitle ? '' : (columnFilter.value?.label ?? $props.column.label), + label: $props.showTitle ? '' : columnFilter.value?.label ?? $props.column.label, }; const selectComponent = { component: markRaw(VnSelect), event: updateEvent, attrs: { - class: `q-pt-none fit ${$props.customClass}`, + class: 'q-px-sm q-pb-xs q-pt-none fit', dense: true, filled: !$props.showTitle, }, @@ -110,24 +109,14 @@ const components = { component: markRaw(QCheckbox), event: updateEvent, attrs: { - class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit', + dense: true, + class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs fit', 'toggle-indeterminate': true, - size: 'sm', }, forceAttrs, }, select: selectComponent, rawSelect: selectComponent, - toggle: { - component: markRaw(QToggle), - event: updateEvent, - attrs: { - class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit', - 'toggle-indeterminate': true, - size: 'sm', - }, - forceAttrs, - }, }; async function addFilter(value, name) { @@ -143,8 +132,19 @@ async function addFilter(value, name) { await arrayData.addFilter({ params: { [field]: value } }); } +function alignRow() { + switch ($props.column.align) { + case 'left': + return 'justify-start items-start'; + case 'right': + return 'justify-end items-end'; + default: + return 'flex-center'; + } +} + const showFilter = computed( - () => $props.column?.columnFilter !== false && $props.column.name != 'tableActions', + () => $props.column?.columnFilter !== false && $props.column.name != 'tableActions' ); const onTabPressed = async () => { @@ -152,8 +152,13 @@ const onTabPressed = async () => { }; </script> <template> - <div v-if="showFilter" class="full-width" style="overflow: hidden"> - <VnColumn + <div + v-if="showFilter" + class="full-width" + :class="alignRow()" + style="max-height: 45px; overflow: hidden" + > + <VnTableColumn :column="$props.column" default="input" v-model="model" @@ -163,8 +168,3 @@ const onTabPressed = async () => { /> </div> </template> -<style lang="scss" scoped> -label.vn-label-padding > .q-field__inner > .q-field__control { - padding: inherit !important; -} -</style> diff --git a/src/components/VnTable/VnOrder.vue b/src/components/VnTable/VnOrder.vue index 47ed9acf4..8ffdfe2bc 100644 --- a/src/components/VnTable/VnOrder.vue +++ b/src/components/VnTable/VnOrder.vue @@ -23,10 +23,6 @@ const $props = defineProps({ type: Boolean, default: false, }, - align: { - type: String, - default: 'end', - }, }); const hover = ref(); const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl }); @@ -45,78 +41,55 @@ async function orderBy(name, direction) { break; } if (!direction) return await arrayData.deleteOrder(name); - await arrayData.addOrder(name, direction); } defineExpose({ orderBy }); - -function textAlignToFlex(textAlign) { - return `justify-content: ${ - { - 'text-center': 'center', - 'text-left': 'start', - 'text-right': 'end', - }[textAlign] || 'start' - };`; -} </script> <template> <div @mouseenter="hover = true" @mouseleave="hover = false" @click="orderBy(name, model?.direction)" - class="items-center no-wrap cursor-pointer title" - :style="textAlignToFlex(align)" + class="row items-center no-wrap cursor-pointer" > <span :title="label">{{ label }}</span> - <div v-if="name && model?.index"> - <QChip - :label="!vertical ? model?.index : ''" - :icon=" - (model?.index || hover) && !vertical - ? model?.direction == 'DESC' - ? 'arrow_downward' - : 'arrow_upward' - : undefined - " - :size="vertical ? '' : 'sm'" - :class="[ - model?.index ? 'color-vn-text' : 'bg-transparent', - vertical ? 'q-px-none' : '', - ]" - class="no-box-shadow" - :clickable="true" - style="min-width: 40px; max-height: 30px" + <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'" > - <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> + {{ model?.index }} + <QIcon + :name=" + model?.index + ? model?.direction == 'DESC' + ? 'arrow_downward' + : 'arrow_upward' + : 'swap_vert' + " + size="xs" + /> + </div> + </QChip> </div> </template> -<style lang="scss" scoped> -.title { - display: flex; - align-items: center; - height: 30px; - width: 100%; - color: var(--vn-label-color); - white-space: nowrap; -} -</style> diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 7ff56860f..6e5f9fef4 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -1,38 +1,22 @@ <script setup> -import { - ref, - onBeforeMount, - onMounted, - onUnmounted, - computed, - watch, - h, - render, - inject, - useAttrs, - nextTick, -} from 'vue'; -import { useArrayData } from 'src/composables/useArrayData'; +import { ref, onBeforeMount, onMounted, computed, watch, useAttrs } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; -import { useQuasar, date } from 'quasar'; +import { useQuasar } from 'quasar'; import { useStateStore } from 'stores/useStateStore'; import { useFilterParams } from 'src/composables/useFilterParams'; -import { dashIfEmpty, toDate } from 'src/filters'; import CrudModel from 'src/components/CrudModel.vue'; import FormModelPopup from 'components/FormModelPopup.vue'; -import VnColumn from 'components/VnTable/VnColumn.vue'; +import VnTableColumn from 'components/VnTable/VnColumn.vue'; import VnFilter from 'components/VnTable/VnFilter.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'; import VnTableFilter from './VnTableFilter.vue'; -import { getColAlign } from 'src/composables/getColAlign'; -const arrayData = useArrayData(useAttrs()['data-key']); const $props = defineProps({ columns: { type: Array, @@ -58,6 +42,10 @@ const $props = defineProps({ type: [Function, Boolean], default: null, }, + rowCtrlClick: { + type: [Function, Boolean], + default: null, + }, redirect: { type: String, default: null, @@ -126,19 +114,7 @@ const $props = defineProps({ type: Boolean, default: false, }, - withFilters: { - type: Boolean, - default: true, - }, - overlay: { - type: Boolean, - default: false, - }, - createComplement: { - type: Object, - }, }); - const { t } = useI18n(); const stateStore = useStateStore(); const route = useRoute(); @@ -156,18 +132,10 @@ const showForm = ref(false); const splittedColumns = ref({ columns: [] }); const columnsVisibilitySkipped = ref(); const createForm = ref(); -const createRef = ref(null); const tableRef = ref(); const params = ref(useFilterParams($attrs['data-key']).params); const orders = ref(useFilterParams($attrs['data-key']).orders); -const app = inject('app'); -const editingRow = ref(null); -const editingField = ref(null); -const isTableMode = computed(() => mode.value == TABLE_MODE); -const showRightIcon = computed(() => $props.rightSearch || $props.rightSearchIcon); -const selectRegex = /select/; -const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); const tableModes = [ { icon: 'view_column', @@ -188,8 +156,7 @@ onBeforeMount(() => { hasParams.value = urlParams && Object.keys(urlParams).length !== 0; }); -onMounted(async () => { - if ($props.isEditable) document.addEventListener('click', clickHandler); +onMounted(() => { mode.value = quasar.platform.is.mobile && !$props.disableOption?.card ? CARD_MODE @@ -211,25 +178,14 @@ onMounted(async () => { } }); -onUnmounted(async () => { - if ($props.isEditable) document.removeEventListener('click', clickHandler); -}); - watch( () => $props.columns, (value) => splitColumns(value), { immediate: true }, ); -defineExpose({ - create: createForm, - reload, - redirect: redirectFn, - selected, - CrudModelRef, - params, - tableRef, -}); +const isTableMode = computed(() => mode.value == TABLE_MODE); +const showRightIcon = computed(() => $props.rightSearch || $props.rightSearchIcon); function splitColumns(columns) { splittedColumns.value = { @@ -275,6 +231,16 @@ const rowClickFunction = computed(() => { 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) { router.push({ path: `/${$props.redirect}/${id}` }); } @@ -296,6 +262,21 @@ function columnName(col) { return name; } +function getColAlign(col) { + return 'text-' + (col.align ?? 'left'); +} + +const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); +defineExpose({ + create: createForm, + reload, + redirect: redirectFn, + selected, + CrudModelRef, + params, + tableRef, +}); + function handleOnDataSaved(_) { if (_.onDataSaved) _.onDataSaved({ CrudModelRef: CrudModelRef.value }); else $props.create.onDataSaved(_); @@ -324,237 +305,6 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) { } } -function isEditableColumn(column) { - const isEditableCol = column?.isEditable ?? true; - const isVisible = column?.visible ?? true; - const hasComponent = column?.component; - - return $props.isEditable && isVisible && hasComponent && isEditableCol; -} - -function hasEditableFormat(column) { - if (isEditableColumn(column)) return 'editable-text'; -} - -const clickHandler = async (event) => { - const clickedElement = event.target.closest('td'); - - const isDateElement = event.target.closest('.q-date'); - const isTimeElement = event.target.closest('.q-time'); - const isQselectDropDown = event.target.closest('.q-select__dropdown-icon'); - - if (isDateElement || isTimeElement || isQselectDropDown) return; - - if (clickedElement === null) { - await destroyInput(editingRow.value, editingField.value); - return; - } - const rowIndex = clickedElement.getAttribute('data-row-index'); - const colField = clickedElement.getAttribute('data-col-field'); - const column = $props.columns.find((col) => col.name === colField); - - if (editingRow.value !== null && editingField.value !== null) { - if (editingRow.value == rowIndex && editingField.value == colField) return; - - await destroyInput(editingRow.value, editingField.value); - } - - if (isEditableColumn(column)) { - await renderInput(Number(rowIndex), colField, clickedElement); - } -}; - -async function handleTabKey(event, rowIndex, colField) { - if (editingRow.value == rowIndex && editingField.value == colField) - await destroyInput(editingRow.value, editingField.value); - - const direction = event.shiftKey ? -1 : 1; - const { nextRowIndex, nextColumnName } = await handleTabNavigation( - rowIndex, - colField, - direction, - ); - - if (nextRowIndex < 0 || nextRowIndex >= arrayData.store.data.length) return; - - event.preventDefault(); - await renderInput(nextRowIndex, nextColumnName, null); -} - -async function renderInput(rowId, field, clickedElement) { - editingField.value = field; - editingRow.value = rowId; - - const originalColumn = $props.columns.find((col) => col.name === field); - const column = { ...originalColumn, ...{ label: '' } }; - const row = CrudModelRef.value.formData[rowId]; - const oldValue = CrudModelRef.value.formData[rowId][column?.name]; - - if (!clickedElement) - clickedElement = document.querySelector( - `[data-row-index="${rowId}"][data-col-field="${field}"]`, - ); - - Array.from(clickedElement.childNodes).forEach((child) => { - child.style.visibility = 'hidden'; - child.style.position = 'relative'; - }); - - const isSelect = selectRegex.test(column?.component); - if (isSelect) column.attrs = { ...column.attrs, 'emit-value': false }; - - const node = h(VnColumn, { - row: row, - class: 'temp-input', - column: column, - modelValue: row[column.name], - componentProp: 'columnField', - autofocus: true, - focusOnMount: true, - eventHandlers: { - 'update:modelValue': async (value) => { - if (isSelect && value) { - row[column.name] = value[column.attrs?.optionValue ?? 'id']; - row[column?.name + 'TextValue'] = - value[column.attrs?.optionLabel ?? 'name']; - await column?.cellEvent?.['update:modelValue']?.( - value, - oldValue, - row, - ); - } else row[column.name] = value; - await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row); - }, - keyup: async (event) => { - if (event.key === 'Enter') - await destroyInput(rowIndex, field, clickedElement); - }, - keydown: async (event) => { - switch (event.key) { - case 'Tab': - await handleTabKey(event, rowId, field); - event.stopPropagation(); - break; - case 'Escape': - await destroyInput(rowId, field, clickedElement); - break; - default: - break; - } - }, - click: (event) => { - column?.cellEvent?.['click']?.(event, row); - }, - }, - }); - - node.appContext = app._context; - render(node, clickedElement); - - if (['toggle'].includes(column?.component)) - node.el?.querySelector('span > div').focus(); - - if (['checkbox', undefined].includes(column?.component)) - node.el?.querySelector('span > div > div').focus(); -} - -async function destroyInput(rowIndex, field, clickedElement) { - if (!clickedElement) - clickedElement = document.querySelector( - `[data-row-index="${rowIndex}"][data-col-field="${field}"]`, - ); - if (clickedElement) { - await nextTick(); - render(null, clickedElement); - Array.from(clickedElement.childNodes).forEach((child) => { - child.style.visibility = 'visible'; - child.style.position = ''; - }); - } - if (editingRow.value !== rowIndex || editingField.value !== field) return; - editingRow.value = null; - editingField.value = null; -} - -async function handleTabNavigation(rowIndex, colName, direction) { - const columns = $props.columns; - const totalColumns = columns.length; - let currentColumnIndex = columns.findIndex((col) => col.name === colName); - - let iterations = 0; - let newColumnIndex = currentColumnIndex; - - do { - iterations++; - newColumnIndex = (newColumnIndex + direction + totalColumns) % totalColumns; - - if (isEditableColumn(columns[newColumnIndex])) break; - } while (iterations < totalColumns); - - if (iterations >= totalColumns + 1) return; - - if (direction === 1 && newColumnIndex <= currentColumnIndex) { - rowIndex++; - } else if (direction === -1 && newColumnIndex >= currentColumnIndex) { - rowIndex--; - } - return { nextRowIndex: rowIndex, nextColumnName: columns[newColumnIndex].name }; -} - -function getCheckboxIcon(value) { - switch (typeof value) { - case 'boolean': - return value ? 'check' : 'close'; - case 'number': - return value === 0 ? 'close' : 'check'; - case 'undefined': - return 'indeterminate_check_box'; - default: - return 'indeterminate_check_box'; - } -} - -function getToggleIcon(value) { - if (value === null) return 'help_outline'; - return value ? 'toggle_on' : 'toggle_off'; -} - -function formatColumnValue(col, row, dashIfEmpty) { - if (col?.format || row[col?.name + 'TextValue']) { - if (selectRegex.test(col?.component) && row[col?.name + 'TextValue']) { - return dashIfEmpty(row[col?.name + 'TextValue']); - } else { - return col.format(row, dashIfEmpty); - } - } - - if (col?.component === 'date') return dashIfEmpty(toDate(row[col?.name])); - - if (col?.component === 'time') - return row[col?.name] >= 5 - ? dashIfEmpty(date.formatDate(new Date(row[col?.name]), 'HH:mm')) - : row[col?.name]; - - if (selectRegex.test(col?.component) && $props.isEditable) { - const { find, url } = col.attrs; - const urlRelation = url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1); - - if (col?.attrs.options) { - const find = col?.attrs.options.find((option) => option.id === row[col.name]); - if (!col.attrs?.optionLabel || !find) return dashIfEmpty(row[col?.name]); - return dashIfEmpty(find[col.attrs?.optionLabel ?? 'name']); - } - - if (typeof row[urlRelation] == 'object') { - if (typeof find == 'object') - return dashIfEmpty(row[urlRelation][find?.label ?? 'name']); - - return dashIfEmpty(row[urlRelation][col?.attrs.optionLabel ?? 'name']); - } - if (typeof row[urlRelation] == 'string') return dashIfEmpty(row[urlRelation]); - } - return dashIfEmpty(row[col?.name]); -} function cardClick(_, row) { if ($props.redirect) router.push({ path: `/${$props.redirect}/${row.id}` }); } @@ -565,7 +315,7 @@ function cardClick(_, row) { v-model="stateStore.rightDrawer" side="right" :width="256" - :overlay="$props.overlay" + show-if-above > <QScrollArea class="fit"> <VnTableFilter @@ -586,7 +336,7 @@ function cardClick(_, row) { <CrudModel v-bind="$attrs" :class="$attrs['class'] ?? 'q-px-md'" - :limit="$attrs['limit'] ?? 100" + :limit="$attrs['limit'] ?? 20" ref="CrudModelRef" @on-fetch="(...args) => emit('onFetch', ...args)" :search-url="searchUrl" @@ -602,12 +352,8 @@ function cardClick(_, row) { <QTable ref="tableRef" v-bind="table" - :class="[ - 'vnTable', - table ? 'selection-cell' : '', - $props.footer ? 'last-row-sticky' : '', - ]" - wrap-cells + class="vnTable" + :class="{ 'last-row-sticky': $props.footer }" :columns="splittedColumns.columns" :rows="rows" v-model:selected="selected" @@ -621,13 +367,11 @@ function cardClick(_, row) { @row-click="(_, row) => rowClickFunction && rowClickFunction(row)" @update:selected="emit('update:selected', $event)" @selection="(details) => handleSelection(details, rows)" - :hide-selected-banner="true" > <template #top-left v-if="!$props.withoutHeader"> - <slot name="top-left"> </slot> + <slot name="top-left"></slot> </template> <template #top-right v-if="!$props.withoutHeader"> - <slot name="top-right"></slot> <VnVisibleColumn v-if="isTableMode" v-model="splittedColumns.columns" @@ -641,7 +385,6 @@ function cardClick(_, row) { dense :options="tableModes.filter((mode) => !mode.disable)" /> - <QBtn v-if="showRightIcon" icon="filter_alt" @@ -653,39 +396,32 @@ function cardClick(_, row) { <template #header-cell="{ col }"> <QTh v-if="col.visible ?? true" - v-bind:class="col.headerClass" - class="body-cell" - :style="col?.width ? `max-width: ${col?.width}` : ''" + :style="col.headerStyle" + :class="col.headerClass" > <div - class="no-padding" - :style="[ - withFilters && $props.columnSearch ? 'height: 75px' : '', - ]" + class="column ellipsis" + :class="`text-${col?.align ?? 'left'}`" + :style="$props.columnSearch ? 'height: 75px' : ''" > - <div style="height: 30px"> + <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?.labelAbbreviation ?? col?.label" + :label="col?.label" :data-key="$attrs['data-key']" :search-url="searchUrl" - :align="getColAlign(col)" /> </div> <VnFilter - v-if=" - $props.columnSearch && - col.columnSearch !== false && - withFilters - " + v-if="$props.columnSearch" :column="col" :show-title="true" :data-key="$attrs['data-key']" v-model="params[columnName(col)]" :search-url="searchUrl" - customClass="header-filter" + class="full-width" /> </div> </QTh> @@ -703,67 +439,32 @@ function cardClick(_, row) { </QTd> </template> <template #body-cell="{ col, row, rowIndex }"> + <!-- Columns --> <QTd - class="no-margin q-px-xs" + auto-width + class="no-margin" + :class="[getColAlign(col), col.columnClass]" + :style="col.style" v-if="col.visible ?? true" - :style="{ - 'max-width': col?.width ?? false, - position: 'relative', - }" - :class="[ - col.columnClass, - 'body-cell no-margin no-padding', - getColAlign(col), - ]" - :data-row-index="rowIndex" - :data-col-field="col?.name" + @click.ctrl=" + ($event) => + rowCtrlClickFunction && rowCtrlClickFunction($event, row) + " > - <div - class="no-padding no-margin peter" - style=" - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - " + <slot + :name="`column-${col.name}`" + :col="col" + :row="row" + :row-index="rowIndex" > - <slot - :name="`column-${col.name}`" - :col="col" + <VnTableColumn + :column="col" :row="row" - :row-index="rowIndex" - > - <QIcon - v-if="col?.component === 'toggle'" - :name=" - col?.getIcon - ? col.getIcon(row[col?.name]) - : getToggleIcon(row[col?.name]) - " - style="color: var(--vn-text-color)" - :class="hasEditableFormat(col)" - size="14px" - /> - <QIcon - v-else-if="col?.component === 'checkbox'" - :name="getCheckboxIcon(row[col?.name])" - style="color: var(--vn-text-color)" - :class="hasEditableFormat(col)" - size="14px" - /> - <span - v-else - :class="hasEditableFormat(col)" - :style=" - typeof col?.style == 'function' - ? col.style(row) - : col?.style - " - style="bottom: 0" - > - {{ formatColumnValue(col, row, dashIfEmpty) }} - </span> - </slot> - </div> + :is-editable="col.isEditable ?? isEditable" + v-model="row[col.name]" + component-prop="columnField" + /> + </slot> </QTd> </template> <template #body-cell-tableActions="{ col, row }"> @@ -784,7 +485,7 @@ function cardClick(_, row) { flat dense :class=" - btn.isPrimary ? 'text-primary-light' : 'color-vn-label' + btn.isPrimary ? 'text-primary-light' : 'color-vn-text ' " :style="`visibility: ${ ((btn.show && btn.show(row)) ?? true) @@ -792,7 +493,6 @@ function cardClick(_, row) { : 'hidden' }`" @click="btn.action(row)" - :data-cy="btn?.name ?? `tableAction-${index}`" /> </QTd> </template> @@ -841,7 +541,7 @@ function cardClick(_, row) { </QCardSection> <!-- Fields --> <QCardSection - class="q-pl-sm q-py-xs" + class="q-pl-sm q-pr-lg q-py-xs" :class="$props.cardClass" > <div @@ -862,7 +562,7 @@ function cardClick(_, row) { :row="row" :row-index="index" > - <VnColumn + <VnTableColumn :column="col" :row="row" :is-editable="false" @@ -889,12 +589,12 @@ function cardClick(_, row) { :title="btn.title" :icon="btn.icon" class="q-pa-xs" + flat :class=" btn.isPrimary ? 'text-primary-light' - : 'color-vn-label' + : 'color-vn-text ' " - flat @click="btn.action(row)" /> </QCardSection> @@ -902,17 +602,14 @@ function cardClick(_, row) { </component> </template> <template #bottom-row="{ cols }" v-if="$props.footer"> - <QTr v-if="rows.length" style="height: 45px"> - <QTh v-if="table.selection" /> + <QTr v-if="rows.length" style="height: 30px"> <QTh v-for="col of cols.filter((cols) => cols.visible ?? true)" :key="col?.id" + class="text-center" :class="getColAlign(col)" > - <slot - :name="`column-footer-${col.name}`" - :isEditableColumn="isEditableColumn(col)" - /> + <slot :name="`column-footer-${col.name}`" /> </QTh> </QTr> </template> @@ -931,7 +628,7 @@ function cardClick(_, row) { size="md" round flat - v-shortcut="'+'" + shortcut="+" :disabled="!disabledAttr" /> <QTooltip> @@ -949,52 +646,39 @@ function cardClick(_, row) { color="primary" fab icon="add" - v-shortcut="'+'" + shortcut="+" data-cy="vnTableCreateBtn" /> <QTooltip self="top right"> {{ createForm?.title }} </QTooltip> </QPageSticky> - <QDialog - v-model="showForm" - transition-show="scale" - transition-hide="scale" - :full-width="createComplement?.isFullWidth ?? false" - data-cy="vn-table-create-dialog" - > + <QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> <FormModelPopup - ref="createRef" v-bind="createForm" :model="$attrs['data-key'] + 'Create'" @on-data-saved="(_, res) => createForm.onDataSaved(res)" > <template #form-inputs="{ data }"> - <div :style="createComplement?.containerStyle"> - <div> - <slot name="previous-create-dialog" :data="data" /> - </div> - <div class="grid-create" :style="createComplement?.columnGridStyle"> - <slot - v-for="column of splittedColumns.create" - :key="column.name" - :name="`column-create-${column.name}`" - :data="data" - :column-name="column.name" - :label="column.label" - > - <VnColumn - :column="column" - :row="{}" - default="input" - v-model="data[column.name]" - :show-label="true" - component-prop="columnCreate" - :data-cy="`${column.name}-create-popup`" - /> - </slot> - <slot name="more-create-dialog" :data="data" /> - </div> + <div class="grid-create"> + <slot + v-for="column of splittedColumns.create" + :key="column.name" + :name="`column-create-${column.name}`" + :data="data" + :column-name="column.name" + :label="column.label" + > + <VnTableColumn + :column="column" + :row="{}" + default="input" + v-model="data[column.name]" + :show-label="true" + component-prop="columnCreate" + /> + </slot> + <slot name="more-create-dialog" :data="data" /> </div> </template> </FormModelPopup> @@ -1012,42 +696,6 @@ es: </i18n> <style lang="scss"> -.selection-cell { - table td:first-child { - padding: 0px; - } -} -.side-padding { - padding-left: 1px; - padding-right: 1px; -} -.editable-text:hover { - border-bottom: 1px dashed var(--q-primary); - @extend .side-padding; -} -.editable-text { - border-bottom: 1px dashed var(--vn-label-color); - @extend .side-padding; -} -.cell-input { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - padding-top: 0px !important; -} -.q-field--labeled .q-field__native, -.q-field--labeled .q-field__prefix, -.q-field--labeled .q-field__suffix { - padding-top: 20px; -} - -.body-cell { - padding-left: 4px !important; - padding-right: 4px !important; - position: relative; -} .bg-chip-secondary { background-color: var(--vn-page-color); color: var(--vn-text-color); @@ -1064,8 +712,8 @@ es: .grid-three { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, max-content)); - width: 100%; + grid-template-columns: repeat(auto-fit, minmax(350px, max-content)); + max-width: 100%; grid-gap: 20px; margin: 0 auto; } @@ -1073,6 +721,7 @@ es: .grid-create { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, max-content)); + max-width: 100%; grid-gap: 20px; margin: 0 auto; } @@ -1088,9 +737,7 @@ es: } } } -.q-table tbody tr td { - position: relative; -} + .q-table { th { padding: 0; @@ -1139,7 +786,6 @@ es: .vn-label-value { display: flex; flex-direction: row; - align-items: center; color: var(--vn-text-color); .value { overflow: hidden; @@ -1191,15 +837,4 @@ es: .q-table__middle.q-virtual-scroll.q-virtual-scroll--vertical.scroll { background-color: var(--vn-section-color); } -.temp-input { - top: 0; - position: absolute; - width: 100%; - height: 100%; - display: flex; -} - -label.header-filter > .q-field__inner > .q-field__control { - padding: inherit; -} </style> diff --git a/src/components/VnTable/VnTableFilter.vue b/src/components/VnTable/VnTableFilter.vue index 79b903e54..732605ce5 100644 --- a/src/components/VnTable/VnTableFilter.vue +++ b/src/components/VnTable/VnTableFilter.vue @@ -27,36 +27,31 @@ function columnName(col) { </script> <template> <VnFilterPanel v-bind="$attrs" :search-button="true" :disable-submit-event="true"> - <template #body="{ params, orders, searchFn }"> + <template #body="{ params, orders }"> <div - class="container" + class="row no-wrap flex-center" v-for="col of columns.filter((c) => c.columnFilter ?? true)" :key="col.id" > - <div class="filter"> - <VnFilter - ref="tableFilterRef" - :column="col" - :data-key="$attrs['data-key']" - v-model="params[columnName(col)]" - :search-url="searchUrl" - /> - </div> - <div class="order"> - <VnTableOrder - v-if="col?.columnFilter !== false && col?.name !== 'tableActions'" - v-model="orders[col.orderBy ?? col.name]" - :name="col.orderBy ?? col.name" - :data-key="$attrs['data-key']" - :search-url="searchUrl" - :vertical="true" - /> - </div> + <VnFilter + ref="tableFilterRef" + :column="col" + :data-key="$attrs['data-key']" + v-model="params[columnName(col)]" + :search-url="searchUrl" + /> + <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 name="moreFilterPanel" :params="params" - :search-fn="searchFn" :orders="orders" :columns="columns" /> @@ -72,21 +67,3 @@ function columnName(col) { </template> </VnFilterPanel> </template> -<style lang="scss" scoped> -.container { - display: flex; - justify-content: center; - align-items: center; - height: 45px; - gap: 10px; -} - -.filter { - width: 70%; - height: 40px; - text-align: center; -} -.order { - width: 10%; -} -</style> diff --git a/src/components/VnTable/VnVisibleColumn.vue b/src/components/VnTable/VnVisibleColumn.vue index 6d15c585e..dad950d73 100644 --- a/src/components/VnTable/VnVisibleColumn.vue +++ b/src/components/VnTable/VnVisibleColumn.vue @@ -32,21 +32,16 @@ const areAllChecksMarked = computed(() => { function setUserConfigViewData(data, isLocal) { if (!data) return; + // Importante: El name de las columnas de la tabla debe conincidir con el name de las variables que devuelve la view config if (!isLocal) localColumns.value = []; - + // Array to Object const skippeds = $props.skip.reduce((a, v) => ({ ...a, [v]: v }), {}); for (let column of columns.value) { - const { label, name, labelAbbreviation } = column; + const { label, name } = column; if (skippeds[name]) continue; column.visible = data[name] ?? true; - if (!isLocal) - localColumns.value.push({ - name, - label, - labelAbbreviation, - visible: column.visible, - }); + if (!isLocal) localColumns.value.push({ name, label, visible: column.visible }); } } @@ -157,11 +152,7 @@ onMounted(async () => { <QCheckbox v-for="col in localColumns" :key="col.name" - :label=" - col?.labelAbbreviation - ? col.labelAbbreviation + ` (${col.label ?? col.name})` - : (col.label ?? col.name) - " + :label="col.label ?? col.name" v-model="col.visible" /> </div> diff --git a/src/components/__tests__/FormModel.spec.js b/src/components/__tests__/FormModel.spec.js index 3dce04374..e35684bc3 100644 --- a/src/components/__tests__/FormModel.spec.js +++ b/src/components/__tests__/FormModel.spec.js @@ -57,7 +57,6 @@ describe('FormModel', () => { vm.state.set(model, formInitialData); expect(vm.hasChanges).toBe(false); - await vm.$nextTick(); vm.formData.mockKey = 'newVal'; await vm.$nextTick(); expect(vm.hasChanges).toBe(true); @@ -94,13 +93,9 @@ describe('FormModel', () => { it('should call axios.patch with the right data', async () => { const spy = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} }); - const { vm } = mount({ propsData: { url, model } }); - - vm.formData = {}; + const { vm } = mount({ propsData: { url, model, formInitialData } }); + vm.formData.mockKey = 'newVal'; await vm.$nextTick(); - vm.formData = { mockKey: 'newVal' }; - await vm.$nextTick(); - await vm.save(); expect(spy).toHaveBeenCalled(); vm.formData.mockKey = 'mockVal'; @@ -111,7 +106,6 @@ describe('FormModel', () => { const { vm } = mount({ propsData: { url, model, formInitialData, urlCreate: 'mockUrlCreate' }, }); - await vm.$nextTick(); vm.formData.mockKey = 'newVal'; await vm.$nextTick(); await vm.save(); @@ -125,7 +119,7 @@ describe('FormModel', () => { }); const spyPatch = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} }); const spySaveFn = vi.spyOn(vm.$props, 'saveFn'); - await vm.$nextTick(); + vm.formData.mockKey = 'newVal'; await vm.$nextTick(); await vm.save(); diff --git a/src/components/__tests__/Leftmenu.spec.js b/src/components/__tests__/Leftmenu.spec.js index 4ab8b527f..10d9d66fb 100644 --- a/src/components/__tests__/Leftmenu.spec.js +++ b/src/components/__tests__/Leftmenu.spec.js @@ -1,11 +1,8 @@ -import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest'; +import { vi, describe, expect, it, beforeAll } from 'vitest'; import { createWrapper, axios } from 'app/test/vitest/helper'; import Leftmenu from 'components/LeftMenu.vue'; -import * as vueRouter from 'vue-router'; -import { useNavigationStore } from 'src/stores/useNavigationStore'; -let vm; -let navigation; +import { useNavigationStore } from 'src/stores/useNavigationStore'; vi.mock('src/router/modules', () => ({ default: [ @@ -24,16 +21,6 @@ vi.mock('src/router/modules', () => ({ { path: '', name: 'CustomerMain', - meta: { - menu: 'Customer', - menuChildren: [ - { - name: 'CustomerCreditContracts', - title: 'creditContracts', - icon: 'vn:solunion', - }, - ], - }, children: [ { path: 'list', @@ -41,13 +28,6 @@ vi.mock('src/router/modules', () => ({ meta: { title: 'list', icon: 'view_list', - menuChildren: [ - { - name: 'CustomerCreditContracts', - title: 'creditContracts', - icon: 'vn:solunion', - }, - ], }, }, { @@ -64,325 +44,51 @@ vi.mock('src/router/modules', () => ({ }, ], })); -vi.spyOn(vueRouter, 'useRoute').mockReturnValue({ - matched: [ - { - path: '/', - redirect: { - name: 'Dashboard', + +describe('Leftmenu', () => { + let vm; + let navigation; + beforeAll(() => { + vi.spyOn(axios, 'get').mockResolvedValue({ + data: [], + }); + + vm = createWrapper(Leftmenu, { + propsData: { + source: 'main', }, - name: 'Main', - meta: {}, - props: { - default: false, - }, - children: [ + }).vm; + + navigation = useNavigationStore(); + navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true)); + navigation.getModules = vi.fn().mockReturnValue({ + value: [ { - path: '/dashboard', - name: 'Dashboard', - meta: { - title: 'dashboard', - icon: 'dashboard', - }, + name: 'customer', + title: 'customer.pageTitles.customers', + icon: 'vn:customer', + module: 'customer', }, ], - }, - { - path: '/customer', - redirect: { - name: 'CustomerMain', - }, - name: 'Customer', - meta: { - title: 'customers', - icon: 'vn:client', - moduleName: 'Customer', - keyBinding: 'c', - menu: 'customer', - }, - }, - ], - query: {}, - params: {}, - meta: { moduleName: 'mockName' }, - path: 'mockName/1', - name: 'Customer', -}); -function mount(source = 'main') { - vi.spyOn(axios, 'get').mockResolvedValue({ - data: [], - }); - const wrapper = createWrapper(Leftmenu, { - propsData: { - source, - }, - }); - - navigation = useNavigationStore(); - navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true)); - navigation.getModules = vi.fn().mockReturnValue({ - value: [ - { - name: 'customer', - title: 'customer.pageTitles.customers', - icon: 'vn:customer', - module: 'customer', - }, - ], - }); - return wrapper; -} - -describe('getRoutes', () => { - afterEach(() => vi.clearAllMocks()); - const getRoutes = vi.fn().mockImplementation((props, getMethodA, getMethodB) => { - const handleRoutes = { - methodA: getMethodA, - methodB: getMethodB, - }; - try { - handleRoutes[props.source](); - } catch (error) { - throw Error('Method not defined'); - } - }); - - const getMethodA = vi.fn(); - const getMethodB = vi.fn(); - const fn = (props) => getRoutes(props, getMethodA, getMethodB); - - it('should call getMethodB when source is card', () => { - let props = { source: 'methodB' }; - fn(props); - - expect(getMethodB).toHaveBeenCalled(); - expect(getMethodA).not.toHaveBeenCalled(); - }); - it('should call getMethodA when source is main', () => { - let props = { source: 'methodA' }; - fn(props); - - expect(getMethodA).toHaveBeenCalled(); - expect(getMethodB).not.toHaveBeenCalled(); - }); - - it('should call getMethodA when source is not exists or undefined', () => { - let props = { source: 'methodC' }; - expect(() => fn(props)).toThrowError('Method not defined'); - - expect(getMethodA).not.toHaveBeenCalled(); - expect(getMethodB).not.toHaveBeenCalled(); - }); -}); - -describe('Leftmenu as card', () => { - beforeAll(() => { - vm = mount('card').vm; - }); - - it('should get routes for card source', async () => { - vm.getRoutes(); - }); -}); -describe('Leftmenu as main', () => { - beforeEach(() => { - vm = mount().vm; - }); - - it('should initialize with default props', () => { - expect(vm.source).toBe('main'); - }); - - it('should filter items based on search input', async () => { - vm.search = 'cust'; - await vm.$nextTick(); - expect(vm.filteredItems[0].name).toEqual('customer'); - expect(vm.filteredItems[0].module).toEqual('customer'); - }); - it('should filter items based on search input', async () => { - vm.search = 'Rou'; - await vm.$nextTick(); - expect(vm.filteredItems).toEqual([]); - }); - - it('should return pinned items', () => { - vm.items = [ - { name: 'Item 1', isPinned: false }, - { name: 'Item 2', isPinned: true }, - ]; - expect(vm.pinnedModules).toEqual( - new Map([['Item 2', { name: 'Item 2', isPinned: true }]]), - ); - }); - - it('should find matches in routes', () => { - const search = 'child1'; - const item = { - children: [ - { name: 'child1', children: [] }, - { name: 'child2', children: [] }, - ], - }; - const matches = vm.findMatches(search, item); - expect(matches).toEqual([{ name: 'child1', children: [] }]); - }); - it('should not proceed if event is already prevented', async () => { - const item = { module: 'testModule', isPinned: false }; - const event = { - preventDefault: vi.fn(), - stopPropagation: vi.fn(), - defaultPrevented: true, - }; - - await vm.togglePinned(item, event); - - expect(event.preventDefault).not.toHaveBeenCalled(); - expect(event.stopPropagation).not.toHaveBeenCalled(); - }); - - it('should call quasar.notify with success message', async () => { - const item = { module: 'testModule', isPinned: false }; - const event = { - preventDefault: vi.fn(), - stopPropagation: vi.fn(), - defaultPrevented: false, - }; - const response = { data: { id: 1 } }; - - vi.spyOn(axios, 'post').mockResolvedValue(response); - vi.spyOn(vm.quasar, 'notify'); - - await vm.togglePinned(item, event); - - expect(vm.quasar.notify).toHaveBeenCalledWith({ - message: 'Data saved', - type: 'positive', }); }); - it('should handle a single matched route with a menu', () => { - const route = { - matched: [{ meta: { menu: 'customer' } }], - }; - - const result = vm.betaGetRoutes(); - - expect(result.meta.menu).toEqual(route.matched[0].meta.menu); - }); - it('should get routes for main source', () => { - vm.props.source = 'main'; - vm.getRoutes(); - expect(navigation.getModules).toHaveBeenCalled(); - }); - - it('should find direct child matches', () => { - const search = 'child1'; - const item = { - children: [{ name: 'child1' }, { name: 'child2' }], - }; - const result = vm.findMatches(search, item); - expect(result).toEqual([{ name: 'child1' }]); - }); - - it('should find nested child matches', () => { - const search = 'child3'; - const item = { - children: [ - { name: 'child1' }, - { - name: 'child2', - children: [{ name: 'child3' }], - }, - ], - }; - const result = vm.findMatches(search, item); - expect(result).toEqual([{ name: 'child3' }]); - }); -}); - -describe('normalize', () => { - beforeAll(() => { - vm = mount('card').vm; - }); - it('should normalize and lowercase text', () => { - const input = 'ÁÉÍÓÚáéíóú'; - const expected = 'aeiouaeiou'; - expect(vm.normalize(input)).toBe(expected); - }); - - it('should handle empty string', () => { - const input = ''; - const expected = ''; - expect(vm.normalize(input)).toBe(expected); - }); - - it('should handle text without diacritics', () => { - const input = 'hello'; - const expected = 'hello'; - expect(vm.normalize(input)).toBe(expected); - }); - - it('should handle mixed text', () => { - const input = 'Héllo Wórld!'; - const expected = 'hello world!'; - expect(vm.normalize(input)).toBe(expected); - }); -}); - -describe('addChildren', () => { - const module = 'testModule'; - beforeEach(() => { - vm = mount().vm; - vi.clearAllMocks(); - }); - - it('should add menu items to parent if matches are found', () => { - const parent = 'testParent'; - const route = { - meta: { - menu: 'testMenu', + it('should return a proper formated object with two child items', async () => { + const expectedMenuItem = [ + { + children: null, + name: 'CustomerList', + title: 'globals.pageTitles.list', + icon: 'view_list', }, - children: [{ name: 'child1' }, { name: 'child2' }], - }; - vm.addChildren(module, route, parent); - - expect(navigation.addMenuItem).toHaveBeenCalled(); - }); - - it('should handle routes with no meta menu', () => { - const route = { - meta: {}, - menus: {}, - }; - - const parent = []; - - vm.addChildren(module, route, parent); - expect(navigation.addMenuItem).toHaveBeenCalled(); - }); - - it('should handle empty parent array', () => { - const parent = []; - const route = { - meta: { - menu: 'child11', + { + children: null, + name: 'CustomerCreate', + title: 'globals.pageTitles.createCustomer', + icon: 'vn:addperson', }, - children: [ - { - name: 'child1', - meta: { - menuChildren: [ - { - name: 'CustomerCreditContracts', - title: 'creditContracts', - icon: 'vn:solunion', - }, - ], - }, - }, - ], - }; - vm.addChildren(module, route, parent); - expect(navigation.addMenuItem).toHaveBeenCalled(); + ]; + const firstMenuItem = vm.items[0]; + expect(firstMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem)); }); }); diff --git a/src/components/__tests__/UserPanel.spec.js b/src/components/__tests__/UserPanel.spec.js index 9e449745a..ac20f911e 100644 --- a/src/components/__tests__/UserPanel.spec.js +++ b/src/components/__tests__/UserPanel.spec.js @@ -1,65 +1,61 @@ -import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { vi, describe, expect, it, beforeEach, beforeAll, afterEach } from 'vitest'; import { createWrapper } from 'app/test/vitest/helper'; import UserPanel from 'src/components/UserPanel.vue'; import axios from 'axios'; import { useState } from 'src/composables/useState'; -vi.mock('src/utils/quasarLang', () => ({ - default: vi.fn(), -})); - describe('UserPanel', () => { - let wrapper; - let vm; - let state; + let wrapper; + let vm; + let state; - beforeEach(() => { - wrapper = createWrapper(UserPanel, {}); - state = useState(); - state.setUser({ - id: 115, - name: 'itmanagement', - nickname: 'itManagementNick', - lang: 'en', - darkMode: false, - companyFk: 442, - warehouseFk: 1, + beforeEach(() => { + wrapper = createWrapper(UserPanel, {}); + state = useState(); + state.setUser({ + id: 115, + name: 'itmanagement', + nickname: 'itManagementNick', + lang: 'en', + darkMode: false, + companyFk: 442, + warehouseFk: 1, + }); + wrapper = wrapper.wrapper; + vm = wrapper.vm; }); - wrapper = wrapper.wrapper; - vm = wrapper.vm; - }); - afterEach(() => { - vi.clearAllMocks(); - }); + afterEach(() => { + vi.clearAllMocks(); + }); - it('should fetch warehouses data on mounted', async () => { - const fetchData = wrapper.findComponent({ name: 'FetchData' }); - expect(fetchData.props('url')).toBe('Warehouses'); - expect(fetchData.props('autoLoad')).toBe(true); - }); + it('should fetch warehouses data on mounted', async () => { + const fetchData = wrapper.findComponent({ name: 'FetchData' }); + expect(fetchData.props('url')).toBe('Warehouses'); + expect(fetchData.props('autoLoad')).toBe(true); + }); - it('should toggle dark mode correctly and update preferences', async () => { - await vm.saveDarkMode(true); - expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true }); - expect(vm.user.darkMode).toBe(true); - await vm.updatePreferences(); - expect(vm.darkMode).toBe(true); - }); + it('should toggle dark mode correctly and update preferences', async () => { + await vm.saveDarkMode(true); + expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true }); + expect(vm.user.darkMode).toBe(true); + vm.updatePreferences(); + expect(vm.darkMode).toBe(true); + }); - it('should change user language and update preferences', async () => { - const userLanguage = 'es'; - await vm.saveLanguage(userLanguage); - expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage }); - expect(vm.user.lang).toBe(userLanguage); - await vm.updatePreferences(); - expect(vm.locale).toBe(userLanguage); - }); + it('should change user language and update preferences', async () => { + const userLanguage = 'es'; + await vm.saveLanguage(userLanguage); + expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage }); + expect(vm.user.lang).toBe(userLanguage); + vm.updatePreferences(); + expect(vm.locale).toBe(userLanguage); + }); - it('should update user data', async () => { - const key = 'name'; - const value = 'itboss'; - await vm.saveUserData(key, value); - expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value }); - }); -}); \ No newline at end of file + it('should update user data', async () => { + const key = 'name'; + const value = 'itboss'; + await vm.saveUserData(key, value); + expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value }); + }); +}); diff --git a/src/components/common/VnCard.vue b/src/components/common/VnCard.vue index 44002c22a..0d80f43ce 100644 --- a/src/components/common/VnCard.vue +++ b/src/components/common/VnCard.vue @@ -10,11 +10,11 @@ import LeftMenu from 'components/LeftMenu.vue'; import RightMenu from 'components/common/RightMenu.vue'; const props = defineProps({ dataKey: { type: String, required: true }, - url: { type: String, default: undefined }, + baseUrl: { type: String, default: undefined }, + customUrl: { type: String, default: undefined }, filter: { type: Object, default: () => {} }, descriptor: { type: Object, required: true }, filterPanel: { type: Object, default: undefined }, - idInWhere: { type: Boolean, default: false }, searchDataKey: { type: String, default: undefined }, searchbarProps: { type: Object, default: undefined }, redirectOnError: { type: Boolean, default: false }, @@ -23,20 +23,25 @@ const props = defineProps({ const stateStore = useStateStore(); const route = useRoute(); const router = useRouter(); +const url = computed(() => { + if (props.baseUrl) { + return `${props.baseUrl}/${route.params.id}`; + } + return props.customUrl; +}); const searchRightDataKey = computed(() => { if (!props.searchDataKey) return route.name; return props.searchDataKey; }); - const arrayData = useArrayData(props.dataKey, { - url: props.url, - userFilter: props.filter, - oneRecord: true, + url: url.value, + filter: props.filter, }); onBeforeMount(async () => { try { - await fetch(route.params.id); + if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; + await arrayData.fetch({ append: false, updateRouter: false }); } catch { const { matched: matches } = router.currentRoute.value; const { path } = matches.at(-1); @@ -44,17 +49,13 @@ onBeforeMount(async () => { } }); -onBeforeRouteUpdate(async (to, from) => { - const id = to.params.id; - if (id !== from.params.id) await fetch(id, true); -}); - -async function fetch(id, append = false) { - const regex = /\/(\d+)/; - if (props.idInWhere) arrayData.store.filter.where = { id }; - else if (!regex.test(props.url)) arrayData.store.url = `${props.url}/${id}`; - else arrayData.store.url = props.url.replace(regex, `/${id}`); - await arrayData.fetch({ append, updateRouter: false }); +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> <template> @@ -82,7 +83,7 @@ async function fetch(id, append = false) { <QPage> <VnSubToolbar /> <div :class="[useCardSize(), $attrs.class]"> - <RouterView :key="$route.path" /> + <RouterView :key="route.path" /> </div> </QPage> </QPageContainer> diff --git a/src/components/common/VnCardBeta.vue b/src/components/common/VnCardBeta.vue index 7c82316dc..f237a300c 100644 --- a/src/components/common/VnCardBeta.vue +++ b/src/components/common/VnCardBeta.vue @@ -1,6 +1,6 @@ <script setup> -import { onBeforeMount } from 'vue'; -import { useRouter, onBeforeRouteUpdate } from 'vue-router'; +import { onBeforeMount, computed } from 'vue'; +import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router'; import { useArrayData } from 'src/composables/useArrayData'; import { useStateStore } from 'stores/useStateStore'; import useCardSize from 'src/composables/useCardSize'; @@ -9,9 +9,10 @@ import VnSubToolbar from '../ui/VnSubToolbar.vue'; const props = defineProps({ dataKey: { type: String, required: true }, - url: { type: String, default: undefined }, - idInWhere: { type: Boolean, default: false }, + baseUrl: { type: String, default: undefined }, + customUrl: { type: String, default: undefined }, filter: { type: Object, default: () => {} }, + userFilter: { type: Object, default: () => {} }, descriptor: { type: Object, required: true }, filterPanel: { type: Object, default: undefined }, searchDataKey: { type: String, default: undefined }, @@ -20,42 +21,46 @@ const props = defineProps({ }); const stateStore = useStateStore(); +const route = useRoute(); const router = useRouter(); +const url = computed(() => { + if (props.baseUrl) { + return `${props.baseUrl}/${route.params.id}`; + } + return props.customUrl; +}); + const arrayData = useArrayData(props.dataKey, { - url: props.url, - userFilter: props.filter, - oneRecord: true, + url: url.value, + filter: props.filter, + userFilter: props.userFilter, }); onBeforeMount(async () => { - const route = router.currentRoute.value; try { - await fetch(route.params.id); + if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; + await arrayData.fetch({ append: false, updateRouter: false }); } catch { - const { matched: matches } = route; + const { matched: matches } = router.currentRoute.value; const { path } = matches.at(-1); router.push({ path: path.replace(/:id.*/, '') }); } }); -onBeforeRouteUpdate(async (to, from) => { - if (hasRouteParam(to.params)) { - const { matched } = router.currentRoute.value; - const { name } = matched.at(-3); - if (name) { - router.push({ name, params: to.params }); +if (props.baseUrl) { + onBeforeRouteUpdate(async (to, from) => { + if (hasRouteParam(to.params)) { + const { matched } = router.currentRoute.value; + const { name } = matched.at(-3); + if (name) { + router.push({ name, params: to.params }); + } } - } - const id = to.params.id; - if (id !== from.params.id) await fetch(id, true); -}); - -async function fetch(id, append = false) { - const regex = /\/(\d+)/; - if (props.idInWhere) arrayData.store.filter.where = { id }; - else if (!regex.test(props.url)) arrayData.store.url = `${props.url}/${id}`; - else arrayData.store.url = props.url.replace(regex, `/${id}`); - await arrayData.fetch({ append, updateRouter: false }); + if (to.params.id !== from.params.id) { + arrayData.store.url = `${props.baseUrl}/${to.params.id}`; + await arrayData.fetch({ append: false, updateRouter: false }); + } + }); } function hasRouteParam(params, valueToCheck = ':addressId') { return Object.values(params).includes(valueToCheck); @@ -69,6 +74,6 @@ function hasRouteParam(params, valueToCheck = ':addressId') { </Teleport> <VnSubToolbar /> <div :class="[useCardSize(), $attrs.class]"> - <RouterView :key="$route.path" /> + <RouterView :key="route.path" /> </div> </template> diff --git a/src/components/common/VnCheckbox.vue b/src/components/common/VnCheckbox.vue deleted file mode 100644 index 27131d45e..000000000 --- a/src/components/common/VnCheckbox.vue +++ /dev/null @@ -1,43 +0,0 @@ -<script setup> -import { computed } from 'vue'; - -const model = defineModel({ type: [Number, Boolean] }); -const $props = defineProps({ - info: { - type: String, - default: null, - }, -}); - -const checkboxModel = computed({ - get() { - if (typeof model.value === 'number') { - return model.value !== 0; - } - return model.value; - }, - set(value) { - if (typeof model.value === 'number') { - model.value = value ? 1 : 0; - } else { - model.value = value; - } - }, -}); -</script> -<template> - <div> - <QCheckbox v-bind="$attrs" v-on="$attrs" v-model="checkboxModel" /> - <QIcon - v-if="info" - v-bind="$attrs" - class="cursor-info q-ml-sm" - name="info" - size="sm" - > - <QTooltip> - {{ info }} - </QTooltip> - </QIcon> - </div> -</template> diff --git a/src/components/common/VnColor.vue b/src/components/common/VnColor.vue deleted file mode 100644 index 8a5a787b0..000000000 --- a/src/components/common/VnColor.vue +++ /dev/null @@ -1,32 +0,0 @@ -<script setup> -const $props = defineProps({ - colors: { - type: String, - default: '{"value": []}', - }, -}); - -const colorArray = JSON.parse($props.colors)?.value; -const maxHeight = 30; -const colorHeight = maxHeight / colorArray?.length; -</script> -<template> - <div v-if="colors" class="color-div" :style="{ height: `${maxHeight}px` }"> - <div - v-for="(color, index) in colorArray" - :key="index" - :style="{ - backgroundColor: `#${color}`, - height: `${colorHeight}px`, - }" - > - - </div> - </div> -</template> -<style scoped> -.color-div { - display: flex; - flex-direction: column; -} -</style> diff --git a/src/components/common/VnComponent.vue b/src/components/common/VnComponent.vue index a9e1c8cff..580bcf348 100644 --- a/src/components/common/VnComponent.vue +++ b/src/components/common/VnComponent.vue @@ -17,8 +17,6 @@ const $props = defineProps({ }, }); -const emit = defineEmits(['blur']); - const componentArray = computed(() => { if (typeof $props.prop === 'object') return [$props.prop]; return $props.prop; @@ -48,8 +46,7 @@ function toValueAttrs(attrs) { <span v-for="toComponent of componentArray" :key="toComponent.name" - class="column fit" - :class="toComponent?.component == 'checkbox' ? 'flex-center' : ''" + class="column flex-center fit" > <component v-if="toComponent?.component" @@ -57,7 +54,6 @@ function toValueAttrs(attrs) { v-bind="mix(toComponent).attrs" v-on="mix(toComponent).event ?? {}" v-model="model" - @blur="emit('blur')" /> </span> </template> diff --git a/src/components/common/VnDmsList.vue b/src/components/common/VnDmsList.vue index 424781a26..36c87bab0 100644 --- a/src/components/common/VnDmsList.vue +++ b/src/components/common/VnDmsList.vue @@ -17,7 +17,7 @@ import { useSession } from 'src/composables/useSession'; const route = useRoute(); const quasar = useQuasar(); const { t } = useI18n(); -const rows = ref([]); +const rows = ref(); const dmsRef = ref(); const formDialog = ref({}); const token = useSession().getTokenMultimedia(); @@ -389,14 +389,6 @@ defineExpose({ </div> </template> </QTable> - <div - v-else - class="info-row q-pa-md text-center" - > - <h5> - {{ t('No data to display') }} - </h5> - </div> </template> </VnPaginate> <QDialog v-model="formDialog.show"> @@ -413,7 +405,7 @@ defineExpose({ fab color="primary" icon="add" - v-shortcut + shortcut="+" @click="showFormDialog()" class="fill-icon" > diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index aeb4a31fd..78f08a479 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -11,7 +11,6 @@ const emit = defineEmits([ 'update:options', 'keyup.enter', 'remove', - 'blur', ]); const $props = defineProps({ @@ -137,7 +136,6 @@ const handleUppercase = () => { :type="$attrs.type" :class="{ required: isRequired }" @keyup.enter="emit('keyup.enter')" - @blur="emit('blur')" @keydown="handleKeydown" :clearable="false" :rules="mixinRules" @@ -145,7 +143,7 @@ const handleUppercase = () => { hide-bottom-space :data-cy="$attrs.dataCy ?? $attrs.label + '_input'" > - <template #prepend v-if="$slots.prepend"> + <template #prepend> <slot name="prepend" /> </template> <template #append> @@ -170,11 +168,11 @@ const handleUppercase = () => { } " ></QIcon> - + <QIcon name="match_case" size="xs" - v-if="!$attrs.disabled && !$attrs.readonly && $props.uppercase" + v-if="!$attrs.disabled && !($attrs.readonly) && $props.uppercase" @click="handleUppercase" class="uppercase-icon" > @@ -182,7 +180,7 @@ const handleUppercase = () => { {{ t('Convert to uppercase') }} </QTooltip> </QIcon> - + <slot name="append" v-if="$slots.append && !$attrs.disabled" /> <QIcon v-if="info" name="info"> <QTooltip max-width="350px"> @@ -196,15 +194,13 @@ const handleUppercase = () => { <style> .uppercase-icon { - transition: - color 0.3s, - transform 0.2s; - cursor: pointer; + transition: color 0.3s, transform 0.2s; + cursor: pointer; } .uppercase-icon:hover { - color: #ed9937; - transform: scale(1.2); + color: #ed9937; + transform: scale(1.2); } </style> <i18n> @@ -218,4 +214,4 @@ const handleUppercase = () => { maxLength: El valor excede los {value} carácteres inputMax: Debe ser menor a {value} Convert to uppercase: Convertir a mayúsculas -</i18n> +</i18n> \ No newline at end of file diff --git a/src/components/common/VnInputDate.vue b/src/components/common/VnInputDate.vue index 73c825e1e..a8888aad8 100644 --- a/src/components/common/VnInputDate.vue +++ b/src/components/common/VnInputDate.vue @@ -42,7 +42,7 @@ const formattedDate = computed({ if (value.at(2) == '/') value = value.split('/').reverse().join('/'); value = date.formatDate( new Date(value).toISOString(), - 'YYYY-MM-DDTHH:mm:ss.SSSZ', + 'YYYY-MM-DDTHH:mm:ss.SSSZ' ); } const [year, month, day] = value.split('-').map((e) => parseInt(e)); @@ -55,7 +55,7 @@ const formattedDate = computed({ orgDate.getHours(), orgDate.getMinutes(), orgDate.getSeconds(), - orgDate.getMilliseconds(), + orgDate.getMilliseconds() ); } } @@ -64,7 +64,7 @@ const formattedDate = computed({ }); const popupDate = computed(() => - model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value, + model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value ); onMounted(() => { // fix quasar bug @@ -73,7 +73,7 @@ onMounted(() => { watch( () => model.value, (val) => (formattedDate.value = val), - { immediate: true }, + { immediate: true } ); const styleAttrs = computed(() => { diff --git a/src/components/common/VnInputNumber.vue b/src/components/common/VnInputNumber.vue index 274f78b21..165cfae3d 100644 --- a/src/components/common/VnInputNumber.vue +++ b/src/components/common/VnInputNumber.vue @@ -8,7 +8,6 @@ defineProps({ }); const model = defineModel({ type: [Number, String] }); -const emit = defineEmits(['blur']); </script> <template> <VnInput @@ -25,6 +24,5 @@ const emit = defineEmits(['blur']); model = parseFloat(val).toFixed(decimalPlaces); } " - @blur="emit('blur')" /> </template> diff --git a/src/components/common/VnPopupProxy.vue b/src/components/common/VnPopupProxy.vue deleted file mode 100644 index f386bfff8..000000000 --- a/src/components/common/VnPopupProxy.vue +++ /dev/null @@ -1,38 +0,0 @@ -<script setup> -import { ref } from 'vue'; - -defineProps({ - label: { - type: String, - default: '', - }, - icon: { - type: String, - required: true, - default: null, - }, - color: { - type: String, - default: 'primary', - }, - tooltip: { - type: String, - default: null, - }, -}); -const popupProxyRef = ref(null); -</script> - -<template> - <QBtn :color="$props.color" :icon="$props.icon" :label="$t($props.label)"> - <template #default> - <slot name="extraIcon"></slot> - <QPopupProxy ref="popupProxyRef" style="max-width: none"> - <QCard> - <slot :popup="popupProxyRef"></slot> - </QCard> - </QPopupProxy> - <QTooltip>{{ $t($props.tooltip) }}</QTooltip> - </template> - </QBtn> -</template> diff --git a/src/components/common/VnSection.vue b/src/components/common/VnSection.vue index 4bd17124f..ef65b841f 100644 --- a/src/components/common/VnSection.vue +++ b/src/components/common/VnSection.vue @@ -106,14 +106,7 @@ function checkIsMain() { :data-key="dataKey" :array-data="arrayData" :columns="columns" - > - <template #moreFilterPanel="{ params, orders, searchFn }"> - <slot - name="moreFilterPanel" - v-bind="{ params, orders, searchFn }" - /> - </template> - </VnTableFilter> + /> </slot> </template> </RightAdvancedMenu> diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 339f90e0e..95fe80a69 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -10,12 +10,7 @@ const emit = defineEmits(['update:modelValue', 'update:options', 'remove']); const $attrs = useAttrs(); const { t } = useI18n(); -const isRequired = computed(() => { - return useRequired($attrs).isRequired; -}); -const requiredFieldRule = computed(() => { - return useRequired($attrs).requiredFieldRule; -}); +const { isRequired, requiredFieldRule } = useRequired($attrs); const $props = defineProps({ modelValue: { @@ -171,8 +166,7 @@ onMounted(() => { }); const arrayDataKey = - $props.dataKey ?? - ($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label)); + $props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label); const arrayData = useArrayData(arrayDataKey, { url: $props.url, @@ -221,7 +215,7 @@ async function fetchFilter(val) { optionFilterValue.value ?? (new RegExp(/\d/g).test(val) ? optionValue.value - : (optionFilter.value ?? optionLabel.value)); + : optionFilter.value ?? optionLabel.value); let defaultWhere = {}; if ($props.filterOptions.length) { @@ -240,7 +234,7 @@ async function fetchFilter(val) { const { data } = await arrayData.applyFilter( { filter: filterOptions }, - { updateRouter: false }, + { updateRouter: false } ); setOptions(data); return data; @@ -273,7 +267,7 @@ async function filterHandler(val, update) { ref.setOptionIndex(-1); ref.moveOptionSelection(1, true); } - }, + } ); } @@ -309,7 +303,7 @@ function handleKeyDown(event) { if (inputValue) { const matchingOption = myOptions.value.find( (option) => - option[optionLabel.value].toLowerCase() === inputValue.toLowerCase(), + option[optionLabel.value].toLowerCase() === inputValue.toLowerCase() ); if (matchingOption) { @@ -321,11 +315,11 @@ function handleKeyDown(event) { } const focusableElements = document.querySelectorAll( - 'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])', + 'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])' ); const currentIndex = Array.prototype.indexOf.call( focusableElements, - event.target, + event.target ); if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) { focusableElements[currentIndex + 1].focus(); diff --git a/src/components/common/VnSelectCache.vue b/src/components/common/VnSelectCache.vue index f0f3357f6..29cf22dc5 100644 --- a/src/components/common/VnSelectCache.vue +++ b/src/components/common/VnSelectCache.vue @@ -14,7 +14,7 @@ const $props = defineProps({ }, }); const options = ref([]); -const emit = defineEmits(['blur']); + onBeforeMount(async () => { const { url, optionValue, optionLabel } = useAttrs(); const findBy = $props.find ?? url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1); @@ -35,5 +35,5 @@ onBeforeMount(async () => { }); </script> <template> - <VnSelect v-bind="$attrs" :options="$attrs.options ?? options" @blur="emit('blur')" /> + <VnSelect v-bind="$attrs" :options="$attrs.options ?? options" /> </template> diff --git a/src/components/common/VnSelectDialog.vue b/src/components/common/VnSelectDialog.vue index 41730b217..a4cd0011d 100644 --- a/src/components/common/VnSelectDialog.vue +++ b/src/components/common/VnSelectDialog.vue @@ -37,6 +37,7 @@ const isAllowedToCreate = computed(() => { defineExpose({ vnSelectDialogRef: select }); </script> + <template> <VnSelect ref="select" @@ -66,6 +67,7 @@ defineExpose({ vnSelectDialogRef: select }); </template> </VnSelect> </template> + <style lang="scss" scoped> .default-icon { cursor: pointer; diff --git a/src/components/common/VnSelectSupplier.vue b/src/components/common/VnSelectSupplier.vue index 5b52ae75b..f86db4f2d 100644 --- a/src/components/common/VnSelectSupplier.vue +++ b/src/components/common/VnSelectSupplier.vue @@ -1,7 +1,9 @@ <script setup> +import { computed } from 'vue'; import VnSelect from 'components/common/VnSelect.vue'; const model = defineModel({ type: [String, Number, Object] }); +const url = 'Suppliers'; </script> <template> @@ -9,13 +11,11 @@ const model = defineModel({ type: [String, Number, Object] }); :label="$t('globals.supplier')" v-bind="$attrs" v-model="model" - url="Suppliers" + :url="url" option-value="id" option-label="nickname" :fields="['id', 'name', 'nickname', 'nif']" - :filter-options="['id', 'name', 'nickname', 'nif']" sort-by="name ASC" - data-cy="vnSupplierSelect" > <template #option="scope"> <QItem v-bind="scope.itemProps"> diff --git a/src/components/common/VnSelectTravelExtended.vue b/src/components/common/VnSelectTravelExtended.vue deleted file mode 100644 index 46538f5f9..000000000 --- a/src/components/common/VnSelectTravelExtended.vue +++ /dev/null @@ -1,50 +0,0 @@ -<script setup> -import VnSelectDialog from './VnSelectDialog.vue'; -import FilterTravelForm from 'src/components/FilterTravelForm.vue'; -import { useI18n } from 'vue-i18n'; -import { toDate } from 'src/filters'; -const { t } = useI18n(); - -const $props = defineProps({ - data: { - type: Object, - required: true, - }, - onFilterTravelSelected: { - type: Function, - required: true, - }, -}); -</script> -<template> - <VnSelectDialog - :label="t('entry.basicData.travel')" - v-bind="$attrs" - url="Travels/filter" - :fields="['id', 'warehouseInName']" - option-value="id" - option-label="warehouseInName" - map-options - hide-selected - :required="true" - action-icon="filter_alt" - :roles-allowed-to-create="['buyer']" - > - <template #form> - <FilterTravelForm @travel-selected="onFilterTravelSelected(data, $event)" /> - </template> - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel> - {{ scope.opt?.agencyModeName }} - - {{ scope.opt?.warehouseInName }} - ({{ toDate(scope.opt?.shipped) }}) → - {{ scope.opt?.warehouseOutName }} - ({{ toDate(scope.opt?.landed) }}) - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelectDialog> -</template> diff --git a/src/components/common/__tests__/VnNotes.spec.js b/src/components/common/__tests__/VnNotes.spec.js index 2603bf03c..8f24a7f14 100644 --- a/src/components/common/__tests__/VnNotes.spec.js +++ b/src/components/common/__tests__/VnNotes.spec.js @@ -1,78 +1,51 @@ -import { - describe, - it, - expect, - vi, - beforeAll, - afterEach, - beforeEach, - afterAll, -} from 'vitest'; +import { describe, it, expect, vi, beforeAll, afterEach, beforeEach } from 'vitest'; import { createWrapper, axios } from 'app/test/vitest/helper'; import VnNotes from 'src/components/ui/VnNotes.vue'; -import vnDate from 'src/boot/vnDate'; describe('VnNotes', () => { let vm; let wrapper; let spyFetch; let postMock; - let patchMock; - let expectedInsertBody; - let expectedUpdateBody; - const defaultOptions = { - url: '/test', - body: { name: 'Tony', lastName: 'Stark' }, - selectType: false, - saveUrl: null, - justInput: false, - }; - function generateWrapper( - options = defaultOptions, - text = null, - observationType = null, - ) { - vi.spyOn(axios, 'get').mockResolvedValue({ data: [] }); + let expectedBody; + const mockData= {name: 'Tony', lastName: 'Stark', text: 'Test Note', observationTypeFk: 1}; + + function generateExpectedBody() { + expectedBody = {...vm.$props.body, ...{ text: vm.newNote.text, observationTypeFk: vm.newNote.observationTypeFk }}; + } + + async function setTestParams(text, observationType, type){ + vm.newNote.text = text; + vm.newNote.observationTypeFk = observationType; + wrapper.setProps({ selectType: type }); + } + + beforeAll(async () => { + vi.spyOn(axios, 'get').mockReturnValue({ data: [] }); + wrapper = createWrapper(VnNotes, { - propsData: options, + propsData: { + url: '/test', + body: { name: 'Tony', lastName: 'Stark' }, + } }); wrapper = wrapper.wrapper; vm = wrapper.vm; - vm.newNote.text = text; - vm.newNote.observationTypeFk = observationType; - } - - function createSpyFetch() { - spyFetch = vi.spyOn(vm.$refs.vnPaginateRef, 'fetch'); - } - - function generateExpectedBody() { - expectedInsertBody = { - ...vm.$props.body, - ...{ text: vm.newNote.text, observationTypeFk: vm.newNote.observationTypeFk }, - }; - expectedUpdateBody = { ...vm.$props.body, ...{ notes: vm.newNote.text } }; - } + }); beforeEach(() => { - postMock = vi.spyOn(axios, 'post'); - patchMock = vi.spyOn(axios, 'patch'); + postMock = vi.spyOn(axios, 'post').mockResolvedValue(mockData); + spyFetch = vi.spyOn(vm.vnPaginateRef, 'fetch').mockImplementation(() => vi.fn()); }); afterEach(() => { vi.clearAllMocks(); - expectedInsertBody = {}; - expectedUpdateBody = {}; - }); - - afterAll(() => { - vi.restoreAllMocks(); + expectedBody = {}; }); describe('insert', () => { - it('should not call axios.post and vnPaginateRef.fetch when newNote.text is null', async () => { - generateWrapper({ selectType: true }); - createSpyFetch(); + it('should not call axios.post and vnPaginateRef.fetch if newNote.text is null', async () => { + await setTestParams( null, null, true ); await vm.insert(); @@ -80,9 +53,8 @@ describe('VnNotes', () => { expect(spyFetch).not.toHaveBeenCalled(); }); - it('should not call axios.post and vnPaginateRef.fetch when newNote.text is empty', async () => { - generateWrapper(null, ''); - createSpyFetch(); + it('should not call axios.post and vnPaginateRef.fetch if newNote.text is empty', async () => { + await setTestParams( "", null, false ); await vm.insert(); @@ -90,9 +62,8 @@ describe('VnNotes', () => { expect(spyFetch).not.toHaveBeenCalled(); }); - it('should not call axios.post and vnPaginateRef.fetch when observationTypeFk is null and selectType is true', async () => { - generateWrapper({ selectType: true }, 'Test Note'); - createSpyFetch(); + it('should not call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is true', async () => { + await setTestParams( "Test Note", null, true ); await vm.insert(); @@ -100,57 +71,37 @@ describe('VnNotes', () => { expect(spyFetch).not.toHaveBeenCalled(); }); - it('should call axios.post and vnPaginateRef.fetch when observationTypeFk is missing and selectType is false', async () => { - generateWrapper(null, 'Test Note'); - createSpyFetch(); + it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is false', async () => { + await setTestParams( "Test Note", null, false ); + generateExpectedBody(); await vm.insert(); - expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedInsertBody); + expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody); + expect(spyFetch).toHaveBeenCalled(); + }); + + it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is setted and selectType is false', async () => { + await setTestParams( "Test Note", 1, false ); + + generateExpectedBody(); + + await vm.insert(); + + expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody); expect(spyFetch).toHaveBeenCalled(); }); it('should call axios.post and vnPaginateRef.fetch when newNote is valid', async () => { - generateWrapper({ selectType: true }, 'Test Note', 1); - createSpyFetch(); - generateExpectedBody(); + await setTestParams( "Test Note", 1, true ); + generateExpectedBody(); + await vm.insert(); - expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedInsertBody); + expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody); expect(spyFetch).toHaveBeenCalled(); }); }); - - describe('update', () => { - it('should call axios.patch with saveUrl when saveUrl is set and justInput is true', async () => { - generateWrapper({ - url: '/business', - justInput: true, - saveUrl: '/saveUrlTest', - }); - generateExpectedBody(); - - await vm.update(); - - expect(patchMock).toHaveBeenCalledWith(vm.$props.saveUrl, expectedUpdateBody); - }); - - it('should call axios.patch with url when saveUrl is not set and justInput is true', async () => { - generateWrapper({ - url: '/business', - body: { workerFk: 1110 }, - justInput: true, - }); - generateExpectedBody(); - - await vm.update(); - - expect(patchMock).toHaveBeenCalledWith( - `${vm.$props.url}/${vm.$props.body.workerFk}`, - expectedUpdateBody, - ); - }); - }); -}); +}); \ No newline at end of file diff --git a/src/components/ui/CardDescriptor.vue b/src/components/ui/CardDescriptor.vue index e6e7e6fa0..43dc15e9b 100644 --- a/src/components/ui/CardDescriptor.vue +++ b/src/components/ui/CardDescriptor.vue @@ -6,7 +6,6 @@ import { useArrayData } from 'composables/useArrayData'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useState } from 'src/composables/useState'; import { useRoute } from 'vue-router'; -import { useClipboard } from 'src/composables/useClipboard'; import VnMoreOptions from './VnMoreOptions.vue'; const $props = defineProps({ @@ -30,6 +29,10 @@ const $props = defineProps({ type: String, default: null, }, + module: { + type: String, + default: null, + }, summary: { type: Object, default: null, @@ -43,7 +46,6 @@ const $props = defineProps({ const state = useState(); const route = useRoute(); const { t } = useI18n(); -const { copyText } = useClipboard(); const { viewSummary } = useSummaryDialog(); let arrayData; let store; @@ -55,13 +57,12 @@ defineExpose({ getData }); onBeforeMount(async () => { arrayData = useArrayData($props.dataKey, { url: $props.url, - userFilter: $props.filter, + filter: $props.filter, skip: 0, - oneRecord: true, }); store = arrayData.store; entity = computed(() => { - const data = store.data ?? {}; + const data = (Array.isArray(store.data) ? store.data[0] : store.data) ?? {}; if (data) emit('onFetch', data); return data; }); @@ -72,7 +73,7 @@ onBeforeMount(async () => { () => [$props.url, $props.filter], async () => { if (!isSameDataKey.value) await getData(); - }, + } ); }); @@ -83,7 +84,7 @@ async function getData() { try { const { data } = await arrayData.fetch({ append: false, updateRouter: false }); state.set($props.dataKey, data); - emit('onFetch', data); + emit('onFetch', Array.isArray(data) ? data[0] : data); } finally { isLoading.value = false; } @@ -101,21 +102,13 @@ function getValueFromPath(path) { return current; } -function copyIdText(id) { - copyText(id, { - component: { - copyValue: id, - }, - }); -} - 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, + : route.matched[1].children[0].redirect ); </script> @@ -154,9 +147,7 @@ const toModule = computed(() => {{ t('components.smartCard.openSummary') }} </QTooltip> </QBtn> - <RouterLink - :to="{ name: `${dataKey}Summary`, params: { id: entity.id } }" - > + <RouterLink :to="{ name: `${module}Summary`, params: { id: entity.id } }"> <QBtn class="link" color="white" @@ -192,22 +183,9 @@ const toModule = computed(() => </slot> </div> </QItemLabel> - <QItem> + <QItem dense> <QItemLabel class="subtitle" caption> #{{ getValueFromPath(subtitle) ?? entity.id }} - <QBtn - round - flat - dense - size="sm" - icon="content_copy" - color="primary" - @click.stop="copyIdText(entity.id)" - > - <QTooltip> - {{ t('globals.copyId') }} - </QTooltip> - </QBtn> </QItemLabel> </QItem> </QList> @@ -315,11 +293,3 @@ const toModule = computed(() => } } </style> -<i18n> - en: - globals: - copyId: Copy ID - es: - globals: - copyId: Copiar ID -</i18n> diff --git a/src/components/ui/CardSummary.vue b/src/components/ui/CardSummary.vue index 6a61994c1..c815b8e16 100644 --- a/src/components/ui/CardSummary.vue +++ b/src/components/ui/CardSummary.vue @@ -40,10 +40,9 @@ const arrayData = useArrayData(props.dataKey, { filter: props.filter, userFilter: props.userFilter, skip: 0, - oneRecord: true, }); const { store } = arrayData; -const entity = computed(() => store.data); +const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data)); const isLoading = ref(false); defineExpose({ @@ -62,7 +61,7 @@ async function fetch() { store.filter = props.filter ?? {}; isLoading.value = true; const { data } = await arrayData.fetch({ append: false, updateRouter: false }); - emit('onFetch', data); + emit('onFetch', Array.isArray(data) ? data[0] : data); isLoading.value = false; } </script> @@ -209,13 +208,4 @@ async function fetch() { .summaryHeader { color: $white; } - -.cardSummary :deep(.q-card__section[content]) { - display: flex; - flex-wrap: wrap; - padding: 0; - > * { - flex: 1; - } -} </style> diff --git a/src/components/ui/SkeletonDescriptor.vue b/src/components/ui/SkeletonDescriptor.vue index f9188221a..9679751f5 100644 --- a/src/components/ui/SkeletonDescriptor.vue +++ b/src/components/ui/SkeletonDescriptor.vue @@ -1,32 +1,53 @@ -<script setup> -defineProps({ - hasImage: { - type: Boolean, - default: false, - }, -}); -</script> <template> - <div id="descriptor-skeleton" class="bg-vn-page"> + <div id="descriptor-skeleton"> <div class="row justify-between q-pa-sm"> - <QSkeleton square size="30px" v-for="i in 3" :key="i" /> + <QSkeleton square size="40px" /> + <QSkeleton square size="40px" /> + <QSkeleton square height="40px" width="20px" /> </div> - <div class="q-pa-xs" v-if="hasImage"> - <QSkeleton square height="200px" width="100%" /> + <div class="col justify-between q-pa-sm q-gutter-y-xs"> + <QSkeleton square height="40px" width="150px" /> + <QSkeleton square height="30px" width="70px" /> </div> - <div class="col justify-between q-pa-md q-gutter-y-xs"> - <QSkeleton square height="25px" width="150px" /> - <QSkeleton square height="15px" width="70px" /> - </div> - <div class="q-pl-sm q-pa-sm q-mb-md"> - <div class="row q-gutter-x-sm q-pa-none q-ma-none" v-for="i in 5" :key="i"> - <QSkeleton type="text" square height="20px" width="30%" /> - <QSkeleton type="text" square height="20px" width="60%" /> + <div class="col q-pl-sm q-pa-sm q-mb-md"> + <div class="row justify-between"> + <QSkeleton type="text" square height="30px" width="20%" /> + <QSkeleton type="text" square height="30px" width="60%" /> + </div> + <div class="row justify-between"> + <QSkeleton type="text" square height="30px" width="20%" /> + <QSkeleton type="text" square height="30px" width="60%" /> + </div> + <div class="row justify-between"> + <QSkeleton type="text" square height="30px" width="20%" /> + <QSkeleton type="text" square height="30px" width="60%" /> + </div> + <div class="row justify-between"> + <QSkeleton type="text" square height="30px" width="20%" /> + <QSkeleton type="text" square height="30px" width="60%" /> + </div> + <div class="row justify-between"> + <QSkeleton type="text" square height="30px" width="20%" /> + <QSkeleton type="text" square height="30px" width="60%" /> + </div> + <div class="row justify-between"> + <QSkeleton type="text" square height="30px" width="20%" /> + <QSkeleton type="text" square height="30px" width="60%" /> </div> </div> - <QCardActions class="q-gutter-x-sm justify-between"> - <QSkeleton size="40px" v-for="i in 5" :key="i" /> + <QCardActions> + <QSkeleton size="40px" /> + <QSkeleton size="40px" /> + <QSkeleton size="40px" /> + <QSkeleton size="40px" /> + <QSkeleton size="40px" /> </QCardActions> </div> </template> + +<style lang="scss" scoped> +#descriptor-skeleton .q-card__actions { + justify-content: space-between; +} +</style> diff --git a/src/components/ui/VnConfirm.vue b/src/components/ui/VnConfirm.vue index c6f539879..a02b56bdb 100644 --- a/src/components/ui/VnConfirm.vue +++ b/src/components/ui/VnConfirm.vue @@ -82,7 +82,7 @@ function cancel() { @click="cancel()" /> </QCardSection> - <QCardSection class="q-pb-none" data-cy="VnConfirm_message"> + <QCardSection class="q-pb-none"> <span v-if="message !== false" v-html="message" /> </QCardSection> <QCardSection class="row items-center q-pt-none"> @@ -95,7 +95,6 @@ function cancel() { :disable="isLoading" flat @click="cancel()" - data-cy="VnConfirm_cancel" /> <QBtn :label="t('globals.confirm')" diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index d6b525dc8..93f069cc6 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -114,7 +114,7 @@ async function clearFilters() { arrayData.resetPagination(); // Filtrar los params no removibles const removableFilters = Object.keys(userParams.value).filter((param) => - $props.unremovableParams.includes(param), + $props.unremovableParams.includes(param) ); const newParams = {}; // Conservar solo los params que no son removibles @@ -162,13 +162,13 @@ const formatTags = (tags) => { const tags = computed(() => { const filteredTags = tagsList.value.filter( - (tag) => !($props.customTags || []).includes(tag.label), + (tag) => !($props.customTags || []).includes(tag.label) ); return formatTags(filteredTags); }); const customTags = computed(() => - tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label)), + tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label)) ); async function remove(key) { @@ -188,13 +188,10 @@ function formatValue(value) { const getLocale = (label) => { const param = label.split('.').at(-1); const globalLocale = `globals.params.${param}`; - const moduleName = route.meta.moduleName; - const moduleLocale = `${moduleName.toLowerCase()}.${param}`; if (te(globalLocale)) return t(globalLocale); - else if (te(moduleLocale)) return t(moduleLocale); + else if (te(t(`params.${param}`))); else { - const camelCaseModuleName = - moduleName.charAt(0).toLowerCase() + moduleName.slice(1); + const camelCaseModuleName = route.meta.moduleName.charAt(0).toLowerCase() + route.meta.moduleName.slice(1); return t(`${camelCaseModuleName}.params.${param}`); } }; @@ -293,9 +290,6 @@ const getLocale = (label) => { /> </template> <style scoped lang="scss"> -.q-field__label.no-pointer-events.absolute.ellipsis { - margin-left: 6px !important; -} .list { width: 256px; } diff --git a/src/components/ui/VnMoreOptions.vue b/src/components/ui/VnMoreOptions.vue index 8a1c7a0f2..39e84be2b 100644 --- a/src/components/ui/VnMoreOptions.vue +++ b/src/components/ui/VnMoreOptions.vue @@ -11,7 +11,7 @@ <QTooltip> {{ $t('components.cardDescriptor.moreOptions') }} </QTooltip> - <QMenu ref="menuRef" data-cy="descriptor-more-opts-menu"> + <QMenu ref="menuRef"> <QList> <slot name="menu" :menu-ref="$refs.menuRef" /> </QList> diff --git a/src/components/ui/VnNotes.vue b/src/components/ui/VnNotes.vue index ec6289a67..1690a94ba 100644 --- a/src/components/ui/VnNotes.vue +++ b/src/components/ui/VnNotes.vue @@ -1,6 +1,6 @@ <script setup> import axios from 'axios'; -import { ref, reactive, useAttrs, computed } from 'vue'; +import { ref, reactive } from 'vue'; import { onBeforeRouteLeave } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; @@ -16,27 +16,12 @@ import VnSelect from 'components/common/VnSelect.vue'; import FetchData from 'components/FetchData.vue'; import VnInput from 'components/common/VnInput.vue'; -const emit = defineEmits(['onFetch']); - -const originalAttrs = useAttrs(); - -const $attrs = computed(() => { - const { style, ...rest } = originalAttrs; - return rest; -}); - -const isRequired = computed(() => { - return Object.keys($attrs).includes('required') -}); - const $props = defineProps({ url: { type: String, default: null }, - saveUrl: {type: String, default: null}, filter: { type: Object, default: () => {} }, body: { type: Object, default: () => {} }, addNote: { type: Boolean, default: false }, selectType: { type: Boolean, default: false }, - justInput: { type: Boolean, default: false }, }); const { t } = useI18n(); @@ -44,13 +29,6 @@ const quasar = useQuasar(); const newNote = reactive({ text: null, observationTypeFk: null }); const observationTypes = ref([]); const vnPaginateRef = ref(); -let originalText; - -function handleClick(e) { - if (e.shiftKey && e.key === 'Enter') return; - if ($props.justInput) confirmAndUpdate(); - else insert(); -} async function insert() { if (!newNote.text || ($props.selectType && !newNote.observationTypeFk)) return; @@ -63,36 +41,8 @@ async function insert() { await axios.post($props.url, newBody); await vnPaginateRef.value.fetch(); } - -function confirmAndUpdate() { - if(!newNote.text && originalText) - quasar - .dialog({ - component: VnConfirm, - componentProps: { - title: t('New note is empty'), - message: t('Are you sure remove this note?'), - }, - }) - .onOk(update) - .onCancel(() => { - newNote.text = originalText; - }); - else update(); -} - -async function update() { - originalText = newNote.text; - const body = $props.body; - const newBody = { - ...body, - ...{ notes: newNote.text }, - }; - await axios.patch(`${$props.saveUrl ?? `${$props.url}/${$props.body.workerFk}`}`, newBody); -} - onBeforeRouteLeave((to, from, next) => { - if ((newNote.text && !$props.justInput) || (newNote.text !== originalText) && $props.justInput) + if (newNote.text) quasar.dialog({ component: VnConfirm, componentProps: { @@ -103,13 +53,6 @@ onBeforeRouteLeave((to, from, next) => { }); else next(); }); - -function fetchData([ data ]) { - newNote.text = data?.notes; - originalText = data?.notes; - emit('onFetch', data); -} - </script> <template> <FetchData @@ -119,19 +62,8 @@ function fetchData([ data ]) { auto-load @on-fetch="(data) => (observationTypes = data)" /> - <FetchData - v-if="justInput" - :url="url" - :filter="filter" - @on-fetch="fetchData" - auto-load - /> - <QCard - class="q-pa-xs q-mb-lg full-width" - :class="{ 'just-input': $props.justInput }" - v-if="$props.addNote || $props.justInput" - > - <QCardSection horizontal v-if="!$props.justInput"> + <QCard class="q-pa-xs q-mb-lg full-width" v-if="$props.addNote"> + <QCardSection horizontal> {{ t('New note') }} </QCardSection> <QCardSection class="q-px-xs q-my-none q-py-none"> @@ -143,19 +75,19 @@ function fetchData([ data ]) { v-model="newNote.observationTypeFk" option-label="description" style="flex: 0.15" - :required="isRequired" + :required="true" @keyup.enter.stop="insert" /> <VnInput v-model.trim="newNote.text" type="textarea" - :label="$props.justInput && newNote.text ? '' : t('Add note here...')" + :label="t('Add note here...')" filled size="lg" autogrow - @keyup.enter.stop="handleClick" - :required="isRequired" + @keyup.enter.stop="insert" clearable + :required="true" > <template #append> <QBtn @@ -163,7 +95,7 @@ function fetchData([ data ]) { icon="save" color="primary" flat - @click="handleClick" + @click="insert" class="q-mb-xs" dense data-cy="saveNote" @@ -174,7 +106,6 @@ function fetchData([ data ]) { </QCardSection> </QCard> <VnPaginate - v-if="!$props.justInput" :data-key="$props.url" :url="$props.url" order="created DESC" @@ -267,11 +198,6 @@ function fetchData([ data ]) { } } } -.just-input { - padding-right: 18px; - margin-bottom: 2px; - box-shadow: none; -} </style> <i18n> es: @@ -279,6 +205,4 @@ function fetchData([ data ]) { New note: Nueva nota Save (Enter): Guardar (Intro) Observation type: Tipo de observación - New note is empty: La nueva nota esta vacia - Are you sure remove this note?: Estas seguro de quitar esta nota? </i18n> diff --git a/src/components/ui/VnStockValueDisplay.vue b/src/components/ui/VnStockValueDisplay.vue deleted file mode 100644 index d8f43323b..000000000 --- a/src/components/ui/VnStockValueDisplay.vue +++ /dev/null @@ -1,41 +0,0 @@ -<script setup> -import { toPercentage } from 'filters/index'; - -import { computed } from 'vue'; - -const props = defineProps({ - value: { - type: Number, - required: true, - }, -}); - -const valueClass = computed(() => - props.value === 0 ? 'neutral' : props.value > 0 ? 'positive' : 'negative', -); -const iconName = computed(() => - props.value === 0 ? 'equal' : props.value > 0 ? 'arrow_upward' : 'arrow_downward', -); -const formattedValue = computed(() => props.value); -</script> -<template> - <span :class="valueClass"> - <QIcon :name="iconName" size="sm" class="value-icon" /> - {{ toPercentage(formattedValue) }} - </span> -</template> - -<style lang="scss" scoped> -.positive { - color: $secondary; -} -.negative { - color: $negative; -} -.neutral { - color: $primary; -} -.value-icon { - margin-right: 4px; -} -</style> diff --git a/src/components/ui/VnSubToolbar.vue b/src/components/ui/VnSubToolbar.vue index 8d4126d1d..5ded4be00 100644 --- a/src/components/ui/VnSubToolbar.vue +++ b/src/components/ui/VnSubToolbar.vue @@ -19,26 +19,23 @@ onMounted(() => { const observer = new MutationObserver( () => (hasContent.value = - actions.value?.childNodes?.length + data.value?.childNodes?.length), + actions.value?.childNodes?.length + data.value?.childNodes?.length) ); if (actions.value) observer.observe(actions.value, opts); if (data.value) observer.observe(data.value, opts); }); -const actionsChildCount = () => !!actions.value?.childNodes?.length; - -onBeforeUnmount(() => stateStore.toggleSubToolbar() && hasSubToolbar); +onBeforeUnmount(() => stateStore.toggleSubToolbar()); </script> <template> <QToolbar id="subToolbar" - v-show="hasContent || $slots['st-actions'] || $slots['st-data']" class="justify-end sticky" + v-show="hasContent || $slots['st-actions'] || $slots['st-data']" > <slot name="st-data"> - <div id="st-data" :class="{ 'full-width': !actionsChildCount() }"> - </div> + <div id="st-data"></div> </slot> <QSpace /> <slot name="st-actions"> diff --git a/src/components/ui/__tests__/CardSummary.spec.js b/src/components/ui/__tests__/CardSummary.spec.js index 2f7f90882..411ebf9bb 100644 --- a/src/components/ui/__tests__/CardSummary.spec.js +++ b/src/components/ui/__tests__/CardSummary.spec.js @@ -51,6 +51,16 @@ describe('CardSummary', () => { expect(vm.store.filter).toEqual('cardFilter'); }); + it('should compute entity correctly from store data', () => { + vm.store.data = [{ id: 1, name: 'Entity 1' }]; + expect(vm.entity).toEqual({ id: 1, name: 'Entity 1' }); + }); + + it('should handle empty data gracefully', () => { + vm.store.data = []; + expect(vm.entity).toBeUndefined(); + }); + it('should respond to prop changes and refetch data', async () => { const newUrl = 'CardSummary/35'; const newKey = 'cardSummaryKey/35'; @@ -62,7 +72,7 @@ describe('CardSummary', () => { expect(vm.store.filter).toEqual({ key: newKey }); }); - it('should return true if route path ends with /summary', () => { + it('should return true if route path ends with /summary' , () => { expect(vm.isSummary).toBe(true); }); -}); +}); \ No newline at end of file diff --git a/src/composables/__tests__/useArrayData.spec.js b/src/composables/__tests__/useArrayData.spec.js index a610ba9eb..d4c5d0949 100644 --- a/src/composables/__tests__/useArrayData.spec.js +++ b/src/composables/__tests__/useArrayData.spec.js @@ -16,7 +16,7 @@ describe('useArrayData', () => { vi.clearAllMocks(); }); - it('should fetch and replace url with new params', async () => { + it('should fetch and repalce url with new params', async () => { vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] }); const arrayData = useArrayData('ArrayData', { url: 'mockUrl' }); @@ -33,11 +33,11 @@ describe('useArrayData', () => { }); expect(routerReplace.path).toEqual('mockSection/list'); expect(JSON.parse(routerReplace.query.params)).toEqual( - expect.objectContaining(params), + expect.objectContaining(params) ); }); - it('should get data and send new URL without keeping parameters, if there is only one record', async () => { + it('Should get data and send new URL without keeping parameters, if there is only one record', async () => { vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }] }); const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} }); @@ -56,7 +56,7 @@ describe('useArrayData', () => { expect(routerPush.query).toBeUndefined(); }); - it('should get data and send new URL keeping parameters, if you have more than one record', async () => { + it('Should get data and send new URL keeping parameters, if you have more than one record', async () => { vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }] }); vi.spyOn(vueRouter, 'useRoute').mockReturnValue({ @@ -95,25 +95,4 @@ describe('useArrayData', () => { expect(routerPush.path).toEqual('mockName/'); expect(routerPush.query.params).toBeDefined(); }); - - it('should return one record', async () => { - vi.spyOn(axios, 'get').mockReturnValueOnce({ - data: [ - { id: 1, name: 'Entity 1' }, - { id: 2, name: 'Entity 2' }, - ], - }); - const arrayData = useArrayData('ArrayData', { url: 'mockUrl', oneRecord: true }); - await arrayData.fetch({}); - - expect(arrayData.store.data).toEqual({ id: 1, name: 'Entity 1' }); - }); - - it('should handle empty data gracefully if has to return one record', async () => { - vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] }); - const arrayData = useArrayData('ArrayData', { url: 'mockUrl', oneRecord: true }); - await arrayData.fetch({}); - - expect(arrayData.store.data).toBeUndefined(); - }); }); diff --git a/src/composables/checkEntryLock.js b/src/composables/checkEntryLock.js deleted file mode 100644 index f964dea27..000000000 --- a/src/composables/checkEntryLock.js +++ /dev/null @@ -1,65 +0,0 @@ -import { useQuasar } from 'quasar'; -import { useI18n } from 'vue-i18n'; -import { useRouter } from 'vue-router'; -import axios from 'axios'; -import VnConfirm from 'components/ui/VnConfirm.vue'; - -export async function checkEntryLock(entryFk, userFk) { - const { t } = useI18n(); - const quasar = useQuasar(); - const { push } = useRouter(); - const { data } = await axios.get(`Entries/${entryFk}`, { - params: { - filter: JSON.stringify({ - fields: ['id', 'locked', 'lockerUserFk'], - include: { relation: 'user', scope: { fields: ['id', 'nickname'] } }, - }), - }, - }); - const entryConfig = await axios.get('EntryConfigs/findOne'); - - if (data?.lockerUserFk && data?.locked) { - const now = new Date(Date.vnNow()).getTime(); - const lockedTime = new Date(data.locked).getTime(); - const timeDiff = (now - lockedTime) / 1000; - const isMaxTimeLockExceeded = entryConfig.data.maxLockTime > timeDiff; - - if (data?.lockerUserFk !== userFk && isMaxTimeLockExceeded) { - quasar - .dialog({ - component: VnConfirm, - componentProps: { - 'data-cy': 'entry-lock-confirm', - title: t('entry.lock.title'), - message: t('entry.lock.message', { - userName: data?.user?.nickname, - time: timeDiff / 60, - }), - }, - }) - .onOk( - async () => - await axios.patch(`Entries/${entryFk}`, { - locked: Date.vnNow(), - lockerUserFk: userFk, - }), - ) - .onCancel(() => { - push({ path: `summary` }); - }); - } - } else { - await axios - .patch(`Entries/${entryFk}`, { - locked: Date.vnNow(), - lockerUserFk: userFk, - }) - .then( - quasar.notify({ - message: t('entry.lock.success'), - color: 'positive', - group: false, - }), - ); - } -} diff --git a/src/composables/getColAlign.js b/src/composables/getColAlign.js deleted file mode 100644 index a930fd7d8..000000000 --- a/src/composables/getColAlign.js +++ /dev/null @@ -1,22 +0,0 @@ -export function getColAlign(col) { - let align; - switch (col.component) { - case 'time': - case 'date': - case 'select': - align = 'left'; - break; - case 'number': - align = 'right'; - break; - case 'checkbox': - align = 'center'; - break; - default: - align = col?.align; - } - - if (/^is[A-Z]/.test(col.name) || /^has[A-Z]/.test(col.name)) align = 'center'; - - return 'text-' + (align ?? 'center'); -} diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js index fcc61972a..bd3cecf08 100644 --- a/src/composables/useArrayData.js +++ b/src/composables/useArrayData.js @@ -57,7 +57,6 @@ export function useArrayData(key, userOptions) { 'navigate', 'mapKey', 'keepData', - 'oneRecord', ]; if (typeof userOptions === 'object') { for (const option in userOptions) { @@ -113,11 +112,7 @@ export function useArrayData(key, userOptions) { store.isLoading = false; canceller = null; - processData(response.data, { - map: !!store.mapKey, - append, - oneRecord: store.oneRecord, - }); + processData(response.data, { map: !!store.mapKey, append }); return response; } @@ -319,11 +314,7 @@ export function useArrayData(key, userOptions) { return { params, limit }; } - function processData(data, { map = true, append = true, oneRecord = false }) { - if (oneRecord) { - store.data = Array.isArray(data) ? data[0] : data; - return; - } + function processData(data, { map = true, append = true }) { if (!append) { store.data = []; store.map = new Map(); diff --git a/src/composables/useRole.js b/src/composables/useRole.js index ff54b409c..3ec65dd0a 100644 --- a/src/composables/useRole.js +++ b/src/composables/useRole.js @@ -27,15 +27,6 @@ export function useRole() { return false; } - function likeAny(roles) { - const roleStore = state.getRoles(); - for (const role of roles) { - if (!roleStore.value.findIndex((rs) => rs.startsWith(role)) !== -1) - return true; - } - - return false; - } function isEmployee() { return hasAny(['employee']); } @@ -44,7 +35,6 @@ export function useRole() { isEmployee, fetch, hasAny, - likeAny, state, }; } diff --git a/src/css/app.scss b/src/css/app.scss index 0c5dc97fa..7296b079f 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -21,10 +21,7 @@ body.body--light { .q-header .q-toolbar { color: var(--vn-text-color); } - - --vn-color-negative: $negative; } - body.body--dark { --vn-header-color: #5d5d5d; --vn-page-color: #222; @@ -40,8 +37,6 @@ body.body--dark { --vn-text-color-contrast: black; background-color: var(--vn-page-color); - - --vn-color-negative: $negative; } a { @@ -80,6 +75,7 @@ a { text-decoration: underline; } +// Removes chrome autofill background input:-webkit-autofill, select:-webkit-autofill { color: var(--vn-text-color); @@ -153,6 +149,11 @@ select:-webkit-autofill { cursor: pointer; } +.vn-table-separation-row { + height: 16px !important; + background-color: var(--vn-section-color) !important; +} + /* Estilo para el asterisco en campos requeridos */ .q-field.required .q-field__label:after { content: ' *'; @@ -211,10 +212,6 @@ select:-webkit-autofill { justify-content: center; } -.q-card__section[dense] { - padding: 0; -} - input[type='number'] { -moz-appearance: textfield; } @@ -229,12 +226,10 @@ input::-webkit-inner-spin-button { max-width: 100%; } -.remove-bg { - filter: brightness(1.1); - mix-blend-mode: multiply; -} - .q-table__container { + /* ===== Scrollbar CSS ===== / + / Firefox */ + * { scrollbar-width: auto; scrollbar-color: var(--vn-label-color) transparent; @@ -275,6 +270,8 @@ input::-webkit-inner-spin-button { font-size: 11pt; } td { + font-size: 11pt; + border-top: 1px solid var(--vn-page-color); border-collapse: collapse; } } @@ -318,6 +315,9 @@ input::-webkit-inner-spin-button { max-width: fit-content; } +.row > .column:has(.q-checkbox) { + max-width: fit-content; +} .q-field__inner { .q-field__control { min-height: auto !important; diff --git a/src/css/quasar.variables.scss b/src/css/quasar.variables.scss index 22c6d2b56..d6e992437 100644 --- a/src/css/quasar.variables.scss +++ b/src/css/quasar.variables.scss @@ -13,7 +13,7 @@ // Tip: Use the "Theme Builder" on Quasar's documentation website. // Tip: to add new colors https://quasar.dev/style/color-palette/#adding-your-own-colors $primary: #ec8916; -$secondary: #89be34; +$secondary: $primary; $positive: #c8e484; $negative: #fb5252; $info: #84d0e2; @@ -30,9 +30,7 @@ $color-spacer: #7979794d; $border-thin-light: 1px solid $color-spacer-light; $primary-light: #f5b351; $dark-shadow-color: black; -$layout-shadow-dark: - 0 0 10px 2px #00000033, - 0 0px 10px #0000003d; +$layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d; $spacing-md: 16px; $color-font-secondary: #777; $width-xs: 400px; diff --git a/src/filters/toDate.js b/src/filters/toDate.js index 002797af5..8fe8f3836 100644 --- a/src/filters/toDate.js +++ b/src/filters/toDate.js @@ -3,8 +3,6 @@ import { useI18n } from 'vue-i18n'; export default function (value, options = {}) { if (!value) return; - if (!isValidDate(value)) return null; - if (!options.dateStyle && !options.timeStyle) { options.day = '2-digit'; options.month = '2-digit'; @@ -12,12 +10,7 @@ export default function (value, options = {}) { } const { locale } = useI18n(); - const newDate = new Date(value); + const date = new Date(value); - return new Intl.DateTimeFormat(locale.value, options).format(newDate); -} -// handle 0000-00-00 -function isValidDate(date) { - const parsedDate = new Date(date); - return parsedDate instanceof Date && !isNaN(parsedDate.getTime()); + return new Intl.DateTimeFormat(locale.value, options).format(date); } diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 9a60e9da1..7d0f3e0b2 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -33,7 +33,6 @@ globals: reset: Reset close: Close cancel: Cancel - isSaveAndContinue: Save and continue clone: Clone confirm: Confirm assign: Assign @@ -157,7 +156,6 @@ globals: changeState: Change state raid: 'Raid {daysInForward} days' isVies: Vies - noData: No data available pageTitles: logIn: Login addressEdit: Update address @@ -170,7 +168,6 @@ globals: workCenters: Work centers modes: Modes zones: Zones - negative: Negative zonesList: List deliveryDays: Delivery days upcomingDeliveries: Upcoming deliveries @@ -178,7 +175,6 @@ globals: alias: Alias aliasUsers: Users subRoles: Subroles - myAccount: Mi cuenta inheritedRoles: Inherited Roles customers: Customers customerCreate: New customer @@ -337,13 +333,10 @@ globals: wasteRecalc: Waste recaclulate operator: Operator parking: Parking - vehicleList: Vehicles - vehicle: Vehicle unsavedPopup: title: Unsaved changes will be lost subtitle: Are you sure exit without saving? params: - description: Description clientFk: Client id salesPersonFk: Sales person warehouseFk: Warehouse @@ -366,13 +359,7 @@ globals: correctingFk: Rectificative daysOnward: Days onward countryFk: Country - countryCodeFk: Country companyFk: Company - model: Model - fuel: Fuel - active: Active - inactive: Inactive - deliveryPoint: Delivery point errors: statusUnauthorized: Access denied statusInternalServerError: An internal server error has ocurred @@ -411,106 +398,6 @@ cau: subtitle: By sending this ticket, all the data related to the error, the section, the user, etc., are already sent. inputLabel: Explain why this error should not appear askPrivileges: Ask for privileges -entry: - list: - newEntry: New entry - tableVisibleColumns: - isExcludedFromAvailable: Exclude from inventory - isOrdered: Ordered - isConfirmed: Ready to label - isReceived: Received - isRaid: Raid - landed: Date - supplierFk: Supplier - reference: Ref/Alb/Guide - invoiceNumber: Invoice - agencyModeId: Agency - isBooked: Booked - companyFk: Company - evaNotes: Notes - warehouseOutFk: Origin - warehouseInFk: Destiny - entryTypeDescription: Entry type - invoiceAmount: Import - travelFk: Travel - summary: - invoiceAmount: Amount - commission: Commission - currency: Currency - invoiceNumber: Invoice number - ordered: Ordered - booked: Booked - excludedFromAvailable: Inventory - travelReference: Reference - travelAgency: Agency - travelShipped: Shipped - travelDelivered: Delivered - travelLanded: Landed - travelReceived: Received - buys: Buys - stickers: Stickers - package: Package - packing: Pack. - grouping: Group. - buyingValue: Buying value - import: Import - pvp: PVP - basicData: - travel: Travel - currency: Currency - commission: Commission - observation: Observation - booked: Booked - excludedFromAvailable: Inventory - buys: - observations: Observations - packagingFk: Box - color: Color - printedStickers: Printed stickers - notes: - observationType: Observation type - latestBuys: - tableVisibleColumns: - image: Picture - itemFk: Item ID - weightByPiece: Weight/Piece - isActive: Active - family: Family - entryFk: Entry - freightValue: Freight value - comissionValue: Commission value - packageValue: Package value - isIgnored: Is ignored - price2: Grouping - price3: Packing - minPrice: Min - ektFk: Ekt - packingOut: Package out - landing: Landing - isExcludedFromAvailable: Exclude from inventory - isRaid: Raid - invoiceNumber: Invoice - reference: Ref/Alb/Guide - params: - isExcludedFromAvailable: Excluir del inventario - isOrdered: Pedida - isConfirmed: Lista para etiquetar - isReceived: Recibida - isRaid: Redada - landed: Fecha - supplierFk: Proveedor - invoiceNumber: Nº Factura - reference: Ref/Alb/Guía - agencyModeId: Agencia - isBooked: Asentado - companyFk: Empresa - travelFk: Envio - evaNotes: Notas - warehouseOutFk: Origen - warehouseInFk: Destino - entryTypeDescription: Tipo entrada - invoiceAmount: Importe - dated: Fecha ticket: params: ticketFk: Ticket ID @@ -740,8 +627,6 @@ wagon: name: Name supplier: - search: Search supplier - searchInfo: Search supplier by id or name list: payMethod: Pay method account: Account @@ -831,8 +716,6 @@ travel: CloneTravelAndEntries: Clone travel and his entries deleteTravel: Delete travel AddEntry: Add entry - availabled: Availabled - availabledHour: Availabled hour thermographs: Thermographs hb: HB basicData: diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 846c442ea..7ca9e4b4c 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -33,11 +33,9 @@ globals: reset: Restaurar close: Cerrar cancel: Cancelar - isSaveAndContinue: Guardar y continuar clone: Clonar confirm: Confirmar assign: Asignar - replace: Sustituir back: Volver yes: Si no: No @@ -50,7 +48,6 @@ globals: rowRemoved: Fila eliminada pleaseWait: Por favor espera... noPinnedModules: No has fijado ningún módulo - split: Split enterToConfirm: Pulsa Enter para confirmar summary: basicData: Datos básicos @@ -59,8 +56,8 @@ globals: today: Hoy yesterday: Ayer dateFormat: es-ES - noSelectedRows: No tienes ninguna línea seleccionada microsip: Abrir en MicroSIP + noSelectedRows: No tienes ninguna línea seleccionada downloadCSVSuccess: Descarga de CSV exitosa reference: Referencia agency: Agencia @@ -80,10 +77,8 @@ globals: requiredField: Campo obligatorio class: clase type: Tipo - reason: Motivo - removeSelection: Eliminar selección + reason: motivo noResults: Sin resultados - results: resultados system: Sistema notificationSent: Notificación enviada warehouse: Almacén @@ -161,7 +156,6 @@ globals: changeState: Cambiar estado raid: 'Redada {daysInForward} días' isVies: Vies - noData: Datos no disponibles pageTitles: logIn: Inicio de sesión addressEdit: Modificar consignatario @@ -173,7 +167,6 @@ globals: agency: Agencia workCenters: Centros de trabajo modes: Modos - negative: Tickets negativos zones: Zonas zonesList: Listado deliveryDays: Días de entrega @@ -294,9 +287,9 @@ globals: buyRequest: Peticiones de compra wasteBreakdown: Deglose de mermas itemCreate: Nuevo artículo - tax: IVA - botanical: Botánico - barcode: Código de barras + tax: 'IVA' + botanical: 'Botánico' + barcode: 'Código de barras' itemTypeCreate: Nueva familia family: Familia lastEntries: Últimas entradas @@ -340,13 +333,10 @@ globals: wasteRecalc: Recalcular mermas operator: Operario parking: Parking - vehicleList: Vehículos - vehicle: Vehículo unsavedPopup: title: Los cambios que no haya guardado se perderán subtitle: ¿Seguro que quiere salir sin guardar? params: - description: Descripción clientFk: Id cliente salesPersonFk: Comercial warehouseFk: Almacén @@ -360,14 +350,13 @@ globals: from: Desde to: Hasta supplierFk: Proveedor - supplierRef: Nº factura + supplierRef: Ref. proveedor serial: Serie amount: Importe awbCode: AWB daysOnward: Días adelante packing: ITP countryFk: País - countryCodeFk: País companyFk: Empresa errors: statusUnauthorized: Acceso denegado @@ -405,87 +394,6 @@ cau: subtitle: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc inputLabel: Explique el motivo por el que no deberia aparecer este fallo askPrivileges: Solicitar permisos -entry: - list: - newEntry: Nueva entrada - tableVisibleColumns: - isExcludedFromAvailable: Excluir del inventario - isOrdered: Pedida - isConfirmed: Lista para etiquetar - isReceived: Recibida - isRaid: Redada - landed: Fecha - supplierFk: Proveedor - invoiceNumber: Nº Factura - reference: Ref/Alb/Guía - agencyModeId: Agencia - isBooked: Asentado - companyFk: Empresa - travelFk: Envio - evaNotes: Notas - warehouseOutFk: Origen - warehouseInFk: Destino - entryTypeDescription: Tipo entrada - invoiceAmount: Importe - summary: - invoiceAmount: Importe - commission: Comisión - currency: Moneda - invoiceNumber: Núm. factura - ordered: Pedida - booked: Contabilizada - excludedFromAvailable: Inventario - travelReference: Referencia - travelAgency: Agencia - travelShipped: F. envio - travelWarehouseOut: Alm. salida - travelDelivered: Enviada - travelLanded: F. entrega - travelReceived: Recibida - buys: Compras - stickers: Etiquetas - package: Embalaje - packing: Pack. - grouping: Group. - buyingValue: Coste - import: Importe - pvp: PVP - basicData: - travel: Envío - currency: Moneda - observation: Observación - commission: Comisión - booked: Asentado - excludedFromAvailable: Inventario - buys: - observations: Observaciónes - packagingFk: Embalaje - color: Color - printedStickers: Etiquetas impresas - notes: - observationType: Tipo de observación - latestBuys: - tableVisibleColumns: - image: Foto - itemFk: Id Artículo - weightByPiece: Peso (gramos)/tallo - isActive: Activo - family: Familia - entryFk: Entrada - freightValue: Porte - comissionValue: Comisión - packageValue: Embalaje - isIgnored: Ignorado - price2: Grouping - price3: Packing - minPrice: Min - ektFk: Ekt - packingOut: Embalaje envíos - landing: Llegada - isExcludedFromAvailable: Excluir del inventario - isRaid: Redada - invoiceNumber: Nº Factura - reference: Ref/Alb/Guía ticket: params: ticketFk: ID de ticket @@ -499,38 +407,6 @@ ticket: freightItemName: Nombre packageItemName: Embalaje longName: Descripción - pageTitles: - tickets: Tickets - list: Listado - ticketCreate: Nuevo ticket - summary: Resumen - basicData: Datos básicos - boxing: Encajado - sms: Sms - notes: Notas - sale: Lineas del pedido - dms: Gestión documental - negative: Tickets negativos - 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: - nickname: Alias - state: Estado - shipped: Enviado - landed: Entregado - salesPerson: Comercial - total: Total card: customerId: ID cliente customerCard: Ficha del cliente @@ -577,48 +453,6 @@ ticket: consigneeStreet: Dirección create: address: Dirección -invoiceOut: - card: - issued: Fecha emisión - customerCard: Ficha del cliente - ticketList: Listado de tickets - summary: - issued: Fecha - dued: Fecha límite - booked: Contabilizada - taxBreakdown: Desglose impositivo - taxableBase: Base imp. - rate: Tarifa - fee: Cuota - tickets: Tickets - totalWithVat: Importe - globalInvoices: - errors: - chooseValidClient: Selecciona un cliente válido - chooseValidCompany: Selecciona una empresa válida - chooseValidPrinter: Selecciona una impresora válida - chooseValidSerialType: Selecciona una tipo de serie válida - fillDates: La fecha de la factura y la fecha máxima deben estar completas - invoiceDateLessThanMaxDate: La fecha de la factura no puede ser menor que la fecha máxima - invoiceWithFutureDate: Existe una factura con una fecha futura - noTicketsToInvoice: No existen tickets para facturar - criticalInvoiceError: Error crítico en la facturación proceso detenido - invalidSerialTypeForAll: El tipo de serie debe ser global cuando se facturan todos los clientes - table: - addressId: Id dirección - streetAddress: Dirección fiscal - statusCard: - percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}' - pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs' - negativeBases: - clientId: Id cliente - base: Base - active: Activo - hasToInvoice: Facturar - verifiedData: Datos comprobados - comercial: Comercial - errors: - downloadCsvFailed: Error al descargar CSV order: field: salesPersonFk: Comercial @@ -629,34 +463,15 @@ order: list: newOrder: Nuevo Pedido summary: - basket: Cesta - notConfirmed: No confirmada - created: Creado - createdFrom: Creado desde - address: Dirección - total: Total - vat: IVA - state: Estado - alias: Alias - items: Artículos - orderTicketList: Tickets del pedido - amount: Monto - confirm: Confirmar - confirmLines: Confirmar lineas -shelving: - list: - parking: Parking - priority: Prioridad - newShelving: Nuevo Carro - summary: - recyclable: Reciclable -parking: - pickingOrder: Orden de recogida - row: Fila - column: Columna - searchBar: - info: Puedes buscar por código de parking - label: Buscar parking... + issued: Fecha + dued: Fecha límite + booked: Contabilizada + taxBreakdown: Desglose impositivo + taxableBase: Base imp. + rate: Tarifa + fee: Cuota + tickets: Tickets + totalWithVat: Importe department: chat: Chat bossDepartment: Jefe de departamento @@ -817,8 +632,8 @@ wagon: volumeNotEmpty: El volumen no puede estar vacío typeNotEmpty: El tipo no puede estar vacío maxTrays: Has alcanzado el número máximo de bandejas - minHeightBetweenTrays: La distancia mínima entre bandejas es - maxWagonHeight: La altura máxima del vagón es + minHeightBetweenTrays: 'La distancia mínima entre bandejas es ' + maxWagonHeight: 'La altura máxima del vagón es ' uncompleteTrays: Hay bandejas sin completar params: label: Etiqueta @@ -826,8 +641,6 @@ wagon: volume: Volumen name: Nombre supplier: - search: Buscar proveedor - searchInfo: Buscar proveedor por id o nombre list: payMethod: Método de pago account: Cuenta @@ -918,8 +731,6 @@ travel: deleteTravel: Eliminar envío AddEntry: Añadir entrada thermographs: Termógrafos - availabled: F. Disponible - availabledHour: Hora Disponible hb: HB basicData: daysInForward: Desplazamiento automatico (redada) @@ -968,7 +779,7 @@ components: cardDescriptor: mainList: Listado principal summary: Resumen - moreOptions: Más opciones + moreOptions: 'Más opciones' leftMenu: addToPinned: Añadir a fijados removeFromPinned: Eliminar de fijados diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 3ad1c79bc..2a84e5aa1 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -2,7 +2,7 @@ import Navbar from 'src/components/NavBar.vue'; </script> <template> - <QLayout view="hHh LpR fFf"> + <QLayout view="hHh LpR fFf" v-shortcut> <Navbar /> <RouterView></RouterView> <QFooter v-if="$q.platform.is.mobile"></QFooter> diff --git a/src/layouts/OutLayout.vue b/src/layouts/OutLayout.vue index eba57c198..4ccc6bf9e 100644 --- a/src/layouts/OutLayout.vue +++ b/src/layouts/OutLayout.vue @@ -1,12 +1,12 @@ <script setup> import { Dark, Quasar } from 'quasar'; -import { computed, onMounted } from 'vue'; +import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { localeEquivalence } from 'src/i18n/index'; import quasarLang from 'src/utils/quasarLang'; -import { langs } from 'src/boot/defaults/constants.js'; const { t, locale } = useI18n(); + const userLocale = computed({ get() { return locale.value; @@ -28,6 +28,7 @@ const darkMode = computed({ Dark.set(value); }, }); +const langs = ['en', 'es']; </script> <template> diff --git a/src/pages/Account/AccountAliasList.vue b/src/pages/Account/AccountAliasList.vue index 19682286c..f6016fb6c 100644 --- a/src/pages/Account/AccountAliasList.vue +++ b/src/pages/Account/AccountAliasList.vue @@ -3,7 +3,6 @@ import { useI18n } from 'vue-i18n'; import { ref, computed } from 'vue'; import VnTable from 'components/VnTable/VnTable.vue'; import VnSection from 'src/components/common/VnSection.vue'; -import exprBuilder from './Alias/AliasExprBuilder'; const tableRef = ref(); const { t } = useI18n(); @@ -32,6 +31,15 @@ const columns = computed(() => [ create: true, }, ]); + +const exprBuilder = (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { alias: { like: `%${value}%` } }; + } +}; </script> <template> diff --git a/src/pages/Account/AccountExprBuilder.js b/src/pages/Account/AccountExprBuilder.js deleted file mode 100644 index 6497a9d30..000000000 --- a/src/pages/Account/AccountExprBuilder.js +++ /dev/null @@ -1,18 +0,0 @@ -export default (param, value) => { - switch (param) { - case 'search': - return /^\d+$/.test(value) - ? { id: value } - : { - or: [ - { name: { like: `%${value}%` } }, - { nickname: { like: `%${value}%` } }, - ], - }; - case 'name': - case 'nickname': - return { [param]: { like: `%${value}%` } }; - case 'roleFk': - return { [param]: value }; - } -}; diff --git a/src/pages/Account/AccountList.vue b/src/pages/Account/AccountList.vue index 976af1d19..ea8daba0d 100644 --- a/src/pages/Account/AccountList.vue +++ b/src/pages/Account/AccountList.vue @@ -4,16 +4,15 @@ import { computed, ref } from 'vue'; import VnTable from 'components/VnTable/VnTable.vue'; import AccountSummary from './Card/AccountSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import exprBuilder from './AccountExprBuilder.js'; -import filter from './Card/AccountFilter.js'; import VnSection from 'src/components/common/VnSection.vue'; import FetchData from 'src/components/FetchData.vue'; import VnInputPassword from 'src/components/common/VnInputPassword.vue'; const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); -const tableRef = ref(); - +const filter = { + include: { relation: 'role', scope: { fields: ['id', 'name'] } }, +}; const dataKey = 'AccountList'; const roles = ref([]); const columns = computed(() => [ @@ -118,6 +117,25 @@ const columns = computed(() => [ ], }, ]); + +function exprBuilder(param, value) { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { + or: [ + { name: { like: `%${value}%` } }, + { nickname: { like: `%${value}%` } }, + ], + }; + case 'name': + case 'nickname': + return { [param]: { like: `%${value}%` } }; + case 'roleFk': + return { [param]: value }; + } +} </script> <template> <FetchData url="VnRoles" @on-fetch="(data) => (roles = data)" auto-load /> diff --git a/src/pages/Account/Alias/AliasExprBuilder.js b/src/pages/Account/Alias/AliasExprBuilder.js deleted file mode 100644 index f7a5a104c..000000000 --- a/src/pages/Account/Alias/AliasExprBuilder.js +++ /dev/null @@ -1,8 +0,0 @@ -export default (param, value) => { - switch (param) { - case 'search': - return /^\d+$/.test(value) - ? { id: value } - : { alias: { like: `%${value}%` } }; - } -}; diff --git a/src/pages/Account/Alias/Card/AliasCard.vue b/src/pages/Account/Alias/Card/AliasCard.vue index f37bd7d0f..3a814edc0 100644 --- a/src/pages/Account/Alias/Card/AliasCard.vue +++ b/src/pages/Account/Alias/Card/AliasCard.vue @@ -1,13 +1,21 @@ <script setup> +import { useI18n } from 'vue-i18n'; import VnCardBeta from 'components/common/VnCardBeta.vue'; import AliasDescriptor from './AliasDescriptor.vue'; +const { t } = useI18n(); </script> <template> <VnCardBeta data-key="Alias" - url="MailAliases" + base-url="MailAliases" :descriptor="AliasDescriptor" search-data-key="AccountAliasList" + :searchbar-props="{ + url: 'MailAliases', + info: t('mailAlias.searchInfo'), + label: t('mailAlias.search'), + searchUrl: 'table', + }" /> </template> diff --git a/src/pages/Account/Alias/Card/AliasDescriptor.vue b/src/pages/Account/Alias/Card/AliasDescriptor.vue index 671ef7fbc..2e01fad01 100644 --- a/src/pages/Account/Alias/Card/AliasDescriptor.vue +++ b/src/pages/Account/Alias/Card/AliasDescriptor.vue @@ -7,6 +7,7 @@ import { useQuasar } from 'quasar'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; +import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; @@ -28,6 +29,9 @@ const entityId = computed(() => { return $props.id || route.params.id; }); +const data = ref(useCardDescription()); +const setData = (entity) => (data.value = useCardDescription(entity.alias, entity.id)); + const removeAlias = () => { quasar .dialog({ @@ -51,8 +55,11 @@ const removeAlias = () => { <CardDescriptor ref="descriptor" :url="`MailAliases/${entityId}`" - data-key="Alias" - title="alias" + module="Alias" + @on-fetch="setData" + data-key="aliasData" + :title="data.title" + :subtitle="data.subtitle" > <template #menu> <QItem v-ripple clickable @click="removeAlias()"> diff --git a/src/pages/Account/Alias/Card/AliasSummary.vue b/src/pages/Account/Alias/Card/AliasSummary.vue index b4b9abd25..1f76fe7c2 100644 --- a/src/pages/Account/Alias/Card/AliasSummary.vue +++ b/src/pages/Account/Alias/Card/AliasSummary.vue @@ -1,11 +1,13 @@ <script setup> -import { computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; +import { useArrayData } from 'src/composables/useArrayData'; + const route = useRoute(); const { t } = useI18n(); @@ -16,15 +18,20 @@ const $props = defineProps({ }, }); +const { store } = useArrayData('Alias'); +const alias = ref(store.data); const entityId = computed(() => $props.id || route.params.id); </script> <template> - <CardSummary ref="summary" :url="`MailAliases/${entityId}`" data-key="Alias"> - <template #header="{ entity: alias }"> - {{ alias.id }} - {{ alias.alias }} - </template> - <template #body="{ entity: alias }"> + <CardSummary + ref="summary" + :url="`MailAliases/${entityId}`" + @on-fetch="(data) => (alias = data)" + data-key="MailAliasesSummary" + > + <template #header> {{ alias.id }} - {{ alias.alias }} </template> + <template #body> <QCard class="vn-one"> <QCardSection class="q-pa-none"> <router-link diff --git a/src/pages/Account/Card/AccountBasicData.vue b/src/pages/Account/Card/AccountBasicData.vue index 393f9eb80..e6c9da6fe 100644 --- a/src/pages/Account/Card/AccountBasicData.vue +++ b/src/pages/Account/Card/AccountBasicData.vue @@ -1,20 +1,46 @@ <script setup> +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectEnum from 'src/components/common/VnSelectEnum.vue'; import FormModel from 'components/FormModel.vue'; import VnInput from 'src/components/common/VnInput.vue'; +import { ref, watch } from 'vue'; + +const route = useRoute(); +const { t } = useI18n(); +const formModelRef = ref(null); + +const accountFilter = { + where: { id: route.params.id }, + fields: ['id', 'email', 'nickname', 'name', 'accountStateFk', 'packages', 'pickup'], + include: [], +}; + +watch( + () => route.params.id, + () => formModelRef.value.reset() +); </script> <template> - <FormModel :url-update="`VnUsers/${$route.params.id}/update-user`" model="Account"> + <FormModel + ref="formModelRef" + url="VnUsers/preview" + :url-update="`VnUsers/${route.params.id}/update-user`" + :filter="accountFilter" + model="Accounts" + auto-load + @on-data-saved="formModelRef.fetch()" + > <template #form="{ data }"> <div class="q-gutter-y-sm"> - <VnInput v-model="data.name" :label="$t('account.card.nickname')" /> - <VnInput v-model="data.nickname" :label="$t('account.card.alias')" /> - <VnInput v-model="data.email" :label="$t('globals.params.email')" /> + <VnInput v-model="data.name" :label="t('account.card.nickname')" /> + <VnInput v-model="data.nickname" :label="t('account.card.alias')" /> + <VnInput v-model="data.email" :label="t('globals.params.email')" /> <VnSelect url="Languages" v-model="data.lang" - :label="$t('account.card.lang')" + :label="t('account.card.lang')" option-value="code" option-label="code" /> @@ -23,7 +49,7 @@ import VnInput from 'src/components/common/VnInput.vue'; table="user" column="twoFactor" v-model="data.twoFactor" - :label="$t('account.card.twoFactor')" + :label="t('account.card.twoFactor')" option-value="code" option-label="code" /> diff --git a/src/pages/Account/Card/AccountCard.vue b/src/pages/Account/Card/AccountCard.vue index a5037e301..35ff7e732 100644 --- a/src/pages/Account/Card/AccountCard.vue +++ b/src/pages/Account/Card/AccountCard.vue @@ -1,14 +1,8 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import AccountDescriptor from './AccountDescriptor.vue'; -import filter from './AccountFilter.js'; </script> + <template> - <VnCardBeta - url="VnUsers/preview" - :id-in-where="true" - data-key="Account" - :descriptor="AccountDescriptor" - :filter="filter" - /> + <VnCardBeta data-key="AccountId" :descriptor="AccountDescriptor" /> </template> diff --git a/src/pages/Account/Card/AccountDescriptor.vue b/src/pages/Account/Card/AccountDescriptor.vue index 49328fe87..4e5328de6 100644 --- a/src/pages/Account/Card/AccountDescriptor.vue +++ b/src/pages/Account/Card/AccountDescriptor.vue @@ -1,18 +1,36 @@ <script setup> import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; +import useCardDescription from 'src/composables/useCardDescription'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; import VnImg from 'src/components/ui/VnImg.vue'; -import filter from './AccountFilter.js'; import useHasAccount from 'src/composables/useHasAccount.js'; -const $props = defineProps({ id: { type: Number, default: null } }); +const $props = defineProps({ + id: { + type: Number, + required: false, + default: null, + }, +}); const route = useRoute(); -const entityId = computed(() => $props.id || route.params.id); +const { t } = useI18n(); +const entityId = computed(() => { + return $props.id || route.params.id; +}); +const data = ref(useCardDescription()); const hasAccount = ref(); +const setData = (entity) => (data.value = useCardDescription(entity.nickname, entity.id)); + +const filter = { + where: { id: entityId }, + fields: ['id', 'nickname', 'name', 'role'], + include: { relation: 'role', scope: { fields: ['id', 'name'] } }, +}; onMounted(async () => { hasAccount.value = await useHasAccount(entityId.value); @@ -23,9 +41,12 @@ onMounted(async () => { <CardDescriptor ref="descriptor" :url="`VnUsers/preview`" - :filter="{ ...filter, where: { id: entityId } }" - data-key="Account" - title="nickname" + :filter="filter" + module="Account" + @on-fetch="setData" + data-key="AccountId" + :title="data.title" + :subtitle="data.subtitle" > <template #menu> <AccountDescriptorMenu :entity-id="entityId" /> @@ -41,7 +62,7 @@ onMounted(async () => { <QIcon name="vn:claims" /> </div> <div class="text-grey-5" style="opacity: 0.4"> - {{ $t('account.imageNotFound') }} + {{ t('account.imageNotFound') }} </div> </div> </div> @@ -49,8 +70,8 @@ onMounted(async () => { </VnImg> </template> <template #body="{ entity }"> - <VnLv :label="$t('account.card.nickname')" :value="entity.name" /> - <VnLv :label="$t('account.card.role')" :value="entity.role?.name" /> + <VnLv :label="t('account.card.nickname')" :value="entity.name" /> + <VnLv :label="t('account.card.role')" :value="entity.role.name" /> </template> <template #actions="{ entity }"> <QCardActions class="q-gutter-x-md"> @@ -63,7 +84,7 @@ onMounted(async () => { size="sm" class="fill-icon" > - <QTooltip>{{ $t('account.card.deactivated') }}</QTooltip> + <QTooltip>{{ t('account.card.deactivated') }}</QTooltip> </QIcon> <QIcon color="primary" @@ -74,7 +95,7 @@ onMounted(async () => { size="sm" class="fill-icon" > - <QTooltip>{{ $t('account.card.enabled') }}</QTooltip> + <QTooltip>{{ t('account.card.enabled') }}</QTooltip> </QIcon> </QCardActions> </template> diff --git a/src/pages/Account/Card/AccountDescriptorMenu.vue b/src/pages/Account/Card/AccountDescriptorMenu.vue index 30584c61f..961323d3a 100644 --- a/src/pages/Account/Card/AccountDescriptorMenu.vue +++ b/src/pages/Account/Card/AccountDescriptorMenu.vue @@ -12,7 +12,6 @@ import VnInputPassword from 'src/components/common/VnInputPassword.vue'; import VnChangePassword from 'src/components/common/VnChangePassword.vue'; import { useQuasar } from 'quasar'; import { useRouter } from 'vue-router'; -import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const $props = defineProps({ hasAccount: { @@ -30,7 +29,7 @@ const router = useRouter(); const state = useState(); const user = state.getUser(); const { notify } = useQuasar(); -const account = computed(() => useArrayData('Account').store.data[0]); +const account = computed(() => useArrayData('AccountId').store.data[0]); account.value.hasAccount = hasAccount.value; const entityId = computed(() => +route.params.id); const hasitManagementAccess = ref(); @@ -125,14 +124,18 @@ onMounted(() => { :promise="sync" > <template #customHTML> - <VnCheckbox - v-model="shouldSyncPassword" + {{ shouldSyncPassword }} + <QCheckbox :label="t('account.card.actions.sync.checkbox')" - :info="t('account.card.actions.sync.tooltip')" + v-model="shouldSyncPassword" + class="full-width" clearable clear-icon="close" - color="primary" - /> + > + <QIcon style="padding-left: 10px" color="primary" name="info" size="sm"> + <QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip> + </QIcon></QCheckbox + > <VnInputPassword v-if="shouldSyncPassword" :label="t('login.password')" @@ -152,7 +155,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.subtitle'), - () => deleteAccount(), + () => deleteAccount() ) " > @@ -171,7 +174,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.enableAccount.title'), t('account.card.actions.enableAccount.subtitle'), - () => updateStatusAccount(true), + () => updateStatusAccount(true) ) " > @@ -185,7 +188,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.subtitle'), - () => updateStatusAccount(false), + () => updateStatusAccount(false) ) " > @@ -200,7 +203,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.activateUser.title'), t('account.card.actions.activateUser.title'), - () => updateStatusUser(true), + () => updateStatusUser(true) ) " > @@ -214,7 +217,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.deactivateUser.title'), t('account.card.actions.deactivateUser.title'), - () => updateStatusUser(false), + () => updateStatusUser(false) ) " > diff --git a/src/pages/Account/Card/AccountFilter.js b/src/pages/Account/Card/AccountFilter.js deleted file mode 100644 index 017876564..000000000 --- a/src/pages/Account/Card/AccountFilter.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - include: { relation: 'role', scope: { fields: ['id', 'name'] } }, -}; diff --git a/src/pages/Account/Card/AccountMailAlias.vue b/src/pages/Account/Card/AccountMailAlias.vue index 7a060cff1..ef1707cf2 100644 --- a/src/pages/Account/Card/AccountMailAlias.vue +++ b/src/pages/Account/Card/AccountMailAlias.vue @@ -86,7 +86,7 @@ watch( () => route.params.id, () => { getAccountData(); - }, + } ); onMounted(async () => await getAccountData(false)); @@ -130,8 +130,7 @@ onMounted(async () => await getAccountData(false)); openConfirmationModal( t('User will be removed from alias'), t('¿Seguro que quieres continuar?'), - () => - deleteMailAlias(row, rows, rowIndex), + () => deleteMailAlias(row, rows, rowIndex) ) " > @@ -158,7 +157,7 @@ onMounted(async () => await getAccountData(false)); icon="add" color="primary" @click="openCreateMailAliasForm()" - v-shortcut="'+'" + shortcut="+" > <QTooltip>{{ t('warehouses.add') }}</QTooltip> </QBtn> diff --git a/src/pages/Account/Card/AccountSummary.vue b/src/pages/Account/Card/AccountSummary.vue index f7a16e8c3..ca17c7975 100644 --- a/src/pages/Account/Card/AccountSummary.vue +++ b/src/pages/Account/Card/AccountSummary.vue @@ -1,41 +1,58 @@ <script setup> -import { computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; + import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import filter from './AccountFilter.js'; + +import { useArrayData } from 'src/composables/useArrayData'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; -const $props = defineProps({ id: { type: Number, default: 0 } }); - const route = useRoute(); +const { t } = useI18n(); + +const $props = defineProps({ + id: { + type: Number, + default: 0, + }, +}); +const { store } = useArrayData('Account'); +const account = ref(store.data); + const entityId = computed(() => $props.id || route.params.id); +const filter = { + where: { id: entityId }, + fields: ['id', 'nickname', 'name', 'role'], + include: { relation: 'role', scope: { fields: ['id', 'name'] } }, +}; </script> <template> <CardSummary - data-key="Account" - ref="AccountSummary" + data-key="AccountId" url="VnUsers/preview" :filter="filter" + @on-fetch="(data) => (account = data)" > - <template #header="{ entity }">{{ entity.id }} - {{ entity.nickname }}</template> - <template #menu> + <template #header>{{ account.id }} - {{ account.nickname }}</template> + <template #menu=""> <AccountDescriptorMenu :entity-id="entityId" /> </template> - <template #body="{ entity }"> + <template #body> <QCard class="vn-one"> <QCardSection class="q-pa-none"> <router-link :to="{ name: 'AccountBasicData', params: { id: entityId } }" class="header header-link" > - {{ $t('globals.pageTitles.basicData') }} + {{ t('globals.pageTitles.basicData') }} <QIcon name="open_in_new" /> </router-link> </QCardSection> - <VnLv :label="$t('account.card.nickname')" :value="entity.name" /> - <VnLv :label="$t('account.card.role')" :value="entity.role?.name" /> + <VnLv :label="t('account.card.nickname')" :value="account.name" /> + <VnLv :label="t('account.card.role')" :value="account.role.name" /> </QCard> </template> </CardSummary> diff --git a/src/pages/Account/Role/AccountRoles.vue b/src/pages/Account/Role/AccountRoles.vue index 02f5400c6..3c3d6b243 100644 --- a/src/pages/Account/Role/AccountRoles.vue +++ b/src/pages/Account/Role/AccountRoles.vue @@ -5,7 +5,6 @@ import VnTable from 'components/VnTable/VnTable.vue'; import { useRoute } from 'vue-router'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import RoleSummary from './Card/RoleSummary.vue'; -import exprBuilder from './RoleExprBuilder.js'; import VnSection from 'src/components/common/VnSection.vue'; const route = useRoute(); @@ -67,7 +66,24 @@ const columns = computed(() => [ ], }, ]); +const exprBuilder = (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { + or: [ + { name: { like: `%${value}%` } }, + { nickname: { like: `%${value}%` } }, + ], + }; + case 'name': + case 'description': + return { [param]: { like: `%${value}%` } }; + } +}; </script> + <template> <VnSection :data-key="dataKey" diff --git a/src/pages/Account/Role/Card/RoleBasicData.vue b/src/pages/Account/Role/Card/RoleBasicData.vue index de70b0fb6..1de9ff387 100644 --- a/src/pages/Account/Role/Card/RoleBasicData.vue +++ b/src/pages/Account/Role/Card/RoleBasicData.vue @@ -1,16 +1,24 @@ <script setup> +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; +const route = useRoute(); +const { t } = useI18n(); </script> <template> - <FormModel model="Role" auto-load> + <FormModel :url="`VnRoles/${route.params.id}`" model="VnRole" auto-load> <template #form="{ data }"> <VnRow> - <VnInput v-model="data.name" :label="$t('globals.name')" /> + <div class="col"> + <VnInput v-model="data.name" :label="t('globals.name')" /> + </div> </VnRow> <VnRow> - <VnInput v-model="data.description" :label="$t('role.description')" /> + <div class="col"> + <VnInput v-model="data.description" :label="t('role.description')" /> + </div> </VnRow> </template> </FormModel> diff --git a/src/pages/Account/Role/Card/RoleCard.vue b/src/pages/Account/Role/Card/RoleCard.vue index ef5b9db04..7664deca8 100644 --- a/src/pages/Account/Role/Card/RoleCard.vue +++ b/src/pages/Account/Role/Card/RoleCard.vue @@ -3,10 +3,5 @@ import VnCardBeta from 'components/common/VnCardBeta.vue'; import RoleDescriptor from './RoleDescriptor.vue'; </script> <template> - <VnCardBeta - url="VnRoles" - data-key="Role" - :id-in-where="true" - :descriptor="RoleDescriptor" - /> + <VnCardBeta data-key="Role" :descriptor="RoleDescriptor" /> </template> diff --git a/src/pages/Account/Role/Card/RoleDescriptor.vue b/src/pages/Account/Role/Card/RoleDescriptor.vue index 517517af0..0a555346d 100644 --- a/src/pages/Account/Role/Card/RoleDescriptor.vue +++ b/src/pages/Account/Role/Card/RoleDescriptor.vue @@ -1,9 +1,10 @@ <script setup> -import { computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; +import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; const $props = defineProps({ @@ -25,6 +26,11 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); +const data = ref(useCardDescription()); +const setData = (entity) => (data.value = useCardDescription(entity.name, entity.id)); +const filter = { + where: { id: entityId }, +}; const removeRole = async () => { await axios.delete(`VnRoles/${entityId.value}`); notify(t('Role removed'), 'positive'); @@ -33,9 +39,13 @@ const removeRole = async () => { <template> <CardDescriptor - url="VnRoles" - :filter="{ where: { id: entityId } }" + :url="`VnRoles/${entityId}`" + :filter="filter" + module="Role" + @on-fetch="setData" data-key="Role" + :title="data.title" + :subtitle="data.subtitle" :summary="$props.summary" > <template #menu> diff --git a/src/pages/Account/Role/Card/RoleSummary.vue b/src/pages/Account/Role/Card/RoleSummary.vue index 410f90b17..f0daa77fb 100644 --- a/src/pages/Account/Role/Card/RoleSummary.vue +++ b/src/pages/Account/Role/Card/RoleSummary.vue @@ -1,9 +1,10 @@ <script setup> -import { computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; +import { useArrayData } from 'src/composables/useArrayData'; const route = useRoute(); const { t } = useI18n(); @@ -15,18 +16,24 @@ const $props = defineProps({ }, }); +const { store } = useArrayData('Role'); +const role = ref(store.data); const entityId = computed(() => $props.id || route.params.id); +const filter = { + where: { id: entityId }, +}; </script> <template> <CardSummary ref="summary" - url="VnRoles" - :filter="{ where: { id: entityId } }" + :url="`VnRoles/${entityId}`" + :filter="filter" + @on-fetch="(data) => (role = data)" data-key="Role" > - <template #header="{ entity }"> {{ entity.id }} - {{ entity.name }} </template> - <template #body="{ entity }"> + <template #header> {{ role.id }} - {{ role.name }} </template> + <template #body> <QCard class="vn-one"> <QCardSection class="q-pa-none"> <a @@ -37,9 +44,9 @@ const entityId = computed(() => $props.id || route.params.id); <QIcon name="open_in_new" /> </a> </QCardSection> - <VnLv :label="t('role.id')" :value="entity.id" /> - <VnLv :label="t('globals.name')" :value="entity.name" /> - <VnLv :label="t('role.description')" :value="entity.description" /> + <VnLv :label="t('role.id')" :value="role.id" /> + <VnLv :label="t('globals.name')" :value="role.name" /> + <VnLv :label="t('role.description')" :value="role.description" /> </QCard> </template> </CardSummary> diff --git a/src/pages/Account/Role/Card/SubRoles.vue b/src/pages/Account/Role/Card/SubRoles.vue index 99cf5e8f0..0077f12b0 100644 --- a/src/pages/Account/Role/Card/SubRoles.vue +++ b/src/pages/Account/Role/Card/SubRoles.vue @@ -63,7 +63,7 @@ watch( store.url = urlPath.value; store.filter = filter.value; fetchSubRoles(); - }, + } ); const fetchSubRoles = () => paginateRef.value.fetch(); @@ -109,7 +109,7 @@ const redirectToRoleSummary = (id) => openConfirmationModal( t('El rol va a ser eliminado'), t('¿Seguro que quieres continuar?'), - () => deleteSubRole(row, rows, rowIndex), + () => deleteSubRole(row, rows, rowIndex) ) " > @@ -131,7 +131,7 @@ const redirectToRoleSummary = (id) => <QBtn fab icon="add" - v-shortcut="'+'" + shortcut="+" color="primary" @click="openCreateSubRoleForm()" > diff --git a/src/pages/Account/Role/RoleExprBuilder.js b/src/pages/Account/Role/RoleExprBuilder.js deleted file mode 100644 index cc4fab399..000000000 --- a/src/pages/Account/Role/RoleExprBuilder.js +++ /dev/null @@ -1,16 +0,0 @@ -export default (param, value) => { - switch (param) { - case 'search': - return /^\d+$/.test(value) - ? { id: value } - : { - or: [ - { name: { like: `%${value}%` } }, - { nickname: { like: `%${value}%` } }, - ], - }; - case 'name': - case 'description': - return { [param]: { like: `%${value}%` } }; - } -}; diff --git a/src/pages/Claim/Card/ClaimBasicData.vue b/src/pages/Claim/Card/ClaimBasicData.vue index 67034da1a..63b0b7c0d 100644 --- a/src/pages/Claim/Card/ClaimBasicData.vue +++ b/src/pages/Claim/Card/ClaimBasicData.vue @@ -28,6 +28,7 @@ const workersOptions = ref([]); model="Claim" :url-update="`Claims/updateClaim/${route.params.id}`" auto-load + :reload="true" > <template #form="{ data, validate }"> <VnRow> diff --git a/src/pages/Claim/Card/ClaimCard.vue b/src/pages/Claim/Card/ClaimCard.vue index 05f3b53a8..e1e000815 100644 --- a/src/pages/Claim/Card/ClaimCard.vue +++ b/src/pages/Claim/Card/ClaimCard.vue @@ -4,11 +4,10 @@ import ClaimDescriptor from './ClaimDescriptor.vue'; import filter from './ClaimFilter.js'; </script> <template> - <VnCardBeta - data-key="Claim" - url="Claims" - :descriptor="ClaimDescriptor" - search-data-key="ClaimList" + <VnCardBeta + data-key="Claim" + base-url="Claims" + :descriptor="ClaimDescriptor" :filter="filter" /> </template> diff --git a/src/pages/Claim/Card/ClaimDescriptor.vue b/src/pages/Claim/Card/ClaimDescriptor.vue index 4551c58fe..02b63dd8e 100644 --- a/src/pages/Claim/Card/ClaimDescriptor.vue +++ b/src/pages/Claim/Card/ClaimDescriptor.vue @@ -3,10 +3,12 @@ import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { toDateHourMinSec, toPercentage } from 'src/filters'; +import { useState } from 'src/composables/useState'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; +import useCardDescription from 'src/composables/useCardDescription'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import { getUrl } from 'src/composables/getUrl'; import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; @@ -21,6 +23,7 @@ const $props = defineProps({ }); const route = useRoute(); +const state = useState(); const { t } = useI18n(); const salixUrl = ref(); const entityId = computed(() => { @@ -36,7 +39,12 @@ const STATE_COLOR = { function stateColor(code) { return STATE_COLOR[code]; } - +const data = ref(useCardDescription()); +const setData = (entity) => { + if (!entity) return; + data.value = useCardDescription(entity?.client?.name, entity.id); + state.set('ClaimDescriptor', entity); +}; onMounted(async () => { salixUrl.value = await getUrl(''); }); @@ -46,7 +54,9 @@ onMounted(async () => { <CardDescriptor :url="`Claims/${entityId}`" :filter="filter" + module="Claim" title="client.name" + @on-fetch="setData" data-key="Claim" > <template #menu="{ entity }"> @@ -85,7 +95,7 @@ onMounted(async () => { /> </template> </VnLv> - <VnLv v-if="entity.ticket?.zone?.id" :label="t('claim.zone')"> + <VnLv :label="t('claim.zone')"> <template #value> <span class="link"> {{ entity.ticket?.zone?.name }} @@ -97,10 +107,11 @@ onMounted(async () => { :label="t('claim.province')" :value="entity.ticket?.address?.province?.name" /> - <VnLv v-if="entity.ticketFk" :label="t('claim.ticketId')"> + <VnLv :label="t('claim.ticketId')"> <template #value> <span class="link"> {{ entity.ticketFk }} + <TicketDescriptorProxy :id="entity.ticketFk" /> </span> </template> diff --git a/src/pages/Claim/Card/ClaimLines.vue b/src/pages/Claim/Card/ClaimLines.vue index dee03b95d..33fadd020 100644 --- a/src/pages/Claim/Card/ClaimLines.vue +++ b/src/pages/Claim/Card/ClaimLines.vue @@ -317,13 +317,7 @@ async function saveWhenHasChanges() { </div> <QPageSticky position="bottom-right" :offset="[25, 25]"> - <QBtn - fab - color="primary" - v-shortcut="'+'" - icon="add" - @click="showImportDialog()" - /> + <QBtn fab color="primary" shortcut="+" icon="add" @click="showImportDialog()" /> </QPageSticky> </template> diff --git a/src/pages/Claim/Card/ClaimNotes.vue b/src/pages/Claim/Card/ClaimNotes.vue index cc6e33779..134ee33ab 100644 --- a/src/pages/Claim/Card/ClaimNotes.vue +++ b/src/pages/Claim/Card/ClaimNotes.vue @@ -1,5 +1,5 @@ <script setup> -import { computed, useAttrs } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useState } from 'src/composables/useState'; import VnNotes from 'src/components/ui/VnNotes.vue'; @@ -7,7 +7,6 @@ import VnNotes from 'src/components/ui/VnNotes.vue'; const route = useRoute(); const state = useState(); const user = state.getUser(); -const $attrs = useAttrs(); const $props = defineProps({ id: { type: [Number, String], default: null }, diff --git a/src/pages/Claim/Card/ClaimPhoto.vue b/src/pages/Claim/Card/ClaimPhoto.vue index d4acc9bbe..d4321d8eb 100644 --- a/src/pages/Claim/Card/ClaimPhoto.vue +++ b/src/pages/Claim/Card/ClaimPhoto.vue @@ -61,7 +61,7 @@ watch( () => { claimDmsFilter.value.where.id = router.currentRoute.value.params.id; claimDmsRef.value.fetch(); - }, + } ); function openDialog(dmsId) { @@ -248,7 +248,7 @@ function onDrag() { <QBtn fab @click="inputFile.nativeEl.click()" - v-shortcut="'+'" + shortcut="+" icon="add" color="primary" > diff --git a/src/pages/Claim/ClaimList.vue b/src/pages/Claim/ClaimList.vue index 41d0c5598..63fd035da 100644 --- a/src/pages/Claim/ClaimList.vue +++ b/src/pages/Claim/ClaimList.vue @@ -132,7 +132,7 @@ const STATE_COLOR = { prefix="claim" :array-data-props="{ url: 'Claims/filter', - order: 'cs.priority ASC, created ASC', + order: ['cs.priority ASC', 'created ASC'], }" > <template #advanced-menu> diff --git a/src/pages/Customer/Card/CustomerAddress.vue b/src/pages/Customer/Card/CustomerAddress.vue index f1799d0cc..1b0d1dde1 100644 --- a/src/pages/Customer/Card/CustomerAddress.vue +++ b/src/pages/Customer/Card/CustomerAddress.vue @@ -61,7 +61,7 @@ watch( (newValue) => { if (!newValue) return; getClientData(newValue); - }, + } ); const getClientData = async (id) => { @@ -137,7 +137,7 @@ const toCustomerAddressEdit = (addressId) => { <QIcon :style="{ 'font-variation-settings': `'FILL' ${isDefaultAddress( - item, + item )}`, }" color="primary" @@ -150,7 +150,7 @@ const toCustomerAddressEdit = (addressId) => { t( isDefaultAddress(item) ? 'Default address' - : 'Set as default', + : 'Set as default' ) }} </QTooltip> @@ -216,7 +216,7 @@ const toCustomerAddressEdit = (addressId) => { color="primary" fab icon="add" - v-shortcut="'+'" + shortcut="+" /> <QTooltip> {{ t('New consignee') }} diff --git a/src/pages/Customer/Card/CustomerBalance.vue b/src/pages/Customer/Card/CustomerBalance.vue index 11db92eab..04ef5f882 100644 --- a/src/pages/Customer/Card/CustomerBalance.vue +++ b/src/pages/Customer/Card/CustomerBalance.vue @@ -158,7 +158,7 @@ const columns = computed(() => [ openConfirmationModal( t('Send compensation'), t('Do you want to report compensation to the client by mail?'), - () => sendEmail(`Receipts/${id}/balance-compensation-email`), + () => sendEmail(`Receipts/${id}/balance-compensation-email`) ), }, ], @@ -291,7 +291,7 @@ const showBalancePdf = ({ id }) => { color="primary" fab icon="add" - v-shortcut="'+'" + shortcut="+" /> <QTooltip> {{ t('New payment') }} diff --git a/src/pages/Customer/Card/CustomerBasicData.vue b/src/pages/Customer/Card/CustomerBasicData.vue index 36ec4763e..e9a349e0b 100644 --- a/src/pages/Customer/Card/CustomerBasicData.vue +++ b/src/pages/Customer/Card/CustomerBasicData.vue @@ -54,10 +54,10 @@ function onBeforeSave(formData, originalData) { auto-load /> <FormModel - :url-update="`Clients/${route.params.id}`" + :url="`Clients/${route.params.id}`" auto-load + model="customer" :mapper="onBeforeSave" - model="Customer" > <template #form="{ data, validate }"> <VnRow> diff --git a/src/pages/Customer/Card/CustomerBillingData.vue b/src/pages/Customer/Card/CustomerBillingData.vue index cc894d01e..f1e78d9e5 100644 --- a/src/pages/Customer/Card/CustomerBillingData.vue +++ b/src/pages/Customer/Card/CustomerBillingData.vue @@ -27,7 +27,7 @@ const getBankEntities = (data, formData) => { </script> <template> - <FormModel :url-update="`Clients/${route.params.id}`" auto-load model="Customer"> + <FormModel :url-update="`Clients/${route.params.id}`" auto-load model="customer"> <template #form="{ data, validate }"> <VnRow> <VnSelect diff --git a/src/pages/Customer/Card/CustomerCard.vue b/src/pages/Customer/Card/CustomerCard.vue index 75fcb98fa..f46884834 100644 --- a/src/pages/Customer/Card/CustomerCard.vue +++ b/src/pages/Customer/Card/CustomerCard.vue @@ -5,8 +5,8 @@ import CustomerDescriptor from './CustomerDescriptor.vue'; <template> <VnCardBeta - data-key="Customer" - :url="`Clients/${$route.params.id}/getCard`" + data-key="Client" + base-url="Clients" :descriptor="CustomerDescriptor" /> </template> diff --git a/src/pages/Customer/Card/CustomerConsumption.vue b/src/pages/Customer/Card/CustomerConsumption.vue index f3949bb32..f0d8dea47 100644 --- a/src/pages/Customer/Card/CustomerConsumption.vue +++ b/src/pages/Customer/Card/CustomerConsumption.vue @@ -61,23 +61,6 @@ const columns = computed(() => [ columnFilter: false, cardVisible: true, }, - { - align: 'left', - name: 'buyerId', - label: t('customer.params.buyerId'), - component: 'select', - attrs: { - url: 'TicketRequests/getItemTypeWorker', - optionLabel: 'nickname', - optionValue: 'id', - - fields: ['id', 'nickname'], - sortBy: ['nickname ASC'], - optionFilter: 'firstName', - }, - cardVisible: false, - visible: false, - }, { name: 'description', align: 'left', @@ -91,7 +74,6 @@ const columns = computed(() => [ name: 'quantity', label: t('globals.quantity'), cardVisible: true, - visible: true, columnFilter: { inWhere: true, }, @@ -137,7 +119,7 @@ const openSendEmailDialog = async () => { openConfirmationModal( t('The consumption report will be sent'), t('Please, confirm'), - () => sendCampaignMetricsEmail({ address: arrayData.store.data.email }), + () => sendCampaignMetricsEmail({ address: arrayData.store.data.email }) ); }; const sendCampaignMetricsEmail = ({ address }) => { @@ -156,11 +138,11 @@ const updateDateParams = (value, params) => { const campaign = campaignList.value.find((c) => c.id === value); if (!campaign) return; - const { dated, scopeDays } = campaign; - const from = new Date(dated); - from.setDate(from.getDate() - scopeDays); - params.from = from; - params.to = dated; + const { dated, previousDays, scopeDays } = campaign; + const _date = new Date(dated); + const [from, to] = dateRange(_date); + params.from = new Date(from.setDate(from.getDate() - previousDays)).toISOString(); + params.to = new Date(to.setDate(to.getDate() + scopeDays)).toISOString(); return params; }; </script> @@ -170,7 +152,7 @@ const updateDateParams = (value, params) => { v-if="campaignList" data-key="CustomerConsumption" url="Clients/consumption" - :order="['itemTypeFk', 'itemName', 'itemSize', 'description']" + :order="['itemTypeFk', 'itemName', 'itemSize', 'description']" :filter="{ where: { clientFk: route.params.id } }" :columns="columns" search-url="consumption" @@ -218,60 +200,29 @@ const updateDateParams = (value, params) => { <div v-if="row.subName" class="subName"> {{ row.subName }} </div> - <FetchedTags :item="row" /> + <FetchedTags :item="row" :max-length="3" /> </template> <template #moreFilterPanel="{ params }"> <div class="column no-wrap flex-center q-gutter-y-md q-mt-xs q-pr-xl"> - <VnSelect - :filled="true" - class="q-px-sm q-pt-none fit" - url="ItemTypes" - v-model="params.typeId" - :label="t('item.list.typeName')" - :fields="['id', 'name', 'categoryFk']" - :include="'category'" - :sortBy="'name ASC'" - dense - > - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel>{{ scope.opt?.name }}</QItemLabel> - <QItemLabel caption>{{ - scope.opt?.category?.name - }}</QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> - <VnSelect - :filled="true" - class="q-px-sm q-pt-none fit" - url="ItemCategories" - v-model="params.categoryId" - :label="t('item.list.category')" - :fields="['id', 'name']" - :sortBy="'name ASC'" - dense - /> <VnSelect v-model="params.campaign" :options="campaignList" :label="t('globals.campaign')" :filled="true" class="q-px-sm q-pt-none fit" - :option-label="(opt) => t(opt.code)" - :fields="['id', 'code', 'dated', 'scopeDays']" - @update:model-value="(data) => updateDateParams(data, params)" dense + option-label="code" + @update:model-value="(data) => updateDateParams(data, params)" > <template #option="scope"> <QItem v-bind="scope.itemProps"> <QItemSection> - <QItemLabel> {{ t(scope.opt?.code) }} </QItemLabel> - <QItemLabel caption> - {{ new Date(scope.opt?.dated).getFullYear() }} - </QItemLabel> + <QItemLabel> + {{ scope.opt?.code }} + {{ + new Date(scope.opt?.dated).getFullYear() + }}</QItemLabel + > </QItemSection> </QItem> </template> @@ -296,21 +247,7 @@ const updateDateParams = (value, params) => { </template> <i18n> -en: - - valentinesDay: Valentine's Day - mothersDay: Mother's Day - allSaints: All Saints' Day - frenchMothersDay: Mother's Day in France es: Enter a new search: Introduce una nueva búsqueda Group by items: Agrupar por artículos - valentinesDay: Día de San Valentín - mothersDay: Día de la Madre - allSaints: Día de Todos los Santos - frenchMothersDay: (Francia) Día de la Madre - Campaign consumption: Consumo campaña - Campaign: Campaña - From: Desde - To: Hasta </i18n> diff --git a/src/pages/Customer/Card/CustomerContacts.vue b/src/pages/Customer/Card/CustomerContacts.vue index d03f71244..c420f650e 100644 --- a/src/pages/Customer/Card/CustomerContacts.vue +++ b/src/pages/Customer/Card/CustomerContacts.vue @@ -62,7 +62,7 @@ const customerContactsRef = ref(null); color="primary" flat icon="add" - v-shortcut="'+'" + shortcut="+" > <QTooltip> {{ t('Add contact') }} diff --git a/src/pages/Customer/Card/CustomerCreditContracts.vue b/src/pages/Customer/Card/CustomerCreditContracts.vue index a49faeb8d..7dc53db72 100644 --- a/src/pages/Customer/Card/CustomerCreditContracts.vue +++ b/src/pages/Customer/Card/CustomerCreditContracts.vue @@ -195,7 +195,7 @@ const updateData = () => { color="primary" fab icon="add" - v-shortcut="'+'" + shortcut="+" /> <QTooltip> {{ t('New contract') }} diff --git a/src/pages/Customer/Card/CustomerDescriptor.vue b/src/pages/Customer/Card/CustomerDescriptor.vue index 89f9d9449..d7a8a59a1 100644 --- a/src/pages/Customer/Card/CustomerDescriptor.vue +++ b/src/pages/Customer/Card/CustomerDescriptor.vue @@ -1,5 +1,5 @@ <script setup> -import { onMounted, ref, computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; @@ -11,15 +11,6 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue'; -import { useState } from 'src/composables/useState'; -const state = useState(); - -const customer = ref(); - -onMounted(async () => { - customer.value = state.get('Customer'); - if (customer.value) customer.value.webAccess = data.value?.account?.isActive; -}); const customerDebt = ref(); const customerCredit = ref(); @@ -55,10 +46,13 @@ const debtWarning = computed(() => { <template> <CardDescriptor + module="Customer" :url="`Clients/${entityId}/getCard`" - :summary="$props.summary" - data-key="Customer" + :title="data.title" + :subtitle="data.subtitle" @on-fetch="setData" + :summary="$props.summary" + data-key="customer" width="lg-width" > <template #menu="{ entity }"> @@ -67,7 +61,7 @@ const debtWarning = computed(() => { <template #body="{ entity }"> <VnLv :label="t('customer.summary.payMethod')" - :value="entity.payMethod?.name" + :value="entity.payMethod.name" /> <VnLv @@ -96,7 +90,7 @@ const debtWarning = computed(() => { </VnLv> <VnLv :label="t('customer.extendedList.tableVisibleColumns.businessTypeFk')" - :value="entity.businessType?.description" + :value="entity.businessType.description" /> </template> <template #icons="{ entity }"> @@ -109,21 +103,7 @@ const debtWarning = computed(() => { > <QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip> </QIcon> - - <QIcon - v-if="entity?.substitutionAllowed" - name="help" - size="xs" - color="primary" - > - <QTooltip>{{ t('Allowed substitution') }}</QTooltip> - </QIcon> - <QIcon - v-if="customer?.isFreezed" - name="vn:frozen" - size="xs" - color="primary" - > + <QIcon v-if="entity.isFreezed" name="vn:frozen" size="xs" color="primary"> <QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip> </QIcon> <QIcon @@ -163,13 +143,13 @@ const debtWarning = computed(() => { <br /> {{ t('unpaidDated', { - dated: toDate(customer.unpaid?.dated), + dated: toDate(customer.unpaid.dated), }) }} <br /> {{ t('unpaidAmount', { - amount: toCurrency(customer.unpaid?.amount), + amount: toCurrency(customer.unpaid.amount), }) }} </QTooltip> diff --git a/src/pages/Customer/Card/CustomerDescriptorMenu.vue b/src/pages/Customer/Card/CustomerDescriptorMenu.vue index aea45721c..fb78eab69 100644 --- a/src/pages/Customer/Card/CustomerDescriptorMenu.vue +++ b/src/pages/Customer/Card/CustomerDescriptorMenu.vue @@ -61,16 +61,6 @@ const openCreateForm = (type) => { .join('&'); useOpenURL(`/#/${type}/list?${params}`); }; -const updateSubstitutionAllowed = async () => { - try { - await axios.patch(`Clients/${route.params.id}`, { - substitutionAllowed: !$props.customer.substitutionAllowed, - }); - notify('globals.notificationSent', 'positive'); - } catch (error) { - notify(error.message, 'positive'); - } -}; </script> <template> @@ -79,13 +69,6 @@ const updateSubstitutionAllowed = async () => { {{ t('globals.pageTitles.createTicket') }} </QItemSection> </QItem> - <QItem v-ripple clickable> - <QItemSection @click="updateSubstitutionAllowed()">{{ - $props.customer.substitutionAllowed - ? t('Disable substitution') - : t('Allow substitution') - }}</QItemSection> - </QItem> <QItem v-ripple clickable> <QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection> </QItem> diff --git a/src/pages/Customer/Card/CustomerFileManagement.vue b/src/pages/Customer/Card/CustomerFileManagement.vue index b565db6e7..134d8dbd6 100644 --- a/src/pages/Customer/Card/CustomerFileManagement.vue +++ b/src/pages/Customer/Card/CustomerFileManagement.vue @@ -236,7 +236,7 @@ const toCustomerFileManagementCreate = () => { @click.stop="toCustomerFileManagementCreate()" color="primary" fab - v-shortcut="'+'" + shortcut="+" icon="add" /> <QTooltip> diff --git a/src/pages/Customer/Card/CustomerFiscalData.vue b/src/pages/Customer/Card/CustomerFiscalData.vue index 93909eb9c..ceeb70bb6 100644 --- a/src/pages/Customer/Card/CustomerFiscalData.vue +++ b/src/pages/Customer/Card/CustomerFiscalData.vue @@ -12,7 +12,6 @@ import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; -import VnCheckbox from 'src/components/common/VnCheckbox.vue'; import { getDifferences, getUpdatedValues } from 'src/filters'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; @@ -74,7 +73,7 @@ async function acceptPropagate({ isEqualizated }) { <FormModel :url-update="`Clients/${route.params.id}/updateFiscalData`" auto-load - model="Customer" + model="customer" :mapper="onBeforeSave" observe-form-changes @on-data-saved="checkEtChanges" @@ -152,11 +151,14 @@ async function acceptPropagate({ isEqualizated }) { </VnRow> <VnRow> <QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" /> - <VnCheckbox - v-model="data.isVies" - :label="t('globals.isVies')" - :info="t('whenActivatingIt')" - /> + <div> + <QCheckbox :label="t('globals.isVies')" v-model="data.isVies" /> + <QIcon name="info" class="cursor-info q-ml-sm" size="sm"> + <QTooltip> + {{ t('whenActivatingIt') }} + </QTooltip> + </QIcon> + </div> </VnRow> <VnRow> @@ -168,11 +170,17 @@ async function acceptPropagate({ isEqualizated }) { </VnRow> <VnRow> - <VnCheckbox - v-model="data.isEqualizated" - :label="t('Is equalizated')" - :info="t('inOrderToInvoice')" - /> + <div> + <QCheckbox + :label="t('Is equalizated')" + v-model="data.isEqualizated" + /> + <QIcon class="cursor-info q-ml-sm" name="info" size="sm"> + <QTooltip> + {{ t('inOrderToInvoice') }} + </QTooltip> + </QIcon> + </div> <QCheckbox :label="t('Daily invoice')" v-model="data.hasDailyInvoice" /> </VnRow> diff --git a/src/pages/Customer/Card/CustomerNotes.vue b/src/pages/Customer/Card/CustomerNotes.vue index 189b59904..b85174696 100644 --- a/src/pages/Customer/Card/CustomerNotes.vue +++ b/src/pages/Customer/Card/CustomerNotes.vue @@ -23,6 +23,5 @@ const noteFilter = computed(() => { :body="{ clientFk: route.params.id }" style="overflow-y: auto" :select-type="true" - required /> </template> diff --git a/src/pages/Customer/Card/CustomerSamples.vue b/src/pages/Customer/Card/CustomerSamples.vue index 19a7f8759..f12691112 100644 --- a/src/pages/Customer/Card/CustomerSamples.vue +++ b/src/pages/Customer/Card/CustomerSamples.vue @@ -104,7 +104,7 @@ const tableRef = ref(); color="primary" fab icon="add" - v-shortcut="'+'" + shortcut="+" /> <QTooltip> {{ t('Send sample') }} diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index 809f10918..3c4106846 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -27,7 +27,7 @@ async function hasCustomerRole() { <FormModel :url-update="`Clients/${route.params.id}/updateUser`" :filter="filter" - model="Customer" + model="customer" :mapper=" ({ account }) => { const { name, email, active } = account; diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index 1c5a08304..9b883daad 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -51,7 +51,11 @@ const exprBuilder = (param, value) => { </QItem> <QItem class="q-mb-sm"> <QItemSection> - <VnInput :label="t('Name')" v-model="params.name" is-outlined /> + <VnInput + :label="t('globals.name')" + v-model="params.name" + is-outlined + /> </QItemSection> </QItem> <QItem class="q-mb-sm"> diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index 0bfca7910..2f2dd5978 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -274,7 +274,6 @@ const columns = computed(() => [ align: 'left', name: 'isActive', label: t('customer.summary.isActive'), - component: 'checkbox', chip: { color: null, condition: (value) => !value, @@ -313,7 +312,6 @@ const columns = computed(() => [ align: 'left', name: 'isFreezed', label: t('customer.extendedList.tableVisibleColumns.isFreezed'), - component: 'checkbox', chip: { color: null, condition: (value) => value, @@ -431,7 +429,7 @@ function handleLocation(data, location) { <VnTable ref="tableRef" :data-key="dataKey" - url="Clients/extendedListFilter" + url="Clients/filter" :create="{ urlCreate: 'Clients/createWithUser', title: t('globals.pageTitles.customerCreate'), diff --git a/src/pages/Customer/Defaulter/CustomerDefaulter.vue b/src/pages/Customer/Defaulter/CustomerDefaulter.vue index dc4ac9162..eca2ad596 100644 --- a/src/pages/Customer/Defaulter/CustomerDefaulter.vue +++ b/src/pages/Customer/Defaulter/CustomerDefaulter.vue @@ -9,7 +9,7 @@ import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.v import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnInput from 'src/components/common/VnInput.vue'; import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue'; -import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; import VnTable from 'src/components/VnTable/VnTable.vue'; import { useArrayData } from 'src/composables/useArrayData'; diff --git a/src/pages/Customer/components/CustomerAddressEdit.vue b/src/pages/Customer/components/CustomerAddressEdit.vue index f852c160a..d650bbbda 100644 --- a/src/pages/Customer/components/CustomerAddressEdit.vue +++ b/src/pages/Customer/components/CustomerAddressEdit.vue @@ -233,7 +233,7 @@ function handleLocation(data, location) { postcode: data.postalCode, city: data.city, province: data.province, - country: data.province?.country, + country: data.province.country, }" @update:model-value="(location) => handleLocation(data, location)" ></VnLocation> @@ -336,7 +336,7 @@ function handleLocation(data, location) { class="cursor-pointer add-icon q-mt-md" flat icon="add" - v-shortcut="'+'" + shortcut="+" > <QTooltip> {{ t('Add note') }} diff --git a/src/pages/Customer/components/CustomerNewPayment.vue b/src/pages/Customer/components/CustomerNewPayment.vue index 8f61bac89..c2c38b55a 100644 --- a/src/pages/Customer/components/CustomerNewPayment.vue +++ b/src/pages/Customer/components/CustomerNewPayment.vue @@ -84,7 +84,7 @@ function setPaymentType(accounting) { viewReceipt.value = isCash.value; if (accountingType.value.daysInFuture) initialData.payed.setDate( - initialData.payed.getDate() + accountingType.value.daysInFuture, + initialData.payed.getDate() + accountingType.value.daysInFuture ); maxAmount.value = accountingType.value && accountingType.value.maxAmount; @@ -114,7 +114,7 @@ function onBeforeSave(data) { if (isCash.value && shouldSendEmail.value && !data.email) return notify(t('There is no assigned email for this client'), 'negative'); - data.bankFk = data.bankFk?.id; + data.bankFk = data.bankFk.id; return data; } @@ -189,7 +189,7 @@ async function getAmountPaid() { :url-create="urlCreate" :mapper="onBeforeSave" @on-data-saved="onDataSaved" - prevent-submit + :prevent-submit="true" > <template #form="{ data, validate }"> <span ref="closeButton" class="row justify-end close-icon" v-close-popup> diff --git a/src/pages/Customer/components/CustomerSamplesCreate.vue b/src/pages/Customer/components/CustomerSamplesCreate.vue index 1294a5d25..754693672 100644 --- a/src/pages/Customer/components/CustomerSamplesCreate.vue +++ b/src/pages/Customer/components/CustomerSamplesCreate.vue @@ -18,7 +18,6 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import CustomerSamplesPreview from 'src/pages/Customer/components/CustomerSamplesPreview.vue'; import FormPopup from 'src/components/FormPopup.vue'; -import { useArrayData } from 'src/composables/useArrayData'; const { dialogRef, onDialogOK } = useDialogPluginComponent(); @@ -40,7 +39,7 @@ const optionsSamplesVisible = ref([]); const sampleType = ref({ hasPreview: false }); const initialData = reactive({}); const entityId = computed(() => route.params.id); -const customer = computed(() => useArrayData('Customer').store?.data); +const customer = computed(() => state.get('customer')); const filterEmailUsers = { where: { userFk: user.value.id } }; const filterClientsAddresses = { include: [ @@ -66,9 +65,9 @@ const filterSamplesVisible = { defineEmits(['confirm', ...useDialogPluginComponent.emits]); onBeforeMount(async () => { - initialData.clientFk = customer.value?.id; - initialData.recipient = customer.value?.email; - initialData.recipientId = customer.value?.id; + initialData.clientFk = customer.value.id; + initialData.recipient = customer.value.email; + initialData.recipientId = customer.value.id; }); const setEmailUser = (data) => { diff --git a/src/pages/Customer/locale/en.yml b/src/pages/Customer/locale/en.yml index b6d495335..118f04a31 100644 --- a/src/pages/Customer/locale/en.yml +++ b/src/pages/Customer/locale/en.yml @@ -107,9 +107,6 @@ customer: defaulterSinced: Defaulted Since hasRecovery: Has Recovery socialName: Social name - typeId: Type - buyerId: Buyer - categoryId: Category city: City phone: Phone postcode: Postcode diff --git a/src/pages/Customer/locale/es.yml b/src/pages/Customer/locale/es.yml index f50d049da..7c33ffee8 100644 --- a/src/pages/Customer/locale/es.yml +++ b/src/pages/Customer/locale/es.yml @@ -108,9 +108,6 @@ customer: hasRecovery: Tiene recobro socialName: Razón social campaign: Campaña - typeId: Familia - buyerId: Comprador - categoryId: Reino city: Ciudad phone: Teléfono postcode: Código postal diff --git a/src/pages/Worker/Department/Card/DepartmentBasicData.vue b/src/pages/Department/Card/DepartmentBasicData.vue similarity index 73% rename from src/pages/Worker/Department/Card/DepartmentBasicData.vue rename to src/pages/Department/Card/DepartmentBasicData.vue index 66210be7b..b13aed2d3 100644 --- a/src/pages/Worker/Department/Card/DepartmentBasicData.vue +++ b/src/pages/Department/Card/DepartmentBasicData.vue @@ -1,16 +1,27 @@ <script setup> +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; + import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; + +const route = useRoute(); +const { t } = useI18n(); </script> <template> - <FormModel model="Department" auto-load class="full-width"> + <FormModel + :url="`Departments/${route.params.id}`" + model="department" + auto-load + class="full-width" + > <template #form="{ data, validate }"> <VnRow> <VnInput - :label="$t('globals.name')" + :label="t('globals.name')" v-model="data.name" :rules="validate('globals.name')" clearable @@ -18,33 +29,33 @@ import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; /> <VnInput v-model="data.code" - :label="$t('globals.code')" + :label="t('globals.code')" :rules="validate('globals.code')" clearable /> </VnRow> <VnRow> <VnInput - :label="$t('department.chat')" + :label="t('department.chat')" v-model="data.chatName" :rules="validate('department.chat')" clearable /> <VnInput v-model="data.notificationEmail" - :label="$t('globals.params.email')" + :label="t('globals.params.email')" :rules="validate('globals.params.email')" clearable /> </VnRow> <VnRow> <VnSelectWorker - :label="$t('department.bossDepartment')" + :label="t('department.bossDepartment')" v-model="data.workerFk" :rules="validate('department.bossDepartment')" /> <VnSelect - :label="$t('department.selfConsumptionCustomer')" + :label="t('department.selfConsumptionCustomer')" v-model="data.clientFk" url="Clients" option-value="id" @@ -56,11 +67,11 @@ import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; </VnRow> <VnRow> <QCheckbox - :label="$t('department.telework')" + :label="t('department.telework')" v-model="data.isTeleworking" /> <QCheckbox - :label="$t('department.notifyOnErrors')" + :label="t('department.notifyOnErrors')" v-model="data.hasToMistake" :false-value="0" :true-value="1" @@ -68,17 +79,17 @@ import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; </VnRow> <VnRow> <QCheckbox - :label="$t('department.worksInProduction')" + :label="t('department.worksInProduction')" v-model="data.isProduction" /> <QCheckbox - :label="$t('department.hasToRefill')" + :label="t('department.hasToRefill')" v-model="data.hasToRefill" /> </VnRow> <VnRow> <QCheckbox - :label="$t('department.hasToSendMail')" + :label="t('department.hasToSendMail')" v-model="data.hasToSendMail" /> </VnRow> diff --git a/src/pages/Worker/Department/Card/DepartmentCard.vue b/src/pages/Department/Card/DepartmentCard.vue similarity index 70% rename from src/pages/Worker/Department/Card/DepartmentCard.vue rename to src/pages/Department/Card/DepartmentCard.vue index 2e3f11521..4b9fe419c 100644 --- a/src/pages/Worker/Department/Card/DepartmentCard.vue +++ b/src/pages/Department/Card/DepartmentCard.vue @@ -1,13 +1,13 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; -import DepartmentDescriptor from 'pages/Worker/Department/Card/DepartmentDescriptor.vue'; +import DepartmentDescriptor from 'pages/Department/Card/DepartmentDescriptor.vue'; </script> <template> <VnCardBeta class="q-pa-md column items-center" v-bind="{ ...$attrs }" data-key="Department" - url="Departments" + base-url="Departments" :descriptor="DepartmentDescriptor" /> </template> diff --git a/src/pages/Worker/Department/Card/DepartmentDescriptor.vue b/src/pages/Department/Card/DepartmentDescriptor.vue similarity index 84% rename from src/pages/Worker/Department/Card/DepartmentDescriptor.vue rename to src/pages/Department/Card/DepartmentDescriptor.vue index 4b7dfd9b8..b219ccfe1 100644 --- a/src/pages/Worker/Department/Card/DepartmentDescriptor.vue +++ b/src/pages/Department/Card/DepartmentDescriptor.vue @@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n'; import { useVnConfirm } from 'composables/useVnConfirm'; import VnLv from 'src/components/ui/VnLv.vue'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; +import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; @@ -31,6 +32,15 @@ const entityId = computed(() => { return $props.id || route.params.id; }); +const department = ref(); + +const data = ref(useCardDescription()); + +const setData = (entity) => { + if (!entity) return; + data.value = useCardDescription(entity.name, entity.id); +}; + const removeDepartment = async () => { await axios.post(`/Departments/${entityId.value}/removeChild`, entityId.value); router.push({ name: 'WorkerDepartment' }); @@ -42,10 +52,19 @@ const { openConfirmationModal } = useVnConfirm(); <template> <CardDescriptor ref="DepartmentDescriptorRef" + module="Department" :url="`Departments/${entityId}`" + :title="data.title" + :subtitle="data.subtitle" :summary="$props.summary" :to-module="{ name: 'WorkerDepartment' }" - data-key="Department" + @on-fetch=" + (data) => { + department = data; + setData(data); + } + " + data-key="department" > <template #menu="{}"> <QItem @@ -55,7 +74,7 @@ const { openConfirmationModal } = useVnConfirm(); openConfirmationModal( t('Are you sure you want to delete it?'), t('Delete department'), - removeDepartment, + removeDepartment ) " > diff --git a/src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue b/src/pages/Department/Card/DepartmentDescriptorProxy.vue similarity index 100% rename from src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue rename to src/pages/Department/Card/DepartmentDescriptorProxy.vue diff --git a/src/pages/Worker/Department/Card/DepartmentSummary.vue b/src/pages/Department/Card/DepartmentSummary.vue similarity index 99% rename from src/pages/Worker/Department/Card/DepartmentSummary.vue rename to src/pages/Department/Card/DepartmentSummary.vue index 3719137e4..3d481601f 100644 --- a/src/pages/Worker/Department/Card/DepartmentSummary.vue +++ b/src/pages/Department/Card/DepartmentSummary.vue @@ -27,7 +27,7 @@ onMounted(async () => { <template> <CardSummary - data-key="Department" + data-key="DepartmentSummary" ref="summary" :url="`Departments/${entityId}`" class="full-width" diff --git a/src/pages/Worker/Department/Card/DepartmentSummaryDialog.vue b/src/pages/Department/Card/DepartmentSummaryDialog.vue similarity index 100% rename from src/pages/Worker/Department/Card/DepartmentSummaryDialog.vue rename to src/pages/Department/Card/DepartmentSummaryDialog.vue diff --git a/src/pages/Entry/Card/EntryBasicData.vue b/src/pages/Entry/Card/EntryBasicData.vue index 6462ed24a..689eea686 100644 --- a/src/pages/Entry/Card/EntryBasicData.vue +++ b/src/pages/Entry/Card/EntryBasicData.vue @@ -1,32 +1,30 @@ <script setup> -import { onMounted, ref } from 'vue'; +import { ref } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useRole } from 'src/composables/useRole'; -import { useState } from 'src/composables/useState'; -import { checkEntryLock } from 'src/composables/checkEntryLock'; import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; +import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; +import FilterTravelForm from 'src/components/FilterTravelForm.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; -import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue'; +import { toDate } from 'src/filters'; import VnSelectSupplier from 'src/components/common/VnSelectSupplier.vue'; const route = useRoute(); const { t } = useI18n(); const { hasAny } = useRole(); const isAdministrative = () => hasAny(['administrative']); -const state = useState(); -const user = state.getUser().fn(); const companiesOptions = ref([]); const currenciesOptions = ref([]); -onMounted(() => { - checkEntryLock(route.params.id, user.id); -}); +const onFilterTravelSelected = (formData, id) => { + formData.travelFk = id; +}; </script> <template> @@ -54,24 +52,46 @@ onMounted(() => { > <template #form="{ data }"> <VnRow> - <VnSelectTravelExtended - :data="data" - v-model="data.travelFk" - :onFilterTravelSelected="(data, result) => (data.travelFk = result)" - /> <VnSelectSupplier v-model="data.supplierFk" hide-selected :required="true" + map-options /> + <VnSelectDialog + :label="t('entry.basicData.travel')" + v-model="data.travelFk" + url="Travels/filter" + :fields="['id', 'warehouseInName']" + option-value="id" + option-label="warehouseInName" + map-options + hide-selected + :required="true" + action-icon="filter_alt" + > + <template #form> + <FilterTravelForm + @travel-selected="onFilterTravelSelected(data, $event)" + /> + </template> + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.agencyModeName }} - + {{ scope.opt?.warehouseInName }} + ({{ toDate(scope.opt?.shipped) }}) → + {{ scope.opt?.warehouseOutName }} + ({{ toDate(scope.opt?.landed) }}) + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelectDialog> </VnRow> <VnRow> <VnInput v-model="data.reference" :label="t('globals.reference')" /> - <VnInputNumber - v-model="data.invoiceAmount" - :label="t('entry.summary.invoiceAmount')" - :positive="false" - /> </VnRow> <VnRow> <VnInput @@ -93,7 +113,8 @@ onMounted(() => { <VnInputNumber :label="t('entry.summary.commission')" v-model="data.commission" - :step="1" + step="1" + autofocus :positive="false" /> <VnSelect @@ -140,7 +161,7 @@ onMounted(() => { :label="t('entry.summary.excludedFromAvailable')" /> <QCheckbox - :disable="!isAdministrative()" + v-if="isAdministrative()" v-model="data.isBooked" :label="t('entry.basicData.booked')" /> diff --git a/src/pages/Entry/Card/EntryBuys.vue b/src/pages/Entry/Card/EntryBuys.vue index 81578c609..6194ce5b8 100644 --- a/src/pages/Entry/Card/EntryBuys.vue +++ b/src/pages/Entry/Card/EntryBuys.vue @@ -1,806 +1,478 @@ <script setup> -import { useStateStore } from 'stores/useStateStore'; -import { useRoute } from 'vue-router'; +import { ref, computed } from 'vue'; +import { useRoute, useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { onMounted, ref } from 'vue'; +import { QBtn } from 'quasar'; -import { useState } from 'src/composables/useState'; - -import FetchData from 'src/components/FetchData.vue'; -import VnTable from 'src/components/VnTable/VnTable.vue'; +import VnPaginate from 'src/components/ui/VnPaginate.vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import FetchedTags from 'components/ui/FetchedTags.vue'; +import VnConfirm from 'components/ui/VnConfirm.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; -import FetchedTags from 'src/components/ui/FetchedTags.vue'; -import VnColor from 'src/components/common/VnColor.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import ItemDescriptor from 'src/pages/Item/Card/ItemDescriptor.vue'; +import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; + +import { useQuasar } from 'quasar'; +import { toCurrency } from 'src/filters'; import axios from 'axios'; -import VnSelectEnum from 'src/components/common/VnSelectEnum.vue'; -import { checkEntryLock } from 'src/composables/checkEntryLock'; +import useNotify from 'src/composables/useNotify.js'; -const $props = defineProps({ - id: { - type: Number, - default: null, - }, - editableMode: { - type: Boolean, - default: true, - }, - tableHeight: { - type: String, - default: null, - }, -}); - -const state = useState(); -const user = state.getUser().fn(); -const stateStore = useStateStore(); -const { t } = useI18n(); +const quasar = useQuasar(); const route = useRoute(); -const selectedRows = ref([]); -const entityId = ref($props.id ?? route.params.id); -const entryBuysRef = ref(); -const footerFetchDataRef = ref(); -const footer = ref({}); -const columns = [ - { - align: 'center', - labelAbbreviation: 'NV', - label: t('Ignore'), - toolTip: t('Ignored for available'), - name: 'isIgnored', - component: 'checkbox', - attrs: { - toggleIndeterminate: false, +const router = useRouter(); +const { t } = useI18n(); +const { notify } = useNotify(); + +const rowsSelected = ref([]); +const entryBuysPaginateRef = ref(null); +const originalRowDataCopy = ref(null); + +const getInputEvents = (colField, props) => { + return colField === 'packagingFk' + ? { 'update:modelValue': () => saveChange(colField, props) } + : { + 'keyup.enter': () => saveChange(colField, props), + blur: () => saveChange(colField, props), + }; +}; + +const tableColumnComponents = computed(() => ({ + item: { + component: QBtn, + props: { + color: 'primary', + flat: true, }, - create: true, - width: '25px', + event: () => ({}), }, - { - label: t('Buyer'), - name: 'workerFk', - component: 'select', - attrs: { - url: 'Workers/search', - fields: ['id', 'nickname'], - optionLabel: 'nickname', - optionValue: 'id', + quantity: { + component: VnInput, + props: { + type: 'number', + min: 0, + class: 'input-number', + dense: true, }, - visible: false, + event: getInputEvents, }, - { - label: t('Family'), - name: 'itemTypeFk', - component: 'select', - attrs: { - url: 'itemTypes', - fields: ['id', 'name'], - optionLabel: 'name', - optionValue: 'id', - }, - visible: false, - }, - { - name: 'id', - isId: true, - visible: false, - isEditable: false, - columnFilter: false, - }, - { - name: 'entryFk', - isId: true, - visible: false, - isEditable: false, - disable: true, - create: true, - columnFilter: false, - }, - { - align: 'center', - label: 'Id', - name: 'itemFk', - component: 'number', - isEditable: false, - width: '35px', - }, - { - labelAbbreviation: '', - label: 'Color', - name: 'hex', - columnSearch: false, - isEditable: false, - width: '9px', - component: 'select', - attrs: { - url: 'Inks', - fields: ['id', 'name'], - }, - }, - { - align: 'center', - label: t('Article'), - name: 'name', - component: 'select', - attrs: { - url: 'Items', - fields: ['id', 'name'], - optionLabel: 'name', - optionValue: 'id', - }, - width: '85px', - isEditable: false, - }, - { - align: 'center', - label: t('Article'), - name: 'itemFk', - visible: false, - create: true, - columnFilter: false, - }, - { - align: 'center', - labelAbbreviation: t('Siz.'), - label: t('Size'), - toolTip: t('Size'), - component: 'number', - name: 'size', - width: '35px', - isEditable: false, - style: () => { - return { color: 'var(--vn-label-color)' }; - }, - }, - { - align: 'center', - labelAbbreviation: t('Sti.'), - label: t('Stickers'), - toolTip: t('Printed Stickers/Stickers'), - name: 'stickers', - component: 'input', - create: true, - attrs: { - positive: false, - }, - cellEvent: { - 'update:modelValue': async (value, oldValue, row) => { - row['quantity'] = value * row['packing']; - row['amount'] = row['quantity'] * row['buyingValue']; - }, - }, - width: '35px', - }, - { - align: 'center', - label: t('Bucket'), - name: 'packagingFk', - component: 'select', - attrs: { - url: 'packagings', + packagingFk: { + component: VnSelect, + props: { + 'option-value': 'id', + 'option-label': 'id', + 'emit-value': true, + 'map-options': true, + 'use-input': true, + 'hide-selected': true, + url: 'Packagings', fields: ['id'], - optionLabel: 'id', - optionValue: 'id', + where: { freightItemFk: true }, + 'sort-by': 'id ASC', + dense: true, }, - create: true, - width: '40px', + event: getInputEvents, }, - { - align: 'center', - label: 'Kg', - name: 'weight', - component: 'number', - create: true, - width: '35px', - format: (row) => parseFloat(row['weight']).toFixed(1), + stickers: { + component: VnInput, + props: { + type: 'number', + min: 0, + class: 'input-number', + dense: true, + }, + event: getInputEvents, }, - { - labelAbbreviation: 'P', - label: 'Packing', - toolTip: 'Packing', - name: 'packing', - component: 'number', - create: true, - cellEvent: { - 'update:modelValue': async (value, oldValue, row) => { - const oldPacking = oldValue === 1 || oldValue === null ? 1 : oldValue; - row['weight'] = (row['weight'] * value) / oldPacking; - row['quantity'] = row['stickers'] * value; - row['amount'] = row['quantity'] * row['buyingValue']; - }, - }, - width: '30px', - style: (row) => { - if (row.groupingMode === 'grouping') - return { color: 'var(--vn-label-color)' }; + printedStickers: { + component: VnInput, + props: { + type: 'number', + min: 0, + class: 'input-number', + dense: true, }, + event: getInputEvents, }, - { - align: 'center', - labelAbbreviation: 'GM', - label: t('Grouping selector'), - toolTip: t('Grouping selector'), - name: 'groupingMode', - component: 'toggle', - attrs: { - 'toggle-indeterminate': true, - trueValue: 'grouping', - falseValue: 'packing', - indeterminateValue: null, - }, - size: 'xs', - width: '25px', - create: true, - rightFilter: false, - getIcon: (value) => { - switch (value) { - case 'grouping': - return 'toggle_on'; - case 'packing': - return 'toggle_off'; - default: - return 'minimize'; - } + weight: { + component: VnInput, + props: { + type: 'number', + min: 0, + dense: true, }, + event: getInputEvents, }, - { - align: 'center', - labelAbbreviation: 'G', - label: 'Grouping', - toolTip: 'Grouping', - name: 'grouping', - component: 'number', - width: '30px', - create: true, - style: (row) => { - if (row.groupingMode === 'packing') return { color: 'var(--vn-label-color)' }; + packing: { + component: VnInput, + props: { + type: 'number', + min: 0, + dense: true, }, + event: getInputEvents, }, - { - align: 'center', - label: t('Quantity'), - name: 'quantity', - component: 'number', - attrs: { - positive: false, + grouping: { + component: VnInput, + props: { + type: 'number', + min: 0, + dense: true, }, - cellEvent: { - 'update:modelValue': async (value, oldValue, row) => { - row['amount'] = value * row['buyingValue']; - }, - }, - width: '45px', - create: true, - style: getQuantityStyle, + event: getInputEvents, }, - { - align: 'center', - labelAbbreviation: t('Cost'), - label: t('Buying value'), - toolTip: t('Buying value'), - name: 'buyingValue', - create: true, - component: 'number', - attrs: { - positive: false, + buyingValue: { + component: VnInput, + props: { + type: 'number', + min: 0, + dense: true, }, - cellEvent: { - 'update:modelValue': async (value, oldValue, row) => { - row['amount'] = row['quantity'] * value; - }, + event: getInputEvents, + }, + price2: { + component: VnInput, + props: { + type: 'number', + min: 0, + dense: true, }, - width: '45px', - format: (row) => parseFloat(row['buyingValue']).toFixed(3), + event: getInputEvents, }, - { - align: 'center', - label: t('Amount'), - name: 'amount', - width: '45px', - component: 'number', - attrs: { - positive: false, + price3: { + component: VnInput, + props: { + type: 'number', + min: 0, + dense: true, }, - isEditable: false, - format: (row) => parseFloat(row['amount']).toFixed(2), - style: getAmountStyle, + event: getInputEvents, }, - { - align: 'center', - labelAbbreviation: t('Pack.'), - label: t('Package'), - toolTip: t('Package'), - name: 'price2', - component: 'number', - width: '35px', - create: true, - format: (row) => parseFloat(row['price2']).toFixed(2), + import: { + component: 'span', + props: {}, + event: () => ({}), }, - { - align: 'center', - label: t('Box'), - name: 'price3', - component: 'number', - cellEvent: { - 'update:modelValue': async (value, oldValue, row) => { - row['price2'] = row['price2'] * (value / oldValue); - }, - }, - width: '35px', - create: true, - format: (row) => parseFloat(row['price3']).toFixed(2), - }, - { - align: 'center', - labelAbbreviation: 'CM', - label: t('Check min price'), - toolTip: t('Check min price'), - name: 'hasMinPrice', - attrs: { - toggleIndeterminate: false, - }, - component: 'checkbox', - cellEvent: { - 'update:modelValue': async (value, oldValue, row) => { - await axios.patch(`Items/${row['itemFk']}`, { - hasMinPrice: value, - }); - }, - }, - width: '25px', - }, - { - align: 'center', - labelAbbreviation: 'Min.', - label: t('Minimum price'), - toolTip: t('Minimum price'), - name: 'minPrice', - component: 'number', - cellEvent: { - 'update:modelValue': async (value, oldValue, row) => { - await axios.patch(`Items/${row['itemFk']}`, { - minPrice: value, - }); - }, - }, - width: '35px', - style: (row) => { - if (!row?.hasMinPrice) return { color: 'var(--vn-label-color)' }; - }, - format: (row) => parseFloat(row['minPrice']).toFixed(2), - }, - { - align: 'center', - labelAbbreviation: t('P.Sen'), - label: t('Packing sent'), - toolTip: t('Packing sent'), - name: 'packingOut', - component: 'number', - isEditable: false, - width: '40px', - style: () => { - return { color: 'var(--vn-label-color)' }; - }, - }, - { - align: 'center', - labelAbbreviation: t('Com.'), - label: t('Comment'), - toolTip: t('Comment'), - name: 'comment', - component: 'input', - isEditable: false, - width: '50px', - }, - { - align: 'center', - labelAbbreviation: 'Prod.', - label: t('Producer'), - toolTip: t('Producer'), - name: 'subName', - isEditable: false, - width: '45px', - style: () => { - return { color: 'var(--vn-label-color)' }; - }, - }, - { - align: 'center', - label: t('Tags'), - name: 'tags', - width: '125px', - columnSearch: false, - }, - { - align: 'center', - labelAbbreviation: 'Comp.', - label: t('Company'), - toolTip: t('Company'), - name: 'company_name', - component: 'input', - isEditable: false, - width: '35px', - style: () => { - return { color: 'var(--vn-label-color)' }; - }, - }, -]; +})); -function getQuantityStyle(row) { - if (row?.quantity !== row?.stickers * row?.packing) - return { color: 'var(--q-negative)' }; -} -function getAmountStyle(row) { - if (row?.isChecked) return { color: 'var(--q-positive)' }; - return { color: 'var(--vn-label-color)' }; -} - -async function beforeSave(data, getChanges) { - try { - const changes = data.updates; - if (!changes) return data; - const patchPromises = []; - - for (const change of changes) { - let patchData = {}; - - if ('hasMinPrice' in change.data) { - patchData.hasMinPrice = change.data?.hasMinPrice; - delete change.data.hasMinPrice; - } - if ('minPrice' in change.data) { - patchData.minPrice = change.data?.minPrice; - delete change.data.minPrice; - } - - if (Object.keys(patchData).length > 0) { - const promise = axios - .get('Buys/findOne', { - params: { - filter: { - fields: ['itemFk'], - where: { id: change.where.id }, - }, - }, - }) - .then((buy) => { - return axios.patch(`Items/${buy.data.itemFk}`, patchData); - }) - .catch((error) => { - console.error('Error processing change: ', change, error); - }); - - patchPromises.push(promise); - } - } - - await Promise.all(patchPromises); - - data.updates = changes.filter((change) => Object.keys(change.data).length > 0); - - return data; - } catch (error) { - console.error('Error in beforeSave:', error); - throw error; - } -} - -function invertQuantitySign(rows, sign) { - for (const row of rows) { - if (sign > 0) row.quantity = Math.abs(row.quantity); - else if (row.quantity > 0) row.quantity = -row.quantity; - } -} -function setIsChecked(rows, value) { - for (const row of rows) { - row.isChecked = value; - } - footerFetchDataRef.value.fetch(); -} - -async function setBuyUltimate(itemFk, data) { - if (!itemFk) return; - const buyUltimate = await axios.get(`Entries/getBuyUltimate`, { - params: { - itemFk, - warehouseFk: user.warehouseFk, - date: Date.vnNew(), +const entriesTableColumns = computed(() => { + return [ + { + label: t('globals.item'), + field: 'itemFk', + name: 'item', + align: 'left', }, - }); - const buyUltimateData = buyUltimate.data[0]; - - const allowedKeys = columns - .filter((col) => col.create === true) - .map((col) => col.name); - - allowedKeys.forEach((key) => { - if (buyUltimateData.hasOwnProperty(key) && key !== 'entryFk') { - if (!['stickers', 'quantity'].includes(key)) data[key] = buyUltimateData[key]; - } - }); -} - -onMounted(() => { - stateStore.rightDrawer = false; - if ($props.editableMode) checkEntryLock(entityId.value, user.id); + { + label: t('globals.quantity'), + field: 'quantity', + name: 'quantity', + align: 'left', + }, + { + label: t('entry.summary.package'), + field: 'packagingFk', + name: 'packagingFk', + align: 'left', + }, + { + label: t('entry.summary.stickers'), + field: 'stickers', + name: 'stickers', + align: 'left', + }, + { + label: t('entry.buys.printedStickers'), + field: 'printedStickers', + name: 'printedStickers', + align: 'left', + }, + { + label: t('globals.weight'), + field: 'weight', + name: 'weight', + align: 'left', + }, + { + label: t('entry.summary.packing'), + field: 'packing', + name: 'packing', + align: 'left', + }, + { + label: t('entry.summary.grouping'), + field: 'grouping', + name: 'grouping', + align: 'left', + }, + { + label: t('entry.summary.buyingValue'), + field: 'buyingValue', + name: 'buyingValue', + align: 'left', + format: (value) => toCurrency(value), + }, + { + label: t('item.fixedPrice.groupingPrice'), + field: 'price2', + name: 'price2', + align: 'left', + }, + { + label: t('item.fixedPrice.packingPrice'), + field: 'price3', + name: 'price3', + align: 'left', + }, + { + label: t('entry.summary.import'), + name: 'import', + align: 'left', + format: (_, row) => toCurrency(row.buyingValue * row.quantity), + }, + ]; }); + +const copyOriginalRowsData = (rows) => { + originalRowDataCopy.value = JSON.parse(JSON.stringify(rows)); +}; + +const saveChange = async (field, { rowIndex, row }) => { + if (originalRowDataCopy.value[rowIndex][field] == row[field]) return; + await axios.patch(`Buys/${row.id}`, row); + originalRowDataCopy.value[rowIndex][field] = row[field]; +}; + +const openRemoveDialog = async () => { + quasar + .dialog({ + component: VnConfirm, + componentProps: { + title: t('Confirm deletion'), + message: t( + `Are you sure you want to delete this buy${ + rowsSelected.value.length > 1 ? 's' : '' + }?` + ), + data: rowsSelected.value, + }, + }) + .onOk(async () => { + await deleteBuys(); + const notifyMessage = t( + `Buy${rowsSelected.value.length > 1 ? 's' : ''} deleted` + ); + notify(notifyMessage, 'positive'); + }); +}; + +const deleteBuys = async () => { + await axios.post('Buys/deleteBuys', { buys: rowsSelected.value }); + entryBuysPaginateRef.value.fetch(); +}; + +const importBuys = () => { + router.push({ name: 'EntryBuysImport' }); +}; + +const toggleGroupingMode = async (buy, mode) => { + const groupingMode = mode === 'grouping' ? mode : 'packing'; + const newGroupingMode = buy.groupingMode === groupingMode ? null : groupingMode; + const params = { + groupingMode: newGroupingMode, + }; + await axios.patch(`Buys/${buy.id}`, params); + buy.groupingMode = newGroupingMode; +}; + +const lockIconType = (groupingMode, mode) => { + if (mode === 'packing') { + return groupingMode === 'packing' ? 'lock' : 'lock_open'; + } else { + return groupingMode === 'grouping' ? 'lock' : 'lock_open'; + } +}; </script> + <template> - <Teleport to="#st-data" v-if="stateStore?.isSubToolbarShown() && editableMode"> - <QBtnGroup push style="column-gap: 1px"> - <QBtnDropdown - label="+/-" - color="primary" - flat - :title="t('Invert quantity value')" - :disable="!selectedRows.length" - data-cy="change-quantity-sign" - > - <QList> - <QItem> - <QItemSection> - <QBtn - flat - @click="invertQuantitySign(selectedRows, -1)" - data-cy="set-negative-quantity" - > - <span style="font-size: large">-</span> - </QBtn> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <QBtn - flat - @click="invertQuantitySign(selectedRows, 1)" - data-cy="set-positive-quantity" - > - <span style="font-size: large">+</span> - </QBtn> - </QItemSection> - </QItem> - </QList> - </QBtnDropdown> - <QBtnDropdown - icon="price_check" - color="primary" - flat - :title="t('Check buy amount')" - :disable="!selectedRows.length" - data-cy="check-buy-amount" - > - <QList> - <QItem> - <QItemSection> - <QBtn - size="sm" - icon="check" - flat - @click="setIsChecked(selectedRows, true)" - data-cy="check-amount" - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <QBtn - size="sm" - icon="close" - flat - @click="setIsChecked(selectedRows, false)" - data-cy="uncheck-amount" - /> - </QItemSection> - </QItem> - </QList> - </QBtnDropdown> - </QBtnGroup> - </Teleport> - <FetchData - ref="footerFetchDataRef" - :url="`Entries/${entityId}/getBuyList`" - :params="{ groupBy: 'GROUP BY b.entryFk' }" - @on-fetch="(data) => (footer = data[0])" - auto-load - /> - <VnTable - ref="entryBuysRef" + <VnSubToolbar> + <template #st-actions> + <QBtnGroup push style="column-gap: 10px"> + <slot name="moreBeforeActions" /> + <QBtn + :label="t('globals.remove')" + color="primary" + icon="delete" + flat + @click="openRemoveDialog()" + :disable="!rowsSelected?.length" + :title="t('globals.remove')" + /> + </QBtnGroup> + </template> + </VnSubToolbar> + <VnPaginate + ref="entryBuysPaginateRef" data-key="EntryBuys" - :url="`Entries/${entityId}/getBuyList`" - save-url="Buys/crud" - :disable-option="{ card: true }" - v-model:selected="selectedRows" - @on-fetch="() => footerFetchDataRef.fetch()" - :table=" - editableMode - ? { - 'row-key': 'id', - selection: 'multiple', - } - : {} - " - :create=" - editableMode - ? { - urlCreate: 'Buys', - title: t('Create buy'), - onDataSaved: () => { - entryBuysRef.reload(); - }, - formInitialData: { entryFk: entityId, isIgnored: false }, - showSaveAndContinueBtn: true, - } - : null - " - :create-complement="{ - isFullWidth: true, - containerStyle: { - display: 'flex', - 'flex-wrap': 'wrap', - gap: '16px', - position: 'relative', - height: '450px', - }, - columnGridStyle: { - 'max-width': '50%', - flex: 1, - 'margin-right': '30px', - }, - }" - :is-editable="editableMode" - :without-header="!editableMode" - :with-filters="editableMode" - :right-search="true" - :right-search-icon="true" - :row-click="false" - :columns="columns" - :beforeSaveFn="beforeSave" - class="buyList" - :table-height="$props.tableHeight ?? '84vh'" + :url="`Entries/${route.params.id}/getBuys`" + @on-fetch="copyOriginalRowsData($event)" auto-load - footer - data-cy="entry-buys" > - <template #column-hex="{ row }"> - <VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" /> - </template> - <template #column-name="{ row }"> - <span class="link"> - {{ row?.name }} - <ItemDescriptorProxy :id="row?.itemFk" /> - </span> - </template> - <template #column-tags="{ row }"> - <FetchedTags :item="row" :columns="3" /> - </template> - <template #column-stickers="{ row }"> - <span :class="editableMode ? 'editable-text' : ''"> - <span style="color: var(--vn-label-color)"> - {{ row.printedStickers }} - </span> - <span>/{{ row.stickers }}</span> - </span> - </template> - <template #column-footer-stickers> - <div> - <span style="color: var(--vn-label-color)"> - {{ footer?.printedStickers }}</span - > - <span>/</span> - <span data-cy="footer-stickers">{{ footer?.stickers }}</span> - </div> - </template> - <template #column-footer-weight> - {{ footer?.weight }} - </template> - <template #column-footer-quantity> - <span :style="getQuantityStyle(footer)" data-cy="footer-quantity"> - {{ footer?.quantity }} - </span> - </template> - <template #column-footer-amount> - <span :style="getAmountStyle(footer)" data-cy="footer-amount"> - {{ footer?.amount }} - </span> - </template> - <template #column-create-itemFk="{ data }"> - <VnSelect - url="Items/search" - v-model="data.itemFk" - :label="t('Article')" - :fields="['id', 'name', 'size', 'producerName']" - :filter-options="['id', 'name', 'size', 'producerName']" - option-label="name" - option-value="id" - @update:modelValue=" - async (value) => { - await setBuyUltimate(value, data); - } - " - :required="true" - data-cy="itemFk-create-popup" - sort-by="nickname DESC" + <template #body="{ rows }"> + <QTable + :rows="rows" + :columns="entriesTableColumns" + selection="multiple" + row-key="id" + class="full-width q-mt-md" + :grid="$q.screen.lt.md" + v-model:selected="rowsSelected" + :no-data-label="t('globals.noResults')" > - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel> - {{ scope.opt.name }} - </QItemLabel> - <QItemLabel caption> - #{{ scope.opt.id }}, {{ scope.opt?.size }}, - {{ scope.opt?.producerName }} - </QItemLabel> - </QItemSection> - </QItem> + <template #body="props"> + <QTr> + <QTd> + <QCheckbox v-model="props.selected" /> + </QTd> + <QTd + v-for="col in props.cols" + :key="col.name" + style="max-width: 100px" + > + <component + :is="tableColumnComponents[col.name].component" + v-bind="tableColumnComponents[col.name].props" + v-model="props.row[col.field]" + v-on=" + tableColumnComponents[col.name].event( + col.field, + props + ) + " + > + <template + v-if=" + col.name === 'grouping' || col.name === 'packing' + " + #append + > + <QBtn + :icon=" + lockIconType(props.row.groupingMode, col.name) + " + @click="toggleGroupingMode(props.row, col.name)" + class="cursor-pointer" + size="sm" + flat + dense + unelevated + push + :style="{ + 'font-variation-settings': `'FILL' ${ + lockIconType( + props.row.groupingMode, + col.name + ) === 'lock' + ? 1 + : 0 + }`, + }" + /> + </template> + <template + v-if="col.name === 'item' || col.name === 'import'" + > + {{ col.value }} + </template> + <ItemDescriptorProxy + v-if="col.name === 'item'" + :id="props.row.item.id" + /> + </component> + </QTd> + </QTr> + <QTr no-hover class="full-width infoRow" style="column-span: all"> + <QTd /> + <QTd cols> + <span>{{ props.row.item.itemType.code }}</span> + </QTd> + <QTd> + <span>{{ props.row.item.size }}</span> + </QTd> + <QTd> + <span>{{ toCurrency(props.row.item.minPrice) }}</span> + </QTd> + <QTd colspan="7"> + <span>{{ props.row.item.concept }}</span> + <span v-if="props.row.item.subName" class="subName"> + {{ props.row.item.subName }} + </span> + <FetchedTags :item="props.row.item" /> + </QTd> + </QTr> </template> - </VnSelect> + <template #item="props"> + <div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition"> + <QCard bordered flat> + <QCardSection> + <QCheckbox v-model="props.selected" dense /> + </QCardSection> + <QSeparator /> + <QList dense> + <QItem v-for="col in props.cols" :key="col.name"> + <component + :is="tableColumnComponents[col.name].component" + v-bind="tableColumnComponents[col.name].props" + v-model="props.row[col.field]" + v-on=" + tableColumnComponents[col.name].event( + col.field, + props + ) + " + class="full-width" + > + <template + v-if=" + col.name === 'item' || + col.name === 'import' + " + > + {{ col.label + ': ' + col.value }} + </template> + </component> + </QItem> + </QList> + </QCard> + </div> + </template> + </QTable> </template> - <template #column-create-groupingMode="{ data }"> - <VnSelectEnum - :label="t('Grouping mode')" - v-model="data.groupingMode" - schema="vn" - table="buy" - column="groupingMode" - option-value="groupingMode" - option-label="groupingMode" - /> - </template> - <template #previous-create-dialog="{ data }"> - <div - style="position: absolute" - :class="{ 'centered-container': !data.itemFk }" - > - <ItemDescriptor :id="data.itemFk" v-if="data.itemFk" /> - <div v-else> - <span>{{ t('globals.noData') }}</span> - </div> - </div> - </template> - </VnTable> + </VnPaginate> + + <QPageSticky :offset="[20, 20]"> + <QBtn fab icon="upload" color="primary" @click="importBuys()" /> + <QTooltip class="text-no-wrap"> + {{ t('Import buys') }} + </QTooltip> + </QPageSticky> </template> -<i18n> -es: - Article: Artículo - Siz.: Med. - Size: Medida - Sti.: Eti. - Bucket: Cubo - Quantity: Cantidad - Amount: Importe - Pack.: Paq. - Package: Paquete - Box: Caja - P.Sen: P.Env - Packing sent: Packing envíos - Com.: Ref. - Comment: Referencia - Minimum price: Precio mínimo - Stickers: Etiquetas - Printed Stickers/Stickers: Etiquetas impresas/Etiquetas - Cost: Cost. - Buying value: Coste - Producer: Productor - Company: Compañia - Tags: Etiquetas - Grouping mode: Modo de agrupación - C.min: P.min - Ignore: Ignorar - Ignored for available: Ignorado para disponible - Grouping selector: Selector de grouping - Check min price: Marcar precio mínimo - Create buy: Crear compra - Invert quantity value: Invertir valor de cantidad - Check buy amount: Marcar como correcta la cantidad de compra -</i18n> + <style lang="scss" scoped> -.centered-container { - display: flex; - justify-content: center; - align-items: center; - position: absolute; - width: 40%; - height: 100%; +.q-table--horizontal-separator tbody tr:nth-child(odd) > td { + border-bottom-width: 0px; + border-top-width: 2px; + border-color: var(--vn-text-color); +} +.infoRow > td { + color: var(--vn-label-color); } </style> + +<i18n> +es: + Import buys: Importar compras + Buy deleted: Compra eliminada + Buys deleted: Compras eliminadas + Confirm deletion: Confirmar eliminación + Are you sure you want to delete this buy?: Seguro que quieres eliminar esta compra? + Are you sure you want to delete this buys?: Seguro que quieres eliminar estas compras? +</i18n> diff --git a/src/pages/Entry/Card/EntryCard.vue b/src/pages/Entry/Card/EntryCard.vue index be82289f4..e00623a21 100644 --- a/src/pages/Entry/Card/EntryCard.vue +++ b/src/pages/Entry/Card/EntryCard.vue @@ -1,13 +1,13 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import EntryDescriptor from './EntryDescriptor.vue'; -import filter from './EntryFilter.js'; +import filter from './EntryFilter.js' </script> <template> <VnCardBeta data-key="Entry" - url="Entries" + base-url="Entries" :descriptor="EntryDescriptor" - :filter="filter" + :user-filter="filter" /> </template> diff --git a/src/pages/Entry/Card/EntryDescriptor.vue b/src/pages/Entry/Card/EntryDescriptor.vue index 69b300cb2..19d13e51a 100644 --- a/src/pages/Entry/Card/EntryDescriptor.vue +++ b/src/pages/Entry/Card/EntryDescriptor.vue @@ -1,19 +1,12 @@ <script setup> import { ref, computed, onMounted } from 'vue'; -import { useRoute, useRouter } from 'vue-router'; +import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { toDate } from 'src/filters'; -import { getUrl } from 'src/composables/getUrl'; -import { useQuasar } from 'quasar'; -import { usePrintService } from 'composables/usePrintService'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; -import axios from 'axios'; - -const quasar = useQuasar(); -const { push } = useRouter(); -const { openReport } = usePrintService(); +import { toDate } from 'src/filters'; +import { getUrl } from 'src/composables/getUrl'; +import EntryDescriptorMenu from './EntryDescriptorMenu.vue'; const $props = defineProps({ id: { @@ -90,63 +83,12 @@ const getEntryRedirectionFilter = (entry) => { to, }); }; - -function showEntryReport() { - openReport(`Entries/${entityId.value}/entry-order-pdf`); -} - -function showNotification(type, message) { - quasar.notify({ - type: type, - message: t(message), - }); -} - -async function recalculateRates(entity) { - try { - const entryConfig = await axios.get('EntryConfigs/findOne'); - if (entryConfig.data?.inventorySupplierFk === entity.supplierFk) { - showNotification( - 'negative', - 'Cannot recalculate prices because this is an inventory entry', - ); - return; - } - - await axios.post(`Entries/${entityId.value}/recalcEntryPrices`); - showNotification('positive', 'Entry prices recalculated'); - } catch (error) { - showNotification('negative', 'Failed to recalculate rates'); - console.error(error); - } -} - -async function cloneEntry() { - try { - const response = await axios.post(`Entries/${entityId.value}/cloneEntry`); - push({ path: `/entry/${response.data}` }); - showNotification('positive', 'Entry cloned'); - } catch (error) { - showNotification('negative', 'Failed to clone entry'); - console.error(error); - } -} - -async function deleteEntry() { - try { - await axios.post(`Entries/${entityId.value}/deleteEntry`); - push({ path: `/entry/list` }); - showNotification('positive', 'Entry deleted'); - } catch (error) { - showNotification('negative', 'Failed to delete entry'); - console.error(error); - } -} </script> <template> <CardDescriptor ref="entryDescriptorRef" + module="Entry" :url="`Entries/${entityId}`" :userFilter="entryFilter" title="supplier.nickname" @@ -154,56 +96,15 @@ async function deleteEntry() { width="lg-width" > <template #menu="{ entity }"> - <QItem - v-ripple - clickable - @click="showEntryReport(entity)" - data-cy="show-entry-report" - > - <QItemSection>{{ t('Show entry report') }}</QItemSection> - </QItem> - <QItem - v-ripple - clickable - @click="recalculateRates(entity)" - data-cy="recalculate-rates" - > - <QItemSection>{{ t('Recalculate rates') }}</QItemSection> - </QItem> - <QItem v-ripple clickable @click="cloneEntry(entity)" data-cy="clone-entry"> - <QItemSection>{{ t('Clone') }}</QItemSection> - </QItem> - <QItem v-ripple clickable @click="deleteEntry(entity)" data-cy="delete-entry"> - <QItemSection>{{ t('Delete') }}</QItemSection> - </QItem> + <EntryDescriptorMenu :id="entity.id" /> </template> <template #body="{ entity }"> - <VnLv :label="t('Travel')"> - <template #value> - <span class="link" v-if="entity?.travelFk"> - {{ entity.travel?.agency?.name }} - {{ entity.travel?.warehouseOut?.code }} → - {{ entity.travel?.warehouseIn?.code }} - <TravelDescriptorProxy :id="entity?.travelFk" /> - </span> - </template> - </VnLv> + <VnLv :label="t('globals.agency')" :value="entity.travel?.agency?.name" /> + <VnLv :label="t('shipped')" :value="toDate(entity.travel?.shipped)" /> + <VnLv :label="t('landed')" :value="toDate(entity.travel?.landed)" /> <VnLv - :label="t('entry.summary.travelShipped')" - :value="toDate(entity.travel?.shipped)" - /> - <VnLv - :label="t('entry.summary.travelLanded')" - :value="toDate(entity.travel?.landed)" - /> - <VnLv :label="t('entry.summary.currency')" :value="entity?.currency?.code" /> - <VnLv - :label="t('entry.summary.invoiceAmount')" - :value="entity?.invoiceAmount" - /> - <VnLv - :label="t('entry.summary.entryType')" - :value="entity?.entryType?.description" + :label="t('globals.warehouseOut')" + :value="entity.travel?.warehouseOut?.name" /> </template> <template #icons="{ entity }"> @@ -230,14 +131,6 @@ async function deleteEntry() { }}</QTooltip > </QIcon> - <QIcon - v-if="!entity?.travelFk" - name="vn:deletedTicket" - size="xs" - color="primary" - > - <QTooltip>{{ t('This entry is deleted') }}</QTooltip> - </QIcon> </QCardActions> </template> <template #actions="{ entity }"> @@ -250,6 +143,21 @@ async function deleteEntry() { > <QTooltip>{{ t('Supplier card') }}</QTooltip> </QBtn> + <QBtn + :to="{ + name: 'TravelMain', + query: { + params: JSON.stringify({ + agencyModeFk: entity.travel?.agencyModeFk, + }), + }, + }" + size="md" + icon="local_airport" + color="primary" + > + <QTooltip>{{ t('All travels with current agency') }}</QTooltip> + </QBtn> <QBtn :to="{ name: 'EntryMain', @@ -269,24 +177,10 @@ async function deleteEntry() { </template> <i18n> es: - Travel: Envío Supplier card: Ficha del proveedor All travels with current agency: Todos los envíos con la agencia actual All entries with current supplier: Todas las entradas con el proveedor actual Show entry report: Ver informe del pedido Inventory entry: Es inventario Virtual entry: Es una redada - shipped: Enviado - landed: Recibido - This entry is deleted: Esta entrada está eliminada - Cannot recalculate prices because this is an inventory entry: No se pueden recalcular los precios porque es una entrada de inventario - Entry deleted: Entrada eliminada - Entry cloned: Entrada clonada - Entry prices recalculated: Precios de la entrada recalculados - Failed to recalculate rates: No se pudieron recalcular las tarifas - Failed to clone entry: No se pudo clonar la entrada - Failed to delete entry: No se pudo eliminar la entrada - Recalculate rates: Recalcular tarifas - Clone: Clonar - Delete: Eliminar </i18n> diff --git a/src/pages/Entry/Card/EntryFilter.js b/src/pages/Entry/Card/EntryFilter.js index d9fd1c2be..3ff62cf27 100644 --- a/src/pages/Entry/Card/EntryFilter.js +++ b/src/pages/Entry/Card/EntryFilter.js @@ -9,7 +9,6 @@ export default { 'shipped', 'agencyModeFk', 'warehouseOutFk', - 'warehouseInFk', 'daysInForward', ], include: [ @@ -22,13 +21,13 @@ export default { { relation: 'warehouseOut', scope: { - fields: ['name', 'code'], + fields: ['name'], }, }, { relation: 'warehouseIn', scope: { - fields: ['name', 'code'], + fields: ['name'], }, }, ], @@ -40,17 +39,5 @@ export default { fields: ['id', 'nickname'], }, }, - { - relation: 'currency', - scope: { - fields: ['id', 'code'], - }, - }, - { - relation: 'entryType', - scope: { - fields: ['code', 'description'], - }, - }, ], }; diff --git a/src/pages/Entry/Card/EntryNotes.vue b/src/pages/Entry/Card/EntryNotes.vue index 459c3b069..55cac0437 100644 --- a/src/pages/Entry/Card/EntryNotes.vue +++ b/src/pages/Entry/Card/EntryNotes.vue @@ -17,7 +17,7 @@ const selected = ref([]); const sortEntryObservationOptions = (data) => { entryObservationsOptions.value = [...data].sort((a, b) => - a.description.localeCompare(b.description), + a.description.localeCompare(b.description) ); }; @@ -142,7 +142,7 @@ const columns = computed(() => [ fab color="primary" icon="add" - v-shortcut="'+'" + shortcut="+" @click="entryObservationsRef.insert()" /> </QPageSticky> diff --git a/src/pages/Entry/Card/EntrySummary.vue b/src/pages/Entry/Card/EntrySummary.vue index c40e2ba46..8c46fb6e6 100644 --- a/src/pages/Entry/Card/EntrySummary.vue +++ b/src/pages/Entry/Card/EntrySummary.vue @@ -2,17 +2,19 @@ import { onMounted, ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { toDate } from 'src/filters'; -import { getUrl } from 'src/composables/getUrl'; -import axios from 'axios'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; -import EntryBuys from './EntryBuys.vue'; -import VnTitle from 'src/components/common/VnTitle.vue'; -import VnCheckbox from 'src/components/common/VnCheckbox.vue'; + +import { toDate, toCurrency, toCelsius } from 'src/filters'; +import { getUrl } from 'src/composables/getUrl'; +import axios from 'axios'; +import FetchedTags from 'src/components/ui/FetchedTags.vue'; import VnToSummary from 'src/components/ui/VnToSummary.vue'; +import EntryDescriptorMenu from './EntryDescriptorMenu.vue'; +import VnRow from 'src/components/ui/VnRow.vue'; +import VnTitle from 'src/components/common/VnTitle.vue'; const route = useRoute(); const { t } = useI18n(); @@ -31,6 +33,117 @@ const entry = ref(); const entryBuys = ref([]); const entryUrl = ref(); +onMounted(async () => { + entryUrl.value = (await getUrl('entry/')) + entityId.value; +}); + +const tableColumnComponents = { + quantity: { + component: () => 'span', + props: () => {}, + }, + stickers: { + component: () => 'span', + props: () => {}, + event: () => {}, + }, + packagingFk: { + component: () => 'span', + props: () => {}, + event: () => {}, + }, + weight: { + component: () => 'span', + props: () => {}, + event: () => {}, + }, + packing: { + component: () => 'span', + props: () => {}, + event: () => {}, + }, + grouping: { + component: () => 'span', + props: () => {}, + event: () => {}, + }, + buyingValue: { + component: () => 'span', + props: () => {}, + event: () => {}, + }, + amount: { + component: () => 'span', + props: () => {}, + event: () => {}, + }, + pvp: { + component: () => 'span', + props: () => {}, + event: () => {}, + }, +}; + +const entriesTableColumns = computed(() => { + return [ + { + label: t('globals.quantity'), + field: 'quantity', + name: 'quantity', + align: 'left', + }, + { + label: t('entry.summary.stickers'), + field: 'stickers', + name: 'stickers', + align: 'left', + }, + { + label: t('entry.summary.package'), + field: 'packagingFk', + name: 'packagingFk', + align: 'left', + }, + { + label: t('globals.weight'), + field: 'weight', + name: 'weight', + align: 'left', + }, + { + label: t('entry.summary.packing'), + field: 'packing', + name: 'packing', + align: 'left', + }, + { + label: t('entry.summary.grouping'), + field: 'grouping', + name: 'grouping', + align: 'left', + }, + { + label: t('entry.summary.buyingValue'), + field: 'buyingValue', + name: 'buyingValue', + align: 'left', + format: (value) => toCurrency(value), + }, + { + label: t('entry.summary.import'), + name: 'amount', + align: 'left', + format: (_, row) => toCurrency(row.buyingValue * row.quantity), + }, + { + label: t('entry.summary.pvp'), + name: 'pvp', + align: 'left', + format: (_, row) => toCurrency(row.price2) + ' / ' + toCurrency(row.price3), + }, + ]; +}); + async function setEntryData(data) { if (data) entry.value = data; await fetchEntryBuys(); @@ -40,18 +153,14 @@ const fetchEntryBuys = async () => { const { data } = await axios.get(`Entries/${entry.value.id}/getBuys`); if (data) entryBuys.value = data; }; - -onMounted(async () => { - entryUrl.value = (await getUrl('entry/')) + entityId.value; -}); </script> + <template> <CardSummary ref="summaryRef" :url="`Entries/${entityId}/getEntry`" @on-fetch="(data) => setEntryData(data)" data-key="EntrySummary" - data-cy="entry-summary" > <template #header-left> <VnToSummary @@ -64,154 +173,159 @@ onMounted(async () => { <template #header> <span>{{ entry.id }} - {{ entry.supplier.nickname }}</span> </template> + <template #menu="{ entity }"> + <EntryDescriptorMenu :id="entity.id" /> + </template> <template #body> <QCard class="vn-one"> <VnTitle :url="`#/entry/${entityId}/basic-data`" :text="t('globals.summary.basicData')" /> - <div class="card-group"> - <div class="card-content"> - <VnLv - :label="t('entry.summary.commission')" - :value="entry?.commission" - /> - <VnLv - :label="t('entry.summary.currency')" - :value="entry?.currency?.name" - /> - <VnLv - :label="t('globals.company')" - :value="entry?.company?.code" - /> - <VnLv :label="t('globals.reference')" :value="entry?.reference" /> - <VnLv - :label="t('entry.summary.invoiceNumber')" - :value="entry?.invoiceNumber" - /> - </div> - <div class="card-content"> - <VnCheckbox - :label="t('entry.summary.ordered')" - v-model="entry.isOrdered" - :disable="true" - size="xs" - /> - <VnCheckbox - :label="t('globals.confirmed')" - v-model="entry.isConfirmed" - :disable="true" - size="xs" - /> - <VnCheckbox - :label="t('entry.summary.booked')" - v-model="entry.isBooked" - :disable="true" - size="xs" - /> - <VnCheckbox - :label="t('entry.summary.excludedFromAvailable')" - v-model="entry.isExcludedFromAvailable" - :disable="true" - size="xs" - /> - </div> - </div> - </QCard> - <QCard class="vn-one" v-if="entry?.travelFk"> - <VnTitle - :url="`#/travel/${entry.travel.id}/summary`" - :text="t('Travel')" + <VnLv :label="t('entry.summary.commission')" :value="entry.commission" /> + <VnLv + :label="t('entry.summary.currency')" + :value="entry.currency?.name" /> - <div class="card-group"> - <div class="card-content"> - <VnLv :label="t('entry.summary.travelReference')"> - <template #value> - <span class="link"> - {{ entry.travel.ref }} - <TravelDescriptorProxy :id="entry.travel.id" /> - </span> - </template> - </VnLv> - <VnLv - :label="t('entry.summary.travelAgency')" - :value="entry.travel.agency?.name" - /> - <VnLv - :label="t('entry.summary.travelShipped')" - :value="toDate(entry.travel.shipped)" - /> - <VnLv - :label="t('globals.warehouseOut')" - :value="entry.travel.warehouseOut?.name" - /> - <VnLv - :label="t('entry.summary.travelLanded')" - :value="toDate(entry.travel.landed)" - /> - <VnLv - :label="t('globals.warehouseIn')" - :value="entry.travel.warehouseIn?.name" - /> - </div> - <div class="card-content"> - <VnCheckbox - :label="t('entry.summary.travelDelivered')" - v-model="entry.travel.isDelivered" - :disable="true" - size="xs" - /> - <VnCheckbox - :label="t('entry.summary.travelReceived')" - v-model="entry.travel.isReceived" - :disable="true" - size="xs" - /> - </div> - </div> + <VnLv :label="t('globals.company')" :value="entry.company.code" /> + <VnLv :label="t('globals.reference')" :value="entry.reference" /> + <VnLv + :label="t('entry.summary.invoiceNumber')" + :value="entry.invoiceNumber" + /> + <VnLv + :label="t('entry.basicData.initialTemperature')" + :value="toCelsius(entry.initialTemperature)" + /> + <VnLv + :label="t('entry.basicData.finalTemperature')" + :value="toCelsius(entry.finalTemperature)" + /> + </QCard> + <QCard class="vn-one"> + <VnTitle + :url="`#/entry/${entityId}/basic-data`" + :text="t('globals.summary.basicData')" + /> + <VnLv :label="t('entry.summary.travelReference')"> + <template #value> + <span class="link"> + {{ entry.travel.ref }} + <TravelDescriptorProxy :id="entry.travel.id" /> + </span> + </template> + </VnLv> + <VnLv + :label="t('entry.summary.travelAgency')" + :value="entry.travel.agency?.name" + /> + <VnLv + :label="t('globals.shipped')" + :value="toDate(entry.travel.shipped)" + /> + <VnLv + :label="t('globals.warehouseOut')" + :value="entry.travel.warehouseOut?.name" + /> + <VnLv + :label="t('entry.summary.travelDelivered')" + :value="entry.travel.isDelivered" + /> + <VnLv :label="t('globals.landed')" :value="toDate(entry.travel.landed)" /> + <VnLv + :label="t('globals.warehouseIn')" + :value="entry.travel.warehouseIn?.name" + /> + <VnLv + :label="t('entry.summary.travelReceived')" + :value="entry.travel.isReceived" + /> + </QCard> + <QCard class="vn-one"> + <VnTitle :url="`#/travel/${entityId}/summary`" :text="t('Travel data')" /> + <VnRow class="block"> + <VnLv :label="t('entry.summary.ordered')" :value="entry.isOrdered" /> + <VnLv :label="t('globals.confirmed')" :value="entry.isConfirmed" /> + <VnLv :label="t('entry.summary.booked')" :value="entry.isBooked" /> + <VnLv + :label="t('entry.summary.excludedFromAvailable')" + :value="entry.isExcludedFromAvailable" + /> + </VnRow> </QCard> <QCard class="vn-max"> <VnTitle :url="`#/entry/${entityId}/buys`" :text="t('entry.summary.buys')" /> - <EntryBuys - v-if="entityId" - :id="Number(entityId)" - :editable-mode="false" - table-height="49vh" - /> + <QTable + :rows="entryBuys" + :columns="entriesTableColumns" + row-key="index" + class="full-width q-mt-md" + :no-data-label="t('globals.noResults')" + > + <template #body="{ cols, row, rowIndex }"> + <QTr no-hover> + <QTd v-for="col in cols" :key="col?.name"> + <component + :is="tableColumnComponents[col?.name].component()" + v-bind="tableColumnComponents[col?.name].props()" + @click="tableColumnComponents[col?.name].event()" + class="col-content" + > + <template + v-if=" + col?.name !== 'observation' && + col?.name !== 'isConfirmed' + " + >{{ col.value }}</template + > + <QTooltip v-if="col.toolTip">{{ + col.toolTip + }}</QTooltip> + </component> + </QTd> + </QTr> + <QTr no-hover> + <QTd> + <span>{{ row.item.itemType.code }}</span> + </QTd> + <QTd> + <span>{{ row.item.id }}</span> + </QTd> + <QTd> + <span>{{ row.item.size }}</span> + </QTd> + <QTd> + <span>{{ toCurrency(row.item.minPrice) }}</span> + </QTd> + <QTd colspan="6"> + <span>{{ row.item.concept }}</span> + <span v-if="row.item.subName" class="subName"> + {{ row.item.subName }} + </span> + <FetchedTags :item="row.item" /> + </QTd> + </QTr> + <!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys --> + <QTr v-if="rowIndex !== entryBuys.length - 1"> + <QTd colspan="10" class="vn-table-separation-row" /> + </QTr> + </template> + </QTable> </QCard> </template> </CardSummary> </template> + <style lang="scss" scoped> -.card-group { - display: flex; - flex-direction: column; -} - -.card-content { - display: flex; - flex-direction: column; - text-overflow: ellipsis; - > div { - max-height: 24px; - } -} - -@media (min-width: 1010px) { - .card-group { - flex-direction: row; - } - .card-content { - flex: 1; - margin-right: 16px; - } +.separation-row { + background-color: var(--vn-section-color) !important; } </style> + <i18n> es: - Travel: Envío - InvoiceIn data: Datos factura + Travel data: Datos envío </i18n> diff --git a/src/pages/Entry/EntryFilter.vue b/src/pages/Entry/EntryFilter.vue index 8c60918a8..0f632c0ef 100644 --- a/src/pages/Entry/EntryFilter.vue +++ b/src/pages/Entry/EntryFilter.vue @@ -19,7 +19,6 @@ const props = defineProps({ const currenciesOptions = ref([]); const companiesOptions = ref([]); -const entryFilterPanel = ref(); </script> <template> @@ -39,7 +38,7 @@ const entryFilterPanel = ref(); @on-fetch="(data) => (currenciesOptions = data)" auto-load /> - <VnFilterPanel ref="entryFilterPanel" :data-key="props.dataKey" :search-button="true"> + <VnFilterPanel :data-key="props.dataKey" :search-button="true"> <template #tags="{ tag, formatFn }"> <div class="q-gutter-x-xs"> <strong>{{ t(`entryFilter.params.${tag.label}`) }}: </strong> @@ -49,65 +48,70 @@ const entryFilterPanel = ref(); <template #body="{ params, searchFn }"> <QItem> <QItemSection> - <QCheckbox - :label="t('params.isExcludedFromAvailable')" - v-model="params.isExcludedFromAvailable" - toggle-indeterminate - > - <QTooltip> - {{ t('params.isExcludedFromAvailable') }} - </QTooltip> - </QCheckbox> - </QItemSection> - <QItemSection> - <QCheckbox - :label="t('params.isOrdered')" - v-model="params.isOrdered" - toggle-indeterminate - > - <QTooltip> - {{ t('entry.list.tableVisibleColumns.isOrdered') }} - </QTooltip> - </QCheckbox> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <QCheckbox - :label="t('params.isReceived')" - v-model="params.isReceived" - toggle-indeterminate - > - <QTooltip> - {{ t('entry.list.tableVisibleColumns.isReceived') }} - </QTooltip> - </QCheckbox> - </QItemSection> - <QItemSection> - <QCheckbox - :label="t('entry.list.tableVisibleColumns.isConfirmed')" - v-model="params.isConfirmed" - toggle-indeterminate - > - <QTooltip> - {{ t('entry.list.tableVisibleColumns.isConfirmed') }} - </QTooltip> - </QCheckbox> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInputDate - :label="t('params.landed')" - v-model="params.landed" - @update:model-value="searchFn()" + <VnInput + v-model="params.search" + :label="t('entryFilter.params.search')" is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <VnInput v-model="params.id" label="Id" is-outlined /> + <VnInput + v-model="params.reference" + :label="t('entryFilter.params.reference')" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.invoiceNumber" + :label="t('entryFilter.params.invoiceNumber')" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.travelFk" + :label="t('entryFilter.params.travelFk')" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('entryFilter.params.companyFk')" + v-model="params.companyFk" + @update:model-value="searchFn()" + :options="companiesOptions" + option-value="id" + option-label="code" + hide-selected + dense + outlined + rounded + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('entryFilter.params.currencyFk')" + v-model="params.currencyFk" + @update:model-value="searchFn()" + :options="currenciesOptions" + option-value="id" + option-label="name" + hide-selected + dense + outlined + rounded + /> </QItemSection> </QItem> <QItem> @@ -121,165 +125,62 @@ const entryFilterPanel = ref(); rounded /> </QItemSection> - <QItemSection> - <VnInput - v-model="params.invoiceNumber" - :label="t('params.invoiceNumber')" - is-outlined - /> - </QItemSection> </QItem> <QItem> <QItemSection> - <VnInput - v-model="params.reference" - :label="t('entry.list.tableVisibleColumns.reference')" - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - :label="t('params.agencyModeId')" - v-model="params.agencyModeId" + <VnInputDate + :label="t('entryFilter.params.created')" + v-model="params.created" @update:model-value="searchFn()" - url="AgencyModes" - :fields="['id', 'name']" - hide-selected - dense - outlined - rounded - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput - v-model="params.evaNotes" - :label="t('params.evaNotes')" is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <VnSelect - :label="t('params.warehouseOutFk')" - v-model="params.warehouseOutFk" + <VnInputDate + :label="t('entryFilter.params.from')" + v-model="params.from" @update:model-value="searchFn()" - url="Warehouses" - :fields="['id', 'name']" - hide-selected - dense - outlined - rounded - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - :label="t('params.warehouseInFk')" - v-model="params.warehouseInFk" - @update:model-value="searchFn()" - url="Warehouses" - :fields="['id', 'name']" - hide-selected - dense - outlined - rounded - > - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel> - {{ scope.opt?.name }} - </QItemLabel> - <QItemLabel caption> - {{ `#${scope.opt?.id} , ${scope.opt?.nickname}` }} - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput - v-model="params.invoiceNumber" - :label="t('params.invoiceNumber')" is-outlined /> </QItemSection> </QItem> - <QItem> <QItemSection> - <VnSelect - :label="t('params.entryTypeCode')" - v-model="params.entryTypeCode" + <VnInputDate + :label="t('entryFilter.params.to')" + v-model="params.to" @update:model-value="searchFn()" - url="EntryTypes" - :fields="['code', 'description']" - option-value="code" - option-label="description" - hide-selected - dense - outlined - rounded + is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <VnInput - v-model="params.evaNotes" - :label="t('params.evaNotes')" - is-outlined + <QCheckbox + :label="t('entryFilter.params.isBooked')" + v-model="params.isBooked" + toggle-indeterminate + /> + </QItemSection> + <QItemSection> + <QCheckbox + :label="t('entryFilter.params.isConfirmed')" + v-model="params.isConfirmed" + toggle-indeterminate + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QCheckbox + :label="t('entryFilter.params.isOrdered')" + v-model="params.isOrdered" + toggle-indeterminate /> </QItemSection> </QItem> </template> </VnFilterPanel> </template> - -<i18n> -en: - params: - isExcludedFromAvailable: Inventory - isOrdered: Ordered - isReceived: Received - isConfirmed: Confirmed - isRaid: Raid - landed: Date - id: Id - supplierFk: Supplier - invoiceNumber: Invoice number - reference: Ref/Alb/Guide - agencyModeId: Agency mode - evaNotes: Notes - warehouseOutFk: Origin - warehouseInFk: Destiny - entryTypeCode: Entry type - hasToShowDeletedEntries: Show deleted entries -es: - params: - isExcludedFromAvailable: Inventario - isOrdered: Pedida - isConfirmed: Confirmado - isReceived: Recibida - isRaid: Raid - landed: Fecha - id: Id - supplierFk: Proveedor - invoiceNumber: Núm. factura - reference: Ref/Alb/Guía - agencyModeId: Modo agencia - evaNotes: Notas - warehouseOutFk: Origen - warehouseInFk: Destino - entryTypeCode: Tipo de entrada - hasToShowDeletedEntries: Mostrar entradas eliminadas -</i18n> diff --git a/src/pages/Entry/EntryList.vue b/src/pages/Entry/EntryList.vue index 3c96a2302..3172c6d0e 100644 --- a/src/pages/Entry/EntryList.vue +++ b/src/pages/Entry/EntryList.vue @@ -1,25 +1,21 @@ <script setup> -import axios from 'axios'; -import VnSection from 'src/components/common/VnSection.vue'; import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; -import { useState } from 'src/composables/useState'; -import { onBeforeMount } from 'vue'; - import EntryFilter from './EntryFilter.vue'; import VnTable from 'components/VnTable/VnTable.vue'; +import { toCelsius, toDate } from 'src/filters'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import EntrySummary from './Card/EntrySummary.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; -import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue'; -import { toDate } from 'src/filters'; +import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; +import VnSection from 'src/components/common/VnSection.vue'; const { t } = useI18n(); const tableRef = ref(); -const defaultEntry = ref({}); -const state = useState(); -const user = state.getUser(); const dataKey = 'EntryList'; -const entryQueryFilter = { +const { viewSummary } = useSummaryDialog(); +const entryFilter = { include: [ { relation: 'suppliers', @@ -44,58 +40,44 @@ const entryQueryFilter = { const columns = computed(() => [ { - labelAbbreviation: 'Ex', - label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), - toolTip: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), - name: 'isExcludedFromAvailable', - component: 'checkbox', - width: '35px', + name: 'status', + columnFilter: false, }, { - labelAbbreviation: 'Pe', - label: t('entry.list.tableVisibleColumns.isOrdered'), - toolTip: t('entry.list.tableVisibleColumns.isOrdered'), - name: 'isOrdered', - component: 'checkbox', - width: '35px', + align: 'left', + label: t('globals.id'), + name: 'id', + isId: true, + chip: { + condition: () => true, + }, }, { - labelAbbreviation: 'LE', - label: t('entry.list.tableVisibleColumns.isConfirmed'), - toolTip: t('entry.list.tableVisibleColumns.isConfirmed'), - name: 'isConfirmed', - component: 'checkbox', - width: '35px', + align: 'left', + label: t('globals.reference'), + name: 'reference', + isTitle: true, + component: 'input', + columnField: { + component: null, + }, + create: true, + cardVisible: true, }, { - labelAbbreviation: 'Re', - label: t('entry.list.tableVisibleColumns.isReceived'), - toolTip: t('entry.list.tableVisibleColumns.isReceived'), - name: 'isReceived', - component: 'checkbox', - width: '35px', - }, - { - label: t('entry.list.tableVisibleColumns.landed'), - name: 'landed', + align: 'left', + label: t('entry.list.tableVisibleColumns.created'), + name: 'created', + create: true, + cardVisible: true, component: 'date', columnField: { component: null, }, - format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.landed)), - width: '105px', - }, - { - label: t('globals.id'), - name: 'id', - isId: true, - component: 'number', - chip: { - condition: () => true, - }, - width: '50px', + format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.created)), }, { + align: 'left', label: t('entry.list.tableVisibleColumns.supplierFk'), name: 'supplierFk', create: true, @@ -104,213 +86,165 @@ const columns = computed(() => [ attrs: { url: 'suppliers', fields: ['id', 'name'], - where: { order: 'name DESC' }, + }, + columnField: { + component: null, }, format: (row, dashIfEmpty) => dashIfEmpty(row.supplierName), - width: '110px', }, { align: 'left', - label: t('entry.list.tableVisibleColumns.invoiceNumber'), - name: 'invoiceNumber', - component: 'input', - }, - { - align: 'left', - label: t('entry.list.tableVisibleColumns.reference'), - name: 'reference', - isTitle: true, - component: 'input', - columnField: { - component: null, - }, + label: t('entry.list.tableVisibleColumns.isBooked'), + name: 'isBooked', cardVisible: true, + create: true, + component: 'checkbox', }, { align: 'left', - label: 'AWB', - name: 'awbCode', - component: 'input', - width: '100px', - }, - { - align: 'left', - label: t('entry.list.tableVisibleColumns.agencyModeId'), - name: 'agencyModeId', + label: t('entry.list.tableVisibleColumns.isConfirmed'), + name: 'isConfirmed', cardVisible: true, - component: 'select', - attrs: { - url: 'agencyModes', - fields: ['id', 'name'], - }, - columnField: { - component: null, - }, - format: (row, dashIfEmpty) => dashIfEmpty(row.agencyModeName), + create: true, + component: 'checkbox', }, { align: 'left', - label: t('entry.list.tableVisibleColumns.evaNotes'), - name: 'evaNotes', - component: 'input', - }, - { - align: 'left', - label: t('entry.list.tableVisibleColumns.warehouseOutFk'), - name: 'warehouseOutFk', + label: t('entry.list.tableVisibleColumns.isOrdered'), + name: 'isOrdered', cardVisible: true, - component: 'select', - attrs: { - url: 'warehouses', - fields: ['id', 'name'], - }, - columnField: { - component: null, - }, - format: (row, dashIfEmpty) => dashIfEmpty(row.warehouseOutName), - width: '65px', + create: true, + component: 'checkbox', }, { align: 'left', - label: t('entry.list.tableVisibleColumns.warehouseInFk'), - name: 'warehouseInFk', - cardVisible: true, - component: 'select', - attrs: { - url: 'warehouses', - fields: ['id', 'name'], - }, - columnField: { - component: null, - }, - format: (row, dashIfEmpty) => dashIfEmpty(row.warehouseInName), - width: '65px', - }, - { - align: 'left', - labelAbbreviation: t('Type'), - label: t('entry.list.tableVisibleColumns.entryTypeDescription'), - toolTip: t('entry.list.tableVisibleColumns.entryTypeDescription'), - name: 'entryTypeCode', - component: 'select', - attrs: { - url: 'entryTypes', - fields: ['code', 'description'], - optionValue: 'code', - optionLabel: 'description', - }, - width: '65px', - format: (row, dashIfEmpty) => dashIfEmpty(row.entryTypeDescription), - }, - { - name: 'companyFk', label: t('entry.list.tableVisibleColumns.companyFk'), - cardVisible: false, - visible: false, - create: true, + name: 'companyFk', component: 'select', attrs: { - optionValue: 'id', + url: 'companies', + fields: ['id', 'code'], optionLabel: 'code', - url: 'Companies', + optionValue: 'id', + }, + columnField: { + component: null, + }, + create: true, + + format: (row, dashIfEmpty) => dashIfEmpty(row.companyCode), + }, + { + align: 'left', + label: t('entry.list.tableVisibleColumns.travelFk'), + name: 'travelFk', + component: 'select', + attrs: { + url: 'travels', + fields: ['id', 'ref'], + optionLabel: 'ref', + optionValue: 'id', + }, + columnField: { + component: null, + }, + create: true, + format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), + }, + { + align: 'left', + label: t('entry.list.tableVisibleColumns.invoiceAmount'), + name: 'invoiceAmount', + cardVisible: true, + }, + { + align: 'left', + name: 'initialTemperature', + label: t('entry.basicData.initialTemperature'), + field: 'initialTemperature', + format: (row) => toCelsius(row.initialTemperature), + }, + { + align: 'left', + name: 'finalTemperature', + label: t('entry.basicData.finalTemperature'), + field: 'finalTemperature', + format: (row) => toCelsius(row.finalTemperature), + }, + { + label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), + name: 'isExcludedFromAvailable', + columnFilter: { + inWhere: true, }, }, { - name: 'travelFk', - label: t('entry.list.tableVisibleColumns.travelFk'), - cardVisible: false, - visible: false, - create: true, + align: 'right', + name: 'tableActions', + actions: [ + { + title: t('components.smartCard.viewSummary'), + icon: 'preview', + action: (row) => viewSummary(row.id, EntrySummary), + isPrimary: true, + }, + ], }, ]); -function getBadgeAttrs(row) { - const date = row.landed; - let today = Date.vnNew(); - today.setHours(0, 0, 0, 0); - let timeTicket = new Date(date); - timeTicket.setHours(0, 0, 0, 0); - - let timeDiff = today - timeTicket; - - if (timeDiff > 0) return { color: 'info', 'text-color': 'black' }; - if (timeDiff < 0) return { color: 'warning', 'text-color': 'black' }; - switch (row.entryTypeCode) { - case 'regularization': - case 'life': - case 'internal': - case 'inventory': - if (!row.isOrdered || !row.isConfirmed) - return { color: 'negative', 'text-color': 'black' }; - break; - case 'product': - case 'packaging': - case 'devaluation': - case 'payment': - case 'transport': - if ( - row.invoiceAmount === null || - (row.invoiceNumber === null && row.reference === null) || - !row.isOrdered || - !row.isConfirmed - ) - return { color: 'negative', 'text-color': 'black' }; - break; - default: - break; - } - return { color: 'transparent' }; -} - -onBeforeMount(async () => { - defaultEntry.value = (await axios.get('EntryConfigs/findOne')).data; -}); </script> <template> <VnSection :data-key="dataKey" + :columns="columns" prefix="entry" url="Entries/filter" :array-data-props="{ url: 'Entries/filter', - order: 'landed DESC', - userFilter: EntryFilter, + order: 'id DESC', + userFilter: entryFilter, }" > <template #advanced-menu> - <EntryFilter :data-key="dataKey" /> + <EntryFilter data-key="EntryList" /> </template> <template #body> <VnTable - v-if="defaultEntry.defaultSupplierFk" ref="tableRef" :data-key="dataKey" - url="Entries/filter" - :filter="entryQueryFilter" - order="landed DESC" :create="{ urlCreate: 'Entries', - title: t('Create entry'), + title: t('entry.list.newEntry'), onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: { - supplierFk: defaultEntry.defaultSupplierFk, - dated: Date.vnNew(), - companyFk: user?.companyFk, - }, + formInitialData: {}, }" :columns="columns" redirect="entry" :right-search="false" > - <template #column-landed="{ row }"> - <QBadge - v-if="row?.travelFk" - v-bind="getBadgeAttrs(row)" - class="q-pa-sm" - style="font-size: 14px" - > - {{ toDate(row.landed) }} - </QBadge> + <template #column-status="{ row }"> + <div class="row q-gutter-xs"> + <QIcon + v-if="!!row.isExcludedFromAvailable" + name="vn:inventory" + color="primary" + > + <QTooltip>{{ + t( + 'entry.list.tableVisibleColumns.isExcludedFromAvailable', + ) + }}</QTooltip> + </QIcon> + <QIcon v-if="!!row.isRaid" name="vn:net" color="primary"> + <QTooltip> + {{ + t('globals.raid', { + daysInForward: row.daysInForward, + }) + }}</QTooltip + > + </QIcon> + </div> </template> <template #column-supplierFk="{ row }"> <span class="link" @click.stop> @@ -318,27 +252,13 @@ onBeforeMount(async () => { <SupplierDescriptorProxy :id="row.supplierFk" /> </span> </template> - <template #column-create-travelFk="{ data }"> - <VnSelectTravelExtended - :data="data" - v-model="data.travelFk" - :onFilterTravelSelected=" - (data, result) => (data.travelFk = result) - " - data-cy="entry-travel-select" - /> + <template #column-travelFk="{ row }"> + <span class="link" @click.stop> + {{ row.travelRef }} + <TravelDescriptorProxy :id="row.travelFk" /> + </span> </template> </VnTable> </template> </VnSection> </template> - -<i18n> -es: - Inventory entry: Es inventario - Virtual entry: Es una redada - Search entries: Buscar entradas - You can search by entry reference: Puedes buscar por referencia de la entrada - Create entry: Crear entrada - Type: Tipo -</i18n> diff --git a/src/pages/Entry/EntryStockBought.vue b/src/pages/Entry/EntryStockBought.vue index 4bd0fe640..fa0bdc12e 100644 --- a/src/pages/Entry/EntryStockBought.vue +++ b/src/pages/Entry/EntryStockBought.vue @@ -34,20 +34,18 @@ const columns = computed(() => [ label: t('entryStockBought.buyer'), isTitle: true, component: 'select', - isEditable: false, cardVisible: true, create: true, attrs: { url: 'Workers/activeWithInheritedRole', - fields: ['id', 'name', 'nickname'], + fields: ['id', 'name'], where: { role: 'buyer' }, optionFilter: 'firstName', - optionLabel: 'nickname', + optionLabel: 'name', optionValue: 'id', useLike: false, }, columnFilter: false, - width: '70px', }, { align: 'center', @@ -57,7 +55,6 @@ const columns = computed(() => [ create: true, component: 'number', summation: true, - width: '50px', }, { align: 'center', @@ -81,7 +78,6 @@ const columns = computed(() => [ actions: [ { title: t('entryStockBought.viewMoreDetails'), - name: 'searchBtn', icon: 'search', isPrimary: true, action: (row) => { @@ -95,7 +91,6 @@ const columns = computed(() => [ }, }, ], - 'data-cy': 'table-actions', }, ]); @@ -163,7 +158,7 @@ function round(value) { @on-fetch=" (data) => { travel = data.find( - (data) => data.warehouseIn?.code.toLowerCase() === 'vnh', + (data) => data.warehouseIn?.code.toLowerCase() === 'vnh' ); } " @@ -184,7 +179,6 @@ function round(value) { @click="openDialog()" :title="t('entryStockBought.editTravel')" color="primary" - data-cy="edit-travel" /> </div> </VnRow> @@ -245,11 +239,10 @@ function round(value) { table-height="80vh" auto-load :column-search="false" - :without-header="true" > <template #column-workerFk="{ row }"> <span class="link" @click.stop> - {{ row?.worker?.user?.nickname }} + {{ row?.worker?.user?.name }} <WorkerDescriptorProxy :id="row?.workerFk" /> </span> </template> @@ -286,11 +279,10 @@ function round(value) { justify-content: center; } .column { - min-width: 40%; - margin-top: 5%; display: flex; flex-direction: column; align-items: center; + min-width: 35%; } .text-negative { color: $negative !important; diff --git a/src/pages/Entry/EntryStockBoughtDetail.vue b/src/pages/Entry/EntryStockBoughtDetail.vue index 1a37994d9..812171825 100644 --- a/src/pages/Entry/EntryStockBoughtDetail.vue +++ b/src/pages/Entry/EntryStockBoughtDetail.vue @@ -21,7 +21,7 @@ const $props = defineProps({ const customUrl = `StockBoughts/getStockBoughtDetail?workerFk=${$props.workerFk}&dated=${$props.dated}`; const columns = [ { - align: 'right', + align: 'left', label: t('Entry'), name: 'entryFk', isTitle: true, @@ -29,7 +29,7 @@ const columns = [ columnFilter: false, }, { - align: 'right', + align: 'left', name: 'itemFk', label: t('Item'), columnFilter: false, @@ -44,21 +44,21 @@ const columns = [ cardVisible: true, }, { - align: 'right', + align: 'left', name: 'volume', label: t('Volume'), columnFilter: false, cardVisible: true, }, { - align: 'right', + align: 'left', label: t('Packaging'), name: 'packagingFk', columnFilter: false, cardVisible: true, }, { - align: 'right', + align: 'left', label: 'Packing', name: 'packing', columnFilter: false, @@ -73,14 +73,12 @@ const columns = [ ref="tableRef" data-key="StockBoughtsDetail" :url="customUrl" - order="volume DESC" + order="itemName DESC" :columns="columns" :right-search="false" :disable-infinite-scroll="true" :disable-option="{ card: true }" :limit="0" - :without-header="true" - :with-filters="false" auto-load > <template #column-entryFk="{ row }"> @@ -101,14 +99,16 @@ const columns = [ </template> <style lang="css" scoped> .container { - max-width: 100%; - width: 50%; + max-width: 50vw; overflow: auto; justify-content: center; align-items: center; margin: auto; background-color: var(--vn-section-color); - padding: 2%; + padding: 4px; +} +.container > div > div > .q-table__top.relative-position.row.items-center { + background-color: red !important; } </style> <i18n> diff --git a/src/pages/Entry/locale/en.yml b/src/pages/Entry/locale/en.yml index 88b16cb03..80f3491a8 100644 --- a/src/pages/Entry/locale/en.yml +++ b/src/pages/Entry/locale/en.yml @@ -1,36 +1,21 @@ entry: - lock: - title: Lock entry - message: This entry has been locked by {userName} for {time} minutes. Do you want to unlock it? - success: The entry has been locked successfully list: newEntry: New entry tableVisibleColumns: - isExcludedFromAvailable: Exclude from inventory - isOrdered: Ordered - isConfirmed: Ready to label - isReceived: Received - isRaid: Raid - landed: Date + created: Creation supplierFk: Supplier - reference: Ref/Alb/Guide - invoiceNumber: Invoice - agencyModeId: Agency isBooked: Booked + isConfirmed: Confirmed + isOrdered: Ordered companyFk: Company - evaNotes: Notes - warehouseOutFk: Origin - warehouseInFk: Destiny - entryTypeDescription: Entry type - invoiceAmount: Import travelFk: Travel - dated: Dated + isExcludedFromAvailable: Inventory + invoiceAmount: Import inventoryEntry: Inventory entry summary: commission: Commission currency: Currency invoiceNumber: Invoice number - invoiceAmount: Invoice amount ordered: Ordered booked: Booked excludedFromAvailable: Inventory @@ -48,7 +33,6 @@ entry: buyingValue: Buying value import: Import pvp: PVP - entryType: Entry type basicData: travel: Travel currency: Currency @@ -85,55 +69,17 @@ entry: landing: Landing isExcludedFromAvailable: Es inventory params: - isExcludedFromAvailable: Exclude from inventory - isOrdered: Ordered - isConfirmed: Ready to label - isReceived: Received - isIgnored: Ignored - isRaid: Raid - landed: Date - supplierFk: Supplier - reference: Ref/Alb/Guide - invoiceNumber: Invoice - agencyModeId: Agency - isBooked: Booked - companyFk: Company - evaNotes: Notes - warehouseOutFk: Origin - warehouseInFk: Destiny - entryTypeDescription: Entry type - invoiceAmount: Import - travelFk: Travel - dated: Dated - itemFk: Item id - hex: Color - name: Item name - size: Size - stickers: Stickers - packagingFk: Packaging - weight: Kg - groupingMode: Grouping selector - grouping: Grouping - quantity: Quantity - buyingValue: Buying value - price2: Package - price3: Box - minPrice: Minumum price - hasMinPrice: Has minimum price - packingOut: Packing out - comment: Comment - subName: Supplier name - tags: Tags - company_name: Company name - itemTypeFk: Item type - workerFk: Worker id + toShipped: To + fromShipped: From + daysOnward: Days onward + daysAgo: Days ago + warehouseInFk: Warehouse in search: Search entries searchInfo: You can search by entry reference descriptorMenu: showEntryReport: Show entry report entryFilter: params: - isExcludedFromAvailable: Exclude from inventory invoiceNumber: Invoice number travelFk: Travel companyFk: Company @@ -145,16 +91,8 @@ entryFilter: isBooked: Booked isConfirmed: Confirmed isOrdered: Ordered - isReceived: Received search: General search reference: Reference - landed: Landed - id: Id - agencyModeId: Agency - evaNotes: Notes - warehouseOutFk: Origin - warehouseInFk: Destiny - entryTypeCode: Entry type myEntries: id: ID landed: Landed diff --git a/src/pages/Entry/locale/es.yml b/src/pages/Entry/locale/es.yml index 3025d64cb..a5b968016 100644 --- a/src/pages/Entry/locale/es.yml +++ b/src/pages/Entry/locale/es.yml @@ -1,36 +1,21 @@ entry: - lock: - title: Entrada bloqueada - message: Esta entrada ha sido bloqueada por {userName} hace {time} minutos. ¿Quieres desbloquearla? - success: La entrada ha sido bloqueada correctamente list: newEntry: Nueva entrada tableVisibleColumns: - isExcludedFromAvailable: Excluir del inventario - isOrdered: Pedida - isConfirmed: Lista para etiquetar - isReceived: Recibida - isRaid: Redada - landed: Fecha + created: Creación supplierFk: Proveedor - invoiceNumber: Nº Factura - reference: Ref/Alb/Guía - agencyModeId: Agencia isBooked: Asentado + isConfirmed: Confirmado + isOrdered: Pedida companyFk: Empresa travelFk: Envio - evaNotes: Notas - warehouseOutFk: Origen - warehouseInFk: Destino - entryTypeDescription: Tipo entrada + isExcludedFromAvailable: Inventario invoiceAmount: Importe - dated: Fecha inventoryEntry: Es inventario summary: commission: Comisión currency: Moneda invoiceNumber: Núm. factura - invoiceAmount: Importe ordered: Pedida booked: Contabilizada excludedFromAvailable: Inventario @@ -49,13 +34,12 @@ entry: buyingValue: Coste import: Importe pvp: PVP - entryType: Tipo entrada basicData: travel: Envío currency: Moneda observation: Observación commission: Comisión - booked: Contabilizada + booked: Asentado excludedFromAvailable: Inventario initialTemperature: Ini °C finalTemperature: Fin °C @@ -85,70 +69,31 @@ entry: packingOut: Embalaje envíos landing: Llegada isExcludedFromAvailable: Es inventario - + params: + toShipped: Hasta + fromShipped: Desde + warehouseInFk: Alm. entrada + daysOnward: Días adelante + daysAgo: Días atras + descriptorMenu: + showEntryReport: Ver informe del pedido search: Buscar entradas searchInfo: Puedes buscar por referencia de entrada - params: - isExcludedFromAvailable: Excluir del inventario - isOrdered: Pedida - isConfirmed: Lista para etiquetar - isReceived: Recibida - isRaid: Redada - isIgnored: Ignorado - landed: Fecha - supplierFk: Proveedor - invoiceNumber: Nº Factura - reference: Ref/Alb/Guía - agencyModeId: Agencia - isBooked: Asentado - companyFk: Empresa - travelFk: Envio - evaNotes: Notas - warehouseOutFk: Origen - warehouseInFk: Destino - entryTypeDescription: Tipo entrada - invoiceAmount: Importe - dated: Fecha - itemFk: Id artículo - hex: Color - name: Nombre artículo - size: Medida - stickers: Etiquetas - packagingFk: Embalaje - weight: Kg - groupinMode: Selector de grouping - grouping: Grouping - quantity: Quantity - buyingValue: Precio de compra - price2: Paquete - price3: Caja - minPrice: Precio mínimo - hasMinPrice: Tiene precio mínimo - packingOut: Packing out - comment: Referencia - subName: Nombre proveedor - tags: Etiquetas - company_name: Nombre empresa - itemTypeFk: Familia - workerFk: Comprador entryFilter: params: - isExcludedFromAvailable: Inventario - isOrdered: Pedida - isConfirmed: Confirmado - isReceived: Recibida - isRaid: Raid - landed: Fecha - id: Id - supplierFk: Proveedor invoiceNumber: Núm. factura - reference: Ref/Alb/Guía - agencyModeId: Modo agencia - evaNotes: Notas - warehouseOutFk: Origen - warehouseInFk: Destino - entryTypeCode: Tipo de entrada - hasToShowDeletedEntries: Mostrar entradas eliminadas + travelFk: Envío + companyFk: Empresa + currencyFk: Moneda + supplierFk: Proveedor + from: Desde + to: Hasta + created: Fecha creación + isBooked: Asentado + isConfirmed: Confirmado + isOrdered: Pedida + search: Búsqueda general + reference: Referencia myEntries: id: ID landed: F. llegada diff --git a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue index 905ddebb2..c01ec4ab4 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue @@ -125,7 +125,7 @@ function deleteFile(dmsFk) { <VnInput clearable clear-icon="close" - :label="t('invoiceIn.supplierRef')" + :label="t('Supplier ref')" v-model="data.supplierRef" /> </VnRow> @@ -149,7 +149,6 @@ function deleteFile(dmsFk) { option-value="id" option-label="id" :filter-options="['id', 'name']" - data-cy="UnDeductibleVatSelect" > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -216,7 +215,7 @@ function deleteFile(dmsFk) { v-else icon="add_circle" round - v-shortcut="'+'" + shortcut="+" padding="xs" @click=" () => { @@ -311,6 +310,7 @@ function deleteFile(dmsFk) { supplierFk: Supplier es: supplierFk: Proveedor + Supplier ref: Ref. proveedor Expedition date: Fecha expedición Operation date: Fecha operación Undeductible VAT: Iva no deducible diff --git a/src/pages/InvoiceIn/Card/InvoiceInCard.vue b/src/pages/InvoiceIn/Card/InvoiceInCard.vue index 34cc26437..8aa35f4d8 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInCard.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInCard.vue @@ -1,18 +1,47 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import InvoiceInDescriptor from './InvoiceInDescriptor.vue'; -import { onBeforeRouteUpdate } from 'vue-router'; -import { setRectificative } from '../composables/setRectificative'; -import filter from './InvoiceInFilter.js'; -onBeforeRouteUpdate(async (to) => await setRectificative(to)); +const filter = { + include: [ + { + relation: 'supplier', + scope: { + include: { + relation: 'contacts', + scope: { where: { email: { neq: null } } }, + }, + }, + }, + { relation: 'invoiceInDueDay' }, + { relation: 'company' }, + { relation: 'currency' }, + { + relation: 'dms', + scope: { + fields: [ + 'dmsTypeFk', + 'reference', + 'hardCopyNumber', + 'workerFk', + 'description', + 'hasFile', + 'file', + 'created', + 'companyFk', + 'warehouseFk', + ], + }, + }, + ], +}; </script> <template> <VnCardBeta data-key="InvoiceIn" - url="InvoiceIns" + base-url="InvoiceIns" :descriptor="InvoiceInDescriptor" - :filter="filter" + :user-filter="filter" /> </template> diff --git a/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue b/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue index 3843f5bf7..da7bd4426 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue @@ -7,7 +7,6 @@ import { toCurrency, toDate } from 'src/filters'; import VnLv from 'src/components/ui/VnLv.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; -import filter from './InvoiceInFilter.js'; import InvoiceInDescriptorMenu from './InvoiceInDescriptorMenu.vue'; const $props = defineProps({ id: { type: Number, default: null } }); @@ -17,10 +16,33 @@ const { t } = useI18n(); const cardDescriptorRef = ref(); const entityId = computed(() => $props.id || +currentRoute.value.params.id); const totalAmount = ref(); -const config = ref(); -const cplusRectificationTypes = ref([]); -const siiTypeInvoiceIns = ref([]); -const invoiceCorrectionTypes = ref([]); + +const filter = { + include: [ + { + relation: 'supplier', + scope: { + include: { + relation: 'contacts', + scope: { + where: { + email: { neq: null }, + }, + }, + }, + }, + }, + { + relation: 'invoiceInDueDay', + }, + { + relation: 'company', + }, + { + relation: 'currency', + }, + ], +}; const invoiceInCorrection = reactive({ correcting: [], corrected: null }); const routes = reactive({ getSupplier: (id) => { @@ -90,6 +112,7 @@ async function setInvoiceCorrection(id) { <template> <CardDescriptor ref="cardDescriptorRef" + module="InvoiceIn" data-key="InvoiceIn" :url="`InvoiceIns/${entityId}`" :filter="filter" diff --git a/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue b/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue index 8b039ec27..c3ab635c8 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue @@ -186,7 +186,7 @@ const createInvoiceInCorrection = async () => { clickable @click="book(entityId)" > - <QItemSection>{{ t('invoiceIn.descriptorMenu.book') }}</QItemSection> + <QItemSection>{{ t('invoiceIn.descriptorMenu.toBook') }}</QItemSection> </QItem> </template> </InvoiceInToBook> @@ -197,7 +197,7 @@ const createInvoiceInCorrection = async () => { @click="triggerMenu('unbook')" > <QItemSection> - {{ t('invoiceIn.descriptorMenu.unbook') }} + {{ t('invoiceIn.descriptorMenu.toUnbook') }} </QItemSection> </QItem> <QItem diff --git a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue index 20cc1cc71..23387ff74 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed, onBeforeMount } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; @@ -11,7 +11,6 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import useNotify from 'src/composables/useNotify.js'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; -import { toCurrency } from 'filters/index'; const route = useRoute(); const { notify } = useNotify(); @@ -25,7 +24,7 @@ const invoiceInFormRef = ref(); const invoiceId = +route.params.id; const filter = { where: { invoiceInFk: invoiceId } }; const areRows = ref(false); -const totals = ref(); + const columns = computed(() => [ { name: 'duedate', @@ -64,8 +63,6 @@ const columns = computed(() => [ }, ]); -const totalAmount = computed(() => getTotal(invoiceInFormRef.value.formData, 'amount')); - const isNotEuro = (code) => code != 'EUR'; async function insert() { @@ -73,10 +70,6 @@ async function insert() { await invoiceInFormRef.value.reload(); notify(t('globals.dataSaved'), 'positive'); } - -onBeforeMount(async () => { - totals.value = (await axios.get(`InvoiceIns/${invoiceId}/getTotals`)).data; -}); </script> <template> <CrudModel @@ -151,7 +144,7 @@ onBeforeMount(async () => { <QTd /> <QTd /> <QTd> - {{ toCurrency(totalAmount) }} + {{ getTotal(rows, 'amount', { currency: 'default' }) }} </QTd> <QTd> <template v-if="isNotEuro(invoiceIn.currency.code)"> @@ -229,19 +222,10 @@ onBeforeMount(async () => { <QBtn color="primary" icon="add" - v-shortcut="'+'" + shortcut="+" size="lg" round - @click=" - () => { - if (!areRows) insert(); - else - invoiceInFormRef.insert({ - amount: (totals.totalTaxableBase - totalAmount).toFixed(2), - invoiceInFk: invoiceId, - }); - } - " + @click="!areRows ? insert() : invoiceInFormRef.insert()" /> </QPageSticky> </template> diff --git a/src/pages/InvoiceIn/Card/InvoiceInFilter.js b/src/pages/InvoiceIn/Card/InvoiceInFilter.js deleted file mode 100644 index 6df8b5830..000000000 --- a/src/pages/InvoiceIn/Card/InvoiceInFilter.js +++ /dev/null @@ -1,33 +0,0 @@ -export default { - include: [ - { - relation: 'supplier', - scope: { - include: { - relation: 'contacts', - scope: { where: { email: { neq: null } } }, - }, - }, - }, - { relation: 'invoiceInDueDay' }, - { relation: 'company' }, - { relation: 'currency' }, - { - relation: 'dms', - scope: { - fields: [ - 'dmsTypeFk', - 'reference', - 'hardCopyNumber', - 'workerFk', - 'description', - 'hasFile', - 'file', - 'created', - 'companyFk', - 'warehouseFk', - ], - }, - }, - ], -}; diff --git a/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue b/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue index 6f8642313..e529ea6cd 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue @@ -218,7 +218,7 @@ const columns = computed(() => [ <QBtn color="primary" icon="add" - v-shortcut="'+'" + shortcut="+" size="lg" round @click="invoiceInFormRef.insert()" diff --git a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue index d358601d3..e546638f2 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue @@ -193,7 +193,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; <InvoiceIntoBook> <template #content="{ book }"> <QBtn - :label="t('Book')" + :label="t('To book')" color="orange-11" text-color="black" @click="book(entityId)" @@ -224,7 +224,10 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; </span> </template> </VnLv> - <VnLv :label="t('invoiceIn.supplierRef')" :value="entity.supplierRef" /> + <VnLv + :label="t('invoiceIn.list.supplierRef')" + :value="entity.supplierRef" + /> <VnLv :label="t('invoiceIn.summary.currency')" :value="entity.currency?.code" @@ -354,7 +357,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; entity.totals.totalTaxableBaseForeignValue && toCurrency( entity.totals.totalTaxableBaseForeignValue, - currency, + currency ) }}</QTd> </QTr> @@ -389,7 +392,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; entity.totals.totalDueDayForeignValue && toCurrency( entity.totals.totalDueDayForeignValue, - currency, + currency ) }} </QTd> @@ -469,5 +472,5 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; Search invoice: Buscar factura recibida You can search by invoice reference: Puedes buscar por referencia de la factura Totals: Totales - Book: Contabilizar + To book: Contabilizar </i18n> diff --git a/src/pages/InvoiceIn/Card/InvoiceInVat.vue b/src/pages/InvoiceIn/Card/InvoiceInVat.vue index e77453bc0..f99e060b8 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInVat.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInVat.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed, nextTick } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useArrayData } from 'src/composables/useArrayData'; @@ -25,6 +25,7 @@ const sageTaxTypes = ref([]); const sageTransactionTypes = ref([]); const rowsSelected = ref([]); const invoiceInFormRef = ref(); +const expenseRef = ref(); defineProps({ actionIcon: { @@ -96,20 +97,6 @@ const columns = computed(() => [ }, ]); -const taxableBaseTotal = computed(() => { - return getTotal(invoiceInFormRef.value.formData, 'taxableBase'); -}); - -const taxRateTotal = computed(() => { - return getTotal(invoiceInFormRef.value.formData, null, { - cb: taxRate, - }); -}); - -const combinedTotal = computed(() => { - return +taxableBaseTotal.value + +taxRateTotal.value; -}); - const filter = { fields: [ 'id', @@ -130,7 +117,7 @@ const isNotEuro = (code) => code != 'EUR'; function taxRate(invoiceInTax) { const sageTaxTypeId = invoiceInTax.taxTypeSageFk; const taxRateSelection = sageTaxTypes.value.find( - (transaction) => transaction.id == sageTaxTypeId, + (transaction) => transaction.id == sageTaxTypeId ); const taxTypeSage = taxRateSelection?.rate ?? 0; const taxableBase = invoiceInTax?.taxableBase ?? 0; @@ -138,26 +125,35 @@ function taxRate(invoiceInTax) { return ((taxTypeSage / 100) * taxableBase).toFixed(2); } -function autocompleteExpense(evt, row, col, ref) { +function autocompleteExpense(evt, row, col) { const val = evt.target.value; if (!val) return; const param = isNaN(val) ? row[col.model] : val; const lookup = expenses.value.find( - ({ id }) => id == useAccountShortToStandard(param), + ({ id }) => id == useAccountShortToStandard(param) ); - ref.vnSelectDialogRef.vnSelectRef.toggleOption(lookup); + expenseRef.value.vnSelectDialogRef.vnSelectRef.toggleOption(lookup); } -function setCursor(ref) { - nextTick(() => { - const select = ref.vnSelectDialogRef - ? ref.vnSelectDialogRef.vnSelectRef - : ref.vnSelectRef; - select.$el.querySelector('input').setSelectionRange(0, 0); +const taxableBaseTotal = computed(() => { + return getTotal(invoiceInFormRef.value.formData, 'taxableBase', ); +}); + +const taxRateTotal = computed(() => { + return getTotal(invoiceInFormRef.value.formData, null, { + cb: taxRate, }); -} +}); + + +const combinedTotal = computed(() => { + return +taxableBaseTotal.value + +taxRateTotal.value; +}); + + + </script> <template> <FetchData @@ -195,24 +191,14 @@ function setCursor(ref) { <template #body-cell-expense="{ row, col }"> <QTd> <VnSelectDialog - :ref="`expenseRef-${row.$index}`" + ref="expenseRef" v-model="row[col.model]" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'name']" :tooltip="t('Create a new expense')" - @keydown.tab=" - autocompleteExpense( - $event, - row, - col, - $refs[`expenseRef-${row.$index}`], - ) - " - @update:model-value=" - setCursor($refs[`expenseRef-${row.$index}`]) - " + @keydown.tab="autocompleteExpense($event, row, col)" > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -228,7 +214,7 @@ function setCursor(ref) { </QTd> </template> <template #body-cell-taxablebase="{ row }"> - <QTd shrink> + <QTd> <VnInputNumber clear-icon="close" v-model="row.taxableBase" @@ -239,16 +225,12 @@ function setCursor(ref) { <template #body-cell-sageiva="{ row, col }"> <QTd> <VnSelect - :ref="`sageivaRef-${row.$index}`" v-model="row[col.model]" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'vat']" data-cy="vat-sageiva" - @update:model-value=" - setCursor($refs[`sageivaRef-${row.$index}`]) - " > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -266,15 +248,11 @@ function setCursor(ref) { <template #body-cell-sagetransaction="{ row, col }"> <QTd> <VnSelect - :ref="`sagetransactionRef-${row.$index}`" v-model="row[col.model]" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'transaction']" - @update:model-value=" - setCursor($refs[`sagetransactionRef-${row.$index}`]) - " > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -292,7 +270,7 @@ function setCursor(ref) { </QTd> </template> <template #body-cell-foreignvalue="{ row }"> - <QTd shrink> + <QTd> <VnInputNumber :class="{ 'no-pointer-events': !isNotEuro(currency), @@ -305,7 +283,7 @@ function setCursor(ref) { row.taxableBase = await getExchange( val, row.currencyFk, - invoiceIn.issued, + invoiceIn.issued ); } " @@ -448,7 +426,7 @@ function setCursor(ref) { color="primary" icon="add" size="lg" - v-shortcut="'+'" + shortcut="+" round @click="invoiceInFormRef.insert()" > diff --git a/src/pages/InvoiceIn/InvoiceInList.vue b/src/pages/InvoiceIn/InvoiceInList.vue index 0960d0d6c..e1723e3b1 100644 --- a/src/pages/InvoiceIn/InvoiceInList.vue +++ b/src/pages/InvoiceIn/InvoiceInList.vue @@ -29,7 +29,6 @@ const cols = computed(() => [ name: 'isBooked', label: t('invoiceIn.isBooked'), columnFilter: false, - component: 'checkbox', }, { align: 'left', @@ -57,7 +56,7 @@ const cols = computed(() => [ { align: 'left', name: 'supplierRef', - label: t('invoiceIn.supplierRef'), + label: t('invoiceIn.list.supplierRef'), }, { align: 'left', @@ -178,7 +177,7 @@ const cols = computed(() => [ :required="true" /> <VnInput - :label="t('invoiceIn.supplierRef')" + :label="t('invoiceIn.list.supplierRef')" v-model="data.supplierRef" /> <VnSelect diff --git a/src/pages/InvoiceIn/InvoiceInToBook.vue b/src/pages/InvoiceIn/InvoiceInToBook.vue index 5bdbe197b..95ce8155a 100644 --- a/src/pages/InvoiceIn/InvoiceInToBook.vue +++ b/src/pages/InvoiceIn/InvoiceInToBook.vue @@ -4,7 +4,6 @@ import { useQuasar } from 'quasar'; import { useI18n } from 'vue-i18n'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; import { useArrayData } from 'src/composables/useArrayData'; -import qs from 'qs'; const { notify, dialog } = useQuasar(); const { t } = useI18n(); @@ -13,51 +12,29 @@ defineExpose({ checkToBook }); const { store } = useArrayData(); async function checkToBook(id) { - let messages = []; - - const hasProblemWithTax = ( - await axios.get('InvoiceInTaxes/count', { - params: { - where: JSON.stringify({ - invoiceInFk: id, - or: [{ taxTypeSageFk: null }, { transactionTypeSageFk: null }], - }), - }, - }) - ).data?.count; - - if (hasProblemWithTax) - messages.push(t('The VAT and Transaction fields have not been informed')); + let directBooking = true; const { data: totals } = await axios.get(`InvoiceIns/${id}/getTotals`); const taxableBaseNotEqualDueDay = totals.totalDueDay != totals.totalTaxableBase; const vatNotEqualDueDay = totals.totalDueDay != totals.totalVat; - if (taxableBaseNotEqualDueDay && vatNotEqualDueDay) - messages.push(t('The sum of the taxable bases does not match the due dates')); + if (taxableBaseNotEqualDueDay && vatNotEqualDueDay) directBooking = false; - const dueDaysCount = ( - await axios.get('InvoiceInDueDays/count', { - params: { - where: JSON.stringify({ - invoiceInFk: id, - dueDated: { gte: Date.vnNew() }, - }), - }, - }) - ).data?.count; + const { data: dueDaysCount } = await axios.get('InvoiceInDueDays/count', { + where: { + invoiceInFk: id, + dueDated: { gte: Date.vnNew() }, + }, + }); - if (dueDaysCount) messages.push(t('Some due dates are less than or equal to today')); + if (dueDaysCount) directBooking = false; - if (!messages.length) toBook(id); - else - dialog({ - component: VnConfirm, - componentProps: { - title: t('Are you sure you want to book this invoice?'), - message: messages.reduce((acc, msg) => `${acc}<p>${msg}</p>`, ''), - }, - }).onOk(() => toBook(id)); + if (directBooking) return toBook(id); + + dialog({ + component: VnConfirm, + componentProps: { title: t('Are you sure you want to book this invoice?') }, + }).onOk(async () => await toBook(id)); } async function toBook(id) { @@ -82,7 +59,4 @@ async function toBook(id) { es: Are you sure you want to book this invoice?: ¿Estás seguro de querer asentar esta factura? It was not able to book the invoice: No se pudo contabilizar la factura - Some due dates are less than or equal to today: Algún vencimiento tiene una fecha menor o igual que hoy - The sum of the taxable bases does not match the due dates: La suma de las bases imponibles no coincide con la de los vencimientos - The VAT and Transaction fields have not been informed: No se han informado los campos de iva y/o transacción </i18n> diff --git a/src/pages/InvoiceIn/locale/en.yml b/src/pages/InvoiceIn/locale/en.yml index 548e6c201..6b21b316b 100644 --- a/src/pages/InvoiceIn/locale/en.yml +++ b/src/pages/InvoiceIn/locale/en.yml @@ -3,10 +3,10 @@ invoiceIn: searchInfo: Search incoming invoices by ID or supplier fiscal name serial: Serial isBooked: Is booked - supplierRef: Invoice nº list: ref: Reference supplier: Supplier + supplierRef: Supplier ref. file: File issued: Issued dueDated: Due dated @@ -19,6 +19,8 @@ invoiceIn: unbook: Unbook delete: Delete clone: Clone + toBook: To book + toUnbook: To unbook deleteInvoice: Delete invoice invoiceDeleted: invoice deleted cloneInvoice: Clone invoice @@ -68,3 +70,4 @@ invoiceIn: isBooked: Is booked account: Ledger account correctingFk: Rectificative + \ No newline at end of file diff --git a/src/pages/InvoiceIn/locale/es.yml b/src/pages/InvoiceIn/locale/es.yml index 142d95f92..3f27c895c 100644 --- a/src/pages/InvoiceIn/locale/es.yml +++ b/src/pages/InvoiceIn/locale/es.yml @@ -3,10 +3,10 @@ invoiceIn: searchInfo: Buscar facturas recibidas por ID o nombre fiscal del proveedor serial: Serie isBooked: Contabilizada - supplierRef: Nº factura list: ref: Referencia supplier: Proveedor + supplierRef: Ref. proveedor issued: F. emisión dueDated: F. vencimiento file: Fichero @@ -15,10 +15,12 @@ invoiceIn: descriptor: ticketList: Listado de tickets descriptorMenu: - book: Contabilizar - unbook: Descontabilizar + book: Asentar + unbook: Desasentar delete: Eliminar clone: Clonar + toBook: Contabilizar + toUnbook: Descontabilizar deleteInvoice: Eliminar factura invoiceDeleted: Factura eliminada cloneInvoice: Clonar factura @@ -66,3 +68,4 @@ invoiceIn: isBooked: Contabilizada account: Cuenta contable correctingFk: Rectificativa + diff --git a/src/pages/InvoiceOut/Card/InvoiceOutCard.vue b/src/pages/InvoiceOut/Card/InvoiceOutCard.vue index a50c9d247..93e3fe042 100644 --- a/src/pages/InvoiceOut/Card/InvoiceOutCard.vue +++ b/src/pages/InvoiceOut/Card/InvoiceOutCard.vue @@ -1,13 +1,11 @@ <script setup> import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue'; import VnCardBeta from 'components/common/VnCardBeta.vue'; -import filter from './InvoiceOutFilter.js'; </script> <template> <VnCardBeta data-key="InvoiceOut" - url="InvoiceOuts" - :filter="filter" + base-url="InvoiceOuts" :descriptor="InvoiceOutDescriptor" /> </template> diff --git a/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue b/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue index dfaf6c109..209f1531e 100644 --- a/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue +++ b/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue @@ -8,8 +8,8 @@ import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy import VnLv from 'src/components/ui/VnLv.vue'; import InvoiceOutDescriptorMenu from './InvoiceOutDescriptorMenu.vue'; +import useCardDescription from 'src/composables/useCardDescription'; import { toCurrency, toDate } from 'src/filters'; -import filter from './InvoiceOutFilter.js'; const $props = defineProps({ id: { @@ -26,20 +26,42 @@ const entityId = computed(() => { return $props.id || route.params.id; }); +const filter = { + include: [ + { + relation: 'company', + scope: { + fields: ['id', 'code'], + }, + }, + { + relation: 'client', + scope: { + fields: ['id', 'name', 'email'], + }, + }, + ], +}; + const descriptor = ref(); function ticketFilter(invoice) { return JSON.stringify({ refFk: invoice.ref }); } +const data = ref(useCardDescription()); +const setData = (entity) => (data.value = useCardDescription(entity.ref, entity.id)); </script> <template> <CardDescriptor ref="descriptor" + module="InvoiceOut" :url="`InvoiceOuts/${entityId}`" :filter="filter" - title="ref" - data-key="InvoiceOut" + :title="data.title" + :subtitle="data.subtitle" + @on-fetch="setData" + data-key="invoiceOutData" width="lg-width" > <template #menu="{ entity, menuRef }"> diff --git a/src/pages/InvoiceOut/Card/InvoiceOutFilter.js b/src/pages/InvoiceOut/Card/InvoiceOutFilter.js deleted file mode 100644 index 48b20faf6..000000000 --- a/src/pages/InvoiceOut/Card/InvoiceOutFilter.js +++ /dev/null @@ -1,16 +0,0 @@ -export default { - include: [ - { - relation: 'company', - scope: { - fields: ['id', 'code'], - }, - }, - { - relation: 'client', - scope: { - fields: ['id', 'name', 'email'], - }, - }, - ], -}; diff --git a/src/pages/Item/components/CreateGenusForm.vue b/src/pages/Item/Card/CreateGenusForm.vue similarity index 100% rename from src/pages/Item/components/CreateGenusForm.vue rename to src/pages/Item/Card/CreateGenusForm.vue diff --git a/src/pages/Item/components/CreateSpecieForm.vue b/src/pages/Item/Card/CreateSpecieForm.vue similarity index 100% rename from src/pages/Item/components/CreateSpecieForm.vue rename to src/pages/Item/Card/CreateSpecieForm.vue diff --git a/src/pages/Item/Card/ItemBarcode.vue b/src/pages/Item/Card/ItemBarcode.vue index 590b524cd..6db5943c7 100644 --- a/src/pages/Item/Card/ItemBarcode.vue +++ b/src/pages/Item/Card/ItemBarcode.vue @@ -92,7 +92,7 @@ const submit = async (rows) => { class="cursor-pointer fill-icon-on-hover" color="primary" icon="add_circle" - v-shortcut="'+'" + shortcut="+" flat > <QTooltip> diff --git a/src/pages/Item/Card/ItemBasicData.vue b/src/pages/Item/Card/ItemBasicData.vue index df7e71684..4c96401f3 100644 --- a/src/pages/Item/Card/ItemBasicData.vue +++ b/src/pages/Item/Card/ItemBasicData.vue @@ -11,7 +11,6 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; import FilterItemForm from 'src/components/FilterItemForm.vue'; import CreateIntrastatForm from './CreateIntrastatForm.vue'; -import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const route = useRoute(); const { t } = useI18n(); @@ -55,8 +54,9 @@ const onIntrastatCreated = (response, formData) => { auto-load /> <FormModel + :url="`Items/${route.params.id}`" :url-update="`Items/${route.params.id}`" - model="Item" + model="item" auto-load :clear-store-on-unmount="false" > @@ -209,20 +209,30 @@ const onIntrastatCreated = (response, formData) => { /> </VnRow> <VnRow class="row q-gutter-md q-mb-md"> - <VnCheckbox - v-model="data.isFragile" - :label="t('item.basicData.isFragile')" - :info="t('item.basicData.isFragileTooltip')" - class="q-mr-sm" - size="xs" - /> - <VnCheckbox - v-model="data.isPhotoRequested" - :label="t('item.basicData.isPhotoRequested')" - :info="t('item.basicData.isPhotoRequestedTooltip')" - class="q-mr-sm" - size="xs" - /> + <div> + <QCheckbox + v-model="data.isFragile" + :label="t('item.basicData.isFragile')" + class="q-mr-sm" + /> + <QIcon name="info" class="cursor-pointer" size="xs"> + <QTooltip max-width="300px"> + {{ t('item.basicData.isFragileTooltip') }} + </QTooltip> + </QIcon> + </div> + <div> + <QCheckbox + v-model="data.isPhotoRequested" + :label="t('item.basicData.isPhotoRequested')" + class="q-mr-sm" + /> + <QIcon name="info" class="cursor-pointer" size="xs"> + <QTooltip> + {{ t('item.basicData.isPhotoRequestedTooltip') }} + </QTooltip> + </QIcon> + </div> </VnRow> <VnRow> <VnInput diff --git a/src/pages/Item/Card/ItemBotanical.vue b/src/pages/Item/Card/ItemBotanical.vue index a40d81589..4894d94fc 100644 --- a/src/pages/Item/Card/ItemBotanical.vue +++ b/src/pages/Item/Card/ItemBotanical.vue @@ -7,8 +7,8 @@ import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; -import CreateGenusForm from '../components/CreateGenusForm.vue'; -import CreateSpecieForm from '../components/CreateSpecieForm.vue'; +import CreateGenusForm from './CreateGenusForm.vue'; +import CreateSpecieForm from './CreateSpecieForm.vue'; const route = useRoute(); const { t } = useI18n(); diff --git a/src/pages/Item/Card/ItemCard.vue b/src/pages/Item/Card/ItemCard.vue index 610b77a02..2546982eb 100644 --- a/src/pages/Item/Card/ItemCard.vue +++ b/src/pages/Item/Card/ItemCard.vue @@ -5,7 +5,7 @@ import ItemDescriptor from './ItemDescriptor.vue'; <template> <VnCardBeta data-key="Item" - :url="`Items/${$route.params.id}/getCard`" + base-url="Items" :descriptor="ItemDescriptor" /> </template> diff --git a/src/pages/Item/Card/ItemDescriptor.vue b/src/pages/Item/Card/ItemDescriptor.vue index a4c58ef4b..c6fee8540 100644 --- a/src/pages/Item/Card/ItemDescriptor.vue +++ b/src/pages/Item/Card/ItemDescriptor.vue @@ -7,6 +7,7 @@ import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue'; +import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import { dashIfEmpty } from 'src/filters'; import { useArrayData } from 'src/composables/useArrayData'; @@ -34,10 +35,6 @@ const $props = defineProps({ type: Number, default: null, }, - proxyRender: { - type: Boolean, - default: false, - }, }); const route = useRoute(); @@ -58,8 +55,10 @@ onMounted(async () => { mounted.value = true; }); +const data = ref(useCardDescription()); const setData = async (entity) => { if (!entity) return; + data.value = useCardDescription(entity.name, entity.id); await updateStock(); }; @@ -91,7 +90,10 @@ const updateStock = async () => { <template> <CardDescriptor - data-key="Item" + data-key="ItemData" + module="Item" + :title="data.title" + :subtitle="data.subtitle" :summary="$props.summary" :url="`Items/${entityId}/getCard`" @on-fetch="setData" @@ -115,7 +117,7 @@ const updateStock = async () => { <template #value> <span class="link"> {{ entity.itemType?.worker?.user?.name }} - <WorkerDescriptorProxy :id="entity.itemType?.worker?.id ?? NaN" /> + <WorkerDescriptorProxy :id="entity.itemType?.worker?.id" /> </span> </template> </VnLv> @@ -150,7 +152,7 @@ const updateStock = async () => { </QCardActions> </template> <template #actions="{}"> - <QCardActions class="row justify-center" v-if="proxyRender"> + <QCardActions class="row justify-center"> <QBtn :to="{ name: 'ItemDiary', @@ -163,16 +165,6 @@ const updateStock = async () => { > <QTooltip>{{ t('item.descriptor.itemDiary') }}</QTooltip> </QBtn> - <QBtn - :to="{ - name: 'ItemLastEntries', - }" - size="md" - icon="vn:regentry" - color="primary" - > - <QTooltip>{{ t('item.descriptor.itemLastEntries') }}</QTooltip> - </QBtn> </QCardActions> </template> </CardDescriptor> diff --git a/src/pages/Item/Card/ItemDescriptorProxy.vue b/src/pages/Item/Card/ItemDescriptorProxy.vue index f686e8221..2ffc9080f 100644 --- a/src/pages/Item/Card/ItemDescriptorProxy.vue +++ b/src/pages/Item/Card/ItemDescriptorProxy.vue @@ -4,7 +4,7 @@ import ItemSummary from './ItemSummary.vue'; const $props = defineProps({ id: { - type: [Number, String], + type: Number, required: true, }, dated: { @@ -21,8 +21,9 @@ const $props = defineProps({ }, }); </script> + <template> - <QPopupProxy style="max-width: 10px"> + <QPopupProxy> <ItemDescriptor v-if="$props.id" :id="$props.id" @@ -30,7 +31,6 @@ const $props = defineProps({ :dated="dated" :sale-fk="saleFk" :warehouse-fk="warehouseFk" - :proxy-render="true" /> </QPopupProxy> </template> diff --git a/src/pages/Item/Card/ItemShelving.vue b/src/pages/Item/Card/ItemShelving.vue index b29e2a2a5..7ad60c9e0 100644 --- a/src/pages/Item/Card/ItemShelving.vue +++ b/src/pages/Item/Card/ItemShelving.vue @@ -110,16 +110,10 @@ const columns = computed(() => [ attrs: { inWhere: true }, align: 'left', }, - { - label: t('globals.visible'), - name: 'stock', - attrs: { inWhere: true }, - align: 'left', - }, ]); const totalLabels = computed(() => - rows.value.reduce((acc, row) => acc + row.stock / row.packing, 0).toFixed(2), + rows.value.reduce((acc, row) => acc + row.stock / row.packing, 0).toFixed(2) ); const removeLines = async () => { @@ -163,7 +157,7 @@ watchEffect(selectedRows); openConfirmationModal( t('shelvings.removeConfirmTitle'), t('shelvings.removeConfirmSubtitle'), - removeLines, + removeLines ) " > diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue index ab26b9cae..5a7d7f818 100644 --- a/src/pages/Item/Card/ItemTags.vue +++ b/src/pages/Item/Card/ItemTags.vue @@ -178,7 +178,7 @@ const insertTag = (rows) => { @click="insertTag(rows)" color="primary" icon="add" - v-shortcut="'+'" + shortcut="+" fab data-cy="createNewTag" > diff --git a/src/pages/Item/ItemFixedPrice.vue b/src/pages/Item/ItemFixedPrice.vue index fdfa1d3d1..1c4382fbd 100644 --- a/src/pages/Item/ItemFixedPrice.vue +++ b/src/pages/Item/ItemFixedPrice.vue @@ -65,19 +65,10 @@ const columns = computed(() => [ name: 'name', ...defaultColumnAttrs, create: true, - columnFilter: { - component: 'select', - attrs: { - url: 'Items', - fields: ['id', 'name', 'subName'], - optionLabel: 'name', - optionValue: 'name', - uppercase: false, - }, - }, }, { label: t('item.fixedPrice.groupingPrice'), + field: 'rate2', name: 'rate2', ...defaultColumnAttrs, component: 'input', @@ -85,6 +76,7 @@ const columns = computed(() => [ }, { label: t('item.fixedPrice.packingPrice'), + field: 'rate3', name: 'rate3', ...defaultColumnAttrs, component: 'input', @@ -93,6 +85,7 @@ const columns = computed(() => [ { label: t('item.fixedPrice.minPrice'), + field: 'minPrice', name: 'minPrice', ...defaultColumnAttrs, component: 'input', @@ -115,6 +108,7 @@ const columns = computed(() => [ }, { label: t('item.fixedPrice.ended'), + field: 'ended', name: 'ended', ...defaultColumnAttrs, columnField: { @@ -130,6 +124,7 @@ const columns = computed(() => [ { label: t('globals.warehouse'), + field: 'warehouseFk', name: 'warehouseFk', ...defaultColumnAttrs, columnClass: 'shrink', @@ -420,6 +415,7 @@ function handleOnDataSave({ CrudModelRef }) { 'row-key': 'id', selection: 'multiple', }" + :use-model="true" v-model:selected="rowsSelected" :create-as-dialog="false" :create="{ diff --git a/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue b/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue index 475dffd8b..b4032ff8a 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue @@ -40,7 +40,12 @@ const itemPackingTypesOptions = ref([]); }" auto-load /> - <FormModel :url-update="`ItemTypes/${route.params.id}`" model="ItemType" auto-load> + <FormModel + :url="`ItemTypes/${route.params.id}`" + :url-update="`ItemTypes/${route.params.id}`" + model="itemTypeBasicData" + auto-load + > <template #form="{ data }"> <VnRow> <VnInput v-model="data.code" :label="t('itemType.shared.code')" /> diff --git a/src/pages/Item/ItemType/Card/ItemTypeCard.vue b/src/pages/Item/ItemType/Card/ItemTypeCard.vue index 84e810de5..fa51e428e 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeCard.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeCard.vue @@ -1,14 +1,12 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import ItemTypeDescriptor from 'src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue'; -import filter from './ItemTypeFilter.js'; </script> <template> <VnCardBeta - data-key="ItemType" - url="ItemTypes" - :filter="filter" + data-key="ItemTypeSummary" + base-url="ItemTypes" :descriptor="ItemTypeDescriptor" /> </template> diff --git a/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue b/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue index 725fb30aa..09d3dbce5 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue @@ -1,11 +1,12 @@ <script setup> -import { computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import filter from './ItemTypeFilter.js'; +import useCardDescription from 'src/composables/useCardDescription'; const $props = defineProps({ id: { @@ -19,31 +20,46 @@ const $props = defineProps({ }); const route = useRoute(); +const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); + +const itemTypeFilter = { + include: [ + { relation: 'worker' }, + { relation: 'category' }, + { relation: 'itemPackingType' }, + { relation: 'temperature' }, + ], +}; + +const data = ref(useCardDescription()); +const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); </script> + <template> <CardDescriptor + module="ItemType" :url="`ItemTypes/${entityId}`" - :filter="filter" - title="code" - data-key="ItemType" + :filter="itemTypeFilter" + :title="data.title" + :subtitle="data.subtitle" + data-key="itemTypeDescriptor" + @on-fetch="setData" > <template #body="{ entity }"> - <VnLv :label="$t('itemType.shared.code')" :value="entity.code" /> - <VnLv :label="$t('itemType.shared.name')" :value="entity.name" /> - <VnLv :label="$t('itemType.shared.worker')"> + <VnLv :label="t('itemType.shared.code')" :value="entity.code" /> + <VnLv :label="t('itemType.shared.name')" :value="entity.name" /> + <VnLv :label="t('itemType.shared.worker')"> <template #value> <span class="link">{{ entity.worker?.firstName }}</span> <WorkerDescriptorProxy :id="entity.worker?.id" /> </template> </VnLv> - <VnLv - :label="$t('itemType.shared.category')" - :value="entity.category?.name" - /> + <VnLv :label="t('itemType.shared.category')" :value="entity.category?.name" /> </template> </CardDescriptor> </template> + diff --git a/src/pages/Item/ItemType/Card/ItemTypeFilter.js b/src/pages/Item/ItemType/Card/ItemTypeFilter.js deleted file mode 100644 index 5651d368d..000000000 --- a/src/pages/Item/ItemType/Card/ItemTypeFilter.js +++ /dev/null @@ -1,8 +0,0 @@ -export default { - include: [ - { relation: 'worker' }, - { relation: 'category' }, - { relation: 'itemPackingType' }, - { relation: 'temperature' }, - ], -}; diff --git a/src/pages/Item/ItemType/Card/ItemTypeSummary.vue b/src/pages/Item/ItemType/Card/ItemTypeSummary.vue index 3b63c4b63..9ba774ca4 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeSummary.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeSummary.vue @@ -3,7 +3,7 @@ import { ref, computed, onUpdated } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import filter from './ItemTypeFilter.js'; + import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnToSummary from 'src/components/ui/VnToSummary.vue'; @@ -21,6 +21,15 @@ const $props = defineProps({ }, }); +const itemTypeFilter = { + include: [ + { relation: 'worker' }, + { relation: 'category' }, + { relation: 'itemPackingType' }, + { relation: 'temperature' }, + ], +}; + const entityId = computed(() => $props.id || route.params.id); const summaryRef = ref(); const itemType = ref(); @@ -34,8 +43,8 @@ async function setItemTypeData(data) { <CardSummary ref="summaryRef" :url="`ItemTypes/${entityId}`" - data-key="ItemType" - :filter="filter" + data-key="ItemTypeSummary" + :filter="itemTypeFilter" @on-fetch="(data) => setItemTypeData(data)" class="full-width" > diff --git a/src/pages/Item/components/ItemProposal.vue b/src/pages/Item/components/ItemProposal.vue deleted file mode 100644 index d2dbea7b3..000000000 --- a/src/pages/Item/components/ItemProposal.vue +++ /dev/null @@ -1,332 +0,0 @@ -<script setup> -import { ref, computed } from 'vue'; -import { useI18n } from 'vue-i18n'; -import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; -import { toCurrency } from 'filters/index'; -import VnStockValueDisplay from 'src/components/ui/VnStockValueDisplay.vue'; -import VnTable from 'src/components/VnTable/VnTable.vue'; -import axios from 'axios'; -import notifyResults from 'src/utils/notifyResults'; -import FetchData from 'components/FetchData.vue'; - -const MATCH = 'match'; - -const { t } = useI18n(); -const $props = defineProps({ - itemLack: { - type: Object, - required: true, - default: () => {}, - }, - replaceAction: { - type: Boolean, - required: false, - default: false, - }, - sales: { - type: Array, - required: false, - default: () => [], - }, -}); -const proposalSelected = ref([]); -const ticketConfig = ref({}); -const proposalTableRef = ref(null); - -const sale = computed(() => $props.sales[0]); -const saleFk = computed(() => sale.value.saleFk); -const filter = computed(() => ({ - itemFk: $props.itemLack.itemFk, - sales: saleFk.value, -})); - -const defaultColumnAttrs = { - align: 'center', - sortable: false, -}; -const emit = defineEmits(['onDialogClosed', 'itemReplaced']); - -const conditionalValuePrice = (price) => - price > 1 + ticketConfig.value.lackAlertPrice / 100 ? 'match' : 'not-match'; - -const columns = computed(() => [ - { - ...defaultColumnAttrs, - label: t('proposal.available'), - name: 'available', - field: 'available', - columnFilter: { - component: 'input', - type: 'number', - columnClass: 'shrink', - }, - columnClass: 'shrink', - }, - { - ...defaultColumnAttrs, - label: t('proposal.counter'), - name: 'counter', - field: 'counter', - columnClass: 'shrink', - style: 'max-width: 75px', - columnFilter: { - component: 'input', - type: 'number', - columnClass: 'shrink', - }, - }, - - { - align: 'left', - sortable: true, - label: t('proposal.longName'), - name: 'longName', - field: 'longName', - columnClass: 'expand', - }, - { - align: 'left', - sortable: true, - label: t('item.list.color'), - name: 'tag5', - field: 'value5', - columnClass: 'expand', - }, - { - align: 'left', - sortable: true, - label: t('item.list.stems'), - name: 'tag6', - field: 'value6', - columnClass: 'expand', - }, - { - align: 'left', - sortable: true, - label: t('item.list.producer'), - name: 'tag7', - field: 'value7', - columnClass: 'expand', - }, - - { - ...defaultColumnAttrs, - label: t('proposal.price2'), - name: 'price2', - style: 'max-width: 75px', - columnFilter: { - component: 'input', - type: 'number', - columnClass: 'shrink', - }, - }, - { - ...defaultColumnAttrs, - label: t('proposal.minQuantity'), - name: 'minQuantity', - field: 'minQuantity', - style: 'max-width: 75px', - columnFilter: { - component: 'input', - type: 'number', - columnClass: 'shrink', - }, - }, - { - ...defaultColumnAttrs, - label: t('proposal.located'), - name: 'located', - field: 'located', - }, - { - align: 'right', - label: '', - name: 'tableActions', - actions: [ - { - title: t('Replace'), - icon: 'change_circle', - show: (row) => isSelectionAvailable(row), - action: change, - isPrimary: true, - }, - ], - }, -]); - -function extractMatchValues(obj) { - return Object.keys(obj) - .filter((key) => key.startsWith(MATCH)) - .map((key) => parseInt(key.replace(MATCH, ''), 10)); -} -const gradientStyle = (value) => { - let color = 'white'; - const perc = parseFloat(value); - switch (true) { - case perc >= 0 && perc < 33: - color = 'primary'; - break; - case perc >= 33 && perc < 66: - color = 'warning'; - break; - - default: - color = 'secondary'; - break; - } - return color; -}; -const statusConditionalValue = (row) => { - const matches = extractMatchValues(row); - const value = matches.reduce((acc, i) => acc + row[`${MATCH}${i}`], 0); - return 100 * (value / matches.length); -}; - -const isSelectionAvailable = (itemProposal) => { - const { price2 } = itemProposal; - const salePrice = sale.value.price; - const byPrice = (100 * price2) / salePrice > ticketConfig.value.lackAlertPrice; - if (byPrice) { - return byPrice; - } - const byQuantity = - (100 * itemProposal.available) / Math.abs($props.itemLack.lack) < - ticketConfig.value.lackAlertPrice; - return byQuantity; -}; - -async function change({ itemFk: substitutionFk }) { - try { - const promises = $props.sales.map(({ saleFk, quantity }) => { - const params = { - saleFk, - substitutionFk, - quantity, - }; - return axios.post('Sales/replaceItem', params); - }); - const results = await Promise.allSettled(promises); - - notifyResults(results, 'saleFk'); - emit('itemReplaced', { - type: 'refresh', - quantity: quantity.value, - itemProposal: proposalSelected.value[0], - }); - proposalSelected.value = []; - } catch (error) { - console.error(error); - } -} - -async function handleTicketConfig(data) { - ticketConfig.value = data[0]; -} -</script> -<template> - <FetchData - url="TicketConfigs" - :filter="{ fields: ['lackAlertPrice'] }" - @on-fetch="handleTicketConfig" - auto-load - /> - - <VnTable - v-if="ticketConfig" - auto-load - data-cy="proposalTable" - ref="proposalTableRef" - data-key="ItemsGetSimilar" - url="Items/getSimilar" - :user-filter="filter" - :columns="columns" - class="full-width q-mt-md" - row-key="id" - :row-click="change" - :is-editable="false" - :right-search="false" - :without-header="true" - :disable-option="{ card: true, table: true }" - > - <template #column-longName="{ row }"> - <QTd - class="flex" - style="max-width: 100%; flex-shrink: 50px; flex-wrap: nowrap" - > - <div - class="middle full-width" - :class="[`proposal-${gradientStyle(statusConditionalValue(row))}`]" - > - <QTooltip> {{ statusConditionalValue(row) }}% </QTooltip> - </div> - <div style="flex: 2 0 100%; align-content: center"> - <div> - <span class="link">{{ row.longName }}</span> - <ItemDescriptorProxy :id="row.id" /> - </div> - </div> - </QTd> - </template> - <template #column-tag5="{ row }"> - <span :class="{ match: !row.match5 }">{{ row.value5 }}</span> - </template> - <template #column-tag6="{ row }"> - <span :class="{ match: !row.match6 }">{{ row.value6 }}</span> - </template> - <template #column-tag7="{ row }"> - <span :class="{ match: !row.match7 }">{{ row.value7 }}</span> - </template> - <template #column-counter="{ row }"> - <span - :class="{ - match: row.counter === 1, - 'not-match': row.counter !== 1, - }" - >{{ row.counter }}</span - > - </template> - <template #column-minQuantity="{ row }"> - {{ row.minQuantity }} - </template> - <template #column-price2="{ row }"> - <div class="flex column items-center content-center"> - <VnStockValueDisplay :value="(sales[0].price - row.price2) / 100" /> - <span :class="[conditionalValuePrice(row.price2)]">{{ - toCurrency(row.price2) - }}</span> - </div> - </template> - </VnTable> -</template> -<style lang="scss" scoped> -@import 'src/css/quasar.variables.scss'; -.middle { - float: left; - margin-right: 2px; - flex: 2 0 5px; -} -.match { - color: $negative; -} -.not-match { - color: inherit; -} -.proposal-warning { - background-color: $warning; -} -.proposal-secondary { - background-color: $secondary; -} -.proposal-primary { - background-color: $primary; -} -.text { - margin: 0.05rem; - padding: 1px; - border: 1px solid var(--vn-label-color); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: smaller; -} -</style> diff --git a/src/pages/Item/components/ItemProposalProxy.vue b/src/pages/Item/components/ItemProposalProxy.vue deleted file mode 100644 index 7da0ce398..000000000 --- a/src/pages/Item/components/ItemProposalProxy.vue +++ /dev/null @@ -1,56 +0,0 @@ -<script setup> -import ItemProposal from './ItemProposal.vue'; -import { useDialogPluginComponent } from 'quasar'; - -const $props = defineProps({ - itemLack: { - type: Object, - required: true, - default: () => {}, - }, - replaceAction: { - type: Boolean, - required: false, - default: false, - }, - sales: { - type: Array, - required: false, - default: () => [], - }, -}); -const { dialogRef } = useDialogPluginComponent(); -const emit = defineEmits([ - 'onDialogClosed', - 'itemReplaced', - ...useDialogPluginComponent.emits, -]); -defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.hide() }); -</script> -<template> - <QDialog ref="dialogRef" transition-show="scale" transition-hide="scale"> - <QCard class="dialog-width"> - <QCardSection class="row items-center q-pb-none"> - <span class="text-h6 text-grey">{{ $t('Item proposal') }}</span> - <QSpace /> - <QBtn icon="close" flat round dense v-close-popup /> - </QCardSection> - <QCardSection> - <ItemProposal - v-bind="$props" - @item-replaced=" - (data) => { - emit('itemReplaced', data); - dialogRef.hide(); - } - " - ></ItemProposal - ></QCardSection> - </QCard> - </QDialog> -</template> -<style lang="scss" scoped> -.dialog-width { - max-width: $width-lg; -} -</style> diff --git a/src/pages/Item/locale/en.yml b/src/pages/Item/locale/en.yml index 9d27fc96e..bc73abb12 100644 --- a/src/pages/Item/locale/en.yml +++ b/src/pages/Item/locale/en.yml @@ -112,7 +112,6 @@ item: available: Available warehouseText: 'Calculated on the warehouse of { warehouseName }' itemDiary: Item diary - itemLastEntries: Last entries producer: Producer clone: title: All its properties will be copied @@ -131,7 +130,6 @@ item: origin: Orig. userName: Buyer weight: Weight - color: Color weightByPiece: Weight/stem stemMultiplier: Multiplier producer: Producer @@ -217,24 +215,4 @@ item: specie: Specie search: 'Search item' searchInfo: 'You can search by id' - regularizeStock: Regularize stock -itemProposal: Items proposal -proposal: - difference: Difference - title: Items proposal - itemFk: Item - longName: Name - subName: Producer - value5: value5 - value6: value6 - value7: value7 - value8: value8 - available: Available - minQuantity: minQuantity - price2: Price - located: Located - counter: Counter - groupingPrice: Grouping Price - itemOldPrice: itemOld Price - status: State - quantityToReplace: Quanity to replace + regularizeStock: Regularize stock \ No newline at end of file diff --git a/src/pages/Item/locale/es.yml b/src/pages/Item/locale/es.yml index 935f5160b..dd5074f5f 100644 --- a/src/pages/Item/locale/es.yml +++ b/src/pages/Item/locale/es.yml @@ -118,7 +118,6 @@ item: available: Disponible warehouseText: 'Calculado sobre el almacén de { warehouseName }' itemDiary: Registro de compra-venta - itemLastEntries: Últimas entradas producer: Productor clone: title: Todas sus propiedades serán copiadas @@ -136,7 +135,6 @@ item: size: Medida origin: Orig. weight: Peso - color: Color weightByPiece: Peso/tallo userName: Comprador stemMultiplier: Multiplicador @@ -222,30 +220,5 @@ item: achieved: 'Conseguido' concept: 'Concepto' state: 'Estado' -itemProposal: Artículos similares -proposal: - substitutionAvailable: Sustitución disponible - notSubstitutionAvailableByPrice: Sustitución no disponible, 30% de diferencia por precio o cantidad - compatibility: Compatibilidad - title: Items de sustitución para los tickets seleccionados - itemFk: Item - longName: Nombre - subName: Productor - value5: value5 - value6: value6 - value7: value7 - value8: value8 - available: Disponible - minQuantity: Min. cantidad - price2: Precio - located: Ubicado - counter: Contador - difference: Diferencial - groupingPrice: Precio Grouping - itemOldPrice: Precio itemOld - status: Estado - quantityToReplace: Cantidad a reemplazar - replace: Sustituir - replaceAndConfirm: Sustituir y confirmar precio -search: 'Buscar artículo' -searchInfo: 'Puedes buscar por id' + search: 'Buscar artículo' + searchInfo: 'Puedes buscar por id' diff --git a/src/pages/Monitor/MonitorOrders.vue b/src/pages/Monitor/MonitorOrders.vue index 873f8abb4..4efab56fb 100644 --- a/src/pages/Monitor/MonitorOrders.vue +++ b/src/pages/Monitor/MonitorOrders.vue @@ -157,7 +157,7 @@ const openTab = (id) => openConfirmationModal( $t('globals.deleteConfirmTitle'), $t('salesOrdersTable.deleteConfirmMessage'), - removeOrders, + removeOrders ) " > diff --git a/src/pages/Monitor/locale/en.yml b/src/pages/Monitor/locale/en.yml index 496c8761a..21324087c 100644 --- a/src/pages/Monitor/locale/en.yml +++ b/src/pages/Monitor/locale/en.yml @@ -38,7 +38,6 @@ salesTicketsTable: payMethod: Pay method department: Department packing: ITP - hasItemLost: Item lost searchBar: label: Search tickets info: Search tickets by id or alias diff --git a/src/pages/Monitor/locale/es.yml b/src/pages/Monitor/locale/es.yml index f6a29879f..30afb1904 100644 --- a/src/pages/Monitor/locale/es.yml +++ b/src/pages/Monitor/locale/es.yml @@ -39,7 +39,6 @@ salesTicketsTable: payMethod: Método de pago department: Departamento packing: ITP - hasItemLost: Artículo perdido searchBar: label: Buscar tickets info: Buscar tickets por identificador o alias diff --git a/src/pages/Order/Card/CatalogFilterValueDialog.vue b/src/pages/Order/Card/CatalogFilterValueDialog.vue index d1bd48c9e..b91e7d229 100644 --- a/src/pages/Order/Card/CatalogFilterValueDialog.vue +++ b/src/pages/Order/Card/CatalogFilterValueDialog.vue @@ -110,7 +110,7 @@ const getSelectedTagValues = async (tag) => { </div> <QBtn icon="add_circle" - v-shortcut="'+'" + shortcut="+" flat class="filter-icon q-mb-md" size="md" diff --git a/src/pages/Order/Card/OrderBasicData.vue b/src/pages/Order/Card/OrderBasicData.vue index 9c02d7494..8594a05f4 100644 --- a/src/pages/Order/Card/OrderBasicData.vue +++ b/src/pages/Order/Card/OrderBasicData.vue @@ -14,6 +14,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; const { t } = useI18n(); const route = useRoute(); const state = useState(); +const ORDER_MODEL = 'order'; const isNew = Boolean(!route.params.id); const clientList = ref([]); @@ -31,7 +32,7 @@ const fetchAddressList = async (addressId) => { }); addressList.value = data; if (addressList.value?.length === 1) { - state.get('Order').addressFk = addressList.value[0].id; + state.get(ORDER_MODEL).addressFk = addressList.value[0].id; } }; @@ -90,8 +91,9 @@ const onClientChange = async (clientId) => { <VnSubToolbar v-if="isNew" /> <div class="q-pa-md"> <FormModel + :url="`Orders/${route.params.id}`" :url-update="`Orders/${route.params.id}/updateBasicData`" - model="Order" + :model="ORDER_MODEL" :filter="orderFilter" @on-fetch="fetchOrderDetails" auto-load diff --git a/src/pages/Order/Card/OrderCard.vue b/src/pages/Order/Card/OrderCard.vue index ad5c73a87..823815f59 100644 --- a/src/pages/Order/Card/OrderCard.vue +++ b/src/pages/Order/Card/OrderCard.vue @@ -1,14 +1,12 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import OrderDescriptor from 'pages/Order/Card/OrderDescriptor.vue'; -import filter from './OrderFilter.js'; </script> <template> <VnCardBeta data-key="Order" - url="Orders" - :filter="filter" + base-url="Orders" :descriptor="OrderDescriptor" /> </template> diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index 76e608983..262f503fd 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -184,7 +184,7 @@ function addOrder(value, field, params) { {{ t( categoryList.find((c) => c.id == customTag.value)?.name || - '', + '' ) }} </strong> @@ -296,7 +296,7 @@ function addOrder(value, field, params) { <template #append> <QBtn icon="add_circle" - v-shortcut="'+'" + shortcut="+" flat color="primary" size="md" diff --git a/src/pages/Order/Card/OrderCatalogItemDialog.vue b/src/pages/Order/Card/OrderCatalogItemDialog.vue index 766945e4d..77f6a8405 100644 --- a/src/pages/Order/Card/OrderCatalogItemDialog.vue +++ b/src/pages/Order/Card/OrderCatalogItemDialog.vue @@ -20,7 +20,7 @@ const props = defineProps({ }); const state = useState(); -const orderData = computed(() => state.get('Order')); +const orderData = computed(() => state.get('orderData')); const prices = ref((props.item.prices || []).map((item) => ({ ...item, quantity: 0 }))); const isLoading = ref(false); @@ -39,11 +39,11 @@ const addToOrder = async () => { }); const { data: orderTotal } = await axios.get( - `Orders/${Number(route.params.id)}/getTotal`, + `Orders/${Number(route.params.id)}/getTotal` ); state.set('orderTotal', orderTotal); - state.set('Order', { + state.set('orderData', { ...orderData.value, items, }); @@ -56,7 +56,7 @@ const canAddToOrder = () => { if (canAddToOrder) { const excedQuantity = prices.value.reduce( (acc, { quantity }) => acc + quantity, - 0, + 0 ); if (excedQuantity > props.item.available) { canAddToOrder = false; diff --git a/src/pages/Order/Card/OrderDescriptor.vue b/src/pages/Order/Card/OrderDescriptor.vue index 0d18864dc..0d5f0146f 100644 --- a/src/pages/Order/Card/OrderDescriptor.vue +++ b/src/pages/Order/Card/OrderDescriptor.vue @@ -4,7 +4,8 @@ import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { toCurrency, toDate } from 'src/filters'; import { useState } from 'src/composables/useState'; -import filter from './OrderFilter.js'; +import useCardDescription from 'src/composables/useCardDescription'; + import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import FetchData from 'components/FetchData.vue'; @@ -23,15 +24,44 @@ const $props = defineProps({ const route = useRoute(); const state = useState(); const { t } = useI18n(); +const data = ref(useCardDescription()); const getTotalRef = ref(); const entityId = computed(() => { return $props.id || route.params.id; }); +const filter = { + include: [ + { relation: 'agencyMode', scope: { fields: ['name'] } }, + { + relation: 'address', + scope: { fields: ['nickname'] }, + }, + { relation: 'rows', scope: { fields: ['id'] } }, + { + relation: 'client', + scope: { + fields: [ + 'salesPersonFk', + 'name', + 'isActive', + 'isFreezed', + 'isTaxDataChecked', + ], + include: { + relation: 'salesPersonUser', + scope: { fields: ['id', 'name'] }, + }, + }, + }, + ], +}; + const setData = (entity) => { if (!entity) return; getTotalRef.value && getTotalRef.value.fetch(); + data.value = useCardDescription(entity?.client?.name, entity?.id); state.set('orderTotal', total); }; @@ -57,9 +87,11 @@ const total = ref(0); ref="descriptor" :url="`Orders/${entityId}`" :filter="filter" - title="client.name" + module="Order" + :title="data.title" + :subtitle="data.subtitle" @on-fetch="setData" - data-key="Order" + data-key="orderData" > <template #body="{ entity }"> <VnLv diff --git a/src/pages/Order/Card/OrderFilter.js b/src/pages/Order/Card/OrderFilter.js deleted file mode 100644 index 3e521b92c..000000000 --- a/src/pages/Order/Card/OrderFilter.js +++ /dev/null @@ -1,26 +0,0 @@ -export default { - include: [ - { relation: 'agencyMode', scope: { fields: ['name'] } }, - { - relation: 'address', - scope: { fields: ['nickname'] }, - }, - { relation: 'rows', scope: { fields: ['id'] } }, - { - relation: 'client', - scope: { - fields: [ - 'salesPersonFk', - 'name', - 'isActive', - 'isFreezed', - 'isTaxDataChecked', - ], - include: { - relation: 'salesPersonUser', - scope: { fields: ['id', 'name'] }, - }, - }, - }, - ], -}; diff --git a/src/pages/Order/Card/OrderLines.vue b/src/pages/Order/Card/OrderLines.vue index 1b864de6f..cf219a244 100644 --- a/src/pages/Order/Card/OrderLines.vue +++ b/src/pages/Order/Card/OrderLines.vue @@ -21,7 +21,7 @@ const router = useRouter(); const route = useRoute(); const { t } = useI18n(); const quasar = useQuasar(); -const descriptorData = useArrayData('Order'); +const descriptorData = useArrayData('orderData'); const componentKey = ref(0); const tableLinesRef = ref(); const order = ref(); @@ -238,7 +238,7 @@ watch( lineFilter.value.where.orderFk = router.currentRoute.value.params.id; tableLinesRef.value.reload(); - }, + } ); </script> diff --git a/src/pages/Order/Card/OrderSummary.vue b/src/pages/Order/Card/OrderSummary.vue index a4bdb2881..a289688e4 100644 --- a/src/pages/Order/Card/OrderSummary.vue +++ b/src/pages/Order/Card/OrderSummary.vue @@ -27,7 +27,7 @@ const $props = defineProps({ const entityId = computed(() => $props.id || route.params.id); const summary = ref(); const quasar = useQuasar(); -const descriptorData = useArrayData('Order'); +const descriptorData = useArrayData('orderData'); const detailsColumns = ref([ { name: 'item', diff --git a/src/pages/Order/OrderList.vue b/src/pages/Order/OrderList.vue index 40990f329..21cb5ed7e 100644 --- a/src/pages/Order/OrderList.vue +++ b/src/pages/Order/OrderList.vue @@ -71,9 +71,8 @@ const columns = computed(() => [ format: (row) => row?.name, }, { - align: 'center', + align: 'left', name: 'isConfirmed', - component: 'checkbox', label: t('module.isConfirmed'), }, { @@ -96,9 +95,7 @@ const columns = computed(() => [ columnField: { component: null, }, - style: () => { - return { color: 'positive' }; - }, + style: 'color="positive"', }, { align: 'left', diff --git a/src/pages/Shelving/Parking/Card/ParkingBasicData.vue b/src/pages/Parking/Card/ParkingBasicData.vue similarity index 68% rename from src/pages/Shelving/Parking/Card/ParkingBasicData.vue rename to src/pages/Parking/Card/ParkingBasicData.vue index 3de358002..550a0684e 100644 --- a/src/pages/Shelving/Parking/Card/ParkingBasicData.vue +++ b/src/pages/Parking/Card/ParkingBasicData.vue @@ -1,11 +1,16 @@ <script setup> -import { ref } from 'vue'; +import { ref, computed } from 'vue'; +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; import VnRow from 'components/ui/VnRow.vue'; import FetchData from 'src/components/FetchData.vue'; import VnInput from 'src/components/common/VnInput.vue'; import FormModel from 'components/FormModel.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; +const { t } = useI18n(); +const route = useRoute(); +const parkingId = computed(() => route.params?.id || null); const sectors = ref([]); const sectorFilter = { fields: ['id', 'description'] }; @@ -22,21 +27,18 @@ const filter = { @on-fetch="(data) => (sectors = data)" auto-load /> - <FormModel model="Parking" auto-load> + <FormModel :url="`Parkings/${parkingId}`" model="parking" :filter="filter" auto-load> <template #form="{ data }"> <VnRow> - <VnInput v-model="data.code" :label="$t('globals.code')" /> - <VnInput - v-model="data.pickingOrder" - :label="$t('parking.pickingOrder')" - /> + <VnInput v-model="data.code" :label="t('globals.code')" /> + <VnInput v-model="data.pickingOrder" :label="t('parking.pickingOrder')" /> </VnRow> <VnRow> <VnSelect v-model="data.sectorFk" option-value="id" option-label="description" - :label="$t('parking.sector')" + :label="t('parking.sector')" :options="sectors" use-input input-debounce="0" diff --git a/src/pages/Shelving/Parking/Card/ParkingCard.vue b/src/pages/Parking/Card/ParkingCard.vue similarity index 53% rename from src/pages/Shelving/Parking/Card/ParkingCard.vue rename to src/pages/Parking/Card/ParkingCard.vue index b32c1b7d3..1cd2df7b7 100644 --- a/src/pages/Shelving/Parking/Card/ParkingCard.vue +++ b/src/pages/Parking/Card/ParkingCard.vue @@ -1,14 +1,12 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; -import ParkingDescriptor from 'pages/Shelving/Parking/Card/ParkingDescriptor.vue'; -import filter from './ParkingFilter.js'; +import ParkingDescriptor from 'pages/Parking/Card/ParkingDescriptor.vue'; </script> <template> <VnCardBeta data-key="Parking" - url="Parkings" - :filter="filter" + base-url="Parkings" :descriptor="ParkingDescriptor" /> </template> diff --git a/src/pages/Shelving/Parking/Card/ParkingDescriptor.vue b/src/pages/Parking/Card/ParkingDescriptor.vue similarity index 58% rename from src/pages/Shelving/Parking/Card/ParkingDescriptor.vue rename to src/pages/Parking/Card/ParkingDescriptor.vue index 46c9f8ea0..d36ea16fc 100644 --- a/src/pages/Shelving/Parking/Card/ParkingDescriptor.vue +++ b/src/pages/Parking/Card/ParkingDescriptor.vue @@ -1,9 +1,10 @@ <script setup> import { computed } from 'vue'; +import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; -import filter from './ParkingFilter.js'; + const props = defineProps({ id: { type: Number, @@ -12,11 +13,18 @@ const props = defineProps({ }, }); +const { t } = useI18n(); const route = useRoute(); const entityId = computed(() => props.id || route.params.id); + +const filter = { + fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'], + include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }], +}; </script> <template> <CardDescriptor + module="Parking" data-key="Parking" :url="`Parkings/${entityId}`" title="code" @@ -24,9 +32,9 @@ const entityId = computed(() => props.id || route.params.id); :to-module="{ name: 'ParkingList' }" > <template #body="{ entity }"> - <VnLv :label="$t('globals.code')" :value="entity.code" /> - <VnLv :label="$t('parking.pickingOrder')" :value="entity.pickingOrder" /> - <VnLv :label="$t('parking.sector')" :value="entity.sector?.description" /> + <VnLv :label="t('globals.code')" :value="entity.code" /> + <VnLv :label="t('parking.pickingOrder')" :value="entity.pickingOrder" /> + <VnLv :label="t('parking.sector')" :value="entity.sector?.description" /> </template> </CardDescriptor> </template> diff --git a/src/pages/Shelving/Parking/Card/ParkingLog.vue b/src/pages/Parking/Card/ParkingLog.vue similarity index 100% rename from src/pages/Shelving/Parking/Card/ParkingLog.vue rename to src/pages/Parking/Card/ParkingLog.vue diff --git a/src/pages/Shelving/Parking/Card/ParkingSummary.vue b/src/pages/Parking/Card/ParkingSummary.vue similarity index 100% rename from src/pages/Shelving/Parking/Card/ParkingSummary.vue rename to src/pages/Parking/Card/ParkingSummary.vue diff --git a/src/pages/Shelving/Parking/ParkingFilter.vue b/src/pages/Parking/ParkingFilter.vue similarity index 100% rename from src/pages/Shelving/Parking/ParkingFilter.vue rename to src/pages/Parking/ParkingFilter.vue diff --git a/src/pages/Shelving/Parking/ParkingList.vue b/src/pages/Parking/ParkingList.vue similarity index 90% rename from src/pages/Shelving/Parking/ParkingList.vue rename to src/pages/Parking/ParkingList.vue index fe6c93ba5..bce87126e 100644 --- a/src/pages/Shelving/Parking/ParkingList.vue +++ b/src/pages/Parking/ParkingList.vue @@ -9,7 +9,6 @@ import CardList from 'components/ui/CardList.vue'; import VnLv from 'components/ui/VnLv.vue'; import ParkingFilter from './ParkingFilter.vue'; import ParkingSummary from './Card/ParkingSummary.vue'; -import exprBuilder from './ParkingExprBuilder.js'; import VnSection from 'src/components/common/VnSection.vue'; const stateStore = useStateStore(); @@ -24,7 +23,19 @@ onUnmounted(() => (stateStore.rightDrawer = false)); const filter = { fields: ['id', 'sectorFk', 'code', 'pickingOrder'], }; + +function exprBuilder(param, value) { + switch (param) { + case 'code': + return { [param]: { like: `%${value}%` } }; + case 'sectorFk': + return { [param]: value }; + case 'search': + return { or: [{ code: { like: `%${value}%` } }, { id: value }] }; + } +} </script> + <template> <VnSection :data-key="dataKey" diff --git a/src/pages/Shelving/Parking/locale/en.yml b/src/pages/Parking/locale/en.yml similarity index 100% rename from src/pages/Shelving/Parking/locale/en.yml rename to src/pages/Parking/locale/en.yml diff --git a/src/pages/Shelving/Parking/locale/es.yml b/src/pages/Parking/locale/es.yml similarity index 100% rename from src/pages/Shelving/Parking/locale/es.yml rename to src/pages/Parking/locale/es.yml diff --git a/src/pages/Route/Agency/AgencyList.vue b/src/pages/Route/Agency/AgencyList.vue index 5c2904bf3..4322b9bc8 100644 --- a/src/pages/Route/Agency/AgencyList.vue +++ b/src/pages/Route/Agency/AgencyList.vue @@ -51,6 +51,7 @@ const columns = computed(() => [ name: 'isAnyVolumeAllowed', component: 'checkbox', cardVisible: true, + disable: true, }, { align: 'right', @@ -71,7 +72,7 @@ const columns = computed(() => [ :data-key :columns="columns" prefix="agency" - :right-filter="true" + :right-filter="false" :array-data-props="{ url: 'Agencies', order: 'name', @@ -82,7 +83,6 @@ const columns = computed(() => [ <VnTable :data-key :columns="columns" - is-editable="false" :right-search="false" :use-model="true" redirect="route/agency" diff --git a/src/pages/Route/Agency/Card/AgencyBasicData.vue b/src/pages/Route/Agency/Card/AgencyBasicData.vue index 4270b136c..599058b3e 100644 --- a/src/pages/Route/Agency/Card/AgencyBasicData.vue +++ b/src/pages/Route/Agency/Card/AgencyBasicData.vue @@ -21,7 +21,7 @@ const warehouses = ref([]); @on-fetch="(data) => (warehouses = data)" auto-load /> - <FormModel :update-url="`Agencies/${routeId}`" model="Agency" auto-load> + <FormModel :url="`Agencies/${routeId}`" model="agency" auto-load> <template #form="{ data }"> <VnRow> <VnInput v-model="data.name" :label="t('globals.name')" /> diff --git a/src/pages/Route/Agency/Card/AgencyCard.vue b/src/pages/Route/Agency/Card/AgencyCard.vue index 7dc31f8ba..35685790a 100644 --- a/src/pages/Route/Agency/Card/AgencyCard.vue +++ b/src/pages/Route/Agency/Card/AgencyCard.vue @@ -3,5 +3,5 @@ import AgencyDescriptor from 'pages/Route/Agency/Card/AgencyDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; </script> <template> - <VnCardBeta data-key="Agency" url="Agencies" :descriptor="AgencyDescriptor" /> + <VnCardBeta data-key="Agency" base-url="Agencies" :descriptor="AgencyDescriptor" /> </template> diff --git a/src/pages/Route/Agency/Card/AgencyDescriptor.vue b/src/pages/Route/Agency/Card/AgencyDescriptor.vue index a0472c6c3..b9772037c 100644 --- a/src/pages/Route/Agency/Card/AgencyDescriptor.vue +++ b/src/pages/Route/Agency/Card/AgencyDescriptor.vue @@ -22,6 +22,7 @@ const card = computed(() => store.data); </script> <template> <CardDescriptor + module="Agency" data-key="Agency" :url="`Agencies/${entityId}`" :title="card?.name" diff --git a/src/pages/Route/Agency/Card/AgencyWorkcenter.vue b/src/pages/Route/Agency/Card/AgencyWorkcenter.vue index 9a9213868..7cabf396d 100644 --- a/src/pages/Route/Agency/Card/AgencyWorkcenter.vue +++ b/src/pages/Route/Agency/Card/AgencyWorkcenter.vue @@ -88,7 +88,7 @@ async function deleteWorCenter(id) { </VnPaginate> </div> <QPageSticky :offset="[18, 18]"> - <QBtn @click.stop="dialog.show()" color="primary" fab v-shortcut="'+'" icon="add"> + <QBtn @click.stop="dialog.show()" color="primary" fab shortcut="+" icon="add"> <QDialog ref="dialog"> <FormModelPopup :title="t('Add work center')" diff --git a/src/pages/Route/Card/RouteCard.vue b/src/pages/Route/Card/RouteCard.vue index c178dc6bf..81b6cfa16 100644 --- a/src/pages/Route/Card/RouteCard.vue +++ b/src/pages/Route/Card/RouteCard.vue @@ -1,13 +1,12 @@ <script setup> import RouteDescriptor from 'pages/Route/Card/RouteDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; -import filter from './RouteFilter.js'; </script> <template> <VnCardBeta data-key="Route" - url="Routes" - :filter="filter" + base-url="Routes" + custom-url="Routes/filter" :descriptor="RouteDescriptor" /> </template> diff --git a/src/pages/Route/Card/RouteDescriptor.vue b/src/pages/Route/Card/RouteDescriptor.vue index 503cd1941..68c08b821 100644 --- a/src/pages/Route/Card/RouteDescriptor.vue +++ b/src/pages/Route/Card/RouteDescriptor.vue @@ -1,14 +1,13 @@ <script setup> import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; +import useCardDescription from 'composables/useCardDescription'; import { dashIfEmpty, toDate } from 'src/filters'; import RouteDescriptorMenu from 'pages/Route/Card/RouteDescriptorMenu.vue'; -import filter from './RouteFilter.js'; -import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; - const $props = defineProps({ id: { type: Number, @@ -18,6 +17,7 @@ const $props = defineProps({ }); const route = useRoute(); +const { t } = useI18n(); const zone = ref(); const zoneId = ref(); const entityId = computed(() => { @@ -36,31 +36,81 @@ const getZone = async () => { const { data: zoneData } = await axios.get(`Zones/${zoneId.value}`); zone.value = zoneData.name; }; + +const filter = { + fields: [ + 'id', + 'workerFk', + 'agencyModeFk', + 'dated', + 'm3', + 'warehouseFk', + 'description', + 'vehicleFk', + 'kmStart', + 'kmEnd', + 'started', + 'finished', + 'cost', + 'isOk', + ], + include: [ + { relation: 'agencyMode', scope: { fields: ['id', 'name'] } }, + { + relation: 'vehicle', + scope: { fields: ['id', 'm3'] }, + }, + { + relation: 'ticket', + scope: { + fields: ['id', 'name', 'zoneFk'], + include: { relation: 'zone', scope: { fields: ['id', 'name'] } }, + }, + }, + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { + fields: ['id'], + include: { relation: 'emailUser', scope: { fields: ['email'] } }, + }, + }, + }, + }, + ], +}; const data = ref(useCardDescription()); const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); onMounted(async () => { getZone(); }); </script> + <template> <CardDescriptor + module="Route" :url="`Routes/${entityId}`" :filter="filter" - :title="null" - data-key="Route" + :title="data.title" + :subtitle="data.subtitle" + data-key="routeData" + @on-fetch="setData" width="lg-width" > <template #body="{ entity }"> - <VnLv :label="$t('Date')" :value="toDate(entity?.dated)" /> - <VnLv :label="$t('Agency')" :value="entity?.agencyMode?.name" /> - <VnLv :label="$t('Zone')" :value="zone" /> + <VnLv :label="t('Date')" :value="toDate(entity?.dated)" /> + <VnLv :label="t('Agency')" :value="entity?.agencyMode?.name" /> + <VnLv :label="t('Zone')" :value="zone" /> <VnLv - :label="$t('Volume')" + :label="t('Volume')" :value="`${dashIfEmpty(entity?.m3)} / ${dashIfEmpty( entity?.vehicle?.m3, )} m³`" /> - <VnLv :label="$t('Description')" :value="entity?.description" /> + <VnLv :label="t('Description')" :value="entity?.description" /> </template> <template #menu="{ entity }"> <RouteDescriptorMenu :route="entity" /> diff --git a/src/pages/Route/Card/RouteFilter.js b/src/pages/Route/Card/RouteFilter.js deleted file mode 100644 index 90ee71bf7..000000000 --- a/src/pages/Route/Card/RouteFilter.js +++ /dev/null @@ -1,39 +0,0 @@ -export default { - fields: [ - 'code', - 'id', - 'workerFk', - 'agencyModeFk', - 'created', - 'm3', - 'warehouseFk', - 'description', - 'vehicleFk', - 'kmStart', - 'kmEnd', - 'started', - 'finished', - 'cost', - 'isOk', - ], - include: [ - { relation: 'agencyMode', scope: { fields: ['id', 'name'] } }, - { - relation: 'vehicle', - scope: { fields: ['id', 'm3'] }, - }, - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { - fields: ['id'], - include: { relation: 'emailUser', scope: { fields: ['email'] } }, - }, - }, - }, - }, - ], -}; diff --git a/src/pages/Route/Card/RouteFilter.vue b/src/pages/Route/Card/RouteFilter.vue index 21858102b..72bfed1da 100644 --- a/src/pages/Route/Card/RouteFilter.vue +++ b/src/pages/Route/Card/RouteFilter.vue @@ -100,7 +100,7 @@ const emit = defineEmits(['search']); <VnSelect :label="t('Vehicle')" v-model="params.vehicleFk" - url="Vehicles/active" + url="Vehicles" sort-by="numberPlate ASC" option-value="id" option-label="numberPlate" diff --git a/src/pages/Route/Card/RouteForm.vue b/src/pages/Route/Card/RouteForm.vue index 667204b15..633ff44bc 100644 --- a/src/pages/Route/Card/RouteForm.vue +++ b/src/pages/Route/Card/RouteForm.vue @@ -11,7 +11,6 @@ import VnInputDate from 'components/common/VnInputDate.vue'; import VnInput from 'components/common/VnInput.vue'; import axios from 'axios'; import VnInputTime from 'components/common/VnInputTime.vue'; -import filter from './RouteFilter.js'; import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); @@ -28,6 +27,52 @@ const defaultInitialData = { isOk: false, }; const maxDistance = ref(); + +const routeFilter = { + fields: [ + 'id', + 'workerFk', + 'agencyModeFk', + 'dated', + 'm3', + 'warehouseFk', + 'description', + 'vehicleFk', + 'kmStart', + 'kmEnd', + 'started', + 'finished', + 'cost', + 'isOk', + ], + include: [ + { relation: 'agencyMode', scope: { fields: ['id', 'name'] } }, + { + relation: 'vehicle', + scope: { fields: ['id', 'm3'] }, + }, + { + relation: 'ticket', + scope: { + fields: ['id', 'name', 'zoneFk'], + include: { relation: 'zone', scope: { fields: ['id', 'name'] } }, + }, + }, + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { + fields: ['id'], + include: { relation: 'emailUser', scope: { fields: ['email'] } }, + }, + }, + }, + }, + ], +}; const onSave = (data, response) => { if (isNew) { axios.post(`Routes/${response?.id}/updateWorkCenter`); @@ -44,10 +89,11 @@ const onSave = (data, response) => { sort-by="id ASC" /> <FormModel + :url="isNew ? null : `Routes/${route.params?.id}`" :url-create="isNew ? 'Routes' : null" :observe-form-changes="!isNew" - :filter="filter" - model="Route" + :filter="routeFilter" + model="route" :auto-load="!isNew" :form-initial-data="isNew ? defaultInitialData : null" @on-data-saved="onSave" @@ -58,7 +104,7 @@ const onSave = (data, response) => { <VnSelect :label="t('Vehicle')" v-model="data.vehicleFk" - url="Vehicles/active" + url="Vehicles" sort-by="numberPlate ASC" option-value="id" option-label="numberPlate" diff --git a/src/pages/Route/Roadmap/RoadmapBasicData.vue b/src/pages/Route/Roadmap/RoadmapBasicData.vue index a9e6059c3..2fe805362 100644 --- a/src/pages/Route/Roadmap/RoadmapBasicData.vue +++ b/src/pages/Route/Roadmap/RoadmapBasicData.vue @@ -11,16 +11,17 @@ import VnSelectSupplier from 'src/components/common/VnSelectSupplier.vue'; const { t } = useI18n(); const router = useRouter(); +const filter = { include: [{ relation: 'supplier' }] }; const onSave = (data, response) => { router.push({ name: 'RoadmapSummary', params: { id: response?.id } }); }; </script> <template> <FormModel - :update-url="`Roadmaps/${$route.params?.id}`" :url="`Roadmaps/${$route.params?.id}`" observe-form-changes - model="Roadmap" + :filter="filter" + model="roadmap" auto-load @on-data-saved="onSave" > diff --git a/src/pages/Route/Roadmap/RoadmapCard.vue b/src/pages/Route/Roadmap/RoadmapCard.vue index 48ba516a1..0b81de673 100644 --- a/src/pages/Route/Roadmap/RoadmapCard.vue +++ b/src/pages/Route/Roadmap/RoadmapCard.vue @@ -3,5 +3,5 @@ import VnCardBeta from 'components/common/VnCardBeta.vue'; import RoadmapDescriptor from 'pages/Route/Roadmap/RoadmapDescriptor.vue'; </script> <template> - <VnCardBeta data-key="Roadmap" url="Roadmaps" :descriptor="RoadmapDescriptor" /> + <VnCardBeta data-key="Roadmap" base-url="Roadmaps" :descriptor="RoadmapDescriptor" /> </template> diff --git a/src/pages/Route/Roadmap/RoadmapDescriptor.vue b/src/pages/Route/Roadmap/RoadmapDescriptor.vue index baa864a15..788173688 100644 --- a/src/pages/Route/Roadmap/RoadmapDescriptor.vue +++ b/src/pages/Route/Roadmap/RoadmapDescriptor.vue @@ -1,13 +1,13 @@ <script setup> -import { computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; +import useCardDescription from 'composables/useCardDescription'; import { dashIfEmpty, toDateHourMin } from 'src/filters'; import SupplierDescriptorProxy from 'pages/Supplier/Card/SupplierDescriptorProxy.vue'; import RoadmapDescriptorMenu from 'pages/Route/Roadmap/RoadmapDescriptorMenu.vue'; -import filter from 'pages/Route/Roadmap/RoadmapFilter.js'; const $props = defineProps({ id: { @@ -23,10 +23,22 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); + +const filter = { include: [{ relation: 'supplier' }] }; +const data = ref(useCardDescription()); +const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); </script> <template> - <CardDescriptor :url="`Roadmaps/${entityId}`" :filter="filter" data-key="Roadmap"> + <CardDescriptor + module="Roadmap" + :url="`Roadmaps/${entityId}`" + :filter="filter" + :title="data.title" + :subtitle="data.subtitle" + data-key="Roadmap" + @on-fetch="setData" + > <template #body="{ entity }"> <VnLv :label="t('Roadmap')" :value="entity?.name" /> <VnLv :label="t('ETD')" :value="toDateHourMin(entity?.etd)" /> diff --git a/src/pages/Route/Roadmap/RoadmapFilter.js b/src/pages/Route/Roadmap/RoadmapFilter.js deleted file mode 100644 index 0ae890363..000000000 --- a/src/pages/Route/Roadmap/RoadmapFilter.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - include: [{ relation: 'supplier' }], -}; diff --git a/src/pages/Route/Roadmap/RoadmapStops.vue b/src/pages/Route/Roadmap/RoadmapStops.vue index e4085d572..d8215ea49 100644 --- a/src/pages/Route/Roadmap/RoadmapStops.vue +++ b/src/pages/Route/Roadmap/RoadmapStops.vue @@ -68,7 +68,7 @@ const updateDefaultStop = (data) => { <QBtn flat icon="add" - v-shortcut="'+'" + shortcut="+" class="cursor-pointer" color="primary" @click="roadmapStopsCrudRef.insert()" diff --git a/src/pages/Route/Roadmap/RoadmapSummary.vue b/src/pages/Route/Roadmap/RoadmapSummary.vue index 0c1c2b903..1fbb1897d 100644 --- a/src/pages/Route/Roadmap/RoadmapSummary.vue +++ b/src/pages/Route/Roadmap/RoadmapSummary.vue @@ -67,6 +67,7 @@ const filter = { }, }, ], + where: { id: entityId }, }; </script> @@ -75,7 +76,7 @@ const filter = { <CardSummary data-key="RoadmapSummary" ref="summary" - :url="`Roadmaps/${entityId}`" + :url="`Roadmaps`" :filter="filter" > <template #header-left> diff --git a/src/pages/Route/RouteExtendedList.vue b/src/pages/Route/RouteExtendedList.vue index 46bc1a690..221fc4754 100644 --- a/src/pages/Route/RouteExtendedList.vue +++ b/src/pages/Route/RouteExtendedList.vue @@ -3,7 +3,7 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useQuasar } from 'quasar'; -import { dashIfEmpty, toDate, toHour } from 'src/filters'; +import { toDate } from 'src/filters'; import { useRouter } from 'vue-router'; import { usePrintService } from 'src/composables/usePrintService'; @@ -38,7 +38,7 @@ const routeFilter = { }; const columns = computed(() => [ { - align: 'center', + align: 'left', name: 'id', label: 'Id', chip: { @@ -48,7 +48,7 @@ const columns = computed(() => [ columnFilter: false, }, { - align: 'center', + align: 'left', name: 'workerFk', label: t('route.Worker'), create: true, @@ -68,10 +68,10 @@ const columns = computed(() => [ }, useLike: false, cardVisible: true, - format: (row, dashIfEmpty) => dashIfEmpty(row.workerUserName), + format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), }, { - align: 'center', + align: 'left', name: 'agencyModeFk', label: t('route.Agency'), isTitle: true, @@ -87,17 +87,17 @@ const columns = computed(() => [ }, }, columnClass: 'expand', - format: (row, dashIfEmpty) => dashIfEmpty(row.agencyName), }, { - align: 'center', + align: 'left', name: 'vehicleFk', label: t('route.Vehicle'), cardVisible: true, create: true, component: 'select', attrs: { - url: 'vehicles/active', + url: 'vehicles', + fields: ['id', 'numberPlate'], optionLabel: 'numberPlate', optionFilterValue: 'numberPlate', find: { @@ -108,31 +108,29 @@ const columns = computed(() => [ columnFilter: { inWhere: true, }, - format: (row, dashIfEmpty) => dashIfEmpty(row.vehiclePlateNumber), }, { - align: 'center', + align: 'left', name: 'dated', label: t('route.Date'), columnFilter: false, cardVisible: true, create: true, component: 'date', - format: ({ dated }, dashIfEmpty) => - dated === '0000-00-00' ? dashIfEmpty(null) : toDate(dated), + format: ({ date }) => toDate(date), }, { - align: 'center', + align: 'left', name: 'from', label: t('route.From'), visible: false, cardVisible: true, create: true, component: 'date', - format: ({ from }) => toDate(from), + format: ({ date }) => toDate(date), }, { - align: 'center', + align: 'left', name: 'to', label: t('route.To'), visible: false, @@ -149,20 +147,18 @@ const columns = computed(() => [ columnClass: 'shrink', }, { - align: 'center', + align: 'left', name: 'started', label: t('route.hourStarted'), component: 'time', columnFilter: false, - format: ({ started }) => toHour(started), }, { - align: 'center', + align: 'left', name: 'finished', label: t('route.hourFinished'), component: 'time', columnFilter: false, - format: ({ finished }) => toHour(finished), }, { align: 'center', @@ -181,7 +177,7 @@ const columns = computed(() => [ visible: false, }, { - align: 'center', + align: 'left', name: 'description', label: t('route.Description'), isTitle: true, @@ -190,7 +186,7 @@ const columns = computed(() => [ field: 'description', }, { - align: 'center', + align: 'left', name: 'isOk', label: t('route.Served'), component: 'checkbox', @@ -304,62 +300,60 @@ const openTicketsDialog = (id) => { <RouteFilter data-key="RouteList" /> </template> </RightMenu> - <QPage class="q-px-md"> - <VnTable - class="route-list" - ref="tableRef" - data-key="RouteList" - url="Routes/filter" - :columns="columns" - :right-search="false" - :is-editable="true" - :filter="routeFilter" - redirect="route" - :row-click="false" - :create="{ - urlCreate: 'Routes', - title: t('route.createRoute'), - onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: {}, - }" - save-url="Routes/crud" - :disable-option="{ card: true }" - table-height="85vh" - v-model:selected="selectedRows" - :table="{ - 'row-key': 'id', - selection: 'multiple', - }" - > - <template #moreBeforeActions> - <QBtn - icon="vn:clone" - color="primary" - class="q-mr-sm" - :disable="!selectedRows?.length" - @click="confirmationDialog = true" - > - <QTooltip>{{ t('route.Clone Selected Routes') }}</QTooltip> - </QBtn> - <QBtn - icon="cloud_download" - color="primary" - class="q-mr-sm" - :disable="!selectedRows?.length" - @click="showRouteReport" - > - <QTooltip>{{ t('route.Download selected routes as PDF') }}</QTooltip> - </QBtn> - <QBtn - icon="check" - color="primary" - class="q-mr-sm" - :disable="!selectedRows?.length" - @click="markAsServed()" - > - <QTooltip>{{ t('route.Mark as served') }}</QTooltip> - </QBtn> - </template> - </VnTable> - </QPage> + <VnTable + class="route-list" + ref="tableRef" + data-key="RouteList" + url="Routes/filter" + :columns="columns" + :right-search="false" + :is-editable="true" + :filter="routeFilter" + redirect="route" + :row-click="false" + :create="{ + urlCreate: 'Routes', + title: t('route.createRoute'), + onDataSaved: ({ id }) => tableRef.redirect(id), + formInitialData: {}, + }" + save-url="Routes/crud" + :disable-option="{ card: true }" + table-height="85vh" + v-model:selected="selectedRows" + :table="{ + 'row-key': 'id', + selection: 'multiple', + }" + > + <template #moreBeforeActions> + <QBtn + icon="vn:clone" + color="primary" + class="q-mr-sm" + :disable="!selectedRows?.length" + @click="confirmationDialog = true" + > + <QTooltip>{{ t('route.Clone Selected Routes') }}</QTooltip> + </QBtn> + <QBtn + icon="cloud_download" + color="primary" + class="q-mr-sm" + :disable="!selectedRows?.length" + @click="showRouteReport" + > + <QTooltip>{{ t('route.Download selected routes as PDF') }}</QTooltip> + </QBtn> + <QBtn + icon="check" + color="primary" + class="q-mr-sm" + :disable="!selectedRows?.length" + @click="markAsServed()" + > + <QTooltip>{{ t('route.Mark as served') }}</QTooltip> + </QBtn> + </template> + </VnTable> </template> diff --git a/src/pages/Route/RouteList.vue b/src/pages/Route/RouteList.vue index 9dad8ba22..bc3227f6c 100644 --- a/src/pages/Route/RouteList.vue +++ b/src/pages/Route/RouteList.vue @@ -38,17 +38,6 @@ const columns = computed(() => [ align: 'left', name: 'workerFk', label: t('route.Worker'), - component: 'select', - attrs: { - url: 'Workers/activeWithInheritedRole', - fields: ['id', 'name'], - useLike: false, - optionFilter: 'firstName', - find: { - value: 'workerFk', - label: 'workerUserName', - }, - }, create: true, cardVisible: true, format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), @@ -59,15 +48,6 @@ const columns = computed(() => [ name: 'agencyName', label: t('route.Agency'), cardVisible: true, - component: 'select', - attrs: { - url: 'agencyModes', - fields: ['id', 'name'], - find: { - value: 'agencyModeFk', - label: 'agencyName', - }, - }, create: true, columnClass: 'expand', columnFilter: false, @@ -77,17 +57,6 @@ const columns = computed(() => [ name: 'vehiclePlateNumber', label: t('route.Vehicle'), cardVisible: true, - component: 'select', - attrs: { - url: 'vehicles', - fields: ['id', 'numberPlate'], - optionLabel: 'numberPlate', - optionFilterValue: 'numberPlate', - find: { - value: 'vehicleFk', - label: 'vehiclePlateNumber', - }, - }, create: true, columnFilter: false, }, diff --git a/src/pages/Route/RouteTickets.vue b/src/pages/Route/RouteTickets.vue index adc7dfdaa..1416f77ce 100644 --- a/src/pages/Route/RouteTickets.vue +++ b/src/pages/Route/RouteTickets.vue @@ -120,8 +120,8 @@ const deletePriorities = async () => { try { await Promise.all( selectedRows.value.map((ticket) => - axios.patch(`Tickets/${ticket?.id}/`, { priority: null }), - ), + axios.patch(`Tickets/${ticket?.id}/`, { priority: null }) + ) ); } finally { refreshKey.value++; @@ -132,8 +132,8 @@ const setOrderedPriority = async () => { try { await Promise.all( ticketList.value.map((ticket, index) => - axios.patch(`Tickets/${ticket?.id}/`, { priority: index + 1 }), - ), + axios.patch(`Tickets/${ticket?.id}/`, { priority: index + 1 }) + ) ); } finally { refreshKey.value++; @@ -162,7 +162,7 @@ const setHighestPriority = async (ticket, ticketList) => { const goToBuscaman = async (ticket = null) => { await openBuscaman( routeEntity.value?.vehicleFk, - ticket ? [ticket] : selectedRows.value, + ticket ? [ticket] : selectedRows.value ); }; @@ -393,13 +393,7 @@ const openSmsDialog = async () => { </VnPaginate> </div> <QPageSticky :offset="[20, 20]"> - <QBtn - fab - icon="add" - v-shortcut="'+'" - color="primary" - @click="openTicketsDialog" - > + <QBtn fab icon="add" shortcut="+" color="primary" @click="openTicketsDialog"> <QTooltip> {{ t('Add ticket') }} </QTooltip> diff --git a/src/pages/Route/Vehicle/Card/VehicleBasicData.vue b/src/pages/Route/Vehicle/Card/VehicleBasicData.vue deleted file mode 100644 index e78bc6edd..000000000 --- a/src/pages/Route/Vehicle/Card/VehicleBasicData.vue +++ /dev/null @@ -1,162 +0,0 @@ -<script setup> -import { ref } from 'vue'; -import FormModel from 'components/FormModel.vue'; -import FetchData from 'src/components/FetchData.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import VnRow from 'components/ui/VnRow.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import VnInputNumber from 'src/components/common/VnInputNumber.vue'; - -const warehouses = ref([]); -const companies = ref([]); -const countries = ref([]); -const fuelTypes = ref([]); -const bankPolicies = ref([]); -const deliveryPoints = ref([]); -</script> -<template> - <FetchData - url="Warehouses" - :filter="{ fields: ['id', 'name'] }" - @on-fetch="(data) => (warehouses = data)" - auto-load - /> - <FetchData - url="Companies" - :filter="{ fields: ['id', 'code'] }" - @on-fetch="(data) => (companies = data)" - auto-load - /> - <FetchData - url="Countries" - :filter="{ fields: ['code'] }" - @on-fetch="(data) => (countries = data)" - auto-load - /> - <FetchData - url="FuelTypes" - :filter="{ fields: ['id', 'name'] }" - @on-fetch="(data) => (fuelTypes = data)" - auto-load - /> - <FetchData - url="DeliveryPoints" - :filter="{ fields: ['id', 'name'] }" - @on-fetch="(data) => (deliveryPoints = data)" - auto-load - /> - <FormModel model="Vehicle" :url-update="`Vehicles/${$route.params.id}`"> - <template #form="{ data }"> - <VnRow> - <VnInput v-model="data.description" :label="$t('globals.description')" /> - <VnInput v-model="data.numberPlate" :label="$t('vehicle.numberPlate')" /> - </VnRow> - <VnRow> - <VnInput - v-model="data.model" - :label="$t('globals.model')" - :required="true" - /> - <VnSelect - url="VehicleTypes" - v-model="data.vehicleTypeFk" - :label="$t('globals.type')" - /> - </VnRow> - <VnRow> - <VnInput - v-model="data.tradeMark" - :label="$t('vehicle.tradeMark')" - :required="true" - /> - <VnInput v-model="data.chassis" :label="$t('vehicle.chassis')" /> - </VnRow> - <VnRow> - <VnSelect - v-model="data.fuelTypeFk" - :label="$t('globals.fuel')" - :options="fuelTypes" - /> - <VnSelect - v-model="data.deliveryPointFk" - :label="$t('globals.deliveryPoint')" - :options="deliveryPoints" - /> - </VnRow> - <VnRow> - <VnSelect - v-model="data.companyFk" - :label="$t('globals.company')" - :options="companies" - option-label="code" - /> - <VnSelect - v-model="data.warehouseFk" - :label="$t('globals.warehouse')" - :options="warehouses" - /> - </VnRow> - <VnRow> - <VnSelect - url="Suppliers" - :filter="{ fields: ['id', 'name'] }" - v-model="data.supplierFk" - :label="$t('globals.supplier')" - /> - <VnSelect - url="Suppliers" - :filter="{ fields: ['id', 'name'] }" - v-model="data.supplierCoolerFk" - :label="$t('vehicle.supplierCooler')" - /> - </VnRow> - <VnRow> - <VnSelect - url="BankPolicies" - :filter="{ fields: ['id', 'ref'] }" - v-model="data.bankPolicyFk" - :label="$t('vehicle.leasing')" - :options="bankPolicies" - option-label="ref" - option-value="id" - /> - <VnInput v-model="data.leasing" :label="$t('vehicle.nLeasing')" /> - </VnRow> - <VnRow> - <VnInputNumber v-model="data.import" :label="$t('globals.amount')" /> - <VnInputNumber - v-model="data.importCooler" - :label="$t('vehicle.amountCooler')" - /> - </VnRow> - <VnRow> - <VnSelect - url="Ppes" - option-label="id" - v-model="data.ppeFk" - :label="$t('vehicle.ppe')" - /> - <VnSelect - v-model="data.countryCodeFk" - :label="$t('globals.country')" - :options="countries" - option-label="code" - option-value="code" - /> - </VnRow> - <VnRow> - <VnInput v-model="data.vin" :label="$t('vehicle.vin')" /> - <span :style="{ 'align-self': $q.screen.gt.xs ? 'end' : 'unset' }"> - <QCheckbox - v-model="data.isActive" - :label="$t('vehicle.isActive')" - :false-value="0" - :true-value="1" - dense - class="q-mt-sm" - /> - </span> - </VnRow> - </template> - </FormModel> -</template> diff --git a/src/pages/Route/Vehicle/Card/VehicleCard.vue b/src/pages/Route/Vehicle/Card/VehicleCard.vue deleted file mode 100644 index f59420aa2..000000000 --- a/src/pages/Route/Vehicle/Card/VehicleCard.vue +++ /dev/null @@ -1,13 +0,0 @@ -<script setup> -import VnCardBeta from 'components/common/VnCardBeta.vue'; -import VehicleDescriptor from './VehicleDescriptor.vue'; -import VehicleFilter from '../VehicleFilter.js'; -</script> -<template> - <VnCardBeta - data-key="Vehicle" - url="Vehicles" - :filter="VehicleFilter" - :descriptor="VehicleDescriptor" - /> -</template> diff --git a/src/pages/Route/Vehicle/Card/VehicleDescriptor.vue b/src/pages/Route/Vehicle/Card/VehicleDescriptor.vue deleted file mode 100644 index d9a2434ab..000000000 --- a/src/pages/Route/Vehicle/Card/VehicleDescriptor.vue +++ /dev/null @@ -1,49 +0,0 @@ -<script setup> -import VnLv from 'src/components/ui/VnLv.vue'; -import CardDescriptor from 'components/ui/CardDescriptor.vue'; -import axios from 'axios'; -import useNotify from 'src/composables/useNotify.js'; - -const { notify } = useNotify(); -</script> -<template> - <CardDescriptor - :url="`Vehicles/${$route.params.id}`" - data-key="Vehicle" - title="numberPlate" - :to-module="{ name: 'VehicleList' }" - > - <template #menu="{ entity }"> - <QItem - data-cy="delete" - v-ripple - clickable - @click=" - async () => { - try { - await axios.delete(`Vehicles/${entity.id}`); - notify('vehicle.remove', 'positive'); - $router.push({ name: 'VehicleList' }); - } catch (e) { - throw e; - } - } - " - > - <QItemSection> - {{ $t('vehicle.delete') }} - </QItemSection> - </QItem> - </template> - <template #body="{ entity }"> - <VnLv :label="$t('vehicle.numberPlate')" :value="entity.numberPlate" /> - <VnLv :label="$t('vehicle.tradeMark')" :value="entity.tradeMark" /> - <VnLv :label="$t('globals.model')" :value="entity.model" /> - <VnLv :label="$t('globals.country')" :value="entity.countryCodeFk" /> - </template> - </CardDescriptor> -</template> -<i18n> -es: - Vehicle removed: Vehículo eliminado -</i18n> diff --git a/src/pages/Route/Vehicle/Card/VehicleSummary.vue b/src/pages/Route/Vehicle/Card/VehicleSummary.vue deleted file mode 100644 index 981870cb2..000000000 --- a/src/pages/Route/Vehicle/Card/VehicleSummary.vue +++ /dev/null @@ -1,127 +0,0 @@ -<script setup> -import { computed } from 'vue'; -import { useRoute } from 'vue-router'; -import CardSummary from 'components/ui/CardSummary.vue'; -import VnLv from 'src/components/ui/VnLv.vue'; -import VnTitle from 'src/components/common/VnTitle.vue'; -import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; -import VehicleFilter from '../VehicleFilter.js'; -import { downloadFile } from 'src/composables/downloadFile'; -import { dashIfEmpty } from 'src/filters'; - -const props = defineProps({ id: { type: [Number, String], default: null } }); - -const route = useRoute(); -const entityId = computed(() => props.id || +route.params.id); -const links = { - 'basic-data': `#/vehicle/${entityId.value}/basic-data`, - notes: `#/vehicle/${entityId.value}/notes`, - dms: `#/vehicle/${entityId.value}/dms`, - 'invoice-in': `#/vehicle/${entityId.value}/invoice-in`, - events: `#/vehicle/${entityId.value}/events`, -}; -</script> -<template> - <CardSummary data-key="Vehicle" :url="`Vehicles/${entityId}`" :filter="VehicleFilter"> - <template #header="{ entity }"> - <div>{{ entity.id }} - {{ entity.numberPlate }}</div> - </template> - <template #body="{ entity }"> - <QCard class="vn-one"> - <QCardSection dense> - <VnTitle - :url="links['basic-data']" - :text="$t('globals.pageTitles.basicData')" - /> - </QCardSection> - <QCardSection content> - <QList dense> - <VnLv - :label="$t('globals.description')" - :value="entity.description" - /> - <VnLv - :label="$t('vehicle.tradeMark')" - :value="entity.tradeMark" - /> - <VnLv :label="$t('globals.model')" :value="entity.model" /> - <VnLv :label="$t('globals.supplier')"> - <template #value> - <span class="link"> - {{ entity.supplier?.name }} - <SupplierDescriptorProxy :id="entity.supplierFk" /> - </span> - </template> - </VnLv> - <VnLv :label="$t('vehicle.supplierCooler')"> - <template #value> - <span class="link"> - {{ entity.supplierCooler?.name }} - <SupplierDescriptorProxy - :id="entity.supplierCoolerFk" - /> - </span> - </template> - </VnLv> - <VnLv :label="$t('vehicle.vin')" :value="entity.vin" /> - </QList> - <QList dense> - <VnLv :label="$t('vehicle.chassis')" :value="entity.chassis" /> - <VnLv - :label="$t('globals.fuel')" - :value="entity.fuelType?.name" - /> - <VnLv :label="$t('vehicle.ppe')" :value="entity.ppeFk" /> - <VnLv :label="$t('vehicle.nLeasing')" :value="entity.leasing" /> - <VnLv - :label="$t('vehicle.leasing')" - :value="entity.bankPolicy?.ref" - > - <template #value> - <span v-text="dashIfEmpty(entity.bankPolicy?.name)" /> - <QBtn - v-if="entity.bankPolicy?.dmsFk" - class="q-ml-xs" - color="primary" - flat - dense - icon="cloud_download" - @click="downloadFile(entity.bankPolicy?.dmsFk)" - > - <QTooltip>{{ $t('globals.download') }}</QTooltip> - </QBtn> - </template> - </VnLv> - <VnLv :label="$t('globals.amount')" :value="entity.import" /> - </QList> - <QList dense> - <VnLv - :label="$t('globals.warehouse')" - :value="entity.warehouse?.name" - /> - <VnLv - :label="$t('globals.company')" - :value="entity.company?.code" - /> - <VnLv - :label="$t('globals.deliveryPoint')" - :value="entity.deliveryPoint?.name" - /> - <VnLv - :label="$t('globals.country')" - :value="entity.countryCodeFk" - /> - <VnLv - :label="$t('vehicle.isKmTruckRate')" - :value="!!entity.isKmTruckRate" - /> - <VnLv - :label="$t('vehicle.isActive')" - :value="!!entity.isActive" - /> - </QList> - </QCardSection> - </QCard> - </template> - </CardSummary> -</template> diff --git a/src/pages/Route/Vehicle/VehicleFilter.js b/src/pages/Route/Vehicle/VehicleFilter.js deleted file mode 100644 index cbf5cc621..000000000 --- a/src/pages/Route/Vehicle/VehicleFilter.js +++ /dev/null @@ -1,76 +0,0 @@ -export default { - fields: [ - 'id', - 'description', - 'isActive', - 'isKmTruckRate', - 'warehouseFk', - 'companyFk', - 'numberPlate', - 'chassis', - 'supplierFk', - 'supplierCoolerFk', - 'tradeMark', - 'fuelTypeFk', - 'import', - 'importCooler', - 'vin', - 'model', - 'ppeFk', - 'countryCodeFk', - 'leasing', - 'bankPolicyFk', - 'vehicleTypeFk', - 'deliveryPointFk', - ], - include: [ - { - relation: 'warehouse', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'company', - scope: { - fields: ['id', 'code'], - }, - }, - { - relation: 'supplier', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'supplierCooler', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'fuelType', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'bankPolicy', - scope: { - fields: ['id', 'ref', 'dmsFk'], - }, - }, - { - relation: 'ppe', - scope: { - fields: ['id'], - }, - }, - { - relation: 'deliveryPoint', - scope: { - fields: ['id', 'name'], - }, - }, - ], -}; diff --git a/src/pages/Route/Vehicle/VehicleList.vue b/src/pages/Route/Vehicle/VehicleList.vue deleted file mode 100644 index e5b945010..000000000 --- a/src/pages/Route/Vehicle/VehicleList.vue +++ /dev/null @@ -1,224 +0,0 @@ -<script setup> -import { ref, computed } from 'vue'; -import { useI18n } from 'vue-i18n'; -import VnTable from 'components/VnTable/VnTable.vue'; -import FetchData from 'src/components/FetchData.vue'; -import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import VehicleSummary from 'src/pages/Route/Vehicle/Card/VehicleSummary.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import VnSection from 'src/components/common/VnSection.vue'; - -const { t } = useI18n(); -const { viewSummary } = useSummaryDialog(); -const warehouses = ref([]); -const companies = ref([]); -const countries = ref([]); -const vehicleStates = ref([]); -const vehicleTypes = ref([]); - -const columns = computed(() => [ - { - name: 'isActive', - columnFilter: false, - align: 'center', - }, - { - name: 'id', - label: t('globals.id'), - isId: true, - chip: { - condition: () => true, - }, - }, - { - name: 'description', - label: t('globals.description'), - }, - { - name: 'tradeMark', - label: t('vehicle.tradeMark'), - cardVisible: true, - }, - { - name: 'numberPlate', - label: t('vehicle.numberPlate'), - isTitle: true, - }, - { - name: 'vehicleTypeFk', - label: t('globals.type'), - format: (row) => row.type, - columnFilter: { - component: 'select', - name: 'vehicleTypeFk', - options: vehicleTypes.value, - }, - cardVisible: true, - }, - { - name: 'vehicleStateFk', - label: t('globals.state'), - columnFilter: { - component: 'select', - name: 'vehicleStateFk', - optionLabel: 'state', - options: vehicleStates.value, - }, - format: (row, dashIfEmpty) => dashIfEmpty(row.state), - }, - { - name: 'chassis', - label: t('vehicle.chassis'), - }, - { - name: 'leasing', - label: t('vehicle.leasing'), - }, - { - name: 'warehouseFk', - label: t('globals.warehouse'), - format: (row, dashIfEmpty) => dashIfEmpty(row.warehouse), - columnFilter: { - component: 'select', - name: 'warehouseFk', - options: warehouses.value, - }, - cardVisible: true, - }, - { - name: 'companyFk', - label: t('globals.company'), - format: (row, dashIfEmpty) => dashIfEmpty(row.company), - columnFilter: { - component: 'select', - name: 'companyFk', - optionLabel: 'code', - options: companies.value, - }, - }, - { - name: 'countryCodeFk', - label: t('globals.country'), - columnFilter: { - component: 'select', - name: 'countryCodeFk', - optionValue: 'code', - optionLabel: 'code', - options: countries.value, - }, - }, - { - align: 'right', - name: 'tableActions', - actions: [ - { - title: t('components.smartCard.openSummary'), - icon: 'preview', - action: (row) => viewSummary(row.id, VehicleSummary), - }, - ], - }, -]); -</script> -<template> - <FetchData - url="Warehouses" - :filter="{ fields: ['id', 'name'] }" - @on-fetch="(data) => (warehouses = data)" - auto-load - /> - <FetchData - url="Companies" - :filter="{ fields: ['id', 'code'] }" - @on-fetch="(data) => (companies = data)" - auto-load - /> - <FetchData - url="Countries" - :filter="{ fields: ['name', 'code'] }" - @on-fetch="(data) => (countries = data)" - auto-load - /> - <FetchData - url="VehicleStates" - :filter="{ fields: ['id', 'state'] }" - @on-fetch="(data) => (vehicleStates = data)" - auto-load - /> - <FetchData - url="VehicleTypes" - :filter="{ fields: ['id', 'name'] }" - @on-fetch="(data) => (vehicleTypes = data)" - auto-load - /> - <VnSection - data-key="VehicleList" - :columns="columns" - prefix="vehicle" - :array-data-props="{ - url: 'Vehicles/filter', - }" - > - <template #body> - <VnTable - ref="tableRef" - data-key="VehicleList" - :columns="columns" - redirect="route/vehicle" - :create="{ - urlCreate: 'Vehicles', - title: t('vehicle.create'), - onDataSaved: ({ id }) => $refs.tableRef.redirect(id), - formInitialData: { isActive: true, isKmTruckRate: false }, - }" - :use-model="true" - :right-search="false" - > - <template #column-isActive="{ row }"> - <span> - <QIcon - v-if="!row.isActive" - name="vn:inactive-car" - color="primary" - size="xs" - > - <QTooltip>{{ $t('globals.inactive') }}</QTooltip> - </QIcon> - </span> - </template> - <template #more-create-dialog="{ data }"> - <VnInput - v-model="data.numberPlate" - :label="$t('vehicle.numberPlate')" - :uppercase="true" - /> - <VnInput v-model="data.tradeMark" :label="$t('vehicle.tradeMark')" /> - <VnInput v-model="data.model" :label="$t('globals.model')" /> - <VnSelect - v-model="data.vehicleTypeFk" - :label="$t('globals.type')" - :options="vehicleTypes" - /> - <VnSelect - v-model="data.warehouseFk" - :label="$t('globals.warehouse')" - :options="warehouses" - /> - <VnSelect - v-model="data.countryCodeFk" - :label="$t('globals.country')" - option-value="code" - option-label="name" - :options="countries" - /> - <VnInput - v-model="data.description" - :label="$t('globals.description')" - /> - <QCheckbox to v-model="data.isActive" :label="$t('globals.active')" /> - </template> - </VnTable> - </template> - </VnSection> -</template> diff --git a/src/pages/Route/Vehicle/locale/en.yml b/src/pages/Route/Vehicle/locale/en.yml deleted file mode 100644 index c92022f9d..000000000 --- a/src/pages/Route/Vehicle/locale/en.yml +++ /dev/null @@ -1,20 +0,0 @@ -vehicle: - tradeMark: Trade Mark - numberPlate: Nº Plate - chassis: Chassis - leasing: Leasing - isKmTruckRate: Trailer - delete: Delete Vehicle - supplierCooler: Supplier Cooler - vin: VIN - ppe: Ppe - isActive: Active - nLeasing: Nº Leasing - create: Create Vehicle - amountCooler: Amount cooler - remove: Vehicle removed - search: Search Vehicle - searchInfo: Search by id or number plate - params: - vehicleTypeFk: Type - vehicleStateFk: State diff --git a/src/pages/Route/Vehicle/locale/es.yml b/src/pages/Route/Vehicle/locale/es.yml deleted file mode 100644 index c878f97ac..000000000 --- a/src/pages/Route/Vehicle/locale/es.yml +++ /dev/null @@ -1,20 +0,0 @@ -vehicle: - tradeMark: Marca - numberPlate: Matrícula - chassis: Nº de bastidor - leasing: Leasing - isKmTruckRate: Trailer - delete: Eliminar vehículo - supplierCooler: Proveedor Frío - vin: VIN - ppe: Nº Inmovilizado - create: Crear vehículo - amountCooler: Importe frío - isActive: Activo - nLeasing: Nº leasing - remove: Vehículo eliminado - search: Buscar Vehículo - searchInfo: Buscar por id o matrícula - params: - vehicleTypeFk: Tipo - vehicleStateFk: Estado diff --git a/src/pages/Shelving/Card/ShelvingCard.vue b/src/pages/Shelving/Card/ShelvingCard.vue index 9e0ac8ad2..41a0db33c 100644 --- a/src/pages/Shelving/Card/ShelvingCard.vue +++ b/src/pages/Shelving/Card/ShelvingCard.vue @@ -1,14 +1,12 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import ShelvingDescriptor from 'pages/Shelving/Card/ShelvingDescriptor.vue'; -import filter from './ShelvingFilter.js'; </script> <template> <VnCardBeta data-key="Shelving" - url="Shelvings" - :filter="filter" + base-url="Shelvings" :descriptor="ShelvingDescriptor" /> </template> diff --git a/src/pages/Shelving/Card/ShelvingDescriptor.vue b/src/pages/Shelving/Card/ShelvingDescriptor.vue index 5e618aa7f..b1ff4a8ae 100644 --- a/src/pages/Shelving/Card/ShelvingDescriptor.vue +++ b/src/pages/Shelving/Card/ShelvingDescriptor.vue @@ -1,12 +1,12 @@ <script setup> -import { computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; +import useCardDescription from 'composables/useCardDescription'; import ShelvingDescriptorMenu from 'pages/Shelving/Card/ShelvingDescriptorMenu.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; -import filter from './ShelvingFilter.js'; const $props = defineProps({ id: { @@ -22,13 +22,35 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); + +const filter = { + include: [ + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { fields: ['nickname'] }, + }, + }, + }, + { relation: 'parking' }, + ], +}; +const data = ref(useCardDescription()); +const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); </script> + <template> <CardDescriptor + module="Shelving" :url="`Shelvings/${entityId}`" :filter="filter" - title="code" - data-key="Shelving" + :title="data.title" + :subtitle="data.subtitle" + data-key="Shelvings" + @on-fetch="setData" > <template #body="{ entity }"> <VnLv :label="t('globals.code')" :value="entity.code" /> diff --git a/src/pages/Shelving/Card/ShelvingFilter.js b/src/pages/Shelving/Card/ShelvingFilter.js deleted file mode 100644 index e302e1b9c..000000000 --- a/src/pages/Shelving/Card/ShelvingFilter.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - include: [ - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { fields: ['nickname'] }, - }, - }, - }, - { relation: 'parking' }, - ], -}; diff --git a/src/pages/Shelving/Card/ShelvingForm.vue b/src/pages/Shelving/Card/ShelvingForm.vue index 078058342..3bbd94a0a 100644 --- a/src/pages/Shelving/Card/ShelvingForm.vue +++ b/src/pages/Shelving/Card/ShelvingForm.vue @@ -1,4 +1,5 @@ <script setup> +import { useI18n } from 'vue-i18n'; import { computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import VnRow from 'components/ui/VnRow.vue'; @@ -6,8 +7,8 @@ import FormModel from 'components/FormModel.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; -import filter from './ShelvingFilter.js'; +const { t } = useI18n(); const route = useRoute(); const router = useRouter(); const entityId = computed(() => route.params.id ?? null); @@ -19,6 +20,22 @@ const defaultInitialData = { isRecyclable: false, }; +const shelvingFilter = { + include: [ + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { fields: ['nickname'] }, + }, + }, + }, + { relation: 'parking' }, + ], +}; + const onSave = (shelving, newShelving) => { if (isNew) { router.push({ name: 'ShelvingBasicData', params: { id: newShelving?.id } }); @@ -28,10 +45,11 @@ const onSave = (shelving, newShelving) => { <template> <VnSubToolbar v-if="isNew" /> <FormModel + :url="isNew ? null : `Shelvings/${entityId}`" :url-create="isNew ? 'Shelvings' : null" :observe-form-changes="!isNew" - :filter="filter" - model="Shelving" + :filter="shelvingFilter" + model="shelving" :auto-load="!isNew" :form-initial-data="isNew ? defaultInitialData : null" @on-data-saved="onSave" @@ -40,7 +58,7 @@ const onSave = (shelving, newShelving) => { <VnRow> <VnInput v-model="data.code" - :label="$t('globals.code')" + :label="t('globals.code')" :rules="validate('Shelving.code')" /> <VnSelect @@ -50,7 +68,7 @@ const onSave = (shelving, newShelving) => { option-label="code" :filter-options="['id', 'code']" :fields="['id', 'code']" - :label="$t('shelving.list.parking')" + :label="t('shelving.list.parking')" :rules="validate('Shelving.parkingFk')" /> </VnRow> @@ -58,12 +76,12 @@ const onSave = (shelving, newShelving) => { <VnInput v-model="data.priority" type="number" - :label="$t('shelving.list.priority')" + :label="t('shelving.list.priority')" :rules="validate('Shelving.priority')" /> <QCheckbox v-model="data.isRecyclable" - :label="$t('shelving.summary.recyclable')" + :label="t('shelving.summary.recyclable')" :rules="validate('Shelving.isRecyclable')" /> </VnRow> diff --git a/src/pages/Shelving/Card/ShelvingSearchbar.vue b/src/pages/Shelving/Card/ShelvingSearchbar.vue index 741b11663..bfc8ad4f5 100644 --- a/src/pages/Shelving/Card/ShelvingSearchbar.vue +++ b/src/pages/Shelving/Card/ShelvingSearchbar.vue @@ -1,15 +1,15 @@ <script setup> import VnSearchbar from 'components/ui/VnSearchbar.vue'; -import exprBuilder from '../ShelvingExprBuilder.js'; +import {useI18n} from "vue-i18n"; +const { t } = useI18n(); </script> <template> <VnSearchbar data-key="ShelvingList" url="Shelvings" - :label="$t('Search shelving')" - :info="$t('You can search by shelving reference')" - :expr-builder="exprBuilder" + :label="t('Search shelving')" + :info="t('You can search by shelving reference')" /> </template> diff --git a/src/pages/Shelving/Card/ShelvingSummary.vue b/src/pages/Shelving/Card/ShelvingSummary.vue index f89ff4d78..39fa4639f 100644 --- a/src/pages/Shelving/Card/ShelvingSummary.vue +++ b/src/pages/Shelving/Card/ShelvingSummary.vue @@ -1,10 +1,10 @@ <script setup> import { computed, ref } from 'vue'; import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'components/ui/VnLv.vue'; import VnUserLink from 'components/ui/VnUserLink.vue'; -import filter from './ShelvingFilter.js'; import ShelvingDescriptorMenu from './ShelvingDescriptorMenu.vue'; const $props = defineProps({ @@ -14,9 +14,25 @@ const $props = defineProps({ }, }); const route = useRoute(); - +const { t } = useI18n(); const summary = ref({}); const entityId = computed(() => $props.id || route.params.id); + +const filter = { + include: [ + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { fields: ['nickname'] }, + }, + }, + }, + { relation: 'parking' }, + ], +}; </script> <template> @@ -25,7 +41,7 @@ const entityId = computed(() => $props.id || route.params.id); ref="summary" :url="`Shelvings/${entityId}`" :filter="filter" - data-key="Shelving" + data-key="ShelvingSummary" > <template #header="{ entity }"> <div>{{ entity.code }}</div> @@ -42,19 +58,16 @@ const entityId = computed(() => $props.id || route.params.id); class="header header-link" :to="{ name: 'ShelvingBasicData', params: { id: entityId } }" > - {{ $t('globals.pageTitles.basicData') }} + {{ t('globals.pageTitles.basicData') }} <QIcon name="open_in_new" /> </RouterLink> - <VnLv :label="$t('globals.code')" :value="entity.code" /> + <VnLv :label="t('globals.code')" :value="entity.code" /> <VnLv - :label="$t('shelving.list.parking')" + :label="t('shelving.list.parking')" :value="entity.parking?.code" /> - <VnLv - :label="$t('shelving.list.priority')" - :value="entity.priority" - /> - <VnLv v-if="entity.worker" :label="$t('globals.worker')"> + <VnLv :label="t('shelving.list.priority')" :value="entity.priority" /> + <VnLv v-if="entity.worker" :label="t('globals.worker')"> <template #value> <VnUserLink :name="entity.worker?.user?.nickname" @@ -63,7 +76,7 @@ const entityId = computed(() => $props.id || route.params.id); </template> </VnLv> <VnLv - :label="$t('shelving.summary.recyclable')" + :label="t('shelving.summary.recyclable')" :value="entity.isRecyclable" /> </QCard> diff --git a/src/pages/Shelving/Parking/Card/ParkingFilter.js b/src/pages/Shelving/Parking/Card/ParkingFilter.js deleted file mode 100644 index fd1855c45..000000000 --- a/src/pages/Shelving/Parking/Card/ParkingFilter.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'], - include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }], -}; diff --git a/src/pages/Shelving/Parking/ParkingExprBuilder.js b/src/pages/Shelving/Parking/ParkingExprBuilder.js deleted file mode 100644 index 16d2262c8..000000000 --- a/src/pages/Shelving/Parking/ParkingExprBuilder.js +++ /dev/null @@ -1,10 +0,0 @@ -export default (param, value) => { - switch (param) { - case 'code': - return { [param]: { like: `%${value}%` } }; - case 'sectorFk': - return { [param]: value }; - case 'search': - return { or: [{ code: { like: `%${value}%` } }, { id: value }] }; - } -}; diff --git a/src/pages/Shelving/ShelvingExprBuilder.js b/src/pages/Shelving/ShelvingExprBuilder.js deleted file mode 100644 index b9aad8a71..000000000 --- a/src/pages/Shelving/ShelvingExprBuilder.js +++ /dev/null @@ -1,10 +0,0 @@ -export default (param, value) => { - switch (param) { - case 'search': - return { code: { like: `%${value}%` } }; - case 'parkingFk': - case 'userFk': - case 'isRecyclable': - return { [param]: value }; - } -}; diff --git a/src/pages/Shelving/ShelvingList.vue b/src/pages/Shelving/ShelvingList.vue index 4e0c21100..cf158e76b 100644 --- a/src/pages/Shelving/ShelvingList.vue +++ b/src/pages/Shelving/ShelvingList.vue @@ -1,5 +1,6 @@ <script setup> import VnPaginate from 'components/ui/VnPaginate.vue'; +import { useI18n } from 'vue-i18n'; import CardList from 'components/ui/CardList.vue'; import VnLv from 'components/ui/VnLv.vue'; import { useRouter } from 'vue-router'; @@ -7,9 +8,9 @@ import ShelvingFilter from 'pages/Shelving/Card/ShelvingFilter.vue'; import ShelvingSummary from 'pages/Shelving/Card/ShelvingSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import VnSection from 'src/components/common/VnSection.vue'; -import exprBuilder from './ShelvingExprBuilder.js'; const router = useRouter(); +const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const dataKey = 'ShelvingList'; @@ -20,6 +21,17 @@ const filter = { function navigate(id) { router.push({ path: `/shelving/${id}` }); } + +function exprBuilder(param, value) { + switch (param) { + case 'search': + return { code: { like: `%${value}%` } }; + case 'parkingFk': + case 'userFk': + case 'isRecyclable': + return { [param]: value }; + } +} </script> <template> @@ -50,18 +62,18 @@ function navigate(id) { > <template #list-items> <VnLv - :label="$t('shelving.list.parking')" - :title-label="$t('shelving.list.parking')" + :label="t('shelving.list.parking')" + :title-label="t('shelving.list.parking')" :value="row.parking?.code" /> <VnLv - :label="$t('shelving.list.priority')" + :label="t('shelving.list.priority')" :value="row?.priority" /> </template> <template #actions> <QBtn - :label="$t('components.smartCard.openSummary')" + :label="t('components.smartCard.openSummary')" @click.stop="viewSummary(row.id, ShelvingSummary)" color="primary" /> @@ -72,9 +84,9 @@ function navigate(id) { </div> <QPageSticky :offset="[20, 20]"> <RouterLink :to="{ name: 'ShelvingCreate' }"> - <QBtn fab icon="add" color="primary" v-shortcut="'+'" /> + <QBtn fab icon="add" color="primary" shortcut="+" /> <QTooltip> - {{ $t('shelving.list.newShelving') }} + {{ t('shelving.list.newShelving') }} </QTooltip> </RouterLink> </QPageSticky> diff --git a/src/pages/Supplier/Card/SupplierAccounts.vue b/src/pages/Supplier/Card/SupplierAccounts.vue index 365eb67a1..4a6901d1d 100644 --- a/src/pages/Supplier/Card/SupplierAccounts.vue +++ b/src/pages/Supplier/Card/SupplierAccounts.vue @@ -71,7 +71,7 @@ function bankEntityFilter(val, update) { filteredBankEntitiesOptions.value = bankEntitiesOptions.value.filter( (bank) => bank.bic.toLowerCase().startsWith(needle) || - bank.name.toLowerCase().includes(needle), + bank.name.toLowerCase().includes(needle) ); }); } @@ -170,7 +170,7 @@ function bankEntityFilter(val, update) { <QIcon name="info" class="cursor-pointer"> <QTooltip>{{ t( - 'Name of the bank account holder if different from the provider', + 'Name of the bank account holder if different from the provider' ) }}</QTooltip> </QIcon> @@ -194,7 +194,7 @@ function bankEntityFilter(val, update) { <QBtn flat icon="add" - v-shortcut + shortcut="+" class="cursor-pointer" color="primary" @click="supplierAccountRef.insert()" diff --git a/src/pages/Supplier/Card/SupplierAddresses.vue b/src/pages/Supplier/Card/SupplierAddresses.vue index c4c0ab7be..e568962ff 100644 --- a/src/pages/Supplier/Card/SupplierAddresses.vue +++ b/src/pages/Supplier/Card/SupplierAddresses.vue @@ -89,7 +89,7 @@ const redirectToUpdateView = (addressData) => { icon="add" color="primary" @click="redirectToCreateView()" - v-shortcut="'+'" + shortcut="+" /> <QTooltip> {{ t('New address') }} diff --git a/src/pages/Supplier/Card/SupplierAgencyTerm.vue b/src/pages/Supplier/Card/SupplierAgencyTerm.vue index ab21f1f76..99b672cc4 100644 --- a/src/pages/Supplier/Card/SupplierAgencyTerm.vue +++ b/src/pages/Supplier/Card/SupplierAgencyTerm.vue @@ -114,7 +114,7 @@ const redirectToCreateView = () => { icon="add" color="primary" @click="redirectToCreateView()" - v-shortcut="'+'" + shortcut="+" /> <QTooltip> {{ t('supplier.agencyTerms.addRow') }} diff --git a/src/pages/Supplier/Card/SupplierBasicData.vue b/src/pages/Supplier/Card/SupplierBasicData.vue index 631700a4a..f6c13b7af 100644 --- a/src/pages/Supplier/Card/SupplierBasicData.vue +++ b/src/pages/Supplier/Card/SupplierBasicData.vue @@ -19,8 +19,9 @@ const companySizes = [ </script> <template> <FormModel + :url="`Suppliers/${route.params.id}`" :url-update="`Suppliers/${route.params.id}`" - model="Supplier" + model="supplier" auto-load :clear-store-on-unmount="false" @on-data-saved="arrayData.fetch({})" diff --git a/src/pages/Supplier/Card/SupplierCard.vue b/src/pages/Supplier/Card/SupplierCard.vue index e30f79f96..594026d18 100644 --- a/src/pages/Supplier/Card/SupplierCard.vue +++ b/src/pages/Supplier/Card/SupplierCard.vue @@ -1,13 +1,19 @@ <script setup> +import VnCard from 'components/common/VnCard.vue'; import SupplierDescriptor from './SupplierDescriptor.vue'; -import VnCardBeta from 'src/components/common/VnCardBeta.vue'; -import filter from './SupplierFilter.js'; +import SupplierListFilter from '../SupplierListFilter.vue'; </script> <template> - <VnCardBeta + <VnCard data-key="Supplier" - url="Suppliers" + base-url="Suppliers" :descriptor="SupplierDescriptor" - :filter="filter" + :filter-panel="SupplierListFilter" + search-data-key="SupplierList" + :searchbar-props="{ + url: 'Suppliers/filter', + searchUrl: 'table', + label: 'Search suppliers', + }" /> </template> diff --git a/src/pages/Supplier/Card/SupplierConsumption.vue b/src/pages/Supplier/Card/SupplierConsumption.vue index 718de95dd..8a7021fb3 100644 --- a/src/pages/Supplier/Card/SupplierConsumption.vue +++ b/src/pages/Supplier/Card/SupplierConsumption.vue @@ -16,7 +16,6 @@ import axios from 'axios'; import { useStateStore } from 'stores/useStateStore'; import { useState } from 'src/composables/useState'; import { useArrayData } from 'composables/useArrayData'; -import RightMenu from 'src/components/common/RightMenu.vue'; const state = useState(); const stateStore = useStateStore(); @@ -174,59 +173,59 @@ onMounted(async () => { </div> </div> </Teleport> - <RightMenu> - <template #right-panel> + <QPage class="column items-center q-pa-md"> + <Teleport to="#right-panel" v-if="stateStore.isHeaderMounted()"> <SupplierConsumptionFilter data-key="SupplierConsumption" /> - </template> - </RightMenu> - <QTable - :rows="rows" - row-key="id" - hide-header - class="full-width q-mt-md" - :no-data-label="t('No results')" - > - <template #body="{ row }"> - <QTr> - <QTd no-hover> - <span class="label">{{ t('supplier.consumption.entry') }}: </span> - <span>{{ row.id }}</span> - </QTd> - <QTd no-hover> - <span class="label">{{ t('globals.date') }}: </span> - <span>{{ toDate(row.shipped) }}</span></QTd - > - <QTd colspan="6" no-hover> - <span class="label">{{ t('globals.reference') }}: </span> - <span>{{ row.invoiceNumber }}</span> - </QTd> - </QTr> - <QTr v-for="(buy, index) in row.buys" :key="index"> - <QTd no-hover> - <QBtn flat color="blue" dense no-caps>{{ buy.itemName }}</QBtn> - <ItemDescriptorProxy :id="buy.itemFk" /> - </QTd> + </Teleport> + <QTable + :rows="rows" + row-key="id" + hide-header + class="full-width q-mt-md" + :no-data-label="t('No results')" + > + <template #body="{ row }"> + <QTr> + <QTd no-hover> + <span class="label">{{ t('supplier.consumption.entry') }}: </span> + <span>{{ row.id }}</span> + </QTd> + <QTd no-hover> + <span class="label">{{ t('globals.date') }}: </span> + <span>{{ toDate(row.shipped) }}</span></QTd + > + <QTd colspan="6" no-hover> + <span class="label">{{ t('globals.reference') }}: </span> + <span>{{ row.invoiceNumber }}</span> + </QTd> + </QTr> + <QTr v-for="(buy, index) in row.buys" :key="index"> + <QTd no-hover> + <QBtn flat color="blue" dense no-caps>{{ buy.itemName }}</QBtn> + <ItemDescriptorProxy :id="buy.itemFk" /> + </QTd> - <QTd no-hover> - <span>{{ buy.subName }}</span> - <FetchedTags :item="buy" /> - </QTd> - <QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd> - <QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd> - <QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd> - </QTr> - <QTr> - <QTd colspan="5" no-hover> - <span class="label">{{ t('Total entry') }}: </span> - <span>{{ row.total }} €</span> - </QTd> - <QTd no-hover> - <span class="label">{{ t('Total stems') }}: </span> - <span>{{ row.quantity }}</span> - </QTd> - </QTr> - </template> - </QTable> + <QTd no-hover> + <span>{{ buy.subName }}</span> + <FetchedTags :item="buy" /> + </QTd> + <QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd> + <QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd> + <QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd> + </QTr> + <QTr> + <QTd colspan="5" no-hover> + <span class="label">{{ t('Total entry') }}: </span> + <span>{{ row.total }} €</span> + </QTd> + <QTd no-hover> + <span class="label">{{ t('Total stems') }}: </span> + <span>{{ row.quantity }}</span> + </QTd> + </QTr> + </template> + </QTable> + </QPage> </template> <style scoped lang="scss"> diff --git a/src/pages/Supplier/Card/SupplierContacts.vue b/src/pages/Supplier/Card/SupplierContacts.vue index f96d92ab1..6781c8d34 100644 --- a/src/pages/Supplier/Card/SupplierContacts.vue +++ b/src/pages/Supplier/Card/SupplierContacts.vue @@ -78,7 +78,7 @@ const insertRow = () => { <QBtn flat icon="add" - v-shortcut="'+'" + shortcut="+" class="cursor-pointer" color="primary" @click="insertRow()" diff --git a/src/pages/Supplier/Card/SupplierDescriptor.vue b/src/pages/Supplier/Card/SupplierDescriptor.vue index 462bdf853..37c9c1cff 100644 --- a/src/pages/Supplier/Card/SupplierDescriptor.vue +++ b/src/pages/Supplier/Card/SupplierDescriptor.vue @@ -7,8 +7,8 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import { toDateString } from 'src/filters'; +import useCardDescription from 'src/composables/useCardDescription'; import { getUrl } from 'src/composables/getUrl'; -import filter from './SupplierFilter.js'; import { useArrayData } from 'src/composables/useArrayData'; const $props = defineProps({ @@ -28,6 +28,42 @@ const { t } = useI18n(); const url = ref(); const arrayData = useArrayData(); +const filter = { + fields: [ + 'id', + 'name', + 'nickname', + 'nif', + 'payMethodFk', + 'payDemFk', + 'payDay', + 'isActive', + 'isReal', + 'isTrucker', + 'account', + ], + include: [ + { + relation: 'payMethod', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'payDem', + scope: { + fields: ['id', 'payDem'], + }, + }, + { + relation: 'client', + scope: { + fields: ['id', 'fi'], + }, + }, + ], +}; + onMounted(async () => { url.value = await getUrl(''); }); @@ -36,6 +72,11 @@ const entityId = computed(() => { return $props.id || route.params.id; }); +const data = ref(useCardDescription()); +const setData = (entity) => { + data.value = useCardDescription(entity.ref, entity.id); +}; + const supplier = computed(() => arrayData.store.data); const getEntryQueryParams = (supplier) => { @@ -62,9 +103,13 @@ const getEntryQueryParams = (supplier) => { <template> <CardDescriptor + module="Supplier" :url="`Suppliers/${entityId}`" + :title="data.title" + :subtitle="data.subtitle" :filter="filter" - data-key="Supplier" + @on-fetch="setData" + data-key="supplierDescriptor" :summary="$props.summary" > <template #body="{ entity }"> diff --git a/src/pages/Supplier/Card/SupplierFilter.js b/src/pages/Supplier/Card/SupplierFilter.js deleted file mode 100644 index 3ce5c3de2..000000000 --- a/src/pages/Supplier/Card/SupplierFilter.js +++ /dev/null @@ -1,35 +0,0 @@ -export default { - fields: [ - 'id', - 'name', - 'nickname', - 'nif', - 'payMethodFk', - 'payDemFk', - 'payDay', - 'isActive', - 'isSerious', - 'isTrucker', - 'account', - ], - include: [ - { - relation: 'payMethod', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'payDem', - scope: { - fields: ['id', 'payDem'], - }, - }, - { - relation: 'client', - scope: { - fields: ['id', 'fi'], - }, - }, - ], -}; diff --git a/src/pages/Supplier/Card/SupplierFiscalData.vue b/src/pages/Supplier/Card/SupplierFiscalData.vue index ecee5b76b..e569eb236 100644 --- a/src/pages/Supplier/Card/SupplierFiscalData.vue +++ b/src/pages/Supplier/Card/SupplierFiscalData.vue @@ -10,7 +10,6 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; import VnAccountNumber from 'src/components/common/VnAccountNumber.vue'; -import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const route = useRoute(); const { t } = useI18n(); @@ -183,11 +182,18 @@ function handleLocation(data, location) { v-model="data.isTrucker" :label="t('supplier.fiscalData.isTrucker')" /> - <VnCheckbox - v-model="data.isVies" - :label="t('globals.isVies')" - :info="t('whenActivatingIt')" - /> + <div class="row items-center"> + <QCheckbox v-model="data.isVies" :label="t('globals.isVies')" /> + <QIcon name="info" size="xs" class="cursor-pointer q-ml-sm"> + <QTooltip> + {{ + t( + 'When activating it, do not enter the country code in the ID field.' + ) + }} + </QTooltip> + </QIcon> + </div> </div> </VnRow> </template> @@ -195,8 +201,6 @@ function handleLocation(data, location) { </template> <i18n> -en: - whenActivatingIt: When activating it, do not enter the country code in the ID field. es: - whenActivatingIt: Al activarlo, no informar el código del país en el campo nif. + When activating it, do not enter the country code in the ID field.: Al activarlo, no informar el código del país en el campo nif </i18n> diff --git a/src/pages/Supplier/SupplierList.vue b/src/pages/Supplier/SupplierList.vue index 600790745..85cc11857 100644 --- a/src/pages/Supplier/SupplierList.vue +++ b/src/pages/Supplier/SupplierList.vue @@ -2,15 +2,14 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import VnTable from 'components/VnTable/VnTable.vue'; -import VnSection from 'src/components/common/VnSection.vue'; +import VnSearchbar from 'components/ui/VnSearchbar.vue'; +import RightMenu from 'src/components/common/RightMenu.vue'; +import SupplierListFilter from './SupplierListFilter.vue'; import VnInput from 'src/components/common/VnInput.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import FetchData from 'src/components/FetchData.vue'; const { t } = useI18n(); const tableRef = ref(); -const dataKey = 'SupplierList'; -const provincesOptions = ref([]); + const columns = computed(() => [ { align: 'left', @@ -105,62 +104,38 @@ const columns = computed(() => [ }, ]); </script> + <template> - <FetchData - url="Provinces" - :filter="{ fields: ['id', 'name'], order: 'name ASC' }" - @on-fetch="(data) => (provincesOptions = data)" - auto-load - /> - <VnSection - :data-key="dataKey" - :columns="columns" - prefix="supplier" - :array-data-props="{ - url: 'Suppliers/filter', - order: 'id ASC', + <VnSearchbar data-key="SuppliersList" :limit="20" :label="t('Search suppliers')" /> + <RightMenu> + <template #right-panel> + <SupplierListFilter data-key="SuppliersList" /> + </template> + </RightMenu> + <VnTable + ref="tableRef" + data-key="SuppliersList" + url="Suppliers/filter" + redirect="supplier" + :create="{ + urlCreate: 'Suppliers/newSupplier', + title: t('Create Supplier'), + onDataSaved: ({ id }) => tableRef.redirect(id), + formInitialData: {}, + mapper: (data) => { + data.name = data.socialName; + + return data; + }, }" + :right-search="false" + order="id ASC" + :columns="columns" > - <template #body> - <VnTable - ref="tableRef" - :data-key="dataKey" - :create="{ - urlCreate: 'Suppliers/newSupplier', - title: t('Create Supplier'), - onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: {}, - mapper: (data) => { - data.name = data.socialName; - delete data.socialName; - return data; - }, - }" - :columns="columns" - redirect="supplier" - :right-search="false" - > - <template #more-create-dialog="{ data }"> - <VnInput - :label="t('globals.name')" - v-model="data.socialName" - :uppercase="true" - /> - </template> - </VnTable> - </template> - <template #moreFilterPanel="{ params, searchFn }"> - <VnSelect - :label="t('globals.params.provinceFk')" - v-model="params.provinceFk" - @update:model-value="searchFn()" - :options="provincesOptions" - filled - dense - class="q-px-sm q-pr-lg" - /> - </template> - </VnSection> + <template #more-create-dialog="{ data }"> + <VnInput :label="t('globals.name')" v-model="data.socialName" :uppercase="true" /> + </template> + </VnTable> </template> <i18n> diff --git a/src/pages/Supplier/SupplierListFilter.vue b/src/pages/Supplier/SupplierListFilter.vue new file mode 100644 index 000000000..b170a35cc --- /dev/null +++ b/src/pages/Supplier/SupplierListFilter.vue @@ -0,0 +1,122 @@ +<script setup> +import { ref } from 'vue'; +import { useI18n } from 'vue-i18n'; + +import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import FetchData from 'components/FetchData.vue'; + +const props = defineProps({ + dataKey: { + type: String, + required: true, + }, +}); + +const { t } = useI18n(); + +const provincesOptions = ref([]); +const countriesOptions = ref([]); +</script> + +<template> + <FetchData + url="Provinces" + :filter="{ fields: ['id', 'name'], order: 'name ASC'}" + @on-fetch="(data) => (provincesOptions = data)" + auto-load + /> + <FetchData + url="countries" + :filter="{ fields: ['id', 'name'], order: 'name ASC'}" + @on-fetch="(data) => (countriesOptions = data)" + auto-load + /> + <VnFilterPanel + :data-key="props.dataKey" + :search-button="true" + :unremovable-params="['supplierFk']" + > + <template #tags="{ tag, formatFn }"> + <div class="q-gutter-x-xs"> + <strong>{{ t(`params.${tag.label}`) }}: </strong> + <span>{{ formatFn(tag.value) }}</span> + </div> + </template> + <template #body="{ params, searchFn }"> + <QItem> + <QItemSection> + <VnInput + v-model="params.search" + :label="t('params.search')" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.nickname" + :label="t('params.nickname')" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput v-model="params.nif" :label="t('params.nif')" is-outlined /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('params.provinceFk')" + v-model="params.provinceFk" + @update:model-value="searchFn()" + :options="provincesOptions" + option-value="id" + option-label="name" + hide-selected + dense + outlined + rounded + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('params.countryFk')" + v-model="params.countryFk" + @update:model-value="searchFn()" + :options="countriesOptions" + option-value="id" + option-label="name" + hide-selected + dense + outlined + rounded + /> + </QItemSection> + </QItem> + </template> + </VnFilterPanel> +</template> + +<i18n> +en: + params: + search: General search + nickname: Alias + nif: Tax number + provinceFk: Province + countryFk: Country +es: + params: + search: Búsqueda general + nickname: Alias + nif: NIF/CIF + provinceFk: Provincia + countryFk: País +</i18n> diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicData.vue b/src/pages/Ticket/Card/BasicData/TicketBasicData.vue index 055c9a0ff..c6a85c287 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicData.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicData.vue @@ -9,9 +9,8 @@ import FetchData from 'components/FetchData.vue'; import { useStateStore } from 'stores/useStateStore'; import { toCurrency } from 'filters/index'; import { useRole } from 'src/composables/useRole'; -import VnCheckbox from 'src/components/common/VnCheckbox.vue'; -const haveNegatives = defineModel('have-negatives', { type: Boolean, required: true }); +const haveNegatives = defineModel('haveNegatives', { type: Boolean, required: true }); const formData = defineModel({ type: Object, required: true }); const stateStore = useStateStore(); @@ -183,19 +182,22 @@ onMounted(async () => { </QCard> <QCard v-if="haveNegatives" - class="q-pa-xs q-mb-md q-ma-md color-vn-text" + class="q-pa-md q-mb-md q-ma-md color-vn-text" bordered flat style="border-color: black" > <QCardSection horizontal class="flex row items-center"> - <VnCheckbox - v-model="formData.withoutNegatives" + <QCheckbox :label="t('basicData.withoutNegatives')" - :info="t('basicData.withoutNegativesInfo')" + v-model="formData.withoutNegatives" :toggle-indeterminate="false" - size="xs" /> + <QIcon name="info" size="xs" class="q-ml-sm"> + <QTooltip max-width="350px"> + {{ t('basicData.withoutNegativesInfo') }} + </QTooltip> + </QIcon> </QCardSection> </QCard> </QDrawer> diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue index 9d70fea38..cf4481537 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue @@ -260,7 +260,7 @@ async function getZone(options) { auto-load /> <QForm> - <VnRow class="row q-gutter-md q-mb-md no-wrap"> + <VnRow> <VnSelect :label="t('ticketList.client')" v-model="clientId" @@ -296,7 +296,7 @@ async function getZone(options) { :rules="validate('ticketList.warehouse')" /> </VnRow> - <VnRow class="row q-gutter-md q-mb-md no-wrap"> + <VnRow> <VnSelect :label="t('basicData.address')" v-model="addressId" diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue index ef2eb75d6..89249b899 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue @@ -1,7 +1,7 @@ <script setup> -import { ref, computed } from 'vue'; +import { ref, onBeforeMount } from 'vue'; import { useI18n } from 'vue-i18n'; -import { useRouter } from 'vue-router'; +import { useRoute, useRouter } from 'vue-router'; import TicketBasicData from './TicketBasicData.vue'; import TicketBasicDataForm from './TicketBasicDataForm.vue'; @@ -9,69 +9,104 @@ import { useVnConfirm } from 'composables/useVnConfirm'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; -import { useArrayData } from 'src/composables/useArrayData'; const { notify } = useNotify(); +const route = useRoute(); const router = useRouter(); const { t } = useI18n(); const stepperRef = ref(null); const { openConfirmationModal } = useVnConfirm(); const step = ref(1); -const haveNegatives = ref(true); +const formData = ref({}); +const initialDataLoaded = ref(false); +const haveNegatives = ref(false); -const ticket = computed(() => useArrayData('Ticket').store?.data); +const ticketFilter = { + include: [ + { relation: 'address' }, + { + relation: 'client', + scope: { + fields: [ + 'salesPersonFk', + 'name', + 'isActive', + 'isFreezed', + 'isTaxDataChecked', + 'credit', + 'email', + 'phone', + 'mobile', + 'hasElectronicInvoice', + ], + include: { + relation: 'salesPersonUser', + scope: { fields: ['id', 'name'] }, + }, + }, + }, + { relation: 'invoiceOut' }, + ], +}; + +const getTicketData = async () => { + const params = { filter: JSON.stringify(ticketFilter) }; + const { data } = await axios.get(`tickets/${route.params.id}`, { params }); + formData.value = data; + initialDataLoaded.value = true; +}; const isFormInvalid = () => { return ( - !ticket.value.clientFk || - !ticket.value.addressFk || - !ticket.value.agencyModeFk || - !ticket.value.companyFk || - !ticket.value.shipped || - !ticket.value.landed || - !ticket.value.zoneFk + !formData.value.clientFk || + !formData.value.addressFk || + !formData.value.agencyModeFk || + !formData.value.companyFk || + !formData.value.shipped || + !formData.value.landed || + !formData.value.zoneFk ); }; const getPriceDifference = async () => { const params = { - landed: ticket.value.landed, - addressId: ticket.value.addressFk, - agencyModeId: ticket.value.agencyModeFk, - zoneId: ticket.value.zoneFk, - warehouseId: ticket.value.warehouseFk, - shipped: ticket.value.shipped, + landed: formData.value.landed, + addressId: formData.value.addressFk, + agencyModeId: formData.value.agencyModeFk, + zoneId: formData.value.zoneFk, + warehouseId: formData.value.warehouseFk, + shipped: formData.value.shipped, }; const { data } = await axios.post( - `tickets/${ticket.value.id}/priceDifference`, + `tickets/${formData.value.id}/priceDifference`, params ); - ticket.value.sale = data; + formData.value.sale = data; }; const submit = async () => { - if (!ticket.value.option) return notify(t('basicData.chooseAnOption'), 'negative'); + if (!formData.value.option) return notify(t('basicData.chooseAnOption'), 'negative'); const params = { - clientFk: ticket.value.clientFk, - nickname: ticket.value.nickname, - agencyModeFk: ticket.value.agencyModeFk, - addressFk: ticket.value.addressFk, - zoneFk: ticket.value.zoneFk, - warehouseFk: ticket.value.warehouseFk, - companyFk: ticket.value.companyFk, - shipped: ticket.value.shipped, - landed: ticket.value.landed, - isDeleted: ticket.value.isDeleted, - option: ticket.value.option, - isWithoutNegatives: ticket.value.withoutNegatives, - withWarningAccept: ticket.value.withWarningAccept, + clientFk: formData.value.clientFk, + nickname: formData.value.nickname, + agencyModeFk: formData.value.agencyModeFk, + addressFk: formData.value.addressFk, + zoneFk: formData.value.zoneFk, + warehouseFk: formData.value.warehouseFk, + companyFk: formData.value.companyFk, + shipped: formData.value.shipped, + landed: formData.value.landed, + isDeleted: formData.value.isDeleted, + option: formData.value.option, + isWithoutNegatives: formData.value.withoutNegatives, + withWarningAccept: formData.value.withWarningAccept, keepPrice: false, }; const { data } = await axios.post( - `tickets/${ticket.value.id}/componentUpdate`, + `tickets/${formData.value.id}/componentUpdate`, params ); @@ -83,7 +118,7 @@ const submit = async () => { }; const submitWithNegatives = async () => { - ticket.value.withWarningAccept = true; + formData.value.withWarningAccept = true; submit(); }; @@ -95,7 +130,7 @@ const onNextStep = async () => { await getPriceDifference(); stepperRef.value.next(); } else if (step.value === 2) { - if (haveNegatives.value && !ticket.value.withoutNegatives) + if (haveNegatives.value && !formData.value.withoutNegatives) openConfirmationModal( t('basicData.negativesConfirmTitle'), t('basicData.negativesConfirmMessage'), @@ -104,10 +139,11 @@ const onNextStep = async () => { else submit(); } }; + +onBeforeMount(async () => await getTicketData()); </script> <template> <QStepper - v-if="ticket" v-model="step" ref="stepperRef" color="primary" @@ -119,10 +155,10 @@ const onNextStep = async () => { }" > <QStep :name="1" :title="t('globals.pageTitles.basicData')" :done="step > 1"> - <TicketBasicDataForm v-model="ticket" /> + <TicketBasicDataForm v-if="initialDataLoaded" v-model="formData" /> </QStep> <QStep :name="2" :title="t('basicData.priceDifference')"> - <TicketBasicData v-model="ticket" v-model:have-negatives="haveNegatives" /> + <TicketBasicData v-model="formData" v-model:have-negatives="haveNegatives" /> </QStep> <template #navigation> <QStepperNavigation class="flex justify-between"> diff --git a/src/pages/Ticket/Card/TicketCard.vue b/src/pages/Ticket/Card/TicketCard.vue index e22d5799a..6886a8e57 100644 --- a/src/pages/Ticket/Card/TicketCard.vue +++ b/src/pages/Ticket/Card/TicketCard.vue @@ -1,13 +1,7 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import TicketDescriptor from './TicketDescriptor.vue'; -import filter from './TicketFilter.js'; </script> <template> - <VnCardBeta - data-key="Ticket" - url="Tickets" - :descriptor="TicketDescriptor" - :filter="filter" - /> + <VnCardBeta data-key="Ticket" base-url="Tickets" :descriptor="TicketDescriptor" /> </template> diff --git a/src/pages/Ticket/Card/TicketComponents.vue b/src/pages/Ticket/Card/TicketComponents.vue index 5936ffc28..842607e0c 100644 --- a/src/pages/Ticket/Card/TicketComponents.vue +++ b/src/pages/Ticket/Card/TicketComponents.vue @@ -19,7 +19,7 @@ import RightMenu from 'src/components/common/RightMenu.vue'; const route = useRoute(); const { t } = useI18n(); const salesRef = ref(null); -const arrayData = useArrayData('Ticket'); +const arrayData = useArrayData('ticketData'); const { store } = arrayData; const ticketData = computed(() => store.data); diff --git a/src/pages/Ticket/Card/TicketDescriptor.vue b/src/pages/Ticket/Card/TicketDescriptor.vue index c5f3233b1..c9849d631 100644 --- a/src/pages/Ticket/Card/TicketDescriptor.vue +++ b/src/pages/Ticket/Card/TicketDescriptor.vue @@ -6,11 +6,9 @@ import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy import CardDescriptor from 'components/ui/CardDescriptor.vue'; import TicketDescriptorMenu from './TicketDescriptorMenu.vue'; import VnLv from 'src/components/ui/VnLv.vue'; +import useCardDescription from 'src/composables/useCardDescription'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import { toDateTimeFormat } from 'src/filters/date'; -import filter from './TicketFilter.js'; -import FetchData from 'src/components/FetchData.vue'; -import TicketProblems from 'src/components/TicketProblems.vue'; const $props = defineProps({ id: { @@ -30,24 +28,100 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); -const problems = ref({}); + +const filter = { + include: [ + { + relation: 'address', + scope: { + fields: ['id', 'name', 'mobile', 'phone', 'incotermsFk'], + }, + }, + { + relation: 'client', + scope: { + fields: [ + 'id', + 'name', + 'salesPersonFk', + 'phone', + 'mobile', + 'email', + 'isActive', + 'isFreezed', + 'isTaxDataChecked', + 'hasElectronicInvoice', + ], + include: [ + { + relation: 'user', + scope: { + fields: ['id', 'lang'], + }, + }, + { relation: 'salesPersonUser' }, + ], + }, + }, + { + relation: 'ticketState', + scope: { + include: { relation: 'state' }, + }, + }, + { + relation: 'warehouse', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'agencyMode', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'zone', + scope: { + fields: [ + 'agencyModeFk', + 'bonus', + 'hour', + 'id', + 'isVolumetric', + 'itemMaxSize', + 'm3Max', + 'name', + 'price', + 'travelingDays', + ], + }, + }, + ], +}; + +const data = ref(useCardDescription()); function ticketFilter(ticket) { return JSON.stringify({ clientFk: ticket.clientFk }); } + +const setData = (entity) => { + data.value = useCardDescription(entity.ref, entity.id); +}; </script> <template> - <FetchData - :url="`Tickets/${entityId}/getTicketProblems`" - auto-load - @on-fetch="(data) => ([problems] = data)" - /> <CardDescriptor + module="Ticket" :url="`Tickets/${entityId}`" :filter="filter" - data-key="Ticket" + :title="data.title" + :subtitle="data.subtitle" + @on-fetch="setData" :summary="$props.summary" + data-key="ticketData" width="lg-width" > <template #menu="{ entity }"> @@ -93,9 +167,48 @@ function ticketFilter(ticket) { <VnLv :label="t('globals.warehouse')" :value="entity.warehouse?.name" /> <VnLv :label="t('globals.alias')" :value="entity.nickname" /> </template> - <template #icons> - <QCardActions class="q-gutter-x-xs"> - <TicketProblems :row="problems" /> + <template #icons="{ entity }"> + <QCardActions class="q-gutter-x-md"> + <QIcon + v-if="entity.client.isActive == false" + name="vn:disabled" + size="xs" + color="primary" + > + <QTooltip>{{ t('Client inactive') }}</QTooltip> + </QIcon> + <QIcon + v-if="entity.client.isFreezed == true" + name="vn:frozen" + size="xs" + color="primary" + > + <QTooltip>{{ t('Client Frozen') }}</QTooltip> + </QIcon> + <QIcon + v-if="entity?.problem?.includes('hasRisk')" + name="vn:risk" + size="xs" + color="primary" + > + <QTooltip>{{ t('Client has debt') }}</QTooltip> + </QIcon> + <QIcon + v-if="entity.client.isTaxDataChecked == false" + name="vn:no036" + size="xs" + color="primary" + > + <QTooltip>{{ t('Client not checked') }}</QTooltip> + </QIcon> + <QIcon + v-if="entity.isDeleted == true" + name="vn:deletedTicket" + size="xs" + color="primary" + > + <QTooltip>{{ t('This ticket is deleted') }}</QTooltip> + </QIcon> </QCardActions> </template> <template #actions="{ entity }"> diff --git a/src/pages/Ticket/Card/TicketExpedition.vue b/src/pages/Ticket/Card/TicketExpedition.vue index f8084ff2f..166e86978 100644 --- a/src/pages/Ticket/Card/TicketExpedition.vue +++ b/src/pages/Ticket/Card/TicketExpedition.vue @@ -40,7 +40,7 @@ const expeditionsFilter = computed(() => ({ order: ['created DESC'], })); -const ticketArrayData = useArrayData('Ticket'); +const ticketArrayData = useArrayData('ticketData'); const ticketStore = ticketArrayData.store; const ticketData = computed(() => ticketStore.data); diff --git a/src/pages/Ticket/Card/TicketFilter.js b/src/pages/Ticket/Card/TicketFilter.js deleted file mode 100644 index 7846f1658..000000000 --- a/src/pages/Ticket/Card/TicketFilter.js +++ /dev/null @@ -1,72 +0,0 @@ -export default { - include: [ - { - relation: 'address', - scope: { - fields: ['id', 'name', 'mobile', 'phone', 'incotermsFk'], - }, - }, - { - relation: 'client', - scope: { - fields: [ - 'id', - 'name', - 'salesPersonFk', - 'phone', - 'mobile', - 'email', - 'isActive', - 'isFreezed', - 'isTaxDataChecked', - 'hasElectronicInvoice', - 'credit', - ], - include: [ - { - relation: 'user', - scope: { - fields: ['id', 'lang'], - }, - }, - { relation: 'salesPersonUser' }, - ], - }, - }, - { - relation: 'ticketState', - scope: { - include: { relation: 'state' }, - }, - }, - { - relation: 'warehouse', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'agencyMode', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'zone', - scope: { - fields: [ - 'agencyModeFk', - 'bonus', - 'hour', - 'id', - 'isVolumetric', - 'itemMaxSize', - 'm3Max', - 'name', - 'price', - 'travelingDays', - ], - }, - }, - ], -}; diff --git a/src/pages/Ticket/Card/TicketNotes.vue b/src/pages/Ticket/Card/TicketNotes.vue index feb88bf84..f558b71cc 100644 --- a/src/pages/Ticket/Card/TicketNotes.vue +++ b/src/pages/Ticket/Card/TicketNotes.vue @@ -32,7 +32,7 @@ watch( crudModelFilter.where.ticketFk = route.params.id; store.filter = crudModelFilter; await ticketNotesCrudRef.value.reload(); - }, + } ); function handleDelete(row) { ticketNotesCrudRef.value.remove([row]); @@ -105,7 +105,7 @@ async function handleSave() { <VnRow v-if="observationTypes.length > rows.length"> <QBtn icon="add_circle" - v-shortcut="'+'" + shortcut="+" flat class="fill-icon-on-hover q-ml-md" color="primary" diff --git a/src/pages/Ticket/Card/TicketPackage.vue b/src/pages/Ticket/Card/TicketPackage.vue index 5fbf4c800..8ebdb4401 100644 --- a/src/pages/Ticket/Card/TicketPackage.vue +++ b/src/pages/Ticket/Card/TicketPackage.vue @@ -41,7 +41,7 @@ watch( crudModelFilter.where.ticketFk = route.params.id; store.filter = crudModelFilter; await ticketPackagingsCrudRef.value.reload(); - }, + } ); </script> @@ -118,7 +118,7 @@ watch( <VnRow> <QBtn icon="add_circle" - v-shortcut="'+'" + shortcut="+" flat class="fill-icon-on-hover q-ml-md" color="primary" diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index 6f02a2ce6..f5fb50ecf 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -14,7 +14,7 @@ import VnImg from 'src/components/ui/VnImg.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import TicketSaleMoreActions from './TicketSaleMoreActions.vue'; -import TicketTransferProxy from './TicketTransferProxy.vue'; +import TicketTransfer from './TicketTransfer.vue'; import { toCurrency, toPercentage } from 'src/filters'; import { useArrayData } from 'composables/useArrayData'; @@ -23,7 +23,6 @@ import useNotify from 'src/composables/useNotify.js'; import axios from 'axios'; import VnTable from 'src/components/VnTable/VnTable.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; -import TicketProblems from 'src/components/TicketProblems.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; const route = useRoute(); @@ -35,7 +34,7 @@ const editPriceProxyRef = ref(null); const editManaProxyRef = ref(null); const stateBtnDropdownRef = ref(null); const quasar = useQuasar(); -const arrayData = useArrayData('Ticket'); +const arrayData = useArrayData('ticketData'); const { store } = arrayData; const selectedRows = ref([]); const hasSelectedRows = computed(() => selectedRows.value.length > 0); @@ -627,9 +626,8 @@ watch( @click="setTransferParams()" data-cy="ticketSaleTransferBtn" > - <QTooltip>{{ t('ticketSale.transferLines') }}</QTooltip> - <TicketTransferProxy - class="full-width" + <QTooltip>{{ t('Transfer lines') }}</QTooltip> + <TicketTransfer :transfer="transfer" :ticket="store.data" @refresh-data="resetChanges()" @@ -699,7 +697,53 @@ watch( :disabled-attr="isTicketEditable" > <template #column-statusIcons="{ row }"> - <TicketProblems :row="row" /> + <router-link + v-if="row.claim?.claimFk" + :to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }" + > + <QIcon color="primary" name="vn:claims" size="xs"> + <QTooltip> + {{ t('ticketSale.claim') }}: + {{ row.claim?.claimFk }} + </QTooltip> + </QIcon> + </router-link> + <QIcon v-if="row.visible < 0" color="primary" name="warning" size="xs"> + <QTooltip> + {{ t('ticketSale.visible') }}: {{ row.visible || 0 }} + </QTooltip> + </QIcon> + <QIcon + v-if="row.reserved" + color="primary" + name="vn:reserva" + size="xs" + data-cy="ticketSaleReservedIcon" + > + <QTooltip> + {{ t('ticketSale.reserved') }} + </QTooltip> + </QIcon> + <QIcon + v-if="row.itemShortage" + color="primary" + name="vn:unavailable" + size="xs" + > + <QTooltip> + {{ t('ticketSale.noVisible') }} + </QTooltip> + </QIcon> + <QIcon + v-if="row.hasComponentLack" + color="primary" + name="vn:components" + size="xs" + > + <QTooltip> + {{ t('ticketSale.hasComponentLack') }} + </QTooltip> + </QIcon> </template> <template #body-cell-picture="{ row }"> <QTd> @@ -837,7 +881,7 @@ watch( color="primary" fab icon="add" - v-shortcut="'+'" + shortcut="+" data-cy="ticketSaleAddToBasketBtn" /> <QTooltip class="text-no-wrap"> diff --git a/src/pages/Ticket/Card/TicketService.vue b/src/pages/Ticket/Card/TicketService.vue index 6ce69a6aa..d045eadee 100644 --- a/src/pages/Ticket/Card/TicketService.vue +++ b/src/pages/Ticket/Card/TicketService.vue @@ -40,7 +40,7 @@ watch( async () => { store.filter = crudModelFilter.value; await ticketServiceCrudRef.value.reload(); - }, + } ); onMounted(async () => await getDefaultTaxClass()); @@ -59,7 +59,7 @@ const createRefund = async () => { t('service.createRefundSuccess', { ticketId: refundTicket.id, }), - 'positive', + 'positive' ); router.push({ name: 'TicketSale', params: { id: refundTicket.id } }); }; @@ -225,7 +225,7 @@ async function handleSave() { color="primary" icon="add" @click="ticketServiceCrudRef.insert()" - v-shortcut="'+'" + shortcut="+" /> </QPageSticky> </template> diff --git a/src/pages/Ticket/Card/TicketSplit.vue b/src/pages/Ticket/Card/TicketSplit.vue deleted file mode 100644 index e79057266..000000000 --- a/src/pages/Ticket/Card/TicketSplit.vue +++ /dev/null @@ -1,37 +0,0 @@ -<script setup> -import { ref } from 'vue'; - -import VnInputDate from 'src/components/common/VnInputDate.vue'; -import split from './components/split'; -const emit = defineEmits(['ticketTransfered']); - -const $props = defineProps({ - ticket: { - type: [Array, Object], - default: () => {}, - }, -}); - -const splitDate = ref(Date.vnNew()); - -const splitSelectedRows = async () => { - const tickets = Array.isArray($props.ticket) ? $props.ticket : [$props.ticket]; - await split(tickets, splitDate.value); - emit('ticketTransfered', tickets); -}; -</script> - -<template> - <VnInputDate class="q-mr-sm" :label="$t('New date')" v-model="splitDate" clearable /> - <QBtn class="q-mr-sm" color="primary" label="Split" @click="splitSelectedRows"></QBtn> -</template> -<style lang="scss"> -.q-table__bottom.row.items-center.q-table__bottom--nodata { - border-top: none; -} -</style> -<i18n> -es: - Sales to transfer: Líneas a transferir - Destination ticket: Ticket destinatario -</i18n> diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index 5838efa88..8cb518823 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -20,7 +20,6 @@ import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnToSummary from 'src/components/ui/VnToSummary.vue'; import TicketDescriptorMenu from './TicketDescriptorMenu.vue'; -import TicketProblems from 'src/components/TicketProblems.vue'; const route = useRoute(); const { notify } = useNotify(); @@ -41,7 +40,7 @@ const editableStates = ref([]); const ticketUrl = ref(); const grafanaUrl = 'https://grafana.verdnatura.es'; const stateBtnDropdownRef = ref(); -const descriptorData = useArrayData('Ticket'); +const descriptorData = useArrayData('ticketData'); onMounted(async () => { ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/'; @@ -321,7 +320,83 @@ onMounted(async () => { <template #body="props"> <QTr :props="props"> <QTd class="q-gutter-x-xs"> - <TicketProblems :row="props.row" /> + <QBtn + flat + round + icon="vn:claims" + v-if="props.row.claim" + color="primary" + :to="{ + name: 'ClaimCard', + params: { + id: props.row.claim.claimFk, + }, + }" + > + <QTooltip> + {{ t('ticket.summary.claim') }}: + {{ props.row.claim.claimFk }} + </QTooltip> + </QBtn> + <QBtn + flat + round + icon="vn:claims" + v-if="props.row.claimBeginning" + color="primary" + :to="{ + name: 'ClaimCard', + params: { + id: props.row.claimBeginning.claimFk, + }, + }" + > + <QTooltip> + {{ t('ticket.summary.claim') }}: + {{ props.row.claimBeginning.claimFk }} + </QTooltip> + </QBtn> + <QIcon + name="warning" + v-show="props.row.visible < 0" + color="primary" + size="xs" + > + <QTooltip> + {{ t('globals.visible') }}: + {{ props.row.visible }} + </QTooltip> + </QIcon> + <QIcon + name="vn:reserved" + v-show="props.row.reserved" + color="primary" + size="xs" + > + <QTooltip> + {{ t('ticket.summary.reserved') }} + </QTooltip> + </QIcon> + <QIcon + name="vn:unavailable" + v-show="props.row.itemShortage" + color="primary" + size="xs" + > + <QTooltip> + {{ t('ticket.summary.itemShortage') }} + </QTooltip> + </QIcon> + <QIcon + name="vn:components" + v-show="props.row.hasComponentLack" + color="primary" + size="xs" + > + <QTooltip> + {{ t('ticket.summary.hasComponentLack') }} + </QTooltip> + </QIcon> </QTd> <QTd> <QBtn class="link" flat> diff --git a/src/pages/Ticket/Card/TicketTracking.vue b/src/pages/Ticket/Card/TicketTracking.vue index acf464fb1..f4b8544d3 100644 --- a/src/pages/Ticket/Card/TicketTracking.vue +++ b/src/pages/Ticket/Card/TicketTracking.vue @@ -19,7 +19,7 @@ watch( async (val) => { paginateFilter.where.ticketFk = val; paginateRef.value.fetch(); - }, + } ); const paginateFilter = reactive({ @@ -119,7 +119,7 @@ const openCreateModal = () => createTrackingDialogRef.value.show(); color="primary" fab icon="add" - v-shortcut="'+'" + shortcut="+" /> <QTooltip class="text-no-wrap"> {{ t('tracking.addState') }} diff --git a/src/pages/Ticket/Card/TicketTransfer.vue b/src/pages/Ticket/Card/TicketTransfer.vue index ffa964c92..005d74a0e 100644 --- a/src/pages/Ticket/Card/TicketTransfer.vue +++ b/src/pages/Ticket/Card/TicketTransfer.vue @@ -1,11 +1,11 @@ <script setup> import { ref, computed, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; + import VnInput from 'src/components/common/VnInput.vue'; import TicketTransferForm from './TicketTransferForm.vue'; import { toDateFormat } from 'src/filters/date.js'; -const emit = defineEmits(['ticketTransfered']); const $props = defineProps({ mana: { @@ -21,15 +21,16 @@ const $props = defineProps({ default: () => {}, }, ticket: { - type: [Array, Object], + type: Object, default: () => {}, }, }); -onMounted(() => (_transfer.value = $props.transfer)); const { t } = useI18n(); +const QPopupProxyRef = ref(null); const transferFormRef = ref(null); const _transfer = ref(); + const transferLinesColumns = computed(() => [ { label: t('ticketList.id'), @@ -85,74 +86,76 @@ const handleRowClick = (row) => { transferFormRef.value.transferSales(ticketId); } }; + +onMounted(() => (_transfer.value = $props.transfer)); </script> <template> - <QTable - :rows="transfer.sales" - :columns="transferLinesColumns" - :title="t('Sales to transfer')" - row-key="id" - :pagination="{ rowsPerPage: 0 }" - class="full-width q-mt-md" - :no-data-label="t('globals.noResults')" - > - <template #body-cell-quantity="{ row }"> - <QTd @click.stop> - <VnInput - v-model.number="row.quantity" - :clearable="false" - style="max-width: 60px" - /> - </QTd> - </template> - </QTable> - <QSeparator vertical spaced /> - <QTable - v-if="transfer.lastActiveTickets" - :rows="transfer.lastActiveTickets" - :columns="destinationTicketColumns" - :title="t('Destination ticket')" - row-key="id" - class="full-width q-mt-md" - @row-click="(_, row) => handleRowClick(row)" - :no-data-label="t('globals.noResults')" - :pagination="{ rowsPerPage: 0 }" - > - <template #body-cell-address="{ row }"> - <QTd @click.stop> - <span> - {{ row.nickname }} - {{ row.name }} - {{ row.street }} - {{ row.postalCode }} - {{ row.city }} - </span> - <QTooltip> - {{ row.nickname }} - {{ row.name }} - {{ row.street }} - {{ row.postalCode }} - {{ row.city }} - </QTooltip> - </QTd> - </template> + <QPopupProxy ref="QPopupProxyRef" data-cy="ticketTransferPopup"> + <QCard class="q-px-md" style="display: flex; width: 80vw"> + <QTable + :rows="transfer.sales" + :columns="transferLinesColumns" + :title="t('Sales to transfer')" + row-key="id" + :pagination="{ rowsPerPage: 0 }" + class="full-width q-mt-md" + :no-data-label="t('globals.noResults')" + > + <template #body-cell-quantity="{ row }"> + <QTd @click.stop> + <VnInput + v-model.number="row.quantity" + :clearable="false" + style="max-width: 60px" + /> + </QTd> + </template> + </QTable> + <QSeparator vertical spaced /> + <QTable + v-if="transfer.lastActiveTickets" + :rows="transfer.lastActiveTickets" + :columns="destinationTicketColumns" + :title="t('Destination ticket')" + row-key="id" + class="full-width q-mt-md" + @row-click="(_, row) => handleRowClick(row)" + > + <template #body-cell-address="{ row }"> + <QTd @click.stop> + <span> + {{ row.nickname }} + {{ row.name }} + {{ row.street }} + {{ row.postalCode }} + {{ row.city }} + </span> + <QTooltip> + {{ row.nickname }} + {{ row.name }} + {{ row.street }} + {{ row.postalCode }} + {{ row.city }} + </QTooltip> + </QTd> + </template> - <template #no-data> - <TicketTransferForm ref="transferFormRef" v-bind="$props" /> - </template> - <template #bottom> - <TicketTransferForm ref="transferFormRef" v-bind="$props" /> - </template> - </QTable> + <template #no-data> + <TicketTransferForm ref="transferFormRef" v-bind="$props" /> + </template> + <template #bottom> + <TicketTransferForm ref="transferFormRef" v-bind="$props" /> + </template> + </QTable> + </QCard> + </QPopupProxy> </template> -<style lang="scss"> -.q-table__bottom.row.items-center.q-table__bottom--nodata { - border-top: none; -} -</style> + <i18n> es: Sales to transfer: Líneas a transferir Destination ticket: Ticket destinatario + Transfer to ticket: Transferir a ticket + New ticket: Nuevo ticket </i18n> diff --git a/src/pages/Ticket/Card/TicketTransferProxy.vue b/src/pages/Ticket/Card/TicketTransferProxy.vue deleted file mode 100644 index 3f3f018df..000000000 --- a/src/pages/Ticket/Card/TicketTransferProxy.vue +++ /dev/null @@ -1,54 +0,0 @@ -<script setup> -import { ref } from 'vue'; -import TicketTransfer from './TicketTransfer.vue'; -import Split from './TicketSplit.vue'; -const emit = defineEmits(['ticketTransfered']); - -const $props = defineProps({ - mana: { - type: Number, - default: null, - }, - newPrice: { - type: Number, - default: 0, - }, - transfer: { - type: Object, - default: () => {}, - }, - ticket: { - type: [Array, Object], - default: () => {}, - }, - split: { - type: Boolean, - default: false, - }, -}); - -const popupProxyRef = ref(null); -const splitRef = ref(null); -const transferRef = ref(null); -</script> - -<template> - <QPopupProxy ref="popupProxyRef" data-cy="ticketTransferPopup"> - <div class="flex row items-center q-ma-lg" v-if="$props.split"> - <Split - ref="splitRef" - @splitSelectedRows="splitSelectedRows" - :ticket="$props.ticket" - /> - </div> - - <div v-else> - <TicketTransfer - ref="transferRef" - :ticket="$props.ticket" - :sales="$props.sales" - :transfer="$props.transfer" - /> - </div> - </QPopupProxy> -</template> diff --git a/src/pages/Ticket/Card/components/split.js b/src/pages/Ticket/Card/components/split.js deleted file mode 100644 index afa1d5cd6..000000000 --- a/src/pages/Ticket/Card/components/split.js +++ /dev/null @@ -1,22 +0,0 @@ -import axios from 'axios'; -import notifyResults from 'src/utils/notifyResults'; - -export default async function (data, date) { - const reducedData = data.reduce((acc, item) => { - const existing = acc.find(({ ticketFk }) => ticketFk === item.id); - if (existing) { - existing.sales.push(item.saleFk); - } else { - acc.push({ ticketFk: item.id, sales: [item.saleFk], date }); - } - return acc; - }, []); - - const promises = reducedData.map((params) => axios.post(`Tickets/split`, params)); - - const results = await Promise.allSettled(promises); - - notifyResults(results, 'ticketFk'); - - return results; -} diff --git a/src/pages/Ticket/Negative/TicketLackDetail.vue b/src/pages/Ticket/Negative/TicketLackDetail.vue deleted file mode 100644 index dcf835d03..000000000 --- a/src/pages/Ticket/Negative/TicketLackDetail.vue +++ /dev/null @@ -1,198 +0,0 @@ -<script setup> -import { computed, onMounted, onUnmounted, ref } from 'vue'; -import { useI18n } from 'vue-i18n'; -import ChangeQuantityDialog from './components/ChangeQuantityDialog.vue'; -import ChangeStateDialog from './components/ChangeStateDialog.vue'; -import ChangeItemDialog from './components/ChangeItemDialog.vue'; -import TicketTransferProxy from '../Card/TicketTransferProxy.vue'; -import FetchData from 'src/components/FetchData.vue'; -import { useStateStore } from 'stores/useStateStore'; -import { useState } from 'src/composables/useState'; - -import { useRoute } from 'vue-router'; -import TicketLackTable from './TicketLackTable.vue'; -import VnPopupProxy from 'src/components/common/VnPopupProxy.vue'; -import ItemProposalProxy from 'src/pages/Item/components/ItemProposalProxy.vue'; - -import { useQuasar } from 'quasar'; -const quasar = useQuasar(); -const { t } = useI18n(); -const editableStates = ref([]); -const stateStore = useStateStore(); -const tableRef = ref(); -const changeItemDialogRef = ref(null); -const changeStateDialogRef = ref(null); -const changeQuantityDialogRef = ref(null); -const showProposalDialog = ref(false); -const showChangeQuantityDialog = ref(false); -const selectedRows = ref([]); -const route = useRoute(); -onMounted(() => { - stateStore.rightDrawer = false; -}); -onUnmounted(() => { - stateStore.rightDrawer = true; -}); - -const entityId = computed(() => route.params.id); -const item = ref({}); - -const itemProposalSelected = ref(null); -const reload = async () => { - tableRef.value.tableRef.reload(); -}; -defineExpose({ reload }); -const filter = computed(() => ({ - scopeDays: route.query.days, - showType: true, - alertLevelCode: 'FREE', - date: Date.vnNew(), - warehouseFk: useState().getUser().value.warehouseFk, -})); -const itemProposalEvt = (data) => { - const { itemProposal } = data; - itemProposalSelected.value = itemProposal; - reload(); -}; - -function onBuysFetched(data) { - Object.assign(item.value, data[0]); -} -const showItemProposal = () => { - quasar - .dialog({ - component: ItemProposalProxy, - componentProps: { - itemLack: tableRef.value.itemLack, - replaceAction: true, - sales: selectedRows.value, - }, - }) - .onOk(itemProposalEvt); -}; -</script> - -<template> - <FetchData - url="States/editableStates" - @on-fetch="(data) => (editableStates = data)" - auto-load - /> - <FetchData - :url="`Items/${entityId}/getCard`" - :fields="['longName']" - @on-fetch="(data) => (item = data)" - auto-load - /> - <FetchData - :url="`Buys/latestBuysFilter`" - :fields="['longName']" - :filter="{ where: { 'i.id': entityId } }" - @on-fetch="onBuysFetched" - auto-load - /> - - <TicketLackTable - ref="tableRef" - :filter="filter" - @update:selection="({ value }, _) => (selectedRows = value)" - > - <template #top-right> - <QBtnGroup push class="q-mr-lg" style="column-gap: 1px"> - <QBtn - data-cy="transferLines" - color="primary" - :disable="!(selectedRows.length === 1)" - > - <template #default> - <QIcon name="vn:splitline" /> - <QIcon name="vn:ticket" /> - - <QTooltip>{{ t('ticketSale.transferLines') }} </QTooltip> - <TicketTransferProxy - ref="transferFormRef" - split="true" - :ticket="selectedRows" - :transfer="{ - sales: selectedRows, - lastActiveTickets: selectedRows.map((row) => row.id), - }" - @ticket-transfered="reload" - ></TicketTransferProxy> - </template> - </QBtn> - <QBtn - color="primary" - @click="showProposalDialog = true" - :disable="selectedRows.length < 1" - data-cy="itemProposal" - > - <QIcon - name="import_export" - class="rotate-90" - @click="showItemProposal" - ></QIcon> - <QTooltip bottom anchor="bottom right"> - {{ t('itemProposal') }} - </QTooltip> - </QBtn> - <VnPopupProxy - data-cy="changeItem" - icon="sync" - :disable="selectedRows.length < 1" - :tooltip="t('negative.detail.modal.changeItem.title')" - > - <template #extraIcon> <QIcon name="vn:item" /> </template> - <template v-slot="{ popup }"> - <ChangeItemDialog - ref="changeItemDialogRef" - @update-item="popup.hide()" - :selected-rows="selectedRows" - /></template> - </VnPopupProxy> - <VnPopupProxy - data-cy="changeState" - icon="sync" - :disable="selectedRows.length < 1" - :tooltip="t('negative.detail.modal.changeState.title')" - > - <template #extraIcon> <QIcon name="vn:eye" /> </template> - <template v-slot="{ popup }"> - <ChangeStateDialog - ref="changeStateDialogRef" - @update-state="popup.hide()" - :selected-rows="selectedRows" - /></template> - </VnPopupProxy> - <VnPopupProxy - data-cy="changeQuantity" - icon="sync" - :disable="selectedRows.length < 1" - :tooltip="t('negative.detail.modal.changeQuantity.title')" - @click="showChangeQuantityDialog = true" - > - <template #extraIcon> <QIcon name="exposure" /> </template> - <template v-slot="{ popup }"> - <ChangeQuantityDialog - ref="changeQuantityDialogRef" - @update-quantity="popup.hide()" - :selected-rows="selectedRows" - /></template> - </VnPopupProxy> </QBtnGroup - ></template> - </TicketLackTable> -</template> -<style lang="scss" scoped> -.list-enter-active, -.list-leave-active { - transition: all 1s ease; -} -.list-enter-from, -.list-leave-to { - opacity: 0; - background-color: $primary; -} -.q-table.q-table__container > div:first-child { - border-radius: unset; -} -</style> diff --git a/src/pages/Ticket/Negative/TicketLackFilter.vue b/src/pages/Ticket/Negative/TicketLackFilter.vue deleted file mode 100644 index 3762f453d..000000000 --- a/src/pages/Ticket/Negative/TicketLackFilter.vue +++ /dev/null @@ -1,175 +0,0 @@ -<script setup> -import { ref } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import FetchData from 'components/FetchData.vue'; -import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; -const { t } = useI18n(); -const props = defineProps({ - dataKey: { - type: String, - required: true, - }, -}); - -const to = Date.vnNew(); -to.setDate(to.getDate() + 1); - -const warehouses = ref(); -const categoriesOptions = ref([]); -const itemTypesRef = ref(null); -const itemTypesOptions = ref([]); - -const itemTypesFilter = { - fields: ['id', 'name', 'categoryFk'], - include: 'category', - order: 'name ASC', - where: {}, -}; -const onCategoryChange = async (categoryFk, search) => { - if (!categoryFk) { - itemTypesFilter.where.categoryFk = null; - delete itemTypesFilter.where.categoryFk; - } else { - itemTypesFilter.where.categoryFk = categoryFk; - } - search(); - await itemTypesRef.value.fetch(); -}; -const emit = defineEmits(['set-user-params']); - -const setUserParams = (params) => { - emit('set-user-params', params); -}; -</script> - -<template> - <FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load /> - <FetchData - url="ItemCategories" - :filter="{ fields: ['id', 'name'], order: 'name ASC' }" - @on-fetch="(data) => (categoriesOptions = data)" - auto-load - /> - - <FetchData - ref="itemTypesRef" - url="ItemTypes" - :filter="itemTypesFilter" - @on-fetch="(data) => (itemTypesOptions = data)" - auto-load - /> - - <VnFilterPanel - :data-key="props.dataKey" - :search-button="true" - @set-user-params="setUserParams" - > - <template #tags="{ tag, formatFn }"> - <div class="q-gutter-x-xs"> - <strong>{{ t(`negative.${tag.label}`) }}</strong> - <span>{{ formatFn(tag.value) }}</span> - </div> - </template> - <template #body="{ params, searchFn }"> - <QList dense class="q-gutter-y-sm q-mt-sm"> - <QItem> - <QItemSection> - <VnInput - v-model="params.days" - :label="t('negative.days')" - dense - is-outlined - type="number" - @update:model-value=" - (value) => { - setUserParams(params); - } - " - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput - v-model="params.id" - :label="t('negative.id')" - dense - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput - v-model="params.producer" - :label="t('negative.producer')" - dense - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput - v-model="params.origen" - :label="t('negative.origen')" - dense - is-outlined - /> - </QItemSection> </QItem - ><QItem> - <QItemSection v-if="categoriesOptions"> - <VnSelect - :label="t('negative.categoryFk')" - v-model="params.categoryFk" - @update:model-value=" - ($event) => onCategoryChange($event, searchFn) - " - :options="categoriesOptions" - option-value="id" - option-label="name" - hide-selected - dense - outlined - rounded - /> </QItemSection - ><QItemSection v-else> - <QSkeleton class="full-width" type="QSelect" /> - </QItemSection> - </QItem> - <QItem> - <QItemSection v-if="itemTypesOptions"> - <VnSelect - :label="t('negative.type')" - v-model="params.typeFk" - @update:model-value="searchFn()" - :options="itemTypesOptions" - option-value="id" - option-label="name" - hide-selected - dense - outlined - rounded - > - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel>{{ scope.opt?.name }}</QItemLabel> - <QItemLabel caption>{{ - scope.opt?.category?.name - }}</QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> </QItemSection - ><QItemSection v-else> - <QSkeleton class="full-width" type="QSelect" /> - </QItemSection> - </QItem> - </QList> - </template> - </VnFilterPanel> -</template> diff --git a/src/pages/Ticket/Negative/TicketLackList.vue b/src/pages/Ticket/Negative/TicketLackList.vue deleted file mode 100644 index d1e8b823a..000000000 --- a/src/pages/Ticket/Negative/TicketLackList.vue +++ /dev/null @@ -1,227 +0,0 @@ -<script setup> -import { computed, ref, reactive } from 'vue'; -import { useI18n } from 'vue-i18n'; -import { useStateStore } from 'stores/useStateStore'; -import VnTable from 'components/VnTable/VnTable.vue'; -import { onBeforeMount } from 'vue'; -import { dashIfEmpty, toDate, toHour } from 'src/filters'; -import { useRouter } from 'vue-router'; -import { useState } from 'src/composables/useState'; -import { useRole } from 'src/composables/useRole'; -import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; -import RightMenu from 'src/components/common/RightMenu.vue'; -import TicketLackFilter from './TicketLackFilter.vue'; -onBeforeMount(() => { - stateStore.$state.rightDrawer = true; -}); -const router = useRouter(); -const stateStore = useStateStore(); -const { t } = useI18n(); -const selectedRows = ref([]); -const tableRef = ref(); -const filterParams = ref({}); -const negativeParams = reactive({ - days: useRole().likeAny('buyer') ? 2 : 0, - warehouseFk: useState().getUser().value.warehouseFk, -}); -const redirectToCreateView = ({ itemFk }) => { - router.push({ - name: 'NegativeDetail', - params: { id: itemFk }, - query: { days: filterParams.value.days ?? negativeParams.days }, - }); -}; -const columns = computed(() => [ - { - name: 'date', - align: 'center', - label: t('negative.date'), - format: ({ timed }) => toDate(timed), - sortable: true, - cardVisible: true, - isId: true, - columnFilter: { - component: 'date', - }, - }, - { - columnClass: 'shrink', - name: 'timed', - align: 'center', - label: t('negative.timed'), - format: ({ timed }) => toHour(timed), - sortable: true, - cardVisible: true, - columnFilter: { - component: 'time', - }, - }, - { - name: 'itemFk', - align: 'center', - label: t('negative.id'), - format: ({ itemFk }) => itemFk, - sortable: true, - columnFilter: { - component: 'input', - type: 'number', - columnClass: 'shrink', - }, - }, - { - name: 'longName', - align: 'center', - label: t('negative.longName'), - field: ({ longName }) => longName, - - sortable: true, - headerStyle: 'width: 350px', - cardVisible: true, - columnClass: 'expand', - }, - { - name: 'producer', - align: 'center', - label: t('negative.supplier'), - field: ({ producer }) => dashIfEmpty(producer), - sortable: true, - columnClass: 'shrink', - }, - { - name: 'inkFk', - align: 'center', - label: t('negative.colour'), - field: ({ inkFk }) => inkFk, - sortable: true, - cardVisible: true, - }, - { - name: 'size', - align: 'center', - label: t('negative.size'), - field: ({ size }) => size, - sortable: true, - cardVisible: true, - columnFilter: { - component: 'input', - type: 'number', - columnClass: 'shrink', - }, - }, - { - name: 'category', - align: 'center', - label: t('negative.origen'), - field: ({ category }) => dashIfEmpty(category), - sortable: true, - cardVisible: true, - }, - { - name: 'lack', - align: 'center', - label: t('negative.lack'), - field: ({ lack }) => lack, - columnFilter: { - component: 'input', - type: 'number', - columnClass: 'shrink', - }, - sortable: true, - headerStyle: 'padding-center: 33px', - cardVisible: true, - }, - { - name: 'tableActions', - align: 'center', - actions: [ - { - title: t('Open details'), - icon: 'edit', - action: redirectToCreateView, - isPrimary: true, - }, - ], - }, -]); - -const setUserParams = (params) => { - filterParams.value = params; -}; -</script> - -<template> - <RightMenu> - <template #right-panel> - <TicketLackFilter data-key="NegativeList" @set-user-params="setUserParams" /> - </template> - </RightMenu> - {{ filterRef }} - <VnTable - ref="tableRef" - data-key="NegativeList" - :url="`Tickets/itemLack`" - :order="['itemFk DESC, date DESC, timed DESC']" - :user-params="negativeParams" - auto-load - :columns="columns" - default-mode="table" - :right-search="false" - :is-editable="false" - :use-model="true" - :map-key="false" - :row-click="redirectToCreateView" - v-model:selected="selectedRows" - :create="false" - :crud-model="{ - disableInfiniteScroll: true, - }" - :table="{ - 'row-key': 'itemFk', - selection: 'multiple', - }" - > - <template #column-itemFk="{ row }"> - <div - style="display: flex; justify-content: space-around; align-items: center" - > - <span @click.stop>{{ row.itemFk }}</span> - </div> - </template> - <template #column-longName="{ row }"> - <span class="link" @click.stop> - {{ row.longName }} - <ItemDescriptorProxy :id="row.itemFk" /> - </span> - </template> - </VnTable> -</template> - -<style lang="scss" scoped> -.list { - max-height: 100%; - padding: 15px; - width: 100%; -} - -.grid-style-transition { - transition: - transform 0.28s, - background-color 0.28s; -} - -#true { - background-color: $positive; -} - -#false { - background-color: $negative; -} - -div.q-dialog__inner > div { - max-width: fit-content !important; -} -.q-btn-group > .q-btn-item:not(:first-child) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} -</style> diff --git a/src/pages/Ticket/Negative/TicketLackTable.vue b/src/pages/Ticket/Negative/TicketLackTable.vue deleted file mode 100644 index 176e8f7ad..000000000 --- a/src/pages/Ticket/Negative/TicketLackTable.vue +++ /dev/null @@ -1,356 +0,0 @@ -<script setup> -import FetchedTags from 'components/ui/FetchedTags.vue'; -import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; -import { computed, ref, watch } from 'vue'; -import { useI18n } from 'vue-i18n'; -import axios from 'axios'; -import FetchData from 'src/components/FetchData.vue'; -import { toDate, toHour } from 'src/filters'; -import useNotify from 'src/composables/useNotify.js'; -import ZoneDescriptorProxy from 'pages/Zone/Card/ZoneDescriptorProxy.vue'; -import { useRoute } from 'vue-router'; -import VnTable from 'src/components/VnTable/VnTable.vue'; -import TicketDescriptorProxy from '../Card/TicketDescriptorProxy.vue'; -import VnInputNumber from 'src/components/common/VnInputNumber.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; - -const $props = defineProps({ - filter: { - type: Object, - default: () => ({}), - }, -}); - -watch( - () => $props.filter, - (v) => { - filterLack.value.where = v; - tableRef.value.reload(filterLack); - }, -); - -const filterLack = ref({ - include: [ - { - relation: 'workers', - scope: { - fields: ['id', 'firstName'], - }, - }, - ], - where: { ...$props.filter }, - order: 'ts.alertLevelCode ASC', -}); - -const selectedRows = ref([]); -const { t } = useI18n(); -const { notify } = useNotify(); -const entityId = computed(() => route.params.id); -const item = ref({}); -const route = useRoute(); -const columns = computed(() => [ - { - name: 'status', - align: 'center', - sortable: false, - columnClass: 'shrink', - columnFilter: false, - }, - { - name: 'ticketFk', - label: t('negative.detail.ticketFk'), - align: 'center', - sortable: true, - columnFilter: { - component: 'input', - type: 'number', - }, - }, - { - name: 'shipped', - label: t('negative.detail.shipped'), - field: 'shipped', - align: 'center', - format: ({ shipped }) => toDate(shipped), - sortable: true, - columnFilter: { - component: 'date', - columnClass: 'shrink', - }, - }, - { - name: 'minTimed', - label: t('negative.detail.theoreticalhour'), - field: 'minTimed', - align: 'center', - sortable: true, - component: 'time', - columnFilter: {}, - }, - { - name: 'alertLevelCode', - label: t('negative.detail.state'), - columnFilter: { - name: 'alertLevelCode', - component: 'select', - attrs: { - url: 'AlertLevels', - fields: ['name', 'code'], - optionLabel: 'code', - optionValue: 'code', - }, - }, - align: 'center', - sortable: true, - }, - { - name: 'zoneName', - label: t('negative.detail.zoneName'), - field: 'zoneName', - align: 'center', - sortable: true, - }, - { - name: 'nickname', - label: t('negative.detail.nickname'), - field: 'nickname', - align: 'center', - sortable: true, - }, - { - name: 'quantity', - label: t('negative.detail.quantity'), - field: 'quantity', - sortable: true, - component: 'input', - type: 'number', - }, -]); - -const emit = defineEmits(['update:selection']); -const itemLack = ref(null); -const fetchItemLack = ref(null); -const tableRef = ref(null); -defineExpose({ tableRef, itemLack }); -watch(selectedRows, () => emit('update:selection', selectedRows)); -const getInputEvents = ({ col, ...rows }) => ({ - 'update:modelValue': () => saveChange(col.name, rows), - 'keyup.enter': () => saveChange(col.name, rows), -}); -const saveChange = async (field, { row }) => { - try { - switch (field) { - case 'alertLevelCode': - await axios.post(`Tickets/state`, { - ticketFk: row.ticketFk, - code: row[field], - }); - break; - - case 'quantity': - await axios.post(`Sales/${row.saleFk}/updateQuantity`, { - quantity: +row.quantity, - }); - break; - } - notify('globals.dataSaved', 'positive'); - fetchItemLack.value.fetch(); - } catch (err) { - console.error('Error saving changes', err); - f; - } -}; - -function onBuysFetched(data) { - Object.assign(item.value, data[0]); -} -</script> - -<template> - <FetchData - ref="fetchItemLack" - :url="`Tickets/itemLack`" - :params="{ id: entityId }" - @on-fetch="(data) => (itemLack = data[0])" - auto-load - /> - <FetchData - :url="`Items/${entityId}/getCard`" - :fields="['longName']" - @on-fetch="(data) => (item = data)" - auto-load - /> - <FetchData - :url="`Buys/latestBuysFilter`" - :fields="['longName']" - :filter="{ where: { 'i.id': entityId } }" - @on-fetch="onBuysFetched" - auto-load - /> - <VnTable - ref="tableRef" - data-key="NegativeItem" - :map-key="false" - :url="`Tickets/itemLack/${entityId}`" - :columns="columns" - auto-load - :create="false" - :create-as-dialog="false" - :use-model="true" - :filter="filterLack" - :order="['ts.alertLevelCode ASC']" - :table="{ - 'row-key': 'id', - selection: 'multiple', - }" - dense - :is-editable="true" - :row-click="false" - :right-search="false" - :right-search-icon="false" - v-model:selected="selectedRows" - :disable-option="{ card: true }" - > - <template #top-left> - <div style="display: flex; align-items: center" v-if="itemLack"> - <!-- <VnImg :id="itemLack.itemFk" class="rounded image-wrapper"></VnImg> --> - <div class="flex column" style="align-items: center"> - <QBadge - ref="badgeLackRef" - class="q-ml-xs" - text-color="white" - :color="itemLack.lack === 0 ? 'positive' : 'negative'" - :label="itemLack.lack" - /> - </div> - <div class="flex column left" style="align-items: flex-start"> - <QBtn flat class="link text-blue"> - {{ item?.longName ?? item.name }} - <ItemDescriptorProxy :id="entityId" /> - <FetchedTags class="q-ml-md" :item="item" :columns="7" /> - </QBtn> - </div> - </div> - </template> - <template #top-right> - <slot name="top-right" /> - </template> - - <template #column-status="{ row }"> - <QTd style="min-width: 150px"> - <div class="icon-container"> - <QIcon - v-if="row.isBasket" - name="vn:basket" - color="primary" - class="cursor-pointer" - size="xs" - > - <QTooltip>{{ t('negative.detail.isBasket') }}</QTooltip> - </QIcon> - <QIcon - v-if="row.hasToIgnore" - name="star" - color="primary" - class="cursor-pointer fill-icon" - size="xs" - > - <QTooltip>{{ t('negative.detail.hasToIgnore') }}</QTooltip> - </QIcon> - <QIcon - v-if="row.hasObservation" - name="change_circle" - color="primary" - class="cursor-pointer" - size="xs" - > - <QTooltip>{{ - t('negative.detail.hasObservation') - }}</QTooltip> </QIcon - ><QIcon - v-if="row.isRookie" - name="vn:Person" - size="xs" - color="primary" - class="cursor-pointer" - > - <QTooltip>{{ t('negative.detail.isRookie') }}</QTooltip> - </QIcon> - <QIcon - v-if="row.peticionCompra" - name="vn:buyrequest" - size="xs" - color="primary" - class="cursor-pointer" - > - <QTooltip>{{ t('negative.detail.peticionCompra') }}</QTooltip> - </QIcon> - <QIcon - v-if="row.turno" - name="vn:calendar" - size="xs" - color="primary" - class="cursor-pointer" - > - <QTooltip>{{ t('negative.detail.turno') }}</QTooltip> - </QIcon> - </div></QTd - > - </template> - <template #column-nickname="{ row }"> - <span class="link" @click.stop> - {{ row.nickname }} - <CustomerDescriptorProxy :id="row.customerId" /> - </span> - </template> - <template #column-ticketFk="{ row }"> - <span class="q-pa-sm link"> - {{ row.id }} - <TicketDescriptorProxy :id="row.id" /> - </span> - </template> - <template #column-alertLevelCode="props"> - <VnSelect - url="States/editableStates" - auto-load - hide-selected - option-value="id" - option-label="name" - v-model="props.row.alertLevelCode" - v-on="getInputEvents(props)" - /> - </template> - - <template #column-zoneName="{ row }"> - <span class="link">{{ row.zoneName }}</span> - <ZoneDescriptorProxy :id="row.zoneFk" /> - </template> - <template #column-quantity="props"> - <VnInputNumber - v-model.number="props.row.quantity" - v-on="getInputEvents(props)" - ></VnInputNumber> - </template> - </VnTable> -</template> -<style lang="scss" scoped> -.icon-container { - display: grid; - grid-template-columns: repeat(3, 0.2fr); - row-gap: 5px; /* Ajusta el espacio entre los iconos según sea necesario */ -} -.icon-container > * { - width: 100%; - height: auto; -} -.list-enter-active, -.list-leave-active { - transition: all 1s ease; -} -.list-enter-from, -.list-leave-to { - opacity: 0; - background-color: $primary; -} -</style> diff --git a/src/pages/Ticket/Negative/components/ChangeItemDialog.vue b/src/pages/Ticket/Negative/components/ChangeItemDialog.vue deleted file mode 100644 index e419b85c0..000000000 --- a/src/pages/Ticket/Negative/components/ChangeItemDialog.vue +++ /dev/null @@ -1,90 +0,0 @@ -<script setup> -import { ref } from 'vue'; -import axios from 'axios'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import notifyResults from 'src/utils/notifyResults'; -const emit = defineEmits(['update-item']); - -const showChangeItemDialog = ref(false); -const newItem = ref(null); -const $props = defineProps({ - selectedRows: { - type: Array, - default: () => [], - }, -}); - -const updateItem = async () => { - try { - showChangeItemDialog.value = true; - const rowsToUpdate = $props.selectedRows.map(({ saleFk, quantity }) => - axios.post(`Sales/replaceItem`, { - saleFk, - substitutionFk: newItem.value, - quantity, - }), - ); - const result = await Promise.allSettled(rowsToUpdate); - notifyResults(result, 'saleFk'); - emit('update-item', newItem.value); - } catch (err) { - console.error('Error updating item:', err); - return err; - } -}; -</script> - -<template> - <QCard class="q-pa-sm"> - <QCardSection class="row items-center justify-center column items-stretch"> - {{ showChangeItemDialog }} - <span>{{ $t('negative.detail.modal.changeItem.title') }}</span> - <VnSelect - url="Items/WithName" - :fields="['id', 'name']" - :sort-by="['id DESC']" - :options="items" - option-label="name" - option-value="id" - v-model="newItem" - > - </VnSelect> - </QCardSection> - <QCardActions align="right"> - <QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup /> - <QBtn - :label="$t('globals.confirm')" - color="primary" - :disable="!newItem" - @click="updateItem" - unelevated - autofocus - /> </QCardActions - ></QCard> -</template> - -<style lang="scss" scoped> -.list { - max-height: 100%; - padding: 15px; - width: 100%; -} - -.grid-style-transition { - transition: - transform 0.28s, - background-color 0.28s; -} - -#true { - background-color: $positive; -} - -#false { - background-color: $negative; -} - -div.q-dialog__inner > div { - max-width: fit-content !important; -} -</style> diff --git a/src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue b/src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue deleted file mode 100644 index 2e9aac4f0..000000000 --- a/src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue +++ /dev/null @@ -1,84 +0,0 @@ -<script setup> -import { ref } from 'vue'; -import axios from 'axios'; -import VnInput from 'src/components/common/VnInput.vue'; -import notifyResults from 'src/utils/notifyResults'; - -const showChangeQuantityDialog = ref(false); -const newQuantity = ref(null); -const $props = defineProps({ - selectedRows: { - type: Array, - default: () => [], - }, -}); -const emit = defineEmits(['update-quantity']); -const updateQuantity = async () => { - try { - showChangeQuantityDialog.value = true; - const rowsToUpdate = $props.selectedRows.map(({ saleFk }) => - axios.post(`Sales/${saleFk}/updateQuantity`, { - saleFk, - quantity: +newQuantity.value, - }), - ); - - const result = await Promise.allSettled(rowsToUpdate); - notifyResults(result, 'saleFk'); - - emit('update-quantity', newQuantity.value); - } catch (err) { - return err; - } -}; -</script> - -<template> - <QCard class="q-pa-sm"> - <QCardSection class="row items-center justify-center column items-stretch"> - <span>{{ $t('negative.detail.modal.changeQuantity.title') }}</span> - <VnInput - type="number" - :min="0" - :label="$t('negative.detail.modal.changeQuantity.placeholder')" - v-model="newQuantity" - /> - </QCardSection> - <QCardActions align="right"> - <QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup /> - <QBtn - :label="$t('globals.confirm')" - color="primary" - :disable="!newQuantity || newQuantity < 0" - @click="updateQuantity" - unelevated - autofocus - /> </QCardActions - ></QCard> -</template> - -<style lang="scss" scoped> -.list { - max-height: 100%; - padding: 15px; - width: 100%; -} - -.grid-style-transition { - transition: - transform 0.28s, - background-color 0.28s; -} - -#true { - background-color: $positive; -} - -#false { - background-color: $negative; -} - -div.q-dialog__inner > div { - max-width: fit-content !important; -} -</style> diff --git a/src/pages/Ticket/Negative/components/ChangeStateDialog.vue b/src/pages/Ticket/Negative/components/ChangeStateDialog.vue deleted file mode 100644 index 1acc7e0ef..000000000 --- a/src/pages/Ticket/Negative/components/ChangeStateDialog.vue +++ /dev/null @@ -1,91 +0,0 @@ -<script setup> -import { ref } from 'vue'; -import axios from 'axios'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import FetchData from 'components/FetchData.vue'; -import notifyResults from 'src/utils/notifyResults'; - -const emit = defineEmits(['update-state']); -const editableStates = ref([]); -const showChangeStateDialog = ref(false); -const newState = ref(null); -const $props = defineProps({ - selectedRows: { - type: Array, - default: () => [], - }, -}); -const updateState = async () => { - try { - showChangeStateDialog.value = true; - const rowsToUpdate = $props.selectedRows.map(({ id }) => - axios.post(`Tickets/state`, { - ticketFk: id, - code: newState.value, - }), - ); - const result = await Promise.allSettled(rowsToUpdate); - notifyResults(result, 'ticketFk'); - - emit('update-state', newState.value); - } catch (err) { - return err; - } -}; -</script> - -<template> - <FetchData - url="States/editableStates" - @on-fetch="(data) => (editableStates = data)" - auto-load - /> - <QCard class="q-pa-sm"> - <QCardSection class="row items-center justify-center column items-stretch"> - <span>{{ $t('negative.detail.modal.changeState.title') }}</span> - <VnSelect - :label="$t('negative.detail.modal.changeState.placeholder')" - v-model="newState" - :options="editableStates" - option-label="name" - option-value="code" - /> - </QCardSection> - <QCardActions align="right"> - <QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup /> - <QBtn - :label="$t('globals.confirm')" - color="primary" - :disable="!newState" - @click="updateState" - unelevated - autofocus - /> </QCardActions - ></QCard> -</template> - -<style lang="scss" scoped> -.list { - max-height: 100%; - padding: 15px; - width: 100%; -} - -.grid-style-transition { - transition: - transform 0.28s, - background-color 0.28s; -} - -#true { - background-color: $positive; -} - -#false { - background-color: $negative; -} - -div.q-dialog__inner > div { - max-width: fit-content !important; -} -</style> diff --git a/src/pages/Ticket/TicketFuture.vue b/src/pages/Ticket/TicketFuture.vue index 92911cd25..0d216bed4 100644 --- a/src/pages/Ticket/TicketFuture.vue +++ b/src/pages/Ticket/TicketFuture.vue @@ -1,22 +1,24 @@ <script setup> -import { ref, computed, reactive, watch } from 'vue'; +import { onMounted, ref, computed, reactive } from 'vue'; import { useI18n } from 'vue-i18n'; +import FetchData from 'components/FetchData.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; -import VnTable from 'src/components/VnTable/VnTable.vue'; import TicketFutureFilter from './TicketFutureFilter.vue'; import { dashIfEmpty, toCurrency } from 'src/filters'; import { useVnConfirm } from 'composables/useVnConfirm'; +import { useArrayData } from 'composables/useArrayData'; import { getDateQBadgeColor } from 'src/composables/getDateQBadgeColor.js'; import useNotify from 'src/composables/useNotify.js'; import { useState } from 'src/composables/useState'; import { toDateTimeFormat } from 'src/filters/date.js'; import axios from 'axios'; -import TicketProblems from 'src/components/TicketProblems.vue'; const state = useState(); const { t } = useI18n(); @@ -24,126 +26,214 @@ const { openConfirmationModal } = useVnConfirm(); const { notify } = useNotify(); const user = state.getUser(); +const itemPackingTypesOptions = ref([]); const selectedTickets = ref([]); -const vnTableRef = ref({}); -const originElRef = ref(null); -const destinationElRef = ref(null); + +const exprBuilder = (param, value) => { + switch (param) { + case 'id': + return { id: value }; + case 'futureId': + return { futureId: value }; + case 'liters': + return { liters: value }; + case 'lines': + return { lines: value }; + case 'iptColFilter': + return { ipt: { like: `%${value}%` } }; + case 'futureIptColFilter': + return { futureIpt: { like: `%${value}%` } }; + case 'totalWithVat': + return { totalWithVat: value }; + } +}; + const userParams = reactive({ futureScopeDays: Date.vnNew().toISOString(), originScopeDays: Date.vnNew().toISOString(), warehouseFk: user.value.warehouseFk, }); +const arrayData = useArrayData('FutureTickets', { + url: 'Tickets/getTicketsFuture', + userParams: userParams, + exprBuilder: exprBuilder, +}); +const { store } = arrayData; + +const params = reactive({ + futureScopeDays: Date.vnNew(), + originScopeDays: Date.vnNew(), + warehouseFk: user.value.warehouseFk, +}); + +const applyColumnFilter = async (col) => { + const paramKey = col.columnFilter?.filterParamKey || col.field; + params[paramKey] = col.columnFilter.filterValue; + await arrayData.addFilter({ params }); +}; + +const getInputEvents = (col) => { + return col.columnFilter.type === 'select' + ? { 'update:modelValue': () => applyColumnFilter(col) } + : { + 'keyup.enter': () => applyColumnFilter(col), + }; +}; + +const tickets = computed(() => store.data); + const ticketColumns = computed(() => [ { - label: '', + label: t('futureTickets.problems'), name: 'problems', - headerClass: 'horizontal-separator', align: 'left', - columnFilter: false, + columnFilter: null, }, { label: t('advanceTickets.ticketId'), - name: 'id', + name: 'ticketId', align: 'center', - headerClass: 'horizontal-separator', + sortable: true, + columnFilter: { + component: VnInput, + type: 'text', + filterValue: null, + filterParamKey: 'id', + event: getInputEvents, + attrs: { + dense: true, + }, + }, }, { label: t('futureTickets.shipped'), name: 'shipped', align: 'left', - columnFilter: false, - headerClass: 'horizontal-separator', + sortable: true, + columnFilter: null, }, { - align: 'center', - class: 'shrink', label: t('advanceTickets.ipt'), name: 'ipt', + field: 'ipt', + align: 'left', + sortable: true, columnFilter: { - component: 'select', + component: VnSelect, + filterParamKey: 'iptColFilter', + type: 'select', + filterValue: null, + event: getInputEvents, attrs: { - url: 'itemPackingTypes', - fields: ['code', 'description'], - where: { isActive: true }, - optionValue: 'code', - optionLabel: 'description', - inWhere: false, + options: itemPackingTypesOptions.value, + 'option-value': 'code', + 'option-label': 'description', + dense: true, }, }, - format: (row, dashIfEmpty) => dashIfEmpty(row.ipt), - headerClass: 'horizontal-separator', + format: (val) => dashIfEmpty(val), }, { label: t('ticketList.state'), name: 'state', align: 'left', - columnFilter: false, - headerClass: 'horizontal-separator', + sortable: true, + columnFilter: null, }, { label: t('advanceTickets.liters'), name: 'liters', + field: 'liters', align: 'left', - headerClass: 'horizontal-separator', + sortable: true, + columnFilter: { + component: VnInput, + type: 'text', + filterValue: null, + event: getInputEvents, + attrs: { + dense: true, + }, + }, }, { label: t('advanceTickets.import'), + field: 'import', name: 'import', align: 'left', - headerClass: 'horizontal-separator', - columnFilter: false, - format: (row) => toCurrency(row.totalWithVat), + sortable: true, }, { label: t('futureTickets.availableLines'), name: 'lines', field: 'lines', align: 'center', - headerClass: 'horizontal-separator', - format: (row, dashIfEmpty) => dashIfEmpty(row.lines), + sortable: true, + columnFilter: { + component: VnInput, + type: 'text', + filterValue: null, + event: getInputEvents, + attrs: { + dense: true, + }, + }, + format: (val) => dashIfEmpty(val), }, { label: t('advanceTickets.futureId'), name: 'futureId', - align: 'center', - headerClass: 'horizontal-separator vertical-separator ', - columnClass: 'vertical-separator', + align: 'left', + sortable: true, + columnFilter: { + component: VnInput, + type: 'text', + filterValue: null, + filterParamKey: 'futureId', + event: getInputEvents, + attrs: { + dense: true, + }, + }, }, { label: t('futureTickets.futureShipped'), name: 'futureShipped', align: 'left', - headerClass: 'horizontal-separator', - columnFilter: false, - format: (row) => toDateTimeFormat(row.futureShipped), + sortable: true, + columnFilter: null, + format: (val) => dashIfEmpty(val), }, + { - align: 'center', label: t('advanceTickets.futureIpt'), - class: 'shrink', name: 'futureIpt', + field: 'futureIpt', + align: 'left', + sortable: true, columnFilter: { - component: 'select', + component: VnSelect, + filterParamKey: 'futureIptColFilter', + type: 'select', + filterValue: null, + event: getInputEvents, attrs: { - url: 'itemPackingTypes', - fields: ['code', 'description'], - where: { isActive: true }, - optionValue: 'code', - optionLabel: 'description', + options: itemPackingTypesOptions.value, + 'option-value': 'code', + 'option-label': 'description', + dense: true, }, }, - headerClass: 'horizontal-separator', - format: (row, dashIfEmpty) => dashIfEmpty(row.futureIpt), + format: (val) => dashIfEmpty(val), }, { label: t('advanceTickets.futureState'), name: 'futureState', align: 'right', - headerClass: 'horizontal-separator', - class: 'expand', - columnFilter: false, - format: (row, dashIfEmpty) => dashIfEmpty(row.futureState), + sortable: true, + columnFilter: null, + format: (val) => dashIfEmpty(val), }, ]); @@ -168,51 +258,26 @@ const moveTicketsFuture = async () => { await axios.post('Tickets/merge', params); notify(t('advanceTickets.moveTicketSuccess'), 'positive'); selectedTickets.value = []; - vnTableRef.value.reload(); + arrayData.fetch({ append: false }); }; - -watch( - () => vnTableRef.value.tableRef?.$el, - ($el) => { - if (!$el) return; - const head = $el.querySelector('thead'); - const firstRow = $el.querySelector('thead > tr'); - - const newRow = document.createElement('tr'); - destinationElRef.value = document.createElement('th'); - originElRef.value = document.createElement('th'); - - newRow.classList.add('bg-header'); - destinationElRef.value.classList.add('text-uppercase', 'color-vn-label'); - originElRef.value.classList.add('text-uppercase', 'color-vn-label'); - - destinationElRef.value.setAttribute('colspan', '7'); - originElRef.value.setAttribute('colspan', '9'); - - originElRef.value.textContent = `${t('advanceTickets.origin')}`; - destinationElRef.value.textContent = `${t('advanceTickets.destination')}`; - - newRow.append(destinationElRef.value, originElRef.value); - head.insertBefore(newRow, firstRow); - }, - { once: true, inmmediate: true }, -); - -watch( - () => vnTableRef.value.params, - () => { - if (originElRef.value && destinationElRef.value) { - destinationElRef.value.textContent = `${t('advanceTickets.origin')}`; - originElRef.value.textContent = `${t('advanceTickets.destination')}`; - } - }, - { deep: true }, -); +onMounted(async () => { + await arrayData.fetch({ append: false }); +}); </script> <template> + <FetchData + url="itemPackingTypes" + :filter="{ + fields: ['code', 'description'], + order: 'description ASC', + where: { isActive: true }, + }" + auto-load + @on-fetch="(data) => (itemPackingTypesOptions = data)" + /> <VnSearchbar - data-key="futureTicket" + data-key="FutureTickets" :label="t('Search ticket')" :info="t('futureTickets.searchInfo')" /> @@ -228,7 +293,7 @@ watch( t(`futureTickets.moveTicketDialogSubtitle`, { selectedTickets: selectedTickets.length, }), - moveTicketsFuture, + moveTicketsFuture ) " > @@ -240,135 +305,235 @@ watch( </VnSubToolbar> <RightMenu> <template #right-panel> - <TicketFutureFilter data-key="futureTickets" /> + <TicketFutureFilter data-key="FutureTickets" /> </template> </RightMenu> <QPage class="column items-center q-pa-md"> - <VnTable - data-key="futureTickets" - ref="vnTableRef" - url="Tickets/getTicketsFuture" - search-url="futureTickets" - :user-params="userParams" - :limit="0" + <QTable + :rows="tickets" :columns="ticketColumns" - :table="{ - 'row-key': '$index', - selection: 'multiple', - }" + row-key="id" + selection="multiple" v-model:selected="selectedTickets" - :right-search="false" - auto-load - :disable-option="{ card: true }" + :pagination="{ rowsPerPage: 0 }" + :no-data-label="t('globals.noResults')" + style="max-width: 99%" > - <template #column-problems="{ row }"> - <span class="q-gutter-x-xs"> - <QIcon - v-if="row.futureAgencyFk !== row.agencyFk && row.agencyFk" - color="primary" - name="vn:agency-term" - size="xs" - class="q-mr-xs" + <template #header="props"> + <QTr> + <QTh class="horizontal-separator" /> + <QTh + class="horizontal-separator text-uppercase color-vn-label" + colspan="8" + translate > - <QTooltip class="column"> - <span> - {{ - t('advanceTickets.originAgency', { - agency: row.futureAgency, - }) - }} - </span> - <span> - {{ - t('advanceTickets.destinationAgency', { - agency: row.agency, - }) - }} - </span> + {{ t('advanceTickets.origin') }} + </QTh> + <QTh + class="horizontal-separator text-uppercase color-vn-label" + colspan="4" + translate + > + {{ t('advanceTickets.destination') }} + </QTh> + </QTr> + <QTr> + <QTh> + <QCheckbox v-model="props.selected" /> + </QTh> + <QTh + v-for="(col, index) in ticketColumns" + :key="index" + :class="{ 'vertical-separator': col.name === 'futureId' }" + > + {{ col.label }} + </QTh> + </QTr> + </template> + <template #top-row="{ cols }"> + <QTr> + <QTd /> + <QTd + v-for="(col, index) in cols" + :key="index" + style="max-width: 100px" + > + <component + :is="col.columnFilter.component" + v-if="col.columnFilter" + v-model="col.columnFilter.filterValue" + v-bind="col.columnFilter.attrs" + v-on="col.columnFilter.event(col)" + dense + /> + </QTd> + </QTr> + </template> + <template #header-cell-availableLines="{ col }"> + <QTh class="vertical-separator"> + {{ col.label }} + </QTh> + </template> + <template #body-cell-problems="{ row }"> + <QTd class="q-gutter-x-xs"> + <QIcon + v-if="row.isTaxDataChecked === 0" + color="primary" + name="vn:no036" + size="xs" + > + <QTooltip> + {{ t('futureTickets.noVerified') }} </QTooltip> </QIcon> - <TicketProblems :row /> - </span> + <QIcon + v-if="row.hasTicketRequest" + color="primary" + name="vn:buyrequest" + size="xs" + > + <QTooltip> + {{ t('futureTickets.purchaseRequest') }} + </QTooltip> + </QIcon> + <QIcon + v-if="row.itemShortage" + color="primary" + name="vn:unavailable" + size="xs" + > + <QTooltip> + {{ t('ticketSale.noVisible') }} + </QTooltip> + </QIcon> + <QIcon + v-if="row.isFreezed" + color="primary" + name="vn:frozen" + size="xs" + > + <QTooltip> + {{ t('futureTickets.clientFrozen') }} + </QTooltip> + </QIcon> + <QIcon v-if="row.risk" color="primary" name="vn:risk" size="xs"> + <QTooltip> + {{ t('futureTickets.risk') }}: {{ row.risk }} + </QTooltip> + </QIcon> + <QIcon + v-if="row.hasComponentLack" + color="primary" + name="vn:components" + size="xs" + > + <QTooltip> + {{ t('futureTickets.componentLack') }} + </QTooltip> + </QIcon> + <QIcon + v-if="row.hasRounding" + color="primary" + name="sync_problem" + size="xs" + > + <QTooltip> + {{ t('futureTickets.rounding') }} + </QTooltip> + </QIcon> + </QTd> </template> - <template #column-id="{ row }"> - <QBtn flat class="link" @click.stop dense> - {{ row.id }} - <TicketDescriptorProxy :id="row.id" /> - </QBtn> + <template #body-cell-ticketId="{ row }"> + <QTd> + <QBtn flat class="link"> + {{ row.id }} + <TicketDescriptorProxy :id="row.id" /> + </QBtn> + </QTd> </template> - <template #column-shipped="{ row }"> - <QBadge - text-color="black" - :color="getDateQBadgeColor(row.shipped)" - class="q-ma-none" - > - {{ toDateTimeFormat(row.shipped) }} - </QBadge> + <template #body-cell-shipped="{ row }"> + <QTd class="shipped"> + <QBadge + text-color="black" + :color="getDateQBadgeColor(row.shipped)" + class="q-ma-none" + > + {{ toDateTimeFormat(row.shipped) }} + </QBadge> + </QTd> </template> - <template #column-state="{ row }"> - <QBadge - v-if="row.state" - text-color="black" - :color="row.classColor" - class="q-ma-none" - dense - > - {{ row.state }} - </QBadge> - <span v-else> {{ dashIfEmpty(row.state) }}</span> + <template #body-cell-state="{ row }"> + <QTd> + <QBadge + text-color="black" + :color="row.classColor" + class="q-ma-none" + dense + > + {{ row.state }} + </QBadge> + </QTd> </template> - <template #column-import="{ row }"> - <QBadge - :text-color=" - totalPriceColor(row.totalWithVat) === 'warning' - ? 'black' - : 'white' - " - :color="totalPriceColor(row.totalWithVat)" - class="q-ma-none" - dense - > - {{ toCurrency(row.totalWithVat || 0) }} - </QBadge> + <template #body-cell-import="{ row }"> + <QTd> + <QBadge + :text-color=" + totalPriceColor(row.totalWithVat) === 'warning' + ? 'black' + : 'white' + " + :color="totalPriceColor(row.totalWithVat)" + class="q-ma-none" + dense + > + {{ toCurrency(row.totalWithVat || 0) }} + </QBadge> + </QTd> </template> - <template #column-futureId="{ row }"> - <QBtn flat class="link" @click.stop dense> - {{ row.futureId }} - <TicketDescriptorProxy :id="row.futureId" /> - </QBtn> + <template #body-cell-futureId="{ row }"> + <QTd class="vertical-separator"> + <QBtn flat class="link" dense> + {{ row.futureId }} + <TicketDescriptorProxy :id="row.futureId" /> + </QBtn> + </QTd> </template> - <template #column-futureShipped="{ row }"> - <QBadge - text-color="black" - :color="getDateQBadgeColor(row.futureShipped)" - class="q-ma-none" - > - {{ toDateTimeFormat(row.futureShipped) }} - </QBadge> + <template #body-cell-futureShipped="{ row }"> + <QTd class="shipped"> + <QBadge + text-color="black" + :color="getDateQBadgeColor(row.futureShipped)" + class="q-ma-none" + > + {{ toDateTimeFormat(row.futureShipped) }} + </QBadge> + </QTd> </template> - <template #column-futureState="{ row }"> - <QBadge - text-color="black" - :color="row.futureClassColor" - class="q-mr-xs" - dense - > - {{ row.futureState }} - </QBadge> + <template #body-cell-futureState="{ row }"> + <QTd> + <QBadge + text-color="black" + :color="row.futureClassColor" + class="q-ma-none" + dense + > + {{ row.futureState }} + </QBadge> + </QTd> </template> - </VnTable> + </QTable> </QPage> </template> <style scoped lang="scss"> -:deep(.vertical-separator) { +.shipped { + min-width: 132px; +} +.vertical-separator { border-left: 4px solid white !important; } -:deep(.horizontal-separator) { - border-top: 4px solid white !important; -} -:deep(.horizontal-bottom-separator) { +.horizontal-separator { border-bottom: 4px solid white !important; } </style> diff --git a/src/pages/Ticket/TicketFutureFilter.vue b/src/pages/Ticket/TicketFutureFilter.vue index 64e060a39..d28b0af71 100644 --- a/src/pages/Ticket/TicketFutureFilter.vue +++ b/src/pages/Ticket/TicketFutureFilter.vue @@ -12,7 +12,7 @@ import axios from 'axios'; import { onMounted } from 'vue'; const { t } = useI18n(); -defineProps({ +const props = defineProps({ dataKey: { type: String, required: true, @@ -58,7 +58,7 @@ onMounted(async () => { auto-load /> <VnFilterPanel - :data-key + :data-key="props.dataKey" :un-removable-params="['warehouseFk', 'originScopeDays ', 'futureScopeDays']" > <template #tags="{ tag, formatFn }"> diff --git a/src/pages/Ticket/locale/en.yml b/src/pages/Ticket/locale/en.yml index cdbb22d9b..f11b32c3a 100644 --- a/src/pages/Ticket/locale/en.yml +++ b/src/pages/Ticket/locale/en.yml @@ -23,8 +23,6 @@ ticketSale: hasComponentLack: Component lack ok: Ok more: More - transferLines: Transfer lines(no basket)/ Split - transferBasket: Some row selected is basket advanceTickets: preparation: Preparation origin: Origin @@ -190,6 +188,7 @@ ticketList: accountPayment: Account payment sendDocuware: Set delivered and send delivery note(s) to the tablet addPayment: Add payment + date: Date company: Company amount: Amount reference: Reference @@ -203,89 +202,9 @@ ticketList: creditCard: Credit card transfers: Transfers province: Province + warehouse: Warehouse + hour: Hour closure: Closure toLines: Go to lines addressNickname: Address nickname ref: Reference - rounding: Rounding - noVerifiedData: No verified data - purchaseRequest: Purchase request - notVisible: Not visible - clientFrozen: Client frozen - componentLack: Component lack -negative: - hour: Hour - id: Id Article - longName: Article - supplier: Supplier - colour: Colour - size: Size - origen: Origin - value: Negative - itemFk: Article - producer: Producer - warehouse: Warehouse - warehouseFk: Warehouse - category: Category - categoryFk: Family - type: Type - typeFk: Type - lack: Negative - inkFk: inkFk - timed: timed - date: Date - minTimed: minTimed - negativeAction: Negative - totalNegative: Total negatives - days: Days - buttonsUpdate: - item: Item - state: State - quantity: Quantity - modalOrigin: - title: Update negatives - question: Select a state to update - modalSplit: - title: Confirm split selected - question: Select a state to update - detail: - saleFk: Sale - itemFk: Article - ticketFk: Ticket - code: Code - nickname: Alias - name: Name - zoneName: Agency name - shipped: Date - theoreticalhour: Theoretical hour - agName: Agency - quantity: Quantity - alertLevelCode: Group state - state: State - peticionCompra: Ticket request - isRookie: Is rookie - turno: Turn line - isBasket: Basket - hasObservation: Has substitution - hasToIgnore: VIP - modal: - changeItem: - title: Update item reference - placeholder: New item - changeState: - title: Update tickets state - placeholder: New state - changeQuantity: - title: Update tickets quantity - placeholder: New quantity - split: - title: Are you sure you want to split selected tickets? - subTitle: Confirm split action - handleSplited: - title: Handle splited tickets - subTitle: Confirm date and agency - split: - ticket: Old ticket - newTicket: New ticket - status: Result - message: Message diff --git a/src/pages/Ticket/locale/es.yml b/src/pages/Ticket/locale/es.yml index 75d3c6a2b..945da8367 100644 --- a/src/pages/Ticket/locale/es.yml +++ b/src/pages/Ticket/locale/es.yml @@ -127,8 +127,6 @@ ticketSale: ok: Ok more: Más address: Consignatario - transferLines: Transferir líneas(no cesta)/ Separar - transferBasket: No disponible para una cesta size: Medida ticketComponents: serie: Serie @@ -215,84 +213,3 @@ ticketList: toLines: Ir a lineas addressNickname: Alias consignatario ref: Referencia -negative: - hour: Hora - id: Id Articulo - longName: Articulo - supplier: Productor - colour: Color - size: Medida - origen: Origen - value: Negativo - warehouseFk: Almacen - producer: Producer - category: Categoría - categoryFk: Familia - typeFk: Familia - warehouse: Almacen - lack: Negativo - inkFk: Color - timed: Hora - date: Fecha - minTimed: Hora - type: Tipo - negativeAction: Negativo - totalNegative: Total negativos - days: Rango de dias - buttonsUpdate: - item: artículo - state: Estado - quantity: Cantidad - modalOrigin: - title: Actualizar negativos - question: Seleccione un estado para guardar - modalSplit: - title: Confirmar acción de split - question: Selecciona un estado - detail: - saleFk: Línea - itemFk: Artículo - ticketFk: Ticket - code: code - nickname: Alias - name: Nombre - zoneName: Agencia - shipped: F. envío - theoreticalhour: Hora teórica - agName: Agencia - quantity: Cantidad - alertLevelCode: Estado agrupado - state: Estado - peticionCompra: Petición compra - isRookie: Cliente nuevo - turno: Linea turno - isBasket: Cesta - hasObservation: Tiene sustitución - hasToIgnore: VIP - modal: - changeItem: - title: Actualizar referencia artículo - placeholder: Nuevo articulo - changeState: - title: Actualizar estado - placeholder: Nuevo estado - changeQuantity: - title: Actualizar cantidad - placeholder: Nueva cantidad - split: - title: ¿Seguro de separar los tickets seleccionados? - subTitle: Confirma separar tickets seleccionados - handleSplited: - title: Gestionar tickets spliteados - subTitle: Confir fecha y agencia - split: - ticket: Ticket viejo - newTicket: Ticket nuevo - status: Estado - message: Mensaje - rounding: Redondeo - noVerifiedData: Sin datos comprobados - purchaseRequest: Petición de compra - notVisible: No visible - clientFrozen: Cliente congelado - componentLack: Faltan componentes diff --git a/src/pages/Travel/Card/TravelBasicData.vue b/src/pages/Travel/Card/TravelBasicData.vue index b1adc8126..4b9aa28ed 100644 --- a/src/pages/Travel/Card/TravelBasicData.vue +++ b/src/pages/Travel/Card/TravelBasicData.vue @@ -9,7 +9,6 @@ import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; -import VnInputTime from 'components/common/VnInputTime.vue'; const route = useRoute(); const { t } = useI18n(); @@ -54,16 +53,7 @@ const warehousesOptionsIn = ref([]); <VnInputDate v-model="data.shipped" :label="t('globals.shipped')" /> <VnInputDate v-model="data.landed" :label="t('globals.landed')" /> </VnRow> - <VnRow> - <VnInputDate - v-model="data.availabled" - :label="t('travel.summary.availabled')" - /> - <VnInputTime - v-model="data.availabled" - :label="t('travel.summary.availabledHour')" - /> - </VnRow> + <VnRow> <VnSelect :label="t('globals.warehouseOut')" @@ -111,3 +101,10 @@ const warehousesOptionsIn = ref([]); </template> </FormModel> </template> + +<i18n> +es: + raidDays: El travel se desplaza automáticamente cada día para estar desde hoy al número de días indicado. Si se deja vacio no se moverá +en: + raidDays: The travel adjusts itself daily to match the number of days set, starting from today. If left blank, it won’t move +</i18n> diff --git a/src/pages/Travel/Card/TravelCard.vue b/src/pages/Travel/Card/TravelCard.vue index cb09eafd6..445675b90 100644 --- a/src/pages/Travel/Card/TravelCard.vue +++ b/src/pages/Travel/Card/TravelCard.vue @@ -1,13 +1,43 @@ <script setup> import TravelDescriptor from './TravelDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; -import filter from './TravelFilter.js'; + +const userFilter = { + fields: [ + 'id', + 'ref', + 'shipped', + 'landed', + 'totalEntries', + 'warehouseInFk', + 'warehouseOutFk', + 'cargoSupplierFk', + 'agencyModeFk', + 'isRaid', + 'isDelivered', + 'isReceived', + ], + include: [ + { + relation: 'warehouseIn', + scope: { + fields: ['name'], + }, + }, + { + relation: 'warehouseOut', + scope: { + fields: ['name'], + }, + }, + ], +}; </script> <template> <VnCardBeta data-key="Travel" - url="Travels" + base-url="Travels" :descriptor="TravelDescriptor" - :filter="filter" + :user-filter="userFilter" /> </template> diff --git a/src/pages/Travel/Card/TravelDescriptor.vue b/src/pages/Travel/Card/TravelDescriptor.vue index 922f89f33..72acf91b8 100644 --- a/src/pages/Travel/Card/TravelDescriptor.vue +++ b/src/pages/Travel/Card/TravelDescriptor.vue @@ -32,6 +32,7 @@ const setData = (entity) => (data.value = useCardDescription(entity.ref, entity. <template> <CardDescriptor + module="Travel" :url="`Travels/${entityId}`" :title="data.title" :subtitle="data.subtitle" diff --git a/src/pages/Travel/Card/TravelFilter.js b/src/pages/Travel/Card/TravelFilter.js index 05436834f..f5f4520fd 100644 --- a/src/pages/Travel/Card/TravelFilter.js +++ b/src/pages/Travel/Card/TravelFilter.js @@ -11,7 +11,6 @@ export default { 'agencyModeFk', 'isRaid', 'daysInForward', - 'availabled', ], include: [ { diff --git a/src/pages/Travel/Card/TravelSummary.vue b/src/pages/Travel/Card/TravelSummary.vue index 9f9552611..16d42f104 100644 --- a/src/pages/Travel/Card/TravelSummary.vue +++ b/src/pages/Travel/Card/TravelSummary.vue @@ -10,8 +10,6 @@ import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue' import FetchData from 'src/components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import { toDate, toCurrency, toCelsius } from 'src/filters'; -import { toDateTimeFormat } from 'src/filters/date.js'; -import { dashIfEmpty } from 'src/filters'; import axios from 'axios'; import TravelDescriptorMenuItems from './TravelDescriptorMenuItems.vue'; @@ -335,12 +333,6 @@ const getLink = (param) => `#/travel/${entityId.value}/${param}`; <VnLv :label="t('globals.reference')" :value="travel.ref" /> <VnLv label="m³" :value="travel.m3" /> <VnLv :label="t('globals.totalEntries')" :value="travel.totalEntries" /> - <VnLv - :label="t('travel.summary.availabled')" - :value=" - dashIfEmpty(toDateTimeFormat(travel.availabled)) - " - /> </QCard> <QCard class="full-width"> <VnTitle :text="t('travel.summary.entries')" /> diff --git a/src/pages/Travel/Card/TravelThermographs.vue b/src/pages/Travel/Card/TravelThermographs.vue index 2376bd6d2..2946c8814 100644 --- a/src/pages/Travel/Card/TravelThermographs.vue +++ b/src/pages/Travel/Card/TravelThermographs.vue @@ -217,7 +217,7 @@ const removeThermograph = async (id) => { icon="add" color="primary" @click="redirectToThermographForm('create')" - v-shortcut="'+'" + shortcut="+" /> <QTooltip class="text-no-wrap"> {{ t('Add thermograph') }} diff --git a/src/pages/Travel/ExtraCommunityFilter.vue b/src/pages/Travel/ExtraCommunityFilter.vue index b22574632..b903aeabf 100644 --- a/src/pages/Travel/ExtraCommunityFilter.vue +++ b/src/pages/Travel/ExtraCommunityFilter.vue @@ -113,7 +113,7 @@ warehouses(); <template #append> <QBtn icon="add" - v-shortcut="'+'" + shortcut="+" flat dense size="12px" diff --git a/src/pages/Travel/TravelList.vue b/src/pages/Travel/TravelList.vue index b227afcb2..e90c01be2 100644 --- a/src/pages/Travel/TravelList.vue +++ b/src/pages/Travel/TravelList.vue @@ -10,9 +10,6 @@ import { getDateQBadgeColor } from 'src/composables/getDateQBadgeColor.js'; import TravelFilter from './TravelFilter.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import VnSection from 'src/components/common/VnSection.vue'; -import VnInputTime from 'src/components/common/VnInputTime.vue'; -import VnInputDate from 'src/components/common/VnInputDate.vue'; -import { toDateTimeFormat } from 'src/filters/date'; const { viewSummary } = useSummaryDialog(); const router = useRouter(); @@ -170,17 +167,6 @@ const columns = computed(() => [ cardVisible: true, create: true, }, - { - align: 'left', - name: 'availabled', - label: t('travel.summary.availabled'), - component: 'input', - columnClass: 'expand', - columnField: { - component: null, - }, - format: (row, dashIfEmpty) => dashIfEmpty(toDateTimeFormat(row.availabled)), - }, { align: 'right', label: '', @@ -283,16 +269,6 @@ const columns = computed(() => [ :class="{ 'is-active': row.isReceived }" /> </template> - <template #more-create-dialog="{ data }"> - <VnInputDate - v-model="data.availabled" - :label="t('travel.summary.availabled')" - /> - <VnInputTime - v-model="data.availabled" - :label="t('travel.summary.availabledHour')" - /> - </template> <template #moreFilterPanel="{ params }"> <VnInputNumber :label="t('params.scopeDays')" diff --git a/src/pages/Wagon/Card/WagonCard.vue b/src/pages/Wagon/Card/WagonCard.vue index 644a30ffa..ed6c83778 100644 --- a/src/pages/Wagon/Card/WagonCard.vue +++ b/src/pages/Wagon/Card/WagonCard.vue @@ -2,5 +2,5 @@ import VnCard from 'components/common/VnCard.vue'; </script> <template> - <VnCard data-key="Wagon" url="Wagons" /> + <VnCard data-key="Wagon" base-url="Wagons" /> </template> diff --git a/src/pages/Wagon/Type/WagonTypeList.vue b/src/pages/Wagon/Type/WagonTypeList.vue index 4c0b078a7..c0943c58e 100644 --- a/src/pages/Wagon/Type/WagonTypeList.vue +++ b/src/pages/Wagon/Type/WagonTypeList.vue @@ -96,13 +96,7 @@ async function remove(row) { > </VnTable> <QPageSticky :offset="[18, 18]"> - <QBtn - @click.stop="dialog.show()" - color="primary" - fab - icon="add" - v-shortcut="'+'" - > + <QBtn @click.stop="dialog.show()" color="primary" fab icon="add" shortcut="+"> <QDialog ref="dialog"> <FormModelPopup :title="t('Create new Wagon type')" diff --git a/src/pages/Worker/Card/WorkerBasicData.vue b/src/pages/Worker/Card/WorkerBasicData.vue index fcf0f0369..6a13e3f39 100644 --- a/src/pages/Worker/Card/WorkerBasicData.vue +++ b/src/pages/Worker/Card/WorkerBasicData.vue @@ -1,5 +1,6 @@ <script setup> -import { ref } from 'vue'; +import { ref, onBeforeMount } from 'vue'; +import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import FetchData from 'components/FetchData.vue'; @@ -10,13 +11,18 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import { useAdvancedSummary } from 'src/composables/useAdvancedSummary'; const { t } = useI18n(); -const form = ref(); const educationLevels = ref([]); const countries = ref([]); const maritalStatus = [ { code: 'M', name: t('Married') }, { code: 'S', name: t('Single') }, ]; +const advancedSummary = ref({}); + +onBeforeMount(async () => { + advancedSummary.value = + (await useAdvancedSummary('Workers', +useRoute().params.id)) ?? {}; +}); </script> <template> <FetchData @@ -32,15 +38,14 @@ const maritalStatus = [ auto-load /> <FormModel - ref="form" + :filter="{ where: { id: +$route.params.id } }" + url="Workers/summary" :url-update="`Workers/${$route.params.id}`" auto-load model="Worker" @on-fetch=" async (data) => { - Object.assign(data, (await useAdvancedSummary('Workers', data.id)) ?? {}); - await $nextTick(); - if (form) form.hasChanges = false; + Object.assign(data, advancedSummary); } " > diff --git a/src/pages/Worker/Card/WorkerCalendar.vue b/src/pages/Worker/Card/WorkerCalendar.vue index df4616011..5ca95a1a4 100644 --- a/src/pages/Worker/Card/WorkerCalendar.vue +++ b/src/pages/Worker/Card/WorkerCalendar.vue @@ -1,8 +1,7 @@ <script setup> -import { nextTick, ref, watch, computed } from 'vue'; +import { nextTick, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; -import { useAcl } from 'src/composables/useAcl'; import WorkerCalendarFilter from 'pages/Worker/Card/WorkerCalendarFilter.vue'; import FetchData from 'components/FetchData.vue'; @@ -10,17 +9,10 @@ import WorkerCalendarItem from 'pages/Worker/Card/WorkerCalendarItem.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import axios from 'axios'; -import VnNotes from 'src/components/ui/VnNotes.vue'; -import { useStateStore } from 'src/stores/useStateStore'; -const stateStore = useStateStore(); const router = useRouter(); const route = useRoute(); const { t } = useI18n(); -const acl = useAcl(); -const canSeeNotes = computed(() => - acl.hasAny([{ model: 'Worker', props: '__get__business', accessType: 'READ' }]), -); const workerIsFreelance = ref(); const WorkerFreelanceRef = ref(); const workerCalendarFilterRef = ref(null); @@ -34,10 +26,6 @@ const contractHolidays = ref(null); const yearHolidays = ref(null); const eventsMap = ref({}); const festiveEventsMap = ref({}); -const saveUrl = ref(); -const body = { - workerFk: route.params.id, -}; const onFetchActiveContract = (data) => { if (!data) return; @@ -79,7 +67,7 @@ const onFetchAbsences = (data) => { name: holidayName, isFestive: true, }, - true, + true ); }); } @@ -158,7 +146,7 @@ watch( async () => { await nextTick(); await activeContractRef.value.fetch(); - }, + } ); watch([year, businessFk], () => refreshData()); </script> @@ -193,20 +181,6 @@ watch([year, businessFk], () => refreshData()); /> </template> </RightMenu> - <Teleport to="#st-data" v-if="stateStore.isSubToolbarShown() && canSeeNotes"> - <VnNotes - :just-input="true" - :url="`Workers/${route.params.id}/business`" - :filter="{ fields: ['id', 'notes', 'workerFk'] }" - :save-url="saveUrl" - @on-fetch=" - (data) => { - saveUrl = `Businesses/${data.id}`; - } - " - :body="body" - /> - </Teleport> <QPage class="column items-center"> <QCard v-if="workerIsFreelance"> <QCardSection class="text-center"> diff --git a/src/pages/Worker/Card/WorkerCalendarFilter.vue b/src/pages/Worker/Card/WorkerCalendarFilter.vue index 48fc4094b..67b7df907 100644 --- a/src/pages/Worker/Card/WorkerCalendarFilter.vue +++ b/src/pages/Worker/Card/WorkerCalendarFilter.vue @@ -180,6 +180,8 @@ const yearList = ref(generateYears()); :is-clearable="false" /> </QItemSection> + </QItem> + <QItem> <QItemSection> <VnSelect :label="t('Contract')" diff --git a/src/pages/Worker/Card/WorkerCard.vue b/src/pages/Worker/Card/WorkerCard.vue index 3b7a62025..1ada15a33 100644 --- a/src/pages/Worker/Card/WorkerCard.vue +++ b/src/pages/Worker/Card/WorkerCard.vue @@ -3,10 +3,5 @@ import WorkerDescriptor from './WorkerDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; </script> <template> - <VnCardBeta - data-key="Worker" - url="Workers/summary" - :id-in-where="true" - :descriptor="WorkerDescriptor" - /> + <VnCardBeta data-key="Worker" custom-url="Workers/summary" :descriptor="WorkerDescriptor" /> </template> diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index de3f634e2..d87fd4a54 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -10,7 +10,7 @@ import axios from 'axios'; import VnImg from 'src/components/ui/VnImg.vue'; import EditPictureForm from 'components/EditPictureForm.vue'; import WorkerDescriptorMenu from './WorkerDescriptorMenu.vue'; -import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; const $props = defineProps({ id: { @@ -21,7 +21,7 @@ const $props = defineProps({ dataKey: { type: String, required: false, - default: 'Worker', + default: 'workerData', }, }); const image = ref(null); @@ -50,8 +50,9 @@ const handlePhotoUpdated = (evt = false) => { <template> <CardDescriptor ref="cardDescriptorRef" + module="Worker" :data-key="dataKey" - url="Workers/summary" + url="Workers/descriptor" :filter="{ where: { id: entityId } }" title="user.nickname" @on-fetch="getIsExcluded" @@ -151,7 +152,7 @@ const handlePhotoUpdated = (evt = false) => { <QBtn :to="{ name: 'AccountCard', - params: { id: entity.user?.id }, + params: { id: entity.user.id }, }" size="md" icon="face" diff --git a/src/pages/Worker/Card/WorkerDescriptorProxy.vue b/src/pages/Worker/Card/WorkerDescriptorProxy.vue index a142570f9..43deb7821 100644 --- a/src/pages/Worker/Card/WorkerDescriptorProxy.vue +++ b/src/pages/Worker/Card/WorkerDescriptorProxy.vue @@ -12,6 +12,11 @@ const $props = defineProps({ <template> <QPopupProxy> - <WorkerDescriptor v-if="$props.id" :id="$props.id" :summary="WorkerSummary" /> + <WorkerDescriptor + v-if="$props.id" + :id="$props.id" + :summary="WorkerSummary" + data-key="workerDescriptorProxy" + /> </QPopupProxy> </template> diff --git a/src/pages/Worker/Card/WorkerFormation.vue b/src/pages/Worker/Card/WorkerFormation.vue index e8680f7dd..6fd5a4eae 100644 --- a/src/pages/Worker/Card/WorkerFormation.vue +++ b/src/pages/Worker/Card/WorkerFormation.vue @@ -94,7 +94,6 @@ const columns = computed(() => [ align: 'left', name: 'hasDiploma', label: t('worker.formation.tableVisibleColumns.hasDiploma'), - component: 'checkbox', create: true, }, { @@ -119,7 +118,7 @@ const columns = computed(() => [ :url="`Workers/${entityId}/trainingCourse`" :url-create="`Workers/${entityId}/trainingCourse`" save-url="TrainingCourses/crud" - :user-filter="courseFilter" + :filter="courseFilter" :create="{ urlCreate: 'trainingCourses', title: t('Create training course'), diff --git a/src/pages/Worker/Card/WorkerMedical.vue b/src/pages/Worker/Card/WorkerMedical.vue index c04f6496b..c220df76a 100644 --- a/src/pages/Worker/Card/WorkerMedical.vue +++ b/src/pages/Worker/Card/WorkerMedical.vue @@ -3,23 +3,11 @@ import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import VnTable from 'components/VnTable/VnTable.vue'; -import { dashIfEmpty } from 'src/filters'; const tableRef = ref(); const { t } = useI18n(); const route = useRoute(); const entityId = computed(() => route.params.id); -const centerFilter = { - include: [ - { - relation: 'center', - scope: { - fields: ['id', 'name'], - }, - }, - ], -}; - const columns = [ { align: 'left', @@ -48,9 +36,6 @@ const columns = [ url: 'medicalCenters', fields: ['id', 'name'], }, - format: (row, dashIfEmpty) => { - return dashIfEmpty(row.center?.name); - }, }, { align: 'left', @@ -99,7 +84,6 @@ const columns = [ ref="tableRef" data-key="WorkerMedical" :url="`Workers/${entityId}/medicalReview`" - :user-filter="centerFilter" save-url="MedicalReviews/crud" :create="{ urlCreate: 'medicalReviews', diff --git a/src/pages/Worker/Card/WorkerOperator.vue b/src/pages/Worker/Card/WorkerOperator.vue index 6faeefe67..cdacc72c0 100644 --- a/src/pages/Worker/Card/WorkerOperator.vue +++ b/src/pages/Worker/Card/WorkerOperator.vue @@ -1,7 +1,7 @@ <script setup> import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import { ref, computed, watch } from 'vue'; +import { ref, computed } from 'vue'; import FetchData from 'components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; @@ -19,7 +19,6 @@ const trainsData = ref([]); const machinesData = ref([]); const route = useRoute(); const routeId = computed(() => route.params.id); -const selected = ref([]); const initialData = computed(() => { return { @@ -42,21 +41,6 @@ async function insert() { await axios.post('Operators', initialData.value); crudModelRef.value.reload(); } - -watch( - () => crudModelRef.value?.formData, - (formData) => { - if (formData && formData.length) { - if (JSON.stringify(selected.value) !== JSON.stringify(formData)) { - selected.value = formData; - } - } else if (selected.value.length > 0) { - selected.value = []; - } - }, - { immediate: true, deep: true } -); - </script> <template> @@ -83,7 +67,6 @@ watch( :data-required="{ workerFk: route.params.id }" ref="crudModelRef" search-url="operator" - :selected="selected" auto-load > <template #body="{ rows }"> diff --git a/src/pages/Worker/Card/WorkerPda.vue b/src/pages/Worker/Card/WorkerPda.vue index 47e13cf6d..f6cb92aac 100644 --- a/src/pages/Worker/Card/WorkerPda.vue +++ b/src/pages/Worker/Card/WorkerPda.vue @@ -101,7 +101,7 @@ function reloadData() { openConfirmationModal( t(`Remove PDA`), t('Do you want to remove this PDA?'), - () => deallocatePDA(row.deviceProductionFk), + () => deallocatePDA(row.deviceProductionFk) ) " > @@ -114,13 +114,7 @@ function reloadData() { </template> </VnPaginate> <QPageSticky :offset="[18, 18]"> - <QBtn - @click.stop="dialog.show()" - color="primary" - fab - icon="add" - v-shortcut="'+'" - > + <QBtn @click.stop="dialog.show()" color="primary" fab icon="add" shortcut="+"> <QDialog ref="dialog"> <FormModelPopup :title="t('Add new device')" diff --git a/src/pages/Worker/Card/WorkerPit.vue b/src/pages/Worker/Card/WorkerPit.vue index 40e814452..79cf1a04f 100644 --- a/src/pages/Worker/Card/WorkerPit.vue +++ b/src/pages/Worker/Card/WorkerPit.vue @@ -221,7 +221,7 @@ const deleteRelative = async (id) => { color="primary" flat icon="add" - v-shortcut="'+'" + shortcut="+" style="flex: 0" data-cy="addRelative" /> diff --git a/src/pages/Worker/Card/WorkerSummary.vue b/src/pages/Worker/Card/WorkerSummary.vue index 78c5dfd82..992f6ec71 100644 --- a/src/pages/Worker/Card/WorkerSummary.vue +++ b/src/pages/Worker/Card/WorkerSummary.vue @@ -9,7 +9,7 @@ import CardSummary from 'components/ui/CardSummary.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnTitle from 'src/components/common/VnTitle.vue'; import RoleDescriptorProxy from 'src/pages/Account/Role/Card/RoleDescriptorProxy.vue'; -import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; import { useAdvancedSummary } from 'src/composables/useAdvancedSummary'; import WorkerDescriptorMenu from './WorkerDescriptorMenu.vue'; diff --git a/src/pages/Worker/Card/WorkerTimeControl.vue b/src/pages/Worker/Card/WorkerTimeControl.vue index 7def6e94c..c580e5202 100644 --- a/src/pages/Worker/Card/WorkerTimeControl.vue +++ b/src/pages/Worker/Card/WorkerTimeControl.vue @@ -64,17 +64,17 @@ const selectedCalendarDates = ref([]); // Date formateada para bindear al componente QDate const selectedDateFormatted = ref(toDateString(defaultDate.value)); -const arrayData = useArrayData('Worker'); +const arrayData = useArrayData('workerData'); const acl = useAcl(); const selectedDateYear = computed(() => moment(selectedDate.value).isoWeekYear()); const worker = computed(() => arrayData.store?.data); const canSend = computed(() => - acl.hasAny([{ model: 'WorkerTimeControl', props: 'sendMail', accessType: 'WRITE' }]), + acl.hasAny([{ model: 'WorkerTimeControl', props: 'sendMail', accessType: 'WRITE' }]) ); const canUpdate = computed(() => acl.hasAny([ { model: 'WorkerTimeControl', props: 'updateMailState', accessType: 'WRITE' }, - ]), + ]) ); const isHimself = computed(() => user.value.id === Number(route.params.id)); @@ -100,7 +100,7 @@ const getHeaderFormattedDate = (date) => { }; const formattedWeekTotalHours = computed(() => - secondsToHoursMinutes(weekTotalHours.value), + secondsToHoursMinutes(weekTotalHours.value) ); const onInputChange = async (date) => { @@ -320,7 +320,7 @@ const getFinishTime = () => { today.setHours(0, 0, 0, 0); let todayInWeek = weekDays.value.find( - (day) => day.dated.getTime() === today.getTime(), + (day) => day.dated.getTime() === today.getTime() ); if (todayInWeek && todayInWeek.hours && todayInWeek.hours.length) { @@ -472,7 +472,7 @@ onMounted(async () => { openConfirmationModal( t('Send time control email'), t('Are you sure you want to send it?'), - resendEmail, + resendEmail ) " > @@ -561,7 +561,7 @@ onMounted(async () => { @show-worker-time-form=" showWorkerTimeForm( { id: hour.id, entryCode: hour.direction }, - 'edit', + 'edit' ) " class="hour-chip" @@ -577,7 +577,7 @@ onMounted(async () => { </span> <QBtn icon="add_circle" - v-shortcut="'+'" + shortcut="+" flat color="primary" class="fill-icon cursor-pointer" diff --git a/src/pages/Worker/WorkerDepartmentTree.vue b/src/pages/Worker/WorkerDepartmentTree.vue index 9baf5ee57..9abf4e312 100644 --- a/src/pages/Worker/WorkerDepartmentTree.vue +++ b/src/pages/Worker/WorkerDepartmentTree.vue @@ -3,7 +3,7 @@ import { onMounted, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useState } from 'src/composables/useState'; import { useQuasar } from 'quasar'; -import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; import CreateDepartmentChild from './CreateDepartmentChild.vue'; import axios from 'axios'; import { useRouter } from 'vue-router'; @@ -173,7 +173,7 @@ function handleEvent(type, event, node) { color="primary" flat icon="add" - v-shortcut="'+'" + shortcut="+" class="cursor-pointer" @click.stop="showCreateNodeForm(node.id)" > diff --git a/src/pages/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue index 03013f011..cbeeff2e9 100644 --- a/src/pages/Zone/Card/ZoneBasicData.vue +++ b/src/pages/Zone/Card/ZoneBasicData.vue @@ -1,7 +1,5 @@ <script setup> import { useI18n } from 'vue-i18n'; -import { ref } from 'vue'; -import FetchData from 'components/FetchData.vue'; import FormModel from 'src/components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; @@ -9,23 +7,10 @@ import VnInputTime from 'src/components/common/VnInputTime.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; const { t } = useI18n(); -const validAddresses = ref([]); -const addresses = ref([]); - -const setFilteredAddresses = (data) => { - const validIds = new Set(validAddresses.value.map((item) => item.addressFk)); - addresses.value = data.filter((address) => validIds.has(address.id)); -}; </script> <template> - <FetchData - url="RoadmapAddresses" - auto-load - @on-fetch="(data) => (validAddresses = data)" - /> - <FetchData url="Addresses" auto-load @on-fetch="setFilteredAddresses" /> - <FormModel auto-load model="Zone"> + <FormModel :url="`Zones/${$route.params.id}`" auto-load model="zone"> <template #form="{ data, validate }"> <VnRow> <VnInput @@ -33,15 +18,15 @@ const setFilteredAddresses = (data) => { :label="t('Name')" clearable v-model="data.name" - :required="true" /> </VnRow> + <VnRow> <VnSelect v-model="data.agencyModeFk" :rules="validate('zone.agencyModeFk')" - url="AgencyModes/isActive" - :fields="['id', 'name']" + url="AgencyModes/isActive" + :fields="['id', 'name']" :label="t('Agency')" emit-value map-options @@ -84,7 +69,7 @@ const setFilteredAddresses = (data) => { type="number" min="0" /> - <VnInputTime v-model="data.hour" :label="t('Closing')" :required="true" /> + <VnInputTime v-model="data.hour" :label="t('Closing')" /> </VnRow> <VnRow> @@ -93,7 +78,7 @@ const setFilteredAddresses = (data) => { :label="t('Price')" type="number" min="0" - :required="true" + required="true" clearable /> <VnInput @@ -101,7 +86,7 @@ const setFilteredAddresses = (data) => { :label="t('Price optimum')" type="number" min="0" - :required="true" + required="true" clearable /> </VnRow> @@ -118,14 +103,12 @@ const setFilteredAddresses = (data) => { v-model="data.addressFk" option-value="id" option-label="nickname" - :options="addresses" + url="Addresses" :fields="['id', 'nickname']" sort-by="id" hide-selected map-options :rules="validate('data.addressFk')" - :filter-options="['id']" - :where="filterWhere" /> </VnRow> <VnRow> diff --git a/src/pages/Zone/Card/ZoneCard.vue b/src/pages/Zone/Card/ZoneCard.vue index 41daff5c0..a470cd5bd 100644 --- a/src/pages/Zone/Card/ZoneCard.vue +++ b/src/pages/Zone/Card/ZoneCard.vue @@ -1,12 +1,13 @@ <script setup> +import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import { computed } from 'vue'; import VnCard from 'components/common/VnCard.vue'; import ZoneDescriptor from './ZoneDescriptor.vue'; import ZoneFilterPanel from '../ZoneFilterPanel.vue'; -import filter from './ZoneFilter.js'; +const { t } = useI18n(); const route = useRoute(); const routeName = computed(() => route.name); @@ -18,16 +19,15 @@ function notIsLocations(ifIsFalse, ifIsTrue) { <template> <VnCard - data-key="Zone" - :url="notIsLocations('Zones', undefined)" + data-key="zone" + :base-url="notIsLocations('Zones', undefined)" :descriptor="ZoneDescriptor" - :filter="filter" :filter-panel="notIsLocations(ZoneFilterPanel, undefined)" :search-data-key="notIsLocations('ZoneList', undefined)" :searchbar-props="{ url: notIsLocations('Zones', 'ZoneLocations'), - label: notIsLocations($t('list.searchZone'), $t('list.searchLocation')), - info: $t('list.searchInfo'), + label: notIsLocations(t('list.searchZone'), t('list.searchLocation')), + info: t('list.searchInfo'), whereFilter: notIsLocations((value) => { return /^\d+$/.test(value) ? { id: value } diff --git a/src/pages/Zone/Card/ZoneDescriptor.vue b/src/pages/Zone/Card/ZoneDescriptor.vue index 27676212e..8355c219e 100644 --- a/src/pages/Zone/Card/ZoneDescriptor.vue +++ b/src/pages/Zone/Card/ZoneDescriptor.vue @@ -1,14 +1,15 @@ <script setup> -import { computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import { toTimeFormat } from 'src/filters/date'; import { toCurrency } from 'filters/index'; +import useCardDescription from 'src/composables/useCardDescription'; import ZoneDescriptorMenuItems from './ZoneDescriptorMenuItems.vue'; -import filter from './ZoneFilter.js'; const $props = defineProps({ id: { @@ -19,22 +20,49 @@ const $props = defineProps({ }); const route = useRoute(); +const { t } = useI18n(); + +const filter = { + include: [ + { + relation: 'agencyMode', + scope: { + fields: ['name', 'id'], + }, + }, + ], +}; + const entityId = computed(() => { return $props.id || route.params.id; }); + +const data = ref(useCardDescription()); +const setData = (entity) => { + data.value = useCardDescription(entity.ref, entity.id); +}; </script> <template> - <CardDescriptor :url="`Zones/${entityId}`" :filter="filter" data-key="Zone"> + <CardDescriptor + module="Zone" + :url="`Zones/${entityId}`" + :title="data.title" + :subtitle="data.subtitle" + :filter="filter" + @on-fetch="setData" + data-key="zoneData" + > <template #menu="{ entity }"> <ZoneDescriptorMenuItems :zone="entity" /> </template> <template #body="{ entity }"> - <VnLv :label="$t('list.agency')" :value="entity.agencyMode?.name" /> - <VnLv :label="$t('zone.closing')" :value="toTimeFormat(entity.hour)" /> - <VnLv :label="$t('zone.travelingDays')" :value="entity.travelingDays" /> - <VnLv :label="$t('list.price')" :value="toCurrency(entity.price)" /> - <VnLv :label="$t('zone.bonus')" :value="toCurrency(entity.bonus)" /> + <VnLv :label="t('list.agency')" :value="entity.agencyMode.name" /> + <VnLv :label="t('zone.closing')" :value="toTimeFormat(entity.hour)" /> + <VnLv :label="t('zone.travelingDays')" :value="entity.travelingDays" /> + <VnLv :label="t('list.price')" :value="toCurrency(entity.price)" /> + <VnLv :label="t('zone.bonus')" :value="toCurrency(entity.bonus)" /> </template> </CardDescriptor> </template> + diff --git a/src/pages/Zone/Card/ZoneEvents.vue b/src/pages/Zone/Card/ZoneEvents.vue index 1e6debd25..a5806bab9 100644 --- a/src/pages/Zone/Card/ZoneEvents.vue +++ b/src/pages/Zone/Card/ZoneEvents.vue @@ -78,13 +78,13 @@ const onZoneEventFormClose = () => { { isNewMode: true, }, - true, + true ) " color="primary" fab icon="add" - v-shortcut="'+'" + shortcut="+" /> <QTooltip class="text-no-wrap"> {{ t('eventsInclusionForm.addEvent') }} diff --git a/src/pages/Zone/Card/ZoneFilter.js b/src/pages/Zone/Card/ZoneFilter.js deleted file mode 100644 index 3298c7c8a..000000000 --- a/src/pages/Zone/Card/ZoneFilter.js +++ /dev/null @@ -1,10 +0,0 @@ -export default { - include: [ - { - relation: 'agencyMode', - scope: { - fields: ['name', 'id'], - }, - }, - ], -}; diff --git a/src/pages/Zone/Card/ZoneSearchbar.vue b/src/pages/Zone/Card/ZoneSearchbar.vue index d1188a1e8..f7a59e97f 100644 --- a/src/pages/Zone/Card/ZoneSearchbar.vue +++ b/src/pages/Zone/Card/ZoneSearchbar.vue @@ -22,50 +22,15 @@ const exprBuilder = (param, value) => { return /^\d+$/.test(value) ? { id: value } : { name: { like: `%${value}%` } }; } }; - -const tableFilter = { - include: [ - { - relation: 'agencyMode', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'address', - scope: { - fields: ['id', 'nickname', 'provinceFk', 'postalCode'], - include: [ - { - relation: 'province', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'postcode', - scope: { - fields: ['code', 'townFk'], - include: { - relation: 'town', - scope: { - fields: ['id', 'name'], - }, - }, - }, - }, - ], - }, - }, - ], -}; </script> <template> <VnSearchbar data-key="ZonesList" url="Zones" - :filter="tableFilter" + :filter="{ + include: { relation: 'agencyMode', scope: { fields: ['name'] } }, + }" :expr-builder="exprBuilder" :label="t('list.searchZone')" :info="t('list.searchInfo')" diff --git a/src/pages/Zone/Card/ZoneSummary.vue b/src/pages/Zone/Card/ZoneSummary.vue index 5b29b495b..124802633 100644 --- a/src/pages/Zone/Card/ZoneSummary.vue +++ b/src/pages/Zone/Card/ZoneSummary.vue @@ -11,7 +11,6 @@ import { getUrl } from 'src/composables/getUrl'; import { toCurrency } from 'filters/index'; import { toTimeFormat } from 'src/filters/date'; import axios from 'axios'; -import filter from './ZoneFilter.js'; import ZoneDescriptorMenuItems from './ZoneDescriptorMenuItems.vue'; const route = useRoute(); @@ -27,6 +26,19 @@ const $props = defineProps({ const entityId = computed(() => $props.id || route.params.id); const zoneUrl = ref(); +const filter = computed(() => { + const filter = { + include: { + relation: 'agencyMode', + fields: ['name'], + }, + where: { + id: entityId, + }, + }; + return filter; +}); + const columns = computed(() => [ { label: t('list.name'), @@ -60,9 +72,9 @@ onMounted(async () => { <template> <CardSummary - data-key="Zone" + data-key="ZoneSummary" ref="summary" - :url="`Zones/${entityId}`" + url="Zones/findOne" :filter="filter" > <template #header="{ entity }"> diff --git a/src/pages/Zone/Card/ZoneWarehouses.vue b/src/pages/Zone/Card/ZoneWarehouses.vue index 165e9c840..c96735697 100644 --- a/src/pages/Zone/Card/ZoneWarehouses.vue +++ b/src/pages/Zone/Card/ZoneWarehouses.vue @@ -109,7 +109,7 @@ const openCreateWarehouseForm = () => createWarehouseDialogRef.value.show(); icon="add" color="primary" @click="openCreateWarehouseForm()" - v-shortcut="'+'" + shortcut="+" > <QTooltip>{{ t('warehouses.add') }}</QTooltip> </QBtn> diff --git a/src/pages/Zone/Delivery/ZoneDeliveryList.vue b/src/pages/Zone/Delivery/ZoneDeliveryList.vue index e3ec8cb2d..975cbdb67 100644 --- a/src/pages/Zone/Delivery/ZoneDeliveryList.vue +++ b/src/pages/Zone/Delivery/ZoneDeliveryList.vue @@ -74,7 +74,7 @@ async function remove(row) { </VnPaginate> </div> <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn @click="create" fab icon="add" v-shortcut="'+'" color="primary" /> + <QBtn @click="create" fab icon="add" shortcut="+" color="primary" /> </QPageSticky> </QPage> </template> diff --git a/src/pages/Zone/Upcoming/ZoneUpcomingList.vue b/src/pages/Zone/Upcoming/ZoneUpcomingList.vue index 7b5c2ddbc..5a7f0bb4c 100644 --- a/src/pages/Zone/Upcoming/ZoneUpcomingList.vue +++ b/src/pages/Zone/Upcoming/ZoneUpcomingList.vue @@ -74,7 +74,7 @@ async function remove(row) { </VnPaginate> </div> <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn @click="create" fab icon="add" v-shortcut="'+'" color="primary" /> + <QBtn @click="create" fab icon="add" shortcut="+" color="primary" /> </QPageSticky> </QPage> </template> diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue index 4df84e4bd..e4a1774fe 100644 --- a/src/pages/Zone/ZoneList.vue +++ b/src/pages/Zone/ZoneList.vue @@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'; import { computed, ref } from 'vue'; import axios from 'axios'; -import { dashIfEmpty, toCurrency } from 'src/filters'; +import { toCurrency } from 'src/filters'; import { toTimeFormat } from 'src/filters/date'; import { useVnConfirm } from 'composables/useVnConfirm'; import useNotify from 'src/composables/useNotify.js'; @@ -17,6 +17,7 @@ import VnInputTime from 'src/components/common/VnInputTime.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import ZoneFilterPanel from './ZoneFilterPanel.vue'; import ZoneSearchbar from './Card/ZoneSearchbar.vue'; +import FetchData from 'src/components/FetchData.vue'; const { t } = useI18n(); const router = useRouter(); @@ -25,6 +26,7 @@ const { viewSummary } = useSummaryDialog(); const { openConfirmationModal } = useVnConfirm(); const tableRef = ref(); const warehouseOptions = ref([]); +const validAddresses = ref([]); const tableFilter = { include: [ @@ -129,7 +131,6 @@ const columns = computed(() => [ label: t('list.addressFk'), cardVisible: true, columnFilter: false, - columnClass: 'expand', }, { align: 'right', @@ -160,18 +161,30 @@ const handleClone = (id) => { openConfirmationModal( t('list.confirmCloneTitle'), t('list.confirmCloneSubtitle'), - () => clone(id), + () => clone(id) ); }; -function formatRow(row) { - if (!row?.address) return '-'; - return dashIfEmpty(`${row?.address?.nickname}, - ${row?.address?.postcode?.town?.name} (${row?.address?.province?.name})`); +function showValidAddresses(row) { + if (row.addressFk) { + const isValid = validAddresses.value.some( + (address) => address.addressFk === row.addressFk + ); + if (isValid) + return `${row.address?.nickname}, + ${row.address?.postcode?.town?.name} (${row.address?.province?.name})`; + else return '-'; + } + return '-'; } </script> <template> + <FetchData + url="RoadmapAddresses" + auto-load + @on-fetch="(data) => (validAddresses = data)" + /> <ZoneSearchbar /> <RightMenu> <template #right-panel> @@ -194,7 +207,7 @@ function formatRow(row) { :right-search="false" > <template #column-addressFk="{ row }"> - {{ dashIfEmpty(formatRow(row)) }} + {{ showValidAddresses(row) }} </template> <template #more-create-dialog="{ data }"> <VnSelect diff --git a/src/router/modules/account/aliasCard.js b/src/router/modules/account/aliasCard.js index a5b00f44b..cbbd31e51 100644 --- a/src/router/modules/account/aliasCard.js +++ b/src/router/modules/account/aliasCard.js @@ -3,7 +3,7 @@ export default { path: ':id', component: () => import('src/pages/Account/Alias/Card/AliasCard.vue'), redirect: { name: 'AliasSummary' }, - meta: { moduleName: 'Alias', menu: ['AliasBasicData', 'AliasUsers'] }, + meta: { menu: ['AliasBasicData', 'AliasUsers'] }, children: [ { name: 'AliasSummary', diff --git a/src/router/modules/account/roleCard.js b/src/router/modules/account/roleCard.js index f8100071f..c36ce71b9 100644 --- a/src/router/modules/account/roleCard.js +++ b/src/router/modules/account/roleCard.js @@ -4,7 +4,6 @@ export default { component: () => import('src/pages/Account/Role/Card/RoleCard.vue'), redirect: { name: 'RoleSummary' }, meta: { - moduleName: 'Role', menu: ['RoleBasicData', 'SubRoles', 'InheritedRoles', 'RoleLog'], }, children: [ diff --git a/src/router/modules/entry.js b/src/router/modules/entry.js index b5656dc5f..f362c7653 100644 --- a/src/router/modules/entry.js +++ b/src/router/modules/entry.js @@ -6,7 +6,13 @@ const entryCard = { component: () => import('src/pages/Entry/Card/EntryCard.vue'), redirect: { name: 'EntrySummary' }, meta: { - menu: ['EntryBasicData', 'EntryBuys', 'EntryNotes', 'EntryDms', 'EntryLog'], + menu: [ + 'EntryBasicData', + 'EntryBuys', + 'EntryNotes', + 'EntryDms', + 'EntryLog', + ], }, children: [ { @@ -85,7 +91,7 @@ export default { 'EntryLatestBuys', 'EntryStockBought', 'EntryWasteRecalc', - ], + ] }, component: RouterView, redirect: { name: 'EntryMain' }, @@ -97,7 +103,7 @@ export default { redirect: { name: 'EntryIndexMain' }, children: [ { - path: '', + path:'', name: 'EntryIndexMain', redirect: { name: 'EntryList' }, component: () => import('src/pages/Entry/EntryList.vue'), @@ -109,7 +115,6 @@ export default { title: 'list', icon: 'view_list', }, - component: () => import('src/pages/Entry/EntryList.vue'), }, entryCard, ], @@ -122,7 +127,7 @@ export default { icon: 'add', }, component: () => import('src/pages/Entry/EntryCreate.vue'), - }, + }, { path: 'my', name: 'MyEntries', @@ -162,4 +167,4 @@ export default { ], }, ], -}; +}; \ No newline at end of file diff --git a/src/router/modules/route.js b/src/router/modules/route.js index 835324d20..946ad3e15 100644 --- a/src/router/modules/route.js +++ b/src/router/modules/route.js @@ -160,36 +160,6 @@ const roadmapCard = { ], }; -const vehicleCard = { - path: ':id', - name: 'VehicleCard', - component: () => import('src/pages/Route/Vehicle/Card/VehicleCard.vue'), - redirect: { name: 'VehicleSummary' }, - meta: { - menu: ['VehicleBasicData'], - }, - children: [ - { - name: 'VehicleSummary', - path: 'summary', - meta: { - title: 'summary', - icon: 'view_list', - }, - component: () => import('src/pages/Route/Vehicle/Card/VehicleSummary.vue'), - }, - { - name: 'VehicleBasicData', - path: 'basic-data', - meta: { - title: 'basicData', - icon: 'vn:settings', - }, - component: () => import('src/pages/Route/Vehicle/Card/VehicleBasicData.vue'), - }, - ], -}; - export default { name: 'Route', path: '/route', @@ -204,7 +174,6 @@ export default { 'RouteRoadmap', 'CmrList', 'AgencyList', - 'VehicleList', ], }, component: RouterView, @@ -311,27 +280,6 @@ export default { agencyCard, ], }, - { - path: 'vehicle', - name: 'RouteVehicle', - redirect: { name: 'VehicleList' }, - meta: { - title: 'vehicle', - icon: 'directions_car', - }, - component: () => import('src/pages/Route/Vehicle/VehicleList.vue'), - children: [ - { - path: 'list', - name: 'VehicleList', - meta: { - title: 'vehicleList', - icon: 'directions_car', - }, - }, - vehicleCard, - ], - }, ], }, ], diff --git a/src/router/modules/shelving.js b/src/router/modules/shelving.js index c085dd8dc..55fb04278 100644 --- a/src/router/modules/shelving.js +++ b/src/router/modules/shelving.js @@ -3,7 +3,7 @@ import { RouterView } from 'vue-router'; const parkingCard = { name: 'ParkingCard', path: ':id', - component: () => import('src/pages/Shelving/Parking/Card/ParkingCard.vue'), + component: () => import('src/pages/Parking/Card/ParkingCard.vue'), redirect: { name: 'ParkingSummary' }, meta: { menu: ['ParkingBasicData', 'ParkingLog'], @@ -16,7 +16,7 @@ const parkingCard = { title: 'summary', icon: 'launch', }, - component: () => import('src/pages/Shelving/Parking/Card/ParkingSummary.vue'), + component: () => import('src/pages/Parking/Card/ParkingSummary.vue'), }, { path: 'basic-data', @@ -25,8 +25,7 @@ const parkingCard = { title: 'basicData', icon: 'vn:settings', }, - component: () => - import('src/pages/Shelving/Parking/Card/ParkingBasicData.vue'), + component: () => import('src/pages/Parking/Card/ParkingBasicData.vue'), }, { path: 'log', @@ -35,7 +34,7 @@ const parkingCard = { title: 'log', icon: 'history', }, - component: () => import('src/pages/Shelving/Parking/Card/ParkingLog.vue'), + component: () => import('src/pages/Parking/Card/ParkingLog.vue'), }, ], }; @@ -128,7 +127,7 @@ export default { title: 'parkingList', icon: 'view_list', }, - component: () => import('src/pages/Shelving/Parking/ParkingList.vue'), + component: () => import('src/pages/Parking/ParkingList.vue'), children: [ { path: 'list', diff --git a/src/router/modules/supplier.js b/src/router/modules/supplier.js index 19763cdf3..4ece4c784 100644 --- a/src/router/modules/supplier.js +++ b/src/router/modules/supplier.js @@ -1,12 +1,19 @@ import { RouterView } from 'vue-router'; -const supplierCard = { - name: 'SupplierCard', - path: ':id', - component: () => import('src/pages/Supplier/Card/SupplierCard.vue'), - redirect: { name: 'SupplierSummary' }, +export default { + path: '/supplier', + name: 'Supplier', meta: { - menu: [ + title: 'suppliers', + icon: 'vn:supplier', + moduleName: 'Supplier', + keyBinding: 'p', + }, + component: RouterView, + redirect: { name: 'SupplierMain' }, + menus: { + main: ['SupplierList'], + card: [ 'SupplierBasicData', 'SupplierFiscalData', 'SupplierBillingData', @@ -20,165 +27,21 @@ const supplierCard = { 'SupplierDms', ], }, - children: [ - { - name: 'SupplierSummary', - path: 'summary', - meta: { - title: 'summary', - icon: 'launch', - }, - component: () => import('src/pages/Supplier/Card/SupplierSummary.vue'), - }, - { - path: 'basic-data', - name: 'SupplierBasicData', - meta: { - title: 'basicData', - icon: 'vn:settings', - }, - component: () => import('src/pages/Supplier/Card/SupplierBasicData.vue'), - }, - { - path: 'fiscal-data', - name: 'SupplierFiscalData', - meta: { - title: 'fiscalData', - icon: 'vn:dfiscales', - }, - component: () => import('src/pages/Supplier/Card/SupplierFiscalData.vue'), - }, - { - path: 'billing-data', - name: 'SupplierBillingData', - meta: { - title: 'billingData', - icon: 'vn:payment', - }, - component: () => import('src/pages/Supplier/Card/SupplierBillingData.vue'), - }, - { - path: 'log', - name: 'SupplierLog', - meta: { - title: 'log', - icon: 'vn:History', - }, - component: () => import('src/pages/Supplier/Card/SupplierLog.vue'), - }, - { - path: 'account', - name: 'SupplierAccounts', - meta: { - title: 'accounts', - icon: 'vn:credit', - }, - component: () => import('src/pages/Supplier/Card/SupplierAccounts.vue'), - }, - { - path: 'contact', - name: 'SupplierContacts', - meta: { - title: 'contacts', - icon: 'contact_phone', - }, - component: () => import('src/pages/Supplier/Card/SupplierContacts.vue'), - }, - { - path: 'address', - name: 'SupplierAddresses', - meta: { - title: 'addresses', - icon: 'vn:delivery', - }, - component: () => import('src/pages/Supplier/Card/SupplierAddresses.vue'), - }, - { - path: 'address/create', - name: 'SupplierAddressesCreate', - component: () => - import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'), - }, - { - path: 'balance', - name: 'SupplierBalance', - meta: { - title: 'balance', - icon: 'balance', - }, - component: () => import('src/pages/Supplier/Card/SupplierBalance.vue'), - }, - { - path: 'consumption', - name: 'SupplierConsumption', - meta: { - title: 'consumption', - icon: 'show_chart', - }, - component: () => import('src/pages/Supplier/Card/SupplierConsumption.vue'), - }, - { - path: 'agency-term', - name: 'SupplierAgencyTerm', - meta: { - title: 'agencyTerm', - icon: 'vn:agency-term', - }, - component: () => import('src/pages/Supplier/Card/SupplierAgencyTerm.vue'), - }, - { - path: 'dms', - name: 'SupplierDms', - meta: { - title: 'dms', - icon: 'smb_share', - }, - component: () => import('src/pages/Supplier/Card/SupplierDms.vue'), - }, - { - path: 'agency-term/create', - name: 'SupplierAgencyTermCreate', - component: () => - import('src/pages/Supplier/Card/SupplierAgencyTermCreate.vue'), - }, - ], -}; - -export default { - name: 'Supplier', - path: '/supplier', - meta: { - title: 'suppliers', - icon: 'vn:supplier', - moduleName: 'Supplier', - keyBinding: 'p', - menu: ['SupplierList'], - }, - component: RouterView, - redirect: { name: 'SupplierMain' }, children: [ { path: '', name: 'SupplierMain', component: () => import('src/components/common/VnModule.vue'), - redirect: { name: 'SupplierIndexMain' }, + redirect: { name: 'SupplierList' }, children: [ { - path: '', - name: 'SupplierIndexMain', - redirect: { name: 'SupplierList' }, + path: 'list', + name: 'SupplierList', + meta: { + title: 'list', + icon: 'view_list', + }, component: () => import('src/pages/Supplier/SupplierList.vue'), - children: [ - { - path: 'list', - name: 'SupplierList', - meta: { - title: 'list', - icon: 'view_list', - }, - }, - supplierCard, - ], }, { path: 'create', @@ -191,5 +54,143 @@ export default { }, ], }, + { + name: 'SupplierCard', + path: ':id', + component: () => import('src/pages/Supplier/Card/SupplierCard.vue'), + redirect: { name: 'SupplierSummary' }, + children: [ + { + name: 'SupplierSummary', + path: 'summary', + meta: { + title: 'summary', + icon: 'launch', + }, + component: () => + import('src/pages/Supplier/Card/SupplierSummary.vue'), + }, + { + path: 'basic-data', + name: 'SupplierBasicData', + meta: { + title: 'basicData', + icon: 'vn:settings', + }, + component: () => + import('src/pages/Supplier/Card/SupplierBasicData.vue'), + }, + { + path: 'fiscal-data', + name: 'SupplierFiscalData', + meta: { + title: 'fiscalData', + icon: 'vn:dfiscales', + }, + component: () => + import('src/pages/Supplier/Card/SupplierFiscalData.vue'), + }, + { + path: 'billing-data', + name: 'SupplierBillingData', + meta: { + title: 'billingData', + icon: 'vn:payment', + }, + component: () => + import('src/pages/Supplier/Card/SupplierBillingData.vue'), + }, + { + path: 'log', + name: 'SupplierLog', + meta: { + title: 'log', + icon: 'vn:History', + }, + component: () => import('src/pages/Supplier/Card/SupplierLog.vue'), + }, + { + path: 'account', + name: 'SupplierAccounts', + meta: { + title: 'accounts', + icon: 'vn:credit', + }, + component: () => + import('src/pages/Supplier/Card/SupplierAccounts.vue'), + }, + { + path: 'contact', + name: 'SupplierContacts', + meta: { + title: 'contacts', + icon: 'contact_phone', + }, + component: () => + import('src/pages/Supplier/Card/SupplierContacts.vue'), + }, + { + path: 'address', + name: 'SupplierAddresses', + meta: { + title: 'addresses', + icon: 'vn:delivery', + }, + component: () => + import('src/pages/Supplier/Card/SupplierAddresses.vue'), + }, + { + path: 'address/create', + name: 'SupplierAddressesCreate', + component: () => + import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'), + }, + { + path: 'balance', + name: 'SupplierBalance', + meta: { + title: 'balance', + icon: 'balance', + }, + component: () => + import('src/pages/Supplier/Card/SupplierBalance.vue'), + }, + { + path: 'consumption', + name: 'SupplierConsumption', + meta: { + title: 'consumption', + icon: 'show_chart', + }, + component: () => + import('src/pages/Supplier/Card/SupplierConsumption.vue'), + }, + { + path: 'agency-term', + name: 'SupplierAgencyTerm', + meta: { + title: 'agencyTerm', + icon: 'vn:agency-term', + }, + component: () => + import('src/pages/Supplier/Card/SupplierAgencyTerm.vue'), + }, + { + path: 'dms', + name: 'SupplierDms', + meta: { + title: 'dms', + icon: 'smb_share', + }, + component: () => import('src/pages/Supplier/Card/SupplierDms.vue'), + }, + { + path: 'agency-term/create', + name: 'SupplierAgencyTermCreate', + component: () => + import('src/pages/Supplier/Card/SupplierAgencyTermCreate.vue'), + }, + ], + }, ], }; diff --git a/src/router/modules/ticket.js b/src/router/modules/ticket.js index bfcb78787..e5b423f64 100644 --- a/src/router/modules/ticket.js +++ b/src/router/modules/ticket.js @@ -192,13 +192,7 @@ export default { icon: 'vn:ticket', moduleName: 'Ticket', keyBinding: 't', - menu: [ - 'TicketList', - 'TicketAdvance', - 'TicketWeekly', - 'TicketFuture', - 'TicketNegative', - ], + menu: ['TicketList', 'TicketAdvance', 'TicketWeekly', 'TicketFuture'], }, component: RouterView, redirect: { name: 'TicketMain' }, @@ -235,32 +229,6 @@ export default { }, component: () => import('src/pages/Ticket/TicketCreate.vue'), }, - { - path: 'negative', - redirect: { name: 'TicketNegative' }, - children: [ - { - name: 'TicketNegative', - meta: { - title: 'negative', - icon: 'exposure', - }, - component: () => - import('src/pages/Ticket/Negative/TicketLackList.vue'), - path: '', - }, - { - name: 'NegativeDetail', - path: ':id', - meta: { - title: 'summary', - icon: 'launch', - }, - component: () => - import('src/pages/Ticket/Negative/TicketLackDetail.vue'), - }, - ], - }, { path: 'weekly', name: 'TicketWeekly', diff --git a/src/router/modules/worker.js b/src/router/modules/worker.js index 3eb95a96e..1d013c596 100644 --- a/src/router/modules/worker.js +++ b/src/router/modules/worker.js @@ -201,10 +201,9 @@ const workerCard = { const departmentCard = { name: 'DepartmentCard', path: ':id', - component: () => import('src/pages/Worker/Department/Card/DepartmentCard.vue'), + component: () => import('src/pages/Department/Card/DepartmentCard.vue'), redirect: { name: 'DepartmentSummary' }, meta: { - moduleName: 'Department', menu: ['DepartmentBasicData'], }, children: [ @@ -215,8 +214,7 @@ const departmentCard = { title: 'summary', icon: 'launch', }, - component: () => - import('src/pages/Worker/Department/Card/DepartmentSummary.vue'), + component: () => import('src/pages/Department/Card/DepartmentSummary.vue'), }, { path: 'basic-data', @@ -225,8 +223,7 @@ const departmentCard = { title: 'basicData', icon: 'vn:settings', }, - component: () => - import('src/pages/Worker/Department/Card/DepartmentBasicData.vue'), + component: () => import('src/pages/Department/Card/DepartmentBasicData.vue'), }, ], }; diff --git a/src/stores/__tests__/useNavigationStore.spec.js b/src/stores/__tests__/useNavigationStore.spec.js deleted file mode 100644 index c5df6157e..000000000 --- a/src/stores/__tests__/useNavigationStore.spec.js +++ /dev/null @@ -1,153 +0,0 @@ -import { setActivePinia, createPinia } from 'pinia'; -import { describe, beforeEach, afterEach, it, expect, vi, beforeAll } from 'vitest'; -import { useNavigationStore } from '../useNavigationStore'; -import axios from 'axios'; - -let store; - -vi.mock('src/router/modules', () => [ - { name: 'Item', meta: {} }, - { name: 'Shelving', meta: {} }, - { name: 'Order', meta: {} }, -]); - -vi.mock('src/filters', () => ({ - toLowerCamel: vi.fn((name) => name.toLowerCase()), -})); - -const modulesMock = [ - { - name: 'Item', - children: null, - title: 'globals.pageTitles.undefined', - icon: undefined, - module: 'item', - isPinned: true, - }, - { - name: 'Shelving', - children: null, - title: 'globals.pageTitles.undefined', - icon: undefined, - module: 'shelving', - isPinned: false, - }, - { - name: 'Order', - children: null, - title: 'globals.pageTitles.undefined', - icon: undefined, - module: 'order', - isPinned: false, - }, -]; - -const pinnedModulesMock = [ - { - name: 'Item', - children: null, - title: 'globals.pageTitles.undefined', - icon: undefined, - module: 'item', - isPinned: true, - }, -]; - -describe('useNavigationStore', () => { - beforeEach(() => { - setActivePinia(createPinia()); - vi.spyOn(axios, 'get').mockResolvedValue({ data: true }); - store = useNavigationStore(); - store.getModules = vi.fn().mockReturnValue({ - value: modulesMock, - }); - store.getPinnedModules = vi.fn().mockReturnValue({ - value: pinnedModulesMock, - }); - }); - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should return modules with correct structure', () => { - const store = useNavigationStore(); - const modules = store.getModules(); - - expect(modules.value).toEqual(modulesMock); - }); - - it('should return pinned modules', () => { - const store = useNavigationStore(); - const pinnedModules = store.getPinnedModules(); - - expect(pinnedModules.value).toEqual(pinnedModulesMock); - }); - - it('should toggle pinned modules', () => { - const store = useNavigationStore(); - - store.togglePinned('item'); - store.togglePinned('shelving'); - expect(store.pinnedModules).toEqual(['item', 'shelving']); - - store.togglePinned('item'); - expect(store.pinnedModules).toEqual(['shelving']); - }); - - it('should fetch pinned modules', async () => { - vi.spyOn(axios, 'get').mockResolvedValue({ - data: [{ id: 1, workerFk: 9, moduleFk: 'order', position: 1 }], - }); - const store = useNavigationStore(); - await store.fetchPinned(); - - expect(store.pinnedModules).toEqual(['order']); - }); - - it('should add menu item correctly', () => { - const store = useNavigationStore(); - const module = 'customer'; - const parent = []; - const route = { - name: 'customer', - title: 'Customer', - icon: 'customer', - meta: { - keyBinding: 'ctrl+shift+c', - name: 'customer', - title: 'Customer', - icon: 'customer', - menu: 'customer', - menuChildren: [{ name: 'customer', title: 'Customer', icon: 'customer' }], - }, - }; - - const result = store.addMenuItem(module, route, parent); - const expectedItem = { - children: [ - { - icon: 'customer', - name: 'customer', - title: 'globals.pageTitles.Customer', - }, - ], - icon: 'customer', - keyBinding: 'ctrl+shift+c', - name: 'customer', - title: 'globals.pageTitles.Customer', - }; - expect(result).toEqual(expectedItem); - expect(parent.length).toBe(1); - expect(parent).toEqual([expectedItem]); - }); - - it('should not add menu item if condition is not met', () => { - const store = useNavigationStore(); - const module = 'testModule'; - const route = { meta: { hidden: true, menuchildren: {} } }; - const parent = []; - const result = store.addMenuItem(module, route, parent); - expect(result).toBeUndefined(); - expect(parent.length).toBe(0); - }); -}); diff --git a/src/stores/useArrayDataStore.js b/src/stores/useArrayDataStore.js index b3996d1e3..8d62fdb4a 100644 --- a/src/stores/useArrayDataStore.js +++ b/src/stores/useArrayDataStore.js @@ -19,7 +19,6 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => { page: 1, mapKey: 'id', keepData: false, - oneRecord: false, }; function get(key) { diff --git a/src/utils/notifyResults.js b/src/utils/notifyResults.js deleted file mode 100644 index e87ad6c6f..000000000 --- a/src/utils/notifyResults.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Notify } from 'quasar'; - -export default function (results, key) { - results.forEach((result, index) => { - if (result.status === 'fulfilled') { - const data = JSON.parse(result.value.config.data); - Notify.create({ - type: 'positive', - message: `Operación (${index + 1}) ${data[key]} completada con éxito.`, - }); - } else { - const data = JSON.parse(result.reason.config.data); - Notify.create({ - type: 'negative', - message: `Operación (${index + 1}) ${data[key]} fallida: ${result.reason.message}`, - }); - } - }); -} diff --git a/test/cypress/integration/Order/orderCatalog.spec.js b/test/cypress/integration/Order/orderCatalog.spec.js index 1770a6b56..cffc47f91 100644 --- a/test/cypress/integration/Order/orderCatalog.spec.js +++ b/test/cypress/integration/Order/orderCatalog.spec.js @@ -45,6 +45,7 @@ describe('OrderCatalog', () => { ).type('{enter}'); cy.get(':nth-child(1) > [data-cy="catalogFilterCategory"]').click(); cy.dataCy('catalogFilterValueDialogBtn').last().click(); + cy.get('[data-cy="catalogFilterValueDialogTagSelect"]').click(); cy.selectOption("[data-cy='catalogFilterValueDialogTagSelect']", 'Tallos'); cy.dataCy('catalogFilterValueDialogValueInput').find('input').focus(); cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('2'); diff --git a/test/cypress/integration/entry/entryList.spec.js b/test/cypress/integration/entry/entryList.spec.js deleted file mode 100644 index 4f99f0cb6..000000000 --- a/test/cypress/integration/entry/entryList.spec.js +++ /dev/null @@ -1,224 +0,0 @@ -describe('Entry', () => { - beforeEach(() => { - cy.viewport(1920, 1080); - cy.login('buyer'); - cy.visit(`/#/entry/list`); - }); - - it('Filter deleted entries and other fields', () => { - createEntry(); - cy.get('.q-notification__message').eq(0).should('have.text', 'Data created'); - cy.waitForElement('[data-cy="entry-buys"]'); - deleteEntry(); - cy.typeSearchbar('{enter}'); - cy.get('span[title="Date"]').click().click(); - cy.typeSearchbar('{enter}'); - cy.url().should('include', 'order'); - cy.get('td[data-row-index="0"][data-col-field="landed"]').should( - 'have.text', - '-', - ); - }); - - it('Create entry, modify travel and add buys', () => { - createEntryAndBuy(); - cy.get('a[data-cy="EntryBasicData-menu-item"]').click(); - selectTravel('two'); - cy.saveCard(); - cy.get('.q-notification__message').eq(0).should('have.text', 'Data created'); - deleteEntry(); - }); - - it('Clone entry and recalculate rates', () => { - createEntry(); - - cy.waitForElement('[data-cy="entry-buys"]'); - - cy.url().then((previousUrl) => { - cy.get('[data-cy="descriptor-more-opts"]').click(); - cy.get('div[data-cy="clone-entry"]').should('be.visible').click(); - - cy.get('.q-notification__message').eq(1).should('have.text', 'Entry cloned'); - - cy.url() - .should('not.eq', previousUrl) - .then(() => { - cy.waitForElement('[data-cy="entry-buys"]'); - - cy.get('[data-cy="descriptor-more-opts"]').click(); - cy.get('div[data-cy="recalculate-rates"]').click(); - - cy.get('.q-notification__message') - .eq(2) - .should('have.text', 'Entry prices recalculated'); - - cy.get('[data-cy="descriptor-more-opts"]').click(); - deleteEntry(); - - cy.log(previousUrl); - - cy.visit(previousUrl); - - cy.waitForElement('[data-cy="entry-buys"]'); - deleteEntry(); - }); - }); - }); - - it('Should notify when entry is lock by another user', () => { - const checkLockMessage = () => { - cy.get('[data-cy="entry-lock-confirm"]').should('be.visible'); - cy.get('[data-cy="VnConfirm_message"] > span').should( - 'contain.text', - 'This entry has been locked by buyerNick', - ); - }; - - createEntry(); - goToEntryBuys(); - cy.get('.q-notification__message') - .eq(1) - .should('have.text', 'The entry has been locked successfully'); - - cy.login('logistic'); - cy.reload(); - checkLockMessage(); - cy.get('[data-cy="VnConfirm_cancel"]').click(); - cy.url().should('include', 'summary'); - - goToEntryBuys(); - checkLockMessage(); - cy.get('[data-cy="VnConfirm_confirm"]').click(); - cy.url().should('include', 'buys'); - - deleteEntry(); - }); - - it('Edit buys and use toolbar actions', () => { - const COLORS = { - negative: 'rgb(251, 82, 82)', - positive: 'rgb(200, 228, 132)', - enabled: 'rgb(255, 255, 255)', - disable: 'rgb(168, 168, 168)', - }; - - const selectCell = (field, row = 0) => - cy.get(`td[data-col-field="${field}"][data-row-index="${row}"]`); - const selectSpan = (field, row = 0) => selectCell(field, row).find('div > span'); - const selectButton = (cySelector) => cy.get(`button[data-cy="${cySelector}"]`); - const clickAndType = (field, value, row = 0) => { - selectCell(field, row).click().type(`${value}{esc}`); - }; - const checkText = (field, expectedText, row = 0) => - selectCell(field, row).should('have.text', expectedText); - const checkColor = (field, expectedColor, row = 0) => - selectSpan(field, row).should('have.css', 'color', expectedColor); - - createEntryAndBuy(); - - selectCell('isIgnored').click().click().type('{esc}'); - checkText('isIgnored', 'close'); - - clickAndType('stickers', '1'); - checkText('stickers', '0/01'); - checkText('quantity', '1'); - checkText('amount', '50.00'); - clickAndType('packing', '2'); - checkText('packing', '12'); - checkText('weight', '12.0'); - checkText('quantity', '12'); - checkText('amount', '600.00'); - checkColor('packing', COLORS.enabled); - - selectCell('groupingMode').click().click().click(); - checkColor('packing', COLORS.disable); - checkColor('grouping', COLORS.enabled); - - selectCell('buyingValue').click().clear().type('{backspace}{backspace}1'); - checkText('amount', '12.00'); - checkColor('minPrice', COLORS.disable); - - selectCell('hasMinPrice').click().click(); - checkColor('minPrice', COLORS.enabled); - selectCell('hasMinPrice').click(); - - cy.saveCard(); - cy.get('span[data-cy="footer-stickers"]').should('have.text', '1'); - cy.get('.q-notification__message').contains('Data saved'); - - selectButton('change-quantity-sign').should('be.disabled'); - selectButton('check-buy-amount').should('be.disabled'); - cy.get('tr.cursor-pointer > .q-table--col-auto-width > .q-checkbox').click(); - selectButton('change-quantity-sign').should('be.enabled'); - selectButton('check-buy-amount').should('be.enabled'); - - selectButton('change-quantity-sign').click(); - selectButton('set-negative-quantity').click(); - checkText('quantity', '-12'); - selectButton('set-positive-quantity').click(); - checkText('quantity', '12'); - checkColor('amount', COLORS.disable); - - selectButton('check-buy-amount').click(); - selectButton('uncheck-amount').click(); - checkColor('amount', COLORS.disable); - - selectButton('check-amount').click(); - checkColor('amount', COLORS.positive); - cy.saveCard(); - - cy.get('span[data-cy="footer-amount"]').should( - 'have.css', - 'color', - COLORS.positive, - ); - - deleteEntry(); - }); - - function goToEntryBuys() { - const entryBuySelector = 'a[data-cy="EntryBuys-menu-item"]'; - cy.get(entryBuySelector).should('be.visible'); - cy.waitForElement('[data-cy="entry-buys"]'); - cy.get(entryBuySelector).click(); - } - - function deleteEntry() { - cy.get('[data-cy="descriptor-more-opts"]').click(); - cy.waitForElement('div[data-cy="delete-entry"]'); - cy.get('div[data-cy="delete-entry"]').should('be.visible').click(); - cy.url().should('include', 'list'); - } - - function createEntryAndBuy() { - createEntry(); - createBuy(); - } - - function createEntry() { - cy.get('button[data-cy="vnTableCreateBtn"]').click(); - selectTravel('one'); - cy.get('button[data-cy="FormModelPopup_save"]').click(); - cy.url().should('include', 'summary'); - cy.get('.q-notification__message').eq(0).should('have.text', 'Data created'); - } - - function selectTravel(warehouse) { - cy.get('i[data-cy="Travel_icon"]').click(); - cy.get('input[data-cy="Warehouse Out_select"]').type(warehouse); - cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click(); - cy.get('button[data-cy="save-filter-travel-form"]').click(); - cy.get('tr').eq(1).click(); - } - - function createBuy() { - cy.get('a[data-cy="EntryBuys-menu-item"]').click(); - cy.get('a[data-cy="EntryBuys-menu-item"]').click(); - cy.get('button[data-cy="vnTableCreateBtn"]').click(); - - cy.get('input[data-cy="itemFk-create-popup"]').type('1'); - cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click(); - cy.get('input[data-cy="Grouping mode_select"]').should('have.value', 'packing'); - cy.get('button[data-cy="FormModelPopup_save"]').click(); - } -}); diff --git a/test/cypress/integration/entry/stockBought.spec.js b/test/cypress/integration/entry/stockBought.spec.js index bc36156b4..078ad19cc 100644 --- a/test/cypress/integration/entry/stockBought.spec.js +++ b/test/cypress/integration/entry/stockBought.spec.js @@ -6,7 +6,6 @@ describe('EntryStockBought', () => { }); it('Should edit the reserved space', () => { cy.get('.q-field__native.q-placeholder').should('have.value', '01/01/2001'); - cy.get('[data-col-field="reserve"][data-row-index="0"]').click(); cy.get('input[name="reserve"]').type('10{enter}'); cy.get('button[title="Save"]').click(); cy.get('.q-notification__message').should('have.text', 'Data saved'); @@ -16,35 +15,25 @@ describe('EntryStockBought', () => { cy.get('input[aria-label="Reserve"]').type('1'); cy.get('input[aria-label="Date"]').eq(1).clear(); cy.get('input[aria-label="Date"]').eq(1).type('01-01'); - cy.get('input[aria-label="Buyer"]').type('buyerBossNick'); - cy.get('div[role="listbox"] > div > div[role="option"]') - .eq(0) - .should('be.visible') - .click(); - - cy.get('[data-cy="FormModelPopup_save"]').click(); + cy.get('input[aria-label="Buyer"]').type('buyerboss{downarrow}{enter}'); cy.get('.q-notification__message').should('have.text', 'Data created'); - - cy.get('[data-col-field="reserve"][data-row-index="1"]').click().clear(); - cy.get('[data-cy="searchBtn"]').eq(1).click(); - cy.get('.q-table__bottom.row.items-center.q-table__bottom--nodata') - .should('have.text', 'warningNo data available') - .type('{esc}'); - cy.get('[data-col-field="reserve"][data-row-index="1"]') - .click() - .type('{backspace}{enter}'); - cy.get('[data-cy="crudModelDefaultSaveBtn"]').should('be.enabled').click(); - cy.get('.q-notification__message').eq(1).should('have.text', 'Data saved'); }); it('Should check detail for the buyer', () => { - cy.get('[data-cy="searchBtn"]').eq(0).click(); + cy.get(':nth-child(1) > .sticky > .q-btn > .q-btn__content > .q-icon').click(); cy.get('tBody > tr').eq(1).its('length').should('eq', 1); }); - + it('Should check detail for the buyerBoss and had no content', () => { + cy.get(':nth-child(2) > .sticky > .q-btn > .q-btn__content > .q-icon').click(); + cy.get('.q-table__bottom.row.items-center.q-table__bottom--nodata').should( + 'have.text', + 'warningNo data available' + ); + }); it('Should edit travel m3 and refresh', () => { - cy.get('[data-cy="edit-travel"]').should('be.visible').click(); - cy.get('input[aria-label="m3"]').clear().type('60'); - cy.get('[data-cy="FormModelPopup_save"]').click(); + cy.get('.vn-row > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.get('input[aria-label="m3"]').clear(); + cy.get('input[aria-label="m3"]').type('60'); + cy.get('.q-mt-lg > .q-btn--standard > .q-btn__content > .block').click(); cy.get('.vn-row > div > :nth-child(2)').should('have.text', '60'); }); }); diff --git a/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js b/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js index 11ca1bb59..2016fca6d 100644 --- a/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js @@ -1,9 +1,9 @@ /// <reference types="cypress" /> describe('InvoiceInBasicData', () => { + const formInputs = '.q-form > .q-card input'; const firstFormSelect = '.q-card > .vn-row:nth-child(1) > .q-select'; + const documentBtns = '[data-cy="dms-buttons"] button'; const dialogInputs = '.q-dialog input'; - const resetBtn = '.q-btn-group--push > .q-btn--flat'; - const getDocumentBtns = (opt) => `[data-cy="dms-buttons"] > :nth-child(${opt})`; beforeEach(() => { cy.login('developer'); @@ -11,16 +11,13 @@ describe('InvoiceInBasicData', () => { }); it('should edit the provideer and supplier ref', () => { - cy.dataCy('UnDeductibleVatSelect').type('4751000000'); - cy.get('.q-menu .q-item').contains('4751000000').click(); - cy.get(resetBtn).click(); - - cy.waitForElement('#formModel').within(() => { - cy.dataCy('vnSupplierSelect').type('Bros nick'); - }) - cy.get('.q-menu .q-item').contains('Bros nick').click(); + cy.selectOption(firstFormSelect, 'Bros'); + cy.get('[title="Reset"]').click(); + cy.get(formInputs).eq(1).type('{selectall}4739'); cy.saveCard(); - cy.get(`${firstFormSelect} input`).invoke('val').should('eq', 'Bros nick'); + + cy.get(`${firstFormSelect} input`).invoke('val').should('eq', 'Plants nick'); + cy.get(formInputs).eq(1).invoke('val').should('eq', '4739'); }); it('should edit, remove and create the dms data', () => { @@ -28,18 +25,18 @@ describe('InvoiceInBasicData', () => { const secondInput = "I don't know what posting here!"; //edit - cy.get(getDocumentBtns(2)).click(); + cy.get(documentBtns).eq(1).click(); cy.get(dialogInputs).eq(0).type(`{selectall}${firtsInput}`); cy.get('textarea').type(`{selectall}${secondInput}`); cy.get('[data-cy="FormModelPopup_save"]').click(); - cy.get(getDocumentBtns(2)).click(); + cy.get(documentBtns).eq(1).click(); cy.get(dialogInputs).eq(0).invoke('val').should('eq', firtsInput); cy.get('textarea').invoke('val').should('eq', secondInput); cy.get('[data-cy="FormModelPopup_save"]').click(); cy.checkNotification('Data saved'); //remove - cy.get(getDocumentBtns(3)).click(); + cy.get(documentBtns).eq(2).click(); cy.get('[data-cy="VnConfirm_confirm"]').click(); cy.checkNotification('Data saved'); @@ -49,7 +46,7 @@ describe('InvoiceInBasicData', () => { 'test/cypress/fixtures/image.jpg', { force: true, - }, + } ); cy.get('[data-cy="FormModelPopup_save"]').click(); cy.checkNotification('Data saved'); diff --git a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js index 1e7ce1003..f8b403a45 100644 --- a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js @@ -36,7 +36,7 @@ describe('InvoiceInVat', () => { cy.get(dialogInputs).eq(0).type(randomInt); cy.get(dialogInputs).eq(1).type('This is a dummy expense'); - cy.get('[data-cy="FormModelPopup_save"]').click(); + cy.get('button[type="submit"]').click(); cy.get('.q-notification__message').should('have.text', 'Data created'); }); }); diff --git a/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js b/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js index 02b7fbb43..5f629df0b 100644 --- a/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js +++ b/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js @@ -7,7 +7,9 @@ describe('InvoiceOut negative bases', () => { }); it('should filter and download as CSV', () => { - cy.get('input[name="ticketFk"]').type('23{enter}'); + cy.get( + ':nth-child(7) > .full-width > :nth-child(1) > .column > div.q-px-xs > .q-field > .q-field__inner > .q-field__control' + ).type('23{enter}'); cy.get('#subToolbar > .q-btn').click(); cy.checkNotification('CSV downloaded successfully'); }); diff --git a/test/cypress/integration/item/ItemProposal.spec.js b/test/cypress/integration/item/ItemProposal.spec.js deleted file mode 100644 index b3ba9f676..000000000 --- a/test/cypress/integration/item/ItemProposal.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -/// <reference types="cypress" /> -describe('ItemProposal', () => { - beforeEach(() => { - const ticketId = 1; - - cy.login('developer'); - cy.visit(`/#/ticket/${ticketId}/summary`); - }); - - describe('Handle item proposal selected', () => {}); -}); diff --git a/test/cypress/integration/item/itemTag.spec.js b/test/cypress/integration/item/itemTag.spec.js index 425eaffe6..17423bc51 100644 --- a/test/cypress/integration/item/itemTag.spec.js +++ b/test/cypress/integration/item/itemTag.spec.js @@ -16,7 +16,10 @@ describe('Item tag', () => { cy.dataCy(newTag).should('be.visible').click().type('Genero{enter}'); cy.dataCy('tagGeneroValue').eq(1).should('be.visible'); cy.dataCy(saveBtn).click(); - cy.checkNotification("The tag or priority can't be repeated for an item"); + cy.get('.q-notification__message').should( + 'have.text', + "The tag or priority can't be repeated for an item", + ); }); it('should add a new tag', () => { diff --git a/test/cypress/integration/parking/parkingBasicData.spec.js b/test/cypress/integration/parking/parkingBasicData.spec.js index f64f23ec8..0d130d335 100644 --- a/test/cypress/integration/parking/parkingBasicData.spec.js +++ b/test/cypress/integration/parking/parkingBasicData.spec.js @@ -13,11 +13,11 @@ describe('ParkingBasicData', () => { cy.get(sectorOpt).click(); cy.get(codeInput).eq(0).clear(); - cy.get(codeInput).eq(0).type('900-001'); + cy.get(codeInput).eq(0).type(123); cy.saveCard(); cy.get(sectorSelect).should('have.value', 'Second sector'); - cy.get(codeInput).should('have.value', '900-001'); + cy.get(codeInput).should('have.value', 123); }); }); diff --git a/test/cypress/integration/route/agency/agencyWorkCenter.spec.js b/test/cypress/integration/route/agency/agencyWorkCenter.spec.js index 82ec6626d..e28caea7c 100644 --- a/test/cypress/integration/route/agency/agencyWorkCenter.spec.js +++ b/test/cypress/integration/route/agency/agencyWorkCenter.spec.js @@ -15,7 +15,6 @@ describe('AgencyWorkCenter', () => { // expect error when duplicate cy.get(createButton).click(); - cy.selectOption(workCenterCombobox, 'workCenterOne'); cy.get('[data-cy="FormModelPopup_save"]').click(); cy.checkNotification('This workCenter is already assigned to this agency'); cy.get('[data-cy="FormModelPopup_cancel"]').click(); diff --git a/test/cypress/integration/route/routeList.spec.js b/test/cypress/integration/route/routeList.spec.js index 976ce7352..4da43ce8e 100644 --- a/test/cypress/integration/route/routeList.spec.js +++ b/test/cypress/integration/route/routeList.spec.js @@ -4,6 +4,9 @@ describe('Route', () => { cy.login('developer'); cy.visit(`/#/route/extended-list`); }); + const getVnSelect = + '> :nth-child(1) > .column > .q-field > .q-field__inner > .q-field__control > .q-field__control-container'; + const getRowColumn = (row, column) => `:nth-child(${row}) > :nth-child(${column})`; it('Route list create route', () => { cy.addBtnClick(); @@ -14,23 +17,15 @@ describe('Route', () => { it('Route list search and edit', () => { cy.get('#searchbar input').type('{enter}'); - cy.get('[data-col-field="description"][data-row-index="0"]') - .click() - .type('routeTestOne{enter}'); + cy.get('input[name="description"]').type('routeTestOne{enter}'); cy.get('.q-table tr') .its('length') .then((rowCount) => { expect(rowCount).to.be.greaterThan(0); }); - cy.get('[data-col-field="workerFk"][data-row-index="0"]') - .click() - .type('{downArrow}{enter}'); - cy.get('[data-col-field="agencyModeFk"][data-row-index="0"]') - .click() - .type('{downArrow}{enter}'); - cy.get('[data-col-field="vehicleFk"][data-row-index="0"]') - .click() - .type('{downArrow}{enter}'); + cy.get(getRowColumn(1, 3) + getVnSelect).type('{downArrow}{enter}'); + cy.get(getRowColumn(1, 4) + getVnSelect).type('{downArrow}{enter}'); + cy.get(getRowColumn(1, 5) + getVnSelect).type('{downArrow}{enter}'); cy.get('button[title="Save"]').click(); cy.get('.q-notification__message').should('have.text', 'Data saved'); }); diff --git a/test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js b/test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js deleted file mode 100644 index 64b9ca0a0..000000000 --- a/test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js +++ /dev/null @@ -1,13 +0,0 @@ -describe('Vehicle', () => { - beforeEach(() => { - cy.viewport(1920, 1080); - cy.login('deliveryAssistant'); - cy.visit(`/#/route/vehicle/7`); - }); - - it('should delete a vehicle', () => { - cy.openActionsDescriptor(); - cy.get('[data-cy="delete"]').click(); - cy.checkNotification('Vehicle removed'); - }); -}); diff --git a/test/cypress/integration/ticket/negative/TicketLackDetail.spec.js b/test/cypress/integration/ticket/negative/TicketLackDetail.spec.js deleted file mode 100644 index 9ea1cff63..000000000 --- a/test/cypress/integration/ticket/negative/TicketLackDetail.spec.js +++ /dev/null @@ -1,147 +0,0 @@ -/// <reference types="cypress" /> -describe('Ticket Lack detail', () => { - beforeEach(() => { - cy.login('developer'); - cy.intercept('GET', /\/api\/Tickets\/itemLack\/5.*$/, { - statusCode: 200, - body: [ - { - saleFk: 33, - code: 'OK', - ticketFk: 142, - nickname: 'Malibu Point', - shipped: '2000-12-31T23:00:00.000Z', - hour: 0, - quantity: 50, - agName: 'Super-Man delivery', - alertLevel: 0, - stateName: 'OK', - stateId: 3, - itemFk: 5, - price: 1.79, - alertLevelCode: 'FREE', - zoneFk: 9, - zoneName: 'Zone superMan', - theoreticalhour: '2011-11-01T22:59:00.000Z', - isRookie: 1, - turno: 1, - peticionCompra: 1, - hasObservation: 1, - hasToIgnore: 1, - isBasket: 1, - minTimed: 0, - customerId: 1104, - customerName: 'Tony Stark', - observationTypeCode: 'administrative', - }, - ], - }).as('getItemLack'); - - cy.visit('/#/ticket/negative/5'); - cy.wait('@getItemLack'); - }); - describe('Table actions', () => { - it.skip('should display only one row in the lack list', () => { - cy.location('href').should('contain', '#/ticket/negative/5'); - - cy.get('[data-cy="changeItem"]').should('be.disabled'); - cy.get('[data-cy="changeState"]').should('be.disabled'); - cy.get('[data-cy="changeQuantity"]').should('be.disabled'); - cy.get('[data-cy="itemProposal"]').should('be.disabled'); - cy.get('[data-cy="transferLines"]').should('be.disabled'); - cy.get('tr.cursor-pointer > :nth-child(1)').click(); - cy.get('[data-cy="changeItem"]').should('be.enabled'); - cy.get('[data-cy="changeState"]').should('be.enabled'); - cy.get('[data-cy="changeQuantity"]').should('be.enabled'); - cy.get('[data-cy="itemProposal"]').should('be.enabled'); - cy.get('[data-cy="transferLines"]').should('be.enabled'); - }); - }); - describe('Item proposal', () => { - beforeEach(() => { - cy.get('tr.cursor-pointer > :nth-child(1)').click(); - - cy.intercept('GET', /\/api\/Items\/getSimilar\?.*$/, { - statusCode: 200, - body: [ - { - id: 1, - longName: 'Ranged weapon longbow 50cm', - subName: 'Stark Industries', - tag5: 'Color', - value5: 'Brown', - match5: 0, - match6: 0, - match7: 0, - match8: 1, - tag6: 'Categoria', - value6: '+1 precission', - tag7: 'Tallos', - value7: '1', - tag8: null, - value8: null, - available: 20, - calc_id: 6, - counter: 0, - minQuantity: 1, - visible: null, - price2: 1, - }, - { - id: 2, - longName: 'Ranged weapon longbow 100cm', - subName: 'Stark Industries', - tag5: 'Color', - value5: 'Brown', - match5: 0, - match6: 1, - match7: 0, - match8: 1, - tag6: 'Categoria', - value6: '+1 precission', - tag7: 'Tallos', - value7: '1', - tag8: null, - value8: null, - available: 50, - calc_id: 6, - counter: 1, - minQuantity: 5, - visible: null, - price2: 10, - }, - { - id: 3, - longName: 'Ranged weapon longbow 200cm', - subName: 'Stark Industries', - tag5: 'Color', - value5: 'Brown', - match5: 1, - match6: 1, - match7: 1, - match8: 1, - tag6: 'Categoria', - value6: '+1 precission', - tag7: 'Tallos', - value7: '1', - tag8: null, - value8: null, - available: 185, - calc_id: 6, - counter: 10, - minQuantity: 10, - visible: null, - price2: 100, - }, - ], - }).as('getItemGetSimilar'); - cy.get('[data-cy="itemProposal"]').click(); - cy.wait('@getItemGetSimilar'); - }); - describe('Replace item if', () => { - it.only('Quantity is less than available', () => { - cy.get(':nth-child(1) > .text-right > .q-btn').click(); - }); - }); - }); -}); diff --git a/test/cypress/integration/ticket/negative/TicketLackList.spec.js b/test/cypress/integration/ticket/negative/TicketLackList.spec.js deleted file mode 100644 index 01ab4f621..000000000 --- a/test/cypress/integration/ticket/negative/TicketLackList.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -/// <reference types="cypress" /> -describe('Ticket Lack list', () => { - beforeEach(() => { - cy.login('developer'); - cy.intercept('GET', /Tickets\/itemLack\?.*$/, { - statusCode: 200, - body: [ - { - itemFk: 5, - longName: 'Ranged weapon pistol 9mm', - warehouseFk: 1, - producer: null, - size: 15, - category: null, - warehouse: 'Warehouse One', - lack: -50, - inkFk: 'SLV', - timed: '2025-01-25T22:59:00.000Z', - minTimed: '23:59', - originFk: 'Holand', - }, - ], - }).as('getLack'); - - cy.visit('/#/ticket/negative'); - }); - - describe('Table actions', () => { - it('should display only one row in the lack list', () => { - cy.wait('@getLack', { timeout: 10000 }); - - cy.get('.q-virtual-scroll__content > :nth-child(1) > .sticky').click(); - cy.location('href').should('contain', '#/ticket/negative/5'); - }); - }); -}); diff --git a/test/cypress/integration/ticket/ticketList.spec.js b/test/cypress/integration/ticket/ticketList.spec.js index 593021e6e..2984a4ee4 100644 --- a/test/cypress/integration/ticket/ticketList.spec.js +++ b/test/cypress/integration/ticket/ticketList.spec.js @@ -53,29 +53,4 @@ describe('TicketList', () => { cy.checkNotification('Data created'); cy.url().should('match', /\/ticket\/\d+\/summary/); }); - - it('should show the corerct problems', () => { - cy.intercept('GET', '**/api/Tickets/filter*', (req) => { - req.headers['cache-control'] = 'no-cache'; - req.headers['pragma'] = 'no-cache'; - req.headers['expires'] = '0'; - - req.on('response', (res) => { - delete res.headers['if-none-match']; - delete res.headers['if-modified-since']; - }); - }).as('ticket'); - - cy.get('[data-cy="Warehouse_select"]').type('Warehouse Five'); - cy.get('.q-menu .q-item').contains('Warehouse Five').click(); - cy.wait('@ticket').then((interception) => { - const data = interception.response.body[1]; - expect(data.hasComponentLack).to.equal(1); - expect(data.isTooLittle).to.equal(1); - expect(data.hasItemShortage).to.equal(1); - }); - cy.get('.icon-components').should('exist'); - cy.get('.icon-unavailable').should('exist'); - cy.get('.icon-isTooLittle').should('exist'); - }); }); diff --git a/test/cypress/integration/vnComponent/VnShortcut.spec.js b/test/cypress/integration/vnComponent/VnShortcut.spec.js index e08c44635..b49b4e964 100644 --- a/test/cypress/integration/vnComponent/VnShortcut.spec.js +++ b/test/cypress/integration/vnComponent/VnShortcut.spec.js @@ -28,17 +28,6 @@ describe('VnShortcuts', () => { }); cy.url().should('include', module); - if (['monitor', 'claim'].includes(module)) { - return; - } - cy.waitForElement('.q-page').should('exist'); - cy.dataCy('vnTableCreateBtn').should('exist'); - cy.get('.q-page').trigger('keydown', { - ctrlKey: true, - altKey: true, - key: '+', - }); - cy.get('#formModel').should('exist'); }); } }); diff --git a/test/cypress/integration/wagon/wagonType/wagonTypeCreate.spec.js b/test/cypress/integration/wagon/wagonType/wagonTypeCreate.spec.js index 2cd43984a..343c1c127 100644 --- a/test/cypress/integration/wagon/wagonType/wagonTypeCreate.spec.js +++ b/test/cypress/integration/wagon/wagonType/wagonTypeCreate.spec.js @@ -9,7 +9,7 @@ describe('WagonTypeCreate', () => { it('should create a new wagon type and then delete it', () => { cy.get('.q-page-sticky > div > .q-btn').click(); cy.get('input').first().type('Example for testing'); - cy.get('[data-cy="FormModelPopup_save"]').click(); + cy.get('button[type="submit"]').click(); cy.get('[title="Remove"] > .q-btn__content > .q-icon').first().click(); }); }); diff --git a/test/cypress/integration/zone/zoneBasicData.spec.js b/test/cypress/integration/zone/zoneBasicData.spec.js index 70ded3f79..95a075fb3 100644 --- a/test/cypress/integration/zone/zoneBasicData.spec.js +++ b/test/cypress/integration/zone/zoneBasicData.spec.js @@ -1,6 +1,5 @@ describe('ZoneBasicData', () => { const priceBasicData = '[data-cy="Price_input"]'; - const saveBtn = '.q-btn-group > .q-btn--standard'; beforeEach(() => { cy.viewport(1280, 720); @@ -9,27 +8,20 @@ describe('ZoneBasicData', () => { }); it('should throw an error if the name is empty', () => { - cy.intercept('GET', /\/api\/Zones\/4./).as('zone'); - - cy.wait('@zone').then(() => { - cy.get('[data-cy="zone-basic-data-name"] input').type( - '{selectall}{backspace}', - ); - }); - - cy.get(saveBtn).click(); + cy.get('[data-cy="zone-basic-data-name"] input').type('{selectall}{backspace}'); + cy.get('.q-btn-group > .q-btn--standard').click(); cy.checkNotification("can't be blank"); }); it('should throw an error if the price is empty', () => { cy.get(priceBasicData).clear(); - cy.get(saveBtn).click(); + cy.get('.q-btn-group > .q-btn--standard').click(); cy.checkNotification('cannot be blank'); }); it("should edit the basicData's zone", () => { cy.get('.q-card > :nth-child(1)').type(' modified'); - cy.get(saveBtn).click(); + cy.get('.q-btn-group > .q-btn--standard').click(); cy.checkNotification('Data saved'); }); }); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index aa4a1219e..2c93fbf84 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -87,55 +87,36 @@ Cypress.Commands.add('getValue', (selector) => { }); // Fill Inputs -Cypress.Commands.add('selectOption', (selector, option, timeout = 2500) => { +Cypress.Commands.add('selectOption', (selector, option, timeout = 5000) => { cy.waitForElement(selector, timeout); - - cy.get(selector, { timeout }) - .should('exist') - .should('be.visible') - .click() - .then(($el) => { - cy.wrap($el.is('input') ? $el : $el.find('input')) - .invoke('attr', 'aria-controls') - .then((ariaControl) => selectItem(selector, option, ariaControl)); + cy.get(selector).click(); + cy.get(selector).invoke('data', 'url').as('dataUrl'); + cy.get(selector) + .clear() + .type(option) + .then(() => { + cy.get('.q-menu', { timeout }) + .should('be.visible') // Asegurarse de que el menú está visible + .and('exist') // Verificar que el menú existe + .then(() => { + cy.get('@dataUrl').then((url) => { + if (url) { + // Esperar a que el menú no esté visible (desaparezca) + cy.get('.q-menu').should('not.be.visible'); + // Ahora esperar a que el menú vuelva a aparecer + cy.get('.q-menu').should('be.visible').and('exist'); + } + }); + }); }); + + // Finalmente, seleccionar la opción deseada + cy.get('.q-menu:visible') // Asegurarse de que estamos dentro del menú visible + .find('.q-item') // Encontrar los elementos de las opciones + .contains(option) // Verificar que existe una opción que contenga el texto deseado + .click(); // Hacer clic en la opción }); -function selectItem(selector, option, ariaControl, hasWrite = true) { - if (!hasWrite) cy.wait(100); - - getItems(ariaControl).then((items) => { - const matchingItem = items - .toArray() - .find((item) => item.innerText.includes(option)); - if (matchingItem) return cy.wrap(matchingItem).click(); - - if (hasWrite) cy.get(selector).clear().type(option, { delay: 0 }); - return selectItem(selector, option, ariaControl, false); - }); -} - -function getItems(ariaControl, startTime = Cypress._.now(), timeout = 2500) { - // Se intenta obtener la lista de opciones del desplegable de manera recursiva - return cy - .get('#' + ariaControl, { timeout }) - .should('exist') - .find('.q-item') - .should('exist') - .then(($items) => { - if (!$items?.length || $items.first().text().trim() === '') { - if (Cypress._.now() - startTime > timeout) { - throw new Error( - `getItems: Tiempo de espera (${timeout}ms) excedido.`, - ); - } - return getItems(ariaControl, startTime, timeout); - } - - return cy.wrap($items); - }); -} - Cypress.Commands.add('countSelectOptions', (selector, option) => { cy.waitForElement(selector); cy.get(selector).click({ force: true }); diff --git a/test/cypress/support/waitUntil.js b/test/cypress/support/waitUntil.js index 359f8643f..5fb47a2d8 100644 --- a/test/cypress/support/waitUntil.js +++ b/test/cypress/support/waitUntil.js @@ -1,7 +1,7 @@ const waitUntil = (subject, checkFunction, originalOptions = {}) => { if (!(checkFunction instanceof Function)) { throw new Error( - '`checkFunction` parameter should be a function. Found: ' + checkFunction, + '`checkFunction` parameter should be a function. Found: ' + checkFunction ); } From 2655e0a8e595eafeea219d30452c95f0fbd9e1f5 Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Mon, 24 Feb 2025 22:46:27 +0100 Subject: [PATCH 22/28] fix: mana axios deacoplate --- src/pages/Ticket/Card/TicketEditMana.vue | 65 +++++++++++-------- src/pages/Ticket/Card/TicketSale.vue | 21 ------ .../Ticket/Card/TicketSaleMoreActions.vue | 5 -- 3 files changed, 37 insertions(+), 54 deletions(-) diff --git a/src/pages/Ticket/Card/TicketEditMana.vue b/src/pages/Ticket/Card/TicketEditMana.vue index c1bc2639b..ff40a6592 100644 --- a/src/pages/Ticket/Card/TicketEditMana.vue +++ b/src/pages/Ticket/Card/TicketEditMana.vue @@ -1,32 +1,26 @@ <script setup> -import { ref } from 'vue'; +import axios from 'axios'; import { useI18n } from 'vue-i18n'; +import { computed, ref } from 'vue'; +import { useRoute } from 'vue-router'; import { toCurrency } from 'src/filters'; import VnUsesMana from 'components/ui/VnUsesMana.vue'; const $props = defineProps({ - mana: { - type: Number, - default: null, - }, newPrice: { type: Number, default: 0, }, - usesMana: { - type: Boolean, - default: false, - }, - manaCode: { - type: String, - default: 'mana', - }, sale: { type: Object, default: null, }, }); +const route = useRoute(); +const mana = ref(null); +const usesMana = ref(false); + const emit = defineEmits(['save', 'cancel']); const { t } = useI18n(); @@ -38,32 +32,47 @@ const save = (sale = $props.sale) => { QPopupProxyRef.value.hide(); }; +const getMana = async () => { + const { data } = await axios.get(`Tickets/${route.params.id}/getSalesPersonMana`); + mana.value = data; + await getUsesMana(); +}; + +const getUsesMana = async () => { + const { data } = await axios.get('Sales/usesMana'); + usesMana.value = data; +}; + const cancel = () => { emit('cancel'); QPopupProxyRef.value.hide(); }; +const hasMana = computed(() => typeof mana.value === 'number'); defineExpose({ save }); </script> <template> - <QPopupProxy ref="QPopupProxyRef" data-cy="ticketEditManaProxy"> + <QPopupProxy + ref="QPopupProxyRef" + @before-show="getMana" + data-cy="ticketEditManaProxy" + > <div class="container"> - <QSpinner v-if="typeof mana === 'number' && mana" color="primary" size="md" /> - <div v-else> - <div class="header">Mana: {{ toCurrency(mana) }}</div> - <div class="q-pa-md"> - <slot :popup="QPopupProxyRef" /> - <div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm"> - <VnUsesMana :mana-code="manaCode" /> - </div> - <div v-if="newPrice" class="column items-center q-mt-lg"> - <span class="text-primary">{{ t('New price') }}</span> - <span class="text-subtitle1"> - {{ toCurrency($props.newPrice) }} - </span> - </div> + <div class="header">Mana: {{ toCurrency(mana) }}</div> + <QSpinner v-if="!hasMana" color="primary" size="md" /> + <div class="q-pa-md" v-else> + <slot :popup="QPopupProxyRef" /> + <div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm"> + <VnUsesMana :mana-code="manaCode" /> + </div> + <div v-if="newPrice" class="column items-center q-mt-lg"> + <span class="text-primary">{{ t('New price') }}</span> + <span class="text-subtitle1"> + {{ toCurrency($props.newPrice) }} + </span> </div> </div> + <div class="row"> <QBtn color="primary" diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index f5fb50ecf..076e06dea 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -44,7 +44,6 @@ const isTicketEditable = ref(false); const sales = ref([]); const editableStatesOptions = ref([]); const selectedSales = ref([]); -const mana = ref(null); const manaCode = ref('mana'); const ticketState = computed(() => store.data?.ticketState?.state?.code); const transfer = ref({ @@ -258,18 +257,6 @@ const DEFAULT_EDIT = { oldQuantity: null, }; const edit = ref({ ...DEFAULT_EDIT }); -const usesMana = ref(null); - -const getUsesMana = async () => { - const { data } = await axios.get('Sales/usesMana'); - usesMana.value = data; -}; - -const getMana = async () => { - const { data } = await axios.get(`Tickets/${route.params.id}/getSalesPersonMana`); - mana.value = data; - await getUsesMana(); -}; const selectedValidSales = computed(() => { if (!sales.value) return; @@ -277,7 +264,6 @@ const selectedValidSales = computed(() => { }); const onOpenEditPricePopover = async (sale) => { - await getMana(); edit.value = { sale: JSON.parse(JSON.stringify(sale)), price: sale.price, @@ -285,7 +271,6 @@ const onOpenEditPricePopover = async (sale) => { }; const onOpenEditDiscountPopover = async (sale) => { - await getMana(); if (isLocked.value) return; if (sale) { edit.value = { @@ -306,7 +291,6 @@ const changePrice = async (sale) => { await confirmUpdate(() => updatePrice(sale, newPrice)); } else updatePrice(sale, newPrice); } - await getMana(); }; const updatePrice = async (sale, newPrice) => { await axios.post(`Sales/${sale.id}/updatePrice`, { newPrice }); @@ -599,9 +583,7 @@ watch( :is-ticket-editable="isTicketEditable" :sales="selectedValidSales" :disable="!hasSelectedRows" - :mana="mana" :ticket-config="ticketConfig" - @get-mana="getMana()" @update-discounts="updateDiscounts" @refresh-table="resetChanges" /> @@ -829,7 +811,6 @@ watch( </QBtn> <TicketEditManaProxy ref="editPriceProxyRef" - :mana="mana" :sale="row" :new-price="getNewPrice" @save="changePrice" @@ -852,10 +833,8 @@ watch( <TicketEditManaProxy ref="editManaProxyRef" - :mana="mana" :sale="row" :new-price="getNewPrice" - :uses-mana="usesMana" :mana-code="manaCode" @save="changeDiscount" > diff --git a/src/pages/Ticket/Card/TicketSaleMoreActions.vue b/src/pages/Ticket/Card/TicketSaleMoreActions.vue index 8b5537edc..840b62507 100644 --- a/src/pages/Ticket/Card/TicketSaleMoreActions.vue +++ b/src/pages/Ticket/Card/TicketSaleMoreActions.vue @@ -34,10 +34,6 @@ const props = defineProps({ type: Array, default: () => [], }, - mana: { - type: Number, - default: null, - }, ticketConfig: { type: Array, default: () => [], @@ -220,7 +216,6 @@ const createRefund = async (withWarehouse) => { <TicketEditManaProxy ref="editManaProxyRef" :sale="row" - :mana="props.mana" @save="changeMultipleDiscount" > <VnInput From e44d6a291515c7e045397836307718d62eb04d7c Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Mon, 24 Feb 2025 22:48:48 +0100 Subject: [PATCH 23/28] test: remove test --- .../Card/__tests__/TicketEditMana.spec.js | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 src/pages/Ticket/Card/__tests__/TicketEditMana.spec.js diff --git a/src/pages/Ticket/Card/__tests__/TicketEditMana.spec.js b/src/pages/Ticket/Card/__tests__/TicketEditMana.spec.js deleted file mode 100644 index f685d4ef0..000000000 --- a/src/pages/Ticket/Card/__tests__/TicketEditMana.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; - -import { createI18n } from 'vue-i18n'; -import TicketEditMana from 'src/pages/Ticket/Card/TicketEditMana.vue'; -import { createWrapper } from 'app/test/vitest/helper'; - -describe('TicketEditMana', () => { - let vm; - let wrapper; - function generateWrapper(props = {}) { - wrapper = createWrapper(TicketEditMana, { - props, - }); - wrapper = wrapper.wrapper; - vm = wrapper.vm; - } - afterEach(() => { - vi.clearAllMocks(); - }); - - describe('mana prop tests', async () => { - it('should show spinner when mana is null', async () => { - generateWrapper({ mana: null }); - await vm.$nextTick(); - expect(vm.hasMana).toBe(false); - }); - - it('should show spinner when mana is undefined', async () => { - generateWrapper({ mana: undefined }); - expect(typeof undefined === 'number').toBe(false); - await vm.$nextTick(); - expect(vm.hasMana).toBe(false); - }); - - it('should display negative mana value', async () => { - generateWrapper({ mana: -1000 }); - expect(typeof -1000 === 'number').toBe(true); - await vm.$nextTick(); - expect(vm.hasMana).toBe(true); - }); - - it('should display zero mana value', async () => { - generateWrapper({ mana: 0 }); - expect(typeof 0 === 'number').toBe(true); - await vm.$nextTick(); - expect(vm.hasMana).toBe(true); - }); - - it('should display positive mana value', async () => { - generateWrapper({ mana: 1000 }); - expect(typeof 1000 === 'number').toBe(true); - await vm.$nextTick(); - expect(vm.hasMana).toBe(true); - }); - }); -}); From 56f8657bbad6bda28b591b95c027a0330bbe0672 Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Mon, 24 Feb 2025 22:58:57 +0100 Subject: [PATCH 24/28] fix: maxium calls exceed --- .../Customer/components/CustomerNewPayment.vue | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pages/Customer/components/CustomerNewPayment.vue b/src/pages/Customer/components/CustomerNewPayment.vue index c2c38b55a..25e403991 100644 --- a/src/pages/Customer/components/CustomerNewPayment.vue +++ b/src/pages/Customer/components/CustomerNewPayment.vue @@ -77,24 +77,23 @@ onBeforeMount(() => { function setPaymentType(accounting) { if (!accounting) return; accountingType.value = accounting.accountingType; - initialData.description = []; initialData.payed = Date.vnNew(); isCash.value = accountingType.value.code == 'cash'; viewReceipt.value = isCash.value; if (accountingType.value.daysInFuture) initialData.payed.setDate( - initialData.payed.getDate() + accountingType.value.daysInFuture + initialData.payed.getDate() + accountingType.value.daysInFuture, ); maxAmount.value = accountingType.value && accountingType.value.maxAmount; - if (accountingType.value.code == 'compensation') return (initialData.description = ''); - if (accountingType.value.receiptDescription) - initialData.description.push(accountingType.value.receiptDescription); - if (initialData.description) initialData.description.push(initialData.description); - initialData.description = initialData.description.join(', '); + let descriptions = []; + if (accountingType.value.receiptDescription) + descriptions.push(accountingType.value.receiptDescription); + if (initialData.description) descriptions.push(initialData.description); + initialData.description = descriptions.join(', '); } const calculateFromAmount = (event) => { From f2eedce55f518d3b23b36d09da1044c4d2347550 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Tue, 25 Feb 2025 08:01:10 +0100 Subject: [PATCH 25/28] fix: merge test to dev --- src/pages/Zone/ZoneList.vue | 52 +++++++------------------------------ 1 file changed, 10 insertions(+), 42 deletions(-) diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue index 6fe3649ed..4df84e4bd 100644 --- a/src/pages/Zone/ZoneList.vue +++ b/src/pages/Zone/ZoneList.vue @@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'; import { computed, ref } from 'vue'; import axios from 'axios'; -import { toCurrency } from 'src/filters'; +import { dashIfEmpty, toCurrency } from 'src/filters'; import { toTimeFormat } from 'src/filters/date'; import { useVnConfirm } from 'composables/useVnConfirm'; import useNotify from 'src/composables/useNotify.js'; @@ -17,7 +17,6 @@ import VnInputTime from 'src/components/common/VnInputTime.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import ZoneFilterPanel from './ZoneFilterPanel.vue'; import ZoneSearchbar from './Card/ZoneSearchbar.vue'; -import FetchData from 'src/components/FetchData.vue'; const { t } = useI18n(); const router = useRouter(); @@ -26,7 +25,6 @@ const { viewSummary } = useSummaryDialog(); const { openConfirmationModal } = useVnConfirm(); const tableRef = ref(); const warehouseOptions = ref([]); -const validAddresses = ref([]); const tableFilter = { include: [ @@ -67,6 +65,7 @@ const tableFilter = { const columns = computed(() => [ { + align: 'left', name: 'id', label: t('list.id'), chip: { @@ -76,8 +75,6 @@ const columns = computed(() => [ columnFilter: { inWhere: true, }, - columnClass: 'shrink-column', - component: 'number', }, { align: 'left', @@ -109,6 +106,7 @@ const columns = computed(() => [ format: (row, dashIfEmpty) => dashIfEmpty(row?.agencyMode?.name), }, { + align: 'left', name: 'price', label: t('list.price'), cardVisible: true, @@ -116,11 +114,9 @@ const columns = computed(() => [ columnFilter: { inWhere: true, }, - columnClass: 'shrink-column', - component: 'number', }, { - align: 'center', + align: 'left', name: 'hour', label: t('list.close'), cardVisible: true, @@ -133,6 +129,7 @@ const columns = computed(() => [ label: t('list.addressFk'), cardVisible: true, columnFilter: false, + columnClass: 'expand', }, { align: 'right', @@ -167,26 +164,14 @@ const handleClone = (id) => { ); }; -function showValidAddresses(row) { - if (row.addressFk) { - const isValid = validAddresses.value.some( - (address) => address.addressFk === row.addressFk, - ); - if (isValid) - return `${row.address?.nickname}, - ${row.address?.postcode?.town?.name} (${row.address?.province?.name})`; - else return '-'; - } - return '-'; +function formatRow(row) { + if (!row?.address) return '-'; + return dashIfEmpty(`${row?.address?.nickname}, + ${row?.address?.postcode?.town?.name} (${row?.address?.province?.name})`); } </script> <template> - <FetchData - url="RoadmapAddresses" - auto-load - @on-fetch="(data) => (validAddresses = data)" - /> <ZoneSearchbar /> <RightMenu> <template #right-panel> @@ -209,7 +194,7 @@ function showValidAddresses(row) { :right-search="false" > <template #column-addressFk="{ row }"> - {{ showValidAddresses(row) }} + {{ dashIfEmpty(formatRow(row)) }} </template> <template #more-create-dialog="{ data }"> <VnSelect @@ -261,20 +246,3 @@ es: Search zone: Buscar zona You can search zones by id or name: Puedes buscar zonas por id o nombre </i18n> - -<style lang="scss" scoped> -.table-container { - display: flex; - justify-content: center; -} -.column { - display: flex; - flex-direction: column; - align-items: center; - min-width: 70%; -} - -:deep(.shrink-column) { - width: 8%; -} -</style> From b9b5bd4c8ad4adde3079816cdd0d5c9be071a0d7 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Tue, 25 Feb 2025 09:40:46 +0100 Subject: [PATCH 26/28] Revert "revert 1015acefb7e400be2d8b5958dba69b4d98276b34" This reverts commit 223a1ea4490ea6ad2a00c60297fd3c74cd713338. --- cypress.config.js | 4 +- package.json | 144 +- quasar.config.js | 1 - src/boot/defaults/constants.js | 2 + src/boot/keyShortcut.js | 17 +- src/boot/qformMixin.js | 23 +- src/boot/quasar.js | 1 + src/components/CreateBankEntityForm.vue | 2 +- src/components/CrudModel.vue | 16 +- src/components/FilterTravelForm.vue | 4 +- src/components/FormModel.vue | 46 +- src/components/FormModelPopup.vue | 50 +- src/components/ItemsFilterPanel.vue | 4 +- src/components/LeftMenu.vue | 67 +- src/components/LeftMenuItem.vue | 1 + src/components/RefundInvoiceForm.vue | 15 +- src/components/TicketProblems.vue | 82 +- src/components/TransferInvoiceForm.vue | 15 +- src/components/VnTable/VnColumn.vue | 51 +- src/components/VnTable/VnFilter.vue | 58 +- src/components/VnTable/VnOrder.vue | 101 +- src/components/VnTable/VnTable.vue | 573 ++++++-- src/components/VnTable/VnTableFilter.vue | 57 +- src/components/VnTable/VnVisibleColumn.vue | 19 +- src/components/__tests__/FormModel.spec.js | 12 +- src/components/__tests__/Leftmenu.spec.js | 376 ++++- src/components/__tests__/UserPanel.spec.js | 100 +- src/components/common/VnCard.vue | 39 +- src/components/common/VnCardBeta.vue | 61 +- src/components/common/VnCheckbox.vue | 43 + src/components/common/VnColor.vue | 32 + src/components/common/VnComponent.vue | 6 +- src/components/common/VnDmsList.vue | 12 +- src/components/common/VnInput.vue | 22 +- src/components/common/VnInputDate.vue | 8 +- src/components/common/VnInputNumber.vue | 2 + src/components/common/VnPopupProxy.vue | 38 + src/components/common/VnSection.vue | 9 +- src/components/common/VnSelect.vue | 22 +- src/components/common/VnSelectCache.vue | 4 +- src/components/common/VnSelectDialog.vue | 2 - src/components/common/VnSelectSupplier.vue | 6 +- .../common/VnSelectTravelExtended.vue | 50 + .../common/__tests__/VnNotes.spec.js | 151 +- src/components/ui/CardDescriptor.vue | 52 +- src/components/ui/CardSummary.vue | 14 +- src/components/ui/SkeletonDescriptor.vue | 65 +- src/components/ui/VnConfirm.vue | 3 +- src/components/ui/VnFilterPanel.vue | 16 +- src/components/ui/VnMoreOptions.vue | 2 +- src/components/ui/VnNotes.vue | 94 +- src/components/ui/VnStockValueDisplay.vue | 41 + src/components/ui/VnSubToolbar.vue | 11 +- .../ui/__tests__/CardSummary.spec.js | 14 +- .../__tests__/useArrayData.spec.js | 29 +- src/composables/checkEntryLock.js | 65 + src/composables/getColAlign.js | 22 + src/composables/useArrayData.js | 13 +- src/composables/useRole.js | 10 + src/css/app.scss | 28 +- src/css/quasar.variables.scss | 6 +- src/filters/toDate.js | 11 +- src/i18n/locale/en.yml | 117 ++ src/i18n/locale/es.yml | 225 ++- src/layouts/MainLayout.vue | 2 +- src/layouts/OutLayout.vue | 5 +- src/pages/Account/AccountAliasList.vue | 10 +- src/pages/Account/AccountExprBuilder.js | 18 + src/pages/Account/AccountList.vue | 26 +- src/pages/Account/Alias/AliasExprBuilder.js | 8 + src/pages/Account/Alias/Card/AliasCard.vue | 10 +- .../Account/Alias/Card/AliasDescriptor.vue | 11 +- src/pages/Account/Alias/Card/AliasSummary.vue | 19 +- src/pages/Account/Card/AccountBasicData.vue | 38 +- src/pages/Account/Card/AccountCard.vue | 10 +- src/pages/Account/Card/AccountDescriptor.vue | 43 +- .../Account/Card/AccountDescriptorMenu.vue | 27 +- src/pages/Account/Card/AccountFilter.js | 3 + src/pages/Account/Card/AccountMailAlias.vue | 7 +- src/pages/Account/Card/AccountSummary.vue | 41 +- src/pages/Account/Role/AccountRoles.vue | 18 +- src/pages/Account/Role/Card/RoleBasicData.vue | 14 +- src/pages/Account/Role/Card/RoleCard.vue | 7 +- .../Account/Role/Card/RoleDescriptor.vue | 16 +- src/pages/Account/Role/Card/RoleSummary.vue | 23 +- src/pages/Account/Role/Card/SubRoles.vue | 6 +- src/pages/Account/Role/RoleExprBuilder.js | 16 + src/pages/Claim/Card/ClaimBasicData.vue | 1 - src/pages/Claim/Card/ClaimCard.vue | 9 +- src/pages/Claim/Card/ClaimDescriptor.vue | 17 +- src/pages/Claim/Card/ClaimLines.vue | 8 +- src/pages/Claim/Card/ClaimNotes.vue | 3 +- src/pages/Claim/Card/ClaimPhoto.vue | 4 +- src/pages/Claim/ClaimList.vue | 2 +- src/pages/Customer/Card/CustomerAddress.vue | 8 +- src/pages/Customer/Card/CustomerBalance.vue | 4 +- src/pages/Customer/Card/CustomerBasicData.vue | 4 +- .../Customer/Card/CustomerBillingData.vue | 2 +- src/pages/Customer/Card/CustomerCard.vue | 4 +- .../Customer/Card/CustomerConsumption.vue | 95 +- src/pages/Customer/Card/CustomerContacts.vue | 2 +- .../Customer/Card/CustomerCreditContracts.vue | 2 +- .../Customer/Card/CustomerDescriptor.vue | 42 +- .../Customer/Card/CustomerDescriptorMenu.vue | 17 + .../Customer/Card/CustomerFileManagement.vue | 2 +- .../Customer/Card/CustomerFiscalData.vue | 32 +- src/pages/Customer/Card/CustomerNotes.vue | 1 + src/pages/Customer/Card/CustomerSamples.vue | 2 +- src/pages/Customer/Card/CustomerWebAccess.vue | 2 +- src/pages/Customer/CustomerFilter.vue | 6 +- src/pages/Customer/CustomerList.vue | 4 +- .../Customer/Defaulter/CustomerDefaulter.vue | 2 +- .../components/CustomerAddressEdit.vue | 4 +- .../components/CustomerNewPayment.vue | 6 +- .../components/CustomerSamplesCreate.vue | 9 +- src/pages/Customer/locale/en.yml | 3 + src/pages/Customer/locale/es.yml | 3 + src/pages/Entry/Card/EntryBasicData.vue | 63 +- src/pages/Entry/Card/EntryBuys.vue | 1232 +++++++++++------ src/pages/Entry/Card/EntryCard.vue | 6 +- src/pages/Entry/Card/EntryDescriptor.vue | 158 ++- src/pages/Entry/Card/EntryFilter.js | 17 +- src/pages/Entry/Card/EntryNotes.vue | 4 +- src/pages/Entry/Card/EntrySummary.vue | 388 ++---- src/pages/Entry/EntryFilter.vue | 257 ++-- src/pages/Entry/EntryList.vue | 368 +++-- src/pages/Entry/EntryStockBought.vue | 18 +- src/pages/Entry/EntryStockBoughtDetail.vue | 22 +- src/pages/Entry/locale/en.yml | 84 +- src/pages/Entry/locale/es.yml | 107 +- .../InvoiceIn/Card/InvoiceInBasicData.vue | 6 +- src/pages/InvoiceIn/Card/InvoiceInCard.vue | 41 +- .../InvoiceIn/Card/InvoiceInDescriptor.vue | 33 +- .../Card/InvoiceInDescriptorMenu.vue | 4 +- src/pages/InvoiceIn/Card/InvoiceInDueDay.vue | 26 +- src/pages/InvoiceIn/Card/InvoiceInFilter.js | 33 + .../InvoiceIn/Card/InvoiceInIntrastat.vue | 2 +- src/pages/InvoiceIn/Card/InvoiceInSummary.vue | 13 +- src/pages/InvoiceIn/Card/InvoiceInVat.vue | 78 +- src/pages/InvoiceIn/InvoiceInList.vue | 5 +- src/pages/InvoiceIn/InvoiceInToBook.vue | 56 +- src/pages/InvoiceIn/locale/en.yml | 5 +- src/pages/InvoiceIn/locale/es.yml | 9 +- src/pages/InvoiceOut/Card/InvoiceOutCard.vue | 4 +- .../InvoiceOut/Card/InvoiceOutDescriptor.vue | 28 +- src/pages/InvoiceOut/Card/InvoiceOutFilter.js | 16 + src/pages/Item/Card/ItemBarcode.vue | 2 +- src/pages/Item/Card/ItemBasicData.vue | 42 +- src/pages/Item/Card/ItemBotanical.vue | 4 +- src/pages/Item/Card/ItemCard.vue | 2 +- src/pages/Item/Card/ItemDescriptor.vue | 26 +- src/pages/Item/Card/ItemDescriptorProxy.vue | 6 +- src/pages/Item/Card/ItemShelving.vue | 10 +- src/pages/Item/Card/ItemTags.vue | 2 +- src/pages/Item/ItemFixedPrice.vue | 16 +- .../Item/ItemType/Card/ItemTypeBasicData.vue | 7 +- src/pages/Item/ItemType/Card/ItemTypeCard.vue | 6 +- .../Item/ItemType/Card/ItemTypeDescriptor.vue | 40 +- .../Item/ItemType/Card/ItemTypeFilter.js | 8 + .../Item/ItemType/Card/ItemTypeSummary.vue | 15 +- .../{Card => components}/CreateGenusForm.vue | 0 .../{Card => components}/CreateSpecieForm.vue | 0 src/pages/Item/components/ItemProposal.vue | 332 +++++ .../Item/components/ItemProposalProxy.vue | 56 + src/pages/Item/locale/en.yml | 24 +- src/pages/Item/locale/es.yml | 31 +- src/pages/Monitor/MonitorOrders.vue | 2 +- src/pages/Monitor/locale/en.yml | 1 + src/pages/Monitor/locale/es.yml | 1 + .../Order/Card/CatalogFilterValueDialog.vue | 2 +- src/pages/Order/Card/OrderBasicData.vue | 6 +- src/pages/Order/Card/OrderCard.vue | 4 +- src/pages/Order/Card/OrderCatalogFilter.vue | 4 +- .../Order/Card/OrderCatalogItemDialog.vue | 8 +- src/pages/Order/Card/OrderDescriptor.vue | 38 +- src/pages/Order/Card/OrderFilter.js | 26 + src/pages/Order/Card/OrderLines.vue | 4 +- src/pages/Order/Card/OrderSummary.vue | 2 +- src/pages/Order/OrderList.vue | 7 +- src/pages/Route/Agency/AgencyList.vue | 4 +- .../Route/Agency/Card/AgencyBasicData.vue | 2 +- src/pages/Route/Agency/Card/AgencyCard.vue | 2 +- .../Route/Agency/Card/AgencyDescriptor.vue | 1 - .../Route/Agency/Card/AgencyWorkcenter.vue | 2 +- src/pages/Route/Card/RouteCard.vue | 5 +- src/pages/Route/Card/RouteDescriptor.vue | 70 +- src/pages/Route/Card/RouteFilter.js | 39 + src/pages/Route/Card/RouteFilter.vue | 2 +- src/pages/Route/Card/RouteForm.vue | 54 +- src/pages/Route/Roadmap/RoadmapBasicData.vue | 5 +- src/pages/Route/Roadmap/RoadmapCard.vue | 2 +- src/pages/Route/Roadmap/RoadmapDescriptor.vue | 18 +- src/pages/Route/Roadmap/RoadmapFilter.js | 3 + src/pages/Route/Roadmap/RoadmapStops.vue | 2 +- src/pages/Route/Roadmap/RoadmapSummary.vue | 3 +- src/pages/Route/RouteExtendedList.vue | 152 +- src/pages/Route/RouteList.vue | 31 + src/pages/Route/RouteTickets.vue | 18 +- .../Route/Vehicle/Card/VehicleBasicData.vue | 162 +++ src/pages/Route/Vehicle/Card/VehicleCard.vue | 13 + .../Route/Vehicle/Card/VehicleDescriptor.vue | 49 + .../Route/Vehicle/Card/VehicleSummary.vue | 127 ++ src/pages/Route/Vehicle/VehicleFilter.js | 76 + src/pages/Route/Vehicle/VehicleList.vue | 224 +++ src/pages/Route/Vehicle/locale/en.yml | 20 + src/pages/Route/Vehicle/locale/es.yml | 20 + src/pages/Shelving/Card/ShelvingCard.vue | 4 +- .../Shelving/Card/ShelvingDescriptor.vue | 30 +- src/pages/Shelving/Card/ShelvingFilter.js | 15 + src/pages/Shelving/Card/ShelvingForm.vue | 32 +- src/pages/Shelving/Card/ShelvingSearchbar.vue | 8 +- src/pages/Shelving/Card/ShelvingSummary.vue | 37 +- .../Parking/Card/ParkingBasicData.vue | 18 +- .../Parking/Card/ParkingCard.vue | 6 +- .../Parking/Card/ParkingDescriptor.vue | 16 +- .../Shelving/Parking/Card/ParkingFilter.js | 4 + .../Parking/Card/ParkingLog.vue | 0 .../Parking/Card/ParkingSummary.vue | 0 .../Shelving/Parking/ParkingExprBuilder.js | 10 + .../{ => Shelving}/Parking/ParkingFilter.vue | 0 .../{ => Shelving}/Parking/ParkingList.vue | 13 +- .../{ => Shelving}/Parking/locale/en.yml | 0 .../{ => Shelving}/Parking/locale/es.yml | 0 src/pages/Shelving/ShelvingExprBuilder.js | 10 + src/pages/Shelving/ShelvingList.vue | 26 +- src/pages/Supplier/Card/SupplierAccounts.vue | 6 +- src/pages/Supplier/Card/SupplierAddresses.vue | 2 +- .../Supplier/Card/SupplierAgencyTerm.vue | 2 +- src/pages/Supplier/Card/SupplierBasicData.vue | 3 +- src/pages/Supplier/Card/SupplierCard.vue | 16 +- .../Supplier/Card/SupplierConsumption.vue | 103 +- src/pages/Supplier/Card/SupplierContacts.vue | 2 +- .../Supplier/Card/SupplierDescriptor.vue | 49 +- src/pages/Supplier/Card/SupplierFilter.js | 35 + .../Supplier/Card/SupplierFiscalData.vue | 22 +- src/pages/Supplier/SupplierList.vue | 91 +- src/pages/Supplier/SupplierListFilter.vue | 122 -- .../Ticket/Card/BasicData/TicketBasicData.vue | 16 +- .../Card/BasicData/TicketBasicDataForm.vue | 4 +- .../Card/BasicData/TicketBasicDataView.vue | 116 +- src/pages/Ticket/Card/TicketCard.vue | 8 +- src/pages/Ticket/Card/TicketComponents.vue | 2 +- src/pages/Ticket/Card/TicketDescriptor.vue | 139 +- src/pages/Ticket/Card/TicketExpedition.vue | 2 +- src/pages/Ticket/Card/TicketFilter.js | 72 + src/pages/Ticket/Card/TicketNotes.vue | 4 +- src/pages/Ticket/Card/TicketPackage.vue | 4 +- src/pages/Ticket/Card/TicketSale.vue | 60 +- src/pages/Ticket/Card/TicketService.vue | 6 +- src/pages/Ticket/Card/TicketSplit.vue | 37 + src/pages/Ticket/Card/TicketSummary.vue | 81 +- src/pages/Ticket/Card/TicketTracking.vue | 4 +- src/pages/Ticket/Card/TicketTransfer.vue | 131 +- src/pages/Ticket/Card/TicketTransferProxy.vue | 54 + src/pages/Ticket/Card/components/split.js | 22 + .../Ticket/Negative/TicketLackDetail.vue | 198 +++ .../Ticket/Negative/TicketLackFilter.vue | 175 +++ src/pages/Ticket/Negative/TicketLackList.vue | 227 +++ src/pages/Ticket/Negative/TicketLackTable.vue | 356 +++++ .../Negative/components/ChangeItemDialog.vue | 90 ++ .../components/ChangeQuantityDialog.vue | 84 ++ .../Negative/components/ChangeStateDialog.vue | 91 ++ src/pages/Ticket/TicketFuture.vue | 555 +++----- src/pages/Ticket/TicketFutureFilter.vue | 4 +- src/pages/Ticket/locale/en.yml | 87 +- src/pages/Ticket/locale/es.yml | 83 ++ src/pages/Travel/Card/TravelBasicData.vue | 19 +- src/pages/Travel/Card/TravelCard.vue | 36 +- src/pages/Travel/Card/TravelDescriptor.vue | 1 - src/pages/Travel/Card/TravelFilter.js | 1 + src/pages/Travel/Card/TravelSummary.vue | 8 + src/pages/Travel/Card/TravelThermographs.vue | 2 +- src/pages/Travel/ExtraCommunityFilter.vue | 2 +- src/pages/Travel/TravelList.vue | 24 + src/pages/Wagon/Card/WagonCard.vue | 2 +- src/pages/Wagon/Type/WagonTypeList.vue | 8 +- src/pages/Worker/Card/WorkerBasicData.vue | 17 +- src/pages/Worker/Card/WorkerCalendar.vue | 32 +- .../Worker/Card/WorkerCalendarFilter.vue | 2 - src/pages/Worker/Card/WorkerCard.vue | 7 +- src/pages/Worker/Card/WorkerDescriptor.vue | 9 +- .../Worker/Card/WorkerDescriptorProxy.vue | 7 +- src/pages/Worker/Card/WorkerFormation.vue | 3 +- src/pages/Worker/Card/WorkerMedical.vue | 16 + src/pages/Worker/Card/WorkerOperator.vue | 19 +- src/pages/Worker/Card/WorkerPda.vue | 10 +- src/pages/Worker/Card/WorkerPit.vue | 2 +- src/pages/Worker/Card/WorkerSummary.vue | 2 +- src/pages/Worker/Card/WorkerTimeControl.vue | 16 +- .../Department/Card/DepartmentBasicData.vue | 35 +- .../Department/Card/DepartmentCard.vue | 4 +- .../Department/Card/DepartmentDescriptor.vue | 23 +- .../Card/DepartmentDescriptorProxy.vue | 0 .../Department/Card/DepartmentSummary.vue | 2 +- .../Card/DepartmentSummaryDialog.vue | 0 src/pages/Worker/WorkerDepartmentTree.vue | 4 +- src/pages/Zone/Card/ZoneBasicData.vue | 33 +- src/pages/Zone/Card/ZoneCard.vue | 12 +- src/pages/Zone/Card/ZoneDescriptor.vue | 44 +- src/pages/Zone/Card/ZoneEvents.vue | 4 +- src/pages/Zone/Card/ZoneFilter.js | 10 + src/pages/Zone/Card/ZoneSearchbar.vue | 41 +- src/pages/Zone/Card/ZoneSummary.vue | 18 +- src/pages/Zone/Card/ZoneWarehouses.vue | 2 +- src/pages/Zone/Delivery/ZoneDeliveryList.vue | 2 +- src/pages/Zone/Upcoming/ZoneUpcomingList.vue | 2 +- src/pages/Zone/ZoneList.vue | 29 +- src/router/modules/account/aliasCard.js | 2 +- src/router/modules/account/roleCard.js | 1 + src/router/modules/entry.js | 17 +- src/router/modules/route.js | 52 + src/router/modules/shelving.js | 11 +- src/router/modules/supplier.js | 315 +++-- src/router/modules/ticket.js | 34 +- src/router/modules/worker.js | 9 +- .../__tests__/useNavigationStore.spec.js | 153 ++ src/stores/useArrayDataStore.js | 1 + src/utils/notifyResults.js | 19 + .../integration/Order/orderCatalog.spec.js | 1 - .../integration/entry/entryList.spec.js | 224 +++ .../integration/entry/stockBought.spec.js | 37 +- .../invoiceIn/invoiceInBasicData.spec.js | 27 +- .../invoiceIn/invoiceInVat.spec.js | 2 +- .../invoiceOutNegativeBases.spec.js | 4 +- .../integration/item/ItemProposal.spec.js | 11 + test/cypress/integration/item/itemTag.spec.js | 5 +- .../parking/parkingBasicData.spec.js | 4 +- .../route/agency/agencyWorkCenter.spec.js | 1 + .../integration/route/routeList.spec.js | 19 +- .../route/vehicle/vehicleDescriptor.spec.js | 13 + .../ticket/negative/TicketLackDetail.spec.js | 147 ++ .../ticket/negative/TicketLackList.spec.js | 36 + .../integration/ticket/ticketList.spec.js | 25 + .../vnComponent/VnShortcut.spec.js | 11 + .../wagon/wagonType/wagonTypeCreate.spec.js | 2 +- .../integration/zone/zoneBasicData.spec.js | 16 +- test/cypress/support/commands.js | 71 +- test/cypress/support/waitUntil.js | 2 +- 338 files changed, 9584 insertions(+), 4379 deletions(-) create mode 100644 src/boot/defaults/constants.js create mode 100644 src/components/common/VnCheckbox.vue create mode 100644 src/components/common/VnColor.vue create mode 100644 src/components/common/VnPopupProxy.vue create mode 100644 src/components/common/VnSelectTravelExtended.vue create mode 100644 src/components/ui/VnStockValueDisplay.vue create mode 100644 src/composables/checkEntryLock.js create mode 100644 src/composables/getColAlign.js create mode 100644 src/pages/Account/AccountExprBuilder.js create mode 100644 src/pages/Account/Alias/AliasExprBuilder.js create mode 100644 src/pages/Account/Card/AccountFilter.js create mode 100644 src/pages/Account/Role/RoleExprBuilder.js create mode 100644 src/pages/InvoiceIn/Card/InvoiceInFilter.js create mode 100644 src/pages/InvoiceOut/Card/InvoiceOutFilter.js create mode 100644 src/pages/Item/ItemType/Card/ItemTypeFilter.js rename src/pages/Item/{Card => components}/CreateGenusForm.vue (100%) rename src/pages/Item/{Card => components}/CreateSpecieForm.vue (100%) create mode 100644 src/pages/Item/components/ItemProposal.vue create mode 100644 src/pages/Item/components/ItemProposalProxy.vue create mode 100644 src/pages/Order/Card/OrderFilter.js create mode 100644 src/pages/Route/Card/RouteFilter.js create mode 100644 src/pages/Route/Roadmap/RoadmapFilter.js create mode 100644 src/pages/Route/Vehicle/Card/VehicleBasicData.vue create mode 100644 src/pages/Route/Vehicle/Card/VehicleCard.vue create mode 100644 src/pages/Route/Vehicle/Card/VehicleDescriptor.vue create mode 100644 src/pages/Route/Vehicle/Card/VehicleSummary.vue create mode 100644 src/pages/Route/Vehicle/VehicleFilter.js create mode 100644 src/pages/Route/Vehicle/VehicleList.vue create mode 100644 src/pages/Route/Vehicle/locale/en.yml create mode 100644 src/pages/Route/Vehicle/locale/es.yml create mode 100644 src/pages/Shelving/Card/ShelvingFilter.js rename src/pages/{ => Shelving}/Parking/Card/ParkingBasicData.vue (68%) rename src/pages/{ => Shelving}/Parking/Card/ParkingCard.vue (53%) rename src/pages/{ => Shelving}/Parking/Card/ParkingDescriptor.vue (58%) create mode 100644 src/pages/Shelving/Parking/Card/ParkingFilter.js rename src/pages/{ => Shelving}/Parking/Card/ParkingLog.vue (100%) rename src/pages/{ => Shelving}/Parking/Card/ParkingSummary.vue (100%) create mode 100644 src/pages/Shelving/Parking/ParkingExprBuilder.js rename src/pages/{ => Shelving}/Parking/ParkingFilter.vue (100%) rename src/pages/{ => Shelving}/Parking/ParkingList.vue (90%) rename src/pages/{ => Shelving}/Parking/locale/en.yml (100%) rename src/pages/{ => Shelving}/Parking/locale/es.yml (100%) create mode 100644 src/pages/Shelving/ShelvingExprBuilder.js create mode 100644 src/pages/Supplier/Card/SupplierFilter.js delete mode 100644 src/pages/Supplier/SupplierListFilter.vue create mode 100644 src/pages/Ticket/Card/TicketFilter.js create mode 100644 src/pages/Ticket/Card/TicketSplit.vue create mode 100644 src/pages/Ticket/Card/TicketTransferProxy.vue create mode 100644 src/pages/Ticket/Card/components/split.js create mode 100644 src/pages/Ticket/Negative/TicketLackDetail.vue create mode 100644 src/pages/Ticket/Negative/TicketLackFilter.vue create mode 100644 src/pages/Ticket/Negative/TicketLackList.vue create mode 100644 src/pages/Ticket/Negative/TicketLackTable.vue create mode 100644 src/pages/Ticket/Negative/components/ChangeItemDialog.vue create mode 100644 src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue create mode 100644 src/pages/Ticket/Negative/components/ChangeStateDialog.vue rename src/pages/{ => Worker}/Department/Card/DepartmentBasicData.vue (73%) rename src/pages/{ => Worker}/Department/Card/DepartmentCard.vue (70%) rename src/pages/{ => Worker}/Department/Card/DepartmentDescriptor.vue (84%) rename src/pages/{ => Worker}/Department/Card/DepartmentDescriptorProxy.vue (100%) rename src/pages/{ => Worker}/Department/Card/DepartmentSummary.vue (99%) rename src/pages/{ => Worker}/Department/Card/DepartmentSummaryDialog.vue (100%) create mode 100644 src/pages/Zone/Card/ZoneFilter.js create mode 100644 src/stores/__tests__/useNavigationStore.spec.js create mode 100644 src/utils/notifyResults.js create mode 100644 test/cypress/integration/entry/entryList.spec.js create mode 100644 test/cypress/integration/item/ItemProposal.spec.js create mode 100644 test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js create mode 100644 test/cypress/integration/ticket/negative/TicketLackDetail.spec.js create mode 100644 test/cypress/integration/ticket/negative/TicketLackList.spec.js diff --git a/cypress.config.js b/cypress.config.js index 1924144f6..a9e27fcfd 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -14,8 +14,8 @@ export default defineConfig({ downloadsFolder: 'test/cypress/downloads', video: false, specPattern: 'test/cypress/integration/**/*.spec.js', - experimentalRunAllSpecs: true, - watchForFileChanges: true, + experimentalRunAllSpecs: false, + watchForFileChanges: false, reporter: 'cypress-mochawesome-reporter', reporterOptions: { charts: true, diff --git a/package.json b/package.json index 17f39cad7..d23ed0ced 100644 --- a/package.json +++ b/package.json @@ -1,74 +1,74 @@ { - "name": "salix-front", - "version": "25.06.0", - "description": "Salix frontend", - "productName": "Salix", - "author": "Verdnatura", - "private": true, - "packageManager": "pnpm@8.15.1", - "type": "module", - "scripts": { - "resetDatabase": "cd ../salix && gulp docker", - "lint": "eslint --ext .js,.vue ./", - "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", - "test:e2e": "cypress open", - "test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run", - "test": "echo \"See package.json => scripts for available tests.\" && exit 0", - "test:unit": "vitest", - "test:unit:ci": "vitest run", - "commitlint": "commitlint --edit", - "prepare": "npx husky install", - "addReferenceTag": "node .husky/addReferenceTag.js", - "docs:dev": "vitepress dev docs", - "docs:build": "vitepress build docs", - "docs:preview": "vitepress preview docs" - }, - "dependencies": { - "@quasar/cli": "^2.4.1", - "@quasar/extras": "^1.16.16", - "axios": "^1.4.0", - "chromium": "^3.0.3", - "croppie": "^2.6.5", - "moment": "^2.30.1", - "pinia": "^2.1.3", - "quasar": "^2.17.7", - "validator": "^13.9.0", - "vue": "^3.5.13", - "vue-i18n": "^9.3.0", - "vue-router": "^4.2.5" - }, - "devDependencies": { - "@commitlint/cli": "^19.2.1", - "@commitlint/config-conventional": "^19.1.0", - "@intlify/unplugin-vue-i18n": "^0.8.2", - "@pinia/testing": "^0.1.2", - "@quasar/app-vite": "^2.0.8", - "@quasar/quasar-app-extension-qcalendar": "^4.0.2", - "@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0", - "@vue/test-utils": "^2.4.4", - "autoprefixer": "^10.4.14", - "cypress": "^13.6.6", - "cypress-mochawesome-reporter": "^3.8.2", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-cypress": "^4.1.0", - "eslint-plugin-vue": "^9.32.0", - "husky": "^8.0.0", - "postcss": "^8.4.23", - "prettier": "^3.4.2", - "sass": "^1.83.4", - "vitepress": "^1.6.3", - "vitest": "^0.34.0" - }, - "engines": { - "node": "^20 || ^18 || ^16", - "npm": ">= 8.1.2", - "yarn": ">= 1.21.1", - "bun": ">= 1.0.25" - }, - "overrides": { - "@vitejs/plugin-vue": "^5.2.1", - "vite": "^6.0.11", - "vitest": "^0.31.1" - } + "name": "salix-front", + "version": "25.08.0", + "description": "Salix frontend", + "productName": "Salix", + "author": "Verdnatura", + "private": true, + "packageManager": "pnpm@8.15.1", + "type": "module", + "scripts": { + "resetDatabase": "cd ../salix && gulp docker", + "lint": "eslint --ext .js,.vue ./", + "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", + "test:e2e": "cypress open", + "test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run", + "test": "echo \"See package.json => scripts for available tests.\" && exit 0", + "test:unit": "vitest", + "test:unit:ci": "vitest run", + "commitlint": "commitlint --edit", + "prepare": "npx husky install", + "addReferenceTag": "node .husky/addReferenceTag.js", + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "dependencies": { + "@quasar/cli": "^2.4.1", + "@quasar/extras": "^1.16.16", + "axios": "^1.4.0", + "chromium": "^3.0.3", + "croppie": "^2.6.5", + "moment": "^2.30.1", + "pinia": "^2.1.3", + "quasar": "^2.17.7", + "validator": "^13.9.0", + "vue": "^3.5.13", + "vue-i18n": "^9.3.0", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@commitlint/cli": "^19.2.1", + "@commitlint/config-conventional": "^19.1.0", + "@intlify/unplugin-vue-i18n": "^0.8.2", + "@pinia/testing": "^0.1.2", + "@quasar/app-vite": "^2.0.8", + "@quasar/quasar-app-extension-qcalendar": "^4.0.2", + "@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0", + "@vue/test-utils": "^2.4.4", + "autoprefixer": "^10.4.14", + "cypress": "^13.6.6", + "cypress-mochawesome-reporter": "^3.8.2", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-cypress": "^4.1.0", + "eslint-plugin-vue": "^9.32.0", + "husky": "^8.0.0", + "postcss": "^8.4.23", + "prettier": "^3.4.2", + "sass": "^1.83.4", + "vitepress": "^1.6.3", + "vitest": "^0.34.0" + }, + "engines": { + "node": "^20 || ^18 || ^16", + "npm": ">= 8.1.2", + "yarn": ">= 1.21.1", + "bun": ">= 1.0.25" + }, + "overrides": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.0.11", + "vitest": "^0.31.1" + } } \ No newline at end of file diff --git a/quasar.config.js b/quasar.config.js index 6d545c026..9467c92af 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -30,7 +30,6 @@ export default configure(function (/* ctx */) { // --> boot files are part of "main.js" // https://v2.quasar.dev/quasar-cli/boot-files boot: ['i18n', 'axios', 'vnDate', 'validations', 'quasar', 'quasar.defaults'], - // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css css: ['app.scss'], diff --git a/src/boot/defaults/constants.js b/src/boot/defaults/constants.js new file mode 100644 index 000000000..c96ceb2d1 --- /dev/null +++ b/src/boot/defaults/constants.js @@ -0,0 +1,2 @@ +export const langs = ['en', 'es']; +export const decimalPlaces = 2; diff --git a/src/boot/keyShortcut.js b/src/boot/keyShortcut.js index 5afb5b74a..6da06c8bf 100644 --- a/src/boot/keyShortcut.js +++ b/src/boot/keyShortcut.js @@ -1,6 +1,6 @@ export default { - mounted: function (el, binding) { - const shortcut = binding.value ?? '+'; + mounted(el, binding) { + const shortcut = binding.value || '+'; const { key, ctrl, alt, callback } = typeof shortcut === 'string' @@ -8,25 +8,24 @@ export default { key: shortcut, ctrl: true, alt: true, - callback: () => - document - .querySelector(`button[shortcut="${shortcut}"]`) - ?.click(), + callback: () => el?.click(), } : binding.value; + if (!el.hasAttribute('shortcut')) { + el.setAttribute('shortcut', key); + } + const handleKeydown = (event) => { if (event.key === key && (!ctrl || event.ctrlKey) && (!alt || event.altKey)) { callback(); } }; - // Attach the event listener to the window window.addEventListener('keydown', handleKeydown); - el._handleKeydown = handleKeydown; }, - unmounted: function (el) { + unmounted(el) { if (el._handleKeydown) { window.removeEventListener('keydown', el._handleKeydown); } diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js index 97d80c670..182c51e47 100644 --- a/src/boot/qformMixin.js +++ b/src/boot/qformMixin.js @@ -9,19 +9,19 @@ export default { if (!form) return; try { const inputsFormCard = form.querySelectorAll( - `input:not([disabled]):not([type="checkbox"])` + `input:not([disabled]):not([type="checkbox"])`, ); if (inputsFormCard.length) { focusFirstInput(inputsFormCard[0]); } const textareas = document.querySelectorAll( - 'textarea:not([disabled]), [contenteditable]:not([disabled])' + 'textarea:not([disabled]), [contenteditable]:not([disabled])', ); if (textareas.length) { focusFirstInput(textareas[textareas.length - 1]); } const inputs = document.querySelectorAll( - 'form#formModel input:not([disabled]):not([type="checkbox"])' + 'form#formModel input:not([disabled]):not([type="checkbox"])', ); const input = inputs[0]; if (!input) return; @@ -30,22 +30,5 @@ export default { } catch (error) { console.error(error); } - form.addEventListener('keyup', function (evt) { - if (evt.key === 'Enter' && !that.$attrs['prevent-submit']) { - const input = evt.target; - if (input.type == 'textarea' && evt.shiftKey) { - evt.preventDefault(); - let { selectionStart, selectionEnd } = input; - input.value = - input.value.substring(0, selectionStart) + - '\n' + - input.value.substring(selectionEnd); - selectionStart = selectionEnd = selectionStart + 1; - return; - } - evt.preventDefault(); - that.onSubmit(); - } - }); }, }; diff --git a/src/boot/quasar.js b/src/boot/quasar.js index 547517682..a8c397b83 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -51,4 +51,5 @@ export default boot(({ app }) => { await useCau(response, message); }; + app.provide('app', app); }); diff --git a/src/components/CreateBankEntityForm.vue b/src/components/CreateBankEntityForm.vue index 2da3aa994..7c4b94a6a 100644 --- a/src/components/CreateBankEntityForm.vue +++ b/src/components/CreateBankEntityForm.vue @@ -14,7 +14,7 @@ const { t } = useI18n(); const bicInputRef = ref(null); const state = useState(); -const customer = computed(() => state.get('customer')); +const customer = computed(() => state.get('Customer')); const countriesFilter = { fields: ['id', 'name', 'code'], diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index d569dfda1..93a2ac96a 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -64,6 +64,10 @@ const $props = defineProps({ type: Function, default: null, }, + beforeSaveFn: { + type: Function, + default: null, + }, goTo: { type: String, default: '', @@ -176,7 +180,11 @@ async function saveChanges(data) { hasChanges.value = false; return; } - const changes = data || getChanges(); + let changes = data || getChanges(); + if ($props.beforeSaveFn) { + changes = await $props.beforeSaveFn(changes, getChanges); + } + try { await axios.post($props.saveUrl || $props.url + '/crud', changes); } finally { @@ -229,12 +237,12 @@ async function remove(data) { componentProps: { title: t('globals.confirmDeletion'), message: t('globals.confirmDeletionMessage'), - newData, + data: { deletes: ids }, ids, + promise: saveChanges, }, }) .onOk(async () => { - await saveChanges({ deletes: ids }); newData = newData.filter((form) => !ids.some((id) => id == form[pk])); fetch(newData); }); @@ -374,6 +382,8 @@ watch(formUrl, async () => { @click="onSubmit" :disable="!hasChanges" :title="t('globals.save')" + v-shortcut="'s'" + shortcut="s" data-cy="crudModelDefaultSaveBtn" /> <slot name="moreAfterActions" /> diff --git a/src/components/FilterTravelForm.vue b/src/components/FilterTravelForm.vue index 4d43c3810..765d97763 100644 --- a/src/components/FilterTravelForm.vue +++ b/src/components/FilterTravelForm.vue @@ -181,6 +181,7 @@ const selectTravel = ({ id }) => { color="primary" :disabled="isLoading" :loading="isLoading" + data-cy="save-filter-travel-form" /> </div> <QTable @@ -191,9 +192,10 @@ const selectTravel = ({ id }) => { :no-data-label="t('Enter a new search')" class="q-mt-lg" @row-click="(_, row) => selectTravel(row)" + data-cy="table-filter-travel-form" > <template #body-cell-id="{ row }"> - <QTd auto-width @click.stop> + <QTd auto-width @click.stop data-cy="travelFk-travel-form"> <QBtn flat color="blue">{{ row.id }}</QBtn> <TravelDescriptorProxy :id="row.id" /> </QTd> diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 3842ff947..04ef13d45 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -1,6 +1,6 @@ <script setup> import axios from 'axios'; -import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue'; +import { onMounted, onUnmounted, computed, ref, watch, nextTick, useAttrs } from 'vue'; import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; @@ -22,6 +22,7 @@ const { validate } = useValidator(); const { notify } = useNotify(); const route = useRoute(); const myForm = ref(null); +const attrs = useAttrs(); const $props = defineProps({ url: { type: String, @@ -84,7 +85,7 @@ const $props = defineProps({ }, reload: { type: Boolean, - default: false, + default: true, }, defaultTrim: { type: Boolean, @@ -105,15 +106,15 @@ const isLoading = ref(false); // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas const isResetting = ref(false); const hasChanges = ref(!$props.observeFormChanges); -const originalData = ref({}); -const formData = computed(() => state.get(modelValue)); +const originalData = computed(() => state.get(modelValue)); +const formData = ref(); const defaultButtons = computed(() => ({ save: { dataCy: 'saveDefaultBtn', color: 'primary', icon: 'save', label: 'globals.save', - click: () => myForm.value.submit(), + click: async () => await save(), type: 'submit', }, reset: { @@ -127,8 +128,6 @@ const defaultButtons = computed(() => ({ })); onMounted(async () => { - originalData.value = JSON.parse(JSON.stringify($props.formInitialData ?? {})); - nextTick(() => (componentIsRendered.value = true)); // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla @@ -160,10 +159,18 @@ if (!$props.url) (val) => updateAndEmit('onFetch', { val }), ); +watch( + originalData, + (val) => { + if (val) formData.value = JSON.parse(JSON.stringify(val)); + }, + { immediate: true }, +); + watch( () => [$props.url, $props.filter], async () => { - originalData.value = null; + state.set(modelValue, null); reset(); await fetch(); }, @@ -198,7 +205,6 @@ async function fetch() { updateAndEmit('onFetch', { val: data }); } catch (e) { state.set(modelValue, {}); - originalData.value = {}; throw e; } } @@ -241,6 +247,7 @@ async function saveAndGo() { } function reset() { + formData.value = JSON.parse(JSON.stringify(originalData.value)); updateAndEmit('onFetch', { val: originalData.value }); if ($props.observeFormChanges) { hasChanges.value = false; @@ -265,7 +272,6 @@ function filter(value, update, filterOptions) { function updateAndEmit(evt, { val, res, old } = { val: null, res: null, old: null }) { state.set(modelValue, val); - originalData.value = val && JSON.parse(JSON.stringify(val)); if (!$props.url) arrayData.store.data = val; emit(evt, state.get(modelValue), res, old); @@ -279,6 +285,22 @@ function trimData(data) { return data; } +async function onKeyup(evt) { + if (evt.key === 'Enter' && !('prevent-submit' in attrs)) { + const input = evt.target; + if (input.type == 'textarea' && evt.shiftKey) { + let { selectionStart, selectionEnd } = input; + input.value = + input.value.substring(0, selectionStart) + + '\n' + + input.value.substring(selectionEnd); + selectionStart = selectionEnd = selectionStart + 1; + return; + } + await save(); + } +} + defineExpose({ save, isLoading, @@ -293,12 +315,12 @@ defineExpose({ <QForm ref="myForm" v-if="formData" - @submit="save" + @submit.prevent + @keyup.prevent="onKeyup" @reset="reset" class="q-pa-md" :style="maxWidth ? 'max-width: ' + maxWidth : ''" id="formModel" - :prevent-submit="$attrs['prevent-submit']" > <QCard> <slot diff --git a/src/components/FormModelPopup.vue b/src/components/FormModelPopup.vue index afdc6efca..85943e91e 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -1,12 +1,13 @@ <script setup> -import { ref, computed } from 'vue'; +import { ref, computed, useAttrs, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useState } from 'src/composables/useState'; import FormModel from 'components/FormModel.vue'; const emit = defineEmits(['onDataSaved', 'onDataCanceled']); -defineProps({ +const props = defineProps({ title: { type: String, default: '', @@ -15,23 +16,41 @@ defineProps({ type: String, default: '', }, + showSaveAndContinueBtn: { + type: Boolean, + default: false, + }, }); const { t } = useI18n(); - +const attrs = useAttrs(); +const state = useState(); const formModelRef = ref(null); const closeButton = ref(null); +const isSaveAndContinue = ref(props.showSaveAndContinueBtn); +const isLoading = computed(() => formModelRef.value?.isLoading); +const reset = computed(() => formModelRef.value?.reset); -const onDataSaved = (formData, requestResponse) => { - if (closeButton.value) closeButton.value.click(); +const onDataSaved = async (formData, requestResponse) => { + if (!isSaveAndContinue.value) closeButton.value?.click(); + if (isSaveAndContinue.value) { + await nextTick(); + state.set(attrs.model, attrs.formInitialData); + } + isSaveAndContinue.value = props.showSaveAndContinueBtn; emit('onDataSaved', formData, requestResponse); }; -const isLoading = computed(() => formModelRef.value?.isLoading); +const onClick = async (saveAndContinue) => { + isSaveAndContinue.value = saveAndContinue; + await formModelRef.value.save(); +}; defineExpose({ isLoading, onDataSaved, + isSaveAndContinue, + reset, }); </script> @@ -59,15 +78,16 @@ defineExpose({ flat :disabled="isLoading" :loading="isLoading" - @click="emit('onDataCanceled')" - v-close-popup data-cy="FormModelPopup_cancel" + v-close-popup z-max + @click="emit('onDataCanceled')" /> <QBtn + :flat="showSaveAndContinueBtn" :label="t('globals.save')" :title="t('globals.save')" - type="submit" + @click="onClick(false)" color="primary" class="q-ml-sm" :disabled="isLoading" @@ -75,6 +95,18 @@ defineExpose({ data-cy="FormModelPopup_save" z-max /> + <QBtn + v-if="showSaveAndContinueBtn" + :label="t('globals.isSaveAndContinue')" + :title="t('globals.isSaveAndContinue')" + color="primary" + class="q-ml-sm" + :disabled="isLoading" + :loading="isLoading" + data-cy="FormModelPopup_isSaveAndContinue" + z-max + @click="onClick(true)" + /> </div> </template> </FormModel> diff --git a/src/components/ItemsFilterPanel.vue b/src/components/ItemsFilterPanel.vue index 36123b834..f73753a6b 100644 --- a/src/components/ItemsFilterPanel.vue +++ b/src/components/ItemsFilterPanel.vue @@ -281,7 +281,7 @@ const setCategoryList = (data) => { <QItem class="q-mt-lg"> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat class="fill-icon-on-hover q-px-xs" color="primary" @@ -327,7 +327,6 @@ en: active: Is active visible: Is visible floramondo: Is floramondo - salesPersonFk: Buyer categoryFk: Category es: @@ -338,7 +337,6 @@ es: active: Activo visible: Visible floramondo: Floramondo - salesPersonFk: Comprador categoryFk: Categoría Plant: Planta natural Flower: Flor fresca diff --git a/src/components/LeftMenu.vue b/src/components/LeftMenu.vue index 644f831d4..9a9949499 100644 --- a/src/components/LeftMenu.vue +++ b/src/components/LeftMenu.vue @@ -41,7 +41,6 @@ const filteredItems = computed(() => { return locale.includes(normalizedSearch); }); }); - const filteredPinnedModules = computed(() => { if (!search.value) return pinnedModules.value; const normalizedSearch = search.value @@ -72,7 +71,7 @@ watch( items.value = []; getRoutes(); }, - { deep: true } + { deep: true }, ); function findMatches(search, item) { @@ -104,33 +103,40 @@ function addChildren(module, route, parent) { } function getRoutes() { - if (props.source === 'main') { - const modules = Object.assign([], navigation.getModules().value); - - for (const item of modules) { - const moduleDef = routes.find( - (route) => toLowerCamel(route.name) === item.module - ); - if (!moduleDef) continue; - item.children = []; - - addChildren(item.module, moduleDef, item.children); - } - - items.value = modules; + const handleRoutes = { + main: getMainRoutes, + card: getCardRoutes, + }; + try { + handleRoutes[props.source](); + } catch (error) { + throw new Error(`Method is not defined`); } +} +function getMainRoutes() { + const modules = Object.assign([], navigation.getModules().value); - if (props.source === 'card') { - const currentRoute = route.matched[1]; - const currentModule = toLowerCamel(currentRoute.name); - let moduleDef = routes.find( - (route) => toLowerCamel(route.name) === currentModule + for (const item of modules) { + const moduleDef = routes.find( + (route) => toLowerCamel(route.name) === item.module, ); + if (!moduleDef) continue; + item.children = []; - if (!moduleDef) return; - if (!moduleDef?.menus) moduleDef = betaGetRoutes(); - addChildren(currentModule, moduleDef, items.value); + addChildren(item.module, moduleDef, item.children); } + + items.value = modules; +} + +function getCardRoutes() { + const currentRoute = route.matched[1]; + const currentModule = toLowerCamel(currentRoute.name); + let moduleDef = routes.find((route) => toLowerCamel(route.name) === currentModule); + + if (!moduleDef) return; + if (!moduleDef?.menus) moduleDef = betaGetRoutes(); + addChildren(currentModule, moduleDef, items.value); } function betaGetRoutes() { @@ -223,9 +229,16 @@ const searchModule = () => { </template> <template v-for="(item, index) in filteredItems" :key="item.name"> <template - v-if="search ||item.children && !filteredPinnedModules.has(item.name)" + v-if=" + search || + (item.children && !filteredPinnedModules.has(item.name)) + " > - <LeftMenuItem :item="item" group="modules" :class="search && index === 0 ? 'searched' : ''"> + <LeftMenuItem + :item="item" + group="modules" + :class="search && index === 0 ? 'searched' : ''" + > <template #side> <QBtn v-if="item.isPinned === true" @@ -342,7 +355,7 @@ const searchModule = () => { .header { color: var(--vn-label-color); } -.searched{ +.searched { background-color: var(--vn-section-hover-color); } </style> diff --git a/src/components/LeftMenuItem.vue b/src/components/LeftMenuItem.vue index a3112b17f..c0cee44fe 100644 --- a/src/components/LeftMenuItem.vue +++ b/src/components/LeftMenuItem.vue @@ -26,6 +26,7 @@ const itemComputed = computed(() => { :to="{ name: itemComputed.name }" clickable v-ripple + :data-cy="`${itemComputed.name}-menu-item`" > <QItemSection avatar v-if="itemComputed.icon"> <QIcon :name="itemComputed.icon" /> diff --git a/src/components/RefundInvoiceForm.vue b/src/components/RefundInvoiceForm.vue index 590acede0..6dcb8b390 100644 --- a/src/components/RefundInvoiceForm.vue +++ b/src/components/RefundInvoiceForm.vue @@ -9,6 +9,7 @@ import VnSelect from 'components/common/VnSelect.vue'; import FormPopup from './FormPopup.vue'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const $props = defineProps({ invoiceOutData: { @@ -131,15 +132,11 @@ const refund = async () => { :required="true" /> </VnRow ><VnRow> - <div> - <QCheckbox - :label="t('Inherit warehouse')" - v-model="invoiceParams.inheritWarehouse" - /> - <QIcon name="info" class="cursor-info q-ml-sm" size="sm"> - <QTooltip>{{ t('Inherit warehouse tooltip') }}</QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="invoiceParams.inheritWarehouse" + :label="t('Inherit warehouse')" + :info="t('Inherit warehouse tooltip')" + /> </VnRow> </template> </FormPopup> diff --git a/src/components/TicketProblems.vue b/src/components/TicketProblems.vue index 934b13a1c..783f2556f 100644 --- a/src/components/TicketProblems.vue +++ b/src/components/TicketProblems.vue @@ -4,26 +4,21 @@ import { toCurrency } from 'src/filters'; defineProps({ row: { type: Object, required: true } }); </script> <template> - <span> - <QIcon - v-if="row.isTaxDataChecked === 0" - name="vn:no036" - color="primary" - size="xs" + <span class="q-gutter-x-xs"> + <router-link + v-if="row.claim?.claimFk" + :to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }" + class="link" > - <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> - </QIcon> - <QIcon v-if="row.hasTicketRequest" name="vn:buyrequest" color="primary" size="xs"> - <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> - </QIcon> - <QIcon v-if="row.itemShortage" name="vn:unavailable" color="primary" size="xs"> - <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> - </QIcon> - <QIcon v-if="row.isFreezed" name="vn:frozen" color="primary" size="xs"> - <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> - </QIcon> + <QIcon name="vn:claims" size="xs"> + <QTooltip> + {{ t('ticketSale.claim') }}: + {{ row.claim?.claimFk }} + </QTooltip> + </QIcon> + </router-link> <QIcon - v-if="row.risk" + v-if="row?.risk" name="vn:risk" :color="row.hasHighRisk ? 'negative' : 'primary'" size="xs" @@ -33,10 +28,57 @@ defineProps({ row: { type: Object, required: true } }); {{ toCurrency(row.risk - row.credit) }} </QTooltip> </QIcon> - <QIcon v-if="row.hasComponentLack" name="vn:components" color="primary" size="xs"> + <QIcon + v-if="row?.hasComponentLack" + name="vn:components" + color="primary" + size="xs" + > <QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip> </QIcon> - <QIcon v-if="row.isTooLittle" name="vn:isTooLittle" color="primary" size="xs"> + <QIcon v-if="row?.hasItemDelay" color="primary" size="xs" name="vn:hasItemDelay"> + <QTooltip> + {{ $t('ticket.summary.hasItemDelay') }} + </QTooltip> + </QIcon> + <QIcon v-if="row?.hasItemLost" color="primary" size="xs" name="vn:hasItemLost"> + <QTooltip> + {{ $t('salesTicketsTable.hasItemLost') }} + </QTooltip> + </QIcon> + <QIcon + v-if="row?.hasItemShortage" + name="vn:unavailable" + color="primary" + size="xs" + > + <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> + </QIcon> + <QIcon v-if="row?.hasRounding" color="primary" name="sync_problem" size="xs"> + <QTooltip> + {{ $t('ticketList.rounding') }} + </QTooltip> + </QIcon> + <QIcon + v-if="row?.hasTicketRequest" + name="vn:buyrequest" + color="primary" + size="xs" + > + <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> + </QIcon> + <QIcon + v-if="row?.isTaxDataChecked !== 0" + name="vn:no036" + color="primary" + size="xs" + > + <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> + </QIcon> + <QIcon v-if="row?.isFreezed" name="vn:frozen" color="primary" size="xs"> + <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> + </QIcon> + <QIcon v-if="row?.isTooLittle" name="vn:isTooLittle" color="primary" size="xs"> <QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip> </QIcon> </span> diff --git a/src/components/TransferInvoiceForm.vue b/src/components/TransferInvoiceForm.vue index aa71070d6..c4ef1454a 100644 --- a/src/components/TransferInvoiceForm.vue +++ b/src/components/TransferInvoiceForm.vue @@ -10,6 +10,7 @@ import VnSelect from 'components/common/VnSelect.vue'; import FormPopup from './FormPopup.vue'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; +import VnCheckbox from './common/VnCheckbox.vue'; const $props = defineProps({ invoiceOutData: { @@ -186,15 +187,11 @@ const makeInvoice = async () => { /> </VnRow> <VnRow> - <div> - <QCheckbox - :label="t('Bill destination client')" - v-model="checked" - /> - <QIcon name="info" class="cursor-info q-ml-sm" size="sm"> - <QTooltip>{{ t('transferInvoiceInfo') }}</QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="checked" + :label="t('Bill destination client')" + :info="t('transferInvoiceInfo')" + /> </VnRow> </template> </FormPopup> diff --git a/src/components/VnTable/VnColumn.vue b/src/components/VnTable/VnColumn.vue index 9e9bfad69..d0e245388 100644 --- a/src/components/VnTable/VnColumn.vue +++ b/src/components/VnTable/VnColumn.vue @@ -1,9 +1,8 @@ <script setup> import { markRaw, computed } from 'vue'; -import { QIcon, QCheckbox } from 'quasar'; +import { QIcon, QToggle } from 'quasar'; import { dashIfEmpty } from 'src/filters'; -/* basic input */ import VnSelect from 'components/common/VnSelect.vue'; import VnSelectCache from 'components/common/VnSelectCache.vue'; import VnInput from 'components/common/VnInput.vue'; @@ -12,8 +11,11 @@ import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputTime from 'components/common/VnInputTime.vue'; import VnComponent from 'components/common/VnComponent.vue'; import VnUserLink from 'components/ui/VnUserLink.vue'; +import VnSelectEnum from '../common/VnSelectEnum.vue'; +import VnCheckbox from '../common/VnCheckbox.vue'; const model = defineModel(undefined, { required: true }); +const emit = defineEmits(['blur']); const $props = defineProps({ column: { type: Object, @@ -39,10 +41,18 @@ const $props = defineProps({ type: Object, default: null, }, + autofocus: { + type: Boolean, + default: false, + }, showLabel: { type: Boolean, default: null, }, + eventHandlers: { + type: Object, + default: null, + }, }); const defaultSelect = { @@ -99,7 +109,8 @@ const defaultComponents = { }, }, checkbox: { - component: markRaw(QCheckbox), + ref: 'checkbox', + component: markRaw(VnCheckbox), attrs: ({ model }) => { const defaultAttrs = { disable: !$props.isEditable, @@ -115,6 +126,10 @@ const defaultComponents = { }, forceAttrs: { label: $props.showLabel && $props.column.label, + autofocus: true, + }, + events: { + blur: () => emit('blur'), }, }, select: { @@ -125,12 +140,19 @@ const defaultComponents = { component: markRaw(VnSelect), ...defaultSelect, }, + selectEnum: { + component: markRaw(VnSelectEnum), + ...defaultSelect, + }, icon: { component: markRaw(QIcon), }, userLink: { component: markRaw(VnUserLink), }, + toggle: { + component: markRaw(QToggle), + }, }; const value = computed(() => { @@ -160,7 +182,28 @@ const col = computed(() => { return newColumn; }); -const components = computed(() => $props.components ?? defaultComponents); +const components = computed(() => { + const sourceComponents = $props.components ?? defaultComponents; + + return Object.keys(sourceComponents).reduce((acc, key) => { + const component = sourceComponents[key]; + + if (!component || typeof component !== 'object') { + acc[key] = component; + return acc; + } + + acc[key] = { + ...component, + attrs: { + ...(component.attrs || {}), + autofocus: $props.autofocus, + }, + event: { ...component?.event, ...$props?.eventHandlers }, + }; + return acc; + }, {}); +}); </script> <template> <div class="row no-wrap"> diff --git a/src/components/VnTable/VnFilter.vue b/src/components/VnTable/VnFilter.vue index 426f5c716..0de3834ea 100644 --- a/src/components/VnTable/VnFilter.vue +++ b/src/components/VnTable/VnFilter.vue @@ -1,14 +1,12 @@ <script setup> import { markRaw, computed } from 'vue'; -import { QCheckbox } from 'quasar'; +import { QCheckbox, QToggle } from 'quasar'; import { useArrayData } from 'composables/useArrayData'; - -/* basic input */ import VnSelect from 'components/common/VnSelect.vue'; import VnInput from 'components/common/VnInput.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputTime from 'components/common/VnInputTime.vue'; -import VnTableColumn from 'components/VnTable/VnColumn.vue'; +import VnColumn from 'components/VnTable/VnColumn.vue'; const $props = defineProps({ column: { @@ -27,6 +25,10 @@ const $props = defineProps({ type: String, default: 'table', }, + customClass: { + type: String, + default: '', + }, }); defineExpose({ addFilter, props: $props }); @@ -34,7 +36,7 @@ defineExpose({ addFilter, props: $props }); const model = defineModel(undefined, { required: true }); const arrayData = useArrayData( $props.dataKey, - $props.searchUrl ? { searchUrl: $props.searchUrl } : null + $props.searchUrl ? { searchUrl: $props.searchUrl } : null, ); const columnFilter = computed(() => $props.column?.columnFilter); @@ -46,19 +48,18 @@ const enterEvent = { const defaultAttrs = { filled: !$props.showTitle, - class: 'q-px-xs q-pb-xs q-pt-none fit', dense: true, }; const forceAttrs = { - label: $props.showTitle ? '' : columnFilter.value?.label ?? $props.column.label, + label: $props.showTitle ? '' : (columnFilter.value?.label ?? $props.column.label), }; const selectComponent = { component: markRaw(VnSelect), event: updateEvent, attrs: { - class: 'q-px-sm q-pb-xs q-pt-none fit', + class: `q-pt-none fit ${$props.customClass}`, dense: true, filled: !$props.showTitle, }, @@ -109,14 +110,24 @@ const components = { component: markRaw(QCheckbox), event: updateEvent, attrs: { - dense: true, - class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs fit', + class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit', 'toggle-indeterminate': true, + size: 'sm', }, forceAttrs, }, select: selectComponent, rawSelect: selectComponent, + toggle: { + component: markRaw(QToggle), + event: updateEvent, + attrs: { + class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit', + 'toggle-indeterminate': true, + size: 'sm', + }, + forceAttrs, + }, }; async function addFilter(value, name) { @@ -132,19 +143,8 @@ async function addFilter(value, name) { await arrayData.addFilter({ params: { [field]: value } }); } -function alignRow() { - switch ($props.column.align) { - case 'left': - return 'justify-start items-start'; - case 'right': - return 'justify-end items-end'; - default: - return 'flex-center'; - } -} - const showFilter = computed( - () => $props.column?.columnFilter !== false && $props.column.name != 'tableActions' + () => $props.column?.columnFilter !== false && $props.column.name != 'tableActions', ); const onTabPressed = async () => { @@ -152,13 +152,8 @@ const onTabPressed = async () => { }; </script> <template> - <div - v-if="showFilter" - class="full-width" - :class="alignRow()" - style="max-height: 45px; overflow: hidden" - > - <VnTableColumn + <div v-if="showFilter" class="full-width" style="overflow: hidden"> + <VnColumn :column="$props.column" default="input" v-model="model" @@ -168,3 +163,8 @@ const onTabPressed = async () => { /> </div> </template> +<style lang="scss" scoped> +label.vn-label-padding > .q-field__inner > .q-field__control { + padding: inherit !important; +} +</style> diff --git a/src/components/VnTable/VnOrder.vue b/src/components/VnTable/VnOrder.vue index 8ffdfe2bc..47ed9acf4 100644 --- a/src/components/VnTable/VnOrder.vue +++ b/src/components/VnTable/VnOrder.vue @@ -23,6 +23,10 @@ const $props = defineProps({ type: Boolean, default: false, }, + align: { + type: String, + default: 'end', + }, }); const hover = ref(); const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl }); @@ -41,55 +45,78 @@ async function orderBy(name, direction) { break; } if (!direction) return await arrayData.deleteOrder(name); + await arrayData.addOrder(name, direction); } defineExpose({ orderBy }); + +function textAlignToFlex(textAlign) { + return `justify-content: ${ + { + 'text-center': 'center', + 'text-left': 'start', + 'text-right': 'end', + }[textAlign] || 'start' + };`; +} </script> <template> <div @mouseenter="hover = true" @mouseleave="hover = false" @click="orderBy(name, model?.direction)" - class="row items-center no-wrap cursor-pointer" + class="items-center no-wrap cursor-pointer title" + :style="textAlignToFlex(align)" > <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'" + <div v-if="name && model?.index"> + <QChip + :label="!vertical ? model?.index : ''" + :icon=" + (model?.index || hover) && !vertical + ? model?.direction == 'DESC' + ? 'arrow_downward' + : 'arrow_upward' + : undefined + " + :size="vertical ? '' : 'sm'" + :class="[ + model?.index ? 'color-vn-text' : 'bg-transparent', + vertical ? 'q-px-none' : '', + ]" + class="no-box-shadow" + :clickable="true" + style="min-width: 40px; max-height: 30px" > - {{ model?.index }} - <QIcon - :name=" - model?.index - ? model?.direction == 'DESC' - ? 'arrow_downward' - : 'arrow_upward' - : 'swap_vert' - " - size="xs" - /> - </div> - </QChip> + <div + class="column flex-center" + v-if="vertical" + :style="!model?.index && 'color: #5d5d5d'" + > + {{ model?.index }} + <QIcon + :name=" + model?.index + ? model?.direction == 'DESC' + ? 'arrow_downward' + : 'arrow_upward' + : 'swap_vert' + " + size="xs" + /> + </div> + </QChip> + </div> </div> </template> +<style lang="scss" scoped> +.title { + display: flex; + align-items: center; + height: 30px; + width: 100%; + color: var(--vn-label-color); + white-space: nowrap; +} +</style> diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 6e5f9fef4..7ff56860f 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -1,22 +1,38 @@ <script setup> -import { ref, onBeforeMount, onMounted, computed, watch, useAttrs } from 'vue'; +import { + ref, + onBeforeMount, + onMounted, + onUnmounted, + computed, + watch, + h, + render, + inject, + useAttrs, + nextTick, +} from 'vue'; +import { useArrayData } from 'src/composables/useArrayData'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; -import { useQuasar } from 'quasar'; +import { useQuasar, date } from 'quasar'; import { useStateStore } from 'stores/useStateStore'; import { useFilterParams } from 'src/composables/useFilterParams'; +import { dashIfEmpty, toDate } from 'src/filters'; import CrudModel from 'src/components/CrudModel.vue'; import FormModelPopup from 'components/FormModelPopup.vue'; -import VnTableColumn from 'components/VnTable/VnColumn.vue'; +import VnColumn from 'components/VnTable/VnColumn.vue'; import VnFilter from 'components/VnTable/VnFilter.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'; import VnTableFilter from './VnTableFilter.vue'; +import { getColAlign } from 'src/composables/getColAlign'; +const arrayData = useArrayData(useAttrs()['data-key']); const $props = defineProps({ columns: { type: Array, @@ -42,10 +58,6 @@ const $props = defineProps({ type: [Function, Boolean], default: null, }, - rowCtrlClick: { - type: [Function, Boolean], - default: null, - }, redirect: { type: String, default: null, @@ -114,7 +126,19 @@ const $props = defineProps({ type: Boolean, default: false, }, + withFilters: { + type: Boolean, + default: true, + }, + overlay: { + type: Boolean, + default: false, + }, + createComplement: { + type: Object, + }, }); + const { t } = useI18n(); const stateStore = useStateStore(); const route = useRoute(); @@ -132,10 +156,18 @@ const showForm = ref(false); const splittedColumns = ref({ columns: [] }); const columnsVisibilitySkipped = ref(); const createForm = ref(); +const createRef = ref(null); const tableRef = ref(); const params = ref(useFilterParams($attrs['data-key']).params); const orders = ref(useFilterParams($attrs['data-key']).orders); +const app = inject('app'); +const editingRow = ref(null); +const editingField = ref(null); +const isTableMode = computed(() => mode.value == TABLE_MODE); +const showRightIcon = computed(() => $props.rightSearch || $props.rightSearchIcon); +const selectRegex = /select/; +const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); const tableModes = [ { icon: 'view_column', @@ -156,7 +188,8 @@ onBeforeMount(() => { hasParams.value = urlParams && Object.keys(urlParams).length !== 0; }); -onMounted(() => { +onMounted(async () => { + if ($props.isEditable) document.addEventListener('click', clickHandler); mode.value = quasar.platform.is.mobile && !$props.disableOption?.card ? CARD_MODE @@ -178,14 +211,25 @@ onMounted(() => { } }); +onUnmounted(async () => { + if ($props.isEditable) document.removeEventListener('click', clickHandler); +}); + watch( () => $props.columns, (value) => splitColumns(value), { immediate: true }, ); -const isTableMode = computed(() => mode.value == TABLE_MODE); -const showRightIcon = computed(() => $props.rightSearch || $props.rightSearchIcon); +defineExpose({ + create: createForm, + reload, + redirect: redirectFn, + selected, + CrudModelRef, + params, + tableRef, +}); function splitColumns(columns) { splittedColumns.value = { @@ -231,16 +275,6 @@ const rowClickFunction = computed(() => { 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) { router.push({ path: `/${$props.redirect}/${id}` }); } @@ -262,21 +296,6 @@ function columnName(col) { return name; } -function getColAlign(col) { - return 'text-' + (col.align ?? 'left'); -} - -const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); -defineExpose({ - create: createForm, - reload, - redirect: redirectFn, - selected, - CrudModelRef, - params, - tableRef, -}); - function handleOnDataSaved(_) { if (_.onDataSaved) _.onDataSaved({ CrudModelRef: CrudModelRef.value }); else $props.create.onDataSaved(_); @@ -305,6 +324,237 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) { } } +function isEditableColumn(column) { + const isEditableCol = column?.isEditable ?? true; + const isVisible = column?.visible ?? true; + const hasComponent = column?.component; + + return $props.isEditable && isVisible && hasComponent && isEditableCol; +} + +function hasEditableFormat(column) { + if (isEditableColumn(column)) return 'editable-text'; +} + +const clickHandler = async (event) => { + const clickedElement = event.target.closest('td'); + + const isDateElement = event.target.closest('.q-date'); + const isTimeElement = event.target.closest('.q-time'); + const isQselectDropDown = event.target.closest('.q-select__dropdown-icon'); + + if (isDateElement || isTimeElement || isQselectDropDown) return; + + if (clickedElement === null) { + await destroyInput(editingRow.value, editingField.value); + return; + } + const rowIndex = clickedElement.getAttribute('data-row-index'); + const colField = clickedElement.getAttribute('data-col-field'); + const column = $props.columns.find((col) => col.name === colField); + + if (editingRow.value !== null && editingField.value !== null) { + if (editingRow.value == rowIndex && editingField.value == colField) return; + + await destroyInput(editingRow.value, editingField.value); + } + + if (isEditableColumn(column)) { + await renderInput(Number(rowIndex), colField, clickedElement); + } +}; + +async function handleTabKey(event, rowIndex, colField) { + if (editingRow.value == rowIndex && editingField.value == colField) + await destroyInput(editingRow.value, editingField.value); + + const direction = event.shiftKey ? -1 : 1; + const { nextRowIndex, nextColumnName } = await handleTabNavigation( + rowIndex, + colField, + direction, + ); + + if (nextRowIndex < 0 || nextRowIndex >= arrayData.store.data.length) return; + + event.preventDefault(); + await renderInput(nextRowIndex, nextColumnName, null); +} + +async function renderInput(rowId, field, clickedElement) { + editingField.value = field; + editingRow.value = rowId; + + const originalColumn = $props.columns.find((col) => col.name === field); + const column = { ...originalColumn, ...{ label: '' } }; + const row = CrudModelRef.value.formData[rowId]; + const oldValue = CrudModelRef.value.formData[rowId][column?.name]; + + if (!clickedElement) + clickedElement = document.querySelector( + `[data-row-index="${rowId}"][data-col-field="${field}"]`, + ); + + Array.from(clickedElement.childNodes).forEach((child) => { + child.style.visibility = 'hidden'; + child.style.position = 'relative'; + }); + + const isSelect = selectRegex.test(column?.component); + if (isSelect) column.attrs = { ...column.attrs, 'emit-value': false }; + + const node = h(VnColumn, { + row: row, + class: 'temp-input', + column: column, + modelValue: row[column.name], + componentProp: 'columnField', + autofocus: true, + focusOnMount: true, + eventHandlers: { + 'update:modelValue': async (value) => { + if (isSelect && value) { + row[column.name] = value[column.attrs?.optionValue ?? 'id']; + row[column?.name + 'TextValue'] = + value[column.attrs?.optionLabel ?? 'name']; + await column?.cellEvent?.['update:modelValue']?.( + value, + oldValue, + row, + ); + } else row[column.name] = value; + await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row); + }, + keyup: async (event) => { + if (event.key === 'Enter') + await destroyInput(rowIndex, field, clickedElement); + }, + keydown: async (event) => { + switch (event.key) { + case 'Tab': + await handleTabKey(event, rowId, field); + event.stopPropagation(); + break; + case 'Escape': + await destroyInput(rowId, field, clickedElement); + break; + default: + break; + } + }, + click: (event) => { + column?.cellEvent?.['click']?.(event, row); + }, + }, + }); + + node.appContext = app._context; + render(node, clickedElement); + + if (['toggle'].includes(column?.component)) + node.el?.querySelector('span > div').focus(); + + if (['checkbox', undefined].includes(column?.component)) + node.el?.querySelector('span > div > div').focus(); +} + +async function destroyInput(rowIndex, field, clickedElement) { + if (!clickedElement) + clickedElement = document.querySelector( + `[data-row-index="${rowIndex}"][data-col-field="${field}"]`, + ); + if (clickedElement) { + await nextTick(); + render(null, clickedElement); + Array.from(clickedElement.childNodes).forEach((child) => { + child.style.visibility = 'visible'; + child.style.position = ''; + }); + } + if (editingRow.value !== rowIndex || editingField.value !== field) return; + editingRow.value = null; + editingField.value = null; +} + +async function handleTabNavigation(rowIndex, colName, direction) { + const columns = $props.columns; + const totalColumns = columns.length; + let currentColumnIndex = columns.findIndex((col) => col.name === colName); + + let iterations = 0; + let newColumnIndex = currentColumnIndex; + + do { + iterations++; + newColumnIndex = (newColumnIndex + direction + totalColumns) % totalColumns; + + if (isEditableColumn(columns[newColumnIndex])) break; + } while (iterations < totalColumns); + + if (iterations >= totalColumns + 1) return; + + if (direction === 1 && newColumnIndex <= currentColumnIndex) { + rowIndex++; + } else if (direction === -1 && newColumnIndex >= currentColumnIndex) { + rowIndex--; + } + return { nextRowIndex: rowIndex, nextColumnName: columns[newColumnIndex].name }; +} + +function getCheckboxIcon(value) { + switch (typeof value) { + case 'boolean': + return value ? 'check' : 'close'; + case 'number': + return value === 0 ? 'close' : 'check'; + case 'undefined': + return 'indeterminate_check_box'; + default: + return 'indeterminate_check_box'; + } +} + +function getToggleIcon(value) { + if (value === null) return 'help_outline'; + return value ? 'toggle_on' : 'toggle_off'; +} + +function formatColumnValue(col, row, dashIfEmpty) { + if (col?.format || row[col?.name + 'TextValue']) { + if (selectRegex.test(col?.component) && row[col?.name + 'TextValue']) { + return dashIfEmpty(row[col?.name + 'TextValue']); + } else { + return col.format(row, dashIfEmpty); + } + } + + if (col?.component === 'date') return dashIfEmpty(toDate(row[col?.name])); + + if (col?.component === 'time') + return row[col?.name] >= 5 + ? dashIfEmpty(date.formatDate(new Date(row[col?.name]), 'HH:mm')) + : row[col?.name]; + + if (selectRegex.test(col?.component) && $props.isEditable) { + const { find, url } = col.attrs; + const urlRelation = url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1); + + if (col?.attrs.options) { + const find = col?.attrs.options.find((option) => option.id === row[col.name]); + if (!col.attrs?.optionLabel || !find) return dashIfEmpty(row[col?.name]); + return dashIfEmpty(find[col.attrs?.optionLabel ?? 'name']); + } + + if (typeof row[urlRelation] == 'object') { + if (typeof find == 'object') + return dashIfEmpty(row[urlRelation][find?.label ?? 'name']); + + return dashIfEmpty(row[urlRelation][col?.attrs.optionLabel ?? 'name']); + } + if (typeof row[urlRelation] == 'string') return dashIfEmpty(row[urlRelation]); + } + return dashIfEmpty(row[col?.name]); +} function cardClick(_, row) { if ($props.redirect) router.push({ path: `/${$props.redirect}/${row.id}` }); } @@ -315,7 +565,7 @@ function cardClick(_, row) { v-model="stateStore.rightDrawer" side="right" :width="256" - show-if-above + :overlay="$props.overlay" > <QScrollArea class="fit"> <VnTableFilter @@ -336,7 +586,7 @@ function cardClick(_, row) { <CrudModel v-bind="$attrs" :class="$attrs['class'] ?? 'q-px-md'" - :limit="$attrs['limit'] ?? 20" + :limit="$attrs['limit'] ?? 100" ref="CrudModelRef" @on-fetch="(...args) => emit('onFetch', ...args)" :search-url="searchUrl" @@ -352,8 +602,12 @@ function cardClick(_, row) { <QTable ref="tableRef" v-bind="table" - class="vnTable" - :class="{ 'last-row-sticky': $props.footer }" + :class="[ + 'vnTable', + table ? 'selection-cell' : '', + $props.footer ? 'last-row-sticky' : '', + ]" + wrap-cells :columns="splittedColumns.columns" :rows="rows" v-model:selected="selected" @@ -367,11 +621,13 @@ function cardClick(_, row) { @row-click="(_, row) => rowClickFunction && rowClickFunction(row)" @update:selected="emit('update:selected', $event)" @selection="(details) => handleSelection(details, rows)" + :hide-selected-banner="true" > <template #top-left v-if="!$props.withoutHeader"> - <slot name="top-left"></slot> + <slot name="top-left"> </slot> </template> <template #top-right v-if="!$props.withoutHeader"> + <slot name="top-right"></slot> <VnVisibleColumn v-if="isTableMode" v-model="splittedColumns.columns" @@ -385,6 +641,7 @@ function cardClick(_, row) { dense :options="tableModes.filter((mode) => !mode.disable)" /> + <QBtn v-if="showRightIcon" icon="filter_alt" @@ -396,32 +653,39 @@ function cardClick(_, row) { <template #header-cell="{ col }"> <QTh v-if="col.visible ?? true" - :style="col.headerStyle" - :class="col.headerClass" + v-bind:class="col.headerClass" + class="body-cell" + :style="col?.width ? `max-width: ${col?.width}` : ''" > <div - class="column ellipsis" - :class="`text-${col?.align ?? 'left'}`" - :style="$props.columnSearch ? 'height: 75px' : ''" + class="no-padding" + :style="[ + withFilters && $props.columnSearch ? 'height: 75px' : '', + ]" > - <div class="row items-center no-wrap" style="height: 30px"> + <div 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" + :label="col?.labelAbbreviation ?? col?.label" :data-key="$attrs['data-key']" :search-url="searchUrl" + :align="getColAlign(col)" /> </div> <VnFilter - v-if="$props.columnSearch" + v-if=" + $props.columnSearch && + col.columnSearch !== false && + withFilters + " :column="col" :show-title="true" :data-key="$attrs['data-key']" v-model="params[columnName(col)]" :search-url="searchUrl" - class="full-width" + customClass="header-filter" /> </div> </QTh> @@ -439,32 +703,67 @@ function cardClick(_, row) { </QTd> </template> <template #body-cell="{ col, row, rowIndex }"> - <!-- Columns --> <QTd - auto-width - class="no-margin" - :class="[getColAlign(col), col.columnClass]" - :style="col.style" + class="no-margin q-px-xs" v-if="col.visible ?? true" - @click.ctrl=" - ($event) => - rowCtrlClickFunction && rowCtrlClickFunction($event, row) - " + :style="{ + 'max-width': col?.width ?? false, + position: 'relative', + }" + :class="[ + col.columnClass, + 'body-cell no-margin no-padding', + getColAlign(col), + ]" + :data-row-index="rowIndex" + :data-col-field="col?.name" > - <slot - :name="`column-${col.name}`" - :col="col" - :row="row" - :row-index="rowIndex" + <div + class="no-padding no-margin peter" + style=" + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + " > - <VnTableColumn - :column="col" + <slot + :name="`column-${col.name}`" + :col="col" :row="row" - :is-editable="col.isEditable ?? isEditable" - v-model="row[col.name]" - component-prop="columnField" - /> - </slot> + :row-index="rowIndex" + > + <QIcon + v-if="col?.component === 'toggle'" + :name=" + col?.getIcon + ? col.getIcon(row[col?.name]) + : getToggleIcon(row[col?.name]) + " + style="color: var(--vn-text-color)" + :class="hasEditableFormat(col)" + size="14px" + /> + <QIcon + v-else-if="col?.component === 'checkbox'" + :name="getCheckboxIcon(row[col?.name])" + style="color: var(--vn-text-color)" + :class="hasEditableFormat(col)" + size="14px" + /> + <span + v-else + :class="hasEditableFormat(col)" + :style=" + typeof col?.style == 'function' + ? col.style(row) + : col?.style + " + style="bottom: 0" + > + {{ formatColumnValue(col, row, dashIfEmpty) }} + </span> + </slot> + </div> </QTd> </template> <template #body-cell-tableActions="{ col, row }"> @@ -485,7 +784,7 @@ function cardClick(_, row) { flat dense :class=" - btn.isPrimary ? 'text-primary-light' : 'color-vn-text ' + btn.isPrimary ? 'text-primary-light' : 'color-vn-label' " :style="`visibility: ${ ((btn.show && btn.show(row)) ?? true) @@ -493,6 +792,7 @@ function cardClick(_, row) { : 'hidden' }`" @click="btn.action(row)" + :data-cy="btn?.name ?? `tableAction-${index}`" /> </QTd> </template> @@ -541,7 +841,7 @@ function cardClick(_, row) { </QCardSection> <!-- Fields --> <QCardSection - class="q-pl-sm q-pr-lg q-py-xs" + class="q-pl-sm q-py-xs" :class="$props.cardClass" > <div @@ -562,7 +862,7 @@ function cardClick(_, row) { :row="row" :row-index="index" > - <VnTableColumn + <VnColumn :column="col" :row="row" :is-editable="false" @@ -589,12 +889,12 @@ function cardClick(_, row) { :title="btn.title" :icon="btn.icon" class="q-pa-xs" - flat :class=" btn.isPrimary ? 'text-primary-light' - : 'color-vn-text ' + : 'color-vn-label' " + flat @click="btn.action(row)" /> </QCardSection> @@ -602,14 +902,17 @@ function cardClick(_, row) { </component> </template> <template #bottom-row="{ cols }" v-if="$props.footer"> - <QTr v-if="rows.length" style="height: 30px"> + <QTr v-if="rows.length" style="height: 45px"> + <QTh v-if="table.selection" /> <QTh v-for="col of cols.filter((cols) => cols.visible ?? true)" :key="col?.id" - class="text-center" :class="getColAlign(col)" > - <slot :name="`column-footer-${col.name}`" /> + <slot + :name="`column-footer-${col.name}`" + :isEditableColumn="isEditableColumn(col)" + /> </QTh> </QTr> </template> @@ -628,7 +931,7 @@ function cardClick(_, row) { size="md" round flat - shortcut="+" + v-shortcut="'+'" :disabled="!disabledAttr" /> <QTooltip> @@ -646,39 +949,52 @@ function cardClick(_, row) { color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" data-cy="vnTableCreateBtn" /> <QTooltip self="top right"> {{ createForm?.title }} </QTooltip> </QPageSticky> - <QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> + <QDialog + v-model="showForm" + transition-show="scale" + transition-hide="scale" + :full-width="createComplement?.isFullWidth ?? false" + data-cy="vn-table-create-dialog" + > <FormModelPopup + ref="createRef" v-bind="createForm" :model="$attrs['data-key'] + 'Create'" @on-data-saved="(_, res) => createForm.onDataSaved(res)" > <template #form-inputs="{ data }"> - <div class="grid-create"> - <slot - v-for="column of splittedColumns.create" - :key="column.name" - :name="`column-create-${column.name}`" - :data="data" - :column-name="column.name" - :label="column.label" - > - <VnTableColumn - :column="column" - :row="{}" - default="input" - v-model="data[column.name]" - :show-label="true" - component-prop="columnCreate" - /> - </slot> - <slot name="more-create-dialog" :data="data" /> + <div :style="createComplement?.containerStyle"> + <div> + <slot name="previous-create-dialog" :data="data" /> + </div> + <div class="grid-create" :style="createComplement?.columnGridStyle"> + <slot + v-for="column of splittedColumns.create" + :key="column.name" + :name="`column-create-${column.name}`" + :data="data" + :column-name="column.name" + :label="column.label" + > + <VnColumn + :column="column" + :row="{}" + default="input" + v-model="data[column.name]" + :show-label="true" + component-prop="columnCreate" + :data-cy="`${column.name}-create-popup`" + /> + </slot> + <slot name="more-create-dialog" :data="data" /> + </div> </div> </template> </FormModelPopup> @@ -696,6 +1012,42 @@ es: </i18n> <style lang="scss"> +.selection-cell { + table td:first-child { + padding: 0px; + } +} +.side-padding { + padding-left: 1px; + padding-right: 1px; +} +.editable-text:hover { + border-bottom: 1px dashed var(--q-primary); + @extend .side-padding; +} +.editable-text { + border-bottom: 1px dashed var(--vn-label-color); + @extend .side-padding; +} +.cell-input { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding-top: 0px !important; +} +.q-field--labeled .q-field__native, +.q-field--labeled .q-field__prefix, +.q-field--labeled .q-field__suffix { + padding-top: 20px; +} + +.body-cell { + padding-left: 4px !important; + padding-right: 4px !important; + position: relative; +} .bg-chip-secondary { background-color: var(--vn-page-color); color: var(--vn-text-color); @@ -712,8 +1064,8 @@ es: .grid-three { display: grid; - grid-template-columns: repeat(auto-fit, minmax(350px, max-content)); - max-width: 100%; + grid-template-columns: repeat(auto-fit, minmax(300px, max-content)); + width: 100%; grid-gap: 20px; margin: 0 auto; } @@ -721,7 +1073,6 @@ es: .grid-create { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, max-content)); - max-width: 100%; grid-gap: 20px; margin: 0 auto; } @@ -737,7 +1088,9 @@ es: } } } - +.q-table tbody tr td { + position: relative; +} .q-table { th { padding: 0; @@ -786,6 +1139,7 @@ es: .vn-label-value { display: flex; flex-direction: row; + align-items: center; color: var(--vn-text-color); .value { overflow: hidden; @@ -837,4 +1191,15 @@ es: .q-table__middle.q-virtual-scroll.q-virtual-scroll--vertical.scroll { background-color: var(--vn-section-color); } +.temp-input { + top: 0; + position: absolute; + width: 100%; + height: 100%; + display: flex; +} + +label.header-filter > .q-field__inner > .q-field__control { + padding: inherit; +} </style> diff --git a/src/components/VnTable/VnTableFilter.vue b/src/components/VnTable/VnTableFilter.vue index 732605ce5..79b903e54 100644 --- a/src/components/VnTable/VnTableFilter.vue +++ b/src/components/VnTable/VnTableFilter.vue @@ -27,31 +27,36 @@ function columnName(col) { </script> <template> <VnFilterPanel v-bind="$attrs" :search-button="true" :disable-submit-event="true"> - <template #body="{ params, orders }"> + <template #body="{ params, orders, searchFn }"> <div - class="row no-wrap flex-center" + class="container" v-for="col of columns.filter((c) => c.columnFilter ?? true)" :key="col.id" > - <VnFilter - ref="tableFilterRef" - :column="col" - :data-key="$attrs['data-key']" - v-model="params[columnName(col)]" - :search-url="searchUrl" - /> - <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 class="filter"> + <VnFilter + ref="tableFilterRef" + :column="col" + :data-key="$attrs['data-key']" + v-model="params[columnName(col)]" + :search-url="searchUrl" + /> + </div> + <div class="order"> + <VnTableOrder + v-if="col?.columnFilter !== false && col?.name !== 'tableActions'" + v-model="orders[col.orderBy ?? col.name]" + :name="col.orderBy ?? col.name" + :data-key="$attrs['data-key']" + :search-url="searchUrl" + :vertical="true" + /> + </div> </div> <slot name="moreFilterPanel" :params="params" + :search-fn="searchFn" :orders="orders" :columns="columns" /> @@ -67,3 +72,21 @@ function columnName(col) { </template> </VnFilterPanel> </template> +<style lang="scss" scoped> +.container { + display: flex; + justify-content: center; + align-items: center; + height: 45px; + gap: 10px; +} + +.filter { + width: 70%; + height: 40px; + text-align: center; +} +.order { + width: 10%; +} +</style> diff --git a/src/components/VnTable/VnVisibleColumn.vue b/src/components/VnTable/VnVisibleColumn.vue index dad950d73..6d15c585e 100644 --- a/src/components/VnTable/VnVisibleColumn.vue +++ b/src/components/VnTable/VnVisibleColumn.vue @@ -32,16 +32,21 @@ const areAllChecksMarked = computed(() => { function setUserConfigViewData(data, isLocal) { if (!data) return; - // Importante: El name de las columnas de la tabla debe conincidir con el name de las variables que devuelve la view config if (!isLocal) localColumns.value = []; - // Array to Object + const skippeds = $props.skip.reduce((a, v) => ({ ...a, [v]: v }), {}); for (let column of columns.value) { - const { label, name } = column; + const { label, name, labelAbbreviation } = column; if (skippeds[name]) continue; column.visible = data[name] ?? true; - if (!isLocal) localColumns.value.push({ name, label, visible: column.visible }); + if (!isLocal) + localColumns.value.push({ + name, + label, + labelAbbreviation, + visible: column.visible, + }); } } @@ -152,7 +157,11 @@ onMounted(async () => { <QCheckbox v-for="col in localColumns" :key="col.name" - :label="col.label ?? col.name" + :label=" + col?.labelAbbreviation + ? col.labelAbbreviation + ` (${col.label ?? col.name})` + : (col.label ?? col.name) + " v-model="col.visible" /> </div> diff --git a/src/components/__tests__/FormModel.spec.js b/src/components/__tests__/FormModel.spec.js index e35684bc3..3dce04374 100644 --- a/src/components/__tests__/FormModel.spec.js +++ b/src/components/__tests__/FormModel.spec.js @@ -57,6 +57,7 @@ describe('FormModel', () => { vm.state.set(model, formInitialData); expect(vm.hasChanges).toBe(false); + await vm.$nextTick(); vm.formData.mockKey = 'newVal'; await vm.$nextTick(); expect(vm.hasChanges).toBe(true); @@ -93,9 +94,13 @@ describe('FormModel', () => { it('should call axios.patch with the right data', async () => { const spy = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} }); - const { vm } = mount({ propsData: { url, model, formInitialData } }); - vm.formData.mockKey = 'newVal'; + const { vm } = mount({ propsData: { url, model } }); + + vm.formData = {}; await vm.$nextTick(); + vm.formData = { mockKey: 'newVal' }; + await vm.$nextTick(); + await vm.save(); expect(spy).toHaveBeenCalled(); vm.formData.mockKey = 'mockVal'; @@ -106,6 +111,7 @@ describe('FormModel', () => { const { vm } = mount({ propsData: { url, model, formInitialData, urlCreate: 'mockUrlCreate' }, }); + await vm.$nextTick(); vm.formData.mockKey = 'newVal'; await vm.$nextTick(); await vm.save(); @@ -119,7 +125,7 @@ describe('FormModel', () => { }); const spyPatch = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} }); const spySaveFn = vi.spyOn(vm.$props, 'saveFn'); - + await vm.$nextTick(); vm.formData.mockKey = 'newVal'; await vm.$nextTick(); await vm.save(); diff --git a/src/components/__tests__/Leftmenu.spec.js b/src/components/__tests__/Leftmenu.spec.js index 10d9d66fb..4ab8b527f 100644 --- a/src/components/__tests__/Leftmenu.spec.js +++ b/src/components/__tests__/Leftmenu.spec.js @@ -1,9 +1,12 @@ -import { vi, describe, expect, it, beforeAll } from 'vitest'; +import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest'; import { createWrapper, axios } from 'app/test/vitest/helper'; import Leftmenu from 'components/LeftMenu.vue'; - +import * as vueRouter from 'vue-router'; import { useNavigationStore } from 'src/stores/useNavigationStore'; +let vm; +let navigation; + vi.mock('src/router/modules', () => ({ default: [ { @@ -21,6 +24,16 @@ vi.mock('src/router/modules', () => ({ { path: '', name: 'CustomerMain', + meta: { + menu: 'Customer', + menuChildren: [ + { + name: 'CustomerCreditContracts', + title: 'creditContracts', + icon: 'vn:solunion', + }, + ], + }, children: [ { path: 'list', @@ -28,6 +41,13 @@ vi.mock('src/router/modules', () => ({ meta: { title: 'list', icon: 'view_list', + menuChildren: [ + { + name: 'CustomerCreditContracts', + title: 'creditContracts', + icon: 'vn:solunion', + }, + ], }, }, { @@ -44,51 +64,325 @@ vi.mock('src/router/modules', () => ({ }, ], })); - -describe('Leftmenu', () => { - let vm; - let navigation; - beforeAll(() => { - vi.spyOn(axios, 'get').mockResolvedValue({ - data: [], - }); - - vm = createWrapper(Leftmenu, { - propsData: { - source: 'main', +vi.spyOn(vueRouter, 'useRoute').mockReturnValue({ + matched: [ + { + path: '/', + redirect: { + name: 'Dashboard', }, - }).vm; - - navigation = useNavigationStore(); - navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true)); - navigation.getModules = vi.fn().mockReturnValue({ - value: [ + name: 'Main', + meta: {}, + props: { + default: false, + }, + children: [ { - name: 'customer', - title: 'customer.pageTitles.customers', - icon: 'vn:customer', - module: 'customer', + path: '/dashboard', + name: 'Dashboard', + meta: { + title: 'dashboard', + icon: 'dashboard', + }, }, ], + }, + { + path: '/customer', + redirect: { + name: 'CustomerMain', + }, + name: 'Customer', + meta: { + title: 'customers', + icon: 'vn:client', + moduleName: 'Customer', + keyBinding: 'c', + menu: 'customer', + }, + }, + ], + query: {}, + params: {}, + meta: { moduleName: 'mockName' }, + path: 'mockName/1', + name: 'Customer', +}); +function mount(source = 'main') { + vi.spyOn(axios, 'get').mockResolvedValue({ + data: [], + }); + const wrapper = createWrapper(Leftmenu, { + propsData: { + source, + }, + }); + + navigation = useNavigationStore(); + navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true)); + navigation.getModules = vi.fn().mockReturnValue({ + value: [ + { + name: 'customer', + title: 'customer.pageTitles.customers', + icon: 'vn:customer', + module: 'customer', + }, + ], + }); + return wrapper; +} + +describe('getRoutes', () => { + afterEach(() => vi.clearAllMocks()); + const getRoutes = vi.fn().mockImplementation((props, getMethodA, getMethodB) => { + const handleRoutes = { + methodA: getMethodA, + methodB: getMethodB, + }; + try { + handleRoutes[props.source](); + } catch (error) { + throw Error('Method not defined'); + } + }); + + const getMethodA = vi.fn(); + const getMethodB = vi.fn(); + const fn = (props) => getRoutes(props, getMethodA, getMethodB); + + it('should call getMethodB when source is card', () => { + let props = { source: 'methodB' }; + fn(props); + + expect(getMethodB).toHaveBeenCalled(); + expect(getMethodA).not.toHaveBeenCalled(); + }); + it('should call getMethodA when source is main', () => { + let props = { source: 'methodA' }; + fn(props); + + expect(getMethodA).toHaveBeenCalled(); + expect(getMethodB).not.toHaveBeenCalled(); + }); + + it('should call getMethodA when source is not exists or undefined', () => { + let props = { source: 'methodC' }; + expect(() => fn(props)).toThrowError('Method not defined'); + + expect(getMethodA).not.toHaveBeenCalled(); + expect(getMethodB).not.toHaveBeenCalled(); + }); +}); + +describe('Leftmenu as card', () => { + beforeAll(() => { + vm = mount('card').vm; + }); + + it('should get routes for card source', async () => { + vm.getRoutes(); + }); +}); +describe('Leftmenu as main', () => { + beforeEach(() => { + vm = mount().vm; + }); + + it('should initialize with default props', () => { + expect(vm.source).toBe('main'); + }); + + it('should filter items based on search input', async () => { + vm.search = 'cust'; + await vm.$nextTick(); + expect(vm.filteredItems[0].name).toEqual('customer'); + expect(vm.filteredItems[0].module).toEqual('customer'); + }); + it('should filter items based on search input', async () => { + vm.search = 'Rou'; + await vm.$nextTick(); + expect(vm.filteredItems).toEqual([]); + }); + + it('should return pinned items', () => { + vm.items = [ + { name: 'Item 1', isPinned: false }, + { name: 'Item 2', isPinned: true }, + ]; + expect(vm.pinnedModules).toEqual( + new Map([['Item 2', { name: 'Item 2', isPinned: true }]]), + ); + }); + + it('should find matches in routes', () => { + const search = 'child1'; + const item = { + children: [ + { name: 'child1', children: [] }, + { name: 'child2', children: [] }, + ], + }; + const matches = vm.findMatches(search, item); + expect(matches).toEqual([{ name: 'child1', children: [] }]); + }); + it('should not proceed if event is already prevented', async () => { + const item = { module: 'testModule', isPinned: false }; + const event = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + defaultPrevented: true, + }; + + await vm.togglePinned(item, event); + + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(event.stopPropagation).not.toHaveBeenCalled(); + }); + + it('should call quasar.notify with success message', async () => { + const item = { module: 'testModule', isPinned: false }; + const event = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + defaultPrevented: false, + }; + const response = { data: { id: 1 } }; + + vi.spyOn(axios, 'post').mockResolvedValue(response); + vi.spyOn(vm.quasar, 'notify'); + + await vm.togglePinned(item, event); + + expect(vm.quasar.notify).toHaveBeenCalledWith({ + message: 'Data saved', + type: 'positive', }); }); - it('should return a proper formated object with two child items', async () => { - const expectedMenuItem = [ - { - children: null, - name: 'CustomerList', - title: 'globals.pageTitles.list', - icon: 'view_list', - }, - { - children: null, - name: 'CustomerCreate', - title: 'globals.pageTitles.createCustomer', - icon: 'vn:addperson', - }, - ]; - const firstMenuItem = vm.items[0]; - expect(firstMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem)); + it('should handle a single matched route with a menu', () => { + const route = { + matched: [{ meta: { menu: 'customer' } }], + }; + + const result = vm.betaGetRoutes(); + + expect(result.meta.menu).toEqual(route.matched[0].meta.menu); + }); + it('should get routes for main source', () => { + vm.props.source = 'main'; + vm.getRoutes(); + expect(navigation.getModules).toHaveBeenCalled(); + }); + + it('should find direct child matches', () => { + const search = 'child1'; + const item = { + children: [{ name: 'child1' }, { name: 'child2' }], + }; + const result = vm.findMatches(search, item); + expect(result).toEqual([{ name: 'child1' }]); + }); + + it('should find nested child matches', () => { + const search = 'child3'; + const item = { + children: [ + { name: 'child1' }, + { + name: 'child2', + children: [{ name: 'child3' }], + }, + ], + }; + const result = vm.findMatches(search, item); + expect(result).toEqual([{ name: 'child3' }]); + }); +}); + +describe('normalize', () => { + beforeAll(() => { + vm = mount('card').vm; + }); + it('should normalize and lowercase text', () => { + const input = 'ÁÉÍÓÚáéíóú'; + const expected = 'aeiouaeiou'; + expect(vm.normalize(input)).toBe(expected); + }); + + it('should handle empty string', () => { + const input = ''; + const expected = ''; + expect(vm.normalize(input)).toBe(expected); + }); + + it('should handle text without diacritics', () => { + const input = 'hello'; + const expected = 'hello'; + expect(vm.normalize(input)).toBe(expected); + }); + + it('should handle mixed text', () => { + const input = 'Héllo Wórld!'; + const expected = 'hello world!'; + expect(vm.normalize(input)).toBe(expected); + }); +}); + +describe('addChildren', () => { + const module = 'testModule'; + beforeEach(() => { + vm = mount().vm; + vi.clearAllMocks(); + }); + + it('should add menu items to parent if matches are found', () => { + const parent = 'testParent'; + const route = { + meta: { + menu: 'testMenu', + }, + children: [{ name: 'child1' }, { name: 'child2' }], + }; + vm.addChildren(module, route, parent); + + expect(navigation.addMenuItem).toHaveBeenCalled(); + }); + + it('should handle routes with no meta menu', () => { + const route = { + meta: {}, + menus: {}, + }; + + const parent = []; + + vm.addChildren(module, route, parent); + expect(navigation.addMenuItem).toHaveBeenCalled(); + }); + + it('should handle empty parent array', () => { + const parent = []; + const route = { + meta: { + menu: 'child11', + }, + children: [ + { + name: 'child1', + meta: { + menuChildren: [ + { + name: 'CustomerCreditContracts', + title: 'creditContracts', + icon: 'vn:solunion', + }, + ], + }, + }, + ], + }; + vm.addChildren(module, route, parent); + expect(navigation.addMenuItem).toHaveBeenCalled(); }); }); diff --git a/src/components/__tests__/UserPanel.spec.js b/src/components/__tests__/UserPanel.spec.js index ac20f911e..9e449745a 100644 --- a/src/components/__tests__/UserPanel.spec.js +++ b/src/components/__tests__/UserPanel.spec.js @@ -1,61 +1,65 @@ -import { vi, describe, expect, it, beforeEach, beforeAll, afterEach } from 'vitest'; +import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest'; import { createWrapper } from 'app/test/vitest/helper'; import UserPanel from 'src/components/UserPanel.vue'; import axios from 'axios'; import { useState } from 'src/composables/useState'; +vi.mock('src/utils/quasarLang', () => ({ + default: vi.fn(), +})); + describe('UserPanel', () => { - let wrapper; - let vm; - let state; + let wrapper; + let vm; + let state; - beforeEach(() => { - wrapper = createWrapper(UserPanel, {}); - state = useState(); - state.setUser({ - id: 115, - name: 'itmanagement', - nickname: 'itManagementNick', - lang: 'en', - darkMode: false, - companyFk: 442, - warehouseFk: 1, - }); - wrapper = wrapper.wrapper; - vm = wrapper.vm; + beforeEach(() => { + wrapper = createWrapper(UserPanel, {}); + state = useState(); + state.setUser({ + id: 115, + name: 'itmanagement', + nickname: 'itManagementNick', + lang: 'en', + darkMode: false, + companyFk: 442, + warehouseFk: 1, }); + wrapper = wrapper.wrapper; + vm = wrapper.vm; + }); - afterEach(() => { - vi.clearAllMocks(); - }); + afterEach(() => { + vi.clearAllMocks(); + }); - it('should fetch warehouses data on mounted', async () => { - const fetchData = wrapper.findComponent({ name: 'FetchData' }); - expect(fetchData.props('url')).toBe('Warehouses'); - expect(fetchData.props('autoLoad')).toBe(true); - }); + it('should fetch warehouses data on mounted', async () => { + const fetchData = wrapper.findComponent({ name: 'FetchData' }); + expect(fetchData.props('url')).toBe('Warehouses'); + expect(fetchData.props('autoLoad')).toBe(true); + }); - it('should toggle dark mode correctly and update preferences', async () => { - await vm.saveDarkMode(true); - expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true }); - expect(vm.user.darkMode).toBe(true); - vm.updatePreferences(); - expect(vm.darkMode).toBe(true); - }); + it('should toggle dark mode correctly and update preferences', async () => { + await vm.saveDarkMode(true); + expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true }); + expect(vm.user.darkMode).toBe(true); + await vm.updatePreferences(); + expect(vm.darkMode).toBe(true); + }); - it('should change user language and update preferences', async () => { - const userLanguage = 'es'; - await vm.saveLanguage(userLanguage); - expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage }); - expect(vm.user.lang).toBe(userLanguage); - vm.updatePreferences(); - expect(vm.locale).toBe(userLanguage); - }); + it('should change user language and update preferences', async () => { + const userLanguage = 'es'; + await vm.saveLanguage(userLanguage); + expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage }); + expect(vm.user.lang).toBe(userLanguage); + await vm.updatePreferences(); + expect(vm.locale).toBe(userLanguage); + }); - it('should update user data', async () => { - const key = 'name'; - const value = 'itboss'; - await vm.saveUserData(key, value); - expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value }); - }); -}); + it('should update user data', async () => { + const key = 'name'; + const value = 'itboss'; + await vm.saveUserData(key, value); + expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value }); + }); +}); \ No newline at end of file diff --git a/src/components/common/VnCard.vue b/src/components/common/VnCard.vue index 0d80f43ce..44002c22a 100644 --- a/src/components/common/VnCard.vue +++ b/src/components/common/VnCard.vue @@ -10,11 +10,11 @@ import LeftMenu from 'components/LeftMenu.vue'; import RightMenu from 'components/common/RightMenu.vue'; const props = defineProps({ dataKey: { type: String, required: true }, - baseUrl: { type: String, default: undefined }, - customUrl: { type: String, default: undefined }, + url: { type: String, default: undefined }, filter: { type: Object, default: () => {} }, descriptor: { type: Object, required: true }, filterPanel: { type: Object, default: undefined }, + idInWhere: { type: Boolean, default: false }, searchDataKey: { type: String, default: undefined }, searchbarProps: { type: Object, default: undefined }, redirectOnError: { type: Boolean, default: false }, @@ -23,25 +23,20 @@ const props = defineProps({ const stateStore = useStateStore(); const route = useRoute(); const router = useRouter(); -const url = computed(() => { - if (props.baseUrl) { - return `${props.baseUrl}/${route.params.id}`; - } - return props.customUrl; -}); const searchRightDataKey = computed(() => { if (!props.searchDataKey) return route.name; return props.searchDataKey; }); + const arrayData = useArrayData(props.dataKey, { - url: url.value, - filter: props.filter, + url: props.url, + userFilter: props.filter, + oneRecord: true, }); onBeforeMount(async () => { try { - if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; - await arrayData.fetch({ append: false, updateRouter: false }); + await fetch(route.params.id); } catch { const { matched: matches } = router.currentRoute.value; const { path } = matches.at(-1); @@ -49,13 +44,17 @@ onBeforeMount(async () => { } }); -if (props.baseUrl) { - onBeforeRouteUpdate(async (to, from) => { - if (to.params.id !== from.params.id) { - arrayData.store.url = `${props.baseUrl}/${to.params.id}`; - await arrayData.fetch({ append: false, updateRouter: false }); - } - }); +onBeforeRouteUpdate(async (to, from) => { + const id = to.params.id; + if (id !== from.params.id) await fetch(id, true); +}); + +async function fetch(id, append = false) { + const regex = /\/(\d+)/; + if (props.idInWhere) arrayData.store.filter.where = { id }; + else if (!regex.test(props.url)) arrayData.store.url = `${props.url}/${id}`; + else arrayData.store.url = props.url.replace(regex, `/${id}`); + await arrayData.fetch({ append, updateRouter: false }); } </script> <template> @@ -83,7 +82,7 @@ if (props.baseUrl) { <QPage> <VnSubToolbar /> <div :class="[useCardSize(), $attrs.class]"> - <RouterView :key="route.path" /> + <RouterView :key="$route.path" /> </div> </QPage> </QPageContainer> diff --git a/src/components/common/VnCardBeta.vue b/src/components/common/VnCardBeta.vue index f237a300c..7c82316dc 100644 --- a/src/components/common/VnCardBeta.vue +++ b/src/components/common/VnCardBeta.vue @@ -1,6 +1,6 @@ <script setup> -import { onBeforeMount, computed } from 'vue'; -import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router'; +import { onBeforeMount } from 'vue'; +import { useRouter, onBeforeRouteUpdate } from 'vue-router'; import { useArrayData } from 'src/composables/useArrayData'; import { useStateStore } from 'stores/useStateStore'; import useCardSize from 'src/composables/useCardSize'; @@ -9,10 +9,9 @@ import VnSubToolbar from '../ui/VnSubToolbar.vue'; const props = defineProps({ dataKey: { type: String, required: true }, - baseUrl: { type: String, default: undefined }, - customUrl: { type: String, default: undefined }, + url: { type: String, default: undefined }, + idInWhere: { type: Boolean, default: false }, filter: { type: Object, default: () => {} }, - userFilter: { type: Object, default: () => {} }, descriptor: { type: Object, required: true }, filterPanel: { type: Object, default: undefined }, searchDataKey: { type: String, default: undefined }, @@ -21,46 +20,42 @@ const props = defineProps({ }); const stateStore = useStateStore(); -const route = useRoute(); const router = useRouter(); -const url = computed(() => { - if (props.baseUrl) { - return `${props.baseUrl}/${route.params.id}`; - } - return props.customUrl; -}); - const arrayData = useArrayData(props.dataKey, { - url: url.value, - filter: props.filter, - userFilter: props.userFilter, + url: props.url, + userFilter: props.filter, + oneRecord: true, }); onBeforeMount(async () => { + const route = router.currentRoute.value; try { - if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; - await arrayData.fetch({ append: false, updateRouter: false }); + await fetch(route.params.id); } catch { - const { matched: matches } = router.currentRoute.value; + const { matched: matches } = route; const { path } = matches.at(-1); router.push({ path: path.replace(/:id.*/, '') }); } }); -if (props.baseUrl) { - onBeforeRouteUpdate(async (to, from) => { - if (hasRouteParam(to.params)) { - const { matched } = router.currentRoute.value; - const { name } = matched.at(-3); - if (name) { - router.push({ name, params: to.params }); - } +onBeforeRouteUpdate(async (to, from) => { + if (hasRouteParam(to.params)) { + const { matched } = router.currentRoute.value; + const { name } = matched.at(-3); + if (name) { + router.push({ name, params: to.params }); } - if (to.params.id !== from.params.id) { - arrayData.store.url = `${props.baseUrl}/${to.params.id}`; - await arrayData.fetch({ append: false, updateRouter: false }); - } - }); + } + const id = to.params.id; + if (id !== from.params.id) await fetch(id, true); +}); + +async function fetch(id, append = false) { + const regex = /\/(\d+)/; + if (props.idInWhere) arrayData.store.filter.where = { id }; + else if (!regex.test(props.url)) arrayData.store.url = `${props.url}/${id}`; + else arrayData.store.url = props.url.replace(regex, `/${id}`); + await arrayData.fetch({ append, updateRouter: false }); } function hasRouteParam(params, valueToCheck = ':addressId') { return Object.values(params).includes(valueToCheck); @@ -74,6 +69,6 @@ function hasRouteParam(params, valueToCheck = ':addressId') { </Teleport> <VnSubToolbar /> <div :class="[useCardSize(), $attrs.class]"> - <RouterView :key="route.path" /> + <RouterView :key="$route.path" /> </div> </template> diff --git a/src/components/common/VnCheckbox.vue b/src/components/common/VnCheckbox.vue new file mode 100644 index 000000000..27131d45e --- /dev/null +++ b/src/components/common/VnCheckbox.vue @@ -0,0 +1,43 @@ +<script setup> +import { computed } from 'vue'; + +const model = defineModel({ type: [Number, Boolean] }); +const $props = defineProps({ + info: { + type: String, + default: null, + }, +}); + +const checkboxModel = computed({ + get() { + if (typeof model.value === 'number') { + return model.value !== 0; + } + return model.value; + }, + set(value) { + if (typeof model.value === 'number') { + model.value = value ? 1 : 0; + } else { + model.value = value; + } + }, +}); +</script> +<template> + <div> + <QCheckbox v-bind="$attrs" v-on="$attrs" v-model="checkboxModel" /> + <QIcon + v-if="info" + v-bind="$attrs" + class="cursor-info q-ml-sm" + name="info" + size="sm" + > + <QTooltip> + {{ info }} + </QTooltip> + </QIcon> + </div> +</template> diff --git a/src/components/common/VnColor.vue b/src/components/common/VnColor.vue new file mode 100644 index 000000000..8a5a787b0 --- /dev/null +++ b/src/components/common/VnColor.vue @@ -0,0 +1,32 @@ +<script setup> +const $props = defineProps({ + colors: { + type: String, + default: '{"value": []}', + }, +}); + +const colorArray = JSON.parse($props.colors)?.value; +const maxHeight = 30; +const colorHeight = maxHeight / colorArray?.length; +</script> +<template> + <div v-if="colors" class="color-div" :style="{ height: `${maxHeight}px` }"> + <div + v-for="(color, index) in colorArray" + :key="index" + :style="{ + backgroundColor: `#${color}`, + height: `${colorHeight}px`, + }" + > + + </div> + </div> +</template> +<style scoped> +.color-div { + display: flex; + flex-direction: column; +} +</style> diff --git a/src/components/common/VnComponent.vue b/src/components/common/VnComponent.vue index 580bcf348..a9e1c8cff 100644 --- a/src/components/common/VnComponent.vue +++ b/src/components/common/VnComponent.vue @@ -17,6 +17,8 @@ const $props = defineProps({ }, }); +const emit = defineEmits(['blur']); + const componentArray = computed(() => { if (typeof $props.prop === 'object') return [$props.prop]; return $props.prop; @@ -46,7 +48,8 @@ function toValueAttrs(attrs) { <span v-for="toComponent of componentArray" :key="toComponent.name" - class="column flex-center fit" + class="column fit" + :class="toComponent?.component == 'checkbox' ? 'flex-center' : ''" > <component v-if="toComponent?.component" @@ -54,6 +57,7 @@ function toValueAttrs(attrs) { v-bind="mix(toComponent).attrs" v-on="mix(toComponent).event ?? {}" v-model="model" + @blur="emit('blur')" /> </span> </template> diff --git a/src/components/common/VnDmsList.vue b/src/components/common/VnDmsList.vue index 36c87bab0..424781a26 100644 --- a/src/components/common/VnDmsList.vue +++ b/src/components/common/VnDmsList.vue @@ -17,7 +17,7 @@ import { useSession } from 'src/composables/useSession'; const route = useRoute(); const quasar = useQuasar(); const { t } = useI18n(); -const rows = ref(); +const rows = ref([]); const dmsRef = ref(); const formDialog = ref({}); const token = useSession().getTokenMultimedia(); @@ -389,6 +389,14 @@ defineExpose({ </div> </template> </QTable> + <div + v-else + class="info-row q-pa-md text-center" + > + <h5> + {{ t('No data to display') }} + </h5> + </div> </template> </VnPaginate> <QDialog v-model="formDialog.show"> @@ -405,7 +413,7 @@ defineExpose({ fab color="primary" icon="add" - shortcut="+" + v-shortcut @click="showFormDialog()" class="fill-icon" > diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index 78f08a479..aeb4a31fd 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -11,6 +11,7 @@ const emit = defineEmits([ 'update:options', 'keyup.enter', 'remove', + 'blur', ]); const $props = defineProps({ @@ -136,6 +137,7 @@ const handleUppercase = () => { :type="$attrs.type" :class="{ required: isRequired }" @keyup.enter="emit('keyup.enter')" + @blur="emit('blur')" @keydown="handleKeydown" :clearable="false" :rules="mixinRules" @@ -143,7 +145,7 @@ const handleUppercase = () => { hide-bottom-space :data-cy="$attrs.dataCy ?? $attrs.label + '_input'" > - <template #prepend> + <template #prepend v-if="$slots.prepend"> <slot name="prepend" /> </template> <template #append> @@ -168,11 +170,11 @@ const handleUppercase = () => { } " ></QIcon> - + <QIcon name="match_case" size="xs" - v-if="!$attrs.disabled && !($attrs.readonly) && $props.uppercase" + v-if="!$attrs.disabled && !$attrs.readonly && $props.uppercase" @click="handleUppercase" class="uppercase-icon" > @@ -180,7 +182,7 @@ const handleUppercase = () => { {{ t('Convert to uppercase') }} </QTooltip> </QIcon> - + <slot name="append" v-if="$slots.append && !$attrs.disabled" /> <QIcon v-if="info" name="info"> <QTooltip max-width="350px"> @@ -194,13 +196,15 @@ const handleUppercase = () => { <style> .uppercase-icon { - transition: color 0.3s, transform 0.2s; - cursor: pointer; + transition: + color 0.3s, + transform 0.2s; + cursor: pointer; } .uppercase-icon:hover { - color: #ed9937; - transform: scale(1.2); + color: #ed9937; + transform: scale(1.2); } </style> <i18n> @@ -214,4 +218,4 @@ const handleUppercase = () => { maxLength: El valor excede los {value} carácteres inputMax: Debe ser menor a {value} Convert to uppercase: Convertir a mayúsculas -</i18n> \ No newline at end of file +</i18n> diff --git a/src/components/common/VnInputDate.vue b/src/components/common/VnInputDate.vue index a8888aad8..73c825e1e 100644 --- a/src/components/common/VnInputDate.vue +++ b/src/components/common/VnInputDate.vue @@ -42,7 +42,7 @@ const formattedDate = computed({ if (value.at(2) == '/') value = value.split('/').reverse().join('/'); value = date.formatDate( new Date(value).toISOString(), - 'YYYY-MM-DDTHH:mm:ss.SSSZ' + 'YYYY-MM-DDTHH:mm:ss.SSSZ', ); } const [year, month, day] = value.split('-').map((e) => parseInt(e)); @@ -55,7 +55,7 @@ const formattedDate = computed({ orgDate.getHours(), orgDate.getMinutes(), orgDate.getSeconds(), - orgDate.getMilliseconds() + orgDate.getMilliseconds(), ); } } @@ -64,7 +64,7 @@ const formattedDate = computed({ }); const popupDate = computed(() => - model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value + model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value, ); onMounted(() => { // fix quasar bug @@ -73,7 +73,7 @@ onMounted(() => { watch( () => model.value, (val) => (formattedDate.value = val), - { immediate: true } + { immediate: true }, ); const styleAttrs = computed(() => { diff --git a/src/components/common/VnInputNumber.vue b/src/components/common/VnInputNumber.vue index 165cfae3d..274f78b21 100644 --- a/src/components/common/VnInputNumber.vue +++ b/src/components/common/VnInputNumber.vue @@ -8,6 +8,7 @@ defineProps({ }); const model = defineModel({ type: [Number, String] }); +const emit = defineEmits(['blur']); </script> <template> <VnInput @@ -24,5 +25,6 @@ const model = defineModel({ type: [Number, String] }); model = parseFloat(val).toFixed(decimalPlaces); } " + @blur="emit('blur')" /> </template> diff --git a/src/components/common/VnPopupProxy.vue b/src/components/common/VnPopupProxy.vue new file mode 100644 index 000000000..f386bfff8 --- /dev/null +++ b/src/components/common/VnPopupProxy.vue @@ -0,0 +1,38 @@ +<script setup> +import { ref } from 'vue'; + +defineProps({ + label: { + type: String, + default: '', + }, + icon: { + type: String, + required: true, + default: null, + }, + color: { + type: String, + default: 'primary', + }, + tooltip: { + type: String, + default: null, + }, +}); +const popupProxyRef = ref(null); +</script> + +<template> + <QBtn :color="$props.color" :icon="$props.icon" :label="$t($props.label)"> + <template #default> + <slot name="extraIcon"></slot> + <QPopupProxy ref="popupProxyRef" style="max-width: none"> + <QCard> + <slot :popup="popupProxyRef"></slot> + </QCard> + </QPopupProxy> + <QTooltip>{{ $t($props.tooltip) }}</QTooltip> + </template> + </QBtn> +</template> diff --git a/src/components/common/VnSection.vue b/src/components/common/VnSection.vue index ef65b841f..4bd17124f 100644 --- a/src/components/common/VnSection.vue +++ b/src/components/common/VnSection.vue @@ -106,7 +106,14 @@ function checkIsMain() { :data-key="dataKey" :array-data="arrayData" :columns="columns" - /> + > + <template #moreFilterPanel="{ params, orders, searchFn }"> + <slot + name="moreFilterPanel" + v-bind="{ params, orders, searchFn }" + /> + </template> + </VnTableFilter> </slot> </template> </RightAdvancedMenu> diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 95fe80a69..339f90e0e 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -10,7 +10,12 @@ const emit = defineEmits(['update:modelValue', 'update:options', 'remove']); const $attrs = useAttrs(); const { t } = useI18n(); -const { isRequired, requiredFieldRule } = useRequired($attrs); +const isRequired = computed(() => { + return useRequired($attrs).isRequired; +}); +const requiredFieldRule = computed(() => { + return useRequired($attrs).requiredFieldRule; +}); const $props = defineProps({ modelValue: { @@ -166,7 +171,8 @@ onMounted(() => { }); const arrayDataKey = - $props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label); + $props.dataKey ?? + ($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label)); const arrayData = useArrayData(arrayDataKey, { url: $props.url, @@ -215,7 +221,7 @@ async function fetchFilter(val) { optionFilterValue.value ?? (new RegExp(/\d/g).test(val) ? optionValue.value - : optionFilter.value ?? optionLabel.value); + : (optionFilter.value ?? optionLabel.value)); let defaultWhere = {}; if ($props.filterOptions.length) { @@ -234,7 +240,7 @@ async function fetchFilter(val) { const { data } = await arrayData.applyFilter( { filter: filterOptions }, - { updateRouter: false } + { updateRouter: false }, ); setOptions(data); return data; @@ -267,7 +273,7 @@ async function filterHandler(val, update) { ref.setOptionIndex(-1); ref.moveOptionSelection(1, true); } - } + }, ); } @@ -303,7 +309,7 @@ function handleKeyDown(event) { if (inputValue) { const matchingOption = myOptions.value.find( (option) => - option[optionLabel.value].toLowerCase() === inputValue.toLowerCase() + option[optionLabel.value].toLowerCase() === inputValue.toLowerCase(), ); if (matchingOption) { @@ -315,11 +321,11 @@ function handleKeyDown(event) { } const focusableElements = document.querySelectorAll( - 'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])' + 'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])', ); const currentIndex = Array.prototype.indexOf.call( focusableElements, - event.target + event.target, ); if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) { focusableElements[currentIndex + 1].focus(); diff --git a/src/components/common/VnSelectCache.vue b/src/components/common/VnSelectCache.vue index 29cf22dc5..f0f3357f6 100644 --- a/src/components/common/VnSelectCache.vue +++ b/src/components/common/VnSelectCache.vue @@ -14,7 +14,7 @@ const $props = defineProps({ }, }); const options = ref([]); - +const emit = defineEmits(['blur']); onBeforeMount(async () => { const { url, optionValue, optionLabel } = useAttrs(); const findBy = $props.find ?? url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1); @@ -35,5 +35,5 @@ onBeforeMount(async () => { }); </script> <template> - <VnSelect v-bind="$attrs" :options="$attrs.options ?? options" /> + <VnSelect v-bind="$attrs" :options="$attrs.options ?? options" @blur="emit('blur')" /> </template> diff --git a/src/components/common/VnSelectDialog.vue b/src/components/common/VnSelectDialog.vue index a4cd0011d..41730b217 100644 --- a/src/components/common/VnSelectDialog.vue +++ b/src/components/common/VnSelectDialog.vue @@ -37,7 +37,6 @@ const isAllowedToCreate = computed(() => { defineExpose({ vnSelectDialogRef: select }); </script> - <template> <VnSelect ref="select" @@ -67,7 +66,6 @@ defineExpose({ vnSelectDialogRef: select }); </template> </VnSelect> </template> - <style lang="scss" scoped> .default-icon { cursor: pointer; diff --git a/src/components/common/VnSelectSupplier.vue b/src/components/common/VnSelectSupplier.vue index f86db4f2d..5b52ae75b 100644 --- a/src/components/common/VnSelectSupplier.vue +++ b/src/components/common/VnSelectSupplier.vue @@ -1,9 +1,7 @@ <script setup> -import { computed } from 'vue'; import VnSelect from 'components/common/VnSelect.vue'; const model = defineModel({ type: [String, Number, Object] }); -const url = 'Suppliers'; </script> <template> @@ -11,11 +9,13 @@ const url = 'Suppliers'; :label="$t('globals.supplier')" v-bind="$attrs" v-model="model" - :url="url" + url="Suppliers" option-value="id" option-label="nickname" :fields="['id', 'name', 'nickname', 'nif']" + :filter-options="['id', 'name', 'nickname', 'nif']" sort-by="name ASC" + data-cy="vnSupplierSelect" > <template #option="scope"> <QItem v-bind="scope.itemProps"> diff --git a/src/components/common/VnSelectTravelExtended.vue b/src/components/common/VnSelectTravelExtended.vue new file mode 100644 index 000000000..46538f5f9 --- /dev/null +++ b/src/components/common/VnSelectTravelExtended.vue @@ -0,0 +1,50 @@ +<script setup> +import VnSelectDialog from './VnSelectDialog.vue'; +import FilterTravelForm from 'src/components/FilterTravelForm.vue'; +import { useI18n } from 'vue-i18n'; +import { toDate } from 'src/filters'; +const { t } = useI18n(); + +const $props = defineProps({ + data: { + type: Object, + required: true, + }, + onFilterTravelSelected: { + type: Function, + required: true, + }, +}); +</script> +<template> + <VnSelectDialog + :label="t('entry.basicData.travel')" + v-bind="$attrs" + url="Travels/filter" + :fields="['id', 'warehouseInName']" + option-value="id" + option-label="warehouseInName" + map-options + hide-selected + :required="true" + action-icon="filter_alt" + :roles-allowed-to-create="['buyer']" + > + <template #form> + <FilterTravelForm @travel-selected="onFilterTravelSelected(data, $event)" /> + </template> + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.agencyModeName }} - + {{ scope.opt?.warehouseInName }} + ({{ toDate(scope.opt?.shipped) }}) → + {{ scope.opt?.warehouseOutName }} + ({{ toDate(scope.opt?.landed) }}) + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelectDialog> +</template> diff --git a/src/components/common/__tests__/VnNotes.spec.js b/src/components/common/__tests__/VnNotes.spec.js index 8f24a7f14..2603bf03c 100644 --- a/src/components/common/__tests__/VnNotes.spec.js +++ b/src/components/common/__tests__/VnNotes.spec.js @@ -1,51 +1,78 @@ -import { describe, it, expect, vi, beforeAll, afterEach, beforeEach } from 'vitest'; +import { + describe, + it, + expect, + vi, + beforeAll, + afterEach, + beforeEach, + afterAll, +} from 'vitest'; import { createWrapper, axios } from 'app/test/vitest/helper'; import VnNotes from 'src/components/ui/VnNotes.vue'; +import vnDate from 'src/boot/vnDate'; describe('VnNotes', () => { let vm; let wrapper; let spyFetch; let postMock; - let expectedBody; - const mockData= {name: 'Tony', lastName: 'Stark', text: 'Test Note', observationTypeFk: 1}; - - function generateExpectedBody() { - expectedBody = {...vm.$props.body, ...{ text: vm.newNote.text, observationTypeFk: vm.newNote.observationTypeFk }}; - } - - async function setTestParams(text, observationType, type){ - vm.newNote.text = text; - vm.newNote.observationTypeFk = observationType; - wrapper.setProps({ selectType: type }); - } - - beforeAll(async () => { - vi.spyOn(axios, 'get').mockReturnValue({ data: [] }); - + let patchMock; + let expectedInsertBody; + let expectedUpdateBody; + const defaultOptions = { + url: '/test', + body: { name: 'Tony', lastName: 'Stark' }, + selectType: false, + saveUrl: null, + justInput: false, + }; + function generateWrapper( + options = defaultOptions, + text = null, + observationType = null, + ) { + vi.spyOn(axios, 'get').mockResolvedValue({ data: [] }); wrapper = createWrapper(VnNotes, { - propsData: { - url: '/test', - body: { name: 'Tony', lastName: 'Stark' }, - } + propsData: options, }); wrapper = wrapper.wrapper; vm = wrapper.vm; - }); + vm.newNote.text = text; + vm.newNote.observationTypeFk = observationType; + } + + function createSpyFetch() { + spyFetch = vi.spyOn(vm.$refs.vnPaginateRef, 'fetch'); + } + + function generateExpectedBody() { + expectedInsertBody = { + ...vm.$props.body, + ...{ text: vm.newNote.text, observationTypeFk: vm.newNote.observationTypeFk }, + }; + expectedUpdateBody = { ...vm.$props.body, ...{ notes: vm.newNote.text } }; + } beforeEach(() => { - postMock = vi.spyOn(axios, 'post').mockResolvedValue(mockData); - spyFetch = vi.spyOn(vm.vnPaginateRef, 'fetch').mockImplementation(() => vi.fn()); + postMock = vi.spyOn(axios, 'post'); + patchMock = vi.spyOn(axios, 'patch'); }); afterEach(() => { vi.clearAllMocks(); - expectedBody = {}; + expectedInsertBody = {}; + expectedUpdateBody = {}; + }); + + afterAll(() => { + vi.restoreAllMocks(); }); describe('insert', () => { - it('should not call axios.post and vnPaginateRef.fetch if newNote.text is null', async () => { - await setTestParams( null, null, true ); + it('should not call axios.post and vnPaginateRef.fetch when newNote.text is null', async () => { + generateWrapper({ selectType: true }); + createSpyFetch(); await vm.insert(); @@ -53,8 +80,9 @@ describe('VnNotes', () => { expect(spyFetch).not.toHaveBeenCalled(); }); - it('should not call axios.post and vnPaginateRef.fetch if newNote.text is empty', async () => { - await setTestParams( "", null, false ); + it('should not call axios.post and vnPaginateRef.fetch when newNote.text is empty', async () => { + generateWrapper(null, ''); + createSpyFetch(); await vm.insert(); @@ -62,8 +90,9 @@ describe('VnNotes', () => { expect(spyFetch).not.toHaveBeenCalled(); }); - it('should not call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is true', async () => { - await setTestParams( "Test Note", null, true ); + it('should not call axios.post and vnPaginateRef.fetch when observationTypeFk is null and selectType is true', async () => { + generateWrapper({ selectType: true }, 'Test Note'); + createSpyFetch(); await vm.insert(); @@ -71,37 +100,57 @@ describe('VnNotes', () => { expect(spyFetch).not.toHaveBeenCalled(); }); - it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is false', async () => { - await setTestParams( "Test Note", null, false ); - + it('should call axios.post and vnPaginateRef.fetch when observationTypeFk is missing and selectType is false', async () => { + generateWrapper(null, 'Test Note'); + createSpyFetch(); generateExpectedBody(); await vm.insert(); - expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody); - expect(spyFetch).toHaveBeenCalled(); - }); - - it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is setted and selectType is false', async () => { - await setTestParams( "Test Note", 1, false ); - - generateExpectedBody(); - - await vm.insert(); - - expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody); + expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedInsertBody); expect(spyFetch).toHaveBeenCalled(); }); it('should call axios.post and vnPaginateRef.fetch when newNote is valid', async () => { - await setTestParams( "Test Note", 1, true ); - + generateWrapper({ selectType: true }, 'Test Note', 1); + createSpyFetch(); generateExpectedBody(); - + await vm.insert(); - expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody); + expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedInsertBody); expect(spyFetch).toHaveBeenCalled(); }); }); -}); \ No newline at end of file + + describe('update', () => { + it('should call axios.patch with saveUrl when saveUrl is set and justInput is true', async () => { + generateWrapper({ + url: '/business', + justInput: true, + saveUrl: '/saveUrlTest', + }); + generateExpectedBody(); + + await vm.update(); + + expect(patchMock).toHaveBeenCalledWith(vm.$props.saveUrl, expectedUpdateBody); + }); + + it('should call axios.patch with url when saveUrl is not set and justInput is true', async () => { + generateWrapper({ + url: '/business', + body: { workerFk: 1110 }, + justInput: true, + }); + generateExpectedBody(); + + await vm.update(); + + expect(patchMock).toHaveBeenCalledWith( + `${vm.$props.url}/${vm.$props.body.workerFk}`, + expectedUpdateBody, + ); + }); + }); +}); diff --git a/src/components/ui/CardDescriptor.vue b/src/components/ui/CardDescriptor.vue index 43dc15e9b..e6e7e6fa0 100644 --- a/src/components/ui/CardDescriptor.vue +++ b/src/components/ui/CardDescriptor.vue @@ -6,6 +6,7 @@ import { useArrayData } from 'composables/useArrayData'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useState } from 'src/composables/useState'; import { useRoute } from 'vue-router'; +import { useClipboard } from 'src/composables/useClipboard'; import VnMoreOptions from './VnMoreOptions.vue'; const $props = defineProps({ @@ -29,10 +30,6 @@ const $props = defineProps({ type: String, default: null, }, - module: { - type: String, - default: null, - }, summary: { type: Object, default: null, @@ -46,6 +43,7 @@ const $props = defineProps({ const state = useState(); const route = useRoute(); const { t } = useI18n(); +const { copyText } = useClipboard(); const { viewSummary } = useSummaryDialog(); let arrayData; let store; @@ -57,12 +55,13 @@ defineExpose({ getData }); onBeforeMount(async () => { arrayData = useArrayData($props.dataKey, { url: $props.url, - filter: $props.filter, + userFilter: $props.filter, skip: 0, + oneRecord: true, }); store = arrayData.store; entity = computed(() => { - const data = (Array.isArray(store.data) ? store.data[0] : store.data) ?? {}; + const data = store.data ?? {}; if (data) emit('onFetch', data); return data; }); @@ -73,7 +72,7 @@ onBeforeMount(async () => { () => [$props.url, $props.filter], async () => { if (!isSameDataKey.value) await getData(); - } + }, ); }); @@ -84,7 +83,7 @@ async function getData() { try { const { data } = await arrayData.fetch({ append: false, updateRouter: false }); state.set($props.dataKey, data); - emit('onFetch', Array.isArray(data) ? data[0] : data); + emit('onFetch', data); } finally { isLoading.value = false; } @@ -102,13 +101,21 @@ function getValueFromPath(path) { return current; } +function copyIdText(id) { + copyText(id, { + component: { + copyValue: id, + }, + }); +} + 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 + : route.matched[1].children[0].redirect, ); </script> @@ -147,7 +154,9 @@ const toModule = computed(() => {{ t('components.smartCard.openSummary') }} </QTooltip> </QBtn> - <RouterLink :to="{ name: `${module}Summary`, params: { id: entity.id } }"> + <RouterLink + :to="{ name: `${dataKey}Summary`, params: { id: entity.id } }" + > <QBtn class="link" color="white" @@ -183,9 +192,22 @@ const toModule = computed(() => </slot> </div> </QItemLabel> - <QItem dense> + <QItem> <QItemLabel class="subtitle" caption> #{{ getValueFromPath(subtitle) ?? entity.id }} + <QBtn + round + flat + dense + size="sm" + icon="content_copy" + color="primary" + @click.stop="copyIdText(entity.id)" + > + <QTooltip> + {{ t('globals.copyId') }} + </QTooltip> + </QBtn> </QItemLabel> </QItem> </QList> @@ -293,3 +315,11 @@ const toModule = computed(() => } } </style> +<i18n> + en: + globals: + copyId: Copy ID + es: + globals: + copyId: Copiar ID +</i18n> diff --git a/src/components/ui/CardSummary.vue b/src/components/ui/CardSummary.vue index c815b8e16..6a61994c1 100644 --- a/src/components/ui/CardSummary.vue +++ b/src/components/ui/CardSummary.vue @@ -40,9 +40,10 @@ const arrayData = useArrayData(props.dataKey, { filter: props.filter, userFilter: props.userFilter, skip: 0, + oneRecord: true, }); const { store } = arrayData; -const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data)); +const entity = computed(() => store.data); const isLoading = ref(false); defineExpose({ @@ -61,7 +62,7 @@ async function fetch() { store.filter = props.filter ?? {}; isLoading.value = true; const { data } = await arrayData.fetch({ append: false, updateRouter: false }); - emit('onFetch', Array.isArray(data) ? data[0] : data); + emit('onFetch', data); isLoading.value = false; } </script> @@ -208,4 +209,13 @@ async function fetch() { .summaryHeader { color: $white; } + +.cardSummary :deep(.q-card__section[content]) { + display: flex; + flex-wrap: wrap; + padding: 0; + > * { + flex: 1; + } +} </style> diff --git a/src/components/ui/SkeletonDescriptor.vue b/src/components/ui/SkeletonDescriptor.vue index 9679751f5..f9188221a 100644 --- a/src/components/ui/SkeletonDescriptor.vue +++ b/src/components/ui/SkeletonDescriptor.vue @@ -1,53 +1,32 @@ +<script setup> +defineProps({ + hasImage: { + type: Boolean, + default: false, + }, +}); +</script> <template> - <div id="descriptor-skeleton"> + <div id="descriptor-skeleton" class="bg-vn-page"> <div class="row justify-between q-pa-sm"> - <QSkeleton square size="40px" /> - <QSkeleton square size="40px" /> - <QSkeleton square height="40px" width="20px" /> + <QSkeleton square size="30px" v-for="i in 3" :key="i" /> </div> - <div class="col justify-between q-pa-sm q-gutter-y-xs"> - <QSkeleton square height="40px" width="150px" /> - <QSkeleton square height="30px" width="70px" /> + <div class="q-pa-xs" v-if="hasImage"> + <QSkeleton square height="200px" width="100%" /> </div> - <div class="col q-pl-sm q-pa-sm q-mb-md"> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> - </div> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> - </div> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> - </div> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> - </div> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> - </div> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> + <div class="col justify-between q-pa-md q-gutter-y-xs"> + <QSkeleton square height="25px" width="150px" /> + <QSkeleton square height="15px" width="70px" /> + </div> + <div class="q-pl-sm q-pa-sm q-mb-md"> + <div class="row q-gutter-x-sm q-pa-none q-ma-none" v-for="i in 5" :key="i"> + <QSkeleton type="text" square height="20px" width="30%" /> + <QSkeleton type="text" square height="20px" width="60%" /> </div> </div> - <QCardActions> - <QSkeleton size="40px" /> - <QSkeleton size="40px" /> - <QSkeleton size="40px" /> - <QSkeleton size="40px" /> - <QSkeleton size="40px" /> + <QCardActions class="q-gutter-x-sm justify-between"> + <QSkeleton size="40px" v-for="i in 5" :key="i" /> </QCardActions> </div> </template> - -<style lang="scss" scoped> -#descriptor-skeleton .q-card__actions { - justify-content: space-between; -} -</style> diff --git a/src/components/ui/VnConfirm.vue b/src/components/ui/VnConfirm.vue index a02b56bdb..c6f539879 100644 --- a/src/components/ui/VnConfirm.vue +++ b/src/components/ui/VnConfirm.vue @@ -82,7 +82,7 @@ function cancel() { @click="cancel()" /> </QCardSection> - <QCardSection class="q-pb-none"> + <QCardSection class="q-pb-none" data-cy="VnConfirm_message"> <span v-if="message !== false" v-html="message" /> </QCardSection> <QCardSection class="row items-center q-pt-none"> @@ -95,6 +95,7 @@ function cancel() { :disable="isLoading" flat @click="cancel()" + data-cy="VnConfirm_cancel" /> <QBtn :label="t('globals.confirm')" diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index 93f069cc6..d6b525dc8 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -114,7 +114,7 @@ async function clearFilters() { arrayData.resetPagination(); // Filtrar los params no removibles const removableFilters = Object.keys(userParams.value).filter((param) => - $props.unremovableParams.includes(param) + $props.unremovableParams.includes(param), ); const newParams = {}; // Conservar solo los params que no son removibles @@ -162,13 +162,13 @@ const formatTags = (tags) => { const tags = computed(() => { const filteredTags = tagsList.value.filter( - (tag) => !($props.customTags || []).includes(tag.label) + (tag) => !($props.customTags || []).includes(tag.label), ); return formatTags(filteredTags); }); const customTags = computed(() => - tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label)) + tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label)), ); async function remove(key) { @@ -188,10 +188,13 @@ function formatValue(value) { const getLocale = (label) => { const param = label.split('.').at(-1); const globalLocale = `globals.params.${param}`; + const moduleName = route.meta.moduleName; + const moduleLocale = `${moduleName.toLowerCase()}.${param}`; if (te(globalLocale)) return t(globalLocale); - else if (te(t(`params.${param}`))); + else if (te(moduleLocale)) return t(moduleLocale); else { - const camelCaseModuleName = route.meta.moduleName.charAt(0).toLowerCase() + route.meta.moduleName.slice(1); + const camelCaseModuleName = + moduleName.charAt(0).toLowerCase() + moduleName.slice(1); return t(`${camelCaseModuleName}.params.${param}`); } }; @@ -290,6 +293,9 @@ const getLocale = (label) => { /> </template> <style scoped lang="scss"> +.q-field__label.no-pointer-events.absolute.ellipsis { + margin-left: 6px !important; +} .list { width: 256px; } diff --git a/src/components/ui/VnMoreOptions.vue b/src/components/ui/VnMoreOptions.vue index 39e84be2b..8a1c7a0f2 100644 --- a/src/components/ui/VnMoreOptions.vue +++ b/src/components/ui/VnMoreOptions.vue @@ -11,7 +11,7 @@ <QTooltip> {{ $t('components.cardDescriptor.moreOptions') }} </QTooltip> - <QMenu ref="menuRef"> + <QMenu ref="menuRef" data-cy="descriptor-more-opts-menu"> <QList> <slot name="menu" :menu-ref="$refs.menuRef" /> </QList> diff --git a/src/components/ui/VnNotes.vue b/src/components/ui/VnNotes.vue index 1690a94ba..ec6289a67 100644 --- a/src/components/ui/VnNotes.vue +++ b/src/components/ui/VnNotes.vue @@ -1,6 +1,6 @@ <script setup> import axios from 'axios'; -import { ref, reactive } from 'vue'; +import { ref, reactive, useAttrs, computed } from 'vue'; import { onBeforeRouteLeave } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; @@ -16,12 +16,27 @@ import VnSelect from 'components/common/VnSelect.vue'; import FetchData from 'components/FetchData.vue'; import VnInput from 'components/common/VnInput.vue'; +const emit = defineEmits(['onFetch']); + +const originalAttrs = useAttrs(); + +const $attrs = computed(() => { + const { style, ...rest } = originalAttrs; + return rest; +}); + +const isRequired = computed(() => { + return Object.keys($attrs).includes('required') +}); + const $props = defineProps({ url: { type: String, default: null }, + saveUrl: {type: String, default: null}, filter: { type: Object, default: () => {} }, body: { type: Object, default: () => {} }, addNote: { type: Boolean, default: false }, selectType: { type: Boolean, default: false }, + justInput: { type: Boolean, default: false }, }); const { t } = useI18n(); @@ -29,6 +44,13 @@ const quasar = useQuasar(); const newNote = reactive({ text: null, observationTypeFk: null }); const observationTypes = ref([]); const vnPaginateRef = ref(); +let originalText; + +function handleClick(e) { + if (e.shiftKey && e.key === 'Enter') return; + if ($props.justInput) confirmAndUpdate(); + else insert(); +} async function insert() { if (!newNote.text || ($props.selectType && !newNote.observationTypeFk)) return; @@ -41,8 +63,36 @@ async function insert() { await axios.post($props.url, newBody); await vnPaginateRef.value.fetch(); } + +function confirmAndUpdate() { + if(!newNote.text && originalText) + quasar + .dialog({ + component: VnConfirm, + componentProps: { + title: t('New note is empty'), + message: t('Are you sure remove this note?'), + }, + }) + .onOk(update) + .onCancel(() => { + newNote.text = originalText; + }); + else update(); +} + +async function update() { + originalText = newNote.text; + const body = $props.body; + const newBody = { + ...body, + ...{ notes: newNote.text }, + }; + await axios.patch(`${$props.saveUrl ?? `${$props.url}/${$props.body.workerFk}`}`, newBody); +} + onBeforeRouteLeave((to, from, next) => { - if (newNote.text) + if ((newNote.text && !$props.justInput) || (newNote.text !== originalText) && $props.justInput) quasar.dialog({ component: VnConfirm, componentProps: { @@ -53,6 +103,13 @@ onBeforeRouteLeave((to, from, next) => { }); else next(); }); + +function fetchData([ data ]) { + newNote.text = data?.notes; + originalText = data?.notes; + emit('onFetch', data); +} + </script> <template> <FetchData @@ -62,8 +119,19 @@ onBeforeRouteLeave((to, from, next) => { auto-load @on-fetch="(data) => (observationTypes = data)" /> - <QCard class="q-pa-xs q-mb-lg full-width" v-if="$props.addNote"> - <QCardSection horizontal> + <FetchData + v-if="justInput" + :url="url" + :filter="filter" + @on-fetch="fetchData" + auto-load + /> + <QCard + class="q-pa-xs q-mb-lg full-width" + :class="{ 'just-input': $props.justInput }" + v-if="$props.addNote || $props.justInput" + > + <QCardSection horizontal v-if="!$props.justInput"> {{ t('New note') }} </QCardSection> <QCardSection class="q-px-xs q-my-none q-py-none"> @@ -75,19 +143,19 @@ onBeforeRouteLeave((to, from, next) => { v-model="newNote.observationTypeFk" option-label="description" style="flex: 0.15" - :required="true" + :required="isRequired" @keyup.enter.stop="insert" /> <VnInput v-model.trim="newNote.text" type="textarea" - :label="t('Add note here...')" + :label="$props.justInput && newNote.text ? '' : t('Add note here...')" filled size="lg" autogrow - @keyup.enter.stop="insert" + @keyup.enter.stop="handleClick" + :required="isRequired" clearable - :required="true" > <template #append> <QBtn @@ -95,7 +163,7 @@ onBeforeRouteLeave((to, from, next) => { icon="save" color="primary" flat - @click="insert" + @click="handleClick" class="q-mb-xs" dense data-cy="saveNote" @@ -106,6 +174,7 @@ onBeforeRouteLeave((to, from, next) => { </QCardSection> </QCard> <VnPaginate + v-if="!$props.justInput" :data-key="$props.url" :url="$props.url" order="created DESC" @@ -198,6 +267,11 @@ onBeforeRouteLeave((to, from, next) => { } } } +.just-input { + padding-right: 18px; + margin-bottom: 2px; + box-shadow: none; +} </style> <i18n> es: @@ -205,4 +279,6 @@ onBeforeRouteLeave((to, from, next) => { New note: Nueva nota Save (Enter): Guardar (Intro) Observation type: Tipo de observación + New note is empty: La nueva nota esta vacia + Are you sure remove this note?: Estas seguro de quitar esta nota? </i18n> diff --git a/src/components/ui/VnStockValueDisplay.vue b/src/components/ui/VnStockValueDisplay.vue new file mode 100644 index 000000000..d8f43323b --- /dev/null +++ b/src/components/ui/VnStockValueDisplay.vue @@ -0,0 +1,41 @@ +<script setup> +import { toPercentage } from 'filters/index'; + +import { computed } from 'vue'; + +const props = defineProps({ + value: { + type: Number, + required: true, + }, +}); + +const valueClass = computed(() => + props.value === 0 ? 'neutral' : props.value > 0 ? 'positive' : 'negative', +); +const iconName = computed(() => + props.value === 0 ? 'equal' : props.value > 0 ? 'arrow_upward' : 'arrow_downward', +); +const formattedValue = computed(() => props.value); +</script> +<template> + <span :class="valueClass"> + <QIcon :name="iconName" size="sm" class="value-icon" /> + {{ toPercentage(formattedValue) }} + </span> +</template> + +<style lang="scss" scoped> +.positive { + color: $secondary; +} +.negative { + color: $negative; +} +.neutral { + color: $primary; +} +.value-icon { + margin-right: 4px; +} +</style> diff --git a/src/components/ui/VnSubToolbar.vue b/src/components/ui/VnSubToolbar.vue index 5ded4be00..8d4126d1d 100644 --- a/src/components/ui/VnSubToolbar.vue +++ b/src/components/ui/VnSubToolbar.vue @@ -19,23 +19,26 @@ onMounted(() => { const observer = new MutationObserver( () => (hasContent.value = - actions.value?.childNodes?.length + data.value?.childNodes?.length) + actions.value?.childNodes?.length + data.value?.childNodes?.length), ); if (actions.value) observer.observe(actions.value, opts); if (data.value) observer.observe(data.value, opts); }); -onBeforeUnmount(() => stateStore.toggleSubToolbar()); +const actionsChildCount = () => !!actions.value?.childNodes?.length; + +onBeforeUnmount(() => stateStore.toggleSubToolbar() && hasSubToolbar); </script> <template> <QToolbar id="subToolbar" - class="justify-end sticky" v-show="hasContent || $slots['st-actions'] || $slots['st-data']" + class="justify-end sticky" > <slot name="st-data"> - <div id="st-data"></div> + <div id="st-data" :class="{ 'full-width': !actionsChildCount() }"> + </div> </slot> <QSpace /> <slot name="st-actions"> diff --git a/src/components/ui/__tests__/CardSummary.spec.js b/src/components/ui/__tests__/CardSummary.spec.js index 411ebf9bb..2f7f90882 100644 --- a/src/components/ui/__tests__/CardSummary.spec.js +++ b/src/components/ui/__tests__/CardSummary.spec.js @@ -51,16 +51,6 @@ describe('CardSummary', () => { expect(vm.store.filter).toEqual('cardFilter'); }); - it('should compute entity correctly from store data', () => { - vm.store.data = [{ id: 1, name: 'Entity 1' }]; - expect(vm.entity).toEqual({ id: 1, name: 'Entity 1' }); - }); - - it('should handle empty data gracefully', () => { - vm.store.data = []; - expect(vm.entity).toBeUndefined(); - }); - it('should respond to prop changes and refetch data', async () => { const newUrl = 'CardSummary/35'; const newKey = 'cardSummaryKey/35'; @@ -72,7 +62,7 @@ describe('CardSummary', () => { expect(vm.store.filter).toEqual({ key: newKey }); }); - it('should return true if route path ends with /summary' , () => { + it('should return true if route path ends with /summary', () => { expect(vm.isSummary).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/src/composables/__tests__/useArrayData.spec.js b/src/composables/__tests__/useArrayData.spec.js index d4c5d0949..a610ba9eb 100644 --- a/src/composables/__tests__/useArrayData.spec.js +++ b/src/composables/__tests__/useArrayData.spec.js @@ -16,7 +16,7 @@ describe('useArrayData', () => { vi.clearAllMocks(); }); - it('should fetch and repalce url with new params', async () => { + it('should fetch and replace url with new params', async () => { vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] }); const arrayData = useArrayData('ArrayData', { url: 'mockUrl' }); @@ -33,11 +33,11 @@ describe('useArrayData', () => { }); expect(routerReplace.path).toEqual('mockSection/list'); expect(JSON.parse(routerReplace.query.params)).toEqual( - expect.objectContaining(params) + expect.objectContaining(params), ); }); - it('Should get data and send new URL without keeping parameters, if there is only one record', async () => { + it('should get data and send new URL without keeping parameters, if there is only one record', async () => { vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }] }); const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} }); @@ -56,7 +56,7 @@ describe('useArrayData', () => { expect(routerPush.query).toBeUndefined(); }); - it('Should get data and send new URL keeping parameters, if you have more than one record', async () => { + it('should get data and send new URL keeping parameters, if you have more than one record', async () => { vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }] }); vi.spyOn(vueRouter, 'useRoute').mockReturnValue({ @@ -95,4 +95,25 @@ describe('useArrayData', () => { expect(routerPush.path).toEqual('mockName/'); expect(routerPush.query.params).toBeDefined(); }); + + it('should return one record', async () => { + vi.spyOn(axios, 'get').mockReturnValueOnce({ + data: [ + { id: 1, name: 'Entity 1' }, + { id: 2, name: 'Entity 2' }, + ], + }); + const arrayData = useArrayData('ArrayData', { url: 'mockUrl', oneRecord: true }); + await arrayData.fetch({}); + + expect(arrayData.store.data).toEqual({ id: 1, name: 'Entity 1' }); + }); + + it('should handle empty data gracefully if has to return one record', async () => { + vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] }); + const arrayData = useArrayData('ArrayData', { url: 'mockUrl', oneRecord: true }); + await arrayData.fetch({}); + + expect(arrayData.store.data).toBeUndefined(); + }); }); diff --git a/src/composables/checkEntryLock.js b/src/composables/checkEntryLock.js new file mode 100644 index 000000000..f964dea27 --- /dev/null +++ b/src/composables/checkEntryLock.js @@ -0,0 +1,65 @@ +import { useQuasar } from 'quasar'; +import { useI18n } from 'vue-i18n'; +import { useRouter } from 'vue-router'; +import axios from 'axios'; +import VnConfirm from 'components/ui/VnConfirm.vue'; + +export async function checkEntryLock(entryFk, userFk) { + const { t } = useI18n(); + const quasar = useQuasar(); + const { push } = useRouter(); + const { data } = await axios.get(`Entries/${entryFk}`, { + params: { + filter: JSON.stringify({ + fields: ['id', 'locked', 'lockerUserFk'], + include: { relation: 'user', scope: { fields: ['id', 'nickname'] } }, + }), + }, + }); + const entryConfig = await axios.get('EntryConfigs/findOne'); + + if (data?.lockerUserFk && data?.locked) { + const now = new Date(Date.vnNow()).getTime(); + const lockedTime = new Date(data.locked).getTime(); + const timeDiff = (now - lockedTime) / 1000; + const isMaxTimeLockExceeded = entryConfig.data.maxLockTime > timeDiff; + + if (data?.lockerUserFk !== userFk && isMaxTimeLockExceeded) { + quasar + .dialog({ + component: VnConfirm, + componentProps: { + 'data-cy': 'entry-lock-confirm', + title: t('entry.lock.title'), + message: t('entry.lock.message', { + userName: data?.user?.nickname, + time: timeDiff / 60, + }), + }, + }) + .onOk( + async () => + await axios.patch(`Entries/${entryFk}`, { + locked: Date.vnNow(), + lockerUserFk: userFk, + }), + ) + .onCancel(() => { + push({ path: `summary` }); + }); + } + } else { + await axios + .patch(`Entries/${entryFk}`, { + locked: Date.vnNow(), + lockerUserFk: userFk, + }) + .then( + quasar.notify({ + message: t('entry.lock.success'), + color: 'positive', + group: false, + }), + ); + } +} diff --git a/src/composables/getColAlign.js b/src/composables/getColAlign.js new file mode 100644 index 000000000..a930fd7d8 --- /dev/null +++ b/src/composables/getColAlign.js @@ -0,0 +1,22 @@ +export function getColAlign(col) { + let align; + switch (col.component) { + case 'time': + case 'date': + case 'select': + align = 'left'; + break; + case 'number': + align = 'right'; + break; + case 'checkbox': + align = 'center'; + break; + default: + align = col?.align; + } + + if (/^is[A-Z]/.test(col.name) || /^has[A-Z]/.test(col.name)) align = 'center'; + + return 'text-' + (align ?? 'center'); +} diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js index bd3cecf08..fcc61972a 100644 --- a/src/composables/useArrayData.js +++ b/src/composables/useArrayData.js @@ -57,6 +57,7 @@ export function useArrayData(key, userOptions) { 'navigate', 'mapKey', 'keepData', + 'oneRecord', ]; if (typeof userOptions === 'object') { for (const option in userOptions) { @@ -112,7 +113,11 @@ export function useArrayData(key, userOptions) { store.isLoading = false; canceller = null; - processData(response.data, { map: !!store.mapKey, append }); + processData(response.data, { + map: !!store.mapKey, + append, + oneRecord: store.oneRecord, + }); return response; } @@ -314,7 +319,11 @@ export function useArrayData(key, userOptions) { return { params, limit }; } - function processData(data, { map = true, append = true }) { + function processData(data, { map = true, append = true, oneRecord = false }) { + if (oneRecord) { + store.data = Array.isArray(data) ? data[0] : data; + return; + } if (!append) { store.data = []; store.map = new Map(); diff --git a/src/composables/useRole.js b/src/composables/useRole.js index 3ec65dd0a..ff54b409c 100644 --- a/src/composables/useRole.js +++ b/src/composables/useRole.js @@ -27,6 +27,15 @@ export function useRole() { return false; } + function likeAny(roles) { + const roleStore = state.getRoles(); + for (const role of roles) { + if (!roleStore.value.findIndex((rs) => rs.startsWith(role)) !== -1) + return true; + } + + return false; + } function isEmployee() { return hasAny(['employee']); } @@ -35,6 +44,7 @@ export function useRole() { isEmployee, fetch, hasAny, + likeAny, state, }; } diff --git a/src/css/app.scss b/src/css/app.scss index 7296b079f..0c5dc97fa 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -21,7 +21,10 @@ body.body--light { .q-header .q-toolbar { color: var(--vn-text-color); } + + --vn-color-negative: $negative; } + body.body--dark { --vn-header-color: #5d5d5d; --vn-page-color: #222; @@ -37,6 +40,8 @@ body.body--dark { --vn-text-color-contrast: black; background-color: var(--vn-page-color); + + --vn-color-negative: $negative; } a { @@ -75,7 +80,6 @@ a { text-decoration: underline; } -// Removes chrome autofill background input:-webkit-autofill, select:-webkit-autofill { color: var(--vn-text-color); @@ -149,11 +153,6 @@ select:-webkit-autofill { cursor: pointer; } -.vn-table-separation-row { - height: 16px !important; - background-color: var(--vn-section-color) !important; -} - /* Estilo para el asterisco en campos requeridos */ .q-field.required .q-field__label:after { content: ' *'; @@ -212,6 +211,10 @@ select:-webkit-autofill { justify-content: center; } +.q-card__section[dense] { + padding: 0; +} + input[type='number'] { -moz-appearance: textfield; } @@ -226,10 +229,12 @@ input::-webkit-inner-spin-button { max-width: 100%; } -.q-table__container { - /* ===== Scrollbar CSS ===== / - / Firefox */ +.remove-bg { + filter: brightness(1.1); + mix-blend-mode: multiply; +} +.q-table__container { * { scrollbar-width: auto; scrollbar-color: var(--vn-label-color) transparent; @@ -270,8 +275,6 @@ input::-webkit-inner-spin-button { font-size: 11pt; } td { - font-size: 11pt; - border-top: 1px solid var(--vn-page-color); border-collapse: collapse; } } @@ -315,9 +318,6 @@ input::-webkit-inner-spin-button { max-width: fit-content; } -.row > .column:has(.q-checkbox) { - max-width: fit-content; -} .q-field__inner { .q-field__control { min-height: auto !important; diff --git a/src/css/quasar.variables.scss b/src/css/quasar.variables.scss index d6e992437..22c6d2b56 100644 --- a/src/css/quasar.variables.scss +++ b/src/css/quasar.variables.scss @@ -13,7 +13,7 @@ // Tip: Use the "Theme Builder" on Quasar's documentation website. // Tip: to add new colors https://quasar.dev/style/color-palette/#adding-your-own-colors $primary: #ec8916; -$secondary: $primary; +$secondary: #89be34; $positive: #c8e484; $negative: #fb5252; $info: #84d0e2; @@ -30,7 +30,9 @@ $color-spacer: #7979794d; $border-thin-light: 1px solid $color-spacer-light; $primary-light: #f5b351; $dark-shadow-color: black; -$layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d; +$layout-shadow-dark: + 0 0 10px 2px #00000033, + 0 0px 10px #0000003d; $spacing-md: 16px; $color-font-secondary: #777; $width-xs: 400px; diff --git a/src/filters/toDate.js b/src/filters/toDate.js index 8fe8f3836..002797af5 100644 --- a/src/filters/toDate.js +++ b/src/filters/toDate.js @@ -3,6 +3,8 @@ import { useI18n } from 'vue-i18n'; export default function (value, options = {}) { if (!value) return; + if (!isValidDate(value)) return null; + if (!options.dateStyle && !options.timeStyle) { options.day = '2-digit'; options.month = '2-digit'; @@ -10,7 +12,12 @@ export default function (value, options = {}) { } const { locale } = useI18n(); - const date = new Date(value); + const newDate = new Date(value); - return new Intl.DateTimeFormat(locale.value, options).format(date); + return new Intl.DateTimeFormat(locale.value, options).format(newDate); +} +// handle 0000-00-00 +function isValidDate(date) { + const parsedDate = new Date(date); + return parsedDate instanceof Date && !isNaN(parsedDate.getTime()); } diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 7d0f3e0b2..9a60e9da1 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -33,6 +33,7 @@ globals: reset: Reset close: Close cancel: Cancel + isSaveAndContinue: Save and continue clone: Clone confirm: Confirm assign: Assign @@ -156,6 +157,7 @@ globals: changeState: Change state raid: 'Raid {daysInForward} days' isVies: Vies + noData: No data available pageTitles: logIn: Login addressEdit: Update address @@ -168,6 +170,7 @@ globals: workCenters: Work centers modes: Modes zones: Zones + negative: Negative zonesList: List deliveryDays: Delivery days upcomingDeliveries: Upcoming deliveries @@ -175,6 +178,7 @@ globals: alias: Alias aliasUsers: Users subRoles: Subroles + myAccount: Mi cuenta inheritedRoles: Inherited Roles customers: Customers customerCreate: New customer @@ -333,10 +337,13 @@ globals: wasteRecalc: Waste recaclulate operator: Operator parking: Parking + vehicleList: Vehicles + vehicle: Vehicle unsavedPopup: title: Unsaved changes will be lost subtitle: Are you sure exit without saving? params: + description: Description clientFk: Client id salesPersonFk: Sales person warehouseFk: Warehouse @@ -359,7 +366,13 @@ globals: correctingFk: Rectificative daysOnward: Days onward countryFk: Country + countryCodeFk: Country companyFk: Company + model: Model + fuel: Fuel + active: Active + inactive: Inactive + deliveryPoint: Delivery point errors: statusUnauthorized: Access denied statusInternalServerError: An internal server error has ocurred @@ -398,6 +411,106 @@ cau: subtitle: By sending this ticket, all the data related to the error, the section, the user, etc., are already sent. inputLabel: Explain why this error should not appear askPrivileges: Ask for privileges +entry: + list: + newEntry: New entry + tableVisibleColumns: + isExcludedFromAvailable: Exclude from inventory + isOrdered: Ordered + isConfirmed: Ready to label + isReceived: Received + isRaid: Raid + landed: Date + supplierFk: Supplier + reference: Ref/Alb/Guide + invoiceNumber: Invoice + agencyModeId: Agency + isBooked: Booked + companyFk: Company + evaNotes: Notes + warehouseOutFk: Origin + warehouseInFk: Destiny + entryTypeDescription: Entry type + invoiceAmount: Import + travelFk: Travel + summary: + invoiceAmount: Amount + commission: Commission + currency: Currency + invoiceNumber: Invoice number + ordered: Ordered + booked: Booked + excludedFromAvailable: Inventory + travelReference: Reference + travelAgency: Agency + travelShipped: Shipped + travelDelivered: Delivered + travelLanded: Landed + travelReceived: Received + buys: Buys + stickers: Stickers + package: Package + packing: Pack. + grouping: Group. + buyingValue: Buying value + import: Import + pvp: PVP + basicData: + travel: Travel + currency: Currency + commission: Commission + observation: Observation + booked: Booked + excludedFromAvailable: Inventory + buys: + observations: Observations + packagingFk: Box + color: Color + printedStickers: Printed stickers + notes: + observationType: Observation type + latestBuys: + tableVisibleColumns: + image: Picture + itemFk: Item ID + weightByPiece: Weight/Piece + isActive: Active + family: Family + entryFk: Entry + freightValue: Freight value + comissionValue: Commission value + packageValue: Package value + isIgnored: Is ignored + price2: Grouping + price3: Packing + minPrice: Min + ektFk: Ekt + packingOut: Package out + landing: Landing + isExcludedFromAvailable: Exclude from inventory + isRaid: Raid + invoiceNumber: Invoice + reference: Ref/Alb/Guide + params: + isExcludedFromAvailable: Excluir del inventario + isOrdered: Pedida + isConfirmed: Lista para etiquetar + isReceived: Recibida + isRaid: Redada + landed: Fecha + supplierFk: Proveedor + invoiceNumber: Nº Factura + reference: Ref/Alb/Guía + agencyModeId: Agencia + isBooked: Asentado + companyFk: Empresa + travelFk: Envio + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeDescription: Tipo entrada + invoiceAmount: Importe + dated: Fecha ticket: params: ticketFk: Ticket ID @@ -627,6 +740,8 @@ wagon: name: Name supplier: + search: Search supplier + searchInfo: Search supplier by id or name list: payMethod: Pay method account: Account @@ -716,6 +831,8 @@ travel: CloneTravelAndEntries: Clone travel and his entries deleteTravel: Delete travel AddEntry: Add entry + availabled: Availabled + availabledHour: Availabled hour thermographs: Thermographs hb: HB basicData: diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 7ca9e4b4c..846c442ea 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -33,9 +33,11 @@ globals: reset: Restaurar close: Cerrar cancel: Cancelar + isSaveAndContinue: Guardar y continuar clone: Clonar confirm: Confirmar assign: Asignar + replace: Sustituir back: Volver yes: Si no: No @@ -48,6 +50,7 @@ globals: rowRemoved: Fila eliminada pleaseWait: Por favor espera... noPinnedModules: No has fijado ningún módulo + split: Split enterToConfirm: Pulsa Enter para confirmar summary: basicData: Datos básicos @@ -56,8 +59,8 @@ globals: today: Hoy yesterday: Ayer dateFormat: es-ES - microsip: Abrir en MicroSIP noSelectedRows: No tienes ninguna línea seleccionada + microsip: Abrir en MicroSIP downloadCSVSuccess: Descarga de CSV exitosa reference: Referencia agency: Agencia @@ -77,8 +80,10 @@ globals: requiredField: Campo obligatorio class: clase type: Tipo - reason: motivo + reason: Motivo + removeSelection: Eliminar selección noResults: Sin resultados + results: resultados system: Sistema notificationSent: Notificación enviada warehouse: Almacén @@ -156,6 +161,7 @@ globals: changeState: Cambiar estado raid: 'Redada {daysInForward} días' isVies: Vies + noData: Datos no disponibles pageTitles: logIn: Inicio de sesión addressEdit: Modificar consignatario @@ -167,6 +173,7 @@ globals: agency: Agencia workCenters: Centros de trabajo modes: Modos + negative: Tickets negativos zones: Zonas zonesList: Listado deliveryDays: Días de entrega @@ -287,9 +294,9 @@ globals: buyRequest: Peticiones de compra wasteBreakdown: Deglose de mermas itemCreate: Nuevo artículo - tax: 'IVA' - botanical: 'Botánico' - barcode: 'Código de barras' + tax: IVA + botanical: Botánico + barcode: Código de barras itemTypeCreate: Nueva familia family: Familia lastEntries: Últimas entradas @@ -333,10 +340,13 @@ globals: wasteRecalc: Recalcular mermas operator: Operario parking: Parking + vehicleList: Vehículos + vehicle: Vehículo unsavedPopup: title: Los cambios que no haya guardado se perderán subtitle: ¿Seguro que quiere salir sin guardar? params: + description: Descripción clientFk: Id cliente salesPersonFk: Comercial warehouseFk: Almacén @@ -350,13 +360,14 @@ globals: from: Desde to: Hasta supplierFk: Proveedor - supplierRef: Ref. proveedor + supplierRef: Nº factura serial: Serie amount: Importe awbCode: AWB daysOnward: Días adelante packing: ITP countryFk: País + countryCodeFk: País companyFk: Empresa errors: statusUnauthorized: Acceso denegado @@ -394,6 +405,87 @@ cau: subtitle: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc inputLabel: Explique el motivo por el que no deberia aparecer este fallo askPrivileges: Solicitar permisos +entry: + list: + newEntry: Nueva entrada + tableVisibleColumns: + isExcludedFromAvailable: Excluir del inventario + isOrdered: Pedida + isConfirmed: Lista para etiquetar + isReceived: Recibida + isRaid: Redada + landed: Fecha + supplierFk: Proveedor + invoiceNumber: Nº Factura + reference: Ref/Alb/Guía + agencyModeId: Agencia + isBooked: Asentado + companyFk: Empresa + travelFk: Envio + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeDescription: Tipo entrada + invoiceAmount: Importe + summary: + invoiceAmount: Importe + commission: Comisión + currency: Moneda + invoiceNumber: Núm. factura + ordered: Pedida + booked: Contabilizada + excludedFromAvailable: Inventario + travelReference: Referencia + travelAgency: Agencia + travelShipped: F. envio + travelWarehouseOut: Alm. salida + travelDelivered: Enviada + travelLanded: F. entrega + travelReceived: Recibida + buys: Compras + stickers: Etiquetas + package: Embalaje + packing: Pack. + grouping: Group. + buyingValue: Coste + import: Importe + pvp: PVP + basicData: + travel: Envío + currency: Moneda + observation: Observación + commission: Comisión + booked: Asentado + excludedFromAvailable: Inventario + buys: + observations: Observaciónes + packagingFk: Embalaje + color: Color + printedStickers: Etiquetas impresas + notes: + observationType: Tipo de observación + latestBuys: + tableVisibleColumns: + image: Foto + itemFk: Id Artículo + weightByPiece: Peso (gramos)/tallo + isActive: Activo + family: Familia + entryFk: Entrada + freightValue: Porte + comissionValue: Comisión + packageValue: Embalaje + isIgnored: Ignorado + price2: Grouping + price3: Packing + minPrice: Min + ektFk: Ekt + packingOut: Embalaje envíos + landing: Llegada + isExcludedFromAvailable: Excluir del inventario + isRaid: Redada + invoiceNumber: Nº Factura + reference: Ref/Alb/Guía ticket: params: ticketFk: ID de ticket @@ -407,6 +499,38 @@ ticket: freightItemName: Nombre packageItemName: Embalaje longName: Descripción + pageTitles: + tickets: Tickets + list: Listado + ticketCreate: Nuevo ticket + summary: Resumen + basicData: Datos básicos + boxing: Encajado + sms: Sms + notes: Notas + sale: Lineas del pedido + dms: Gestión documental + negative: Tickets negativos + 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: + nickname: Alias + state: Estado + shipped: Enviado + landed: Entregado + salesPerson: Comercial + total: Total card: customerId: ID cliente customerCard: Ficha del cliente @@ -453,15 +577,11 @@ ticket: consigneeStreet: Dirección create: address: Dirección -order: - field: - salesPersonFk: Comercial - form: - clientFk: Cliente - addressFk: Dirección - agencyModeFk: Agencia - list: - newOrder: Nuevo Pedido +invoiceOut: + card: + issued: Fecha emisión + customerCard: Ficha del cliente + ticketList: Listado de tickets summary: issued: Fecha dued: Fecha límite @@ -472,6 +592,71 @@ order: fee: Cuota tickets: Tickets totalWithVat: Importe + globalInvoices: + errors: + chooseValidClient: Selecciona un cliente válido + chooseValidCompany: Selecciona una empresa válida + chooseValidPrinter: Selecciona una impresora válida + chooseValidSerialType: Selecciona una tipo de serie válida + fillDates: La fecha de la factura y la fecha máxima deben estar completas + invoiceDateLessThanMaxDate: La fecha de la factura no puede ser menor que la fecha máxima + invoiceWithFutureDate: Existe una factura con una fecha futura + noTicketsToInvoice: No existen tickets para facturar + criticalInvoiceError: Error crítico en la facturación proceso detenido + invalidSerialTypeForAll: El tipo de serie debe ser global cuando se facturan todos los clientes + table: + addressId: Id dirección + streetAddress: Dirección fiscal + statusCard: + percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}' + pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs' + negativeBases: + clientId: Id cliente + base: Base + active: Activo + hasToInvoice: Facturar + verifiedData: Datos comprobados + comercial: Comercial + errors: + downloadCsvFailed: Error al descargar CSV +order: + field: + salesPersonFk: Comercial + form: + clientFk: Cliente + addressFk: Dirección + agencyModeFk: Agencia + list: + newOrder: Nuevo Pedido + summary: + basket: Cesta + notConfirmed: No confirmada + created: Creado + createdFrom: Creado desde + address: Dirección + total: Total + vat: IVA + state: Estado + alias: Alias + items: Artículos + orderTicketList: Tickets del pedido + amount: Monto + confirm: Confirmar + confirmLines: Confirmar lineas +shelving: + list: + parking: Parking + priority: Prioridad + newShelving: Nuevo Carro + summary: + recyclable: Reciclable +parking: + pickingOrder: Orden de recogida + row: Fila + column: Columna + searchBar: + info: Puedes buscar por código de parking + label: Buscar parking... department: chat: Chat bossDepartment: Jefe de departamento @@ -632,8 +817,8 @@ wagon: volumeNotEmpty: El volumen no puede estar vacío typeNotEmpty: El tipo no puede estar vacío maxTrays: Has alcanzado el número máximo de bandejas - minHeightBetweenTrays: 'La distancia mínima entre bandejas es ' - maxWagonHeight: 'La altura máxima del vagón es ' + minHeightBetweenTrays: La distancia mínima entre bandejas es + maxWagonHeight: La altura máxima del vagón es uncompleteTrays: Hay bandejas sin completar params: label: Etiqueta @@ -641,6 +826,8 @@ wagon: volume: Volumen name: Nombre supplier: + search: Buscar proveedor + searchInfo: Buscar proveedor por id o nombre list: payMethod: Método de pago account: Cuenta @@ -731,6 +918,8 @@ travel: deleteTravel: Eliminar envío AddEntry: Añadir entrada thermographs: Termógrafos + availabled: F. Disponible + availabledHour: Hora Disponible hb: HB basicData: daysInForward: Desplazamiento automatico (redada) @@ -779,7 +968,7 @@ components: cardDescriptor: mainList: Listado principal summary: Resumen - moreOptions: 'Más opciones' + moreOptions: Más opciones leftMenu: addToPinned: Añadir a fijados removeFromPinned: Eliminar de fijados diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 2a84e5aa1..3ad1c79bc 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -2,7 +2,7 @@ import Navbar from 'src/components/NavBar.vue'; </script> <template> - <QLayout view="hHh LpR fFf" v-shortcut> + <QLayout view="hHh LpR fFf"> <Navbar /> <RouterView></RouterView> <QFooter v-if="$q.platform.is.mobile"></QFooter> diff --git a/src/layouts/OutLayout.vue b/src/layouts/OutLayout.vue index 4ccc6bf9e..eba57c198 100644 --- a/src/layouts/OutLayout.vue +++ b/src/layouts/OutLayout.vue @@ -1,12 +1,12 @@ <script setup> import { Dark, Quasar } from 'quasar'; -import { computed } from 'vue'; +import { computed, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; import { localeEquivalence } from 'src/i18n/index'; import quasarLang from 'src/utils/quasarLang'; +import { langs } from 'src/boot/defaults/constants.js'; const { t, locale } = useI18n(); - const userLocale = computed({ get() { return locale.value; @@ -28,7 +28,6 @@ const darkMode = computed({ Dark.set(value); }, }); -const langs = ['en', 'es']; </script> <template> diff --git a/src/pages/Account/AccountAliasList.vue b/src/pages/Account/AccountAliasList.vue index f6016fb6c..19682286c 100644 --- a/src/pages/Account/AccountAliasList.vue +++ b/src/pages/Account/AccountAliasList.vue @@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n'; import { ref, computed } from 'vue'; import VnTable from 'components/VnTable/VnTable.vue'; import VnSection from 'src/components/common/VnSection.vue'; +import exprBuilder from './Alias/AliasExprBuilder'; const tableRef = ref(); const { t } = useI18n(); @@ -31,15 +32,6 @@ const columns = computed(() => [ create: true, }, ]); - -const exprBuilder = (param, value) => { - switch (param) { - case 'search': - return /^\d+$/.test(value) - ? { id: value } - : { alias: { like: `%${value}%` } }; - } -}; </script> <template> diff --git a/src/pages/Account/AccountExprBuilder.js b/src/pages/Account/AccountExprBuilder.js new file mode 100644 index 000000000..6497a9d30 --- /dev/null +++ b/src/pages/Account/AccountExprBuilder.js @@ -0,0 +1,18 @@ +export default (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { + or: [ + { name: { like: `%${value}%` } }, + { nickname: { like: `%${value}%` } }, + ], + }; + case 'name': + case 'nickname': + return { [param]: { like: `%${value}%` } }; + case 'roleFk': + return { [param]: value }; + } +}; diff --git a/src/pages/Account/AccountList.vue b/src/pages/Account/AccountList.vue index ea8daba0d..976af1d19 100644 --- a/src/pages/Account/AccountList.vue +++ b/src/pages/Account/AccountList.vue @@ -4,15 +4,16 @@ import { computed, ref } from 'vue'; import VnTable from 'components/VnTable/VnTable.vue'; import AccountSummary from './Card/AccountSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import exprBuilder from './AccountExprBuilder.js'; +import filter from './Card/AccountFilter.js'; import VnSection from 'src/components/common/VnSection.vue'; import FetchData from 'src/components/FetchData.vue'; import VnInputPassword from 'src/components/common/VnInputPassword.vue'; const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); -const filter = { - include: { relation: 'role', scope: { fields: ['id', 'name'] } }, -}; +const tableRef = ref(); + const dataKey = 'AccountList'; const roles = ref([]); const columns = computed(() => [ @@ -117,25 +118,6 @@ const columns = computed(() => [ ], }, ]); - -function exprBuilder(param, value) { - switch (param) { - case 'search': - return /^\d+$/.test(value) - ? { id: value } - : { - or: [ - { name: { like: `%${value}%` } }, - { nickname: { like: `%${value}%` } }, - ], - }; - case 'name': - case 'nickname': - return { [param]: { like: `%${value}%` } }; - case 'roleFk': - return { [param]: value }; - } -} </script> <template> <FetchData url="VnRoles" @on-fetch="(data) => (roles = data)" auto-load /> diff --git a/src/pages/Account/Alias/AliasExprBuilder.js b/src/pages/Account/Alias/AliasExprBuilder.js new file mode 100644 index 000000000..f7a5a104c --- /dev/null +++ b/src/pages/Account/Alias/AliasExprBuilder.js @@ -0,0 +1,8 @@ +export default (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { alias: { like: `%${value}%` } }; + } +}; diff --git a/src/pages/Account/Alias/Card/AliasCard.vue b/src/pages/Account/Alias/Card/AliasCard.vue index 3a814edc0..f37bd7d0f 100644 --- a/src/pages/Account/Alias/Card/AliasCard.vue +++ b/src/pages/Account/Alias/Card/AliasCard.vue @@ -1,21 +1,13 @@ <script setup> -import { useI18n } from 'vue-i18n'; import VnCardBeta from 'components/common/VnCardBeta.vue'; import AliasDescriptor from './AliasDescriptor.vue'; -const { t } = useI18n(); </script> <template> <VnCardBeta data-key="Alias" - base-url="MailAliases" + url="MailAliases" :descriptor="AliasDescriptor" search-data-key="AccountAliasList" - :searchbar-props="{ - url: 'MailAliases', - info: t('mailAlias.searchInfo'), - label: t('mailAlias.search'), - searchUrl: 'table', - }" /> </template> diff --git a/src/pages/Account/Alias/Card/AliasDescriptor.vue b/src/pages/Account/Alias/Card/AliasDescriptor.vue index 2e01fad01..671ef7fbc 100644 --- a/src/pages/Account/Alias/Card/AliasDescriptor.vue +++ b/src/pages/Account/Alias/Card/AliasDescriptor.vue @@ -7,7 +7,6 @@ import { useQuasar } from 'quasar'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; @@ -29,9 +28,6 @@ const entityId = computed(() => { return $props.id || route.params.id; }); -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.alias, entity.id)); - const removeAlias = () => { quasar .dialog({ @@ -55,11 +51,8 @@ const removeAlias = () => { <CardDescriptor ref="descriptor" :url="`MailAliases/${entityId}`" - module="Alias" - @on-fetch="setData" - data-key="aliasData" - :title="data.title" - :subtitle="data.subtitle" + data-key="Alias" + title="alias" > <template #menu> <QItem v-ripple clickable @click="removeAlias()"> diff --git a/src/pages/Account/Alias/Card/AliasSummary.vue b/src/pages/Account/Alias/Card/AliasSummary.vue index 1f76fe7c2..b4b9abd25 100644 --- a/src/pages/Account/Alias/Card/AliasSummary.vue +++ b/src/pages/Account/Alias/Card/AliasSummary.vue @@ -1,13 +1,11 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import { useArrayData } from 'src/composables/useArrayData'; - const route = useRoute(); const { t } = useI18n(); @@ -18,20 +16,15 @@ const $props = defineProps({ }, }); -const { store } = useArrayData('Alias'); -const alias = ref(store.data); const entityId = computed(() => $props.id || route.params.id); </script> <template> - <CardSummary - ref="summary" - :url="`MailAliases/${entityId}`" - @on-fetch="(data) => (alias = data)" - data-key="MailAliasesSummary" - > - <template #header> {{ alias.id }} - {{ alias.alias }} </template> - <template #body> + <CardSummary ref="summary" :url="`MailAliases/${entityId}`" data-key="Alias"> + <template #header="{ entity: alias }"> + {{ alias.id }} - {{ alias.alias }} + </template> + <template #body="{ entity: alias }"> <QCard class="vn-one"> <QCardSection class="q-pa-none"> <router-link diff --git a/src/pages/Account/Card/AccountBasicData.vue b/src/pages/Account/Card/AccountBasicData.vue index e6c9da6fe..393f9eb80 100644 --- a/src/pages/Account/Card/AccountBasicData.vue +++ b/src/pages/Account/Card/AccountBasicData.vue @@ -1,46 +1,20 @@ <script setup> -import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectEnum from 'src/components/common/VnSelectEnum.vue'; import FormModel from 'components/FormModel.vue'; import VnInput from 'src/components/common/VnInput.vue'; -import { ref, watch } from 'vue'; - -const route = useRoute(); -const { t } = useI18n(); -const formModelRef = ref(null); - -const accountFilter = { - where: { id: route.params.id }, - fields: ['id', 'email', 'nickname', 'name', 'accountStateFk', 'packages', 'pickup'], - include: [], -}; - -watch( - () => route.params.id, - () => formModelRef.value.reset() -); </script> <template> - <FormModel - ref="formModelRef" - url="VnUsers/preview" - :url-update="`VnUsers/${route.params.id}/update-user`" - :filter="accountFilter" - model="Accounts" - auto-load - @on-data-saved="formModelRef.fetch()" - > + <FormModel :url-update="`VnUsers/${$route.params.id}/update-user`" model="Account"> <template #form="{ data }"> <div class="q-gutter-y-sm"> - <VnInput v-model="data.name" :label="t('account.card.nickname')" /> - <VnInput v-model="data.nickname" :label="t('account.card.alias')" /> - <VnInput v-model="data.email" :label="t('globals.params.email')" /> + <VnInput v-model="data.name" :label="$t('account.card.nickname')" /> + <VnInput v-model="data.nickname" :label="$t('account.card.alias')" /> + <VnInput v-model="data.email" :label="$t('globals.params.email')" /> <VnSelect url="Languages" v-model="data.lang" - :label="t('account.card.lang')" + :label="$t('account.card.lang')" option-value="code" option-label="code" /> @@ -49,7 +23,7 @@ watch( table="user" column="twoFactor" v-model="data.twoFactor" - :label="t('account.card.twoFactor')" + :label="$t('account.card.twoFactor')" option-value="code" option-label="code" /> diff --git a/src/pages/Account/Card/AccountCard.vue b/src/pages/Account/Card/AccountCard.vue index 35ff7e732..a5037e301 100644 --- a/src/pages/Account/Card/AccountCard.vue +++ b/src/pages/Account/Card/AccountCard.vue @@ -1,8 +1,14 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import AccountDescriptor from './AccountDescriptor.vue'; +import filter from './AccountFilter.js'; </script> - <template> - <VnCardBeta data-key="AccountId" :descriptor="AccountDescriptor" /> + <VnCardBeta + url="VnUsers/preview" + :id-in-where="true" + data-key="Account" + :descriptor="AccountDescriptor" + :filter="filter" + /> </template> diff --git a/src/pages/Account/Card/AccountDescriptor.vue b/src/pages/Account/Card/AccountDescriptor.vue index 4e5328de6..49328fe87 100644 --- a/src/pages/Account/Card/AccountDescriptor.vue +++ b/src/pages/Account/Card/AccountDescriptor.vue @@ -1,36 +1,18 @@ <script setup> import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; import VnImg from 'src/components/ui/VnImg.vue'; +import filter from './AccountFilter.js'; import useHasAccount from 'src/composables/useHasAccount.js'; -const $props = defineProps({ - id: { - type: Number, - required: false, - default: null, - }, -}); +const $props = defineProps({ id: { type: Number, default: null } }); const route = useRoute(); -const { t } = useI18n(); -const entityId = computed(() => { - return $props.id || route.params.id; -}); -const data = ref(useCardDescription()); +const entityId = computed(() => $props.id || route.params.id); const hasAccount = ref(); -const setData = (entity) => (data.value = useCardDescription(entity.nickname, entity.id)); - -const filter = { - where: { id: entityId }, - fields: ['id', 'nickname', 'name', 'role'], - include: { relation: 'role', scope: { fields: ['id', 'name'] } }, -}; onMounted(async () => { hasAccount.value = await useHasAccount(entityId.value); @@ -41,12 +23,9 @@ onMounted(async () => { <CardDescriptor ref="descriptor" :url="`VnUsers/preview`" - :filter="filter" - module="Account" - @on-fetch="setData" - data-key="AccountId" - :title="data.title" - :subtitle="data.subtitle" + :filter="{ ...filter, where: { id: entityId } }" + data-key="Account" + title="nickname" > <template #menu> <AccountDescriptorMenu :entity-id="entityId" /> @@ -62,7 +41,7 @@ onMounted(async () => { <QIcon name="vn:claims" /> </div> <div class="text-grey-5" style="opacity: 0.4"> - {{ t('account.imageNotFound') }} + {{ $t('account.imageNotFound') }} </div> </div> </div> @@ -70,8 +49,8 @@ onMounted(async () => { </VnImg> </template> <template #body="{ entity }"> - <VnLv :label="t('account.card.nickname')" :value="entity.name" /> - <VnLv :label="t('account.card.role')" :value="entity.role.name" /> + <VnLv :label="$t('account.card.nickname')" :value="entity.name" /> + <VnLv :label="$t('account.card.role')" :value="entity.role?.name" /> </template> <template #actions="{ entity }"> <QCardActions class="q-gutter-x-md"> @@ -84,7 +63,7 @@ onMounted(async () => { size="sm" class="fill-icon" > - <QTooltip>{{ t('account.card.deactivated') }}</QTooltip> + <QTooltip>{{ $t('account.card.deactivated') }}</QTooltip> </QIcon> <QIcon color="primary" @@ -95,7 +74,7 @@ onMounted(async () => { size="sm" class="fill-icon" > - <QTooltip>{{ t('account.card.enabled') }}</QTooltip> + <QTooltip>{{ $t('account.card.enabled') }}</QTooltip> </QIcon> </QCardActions> </template> diff --git a/src/pages/Account/Card/AccountDescriptorMenu.vue b/src/pages/Account/Card/AccountDescriptorMenu.vue index 961323d3a..30584c61f 100644 --- a/src/pages/Account/Card/AccountDescriptorMenu.vue +++ b/src/pages/Account/Card/AccountDescriptorMenu.vue @@ -12,6 +12,7 @@ import VnInputPassword from 'src/components/common/VnInputPassword.vue'; import VnChangePassword from 'src/components/common/VnChangePassword.vue'; import { useQuasar } from 'quasar'; import { useRouter } from 'vue-router'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const $props = defineProps({ hasAccount: { @@ -29,7 +30,7 @@ const router = useRouter(); const state = useState(); const user = state.getUser(); const { notify } = useQuasar(); -const account = computed(() => useArrayData('AccountId').store.data[0]); +const account = computed(() => useArrayData('Account').store.data[0]); account.value.hasAccount = hasAccount.value; const entityId = computed(() => +route.params.id); const hasitManagementAccess = ref(); @@ -124,18 +125,14 @@ onMounted(() => { :promise="sync" > <template #customHTML> - {{ shouldSyncPassword }} - <QCheckbox - :label="t('account.card.actions.sync.checkbox')" + <VnCheckbox v-model="shouldSyncPassword" - class="full-width" + :label="t('account.card.actions.sync.checkbox')" + :info="t('account.card.actions.sync.tooltip')" clearable clear-icon="close" - > - <QIcon style="padding-left: 10px" color="primary" name="info" size="sm"> - <QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip> - </QIcon></QCheckbox - > + color="primary" + /> <VnInputPassword v-if="shouldSyncPassword" :label="t('login.password')" @@ -155,7 +152,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.subtitle'), - () => deleteAccount() + () => deleteAccount(), ) " > @@ -174,7 +171,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.enableAccount.title'), t('account.card.actions.enableAccount.subtitle'), - () => updateStatusAccount(true) + () => updateStatusAccount(true), ) " > @@ -188,7 +185,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.subtitle'), - () => updateStatusAccount(false) + () => updateStatusAccount(false), ) " > @@ -203,7 +200,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.activateUser.title'), t('account.card.actions.activateUser.title'), - () => updateStatusUser(true) + () => updateStatusUser(true), ) " > @@ -217,7 +214,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.deactivateUser.title'), t('account.card.actions.deactivateUser.title'), - () => updateStatusUser(false) + () => updateStatusUser(false), ) " > diff --git a/src/pages/Account/Card/AccountFilter.js b/src/pages/Account/Card/AccountFilter.js new file mode 100644 index 000000000..017876564 --- /dev/null +++ b/src/pages/Account/Card/AccountFilter.js @@ -0,0 +1,3 @@ +export default { + include: { relation: 'role', scope: { fields: ['id', 'name'] } }, +}; diff --git a/src/pages/Account/Card/AccountMailAlias.vue b/src/pages/Account/Card/AccountMailAlias.vue index ef1707cf2..7a060cff1 100644 --- a/src/pages/Account/Card/AccountMailAlias.vue +++ b/src/pages/Account/Card/AccountMailAlias.vue @@ -86,7 +86,7 @@ watch( () => route.params.id, () => { getAccountData(); - } + }, ); onMounted(async () => await getAccountData(false)); @@ -130,7 +130,8 @@ onMounted(async () => await getAccountData(false)); openConfirmationModal( t('User will be removed from alias'), t('¿Seguro que quieres continuar?'), - () => deleteMailAlias(row, rows, rowIndex) + () => + deleteMailAlias(row, rows, rowIndex), ) " > @@ -157,7 +158,7 @@ onMounted(async () => await getAccountData(false)); icon="add" color="primary" @click="openCreateMailAliasForm()" - shortcut="+" + v-shortcut="'+'" > <QTooltip>{{ t('warehouses.add') }}</QTooltip> </QBtn> diff --git a/src/pages/Account/Card/AccountSummary.vue b/src/pages/Account/Card/AccountSummary.vue index ca17c7975..f7a16e8c3 100644 --- a/src/pages/Account/Card/AccountSummary.vue +++ b/src/pages/Account/Card/AccountSummary.vue @@ -1,58 +1,41 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; - import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; - -import { useArrayData } from 'src/composables/useArrayData'; +import filter from './AccountFilter.js'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; +const $props = defineProps({ id: { type: Number, default: 0 } }); + const route = useRoute(); -const { t } = useI18n(); - -const $props = defineProps({ - id: { - type: Number, - default: 0, - }, -}); -const { store } = useArrayData('Account'); -const account = ref(store.data); - const entityId = computed(() => $props.id || route.params.id); -const filter = { - where: { id: entityId }, - fields: ['id', 'nickname', 'name', 'role'], - include: { relation: 'role', scope: { fields: ['id', 'name'] } }, -}; </script> <template> <CardSummary - data-key="AccountId" + data-key="Account" + ref="AccountSummary" url="VnUsers/preview" :filter="filter" - @on-fetch="(data) => (account = data)" > - <template #header>{{ account.id }} - {{ account.nickname }}</template> - <template #menu=""> + <template #header="{ entity }">{{ entity.id }} - {{ entity.nickname }}</template> + <template #menu> <AccountDescriptorMenu :entity-id="entityId" /> </template> - <template #body> + <template #body="{ entity }"> <QCard class="vn-one"> <QCardSection class="q-pa-none"> <router-link :to="{ name: 'AccountBasicData', params: { id: entityId } }" class="header header-link" > - {{ t('globals.pageTitles.basicData') }} + {{ $t('globals.pageTitles.basicData') }} <QIcon name="open_in_new" /> </router-link> </QCardSection> - <VnLv :label="t('account.card.nickname')" :value="account.name" /> - <VnLv :label="t('account.card.role')" :value="account.role.name" /> + <VnLv :label="$t('account.card.nickname')" :value="entity.name" /> + <VnLv :label="$t('account.card.role')" :value="entity.role?.name" /> </QCard> </template> </CardSummary> diff --git a/src/pages/Account/Role/AccountRoles.vue b/src/pages/Account/Role/AccountRoles.vue index 3c3d6b243..02f5400c6 100644 --- a/src/pages/Account/Role/AccountRoles.vue +++ b/src/pages/Account/Role/AccountRoles.vue @@ -5,6 +5,7 @@ import VnTable from 'components/VnTable/VnTable.vue'; import { useRoute } from 'vue-router'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import RoleSummary from './Card/RoleSummary.vue'; +import exprBuilder from './RoleExprBuilder.js'; import VnSection from 'src/components/common/VnSection.vue'; const route = useRoute(); @@ -66,24 +67,7 @@ const columns = computed(() => [ ], }, ]); -const exprBuilder = (param, value) => { - switch (param) { - case 'search': - return /^\d+$/.test(value) - ? { id: value } - : { - or: [ - { name: { like: `%${value}%` } }, - { nickname: { like: `%${value}%` } }, - ], - }; - case 'name': - case 'description': - return { [param]: { like: `%${value}%` } }; - } -}; </script> - <template> <VnSection :data-key="dataKey" diff --git a/src/pages/Account/Role/Card/RoleBasicData.vue b/src/pages/Account/Role/Card/RoleBasicData.vue index 1de9ff387..de70b0fb6 100644 --- a/src/pages/Account/Role/Card/RoleBasicData.vue +++ b/src/pages/Account/Role/Card/RoleBasicData.vue @@ -1,24 +1,16 @@ <script setup> -import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; -const route = useRoute(); -const { t } = useI18n(); </script> <template> - <FormModel :url="`VnRoles/${route.params.id}`" model="VnRole" auto-load> + <FormModel model="Role" auto-load> <template #form="{ data }"> <VnRow> - <div class="col"> - <VnInput v-model="data.name" :label="t('globals.name')" /> - </div> + <VnInput v-model="data.name" :label="$t('globals.name')" /> </VnRow> <VnRow> - <div class="col"> - <VnInput v-model="data.description" :label="t('role.description')" /> - </div> + <VnInput v-model="data.description" :label="$t('role.description')" /> </VnRow> </template> </FormModel> diff --git a/src/pages/Account/Role/Card/RoleCard.vue b/src/pages/Account/Role/Card/RoleCard.vue index 7664deca8..ef5b9db04 100644 --- a/src/pages/Account/Role/Card/RoleCard.vue +++ b/src/pages/Account/Role/Card/RoleCard.vue @@ -3,5 +3,10 @@ import VnCardBeta from 'components/common/VnCardBeta.vue'; import RoleDescriptor from './RoleDescriptor.vue'; </script> <template> - <VnCardBeta data-key="Role" :descriptor="RoleDescriptor" /> + <VnCardBeta + url="VnRoles" + data-key="Role" + :id-in-where="true" + :descriptor="RoleDescriptor" + /> </template> diff --git a/src/pages/Account/Role/Card/RoleDescriptor.vue b/src/pages/Account/Role/Card/RoleDescriptor.vue index 0a555346d..517517af0 100644 --- a/src/pages/Account/Role/Card/RoleDescriptor.vue +++ b/src/pages/Account/Role/Card/RoleDescriptor.vue @@ -1,10 +1,9 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; const $props = defineProps({ @@ -26,11 +25,6 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.name, entity.id)); -const filter = { - where: { id: entityId }, -}; const removeRole = async () => { await axios.delete(`VnRoles/${entityId.value}`); notify(t('Role removed'), 'positive'); @@ -39,13 +33,9 @@ const removeRole = async () => { <template> <CardDescriptor - :url="`VnRoles/${entityId}`" - :filter="filter" - module="Role" - @on-fetch="setData" + url="VnRoles" + :filter="{ where: { id: entityId } }" data-key="Role" - :title="data.title" - :subtitle="data.subtitle" :summary="$props.summary" > <template #menu> diff --git a/src/pages/Account/Role/Card/RoleSummary.vue b/src/pages/Account/Role/Card/RoleSummary.vue index f0daa77fb..410f90b17 100644 --- a/src/pages/Account/Role/Card/RoleSummary.vue +++ b/src/pages/Account/Role/Card/RoleSummary.vue @@ -1,10 +1,9 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import { useArrayData } from 'src/composables/useArrayData'; const route = useRoute(); const { t } = useI18n(); @@ -16,24 +15,18 @@ const $props = defineProps({ }, }); -const { store } = useArrayData('Role'); -const role = ref(store.data); const entityId = computed(() => $props.id || route.params.id); -const filter = { - where: { id: entityId }, -}; </script> <template> <CardSummary ref="summary" - :url="`VnRoles/${entityId}`" - :filter="filter" - @on-fetch="(data) => (role = data)" + url="VnRoles" + :filter="{ where: { id: entityId } }" data-key="Role" > - <template #header> {{ role.id }} - {{ role.name }} </template> - <template #body> + <template #header="{ entity }"> {{ entity.id }} - {{ entity.name }} </template> + <template #body="{ entity }"> <QCard class="vn-one"> <QCardSection class="q-pa-none"> <a @@ -44,9 +37,9 @@ const filter = { <QIcon name="open_in_new" /> </a> </QCardSection> - <VnLv :label="t('role.id')" :value="role.id" /> - <VnLv :label="t('globals.name')" :value="role.name" /> - <VnLv :label="t('role.description')" :value="role.description" /> + <VnLv :label="t('role.id')" :value="entity.id" /> + <VnLv :label="t('globals.name')" :value="entity.name" /> + <VnLv :label="t('role.description')" :value="entity.description" /> </QCard> </template> </CardSummary> diff --git a/src/pages/Account/Role/Card/SubRoles.vue b/src/pages/Account/Role/Card/SubRoles.vue index 0077f12b0..99cf5e8f0 100644 --- a/src/pages/Account/Role/Card/SubRoles.vue +++ b/src/pages/Account/Role/Card/SubRoles.vue @@ -63,7 +63,7 @@ watch( store.url = urlPath.value; store.filter = filter.value; fetchSubRoles(); - } + }, ); const fetchSubRoles = () => paginateRef.value.fetch(); @@ -109,7 +109,7 @@ const redirectToRoleSummary = (id) => openConfirmationModal( t('El rol va a ser eliminado'), t('¿Seguro que quieres continuar?'), - () => deleteSubRole(row, rows, rowIndex) + () => deleteSubRole(row, rows, rowIndex), ) " > @@ -131,7 +131,7 @@ const redirectToRoleSummary = (id) => <QBtn fab icon="add" - shortcut="+" + v-shortcut="'+'" color="primary" @click="openCreateSubRoleForm()" > diff --git a/src/pages/Account/Role/RoleExprBuilder.js b/src/pages/Account/Role/RoleExprBuilder.js new file mode 100644 index 000000000..cc4fab399 --- /dev/null +++ b/src/pages/Account/Role/RoleExprBuilder.js @@ -0,0 +1,16 @@ +export default (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { + or: [ + { name: { like: `%${value}%` } }, + { nickname: { like: `%${value}%` } }, + ], + }; + case 'name': + case 'description': + return { [param]: { like: `%${value}%` } }; + } +}; diff --git a/src/pages/Claim/Card/ClaimBasicData.vue b/src/pages/Claim/Card/ClaimBasicData.vue index 63b0b7c0d..67034da1a 100644 --- a/src/pages/Claim/Card/ClaimBasicData.vue +++ b/src/pages/Claim/Card/ClaimBasicData.vue @@ -28,7 +28,6 @@ const workersOptions = ref([]); model="Claim" :url-update="`Claims/updateClaim/${route.params.id}`" auto-load - :reload="true" > <template #form="{ data, validate }"> <VnRow> diff --git a/src/pages/Claim/Card/ClaimCard.vue b/src/pages/Claim/Card/ClaimCard.vue index e1e000815..05f3b53a8 100644 --- a/src/pages/Claim/Card/ClaimCard.vue +++ b/src/pages/Claim/Card/ClaimCard.vue @@ -4,10 +4,11 @@ import ClaimDescriptor from './ClaimDescriptor.vue'; import filter from './ClaimFilter.js'; </script> <template> - <VnCardBeta - data-key="Claim" - base-url="Claims" - :descriptor="ClaimDescriptor" + <VnCardBeta + data-key="Claim" + url="Claims" + :descriptor="ClaimDescriptor" + search-data-key="ClaimList" :filter="filter" /> </template> diff --git a/src/pages/Claim/Card/ClaimDescriptor.vue b/src/pages/Claim/Card/ClaimDescriptor.vue index 02b63dd8e..4551c58fe 100644 --- a/src/pages/Claim/Card/ClaimDescriptor.vue +++ b/src/pages/Claim/Card/ClaimDescriptor.vue @@ -3,12 +3,10 @@ import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { toDateHourMinSec, toPercentage } from 'src/filters'; -import { useState } from 'src/composables/useState'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import { getUrl } from 'src/composables/getUrl'; import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; @@ -23,7 +21,6 @@ const $props = defineProps({ }); const route = useRoute(); -const state = useState(); const { t } = useI18n(); const salixUrl = ref(); const entityId = computed(() => { @@ -39,12 +36,7 @@ const STATE_COLOR = { function stateColor(code) { return STATE_COLOR[code]; } -const data = ref(useCardDescription()); -const setData = (entity) => { - if (!entity) return; - data.value = useCardDescription(entity?.client?.name, entity.id); - state.set('ClaimDescriptor', entity); -}; + onMounted(async () => { salixUrl.value = await getUrl(''); }); @@ -54,9 +46,7 @@ onMounted(async () => { <CardDescriptor :url="`Claims/${entityId}`" :filter="filter" - module="Claim" title="client.name" - @on-fetch="setData" data-key="Claim" > <template #menu="{ entity }"> @@ -95,7 +85,7 @@ onMounted(async () => { /> </template> </VnLv> - <VnLv :label="t('claim.zone')"> + <VnLv v-if="entity.ticket?.zone?.id" :label="t('claim.zone')"> <template #value> <span class="link"> {{ entity.ticket?.zone?.name }} @@ -107,11 +97,10 @@ onMounted(async () => { :label="t('claim.province')" :value="entity.ticket?.address?.province?.name" /> - <VnLv :label="t('claim.ticketId')"> + <VnLv v-if="entity.ticketFk" :label="t('claim.ticketId')"> <template #value> <span class="link"> {{ entity.ticketFk }} - <TicketDescriptorProxy :id="entity.ticketFk" /> </span> </template> diff --git a/src/pages/Claim/Card/ClaimLines.vue b/src/pages/Claim/Card/ClaimLines.vue index 33fadd020..dee03b95d 100644 --- a/src/pages/Claim/Card/ClaimLines.vue +++ b/src/pages/Claim/Card/ClaimLines.vue @@ -317,7 +317,13 @@ async function saveWhenHasChanges() { </div> <QPageSticky position="bottom-right" :offset="[25, 25]"> - <QBtn fab color="primary" shortcut="+" icon="add" @click="showImportDialog()" /> + <QBtn + fab + color="primary" + v-shortcut="'+'" + icon="add" + @click="showImportDialog()" + /> </QPageSticky> </template> diff --git a/src/pages/Claim/Card/ClaimNotes.vue b/src/pages/Claim/Card/ClaimNotes.vue index 134ee33ab..cc6e33779 100644 --- a/src/pages/Claim/Card/ClaimNotes.vue +++ b/src/pages/Claim/Card/ClaimNotes.vue @@ -1,5 +1,5 @@ <script setup> -import { computed } from 'vue'; +import { computed, useAttrs } from 'vue'; import { useRoute } from 'vue-router'; import { useState } from 'src/composables/useState'; import VnNotes from 'src/components/ui/VnNotes.vue'; @@ -7,6 +7,7 @@ import VnNotes from 'src/components/ui/VnNotes.vue'; const route = useRoute(); const state = useState(); const user = state.getUser(); +const $attrs = useAttrs(); const $props = defineProps({ id: { type: [Number, String], default: null }, diff --git a/src/pages/Claim/Card/ClaimPhoto.vue b/src/pages/Claim/Card/ClaimPhoto.vue index d4321d8eb..d4acc9bbe 100644 --- a/src/pages/Claim/Card/ClaimPhoto.vue +++ b/src/pages/Claim/Card/ClaimPhoto.vue @@ -61,7 +61,7 @@ watch( () => { claimDmsFilter.value.where.id = router.currentRoute.value.params.id; claimDmsRef.value.fetch(); - } + }, ); function openDialog(dmsId) { @@ -248,7 +248,7 @@ function onDrag() { <QBtn fab @click="inputFile.nativeEl.click()" - shortcut="+" + v-shortcut="'+'" icon="add" color="primary" > diff --git a/src/pages/Claim/ClaimList.vue b/src/pages/Claim/ClaimList.vue index 63fd035da..41d0c5598 100644 --- a/src/pages/Claim/ClaimList.vue +++ b/src/pages/Claim/ClaimList.vue @@ -132,7 +132,7 @@ const STATE_COLOR = { prefix="claim" :array-data-props="{ url: 'Claims/filter', - order: ['cs.priority ASC', 'created ASC'], + order: 'cs.priority ASC, created ASC', }" > <template #advanced-menu> diff --git a/src/pages/Customer/Card/CustomerAddress.vue b/src/pages/Customer/Card/CustomerAddress.vue index 1b0d1dde1..f1799d0cc 100644 --- a/src/pages/Customer/Card/CustomerAddress.vue +++ b/src/pages/Customer/Card/CustomerAddress.vue @@ -61,7 +61,7 @@ watch( (newValue) => { if (!newValue) return; getClientData(newValue); - } + }, ); const getClientData = async (id) => { @@ -137,7 +137,7 @@ const toCustomerAddressEdit = (addressId) => { <QIcon :style="{ 'font-variation-settings': `'FILL' ${isDefaultAddress( - item + item, )}`, }" color="primary" @@ -150,7 +150,7 @@ const toCustomerAddressEdit = (addressId) => { t( isDefaultAddress(item) ? 'Default address' - : 'Set as default' + : 'Set as default', ) }} </QTooltip> @@ -216,7 +216,7 @@ const toCustomerAddressEdit = (addressId) => { color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('New consignee') }} diff --git a/src/pages/Customer/Card/CustomerBalance.vue b/src/pages/Customer/Card/CustomerBalance.vue index 04ef5f882..11db92eab 100644 --- a/src/pages/Customer/Card/CustomerBalance.vue +++ b/src/pages/Customer/Card/CustomerBalance.vue @@ -158,7 +158,7 @@ const columns = computed(() => [ openConfirmationModal( t('Send compensation'), t('Do you want to report compensation to the client by mail?'), - () => sendEmail(`Receipts/${id}/balance-compensation-email`) + () => sendEmail(`Receipts/${id}/balance-compensation-email`), ), }, ], @@ -291,7 +291,7 @@ const showBalancePdf = ({ id }) => { color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('New payment') }} diff --git a/src/pages/Customer/Card/CustomerBasicData.vue b/src/pages/Customer/Card/CustomerBasicData.vue index e9a349e0b..36ec4763e 100644 --- a/src/pages/Customer/Card/CustomerBasicData.vue +++ b/src/pages/Customer/Card/CustomerBasicData.vue @@ -54,10 +54,10 @@ function onBeforeSave(formData, originalData) { auto-load /> <FormModel - :url="`Clients/${route.params.id}`" + :url-update="`Clients/${route.params.id}`" auto-load - model="customer" :mapper="onBeforeSave" + model="Customer" > <template #form="{ data, validate }"> <VnRow> diff --git a/src/pages/Customer/Card/CustomerBillingData.vue b/src/pages/Customer/Card/CustomerBillingData.vue index f1e78d9e5..cc894d01e 100644 --- a/src/pages/Customer/Card/CustomerBillingData.vue +++ b/src/pages/Customer/Card/CustomerBillingData.vue @@ -27,7 +27,7 @@ const getBankEntities = (data, formData) => { </script> <template> - <FormModel :url-update="`Clients/${route.params.id}`" auto-load model="customer"> + <FormModel :url-update="`Clients/${route.params.id}`" auto-load model="Customer"> <template #form="{ data, validate }"> <VnRow> <VnSelect diff --git a/src/pages/Customer/Card/CustomerCard.vue b/src/pages/Customer/Card/CustomerCard.vue index f46884834..75fcb98fa 100644 --- a/src/pages/Customer/Card/CustomerCard.vue +++ b/src/pages/Customer/Card/CustomerCard.vue @@ -5,8 +5,8 @@ import CustomerDescriptor from './CustomerDescriptor.vue'; <template> <VnCardBeta - data-key="Client" - base-url="Clients" + data-key="Customer" + :url="`Clients/${$route.params.id}/getCard`" :descriptor="CustomerDescriptor" /> </template> diff --git a/src/pages/Customer/Card/CustomerConsumption.vue b/src/pages/Customer/Card/CustomerConsumption.vue index f0d8dea47..f3949bb32 100644 --- a/src/pages/Customer/Card/CustomerConsumption.vue +++ b/src/pages/Customer/Card/CustomerConsumption.vue @@ -61,6 +61,23 @@ const columns = computed(() => [ columnFilter: false, cardVisible: true, }, + { + align: 'left', + name: 'buyerId', + label: t('customer.params.buyerId'), + component: 'select', + attrs: { + url: 'TicketRequests/getItemTypeWorker', + optionLabel: 'nickname', + optionValue: 'id', + + fields: ['id', 'nickname'], + sortBy: ['nickname ASC'], + optionFilter: 'firstName', + }, + cardVisible: false, + visible: false, + }, { name: 'description', align: 'left', @@ -74,6 +91,7 @@ const columns = computed(() => [ name: 'quantity', label: t('globals.quantity'), cardVisible: true, + visible: true, columnFilter: { inWhere: true, }, @@ -119,7 +137,7 @@ const openSendEmailDialog = async () => { openConfirmationModal( t('The consumption report will be sent'), t('Please, confirm'), - () => sendCampaignMetricsEmail({ address: arrayData.store.data.email }) + () => sendCampaignMetricsEmail({ address: arrayData.store.data.email }), ); }; const sendCampaignMetricsEmail = ({ address }) => { @@ -138,11 +156,11 @@ const updateDateParams = (value, params) => { const campaign = campaignList.value.find((c) => c.id === value); if (!campaign) return; - const { dated, previousDays, scopeDays } = campaign; - const _date = new Date(dated); - const [from, to] = dateRange(_date); - params.from = new Date(from.setDate(from.getDate() - previousDays)).toISOString(); - params.to = new Date(to.setDate(to.getDate() + scopeDays)).toISOString(); + const { dated, scopeDays } = campaign; + const from = new Date(dated); + from.setDate(from.getDate() - scopeDays); + params.from = from; + params.to = dated; return params; }; </script> @@ -152,7 +170,7 @@ const updateDateParams = (value, params) => { v-if="campaignList" data-key="CustomerConsumption" url="Clients/consumption" - :order="['itemTypeFk', 'itemName', 'itemSize', 'description']" + :order="['itemTypeFk', 'itemName', 'itemSize', 'description']" :filter="{ where: { clientFk: route.params.id } }" :columns="columns" search-url="consumption" @@ -200,29 +218,60 @@ const updateDateParams = (value, params) => { <div v-if="row.subName" class="subName"> {{ row.subName }} </div> - <FetchedTags :item="row" :max-length="3" /> + <FetchedTags :item="row" /> </template> <template #moreFilterPanel="{ params }"> <div class="column no-wrap flex-center q-gutter-y-md q-mt-xs q-pr-xl"> + <VnSelect + :filled="true" + class="q-px-sm q-pt-none fit" + url="ItemTypes" + v-model="params.typeId" + :label="t('item.list.typeName')" + :fields="['id', 'name', 'categoryFk']" + :include="'category'" + :sortBy="'name ASC'" + dense + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel>{{ scope.opt?.name }}</QItemLabel> + <QItemLabel caption>{{ + scope.opt?.category?.name + }}</QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnSelect + :filled="true" + class="q-px-sm q-pt-none fit" + url="ItemCategories" + v-model="params.categoryId" + :label="t('item.list.category')" + :fields="['id', 'name']" + :sortBy="'name ASC'" + dense + /> <VnSelect v-model="params.campaign" :options="campaignList" :label="t('globals.campaign')" :filled="true" class="q-px-sm q-pt-none fit" - dense - option-label="code" + :option-label="(opt) => t(opt.code)" + :fields="['id', 'code', 'dated', 'scopeDays']" @update:model-value="(data) => updateDateParams(data, params)" + dense > <template #option="scope"> <QItem v-bind="scope.itemProps"> <QItemSection> - <QItemLabel> - {{ scope.opt?.code }} - {{ - new Date(scope.opt?.dated).getFullYear() - }}</QItemLabel - > + <QItemLabel> {{ t(scope.opt?.code) }} </QItemLabel> + <QItemLabel caption> + {{ new Date(scope.opt?.dated).getFullYear() }} + </QItemLabel> </QItemSection> </QItem> </template> @@ -247,7 +296,21 @@ const updateDateParams = (value, params) => { </template> <i18n> +en: + + valentinesDay: Valentine's Day + mothersDay: Mother's Day + allSaints: All Saints' Day + frenchMothersDay: Mother's Day in France es: Enter a new search: Introduce una nueva búsqueda Group by items: Agrupar por artículos + valentinesDay: Día de San Valentín + mothersDay: Día de la Madre + allSaints: Día de Todos los Santos + frenchMothersDay: (Francia) Día de la Madre + Campaign consumption: Consumo campaña + Campaign: Campaña + From: Desde + To: Hasta </i18n> diff --git a/src/pages/Customer/Card/CustomerContacts.vue b/src/pages/Customer/Card/CustomerContacts.vue index c420f650e..d03f71244 100644 --- a/src/pages/Customer/Card/CustomerContacts.vue +++ b/src/pages/Customer/Card/CustomerContacts.vue @@ -62,7 +62,7 @@ const customerContactsRef = ref(null); color="primary" flat icon="add" - shortcut="+" + v-shortcut="'+'" > <QTooltip> {{ t('Add contact') }} diff --git a/src/pages/Customer/Card/CustomerCreditContracts.vue b/src/pages/Customer/Card/CustomerCreditContracts.vue index 7dc53db72..a49faeb8d 100644 --- a/src/pages/Customer/Card/CustomerCreditContracts.vue +++ b/src/pages/Customer/Card/CustomerCreditContracts.vue @@ -195,7 +195,7 @@ const updateData = () => { color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('New contract') }} diff --git a/src/pages/Customer/Card/CustomerDescriptor.vue b/src/pages/Customer/Card/CustomerDescriptor.vue index d7a8a59a1..89f9d9449 100644 --- a/src/pages/Customer/Card/CustomerDescriptor.vue +++ b/src/pages/Customer/Card/CustomerDescriptor.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed } from 'vue'; +import { onMounted, ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; @@ -11,6 +11,15 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue'; +import { useState } from 'src/composables/useState'; +const state = useState(); + +const customer = ref(); + +onMounted(async () => { + customer.value = state.get('Customer'); + if (customer.value) customer.value.webAccess = data.value?.account?.isActive; +}); const customerDebt = ref(); const customerCredit = ref(); @@ -46,13 +55,10 @@ const debtWarning = computed(() => { <template> <CardDescriptor - module="Customer" :url="`Clients/${entityId}/getCard`" - :title="data.title" - :subtitle="data.subtitle" - @on-fetch="setData" :summary="$props.summary" - data-key="customer" + data-key="Customer" + @on-fetch="setData" width="lg-width" > <template #menu="{ entity }"> @@ -61,7 +67,7 @@ const debtWarning = computed(() => { <template #body="{ entity }"> <VnLv :label="t('customer.summary.payMethod')" - :value="entity.payMethod.name" + :value="entity.payMethod?.name" /> <VnLv @@ -90,7 +96,7 @@ const debtWarning = computed(() => { </VnLv> <VnLv :label="t('customer.extendedList.tableVisibleColumns.businessTypeFk')" - :value="entity.businessType.description" + :value="entity.businessType?.description" /> </template> <template #icons="{ entity }"> @@ -103,7 +109,21 @@ const debtWarning = computed(() => { > <QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip> </QIcon> - <QIcon v-if="entity.isFreezed" name="vn:frozen" size="xs" color="primary"> + + <QIcon + v-if="entity?.substitutionAllowed" + name="help" + size="xs" + color="primary" + > + <QTooltip>{{ t('Allowed substitution') }}</QTooltip> + </QIcon> + <QIcon + v-if="customer?.isFreezed" + name="vn:frozen" + size="xs" + color="primary" + > <QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip> </QIcon> <QIcon @@ -143,13 +163,13 @@ const debtWarning = computed(() => { <br /> {{ t('unpaidDated', { - dated: toDate(customer.unpaid.dated), + dated: toDate(customer.unpaid?.dated), }) }} <br /> {{ t('unpaidAmount', { - amount: toCurrency(customer.unpaid.amount), + amount: toCurrency(customer.unpaid?.amount), }) }} </QTooltip> diff --git a/src/pages/Customer/Card/CustomerDescriptorMenu.vue b/src/pages/Customer/Card/CustomerDescriptorMenu.vue index fb78eab69..aea45721c 100644 --- a/src/pages/Customer/Card/CustomerDescriptorMenu.vue +++ b/src/pages/Customer/Card/CustomerDescriptorMenu.vue @@ -61,6 +61,16 @@ const openCreateForm = (type) => { .join('&'); useOpenURL(`/#/${type}/list?${params}`); }; +const updateSubstitutionAllowed = async () => { + try { + await axios.patch(`Clients/${route.params.id}`, { + substitutionAllowed: !$props.customer.substitutionAllowed, + }); + notify('globals.notificationSent', 'positive'); + } catch (error) { + notify(error.message, 'positive'); + } +}; </script> <template> @@ -69,6 +79,13 @@ const openCreateForm = (type) => { {{ t('globals.pageTitles.createTicket') }} </QItemSection> </QItem> + <QItem v-ripple clickable> + <QItemSection @click="updateSubstitutionAllowed()">{{ + $props.customer.substitutionAllowed + ? t('Disable substitution') + : t('Allow substitution') + }}</QItemSection> + </QItem> <QItem v-ripple clickable> <QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection> </QItem> diff --git a/src/pages/Customer/Card/CustomerFileManagement.vue b/src/pages/Customer/Card/CustomerFileManagement.vue index 134d8dbd6..b565db6e7 100644 --- a/src/pages/Customer/Card/CustomerFileManagement.vue +++ b/src/pages/Customer/Card/CustomerFileManagement.vue @@ -236,7 +236,7 @@ const toCustomerFileManagementCreate = () => { @click.stop="toCustomerFileManagementCreate()" color="primary" fab - shortcut="+" + v-shortcut="'+'" icon="add" /> <QTooltip> diff --git a/src/pages/Customer/Card/CustomerFiscalData.vue b/src/pages/Customer/Card/CustomerFiscalData.vue index ceeb70bb6..93909eb9c 100644 --- a/src/pages/Customer/Card/CustomerFiscalData.vue +++ b/src/pages/Customer/Card/CustomerFiscalData.vue @@ -12,6 +12,7 @@ import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; import { getDifferences, getUpdatedValues } from 'src/filters'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; @@ -73,7 +74,7 @@ async function acceptPropagate({ isEqualizated }) { <FormModel :url-update="`Clients/${route.params.id}/updateFiscalData`" auto-load - model="customer" + model="Customer" :mapper="onBeforeSave" observe-form-changes @on-data-saved="checkEtChanges" @@ -151,14 +152,11 @@ async function acceptPropagate({ isEqualizated }) { </VnRow> <VnRow> <QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" /> - <div> - <QCheckbox :label="t('globals.isVies')" v-model="data.isVies" /> - <QIcon name="info" class="cursor-info q-ml-sm" size="sm"> - <QTooltip> - {{ t('whenActivatingIt') }} - </QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="data.isVies" + :label="t('globals.isVies')" + :info="t('whenActivatingIt')" + /> </VnRow> <VnRow> @@ -170,17 +168,11 @@ async function acceptPropagate({ isEqualizated }) { </VnRow> <VnRow> - <div> - <QCheckbox - :label="t('Is equalizated')" - v-model="data.isEqualizated" - /> - <QIcon class="cursor-info q-ml-sm" name="info" size="sm"> - <QTooltip> - {{ t('inOrderToInvoice') }} - </QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="data.isEqualizated" + :label="t('Is equalizated')" + :info="t('inOrderToInvoice')" + /> <QCheckbox :label="t('Daily invoice')" v-model="data.hasDailyInvoice" /> </VnRow> diff --git a/src/pages/Customer/Card/CustomerNotes.vue b/src/pages/Customer/Card/CustomerNotes.vue index b85174696..189b59904 100644 --- a/src/pages/Customer/Card/CustomerNotes.vue +++ b/src/pages/Customer/Card/CustomerNotes.vue @@ -23,5 +23,6 @@ const noteFilter = computed(() => { :body="{ clientFk: route.params.id }" style="overflow-y: auto" :select-type="true" + required /> </template> diff --git a/src/pages/Customer/Card/CustomerSamples.vue b/src/pages/Customer/Card/CustomerSamples.vue index f12691112..19a7f8759 100644 --- a/src/pages/Customer/Card/CustomerSamples.vue +++ b/src/pages/Customer/Card/CustomerSamples.vue @@ -104,7 +104,7 @@ const tableRef = ref(); color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('Send sample') }} diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index 3c4106846..809f10918 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -27,7 +27,7 @@ async function hasCustomerRole() { <FormModel :url-update="`Clients/${route.params.id}/updateUser`" :filter="filter" - model="customer" + model="Customer" :mapper=" ({ account }) => { const { name, email, active } = account; diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index 9b883daad..1c5a08304 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -51,11 +51,7 @@ const exprBuilder = (param, value) => { </QItem> <QItem class="q-mb-sm"> <QItemSection> - <VnInput - :label="t('globals.name')" - v-model="params.name" - is-outlined - /> + <VnInput :label="t('Name')" v-model="params.name" is-outlined /> </QItemSection> </QItem> <QItem class="q-mb-sm"> diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index 2f2dd5978..0bfca7910 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -274,6 +274,7 @@ const columns = computed(() => [ align: 'left', name: 'isActive', label: t('customer.summary.isActive'), + component: 'checkbox', chip: { color: null, condition: (value) => !value, @@ -312,6 +313,7 @@ const columns = computed(() => [ align: 'left', name: 'isFreezed', label: t('customer.extendedList.tableVisibleColumns.isFreezed'), + component: 'checkbox', chip: { color: null, condition: (value) => value, @@ -429,7 +431,7 @@ function handleLocation(data, location) { <VnTable ref="tableRef" :data-key="dataKey" - url="Clients/filter" + url="Clients/extendedListFilter" :create="{ urlCreate: 'Clients/createWithUser', title: t('globals.pageTitles.customerCreate'), diff --git a/src/pages/Customer/Defaulter/CustomerDefaulter.vue b/src/pages/Customer/Defaulter/CustomerDefaulter.vue index eca2ad596..dc4ac9162 100644 --- a/src/pages/Customer/Defaulter/CustomerDefaulter.vue +++ b/src/pages/Customer/Defaulter/CustomerDefaulter.vue @@ -9,7 +9,7 @@ import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.v import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnInput from 'src/components/common/VnInput.vue'; import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue'; -import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; import VnTable from 'src/components/VnTable/VnTable.vue'; import { useArrayData } from 'src/composables/useArrayData'; diff --git a/src/pages/Customer/components/CustomerAddressEdit.vue b/src/pages/Customer/components/CustomerAddressEdit.vue index d650bbbda..f852c160a 100644 --- a/src/pages/Customer/components/CustomerAddressEdit.vue +++ b/src/pages/Customer/components/CustomerAddressEdit.vue @@ -233,7 +233,7 @@ function handleLocation(data, location) { postcode: data.postalCode, city: data.city, province: data.province, - country: data.province.country, + country: data.province?.country, }" @update:model-value="(location) => handleLocation(data, location)" ></VnLocation> @@ -336,7 +336,7 @@ function handleLocation(data, location) { class="cursor-pointer add-icon q-mt-md" flat icon="add" - shortcut="+" + v-shortcut="'+'" > <QTooltip> {{ t('Add note') }} diff --git a/src/pages/Customer/components/CustomerNewPayment.vue b/src/pages/Customer/components/CustomerNewPayment.vue index c2c38b55a..8f61bac89 100644 --- a/src/pages/Customer/components/CustomerNewPayment.vue +++ b/src/pages/Customer/components/CustomerNewPayment.vue @@ -84,7 +84,7 @@ function setPaymentType(accounting) { viewReceipt.value = isCash.value; if (accountingType.value.daysInFuture) initialData.payed.setDate( - initialData.payed.getDate() + accountingType.value.daysInFuture + initialData.payed.getDate() + accountingType.value.daysInFuture, ); maxAmount.value = accountingType.value && accountingType.value.maxAmount; @@ -114,7 +114,7 @@ function onBeforeSave(data) { if (isCash.value && shouldSendEmail.value && !data.email) return notify(t('There is no assigned email for this client'), 'negative'); - data.bankFk = data.bankFk.id; + data.bankFk = data.bankFk?.id; return data; } @@ -189,7 +189,7 @@ async function getAmountPaid() { :url-create="urlCreate" :mapper="onBeforeSave" @on-data-saved="onDataSaved" - :prevent-submit="true" + prevent-submit > <template #form="{ data, validate }"> <span ref="closeButton" class="row justify-end close-icon" v-close-popup> diff --git a/src/pages/Customer/components/CustomerSamplesCreate.vue b/src/pages/Customer/components/CustomerSamplesCreate.vue index 754693672..1294a5d25 100644 --- a/src/pages/Customer/components/CustomerSamplesCreate.vue +++ b/src/pages/Customer/components/CustomerSamplesCreate.vue @@ -18,6 +18,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import CustomerSamplesPreview from 'src/pages/Customer/components/CustomerSamplesPreview.vue'; import FormPopup from 'src/components/FormPopup.vue'; +import { useArrayData } from 'src/composables/useArrayData'; const { dialogRef, onDialogOK } = useDialogPluginComponent(); @@ -39,7 +40,7 @@ const optionsSamplesVisible = ref([]); const sampleType = ref({ hasPreview: false }); const initialData = reactive({}); const entityId = computed(() => route.params.id); -const customer = computed(() => state.get('customer')); +const customer = computed(() => useArrayData('Customer').store?.data); const filterEmailUsers = { where: { userFk: user.value.id } }; const filterClientsAddresses = { include: [ @@ -65,9 +66,9 @@ const filterSamplesVisible = { defineEmits(['confirm', ...useDialogPluginComponent.emits]); onBeforeMount(async () => { - initialData.clientFk = customer.value.id; - initialData.recipient = customer.value.email; - initialData.recipientId = customer.value.id; + initialData.clientFk = customer.value?.id; + initialData.recipient = customer.value?.email; + initialData.recipientId = customer.value?.id; }); const setEmailUser = (data) => { diff --git a/src/pages/Customer/locale/en.yml b/src/pages/Customer/locale/en.yml index 118f04a31..b6d495335 100644 --- a/src/pages/Customer/locale/en.yml +++ b/src/pages/Customer/locale/en.yml @@ -107,6 +107,9 @@ customer: defaulterSinced: Defaulted Since hasRecovery: Has Recovery socialName: Social name + typeId: Type + buyerId: Buyer + categoryId: Category city: City phone: Phone postcode: Postcode diff --git a/src/pages/Customer/locale/es.yml b/src/pages/Customer/locale/es.yml index 7c33ffee8..f50d049da 100644 --- a/src/pages/Customer/locale/es.yml +++ b/src/pages/Customer/locale/es.yml @@ -108,6 +108,9 @@ customer: hasRecovery: Tiene recobro socialName: Razón social campaign: Campaña + typeId: Familia + buyerId: Comprador + categoryId: Reino city: Ciudad phone: Teléfono postcode: Código postal diff --git a/src/pages/Entry/Card/EntryBasicData.vue b/src/pages/Entry/Card/EntryBasicData.vue index 689eea686..6462ed24a 100644 --- a/src/pages/Entry/Card/EntryBasicData.vue +++ b/src/pages/Entry/Card/EntryBasicData.vue @@ -1,30 +1,32 @@ <script setup> -import { ref } from 'vue'; +import { onMounted, ref } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useRole } from 'src/composables/useRole'; +import { useState } from 'src/composables/useState'; +import { checkEntryLock } from 'src/composables/checkEntryLock'; import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; -import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; -import FilterTravelForm from 'src/components/FilterTravelForm.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; -import { toDate } from 'src/filters'; +import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue'; import VnSelectSupplier from 'src/components/common/VnSelectSupplier.vue'; const route = useRoute(); const { t } = useI18n(); const { hasAny } = useRole(); const isAdministrative = () => hasAny(['administrative']); +const state = useState(); +const user = state.getUser().fn(); const companiesOptions = ref([]); const currenciesOptions = ref([]); -const onFilterTravelSelected = (formData, id) => { - formData.travelFk = id; -}; +onMounted(() => { + checkEntryLock(route.params.id, user.id); +}); </script> <template> @@ -52,46 +54,24 @@ const onFilterTravelSelected = (formData, id) => { > <template #form="{ data }"> <VnRow> + <VnSelectTravelExtended + :data="data" + v-model="data.travelFk" + :onFilterTravelSelected="(data, result) => (data.travelFk = result)" + /> <VnSelectSupplier v-model="data.supplierFk" hide-selected :required="true" - map-options /> - <VnSelectDialog - :label="t('entry.basicData.travel')" - v-model="data.travelFk" - url="Travels/filter" - :fields="['id', 'warehouseInName']" - option-value="id" - option-label="warehouseInName" - map-options - hide-selected - :required="true" - action-icon="filter_alt" - > - <template #form> - <FilterTravelForm - @travel-selected="onFilterTravelSelected(data, $event)" - /> - </template> - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel> - {{ scope.opt?.agencyModeName }} - - {{ scope.opt?.warehouseInName }} - ({{ toDate(scope.opt?.shipped) }}) → - {{ scope.opt?.warehouseOutName }} - ({{ toDate(scope.opt?.landed) }}) - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelectDialog> </VnRow> <VnRow> <VnInput v-model="data.reference" :label="t('globals.reference')" /> + <VnInputNumber + v-model="data.invoiceAmount" + :label="t('entry.summary.invoiceAmount')" + :positive="false" + /> </VnRow> <VnRow> <VnInput @@ -113,8 +93,7 @@ const onFilterTravelSelected = (formData, id) => { <VnInputNumber :label="t('entry.summary.commission')" v-model="data.commission" - step="1" - autofocus + :step="1" :positive="false" /> <VnSelect @@ -161,7 +140,7 @@ const onFilterTravelSelected = (formData, id) => { :label="t('entry.summary.excludedFromAvailable')" /> <QCheckbox - v-if="isAdministrative()" + :disable="!isAdministrative()" v-model="data.isBooked" :label="t('entry.basicData.booked')" /> diff --git a/src/pages/Entry/Card/EntryBuys.vue b/src/pages/Entry/Card/EntryBuys.vue index 6194ce5b8..81578c609 100644 --- a/src/pages/Entry/Card/EntryBuys.vue +++ b/src/pages/Entry/Card/EntryBuys.vue @@ -1,478 +1,806 @@ <script setup> -import { ref, computed } from 'vue'; -import { useRoute, useRouter } from 'vue-router'; +import { useStateStore } from 'stores/useStateStore'; +import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { QBtn } from 'quasar'; +import { onMounted, ref } from 'vue'; -import VnPaginate from 'src/components/ui/VnPaginate.vue'; -import VnSelect from 'components/common/VnSelect.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import FetchedTags from 'components/ui/FetchedTags.vue'; -import VnConfirm from 'components/ui/VnConfirm.vue'; +import { useState } from 'src/composables/useState'; + +import FetchData from 'src/components/FetchData.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; -import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; - -import { useQuasar } from 'quasar'; -import { toCurrency } from 'src/filters'; +import FetchedTags from 'src/components/ui/FetchedTags.vue'; +import VnColor from 'src/components/common/VnColor.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import ItemDescriptor from 'src/pages/Item/Card/ItemDescriptor.vue'; import axios from 'axios'; -import useNotify from 'src/composables/useNotify.js'; +import VnSelectEnum from 'src/components/common/VnSelectEnum.vue'; +import { checkEntryLock } from 'src/composables/checkEntryLock'; -const quasar = useQuasar(); -const route = useRoute(); -const router = useRouter(); -const { t } = useI18n(); -const { notify } = useNotify(); - -const rowsSelected = ref([]); -const entryBuysPaginateRef = ref(null); -const originalRowDataCopy = ref(null); - -const getInputEvents = (colField, props) => { - return colField === 'packagingFk' - ? { 'update:modelValue': () => saveChange(colField, props) } - : { - 'keyup.enter': () => saveChange(colField, props), - blur: () => saveChange(colField, props), - }; -}; - -const tableColumnComponents = computed(() => ({ - item: { - component: QBtn, - props: { - color: 'primary', - flat: true, - }, - event: () => ({}), +const $props = defineProps({ + id: { + type: Number, + default: null, }, - quantity: { - component: VnInput, - props: { - type: 'number', - min: 0, - class: 'input-number', - dense: true, - }, - event: getInputEvents, + editableMode: { + type: Boolean, + default: true, }, - packagingFk: { - component: VnSelect, - props: { - 'option-value': 'id', - 'option-label': 'id', - 'emit-value': true, - 'map-options': true, - 'use-input': true, - 'hide-selected': true, - url: 'Packagings', - fields: ['id'], - where: { freightItemFk: true }, - 'sort-by': 'id ASC', - dense: true, - }, - event: getInputEvents, + tableHeight: { + type: String, + default: null, }, - stickers: { - component: VnInput, - props: { - type: 'number', - min: 0, - class: 'input-number', - dense: true, - }, - event: getInputEvents, - }, - printedStickers: { - component: VnInput, - props: { - type: 'number', - min: 0, - class: 'input-number', - dense: true, - }, - event: getInputEvents, - }, - weight: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - packing: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - grouping: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - buyingValue: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - price2: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - price3: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - import: { - component: 'span', - props: {}, - event: () => ({}), - }, -})); - -const entriesTableColumns = computed(() => { - return [ - { - label: t('globals.item'), - field: 'itemFk', - name: 'item', - align: 'left', - }, - { - label: t('globals.quantity'), - field: 'quantity', - name: 'quantity', - align: 'left', - }, - { - label: t('entry.summary.package'), - field: 'packagingFk', - name: 'packagingFk', - align: 'left', - }, - { - label: t('entry.summary.stickers'), - field: 'stickers', - name: 'stickers', - align: 'left', - }, - { - label: t('entry.buys.printedStickers'), - field: 'printedStickers', - name: 'printedStickers', - align: 'left', - }, - { - label: t('globals.weight'), - field: 'weight', - name: 'weight', - align: 'left', - }, - { - label: t('entry.summary.packing'), - field: 'packing', - name: 'packing', - align: 'left', - }, - { - label: t('entry.summary.grouping'), - field: 'grouping', - name: 'grouping', - align: 'left', - }, - { - label: t('entry.summary.buyingValue'), - field: 'buyingValue', - name: 'buyingValue', - align: 'left', - format: (value) => toCurrency(value), - }, - { - label: t('item.fixedPrice.groupingPrice'), - field: 'price2', - name: 'price2', - align: 'left', - }, - { - label: t('item.fixedPrice.packingPrice'), - field: 'price3', - name: 'price3', - align: 'left', - }, - { - label: t('entry.summary.import'), - name: 'import', - align: 'left', - format: (_, row) => toCurrency(row.buyingValue * row.quantity), - }, - ]; }); -const copyOriginalRowsData = (rows) => { - originalRowDataCopy.value = JSON.parse(JSON.stringify(rows)); -}; - -const saveChange = async (field, { rowIndex, row }) => { - if (originalRowDataCopy.value[rowIndex][field] == row[field]) return; - await axios.patch(`Buys/${row.id}`, row); - originalRowDataCopy.value[rowIndex][field] = row[field]; -}; - -const openRemoveDialog = async () => { - quasar - .dialog({ - component: VnConfirm, - componentProps: { - title: t('Confirm deletion'), - message: t( - `Are you sure you want to delete this buy${ - rowsSelected.value.length > 1 ? 's' : '' - }?` - ), - data: rowsSelected.value, +const state = useState(); +const user = state.getUser().fn(); +const stateStore = useStateStore(); +const { t } = useI18n(); +const route = useRoute(); +const selectedRows = ref([]); +const entityId = ref($props.id ?? route.params.id); +const entryBuysRef = ref(); +const footerFetchDataRef = ref(); +const footer = ref({}); +const columns = [ + { + align: 'center', + labelAbbreviation: 'NV', + label: t('Ignore'), + toolTip: t('Ignored for available'), + name: 'isIgnored', + component: 'checkbox', + attrs: { + toggleIndeterminate: false, + }, + create: true, + width: '25px', + }, + { + label: t('Buyer'), + name: 'workerFk', + component: 'select', + attrs: { + url: 'Workers/search', + fields: ['id', 'nickname'], + optionLabel: 'nickname', + optionValue: 'id', + }, + visible: false, + }, + { + label: t('Family'), + name: 'itemTypeFk', + component: 'select', + attrs: { + url: 'itemTypes', + fields: ['id', 'name'], + optionLabel: 'name', + optionValue: 'id', + }, + visible: false, + }, + { + name: 'id', + isId: true, + visible: false, + isEditable: false, + columnFilter: false, + }, + { + name: 'entryFk', + isId: true, + visible: false, + isEditable: false, + disable: true, + create: true, + columnFilter: false, + }, + { + align: 'center', + label: 'Id', + name: 'itemFk', + component: 'number', + isEditable: false, + width: '35px', + }, + { + labelAbbreviation: '', + label: 'Color', + name: 'hex', + columnSearch: false, + isEditable: false, + width: '9px', + component: 'select', + attrs: { + url: 'Inks', + fields: ['id', 'name'], + }, + }, + { + align: 'center', + label: t('Article'), + name: 'name', + component: 'select', + attrs: { + url: 'Items', + fields: ['id', 'name'], + optionLabel: 'name', + optionValue: 'id', + }, + width: '85px', + isEditable: false, + }, + { + align: 'center', + label: t('Article'), + name: 'itemFk', + visible: false, + create: true, + columnFilter: false, + }, + { + align: 'center', + labelAbbreviation: t('Siz.'), + label: t('Size'), + toolTip: t('Size'), + component: 'number', + name: 'size', + width: '35px', + isEditable: false, + style: () => { + return { color: 'var(--vn-label-color)' }; + }, + }, + { + align: 'center', + labelAbbreviation: t('Sti.'), + label: t('Stickers'), + toolTip: t('Printed Stickers/Stickers'), + name: 'stickers', + component: 'input', + create: true, + attrs: { + positive: false, + }, + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + row['quantity'] = value * row['packing']; + row['amount'] = row['quantity'] * row['buyingValue']; }, - }) - .onOk(async () => { - await deleteBuys(); - const notifyMessage = t( - `Buy${rowsSelected.value.length > 1 ? 's' : ''} deleted` - ); - notify(notifyMessage, 'positive'); - }); -}; + }, + width: '35px', + }, + { + align: 'center', + label: t('Bucket'), + name: 'packagingFk', + component: 'select', + attrs: { + url: 'packagings', + fields: ['id'], + optionLabel: 'id', + optionValue: 'id', + }, + create: true, + width: '40px', + }, + { + align: 'center', + label: 'Kg', + name: 'weight', + component: 'number', + create: true, + width: '35px', + format: (row) => parseFloat(row['weight']).toFixed(1), + }, + { + labelAbbreviation: 'P', + label: 'Packing', + toolTip: 'Packing', + name: 'packing', + component: 'number', + create: true, + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + const oldPacking = oldValue === 1 || oldValue === null ? 1 : oldValue; + row['weight'] = (row['weight'] * value) / oldPacking; + row['quantity'] = row['stickers'] * value; + row['amount'] = row['quantity'] * row['buyingValue']; + }, + }, + width: '30px', + style: (row) => { + if (row.groupingMode === 'grouping') + return { color: 'var(--vn-label-color)' }; + }, + }, + { + align: 'center', + labelAbbreviation: 'GM', + label: t('Grouping selector'), + toolTip: t('Grouping selector'), + name: 'groupingMode', + component: 'toggle', + attrs: { + 'toggle-indeterminate': true, + trueValue: 'grouping', + falseValue: 'packing', + indeterminateValue: null, + }, + size: 'xs', + width: '25px', + create: true, + rightFilter: false, + getIcon: (value) => { + switch (value) { + case 'grouping': + return 'toggle_on'; + case 'packing': + return 'toggle_off'; + default: + return 'minimize'; + } + }, + }, + { + align: 'center', + labelAbbreviation: 'G', + label: 'Grouping', + toolTip: 'Grouping', + name: 'grouping', + component: 'number', + width: '30px', + create: true, + style: (row) => { + if (row.groupingMode === 'packing') return { color: 'var(--vn-label-color)' }; + }, + }, + { + align: 'center', + label: t('Quantity'), + name: 'quantity', + component: 'number', + attrs: { + positive: false, + }, + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + row['amount'] = value * row['buyingValue']; + }, + }, + width: '45px', + create: true, + style: getQuantityStyle, + }, + { + align: 'center', + labelAbbreviation: t('Cost'), + label: t('Buying value'), + toolTip: t('Buying value'), + name: 'buyingValue', + create: true, + component: 'number', + attrs: { + positive: false, + }, + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + row['amount'] = row['quantity'] * value; + }, + }, + width: '45px', + format: (row) => parseFloat(row['buyingValue']).toFixed(3), + }, + { + align: 'center', + label: t('Amount'), + name: 'amount', + width: '45px', + component: 'number', + attrs: { + positive: false, + }, + isEditable: false, + format: (row) => parseFloat(row['amount']).toFixed(2), + style: getAmountStyle, + }, + { + align: 'center', + labelAbbreviation: t('Pack.'), + label: t('Package'), + toolTip: t('Package'), + name: 'price2', + component: 'number', + width: '35px', + create: true, + format: (row) => parseFloat(row['price2']).toFixed(2), + }, + { + align: 'center', + label: t('Box'), + name: 'price3', + component: 'number', + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + row['price2'] = row['price2'] * (value / oldValue); + }, + }, + width: '35px', + create: true, + format: (row) => parseFloat(row['price3']).toFixed(2), + }, + { + align: 'center', + labelAbbreviation: 'CM', + label: t('Check min price'), + toolTip: t('Check min price'), + name: 'hasMinPrice', + attrs: { + toggleIndeterminate: false, + }, + component: 'checkbox', + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + await axios.patch(`Items/${row['itemFk']}`, { + hasMinPrice: value, + }); + }, + }, + width: '25px', + }, + { + align: 'center', + labelAbbreviation: 'Min.', + label: t('Minimum price'), + toolTip: t('Minimum price'), + name: 'minPrice', + component: 'number', + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + await axios.patch(`Items/${row['itemFk']}`, { + minPrice: value, + }); + }, + }, + width: '35px', + style: (row) => { + if (!row?.hasMinPrice) return { color: 'var(--vn-label-color)' }; + }, + format: (row) => parseFloat(row['minPrice']).toFixed(2), + }, + { + align: 'center', + labelAbbreviation: t('P.Sen'), + label: t('Packing sent'), + toolTip: t('Packing sent'), + name: 'packingOut', + component: 'number', + isEditable: false, + width: '40px', + style: () => { + return { color: 'var(--vn-label-color)' }; + }, + }, + { + align: 'center', + labelAbbreviation: t('Com.'), + label: t('Comment'), + toolTip: t('Comment'), + name: 'comment', + component: 'input', + isEditable: false, + width: '50px', + }, + { + align: 'center', + labelAbbreviation: 'Prod.', + label: t('Producer'), + toolTip: t('Producer'), + name: 'subName', + isEditable: false, + width: '45px', + style: () => { + return { color: 'var(--vn-label-color)' }; + }, + }, + { + align: 'center', + label: t('Tags'), + name: 'tags', + width: '125px', + columnSearch: false, + }, + { + align: 'center', + labelAbbreviation: 'Comp.', + label: t('Company'), + toolTip: t('Company'), + name: 'company_name', + component: 'input', + isEditable: false, + width: '35px', + style: () => { + return { color: 'var(--vn-label-color)' }; + }, + }, +]; -const deleteBuys = async () => { - await axios.post('Buys/deleteBuys', { buys: rowsSelected.value }); - entryBuysPaginateRef.value.fetch(); -}; +function getQuantityStyle(row) { + if (row?.quantity !== row?.stickers * row?.packing) + return { color: 'var(--q-negative)' }; +} +function getAmountStyle(row) { + if (row?.isChecked) return { color: 'var(--q-positive)' }; + return { color: 'var(--vn-label-color)' }; +} -const importBuys = () => { - router.push({ name: 'EntryBuysImport' }); -}; +async function beforeSave(data, getChanges) { + try { + const changes = data.updates; + if (!changes) return data; + const patchPromises = []; -const toggleGroupingMode = async (buy, mode) => { - const groupingMode = mode === 'grouping' ? mode : 'packing'; - const newGroupingMode = buy.groupingMode === groupingMode ? null : groupingMode; - const params = { - groupingMode: newGroupingMode, - }; - await axios.patch(`Buys/${buy.id}`, params); - buy.groupingMode = newGroupingMode; -}; + for (const change of changes) { + let patchData = {}; -const lockIconType = (groupingMode, mode) => { - if (mode === 'packing') { - return groupingMode === 'packing' ? 'lock' : 'lock_open'; - } else { - return groupingMode === 'grouping' ? 'lock' : 'lock_open'; + if ('hasMinPrice' in change.data) { + patchData.hasMinPrice = change.data?.hasMinPrice; + delete change.data.hasMinPrice; + } + if ('minPrice' in change.data) { + patchData.minPrice = change.data?.minPrice; + delete change.data.minPrice; + } + + if (Object.keys(patchData).length > 0) { + const promise = axios + .get('Buys/findOne', { + params: { + filter: { + fields: ['itemFk'], + where: { id: change.where.id }, + }, + }, + }) + .then((buy) => { + return axios.patch(`Items/${buy.data.itemFk}`, patchData); + }) + .catch((error) => { + console.error('Error processing change: ', change, error); + }); + + patchPromises.push(promise); + } + } + + await Promise.all(patchPromises); + + data.updates = changes.filter((change) => Object.keys(change.data).length > 0); + + return data; + } catch (error) { + console.error('Error in beforeSave:', error); + throw error; } -}; +} + +function invertQuantitySign(rows, sign) { + for (const row of rows) { + if (sign > 0) row.quantity = Math.abs(row.quantity); + else if (row.quantity > 0) row.quantity = -row.quantity; + } +} +function setIsChecked(rows, value) { + for (const row of rows) { + row.isChecked = value; + } + footerFetchDataRef.value.fetch(); +} + +async function setBuyUltimate(itemFk, data) { + if (!itemFk) return; + const buyUltimate = await axios.get(`Entries/getBuyUltimate`, { + params: { + itemFk, + warehouseFk: user.warehouseFk, + date: Date.vnNew(), + }, + }); + const buyUltimateData = buyUltimate.data[0]; + + const allowedKeys = columns + .filter((col) => col.create === true) + .map((col) => col.name); + + allowedKeys.forEach((key) => { + if (buyUltimateData.hasOwnProperty(key) && key !== 'entryFk') { + if (!['stickers', 'quantity'].includes(key)) data[key] = buyUltimateData[key]; + } + }); +} + +onMounted(() => { + stateStore.rightDrawer = false; + if ($props.editableMode) checkEntryLock(entityId.value, user.id); +}); </script> - <template> - <VnSubToolbar> - <template #st-actions> - <QBtnGroup push style="column-gap: 10px"> - <slot name="moreBeforeActions" /> - <QBtn - :label="t('globals.remove')" - color="primary" - icon="delete" - flat - @click="openRemoveDialog()" - :disable="!rowsSelected?.length" - :title="t('globals.remove')" - /> - </QBtnGroup> - </template> - </VnSubToolbar> - <VnPaginate - ref="entryBuysPaginateRef" - data-key="EntryBuys" - :url="`Entries/${route.params.id}/getBuys`" - @on-fetch="copyOriginalRowsData($event)" - auto-load - > - <template #body="{ rows }"> - <QTable - :rows="rows" - :columns="entriesTableColumns" - selection="multiple" - row-key="id" - class="full-width q-mt-md" - :grid="$q.screen.lt.md" - v-model:selected="rowsSelected" - :no-data-label="t('globals.noResults')" + <Teleport to="#st-data" v-if="stateStore?.isSubToolbarShown() && editableMode"> + <QBtnGroup push style="column-gap: 1px"> + <QBtnDropdown + label="+/-" + color="primary" + flat + :title="t('Invert quantity value')" + :disable="!selectedRows.length" + data-cy="change-quantity-sign" > - <template #body="props"> - <QTr> - <QTd> - <QCheckbox v-model="props.selected" /> - </QTd> - <QTd - v-for="col in props.cols" - :key="col.name" - style="max-width: 100px" - > - <component - :is="tableColumnComponents[col.name].component" - v-bind="tableColumnComponents[col.name].props" - v-model="props.row[col.field]" - v-on=" - tableColumnComponents[col.name].event( - col.field, - props - ) - " + <QList> + <QItem> + <QItemSection> + <QBtn + flat + @click="invertQuantitySign(selectedRows, -1)" + data-cy="set-negative-quantity" > - <template - v-if=" - col.name === 'grouping' || col.name === 'packing' - " - #append - > - <QBtn - :icon=" - lockIconType(props.row.groupingMode, col.name) - " - @click="toggleGroupingMode(props.row, col.name)" - class="cursor-pointer" - size="sm" - flat - dense - unelevated - push - :style="{ - 'font-variation-settings': `'FILL' ${ - lockIconType( - props.row.groupingMode, - col.name - ) === 'lock' - ? 1 - : 0 - }`, - }" - /> - </template> - <template - v-if="col.name === 'item' || col.name === 'import'" - > - {{ col.value }} - </template> - <ItemDescriptorProxy - v-if="col.name === 'item'" - :id="props.row.item.id" - /> - </component> - </QTd> - </QTr> - <QTr no-hover class="full-width infoRow" style="column-span: all"> - <QTd /> - <QTd cols> - <span>{{ props.row.item.itemType.code }}</span> - </QTd> - <QTd> - <span>{{ props.row.item.size }}</span> - </QTd> - <QTd> - <span>{{ toCurrency(props.row.item.minPrice) }}</span> - </QTd> - <QTd colspan="7"> - <span>{{ props.row.item.concept }}</span> - <span v-if="props.row.item.subName" class="subName"> - {{ props.row.item.subName }} - </span> - <FetchedTags :item="props.row.item" /> - </QTd> - </QTr> - </template> - <template #item="props"> - <div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition"> - <QCard bordered flat> - <QCardSection> - <QCheckbox v-model="props.selected" dense /> - </QCardSection> - <QSeparator /> - <QList dense> - <QItem v-for="col in props.cols" :key="col.name"> - <component - :is="tableColumnComponents[col.name].component" - v-bind="tableColumnComponents[col.name].props" - v-model="props.row[col.field]" - v-on=" - tableColumnComponents[col.name].event( - col.field, - props - ) - " - class="full-width" - > - <template - v-if=" - col.name === 'item' || - col.name === 'import' - " - > - {{ col.label + ': ' + col.value }} - </template> - </component> - </QItem> - </QList> - </QCard> - </div> - </template> - </QTable> + <span style="font-size: large">-</span> + </QBtn> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QBtn + flat + @click="invertQuantitySign(selectedRows, 1)" + data-cy="set-positive-quantity" + > + <span style="font-size: large">+</span> + </QBtn> + </QItemSection> + </QItem> + </QList> + </QBtnDropdown> + <QBtnDropdown + icon="price_check" + color="primary" + flat + :title="t('Check buy amount')" + :disable="!selectedRows.length" + data-cy="check-buy-amount" + > + <QList> + <QItem> + <QItemSection> + <QBtn + size="sm" + icon="check" + flat + @click="setIsChecked(selectedRows, true)" + data-cy="check-amount" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QBtn + size="sm" + icon="close" + flat + @click="setIsChecked(selectedRows, false)" + data-cy="uncheck-amount" + /> + </QItemSection> + </QItem> + </QList> + </QBtnDropdown> + </QBtnGroup> + </Teleport> + <FetchData + ref="footerFetchDataRef" + :url="`Entries/${entityId}/getBuyList`" + :params="{ groupBy: 'GROUP BY b.entryFk' }" + @on-fetch="(data) => (footer = data[0])" + auto-load + /> + <VnTable + ref="entryBuysRef" + data-key="EntryBuys" + :url="`Entries/${entityId}/getBuyList`" + save-url="Buys/crud" + :disable-option="{ card: true }" + v-model:selected="selectedRows" + @on-fetch="() => footerFetchDataRef.fetch()" + :table=" + editableMode + ? { + 'row-key': 'id', + selection: 'multiple', + } + : {} + " + :create=" + editableMode + ? { + urlCreate: 'Buys', + title: t('Create buy'), + onDataSaved: () => { + entryBuysRef.reload(); + }, + formInitialData: { entryFk: entityId, isIgnored: false }, + showSaveAndContinueBtn: true, + } + : null + " + :create-complement="{ + isFullWidth: true, + containerStyle: { + display: 'flex', + 'flex-wrap': 'wrap', + gap: '16px', + position: 'relative', + height: '450px', + }, + columnGridStyle: { + 'max-width': '50%', + flex: 1, + 'margin-right': '30px', + }, + }" + :is-editable="editableMode" + :without-header="!editableMode" + :with-filters="editableMode" + :right-search="true" + :right-search-icon="true" + :row-click="false" + :columns="columns" + :beforeSaveFn="beforeSave" + class="buyList" + :table-height="$props.tableHeight ?? '84vh'" + auto-load + footer + data-cy="entry-buys" + > + <template #column-hex="{ row }"> + <VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" /> </template> - </VnPaginate> - - <QPageSticky :offset="[20, 20]"> - <QBtn fab icon="upload" color="primary" @click="importBuys()" /> - <QTooltip class="text-no-wrap"> - {{ t('Import buys') }} - </QTooltip> - </QPageSticky> + <template #column-name="{ row }"> + <span class="link"> + {{ row?.name }} + <ItemDescriptorProxy :id="row?.itemFk" /> + </span> + </template> + <template #column-tags="{ row }"> + <FetchedTags :item="row" :columns="3" /> + </template> + <template #column-stickers="{ row }"> + <span :class="editableMode ? 'editable-text' : ''"> + <span style="color: var(--vn-label-color)"> + {{ row.printedStickers }} + </span> + <span>/{{ row.stickers }}</span> + </span> + </template> + <template #column-footer-stickers> + <div> + <span style="color: var(--vn-label-color)"> + {{ footer?.printedStickers }}</span + > + <span>/</span> + <span data-cy="footer-stickers">{{ footer?.stickers }}</span> + </div> + </template> + <template #column-footer-weight> + {{ footer?.weight }} + </template> + <template #column-footer-quantity> + <span :style="getQuantityStyle(footer)" data-cy="footer-quantity"> + {{ footer?.quantity }} + </span> + </template> + <template #column-footer-amount> + <span :style="getAmountStyle(footer)" data-cy="footer-amount"> + {{ footer?.amount }} + </span> + </template> + <template #column-create-itemFk="{ data }"> + <VnSelect + url="Items/search" + v-model="data.itemFk" + :label="t('Article')" + :fields="['id', 'name', 'size', 'producerName']" + :filter-options="['id', 'name', 'size', 'producerName']" + option-label="name" + option-value="id" + @update:modelValue=" + async (value) => { + await setBuyUltimate(value, data); + } + " + :required="true" + data-cy="itemFk-create-popup" + sort-by="nickname DESC" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt.name }} + </QItemLabel> + <QItemLabel caption> + #{{ scope.opt.id }}, {{ scope.opt?.size }}, + {{ scope.opt?.producerName }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </template> + <template #column-create-groupingMode="{ data }"> + <VnSelectEnum + :label="t('Grouping mode')" + v-model="data.groupingMode" + schema="vn" + table="buy" + column="groupingMode" + option-value="groupingMode" + option-label="groupingMode" + /> + </template> + <template #previous-create-dialog="{ data }"> + <div + style="position: absolute" + :class="{ 'centered-container': !data.itemFk }" + > + <ItemDescriptor :id="data.itemFk" v-if="data.itemFk" /> + <div v-else> + <span>{{ t('globals.noData') }}</span> + </div> + </div> + </template> + </VnTable> </template> - -<style lang="scss" scoped> -.q-table--horizontal-separator tbody tr:nth-child(odd) > td { - border-bottom-width: 0px; - border-top-width: 2px; - border-color: var(--vn-text-color); -} -.infoRow > td { - color: var(--vn-label-color); -} -</style> - <i18n> es: - Import buys: Importar compras - Buy deleted: Compra eliminada - Buys deleted: Compras eliminadas - Confirm deletion: Confirmar eliminación - Are you sure you want to delete this buy?: Seguro que quieres eliminar esta compra? - Are you sure you want to delete this buys?: Seguro que quieres eliminar estas compras? + Article: Artículo + Siz.: Med. + Size: Medida + Sti.: Eti. + Bucket: Cubo + Quantity: Cantidad + Amount: Importe + Pack.: Paq. + Package: Paquete + Box: Caja + P.Sen: P.Env + Packing sent: Packing envíos + Com.: Ref. + Comment: Referencia + Minimum price: Precio mínimo + Stickers: Etiquetas + Printed Stickers/Stickers: Etiquetas impresas/Etiquetas + Cost: Cost. + Buying value: Coste + Producer: Productor + Company: Compañia + Tags: Etiquetas + Grouping mode: Modo de agrupación + C.min: P.min + Ignore: Ignorar + Ignored for available: Ignorado para disponible + Grouping selector: Selector de grouping + Check min price: Marcar precio mínimo + Create buy: Crear compra + Invert quantity value: Invertir valor de cantidad + Check buy amount: Marcar como correcta la cantidad de compra </i18n> +<style lang="scss" scoped> +.centered-container { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + width: 40%; + height: 100%; +} +</style> diff --git a/src/pages/Entry/Card/EntryCard.vue b/src/pages/Entry/Card/EntryCard.vue index e00623a21..be82289f4 100644 --- a/src/pages/Entry/Card/EntryCard.vue +++ b/src/pages/Entry/Card/EntryCard.vue @@ -1,13 +1,13 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import EntryDescriptor from './EntryDescriptor.vue'; -import filter from './EntryFilter.js' +import filter from './EntryFilter.js'; </script> <template> <VnCardBeta data-key="Entry" - base-url="Entries" + url="Entries" :descriptor="EntryDescriptor" - :user-filter="filter" + :filter="filter" /> </template> diff --git a/src/pages/Entry/Card/EntryDescriptor.vue b/src/pages/Entry/Card/EntryDescriptor.vue index 19d13e51a..69b300cb2 100644 --- a/src/pages/Entry/Card/EntryDescriptor.vue +++ b/src/pages/Entry/Card/EntryDescriptor.vue @@ -1,12 +1,19 @@ <script setup> import { ref, computed, onMounted } from 'vue'; -import { useRoute } from 'vue-router'; +import { useRoute, useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import CardDescriptor from 'components/ui/CardDescriptor.vue'; -import VnLv from 'src/components/ui/VnLv.vue'; import { toDate } from 'src/filters'; import { getUrl } from 'src/composables/getUrl'; -import EntryDescriptorMenu from './EntryDescriptorMenu.vue'; +import { useQuasar } from 'quasar'; +import { usePrintService } from 'composables/usePrintService'; +import CardDescriptor from 'components/ui/CardDescriptor.vue'; +import VnLv from 'src/components/ui/VnLv.vue'; +import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; +import axios from 'axios'; + +const quasar = useQuasar(); +const { push } = useRouter(); +const { openReport } = usePrintService(); const $props = defineProps({ id: { @@ -83,12 +90,63 @@ const getEntryRedirectionFilter = (entry) => { to, }); }; + +function showEntryReport() { + openReport(`Entries/${entityId.value}/entry-order-pdf`); +} + +function showNotification(type, message) { + quasar.notify({ + type: type, + message: t(message), + }); +} + +async function recalculateRates(entity) { + try { + const entryConfig = await axios.get('EntryConfigs/findOne'); + if (entryConfig.data?.inventorySupplierFk === entity.supplierFk) { + showNotification( + 'negative', + 'Cannot recalculate prices because this is an inventory entry', + ); + return; + } + + await axios.post(`Entries/${entityId.value}/recalcEntryPrices`); + showNotification('positive', 'Entry prices recalculated'); + } catch (error) { + showNotification('negative', 'Failed to recalculate rates'); + console.error(error); + } +} + +async function cloneEntry() { + try { + const response = await axios.post(`Entries/${entityId.value}/cloneEntry`); + push({ path: `/entry/${response.data}` }); + showNotification('positive', 'Entry cloned'); + } catch (error) { + showNotification('negative', 'Failed to clone entry'); + console.error(error); + } +} + +async function deleteEntry() { + try { + await axios.post(`Entries/${entityId.value}/deleteEntry`); + push({ path: `/entry/list` }); + showNotification('positive', 'Entry deleted'); + } catch (error) { + showNotification('negative', 'Failed to delete entry'); + console.error(error); + } +} </script> <template> <CardDescriptor ref="entryDescriptorRef" - module="Entry" :url="`Entries/${entityId}`" :userFilter="entryFilter" title="supplier.nickname" @@ -96,15 +154,56 @@ const getEntryRedirectionFilter = (entry) => { width="lg-width" > <template #menu="{ entity }"> - <EntryDescriptorMenu :id="entity.id" /> + <QItem + v-ripple + clickable + @click="showEntryReport(entity)" + data-cy="show-entry-report" + > + <QItemSection>{{ t('Show entry report') }}</QItemSection> + </QItem> + <QItem + v-ripple + clickable + @click="recalculateRates(entity)" + data-cy="recalculate-rates" + > + <QItemSection>{{ t('Recalculate rates') }}</QItemSection> + </QItem> + <QItem v-ripple clickable @click="cloneEntry(entity)" data-cy="clone-entry"> + <QItemSection>{{ t('Clone') }}</QItemSection> + </QItem> + <QItem v-ripple clickable @click="deleteEntry(entity)" data-cy="delete-entry"> + <QItemSection>{{ t('Delete') }}</QItemSection> + </QItem> </template> <template #body="{ entity }"> - <VnLv :label="t('globals.agency')" :value="entity.travel?.agency?.name" /> - <VnLv :label="t('shipped')" :value="toDate(entity.travel?.shipped)" /> - <VnLv :label="t('landed')" :value="toDate(entity.travel?.landed)" /> + <VnLv :label="t('Travel')"> + <template #value> + <span class="link" v-if="entity?.travelFk"> + {{ entity.travel?.agency?.name }} + {{ entity.travel?.warehouseOut?.code }} → + {{ entity.travel?.warehouseIn?.code }} + <TravelDescriptorProxy :id="entity?.travelFk" /> + </span> + </template> + </VnLv> <VnLv - :label="t('globals.warehouseOut')" - :value="entity.travel?.warehouseOut?.name" + :label="t('entry.summary.travelShipped')" + :value="toDate(entity.travel?.shipped)" + /> + <VnLv + :label="t('entry.summary.travelLanded')" + :value="toDate(entity.travel?.landed)" + /> + <VnLv :label="t('entry.summary.currency')" :value="entity?.currency?.code" /> + <VnLv + :label="t('entry.summary.invoiceAmount')" + :value="entity?.invoiceAmount" + /> + <VnLv + :label="t('entry.summary.entryType')" + :value="entity?.entryType?.description" /> </template> <template #icons="{ entity }"> @@ -131,6 +230,14 @@ const getEntryRedirectionFilter = (entry) => { }}</QTooltip > </QIcon> + <QIcon + v-if="!entity?.travelFk" + name="vn:deletedTicket" + size="xs" + color="primary" + > + <QTooltip>{{ t('This entry is deleted') }}</QTooltip> + </QIcon> </QCardActions> </template> <template #actions="{ entity }"> @@ -143,21 +250,6 @@ const getEntryRedirectionFilter = (entry) => { > <QTooltip>{{ t('Supplier card') }}</QTooltip> </QBtn> - <QBtn - :to="{ - name: 'TravelMain', - query: { - params: JSON.stringify({ - agencyModeFk: entity.travel?.agencyModeFk, - }), - }, - }" - size="md" - icon="local_airport" - color="primary" - > - <QTooltip>{{ t('All travels with current agency') }}</QTooltip> - </QBtn> <QBtn :to="{ name: 'EntryMain', @@ -177,10 +269,24 @@ const getEntryRedirectionFilter = (entry) => { </template> <i18n> es: + Travel: Envío Supplier card: Ficha del proveedor All travels with current agency: Todos los envíos con la agencia actual All entries with current supplier: Todas las entradas con el proveedor actual Show entry report: Ver informe del pedido Inventory entry: Es inventario Virtual entry: Es una redada + shipped: Enviado + landed: Recibido + This entry is deleted: Esta entrada está eliminada + Cannot recalculate prices because this is an inventory entry: No se pueden recalcular los precios porque es una entrada de inventario + Entry deleted: Entrada eliminada + Entry cloned: Entrada clonada + Entry prices recalculated: Precios de la entrada recalculados + Failed to recalculate rates: No se pudieron recalcular las tarifas + Failed to clone entry: No se pudo clonar la entrada + Failed to delete entry: No se pudo eliminar la entrada + Recalculate rates: Recalcular tarifas + Clone: Clonar + Delete: Eliminar </i18n> diff --git a/src/pages/Entry/Card/EntryFilter.js b/src/pages/Entry/Card/EntryFilter.js index 3ff62cf27..d9fd1c2be 100644 --- a/src/pages/Entry/Card/EntryFilter.js +++ b/src/pages/Entry/Card/EntryFilter.js @@ -9,6 +9,7 @@ export default { 'shipped', 'agencyModeFk', 'warehouseOutFk', + 'warehouseInFk', 'daysInForward', ], include: [ @@ -21,13 +22,13 @@ export default { { relation: 'warehouseOut', scope: { - fields: ['name'], + fields: ['name', 'code'], }, }, { relation: 'warehouseIn', scope: { - fields: ['name'], + fields: ['name', 'code'], }, }, ], @@ -39,5 +40,17 @@ export default { fields: ['id', 'nickname'], }, }, + { + relation: 'currency', + scope: { + fields: ['id', 'code'], + }, + }, + { + relation: 'entryType', + scope: { + fields: ['code', 'description'], + }, + }, ], }; diff --git a/src/pages/Entry/Card/EntryNotes.vue b/src/pages/Entry/Card/EntryNotes.vue index 55cac0437..459c3b069 100644 --- a/src/pages/Entry/Card/EntryNotes.vue +++ b/src/pages/Entry/Card/EntryNotes.vue @@ -17,7 +17,7 @@ const selected = ref([]); const sortEntryObservationOptions = (data) => { entryObservationsOptions.value = [...data].sort((a, b) => - a.description.localeCompare(b.description) + a.description.localeCompare(b.description), ); }; @@ -142,7 +142,7 @@ const columns = computed(() => [ fab color="primary" icon="add" - shortcut="+" + v-shortcut="'+'" @click="entryObservationsRef.insert()" /> </QPageSticky> diff --git a/src/pages/Entry/Card/EntrySummary.vue b/src/pages/Entry/Card/EntrySummary.vue index 8c46fb6e6..c40e2ba46 100644 --- a/src/pages/Entry/Card/EntrySummary.vue +++ b/src/pages/Entry/Card/EntrySummary.vue @@ -2,19 +2,17 @@ import { onMounted, ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; +import { toDate } from 'src/filters'; +import { getUrl } from 'src/composables/getUrl'; +import axios from 'axios'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; - -import { toDate, toCurrency, toCelsius } from 'src/filters'; -import { getUrl } from 'src/composables/getUrl'; -import axios from 'axios'; -import FetchedTags from 'src/components/ui/FetchedTags.vue'; -import VnToSummary from 'src/components/ui/VnToSummary.vue'; -import EntryDescriptorMenu from './EntryDescriptorMenu.vue'; -import VnRow from 'src/components/ui/VnRow.vue'; +import EntryBuys from './EntryBuys.vue'; import VnTitle from 'src/components/common/VnTitle.vue'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; +import VnToSummary from 'src/components/ui/VnToSummary.vue'; const route = useRoute(); const { t } = useI18n(); @@ -33,117 +31,6 @@ const entry = ref(); const entryBuys = ref([]); const entryUrl = ref(); -onMounted(async () => { - entryUrl.value = (await getUrl('entry/')) + entityId.value; -}); - -const tableColumnComponents = { - quantity: { - component: () => 'span', - props: () => {}, - }, - stickers: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - packagingFk: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - weight: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - packing: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - grouping: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - buyingValue: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - amount: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - pvp: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, -}; - -const entriesTableColumns = computed(() => { - return [ - { - label: t('globals.quantity'), - field: 'quantity', - name: 'quantity', - align: 'left', - }, - { - label: t('entry.summary.stickers'), - field: 'stickers', - name: 'stickers', - align: 'left', - }, - { - label: t('entry.summary.package'), - field: 'packagingFk', - name: 'packagingFk', - align: 'left', - }, - { - label: t('globals.weight'), - field: 'weight', - name: 'weight', - align: 'left', - }, - { - label: t('entry.summary.packing'), - field: 'packing', - name: 'packing', - align: 'left', - }, - { - label: t('entry.summary.grouping'), - field: 'grouping', - name: 'grouping', - align: 'left', - }, - { - label: t('entry.summary.buyingValue'), - field: 'buyingValue', - name: 'buyingValue', - align: 'left', - format: (value) => toCurrency(value), - }, - { - label: t('entry.summary.import'), - name: 'amount', - align: 'left', - format: (_, row) => toCurrency(row.buyingValue * row.quantity), - }, - { - label: t('entry.summary.pvp'), - name: 'pvp', - align: 'left', - format: (_, row) => toCurrency(row.price2) + ' / ' + toCurrency(row.price3), - }, - ]; -}); - async function setEntryData(data) { if (data) entry.value = data; await fetchEntryBuys(); @@ -153,14 +40,18 @@ const fetchEntryBuys = async () => { const { data } = await axios.get(`Entries/${entry.value.id}/getBuys`); if (data) entryBuys.value = data; }; -</script> +onMounted(async () => { + entryUrl.value = (await getUrl('entry/')) + entityId.value; +}); +</script> <template> <CardSummary ref="summaryRef" :url="`Entries/${entityId}/getEntry`" @on-fetch="(data) => setEntryData(data)" data-key="EntrySummary" + data-cy="entry-summary" > <template #header-left> <VnToSummary @@ -173,159 +64,154 @@ const fetchEntryBuys = async () => { <template #header> <span>{{ entry.id }} - {{ entry.supplier.nickname }}</span> </template> - <template #menu="{ entity }"> - <EntryDescriptorMenu :id="entity.id" /> - </template> <template #body> <QCard class="vn-one"> <VnTitle :url="`#/entry/${entityId}/basic-data`" :text="t('globals.summary.basicData')" /> - <VnLv :label="t('entry.summary.commission')" :value="entry.commission" /> - <VnLv - :label="t('entry.summary.currency')" - :value="entry.currency?.name" - /> - <VnLv :label="t('globals.company')" :value="entry.company.code" /> - <VnLv :label="t('globals.reference')" :value="entry.reference" /> - <VnLv - :label="t('entry.summary.invoiceNumber')" - :value="entry.invoiceNumber" - /> - <VnLv - :label="t('entry.basicData.initialTemperature')" - :value="toCelsius(entry.initialTemperature)" - /> - <VnLv - :label="t('entry.basicData.finalTemperature')" - :value="toCelsius(entry.finalTemperature)" - /> + <div class="card-group"> + <div class="card-content"> + <VnLv + :label="t('entry.summary.commission')" + :value="entry?.commission" + /> + <VnLv + :label="t('entry.summary.currency')" + :value="entry?.currency?.name" + /> + <VnLv + :label="t('globals.company')" + :value="entry?.company?.code" + /> + <VnLv :label="t('globals.reference')" :value="entry?.reference" /> + <VnLv + :label="t('entry.summary.invoiceNumber')" + :value="entry?.invoiceNumber" + /> + </div> + <div class="card-content"> + <VnCheckbox + :label="t('entry.summary.ordered')" + v-model="entry.isOrdered" + :disable="true" + size="xs" + /> + <VnCheckbox + :label="t('globals.confirmed')" + v-model="entry.isConfirmed" + :disable="true" + size="xs" + /> + <VnCheckbox + :label="t('entry.summary.booked')" + v-model="entry.isBooked" + :disable="true" + size="xs" + /> + <VnCheckbox + :label="t('entry.summary.excludedFromAvailable')" + v-model="entry.isExcludedFromAvailable" + :disable="true" + size="xs" + /> + </div> + </div> </QCard> - <QCard class="vn-one"> + <QCard class="vn-one" v-if="entry?.travelFk"> <VnTitle - :url="`#/entry/${entityId}/basic-data`" - :text="t('globals.summary.basicData')" + :url="`#/travel/${entry.travel.id}/summary`" + :text="t('Travel')" /> - <VnLv :label="t('entry.summary.travelReference')"> - <template #value> - <span class="link"> - {{ entry.travel.ref }} - <TravelDescriptorProxy :id="entry.travel.id" /> - </span> - </template> - </VnLv> - <VnLv - :label="t('entry.summary.travelAgency')" - :value="entry.travel.agency?.name" - /> - <VnLv - :label="t('globals.shipped')" - :value="toDate(entry.travel.shipped)" - /> - <VnLv - :label="t('globals.warehouseOut')" - :value="entry.travel.warehouseOut?.name" - /> - <VnLv - :label="t('entry.summary.travelDelivered')" - :value="entry.travel.isDelivered" - /> - <VnLv :label="t('globals.landed')" :value="toDate(entry.travel.landed)" /> - <VnLv - :label="t('globals.warehouseIn')" - :value="entry.travel.warehouseIn?.name" - /> - <VnLv - :label="t('entry.summary.travelReceived')" - :value="entry.travel.isReceived" - /> - </QCard> - <QCard class="vn-one"> - <VnTitle :url="`#/travel/${entityId}/summary`" :text="t('Travel data')" /> - <VnRow class="block"> - <VnLv :label="t('entry.summary.ordered')" :value="entry.isOrdered" /> - <VnLv :label="t('globals.confirmed')" :value="entry.isConfirmed" /> - <VnLv :label="t('entry.summary.booked')" :value="entry.isBooked" /> - <VnLv - :label="t('entry.summary.excludedFromAvailable')" - :value="entry.isExcludedFromAvailable" - /> - </VnRow> + <div class="card-group"> + <div class="card-content"> + <VnLv :label="t('entry.summary.travelReference')"> + <template #value> + <span class="link"> + {{ entry.travel.ref }} + <TravelDescriptorProxy :id="entry.travel.id" /> + </span> + </template> + </VnLv> + <VnLv + :label="t('entry.summary.travelAgency')" + :value="entry.travel.agency?.name" + /> + <VnLv + :label="t('entry.summary.travelShipped')" + :value="toDate(entry.travel.shipped)" + /> + <VnLv + :label="t('globals.warehouseOut')" + :value="entry.travel.warehouseOut?.name" + /> + <VnLv + :label="t('entry.summary.travelLanded')" + :value="toDate(entry.travel.landed)" + /> + <VnLv + :label="t('globals.warehouseIn')" + :value="entry.travel.warehouseIn?.name" + /> + </div> + <div class="card-content"> + <VnCheckbox + :label="t('entry.summary.travelDelivered')" + v-model="entry.travel.isDelivered" + :disable="true" + size="xs" + /> + <VnCheckbox + :label="t('entry.summary.travelReceived')" + v-model="entry.travel.isReceived" + :disable="true" + size="xs" + /> + </div> + </div> </QCard> <QCard class="vn-max"> <VnTitle :url="`#/entry/${entityId}/buys`" :text="t('entry.summary.buys')" /> - <QTable - :rows="entryBuys" - :columns="entriesTableColumns" - row-key="index" - class="full-width q-mt-md" - :no-data-label="t('globals.noResults')" - > - <template #body="{ cols, row, rowIndex }"> - <QTr no-hover> - <QTd v-for="col in cols" :key="col?.name"> - <component - :is="tableColumnComponents[col?.name].component()" - v-bind="tableColumnComponents[col?.name].props()" - @click="tableColumnComponents[col?.name].event()" - class="col-content" - > - <template - v-if=" - col?.name !== 'observation' && - col?.name !== 'isConfirmed' - " - >{{ col.value }}</template - > - <QTooltip v-if="col.toolTip">{{ - col.toolTip - }}</QTooltip> - </component> - </QTd> - </QTr> - <QTr no-hover> - <QTd> - <span>{{ row.item.itemType.code }}</span> - </QTd> - <QTd> - <span>{{ row.item.id }}</span> - </QTd> - <QTd> - <span>{{ row.item.size }}</span> - </QTd> - <QTd> - <span>{{ toCurrency(row.item.minPrice) }}</span> - </QTd> - <QTd colspan="6"> - <span>{{ row.item.concept }}</span> - <span v-if="row.item.subName" class="subName"> - {{ row.item.subName }} - </span> - <FetchedTags :item="row.item" /> - </QTd> - </QTr> - <!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys --> - <QTr v-if="rowIndex !== entryBuys.length - 1"> - <QTd colspan="10" class="vn-table-separation-row" /> - </QTr> - </template> - </QTable> + <EntryBuys + v-if="entityId" + :id="Number(entityId)" + :editable-mode="false" + table-height="49vh" + /> </QCard> </template> </CardSummary> </template> - <style lang="scss" scoped> -.separation-row { - background-color: var(--vn-section-color) !important; +.card-group { + display: flex; + flex-direction: column; +} + +.card-content { + display: flex; + flex-direction: column; + text-overflow: ellipsis; + > div { + max-height: 24px; + } +} + +@media (min-width: 1010px) { + .card-group { + flex-direction: row; + } + .card-content { + flex: 1; + margin-right: 16px; + } } </style> - <i18n> es: - Travel data: Datos envío + Travel: Envío + InvoiceIn data: Datos factura </i18n> diff --git a/src/pages/Entry/EntryFilter.vue b/src/pages/Entry/EntryFilter.vue index 0f632c0ef..8c60918a8 100644 --- a/src/pages/Entry/EntryFilter.vue +++ b/src/pages/Entry/EntryFilter.vue @@ -19,6 +19,7 @@ const props = defineProps({ const currenciesOptions = ref([]); const companiesOptions = ref([]); +const entryFilterPanel = ref(); </script> <template> @@ -38,7 +39,7 @@ const companiesOptions = ref([]); @on-fetch="(data) => (currenciesOptions = data)" auto-load /> - <VnFilterPanel :data-key="props.dataKey" :search-button="true"> + <VnFilterPanel ref="entryFilterPanel" :data-key="props.dataKey" :search-button="true"> <template #tags="{ tag, formatFn }"> <div class="q-gutter-x-xs"> <strong>{{ t(`entryFilter.params.${tag.label}`) }}: </strong> @@ -48,70 +49,65 @@ const companiesOptions = ref([]); <template #body="{ params, searchFn }"> <QItem> <QItemSection> - <VnInput - v-model="params.search" - :label="t('entryFilter.params.search')" - is-outlined - /> + <QCheckbox + :label="t('params.isExcludedFromAvailable')" + v-model="params.isExcludedFromAvailable" + toggle-indeterminate + > + <QTooltip> + {{ t('params.isExcludedFromAvailable') }} + </QTooltip> + </QCheckbox> + </QItemSection> + <QItemSection> + <QCheckbox + :label="t('params.isOrdered')" + v-model="params.isOrdered" + toggle-indeterminate + > + <QTooltip> + {{ t('entry.list.tableVisibleColumns.isOrdered') }} + </QTooltip> + </QCheckbox> </QItemSection> </QItem> <QItem> <QItemSection> - <VnInput - v-model="params.reference" - :label="t('entryFilter.params.reference')" - is-outlined - /> + <QCheckbox + :label="t('params.isReceived')" + v-model="params.isReceived" + toggle-indeterminate + > + <QTooltip> + {{ t('entry.list.tableVisibleColumns.isReceived') }} + </QTooltip> + </QCheckbox> + </QItemSection> + <QItemSection> + <QCheckbox + :label="t('entry.list.tableVisibleColumns.isConfirmed')" + v-model="params.isConfirmed" + toggle-indeterminate + > + <QTooltip> + {{ t('entry.list.tableVisibleColumns.isConfirmed') }} + </QTooltip> + </QCheckbox> </QItemSection> </QItem> <QItem> <QItemSection> - <VnInput - v-model="params.invoiceNumber" - :label="t('entryFilter.params.invoiceNumber')" - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput - v-model="params.travelFk" - :label="t('entryFilter.params.travelFk')" - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - :label="t('entryFilter.params.companyFk')" - v-model="params.companyFk" + <VnInputDate + :label="t('params.landed')" + v-model="params.landed" @update:model-value="searchFn()" - :options="companiesOptions" - option-value="id" - option-label="code" - hide-selected - dense - outlined - rounded + is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <VnSelect - :label="t('entryFilter.params.currencyFk')" - v-model="params.currencyFk" - @update:model-value="searchFn()" - :options="currenciesOptions" - option-value="id" - option-label="name" - hide-selected - dense - outlined - rounded - /> + <VnInput v-model="params.id" label="Id" is-outlined /> </QItemSection> </QItem> <QItem> @@ -125,62 +121,165 @@ const companiesOptions = ref([]); rounded /> </QItemSection> - </QItem> - <QItem> <QItemSection> - <VnInputDate - :label="t('entryFilter.params.created')" - v-model="params.created" - @update:model-value="searchFn()" + <VnInput + v-model="params.invoiceNumber" + :label="t('params.invoiceNumber')" is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <VnInputDate - :label="t('entryFilter.params.from')" - v-model="params.from" - @update:model-value="searchFn()" + <VnInput + v-model="params.reference" + :label="t('entry.list.tableVisibleColumns.reference')" is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <VnInputDate - :label="t('entryFilter.params.to')" - v-model="params.to" + <VnSelect + :label="t('params.agencyModeId')" + v-model="params.agencyModeId" @update:model-value="searchFn()" + url="AgencyModes" + :fields="['id', 'name']" + hide-selected + dense + outlined + rounded + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.evaNotes" + :label="t('params.evaNotes')" is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <QCheckbox - :label="t('entryFilter.params.isBooked')" - v-model="params.isBooked" - toggle-indeterminate - /> - </QItemSection> - <QItemSection> - <QCheckbox - :label="t('entryFilter.params.isConfirmed')" - v-model="params.isConfirmed" - toggle-indeterminate + <VnSelect + :label="t('params.warehouseOutFk')" + v-model="params.warehouseOutFk" + @update:model-value="searchFn()" + url="Warehouses" + :fields="['id', 'name']" + hide-selected + dense + outlined + rounded /> </QItemSection> </QItem> <QItem> <QItemSection> - <QCheckbox - :label="t('entryFilter.params.isOrdered')" - v-model="params.isOrdered" - toggle-indeterminate + <VnSelect + :label="t('params.warehouseInFk')" + v-model="params.warehouseInFk" + @update:model-value="searchFn()" + url="Warehouses" + :fields="['id', 'name']" + hide-selected + dense + outlined + rounded + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.name }} + </QItemLabel> + <QItemLabel caption> + {{ `#${scope.opt?.id} , ${scope.opt?.nickname}` }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.invoiceNumber" + :label="t('params.invoiceNumber')" + is-outlined + /> + </QItemSection> + </QItem> + + <QItem> + <QItemSection> + <VnSelect + :label="t('params.entryTypeCode')" + v-model="params.entryTypeCode" + @update:model-value="searchFn()" + url="EntryTypes" + :fields="['code', 'description']" + option-value="code" + option-label="description" + hide-selected + dense + outlined + rounded + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.evaNotes" + :label="t('params.evaNotes')" + is-outlined /> </QItemSection> </QItem> </template> </VnFilterPanel> </template> + +<i18n> +en: + params: + isExcludedFromAvailable: Inventory + isOrdered: Ordered + isReceived: Received + isConfirmed: Confirmed + isRaid: Raid + landed: Date + id: Id + supplierFk: Supplier + invoiceNumber: Invoice number + reference: Ref/Alb/Guide + agencyModeId: Agency mode + evaNotes: Notes + warehouseOutFk: Origin + warehouseInFk: Destiny + entryTypeCode: Entry type + hasToShowDeletedEntries: Show deleted entries +es: + params: + isExcludedFromAvailable: Inventario + isOrdered: Pedida + isConfirmed: Confirmado + isReceived: Recibida + isRaid: Raid + landed: Fecha + id: Id + supplierFk: Proveedor + invoiceNumber: Núm. factura + reference: Ref/Alb/Guía + agencyModeId: Modo agencia + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeCode: Tipo de entrada + hasToShowDeletedEntries: Mostrar entradas eliminadas +</i18n> diff --git a/src/pages/Entry/EntryList.vue b/src/pages/Entry/EntryList.vue index 3172c6d0e..3c96a2302 100644 --- a/src/pages/Entry/EntryList.vue +++ b/src/pages/Entry/EntryList.vue @@ -1,21 +1,25 @@ <script setup> +import axios from 'axios'; +import VnSection from 'src/components/common/VnSection.vue'; import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useState } from 'src/composables/useState'; +import { onBeforeMount } from 'vue'; + import EntryFilter from './EntryFilter.vue'; import VnTable from 'components/VnTable/VnTable.vue'; -import { toCelsius, toDate } from 'src/filters'; -import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import EntrySummary from './Card/EntrySummary.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; -import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; -import VnSection from 'src/components/common/VnSection.vue'; +import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue'; +import { toDate } from 'src/filters'; const { t } = useI18n(); const tableRef = ref(); +const defaultEntry = ref({}); +const state = useState(); +const user = state.getUser(); const dataKey = 'EntryList'; -const { viewSummary } = useSummaryDialog(); -const entryFilter = { +const entryQueryFilter = { include: [ { relation: 'suppliers', @@ -40,44 +44,58 @@ const entryFilter = { const columns = computed(() => [ { - name: 'status', - columnFilter: false, + labelAbbreviation: 'Ex', + label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), + toolTip: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), + name: 'isExcludedFromAvailable', + component: 'checkbox', + width: '35px', }, { - align: 'left', - label: t('globals.id'), - name: 'id', - isId: true, - chip: { - condition: () => true, - }, + labelAbbreviation: 'Pe', + label: t('entry.list.tableVisibleColumns.isOrdered'), + toolTip: t('entry.list.tableVisibleColumns.isOrdered'), + name: 'isOrdered', + component: 'checkbox', + width: '35px', }, { - align: 'left', - label: t('globals.reference'), - name: 'reference', - isTitle: true, - component: 'input', - columnField: { - component: null, - }, - create: true, - cardVisible: true, + labelAbbreviation: 'LE', + label: t('entry.list.tableVisibleColumns.isConfirmed'), + toolTip: t('entry.list.tableVisibleColumns.isConfirmed'), + name: 'isConfirmed', + component: 'checkbox', + width: '35px', }, { - align: 'left', - label: t('entry.list.tableVisibleColumns.created'), - name: 'created', - create: true, - cardVisible: true, + labelAbbreviation: 'Re', + label: t('entry.list.tableVisibleColumns.isReceived'), + toolTip: t('entry.list.tableVisibleColumns.isReceived'), + name: 'isReceived', + component: 'checkbox', + width: '35px', + }, + { + label: t('entry.list.tableVisibleColumns.landed'), + name: 'landed', component: 'date', columnField: { component: null, }, - format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.created)), + format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.landed)), + width: '105px', + }, + { + label: t('globals.id'), + name: 'id', + isId: true, + component: 'number', + chip: { + condition: () => true, + }, + width: '50px', }, { - align: 'left', label: t('entry.list.tableVisibleColumns.supplierFk'), name: 'supplierFk', create: true, @@ -86,165 +104,213 @@ const columns = computed(() => [ attrs: { url: 'suppliers', fields: ['id', 'name'], - }, - columnField: { - component: null, + where: { order: 'name DESC' }, }, format: (row, dashIfEmpty) => dashIfEmpty(row.supplierName), + width: '110px', }, { align: 'left', - label: t('entry.list.tableVisibleColumns.isBooked'), - name: 'isBooked', + label: t('entry.list.tableVisibleColumns.invoiceNumber'), + name: 'invoiceNumber', + component: 'input', + }, + { + align: 'left', + label: t('entry.list.tableVisibleColumns.reference'), + name: 'reference', + isTitle: true, + component: 'input', + columnField: { + component: null, + }, cardVisible: true, - create: true, - component: 'checkbox', }, { align: 'left', - label: t('entry.list.tableVisibleColumns.isConfirmed'), - name: 'isConfirmed', + label: 'AWB', + name: 'awbCode', + component: 'input', + width: '100px', + }, + { + align: 'left', + label: t('entry.list.tableVisibleColumns.agencyModeId'), + name: 'agencyModeId', cardVisible: true, - create: true, - component: 'checkbox', + component: 'select', + attrs: { + url: 'agencyModes', + fields: ['id', 'name'], + }, + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row.agencyModeName), }, { align: 'left', - label: t('entry.list.tableVisibleColumns.isOrdered'), - name: 'isOrdered', + label: t('entry.list.tableVisibleColumns.evaNotes'), + name: 'evaNotes', + component: 'input', + }, + { + align: 'left', + label: t('entry.list.tableVisibleColumns.warehouseOutFk'), + name: 'warehouseOutFk', cardVisible: true, - create: true, - component: 'checkbox', + component: 'select', + attrs: { + url: 'warehouses', + fields: ['id', 'name'], + }, + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row.warehouseOutName), + width: '65px', }, { align: 'left', - label: t('entry.list.tableVisibleColumns.companyFk'), + label: t('entry.list.tableVisibleColumns.warehouseInFk'), + name: 'warehouseInFk', + cardVisible: true, + component: 'select', + attrs: { + url: 'warehouses', + fields: ['id', 'name'], + }, + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row.warehouseInName), + width: '65px', + }, + { + align: 'left', + labelAbbreviation: t('Type'), + label: t('entry.list.tableVisibleColumns.entryTypeDescription'), + toolTip: t('entry.list.tableVisibleColumns.entryTypeDescription'), + name: 'entryTypeCode', + component: 'select', + attrs: { + url: 'entryTypes', + fields: ['code', 'description'], + optionValue: 'code', + optionLabel: 'description', + }, + width: '65px', + format: (row, dashIfEmpty) => dashIfEmpty(row.entryTypeDescription), + }, + { name: 'companyFk', + label: t('entry.list.tableVisibleColumns.companyFk'), + cardVisible: false, + visible: false, + create: true, component: 'select', attrs: { - url: 'companies', - fields: ['id', 'code'], + optionValue: 'id', optionLabel: 'code', - optionValue: 'id', + url: 'Companies', }, - columnField: { - component: null, - }, - create: true, - - format: (row, dashIfEmpty) => dashIfEmpty(row.companyCode), }, { - align: 'left', - label: t('entry.list.tableVisibleColumns.travelFk'), name: 'travelFk', - component: 'select', - attrs: { - url: 'travels', - fields: ['id', 'ref'], - optionLabel: 'ref', - optionValue: 'id', - }, - columnField: { - component: null, - }, + label: t('entry.list.tableVisibleColumns.travelFk'), + cardVisible: false, + visible: false, create: true, - format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), - }, - { - align: 'left', - label: t('entry.list.tableVisibleColumns.invoiceAmount'), - name: 'invoiceAmount', - cardVisible: true, - }, - { - align: 'left', - name: 'initialTemperature', - label: t('entry.basicData.initialTemperature'), - field: 'initialTemperature', - format: (row) => toCelsius(row.initialTemperature), - }, - { - align: 'left', - name: 'finalTemperature', - label: t('entry.basicData.finalTemperature'), - field: 'finalTemperature', - format: (row) => toCelsius(row.finalTemperature), - }, - { - label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), - name: 'isExcludedFromAvailable', - columnFilter: { - inWhere: true, - }, - }, - { - align: 'right', - name: 'tableActions', - actions: [ - { - title: t('components.smartCard.viewSummary'), - icon: 'preview', - action: (row) => viewSummary(row.id, EntrySummary), - isPrimary: true, - }, - ], }, ]); +function getBadgeAttrs(row) { + const date = row.landed; + let today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + let timeTicket = new Date(date); + timeTicket.setHours(0, 0, 0, 0); + + let timeDiff = today - timeTicket; + + if (timeDiff > 0) return { color: 'info', 'text-color': 'black' }; + if (timeDiff < 0) return { color: 'warning', 'text-color': 'black' }; + switch (row.entryTypeCode) { + case 'regularization': + case 'life': + case 'internal': + case 'inventory': + if (!row.isOrdered || !row.isConfirmed) + return { color: 'negative', 'text-color': 'black' }; + break; + case 'product': + case 'packaging': + case 'devaluation': + case 'payment': + case 'transport': + if ( + row.invoiceAmount === null || + (row.invoiceNumber === null && row.reference === null) || + !row.isOrdered || + !row.isConfirmed + ) + return { color: 'negative', 'text-color': 'black' }; + break; + default: + break; + } + return { color: 'transparent' }; +} + +onBeforeMount(async () => { + defaultEntry.value = (await axios.get('EntryConfigs/findOne')).data; +}); </script> <template> <VnSection :data-key="dataKey" - :columns="columns" prefix="entry" url="Entries/filter" :array-data-props="{ url: 'Entries/filter', - order: 'id DESC', - userFilter: entryFilter, + order: 'landed DESC', + userFilter: EntryFilter, }" > <template #advanced-menu> - <EntryFilter data-key="EntryList" /> + <EntryFilter :data-key="dataKey" /> </template> <template #body> <VnTable + v-if="defaultEntry.defaultSupplierFk" ref="tableRef" :data-key="dataKey" + url="Entries/filter" + :filter="entryQueryFilter" + order="landed DESC" :create="{ urlCreate: 'Entries', - title: t('entry.list.newEntry'), + title: t('Create entry'), onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: {}, + formInitialData: { + supplierFk: defaultEntry.defaultSupplierFk, + dated: Date.vnNew(), + companyFk: user?.companyFk, + }, }" :columns="columns" redirect="entry" :right-search="false" > - <template #column-status="{ row }"> - <div class="row q-gutter-xs"> - <QIcon - v-if="!!row.isExcludedFromAvailable" - name="vn:inventory" - color="primary" - > - <QTooltip>{{ - t( - 'entry.list.tableVisibleColumns.isExcludedFromAvailable', - ) - }}</QTooltip> - </QIcon> - <QIcon v-if="!!row.isRaid" name="vn:net" color="primary"> - <QTooltip> - {{ - t('globals.raid', { - daysInForward: row.daysInForward, - }) - }}</QTooltip - > - </QIcon> - </div> + <template #column-landed="{ row }"> + <QBadge + v-if="row?.travelFk" + v-bind="getBadgeAttrs(row)" + class="q-pa-sm" + style="font-size: 14px" + > + {{ toDate(row.landed) }} + </QBadge> </template> <template #column-supplierFk="{ row }"> <span class="link" @click.stop> @@ -252,13 +318,27 @@ const columns = computed(() => [ <SupplierDescriptorProxy :id="row.supplierFk" /> </span> </template> - <template #column-travelFk="{ row }"> - <span class="link" @click.stop> - {{ row.travelRef }} - <TravelDescriptorProxy :id="row.travelFk" /> - </span> + <template #column-create-travelFk="{ data }"> + <VnSelectTravelExtended + :data="data" + v-model="data.travelFk" + :onFilterTravelSelected=" + (data, result) => (data.travelFk = result) + " + data-cy="entry-travel-select" + /> </template> </VnTable> </template> </VnSection> </template> + +<i18n> +es: + Inventory entry: Es inventario + Virtual entry: Es una redada + Search entries: Buscar entradas + You can search by entry reference: Puedes buscar por referencia de la entrada + Create entry: Crear entrada + Type: Tipo +</i18n> diff --git a/src/pages/Entry/EntryStockBought.vue b/src/pages/Entry/EntryStockBought.vue index fa0bdc12e..4bd0fe640 100644 --- a/src/pages/Entry/EntryStockBought.vue +++ b/src/pages/Entry/EntryStockBought.vue @@ -34,18 +34,20 @@ const columns = computed(() => [ label: t('entryStockBought.buyer'), isTitle: true, component: 'select', + isEditable: false, cardVisible: true, create: true, attrs: { url: 'Workers/activeWithInheritedRole', - fields: ['id', 'name'], + fields: ['id', 'name', 'nickname'], where: { role: 'buyer' }, optionFilter: 'firstName', - optionLabel: 'name', + optionLabel: 'nickname', optionValue: 'id', useLike: false, }, columnFilter: false, + width: '70px', }, { align: 'center', @@ -55,6 +57,7 @@ const columns = computed(() => [ create: true, component: 'number', summation: true, + width: '50px', }, { align: 'center', @@ -78,6 +81,7 @@ const columns = computed(() => [ actions: [ { title: t('entryStockBought.viewMoreDetails'), + name: 'searchBtn', icon: 'search', isPrimary: true, action: (row) => { @@ -91,6 +95,7 @@ const columns = computed(() => [ }, }, ], + 'data-cy': 'table-actions', }, ]); @@ -158,7 +163,7 @@ function round(value) { @on-fetch=" (data) => { travel = data.find( - (data) => data.warehouseIn?.code.toLowerCase() === 'vnh' + (data) => data.warehouseIn?.code.toLowerCase() === 'vnh', ); } " @@ -179,6 +184,7 @@ function round(value) { @click="openDialog()" :title="t('entryStockBought.editTravel')" color="primary" + data-cy="edit-travel" /> </div> </VnRow> @@ -239,10 +245,11 @@ function round(value) { table-height="80vh" auto-load :column-search="false" + :without-header="true" > <template #column-workerFk="{ row }"> <span class="link" @click.stop> - {{ row?.worker?.user?.name }} + {{ row?.worker?.user?.nickname }} <WorkerDescriptorProxy :id="row?.workerFk" /> </span> </template> @@ -279,10 +286,11 @@ function round(value) { justify-content: center; } .column { + min-width: 40%; + margin-top: 5%; display: flex; flex-direction: column; align-items: center; - min-width: 35%; } .text-negative { color: $negative !important; diff --git a/src/pages/Entry/EntryStockBoughtDetail.vue b/src/pages/Entry/EntryStockBoughtDetail.vue index 812171825..1a37994d9 100644 --- a/src/pages/Entry/EntryStockBoughtDetail.vue +++ b/src/pages/Entry/EntryStockBoughtDetail.vue @@ -21,7 +21,7 @@ const $props = defineProps({ const customUrl = `StockBoughts/getStockBoughtDetail?workerFk=${$props.workerFk}&dated=${$props.dated}`; const columns = [ { - align: 'left', + align: 'right', label: t('Entry'), name: 'entryFk', isTitle: true, @@ -29,7 +29,7 @@ const columns = [ columnFilter: false, }, { - align: 'left', + align: 'right', name: 'itemFk', label: t('Item'), columnFilter: false, @@ -44,21 +44,21 @@ const columns = [ cardVisible: true, }, { - align: 'left', + align: 'right', name: 'volume', label: t('Volume'), columnFilter: false, cardVisible: true, }, { - align: 'left', + align: 'right', label: t('Packaging'), name: 'packagingFk', columnFilter: false, cardVisible: true, }, { - align: 'left', + align: 'right', label: 'Packing', name: 'packing', columnFilter: false, @@ -73,12 +73,14 @@ const columns = [ ref="tableRef" data-key="StockBoughtsDetail" :url="customUrl" - order="itemName DESC" + order="volume DESC" :columns="columns" :right-search="false" :disable-infinite-scroll="true" :disable-option="{ card: true }" :limit="0" + :without-header="true" + :with-filters="false" auto-load > <template #column-entryFk="{ row }"> @@ -99,16 +101,14 @@ const columns = [ </template> <style lang="css" scoped> .container { - max-width: 50vw; + max-width: 100%; + width: 50%; overflow: auto; justify-content: center; align-items: center; margin: auto; background-color: var(--vn-section-color); - padding: 4px; -} -.container > div > div > .q-table__top.relative-position.row.items-center { - background-color: red !important; + padding: 2%; } </style> <i18n> diff --git a/src/pages/Entry/locale/en.yml b/src/pages/Entry/locale/en.yml index 80f3491a8..88b16cb03 100644 --- a/src/pages/Entry/locale/en.yml +++ b/src/pages/Entry/locale/en.yml @@ -1,21 +1,36 @@ entry: + lock: + title: Lock entry + message: This entry has been locked by {userName} for {time} minutes. Do you want to unlock it? + success: The entry has been locked successfully list: newEntry: New entry tableVisibleColumns: - created: Creation - supplierFk: Supplier - isBooked: Booked - isConfirmed: Confirmed + isExcludedFromAvailable: Exclude from inventory isOrdered: Ordered + isConfirmed: Ready to label + isReceived: Received + isRaid: Raid + landed: Date + supplierFk: Supplier + reference: Ref/Alb/Guide + invoiceNumber: Invoice + agencyModeId: Agency + isBooked: Booked companyFk: Company - travelFk: Travel - isExcludedFromAvailable: Inventory + evaNotes: Notes + warehouseOutFk: Origin + warehouseInFk: Destiny + entryTypeDescription: Entry type invoiceAmount: Import + travelFk: Travel + dated: Dated inventoryEntry: Inventory entry summary: commission: Commission currency: Currency invoiceNumber: Invoice number + invoiceAmount: Invoice amount ordered: Ordered booked: Booked excludedFromAvailable: Inventory @@ -33,6 +48,7 @@ entry: buyingValue: Buying value import: Import pvp: PVP + entryType: Entry type basicData: travel: Travel currency: Currency @@ -69,17 +85,55 @@ entry: landing: Landing isExcludedFromAvailable: Es inventory params: - toShipped: To - fromShipped: From - daysOnward: Days onward - daysAgo: Days ago - warehouseInFk: Warehouse in + isExcludedFromAvailable: Exclude from inventory + isOrdered: Ordered + isConfirmed: Ready to label + isReceived: Received + isIgnored: Ignored + isRaid: Raid + landed: Date + supplierFk: Supplier + reference: Ref/Alb/Guide + invoiceNumber: Invoice + agencyModeId: Agency + isBooked: Booked + companyFk: Company + evaNotes: Notes + warehouseOutFk: Origin + warehouseInFk: Destiny + entryTypeDescription: Entry type + invoiceAmount: Import + travelFk: Travel + dated: Dated + itemFk: Item id + hex: Color + name: Item name + size: Size + stickers: Stickers + packagingFk: Packaging + weight: Kg + groupingMode: Grouping selector + grouping: Grouping + quantity: Quantity + buyingValue: Buying value + price2: Package + price3: Box + minPrice: Minumum price + hasMinPrice: Has minimum price + packingOut: Packing out + comment: Comment + subName: Supplier name + tags: Tags + company_name: Company name + itemTypeFk: Item type + workerFk: Worker id search: Search entries searchInfo: You can search by entry reference descriptorMenu: showEntryReport: Show entry report entryFilter: params: + isExcludedFromAvailable: Exclude from inventory invoiceNumber: Invoice number travelFk: Travel companyFk: Company @@ -91,8 +145,16 @@ entryFilter: isBooked: Booked isConfirmed: Confirmed isOrdered: Ordered + isReceived: Received search: General search reference: Reference + landed: Landed + id: Id + agencyModeId: Agency + evaNotes: Notes + warehouseOutFk: Origin + warehouseInFk: Destiny + entryTypeCode: Entry type myEntries: id: ID landed: Landed diff --git a/src/pages/Entry/locale/es.yml b/src/pages/Entry/locale/es.yml index a5b968016..3025d64cb 100644 --- a/src/pages/Entry/locale/es.yml +++ b/src/pages/Entry/locale/es.yml @@ -1,21 +1,36 @@ entry: + lock: + title: Entrada bloqueada + message: Esta entrada ha sido bloqueada por {userName} hace {time} minutos. ¿Quieres desbloquearla? + success: La entrada ha sido bloqueada correctamente list: newEntry: Nueva entrada tableVisibleColumns: - created: Creación - supplierFk: Proveedor - isBooked: Asentado - isConfirmed: Confirmado + isExcludedFromAvailable: Excluir del inventario isOrdered: Pedida + isConfirmed: Lista para etiquetar + isReceived: Recibida + isRaid: Redada + landed: Fecha + supplierFk: Proveedor + invoiceNumber: Nº Factura + reference: Ref/Alb/Guía + agencyModeId: Agencia + isBooked: Asentado companyFk: Empresa travelFk: Envio - isExcludedFromAvailable: Inventario + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeDescription: Tipo entrada invoiceAmount: Importe + dated: Fecha inventoryEntry: Es inventario summary: commission: Comisión currency: Moneda invoiceNumber: Núm. factura + invoiceAmount: Importe ordered: Pedida booked: Contabilizada excludedFromAvailable: Inventario @@ -34,12 +49,13 @@ entry: buyingValue: Coste import: Importe pvp: PVP + entryType: Tipo entrada basicData: travel: Envío currency: Moneda observation: Observación commission: Comisión - booked: Asentado + booked: Contabilizada excludedFromAvailable: Inventario initialTemperature: Ini °C finalTemperature: Fin °C @@ -69,31 +85,70 @@ entry: packingOut: Embalaje envíos landing: Llegada isExcludedFromAvailable: Es inventario - params: - toShipped: Hasta - fromShipped: Desde - warehouseInFk: Alm. entrada - daysOnward: Días adelante - daysAgo: Días atras - descriptorMenu: - showEntryReport: Ver informe del pedido + search: Buscar entradas searchInfo: Puedes buscar por referencia de entrada + params: + isExcludedFromAvailable: Excluir del inventario + isOrdered: Pedida + isConfirmed: Lista para etiquetar + isReceived: Recibida + isRaid: Redada + isIgnored: Ignorado + landed: Fecha + supplierFk: Proveedor + invoiceNumber: Nº Factura + reference: Ref/Alb/Guía + agencyModeId: Agencia + isBooked: Asentado + companyFk: Empresa + travelFk: Envio + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeDescription: Tipo entrada + invoiceAmount: Importe + dated: Fecha + itemFk: Id artículo + hex: Color + name: Nombre artículo + size: Medida + stickers: Etiquetas + packagingFk: Embalaje + weight: Kg + groupinMode: Selector de grouping + grouping: Grouping + quantity: Quantity + buyingValue: Precio de compra + price2: Paquete + price3: Caja + minPrice: Precio mínimo + hasMinPrice: Tiene precio mínimo + packingOut: Packing out + comment: Referencia + subName: Nombre proveedor + tags: Etiquetas + company_name: Nombre empresa + itemTypeFk: Familia + workerFk: Comprador entryFilter: params: - invoiceNumber: Núm. factura - travelFk: Envío - companyFk: Empresa - currencyFk: Moneda - supplierFk: Proveedor - from: Desde - to: Hasta - created: Fecha creación - isBooked: Asentado - isConfirmed: Confirmado + isExcludedFromAvailable: Inventario isOrdered: Pedida - search: Búsqueda general - reference: Referencia + isConfirmed: Confirmado + isReceived: Recibida + isRaid: Raid + landed: Fecha + id: Id + supplierFk: Proveedor + invoiceNumber: Núm. factura + reference: Ref/Alb/Guía + agencyModeId: Modo agencia + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeCode: Tipo de entrada + hasToShowDeletedEntries: Mostrar entradas eliminadas myEntries: id: ID landed: F. llegada diff --git a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue index c01ec4ab4..905ddebb2 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue @@ -125,7 +125,7 @@ function deleteFile(dmsFk) { <VnInput clearable clear-icon="close" - :label="t('Supplier ref')" + :label="t('invoiceIn.supplierRef')" v-model="data.supplierRef" /> </VnRow> @@ -149,6 +149,7 @@ function deleteFile(dmsFk) { option-value="id" option-label="id" :filter-options="['id', 'name']" + data-cy="UnDeductibleVatSelect" > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -215,7 +216,7 @@ function deleteFile(dmsFk) { v-else icon="add_circle" round - shortcut="+" + v-shortcut="'+'" padding="xs" @click=" () => { @@ -310,7 +311,6 @@ function deleteFile(dmsFk) { supplierFk: Supplier es: supplierFk: Proveedor - Supplier ref: Ref. proveedor Expedition date: Fecha expedición Operation date: Fecha operación Undeductible VAT: Iva no deducible diff --git a/src/pages/InvoiceIn/Card/InvoiceInCard.vue b/src/pages/InvoiceIn/Card/InvoiceInCard.vue index 8aa35f4d8..34cc26437 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInCard.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInCard.vue @@ -1,47 +1,18 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import InvoiceInDescriptor from './InvoiceInDescriptor.vue'; +import { onBeforeRouteUpdate } from 'vue-router'; +import { setRectificative } from '../composables/setRectificative'; +import filter from './InvoiceInFilter.js'; -const filter = { - include: [ - { - relation: 'supplier', - scope: { - include: { - relation: 'contacts', - scope: { where: { email: { neq: null } } }, - }, - }, - }, - { relation: 'invoiceInDueDay' }, - { relation: 'company' }, - { relation: 'currency' }, - { - relation: 'dms', - scope: { - fields: [ - 'dmsTypeFk', - 'reference', - 'hardCopyNumber', - 'workerFk', - 'description', - 'hasFile', - 'file', - 'created', - 'companyFk', - 'warehouseFk', - ], - }, - }, - ], -}; +onBeforeRouteUpdate(async (to) => await setRectificative(to)); </script> <template> <VnCardBeta data-key="InvoiceIn" - base-url="InvoiceIns" + url="InvoiceIns" :descriptor="InvoiceInDescriptor" - :user-filter="filter" + :filter="filter" /> </template> diff --git a/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue b/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue index da7bd4426..3843f5bf7 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue @@ -7,6 +7,7 @@ import { toCurrency, toDate } from 'src/filters'; import VnLv from 'src/components/ui/VnLv.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; +import filter from './InvoiceInFilter.js'; import InvoiceInDescriptorMenu from './InvoiceInDescriptorMenu.vue'; const $props = defineProps({ id: { type: Number, default: null } }); @@ -16,33 +17,10 @@ const { t } = useI18n(); const cardDescriptorRef = ref(); const entityId = computed(() => $props.id || +currentRoute.value.params.id); const totalAmount = ref(); - -const filter = { - include: [ - { - relation: 'supplier', - scope: { - include: { - relation: 'contacts', - scope: { - where: { - email: { neq: null }, - }, - }, - }, - }, - }, - { - relation: 'invoiceInDueDay', - }, - { - relation: 'company', - }, - { - relation: 'currency', - }, - ], -}; +const config = ref(); +const cplusRectificationTypes = ref([]); +const siiTypeInvoiceIns = ref([]); +const invoiceCorrectionTypes = ref([]); const invoiceInCorrection = reactive({ correcting: [], corrected: null }); const routes = reactive({ getSupplier: (id) => { @@ -112,7 +90,6 @@ async function setInvoiceCorrection(id) { <template> <CardDescriptor ref="cardDescriptorRef" - module="InvoiceIn" data-key="InvoiceIn" :url="`InvoiceIns/${entityId}`" :filter="filter" diff --git a/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue b/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue index c3ab635c8..8b039ec27 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue @@ -186,7 +186,7 @@ const createInvoiceInCorrection = async () => { clickable @click="book(entityId)" > - <QItemSection>{{ t('invoiceIn.descriptorMenu.toBook') }}</QItemSection> + <QItemSection>{{ t('invoiceIn.descriptorMenu.book') }}</QItemSection> </QItem> </template> </InvoiceInToBook> @@ -197,7 +197,7 @@ const createInvoiceInCorrection = async () => { @click="triggerMenu('unbook')" > <QItemSection> - {{ t('invoiceIn.descriptorMenu.toUnbook') }} + {{ t('invoiceIn.descriptorMenu.unbook') }} </QItemSection> </QItem> <QItem diff --git a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue index 23387ff74..20cc1cc71 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed } from 'vue'; +import { ref, computed, onBeforeMount } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; @@ -11,6 +11,7 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import useNotify from 'src/composables/useNotify.js'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +import { toCurrency } from 'filters/index'; const route = useRoute(); const { notify } = useNotify(); @@ -24,7 +25,7 @@ const invoiceInFormRef = ref(); const invoiceId = +route.params.id; const filter = { where: { invoiceInFk: invoiceId } }; const areRows = ref(false); - +const totals = ref(); const columns = computed(() => [ { name: 'duedate', @@ -63,6 +64,8 @@ const columns = computed(() => [ }, ]); +const totalAmount = computed(() => getTotal(invoiceInFormRef.value.formData, 'amount')); + const isNotEuro = (code) => code != 'EUR'; async function insert() { @@ -70,6 +73,10 @@ async function insert() { await invoiceInFormRef.value.reload(); notify(t('globals.dataSaved'), 'positive'); } + +onBeforeMount(async () => { + totals.value = (await axios.get(`InvoiceIns/${invoiceId}/getTotals`)).data; +}); </script> <template> <CrudModel @@ -144,7 +151,7 @@ async function insert() { <QTd /> <QTd /> <QTd> - {{ getTotal(rows, 'amount', { currency: 'default' }) }} + {{ toCurrency(totalAmount) }} </QTd> <QTd> <template v-if="isNotEuro(invoiceIn.currency.code)"> @@ -222,10 +229,19 @@ async function insert() { <QBtn color="primary" icon="add" - shortcut="+" + v-shortcut="'+'" size="lg" round - @click="!areRows ? insert() : invoiceInFormRef.insert()" + @click=" + () => { + if (!areRows) insert(); + else + invoiceInFormRef.insert({ + amount: (totals.totalTaxableBase - totalAmount).toFixed(2), + invoiceInFk: invoiceId, + }); + } + " /> </QPageSticky> </template> diff --git a/src/pages/InvoiceIn/Card/InvoiceInFilter.js b/src/pages/InvoiceIn/Card/InvoiceInFilter.js new file mode 100644 index 000000000..6df8b5830 --- /dev/null +++ b/src/pages/InvoiceIn/Card/InvoiceInFilter.js @@ -0,0 +1,33 @@ +export default { + include: [ + { + relation: 'supplier', + scope: { + include: { + relation: 'contacts', + scope: { where: { email: { neq: null } } }, + }, + }, + }, + { relation: 'invoiceInDueDay' }, + { relation: 'company' }, + { relation: 'currency' }, + { + relation: 'dms', + scope: { + fields: [ + 'dmsTypeFk', + 'reference', + 'hardCopyNumber', + 'workerFk', + 'description', + 'hasFile', + 'file', + 'created', + 'companyFk', + 'warehouseFk', + ], + }, + }, + ], +}; diff --git a/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue b/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue index e529ea6cd..6f8642313 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue @@ -218,7 +218,7 @@ const columns = computed(() => [ <QBtn color="primary" icon="add" - shortcut="+" + v-shortcut="'+'" size="lg" round @click="invoiceInFormRef.insert()" diff --git a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue index e546638f2..d358601d3 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue @@ -193,7 +193,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; <InvoiceIntoBook> <template #content="{ book }"> <QBtn - :label="t('To book')" + :label="t('Book')" color="orange-11" text-color="black" @click="book(entityId)" @@ -224,10 +224,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; </span> </template> </VnLv> - <VnLv - :label="t('invoiceIn.list.supplierRef')" - :value="entity.supplierRef" - /> + <VnLv :label="t('invoiceIn.supplierRef')" :value="entity.supplierRef" /> <VnLv :label="t('invoiceIn.summary.currency')" :value="entity.currency?.code" @@ -357,7 +354,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; entity.totals.totalTaxableBaseForeignValue && toCurrency( entity.totals.totalTaxableBaseForeignValue, - currency + currency, ) }}</QTd> </QTr> @@ -392,7 +389,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; entity.totals.totalDueDayForeignValue && toCurrency( entity.totals.totalDueDayForeignValue, - currency + currency, ) }} </QTd> @@ -472,5 +469,5 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; Search invoice: Buscar factura recibida You can search by invoice reference: Puedes buscar por referencia de la factura Totals: Totales - To book: Contabilizar + Book: Contabilizar </i18n> diff --git a/src/pages/InvoiceIn/Card/InvoiceInVat.vue b/src/pages/InvoiceIn/Card/InvoiceInVat.vue index f99e060b8..e77453bc0 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInVat.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInVat.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed } from 'vue'; +import { ref, computed, nextTick } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useArrayData } from 'src/composables/useArrayData'; @@ -25,7 +25,6 @@ const sageTaxTypes = ref([]); const sageTransactionTypes = ref([]); const rowsSelected = ref([]); const invoiceInFormRef = ref(); -const expenseRef = ref(); defineProps({ actionIcon: { @@ -97,6 +96,20 @@ const columns = computed(() => [ }, ]); +const taxableBaseTotal = computed(() => { + return getTotal(invoiceInFormRef.value.formData, 'taxableBase'); +}); + +const taxRateTotal = computed(() => { + return getTotal(invoiceInFormRef.value.formData, null, { + cb: taxRate, + }); +}); + +const combinedTotal = computed(() => { + return +taxableBaseTotal.value + +taxRateTotal.value; +}); + const filter = { fields: [ 'id', @@ -117,7 +130,7 @@ const isNotEuro = (code) => code != 'EUR'; function taxRate(invoiceInTax) { const sageTaxTypeId = invoiceInTax.taxTypeSageFk; const taxRateSelection = sageTaxTypes.value.find( - (transaction) => transaction.id == sageTaxTypeId + (transaction) => transaction.id == sageTaxTypeId, ); const taxTypeSage = taxRateSelection?.rate ?? 0; const taxableBase = invoiceInTax?.taxableBase ?? 0; @@ -125,35 +138,26 @@ function taxRate(invoiceInTax) { return ((taxTypeSage / 100) * taxableBase).toFixed(2); } -function autocompleteExpense(evt, row, col) { +function autocompleteExpense(evt, row, col, ref) { const val = evt.target.value; if (!val) return; const param = isNaN(val) ? row[col.model] : val; const lookup = expenses.value.find( - ({ id }) => id == useAccountShortToStandard(param) + ({ id }) => id == useAccountShortToStandard(param), ); - expenseRef.value.vnSelectDialogRef.vnSelectRef.toggleOption(lookup); + ref.vnSelectDialogRef.vnSelectRef.toggleOption(lookup); } -const taxableBaseTotal = computed(() => { - return getTotal(invoiceInFormRef.value.formData, 'taxableBase', ); -}); - -const taxRateTotal = computed(() => { - return getTotal(invoiceInFormRef.value.formData, null, { - cb: taxRate, +function setCursor(ref) { + nextTick(() => { + const select = ref.vnSelectDialogRef + ? ref.vnSelectDialogRef.vnSelectRef + : ref.vnSelectRef; + select.$el.querySelector('input').setSelectionRange(0, 0); }); -}); - - -const combinedTotal = computed(() => { - return +taxableBaseTotal.value + +taxRateTotal.value; -}); - - - +} </script> <template> <FetchData @@ -191,14 +195,24 @@ const combinedTotal = computed(() => { <template #body-cell-expense="{ row, col }"> <QTd> <VnSelectDialog - ref="expenseRef" + :ref="`expenseRef-${row.$index}`" v-model="row[col.model]" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'name']" :tooltip="t('Create a new expense')" - @keydown.tab="autocompleteExpense($event, row, col)" + @keydown.tab=" + autocompleteExpense( + $event, + row, + col, + $refs[`expenseRef-${row.$index}`], + ) + " + @update:model-value=" + setCursor($refs[`expenseRef-${row.$index}`]) + " > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -214,7 +228,7 @@ const combinedTotal = computed(() => { </QTd> </template> <template #body-cell-taxablebase="{ row }"> - <QTd> + <QTd shrink> <VnInputNumber clear-icon="close" v-model="row.taxableBase" @@ -225,12 +239,16 @@ const combinedTotal = computed(() => { <template #body-cell-sageiva="{ row, col }"> <QTd> <VnSelect + :ref="`sageivaRef-${row.$index}`" v-model="row[col.model]" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'vat']" data-cy="vat-sageiva" + @update:model-value=" + setCursor($refs[`sageivaRef-${row.$index}`]) + " > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -248,11 +266,15 @@ const combinedTotal = computed(() => { <template #body-cell-sagetransaction="{ row, col }"> <QTd> <VnSelect + :ref="`sagetransactionRef-${row.$index}`" v-model="row[col.model]" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'transaction']" + @update:model-value=" + setCursor($refs[`sagetransactionRef-${row.$index}`]) + " > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -270,7 +292,7 @@ const combinedTotal = computed(() => { </QTd> </template> <template #body-cell-foreignvalue="{ row }"> - <QTd> + <QTd shrink> <VnInputNumber :class="{ 'no-pointer-events': !isNotEuro(currency), @@ -283,7 +305,7 @@ const combinedTotal = computed(() => { row.taxableBase = await getExchange( val, row.currencyFk, - invoiceIn.issued + invoiceIn.issued, ); } " @@ -426,7 +448,7 @@ const combinedTotal = computed(() => { color="primary" icon="add" size="lg" - shortcut="+" + v-shortcut="'+'" round @click="invoiceInFormRef.insert()" > diff --git a/src/pages/InvoiceIn/InvoiceInList.vue b/src/pages/InvoiceIn/InvoiceInList.vue index e1723e3b1..0960d0d6c 100644 --- a/src/pages/InvoiceIn/InvoiceInList.vue +++ b/src/pages/InvoiceIn/InvoiceInList.vue @@ -29,6 +29,7 @@ const cols = computed(() => [ name: 'isBooked', label: t('invoiceIn.isBooked'), columnFilter: false, + component: 'checkbox', }, { align: 'left', @@ -56,7 +57,7 @@ const cols = computed(() => [ { align: 'left', name: 'supplierRef', - label: t('invoiceIn.list.supplierRef'), + label: t('invoiceIn.supplierRef'), }, { align: 'left', @@ -177,7 +178,7 @@ const cols = computed(() => [ :required="true" /> <VnInput - :label="t('invoiceIn.list.supplierRef')" + :label="t('invoiceIn.supplierRef')" v-model="data.supplierRef" /> <VnSelect diff --git a/src/pages/InvoiceIn/InvoiceInToBook.vue b/src/pages/InvoiceIn/InvoiceInToBook.vue index 95ce8155a..5bdbe197b 100644 --- a/src/pages/InvoiceIn/InvoiceInToBook.vue +++ b/src/pages/InvoiceIn/InvoiceInToBook.vue @@ -4,6 +4,7 @@ import { useQuasar } from 'quasar'; import { useI18n } from 'vue-i18n'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; import { useArrayData } from 'src/composables/useArrayData'; +import qs from 'qs'; const { notify, dialog } = useQuasar(); const { t } = useI18n(); @@ -12,29 +13,51 @@ defineExpose({ checkToBook }); const { store } = useArrayData(); async function checkToBook(id) { - let directBooking = true; + let messages = []; + + const hasProblemWithTax = ( + await axios.get('InvoiceInTaxes/count', { + params: { + where: JSON.stringify({ + invoiceInFk: id, + or: [{ taxTypeSageFk: null }, { transactionTypeSageFk: null }], + }), + }, + }) + ).data?.count; + + if (hasProblemWithTax) + messages.push(t('The VAT and Transaction fields have not been informed')); const { data: totals } = await axios.get(`InvoiceIns/${id}/getTotals`); const taxableBaseNotEqualDueDay = totals.totalDueDay != totals.totalTaxableBase; const vatNotEqualDueDay = totals.totalDueDay != totals.totalVat; - if (taxableBaseNotEqualDueDay && vatNotEqualDueDay) directBooking = false; + if (taxableBaseNotEqualDueDay && vatNotEqualDueDay) + messages.push(t('The sum of the taxable bases does not match the due dates')); - const { data: dueDaysCount } = await axios.get('InvoiceInDueDays/count', { - where: { - invoiceInFk: id, - dueDated: { gte: Date.vnNew() }, - }, - }); + const dueDaysCount = ( + await axios.get('InvoiceInDueDays/count', { + params: { + where: JSON.stringify({ + invoiceInFk: id, + dueDated: { gte: Date.vnNew() }, + }), + }, + }) + ).data?.count; - if (dueDaysCount) directBooking = false; + if (dueDaysCount) messages.push(t('Some due dates are less than or equal to today')); - if (directBooking) return toBook(id); - - dialog({ - component: VnConfirm, - componentProps: { title: t('Are you sure you want to book this invoice?') }, - }).onOk(async () => await toBook(id)); + if (!messages.length) toBook(id); + else + dialog({ + component: VnConfirm, + componentProps: { + title: t('Are you sure you want to book this invoice?'), + message: messages.reduce((acc, msg) => `${acc}<p>${msg}</p>`, ''), + }, + }).onOk(() => toBook(id)); } async function toBook(id) { @@ -59,4 +82,7 @@ async function toBook(id) { es: Are you sure you want to book this invoice?: ¿Estás seguro de querer asentar esta factura? It was not able to book the invoice: No se pudo contabilizar la factura + Some due dates are less than or equal to today: Algún vencimiento tiene una fecha menor o igual que hoy + The sum of the taxable bases does not match the due dates: La suma de las bases imponibles no coincide con la de los vencimientos + The VAT and Transaction fields have not been informed: No se han informado los campos de iva y/o transacción </i18n> diff --git a/src/pages/InvoiceIn/locale/en.yml b/src/pages/InvoiceIn/locale/en.yml index 6b21b316b..548e6c201 100644 --- a/src/pages/InvoiceIn/locale/en.yml +++ b/src/pages/InvoiceIn/locale/en.yml @@ -3,10 +3,10 @@ invoiceIn: searchInfo: Search incoming invoices by ID or supplier fiscal name serial: Serial isBooked: Is booked + supplierRef: Invoice nº list: ref: Reference supplier: Supplier - supplierRef: Supplier ref. file: File issued: Issued dueDated: Due dated @@ -19,8 +19,6 @@ invoiceIn: unbook: Unbook delete: Delete clone: Clone - toBook: To book - toUnbook: To unbook deleteInvoice: Delete invoice invoiceDeleted: invoice deleted cloneInvoice: Clone invoice @@ -70,4 +68,3 @@ invoiceIn: isBooked: Is booked account: Ledger account correctingFk: Rectificative - \ No newline at end of file diff --git a/src/pages/InvoiceIn/locale/es.yml b/src/pages/InvoiceIn/locale/es.yml index 3f27c895c..142d95f92 100644 --- a/src/pages/InvoiceIn/locale/es.yml +++ b/src/pages/InvoiceIn/locale/es.yml @@ -3,10 +3,10 @@ invoiceIn: searchInfo: Buscar facturas recibidas por ID o nombre fiscal del proveedor serial: Serie isBooked: Contabilizada + supplierRef: Nº factura list: ref: Referencia supplier: Proveedor - supplierRef: Ref. proveedor issued: F. emisión dueDated: F. vencimiento file: Fichero @@ -15,12 +15,10 @@ invoiceIn: descriptor: ticketList: Listado de tickets descriptorMenu: - book: Asentar - unbook: Desasentar + book: Contabilizar + unbook: Descontabilizar delete: Eliminar clone: Clonar - toBook: Contabilizar - toUnbook: Descontabilizar deleteInvoice: Eliminar factura invoiceDeleted: Factura eliminada cloneInvoice: Clonar factura @@ -68,4 +66,3 @@ invoiceIn: isBooked: Contabilizada account: Cuenta contable correctingFk: Rectificativa - diff --git a/src/pages/InvoiceOut/Card/InvoiceOutCard.vue b/src/pages/InvoiceOut/Card/InvoiceOutCard.vue index 93e3fe042..a50c9d247 100644 --- a/src/pages/InvoiceOut/Card/InvoiceOutCard.vue +++ b/src/pages/InvoiceOut/Card/InvoiceOutCard.vue @@ -1,11 +1,13 @@ <script setup> import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue'; import VnCardBeta from 'components/common/VnCardBeta.vue'; +import filter from './InvoiceOutFilter.js'; </script> <template> <VnCardBeta data-key="InvoiceOut" - base-url="InvoiceOuts" + url="InvoiceOuts" + :filter="filter" :descriptor="InvoiceOutDescriptor" /> </template> diff --git a/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue b/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue index 209f1531e..dfaf6c109 100644 --- a/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue +++ b/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue @@ -8,8 +8,8 @@ import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy import VnLv from 'src/components/ui/VnLv.vue'; import InvoiceOutDescriptorMenu from './InvoiceOutDescriptorMenu.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import { toCurrency, toDate } from 'src/filters'; +import filter from './InvoiceOutFilter.js'; const $props = defineProps({ id: { @@ -26,42 +26,20 @@ const entityId = computed(() => { return $props.id || route.params.id; }); -const filter = { - include: [ - { - relation: 'company', - scope: { - fields: ['id', 'code'], - }, - }, - { - relation: 'client', - scope: { - fields: ['id', 'name', 'email'], - }, - }, - ], -}; - const descriptor = ref(); function ticketFilter(invoice) { return JSON.stringify({ refFk: invoice.ref }); } -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.ref, entity.id)); </script> <template> <CardDescriptor ref="descriptor" - module="InvoiceOut" :url="`InvoiceOuts/${entityId}`" :filter="filter" - :title="data.title" - :subtitle="data.subtitle" - @on-fetch="setData" - data-key="invoiceOutData" + title="ref" + data-key="InvoiceOut" width="lg-width" > <template #menu="{ entity, menuRef }"> diff --git a/src/pages/InvoiceOut/Card/InvoiceOutFilter.js b/src/pages/InvoiceOut/Card/InvoiceOutFilter.js new file mode 100644 index 000000000..48b20faf6 --- /dev/null +++ b/src/pages/InvoiceOut/Card/InvoiceOutFilter.js @@ -0,0 +1,16 @@ +export default { + include: [ + { + relation: 'company', + scope: { + fields: ['id', 'code'], + }, + }, + { + relation: 'client', + scope: { + fields: ['id', 'name', 'email'], + }, + }, + ], +}; diff --git a/src/pages/Item/Card/ItemBarcode.vue b/src/pages/Item/Card/ItemBarcode.vue index 6db5943c7..590b524cd 100644 --- a/src/pages/Item/Card/ItemBarcode.vue +++ b/src/pages/Item/Card/ItemBarcode.vue @@ -92,7 +92,7 @@ const submit = async (rows) => { class="cursor-pointer fill-icon-on-hover" color="primary" icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat > <QTooltip> diff --git a/src/pages/Item/Card/ItemBasicData.vue b/src/pages/Item/Card/ItemBasicData.vue index 4c96401f3..df7e71684 100644 --- a/src/pages/Item/Card/ItemBasicData.vue +++ b/src/pages/Item/Card/ItemBasicData.vue @@ -11,6 +11,7 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; import FilterItemForm from 'src/components/FilterItemForm.vue'; import CreateIntrastatForm from './CreateIntrastatForm.vue'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const route = useRoute(); const { t } = useI18n(); @@ -54,9 +55,8 @@ const onIntrastatCreated = (response, formData) => { auto-load /> <FormModel - :url="`Items/${route.params.id}`" :url-update="`Items/${route.params.id}`" - model="item" + model="Item" auto-load :clear-store-on-unmount="false" > @@ -209,30 +209,20 @@ const onIntrastatCreated = (response, formData) => { /> </VnRow> <VnRow class="row q-gutter-md q-mb-md"> - <div> - <QCheckbox - v-model="data.isFragile" - :label="t('item.basicData.isFragile')" - class="q-mr-sm" - /> - <QIcon name="info" class="cursor-pointer" size="xs"> - <QTooltip max-width="300px"> - {{ t('item.basicData.isFragileTooltip') }} - </QTooltip> - </QIcon> - </div> - <div> - <QCheckbox - v-model="data.isPhotoRequested" - :label="t('item.basicData.isPhotoRequested')" - class="q-mr-sm" - /> - <QIcon name="info" class="cursor-pointer" size="xs"> - <QTooltip> - {{ t('item.basicData.isPhotoRequestedTooltip') }} - </QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="data.isFragile" + :label="t('item.basicData.isFragile')" + :info="t('item.basicData.isFragileTooltip')" + class="q-mr-sm" + size="xs" + /> + <VnCheckbox + v-model="data.isPhotoRequested" + :label="t('item.basicData.isPhotoRequested')" + :info="t('item.basicData.isPhotoRequestedTooltip')" + class="q-mr-sm" + size="xs" + /> </VnRow> <VnRow> <VnInput diff --git a/src/pages/Item/Card/ItemBotanical.vue b/src/pages/Item/Card/ItemBotanical.vue index 4894d94fc..a40d81589 100644 --- a/src/pages/Item/Card/ItemBotanical.vue +++ b/src/pages/Item/Card/ItemBotanical.vue @@ -7,8 +7,8 @@ import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; -import CreateGenusForm from './CreateGenusForm.vue'; -import CreateSpecieForm from './CreateSpecieForm.vue'; +import CreateGenusForm from '../components/CreateGenusForm.vue'; +import CreateSpecieForm from '../components/CreateSpecieForm.vue'; const route = useRoute(); const { t } = useI18n(); diff --git a/src/pages/Item/Card/ItemCard.vue b/src/pages/Item/Card/ItemCard.vue index 2546982eb..610b77a02 100644 --- a/src/pages/Item/Card/ItemCard.vue +++ b/src/pages/Item/Card/ItemCard.vue @@ -5,7 +5,7 @@ import ItemDescriptor from './ItemDescriptor.vue'; <template> <VnCardBeta data-key="Item" - base-url="Items" + :url="`Items/${$route.params.id}/getCard`" :descriptor="ItemDescriptor" /> </template> diff --git a/src/pages/Item/Card/ItemDescriptor.vue b/src/pages/Item/Card/ItemDescriptor.vue index c6fee8540..a4c58ef4b 100644 --- a/src/pages/Item/Card/ItemDescriptor.vue +++ b/src/pages/Item/Card/ItemDescriptor.vue @@ -7,7 +7,6 @@ import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import { dashIfEmpty } from 'src/filters'; import { useArrayData } from 'src/composables/useArrayData'; @@ -35,6 +34,10 @@ const $props = defineProps({ type: Number, default: null, }, + proxyRender: { + type: Boolean, + default: false, + }, }); const route = useRoute(); @@ -55,10 +58,8 @@ onMounted(async () => { mounted.value = true; }); -const data = ref(useCardDescription()); const setData = async (entity) => { if (!entity) return; - data.value = useCardDescription(entity.name, entity.id); await updateStock(); }; @@ -90,10 +91,7 @@ const updateStock = async () => { <template> <CardDescriptor - data-key="ItemData" - module="Item" - :title="data.title" - :subtitle="data.subtitle" + data-key="Item" :summary="$props.summary" :url="`Items/${entityId}/getCard`" @on-fetch="setData" @@ -117,7 +115,7 @@ const updateStock = async () => { <template #value> <span class="link"> {{ entity.itemType?.worker?.user?.name }} - <WorkerDescriptorProxy :id="entity.itemType?.worker?.id" /> + <WorkerDescriptorProxy :id="entity.itemType?.worker?.id ?? NaN" /> </span> </template> </VnLv> @@ -152,7 +150,7 @@ const updateStock = async () => { </QCardActions> </template> <template #actions="{}"> - <QCardActions class="row justify-center"> + <QCardActions class="row justify-center" v-if="proxyRender"> <QBtn :to="{ name: 'ItemDiary', @@ -165,6 +163,16 @@ const updateStock = async () => { > <QTooltip>{{ t('item.descriptor.itemDiary') }}</QTooltip> </QBtn> + <QBtn + :to="{ + name: 'ItemLastEntries', + }" + size="md" + icon="vn:regentry" + color="primary" + > + <QTooltip>{{ t('item.descriptor.itemLastEntries') }}</QTooltip> + </QBtn> </QCardActions> </template> </CardDescriptor> diff --git a/src/pages/Item/Card/ItemDescriptorProxy.vue b/src/pages/Item/Card/ItemDescriptorProxy.vue index 2ffc9080f..f686e8221 100644 --- a/src/pages/Item/Card/ItemDescriptorProxy.vue +++ b/src/pages/Item/Card/ItemDescriptorProxy.vue @@ -4,7 +4,7 @@ import ItemSummary from './ItemSummary.vue'; const $props = defineProps({ id: { - type: Number, + type: [Number, String], required: true, }, dated: { @@ -21,9 +21,8 @@ const $props = defineProps({ }, }); </script> - <template> - <QPopupProxy> + <QPopupProxy style="max-width: 10px"> <ItemDescriptor v-if="$props.id" :id="$props.id" @@ -31,6 +30,7 @@ const $props = defineProps({ :dated="dated" :sale-fk="saleFk" :warehouse-fk="warehouseFk" + :proxy-render="true" /> </QPopupProxy> </template> diff --git a/src/pages/Item/Card/ItemShelving.vue b/src/pages/Item/Card/ItemShelving.vue index 7ad60c9e0..b29e2a2a5 100644 --- a/src/pages/Item/Card/ItemShelving.vue +++ b/src/pages/Item/Card/ItemShelving.vue @@ -110,10 +110,16 @@ const columns = computed(() => [ attrs: { inWhere: true }, align: 'left', }, + { + label: t('globals.visible'), + name: 'stock', + attrs: { inWhere: true }, + align: 'left', + }, ]); const totalLabels = computed(() => - rows.value.reduce((acc, row) => acc + row.stock / row.packing, 0).toFixed(2) + rows.value.reduce((acc, row) => acc + row.stock / row.packing, 0).toFixed(2), ); const removeLines = async () => { @@ -157,7 +163,7 @@ watchEffect(selectedRows); openConfirmationModal( t('shelvings.removeConfirmTitle'), t('shelvings.removeConfirmSubtitle'), - removeLines + removeLines, ) " > diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue index 5a7d7f818..ab26b9cae 100644 --- a/src/pages/Item/Card/ItemTags.vue +++ b/src/pages/Item/Card/ItemTags.vue @@ -178,7 +178,7 @@ const insertTag = (rows) => { @click="insertTag(rows)" color="primary" icon="add" - shortcut="+" + v-shortcut="'+'" fab data-cy="createNewTag" > diff --git a/src/pages/Item/ItemFixedPrice.vue b/src/pages/Item/ItemFixedPrice.vue index 1c4382fbd..fdfa1d3d1 100644 --- a/src/pages/Item/ItemFixedPrice.vue +++ b/src/pages/Item/ItemFixedPrice.vue @@ -65,10 +65,19 @@ const columns = computed(() => [ name: 'name', ...defaultColumnAttrs, create: true, + columnFilter: { + component: 'select', + attrs: { + url: 'Items', + fields: ['id', 'name', 'subName'], + optionLabel: 'name', + optionValue: 'name', + uppercase: false, + }, + }, }, { label: t('item.fixedPrice.groupingPrice'), - field: 'rate2', name: 'rate2', ...defaultColumnAttrs, component: 'input', @@ -76,7 +85,6 @@ const columns = computed(() => [ }, { label: t('item.fixedPrice.packingPrice'), - field: 'rate3', name: 'rate3', ...defaultColumnAttrs, component: 'input', @@ -85,7 +93,6 @@ const columns = computed(() => [ { label: t('item.fixedPrice.minPrice'), - field: 'minPrice', name: 'minPrice', ...defaultColumnAttrs, component: 'input', @@ -108,7 +115,6 @@ const columns = computed(() => [ }, { label: t('item.fixedPrice.ended'), - field: 'ended', name: 'ended', ...defaultColumnAttrs, columnField: { @@ -124,7 +130,6 @@ const columns = computed(() => [ { label: t('globals.warehouse'), - field: 'warehouseFk', name: 'warehouseFk', ...defaultColumnAttrs, columnClass: 'shrink', @@ -415,7 +420,6 @@ function handleOnDataSave({ CrudModelRef }) { 'row-key': 'id', selection: 'multiple', }" - :use-model="true" v-model:selected="rowsSelected" :create-as-dialog="false" :create="{ diff --git a/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue b/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue index b4032ff8a..475dffd8b 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue @@ -40,12 +40,7 @@ const itemPackingTypesOptions = ref([]); }" auto-load /> - <FormModel - :url="`ItemTypes/${route.params.id}`" - :url-update="`ItemTypes/${route.params.id}`" - model="itemTypeBasicData" - auto-load - > + <FormModel :url-update="`ItemTypes/${route.params.id}`" model="ItemType" auto-load> <template #form="{ data }"> <VnRow> <VnInput v-model="data.code" :label="t('itemType.shared.code')" /> diff --git a/src/pages/Item/ItemType/Card/ItemTypeCard.vue b/src/pages/Item/ItemType/Card/ItemTypeCard.vue index fa51e428e..84e810de5 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeCard.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeCard.vue @@ -1,12 +1,14 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import ItemTypeDescriptor from 'src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue'; +import filter from './ItemTypeFilter.js'; </script> <template> <VnCardBeta - data-key="ItemTypeSummary" - base-url="ItemTypes" + data-key="ItemType" + url="ItemTypes" + :filter="filter" :descriptor="ItemTypeDescriptor" /> </template> diff --git a/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue b/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue index 09d3dbce5..725fb30aa 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue @@ -1,12 +1,11 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import useCardDescription from 'src/composables/useCardDescription'; +import filter from './ItemTypeFilter.js'; const $props = defineProps({ id: { @@ -20,46 +19,31 @@ const $props = defineProps({ }); const route = useRoute(); -const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); - -const itemTypeFilter = { - include: [ - { relation: 'worker' }, - { relation: 'category' }, - { relation: 'itemPackingType' }, - { relation: 'temperature' }, - ], -}; - -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); </script> - <template> <CardDescriptor - module="ItemType" :url="`ItemTypes/${entityId}`" - :filter="itemTypeFilter" - :title="data.title" - :subtitle="data.subtitle" - data-key="itemTypeDescriptor" - @on-fetch="setData" + :filter="filter" + title="code" + data-key="ItemType" > <template #body="{ entity }"> - <VnLv :label="t('itemType.shared.code')" :value="entity.code" /> - <VnLv :label="t('itemType.shared.name')" :value="entity.name" /> - <VnLv :label="t('itemType.shared.worker')"> + <VnLv :label="$t('itemType.shared.code')" :value="entity.code" /> + <VnLv :label="$t('itemType.shared.name')" :value="entity.name" /> + <VnLv :label="$t('itemType.shared.worker')"> <template #value> <span class="link">{{ entity.worker?.firstName }}</span> <WorkerDescriptorProxy :id="entity.worker?.id" /> </template> </VnLv> - <VnLv :label="t('itemType.shared.category')" :value="entity.category?.name" /> + <VnLv + :label="$t('itemType.shared.category')" + :value="entity.category?.name" + /> </template> </CardDescriptor> </template> - diff --git a/src/pages/Item/ItemType/Card/ItemTypeFilter.js b/src/pages/Item/ItemType/Card/ItemTypeFilter.js new file mode 100644 index 000000000..5651d368d --- /dev/null +++ b/src/pages/Item/ItemType/Card/ItemTypeFilter.js @@ -0,0 +1,8 @@ +export default { + include: [ + { relation: 'worker' }, + { relation: 'category' }, + { relation: 'itemPackingType' }, + { relation: 'temperature' }, + ], +}; diff --git a/src/pages/Item/ItemType/Card/ItemTypeSummary.vue b/src/pages/Item/ItemType/Card/ItemTypeSummary.vue index 9ba774ca4..3b63c4b63 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeSummary.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeSummary.vue @@ -3,7 +3,7 @@ import { ref, computed, onUpdated } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; - +import filter from './ItemTypeFilter.js'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnToSummary from 'src/components/ui/VnToSummary.vue'; @@ -21,15 +21,6 @@ const $props = defineProps({ }, }); -const itemTypeFilter = { - include: [ - { relation: 'worker' }, - { relation: 'category' }, - { relation: 'itemPackingType' }, - { relation: 'temperature' }, - ], -}; - const entityId = computed(() => $props.id || route.params.id); const summaryRef = ref(); const itemType = ref(); @@ -43,8 +34,8 @@ async function setItemTypeData(data) { <CardSummary ref="summaryRef" :url="`ItemTypes/${entityId}`" - data-key="ItemTypeSummary" - :filter="itemTypeFilter" + data-key="ItemType" + :filter="filter" @on-fetch="(data) => setItemTypeData(data)" class="full-width" > diff --git a/src/pages/Item/Card/CreateGenusForm.vue b/src/pages/Item/components/CreateGenusForm.vue similarity index 100% rename from src/pages/Item/Card/CreateGenusForm.vue rename to src/pages/Item/components/CreateGenusForm.vue diff --git a/src/pages/Item/Card/CreateSpecieForm.vue b/src/pages/Item/components/CreateSpecieForm.vue similarity index 100% rename from src/pages/Item/Card/CreateSpecieForm.vue rename to src/pages/Item/components/CreateSpecieForm.vue diff --git a/src/pages/Item/components/ItemProposal.vue b/src/pages/Item/components/ItemProposal.vue new file mode 100644 index 000000000..d2dbea7b3 --- /dev/null +++ b/src/pages/Item/components/ItemProposal.vue @@ -0,0 +1,332 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; +import { toCurrency } from 'filters/index'; +import VnStockValueDisplay from 'src/components/ui/VnStockValueDisplay.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import axios from 'axios'; +import notifyResults from 'src/utils/notifyResults'; +import FetchData from 'components/FetchData.vue'; + +const MATCH = 'match'; + +const { t } = useI18n(); +const $props = defineProps({ + itemLack: { + type: Object, + required: true, + default: () => {}, + }, + replaceAction: { + type: Boolean, + required: false, + default: false, + }, + sales: { + type: Array, + required: false, + default: () => [], + }, +}); +const proposalSelected = ref([]); +const ticketConfig = ref({}); +const proposalTableRef = ref(null); + +const sale = computed(() => $props.sales[0]); +const saleFk = computed(() => sale.value.saleFk); +const filter = computed(() => ({ + itemFk: $props.itemLack.itemFk, + sales: saleFk.value, +})); + +const defaultColumnAttrs = { + align: 'center', + sortable: false, +}; +const emit = defineEmits(['onDialogClosed', 'itemReplaced']); + +const conditionalValuePrice = (price) => + price > 1 + ticketConfig.value.lackAlertPrice / 100 ? 'match' : 'not-match'; + +const columns = computed(() => [ + { + ...defaultColumnAttrs, + label: t('proposal.available'), + name: 'available', + field: 'available', + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + columnClass: 'shrink', + }, + { + ...defaultColumnAttrs, + label: t('proposal.counter'), + name: 'counter', + field: 'counter', + columnClass: 'shrink', + style: 'max-width: 75px', + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + }, + + { + align: 'left', + sortable: true, + label: t('proposal.longName'), + name: 'longName', + field: 'longName', + columnClass: 'expand', + }, + { + align: 'left', + sortable: true, + label: t('item.list.color'), + name: 'tag5', + field: 'value5', + columnClass: 'expand', + }, + { + align: 'left', + sortable: true, + label: t('item.list.stems'), + name: 'tag6', + field: 'value6', + columnClass: 'expand', + }, + { + align: 'left', + sortable: true, + label: t('item.list.producer'), + name: 'tag7', + field: 'value7', + columnClass: 'expand', + }, + + { + ...defaultColumnAttrs, + label: t('proposal.price2'), + name: 'price2', + style: 'max-width: 75px', + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + }, + { + ...defaultColumnAttrs, + label: t('proposal.minQuantity'), + name: 'minQuantity', + field: 'minQuantity', + style: 'max-width: 75px', + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + }, + { + ...defaultColumnAttrs, + label: t('proposal.located'), + name: 'located', + field: 'located', + }, + { + align: 'right', + label: '', + name: 'tableActions', + actions: [ + { + title: t('Replace'), + icon: 'change_circle', + show: (row) => isSelectionAvailable(row), + action: change, + isPrimary: true, + }, + ], + }, +]); + +function extractMatchValues(obj) { + return Object.keys(obj) + .filter((key) => key.startsWith(MATCH)) + .map((key) => parseInt(key.replace(MATCH, ''), 10)); +} +const gradientStyle = (value) => { + let color = 'white'; + const perc = parseFloat(value); + switch (true) { + case perc >= 0 && perc < 33: + color = 'primary'; + break; + case perc >= 33 && perc < 66: + color = 'warning'; + break; + + default: + color = 'secondary'; + break; + } + return color; +}; +const statusConditionalValue = (row) => { + const matches = extractMatchValues(row); + const value = matches.reduce((acc, i) => acc + row[`${MATCH}${i}`], 0); + return 100 * (value / matches.length); +}; + +const isSelectionAvailable = (itemProposal) => { + const { price2 } = itemProposal; + const salePrice = sale.value.price; + const byPrice = (100 * price2) / salePrice > ticketConfig.value.lackAlertPrice; + if (byPrice) { + return byPrice; + } + const byQuantity = + (100 * itemProposal.available) / Math.abs($props.itemLack.lack) < + ticketConfig.value.lackAlertPrice; + return byQuantity; +}; + +async function change({ itemFk: substitutionFk }) { + try { + const promises = $props.sales.map(({ saleFk, quantity }) => { + const params = { + saleFk, + substitutionFk, + quantity, + }; + return axios.post('Sales/replaceItem', params); + }); + const results = await Promise.allSettled(promises); + + notifyResults(results, 'saleFk'); + emit('itemReplaced', { + type: 'refresh', + quantity: quantity.value, + itemProposal: proposalSelected.value[0], + }); + proposalSelected.value = []; + } catch (error) { + console.error(error); + } +} + +async function handleTicketConfig(data) { + ticketConfig.value = data[0]; +} +</script> +<template> + <FetchData + url="TicketConfigs" + :filter="{ fields: ['lackAlertPrice'] }" + @on-fetch="handleTicketConfig" + auto-load + /> + + <VnTable + v-if="ticketConfig" + auto-load + data-cy="proposalTable" + ref="proposalTableRef" + data-key="ItemsGetSimilar" + url="Items/getSimilar" + :user-filter="filter" + :columns="columns" + class="full-width q-mt-md" + row-key="id" + :row-click="change" + :is-editable="false" + :right-search="false" + :without-header="true" + :disable-option="{ card: true, table: true }" + > + <template #column-longName="{ row }"> + <QTd + class="flex" + style="max-width: 100%; flex-shrink: 50px; flex-wrap: nowrap" + > + <div + class="middle full-width" + :class="[`proposal-${gradientStyle(statusConditionalValue(row))}`]" + > + <QTooltip> {{ statusConditionalValue(row) }}% </QTooltip> + </div> + <div style="flex: 2 0 100%; align-content: center"> + <div> + <span class="link">{{ row.longName }}</span> + <ItemDescriptorProxy :id="row.id" /> + </div> + </div> + </QTd> + </template> + <template #column-tag5="{ row }"> + <span :class="{ match: !row.match5 }">{{ row.value5 }}</span> + </template> + <template #column-tag6="{ row }"> + <span :class="{ match: !row.match6 }">{{ row.value6 }}</span> + </template> + <template #column-tag7="{ row }"> + <span :class="{ match: !row.match7 }">{{ row.value7 }}</span> + </template> + <template #column-counter="{ row }"> + <span + :class="{ + match: row.counter === 1, + 'not-match': row.counter !== 1, + }" + >{{ row.counter }}</span + > + </template> + <template #column-minQuantity="{ row }"> + {{ row.minQuantity }} + </template> + <template #column-price2="{ row }"> + <div class="flex column items-center content-center"> + <VnStockValueDisplay :value="(sales[0].price - row.price2) / 100" /> + <span :class="[conditionalValuePrice(row.price2)]">{{ + toCurrency(row.price2) + }}</span> + </div> + </template> + </VnTable> +</template> +<style lang="scss" scoped> +@import 'src/css/quasar.variables.scss'; +.middle { + float: left; + margin-right: 2px; + flex: 2 0 5px; +} +.match { + color: $negative; +} +.not-match { + color: inherit; +} +.proposal-warning { + background-color: $warning; +} +.proposal-secondary { + background-color: $secondary; +} +.proposal-primary { + background-color: $primary; +} +.text { + margin: 0.05rem; + padding: 1px; + border: 1px solid var(--vn-label-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: smaller; +} +</style> diff --git a/src/pages/Item/components/ItemProposalProxy.vue b/src/pages/Item/components/ItemProposalProxy.vue new file mode 100644 index 000000000..7da0ce398 --- /dev/null +++ b/src/pages/Item/components/ItemProposalProxy.vue @@ -0,0 +1,56 @@ +<script setup> +import ItemProposal from './ItemProposal.vue'; +import { useDialogPluginComponent } from 'quasar'; + +const $props = defineProps({ + itemLack: { + type: Object, + required: true, + default: () => {}, + }, + replaceAction: { + type: Boolean, + required: false, + default: false, + }, + sales: { + type: Array, + required: false, + default: () => [], + }, +}); +const { dialogRef } = useDialogPluginComponent(); +const emit = defineEmits([ + 'onDialogClosed', + 'itemReplaced', + ...useDialogPluginComponent.emits, +]); +defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.hide() }); +</script> +<template> + <QDialog ref="dialogRef" transition-show="scale" transition-hide="scale"> + <QCard class="dialog-width"> + <QCardSection class="row items-center q-pb-none"> + <span class="text-h6 text-grey">{{ $t('Item proposal') }}</span> + <QSpace /> + <QBtn icon="close" flat round dense v-close-popup /> + </QCardSection> + <QCardSection> + <ItemProposal + v-bind="$props" + @item-replaced=" + (data) => { + emit('itemReplaced', data); + dialogRef.hide(); + } + " + ></ItemProposal + ></QCardSection> + </QCard> + </QDialog> +</template> +<style lang="scss" scoped> +.dialog-width { + max-width: $width-lg; +} +</style> diff --git a/src/pages/Item/locale/en.yml b/src/pages/Item/locale/en.yml index bc73abb12..9d27fc96e 100644 --- a/src/pages/Item/locale/en.yml +++ b/src/pages/Item/locale/en.yml @@ -112,6 +112,7 @@ item: available: Available warehouseText: 'Calculated on the warehouse of { warehouseName }' itemDiary: Item diary + itemLastEntries: Last entries producer: Producer clone: title: All its properties will be copied @@ -130,6 +131,7 @@ item: origin: Orig. userName: Buyer weight: Weight + color: Color weightByPiece: Weight/stem stemMultiplier: Multiplier producer: Producer @@ -215,4 +217,24 @@ item: specie: Specie search: 'Search item' searchInfo: 'You can search by id' - regularizeStock: Regularize stock \ No newline at end of file + regularizeStock: Regularize stock +itemProposal: Items proposal +proposal: + difference: Difference + title: Items proposal + itemFk: Item + longName: Name + subName: Producer + value5: value5 + value6: value6 + value7: value7 + value8: value8 + available: Available + minQuantity: minQuantity + price2: Price + located: Located + counter: Counter + groupingPrice: Grouping Price + itemOldPrice: itemOld Price + status: State + quantityToReplace: Quanity to replace diff --git a/src/pages/Item/locale/es.yml b/src/pages/Item/locale/es.yml index dd5074f5f..935f5160b 100644 --- a/src/pages/Item/locale/es.yml +++ b/src/pages/Item/locale/es.yml @@ -118,6 +118,7 @@ item: available: Disponible warehouseText: 'Calculado sobre el almacén de { warehouseName }' itemDiary: Registro de compra-venta + itemLastEntries: Últimas entradas producer: Productor clone: title: Todas sus propiedades serán copiadas @@ -135,6 +136,7 @@ item: size: Medida origin: Orig. weight: Peso + color: Color weightByPiece: Peso/tallo userName: Comprador stemMultiplier: Multiplicador @@ -220,5 +222,30 @@ item: achieved: 'Conseguido' concept: 'Concepto' state: 'Estado' - search: 'Buscar artículo' - searchInfo: 'Puedes buscar por id' +itemProposal: Artículos similares +proposal: + substitutionAvailable: Sustitución disponible + notSubstitutionAvailableByPrice: Sustitución no disponible, 30% de diferencia por precio o cantidad + compatibility: Compatibilidad + title: Items de sustitución para los tickets seleccionados + itemFk: Item + longName: Nombre + subName: Productor + value5: value5 + value6: value6 + value7: value7 + value8: value8 + available: Disponible + minQuantity: Min. cantidad + price2: Precio + located: Ubicado + counter: Contador + difference: Diferencial + groupingPrice: Precio Grouping + itemOldPrice: Precio itemOld + status: Estado + quantityToReplace: Cantidad a reemplazar + replace: Sustituir + replaceAndConfirm: Sustituir y confirmar precio +search: 'Buscar artículo' +searchInfo: 'Puedes buscar por id' diff --git a/src/pages/Monitor/MonitorOrders.vue b/src/pages/Monitor/MonitorOrders.vue index 4efab56fb..873f8abb4 100644 --- a/src/pages/Monitor/MonitorOrders.vue +++ b/src/pages/Monitor/MonitorOrders.vue @@ -157,7 +157,7 @@ const openTab = (id) => openConfirmationModal( $t('globals.deleteConfirmTitle'), $t('salesOrdersTable.deleteConfirmMessage'), - removeOrders + removeOrders, ) " > diff --git a/src/pages/Monitor/locale/en.yml b/src/pages/Monitor/locale/en.yml index 21324087c..496c8761a 100644 --- a/src/pages/Monitor/locale/en.yml +++ b/src/pages/Monitor/locale/en.yml @@ -38,6 +38,7 @@ salesTicketsTable: payMethod: Pay method department: Department packing: ITP + hasItemLost: Item lost searchBar: label: Search tickets info: Search tickets by id or alias diff --git a/src/pages/Monitor/locale/es.yml b/src/pages/Monitor/locale/es.yml index 30afb1904..f6a29879f 100644 --- a/src/pages/Monitor/locale/es.yml +++ b/src/pages/Monitor/locale/es.yml @@ -39,6 +39,7 @@ salesTicketsTable: payMethod: Método de pago department: Departamento packing: ITP + hasItemLost: Artículo perdido searchBar: label: Buscar tickets info: Buscar tickets por identificador o alias diff --git a/src/pages/Order/Card/CatalogFilterValueDialog.vue b/src/pages/Order/Card/CatalogFilterValueDialog.vue index b91e7d229..d1bd48c9e 100644 --- a/src/pages/Order/Card/CatalogFilterValueDialog.vue +++ b/src/pages/Order/Card/CatalogFilterValueDialog.vue @@ -110,7 +110,7 @@ const getSelectedTagValues = async (tag) => { </div> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat class="filter-icon q-mb-md" size="md" diff --git a/src/pages/Order/Card/OrderBasicData.vue b/src/pages/Order/Card/OrderBasicData.vue index 8594a05f4..9c02d7494 100644 --- a/src/pages/Order/Card/OrderBasicData.vue +++ b/src/pages/Order/Card/OrderBasicData.vue @@ -14,7 +14,6 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; const { t } = useI18n(); const route = useRoute(); const state = useState(); -const ORDER_MODEL = 'order'; const isNew = Boolean(!route.params.id); const clientList = ref([]); @@ -32,7 +31,7 @@ const fetchAddressList = async (addressId) => { }); addressList.value = data; if (addressList.value?.length === 1) { - state.get(ORDER_MODEL).addressFk = addressList.value[0].id; + state.get('Order').addressFk = addressList.value[0].id; } }; @@ -91,9 +90,8 @@ const onClientChange = async (clientId) => { <VnSubToolbar v-if="isNew" /> <div class="q-pa-md"> <FormModel - :url="`Orders/${route.params.id}`" :url-update="`Orders/${route.params.id}/updateBasicData`" - :model="ORDER_MODEL" + model="Order" :filter="orderFilter" @on-fetch="fetchOrderDetails" auto-load diff --git a/src/pages/Order/Card/OrderCard.vue b/src/pages/Order/Card/OrderCard.vue index 823815f59..ad5c73a87 100644 --- a/src/pages/Order/Card/OrderCard.vue +++ b/src/pages/Order/Card/OrderCard.vue @@ -1,12 +1,14 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import OrderDescriptor from 'pages/Order/Card/OrderDescriptor.vue'; +import filter from './OrderFilter.js'; </script> <template> <VnCardBeta data-key="Order" - base-url="Orders" + url="Orders" + :filter="filter" :descriptor="OrderDescriptor" /> </template> diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index 262f503fd..76e608983 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -184,7 +184,7 @@ function addOrder(value, field, params) { {{ t( categoryList.find((c) => c.id == customTag.value)?.name || - '' + '', ) }} </strong> @@ -296,7 +296,7 @@ function addOrder(value, field, params) { <template #append> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat color="primary" size="md" diff --git a/src/pages/Order/Card/OrderCatalogItemDialog.vue b/src/pages/Order/Card/OrderCatalogItemDialog.vue index 77f6a8405..766945e4d 100644 --- a/src/pages/Order/Card/OrderCatalogItemDialog.vue +++ b/src/pages/Order/Card/OrderCatalogItemDialog.vue @@ -20,7 +20,7 @@ const props = defineProps({ }); const state = useState(); -const orderData = computed(() => state.get('orderData')); +const orderData = computed(() => state.get('Order')); const prices = ref((props.item.prices || []).map((item) => ({ ...item, quantity: 0 }))); const isLoading = ref(false); @@ -39,11 +39,11 @@ const addToOrder = async () => { }); const { data: orderTotal } = await axios.get( - `Orders/${Number(route.params.id)}/getTotal` + `Orders/${Number(route.params.id)}/getTotal`, ); state.set('orderTotal', orderTotal); - state.set('orderData', { + state.set('Order', { ...orderData.value, items, }); @@ -56,7 +56,7 @@ const canAddToOrder = () => { if (canAddToOrder) { const excedQuantity = prices.value.reduce( (acc, { quantity }) => acc + quantity, - 0 + 0, ); if (excedQuantity > props.item.available) { canAddToOrder = false; diff --git a/src/pages/Order/Card/OrderDescriptor.vue b/src/pages/Order/Card/OrderDescriptor.vue index 0d5f0146f..0d18864dc 100644 --- a/src/pages/Order/Card/OrderDescriptor.vue +++ b/src/pages/Order/Card/OrderDescriptor.vue @@ -4,8 +4,7 @@ import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { toCurrency, toDate } from 'src/filters'; import { useState } from 'src/composables/useState'; -import useCardDescription from 'src/composables/useCardDescription'; - +import filter from './OrderFilter.js'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import FetchData from 'components/FetchData.vue'; @@ -24,44 +23,15 @@ const $props = defineProps({ const route = useRoute(); const state = useState(); const { t } = useI18n(); -const data = ref(useCardDescription()); const getTotalRef = ref(); const entityId = computed(() => { return $props.id || route.params.id; }); -const filter = { - include: [ - { relation: 'agencyMode', scope: { fields: ['name'] } }, - { - relation: 'address', - scope: { fields: ['nickname'] }, - }, - { relation: 'rows', scope: { fields: ['id'] } }, - { - relation: 'client', - scope: { - fields: [ - 'salesPersonFk', - 'name', - 'isActive', - 'isFreezed', - 'isTaxDataChecked', - ], - include: { - relation: 'salesPersonUser', - scope: { fields: ['id', 'name'] }, - }, - }, - }, - ], -}; - const setData = (entity) => { if (!entity) return; getTotalRef.value && getTotalRef.value.fetch(); - data.value = useCardDescription(entity?.client?.name, entity?.id); state.set('orderTotal', total); }; @@ -87,11 +57,9 @@ const total = ref(0); ref="descriptor" :url="`Orders/${entityId}`" :filter="filter" - module="Order" - :title="data.title" - :subtitle="data.subtitle" + title="client.name" @on-fetch="setData" - data-key="orderData" + data-key="Order" > <template #body="{ entity }"> <VnLv diff --git a/src/pages/Order/Card/OrderFilter.js b/src/pages/Order/Card/OrderFilter.js new file mode 100644 index 000000000..3e521b92c --- /dev/null +++ b/src/pages/Order/Card/OrderFilter.js @@ -0,0 +1,26 @@ +export default { + include: [ + { relation: 'agencyMode', scope: { fields: ['name'] } }, + { + relation: 'address', + scope: { fields: ['nickname'] }, + }, + { relation: 'rows', scope: { fields: ['id'] } }, + { + relation: 'client', + scope: { + fields: [ + 'salesPersonFk', + 'name', + 'isActive', + 'isFreezed', + 'isTaxDataChecked', + ], + include: { + relation: 'salesPersonUser', + scope: { fields: ['id', 'name'] }, + }, + }, + }, + ], +}; diff --git a/src/pages/Order/Card/OrderLines.vue b/src/pages/Order/Card/OrderLines.vue index cf219a244..1b864de6f 100644 --- a/src/pages/Order/Card/OrderLines.vue +++ b/src/pages/Order/Card/OrderLines.vue @@ -21,7 +21,7 @@ const router = useRouter(); const route = useRoute(); const { t } = useI18n(); const quasar = useQuasar(); -const descriptorData = useArrayData('orderData'); +const descriptorData = useArrayData('Order'); const componentKey = ref(0); const tableLinesRef = ref(); const order = ref(); @@ -238,7 +238,7 @@ watch( lineFilter.value.where.orderFk = router.currentRoute.value.params.id; tableLinesRef.value.reload(); - } + }, ); </script> diff --git a/src/pages/Order/Card/OrderSummary.vue b/src/pages/Order/Card/OrderSummary.vue index a289688e4..a4bdb2881 100644 --- a/src/pages/Order/Card/OrderSummary.vue +++ b/src/pages/Order/Card/OrderSummary.vue @@ -27,7 +27,7 @@ const $props = defineProps({ const entityId = computed(() => $props.id || route.params.id); const summary = ref(); const quasar = useQuasar(); -const descriptorData = useArrayData('orderData'); +const descriptorData = useArrayData('Order'); const detailsColumns = ref([ { name: 'item', diff --git a/src/pages/Order/OrderList.vue b/src/pages/Order/OrderList.vue index 21cb5ed7e..40990f329 100644 --- a/src/pages/Order/OrderList.vue +++ b/src/pages/Order/OrderList.vue @@ -71,8 +71,9 @@ const columns = computed(() => [ format: (row) => row?.name, }, { - align: 'left', + align: 'center', name: 'isConfirmed', + component: 'checkbox', label: t('module.isConfirmed'), }, { @@ -95,7 +96,9 @@ const columns = computed(() => [ columnField: { component: null, }, - style: 'color="positive"', + style: () => { + return { color: 'positive' }; + }, }, { align: 'left', diff --git a/src/pages/Route/Agency/AgencyList.vue b/src/pages/Route/Agency/AgencyList.vue index 4322b9bc8..5c2904bf3 100644 --- a/src/pages/Route/Agency/AgencyList.vue +++ b/src/pages/Route/Agency/AgencyList.vue @@ -51,7 +51,6 @@ const columns = computed(() => [ name: 'isAnyVolumeAllowed', component: 'checkbox', cardVisible: true, - disable: true, }, { align: 'right', @@ -72,7 +71,7 @@ const columns = computed(() => [ :data-key :columns="columns" prefix="agency" - :right-filter="false" + :right-filter="true" :array-data-props="{ url: 'Agencies', order: 'name', @@ -83,6 +82,7 @@ const columns = computed(() => [ <VnTable :data-key :columns="columns" + is-editable="false" :right-search="false" :use-model="true" redirect="route/agency" diff --git a/src/pages/Route/Agency/Card/AgencyBasicData.vue b/src/pages/Route/Agency/Card/AgencyBasicData.vue index 599058b3e..4270b136c 100644 --- a/src/pages/Route/Agency/Card/AgencyBasicData.vue +++ b/src/pages/Route/Agency/Card/AgencyBasicData.vue @@ -21,7 +21,7 @@ const warehouses = ref([]); @on-fetch="(data) => (warehouses = data)" auto-load /> - <FormModel :url="`Agencies/${routeId}`" model="agency" auto-load> + <FormModel :update-url="`Agencies/${routeId}`" model="Agency" auto-load> <template #form="{ data }"> <VnRow> <VnInput v-model="data.name" :label="t('globals.name')" /> diff --git a/src/pages/Route/Agency/Card/AgencyCard.vue b/src/pages/Route/Agency/Card/AgencyCard.vue index 35685790a..7dc31f8ba 100644 --- a/src/pages/Route/Agency/Card/AgencyCard.vue +++ b/src/pages/Route/Agency/Card/AgencyCard.vue @@ -3,5 +3,5 @@ import AgencyDescriptor from 'pages/Route/Agency/Card/AgencyDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; </script> <template> - <VnCardBeta data-key="Agency" base-url="Agencies" :descriptor="AgencyDescriptor" /> + <VnCardBeta data-key="Agency" url="Agencies" :descriptor="AgencyDescriptor" /> </template> diff --git a/src/pages/Route/Agency/Card/AgencyDescriptor.vue b/src/pages/Route/Agency/Card/AgencyDescriptor.vue index b9772037c..a0472c6c3 100644 --- a/src/pages/Route/Agency/Card/AgencyDescriptor.vue +++ b/src/pages/Route/Agency/Card/AgencyDescriptor.vue @@ -22,7 +22,6 @@ const card = computed(() => store.data); </script> <template> <CardDescriptor - module="Agency" data-key="Agency" :url="`Agencies/${entityId}`" :title="card?.name" diff --git a/src/pages/Route/Agency/Card/AgencyWorkcenter.vue b/src/pages/Route/Agency/Card/AgencyWorkcenter.vue index 7cabf396d..9a9213868 100644 --- a/src/pages/Route/Agency/Card/AgencyWorkcenter.vue +++ b/src/pages/Route/Agency/Card/AgencyWorkcenter.vue @@ -88,7 +88,7 @@ async function deleteWorCenter(id) { </VnPaginate> </div> <QPageSticky :offset="[18, 18]"> - <QBtn @click.stop="dialog.show()" color="primary" fab shortcut="+" icon="add"> + <QBtn @click.stop="dialog.show()" color="primary" fab v-shortcut="'+'" icon="add"> <QDialog ref="dialog"> <FormModelPopup :title="t('Add work center')" diff --git a/src/pages/Route/Card/RouteCard.vue b/src/pages/Route/Card/RouteCard.vue index 81b6cfa16..c178dc6bf 100644 --- a/src/pages/Route/Card/RouteCard.vue +++ b/src/pages/Route/Card/RouteCard.vue @@ -1,12 +1,13 @@ <script setup> import RouteDescriptor from 'pages/Route/Card/RouteDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; +import filter from './RouteFilter.js'; </script> <template> <VnCardBeta data-key="Route" - base-url="Routes" - custom-url="Routes/filter" + url="Routes" + :filter="filter" :descriptor="RouteDescriptor" /> </template> diff --git a/src/pages/Route/Card/RouteDescriptor.vue b/src/pages/Route/Card/RouteDescriptor.vue index 68c08b821..503cd1941 100644 --- a/src/pages/Route/Card/RouteDescriptor.vue +++ b/src/pages/Route/Card/RouteDescriptor.vue @@ -1,13 +1,14 @@ <script setup> import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; -import useCardDescription from 'composables/useCardDescription'; import { dashIfEmpty, toDate } from 'src/filters'; import RouteDescriptorMenu from 'pages/Route/Card/RouteDescriptorMenu.vue'; +import filter from './RouteFilter.js'; +import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; + const $props = defineProps({ id: { type: Number, @@ -17,7 +18,6 @@ const $props = defineProps({ }); const route = useRoute(); -const { t } = useI18n(); const zone = ref(); const zoneId = ref(); const entityId = computed(() => { @@ -36,81 +36,31 @@ const getZone = async () => { const { data: zoneData } = await axios.get(`Zones/${zoneId.value}`); zone.value = zoneData.name; }; - -const filter = { - fields: [ - 'id', - 'workerFk', - 'agencyModeFk', - 'dated', - 'm3', - 'warehouseFk', - 'description', - 'vehicleFk', - 'kmStart', - 'kmEnd', - 'started', - 'finished', - 'cost', - 'isOk', - ], - include: [ - { relation: 'agencyMode', scope: { fields: ['id', 'name'] } }, - { - relation: 'vehicle', - scope: { fields: ['id', 'm3'] }, - }, - { - relation: 'ticket', - scope: { - fields: ['id', 'name', 'zoneFk'], - include: { relation: 'zone', scope: { fields: ['id', 'name'] } }, - }, - }, - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { - fields: ['id'], - include: { relation: 'emailUser', scope: { fields: ['email'] } }, - }, - }, - }, - }, - ], -}; const data = ref(useCardDescription()); const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); onMounted(async () => { getZone(); }); </script> - <template> <CardDescriptor - module="Route" :url="`Routes/${entityId}`" :filter="filter" - :title="data.title" - :subtitle="data.subtitle" - data-key="routeData" - @on-fetch="setData" + :title="null" + data-key="Route" width="lg-width" > <template #body="{ entity }"> - <VnLv :label="t('Date')" :value="toDate(entity?.dated)" /> - <VnLv :label="t('Agency')" :value="entity?.agencyMode?.name" /> - <VnLv :label="t('Zone')" :value="zone" /> + <VnLv :label="$t('Date')" :value="toDate(entity?.dated)" /> + <VnLv :label="$t('Agency')" :value="entity?.agencyMode?.name" /> + <VnLv :label="$t('Zone')" :value="zone" /> <VnLv - :label="t('Volume')" + :label="$t('Volume')" :value="`${dashIfEmpty(entity?.m3)} / ${dashIfEmpty( entity?.vehicle?.m3, )} m³`" /> - <VnLv :label="t('Description')" :value="entity?.description" /> + <VnLv :label="$t('Description')" :value="entity?.description" /> </template> <template #menu="{ entity }"> <RouteDescriptorMenu :route="entity" /> diff --git a/src/pages/Route/Card/RouteFilter.js b/src/pages/Route/Card/RouteFilter.js new file mode 100644 index 000000000..90ee71bf7 --- /dev/null +++ b/src/pages/Route/Card/RouteFilter.js @@ -0,0 +1,39 @@ +export default { + fields: [ + 'code', + 'id', + 'workerFk', + 'agencyModeFk', + 'created', + 'm3', + 'warehouseFk', + 'description', + 'vehicleFk', + 'kmStart', + 'kmEnd', + 'started', + 'finished', + 'cost', + 'isOk', + ], + include: [ + { relation: 'agencyMode', scope: { fields: ['id', 'name'] } }, + { + relation: 'vehicle', + scope: { fields: ['id', 'm3'] }, + }, + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { + fields: ['id'], + include: { relation: 'emailUser', scope: { fields: ['email'] } }, + }, + }, + }, + }, + ], +}; diff --git a/src/pages/Route/Card/RouteFilter.vue b/src/pages/Route/Card/RouteFilter.vue index 72bfed1da..21858102b 100644 --- a/src/pages/Route/Card/RouteFilter.vue +++ b/src/pages/Route/Card/RouteFilter.vue @@ -100,7 +100,7 @@ const emit = defineEmits(['search']); <VnSelect :label="t('Vehicle')" v-model="params.vehicleFk" - url="Vehicles" + url="Vehicles/active" sort-by="numberPlate ASC" option-value="id" option-label="numberPlate" diff --git a/src/pages/Route/Card/RouteForm.vue b/src/pages/Route/Card/RouteForm.vue index 633ff44bc..667204b15 100644 --- a/src/pages/Route/Card/RouteForm.vue +++ b/src/pages/Route/Card/RouteForm.vue @@ -11,6 +11,7 @@ import VnInputDate from 'components/common/VnInputDate.vue'; import VnInput from 'components/common/VnInput.vue'; import axios from 'axios'; import VnInputTime from 'components/common/VnInputTime.vue'; +import filter from './RouteFilter.js'; import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); @@ -27,52 +28,6 @@ const defaultInitialData = { isOk: false, }; const maxDistance = ref(); - -const routeFilter = { - fields: [ - 'id', - 'workerFk', - 'agencyModeFk', - 'dated', - 'm3', - 'warehouseFk', - 'description', - 'vehicleFk', - 'kmStart', - 'kmEnd', - 'started', - 'finished', - 'cost', - 'isOk', - ], - include: [ - { relation: 'agencyMode', scope: { fields: ['id', 'name'] } }, - { - relation: 'vehicle', - scope: { fields: ['id', 'm3'] }, - }, - { - relation: 'ticket', - scope: { - fields: ['id', 'name', 'zoneFk'], - include: { relation: 'zone', scope: { fields: ['id', 'name'] } }, - }, - }, - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { - fields: ['id'], - include: { relation: 'emailUser', scope: { fields: ['email'] } }, - }, - }, - }, - }, - ], -}; const onSave = (data, response) => { if (isNew) { axios.post(`Routes/${response?.id}/updateWorkCenter`); @@ -89,11 +44,10 @@ const onSave = (data, response) => { sort-by="id ASC" /> <FormModel - :url="isNew ? null : `Routes/${route.params?.id}`" :url-create="isNew ? 'Routes' : null" :observe-form-changes="!isNew" - :filter="routeFilter" - model="route" + :filter="filter" + model="Route" :auto-load="!isNew" :form-initial-data="isNew ? defaultInitialData : null" @on-data-saved="onSave" @@ -104,7 +58,7 @@ const onSave = (data, response) => { <VnSelect :label="t('Vehicle')" v-model="data.vehicleFk" - url="Vehicles" + url="Vehicles/active" sort-by="numberPlate ASC" option-value="id" option-label="numberPlate" diff --git a/src/pages/Route/Roadmap/RoadmapBasicData.vue b/src/pages/Route/Roadmap/RoadmapBasicData.vue index 2fe805362..a9e6059c3 100644 --- a/src/pages/Route/Roadmap/RoadmapBasicData.vue +++ b/src/pages/Route/Roadmap/RoadmapBasicData.vue @@ -11,17 +11,16 @@ import VnSelectSupplier from 'src/components/common/VnSelectSupplier.vue'; const { t } = useI18n(); const router = useRouter(); -const filter = { include: [{ relation: 'supplier' }] }; const onSave = (data, response) => { router.push({ name: 'RoadmapSummary', params: { id: response?.id } }); }; </script> <template> <FormModel + :update-url="`Roadmaps/${$route.params?.id}`" :url="`Roadmaps/${$route.params?.id}`" observe-form-changes - :filter="filter" - model="roadmap" + model="Roadmap" auto-load @on-data-saved="onSave" > diff --git a/src/pages/Route/Roadmap/RoadmapCard.vue b/src/pages/Route/Roadmap/RoadmapCard.vue index 0b81de673..48ba516a1 100644 --- a/src/pages/Route/Roadmap/RoadmapCard.vue +++ b/src/pages/Route/Roadmap/RoadmapCard.vue @@ -3,5 +3,5 @@ import VnCardBeta from 'components/common/VnCardBeta.vue'; import RoadmapDescriptor from 'pages/Route/Roadmap/RoadmapDescriptor.vue'; </script> <template> - <VnCardBeta data-key="Roadmap" base-url="Roadmaps" :descriptor="RoadmapDescriptor" /> + <VnCardBeta data-key="Roadmap" url="Roadmaps" :descriptor="RoadmapDescriptor" /> </template> diff --git a/src/pages/Route/Roadmap/RoadmapDescriptor.vue b/src/pages/Route/Roadmap/RoadmapDescriptor.vue index 788173688..baa864a15 100644 --- a/src/pages/Route/Roadmap/RoadmapDescriptor.vue +++ b/src/pages/Route/Roadmap/RoadmapDescriptor.vue @@ -1,13 +1,13 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; -import useCardDescription from 'composables/useCardDescription'; import { dashIfEmpty, toDateHourMin } from 'src/filters'; import SupplierDescriptorProxy from 'pages/Supplier/Card/SupplierDescriptorProxy.vue'; import RoadmapDescriptorMenu from 'pages/Route/Roadmap/RoadmapDescriptorMenu.vue'; +import filter from 'pages/Route/Roadmap/RoadmapFilter.js'; const $props = defineProps({ id: { @@ -23,22 +23,10 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); - -const filter = { include: [{ relation: 'supplier' }] }; -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); </script> <template> - <CardDescriptor - module="Roadmap" - :url="`Roadmaps/${entityId}`" - :filter="filter" - :title="data.title" - :subtitle="data.subtitle" - data-key="Roadmap" - @on-fetch="setData" - > + <CardDescriptor :url="`Roadmaps/${entityId}`" :filter="filter" data-key="Roadmap"> <template #body="{ entity }"> <VnLv :label="t('Roadmap')" :value="entity?.name" /> <VnLv :label="t('ETD')" :value="toDateHourMin(entity?.etd)" /> diff --git a/src/pages/Route/Roadmap/RoadmapFilter.js b/src/pages/Route/Roadmap/RoadmapFilter.js new file mode 100644 index 000000000..0ae890363 --- /dev/null +++ b/src/pages/Route/Roadmap/RoadmapFilter.js @@ -0,0 +1,3 @@ +export default { + include: [{ relation: 'supplier' }], +}; diff --git a/src/pages/Route/Roadmap/RoadmapStops.vue b/src/pages/Route/Roadmap/RoadmapStops.vue index d8215ea49..e4085d572 100644 --- a/src/pages/Route/Roadmap/RoadmapStops.vue +++ b/src/pages/Route/Roadmap/RoadmapStops.vue @@ -68,7 +68,7 @@ const updateDefaultStop = (data) => { <QBtn flat icon="add" - shortcut="+" + v-shortcut="'+'" class="cursor-pointer" color="primary" @click="roadmapStopsCrudRef.insert()" diff --git a/src/pages/Route/Roadmap/RoadmapSummary.vue b/src/pages/Route/Roadmap/RoadmapSummary.vue index 1fbb1897d..0c1c2b903 100644 --- a/src/pages/Route/Roadmap/RoadmapSummary.vue +++ b/src/pages/Route/Roadmap/RoadmapSummary.vue @@ -67,7 +67,6 @@ const filter = { }, }, ], - where: { id: entityId }, }; </script> @@ -76,7 +75,7 @@ const filter = { <CardSummary data-key="RoadmapSummary" ref="summary" - :url="`Roadmaps`" + :url="`Roadmaps/${entityId}`" :filter="filter" > <template #header-left> diff --git a/src/pages/Route/RouteExtendedList.vue b/src/pages/Route/RouteExtendedList.vue index 221fc4754..46bc1a690 100644 --- a/src/pages/Route/RouteExtendedList.vue +++ b/src/pages/Route/RouteExtendedList.vue @@ -3,7 +3,7 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useQuasar } from 'quasar'; -import { toDate } from 'src/filters'; +import { dashIfEmpty, toDate, toHour } from 'src/filters'; import { useRouter } from 'vue-router'; import { usePrintService } from 'src/composables/usePrintService'; @@ -38,7 +38,7 @@ const routeFilter = { }; const columns = computed(() => [ { - align: 'left', + align: 'center', name: 'id', label: 'Id', chip: { @@ -48,7 +48,7 @@ const columns = computed(() => [ columnFilter: false, }, { - align: 'left', + align: 'center', name: 'workerFk', label: t('route.Worker'), create: true, @@ -68,10 +68,10 @@ const columns = computed(() => [ }, useLike: false, cardVisible: true, - format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), + format: (row, dashIfEmpty) => dashIfEmpty(row.workerUserName), }, { - align: 'left', + align: 'center', name: 'agencyModeFk', label: t('route.Agency'), isTitle: true, @@ -87,17 +87,17 @@ const columns = computed(() => [ }, }, columnClass: 'expand', + format: (row, dashIfEmpty) => dashIfEmpty(row.agencyName), }, { - align: 'left', + align: 'center', name: 'vehicleFk', label: t('route.Vehicle'), cardVisible: true, create: true, component: 'select', attrs: { - url: 'vehicles', - fields: ['id', 'numberPlate'], + url: 'vehicles/active', optionLabel: 'numberPlate', optionFilterValue: 'numberPlate', find: { @@ -108,29 +108,31 @@ const columns = computed(() => [ columnFilter: { inWhere: true, }, + format: (row, dashIfEmpty) => dashIfEmpty(row.vehiclePlateNumber), }, { - align: 'left', + align: 'center', name: 'dated', label: t('route.Date'), columnFilter: false, cardVisible: true, create: true, component: 'date', - format: ({ date }) => toDate(date), + format: ({ dated }, dashIfEmpty) => + dated === '0000-00-00' ? dashIfEmpty(null) : toDate(dated), }, { - align: 'left', + align: 'center', name: 'from', label: t('route.From'), visible: false, cardVisible: true, create: true, component: 'date', - format: ({ date }) => toDate(date), + format: ({ from }) => toDate(from), }, { - align: 'left', + align: 'center', name: 'to', label: t('route.To'), visible: false, @@ -147,18 +149,20 @@ const columns = computed(() => [ columnClass: 'shrink', }, { - align: 'left', + align: 'center', name: 'started', label: t('route.hourStarted'), component: 'time', columnFilter: false, + format: ({ started }) => toHour(started), }, { - align: 'left', + align: 'center', name: 'finished', label: t('route.hourFinished'), component: 'time', columnFilter: false, + format: ({ finished }) => toHour(finished), }, { align: 'center', @@ -177,7 +181,7 @@ const columns = computed(() => [ visible: false, }, { - align: 'left', + align: 'center', name: 'description', label: t('route.Description'), isTitle: true, @@ -186,7 +190,7 @@ const columns = computed(() => [ field: 'description', }, { - align: 'left', + align: 'center', name: 'isOk', label: t('route.Served'), component: 'checkbox', @@ -300,60 +304,62 @@ const openTicketsDialog = (id) => { <RouteFilter data-key="RouteList" /> </template> </RightMenu> - <VnTable - class="route-list" - ref="tableRef" - data-key="RouteList" - url="Routes/filter" - :columns="columns" - :right-search="false" - :is-editable="true" - :filter="routeFilter" - redirect="route" - :row-click="false" - :create="{ - urlCreate: 'Routes', - title: t('route.createRoute'), - onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: {}, - }" - save-url="Routes/crud" - :disable-option="{ card: true }" - table-height="85vh" - v-model:selected="selectedRows" - :table="{ - 'row-key': 'id', - selection: 'multiple', - }" - > - <template #moreBeforeActions> - <QBtn - icon="vn:clone" - color="primary" - class="q-mr-sm" - :disable="!selectedRows?.length" - @click="confirmationDialog = true" - > - <QTooltip>{{ t('route.Clone Selected Routes') }}</QTooltip> - </QBtn> - <QBtn - icon="cloud_download" - color="primary" - class="q-mr-sm" - :disable="!selectedRows?.length" - @click="showRouteReport" - > - <QTooltip>{{ t('route.Download selected routes as PDF') }}</QTooltip> - </QBtn> - <QBtn - icon="check" - color="primary" - class="q-mr-sm" - :disable="!selectedRows?.length" - @click="markAsServed()" - > - <QTooltip>{{ t('route.Mark as served') }}</QTooltip> - </QBtn> - </template> - </VnTable> + <QPage class="q-px-md"> + <VnTable + class="route-list" + ref="tableRef" + data-key="RouteList" + url="Routes/filter" + :columns="columns" + :right-search="false" + :is-editable="true" + :filter="routeFilter" + redirect="route" + :row-click="false" + :create="{ + urlCreate: 'Routes', + title: t('route.createRoute'), + onDataSaved: ({ id }) => tableRef.redirect(id), + formInitialData: {}, + }" + save-url="Routes/crud" + :disable-option="{ card: true }" + table-height="85vh" + v-model:selected="selectedRows" + :table="{ + 'row-key': 'id', + selection: 'multiple', + }" + > + <template #moreBeforeActions> + <QBtn + icon="vn:clone" + color="primary" + class="q-mr-sm" + :disable="!selectedRows?.length" + @click="confirmationDialog = true" + > + <QTooltip>{{ t('route.Clone Selected Routes') }}</QTooltip> + </QBtn> + <QBtn + icon="cloud_download" + color="primary" + class="q-mr-sm" + :disable="!selectedRows?.length" + @click="showRouteReport" + > + <QTooltip>{{ t('route.Download selected routes as PDF') }}</QTooltip> + </QBtn> + <QBtn + icon="check" + color="primary" + class="q-mr-sm" + :disable="!selectedRows?.length" + @click="markAsServed()" + > + <QTooltip>{{ t('route.Mark as served') }}</QTooltip> + </QBtn> + </template> + </VnTable> + </QPage> </template> diff --git a/src/pages/Route/RouteList.vue b/src/pages/Route/RouteList.vue index bc3227f6c..9dad8ba22 100644 --- a/src/pages/Route/RouteList.vue +++ b/src/pages/Route/RouteList.vue @@ -38,6 +38,17 @@ const columns = computed(() => [ align: 'left', name: 'workerFk', label: t('route.Worker'), + component: 'select', + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + useLike: false, + optionFilter: 'firstName', + find: { + value: 'workerFk', + label: 'workerUserName', + }, + }, create: true, cardVisible: true, format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), @@ -48,6 +59,15 @@ const columns = computed(() => [ name: 'agencyName', label: t('route.Agency'), cardVisible: true, + component: 'select', + attrs: { + url: 'agencyModes', + fields: ['id', 'name'], + find: { + value: 'agencyModeFk', + label: 'agencyName', + }, + }, create: true, columnClass: 'expand', columnFilter: false, @@ -57,6 +77,17 @@ const columns = computed(() => [ name: 'vehiclePlateNumber', label: t('route.Vehicle'), cardVisible: true, + component: 'select', + attrs: { + url: 'vehicles', + fields: ['id', 'numberPlate'], + optionLabel: 'numberPlate', + optionFilterValue: 'numberPlate', + find: { + value: 'vehicleFk', + label: 'vehiclePlateNumber', + }, + }, create: true, columnFilter: false, }, diff --git a/src/pages/Route/RouteTickets.vue b/src/pages/Route/RouteTickets.vue index 1416f77ce..adc7dfdaa 100644 --- a/src/pages/Route/RouteTickets.vue +++ b/src/pages/Route/RouteTickets.vue @@ -120,8 +120,8 @@ const deletePriorities = async () => { try { await Promise.all( selectedRows.value.map((ticket) => - axios.patch(`Tickets/${ticket?.id}/`, { priority: null }) - ) + axios.patch(`Tickets/${ticket?.id}/`, { priority: null }), + ), ); } finally { refreshKey.value++; @@ -132,8 +132,8 @@ const setOrderedPriority = async () => { try { await Promise.all( ticketList.value.map((ticket, index) => - axios.patch(`Tickets/${ticket?.id}/`, { priority: index + 1 }) - ) + axios.patch(`Tickets/${ticket?.id}/`, { priority: index + 1 }), + ), ); } finally { refreshKey.value++; @@ -162,7 +162,7 @@ const setHighestPriority = async (ticket, ticketList) => { const goToBuscaman = async (ticket = null) => { await openBuscaman( routeEntity.value?.vehicleFk, - ticket ? [ticket] : selectedRows.value + ticket ? [ticket] : selectedRows.value, ); }; @@ -393,7 +393,13 @@ const openSmsDialog = async () => { </VnPaginate> </div> <QPageSticky :offset="[20, 20]"> - <QBtn fab icon="add" shortcut="+" color="primary" @click="openTicketsDialog"> + <QBtn + fab + icon="add" + v-shortcut="'+'" + color="primary" + @click="openTicketsDialog" + > <QTooltip> {{ t('Add ticket') }} </QTooltip> diff --git a/src/pages/Route/Vehicle/Card/VehicleBasicData.vue b/src/pages/Route/Vehicle/Card/VehicleBasicData.vue new file mode 100644 index 000000000..e78bc6edd --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleBasicData.vue @@ -0,0 +1,162 @@ +<script setup> +import { ref } from 'vue'; +import FormModel from 'components/FormModel.vue'; +import FetchData from 'src/components/FetchData.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; + +const warehouses = ref([]); +const companies = ref([]); +const countries = ref([]); +const fuelTypes = ref([]); +const bankPolicies = ref([]); +const deliveryPoints = ref([]); +</script> +<template> + <FetchData + url="Warehouses" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (warehouses = data)" + auto-load + /> + <FetchData + url="Companies" + :filter="{ fields: ['id', 'code'] }" + @on-fetch="(data) => (companies = data)" + auto-load + /> + <FetchData + url="Countries" + :filter="{ fields: ['code'] }" + @on-fetch="(data) => (countries = data)" + auto-load + /> + <FetchData + url="FuelTypes" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (fuelTypes = data)" + auto-load + /> + <FetchData + url="DeliveryPoints" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (deliveryPoints = data)" + auto-load + /> + <FormModel model="Vehicle" :url-update="`Vehicles/${$route.params.id}`"> + <template #form="{ data }"> + <VnRow> + <VnInput v-model="data.description" :label="$t('globals.description')" /> + <VnInput v-model="data.numberPlate" :label="$t('vehicle.numberPlate')" /> + </VnRow> + <VnRow> + <VnInput + v-model="data.model" + :label="$t('globals.model')" + :required="true" + /> + <VnSelect + url="VehicleTypes" + v-model="data.vehicleTypeFk" + :label="$t('globals.type')" + /> + </VnRow> + <VnRow> + <VnInput + v-model="data.tradeMark" + :label="$t('vehicle.tradeMark')" + :required="true" + /> + <VnInput v-model="data.chassis" :label="$t('vehicle.chassis')" /> + </VnRow> + <VnRow> + <VnSelect + v-model="data.fuelTypeFk" + :label="$t('globals.fuel')" + :options="fuelTypes" + /> + <VnSelect + v-model="data.deliveryPointFk" + :label="$t('globals.deliveryPoint')" + :options="deliveryPoints" + /> + </VnRow> + <VnRow> + <VnSelect + v-model="data.companyFk" + :label="$t('globals.company')" + :options="companies" + option-label="code" + /> + <VnSelect + v-model="data.warehouseFk" + :label="$t('globals.warehouse')" + :options="warehouses" + /> + </VnRow> + <VnRow> + <VnSelect + url="Suppliers" + :filter="{ fields: ['id', 'name'] }" + v-model="data.supplierFk" + :label="$t('globals.supplier')" + /> + <VnSelect + url="Suppliers" + :filter="{ fields: ['id', 'name'] }" + v-model="data.supplierCoolerFk" + :label="$t('vehicle.supplierCooler')" + /> + </VnRow> + <VnRow> + <VnSelect + url="BankPolicies" + :filter="{ fields: ['id', 'ref'] }" + v-model="data.bankPolicyFk" + :label="$t('vehicle.leasing')" + :options="bankPolicies" + option-label="ref" + option-value="id" + /> + <VnInput v-model="data.leasing" :label="$t('vehicle.nLeasing')" /> + </VnRow> + <VnRow> + <VnInputNumber v-model="data.import" :label="$t('globals.amount')" /> + <VnInputNumber + v-model="data.importCooler" + :label="$t('vehicle.amountCooler')" + /> + </VnRow> + <VnRow> + <VnSelect + url="Ppes" + option-label="id" + v-model="data.ppeFk" + :label="$t('vehicle.ppe')" + /> + <VnSelect + v-model="data.countryCodeFk" + :label="$t('globals.country')" + :options="countries" + option-label="code" + option-value="code" + /> + </VnRow> + <VnRow> + <VnInput v-model="data.vin" :label="$t('vehicle.vin')" /> + <span :style="{ 'align-self': $q.screen.gt.xs ? 'end' : 'unset' }"> + <QCheckbox + v-model="data.isActive" + :label="$t('vehicle.isActive')" + :false-value="0" + :true-value="1" + dense + class="q-mt-sm" + /> + </span> + </VnRow> + </template> + </FormModel> +</template> diff --git a/src/pages/Route/Vehicle/Card/VehicleCard.vue b/src/pages/Route/Vehicle/Card/VehicleCard.vue new file mode 100644 index 000000000..f59420aa2 --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleCard.vue @@ -0,0 +1,13 @@ +<script setup> +import VnCardBeta from 'components/common/VnCardBeta.vue'; +import VehicleDescriptor from './VehicleDescriptor.vue'; +import VehicleFilter from '../VehicleFilter.js'; +</script> +<template> + <VnCardBeta + data-key="Vehicle" + url="Vehicles" + :filter="VehicleFilter" + :descriptor="VehicleDescriptor" + /> +</template> diff --git a/src/pages/Route/Vehicle/Card/VehicleDescriptor.vue b/src/pages/Route/Vehicle/Card/VehicleDescriptor.vue new file mode 100644 index 000000000..d9a2434ab --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleDescriptor.vue @@ -0,0 +1,49 @@ +<script setup> +import VnLv from 'src/components/ui/VnLv.vue'; +import CardDescriptor from 'components/ui/CardDescriptor.vue'; +import axios from 'axios'; +import useNotify from 'src/composables/useNotify.js'; + +const { notify } = useNotify(); +</script> +<template> + <CardDescriptor + :url="`Vehicles/${$route.params.id}`" + data-key="Vehicle" + title="numberPlate" + :to-module="{ name: 'VehicleList' }" + > + <template #menu="{ entity }"> + <QItem + data-cy="delete" + v-ripple + clickable + @click=" + async () => { + try { + await axios.delete(`Vehicles/${entity.id}`); + notify('vehicle.remove', 'positive'); + $router.push({ name: 'VehicleList' }); + } catch (e) { + throw e; + } + } + " + > + <QItemSection> + {{ $t('vehicle.delete') }} + </QItemSection> + </QItem> + </template> + <template #body="{ entity }"> + <VnLv :label="$t('vehicle.numberPlate')" :value="entity.numberPlate" /> + <VnLv :label="$t('vehicle.tradeMark')" :value="entity.tradeMark" /> + <VnLv :label="$t('globals.model')" :value="entity.model" /> + <VnLv :label="$t('globals.country')" :value="entity.countryCodeFk" /> + </template> + </CardDescriptor> +</template> +<i18n> +es: + Vehicle removed: Vehículo eliminado +</i18n> diff --git a/src/pages/Route/Vehicle/Card/VehicleSummary.vue b/src/pages/Route/Vehicle/Card/VehicleSummary.vue new file mode 100644 index 000000000..981870cb2 --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleSummary.vue @@ -0,0 +1,127 @@ +<script setup> +import { computed } from 'vue'; +import { useRoute } from 'vue-router'; +import CardSummary from 'components/ui/CardSummary.vue'; +import VnLv from 'src/components/ui/VnLv.vue'; +import VnTitle from 'src/components/common/VnTitle.vue'; +import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; +import VehicleFilter from '../VehicleFilter.js'; +import { downloadFile } from 'src/composables/downloadFile'; +import { dashIfEmpty } from 'src/filters'; + +const props = defineProps({ id: { type: [Number, String], default: null } }); + +const route = useRoute(); +const entityId = computed(() => props.id || +route.params.id); +const links = { + 'basic-data': `#/vehicle/${entityId.value}/basic-data`, + notes: `#/vehicle/${entityId.value}/notes`, + dms: `#/vehicle/${entityId.value}/dms`, + 'invoice-in': `#/vehicle/${entityId.value}/invoice-in`, + events: `#/vehicle/${entityId.value}/events`, +}; +</script> +<template> + <CardSummary data-key="Vehicle" :url="`Vehicles/${entityId}`" :filter="VehicleFilter"> + <template #header="{ entity }"> + <div>{{ entity.id }} - {{ entity.numberPlate }}</div> + </template> + <template #body="{ entity }"> + <QCard class="vn-one"> + <QCardSection dense> + <VnTitle + :url="links['basic-data']" + :text="$t('globals.pageTitles.basicData')" + /> + </QCardSection> + <QCardSection content> + <QList dense> + <VnLv + :label="$t('globals.description')" + :value="entity.description" + /> + <VnLv + :label="$t('vehicle.tradeMark')" + :value="entity.tradeMark" + /> + <VnLv :label="$t('globals.model')" :value="entity.model" /> + <VnLv :label="$t('globals.supplier')"> + <template #value> + <span class="link"> + {{ entity.supplier?.name }} + <SupplierDescriptorProxy :id="entity.supplierFk" /> + </span> + </template> + </VnLv> + <VnLv :label="$t('vehicle.supplierCooler')"> + <template #value> + <span class="link"> + {{ entity.supplierCooler?.name }} + <SupplierDescriptorProxy + :id="entity.supplierCoolerFk" + /> + </span> + </template> + </VnLv> + <VnLv :label="$t('vehicle.vin')" :value="entity.vin" /> + </QList> + <QList dense> + <VnLv :label="$t('vehicle.chassis')" :value="entity.chassis" /> + <VnLv + :label="$t('globals.fuel')" + :value="entity.fuelType?.name" + /> + <VnLv :label="$t('vehicle.ppe')" :value="entity.ppeFk" /> + <VnLv :label="$t('vehicle.nLeasing')" :value="entity.leasing" /> + <VnLv + :label="$t('vehicle.leasing')" + :value="entity.bankPolicy?.ref" + > + <template #value> + <span v-text="dashIfEmpty(entity.bankPolicy?.name)" /> + <QBtn + v-if="entity.bankPolicy?.dmsFk" + class="q-ml-xs" + color="primary" + flat + dense + icon="cloud_download" + @click="downloadFile(entity.bankPolicy?.dmsFk)" + > + <QTooltip>{{ $t('globals.download') }}</QTooltip> + </QBtn> + </template> + </VnLv> + <VnLv :label="$t('globals.amount')" :value="entity.import" /> + </QList> + <QList dense> + <VnLv + :label="$t('globals.warehouse')" + :value="entity.warehouse?.name" + /> + <VnLv + :label="$t('globals.company')" + :value="entity.company?.code" + /> + <VnLv + :label="$t('globals.deliveryPoint')" + :value="entity.deliveryPoint?.name" + /> + <VnLv + :label="$t('globals.country')" + :value="entity.countryCodeFk" + /> + <VnLv + :label="$t('vehicle.isKmTruckRate')" + :value="!!entity.isKmTruckRate" + /> + <VnLv + :label="$t('vehicle.isActive')" + :value="!!entity.isActive" + /> + </QList> + </QCardSection> + </QCard> + </template> + </CardSummary> +</template> diff --git a/src/pages/Route/Vehicle/VehicleFilter.js b/src/pages/Route/Vehicle/VehicleFilter.js new file mode 100644 index 000000000..cbf5cc621 --- /dev/null +++ b/src/pages/Route/Vehicle/VehicleFilter.js @@ -0,0 +1,76 @@ +export default { + fields: [ + 'id', + 'description', + 'isActive', + 'isKmTruckRate', + 'warehouseFk', + 'companyFk', + 'numberPlate', + 'chassis', + 'supplierFk', + 'supplierCoolerFk', + 'tradeMark', + 'fuelTypeFk', + 'import', + 'importCooler', + 'vin', + 'model', + 'ppeFk', + 'countryCodeFk', + 'leasing', + 'bankPolicyFk', + 'vehicleTypeFk', + 'deliveryPointFk', + ], + include: [ + { + relation: 'warehouse', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'company', + scope: { + fields: ['id', 'code'], + }, + }, + { + relation: 'supplier', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'supplierCooler', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'fuelType', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'bankPolicy', + scope: { + fields: ['id', 'ref', 'dmsFk'], + }, + }, + { + relation: 'ppe', + scope: { + fields: ['id'], + }, + }, + { + relation: 'deliveryPoint', + scope: { + fields: ['id', 'name'], + }, + }, + ], +}; diff --git a/src/pages/Route/Vehicle/VehicleList.vue b/src/pages/Route/Vehicle/VehicleList.vue new file mode 100644 index 000000000..e5b945010 --- /dev/null +++ b/src/pages/Route/Vehicle/VehicleList.vue @@ -0,0 +1,224 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import VnTable from 'components/VnTable/VnTable.vue'; +import FetchData from 'src/components/FetchData.vue'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import VehicleSummary from 'src/pages/Route/Vehicle/Card/VehicleSummary.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import VnSection from 'src/components/common/VnSection.vue'; + +const { t } = useI18n(); +const { viewSummary } = useSummaryDialog(); +const warehouses = ref([]); +const companies = ref([]); +const countries = ref([]); +const vehicleStates = ref([]); +const vehicleTypes = ref([]); + +const columns = computed(() => [ + { + name: 'isActive', + columnFilter: false, + align: 'center', + }, + { + name: 'id', + label: t('globals.id'), + isId: true, + chip: { + condition: () => true, + }, + }, + { + name: 'description', + label: t('globals.description'), + }, + { + name: 'tradeMark', + label: t('vehicle.tradeMark'), + cardVisible: true, + }, + { + name: 'numberPlate', + label: t('vehicle.numberPlate'), + isTitle: true, + }, + { + name: 'vehicleTypeFk', + label: t('globals.type'), + format: (row) => row.type, + columnFilter: { + component: 'select', + name: 'vehicleTypeFk', + options: vehicleTypes.value, + }, + cardVisible: true, + }, + { + name: 'vehicleStateFk', + label: t('globals.state'), + columnFilter: { + component: 'select', + name: 'vehicleStateFk', + optionLabel: 'state', + options: vehicleStates.value, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row.state), + }, + { + name: 'chassis', + label: t('vehicle.chassis'), + }, + { + name: 'leasing', + label: t('vehicle.leasing'), + }, + { + name: 'warehouseFk', + label: t('globals.warehouse'), + format: (row, dashIfEmpty) => dashIfEmpty(row.warehouse), + columnFilter: { + component: 'select', + name: 'warehouseFk', + options: warehouses.value, + }, + cardVisible: true, + }, + { + name: 'companyFk', + label: t('globals.company'), + format: (row, dashIfEmpty) => dashIfEmpty(row.company), + columnFilter: { + component: 'select', + name: 'companyFk', + optionLabel: 'code', + options: companies.value, + }, + }, + { + name: 'countryCodeFk', + label: t('globals.country'), + columnFilter: { + component: 'select', + name: 'countryCodeFk', + optionValue: 'code', + optionLabel: 'code', + options: countries.value, + }, + }, + { + align: 'right', + name: 'tableActions', + actions: [ + { + title: t('components.smartCard.openSummary'), + icon: 'preview', + action: (row) => viewSummary(row.id, VehicleSummary), + }, + ], + }, +]); +</script> +<template> + <FetchData + url="Warehouses" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (warehouses = data)" + auto-load + /> + <FetchData + url="Companies" + :filter="{ fields: ['id', 'code'] }" + @on-fetch="(data) => (companies = data)" + auto-load + /> + <FetchData + url="Countries" + :filter="{ fields: ['name', 'code'] }" + @on-fetch="(data) => (countries = data)" + auto-load + /> + <FetchData + url="VehicleStates" + :filter="{ fields: ['id', 'state'] }" + @on-fetch="(data) => (vehicleStates = data)" + auto-load + /> + <FetchData + url="VehicleTypes" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (vehicleTypes = data)" + auto-load + /> + <VnSection + data-key="VehicleList" + :columns="columns" + prefix="vehicle" + :array-data-props="{ + url: 'Vehicles/filter', + }" + > + <template #body> + <VnTable + ref="tableRef" + data-key="VehicleList" + :columns="columns" + redirect="route/vehicle" + :create="{ + urlCreate: 'Vehicles', + title: t('vehicle.create'), + onDataSaved: ({ id }) => $refs.tableRef.redirect(id), + formInitialData: { isActive: true, isKmTruckRate: false }, + }" + :use-model="true" + :right-search="false" + > + <template #column-isActive="{ row }"> + <span> + <QIcon + v-if="!row.isActive" + name="vn:inactive-car" + color="primary" + size="xs" + > + <QTooltip>{{ $t('globals.inactive') }}</QTooltip> + </QIcon> + </span> + </template> + <template #more-create-dialog="{ data }"> + <VnInput + v-model="data.numberPlate" + :label="$t('vehicle.numberPlate')" + :uppercase="true" + /> + <VnInput v-model="data.tradeMark" :label="$t('vehicle.tradeMark')" /> + <VnInput v-model="data.model" :label="$t('globals.model')" /> + <VnSelect + v-model="data.vehicleTypeFk" + :label="$t('globals.type')" + :options="vehicleTypes" + /> + <VnSelect + v-model="data.warehouseFk" + :label="$t('globals.warehouse')" + :options="warehouses" + /> + <VnSelect + v-model="data.countryCodeFk" + :label="$t('globals.country')" + option-value="code" + option-label="name" + :options="countries" + /> + <VnInput + v-model="data.description" + :label="$t('globals.description')" + /> + <QCheckbox to v-model="data.isActive" :label="$t('globals.active')" /> + </template> + </VnTable> + </template> + </VnSection> +</template> diff --git a/src/pages/Route/Vehicle/locale/en.yml b/src/pages/Route/Vehicle/locale/en.yml new file mode 100644 index 000000000..c92022f9d --- /dev/null +++ b/src/pages/Route/Vehicle/locale/en.yml @@ -0,0 +1,20 @@ +vehicle: + tradeMark: Trade Mark + numberPlate: Nº Plate + chassis: Chassis + leasing: Leasing + isKmTruckRate: Trailer + delete: Delete Vehicle + supplierCooler: Supplier Cooler + vin: VIN + ppe: Ppe + isActive: Active + nLeasing: Nº Leasing + create: Create Vehicle + amountCooler: Amount cooler + remove: Vehicle removed + search: Search Vehicle + searchInfo: Search by id or number plate + params: + vehicleTypeFk: Type + vehicleStateFk: State diff --git a/src/pages/Route/Vehicle/locale/es.yml b/src/pages/Route/Vehicle/locale/es.yml new file mode 100644 index 000000000..c878f97ac --- /dev/null +++ b/src/pages/Route/Vehicle/locale/es.yml @@ -0,0 +1,20 @@ +vehicle: + tradeMark: Marca + numberPlate: Matrícula + chassis: Nº de bastidor + leasing: Leasing + isKmTruckRate: Trailer + delete: Eliminar vehículo + supplierCooler: Proveedor Frío + vin: VIN + ppe: Nº Inmovilizado + create: Crear vehículo + amountCooler: Importe frío + isActive: Activo + nLeasing: Nº leasing + remove: Vehículo eliminado + search: Buscar Vehículo + searchInfo: Buscar por id o matrícula + params: + vehicleTypeFk: Tipo + vehicleStateFk: Estado diff --git a/src/pages/Shelving/Card/ShelvingCard.vue b/src/pages/Shelving/Card/ShelvingCard.vue index 41a0db33c..9e0ac8ad2 100644 --- a/src/pages/Shelving/Card/ShelvingCard.vue +++ b/src/pages/Shelving/Card/ShelvingCard.vue @@ -1,12 +1,14 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import ShelvingDescriptor from 'pages/Shelving/Card/ShelvingDescriptor.vue'; +import filter from './ShelvingFilter.js'; </script> <template> <VnCardBeta data-key="Shelving" - base-url="Shelvings" + url="Shelvings" + :filter="filter" :descriptor="ShelvingDescriptor" /> </template> diff --git a/src/pages/Shelving/Card/ShelvingDescriptor.vue b/src/pages/Shelving/Card/ShelvingDescriptor.vue index b1ff4a8ae..5e618aa7f 100644 --- a/src/pages/Shelving/Card/ShelvingDescriptor.vue +++ b/src/pages/Shelving/Card/ShelvingDescriptor.vue @@ -1,12 +1,12 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; -import useCardDescription from 'composables/useCardDescription'; import ShelvingDescriptorMenu from 'pages/Shelving/Card/ShelvingDescriptorMenu.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; +import filter from './ShelvingFilter.js'; const $props = defineProps({ id: { @@ -22,35 +22,13 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); - -const filter = { - include: [ - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { fields: ['nickname'] }, - }, - }, - }, - { relation: 'parking' }, - ], -}; -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); </script> - <template> <CardDescriptor - module="Shelving" :url="`Shelvings/${entityId}`" :filter="filter" - :title="data.title" - :subtitle="data.subtitle" - data-key="Shelvings" - @on-fetch="setData" + title="code" + data-key="Shelving" > <template #body="{ entity }"> <VnLv :label="t('globals.code')" :value="entity.code" /> diff --git a/src/pages/Shelving/Card/ShelvingFilter.js b/src/pages/Shelving/Card/ShelvingFilter.js new file mode 100644 index 000000000..e302e1b9c --- /dev/null +++ b/src/pages/Shelving/Card/ShelvingFilter.js @@ -0,0 +1,15 @@ +export default { + include: [ + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { fields: ['nickname'] }, + }, + }, + }, + { relation: 'parking' }, + ], +}; diff --git a/src/pages/Shelving/Card/ShelvingForm.vue b/src/pages/Shelving/Card/ShelvingForm.vue index 3bbd94a0a..078058342 100644 --- a/src/pages/Shelving/Card/ShelvingForm.vue +++ b/src/pages/Shelving/Card/ShelvingForm.vue @@ -1,5 +1,4 @@ <script setup> -import { useI18n } from 'vue-i18n'; import { computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import VnRow from 'components/ui/VnRow.vue'; @@ -7,8 +6,8 @@ import FormModel from 'components/FormModel.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; +import filter from './ShelvingFilter.js'; -const { t } = useI18n(); const route = useRoute(); const router = useRouter(); const entityId = computed(() => route.params.id ?? null); @@ -20,22 +19,6 @@ const defaultInitialData = { isRecyclable: false, }; -const shelvingFilter = { - include: [ - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { fields: ['nickname'] }, - }, - }, - }, - { relation: 'parking' }, - ], -}; - const onSave = (shelving, newShelving) => { if (isNew) { router.push({ name: 'ShelvingBasicData', params: { id: newShelving?.id } }); @@ -45,11 +28,10 @@ const onSave = (shelving, newShelving) => { <template> <VnSubToolbar v-if="isNew" /> <FormModel - :url="isNew ? null : `Shelvings/${entityId}`" :url-create="isNew ? 'Shelvings' : null" :observe-form-changes="!isNew" - :filter="shelvingFilter" - model="shelving" + :filter="filter" + model="Shelving" :auto-load="!isNew" :form-initial-data="isNew ? defaultInitialData : null" @on-data-saved="onSave" @@ -58,7 +40,7 @@ const onSave = (shelving, newShelving) => { <VnRow> <VnInput v-model="data.code" - :label="t('globals.code')" + :label="$t('globals.code')" :rules="validate('Shelving.code')" /> <VnSelect @@ -68,7 +50,7 @@ const onSave = (shelving, newShelving) => { option-label="code" :filter-options="['id', 'code']" :fields="['id', 'code']" - :label="t('shelving.list.parking')" + :label="$t('shelving.list.parking')" :rules="validate('Shelving.parkingFk')" /> </VnRow> @@ -76,12 +58,12 @@ const onSave = (shelving, newShelving) => { <VnInput v-model="data.priority" type="number" - :label="t('shelving.list.priority')" + :label="$t('shelving.list.priority')" :rules="validate('Shelving.priority')" /> <QCheckbox v-model="data.isRecyclable" - :label="t('shelving.summary.recyclable')" + :label="$t('shelving.summary.recyclable')" :rules="validate('Shelving.isRecyclable')" /> </VnRow> diff --git a/src/pages/Shelving/Card/ShelvingSearchbar.vue b/src/pages/Shelving/Card/ShelvingSearchbar.vue index bfc8ad4f5..741b11663 100644 --- a/src/pages/Shelving/Card/ShelvingSearchbar.vue +++ b/src/pages/Shelving/Card/ShelvingSearchbar.vue @@ -1,15 +1,15 @@ <script setup> import VnSearchbar from 'components/ui/VnSearchbar.vue'; -import {useI18n} from "vue-i18n"; -const { t } = useI18n(); +import exprBuilder from '../ShelvingExprBuilder.js'; </script> <template> <VnSearchbar data-key="ShelvingList" url="Shelvings" - :label="t('Search shelving')" - :info="t('You can search by shelving reference')" + :label="$t('Search shelving')" + :info="$t('You can search by shelving reference')" + :expr-builder="exprBuilder" /> </template> diff --git a/src/pages/Shelving/Card/ShelvingSummary.vue b/src/pages/Shelving/Card/ShelvingSummary.vue index 39fa4639f..f89ff4d78 100644 --- a/src/pages/Shelving/Card/ShelvingSummary.vue +++ b/src/pages/Shelving/Card/ShelvingSummary.vue @@ -1,10 +1,10 @@ <script setup> import { computed, ref } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'components/ui/VnLv.vue'; import VnUserLink from 'components/ui/VnUserLink.vue'; +import filter from './ShelvingFilter.js'; import ShelvingDescriptorMenu from './ShelvingDescriptorMenu.vue'; const $props = defineProps({ @@ -14,25 +14,9 @@ const $props = defineProps({ }, }); const route = useRoute(); -const { t } = useI18n(); + const summary = ref({}); const entityId = computed(() => $props.id || route.params.id); - -const filter = { - include: [ - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { fields: ['nickname'] }, - }, - }, - }, - { relation: 'parking' }, - ], -}; </script> <template> @@ -41,7 +25,7 @@ const filter = { ref="summary" :url="`Shelvings/${entityId}`" :filter="filter" - data-key="ShelvingSummary" + data-key="Shelving" > <template #header="{ entity }"> <div>{{ entity.code }}</div> @@ -58,16 +42,19 @@ const filter = { class="header header-link" :to="{ name: 'ShelvingBasicData', params: { id: entityId } }" > - {{ t('globals.pageTitles.basicData') }} + {{ $t('globals.pageTitles.basicData') }} <QIcon name="open_in_new" /> </RouterLink> - <VnLv :label="t('globals.code')" :value="entity.code" /> + <VnLv :label="$t('globals.code')" :value="entity.code" /> <VnLv - :label="t('shelving.list.parking')" + :label="$t('shelving.list.parking')" :value="entity.parking?.code" /> - <VnLv :label="t('shelving.list.priority')" :value="entity.priority" /> - <VnLv v-if="entity.worker" :label="t('globals.worker')"> + <VnLv + :label="$t('shelving.list.priority')" + :value="entity.priority" + /> + <VnLv v-if="entity.worker" :label="$t('globals.worker')"> <template #value> <VnUserLink :name="entity.worker?.user?.nickname" @@ -76,7 +63,7 @@ const filter = { </template> </VnLv> <VnLv - :label="t('shelving.summary.recyclable')" + :label="$t('shelving.summary.recyclable')" :value="entity.isRecyclable" /> </QCard> diff --git a/src/pages/Parking/Card/ParkingBasicData.vue b/src/pages/Shelving/Parking/Card/ParkingBasicData.vue similarity index 68% rename from src/pages/Parking/Card/ParkingBasicData.vue rename to src/pages/Shelving/Parking/Card/ParkingBasicData.vue index 550a0684e..3de358002 100644 --- a/src/pages/Parking/Card/ParkingBasicData.vue +++ b/src/pages/Shelving/Parking/Card/ParkingBasicData.vue @@ -1,16 +1,11 @@ <script setup> -import { ref, computed } from 'vue'; -import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; +import { ref } from 'vue'; import VnRow from 'components/ui/VnRow.vue'; import FetchData from 'src/components/FetchData.vue'; import VnInput from 'src/components/common/VnInput.vue'; import FormModel from 'components/FormModel.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; -const { t } = useI18n(); -const route = useRoute(); -const parkingId = computed(() => route.params?.id || null); const sectors = ref([]); const sectorFilter = { fields: ['id', 'description'] }; @@ -27,18 +22,21 @@ const filter = { @on-fetch="(data) => (sectors = data)" auto-load /> - <FormModel :url="`Parkings/${parkingId}`" model="parking" :filter="filter" auto-load> + <FormModel model="Parking" auto-load> <template #form="{ data }"> <VnRow> - <VnInput v-model="data.code" :label="t('globals.code')" /> - <VnInput v-model="data.pickingOrder" :label="t('parking.pickingOrder')" /> + <VnInput v-model="data.code" :label="$t('globals.code')" /> + <VnInput + v-model="data.pickingOrder" + :label="$t('parking.pickingOrder')" + /> </VnRow> <VnRow> <VnSelect v-model="data.sectorFk" option-value="id" option-label="description" - :label="t('parking.sector')" + :label="$t('parking.sector')" :options="sectors" use-input input-debounce="0" diff --git a/src/pages/Parking/Card/ParkingCard.vue b/src/pages/Shelving/Parking/Card/ParkingCard.vue similarity index 53% rename from src/pages/Parking/Card/ParkingCard.vue rename to src/pages/Shelving/Parking/Card/ParkingCard.vue index 1cd2df7b7..b32c1b7d3 100644 --- a/src/pages/Parking/Card/ParkingCard.vue +++ b/src/pages/Shelving/Parking/Card/ParkingCard.vue @@ -1,12 +1,14 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; -import ParkingDescriptor from 'pages/Parking/Card/ParkingDescriptor.vue'; +import ParkingDescriptor from 'pages/Shelving/Parking/Card/ParkingDescriptor.vue'; +import filter from './ParkingFilter.js'; </script> <template> <VnCardBeta data-key="Parking" - base-url="Parkings" + url="Parkings" + :filter="filter" :descriptor="ParkingDescriptor" /> </template> diff --git a/src/pages/Parking/Card/ParkingDescriptor.vue b/src/pages/Shelving/Parking/Card/ParkingDescriptor.vue similarity index 58% rename from src/pages/Parking/Card/ParkingDescriptor.vue rename to src/pages/Shelving/Parking/Card/ParkingDescriptor.vue index d36ea16fc..46c9f8ea0 100644 --- a/src/pages/Parking/Card/ParkingDescriptor.vue +++ b/src/pages/Shelving/Parking/Card/ParkingDescriptor.vue @@ -1,10 +1,9 @@ <script setup> import { computed } from 'vue'; -import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; - +import filter from './ParkingFilter.js'; const props = defineProps({ id: { type: Number, @@ -13,18 +12,11 @@ const props = defineProps({ }, }); -const { t } = useI18n(); const route = useRoute(); const entityId = computed(() => props.id || route.params.id); - -const filter = { - fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'], - include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }], -}; </script> <template> <CardDescriptor - module="Parking" data-key="Parking" :url="`Parkings/${entityId}`" title="code" @@ -32,9 +24,9 @@ const filter = { :to-module="{ name: 'ParkingList' }" > <template #body="{ entity }"> - <VnLv :label="t('globals.code')" :value="entity.code" /> - <VnLv :label="t('parking.pickingOrder')" :value="entity.pickingOrder" /> - <VnLv :label="t('parking.sector')" :value="entity.sector?.description" /> + <VnLv :label="$t('globals.code')" :value="entity.code" /> + <VnLv :label="$t('parking.pickingOrder')" :value="entity.pickingOrder" /> + <VnLv :label="$t('parking.sector')" :value="entity.sector?.description" /> </template> </CardDescriptor> </template> diff --git a/src/pages/Shelving/Parking/Card/ParkingFilter.js b/src/pages/Shelving/Parking/Card/ParkingFilter.js new file mode 100644 index 000000000..fd1855c45 --- /dev/null +++ b/src/pages/Shelving/Parking/Card/ParkingFilter.js @@ -0,0 +1,4 @@ +export default { + fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'], + include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }], +}; diff --git a/src/pages/Parking/Card/ParkingLog.vue b/src/pages/Shelving/Parking/Card/ParkingLog.vue similarity index 100% rename from src/pages/Parking/Card/ParkingLog.vue rename to src/pages/Shelving/Parking/Card/ParkingLog.vue diff --git a/src/pages/Parking/Card/ParkingSummary.vue b/src/pages/Shelving/Parking/Card/ParkingSummary.vue similarity index 100% rename from src/pages/Parking/Card/ParkingSummary.vue rename to src/pages/Shelving/Parking/Card/ParkingSummary.vue diff --git a/src/pages/Shelving/Parking/ParkingExprBuilder.js b/src/pages/Shelving/Parking/ParkingExprBuilder.js new file mode 100644 index 000000000..16d2262c8 --- /dev/null +++ b/src/pages/Shelving/Parking/ParkingExprBuilder.js @@ -0,0 +1,10 @@ +export default (param, value) => { + switch (param) { + case 'code': + return { [param]: { like: `%${value}%` } }; + case 'sectorFk': + return { [param]: value }; + case 'search': + return { or: [{ code: { like: `%${value}%` } }, { id: value }] }; + } +}; diff --git a/src/pages/Parking/ParkingFilter.vue b/src/pages/Shelving/Parking/ParkingFilter.vue similarity index 100% rename from src/pages/Parking/ParkingFilter.vue rename to src/pages/Shelving/Parking/ParkingFilter.vue diff --git a/src/pages/Parking/ParkingList.vue b/src/pages/Shelving/Parking/ParkingList.vue similarity index 90% rename from src/pages/Parking/ParkingList.vue rename to src/pages/Shelving/Parking/ParkingList.vue index bce87126e..fe6c93ba5 100644 --- a/src/pages/Parking/ParkingList.vue +++ b/src/pages/Shelving/Parking/ParkingList.vue @@ -9,6 +9,7 @@ import CardList from 'components/ui/CardList.vue'; import VnLv from 'components/ui/VnLv.vue'; import ParkingFilter from './ParkingFilter.vue'; import ParkingSummary from './Card/ParkingSummary.vue'; +import exprBuilder from './ParkingExprBuilder.js'; import VnSection from 'src/components/common/VnSection.vue'; const stateStore = useStateStore(); @@ -23,19 +24,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); const filter = { fields: ['id', 'sectorFk', 'code', 'pickingOrder'], }; - -function exprBuilder(param, value) { - switch (param) { - case 'code': - return { [param]: { like: `%${value}%` } }; - case 'sectorFk': - return { [param]: value }; - case 'search': - return { or: [{ code: { like: `%${value}%` } }, { id: value }] }; - } -} </script> - <template> <VnSection :data-key="dataKey" diff --git a/src/pages/Parking/locale/en.yml b/src/pages/Shelving/Parking/locale/en.yml similarity index 100% rename from src/pages/Parking/locale/en.yml rename to src/pages/Shelving/Parking/locale/en.yml diff --git a/src/pages/Parking/locale/es.yml b/src/pages/Shelving/Parking/locale/es.yml similarity index 100% rename from src/pages/Parking/locale/es.yml rename to src/pages/Shelving/Parking/locale/es.yml diff --git a/src/pages/Shelving/ShelvingExprBuilder.js b/src/pages/Shelving/ShelvingExprBuilder.js new file mode 100644 index 000000000..b9aad8a71 --- /dev/null +++ b/src/pages/Shelving/ShelvingExprBuilder.js @@ -0,0 +1,10 @@ +export default (param, value) => { + switch (param) { + case 'search': + return { code: { like: `%${value}%` } }; + case 'parkingFk': + case 'userFk': + case 'isRecyclable': + return { [param]: value }; + } +}; diff --git a/src/pages/Shelving/ShelvingList.vue b/src/pages/Shelving/ShelvingList.vue index cf158e76b..4e0c21100 100644 --- a/src/pages/Shelving/ShelvingList.vue +++ b/src/pages/Shelving/ShelvingList.vue @@ -1,6 +1,5 @@ <script setup> import VnPaginate from 'components/ui/VnPaginate.vue'; -import { useI18n } from 'vue-i18n'; import CardList from 'components/ui/CardList.vue'; import VnLv from 'components/ui/VnLv.vue'; import { useRouter } from 'vue-router'; @@ -8,9 +7,9 @@ import ShelvingFilter from 'pages/Shelving/Card/ShelvingFilter.vue'; import ShelvingSummary from 'pages/Shelving/Card/ShelvingSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import VnSection from 'src/components/common/VnSection.vue'; +import exprBuilder from './ShelvingExprBuilder.js'; const router = useRouter(); -const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const dataKey = 'ShelvingList'; @@ -21,17 +20,6 @@ const filter = { function navigate(id) { router.push({ path: `/shelving/${id}` }); } - -function exprBuilder(param, value) { - switch (param) { - case 'search': - return { code: { like: `%${value}%` } }; - case 'parkingFk': - case 'userFk': - case 'isRecyclable': - return { [param]: value }; - } -} </script> <template> @@ -62,18 +50,18 @@ function exprBuilder(param, value) { > <template #list-items> <VnLv - :label="t('shelving.list.parking')" - :title-label="t('shelving.list.parking')" + :label="$t('shelving.list.parking')" + :title-label="$t('shelving.list.parking')" :value="row.parking?.code" /> <VnLv - :label="t('shelving.list.priority')" + :label="$t('shelving.list.priority')" :value="row?.priority" /> </template> <template #actions> <QBtn - :label="t('components.smartCard.openSummary')" + :label="$t('components.smartCard.openSummary')" @click.stop="viewSummary(row.id, ShelvingSummary)" color="primary" /> @@ -84,9 +72,9 @@ function exprBuilder(param, value) { </div> <QPageSticky :offset="[20, 20]"> <RouterLink :to="{ name: 'ShelvingCreate' }"> - <QBtn fab icon="add" color="primary" shortcut="+" /> + <QBtn fab icon="add" color="primary" v-shortcut="'+'" /> <QTooltip> - {{ t('shelving.list.newShelving') }} + {{ $t('shelving.list.newShelving') }} </QTooltip> </RouterLink> </QPageSticky> diff --git a/src/pages/Supplier/Card/SupplierAccounts.vue b/src/pages/Supplier/Card/SupplierAccounts.vue index 4a6901d1d..365eb67a1 100644 --- a/src/pages/Supplier/Card/SupplierAccounts.vue +++ b/src/pages/Supplier/Card/SupplierAccounts.vue @@ -71,7 +71,7 @@ function bankEntityFilter(val, update) { filteredBankEntitiesOptions.value = bankEntitiesOptions.value.filter( (bank) => bank.bic.toLowerCase().startsWith(needle) || - bank.name.toLowerCase().includes(needle) + bank.name.toLowerCase().includes(needle), ); }); } @@ -170,7 +170,7 @@ function bankEntityFilter(val, update) { <QIcon name="info" class="cursor-pointer"> <QTooltip>{{ t( - 'Name of the bank account holder if different from the provider' + 'Name of the bank account holder if different from the provider', ) }}</QTooltip> </QIcon> @@ -194,7 +194,7 @@ function bankEntityFilter(val, update) { <QBtn flat icon="add" - shortcut="+" + v-shortcut class="cursor-pointer" color="primary" @click="supplierAccountRef.insert()" diff --git a/src/pages/Supplier/Card/SupplierAddresses.vue b/src/pages/Supplier/Card/SupplierAddresses.vue index e568962ff..c4c0ab7be 100644 --- a/src/pages/Supplier/Card/SupplierAddresses.vue +++ b/src/pages/Supplier/Card/SupplierAddresses.vue @@ -89,7 +89,7 @@ const redirectToUpdateView = (addressData) => { icon="add" color="primary" @click="redirectToCreateView()" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('New address') }} diff --git a/src/pages/Supplier/Card/SupplierAgencyTerm.vue b/src/pages/Supplier/Card/SupplierAgencyTerm.vue index 99b672cc4..ab21f1f76 100644 --- a/src/pages/Supplier/Card/SupplierAgencyTerm.vue +++ b/src/pages/Supplier/Card/SupplierAgencyTerm.vue @@ -114,7 +114,7 @@ const redirectToCreateView = () => { icon="add" color="primary" @click="redirectToCreateView()" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('supplier.agencyTerms.addRow') }} diff --git a/src/pages/Supplier/Card/SupplierBasicData.vue b/src/pages/Supplier/Card/SupplierBasicData.vue index f6c13b7af..631700a4a 100644 --- a/src/pages/Supplier/Card/SupplierBasicData.vue +++ b/src/pages/Supplier/Card/SupplierBasicData.vue @@ -19,9 +19,8 @@ const companySizes = [ </script> <template> <FormModel - :url="`Suppliers/${route.params.id}`" :url-update="`Suppliers/${route.params.id}`" - model="supplier" + model="Supplier" auto-load :clear-store-on-unmount="false" @on-data-saved="arrayData.fetch({})" diff --git a/src/pages/Supplier/Card/SupplierCard.vue b/src/pages/Supplier/Card/SupplierCard.vue index 594026d18..e30f79f96 100644 --- a/src/pages/Supplier/Card/SupplierCard.vue +++ b/src/pages/Supplier/Card/SupplierCard.vue @@ -1,19 +1,13 @@ <script setup> -import VnCard from 'components/common/VnCard.vue'; import SupplierDescriptor from './SupplierDescriptor.vue'; -import SupplierListFilter from '../SupplierListFilter.vue'; +import VnCardBeta from 'src/components/common/VnCardBeta.vue'; +import filter from './SupplierFilter.js'; </script> <template> - <VnCard + <VnCardBeta data-key="Supplier" - base-url="Suppliers" + url="Suppliers" :descriptor="SupplierDescriptor" - :filter-panel="SupplierListFilter" - search-data-key="SupplierList" - :searchbar-props="{ - url: 'Suppliers/filter', - searchUrl: 'table', - label: 'Search suppliers', - }" + :filter="filter" /> </template> diff --git a/src/pages/Supplier/Card/SupplierConsumption.vue b/src/pages/Supplier/Card/SupplierConsumption.vue index 8a7021fb3..718de95dd 100644 --- a/src/pages/Supplier/Card/SupplierConsumption.vue +++ b/src/pages/Supplier/Card/SupplierConsumption.vue @@ -16,6 +16,7 @@ import axios from 'axios'; import { useStateStore } from 'stores/useStateStore'; import { useState } from 'src/composables/useState'; import { useArrayData } from 'composables/useArrayData'; +import RightMenu from 'src/components/common/RightMenu.vue'; const state = useState(); const stateStore = useStateStore(); @@ -173,59 +174,59 @@ onMounted(async () => { </div> </div> </Teleport> - <QPage class="column items-center q-pa-md"> - <Teleport to="#right-panel" v-if="stateStore.isHeaderMounted()"> + <RightMenu> + <template #right-panel> <SupplierConsumptionFilter data-key="SupplierConsumption" /> - </Teleport> - <QTable - :rows="rows" - row-key="id" - hide-header - class="full-width q-mt-md" - :no-data-label="t('No results')" - > - <template #body="{ row }"> - <QTr> - <QTd no-hover> - <span class="label">{{ t('supplier.consumption.entry') }}: </span> - <span>{{ row.id }}</span> - </QTd> - <QTd no-hover> - <span class="label">{{ t('globals.date') }}: </span> - <span>{{ toDate(row.shipped) }}</span></QTd - > - <QTd colspan="6" no-hover> - <span class="label">{{ t('globals.reference') }}: </span> - <span>{{ row.invoiceNumber }}</span> - </QTd> - </QTr> - <QTr v-for="(buy, index) in row.buys" :key="index"> - <QTd no-hover> - <QBtn flat color="blue" dense no-caps>{{ buy.itemName }}</QBtn> - <ItemDescriptorProxy :id="buy.itemFk" /> - </QTd> + </template> + </RightMenu> + <QTable + :rows="rows" + row-key="id" + hide-header + class="full-width q-mt-md" + :no-data-label="t('No results')" + > + <template #body="{ row }"> + <QTr> + <QTd no-hover> + <span class="label">{{ t('supplier.consumption.entry') }}: </span> + <span>{{ row.id }}</span> + </QTd> + <QTd no-hover> + <span class="label">{{ t('globals.date') }}: </span> + <span>{{ toDate(row.shipped) }}</span></QTd + > + <QTd colspan="6" no-hover> + <span class="label">{{ t('globals.reference') }}: </span> + <span>{{ row.invoiceNumber }}</span> + </QTd> + </QTr> + <QTr v-for="(buy, index) in row.buys" :key="index"> + <QTd no-hover> + <QBtn flat color="blue" dense no-caps>{{ buy.itemName }}</QBtn> + <ItemDescriptorProxy :id="buy.itemFk" /> + </QTd> - <QTd no-hover> - <span>{{ buy.subName }}</span> - <FetchedTags :item="buy" /> - </QTd> - <QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd> - <QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd> - <QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd> - </QTr> - <QTr> - <QTd colspan="5" no-hover> - <span class="label">{{ t('Total entry') }}: </span> - <span>{{ row.total }} €</span> - </QTd> - <QTd no-hover> - <span class="label">{{ t('Total stems') }}: </span> - <span>{{ row.quantity }}</span> - </QTd> - </QTr> - </template> - </QTable> - </QPage> + <QTd no-hover> + <span>{{ buy.subName }}</span> + <FetchedTags :item="buy" /> + </QTd> + <QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd> + <QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd> + <QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd> + </QTr> + <QTr> + <QTd colspan="5" no-hover> + <span class="label">{{ t('Total entry') }}: </span> + <span>{{ row.total }} €</span> + </QTd> + <QTd no-hover> + <span class="label">{{ t('Total stems') }}: </span> + <span>{{ row.quantity }}</span> + </QTd> + </QTr> + </template> + </QTable> </template> <style scoped lang="scss"> diff --git a/src/pages/Supplier/Card/SupplierContacts.vue b/src/pages/Supplier/Card/SupplierContacts.vue index 6781c8d34..f96d92ab1 100644 --- a/src/pages/Supplier/Card/SupplierContacts.vue +++ b/src/pages/Supplier/Card/SupplierContacts.vue @@ -78,7 +78,7 @@ const insertRow = () => { <QBtn flat icon="add" - shortcut="+" + v-shortcut="'+'" class="cursor-pointer" color="primary" @click="insertRow()" diff --git a/src/pages/Supplier/Card/SupplierDescriptor.vue b/src/pages/Supplier/Card/SupplierDescriptor.vue index 37c9c1cff..462bdf853 100644 --- a/src/pages/Supplier/Card/SupplierDescriptor.vue +++ b/src/pages/Supplier/Card/SupplierDescriptor.vue @@ -7,8 +7,8 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import { toDateString } from 'src/filters'; -import useCardDescription from 'src/composables/useCardDescription'; import { getUrl } from 'src/composables/getUrl'; +import filter from './SupplierFilter.js'; import { useArrayData } from 'src/composables/useArrayData'; const $props = defineProps({ @@ -28,42 +28,6 @@ const { t } = useI18n(); const url = ref(); const arrayData = useArrayData(); -const filter = { - fields: [ - 'id', - 'name', - 'nickname', - 'nif', - 'payMethodFk', - 'payDemFk', - 'payDay', - 'isActive', - 'isReal', - 'isTrucker', - 'account', - ], - include: [ - { - relation: 'payMethod', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'payDem', - scope: { - fields: ['id', 'payDem'], - }, - }, - { - relation: 'client', - scope: { - fields: ['id', 'fi'], - }, - }, - ], -}; - onMounted(async () => { url.value = await getUrl(''); }); @@ -72,11 +36,6 @@ const entityId = computed(() => { return $props.id || route.params.id; }); -const data = ref(useCardDescription()); -const setData = (entity) => { - data.value = useCardDescription(entity.ref, entity.id); -}; - const supplier = computed(() => arrayData.store.data); const getEntryQueryParams = (supplier) => { @@ -103,13 +62,9 @@ const getEntryQueryParams = (supplier) => { <template> <CardDescriptor - module="Supplier" :url="`Suppliers/${entityId}`" - :title="data.title" - :subtitle="data.subtitle" :filter="filter" - @on-fetch="setData" - data-key="supplierDescriptor" + data-key="Supplier" :summary="$props.summary" > <template #body="{ entity }"> diff --git a/src/pages/Supplier/Card/SupplierFilter.js b/src/pages/Supplier/Card/SupplierFilter.js new file mode 100644 index 000000000..3ce5c3de2 --- /dev/null +++ b/src/pages/Supplier/Card/SupplierFilter.js @@ -0,0 +1,35 @@ +export default { + fields: [ + 'id', + 'name', + 'nickname', + 'nif', + 'payMethodFk', + 'payDemFk', + 'payDay', + 'isActive', + 'isSerious', + 'isTrucker', + 'account', + ], + include: [ + { + relation: 'payMethod', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'payDem', + scope: { + fields: ['id', 'payDem'], + }, + }, + { + relation: 'client', + scope: { + fields: ['id', 'fi'], + }, + }, + ], +}; diff --git a/src/pages/Supplier/Card/SupplierFiscalData.vue b/src/pages/Supplier/Card/SupplierFiscalData.vue index e569eb236..ecee5b76b 100644 --- a/src/pages/Supplier/Card/SupplierFiscalData.vue +++ b/src/pages/Supplier/Card/SupplierFiscalData.vue @@ -10,6 +10,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; import VnAccountNumber from 'src/components/common/VnAccountNumber.vue'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const route = useRoute(); const { t } = useI18n(); @@ -182,18 +183,11 @@ function handleLocation(data, location) { v-model="data.isTrucker" :label="t('supplier.fiscalData.isTrucker')" /> - <div class="row items-center"> - <QCheckbox v-model="data.isVies" :label="t('globals.isVies')" /> - <QIcon name="info" size="xs" class="cursor-pointer q-ml-sm"> - <QTooltip> - {{ - t( - 'When activating it, do not enter the country code in the ID field.' - ) - }} - </QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="data.isVies" + :label="t('globals.isVies')" + :info="t('whenActivatingIt')" + /> </div> </VnRow> </template> @@ -201,6 +195,8 @@ function handleLocation(data, location) { </template> <i18n> +en: + whenActivatingIt: When activating it, do not enter the country code in the ID field. es: - When activating it, do not enter the country code in the ID field.: Al activarlo, no informar el código del país en el campo nif + whenActivatingIt: Al activarlo, no informar el código del país en el campo nif. </i18n> diff --git a/src/pages/Supplier/SupplierList.vue b/src/pages/Supplier/SupplierList.vue index 85cc11857..600790745 100644 --- a/src/pages/Supplier/SupplierList.vue +++ b/src/pages/Supplier/SupplierList.vue @@ -2,14 +2,15 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import VnTable from 'components/VnTable/VnTable.vue'; -import VnSearchbar from 'components/ui/VnSearchbar.vue'; -import RightMenu from 'src/components/common/RightMenu.vue'; -import SupplierListFilter from './SupplierListFilter.vue'; +import VnSection from 'src/components/common/VnSection.vue'; import VnInput from 'src/components/common/VnInput.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import FetchData from 'src/components/FetchData.vue'; const { t } = useI18n(); const tableRef = ref(); - +const dataKey = 'SupplierList'; +const provincesOptions = ref([]); const columns = computed(() => [ { align: 'left', @@ -104,38 +105,62 @@ const columns = computed(() => [ }, ]); </script> - <template> - <VnSearchbar data-key="SuppliersList" :limit="20" :label="t('Search suppliers')" /> - <RightMenu> - <template #right-panel> - <SupplierListFilter data-key="SuppliersList" /> - </template> - </RightMenu> - <VnTable - ref="tableRef" - data-key="SuppliersList" - url="Suppliers/filter" - redirect="supplier" - :create="{ - urlCreate: 'Suppliers/newSupplier', - title: t('Create Supplier'), - onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: {}, - mapper: (data) => { - data.name = data.socialName; - - return data; - }, - }" - :right-search="false" - order="id ASC" + <FetchData + url="Provinces" + :filter="{ fields: ['id', 'name'], order: 'name ASC' }" + @on-fetch="(data) => (provincesOptions = data)" + auto-load + /> + <VnSection + :data-key="dataKey" :columns="columns" + prefix="supplier" + :array-data-props="{ + url: 'Suppliers/filter', + order: 'id ASC', + }" > - <template #more-create-dialog="{ data }"> - <VnInput :label="t('globals.name')" v-model="data.socialName" :uppercase="true" /> - </template> - </VnTable> + <template #body> + <VnTable + ref="tableRef" + :data-key="dataKey" + :create="{ + urlCreate: 'Suppliers/newSupplier', + title: t('Create Supplier'), + onDataSaved: ({ id }) => tableRef.redirect(id), + formInitialData: {}, + mapper: (data) => { + data.name = data.socialName; + delete data.socialName; + return data; + }, + }" + :columns="columns" + redirect="supplier" + :right-search="false" + > + <template #more-create-dialog="{ data }"> + <VnInput + :label="t('globals.name')" + v-model="data.socialName" + :uppercase="true" + /> + </template> + </VnTable> + </template> + <template #moreFilterPanel="{ params, searchFn }"> + <VnSelect + :label="t('globals.params.provinceFk')" + v-model="params.provinceFk" + @update:model-value="searchFn()" + :options="provincesOptions" + filled + dense + class="q-px-sm q-pr-lg" + /> + </template> + </VnSection> </template> <i18n> diff --git a/src/pages/Supplier/SupplierListFilter.vue b/src/pages/Supplier/SupplierListFilter.vue deleted file mode 100644 index b170a35cc..000000000 --- a/src/pages/Supplier/SupplierListFilter.vue +++ /dev/null @@ -1,122 +0,0 @@ -<script setup> -import { ref } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import FetchData from 'components/FetchData.vue'; - -const props = defineProps({ - dataKey: { - type: String, - required: true, - }, -}); - -const { t } = useI18n(); - -const provincesOptions = ref([]); -const countriesOptions = ref([]); -</script> - -<template> - <FetchData - url="Provinces" - :filter="{ fields: ['id', 'name'], order: 'name ASC'}" - @on-fetch="(data) => (provincesOptions = data)" - auto-load - /> - <FetchData - url="countries" - :filter="{ fields: ['id', 'name'], order: 'name ASC'}" - @on-fetch="(data) => (countriesOptions = data)" - auto-load - /> - <VnFilterPanel - :data-key="props.dataKey" - :search-button="true" - :unremovable-params="['supplierFk']" - > - <template #tags="{ tag, formatFn }"> - <div class="q-gutter-x-xs"> - <strong>{{ t(`params.${tag.label}`) }}: </strong> - <span>{{ formatFn(tag.value) }}</span> - </div> - </template> - <template #body="{ params, searchFn }"> - <QItem> - <QItemSection> - <VnInput - v-model="params.search" - :label="t('params.search')" - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput - v-model="params.nickname" - :label="t('params.nickname')" - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput v-model="params.nif" :label="t('params.nif')" is-outlined /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - :label="t('params.provinceFk')" - v-model="params.provinceFk" - @update:model-value="searchFn()" - :options="provincesOptions" - option-value="id" - option-label="name" - hide-selected - dense - outlined - rounded - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - :label="t('params.countryFk')" - v-model="params.countryFk" - @update:model-value="searchFn()" - :options="countriesOptions" - option-value="id" - option-label="name" - hide-selected - dense - outlined - rounded - /> - </QItemSection> - </QItem> - </template> - </VnFilterPanel> -</template> - -<i18n> -en: - params: - search: General search - nickname: Alias - nif: Tax number - provinceFk: Province - countryFk: Country -es: - params: - search: Búsqueda general - nickname: Alias - nif: NIF/CIF - provinceFk: Provincia - countryFk: País -</i18n> diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicData.vue b/src/pages/Ticket/Card/BasicData/TicketBasicData.vue index c6a85c287..055c9a0ff 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicData.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicData.vue @@ -9,8 +9,9 @@ import FetchData from 'components/FetchData.vue'; import { useStateStore } from 'stores/useStateStore'; import { toCurrency } from 'filters/index'; import { useRole } from 'src/composables/useRole'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; -const haveNegatives = defineModel('haveNegatives', { type: Boolean, required: true }); +const haveNegatives = defineModel('have-negatives', { type: Boolean, required: true }); const formData = defineModel({ type: Object, required: true }); const stateStore = useStateStore(); @@ -182,22 +183,19 @@ onMounted(async () => { </QCard> <QCard v-if="haveNegatives" - class="q-pa-md q-mb-md q-ma-md color-vn-text" + class="q-pa-xs q-mb-md q-ma-md color-vn-text" bordered flat style="border-color: black" > <QCardSection horizontal class="flex row items-center"> - <QCheckbox - :label="t('basicData.withoutNegatives')" + <VnCheckbox v-model="formData.withoutNegatives" + :label="t('basicData.withoutNegatives')" + :info="t('basicData.withoutNegativesInfo')" :toggle-indeterminate="false" + size="xs" /> - <QIcon name="info" size="xs" class="q-ml-sm"> - <QTooltip max-width="350px"> - {{ t('basicData.withoutNegativesInfo') }} - </QTooltip> - </QIcon> </QCardSection> </QCard> </QDrawer> diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue index cf4481537..9d70fea38 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue @@ -260,7 +260,7 @@ async function getZone(options) { auto-load /> <QForm> - <VnRow> + <VnRow class="row q-gutter-md q-mb-md no-wrap"> <VnSelect :label="t('ticketList.client')" v-model="clientId" @@ -296,7 +296,7 @@ async function getZone(options) { :rules="validate('ticketList.warehouse')" /> </VnRow> - <VnRow> + <VnRow class="row q-gutter-md q-mb-md no-wrap"> <VnSelect :label="t('basicData.address')" v-model="addressId" diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue index 89249b899..ef2eb75d6 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue @@ -1,7 +1,7 @@ <script setup> -import { ref, onBeforeMount } from 'vue'; +import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; -import { useRoute, useRouter } from 'vue-router'; +import { useRouter } from 'vue-router'; import TicketBasicData from './TicketBasicData.vue'; import TicketBasicDataForm from './TicketBasicDataForm.vue'; @@ -9,104 +9,69 @@ import { useVnConfirm } from 'composables/useVnConfirm'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; +import { useArrayData } from 'src/composables/useArrayData'; const { notify } = useNotify(); -const route = useRoute(); const router = useRouter(); const { t } = useI18n(); const stepperRef = ref(null); const { openConfirmationModal } = useVnConfirm(); const step = ref(1); -const formData = ref({}); -const initialDataLoaded = ref(false); -const haveNegatives = ref(false); +const haveNegatives = ref(true); -const ticketFilter = { - include: [ - { relation: 'address' }, - { - relation: 'client', - scope: { - fields: [ - 'salesPersonFk', - 'name', - 'isActive', - 'isFreezed', - 'isTaxDataChecked', - 'credit', - 'email', - 'phone', - 'mobile', - 'hasElectronicInvoice', - ], - include: { - relation: 'salesPersonUser', - scope: { fields: ['id', 'name'] }, - }, - }, - }, - { relation: 'invoiceOut' }, - ], -}; - -const getTicketData = async () => { - const params = { filter: JSON.stringify(ticketFilter) }; - const { data } = await axios.get(`tickets/${route.params.id}`, { params }); - formData.value = data; - initialDataLoaded.value = true; -}; +const ticket = computed(() => useArrayData('Ticket').store?.data); const isFormInvalid = () => { return ( - !formData.value.clientFk || - !formData.value.addressFk || - !formData.value.agencyModeFk || - !formData.value.companyFk || - !formData.value.shipped || - !formData.value.landed || - !formData.value.zoneFk + !ticket.value.clientFk || + !ticket.value.addressFk || + !ticket.value.agencyModeFk || + !ticket.value.companyFk || + !ticket.value.shipped || + !ticket.value.landed || + !ticket.value.zoneFk ); }; const getPriceDifference = async () => { const params = { - landed: formData.value.landed, - addressId: formData.value.addressFk, - agencyModeId: formData.value.agencyModeFk, - zoneId: formData.value.zoneFk, - warehouseId: formData.value.warehouseFk, - shipped: formData.value.shipped, + landed: ticket.value.landed, + addressId: ticket.value.addressFk, + agencyModeId: ticket.value.agencyModeFk, + zoneId: ticket.value.zoneFk, + warehouseId: ticket.value.warehouseFk, + shipped: ticket.value.shipped, }; const { data } = await axios.post( - `tickets/${formData.value.id}/priceDifference`, + `tickets/${ticket.value.id}/priceDifference`, params ); - formData.value.sale = data; + ticket.value.sale = data; }; const submit = async () => { - if (!formData.value.option) return notify(t('basicData.chooseAnOption'), 'negative'); + if (!ticket.value.option) return notify(t('basicData.chooseAnOption'), 'negative'); const params = { - clientFk: formData.value.clientFk, - nickname: formData.value.nickname, - agencyModeFk: formData.value.agencyModeFk, - addressFk: formData.value.addressFk, - zoneFk: formData.value.zoneFk, - warehouseFk: formData.value.warehouseFk, - companyFk: formData.value.companyFk, - shipped: formData.value.shipped, - landed: formData.value.landed, - isDeleted: formData.value.isDeleted, - option: formData.value.option, - isWithoutNegatives: formData.value.withoutNegatives, - withWarningAccept: formData.value.withWarningAccept, + clientFk: ticket.value.clientFk, + nickname: ticket.value.nickname, + agencyModeFk: ticket.value.agencyModeFk, + addressFk: ticket.value.addressFk, + zoneFk: ticket.value.zoneFk, + warehouseFk: ticket.value.warehouseFk, + companyFk: ticket.value.companyFk, + shipped: ticket.value.shipped, + landed: ticket.value.landed, + isDeleted: ticket.value.isDeleted, + option: ticket.value.option, + isWithoutNegatives: ticket.value.withoutNegatives, + withWarningAccept: ticket.value.withWarningAccept, keepPrice: false, }; const { data } = await axios.post( - `tickets/${formData.value.id}/componentUpdate`, + `tickets/${ticket.value.id}/componentUpdate`, params ); @@ -118,7 +83,7 @@ const submit = async () => { }; const submitWithNegatives = async () => { - formData.value.withWarningAccept = true; + ticket.value.withWarningAccept = true; submit(); }; @@ -130,7 +95,7 @@ const onNextStep = async () => { await getPriceDifference(); stepperRef.value.next(); } else if (step.value === 2) { - if (haveNegatives.value && !formData.value.withoutNegatives) + if (haveNegatives.value && !ticket.value.withoutNegatives) openConfirmationModal( t('basicData.negativesConfirmTitle'), t('basicData.negativesConfirmMessage'), @@ -139,11 +104,10 @@ const onNextStep = async () => { else submit(); } }; - -onBeforeMount(async () => await getTicketData()); </script> <template> <QStepper + v-if="ticket" v-model="step" ref="stepperRef" color="primary" @@ -155,10 +119,10 @@ onBeforeMount(async () => await getTicketData()); }" > <QStep :name="1" :title="t('globals.pageTitles.basicData')" :done="step > 1"> - <TicketBasicDataForm v-if="initialDataLoaded" v-model="formData" /> + <TicketBasicDataForm v-model="ticket" /> </QStep> <QStep :name="2" :title="t('basicData.priceDifference')"> - <TicketBasicData v-model="formData" v-model:have-negatives="haveNegatives" /> + <TicketBasicData v-model="ticket" v-model:have-negatives="haveNegatives" /> </QStep> <template #navigation> <QStepperNavigation class="flex justify-between"> diff --git a/src/pages/Ticket/Card/TicketCard.vue b/src/pages/Ticket/Card/TicketCard.vue index 6886a8e57..e22d5799a 100644 --- a/src/pages/Ticket/Card/TicketCard.vue +++ b/src/pages/Ticket/Card/TicketCard.vue @@ -1,7 +1,13 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import TicketDescriptor from './TicketDescriptor.vue'; +import filter from './TicketFilter.js'; </script> <template> - <VnCardBeta data-key="Ticket" base-url="Tickets" :descriptor="TicketDescriptor" /> + <VnCardBeta + data-key="Ticket" + url="Tickets" + :descriptor="TicketDescriptor" + :filter="filter" + /> </template> diff --git a/src/pages/Ticket/Card/TicketComponents.vue b/src/pages/Ticket/Card/TicketComponents.vue index 842607e0c..5936ffc28 100644 --- a/src/pages/Ticket/Card/TicketComponents.vue +++ b/src/pages/Ticket/Card/TicketComponents.vue @@ -19,7 +19,7 @@ import RightMenu from 'src/components/common/RightMenu.vue'; const route = useRoute(); const { t } = useI18n(); const salesRef = ref(null); -const arrayData = useArrayData('ticketData'); +const arrayData = useArrayData('Ticket'); const { store } = arrayData; const ticketData = computed(() => store.data); diff --git a/src/pages/Ticket/Card/TicketDescriptor.vue b/src/pages/Ticket/Card/TicketDescriptor.vue index c9849d631..c5f3233b1 100644 --- a/src/pages/Ticket/Card/TicketDescriptor.vue +++ b/src/pages/Ticket/Card/TicketDescriptor.vue @@ -6,9 +6,11 @@ import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy import CardDescriptor from 'components/ui/CardDescriptor.vue'; import TicketDescriptorMenu from './TicketDescriptorMenu.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import { toDateTimeFormat } from 'src/filters/date'; +import filter from './TicketFilter.js'; +import FetchData from 'src/components/FetchData.vue'; +import TicketProblems from 'src/components/TicketProblems.vue'; const $props = defineProps({ id: { @@ -28,100 +30,24 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); - -const filter = { - include: [ - { - relation: 'address', - scope: { - fields: ['id', 'name', 'mobile', 'phone', 'incotermsFk'], - }, - }, - { - relation: 'client', - scope: { - fields: [ - 'id', - 'name', - 'salesPersonFk', - 'phone', - 'mobile', - 'email', - 'isActive', - 'isFreezed', - 'isTaxDataChecked', - 'hasElectronicInvoice', - ], - include: [ - { - relation: 'user', - scope: { - fields: ['id', 'lang'], - }, - }, - { relation: 'salesPersonUser' }, - ], - }, - }, - { - relation: 'ticketState', - scope: { - include: { relation: 'state' }, - }, - }, - { - relation: 'warehouse', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'agencyMode', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'zone', - scope: { - fields: [ - 'agencyModeFk', - 'bonus', - 'hour', - 'id', - 'isVolumetric', - 'itemMaxSize', - 'm3Max', - 'name', - 'price', - 'travelingDays', - ], - }, - }, - ], -}; - -const data = ref(useCardDescription()); +const problems = ref({}); function ticketFilter(ticket) { return JSON.stringify({ clientFk: ticket.clientFk }); } - -const setData = (entity) => { - data.value = useCardDescription(entity.ref, entity.id); -}; </script> <template> + <FetchData + :url="`Tickets/${entityId}/getTicketProblems`" + auto-load + @on-fetch="(data) => ([problems] = data)" + /> <CardDescriptor - module="Ticket" :url="`Tickets/${entityId}`" :filter="filter" - :title="data.title" - :subtitle="data.subtitle" - @on-fetch="setData" + data-key="Ticket" :summary="$props.summary" - data-key="ticketData" width="lg-width" > <template #menu="{ entity }"> @@ -167,48 +93,9 @@ const setData = (entity) => { <VnLv :label="t('globals.warehouse')" :value="entity.warehouse?.name" /> <VnLv :label="t('globals.alias')" :value="entity.nickname" /> </template> - <template #icons="{ entity }"> - <QCardActions class="q-gutter-x-md"> - <QIcon - v-if="entity.client.isActive == false" - name="vn:disabled" - size="xs" - color="primary" - > - <QTooltip>{{ t('Client inactive') }}</QTooltip> - </QIcon> - <QIcon - v-if="entity.client.isFreezed == true" - name="vn:frozen" - size="xs" - color="primary" - > - <QTooltip>{{ t('Client Frozen') }}</QTooltip> - </QIcon> - <QIcon - v-if="entity?.problem?.includes('hasRisk')" - name="vn:risk" - size="xs" - color="primary" - > - <QTooltip>{{ t('Client has debt') }}</QTooltip> - </QIcon> - <QIcon - v-if="entity.client.isTaxDataChecked == false" - name="vn:no036" - size="xs" - color="primary" - > - <QTooltip>{{ t('Client not checked') }}</QTooltip> - </QIcon> - <QIcon - v-if="entity.isDeleted == true" - name="vn:deletedTicket" - size="xs" - color="primary" - > - <QTooltip>{{ t('This ticket is deleted') }}</QTooltip> - </QIcon> + <template #icons> + <QCardActions class="q-gutter-x-xs"> + <TicketProblems :row="problems" /> </QCardActions> </template> <template #actions="{ entity }"> diff --git a/src/pages/Ticket/Card/TicketExpedition.vue b/src/pages/Ticket/Card/TicketExpedition.vue index 166e86978..f8084ff2f 100644 --- a/src/pages/Ticket/Card/TicketExpedition.vue +++ b/src/pages/Ticket/Card/TicketExpedition.vue @@ -40,7 +40,7 @@ const expeditionsFilter = computed(() => ({ order: ['created DESC'], })); -const ticketArrayData = useArrayData('ticketData'); +const ticketArrayData = useArrayData('Ticket'); const ticketStore = ticketArrayData.store; const ticketData = computed(() => ticketStore.data); diff --git a/src/pages/Ticket/Card/TicketFilter.js b/src/pages/Ticket/Card/TicketFilter.js new file mode 100644 index 000000000..7846f1658 --- /dev/null +++ b/src/pages/Ticket/Card/TicketFilter.js @@ -0,0 +1,72 @@ +export default { + include: [ + { + relation: 'address', + scope: { + fields: ['id', 'name', 'mobile', 'phone', 'incotermsFk'], + }, + }, + { + relation: 'client', + scope: { + fields: [ + 'id', + 'name', + 'salesPersonFk', + 'phone', + 'mobile', + 'email', + 'isActive', + 'isFreezed', + 'isTaxDataChecked', + 'hasElectronicInvoice', + 'credit', + ], + include: [ + { + relation: 'user', + scope: { + fields: ['id', 'lang'], + }, + }, + { relation: 'salesPersonUser' }, + ], + }, + }, + { + relation: 'ticketState', + scope: { + include: { relation: 'state' }, + }, + }, + { + relation: 'warehouse', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'agencyMode', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'zone', + scope: { + fields: [ + 'agencyModeFk', + 'bonus', + 'hour', + 'id', + 'isVolumetric', + 'itemMaxSize', + 'm3Max', + 'name', + 'price', + 'travelingDays', + ], + }, + }, + ], +}; diff --git a/src/pages/Ticket/Card/TicketNotes.vue b/src/pages/Ticket/Card/TicketNotes.vue index f558b71cc..feb88bf84 100644 --- a/src/pages/Ticket/Card/TicketNotes.vue +++ b/src/pages/Ticket/Card/TicketNotes.vue @@ -32,7 +32,7 @@ watch( crudModelFilter.where.ticketFk = route.params.id; store.filter = crudModelFilter; await ticketNotesCrudRef.value.reload(); - } + }, ); function handleDelete(row) { ticketNotesCrudRef.value.remove([row]); @@ -105,7 +105,7 @@ async function handleSave() { <VnRow v-if="observationTypes.length > rows.length"> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat class="fill-icon-on-hover q-ml-md" color="primary" diff --git a/src/pages/Ticket/Card/TicketPackage.vue b/src/pages/Ticket/Card/TicketPackage.vue index 8ebdb4401..5fbf4c800 100644 --- a/src/pages/Ticket/Card/TicketPackage.vue +++ b/src/pages/Ticket/Card/TicketPackage.vue @@ -41,7 +41,7 @@ watch( crudModelFilter.where.ticketFk = route.params.id; store.filter = crudModelFilter; await ticketPackagingsCrudRef.value.reload(); - } + }, ); </script> @@ -118,7 +118,7 @@ watch( <VnRow> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat class="fill-icon-on-hover q-ml-md" color="primary" diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index f5fb50ecf..6f02a2ce6 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -14,7 +14,7 @@ import VnImg from 'src/components/ui/VnImg.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import TicketSaleMoreActions from './TicketSaleMoreActions.vue'; -import TicketTransfer from './TicketTransfer.vue'; +import TicketTransferProxy from './TicketTransferProxy.vue'; import { toCurrency, toPercentage } from 'src/filters'; import { useArrayData } from 'composables/useArrayData'; @@ -23,6 +23,7 @@ import useNotify from 'src/composables/useNotify.js'; import axios from 'axios'; import VnTable from 'src/components/VnTable/VnTable.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; +import TicketProblems from 'src/components/TicketProblems.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; const route = useRoute(); @@ -34,7 +35,7 @@ const editPriceProxyRef = ref(null); const editManaProxyRef = ref(null); const stateBtnDropdownRef = ref(null); const quasar = useQuasar(); -const arrayData = useArrayData('ticketData'); +const arrayData = useArrayData('Ticket'); const { store } = arrayData; const selectedRows = ref([]); const hasSelectedRows = computed(() => selectedRows.value.length > 0); @@ -626,8 +627,9 @@ watch( @click="setTransferParams()" data-cy="ticketSaleTransferBtn" > - <QTooltip>{{ t('Transfer lines') }}</QTooltip> - <TicketTransfer + <QTooltip>{{ t('ticketSale.transferLines') }}</QTooltip> + <TicketTransferProxy + class="full-width" :transfer="transfer" :ticket="store.data" @refresh-data="resetChanges()" @@ -697,53 +699,7 @@ watch( :disabled-attr="isTicketEditable" > <template #column-statusIcons="{ row }"> - <router-link - v-if="row.claim?.claimFk" - :to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }" - > - <QIcon color="primary" name="vn:claims" size="xs"> - <QTooltip> - {{ t('ticketSale.claim') }}: - {{ row.claim?.claimFk }} - </QTooltip> - </QIcon> - </router-link> - <QIcon v-if="row.visible < 0" color="primary" name="warning" size="xs"> - <QTooltip> - {{ t('ticketSale.visible') }}: {{ row.visible || 0 }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.reserved" - color="primary" - name="vn:reserva" - size="xs" - data-cy="ticketSaleReservedIcon" - > - <QTooltip> - {{ t('ticketSale.reserved') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.itemShortage" - color="primary" - name="vn:unavailable" - size="xs" - > - <QTooltip> - {{ t('ticketSale.noVisible') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.hasComponentLack" - color="primary" - name="vn:components" - size="xs" - > - <QTooltip> - {{ t('ticketSale.hasComponentLack') }} - </QTooltip> - </QIcon> + <TicketProblems :row="row" /> </template> <template #body-cell-picture="{ row }"> <QTd> @@ -881,7 +837,7 @@ watch( color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" data-cy="ticketSaleAddToBasketBtn" /> <QTooltip class="text-no-wrap"> diff --git a/src/pages/Ticket/Card/TicketService.vue b/src/pages/Ticket/Card/TicketService.vue index d045eadee..6ce69a6aa 100644 --- a/src/pages/Ticket/Card/TicketService.vue +++ b/src/pages/Ticket/Card/TicketService.vue @@ -40,7 +40,7 @@ watch( async () => { store.filter = crudModelFilter.value; await ticketServiceCrudRef.value.reload(); - } + }, ); onMounted(async () => await getDefaultTaxClass()); @@ -59,7 +59,7 @@ const createRefund = async () => { t('service.createRefundSuccess', { ticketId: refundTicket.id, }), - 'positive' + 'positive', ); router.push({ name: 'TicketSale', params: { id: refundTicket.id } }); }; @@ -225,7 +225,7 @@ async function handleSave() { color="primary" icon="add" @click="ticketServiceCrudRef.insert()" - shortcut="+" + v-shortcut="'+'" /> </QPageSticky> </template> diff --git a/src/pages/Ticket/Card/TicketSplit.vue b/src/pages/Ticket/Card/TicketSplit.vue new file mode 100644 index 000000000..e79057266 --- /dev/null +++ b/src/pages/Ticket/Card/TicketSplit.vue @@ -0,0 +1,37 @@ +<script setup> +import { ref } from 'vue'; + +import VnInputDate from 'src/components/common/VnInputDate.vue'; +import split from './components/split'; +const emit = defineEmits(['ticketTransfered']); + +const $props = defineProps({ + ticket: { + type: [Array, Object], + default: () => {}, + }, +}); + +const splitDate = ref(Date.vnNew()); + +const splitSelectedRows = async () => { + const tickets = Array.isArray($props.ticket) ? $props.ticket : [$props.ticket]; + await split(tickets, splitDate.value); + emit('ticketTransfered', tickets); +}; +</script> + +<template> + <VnInputDate class="q-mr-sm" :label="$t('New date')" v-model="splitDate" clearable /> + <QBtn class="q-mr-sm" color="primary" label="Split" @click="splitSelectedRows"></QBtn> +</template> +<style lang="scss"> +.q-table__bottom.row.items-center.q-table__bottom--nodata { + border-top: none; +} +</style> +<i18n> +es: + Sales to transfer: Líneas a transferir + Destination ticket: Ticket destinatario +</i18n> diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index 8cb518823..5838efa88 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -20,6 +20,7 @@ import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnToSummary from 'src/components/ui/VnToSummary.vue'; import TicketDescriptorMenu from './TicketDescriptorMenu.vue'; +import TicketProblems from 'src/components/TicketProblems.vue'; const route = useRoute(); const { notify } = useNotify(); @@ -40,7 +41,7 @@ const editableStates = ref([]); const ticketUrl = ref(); const grafanaUrl = 'https://grafana.verdnatura.es'; const stateBtnDropdownRef = ref(); -const descriptorData = useArrayData('ticketData'); +const descriptorData = useArrayData('Ticket'); onMounted(async () => { ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/'; @@ -320,83 +321,7 @@ onMounted(async () => { <template #body="props"> <QTr :props="props"> <QTd class="q-gutter-x-xs"> - <QBtn - flat - round - icon="vn:claims" - v-if="props.row.claim" - color="primary" - :to="{ - name: 'ClaimCard', - params: { - id: props.row.claim.claimFk, - }, - }" - > - <QTooltip> - {{ t('ticket.summary.claim') }}: - {{ props.row.claim.claimFk }} - </QTooltip> - </QBtn> - <QBtn - flat - round - icon="vn:claims" - v-if="props.row.claimBeginning" - color="primary" - :to="{ - name: 'ClaimCard', - params: { - id: props.row.claimBeginning.claimFk, - }, - }" - > - <QTooltip> - {{ t('ticket.summary.claim') }}: - {{ props.row.claimBeginning.claimFk }} - </QTooltip> - </QBtn> - <QIcon - name="warning" - v-show="props.row.visible < 0" - color="primary" - size="xs" - > - <QTooltip> - {{ t('globals.visible') }}: - {{ props.row.visible }} - </QTooltip> - </QIcon> - <QIcon - name="vn:reserved" - v-show="props.row.reserved" - color="primary" - size="xs" - > - <QTooltip> - {{ t('ticket.summary.reserved') }} - </QTooltip> - </QIcon> - <QIcon - name="vn:unavailable" - v-show="props.row.itemShortage" - color="primary" - size="xs" - > - <QTooltip> - {{ t('ticket.summary.itemShortage') }} - </QTooltip> - </QIcon> - <QIcon - name="vn:components" - v-show="props.row.hasComponentLack" - color="primary" - size="xs" - > - <QTooltip> - {{ t('ticket.summary.hasComponentLack') }} - </QTooltip> - </QIcon> + <TicketProblems :row="props.row" /> </QTd> <QTd> <QBtn class="link" flat> diff --git a/src/pages/Ticket/Card/TicketTracking.vue b/src/pages/Ticket/Card/TicketTracking.vue index f4b8544d3..acf464fb1 100644 --- a/src/pages/Ticket/Card/TicketTracking.vue +++ b/src/pages/Ticket/Card/TicketTracking.vue @@ -19,7 +19,7 @@ watch( async (val) => { paginateFilter.where.ticketFk = val; paginateRef.value.fetch(); - } + }, ); const paginateFilter = reactive({ @@ -119,7 +119,7 @@ const openCreateModal = () => createTrackingDialogRef.value.show(); color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip class="text-no-wrap"> {{ t('tracking.addState') }} diff --git a/src/pages/Ticket/Card/TicketTransfer.vue b/src/pages/Ticket/Card/TicketTransfer.vue index 005d74a0e..ffa964c92 100644 --- a/src/pages/Ticket/Card/TicketTransfer.vue +++ b/src/pages/Ticket/Card/TicketTransfer.vue @@ -1,11 +1,11 @@ <script setup> import { ref, computed, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; - import VnInput from 'src/components/common/VnInput.vue'; import TicketTransferForm from './TicketTransferForm.vue'; import { toDateFormat } from 'src/filters/date.js'; +const emit = defineEmits(['ticketTransfered']); const $props = defineProps({ mana: { @@ -21,16 +21,15 @@ const $props = defineProps({ default: () => {}, }, ticket: { - type: Object, + type: [Array, Object], default: () => {}, }, }); +onMounted(() => (_transfer.value = $props.transfer)); const { t } = useI18n(); -const QPopupProxyRef = ref(null); const transferFormRef = ref(null); const _transfer = ref(); - const transferLinesColumns = computed(() => [ { label: t('ticketList.id'), @@ -86,76 +85,74 @@ const handleRowClick = (row) => { transferFormRef.value.transferSales(ticketId); } }; - -onMounted(() => (_transfer.value = $props.transfer)); </script> <template> - <QPopupProxy ref="QPopupProxyRef" data-cy="ticketTransferPopup"> - <QCard class="q-px-md" style="display: flex; width: 80vw"> - <QTable - :rows="transfer.sales" - :columns="transferLinesColumns" - :title="t('Sales to transfer')" - row-key="id" - :pagination="{ rowsPerPage: 0 }" - class="full-width q-mt-md" - :no-data-label="t('globals.noResults')" - > - <template #body-cell-quantity="{ row }"> - <QTd @click.stop> - <VnInput - v-model.number="row.quantity" - :clearable="false" - style="max-width: 60px" - /> - </QTd> - </template> - </QTable> - <QSeparator vertical spaced /> - <QTable - v-if="transfer.lastActiveTickets" - :rows="transfer.lastActiveTickets" - :columns="destinationTicketColumns" - :title="t('Destination ticket')" - row-key="id" - class="full-width q-mt-md" - @row-click="(_, row) => handleRowClick(row)" - > - <template #body-cell-address="{ row }"> - <QTd @click.stop> - <span> - {{ row.nickname }} - {{ row.name }} - {{ row.street }} - {{ row.postalCode }} - {{ row.city }} - </span> - <QTooltip> - {{ row.nickname }} - {{ row.name }} - {{ row.street }} - {{ row.postalCode }} - {{ row.city }} - </QTooltip> - </QTd> - </template> + <QTable + :rows="transfer.sales" + :columns="transferLinesColumns" + :title="t('Sales to transfer')" + row-key="id" + :pagination="{ rowsPerPage: 0 }" + class="full-width q-mt-md" + :no-data-label="t('globals.noResults')" + > + <template #body-cell-quantity="{ row }"> + <QTd @click.stop> + <VnInput + v-model.number="row.quantity" + :clearable="false" + style="max-width: 60px" + /> + </QTd> + </template> + </QTable> + <QSeparator vertical spaced /> + <QTable + v-if="transfer.lastActiveTickets" + :rows="transfer.lastActiveTickets" + :columns="destinationTicketColumns" + :title="t('Destination ticket')" + row-key="id" + class="full-width q-mt-md" + @row-click="(_, row) => handleRowClick(row)" + :no-data-label="t('globals.noResults')" + :pagination="{ rowsPerPage: 0 }" + > + <template #body-cell-address="{ row }"> + <QTd @click.stop> + <span> + {{ row.nickname }} + {{ row.name }} + {{ row.street }} + {{ row.postalCode }} + {{ row.city }} + </span> + <QTooltip> + {{ row.nickname }} + {{ row.name }} + {{ row.street }} + {{ row.postalCode }} + {{ row.city }} + </QTooltip> + </QTd> + </template> - <template #no-data> - <TicketTransferForm ref="transferFormRef" v-bind="$props" /> - </template> - <template #bottom> - <TicketTransferForm ref="transferFormRef" v-bind="$props" /> - </template> - </QTable> - </QCard> - </QPopupProxy> + <template #no-data> + <TicketTransferForm ref="transferFormRef" v-bind="$props" /> + </template> + <template #bottom> + <TicketTransferForm ref="transferFormRef" v-bind="$props" /> + </template> + </QTable> </template> - +<style lang="scss"> +.q-table__bottom.row.items-center.q-table__bottom--nodata { + border-top: none; +} +</style> <i18n> es: Sales to transfer: Líneas a transferir Destination ticket: Ticket destinatario - Transfer to ticket: Transferir a ticket - New ticket: Nuevo ticket </i18n> diff --git a/src/pages/Ticket/Card/TicketTransferProxy.vue b/src/pages/Ticket/Card/TicketTransferProxy.vue new file mode 100644 index 000000000..3f3f018df --- /dev/null +++ b/src/pages/Ticket/Card/TicketTransferProxy.vue @@ -0,0 +1,54 @@ +<script setup> +import { ref } from 'vue'; +import TicketTransfer from './TicketTransfer.vue'; +import Split from './TicketSplit.vue'; +const emit = defineEmits(['ticketTransfered']); + +const $props = defineProps({ + mana: { + type: Number, + default: null, + }, + newPrice: { + type: Number, + default: 0, + }, + transfer: { + type: Object, + default: () => {}, + }, + ticket: { + type: [Array, Object], + default: () => {}, + }, + split: { + type: Boolean, + default: false, + }, +}); + +const popupProxyRef = ref(null); +const splitRef = ref(null); +const transferRef = ref(null); +</script> + +<template> + <QPopupProxy ref="popupProxyRef" data-cy="ticketTransferPopup"> + <div class="flex row items-center q-ma-lg" v-if="$props.split"> + <Split + ref="splitRef" + @splitSelectedRows="splitSelectedRows" + :ticket="$props.ticket" + /> + </div> + + <div v-else> + <TicketTransfer + ref="transferRef" + :ticket="$props.ticket" + :sales="$props.sales" + :transfer="$props.transfer" + /> + </div> + </QPopupProxy> +</template> diff --git a/src/pages/Ticket/Card/components/split.js b/src/pages/Ticket/Card/components/split.js new file mode 100644 index 000000000..afa1d5cd6 --- /dev/null +++ b/src/pages/Ticket/Card/components/split.js @@ -0,0 +1,22 @@ +import axios from 'axios'; +import notifyResults from 'src/utils/notifyResults'; + +export default async function (data, date) { + const reducedData = data.reduce((acc, item) => { + const existing = acc.find(({ ticketFk }) => ticketFk === item.id); + if (existing) { + existing.sales.push(item.saleFk); + } else { + acc.push({ ticketFk: item.id, sales: [item.saleFk], date }); + } + return acc; + }, []); + + const promises = reducedData.map((params) => axios.post(`Tickets/split`, params)); + + const results = await Promise.allSettled(promises); + + notifyResults(results, 'ticketFk'); + + return results; +} diff --git a/src/pages/Ticket/Negative/TicketLackDetail.vue b/src/pages/Ticket/Negative/TicketLackDetail.vue new file mode 100644 index 000000000..dcf835d03 --- /dev/null +++ b/src/pages/Ticket/Negative/TicketLackDetail.vue @@ -0,0 +1,198 @@ +<script setup> +import { computed, onMounted, onUnmounted, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import ChangeQuantityDialog from './components/ChangeQuantityDialog.vue'; +import ChangeStateDialog from './components/ChangeStateDialog.vue'; +import ChangeItemDialog from './components/ChangeItemDialog.vue'; +import TicketTransferProxy from '../Card/TicketTransferProxy.vue'; +import FetchData from 'src/components/FetchData.vue'; +import { useStateStore } from 'stores/useStateStore'; +import { useState } from 'src/composables/useState'; + +import { useRoute } from 'vue-router'; +import TicketLackTable from './TicketLackTable.vue'; +import VnPopupProxy from 'src/components/common/VnPopupProxy.vue'; +import ItemProposalProxy from 'src/pages/Item/components/ItemProposalProxy.vue'; + +import { useQuasar } from 'quasar'; +const quasar = useQuasar(); +const { t } = useI18n(); +const editableStates = ref([]); +const stateStore = useStateStore(); +const tableRef = ref(); +const changeItemDialogRef = ref(null); +const changeStateDialogRef = ref(null); +const changeQuantityDialogRef = ref(null); +const showProposalDialog = ref(false); +const showChangeQuantityDialog = ref(false); +const selectedRows = ref([]); +const route = useRoute(); +onMounted(() => { + stateStore.rightDrawer = false; +}); +onUnmounted(() => { + stateStore.rightDrawer = true; +}); + +const entityId = computed(() => route.params.id); +const item = ref({}); + +const itemProposalSelected = ref(null); +const reload = async () => { + tableRef.value.tableRef.reload(); +}; +defineExpose({ reload }); +const filter = computed(() => ({ + scopeDays: route.query.days, + showType: true, + alertLevelCode: 'FREE', + date: Date.vnNew(), + warehouseFk: useState().getUser().value.warehouseFk, +})); +const itemProposalEvt = (data) => { + const { itemProposal } = data; + itemProposalSelected.value = itemProposal; + reload(); +}; + +function onBuysFetched(data) { + Object.assign(item.value, data[0]); +} +const showItemProposal = () => { + quasar + .dialog({ + component: ItemProposalProxy, + componentProps: { + itemLack: tableRef.value.itemLack, + replaceAction: true, + sales: selectedRows.value, + }, + }) + .onOk(itemProposalEvt); +}; +</script> + +<template> + <FetchData + url="States/editableStates" + @on-fetch="(data) => (editableStates = data)" + auto-load + /> + <FetchData + :url="`Items/${entityId}/getCard`" + :fields="['longName']" + @on-fetch="(data) => (item = data)" + auto-load + /> + <FetchData + :url="`Buys/latestBuysFilter`" + :fields="['longName']" + :filter="{ where: { 'i.id': entityId } }" + @on-fetch="onBuysFetched" + auto-load + /> + + <TicketLackTable + ref="tableRef" + :filter="filter" + @update:selection="({ value }, _) => (selectedRows = value)" + > + <template #top-right> + <QBtnGroup push class="q-mr-lg" style="column-gap: 1px"> + <QBtn + data-cy="transferLines" + color="primary" + :disable="!(selectedRows.length === 1)" + > + <template #default> + <QIcon name="vn:splitline" /> + <QIcon name="vn:ticket" /> + + <QTooltip>{{ t('ticketSale.transferLines') }} </QTooltip> + <TicketTransferProxy + ref="transferFormRef" + split="true" + :ticket="selectedRows" + :transfer="{ + sales: selectedRows, + lastActiveTickets: selectedRows.map((row) => row.id), + }" + @ticket-transfered="reload" + ></TicketTransferProxy> + </template> + </QBtn> + <QBtn + color="primary" + @click="showProposalDialog = true" + :disable="selectedRows.length < 1" + data-cy="itemProposal" + > + <QIcon + name="import_export" + class="rotate-90" + @click="showItemProposal" + ></QIcon> + <QTooltip bottom anchor="bottom right"> + {{ t('itemProposal') }} + </QTooltip> + </QBtn> + <VnPopupProxy + data-cy="changeItem" + icon="sync" + :disable="selectedRows.length < 1" + :tooltip="t('negative.detail.modal.changeItem.title')" + > + <template #extraIcon> <QIcon name="vn:item" /> </template> + <template v-slot="{ popup }"> + <ChangeItemDialog + ref="changeItemDialogRef" + @update-item="popup.hide()" + :selected-rows="selectedRows" + /></template> + </VnPopupProxy> + <VnPopupProxy + data-cy="changeState" + icon="sync" + :disable="selectedRows.length < 1" + :tooltip="t('negative.detail.modal.changeState.title')" + > + <template #extraIcon> <QIcon name="vn:eye" /> </template> + <template v-slot="{ popup }"> + <ChangeStateDialog + ref="changeStateDialogRef" + @update-state="popup.hide()" + :selected-rows="selectedRows" + /></template> + </VnPopupProxy> + <VnPopupProxy + data-cy="changeQuantity" + icon="sync" + :disable="selectedRows.length < 1" + :tooltip="t('negative.detail.modal.changeQuantity.title')" + @click="showChangeQuantityDialog = true" + > + <template #extraIcon> <QIcon name="exposure" /> </template> + <template v-slot="{ popup }"> + <ChangeQuantityDialog + ref="changeQuantityDialogRef" + @update-quantity="popup.hide()" + :selected-rows="selectedRows" + /></template> + </VnPopupProxy> </QBtnGroup + ></template> + </TicketLackTable> +</template> +<style lang="scss" scoped> +.list-enter-active, +.list-leave-active { + transition: all 1s ease; +} +.list-enter-from, +.list-leave-to { + opacity: 0; + background-color: $primary; +} +.q-table.q-table__container > div:first-child { + border-radius: unset; +} +</style> diff --git a/src/pages/Ticket/Negative/TicketLackFilter.vue b/src/pages/Ticket/Negative/TicketLackFilter.vue new file mode 100644 index 000000000..3762f453d --- /dev/null +++ b/src/pages/Ticket/Negative/TicketLackFilter.vue @@ -0,0 +1,175 @@ +<script setup> +import { ref } from 'vue'; +import { useI18n } from 'vue-i18n'; + +import FetchData from 'components/FetchData.vue'; +import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +const { t } = useI18n(); +const props = defineProps({ + dataKey: { + type: String, + required: true, + }, +}); + +const to = Date.vnNew(); +to.setDate(to.getDate() + 1); + +const warehouses = ref(); +const categoriesOptions = ref([]); +const itemTypesRef = ref(null); +const itemTypesOptions = ref([]); + +const itemTypesFilter = { + fields: ['id', 'name', 'categoryFk'], + include: 'category', + order: 'name ASC', + where: {}, +}; +const onCategoryChange = async (categoryFk, search) => { + if (!categoryFk) { + itemTypesFilter.where.categoryFk = null; + delete itemTypesFilter.where.categoryFk; + } else { + itemTypesFilter.where.categoryFk = categoryFk; + } + search(); + await itemTypesRef.value.fetch(); +}; +const emit = defineEmits(['set-user-params']); + +const setUserParams = (params) => { + emit('set-user-params', params); +}; +</script> + +<template> + <FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load /> + <FetchData + url="ItemCategories" + :filter="{ fields: ['id', 'name'], order: 'name ASC' }" + @on-fetch="(data) => (categoriesOptions = data)" + auto-load + /> + + <FetchData + ref="itemTypesRef" + url="ItemTypes" + :filter="itemTypesFilter" + @on-fetch="(data) => (itemTypesOptions = data)" + auto-load + /> + + <VnFilterPanel + :data-key="props.dataKey" + :search-button="true" + @set-user-params="setUserParams" + > + <template #tags="{ tag, formatFn }"> + <div class="q-gutter-x-xs"> + <strong>{{ t(`negative.${tag.label}`) }}</strong> + <span>{{ formatFn(tag.value) }}</span> + </div> + </template> + <template #body="{ params, searchFn }"> + <QList dense class="q-gutter-y-sm q-mt-sm"> + <QItem> + <QItemSection> + <VnInput + v-model="params.days" + :label="t('negative.days')" + dense + is-outlined + type="number" + @update:model-value=" + (value) => { + setUserParams(params); + } + " + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.id" + :label="t('negative.id')" + dense + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.producer" + :label="t('negative.producer')" + dense + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.origen" + :label="t('negative.origen')" + dense + is-outlined + /> + </QItemSection> </QItem + ><QItem> + <QItemSection v-if="categoriesOptions"> + <VnSelect + :label="t('negative.categoryFk')" + v-model="params.categoryFk" + @update:model-value=" + ($event) => onCategoryChange($event, searchFn) + " + :options="categoriesOptions" + option-value="id" + option-label="name" + hide-selected + dense + outlined + rounded + /> </QItemSection + ><QItemSection v-else> + <QSkeleton class="full-width" type="QSelect" /> + </QItemSection> + </QItem> + <QItem> + <QItemSection v-if="itemTypesOptions"> + <VnSelect + :label="t('negative.type')" + v-model="params.typeFk" + @update:model-value="searchFn()" + :options="itemTypesOptions" + option-value="id" + option-label="name" + hide-selected + dense + outlined + rounded + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel>{{ scope.opt?.name }}</QItemLabel> + <QItemLabel caption>{{ + scope.opt?.category?.name + }}</QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> </QItemSection + ><QItemSection v-else> + <QSkeleton class="full-width" type="QSelect" /> + </QItemSection> + </QItem> + </QList> + </template> + </VnFilterPanel> +</template> diff --git a/src/pages/Ticket/Negative/TicketLackList.vue b/src/pages/Ticket/Negative/TicketLackList.vue new file mode 100644 index 000000000..d1e8b823a --- /dev/null +++ b/src/pages/Ticket/Negative/TicketLackList.vue @@ -0,0 +1,227 @@ +<script setup> +import { computed, ref, reactive } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useStateStore } from 'stores/useStateStore'; +import VnTable from 'components/VnTable/VnTable.vue'; +import { onBeforeMount } from 'vue'; +import { dashIfEmpty, toDate, toHour } from 'src/filters'; +import { useRouter } from 'vue-router'; +import { useState } from 'src/composables/useState'; +import { useRole } from 'src/composables/useRole'; +import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; +import RightMenu from 'src/components/common/RightMenu.vue'; +import TicketLackFilter from './TicketLackFilter.vue'; +onBeforeMount(() => { + stateStore.$state.rightDrawer = true; +}); +const router = useRouter(); +const stateStore = useStateStore(); +const { t } = useI18n(); +const selectedRows = ref([]); +const tableRef = ref(); +const filterParams = ref({}); +const negativeParams = reactive({ + days: useRole().likeAny('buyer') ? 2 : 0, + warehouseFk: useState().getUser().value.warehouseFk, +}); +const redirectToCreateView = ({ itemFk }) => { + router.push({ + name: 'NegativeDetail', + params: { id: itemFk }, + query: { days: filterParams.value.days ?? negativeParams.days }, + }); +}; +const columns = computed(() => [ + { + name: 'date', + align: 'center', + label: t('negative.date'), + format: ({ timed }) => toDate(timed), + sortable: true, + cardVisible: true, + isId: true, + columnFilter: { + component: 'date', + }, + }, + { + columnClass: 'shrink', + name: 'timed', + align: 'center', + label: t('negative.timed'), + format: ({ timed }) => toHour(timed), + sortable: true, + cardVisible: true, + columnFilter: { + component: 'time', + }, + }, + { + name: 'itemFk', + align: 'center', + label: t('negative.id'), + format: ({ itemFk }) => itemFk, + sortable: true, + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + }, + { + name: 'longName', + align: 'center', + label: t('negative.longName'), + field: ({ longName }) => longName, + + sortable: true, + headerStyle: 'width: 350px', + cardVisible: true, + columnClass: 'expand', + }, + { + name: 'producer', + align: 'center', + label: t('negative.supplier'), + field: ({ producer }) => dashIfEmpty(producer), + sortable: true, + columnClass: 'shrink', + }, + { + name: 'inkFk', + align: 'center', + label: t('negative.colour'), + field: ({ inkFk }) => inkFk, + sortable: true, + cardVisible: true, + }, + { + name: 'size', + align: 'center', + label: t('negative.size'), + field: ({ size }) => size, + sortable: true, + cardVisible: true, + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + }, + { + name: 'category', + align: 'center', + label: t('negative.origen'), + field: ({ category }) => dashIfEmpty(category), + sortable: true, + cardVisible: true, + }, + { + name: 'lack', + align: 'center', + label: t('negative.lack'), + field: ({ lack }) => lack, + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + sortable: true, + headerStyle: 'padding-center: 33px', + cardVisible: true, + }, + { + name: 'tableActions', + align: 'center', + actions: [ + { + title: t('Open details'), + icon: 'edit', + action: redirectToCreateView, + isPrimary: true, + }, + ], + }, +]); + +const setUserParams = (params) => { + filterParams.value = params; +}; +</script> + +<template> + <RightMenu> + <template #right-panel> + <TicketLackFilter data-key="NegativeList" @set-user-params="setUserParams" /> + </template> + </RightMenu> + {{ filterRef }} + <VnTable + ref="tableRef" + data-key="NegativeList" + :url="`Tickets/itemLack`" + :order="['itemFk DESC, date DESC, timed DESC']" + :user-params="negativeParams" + auto-load + :columns="columns" + default-mode="table" + :right-search="false" + :is-editable="false" + :use-model="true" + :map-key="false" + :row-click="redirectToCreateView" + v-model:selected="selectedRows" + :create="false" + :crud-model="{ + disableInfiniteScroll: true, + }" + :table="{ + 'row-key': 'itemFk', + selection: 'multiple', + }" + > + <template #column-itemFk="{ row }"> + <div + style="display: flex; justify-content: space-around; align-items: center" + > + <span @click.stop>{{ row.itemFk }}</span> + </div> + </template> + <template #column-longName="{ row }"> + <span class="link" @click.stop> + {{ row.longName }} + <ItemDescriptorProxy :id="row.itemFk" /> + </span> + </template> + </VnTable> +</template> + +<style lang="scss" scoped> +.list { + max-height: 100%; + padding: 15px; + width: 100%; +} + +.grid-style-transition { + transition: + transform 0.28s, + background-color 0.28s; +} + +#true { + background-color: $positive; +} + +#false { + background-color: $negative; +} + +div.q-dialog__inner > div { + max-width: fit-content !important; +} +.q-btn-group > .q-btn-item:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +</style> diff --git a/src/pages/Ticket/Negative/TicketLackTable.vue b/src/pages/Ticket/Negative/TicketLackTable.vue new file mode 100644 index 000000000..176e8f7ad --- /dev/null +++ b/src/pages/Ticket/Negative/TicketLackTable.vue @@ -0,0 +1,356 @@ +<script setup> +import FetchedTags from 'components/ui/FetchedTags.vue'; +import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; +import { computed, ref, watch } from 'vue'; +import { useI18n } from 'vue-i18n'; +import axios from 'axios'; +import FetchData from 'src/components/FetchData.vue'; +import { toDate, toHour } from 'src/filters'; +import useNotify from 'src/composables/useNotify.js'; +import ZoneDescriptorProxy from 'pages/Zone/Card/ZoneDescriptorProxy.vue'; +import { useRoute } from 'vue-router'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import TicketDescriptorProxy from '../Card/TicketDescriptorProxy.vue'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; + +const $props = defineProps({ + filter: { + type: Object, + default: () => ({}), + }, +}); + +watch( + () => $props.filter, + (v) => { + filterLack.value.where = v; + tableRef.value.reload(filterLack); + }, +); + +const filterLack = ref({ + include: [ + { + relation: 'workers', + scope: { + fields: ['id', 'firstName'], + }, + }, + ], + where: { ...$props.filter }, + order: 'ts.alertLevelCode ASC', +}); + +const selectedRows = ref([]); +const { t } = useI18n(); +const { notify } = useNotify(); +const entityId = computed(() => route.params.id); +const item = ref({}); +const route = useRoute(); +const columns = computed(() => [ + { + name: 'status', + align: 'center', + sortable: false, + columnClass: 'shrink', + columnFilter: false, + }, + { + name: 'ticketFk', + label: t('negative.detail.ticketFk'), + align: 'center', + sortable: true, + columnFilter: { + component: 'input', + type: 'number', + }, + }, + { + name: 'shipped', + label: t('negative.detail.shipped'), + field: 'shipped', + align: 'center', + format: ({ shipped }) => toDate(shipped), + sortable: true, + columnFilter: { + component: 'date', + columnClass: 'shrink', + }, + }, + { + name: 'minTimed', + label: t('negative.detail.theoreticalhour'), + field: 'minTimed', + align: 'center', + sortable: true, + component: 'time', + columnFilter: {}, + }, + { + name: 'alertLevelCode', + label: t('negative.detail.state'), + columnFilter: { + name: 'alertLevelCode', + component: 'select', + attrs: { + url: 'AlertLevels', + fields: ['name', 'code'], + optionLabel: 'code', + optionValue: 'code', + }, + }, + align: 'center', + sortable: true, + }, + { + name: 'zoneName', + label: t('negative.detail.zoneName'), + field: 'zoneName', + align: 'center', + sortable: true, + }, + { + name: 'nickname', + label: t('negative.detail.nickname'), + field: 'nickname', + align: 'center', + sortable: true, + }, + { + name: 'quantity', + label: t('negative.detail.quantity'), + field: 'quantity', + sortable: true, + component: 'input', + type: 'number', + }, +]); + +const emit = defineEmits(['update:selection']); +const itemLack = ref(null); +const fetchItemLack = ref(null); +const tableRef = ref(null); +defineExpose({ tableRef, itemLack }); +watch(selectedRows, () => emit('update:selection', selectedRows)); +const getInputEvents = ({ col, ...rows }) => ({ + 'update:modelValue': () => saveChange(col.name, rows), + 'keyup.enter': () => saveChange(col.name, rows), +}); +const saveChange = async (field, { row }) => { + try { + switch (field) { + case 'alertLevelCode': + await axios.post(`Tickets/state`, { + ticketFk: row.ticketFk, + code: row[field], + }); + break; + + case 'quantity': + await axios.post(`Sales/${row.saleFk}/updateQuantity`, { + quantity: +row.quantity, + }); + break; + } + notify('globals.dataSaved', 'positive'); + fetchItemLack.value.fetch(); + } catch (err) { + console.error('Error saving changes', err); + f; + } +}; + +function onBuysFetched(data) { + Object.assign(item.value, data[0]); +} +</script> + +<template> + <FetchData + ref="fetchItemLack" + :url="`Tickets/itemLack`" + :params="{ id: entityId }" + @on-fetch="(data) => (itemLack = data[0])" + auto-load + /> + <FetchData + :url="`Items/${entityId}/getCard`" + :fields="['longName']" + @on-fetch="(data) => (item = data)" + auto-load + /> + <FetchData + :url="`Buys/latestBuysFilter`" + :fields="['longName']" + :filter="{ where: { 'i.id': entityId } }" + @on-fetch="onBuysFetched" + auto-load + /> + <VnTable + ref="tableRef" + data-key="NegativeItem" + :map-key="false" + :url="`Tickets/itemLack/${entityId}`" + :columns="columns" + auto-load + :create="false" + :create-as-dialog="false" + :use-model="true" + :filter="filterLack" + :order="['ts.alertLevelCode ASC']" + :table="{ + 'row-key': 'id', + selection: 'multiple', + }" + dense + :is-editable="true" + :row-click="false" + :right-search="false" + :right-search-icon="false" + v-model:selected="selectedRows" + :disable-option="{ card: true }" + > + <template #top-left> + <div style="display: flex; align-items: center" v-if="itemLack"> + <!-- <VnImg :id="itemLack.itemFk" class="rounded image-wrapper"></VnImg> --> + <div class="flex column" style="align-items: center"> + <QBadge + ref="badgeLackRef" + class="q-ml-xs" + text-color="white" + :color="itemLack.lack === 0 ? 'positive' : 'negative'" + :label="itemLack.lack" + /> + </div> + <div class="flex column left" style="align-items: flex-start"> + <QBtn flat class="link text-blue"> + {{ item?.longName ?? item.name }} + <ItemDescriptorProxy :id="entityId" /> + <FetchedTags class="q-ml-md" :item="item" :columns="7" /> + </QBtn> + </div> + </div> + </template> + <template #top-right> + <slot name="top-right" /> + </template> + + <template #column-status="{ row }"> + <QTd style="min-width: 150px"> + <div class="icon-container"> + <QIcon + v-if="row.isBasket" + name="vn:basket" + color="primary" + class="cursor-pointer" + size="xs" + > + <QTooltip>{{ t('negative.detail.isBasket') }}</QTooltip> + </QIcon> + <QIcon + v-if="row.hasToIgnore" + name="star" + color="primary" + class="cursor-pointer fill-icon" + size="xs" + > + <QTooltip>{{ t('negative.detail.hasToIgnore') }}</QTooltip> + </QIcon> + <QIcon + v-if="row.hasObservation" + name="change_circle" + color="primary" + class="cursor-pointer" + size="xs" + > + <QTooltip>{{ + t('negative.detail.hasObservation') + }}</QTooltip> </QIcon + ><QIcon + v-if="row.isRookie" + name="vn:Person" + size="xs" + color="primary" + class="cursor-pointer" + > + <QTooltip>{{ t('negative.detail.isRookie') }}</QTooltip> + </QIcon> + <QIcon + v-if="row.peticionCompra" + name="vn:buyrequest" + size="xs" + color="primary" + class="cursor-pointer" + > + <QTooltip>{{ t('negative.detail.peticionCompra') }}</QTooltip> + </QIcon> + <QIcon + v-if="row.turno" + name="vn:calendar" + size="xs" + color="primary" + class="cursor-pointer" + > + <QTooltip>{{ t('negative.detail.turno') }}</QTooltip> + </QIcon> + </div></QTd + > + </template> + <template #column-nickname="{ row }"> + <span class="link" @click.stop> + {{ row.nickname }} + <CustomerDescriptorProxy :id="row.customerId" /> + </span> + </template> + <template #column-ticketFk="{ row }"> + <span class="q-pa-sm link"> + {{ row.id }} + <TicketDescriptorProxy :id="row.id" /> + </span> + </template> + <template #column-alertLevelCode="props"> + <VnSelect + url="States/editableStates" + auto-load + hide-selected + option-value="id" + option-label="name" + v-model="props.row.alertLevelCode" + v-on="getInputEvents(props)" + /> + </template> + + <template #column-zoneName="{ row }"> + <span class="link">{{ row.zoneName }}</span> + <ZoneDescriptorProxy :id="row.zoneFk" /> + </template> + <template #column-quantity="props"> + <VnInputNumber + v-model.number="props.row.quantity" + v-on="getInputEvents(props)" + ></VnInputNumber> + </template> + </VnTable> +</template> +<style lang="scss" scoped> +.icon-container { + display: grid; + grid-template-columns: repeat(3, 0.2fr); + row-gap: 5px; /* Ajusta el espacio entre los iconos según sea necesario */ +} +.icon-container > * { + width: 100%; + height: auto; +} +.list-enter-active, +.list-leave-active { + transition: all 1s ease; +} +.list-enter-from, +.list-leave-to { + opacity: 0; + background-color: $primary; +} +</style> diff --git a/src/pages/Ticket/Negative/components/ChangeItemDialog.vue b/src/pages/Ticket/Negative/components/ChangeItemDialog.vue new file mode 100644 index 000000000..e419b85c0 --- /dev/null +++ b/src/pages/Ticket/Negative/components/ChangeItemDialog.vue @@ -0,0 +1,90 @@ +<script setup> +import { ref } from 'vue'; +import axios from 'axios'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import notifyResults from 'src/utils/notifyResults'; +const emit = defineEmits(['update-item']); + +const showChangeItemDialog = ref(false); +const newItem = ref(null); +const $props = defineProps({ + selectedRows: { + type: Array, + default: () => [], + }, +}); + +const updateItem = async () => { + try { + showChangeItemDialog.value = true; + const rowsToUpdate = $props.selectedRows.map(({ saleFk, quantity }) => + axios.post(`Sales/replaceItem`, { + saleFk, + substitutionFk: newItem.value, + quantity, + }), + ); + const result = await Promise.allSettled(rowsToUpdate); + notifyResults(result, 'saleFk'); + emit('update-item', newItem.value); + } catch (err) { + console.error('Error updating item:', err); + return err; + } +}; +</script> + +<template> + <QCard class="q-pa-sm"> + <QCardSection class="row items-center justify-center column items-stretch"> + {{ showChangeItemDialog }} + <span>{{ $t('negative.detail.modal.changeItem.title') }}</span> + <VnSelect + url="Items/WithName" + :fields="['id', 'name']" + :sort-by="['id DESC']" + :options="items" + option-label="name" + option-value="id" + v-model="newItem" + > + </VnSelect> + </QCardSection> + <QCardActions align="right"> + <QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup /> + <QBtn + :label="$t('globals.confirm')" + color="primary" + :disable="!newItem" + @click="updateItem" + unelevated + autofocus + /> </QCardActions + ></QCard> +</template> + +<style lang="scss" scoped> +.list { + max-height: 100%; + padding: 15px; + width: 100%; +} + +.grid-style-transition { + transition: + transform 0.28s, + background-color 0.28s; +} + +#true { + background-color: $positive; +} + +#false { + background-color: $negative; +} + +div.q-dialog__inner > div { + max-width: fit-content !important; +} +</style> diff --git a/src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue b/src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue new file mode 100644 index 000000000..2e9aac4f0 --- /dev/null +++ b/src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue @@ -0,0 +1,84 @@ +<script setup> +import { ref } from 'vue'; +import axios from 'axios'; +import VnInput from 'src/components/common/VnInput.vue'; +import notifyResults from 'src/utils/notifyResults'; + +const showChangeQuantityDialog = ref(false); +const newQuantity = ref(null); +const $props = defineProps({ + selectedRows: { + type: Array, + default: () => [], + }, +}); +const emit = defineEmits(['update-quantity']); +const updateQuantity = async () => { + try { + showChangeQuantityDialog.value = true; + const rowsToUpdate = $props.selectedRows.map(({ saleFk }) => + axios.post(`Sales/${saleFk}/updateQuantity`, { + saleFk, + quantity: +newQuantity.value, + }), + ); + + const result = await Promise.allSettled(rowsToUpdate); + notifyResults(result, 'saleFk'); + + emit('update-quantity', newQuantity.value); + } catch (err) { + return err; + } +}; +</script> + +<template> + <QCard class="q-pa-sm"> + <QCardSection class="row items-center justify-center column items-stretch"> + <span>{{ $t('negative.detail.modal.changeQuantity.title') }}</span> + <VnInput + type="number" + :min="0" + :label="$t('negative.detail.modal.changeQuantity.placeholder')" + v-model="newQuantity" + /> + </QCardSection> + <QCardActions align="right"> + <QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup /> + <QBtn + :label="$t('globals.confirm')" + color="primary" + :disable="!newQuantity || newQuantity < 0" + @click="updateQuantity" + unelevated + autofocus + /> </QCardActions + ></QCard> +</template> + +<style lang="scss" scoped> +.list { + max-height: 100%; + padding: 15px; + width: 100%; +} + +.grid-style-transition { + transition: + transform 0.28s, + background-color 0.28s; +} + +#true { + background-color: $positive; +} + +#false { + background-color: $negative; +} + +div.q-dialog__inner > div { + max-width: fit-content !important; +} +</style> diff --git a/src/pages/Ticket/Negative/components/ChangeStateDialog.vue b/src/pages/Ticket/Negative/components/ChangeStateDialog.vue new file mode 100644 index 000000000..1acc7e0ef --- /dev/null +++ b/src/pages/Ticket/Negative/components/ChangeStateDialog.vue @@ -0,0 +1,91 @@ +<script setup> +import { ref } from 'vue'; +import axios from 'axios'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import FetchData from 'components/FetchData.vue'; +import notifyResults from 'src/utils/notifyResults'; + +const emit = defineEmits(['update-state']); +const editableStates = ref([]); +const showChangeStateDialog = ref(false); +const newState = ref(null); +const $props = defineProps({ + selectedRows: { + type: Array, + default: () => [], + }, +}); +const updateState = async () => { + try { + showChangeStateDialog.value = true; + const rowsToUpdate = $props.selectedRows.map(({ id }) => + axios.post(`Tickets/state`, { + ticketFk: id, + code: newState.value, + }), + ); + const result = await Promise.allSettled(rowsToUpdate); + notifyResults(result, 'ticketFk'); + + emit('update-state', newState.value); + } catch (err) { + return err; + } +}; +</script> + +<template> + <FetchData + url="States/editableStates" + @on-fetch="(data) => (editableStates = data)" + auto-load + /> + <QCard class="q-pa-sm"> + <QCardSection class="row items-center justify-center column items-stretch"> + <span>{{ $t('negative.detail.modal.changeState.title') }}</span> + <VnSelect + :label="$t('negative.detail.modal.changeState.placeholder')" + v-model="newState" + :options="editableStates" + option-label="name" + option-value="code" + /> + </QCardSection> + <QCardActions align="right"> + <QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup /> + <QBtn + :label="$t('globals.confirm')" + color="primary" + :disable="!newState" + @click="updateState" + unelevated + autofocus + /> </QCardActions + ></QCard> +</template> + +<style lang="scss" scoped> +.list { + max-height: 100%; + padding: 15px; + width: 100%; +} + +.grid-style-transition { + transition: + transform 0.28s, + background-color 0.28s; +} + +#true { + background-color: $positive; +} + +#false { + background-color: $negative; +} + +div.q-dialog__inner > div { + max-width: fit-content !important; +} +</style> diff --git a/src/pages/Ticket/TicketFuture.vue b/src/pages/Ticket/TicketFuture.vue index 0d216bed4..92911cd25 100644 --- a/src/pages/Ticket/TicketFuture.vue +++ b/src/pages/Ticket/TicketFuture.vue @@ -1,24 +1,22 @@ <script setup> -import { onMounted, ref, computed, reactive } from 'vue'; +import { ref, computed, reactive, watch } from 'vue'; import { useI18n } from 'vue-i18n'; -import FetchData from 'components/FetchData.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; import TicketFutureFilter from './TicketFutureFilter.vue'; import { dashIfEmpty, toCurrency } from 'src/filters'; import { useVnConfirm } from 'composables/useVnConfirm'; -import { useArrayData } from 'composables/useArrayData'; import { getDateQBadgeColor } from 'src/composables/getDateQBadgeColor.js'; import useNotify from 'src/composables/useNotify.js'; import { useState } from 'src/composables/useState'; import { toDateTimeFormat } from 'src/filters/date.js'; import axios from 'axios'; +import TicketProblems from 'src/components/TicketProblems.vue'; const state = useState(); const { t } = useI18n(); @@ -26,214 +24,126 @@ const { openConfirmationModal } = useVnConfirm(); const { notify } = useNotify(); const user = state.getUser(); -const itemPackingTypesOptions = ref([]); const selectedTickets = ref([]); - -const exprBuilder = (param, value) => { - switch (param) { - case 'id': - return { id: value }; - case 'futureId': - return { futureId: value }; - case 'liters': - return { liters: value }; - case 'lines': - return { lines: value }; - case 'iptColFilter': - return { ipt: { like: `%${value}%` } }; - case 'futureIptColFilter': - return { futureIpt: { like: `%${value}%` } }; - case 'totalWithVat': - return { totalWithVat: value }; - } -}; - +const vnTableRef = ref({}); +const originElRef = ref(null); +const destinationElRef = ref(null); const userParams = reactive({ futureScopeDays: Date.vnNew().toISOString(), originScopeDays: Date.vnNew().toISOString(), warehouseFk: user.value.warehouseFk, }); -const arrayData = useArrayData('FutureTickets', { - url: 'Tickets/getTicketsFuture', - userParams: userParams, - exprBuilder: exprBuilder, -}); -const { store } = arrayData; - -const params = reactive({ - futureScopeDays: Date.vnNew(), - originScopeDays: Date.vnNew(), - warehouseFk: user.value.warehouseFk, -}); - -const applyColumnFilter = async (col) => { - const paramKey = col.columnFilter?.filterParamKey || col.field; - params[paramKey] = col.columnFilter.filterValue; - await arrayData.addFilter({ params }); -}; - -const getInputEvents = (col) => { - return col.columnFilter.type === 'select' - ? { 'update:modelValue': () => applyColumnFilter(col) } - : { - 'keyup.enter': () => applyColumnFilter(col), - }; -}; - -const tickets = computed(() => store.data); - const ticketColumns = computed(() => [ { - label: t('futureTickets.problems'), + label: '', name: 'problems', + headerClass: 'horizontal-separator', align: 'left', - columnFilter: null, + columnFilter: false, }, { label: t('advanceTickets.ticketId'), - name: 'ticketId', + name: 'id', align: 'center', - sortable: true, - columnFilter: { - component: VnInput, - type: 'text', - filterValue: null, - filterParamKey: 'id', - event: getInputEvents, - attrs: { - dense: true, - }, - }, + headerClass: 'horizontal-separator', }, { label: t('futureTickets.shipped'), name: 'shipped', align: 'left', - sortable: true, - columnFilter: null, + columnFilter: false, + headerClass: 'horizontal-separator', }, { + align: 'center', + class: 'shrink', label: t('advanceTickets.ipt'), name: 'ipt', - field: 'ipt', - align: 'left', - sortable: true, columnFilter: { - component: VnSelect, - filterParamKey: 'iptColFilter', - type: 'select', - filterValue: null, - event: getInputEvents, + component: 'select', attrs: { - options: itemPackingTypesOptions.value, - 'option-value': 'code', - 'option-label': 'description', - dense: true, + url: 'itemPackingTypes', + fields: ['code', 'description'], + where: { isActive: true }, + optionValue: 'code', + optionLabel: 'description', + inWhere: false, }, }, - format: (val) => dashIfEmpty(val), + format: (row, dashIfEmpty) => dashIfEmpty(row.ipt), + headerClass: 'horizontal-separator', }, { label: t('ticketList.state'), name: 'state', align: 'left', - sortable: true, - columnFilter: null, + columnFilter: false, + headerClass: 'horizontal-separator', }, { label: t('advanceTickets.liters'), name: 'liters', - field: 'liters', align: 'left', - sortable: true, - columnFilter: { - component: VnInput, - type: 'text', - filterValue: null, - event: getInputEvents, - attrs: { - dense: true, - }, - }, + headerClass: 'horizontal-separator', }, { label: t('advanceTickets.import'), - field: 'import', name: 'import', align: 'left', - sortable: true, + headerClass: 'horizontal-separator', + columnFilter: false, + format: (row) => toCurrency(row.totalWithVat), }, { label: t('futureTickets.availableLines'), name: 'lines', field: 'lines', align: 'center', - sortable: true, - columnFilter: { - component: VnInput, - type: 'text', - filterValue: null, - event: getInputEvents, - attrs: { - dense: true, - }, - }, - format: (val) => dashIfEmpty(val), + headerClass: 'horizontal-separator', + format: (row, dashIfEmpty) => dashIfEmpty(row.lines), }, { label: t('advanceTickets.futureId'), name: 'futureId', - align: 'left', - sortable: true, - columnFilter: { - component: VnInput, - type: 'text', - filterValue: null, - filterParamKey: 'futureId', - event: getInputEvents, - attrs: { - dense: true, - }, - }, + align: 'center', + headerClass: 'horizontal-separator vertical-separator ', + columnClass: 'vertical-separator', }, { label: t('futureTickets.futureShipped'), name: 'futureShipped', align: 'left', - sortable: true, - columnFilter: null, - format: (val) => dashIfEmpty(val), + headerClass: 'horizontal-separator', + columnFilter: false, + format: (row) => toDateTimeFormat(row.futureShipped), }, - { + align: 'center', label: t('advanceTickets.futureIpt'), + class: 'shrink', name: 'futureIpt', - field: 'futureIpt', - align: 'left', - sortable: true, columnFilter: { - component: VnSelect, - filterParamKey: 'futureIptColFilter', - type: 'select', - filterValue: null, - event: getInputEvents, + component: 'select', attrs: { - options: itemPackingTypesOptions.value, - 'option-value': 'code', - 'option-label': 'description', - dense: true, + url: 'itemPackingTypes', + fields: ['code', 'description'], + where: { isActive: true }, + optionValue: 'code', + optionLabel: 'description', }, }, - format: (val) => dashIfEmpty(val), + headerClass: 'horizontal-separator', + format: (row, dashIfEmpty) => dashIfEmpty(row.futureIpt), }, { label: t('advanceTickets.futureState'), name: 'futureState', align: 'right', - sortable: true, - columnFilter: null, - format: (val) => dashIfEmpty(val), + headerClass: 'horizontal-separator', + class: 'expand', + columnFilter: false, + format: (row, dashIfEmpty) => dashIfEmpty(row.futureState), }, ]); @@ -258,26 +168,51 @@ const moveTicketsFuture = async () => { await axios.post('Tickets/merge', params); notify(t('advanceTickets.moveTicketSuccess'), 'positive'); selectedTickets.value = []; - arrayData.fetch({ append: false }); + vnTableRef.value.reload(); }; -onMounted(async () => { - await arrayData.fetch({ append: false }); -}); + +watch( + () => vnTableRef.value.tableRef?.$el, + ($el) => { + if (!$el) return; + const head = $el.querySelector('thead'); + const firstRow = $el.querySelector('thead > tr'); + + const newRow = document.createElement('tr'); + destinationElRef.value = document.createElement('th'); + originElRef.value = document.createElement('th'); + + newRow.classList.add('bg-header'); + destinationElRef.value.classList.add('text-uppercase', 'color-vn-label'); + originElRef.value.classList.add('text-uppercase', 'color-vn-label'); + + destinationElRef.value.setAttribute('colspan', '7'); + originElRef.value.setAttribute('colspan', '9'); + + originElRef.value.textContent = `${t('advanceTickets.origin')}`; + destinationElRef.value.textContent = `${t('advanceTickets.destination')}`; + + newRow.append(destinationElRef.value, originElRef.value); + head.insertBefore(newRow, firstRow); + }, + { once: true, inmmediate: true }, +); + +watch( + () => vnTableRef.value.params, + () => { + if (originElRef.value && destinationElRef.value) { + destinationElRef.value.textContent = `${t('advanceTickets.origin')}`; + originElRef.value.textContent = `${t('advanceTickets.destination')}`; + } + }, + { deep: true }, +); </script> <template> - <FetchData - url="itemPackingTypes" - :filter="{ - fields: ['code', 'description'], - order: 'description ASC', - where: { isActive: true }, - }" - auto-load - @on-fetch="(data) => (itemPackingTypesOptions = data)" - /> <VnSearchbar - data-key="FutureTickets" + data-key="futureTicket" :label="t('Search ticket')" :info="t('futureTickets.searchInfo')" /> @@ -293,7 +228,7 @@ onMounted(async () => { t(`futureTickets.moveTicketDialogSubtitle`, { selectedTickets: selectedTickets.length, }), - moveTicketsFuture + moveTicketsFuture, ) " > @@ -305,235 +240,135 @@ onMounted(async () => { </VnSubToolbar> <RightMenu> <template #right-panel> - <TicketFutureFilter data-key="FutureTickets" /> + <TicketFutureFilter data-key="futureTickets" /> </template> </RightMenu> <QPage class="column items-center q-pa-md"> - <QTable - :rows="tickets" + <VnTable + data-key="futureTickets" + ref="vnTableRef" + url="Tickets/getTicketsFuture" + search-url="futureTickets" + :user-params="userParams" + :limit="0" :columns="ticketColumns" - row-key="id" - selection="multiple" + :table="{ + 'row-key': '$index', + selection: 'multiple', + }" v-model:selected="selectedTickets" - :pagination="{ rowsPerPage: 0 }" - :no-data-label="t('globals.noResults')" - style="max-width: 99%" + :right-search="false" + auto-load + :disable-option="{ card: true }" > - <template #header="props"> - <QTr> - <QTh class="horizontal-separator" /> - <QTh - class="horizontal-separator text-uppercase color-vn-label" - colspan="8" - translate - > - {{ t('advanceTickets.origin') }} - </QTh> - <QTh - class="horizontal-separator text-uppercase color-vn-label" - colspan="4" - translate - > - {{ t('advanceTickets.destination') }} - </QTh> - </QTr> - <QTr> - <QTh> - <QCheckbox v-model="props.selected" /> - </QTh> - <QTh - v-for="(col, index) in ticketColumns" - :key="index" - :class="{ 'vertical-separator': col.name === 'futureId' }" - > - {{ col.label }} - </QTh> - </QTr> - </template> - <template #top-row="{ cols }"> - <QTr> - <QTd /> - <QTd - v-for="(col, index) in cols" - :key="index" - style="max-width: 100px" - > - <component - :is="col.columnFilter.component" - v-if="col.columnFilter" - v-model="col.columnFilter.filterValue" - v-bind="col.columnFilter.attrs" - v-on="col.columnFilter.event(col)" - dense - /> - </QTd> - </QTr> - </template> - <template #header-cell-availableLines="{ col }"> - <QTh class="vertical-separator"> - {{ col.label }} - </QTh> - </template> - <template #body-cell-problems="{ row }"> - <QTd class="q-gutter-x-xs"> + <template #column-problems="{ row }"> + <span class="q-gutter-x-xs"> <QIcon - v-if="row.isTaxDataChecked === 0" + v-if="row.futureAgencyFk !== row.agencyFk && row.agencyFk" color="primary" - name="vn:no036" + name="vn:agency-term" size="xs" + class="q-mr-xs" > - <QTooltip> - {{ t('futureTickets.noVerified') }} + <QTooltip class="column"> + <span> + {{ + t('advanceTickets.originAgency', { + agency: row.futureAgency, + }) + }} + </span> + <span> + {{ + t('advanceTickets.destinationAgency', { + agency: row.agency, + }) + }} + </span> </QTooltip> </QIcon> - <QIcon - v-if="row.hasTicketRequest" - color="primary" - name="vn:buyrequest" - size="xs" - > - <QTooltip> - {{ t('futureTickets.purchaseRequest') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.itemShortage" - color="primary" - name="vn:unavailable" - size="xs" - > - <QTooltip> - {{ t('ticketSale.noVisible') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.isFreezed" - color="primary" - name="vn:frozen" - size="xs" - > - <QTooltip> - {{ t('futureTickets.clientFrozen') }} - </QTooltip> - </QIcon> - <QIcon v-if="row.risk" color="primary" name="vn:risk" size="xs"> - <QTooltip> - {{ t('futureTickets.risk') }}: {{ row.risk }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.hasComponentLack" - color="primary" - name="vn:components" - size="xs" - > - <QTooltip> - {{ t('futureTickets.componentLack') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.hasRounding" - color="primary" - name="sync_problem" - size="xs" - > - <QTooltip> - {{ t('futureTickets.rounding') }} - </QTooltip> - </QIcon> - </QTd> + <TicketProblems :row /> + </span> </template> - <template #body-cell-ticketId="{ row }"> - <QTd> - <QBtn flat class="link"> - {{ row.id }} - <TicketDescriptorProxy :id="row.id" /> - </QBtn> - </QTd> + <template #column-id="{ row }"> + <QBtn flat class="link" @click.stop dense> + {{ row.id }} + <TicketDescriptorProxy :id="row.id" /> + </QBtn> </template> - <template #body-cell-shipped="{ row }"> - <QTd class="shipped"> - <QBadge - text-color="black" - :color="getDateQBadgeColor(row.shipped)" - class="q-ma-none" - > - {{ toDateTimeFormat(row.shipped) }} - </QBadge> - </QTd> + <template #column-shipped="{ row }"> + <QBadge + text-color="black" + :color="getDateQBadgeColor(row.shipped)" + class="q-ma-none" + > + {{ toDateTimeFormat(row.shipped) }} + </QBadge> </template> - <template #body-cell-state="{ row }"> - <QTd> - <QBadge - text-color="black" - :color="row.classColor" - class="q-ma-none" - dense - > - {{ row.state }} - </QBadge> - </QTd> + <template #column-state="{ row }"> + <QBadge + v-if="row.state" + text-color="black" + :color="row.classColor" + class="q-ma-none" + dense + > + {{ row.state }} + </QBadge> + <span v-else> {{ dashIfEmpty(row.state) }}</span> </template> - <template #body-cell-import="{ row }"> - <QTd> - <QBadge - :text-color=" - totalPriceColor(row.totalWithVat) === 'warning' - ? 'black' - : 'white' - " - :color="totalPriceColor(row.totalWithVat)" - class="q-ma-none" - dense - > - {{ toCurrency(row.totalWithVat || 0) }} - </QBadge> - </QTd> + <template #column-import="{ row }"> + <QBadge + :text-color=" + totalPriceColor(row.totalWithVat) === 'warning' + ? 'black' + : 'white' + " + :color="totalPriceColor(row.totalWithVat)" + class="q-ma-none" + dense + > + {{ toCurrency(row.totalWithVat || 0) }} + </QBadge> </template> - <template #body-cell-futureId="{ row }"> - <QTd class="vertical-separator"> - <QBtn flat class="link" dense> - {{ row.futureId }} - <TicketDescriptorProxy :id="row.futureId" /> - </QBtn> - </QTd> + <template #column-futureId="{ row }"> + <QBtn flat class="link" @click.stop dense> + {{ row.futureId }} + <TicketDescriptorProxy :id="row.futureId" /> + </QBtn> </template> - <template #body-cell-futureShipped="{ row }"> - <QTd class="shipped"> - <QBadge - text-color="black" - :color="getDateQBadgeColor(row.futureShipped)" - class="q-ma-none" - > - {{ toDateTimeFormat(row.futureShipped) }} - </QBadge> - </QTd> + <template #column-futureShipped="{ row }"> + <QBadge + text-color="black" + :color="getDateQBadgeColor(row.futureShipped)" + class="q-ma-none" + > + {{ toDateTimeFormat(row.futureShipped) }} + </QBadge> </template> - <template #body-cell-futureState="{ row }"> - <QTd> - <QBadge - text-color="black" - :color="row.futureClassColor" - class="q-ma-none" - dense - > - {{ row.futureState }} - </QBadge> - </QTd> + <template #column-futureState="{ row }"> + <QBadge + text-color="black" + :color="row.futureClassColor" + class="q-mr-xs" + dense + > + {{ row.futureState }} + </QBadge> </template> - </QTable> + </VnTable> </QPage> </template> <style scoped lang="scss"> -.shipped { - min-width: 132px; -} -.vertical-separator { +:deep(.vertical-separator) { border-left: 4px solid white !important; } -.horizontal-separator { +:deep(.horizontal-separator) { + border-top: 4px solid white !important; +} +:deep(.horizontal-bottom-separator) { border-bottom: 4px solid white !important; } </style> diff --git a/src/pages/Ticket/TicketFutureFilter.vue b/src/pages/Ticket/TicketFutureFilter.vue index d28b0af71..64e060a39 100644 --- a/src/pages/Ticket/TicketFutureFilter.vue +++ b/src/pages/Ticket/TicketFutureFilter.vue @@ -12,7 +12,7 @@ import axios from 'axios'; import { onMounted } from 'vue'; const { t } = useI18n(); -const props = defineProps({ +defineProps({ dataKey: { type: String, required: true, @@ -58,7 +58,7 @@ onMounted(async () => { auto-load /> <VnFilterPanel - :data-key="props.dataKey" + :data-key :un-removable-params="['warehouseFk', 'originScopeDays ', 'futureScopeDays']" > <template #tags="{ tag, formatFn }"> diff --git a/src/pages/Ticket/locale/en.yml b/src/pages/Ticket/locale/en.yml index f11b32c3a..cdbb22d9b 100644 --- a/src/pages/Ticket/locale/en.yml +++ b/src/pages/Ticket/locale/en.yml @@ -23,6 +23,8 @@ ticketSale: hasComponentLack: Component lack ok: Ok more: More + transferLines: Transfer lines(no basket)/ Split + transferBasket: Some row selected is basket advanceTickets: preparation: Preparation origin: Origin @@ -188,7 +190,6 @@ ticketList: accountPayment: Account payment sendDocuware: Set delivered and send delivery note(s) to the tablet addPayment: Add payment - date: Date company: Company amount: Amount reference: Reference @@ -202,9 +203,89 @@ ticketList: creditCard: Credit card transfers: Transfers province: Province - warehouse: Warehouse - hour: Hour closure: Closure toLines: Go to lines addressNickname: Address nickname ref: Reference + rounding: Rounding + noVerifiedData: No verified data + purchaseRequest: Purchase request + notVisible: Not visible + clientFrozen: Client frozen + componentLack: Component lack +negative: + hour: Hour + id: Id Article + longName: Article + supplier: Supplier + colour: Colour + size: Size + origen: Origin + value: Negative + itemFk: Article + producer: Producer + warehouse: Warehouse + warehouseFk: Warehouse + category: Category + categoryFk: Family + type: Type + typeFk: Type + lack: Negative + inkFk: inkFk + timed: timed + date: Date + minTimed: minTimed + negativeAction: Negative + totalNegative: Total negatives + days: Days + buttonsUpdate: + item: Item + state: State + quantity: Quantity + modalOrigin: + title: Update negatives + question: Select a state to update + modalSplit: + title: Confirm split selected + question: Select a state to update + detail: + saleFk: Sale + itemFk: Article + ticketFk: Ticket + code: Code + nickname: Alias + name: Name + zoneName: Agency name + shipped: Date + theoreticalhour: Theoretical hour + agName: Agency + quantity: Quantity + alertLevelCode: Group state + state: State + peticionCompra: Ticket request + isRookie: Is rookie + turno: Turn line + isBasket: Basket + hasObservation: Has substitution + hasToIgnore: VIP + modal: + changeItem: + title: Update item reference + placeholder: New item + changeState: + title: Update tickets state + placeholder: New state + changeQuantity: + title: Update tickets quantity + placeholder: New quantity + split: + title: Are you sure you want to split selected tickets? + subTitle: Confirm split action + handleSplited: + title: Handle splited tickets + subTitle: Confirm date and agency + split: + ticket: Old ticket + newTicket: New ticket + status: Result + message: Message diff --git a/src/pages/Ticket/locale/es.yml b/src/pages/Ticket/locale/es.yml index 945da8367..75d3c6a2b 100644 --- a/src/pages/Ticket/locale/es.yml +++ b/src/pages/Ticket/locale/es.yml @@ -127,6 +127,8 @@ ticketSale: ok: Ok more: Más address: Consignatario + transferLines: Transferir líneas(no cesta)/ Separar + transferBasket: No disponible para una cesta size: Medida ticketComponents: serie: Serie @@ -213,3 +215,84 @@ ticketList: toLines: Ir a lineas addressNickname: Alias consignatario ref: Referencia +negative: + hour: Hora + id: Id Articulo + longName: Articulo + supplier: Productor + colour: Color + size: Medida + origen: Origen + value: Negativo + warehouseFk: Almacen + producer: Producer + category: Categoría + categoryFk: Familia + typeFk: Familia + warehouse: Almacen + lack: Negativo + inkFk: Color + timed: Hora + date: Fecha + minTimed: Hora + type: Tipo + negativeAction: Negativo + totalNegative: Total negativos + days: Rango de dias + buttonsUpdate: + item: artículo + state: Estado + quantity: Cantidad + modalOrigin: + title: Actualizar negativos + question: Seleccione un estado para guardar + modalSplit: + title: Confirmar acción de split + question: Selecciona un estado + detail: + saleFk: Línea + itemFk: Artículo + ticketFk: Ticket + code: code + nickname: Alias + name: Nombre + zoneName: Agencia + shipped: F. envío + theoreticalhour: Hora teórica + agName: Agencia + quantity: Cantidad + alertLevelCode: Estado agrupado + state: Estado + peticionCompra: Petición compra + isRookie: Cliente nuevo + turno: Linea turno + isBasket: Cesta + hasObservation: Tiene sustitución + hasToIgnore: VIP + modal: + changeItem: + title: Actualizar referencia artículo + placeholder: Nuevo articulo + changeState: + title: Actualizar estado + placeholder: Nuevo estado + changeQuantity: + title: Actualizar cantidad + placeholder: Nueva cantidad + split: + title: ¿Seguro de separar los tickets seleccionados? + subTitle: Confirma separar tickets seleccionados + handleSplited: + title: Gestionar tickets spliteados + subTitle: Confir fecha y agencia + split: + ticket: Ticket viejo + newTicket: Ticket nuevo + status: Estado + message: Mensaje + rounding: Redondeo + noVerifiedData: Sin datos comprobados + purchaseRequest: Petición de compra + notVisible: No visible + clientFrozen: Cliente congelado + componentLack: Faltan componentes diff --git a/src/pages/Travel/Card/TravelBasicData.vue b/src/pages/Travel/Card/TravelBasicData.vue index 4b9aa28ed..b1adc8126 100644 --- a/src/pages/Travel/Card/TravelBasicData.vue +++ b/src/pages/Travel/Card/TravelBasicData.vue @@ -9,6 +9,7 @@ import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; +import VnInputTime from 'components/common/VnInputTime.vue'; const route = useRoute(); const { t } = useI18n(); @@ -53,7 +54,16 @@ const warehousesOptionsIn = ref([]); <VnInputDate v-model="data.shipped" :label="t('globals.shipped')" /> <VnInputDate v-model="data.landed" :label="t('globals.landed')" /> </VnRow> - + <VnRow> + <VnInputDate + v-model="data.availabled" + :label="t('travel.summary.availabled')" + /> + <VnInputTime + v-model="data.availabled" + :label="t('travel.summary.availabledHour')" + /> + </VnRow> <VnRow> <VnSelect :label="t('globals.warehouseOut')" @@ -101,10 +111,3 @@ const warehousesOptionsIn = ref([]); </template> </FormModel> </template> - -<i18n> -es: - raidDays: El travel se desplaza automáticamente cada día para estar desde hoy al número de días indicado. Si se deja vacio no se moverá -en: - raidDays: The travel adjusts itself daily to match the number of days set, starting from today. If left blank, it won’t move -</i18n> diff --git a/src/pages/Travel/Card/TravelCard.vue b/src/pages/Travel/Card/TravelCard.vue index 445675b90..cb09eafd6 100644 --- a/src/pages/Travel/Card/TravelCard.vue +++ b/src/pages/Travel/Card/TravelCard.vue @@ -1,43 +1,13 @@ <script setup> import TravelDescriptor from './TravelDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; - -const userFilter = { - fields: [ - 'id', - 'ref', - 'shipped', - 'landed', - 'totalEntries', - 'warehouseInFk', - 'warehouseOutFk', - 'cargoSupplierFk', - 'agencyModeFk', - 'isRaid', - 'isDelivered', - 'isReceived', - ], - include: [ - { - relation: 'warehouseIn', - scope: { - fields: ['name'], - }, - }, - { - relation: 'warehouseOut', - scope: { - fields: ['name'], - }, - }, - ], -}; +import filter from './TravelFilter.js'; </script> <template> <VnCardBeta data-key="Travel" - base-url="Travels" + url="Travels" :descriptor="TravelDescriptor" - :user-filter="userFilter" + :filter="filter" /> </template> diff --git a/src/pages/Travel/Card/TravelDescriptor.vue b/src/pages/Travel/Card/TravelDescriptor.vue index 72acf91b8..922f89f33 100644 --- a/src/pages/Travel/Card/TravelDescriptor.vue +++ b/src/pages/Travel/Card/TravelDescriptor.vue @@ -32,7 +32,6 @@ const setData = (entity) => (data.value = useCardDescription(entity.ref, entity. <template> <CardDescriptor - module="Travel" :url="`Travels/${entityId}`" :title="data.title" :subtitle="data.subtitle" diff --git a/src/pages/Travel/Card/TravelFilter.js b/src/pages/Travel/Card/TravelFilter.js index f5f4520fd..05436834f 100644 --- a/src/pages/Travel/Card/TravelFilter.js +++ b/src/pages/Travel/Card/TravelFilter.js @@ -11,6 +11,7 @@ export default { 'agencyModeFk', 'isRaid', 'daysInForward', + 'availabled', ], include: [ { diff --git a/src/pages/Travel/Card/TravelSummary.vue b/src/pages/Travel/Card/TravelSummary.vue index 16d42f104..9f9552611 100644 --- a/src/pages/Travel/Card/TravelSummary.vue +++ b/src/pages/Travel/Card/TravelSummary.vue @@ -10,6 +10,8 @@ import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue' import FetchData from 'src/components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import { toDate, toCurrency, toCelsius } from 'src/filters'; +import { toDateTimeFormat } from 'src/filters/date.js'; +import { dashIfEmpty } from 'src/filters'; import axios from 'axios'; import TravelDescriptorMenuItems from './TravelDescriptorMenuItems.vue'; @@ -333,6 +335,12 @@ const getLink = (param) => `#/travel/${entityId.value}/${param}`; <VnLv :label="t('globals.reference')" :value="travel.ref" /> <VnLv label="m³" :value="travel.m3" /> <VnLv :label="t('globals.totalEntries')" :value="travel.totalEntries" /> + <VnLv + :label="t('travel.summary.availabled')" + :value=" + dashIfEmpty(toDateTimeFormat(travel.availabled)) + " + /> </QCard> <QCard class="full-width"> <VnTitle :text="t('travel.summary.entries')" /> diff --git a/src/pages/Travel/Card/TravelThermographs.vue b/src/pages/Travel/Card/TravelThermographs.vue index 2946c8814..2376bd6d2 100644 --- a/src/pages/Travel/Card/TravelThermographs.vue +++ b/src/pages/Travel/Card/TravelThermographs.vue @@ -217,7 +217,7 @@ const removeThermograph = async (id) => { icon="add" color="primary" @click="redirectToThermographForm('create')" - shortcut="+" + v-shortcut="'+'" /> <QTooltip class="text-no-wrap"> {{ t('Add thermograph') }} diff --git a/src/pages/Travel/ExtraCommunityFilter.vue b/src/pages/Travel/ExtraCommunityFilter.vue index b903aeabf..b22574632 100644 --- a/src/pages/Travel/ExtraCommunityFilter.vue +++ b/src/pages/Travel/ExtraCommunityFilter.vue @@ -113,7 +113,7 @@ warehouses(); <template #append> <QBtn icon="add" - shortcut="+" + v-shortcut="'+'" flat dense size="12px" diff --git a/src/pages/Travel/TravelList.vue b/src/pages/Travel/TravelList.vue index e90c01be2..b227afcb2 100644 --- a/src/pages/Travel/TravelList.vue +++ b/src/pages/Travel/TravelList.vue @@ -10,6 +10,9 @@ import { getDateQBadgeColor } from 'src/composables/getDateQBadgeColor.js'; import TravelFilter from './TravelFilter.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import VnSection from 'src/components/common/VnSection.vue'; +import VnInputTime from 'src/components/common/VnInputTime.vue'; +import VnInputDate from 'src/components/common/VnInputDate.vue'; +import { toDateTimeFormat } from 'src/filters/date'; const { viewSummary } = useSummaryDialog(); const router = useRouter(); @@ -167,6 +170,17 @@ const columns = computed(() => [ cardVisible: true, create: true, }, + { + align: 'left', + name: 'availabled', + label: t('travel.summary.availabled'), + component: 'input', + columnClass: 'expand', + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(toDateTimeFormat(row.availabled)), + }, { align: 'right', label: '', @@ -269,6 +283,16 @@ const columns = computed(() => [ :class="{ 'is-active': row.isReceived }" /> </template> + <template #more-create-dialog="{ data }"> + <VnInputDate + v-model="data.availabled" + :label="t('travel.summary.availabled')" + /> + <VnInputTime + v-model="data.availabled" + :label="t('travel.summary.availabledHour')" + /> + </template> <template #moreFilterPanel="{ params }"> <VnInputNumber :label="t('params.scopeDays')" diff --git a/src/pages/Wagon/Card/WagonCard.vue b/src/pages/Wagon/Card/WagonCard.vue index ed6c83778..644a30ffa 100644 --- a/src/pages/Wagon/Card/WagonCard.vue +++ b/src/pages/Wagon/Card/WagonCard.vue @@ -2,5 +2,5 @@ import VnCard from 'components/common/VnCard.vue'; </script> <template> - <VnCard data-key="Wagon" base-url="Wagons" /> + <VnCard data-key="Wagon" url="Wagons" /> </template> diff --git a/src/pages/Wagon/Type/WagonTypeList.vue b/src/pages/Wagon/Type/WagonTypeList.vue index c0943c58e..4c0b078a7 100644 --- a/src/pages/Wagon/Type/WagonTypeList.vue +++ b/src/pages/Wagon/Type/WagonTypeList.vue @@ -96,7 +96,13 @@ async function remove(row) { > </VnTable> <QPageSticky :offset="[18, 18]"> - <QBtn @click.stop="dialog.show()" color="primary" fab icon="add" shortcut="+"> + <QBtn + @click.stop="dialog.show()" + color="primary" + fab + icon="add" + v-shortcut="'+'" + > <QDialog ref="dialog"> <FormModelPopup :title="t('Create new Wagon type')" diff --git a/src/pages/Worker/Card/WorkerBasicData.vue b/src/pages/Worker/Card/WorkerBasicData.vue index 6a13e3f39..fcf0f0369 100644 --- a/src/pages/Worker/Card/WorkerBasicData.vue +++ b/src/pages/Worker/Card/WorkerBasicData.vue @@ -1,6 +1,5 @@ <script setup> -import { ref, onBeforeMount } from 'vue'; -import { useRoute } from 'vue-router'; +import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import FetchData from 'components/FetchData.vue'; @@ -11,18 +10,13 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import { useAdvancedSummary } from 'src/composables/useAdvancedSummary'; const { t } = useI18n(); +const form = ref(); const educationLevels = ref([]); const countries = ref([]); const maritalStatus = [ { code: 'M', name: t('Married') }, { code: 'S', name: t('Single') }, ]; -const advancedSummary = ref({}); - -onBeforeMount(async () => { - advancedSummary.value = - (await useAdvancedSummary('Workers', +useRoute().params.id)) ?? {}; -}); </script> <template> <FetchData @@ -38,14 +32,15 @@ onBeforeMount(async () => { auto-load /> <FormModel - :filter="{ where: { id: +$route.params.id } }" - url="Workers/summary" + ref="form" :url-update="`Workers/${$route.params.id}`" auto-load model="Worker" @on-fetch=" async (data) => { - Object.assign(data, advancedSummary); + Object.assign(data, (await useAdvancedSummary('Workers', data.id)) ?? {}); + await $nextTick(); + if (form) form.hasChanges = false; } " > diff --git a/src/pages/Worker/Card/WorkerCalendar.vue b/src/pages/Worker/Card/WorkerCalendar.vue index 5ca95a1a4..df4616011 100644 --- a/src/pages/Worker/Card/WorkerCalendar.vue +++ b/src/pages/Worker/Card/WorkerCalendar.vue @@ -1,7 +1,8 @@ <script setup> -import { nextTick, ref, watch } from 'vue'; +import { nextTick, ref, watch, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; +import { useAcl } from 'src/composables/useAcl'; import WorkerCalendarFilter from 'pages/Worker/Card/WorkerCalendarFilter.vue'; import FetchData from 'components/FetchData.vue'; @@ -9,10 +10,17 @@ import WorkerCalendarItem from 'pages/Worker/Card/WorkerCalendarItem.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import axios from 'axios'; +import VnNotes from 'src/components/ui/VnNotes.vue'; +import { useStateStore } from 'src/stores/useStateStore'; +const stateStore = useStateStore(); const router = useRouter(); const route = useRoute(); const { t } = useI18n(); +const acl = useAcl(); +const canSeeNotes = computed(() => + acl.hasAny([{ model: 'Worker', props: '__get__business', accessType: 'READ' }]), +); const workerIsFreelance = ref(); const WorkerFreelanceRef = ref(); const workerCalendarFilterRef = ref(null); @@ -26,6 +34,10 @@ const contractHolidays = ref(null); const yearHolidays = ref(null); const eventsMap = ref({}); const festiveEventsMap = ref({}); +const saveUrl = ref(); +const body = { + workerFk: route.params.id, +}; const onFetchActiveContract = (data) => { if (!data) return; @@ -67,7 +79,7 @@ const onFetchAbsences = (data) => { name: holidayName, isFestive: true, }, - true + true, ); }); } @@ -146,7 +158,7 @@ watch( async () => { await nextTick(); await activeContractRef.value.fetch(); - } + }, ); watch([year, businessFk], () => refreshData()); </script> @@ -181,6 +193,20 @@ watch([year, businessFk], () => refreshData()); /> </template> </RightMenu> + <Teleport to="#st-data" v-if="stateStore.isSubToolbarShown() && canSeeNotes"> + <VnNotes + :just-input="true" + :url="`Workers/${route.params.id}/business`" + :filter="{ fields: ['id', 'notes', 'workerFk'] }" + :save-url="saveUrl" + @on-fetch=" + (data) => { + saveUrl = `Businesses/${data.id}`; + } + " + :body="body" + /> + </Teleport> <QPage class="column items-center"> <QCard v-if="workerIsFreelance"> <QCardSection class="text-center"> diff --git a/src/pages/Worker/Card/WorkerCalendarFilter.vue b/src/pages/Worker/Card/WorkerCalendarFilter.vue index 67b7df907..48fc4094b 100644 --- a/src/pages/Worker/Card/WorkerCalendarFilter.vue +++ b/src/pages/Worker/Card/WorkerCalendarFilter.vue @@ -180,8 +180,6 @@ const yearList = ref(generateYears()); :is-clearable="false" /> </QItemSection> - </QItem> - <QItem> <QItemSection> <VnSelect :label="t('Contract')" diff --git a/src/pages/Worker/Card/WorkerCard.vue b/src/pages/Worker/Card/WorkerCard.vue index 1ada15a33..3b7a62025 100644 --- a/src/pages/Worker/Card/WorkerCard.vue +++ b/src/pages/Worker/Card/WorkerCard.vue @@ -3,5 +3,10 @@ import WorkerDescriptor from './WorkerDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; </script> <template> - <VnCardBeta data-key="Worker" custom-url="Workers/summary" :descriptor="WorkerDescriptor" /> + <VnCardBeta + data-key="Worker" + url="Workers/summary" + :id-in-where="true" + :descriptor="WorkerDescriptor" + /> </template> diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index 2b0af4926..0e946f1dd 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -10,7 +10,7 @@ import axios from 'axios'; import VnImg from 'src/components/ui/VnImg.vue'; import EditPictureForm from 'components/EditPictureForm.vue'; import WorkerDescriptorMenu from './WorkerDescriptorMenu.vue'; -import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; const $props = defineProps({ id: { @@ -21,7 +21,7 @@ const $props = defineProps({ dataKey: { type: String, required: false, - default: 'workerData', + default: 'Worker', }, }); const image = ref(null); @@ -50,9 +50,8 @@ const handlePhotoUpdated = (evt = false) => { <template> <CardDescriptor ref="cardDescriptorRef" - module="Worker" :data-key="dataKey" - url="Workers/descriptor" + url="Workers/summary" :filter="{ where: { id: entityId } }" title="user.nickname" @on-fetch="getIsExcluded" @@ -153,7 +152,7 @@ const handlePhotoUpdated = (evt = false) => { <QBtn :to="{ name: 'AccountCard', - params: { id: entity.user.id }, + params: { id: entity.user?.id }, }" size="md" icon="face" diff --git a/src/pages/Worker/Card/WorkerDescriptorProxy.vue b/src/pages/Worker/Card/WorkerDescriptorProxy.vue index 43deb7821..a142570f9 100644 --- a/src/pages/Worker/Card/WorkerDescriptorProxy.vue +++ b/src/pages/Worker/Card/WorkerDescriptorProxy.vue @@ -12,11 +12,6 @@ const $props = defineProps({ <template> <QPopupProxy> - <WorkerDescriptor - v-if="$props.id" - :id="$props.id" - :summary="WorkerSummary" - data-key="workerDescriptorProxy" - /> + <WorkerDescriptor v-if="$props.id" :id="$props.id" :summary="WorkerSummary" /> </QPopupProxy> </template> diff --git a/src/pages/Worker/Card/WorkerFormation.vue b/src/pages/Worker/Card/WorkerFormation.vue index 6fd5a4eae..e8680f7dd 100644 --- a/src/pages/Worker/Card/WorkerFormation.vue +++ b/src/pages/Worker/Card/WorkerFormation.vue @@ -94,6 +94,7 @@ const columns = computed(() => [ align: 'left', name: 'hasDiploma', label: t('worker.formation.tableVisibleColumns.hasDiploma'), + component: 'checkbox', create: true, }, { @@ -118,7 +119,7 @@ const columns = computed(() => [ :url="`Workers/${entityId}/trainingCourse`" :url-create="`Workers/${entityId}/trainingCourse`" save-url="TrainingCourses/crud" - :filter="courseFilter" + :user-filter="courseFilter" :create="{ urlCreate: 'trainingCourses', title: t('Create training course'), diff --git a/src/pages/Worker/Card/WorkerMedical.vue b/src/pages/Worker/Card/WorkerMedical.vue index c220df76a..c04f6496b 100644 --- a/src/pages/Worker/Card/WorkerMedical.vue +++ b/src/pages/Worker/Card/WorkerMedical.vue @@ -3,11 +3,23 @@ import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import VnTable from 'components/VnTable/VnTable.vue'; +import { dashIfEmpty } from 'src/filters'; const tableRef = ref(); const { t } = useI18n(); const route = useRoute(); const entityId = computed(() => route.params.id); +const centerFilter = { + include: [ + { + relation: 'center', + scope: { + fields: ['id', 'name'], + }, + }, + ], +}; + const columns = [ { align: 'left', @@ -36,6 +48,9 @@ const columns = [ url: 'medicalCenters', fields: ['id', 'name'], }, + format: (row, dashIfEmpty) => { + return dashIfEmpty(row.center?.name); + }, }, { align: 'left', @@ -84,6 +99,7 @@ const columns = [ ref="tableRef" data-key="WorkerMedical" :url="`Workers/${entityId}/medicalReview`" + :user-filter="centerFilter" save-url="MedicalReviews/crud" :create="{ urlCreate: 'medicalReviews', diff --git a/src/pages/Worker/Card/WorkerOperator.vue b/src/pages/Worker/Card/WorkerOperator.vue index cdacc72c0..6faeefe67 100644 --- a/src/pages/Worker/Card/WorkerOperator.vue +++ b/src/pages/Worker/Card/WorkerOperator.vue @@ -1,7 +1,7 @@ <script setup> import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import { ref, computed } from 'vue'; +import { ref, computed, watch } from 'vue'; import FetchData from 'components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; @@ -19,6 +19,7 @@ const trainsData = ref([]); const machinesData = ref([]); const route = useRoute(); const routeId = computed(() => route.params.id); +const selected = ref([]); const initialData = computed(() => { return { @@ -41,6 +42,21 @@ async function insert() { await axios.post('Operators', initialData.value); crudModelRef.value.reload(); } + +watch( + () => crudModelRef.value?.formData, + (formData) => { + if (formData && formData.length) { + if (JSON.stringify(selected.value) !== JSON.stringify(formData)) { + selected.value = formData; + } + } else if (selected.value.length > 0) { + selected.value = []; + } + }, + { immediate: true, deep: true } +); + </script> <template> @@ -67,6 +83,7 @@ async function insert() { :data-required="{ workerFk: route.params.id }" ref="crudModelRef" search-url="operator" + :selected="selected" auto-load > <template #body="{ rows }"> diff --git a/src/pages/Worker/Card/WorkerPda.vue b/src/pages/Worker/Card/WorkerPda.vue index f6cb92aac..47e13cf6d 100644 --- a/src/pages/Worker/Card/WorkerPda.vue +++ b/src/pages/Worker/Card/WorkerPda.vue @@ -101,7 +101,7 @@ function reloadData() { openConfirmationModal( t(`Remove PDA`), t('Do you want to remove this PDA?'), - () => deallocatePDA(row.deviceProductionFk) + () => deallocatePDA(row.deviceProductionFk), ) " > @@ -114,7 +114,13 @@ function reloadData() { </template> </VnPaginate> <QPageSticky :offset="[18, 18]"> - <QBtn @click.stop="dialog.show()" color="primary" fab icon="add" shortcut="+"> + <QBtn + @click.stop="dialog.show()" + color="primary" + fab + icon="add" + v-shortcut="'+'" + > <QDialog ref="dialog"> <FormModelPopup :title="t('Add new device')" diff --git a/src/pages/Worker/Card/WorkerPit.vue b/src/pages/Worker/Card/WorkerPit.vue index 79cf1a04f..40e814452 100644 --- a/src/pages/Worker/Card/WorkerPit.vue +++ b/src/pages/Worker/Card/WorkerPit.vue @@ -221,7 +221,7 @@ const deleteRelative = async (id) => { color="primary" flat icon="add" - shortcut="+" + v-shortcut="'+'" style="flex: 0" data-cy="addRelative" /> diff --git a/src/pages/Worker/Card/WorkerSummary.vue b/src/pages/Worker/Card/WorkerSummary.vue index 992f6ec71..78c5dfd82 100644 --- a/src/pages/Worker/Card/WorkerSummary.vue +++ b/src/pages/Worker/Card/WorkerSummary.vue @@ -9,7 +9,7 @@ import CardSummary from 'components/ui/CardSummary.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnTitle from 'src/components/common/VnTitle.vue'; import RoleDescriptorProxy from 'src/pages/Account/Role/Card/RoleDescriptorProxy.vue'; -import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; import { useAdvancedSummary } from 'src/composables/useAdvancedSummary'; import WorkerDescriptorMenu from './WorkerDescriptorMenu.vue'; diff --git a/src/pages/Worker/Card/WorkerTimeControl.vue b/src/pages/Worker/Card/WorkerTimeControl.vue index c580e5202..7def6e94c 100644 --- a/src/pages/Worker/Card/WorkerTimeControl.vue +++ b/src/pages/Worker/Card/WorkerTimeControl.vue @@ -64,17 +64,17 @@ const selectedCalendarDates = ref([]); // Date formateada para bindear al componente QDate const selectedDateFormatted = ref(toDateString(defaultDate.value)); -const arrayData = useArrayData('workerData'); +const arrayData = useArrayData('Worker'); const acl = useAcl(); const selectedDateYear = computed(() => moment(selectedDate.value).isoWeekYear()); const worker = computed(() => arrayData.store?.data); const canSend = computed(() => - acl.hasAny([{ model: 'WorkerTimeControl', props: 'sendMail', accessType: 'WRITE' }]) + acl.hasAny([{ model: 'WorkerTimeControl', props: 'sendMail', accessType: 'WRITE' }]), ); const canUpdate = computed(() => acl.hasAny([ { model: 'WorkerTimeControl', props: 'updateMailState', accessType: 'WRITE' }, - ]) + ]), ); const isHimself = computed(() => user.value.id === Number(route.params.id)); @@ -100,7 +100,7 @@ const getHeaderFormattedDate = (date) => { }; const formattedWeekTotalHours = computed(() => - secondsToHoursMinutes(weekTotalHours.value) + secondsToHoursMinutes(weekTotalHours.value), ); const onInputChange = async (date) => { @@ -320,7 +320,7 @@ const getFinishTime = () => { today.setHours(0, 0, 0, 0); let todayInWeek = weekDays.value.find( - (day) => day.dated.getTime() === today.getTime() + (day) => day.dated.getTime() === today.getTime(), ); if (todayInWeek && todayInWeek.hours && todayInWeek.hours.length) { @@ -472,7 +472,7 @@ onMounted(async () => { openConfirmationModal( t('Send time control email'), t('Are you sure you want to send it?'), - resendEmail + resendEmail, ) " > @@ -561,7 +561,7 @@ onMounted(async () => { @show-worker-time-form=" showWorkerTimeForm( { id: hour.id, entryCode: hour.direction }, - 'edit' + 'edit', ) " class="hour-chip" @@ -577,7 +577,7 @@ onMounted(async () => { </span> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat color="primary" class="fill-icon cursor-pointer" diff --git a/src/pages/Department/Card/DepartmentBasicData.vue b/src/pages/Worker/Department/Card/DepartmentBasicData.vue similarity index 73% rename from src/pages/Department/Card/DepartmentBasicData.vue rename to src/pages/Worker/Department/Card/DepartmentBasicData.vue index b13aed2d3..66210be7b 100644 --- a/src/pages/Department/Card/DepartmentBasicData.vue +++ b/src/pages/Worker/Department/Card/DepartmentBasicData.vue @@ -1,27 +1,16 @@ <script setup> -import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; - import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; - -const route = useRoute(); -const { t } = useI18n(); </script> <template> - <FormModel - :url="`Departments/${route.params.id}`" - model="department" - auto-load - class="full-width" - > + <FormModel model="Department" auto-load class="full-width"> <template #form="{ data, validate }"> <VnRow> <VnInput - :label="t('globals.name')" + :label="$t('globals.name')" v-model="data.name" :rules="validate('globals.name')" clearable @@ -29,33 +18,33 @@ const { t } = useI18n(); /> <VnInput v-model="data.code" - :label="t('globals.code')" + :label="$t('globals.code')" :rules="validate('globals.code')" clearable /> </VnRow> <VnRow> <VnInput - :label="t('department.chat')" + :label="$t('department.chat')" v-model="data.chatName" :rules="validate('department.chat')" clearable /> <VnInput v-model="data.notificationEmail" - :label="t('globals.params.email')" + :label="$t('globals.params.email')" :rules="validate('globals.params.email')" clearable /> </VnRow> <VnRow> <VnSelectWorker - :label="t('department.bossDepartment')" + :label="$t('department.bossDepartment')" v-model="data.workerFk" :rules="validate('department.bossDepartment')" /> <VnSelect - :label="t('department.selfConsumptionCustomer')" + :label="$t('department.selfConsumptionCustomer')" v-model="data.clientFk" url="Clients" option-value="id" @@ -67,11 +56,11 @@ const { t } = useI18n(); </VnRow> <VnRow> <QCheckbox - :label="t('department.telework')" + :label="$t('department.telework')" v-model="data.isTeleworking" /> <QCheckbox - :label="t('department.notifyOnErrors')" + :label="$t('department.notifyOnErrors')" v-model="data.hasToMistake" :false-value="0" :true-value="1" @@ -79,17 +68,17 @@ const { t } = useI18n(); </VnRow> <VnRow> <QCheckbox - :label="t('department.worksInProduction')" + :label="$t('department.worksInProduction')" v-model="data.isProduction" /> <QCheckbox - :label="t('department.hasToRefill')" + :label="$t('department.hasToRefill')" v-model="data.hasToRefill" /> </VnRow> <VnRow> <QCheckbox - :label="t('department.hasToSendMail')" + :label="$t('department.hasToSendMail')" v-model="data.hasToSendMail" /> </VnRow> diff --git a/src/pages/Department/Card/DepartmentCard.vue b/src/pages/Worker/Department/Card/DepartmentCard.vue similarity index 70% rename from src/pages/Department/Card/DepartmentCard.vue rename to src/pages/Worker/Department/Card/DepartmentCard.vue index 4b9fe419c..2e3f11521 100644 --- a/src/pages/Department/Card/DepartmentCard.vue +++ b/src/pages/Worker/Department/Card/DepartmentCard.vue @@ -1,13 +1,13 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; -import DepartmentDescriptor from 'pages/Department/Card/DepartmentDescriptor.vue'; +import DepartmentDescriptor from 'pages/Worker/Department/Card/DepartmentDescriptor.vue'; </script> <template> <VnCardBeta class="q-pa-md column items-center" v-bind="{ ...$attrs }" data-key="Department" - base-url="Departments" + url="Departments" :descriptor="DepartmentDescriptor" /> </template> diff --git a/src/pages/Department/Card/DepartmentDescriptor.vue b/src/pages/Worker/Department/Card/DepartmentDescriptor.vue similarity index 84% rename from src/pages/Department/Card/DepartmentDescriptor.vue rename to src/pages/Worker/Department/Card/DepartmentDescriptor.vue index b219ccfe1..4b7dfd9b8 100644 --- a/src/pages/Department/Card/DepartmentDescriptor.vue +++ b/src/pages/Worker/Department/Card/DepartmentDescriptor.vue @@ -5,7 +5,6 @@ import { useI18n } from 'vue-i18n'; import { useVnConfirm } from 'composables/useVnConfirm'; import VnLv from 'src/components/ui/VnLv.vue'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; @@ -32,15 +31,6 @@ const entityId = computed(() => { return $props.id || route.params.id; }); -const department = ref(); - -const data = ref(useCardDescription()); - -const setData = (entity) => { - if (!entity) return; - data.value = useCardDescription(entity.name, entity.id); -}; - const removeDepartment = async () => { await axios.post(`/Departments/${entityId.value}/removeChild`, entityId.value); router.push({ name: 'WorkerDepartment' }); @@ -52,19 +42,10 @@ const { openConfirmationModal } = useVnConfirm(); <template> <CardDescriptor ref="DepartmentDescriptorRef" - module="Department" :url="`Departments/${entityId}`" - :title="data.title" - :subtitle="data.subtitle" :summary="$props.summary" :to-module="{ name: 'WorkerDepartment' }" - @on-fetch=" - (data) => { - department = data; - setData(data); - } - " - data-key="department" + data-key="Department" > <template #menu="{}"> <QItem @@ -74,7 +55,7 @@ const { openConfirmationModal } = useVnConfirm(); openConfirmationModal( t('Are you sure you want to delete it?'), t('Delete department'), - removeDepartment + removeDepartment, ) " > diff --git a/src/pages/Department/Card/DepartmentDescriptorProxy.vue b/src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue similarity index 100% rename from src/pages/Department/Card/DepartmentDescriptorProxy.vue rename to src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue diff --git a/src/pages/Department/Card/DepartmentSummary.vue b/src/pages/Worker/Department/Card/DepartmentSummary.vue similarity index 99% rename from src/pages/Department/Card/DepartmentSummary.vue rename to src/pages/Worker/Department/Card/DepartmentSummary.vue index 3d481601f..3719137e4 100644 --- a/src/pages/Department/Card/DepartmentSummary.vue +++ b/src/pages/Worker/Department/Card/DepartmentSummary.vue @@ -27,7 +27,7 @@ onMounted(async () => { <template> <CardSummary - data-key="DepartmentSummary" + data-key="Department" ref="summary" :url="`Departments/${entityId}`" class="full-width" diff --git a/src/pages/Department/Card/DepartmentSummaryDialog.vue b/src/pages/Worker/Department/Card/DepartmentSummaryDialog.vue similarity index 100% rename from src/pages/Department/Card/DepartmentSummaryDialog.vue rename to src/pages/Worker/Department/Card/DepartmentSummaryDialog.vue diff --git a/src/pages/Worker/WorkerDepartmentTree.vue b/src/pages/Worker/WorkerDepartmentTree.vue index 9abf4e312..9baf5ee57 100644 --- a/src/pages/Worker/WorkerDepartmentTree.vue +++ b/src/pages/Worker/WorkerDepartmentTree.vue @@ -3,7 +3,7 @@ import { onMounted, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useState } from 'src/composables/useState'; import { useQuasar } from 'quasar'; -import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; import CreateDepartmentChild from './CreateDepartmentChild.vue'; import axios from 'axios'; import { useRouter } from 'vue-router'; @@ -173,7 +173,7 @@ function handleEvent(type, event, node) { color="primary" flat icon="add" - shortcut="+" + v-shortcut="'+'" class="cursor-pointer" @click.stop="showCreateNodeForm(node.id)" > diff --git a/src/pages/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue index cbeeff2e9..03013f011 100644 --- a/src/pages/Zone/Card/ZoneBasicData.vue +++ b/src/pages/Zone/Card/ZoneBasicData.vue @@ -1,5 +1,7 @@ <script setup> import { useI18n } from 'vue-i18n'; +import { ref } from 'vue'; +import FetchData from 'components/FetchData.vue'; import FormModel from 'src/components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; @@ -7,10 +9,23 @@ import VnInputTime from 'src/components/common/VnInputTime.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; const { t } = useI18n(); +const validAddresses = ref([]); +const addresses = ref([]); + +const setFilteredAddresses = (data) => { + const validIds = new Set(validAddresses.value.map((item) => item.addressFk)); + addresses.value = data.filter((address) => validIds.has(address.id)); +}; </script> <template> - <FormModel :url="`Zones/${$route.params.id}`" auto-load model="zone"> + <FetchData + url="RoadmapAddresses" + auto-load + @on-fetch="(data) => (validAddresses = data)" + /> + <FetchData url="Addresses" auto-load @on-fetch="setFilteredAddresses" /> + <FormModel auto-load model="Zone"> <template #form="{ data, validate }"> <VnRow> <VnInput @@ -18,15 +33,15 @@ const { t } = useI18n(); :label="t('Name')" clearable v-model="data.name" + :required="true" /> </VnRow> - <VnRow> <VnSelect v-model="data.agencyModeFk" :rules="validate('zone.agencyModeFk')" - url="AgencyModes/isActive" - :fields="['id', 'name']" + url="AgencyModes/isActive" + :fields="['id', 'name']" :label="t('Agency')" emit-value map-options @@ -69,7 +84,7 @@ const { t } = useI18n(); type="number" min="0" /> - <VnInputTime v-model="data.hour" :label="t('Closing')" /> + <VnInputTime v-model="data.hour" :label="t('Closing')" :required="true" /> </VnRow> <VnRow> @@ -78,7 +93,7 @@ const { t } = useI18n(); :label="t('Price')" type="number" min="0" - required="true" + :required="true" clearable /> <VnInput @@ -86,7 +101,7 @@ const { t } = useI18n(); :label="t('Price optimum')" type="number" min="0" - required="true" + :required="true" clearable /> </VnRow> @@ -103,12 +118,14 @@ const { t } = useI18n(); v-model="data.addressFk" option-value="id" option-label="nickname" - url="Addresses" + :options="addresses" :fields="['id', 'nickname']" sort-by="id" hide-selected map-options :rules="validate('data.addressFk')" + :filter-options="['id']" + :where="filterWhere" /> </VnRow> <VnRow> diff --git a/src/pages/Zone/Card/ZoneCard.vue b/src/pages/Zone/Card/ZoneCard.vue index a470cd5bd..41daff5c0 100644 --- a/src/pages/Zone/Card/ZoneCard.vue +++ b/src/pages/Zone/Card/ZoneCard.vue @@ -1,13 +1,12 @@ <script setup> -import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import { computed } from 'vue'; import VnCard from 'components/common/VnCard.vue'; import ZoneDescriptor from './ZoneDescriptor.vue'; import ZoneFilterPanel from '../ZoneFilterPanel.vue'; +import filter from './ZoneFilter.js'; -const { t } = useI18n(); const route = useRoute(); const routeName = computed(() => route.name); @@ -19,15 +18,16 @@ function notIsLocations(ifIsFalse, ifIsTrue) { <template> <VnCard - data-key="zone" - :base-url="notIsLocations('Zones', undefined)" + data-key="Zone" + :url="notIsLocations('Zones', undefined)" :descriptor="ZoneDescriptor" + :filter="filter" :filter-panel="notIsLocations(ZoneFilterPanel, undefined)" :search-data-key="notIsLocations('ZoneList', undefined)" :searchbar-props="{ url: notIsLocations('Zones', 'ZoneLocations'), - label: notIsLocations(t('list.searchZone'), t('list.searchLocation')), - info: t('list.searchInfo'), + label: notIsLocations($t('list.searchZone'), $t('list.searchLocation')), + info: $t('list.searchInfo'), whereFilter: notIsLocations((value) => { return /^\d+$/.test(value) ? { id: value } diff --git a/src/pages/Zone/Card/ZoneDescriptor.vue b/src/pages/Zone/Card/ZoneDescriptor.vue index 8355c219e..27676212e 100644 --- a/src/pages/Zone/Card/ZoneDescriptor.vue +++ b/src/pages/Zone/Card/ZoneDescriptor.vue @@ -1,15 +1,14 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import { toTimeFormat } from 'src/filters/date'; import { toCurrency } from 'filters/index'; -import useCardDescription from 'src/composables/useCardDescription'; import ZoneDescriptorMenuItems from './ZoneDescriptorMenuItems.vue'; +import filter from './ZoneFilter.js'; const $props = defineProps({ id: { @@ -20,49 +19,22 @@ const $props = defineProps({ }); const route = useRoute(); -const { t } = useI18n(); - -const filter = { - include: [ - { - relation: 'agencyMode', - scope: { - fields: ['name', 'id'], - }, - }, - ], -}; - const entityId = computed(() => { return $props.id || route.params.id; }); - -const data = ref(useCardDescription()); -const setData = (entity) => { - data.value = useCardDescription(entity.ref, entity.id); -}; </script> <template> - <CardDescriptor - module="Zone" - :url="`Zones/${entityId}`" - :title="data.title" - :subtitle="data.subtitle" - :filter="filter" - @on-fetch="setData" - data-key="zoneData" - > + <CardDescriptor :url="`Zones/${entityId}`" :filter="filter" data-key="Zone"> <template #menu="{ entity }"> <ZoneDescriptorMenuItems :zone="entity" /> </template> <template #body="{ entity }"> - <VnLv :label="t('list.agency')" :value="entity.agencyMode.name" /> - <VnLv :label="t('zone.closing')" :value="toTimeFormat(entity.hour)" /> - <VnLv :label="t('zone.travelingDays')" :value="entity.travelingDays" /> - <VnLv :label="t('list.price')" :value="toCurrency(entity.price)" /> - <VnLv :label="t('zone.bonus')" :value="toCurrency(entity.bonus)" /> + <VnLv :label="$t('list.agency')" :value="entity.agencyMode?.name" /> + <VnLv :label="$t('zone.closing')" :value="toTimeFormat(entity.hour)" /> + <VnLv :label="$t('zone.travelingDays')" :value="entity.travelingDays" /> + <VnLv :label="$t('list.price')" :value="toCurrency(entity.price)" /> + <VnLv :label="$t('zone.bonus')" :value="toCurrency(entity.bonus)" /> </template> </CardDescriptor> </template> - diff --git a/src/pages/Zone/Card/ZoneEvents.vue b/src/pages/Zone/Card/ZoneEvents.vue index a5806bab9..1e6debd25 100644 --- a/src/pages/Zone/Card/ZoneEvents.vue +++ b/src/pages/Zone/Card/ZoneEvents.vue @@ -78,13 +78,13 @@ const onZoneEventFormClose = () => { { isNewMode: true, }, - true + true, ) " color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip class="text-no-wrap"> {{ t('eventsInclusionForm.addEvent') }} diff --git a/src/pages/Zone/Card/ZoneFilter.js b/src/pages/Zone/Card/ZoneFilter.js new file mode 100644 index 000000000..3298c7c8a --- /dev/null +++ b/src/pages/Zone/Card/ZoneFilter.js @@ -0,0 +1,10 @@ +export default { + include: [ + { + relation: 'agencyMode', + scope: { + fields: ['name', 'id'], + }, + }, + ], +}; diff --git a/src/pages/Zone/Card/ZoneSearchbar.vue b/src/pages/Zone/Card/ZoneSearchbar.vue index f7a59e97f..d1188a1e8 100644 --- a/src/pages/Zone/Card/ZoneSearchbar.vue +++ b/src/pages/Zone/Card/ZoneSearchbar.vue @@ -22,15 +22,50 @@ const exprBuilder = (param, value) => { return /^\d+$/.test(value) ? { id: value } : { name: { like: `%${value}%` } }; } }; + +const tableFilter = { + include: [ + { + relation: 'agencyMode', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'address', + scope: { + fields: ['id', 'nickname', 'provinceFk', 'postalCode'], + include: [ + { + relation: 'province', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'postcode', + scope: { + fields: ['code', 'townFk'], + include: { + relation: 'town', + scope: { + fields: ['id', 'name'], + }, + }, + }, + }, + ], + }, + }, + ], +}; </script> <template> <VnSearchbar data-key="ZonesList" url="Zones" - :filter="{ - include: { relation: 'agencyMode', scope: { fields: ['name'] } }, - }" + :filter="tableFilter" :expr-builder="exprBuilder" :label="t('list.searchZone')" :info="t('list.searchInfo')" diff --git a/src/pages/Zone/Card/ZoneSummary.vue b/src/pages/Zone/Card/ZoneSummary.vue index 124802633..5b29b495b 100644 --- a/src/pages/Zone/Card/ZoneSummary.vue +++ b/src/pages/Zone/Card/ZoneSummary.vue @@ -11,6 +11,7 @@ import { getUrl } from 'src/composables/getUrl'; import { toCurrency } from 'filters/index'; import { toTimeFormat } from 'src/filters/date'; import axios from 'axios'; +import filter from './ZoneFilter.js'; import ZoneDescriptorMenuItems from './ZoneDescriptorMenuItems.vue'; const route = useRoute(); @@ -26,19 +27,6 @@ const $props = defineProps({ const entityId = computed(() => $props.id || route.params.id); const zoneUrl = ref(); -const filter = computed(() => { - const filter = { - include: { - relation: 'agencyMode', - fields: ['name'], - }, - where: { - id: entityId, - }, - }; - return filter; -}); - const columns = computed(() => [ { label: t('list.name'), @@ -72,9 +60,9 @@ onMounted(async () => { <template> <CardSummary - data-key="ZoneSummary" + data-key="Zone" ref="summary" - url="Zones/findOne" + :url="`Zones/${entityId}`" :filter="filter" > <template #header="{ entity }"> diff --git a/src/pages/Zone/Card/ZoneWarehouses.vue b/src/pages/Zone/Card/ZoneWarehouses.vue index c96735697..165e9c840 100644 --- a/src/pages/Zone/Card/ZoneWarehouses.vue +++ b/src/pages/Zone/Card/ZoneWarehouses.vue @@ -109,7 +109,7 @@ const openCreateWarehouseForm = () => createWarehouseDialogRef.value.show(); icon="add" color="primary" @click="openCreateWarehouseForm()" - shortcut="+" + v-shortcut="'+'" > <QTooltip>{{ t('warehouses.add') }}</QTooltip> </QBtn> diff --git a/src/pages/Zone/Delivery/ZoneDeliveryList.vue b/src/pages/Zone/Delivery/ZoneDeliveryList.vue index 975cbdb67..e3ec8cb2d 100644 --- a/src/pages/Zone/Delivery/ZoneDeliveryList.vue +++ b/src/pages/Zone/Delivery/ZoneDeliveryList.vue @@ -74,7 +74,7 @@ async function remove(row) { </VnPaginate> </div> <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn @click="create" fab icon="add" shortcut="+" color="primary" /> + <QBtn @click="create" fab icon="add" v-shortcut="'+'" color="primary" /> </QPageSticky> </QPage> </template> diff --git a/src/pages/Zone/Upcoming/ZoneUpcomingList.vue b/src/pages/Zone/Upcoming/ZoneUpcomingList.vue index 5a7f0bb4c..7b5c2ddbc 100644 --- a/src/pages/Zone/Upcoming/ZoneUpcomingList.vue +++ b/src/pages/Zone/Upcoming/ZoneUpcomingList.vue @@ -74,7 +74,7 @@ async function remove(row) { </VnPaginate> </div> <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn @click="create" fab icon="add" shortcut="+" color="primary" /> + <QBtn @click="create" fab icon="add" v-shortcut="'+'" color="primary" /> </QPageSticky> </QPage> </template> diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue index e4a1774fe..4df84e4bd 100644 --- a/src/pages/Zone/ZoneList.vue +++ b/src/pages/Zone/ZoneList.vue @@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'; import { computed, ref } from 'vue'; import axios from 'axios'; -import { toCurrency } from 'src/filters'; +import { dashIfEmpty, toCurrency } from 'src/filters'; import { toTimeFormat } from 'src/filters/date'; import { useVnConfirm } from 'composables/useVnConfirm'; import useNotify from 'src/composables/useNotify.js'; @@ -17,7 +17,6 @@ import VnInputTime from 'src/components/common/VnInputTime.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import ZoneFilterPanel from './ZoneFilterPanel.vue'; import ZoneSearchbar from './Card/ZoneSearchbar.vue'; -import FetchData from 'src/components/FetchData.vue'; const { t } = useI18n(); const router = useRouter(); @@ -26,7 +25,6 @@ const { viewSummary } = useSummaryDialog(); const { openConfirmationModal } = useVnConfirm(); const tableRef = ref(); const warehouseOptions = ref([]); -const validAddresses = ref([]); const tableFilter = { include: [ @@ -131,6 +129,7 @@ const columns = computed(() => [ label: t('list.addressFk'), cardVisible: true, columnFilter: false, + columnClass: 'expand', }, { align: 'right', @@ -161,30 +160,18 @@ const handleClone = (id) => { openConfirmationModal( t('list.confirmCloneTitle'), t('list.confirmCloneSubtitle'), - () => clone(id) + () => clone(id), ); }; -function showValidAddresses(row) { - if (row.addressFk) { - const isValid = validAddresses.value.some( - (address) => address.addressFk === row.addressFk - ); - if (isValid) - return `${row.address?.nickname}, - ${row.address?.postcode?.town?.name} (${row.address?.province?.name})`; - else return '-'; - } - return '-'; +function formatRow(row) { + if (!row?.address) return '-'; + return dashIfEmpty(`${row?.address?.nickname}, + ${row?.address?.postcode?.town?.name} (${row?.address?.province?.name})`); } </script> <template> - <FetchData - url="RoadmapAddresses" - auto-load - @on-fetch="(data) => (validAddresses = data)" - /> <ZoneSearchbar /> <RightMenu> <template #right-panel> @@ -207,7 +194,7 @@ function showValidAddresses(row) { :right-search="false" > <template #column-addressFk="{ row }"> - {{ showValidAddresses(row) }} + {{ dashIfEmpty(formatRow(row)) }} </template> <template #more-create-dialog="{ data }"> <VnSelect diff --git a/src/router/modules/account/aliasCard.js b/src/router/modules/account/aliasCard.js index cbbd31e51..a5b00f44b 100644 --- a/src/router/modules/account/aliasCard.js +++ b/src/router/modules/account/aliasCard.js @@ -3,7 +3,7 @@ export default { path: ':id', component: () => import('src/pages/Account/Alias/Card/AliasCard.vue'), redirect: { name: 'AliasSummary' }, - meta: { menu: ['AliasBasicData', 'AliasUsers'] }, + meta: { moduleName: 'Alias', menu: ['AliasBasicData', 'AliasUsers'] }, children: [ { name: 'AliasSummary', diff --git a/src/router/modules/account/roleCard.js b/src/router/modules/account/roleCard.js index c36ce71b9..f8100071f 100644 --- a/src/router/modules/account/roleCard.js +++ b/src/router/modules/account/roleCard.js @@ -4,6 +4,7 @@ export default { component: () => import('src/pages/Account/Role/Card/RoleCard.vue'), redirect: { name: 'RoleSummary' }, meta: { + moduleName: 'Role', menu: ['RoleBasicData', 'SubRoles', 'InheritedRoles', 'RoleLog'], }, children: [ diff --git a/src/router/modules/entry.js b/src/router/modules/entry.js index f362c7653..b5656dc5f 100644 --- a/src/router/modules/entry.js +++ b/src/router/modules/entry.js @@ -6,13 +6,7 @@ const entryCard = { component: () => import('src/pages/Entry/Card/EntryCard.vue'), redirect: { name: 'EntrySummary' }, meta: { - menu: [ - 'EntryBasicData', - 'EntryBuys', - 'EntryNotes', - 'EntryDms', - 'EntryLog', - ], + menu: ['EntryBasicData', 'EntryBuys', 'EntryNotes', 'EntryDms', 'EntryLog'], }, children: [ { @@ -91,7 +85,7 @@ export default { 'EntryLatestBuys', 'EntryStockBought', 'EntryWasteRecalc', - ] + ], }, component: RouterView, redirect: { name: 'EntryMain' }, @@ -103,7 +97,7 @@ export default { redirect: { name: 'EntryIndexMain' }, children: [ { - path:'', + path: '', name: 'EntryIndexMain', redirect: { name: 'EntryList' }, component: () => import('src/pages/Entry/EntryList.vue'), @@ -115,6 +109,7 @@ export default { title: 'list', icon: 'view_list', }, + component: () => import('src/pages/Entry/EntryList.vue'), }, entryCard, ], @@ -127,7 +122,7 @@ export default { icon: 'add', }, component: () => import('src/pages/Entry/EntryCreate.vue'), - }, + }, { path: 'my', name: 'MyEntries', @@ -167,4 +162,4 @@ export default { ], }, ], -}; \ No newline at end of file +}; diff --git a/src/router/modules/route.js b/src/router/modules/route.js index 946ad3e15..835324d20 100644 --- a/src/router/modules/route.js +++ b/src/router/modules/route.js @@ -160,6 +160,36 @@ const roadmapCard = { ], }; +const vehicleCard = { + path: ':id', + name: 'VehicleCard', + component: () => import('src/pages/Route/Vehicle/Card/VehicleCard.vue'), + redirect: { name: 'VehicleSummary' }, + meta: { + menu: ['VehicleBasicData'], + }, + children: [ + { + name: 'VehicleSummary', + path: 'summary', + meta: { + title: 'summary', + icon: 'view_list', + }, + component: () => import('src/pages/Route/Vehicle/Card/VehicleSummary.vue'), + }, + { + name: 'VehicleBasicData', + path: 'basic-data', + meta: { + title: 'basicData', + icon: 'vn:settings', + }, + component: () => import('src/pages/Route/Vehicle/Card/VehicleBasicData.vue'), + }, + ], +}; + export default { name: 'Route', path: '/route', @@ -174,6 +204,7 @@ export default { 'RouteRoadmap', 'CmrList', 'AgencyList', + 'VehicleList', ], }, component: RouterView, @@ -280,6 +311,27 @@ export default { agencyCard, ], }, + { + path: 'vehicle', + name: 'RouteVehicle', + redirect: { name: 'VehicleList' }, + meta: { + title: 'vehicle', + icon: 'directions_car', + }, + component: () => import('src/pages/Route/Vehicle/VehicleList.vue'), + children: [ + { + path: 'list', + name: 'VehicleList', + meta: { + title: 'vehicleList', + icon: 'directions_car', + }, + }, + vehicleCard, + ], + }, ], }, ], diff --git a/src/router/modules/shelving.js b/src/router/modules/shelving.js index 55fb04278..c085dd8dc 100644 --- a/src/router/modules/shelving.js +++ b/src/router/modules/shelving.js @@ -3,7 +3,7 @@ import { RouterView } from 'vue-router'; const parkingCard = { name: 'ParkingCard', path: ':id', - component: () => import('src/pages/Parking/Card/ParkingCard.vue'), + component: () => import('src/pages/Shelving/Parking/Card/ParkingCard.vue'), redirect: { name: 'ParkingSummary' }, meta: { menu: ['ParkingBasicData', 'ParkingLog'], @@ -16,7 +16,7 @@ const parkingCard = { title: 'summary', icon: 'launch', }, - component: () => import('src/pages/Parking/Card/ParkingSummary.vue'), + component: () => import('src/pages/Shelving/Parking/Card/ParkingSummary.vue'), }, { path: 'basic-data', @@ -25,7 +25,8 @@ const parkingCard = { title: 'basicData', icon: 'vn:settings', }, - component: () => import('src/pages/Parking/Card/ParkingBasicData.vue'), + component: () => + import('src/pages/Shelving/Parking/Card/ParkingBasicData.vue'), }, { path: 'log', @@ -34,7 +35,7 @@ const parkingCard = { title: 'log', icon: 'history', }, - component: () => import('src/pages/Parking/Card/ParkingLog.vue'), + component: () => import('src/pages/Shelving/Parking/Card/ParkingLog.vue'), }, ], }; @@ -127,7 +128,7 @@ export default { title: 'parkingList', icon: 'view_list', }, - component: () => import('src/pages/Parking/ParkingList.vue'), + component: () => import('src/pages/Shelving/Parking/ParkingList.vue'), children: [ { path: 'list', diff --git a/src/router/modules/supplier.js b/src/router/modules/supplier.js index 4ece4c784..19763cdf3 100644 --- a/src/router/modules/supplier.js +++ b/src/router/modules/supplier.js @@ -1,19 +1,12 @@ import { RouterView } from 'vue-router'; -export default { - path: '/supplier', - name: 'Supplier', +const supplierCard = { + name: 'SupplierCard', + path: ':id', + component: () => import('src/pages/Supplier/Card/SupplierCard.vue'), + redirect: { name: 'SupplierSummary' }, meta: { - title: 'suppliers', - icon: 'vn:supplier', - moduleName: 'Supplier', - keyBinding: 'p', - }, - component: RouterView, - redirect: { name: 'SupplierMain' }, - menus: { - main: ['SupplierList'], - card: [ + menu: [ 'SupplierBasicData', 'SupplierFiscalData', 'SupplierBillingData', @@ -27,21 +20,165 @@ export default { 'SupplierDms', ], }, + children: [ + { + name: 'SupplierSummary', + path: 'summary', + meta: { + title: 'summary', + icon: 'launch', + }, + component: () => import('src/pages/Supplier/Card/SupplierSummary.vue'), + }, + { + path: 'basic-data', + name: 'SupplierBasicData', + meta: { + title: 'basicData', + icon: 'vn:settings', + }, + component: () => import('src/pages/Supplier/Card/SupplierBasicData.vue'), + }, + { + path: 'fiscal-data', + name: 'SupplierFiscalData', + meta: { + title: 'fiscalData', + icon: 'vn:dfiscales', + }, + component: () => import('src/pages/Supplier/Card/SupplierFiscalData.vue'), + }, + { + path: 'billing-data', + name: 'SupplierBillingData', + meta: { + title: 'billingData', + icon: 'vn:payment', + }, + component: () => import('src/pages/Supplier/Card/SupplierBillingData.vue'), + }, + { + path: 'log', + name: 'SupplierLog', + meta: { + title: 'log', + icon: 'vn:History', + }, + component: () => import('src/pages/Supplier/Card/SupplierLog.vue'), + }, + { + path: 'account', + name: 'SupplierAccounts', + meta: { + title: 'accounts', + icon: 'vn:credit', + }, + component: () => import('src/pages/Supplier/Card/SupplierAccounts.vue'), + }, + { + path: 'contact', + name: 'SupplierContacts', + meta: { + title: 'contacts', + icon: 'contact_phone', + }, + component: () => import('src/pages/Supplier/Card/SupplierContacts.vue'), + }, + { + path: 'address', + name: 'SupplierAddresses', + meta: { + title: 'addresses', + icon: 'vn:delivery', + }, + component: () => import('src/pages/Supplier/Card/SupplierAddresses.vue'), + }, + { + path: 'address/create', + name: 'SupplierAddressesCreate', + component: () => + import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'), + }, + { + path: 'balance', + name: 'SupplierBalance', + meta: { + title: 'balance', + icon: 'balance', + }, + component: () => import('src/pages/Supplier/Card/SupplierBalance.vue'), + }, + { + path: 'consumption', + name: 'SupplierConsumption', + meta: { + title: 'consumption', + icon: 'show_chart', + }, + component: () => import('src/pages/Supplier/Card/SupplierConsumption.vue'), + }, + { + path: 'agency-term', + name: 'SupplierAgencyTerm', + meta: { + title: 'agencyTerm', + icon: 'vn:agency-term', + }, + component: () => import('src/pages/Supplier/Card/SupplierAgencyTerm.vue'), + }, + { + path: 'dms', + name: 'SupplierDms', + meta: { + title: 'dms', + icon: 'smb_share', + }, + component: () => import('src/pages/Supplier/Card/SupplierDms.vue'), + }, + { + path: 'agency-term/create', + name: 'SupplierAgencyTermCreate', + component: () => + import('src/pages/Supplier/Card/SupplierAgencyTermCreate.vue'), + }, + ], +}; + +export default { + name: 'Supplier', + path: '/supplier', + meta: { + title: 'suppliers', + icon: 'vn:supplier', + moduleName: 'Supplier', + keyBinding: 'p', + menu: ['SupplierList'], + }, + component: RouterView, + redirect: { name: 'SupplierMain' }, children: [ { path: '', name: 'SupplierMain', component: () => import('src/components/common/VnModule.vue'), - redirect: { name: 'SupplierList' }, + redirect: { name: 'SupplierIndexMain' }, children: [ { - path: 'list', - name: 'SupplierList', - meta: { - title: 'list', - icon: 'view_list', - }, + path: '', + name: 'SupplierIndexMain', + redirect: { name: 'SupplierList' }, component: () => import('src/pages/Supplier/SupplierList.vue'), + children: [ + { + path: 'list', + name: 'SupplierList', + meta: { + title: 'list', + icon: 'view_list', + }, + }, + supplierCard, + ], }, { path: 'create', @@ -54,143 +191,5 @@ export default { }, ], }, - { - name: 'SupplierCard', - path: ':id', - component: () => import('src/pages/Supplier/Card/SupplierCard.vue'), - redirect: { name: 'SupplierSummary' }, - children: [ - { - name: 'SupplierSummary', - path: 'summary', - meta: { - title: 'summary', - icon: 'launch', - }, - component: () => - import('src/pages/Supplier/Card/SupplierSummary.vue'), - }, - { - path: 'basic-data', - name: 'SupplierBasicData', - meta: { - title: 'basicData', - icon: 'vn:settings', - }, - component: () => - import('src/pages/Supplier/Card/SupplierBasicData.vue'), - }, - { - path: 'fiscal-data', - name: 'SupplierFiscalData', - meta: { - title: 'fiscalData', - icon: 'vn:dfiscales', - }, - component: () => - import('src/pages/Supplier/Card/SupplierFiscalData.vue'), - }, - { - path: 'billing-data', - name: 'SupplierBillingData', - meta: { - title: 'billingData', - icon: 'vn:payment', - }, - component: () => - import('src/pages/Supplier/Card/SupplierBillingData.vue'), - }, - { - path: 'log', - name: 'SupplierLog', - meta: { - title: 'log', - icon: 'vn:History', - }, - component: () => import('src/pages/Supplier/Card/SupplierLog.vue'), - }, - { - path: 'account', - name: 'SupplierAccounts', - meta: { - title: 'accounts', - icon: 'vn:credit', - }, - component: () => - import('src/pages/Supplier/Card/SupplierAccounts.vue'), - }, - { - path: 'contact', - name: 'SupplierContacts', - meta: { - title: 'contacts', - icon: 'contact_phone', - }, - component: () => - import('src/pages/Supplier/Card/SupplierContacts.vue'), - }, - { - path: 'address', - name: 'SupplierAddresses', - meta: { - title: 'addresses', - icon: 'vn:delivery', - }, - component: () => - import('src/pages/Supplier/Card/SupplierAddresses.vue'), - }, - { - path: 'address/create', - name: 'SupplierAddressesCreate', - component: () => - import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'), - }, - { - path: 'balance', - name: 'SupplierBalance', - meta: { - title: 'balance', - icon: 'balance', - }, - component: () => - import('src/pages/Supplier/Card/SupplierBalance.vue'), - }, - { - path: 'consumption', - name: 'SupplierConsumption', - meta: { - title: 'consumption', - icon: 'show_chart', - }, - component: () => - import('src/pages/Supplier/Card/SupplierConsumption.vue'), - }, - { - path: 'agency-term', - name: 'SupplierAgencyTerm', - meta: { - title: 'agencyTerm', - icon: 'vn:agency-term', - }, - component: () => - import('src/pages/Supplier/Card/SupplierAgencyTerm.vue'), - }, - { - path: 'dms', - name: 'SupplierDms', - meta: { - title: 'dms', - icon: 'smb_share', - }, - component: () => import('src/pages/Supplier/Card/SupplierDms.vue'), - }, - { - path: 'agency-term/create', - name: 'SupplierAgencyTermCreate', - component: () => - import('src/pages/Supplier/Card/SupplierAgencyTermCreate.vue'), - }, - ], - }, ], }; diff --git a/src/router/modules/ticket.js b/src/router/modules/ticket.js index e5b423f64..bfcb78787 100644 --- a/src/router/modules/ticket.js +++ b/src/router/modules/ticket.js @@ -192,7 +192,13 @@ export default { icon: 'vn:ticket', moduleName: 'Ticket', keyBinding: 't', - menu: ['TicketList', 'TicketAdvance', 'TicketWeekly', 'TicketFuture'], + menu: [ + 'TicketList', + 'TicketAdvance', + 'TicketWeekly', + 'TicketFuture', + 'TicketNegative', + ], }, component: RouterView, redirect: { name: 'TicketMain' }, @@ -229,6 +235,32 @@ export default { }, component: () => import('src/pages/Ticket/TicketCreate.vue'), }, + { + path: 'negative', + redirect: { name: 'TicketNegative' }, + children: [ + { + name: 'TicketNegative', + meta: { + title: 'negative', + icon: 'exposure', + }, + component: () => + import('src/pages/Ticket/Negative/TicketLackList.vue'), + path: '', + }, + { + name: 'NegativeDetail', + path: ':id', + meta: { + title: 'summary', + icon: 'launch', + }, + component: () => + import('src/pages/Ticket/Negative/TicketLackDetail.vue'), + }, + ], + }, { path: 'weekly', name: 'TicketWeekly', diff --git a/src/router/modules/worker.js b/src/router/modules/worker.js index 1d013c596..3eb95a96e 100644 --- a/src/router/modules/worker.js +++ b/src/router/modules/worker.js @@ -201,9 +201,10 @@ const workerCard = { const departmentCard = { name: 'DepartmentCard', path: ':id', - component: () => import('src/pages/Department/Card/DepartmentCard.vue'), + component: () => import('src/pages/Worker/Department/Card/DepartmentCard.vue'), redirect: { name: 'DepartmentSummary' }, meta: { + moduleName: 'Department', menu: ['DepartmentBasicData'], }, children: [ @@ -214,7 +215,8 @@ const departmentCard = { title: 'summary', icon: 'launch', }, - component: () => import('src/pages/Department/Card/DepartmentSummary.vue'), + component: () => + import('src/pages/Worker/Department/Card/DepartmentSummary.vue'), }, { path: 'basic-data', @@ -223,7 +225,8 @@ const departmentCard = { title: 'basicData', icon: 'vn:settings', }, - component: () => import('src/pages/Department/Card/DepartmentBasicData.vue'), + component: () => + import('src/pages/Worker/Department/Card/DepartmentBasicData.vue'), }, ], }; diff --git a/src/stores/__tests__/useNavigationStore.spec.js b/src/stores/__tests__/useNavigationStore.spec.js new file mode 100644 index 000000000..c5df6157e --- /dev/null +++ b/src/stores/__tests__/useNavigationStore.spec.js @@ -0,0 +1,153 @@ +import { setActivePinia, createPinia } from 'pinia'; +import { describe, beforeEach, afterEach, it, expect, vi, beforeAll } from 'vitest'; +import { useNavigationStore } from '../useNavigationStore'; +import axios from 'axios'; + +let store; + +vi.mock('src/router/modules', () => [ + { name: 'Item', meta: {} }, + { name: 'Shelving', meta: {} }, + { name: 'Order', meta: {} }, +]); + +vi.mock('src/filters', () => ({ + toLowerCamel: vi.fn((name) => name.toLowerCase()), +})); + +const modulesMock = [ + { + name: 'Item', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'item', + isPinned: true, + }, + { + name: 'Shelving', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'shelving', + isPinned: false, + }, + { + name: 'Order', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'order', + isPinned: false, + }, +]; + +const pinnedModulesMock = [ + { + name: 'Item', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'item', + isPinned: true, + }, +]; + +describe('useNavigationStore', () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.spyOn(axios, 'get').mockResolvedValue({ data: true }); + store = useNavigationStore(); + store.getModules = vi.fn().mockReturnValue({ + value: modulesMock, + }); + store.getPinnedModules = vi.fn().mockReturnValue({ + value: pinnedModulesMock, + }); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return modules with correct structure', () => { + const store = useNavigationStore(); + const modules = store.getModules(); + + expect(modules.value).toEqual(modulesMock); + }); + + it('should return pinned modules', () => { + const store = useNavigationStore(); + const pinnedModules = store.getPinnedModules(); + + expect(pinnedModules.value).toEqual(pinnedModulesMock); + }); + + it('should toggle pinned modules', () => { + const store = useNavigationStore(); + + store.togglePinned('item'); + store.togglePinned('shelving'); + expect(store.pinnedModules).toEqual(['item', 'shelving']); + + store.togglePinned('item'); + expect(store.pinnedModules).toEqual(['shelving']); + }); + + it('should fetch pinned modules', async () => { + vi.spyOn(axios, 'get').mockResolvedValue({ + data: [{ id: 1, workerFk: 9, moduleFk: 'order', position: 1 }], + }); + const store = useNavigationStore(); + await store.fetchPinned(); + + expect(store.pinnedModules).toEqual(['order']); + }); + + it('should add menu item correctly', () => { + const store = useNavigationStore(); + const module = 'customer'; + const parent = []; + const route = { + name: 'customer', + title: 'Customer', + icon: 'customer', + meta: { + keyBinding: 'ctrl+shift+c', + name: 'customer', + title: 'Customer', + icon: 'customer', + menu: 'customer', + menuChildren: [{ name: 'customer', title: 'Customer', icon: 'customer' }], + }, + }; + + const result = store.addMenuItem(module, route, parent); + const expectedItem = { + children: [ + { + icon: 'customer', + name: 'customer', + title: 'globals.pageTitles.Customer', + }, + ], + icon: 'customer', + keyBinding: 'ctrl+shift+c', + name: 'customer', + title: 'globals.pageTitles.Customer', + }; + expect(result).toEqual(expectedItem); + expect(parent.length).toBe(1); + expect(parent).toEqual([expectedItem]); + }); + + it('should not add menu item if condition is not met', () => { + const store = useNavigationStore(); + const module = 'testModule'; + const route = { meta: { hidden: true, menuchildren: {} } }; + const parent = []; + const result = store.addMenuItem(module, route, parent); + expect(result).toBeUndefined(); + expect(parent.length).toBe(0); + }); +}); diff --git a/src/stores/useArrayDataStore.js b/src/stores/useArrayDataStore.js index 8d62fdb4a..b3996d1e3 100644 --- a/src/stores/useArrayDataStore.js +++ b/src/stores/useArrayDataStore.js @@ -19,6 +19,7 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => { page: 1, mapKey: 'id', keepData: false, + oneRecord: false, }; function get(key) { diff --git a/src/utils/notifyResults.js b/src/utils/notifyResults.js new file mode 100644 index 000000000..e87ad6c6f --- /dev/null +++ b/src/utils/notifyResults.js @@ -0,0 +1,19 @@ +import { Notify } from 'quasar'; + +export default function (results, key) { + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + const data = JSON.parse(result.value.config.data); + Notify.create({ + type: 'positive', + message: `Operación (${index + 1}) ${data[key]} completada con éxito.`, + }); + } else { + const data = JSON.parse(result.reason.config.data); + Notify.create({ + type: 'negative', + message: `Operación (${index + 1}) ${data[key]} fallida: ${result.reason.message}`, + }); + } + }); +} diff --git a/test/cypress/integration/Order/orderCatalog.spec.js b/test/cypress/integration/Order/orderCatalog.spec.js index cffc47f91..1770a6b56 100644 --- a/test/cypress/integration/Order/orderCatalog.spec.js +++ b/test/cypress/integration/Order/orderCatalog.spec.js @@ -45,7 +45,6 @@ describe('OrderCatalog', () => { ).type('{enter}'); cy.get(':nth-child(1) > [data-cy="catalogFilterCategory"]').click(); cy.dataCy('catalogFilterValueDialogBtn').last().click(); - cy.get('[data-cy="catalogFilterValueDialogTagSelect"]').click(); cy.selectOption("[data-cy='catalogFilterValueDialogTagSelect']", 'Tallos'); cy.dataCy('catalogFilterValueDialogValueInput').find('input').focus(); cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('2'); diff --git a/test/cypress/integration/entry/entryList.spec.js b/test/cypress/integration/entry/entryList.spec.js new file mode 100644 index 000000000..4f99f0cb6 --- /dev/null +++ b/test/cypress/integration/entry/entryList.spec.js @@ -0,0 +1,224 @@ +describe('Entry', () => { + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('buyer'); + cy.visit(`/#/entry/list`); + }); + + it('Filter deleted entries and other fields', () => { + createEntry(); + cy.get('.q-notification__message').eq(0).should('have.text', 'Data created'); + cy.waitForElement('[data-cy="entry-buys"]'); + deleteEntry(); + cy.typeSearchbar('{enter}'); + cy.get('span[title="Date"]').click().click(); + cy.typeSearchbar('{enter}'); + cy.url().should('include', 'order'); + cy.get('td[data-row-index="0"][data-col-field="landed"]').should( + 'have.text', + '-', + ); + }); + + it('Create entry, modify travel and add buys', () => { + createEntryAndBuy(); + cy.get('a[data-cy="EntryBasicData-menu-item"]').click(); + selectTravel('two'); + cy.saveCard(); + cy.get('.q-notification__message').eq(0).should('have.text', 'Data created'); + deleteEntry(); + }); + + it('Clone entry and recalculate rates', () => { + createEntry(); + + cy.waitForElement('[data-cy="entry-buys"]'); + + cy.url().then((previousUrl) => { + cy.get('[data-cy="descriptor-more-opts"]').click(); + cy.get('div[data-cy="clone-entry"]').should('be.visible').click(); + + cy.get('.q-notification__message').eq(1).should('have.text', 'Entry cloned'); + + cy.url() + .should('not.eq', previousUrl) + .then(() => { + cy.waitForElement('[data-cy="entry-buys"]'); + + cy.get('[data-cy="descriptor-more-opts"]').click(); + cy.get('div[data-cy="recalculate-rates"]').click(); + + cy.get('.q-notification__message') + .eq(2) + .should('have.text', 'Entry prices recalculated'); + + cy.get('[data-cy="descriptor-more-opts"]').click(); + deleteEntry(); + + cy.log(previousUrl); + + cy.visit(previousUrl); + + cy.waitForElement('[data-cy="entry-buys"]'); + deleteEntry(); + }); + }); + }); + + it('Should notify when entry is lock by another user', () => { + const checkLockMessage = () => { + cy.get('[data-cy="entry-lock-confirm"]').should('be.visible'); + cy.get('[data-cy="VnConfirm_message"] > span').should( + 'contain.text', + 'This entry has been locked by buyerNick', + ); + }; + + createEntry(); + goToEntryBuys(); + cy.get('.q-notification__message') + .eq(1) + .should('have.text', 'The entry has been locked successfully'); + + cy.login('logistic'); + cy.reload(); + checkLockMessage(); + cy.get('[data-cy="VnConfirm_cancel"]').click(); + cy.url().should('include', 'summary'); + + goToEntryBuys(); + checkLockMessage(); + cy.get('[data-cy="VnConfirm_confirm"]').click(); + cy.url().should('include', 'buys'); + + deleteEntry(); + }); + + it('Edit buys and use toolbar actions', () => { + const COLORS = { + negative: 'rgb(251, 82, 82)', + positive: 'rgb(200, 228, 132)', + enabled: 'rgb(255, 255, 255)', + disable: 'rgb(168, 168, 168)', + }; + + const selectCell = (field, row = 0) => + cy.get(`td[data-col-field="${field}"][data-row-index="${row}"]`); + const selectSpan = (field, row = 0) => selectCell(field, row).find('div > span'); + const selectButton = (cySelector) => cy.get(`button[data-cy="${cySelector}"]`); + const clickAndType = (field, value, row = 0) => { + selectCell(field, row).click().type(`${value}{esc}`); + }; + const checkText = (field, expectedText, row = 0) => + selectCell(field, row).should('have.text', expectedText); + const checkColor = (field, expectedColor, row = 0) => + selectSpan(field, row).should('have.css', 'color', expectedColor); + + createEntryAndBuy(); + + selectCell('isIgnored').click().click().type('{esc}'); + checkText('isIgnored', 'close'); + + clickAndType('stickers', '1'); + checkText('stickers', '0/01'); + checkText('quantity', '1'); + checkText('amount', '50.00'); + clickAndType('packing', '2'); + checkText('packing', '12'); + checkText('weight', '12.0'); + checkText('quantity', '12'); + checkText('amount', '600.00'); + checkColor('packing', COLORS.enabled); + + selectCell('groupingMode').click().click().click(); + checkColor('packing', COLORS.disable); + checkColor('grouping', COLORS.enabled); + + selectCell('buyingValue').click().clear().type('{backspace}{backspace}1'); + checkText('amount', '12.00'); + checkColor('minPrice', COLORS.disable); + + selectCell('hasMinPrice').click().click(); + checkColor('minPrice', COLORS.enabled); + selectCell('hasMinPrice').click(); + + cy.saveCard(); + cy.get('span[data-cy="footer-stickers"]').should('have.text', '1'); + cy.get('.q-notification__message').contains('Data saved'); + + selectButton('change-quantity-sign').should('be.disabled'); + selectButton('check-buy-amount').should('be.disabled'); + cy.get('tr.cursor-pointer > .q-table--col-auto-width > .q-checkbox').click(); + selectButton('change-quantity-sign').should('be.enabled'); + selectButton('check-buy-amount').should('be.enabled'); + + selectButton('change-quantity-sign').click(); + selectButton('set-negative-quantity').click(); + checkText('quantity', '-12'); + selectButton('set-positive-quantity').click(); + checkText('quantity', '12'); + checkColor('amount', COLORS.disable); + + selectButton('check-buy-amount').click(); + selectButton('uncheck-amount').click(); + checkColor('amount', COLORS.disable); + + selectButton('check-amount').click(); + checkColor('amount', COLORS.positive); + cy.saveCard(); + + cy.get('span[data-cy="footer-amount"]').should( + 'have.css', + 'color', + COLORS.positive, + ); + + deleteEntry(); + }); + + function goToEntryBuys() { + const entryBuySelector = 'a[data-cy="EntryBuys-menu-item"]'; + cy.get(entryBuySelector).should('be.visible'); + cy.waitForElement('[data-cy="entry-buys"]'); + cy.get(entryBuySelector).click(); + } + + function deleteEntry() { + cy.get('[data-cy="descriptor-more-opts"]').click(); + cy.waitForElement('div[data-cy="delete-entry"]'); + cy.get('div[data-cy="delete-entry"]').should('be.visible').click(); + cy.url().should('include', 'list'); + } + + function createEntryAndBuy() { + createEntry(); + createBuy(); + } + + function createEntry() { + cy.get('button[data-cy="vnTableCreateBtn"]').click(); + selectTravel('one'); + cy.get('button[data-cy="FormModelPopup_save"]').click(); + cy.url().should('include', 'summary'); + cy.get('.q-notification__message').eq(0).should('have.text', 'Data created'); + } + + function selectTravel(warehouse) { + cy.get('i[data-cy="Travel_icon"]').click(); + cy.get('input[data-cy="Warehouse Out_select"]').type(warehouse); + cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click(); + cy.get('button[data-cy="save-filter-travel-form"]').click(); + cy.get('tr').eq(1).click(); + } + + function createBuy() { + cy.get('a[data-cy="EntryBuys-menu-item"]').click(); + cy.get('a[data-cy="EntryBuys-menu-item"]').click(); + cy.get('button[data-cy="vnTableCreateBtn"]').click(); + + cy.get('input[data-cy="itemFk-create-popup"]').type('1'); + cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click(); + cy.get('input[data-cy="Grouping mode_select"]').should('have.value', 'packing'); + cy.get('button[data-cy="FormModelPopup_save"]').click(); + } +}); diff --git a/test/cypress/integration/entry/stockBought.spec.js b/test/cypress/integration/entry/stockBought.spec.js index 078ad19cc..bc36156b4 100644 --- a/test/cypress/integration/entry/stockBought.spec.js +++ b/test/cypress/integration/entry/stockBought.spec.js @@ -6,6 +6,7 @@ describe('EntryStockBought', () => { }); it('Should edit the reserved space', () => { cy.get('.q-field__native.q-placeholder').should('have.value', '01/01/2001'); + cy.get('[data-col-field="reserve"][data-row-index="0"]').click(); cy.get('input[name="reserve"]').type('10{enter}'); cy.get('button[title="Save"]').click(); cy.get('.q-notification__message').should('have.text', 'Data saved'); @@ -15,25 +16,35 @@ describe('EntryStockBought', () => { cy.get('input[aria-label="Reserve"]').type('1'); cy.get('input[aria-label="Date"]').eq(1).clear(); cy.get('input[aria-label="Date"]').eq(1).type('01-01'); - cy.get('input[aria-label="Buyer"]').type('buyerboss{downarrow}{enter}'); + cy.get('input[aria-label="Buyer"]').type('buyerBossNick'); + cy.get('div[role="listbox"] > div > div[role="option"]') + .eq(0) + .should('be.visible') + .click(); + + cy.get('[data-cy="FormModelPopup_save"]').click(); cy.get('.q-notification__message').should('have.text', 'Data created'); + + cy.get('[data-col-field="reserve"][data-row-index="1"]').click().clear(); + cy.get('[data-cy="searchBtn"]').eq(1).click(); + cy.get('.q-table__bottom.row.items-center.q-table__bottom--nodata') + .should('have.text', 'warningNo data available') + .type('{esc}'); + cy.get('[data-col-field="reserve"][data-row-index="1"]') + .click() + .type('{backspace}{enter}'); + cy.get('[data-cy="crudModelDefaultSaveBtn"]').should('be.enabled').click(); + cy.get('.q-notification__message').eq(1).should('have.text', 'Data saved'); }); it('Should check detail for the buyer', () => { - cy.get(':nth-child(1) > .sticky > .q-btn > .q-btn__content > .q-icon').click(); + cy.get('[data-cy="searchBtn"]').eq(0).click(); cy.get('tBody > tr').eq(1).its('length').should('eq', 1); }); - it('Should check detail for the buyerBoss and had no content', () => { - cy.get(':nth-child(2) > .sticky > .q-btn > .q-btn__content > .q-icon').click(); - cy.get('.q-table__bottom.row.items-center.q-table__bottom--nodata').should( - 'have.text', - 'warningNo data available' - ); - }); + it('Should edit travel m3 and refresh', () => { - cy.get('.vn-row > div > .q-btn > .q-btn__content > .q-icon').click(); - cy.get('input[aria-label="m3"]').clear(); - cy.get('input[aria-label="m3"]').type('60'); - cy.get('.q-mt-lg > .q-btn--standard > .q-btn__content > .block').click(); + cy.get('[data-cy="edit-travel"]').should('be.visible').click(); + cy.get('input[aria-label="m3"]').clear().type('60'); + cy.get('[data-cy="FormModelPopup_save"]').click(); cy.get('.vn-row > div > :nth-child(2)').should('have.text', '60'); }); }); diff --git a/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js b/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js index 2016fca6d..11ca1bb59 100644 --- a/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js @@ -1,9 +1,9 @@ /// <reference types="cypress" /> describe('InvoiceInBasicData', () => { - const formInputs = '.q-form > .q-card input'; const firstFormSelect = '.q-card > .vn-row:nth-child(1) > .q-select'; - const documentBtns = '[data-cy="dms-buttons"] button'; const dialogInputs = '.q-dialog input'; + const resetBtn = '.q-btn-group--push > .q-btn--flat'; + const getDocumentBtns = (opt) => `[data-cy="dms-buttons"] > :nth-child(${opt})`; beforeEach(() => { cy.login('developer'); @@ -11,13 +11,16 @@ describe('InvoiceInBasicData', () => { }); it('should edit the provideer and supplier ref', () => { - cy.selectOption(firstFormSelect, 'Bros'); - cy.get('[title="Reset"]').click(); - cy.get(formInputs).eq(1).type('{selectall}4739'); - cy.saveCard(); + cy.dataCy('UnDeductibleVatSelect').type('4751000000'); + cy.get('.q-menu .q-item').contains('4751000000').click(); + cy.get(resetBtn).click(); - cy.get(`${firstFormSelect} input`).invoke('val').should('eq', 'Plants nick'); - cy.get(formInputs).eq(1).invoke('val').should('eq', '4739'); + cy.waitForElement('#formModel').within(() => { + cy.dataCy('vnSupplierSelect').type('Bros nick'); + }) + cy.get('.q-menu .q-item').contains('Bros nick').click(); + cy.saveCard(); + cy.get(`${firstFormSelect} input`).invoke('val').should('eq', 'Bros nick'); }); it('should edit, remove and create the dms data', () => { @@ -25,18 +28,18 @@ describe('InvoiceInBasicData', () => { const secondInput = "I don't know what posting here!"; //edit - cy.get(documentBtns).eq(1).click(); + cy.get(getDocumentBtns(2)).click(); cy.get(dialogInputs).eq(0).type(`{selectall}${firtsInput}`); cy.get('textarea').type(`{selectall}${secondInput}`); cy.get('[data-cy="FormModelPopup_save"]').click(); - cy.get(documentBtns).eq(1).click(); + cy.get(getDocumentBtns(2)).click(); cy.get(dialogInputs).eq(0).invoke('val').should('eq', firtsInput); cy.get('textarea').invoke('val').should('eq', secondInput); cy.get('[data-cy="FormModelPopup_save"]').click(); cy.checkNotification('Data saved'); //remove - cy.get(documentBtns).eq(2).click(); + cy.get(getDocumentBtns(3)).click(); cy.get('[data-cy="VnConfirm_confirm"]').click(); cy.checkNotification('Data saved'); @@ -46,7 +49,7 @@ describe('InvoiceInBasicData', () => { 'test/cypress/fixtures/image.jpg', { force: true, - } + }, ); cy.get('[data-cy="FormModelPopup_save"]').click(); cy.checkNotification('Data saved'); diff --git a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js index f8b403a45..1e7ce1003 100644 --- a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js @@ -36,7 +36,7 @@ describe('InvoiceInVat', () => { cy.get(dialogInputs).eq(0).type(randomInt); cy.get(dialogInputs).eq(1).type('This is a dummy expense'); - cy.get('button[type="submit"]').click(); + cy.get('[data-cy="FormModelPopup_save"]').click(); cy.get('.q-notification__message').should('have.text', 'Data created'); }); }); diff --git a/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js b/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js index 5f629df0b..02b7fbb43 100644 --- a/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js +++ b/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js @@ -7,9 +7,7 @@ describe('InvoiceOut negative bases', () => { }); it('should filter and download as CSV', () => { - cy.get( - ':nth-child(7) > .full-width > :nth-child(1) > .column > div.q-px-xs > .q-field > .q-field__inner > .q-field__control' - ).type('23{enter}'); + cy.get('input[name="ticketFk"]').type('23{enter}'); cy.get('#subToolbar > .q-btn').click(); cy.checkNotification('CSV downloaded successfully'); }); diff --git a/test/cypress/integration/item/ItemProposal.spec.js b/test/cypress/integration/item/ItemProposal.spec.js new file mode 100644 index 000000000..b3ba9f676 --- /dev/null +++ b/test/cypress/integration/item/ItemProposal.spec.js @@ -0,0 +1,11 @@ +/// <reference types="cypress" /> +describe('ItemProposal', () => { + beforeEach(() => { + const ticketId = 1; + + cy.login('developer'); + cy.visit(`/#/ticket/${ticketId}/summary`); + }); + + describe('Handle item proposal selected', () => {}); +}); diff --git a/test/cypress/integration/item/itemTag.spec.js b/test/cypress/integration/item/itemTag.spec.js index 17423bc51..425eaffe6 100644 --- a/test/cypress/integration/item/itemTag.spec.js +++ b/test/cypress/integration/item/itemTag.spec.js @@ -16,10 +16,7 @@ describe('Item tag', () => { cy.dataCy(newTag).should('be.visible').click().type('Genero{enter}'); cy.dataCy('tagGeneroValue').eq(1).should('be.visible'); cy.dataCy(saveBtn).click(); - cy.get('.q-notification__message').should( - 'have.text', - "The tag or priority can't be repeated for an item", - ); + cy.checkNotification("The tag or priority can't be repeated for an item"); }); it('should add a new tag', () => { diff --git a/test/cypress/integration/parking/parkingBasicData.spec.js b/test/cypress/integration/parking/parkingBasicData.spec.js index 0d130d335..f64f23ec8 100644 --- a/test/cypress/integration/parking/parkingBasicData.spec.js +++ b/test/cypress/integration/parking/parkingBasicData.spec.js @@ -13,11 +13,11 @@ describe('ParkingBasicData', () => { cy.get(sectorOpt).click(); cy.get(codeInput).eq(0).clear(); - cy.get(codeInput).eq(0).type(123); + cy.get(codeInput).eq(0).type('900-001'); cy.saveCard(); cy.get(sectorSelect).should('have.value', 'Second sector'); - cy.get(codeInput).should('have.value', 123); + cy.get(codeInput).should('have.value', '900-001'); }); }); diff --git a/test/cypress/integration/route/agency/agencyWorkCenter.spec.js b/test/cypress/integration/route/agency/agencyWorkCenter.spec.js index e28caea7c..82ec6626d 100644 --- a/test/cypress/integration/route/agency/agencyWorkCenter.spec.js +++ b/test/cypress/integration/route/agency/agencyWorkCenter.spec.js @@ -15,6 +15,7 @@ describe('AgencyWorkCenter', () => { // expect error when duplicate cy.get(createButton).click(); + cy.selectOption(workCenterCombobox, 'workCenterOne'); cy.get('[data-cy="FormModelPopup_save"]').click(); cy.checkNotification('This workCenter is already assigned to this agency'); cy.get('[data-cy="FormModelPopup_cancel"]').click(); diff --git a/test/cypress/integration/route/routeList.spec.js b/test/cypress/integration/route/routeList.spec.js index 4da43ce8e..976ce7352 100644 --- a/test/cypress/integration/route/routeList.spec.js +++ b/test/cypress/integration/route/routeList.spec.js @@ -4,9 +4,6 @@ describe('Route', () => { cy.login('developer'); cy.visit(`/#/route/extended-list`); }); - const getVnSelect = - '> :nth-child(1) > .column > .q-field > .q-field__inner > .q-field__control > .q-field__control-container'; - const getRowColumn = (row, column) => `:nth-child(${row}) > :nth-child(${column})`; it('Route list create route', () => { cy.addBtnClick(); @@ -17,15 +14,23 @@ describe('Route', () => { it('Route list search and edit', () => { cy.get('#searchbar input').type('{enter}'); - cy.get('input[name="description"]').type('routeTestOne{enter}'); + cy.get('[data-col-field="description"][data-row-index="0"]') + .click() + .type('routeTestOne{enter}'); cy.get('.q-table tr') .its('length') .then((rowCount) => { expect(rowCount).to.be.greaterThan(0); }); - cy.get(getRowColumn(1, 3) + getVnSelect).type('{downArrow}{enter}'); - cy.get(getRowColumn(1, 4) + getVnSelect).type('{downArrow}{enter}'); - cy.get(getRowColumn(1, 5) + getVnSelect).type('{downArrow}{enter}'); + cy.get('[data-col-field="workerFk"][data-row-index="0"]') + .click() + .type('{downArrow}{enter}'); + cy.get('[data-col-field="agencyModeFk"][data-row-index="0"]') + .click() + .type('{downArrow}{enter}'); + cy.get('[data-col-field="vehicleFk"][data-row-index="0"]') + .click() + .type('{downArrow}{enter}'); cy.get('button[title="Save"]').click(); cy.get('.q-notification__message').should('have.text', 'Data saved'); }); diff --git a/test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js b/test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js new file mode 100644 index 000000000..64b9ca0a0 --- /dev/null +++ b/test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js @@ -0,0 +1,13 @@ +describe('Vehicle', () => { + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('deliveryAssistant'); + cy.visit(`/#/route/vehicle/7`); + }); + + it('should delete a vehicle', () => { + cy.openActionsDescriptor(); + cy.get('[data-cy="delete"]').click(); + cy.checkNotification('Vehicle removed'); + }); +}); diff --git a/test/cypress/integration/ticket/negative/TicketLackDetail.spec.js b/test/cypress/integration/ticket/negative/TicketLackDetail.spec.js new file mode 100644 index 000000000..9ea1cff63 --- /dev/null +++ b/test/cypress/integration/ticket/negative/TicketLackDetail.spec.js @@ -0,0 +1,147 @@ +/// <reference types="cypress" /> +describe('Ticket Lack detail', () => { + beforeEach(() => { + cy.login('developer'); + cy.intercept('GET', /\/api\/Tickets\/itemLack\/5.*$/, { + statusCode: 200, + body: [ + { + saleFk: 33, + code: 'OK', + ticketFk: 142, + nickname: 'Malibu Point', + shipped: '2000-12-31T23:00:00.000Z', + hour: 0, + quantity: 50, + agName: 'Super-Man delivery', + alertLevel: 0, + stateName: 'OK', + stateId: 3, + itemFk: 5, + price: 1.79, + alertLevelCode: 'FREE', + zoneFk: 9, + zoneName: 'Zone superMan', + theoreticalhour: '2011-11-01T22:59:00.000Z', + isRookie: 1, + turno: 1, + peticionCompra: 1, + hasObservation: 1, + hasToIgnore: 1, + isBasket: 1, + minTimed: 0, + customerId: 1104, + customerName: 'Tony Stark', + observationTypeCode: 'administrative', + }, + ], + }).as('getItemLack'); + + cy.visit('/#/ticket/negative/5'); + cy.wait('@getItemLack'); + }); + describe('Table actions', () => { + it.skip('should display only one row in the lack list', () => { + cy.location('href').should('contain', '#/ticket/negative/5'); + + cy.get('[data-cy="changeItem"]').should('be.disabled'); + cy.get('[data-cy="changeState"]').should('be.disabled'); + cy.get('[data-cy="changeQuantity"]').should('be.disabled'); + cy.get('[data-cy="itemProposal"]').should('be.disabled'); + cy.get('[data-cy="transferLines"]').should('be.disabled'); + cy.get('tr.cursor-pointer > :nth-child(1)').click(); + cy.get('[data-cy="changeItem"]').should('be.enabled'); + cy.get('[data-cy="changeState"]').should('be.enabled'); + cy.get('[data-cy="changeQuantity"]').should('be.enabled'); + cy.get('[data-cy="itemProposal"]').should('be.enabled'); + cy.get('[data-cy="transferLines"]').should('be.enabled'); + }); + }); + describe('Item proposal', () => { + beforeEach(() => { + cy.get('tr.cursor-pointer > :nth-child(1)').click(); + + cy.intercept('GET', /\/api\/Items\/getSimilar\?.*$/, { + statusCode: 200, + body: [ + { + id: 1, + longName: 'Ranged weapon longbow 50cm', + subName: 'Stark Industries', + tag5: 'Color', + value5: 'Brown', + match5: 0, + match6: 0, + match7: 0, + match8: 1, + tag6: 'Categoria', + value6: '+1 precission', + tag7: 'Tallos', + value7: '1', + tag8: null, + value8: null, + available: 20, + calc_id: 6, + counter: 0, + minQuantity: 1, + visible: null, + price2: 1, + }, + { + id: 2, + longName: 'Ranged weapon longbow 100cm', + subName: 'Stark Industries', + tag5: 'Color', + value5: 'Brown', + match5: 0, + match6: 1, + match7: 0, + match8: 1, + tag6: 'Categoria', + value6: '+1 precission', + tag7: 'Tallos', + value7: '1', + tag8: null, + value8: null, + available: 50, + calc_id: 6, + counter: 1, + minQuantity: 5, + visible: null, + price2: 10, + }, + { + id: 3, + longName: 'Ranged weapon longbow 200cm', + subName: 'Stark Industries', + tag5: 'Color', + value5: 'Brown', + match5: 1, + match6: 1, + match7: 1, + match8: 1, + tag6: 'Categoria', + value6: '+1 precission', + tag7: 'Tallos', + value7: '1', + tag8: null, + value8: null, + available: 185, + calc_id: 6, + counter: 10, + minQuantity: 10, + visible: null, + price2: 100, + }, + ], + }).as('getItemGetSimilar'); + cy.get('[data-cy="itemProposal"]').click(); + cy.wait('@getItemGetSimilar'); + }); + describe('Replace item if', () => { + it.only('Quantity is less than available', () => { + cy.get(':nth-child(1) > .text-right > .q-btn').click(); + }); + }); + }); +}); diff --git a/test/cypress/integration/ticket/negative/TicketLackList.spec.js b/test/cypress/integration/ticket/negative/TicketLackList.spec.js new file mode 100644 index 000000000..01ab4f621 --- /dev/null +++ b/test/cypress/integration/ticket/negative/TicketLackList.spec.js @@ -0,0 +1,36 @@ +/// <reference types="cypress" /> +describe('Ticket Lack list', () => { + beforeEach(() => { + cy.login('developer'); + cy.intercept('GET', /Tickets\/itemLack\?.*$/, { + statusCode: 200, + body: [ + { + itemFk: 5, + longName: 'Ranged weapon pistol 9mm', + warehouseFk: 1, + producer: null, + size: 15, + category: null, + warehouse: 'Warehouse One', + lack: -50, + inkFk: 'SLV', + timed: '2025-01-25T22:59:00.000Z', + minTimed: '23:59', + originFk: 'Holand', + }, + ], + }).as('getLack'); + + cy.visit('/#/ticket/negative'); + }); + + describe('Table actions', () => { + it('should display only one row in the lack list', () => { + cy.wait('@getLack', { timeout: 10000 }); + + cy.get('.q-virtual-scroll__content > :nth-child(1) > .sticky').click(); + cy.location('href').should('contain', '#/ticket/negative/5'); + }); + }); +}); diff --git a/test/cypress/integration/ticket/ticketList.spec.js b/test/cypress/integration/ticket/ticketList.spec.js index 2984a4ee4..593021e6e 100644 --- a/test/cypress/integration/ticket/ticketList.spec.js +++ b/test/cypress/integration/ticket/ticketList.spec.js @@ -53,4 +53,29 @@ describe('TicketList', () => { cy.checkNotification('Data created'); cy.url().should('match', /\/ticket\/\d+\/summary/); }); + + it('should show the corerct problems', () => { + cy.intercept('GET', '**/api/Tickets/filter*', (req) => { + req.headers['cache-control'] = 'no-cache'; + req.headers['pragma'] = 'no-cache'; + req.headers['expires'] = '0'; + + req.on('response', (res) => { + delete res.headers['if-none-match']; + delete res.headers['if-modified-since']; + }); + }).as('ticket'); + + cy.get('[data-cy="Warehouse_select"]').type('Warehouse Five'); + cy.get('.q-menu .q-item').contains('Warehouse Five').click(); + cy.wait('@ticket').then((interception) => { + const data = interception.response.body[1]; + expect(data.hasComponentLack).to.equal(1); + expect(data.isTooLittle).to.equal(1); + expect(data.hasItemShortage).to.equal(1); + }); + cy.get('.icon-components').should('exist'); + cy.get('.icon-unavailable').should('exist'); + cy.get('.icon-isTooLittle').should('exist'); + }); }); diff --git a/test/cypress/integration/vnComponent/VnShortcut.spec.js b/test/cypress/integration/vnComponent/VnShortcut.spec.js index b49b4e964..e08c44635 100644 --- a/test/cypress/integration/vnComponent/VnShortcut.spec.js +++ b/test/cypress/integration/vnComponent/VnShortcut.spec.js @@ -28,6 +28,17 @@ describe('VnShortcuts', () => { }); cy.url().should('include', module); + if (['monitor', 'claim'].includes(module)) { + return; + } + cy.waitForElement('.q-page').should('exist'); + cy.dataCy('vnTableCreateBtn').should('exist'); + cy.get('.q-page').trigger('keydown', { + ctrlKey: true, + altKey: true, + key: '+', + }); + cy.get('#formModel').should('exist'); }); } }); diff --git a/test/cypress/integration/wagon/wagonType/wagonTypeCreate.spec.js b/test/cypress/integration/wagon/wagonType/wagonTypeCreate.spec.js index 343c1c127..2cd43984a 100644 --- a/test/cypress/integration/wagon/wagonType/wagonTypeCreate.spec.js +++ b/test/cypress/integration/wagon/wagonType/wagonTypeCreate.spec.js @@ -9,7 +9,7 @@ describe('WagonTypeCreate', () => { it('should create a new wagon type and then delete it', () => { cy.get('.q-page-sticky > div > .q-btn').click(); cy.get('input').first().type('Example for testing'); - cy.get('button[type="submit"]').click(); + cy.get('[data-cy="FormModelPopup_save"]').click(); cy.get('[title="Remove"] > .q-btn__content > .q-icon').first().click(); }); }); diff --git a/test/cypress/integration/zone/zoneBasicData.spec.js b/test/cypress/integration/zone/zoneBasicData.spec.js index 95a075fb3..70ded3f79 100644 --- a/test/cypress/integration/zone/zoneBasicData.spec.js +++ b/test/cypress/integration/zone/zoneBasicData.spec.js @@ -1,5 +1,6 @@ describe('ZoneBasicData', () => { const priceBasicData = '[data-cy="Price_input"]'; + const saveBtn = '.q-btn-group > .q-btn--standard'; beforeEach(() => { cy.viewport(1280, 720); @@ -8,20 +9,27 @@ describe('ZoneBasicData', () => { }); it('should throw an error if the name is empty', () => { - cy.get('[data-cy="zone-basic-data-name"] input').type('{selectall}{backspace}'); - cy.get('.q-btn-group > .q-btn--standard').click(); + cy.intercept('GET', /\/api\/Zones\/4./).as('zone'); + + cy.wait('@zone').then(() => { + cy.get('[data-cy="zone-basic-data-name"] input').type( + '{selectall}{backspace}', + ); + }); + + cy.get(saveBtn).click(); cy.checkNotification("can't be blank"); }); it('should throw an error if the price is empty', () => { cy.get(priceBasicData).clear(); - cy.get('.q-btn-group > .q-btn--standard').click(); + cy.get(saveBtn).click(); cy.checkNotification('cannot be blank'); }); it("should edit the basicData's zone", () => { cy.get('.q-card > :nth-child(1)').type(' modified'); - cy.get('.q-btn-group > .q-btn--standard').click(); + cy.get(saveBtn).click(); cy.checkNotification('Data saved'); }); }); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 2c93fbf84..aa4a1219e 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -87,36 +87,55 @@ Cypress.Commands.add('getValue', (selector) => { }); // Fill Inputs -Cypress.Commands.add('selectOption', (selector, option, timeout = 5000) => { +Cypress.Commands.add('selectOption', (selector, option, timeout = 2500) => { cy.waitForElement(selector, timeout); - cy.get(selector).click(); - cy.get(selector).invoke('data', 'url').as('dataUrl'); - cy.get(selector) - .clear() - .type(option) - .then(() => { - cy.get('.q-menu', { timeout }) - .should('be.visible') // Asegurarse de que el menú está visible - .and('exist') // Verificar que el menú existe - .then(() => { - cy.get('@dataUrl').then((url) => { - if (url) { - // Esperar a que el menú no esté visible (desaparezca) - cy.get('.q-menu').should('not.be.visible'); - // Ahora esperar a que el menú vuelva a aparecer - cy.get('.q-menu').should('be.visible').and('exist'); - } - }); - }); - }); - // Finalmente, seleccionar la opción deseada - cy.get('.q-menu:visible') // Asegurarse de que estamos dentro del menú visible - .find('.q-item') // Encontrar los elementos de las opciones - .contains(option) // Verificar que existe una opción que contenga el texto deseado - .click(); // Hacer clic en la opción + cy.get(selector, { timeout }) + .should('exist') + .should('be.visible') + .click() + .then(($el) => { + cy.wrap($el.is('input') ? $el : $el.find('input')) + .invoke('attr', 'aria-controls') + .then((ariaControl) => selectItem(selector, option, ariaControl)); + }); }); +function selectItem(selector, option, ariaControl, hasWrite = true) { + if (!hasWrite) cy.wait(100); + + getItems(ariaControl).then((items) => { + const matchingItem = items + .toArray() + .find((item) => item.innerText.includes(option)); + if (matchingItem) return cy.wrap(matchingItem).click(); + + if (hasWrite) cy.get(selector).clear().type(option, { delay: 0 }); + return selectItem(selector, option, ariaControl, false); + }); +} + +function getItems(ariaControl, startTime = Cypress._.now(), timeout = 2500) { + // Se intenta obtener la lista de opciones del desplegable de manera recursiva + return cy + .get('#' + ariaControl, { timeout }) + .should('exist') + .find('.q-item') + .should('exist') + .then(($items) => { + if (!$items?.length || $items.first().text().trim() === '') { + if (Cypress._.now() - startTime > timeout) { + throw new Error( + `getItems: Tiempo de espera (${timeout}ms) excedido.`, + ); + } + return getItems(ariaControl, startTime, timeout); + } + + return cy.wrap($items); + }); +} + Cypress.Commands.add('countSelectOptions', (selector, option) => { cy.waitForElement(selector); cy.get(selector).click({ force: true }); diff --git a/test/cypress/support/waitUntil.js b/test/cypress/support/waitUntil.js index 5fb47a2d8..359f8643f 100644 --- a/test/cypress/support/waitUntil.js +++ b/test/cypress/support/waitUntil.js @@ -1,7 +1,7 @@ const waitUntil = (subject, checkFunction, originalOptions = {}) => { if (!(checkFunction instanceof Function)) { throw new Error( - '`checkFunction` parameter should be a function. Found: ' + checkFunction + '`checkFunction` parameter should be a function. Found: ' + checkFunction, ); } From c67ae3e17e3261692185d92861e62084cbc68b99 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Tue, 25 Feb 2025 09:53:02 +0100 Subject: [PATCH 27/28] Revert "revert 1015acefb7e400be2d8b5958dba69b4d98276b34" This reverts commit 223a1ea4490ea6ad2a00c60297fd3c74cd713338. --- cypress.config.js | 15 +- quasar.config.js | 1 - src/boot/defaults/constants.js | 2 + src/boot/keyShortcut.js | 17 +- src/boot/qformMixin.js | 23 +- src/boot/quasar.js | 1 + src/components/CreateBankEntityForm.vue | 2 +- src/components/CrudModel.vue | 16 +- src/components/FilterTravelForm.vue | 4 +- src/components/FormModel.vue | 45 +- src/components/FormModelPopup.vue | 50 +- src/components/ItemsFilterPanel.vue | 4 +- src/components/LeftMenu.vue | 67 +- src/components/LeftMenuItem.vue | 1 + src/components/RefundInvoiceForm.vue | 15 +- src/components/TicketProblems.vue | 82 +- src/components/TransferInvoiceForm.vue | 15 +- src/components/VnTable/VnColumn.vue | 51 +- src/components/VnTable/VnFilter.vue | 58 +- src/components/VnTable/VnOrder.vue | 101 +- src/components/VnTable/VnTable.vue | 572 ++++++-- src/components/VnTable/VnTableFilter.vue | 57 +- src/components/VnTable/VnVisibleColumn.vue | 19 +- src/components/__tests__/FormModel.spec.js | 12 +- src/components/__tests__/Leftmenu.spec.js | 376 ++++- src/components/__tests__/UserPanel.spec.js | 100 +- src/components/common/VnCard.vue | 39 +- src/components/common/VnCardBeta.vue | 61 +- src/components/common/VnCheckbox.vue | 43 + src/components/common/VnColor.vue | 32 + src/components/common/VnComponent.vue | 6 +- src/components/common/VnDmsList.vue | 12 +- src/components/common/VnInput.vue | 22 +- src/components/common/VnInputDate.vue | 8 +- src/components/common/VnInputNumber.vue | 2 + src/components/common/VnPopupProxy.vue | 38 + src/components/common/VnSection.vue | 9 +- src/components/common/VnSelect.vue | 22 +- src/components/common/VnSelectCache.vue | 4 +- src/components/common/VnSelectDialog.vue | 2 - src/components/common/VnSelectSupplier.vue | 6 +- .../common/VnSelectTravelExtended.vue | 50 + .../common/__tests__/VnNotes.spec.js | 151 +- src/components/ui/CardDescriptor.vue | 46 +- src/components/ui/CardSummary.vue | 14 +- src/components/ui/SkeletonDescriptor.vue | 65 +- src/components/ui/VnConfirm.vue | 3 +- src/components/ui/VnFilterPanel.vue | 16 +- src/components/ui/VnMoreOptions.vue | 2 +- src/components/ui/VnNotes.vue | 94 +- src/components/ui/VnStockValueDisplay.vue | 41 + src/components/ui/VnSubToolbar.vue | 11 +- .../ui/__tests__/CardSummary.spec.js | 14 +- .../__tests__/useArrayData.spec.js | 29 +- src/composables/checkEntryLock.js | 65 + src/composables/getColAlign.js | 22 + src/composables/useArrayData.js | 13 +- src/composables/useRole.js | 10 + src/css/app.scss | 28 +- src/css/quasar.variables.scss | 6 +- src/filters/toDate.js | 11 +- src/i18n/locale/en.yml | 117 ++ src/i18n/locale/es.yml | 225 ++- src/layouts/MainLayout.vue | 2 +- src/layouts/OutLayout.vue | 5 +- src/pages/Account/AccountAliasList.vue | 10 +- src/pages/Account/AccountExprBuilder.js | 18 + src/pages/Account/AccountList.vue | 26 +- src/pages/Account/Alias/AliasExprBuilder.js | 8 + src/pages/Account/Alias/Card/AliasCard.vue | 10 +- .../Account/Alias/Card/AliasDescriptor.vue | 11 +- src/pages/Account/Alias/Card/AliasSummary.vue | 19 +- src/pages/Account/Card/AccountBasicData.vue | 38 +- src/pages/Account/Card/AccountCard.vue | 10 +- src/pages/Account/Card/AccountDescriptor.vue | 43 +- .../Account/Card/AccountDescriptorMenu.vue | 27 +- src/pages/Account/Card/AccountFilter.js | 3 + src/pages/Account/Card/AccountMailAlias.vue | 7 +- src/pages/Account/Card/AccountSummary.vue | 41 +- src/pages/Account/Role/AccountRoles.vue | 18 +- src/pages/Account/Role/Card/RoleBasicData.vue | 14 +- src/pages/Account/Role/Card/RoleCard.vue | 7 +- .../Account/Role/Card/RoleDescriptor.vue | 16 +- src/pages/Account/Role/Card/RoleSummary.vue | 23 +- src/pages/Account/Role/Card/SubRoles.vue | 6 +- src/pages/Account/Role/RoleExprBuilder.js | 16 + src/pages/Claim/Card/ClaimBasicData.vue | 1 - src/pages/Claim/Card/ClaimCard.vue | 9 +- src/pages/Claim/Card/ClaimDescriptor.vue | 17 +- src/pages/Claim/Card/ClaimLines.vue | 8 +- src/pages/Claim/Card/ClaimNotes.vue | 3 +- src/pages/Claim/Card/ClaimPhoto.vue | 4 +- src/pages/Claim/ClaimList.vue | 2 +- src/pages/Customer/Card/CustomerAddress.vue | 8 +- src/pages/Customer/Card/CustomerBalance.vue | 4 +- src/pages/Customer/Card/CustomerBasicData.vue | 4 +- .../Customer/Card/CustomerBillingData.vue | 2 +- src/pages/Customer/Card/CustomerCard.vue | 4 +- .../Customer/Card/CustomerConsumption.vue | 95 +- src/pages/Customer/Card/CustomerContacts.vue | 2 +- .../Customer/Card/CustomerCreditContracts.vue | 2 +- .../Customer/Card/CustomerDescriptor.vue | 42 +- .../Customer/Card/CustomerDescriptorMenu.vue | 17 + .../Customer/Card/CustomerFileManagement.vue | 2 +- .../Customer/Card/CustomerFiscalData.vue | 32 +- src/pages/Customer/Card/CustomerNotes.vue | 1 + src/pages/Customer/Card/CustomerSamples.vue | 2 +- src/pages/Customer/Card/CustomerWebAccess.vue | 2 +- src/pages/Customer/CustomerFilter.vue | 6 +- src/pages/Customer/CustomerList.vue | 4 +- .../Customer/Defaulter/CustomerDefaulter.vue | 2 +- .../components/CustomerAddressEdit.vue | 4 +- .../components/CustomerNewPayment.vue | 6 +- .../components/CustomerSamplesCreate.vue | 9 +- src/pages/Customer/locale/en.yml | 3 + src/pages/Customer/locale/es.yml | 3 + src/pages/Entry/Card/EntryBasicData.vue | 63 +- src/pages/Entry/Card/EntryBuys.vue | 1232 +++++++++++------ src/pages/Entry/Card/EntryCard.vue | 6 +- src/pages/Entry/Card/EntryDescriptor.vue | 158 ++- src/pages/Entry/Card/EntryFilter.js | 17 +- src/pages/Entry/Card/EntryNotes.vue | 4 +- src/pages/Entry/Card/EntrySummary.vue | 388 ++---- src/pages/Entry/EntryFilter.vue | 257 ++-- src/pages/Entry/EntryList.vue | 368 +++-- src/pages/Entry/EntryStockBought.vue | 18 +- src/pages/Entry/EntryStockBoughtDetail.vue | 22 +- src/pages/Entry/locale/en.yml | 84 +- src/pages/Entry/locale/es.yml | 107 +- .../InvoiceIn/Card/InvoiceInBasicData.vue | 6 +- src/pages/InvoiceIn/Card/InvoiceInCard.vue | 41 +- .../InvoiceIn/Card/InvoiceInDescriptor.vue | 33 +- .../Card/InvoiceInDescriptorMenu.vue | 4 +- src/pages/InvoiceIn/Card/InvoiceInDueDay.vue | 26 +- src/pages/InvoiceIn/Card/InvoiceInFilter.js | 33 + .../InvoiceIn/Card/InvoiceInIntrastat.vue | 2 +- src/pages/InvoiceIn/Card/InvoiceInSummary.vue | 13 +- src/pages/InvoiceIn/Card/InvoiceInVat.vue | 78 +- src/pages/InvoiceIn/InvoiceInList.vue | 5 +- src/pages/InvoiceIn/InvoiceInToBook.vue | 56 +- src/pages/InvoiceIn/locale/en.yml | 5 +- src/pages/InvoiceIn/locale/es.yml | 9 +- src/pages/InvoiceOut/Card/InvoiceOutCard.vue | 4 +- .../InvoiceOut/Card/InvoiceOutDescriptor.vue | 28 +- src/pages/InvoiceOut/Card/InvoiceOutFilter.js | 16 + src/pages/Item/Card/ItemBarcode.vue | 2 +- src/pages/Item/Card/ItemBasicData.vue | 42 +- src/pages/Item/Card/ItemBotanical.vue | 4 +- src/pages/Item/Card/ItemCard.vue | 2 +- src/pages/Item/Card/ItemDescriptor.vue | 26 +- src/pages/Item/Card/ItemDescriptorProxy.vue | 6 +- src/pages/Item/Card/ItemShelving.vue | 10 +- src/pages/Item/Card/ItemTags.vue | 2 +- src/pages/Item/ItemFixedPrice.vue | 16 +- .../Item/ItemType/Card/ItemTypeBasicData.vue | 7 +- src/pages/Item/ItemType/Card/ItemTypeCard.vue | 6 +- .../Item/ItemType/Card/ItemTypeDescriptor.vue | 40 +- .../Item/ItemType/Card/ItemTypeFilter.js | 8 + .../Item/ItemType/Card/ItemTypeSummary.vue | 15 +- .../{Card => components}/CreateGenusForm.vue | 0 .../{Card => components}/CreateSpecieForm.vue | 0 src/pages/Item/components/ItemProposal.vue | 332 +++++ .../Item/components/ItemProposalProxy.vue | 56 + src/pages/Item/locale/en.yml | 24 +- src/pages/Item/locale/es.yml | 31 +- src/pages/Monitor/MonitorOrders.vue | 2 +- src/pages/Monitor/locale/en.yml | 1 + src/pages/Monitor/locale/es.yml | 1 + .../Order/Card/CatalogFilterValueDialog.vue | 2 +- src/pages/Order/Card/OrderBasicData.vue | 6 +- src/pages/Order/Card/OrderCard.vue | 4 +- src/pages/Order/Card/OrderCatalogFilter.vue | 4 +- .../Order/Card/OrderCatalogItemDialog.vue | 8 +- src/pages/Order/Card/OrderDescriptor.vue | 38 +- src/pages/Order/Card/OrderFilter.js | 26 + src/pages/Order/Card/OrderLines.vue | 4 +- src/pages/Order/Card/OrderSummary.vue | 2 +- src/pages/Order/OrderList.vue | 7 +- src/pages/Route/Agency/AgencyList.vue | 4 +- .../Route/Agency/Card/AgencyBasicData.vue | 2 +- src/pages/Route/Agency/Card/AgencyCard.vue | 2 +- .../Route/Agency/Card/AgencyDescriptor.vue | 1 - .../Route/Agency/Card/AgencyWorkcenter.vue | 2 +- src/pages/Route/Card/RouteCard.vue | 5 +- src/pages/Route/Card/RouteDescriptor.vue | 70 +- src/pages/Route/Card/RouteFilter.js | 39 + src/pages/Route/Card/RouteFilter.vue | 2 +- src/pages/Route/Card/RouteForm.vue | 54 +- src/pages/Route/Roadmap/RoadmapBasicData.vue | 5 +- src/pages/Route/Roadmap/RoadmapCard.vue | 2 +- src/pages/Route/Roadmap/RoadmapDescriptor.vue | 18 +- src/pages/Route/Roadmap/RoadmapFilter.js | 3 + src/pages/Route/Roadmap/RoadmapStops.vue | 2 +- src/pages/Route/Roadmap/RoadmapSummary.vue | 3 +- src/pages/Route/RouteExtendedList.vue | 152 +- src/pages/Route/RouteList.vue | 31 + src/pages/Route/RouteTickets.vue | 18 +- .../Route/Vehicle/Card/VehicleBasicData.vue | 162 +++ src/pages/Route/Vehicle/Card/VehicleCard.vue | 13 + .../Route/Vehicle/Card/VehicleDescriptor.vue | 49 + .../Route/Vehicle/Card/VehicleSummary.vue | 127 ++ src/pages/Route/Vehicle/VehicleFilter.js | 76 + src/pages/Route/Vehicle/VehicleList.vue | 224 +++ src/pages/Route/Vehicle/locale/en.yml | 20 + src/pages/Route/Vehicle/locale/es.yml | 20 + src/pages/Shelving/Card/ShelvingCard.vue | 4 +- .../Shelving/Card/ShelvingDescriptor.vue | 30 +- src/pages/Shelving/Card/ShelvingFilter.js | 15 + src/pages/Shelving/Card/ShelvingForm.vue | 32 +- src/pages/Shelving/Card/ShelvingSearchbar.vue | 8 +- src/pages/Shelving/Card/ShelvingSummary.vue | 37 +- .../Parking/Card/ParkingBasicData.vue | 18 +- .../Parking/Card/ParkingCard.vue | 6 +- .../Parking/Card/ParkingDescriptor.vue | 16 +- .../Shelving/Parking/Card/ParkingFilter.js | 4 + .../Parking/Card/ParkingLog.vue | 0 .../Parking/Card/ParkingSummary.vue | 0 .../Shelving/Parking/ParkingExprBuilder.js | 10 + .../{ => Shelving}/Parking/ParkingFilter.vue | 0 .../{ => Shelving}/Parking/ParkingList.vue | 13 +- .../{ => Shelving}/Parking/locale/en.yml | 0 .../{ => Shelving}/Parking/locale/es.yml | 0 src/pages/Shelving/ShelvingExprBuilder.js | 10 + src/pages/Shelving/ShelvingList.vue | 26 +- src/pages/Supplier/Card/SupplierAccounts.vue | 6 +- src/pages/Supplier/Card/SupplierAddresses.vue | 2 +- .../Supplier/Card/SupplierAgencyTerm.vue | 2 +- src/pages/Supplier/Card/SupplierBasicData.vue | 3 +- src/pages/Supplier/Card/SupplierCard.vue | 16 +- .../Supplier/Card/SupplierConsumption.vue | 103 +- src/pages/Supplier/Card/SupplierContacts.vue | 2 +- .../Supplier/Card/SupplierDescriptor.vue | 49 +- src/pages/Supplier/Card/SupplierFilter.js | 35 + .../Supplier/Card/SupplierFiscalData.vue | 22 +- src/pages/Supplier/SupplierList.vue | 91 +- src/pages/Supplier/SupplierListFilter.vue | 122 -- .../Ticket/Card/BasicData/TicketBasicData.vue | 16 +- .../Card/BasicData/TicketBasicDataForm.vue | 4 +- .../Card/BasicData/TicketBasicDataView.vue | 116 +- src/pages/Ticket/Card/TicketCard.vue | 8 +- src/pages/Ticket/Card/TicketComponents.vue | 2 +- src/pages/Ticket/Card/TicketDescriptor.vue | 139 +- src/pages/Ticket/Card/TicketExpedition.vue | 2 +- src/pages/Ticket/Card/TicketFilter.js | 72 + src/pages/Ticket/Card/TicketNotes.vue | 4 +- src/pages/Ticket/Card/TicketPackage.vue | 4 +- src/pages/Ticket/Card/TicketSale.vue | 60 +- src/pages/Ticket/Card/TicketService.vue | 6 +- src/pages/Ticket/Card/TicketSplit.vue | 37 + src/pages/Ticket/Card/TicketSummary.vue | 81 +- src/pages/Ticket/Card/TicketTracking.vue | 4 +- src/pages/Ticket/Card/TicketTransfer.vue | 131 +- src/pages/Ticket/Card/TicketTransferProxy.vue | 54 + src/pages/Ticket/Card/components/split.js | 22 + .../Ticket/Negative/TicketLackDetail.vue | 198 +++ .../Ticket/Negative/TicketLackFilter.vue | 175 +++ src/pages/Ticket/Negative/TicketLackList.vue | 227 +++ src/pages/Ticket/Negative/TicketLackTable.vue | 356 +++++ .../Negative/components/ChangeItemDialog.vue | 90 ++ .../components/ChangeQuantityDialog.vue | 84 ++ .../Negative/components/ChangeStateDialog.vue | 91 ++ src/pages/Ticket/TicketFuture.vue | 555 +++----- src/pages/Ticket/TicketFutureFilter.vue | 4 +- src/pages/Ticket/locale/en.yml | 87 +- src/pages/Ticket/locale/es.yml | 83 ++ src/pages/Travel/Card/TravelBasicData.vue | 19 +- src/pages/Travel/Card/TravelCard.vue | 36 +- src/pages/Travel/Card/TravelDescriptor.vue | 1 - src/pages/Travel/Card/TravelFilter.js | 1 + src/pages/Travel/Card/TravelSummary.vue | 8 + src/pages/Travel/Card/TravelThermographs.vue | 2 +- src/pages/Travel/ExtraCommunityFilter.vue | 2 +- src/pages/Travel/TravelList.vue | 24 + src/pages/Wagon/Card/WagonCard.vue | 2 +- src/pages/Wagon/Type/WagonTypeList.vue | 8 +- src/pages/Worker/Card/WorkerBasicData.vue | 17 +- src/pages/Worker/Card/WorkerCalendar.vue | 32 +- .../Worker/Card/WorkerCalendarFilter.vue | 2 - src/pages/Worker/Card/WorkerCard.vue | 7 +- src/pages/Worker/Card/WorkerDescriptor.vue | 9 +- .../Worker/Card/WorkerDescriptorProxy.vue | 7 +- src/pages/Worker/Card/WorkerFormation.vue | 3 +- src/pages/Worker/Card/WorkerMedical.vue | 16 + src/pages/Worker/Card/WorkerOperator.vue | 19 +- src/pages/Worker/Card/WorkerPda.vue | 10 +- src/pages/Worker/Card/WorkerPit.vue | 2 +- src/pages/Worker/Card/WorkerSummary.vue | 2 +- src/pages/Worker/Card/WorkerTimeControl.vue | 16 +- .../Department/Card/DepartmentBasicData.vue | 35 +- .../Department/Card/DepartmentCard.vue | 4 +- .../Department/Card/DepartmentDescriptor.vue | 23 +- .../Card/DepartmentDescriptorProxy.vue | 0 .../Department/Card/DepartmentSummary.vue | 2 +- .../Card/DepartmentSummaryDialog.vue | 0 src/pages/Worker/WorkerDepartmentTree.vue | 4 +- src/pages/Zone/Card/ZoneBasicData.vue | 33 +- src/pages/Zone/Card/ZoneCard.vue | 12 +- src/pages/Zone/Card/ZoneDescriptor.vue | 44 +- src/pages/Zone/Card/ZoneEvents.vue | 4 +- src/pages/Zone/Card/ZoneFilter.js | 10 + src/pages/Zone/Card/ZoneSearchbar.vue | 41 +- src/pages/Zone/Card/ZoneSummary.vue | 18 +- src/pages/Zone/Card/ZoneWarehouses.vue | 2 +- src/pages/Zone/Delivery/ZoneDeliveryList.vue | 2 +- src/pages/Zone/Upcoming/ZoneUpcomingList.vue | 2 +- src/router/modules/account/aliasCard.js | 2 +- src/router/modules/account/roleCard.js | 1 + src/router/modules/entry.js | 17 +- src/router/modules/route.js | 52 + src/router/modules/shelving.js | 11 +- src/router/modules/supplier.js | 315 +++-- src/router/modules/ticket.js | 34 +- src/router/modules/worker.js | 9 +- .../__tests__/useNavigationStore.spec.js | 153 ++ src/stores/useArrayDataStore.js | 1 + src/utils/notifyResults.js | 19 + .../integration/Order/orderCatalog.spec.js | 1 - .../integration/entry/stockBought.spec.js | 37 +- .../invoiceIn/invoiceInBasicData.spec.js | 27 +- .../invoiceIn/invoiceInVat.spec.js | 2 +- .../invoiceOutNegativeBases.spec.js | 4 +- .../integration/item/ItemProposal.spec.js | 11 + test/cypress/integration/item/itemTag.spec.js | 5 +- .../parking/parkingBasicData.spec.js | 4 +- .../route/agency/agencyWorkCenter.spec.js | 1 + .../integration/route/routeList.spec.js | 19 +- .../route/vehicle/vehicleDescriptor.spec.js | 13 + .../ticket/negative/TicketLackDetail.spec.js | 147 ++ .../ticket/negative/TicketLackList.spec.js | 36 + .../integration/ticket/ticketList.spec.js | 25 + .../vnComponent/VnShortcut.spec.js | 11 + .../integration/zone/zoneBasicData.spec.js | 16 +- test/cypress/support/commands.js | 71 +- test/cypress/support/waitUntil.js | 2 +- 334 files changed, 9284 insertions(+), 4283 deletions(-) create mode 100644 src/boot/defaults/constants.js create mode 100644 src/components/common/VnCheckbox.vue create mode 100644 src/components/common/VnColor.vue create mode 100644 src/components/common/VnPopupProxy.vue create mode 100644 src/components/common/VnSelectTravelExtended.vue create mode 100644 src/components/ui/VnStockValueDisplay.vue create mode 100644 src/composables/checkEntryLock.js create mode 100644 src/composables/getColAlign.js create mode 100644 src/pages/Account/AccountExprBuilder.js create mode 100644 src/pages/Account/Alias/AliasExprBuilder.js create mode 100644 src/pages/Account/Card/AccountFilter.js create mode 100644 src/pages/Account/Role/RoleExprBuilder.js create mode 100644 src/pages/InvoiceIn/Card/InvoiceInFilter.js create mode 100644 src/pages/InvoiceOut/Card/InvoiceOutFilter.js create mode 100644 src/pages/Item/ItemType/Card/ItemTypeFilter.js rename src/pages/Item/{Card => components}/CreateGenusForm.vue (100%) rename src/pages/Item/{Card => components}/CreateSpecieForm.vue (100%) create mode 100644 src/pages/Item/components/ItemProposal.vue create mode 100644 src/pages/Item/components/ItemProposalProxy.vue create mode 100644 src/pages/Order/Card/OrderFilter.js create mode 100644 src/pages/Route/Card/RouteFilter.js create mode 100644 src/pages/Route/Roadmap/RoadmapFilter.js create mode 100644 src/pages/Route/Vehicle/Card/VehicleBasicData.vue create mode 100644 src/pages/Route/Vehicle/Card/VehicleCard.vue create mode 100644 src/pages/Route/Vehicle/Card/VehicleDescriptor.vue create mode 100644 src/pages/Route/Vehicle/Card/VehicleSummary.vue create mode 100644 src/pages/Route/Vehicle/VehicleFilter.js create mode 100644 src/pages/Route/Vehicle/VehicleList.vue create mode 100644 src/pages/Route/Vehicle/locale/en.yml create mode 100644 src/pages/Route/Vehicle/locale/es.yml create mode 100644 src/pages/Shelving/Card/ShelvingFilter.js rename src/pages/{ => Shelving}/Parking/Card/ParkingBasicData.vue (68%) rename src/pages/{ => Shelving}/Parking/Card/ParkingCard.vue (53%) rename src/pages/{ => Shelving}/Parking/Card/ParkingDescriptor.vue (58%) create mode 100644 src/pages/Shelving/Parking/Card/ParkingFilter.js rename src/pages/{ => Shelving}/Parking/Card/ParkingLog.vue (100%) rename src/pages/{ => Shelving}/Parking/Card/ParkingSummary.vue (100%) create mode 100644 src/pages/Shelving/Parking/ParkingExprBuilder.js rename src/pages/{ => Shelving}/Parking/ParkingFilter.vue (100%) rename src/pages/{ => Shelving}/Parking/ParkingList.vue (90%) rename src/pages/{ => Shelving}/Parking/locale/en.yml (100%) rename src/pages/{ => Shelving}/Parking/locale/es.yml (100%) create mode 100644 src/pages/Shelving/ShelvingExprBuilder.js create mode 100644 src/pages/Supplier/Card/SupplierFilter.js delete mode 100644 src/pages/Supplier/SupplierListFilter.vue create mode 100644 src/pages/Ticket/Card/TicketFilter.js create mode 100644 src/pages/Ticket/Card/TicketSplit.vue create mode 100644 src/pages/Ticket/Card/TicketTransferProxy.vue create mode 100644 src/pages/Ticket/Card/components/split.js create mode 100644 src/pages/Ticket/Negative/TicketLackDetail.vue create mode 100644 src/pages/Ticket/Negative/TicketLackFilter.vue create mode 100644 src/pages/Ticket/Negative/TicketLackList.vue create mode 100644 src/pages/Ticket/Negative/TicketLackTable.vue create mode 100644 src/pages/Ticket/Negative/components/ChangeItemDialog.vue create mode 100644 src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue create mode 100644 src/pages/Ticket/Negative/components/ChangeStateDialog.vue rename src/pages/{ => Worker}/Department/Card/DepartmentBasicData.vue (73%) rename src/pages/{ => Worker}/Department/Card/DepartmentCard.vue (70%) rename src/pages/{ => Worker}/Department/Card/DepartmentDescriptor.vue (84%) rename src/pages/{ => Worker}/Department/Card/DepartmentDescriptorProxy.vue (100%) rename src/pages/{ => Worker}/Department/Card/DepartmentSummary.vue (99%) rename src/pages/{ => Worker}/Department/Card/DepartmentSummaryDialog.vue (100%) create mode 100644 src/pages/Zone/Card/ZoneFilter.js create mode 100644 src/stores/__tests__/useNavigationStore.spec.js create mode 100644 src/utils/notifyResults.js create mode 100644 test/cypress/integration/item/ItemProposal.spec.js create mode 100644 test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js create mode 100644 test/cypress/integration/ticket/negative/TicketLackDetail.spec.js create mode 100644 test/cypress/integration/ticket/negative/TicketLackList.spec.js diff --git a/cypress.config.js b/cypress.config.js index dfe963a12..dd7de895c 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -39,10 +39,17 @@ export default defineConfig({ downloadsFolder: 'test/cypress/downloads', video: false, specPattern: 'test/cypress/integration/**/*.spec.js', - experimentalRunAllSpecs: true, - watchForFileChanges: true, - reporter, - reporterOptions, + experimentalRunAllSpecs: false, + watchForFileChanges: false, + reporter: 'cypress-mochawesome-reporter', + reporterOptions: { + charts: true, + reportPageTitle: 'Cypress Inline Reporter', + reportFilename: '[status]_[datetime]-report', + embeddedScreenshots: true, + reportDir: 'test/cypress/reports', + inlineAssets: true, + }, component: { componentFolder: 'src', testFiles: '**/*.spec.js', diff --git a/quasar.config.js b/quasar.config.js index df2cf246d..8b6125a90 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -31,7 +31,6 @@ export default configure(function (/* ctx */) { // --> boot files are part of "main.js" // https://v2.quasar.dev/quasar-cli/boot-files boot: ['i18n', 'axios', 'vnDate', 'validations', 'quasar', 'quasar.defaults'], - // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css css: ['app.scss'], diff --git a/src/boot/defaults/constants.js b/src/boot/defaults/constants.js new file mode 100644 index 000000000..c96ceb2d1 --- /dev/null +++ b/src/boot/defaults/constants.js @@ -0,0 +1,2 @@ +export const langs = ['en', 'es']; +export const decimalPlaces = 2; diff --git a/src/boot/keyShortcut.js b/src/boot/keyShortcut.js index 5afb5b74a..6da06c8bf 100644 --- a/src/boot/keyShortcut.js +++ b/src/boot/keyShortcut.js @@ -1,6 +1,6 @@ export default { - mounted: function (el, binding) { - const shortcut = binding.value ?? '+'; + mounted(el, binding) { + const shortcut = binding.value || '+'; const { key, ctrl, alt, callback } = typeof shortcut === 'string' @@ -8,25 +8,24 @@ export default { key: shortcut, ctrl: true, alt: true, - callback: () => - document - .querySelector(`button[shortcut="${shortcut}"]`) - ?.click(), + callback: () => el?.click(), } : binding.value; + if (!el.hasAttribute('shortcut')) { + el.setAttribute('shortcut', key); + } + const handleKeydown = (event) => { if (event.key === key && (!ctrl || event.ctrlKey) && (!alt || event.altKey)) { callback(); } }; - // Attach the event listener to the window window.addEventListener('keydown', handleKeydown); - el._handleKeydown = handleKeydown; }, - unmounted: function (el) { + unmounted(el) { if (el._handleKeydown) { window.removeEventListener('keydown', el._handleKeydown); } diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js index 97d80c670..182c51e47 100644 --- a/src/boot/qformMixin.js +++ b/src/boot/qformMixin.js @@ -9,19 +9,19 @@ export default { if (!form) return; try { const inputsFormCard = form.querySelectorAll( - `input:not([disabled]):not([type="checkbox"])` + `input:not([disabled]):not([type="checkbox"])`, ); if (inputsFormCard.length) { focusFirstInput(inputsFormCard[0]); } const textareas = document.querySelectorAll( - 'textarea:not([disabled]), [contenteditable]:not([disabled])' + 'textarea:not([disabled]), [contenteditable]:not([disabled])', ); if (textareas.length) { focusFirstInput(textareas[textareas.length - 1]); } const inputs = document.querySelectorAll( - 'form#formModel input:not([disabled]):not([type="checkbox"])' + 'form#formModel input:not([disabled]):not([type="checkbox"])', ); const input = inputs[0]; if (!input) return; @@ -30,22 +30,5 @@ export default { } catch (error) { console.error(error); } - form.addEventListener('keyup', function (evt) { - if (evt.key === 'Enter' && !that.$attrs['prevent-submit']) { - const input = evt.target; - if (input.type == 'textarea' && evt.shiftKey) { - evt.preventDefault(); - let { selectionStart, selectionEnd } = input; - input.value = - input.value.substring(0, selectionStart) + - '\n' + - input.value.substring(selectionEnd); - selectionStart = selectionEnd = selectionStart + 1; - return; - } - evt.preventDefault(); - that.onSubmit(); - } - }); }, }; diff --git a/src/boot/quasar.js b/src/boot/quasar.js index 547517682..a8c397b83 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -51,4 +51,5 @@ export default boot(({ app }) => { await useCau(response, message); }; + app.provide('app', app); }); diff --git a/src/components/CreateBankEntityForm.vue b/src/components/CreateBankEntityForm.vue index 2da3aa994..7c4b94a6a 100644 --- a/src/components/CreateBankEntityForm.vue +++ b/src/components/CreateBankEntityForm.vue @@ -14,7 +14,7 @@ const { t } = useI18n(); const bicInputRef = ref(null); const state = useState(); -const customer = computed(() => state.get('customer')); +const customer = computed(() => state.get('Customer')); const countriesFilter = { fields: ['id', 'name', 'code'], diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index d569dfda1..93a2ac96a 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -64,6 +64,10 @@ const $props = defineProps({ type: Function, default: null, }, + beforeSaveFn: { + type: Function, + default: null, + }, goTo: { type: String, default: '', @@ -176,7 +180,11 @@ async function saveChanges(data) { hasChanges.value = false; return; } - const changes = data || getChanges(); + let changes = data || getChanges(); + if ($props.beforeSaveFn) { + changes = await $props.beforeSaveFn(changes, getChanges); + } + try { await axios.post($props.saveUrl || $props.url + '/crud', changes); } finally { @@ -229,12 +237,12 @@ async function remove(data) { componentProps: { title: t('globals.confirmDeletion'), message: t('globals.confirmDeletionMessage'), - newData, + data: { deletes: ids }, ids, + promise: saveChanges, }, }) .onOk(async () => { - await saveChanges({ deletes: ids }); newData = newData.filter((form) => !ids.some((id) => id == form[pk])); fetch(newData); }); @@ -374,6 +382,8 @@ watch(formUrl, async () => { @click="onSubmit" :disable="!hasChanges" :title="t('globals.save')" + v-shortcut="'s'" + shortcut="s" data-cy="crudModelDefaultSaveBtn" /> <slot name="moreAfterActions" /> diff --git a/src/components/FilterTravelForm.vue b/src/components/FilterTravelForm.vue index 4d43c3810..765d97763 100644 --- a/src/components/FilterTravelForm.vue +++ b/src/components/FilterTravelForm.vue @@ -181,6 +181,7 @@ const selectTravel = ({ id }) => { color="primary" :disabled="isLoading" :loading="isLoading" + data-cy="save-filter-travel-form" /> </div> <QTable @@ -191,9 +192,10 @@ const selectTravel = ({ id }) => { :no-data-label="t('Enter a new search')" class="q-mt-lg" @row-click="(_, row) => selectTravel(row)" + data-cy="table-filter-travel-form" > <template #body-cell-id="{ row }"> - <QTd auto-width @click.stop> + <QTd auto-width @click.stop data-cy="travelFk-travel-form"> <QBtn flat color="blue">{{ row.id }}</QBtn> <TravelDescriptorProxy :id="row.id" /> </QTd> diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 19d917149..5e67a1310 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -1,6 +1,6 @@ <script setup> import axios from 'axios'; -import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue'; +import { onMounted, onUnmounted, computed, ref, watch, nextTick, useAttrs } from 'vue'; import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; @@ -23,6 +23,7 @@ const { validate } = useValidator(); const { notify } = useNotify(); const route = useRoute(); const myForm = ref(null); +const attrs = useAttrs(); const $props = defineProps({ url: { type: String, @@ -85,7 +86,7 @@ const $props = defineProps({ }, reload: { type: Boolean, - default: false, + default: true, }, defaultTrim: { type: Boolean, @@ -106,15 +107,15 @@ const isLoading = ref(false); // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas const isResetting = ref(false); const hasChanges = ref(!$props.observeFormChanges); -const originalData = ref({}); -const formData = computed(() => state.get(modelValue)); +const originalData = computed(() => state.get(modelValue)); +const formData = ref(); const defaultButtons = computed(() => ({ save: { dataCy: 'saveDefaultBtn', color: 'primary', icon: 'save', label: 'globals.save', - click: () => myForm.value.submit(), + click: async () => await save(), type: 'submit', }, reset: { @@ -128,8 +129,6 @@ const defaultButtons = computed(() => ({ })); onMounted(async () => { - originalData.value = JSON.parse(JSON.stringify($props.formInitialData ?? {})); - nextTick(() => (componentIsRendered.value = true)); // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla @@ -161,10 +160,18 @@ if (!$props.url) (val) => updateAndEmit('onFetch', { val }), ); +watch( + originalData, + (val) => { + if (val) formData.value = JSON.parse(JSON.stringify(val)); + }, + { immediate: true }, +); + watch( () => [$props.url, $props.filter], async () => { - originalData.value = null; + state.set(modelValue, null); reset(); await fetch(); }, @@ -199,7 +206,6 @@ async function fetch() { updateAndEmit('onFetch', { val: data }); } catch (e) { state.set(modelValue, {}); - originalData.value = {}; throw e; } } @@ -242,6 +248,7 @@ async function saveAndGo() { } function reset() { + formData.value = JSON.parse(JSON.stringify(originalData.value)); updateAndEmit('onFetch', { val: originalData.value }); if ($props.observeFormChanges) { hasChanges.value = false; @@ -266,7 +273,6 @@ function filter(value, update, filterOptions) { function updateAndEmit(evt, { val, res, old } = { val: null, res: null, old: null }) { state.set(modelValue, val); - originalData.value = val && JSON.parse(JSON.stringify(val)); if (!$props.url) arrayData.store.data = val; emit(evt, state.get(modelValue), res, old); @@ -301,6 +307,22 @@ async function onKeyup(evt) { } } +async function onKeyup(evt) { + if (evt.key === 'Enter' && !('prevent-submit' in attrs)) { + const input = evt.target; + if (input.type == 'textarea' && evt.shiftKey) { + let { selectionStart, selectionEnd } = input; + input.value = + input.value.substring(0, selectionStart) + + '\n' + + input.value.substring(selectionEnd); + selectionStart = selectionEnd = selectionStart + 1; + return; + } + await save(); + } +} + defineExpose({ save, isLoading, @@ -315,7 +337,8 @@ defineExpose({ <QForm ref="myForm" v-if="formData" - @submit="save" + @submit.prevent + @keyup.prevent="onKeyup" @reset="reset" class="q-pa-md" :style="maxWidth ? 'max-width: ' + maxWidth : ''" diff --git a/src/components/FormModelPopup.vue b/src/components/FormModelPopup.vue index afdc6efca..85943e91e 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -1,12 +1,13 @@ <script setup> -import { ref, computed } from 'vue'; +import { ref, computed, useAttrs, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useState } from 'src/composables/useState'; import FormModel from 'components/FormModel.vue'; const emit = defineEmits(['onDataSaved', 'onDataCanceled']); -defineProps({ +const props = defineProps({ title: { type: String, default: '', @@ -15,23 +16,41 @@ defineProps({ type: String, default: '', }, + showSaveAndContinueBtn: { + type: Boolean, + default: false, + }, }); const { t } = useI18n(); - +const attrs = useAttrs(); +const state = useState(); const formModelRef = ref(null); const closeButton = ref(null); +const isSaveAndContinue = ref(props.showSaveAndContinueBtn); +const isLoading = computed(() => formModelRef.value?.isLoading); +const reset = computed(() => formModelRef.value?.reset); -const onDataSaved = (formData, requestResponse) => { - if (closeButton.value) closeButton.value.click(); +const onDataSaved = async (formData, requestResponse) => { + if (!isSaveAndContinue.value) closeButton.value?.click(); + if (isSaveAndContinue.value) { + await nextTick(); + state.set(attrs.model, attrs.formInitialData); + } + isSaveAndContinue.value = props.showSaveAndContinueBtn; emit('onDataSaved', formData, requestResponse); }; -const isLoading = computed(() => formModelRef.value?.isLoading); +const onClick = async (saveAndContinue) => { + isSaveAndContinue.value = saveAndContinue; + await formModelRef.value.save(); +}; defineExpose({ isLoading, onDataSaved, + isSaveAndContinue, + reset, }); </script> @@ -59,15 +78,16 @@ defineExpose({ flat :disabled="isLoading" :loading="isLoading" - @click="emit('onDataCanceled')" - v-close-popup data-cy="FormModelPopup_cancel" + v-close-popup z-max + @click="emit('onDataCanceled')" /> <QBtn + :flat="showSaveAndContinueBtn" :label="t('globals.save')" :title="t('globals.save')" - type="submit" + @click="onClick(false)" color="primary" class="q-ml-sm" :disabled="isLoading" @@ -75,6 +95,18 @@ defineExpose({ data-cy="FormModelPopup_save" z-max /> + <QBtn + v-if="showSaveAndContinueBtn" + :label="t('globals.isSaveAndContinue')" + :title="t('globals.isSaveAndContinue')" + color="primary" + class="q-ml-sm" + :disabled="isLoading" + :loading="isLoading" + data-cy="FormModelPopup_isSaveAndContinue" + z-max + @click="onClick(true)" + /> </div> </template> </FormModel> diff --git a/src/components/ItemsFilterPanel.vue b/src/components/ItemsFilterPanel.vue index 36123b834..f73753a6b 100644 --- a/src/components/ItemsFilterPanel.vue +++ b/src/components/ItemsFilterPanel.vue @@ -281,7 +281,7 @@ const setCategoryList = (data) => { <QItem class="q-mt-lg"> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat class="fill-icon-on-hover q-px-xs" color="primary" @@ -327,7 +327,6 @@ en: active: Is active visible: Is visible floramondo: Is floramondo - salesPersonFk: Buyer categoryFk: Category es: @@ -338,7 +337,6 @@ es: active: Activo visible: Visible floramondo: Floramondo - salesPersonFk: Comprador categoryFk: Categoría Plant: Planta natural Flower: Flor fresca diff --git a/src/components/LeftMenu.vue b/src/components/LeftMenu.vue index 644f831d4..9a9949499 100644 --- a/src/components/LeftMenu.vue +++ b/src/components/LeftMenu.vue @@ -41,7 +41,6 @@ const filteredItems = computed(() => { return locale.includes(normalizedSearch); }); }); - const filteredPinnedModules = computed(() => { if (!search.value) return pinnedModules.value; const normalizedSearch = search.value @@ -72,7 +71,7 @@ watch( items.value = []; getRoutes(); }, - { deep: true } + { deep: true }, ); function findMatches(search, item) { @@ -104,33 +103,40 @@ function addChildren(module, route, parent) { } function getRoutes() { - if (props.source === 'main') { - const modules = Object.assign([], navigation.getModules().value); - - for (const item of modules) { - const moduleDef = routes.find( - (route) => toLowerCamel(route.name) === item.module - ); - if (!moduleDef) continue; - item.children = []; - - addChildren(item.module, moduleDef, item.children); - } - - items.value = modules; + const handleRoutes = { + main: getMainRoutes, + card: getCardRoutes, + }; + try { + handleRoutes[props.source](); + } catch (error) { + throw new Error(`Method is not defined`); } +} +function getMainRoutes() { + const modules = Object.assign([], navigation.getModules().value); - if (props.source === 'card') { - const currentRoute = route.matched[1]; - const currentModule = toLowerCamel(currentRoute.name); - let moduleDef = routes.find( - (route) => toLowerCamel(route.name) === currentModule + for (const item of modules) { + const moduleDef = routes.find( + (route) => toLowerCamel(route.name) === item.module, ); + if (!moduleDef) continue; + item.children = []; - if (!moduleDef) return; - if (!moduleDef?.menus) moduleDef = betaGetRoutes(); - addChildren(currentModule, moduleDef, items.value); + addChildren(item.module, moduleDef, item.children); } + + items.value = modules; +} + +function getCardRoutes() { + const currentRoute = route.matched[1]; + const currentModule = toLowerCamel(currentRoute.name); + let moduleDef = routes.find((route) => toLowerCamel(route.name) === currentModule); + + if (!moduleDef) return; + if (!moduleDef?.menus) moduleDef = betaGetRoutes(); + addChildren(currentModule, moduleDef, items.value); } function betaGetRoutes() { @@ -223,9 +229,16 @@ const searchModule = () => { </template> <template v-for="(item, index) in filteredItems" :key="item.name"> <template - v-if="search ||item.children && !filteredPinnedModules.has(item.name)" + v-if=" + search || + (item.children && !filteredPinnedModules.has(item.name)) + " > - <LeftMenuItem :item="item" group="modules" :class="search && index === 0 ? 'searched' : ''"> + <LeftMenuItem + :item="item" + group="modules" + :class="search && index === 0 ? 'searched' : ''" + > <template #side> <QBtn v-if="item.isPinned === true" @@ -342,7 +355,7 @@ const searchModule = () => { .header { color: var(--vn-label-color); } -.searched{ +.searched { background-color: var(--vn-section-hover-color); } </style> diff --git a/src/components/LeftMenuItem.vue b/src/components/LeftMenuItem.vue index a3112b17f..c0cee44fe 100644 --- a/src/components/LeftMenuItem.vue +++ b/src/components/LeftMenuItem.vue @@ -26,6 +26,7 @@ const itemComputed = computed(() => { :to="{ name: itemComputed.name }" clickable v-ripple + :data-cy="`${itemComputed.name}-menu-item`" > <QItemSection avatar v-if="itemComputed.icon"> <QIcon :name="itemComputed.icon" /> diff --git a/src/components/RefundInvoiceForm.vue b/src/components/RefundInvoiceForm.vue index 590acede0..6dcb8b390 100644 --- a/src/components/RefundInvoiceForm.vue +++ b/src/components/RefundInvoiceForm.vue @@ -9,6 +9,7 @@ import VnSelect from 'components/common/VnSelect.vue'; import FormPopup from './FormPopup.vue'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const $props = defineProps({ invoiceOutData: { @@ -131,15 +132,11 @@ const refund = async () => { :required="true" /> </VnRow ><VnRow> - <div> - <QCheckbox - :label="t('Inherit warehouse')" - v-model="invoiceParams.inheritWarehouse" - /> - <QIcon name="info" class="cursor-info q-ml-sm" size="sm"> - <QTooltip>{{ t('Inherit warehouse tooltip') }}</QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="invoiceParams.inheritWarehouse" + :label="t('Inherit warehouse')" + :info="t('Inherit warehouse tooltip')" + /> </VnRow> </template> </FormPopup> diff --git a/src/components/TicketProblems.vue b/src/components/TicketProblems.vue index 934b13a1c..783f2556f 100644 --- a/src/components/TicketProblems.vue +++ b/src/components/TicketProblems.vue @@ -4,26 +4,21 @@ import { toCurrency } from 'src/filters'; defineProps({ row: { type: Object, required: true } }); </script> <template> - <span> - <QIcon - v-if="row.isTaxDataChecked === 0" - name="vn:no036" - color="primary" - size="xs" + <span class="q-gutter-x-xs"> + <router-link + v-if="row.claim?.claimFk" + :to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }" + class="link" > - <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> - </QIcon> - <QIcon v-if="row.hasTicketRequest" name="vn:buyrequest" color="primary" size="xs"> - <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> - </QIcon> - <QIcon v-if="row.itemShortage" name="vn:unavailable" color="primary" size="xs"> - <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> - </QIcon> - <QIcon v-if="row.isFreezed" name="vn:frozen" color="primary" size="xs"> - <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> - </QIcon> + <QIcon name="vn:claims" size="xs"> + <QTooltip> + {{ t('ticketSale.claim') }}: + {{ row.claim?.claimFk }} + </QTooltip> + </QIcon> + </router-link> <QIcon - v-if="row.risk" + v-if="row?.risk" name="vn:risk" :color="row.hasHighRisk ? 'negative' : 'primary'" size="xs" @@ -33,10 +28,57 @@ defineProps({ row: { type: Object, required: true } }); {{ toCurrency(row.risk - row.credit) }} </QTooltip> </QIcon> - <QIcon v-if="row.hasComponentLack" name="vn:components" color="primary" size="xs"> + <QIcon + v-if="row?.hasComponentLack" + name="vn:components" + color="primary" + size="xs" + > <QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip> </QIcon> - <QIcon v-if="row.isTooLittle" name="vn:isTooLittle" color="primary" size="xs"> + <QIcon v-if="row?.hasItemDelay" color="primary" size="xs" name="vn:hasItemDelay"> + <QTooltip> + {{ $t('ticket.summary.hasItemDelay') }} + </QTooltip> + </QIcon> + <QIcon v-if="row?.hasItemLost" color="primary" size="xs" name="vn:hasItemLost"> + <QTooltip> + {{ $t('salesTicketsTable.hasItemLost') }} + </QTooltip> + </QIcon> + <QIcon + v-if="row?.hasItemShortage" + name="vn:unavailable" + color="primary" + size="xs" + > + <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> + </QIcon> + <QIcon v-if="row?.hasRounding" color="primary" name="sync_problem" size="xs"> + <QTooltip> + {{ $t('ticketList.rounding') }} + </QTooltip> + </QIcon> + <QIcon + v-if="row?.hasTicketRequest" + name="vn:buyrequest" + color="primary" + size="xs" + > + <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> + </QIcon> + <QIcon + v-if="row?.isTaxDataChecked !== 0" + name="vn:no036" + color="primary" + size="xs" + > + <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> + </QIcon> + <QIcon v-if="row?.isFreezed" name="vn:frozen" color="primary" size="xs"> + <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> + </QIcon> + <QIcon v-if="row?.isTooLittle" name="vn:isTooLittle" color="primary" size="xs"> <QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip> </QIcon> </span> diff --git a/src/components/TransferInvoiceForm.vue b/src/components/TransferInvoiceForm.vue index aa71070d6..c4ef1454a 100644 --- a/src/components/TransferInvoiceForm.vue +++ b/src/components/TransferInvoiceForm.vue @@ -10,6 +10,7 @@ import VnSelect from 'components/common/VnSelect.vue'; import FormPopup from './FormPopup.vue'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; +import VnCheckbox from './common/VnCheckbox.vue'; const $props = defineProps({ invoiceOutData: { @@ -186,15 +187,11 @@ const makeInvoice = async () => { /> </VnRow> <VnRow> - <div> - <QCheckbox - :label="t('Bill destination client')" - v-model="checked" - /> - <QIcon name="info" class="cursor-info q-ml-sm" size="sm"> - <QTooltip>{{ t('transferInvoiceInfo') }}</QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="checked" + :label="t('Bill destination client')" + :info="t('transferInvoiceInfo')" + /> </VnRow> </template> </FormPopup> diff --git a/src/components/VnTable/VnColumn.vue b/src/components/VnTable/VnColumn.vue index 9e9bfad69..d0e245388 100644 --- a/src/components/VnTable/VnColumn.vue +++ b/src/components/VnTable/VnColumn.vue @@ -1,9 +1,8 @@ <script setup> import { markRaw, computed } from 'vue'; -import { QIcon, QCheckbox } from 'quasar'; +import { QIcon, QToggle } from 'quasar'; import { dashIfEmpty } from 'src/filters'; -/* basic input */ import VnSelect from 'components/common/VnSelect.vue'; import VnSelectCache from 'components/common/VnSelectCache.vue'; import VnInput from 'components/common/VnInput.vue'; @@ -12,8 +11,11 @@ import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputTime from 'components/common/VnInputTime.vue'; import VnComponent from 'components/common/VnComponent.vue'; import VnUserLink from 'components/ui/VnUserLink.vue'; +import VnSelectEnum from '../common/VnSelectEnum.vue'; +import VnCheckbox from '../common/VnCheckbox.vue'; const model = defineModel(undefined, { required: true }); +const emit = defineEmits(['blur']); const $props = defineProps({ column: { type: Object, @@ -39,10 +41,18 @@ const $props = defineProps({ type: Object, default: null, }, + autofocus: { + type: Boolean, + default: false, + }, showLabel: { type: Boolean, default: null, }, + eventHandlers: { + type: Object, + default: null, + }, }); const defaultSelect = { @@ -99,7 +109,8 @@ const defaultComponents = { }, }, checkbox: { - component: markRaw(QCheckbox), + ref: 'checkbox', + component: markRaw(VnCheckbox), attrs: ({ model }) => { const defaultAttrs = { disable: !$props.isEditable, @@ -115,6 +126,10 @@ const defaultComponents = { }, forceAttrs: { label: $props.showLabel && $props.column.label, + autofocus: true, + }, + events: { + blur: () => emit('blur'), }, }, select: { @@ -125,12 +140,19 @@ const defaultComponents = { component: markRaw(VnSelect), ...defaultSelect, }, + selectEnum: { + component: markRaw(VnSelectEnum), + ...defaultSelect, + }, icon: { component: markRaw(QIcon), }, userLink: { component: markRaw(VnUserLink), }, + toggle: { + component: markRaw(QToggle), + }, }; const value = computed(() => { @@ -160,7 +182,28 @@ const col = computed(() => { return newColumn; }); -const components = computed(() => $props.components ?? defaultComponents); +const components = computed(() => { + const sourceComponents = $props.components ?? defaultComponents; + + return Object.keys(sourceComponents).reduce((acc, key) => { + const component = sourceComponents[key]; + + if (!component || typeof component !== 'object') { + acc[key] = component; + return acc; + } + + acc[key] = { + ...component, + attrs: { + ...(component.attrs || {}), + autofocus: $props.autofocus, + }, + event: { ...component?.event, ...$props?.eventHandlers }, + }; + return acc; + }, {}); +}); </script> <template> <div class="row no-wrap"> diff --git a/src/components/VnTable/VnFilter.vue b/src/components/VnTable/VnFilter.vue index 426f5c716..0de3834ea 100644 --- a/src/components/VnTable/VnFilter.vue +++ b/src/components/VnTable/VnFilter.vue @@ -1,14 +1,12 @@ <script setup> import { markRaw, computed } from 'vue'; -import { QCheckbox } from 'quasar'; +import { QCheckbox, QToggle } from 'quasar'; import { useArrayData } from 'composables/useArrayData'; - -/* basic input */ import VnSelect from 'components/common/VnSelect.vue'; import VnInput from 'components/common/VnInput.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputTime from 'components/common/VnInputTime.vue'; -import VnTableColumn from 'components/VnTable/VnColumn.vue'; +import VnColumn from 'components/VnTable/VnColumn.vue'; const $props = defineProps({ column: { @@ -27,6 +25,10 @@ const $props = defineProps({ type: String, default: 'table', }, + customClass: { + type: String, + default: '', + }, }); defineExpose({ addFilter, props: $props }); @@ -34,7 +36,7 @@ defineExpose({ addFilter, props: $props }); const model = defineModel(undefined, { required: true }); const arrayData = useArrayData( $props.dataKey, - $props.searchUrl ? { searchUrl: $props.searchUrl } : null + $props.searchUrl ? { searchUrl: $props.searchUrl } : null, ); const columnFilter = computed(() => $props.column?.columnFilter); @@ -46,19 +48,18 @@ const enterEvent = { const defaultAttrs = { filled: !$props.showTitle, - class: 'q-px-xs q-pb-xs q-pt-none fit', dense: true, }; const forceAttrs = { - label: $props.showTitle ? '' : columnFilter.value?.label ?? $props.column.label, + label: $props.showTitle ? '' : (columnFilter.value?.label ?? $props.column.label), }; const selectComponent = { component: markRaw(VnSelect), event: updateEvent, attrs: { - class: 'q-px-sm q-pb-xs q-pt-none fit', + class: `q-pt-none fit ${$props.customClass}`, dense: true, filled: !$props.showTitle, }, @@ -109,14 +110,24 @@ const components = { component: markRaw(QCheckbox), event: updateEvent, attrs: { - dense: true, - class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs fit', + class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit', 'toggle-indeterminate': true, + size: 'sm', }, forceAttrs, }, select: selectComponent, rawSelect: selectComponent, + toggle: { + component: markRaw(QToggle), + event: updateEvent, + attrs: { + class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit', + 'toggle-indeterminate': true, + size: 'sm', + }, + forceAttrs, + }, }; async function addFilter(value, name) { @@ -132,19 +143,8 @@ async function addFilter(value, name) { await arrayData.addFilter({ params: { [field]: value } }); } -function alignRow() { - switch ($props.column.align) { - case 'left': - return 'justify-start items-start'; - case 'right': - return 'justify-end items-end'; - default: - return 'flex-center'; - } -} - const showFilter = computed( - () => $props.column?.columnFilter !== false && $props.column.name != 'tableActions' + () => $props.column?.columnFilter !== false && $props.column.name != 'tableActions', ); const onTabPressed = async () => { @@ -152,13 +152,8 @@ const onTabPressed = async () => { }; </script> <template> - <div - v-if="showFilter" - class="full-width" - :class="alignRow()" - style="max-height: 45px; overflow: hidden" - > - <VnTableColumn + <div v-if="showFilter" class="full-width" style="overflow: hidden"> + <VnColumn :column="$props.column" default="input" v-model="model" @@ -168,3 +163,8 @@ const onTabPressed = async () => { /> </div> </template> +<style lang="scss" scoped> +label.vn-label-padding > .q-field__inner > .q-field__control { + padding: inherit !important; +} +</style> diff --git a/src/components/VnTable/VnOrder.vue b/src/components/VnTable/VnOrder.vue index 8ffdfe2bc..47ed9acf4 100644 --- a/src/components/VnTable/VnOrder.vue +++ b/src/components/VnTable/VnOrder.vue @@ -23,6 +23,10 @@ const $props = defineProps({ type: Boolean, default: false, }, + align: { + type: String, + default: 'end', + }, }); const hover = ref(); const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl }); @@ -41,55 +45,78 @@ async function orderBy(name, direction) { break; } if (!direction) return await arrayData.deleteOrder(name); + await arrayData.addOrder(name, direction); } defineExpose({ orderBy }); + +function textAlignToFlex(textAlign) { + return `justify-content: ${ + { + 'text-center': 'center', + 'text-left': 'start', + 'text-right': 'end', + }[textAlign] || 'start' + };`; +} </script> <template> <div @mouseenter="hover = true" @mouseleave="hover = false" @click="orderBy(name, model?.direction)" - class="row items-center no-wrap cursor-pointer" + class="items-center no-wrap cursor-pointer title" + :style="textAlignToFlex(align)" > <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'" + <div v-if="name && model?.index"> + <QChip + :label="!vertical ? model?.index : ''" + :icon=" + (model?.index || hover) && !vertical + ? model?.direction == 'DESC' + ? 'arrow_downward' + : 'arrow_upward' + : undefined + " + :size="vertical ? '' : 'sm'" + :class="[ + model?.index ? 'color-vn-text' : 'bg-transparent', + vertical ? 'q-px-none' : '', + ]" + class="no-box-shadow" + :clickable="true" + style="min-width: 40px; max-height: 30px" > - {{ model?.index }} - <QIcon - :name=" - model?.index - ? model?.direction == 'DESC' - ? 'arrow_downward' - : 'arrow_upward' - : 'swap_vert' - " - size="xs" - /> - </div> - </QChip> + <div + class="column flex-center" + v-if="vertical" + :style="!model?.index && 'color: #5d5d5d'" + > + {{ model?.index }} + <QIcon + :name=" + model?.index + ? model?.direction == 'DESC' + ? 'arrow_downward' + : 'arrow_upward' + : 'swap_vert' + " + size="xs" + /> + </div> + </QChip> + </div> </div> </template> +<style lang="scss" scoped> +.title { + display: flex; + align-items: center; + height: 30px; + width: 100%; + color: var(--vn-label-color); + white-space: nowrap; +} +</style> diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 532c89456..d67d157c2 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -1,22 +1,38 @@ <script setup> -import { ref, onBeforeMount, onMounted, computed, watch, useAttrs } from 'vue'; +import { + ref, + onBeforeMount, + onMounted, + onUnmounted, + computed, + watch, + h, + render, + inject, + useAttrs, + nextTick, +} from 'vue'; +import { useArrayData } from 'src/composables/useArrayData'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; -import { useQuasar } from 'quasar'; +import { useQuasar, date } from 'quasar'; import { useStateStore } from 'stores/useStateStore'; import { useFilterParams } from 'src/composables/useFilterParams'; +import { dashIfEmpty, toDate } from 'src/filters'; import CrudModel from 'src/components/CrudModel.vue'; import FormModelPopup from 'components/FormModelPopup.vue'; -import VnTableColumn from 'components/VnTable/VnColumn.vue'; +import VnColumn from 'components/VnTable/VnColumn.vue'; import VnFilter from 'components/VnTable/VnFilter.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'; import VnTableFilter from './VnTableFilter.vue'; +import { getColAlign } from 'src/composables/getColAlign'; +const arrayData = useArrayData(useAttrs()['data-key']); const $props = defineProps({ columns: { type: Array, @@ -42,10 +58,6 @@ const $props = defineProps({ type: [Function, Boolean], default: null, }, - rowCtrlClick: { - type: [Function, Boolean], - default: null, - }, redirect: { type: String, default: null, @@ -114,7 +126,19 @@ const $props = defineProps({ type: Boolean, default: false, }, + withFilters: { + type: Boolean, + default: true, + }, + overlay: { + type: Boolean, + default: false, + }, + createComplement: { + type: Object, + }, }); + const { t } = useI18n(); const stateStore = useStateStore(); const route = useRoute(); @@ -132,10 +156,18 @@ const showForm = ref(false); const splittedColumns = ref({ columns: [] }); const columnsVisibilitySkipped = ref(); const createForm = ref(); +const createRef = ref(null); const tableRef = ref(); const params = ref(useFilterParams($attrs['data-key']).params); const orders = ref(useFilterParams($attrs['data-key']).orders); +const app = inject('app'); +const editingRow = ref(null); +const editingField = ref(null); +const isTableMode = computed(() => mode.value == TABLE_MODE); +const showRightIcon = computed(() => $props.rightSearch || $props.rightSearchIcon); +const selectRegex = /select/; +const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); const tableModes = [ { icon: 'view_column', @@ -156,7 +188,8 @@ onBeforeMount(() => { hasParams.value = urlParams && Object.keys(urlParams).length !== 0; }); -onMounted(() => { +onMounted(async () => { + if ($props.isEditable) document.addEventListener('click', clickHandler); mode.value = quasar.platform.is.mobile && !$props.disableOption?.card ? CARD_MODE @@ -178,14 +211,25 @@ onMounted(() => { } }); +onUnmounted(async () => { + if ($props.isEditable) document.removeEventListener('click', clickHandler); +}); + watch( () => $props.columns, (value) => splitColumns(value), { immediate: true }, ); -const isTableMode = computed(() => mode.value == TABLE_MODE); -const showRightIcon = computed(() => $props.rightSearch || $props.rightSearchIcon); +defineExpose({ + create: createForm, + reload, + redirect: redirectFn, + selected, + CrudModelRef, + params, + tableRef, +}); function splitColumns(columns) { splittedColumns.value = { @@ -231,16 +275,6 @@ const rowClickFunction = computed(() => { 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) { router.push({ path: `/${$props.redirect}/${id}` }); } @@ -262,21 +296,6 @@ function columnName(col) { return name; } -function getColAlign(col) { - return 'text-' + (col.align ?? 'left'); -} - -const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); -defineExpose({ - create: createForm, - reload, - redirect: redirectFn, - selected, - CrudModelRef, - params, - tableRef, -}); - function handleOnDataSaved(_) { if (_.onDataSaved) _.onDataSaved({ CrudModelRef: CrudModelRef.value }); else $props.create.onDataSaved(_); @@ -305,6 +324,237 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) { } } +function isEditableColumn(column) { + const isEditableCol = column?.isEditable ?? true; + const isVisible = column?.visible ?? true; + const hasComponent = column?.component; + + return $props.isEditable && isVisible && hasComponent && isEditableCol; +} + +function hasEditableFormat(column) { + if (isEditableColumn(column)) return 'editable-text'; +} + +const clickHandler = async (event) => { + const clickedElement = event.target.closest('td'); + + const isDateElement = event.target.closest('.q-date'); + const isTimeElement = event.target.closest('.q-time'); + const isQselectDropDown = event.target.closest('.q-select__dropdown-icon'); + + if (isDateElement || isTimeElement || isQselectDropDown) return; + + if (clickedElement === null) { + await destroyInput(editingRow.value, editingField.value); + return; + } + const rowIndex = clickedElement.getAttribute('data-row-index'); + const colField = clickedElement.getAttribute('data-col-field'); + const column = $props.columns.find((col) => col.name === colField); + + if (editingRow.value !== null && editingField.value !== null) { + if (editingRow.value == rowIndex && editingField.value == colField) return; + + await destroyInput(editingRow.value, editingField.value); + } + + if (isEditableColumn(column)) { + await renderInput(Number(rowIndex), colField, clickedElement); + } +}; + +async function handleTabKey(event, rowIndex, colField) { + if (editingRow.value == rowIndex && editingField.value == colField) + await destroyInput(editingRow.value, editingField.value); + + const direction = event.shiftKey ? -1 : 1; + const { nextRowIndex, nextColumnName } = await handleTabNavigation( + rowIndex, + colField, + direction, + ); + + if (nextRowIndex < 0 || nextRowIndex >= arrayData.store.data.length) return; + + event.preventDefault(); + await renderInput(nextRowIndex, nextColumnName, null); +} + +async function renderInput(rowId, field, clickedElement) { + editingField.value = field; + editingRow.value = rowId; + + const originalColumn = $props.columns.find((col) => col.name === field); + const column = { ...originalColumn, ...{ label: '' } }; + const row = CrudModelRef.value.formData[rowId]; + const oldValue = CrudModelRef.value.formData[rowId][column?.name]; + + if (!clickedElement) + clickedElement = document.querySelector( + `[data-row-index="${rowId}"][data-col-field="${field}"]`, + ); + + Array.from(clickedElement.childNodes).forEach((child) => { + child.style.visibility = 'hidden'; + child.style.position = 'relative'; + }); + + const isSelect = selectRegex.test(column?.component); + if (isSelect) column.attrs = { ...column.attrs, 'emit-value': false }; + + const node = h(VnColumn, { + row: row, + class: 'temp-input', + column: column, + modelValue: row[column.name], + componentProp: 'columnField', + autofocus: true, + focusOnMount: true, + eventHandlers: { + 'update:modelValue': async (value) => { + if (isSelect && value) { + row[column.name] = value[column.attrs?.optionValue ?? 'id']; + row[column?.name + 'TextValue'] = + value[column.attrs?.optionLabel ?? 'name']; + await column?.cellEvent?.['update:modelValue']?.( + value, + oldValue, + row, + ); + } else row[column.name] = value; + await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row); + }, + keyup: async (event) => { + if (event.key === 'Enter') + await destroyInput(rowIndex, field, clickedElement); + }, + keydown: async (event) => { + switch (event.key) { + case 'Tab': + await handleTabKey(event, rowId, field); + event.stopPropagation(); + break; + case 'Escape': + await destroyInput(rowId, field, clickedElement); + break; + default: + break; + } + }, + click: (event) => { + column?.cellEvent?.['click']?.(event, row); + }, + }, + }); + + node.appContext = app._context; + render(node, clickedElement); + + if (['toggle'].includes(column?.component)) + node.el?.querySelector('span > div').focus(); + + if (['checkbox', undefined].includes(column?.component)) + node.el?.querySelector('span > div > div').focus(); +} + +async function destroyInput(rowIndex, field, clickedElement) { + if (!clickedElement) + clickedElement = document.querySelector( + `[data-row-index="${rowIndex}"][data-col-field="${field}"]`, + ); + if (clickedElement) { + await nextTick(); + render(null, clickedElement); + Array.from(clickedElement.childNodes).forEach((child) => { + child.style.visibility = 'visible'; + child.style.position = ''; + }); + } + if (editingRow.value !== rowIndex || editingField.value !== field) return; + editingRow.value = null; + editingField.value = null; +} + +async function handleTabNavigation(rowIndex, colName, direction) { + const columns = $props.columns; + const totalColumns = columns.length; + let currentColumnIndex = columns.findIndex((col) => col.name === colName); + + let iterations = 0; + let newColumnIndex = currentColumnIndex; + + do { + iterations++; + newColumnIndex = (newColumnIndex + direction + totalColumns) % totalColumns; + + if (isEditableColumn(columns[newColumnIndex])) break; + } while (iterations < totalColumns); + + if (iterations >= totalColumns + 1) return; + + if (direction === 1 && newColumnIndex <= currentColumnIndex) { + rowIndex++; + } else if (direction === -1 && newColumnIndex >= currentColumnIndex) { + rowIndex--; + } + return { nextRowIndex: rowIndex, nextColumnName: columns[newColumnIndex].name }; +} + +function getCheckboxIcon(value) { + switch (typeof value) { + case 'boolean': + return value ? 'check' : 'close'; + case 'number': + return value === 0 ? 'close' : 'check'; + case 'undefined': + return 'indeterminate_check_box'; + default: + return 'indeterminate_check_box'; + } +} + +function getToggleIcon(value) { + if (value === null) return 'help_outline'; + return value ? 'toggle_on' : 'toggle_off'; +} + +function formatColumnValue(col, row, dashIfEmpty) { + if (col?.format || row[col?.name + 'TextValue']) { + if (selectRegex.test(col?.component) && row[col?.name + 'TextValue']) { + return dashIfEmpty(row[col?.name + 'TextValue']); + } else { + return col.format(row, dashIfEmpty); + } + } + + if (col?.component === 'date') return dashIfEmpty(toDate(row[col?.name])); + + if (col?.component === 'time') + return row[col?.name] >= 5 + ? dashIfEmpty(date.formatDate(new Date(row[col?.name]), 'HH:mm')) + : row[col?.name]; + + if (selectRegex.test(col?.component) && $props.isEditable) { + const { find, url } = col.attrs; + const urlRelation = url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1); + + if (col?.attrs.options) { + const find = col?.attrs.options.find((option) => option.id === row[col.name]); + if (!col.attrs?.optionLabel || !find) return dashIfEmpty(row[col?.name]); + return dashIfEmpty(find[col.attrs?.optionLabel ?? 'name']); + } + + if (typeof row[urlRelation] == 'object') { + if (typeof find == 'object') + return dashIfEmpty(row[urlRelation][find?.label ?? 'name']); + + return dashIfEmpty(row[urlRelation][col?.attrs.optionLabel ?? 'name']); + } + if (typeof row[urlRelation] == 'string') return dashIfEmpty(row[urlRelation]); + } + return dashIfEmpty(row[col?.name]); +} function cardClick(_, row) { if ($props.redirect) router.push({ path: `/${$props.redirect}/${row.id}` }); } @@ -315,7 +565,7 @@ function cardClick(_, row) { v-model="stateStore.rightDrawer" side="right" :width="256" - show-if-above + :overlay="$props.overlay" > <QScrollArea class="fit"> <VnTableFilter @@ -336,7 +586,7 @@ function cardClick(_, row) { <CrudModel v-bind="$attrs" :class="$attrs['class'] ?? 'q-px-md'" - :limit="$attrs['limit'] ?? 20" + :limit="$attrs['limit'] ?? 100" ref="CrudModelRef" @on-fetch="(...args) => emit('onFetch', ...args)" :search-url="searchUrl" @@ -352,8 +602,12 @@ function cardClick(_, row) { <QTable ref="tableRef" v-bind="table" - class="vnTable" - :class="{ 'last-row-sticky': $props.footer }" + :class="[ + 'vnTable', + table ? 'selection-cell' : '', + $props.footer ? 'last-row-sticky' : '', + ]" + wrap-cells :columns="splittedColumns.columns" :rows="rows" v-model:selected="selected" @@ -367,11 +621,13 @@ function cardClick(_, row) { @row-click="(_, row) => rowClickFunction && rowClickFunction(row)" @update:selected="emit('update:selected', $event)" @selection="(details) => handleSelection(details, rows)" + :hide-selected-banner="true" > <template #top-left v-if="!$props.withoutHeader"> - <slot name="top-left"></slot> + <slot name="top-left"> </slot> </template> <template #top-right v-if="!$props.withoutHeader"> + <slot name="top-right"></slot> <VnVisibleColumn v-if="isTableMode" v-model="splittedColumns.columns" @@ -389,32 +645,39 @@ function cardClick(_, row) { <template #header-cell="{ col }"> <QTh v-if="col.visible ?? true" - :style="col.headerStyle" - :class="col.headerClass" + v-bind:class="col.headerClass" + class="body-cell" + :style="col?.width ? `max-width: ${col?.width}` : ''" > <div - class="column ellipsis" - :class="`text-${col?.align ?? 'left'}`" - :style="$props.columnSearch ? 'height: 75px' : ''" + class="no-padding" + :style="[ + withFilters && $props.columnSearch ? 'height: 75px' : '', + ]" > - <div class="row items-center no-wrap" style="height: 30px"> + <div 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" + :label="col?.labelAbbreviation ?? col?.label" :data-key="$attrs['data-key']" :search-url="searchUrl" + :align="getColAlign(col)" /> </div> <VnFilter - v-if="$props.columnSearch" + v-if=" + $props.columnSearch && + col.columnSearch !== false && + withFilters + " :column="col" :show-title="true" :data-key="$attrs['data-key']" v-model="params[columnName(col)]" :search-url="searchUrl" - class="full-width" + customClass="header-filter" /> </div> </QTh> @@ -432,32 +695,67 @@ function cardClick(_, row) { </QTd> </template> <template #body-cell="{ col, row, rowIndex }"> - <!-- Columns --> <QTd - auto-width - class="no-margin" - :class="[getColAlign(col), col.columnClass]" - :style="col.style" + class="no-margin q-px-xs" v-if="col.visible ?? true" - @click.ctrl=" - ($event) => - rowCtrlClickFunction && rowCtrlClickFunction($event, row) - " + :style="{ + 'max-width': col?.width ?? false, + position: 'relative', + }" + :class="[ + col.columnClass, + 'body-cell no-margin no-padding', + getColAlign(col), + ]" + :data-row-index="rowIndex" + :data-col-field="col?.name" > - <slot - :name="`column-${col.name}`" - :col="col" - :row="row" - :row-index="rowIndex" + <div + class="no-padding no-margin peter" + style=" + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + " > - <VnTableColumn - :column="col" + <slot + :name="`column-${col.name}`" + :col="col" :row="row" - :is-editable="col.isEditable ?? isEditable" - v-model="row[col.name]" - component-prop="columnField" - /> - </slot> + :row-index="rowIndex" + > + <QIcon + v-if="col?.component === 'toggle'" + :name=" + col?.getIcon + ? col.getIcon(row[col?.name]) + : getToggleIcon(row[col?.name]) + " + style="color: var(--vn-text-color)" + :class="hasEditableFormat(col)" + size="14px" + /> + <QIcon + v-else-if="col?.component === 'checkbox'" + :name="getCheckboxIcon(row[col?.name])" + style="color: var(--vn-text-color)" + :class="hasEditableFormat(col)" + size="14px" + /> + <span + v-else + :class="hasEditableFormat(col)" + :style=" + typeof col?.style == 'function' + ? col.style(row) + : col?.style + " + style="bottom: 0" + > + {{ formatColumnValue(col, row, dashIfEmpty) }} + </span> + </slot> + </div> </QTd> </template> <template #body-cell-tableActions="{ col, row }"> @@ -478,7 +776,7 @@ function cardClick(_, row) { flat dense :class=" - btn.isPrimary ? 'text-primary-light' : 'color-vn-text ' + btn.isPrimary ? 'text-primary-light' : 'color-vn-label' " :style="`visibility: ${ ((btn.show && btn.show(row)) ?? true) @@ -486,6 +784,7 @@ function cardClick(_, row) { : 'hidden' }`" @click="btn.action(row)" + :data-cy="btn?.name ?? `tableAction-${index}`" /> </QTd> </template> @@ -534,7 +833,7 @@ function cardClick(_, row) { </QCardSection> <!-- Fields --> <QCardSection - class="q-pl-sm q-pr-lg q-py-xs" + class="q-pl-sm q-py-xs" :class="$props.cardClass" > <div @@ -555,7 +854,7 @@ function cardClick(_, row) { :row="row" :row-index="index" > - <VnTableColumn + <VnColumn :column="col" :row="row" :is-editable="false" @@ -583,12 +882,12 @@ function cardClick(_, row) { :icon="btn.icon" data-cy="cardBtn" class="q-pa-xs" - flat :class=" btn.isPrimary ? 'text-primary-light' - : 'color-vn-text ' + : 'color-vn-label' " + flat @click="btn.action(row)" /> </QCardSection> @@ -596,14 +895,17 @@ function cardClick(_, row) { </component> </template> <template #bottom-row="{ cols }" v-if="$props.footer"> - <QTr v-if="rows.length" style="height: 30px"> + <QTr v-if="rows.length" style="height: 45px"> + <QTh v-if="table.selection" /> <QTh v-for="col of cols.filter((cols) => cols.visible ?? true)" :key="col?.id" - class="text-center" :class="getColAlign(col)" > - <slot :name="`column-footer-${col.name}`" /> + <slot + :name="`column-footer-${col.name}`" + :isEditableColumn="isEditableColumn(col)" + /> </QTh> </QTr> </template> @@ -622,7 +924,7 @@ function cardClick(_, row) { size="md" round flat - shortcut="+" + v-shortcut="'+'" :disabled="!disabledAttr" /> <QTooltip> @@ -640,39 +942,52 @@ function cardClick(_, row) { color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" data-cy="vnTableCreateBtn" /> <QTooltip self="top right"> {{ createForm?.title }} </QTooltip> </QPageSticky> - <QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> + <QDialog + v-model="showForm" + transition-show="scale" + transition-hide="scale" + :full-width="createComplement?.isFullWidth ?? false" + data-cy="vn-table-create-dialog" + > <FormModelPopup + ref="createRef" v-bind="createForm" :model="$attrs['data-key'] + 'Create'" @on-data-saved="(_, res) => createForm.onDataSaved(res)" > <template #form-inputs="{ data }"> - <div class="grid-create"> - <slot - v-for="column of splittedColumns.create" - :key="column.name" - :name="`column-create-${column.name}`" - :data="data" - :column-name="column.name" - :label="column.label" - > - <VnTableColumn - :column="column" - :row="{}" - default="input" - v-model="data[column.name]" - :show-label="true" - component-prop="columnCreate" - /> - </slot> - <slot name="more-create-dialog" :data="data" /> + <div :style="createComplement?.containerStyle"> + <div> + <slot name="previous-create-dialog" :data="data" /> + </div> + <div class="grid-create" :style="createComplement?.columnGridStyle"> + <slot + v-for="column of splittedColumns.create" + :key="column.name" + :name="`column-create-${column.name}`" + :data="data" + :column-name="column.name" + :label="column.label" + > + <VnColumn + :column="column" + :row="{}" + default="input" + v-model="data[column.name]" + :show-label="true" + component-prop="columnCreate" + :data-cy="`${column.name}-create-popup`" + /> + </slot> + <slot name="more-create-dialog" :data="data" /> + </div> </div> </template> </FormModelPopup> @@ -690,6 +1005,42 @@ es: </i18n> <style lang="scss"> +.selection-cell { + table td:first-child { + padding: 0px; + } +} +.side-padding { + padding-left: 1px; + padding-right: 1px; +} +.editable-text:hover { + border-bottom: 1px dashed var(--q-primary); + @extend .side-padding; +} +.editable-text { + border-bottom: 1px dashed var(--vn-label-color); + @extend .side-padding; +} +.cell-input { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding-top: 0px !important; +} +.q-field--labeled .q-field__native, +.q-field--labeled .q-field__prefix, +.q-field--labeled .q-field__suffix { + padding-top: 20px; +} + +.body-cell { + padding-left: 4px !important; + padding-right: 4px !important; + position: relative; +} .bg-chip-secondary { background-color: var(--vn-page-color); color: var(--vn-text-color); @@ -706,8 +1057,8 @@ es: .grid-three { display: grid; - grid-template-columns: repeat(auto-fit, minmax(350px, max-content)); - max-width: 100%; + grid-template-columns: repeat(auto-fit, minmax(300px, max-content)); + width: 100%; grid-gap: 20px; margin: 0 auto; } @@ -715,7 +1066,6 @@ es: .grid-create { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, max-content)); - max-width: 100%; grid-gap: 20px; margin: 0 auto; } @@ -731,7 +1081,9 @@ es: } } } - +.q-table tbody tr td { + position: relative; +} .q-table { th { padding: 0; @@ -780,6 +1132,7 @@ es: .vn-label-value { display: flex; flex-direction: row; + align-items: center; color: var(--vn-text-color); .value { overflow: hidden; @@ -831,4 +1184,15 @@ es: .q-table__middle.q-virtual-scroll.q-virtual-scroll--vertical.scroll { background-color: var(--vn-section-color); } +.temp-input { + top: 0; + position: absolute; + width: 100%; + height: 100%; + display: flex; +} + +label.header-filter > .q-field__inner > .q-field__control { + padding: inherit; +} </style> diff --git a/src/components/VnTable/VnTableFilter.vue b/src/components/VnTable/VnTableFilter.vue index 732605ce5..79b903e54 100644 --- a/src/components/VnTable/VnTableFilter.vue +++ b/src/components/VnTable/VnTableFilter.vue @@ -27,31 +27,36 @@ function columnName(col) { </script> <template> <VnFilterPanel v-bind="$attrs" :search-button="true" :disable-submit-event="true"> - <template #body="{ params, orders }"> + <template #body="{ params, orders, searchFn }"> <div - class="row no-wrap flex-center" + class="container" v-for="col of columns.filter((c) => c.columnFilter ?? true)" :key="col.id" > - <VnFilter - ref="tableFilterRef" - :column="col" - :data-key="$attrs['data-key']" - v-model="params[columnName(col)]" - :search-url="searchUrl" - /> - <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 class="filter"> + <VnFilter + ref="tableFilterRef" + :column="col" + :data-key="$attrs['data-key']" + v-model="params[columnName(col)]" + :search-url="searchUrl" + /> + </div> + <div class="order"> + <VnTableOrder + v-if="col?.columnFilter !== false && col?.name !== 'tableActions'" + v-model="orders[col.orderBy ?? col.name]" + :name="col.orderBy ?? col.name" + :data-key="$attrs['data-key']" + :search-url="searchUrl" + :vertical="true" + /> + </div> </div> <slot name="moreFilterPanel" :params="params" + :search-fn="searchFn" :orders="orders" :columns="columns" /> @@ -67,3 +72,21 @@ function columnName(col) { </template> </VnFilterPanel> </template> +<style lang="scss" scoped> +.container { + display: flex; + justify-content: center; + align-items: center; + height: 45px; + gap: 10px; +} + +.filter { + width: 70%; + height: 40px; + text-align: center; +} +.order { + width: 10%; +} +</style> diff --git a/src/components/VnTable/VnVisibleColumn.vue b/src/components/VnTable/VnVisibleColumn.vue index dad950d73..6d15c585e 100644 --- a/src/components/VnTable/VnVisibleColumn.vue +++ b/src/components/VnTable/VnVisibleColumn.vue @@ -32,16 +32,21 @@ const areAllChecksMarked = computed(() => { function setUserConfigViewData(data, isLocal) { if (!data) return; - // Importante: El name de las columnas de la tabla debe conincidir con el name de las variables que devuelve la view config if (!isLocal) localColumns.value = []; - // Array to Object + const skippeds = $props.skip.reduce((a, v) => ({ ...a, [v]: v }), {}); for (let column of columns.value) { - const { label, name } = column; + const { label, name, labelAbbreviation } = column; if (skippeds[name]) continue; column.visible = data[name] ?? true; - if (!isLocal) localColumns.value.push({ name, label, visible: column.visible }); + if (!isLocal) + localColumns.value.push({ + name, + label, + labelAbbreviation, + visible: column.visible, + }); } } @@ -152,7 +157,11 @@ onMounted(async () => { <QCheckbox v-for="col in localColumns" :key="col.name" - :label="col.label ?? col.name" + :label=" + col?.labelAbbreviation + ? col.labelAbbreviation + ` (${col.label ?? col.name})` + : (col.label ?? col.name) + " v-model="col.visible" /> </div> diff --git a/src/components/__tests__/FormModel.spec.js b/src/components/__tests__/FormModel.spec.js index e35684bc3..3dce04374 100644 --- a/src/components/__tests__/FormModel.spec.js +++ b/src/components/__tests__/FormModel.spec.js @@ -57,6 +57,7 @@ describe('FormModel', () => { vm.state.set(model, formInitialData); expect(vm.hasChanges).toBe(false); + await vm.$nextTick(); vm.formData.mockKey = 'newVal'; await vm.$nextTick(); expect(vm.hasChanges).toBe(true); @@ -93,9 +94,13 @@ describe('FormModel', () => { it('should call axios.patch with the right data', async () => { const spy = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} }); - const { vm } = mount({ propsData: { url, model, formInitialData } }); - vm.formData.mockKey = 'newVal'; + const { vm } = mount({ propsData: { url, model } }); + + vm.formData = {}; await vm.$nextTick(); + vm.formData = { mockKey: 'newVal' }; + await vm.$nextTick(); + await vm.save(); expect(spy).toHaveBeenCalled(); vm.formData.mockKey = 'mockVal'; @@ -106,6 +111,7 @@ describe('FormModel', () => { const { vm } = mount({ propsData: { url, model, formInitialData, urlCreate: 'mockUrlCreate' }, }); + await vm.$nextTick(); vm.formData.mockKey = 'newVal'; await vm.$nextTick(); await vm.save(); @@ -119,7 +125,7 @@ describe('FormModel', () => { }); const spyPatch = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} }); const spySaveFn = vi.spyOn(vm.$props, 'saveFn'); - + await vm.$nextTick(); vm.formData.mockKey = 'newVal'; await vm.$nextTick(); await vm.save(); diff --git a/src/components/__tests__/Leftmenu.spec.js b/src/components/__tests__/Leftmenu.spec.js index 10d9d66fb..4ab8b527f 100644 --- a/src/components/__tests__/Leftmenu.spec.js +++ b/src/components/__tests__/Leftmenu.spec.js @@ -1,9 +1,12 @@ -import { vi, describe, expect, it, beforeAll } from 'vitest'; +import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest'; import { createWrapper, axios } from 'app/test/vitest/helper'; import Leftmenu from 'components/LeftMenu.vue'; - +import * as vueRouter from 'vue-router'; import { useNavigationStore } from 'src/stores/useNavigationStore'; +let vm; +let navigation; + vi.mock('src/router/modules', () => ({ default: [ { @@ -21,6 +24,16 @@ vi.mock('src/router/modules', () => ({ { path: '', name: 'CustomerMain', + meta: { + menu: 'Customer', + menuChildren: [ + { + name: 'CustomerCreditContracts', + title: 'creditContracts', + icon: 'vn:solunion', + }, + ], + }, children: [ { path: 'list', @@ -28,6 +41,13 @@ vi.mock('src/router/modules', () => ({ meta: { title: 'list', icon: 'view_list', + menuChildren: [ + { + name: 'CustomerCreditContracts', + title: 'creditContracts', + icon: 'vn:solunion', + }, + ], }, }, { @@ -44,51 +64,325 @@ vi.mock('src/router/modules', () => ({ }, ], })); - -describe('Leftmenu', () => { - let vm; - let navigation; - beforeAll(() => { - vi.spyOn(axios, 'get').mockResolvedValue({ - data: [], - }); - - vm = createWrapper(Leftmenu, { - propsData: { - source: 'main', +vi.spyOn(vueRouter, 'useRoute').mockReturnValue({ + matched: [ + { + path: '/', + redirect: { + name: 'Dashboard', }, - }).vm; - - navigation = useNavigationStore(); - navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true)); - navigation.getModules = vi.fn().mockReturnValue({ - value: [ + name: 'Main', + meta: {}, + props: { + default: false, + }, + children: [ { - name: 'customer', - title: 'customer.pageTitles.customers', - icon: 'vn:customer', - module: 'customer', + path: '/dashboard', + name: 'Dashboard', + meta: { + title: 'dashboard', + icon: 'dashboard', + }, }, ], + }, + { + path: '/customer', + redirect: { + name: 'CustomerMain', + }, + name: 'Customer', + meta: { + title: 'customers', + icon: 'vn:client', + moduleName: 'Customer', + keyBinding: 'c', + menu: 'customer', + }, + }, + ], + query: {}, + params: {}, + meta: { moduleName: 'mockName' }, + path: 'mockName/1', + name: 'Customer', +}); +function mount(source = 'main') { + vi.spyOn(axios, 'get').mockResolvedValue({ + data: [], + }); + const wrapper = createWrapper(Leftmenu, { + propsData: { + source, + }, + }); + + navigation = useNavigationStore(); + navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true)); + navigation.getModules = vi.fn().mockReturnValue({ + value: [ + { + name: 'customer', + title: 'customer.pageTitles.customers', + icon: 'vn:customer', + module: 'customer', + }, + ], + }); + return wrapper; +} + +describe('getRoutes', () => { + afterEach(() => vi.clearAllMocks()); + const getRoutes = vi.fn().mockImplementation((props, getMethodA, getMethodB) => { + const handleRoutes = { + methodA: getMethodA, + methodB: getMethodB, + }; + try { + handleRoutes[props.source](); + } catch (error) { + throw Error('Method not defined'); + } + }); + + const getMethodA = vi.fn(); + const getMethodB = vi.fn(); + const fn = (props) => getRoutes(props, getMethodA, getMethodB); + + it('should call getMethodB when source is card', () => { + let props = { source: 'methodB' }; + fn(props); + + expect(getMethodB).toHaveBeenCalled(); + expect(getMethodA).not.toHaveBeenCalled(); + }); + it('should call getMethodA when source is main', () => { + let props = { source: 'methodA' }; + fn(props); + + expect(getMethodA).toHaveBeenCalled(); + expect(getMethodB).not.toHaveBeenCalled(); + }); + + it('should call getMethodA when source is not exists or undefined', () => { + let props = { source: 'methodC' }; + expect(() => fn(props)).toThrowError('Method not defined'); + + expect(getMethodA).not.toHaveBeenCalled(); + expect(getMethodB).not.toHaveBeenCalled(); + }); +}); + +describe('Leftmenu as card', () => { + beforeAll(() => { + vm = mount('card').vm; + }); + + it('should get routes for card source', async () => { + vm.getRoutes(); + }); +}); +describe('Leftmenu as main', () => { + beforeEach(() => { + vm = mount().vm; + }); + + it('should initialize with default props', () => { + expect(vm.source).toBe('main'); + }); + + it('should filter items based on search input', async () => { + vm.search = 'cust'; + await vm.$nextTick(); + expect(vm.filteredItems[0].name).toEqual('customer'); + expect(vm.filteredItems[0].module).toEqual('customer'); + }); + it('should filter items based on search input', async () => { + vm.search = 'Rou'; + await vm.$nextTick(); + expect(vm.filteredItems).toEqual([]); + }); + + it('should return pinned items', () => { + vm.items = [ + { name: 'Item 1', isPinned: false }, + { name: 'Item 2', isPinned: true }, + ]; + expect(vm.pinnedModules).toEqual( + new Map([['Item 2', { name: 'Item 2', isPinned: true }]]), + ); + }); + + it('should find matches in routes', () => { + const search = 'child1'; + const item = { + children: [ + { name: 'child1', children: [] }, + { name: 'child2', children: [] }, + ], + }; + const matches = vm.findMatches(search, item); + expect(matches).toEqual([{ name: 'child1', children: [] }]); + }); + it('should not proceed if event is already prevented', async () => { + const item = { module: 'testModule', isPinned: false }; + const event = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + defaultPrevented: true, + }; + + await vm.togglePinned(item, event); + + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(event.stopPropagation).not.toHaveBeenCalled(); + }); + + it('should call quasar.notify with success message', async () => { + const item = { module: 'testModule', isPinned: false }; + const event = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + defaultPrevented: false, + }; + const response = { data: { id: 1 } }; + + vi.spyOn(axios, 'post').mockResolvedValue(response); + vi.spyOn(vm.quasar, 'notify'); + + await vm.togglePinned(item, event); + + expect(vm.quasar.notify).toHaveBeenCalledWith({ + message: 'Data saved', + type: 'positive', }); }); - it('should return a proper formated object with two child items', async () => { - const expectedMenuItem = [ - { - children: null, - name: 'CustomerList', - title: 'globals.pageTitles.list', - icon: 'view_list', - }, - { - children: null, - name: 'CustomerCreate', - title: 'globals.pageTitles.createCustomer', - icon: 'vn:addperson', - }, - ]; - const firstMenuItem = vm.items[0]; - expect(firstMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem)); + it('should handle a single matched route with a menu', () => { + const route = { + matched: [{ meta: { menu: 'customer' } }], + }; + + const result = vm.betaGetRoutes(); + + expect(result.meta.menu).toEqual(route.matched[0].meta.menu); + }); + it('should get routes for main source', () => { + vm.props.source = 'main'; + vm.getRoutes(); + expect(navigation.getModules).toHaveBeenCalled(); + }); + + it('should find direct child matches', () => { + const search = 'child1'; + const item = { + children: [{ name: 'child1' }, { name: 'child2' }], + }; + const result = vm.findMatches(search, item); + expect(result).toEqual([{ name: 'child1' }]); + }); + + it('should find nested child matches', () => { + const search = 'child3'; + const item = { + children: [ + { name: 'child1' }, + { + name: 'child2', + children: [{ name: 'child3' }], + }, + ], + }; + const result = vm.findMatches(search, item); + expect(result).toEqual([{ name: 'child3' }]); + }); +}); + +describe('normalize', () => { + beforeAll(() => { + vm = mount('card').vm; + }); + it('should normalize and lowercase text', () => { + const input = 'ÁÉÍÓÚáéíóú'; + const expected = 'aeiouaeiou'; + expect(vm.normalize(input)).toBe(expected); + }); + + it('should handle empty string', () => { + const input = ''; + const expected = ''; + expect(vm.normalize(input)).toBe(expected); + }); + + it('should handle text without diacritics', () => { + const input = 'hello'; + const expected = 'hello'; + expect(vm.normalize(input)).toBe(expected); + }); + + it('should handle mixed text', () => { + const input = 'Héllo Wórld!'; + const expected = 'hello world!'; + expect(vm.normalize(input)).toBe(expected); + }); +}); + +describe('addChildren', () => { + const module = 'testModule'; + beforeEach(() => { + vm = mount().vm; + vi.clearAllMocks(); + }); + + it('should add menu items to parent if matches are found', () => { + const parent = 'testParent'; + const route = { + meta: { + menu: 'testMenu', + }, + children: [{ name: 'child1' }, { name: 'child2' }], + }; + vm.addChildren(module, route, parent); + + expect(navigation.addMenuItem).toHaveBeenCalled(); + }); + + it('should handle routes with no meta menu', () => { + const route = { + meta: {}, + menus: {}, + }; + + const parent = []; + + vm.addChildren(module, route, parent); + expect(navigation.addMenuItem).toHaveBeenCalled(); + }); + + it('should handle empty parent array', () => { + const parent = []; + const route = { + meta: { + menu: 'child11', + }, + children: [ + { + name: 'child1', + meta: { + menuChildren: [ + { + name: 'CustomerCreditContracts', + title: 'creditContracts', + icon: 'vn:solunion', + }, + ], + }, + }, + ], + }; + vm.addChildren(module, route, parent); + expect(navigation.addMenuItem).toHaveBeenCalled(); }); }); diff --git a/src/components/__tests__/UserPanel.spec.js b/src/components/__tests__/UserPanel.spec.js index ac20f911e..9e449745a 100644 --- a/src/components/__tests__/UserPanel.spec.js +++ b/src/components/__tests__/UserPanel.spec.js @@ -1,61 +1,65 @@ -import { vi, describe, expect, it, beforeEach, beforeAll, afterEach } from 'vitest'; +import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest'; import { createWrapper } from 'app/test/vitest/helper'; import UserPanel from 'src/components/UserPanel.vue'; import axios from 'axios'; import { useState } from 'src/composables/useState'; +vi.mock('src/utils/quasarLang', () => ({ + default: vi.fn(), +})); + describe('UserPanel', () => { - let wrapper; - let vm; - let state; + let wrapper; + let vm; + let state; - beforeEach(() => { - wrapper = createWrapper(UserPanel, {}); - state = useState(); - state.setUser({ - id: 115, - name: 'itmanagement', - nickname: 'itManagementNick', - lang: 'en', - darkMode: false, - companyFk: 442, - warehouseFk: 1, - }); - wrapper = wrapper.wrapper; - vm = wrapper.vm; + beforeEach(() => { + wrapper = createWrapper(UserPanel, {}); + state = useState(); + state.setUser({ + id: 115, + name: 'itmanagement', + nickname: 'itManagementNick', + lang: 'en', + darkMode: false, + companyFk: 442, + warehouseFk: 1, }); + wrapper = wrapper.wrapper; + vm = wrapper.vm; + }); - afterEach(() => { - vi.clearAllMocks(); - }); + afterEach(() => { + vi.clearAllMocks(); + }); - it('should fetch warehouses data on mounted', async () => { - const fetchData = wrapper.findComponent({ name: 'FetchData' }); - expect(fetchData.props('url')).toBe('Warehouses'); - expect(fetchData.props('autoLoad')).toBe(true); - }); + it('should fetch warehouses data on mounted', async () => { + const fetchData = wrapper.findComponent({ name: 'FetchData' }); + expect(fetchData.props('url')).toBe('Warehouses'); + expect(fetchData.props('autoLoad')).toBe(true); + }); - it('should toggle dark mode correctly and update preferences', async () => { - await vm.saveDarkMode(true); - expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true }); - expect(vm.user.darkMode).toBe(true); - vm.updatePreferences(); - expect(vm.darkMode).toBe(true); - }); + it('should toggle dark mode correctly and update preferences', async () => { + await vm.saveDarkMode(true); + expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true }); + expect(vm.user.darkMode).toBe(true); + await vm.updatePreferences(); + expect(vm.darkMode).toBe(true); + }); - it('should change user language and update preferences', async () => { - const userLanguage = 'es'; - await vm.saveLanguage(userLanguage); - expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage }); - expect(vm.user.lang).toBe(userLanguage); - vm.updatePreferences(); - expect(vm.locale).toBe(userLanguage); - }); + it('should change user language and update preferences', async () => { + const userLanguage = 'es'; + await vm.saveLanguage(userLanguage); + expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage }); + expect(vm.user.lang).toBe(userLanguage); + await vm.updatePreferences(); + expect(vm.locale).toBe(userLanguage); + }); - it('should update user data', async () => { - const key = 'name'; - const value = 'itboss'; - await vm.saveUserData(key, value); - expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value }); - }); -}); + it('should update user data', async () => { + const key = 'name'; + const value = 'itboss'; + await vm.saveUserData(key, value); + expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value }); + }); +}); \ No newline at end of file diff --git a/src/components/common/VnCard.vue b/src/components/common/VnCard.vue index 0d80f43ce..44002c22a 100644 --- a/src/components/common/VnCard.vue +++ b/src/components/common/VnCard.vue @@ -10,11 +10,11 @@ import LeftMenu from 'components/LeftMenu.vue'; import RightMenu from 'components/common/RightMenu.vue'; const props = defineProps({ dataKey: { type: String, required: true }, - baseUrl: { type: String, default: undefined }, - customUrl: { type: String, default: undefined }, + url: { type: String, default: undefined }, filter: { type: Object, default: () => {} }, descriptor: { type: Object, required: true }, filterPanel: { type: Object, default: undefined }, + idInWhere: { type: Boolean, default: false }, searchDataKey: { type: String, default: undefined }, searchbarProps: { type: Object, default: undefined }, redirectOnError: { type: Boolean, default: false }, @@ -23,25 +23,20 @@ const props = defineProps({ const stateStore = useStateStore(); const route = useRoute(); const router = useRouter(); -const url = computed(() => { - if (props.baseUrl) { - return `${props.baseUrl}/${route.params.id}`; - } - return props.customUrl; -}); const searchRightDataKey = computed(() => { if (!props.searchDataKey) return route.name; return props.searchDataKey; }); + const arrayData = useArrayData(props.dataKey, { - url: url.value, - filter: props.filter, + url: props.url, + userFilter: props.filter, + oneRecord: true, }); onBeforeMount(async () => { try { - if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; - await arrayData.fetch({ append: false, updateRouter: false }); + await fetch(route.params.id); } catch { const { matched: matches } = router.currentRoute.value; const { path } = matches.at(-1); @@ -49,13 +44,17 @@ onBeforeMount(async () => { } }); -if (props.baseUrl) { - onBeforeRouteUpdate(async (to, from) => { - if (to.params.id !== from.params.id) { - arrayData.store.url = `${props.baseUrl}/${to.params.id}`; - await arrayData.fetch({ append: false, updateRouter: false }); - } - }); +onBeforeRouteUpdate(async (to, from) => { + const id = to.params.id; + if (id !== from.params.id) await fetch(id, true); +}); + +async function fetch(id, append = false) { + const regex = /\/(\d+)/; + if (props.idInWhere) arrayData.store.filter.where = { id }; + else if (!regex.test(props.url)) arrayData.store.url = `${props.url}/${id}`; + else arrayData.store.url = props.url.replace(regex, `/${id}`); + await arrayData.fetch({ append, updateRouter: false }); } </script> <template> @@ -83,7 +82,7 @@ if (props.baseUrl) { <QPage> <VnSubToolbar /> <div :class="[useCardSize(), $attrs.class]"> - <RouterView :key="route.path" /> + <RouterView :key="$route.path" /> </div> </QPage> </QPageContainer> diff --git a/src/components/common/VnCardBeta.vue b/src/components/common/VnCardBeta.vue index f237a300c..7c82316dc 100644 --- a/src/components/common/VnCardBeta.vue +++ b/src/components/common/VnCardBeta.vue @@ -1,6 +1,6 @@ <script setup> -import { onBeforeMount, computed } from 'vue'; -import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router'; +import { onBeforeMount } from 'vue'; +import { useRouter, onBeforeRouteUpdate } from 'vue-router'; import { useArrayData } from 'src/composables/useArrayData'; import { useStateStore } from 'stores/useStateStore'; import useCardSize from 'src/composables/useCardSize'; @@ -9,10 +9,9 @@ import VnSubToolbar from '../ui/VnSubToolbar.vue'; const props = defineProps({ dataKey: { type: String, required: true }, - baseUrl: { type: String, default: undefined }, - customUrl: { type: String, default: undefined }, + url: { type: String, default: undefined }, + idInWhere: { type: Boolean, default: false }, filter: { type: Object, default: () => {} }, - userFilter: { type: Object, default: () => {} }, descriptor: { type: Object, required: true }, filterPanel: { type: Object, default: undefined }, searchDataKey: { type: String, default: undefined }, @@ -21,46 +20,42 @@ const props = defineProps({ }); const stateStore = useStateStore(); -const route = useRoute(); const router = useRouter(); -const url = computed(() => { - if (props.baseUrl) { - return `${props.baseUrl}/${route.params.id}`; - } - return props.customUrl; -}); - const arrayData = useArrayData(props.dataKey, { - url: url.value, - filter: props.filter, - userFilter: props.userFilter, + url: props.url, + userFilter: props.filter, + oneRecord: true, }); onBeforeMount(async () => { + const route = router.currentRoute.value; try { - if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; - await arrayData.fetch({ append: false, updateRouter: false }); + await fetch(route.params.id); } catch { - const { matched: matches } = router.currentRoute.value; + const { matched: matches } = route; const { path } = matches.at(-1); router.push({ path: path.replace(/:id.*/, '') }); } }); -if (props.baseUrl) { - onBeforeRouteUpdate(async (to, from) => { - if (hasRouteParam(to.params)) { - const { matched } = router.currentRoute.value; - const { name } = matched.at(-3); - if (name) { - router.push({ name, params: to.params }); - } +onBeforeRouteUpdate(async (to, from) => { + if (hasRouteParam(to.params)) { + const { matched } = router.currentRoute.value; + const { name } = matched.at(-3); + if (name) { + router.push({ name, params: to.params }); } - if (to.params.id !== from.params.id) { - arrayData.store.url = `${props.baseUrl}/${to.params.id}`; - await arrayData.fetch({ append: false, updateRouter: false }); - } - }); + } + const id = to.params.id; + if (id !== from.params.id) await fetch(id, true); +}); + +async function fetch(id, append = false) { + const regex = /\/(\d+)/; + if (props.idInWhere) arrayData.store.filter.where = { id }; + else if (!regex.test(props.url)) arrayData.store.url = `${props.url}/${id}`; + else arrayData.store.url = props.url.replace(regex, `/${id}`); + await arrayData.fetch({ append, updateRouter: false }); } function hasRouteParam(params, valueToCheck = ':addressId') { return Object.values(params).includes(valueToCheck); @@ -74,6 +69,6 @@ function hasRouteParam(params, valueToCheck = ':addressId') { </Teleport> <VnSubToolbar /> <div :class="[useCardSize(), $attrs.class]"> - <RouterView :key="route.path" /> + <RouterView :key="$route.path" /> </div> </template> diff --git a/src/components/common/VnCheckbox.vue b/src/components/common/VnCheckbox.vue new file mode 100644 index 000000000..27131d45e --- /dev/null +++ b/src/components/common/VnCheckbox.vue @@ -0,0 +1,43 @@ +<script setup> +import { computed } from 'vue'; + +const model = defineModel({ type: [Number, Boolean] }); +const $props = defineProps({ + info: { + type: String, + default: null, + }, +}); + +const checkboxModel = computed({ + get() { + if (typeof model.value === 'number') { + return model.value !== 0; + } + return model.value; + }, + set(value) { + if (typeof model.value === 'number') { + model.value = value ? 1 : 0; + } else { + model.value = value; + } + }, +}); +</script> +<template> + <div> + <QCheckbox v-bind="$attrs" v-on="$attrs" v-model="checkboxModel" /> + <QIcon + v-if="info" + v-bind="$attrs" + class="cursor-info q-ml-sm" + name="info" + size="sm" + > + <QTooltip> + {{ info }} + </QTooltip> + </QIcon> + </div> +</template> diff --git a/src/components/common/VnColor.vue b/src/components/common/VnColor.vue new file mode 100644 index 000000000..8a5a787b0 --- /dev/null +++ b/src/components/common/VnColor.vue @@ -0,0 +1,32 @@ +<script setup> +const $props = defineProps({ + colors: { + type: String, + default: '{"value": []}', + }, +}); + +const colorArray = JSON.parse($props.colors)?.value; +const maxHeight = 30; +const colorHeight = maxHeight / colorArray?.length; +</script> +<template> + <div v-if="colors" class="color-div" :style="{ height: `${maxHeight}px` }"> + <div + v-for="(color, index) in colorArray" + :key="index" + :style="{ + backgroundColor: `#${color}`, + height: `${colorHeight}px`, + }" + > + + </div> + </div> +</template> +<style scoped> +.color-div { + display: flex; + flex-direction: column; +} +</style> diff --git a/src/components/common/VnComponent.vue b/src/components/common/VnComponent.vue index 580bcf348..a9e1c8cff 100644 --- a/src/components/common/VnComponent.vue +++ b/src/components/common/VnComponent.vue @@ -17,6 +17,8 @@ const $props = defineProps({ }, }); +const emit = defineEmits(['blur']); + const componentArray = computed(() => { if (typeof $props.prop === 'object') return [$props.prop]; return $props.prop; @@ -46,7 +48,8 @@ function toValueAttrs(attrs) { <span v-for="toComponent of componentArray" :key="toComponent.name" - class="column flex-center fit" + class="column fit" + :class="toComponent?.component == 'checkbox' ? 'flex-center' : ''" > <component v-if="toComponent?.component" @@ -54,6 +57,7 @@ function toValueAttrs(attrs) { v-bind="mix(toComponent).attrs" v-on="mix(toComponent).event ?? {}" v-model="model" + @blur="emit('blur')" /> </span> </template> diff --git a/src/components/common/VnDmsList.vue b/src/components/common/VnDmsList.vue index 36c87bab0..424781a26 100644 --- a/src/components/common/VnDmsList.vue +++ b/src/components/common/VnDmsList.vue @@ -17,7 +17,7 @@ import { useSession } from 'src/composables/useSession'; const route = useRoute(); const quasar = useQuasar(); const { t } = useI18n(); -const rows = ref(); +const rows = ref([]); const dmsRef = ref(); const formDialog = ref({}); const token = useSession().getTokenMultimedia(); @@ -389,6 +389,14 @@ defineExpose({ </div> </template> </QTable> + <div + v-else + class="info-row q-pa-md text-center" + > + <h5> + {{ t('No data to display') }} + </h5> + </div> </template> </VnPaginate> <QDialog v-model="formDialog.show"> @@ -405,7 +413,7 @@ defineExpose({ fab color="primary" icon="add" - shortcut="+" + v-shortcut @click="showFormDialog()" class="fill-icon" > diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index 78f08a479..aeb4a31fd 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -11,6 +11,7 @@ const emit = defineEmits([ 'update:options', 'keyup.enter', 'remove', + 'blur', ]); const $props = defineProps({ @@ -136,6 +137,7 @@ const handleUppercase = () => { :type="$attrs.type" :class="{ required: isRequired }" @keyup.enter="emit('keyup.enter')" + @blur="emit('blur')" @keydown="handleKeydown" :clearable="false" :rules="mixinRules" @@ -143,7 +145,7 @@ const handleUppercase = () => { hide-bottom-space :data-cy="$attrs.dataCy ?? $attrs.label + '_input'" > - <template #prepend> + <template #prepend v-if="$slots.prepend"> <slot name="prepend" /> </template> <template #append> @@ -168,11 +170,11 @@ const handleUppercase = () => { } " ></QIcon> - + <QIcon name="match_case" size="xs" - v-if="!$attrs.disabled && !($attrs.readonly) && $props.uppercase" + v-if="!$attrs.disabled && !$attrs.readonly && $props.uppercase" @click="handleUppercase" class="uppercase-icon" > @@ -180,7 +182,7 @@ const handleUppercase = () => { {{ t('Convert to uppercase') }} </QTooltip> </QIcon> - + <slot name="append" v-if="$slots.append && !$attrs.disabled" /> <QIcon v-if="info" name="info"> <QTooltip max-width="350px"> @@ -194,13 +196,15 @@ const handleUppercase = () => { <style> .uppercase-icon { - transition: color 0.3s, transform 0.2s; - cursor: pointer; + transition: + color 0.3s, + transform 0.2s; + cursor: pointer; } .uppercase-icon:hover { - color: #ed9937; - transform: scale(1.2); + color: #ed9937; + transform: scale(1.2); } </style> <i18n> @@ -214,4 +218,4 @@ const handleUppercase = () => { maxLength: El valor excede los {value} carácteres inputMax: Debe ser menor a {value} Convert to uppercase: Convertir a mayúsculas -</i18n> \ No newline at end of file +</i18n> diff --git a/src/components/common/VnInputDate.vue b/src/components/common/VnInputDate.vue index 13c141343..1f4705faa 100644 --- a/src/components/common/VnInputDate.vue +++ b/src/components/common/VnInputDate.vue @@ -42,7 +42,7 @@ const formattedDate = computed({ if (value.at(2) == '/') value = value.split('/').reverse().join('/'); value = date.formatDate( new Date(value).toISOString(), - 'YYYY-MM-DDTHH:mm:ss.SSSZ' + 'YYYY-MM-DDTHH:mm:ss.SSSZ', ); } const [year, month, day] = value.split('-').map((e) => parseInt(e)); @@ -55,7 +55,7 @@ const formattedDate = computed({ orgDate.getHours(), orgDate.getMinutes(), orgDate.getSeconds(), - orgDate.getMilliseconds() + orgDate.getMilliseconds(), ); } } @@ -64,7 +64,7 @@ const formattedDate = computed({ }); const popupDate = computed(() => - model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value + model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value, ); onMounted(() => { // fix quasar bug @@ -73,7 +73,7 @@ onMounted(() => { watch( () => model.value, (val) => (formattedDate.value = val), - { immediate: true } + { immediate: true }, ); const styleAttrs = computed(() => { diff --git a/src/components/common/VnInputNumber.vue b/src/components/common/VnInputNumber.vue index 165cfae3d..274f78b21 100644 --- a/src/components/common/VnInputNumber.vue +++ b/src/components/common/VnInputNumber.vue @@ -8,6 +8,7 @@ defineProps({ }); const model = defineModel({ type: [Number, String] }); +const emit = defineEmits(['blur']); </script> <template> <VnInput @@ -24,5 +25,6 @@ const model = defineModel({ type: [Number, String] }); model = parseFloat(val).toFixed(decimalPlaces); } " + @blur="emit('blur')" /> </template> diff --git a/src/components/common/VnPopupProxy.vue b/src/components/common/VnPopupProxy.vue new file mode 100644 index 000000000..f386bfff8 --- /dev/null +++ b/src/components/common/VnPopupProxy.vue @@ -0,0 +1,38 @@ +<script setup> +import { ref } from 'vue'; + +defineProps({ + label: { + type: String, + default: '', + }, + icon: { + type: String, + required: true, + default: null, + }, + color: { + type: String, + default: 'primary', + }, + tooltip: { + type: String, + default: null, + }, +}); +const popupProxyRef = ref(null); +</script> + +<template> + <QBtn :color="$props.color" :icon="$props.icon" :label="$t($props.label)"> + <template #default> + <slot name="extraIcon"></slot> + <QPopupProxy ref="popupProxyRef" style="max-width: none"> + <QCard> + <slot :popup="popupProxyRef"></slot> + </QCard> + </QPopupProxy> + <QTooltip>{{ $t($props.tooltip) }}</QTooltip> + </template> + </QBtn> +</template> diff --git a/src/components/common/VnSection.vue b/src/components/common/VnSection.vue index ef65b841f..4bd17124f 100644 --- a/src/components/common/VnSection.vue +++ b/src/components/common/VnSection.vue @@ -106,7 +106,14 @@ function checkIsMain() { :data-key="dataKey" :array-data="arrayData" :columns="columns" - /> + > + <template #moreFilterPanel="{ params, orders, searchFn }"> + <slot + name="moreFilterPanel" + v-bind="{ params, orders, searchFn }" + /> + </template> + </VnTableFilter> </slot> </template> </RightAdvancedMenu> diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 95fe80a69..339f90e0e 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -10,7 +10,12 @@ const emit = defineEmits(['update:modelValue', 'update:options', 'remove']); const $attrs = useAttrs(); const { t } = useI18n(); -const { isRequired, requiredFieldRule } = useRequired($attrs); +const isRequired = computed(() => { + return useRequired($attrs).isRequired; +}); +const requiredFieldRule = computed(() => { + return useRequired($attrs).requiredFieldRule; +}); const $props = defineProps({ modelValue: { @@ -166,7 +171,8 @@ onMounted(() => { }); const arrayDataKey = - $props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label); + $props.dataKey ?? + ($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label)); const arrayData = useArrayData(arrayDataKey, { url: $props.url, @@ -215,7 +221,7 @@ async function fetchFilter(val) { optionFilterValue.value ?? (new RegExp(/\d/g).test(val) ? optionValue.value - : optionFilter.value ?? optionLabel.value); + : (optionFilter.value ?? optionLabel.value)); let defaultWhere = {}; if ($props.filterOptions.length) { @@ -234,7 +240,7 @@ async function fetchFilter(val) { const { data } = await arrayData.applyFilter( { filter: filterOptions }, - { updateRouter: false } + { updateRouter: false }, ); setOptions(data); return data; @@ -267,7 +273,7 @@ async function filterHandler(val, update) { ref.setOptionIndex(-1); ref.moveOptionSelection(1, true); } - } + }, ); } @@ -303,7 +309,7 @@ function handleKeyDown(event) { if (inputValue) { const matchingOption = myOptions.value.find( (option) => - option[optionLabel.value].toLowerCase() === inputValue.toLowerCase() + option[optionLabel.value].toLowerCase() === inputValue.toLowerCase(), ); if (matchingOption) { @@ -315,11 +321,11 @@ function handleKeyDown(event) { } const focusableElements = document.querySelectorAll( - 'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])' + 'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])', ); const currentIndex = Array.prototype.indexOf.call( focusableElements, - event.target + event.target, ); if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) { focusableElements[currentIndex + 1].focus(); diff --git a/src/components/common/VnSelectCache.vue b/src/components/common/VnSelectCache.vue index 29cf22dc5..f0f3357f6 100644 --- a/src/components/common/VnSelectCache.vue +++ b/src/components/common/VnSelectCache.vue @@ -14,7 +14,7 @@ const $props = defineProps({ }, }); const options = ref([]); - +const emit = defineEmits(['blur']); onBeforeMount(async () => { const { url, optionValue, optionLabel } = useAttrs(); const findBy = $props.find ?? url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1); @@ -35,5 +35,5 @@ onBeforeMount(async () => { }); </script> <template> - <VnSelect v-bind="$attrs" :options="$attrs.options ?? options" /> + <VnSelect v-bind="$attrs" :options="$attrs.options ?? options" @blur="emit('blur')" /> </template> diff --git a/src/components/common/VnSelectDialog.vue b/src/components/common/VnSelectDialog.vue index a4cd0011d..41730b217 100644 --- a/src/components/common/VnSelectDialog.vue +++ b/src/components/common/VnSelectDialog.vue @@ -37,7 +37,6 @@ const isAllowedToCreate = computed(() => { defineExpose({ vnSelectDialogRef: select }); </script> - <template> <VnSelect ref="select" @@ -67,7 +66,6 @@ defineExpose({ vnSelectDialogRef: select }); </template> </VnSelect> </template> - <style lang="scss" scoped> .default-icon { cursor: pointer; diff --git a/src/components/common/VnSelectSupplier.vue b/src/components/common/VnSelectSupplier.vue index f86db4f2d..5b52ae75b 100644 --- a/src/components/common/VnSelectSupplier.vue +++ b/src/components/common/VnSelectSupplier.vue @@ -1,9 +1,7 @@ <script setup> -import { computed } from 'vue'; import VnSelect from 'components/common/VnSelect.vue'; const model = defineModel({ type: [String, Number, Object] }); -const url = 'Suppliers'; </script> <template> @@ -11,11 +9,13 @@ const url = 'Suppliers'; :label="$t('globals.supplier')" v-bind="$attrs" v-model="model" - :url="url" + url="Suppliers" option-value="id" option-label="nickname" :fields="['id', 'name', 'nickname', 'nif']" + :filter-options="['id', 'name', 'nickname', 'nif']" sort-by="name ASC" + data-cy="vnSupplierSelect" > <template #option="scope"> <QItem v-bind="scope.itemProps"> diff --git a/src/components/common/VnSelectTravelExtended.vue b/src/components/common/VnSelectTravelExtended.vue new file mode 100644 index 000000000..46538f5f9 --- /dev/null +++ b/src/components/common/VnSelectTravelExtended.vue @@ -0,0 +1,50 @@ +<script setup> +import VnSelectDialog from './VnSelectDialog.vue'; +import FilterTravelForm from 'src/components/FilterTravelForm.vue'; +import { useI18n } from 'vue-i18n'; +import { toDate } from 'src/filters'; +const { t } = useI18n(); + +const $props = defineProps({ + data: { + type: Object, + required: true, + }, + onFilterTravelSelected: { + type: Function, + required: true, + }, +}); +</script> +<template> + <VnSelectDialog + :label="t('entry.basicData.travel')" + v-bind="$attrs" + url="Travels/filter" + :fields="['id', 'warehouseInName']" + option-value="id" + option-label="warehouseInName" + map-options + hide-selected + :required="true" + action-icon="filter_alt" + :roles-allowed-to-create="['buyer']" + > + <template #form> + <FilterTravelForm @travel-selected="onFilterTravelSelected(data, $event)" /> + </template> + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.agencyModeName }} - + {{ scope.opt?.warehouseInName }} + ({{ toDate(scope.opt?.shipped) }}) → + {{ scope.opt?.warehouseOutName }} + ({{ toDate(scope.opt?.landed) }}) + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelectDialog> +</template> diff --git a/src/components/common/__tests__/VnNotes.spec.js b/src/components/common/__tests__/VnNotes.spec.js index 8f24a7f14..2603bf03c 100644 --- a/src/components/common/__tests__/VnNotes.spec.js +++ b/src/components/common/__tests__/VnNotes.spec.js @@ -1,51 +1,78 @@ -import { describe, it, expect, vi, beforeAll, afterEach, beforeEach } from 'vitest'; +import { + describe, + it, + expect, + vi, + beforeAll, + afterEach, + beforeEach, + afterAll, +} from 'vitest'; import { createWrapper, axios } from 'app/test/vitest/helper'; import VnNotes from 'src/components/ui/VnNotes.vue'; +import vnDate from 'src/boot/vnDate'; describe('VnNotes', () => { let vm; let wrapper; let spyFetch; let postMock; - let expectedBody; - const mockData= {name: 'Tony', lastName: 'Stark', text: 'Test Note', observationTypeFk: 1}; - - function generateExpectedBody() { - expectedBody = {...vm.$props.body, ...{ text: vm.newNote.text, observationTypeFk: vm.newNote.observationTypeFk }}; - } - - async function setTestParams(text, observationType, type){ - vm.newNote.text = text; - vm.newNote.observationTypeFk = observationType; - wrapper.setProps({ selectType: type }); - } - - beforeAll(async () => { - vi.spyOn(axios, 'get').mockReturnValue({ data: [] }); - + let patchMock; + let expectedInsertBody; + let expectedUpdateBody; + const defaultOptions = { + url: '/test', + body: { name: 'Tony', lastName: 'Stark' }, + selectType: false, + saveUrl: null, + justInput: false, + }; + function generateWrapper( + options = defaultOptions, + text = null, + observationType = null, + ) { + vi.spyOn(axios, 'get').mockResolvedValue({ data: [] }); wrapper = createWrapper(VnNotes, { - propsData: { - url: '/test', - body: { name: 'Tony', lastName: 'Stark' }, - } + propsData: options, }); wrapper = wrapper.wrapper; vm = wrapper.vm; - }); + vm.newNote.text = text; + vm.newNote.observationTypeFk = observationType; + } + + function createSpyFetch() { + spyFetch = vi.spyOn(vm.$refs.vnPaginateRef, 'fetch'); + } + + function generateExpectedBody() { + expectedInsertBody = { + ...vm.$props.body, + ...{ text: vm.newNote.text, observationTypeFk: vm.newNote.observationTypeFk }, + }; + expectedUpdateBody = { ...vm.$props.body, ...{ notes: vm.newNote.text } }; + } beforeEach(() => { - postMock = vi.spyOn(axios, 'post').mockResolvedValue(mockData); - spyFetch = vi.spyOn(vm.vnPaginateRef, 'fetch').mockImplementation(() => vi.fn()); + postMock = vi.spyOn(axios, 'post'); + patchMock = vi.spyOn(axios, 'patch'); }); afterEach(() => { vi.clearAllMocks(); - expectedBody = {}; + expectedInsertBody = {}; + expectedUpdateBody = {}; + }); + + afterAll(() => { + vi.restoreAllMocks(); }); describe('insert', () => { - it('should not call axios.post and vnPaginateRef.fetch if newNote.text is null', async () => { - await setTestParams( null, null, true ); + it('should not call axios.post and vnPaginateRef.fetch when newNote.text is null', async () => { + generateWrapper({ selectType: true }); + createSpyFetch(); await vm.insert(); @@ -53,8 +80,9 @@ describe('VnNotes', () => { expect(spyFetch).not.toHaveBeenCalled(); }); - it('should not call axios.post and vnPaginateRef.fetch if newNote.text is empty', async () => { - await setTestParams( "", null, false ); + it('should not call axios.post and vnPaginateRef.fetch when newNote.text is empty', async () => { + generateWrapper(null, ''); + createSpyFetch(); await vm.insert(); @@ -62,8 +90,9 @@ describe('VnNotes', () => { expect(spyFetch).not.toHaveBeenCalled(); }); - it('should not call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is true', async () => { - await setTestParams( "Test Note", null, true ); + it('should not call axios.post and vnPaginateRef.fetch when observationTypeFk is null and selectType is true', async () => { + generateWrapper({ selectType: true }, 'Test Note'); + createSpyFetch(); await vm.insert(); @@ -71,37 +100,57 @@ describe('VnNotes', () => { expect(spyFetch).not.toHaveBeenCalled(); }); - it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is missing and selectType is false', async () => { - await setTestParams( "Test Note", null, false ); - + it('should call axios.post and vnPaginateRef.fetch when observationTypeFk is missing and selectType is false', async () => { + generateWrapper(null, 'Test Note'); + createSpyFetch(); generateExpectedBody(); await vm.insert(); - expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody); - expect(spyFetch).toHaveBeenCalled(); - }); - - it('should call axios.post and vnPaginateRef.fetch if observationTypeFk is setted and selectType is false', async () => { - await setTestParams( "Test Note", 1, false ); - - generateExpectedBody(); - - await vm.insert(); - - expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody); + expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedInsertBody); expect(spyFetch).toHaveBeenCalled(); }); it('should call axios.post and vnPaginateRef.fetch when newNote is valid', async () => { - await setTestParams( "Test Note", 1, true ); - + generateWrapper({ selectType: true }, 'Test Note', 1); + createSpyFetch(); generateExpectedBody(); - + await vm.insert(); - expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedBody); + expect(postMock).toHaveBeenCalledWith(vm.$props.url, expectedInsertBody); expect(spyFetch).toHaveBeenCalled(); }); }); -}); \ No newline at end of file + + describe('update', () => { + it('should call axios.patch with saveUrl when saveUrl is set and justInput is true', async () => { + generateWrapper({ + url: '/business', + justInput: true, + saveUrl: '/saveUrlTest', + }); + generateExpectedBody(); + + await vm.update(); + + expect(patchMock).toHaveBeenCalledWith(vm.$props.saveUrl, expectedUpdateBody); + }); + + it('should call axios.patch with url when saveUrl is not set and justInput is true', async () => { + generateWrapper({ + url: '/business', + body: { workerFk: 1110 }, + justInput: true, + }); + generateExpectedBody(); + + await vm.update(); + + expect(patchMock).toHaveBeenCalledWith( + `${vm.$props.url}/${vm.$props.body.workerFk}`, + expectedUpdateBody, + ); + }); + }); +}); diff --git a/src/components/ui/CardDescriptor.vue b/src/components/ui/CardDescriptor.vue index 01027e230..b8db68bee 100644 --- a/src/components/ui/CardDescriptor.vue +++ b/src/components/ui/CardDescriptor.vue @@ -6,6 +6,7 @@ import { useArrayData } from 'composables/useArrayData'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useState } from 'src/composables/useState'; import { useRoute } from 'vue-router'; +import { useClipboard } from 'src/composables/useClipboard'; import VnMoreOptions from './VnMoreOptions.vue'; const $props = defineProps({ @@ -29,10 +30,6 @@ const $props = defineProps({ type: String, default: null, }, - module: { - type: String, - default: null, - }, summary: { type: Object, default: null, @@ -46,6 +43,7 @@ const $props = defineProps({ const state = useState(); const route = useRoute(); const { t } = useI18n(); +const { copyText } = useClipboard(); const { viewSummary } = useSummaryDialog(); let arrayData; let store; @@ -57,12 +55,13 @@ defineExpose({ getData }); onBeforeMount(async () => { arrayData = useArrayData($props.dataKey, { url: $props.url, - filter: $props.filter, + userFilter: $props.filter, skip: 0, + oneRecord: true, }); store = arrayData.store; entity = computed(() => { - const data = (Array.isArray(store.data) ? store.data[0] : store.data) ?? {}; + const data = store.data ?? {}; if (data) emit('onFetch', data); return data; }); @@ -84,7 +83,7 @@ async function getData() { try { const { data } = await arrayData.fetch({ append: false, updateRouter: false }); state.set($props.dataKey, data); - emit('onFetch', Array.isArray(data) ? data[0] : data); + emit('onFetch', data); } finally { isLoading.value = false; } @@ -102,6 +101,14 @@ function getValueFromPath(path) { return current; } +function copyIdText(id) { + copyText(id, { + component: { + copyValue: id, + }, + }); +} + const emit = defineEmits(['onFetch']); const iconModule = computed(() => route.matched[1].meta.icon); @@ -147,7 +154,9 @@ const toModule = computed(() => {{ t('components.smartCard.openSummary') }} </QTooltip> </QBtn> - <RouterLink :to="{ name: `${module}Summary`, params: { id: entity.id } }"> + <RouterLink + :to="{ name: `${dataKey}Summary`, params: { id: entity.id } }" + > <QBtn class="link" color="white" @@ -186,6 +195,19 @@ const toModule = computed(() => <QItem> <QItemLabel class="subtitle"> #{{ getValueFromPath(subtitle) ?? entity.id }} + <QBtn + round + flat + dense + size="sm" + icon="content_copy" + color="primary" + @click.stop="copyIdText(entity.id)" + > + <QTooltip> + {{ t('globals.copyId') }} + </QTooltip> + </QBtn> </QItemLabel> <QBtn @@ -308,3 +330,11 @@ const toModule = computed(() => } } </style> +<i18n> + en: + globals: + copyId: Copy ID + es: + globals: + copyId: Copiar ID +</i18n> diff --git a/src/components/ui/CardSummary.vue b/src/components/ui/CardSummary.vue index c815b8e16..6a61994c1 100644 --- a/src/components/ui/CardSummary.vue +++ b/src/components/ui/CardSummary.vue @@ -40,9 +40,10 @@ const arrayData = useArrayData(props.dataKey, { filter: props.filter, userFilter: props.userFilter, skip: 0, + oneRecord: true, }); const { store } = arrayData; -const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data)); +const entity = computed(() => store.data); const isLoading = ref(false); defineExpose({ @@ -61,7 +62,7 @@ async function fetch() { store.filter = props.filter ?? {}; isLoading.value = true; const { data } = await arrayData.fetch({ append: false, updateRouter: false }); - emit('onFetch', Array.isArray(data) ? data[0] : data); + emit('onFetch', data); isLoading.value = false; } </script> @@ -208,4 +209,13 @@ async function fetch() { .summaryHeader { color: $white; } + +.cardSummary :deep(.q-card__section[content]) { + display: flex; + flex-wrap: wrap; + padding: 0; + > * { + flex: 1; + } +} </style> diff --git a/src/components/ui/SkeletonDescriptor.vue b/src/components/ui/SkeletonDescriptor.vue index 9679751f5..f9188221a 100644 --- a/src/components/ui/SkeletonDescriptor.vue +++ b/src/components/ui/SkeletonDescriptor.vue @@ -1,53 +1,32 @@ +<script setup> +defineProps({ + hasImage: { + type: Boolean, + default: false, + }, +}); +</script> <template> - <div id="descriptor-skeleton"> + <div id="descriptor-skeleton" class="bg-vn-page"> <div class="row justify-between q-pa-sm"> - <QSkeleton square size="40px" /> - <QSkeleton square size="40px" /> - <QSkeleton square height="40px" width="20px" /> + <QSkeleton square size="30px" v-for="i in 3" :key="i" /> </div> - <div class="col justify-between q-pa-sm q-gutter-y-xs"> - <QSkeleton square height="40px" width="150px" /> - <QSkeleton square height="30px" width="70px" /> + <div class="q-pa-xs" v-if="hasImage"> + <QSkeleton square height="200px" width="100%" /> </div> - <div class="col q-pl-sm q-pa-sm q-mb-md"> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> - </div> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> - </div> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> - </div> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> - </div> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> - </div> - <div class="row justify-between"> - <QSkeleton type="text" square height="30px" width="20%" /> - <QSkeleton type="text" square height="30px" width="60%" /> + <div class="col justify-between q-pa-md q-gutter-y-xs"> + <QSkeleton square height="25px" width="150px" /> + <QSkeleton square height="15px" width="70px" /> + </div> + <div class="q-pl-sm q-pa-sm q-mb-md"> + <div class="row q-gutter-x-sm q-pa-none q-ma-none" v-for="i in 5" :key="i"> + <QSkeleton type="text" square height="20px" width="30%" /> + <QSkeleton type="text" square height="20px" width="60%" /> </div> </div> - <QCardActions> - <QSkeleton size="40px" /> - <QSkeleton size="40px" /> - <QSkeleton size="40px" /> - <QSkeleton size="40px" /> - <QSkeleton size="40px" /> + <QCardActions class="q-gutter-x-sm justify-between"> + <QSkeleton size="40px" v-for="i in 5" :key="i" /> </QCardActions> </div> </template> - -<style lang="scss" scoped> -#descriptor-skeleton .q-card__actions { - justify-content: space-between; -} -</style> diff --git a/src/components/ui/VnConfirm.vue b/src/components/ui/VnConfirm.vue index a02b56bdb..c6f539879 100644 --- a/src/components/ui/VnConfirm.vue +++ b/src/components/ui/VnConfirm.vue @@ -82,7 +82,7 @@ function cancel() { @click="cancel()" /> </QCardSection> - <QCardSection class="q-pb-none"> + <QCardSection class="q-pb-none" data-cy="VnConfirm_message"> <span v-if="message !== false" v-html="message" /> </QCardSection> <QCardSection class="row items-center q-pt-none"> @@ -95,6 +95,7 @@ function cancel() { :disable="isLoading" flat @click="cancel()" + data-cy="VnConfirm_cancel" /> <QBtn :label="t('globals.confirm')" diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index 93f069cc6..d6b525dc8 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -114,7 +114,7 @@ async function clearFilters() { arrayData.resetPagination(); // Filtrar los params no removibles const removableFilters = Object.keys(userParams.value).filter((param) => - $props.unremovableParams.includes(param) + $props.unremovableParams.includes(param), ); const newParams = {}; // Conservar solo los params que no son removibles @@ -162,13 +162,13 @@ const formatTags = (tags) => { const tags = computed(() => { const filteredTags = tagsList.value.filter( - (tag) => !($props.customTags || []).includes(tag.label) + (tag) => !($props.customTags || []).includes(tag.label), ); return formatTags(filteredTags); }); const customTags = computed(() => - tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label)) + tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label)), ); async function remove(key) { @@ -188,10 +188,13 @@ function formatValue(value) { const getLocale = (label) => { const param = label.split('.').at(-1); const globalLocale = `globals.params.${param}`; + const moduleName = route.meta.moduleName; + const moduleLocale = `${moduleName.toLowerCase()}.${param}`; if (te(globalLocale)) return t(globalLocale); - else if (te(t(`params.${param}`))); + else if (te(moduleLocale)) return t(moduleLocale); else { - const camelCaseModuleName = route.meta.moduleName.charAt(0).toLowerCase() + route.meta.moduleName.slice(1); + const camelCaseModuleName = + moduleName.charAt(0).toLowerCase() + moduleName.slice(1); return t(`${camelCaseModuleName}.params.${param}`); } }; @@ -290,6 +293,9 @@ const getLocale = (label) => { /> </template> <style scoped lang="scss"> +.q-field__label.no-pointer-events.absolute.ellipsis { + margin-left: 6px !important; +} .list { width: 256px; } diff --git a/src/components/ui/VnMoreOptions.vue b/src/components/ui/VnMoreOptions.vue index 39e84be2b..8a1c7a0f2 100644 --- a/src/components/ui/VnMoreOptions.vue +++ b/src/components/ui/VnMoreOptions.vue @@ -11,7 +11,7 @@ <QTooltip> {{ $t('components.cardDescriptor.moreOptions') }} </QTooltip> - <QMenu ref="menuRef"> + <QMenu ref="menuRef" data-cy="descriptor-more-opts-menu"> <QList> <slot name="menu" :menu-ref="$refs.menuRef" /> </QList> diff --git a/src/components/ui/VnNotes.vue b/src/components/ui/VnNotes.vue index 1690a94ba..ec6289a67 100644 --- a/src/components/ui/VnNotes.vue +++ b/src/components/ui/VnNotes.vue @@ -1,6 +1,6 @@ <script setup> import axios from 'axios'; -import { ref, reactive } from 'vue'; +import { ref, reactive, useAttrs, computed } from 'vue'; import { onBeforeRouteLeave } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; @@ -16,12 +16,27 @@ import VnSelect from 'components/common/VnSelect.vue'; import FetchData from 'components/FetchData.vue'; import VnInput from 'components/common/VnInput.vue'; +const emit = defineEmits(['onFetch']); + +const originalAttrs = useAttrs(); + +const $attrs = computed(() => { + const { style, ...rest } = originalAttrs; + return rest; +}); + +const isRequired = computed(() => { + return Object.keys($attrs).includes('required') +}); + const $props = defineProps({ url: { type: String, default: null }, + saveUrl: {type: String, default: null}, filter: { type: Object, default: () => {} }, body: { type: Object, default: () => {} }, addNote: { type: Boolean, default: false }, selectType: { type: Boolean, default: false }, + justInput: { type: Boolean, default: false }, }); const { t } = useI18n(); @@ -29,6 +44,13 @@ const quasar = useQuasar(); const newNote = reactive({ text: null, observationTypeFk: null }); const observationTypes = ref([]); const vnPaginateRef = ref(); +let originalText; + +function handleClick(e) { + if (e.shiftKey && e.key === 'Enter') return; + if ($props.justInput) confirmAndUpdate(); + else insert(); +} async function insert() { if (!newNote.text || ($props.selectType && !newNote.observationTypeFk)) return; @@ -41,8 +63,36 @@ async function insert() { await axios.post($props.url, newBody); await vnPaginateRef.value.fetch(); } + +function confirmAndUpdate() { + if(!newNote.text && originalText) + quasar + .dialog({ + component: VnConfirm, + componentProps: { + title: t('New note is empty'), + message: t('Are you sure remove this note?'), + }, + }) + .onOk(update) + .onCancel(() => { + newNote.text = originalText; + }); + else update(); +} + +async function update() { + originalText = newNote.text; + const body = $props.body; + const newBody = { + ...body, + ...{ notes: newNote.text }, + }; + await axios.patch(`${$props.saveUrl ?? `${$props.url}/${$props.body.workerFk}`}`, newBody); +} + onBeforeRouteLeave((to, from, next) => { - if (newNote.text) + if ((newNote.text && !$props.justInput) || (newNote.text !== originalText) && $props.justInput) quasar.dialog({ component: VnConfirm, componentProps: { @@ -53,6 +103,13 @@ onBeforeRouteLeave((to, from, next) => { }); else next(); }); + +function fetchData([ data ]) { + newNote.text = data?.notes; + originalText = data?.notes; + emit('onFetch', data); +} + </script> <template> <FetchData @@ -62,8 +119,19 @@ onBeforeRouteLeave((to, from, next) => { auto-load @on-fetch="(data) => (observationTypes = data)" /> - <QCard class="q-pa-xs q-mb-lg full-width" v-if="$props.addNote"> - <QCardSection horizontal> + <FetchData + v-if="justInput" + :url="url" + :filter="filter" + @on-fetch="fetchData" + auto-load + /> + <QCard + class="q-pa-xs q-mb-lg full-width" + :class="{ 'just-input': $props.justInput }" + v-if="$props.addNote || $props.justInput" + > + <QCardSection horizontal v-if="!$props.justInput"> {{ t('New note') }} </QCardSection> <QCardSection class="q-px-xs q-my-none q-py-none"> @@ -75,19 +143,19 @@ onBeforeRouteLeave((to, from, next) => { v-model="newNote.observationTypeFk" option-label="description" style="flex: 0.15" - :required="true" + :required="isRequired" @keyup.enter.stop="insert" /> <VnInput v-model.trim="newNote.text" type="textarea" - :label="t('Add note here...')" + :label="$props.justInput && newNote.text ? '' : t('Add note here...')" filled size="lg" autogrow - @keyup.enter.stop="insert" + @keyup.enter.stop="handleClick" + :required="isRequired" clearable - :required="true" > <template #append> <QBtn @@ -95,7 +163,7 @@ onBeforeRouteLeave((to, from, next) => { icon="save" color="primary" flat - @click="insert" + @click="handleClick" class="q-mb-xs" dense data-cy="saveNote" @@ -106,6 +174,7 @@ onBeforeRouteLeave((to, from, next) => { </QCardSection> </QCard> <VnPaginate + v-if="!$props.justInput" :data-key="$props.url" :url="$props.url" order="created DESC" @@ -198,6 +267,11 @@ onBeforeRouteLeave((to, from, next) => { } } } +.just-input { + padding-right: 18px; + margin-bottom: 2px; + box-shadow: none; +} </style> <i18n> es: @@ -205,4 +279,6 @@ onBeforeRouteLeave((to, from, next) => { New note: Nueva nota Save (Enter): Guardar (Intro) Observation type: Tipo de observación + New note is empty: La nueva nota esta vacia + Are you sure remove this note?: Estas seguro de quitar esta nota? </i18n> diff --git a/src/components/ui/VnStockValueDisplay.vue b/src/components/ui/VnStockValueDisplay.vue new file mode 100644 index 000000000..d8f43323b --- /dev/null +++ b/src/components/ui/VnStockValueDisplay.vue @@ -0,0 +1,41 @@ +<script setup> +import { toPercentage } from 'filters/index'; + +import { computed } from 'vue'; + +const props = defineProps({ + value: { + type: Number, + required: true, + }, +}); + +const valueClass = computed(() => + props.value === 0 ? 'neutral' : props.value > 0 ? 'positive' : 'negative', +); +const iconName = computed(() => + props.value === 0 ? 'equal' : props.value > 0 ? 'arrow_upward' : 'arrow_downward', +); +const formattedValue = computed(() => props.value); +</script> +<template> + <span :class="valueClass"> + <QIcon :name="iconName" size="sm" class="value-icon" /> + {{ toPercentage(formattedValue) }} + </span> +</template> + +<style lang="scss" scoped> +.positive { + color: $secondary; +} +.negative { + color: $negative; +} +.neutral { + color: $primary; +} +.value-icon { + margin-right: 4px; +} +</style> diff --git a/src/components/ui/VnSubToolbar.vue b/src/components/ui/VnSubToolbar.vue index 5ded4be00..8d4126d1d 100644 --- a/src/components/ui/VnSubToolbar.vue +++ b/src/components/ui/VnSubToolbar.vue @@ -19,23 +19,26 @@ onMounted(() => { const observer = new MutationObserver( () => (hasContent.value = - actions.value?.childNodes?.length + data.value?.childNodes?.length) + actions.value?.childNodes?.length + data.value?.childNodes?.length), ); if (actions.value) observer.observe(actions.value, opts); if (data.value) observer.observe(data.value, opts); }); -onBeforeUnmount(() => stateStore.toggleSubToolbar()); +const actionsChildCount = () => !!actions.value?.childNodes?.length; + +onBeforeUnmount(() => stateStore.toggleSubToolbar() && hasSubToolbar); </script> <template> <QToolbar id="subToolbar" - class="justify-end sticky" v-show="hasContent || $slots['st-actions'] || $slots['st-data']" + class="justify-end sticky" > <slot name="st-data"> - <div id="st-data"></div> + <div id="st-data" :class="{ 'full-width': !actionsChildCount() }"> + </div> </slot> <QSpace /> <slot name="st-actions"> diff --git a/src/components/ui/__tests__/CardSummary.spec.js b/src/components/ui/__tests__/CardSummary.spec.js index 411ebf9bb..2f7f90882 100644 --- a/src/components/ui/__tests__/CardSummary.spec.js +++ b/src/components/ui/__tests__/CardSummary.spec.js @@ -51,16 +51,6 @@ describe('CardSummary', () => { expect(vm.store.filter).toEqual('cardFilter'); }); - it('should compute entity correctly from store data', () => { - vm.store.data = [{ id: 1, name: 'Entity 1' }]; - expect(vm.entity).toEqual({ id: 1, name: 'Entity 1' }); - }); - - it('should handle empty data gracefully', () => { - vm.store.data = []; - expect(vm.entity).toBeUndefined(); - }); - it('should respond to prop changes and refetch data', async () => { const newUrl = 'CardSummary/35'; const newKey = 'cardSummaryKey/35'; @@ -72,7 +62,7 @@ describe('CardSummary', () => { expect(vm.store.filter).toEqual({ key: newKey }); }); - it('should return true if route path ends with /summary' , () => { + it('should return true if route path ends with /summary', () => { expect(vm.isSummary).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/src/composables/__tests__/useArrayData.spec.js b/src/composables/__tests__/useArrayData.spec.js index d4c5d0949..a610ba9eb 100644 --- a/src/composables/__tests__/useArrayData.spec.js +++ b/src/composables/__tests__/useArrayData.spec.js @@ -16,7 +16,7 @@ describe('useArrayData', () => { vi.clearAllMocks(); }); - it('should fetch and repalce url with new params', async () => { + it('should fetch and replace url with new params', async () => { vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] }); const arrayData = useArrayData('ArrayData', { url: 'mockUrl' }); @@ -33,11 +33,11 @@ describe('useArrayData', () => { }); expect(routerReplace.path).toEqual('mockSection/list'); expect(JSON.parse(routerReplace.query.params)).toEqual( - expect.objectContaining(params) + expect.objectContaining(params), ); }); - it('Should get data and send new URL without keeping parameters, if there is only one record', async () => { + it('should get data and send new URL without keeping parameters, if there is only one record', async () => { vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }] }); const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} }); @@ -56,7 +56,7 @@ describe('useArrayData', () => { expect(routerPush.query).toBeUndefined(); }); - it('Should get data and send new URL keeping parameters, if you have more than one record', async () => { + it('should get data and send new URL keeping parameters, if you have more than one record', async () => { vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }] }); vi.spyOn(vueRouter, 'useRoute').mockReturnValue({ @@ -95,4 +95,25 @@ describe('useArrayData', () => { expect(routerPush.path).toEqual('mockName/'); expect(routerPush.query.params).toBeDefined(); }); + + it('should return one record', async () => { + vi.spyOn(axios, 'get').mockReturnValueOnce({ + data: [ + { id: 1, name: 'Entity 1' }, + { id: 2, name: 'Entity 2' }, + ], + }); + const arrayData = useArrayData('ArrayData', { url: 'mockUrl', oneRecord: true }); + await arrayData.fetch({}); + + expect(arrayData.store.data).toEqual({ id: 1, name: 'Entity 1' }); + }); + + it('should handle empty data gracefully if has to return one record', async () => { + vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] }); + const arrayData = useArrayData('ArrayData', { url: 'mockUrl', oneRecord: true }); + await arrayData.fetch({}); + + expect(arrayData.store.data).toBeUndefined(); + }); }); diff --git a/src/composables/checkEntryLock.js b/src/composables/checkEntryLock.js new file mode 100644 index 000000000..f964dea27 --- /dev/null +++ b/src/composables/checkEntryLock.js @@ -0,0 +1,65 @@ +import { useQuasar } from 'quasar'; +import { useI18n } from 'vue-i18n'; +import { useRouter } from 'vue-router'; +import axios from 'axios'; +import VnConfirm from 'components/ui/VnConfirm.vue'; + +export async function checkEntryLock(entryFk, userFk) { + const { t } = useI18n(); + const quasar = useQuasar(); + const { push } = useRouter(); + const { data } = await axios.get(`Entries/${entryFk}`, { + params: { + filter: JSON.stringify({ + fields: ['id', 'locked', 'lockerUserFk'], + include: { relation: 'user', scope: { fields: ['id', 'nickname'] } }, + }), + }, + }); + const entryConfig = await axios.get('EntryConfigs/findOne'); + + if (data?.lockerUserFk && data?.locked) { + const now = new Date(Date.vnNow()).getTime(); + const lockedTime = new Date(data.locked).getTime(); + const timeDiff = (now - lockedTime) / 1000; + const isMaxTimeLockExceeded = entryConfig.data.maxLockTime > timeDiff; + + if (data?.lockerUserFk !== userFk && isMaxTimeLockExceeded) { + quasar + .dialog({ + component: VnConfirm, + componentProps: { + 'data-cy': 'entry-lock-confirm', + title: t('entry.lock.title'), + message: t('entry.lock.message', { + userName: data?.user?.nickname, + time: timeDiff / 60, + }), + }, + }) + .onOk( + async () => + await axios.patch(`Entries/${entryFk}`, { + locked: Date.vnNow(), + lockerUserFk: userFk, + }), + ) + .onCancel(() => { + push({ path: `summary` }); + }); + } + } else { + await axios + .patch(`Entries/${entryFk}`, { + locked: Date.vnNow(), + lockerUserFk: userFk, + }) + .then( + quasar.notify({ + message: t('entry.lock.success'), + color: 'positive', + group: false, + }), + ); + } +} diff --git a/src/composables/getColAlign.js b/src/composables/getColAlign.js new file mode 100644 index 000000000..a930fd7d8 --- /dev/null +++ b/src/composables/getColAlign.js @@ -0,0 +1,22 @@ +export function getColAlign(col) { + let align; + switch (col.component) { + case 'time': + case 'date': + case 'select': + align = 'left'; + break; + case 'number': + align = 'right'; + break; + case 'checkbox': + align = 'center'; + break; + default: + align = col?.align; + } + + if (/^is[A-Z]/.test(col.name) || /^has[A-Z]/.test(col.name)) align = 'center'; + + return 'text-' + (align ?? 'center'); +} diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js index bd3cecf08..fcc61972a 100644 --- a/src/composables/useArrayData.js +++ b/src/composables/useArrayData.js @@ -57,6 +57,7 @@ export function useArrayData(key, userOptions) { 'navigate', 'mapKey', 'keepData', + 'oneRecord', ]; if (typeof userOptions === 'object') { for (const option in userOptions) { @@ -112,7 +113,11 @@ export function useArrayData(key, userOptions) { store.isLoading = false; canceller = null; - processData(response.data, { map: !!store.mapKey, append }); + processData(response.data, { + map: !!store.mapKey, + append, + oneRecord: store.oneRecord, + }); return response; } @@ -314,7 +319,11 @@ export function useArrayData(key, userOptions) { return { params, limit }; } - function processData(data, { map = true, append = true }) { + function processData(data, { map = true, append = true, oneRecord = false }) { + if (oneRecord) { + store.data = Array.isArray(data) ? data[0] : data; + return; + } if (!append) { store.data = []; store.map = new Map(); diff --git a/src/composables/useRole.js b/src/composables/useRole.js index 3ec65dd0a..ff54b409c 100644 --- a/src/composables/useRole.js +++ b/src/composables/useRole.js @@ -27,6 +27,15 @@ export function useRole() { return false; } + function likeAny(roles) { + const roleStore = state.getRoles(); + for (const role of roles) { + if (!roleStore.value.findIndex((rs) => rs.startsWith(role)) !== -1) + return true; + } + + return false; + } function isEmployee() { return hasAny(['employee']); } @@ -35,6 +44,7 @@ export function useRole() { isEmployee, fetch, hasAny, + likeAny, state, }; } diff --git a/src/css/app.scss b/src/css/app.scss index a002db9d2..994ae7ff1 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -21,7 +21,10 @@ body.body--light { .q-header .q-toolbar { color: var(--vn-text-color); } + + --vn-color-negative: $negative; } + body.body--dark { --vn-header-color: #5d5d5d; --vn-page-color: #222; @@ -37,6 +40,8 @@ body.body--dark { --vn-text-color-contrast: black; background-color: var(--vn-page-color); + + --vn-color-negative: $negative; } a { @@ -75,7 +80,6 @@ a { text-decoration: underline; } -// Removes chrome autofill background input:-webkit-autofill, select:-webkit-autofill { color: var(--vn-text-color); @@ -149,11 +153,6 @@ select:-webkit-autofill { cursor: pointer; } -.vn-table-separation-row { - height: 16px !important; - background-color: var(--vn-section-color) !important; -} - /* Estilo para el asterisco en campos requeridos */ .q-field.required .q-field__label:after { content: ' *'; @@ -212,6 +211,10 @@ select:-webkit-autofill { justify-content: center; } +.q-card__section[dense] { + padding: 0; +} + input[type='number'] { -moz-appearance: textfield; } @@ -226,10 +229,12 @@ input::-webkit-inner-spin-button { max-width: 100%; } -.q-table__container { - /* ===== Scrollbar CSS ===== / - / Firefox */ +.remove-bg { + filter: brightness(1.1); + mix-blend-mode: multiply; +} +.q-table__container { * { scrollbar-width: auto; scrollbar-color: var(--vn-label-color) transparent; @@ -270,8 +275,6 @@ input::-webkit-inner-spin-button { font-size: 11pt; } td { - font-size: 11pt; - border-top: 1px solid var(--vn-page-color); border-collapse: collapse; } } @@ -315,9 +318,6 @@ input::-webkit-inner-spin-button { max-width: fit-content; } -.row > .column:has(.q-checkbox) { - max-width: fit-content; -} .q-field__inner { .q-field__control { min-height: auto !important; diff --git a/src/css/quasar.variables.scss b/src/css/quasar.variables.scss index d6e992437..22c6d2b56 100644 --- a/src/css/quasar.variables.scss +++ b/src/css/quasar.variables.scss @@ -13,7 +13,7 @@ // Tip: Use the "Theme Builder" on Quasar's documentation website. // Tip: to add new colors https://quasar.dev/style/color-palette/#adding-your-own-colors $primary: #ec8916; -$secondary: $primary; +$secondary: #89be34; $positive: #c8e484; $negative: #fb5252; $info: #84d0e2; @@ -30,7 +30,9 @@ $color-spacer: #7979794d; $border-thin-light: 1px solid $color-spacer-light; $primary-light: #f5b351; $dark-shadow-color: black; -$layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d; +$layout-shadow-dark: + 0 0 10px 2px #00000033, + 0 0px 10px #0000003d; $spacing-md: 16px; $color-font-secondary: #777; $width-xs: 400px; diff --git a/src/filters/toDate.js b/src/filters/toDate.js index 8fe8f3836..002797af5 100644 --- a/src/filters/toDate.js +++ b/src/filters/toDate.js @@ -3,6 +3,8 @@ import { useI18n } from 'vue-i18n'; export default function (value, options = {}) { if (!value) return; + if (!isValidDate(value)) return null; + if (!options.dateStyle && !options.timeStyle) { options.day = '2-digit'; options.month = '2-digit'; @@ -10,7 +12,12 @@ export default function (value, options = {}) { } const { locale } = useI18n(); - const date = new Date(value); + const newDate = new Date(value); - return new Intl.DateTimeFormat(locale.value, options).format(date); + return new Intl.DateTimeFormat(locale.value, options).format(newDate); +} +// handle 0000-00-00 +function isValidDate(date) { + const parsedDate = new Date(date); + return parsedDate instanceof Date && !isNaN(parsedDate.getTime()); } diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 7d0f3e0b2..9a60e9da1 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -33,6 +33,7 @@ globals: reset: Reset close: Close cancel: Cancel + isSaveAndContinue: Save and continue clone: Clone confirm: Confirm assign: Assign @@ -156,6 +157,7 @@ globals: changeState: Change state raid: 'Raid {daysInForward} days' isVies: Vies + noData: No data available pageTitles: logIn: Login addressEdit: Update address @@ -168,6 +170,7 @@ globals: workCenters: Work centers modes: Modes zones: Zones + negative: Negative zonesList: List deliveryDays: Delivery days upcomingDeliveries: Upcoming deliveries @@ -175,6 +178,7 @@ globals: alias: Alias aliasUsers: Users subRoles: Subroles + myAccount: Mi cuenta inheritedRoles: Inherited Roles customers: Customers customerCreate: New customer @@ -333,10 +337,13 @@ globals: wasteRecalc: Waste recaclulate operator: Operator parking: Parking + vehicleList: Vehicles + vehicle: Vehicle unsavedPopup: title: Unsaved changes will be lost subtitle: Are you sure exit without saving? params: + description: Description clientFk: Client id salesPersonFk: Sales person warehouseFk: Warehouse @@ -359,7 +366,13 @@ globals: correctingFk: Rectificative daysOnward: Days onward countryFk: Country + countryCodeFk: Country companyFk: Company + model: Model + fuel: Fuel + active: Active + inactive: Inactive + deliveryPoint: Delivery point errors: statusUnauthorized: Access denied statusInternalServerError: An internal server error has ocurred @@ -398,6 +411,106 @@ cau: subtitle: By sending this ticket, all the data related to the error, the section, the user, etc., are already sent. inputLabel: Explain why this error should not appear askPrivileges: Ask for privileges +entry: + list: + newEntry: New entry + tableVisibleColumns: + isExcludedFromAvailable: Exclude from inventory + isOrdered: Ordered + isConfirmed: Ready to label + isReceived: Received + isRaid: Raid + landed: Date + supplierFk: Supplier + reference: Ref/Alb/Guide + invoiceNumber: Invoice + agencyModeId: Agency + isBooked: Booked + companyFk: Company + evaNotes: Notes + warehouseOutFk: Origin + warehouseInFk: Destiny + entryTypeDescription: Entry type + invoiceAmount: Import + travelFk: Travel + summary: + invoiceAmount: Amount + commission: Commission + currency: Currency + invoiceNumber: Invoice number + ordered: Ordered + booked: Booked + excludedFromAvailable: Inventory + travelReference: Reference + travelAgency: Agency + travelShipped: Shipped + travelDelivered: Delivered + travelLanded: Landed + travelReceived: Received + buys: Buys + stickers: Stickers + package: Package + packing: Pack. + grouping: Group. + buyingValue: Buying value + import: Import + pvp: PVP + basicData: + travel: Travel + currency: Currency + commission: Commission + observation: Observation + booked: Booked + excludedFromAvailable: Inventory + buys: + observations: Observations + packagingFk: Box + color: Color + printedStickers: Printed stickers + notes: + observationType: Observation type + latestBuys: + tableVisibleColumns: + image: Picture + itemFk: Item ID + weightByPiece: Weight/Piece + isActive: Active + family: Family + entryFk: Entry + freightValue: Freight value + comissionValue: Commission value + packageValue: Package value + isIgnored: Is ignored + price2: Grouping + price3: Packing + minPrice: Min + ektFk: Ekt + packingOut: Package out + landing: Landing + isExcludedFromAvailable: Exclude from inventory + isRaid: Raid + invoiceNumber: Invoice + reference: Ref/Alb/Guide + params: + isExcludedFromAvailable: Excluir del inventario + isOrdered: Pedida + isConfirmed: Lista para etiquetar + isReceived: Recibida + isRaid: Redada + landed: Fecha + supplierFk: Proveedor + invoiceNumber: Nº Factura + reference: Ref/Alb/Guía + agencyModeId: Agencia + isBooked: Asentado + companyFk: Empresa + travelFk: Envio + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeDescription: Tipo entrada + invoiceAmount: Importe + dated: Fecha ticket: params: ticketFk: Ticket ID @@ -627,6 +740,8 @@ wagon: name: Name supplier: + search: Search supplier + searchInfo: Search supplier by id or name list: payMethod: Pay method account: Account @@ -716,6 +831,8 @@ travel: CloneTravelAndEntries: Clone travel and his entries deleteTravel: Delete travel AddEntry: Add entry + availabled: Availabled + availabledHour: Availabled hour thermographs: Thermographs hb: HB basicData: diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 7ca9e4b4c..846c442ea 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -33,9 +33,11 @@ globals: reset: Restaurar close: Cerrar cancel: Cancelar + isSaveAndContinue: Guardar y continuar clone: Clonar confirm: Confirmar assign: Asignar + replace: Sustituir back: Volver yes: Si no: No @@ -48,6 +50,7 @@ globals: rowRemoved: Fila eliminada pleaseWait: Por favor espera... noPinnedModules: No has fijado ningún módulo + split: Split enterToConfirm: Pulsa Enter para confirmar summary: basicData: Datos básicos @@ -56,8 +59,8 @@ globals: today: Hoy yesterday: Ayer dateFormat: es-ES - microsip: Abrir en MicroSIP noSelectedRows: No tienes ninguna línea seleccionada + microsip: Abrir en MicroSIP downloadCSVSuccess: Descarga de CSV exitosa reference: Referencia agency: Agencia @@ -77,8 +80,10 @@ globals: requiredField: Campo obligatorio class: clase type: Tipo - reason: motivo + reason: Motivo + removeSelection: Eliminar selección noResults: Sin resultados + results: resultados system: Sistema notificationSent: Notificación enviada warehouse: Almacén @@ -156,6 +161,7 @@ globals: changeState: Cambiar estado raid: 'Redada {daysInForward} días' isVies: Vies + noData: Datos no disponibles pageTitles: logIn: Inicio de sesión addressEdit: Modificar consignatario @@ -167,6 +173,7 @@ globals: agency: Agencia workCenters: Centros de trabajo modes: Modos + negative: Tickets negativos zones: Zonas zonesList: Listado deliveryDays: Días de entrega @@ -287,9 +294,9 @@ globals: buyRequest: Peticiones de compra wasteBreakdown: Deglose de mermas itemCreate: Nuevo artículo - tax: 'IVA' - botanical: 'Botánico' - barcode: 'Código de barras' + tax: IVA + botanical: Botánico + barcode: Código de barras itemTypeCreate: Nueva familia family: Familia lastEntries: Últimas entradas @@ -333,10 +340,13 @@ globals: wasteRecalc: Recalcular mermas operator: Operario parking: Parking + vehicleList: Vehículos + vehicle: Vehículo unsavedPopup: title: Los cambios que no haya guardado se perderán subtitle: ¿Seguro que quiere salir sin guardar? params: + description: Descripción clientFk: Id cliente salesPersonFk: Comercial warehouseFk: Almacén @@ -350,13 +360,14 @@ globals: from: Desde to: Hasta supplierFk: Proveedor - supplierRef: Ref. proveedor + supplierRef: Nº factura serial: Serie amount: Importe awbCode: AWB daysOnward: Días adelante packing: ITP countryFk: País + countryCodeFk: País companyFk: Empresa errors: statusUnauthorized: Acceso denegado @@ -394,6 +405,87 @@ cau: subtitle: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc inputLabel: Explique el motivo por el que no deberia aparecer este fallo askPrivileges: Solicitar permisos +entry: + list: + newEntry: Nueva entrada + tableVisibleColumns: + isExcludedFromAvailable: Excluir del inventario + isOrdered: Pedida + isConfirmed: Lista para etiquetar + isReceived: Recibida + isRaid: Redada + landed: Fecha + supplierFk: Proveedor + invoiceNumber: Nº Factura + reference: Ref/Alb/Guía + agencyModeId: Agencia + isBooked: Asentado + companyFk: Empresa + travelFk: Envio + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeDescription: Tipo entrada + invoiceAmount: Importe + summary: + invoiceAmount: Importe + commission: Comisión + currency: Moneda + invoiceNumber: Núm. factura + ordered: Pedida + booked: Contabilizada + excludedFromAvailable: Inventario + travelReference: Referencia + travelAgency: Agencia + travelShipped: F. envio + travelWarehouseOut: Alm. salida + travelDelivered: Enviada + travelLanded: F. entrega + travelReceived: Recibida + buys: Compras + stickers: Etiquetas + package: Embalaje + packing: Pack. + grouping: Group. + buyingValue: Coste + import: Importe + pvp: PVP + basicData: + travel: Envío + currency: Moneda + observation: Observación + commission: Comisión + booked: Asentado + excludedFromAvailable: Inventario + buys: + observations: Observaciónes + packagingFk: Embalaje + color: Color + printedStickers: Etiquetas impresas + notes: + observationType: Tipo de observación + latestBuys: + tableVisibleColumns: + image: Foto + itemFk: Id Artículo + weightByPiece: Peso (gramos)/tallo + isActive: Activo + family: Familia + entryFk: Entrada + freightValue: Porte + comissionValue: Comisión + packageValue: Embalaje + isIgnored: Ignorado + price2: Grouping + price3: Packing + minPrice: Min + ektFk: Ekt + packingOut: Embalaje envíos + landing: Llegada + isExcludedFromAvailable: Excluir del inventario + isRaid: Redada + invoiceNumber: Nº Factura + reference: Ref/Alb/Guía ticket: params: ticketFk: ID de ticket @@ -407,6 +499,38 @@ ticket: freightItemName: Nombre packageItemName: Embalaje longName: Descripción + pageTitles: + tickets: Tickets + list: Listado + ticketCreate: Nuevo ticket + summary: Resumen + basicData: Datos básicos + boxing: Encajado + sms: Sms + notes: Notas + sale: Lineas del pedido + dms: Gestión documental + negative: Tickets negativos + 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: + nickname: Alias + state: Estado + shipped: Enviado + landed: Entregado + salesPerson: Comercial + total: Total card: customerId: ID cliente customerCard: Ficha del cliente @@ -453,15 +577,11 @@ ticket: consigneeStreet: Dirección create: address: Dirección -order: - field: - salesPersonFk: Comercial - form: - clientFk: Cliente - addressFk: Dirección - agencyModeFk: Agencia - list: - newOrder: Nuevo Pedido +invoiceOut: + card: + issued: Fecha emisión + customerCard: Ficha del cliente + ticketList: Listado de tickets summary: issued: Fecha dued: Fecha límite @@ -472,6 +592,71 @@ order: fee: Cuota tickets: Tickets totalWithVat: Importe + globalInvoices: + errors: + chooseValidClient: Selecciona un cliente válido + chooseValidCompany: Selecciona una empresa válida + chooseValidPrinter: Selecciona una impresora válida + chooseValidSerialType: Selecciona una tipo de serie válida + fillDates: La fecha de la factura y la fecha máxima deben estar completas + invoiceDateLessThanMaxDate: La fecha de la factura no puede ser menor que la fecha máxima + invoiceWithFutureDate: Existe una factura con una fecha futura + noTicketsToInvoice: No existen tickets para facturar + criticalInvoiceError: Error crítico en la facturación proceso detenido + invalidSerialTypeForAll: El tipo de serie debe ser global cuando se facturan todos los clientes + table: + addressId: Id dirección + streetAddress: Dirección fiscal + statusCard: + percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}' + pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs' + negativeBases: + clientId: Id cliente + base: Base + active: Activo + hasToInvoice: Facturar + verifiedData: Datos comprobados + comercial: Comercial + errors: + downloadCsvFailed: Error al descargar CSV +order: + field: + salesPersonFk: Comercial + form: + clientFk: Cliente + addressFk: Dirección + agencyModeFk: Agencia + list: + newOrder: Nuevo Pedido + summary: + basket: Cesta + notConfirmed: No confirmada + created: Creado + createdFrom: Creado desde + address: Dirección + total: Total + vat: IVA + state: Estado + alias: Alias + items: Artículos + orderTicketList: Tickets del pedido + amount: Monto + confirm: Confirmar + confirmLines: Confirmar lineas +shelving: + list: + parking: Parking + priority: Prioridad + newShelving: Nuevo Carro + summary: + recyclable: Reciclable +parking: + pickingOrder: Orden de recogida + row: Fila + column: Columna + searchBar: + info: Puedes buscar por código de parking + label: Buscar parking... department: chat: Chat bossDepartment: Jefe de departamento @@ -632,8 +817,8 @@ wagon: volumeNotEmpty: El volumen no puede estar vacío typeNotEmpty: El tipo no puede estar vacío maxTrays: Has alcanzado el número máximo de bandejas - minHeightBetweenTrays: 'La distancia mínima entre bandejas es ' - maxWagonHeight: 'La altura máxima del vagón es ' + minHeightBetweenTrays: La distancia mínima entre bandejas es + maxWagonHeight: La altura máxima del vagón es uncompleteTrays: Hay bandejas sin completar params: label: Etiqueta @@ -641,6 +826,8 @@ wagon: volume: Volumen name: Nombre supplier: + search: Buscar proveedor + searchInfo: Buscar proveedor por id o nombre list: payMethod: Método de pago account: Cuenta @@ -731,6 +918,8 @@ travel: deleteTravel: Eliminar envío AddEntry: Añadir entrada thermographs: Termógrafos + availabled: F. Disponible + availabledHour: Hora Disponible hb: HB basicData: daysInForward: Desplazamiento automatico (redada) @@ -779,7 +968,7 @@ components: cardDescriptor: mainList: Listado principal summary: Resumen - moreOptions: 'Más opciones' + moreOptions: Más opciones leftMenu: addToPinned: Añadir a fijados removeFromPinned: Eliminar de fijados diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 2a84e5aa1..3ad1c79bc 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -2,7 +2,7 @@ import Navbar from 'src/components/NavBar.vue'; </script> <template> - <QLayout view="hHh LpR fFf" v-shortcut> + <QLayout view="hHh LpR fFf"> <Navbar /> <RouterView></RouterView> <QFooter v-if="$q.platform.is.mobile"></QFooter> diff --git a/src/layouts/OutLayout.vue b/src/layouts/OutLayout.vue index 4ccc6bf9e..eba57c198 100644 --- a/src/layouts/OutLayout.vue +++ b/src/layouts/OutLayout.vue @@ -1,12 +1,12 @@ <script setup> import { Dark, Quasar } from 'quasar'; -import { computed } from 'vue'; +import { computed, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; import { localeEquivalence } from 'src/i18n/index'; import quasarLang from 'src/utils/quasarLang'; +import { langs } from 'src/boot/defaults/constants.js'; const { t, locale } = useI18n(); - const userLocale = computed({ get() { return locale.value; @@ -28,7 +28,6 @@ const darkMode = computed({ Dark.set(value); }, }); -const langs = ['en', 'es']; </script> <template> diff --git a/src/pages/Account/AccountAliasList.vue b/src/pages/Account/AccountAliasList.vue index f6016fb6c..19682286c 100644 --- a/src/pages/Account/AccountAliasList.vue +++ b/src/pages/Account/AccountAliasList.vue @@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n'; import { ref, computed } from 'vue'; import VnTable from 'components/VnTable/VnTable.vue'; import VnSection from 'src/components/common/VnSection.vue'; +import exprBuilder from './Alias/AliasExprBuilder'; const tableRef = ref(); const { t } = useI18n(); @@ -31,15 +32,6 @@ const columns = computed(() => [ create: true, }, ]); - -const exprBuilder = (param, value) => { - switch (param) { - case 'search': - return /^\d+$/.test(value) - ? { id: value } - : { alias: { like: `%${value}%` } }; - } -}; </script> <template> diff --git a/src/pages/Account/AccountExprBuilder.js b/src/pages/Account/AccountExprBuilder.js new file mode 100644 index 000000000..6497a9d30 --- /dev/null +++ b/src/pages/Account/AccountExprBuilder.js @@ -0,0 +1,18 @@ +export default (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { + or: [ + { name: { like: `%${value}%` } }, + { nickname: { like: `%${value}%` } }, + ], + }; + case 'name': + case 'nickname': + return { [param]: { like: `%${value}%` } }; + case 'roleFk': + return { [param]: value }; + } +}; diff --git a/src/pages/Account/AccountList.vue b/src/pages/Account/AccountList.vue index ea8daba0d..976af1d19 100644 --- a/src/pages/Account/AccountList.vue +++ b/src/pages/Account/AccountList.vue @@ -4,15 +4,16 @@ import { computed, ref } from 'vue'; import VnTable from 'components/VnTable/VnTable.vue'; import AccountSummary from './Card/AccountSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import exprBuilder from './AccountExprBuilder.js'; +import filter from './Card/AccountFilter.js'; import VnSection from 'src/components/common/VnSection.vue'; import FetchData from 'src/components/FetchData.vue'; import VnInputPassword from 'src/components/common/VnInputPassword.vue'; const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); -const filter = { - include: { relation: 'role', scope: { fields: ['id', 'name'] } }, -}; +const tableRef = ref(); + const dataKey = 'AccountList'; const roles = ref([]); const columns = computed(() => [ @@ -117,25 +118,6 @@ const columns = computed(() => [ ], }, ]); - -function exprBuilder(param, value) { - switch (param) { - case 'search': - return /^\d+$/.test(value) - ? { id: value } - : { - or: [ - { name: { like: `%${value}%` } }, - { nickname: { like: `%${value}%` } }, - ], - }; - case 'name': - case 'nickname': - return { [param]: { like: `%${value}%` } }; - case 'roleFk': - return { [param]: value }; - } -} </script> <template> <FetchData url="VnRoles" @on-fetch="(data) => (roles = data)" auto-load /> diff --git a/src/pages/Account/Alias/AliasExprBuilder.js b/src/pages/Account/Alias/AliasExprBuilder.js new file mode 100644 index 000000000..f7a5a104c --- /dev/null +++ b/src/pages/Account/Alias/AliasExprBuilder.js @@ -0,0 +1,8 @@ +export default (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { alias: { like: `%${value}%` } }; + } +}; diff --git a/src/pages/Account/Alias/Card/AliasCard.vue b/src/pages/Account/Alias/Card/AliasCard.vue index 3a814edc0..f37bd7d0f 100644 --- a/src/pages/Account/Alias/Card/AliasCard.vue +++ b/src/pages/Account/Alias/Card/AliasCard.vue @@ -1,21 +1,13 @@ <script setup> -import { useI18n } from 'vue-i18n'; import VnCardBeta from 'components/common/VnCardBeta.vue'; import AliasDescriptor from './AliasDescriptor.vue'; -const { t } = useI18n(); </script> <template> <VnCardBeta data-key="Alias" - base-url="MailAliases" + url="MailAliases" :descriptor="AliasDescriptor" search-data-key="AccountAliasList" - :searchbar-props="{ - url: 'MailAliases', - info: t('mailAlias.searchInfo'), - label: t('mailAlias.search'), - searchUrl: 'table', - }" /> </template> diff --git a/src/pages/Account/Alias/Card/AliasDescriptor.vue b/src/pages/Account/Alias/Card/AliasDescriptor.vue index 2e01fad01..671ef7fbc 100644 --- a/src/pages/Account/Alias/Card/AliasDescriptor.vue +++ b/src/pages/Account/Alias/Card/AliasDescriptor.vue @@ -7,7 +7,6 @@ import { useQuasar } from 'quasar'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; @@ -29,9 +28,6 @@ const entityId = computed(() => { return $props.id || route.params.id; }); -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.alias, entity.id)); - const removeAlias = () => { quasar .dialog({ @@ -55,11 +51,8 @@ const removeAlias = () => { <CardDescriptor ref="descriptor" :url="`MailAliases/${entityId}`" - module="Alias" - @on-fetch="setData" - data-key="aliasData" - :title="data.title" - :subtitle="data.subtitle" + data-key="Alias" + title="alias" > <template #menu> <QItem v-ripple clickable @click="removeAlias()"> diff --git a/src/pages/Account/Alias/Card/AliasSummary.vue b/src/pages/Account/Alias/Card/AliasSummary.vue index 1f76fe7c2..b4b9abd25 100644 --- a/src/pages/Account/Alias/Card/AliasSummary.vue +++ b/src/pages/Account/Alias/Card/AliasSummary.vue @@ -1,13 +1,11 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import { useArrayData } from 'src/composables/useArrayData'; - const route = useRoute(); const { t } = useI18n(); @@ -18,20 +16,15 @@ const $props = defineProps({ }, }); -const { store } = useArrayData('Alias'); -const alias = ref(store.data); const entityId = computed(() => $props.id || route.params.id); </script> <template> - <CardSummary - ref="summary" - :url="`MailAliases/${entityId}`" - @on-fetch="(data) => (alias = data)" - data-key="MailAliasesSummary" - > - <template #header> {{ alias.id }} - {{ alias.alias }} </template> - <template #body> + <CardSummary ref="summary" :url="`MailAliases/${entityId}`" data-key="Alias"> + <template #header="{ entity: alias }"> + {{ alias.id }} - {{ alias.alias }} + </template> + <template #body="{ entity: alias }"> <QCard class="vn-one"> <QCardSection class="q-pa-none"> <router-link diff --git a/src/pages/Account/Card/AccountBasicData.vue b/src/pages/Account/Card/AccountBasicData.vue index e6c9da6fe..393f9eb80 100644 --- a/src/pages/Account/Card/AccountBasicData.vue +++ b/src/pages/Account/Card/AccountBasicData.vue @@ -1,46 +1,20 @@ <script setup> -import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectEnum from 'src/components/common/VnSelectEnum.vue'; import FormModel from 'components/FormModel.vue'; import VnInput from 'src/components/common/VnInput.vue'; -import { ref, watch } from 'vue'; - -const route = useRoute(); -const { t } = useI18n(); -const formModelRef = ref(null); - -const accountFilter = { - where: { id: route.params.id }, - fields: ['id', 'email', 'nickname', 'name', 'accountStateFk', 'packages', 'pickup'], - include: [], -}; - -watch( - () => route.params.id, - () => formModelRef.value.reset() -); </script> <template> - <FormModel - ref="formModelRef" - url="VnUsers/preview" - :url-update="`VnUsers/${route.params.id}/update-user`" - :filter="accountFilter" - model="Accounts" - auto-load - @on-data-saved="formModelRef.fetch()" - > + <FormModel :url-update="`VnUsers/${$route.params.id}/update-user`" model="Account"> <template #form="{ data }"> <div class="q-gutter-y-sm"> - <VnInput v-model="data.name" :label="t('account.card.nickname')" /> - <VnInput v-model="data.nickname" :label="t('account.card.alias')" /> - <VnInput v-model="data.email" :label="t('globals.params.email')" /> + <VnInput v-model="data.name" :label="$t('account.card.nickname')" /> + <VnInput v-model="data.nickname" :label="$t('account.card.alias')" /> + <VnInput v-model="data.email" :label="$t('globals.params.email')" /> <VnSelect url="Languages" v-model="data.lang" - :label="t('account.card.lang')" + :label="$t('account.card.lang')" option-value="code" option-label="code" /> @@ -49,7 +23,7 @@ watch( table="user" column="twoFactor" v-model="data.twoFactor" - :label="t('account.card.twoFactor')" + :label="$t('account.card.twoFactor')" option-value="code" option-label="code" /> diff --git a/src/pages/Account/Card/AccountCard.vue b/src/pages/Account/Card/AccountCard.vue index 35ff7e732..a5037e301 100644 --- a/src/pages/Account/Card/AccountCard.vue +++ b/src/pages/Account/Card/AccountCard.vue @@ -1,8 +1,14 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import AccountDescriptor from './AccountDescriptor.vue'; +import filter from './AccountFilter.js'; </script> - <template> - <VnCardBeta data-key="AccountId" :descriptor="AccountDescriptor" /> + <VnCardBeta + url="VnUsers/preview" + :id-in-where="true" + data-key="Account" + :descriptor="AccountDescriptor" + :filter="filter" + /> </template> diff --git a/src/pages/Account/Card/AccountDescriptor.vue b/src/pages/Account/Card/AccountDescriptor.vue index 4e5328de6..49328fe87 100644 --- a/src/pages/Account/Card/AccountDescriptor.vue +++ b/src/pages/Account/Card/AccountDescriptor.vue @@ -1,36 +1,18 @@ <script setup> import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; import VnImg from 'src/components/ui/VnImg.vue'; +import filter from './AccountFilter.js'; import useHasAccount from 'src/composables/useHasAccount.js'; -const $props = defineProps({ - id: { - type: Number, - required: false, - default: null, - }, -}); +const $props = defineProps({ id: { type: Number, default: null } }); const route = useRoute(); -const { t } = useI18n(); -const entityId = computed(() => { - return $props.id || route.params.id; -}); -const data = ref(useCardDescription()); +const entityId = computed(() => $props.id || route.params.id); const hasAccount = ref(); -const setData = (entity) => (data.value = useCardDescription(entity.nickname, entity.id)); - -const filter = { - where: { id: entityId }, - fields: ['id', 'nickname', 'name', 'role'], - include: { relation: 'role', scope: { fields: ['id', 'name'] } }, -}; onMounted(async () => { hasAccount.value = await useHasAccount(entityId.value); @@ -41,12 +23,9 @@ onMounted(async () => { <CardDescriptor ref="descriptor" :url="`VnUsers/preview`" - :filter="filter" - module="Account" - @on-fetch="setData" - data-key="AccountId" - :title="data.title" - :subtitle="data.subtitle" + :filter="{ ...filter, where: { id: entityId } }" + data-key="Account" + title="nickname" > <template #menu> <AccountDescriptorMenu :entity-id="entityId" /> @@ -62,7 +41,7 @@ onMounted(async () => { <QIcon name="vn:claims" /> </div> <div class="text-grey-5" style="opacity: 0.4"> - {{ t('account.imageNotFound') }} + {{ $t('account.imageNotFound') }} </div> </div> </div> @@ -70,8 +49,8 @@ onMounted(async () => { </VnImg> </template> <template #body="{ entity }"> - <VnLv :label="t('account.card.nickname')" :value="entity.name" /> - <VnLv :label="t('account.card.role')" :value="entity.role.name" /> + <VnLv :label="$t('account.card.nickname')" :value="entity.name" /> + <VnLv :label="$t('account.card.role')" :value="entity.role?.name" /> </template> <template #actions="{ entity }"> <QCardActions class="q-gutter-x-md"> @@ -84,7 +63,7 @@ onMounted(async () => { size="sm" class="fill-icon" > - <QTooltip>{{ t('account.card.deactivated') }}</QTooltip> + <QTooltip>{{ $t('account.card.deactivated') }}</QTooltip> </QIcon> <QIcon color="primary" @@ -95,7 +74,7 @@ onMounted(async () => { size="sm" class="fill-icon" > - <QTooltip>{{ t('account.card.enabled') }}</QTooltip> + <QTooltip>{{ $t('account.card.enabled') }}</QTooltip> </QIcon> </QCardActions> </template> diff --git a/src/pages/Account/Card/AccountDescriptorMenu.vue b/src/pages/Account/Card/AccountDescriptorMenu.vue index 961323d3a..30584c61f 100644 --- a/src/pages/Account/Card/AccountDescriptorMenu.vue +++ b/src/pages/Account/Card/AccountDescriptorMenu.vue @@ -12,6 +12,7 @@ import VnInputPassword from 'src/components/common/VnInputPassword.vue'; import VnChangePassword from 'src/components/common/VnChangePassword.vue'; import { useQuasar } from 'quasar'; import { useRouter } from 'vue-router'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const $props = defineProps({ hasAccount: { @@ -29,7 +30,7 @@ const router = useRouter(); const state = useState(); const user = state.getUser(); const { notify } = useQuasar(); -const account = computed(() => useArrayData('AccountId').store.data[0]); +const account = computed(() => useArrayData('Account').store.data[0]); account.value.hasAccount = hasAccount.value; const entityId = computed(() => +route.params.id); const hasitManagementAccess = ref(); @@ -124,18 +125,14 @@ onMounted(() => { :promise="sync" > <template #customHTML> - {{ shouldSyncPassword }} - <QCheckbox - :label="t('account.card.actions.sync.checkbox')" + <VnCheckbox v-model="shouldSyncPassword" - class="full-width" + :label="t('account.card.actions.sync.checkbox')" + :info="t('account.card.actions.sync.tooltip')" clearable clear-icon="close" - > - <QIcon style="padding-left: 10px" color="primary" name="info" size="sm"> - <QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip> - </QIcon></QCheckbox - > + color="primary" + /> <VnInputPassword v-if="shouldSyncPassword" :label="t('login.password')" @@ -155,7 +152,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.subtitle'), - () => deleteAccount() + () => deleteAccount(), ) " > @@ -174,7 +171,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.enableAccount.title'), t('account.card.actions.enableAccount.subtitle'), - () => updateStatusAccount(true) + () => updateStatusAccount(true), ) " > @@ -188,7 +185,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.subtitle'), - () => updateStatusAccount(false) + () => updateStatusAccount(false), ) " > @@ -203,7 +200,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.activateUser.title'), t('account.card.actions.activateUser.title'), - () => updateStatusUser(true) + () => updateStatusUser(true), ) " > @@ -217,7 +214,7 @@ onMounted(() => { openConfirmationModal( t('account.card.actions.deactivateUser.title'), t('account.card.actions.deactivateUser.title'), - () => updateStatusUser(false) + () => updateStatusUser(false), ) " > diff --git a/src/pages/Account/Card/AccountFilter.js b/src/pages/Account/Card/AccountFilter.js new file mode 100644 index 000000000..017876564 --- /dev/null +++ b/src/pages/Account/Card/AccountFilter.js @@ -0,0 +1,3 @@ +export default { + include: { relation: 'role', scope: { fields: ['id', 'name'] } }, +}; diff --git a/src/pages/Account/Card/AccountMailAlias.vue b/src/pages/Account/Card/AccountMailAlias.vue index ef1707cf2..7a060cff1 100644 --- a/src/pages/Account/Card/AccountMailAlias.vue +++ b/src/pages/Account/Card/AccountMailAlias.vue @@ -86,7 +86,7 @@ watch( () => route.params.id, () => { getAccountData(); - } + }, ); onMounted(async () => await getAccountData(false)); @@ -130,7 +130,8 @@ onMounted(async () => await getAccountData(false)); openConfirmationModal( t('User will be removed from alias'), t('¿Seguro que quieres continuar?'), - () => deleteMailAlias(row, rows, rowIndex) + () => + deleteMailAlias(row, rows, rowIndex), ) " > @@ -157,7 +158,7 @@ onMounted(async () => await getAccountData(false)); icon="add" color="primary" @click="openCreateMailAliasForm()" - shortcut="+" + v-shortcut="'+'" > <QTooltip>{{ t('warehouses.add') }}</QTooltip> </QBtn> diff --git a/src/pages/Account/Card/AccountSummary.vue b/src/pages/Account/Card/AccountSummary.vue index ca17c7975..f7a16e8c3 100644 --- a/src/pages/Account/Card/AccountSummary.vue +++ b/src/pages/Account/Card/AccountSummary.vue @@ -1,58 +1,41 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; - import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; - -import { useArrayData } from 'src/composables/useArrayData'; +import filter from './AccountFilter.js'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; +const $props = defineProps({ id: { type: Number, default: 0 } }); + const route = useRoute(); -const { t } = useI18n(); - -const $props = defineProps({ - id: { - type: Number, - default: 0, - }, -}); -const { store } = useArrayData('Account'); -const account = ref(store.data); - const entityId = computed(() => $props.id || route.params.id); -const filter = { - where: { id: entityId }, - fields: ['id', 'nickname', 'name', 'role'], - include: { relation: 'role', scope: { fields: ['id', 'name'] } }, -}; </script> <template> <CardSummary - data-key="AccountId" + data-key="Account" + ref="AccountSummary" url="VnUsers/preview" :filter="filter" - @on-fetch="(data) => (account = data)" > - <template #header>{{ account.id }} - {{ account.nickname }}</template> - <template #menu=""> + <template #header="{ entity }">{{ entity.id }} - {{ entity.nickname }}</template> + <template #menu> <AccountDescriptorMenu :entity-id="entityId" /> </template> - <template #body> + <template #body="{ entity }"> <QCard class="vn-one"> <QCardSection class="q-pa-none"> <router-link :to="{ name: 'AccountBasicData', params: { id: entityId } }" class="header header-link" > - {{ t('globals.pageTitles.basicData') }} + {{ $t('globals.pageTitles.basicData') }} <QIcon name="open_in_new" /> </router-link> </QCardSection> - <VnLv :label="t('account.card.nickname')" :value="account.name" /> - <VnLv :label="t('account.card.role')" :value="account.role.name" /> + <VnLv :label="$t('account.card.nickname')" :value="entity.name" /> + <VnLv :label="$t('account.card.role')" :value="entity.role?.name" /> </QCard> </template> </CardSummary> diff --git a/src/pages/Account/Role/AccountRoles.vue b/src/pages/Account/Role/AccountRoles.vue index 3c3d6b243..02f5400c6 100644 --- a/src/pages/Account/Role/AccountRoles.vue +++ b/src/pages/Account/Role/AccountRoles.vue @@ -5,6 +5,7 @@ import VnTable from 'components/VnTable/VnTable.vue'; import { useRoute } from 'vue-router'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import RoleSummary from './Card/RoleSummary.vue'; +import exprBuilder from './RoleExprBuilder.js'; import VnSection from 'src/components/common/VnSection.vue'; const route = useRoute(); @@ -66,24 +67,7 @@ const columns = computed(() => [ ], }, ]); -const exprBuilder = (param, value) => { - switch (param) { - case 'search': - return /^\d+$/.test(value) - ? { id: value } - : { - or: [ - { name: { like: `%${value}%` } }, - { nickname: { like: `%${value}%` } }, - ], - }; - case 'name': - case 'description': - return { [param]: { like: `%${value}%` } }; - } -}; </script> - <template> <VnSection :data-key="dataKey" diff --git a/src/pages/Account/Role/Card/RoleBasicData.vue b/src/pages/Account/Role/Card/RoleBasicData.vue index 1de9ff387..de70b0fb6 100644 --- a/src/pages/Account/Role/Card/RoleBasicData.vue +++ b/src/pages/Account/Role/Card/RoleBasicData.vue @@ -1,24 +1,16 @@ <script setup> -import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; -const route = useRoute(); -const { t } = useI18n(); </script> <template> - <FormModel :url="`VnRoles/${route.params.id}`" model="VnRole" auto-load> + <FormModel model="Role" auto-load> <template #form="{ data }"> <VnRow> - <div class="col"> - <VnInput v-model="data.name" :label="t('globals.name')" /> - </div> + <VnInput v-model="data.name" :label="$t('globals.name')" /> </VnRow> <VnRow> - <div class="col"> - <VnInput v-model="data.description" :label="t('role.description')" /> - </div> + <VnInput v-model="data.description" :label="$t('role.description')" /> </VnRow> </template> </FormModel> diff --git a/src/pages/Account/Role/Card/RoleCard.vue b/src/pages/Account/Role/Card/RoleCard.vue index 7664deca8..ef5b9db04 100644 --- a/src/pages/Account/Role/Card/RoleCard.vue +++ b/src/pages/Account/Role/Card/RoleCard.vue @@ -3,5 +3,10 @@ import VnCardBeta from 'components/common/VnCardBeta.vue'; import RoleDescriptor from './RoleDescriptor.vue'; </script> <template> - <VnCardBeta data-key="Role" :descriptor="RoleDescriptor" /> + <VnCardBeta + url="VnRoles" + data-key="Role" + :id-in-where="true" + :descriptor="RoleDescriptor" + /> </template> diff --git a/src/pages/Account/Role/Card/RoleDescriptor.vue b/src/pages/Account/Role/Card/RoleDescriptor.vue index 0a555346d..517517af0 100644 --- a/src/pages/Account/Role/Card/RoleDescriptor.vue +++ b/src/pages/Account/Role/Card/RoleDescriptor.vue @@ -1,10 +1,9 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; const $props = defineProps({ @@ -26,11 +25,6 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.name, entity.id)); -const filter = { - where: { id: entityId }, -}; const removeRole = async () => { await axios.delete(`VnRoles/${entityId.value}`); notify(t('Role removed'), 'positive'); @@ -39,13 +33,9 @@ const removeRole = async () => { <template> <CardDescriptor - :url="`VnRoles/${entityId}`" - :filter="filter" - module="Role" - @on-fetch="setData" + url="VnRoles" + :filter="{ where: { id: entityId } }" data-key="Role" - :title="data.title" - :subtitle="data.subtitle" :summary="$props.summary" > <template #menu> diff --git a/src/pages/Account/Role/Card/RoleSummary.vue b/src/pages/Account/Role/Card/RoleSummary.vue index f0daa77fb..410f90b17 100644 --- a/src/pages/Account/Role/Card/RoleSummary.vue +++ b/src/pages/Account/Role/Card/RoleSummary.vue @@ -1,10 +1,9 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import { useArrayData } from 'src/composables/useArrayData'; const route = useRoute(); const { t } = useI18n(); @@ -16,24 +15,18 @@ const $props = defineProps({ }, }); -const { store } = useArrayData('Role'); -const role = ref(store.data); const entityId = computed(() => $props.id || route.params.id); -const filter = { - where: { id: entityId }, -}; </script> <template> <CardSummary ref="summary" - :url="`VnRoles/${entityId}`" - :filter="filter" - @on-fetch="(data) => (role = data)" + url="VnRoles" + :filter="{ where: { id: entityId } }" data-key="Role" > - <template #header> {{ role.id }} - {{ role.name }} </template> - <template #body> + <template #header="{ entity }"> {{ entity.id }} - {{ entity.name }} </template> + <template #body="{ entity }"> <QCard class="vn-one"> <QCardSection class="q-pa-none"> <a @@ -44,9 +37,9 @@ const filter = { <QIcon name="open_in_new" /> </a> </QCardSection> - <VnLv :label="t('role.id')" :value="role.id" /> - <VnLv :label="t('globals.name')" :value="role.name" /> - <VnLv :label="t('role.description')" :value="role.description" /> + <VnLv :label="t('role.id')" :value="entity.id" /> + <VnLv :label="t('globals.name')" :value="entity.name" /> + <VnLv :label="t('role.description')" :value="entity.description" /> </QCard> </template> </CardSummary> diff --git a/src/pages/Account/Role/Card/SubRoles.vue b/src/pages/Account/Role/Card/SubRoles.vue index 0077f12b0..99cf5e8f0 100644 --- a/src/pages/Account/Role/Card/SubRoles.vue +++ b/src/pages/Account/Role/Card/SubRoles.vue @@ -63,7 +63,7 @@ watch( store.url = urlPath.value; store.filter = filter.value; fetchSubRoles(); - } + }, ); const fetchSubRoles = () => paginateRef.value.fetch(); @@ -109,7 +109,7 @@ const redirectToRoleSummary = (id) => openConfirmationModal( t('El rol va a ser eliminado'), t('¿Seguro que quieres continuar?'), - () => deleteSubRole(row, rows, rowIndex) + () => deleteSubRole(row, rows, rowIndex), ) " > @@ -131,7 +131,7 @@ const redirectToRoleSummary = (id) => <QBtn fab icon="add" - shortcut="+" + v-shortcut="'+'" color="primary" @click="openCreateSubRoleForm()" > diff --git a/src/pages/Account/Role/RoleExprBuilder.js b/src/pages/Account/Role/RoleExprBuilder.js new file mode 100644 index 000000000..cc4fab399 --- /dev/null +++ b/src/pages/Account/Role/RoleExprBuilder.js @@ -0,0 +1,16 @@ +export default (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { + or: [ + { name: { like: `%${value}%` } }, + { nickname: { like: `%${value}%` } }, + ], + }; + case 'name': + case 'description': + return { [param]: { like: `%${value}%` } }; + } +}; diff --git a/src/pages/Claim/Card/ClaimBasicData.vue b/src/pages/Claim/Card/ClaimBasicData.vue index 63b0b7c0d..67034da1a 100644 --- a/src/pages/Claim/Card/ClaimBasicData.vue +++ b/src/pages/Claim/Card/ClaimBasicData.vue @@ -28,7 +28,6 @@ const workersOptions = ref([]); model="Claim" :url-update="`Claims/updateClaim/${route.params.id}`" auto-load - :reload="true" > <template #form="{ data, validate }"> <VnRow> diff --git a/src/pages/Claim/Card/ClaimCard.vue b/src/pages/Claim/Card/ClaimCard.vue index e1e000815..05f3b53a8 100644 --- a/src/pages/Claim/Card/ClaimCard.vue +++ b/src/pages/Claim/Card/ClaimCard.vue @@ -4,10 +4,11 @@ import ClaimDescriptor from './ClaimDescriptor.vue'; import filter from './ClaimFilter.js'; </script> <template> - <VnCardBeta - data-key="Claim" - base-url="Claims" - :descriptor="ClaimDescriptor" + <VnCardBeta + data-key="Claim" + url="Claims" + :descriptor="ClaimDescriptor" + search-data-key="ClaimList" :filter="filter" /> </template> diff --git a/src/pages/Claim/Card/ClaimDescriptor.vue b/src/pages/Claim/Card/ClaimDescriptor.vue index 02b63dd8e..4551c58fe 100644 --- a/src/pages/Claim/Card/ClaimDescriptor.vue +++ b/src/pages/Claim/Card/ClaimDescriptor.vue @@ -3,12 +3,10 @@ import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { toDateHourMinSec, toPercentage } from 'src/filters'; -import { useState } from 'src/composables/useState'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import { getUrl } from 'src/composables/getUrl'; import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; @@ -23,7 +21,6 @@ const $props = defineProps({ }); const route = useRoute(); -const state = useState(); const { t } = useI18n(); const salixUrl = ref(); const entityId = computed(() => { @@ -39,12 +36,7 @@ const STATE_COLOR = { function stateColor(code) { return STATE_COLOR[code]; } -const data = ref(useCardDescription()); -const setData = (entity) => { - if (!entity) return; - data.value = useCardDescription(entity?.client?.name, entity.id); - state.set('ClaimDescriptor', entity); -}; + onMounted(async () => { salixUrl.value = await getUrl(''); }); @@ -54,9 +46,7 @@ onMounted(async () => { <CardDescriptor :url="`Claims/${entityId}`" :filter="filter" - module="Claim" title="client.name" - @on-fetch="setData" data-key="Claim" > <template #menu="{ entity }"> @@ -95,7 +85,7 @@ onMounted(async () => { /> </template> </VnLv> - <VnLv :label="t('claim.zone')"> + <VnLv v-if="entity.ticket?.zone?.id" :label="t('claim.zone')"> <template #value> <span class="link"> {{ entity.ticket?.zone?.name }} @@ -107,11 +97,10 @@ onMounted(async () => { :label="t('claim.province')" :value="entity.ticket?.address?.province?.name" /> - <VnLv :label="t('claim.ticketId')"> + <VnLv v-if="entity.ticketFk" :label="t('claim.ticketId')"> <template #value> <span class="link"> {{ entity.ticketFk }} - <TicketDescriptorProxy :id="entity.ticketFk" /> </span> </template> diff --git a/src/pages/Claim/Card/ClaimLines.vue b/src/pages/Claim/Card/ClaimLines.vue index 33fadd020..dee03b95d 100644 --- a/src/pages/Claim/Card/ClaimLines.vue +++ b/src/pages/Claim/Card/ClaimLines.vue @@ -317,7 +317,13 @@ async function saveWhenHasChanges() { </div> <QPageSticky position="bottom-right" :offset="[25, 25]"> - <QBtn fab color="primary" shortcut="+" icon="add" @click="showImportDialog()" /> + <QBtn + fab + color="primary" + v-shortcut="'+'" + icon="add" + @click="showImportDialog()" + /> </QPageSticky> </template> diff --git a/src/pages/Claim/Card/ClaimNotes.vue b/src/pages/Claim/Card/ClaimNotes.vue index 134ee33ab..cc6e33779 100644 --- a/src/pages/Claim/Card/ClaimNotes.vue +++ b/src/pages/Claim/Card/ClaimNotes.vue @@ -1,5 +1,5 @@ <script setup> -import { computed } from 'vue'; +import { computed, useAttrs } from 'vue'; import { useRoute } from 'vue-router'; import { useState } from 'src/composables/useState'; import VnNotes from 'src/components/ui/VnNotes.vue'; @@ -7,6 +7,7 @@ import VnNotes from 'src/components/ui/VnNotes.vue'; const route = useRoute(); const state = useState(); const user = state.getUser(); +const $attrs = useAttrs(); const $props = defineProps({ id: { type: [Number, String], default: null }, diff --git a/src/pages/Claim/Card/ClaimPhoto.vue b/src/pages/Claim/Card/ClaimPhoto.vue index d4321d8eb..d4acc9bbe 100644 --- a/src/pages/Claim/Card/ClaimPhoto.vue +++ b/src/pages/Claim/Card/ClaimPhoto.vue @@ -61,7 +61,7 @@ watch( () => { claimDmsFilter.value.where.id = router.currentRoute.value.params.id; claimDmsRef.value.fetch(); - } + }, ); function openDialog(dmsId) { @@ -248,7 +248,7 @@ function onDrag() { <QBtn fab @click="inputFile.nativeEl.click()" - shortcut="+" + v-shortcut="'+'" icon="add" color="primary" > diff --git a/src/pages/Claim/ClaimList.vue b/src/pages/Claim/ClaimList.vue index 63fd035da..41d0c5598 100644 --- a/src/pages/Claim/ClaimList.vue +++ b/src/pages/Claim/ClaimList.vue @@ -132,7 +132,7 @@ const STATE_COLOR = { prefix="claim" :array-data-props="{ url: 'Claims/filter', - order: ['cs.priority ASC', 'created ASC'], + order: 'cs.priority ASC, created ASC', }" > <template #advanced-menu> diff --git a/src/pages/Customer/Card/CustomerAddress.vue b/src/pages/Customer/Card/CustomerAddress.vue index 1b0d1dde1..f1799d0cc 100644 --- a/src/pages/Customer/Card/CustomerAddress.vue +++ b/src/pages/Customer/Card/CustomerAddress.vue @@ -61,7 +61,7 @@ watch( (newValue) => { if (!newValue) return; getClientData(newValue); - } + }, ); const getClientData = async (id) => { @@ -137,7 +137,7 @@ const toCustomerAddressEdit = (addressId) => { <QIcon :style="{ 'font-variation-settings': `'FILL' ${isDefaultAddress( - item + item, )}`, }" color="primary" @@ -150,7 +150,7 @@ const toCustomerAddressEdit = (addressId) => { t( isDefaultAddress(item) ? 'Default address' - : 'Set as default' + : 'Set as default', ) }} </QTooltip> @@ -216,7 +216,7 @@ const toCustomerAddressEdit = (addressId) => { color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('New consignee') }} diff --git a/src/pages/Customer/Card/CustomerBalance.vue b/src/pages/Customer/Card/CustomerBalance.vue index 04ef5f882..11db92eab 100644 --- a/src/pages/Customer/Card/CustomerBalance.vue +++ b/src/pages/Customer/Card/CustomerBalance.vue @@ -158,7 +158,7 @@ const columns = computed(() => [ openConfirmationModal( t('Send compensation'), t('Do you want to report compensation to the client by mail?'), - () => sendEmail(`Receipts/${id}/balance-compensation-email`) + () => sendEmail(`Receipts/${id}/balance-compensation-email`), ), }, ], @@ -291,7 +291,7 @@ const showBalancePdf = ({ id }) => { color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('New payment') }} diff --git a/src/pages/Customer/Card/CustomerBasicData.vue b/src/pages/Customer/Card/CustomerBasicData.vue index e9a349e0b..36ec4763e 100644 --- a/src/pages/Customer/Card/CustomerBasicData.vue +++ b/src/pages/Customer/Card/CustomerBasicData.vue @@ -54,10 +54,10 @@ function onBeforeSave(formData, originalData) { auto-load /> <FormModel - :url="`Clients/${route.params.id}`" + :url-update="`Clients/${route.params.id}`" auto-load - model="customer" :mapper="onBeforeSave" + model="Customer" > <template #form="{ data, validate }"> <VnRow> diff --git a/src/pages/Customer/Card/CustomerBillingData.vue b/src/pages/Customer/Card/CustomerBillingData.vue index f1e78d9e5..cc894d01e 100644 --- a/src/pages/Customer/Card/CustomerBillingData.vue +++ b/src/pages/Customer/Card/CustomerBillingData.vue @@ -27,7 +27,7 @@ const getBankEntities = (data, formData) => { </script> <template> - <FormModel :url-update="`Clients/${route.params.id}`" auto-load model="customer"> + <FormModel :url-update="`Clients/${route.params.id}`" auto-load model="Customer"> <template #form="{ data, validate }"> <VnRow> <VnSelect diff --git a/src/pages/Customer/Card/CustomerCard.vue b/src/pages/Customer/Card/CustomerCard.vue index f46884834..75fcb98fa 100644 --- a/src/pages/Customer/Card/CustomerCard.vue +++ b/src/pages/Customer/Card/CustomerCard.vue @@ -5,8 +5,8 @@ import CustomerDescriptor from './CustomerDescriptor.vue'; <template> <VnCardBeta - data-key="Client" - base-url="Clients" + data-key="Customer" + :url="`Clients/${$route.params.id}/getCard`" :descriptor="CustomerDescriptor" /> </template> diff --git a/src/pages/Customer/Card/CustomerConsumption.vue b/src/pages/Customer/Card/CustomerConsumption.vue index f0d8dea47..f3949bb32 100644 --- a/src/pages/Customer/Card/CustomerConsumption.vue +++ b/src/pages/Customer/Card/CustomerConsumption.vue @@ -61,6 +61,23 @@ const columns = computed(() => [ columnFilter: false, cardVisible: true, }, + { + align: 'left', + name: 'buyerId', + label: t('customer.params.buyerId'), + component: 'select', + attrs: { + url: 'TicketRequests/getItemTypeWorker', + optionLabel: 'nickname', + optionValue: 'id', + + fields: ['id', 'nickname'], + sortBy: ['nickname ASC'], + optionFilter: 'firstName', + }, + cardVisible: false, + visible: false, + }, { name: 'description', align: 'left', @@ -74,6 +91,7 @@ const columns = computed(() => [ name: 'quantity', label: t('globals.quantity'), cardVisible: true, + visible: true, columnFilter: { inWhere: true, }, @@ -119,7 +137,7 @@ const openSendEmailDialog = async () => { openConfirmationModal( t('The consumption report will be sent'), t('Please, confirm'), - () => sendCampaignMetricsEmail({ address: arrayData.store.data.email }) + () => sendCampaignMetricsEmail({ address: arrayData.store.data.email }), ); }; const sendCampaignMetricsEmail = ({ address }) => { @@ -138,11 +156,11 @@ const updateDateParams = (value, params) => { const campaign = campaignList.value.find((c) => c.id === value); if (!campaign) return; - const { dated, previousDays, scopeDays } = campaign; - const _date = new Date(dated); - const [from, to] = dateRange(_date); - params.from = new Date(from.setDate(from.getDate() - previousDays)).toISOString(); - params.to = new Date(to.setDate(to.getDate() + scopeDays)).toISOString(); + const { dated, scopeDays } = campaign; + const from = new Date(dated); + from.setDate(from.getDate() - scopeDays); + params.from = from; + params.to = dated; return params; }; </script> @@ -152,7 +170,7 @@ const updateDateParams = (value, params) => { v-if="campaignList" data-key="CustomerConsumption" url="Clients/consumption" - :order="['itemTypeFk', 'itemName', 'itemSize', 'description']" + :order="['itemTypeFk', 'itemName', 'itemSize', 'description']" :filter="{ where: { clientFk: route.params.id } }" :columns="columns" search-url="consumption" @@ -200,29 +218,60 @@ const updateDateParams = (value, params) => { <div v-if="row.subName" class="subName"> {{ row.subName }} </div> - <FetchedTags :item="row" :max-length="3" /> + <FetchedTags :item="row" /> </template> <template #moreFilterPanel="{ params }"> <div class="column no-wrap flex-center q-gutter-y-md q-mt-xs q-pr-xl"> + <VnSelect + :filled="true" + class="q-px-sm q-pt-none fit" + url="ItemTypes" + v-model="params.typeId" + :label="t('item.list.typeName')" + :fields="['id', 'name', 'categoryFk']" + :include="'category'" + :sortBy="'name ASC'" + dense + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel>{{ scope.opt?.name }}</QItemLabel> + <QItemLabel caption>{{ + scope.opt?.category?.name + }}</QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnSelect + :filled="true" + class="q-px-sm q-pt-none fit" + url="ItemCategories" + v-model="params.categoryId" + :label="t('item.list.category')" + :fields="['id', 'name']" + :sortBy="'name ASC'" + dense + /> <VnSelect v-model="params.campaign" :options="campaignList" :label="t('globals.campaign')" :filled="true" class="q-px-sm q-pt-none fit" - dense - option-label="code" + :option-label="(opt) => t(opt.code)" + :fields="['id', 'code', 'dated', 'scopeDays']" @update:model-value="(data) => updateDateParams(data, params)" + dense > <template #option="scope"> <QItem v-bind="scope.itemProps"> <QItemSection> - <QItemLabel> - {{ scope.opt?.code }} - {{ - new Date(scope.opt?.dated).getFullYear() - }}</QItemLabel - > + <QItemLabel> {{ t(scope.opt?.code) }} </QItemLabel> + <QItemLabel caption> + {{ new Date(scope.opt?.dated).getFullYear() }} + </QItemLabel> </QItemSection> </QItem> </template> @@ -247,7 +296,21 @@ const updateDateParams = (value, params) => { </template> <i18n> +en: + + valentinesDay: Valentine's Day + mothersDay: Mother's Day + allSaints: All Saints' Day + frenchMothersDay: Mother's Day in France es: Enter a new search: Introduce una nueva búsqueda Group by items: Agrupar por artículos + valentinesDay: Día de San Valentín + mothersDay: Día de la Madre + allSaints: Día de Todos los Santos + frenchMothersDay: (Francia) Día de la Madre + Campaign consumption: Consumo campaña + Campaign: Campaña + From: Desde + To: Hasta </i18n> diff --git a/src/pages/Customer/Card/CustomerContacts.vue b/src/pages/Customer/Card/CustomerContacts.vue index c420f650e..d03f71244 100644 --- a/src/pages/Customer/Card/CustomerContacts.vue +++ b/src/pages/Customer/Card/CustomerContacts.vue @@ -62,7 +62,7 @@ const customerContactsRef = ref(null); color="primary" flat icon="add" - shortcut="+" + v-shortcut="'+'" > <QTooltip> {{ t('Add contact') }} diff --git a/src/pages/Customer/Card/CustomerCreditContracts.vue b/src/pages/Customer/Card/CustomerCreditContracts.vue index 7dc53db72..a49faeb8d 100644 --- a/src/pages/Customer/Card/CustomerCreditContracts.vue +++ b/src/pages/Customer/Card/CustomerCreditContracts.vue @@ -195,7 +195,7 @@ const updateData = () => { color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('New contract') }} diff --git a/src/pages/Customer/Card/CustomerDescriptor.vue b/src/pages/Customer/Card/CustomerDescriptor.vue index d7a8a59a1..89f9d9449 100644 --- a/src/pages/Customer/Card/CustomerDescriptor.vue +++ b/src/pages/Customer/Card/CustomerDescriptor.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed } from 'vue'; +import { onMounted, ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; @@ -11,6 +11,15 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue'; +import { useState } from 'src/composables/useState'; +const state = useState(); + +const customer = ref(); + +onMounted(async () => { + customer.value = state.get('Customer'); + if (customer.value) customer.value.webAccess = data.value?.account?.isActive; +}); const customerDebt = ref(); const customerCredit = ref(); @@ -46,13 +55,10 @@ const debtWarning = computed(() => { <template> <CardDescriptor - module="Customer" :url="`Clients/${entityId}/getCard`" - :title="data.title" - :subtitle="data.subtitle" - @on-fetch="setData" :summary="$props.summary" - data-key="customer" + data-key="Customer" + @on-fetch="setData" width="lg-width" > <template #menu="{ entity }"> @@ -61,7 +67,7 @@ const debtWarning = computed(() => { <template #body="{ entity }"> <VnLv :label="t('customer.summary.payMethod')" - :value="entity.payMethod.name" + :value="entity.payMethod?.name" /> <VnLv @@ -90,7 +96,7 @@ const debtWarning = computed(() => { </VnLv> <VnLv :label="t('customer.extendedList.tableVisibleColumns.businessTypeFk')" - :value="entity.businessType.description" + :value="entity.businessType?.description" /> </template> <template #icons="{ entity }"> @@ -103,7 +109,21 @@ const debtWarning = computed(() => { > <QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip> </QIcon> - <QIcon v-if="entity.isFreezed" name="vn:frozen" size="xs" color="primary"> + + <QIcon + v-if="entity?.substitutionAllowed" + name="help" + size="xs" + color="primary" + > + <QTooltip>{{ t('Allowed substitution') }}</QTooltip> + </QIcon> + <QIcon + v-if="customer?.isFreezed" + name="vn:frozen" + size="xs" + color="primary" + > <QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip> </QIcon> <QIcon @@ -143,13 +163,13 @@ const debtWarning = computed(() => { <br /> {{ t('unpaidDated', { - dated: toDate(customer.unpaid.dated), + dated: toDate(customer.unpaid?.dated), }) }} <br /> {{ t('unpaidAmount', { - amount: toCurrency(customer.unpaid.amount), + amount: toCurrency(customer.unpaid?.amount), }) }} </QTooltip> diff --git a/src/pages/Customer/Card/CustomerDescriptorMenu.vue b/src/pages/Customer/Card/CustomerDescriptorMenu.vue index fb78eab69..aea45721c 100644 --- a/src/pages/Customer/Card/CustomerDescriptorMenu.vue +++ b/src/pages/Customer/Card/CustomerDescriptorMenu.vue @@ -61,6 +61,16 @@ const openCreateForm = (type) => { .join('&'); useOpenURL(`/#/${type}/list?${params}`); }; +const updateSubstitutionAllowed = async () => { + try { + await axios.patch(`Clients/${route.params.id}`, { + substitutionAllowed: !$props.customer.substitutionAllowed, + }); + notify('globals.notificationSent', 'positive'); + } catch (error) { + notify(error.message, 'positive'); + } +}; </script> <template> @@ -69,6 +79,13 @@ const openCreateForm = (type) => { {{ t('globals.pageTitles.createTicket') }} </QItemSection> </QItem> + <QItem v-ripple clickable> + <QItemSection @click="updateSubstitutionAllowed()">{{ + $props.customer.substitutionAllowed + ? t('Disable substitution') + : t('Allow substitution') + }}</QItemSection> + </QItem> <QItem v-ripple clickable> <QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection> </QItem> diff --git a/src/pages/Customer/Card/CustomerFileManagement.vue b/src/pages/Customer/Card/CustomerFileManagement.vue index 134d8dbd6..b565db6e7 100644 --- a/src/pages/Customer/Card/CustomerFileManagement.vue +++ b/src/pages/Customer/Card/CustomerFileManagement.vue @@ -236,7 +236,7 @@ const toCustomerFileManagementCreate = () => { @click.stop="toCustomerFileManagementCreate()" color="primary" fab - shortcut="+" + v-shortcut="'+'" icon="add" /> <QTooltip> diff --git a/src/pages/Customer/Card/CustomerFiscalData.vue b/src/pages/Customer/Card/CustomerFiscalData.vue index ceeb70bb6..93909eb9c 100644 --- a/src/pages/Customer/Card/CustomerFiscalData.vue +++ b/src/pages/Customer/Card/CustomerFiscalData.vue @@ -12,6 +12,7 @@ import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; import { getDifferences, getUpdatedValues } from 'src/filters'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; @@ -73,7 +74,7 @@ async function acceptPropagate({ isEqualizated }) { <FormModel :url-update="`Clients/${route.params.id}/updateFiscalData`" auto-load - model="customer" + model="Customer" :mapper="onBeforeSave" observe-form-changes @on-data-saved="checkEtChanges" @@ -151,14 +152,11 @@ async function acceptPropagate({ isEqualizated }) { </VnRow> <VnRow> <QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" /> - <div> - <QCheckbox :label="t('globals.isVies')" v-model="data.isVies" /> - <QIcon name="info" class="cursor-info q-ml-sm" size="sm"> - <QTooltip> - {{ t('whenActivatingIt') }} - </QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="data.isVies" + :label="t('globals.isVies')" + :info="t('whenActivatingIt')" + /> </VnRow> <VnRow> @@ -170,17 +168,11 @@ async function acceptPropagate({ isEqualizated }) { </VnRow> <VnRow> - <div> - <QCheckbox - :label="t('Is equalizated')" - v-model="data.isEqualizated" - /> - <QIcon class="cursor-info q-ml-sm" name="info" size="sm"> - <QTooltip> - {{ t('inOrderToInvoice') }} - </QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="data.isEqualizated" + :label="t('Is equalizated')" + :info="t('inOrderToInvoice')" + /> <QCheckbox :label="t('Daily invoice')" v-model="data.hasDailyInvoice" /> </VnRow> diff --git a/src/pages/Customer/Card/CustomerNotes.vue b/src/pages/Customer/Card/CustomerNotes.vue index b85174696..189b59904 100644 --- a/src/pages/Customer/Card/CustomerNotes.vue +++ b/src/pages/Customer/Card/CustomerNotes.vue @@ -23,5 +23,6 @@ const noteFilter = computed(() => { :body="{ clientFk: route.params.id }" style="overflow-y: auto" :select-type="true" + required /> </template> diff --git a/src/pages/Customer/Card/CustomerSamples.vue b/src/pages/Customer/Card/CustomerSamples.vue index f12691112..19a7f8759 100644 --- a/src/pages/Customer/Card/CustomerSamples.vue +++ b/src/pages/Customer/Card/CustomerSamples.vue @@ -104,7 +104,7 @@ const tableRef = ref(); color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('Send sample') }} diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index 3c4106846..809f10918 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -27,7 +27,7 @@ async function hasCustomerRole() { <FormModel :url-update="`Clients/${route.params.id}/updateUser`" :filter="filter" - model="customer" + model="Customer" :mapper=" ({ account }) => { const { name, email, active } = account; diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index 9b883daad..1c5a08304 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -51,11 +51,7 @@ const exprBuilder = (param, value) => { </QItem> <QItem class="q-mb-sm"> <QItemSection> - <VnInput - :label="t('globals.name')" - v-model="params.name" - is-outlined - /> + <VnInput :label="t('Name')" v-model="params.name" is-outlined /> </QItemSection> </QItem> <QItem class="q-mb-sm"> diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index 2f2dd5978..0bfca7910 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -274,6 +274,7 @@ const columns = computed(() => [ align: 'left', name: 'isActive', label: t('customer.summary.isActive'), + component: 'checkbox', chip: { color: null, condition: (value) => !value, @@ -312,6 +313,7 @@ const columns = computed(() => [ align: 'left', name: 'isFreezed', label: t('customer.extendedList.tableVisibleColumns.isFreezed'), + component: 'checkbox', chip: { color: null, condition: (value) => value, @@ -429,7 +431,7 @@ function handleLocation(data, location) { <VnTable ref="tableRef" :data-key="dataKey" - url="Clients/filter" + url="Clients/extendedListFilter" :create="{ urlCreate: 'Clients/createWithUser', title: t('globals.pageTitles.customerCreate'), diff --git a/src/pages/Customer/Defaulter/CustomerDefaulter.vue b/src/pages/Customer/Defaulter/CustomerDefaulter.vue index eca2ad596..dc4ac9162 100644 --- a/src/pages/Customer/Defaulter/CustomerDefaulter.vue +++ b/src/pages/Customer/Defaulter/CustomerDefaulter.vue @@ -9,7 +9,7 @@ import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.v import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnInput from 'src/components/common/VnInput.vue'; import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue'; -import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; import VnTable from 'src/components/VnTable/VnTable.vue'; import { useArrayData } from 'src/composables/useArrayData'; diff --git a/src/pages/Customer/components/CustomerAddressEdit.vue b/src/pages/Customer/components/CustomerAddressEdit.vue index d650bbbda..f852c160a 100644 --- a/src/pages/Customer/components/CustomerAddressEdit.vue +++ b/src/pages/Customer/components/CustomerAddressEdit.vue @@ -233,7 +233,7 @@ function handleLocation(data, location) { postcode: data.postalCode, city: data.city, province: data.province, - country: data.province.country, + country: data.province?.country, }" @update:model-value="(location) => handleLocation(data, location)" ></VnLocation> @@ -336,7 +336,7 @@ function handleLocation(data, location) { class="cursor-pointer add-icon q-mt-md" flat icon="add" - shortcut="+" + v-shortcut="'+'" > <QTooltip> {{ t('Add note') }} diff --git a/src/pages/Customer/components/CustomerNewPayment.vue b/src/pages/Customer/components/CustomerNewPayment.vue index c2c38b55a..8f61bac89 100644 --- a/src/pages/Customer/components/CustomerNewPayment.vue +++ b/src/pages/Customer/components/CustomerNewPayment.vue @@ -84,7 +84,7 @@ function setPaymentType(accounting) { viewReceipt.value = isCash.value; if (accountingType.value.daysInFuture) initialData.payed.setDate( - initialData.payed.getDate() + accountingType.value.daysInFuture + initialData.payed.getDate() + accountingType.value.daysInFuture, ); maxAmount.value = accountingType.value && accountingType.value.maxAmount; @@ -114,7 +114,7 @@ function onBeforeSave(data) { if (isCash.value && shouldSendEmail.value && !data.email) return notify(t('There is no assigned email for this client'), 'negative'); - data.bankFk = data.bankFk.id; + data.bankFk = data.bankFk?.id; return data; } @@ -189,7 +189,7 @@ async function getAmountPaid() { :url-create="urlCreate" :mapper="onBeforeSave" @on-data-saved="onDataSaved" - :prevent-submit="true" + prevent-submit > <template #form="{ data, validate }"> <span ref="closeButton" class="row justify-end close-icon" v-close-popup> diff --git a/src/pages/Customer/components/CustomerSamplesCreate.vue b/src/pages/Customer/components/CustomerSamplesCreate.vue index 754693672..1294a5d25 100644 --- a/src/pages/Customer/components/CustomerSamplesCreate.vue +++ b/src/pages/Customer/components/CustomerSamplesCreate.vue @@ -18,6 +18,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import CustomerSamplesPreview from 'src/pages/Customer/components/CustomerSamplesPreview.vue'; import FormPopup from 'src/components/FormPopup.vue'; +import { useArrayData } from 'src/composables/useArrayData'; const { dialogRef, onDialogOK } = useDialogPluginComponent(); @@ -39,7 +40,7 @@ const optionsSamplesVisible = ref([]); const sampleType = ref({ hasPreview: false }); const initialData = reactive({}); const entityId = computed(() => route.params.id); -const customer = computed(() => state.get('customer')); +const customer = computed(() => useArrayData('Customer').store?.data); const filterEmailUsers = { where: { userFk: user.value.id } }; const filterClientsAddresses = { include: [ @@ -65,9 +66,9 @@ const filterSamplesVisible = { defineEmits(['confirm', ...useDialogPluginComponent.emits]); onBeforeMount(async () => { - initialData.clientFk = customer.value.id; - initialData.recipient = customer.value.email; - initialData.recipientId = customer.value.id; + initialData.clientFk = customer.value?.id; + initialData.recipient = customer.value?.email; + initialData.recipientId = customer.value?.id; }); const setEmailUser = (data) => { diff --git a/src/pages/Customer/locale/en.yml b/src/pages/Customer/locale/en.yml index 118f04a31..b6d495335 100644 --- a/src/pages/Customer/locale/en.yml +++ b/src/pages/Customer/locale/en.yml @@ -107,6 +107,9 @@ customer: defaulterSinced: Defaulted Since hasRecovery: Has Recovery socialName: Social name + typeId: Type + buyerId: Buyer + categoryId: Category city: City phone: Phone postcode: Postcode diff --git a/src/pages/Customer/locale/es.yml b/src/pages/Customer/locale/es.yml index 7c33ffee8..f50d049da 100644 --- a/src/pages/Customer/locale/es.yml +++ b/src/pages/Customer/locale/es.yml @@ -108,6 +108,9 @@ customer: hasRecovery: Tiene recobro socialName: Razón social campaign: Campaña + typeId: Familia + buyerId: Comprador + categoryId: Reino city: Ciudad phone: Teléfono postcode: Código postal diff --git a/src/pages/Entry/Card/EntryBasicData.vue b/src/pages/Entry/Card/EntryBasicData.vue index 689eea686..6462ed24a 100644 --- a/src/pages/Entry/Card/EntryBasicData.vue +++ b/src/pages/Entry/Card/EntryBasicData.vue @@ -1,30 +1,32 @@ <script setup> -import { ref } from 'vue'; +import { onMounted, ref } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useRole } from 'src/composables/useRole'; +import { useState } from 'src/composables/useState'; +import { checkEntryLock } from 'src/composables/checkEntryLock'; import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; -import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; -import FilterTravelForm from 'src/components/FilterTravelForm.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; -import { toDate } from 'src/filters'; +import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue'; import VnSelectSupplier from 'src/components/common/VnSelectSupplier.vue'; const route = useRoute(); const { t } = useI18n(); const { hasAny } = useRole(); const isAdministrative = () => hasAny(['administrative']); +const state = useState(); +const user = state.getUser().fn(); const companiesOptions = ref([]); const currenciesOptions = ref([]); -const onFilterTravelSelected = (formData, id) => { - formData.travelFk = id; -}; +onMounted(() => { + checkEntryLock(route.params.id, user.id); +}); </script> <template> @@ -52,46 +54,24 @@ const onFilterTravelSelected = (formData, id) => { > <template #form="{ data }"> <VnRow> + <VnSelectTravelExtended + :data="data" + v-model="data.travelFk" + :onFilterTravelSelected="(data, result) => (data.travelFk = result)" + /> <VnSelectSupplier v-model="data.supplierFk" hide-selected :required="true" - map-options /> - <VnSelectDialog - :label="t('entry.basicData.travel')" - v-model="data.travelFk" - url="Travels/filter" - :fields="['id', 'warehouseInName']" - option-value="id" - option-label="warehouseInName" - map-options - hide-selected - :required="true" - action-icon="filter_alt" - > - <template #form> - <FilterTravelForm - @travel-selected="onFilterTravelSelected(data, $event)" - /> - </template> - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel> - {{ scope.opt?.agencyModeName }} - - {{ scope.opt?.warehouseInName }} - ({{ toDate(scope.opt?.shipped) }}) → - {{ scope.opt?.warehouseOutName }} - ({{ toDate(scope.opt?.landed) }}) - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelectDialog> </VnRow> <VnRow> <VnInput v-model="data.reference" :label="t('globals.reference')" /> + <VnInputNumber + v-model="data.invoiceAmount" + :label="t('entry.summary.invoiceAmount')" + :positive="false" + /> </VnRow> <VnRow> <VnInput @@ -113,8 +93,7 @@ const onFilterTravelSelected = (formData, id) => { <VnInputNumber :label="t('entry.summary.commission')" v-model="data.commission" - step="1" - autofocus + :step="1" :positive="false" /> <VnSelect @@ -161,7 +140,7 @@ const onFilterTravelSelected = (formData, id) => { :label="t('entry.summary.excludedFromAvailable')" /> <QCheckbox - v-if="isAdministrative()" + :disable="!isAdministrative()" v-model="data.isBooked" :label="t('entry.basicData.booked')" /> diff --git a/src/pages/Entry/Card/EntryBuys.vue b/src/pages/Entry/Card/EntryBuys.vue index 6194ce5b8..81578c609 100644 --- a/src/pages/Entry/Card/EntryBuys.vue +++ b/src/pages/Entry/Card/EntryBuys.vue @@ -1,478 +1,806 @@ <script setup> -import { ref, computed } from 'vue'; -import { useRoute, useRouter } from 'vue-router'; +import { useStateStore } from 'stores/useStateStore'; +import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { QBtn } from 'quasar'; +import { onMounted, ref } from 'vue'; -import VnPaginate from 'src/components/ui/VnPaginate.vue'; -import VnSelect from 'components/common/VnSelect.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import FetchedTags from 'components/ui/FetchedTags.vue'; -import VnConfirm from 'components/ui/VnConfirm.vue'; +import { useState } from 'src/composables/useState'; + +import FetchData from 'src/components/FetchData.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; -import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; - -import { useQuasar } from 'quasar'; -import { toCurrency } from 'src/filters'; +import FetchedTags from 'src/components/ui/FetchedTags.vue'; +import VnColor from 'src/components/common/VnColor.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import ItemDescriptor from 'src/pages/Item/Card/ItemDescriptor.vue'; import axios from 'axios'; -import useNotify from 'src/composables/useNotify.js'; +import VnSelectEnum from 'src/components/common/VnSelectEnum.vue'; +import { checkEntryLock } from 'src/composables/checkEntryLock'; -const quasar = useQuasar(); -const route = useRoute(); -const router = useRouter(); -const { t } = useI18n(); -const { notify } = useNotify(); - -const rowsSelected = ref([]); -const entryBuysPaginateRef = ref(null); -const originalRowDataCopy = ref(null); - -const getInputEvents = (colField, props) => { - return colField === 'packagingFk' - ? { 'update:modelValue': () => saveChange(colField, props) } - : { - 'keyup.enter': () => saveChange(colField, props), - blur: () => saveChange(colField, props), - }; -}; - -const tableColumnComponents = computed(() => ({ - item: { - component: QBtn, - props: { - color: 'primary', - flat: true, - }, - event: () => ({}), +const $props = defineProps({ + id: { + type: Number, + default: null, }, - quantity: { - component: VnInput, - props: { - type: 'number', - min: 0, - class: 'input-number', - dense: true, - }, - event: getInputEvents, + editableMode: { + type: Boolean, + default: true, }, - packagingFk: { - component: VnSelect, - props: { - 'option-value': 'id', - 'option-label': 'id', - 'emit-value': true, - 'map-options': true, - 'use-input': true, - 'hide-selected': true, - url: 'Packagings', - fields: ['id'], - where: { freightItemFk: true }, - 'sort-by': 'id ASC', - dense: true, - }, - event: getInputEvents, + tableHeight: { + type: String, + default: null, }, - stickers: { - component: VnInput, - props: { - type: 'number', - min: 0, - class: 'input-number', - dense: true, - }, - event: getInputEvents, - }, - printedStickers: { - component: VnInput, - props: { - type: 'number', - min: 0, - class: 'input-number', - dense: true, - }, - event: getInputEvents, - }, - weight: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - packing: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - grouping: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - buyingValue: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - price2: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - price3: { - component: VnInput, - props: { - type: 'number', - min: 0, - dense: true, - }, - event: getInputEvents, - }, - import: { - component: 'span', - props: {}, - event: () => ({}), - }, -})); - -const entriesTableColumns = computed(() => { - return [ - { - label: t('globals.item'), - field: 'itemFk', - name: 'item', - align: 'left', - }, - { - label: t('globals.quantity'), - field: 'quantity', - name: 'quantity', - align: 'left', - }, - { - label: t('entry.summary.package'), - field: 'packagingFk', - name: 'packagingFk', - align: 'left', - }, - { - label: t('entry.summary.stickers'), - field: 'stickers', - name: 'stickers', - align: 'left', - }, - { - label: t('entry.buys.printedStickers'), - field: 'printedStickers', - name: 'printedStickers', - align: 'left', - }, - { - label: t('globals.weight'), - field: 'weight', - name: 'weight', - align: 'left', - }, - { - label: t('entry.summary.packing'), - field: 'packing', - name: 'packing', - align: 'left', - }, - { - label: t('entry.summary.grouping'), - field: 'grouping', - name: 'grouping', - align: 'left', - }, - { - label: t('entry.summary.buyingValue'), - field: 'buyingValue', - name: 'buyingValue', - align: 'left', - format: (value) => toCurrency(value), - }, - { - label: t('item.fixedPrice.groupingPrice'), - field: 'price2', - name: 'price2', - align: 'left', - }, - { - label: t('item.fixedPrice.packingPrice'), - field: 'price3', - name: 'price3', - align: 'left', - }, - { - label: t('entry.summary.import'), - name: 'import', - align: 'left', - format: (_, row) => toCurrency(row.buyingValue * row.quantity), - }, - ]; }); -const copyOriginalRowsData = (rows) => { - originalRowDataCopy.value = JSON.parse(JSON.stringify(rows)); -}; - -const saveChange = async (field, { rowIndex, row }) => { - if (originalRowDataCopy.value[rowIndex][field] == row[field]) return; - await axios.patch(`Buys/${row.id}`, row); - originalRowDataCopy.value[rowIndex][field] = row[field]; -}; - -const openRemoveDialog = async () => { - quasar - .dialog({ - component: VnConfirm, - componentProps: { - title: t('Confirm deletion'), - message: t( - `Are you sure you want to delete this buy${ - rowsSelected.value.length > 1 ? 's' : '' - }?` - ), - data: rowsSelected.value, +const state = useState(); +const user = state.getUser().fn(); +const stateStore = useStateStore(); +const { t } = useI18n(); +const route = useRoute(); +const selectedRows = ref([]); +const entityId = ref($props.id ?? route.params.id); +const entryBuysRef = ref(); +const footerFetchDataRef = ref(); +const footer = ref({}); +const columns = [ + { + align: 'center', + labelAbbreviation: 'NV', + label: t('Ignore'), + toolTip: t('Ignored for available'), + name: 'isIgnored', + component: 'checkbox', + attrs: { + toggleIndeterminate: false, + }, + create: true, + width: '25px', + }, + { + label: t('Buyer'), + name: 'workerFk', + component: 'select', + attrs: { + url: 'Workers/search', + fields: ['id', 'nickname'], + optionLabel: 'nickname', + optionValue: 'id', + }, + visible: false, + }, + { + label: t('Family'), + name: 'itemTypeFk', + component: 'select', + attrs: { + url: 'itemTypes', + fields: ['id', 'name'], + optionLabel: 'name', + optionValue: 'id', + }, + visible: false, + }, + { + name: 'id', + isId: true, + visible: false, + isEditable: false, + columnFilter: false, + }, + { + name: 'entryFk', + isId: true, + visible: false, + isEditable: false, + disable: true, + create: true, + columnFilter: false, + }, + { + align: 'center', + label: 'Id', + name: 'itemFk', + component: 'number', + isEditable: false, + width: '35px', + }, + { + labelAbbreviation: '', + label: 'Color', + name: 'hex', + columnSearch: false, + isEditable: false, + width: '9px', + component: 'select', + attrs: { + url: 'Inks', + fields: ['id', 'name'], + }, + }, + { + align: 'center', + label: t('Article'), + name: 'name', + component: 'select', + attrs: { + url: 'Items', + fields: ['id', 'name'], + optionLabel: 'name', + optionValue: 'id', + }, + width: '85px', + isEditable: false, + }, + { + align: 'center', + label: t('Article'), + name: 'itemFk', + visible: false, + create: true, + columnFilter: false, + }, + { + align: 'center', + labelAbbreviation: t('Siz.'), + label: t('Size'), + toolTip: t('Size'), + component: 'number', + name: 'size', + width: '35px', + isEditable: false, + style: () => { + return { color: 'var(--vn-label-color)' }; + }, + }, + { + align: 'center', + labelAbbreviation: t('Sti.'), + label: t('Stickers'), + toolTip: t('Printed Stickers/Stickers'), + name: 'stickers', + component: 'input', + create: true, + attrs: { + positive: false, + }, + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + row['quantity'] = value * row['packing']; + row['amount'] = row['quantity'] * row['buyingValue']; }, - }) - .onOk(async () => { - await deleteBuys(); - const notifyMessage = t( - `Buy${rowsSelected.value.length > 1 ? 's' : ''} deleted` - ); - notify(notifyMessage, 'positive'); - }); -}; + }, + width: '35px', + }, + { + align: 'center', + label: t('Bucket'), + name: 'packagingFk', + component: 'select', + attrs: { + url: 'packagings', + fields: ['id'], + optionLabel: 'id', + optionValue: 'id', + }, + create: true, + width: '40px', + }, + { + align: 'center', + label: 'Kg', + name: 'weight', + component: 'number', + create: true, + width: '35px', + format: (row) => parseFloat(row['weight']).toFixed(1), + }, + { + labelAbbreviation: 'P', + label: 'Packing', + toolTip: 'Packing', + name: 'packing', + component: 'number', + create: true, + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + const oldPacking = oldValue === 1 || oldValue === null ? 1 : oldValue; + row['weight'] = (row['weight'] * value) / oldPacking; + row['quantity'] = row['stickers'] * value; + row['amount'] = row['quantity'] * row['buyingValue']; + }, + }, + width: '30px', + style: (row) => { + if (row.groupingMode === 'grouping') + return { color: 'var(--vn-label-color)' }; + }, + }, + { + align: 'center', + labelAbbreviation: 'GM', + label: t('Grouping selector'), + toolTip: t('Grouping selector'), + name: 'groupingMode', + component: 'toggle', + attrs: { + 'toggle-indeterminate': true, + trueValue: 'grouping', + falseValue: 'packing', + indeterminateValue: null, + }, + size: 'xs', + width: '25px', + create: true, + rightFilter: false, + getIcon: (value) => { + switch (value) { + case 'grouping': + return 'toggle_on'; + case 'packing': + return 'toggle_off'; + default: + return 'minimize'; + } + }, + }, + { + align: 'center', + labelAbbreviation: 'G', + label: 'Grouping', + toolTip: 'Grouping', + name: 'grouping', + component: 'number', + width: '30px', + create: true, + style: (row) => { + if (row.groupingMode === 'packing') return { color: 'var(--vn-label-color)' }; + }, + }, + { + align: 'center', + label: t('Quantity'), + name: 'quantity', + component: 'number', + attrs: { + positive: false, + }, + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + row['amount'] = value * row['buyingValue']; + }, + }, + width: '45px', + create: true, + style: getQuantityStyle, + }, + { + align: 'center', + labelAbbreviation: t('Cost'), + label: t('Buying value'), + toolTip: t('Buying value'), + name: 'buyingValue', + create: true, + component: 'number', + attrs: { + positive: false, + }, + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + row['amount'] = row['quantity'] * value; + }, + }, + width: '45px', + format: (row) => parseFloat(row['buyingValue']).toFixed(3), + }, + { + align: 'center', + label: t('Amount'), + name: 'amount', + width: '45px', + component: 'number', + attrs: { + positive: false, + }, + isEditable: false, + format: (row) => parseFloat(row['amount']).toFixed(2), + style: getAmountStyle, + }, + { + align: 'center', + labelAbbreviation: t('Pack.'), + label: t('Package'), + toolTip: t('Package'), + name: 'price2', + component: 'number', + width: '35px', + create: true, + format: (row) => parseFloat(row['price2']).toFixed(2), + }, + { + align: 'center', + label: t('Box'), + name: 'price3', + component: 'number', + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + row['price2'] = row['price2'] * (value / oldValue); + }, + }, + width: '35px', + create: true, + format: (row) => parseFloat(row['price3']).toFixed(2), + }, + { + align: 'center', + labelAbbreviation: 'CM', + label: t('Check min price'), + toolTip: t('Check min price'), + name: 'hasMinPrice', + attrs: { + toggleIndeterminate: false, + }, + component: 'checkbox', + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + await axios.patch(`Items/${row['itemFk']}`, { + hasMinPrice: value, + }); + }, + }, + width: '25px', + }, + { + align: 'center', + labelAbbreviation: 'Min.', + label: t('Minimum price'), + toolTip: t('Minimum price'), + name: 'minPrice', + component: 'number', + cellEvent: { + 'update:modelValue': async (value, oldValue, row) => { + await axios.patch(`Items/${row['itemFk']}`, { + minPrice: value, + }); + }, + }, + width: '35px', + style: (row) => { + if (!row?.hasMinPrice) return { color: 'var(--vn-label-color)' }; + }, + format: (row) => parseFloat(row['minPrice']).toFixed(2), + }, + { + align: 'center', + labelAbbreviation: t('P.Sen'), + label: t('Packing sent'), + toolTip: t('Packing sent'), + name: 'packingOut', + component: 'number', + isEditable: false, + width: '40px', + style: () => { + return { color: 'var(--vn-label-color)' }; + }, + }, + { + align: 'center', + labelAbbreviation: t('Com.'), + label: t('Comment'), + toolTip: t('Comment'), + name: 'comment', + component: 'input', + isEditable: false, + width: '50px', + }, + { + align: 'center', + labelAbbreviation: 'Prod.', + label: t('Producer'), + toolTip: t('Producer'), + name: 'subName', + isEditable: false, + width: '45px', + style: () => { + return { color: 'var(--vn-label-color)' }; + }, + }, + { + align: 'center', + label: t('Tags'), + name: 'tags', + width: '125px', + columnSearch: false, + }, + { + align: 'center', + labelAbbreviation: 'Comp.', + label: t('Company'), + toolTip: t('Company'), + name: 'company_name', + component: 'input', + isEditable: false, + width: '35px', + style: () => { + return { color: 'var(--vn-label-color)' }; + }, + }, +]; -const deleteBuys = async () => { - await axios.post('Buys/deleteBuys', { buys: rowsSelected.value }); - entryBuysPaginateRef.value.fetch(); -}; +function getQuantityStyle(row) { + if (row?.quantity !== row?.stickers * row?.packing) + return { color: 'var(--q-negative)' }; +} +function getAmountStyle(row) { + if (row?.isChecked) return { color: 'var(--q-positive)' }; + return { color: 'var(--vn-label-color)' }; +} -const importBuys = () => { - router.push({ name: 'EntryBuysImport' }); -}; +async function beforeSave(data, getChanges) { + try { + const changes = data.updates; + if (!changes) return data; + const patchPromises = []; -const toggleGroupingMode = async (buy, mode) => { - const groupingMode = mode === 'grouping' ? mode : 'packing'; - const newGroupingMode = buy.groupingMode === groupingMode ? null : groupingMode; - const params = { - groupingMode: newGroupingMode, - }; - await axios.patch(`Buys/${buy.id}`, params); - buy.groupingMode = newGroupingMode; -}; + for (const change of changes) { + let patchData = {}; -const lockIconType = (groupingMode, mode) => { - if (mode === 'packing') { - return groupingMode === 'packing' ? 'lock' : 'lock_open'; - } else { - return groupingMode === 'grouping' ? 'lock' : 'lock_open'; + if ('hasMinPrice' in change.data) { + patchData.hasMinPrice = change.data?.hasMinPrice; + delete change.data.hasMinPrice; + } + if ('minPrice' in change.data) { + patchData.minPrice = change.data?.minPrice; + delete change.data.minPrice; + } + + if (Object.keys(patchData).length > 0) { + const promise = axios + .get('Buys/findOne', { + params: { + filter: { + fields: ['itemFk'], + where: { id: change.where.id }, + }, + }, + }) + .then((buy) => { + return axios.patch(`Items/${buy.data.itemFk}`, patchData); + }) + .catch((error) => { + console.error('Error processing change: ', change, error); + }); + + patchPromises.push(promise); + } + } + + await Promise.all(patchPromises); + + data.updates = changes.filter((change) => Object.keys(change.data).length > 0); + + return data; + } catch (error) { + console.error('Error in beforeSave:', error); + throw error; } -}; +} + +function invertQuantitySign(rows, sign) { + for (const row of rows) { + if (sign > 0) row.quantity = Math.abs(row.quantity); + else if (row.quantity > 0) row.quantity = -row.quantity; + } +} +function setIsChecked(rows, value) { + for (const row of rows) { + row.isChecked = value; + } + footerFetchDataRef.value.fetch(); +} + +async function setBuyUltimate(itemFk, data) { + if (!itemFk) return; + const buyUltimate = await axios.get(`Entries/getBuyUltimate`, { + params: { + itemFk, + warehouseFk: user.warehouseFk, + date: Date.vnNew(), + }, + }); + const buyUltimateData = buyUltimate.data[0]; + + const allowedKeys = columns + .filter((col) => col.create === true) + .map((col) => col.name); + + allowedKeys.forEach((key) => { + if (buyUltimateData.hasOwnProperty(key) && key !== 'entryFk') { + if (!['stickers', 'quantity'].includes(key)) data[key] = buyUltimateData[key]; + } + }); +} + +onMounted(() => { + stateStore.rightDrawer = false; + if ($props.editableMode) checkEntryLock(entityId.value, user.id); +}); </script> - <template> - <VnSubToolbar> - <template #st-actions> - <QBtnGroup push style="column-gap: 10px"> - <slot name="moreBeforeActions" /> - <QBtn - :label="t('globals.remove')" - color="primary" - icon="delete" - flat - @click="openRemoveDialog()" - :disable="!rowsSelected?.length" - :title="t('globals.remove')" - /> - </QBtnGroup> - </template> - </VnSubToolbar> - <VnPaginate - ref="entryBuysPaginateRef" - data-key="EntryBuys" - :url="`Entries/${route.params.id}/getBuys`" - @on-fetch="copyOriginalRowsData($event)" - auto-load - > - <template #body="{ rows }"> - <QTable - :rows="rows" - :columns="entriesTableColumns" - selection="multiple" - row-key="id" - class="full-width q-mt-md" - :grid="$q.screen.lt.md" - v-model:selected="rowsSelected" - :no-data-label="t('globals.noResults')" + <Teleport to="#st-data" v-if="stateStore?.isSubToolbarShown() && editableMode"> + <QBtnGroup push style="column-gap: 1px"> + <QBtnDropdown + label="+/-" + color="primary" + flat + :title="t('Invert quantity value')" + :disable="!selectedRows.length" + data-cy="change-quantity-sign" > - <template #body="props"> - <QTr> - <QTd> - <QCheckbox v-model="props.selected" /> - </QTd> - <QTd - v-for="col in props.cols" - :key="col.name" - style="max-width: 100px" - > - <component - :is="tableColumnComponents[col.name].component" - v-bind="tableColumnComponents[col.name].props" - v-model="props.row[col.field]" - v-on=" - tableColumnComponents[col.name].event( - col.field, - props - ) - " + <QList> + <QItem> + <QItemSection> + <QBtn + flat + @click="invertQuantitySign(selectedRows, -1)" + data-cy="set-negative-quantity" > - <template - v-if=" - col.name === 'grouping' || col.name === 'packing' - " - #append - > - <QBtn - :icon=" - lockIconType(props.row.groupingMode, col.name) - " - @click="toggleGroupingMode(props.row, col.name)" - class="cursor-pointer" - size="sm" - flat - dense - unelevated - push - :style="{ - 'font-variation-settings': `'FILL' ${ - lockIconType( - props.row.groupingMode, - col.name - ) === 'lock' - ? 1 - : 0 - }`, - }" - /> - </template> - <template - v-if="col.name === 'item' || col.name === 'import'" - > - {{ col.value }} - </template> - <ItemDescriptorProxy - v-if="col.name === 'item'" - :id="props.row.item.id" - /> - </component> - </QTd> - </QTr> - <QTr no-hover class="full-width infoRow" style="column-span: all"> - <QTd /> - <QTd cols> - <span>{{ props.row.item.itemType.code }}</span> - </QTd> - <QTd> - <span>{{ props.row.item.size }}</span> - </QTd> - <QTd> - <span>{{ toCurrency(props.row.item.minPrice) }}</span> - </QTd> - <QTd colspan="7"> - <span>{{ props.row.item.concept }}</span> - <span v-if="props.row.item.subName" class="subName"> - {{ props.row.item.subName }} - </span> - <FetchedTags :item="props.row.item" /> - </QTd> - </QTr> - </template> - <template #item="props"> - <div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition"> - <QCard bordered flat> - <QCardSection> - <QCheckbox v-model="props.selected" dense /> - </QCardSection> - <QSeparator /> - <QList dense> - <QItem v-for="col in props.cols" :key="col.name"> - <component - :is="tableColumnComponents[col.name].component" - v-bind="tableColumnComponents[col.name].props" - v-model="props.row[col.field]" - v-on=" - tableColumnComponents[col.name].event( - col.field, - props - ) - " - class="full-width" - > - <template - v-if=" - col.name === 'item' || - col.name === 'import' - " - > - {{ col.label + ': ' + col.value }} - </template> - </component> - </QItem> - </QList> - </QCard> - </div> - </template> - </QTable> + <span style="font-size: large">-</span> + </QBtn> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QBtn + flat + @click="invertQuantitySign(selectedRows, 1)" + data-cy="set-positive-quantity" + > + <span style="font-size: large">+</span> + </QBtn> + </QItemSection> + </QItem> + </QList> + </QBtnDropdown> + <QBtnDropdown + icon="price_check" + color="primary" + flat + :title="t('Check buy amount')" + :disable="!selectedRows.length" + data-cy="check-buy-amount" + > + <QList> + <QItem> + <QItemSection> + <QBtn + size="sm" + icon="check" + flat + @click="setIsChecked(selectedRows, true)" + data-cy="check-amount" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QBtn + size="sm" + icon="close" + flat + @click="setIsChecked(selectedRows, false)" + data-cy="uncheck-amount" + /> + </QItemSection> + </QItem> + </QList> + </QBtnDropdown> + </QBtnGroup> + </Teleport> + <FetchData + ref="footerFetchDataRef" + :url="`Entries/${entityId}/getBuyList`" + :params="{ groupBy: 'GROUP BY b.entryFk' }" + @on-fetch="(data) => (footer = data[0])" + auto-load + /> + <VnTable + ref="entryBuysRef" + data-key="EntryBuys" + :url="`Entries/${entityId}/getBuyList`" + save-url="Buys/crud" + :disable-option="{ card: true }" + v-model:selected="selectedRows" + @on-fetch="() => footerFetchDataRef.fetch()" + :table=" + editableMode + ? { + 'row-key': 'id', + selection: 'multiple', + } + : {} + " + :create=" + editableMode + ? { + urlCreate: 'Buys', + title: t('Create buy'), + onDataSaved: () => { + entryBuysRef.reload(); + }, + formInitialData: { entryFk: entityId, isIgnored: false }, + showSaveAndContinueBtn: true, + } + : null + " + :create-complement="{ + isFullWidth: true, + containerStyle: { + display: 'flex', + 'flex-wrap': 'wrap', + gap: '16px', + position: 'relative', + height: '450px', + }, + columnGridStyle: { + 'max-width': '50%', + flex: 1, + 'margin-right': '30px', + }, + }" + :is-editable="editableMode" + :without-header="!editableMode" + :with-filters="editableMode" + :right-search="true" + :right-search-icon="true" + :row-click="false" + :columns="columns" + :beforeSaveFn="beforeSave" + class="buyList" + :table-height="$props.tableHeight ?? '84vh'" + auto-load + footer + data-cy="entry-buys" + > + <template #column-hex="{ row }"> + <VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" /> </template> - </VnPaginate> - - <QPageSticky :offset="[20, 20]"> - <QBtn fab icon="upload" color="primary" @click="importBuys()" /> - <QTooltip class="text-no-wrap"> - {{ t('Import buys') }} - </QTooltip> - </QPageSticky> + <template #column-name="{ row }"> + <span class="link"> + {{ row?.name }} + <ItemDescriptorProxy :id="row?.itemFk" /> + </span> + </template> + <template #column-tags="{ row }"> + <FetchedTags :item="row" :columns="3" /> + </template> + <template #column-stickers="{ row }"> + <span :class="editableMode ? 'editable-text' : ''"> + <span style="color: var(--vn-label-color)"> + {{ row.printedStickers }} + </span> + <span>/{{ row.stickers }}</span> + </span> + </template> + <template #column-footer-stickers> + <div> + <span style="color: var(--vn-label-color)"> + {{ footer?.printedStickers }}</span + > + <span>/</span> + <span data-cy="footer-stickers">{{ footer?.stickers }}</span> + </div> + </template> + <template #column-footer-weight> + {{ footer?.weight }} + </template> + <template #column-footer-quantity> + <span :style="getQuantityStyle(footer)" data-cy="footer-quantity"> + {{ footer?.quantity }} + </span> + </template> + <template #column-footer-amount> + <span :style="getAmountStyle(footer)" data-cy="footer-amount"> + {{ footer?.amount }} + </span> + </template> + <template #column-create-itemFk="{ data }"> + <VnSelect + url="Items/search" + v-model="data.itemFk" + :label="t('Article')" + :fields="['id', 'name', 'size', 'producerName']" + :filter-options="['id', 'name', 'size', 'producerName']" + option-label="name" + option-value="id" + @update:modelValue=" + async (value) => { + await setBuyUltimate(value, data); + } + " + :required="true" + data-cy="itemFk-create-popup" + sort-by="nickname DESC" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt.name }} + </QItemLabel> + <QItemLabel caption> + #{{ scope.opt.id }}, {{ scope.opt?.size }}, + {{ scope.opt?.producerName }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </template> + <template #column-create-groupingMode="{ data }"> + <VnSelectEnum + :label="t('Grouping mode')" + v-model="data.groupingMode" + schema="vn" + table="buy" + column="groupingMode" + option-value="groupingMode" + option-label="groupingMode" + /> + </template> + <template #previous-create-dialog="{ data }"> + <div + style="position: absolute" + :class="{ 'centered-container': !data.itemFk }" + > + <ItemDescriptor :id="data.itemFk" v-if="data.itemFk" /> + <div v-else> + <span>{{ t('globals.noData') }}</span> + </div> + </div> + </template> + </VnTable> </template> - -<style lang="scss" scoped> -.q-table--horizontal-separator tbody tr:nth-child(odd) > td { - border-bottom-width: 0px; - border-top-width: 2px; - border-color: var(--vn-text-color); -} -.infoRow > td { - color: var(--vn-label-color); -} -</style> - <i18n> es: - Import buys: Importar compras - Buy deleted: Compra eliminada - Buys deleted: Compras eliminadas - Confirm deletion: Confirmar eliminación - Are you sure you want to delete this buy?: Seguro que quieres eliminar esta compra? - Are you sure you want to delete this buys?: Seguro que quieres eliminar estas compras? + Article: Artículo + Siz.: Med. + Size: Medida + Sti.: Eti. + Bucket: Cubo + Quantity: Cantidad + Amount: Importe + Pack.: Paq. + Package: Paquete + Box: Caja + P.Sen: P.Env + Packing sent: Packing envíos + Com.: Ref. + Comment: Referencia + Minimum price: Precio mínimo + Stickers: Etiquetas + Printed Stickers/Stickers: Etiquetas impresas/Etiquetas + Cost: Cost. + Buying value: Coste + Producer: Productor + Company: Compañia + Tags: Etiquetas + Grouping mode: Modo de agrupación + C.min: P.min + Ignore: Ignorar + Ignored for available: Ignorado para disponible + Grouping selector: Selector de grouping + Check min price: Marcar precio mínimo + Create buy: Crear compra + Invert quantity value: Invertir valor de cantidad + Check buy amount: Marcar como correcta la cantidad de compra </i18n> +<style lang="scss" scoped> +.centered-container { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + width: 40%; + height: 100%; +} +</style> diff --git a/src/pages/Entry/Card/EntryCard.vue b/src/pages/Entry/Card/EntryCard.vue index e00623a21..be82289f4 100644 --- a/src/pages/Entry/Card/EntryCard.vue +++ b/src/pages/Entry/Card/EntryCard.vue @@ -1,13 +1,13 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import EntryDescriptor from './EntryDescriptor.vue'; -import filter from './EntryFilter.js' +import filter from './EntryFilter.js'; </script> <template> <VnCardBeta data-key="Entry" - base-url="Entries" + url="Entries" :descriptor="EntryDescriptor" - :user-filter="filter" + :filter="filter" /> </template> diff --git a/src/pages/Entry/Card/EntryDescriptor.vue b/src/pages/Entry/Card/EntryDescriptor.vue index 19d13e51a..69b300cb2 100644 --- a/src/pages/Entry/Card/EntryDescriptor.vue +++ b/src/pages/Entry/Card/EntryDescriptor.vue @@ -1,12 +1,19 @@ <script setup> import { ref, computed, onMounted } from 'vue'; -import { useRoute } from 'vue-router'; +import { useRoute, useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import CardDescriptor from 'components/ui/CardDescriptor.vue'; -import VnLv from 'src/components/ui/VnLv.vue'; import { toDate } from 'src/filters'; import { getUrl } from 'src/composables/getUrl'; -import EntryDescriptorMenu from './EntryDescriptorMenu.vue'; +import { useQuasar } from 'quasar'; +import { usePrintService } from 'composables/usePrintService'; +import CardDescriptor from 'components/ui/CardDescriptor.vue'; +import VnLv from 'src/components/ui/VnLv.vue'; +import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; +import axios from 'axios'; + +const quasar = useQuasar(); +const { push } = useRouter(); +const { openReport } = usePrintService(); const $props = defineProps({ id: { @@ -83,12 +90,63 @@ const getEntryRedirectionFilter = (entry) => { to, }); }; + +function showEntryReport() { + openReport(`Entries/${entityId.value}/entry-order-pdf`); +} + +function showNotification(type, message) { + quasar.notify({ + type: type, + message: t(message), + }); +} + +async function recalculateRates(entity) { + try { + const entryConfig = await axios.get('EntryConfigs/findOne'); + if (entryConfig.data?.inventorySupplierFk === entity.supplierFk) { + showNotification( + 'negative', + 'Cannot recalculate prices because this is an inventory entry', + ); + return; + } + + await axios.post(`Entries/${entityId.value}/recalcEntryPrices`); + showNotification('positive', 'Entry prices recalculated'); + } catch (error) { + showNotification('negative', 'Failed to recalculate rates'); + console.error(error); + } +} + +async function cloneEntry() { + try { + const response = await axios.post(`Entries/${entityId.value}/cloneEntry`); + push({ path: `/entry/${response.data}` }); + showNotification('positive', 'Entry cloned'); + } catch (error) { + showNotification('negative', 'Failed to clone entry'); + console.error(error); + } +} + +async function deleteEntry() { + try { + await axios.post(`Entries/${entityId.value}/deleteEntry`); + push({ path: `/entry/list` }); + showNotification('positive', 'Entry deleted'); + } catch (error) { + showNotification('negative', 'Failed to delete entry'); + console.error(error); + } +} </script> <template> <CardDescriptor ref="entryDescriptorRef" - module="Entry" :url="`Entries/${entityId}`" :userFilter="entryFilter" title="supplier.nickname" @@ -96,15 +154,56 @@ const getEntryRedirectionFilter = (entry) => { width="lg-width" > <template #menu="{ entity }"> - <EntryDescriptorMenu :id="entity.id" /> + <QItem + v-ripple + clickable + @click="showEntryReport(entity)" + data-cy="show-entry-report" + > + <QItemSection>{{ t('Show entry report') }}</QItemSection> + </QItem> + <QItem + v-ripple + clickable + @click="recalculateRates(entity)" + data-cy="recalculate-rates" + > + <QItemSection>{{ t('Recalculate rates') }}</QItemSection> + </QItem> + <QItem v-ripple clickable @click="cloneEntry(entity)" data-cy="clone-entry"> + <QItemSection>{{ t('Clone') }}</QItemSection> + </QItem> + <QItem v-ripple clickable @click="deleteEntry(entity)" data-cy="delete-entry"> + <QItemSection>{{ t('Delete') }}</QItemSection> + </QItem> </template> <template #body="{ entity }"> - <VnLv :label="t('globals.agency')" :value="entity.travel?.agency?.name" /> - <VnLv :label="t('shipped')" :value="toDate(entity.travel?.shipped)" /> - <VnLv :label="t('landed')" :value="toDate(entity.travel?.landed)" /> + <VnLv :label="t('Travel')"> + <template #value> + <span class="link" v-if="entity?.travelFk"> + {{ entity.travel?.agency?.name }} + {{ entity.travel?.warehouseOut?.code }} → + {{ entity.travel?.warehouseIn?.code }} + <TravelDescriptorProxy :id="entity?.travelFk" /> + </span> + </template> + </VnLv> <VnLv - :label="t('globals.warehouseOut')" - :value="entity.travel?.warehouseOut?.name" + :label="t('entry.summary.travelShipped')" + :value="toDate(entity.travel?.shipped)" + /> + <VnLv + :label="t('entry.summary.travelLanded')" + :value="toDate(entity.travel?.landed)" + /> + <VnLv :label="t('entry.summary.currency')" :value="entity?.currency?.code" /> + <VnLv + :label="t('entry.summary.invoiceAmount')" + :value="entity?.invoiceAmount" + /> + <VnLv + :label="t('entry.summary.entryType')" + :value="entity?.entryType?.description" /> </template> <template #icons="{ entity }"> @@ -131,6 +230,14 @@ const getEntryRedirectionFilter = (entry) => { }}</QTooltip > </QIcon> + <QIcon + v-if="!entity?.travelFk" + name="vn:deletedTicket" + size="xs" + color="primary" + > + <QTooltip>{{ t('This entry is deleted') }}</QTooltip> + </QIcon> </QCardActions> </template> <template #actions="{ entity }"> @@ -143,21 +250,6 @@ const getEntryRedirectionFilter = (entry) => { > <QTooltip>{{ t('Supplier card') }}</QTooltip> </QBtn> - <QBtn - :to="{ - name: 'TravelMain', - query: { - params: JSON.stringify({ - agencyModeFk: entity.travel?.agencyModeFk, - }), - }, - }" - size="md" - icon="local_airport" - color="primary" - > - <QTooltip>{{ t('All travels with current agency') }}</QTooltip> - </QBtn> <QBtn :to="{ name: 'EntryMain', @@ -177,10 +269,24 @@ const getEntryRedirectionFilter = (entry) => { </template> <i18n> es: + Travel: Envío Supplier card: Ficha del proveedor All travels with current agency: Todos los envíos con la agencia actual All entries with current supplier: Todas las entradas con el proveedor actual Show entry report: Ver informe del pedido Inventory entry: Es inventario Virtual entry: Es una redada + shipped: Enviado + landed: Recibido + This entry is deleted: Esta entrada está eliminada + Cannot recalculate prices because this is an inventory entry: No se pueden recalcular los precios porque es una entrada de inventario + Entry deleted: Entrada eliminada + Entry cloned: Entrada clonada + Entry prices recalculated: Precios de la entrada recalculados + Failed to recalculate rates: No se pudieron recalcular las tarifas + Failed to clone entry: No se pudo clonar la entrada + Failed to delete entry: No se pudo eliminar la entrada + Recalculate rates: Recalcular tarifas + Clone: Clonar + Delete: Eliminar </i18n> diff --git a/src/pages/Entry/Card/EntryFilter.js b/src/pages/Entry/Card/EntryFilter.js index 3ff62cf27..d9fd1c2be 100644 --- a/src/pages/Entry/Card/EntryFilter.js +++ b/src/pages/Entry/Card/EntryFilter.js @@ -9,6 +9,7 @@ export default { 'shipped', 'agencyModeFk', 'warehouseOutFk', + 'warehouseInFk', 'daysInForward', ], include: [ @@ -21,13 +22,13 @@ export default { { relation: 'warehouseOut', scope: { - fields: ['name'], + fields: ['name', 'code'], }, }, { relation: 'warehouseIn', scope: { - fields: ['name'], + fields: ['name', 'code'], }, }, ], @@ -39,5 +40,17 @@ export default { fields: ['id', 'nickname'], }, }, + { + relation: 'currency', + scope: { + fields: ['id', 'code'], + }, + }, + { + relation: 'entryType', + scope: { + fields: ['code', 'description'], + }, + }, ], }; diff --git a/src/pages/Entry/Card/EntryNotes.vue b/src/pages/Entry/Card/EntryNotes.vue index 55cac0437..459c3b069 100644 --- a/src/pages/Entry/Card/EntryNotes.vue +++ b/src/pages/Entry/Card/EntryNotes.vue @@ -17,7 +17,7 @@ const selected = ref([]); const sortEntryObservationOptions = (data) => { entryObservationsOptions.value = [...data].sort((a, b) => - a.description.localeCompare(b.description) + a.description.localeCompare(b.description), ); }; @@ -142,7 +142,7 @@ const columns = computed(() => [ fab color="primary" icon="add" - shortcut="+" + v-shortcut="'+'" @click="entryObservationsRef.insert()" /> </QPageSticky> diff --git a/src/pages/Entry/Card/EntrySummary.vue b/src/pages/Entry/Card/EntrySummary.vue index 8c46fb6e6..c40e2ba46 100644 --- a/src/pages/Entry/Card/EntrySummary.vue +++ b/src/pages/Entry/Card/EntrySummary.vue @@ -2,19 +2,17 @@ import { onMounted, ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; +import { toDate } from 'src/filters'; +import { getUrl } from 'src/composables/getUrl'; +import axios from 'axios'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; - -import { toDate, toCurrency, toCelsius } from 'src/filters'; -import { getUrl } from 'src/composables/getUrl'; -import axios from 'axios'; -import FetchedTags from 'src/components/ui/FetchedTags.vue'; -import VnToSummary from 'src/components/ui/VnToSummary.vue'; -import EntryDescriptorMenu from './EntryDescriptorMenu.vue'; -import VnRow from 'src/components/ui/VnRow.vue'; +import EntryBuys from './EntryBuys.vue'; import VnTitle from 'src/components/common/VnTitle.vue'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; +import VnToSummary from 'src/components/ui/VnToSummary.vue'; const route = useRoute(); const { t } = useI18n(); @@ -33,117 +31,6 @@ const entry = ref(); const entryBuys = ref([]); const entryUrl = ref(); -onMounted(async () => { - entryUrl.value = (await getUrl('entry/')) + entityId.value; -}); - -const tableColumnComponents = { - quantity: { - component: () => 'span', - props: () => {}, - }, - stickers: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - packagingFk: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - weight: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - packing: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - grouping: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - buyingValue: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - amount: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, - pvp: { - component: () => 'span', - props: () => {}, - event: () => {}, - }, -}; - -const entriesTableColumns = computed(() => { - return [ - { - label: t('globals.quantity'), - field: 'quantity', - name: 'quantity', - align: 'left', - }, - { - label: t('entry.summary.stickers'), - field: 'stickers', - name: 'stickers', - align: 'left', - }, - { - label: t('entry.summary.package'), - field: 'packagingFk', - name: 'packagingFk', - align: 'left', - }, - { - label: t('globals.weight'), - field: 'weight', - name: 'weight', - align: 'left', - }, - { - label: t('entry.summary.packing'), - field: 'packing', - name: 'packing', - align: 'left', - }, - { - label: t('entry.summary.grouping'), - field: 'grouping', - name: 'grouping', - align: 'left', - }, - { - label: t('entry.summary.buyingValue'), - field: 'buyingValue', - name: 'buyingValue', - align: 'left', - format: (value) => toCurrency(value), - }, - { - label: t('entry.summary.import'), - name: 'amount', - align: 'left', - format: (_, row) => toCurrency(row.buyingValue * row.quantity), - }, - { - label: t('entry.summary.pvp'), - name: 'pvp', - align: 'left', - format: (_, row) => toCurrency(row.price2) + ' / ' + toCurrency(row.price3), - }, - ]; -}); - async function setEntryData(data) { if (data) entry.value = data; await fetchEntryBuys(); @@ -153,14 +40,18 @@ const fetchEntryBuys = async () => { const { data } = await axios.get(`Entries/${entry.value.id}/getBuys`); if (data) entryBuys.value = data; }; -</script> +onMounted(async () => { + entryUrl.value = (await getUrl('entry/')) + entityId.value; +}); +</script> <template> <CardSummary ref="summaryRef" :url="`Entries/${entityId}/getEntry`" @on-fetch="(data) => setEntryData(data)" data-key="EntrySummary" + data-cy="entry-summary" > <template #header-left> <VnToSummary @@ -173,159 +64,154 @@ const fetchEntryBuys = async () => { <template #header> <span>{{ entry.id }} - {{ entry.supplier.nickname }}</span> </template> - <template #menu="{ entity }"> - <EntryDescriptorMenu :id="entity.id" /> - </template> <template #body> <QCard class="vn-one"> <VnTitle :url="`#/entry/${entityId}/basic-data`" :text="t('globals.summary.basicData')" /> - <VnLv :label="t('entry.summary.commission')" :value="entry.commission" /> - <VnLv - :label="t('entry.summary.currency')" - :value="entry.currency?.name" - /> - <VnLv :label="t('globals.company')" :value="entry.company.code" /> - <VnLv :label="t('globals.reference')" :value="entry.reference" /> - <VnLv - :label="t('entry.summary.invoiceNumber')" - :value="entry.invoiceNumber" - /> - <VnLv - :label="t('entry.basicData.initialTemperature')" - :value="toCelsius(entry.initialTemperature)" - /> - <VnLv - :label="t('entry.basicData.finalTemperature')" - :value="toCelsius(entry.finalTemperature)" - /> + <div class="card-group"> + <div class="card-content"> + <VnLv + :label="t('entry.summary.commission')" + :value="entry?.commission" + /> + <VnLv + :label="t('entry.summary.currency')" + :value="entry?.currency?.name" + /> + <VnLv + :label="t('globals.company')" + :value="entry?.company?.code" + /> + <VnLv :label="t('globals.reference')" :value="entry?.reference" /> + <VnLv + :label="t('entry.summary.invoiceNumber')" + :value="entry?.invoiceNumber" + /> + </div> + <div class="card-content"> + <VnCheckbox + :label="t('entry.summary.ordered')" + v-model="entry.isOrdered" + :disable="true" + size="xs" + /> + <VnCheckbox + :label="t('globals.confirmed')" + v-model="entry.isConfirmed" + :disable="true" + size="xs" + /> + <VnCheckbox + :label="t('entry.summary.booked')" + v-model="entry.isBooked" + :disable="true" + size="xs" + /> + <VnCheckbox + :label="t('entry.summary.excludedFromAvailable')" + v-model="entry.isExcludedFromAvailable" + :disable="true" + size="xs" + /> + </div> + </div> </QCard> - <QCard class="vn-one"> + <QCard class="vn-one" v-if="entry?.travelFk"> <VnTitle - :url="`#/entry/${entityId}/basic-data`" - :text="t('globals.summary.basicData')" + :url="`#/travel/${entry.travel.id}/summary`" + :text="t('Travel')" /> - <VnLv :label="t('entry.summary.travelReference')"> - <template #value> - <span class="link"> - {{ entry.travel.ref }} - <TravelDescriptorProxy :id="entry.travel.id" /> - </span> - </template> - </VnLv> - <VnLv - :label="t('entry.summary.travelAgency')" - :value="entry.travel.agency?.name" - /> - <VnLv - :label="t('globals.shipped')" - :value="toDate(entry.travel.shipped)" - /> - <VnLv - :label="t('globals.warehouseOut')" - :value="entry.travel.warehouseOut?.name" - /> - <VnLv - :label="t('entry.summary.travelDelivered')" - :value="entry.travel.isDelivered" - /> - <VnLv :label="t('globals.landed')" :value="toDate(entry.travel.landed)" /> - <VnLv - :label="t('globals.warehouseIn')" - :value="entry.travel.warehouseIn?.name" - /> - <VnLv - :label="t('entry.summary.travelReceived')" - :value="entry.travel.isReceived" - /> - </QCard> - <QCard class="vn-one"> - <VnTitle :url="`#/travel/${entityId}/summary`" :text="t('Travel data')" /> - <VnRow class="block"> - <VnLv :label="t('entry.summary.ordered')" :value="entry.isOrdered" /> - <VnLv :label="t('globals.confirmed')" :value="entry.isConfirmed" /> - <VnLv :label="t('entry.summary.booked')" :value="entry.isBooked" /> - <VnLv - :label="t('entry.summary.excludedFromAvailable')" - :value="entry.isExcludedFromAvailable" - /> - </VnRow> + <div class="card-group"> + <div class="card-content"> + <VnLv :label="t('entry.summary.travelReference')"> + <template #value> + <span class="link"> + {{ entry.travel.ref }} + <TravelDescriptorProxy :id="entry.travel.id" /> + </span> + </template> + </VnLv> + <VnLv + :label="t('entry.summary.travelAgency')" + :value="entry.travel.agency?.name" + /> + <VnLv + :label="t('entry.summary.travelShipped')" + :value="toDate(entry.travel.shipped)" + /> + <VnLv + :label="t('globals.warehouseOut')" + :value="entry.travel.warehouseOut?.name" + /> + <VnLv + :label="t('entry.summary.travelLanded')" + :value="toDate(entry.travel.landed)" + /> + <VnLv + :label="t('globals.warehouseIn')" + :value="entry.travel.warehouseIn?.name" + /> + </div> + <div class="card-content"> + <VnCheckbox + :label="t('entry.summary.travelDelivered')" + v-model="entry.travel.isDelivered" + :disable="true" + size="xs" + /> + <VnCheckbox + :label="t('entry.summary.travelReceived')" + v-model="entry.travel.isReceived" + :disable="true" + size="xs" + /> + </div> + </div> </QCard> <QCard class="vn-max"> <VnTitle :url="`#/entry/${entityId}/buys`" :text="t('entry.summary.buys')" /> - <QTable - :rows="entryBuys" - :columns="entriesTableColumns" - row-key="index" - class="full-width q-mt-md" - :no-data-label="t('globals.noResults')" - > - <template #body="{ cols, row, rowIndex }"> - <QTr no-hover> - <QTd v-for="col in cols" :key="col?.name"> - <component - :is="tableColumnComponents[col?.name].component()" - v-bind="tableColumnComponents[col?.name].props()" - @click="tableColumnComponents[col?.name].event()" - class="col-content" - > - <template - v-if=" - col?.name !== 'observation' && - col?.name !== 'isConfirmed' - " - >{{ col.value }}</template - > - <QTooltip v-if="col.toolTip">{{ - col.toolTip - }}</QTooltip> - </component> - </QTd> - </QTr> - <QTr no-hover> - <QTd> - <span>{{ row.item.itemType.code }}</span> - </QTd> - <QTd> - <span>{{ row.item.id }}</span> - </QTd> - <QTd> - <span>{{ row.item.size }}</span> - </QTd> - <QTd> - <span>{{ toCurrency(row.item.minPrice) }}</span> - </QTd> - <QTd colspan="6"> - <span>{{ row.item.concept }}</span> - <span v-if="row.item.subName" class="subName"> - {{ row.item.subName }} - </span> - <FetchedTags :item="row.item" /> - </QTd> - </QTr> - <!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys --> - <QTr v-if="rowIndex !== entryBuys.length - 1"> - <QTd colspan="10" class="vn-table-separation-row" /> - </QTr> - </template> - </QTable> + <EntryBuys + v-if="entityId" + :id="Number(entityId)" + :editable-mode="false" + table-height="49vh" + /> </QCard> </template> </CardSummary> </template> - <style lang="scss" scoped> -.separation-row { - background-color: var(--vn-section-color) !important; +.card-group { + display: flex; + flex-direction: column; +} + +.card-content { + display: flex; + flex-direction: column; + text-overflow: ellipsis; + > div { + max-height: 24px; + } +} + +@media (min-width: 1010px) { + .card-group { + flex-direction: row; + } + .card-content { + flex: 1; + margin-right: 16px; + } } </style> - <i18n> es: - Travel data: Datos envío + Travel: Envío + InvoiceIn data: Datos factura </i18n> diff --git a/src/pages/Entry/EntryFilter.vue b/src/pages/Entry/EntryFilter.vue index 0f632c0ef..8c60918a8 100644 --- a/src/pages/Entry/EntryFilter.vue +++ b/src/pages/Entry/EntryFilter.vue @@ -19,6 +19,7 @@ const props = defineProps({ const currenciesOptions = ref([]); const companiesOptions = ref([]); +const entryFilterPanel = ref(); </script> <template> @@ -38,7 +39,7 @@ const companiesOptions = ref([]); @on-fetch="(data) => (currenciesOptions = data)" auto-load /> - <VnFilterPanel :data-key="props.dataKey" :search-button="true"> + <VnFilterPanel ref="entryFilterPanel" :data-key="props.dataKey" :search-button="true"> <template #tags="{ tag, formatFn }"> <div class="q-gutter-x-xs"> <strong>{{ t(`entryFilter.params.${tag.label}`) }}: </strong> @@ -48,70 +49,65 @@ const companiesOptions = ref([]); <template #body="{ params, searchFn }"> <QItem> <QItemSection> - <VnInput - v-model="params.search" - :label="t('entryFilter.params.search')" - is-outlined - /> + <QCheckbox + :label="t('params.isExcludedFromAvailable')" + v-model="params.isExcludedFromAvailable" + toggle-indeterminate + > + <QTooltip> + {{ t('params.isExcludedFromAvailable') }} + </QTooltip> + </QCheckbox> + </QItemSection> + <QItemSection> + <QCheckbox + :label="t('params.isOrdered')" + v-model="params.isOrdered" + toggle-indeterminate + > + <QTooltip> + {{ t('entry.list.tableVisibleColumns.isOrdered') }} + </QTooltip> + </QCheckbox> </QItemSection> </QItem> <QItem> <QItemSection> - <VnInput - v-model="params.reference" - :label="t('entryFilter.params.reference')" - is-outlined - /> + <QCheckbox + :label="t('params.isReceived')" + v-model="params.isReceived" + toggle-indeterminate + > + <QTooltip> + {{ t('entry.list.tableVisibleColumns.isReceived') }} + </QTooltip> + </QCheckbox> + </QItemSection> + <QItemSection> + <QCheckbox + :label="t('entry.list.tableVisibleColumns.isConfirmed')" + v-model="params.isConfirmed" + toggle-indeterminate + > + <QTooltip> + {{ t('entry.list.tableVisibleColumns.isConfirmed') }} + </QTooltip> + </QCheckbox> </QItemSection> </QItem> <QItem> <QItemSection> - <VnInput - v-model="params.invoiceNumber" - :label="t('entryFilter.params.invoiceNumber')" - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput - v-model="params.travelFk" - :label="t('entryFilter.params.travelFk')" - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - :label="t('entryFilter.params.companyFk')" - v-model="params.companyFk" + <VnInputDate + :label="t('params.landed')" + v-model="params.landed" @update:model-value="searchFn()" - :options="companiesOptions" - option-value="id" - option-label="code" - hide-selected - dense - outlined - rounded + is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <VnSelect - :label="t('entryFilter.params.currencyFk')" - v-model="params.currencyFk" - @update:model-value="searchFn()" - :options="currenciesOptions" - option-value="id" - option-label="name" - hide-selected - dense - outlined - rounded - /> + <VnInput v-model="params.id" label="Id" is-outlined /> </QItemSection> </QItem> <QItem> @@ -125,62 +121,165 @@ const companiesOptions = ref([]); rounded /> </QItemSection> - </QItem> - <QItem> <QItemSection> - <VnInputDate - :label="t('entryFilter.params.created')" - v-model="params.created" - @update:model-value="searchFn()" + <VnInput + v-model="params.invoiceNumber" + :label="t('params.invoiceNumber')" is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <VnInputDate - :label="t('entryFilter.params.from')" - v-model="params.from" - @update:model-value="searchFn()" + <VnInput + v-model="params.reference" + :label="t('entry.list.tableVisibleColumns.reference')" is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <VnInputDate - :label="t('entryFilter.params.to')" - v-model="params.to" + <VnSelect + :label="t('params.agencyModeId')" + v-model="params.agencyModeId" @update:model-value="searchFn()" + url="AgencyModes" + :fields="['id', 'name']" + hide-selected + dense + outlined + rounded + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.evaNotes" + :label="t('params.evaNotes')" is-outlined /> </QItemSection> </QItem> <QItem> <QItemSection> - <QCheckbox - :label="t('entryFilter.params.isBooked')" - v-model="params.isBooked" - toggle-indeterminate - /> - </QItemSection> - <QItemSection> - <QCheckbox - :label="t('entryFilter.params.isConfirmed')" - v-model="params.isConfirmed" - toggle-indeterminate + <VnSelect + :label="t('params.warehouseOutFk')" + v-model="params.warehouseOutFk" + @update:model-value="searchFn()" + url="Warehouses" + :fields="['id', 'name']" + hide-selected + dense + outlined + rounded /> </QItemSection> </QItem> <QItem> <QItemSection> - <QCheckbox - :label="t('entryFilter.params.isOrdered')" - v-model="params.isOrdered" - toggle-indeterminate + <VnSelect + :label="t('params.warehouseInFk')" + v-model="params.warehouseInFk" + @update:model-value="searchFn()" + url="Warehouses" + :fields="['id', 'name']" + hide-selected + dense + outlined + rounded + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.name }} + </QItemLabel> + <QItemLabel caption> + {{ `#${scope.opt?.id} , ${scope.opt?.nickname}` }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.invoiceNumber" + :label="t('params.invoiceNumber')" + is-outlined + /> + </QItemSection> + </QItem> + + <QItem> + <QItemSection> + <VnSelect + :label="t('params.entryTypeCode')" + v-model="params.entryTypeCode" + @update:model-value="searchFn()" + url="EntryTypes" + :fields="['code', 'description']" + option-value="code" + option-label="description" + hide-selected + dense + outlined + rounded + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.evaNotes" + :label="t('params.evaNotes')" + is-outlined /> </QItemSection> </QItem> </template> </VnFilterPanel> </template> + +<i18n> +en: + params: + isExcludedFromAvailable: Inventory + isOrdered: Ordered + isReceived: Received + isConfirmed: Confirmed + isRaid: Raid + landed: Date + id: Id + supplierFk: Supplier + invoiceNumber: Invoice number + reference: Ref/Alb/Guide + agencyModeId: Agency mode + evaNotes: Notes + warehouseOutFk: Origin + warehouseInFk: Destiny + entryTypeCode: Entry type + hasToShowDeletedEntries: Show deleted entries +es: + params: + isExcludedFromAvailable: Inventario + isOrdered: Pedida + isConfirmed: Confirmado + isReceived: Recibida + isRaid: Raid + landed: Fecha + id: Id + supplierFk: Proveedor + invoiceNumber: Núm. factura + reference: Ref/Alb/Guía + agencyModeId: Modo agencia + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeCode: Tipo de entrada + hasToShowDeletedEntries: Mostrar entradas eliminadas +</i18n> diff --git a/src/pages/Entry/EntryList.vue b/src/pages/Entry/EntryList.vue index 3172c6d0e..3c96a2302 100644 --- a/src/pages/Entry/EntryList.vue +++ b/src/pages/Entry/EntryList.vue @@ -1,21 +1,25 @@ <script setup> +import axios from 'axios'; +import VnSection from 'src/components/common/VnSection.vue'; import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useState } from 'src/composables/useState'; +import { onBeforeMount } from 'vue'; + import EntryFilter from './EntryFilter.vue'; import VnTable from 'components/VnTable/VnTable.vue'; -import { toCelsius, toDate } from 'src/filters'; -import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import EntrySummary from './Card/EntrySummary.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; -import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; -import VnSection from 'src/components/common/VnSection.vue'; +import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue'; +import { toDate } from 'src/filters'; const { t } = useI18n(); const tableRef = ref(); +const defaultEntry = ref({}); +const state = useState(); +const user = state.getUser(); const dataKey = 'EntryList'; -const { viewSummary } = useSummaryDialog(); -const entryFilter = { +const entryQueryFilter = { include: [ { relation: 'suppliers', @@ -40,44 +44,58 @@ const entryFilter = { const columns = computed(() => [ { - name: 'status', - columnFilter: false, + labelAbbreviation: 'Ex', + label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), + toolTip: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), + name: 'isExcludedFromAvailable', + component: 'checkbox', + width: '35px', }, { - align: 'left', - label: t('globals.id'), - name: 'id', - isId: true, - chip: { - condition: () => true, - }, + labelAbbreviation: 'Pe', + label: t('entry.list.tableVisibleColumns.isOrdered'), + toolTip: t('entry.list.tableVisibleColumns.isOrdered'), + name: 'isOrdered', + component: 'checkbox', + width: '35px', }, { - align: 'left', - label: t('globals.reference'), - name: 'reference', - isTitle: true, - component: 'input', - columnField: { - component: null, - }, - create: true, - cardVisible: true, + labelAbbreviation: 'LE', + label: t('entry.list.tableVisibleColumns.isConfirmed'), + toolTip: t('entry.list.tableVisibleColumns.isConfirmed'), + name: 'isConfirmed', + component: 'checkbox', + width: '35px', }, { - align: 'left', - label: t('entry.list.tableVisibleColumns.created'), - name: 'created', - create: true, - cardVisible: true, + labelAbbreviation: 'Re', + label: t('entry.list.tableVisibleColumns.isReceived'), + toolTip: t('entry.list.tableVisibleColumns.isReceived'), + name: 'isReceived', + component: 'checkbox', + width: '35px', + }, + { + label: t('entry.list.tableVisibleColumns.landed'), + name: 'landed', component: 'date', columnField: { component: null, }, - format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.created)), + format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.landed)), + width: '105px', + }, + { + label: t('globals.id'), + name: 'id', + isId: true, + component: 'number', + chip: { + condition: () => true, + }, + width: '50px', }, { - align: 'left', label: t('entry.list.tableVisibleColumns.supplierFk'), name: 'supplierFk', create: true, @@ -86,165 +104,213 @@ const columns = computed(() => [ attrs: { url: 'suppliers', fields: ['id', 'name'], - }, - columnField: { - component: null, + where: { order: 'name DESC' }, }, format: (row, dashIfEmpty) => dashIfEmpty(row.supplierName), + width: '110px', }, { align: 'left', - label: t('entry.list.tableVisibleColumns.isBooked'), - name: 'isBooked', + label: t('entry.list.tableVisibleColumns.invoiceNumber'), + name: 'invoiceNumber', + component: 'input', + }, + { + align: 'left', + label: t('entry.list.tableVisibleColumns.reference'), + name: 'reference', + isTitle: true, + component: 'input', + columnField: { + component: null, + }, cardVisible: true, - create: true, - component: 'checkbox', }, { align: 'left', - label: t('entry.list.tableVisibleColumns.isConfirmed'), - name: 'isConfirmed', + label: 'AWB', + name: 'awbCode', + component: 'input', + width: '100px', + }, + { + align: 'left', + label: t('entry.list.tableVisibleColumns.agencyModeId'), + name: 'agencyModeId', cardVisible: true, - create: true, - component: 'checkbox', + component: 'select', + attrs: { + url: 'agencyModes', + fields: ['id', 'name'], + }, + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row.agencyModeName), }, { align: 'left', - label: t('entry.list.tableVisibleColumns.isOrdered'), - name: 'isOrdered', + label: t('entry.list.tableVisibleColumns.evaNotes'), + name: 'evaNotes', + component: 'input', + }, + { + align: 'left', + label: t('entry.list.tableVisibleColumns.warehouseOutFk'), + name: 'warehouseOutFk', cardVisible: true, - create: true, - component: 'checkbox', + component: 'select', + attrs: { + url: 'warehouses', + fields: ['id', 'name'], + }, + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row.warehouseOutName), + width: '65px', }, { align: 'left', - label: t('entry.list.tableVisibleColumns.companyFk'), + label: t('entry.list.tableVisibleColumns.warehouseInFk'), + name: 'warehouseInFk', + cardVisible: true, + component: 'select', + attrs: { + url: 'warehouses', + fields: ['id', 'name'], + }, + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row.warehouseInName), + width: '65px', + }, + { + align: 'left', + labelAbbreviation: t('Type'), + label: t('entry.list.tableVisibleColumns.entryTypeDescription'), + toolTip: t('entry.list.tableVisibleColumns.entryTypeDescription'), + name: 'entryTypeCode', + component: 'select', + attrs: { + url: 'entryTypes', + fields: ['code', 'description'], + optionValue: 'code', + optionLabel: 'description', + }, + width: '65px', + format: (row, dashIfEmpty) => dashIfEmpty(row.entryTypeDescription), + }, + { name: 'companyFk', + label: t('entry.list.tableVisibleColumns.companyFk'), + cardVisible: false, + visible: false, + create: true, component: 'select', attrs: { - url: 'companies', - fields: ['id', 'code'], + optionValue: 'id', optionLabel: 'code', - optionValue: 'id', + url: 'Companies', }, - columnField: { - component: null, - }, - create: true, - - format: (row, dashIfEmpty) => dashIfEmpty(row.companyCode), }, { - align: 'left', - label: t('entry.list.tableVisibleColumns.travelFk'), name: 'travelFk', - component: 'select', - attrs: { - url: 'travels', - fields: ['id', 'ref'], - optionLabel: 'ref', - optionValue: 'id', - }, - columnField: { - component: null, - }, + label: t('entry.list.tableVisibleColumns.travelFk'), + cardVisible: false, + visible: false, create: true, - format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), - }, - { - align: 'left', - label: t('entry.list.tableVisibleColumns.invoiceAmount'), - name: 'invoiceAmount', - cardVisible: true, - }, - { - align: 'left', - name: 'initialTemperature', - label: t('entry.basicData.initialTemperature'), - field: 'initialTemperature', - format: (row) => toCelsius(row.initialTemperature), - }, - { - align: 'left', - name: 'finalTemperature', - label: t('entry.basicData.finalTemperature'), - field: 'finalTemperature', - format: (row) => toCelsius(row.finalTemperature), - }, - { - label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), - name: 'isExcludedFromAvailable', - columnFilter: { - inWhere: true, - }, - }, - { - align: 'right', - name: 'tableActions', - actions: [ - { - title: t('components.smartCard.viewSummary'), - icon: 'preview', - action: (row) => viewSummary(row.id, EntrySummary), - isPrimary: true, - }, - ], }, ]); +function getBadgeAttrs(row) { + const date = row.landed; + let today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + let timeTicket = new Date(date); + timeTicket.setHours(0, 0, 0, 0); + + let timeDiff = today - timeTicket; + + if (timeDiff > 0) return { color: 'info', 'text-color': 'black' }; + if (timeDiff < 0) return { color: 'warning', 'text-color': 'black' }; + switch (row.entryTypeCode) { + case 'regularization': + case 'life': + case 'internal': + case 'inventory': + if (!row.isOrdered || !row.isConfirmed) + return { color: 'negative', 'text-color': 'black' }; + break; + case 'product': + case 'packaging': + case 'devaluation': + case 'payment': + case 'transport': + if ( + row.invoiceAmount === null || + (row.invoiceNumber === null && row.reference === null) || + !row.isOrdered || + !row.isConfirmed + ) + return { color: 'negative', 'text-color': 'black' }; + break; + default: + break; + } + return { color: 'transparent' }; +} + +onBeforeMount(async () => { + defaultEntry.value = (await axios.get('EntryConfigs/findOne')).data; +}); </script> <template> <VnSection :data-key="dataKey" - :columns="columns" prefix="entry" url="Entries/filter" :array-data-props="{ url: 'Entries/filter', - order: 'id DESC', - userFilter: entryFilter, + order: 'landed DESC', + userFilter: EntryFilter, }" > <template #advanced-menu> - <EntryFilter data-key="EntryList" /> + <EntryFilter :data-key="dataKey" /> </template> <template #body> <VnTable + v-if="defaultEntry.defaultSupplierFk" ref="tableRef" :data-key="dataKey" + url="Entries/filter" + :filter="entryQueryFilter" + order="landed DESC" :create="{ urlCreate: 'Entries', - title: t('entry.list.newEntry'), + title: t('Create entry'), onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: {}, + formInitialData: { + supplierFk: defaultEntry.defaultSupplierFk, + dated: Date.vnNew(), + companyFk: user?.companyFk, + }, }" :columns="columns" redirect="entry" :right-search="false" > - <template #column-status="{ row }"> - <div class="row q-gutter-xs"> - <QIcon - v-if="!!row.isExcludedFromAvailable" - name="vn:inventory" - color="primary" - > - <QTooltip>{{ - t( - 'entry.list.tableVisibleColumns.isExcludedFromAvailable', - ) - }}</QTooltip> - </QIcon> - <QIcon v-if="!!row.isRaid" name="vn:net" color="primary"> - <QTooltip> - {{ - t('globals.raid', { - daysInForward: row.daysInForward, - }) - }}</QTooltip - > - </QIcon> - </div> + <template #column-landed="{ row }"> + <QBadge + v-if="row?.travelFk" + v-bind="getBadgeAttrs(row)" + class="q-pa-sm" + style="font-size: 14px" + > + {{ toDate(row.landed) }} + </QBadge> </template> <template #column-supplierFk="{ row }"> <span class="link" @click.stop> @@ -252,13 +318,27 @@ const columns = computed(() => [ <SupplierDescriptorProxy :id="row.supplierFk" /> </span> </template> - <template #column-travelFk="{ row }"> - <span class="link" @click.stop> - {{ row.travelRef }} - <TravelDescriptorProxy :id="row.travelFk" /> - </span> + <template #column-create-travelFk="{ data }"> + <VnSelectTravelExtended + :data="data" + v-model="data.travelFk" + :onFilterTravelSelected=" + (data, result) => (data.travelFk = result) + " + data-cy="entry-travel-select" + /> </template> </VnTable> </template> </VnSection> </template> + +<i18n> +es: + Inventory entry: Es inventario + Virtual entry: Es una redada + Search entries: Buscar entradas + You can search by entry reference: Puedes buscar por referencia de la entrada + Create entry: Crear entrada + Type: Tipo +</i18n> diff --git a/src/pages/Entry/EntryStockBought.vue b/src/pages/Entry/EntryStockBought.vue index fa0bdc12e..4bd0fe640 100644 --- a/src/pages/Entry/EntryStockBought.vue +++ b/src/pages/Entry/EntryStockBought.vue @@ -34,18 +34,20 @@ const columns = computed(() => [ label: t('entryStockBought.buyer'), isTitle: true, component: 'select', + isEditable: false, cardVisible: true, create: true, attrs: { url: 'Workers/activeWithInheritedRole', - fields: ['id', 'name'], + fields: ['id', 'name', 'nickname'], where: { role: 'buyer' }, optionFilter: 'firstName', - optionLabel: 'name', + optionLabel: 'nickname', optionValue: 'id', useLike: false, }, columnFilter: false, + width: '70px', }, { align: 'center', @@ -55,6 +57,7 @@ const columns = computed(() => [ create: true, component: 'number', summation: true, + width: '50px', }, { align: 'center', @@ -78,6 +81,7 @@ const columns = computed(() => [ actions: [ { title: t('entryStockBought.viewMoreDetails'), + name: 'searchBtn', icon: 'search', isPrimary: true, action: (row) => { @@ -91,6 +95,7 @@ const columns = computed(() => [ }, }, ], + 'data-cy': 'table-actions', }, ]); @@ -158,7 +163,7 @@ function round(value) { @on-fetch=" (data) => { travel = data.find( - (data) => data.warehouseIn?.code.toLowerCase() === 'vnh' + (data) => data.warehouseIn?.code.toLowerCase() === 'vnh', ); } " @@ -179,6 +184,7 @@ function round(value) { @click="openDialog()" :title="t('entryStockBought.editTravel')" color="primary" + data-cy="edit-travel" /> </div> </VnRow> @@ -239,10 +245,11 @@ function round(value) { table-height="80vh" auto-load :column-search="false" + :without-header="true" > <template #column-workerFk="{ row }"> <span class="link" @click.stop> - {{ row?.worker?.user?.name }} + {{ row?.worker?.user?.nickname }} <WorkerDescriptorProxy :id="row?.workerFk" /> </span> </template> @@ -279,10 +286,11 @@ function round(value) { justify-content: center; } .column { + min-width: 40%; + margin-top: 5%; display: flex; flex-direction: column; align-items: center; - min-width: 35%; } .text-negative { color: $negative !important; diff --git a/src/pages/Entry/EntryStockBoughtDetail.vue b/src/pages/Entry/EntryStockBoughtDetail.vue index 812171825..1a37994d9 100644 --- a/src/pages/Entry/EntryStockBoughtDetail.vue +++ b/src/pages/Entry/EntryStockBoughtDetail.vue @@ -21,7 +21,7 @@ const $props = defineProps({ const customUrl = `StockBoughts/getStockBoughtDetail?workerFk=${$props.workerFk}&dated=${$props.dated}`; const columns = [ { - align: 'left', + align: 'right', label: t('Entry'), name: 'entryFk', isTitle: true, @@ -29,7 +29,7 @@ const columns = [ columnFilter: false, }, { - align: 'left', + align: 'right', name: 'itemFk', label: t('Item'), columnFilter: false, @@ -44,21 +44,21 @@ const columns = [ cardVisible: true, }, { - align: 'left', + align: 'right', name: 'volume', label: t('Volume'), columnFilter: false, cardVisible: true, }, { - align: 'left', + align: 'right', label: t('Packaging'), name: 'packagingFk', columnFilter: false, cardVisible: true, }, { - align: 'left', + align: 'right', label: 'Packing', name: 'packing', columnFilter: false, @@ -73,12 +73,14 @@ const columns = [ ref="tableRef" data-key="StockBoughtsDetail" :url="customUrl" - order="itemName DESC" + order="volume DESC" :columns="columns" :right-search="false" :disable-infinite-scroll="true" :disable-option="{ card: true }" :limit="0" + :without-header="true" + :with-filters="false" auto-load > <template #column-entryFk="{ row }"> @@ -99,16 +101,14 @@ const columns = [ </template> <style lang="css" scoped> .container { - max-width: 50vw; + max-width: 100%; + width: 50%; overflow: auto; justify-content: center; align-items: center; margin: auto; background-color: var(--vn-section-color); - padding: 4px; -} -.container > div > div > .q-table__top.relative-position.row.items-center { - background-color: red !important; + padding: 2%; } </style> <i18n> diff --git a/src/pages/Entry/locale/en.yml b/src/pages/Entry/locale/en.yml index 80f3491a8..88b16cb03 100644 --- a/src/pages/Entry/locale/en.yml +++ b/src/pages/Entry/locale/en.yml @@ -1,21 +1,36 @@ entry: + lock: + title: Lock entry + message: This entry has been locked by {userName} for {time} minutes. Do you want to unlock it? + success: The entry has been locked successfully list: newEntry: New entry tableVisibleColumns: - created: Creation - supplierFk: Supplier - isBooked: Booked - isConfirmed: Confirmed + isExcludedFromAvailable: Exclude from inventory isOrdered: Ordered + isConfirmed: Ready to label + isReceived: Received + isRaid: Raid + landed: Date + supplierFk: Supplier + reference: Ref/Alb/Guide + invoiceNumber: Invoice + agencyModeId: Agency + isBooked: Booked companyFk: Company - travelFk: Travel - isExcludedFromAvailable: Inventory + evaNotes: Notes + warehouseOutFk: Origin + warehouseInFk: Destiny + entryTypeDescription: Entry type invoiceAmount: Import + travelFk: Travel + dated: Dated inventoryEntry: Inventory entry summary: commission: Commission currency: Currency invoiceNumber: Invoice number + invoiceAmount: Invoice amount ordered: Ordered booked: Booked excludedFromAvailable: Inventory @@ -33,6 +48,7 @@ entry: buyingValue: Buying value import: Import pvp: PVP + entryType: Entry type basicData: travel: Travel currency: Currency @@ -69,17 +85,55 @@ entry: landing: Landing isExcludedFromAvailable: Es inventory params: - toShipped: To - fromShipped: From - daysOnward: Days onward - daysAgo: Days ago - warehouseInFk: Warehouse in + isExcludedFromAvailable: Exclude from inventory + isOrdered: Ordered + isConfirmed: Ready to label + isReceived: Received + isIgnored: Ignored + isRaid: Raid + landed: Date + supplierFk: Supplier + reference: Ref/Alb/Guide + invoiceNumber: Invoice + agencyModeId: Agency + isBooked: Booked + companyFk: Company + evaNotes: Notes + warehouseOutFk: Origin + warehouseInFk: Destiny + entryTypeDescription: Entry type + invoiceAmount: Import + travelFk: Travel + dated: Dated + itemFk: Item id + hex: Color + name: Item name + size: Size + stickers: Stickers + packagingFk: Packaging + weight: Kg + groupingMode: Grouping selector + grouping: Grouping + quantity: Quantity + buyingValue: Buying value + price2: Package + price3: Box + minPrice: Minumum price + hasMinPrice: Has minimum price + packingOut: Packing out + comment: Comment + subName: Supplier name + tags: Tags + company_name: Company name + itemTypeFk: Item type + workerFk: Worker id search: Search entries searchInfo: You can search by entry reference descriptorMenu: showEntryReport: Show entry report entryFilter: params: + isExcludedFromAvailable: Exclude from inventory invoiceNumber: Invoice number travelFk: Travel companyFk: Company @@ -91,8 +145,16 @@ entryFilter: isBooked: Booked isConfirmed: Confirmed isOrdered: Ordered + isReceived: Received search: General search reference: Reference + landed: Landed + id: Id + agencyModeId: Agency + evaNotes: Notes + warehouseOutFk: Origin + warehouseInFk: Destiny + entryTypeCode: Entry type myEntries: id: ID landed: Landed diff --git a/src/pages/Entry/locale/es.yml b/src/pages/Entry/locale/es.yml index a5b968016..3025d64cb 100644 --- a/src/pages/Entry/locale/es.yml +++ b/src/pages/Entry/locale/es.yml @@ -1,21 +1,36 @@ entry: + lock: + title: Entrada bloqueada + message: Esta entrada ha sido bloqueada por {userName} hace {time} minutos. ¿Quieres desbloquearla? + success: La entrada ha sido bloqueada correctamente list: newEntry: Nueva entrada tableVisibleColumns: - created: Creación - supplierFk: Proveedor - isBooked: Asentado - isConfirmed: Confirmado + isExcludedFromAvailable: Excluir del inventario isOrdered: Pedida + isConfirmed: Lista para etiquetar + isReceived: Recibida + isRaid: Redada + landed: Fecha + supplierFk: Proveedor + invoiceNumber: Nº Factura + reference: Ref/Alb/Guía + agencyModeId: Agencia + isBooked: Asentado companyFk: Empresa travelFk: Envio - isExcludedFromAvailable: Inventario + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeDescription: Tipo entrada invoiceAmount: Importe + dated: Fecha inventoryEntry: Es inventario summary: commission: Comisión currency: Moneda invoiceNumber: Núm. factura + invoiceAmount: Importe ordered: Pedida booked: Contabilizada excludedFromAvailable: Inventario @@ -34,12 +49,13 @@ entry: buyingValue: Coste import: Importe pvp: PVP + entryType: Tipo entrada basicData: travel: Envío currency: Moneda observation: Observación commission: Comisión - booked: Asentado + booked: Contabilizada excludedFromAvailable: Inventario initialTemperature: Ini °C finalTemperature: Fin °C @@ -69,31 +85,70 @@ entry: packingOut: Embalaje envíos landing: Llegada isExcludedFromAvailable: Es inventario - params: - toShipped: Hasta - fromShipped: Desde - warehouseInFk: Alm. entrada - daysOnward: Días adelante - daysAgo: Días atras - descriptorMenu: - showEntryReport: Ver informe del pedido + search: Buscar entradas searchInfo: Puedes buscar por referencia de entrada + params: + isExcludedFromAvailable: Excluir del inventario + isOrdered: Pedida + isConfirmed: Lista para etiquetar + isReceived: Recibida + isRaid: Redada + isIgnored: Ignorado + landed: Fecha + supplierFk: Proveedor + invoiceNumber: Nº Factura + reference: Ref/Alb/Guía + agencyModeId: Agencia + isBooked: Asentado + companyFk: Empresa + travelFk: Envio + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeDescription: Tipo entrada + invoiceAmount: Importe + dated: Fecha + itemFk: Id artículo + hex: Color + name: Nombre artículo + size: Medida + stickers: Etiquetas + packagingFk: Embalaje + weight: Kg + groupinMode: Selector de grouping + grouping: Grouping + quantity: Quantity + buyingValue: Precio de compra + price2: Paquete + price3: Caja + minPrice: Precio mínimo + hasMinPrice: Tiene precio mínimo + packingOut: Packing out + comment: Referencia + subName: Nombre proveedor + tags: Etiquetas + company_name: Nombre empresa + itemTypeFk: Familia + workerFk: Comprador entryFilter: params: - invoiceNumber: Núm. factura - travelFk: Envío - companyFk: Empresa - currencyFk: Moneda - supplierFk: Proveedor - from: Desde - to: Hasta - created: Fecha creación - isBooked: Asentado - isConfirmed: Confirmado + isExcludedFromAvailable: Inventario isOrdered: Pedida - search: Búsqueda general - reference: Referencia + isConfirmed: Confirmado + isReceived: Recibida + isRaid: Raid + landed: Fecha + id: Id + supplierFk: Proveedor + invoiceNumber: Núm. factura + reference: Ref/Alb/Guía + agencyModeId: Modo agencia + evaNotes: Notas + warehouseOutFk: Origen + warehouseInFk: Destino + entryTypeCode: Tipo de entrada + hasToShowDeletedEntries: Mostrar entradas eliminadas myEntries: id: ID landed: F. llegada diff --git a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue index c01ec4ab4..905ddebb2 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue @@ -125,7 +125,7 @@ function deleteFile(dmsFk) { <VnInput clearable clear-icon="close" - :label="t('Supplier ref')" + :label="t('invoiceIn.supplierRef')" v-model="data.supplierRef" /> </VnRow> @@ -149,6 +149,7 @@ function deleteFile(dmsFk) { option-value="id" option-label="id" :filter-options="['id', 'name']" + data-cy="UnDeductibleVatSelect" > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -215,7 +216,7 @@ function deleteFile(dmsFk) { v-else icon="add_circle" round - shortcut="+" + v-shortcut="'+'" padding="xs" @click=" () => { @@ -310,7 +311,6 @@ function deleteFile(dmsFk) { supplierFk: Supplier es: supplierFk: Proveedor - Supplier ref: Ref. proveedor Expedition date: Fecha expedición Operation date: Fecha operación Undeductible VAT: Iva no deducible diff --git a/src/pages/InvoiceIn/Card/InvoiceInCard.vue b/src/pages/InvoiceIn/Card/InvoiceInCard.vue index 8aa35f4d8..34cc26437 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInCard.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInCard.vue @@ -1,47 +1,18 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import InvoiceInDescriptor from './InvoiceInDescriptor.vue'; +import { onBeforeRouteUpdate } from 'vue-router'; +import { setRectificative } from '../composables/setRectificative'; +import filter from './InvoiceInFilter.js'; -const filter = { - include: [ - { - relation: 'supplier', - scope: { - include: { - relation: 'contacts', - scope: { where: { email: { neq: null } } }, - }, - }, - }, - { relation: 'invoiceInDueDay' }, - { relation: 'company' }, - { relation: 'currency' }, - { - relation: 'dms', - scope: { - fields: [ - 'dmsTypeFk', - 'reference', - 'hardCopyNumber', - 'workerFk', - 'description', - 'hasFile', - 'file', - 'created', - 'companyFk', - 'warehouseFk', - ], - }, - }, - ], -}; +onBeforeRouteUpdate(async (to) => await setRectificative(to)); </script> <template> <VnCardBeta data-key="InvoiceIn" - base-url="InvoiceIns" + url="InvoiceIns" :descriptor="InvoiceInDescriptor" - :user-filter="filter" + :filter="filter" /> </template> diff --git a/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue b/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue index da7bd4426..3843f5bf7 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue @@ -7,6 +7,7 @@ import { toCurrency, toDate } from 'src/filters'; import VnLv from 'src/components/ui/VnLv.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; +import filter from './InvoiceInFilter.js'; import InvoiceInDescriptorMenu from './InvoiceInDescriptorMenu.vue'; const $props = defineProps({ id: { type: Number, default: null } }); @@ -16,33 +17,10 @@ const { t } = useI18n(); const cardDescriptorRef = ref(); const entityId = computed(() => $props.id || +currentRoute.value.params.id); const totalAmount = ref(); - -const filter = { - include: [ - { - relation: 'supplier', - scope: { - include: { - relation: 'contacts', - scope: { - where: { - email: { neq: null }, - }, - }, - }, - }, - }, - { - relation: 'invoiceInDueDay', - }, - { - relation: 'company', - }, - { - relation: 'currency', - }, - ], -}; +const config = ref(); +const cplusRectificationTypes = ref([]); +const siiTypeInvoiceIns = ref([]); +const invoiceCorrectionTypes = ref([]); const invoiceInCorrection = reactive({ correcting: [], corrected: null }); const routes = reactive({ getSupplier: (id) => { @@ -112,7 +90,6 @@ async function setInvoiceCorrection(id) { <template> <CardDescriptor ref="cardDescriptorRef" - module="InvoiceIn" data-key="InvoiceIn" :url="`InvoiceIns/${entityId}`" :filter="filter" diff --git a/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue b/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue index c3ab635c8..8b039ec27 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDescriptorMenu.vue @@ -186,7 +186,7 @@ const createInvoiceInCorrection = async () => { clickable @click="book(entityId)" > - <QItemSection>{{ t('invoiceIn.descriptorMenu.toBook') }}</QItemSection> + <QItemSection>{{ t('invoiceIn.descriptorMenu.book') }}</QItemSection> </QItem> </template> </InvoiceInToBook> @@ -197,7 +197,7 @@ const createInvoiceInCorrection = async () => { @click="triggerMenu('unbook')" > <QItemSection> - {{ t('invoiceIn.descriptorMenu.toUnbook') }} + {{ t('invoiceIn.descriptorMenu.unbook') }} </QItemSection> </QItem> <QItem diff --git a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue index 23387ff74..20cc1cc71 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed } from 'vue'; +import { ref, computed, onBeforeMount } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; @@ -11,6 +11,7 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import useNotify from 'src/composables/useNotify.js'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +import { toCurrency } from 'filters/index'; const route = useRoute(); const { notify } = useNotify(); @@ -24,7 +25,7 @@ const invoiceInFormRef = ref(); const invoiceId = +route.params.id; const filter = { where: { invoiceInFk: invoiceId } }; const areRows = ref(false); - +const totals = ref(); const columns = computed(() => [ { name: 'duedate', @@ -63,6 +64,8 @@ const columns = computed(() => [ }, ]); +const totalAmount = computed(() => getTotal(invoiceInFormRef.value.formData, 'amount')); + const isNotEuro = (code) => code != 'EUR'; async function insert() { @@ -70,6 +73,10 @@ async function insert() { await invoiceInFormRef.value.reload(); notify(t('globals.dataSaved'), 'positive'); } + +onBeforeMount(async () => { + totals.value = (await axios.get(`InvoiceIns/${invoiceId}/getTotals`)).data; +}); </script> <template> <CrudModel @@ -144,7 +151,7 @@ async function insert() { <QTd /> <QTd /> <QTd> - {{ getTotal(rows, 'amount', { currency: 'default' }) }} + {{ toCurrency(totalAmount) }} </QTd> <QTd> <template v-if="isNotEuro(invoiceIn.currency.code)"> @@ -222,10 +229,19 @@ async function insert() { <QBtn color="primary" icon="add" - shortcut="+" + v-shortcut="'+'" size="lg" round - @click="!areRows ? insert() : invoiceInFormRef.insert()" + @click=" + () => { + if (!areRows) insert(); + else + invoiceInFormRef.insert({ + amount: (totals.totalTaxableBase - totalAmount).toFixed(2), + invoiceInFk: invoiceId, + }); + } + " /> </QPageSticky> </template> diff --git a/src/pages/InvoiceIn/Card/InvoiceInFilter.js b/src/pages/InvoiceIn/Card/InvoiceInFilter.js new file mode 100644 index 000000000..6df8b5830 --- /dev/null +++ b/src/pages/InvoiceIn/Card/InvoiceInFilter.js @@ -0,0 +1,33 @@ +export default { + include: [ + { + relation: 'supplier', + scope: { + include: { + relation: 'contacts', + scope: { where: { email: { neq: null } } }, + }, + }, + }, + { relation: 'invoiceInDueDay' }, + { relation: 'company' }, + { relation: 'currency' }, + { + relation: 'dms', + scope: { + fields: [ + 'dmsTypeFk', + 'reference', + 'hardCopyNumber', + 'workerFk', + 'description', + 'hasFile', + 'file', + 'created', + 'companyFk', + 'warehouseFk', + ], + }, + }, + ], +}; diff --git a/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue b/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue index e529ea6cd..6f8642313 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue @@ -218,7 +218,7 @@ const columns = computed(() => [ <QBtn color="primary" icon="add" - shortcut="+" + v-shortcut="'+'" size="lg" round @click="invoiceInFormRef.insert()" diff --git a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue index e546638f2..d358601d3 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue @@ -193,7 +193,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; <InvoiceIntoBook> <template #content="{ book }"> <QBtn - :label="t('To book')" + :label="t('Book')" color="orange-11" text-color="black" @click="book(entityId)" @@ -224,10 +224,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; </span> </template> </VnLv> - <VnLv - :label="t('invoiceIn.list.supplierRef')" - :value="entity.supplierRef" - /> + <VnLv :label="t('invoiceIn.supplierRef')" :value="entity.supplierRef" /> <VnLv :label="t('invoiceIn.summary.currency')" :value="entity.currency?.code" @@ -357,7 +354,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; entity.totals.totalTaxableBaseForeignValue && toCurrency( entity.totals.totalTaxableBaseForeignValue, - currency + currency, ) }}</QTd> </QTr> @@ -392,7 +389,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; entity.totals.totalDueDayForeignValue && toCurrency( entity.totals.totalDueDayForeignValue, - currency + currency, ) }} </QTd> @@ -472,5 +469,5 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; Search invoice: Buscar factura recibida You can search by invoice reference: Puedes buscar por referencia de la factura Totals: Totales - To book: Contabilizar + Book: Contabilizar </i18n> diff --git a/src/pages/InvoiceIn/Card/InvoiceInVat.vue b/src/pages/InvoiceIn/Card/InvoiceInVat.vue index f99e060b8..e77453bc0 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInVat.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInVat.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed } from 'vue'; +import { ref, computed, nextTick } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useArrayData } from 'src/composables/useArrayData'; @@ -25,7 +25,6 @@ const sageTaxTypes = ref([]); const sageTransactionTypes = ref([]); const rowsSelected = ref([]); const invoiceInFormRef = ref(); -const expenseRef = ref(); defineProps({ actionIcon: { @@ -97,6 +96,20 @@ const columns = computed(() => [ }, ]); +const taxableBaseTotal = computed(() => { + return getTotal(invoiceInFormRef.value.formData, 'taxableBase'); +}); + +const taxRateTotal = computed(() => { + return getTotal(invoiceInFormRef.value.formData, null, { + cb: taxRate, + }); +}); + +const combinedTotal = computed(() => { + return +taxableBaseTotal.value + +taxRateTotal.value; +}); + const filter = { fields: [ 'id', @@ -117,7 +130,7 @@ const isNotEuro = (code) => code != 'EUR'; function taxRate(invoiceInTax) { const sageTaxTypeId = invoiceInTax.taxTypeSageFk; const taxRateSelection = sageTaxTypes.value.find( - (transaction) => transaction.id == sageTaxTypeId + (transaction) => transaction.id == sageTaxTypeId, ); const taxTypeSage = taxRateSelection?.rate ?? 0; const taxableBase = invoiceInTax?.taxableBase ?? 0; @@ -125,35 +138,26 @@ function taxRate(invoiceInTax) { return ((taxTypeSage / 100) * taxableBase).toFixed(2); } -function autocompleteExpense(evt, row, col) { +function autocompleteExpense(evt, row, col, ref) { const val = evt.target.value; if (!val) return; const param = isNaN(val) ? row[col.model] : val; const lookup = expenses.value.find( - ({ id }) => id == useAccountShortToStandard(param) + ({ id }) => id == useAccountShortToStandard(param), ); - expenseRef.value.vnSelectDialogRef.vnSelectRef.toggleOption(lookup); + ref.vnSelectDialogRef.vnSelectRef.toggleOption(lookup); } -const taxableBaseTotal = computed(() => { - return getTotal(invoiceInFormRef.value.formData, 'taxableBase', ); -}); - -const taxRateTotal = computed(() => { - return getTotal(invoiceInFormRef.value.formData, null, { - cb: taxRate, +function setCursor(ref) { + nextTick(() => { + const select = ref.vnSelectDialogRef + ? ref.vnSelectDialogRef.vnSelectRef + : ref.vnSelectRef; + select.$el.querySelector('input').setSelectionRange(0, 0); }); -}); - - -const combinedTotal = computed(() => { - return +taxableBaseTotal.value + +taxRateTotal.value; -}); - - - +} </script> <template> <FetchData @@ -191,14 +195,24 @@ const combinedTotal = computed(() => { <template #body-cell-expense="{ row, col }"> <QTd> <VnSelectDialog - ref="expenseRef" + :ref="`expenseRef-${row.$index}`" v-model="row[col.model]" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'name']" :tooltip="t('Create a new expense')" - @keydown.tab="autocompleteExpense($event, row, col)" + @keydown.tab=" + autocompleteExpense( + $event, + row, + col, + $refs[`expenseRef-${row.$index}`], + ) + " + @update:model-value=" + setCursor($refs[`expenseRef-${row.$index}`]) + " > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -214,7 +228,7 @@ const combinedTotal = computed(() => { </QTd> </template> <template #body-cell-taxablebase="{ row }"> - <QTd> + <QTd shrink> <VnInputNumber clear-icon="close" v-model="row.taxableBase" @@ -225,12 +239,16 @@ const combinedTotal = computed(() => { <template #body-cell-sageiva="{ row, col }"> <QTd> <VnSelect + :ref="`sageivaRef-${row.$index}`" v-model="row[col.model]" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'vat']" data-cy="vat-sageiva" + @update:model-value=" + setCursor($refs[`sageivaRef-${row.$index}`]) + " > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -248,11 +266,15 @@ const combinedTotal = computed(() => { <template #body-cell-sagetransaction="{ row, col }"> <QTd> <VnSelect + :ref="`sagetransactionRef-${row.$index}`" v-model="row[col.model]" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'transaction']" + @update:model-value=" + setCursor($refs[`sagetransactionRef-${row.$index}`]) + " > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -270,7 +292,7 @@ const combinedTotal = computed(() => { </QTd> </template> <template #body-cell-foreignvalue="{ row }"> - <QTd> + <QTd shrink> <VnInputNumber :class="{ 'no-pointer-events': !isNotEuro(currency), @@ -283,7 +305,7 @@ const combinedTotal = computed(() => { row.taxableBase = await getExchange( val, row.currencyFk, - invoiceIn.issued + invoiceIn.issued, ); } " @@ -426,7 +448,7 @@ const combinedTotal = computed(() => { color="primary" icon="add" size="lg" - shortcut="+" + v-shortcut="'+'" round @click="invoiceInFormRef.insert()" > diff --git a/src/pages/InvoiceIn/InvoiceInList.vue b/src/pages/InvoiceIn/InvoiceInList.vue index e1723e3b1..0960d0d6c 100644 --- a/src/pages/InvoiceIn/InvoiceInList.vue +++ b/src/pages/InvoiceIn/InvoiceInList.vue @@ -29,6 +29,7 @@ const cols = computed(() => [ name: 'isBooked', label: t('invoiceIn.isBooked'), columnFilter: false, + component: 'checkbox', }, { align: 'left', @@ -56,7 +57,7 @@ const cols = computed(() => [ { align: 'left', name: 'supplierRef', - label: t('invoiceIn.list.supplierRef'), + label: t('invoiceIn.supplierRef'), }, { align: 'left', @@ -177,7 +178,7 @@ const cols = computed(() => [ :required="true" /> <VnInput - :label="t('invoiceIn.list.supplierRef')" + :label="t('invoiceIn.supplierRef')" v-model="data.supplierRef" /> <VnSelect diff --git a/src/pages/InvoiceIn/InvoiceInToBook.vue b/src/pages/InvoiceIn/InvoiceInToBook.vue index 95ce8155a..5bdbe197b 100644 --- a/src/pages/InvoiceIn/InvoiceInToBook.vue +++ b/src/pages/InvoiceIn/InvoiceInToBook.vue @@ -4,6 +4,7 @@ import { useQuasar } from 'quasar'; import { useI18n } from 'vue-i18n'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; import { useArrayData } from 'src/composables/useArrayData'; +import qs from 'qs'; const { notify, dialog } = useQuasar(); const { t } = useI18n(); @@ -12,29 +13,51 @@ defineExpose({ checkToBook }); const { store } = useArrayData(); async function checkToBook(id) { - let directBooking = true; + let messages = []; + + const hasProblemWithTax = ( + await axios.get('InvoiceInTaxes/count', { + params: { + where: JSON.stringify({ + invoiceInFk: id, + or: [{ taxTypeSageFk: null }, { transactionTypeSageFk: null }], + }), + }, + }) + ).data?.count; + + if (hasProblemWithTax) + messages.push(t('The VAT and Transaction fields have not been informed')); const { data: totals } = await axios.get(`InvoiceIns/${id}/getTotals`); const taxableBaseNotEqualDueDay = totals.totalDueDay != totals.totalTaxableBase; const vatNotEqualDueDay = totals.totalDueDay != totals.totalVat; - if (taxableBaseNotEqualDueDay && vatNotEqualDueDay) directBooking = false; + if (taxableBaseNotEqualDueDay && vatNotEqualDueDay) + messages.push(t('The sum of the taxable bases does not match the due dates')); - const { data: dueDaysCount } = await axios.get('InvoiceInDueDays/count', { - where: { - invoiceInFk: id, - dueDated: { gte: Date.vnNew() }, - }, - }); + const dueDaysCount = ( + await axios.get('InvoiceInDueDays/count', { + params: { + where: JSON.stringify({ + invoiceInFk: id, + dueDated: { gte: Date.vnNew() }, + }), + }, + }) + ).data?.count; - if (dueDaysCount) directBooking = false; + if (dueDaysCount) messages.push(t('Some due dates are less than or equal to today')); - if (directBooking) return toBook(id); - - dialog({ - component: VnConfirm, - componentProps: { title: t('Are you sure you want to book this invoice?') }, - }).onOk(async () => await toBook(id)); + if (!messages.length) toBook(id); + else + dialog({ + component: VnConfirm, + componentProps: { + title: t('Are you sure you want to book this invoice?'), + message: messages.reduce((acc, msg) => `${acc}<p>${msg}</p>`, ''), + }, + }).onOk(() => toBook(id)); } async function toBook(id) { @@ -59,4 +82,7 @@ async function toBook(id) { es: Are you sure you want to book this invoice?: ¿Estás seguro de querer asentar esta factura? It was not able to book the invoice: No se pudo contabilizar la factura + Some due dates are less than or equal to today: Algún vencimiento tiene una fecha menor o igual que hoy + The sum of the taxable bases does not match the due dates: La suma de las bases imponibles no coincide con la de los vencimientos + The VAT and Transaction fields have not been informed: No se han informado los campos de iva y/o transacción </i18n> diff --git a/src/pages/InvoiceIn/locale/en.yml b/src/pages/InvoiceIn/locale/en.yml index 6b21b316b..548e6c201 100644 --- a/src/pages/InvoiceIn/locale/en.yml +++ b/src/pages/InvoiceIn/locale/en.yml @@ -3,10 +3,10 @@ invoiceIn: searchInfo: Search incoming invoices by ID or supplier fiscal name serial: Serial isBooked: Is booked + supplierRef: Invoice nº list: ref: Reference supplier: Supplier - supplierRef: Supplier ref. file: File issued: Issued dueDated: Due dated @@ -19,8 +19,6 @@ invoiceIn: unbook: Unbook delete: Delete clone: Clone - toBook: To book - toUnbook: To unbook deleteInvoice: Delete invoice invoiceDeleted: invoice deleted cloneInvoice: Clone invoice @@ -70,4 +68,3 @@ invoiceIn: isBooked: Is booked account: Ledger account correctingFk: Rectificative - \ No newline at end of file diff --git a/src/pages/InvoiceIn/locale/es.yml b/src/pages/InvoiceIn/locale/es.yml index 3f27c895c..142d95f92 100644 --- a/src/pages/InvoiceIn/locale/es.yml +++ b/src/pages/InvoiceIn/locale/es.yml @@ -3,10 +3,10 @@ invoiceIn: searchInfo: Buscar facturas recibidas por ID o nombre fiscal del proveedor serial: Serie isBooked: Contabilizada + supplierRef: Nº factura list: ref: Referencia supplier: Proveedor - supplierRef: Ref. proveedor issued: F. emisión dueDated: F. vencimiento file: Fichero @@ -15,12 +15,10 @@ invoiceIn: descriptor: ticketList: Listado de tickets descriptorMenu: - book: Asentar - unbook: Desasentar + book: Contabilizar + unbook: Descontabilizar delete: Eliminar clone: Clonar - toBook: Contabilizar - toUnbook: Descontabilizar deleteInvoice: Eliminar factura invoiceDeleted: Factura eliminada cloneInvoice: Clonar factura @@ -68,4 +66,3 @@ invoiceIn: isBooked: Contabilizada account: Cuenta contable correctingFk: Rectificativa - diff --git a/src/pages/InvoiceOut/Card/InvoiceOutCard.vue b/src/pages/InvoiceOut/Card/InvoiceOutCard.vue index 93e3fe042..a50c9d247 100644 --- a/src/pages/InvoiceOut/Card/InvoiceOutCard.vue +++ b/src/pages/InvoiceOut/Card/InvoiceOutCard.vue @@ -1,11 +1,13 @@ <script setup> import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue'; import VnCardBeta from 'components/common/VnCardBeta.vue'; +import filter from './InvoiceOutFilter.js'; </script> <template> <VnCardBeta data-key="InvoiceOut" - base-url="InvoiceOuts" + url="InvoiceOuts" + :filter="filter" :descriptor="InvoiceOutDescriptor" /> </template> diff --git a/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue b/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue index 209f1531e..dfaf6c109 100644 --- a/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue +++ b/src/pages/InvoiceOut/Card/InvoiceOutDescriptor.vue @@ -8,8 +8,8 @@ import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy import VnLv from 'src/components/ui/VnLv.vue'; import InvoiceOutDescriptorMenu from './InvoiceOutDescriptorMenu.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import { toCurrency, toDate } from 'src/filters'; +import filter from './InvoiceOutFilter.js'; const $props = defineProps({ id: { @@ -26,42 +26,20 @@ const entityId = computed(() => { return $props.id || route.params.id; }); -const filter = { - include: [ - { - relation: 'company', - scope: { - fields: ['id', 'code'], - }, - }, - { - relation: 'client', - scope: { - fields: ['id', 'name', 'email'], - }, - }, - ], -}; - const descriptor = ref(); function ticketFilter(invoice) { return JSON.stringify({ refFk: invoice.ref }); } -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.ref, entity.id)); </script> <template> <CardDescriptor ref="descriptor" - module="InvoiceOut" :url="`InvoiceOuts/${entityId}`" :filter="filter" - :title="data.title" - :subtitle="data.subtitle" - @on-fetch="setData" - data-key="invoiceOutData" + title="ref" + data-key="InvoiceOut" width="lg-width" > <template #menu="{ entity, menuRef }"> diff --git a/src/pages/InvoiceOut/Card/InvoiceOutFilter.js b/src/pages/InvoiceOut/Card/InvoiceOutFilter.js new file mode 100644 index 000000000..48b20faf6 --- /dev/null +++ b/src/pages/InvoiceOut/Card/InvoiceOutFilter.js @@ -0,0 +1,16 @@ +export default { + include: [ + { + relation: 'company', + scope: { + fields: ['id', 'code'], + }, + }, + { + relation: 'client', + scope: { + fields: ['id', 'name', 'email'], + }, + }, + ], +}; diff --git a/src/pages/Item/Card/ItemBarcode.vue b/src/pages/Item/Card/ItemBarcode.vue index 6db5943c7..590b524cd 100644 --- a/src/pages/Item/Card/ItemBarcode.vue +++ b/src/pages/Item/Card/ItemBarcode.vue @@ -92,7 +92,7 @@ const submit = async (rows) => { class="cursor-pointer fill-icon-on-hover" color="primary" icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat > <QTooltip> diff --git a/src/pages/Item/Card/ItemBasicData.vue b/src/pages/Item/Card/ItemBasicData.vue index 4c96401f3..df7e71684 100644 --- a/src/pages/Item/Card/ItemBasicData.vue +++ b/src/pages/Item/Card/ItemBasicData.vue @@ -11,6 +11,7 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; import FilterItemForm from 'src/components/FilterItemForm.vue'; import CreateIntrastatForm from './CreateIntrastatForm.vue'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const route = useRoute(); const { t } = useI18n(); @@ -54,9 +55,8 @@ const onIntrastatCreated = (response, formData) => { auto-load /> <FormModel - :url="`Items/${route.params.id}`" :url-update="`Items/${route.params.id}`" - model="item" + model="Item" auto-load :clear-store-on-unmount="false" > @@ -209,30 +209,20 @@ const onIntrastatCreated = (response, formData) => { /> </VnRow> <VnRow class="row q-gutter-md q-mb-md"> - <div> - <QCheckbox - v-model="data.isFragile" - :label="t('item.basicData.isFragile')" - class="q-mr-sm" - /> - <QIcon name="info" class="cursor-pointer" size="xs"> - <QTooltip max-width="300px"> - {{ t('item.basicData.isFragileTooltip') }} - </QTooltip> - </QIcon> - </div> - <div> - <QCheckbox - v-model="data.isPhotoRequested" - :label="t('item.basicData.isPhotoRequested')" - class="q-mr-sm" - /> - <QIcon name="info" class="cursor-pointer" size="xs"> - <QTooltip> - {{ t('item.basicData.isPhotoRequestedTooltip') }} - </QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="data.isFragile" + :label="t('item.basicData.isFragile')" + :info="t('item.basicData.isFragileTooltip')" + class="q-mr-sm" + size="xs" + /> + <VnCheckbox + v-model="data.isPhotoRequested" + :label="t('item.basicData.isPhotoRequested')" + :info="t('item.basicData.isPhotoRequestedTooltip')" + class="q-mr-sm" + size="xs" + /> </VnRow> <VnRow> <VnInput diff --git a/src/pages/Item/Card/ItemBotanical.vue b/src/pages/Item/Card/ItemBotanical.vue index 4894d94fc..a40d81589 100644 --- a/src/pages/Item/Card/ItemBotanical.vue +++ b/src/pages/Item/Card/ItemBotanical.vue @@ -7,8 +7,8 @@ import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; -import CreateGenusForm from './CreateGenusForm.vue'; -import CreateSpecieForm from './CreateSpecieForm.vue'; +import CreateGenusForm from '../components/CreateGenusForm.vue'; +import CreateSpecieForm from '../components/CreateSpecieForm.vue'; const route = useRoute(); const { t } = useI18n(); diff --git a/src/pages/Item/Card/ItemCard.vue b/src/pages/Item/Card/ItemCard.vue index 2546982eb..610b77a02 100644 --- a/src/pages/Item/Card/ItemCard.vue +++ b/src/pages/Item/Card/ItemCard.vue @@ -5,7 +5,7 @@ import ItemDescriptor from './ItemDescriptor.vue'; <template> <VnCardBeta data-key="Item" - base-url="Items" + :url="`Items/${$route.params.id}/getCard`" :descriptor="ItemDescriptor" /> </template> diff --git a/src/pages/Item/Card/ItemDescriptor.vue b/src/pages/Item/Card/ItemDescriptor.vue index c6fee8540..a4c58ef4b 100644 --- a/src/pages/Item/Card/ItemDescriptor.vue +++ b/src/pages/Item/Card/ItemDescriptor.vue @@ -7,7 +7,6 @@ import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import { dashIfEmpty } from 'src/filters'; import { useArrayData } from 'src/composables/useArrayData'; @@ -35,6 +34,10 @@ const $props = defineProps({ type: Number, default: null, }, + proxyRender: { + type: Boolean, + default: false, + }, }); const route = useRoute(); @@ -55,10 +58,8 @@ onMounted(async () => { mounted.value = true; }); -const data = ref(useCardDescription()); const setData = async (entity) => { if (!entity) return; - data.value = useCardDescription(entity.name, entity.id); await updateStock(); }; @@ -90,10 +91,7 @@ const updateStock = async () => { <template> <CardDescriptor - data-key="ItemData" - module="Item" - :title="data.title" - :subtitle="data.subtitle" + data-key="Item" :summary="$props.summary" :url="`Items/${entityId}/getCard`" @on-fetch="setData" @@ -117,7 +115,7 @@ const updateStock = async () => { <template #value> <span class="link"> {{ entity.itemType?.worker?.user?.name }} - <WorkerDescriptorProxy :id="entity.itemType?.worker?.id" /> + <WorkerDescriptorProxy :id="entity.itemType?.worker?.id ?? NaN" /> </span> </template> </VnLv> @@ -152,7 +150,7 @@ const updateStock = async () => { </QCardActions> </template> <template #actions="{}"> - <QCardActions class="row justify-center"> + <QCardActions class="row justify-center" v-if="proxyRender"> <QBtn :to="{ name: 'ItemDiary', @@ -165,6 +163,16 @@ const updateStock = async () => { > <QTooltip>{{ t('item.descriptor.itemDiary') }}</QTooltip> </QBtn> + <QBtn + :to="{ + name: 'ItemLastEntries', + }" + size="md" + icon="vn:regentry" + color="primary" + > + <QTooltip>{{ t('item.descriptor.itemLastEntries') }}</QTooltip> + </QBtn> </QCardActions> </template> </CardDescriptor> diff --git a/src/pages/Item/Card/ItemDescriptorProxy.vue b/src/pages/Item/Card/ItemDescriptorProxy.vue index 2ffc9080f..f686e8221 100644 --- a/src/pages/Item/Card/ItemDescriptorProxy.vue +++ b/src/pages/Item/Card/ItemDescriptorProxy.vue @@ -4,7 +4,7 @@ import ItemSummary from './ItemSummary.vue'; const $props = defineProps({ id: { - type: Number, + type: [Number, String], required: true, }, dated: { @@ -21,9 +21,8 @@ const $props = defineProps({ }, }); </script> - <template> - <QPopupProxy> + <QPopupProxy style="max-width: 10px"> <ItemDescriptor v-if="$props.id" :id="$props.id" @@ -31,6 +30,7 @@ const $props = defineProps({ :dated="dated" :sale-fk="saleFk" :warehouse-fk="warehouseFk" + :proxy-render="true" /> </QPopupProxy> </template> diff --git a/src/pages/Item/Card/ItemShelving.vue b/src/pages/Item/Card/ItemShelving.vue index 7ad60c9e0..b29e2a2a5 100644 --- a/src/pages/Item/Card/ItemShelving.vue +++ b/src/pages/Item/Card/ItemShelving.vue @@ -110,10 +110,16 @@ const columns = computed(() => [ attrs: { inWhere: true }, align: 'left', }, + { + label: t('globals.visible'), + name: 'stock', + attrs: { inWhere: true }, + align: 'left', + }, ]); const totalLabels = computed(() => - rows.value.reduce((acc, row) => acc + row.stock / row.packing, 0).toFixed(2) + rows.value.reduce((acc, row) => acc + row.stock / row.packing, 0).toFixed(2), ); const removeLines = async () => { @@ -157,7 +163,7 @@ watchEffect(selectedRows); openConfirmationModal( t('shelvings.removeConfirmTitle'), t('shelvings.removeConfirmSubtitle'), - removeLines + removeLines, ) " > diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue index 5a7d7f818..ab26b9cae 100644 --- a/src/pages/Item/Card/ItemTags.vue +++ b/src/pages/Item/Card/ItemTags.vue @@ -178,7 +178,7 @@ const insertTag = (rows) => { @click="insertTag(rows)" color="primary" icon="add" - shortcut="+" + v-shortcut="'+'" fab data-cy="createNewTag" > diff --git a/src/pages/Item/ItemFixedPrice.vue b/src/pages/Item/ItemFixedPrice.vue index 1c4382fbd..fdfa1d3d1 100644 --- a/src/pages/Item/ItemFixedPrice.vue +++ b/src/pages/Item/ItemFixedPrice.vue @@ -65,10 +65,19 @@ const columns = computed(() => [ name: 'name', ...defaultColumnAttrs, create: true, + columnFilter: { + component: 'select', + attrs: { + url: 'Items', + fields: ['id', 'name', 'subName'], + optionLabel: 'name', + optionValue: 'name', + uppercase: false, + }, + }, }, { label: t('item.fixedPrice.groupingPrice'), - field: 'rate2', name: 'rate2', ...defaultColumnAttrs, component: 'input', @@ -76,7 +85,6 @@ const columns = computed(() => [ }, { label: t('item.fixedPrice.packingPrice'), - field: 'rate3', name: 'rate3', ...defaultColumnAttrs, component: 'input', @@ -85,7 +93,6 @@ const columns = computed(() => [ { label: t('item.fixedPrice.minPrice'), - field: 'minPrice', name: 'minPrice', ...defaultColumnAttrs, component: 'input', @@ -108,7 +115,6 @@ const columns = computed(() => [ }, { label: t('item.fixedPrice.ended'), - field: 'ended', name: 'ended', ...defaultColumnAttrs, columnField: { @@ -124,7 +130,6 @@ const columns = computed(() => [ { label: t('globals.warehouse'), - field: 'warehouseFk', name: 'warehouseFk', ...defaultColumnAttrs, columnClass: 'shrink', @@ -415,7 +420,6 @@ function handleOnDataSave({ CrudModelRef }) { 'row-key': 'id', selection: 'multiple', }" - :use-model="true" v-model:selected="rowsSelected" :create-as-dialog="false" :create="{ diff --git a/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue b/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue index b4032ff8a..475dffd8b 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeBasicData.vue @@ -40,12 +40,7 @@ const itemPackingTypesOptions = ref([]); }" auto-load /> - <FormModel - :url="`ItemTypes/${route.params.id}`" - :url-update="`ItemTypes/${route.params.id}`" - model="itemTypeBasicData" - auto-load - > + <FormModel :url-update="`ItemTypes/${route.params.id}`" model="ItemType" auto-load> <template #form="{ data }"> <VnRow> <VnInput v-model="data.code" :label="t('itemType.shared.code')" /> diff --git a/src/pages/Item/ItemType/Card/ItemTypeCard.vue b/src/pages/Item/ItemType/Card/ItemTypeCard.vue index fa51e428e..84e810de5 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeCard.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeCard.vue @@ -1,12 +1,14 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import ItemTypeDescriptor from 'src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue'; +import filter from './ItemTypeFilter.js'; </script> <template> <VnCardBeta - data-key="ItemTypeSummary" - base-url="ItemTypes" + data-key="ItemType" + url="ItemTypes" + :filter="filter" :descriptor="ItemTypeDescriptor" /> </template> diff --git a/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue b/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue index 09d3dbce5..725fb30aa 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeDescriptor.vue @@ -1,12 +1,11 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import useCardDescription from 'src/composables/useCardDescription'; +import filter from './ItemTypeFilter.js'; const $props = defineProps({ id: { @@ -20,46 +19,31 @@ const $props = defineProps({ }); const route = useRoute(); -const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); - -const itemTypeFilter = { - include: [ - { relation: 'worker' }, - { relation: 'category' }, - { relation: 'itemPackingType' }, - { relation: 'temperature' }, - ], -}; - -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); </script> - <template> <CardDescriptor - module="ItemType" :url="`ItemTypes/${entityId}`" - :filter="itemTypeFilter" - :title="data.title" - :subtitle="data.subtitle" - data-key="itemTypeDescriptor" - @on-fetch="setData" + :filter="filter" + title="code" + data-key="ItemType" > <template #body="{ entity }"> - <VnLv :label="t('itemType.shared.code')" :value="entity.code" /> - <VnLv :label="t('itemType.shared.name')" :value="entity.name" /> - <VnLv :label="t('itemType.shared.worker')"> + <VnLv :label="$t('itemType.shared.code')" :value="entity.code" /> + <VnLv :label="$t('itemType.shared.name')" :value="entity.name" /> + <VnLv :label="$t('itemType.shared.worker')"> <template #value> <span class="link">{{ entity.worker?.firstName }}</span> <WorkerDescriptorProxy :id="entity.worker?.id" /> </template> </VnLv> - <VnLv :label="t('itemType.shared.category')" :value="entity.category?.name" /> + <VnLv + :label="$t('itemType.shared.category')" + :value="entity.category?.name" + /> </template> </CardDescriptor> </template> - diff --git a/src/pages/Item/ItemType/Card/ItemTypeFilter.js b/src/pages/Item/ItemType/Card/ItemTypeFilter.js new file mode 100644 index 000000000..5651d368d --- /dev/null +++ b/src/pages/Item/ItemType/Card/ItemTypeFilter.js @@ -0,0 +1,8 @@ +export default { + include: [ + { relation: 'worker' }, + { relation: 'category' }, + { relation: 'itemPackingType' }, + { relation: 'temperature' }, + ], +}; diff --git a/src/pages/Item/ItemType/Card/ItemTypeSummary.vue b/src/pages/Item/ItemType/Card/ItemTypeSummary.vue index 9ba774ca4..3b63c4b63 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeSummary.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeSummary.vue @@ -3,7 +3,7 @@ import { ref, computed, onUpdated } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; - +import filter from './ItemTypeFilter.js'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnToSummary from 'src/components/ui/VnToSummary.vue'; @@ -21,15 +21,6 @@ const $props = defineProps({ }, }); -const itemTypeFilter = { - include: [ - { relation: 'worker' }, - { relation: 'category' }, - { relation: 'itemPackingType' }, - { relation: 'temperature' }, - ], -}; - const entityId = computed(() => $props.id || route.params.id); const summaryRef = ref(); const itemType = ref(); @@ -43,8 +34,8 @@ async function setItemTypeData(data) { <CardSummary ref="summaryRef" :url="`ItemTypes/${entityId}`" - data-key="ItemTypeSummary" - :filter="itemTypeFilter" + data-key="ItemType" + :filter="filter" @on-fetch="(data) => setItemTypeData(data)" class="full-width" > diff --git a/src/pages/Item/Card/CreateGenusForm.vue b/src/pages/Item/components/CreateGenusForm.vue similarity index 100% rename from src/pages/Item/Card/CreateGenusForm.vue rename to src/pages/Item/components/CreateGenusForm.vue diff --git a/src/pages/Item/Card/CreateSpecieForm.vue b/src/pages/Item/components/CreateSpecieForm.vue similarity index 100% rename from src/pages/Item/Card/CreateSpecieForm.vue rename to src/pages/Item/components/CreateSpecieForm.vue diff --git a/src/pages/Item/components/ItemProposal.vue b/src/pages/Item/components/ItemProposal.vue new file mode 100644 index 000000000..d2dbea7b3 --- /dev/null +++ b/src/pages/Item/components/ItemProposal.vue @@ -0,0 +1,332 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; +import { toCurrency } from 'filters/index'; +import VnStockValueDisplay from 'src/components/ui/VnStockValueDisplay.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import axios from 'axios'; +import notifyResults from 'src/utils/notifyResults'; +import FetchData from 'components/FetchData.vue'; + +const MATCH = 'match'; + +const { t } = useI18n(); +const $props = defineProps({ + itemLack: { + type: Object, + required: true, + default: () => {}, + }, + replaceAction: { + type: Boolean, + required: false, + default: false, + }, + sales: { + type: Array, + required: false, + default: () => [], + }, +}); +const proposalSelected = ref([]); +const ticketConfig = ref({}); +const proposalTableRef = ref(null); + +const sale = computed(() => $props.sales[0]); +const saleFk = computed(() => sale.value.saleFk); +const filter = computed(() => ({ + itemFk: $props.itemLack.itemFk, + sales: saleFk.value, +})); + +const defaultColumnAttrs = { + align: 'center', + sortable: false, +}; +const emit = defineEmits(['onDialogClosed', 'itemReplaced']); + +const conditionalValuePrice = (price) => + price > 1 + ticketConfig.value.lackAlertPrice / 100 ? 'match' : 'not-match'; + +const columns = computed(() => [ + { + ...defaultColumnAttrs, + label: t('proposal.available'), + name: 'available', + field: 'available', + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + columnClass: 'shrink', + }, + { + ...defaultColumnAttrs, + label: t('proposal.counter'), + name: 'counter', + field: 'counter', + columnClass: 'shrink', + style: 'max-width: 75px', + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + }, + + { + align: 'left', + sortable: true, + label: t('proposal.longName'), + name: 'longName', + field: 'longName', + columnClass: 'expand', + }, + { + align: 'left', + sortable: true, + label: t('item.list.color'), + name: 'tag5', + field: 'value5', + columnClass: 'expand', + }, + { + align: 'left', + sortable: true, + label: t('item.list.stems'), + name: 'tag6', + field: 'value6', + columnClass: 'expand', + }, + { + align: 'left', + sortable: true, + label: t('item.list.producer'), + name: 'tag7', + field: 'value7', + columnClass: 'expand', + }, + + { + ...defaultColumnAttrs, + label: t('proposal.price2'), + name: 'price2', + style: 'max-width: 75px', + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + }, + { + ...defaultColumnAttrs, + label: t('proposal.minQuantity'), + name: 'minQuantity', + field: 'minQuantity', + style: 'max-width: 75px', + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + }, + { + ...defaultColumnAttrs, + label: t('proposal.located'), + name: 'located', + field: 'located', + }, + { + align: 'right', + label: '', + name: 'tableActions', + actions: [ + { + title: t('Replace'), + icon: 'change_circle', + show: (row) => isSelectionAvailable(row), + action: change, + isPrimary: true, + }, + ], + }, +]); + +function extractMatchValues(obj) { + return Object.keys(obj) + .filter((key) => key.startsWith(MATCH)) + .map((key) => parseInt(key.replace(MATCH, ''), 10)); +} +const gradientStyle = (value) => { + let color = 'white'; + const perc = parseFloat(value); + switch (true) { + case perc >= 0 && perc < 33: + color = 'primary'; + break; + case perc >= 33 && perc < 66: + color = 'warning'; + break; + + default: + color = 'secondary'; + break; + } + return color; +}; +const statusConditionalValue = (row) => { + const matches = extractMatchValues(row); + const value = matches.reduce((acc, i) => acc + row[`${MATCH}${i}`], 0); + return 100 * (value / matches.length); +}; + +const isSelectionAvailable = (itemProposal) => { + const { price2 } = itemProposal; + const salePrice = sale.value.price; + const byPrice = (100 * price2) / salePrice > ticketConfig.value.lackAlertPrice; + if (byPrice) { + return byPrice; + } + const byQuantity = + (100 * itemProposal.available) / Math.abs($props.itemLack.lack) < + ticketConfig.value.lackAlertPrice; + return byQuantity; +}; + +async function change({ itemFk: substitutionFk }) { + try { + const promises = $props.sales.map(({ saleFk, quantity }) => { + const params = { + saleFk, + substitutionFk, + quantity, + }; + return axios.post('Sales/replaceItem', params); + }); + const results = await Promise.allSettled(promises); + + notifyResults(results, 'saleFk'); + emit('itemReplaced', { + type: 'refresh', + quantity: quantity.value, + itemProposal: proposalSelected.value[0], + }); + proposalSelected.value = []; + } catch (error) { + console.error(error); + } +} + +async function handleTicketConfig(data) { + ticketConfig.value = data[0]; +} +</script> +<template> + <FetchData + url="TicketConfigs" + :filter="{ fields: ['lackAlertPrice'] }" + @on-fetch="handleTicketConfig" + auto-load + /> + + <VnTable + v-if="ticketConfig" + auto-load + data-cy="proposalTable" + ref="proposalTableRef" + data-key="ItemsGetSimilar" + url="Items/getSimilar" + :user-filter="filter" + :columns="columns" + class="full-width q-mt-md" + row-key="id" + :row-click="change" + :is-editable="false" + :right-search="false" + :without-header="true" + :disable-option="{ card: true, table: true }" + > + <template #column-longName="{ row }"> + <QTd + class="flex" + style="max-width: 100%; flex-shrink: 50px; flex-wrap: nowrap" + > + <div + class="middle full-width" + :class="[`proposal-${gradientStyle(statusConditionalValue(row))}`]" + > + <QTooltip> {{ statusConditionalValue(row) }}% </QTooltip> + </div> + <div style="flex: 2 0 100%; align-content: center"> + <div> + <span class="link">{{ row.longName }}</span> + <ItemDescriptorProxy :id="row.id" /> + </div> + </div> + </QTd> + </template> + <template #column-tag5="{ row }"> + <span :class="{ match: !row.match5 }">{{ row.value5 }}</span> + </template> + <template #column-tag6="{ row }"> + <span :class="{ match: !row.match6 }">{{ row.value6 }}</span> + </template> + <template #column-tag7="{ row }"> + <span :class="{ match: !row.match7 }">{{ row.value7 }}</span> + </template> + <template #column-counter="{ row }"> + <span + :class="{ + match: row.counter === 1, + 'not-match': row.counter !== 1, + }" + >{{ row.counter }}</span + > + </template> + <template #column-minQuantity="{ row }"> + {{ row.minQuantity }} + </template> + <template #column-price2="{ row }"> + <div class="flex column items-center content-center"> + <VnStockValueDisplay :value="(sales[0].price - row.price2) / 100" /> + <span :class="[conditionalValuePrice(row.price2)]">{{ + toCurrency(row.price2) + }}</span> + </div> + </template> + </VnTable> +</template> +<style lang="scss" scoped> +@import 'src/css/quasar.variables.scss'; +.middle { + float: left; + margin-right: 2px; + flex: 2 0 5px; +} +.match { + color: $negative; +} +.not-match { + color: inherit; +} +.proposal-warning { + background-color: $warning; +} +.proposal-secondary { + background-color: $secondary; +} +.proposal-primary { + background-color: $primary; +} +.text { + margin: 0.05rem; + padding: 1px; + border: 1px solid var(--vn-label-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: smaller; +} +</style> diff --git a/src/pages/Item/components/ItemProposalProxy.vue b/src/pages/Item/components/ItemProposalProxy.vue new file mode 100644 index 000000000..7da0ce398 --- /dev/null +++ b/src/pages/Item/components/ItemProposalProxy.vue @@ -0,0 +1,56 @@ +<script setup> +import ItemProposal from './ItemProposal.vue'; +import { useDialogPluginComponent } from 'quasar'; + +const $props = defineProps({ + itemLack: { + type: Object, + required: true, + default: () => {}, + }, + replaceAction: { + type: Boolean, + required: false, + default: false, + }, + sales: { + type: Array, + required: false, + default: () => [], + }, +}); +const { dialogRef } = useDialogPluginComponent(); +const emit = defineEmits([ + 'onDialogClosed', + 'itemReplaced', + ...useDialogPluginComponent.emits, +]); +defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.hide() }); +</script> +<template> + <QDialog ref="dialogRef" transition-show="scale" transition-hide="scale"> + <QCard class="dialog-width"> + <QCardSection class="row items-center q-pb-none"> + <span class="text-h6 text-grey">{{ $t('Item proposal') }}</span> + <QSpace /> + <QBtn icon="close" flat round dense v-close-popup /> + </QCardSection> + <QCardSection> + <ItemProposal + v-bind="$props" + @item-replaced=" + (data) => { + emit('itemReplaced', data); + dialogRef.hide(); + } + " + ></ItemProposal + ></QCardSection> + </QCard> + </QDialog> +</template> +<style lang="scss" scoped> +.dialog-width { + max-width: $width-lg; +} +</style> diff --git a/src/pages/Item/locale/en.yml b/src/pages/Item/locale/en.yml index bc73abb12..9d27fc96e 100644 --- a/src/pages/Item/locale/en.yml +++ b/src/pages/Item/locale/en.yml @@ -112,6 +112,7 @@ item: available: Available warehouseText: 'Calculated on the warehouse of { warehouseName }' itemDiary: Item diary + itemLastEntries: Last entries producer: Producer clone: title: All its properties will be copied @@ -130,6 +131,7 @@ item: origin: Orig. userName: Buyer weight: Weight + color: Color weightByPiece: Weight/stem stemMultiplier: Multiplier producer: Producer @@ -215,4 +217,24 @@ item: specie: Specie search: 'Search item' searchInfo: 'You can search by id' - regularizeStock: Regularize stock \ No newline at end of file + regularizeStock: Regularize stock +itemProposal: Items proposal +proposal: + difference: Difference + title: Items proposal + itemFk: Item + longName: Name + subName: Producer + value5: value5 + value6: value6 + value7: value7 + value8: value8 + available: Available + minQuantity: minQuantity + price2: Price + located: Located + counter: Counter + groupingPrice: Grouping Price + itemOldPrice: itemOld Price + status: State + quantityToReplace: Quanity to replace diff --git a/src/pages/Item/locale/es.yml b/src/pages/Item/locale/es.yml index dd5074f5f..935f5160b 100644 --- a/src/pages/Item/locale/es.yml +++ b/src/pages/Item/locale/es.yml @@ -118,6 +118,7 @@ item: available: Disponible warehouseText: 'Calculado sobre el almacén de { warehouseName }' itemDiary: Registro de compra-venta + itemLastEntries: Últimas entradas producer: Productor clone: title: Todas sus propiedades serán copiadas @@ -135,6 +136,7 @@ item: size: Medida origin: Orig. weight: Peso + color: Color weightByPiece: Peso/tallo userName: Comprador stemMultiplier: Multiplicador @@ -220,5 +222,30 @@ item: achieved: 'Conseguido' concept: 'Concepto' state: 'Estado' - search: 'Buscar artículo' - searchInfo: 'Puedes buscar por id' +itemProposal: Artículos similares +proposal: + substitutionAvailable: Sustitución disponible + notSubstitutionAvailableByPrice: Sustitución no disponible, 30% de diferencia por precio o cantidad + compatibility: Compatibilidad + title: Items de sustitución para los tickets seleccionados + itemFk: Item + longName: Nombre + subName: Productor + value5: value5 + value6: value6 + value7: value7 + value8: value8 + available: Disponible + minQuantity: Min. cantidad + price2: Precio + located: Ubicado + counter: Contador + difference: Diferencial + groupingPrice: Precio Grouping + itemOldPrice: Precio itemOld + status: Estado + quantityToReplace: Cantidad a reemplazar + replace: Sustituir + replaceAndConfirm: Sustituir y confirmar precio +search: 'Buscar artículo' +searchInfo: 'Puedes buscar por id' diff --git a/src/pages/Monitor/MonitorOrders.vue b/src/pages/Monitor/MonitorOrders.vue index 4efab56fb..873f8abb4 100644 --- a/src/pages/Monitor/MonitorOrders.vue +++ b/src/pages/Monitor/MonitorOrders.vue @@ -157,7 +157,7 @@ const openTab = (id) => openConfirmationModal( $t('globals.deleteConfirmTitle'), $t('salesOrdersTable.deleteConfirmMessage'), - removeOrders + removeOrders, ) " > diff --git a/src/pages/Monitor/locale/en.yml b/src/pages/Monitor/locale/en.yml index 21324087c..496c8761a 100644 --- a/src/pages/Monitor/locale/en.yml +++ b/src/pages/Monitor/locale/en.yml @@ -38,6 +38,7 @@ salesTicketsTable: payMethod: Pay method department: Department packing: ITP + hasItemLost: Item lost searchBar: label: Search tickets info: Search tickets by id or alias diff --git a/src/pages/Monitor/locale/es.yml b/src/pages/Monitor/locale/es.yml index 30afb1904..f6a29879f 100644 --- a/src/pages/Monitor/locale/es.yml +++ b/src/pages/Monitor/locale/es.yml @@ -39,6 +39,7 @@ salesTicketsTable: payMethod: Método de pago department: Departamento packing: ITP + hasItemLost: Artículo perdido searchBar: label: Buscar tickets info: Buscar tickets por identificador o alias diff --git a/src/pages/Order/Card/CatalogFilterValueDialog.vue b/src/pages/Order/Card/CatalogFilterValueDialog.vue index b91e7d229..d1bd48c9e 100644 --- a/src/pages/Order/Card/CatalogFilterValueDialog.vue +++ b/src/pages/Order/Card/CatalogFilterValueDialog.vue @@ -110,7 +110,7 @@ const getSelectedTagValues = async (tag) => { </div> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat class="filter-icon q-mb-md" size="md" diff --git a/src/pages/Order/Card/OrderBasicData.vue b/src/pages/Order/Card/OrderBasicData.vue index 8594a05f4..9c02d7494 100644 --- a/src/pages/Order/Card/OrderBasicData.vue +++ b/src/pages/Order/Card/OrderBasicData.vue @@ -14,7 +14,6 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; const { t } = useI18n(); const route = useRoute(); const state = useState(); -const ORDER_MODEL = 'order'; const isNew = Boolean(!route.params.id); const clientList = ref([]); @@ -32,7 +31,7 @@ const fetchAddressList = async (addressId) => { }); addressList.value = data; if (addressList.value?.length === 1) { - state.get(ORDER_MODEL).addressFk = addressList.value[0].id; + state.get('Order').addressFk = addressList.value[0].id; } }; @@ -91,9 +90,8 @@ const onClientChange = async (clientId) => { <VnSubToolbar v-if="isNew" /> <div class="q-pa-md"> <FormModel - :url="`Orders/${route.params.id}`" :url-update="`Orders/${route.params.id}/updateBasicData`" - :model="ORDER_MODEL" + model="Order" :filter="orderFilter" @on-fetch="fetchOrderDetails" auto-load diff --git a/src/pages/Order/Card/OrderCard.vue b/src/pages/Order/Card/OrderCard.vue index 823815f59..ad5c73a87 100644 --- a/src/pages/Order/Card/OrderCard.vue +++ b/src/pages/Order/Card/OrderCard.vue @@ -1,12 +1,14 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import OrderDescriptor from 'pages/Order/Card/OrderDescriptor.vue'; +import filter from './OrderFilter.js'; </script> <template> <VnCardBeta data-key="Order" - base-url="Orders" + url="Orders" + :filter="filter" :descriptor="OrderDescriptor" /> </template> diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index 262f503fd..76e608983 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -184,7 +184,7 @@ function addOrder(value, field, params) { {{ t( categoryList.find((c) => c.id == customTag.value)?.name || - '' + '', ) }} </strong> @@ -296,7 +296,7 @@ function addOrder(value, field, params) { <template #append> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat color="primary" size="md" diff --git a/src/pages/Order/Card/OrderCatalogItemDialog.vue b/src/pages/Order/Card/OrderCatalogItemDialog.vue index 77f6a8405..766945e4d 100644 --- a/src/pages/Order/Card/OrderCatalogItemDialog.vue +++ b/src/pages/Order/Card/OrderCatalogItemDialog.vue @@ -20,7 +20,7 @@ const props = defineProps({ }); const state = useState(); -const orderData = computed(() => state.get('orderData')); +const orderData = computed(() => state.get('Order')); const prices = ref((props.item.prices || []).map((item) => ({ ...item, quantity: 0 }))); const isLoading = ref(false); @@ -39,11 +39,11 @@ const addToOrder = async () => { }); const { data: orderTotal } = await axios.get( - `Orders/${Number(route.params.id)}/getTotal` + `Orders/${Number(route.params.id)}/getTotal`, ); state.set('orderTotal', orderTotal); - state.set('orderData', { + state.set('Order', { ...orderData.value, items, }); @@ -56,7 +56,7 @@ const canAddToOrder = () => { if (canAddToOrder) { const excedQuantity = prices.value.reduce( (acc, { quantity }) => acc + quantity, - 0 + 0, ); if (excedQuantity > props.item.available) { canAddToOrder = false; diff --git a/src/pages/Order/Card/OrderDescriptor.vue b/src/pages/Order/Card/OrderDescriptor.vue index 0d5f0146f..0d18864dc 100644 --- a/src/pages/Order/Card/OrderDescriptor.vue +++ b/src/pages/Order/Card/OrderDescriptor.vue @@ -4,8 +4,7 @@ import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { toCurrency, toDate } from 'src/filters'; import { useState } from 'src/composables/useState'; -import useCardDescription from 'src/composables/useCardDescription'; - +import filter from './OrderFilter.js'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import FetchData from 'components/FetchData.vue'; @@ -24,44 +23,15 @@ const $props = defineProps({ const route = useRoute(); const state = useState(); const { t } = useI18n(); -const data = ref(useCardDescription()); const getTotalRef = ref(); const entityId = computed(() => { return $props.id || route.params.id; }); -const filter = { - include: [ - { relation: 'agencyMode', scope: { fields: ['name'] } }, - { - relation: 'address', - scope: { fields: ['nickname'] }, - }, - { relation: 'rows', scope: { fields: ['id'] } }, - { - relation: 'client', - scope: { - fields: [ - 'salesPersonFk', - 'name', - 'isActive', - 'isFreezed', - 'isTaxDataChecked', - ], - include: { - relation: 'salesPersonUser', - scope: { fields: ['id', 'name'] }, - }, - }, - }, - ], -}; - const setData = (entity) => { if (!entity) return; getTotalRef.value && getTotalRef.value.fetch(); - data.value = useCardDescription(entity?.client?.name, entity?.id); state.set('orderTotal', total); }; @@ -87,11 +57,9 @@ const total = ref(0); ref="descriptor" :url="`Orders/${entityId}`" :filter="filter" - module="Order" - :title="data.title" - :subtitle="data.subtitle" + title="client.name" @on-fetch="setData" - data-key="orderData" + data-key="Order" > <template #body="{ entity }"> <VnLv diff --git a/src/pages/Order/Card/OrderFilter.js b/src/pages/Order/Card/OrderFilter.js new file mode 100644 index 000000000..3e521b92c --- /dev/null +++ b/src/pages/Order/Card/OrderFilter.js @@ -0,0 +1,26 @@ +export default { + include: [ + { relation: 'agencyMode', scope: { fields: ['name'] } }, + { + relation: 'address', + scope: { fields: ['nickname'] }, + }, + { relation: 'rows', scope: { fields: ['id'] } }, + { + relation: 'client', + scope: { + fields: [ + 'salesPersonFk', + 'name', + 'isActive', + 'isFreezed', + 'isTaxDataChecked', + ], + include: { + relation: 'salesPersonUser', + scope: { fields: ['id', 'name'] }, + }, + }, + }, + ], +}; diff --git a/src/pages/Order/Card/OrderLines.vue b/src/pages/Order/Card/OrderLines.vue index cf219a244..1b864de6f 100644 --- a/src/pages/Order/Card/OrderLines.vue +++ b/src/pages/Order/Card/OrderLines.vue @@ -21,7 +21,7 @@ const router = useRouter(); const route = useRoute(); const { t } = useI18n(); const quasar = useQuasar(); -const descriptorData = useArrayData('orderData'); +const descriptorData = useArrayData('Order'); const componentKey = ref(0); const tableLinesRef = ref(); const order = ref(); @@ -238,7 +238,7 @@ watch( lineFilter.value.where.orderFk = router.currentRoute.value.params.id; tableLinesRef.value.reload(); - } + }, ); </script> diff --git a/src/pages/Order/Card/OrderSummary.vue b/src/pages/Order/Card/OrderSummary.vue index a289688e4..a4bdb2881 100644 --- a/src/pages/Order/Card/OrderSummary.vue +++ b/src/pages/Order/Card/OrderSummary.vue @@ -27,7 +27,7 @@ const $props = defineProps({ const entityId = computed(() => $props.id || route.params.id); const summary = ref(); const quasar = useQuasar(); -const descriptorData = useArrayData('orderData'); +const descriptorData = useArrayData('Order'); const detailsColumns = ref([ { name: 'item', diff --git a/src/pages/Order/OrderList.vue b/src/pages/Order/OrderList.vue index 21cb5ed7e..40990f329 100644 --- a/src/pages/Order/OrderList.vue +++ b/src/pages/Order/OrderList.vue @@ -71,8 +71,9 @@ const columns = computed(() => [ format: (row) => row?.name, }, { - align: 'left', + align: 'center', name: 'isConfirmed', + component: 'checkbox', label: t('module.isConfirmed'), }, { @@ -95,7 +96,9 @@ const columns = computed(() => [ columnField: { component: null, }, - style: 'color="positive"', + style: () => { + return { color: 'positive' }; + }, }, { align: 'left', diff --git a/src/pages/Route/Agency/AgencyList.vue b/src/pages/Route/Agency/AgencyList.vue index 4322b9bc8..5c2904bf3 100644 --- a/src/pages/Route/Agency/AgencyList.vue +++ b/src/pages/Route/Agency/AgencyList.vue @@ -51,7 +51,6 @@ const columns = computed(() => [ name: 'isAnyVolumeAllowed', component: 'checkbox', cardVisible: true, - disable: true, }, { align: 'right', @@ -72,7 +71,7 @@ const columns = computed(() => [ :data-key :columns="columns" prefix="agency" - :right-filter="false" + :right-filter="true" :array-data-props="{ url: 'Agencies', order: 'name', @@ -83,6 +82,7 @@ const columns = computed(() => [ <VnTable :data-key :columns="columns" + is-editable="false" :right-search="false" :use-model="true" redirect="route/agency" diff --git a/src/pages/Route/Agency/Card/AgencyBasicData.vue b/src/pages/Route/Agency/Card/AgencyBasicData.vue index 599058b3e..4270b136c 100644 --- a/src/pages/Route/Agency/Card/AgencyBasicData.vue +++ b/src/pages/Route/Agency/Card/AgencyBasicData.vue @@ -21,7 +21,7 @@ const warehouses = ref([]); @on-fetch="(data) => (warehouses = data)" auto-load /> - <FormModel :url="`Agencies/${routeId}`" model="agency" auto-load> + <FormModel :update-url="`Agencies/${routeId}`" model="Agency" auto-load> <template #form="{ data }"> <VnRow> <VnInput v-model="data.name" :label="t('globals.name')" /> diff --git a/src/pages/Route/Agency/Card/AgencyCard.vue b/src/pages/Route/Agency/Card/AgencyCard.vue index 35685790a..7dc31f8ba 100644 --- a/src/pages/Route/Agency/Card/AgencyCard.vue +++ b/src/pages/Route/Agency/Card/AgencyCard.vue @@ -3,5 +3,5 @@ import AgencyDescriptor from 'pages/Route/Agency/Card/AgencyDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; </script> <template> - <VnCardBeta data-key="Agency" base-url="Agencies" :descriptor="AgencyDescriptor" /> + <VnCardBeta data-key="Agency" url="Agencies" :descriptor="AgencyDescriptor" /> </template> diff --git a/src/pages/Route/Agency/Card/AgencyDescriptor.vue b/src/pages/Route/Agency/Card/AgencyDescriptor.vue index b9772037c..a0472c6c3 100644 --- a/src/pages/Route/Agency/Card/AgencyDescriptor.vue +++ b/src/pages/Route/Agency/Card/AgencyDescriptor.vue @@ -22,7 +22,6 @@ const card = computed(() => store.data); </script> <template> <CardDescriptor - module="Agency" data-key="Agency" :url="`Agencies/${entityId}`" :title="card?.name" diff --git a/src/pages/Route/Agency/Card/AgencyWorkcenter.vue b/src/pages/Route/Agency/Card/AgencyWorkcenter.vue index 7cabf396d..9a9213868 100644 --- a/src/pages/Route/Agency/Card/AgencyWorkcenter.vue +++ b/src/pages/Route/Agency/Card/AgencyWorkcenter.vue @@ -88,7 +88,7 @@ async function deleteWorCenter(id) { </VnPaginate> </div> <QPageSticky :offset="[18, 18]"> - <QBtn @click.stop="dialog.show()" color="primary" fab shortcut="+" icon="add"> + <QBtn @click.stop="dialog.show()" color="primary" fab v-shortcut="'+'" icon="add"> <QDialog ref="dialog"> <FormModelPopup :title="t('Add work center')" diff --git a/src/pages/Route/Card/RouteCard.vue b/src/pages/Route/Card/RouteCard.vue index 81b6cfa16..c178dc6bf 100644 --- a/src/pages/Route/Card/RouteCard.vue +++ b/src/pages/Route/Card/RouteCard.vue @@ -1,12 +1,13 @@ <script setup> import RouteDescriptor from 'pages/Route/Card/RouteDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; +import filter from './RouteFilter.js'; </script> <template> <VnCardBeta data-key="Route" - base-url="Routes" - custom-url="Routes/filter" + url="Routes" + :filter="filter" :descriptor="RouteDescriptor" /> </template> diff --git a/src/pages/Route/Card/RouteDescriptor.vue b/src/pages/Route/Card/RouteDescriptor.vue index 68c08b821..503cd1941 100644 --- a/src/pages/Route/Card/RouteDescriptor.vue +++ b/src/pages/Route/Card/RouteDescriptor.vue @@ -1,13 +1,14 @@ <script setup> import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; -import useCardDescription from 'composables/useCardDescription'; import { dashIfEmpty, toDate } from 'src/filters'; import RouteDescriptorMenu from 'pages/Route/Card/RouteDescriptorMenu.vue'; +import filter from './RouteFilter.js'; +import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; + const $props = defineProps({ id: { type: Number, @@ -17,7 +18,6 @@ const $props = defineProps({ }); const route = useRoute(); -const { t } = useI18n(); const zone = ref(); const zoneId = ref(); const entityId = computed(() => { @@ -36,81 +36,31 @@ const getZone = async () => { const { data: zoneData } = await axios.get(`Zones/${zoneId.value}`); zone.value = zoneData.name; }; - -const filter = { - fields: [ - 'id', - 'workerFk', - 'agencyModeFk', - 'dated', - 'm3', - 'warehouseFk', - 'description', - 'vehicleFk', - 'kmStart', - 'kmEnd', - 'started', - 'finished', - 'cost', - 'isOk', - ], - include: [ - { relation: 'agencyMode', scope: { fields: ['id', 'name'] } }, - { - relation: 'vehicle', - scope: { fields: ['id', 'm3'] }, - }, - { - relation: 'ticket', - scope: { - fields: ['id', 'name', 'zoneFk'], - include: { relation: 'zone', scope: { fields: ['id', 'name'] } }, - }, - }, - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { - fields: ['id'], - include: { relation: 'emailUser', scope: { fields: ['email'] } }, - }, - }, - }, - }, - ], -}; const data = ref(useCardDescription()); const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); onMounted(async () => { getZone(); }); </script> - <template> <CardDescriptor - module="Route" :url="`Routes/${entityId}`" :filter="filter" - :title="data.title" - :subtitle="data.subtitle" - data-key="routeData" - @on-fetch="setData" + :title="null" + data-key="Route" width="lg-width" > <template #body="{ entity }"> - <VnLv :label="t('Date')" :value="toDate(entity?.dated)" /> - <VnLv :label="t('Agency')" :value="entity?.agencyMode?.name" /> - <VnLv :label="t('Zone')" :value="zone" /> + <VnLv :label="$t('Date')" :value="toDate(entity?.dated)" /> + <VnLv :label="$t('Agency')" :value="entity?.agencyMode?.name" /> + <VnLv :label="$t('Zone')" :value="zone" /> <VnLv - :label="t('Volume')" + :label="$t('Volume')" :value="`${dashIfEmpty(entity?.m3)} / ${dashIfEmpty( entity?.vehicle?.m3, )} m³`" /> - <VnLv :label="t('Description')" :value="entity?.description" /> + <VnLv :label="$t('Description')" :value="entity?.description" /> </template> <template #menu="{ entity }"> <RouteDescriptorMenu :route="entity" /> diff --git a/src/pages/Route/Card/RouteFilter.js b/src/pages/Route/Card/RouteFilter.js new file mode 100644 index 000000000..90ee71bf7 --- /dev/null +++ b/src/pages/Route/Card/RouteFilter.js @@ -0,0 +1,39 @@ +export default { + fields: [ + 'code', + 'id', + 'workerFk', + 'agencyModeFk', + 'created', + 'm3', + 'warehouseFk', + 'description', + 'vehicleFk', + 'kmStart', + 'kmEnd', + 'started', + 'finished', + 'cost', + 'isOk', + ], + include: [ + { relation: 'agencyMode', scope: { fields: ['id', 'name'] } }, + { + relation: 'vehicle', + scope: { fields: ['id', 'm3'] }, + }, + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { + fields: ['id'], + include: { relation: 'emailUser', scope: { fields: ['email'] } }, + }, + }, + }, + }, + ], +}; diff --git a/src/pages/Route/Card/RouteFilter.vue b/src/pages/Route/Card/RouteFilter.vue index 72bfed1da..21858102b 100644 --- a/src/pages/Route/Card/RouteFilter.vue +++ b/src/pages/Route/Card/RouteFilter.vue @@ -100,7 +100,7 @@ const emit = defineEmits(['search']); <VnSelect :label="t('Vehicle')" v-model="params.vehicleFk" - url="Vehicles" + url="Vehicles/active" sort-by="numberPlate ASC" option-value="id" option-label="numberPlate" diff --git a/src/pages/Route/Card/RouteForm.vue b/src/pages/Route/Card/RouteForm.vue index 633ff44bc..667204b15 100644 --- a/src/pages/Route/Card/RouteForm.vue +++ b/src/pages/Route/Card/RouteForm.vue @@ -11,6 +11,7 @@ import VnInputDate from 'components/common/VnInputDate.vue'; import VnInput from 'components/common/VnInput.vue'; import axios from 'axios'; import VnInputTime from 'components/common/VnInputTime.vue'; +import filter from './RouteFilter.js'; import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); @@ -27,52 +28,6 @@ const defaultInitialData = { isOk: false, }; const maxDistance = ref(); - -const routeFilter = { - fields: [ - 'id', - 'workerFk', - 'agencyModeFk', - 'dated', - 'm3', - 'warehouseFk', - 'description', - 'vehicleFk', - 'kmStart', - 'kmEnd', - 'started', - 'finished', - 'cost', - 'isOk', - ], - include: [ - { relation: 'agencyMode', scope: { fields: ['id', 'name'] } }, - { - relation: 'vehicle', - scope: { fields: ['id', 'm3'] }, - }, - { - relation: 'ticket', - scope: { - fields: ['id', 'name', 'zoneFk'], - include: { relation: 'zone', scope: { fields: ['id', 'name'] } }, - }, - }, - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { - fields: ['id'], - include: { relation: 'emailUser', scope: { fields: ['email'] } }, - }, - }, - }, - }, - ], -}; const onSave = (data, response) => { if (isNew) { axios.post(`Routes/${response?.id}/updateWorkCenter`); @@ -89,11 +44,10 @@ const onSave = (data, response) => { sort-by="id ASC" /> <FormModel - :url="isNew ? null : `Routes/${route.params?.id}`" :url-create="isNew ? 'Routes' : null" :observe-form-changes="!isNew" - :filter="routeFilter" - model="route" + :filter="filter" + model="Route" :auto-load="!isNew" :form-initial-data="isNew ? defaultInitialData : null" @on-data-saved="onSave" @@ -104,7 +58,7 @@ const onSave = (data, response) => { <VnSelect :label="t('Vehicle')" v-model="data.vehicleFk" - url="Vehicles" + url="Vehicles/active" sort-by="numberPlate ASC" option-value="id" option-label="numberPlate" diff --git a/src/pages/Route/Roadmap/RoadmapBasicData.vue b/src/pages/Route/Roadmap/RoadmapBasicData.vue index 2fe805362..a9e6059c3 100644 --- a/src/pages/Route/Roadmap/RoadmapBasicData.vue +++ b/src/pages/Route/Roadmap/RoadmapBasicData.vue @@ -11,17 +11,16 @@ import VnSelectSupplier from 'src/components/common/VnSelectSupplier.vue'; const { t } = useI18n(); const router = useRouter(); -const filter = { include: [{ relation: 'supplier' }] }; const onSave = (data, response) => { router.push({ name: 'RoadmapSummary', params: { id: response?.id } }); }; </script> <template> <FormModel + :update-url="`Roadmaps/${$route.params?.id}`" :url="`Roadmaps/${$route.params?.id}`" observe-form-changes - :filter="filter" - model="roadmap" + model="Roadmap" auto-load @on-data-saved="onSave" > diff --git a/src/pages/Route/Roadmap/RoadmapCard.vue b/src/pages/Route/Roadmap/RoadmapCard.vue index 0b81de673..48ba516a1 100644 --- a/src/pages/Route/Roadmap/RoadmapCard.vue +++ b/src/pages/Route/Roadmap/RoadmapCard.vue @@ -3,5 +3,5 @@ import VnCardBeta from 'components/common/VnCardBeta.vue'; import RoadmapDescriptor from 'pages/Route/Roadmap/RoadmapDescriptor.vue'; </script> <template> - <VnCardBeta data-key="Roadmap" base-url="Roadmaps" :descriptor="RoadmapDescriptor" /> + <VnCardBeta data-key="Roadmap" url="Roadmaps" :descriptor="RoadmapDescriptor" /> </template> diff --git a/src/pages/Route/Roadmap/RoadmapDescriptor.vue b/src/pages/Route/Roadmap/RoadmapDescriptor.vue index 788173688..baa864a15 100644 --- a/src/pages/Route/Roadmap/RoadmapDescriptor.vue +++ b/src/pages/Route/Roadmap/RoadmapDescriptor.vue @@ -1,13 +1,13 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; -import useCardDescription from 'composables/useCardDescription'; import { dashIfEmpty, toDateHourMin } from 'src/filters'; import SupplierDescriptorProxy from 'pages/Supplier/Card/SupplierDescriptorProxy.vue'; import RoadmapDescriptorMenu from 'pages/Route/Roadmap/RoadmapDescriptorMenu.vue'; +import filter from 'pages/Route/Roadmap/RoadmapFilter.js'; const $props = defineProps({ id: { @@ -23,22 +23,10 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); - -const filter = { include: [{ relation: 'supplier' }] }; -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); </script> <template> - <CardDescriptor - module="Roadmap" - :url="`Roadmaps/${entityId}`" - :filter="filter" - :title="data.title" - :subtitle="data.subtitle" - data-key="Roadmap" - @on-fetch="setData" - > + <CardDescriptor :url="`Roadmaps/${entityId}`" :filter="filter" data-key="Roadmap"> <template #body="{ entity }"> <VnLv :label="t('Roadmap')" :value="entity?.name" /> <VnLv :label="t('ETD')" :value="toDateHourMin(entity?.etd)" /> diff --git a/src/pages/Route/Roadmap/RoadmapFilter.js b/src/pages/Route/Roadmap/RoadmapFilter.js new file mode 100644 index 000000000..0ae890363 --- /dev/null +++ b/src/pages/Route/Roadmap/RoadmapFilter.js @@ -0,0 +1,3 @@ +export default { + include: [{ relation: 'supplier' }], +}; diff --git a/src/pages/Route/Roadmap/RoadmapStops.vue b/src/pages/Route/Roadmap/RoadmapStops.vue index d8215ea49..e4085d572 100644 --- a/src/pages/Route/Roadmap/RoadmapStops.vue +++ b/src/pages/Route/Roadmap/RoadmapStops.vue @@ -68,7 +68,7 @@ const updateDefaultStop = (data) => { <QBtn flat icon="add" - shortcut="+" + v-shortcut="'+'" class="cursor-pointer" color="primary" @click="roadmapStopsCrudRef.insert()" diff --git a/src/pages/Route/Roadmap/RoadmapSummary.vue b/src/pages/Route/Roadmap/RoadmapSummary.vue index 1fbb1897d..0c1c2b903 100644 --- a/src/pages/Route/Roadmap/RoadmapSummary.vue +++ b/src/pages/Route/Roadmap/RoadmapSummary.vue @@ -67,7 +67,6 @@ const filter = { }, }, ], - where: { id: entityId }, }; </script> @@ -76,7 +75,7 @@ const filter = { <CardSummary data-key="RoadmapSummary" ref="summary" - :url="`Roadmaps`" + :url="`Roadmaps/${entityId}`" :filter="filter" > <template #header-left> diff --git a/src/pages/Route/RouteExtendedList.vue b/src/pages/Route/RouteExtendedList.vue index 7cc00aa5c..f32dcd0d9 100644 --- a/src/pages/Route/RouteExtendedList.vue +++ b/src/pages/Route/RouteExtendedList.vue @@ -3,7 +3,7 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useQuasar } from 'quasar'; -import { toDate } from 'src/filters'; +import { dashIfEmpty, toDate, toHour } from 'src/filters'; import { useRouter } from 'vue-router'; import { usePrintService } from 'src/composables/usePrintService'; @@ -38,7 +38,7 @@ const routeFilter = { }; const columns = computed(() => [ { - align: 'left', + align: 'center', name: 'id', label: 'Id', chip: { @@ -48,7 +48,7 @@ const columns = computed(() => [ columnFilter: false, }, { - align: 'left', + align: 'center', name: 'workerFk', label: t('route.Worker'), create: true, @@ -68,10 +68,10 @@ const columns = computed(() => [ }, useLike: false, cardVisible: true, - format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), + format: (row, dashIfEmpty) => dashIfEmpty(row.workerUserName), }, { - align: 'left', + align: 'center', name: 'agencyModeFk', label: t('route.Agency'), isTitle: true, @@ -87,17 +87,17 @@ const columns = computed(() => [ }, }, columnClass: 'expand', + format: (row, dashIfEmpty) => dashIfEmpty(row.agencyName), }, { - align: 'left', + align: 'center', name: 'vehicleFk', label: t('route.Vehicle'), cardVisible: true, create: true, component: 'select', attrs: { - url: 'vehicles', - fields: ['id', 'numberPlate'], + url: 'vehicles/active', optionLabel: 'numberPlate', optionFilterValue: 'numberPlate', find: { @@ -108,29 +108,31 @@ const columns = computed(() => [ columnFilter: { inWhere: true, }, + format: (row, dashIfEmpty) => dashIfEmpty(row.vehiclePlateNumber), }, { - align: 'left', + align: 'center', name: 'dated', label: t('route.Date'), columnFilter: false, cardVisible: true, create: true, component: 'date', - format: ({ date }) => toDate(date), + format: ({ dated }, dashIfEmpty) => + dated === '0000-00-00' ? dashIfEmpty(null) : toDate(dated), }, { - align: 'left', + align: 'center', name: 'from', label: t('route.From'), visible: false, cardVisible: true, create: true, component: 'date', - format: ({ date }) => toDate(date), + format: ({ from }) => toDate(from), }, { - align: 'left', + align: 'center', name: 'to', label: t('route.To'), visible: false, @@ -147,18 +149,20 @@ const columns = computed(() => [ columnClass: 'shrink', }, { - align: 'left', + align: 'center', name: 'started', label: t('route.hourStarted'), component: 'time', columnFilter: false, + format: ({ started }) => toHour(started), }, { - align: 'left', + align: 'center', name: 'finished', label: t('route.hourFinished'), component: 'time', columnFilter: false, + format: ({ finished }) => toHour(finished), }, { align: 'center', @@ -177,7 +181,7 @@ const columns = computed(() => [ visible: false, }, { - align: 'left', + align: 'center', name: 'description', label: t('route.Description'), isTitle: true, @@ -186,7 +190,7 @@ const columns = computed(() => [ field: 'description', }, { - align: 'left', + align: 'center', name: 'isOk', label: t('route.Served'), component: 'checkbox', @@ -300,60 +304,62 @@ const openTicketsDialog = (id) => { <RouteFilter data-key="RouteList" /> </template> </RightMenu> - <VnTable - class="route-list" - ref="tableRef" - data-key="RouteList" - url="Routes/filter" - :columns="columns" - :right-search="false" - :is-editable="true" - :filter="routeFilter" - redirect="route" - :row-click="false" - :create="{ - urlCreate: 'Routes', - title: t('route.createRoute'), - onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: {}, - }" - save-url="Routes/crud" - :disable-option="{ card: true }" - table-height="85vh" - v-model:selected="selectedRows" - :table="{ - 'row-key': 'id', - selection: 'multiple', - }" - > - <template #moreBeforeActions> - <QBtn - icon="vn:clone" - color="primary" - class="q-mr-sm" - :disable="!selectedRows?.length" - @click="confirmationDialog = true" - > - <QTooltip>{{ t('route.Clone Selected Routes') }}</QTooltip> - </QBtn> - <QBtn - icon="cloud_download" - color="primary" - class="q-mr-sm" - :disable="!selectedRows?.length" - @click="showRouteReport" - > - <QTooltip>{{ t('route.Download selected routes as PDF') }}</QTooltip> - </QBtn> - <QBtn - icon="check" - color="primary" - class="q-mr-sm" - :disable="!selectedRows?.length" - @click="markAsServed()" - > - <QTooltip>{{ t('route.Mark as served') }}</QTooltip> - </QBtn> - </template> - </VnTable> + <QPage class="q-px-md"> + <VnTable + class="route-list" + ref="tableRef" + data-key="RouteList" + url="Routes/filter" + :columns="columns" + :right-search="false" + :is-editable="true" + :filter="routeFilter" + redirect="route" + :row-click="false" + :create="{ + urlCreate: 'Routes', + title: t('route.createRoute'), + onDataSaved: ({ id }) => tableRef.redirect(id), + formInitialData: {}, + }" + save-url="Routes/crud" + :disable-option="{ card: true }" + table-height="85vh" + v-model:selected="selectedRows" + :table="{ + 'row-key': 'id', + selection: 'multiple', + }" + > + <template #moreBeforeActions> + <QBtn + icon="vn:clone" + color="primary" + class="q-mr-sm" + :disable="!selectedRows?.length" + @click="confirmationDialog = true" + > + <QTooltip>{{ t('route.Clone Selected Routes') }}</QTooltip> + </QBtn> + <QBtn + icon="cloud_download" + color="primary" + class="q-mr-sm" + :disable="!selectedRows?.length" + @click="showRouteReport" + > + <QTooltip>{{ t('route.Download selected routes as PDF') }}</QTooltip> + </QBtn> + <QBtn + icon="check" + color="primary" + class="q-mr-sm" + :disable="!selectedRows?.length" + @click="markAsServed()" + > + <QTooltip>{{ t('route.Mark as served') }}</QTooltip> + </QBtn> + </template> + </VnTable> + </QPage> </template> diff --git a/src/pages/Route/RouteList.vue b/src/pages/Route/RouteList.vue index bc3227f6c..9dad8ba22 100644 --- a/src/pages/Route/RouteList.vue +++ b/src/pages/Route/RouteList.vue @@ -38,6 +38,17 @@ const columns = computed(() => [ align: 'left', name: 'workerFk', label: t('route.Worker'), + component: 'select', + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + useLike: false, + optionFilter: 'firstName', + find: { + value: 'workerFk', + label: 'workerUserName', + }, + }, create: true, cardVisible: true, format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), @@ -48,6 +59,15 @@ const columns = computed(() => [ name: 'agencyName', label: t('route.Agency'), cardVisible: true, + component: 'select', + attrs: { + url: 'agencyModes', + fields: ['id', 'name'], + find: { + value: 'agencyModeFk', + label: 'agencyName', + }, + }, create: true, columnClass: 'expand', columnFilter: false, @@ -57,6 +77,17 @@ const columns = computed(() => [ name: 'vehiclePlateNumber', label: t('route.Vehicle'), cardVisible: true, + component: 'select', + attrs: { + url: 'vehicles', + fields: ['id', 'numberPlate'], + optionLabel: 'numberPlate', + optionFilterValue: 'numberPlate', + find: { + value: 'vehicleFk', + label: 'vehiclePlateNumber', + }, + }, create: true, columnFilter: false, }, diff --git a/src/pages/Route/RouteTickets.vue b/src/pages/Route/RouteTickets.vue index 1416f77ce..adc7dfdaa 100644 --- a/src/pages/Route/RouteTickets.vue +++ b/src/pages/Route/RouteTickets.vue @@ -120,8 +120,8 @@ const deletePriorities = async () => { try { await Promise.all( selectedRows.value.map((ticket) => - axios.patch(`Tickets/${ticket?.id}/`, { priority: null }) - ) + axios.patch(`Tickets/${ticket?.id}/`, { priority: null }), + ), ); } finally { refreshKey.value++; @@ -132,8 +132,8 @@ const setOrderedPriority = async () => { try { await Promise.all( ticketList.value.map((ticket, index) => - axios.patch(`Tickets/${ticket?.id}/`, { priority: index + 1 }) - ) + axios.patch(`Tickets/${ticket?.id}/`, { priority: index + 1 }), + ), ); } finally { refreshKey.value++; @@ -162,7 +162,7 @@ const setHighestPriority = async (ticket, ticketList) => { const goToBuscaman = async (ticket = null) => { await openBuscaman( routeEntity.value?.vehicleFk, - ticket ? [ticket] : selectedRows.value + ticket ? [ticket] : selectedRows.value, ); }; @@ -393,7 +393,13 @@ const openSmsDialog = async () => { </VnPaginate> </div> <QPageSticky :offset="[20, 20]"> - <QBtn fab icon="add" shortcut="+" color="primary" @click="openTicketsDialog"> + <QBtn + fab + icon="add" + v-shortcut="'+'" + color="primary" + @click="openTicketsDialog" + > <QTooltip> {{ t('Add ticket') }} </QTooltip> diff --git a/src/pages/Route/Vehicle/Card/VehicleBasicData.vue b/src/pages/Route/Vehicle/Card/VehicleBasicData.vue new file mode 100644 index 000000000..e78bc6edd --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleBasicData.vue @@ -0,0 +1,162 @@ +<script setup> +import { ref } from 'vue'; +import FormModel from 'components/FormModel.vue'; +import FetchData from 'src/components/FetchData.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; + +const warehouses = ref([]); +const companies = ref([]); +const countries = ref([]); +const fuelTypes = ref([]); +const bankPolicies = ref([]); +const deliveryPoints = ref([]); +</script> +<template> + <FetchData + url="Warehouses" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (warehouses = data)" + auto-load + /> + <FetchData + url="Companies" + :filter="{ fields: ['id', 'code'] }" + @on-fetch="(data) => (companies = data)" + auto-load + /> + <FetchData + url="Countries" + :filter="{ fields: ['code'] }" + @on-fetch="(data) => (countries = data)" + auto-load + /> + <FetchData + url="FuelTypes" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (fuelTypes = data)" + auto-load + /> + <FetchData + url="DeliveryPoints" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (deliveryPoints = data)" + auto-load + /> + <FormModel model="Vehicle" :url-update="`Vehicles/${$route.params.id}`"> + <template #form="{ data }"> + <VnRow> + <VnInput v-model="data.description" :label="$t('globals.description')" /> + <VnInput v-model="data.numberPlate" :label="$t('vehicle.numberPlate')" /> + </VnRow> + <VnRow> + <VnInput + v-model="data.model" + :label="$t('globals.model')" + :required="true" + /> + <VnSelect + url="VehicleTypes" + v-model="data.vehicleTypeFk" + :label="$t('globals.type')" + /> + </VnRow> + <VnRow> + <VnInput + v-model="data.tradeMark" + :label="$t('vehicle.tradeMark')" + :required="true" + /> + <VnInput v-model="data.chassis" :label="$t('vehicle.chassis')" /> + </VnRow> + <VnRow> + <VnSelect + v-model="data.fuelTypeFk" + :label="$t('globals.fuel')" + :options="fuelTypes" + /> + <VnSelect + v-model="data.deliveryPointFk" + :label="$t('globals.deliveryPoint')" + :options="deliveryPoints" + /> + </VnRow> + <VnRow> + <VnSelect + v-model="data.companyFk" + :label="$t('globals.company')" + :options="companies" + option-label="code" + /> + <VnSelect + v-model="data.warehouseFk" + :label="$t('globals.warehouse')" + :options="warehouses" + /> + </VnRow> + <VnRow> + <VnSelect + url="Suppliers" + :filter="{ fields: ['id', 'name'] }" + v-model="data.supplierFk" + :label="$t('globals.supplier')" + /> + <VnSelect + url="Suppliers" + :filter="{ fields: ['id', 'name'] }" + v-model="data.supplierCoolerFk" + :label="$t('vehicle.supplierCooler')" + /> + </VnRow> + <VnRow> + <VnSelect + url="BankPolicies" + :filter="{ fields: ['id', 'ref'] }" + v-model="data.bankPolicyFk" + :label="$t('vehicle.leasing')" + :options="bankPolicies" + option-label="ref" + option-value="id" + /> + <VnInput v-model="data.leasing" :label="$t('vehicle.nLeasing')" /> + </VnRow> + <VnRow> + <VnInputNumber v-model="data.import" :label="$t('globals.amount')" /> + <VnInputNumber + v-model="data.importCooler" + :label="$t('vehicle.amountCooler')" + /> + </VnRow> + <VnRow> + <VnSelect + url="Ppes" + option-label="id" + v-model="data.ppeFk" + :label="$t('vehicle.ppe')" + /> + <VnSelect + v-model="data.countryCodeFk" + :label="$t('globals.country')" + :options="countries" + option-label="code" + option-value="code" + /> + </VnRow> + <VnRow> + <VnInput v-model="data.vin" :label="$t('vehicle.vin')" /> + <span :style="{ 'align-self': $q.screen.gt.xs ? 'end' : 'unset' }"> + <QCheckbox + v-model="data.isActive" + :label="$t('vehicle.isActive')" + :false-value="0" + :true-value="1" + dense + class="q-mt-sm" + /> + </span> + </VnRow> + </template> + </FormModel> +</template> diff --git a/src/pages/Route/Vehicle/Card/VehicleCard.vue b/src/pages/Route/Vehicle/Card/VehicleCard.vue new file mode 100644 index 000000000..f59420aa2 --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleCard.vue @@ -0,0 +1,13 @@ +<script setup> +import VnCardBeta from 'components/common/VnCardBeta.vue'; +import VehicleDescriptor from './VehicleDescriptor.vue'; +import VehicleFilter from '../VehicleFilter.js'; +</script> +<template> + <VnCardBeta + data-key="Vehicle" + url="Vehicles" + :filter="VehicleFilter" + :descriptor="VehicleDescriptor" + /> +</template> diff --git a/src/pages/Route/Vehicle/Card/VehicleDescriptor.vue b/src/pages/Route/Vehicle/Card/VehicleDescriptor.vue new file mode 100644 index 000000000..d9a2434ab --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleDescriptor.vue @@ -0,0 +1,49 @@ +<script setup> +import VnLv from 'src/components/ui/VnLv.vue'; +import CardDescriptor from 'components/ui/CardDescriptor.vue'; +import axios from 'axios'; +import useNotify from 'src/composables/useNotify.js'; + +const { notify } = useNotify(); +</script> +<template> + <CardDescriptor + :url="`Vehicles/${$route.params.id}`" + data-key="Vehicle" + title="numberPlate" + :to-module="{ name: 'VehicleList' }" + > + <template #menu="{ entity }"> + <QItem + data-cy="delete" + v-ripple + clickable + @click=" + async () => { + try { + await axios.delete(`Vehicles/${entity.id}`); + notify('vehicle.remove', 'positive'); + $router.push({ name: 'VehicleList' }); + } catch (e) { + throw e; + } + } + " + > + <QItemSection> + {{ $t('vehicle.delete') }} + </QItemSection> + </QItem> + </template> + <template #body="{ entity }"> + <VnLv :label="$t('vehicle.numberPlate')" :value="entity.numberPlate" /> + <VnLv :label="$t('vehicle.tradeMark')" :value="entity.tradeMark" /> + <VnLv :label="$t('globals.model')" :value="entity.model" /> + <VnLv :label="$t('globals.country')" :value="entity.countryCodeFk" /> + </template> + </CardDescriptor> +</template> +<i18n> +es: + Vehicle removed: Vehículo eliminado +</i18n> diff --git a/src/pages/Route/Vehicle/Card/VehicleSummary.vue b/src/pages/Route/Vehicle/Card/VehicleSummary.vue new file mode 100644 index 000000000..981870cb2 --- /dev/null +++ b/src/pages/Route/Vehicle/Card/VehicleSummary.vue @@ -0,0 +1,127 @@ +<script setup> +import { computed } from 'vue'; +import { useRoute } from 'vue-router'; +import CardSummary from 'components/ui/CardSummary.vue'; +import VnLv from 'src/components/ui/VnLv.vue'; +import VnTitle from 'src/components/common/VnTitle.vue'; +import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; +import VehicleFilter from '../VehicleFilter.js'; +import { downloadFile } from 'src/composables/downloadFile'; +import { dashIfEmpty } from 'src/filters'; + +const props = defineProps({ id: { type: [Number, String], default: null } }); + +const route = useRoute(); +const entityId = computed(() => props.id || +route.params.id); +const links = { + 'basic-data': `#/vehicle/${entityId.value}/basic-data`, + notes: `#/vehicle/${entityId.value}/notes`, + dms: `#/vehicle/${entityId.value}/dms`, + 'invoice-in': `#/vehicle/${entityId.value}/invoice-in`, + events: `#/vehicle/${entityId.value}/events`, +}; +</script> +<template> + <CardSummary data-key="Vehicle" :url="`Vehicles/${entityId}`" :filter="VehicleFilter"> + <template #header="{ entity }"> + <div>{{ entity.id }} - {{ entity.numberPlate }}</div> + </template> + <template #body="{ entity }"> + <QCard class="vn-one"> + <QCardSection dense> + <VnTitle + :url="links['basic-data']" + :text="$t('globals.pageTitles.basicData')" + /> + </QCardSection> + <QCardSection content> + <QList dense> + <VnLv + :label="$t('globals.description')" + :value="entity.description" + /> + <VnLv + :label="$t('vehicle.tradeMark')" + :value="entity.tradeMark" + /> + <VnLv :label="$t('globals.model')" :value="entity.model" /> + <VnLv :label="$t('globals.supplier')"> + <template #value> + <span class="link"> + {{ entity.supplier?.name }} + <SupplierDescriptorProxy :id="entity.supplierFk" /> + </span> + </template> + </VnLv> + <VnLv :label="$t('vehicle.supplierCooler')"> + <template #value> + <span class="link"> + {{ entity.supplierCooler?.name }} + <SupplierDescriptorProxy + :id="entity.supplierCoolerFk" + /> + </span> + </template> + </VnLv> + <VnLv :label="$t('vehicle.vin')" :value="entity.vin" /> + </QList> + <QList dense> + <VnLv :label="$t('vehicle.chassis')" :value="entity.chassis" /> + <VnLv + :label="$t('globals.fuel')" + :value="entity.fuelType?.name" + /> + <VnLv :label="$t('vehicle.ppe')" :value="entity.ppeFk" /> + <VnLv :label="$t('vehicle.nLeasing')" :value="entity.leasing" /> + <VnLv + :label="$t('vehicle.leasing')" + :value="entity.bankPolicy?.ref" + > + <template #value> + <span v-text="dashIfEmpty(entity.bankPolicy?.name)" /> + <QBtn + v-if="entity.bankPolicy?.dmsFk" + class="q-ml-xs" + color="primary" + flat + dense + icon="cloud_download" + @click="downloadFile(entity.bankPolicy?.dmsFk)" + > + <QTooltip>{{ $t('globals.download') }}</QTooltip> + </QBtn> + </template> + </VnLv> + <VnLv :label="$t('globals.amount')" :value="entity.import" /> + </QList> + <QList dense> + <VnLv + :label="$t('globals.warehouse')" + :value="entity.warehouse?.name" + /> + <VnLv + :label="$t('globals.company')" + :value="entity.company?.code" + /> + <VnLv + :label="$t('globals.deliveryPoint')" + :value="entity.deliveryPoint?.name" + /> + <VnLv + :label="$t('globals.country')" + :value="entity.countryCodeFk" + /> + <VnLv + :label="$t('vehicle.isKmTruckRate')" + :value="!!entity.isKmTruckRate" + /> + <VnLv + :label="$t('vehicle.isActive')" + :value="!!entity.isActive" + /> + </QList> + </QCardSection> + </QCard> + </template> + </CardSummary> +</template> diff --git a/src/pages/Route/Vehicle/VehicleFilter.js b/src/pages/Route/Vehicle/VehicleFilter.js new file mode 100644 index 000000000..cbf5cc621 --- /dev/null +++ b/src/pages/Route/Vehicle/VehicleFilter.js @@ -0,0 +1,76 @@ +export default { + fields: [ + 'id', + 'description', + 'isActive', + 'isKmTruckRate', + 'warehouseFk', + 'companyFk', + 'numberPlate', + 'chassis', + 'supplierFk', + 'supplierCoolerFk', + 'tradeMark', + 'fuelTypeFk', + 'import', + 'importCooler', + 'vin', + 'model', + 'ppeFk', + 'countryCodeFk', + 'leasing', + 'bankPolicyFk', + 'vehicleTypeFk', + 'deliveryPointFk', + ], + include: [ + { + relation: 'warehouse', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'company', + scope: { + fields: ['id', 'code'], + }, + }, + { + relation: 'supplier', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'supplierCooler', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'fuelType', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'bankPolicy', + scope: { + fields: ['id', 'ref', 'dmsFk'], + }, + }, + { + relation: 'ppe', + scope: { + fields: ['id'], + }, + }, + { + relation: 'deliveryPoint', + scope: { + fields: ['id', 'name'], + }, + }, + ], +}; diff --git a/src/pages/Route/Vehicle/VehicleList.vue b/src/pages/Route/Vehicle/VehicleList.vue new file mode 100644 index 000000000..e5b945010 --- /dev/null +++ b/src/pages/Route/Vehicle/VehicleList.vue @@ -0,0 +1,224 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import VnTable from 'components/VnTable/VnTable.vue'; +import FetchData from 'src/components/FetchData.vue'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import VehicleSummary from 'src/pages/Route/Vehicle/Card/VehicleSummary.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import VnSection from 'src/components/common/VnSection.vue'; + +const { t } = useI18n(); +const { viewSummary } = useSummaryDialog(); +const warehouses = ref([]); +const companies = ref([]); +const countries = ref([]); +const vehicleStates = ref([]); +const vehicleTypes = ref([]); + +const columns = computed(() => [ + { + name: 'isActive', + columnFilter: false, + align: 'center', + }, + { + name: 'id', + label: t('globals.id'), + isId: true, + chip: { + condition: () => true, + }, + }, + { + name: 'description', + label: t('globals.description'), + }, + { + name: 'tradeMark', + label: t('vehicle.tradeMark'), + cardVisible: true, + }, + { + name: 'numberPlate', + label: t('vehicle.numberPlate'), + isTitle: true, + }, + { + name: 'vehicleTypeFk', + label: t('globals.type'), + format: (row) => row.type, + columnFilter: { + component: 'select', + name: 'vehicleTypeFk', + options: vehicleTypes.value, + }, + cardVisible: true, + }, + { + name: 'vehicleStateFk', + label: t('globals.state'), + columnFilter: { + component: 'select', + name: 'vehicleStateFk', + optionLabel: 'state', + options: vehicleStates.value, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row.state), + }, + { + name: 'chassis', + label: t('vehicle.chassis'), + }, + { + name: 'leasing', + label: t('vehicle.leasing'), + }, + { + name: 'warehouseFk', + label: t('globals.warehouse'), + format: (row, dashIfEmpty) => dashIfEmpty(row.warehouse), + columnFilter: { + component: 'select', + name: 'warehouseFk', + options: warehouses.value, + }, + cardVisible: true, + }, + { + name: 'companyFk', + label: t('globals.company'), + format: (row, dashIfEmpty) => dashIfEmpty(row.company), + columnFilter: { + component: 'select', + name: 'companyFk', + optionLabel: 'code', + options: companies.value, + }, + }, + { + name: 'countryCodeFk', + label: t('globals.country'), + columnFilter: { + component: 'select', + name: 'countryCodeFk', + optionValue: 'code', + optionLabel: 'code', + options: countries.value, + }, + }, + { + align: 'right', + name: 'tableActions', + actions: [ + { + title: t('components.smartCard.openSummary'), + icon: 'preview', + action: (row) => viewSummary(row.id, VehicleSummary), + }, + ], + }, +]); +</script> +<template> + <FetchData + url="Warehouses" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (warehouses = data)" + auto-load + /> + <FetchData + url="Companies" + :filter="{ fields: ['id', 'code'] }" + @on-fetch="(data) => (companies = data)" + auto-load + /> + <FetchData + url="Countries" + :filter="{ fields: ['name', 'code'] }" + @on-fetch="(data) => (countries = data)" + auto-load + /> + <FetchData + url="VehicleStates" + :filter="{ fields: ['id', 'state'] }" + @on-fetch="(data) => (vehicleStates = data)" + auto-load + /> + <FetchData + url="VehicleTypes" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (vehicleTypes = data)" + auto-load + /> + <VnSection + data-key="VehicleList" + :columns="columns" + prefix="vehicle" + :array-data-props="{ + url: 'Vehicles/filter', + }" + > + <template #body> + <VnTable + ref="tableRef" + data-key="VehicleList" + :columns="columns" + redirect="route/vehicle" + :create="{ + urlCreate: 'Vehicles', + title: t('vehicle.create'), + onDataSaved: ({ id }) => $refs.tableRef.redirect(id), + formInitialData: { isActive: true, isKmTruckRate: false }, + }" + :use-model="true" + :right-search="false" + > + <template #column-isActive="{ row }"> + <span> + <QIcon + v-if="!row.isActive" + name="vn:inactive-car" + color="primary" + size="xs" + > + <QTooltip>{{ $t('globals.inactive') }}</QTooltip> + </QIcon> + </span> + </template> + <template #more-create-dialog="{ data }"> + <VnInput + v-model="data.numberPlate" + :label="$t('vehicle.numberPlate')" + :uppercase="true" + /> + <VnInput v-model="data.tradeMark" :label="$t('vehicle.tradeMark')" /> + <VnInput v-model="data.model" :label="$t('globals.model')" /> + <VnSelect + v-model="data.vehicleTypeFk" + :label="$t('globals.type')" + :options="vehicleTypes" + /> + <VnSelect + v-model="data.warehouseFk" + :label="$t('globals.warehouse')" + :options="warehouses" + /> + <VnSelect + v-model="data.countryCodeFk" + :label="$t('globals.country')" + option-value="code" + option-label="name" + :options="countries" + /> + <VnInput + v-model="data.description" + :label="$t('globals.description')" + /> + <QCheckbox to v-model="data.isActive" :label="$t('globals.active')" /> + </template> + </VnTable> + </template> + </VnSection> +</template> diff --git a/src/pages/Route/Vehicle/locale/en.yml b/src/pages/Route/Vehicle/locale/en.yml new file mode 100644 index 000000000..c92022f9d --- /dev/null +++ b/src/pages/Route/Vehicle/locale/en.yml @@ -0,0 +1,20 @@ +vehicle: + tradeMark: Trade Mark + numberPlate: Nº Plate + chassis: Chassis + leasing: Leasing + isKmTruckRate: Trailer + delete: Delete Vehicle + supplierCooler: Supplier Cooler + vin: VIN + ppe: Ppe + isActive: Active + nLeasing: Nº Leasing + create: Create Vehicle + amountCooler: Amount cooler + remove: Vehicle removed + search: Search Vehicle + searchInfo: Search by id or number plate + params: + vehicleTypeFk: Type + vehicleStateFk: State diff --git a/src/pages/Route/Vehicle/locale/es.yml b/src/pages/Route/Vehicle/locale/es.yml new file mode 100644 index 000000000..c878f97ac --- /dev/null +++ b/src/pages/Route/Vehicle/locale/es.yml @@ -0,0 +1,20 @@ +vehicle: + tradeMark: Marca + numberPlate: Matrícula + chassis: Nº de bastidor + leasing: Leasing + isKmTruckRate: Trailer + delete: Eliminar vehículo + supplierCooler: Proveedor Frío + vin: VIN + ppe: Nº Inmovilizado + create: Crear vehículo + amountCooler: Importe frío + isActive: Activo + nLeasing: Nº leasing + remove: Vehículo eliminado + search: Buscar Vehículo + searchInfo: Buscar por id o matrícula + params: + vehicleTypeFk: Tipo + vehicleStateFk: Estado diff --git a/src/pages/Shelving/Card/ShelvingCard.vue b/src/pages/Shelving/Card/ShelvingCard.vue index 41a0db33c..9e0ac8ad2 100644 --- a/src/pages/Shelving/Card/ShelvingCard.vue +++ b/src/pages/Shelving/Card/ShelvingCard.vue @@ -1,12 +1,14 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import ShelvingDescriptor from 'pages/Shelving/Card/ShelvingDescriptor.vue'; +import filter from './ShelvingFilter.js'; </script> <template> <VnCardBeta data-key="Shelving" - base-url="Shelvings" + url="Shelvings" + :filter="filter" :descriptor="ShelvingDescriptor" /> </template> diff --git a/src/pages/Shelving/Card/ShelvingDescriptor.vue b/src/pages/Shelving/Card/ShelvingDescriptor.vue index b1ff4a8ae..5e618aa7f 100644 --- a/src/pages/Shelving/Card/ShelvingDescriptor.vue +++ b/src/pages/Shelving/Card/ShelvingDescriptor.vue @@ -1,12 +1,12 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; -import useCardDescription from 'composables/useCardDescription'; import ShelvingDescriptorMenu from 'pages/Shelving/Card/ShelvingDescriptorMenu.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; +import filter from './ShelvingFilter.js'; const $props = defineProps({ id: { @@ -22,35 +22,13 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); - -const filter = { - include: [ - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { fields: ['nickname'] }, - }, - }, - }, - { relation: 'parking' }, - ], -}; -const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity.code, entity.id)); </script> - <template> <CardDescriptor - module="Shelving" :url="`Shelvings/${entityId}`" :filter="filter" - :title="data.title" - :subtitle="data.subtitle" - data-key="Shelvings" - @on-fetch="setData" + title="code" + data-key="Shelving" > <template #body="{ entity }"> <VnLv :label="t('globals.code')" :value="entity.code" /> diff --git a/src/pages/Shelving/Card/ShelvingFilter.js b/src/pages/Shelving/Card/ShelvingFilter.js new file mode 100644 index 000000000..e302e1b9c --- /dev/null +++ b/src/pages/Shelving/Card/ShelvingFilter.js @@ -0,0 +1,15 @@ +export default { + include: [ + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { fields: ['nickname'] }, + }, + }, + }, + { relation: 'parking' }, + ], +}; diff --git a/src/pages/Shelving/Card/ShelvingForm.vue b/src/pages/Shelving/Card/ShelvingForm.vue index 3bbd94a0a..078058342 100644 --- a/src/pages/Shelving/Card/ShelvingForm.vue +++ b/src/pages/Shelving/Card/ShelvingForm.vue @@ -1,5 +1,4 @@ <script setup> -import { useI18n } from 'vue-i18n'; import { computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import VnRow from 'components/ui/VnRow.vue'; @@ -7,8 +6,8 @@ import FormModel from 'components/FormModel.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; +import filter from './ShelvingFilter.js'; -const { t } = useI18n(); const route = useRoute(); const router = useRouter(); const entityId = computed(() => route.params.id ?? null); @@ -20,22 +19,6 @@ const defaultInitialData = { isRecyclable: false, }; -const shelvingFilter = { - include: [ - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { fields: ['nickname'] }, - }, - }, - }, - { relation: 'parking' }, - ], -}; - const onSave = (shelving, newShelving) => { if (isNew) { router.push({ name: 'ShelvingBasicData', params: { id: newShelving?.id } }); @@ -45,11 +28,10 @@ const onSave = (shelving, newShelving) => { <template> <VnSubToolbar v-if="isNew" /> <FormModel - :url="isNew ? null : `Shelvings/${entityId}`" :url-create="isNew ? 'Shelvings' : null" :observe-form-changes="!isNew" - :filter="shelvingFilter" - model="shelving" + :filter="filter" + model="Shelving" :auto-load="!isNew" :form-initial-data="isNew ? defaultInitialData : null" @on-data-saved="onSave" @@ -58,7 +40,7 @@ const onSave = (shelving, newShelving) => { <VnRow> <VnInput v-model="data.code" - :label="t('globals.code')" + :label="$t('globals.code')" :rules="validate('Shelving.code')" /> <VnSelect @@ -68,7 +50,7 @@ const onSave = (shelving, newShelving) => { option-label="code" :filter-options="['id', 'code']" :fields="['id', 'code']" - :label="t('shelving.list.parking')" + :label="$t('shelving.list.parking')" :rules="validate('Shelving.parkingFk')" /> </VnRow> @@ -76,12 +58,12 @@ const onSave = (shelving, newShelving) => { <VnInput v-model="data.priority" type="number" - :label="t('shelving.list.priority')" + :label="$t('shelving.list.priority')" :rules="validate('Shelving.priority')" /> <QCheckbox v-model="data.isRecyclable" - :label="t('shelving.summary.recyclable')" + :label="$t('shelving.summary.recyclable')" :rules="validate('Shelving.isRecyclable')" /> </VnRow> diff --git a/src/pages/Shelving/Card/ShelvingSearchbar.vue b/src/pages/Shelving/Card/ShelvingSearchbar.vue index bfc8ad4f5..741b11663 100644 --- a/src/pages/Shelving/Card/ShelvingSearchbar.vue +++ b/src/pages/Shelving/Card/ShelvingSearchbar.vue @@ -1,15 +1,15 @@ <script setup> import VnSearchbar from 'components/ui/VnSearchbar.vue'; -import {useI18n} from "vue-i18n"; -const { t } = useI18n(); +import exprBuilder from '../ShelvingExprBuilder.js'; </script> <template> <VnSearchbar data-key="ShelvingList" url="Shelvings" - :label="t('Search shelving')" - :info="t('You can search by shelving reference')" + :label="$t('Search shelving')" + :info="$t('You can search by shelving reference')" + :expr-builder="exprBuilder" /> </template> diff --git a/src/pages/Shelving/Card/ShelvingSummary.vue b/src/pages/Shelving/Card/ShelvingSummary.vue index 39fa4639f..f89ff4d78 100644 --- a/src/pages/Shelving/Card/ShelvingSummary.vue +++ b/src/pages/Shelving/Card/ShelvingSummary.vue @@ -1,10 +1,10 @@ <script setup> import { computed, ref } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'components/ui/VnLv.vue'; import VnUserLink from 'components/ui/VnUserLink.vue'; +import filter from './ShelvingFilter.js'; import ShelvingDescriptorMenu from './ShelvingDescriptorMenu.vue'; const $props = defineProps({ @@ -14,25 +14,9 @@ const $props = defineProps({ }, }); const route = useRoute(); -const { t } = useI18n(); + const summary = ref({}); const entityId = computed(() => $props.id || route.params.id); - -const filter = { - include: [ - { - relation: 'worker', - scope: { - fields: ['id'], - include: { - relation: 'user', - scope: { fields: ['nickname'] }, - }, - }, - }, - { relation: 'parking' }, - ], -}; </script> <template> @@ -41,7 +25,7 @@ const filter = { ref="summary" :url="`Shelvings/${entityId}`" :filter="filter" - data-key="ShelvingSummary" + data-key="Shelving" > <template #header="{ entity }"> <div>{{ entity.code }}</div> @@ -58,16 +42,19 @@ const filter = { class="header header-link" :to="{ name: 'ShelvingBasicData', params: { id: entityId } }" > - {{ t('globals.pageTitles.basicData') }} + {{ $t('globals.pageTitles.basicData') }} <QIcon name="open_in_new" /> </RouterLink> - <VnLv :label="t('globals.code')" :value="entity.code" /> + <VnLv :label="$t('globals.code')" :value="entity.code" /> <VnLv - :label="t('shelving.list.parking')" + :label="$t('shelving.list.parking')" :value="entity.parking?.code" /> - <VnLv :label="t('shelving.list.priority')" :value="entity.priority" /> - <VnLv v-if="entity.worker" :label="t('globals.worker')"> + <VnLv + :label="$t('shelving.list.priority')" + :value="entity.priority" + /> + <VnLv v-if="entity.worker" :label="$t('globals.worker')"> <template #value> <VnUserLink :name="entity.worker?.user?.nickname" @@ -76,7 +63,7 @@ const filter = { </template> </VnLv> <VnLv - :label="t('shelving.summary.recyclable')" + :label="$t('shelving.summary.recyclable')" :value="entity.isRecyclable" /> </QCard> diff --git a/src/pages/Parking/Card/ParkingBasicData.vue b/src/pages/Shelving/Parking/Card/ParkingBasicData.vue similarity index 68% rename from src/pages/Parking/Card/ParkingBasicData.vue rename to src/pages/Shelving/Parking/Card/ParkingBasicData.vue index 550a0684e..3de358002 100644 --- a/src/pages/Parking/Card/ParkingBasicData.vue +++ b/src/pages/Shelving/Parking/Card/ParkingBasicData.vue @@ -1,16 +1,11 @@ <script setup> -import { ref, computed } from 'vue'; -import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; +import { ref } from 'vue'; import VnRow from 'components/ui/VnRow.vue'; import FetchData from 'src/components/FetchData.vue'; import VnInput from 'src/components/common/VnInput.vue'; import FormModel from 'components/FormModel.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; -const { t } = useI18n(); -const route = useRoute(); -const parkingId = computed(() => route.params?.id || null); const sectors = ref([]); const sectorFilter = { fields: ['id', 'description'] }; @@ -27,18 +22,21 @@ const filter = { @on-fetch="(data) => (sectors = data)" auto-load /> - <FormModel :url="`Parkings/${parkingId}`" model="parking" :filter="filter" auto-load> + <FormModel model="Parking" auto-load> <template #form="{ data }"> <VnRow> - <VnInput v-model="data.code" :label="t('globals.code')" /> - <VnInput v-model="data.pickingOrder" :label="t('parking.pickingOrder')" /> + <VnInput v-model="data.code" :label="$t('globals.code')" /> + <VnInput + v-model="data.pickingOrder" + :label="$t('parking.pickingOrder')" + /> </VnRow> <VnRow> <VnSelect v-model="data.sectorFk" option-value="id" option-label="description" - :label="t('parking.sector')" + :label="$t('parking.sector')" :options="sectors" use-input input-debounce="0" diff --git a/src/pages/Parking/Card/ParkingCard.vue b/src/pages/Shelving/Parking/Card/ParkingCard.vue similarity index 53% rename from src/pages/Parking/Card/ParkingCard.vue rename to src/pages/Shelving/Parking/Card/ParkingCard.vue index 1cd2df7b7..b32c1b7d3 100644 --- a/src/pages/Parking/Card/ParkingCard.vue +++ b/src/pages/Shelving/Parking/Card/ParkingCard.vue @@ -1,12 +1,14 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; -import ParkingDescriptor from 'pages/Parking/Card/ParkingDescriptor.vue'; +import ParkingDescriptor from 'pages/Shelving/Parking/Card/ParkingDescriptor.vue'; +import filter from './ParkingFilter.js'; </script> <template> <VnCardBeta data-key="Parking" - base-url="Parkings" + url="Parkings" + :filter="filter" :descriptor="ParkingDescriptor" /> </template> diff --git a/src/pages/Parking/Card/ParkingDescriptor.vue b/src/pages/Shelving/Parking/Card/ParkingDescriptor.vue similarity index 58% rename from src/pages/Parking/Card/ParkingDescriptor.vue rename to src/pages/Shelving/Parking/Card/ParkingDescriptor.vue index d36ea16fc..46c9f8ea0 100644 --- a/src/pages/Parking/Card/ParkingDescriptor.vue +++ b/src/pages/Shelving/Parking/Card/ParkingDescriptor.vue @@ -1,10 +1,9 @@ <script setup> import { computed } from 'vue'; -import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'components/ui/VnLv.vue'; - +import filter from './ParkingFilter.js'; const props = defineProps({ id: { type: Number, @@ -13,18 +12,11 @@ const props = defineProps({ }, }); -const { t } = useI18n(); const route = useRoute(); const entityId = computed(() => props.id || route.params.id); - -const filter = { - fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'], - include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }], -}; </script> <template> <CardDescriptor - module="Parking" data-key="Parking" :url="`Parkings/${entityId}`" title="code" @@ -32,9 +24,9 @@ const filter = { :to-module="{ name: 'ParkingList' }" > <template #body="{ entity }"> - <VnLv :label="t('globals.code')" :value="entity.code" /> - <VnLv :label="t('parking.pickingOrder')" :value="entity.pickingOrder" /> - <VnLv :label="t('parking.sector')" :value="entity.sector?.description" /> + <VnLv :label="$t('globals.code')" :value="entity.code" /> + <VnLv :label="$t('parking.pickingOrder')" :value="entity.pickingOrder" /> + <VnLv :label="$t('parking.sector')" :value="entity.sector?.description" /> </template> </CardDescriptor> </template> diff --git a/src/pages/Shelving/Parking/Card/ParkingFilter.js b/src/pages/Shelving/Parking/Card/ParkingFilter.js new file mode 100644 index 000000000..fd1855c45 --- /dev/null +++ b/src/pages/Shelving/Parking/Card/ParkingFilter.js @@ -0,0 +1,4 @@ +export default { + fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'], + include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }], +}; diff --git a/src/pages/Parking/Card/ParkingLog.vue b/src/pages/Shelving/Parking/Card/ParkingLog.vue similarity index 100% rename from src/pages/Parking/Card/ParkingLog.vue rename to src/pages/Shelving/Parking/Card/ParkingLog.vue diff --git a/src/pages/Parking/Card/ParkingSummary.vue b/src/pages/Shelving/Parking/Card/ParkingSummary.vue similarity index 100% rename from src/pages/Parking/Card/ParkingSummary.vue rename to src/pages/Shelving/Parking/Card/ParkingSummary.vue diff --git a/src/pages/Shelving/Parking/ParkingExprBuilder.js b/src/pages/Shelving/Parking/ParkingExprBuilder.js new file mode 100644 index 000000000..16d2262c8 --- /dev/null +++ b/src/pages/Shelving/Parking/ParkingExprBuilder.js @@ -0,0 +1,10 @@ +export default (param, value) => { + switch (param) { + case 'code': + return { [param]: { like: `%${value}%` } }; + case 'sectorFk': + return { [param]: value }; + case 'search': + return { or: [{ code: { like: `%${value}%` } }, { id: value }] }; + } +}; diff --git a/src/pages/Parking/ParkingFilter.vue b/src/pages/Shelving/Parking/ParkingFilter.vue similarity index 100% rename from src/pages/Parking/ParkingFilter.vue rename to src/pages/Shelving/Parking/ParkingFilter.vue diff --git a/src/pages/Parking/ParkingList.vue b/src/pages/Shelving/Parking/ParkingList.vue similarity index 90% rename from src/pages/Parking/ParkingList.vue rename to src/pages/Shelving/Parking/ParkingList.vue index bce87126e..fe6c93ba5 100644 --- a/src/pages/Parking/ParkingList.vue +++ b/src/pages/Shelving/Parking/ParkingList.vue @@ -9,6 +9,7 @@ import CardList from 'components/ui/CardList.vue'; import VnLv from 'components/ui/VnLv.vue'; import ParkingFilter from './ParkingFilter.vue'; import ParkingSummary from './Card/ParkingSummary.vue'; +import exprBuilder from './ParkingExprBuilder.js'; import VnSection from 'src/components/common/VnSection.vue'; const stateStore = useStateStore(); @@ -23,19 +24,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); const filter = { fields: ['id', 'sectorFk', 'code', 'pickingOrder'], }; - -function exprBuilder(param, value) { - switch (param) { - case 'code': - return { [param]: { like: `%${value}%` } }; - case 'sectorFk': - return { [param]: value }; - case 'search': - return { or: [{ code: { like: `%${value}%` } }, { id: value }] }; - } -} </script> - <template> <VnSection :data-key="dataKey" diff --git a/src/pages/Parking/locale/en.yml b/src/pages/Shelving/Parking/locale/en.yml similarity index 100% rename from src/pages/Parking/locale/en.yml rename to src/pages/Shelving/Parking/locale/en.yml diff --git a/src/pages/Parking/locale/es.yml b/src/pages/Shelving/Parking/locale/es.yml similarity index 100% rename from src/pages/Parking/locale/es.yml rename to src/pages/Shelving/Parking/locale/es.yml diff --git a/src/pages/Shelving/ShelvingExprBuilder.js b/src/pages/Shelving/ShelvingExprBuilder.js new file mode 100644 index 000000000..b9aad8a71 --- /dev/null +++ b/src/pages/Shelving/ShelvingExprBuilder.js @@ -0,0 +1,10 @@ +export default (param, value) => { + switch (param) { + case 'search': + return { code: { like: `%${value}%` } }; + case 'parkingFk': + case 'userFk': + case 'isRecyclable': + return { [param]: value }; + } +}; diff --git a/src/pages/Shelving/ShelvingList.vue b/src/pages/Shelving/ShelvingList.vue index cf158e76b..4e0c21100 100644 --- a/src/pages/Shelving/ShelvingList.vue +++ b/src/pages/Shelving/ShelvingList.vue @@ -1,6 +1,5 @@ <script setup> import VnPaginate from 'components/ui/VnPaginate.vue'; -import { useI18n } from 'vue-i18n'; import CardList from 'components/ui/CardList.vue'; import VnLv from 'components/ui/VnLv.vue'; import { useRouter } from 'vue-router'; @@ -8,9 +7,9 @@ import ShelvingFilter from 'pages/Shelving/Card/ShelvingFilter.vue'; import ShelvingSummary from 'pages/Shelving/Card/ShelvingSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import VnSection from 'src/components/common/VnSection.vue'; +import exprBuilder from './ShelvingExprBuilder.js'; const router = useRouter(); -const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const dataKey = 'ShelvingList'; @@ -21,17 +20,6 @@ const filter = { function navigate(id) { router.push({ path: `/shelving/${id}` }); } - -function exprBuilder(param, value) { - switch (param) { - case 'search': - return { code: { like: `%${value}%` } }; - case 'parkingFk': - case 'userFk': - case 'isRecyclable': - return { [param]: value }; - } -} </script> <template> @@ -62,18 +50,18 @@ function exprBuilder(param, value) { > <template #list-items> <VnLv - :label="t('shelving.list.parking')" - :title-label="t('shelving.list.parking')" + :label="$t('shelving.list.parking')" + :title-label="$t('shelving.list.parking')" :value="row.parking?.code" /> <VnLv - :label="t('shelving.list.priority')" + :label="$t('shelving.list.priority')" :value="row?.priority" /> </template> <template #actions> <QBtn - :label="t('components.smartCard.openSummary')" + :label="$t('components.smartCard.openSummary')" @click.stop="viewSummary(row.id, ShelvingSummary)" color="primary" /> @@ -84,9 +72,9 @@ function exprBuilder(param, value) { </div> <QPageSticky :offset="[20, 20]"> <RouterLink :to="{ name: 'ShelvingCreate' }"> - <QBtn fab icon="add" color="primary" shortcut="+" /> + <QBtn fab icon="add" color="primary" v-shortcut="'+'" /> <QTooltip> - {{ t('shelving.list.newShelving') }} + {{ $t('shelving.list.newShelving') }} </QTooltip> </RouterLink> </QPageSticky> diff --git a/src/pages/Supplier/Card/SupplierAccounts.vue b/src/pages/Supplier/Card/SupplierAccounts.vue index 4a6901d1d..365eb67a1 100644 --- a/src/pages/Supplier/Card/SupplierAccounts.vue +++ b/src/pages/Supplier/Card/SupplierAccounts.vue @@ -71,7 +71,7 @@ function bankEntityFilter(val, update) { filteredBankEntitiesOptions.value = bankEntitiesOptions.value.filter( (bank) => bank.bic.toLowerCase().startsWith(needle) || - bank.name.toLowerCase().includes(needle) + bank.name.toLowerCase().includes(needle), ); }); } @@ -170,7 +170,7 @@ function bankEntityFilter(val, update) { <QIcon name="info" class="cursor-pointer"> <QTooltip>{{ t( - 'Name of the bank account holder if different from the provider' + 'Name of the bank account holder if different from the provider', ) }}</QTooltip> </QIcon> @@ -194,7 +194,7 @@ function bankEntityFilter(val, update) { <QBtn flat icon="add" - shortcut="+" + v-shortcut class="cursor-pointer" color="primary" @click="supplierAccountRef.insert()" diff --git a/src/pages/Supplier/Card/SupplierAddresses.vue b/src/pages/Supplier/Card/SupplierAddresses.vue index e568962ff..c4c0ab7be 100644 --- a/src/pages/Supplier/Card/SupplierAddresses.vue +++ b/src/pages/Supplier/Card/SupplierAddresses.vue @@ -89,7 +89,7 @@ const redirectToUpdateView = (addressData) => { icon="add" color="primary" @click="redirectToCreateView()" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('New address') }} diff --git a/src/pages/Supplier/Card/SupplierAgencyTerm.vue b/src/pages/Supplier/Card/SupplierAgencyTerm.vue index 99b672cc4..ab21f1f76 100644 --- a/src/pages/Supplier/Card/SupplierAgencyTerm.vue +++ b/src/pages/Supplier/Card/SupplierAgencyTerm.vue @@ -114,7 +114,7 @@ const redirectToCreateView = () => { icon="add" color="primary" @click="redirectToCreateView()" - shortcut="+" + v-shortcut="'+'" /> <QTooltip> {{ t('supplier.agencyTerms.addRow') }} diff --git a/src/pages/Supplier/Card/SupplierBasicData.vue b/src/pages/Supplier/Card/SupplierBasicData.vue index f6c13b7af..631700a4a 100644 --- a/src/pages/Supplier/Card/SupplierBasicData.vue +++ b/src/pages/Supplier/Card/SupplierBasicData.vue @@ -19,9 +19,8 @@ const companySizes = [ </script> <template> <FormModel - :url="`Suppliers/${route.params.id}`" :url-update="`Suppliers/${route.params.id}`" - model="supplier" + model="Supplier" auto-load :clear-store-on-unmount="false" @on-data-saved="arrayData.fetch({})" diff --git a/src/pages/Supplier/Card/SupplierCard.vue b/src/pages/Supplier/Card/SupplierCard.vue index 594026d18..e30f79f96 100644 --- a/src/pages/Supplier/Card/SupplierCard.vue +++ b/src/pages/Supplier/Card/SupplierCard.vue @@ -1,19 +1,13 @@ <script setup> -import VnCard from 'components/common/VnCard.vue'; import SupplierDescriptor from './SupplierDescriptor.vue'; -import SupplierListFilter from '../SupplierListFilter.vue'; +import VnCardBeta from 'src/components/common/VnCardBeta.vue'; +import filter from './SupplierFilter.js'; </script> <template> - <VnCard + <VnCardBeta data-key="Supplier" - base-url="Suppliers" + url="Suppliers" :descriptor="SupplierDescriptor" - :filter-panel="SupplierListFilter" - search-data-key="SupplierList" - :searchbar-props="{ - url: 'Suppliers/filter', - searchUrl: 'table', - label: 'Search suppliers', - }" + :filter="filter" /> </template> diff --git a/src/pages/Supplier/Card/SupplierConsumption.vue b/src/pages/Supplier/Card/SupplierConsumption.vue index 8a7021fb3..718de95dd 100644 --- a/src/pages/Supplier/Card/SupplierConsumption.vue +++ b/src/pages/Supplier/Card/SupplierConsumption.vue @@ -16,6 +16,7 @@ import axios from 'axios'; import { useStateStore } from 'stores/useStateStore'; import { useState } from 'src/composables/useState'; import { useArrayData } from 'composables/useArrayData'; +import RightMenu from 'src/components/common/RightMenu.vue'; const state = useState(); const stateStore = useStateStore(); @@ -173,59 +174,59 @@ onMounted(async () => { </div> </div> </Teleport> - <QPage class="column items-center q-pa-md"> - <Teleport to="#right-panel" v-if="stateStore.isHeaderMounted()"> + <RightMenu> + <template #right-panel> <SupplierConsumptionFilter data-key="SupplierConsumption" /> - </Teleport> - <QTable - :rows="rows" - row-key="id" - hide-header - class="full-width q-mt-md" - :no-data-label="t('No results')" - > - <template #body="{ row }"> - <QTr> - <QTd no-hover> - <span class="label">{{ t('supplier.consumption.entry') }}: </span> - <span>{{ row.id }}</span> - </QTd> - <QTd no-hover> - <span class="label">{{ t('globals.date') }}: </span> - <span>{{ toDate(row.shipped) }}</span></QTd - > - <QTd colspan="6" no-hover> - <span class="label">{{ t('globals.reference') }}: </span> - <span>{{ row.invoiceNumber }}</span> - </QTd> - </QTr> - <QTr v-for="(buy, index) in row.buys" :key="index"> - <QTd no-hover> - <QBtn flat color="blue" dense no-caps>{{ buy.itemName }}</QBtn> - <ItemDescriptorProxy :id="buy.itemFk" /> - </QTd> + </template> + </RightMenu> + <QTable + :rows="rows" + row-key="id" + hide-header + class="full-width q-mt-md" + :no-data-label="t('No results')" + > + <template #body="{ row }"> + <QTr> + <QTd no-hover> + <span class="label">{{ t('supplier.consumption.entry') }}: </span> + <span>{{ row.id }}</span> + </QTd> + <QTd no-hover> + <span class="label">{{ t('globals.date') }}: </span> + <span>{{ toDate(row.shipped) }}</span></QTd + > + <QTd colspan="6" no-hover> + <span class="label">{{ t('globals.reference') }}: </span> + <span>{{ row.invoiceNumber }}</span> + </QTd> + </QTr> + <QTr v-for="(buy, index) in row.buys" :key="index"> + <QTd no-hover> + <QBtn flat color="blue" dense no-caps>{{ buy.itemName }}</QBtn> + <ItemDescriptorProxy :id="buy.itemFk" /> + </QTd> - <QTd no-hover> - <span>{{ buy.subName }}</span> - <FetchedTags :item="buy" /> - </QTd> - <QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd> - <QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd> - <QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd> - </QTr> - <QTr> - <QTd colspan="5" no-hover> - <span class="label">{{ t('Total entry') }}: </span> - <span>{{ row.total }} €</span> - </QTd> - <QTd no-hover> - <span class="label">{{ t('Total stems') }}: </span> - <span>{{ row.quantity }}</span> - </QTd> - </QTr> - </template> - </QTable> - </QPage> + <QTd no-hover> + <span>{{ buy.subName }}</span> + <FetchedTags :item="buy" /> + </QTd> + <QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd> + <QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd> + <QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd> + </QTr> + <QTr> + <QTd colspan="5" no-hover> + <span class="label">{{ t('Total entry') }}: </span> + <span>{{ row.total }} €</span> + </QTd> + <QTd no-hover> + <span class="label">{{ t('Total stems') }}: </span> + <span>{{ row.quantity }}</span> + </QTd> + </QTr> + </template> + </QTable> </template> <style scoped lang="scss"> diff --git a/src/pages/Supplier/Card/SupplierContacts.vue b/src/pages/Supplier/Card/SupplierContacts.vue index 6781c8d34..f96d92ab1 100644 --- a/src/pages/Supplier/Card/SupplierContacts.vue +++ b/src/pages/Supplier/Card/SupplierContacts.vue @@ -78,7 +78,7 @@ const insertRow = () => { <QBtn flat icon="add" - shortcut="+" + v-shortcut="'+'" class="cursor-pointer" color="primary" @click="insertRow()" diff --git a/src/pages/Supplier/Card/SupplierDescriptor.vue b/src/pages/Supplier/Card/SupplierDescriptor.vue index 37c9c1cff..462bdf853 100644 --- a/src/pages/Supplier/Card/SupplierDescriptor.vue +++ b/src/pages/Supplier/Card/SupplierDescriptor.vue @@ -7,8 +7,8 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import { toDateString } from 'src/filters'; -import useCardDescription from 'src/composables/useCardDescription'; import { getUrl } from 'src/composables/getUrl'; +import filter from './SupplierFilter.js'; import { useArrayData } from 'src/composables/useArrayData'; const $props = defineProps({ @@ -28,42 +28,6 @@ const { t } = useI18n(); const url = ref(); const arrayData = useArrayData(); -const filter = { - fields: [ - 'id', - 'name', - 'nickname', - 'nif', - 'payMethodFk', - 'payDemFk', - 'payDay', - 'isActive', - 'isReal', - 'isTrucker', - 'account', - ], - include: [ - { - relation: 'payMethod', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'payDem', - scope: { - fields: ['id', 'payDem'], - }, - }, - { - relation: 'client', - scope: { - fields: ['id', 'fi'], - }, - }, - ], -}; - onMounted(async () => { url.value = await getUrl(''); }); @@ -72,11 +36,6 @@ const entityId = computed(() => { return $props.id || route.params.id; }); -const data = ref(useCardDescription()); -const setData = (entity) => { - data.value = useCardDescription(entity.ref, entity.id); -}; - const supplier = computed(() => arrayData.store.data); const getEntryQueryParams = (supplier) => { @@ -103,13 +62,9 @@ const getEntryQueryParams = (supplier) => { <template> <CardDescriptor - module="Supplier" :url="`Suppliers/${entityId}`" - :title="data.title" - :subtitle="data.subtitle" :filter="filter" - @on-fetch="setData" - data-key="supplierDescriptor" + data-key="Supplier" :summary="$props.summary" > <template #body="{ entity }"> diff --git a/src/pages/Supplier/Card/SupplierFilter.js b/src/pages/Supplier/Card/SupplierFilter.js new file mode 100644 index 000000000..3ce5c3de2 --- /dev/null +++ b/src/pages/Supplier/Card/SupplierFilter.js @@ -0,0 +1,35 @@ +export default { + fields: [ + 'id', + 'name', + 'nickname', + 'nif', + 'payMethodFk', + 'payDemFk', + 'payDay', + 'isActive', + 'isSerious', + 'isTrucker', + 'account', + ], + include: [ + { + relation: 'payMethod', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'payDem', + scope: { + fields: ['id', 'payDem'], + }, + }, + { + relation: 'client', + scope: { + fields: ['id', 'fi'], + }, + }, + ], +}; diff --git a/src/pages/Supplier/Card/SupplierFiscalData.vue b/src/pages/Supplier/Card/SupplierFiscalData.vue index e569eb236..ecee5b76b 100644 --- a/src/pages/Supplier/Card/SupplierFiscalData.vue +++ b/src/pages/Supplier/Card/SupplierFiscalData.vue @@ -10,6 +10,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; import VnAccountNumber from 'src/components/common/VnAccountNumber.vue'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; const route = useRoute(); const { t } = useI18n(); @@ -182,18 +183,11 @@ function handleLocation(data, location) { v-model="data.isTrucker" :label="t('supplier.fiscalData.isTrucker')" /> - <div class="row items-center"> - <QCheckbox v-model="data.isVies" :label="t('globals.isVies')" /> - <QIcon name="info" size="xs" class="cursor-pointer q-ml-sm"> - <QTooltip> - {{ - t( - 'When activating it, do not enter the country code in the ID field.' - ) - }} - </QTooltip> - </QIcon> - </div> + <VnCheckbox + v-model="data.isVies" + :label="t('globals.isVies')" + :info="t('whenActivatingIt')" + /> </div> </VnRow> </template> @@ -201,6 +195,8 @@ function handleLocation(data, location) { </template> <i18n> +en: + whenActivatingIt: When activating it, do not enter the country code in the ID field. es: - When activating it, do not enter the country code in the ID field.: Al activarlo, no informar el código del país en el campo nif + whenActivatingIt: Al activarlo, no informar el código del país en el campo nif. </i18n> diff --git a/src/pages/Supplier/SupplierList.vue b/src/pages/Supplier/SupplierList.vue index 85cc11857..600790745 100644 --- a/src/pages/Supplier/SupplierList.vue +++ b/src/pages/Supplier/SupplierList.vue @@ -2,14 +2,15 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import VnTable from 'components/VnTable/VnTable.vue'; -import VnSearchbar from 'components/ui/VnSearchbar.vue'; -import RightMenu from 'src/components/common/RightMenu.vue'; -import SupplierListFilter from './SupplierListFilter.vue'; +import VnSection from 'src/components/common/VnSection.vue'; import VnInput from 'src/components/common/VnInput.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import FetchData from 'src/components/FetchData.vue'; const { t } = useI18n(); const tableRef = ref(); - +const dataKey = 'SupplierList'; +const provincesOptions = ref([]); const columns = computed(() => [ { align: 'left', @@ -104,38 +105,62 @@ const columns = computed(() => [ }, ]); </script> - <template> - <VnSearchbar data-key="SuppliersList" :limit="20" :label="t('Search suppliers')" /> - <RightMenu> - <template #right-panel> - <SupplierListFilter data-key="SuppliersList" /> - </template> - </RightMenu> - <VnTable - ref="tableRef" - data-key="SuppliersList" - url="Suppliers/filter" - redirect="supplier" - :create="{ - urlCreate: 'Suppliers/newSupplier', - title: t('Create Supplier'), - onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: {}, - mapper: (data) => { - data.name = data.socialName; - - return data; - }, - }" - :right-search="false" - order="id ASC" + <FetchData + url="Provinces" + :filter="{ fields: ['id', 'name'], order: 'name ASC' }" + @on-fetch="(data) => (provincesOptions = data)" + auto-load + /> + <VnSection + :data-key="dataKey" :columns="columns" + prefix="supplier" + :array-data-props="{ + url: 'Suppliers/filter', + order: 'id ASC', + }" > - <template #more-create-dialog="{ data }"> - <VnInput :label="t('globals.name')" v-model="data.socialName" :uppercase="true" /> - </template> - </VnTable> + <template #body> + <VnTable + ref="tableRef" + :data-key="dataKey" + :create="{ + urlCreate: 'Suppliers/newSupplier', + title: t('Create Supplier'), + onDataSaved: ({ id }) => tableRef.redirect(id), + formInitialData: {}, + mapper: (data) => { + data.name = data.socialName; + delete data.socialName; + return data; + }, + }" + :columns="columns" + redirect="supplier" + :right-search="false" + > + <template #more-create-dialog="{ data }"> + <VnInput + :label="t('globals.name')" + v-model="data.socialName" + :uppercase="true" + /> + </template> + </VnTable> + </template> + <template #moreFilterPanel="{ params, searchFn }"> + <VnSelect + :label="t('globals.params.provinceFk')" + v-model="params.provinceFk" + @update:model-value="searchFn()" + :options="provincesOptions" + filled + dense + class="q-px-sm q-pr-lg" + /> + </template> + </VnSection> </template> <i18n> diff --git a/src/pages/Supplier/SupplierListFilter.vue b/src/pages/Supplier/SupplierListFilter.vue deleted file mode 100644 index b170a35cc..000000000 --- a/src/pages/Supplier/SupplierListFilter.vue +++ /dev/null @@ -1,122 +0,0 @@ -<script setup> -import { ref } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import FetchData from 'components/FetchData.vue'; - -const props = defineProps({ - dataKey: { - type: String, - required: true, - }, -}); - -const { t } = useI18n(); - -const provincesOptions = ref([]); -const countriesOptions = ref([]); -</script> - -<template> - <FetchData - url="Provinces" - :filter="{ fields: ['id', 'name'], order: 'name ASC'}" - @on-fetch="(data) => (provincesOptions = data)" - auto-load - /> - <FetchData - url="countries" - :filter="{ fields: ['id', 'name'], order: 'name ASC'}" - @on-fetch="(data) => (countriesOptions = data)" - auto-load - /> - <VnFilterPanel - :data-key="props.dataKey" - :search-button="true" - :unremovable-params="['supplierFk']" - > - <template #tags="{ tag, formatFn }"> - <div class="q-gutter-x-xs"> - <strong>{{ t(`params.${tag.label}`) }}: </strong> - <span>{{ formatFn(tag.value) }}</span> - </div> - </template> - <template #body="{ params, searchFn }"> - <QItem> - <QItemSection> - <VnInput - v-model="params.search" - :label="t('params.search')" - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput - v-model="params.nickname" - :label="t('params.nickname')" - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput v-model="params.nif" :label="t('params.nif')" is-outlined /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - :label="t('params.provinceFk')" - v-model="params.provinceFk" - @update:model-value="searchFn()" - :options="provincesOptions" - option-value="id" - option-label="name" - hide-selected - dense - outlined - rounded - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - :label="t('params.countryFk')" - v-model="params.countryFk" - @update:model-value="searchFn()" - :options="countriesOptions" - option-value="id" - option-label="name" - hide-selected - dense - outlined - rounded - /> - </QItemSection> - </QItem> - </template> - </VnFilterPanel> -</template> - -<i18n> -en: - params: - search: General search - nickname: Alias - nif: Tax number - provinceFk: Province - countryFk: Country -es: - params: - search: Búsqueda general - nickname: Alias - nif: NIF/CIF - provinceFk: Provincia - countryFk: País -</i18n> diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicData.vue b/src/pages/Ticket/Card/BasicData/TicketBasicData.vue index c6a85c287..055c9a0ff 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicData.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicData.vue @@ -9,8 +9,9 @@ import FetchData from 'components/FetchData.vue'; import { useStateStore } from 'stores/useStateStore'; import { toCurrency } from 'filters/index'; import { useRole } from 'src/composables/useRole'; +import VnCheckbox from 'src/components/common/VnCheckbox.vue'; -const haveNegatives = defineModel('haveNegatives', { type: Boolean, required: true }); +const haveNegatives = defineModel('have-negatives', { type: Boolean, required: true }); const formData = defineModel({ type: Object, required: true }); const stateStore = useStateStore(); @@ -182,22 +183,19 @@ onMounted(async () => { </QCard> <QCard v-if="haveNegatives" - class="q-pa-md q-mb-md q-ma-md color-vn-text" + class="q-pa-xs q-mb-md q-ma-md color-vn-text" bordered flat style="border-color: black" > <QCardSection horizontal class="flex row items-center"> - <QCheckbox - :label="t('basicData.withoutNegatives')" + <VnCheckbox v-model="formData.withoutNegatives" + :label="t('basicData.withoutNegatives')" + :info="t('basicData.withoutNegativesInfo')" :toggle-indeterminate="false" + size="xs" /> - <QIcon name="info" size="xs" class="q-ml-sm"> - <QTooltip max-width="350px"> - {{ t('basicData.withoutNegativesInfo') }} - </QTooltip> - </QIcon> </QCardSection> </QCard> </QDrawer> diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue index cf4481537..9d70fea38 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue @@ -260,7 +260,7 @@ async function getZone(options) { auto-load /> <QForm> - <VnRow> + <VnRow class="row q-gutter-md q-mb-md no-wrap"> <VnSelect :label="t('ticketList.client')" v-model="clientId" @@ -296,7 +296,7 @@ async function getZone(options) { :rules="validate('ticketList.warehouse')" /> </VnRow> - <VnRow> + <VnRow class="row q-gutter-md q-mb-md no-wrap"> <VnSelect :label="t('basicData.address')" v-model="addressId" diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue index 89249b899..ef2eb75d6 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue @@ -1,7 +1,7 @@ <script setup> -import { ref, onBeforeMount } from 'vue'; +import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; -import { useRoute, useRouter } from 'vue-router'; +import { useRouter } from 'vue-router'; import TicketBasicData from './TicketBasicData.vue'; import TicketBasicDataForm from './TicketBasicDataForm.vue'; @@ -9,104 +9,69 @@ import { useVnConfirm } from 'composables/useVnConfirm'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; +import { useArrayData } from 'src/composables/useArrayData'; const { notify } = useNotify(); -const route = useRoute(); const router = useRouter(); const { t } = useI18n(); const stepperRef = ref(null); const { openConfirmationModal } = useVnConfirm(); const step = ref(1); -const formData = ref({}); -const initialDataLoaded = ref(false); -const haveNegatives = ref(false); +const haveNegatives = ref(true); -const ticketFilter = { - include: [ - { relation: 'address' }, - { - relation: 'client', - scope: { - fields: [ - 'salesPersonFk', - 'name', - 'isActive', - 'isFreezed', - 'isTaxDataChecked', - 'credit', - 'email', - 'phone', - 'mobile', - 'hasElectronicInvoice', - ], - include: { - relation: 'salesPersonUser', - scope: { fields: ['id', 'name'] }, - }, - }, - }, - { relation: 'invoiceOut' }, - ], -}; - -const getTicketData = async () => { - const params = { filter: JSON.stringify(ticketFilter) }; - const { data } = await axios.get(`tickets/${route.params.id}`, { params }); - formData.value = data; - initialDataLoaded.value = true; -}; +const ticket = computed(() => useArrayData('Ticket').store?.data); const isFormInvalid = () => { return ( - !formData.value.clientFk || - !formData.value.addressFk || - !formData.value.agencyModeFk || - !formData.value.companyFk || - !formData.value.shipped || - !formData.value.landed || - !formData.value.zoneFk + !ticket.value.clientFk || + !ticket.value.addressFk || + !ticket.value.agencyModeFk || + !ticket.value.companyFk || + !ticket.value.shipped || + !ticket.value.landed || + !ticket.value.zoneFk ); }; const getPriceDifference = async () => { const params = { - landed: formData.value.landed, - addressId: formData.value.addressFk, - agencyModeId: formData.value.agencyModeFk, - zoneId: formData.value.zoneFk, - warehouseId: formData.value.warehouseFk, - shipped: formData.value.shipped, + landed: ticket.value.landed, + addressId: ticket.value.addressFk, + agencyModeId: ticket.value.agencyModeFk, + zoneId: ticket.value.zoneFk, + warehouseId: ticket.value.warehouseFk, + shipped: ticket.value.shipped, }; const { data } = await axios.post( - `tickets/${formData.value.id}/priceDifference`, + `tickets/${ticket.value.id}/priceDifference`, params ); - formData.value.sale = data; + ticket.value.sale = data; }; const submit = async () => { - if (!formData.value.option) return notify(t('basicData.chooseAnOption'), 'negative'); + if (!ticket.value.option) return notify(t('basicData.chooseAnOption'), 'negative'); const params = { - clientFk: formData.value.clientFk, - nickname: formData.value.nickname, - agencyModeFk: formData.value.agencyModeFk, - addressFk: formData.value.addressFk, - zoneFk: formData.value.zoneFk, - warehouseFk: formData.value.warehouseFk, - companyFk: formData.value.companyFk, - shipped: formData.value.shipped, - landed: formData.value.landed, - isDeleted: formData.value.isDeleted, - option: formData.value.option, - isWithoutNegatives: formData.value.withoutNegatives, - withWarningAccept: formData.value.withWarningAccept, + clientFk: ticket.value.clientFk, + nickname: ticket.value.nickname, + agencyModeFk: ticket.value.agencyModeFk, + addressFk: ticket.value.addressFk, + zoneFk: ticket.value.zoneFk, + warehouseFk: ticket.value.warehouseFk, + companyFk: ticket.value.companyFk, + shipped: ticket.value.shipped, + landed: ticket.value.landed, + isDeleted: ticket.value.isDeleted, + option: ticket.value.option, + isWithoutNegatives: ticket.value.withoutNegatives, + withWarningAccept: ticket.value.withWarningAccept, keepPrice: false, }; const { data } = await axios.post( - `tickets/${formData.value.id}/componentUpdate`, + `tickets/${ticket.value.id}/componentUpdate`, params ); @@ -118,7 +83,7 @@ const submit = async () => { }; const submitWithNegatives = async () => { - formData.value.withWarningAccept = true; + ticket.value.withWarningAccept = true; submit(); }; @@ -130,7 +95,7 @@ const onNextStep = async () => { await getPriceDifference(); stepperRef.value.next(); } else if (step.value === 2) { - if (haveNegatives.value && !formData.value.withoutNegatives) + if (haveNegatives.value && !ticket.value.withoutNegatives) openConfirmationModal( t('basicData.negativesConfirmTitle'), t('basicData.negativesConfirmMessage'), @@ -139,11 +104,10 @@ const onNextStep = async () => { else submit(); } }; - -onBeforeMount(async () => await getTicketData()); </script> <template> <QStepper + v-if="ticket" v-model="step" ref="stepperRef" color="primary" @@ -155,10 +119,10 @@ onBeforeMount(async () => await getTicketData()); }" > <QStep :name="1" :title="t('globals.pageTitles.basicData')" :done="step > 1"> - <TicketBasicDataForm v-if="initialDataLoaded" v-model="formData" /> + <TicketBasicDataForm v-model="ticket" /> </QStep> <QStep :name="2" :title="t('basicData.priceDifference')"> - <TicketBasicData v-model="formData" v-model:have-negatives="haveNegatives" /> + <TicketBasicData v-model="ticket" v-model:have-negatives="haveNegatives" /> </QStep> <template #navigation> <QStepperNavigation class="flex justify-between"> diff --git a/src/pages/Ticket/Card/TicketCard.vue b/src/pages/Ticket/Card/TicketCard.vue index 6886a8e57..e22d5799a 100644 --- a/src/pages/Ticket/Card/TicketCard.vue +++ b/src/pages/Ticket/Card/TicketCard.vue @@ -1,7 +1,13 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; import TicketDescriptor from './TicketDescriptor.vue'; +import filter from './TicketFilter.js'; </script> <template> - <VnCardBeta data-key="Ticket" base-url="Tickets" :descriptor="TicketDescriptor" /> + <VnCardBeta + data-key="Ticket" + url="Tickets" + :descriptor="TicketDescriptor" + :filter="filter" + /> </template> diff --git a/src/pages/Ticket/Card/TicketComponents.vue b/src/pages/Ticket/Card/TicketComponents.vue index 842607e0c..5936ffc28 100644 --- a/src/pages/Ticket/Card/TicketComponents.vue +++ b/src/pages/Ticket/Card/TicketComponents.vue @@ -19,7 +19,7 @@ import RightMenu from 'src/components/common/RightMenu.vue'; const route = useRoute(); const { t } = useI18n(); const salesRef = ref(null); -const arrayData = useArrayData('ticketData'); +const arrayData = useArrayData('Ticket'); const { store } = arrayData; const ticketData = computed(() => store.data); diff --git a/src/pages/Ticket/Card/TicketDescriptor.vue b/src/pages/Ticket/Card/TicketDescriptor.vue index c9849d631..c5f3233b1 100644 --- a/src/pages/Ticket/Card/TicketDescriptor.vue +++ b/src/pages/Ticket/Card/TicketDescriptor.vue @@ -6,9 +6,11 @@ import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy import CardDescriptor from 'components/ui/CardDescriptor.vue'; import TicketDescriptorMenu from './TicketDescriptorMenu.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import { toDateTimeFormat } from 'src/filters/date'; +import filter from './TicketFilter.js'; +import FetchData from 'src/components/FetchData.vue'; +import TicketProblems from 'src/components/TicketProblems.vue'; const $props = defineProps({ id: { @@ -28,100 +30,24 @@ const { t } = useI18n(); const entityId = computed(() => { return $props.id || route.params.id; }); - -const filter = { - include: [ - { - relation: 'address', - scope: { - fields: ['id', 'name', 'mobile', 'phone', 'incotermsFk'], - }, - }, - { - relation: 'client', - scope: { - fields: [ - 'id', - 'name', - 'salesPersonFk', - 'phone', - 'mobile', - 'email', - 'isActive', - 'isFreezed', - 'isTaxDataChecked', - 'hasElectronicInvoice', - ], - include: [ - { - relation: 'user', - scope: { - fields: ['id', 'lang'], - }, - }, - { relation: 'salesPersonUser' }, - ], - }, - }, - { - relation: 'ticketState', - scope: { - include: { relation: 'state' }, - }, - }, - { - relation: 'warehouse', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'agencyMode', - scope: { - fields: ['id', 'name'], - }, - }, - { - relation: 'zone', - scope: { - fields: [ - 'agencyModeFk', - 'bonus', - 'hour', - 'id', - 'isVolumetric', - 'itemMaxSize', - 'm3Max', - 'name', - 'price', - 'travelingDays', - ], - }, - }, - ], -}; - -const data = ref(useCardDescription()); +const problems = ref({}); function ticketFilter(ticket) { return JSON.stringify({ clientFk: ticket.clientFk }); } - -const setData = (entity) => { - data.value = useCardDescription(entity.ref, entity.id); -}; </script> <template> + <FetchData + :url="`Tickets/${entityId}/getTicketProblems`" + auto-load + @on-fetch="(data) => ([problems] = data)" + /> <CardDescriptor - module="Ticket" :url="`Tickets/${entityId}`" :filter="filter" - :title="data.title" - :subtitle="data.subtitle" - @on-fetch="setData" + data-key="Ticket" :summary="$props.summary" - data-key="ticketData" width="lg-width" > <template #menu="{ entity }"> @@ -167,48 +93,9 @@ const setData = (entity) => { <VnLv :label="t('globals.warehouse')" :value="entity.warehouse?.name" /> <VnLv :label="t('globals.alias')" :value="entity.nickname" /> </template> - <template #icons="{ entity }"> - <QCardActions class="q-gutter-x-md"> - <QIcon - v-if="entity.client.isActive == false" - name="vn:disabled" - size="xs" - color="primary" - > - <QTooltip>{{ t('Client inactive') }}</QTooltip> - </QIcon> - <QIcon - v-if="entity.client.isFreezed == true" - name="vn:frozen" - size="xs" - color="primary" - > - <QTooltip>{{ t('Client Frozen') }}</QTooltip> - </QIcon> - <QIcon - v-if="entity?.problem?.includes('hasRisk')" - name="vn:risk" - size="xs" - color="primary" - > - <QTooltip>{{ t('Client has debt') }}</QTooltip> - </QIcon> - <QIcon - v-if="entity.client.isTaxDataChecked == false" - name="vn:no036" - size="xs" - color="primary" - > - <QTooltip>{{ t('Client not checked') }}</QTooltip> - </QIcon> - <QIcon - v-if="entity.isDeleted == true" - name="vn:deletedTicket" - size="xs" - color="primary" - > - <QTooltip>{{ t('This ticket is deleted') }}</QTooltip> - </QIcon> + <template #icons> + <QCardActions class="q-gutter-x-xs"> + <TicketProblems :row="problems" /> </QCardActions> </template> <template #actions="{ entity }"> diff --git a/src/pages/Ticket/Card/TicketExpedition.vue b/src/pages/Ticket/Card/TicketExpedition.vue index 166e86978..f8084ff2f 100644 --- a/src/pages/Ticket/Card/TicketExpedition.vue +++ b/src/pages/Ticket/Card/TicketExpedition.vue @@ -40,7 +40,7 @@ const expeditionsFilter = computed(() => ({ order: ['created DESC'], })); -const ticketArrayData = useArrayData('ticketData'); +const ticketArrayData = useArrayData('Ticket'); const ticketStore = ticketArrayData.store; const ticketData = computed(() => ticketStore.data); diff --git a/src/pages/Ticket/Card/TicketFilter.js b/src/pages/Ticket/Card/TicketFilter.js new file mode 100644 index 000000000..7846f1658 --- /dev/null +++ b/src/pages/Ticket/Card/TicketFilter.js @@ -0,0 +1,72 @@ +export default { + include: [ + { + relation: 'address', + scope: { + fields: ['id', 'name', 'mobile', 'phone', 'incotermsFk'], + }, + }, + { + relation: 'client', + scope: { + fields: [ + 'id', + 'name', + 'salesPersonFk', + 'phone', + 'mobile', + 'email', + 'isActive', + 'isFreezed', + 'isTaxDataChecked', + 'hasElectronicInvoice', + 'credit', + ], + include: [ + { + relation: 'user', + scope: { + fields: ['id', 'lang'], + }, + }, + { relation: 'salesPersonUser' }, + ], + }, + }, + { + relation: 'ticketState', + scope: { + include: { relation: 'state' }, + }, + }, + { + relation: 'warehouse', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'agencyMode', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'zone', + scope: { + fields: [ + 'agencyModeFk', + 'bonus', + 'hour', + 'id', + 'isVolumetric', + 'itemMaxSize', + 'm3Max', + 'name', + 'price', + 'travelingDays', + ], + }, + }, + ], +}; diff --git a/src/pages/Ticket/Card/TicketNotes.vue b/src/pages/Ticket/Card/TicketNotes.vue index f558b71cc..feb88bf84 100644 --- a/src/pages/Ticket/Card/TicketNotes.vue +++ b/src/pages/Ticket/Card/TicketNotes.vue @@ -32,7 +32,7 @@ watch( crudModelFilter.where.ticketFk = route.params.id; store.filter = crudModelFilter; await ticketNotesCrudRef.value.reload(); - } + }, ); function handleDelete(row) { ticketNotesCrudRef.value.remove([row]); @@ -105,7 +105,7 @@ async function handleSave() { <VnRow v-if="observationTypes.length > rows.length"> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat class="fill-icon-on-hover q-ml-md" color="primary" diff --git a/src/pages/Ticket/Card/TicketPackage.vue b/src/pages/Ticket/Card/TicketPackage.vue index 8ebdb4401..5fbf4c800 100644 --- a/src/pages/Ticket/Card/TicketPackage.vue +++ b/src/pages/Ticket/Card/TicketPackage.vue @@ -41,7 +41,7 @@ watch( crudModelFilter.where.ticketFk = route.params.id; store.filter = crudModelFilter; await ticketPackagingsCrudRef.value.reload(); - } + }, ); </script> @@ -118,7 +118,7 @@ watch( <VnRow> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat class="fill-icon-on-hover q-ml-md" color="primary" diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index f5fb50ecf..6f02a2ce6 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -14,7 +14,7 @@ import VnImg from 'src/components/ui/VnImg.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import TicketSaleMoreActions from './TicketSaleMoreActions.vue'; -import TicketTransfer from './TicketTransfer.vue'; +import TicketTransferProxy from './TicketTransferProxy.vue'; import { toCurrency, toPercentage } from 'src/filters'; import { useArrayData } from 'composables/useArrayData'; @@ -23,6 +23,7 @@ import useNotify from 'src/composables/useNotify.js'; import axios from 'axios'; import VnTable from 'src/components/VnTable/VnTable.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; +import TicketProblems from 'src/components/TicketProblems.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; const route = useRoute(); @@ -34,7 +35,7 @@ const editPriceProxyRef = ref(null); const editManaProxyRef = ref(null); const stateBtnDropdownRef = ref(null); const quasar = useQuasar(); -const arrayData = useArrayData('ticketData'); +const arrayData = useArrayData('Ticket'); const { store } = arrayData; const selectedRows = ref([]); const hasSelectedRows = computed(() => selectedRows.value.length > 0); @@ -626,8 +627,9 @@ watch( @click="setTransferParams()" data-cy="ticketSaleTransferBtn" > - <QTooltip>{{ t('Transfer lines') }}</QTooltip> - <TicketTransfer + <QTooltip>{{ t('ticketSale.transferLines') }}</QTooltip> + <TicketTransferProxy + class="full-width" :transfer="transfer" :ticket="store.data" @refresh-data="resetChanges()" @@ -697,53 +699,7 @@ watch( :disabled-attr="isTicketEditable" > <template #column-statusIcons="{ row }"> - <router-link - v-if="row.claim?.claimFk" - :to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }" - > - <QIcon color="primary" name="vn:claims" size="xs"> - <QTooltip> - {{ t('ticketSale.claim') }}: - {{ row.claim?.claimFk }} - </QTooltip> - </QIcon> - </router-link> - <QIcon v-if="row.visible < 0" color="primary" name="warning" size="xs"> - <QTooltip> - {{ t('ticketSale.visible') }}: {{ row.visible || 0 }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.reserved" - color="primary" - name="vn:reserva" - size="xs" - data-cy="ticketSaleReservedIcon" - > - <QTooltip> - {{ t('ticketSale.reserved') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.itemShortage" - color="primary" - name="vn:unavailable" - size="xs" - > - <QTooltip> - {{ t('ticketSale.noVisible') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.hasComponentLack" - color="primary" - name="vn:components" - size="xs" - > - <QTooltip> - {{ t('ticketSale.hasComponentLack') }} - </QTooltip> - </QIcon> + <TicketProblems :row="row" /> </template> <template #body-cell-picture="{ row }"> <QTd> @@ -881,7 +837,7 @@ watch( color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" data-cy="ticketSaleAddToBasketBtn" /> <QTooltip class="text-no-wrap"> diff --git a/src/pages/Ticket/Card/TicketService.vue b/src/pages/Ticket/Card/TicketService.vue index d045eadee..6ce69a6aa 100644 --- a/src/pages/Ticket/Card/TicketService.vue +++ b/src/pages/Ticket/Card/TicketService.vue @@ -40,7 +40,7 @@ watch( async () => { store.filter = crudModelFilter.value; await ticketServiceCrudRef.value.reload(); - } + }, ); onMounted(async () => await getDefaultTaxClass()); @@ -59,7 +59,7 @@ const createRefund = async () => { t('service.createRefundSuccess', { ticketId: refundTicket.id, }), - 'positive' + 'positive', ); router.push({ name: 'TicketSale', params: { id: refundTicket.id } }); }; @@ -225,7 +225,7 @@ async function handleSave() { color="primary" icon="add" @click="ticketServiceCrudRef.insert()" - shortcut="+" + v-shortcut="'+'" /> </QPageSticky> </template> diff --git a/src/pages/Ticket/Card/TicketSplit.vue b/src/pages/Ticket/Card/TicketSplit.vue new file mode 100644 index 000000000..e79057266 --- /dev/null +++ b/src/pages/Ticket/Card/TicketSplit.vue @@ -0,0 +1,37 @@ +<script setup> +import { ref } from 'vue'; + +import VnInputDate from 'src/components/common/VnInputDate.vue'; +import split from './components/split'; +const emit = defineEmits(['ticketTransfered']); + +const $props = defineProps({ + ticket: { + type: [Array, Object], + default: () => {}, + }, +}); + +const splitDate = ref(Date.vnNew()); + +const splitSelectedRows = async () => { + const tickets = Array.isArray($props.ticket) ? $props.ticket : [$props.ticket]; + await split(tickets, splitDate.value); + emit('ticketTransfered', tickets); +}; +</script> + +<template> + <VnInputDate class="q-mr-sm" :label="$t('New date')" v-model="splitDate" clearable /> + <QBtn class="q-mr-sm" color="primary" label="Split" @click="splitSelectedRows"></QBtn> +</template> +<style lang="scss"> +.q-table__bottom.row.items-center.q-table__bottom--nodata { + border-top: none; +} +</style> +<i18n> +es: + Sales to transfer: Líneas a transferir + Destination ticket: Ticket destinatario +</i18n> diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index 8cb518823..5838efa88 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -20,6 +20,7 @@ import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnToSummary from 'src/components/ui/VnToSummary.vue'; import TicketDescriptorMenu from './TicketDescriptorMenu.vue'; +import TicketProblems from 'src/components/TicketProblems.vue'; const route = useRoute(); const { notify } = useNotify(); @@ -40,7 +41,7 @@ const editableStates = ref([]); const ticketUrl = ref(); const grafanaUrl = 'https://grafana.verdnatura.es'; const stateBtnDropdownRef = ref(); -const descriptorData = useArrayData('ticketData'); +const descriptorData = useArrayData('Ticket'); onMounted(async () => { ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/'; @@ -320,83 +321,7 @@ onMounted(async () => { <template #body="props"> <QTr :props="props"> <QTd class="q-gutter-x-xs"> - <QBtn - flat - round - icon="vn:claims" - v-if="props.row.claim" - color="primary" - :to="{ - name: 'ClaimCard', - params: { - id: props.row.claim.claimFk, - }, - }" - > - <QTooltip> - {{ t('ticket.summary.claim') }}: - {{ props.row.claim.claimFk }} - </QTooltip> - </QBtn> - <QBtn - flat - round - icon="vn:claims" - v-if="props.row.claimBeginning" - color="primary" - :to="{ - name: 'ClaimCard', - params: { - id: props.row.claimBeginning.claimFk, - }, - }" - > - <QTooltip> - {{ t('ticket.summary.claim') }}: - {{ props.row.claimBeginning.claimFk }} - </QTooltip> - </QBtn> - <QIcon - name="warning" - v-show="props.row.visible < 0" - color="primary" - size="xs" - > - <QTooltip> - {{ t('globals.visible') }}: - {{ props.row.visible }} - </QTooltip> - </QIcon> - <QIcon - name="vn:reserved" - v-show="props.row.reserved" - color="primary" - size="xs" - > - <QTooltip> - {{ t('ticket.summary.reserved') }} - </QTooltip> - </QIcon> - <QIcon - name="vn:unavailable" - v-show="props.row.itemShortage" - color="primary" - size="xs" - > - <QTooltip> - {{ t('ticket.summary.itemShortage') }} - </QTooltip> - </QIcon> - <QIcon - name="vn:components" - v-show="props.row.hasComponentLack" - color="primary" - size="xs" - > - <QTooltip> - {{ t('ticket.summary.hasComponentLack') }} - </QTooltip> - </QIcon> + <TicketProblems :row="props.row" /> </QTd> <QTd> <QBtn class="link" flat> diff --git a/src/pages/Ticket/Card/TicketTracking.vue b/src/pages/Ticket/Card/TicketTracking.vue index f4b8544d3..acf464fb1 100644 --- a/src/pages/Ticket/Card/TicketTracking.vue +++ b/src/pages/Ticket/Card/TicketTracking.vue @@ -19,7 +19,7 @@ watch( async (val) => { paginateFilter.where.ticketFk = val; paginateRef.value.fetch(); - } + }, ); const paginateFilter = reactive({ @@ -119,7 +119,7 @@ const openCreateModal = () => createTrackingDialogRef.value.show(); color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip class="text-no-wrap"> {{ t('tracking.addState') }} diff --git a/src/pages/Ticket/Card/TicketTransfer.vue b/src/pages/Ticket/Card/TicketTransfer.vue index 005d74a0e..ffa964c92 100644 --- a/src/pages/Ticket/Card/TicketTransfer.vue +++ b/src/pages/Ticket/Card/TicketTransfer.vue @@ -1,11 +1,11 @@ <script setup> import { ref, computed, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; - import VnInput from 'src/components/common/VnInput.vue'; import TicketTransferForm from './TicketTransferForm.vue'; import { toDateFormat } from 'src/filters/date.js'; +const emit = defineEmits(['ticketTransfered']); const $props = defineProps({ mana: { @@ -21,16 +21,15 @@ const $props = defineProps({ default: () => {}, }, ticket: { - type: Object, + type: [Array, Object], default: () => {}, }, }); +onMounted(() => (_transfer.value = $props.transfer)); const { t } = useI18n(); -const QPopupProxyRef = ref(null); const transferFormRef = ref(null); const _transfer = ref(); - const transferLinesColumns = computed(() => [ { label: t('ticketList.id'), @@ -86,76 +85,74 @@ const handleRowClick = (row) => { transferFormRef.value.transferSales(ticketId); } }; - -onMounted(() => (_transfer.value = $props.transfer)); </script> <template> - <QPopupProxy ref="QPopupProxyRef" data-cy="ticketTransferPopup"> - <QCard class="q-px-md" style="display: flex; width: 80vw"> - <QTable - :rows="transfer.sales" - :columns="transferLinesColumns" - :title="t('Sales to transfer')" - row-key="id" - :pagination="{ rowsPerPage: 0 }" - class="full-width q-mt-md" - :no-data-label="t('globals.noResults')" - > - <template #body-cell-quantity="{ row }"> - <QTd @click.stop> - <VnInput - v-model.number="row.quantity" - :clearable="false" - style="max-width: 60px" - /> - </QTd> - </template> - </QTable> - <QSeparator vertical spaced /> - <QTable - v-if="transfer.lastActiveTickets" - :rows="transfer.lastActiveTickets" - :columns="destinationTicketColumns" - :title="t('Destination ticket')" - row-key="id" - class="full-width q-mt-md" - @row-click="(_, row) => handleRowClick(row)" - > - <template #body-cell-address="{ row }"> - <QTd @click.stop> - <span> - {{ row.nickname }} - {{ row.name }} - {{ row.street }} - {{ row.postalCode }} - {{ row.city }} - </span> - <QTooltip> - {{ row.nickname }} - {{ row.name }} - {{ row.street }} - {{ row.postalCode }} - {{ row.city }} - </QTooltip> - </QTd> - </template> + <QTable + :rows="transfer.sales" + :columns="transferLinesColumns" + :title="t('Sales to transfer')" + row-key="id" + :pagination="{ rowsPerPage: 0 }" + class="full-width q-mt-md" + :no-data-label="t('globals.noResults')" + > + <template #body-cell-quantity="{ row }"> + <QTd @click.stop> + <VnInput + v-model.number="row.quantity" + :clearable="false" + style="max-width: 60px" + /> + </QTd> + </template> + </QTable> + <QSeparator vertical spaced /> + <QTable + v-if="transfer.lastActiveTickets" + :rows="transfer.lastActiveTickets" + :columns="destinationTicketColumns" + :title="t('Destination ticket')" + row-key="id" + class="full-width q-mt-md" + @row-click="(_, row) => handleRowClick(row)" + :no-data-label="t('globals.noResults')" + :pagination="{ rowsPerPage: 0 }" + > + <template #body-cell-address="{ row }"> + <QTd @click.stop> + <span> + {{ row.nickname }} + {{ row.name }} + {{ row.street }} + {{ row.postalCode }} + {{ row.city }} + </span> + <QTooltip> + {{ row.nickname }} + {{ row.name }} + {{ row.street }} + {{ row.postalCode }} + {{ row.city }} + </QTooltip> + </QTd> + </template> - <template #no-data> - <TicketTransferForm ref="transferFormRef" v-bind="$props" /> - </template> - <template #bottom> - <TicketTransferForm ref="transferFormRef" v-bind="$props" /> - </template> - </QTable> - </QCard> - </QPopupProxy> + <template #no-data> + <TicketTransferForm ref="transferFormRef" v-bind="$props" /> + </template> + <template #bottom> + <TicketTransferForm ref="transferFormRef" v-bind="$props" /> + </template> + </QTable> </template> - +<style lang="scss"> +.q-table__bottom.row.items-center.q-table__bottom--nodata { + border-top: none; +} +</style> <i18n> es: Sales to transfer: Líneas a transferir Destination ticket: Ticket destinatario - Transfer to ticket: Transferir a ticket - New ticket: Nuevo ticket </i18n> diff --git a/src/pages/Ticket/Card/TicketTransferProxy.vue b/src/pages/Ticket/Card/TicketTransferProxy.vue new file mode 100644 index 000000000..3f3f018df --- /dev/null +++ b/src/pages/Ticket/Card/TicketTransferProxy.vue @@ -0,0 +1,54 @@ +<script setup> +import { ref } from 'vue'; +import TicketTransfer from './TicketTransfer.vue'; +import Split from './TicketSplit.vue'; +const emit = defineEmits(['ticketTransfered']); + +const $props = defineProps({ + mana: { + type: Number, + default: null, + }, + newPrice: { + type: Number, + default: 0, + }, + transfer: { + type: Object, + default: () => {}, + }, + ticket: { + type: [Array, Object], + default: () => {}, + }, + split: { + type: Boolean, + default: false, + }, +}); + +const popupProxyRef = ref(null); +const splitRef = ref(null); +const transferRef = ref(null); +</script> + +<template> + <QPopupProxy ref="popupProxyRef" data-cy="ticketTransferPopup"> + <div class="flex row items-center q-ma-lg" v-if="$props.split"> + <Split + ref="splitRef" + @splitSelectedRows="splitSelectedRows" + :ticket="$props.ticket" + /> + </div> + + <div v-else> + <TicketTransfer + ref="transferRef" + :ticket="$props.ticket" + :sales="$props.sales" + :transfer="$props.transfer" + /> + </div> + </QPopupProxy> +</template> diff --git a/src/pages/Ticket/Card/components/split.js b/src/pages/Ticket/Card/components/split.js new file mode 100644 index 000000000..afa1d5cd6 --- /dev/null +++ b/src/pages/Ticket/Card/components/split.js @@ -0,0 +1,22 @@ +import axios from 'axios'; +import notifyResults from 'src/utils/notifyResults'; + +export default async function (data, date) { + const reducedData = data.reduce((acc, item) => { + const existing = acc.find(({ ticketFk }) => ticketFk === item.id); + if (existing) { + existing.sales.push(item.saleFk); + } else { + acc.push({ ticketFk: item.id, sales: [item.saleFk], date }); + } + return acc; + }, []); + + const promises = reducedData.map((params) => axios.post(`Tickets/split`, params)); + + const results = await Promise.allSettled(promises); + + notifyResults(results, 'ticketFk'); + + return results; +} diff --git a/src/pages/Ticket/Negative/TicketLackDetail.vue b/src/pages/Ticket/Negative/TicketLackDetail.vue new file mode 100644 index 000000000..dcf835d03 --- /dev/null +++ b/src/pages/Ticket/Negative/TicketLackDetail.vue @@ -0,0 +1,198 @@ +<script setup> +import { computed, onMounted, onUnmounted, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import ChangeQuantityDialog from './components/ChangeQuantityDialog.vue'; +import ChangeStateDialog from './components/ChangeStateDialog.vue'; +import ChangeItemDialog from './components/ChangeItemDialog.vue'; +import TicketTransferProxy from '../Card/TicketTransferProxy.vue'; +import FetchData from 'src/components/FetchData.vue'; +import { useStateStore } from 'stores/useStateStore'; +import { useState } from 'src/composables/useState'; + +import { useRoute } from 'vue-router'; +import TicketLackTable from './TicketLackTable.vue'; +import VnPopupProxy from 'src/components/common/VnPopupProxy.vue'; +import ItemProposalProxy from 'src/pages/Item/components/ItemProposalProxy.vue'; + +import { useQuasar } from 'quasar'; +const quasar = useQuasar(); +const { t } = useI18n(); +const editableStates = ref([]); +const stateStore = useStateStore(); +const tableRef = ref(); +const changeItemDialogRef = ref(null); +const changeStateDialogRef = ref(null); +const changeQuantityDialogRef = ref(null); +const showProposalDialog = ref(false); +const showChangeQuantityDialog = ref(false); +const selectedRows = ref([]); +const route = useRoute(); +onMounted(() => { + stateStore.rightDrawer = false; +}); +onUnmounted(() => { + stateStore.rightDrawer = true; +}); + +const entityId = computed(() => route.params.id); +const item = ref({}); + +const itemProposalSelected = ref(null); +const reload = async () => { + tableRef.value.tableRef.reload(); +}; +defineExpose({ reload }); +const filter = computed(() => ({ + scopeDays: route.query.days, + showType: true, + alertLevelCode: 'FREE', + date: Date.vnNew(), + warehouseFk: useState().getUser().value.warehouseFk, +})); +const itemProposalEvt = (data) => { + const { itemProposal } = data; + itemProposalSelected.value = itemProposal; + reload(); +}; + +function onBuysFetched(data) { + Object.assign(item.value, data[0]); +} +const showItemProposal = () => { + quasar + .dialog({ + component: ItemProposalProxy, + componentProps: { + itemLack: tableRef.value.itemLack, + replaceAction: true, + sales: selectedRows.value, + }, + }) + .onOk(itemProposalEvt); +}; +</script> + +<template> + <FetchData + url="States/editableStates" + @on-fetch="(data) => (editableStates = data)" + auto-load + /> + <FetchData + :url="`Items/${entityId}/getCard`" + :fields="['longName']" + @on-fetch="(data) => (item = data)" + auto-load + /> + <FetchData + :url="`Buys/latestBuysFilter`" + :fields="['longName']" + :filter="{ where: { 'i.id': entityId } }" + @on-fetch="onBuysFetched" + auto-load + /> + + <TicketLackTable + ref="tableRef" + :filter="filter" + @update:selection="({ value }, _) => (selectedRows = value)" + > + <template #top-right> + <QBtnGroup push class="q-mr-lg" style="column-gap: 1px"> + <QBtn + data-cy="transferLines" + color="primary" + :disable="!(selectedRows.length === 1)" + > + <template #default> + <QIcon name="vn:splitline" /> + <QIcon name="vn:ticket" /> + + <QTooltip>{{ t('ticketSale.transferLines') }} </QTooltip> + <TicketTransferProxy + ref="transferFormRef" + split="true" + :ticket="selectedRows" + :transfer="{ + sales: selectedRows, + lastActiveTickets: selectedRows.map((row) => row.id), + }" + @ticket-transfered="reload" + ></TicketTransferProxy> + </template> + </QBtn> + <QBtn + color="primary" + @click="showProposalDialog = true" + :disable="selectedRows.length < 1" + data-cy="itemProposal" + > + <QIcon + name="import_export" + class="rotate-90" + @click="showItemProposal" + ></QIcon> + <QTooltip bottom anchor="bottom right"> + {{ t('itemProposal') }} + </QTooltip> + </QBtn> + <VnPopupProxy + data-cy="changeItem" + icon="sync" + :disable="selectedRows.length < 1" + :tooltip="t('negative.detail.modal.changeItem.title')" + > + <template #extraIcon> <QIcon name="vn:item" /> </template> + <template v-slot="{ popup }"> + <ChangeItemDialog + ref="changeItemDialogRef" + @update-item="popup.hide()" + :selected-rows="selectedRows" + /></template> + </VnPopupProxy> + <VnPopupProxy + data-cy="changeState" + icon="sync" + :disable="selectedRows.length < 1" + :tooltip="t('negative.detail.modal.changeState.title')" + > + <template #extraIcon> <QIcon name="vn:eye" /> </template> + <template v-slot="{ popup }"> + <ChangeStateDialog + ref="changeStateDialogRef" + @update-state="popup.hide()" + :selected-rows="selectedRows" + /></template> + </VnPopupProxy> + <VnPopupProxy + data-cy="changeQuantity" + icon="sync" + :disable="selectedRows.length < 1" + :tooltip="t('negative.detail.modal.changeQuantity.title')" + @click="showChangeQuantityDialog = true" + > + <template #extraIcon> <QIcon name="exposure" /> </template> + <template v-slot="{ popup }"> + <ChangeQuantityDialog + ref="changeQuantityDialogRef" + @update-quantity="popup.hide()" + :selected-rows="selectedRows" + /></template> + </VnPopupProxy> </QBtnGroup + ></template> + </TicketLackTable> +</template> +<style lang="scss" scoped> +.list-enter-active, +.list-leave-active { + transition: all 1s ease; +} +.list-enter-from, +.list-leave-to { + opacity: 0; + background-color: $primary; +} +.q-table.q-table__container > div:first-child { + border-radius: unset; +} +</style> diff --git a/src/pages/Ticket/Negative/TicketLackFilter.vue b/src/pages/Ticket/Negative/TicketLackFilter.vue new file mode 100644 index 000000000..3762f453d --- /dev/null +++ b/src/pages/Ticket/Negative/TicketLackFilter.vue @@ -0,0 +1,175 @@ +<script setup> +import { ref } from 'vue'; +import { useI18n } from 'vue-i18n'; + +import FetchData from 'components/FetchData.vue'; +import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +const { t } = useI18n(); +const props = defineProps({ + dataKey: { + type: String, + required: true, + }, +}); + +const to = Date.vnNew(); +to.setDate(to.getDate() + 1); + +const warehouses = ref(); +const categoriesOptions = ref([]); +const itemTypesRef = ref(null); +const itemTypesOptions = ref([]); + +const itemTypesFilter = { + fields: ['id', 'name', 'categoryFk'], + include: 'category', + order: 'name ASC', + where: {}, +}; +const onCategoryChange = async (categoryFk, search) => { + if (!categoryFk) { + itemTypesFilter.where.categoryFk = null; + delete itemTypesFilter.where.categoryFk; + } else { + itemTypesFilter.where.categoryFk = categoryFk; + } + search(); + await itemTypesRef.value.fetch(); +}; +const emit = defineEmits(['set-user-params']); + +const setUserParams = (params) => { + emit('set-user-params', params); +}; +</script> + +<template> + <FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load /> + <FetchData + url="ItemCategories" + :filter="{ fields: ['id', 'name'], order: 'name ASC' }" + @on-fetch="(data) => (categoriesOptions = data)" + auto-load + /> + + <FetchData + ref="itemTypesRef" + url="ItemTypes" + :filter="itemTypesFilter" + @on-fetch="(data) => (itemTypesOptions = data)" + auto-load + /> + + <VnFilterPanel + :data-key="props.dataKey" + :search-button="true" + @set-user-params="setUserParams" + > + <template #tags="{ tag, formatFn }"> + <div class="q-gutter-x-xs"> + <strong>{{ t(`negative.${tag.label}`) }}</strong> + <span>{{ formatFn(tag.value) }}</span> + </div> + </template> + <template #body="{ params, searchFn }"> + <QList dense class="q-gutter-y-sm q-mt-sm"> + <QItem> + <QItemSection> + <VnInput + v-model="params.days" + :label="t('negative.days')" + dense + is-outlined + type="number" + @update:model-value=" + (value) => { + setUserParams(params); + } + " + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.id" + :label="t('negative.id')" + dense + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.producer" + :label="t('negative.producer')" + dense + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.origen" + :label="t('negative.origen')" + dense + is-outlined + /> + </QItemSection> </QItem + ><QItem> + <QItemSection v-if="categoriesOptions"> + <VnSelect + :label="t('negative.categoryFk')" + v-model="params.categoryFk" + @update:model-value=" + ($event) => onCategoryChange($event, searchFn) + " + :options="categoriesOptions" + option-value="id" + option-label="name" + hide-selected + dense + outlined + rounded + /> </QItemSection + ><QItemSection v-else> + <QSkeleton class="full-width" type="QSelect" /> + </QItemSection> + </QItem> + <QItem> + <QItemSection v-if="itemTypesOptions"> + <VnSelect + :label="t('negative.type')" + v-model="params.typeFk" + @update:model-value="searchFn()" + :options="itemTypesOptions" + option-value="id" + option-label="name" + hide-selected + dense + outlined + rounded + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel>{{ scope.opt?.name }}</QItemLabel> + <QItemLabel caption>{{ + scope.opt?.category?.name + }}</QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> </QItemSection + ><QItemSection v-else> + <QSkeleton class="full-width" type="QSelect" /> + </QItemSection> + </QItem> + </QList> + </template> + </VnFilterPanel> +</template> diff --git a/src/pages/Ticket/Negative/TicketLackList.vue b/src/pages/Ticket/Negative/TicketLackList.vue new file mode 100644 index 000000000..d1e8b823a --- /dev/null +++ b/src/pages/Ticket/Negative/TicketLackList.vue @@ -0,0 +1,227 @@ +<script setup> +import { computed, ref, reactive } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useStateStore } from 'stores/useStateStore'; +import VnTable from 'components/VnTable/VnTable.vue'; +import { onBeforeMount } from 'vue'; +import { dashIfEmpty, toDate, toHour } from 'src/filters'; +import { useRouter } from 'vue-router'; +import { useState } from 'src/composables/useState'; +import { useRole } from 'src/composables/useRole'; +import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; +import RightMenu from 'src/components/common/RightMenu.vue'; +import TicketLackFilter from './TicketLackFilter.vue'; +onBeforeMount(() => { + stateStore.$state.rightDrawer = true; +}); +const router = useRouter(); +const stateStore = useStateStore(); +const { t } = useI18n(); +const selectedRows = ref([]); +const tableRef = ref(); +const filterParams = ref({}); +const negativeParams = reactive({ + days: useRole().likeAny('buyer') ? 2 : 0, + warehouseFk: useState().getUser().value.warehouseFk, +}); +const redirectToCreateView = ({ itemFk }) => { + router.push({ + name: 'NegativeDetail', + params: { id: itemFk }, + query: { days: filterParams.value.days ?? negativeParams.days }, + }); +}; +const columns = computed(() => [ + { + name: 'date', + align: 'center', + label: t('negative.date'), + format: ({ timed }) => toDate(timed), + sortable: true, + cardVisible: true, + isId: true, + columnFilter: { + component: 'date', + }, + }, + { + columnClass: 'shrink', + name: 'timed', + align: 'center', + label: t('negative.timed'), + format: ({ timed }) => toHour(timed), + sortable: true, + cardVisible: true, + columnFilter: { + component: 'time', + }, + }, + { + name: 'itemFk', + align: 'center', + label: t('negative.id'), + format: ({ itemFk }) => itemFk, + sortable: true, + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + }, + { + name: 'longName', + align: 'center', + label: t('negative.longName'), + field: ({ longName }) => longName, + + sortable: true, + headerStyle: 'width: 350px', + cardVisible: true, + columnClass: 'expand', + }, + { + name: 'producer', + align: 'center', + label: t('negative.supplier'), + field: ({ producer }) => dashIfEmpty(producer), + sortable: true, + columnClass: 'shrink', + }, + { + name: 'inkFk', + align: 'center', + label: t('negative.colour'), + field: ({ inkFk }) => inkFk, + sortable: true, + cardVisible: true, + }, + { + name: 'size', + align: 'center', + label: t('negative.size'), + field: ({ size }) => size, + sortable: true, + cardVisible: true, + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + }, + { + name: 'category', + align: 'center', + label: t('negative.origen'), + field: ({ category }) => dashIfEmpty(category), + sortable: true, + cardVisible: true, + }, + { + name: 'lack', + align: 'center', + label: t('negative.lack'), + field: ({ lack }) => lack, + columnFilter: { + component: 'input', + type: 'number', + columnClass: 'shrink', + }, + sortable: true, + headerStyle: 'padding-center: 33px', + cardVisible: true, + }, + { + name: 'tableActions', + align: 'center', + actions: [ + { + title: t('Open details'), + icon: 'edit', + action: redirectToCreateView, + isPrimary: true, + }, + ], + }, +]); + +const setUserParams = (params) => { + filterParams.value = params; +}; +</script> + +<template> + <RightMenu> + <template #right-panel> + <TicketLackFilter data-key="NegativeList" @set-user-params="setUserParams" /> + </template> + </RightMenu> + {{ filterRef }} + <VnTable + ref="tableRef" + data-key="NegativeList" + :url="`Tickets/itemLack`" + :order="['itemFk DESC, date DESC, timed DESC']" + :user-params="negativeParams" + auto-load + :columns="columns" + default-mode="table" + :right-search="false" + :is-editable="false" + :use-model="true" + :map-key="false" + :row-click="redirectToCreateView" + v-model:selected="selectedRows" + :create="false" + :crud-model="{ + disableInfiniteScroll: true, + }" + :table="{ + 'row-key': 'itemFk', + selection: 'multiple', + }" + > + <template #column-itemFk="{ row }"> + <div + style="display: flex; justify-content: space-around; align-items: center" + > + <span @click.stop>{{ row.itemFk }}</span> + </div> + </template> + <template #column-longName="{ row }"> + <span class="link" @click.stop> + {{ row.longName }} + <ItemDescriptorProxy :id="row.itemFk" /> + </span> + </template> + </VnTable> +</template> + +<style lang="scss" scoped> +.list { + max-height: 100%; + padding: 15px; + width: 100%; +} + +.grid-style-transition { + transition: + transform 0.28s, + background-color 0.28s; +} + +#true { + background-color: $positive; +} + +#false { + background-color: $negative; +} + +div.q-dialog__inner > div { + max-width: fit-content !important; +} +.q-btn-group > .q-btn-item:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +</style> diff --git a/src/pages/Ticket/Negative/TicketLackTable.vue b/src/pages/Ticket/Negative/TicketLackTable.vue new file mode 100644 index 000000000..176e8f7ad --- /dev/null +++ b/src/pages/Ticket/Negative/TicketLackTable.vue @@ -0,0 +1,356 @@ +<script setup> +import FetchedTags from 'components/ui/FetchedTags.vue'; +import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; +import { computed, ref, watch } from 'vue'; +import { useI18n } from 'vue-i18n'; +import axios from 'axios'; +import FetchData from 'src/components/FetchData.vue'; +import { toDate, toHour } from 'src/filters'; +import useNotify from 'src/composables/useNotify.js'; +import ZoneDescriptorProxy from 'pages/Zone/Card/ZoneDescriptorProxy.vue'; +import { useRoute } from 'vue-router'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import TicketDescriptorProxy from '../Card/TicketDescriptorProxy.vue'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; + +const $props = defineProps({ + filter: { + type: Object, + default: () => ({}), + }, +}); + +watch( + () => $props.filter, + (v) => { + filterLack.value.where = v; + tableRef.value.reload(filterLack); + }, +); + +const filterLack = ref({ + include: [ + { + relation: 'workers', + scope: { + fields: ['id', 'firstName'], + }, + }, + ], + where: { ...$props.filter }, + order: 'ts.alertLevelCode ASC', +}); + +const selectedRows = ref([]); +const { t } = useI18n(); +const { notify } = useNotify(); +const entityId = computed(() => route.params.id); +const item = ref({}); +const route = useRoute(); +const columns = computed(() => [ + { + name: 'status', + align: 'center', + sortable: false, + columnClass: 'shrink', + columnFilter: false, + }, + { + name: 'ticketFk', + label: t('negative.detail.ticketFk'), + align: 'center', + sortable: true, + columnFilter: { + component: 'input', + type: 'number', + }, + }, + { + name: 'shipped', + label: t('negative.detail.shipped'), + field: 'shipped', + align: 'center', + format: ({ shipped }) => toDate(shipped), + sortable: true, + columnFilter: { + component: 'date', + columnClass: 'shrink', + }, + }, + { + name: 'minTimed', + label: t('negative.detail.theoreticalhour'), + field: 'minTimed', + align: 'center', + sortable: true, + component: 'time', + columnFilter: {}, + }, + { + name: 'alertLevelCode', + label: t('negative.detail.state'), + columnFilter: { + name: 'alertLevelCode', + component: 'select', + attrs: { + url: 'AlertLevels', + fields: ['name', 'code'], + optionLabel: 'code', + optionValue: 'code', + }, + }, + align: 'center', + sortable: true, + }, + { + name: 'zoneName', + label: t('negative.detail.zoneName'), + field: 'zoneName', + align: 'center', + sortable: true, + }, + { + name: 'nickname', + label: t('negative.detail.nickname'), + field: 'nickname', + align: 'center', + sortable: true, + }, + { + name: 'quantity', + label: t('negative.detail.quantity'), + field: 'quantity', + sortable: true, + component: 'input', + type: 'number', + }, +]); + +const emit = defineEmits(['update:selection']); +const itemLack = ref(null); +const fetchItemLack = ref(null); +const tableRef = ref(null); +defineExpose({ tableRef, itemLack }); +watch(selectedRows, () => emit('update:selection', selectedRows)); +const getInputEvents = ({ col, ...rows }) => ({ + 'update:modelValue': () => saveChange(col.name, rows), + 'keyup.enter': () => saveChange(col.name, rows), +}); +const saveChange = async (field, { row }) => { + try { + switch (field) { + case 'alertLevelCode': + await axios.post(`Tickets/state`, { + ticketFk: row.ticketFk, + code: row[field], + }); + break; + + case 'quantity': + await axios.post(`Sales/${row.saleFk}/updateQuantity`, { + quantity: +row.quantity, + }); + break; + } + notify('globals.dataSaved', 'positive'); + fetchItemLack.value.fetch(); + } catch (err) { + console.error('Error saving changes', err); + f; + } +}; + +function onBuysFetched(data) { + Object.assign(item.value, data[0]); +} +</script> + +<template> + <FetchData + ref="fetchItemLack" + :url="`Tickets/itemLack`" + :params="{ id: entityId }" + @on-fetch="(data) => (itemLack = data[0])" + auto-load + /> + <FetchData + :url="`Items/${entityId}/getCard`" + :fields="['longName']" + @on-fetch="(data) => (item = data)" + auto-load + /> + <FetchData + :url="`Buys/latestBuysFilter`" + :fields="['longName']" + :filter="{ where: { 'i.id': entityId } }" + @on-fetch="onBuysFetched" + auto-load + /> + <VnTable + ref="tableRef" + data-key="NegativeItem" + :map-key="false" + :url="`Tickets/itemLack/${entityId}`" + :columns="columns" + auto-load + :create="false" + :create-as-dialog="false" + :use-model="true" + :filter="filterLack" + :order="['ts.alertLevelCode ASC']" + :table="{ + 'row-key': 'id', + selection: 'multiple', + }" + dense + :is-editable="true" + :row-click="false" + :right-search="false" + :right-search-icon="false" + v-model:selected="selectedRows" + :disable-option="{ card: true }" + > + <template #top-left> + <div style="display: flex; align-items: center" v-if="itemLack"> + <!-- <VnImg :id="itemLack.itemFk" class="rounded image-wrapper"></VnImg> --> + <div class="flex column" style="align-items: center"> + <QBadge + ref="badgeLackRef" + class="q-ml-xs" + text-color="white" + :color="itemLack.lack === 0 ? 'positive' : 'negative'" + :label="itemLack.lack" + /> + </div> + <div class="flex column left" style="align-items: flex-start"> + <QBtn flat class="link text-blue"> + {{ item?.longName ?? item.name }} + <ItemDescriptorProxy :id="entityId" /> + <FetchedTags class="q-ml-md" :item="item" :columns="7" /> + </QBtn> + </div> + </div> + </template> + <template #top-right> + <slot name="top-right" /> + </template> + + <template #column-status="{ row }"> + <QTd style="min-width: 150px"> + <div class="icon-container"> + <QIcon + v-if="row.isBasket" + name="vn:basket" + color="primary" + class="cursor-pointer" + size="xs" + > + <QTooltip>{{ t('negative.detail.isBasket') }}</QTooltip> + </QIcon> + <QIcon + v-if="row.hasToIgnore" + name="star" + color="primary" + class="cursor-pointer fill-icon" + size="xs" + > + <QTooltip>{{ t('negative.detail.hasToIgnore') }}</QTooltip> + </QIcon> + <QIcon + v-if="row.hasObservation" + name="change_circle" + color="primary" + class="cursor-pointer" + size="xs" + > + <QTooltip>{{ + t('negative.detail.hasObservation') + }}</QTooltip> </QIcon + ><QIcon + v-if="row.isRookie" + name="vn:Person" + size="xs" + color="primary" + class="cursor-pointer" + > + <QTooltip>{{ t('negative.detail.isRookie') }}</QTooltip> + </QIcon> + <QIcon + v-if="row.peticionCompra" + name="vn:buyrequest" + size="xs" + color="primary" + class="cursor-pointer" + > + <QTooltip>{{ t('negative.detail.peticionCompra') }}</QTooltip> + </QIcon> + <QIcon + v-if="row.turno" + name="vn:calendar" + size="xs" + color="primary" + class="cursor-pointer" + > + <QTooltip>{{ t('negative.detail.turno') }}</QTooltip> + </QIcon> + </div></QTd + > + </template> + <template #column-nickname="{ row }"> + <span class="link" @click.stop> + {{ row.nickname }} + <CustomerDescriptorProxy :id="row.customerId" /> + </span> + </template> + <template #column-ticketFk="{ row }"> + <span class="q-pa-sm link"> + {{ row.id }} + <TicketDescriptorProxy :id="row.id" /> + </span> + </template> + <template #column-alertLevelCode="props"> + <VnSelect + url="States/editableStates" + auto-load + hide-selected + option-value="id" + option-label="name" + v-model="props.row.alertLevelCode" + v-on="getInputEvents(props)" + /> + </template> + + <template #column-zoneName="{ row }"> + <span class="link">{{ row.zoneName }}</span> + <ZoneDescriptorProxy :id="row.zoneFk" /> + </template> + <template #column-quantity="props"> + <VnInputNumber + v-model.number="props.row.quantity" + v-on="getInputEvents(props)" + ></VnInputNumber> + </template> + </VnTable> +</template> +<style lang="scss" scoped> +.icon-container { + display: grid; + grid-template-columns: repeat(3, 0.2fr); + row-gap: 5px; /* Ajusta el espacio entre los iconos según sea necesario */ +} +.icon-container > * { + width: 100%; + height: auto; +} +.list-enter-active, +.list-leave-active { + transition: all 1s ease; +} +.list-enter-from, +.list-leave-to { + opacity: 0; + background-color: $primary; +} +</style> diff --git a/src/pages/Ticket/Negative/components/ChangeItemDialog.vue b/src/pages/Ticket/Negative/components/ChangeItemDialog.vue new file mode 100644 index 000000000..e419b85c0 --- /dev/null +++ b/src/pages/Ticket/Negative/components/ChangeItemDialog.vue @@ -0,0 +1,90 @@ +<script setup> +import { ref } from 'vue'; +import axios from 'axios'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import notifyResults from 'src/utils/notifyResults'; +const emit = defineEmits(['update-item']); + +const showChangeItemDialog = ref(false); +const newItem = ref(null); +const $props = defineProps({ + selectedRows: { + type: Array, + default: () => [], + }, +}); + +const updateItem = async () => { + try { + showChangeItemDialog.value = true; + const rowsToUpdate = $props.selectedRows.map(({ saleFk, quantity }) => + axios.post(`Sales/replaceItem`, { + saleFk, + substitutionFk: newItem.value, + quantity, + }), + ); + const result = await Promise.allSettled(rowsToUpdate); + notifyResults(result, 'saleFk'); + emit('update-item', newItem.value); + } catch (err) { + console.error('Error updating item:', err); + return err; + } +}; +</script> + +<template> + <QCard class="q-pa-sm"> + <QCardSection class="row items-center justify-center column items-stretch"> + {{ showChangeItemDialog }} + <span>{{ $t('negative.detail.modal.changeItem.title') }}</span> + <VnSelect + url="Items/WithName" + :fields="['id', 'name']" + :sort-by="['id DESC']" + :options="items" + option-label="name" + option-value="id" + v-model="newItem" + > + </VnSelect> + </QCardSection> + <QCardActions align="right"> + <QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup /> + <QBtn + :label="$t('globals.confirm')" + color="primary" + :disable="!newItem" + @click="updateItem" + unelevated + autofocus + /> </QCardActions + ></QCard> +</template> + +<style lang="scss" scoped> +.list { + max-height: 100%; + padding: 15px; + width: 100%; +} + +.grid-style-transition { + transition: + transform 0.28s, + background-color 0.28s; +} + +#true { + background-color: $positive; +} + +#false { + background-color: $negative; +} + +div.q-dialog__inner > div { + max-width: fit-content !important; +} +</style> diff --git a/src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue b/src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue new file mode 100644 index 000000000..2e9aac4f0 --- /dev/null +++ b/src/pages/Ticket/Negative/components/ChangeQuantityDialog.vue @@ -0,0 +1,84 @@ +<script setup> +import { ref } from 'vue'; +import axios from 'axios'; +import VnInput from 'src/components/common/VnInput.vue'; +import notifyResults from 'src/utils/notifyResults'; + +const showChangeQuantityDialog = ref(false); +const newQuantity = ref(null); +const $props = defineProps({ + selectedRows: { + type: Array, + default: () => [], + }, +}); +const emit = defineEmits(['update-quantity']); +const updateQuantity = async () => { + try { + showChangeQuantityDialog.value = true; + const rowsToUpdate = $props.selectedRows.map(({ saleFk }) => + axios.post(`Sales/${saleFk}/updateQuantity`, { + saleFk, + quantity: +newQuantity.value, + }), + ); + + const result = await Promise.allSettled(rowsToUpdate); + notifyResults(result, 'saleFk'); + + emit('update-quantity', newQuantity.value); + } catch (err) { + return err; + } +}; +</script> + +<template> + <QCard class="q-pa-sm"> + <QCardSection class="row items-center justify-center column items-stretch"> + <span>{{ $t('negative.detail.modal.changeQuantity.title') }}</span> + <VnInput + type="number" + :min="0" + :label="$t('negative.detail.modal.changeQuantity.placeholder')" + v-model="newQuantity" + /> + </QCardSection> + <QCardActions align="right"> + <QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup /> + <QBtn + :label="$t('globals.confirm')" + color="primary" + :disable="!newQuantity || newQuantity < 0" + @click="updateQuantity" + unelevated + autofocus + /> </QCardActions + ></QCard> +</template> + +<style lang="scss" scoped> +.list { + max-height: 100%; + padding: 15px; + width: 100%; +} + +.grid-style-transition { + transition: + transform 0.28s, + background-color 0.28s; +} + +#true { + background-color: $positive; +} + +#false { + background-color: $negative; +} + +div.q-dialog__inner > div { + max-width: fit-content !important; +} +</style> diff --git a/src/pages/Ticket/Negative/components/ChangeStateDialog.vue b/src/pages/Ticket/Negative/components/ChangeStateDialog.vue new file mode 100644 index 000000000..1acc7e0ef --- /dev/null +++ b/src/pages/Ticket/Negative/components/ChangeStateDialog.vue @@ -0,0 +1,91 @@ +<script setup> +import { ref } from 'vue'; +import axios from 'axios'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import FetchData from 'components/FetchData.vue'; +import notifyResults from 'src/utils/notifyResults'; + +const emit = defineEmits(['update-state']); +const editableStates = ref([]); +const showChangeStateDialog = ref(false); +const newState = ref(null); +const $props = defineProps({ + selectedRows: { + type: Array, + default: () => [], + }, +}); +const updateState = async () => { + try { + showChangeStateDialog.value = true; + const rowsToUpdate = $props.selectedRows.map(({ id }) => + axios.post(`Tickets/state`, { + ticketFk: id, + code: newState.value, + }), + ); + const result = await Promise.allSettled(rowsToUpdate); + notifyResults(result, 'ticketFk'); + + emit('update-state', newState.value); + } catch (err) { + return err; + } +}; +</script> + +<template> + <FetchData + url="States/editableStates" + @on-fetch="(data) => (editableStates = data)" + auto-load + /> + <QCard class="q-pa-sm"> + <QCardSection class="row items-center justify-center column items-stretch"> + <span>{{ $t('negative.detail.modal.changeState.title') }}</span> + <VnSelect + :label="$t('negative.detail.modal.changeState.placeholder')" + v-model="newState" + :options="editableStates" + option-label="name" + option-value="code" + /> + </QCardSection> + <QCardActions align="right"> + <QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup /> + <QBtn + :label="$t('globals.confirm')" + color="primary" + :disable="!newState" + @click="updateState" + unelevated + autofocus + /> </QCardActions + ></QCard> +</template> + +<style lang="scss" scoped> +.list { + max-height: 100%; + padding: 15px; + width: 100%; +} + +.grid-style-transition { + transition: + transform 0.28s, + background-color 0.28s; +} + +#true { + background-color: $positive; +} + +#false { + background-color: $negative; +} + +div.q-dialog__inner > div { + max-width: fit-content !important; +} +</style> diff --git a/src/pages/Ticket/TicketFuture.vue b/src/pages/Ticket/TicketFuture.vue index 0d216bed4..92911cd25 100644 --- a/src/pages/Ticket/TicketFuture.vue +++ b/src/pages/Ticket/TicketFuture.vue @@ -1,24 +1,22 @@ <script setup> -import { onMounted, ref, computed, reactive } from 'vue'; +import { ref, computed, reactive, watch } from 'vue'; import { useI18n } from 'vue-i18n'; -import FetchData from 'components/FetchData.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; import TicketFutureFilter from './TicketFutureFilter.vue'; import { dashIfEmpty, toCurrency } from 'src/filters'; import { useVnConfirm } from 'composables/useVnConfirm'; -import { useArrayData } from 'composables/useArrayData'; import { getDateQBadgeColor } from 'src/composables/getDateQBadgeColor.js'; import useNotify from 'src/composables/useNotify.js'; import { useState } from 'src/composables/useState'; import { toDateTimeFormat } from 'src/filters/date.js'; import axios from 'axios'; +import TicketProblems from 'src/components/TicketProblems.vue'; const state = useState(); const { t } = useI18n(); @@ -26,214 +24,126 @@ const { openConfirmationModal } = useVnConfirm(); const { notify } = useNotify(); const user = state.getUser(); -const itemPackingTypesOptions = ref([]); const selectedTickets = ref([]); - -const exprBuilder = (param, value) => { - switch (param) { - case 'id': - return { id: value }; - case 'futureId': - return { futureId: value }; - case 'liters': - return { liters: value }; - case 'lines': - return { lines: value }; - case 'iptColFilter': - return { ipt: { like: `%${value}%` } }; - case 'futureIptColFilter': - return { futureIpt: { like: `%${value}%` } }; - case 'totalWithVat': - return { totalWithVat: value }; - } -}; - +const vnTableRef = ref({}); +const originElRef = ref(null); +const destinationElRef = ref(null); const userParams = reactive({ futureScopeDays: Date.vnNew().toISOString(), originScopeDays: Date.vnNew().toISOString(), warehouseFk: user.value.warehouseFk, }); -const arrayData = useArrayData('FutureTickets', { - url: 'Tickets/getTicketsFuture', - userParams: userParams, - exprBuilder: exprBuilder, -}); -const { store } = arrayData; - -const params = reactive({ - futureScopeDays: Date.vnNew(), - originScopeDays: Date.vnNew(), - warehouseFk: user.value.warehouseFk, -}); - -const applyColumnFilter = async (col) => { - const paramKey = col.columnFilter?.filterParamKey || col.field; - params[paramKey] = col.columnFilter.filterValue; - await arrayData.addFilter({ params }); -}; - -const getInputEvents = (col) => { - return col.columnFilter.type === 'select' - ? { 'update:modelValue': () => applyColumnFilter(col) } - : { - 'keyup.enter': () => applyColumnFilter(col), - }; -}; - -const tickets = computed(() => store.data); - const ticketColumns = computed(() => [ { - label: t('futureTickets.problems'), + label: '', name: 'problems', + headerClass: 'horizontal-separator', align: 'left', - columnFilter: null, + columnFilter: false, }, { label: t('advanceTickets.ticketId'), - name: 'ticketId', + name: 'id', align: 'center', - sortable: true, - columnFilter: { - component: VnInput, - type: 'text', - filterValue: null, - filterParamKey: 'id', - event: getInputEvents, - attrs: { - dense: true, - }, - }, + headerClass: 'horizontal-separator', }, { label: t('futureTickets.shipped'), name: 'shipped', align: 'left', - sortable: true, - columnFilter: null, + columnFilter: false, + headerClass: 'horizontal-separator', }, { + align: 'center', + class: 'shrink', label: t('advanceTickets.ipt'), name: 'ipt', - field: 'ipt', - align: 'left', - sortable: true, columnFilter: { - component: VnSelect, - filterParamKey: 'iptColFilter', - type: 'select', - filterValue: null, - event: getInputEvents, + component: 'select', attrs: { - options: itemPackingTypesOptions.value, - 'option-value': 'code', - 'option-label': 'description', - dense: true, + url: 'itemPackingTypes', + fields: ['code', 'description'], + where: { isActive: true }, + optionValue: 'code', + optionLabel: 'description', + inWhere: false, }, }, - format: (val) => dashIfEmpty(val), + format: (row, dashIfEmpty) => dashIfEmpty(row.ipt), + headerClass: 'horizontal-separator', }, { label: t('ticketList.state'), name: 'state', align: 'left', - sortable: true, - columnFilter: null, + columnFilter: false, + headerClass: 'horizontal-separator', }, { label: t('advanceTickets.liters'), name: 'liters', - field: 'liters', align: 'left', - sortable: true, - columnFilter: { - component: VnInput, - type: 'text', - filterValue: null, - event: getInputEvents, - attrs: { - dense: true, - }, - }, + headerClass: 'horizontal-separator', }, { label: t('advanceTickets.import'), - field: 'import', name: 'import', align: 'left', - sortable: true, + headerClass: 'horizontal-separator', + columnFilter: false, + format: (row) => toCurrency(row.totalWithVat), }, { label: t('futureTickets.availableLines'), name: 'lines', field: 'lines', align: 'center', - sortable: true, - columnFilter: { - component: VnInput, - type: 'text', - filterValue: null, - event: getInputEvents, - attrs: { - dense: true, - }, - }, - format: (val) => dashIfEmpty(val), + headerClass: 'horizontal-separator', + format: (row, dashIfEmpty) => dashIfEmpty(row.lines), }, { label: t('advanceTickets.futureId'), name: 'futureId', - align: 'left', - sortable: true, - columnFilter: { - component: VnInput, - type: 'text', - filterValue: null, - filterParamKey: 'futureId', - event: getInputEvents, - attrs: { - dense: true, - }, - }, + align: 'center', + headerClass: 'horizontal-separator vertical-separator ', + columnClass: 'vertical-separator', }, { label: t('futureTickets.futureShipped'), name: 'futureShipped', align: 'left', - sortable: true, - columnFilter: null, - format: (val) => dashIfEmpty(val), + headerClass: 'horizontal-separator', + columnFilter: false, + format: (row) => toDateTimeFormat(row.futureShipped), }, - { + align: 'center', label: t('advanceTickets.futureIpt'), + class: 'shrink', name: 'futureIpt', - field: 'futureIpt', - align: 'left', - sortable: true, columnFilter: { - component: VnSelect, - filterParamKey: 'futureIptColFilter', - type: 'select', - filterValue: null, - event: getInputEvents, + component: 'select', attrs: { - options: itemPackingTypesOptions.value, - 'option-value': 'code', - 'option-label': 'description', - dense: true, + url: 'itemPackingTypes', + fields: ['code', 'description'], + where: { isActive: true }, + optionValue: 'code', + optionLabel: 'description', }, }, - format: (val) => dashIfEmpty(val), + headerClass: 'horizontal-separator', + format: (row, dashIfEmpty) => dashIfEmpty(row.futureIpt), }, { label: t('advanceTickets.futureState'), name: 'futureState', align: 'right', - sortable: true, - columnFilter: null, - format: (val) => dashIfEmpty(val), + headerClass: 'horizontal-separator', + class: 'expand', + columnFilter: false, + format: (row, dashIfEmpty) => dashIfEmpty(row.futureState), }, ]); @@ -258,26 +168,51 @@ const moveTicketsFuture = async () => { await axios.post('Tickets/merge', params); notify(t('advanceTickets.moveTicketSuccess'), 'positive'); selectedTickets.value = []; - arrayData.fetch({ append: false }); + vnTableRef.value.reload(); }; -onMounted(async () => { - await arrayData.fetch({ append: false }); -}); + +watch( + () => vnTableRef.value.tableRef?.$el, + ($el) => { + if (!$el) return; + const head = $el.querySelector('thead'); + const firstRow = $el.querySelector('thead > tr'); + + const newRow = document.createElement('tr'); + destinationElRef.value = document.createElement('th'); + originElRef.value = document.createElement('th'); + + newRow.classList.add('bg-header'); + destinationElRef.value.classList.add('text-uppercase', 'color-vn-label'); + originElRef.value.classList.add('text-uppercase', 'color-vn-label'); + + destinationElRef.value.setAttribute('colspan', '7'); + originElRef.value.setAttribute('colspan', '9'); + + originElRef.value.textContent = `${t('advanceTickets.origin')}`; + destinationElRef.value.textContent = `${t('advanceTickets.destination')}`; + + newRow.append(destinationElRef.value, originElRef.value); + head.insertBefore(newRow, firstRow); + }, + { once: true, inmmediate: true }, +); + +watch( + () => vnTableRef.value.params, + () => { + if (originElRef.value && destinationElRef.value) { + destinationElRef.value.textContent = `${t('advanceTickets.origin')}`; + originElRef.value.textContent = `${t('advanceTickets.destination')}`; + } + }, + { deep: true }, +); </script> <template> - <FetchData - url="itemPackingTypes" - :filter="{ - fields: ['code', 'description'], - order: 'description ASC', - where: { isActive: true }, - }" - auto-load - @on-fetch="(data) => (itemPackingTypesOptions = data)" - /> <VnSearchbar - data-key="FutureTickets" + data-key="futureTicket" :label="t('Search ticket')" :info="t('futureTickets.searchInfo')" /> @@ -293,7 +228,7 @@ onMounted(async () => { t(`futureTickets.moveTicketDialogSubtitle`, { selectedTickets: selectedTickets.length, }), - moveTicketsFuture + moveTicketsFuture, ) " > @@ -305,235 +240,135 @@ onMounted(async () => { </VnSubToolbar> <RightMenu> <template #right-panel> - <TicketFutureFilter data-key="FutureTickets" /> + <TicketFutureFilter data-key="futureTickets" /> </template> </RightMenu> <QPage class="column items-center q-pa-md"> - <QTable - :rows="tickets" + <VnTable + data-key="futureTickets" + ref="vnTableRef" + url="Tickets/getTicketsFuture" + search-url="futureTickets" + :user-params="userParams" + :limit="0" :columns="ticketColumns" - row-key="id" - selection="multiple" + :table="{ + 'row-key': '$index', + selection: 'multiple', + }" v-model:selected="selectedTickets" - :pagination="{ rowsPerPage: 0 }" - :no-data-label="t('globals.noResults')" - style="max-width: 99%" + :right-search="false" + auto-load + :disable-option="{ card: true }" > - <template #header="props"> - <QTr> - <QTh class="horizontal-separator" /> - <QTh - class="horizontal-separator text-uppercase color-vn-label" - colspan="8" - translate - > - {{ t('advanceTickets.origin') }} - </QTh> - <QTh - class="horizontal-separator text-uppercase color-vn-label" - colspan="4" - translate - > - {{ t('advanceTickets.destination') }} - </QTh> - </QTr> - <QTr> - <QTh> - <QCheckbox v-model="props.selected" /> - </QTh> - <QTh - v-for="(col, index) in ticketColumns" - :key="index" - :class="{ 'vertical-separator': col.name === 'futureId' }" - > - {{ col.label }} - </QTh> - </QTr> - </template> - <template #top-row="{ cols }"> - <QTr> - <QTd /> - <QTd - v-for="(col, index) in cols" - :key="index" - style="max-width: 100px" - > - <component - :is="col.columnFilter.component" - v-if="col.columnFilter" - v-model="col.columnFilter.filterValue" - v-bind="col.columnFilter.attrs" - v-on="col.columnFilter.event(col)" - dense - /> - </QTd> - </QTr> - </template> - <template #header-cell-availableLines="{ col }"> - <QTh class="vertical-separator"> - {{ col.label }} - </QTh> - </template> - <template #body-cell-problems="{ row }"> - <QTd class="q-gutter-x-xs"> + <template #column-problems="{ row }"> + <span class="q-gutter-x-xs"> <QIcon - v-if="row.isTaxDataChecked === 0" + v-if="row.futureAgencyFk !== row.agencyFk && row.agencyFk" color="primary" - name="vn:no036" + name="vn:agency-term" size="xs" + class="q-mr-xs" > - <QTooltip> - {{ t('futureTickets.noVerified') }} + <QTooltip class="column"> + <span> + {{ + t('advanceTickets.originAgency', { + agency: row.futureAgency, + }) + }} + </span> + <span> + {{ + t('advanceTickets.destinationAgency', { + agency: row.agency, + }) + }} + </span> </QTooltip> </QIcon> - <QIcon - v-if="row.hasTicketRequest" - color="primary" - name="vn:buyrequest" - size="xs" - > - <QTooltip> - {{ t('futureTickets.purchaseRequest') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.itemShortage" - color="primary" - name="vn:unavailable" - size="xs" - > - <QTooltip> - {{ t('ticketSale.noVisible') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.isFreezed" - color="primary" - name="vn:frozen" - size="xs" - > - <QTooltip> - {{ t('futureTickets.clientFrozen') }} - </QTooltip> - </QIcon> - <QIcon v-if="row.risk" color="primary" name="vn:risk" size="xs"> - <QTooltip> - {{ t('futureTickets.risk') }}: {{ row.risk }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.hasComponentLack" - color="primary" - name="vn:components" - size="xs" - > - <QTooltip> - {{ t('futureTickets.componentLack') }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.hasRounding" - color="primary" - name="sync_problem" - size="xs" - > - <QTooltip> - {{ t('futureTickets.rounding') }} - </QTooltip> - </QIcon> - </QTd> + <TicketProblems :row /> + </span> </template> - <template #body-cell-ticketId="{ row }"> - <QTd> - <QBtn flat class="link"> - {{ row.id }} - <TicketDescriptorProxy :id="row.id" /> - </QBtn> - </QTd> + <template #column-id="{ row }"> + <QBtn flat class="link" @click.stop dense> + {{ row.id }} + <TicketDescriptorProxy :id="row.id" /> + </QBtn> </template> - <template #body-cell-shipped="{ row }"> - <QTd class="shipped"> - <QBadge - text-color="black" - :color="getDateQBadgeColor(row.shipped)" - class="q-ma-none" - > - {{ toDateTimeFormat(row.shipped) }} - </QBadge> - </QTd> + <template #column-shipped="{ row }"> + <QBadge + text-color="black" + :color="getDateQBadgeColor(row.shipped)" + class="q-ma-none" + > + {{ toDateTimeFormat(row.shipped) }} + </QBadge> </template> - <template #body-cell-state="{ row }"> - <QTd> - <QBadge - text-color="black" - :color="row.classColor" - class="q-ma-none" - dense - > - {{ row.state }} - </QBadge> - </QTd> + <template #column-state="{ row }"> + <QBadge + v-if="row.state" + text-color="black" + :color="row.classColor" + class="q-ma-none" + dense + > + {{ row.state }} + </QBadge> + <span v-else> {{ dashIfEmpty(row.state) }}</span> </template> - <template #body-cell-import="{ row }"> - <QTd> - <QBadge - :text-color=" - totalPriceColor(row.totalWithVat) === 'warning' - ? 'black' - : 'white' - " - :color="totalPriceColor(row.totalWithVat)" - class="q-ma-none" - dense - > - {{ toCurrency(row.totalWithVat || 0) }} - </QBadge> - </QTd> + <template #column-import="{ row }"> + <QBadge + :text-color=" + totalPriceColor(row.totalWithVat) === 'warning' + ? 'black' + : 'white' + " + :color="totalPriceColor(row.totalWithVat)" + class="q-ma-none" + dense + > + {{ toCurrency(row.totalWithVat || 0) }} + </QBadge> </template> - <template #body-cell-futureId="{ row }"> - <QTd class="vertical-separator"> - <QBtn flat class="link" dense> - {{ row.futureId }} - <TicketDescriptorProxy :id="row.futureId" /> - </QBtn> - </QTd> + <template #column-futureId="{ row }"> + <QBtn flat class="link" @click.stop dense> + {{ row.futureId }} + <TicketDescriptorProxy :id="row.futureId" /> + </QBtn> </template> - <template #body-cell-futureShipped="{ row }"> - <QTd class="shipped"> - <QBadge - text-color="black" - :color="getDateQBadgeColor(row.futureShipped)" - class="q-ma-none" - > - {{ toDateTimeFormat(row.futureShipped) }} - </QBadge> - </QTd> + <template #column-futureShipped="{ row }"> + <QBadge + text-color="black" + :color="getDateQBadgeColor(row.futureShipped)" + class="q-ma-none" + > + {{ toDateTimeFormat(row.futureShipped) }} + </QBadge> </template> - <template #body-cell-futureState="{ row }"> - <QTd> - <QBadge - text-color="black" - :color="row.futureClassColor" - class="q-ma-none" - dense - > - {{ row.futureState }} - </QBadge> - </QTd> + <template #column-futureState="{ row }"> + <QBadge + text-color="black" + :color="row.futureClassColor" + class="q-mr-xs" + dense + > + {{ row.futureState }} + </QBadge> </template> - </QTable> + </VnTable> </QPage> </template> <style scoped lang="scss"> -.shipped { - min-width: 132px; -} -.vertical-separator { +:deep(.vertical-separator) { border-left: 4px solid white !important; } -.horizontal-separator { +:deep(.horizontal-separator) { + border-top: 4px solid white !important; +} +:deep(.horizontal-bottom-separator) { border-bottom: 4px solid white !important; } </style> diff --git a/src/pages/Ticket/TicketFutureFilter.vue b/src/pages/Ticket/TicketFutureFilter.vue index d28b0af71..64e060a39 100644 --- a/src/pages/Ticket/TicketFutureFilter.vue +++ b/src/pages/Ticket/TicketFutureFilter.vue @@ -12,7 +12,7 @@ import axios from 'axios'; import { onMounted } from 'vue'; const { t } = useI18n(); -const props = defineProps({ +defineProps({ dataKey: { type: String, required: true, @@ -58,7 +58,7 @@ onMounted(async () => { auto-load /> <VnFilterPanel - :data-key="props.dataKey" + :data-key :un-removable-params="['warehouseFk', 'originScopeDays ', 'futureScopeDays']" > <template #tags="{ tag, formatFn }"> diff --git a/src/pages/Ticket/locale/en.yml b/src/pages/Ticket/locale/en.yml index f11b32c3a..cdbb22d9b 100644 --- a/src/pages/Ticket/locale/en.yml +++ b/src/pages/Ticket/locale/en.yml @@ -23,6 +23,8 @@ ticketSale: hasComponentLack: Component lack ok: Ok more: More + transferLines: Transfer lines(no basket)/ Split + transferBasket: Some row selected is basket advanceTickets: preparation: Preparation origin: Origin @@ -188,7 +190,6 @@ ticketList: accountPayment: Account payment sendDocuware: Set delivered and send delivery note(s) to the tablet addPayment: Add payment - date: Date company: Company amount: Amount reference: Reference @@ -202,9 +203,89 @@ ticketList: creditCard: Credit card transfers: Transfers province: Province - warehouse: Warehouse - hour: Hour closure: Closure toLines: Go to lines addressNickname: Address nickname ref: Reference + rounding: Rounding + noVerifiedData: No verified data + purchaseRequest: Purchase request + notVisible: Not visible + clientFrozen: Client frozen + componentLack: Component lack +negative: + hour: Hour + id: Id Article + longName: Article + supplier: Supplier + colour: Colour + size: Size + origen: Origin + value: Negative + itemFk: Article + producer: Producer + warehouse: Warehouse + warehouseFk: Warehouse + category: Category + categoryFk: Family + type: Type + typeFk: Type + lack: Negative + inkFk: inkFk + timed: timed + date: Date + minTimed: minTimed + negativeAction: Negative + totalNegative: Total negatives + days: Days + buttonsUpdate: + item: Item + state: State + quantity: Quantity + modalOrigin: + title: Update negatives + question: Select a state to update + modalSplit: + title: Confirm split selected + question: Select a state to update + detail: + saleFk: Sale + itemFk: Article + ticketFk: Ticket + code: Code + nickname: Alias + name: Name + zoneName: Agency name + shipped: Date + theoreticalhour: Theoretical hour + agName: Agency + quantity: Quantity + alertLevelCode: Group state + state: State + peticionCompra: Ticket request + isRookie: Is rookie + turno: Turn line + isBasket: Basket + hasObservation: Has substitution + hasToIgnore: VIP + modal: + changeItem: + title: Update item reference + placeholder: New item + changeState: + title: Update tickets state + placeholder: New state + changeQuantity: + title: Update tickets quantity + placeholder: New quantity + split: + title: Are you sure you want to split selected tickets? + subTitle: Confirm split action + handleSplited: + title: Handle splited tickets + subTitle: Confirm date and agency + split: + ticket: Old ticket + newTicket: New ticket + status: Result + message: Message diff --git a/src/pages/Ticket/locale/es.yml b/src/pages/Ticket/locale/es.yml index 945da8367..75d3c6a2b 100644 --- a/src/pages/Ticket/locale/es.yml +++ b/src/pages/Ticket/locale/es.yml @@ -127,6 +127,8 @@ ticketSale: ok: Ok more: Más address: Consignatario + transferLines: Transferir líneas(no cesta)/ Separar + transferBasket: No disponible para una cesta size: Medida ticketComponents: serie: Serie @@ -213,3 +215,84 @@ ticketList: toLines: Ir a lineas addressNickname: Alias consignatario ref: Referencia +negative: + hour: Hora + id: Id Articulo + longName: Articulo + supplier: Productor + colour: Color + size: Medida + origen: Origen + value: Negativo + warehouseFk: Almacen + producer: Producer + category: Categoría + categoryFk: Familia + typeFk: Familia + warehouse: Almacen + lack: Negativo + inkFk: Color + timed: Hora + date: Fecha + minTimed: Hora + type: Tipo + negativeAction: Negativo + totalNegative: Total negativos + days: Rango de dias + buttonsUpdate: + item: artículo + state: Estado + quantity: Cantidad + modalOrigin: + title: Actualizar negativos + question: Seleccione un estado para guardar + modalSplit: + title: Confirmar acción de split + question: Selecciona un estado + detail: + saleFk: Línea + itemFk: Artículo + ticketFk: Ticket + code: code + nickname: Alias + name: Nombre + zoneName: Agencia + shipped: F. envío + theoreticalhour: Hora teórica + agName: Agencia + quantity: Cantidad + alertLevelCode: Estado agrupado + state: Estado + peticionCompra: Petición compra + isRookie: Cliente nuevo + turno: Linea turno + isBasket: Cesta + hasObservation: Tiene sustitución + hasToIgnore: VIP + modal: + changeItem: + title: Actualizar referencia artículo + placeholder: Nuevo articulo + changeState: + title: Actualizar estado + placeholder: Nuevo estado + changeQuantity: + title: Actualizar cantidad + placeholder: Nueva cantidad + split: + title: ¿Seguro de separar los tickets seleccionados? + subTitle: Confirma separar tickets seleccionados + handleSplited: + title: Gestionar tickets spliteados + subTitle: Confir fecha y agencia + split: + ticket: Ticket viejo + newTicket: Ticket nuevo + status: Estado + message: Mensaje + rounding: Redondeo + noVerifiedData: Sin datos comprobados + purchaseRequest: Petición de compra + notVisible: No visible + clientFrozen: Cliente congelado + componentLack: Faltan componentes diff --git a/src/pages/Travel/Card/TravelBasicData.vue b/src/pages/Travel/Card/TravelBasicData.vue index 4b9aa28ed..b1adc8126 100644 --- a/src/pages/Travel/Card/TravelBasicData.vue +++ b/src/pages/Travel/Card/TravelBasicData.vue @@ -9,6 +9,7 @@ import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; +import VnInputTime from 'components/common/VnInputTime.vue'; const route = useRoute(); const { t } = useI18n(); @@ -53,7 +54,16 @@ const warehousesOptionsIn = ref([]); <VnInputDate v-model="data.shipped" :label="t('globals.shipped')" /> <VnInputDate v-model="data.landed" :label="t('globals.landed')" /> </VnRow> - + <VnRow> + <VnInputDate + v-model="data.availabled" + :label="t('travel.summary.availabled')" + /> + <VnInputTime + v-model="data.availabled" + :label="t('travel.summary.availabledHour')" + /> + </VnRow> <VnRow> <VnSelect :label="t('globals.warehouseOut')" @@ -101,10 +111,3 @@ const warehousesOptionsIn = ref([]); </template> </FormModel> </template> - -<i18n> -es: - raidDays: El travel se desplaza automáticamente cada día para estar desde hoy al número de días indicado. Si se deja vacio no se moverá -en: - raidDays: The travel adjusts itself daily to match the number of days set, starting from today. If left blank, it won’t move -</i18n> diff --git a/src/pages/Travel/Card/TravelCard.vue b/src/pages/Travel/Card/TravelCard.vue index 445675b90..cb09eafd6 100644 --- a/src/pages/Travel/Card/TravelCard.vue +++ b/src/pages/Travel/Card/TravelCard.vue @@ -1,43 +1,13 @@ <script setup> import TravelDescriptor from './TravelDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; - -const userFilter = { - fields: [ - 'id', - 'ref', - 'shipped', - 'landed', - 'totalEntries', - 'warehouseInFk', - 'warehouseOutFk', - 'cargoSupplierFk', - 'agencyModeFk', - 'isRaid', - 'isDelivered', - 'isReceived', - ], - include: [ - { - relation: 'warehouseIn', - scope: { - fields: ['name'], - }, - }, - { - relation: 'warehouseOut', - scope: { - fields: ['name'], - }, - }, - ], -}; +import filter from './TravelFilter.js'; </script> <template> <VnCardBeta data-key="Travel" - base-url="Travels" + url="Travels" :descriptor="TravelDescriptor" - :user-filter="userFilter" + :filter="filter" /> </template> diff --git a/src/pages/Travel/Card/TravelDescriptor.vue b/src/pages/Travel/Card/TravelDescriptor.vue index 72acf91b8..922f89f33 100644 --- a/src/pages/Travel/Card/TravelDescriptor.vue +++ b/src/pages/Travel/Card/TravelDescriptor.vue @@ -32,7 +32,6 @@ const setData = (entity) => (data.value = useCardDescription(entity.ref, entity. <template> <CardDescriptor - module="Travel" :url="`Travels/${entityId}`" :title="data.title" :subtitle="data.subtitle" diff --git a/src/pages/Travel/Card/TravelFilter.js b/src/pages/Travel/Card/TravelFilter.js index f5f4520fd..05436834f 100644 --- a/src/pages/Travel/Card/TravelFilter.js +++ b/src/pages/Travel/Card/TravelFilter.js @@ -11,6 +11,7 @@ export default { 'agencyModeFk', 'isRaid', 'daysInForward', + 'availabled', ], include: [ { diff --git a/src/pages/Travel/Card/TravelSummary.vue b/src/pages/Travel/Card/TravelSummary.vue index 16d42f104..9f9552611 100644 --- a/src/pages/Travel/Card/TravelSummary.vue +++ b/src/pages/Travel/Card/TravelSummary.vue @@ -10,6 +10,8 @@ import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue' import FetchData from 'src/components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import { toDate, toCurrency, toCelsius } from 'src/filters'; +import { toDateTimeFormat } from 'src/filters/date.js'; +import { dashIfEmpty } from 'src/filters'; import axios from 'axios'; import TravelDescriptorMenuItems from './TravelDescriptorMenuItems.vue'; @@ -333,6 +335,12 @@ const getLink = (param) => `#/travel/${entityId.value}/${param}`; <VnLv :label="t('globals.reference')" :value="travel.ref" /> <VnLv label="m³" :value="travel.m3" /> <VnLv :label="t('globals.totalEntries')" :value="travel.totalEntries" /> + <VnLv + :label="t('travel.summary.availabled')" + :value=" + dashIfEmpty(toDateTimeFormat(travel.availabled)) + " + /> </QCard> <QCard class="full-width"> <VnTitle :text="t('travel.summary.entries')" /> diff --git a/src/pages/Travel/Card/TravelThermographs.vue b/src/pages/Travel/Card/TravelThermographs.vue index 2946c8814..2376bd6d2 100644 --- a/src/pages/Travel/Card/TravelThermographs.vue +++ b/src/pages/Travel/Card/TravelThermographs.vue @@ -217,7 +217,7 @@ const removeThermograph = async (id) => { icon="add" color="primary" @click="redirectToThermographForm('create')" - shortcut="+" + v-shortcut="'+'" /> <QTooltip class="text-no-wrap"> {{ t('Add thermograph') }} diff --git a/src/pages/Travel/ExtraCommunityFilter.vue b/src/pages/Travel/ExtraCommunityFilter.vue index 371f06340..29d342334 100644 --- a/src/pages/Travel/ExtraCommunityFilter.vue +++ b/src/pages/Travel/ExtraCommunityFilter.vue @@ -113,7 +113,7 @@ warehouses(); <template #append> <QBtn icon="add" - shortcut="+" + v-shortcut="'+'" flat dense size="12px" diff --git a/src/pages/Travel/TravelList.vue b/src/pages/Travel/TravelList.vue index e90c01be2..b227afcb2 100644 --- a/src/pages/Travel/TravelList.vue +++ b/src/pages/Travel/TravelList.vue @@ -10,6 +10,9 @@ import { getDateQBadgeColor } from 'src/composables/getDateQBadgeColor.js'; import TravelFilter from './TravelFilter.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import VnSection from 'src/components/common/VnSection.vue'; +import VnInputTime from 'src/components/common/VnInputTime.vue'; +import VnInputDate from 'src/components/common/VnInputDate.vue'; +import { toDateTimeFormat } from 'src/filters/date'; const { viewSummary } = useSummaryDialog(); const router = useRouter(); @@ -167,6 +170,17 @@ const columns = computed(() => [ cardVisible: true, create: true, }, + { + align: 'left', + name: 'availabled', + label: t('travel.summary.availabled'), + component: 'input', + columnClass: 'expand', + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(toDateTimeFormat(row.availabled)), + }, { align: 'right', label: '', @@ -269,6 +283,16 @@ const columns = computed(() => [ :class="{ 'is-active': row.isReceived }" /> </template> + <template #more-create-dialog="{ data }"> + <VnInputDate + v-model="data.availabled" + :label="t('travel.summary.availabled')" + /> + <VnInputTime + v-model="data.availabled" + :label="t('travel.summary.availabledHour')" + /> + </template> <template #moreFilterPanel="{ params }"> <VnInputNumber :label="t('params.scopeDays')" diff --git a/src/pages/Wagon/Card/WagonCard.vue b/src/pages/Wagon/Card/WagonCard.vue index ed6c83778..644a30ffa 100644 --- a/src/pages/Wagon/Card/WagonCard.vue +++ b/src/pages/Wagon/Card/WagonCard.vue @@ -2,5 +2,5 @@ import VnCard from 'components/common/VnCard.vue'; </script> <template> - <VnCard data-key="Wagon" base-url="Wagons" /> + <VnCard data-key="Wagon" url="Wagons" /> </template> diff --git a/src/pages/Wagon/Type/WagonTypeList.vue b/src/pages/Wagon/Type/WagonTypeList.vue index c0943c58e..4c0b078a7 100644 --- a/src/pages/Wagon/Type/WagonTypeList.vue +++ b/src/pages/Wagon/Type/WagonTypeList.vue @@ -96,7 +96,13 @@ async function remove(row) { > </VnTable> <QPageSticky :offset="[18, 18]"> - <QBtn @click.stop="dialog.show()" color="primary" fab icon="add" shortcut="+"> + <QBtn + @click.stop="dialog.show()" + color="primary" + fab + icon="add" + v-shortcut="'+'" + > <QDialog ref="dialog"> <FormModelPopup :title="t('Create new Wagon type')" diff --git a/src/pages/Worker/Card/WorkerBasicData.vue b/src/pages/Worker/Card/WorkerBasicData.vue index 6a13e3f39..fcf0f0369 100644 --- a/src/pages/Worker/Card/WorkerBasicData.vue +++ b/src/pages/Worker/Card/WorkerBasicData.vue @@ -1,6 +1,5 @@ <script setup> -import { ref, onBeforeMount } from 'vue'; -import { useRoute } from 'vue-router'; +import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import FetchData from 'components/FetchData.vue'; @@ -11,18 +10,13 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import { useAdvancedSummary } from 'src/composables/useAdvancedSummary'; const { t } = useI18n(); +const form = ref(); const educationLevels = ref([]); const countries = ref([]); const maritalStatus = [ { code: 'M', name: t('Married') }, { code: 'S', name: t('Single') }, ]; -const advancedSummary = ref({}); - -onBeforeMount(async () => { - advancedSummary.value = - (await useAdvancedSummary('Workers', +useRoute().params.id)) ?? {}; -}); </script> <template> <FetchData @@ -38,14 +32,15 @@ onBeforeMount(async () => { auto-load /> <FormModel - :filter="{ where: { id: +$route.params.id } }" - url="Workers/summary" + ref="form" :url-update="`Workers/${$route.params.id}`" auto-load model="Worker" @on-fetch=" async (data) => { - Object.assign(data, advancedSummary); + Object.assign(data, (await useAdvancedSummary('Workers', data.id)) ?? {}); + await $nextTick(); + if (form) form.hasChanges = false; } " > diff --git a/src/pages/Worker/Card/WorkerCalendar.vue b/src/pages/Worker/Card/WorkerCalendar.vue index 5ca95a1a4..df4616011 100644 --- a/src/pages/Worker/Card/WorkerCalendar.vue +++ b/src/pages/Worker/Card/WorkerCalendar.vue @@ -1,7 +1,8 @@ <script setup> -import { nextTick, ref, watch } from 'vue'; +import { nextTick, ref, watch, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; +import { useAcl } from 'src/composables/useAcl'; import WorkerCalendarFilter from 'pages/Worker/Card/WorkerCalendarFilter.vue'; import FetchData from 'components/FetchData.vue'; @@ -9,10 +10,17 @@ import WorkerCalendarItem from 'pages/Worker/Card/WorkerCalendarItem.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import axios from 'axios'; +import VnNotes from 'src/components/ui/VnNotes.vue'; +import { useStateStore } from 'src/stores/useStateStore'; +const stateStore = useStateStore(); const router = useRouter(); const route = useRoute(); const { t } = useI18n(); +const acl = useAcl(); +const canSeeNotes = computed(() => + acl.hasAny([{ model: 'Worker', props: '__get__business', accessType: 'READ' }]), +); const workerIsFreelance = ref(); const WorkerFreelanceRef = ref(); const workerCalendarFilterRef = ref(null); @@ -26,6 +34,10 @@ const contractHolidays = ref(null); const yearHolidays = ref(null); const eventsMap = ref({}); const festiveEventsMap = ref({}); +const saveUrl = ref(); +const body = { + workerFk: route.params.id, +}; const onFetchActiveContract = (data) => { if (!data) return; @@ -67,7 +79,7 @@ const onFetchAbsences = (data) => { name: holidayName, isFestive: true, }, - true + true, ); }); } @@ -146,7 +158,7 @@ watch( async () => { await nextTick(); await activeContractRef.value.fetch(); - } + }, ); watch([year, businessFk], () => refreshData()); </script> @@ -181,6 +193,20 @@ watch([year, businessFk], () => refreshData()); /> </template> </RightMenu> + <Teleport to="#st-data" v-if="stateStore.isSubToolbarShown() && canSeeNotes"> + <VnNotes + :just-input="true" + :url="`Workers/${route.params.id}/business`" + :filter="{ fields: ['id', 'notes', 'workerFk'] }" + :save-url="saveUrl" + @on-fetch=" + (data) => { + saveUrl = `Businesses/${data.id}`; + } + " + :body="body" + /> + </Teleport> <QPage class="column items-center"> <QCard v-if="workerIsFreelance"> <QCardSection class="text-center"> diff --git a/src/pages/Worker/Card/WorkerCalendarFilter.vue b/src/pages/Worker/Card/WorkerCalendarFilter.vue index 67b7df907..48fc4094b 100644 --- a/src/pages/Worker/Card/WorkerCalendarFilter.vue +++ b/src/pages/Worker/Card/WorkerCalendarFilter.vue @@ -180,8 +180,6 @@ const yearList = ref(generateYears()); :is-clearable="false" /> </QItemSection> - </QItem> - <QItem> <QItemSection> <VnSelect :label="t('Contract')" diff --git a/src/pages/Worker/Card/WorkerCard.vue b/src/pages/Worker/Card/WorkerCard.vue index 1ada15a33..3b7a62025 100644 --- a/src/pages/Worker/Card/WorkerCard.vue +++ b/src/pages/Worker/Card/WorkerCard.vue @@ -3,5 +3,10 @@ import WorkerDescriptor from './WorkerDescriptor.vue'; import VnCardBeta from 'src/components/common/VnCardBeta.vue'; </script> <template> - <VnCardBeta data-key="Worker" custom-url="Workers/summary" :descriptor="WorkerDescriptor" /> + <VnCardBeta + data-key="Worker" + url="Workers/summary" + :id-in-where="true" + :descriptor="WorkerDescriptor" + /> </template> diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index d87fd4a54..de3f634e2 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -10,7 +10,7 @@ import axios from 'axios'; import VnImg from 'src/components/ui/VnImg.vue'; import EditPictureForm from 'components/EditPictureForm.vue'; import WorkerDescriptorMenu from './WorkerDescriptorMenu.vue'; -import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; const $props = defineProps({ id: { @@ -21,7 +21,7 @@ const $props = defineProps({ dataKey: { type: String, required: false, - default: 'workerData', + default: 'Worker', }, }); const image = ref(null); @@ -50,9 +50,8 @@ const handlePhotoUpdated = (evt = false) => { <template> <CardDescriptor ref="cardDescriptorRef" - module="Worker" :data-key="dataKey" - url="Workers/descriptor" + url="Workers/summary" :filter="{ where: { id: entityId } }" title="user.nickname" @on-fetch="getIsExcluded" @@ -152,7 +151,7 @@ const handlePhotoUpdated = (evt = false) => { <QBtn :to="{ name: 'AccountCard', - params: { id: entity.user.id }, + params: { id: entity.user?.id }, }" size="md" icon="face" diff --git a/src/pages/Worker/Card/WorkerDescriptorProxy.vue b/src/pages/Worker/Card/WorkerDescriptorProxy.vue index 43deb7821..a142570f9 100644 --- a/src/pages/Worker/Card/WorkerDescriptorProxy.vue +++ b/src/pages/Worker/Card/WorkerDescriptorProxy.vue @@ -12,11 +12,6 @@ const $props = defineProps({ <template> <QPopupProxy> - <WorkerDescriptor - v-if="$props.id" - :id="$props.id" - :summary="WorkerSummary" - data-key="workerDescriptorProxy" - /> + <WorkerDescriptor v-if="$props.id" :id="$props.id" :summary="WorkerSummary" /> </QPopupProxy> </template> diff --git a/src/pages/Worker/Card/WorkerFormation.vue b/src/pages/Worker/Card/WorkerFormation.vue index 6fd5a4eae..e8680f7dd 100644 --- a/src/pages/Worker/Card/WorkerFormation.vue +++ b/src/pages/Worker/Card/WorkerFormation.vue @@ -94,6 +94,7 @@ const columns = computed(() => [ align: 'left', name: 'hasDiploma', label: t('worker.formation.tableVisibleColumns.hasDiploma'), + component: 'checkbox', create: true, }, { @@ -118,7 +119,7 @@ const columns = computed(() => [ :url="`Workers/${entityId}/trainingCourse`" :url-create="`Workers/${entityId}/trainingCourse`" save-url="TrainingCourses/crud" - :filter="courseFilter" + :user-filter="courseFilter" :create="{ urlCreate: 'trainingCourses', title: t('Create training course'), diff --git a/src/pages/Worker/Card/WorkerMedical.vue b/src/pages/Worker/Card/WorkerMedical.vue index c220df76a..c04f6496b 100644 --- a/src/pages/Worker/Card/WorkerMedical.vue +++ b/src/pages/Worker/Card/WorkerMedical.vue @@ -3,11 +3,23 @@ import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import VnTable from 'components/VnTable/VnTable.vue'; +import { dashIfEmpty } from 'src/filters'; const tableRef = ref(); const { t } = useI18n(); const route = useRoute(); const entityId = computed(() => route.params.id); +const centerFilter = { + include: [ + { + relation: 'center', + scope: { + fields: ['id', 'name'], + }, + }, + ], +}; + const columns = [ { align: 'left', @@ -36,6 +48,9 @@ const columns = [ url: 'medicalCenters', fields: ['id', 'name'], }, + format: (row, dashIfEmpty) => { + return dashIfEmpty(row.center?.name); + }, }, { align: 'left', @@ -84,6 +99,7 @@ const columns = [ ref="tableRef" data-key="WorkerMedical" :url="`Workers/${entityId}/medicalReview`" + :user-filter="centerFilter" save-url="MedicalReviews/crud" :create="{ urlCreate: 'medicalReviews', diff --git a/src/pages/Worker/Card/WorkerOperator.vue b/src/pages/Worker/Card/WorkerOperator.vue index cdacc72c0..6faeefe67 100644 --- a/src/pages/Worker/Card/WorkerOperator.vue +++ b/src/pages/Worker/Card/WorkerOperator.vue @@ -1,7 +1,7 @@ <script setup> import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import { ref, computed } from 'vue'; +import { ref, computed, watch } from 'vue'; import FetchData from 'components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; @@ -19,6 +19,7 @@ const trainsData = ref([]); const machinesData = ref([]); const route = useRoute(); const routeId = computed(() => route.params.id); +const selected = ref([]); const initialData = computed(() => { return { @@ -41,6 +42,21 @@ async function insert() { await axios.post('Operators', initialData.value); crudModelRef.value.reload(); } + +watch( + () => crudModelRef.value?.formData, + (formData) => { + if (formData && formData.length) { + if (JSON.stringify(selected.value) !== JSON.stringify(formData)) { + selected.value = formData; + } + } else if (selected.value.length > 0) { + selected.value = []; + } + }, + { immediate: true, deep: true } +); + </script> <template> @@ -67,6 +83,7 @@ async function insert() { :data-required="{ workerFk: route.params.id }" ref="crudModelRef" search-url="operator" + :selected="selected" auto-load > <template #body="{ rows }"> diff --git a/src/pages/Worker/Card/WorkerPda.vue b/src/pages/Worker/Card/WorkerPda.vue index f6cb92aac..47e13cf6d 100644 --- a/src/pages/Worker/Card/WorkerPda.vue +++ b/src/pages/Worker/Card/WorkerPda.vue @@ -101,7 +101,7 @@ function reloadData() { openConfirmationModal( t(`Remove PDA`), t('Do you want to remove this PDA?'), - () => deallocatePDA(row.deviceProductionFk) + () => deallocatePDA(row.deviceProductionFk), ) " > @@ -114,7 +114,13 @@ function reloadData() { </template> </VnPaginate> <QPageSticky :offset="[18, 18]"> - <QBtn @click.stop="dialog.show()" color="primary" fab icon="add" shortcut="+"> + <QBtn + @click.stop="dialog.show()" + color="primary" + fab + icon="add" + v-shortcut="'+'" + > <QDialog ref="dialog"> <FormModelPopup :title="t('Add new device')" diff --git a/src/pages/Worker/Card/WorkerPit.vue b/src/pages/Worker/Card/WorkerPit.vue index d9ac1a02c..3de60d6a0 100644 --- a/src/pages/Worker/Card/WorkerPit.vue +++ b/src/pages/Worker/Card/WorkerPit.vue @@ -222,7 +222,7 @@ const deleteRelative = async (id) => { color="primary" flat icon="add" - shortcut="+" + v-shortcut="'+'" style="flex: 0" data-cy="addRelative" /> diff --git a/src/pages/Worker/Card/WorkerSummary.vue b/src/pages/Worker/Card/WorkerSummary.vue index 992f6ec71..78c5dfd82 100644 --- a/src/pages/Worker/Card/WorkerSummary.vue +++ b/src/pages/Worker/Card/WorkerSummary.vue @@ -9,7 +9,7 @@ import CardSummary from 'components/ui/CardSummary.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnTitle from 'src/components/common/VnTitle.vue'; import RoleDescriptorProxy from 'src/pages/Account/Role/Card/RoleDescriptorProxy.vue'; -import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; import { useAdvancedSummary } from 'src/composables/useAdvancedSummary'; import WorkerDescriptorMenu from './WorkerDescriptorMenu.vue'; diff --git a/src/pages/Worker/Card/WorkerTimeControl.vue b/src/pages/Worker/Card/WorkerTimeControl.vue index c580e5202..7def6e94c 100644 --- a/src/pages/Worker/Card/WorkerTimeControl.vue +++ b/src/pages/Worker/Card/WorkerTimeControl.vue @@ -64,17 +64,17 @@ const selectedCalendarDates = ref([]); // Date formateada para bindear al componente QDate const selectedDateFormatted = ref(toDateString(defaultDate.value)); -const arrayData = useArrayData('workerData'); +const arrayData = useArrayData('Worker'); const acl = useAcl(); const selectedDateYear = computed(() => moment(selectedDate.value).isoWeekYear()); const worker = computed(() => arrayData.store?.data); const canSend = computed(() => - acl.hasAny([{ model: 'WorkerTimeControl', props: 'sendMail', accessType: 'WRITE' }]) + acl.hasAny([{ model: 'WorkerTimeControl', props: 'sendMail', accessType: 'WRITE' }]), ); const canUpdate = computed(() => acl.hasAny([ { model: 'WorkerTimeControl', props: 'updateMailState', accessType: 'WRITE' }, - ]) + ]), ); const isHimself = computed(() => user.value.id === Number(route.params.id)); @@ -100,7 +100,7 @@ const getHeaderFormattedDate = (date) => { }; const formattedWeekTotalHours = computed(() => - secondsToHoursMinutes(weekTotalHours.value) + secondsToHoursMinutes(weekTotalHours.value), ); const onInputChange = async (date) => { @@ -320,7 +320,7 @@ const getFinishTime = () => { today.setHours(0, 0, 0, 0); let todayInWeek = weekDays.value.find( - (day) => day.dated.getTime() === today.getTime() + (day) => day.dated.getTime() === today.getTime(), ); if (todayInWeek && todayInWeek.hours && todayInWeek.hours.length) { @@ -472,7 +472,7 @@ onMounted(async () => { openConfirmationModal( t('Send time control email'), t('Are you sure you want to send it?'), - resendEmail + resendEmail, ) " > @@ -561,7 +561,7 @@ onMounted(async () => { @show-worker-time-form=" showWorkerTimeForm( { id: hour.id, entryCode: hour.direction }, - 'edit' + 'edit', ) " class="hour-chip" @@ -577,7 +577,7 @@ onMounted(async () => { </span> <QBtn icon="add_circle" - shortcut="+" + v-shortcut="'+'" flat color="primary" class="fill-icon cursor-pointer" diff --git a/src/pages/Department/Card/DepartmentBasicData.vue b/src/pages/Worker/Department/Card/DepartmentBasicData.vue similarity index 73% rename from src/pages/Department/Card/DepartmentBasicData.vue rename to src/pages/Worker/Department/Card/DepartmentBasicData.vue index b13aed2d3..66210be7b 100644 --- a/src/pages/Department/Card/DepartmentBasicData.vue +++ b/src/pages/Worker/Department/Card/DepartmentBasicData.vue @@ -1,27 +1,16 @@ <script setup> -import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; - import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; - -const route = useRoute(); -const { t } = useI18n(); </script> <template> - <FormModel - :url="`Departments/${route.params.id}`" - model="department" - auto-load - class="full-width" - > + <FormModel model="Department" auto-load class="full-width"> <template #form="{ data, validate }"> <VnRow> <VnInput - :label="t('globals.name')" + :label="$t('globals.name')" v-model="data.name" :rules="validate('globals.name')" clearable @@ -29,33 +18,33 @@ const { t } = useI18n(); /> <VnInput v-model="data.code" - :label="t('globals.code')" + :label="$t('globals.code')" :rules="validate('globals.code')" clearable /> </VnRow> <VnRow> <VnInput - :label="t('department.chat')" + :label="$t('department.chat')" v-model="data.chatName" :rules="validate('department.chat')" clearable /> <VnInput v-model="data.notificationEmail" - :label="t('globals.params.email')" + :label="$t('globals.params.email')" :rules="validate('globals.params.email')" clearable /> </VnRow> <VnRow> <VnSelectWorker - :label="t('department.bossDepartment')" + :label="$t('department.bossDepartment')" v-model="data.workerFk" :rules="validate('department.bossDepartment')" /> <VnSelect - :label="t('department.selfConsumptionCustomer')" + :label="$t('department.selfConsumptionCustomer')" v-model="data.clientFk" url="Clients" option-value="id" @@ -67,11 +56,11 @@ const { t } = useI18n(); </VnRow> <VnRow> <QCheckbox - :label="t('department.telework')" + :label="$t('department.telework')" v-model="data.isTeleworking" /> <QCheckbox - :label="t('department.notifyOnErrors')" + :label="$t('department.notifyOnErrors')" v-model="data.hasToMistake" :false-value="0" :true-value="1" @@ -79,17 +68,17 @@ const { t } = useI18n(); </VnRow> <VnRow> <QCheckbox - :label="t('department.worksInProduction')" + :label="$t('department.worksInProduction')" v-model="data.isProduction" /> <QCheckbox - :label="t('department.hasToRefill')" + :label="$t('department.hasToRefill')" v-model="data.hasToRefill" /> </VnRow> <VnRow> <QCheckbox - :label="t('department.hasToSendMail')" + :label="$t('department.hasToSendMail')" v-model="data.hasToSendMail" /> </VnRow> diff --git a/src/pages/Department/Card/DepartmentCard.vue b/src/pages/Worker/Department/Card/DepartmentCard.vue similarity index 70% rename from src/pages/Department/Card/DepartmentCard.vue rename to src/pages/Worker/Department/Card/DepartmentCard.vue index 4b9fe419c..2e3f11521 100644 --- a/src/pages/Department/Card/DepartmentCard.vue +++ b/src/pages/Worker/Department/Card/DepartmentCard.vue @@ -1,13 +1,13 @@ <script setup> import VnCardBeta from 'components/common/VnCardBeta.vue'; -import DepartmentDescriptor from 'pages/Department/Card/DepartmentDescriptor.vue'; +import DepartmentDescriptor from 'pages/Worker/Department/Card/DepartmentDescriptor.vue'; </script> <template> <VnCardBeta class="q-pa-md column items-center" v-bind="{ ...$attrs }" data-key="Department" - base-url="Departments" + url="Departments" :descriptor="DepartmentDescriptor" /> </template> diff --git a/src/pages/Department/Card/DepartmentDescriptor.vue b/src/pages/Worker/Department/Card/DepartmentDescriptor.vue similarity index 84% rename from src/pages/Department/Card/DepartmentDescriptor.vue rename to src/pages/Worker/Department/Card/DepartmentDescriptor.vue index b219ccfe1..4b7dfd9b8 100644 --- a/src/pages/Department/Card/DepartmentDescriptor.vue +++ b/src/pages/Worker/Department/Card/DepartmentDescriptor.vue @@ -5,7 +5,6 @@ import { useI18n } from 'vue-i18n'; import { useVnConfirm } from 'composables/useVnConfirm'; import VnLv from 'src/components/ui/VnLv.vue'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; @@ -32,15 +31,6 @@ const entityId = computed(() => { return $props.id || route.params.id; }); -const department = ref(); - -const data = ref(useCardDescription()); - -const setData = (entity) => { - if (!entity) return; - data.value = useCardDescription(entity.name, entity.id); -}; - const removeDepartment = async () => { await axios.post(`/Departments/${entityId.value}/removeChild`, entityId.value); router.push({ name: 'WorkerDepartment' }); @@ -52,19 +42,10 @@ const { openConfirmationModal } = useVnConfirm(); <template> <CardDescriptor ref="DepartmentDescriptorRef" - module="Department" :url="`Departments/${entityId}`" - :title="data.title" - :subtitle="data.subtitle" :summary="$props.summary" :to-module="{ name: 'WorkerDepartment' }" - @on-fetch=" - (data) => { - department = data; - setData(data); - } - " - data-key="department" + data-key="Department" > <template #menu="{}"> <QItem @@ -74,7 +55,7 @@ const { openConfirmationModal } = useVnConfirm(); openConfirmationModal( t('Are you sure you want to delete it?'), t('Delete department'), - removeDepartment + removeDepartment, ) " > diff --git a/src/pages/Department/Card/DepartmentDescriptorProxy.vue b/src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue similarity index 100% rename from src/pages/Department/Card/DepartmentDescriptorProxy.vue rename to src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue diff --git a/src/pages/Department/Card/DepartmentSummary.vue b/src/pages/Worker/Department/Card/DepartmentSummary.vue similarity index 99% rename from src/pages/Department/Card/DepartmentSummary.vue rename to src/pages/Worker/Department/Card/DepartmentSummary.vue index 3d481601f..3719137e4 100644 --- a/src/pages/Department/Card/DepartmentSummary.vue +++ b/src/pages/Worker/Department/Card/DepartmentSummary.vue @@ -27,7 +27,7 @@ onMounted(async () => { <template> <CardSummary - data-key="DepartmentSummary" + data-key="Department" ref="summary" :url="`Departments/${entityId}`" class="full-width" diff --git a/src/pages/Department/Card/DepartmentSummaryDialog.vue b/src/pages/Worker/Department/Card/DepartmentSummaryDialog.vue similarity index 100% rename from src/pages/Department/Card/DepartmentSummaryDialog.vue rename to src/pages/Worker/Department/Card/DepartmentSummaryDialog.vue diff --git a/src/pages/Worker/WorkerDepartmentTree.vue b/src/pages/Worker/WorkerDepartmentTree.vue index 9abf4e312..9baf5ee57 100644 --- a/src/pages/Worker/WorkerDepartmentTree.vue +++ b/src/pages/Worker/WorkerDepartmentTree.vue @@ -3,7 +3,7 @@ import { onMounted, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useState } from 'src/composables/useState'; import { useQuasar } from 'quasar'; -import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; +import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; import CreateDepartmentChild from './CreateDepartmentChild.vue'; import axios from 'axios'; import { useRouter } from 'vue-router'; @@ -173,7 +173,7 @@ function handleEvent(type, event, node) { color="primary" flat icon="add" - shortcut="+" + v-shortcut="'+'" class="cursor-pointer" @click.stop="showCreateNodeForm(node.id)" > diff --git a/src/pages/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue index cbeeff2e9..03013f011 100644 --- a/src/pages/Zone/Card/ZoneBasicData.vue +++ b/src/pages/Zone/Card/ZoneBasicData.vue @@ -1,5 +1,7 @@ <script setup> import { useI18n } from 'vue-i18n'; +import { ref } from 'vue'; +import FetchData from 'components/FetchData.vue'; import FormModel from 'src/components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; @@ -7,10 +9,23 @@ import VnInputTime from 'src/components/common/VnInputTime.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; const { t } = useI18n(); +const validAddresses = ref([]); +const addresses = ref([]); + +const setFilteredAddresses = (data) => { + const validIds = new Set(validAddresses.value.map((item) => item.addressFk)); + addresses.value = data.filter((address) => validIds.has(address.id)); +}; </script> <template> - <FormModel :url="`Zones/${$route.params.id}`" auto-load model="zone"> + <FetchData + url="RoadmapAddresses" + auto-load + @on-fetch="(data) => (validAddresses = data)" + /> + <FetchData url="Addresses" auto-load @on-fetch="setFilteredAddresses" /> + <FormModel auto-load model="Zone"> <template #form="{ data, validate }"> <VnRow> <VnInput @@ -18,15 +33,15 @@ const { t } = useI18n(); :label="t('Name')" clearable v-model="data.name" + :required="true" /> </VnRow> - <VnRow> <VnSelect v-model="data.agencyModeFk" :rules="validate('zone.agencyModeFk')" - url="AgencyModes/isActive" - :fields="['id', 'name']" + url="AgencyModes/isActive" + :fields="['id', 'name']" :label="t('Agency')" emit-value map-options @@ -69,7 +84,7 @@ const { t } = useI18n(); type="number" min="0" /> - <VnInputTime v-model="data.hour" :label="t('Closing')" /> + <VnInputTime v-model="data.hour" :label="t('Closing')" :required="true" /> </VnRow> <VnRow> @@ -78,7 +93,7 @@ const { t } = useI18n(); :label="t('Price')" type="number" min="0" - required="true" + :required="true" clearable /> <VnInput @@ -86,7 +101,7 @@ const { t } = useI18n(); :label="t('Price optimum')" type="number" min="0" - required="true" + :required="true" clearable /> </VnRow> @@ -103,12 +118,14 @@ const { t } = useI18n(); v-model="data.addressFk" option-value="id" option-label="nickname" - url="Addresses" + :options="addresses" :fields="['id', 'nickname']" sort-by="id" hide-selected map-options :rules="validate('data.addressFk')" + :filter-options="['id']" + :where="filterWhere" /> </VnRow> <VnRow> diff --git a/src/pages/Zone/Card/ZoneCard.vue b/src/pages/Zone/Card/ZoneCard.vue index a470cd5bd..41daff5c0 100644 --- a/src/pages/Zone/Card/ZoneCard.vue +++ b/src/pages/Zone/Card/ZoneCard.vue @@ -1,13 +1,12 @@ <script setup> -import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import { computed } from 'vue'; import VnCard from 'components/common/VnCard.vue'; import ZoneDescriptor from './ZoneDescriptor.vue'; import ZoneFilterPanel from '../ZoneFilterPanel.vue'; +import filter from './ZoneFilter.js'; -const { t } = useI18n(); const route = useRoute(); const routeName = computed(() => route.name); @@ -19,15 +18,16 @@ function notIsLocations(ifIsFalse, ifIsTrue) { <template> <VnCard - data-key="zone" - :base-url="notIsLocations('Zones', undefined)" + data-key="Zone" + :url="notIsLocations('Zones', undefined)" :descriptor="ZoneDescriptor" + :filter="filter" :filter-panel="notIsLocations(ZoneFilterPanel, undefined)" :search-data-key="notIsLocations('ZoneList', undefined)" :searchbar-props="{ url: notIsLocations('Zones', 'ZoneLocations'), - label: notIsLocations(t('list.searchZone'), t('list.searchLocation')), - info: t('list.searchInfo'), + label: notIsLocations($t('list.searchZone'), $t('list.searchLocation')), + info: $t('list.searchInfo'), whereFilter: notIsLocations((value) => { return /^\d+$/.test(value) ? { id: value } diff --git a/src/pages/Zone/Card/ZoneDescriptor.vue b/src/pages/Zone/Card/ZoneDescriptor.vue index 8355c219e..27676212e 100644 --- a/src/pages/Zone/Card/ZoneDescriptor.vue +++ b/src/pages/Zone/Card/ZoneDescriptor.vue @@ -1,15 +1,14 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import { toTimeFormat } from 'src/filters/date'; import { toCurrency } from 'filters/index'; -import useCardDescription from 'src/composables/useCardDescription'; import ZoneDescriptorMenuItems from './ZoneDescriptorMenuItems.vue'; +import filter from './ZoneFilter.js'; const $props = defineProps({ id: { @@ -20,49 +19,22 @@ const $props = defineProps({ }); const route = useRoute(); -const { t } = useI18n(); - -const filter = { - include: [ - { - relation: 'agencyMode', - scope: { - fields: ['name', 'id'], - }, - }, - ], -}; - const entityId = computed(() => { return $props.id || route.params.id; }); - -const data = ref(useCardDescription()); -const setData = (entity) => { - data.value = useCardDescription(entity.ref, entity.id); -}; </script> <template> - <CardDescriptor - module="Zone" - :url="`Zones/${entityId}`" - :title="data.title" - :subtitle="data.subtitle" - :filter="filter" - @on-fetch="setData" - data-key="zoneData" - > + <CardDescriptor :url="`Zones/${entityId}`" :filter="filter" data-key="Zone"> <template #menu="{ entity }"> <ZoneDescriptorMenuItems :zone="entity" /> </template> <template #body="{ entity }"> - <VnLv :label="t('list.agency')" :value="entity.agencyMode.name" /> - <VnLv :label="t('zone.closing')" :value="toTimeFormat(entity.hour)" /> - <VnLv :label="t('zone.travelingDays')" :value="entity.travelingDays" /> - <VnLv :label="t('list.price')" :value="toCurrency(entity.price)" /> - <VnLv :label="t('zone.bonus')" :value="toCurrency(entity.bonus)" /> + <VnLv :label="$t('list.agency')" :value="entity.agencyMode?.name" /> + <VnLv :label="$t('zone.closing')" :value="toTimeFormat(entity.hour)" /> + <VnLv :label="$t('zone.travelingDays')" :value="entity.travelingDays" /> + <VnLv :label="$t('list.price')" :value="toCurrency(entity.price)" /> + <VnLv :label="$t('zone.bonus')" :value="toCurrency(entity.bonus)" /> </template> </CardDescriptor> </template> - diff --git a/src/pages/Zone/Card/ZoneEvents.vue b/src/pages/Zone/Card/ZoneEvents.vue index a5806bab9..1e6debd25 100644 --- a/src/pages/Zone/Card/ZoneEvents.vue +++ b/src/pages/Zone/Card/ZoneEvents.vue @@ -78,13 +78,13 @@ const onZoneEventFormClose = () => { { isNewMode: true, }, - true + true, ) " color="primary" fab icon="add" - shortcut="+" + v-shortcut="'+'" /> <QTooltip class="text-no-wrap"> {{ t('eventsInclusionForm.addEvent') }} diff --git a/src/pages/Zone/Card/ZoneFilter.js b/src/pages/Zone/Card/ZoneFilter.js new file mode 100644 index 000000000..3298c7c8a --- /dev/null +++ b/src/pages/Zone/Card/ZoneFilter.js @@ -0,0 +1,10 @@ +export default { + include: [ + { + relation: 'agencyMode', + scope: { + fields: ['name', 'id'], + }, + }, + ], +}; diff --git a/src/pages/Zone/Card/ZoneSearchbar.vue b/src/pages/Zone/Card/ZoneSearchbar.vue index f7a59e97f..d1188a1e8 100644 --- a/src/pages/Zone/Card/ZoneSearchbar.vue +++ b/src/pages/Zone/Card/ZoneSearchbar.vue @@ -22,15 +22,50 @@ const exprBuilder = (param, value) => { return /^\d+$/.test(value) ? { id: value } : { name: { like: `%${value}%` } }; } }; + +const tableFilter = { + include: [ + { + relation: 'agencyMode', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'address', + scope: { + fields: ['id', 'nickname', 'provinceFk', 'postalCode'], + include: [ + { + relation: 'province', + scope: { + fields: ['id', 'name'], + }, + }, + { + relation: 'postcode', + scope: { + fields: ['code', 'townFk'], + include: { + relation: 'town', + scope: { + fields: ['id', 'name'], + }, + }, + }, + }, + ], + }, + }, + ], +}; </script> <template> <VnSearchbar data-key="ZonesList" url="Zones" - :filter="{ - include: { relation: 'agencyMode', scope: { fields: ['name'] } }, - }" + :filter="tableFilter" :expr-builder="exprBuilder" :label="t('list.searchZone')" :info="t('list.searchInfo')" diff --git a/src/pages/Zone/Card/ZoneSummary.vue b/src/pages/Zone/Card/ZoneSummary.vue index 124802633..5b29b495b 100644 --- a/src/pages/Zone/Card/ZoneSummary.vue +++ b/src/pages/Zone/Card/ZoneSummary.vue @@ -11,6 +11,7 @@ import { getUrl } from 'src/composables/getUrl'; import { toCurrency } from 'filters/index'; import { toTimeFormat } from 'src/filters/date'; import axios from 'axios'; +import filter from './ZoneFilter.js'; import ZoneDescriptorMenuItems from './ZoneDescriptorMenuItems.vue'; const route = useRoute(); @@ -26,19 +27,6 @@ const $props = defineProps({ const entityId = computed(() => $props.id || route.params.id); const zoneUrl = ref(); -const filter = computed(() => { - const filter = { - include: { - relation: 'agencyMode', - fields: ['name'], - }, - where: { - id: entityId, - }, - }; - return filter; -}); - const columns = computed(() => [ { label: t('list.name'), @@ -72,9 +60,9 @@ onMounted(async () => { <template> <CardSummary - data-key="ZoneSummary" + data-key="Zone" ref="summary" - url="Zones/findOne" + :url="`Zones/${entityId}`" :filter="filter" > <template #header="{ entity }"> diff --git a/src/pages/Zone/Card/ZoneWarehouses.vue b/src/pages/Zone/Card/ZoneWarehouses.vue index c96735697..165e9c840 100644 --- a/src/pages/Zone/Card/ZoneWarehouses.vue +++ b/src/pages/Zone/Card/ZoneWarehouses.vue @@ -109,7 +109,7 @@ const openCreateWarehouseForm = () => createWarehouseDialogRef.value.show(); icon="add" color="primary" @click="openCreateWarehouseForm()" - shortcut="+" + v-shortcut="'+'" > <QTooltip>{{ t('warehouses.add') }}</QTooltip> </QBtn> diff --git a/src/pages/Zone/Delivery/ZoneDeliveryList.vue b/src/pages/Zone/Delivery/ZoneDeliveryList.vue index 975cbdb67..e3ec8cb2d 100644 --- a/src/pages/Zone/Delivery/ZoneDeliveryList.vue +++ b/src/pages/Zone/Delivery/ZoneDeliveryList.vue @@ -74,7 +74,7 @@ async function remove(row) { </VnPaginate> </div> <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn @click="create" fab icon="add" shortcut="+" color="primary" /> + <QBtn @click="create" fab icon="add" v-shortcut="'+'" color="primary" /> </QPageSticky> </QPage> </template> diff --git a/src/pages/Zone/Upcoming/ZoneUpcomingList.vue b/src/pages/Zone/Upcoming/ZoneUpcomingList.vue index 5a7f0bb4c..7b5c2ddbc 100644 --- a/src/pages/Zone/Upcoming/ZoneUpcomingList.vue +++ b/src/pages/Zone/Upcoming/ZoneUpcomingList.vue @@ -74,7 +74,7 @@ async function remove(row) { </VnPaginate> </div> <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn @click="create" fab icon="add" shortcut="+" color="primary" /> + <QBtn @click="create" fab icon="add" v-shortcut="'+'" color="primary" /> </QPageSticky> </QPage> </template> diff --git a/src/router/modules/account/aliasCard.js b/src/router/modules/account/aliasCard.js index cbbd31e51..a5b00f44b 100644 --- a/src/router/modules/account/aliasCard.js +++ b/src/router/modules/account/aliasCard.js @@ -3,7 +3,7 @@ export default { path: ':id', component: () => import('src/pages/Account/Alias/Card/AliasCard.vue'), redirect: { name: 'AliasSummary' }, - meta: { menu: ['AliasBasicData', 'AliasUsers'] }, + meta: { moduleName: 'Alias', menu: ['AliasBasicData', 'AliasUsers'] }, children: [ { name: 'AliasSummary', diff --git a/src/router/modules/account/roleCard.js b/src/router/modules/account/roleCard.js index c36ce71b9..f8100071f 100644 --- a/src/router/modules/account/roleCard.js +++ b/src/router/modules/account/roleCard.js @@ -4,6 +4,7 @@ export default { component: () => import('src/pages/Account/Role/Card/RoleCard.vue'), redirect: { name: 'RoleSummary' }, meta: { + moduleName: 'Role', menu: ['RoleBasicData', 'SubRoles', 'InheritedRoles', 'RoleLog'], }, children: [ diff --git a/src/router/modules/entry.js b/src/router/modules/entry.js index f362c7653..b5656dc5f 100644 --- a/src/router/modules/entry.js +++ b/src/router/modules/entry.js @@ -6,13 +6,7 @@ const entryCard = { component: () => import('src/pages/Entry/Card/EntryCard.vue'), redirect: { name: 'EntrySummary' }, meta: { - menu: [ - 'EntryBasicData', - 'EntryBuys', - 'EntryNotes', - 'EntryDms', - 'EntryLog', - ], + menu: ['EntryBasicData', 'EntryBuys', 'EntryNotes', 'EntryDms', 'EntryLog'], }, children: [ { @@ -91,7 +85,7 @@ export default { 'EntryLatestBuys', 'EntryStockBought', 'EntryWasteRecalc', - ] + ], }, component: RouterView, redirect: { name: 'EntryMain' }, @@ -103,7 +97,7 @@ export default { redirect: { name: 'EntryIndexMain' }, children: [ { - path:'', + path: '', name: 'EntryIndexMain', redirect: { name: 'EntryList' }, component: () => import('src/pages/Entry/EntryList.vue'), @@ -115,6 +109,7 @@ export default { title: 'list', icon: 'view_list', }, + component: () => import('src/pages/Entry/EntryList.vue'), }, entryCard, ], @@ -127,7 +122,7 @@ export default { icon: 'add', }, component: () => import('src/pages/Entry/EntryCreate.vue'), - }, + }, { path: 'my', name: 'MyEntries', @@ -167,4 +162,4 @@ export default { ], }, ], -}; \ No newline at end of file +}; diff --git a/src/router/modules/route.js b/src/router/modules/route.js index 946ad3e15..835324d20 100644 --- a/src/router/modules/route.js +++ b/src/router/modules/route.js @@ -160,6 +160,36 @@ const roadmapCard = { ], }; +const vehicleCard = { + path: ':id', + name: 'VehicleCard', + component: () => import('src/pages/Route/Vehicle/Card/VehicleCard.vue'), + redirect: { name: 'VehicleSummary' }, + meta: { + menu: ['VehicleBasicData'], + }, + children: [ + { + name: 'VehicleSummary', + path: 'summary', + meta: { + title: 'summary', + icon: 'view_list', + }, + component: () => import('src/pages/Route/Vehicle/Card/VehicleSummary.vue'), + }, + { + name: 'VehicleBasicData', + path: 'basic-data', + meta: { + title: 'basicData', + icon: 'vn:settings', + }, + component: () => import('src/pages/Route/Vehicle/Card/VehicleBasicData.vue'), + }, + ], +}; + export default { name: 'Route', path: '/route', @@ -174,6 +204,7 @@ export default { 'RouteRoadmap', 'CmrList', 'AgencyList', + 'VehicleList', ], }, component: RouterView, @@ -280,6 +311,27 @@ export default { agencyCard, ], }, + { + path: 'vehicle', + name: 'RouteVehicle', + redirect: { name: 'VehicleList' }, + meta: { + title: 'vehicle', + icon: 'directions_car', + }, + component: () => import('src/pages/Route/Vehicle/VehicleList.vue'), + children: [ + { + path: 'list', + name: 'VehicleList', + meta: { + title: 'vehicleList', + icon: 'directions_car', + }, + }, + vehicleCard, + ], + }, ], }, ], diff --git a/src/router/modules/shelving.js b/src/router/modules/shelving.js index 55fb04278..c085dd8dc 100644 --- a/src/router/modules/shelving.js +++ b/src/router/modules/shelving.js @@ -3,7 +3,7 @@ import { RouterView } from 'vue-router'; const parkingCard = { name: 'ParkingCard', path: ':id', - component: () => import('src/pages/Parking/Card/ParkingCard.vue'), + component: () => import('src/pages/Shelving/Parking/Card/ParkingCard.vue'), redirect: { name: 'ParkingSummary' }, meta: { menu: ['ParkingBasicData', 'ParkingLog'], @@ -16,7 +16,7 @@ const parkingCard = { title: 'summary', icon: 'launch', }, - component: () => import('src/pages/Parking/Card/ParkingSummary.vue'), + component: () => import('src/pages/Shelving/Parking/Card/ParkingSummary.vue'), }, { path: 'basic-data', @@ -25,7 +25,8 @@ const parkingCard = { title: 'basicData', icon: 'vn:settings', }, - component: () => import('src/pages/Parking/Card/ParkingBasicData.vue'), + component: () => + import('src/pages/Shelving/Parking/Card/ParkingBasicData.vue'), }, { path: 'log', @@ -34,7 +35,7 @@ const parkingCard = { title: 'log', icon: 'history', }, - component: () => import('src/pages/Parking/Card/ParkingLog.vue'), + component: () => import('src/pages/Shelving/Parking/Card/ParkingLog.vue'), }, ], }; @@ -127,7 +128,7 @@ export default { title: 'parkingList', icon: 'view_list', }, - component: () => import('src/pages/Parking/ParkingList.vue'), + component: () => import('src/pages/Shelving/Parking/ParkingList.vue'), children: [ { path: 'list', diff --git a/src/router/modules/supplier.js b/src/router/modules/supplier.js index 4ece4c784..19763cdf3 100644 --- a/src/router/modules/supplier.js +++ b/src/router/modules/supplier.js @@ -1,19 +1,12 @@ import { RouterView } from 'vue-router'; -export default { - path: '/supplier', - name: 'Supplier', +const supplierCard = { + name: 'SupplierCard', + path: ':id', + component: () => import('src/pages/Supplier/Card/SupplierCard.vue'), + redirect: { name: 'SupplierSummary' }, meta: { - title: 'suppliers', - icon: 'vn:supplier', - moduleName: 'Supplier', - keyBinding: 'p', - }, - component: RouterView, - redirect: { name: 'SupplierMain' }, - menus: { - main: ['SupplierList'], - card: [ + menu: [ 'SupplierBasicData', 'SupplierFiscalData', 'SupplierBillingData', @@ -27,21 +20,165 @@ export default { 'SupplierDms', ], }, + children: [ + { + name: 'SupplierSummary', + path: 'summary', + meta: { + title: 'summary', + icon: 'launch', + }, + component: () => import('src/pages/Supplier/Card/SupplierSummary.vue'), + }, + { + path: 'basic-data', + name: 'SupplierBasicData', + meta: { + title: 'basicData', + icon: 'vn:settings', + }, + component: () => import('src/pages/Supplier/Card/SupplierBasicData.vue'), + }, + { + path: 'fiscal-data', + name: 'SupplierFiscalData', + meta: { + title: 'fiscalData', + icon: 'vn:dfiscales', + }, + component: () => import('src/pages/Supplier/Card/SupplierFiscalData.vue'), + }, + { + path: 'billing-data', + name: 'SupplierBillingData', + meta: { + title: 'billingData', + icon: 'vn:payment', + }, + component: () => import('src/pages/Supplier/Card/SupplierBillingData.vue'), + }, + { + path: 'log', + name: 'SupplierLog', + meta: { + title: 'log', + icon: 'vn:History', + }, + component: () => import('src/pages/Supplier/Card/SupplierLog.vue'), + }, + { + path: 'account', + name: 'SupplierAccounts', + meta: { + title: 'accounts', + icon: 'vn:credit', + }, + component: () => import('src/pages/Supplier/Card/SupplierAccounts.vue'), + }, + { + path: 'contact', + name: 'SupplierContacts', + meta: { + title: 'contacts', + icon: 'contact_phone', + }, + component: () => import('src/pages/Supplier/Card/SupplierContacts.vue'), + }, + { + path: 'address', + name: 'SupplierAddresses', + meta: { + title: 'addresses', + icon: 'vn:delivery', + }, + component: () => import('src/pages/Supplier/Card/SupplierAddresses.vue'), + }, + { + path: 'address/create', + name: 'SupplierAddressesCreate', + component: () => + import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'), + }, + { + path: 'balance', + name: 'SupplierBalance', + meta: { + title: 'balance', + icon: 'balance', + }, + component: () => import('src/pages/Supplier/Card/SupplierBalance.vue'), + }, + { + path: 'consumption', + name: 'SupplierConsumption', + meta: { + title: 'consumption', + icon: 'show_chart', + }, + component: () => import('src/pages/Supplier/Card/SupplierConsumption.vue'), + }, + { + path: 'agency-term', + name: 'SupplierAgencyTerm', + meta: { + title: 'agencyTerm', + icon: 'vn:agency-term', + }, + component: () => import('src/pages/Supplier/Card/SupplierAgencyTerm.vue'), + }, + { + path: 'dms', + name: 'SupplierDms', + meta: { + title: 'dms', + icon: 'smb_share', + }, + component: () => import('src/pages/Supplier/Card/SupplierDms.vue'), + }, + { + path: 'agency-term/create', + name: 'SupplierAgencyTermCreate', + component: () => + import('src/pages/Supplier/Card/SupplierAgencyTermCreate.vue'), + }, + ], +}; + +export default { + name: 'Supplier', + path: '/supplier', + meta: { + title: 'suppliers', + icon: 'vn:supplier', + moduleName: 'Supplier', + keyBinding: 'p', + menu: ['SupplierList'], + }, + component: RouterView, + redirect: { name: 'SupplierMain' }, children: [ { path: '', name: 'SupplierMain', component: () => import('src/components/common/VnModule.vue'), - redirect: { name: 'SupplierList' }, + redirect: { name: 'SupplierIndexMain' }, children: [ { - path: 'list', - name: 'SupplierList', - meta: { - title: 'list', - icon: 'view_list', - }, + path: '', + name: 'SupplierIndexMain', + redirect: { name: 'SupplierList' }, component: () => import('src/pages/Supplier/SupplierList.vue'), + children: [ + { + path: 'list', + name: 'SupplierList', + meta: { + title: 'list', + icon: 'view_list', + }, + }, + supplierCard, + ], }, { path: 'create', @@ -54,143 +191,5 @@ export default { }, ], }, - { - name: 'SupplierCard', - path: ':id', - component: () => import('src/pages/Supplier/Card/SupplierCard.vue'), - redirect: { name: 'SupplierSummary' }, - children: [ - { - name: 'SupplierSummary', - path: 'summary', - meta: { - title: 'summary', - icon: 'launch', - }, - component: () => - import('src/pages/Supplier/Card/SupplierSummary.vue'), - }, - { - path: 'basic-data', - name: 'SupplierBasicData', - meta: { - title: 'basicData', - icon: 'vn:settings', - }, - component: () => - import('src/pages/Supplier/Card/SupplierBasicData.vue'), - }, - { - path: 'fiscal-data', - name: 'SupplierFiscalData', - meta: { - title: 'fiscalData', - icon: 'vn:dfiscales', - }, - component: () => - import('src/pages/Supplier/Card/SupplierFiscalData.vue'), - }, - { - path: 'billing-data', - name: 'SupplierBillingData', - meta: { - title: 'billingData', - icon: 'vn:payment', - }, - component: () => - import('src/pages/Supplier/Card/SupplierBillingData.vue'), - }, - { - path: 'log', - name: 'SupplierLog', - meta: { - title: 'log', - icon: 'vn:History', - }, - component: () => import('src/pages/Supplier/Card/SupplierLog.vue'), - }, - { - path: 'account', - name: 'SupplierAccounts', - meta: { - title: 'accounts', - icon: 'vn:credit', - }, - component: () => - import('src/pages/Supplier/Card/SupplierAccounts.vue'), - }, - { - path: 'contact', - name: 'SupplierContacts', - meta: { - title: 'contacts', - icon: 'contact_phone', - }, - component: () => - import('src/pages/Supplier/Card/SupplierContacts.vue'), - }, - { - path: 'address', - name: 'SupplierAddresses', - meta: { - title: 'addresses', - icon: 'vn:delivery', - }, - component: () => - import('src/pages/Supplier/Card/SupplierAddresses.vue'), - }, - { - path: 'address/create', - name: 'SupplierAddressesCreate', - component: () => - import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'), - }, - { - path: 'balance', - name: 'SupplierBalance', - meta: { - title: 'balance', - icon: 'balance', - }, - component: () => - import('src/pages/Supplier/Card/SupplierBalance.vue'), - }, - { - path: 'consumption', - name: 'SupplierConsumption', - meta: { - title: 'consumption', - icon: 'show_chart', - }, - component: () => - import('src/pages/Supplier/Card/SupplierConsumption.vue'), - }, - { - path: 'agency-term', - name: 'SupplierAgencyTerm', - meta: { - title: 'agencyTerm', - icon: 'vn:agency-term', - }, - component: () => - import('src/pages/Supplier/Card/SupplierAgencyTerm.vue'), - }, - { - path: 'dms', - name: 'SupplierDms', - meta: { - title: 'dms', - icon: 'smb_share', - }, - component: () => import('src/pages/Supplier/Card/SupplierDms.vue'), - }, - { - path: 'agency-term/create', - name: 'SupplierAgencyTermCreate', - component: () => - import('src/pages/Supplier/Card/SupplierAgencyTermCreate.vue'), - }, - ], - }, ], }; diff --git a/src/router/modules/ticket.js b/src/router/modules/ticket.js index e5b423f64..bfcb78787 100644 --- a/src/router/modules/ticket.js +++ b/src/router/modules/ticket.js @@ -192,7 +192,13 @@ export default { icon: 'vn:ticket', moduleName: 'Ticket', keyBinding: 't', - menu: ['TicketList', 'TicketAdvance', 'TicketWeekly', 'TicketFuture'], + menu: [ + 'TicketList', + 'TicketAdvance', + 'TicketWeekly', + 'TicketFuture', + 'TicketNegative', + ], }, component: RouterView, redirect: { name: 'TicketMain' }, @@ -229,6 +235,32 @@ export default { }, component: () => import('src/pages/Ticket/TicketCreate.vue'), }, + { + path: 'negative', + redirect: { name: 'TicketNegative' }, + children: [ + { + name: 'TicketNegative', + meta: { + title: 'negative', + icon: 'exposure', + }, + component: () => + import('src/pages/Ticket/Negative/TicketLackList.vue'), + path: '', + }, + { + name: 'NegativeDetail', + path: ':id', + meta: { + title: 'summary', + icon: 'launch', + }, + component: () => + import('src/pages/Ticket/Negative/TicketLackDetail.vue'), + }, + ], + }, { path: 'weekly', name: 'TicketWeekly', diff --git a/src/router/modules/worker.js b/src/router/modules/worker.js index 1d013c596..3eb95a96e 100644 --- a/src/router/modules/worker.js +++ b/src/router/modules/worker.js @@ -201,9 +201,10 @@ const workerCard = { const departmentCard = { name: 'DepartmentCard', path: ':id', - component: () => import('src/pages/Department/Card/DepartmentCard.vue'), + component: () => import('src/pages/Worker/Department/Card/DepartmentCard.vue'), redirect: { name: 'DepartmentSummary' }, meta: { + moduleName: 'Department', menu: ['DepartmentBasicData'], }, children: [ @@ -214,7 +215,8 @@ const departmentCard = { title: 'summary', icon: 'launch', }, - component: () => import('src/pages/Department/Card/DepartmentSummary.vue'), + component: () => + import('src/pages/Worker/Department/Card/DepartmentSummary.vue'), }, { path: 'basic-data', @@ -223,7 +225,8 @@ const departmentCard = { title: 'basicData', icon: 'vn:settings', }, - component: () => import('src/pages/Department/Card/DepartmentBasicData.vue'), + component: () => + import('src/pages/Worker/Department/Card/DepartmentBasicData.vue'), }, ], }; diff --git a/src/stores/__tests__/useNavigationStore.spec.js b/src/stores/__tests__/useNavigationStore.spec.js new file mode 100644 index 000000000..c5df6157e --- /dev/null +++ b/src/stores/__tests__/useNavigationStore.spec.js @@ -0,0 +1,153 @@ +import { setActivePinia, createPinia } from 'pinia'; +import { describe, beforeEach, afterEach, it, expect, vi, beforeAll } from 'vitest'; +import { useNavigationStore } from '../useNavigationStore'; +import axios from 'axios'; + +let store; + +vi.mock('src/router/modules', () => [ + { name: 'Item', meta: {} }, + { name: 'Shelving', meta: {} }, + { name: 'Order', meta: {} }, +]); + +vi.mock('src/filters', () => ({ + toLowerCamel: vi.fn((name) => name.toLowerCase()), +})); + +const modulesMock = [ + { + name: 'Item', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'item', + isPinned: true, + }, + { + name: 'Shelving', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'shelving', + isPinned: false, + }, + { + name: 'Order', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'order', + isPinned: false, + }, +]; + +const pinnedModulesMock = [ + { + name: 'Item', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'item', + isPinned: true, + }, +]; + +describe('useNavigationStore', () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.spyOn(axios, 'get').mockResolvedValue({ data: true }); + store = useNavigationStore(); + store.getModules = vi.fn().mockReturnValue({ + value: modulesMock, + }); + store.getPinnedModules = vi.fn().mockReturnValue({ + value: pinnedModulesMock, + }); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return modules with correct structure', () => { + const store = useNavigationStore(); + const modules = store.getModules(); + + expect(modules.value).toEqual(modulesMock); + }); + + it('should return pinned modules', () => { + const store = useNavigationStore(); + const pinnedModules = store.getPinnedModules(); + + expect(pinnedModules.value).toEqual(pinnedModulesMock); + }); + + it('should toggle pinned modules', () => { + const store = useNavigationStore(); + + store.togglePinned('item'); + store.togglePinned('shelving'); + expect(store.pinnedModules).toEqual(['item', 'shelving']); + + store.togglePinned('item'); + expect(store.pinnedModules).toEqual(['shelving']); + }); + + it('should fetch pinned modules', async () => { + vi.spyOn(axios, 'get').mockResolvedValue({ + data: [{ id: 1, workerFk: 9, moduleFk: 'order', position: 1 }], + }); + const store = useNavigationStore(); + await store.fetchPinned(); + + expect(store.pinnedModules).toEqual(['order']); + }); + + it('should add menu item correctly', () => { + const store = useNavigationStore(); + const module = 'customer'; + const parent = []; + const route = { + name: 'customer', + title: 'Customer', + icon: 'customer', + meta: { + keyBinding: 'ctrl+shift+c', + name: 'customer', + title: 'Customer', + icon: 'customer', + menu: 'customer', + menuChildren: [{ name: 'customer', title: 'Customer', icon: 'customer' }], + }, + }; + + const result = store.addMenuItem(module, route, parent); + const expectedItem = { + children: [ + { + icon: 'customer', + name: 'customer', + title: 'globals.pageTitles.Customer', + }, + ], + icon: 'customer', + keyBinding: 'ctrl+shift+c', + name: 'customer', + title: 'globals.pageTitles.Customer', + }; + expect(result).toEqual(expectedItem); + expect(parent.length).toBe(1); + expect(parent).toEqual([expectedItem]); + }); + + it('should not add menu item if condition is not met', () => { + const store = useNavigationStore(); + const module = 'testModule'; + const route = { meta: { hidden: true, menuchildren: {} } }; + const parent = []; + const result = store.addMenuItem(module, route, parent); + expect(result).toBeUndefined(); + expect(parent.length).toBe(0); + }); +}); diff --git a/src/stores/useArrayDataStore.js b/src/stores/useArrayDataStore.js index 8d62fdb4a..b3996d1e3 100644 --- a/src/stores/useArrayDataStore.js +++ b/src/stores/useArrayDataStore.js @@ -19,6 +19,7 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => { page: 1, mapKey: 'id', keepData: false, + oneRecord: false, }; function get(key) { diff --git a/src/utils/notifyResults.js b/src/utils/notifyResults.js new file mode 100644 index 000000000..e87ad6c6f --- /dev/null +++ b/src/utils/notifyResults.js @@ -0,0 +1,19 @@ +import { Notify } from 'quasar'; + +export default function (results, key) { + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + const data = JSON.parse(result.value.config.data); + Notify.create({ + type: 'positive', + message: `Operación (${index + 1}) ${data[key]} completada con éxito.`, + }); + } else { + const data = JSON.parse(result.reason.config.data); + Notify.create({ + type: 'negative', + message: `Operación (${index + 1}) ${data[key]} fallida: ${result.reason.message}`, + }); + } + }); +} diff --git a/test/cypress/integration/Order/orderCatalog.spec.js b/test/cypress/integration/Order/orderCatalog.spec.js index 9e01eb915..a106d0e8a 100644 --- a/test/cypress/integration/Order/orderCatalog.spec.js +++ b/test/cypress/integration/Order/orderCatalog.spec.js @@ -45,7 +45,6 @@ describe('OrderCatalog', () => { ).type('{enter}'); cy.get(':nth-child(1) > [data-cy="catalogFilterCategory"]').click(); cy.dataCy('catalogFilterValueDialogBtn').last().click(); - cy.get('[data-cy="catalogFilterValueDialogTagSelect"]').click(); cy.selectOption("[data-cy='catalogFilterValueDialogTagSelect']", 'Tallos'); cy.dataCy('catalogFilterValueDialogValueInput').find('input').focus(); cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('2'); diff --git a/test/cypress/integration/entry/stockBought.spec.js b/test/cypress/integration/entry/stockBought.spec.js index 193d9e448..b282a19a5 100644 --- a/test/cypress/integration/entry/stockBought.spec.js +++ b/test/cypress/integration/entry/stockBought.spec.js @@ -6,6 +6,7 @@ describe('EntryStockBought', () => { }); it('Should edit the reserved space', () => { cy.get('.q-field__native.q-placeholder').should('have.value', '01/01/2001'); + cy.get('[data-col-field="reserve"][data-row-index="0"]').click(); cy.get('input[name="reserve"]').type('10{enter}'); cy.get('button[title="Save"]').click(); cy.checkNotification('Data saved'); @@ -15,25 +16,35 @@ describe('EntryStockBought', () => { cy.get('input[aria-label="Reserve"]').type('1'); cy.get('input[aria-label="Date"]').eq(1).clear(); cy.get('input[aria-label="Date"]').eq(1).type('01-01'); - cy.get('input[aria-label="Buyer"]').type('buyerboss{downarrow}{enter}'); + cy.get('input[aria-label="Buyer"]').type('buyerBossNick'); + cy.get('div[role="listbox"] > div > div[role="option"]') + .eq(0) + .should('be.visible') + .click(); + + cy.get('[data-cy="FormModelPopup_save"]').click(); cy.get('.q-notification__message').should('have.text', 'Data created'); + + cy.get('[data-col-field="reserve"][data-row-index="1"]').click().clear(); + cy.get('[data-cy="searchBtn"]').eq(1).click(); + cy.get('.q-table__bottom.row.items-center.q-table__bottom--nodata') + .should('have.text', 'warningNo data available') + .type('{esc}'); + cy.get('[data-col-field="reserve"][data-row-index="1"]') + .click() + .type('{backspace}{enter}'); + cy.get('[data-cy="crudModelDefaultSaveBtn"]').should('be.enabled').click(); + cy.get('.q-notification__message').eq(1).should('have.text', 'Data saved'); }); it('Should check detail for the buyer', () => { - cy.get(':nth-child(1) > .sticky > .q-btn > .q-btn__content > .q-icon').click(); + cy.get('[data-cy="searchBtn"]').eq(0).click(); cy.get('tBody > tr').eq(1).its('length').should('eq', 1); }); - it('Should check detail for the buyerBoss and had no content', () => { - cy.get(':nth-child(2) > .sticky > .q-btn > .q-btn__content > .q-icon').click(); - cy.get('.q-table__bottom.row.items-center.q-table__bottom--nodata').should( - 'have.text', - 'warningNo data available' - ); - }); + it('Should edit travel m3 and refresh', () => { - cy.get('.vn-row > div > .q-btn > .q-btn__content > .q-icon').click(); - cy.get('input[aria-label="m3"]').clear(); - cy.get('input[aria-label="m3"]').type('60'); - cy.get('.q-mt-lg > .q-btn--standard > .q-btn__content > .block').click(); + cy.get('[data-cy="edit-travel"]').should('be.visible').click(); + cy.get('input[aria-label="m3"]').clear().type('60'); + cy.get('[data-cy="FormModelPopup_save"]').click(); cy.get('.vn-row > div > :nth-child(2)').should('have.text', '60'); }); }); diff --git a/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js b/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js index 2016fca6d..11ca1bb59 100644 --- a/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js @@ -1,9 +1,9 @@ /// <reference types="cypress" /> describe('InvoiceInBasicData', () => { - const formInputs = '.q-form > .q-card input'; const firstFormSelect = '.q-card > .vn-row:nth-child(1) > .q-select'; - const documentBtns = '[data-cy="dms-buttons"] button'; const dialogInputs = '.q-dialog input'; + const resetBtn = '.q-btn-group--push > .q-btn--flat'; + const getDocumentBtns = (opt) => `[data-cy="dms-buttons"] > :nth-child(${opt})`; beforeEach(() => { cy.login('developer'); @@ -11,13 +11,16 @@ describe('InvoiceInBasicData', () => { }); it('should edit the provideer and supplier ref', () => { - cy.selectOption(firstFormSelect, 'Bros'); - cy.get('[title="Reset"]').click(); - cy.get(formInputs).eq(1).type('{selectall}4739'); - cy.saveCard(); + cy.dataCy('UnDeductibleVatSelect').type('4751000000'); + cy.get('.q-menu .q-item').contains('4751000000').click(); + cy.get(resetBtn).click(); - cy.get(`${firstFormSelect} input`).invoke('val').should('eq', 'Plants nick'); - cy.get(formInputs).eq(1).invoke('val').should('eq', '4739'); + cy.waitForElement('#formModel').within(() => { + cy.dataCy('vnSupplierSelect').type('Bros nick'); + }) + cy.get('.q-menu .q-item').contains('Bros nick').click(); + cy.saveCard(); + cy.get(`${firstFormSelect} input`).invoke('val').should('eq', 'Bros nick'); }); it('should edit, remove and create the dms data', () => { @@ -25,18 +28,18 @@ describe('InvoiceInBasicData', () => { const secondInput = "I don't know what posting here!"; //edit - cy.get(documentBtns).eq(1).click(); + cy.get(getDocumentBtns(2)).click(); cy.get(dialogInputs).eq(0).type(`{selectall}${firtsInput}`); cy.get('textarea').type(`{selectall}${secondInput}`); cy.get('[data-cy="FormModelPopup_save"]').click(); - cy.get(documentBtns).eq(1).click(); + cy.get(getDocumentBtns(2)).click(); cy.get(dialogInputs).eq(0).invoke('val').should('eq', firtsInput); cy.get('textarea').invoke('val').should('eq', secondInput); cy.get('[data-cy="FormModelPopup_save"]').click(); cy.checkNotification('Data saved'); //remove - cy.get(documentBtns).eq(2).click(); + cy.get(getDocumentBtns(3)).click(); cy.get('[data-cy="VnConfirm_confirm"]').click(); cy.checkNotification('Data saved'); @@ -46,7 +49,7 @@ describe('InvoiceInBasicData', () => { 'test/cypress/fixtures/image.jpg', { force: true, - } + }, ); cy.get('[data-cy="FormModelPopup_save"]').click(); cy.checkNotification('Data saved'); diff --git a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js index f8b403a45..1e7ce1003 100644 --- a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js @@ -36,7 +36,7 @@ describe('InvoiceInVat', () => { cy.get(dialogInputs).eq(0).type(randomInt); cy.get(dialogInputs).eq(1).type('This is a dummy expense'); - cy.get('button[type="submit"]').click(); + cy.get('[data-cy="FormModelPopup_save"]').click(); cy.get('.q-notification__message').should('have.text', 'Data created'); }); }); diff --git a/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js b/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js index 4f28cc490..4d530de05 100644 --- a/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js +++ b/test/cypress/integration/invoiceOut/invoiceOutNegativeBases.spec.js @@ -22,9 +22,7 @@ describe('InvoiceOut negative bases', () => { }); it('should filter and download as CSV', () => { - cy.get( - ':nth-child(7) > .full-width > :nth-child(1) > .column > div.q-px-xs > .q-field > .q-field__inner > .q-field__control' - ).type('23{enter}'); + cy.get('input[name="ticketFk"]').type('23{enter}'); cy.get('#subToolbar > .q-btn').click(); cy.checkNotification('CSV downloaded successfully'); }); diff --git a/test/cypress/integration/item/ItemProposal.spec.js b/test/cypress/integration/item/ItemProposal.spec.js new file mode 100644 index 000000000..b3ba9f676 --- /dev/null +++ b/test/cypress/integration/item/ItemProposal.spec.js @@ -0,0 +1,11 @@ +/// <reference types="cypress" /> +describe('ItemProposal', () => { + beforeEach(() => { + const ticketId = 1; + + cy.login('developer'); + cy.visit(`/#/ticket/${ticketId}/summary`); + }); + + describe('Handle item proposal selected', () => {}); +}); diff --git a/test/cypress/integration/item/itemTag.spec.js b/test/cypress/integration/item/itemTag.spec.js index 17423bc51..425eaffe6 100644 --- a/test/cypress/integration/item/itemTag.spec.js +++ b/test/cypress/integration/item/itemTag.spec.js @@ -16,10 +16,7 @@ describe('Item tag', () => { cy.dataCy(newTag).should('be.visible').click().type('Genero{enter}'); cy.dataCy('tagGeneroValue').eq(1).should('be.visible'); cy.dataCy(saveBtn).click(); - cy.get('.q-notification__message').should( - 'have.text', - "The tag or priority can't be repeated for an item", - ); + cy.checkNotification("The tag or priority can't be repeated for an item"); }); it('should add a new tag', () => { diff --git a/test/cypress/integration/parking/parkingBasicData.spec.js b/test/cypress/integration/parking/parkingBasicData.spec.js index 0d130d335..f64f23ec8 100644 --- a/test/cypress/integration/parking/parkingBasicData.spec.js +++ b/test/cypress/integration/parking/parkingBasicData.spec.js @@ -13,11 +13,11 @@ describe('ParkingBasicData', () => { cy.get(sectorOpt).click(); cy.get(codeInput).eq(0).clear(); - cy.get(codeInput).eq(0).type(123); + cy.get(codeInput).eq(0).type('900-001'); cy.saveCard(); cy.get(sectorSelect).should('have.value', 'Second sector'); - cy.get(codeInput).should('have.value', 123); + cy.get(codeInput).should('have.value', '900-001'); }); }); diff --git a/test/cypress/integration/route/agency/agencyWorkCenter.spec.js b/test/cypress/integration/route/agency/agencyWorkCenter.spec.js index 796738127..5679ceba1 100644 --- a/test/cypress/integration/route/agency/agencyWorkCenter.spec.js +++ b/test/cypress/integration/route/agency/agencyWorkCenter.spec.js @@ -15,6 +15,7 @@ describe.skip('AgencyWorkCenter', () => { // expect error when duplicate cy.get(createButton).click(); + cy.selectOption(workCenterCombobox, 'workCenterOne'); cy.get('[data-cy="FormModelPopup_save"]').click(); cy.checkNotification('This workCenter is already assigned to this agency'); cy.get('[data-cy="FormModelPopup_cancel"]').click(); diff --git a/test/cypress/integration/route/routeList.spec.js b/test/cypress/integration/route/routeList.spec.js index 5ff157d2a..04278cfc5 100644 --- a/test/cypress/integration/route/routeList.spec.js +++ b/test/cypress/integration/route/routeList.spec.js @@ -4,9 +4,6 @@ describe('Route', () => { cy.login('developer'); cy.visit(`/#/route/extended-list`); }); - const getVnSelect = - '> :nth-child(1) > .column > .q-field > .q-field__inner > .q-field__control > .q-field__control-container'; - const getRowColumn = (row, column) => `:nth-child(${row}) > :nth-child(${column})`; it('Route list create route', () => { cy.addBtnClick(); @@ -17,15 +14,23 @@ describe('Route', () => { it('Route list search and edit', () => { cy.get('#searchbar input').type('{enter}'); - cy.get('input[name="description"]').type('routeTestOne{enter}'); + cy.get('[data-col-field="description"][data-row-index="0"]') + .click() + .type('routeTestOne{enter}'); cy.get('.q-table tr') .its('length') .then((rowCount) => { expect(rowCount).to.be.greaterThan(0); }); - cy.get(getRowColumn(1, 3) + getVnSelect).type('{downArrow}{enter}'); - cy.get(getRowColumn(1, 4) + getVnSelect).type('{downArrow}{enter}'); - cy.get(getRowColumn(1, 5) + getVnSelect).type('{downArrow}{enter}'); + cy.get('[data-col-field="workerFk"][data-row-index="0"]') + .click() + .type('{downArrow}{enter}'); + cy.get('[data-col-field="agencyModeFk"][data-row-index="0"]') + .click() + .type('{downArrow}{enter}'); + cy.get('[data-col-field="vehicleFk"][data-row-index="0"]') + .click() + .type('{downArrow}{enter}'); cy.get('button[title="Save"]').click(); cy.get('.q-notification__message').should('have.text', 'Data saved'); }); diff --git a/test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js b/test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js new file mode 100644 index 000000000..64b9ca0a0 --- /dev/null +++ b/test/cypress/integration/route/vehicle/vehicleDescriptor.spec.js @@ -0,0 +1,13 @@ +describe('Vehicle', () => { + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('deliveryAssistant'); + cy.visit(`/#/route/vehicle/7`); + }); + + it('should delete a vehicle', () => { + cy.openActionsDescriptor(); + cy.get('[data-cy="delete"]').click(); + cy.checkNotification('Vehicle removed'); + }); +}); diff --git a/test/cypress/integration/ticket/negative/TicketLackDetail.spec.js b/test/cypress/integration/ticket/negative/TicketLackDetail.spec.js new file mode 100644 index 000000000..9ea1cff63 --- /dev/null +++ b/test/cypress/integration/ticket/negative/TicketLackDetail.spec.js @@ -0,0 +1,147 @@ +/// <reference types="cypress" /> +describe('Ticket Lack detail', () => { + beforeEach(() => { + cy.login('developer'); + cy.intercept('GET', /\/api\/Tickets\/itemLack\/5.*$/, { + statusCode: 200, + body: [ + { + saleFk: 33, + code: 'OK', + ticketFk: 142, + nickname: 'Malibu Point', + shipped: '2000-12-31T23:00:00.000Z', + hour: 0, + quantity: 50, + agName: 'Super-Man delivery', + alertLevel: 0, + stateName: 'OK', + stateId: 3, + itemFk: 5, + price: 1.79, + alertLevelCode: 'FREE', + zoneFk: 9, + zoneName: 'Zone superMan', + theoreticalhour: '2011-11-01T22:59:00.000Z', + isRookie: 1, + turno: 1, + peticionCompra: 1, + hasObservation: 1, + hasToIgnore: 1, + isBasket: 1, + minTimed: 0, + customerId: 1104, + customerName: 'Tony Stark', + observationTypeCode: 'administrative', + }, + ], + }).as('getItemLack'); + + cy.visit('/#/ticket/negative/5'); + cy.wait('@getItemLack'); + }); + describe('Table actions', () => { + it.skip('should display only one row in the lack list', () => { + cy.location('href').should('contain', '#/ticket/negative/5'); + + cy.get('[data-cy="changeItem"]').should('be.disabled'); + cy.get('[data-cy="changeState"]').should('be.disabled'); + cy.get('[data-cy="changeQuantity"]').should('be.disabled'); + cy.get('[data-cy="itemProposal"]').should('be.disabled'); + cy.get('[data-cy="transferLines"]').should('be.disabled'); + cy.get('tr.cursor-pointer > :nth-child(1)').click(); + cy.get('[data-cy="changeItem"]').should('be.enabled'); + cy.get('[data-cy="changeState"]').should('be.enabled'); + cy.get('[data-cy="changeQuantity"]').should('be.enabled'); + cy.get('[data-cy="itemProposal"]').should('be.enabled'); + cy.get('[data-cy="transferLines"]').should('be.enabled'); + }); + }); + describe('Item proposal', () => { + beforeEach(() => { + cy.get('tr.cursor-pointer > :nth-child(1)').click(); + + cy.intercept('GET', /\/api\/Items\/getSimilar\?.*$/, { + statusCode: 200, + body: [ + { + id: 1, + longName: 'Ranged weapon longbow 50cm', + subName: 'Stark Industries', + tag5: 'Color', + value5: 'Brown', + match5: 0, + match6: 0, + match7: 0, + match8: 1, + tag6: 'Categoria', + value6: '+1 precission', + tag7: 'Tallos', + value7: '1', + tag8: null, + value8: null, + available: 20, + calc_id: 6, + counter: 0, + minQuantity: 1, + visible: null, + price2: 1, + }, + { + id: 2, + longName: 'Ranged weapon longbow 100cm', + subName: 'Stark Industries', + tag5: 'Color', + value5: 'Brown', + match5: 0, + match6: 1, + match7: 0, + match8: 1, + tag6: 'Categoria', + value6: '+1 precission', + tag7: 'Tallos', + value7: '1', + tag8: null, + value8: null, + available: 50, + calc_id: 6, + counter: 1, + minQuantity: 5, + visible: null, + price2: 10, + }, + { + id: 3, + longName: 'Ranged weapon longbow 200cm', + subName: 'Stark Industries', + tag5: 'Color', + value5: 'Brown', + match5: 1, + match6: 1, + match7: 1, + match8: 1, + tag6: 'Categoria', + value6: '+1 precission', + tag7: 'Tallos', + value7: '1', + tag8: null, + value8: null, + available: 185, + calc_id: 6, + counter: 10, + minQuantity: 10, + visible: null, + price2: 100, + }, + ], + }).as('getItemGetSimilar'); + cy.get('[data-cy="itemProposal"]').click(); + cy.wait('@getItemGetSimilar'); + }); + describe('Replace item if', () => { + it.only('Quantity is less than available', () => { + cy.get(':nth-child(1) > .text-right > .q-btn').click(); + }); + }); + }); +}); diff --git a/test/cypress/integration/ticket/negative/TicketLackList.spec.js b/test/cypress/integration/ticket/negative/TicketLackList.spec.js new file mode 100644 index 000000000..01ab4f621 --- /dev/null +++ b/test/cypress/integration/ticket/negative/TicketLackList.spec.js @@ -0,0 +1,36 @@ +/// <reference types="cypress" /> +describe('Ticket Lack list', () => { + beforeEach(() => { + cy.login('developer'); + cy.intercept('GET', /Tickets\/itemLack\?.*$/, { + statusCode: 200, + body: [ + { + itemFk: 5, + longName: 'Ranged weapon pistol 9mm', + warehouseFk: 1, + producer: null, + size: 15, + category: null, + warehouse: 'Warehouse One', + lack: -50, + inkFk: 'SLV', + timed: '2025-01-25T22:59:00.000Z', + minTimed: '23:59', + originFk: 'Holand', + }, + ], + }).as('getLack'); + + cy.visit('/#/ticket/negative'); + }); + + describe('Table actions', () => { + it('should display only one row in the lack list', () => { + cy.wait('@getLack', { timeout: 10000 }); + + cy.get('.q-virtual-scroll__content > :nth-child(1) > .sticky').click(); + cy.location('href').should('contain', '#/ticket/negative/5'); + }); + }); +}); diff --git a/test/cypress/integration/ticket/ticketList.spec.js b/test/cypress/integration/ticket/ticketList.spec.js index 2984a4ee4..593021e6e 100644 --- a/test/cypress/integration/ticket/ticketList.spec.js +++ b/test/cypress/integration/ticket/ticketList.spec.js @@ -53,4 +53,29 @@ describe('TicketList', () => { cy.checkNotification('Data created'); cy.url().should('match', /\/ticket\/\d+\/summary/); }); + + it('should show the corerct problems', () => { + cy.intercept('GET', '**/api/Tickets/filter*', (req) => { + req.headers['cache-control'] = 'no-cache'; + req.headers['pragma'] = 'no-cache'; + req.headers['expires'] = '0'; + + req.on('response', (res) => { + delete res.headers['if-none-match']; + delete res.headers['if-modified-since']; + }); + }).as('ticket'); + + cy.get('[data-cy="Warehouse_select"]').type('Warehouse Five'); + cy.get('.q-menu .q-item').contains('Warehouse Five').click(); + cy.wait('@ticket').then((interception) => { + const data = interception.response.body[1]; + expect(data.hasComponentLack).to.equal(1); + expect(data.isTooLittle).to.equal(1); + expect(data.hasItemShortage).to.equal(1); + }); + cy.get('.icon-components').should('exist'); + cy.get('.icon-unavailable').should('exist'); + cy.get('.icon-isTooLittle').should('exist'); + }); }); diff --git a/test/cypress/integration/vnComponent/VnShortcut.spec.js b/test/cypress/integration/vnComponent/VnShortcut.spec.js index b49b4e964..e08c44635 100644 --- a/test/cypress/integration/vnComponent/VnShortcut.spec.js +++ b/test/cypress/integration/vnComponent/VnShortcut.spec.js @@ -28,6 +28,17 @@ describe('VnShortcuts', () => { }); cy.url().should('include', module); + if (['monitor', 'claim'].includes(module)) { + return; + } + cy.waitForElement('.q-page').should('exist'); + cy.dataCy('vnTableCreateBtn').should('exist'); + cy.get('.q-page').trigger('keydown', { + ctrlKey: true, + altKey: true, + key: '+', + }); + cy.get('#formModel').should('exist'); }); } }); diff --git a/test/cypress/integration/zone/zoneBasicData.spec.js b/test/cypress/integration/zone/zoneBasicData.spec.js index 95a075fb3..70ded3f79 100644 --- a/test/cypress/integration/zone/zoneBasicData.spec.js +++ b/test/cypress/integration/zone/zoneBasicData.spec.js @@ -1,5 +1,6 @@ describe('ZoneBasicData', () => { const priceBasicData = '[data-cy="Price_input"]'; + const saveBtn = '.q-btn-group > .q-btn--standard'; beforeEach(() => { cy.viewport(1280, 720); @@ -8,20 +9,27 @@ describe('ZoneBasicData', () => { }); it('should throw an error if the name is empty', () => { - cy.get('[data-cy="zone-basic-data-name"] input').type('{selectall}{backspace}'); - cy.get('.q-btn-group > .q-btn--standard').click(); + cy.intercept('GET', /\/api\/Zones\/4./).as('zone'); + + cy.wait('@zone').then(() => { + cy.get('[data-cy="zone-basic-data-name"] input').type( + '{selectall}{backspace}', + ); + }); + + cy.get(saveBtn).click(); cy.checkNotification("can't be blank"); }); it('should throw an error if the price is empty', () => { cy.get(priceBasicData).clear(); - cy.get('.q-btn-group > .q-btn--standard').click(); + cy.get(saveBtn).click(); cy.checkNotification('cannot be blank'); }); it("should edit the basicData's zone", () => { cy.get('.q-card > :nth-child(1)').type(' modified'); - cy.get('.q-btn-group > .q-btn--standard').click(); + cy.get(saveBtn).click(); cy.checkNotification('Data saved'); }); }); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 92b38dc94..bc8158b62 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -89,36 +89,55 @@ Cypress.Commands.add('getValue', (selector) => { }); // Fill Inputs -Cypress.Commands.add('selectOption', (selector, option, timeout = 5000) => { +Cypress.Commands.add('selectOption', (selector, option, timeout = 2500) => { cy.waitForElement(selector, timeout); - cy.get(selector).click(); - cy.get(selector).invoke('data', 'url').as('dataUrl'); - cy.get(selector) - .clear() - .type(option) - .then(() => { - cy.get('.q-menu', { timeout }) - .should('be.visible') // Asegurarse de que el menú está visible - .and('exist') // Verificar que el menú existe - .then(() => { - cy.get('@dataUrl').then((url) => { - if (url) { - // Esperar a que el menú no esté visible (desaparezca) - cy.get('.q-menu').should('not.be.visible'); - // Ahora esperar a que el menú vuelva a aparecer - cy.get('.q-menu').should('be.visible').and('exist'); - } - }); - }); - }); - // Finalmente, seleccionar la opción deseada - cy.get('.q-menu:visible') // Asegurarse de que estamos dentro del menú visible - .find('.q-item') // Encontrar los elementos de las opciones - .contains(option) // Verificar que existe una opción que contenga el texto deseado - .click(); // Hacer clic en la opción + cy.get(selector, { timeout }) + .should('exist') + .should('be.visible') + .click() + .then(($el) => { + cy.wrap($el.is('input') ? $el : $el.find('input')) + .invoke('attr', 'aria-controls') + .then((ariaControl) => selectItem(selector, option, ariaControl)); + }); }); +function selectItem(selector, option, ariaControl, hasWrite = true) { + if (!hasWrite) cy.wait(100); + + getItems(ariaControl).then((items) => { + const matchingItem = items + .toArray() + .find((item) => item.innerText.includes(option)); + if (matchingItem) return cy.wrap(matchingItem).click(); + + if (hasWrite) cy.get(selector).clear().type(option, { delay: 0 }); + return selectItem(selector, option, ariaControl, false); + }); +} + +function getItems(ariaControl, startTime = Cypress._.now(), timeout = 2500) { + // Se intenta obtener la lista de opciones del desplegable de manera recursiva + return cy + .get('#' + ariaControl, { timeout }) + .should('exist') + .find('.q-item') + .should('exist') + .then(($items) => { + if (!$items?.length || $items.first().text().trim() === '') { + if (Cypress._.now() - startTime > timeout) { + throw new Error( + `getItems: Tiempo de espera (${timeout}ms) excedido.`, + ); + } + return getItems(ariaControl, startTime, timeout); + } + + return cy.wrap($items); + }); +} + Cypress.Commands.add('countSelectOptions', (selector, option) => { cy.waitForElement(selector); cy.get(selector).click({ force: true }); diff --git a/test/cypress/support/waitUntil.js b/test/cypress/support/waitUntil.js index 5fb47a2d8..359f8643f 100644 --- a/test/cypress/support/waitUntil.js +++ b/test/cypress/support/waitUntil.js @@ -1,7 +1,7 @@ const waitUntil = (subject, checkFunction, originalOptions = {}) => { if (!(checkFunction instanceof Function)) { throw new Error( - '`checkFunction` parameter should be a function. Found: ' + checkFunction + '`checkFunction` parameter should be a function. Found: ' + checkFunction, ); } From 0b3e8dedf9a3dfde8e414c12e071d4a8a08f9019 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Tue, 25 Feb 2025 09:56:16 +0100 Subject: [PATCH 28/28] fix: merge revert --- src/components/FormModel.vue | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index c1cd80ce3..182eeaafe 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -307,38 +307,6 @@ async function onKeyup(evt) { } } -async function onKeyup(evt) { - if (evt.key === 'Enter' && !('prevent-submit' in attrs)) { - const input = evt.target; - if (input.type == 'textarea' && evt.shiftKey) { - let { selectionStart, selectionEnd } = input; - input.value = - input.value.substring(0, selectionStart) + - '\n' + - input.value.substring(selectionEnd); - selectionStart = selectionEnd = selectionStart + 1; - return; - } - await save(); - } -} - -async function onKeyup(evt) { - if (evt.key === 'Enter' && !('prevent-submit' in attrs)) { - const input = evt.target; - if (input.type == 'textarea' && evt.shiftKey) { - let { selectionStart, selectionEnd } = input; - input.value = - input.value.substring(0, selectionStart) + - '\n' + - input.value.substring(selectionEnd); - selectionStart = selectionEnd = selectionStart + 1; - return; - } - await save(); - } -} - defineExpose({ save, isLoading,