diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js index cb31391b33d..182c51e4793 100644 --- a/src/boot/qformMixin.js +++ b/src/boot/qformMixin.js @@ -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/components/FormModel.vue b/src/components/FormModel.vue index 633f1254d9e..5681ce11c85 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, @@ -113,7 +114,7 @@ const defaultButtons = computed(() => ({ color: 'primary', icon: 'save', label: 'globals.save', - click: () => myForm.value.onSubmit(false), + click: async () => await save(), type: 'submit', }, reset: { @@ -208,8 +209,7 @@ async function fetch() { } } -async function save(prevent = false) { - if (prevent) return; +async function save() { if ($props.observeFormChanges && !hasChanges.value) return notify('globals.noChanges', 'negative'); @@ -284,6 +284,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, @@ -298,12 +314,12 @@ defineExpose({ <QForm ref="myForm" v-if="formData" - @submit="save(!!$event)" + @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 98b6117431e..672eeff7a23 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -27,10 +27,15 @@ const formModelRef = ref(null); const closeButton = ref(null); const isSaveAndContinue = ref(false); const onDataSaved = (formData, requestResponse) => { - if (closeButton.value && isSaveAndContinue) closeButton.value.click(); + if (closeButton.value && !isSaveAndContinue.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); const reset = computed(() => formModelRef.value?.reset); @@ -78,10 +83,7 @@ defineExpose({ :flat="showSaveAndContinueBtn" :label="t('globals.save')" :title="t('globals.save')" - @click=" - formModelRef.save(); - isSaveAndContinue = false; - " + @click="onClick(false)" color="primary" class="q-ml-sm" :disabled="isLoading" @@ -99,10 +101,7 @@ defineExpose({ :loading="isLoading" data-cy="FormModelPopup_isSaveAndContinue" z-max - @click=" - isSaveAndContinue = true; - formModelRef.save(); - " + @click="onClick(true)" /> </div> </template> diff --git a/src/css/app.scss b/src/css/app.scss index 0c5dc97fa7e..994ae7ff112 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -335,3 +335,7 @@ input::-webkit-inner-spin-button { border: 1px solid; box-shadow: 0 4px 6px #00000000; } + +.containerShrinked { + width: 80%; +} diff --git a/src/pages/Customer/components/CustomerNewPayment.vue b/src/pages/Customer/components/CustomerNewPayment.vue index 7f45cd7db6a..8f61bac89ec 100644 --- a/src/pages/Customer/components/CustomerNewPayment.vue +++ b/src/pages/Customer/components/CustomerNewPayment.vue @@ -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/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue index b38d2749b6f..03013f011b9 100644 --- a/src/pages/Zone/Card/ZoneBasicData.vue +++ b/src/pages/Zone/Card/ZoneBasicData.vue @@ -25,7 +25,7 @@ const setFilteredAddresses = (data) => { @on-fetch="(data) => (validAddresses = data)" /> <FetchData url="Addresses" auto-load @on-fetch="setFilteredAddresses" /> - <FormModel auto-load model="zone"> + <FormModel auto-load model="Zone"> <template #form="{ data, validate }"> <VnRow> <VnInput @@ -33,6 +33,7 @@ const setFilteredAddresses = (data) => { :label="t('Name')" clearable v-model="data.name" + :required="true" /> </VnRow> <VnRow> @@ -83,7 +84,7 @@ const setFilteredAddresses = (data) => { type="number" min="0" /> - <VnInputTime v-model="data.hour" :label="t('Closing')" /> + <VnInputTime v-model="data.hour" :label="t('Closing')" :required="true" /> </VnRow> <VnRow> @@ -92,7 +93,7 @@ const setFilteredAddresses = (data) => { :label="t('Price')" type="number" min="0" - required="true" + :required="true" clearable /> <VnInput @@ -100,7 +101,7 @@ const setFilteredAddresses = (data) => { :label="t('Price optimum')" type="number" min="0" - required="true" + :required="true" clearable /> </VnRow> diff --git a/src/pages/Zone/Card/ZoneCalendar.vue b/src/pages/Zone/Card/ZoneCalendar.vue deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/pages/Zone/ZoneFilterPanel.vue b/src/pages/Zone/ZoneFilterPanel.vue index 3a35527ab6a..bbe12189a22 100644 --- a/src/pages/Zone/ZoneFilterPanel.vue +++ b/src/pages/Zone/ZoneFilterPanel.vue @@ -38,7 +38,12 @@ const agencies = ref([]); <template #body="{ params, searchFn }"> <QItem> <QItemSection> - <VnInput :label="t('list.name')" v-model="params.name" is-outlined /> + <VnInput + :label="t('list.name')" + v-model="params.name" + is-outlined + data-cy="zoneFilterPanelNameInput" + /> </QItemSection> </QItem> <QItem> @@ -53,6 +58,7 @@ const agencies = ref([]); dense outlined rounded + data-cy="zoneFilterPanelAgencySelect" > </VnSelect> </QItemSection> diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue index 1fa539c9107..a82bbb28540 100644 --- a/src/pages/Zone/ZoneList.vue +++ b/src/pages/Zone/ZoneList.vue @@ -65,7 +65,6 @@ const tableFilter = { const columns = computed(() => [ { - align: 'left', name: 'id', label: t('list.id'), chip: { @@ -75,6 +74,8 @@ const columns = computed(() => [ columnFilter: { inWhere: true, }, + columnClass: 'shrink-column', + component: 'number', }, { align: 'left', @@ -106,7 +107,6 @@ const columns = computed(() => [ format: (row, dashIfEmpty) => dashIfEmpty(row?.agencyMode?.name), }, { - align: 'left', name: 'price', label: t('list.price'), cardVisible: true, @@ -114,9 +114,11 @@ const columns = computed(() => [ columnFilter: { inWhere: true, }, + columnClass: 'shrink-column', + component: 'number', }, { - align: 'left', + align: 'center', name: 'hour', label: t('list.close'), cardVisible: true, @@ -129,6 +131,7 @@ const columns = computed(() => [ label: t('list.addressFk'), cardVisible: true, columnFilter: false, + columnClass: 'expand', }, { align: 'right', @@ -177,67 +180,73 @@ function formatRow(row) { <ZoneFilterPanel data-key="ZonesList" /> </template> </RightMenu> - <VnTable - ref="tableRef" - data-key="ZonesList" - url="Zones" - :create="{ - urlCreate: 'Zones', - title: t('list.createZone'), - onDataSaved: ({ id }) => tableRef.redirect(`${id}/location`), - formInitialData: {}, - }" - :user-filter="tableFilter" - :columns="columns" - redirect="zone" - :right-search="false" - > - <template #column-addressFk="{ row }"> - {{ dashIfEmpty(formatRow(row)) }} - </template> - <template #more-create-dialog="{ data }"> - <VnSelect - url="AgencyModes" - v-model="data.agencyModeFk" - option-value="id" - option-label="name" - :label="t('list.agency')" - /> - <VnInput - v-model="data.price" - :label="t('list.price')" - min="0" - type="number" - required="true" - /> - <VnInput - v-model="data.bonus" - :label="t('zone.bonus')" - min="0" - type="number" - /> - <VnInput - v-model="data.travelingDays" - :label="t('zone.travelingDays')" - type="number" - min="0" - /> - <VnInputTime v-model="data.hour" :label="t('list.close')" /> - <VnSelect - url="Warehouses" - v-model="data.warehouseFK" - option-value="id" - option-label="name" - :label="t('list.warehouse')" - :options="warehouseOptions" - /> - <QCheckbox - v-model="data.isVolumetric" - :label="t('list.isVolumetric')" - :toggle-indeterminate="false" - /> - </template> - </VnTable> + <div class="table-container"> + <div class="column items-center"> + <VnTable + ref="tableRef" + data-key="ZonesList" + url="Zones" + :create="{ + urlCreate: 'Zones', + title: t('list.createZone'), + onDataSaved: ({ id }) => tableRef.redirect(`${id}/location`), + formInitialData: {}, + }" + :user-filter="tableFilter" + :columns="columns" + redirect="zone" + :right-search="false" + table-height="85vh" + order="id ASC" + > + <template #column-addressFk="{ row }"> + {{ dashIfEmpty(formatRow(row)) }} + </template> + <template #more-create-dialog="{ data }"> + <VnSelect + url="AgencyModes" + v-model="data.agencyModeFk" + option-value="id" + option-label="name" + :label="t('list.agency')" + /> + <VnInput + v-model="data.price" + :label="t('list.price')" + min="0" + type="number" + required="true" + /> + <VnInput + v-model="data.bonus" + :label="t('zone.bonus')" + min="0" + type="number" + /> + <VnInput + v-model="data.travelingDays" + :label="t('zone.travelingDays')" + type="number" + min="0" + /> + <VnInputTime v-model="data.hour" :label="t('list.close')" /> + <VnSelect + url="Warehouses" + v-model="data.warehouseFK" + option-value="id" + option-label="name" + :label="t('list.warehouse')" + :options="warehouseOptions" + /> + <QCheckbox + v-model="data.isVolumetric" + :label="t('list.isVolumetric')" + :toggle-indeterminate="false" + /> + </template> + </VnTable> + </div> + </div> </template> <i18n> @@ -245,3 +254,20 @@ 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> diff --git a/src/pages/Zone/ZoneUpcoming.vue b/src/pages/Zone/ZoneUpcoming.vue index c74ae6078f6..adcdfbc0447 100644 --- a/src/pages/Zone/ZoneUpcoming.vue +++ b/src/pages/Zone/ZoneUpcoming.vue @@ -56,7 +56,7 @@ onMounted(() => weekdayStore.initStore()); <ZoneSearchbar /> <VnSubToolbar /> <QPage class="column items-center q-pa-md"> - <QCard class="full-width q-pa-md"> + <QCard class="containerShrinked q-pa-md"> <div v-for="(detail, index) in details" :key="index" diff --git a/src/pages/Zone/locale/en.yml b/src/pages/Zone/locale/en.yml index 5fd1a3ea752..e53e7b560af 100644 --- a/src/pages/Zone/locale/en.yml +++ b/src/pages/Zone/locale/en.yml @@ -44,6 +44,8 @@ summary: filterPanel: name: Name agencyModeFk: Agency + id: ID + price: Price deliveryPanel: pickup: Pick up delivery: Delivery diff --git a/src/pages/Zone/locale/es.yml b/src/pages/Zone/locale/es.yml index 575b12f7af9..bc31e74a957 100644 --- a/src/pages/Zone/locale/es.yml +++ b/src/pages/Zone/locale/es.yml @@ -45,6 +45,8 @@ summary: filterPanel: name: Nombre agencyModeFk: Agencia + id: ID + price: Precio deliveryPanel: pickup: Recogida delivery: Entrega diff --git a/test/cypress/integration/zone/zoneList.spec.js b/test/cypress/integration/zone/zoneList.spec.js index 8d01d4e4e8f..68e92463540 100644 --- a/test/cypress/integration/zone/zoneList.spec.js +++ b/test/cypress/integration/zone/zoneList.spec.js @@ -1,4 +1,5 @@ describe('ZoneList', () => { + const agency = 'inhouse pickup'; beforeEach(() => { cy.viewport(1280, 720); cy.login('developer'); @@ -6,11 +7,15 @@ describe('ZoneList', () => { }); it('should filter by agency', () => { - cy.get('input[aria-label="Agency"]').type('{downArrow}{enter}'); + cy.dataCy('zoneFilterPanelNameInput').type('{downArrow}{enter}'); }); it('should open the zone summary', () => { - cy.get('input[aria-label="Name"]').type('zone refund'); - cy.get('.q-scrollarea__content > .q-btn--standard > .q-btn__content').click(); + cy.dataCy('zoneFilterPanelAgencySelect').type(agency); + cy.get('.q-menu .q-item').contains(agency).click(); + cy.get(':nth-child(1) > [data-col-field="agencyModeFk"]').should( + 'include.text', + agency, + ); }); });