diff --git a/CHANGELOG.md b/CHANGELOG.md index 03812d252..e110e4cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,87 @@ +# Version 24.50 - 2024-12-10 + +### Added 🆕 + +- feat: add reportFileName option by:Javier Segarra +- feat: all clients just with global series by:jgallego +- feat: improve Merge branch 'test' into dev by:Javier Segarra +- feat: manual invoice in two lines by:jgallego +- feat: manualInvoice with address by:jgallego +- feat: randomize functions and example by:Javier Segarra +- feat: refs #6999 added search when user tabs on a filter with value by:Jon +- feat: refs #6999 added tab to search in VnTable filter by:Jon +- feat: refs #7346 #7346 improve form by:Javier Segarra +- feat: refs #7346 address ordered by:jgallego +- feat: refs #7346 radioButton by:jgallego +- feat: refs #7346 style radioButton by:jgallego +- feat: refs #7346 traducciones en cammelCase (7346-manualInvoice) by:jgallego +- feat: refs #8038 added new functionality in VnSelect and refactor styles by:Jon +- feat: refs #8061 #8061 updates by:Javier Segarra +- feat: refs #8087 reactive data by:jorgep +- feat: refs #8087 refs#8087 Redadas en travel by:Carlos Andrés +- feat: refs #8138 add component ticket problems by:pablone +- feat: refs #8163 add max length and more tests by:wbuezas +- feat: refs #8163 add prop by:wbuezas +- feat: refs #8163 add VnInput insert functionality and e2e test by:wbuezas +- feat: refs #8163 limit with maxLength by:Javier Segarra +- feat: refs #8163 maxLength SupplierFD account by:Javier Segarra +- feat: refs #8163 maxLengthVnInput by:Javier Segarra +- feat: refs #8163 use VnAccountNumber in VnAccountNumber by:Javier Segarra +- feat: refs #8166 show notification by:jorgep + +### Changed 📦 + +- feat: refs #8038 added new functionality in VnSelect and refactor styles by:Jon +- perf: add dataCy by:Javier Segarra +- perf: refs #7346 #7346 Imrpove interface dialog by:Javier Segarra +- perf: refs #7346 #7346 use v-show instead v-if by:Javier Segarra +- perf: refs #8036 currentFilter by:alexm +- perf: refs #8061 filter autonomy by:Javier Segarra +- perf: refs #8061 solve conflicts and random posCode it by:Javier Segarra +- perf: refs #8061 use opts from VnSelect by:Javier Segarra +- perf: refs #8163 #8061 createNewPostCodeForm by:Javier Segarra +- perf: remove console by:Javier Segarra +- perf: remove timeout by:Javier Segarra +- perf: test command fillInForm by:Javier Segarra +- refactor: refs #8162 remove comment by:wbuezas +- refactor: remove unnecesary things by:wbuezas + +### Fixed 🛠️ + +- fix: #8016 fetching data by:Javier Segarra +- fix: icons by:jgallego +- fix: refs #7229 download file by:jorgep +- fix: refs #7229 remove catch by:jorgep +- fix: refs #7229 set url by:jorgep +- fix: refs #7229 test by:jorgep +- fix: refs #7229 url by:jorgep +- fix: refs #7229 url + test by:jorgep +- fix: refs #7304 7304 clean warning by:carlossa +- fix: refs #7304 fix list by:carlossa +- fix: refs #7304 fix warning by:carlossa +- fix: refs #7346 traslations by:jgallego +- fix: refs #7529 add save by:carlossa +- fix: refs #7529 fix e2e by:carlossa +- fix: refs #7529 fix front by:carlossa +- fix: refs #7529 fix scss by:carlossa +- fix: refs #7529 fix te2e by:carlossa +- fix: refs #7529 fix workerPit e2e by:carlossa +- fix: refs #7529 front by:carlossa +- fix: refs #8036 apply exprBuilder after save filters by:alexm +- fix: refs #8036 only add where when required by:alexm +- fix: refs #8038 solve conflicts by:Jon +- fix: refs #8061 improve code dependencies (origin/8061_improve_newCP) by:Javier Segarra +- fix: refs #8138 move component from ui folder by:pablone +- fix: refs #8138 sme minor issues by:pablone +- fix: refs #8163 #8061 createNewPostCodeForm by:Javier Segarra +- fix: refs #8163 minor problem when keypress by:Javier Segarra +- fix: refs #8166 show zone error by:jorgep +- fix: removed selectedClient by:jgallego +- refs #7529 fix workerPit by:carlossa +- revert: refs #8061 test #8061 updates by:Javier Segarra +- test: fix own test by:Javier Segarra +- test: refs #8162 #8162 fix TicketList spec by:Javier Segarra + # Version 24.48 - 2024-11-25 ### Added 🆕 diff --git a/Jenkinsfile b/Jenkinsfile index 1766e3aea..c20da8ab2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,7 +4,8 @@ def PROTECTED_BRANCH def BRANCH_ENV = [ test: 'test', - master: 'production' + master: 'production', + beta: 'production' ] node { @@ -15,7 +16,8 @@ node { PROTECTED_BRANCH = [ 'dev', 'test', - 'master' + 'master', + 'beta' ].contains(env.BRANCH_NAME) // https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables diff --git a/package.json b/package.json index 04b75a0b0..39d49519b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "24.50.0", + "version": "24.52.0", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", @@ -64,4 +64,4 @@ "vite": "^5.1.4", "vitest": "^0.31.1" } -} \ No newline at end of file +} diff --git a/src/boot/mainShortcutMixin.js b/src/boot/mainShortcutMixin.js new file mode 100644 index 000000000..481077e37 --- /dev/null +++ b/src/boot/mainShortcutMixin.js @@ -0,0 +1,36 @@ +import routes from 'src/router/modules'; +import { useRouter } from 'vue-router'; + +let isNotified = false; + +export default { + created: function () { + const router = useRouter(); + const keyBindingMap = routes + .filter((route) => route.meta.keyBinding) + .reduce((map, route) => { + map['Key' + route.meta.keyBinding.toUpperCase()] = route.path; + return map; + }, {}); + + const handleKeyDown = (event) => { + const { ctrlKey, altKey, code } = event; + + if (ctrlKey && altKey && keyBindingMap[code] && !isNotified) { + event.preventDefault(); + router.push(keyBindingMap[code]); + isNotified = true; + } + }; + + const handleKeyUp = (event) => { + const { ctrlKey, altKey } = event; + if (!ctrlKey || !altKey) { + isNotified = false; + } + }; + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + }, +}; diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js index fc7852369..8a75e1af7 100644 --- a/src/boot/qformMixin.js +++ b/src/boot/qformMixin.js @@ -1,30 +1,52 @@ -import { getCurrentInstance } from 'vue'; - +function focusFirstInput(input) { + input.focus(); + return; +} export default { mounted: function () { - const vm = getCurrentInstance(); - if (vm.type.name === 'QForm') { - if (!['searchbarForm', 'filterPanelForm'].includes(this.$el?.id)) { - // TODO: AUTOFOCUS IS NOT FOCUSING - const that = this; - this.$el.addEventListener('keyup', function (evt) { - if (evt.key === 'Enter') { - const input = evt.target; - if (input.type == 'textarea' && evt.shiftKey) { - evt.preventDefault(); - let { selectionStart, selectionEnd } = input; - input.value = - input.value.substring(0, selectionStart) + - '\n' + - input.value.substring(selectionEnd); - selectionStart = selectionEnd = selectionStart + 1; - return; - } - evt.preventDefault(); - that.onSubmit(); - } - }); + const that = this; + + const form = document.querySelector('.q-form#formModel'); + if (!form) return; + try { + const inputsFormCard = form.querySelectorAll( + `input:not([disabled]):not([type="checkbox"])` + ); + if (inputsFormCard.length) { + focusFirstInput(inputsFormCard[0]); } + const textareas = document.querySelectorAll( + 'textarea:not([disabled]), [contenteditable]:not([disabled])' + ); + if (textareas.length) { + focusFirstInput(textareas[textareas.length - 1]); + } + const inputs = document.querySelectorAll( + 'form#formModel input:not([disabled]):not([type="checkbox"])' + ); + const input = inputs[0]; + if (!input) return; + + focusFirstInput(input); + } catch (error) { + console.error(error); } + form.addEventListener('keyup', function (evt) { + if (evt.key === 'Enter') { + 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 01fe68d8b..547517682 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -1,15 +1,18 @@ +import axios from 'axios'; import { boot } from 'quasar/wrappers'; import qFormMixin from './qformMixin'; import keyShortcut from './keyShortcut'; -import useNotify from 'src/composables/useNotify.js'; -import { CanceledError } from 'axios'; - -const { notify } = useNotify(); +import { QForm } from 'quasar'; +import { QLayout } from 'quasar'; +import mainShortcutMixin from './mainShortcutMixin'; +import { useCau } from 'src/composables/useCau'; export default boot(({ app }) => { - app.mixin(qFormMixin); + QForm.mixins = [qFormMixin]; + QLayout.mixins = [mainShortcutMixin]; + app.directive('shortcut', keyShortcut); - app.config.errorHandler = (error) => { + app.config.errorHandler = async (error) => { let message; const response = error.response; const responseData = response?.data; @@ -40,12 +43,12 @@ export default boot(({ app }) => { } console.error(error); - if (error instanceof CanceledError) { + if (error instanceof axios.CanceledError) { const env = process.env.NODE_ENV; if (env && env !== 'development') return; message = 'Duplicate request'; } - notify(message ?? 'globals.error', 'negative', 'error'); + await useCau(response, message); }; }); diff --git a/src/components/CreateNewPostcodeForm.vue b/src/components/CreateNewPostcodeForm.vue index d3d6708f0..c656fcb2f 100644 --- a/src/components/CreateNewPostcodeForm.vue +++ b/src/components/CreateNewPostcodeForm.vue @@ -25,7 +25,6 @@ const townsFetchDataRef = ref(false); const townFilter = ref({}); const countriesRef = ref(false); -const provincesFetchDataRef = ref(false); const provincesOptions = ref([]); const townsOptions = ref([]); const town = ref({}); @@ -71,9 +70,6 @@ async function setProvince(id, data) { await fetchTowns(); } async function onProvinceCreated(data) { - await provincesFetchDataRef.value.fetch({ - where: { countryFk: postcodeFormData.countryFk }, - }); postcodeFormData.provinceFk = data.id; } function provinceByCountry(countryFk = postcodeFormData.countryFk) { @@ -92,7 +88,6 @@ function setTown(newTown, data) { data.countryFk = newTown?.province?.countryFk ?? newTown; } async function onCityCreated(newTown, formData) { - await provincesFetchDataRef.value.fetch(); newTown.province = provincesOptions.value.find( (province) => province.id === newTown.provinceFk ); @@ -125,14 +120,6 @@ async function filterTowns(name) { </script> <template> - <FetchData - ref="provincesFetchDataRef" - @on-fetch="handleProvinces" - :sort-by="['name ASC']" - :limit="30" - auto-load - url="Provinces/location" - /> <FetchData ref="townsFetchDataRef" :sort-by="['name ASC']" @@ -205,6 +192,11 @@ async function filterTowns(name) { :country-fk="data.countryFk" :province-selected="data.provinceFk" @update:model-value="(value) => setProvince(value, data)" + @update:options=" + (data) => { + provincesOptions = data; + } + " v-model="data.provinceFk" @on-province-created="onProvinceCreated" required diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index eff4c9095..e2608d86f 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -250,7 +250,7 @@ function getChanges() { for (const [i, row] of formData.value.entries()) { if (!row[pk]) { creates.push(row); - } else if (originalData.value) { + } else if (originalData.value[i]) { const data = getDifferences(originalData.value[i], row); if (!isEmpty(data)) { updates.push({ diff --git a/src/components/EditTableCellValueForm.vue b/src/components/EditTableCellValueForm.vue index 7755df9ab..172866191 100644 --- a/src/components/EditTableCellValueForm.vue +++ b/src/components/EditTableCellValueForm.vue @@ -85,12 +85,14 @@ const closeForm = () => { hide-selected option-label="label" v-model="selectedField" + data-cy="field-to-edit" /> <component :is="inputs[selectedField?.component || 'input']" v-bind="selectedField?.attrs || {}" v-model="newValue" :label="t('Value')" + data-cy="value-to-edit" style="width: 200px" /> </VnRow> diff --git a/src/components/FormModelPopup.vue b/src/components/FormModelPopup.vue index d91f07535..afdc6efca 100644 --- a/src/components/FormModelPopup.vue +++ b/src/components/FormModelPopup.vue @@ -62,6 +62,7 @@ defineExpose({ @click="emit('onDataCanceled')" v-close-popup data-cy="FormModelPopup_cancel" + z-max /> <QBtn :label="t('globals.save')" @@ -72,6 +73,7 @@ defineExpose({ :disabled="isLoading" :loading="isLoading" data-cy="FormModelPopup_save" + z-max /> </div> </template> diff --git a/src/components/ItemsFilterPanel.vue b/src/components/ItemsFilterPanel.vue index 405577095..084feb377 100644 --- a/src/components/ItemsFilterPanel.vue +++ b/src/components/ItemsFilterPanel.vue @@ -9,6 +9,8 @@ import VnSelect from 'components/common/VnSelect.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import axios from 'axios'; +import { getParamWhere } from 'src/filters'; +import { useRoute } from 'vue-router'; const { t } = useI18n(); const props = defineProps({ @@ -26,28 +28,21 @@ const props = defineProps({ }, }); -const itemCategories = ref([]); -const selectedCategoryFk = ref(null); -const selectedTypeFk = ref(null); +const route = useRoute(); + const itemTypesOptions = ref([]); const suppliersOptions = ref([]); const tagOptions = ref([]); const tagValues = ref([]); +const categoryList = ref(null); +const selectedCategoryFk = ref(getParamWhere(route.query.table, 'categoryFk', false)); +const selectedTypeFk = ref(getParamWhere(route.query.table, 'typeFk', false)); -const categoryList = computed(() => { - return (itemCategories.value || []) - .filter((category) => category.display) - .map((category) => ({ - ...category, - icon: `vn:${(category.icon || '').split('-')[1]}`, - })); -}); - -const selectedCategory = computed(() => - (itemCategories.value || []).find( +const selectedCategory = computed(() => { + return (categoryList.value || []).find( (category) => category?.id === selectedCategoryFk.value - ) -); + ); +}); const selectedType = computed(() => { return (itemTypesOptions.value || []).find( @@ -87,7 +82,7 @@ const applyTags = (params, search) => { search(); }; -const fetchItemTypes = async (id) => { +const fetchItemTypes = async (id = selectedCategoryFk.value) => { const filter = { fields: ['id', 'name', 'categoryFk'], where: { categoryFk: id }, @@ -126,15 +121,19 @@ const removeTag = (index, params, search) => { (tagValues.value || []).splice(index, 1); applyTags(params, search); }; +const setCategoryList = (data) => { + categoryList.value = (data || []) + .filter((category) => category.display) + .map((category) => ({ + ...category, + icon: `vn:${(category.icon || '').split('-')[1]}`, + })); + fetchItemTypes(); +}; </script> <template> - <FetchData - url="ItemCategories" - limit="30" - auto-load - @on-fetch="(data) => (itemCategories = data)" - /> + <FetchData url="ItemCategories" limit="30" auto-load @on-fetch="setCategoryList" /> <FetchData url="Suppliers" limit="30" diff --git a/src/components/LeftMenu.vue b/src/components/LeftMenu.vue index ab2931dfd..31ad9ebed 100644 --- a/src/components/LeftMenu.vue +++ b/src/components/LeftMenu.vue @@ -177,6 +177,7 @@ function normalize(text) { class="full-width" filled dense + autofocus /> </QItem> <QSeparator /> diff --git a/src/components/VnSelectProvince.vue b/src/components/VnSelectProvince.vue index 7d1297abf..d73ee964e 100644 --- a/src/components/VnSelectProvince.vue +++ b/src/components/VnSelectProvince.vue @@ -7,7 +7,7 @@ import VnSelectDialog from 'components/common/VnSelectDialog.vue'; import FetchData from 'components/FetchData.vue'; import CreateNewProvinceForm from './CreateNewProvinceForm.vue'; -const emit = defineEmits(['onProvinceCreated', 'onProvinceFetched']); +const emit = defineEmits(['onProvinceCreated', 'onProvinceFetched', 'update:options']); const $props = defineProps({ countryFk: { type: Number, @@ -41,6 +41,7 @@ async function onProvinceCreated(_, data) { } async function handleProvinces(data) { provincesOptions.value = data; + emit('update:options', data); } watch( diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 5bc634022..52a9e3fed 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -618,6 +618,7 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) { $props.rowClick && $props.rowClick(row); } " + style="height: 100%" > <QCardSection vertical diff --git a/src/components/common/VnLocation.vue b/src/components/common/VnLocation.vue index af273150e..a8840f243 100644 --- a/src/components/common/VnLocation.vue +++ b/src/components/common/VnLocation.vue @@ -2,7 +2,7 @@ import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue'; import VnSelectDialog from 'components/common/VnSelectDialog.vue'; import { useI18n } from 'vue-i18n'; -import { ref } from 'vue'; +import { computed } from 'vue'; import { useAttrs } from 'vue'; import { useRequired } from 'src/composables/useRequired'; const { t } = useI18n(); @@ -43,7 +43,7 @@ const formatLocation = (obj, properties) => { return filteredParts.join(', '); }; -const modelValue = ref( +const modelValue = computed(() => props.location ? formatLocation(props.location, locationProperties) : null ); diff --git a/src/components/common/VnLog.vue b/src/components/common/VnLog.vue index 8c71c0997..9eca3c711 100644 --- a/src/components/common/VnLog.vue +++ b/src/components/common/VnLog.vue @@ -238,6 +238,7 @@ async function openPointRecord(id, modelLog) { pointRecord.value = parseProps(propNames, locale, data); } async function setLogTree(data) { + if (!data) return; logTree.value = getLogTree(data); } diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index e116be32a..e5ac05231 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -268,7 +268,7 @@ async function onScroll({ to, direction, from, index }) { defineExpose({ opts: myOptions }); function handleKeyDown(event) { - if (event.key === 'Tab') { + if (event.key === 'Tab' && !event.shiftKey) { event.preventDefault(); const inputValue = vnSelectRef.value?.inputValue; @@ -286,6 +286,17 @@ function handleKeyDown(event) { } vnSelectRef.value?.hidePopup(); } + + const focusableElements = document.querySelectorAll( + 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])' + ); + const currentIndex = Array.prototype.indexOf.call( + focusableElements, + event.target + ); + if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) { + focusableElements[currentIndex + 1].focus(); + } } } </script> diff --git a/src/components/common/VnSelectWorker.vue b/src/components/common/VnSelectWorker.vue new file mode 100644 index 000000000..b0fef4443 --- /dev/null +++ b/src/components/common/VnSelectWorker.vue @@ -0,0 +1,85 @@ +<script setup> +import { computed, useAttrs } from 'vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import VnAvatar from 'src/components/ui/VnAvatar.vue'; + +const emit = defineEmits(['update:modelValue']); +const $props = defineProps({ + hasAvatar: { + type: Boolean, + default: false, + }, + hasInfo: { + type: Boolean, + default: false, + }, + modelValue: { + type: [String, Number, Object], + default: null, + }, +}); + +const $attrs = useAttrs(); + +const value = computed({ + get() { + return $props.modelValue; + }, + set(val) { + emit('update:modelValue', val); + }, +}); + +const url = computed(() => { + let url = 'Workers/search'; + const { departmentCodes } = $attrs.params ?? {}; + if (!departmentCodes) return url; + const params = new URLSearchParams({ + departmentCodes: JSON.stringify(departmentCodes), + }); + + return url.concat(`?${params.toString()}`); +}); +</script> + +<template> + <VnSelect + :label="$t('globals.worker')" + v-bind="$attrs" + v-model="value" + :url="url" + option-value="id" + option-label="nickname" + :fields="['id', 'name', 'nickname', 'code']" + sort-by="nickname ASC" + > + <template #prepend v-if="$props.hasAvatar"> + <VnAvatar :worker-id="value" color="primary" :title="title" /> + </template> + <template #append v-if="$props.hasInfo"> + <QIcon name="info" class="cursor-pointer"> + <QTooltip>{{ $t($props.hasInfo) }}</QTooltip> + </QIcon> + </template> + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt.name }} + </QItemLabel> + <QItemLabel v-if="!scope.opt.id"> + {{ scope.opt.nickname }} + </QItemLabel> + <QItemLabel caption v-else> + {{ scope.opt.nickname }}, {{ scope.opt.code }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> +</template> + +<i18n> +es: + Responsible for approving invoices: Responsable de aprobar las facturas +</i18n> diff --git a/src/components/ui/CatalogItem.vue b/src/components/ui/CatalogItem.vue index 7dca19770..74a36ff2e 100644 --- a/src/components/ui/CatalogItem.vue +++ b/src/components/ui/CatalogItem.vue @@ -67,7 +67,7 @@ const dialog = ref(null); <QTooltip>{{ t('globals.add') }}</QTooltip> <QPopupProxy ref="dialog"> <OrderCatalogItemDialog - :prices="item.prices" + :item="item" @added="() => dialog.hide()" /> </QPopupProxy> diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index 8c0dbda94..67f6f387b 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -61,6 +61,7 @@ const emit = defineEmits([ 'update:modelValue', 'refresh', 'clear', + 'search', 'init', 'remove', 'setUserParams', @@ -78,7 +79,7 @@ const userParams = ref({}); defineExpose({ search, sanitizer, params: userParams }); onMounted(() => { - userParams.value = $props.modelValue ?? {}; + if (!userParams.value) userParams.value = $props.modelValue ?? {}; emit('init', { params: userParams.value }); }); @@ -104,7 +105,8 @@ watch( watch( () => arrayData.store.userParams, - (val, oldValue) => (val || oldValue) && setUserParams(val) + (val, oldValue) => (val || oldValue) && setUserParams(val), + { immediate: true } ); watch( diff --git a/src/components/ui/VnNotes.vue b/src/components/ui/VnNotes.vue index bcbf0945e..e308ea9bb 100644 --- a/src/components/ui/VnNotes.vue +++ b/src/components/ui/VnNotes.vue @@ -6,7 +6,6 @@ import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; import { toDateHourMin } from 'src/filters'; -import { useState } from 'src/composables/useState'; import VnPaginate from 'components/ui/VnPaginate.vue'; import VnUserLink from 'components/ui/VnUserLink.vue'; @@ -26,9 +25,7 @@ const $props = defineProps({ }); const { t } = useI18n(); -const state = useState(); const quasar = useQuasar(); -const currentUser = ref(state.getUser()); const newNote = reactive({ text: null, observationTypeFk: null }); const observationTypes = ref([]); const vnPaginateRef = ref(); diff --git a/src/composables/useCau.js b/src/composables/useCau.js new file mode 100644 index 000000000..29319bd9a --- /dev/null +++ b/src/composables/useCau.js @@ -0,0 +1,73 @@ +import VnInput from 'src/components/common/VnInput.vue'; +import { useVnConfirm } from 'src/composables/useVnConfirm'; +import axios from 'axios'; +import { ref } from 'vue'; +import { i18n } from 'src/boot/i18n'; +import useNotify from 'src/composables/useNotify.js'; + +export async function useCau(res, message) { + const { notify } = useNotify(); + const { openConfirmationModal } = useVnConfirm(); + const { config, headers, request, status, statusText, data } = res || {}; + const { params, url, method, signal, headers: confHeaders } = config || {}; + const { message: resMessage, code, name } = data?.error || {}; + + const additionalData = { + path: location.hash, + message: resMessage, + code, + request: request?.responseURL, + status, + name, + statusText: statusText, + config: { + url, + method, + params, + headers: confHeaders, + aborted: signal?.aborted, + version: headers?.['salix-version'], + }, + }; + const opts = { + actions: [ + { + icon: 'support_agent', + color: 'primary', + dense: true, + flat: false, + round: true, + handler: async () => { + const locale = i18n.global.t; + const reason = ref( + code == 'ACCESS_DENIED' ? locale('cau.askPrivileges') : '' + ); + openConfirmationModal( + locale('cau.title'), + locale('cau.subtitle'), + async () => { + await axios.post('OsTickets/send-to-support', { + reason: reason.value, + additionalData, + }); + }, + null, + { + component: VnInput, + props: { + modelValue: reason, + 'onUpdate:modelValue': (val) => (reason.value = val), + label: locale('cau.inputLabel'), + class: 'full-width', + required: true, + autofocus: true, + }, + } + ); + }, + }, + ], + }; + + notify(message ?? 'globals.error', 'negative', 'error', opts); +} diff --git a/src/composables/useNotify.js b/src/composables/useNotify.js index 2f0e1c257..309156d2a 100644 --- a/src/composables/useNotify.js +++ b/src/composables/useNotify.js @@ -2,7 +2,7 @@ import { Notify } from 'quasar'; import { i18n } from 'src/boot/i18n'; export default function useNotify() { - const notify = (message, type, icon) => { + const notify = (message, type, icon, opts = {}) => { const defaultIcons = { warning: 'warning', negative: 'error', @@ -13,6 +13,7 @@ export default function useNotify() { message: i18n.global.t(message), type: type, icon: icon ? icon : defaultIcons[type], + ...opts, }); }; diff --git a/src/composables/useRole.js b/src/composables/useRole.js index d1a6d6ef3..3ec65dd0a 100644 --- a/src/composables/useRole.js +++ b/src/composables/useRole.js @@ -20,7 +20,7 @@ export function useRole() { function hasAny(roles) { const roleStore = state.getRoles(); - + if (typeof roles === 'string') roles = [roles]; for (const role of roles) { if (roleStore.value.indexOf(role) !== -1) return true; } diff --git a/src/composables/useVnConfirm.js b/src/composables/useVnConfirm.js index 76c3f4f28..4438ad11d 100644 --- a/src/composables/useVnConfirm.js +++ b/src/composables/useVnConfirm.js @@ -1,22 +1,29 @@ +import { h } from 'vue'; +import { Dialog } from 'quasar'; import VnConfirm from 'components/ui/VnConfirm.vue'; -import { useQuasar } from 'quasar'; export function useVnConfirm() { - const quasar = useQuasar(); - - const openConfirmationModal = (title, message, promise, successFn) => { - quasar - .dialog({ - component: VnConfirm, - componentProps: { + const openConfirmationModal = ( + title, + message, + promise, + successFn, + customHTML = {} + ) => { + const { component, props } = customHTML; + Dialog.create({ + component: h( + VnConfirm, + { title: title, message: message, promise: promise, }, - }) - .onOk(async () => { - if (successFn) successFn(); - }); + { customHTML: () => h(component, props) } + ), + }).onOk(async () => { + if (successFn) successFn(); + }); }; return { openConfirmationModal }; diff --git a/src/filters/getParamWhere.js b/src/filters/getParamWhere.js index ef00a93ae..baba46f69 100644 --- a/src/filters/getParamWhere.js +++ b/src/filters/getParamWhere.js @@ -1,4 +1,3 @@ -// parsing JSON safely function parseJSON(str, fallback) { try { return JSON.parse(str ?? '{}'); diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index b4765a20d..cea5c76ad 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -332,6 +332,7 @@ globals: fi: FI myTeam: My team departmentFk: Department + countryFk: Country changePass: Change password deleteConfirmTitle: Delete selected elements changeState: Change state @@ -369,6 +370,11 @@ resetPassword: repeatPassword: Repeat password passwordNotMatch: Passwords don't match passwordChanged: Password changed +cau: + title: Send 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 @@ -398,8 +404,8 @@ entry: buys: Buys stickers: Stickers package: Package - packing: Packing - grouping: Grouping + packing: Pack. + grouping: Group. buyingValue: Buying value import: Import pvp: PVP diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index be2b76a06..289e514bb 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -336,6 +336,7 @@ globals: SSN: NSS fi: NIF myTeam: Mi equipo + countryFk: País changePass: Cambiar contraseña deleteConfirmTitle: Eliminar los elementos seleccionados changeState: Cambiar estado @@ -371,6 +372,11 @@ resetPassword: repeatPassword: Repetir contraseña passwordNotMatch: Las contraseñas no coinciden passwordChanged: Contraseña cambiada +cau: + title: Enviar 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 @@ -401,8 +407,8 @@ entry: buys: Compras stickers: Etiquetas package: Embalaje - packing: Packing - grouping: Grouping + packing: Pack. + grouping: Group. buyingValue: Coste import: Importe pvp: PVP diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 754b084fc..2a84e5aa1 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -1,50 +1,10 @@ <script setup> -import { useQuasar } from 'quasar'; import Navbar from 'src/components/NavBar.vue'; -import { useRouter } from 'vue-router'; -import routes from 'src/router/modules'; -import { onMounted } from 'vue'; - -const quasar = useQuasar(); - -onMounted(() => { - let isNotified = false; - - const router = useRouter(); - const keyBindingMap = routes - .filter((route) => route.meta.keyBinding) - .reduce((map, route) => { - map['Key' + route.meta.keyBinding.toUpperCase()] = route.path; - return map; - }, {}); - - const handleKeyDown = (event) => { - const { ctrlKey, altKey, code } = event; - - if (ctrlKey && altKey && keyBindingMap[code] && !isNotified) { - event.preventDefault(); - router.push(keyBindingMap[code]); - isNotified = true; - } - }; - - const handleKeyUp = (event) => { - const { ctrlKey, altKey } = event; - - if (!ctrlKey || !altKey) { - isNotified = false; - } - }; - - window.addEventListener('keydown', handleKeyDown); - window.addEventListener('keyup', handleKeyUp); -}); </script> - <template> <QLayout view="hHh LpR fFf" v-shortcut> <Navbar /> <RouterView></RouterView> - <QFooter v-if="quasar.platform.is.mobile"></QFooter> + <QFooter v-if="$q.platform.is.mobile"></QFooter> </QLayout> </template> diff --git a/src/pages/Account/AccountFilter.vue b/src/pages/Account/AccountFilter.vue index 46fac875a..50c3ee1ac 100644 --- a/src/pages/Account/AccountFilter.vue +++ b/src/pages/Account/AccountFilter.vue @@ -31,7 +31,6 @@ const rolesOptions = ref([]); <VnFilterPanel :data-key="props.dataKey" :search-button="true" - :hidden-tags="['search']" :redirect="false" search-url="table" > diff --git a/src/pages/Account/AccountList.vue b/src/pages/Account/AccountList.vue index cbaaf8e26..341dd92a2 100644 --- a/src/pages/Account/AccountList.vue +++ b/src/pages/Account/AccountList.vue @@ -7,6 +7,7 @@ import AccountSummary from './Card/AccountSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import AccountFilter from './AccountFilter.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; +import VnInput from 'src/components/common/VnInput.vue'; const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const tableRef = ref(); @@ -22,10 +23,27 @@ const columns = computed(() => [ field: 'id', cardVisible: true, }, + { + align: 'left', + name: 'name', + label: t('Name'), + component: 'input', + columnField: { + component: null, + }, + cardVisible: true, + create: true, + }, { align: 'left', name: 'roleFk', - label: t('role'), + label: t('Role'), + component: 'select', + attrs: { + url: 'VnRoles', + optionValue: 'id', + optionLabel: 'name', + }, columnFilter: { component: 'select', name: 'roleFk', @@ -35,7 +53,11 @@ const columns = computed(() => [ optionLabel: 'name', }, }, + columnField: { + component: null, + }, format: ({ role }, dashIfEmpty) => dashIfEmpty(role?.name), + create: true, }, { align: 'left', @@ -51,20 +73,32 @@ const columns = computed(() => [ }, { align: 'left', - name: 'name', - label: t('Name'), + name: 'email', + label: t('Email'), component: 'input', columnField: { component: null, }, - cardVisible: true, create: true, + visible: false, }, { align: 'left', - name: 'email', - label: t('email'), - component: 'input', + name: 'password', + label: t('Password'), + columnField: { + component: null, + }, + attrs: {}, + required: true, + visible: false, + }, + + { + align: 'left', + name: 'active', + label: t('Active'), + component: 'checkbox', create: true, visible: false, }, @@ -101,7 +135,6 @@ const exprBuilder = (param, value) => { } }; </script> - <template> <VnSearchbar data-key="AccountList" @@ -119,6 +152,12 @@ const exprBuilder = (param, value) => { ref="tableRef" data-key="AccountList" url="VnUsers/preview" + :create="{ + urlCreate: 'VnUsers', + title: t('Create user'), + onDataSaved: ({ id }) => tableRef.redirect(id), + formInitialData: {}, + }" :filter="filter" order="id DESC" :columns="columns" @@ -127,7 +166,19 @@ const exprBuilder = (param, value) => { :use-model="true" :right-search="false" auto-load - /> + > + <template #more-create-dialog="{ data }"> + <QCardSection> + <VnInput + :label="t('Password')" + v-model="data.password" + type="password" + :required="true" + autocomplete="new-password" + /> + </QCardSection> + </template> + </VnTable> </template> <i18n> @@ -135,4 +186,7 @@ const exprBuilder = (param, value) => { Id: Id Nickname: Nickname Name: Nombre + Password: Contraseña + Active: Activo + Role: Rol </i18n> diff --git a/src/pages/Account/Acls/AclFilter.vue b/src/pages/Account/Acls/AclFilter.vue index 8609672b6..8035f92b8 100644 --- a/src/pages/Account/Acls/AclFilter.vue +++ b/src/pages/Account/Acls/AclFilter.vue @@ -37,11 +37,7 @@ onBeforeMount(() => { @on-fetch="(data) => (rolesOptions = data)" auto-load /> - <VnFilterPanel - :data-key="props.dataKey" - :search-button="true" - :hidden-tags="['search']" - > + <VnFilterPanel :data-key="props.dataKey" :search-button="true"> <template #tags="{ tag, formatFn }"> <div class="q-gutter-x-xs"> <strong>{{ t(`acls.aclFilter.${tag.label}`) }}: </strong> diff --git a/src/pages/Account/Card/AccountDescriptorMenu.vue b/src/pages/Account/Card/AccountDescriptorMenu.vue index 6f1d2ca1f..1780b4247 100644 --- a/src/pages/Account/Card/AccountDescriptorMenu.vue +++ b/src/pages/Account/Card/AccountDescriptorMenu.vue @@ -8,7 +8,7 @@ import { useAcl } from 'src/composables/useAcl'; import { useArrayData } from 'src/composables/useArrayData'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; import VnChangePassword from 'src/components/common/VnChangePassword.vue'; -import useNotify from 'src/composables/useNotify.js'; +import { useQuasar } from 'quasar'; const $props = defineProps({ hasAccount: { @@ -21,7 +21,7 @@ const { t } = useI18n(); const { hasAccount } = toRefs($props); const { openConfirmationModal } = useVnConfirm(); const route = useRoute(); -const { notify } = useNotify(); +const { notify } = useQuasar(); const account = computed(() => useArrayData('AccountId').store.data[0]); account.value.hasAccount = hasAccount.value; const entityId = computed(() => +route.params.id); diff --git a/src/pages/Account/Role/AccountRolesFilter.vue b/src/pages/Account/Role/AccountRolesFilter.vue index ff4411897..cbe7a70c8 100644 --- a/src/pages/Account/Role/AccountRolesFilter.vue +++ b/src/pages/Account/Role/AccountRolesFilter.vue @@ -13,12 +13,7 @@ const props = defineProps({ </script> <template> - <VnFilterPanel - :data-key="props.dataKey" - :search-button="true" - :hidden-tags="['search']" - :redirect="false" - > + <VnFilterPanel :data-key="props.dataKey" :search-button="true" :redirect="false"> <template #tags="{ tag, formatFn }"> <div class="q-gutter-x-xs"> <strong>{{ t(`role.${tag.label}`) }}: </strong> diff --git a/src/pages/Customer/Card/CustomerBasicData.vue b/src/pages/Customer/Card/CustomerBasicData.vue index 1abb9f170..33f9732e2 100644 --- a/src/pages/Customer/Card/CustomerBasicData.vue +++ b/src/pages/Customer/Card/CustomerBasicData.vue @@ -8,7 +8,7 @@ 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 VnAvatar from 'src/components/ui/VnAvatar.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; import { getDifferences, getUpdatedValues } from 'src/filters'; const route = useRoute(); @@ -16,7 +16,6 @@ const { t } = useI18n(); const businessTypes = ref([]); const contactChannels = ref([]); -const title = ref(); const handleSalesModelValue = (val) => ({ or: [ { id: val }, @@ -119,41 +118,17 @@ function onBeforeSave(formData, originalData) { /> </VnRow> <VnRow> - <VnSelect - url="Workers/search" - v-model="data.salesPersonFk" + <VnSelectWorker :label="t('customer.summary.salesPerson')" + v-model="data.salesPersonFk" :params="{ departmentCodes: ['VT', 'shopping'], }" - :fields="['id', 'nickname']" - sort-by="nickname ASC" - option-label="nickname" - option-value="id" + :has-avatar="true" :rules="validate('client.salesPersonFk')" :expr-builder="exprBuilder" emit-value - auto-load - > - <template #prepend> - <VnAvatar - :worker-id="data.salesPersonFk" - color="primary" - :title="title" - /> - </template> - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel>{{ scope.opt?.name }}</QItemLabel> - <QItemLabel caption - >{{ scope.opt?.nickname }}, - {{ scope.opt?.code }}</QItemLabel - > - </QItemSection> - </QItem> - </template> - </VnSelect> + /> <VnSelect v-model="data.contactChannelFk" :options="contactChannels" diff --git a/src/pages/Customer/Card/CustomerSummary.vue b/src/pages/Customer/Card/CustomerSummary.vue index 2cad13115..ae4c7f3ab 100644 --- a/src/pages/Customer/Card/CustomerSummary.vue +++ b/src/pages/Customer/Card/CustomerSummary.vue @@ -1,12 +1,11 @@ <script setup> -import { computed, ref, onMounted } from 'vue'; +import { computed, ref } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import { toCurrency, toPercentage, toDate, dashOrCurrency } from 'src/filters'; import CardSummary from 'components/ui/CardSummary.vue'; -import { getUrl } from 'src/composables/getUrl'; import VnLv from 'src/components/ui/VnLv.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import VnLinkMail from 'src/components/ui/VnLinkMail.vue'; @@ -102,7 +101,7 @@ const sumRisk = ({ clientRisks }) => { <VnLv :value="entity.email" copy ><template #label> {{ t('globals.params.email') }} - <VnLinkMail email="entity.email"></VnLinkMail> </template + <VnLinkMail :email="entity.email"></VnLinkMail> </template ></VnLv> <VnLv :label="t('customer.summary.salesPerson')" diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index ea901c65a..3c4106846 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -29,7 +29,8 @@ async function hasCustomerRole() { :filter="filter" model="customer" :mapper=" - ({ active, name, email }) => { + ({ account }) => { + const { name, email, active } = account; return { active, name, diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index cd567d415..96f670542 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnSelect from 'components/common/VnSelect.vue'; import VnInput from 'src/components/common/VnInput.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); defineProps({ @@ -65,19 +66,14 @@ const exprBuilder = (param, value) => { </QItem> <QItem class="q-mb-sm"> <QItemSection> - <VnSelect - url="Workers/search" + <VnSelectWorker + :label="t('Salesperson')" + v-model="params.salesPersonFk" :params="{ departmentCodes: ['VT'], }" - auto-load - :label="t('Salesperson')" :expr-builder="exprBuilder" - v-model="params.salesPersonFk" @update:model-value="searchFn()" - option-value="id" - option-label="name" - sort-by="nickname ASC" emit-value map-options use-input @@ -86,18 +82,7 @@ const exprBuilder = (param, value) => { outlined rounded :input-debounce="0" - > - <template #option="{ itemProps, opt }"> - <QItem v-bind="itemProps"> - <QItemSection> - <QItemLabel>{{ opt.name }}</QItemLabel> - <QItemLabel caption> - {{ opt.nickname }},{{ opt.code }} - </QItemLabel> - </QItemSection> - </QItem> - </template></VnSelect - > + /> </QItemSection> </QItem> <QItem class="q-mb-sm"> diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index 865287aeb..e86e35966 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -2,7 +2,6 @@ import { ref, computed, markRaw } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; -import VnSelect from 'src/components/common/VnSelect.vue'; import VnTable from 'components/VnTable/VnTable.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue'; @@ -12,7 +11,7 @@ import RightMenu from 'src/components/common/RightMenu.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import { toDate } from 'src/filters'; import CustomerFilter from './CustomerFilter.vue'; -import VnAvatar from 'src/components/ui/VnAvatar.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const router = useRouter(); @@ -422,40 +421,17 @@ function handleLocation(data, location) { auto-load > <template #more-create-dialog="{ data }"> - <VnSelect - url="Workers/search" - v-model="data.salesPersonFk" + <VnSelectWorker :label="t('customer.summary.salesPerson')" + v-model="data.salesPersonFk" :params="{ departmentCodes: ['VT', 'shopping'], }" - :fields="['id', 'nickname', 'code']" - sort-by="nickname ASC" - option-label="nickname" - option-value="id" + :has-avatar="true" + :id-value="data.salesPersonFk" emit-value auto-load - > - <template #prepend> - <VnAvatar - :worker-id="data.salesPersonFk" - color="primary" - :title="title" - /> - </template> - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel>{{ scope.opt?.name }}</QItemLabel> - <QItemLabel caption - >{{ scope.opt?.nickname }}, - {{ scope.opt?.code }}</QItemLabel - > - </QItemSection> - </QItem> - </template> - </VnSelect> - + /> <VnLocation :acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]" v-model="data.location" diff --git a/src/pages/Department/Card/DepartmentBasicData.vue b/src/pages/Department/Card/DepartmentBasicData.vue index 07bccd971..22ce06821 100644 --- a/src/pages/Department/Card/DepartmentBasicData.vue +++ b/src/pages/Department/Card/DepartmentBasicData.vue @@ -6,6 +6,7 @@ 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(); @@ -48,14 +49,9 @@ const { t } = useI18n(); /> </VnRow> <VnRow> - <VnSelect + <VnSelectWorker :label="t('department.bossDepartment')" v-model="data.workerFk" - url="Workers/search" - option-value="id" - option-label="name" - hide-selected - map-options :rules="validate('department.workerFk')" /> <VnSelect diff --git a/src/pages/InvoiceOut/InvoiceOutList.vue b/src/pages/InvoiceOut/InvoiceOutList.vue index c8fffb0ef..0aeae622d 100644 --- a/src/pages/InvoiceOut/InvoiceOutList.vue +++ b/src/pages/InvoiceOut/InvoiceOutList.vue @@ -52,7 +52,9 @@ const columns = computed(() => [ label: t('invoiceOutList.tableVisibleColumns.id'), chip: { condition: () => true }, isId: true, - columnFilter: { name: 'search' }, + columnFilter: { + name: 'id', + }, }, { align: 'left', @@ -84,8 +86,15 @@ const columns = computed(() => [ label: t('globals.client'), cardVisible: true, component: 'select', - attrs: { url: 'Clients', fields: ['id', 'name'] }, - columnField: { component: null }, + attrs: { + url: 'Clients', + fields: ['id', 'socialName'], + optionLabel: 'socialName', + optionValue: 'id', + }, + columnField: { + component: null, + }, }, { align: 'left', diff --git a/src/pages/InvoiceOut/locale/es.yml b/src/pages/InvoiceOut/locale/es.yml index bf5126641..106168a5d 100644 --- a/src/pages/InvoiceOut/locale/es.yml +++ b/src/pages/InvoiceOut/locale/es.yml @@ -11,7 +11,7 @@ invoiceOutList: ref: Referencia issued: Fecha emisión created: F. creación - dueDate: F. máxima + dueDate: Fecha vencimiento invoiceOutSerial: Serial ticket: Ticket taxArea: Area diff --git a/src/pages/Item/Card/ItemBarcode.vue b/src/pages/Item/Card/ItemBarcode.vue index 197e9142f..6db5943c7 100644 --- a/src/pages/Item/Card/ItemBarcode.vue +++ b/src/pages/Item/Card/ItemBarcode.vue @@ -2,12 +2,15 @@ import { ref, nextTick } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; +import axios from 'axios'; import CrudModel from 'src/components/CrudModel.vue'; import VnInput from 'src/components/common/VnInput.vue'; +import useNotify from 'src/composables/useNotify.js'; const route = useRoute(); const { t } = useI18n(); +const { notify } = useNotify(); const itemBarcodeRef = ref(null); @@ -23,6 +26,24 @@ const focusLastInput = () => { if (lastInput) lastInput.focus(); }); }; + +const removeRow = (row) => { + itemBarcodeRef.value.remove([row]); +}; + +const submit = async (rows) => { + const params = rows[rows.length - 1]; + let { data } = await axios.get('ItemBarcodes'); + const code = params.code; + + if (data.some((codes) => codes.code === code)) { + notify(t('Codes can not be repeated'), 'negative'); + itemBarcodeRef.value.reset(); + return; + } + await axios.patch(`ItemBarcodes`, params); + notify(t('globals.dataSaved'), 'positive'); +}; </script> <template> <div class="full-width flex justify-center"> @@ -39,6 +60,7 @@ const focusLastInput = () => { ref="itemBarcodeRef" url="ItemBarcodes" auto-load + :save-fn="submit" > <template #body="{ rows }"> <QCard class="q-px-lg q-py-md"> @@ -54,7 +76,7 @@ const focusLastInput = () => { focusable-input /> <QIcon - @click="itemBarcodeRef.remove([row])" + @click="removeRow(row)" class="cursor-pointer q-ml-md" color="primary" name="delete" @@ -89,4 +111,5 @@ es: Code: Código Remove barcode: Quitar código de barras Add barcode: Añadir código de barras + Codes can not be repeated: Los códigos no puden ser repetidos </i18n> diff --git a/src/pages/Item/Card/ItemBasicData.vue b/src/pages/Item/Card/ItemBasicData.vue index 1b0342668..a1788617f 100644 --- a/src/pages/Item/Card/ItemBasicData.vue +++ b/src/pages/Item/Card/ItemBasicData.vue @@ -70,6 +70,7 @@ const onIntrastatCreated = (response, formData) => { option-label="name" hide-selected map-options + required > <template #option="scope"> <QItem v-bind="scope.itemProps"> diff --git a/src/pages/Item/Card/ItemBotanical.vue b/src/pages/Item/Card/ItemBotanical.vue index c4b561772..57774f75e 100644 --- a/src/pages/Item/Card/ItemBotanical.vue +++ b/src/pages/Item/Card/ItemBotanical.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, onMounted, reactive, computed } from 'vue'; +import { ref, reactive, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; diff --git a/src/pages/Item/Card/ItemDescriptorImage.vue b/src/pages/Item/Card/ItemDescriptorImage.vue index 735e5eb4f..422725a38 100644 --- a/src/pages/Item/Card/ItemDescriptorImage.vue +++ b/src/pages/Item/Card/ItemDescriptorImage.vue @@ -67,7 +67,7 @@ const handlePhotoUpdated = (evt = false) => { <template> <div class="relative-position"> - <VnImg ref="image" :id="$props.entityId" zoom-resolution="1600x900"> + <VnImg ref="image" :id="parseInt($props.entityId)" zoom-resolution="1600x900"> <template #error> <div class="absolute-full picture text-center q-pa-md flex flex-center"> <div> diff --git a/src/pages/Item/Card/ItemDiary.vue b/src/pages/Item/Card/ItemDiary.vue index a3ac6645e..b94ff9255 100644 --- a/src/pages/Item/Card/ItemDiary.vue +++ b/src/pages/Item/Card/ItemDiary.vue @@ -1,5 +1,5 @@ <script setup> -import { onMounted, computed, onUnmounted, reactive, ref, nextTick, watch } from 'vue'; +import { onMounted, computed, reactive, ref, nextTick, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; @@ -12,20 +12,18 @@ import FetchData from 'components/FetchData.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; -import { useStateStore } from 'stores/useStateStore'; import { toDateFormat } from 'src/filters/date.js'; import { dashIfEmpty } from 'src/filters'; import { date } from 'quasar'; import { useState } from 'src/composables/useState'; import { useArrayData } from 'src/composables/useArrayData'; import axios from 'axios'; +import VnSubToolbar from 'components/ui/VnSubToolbar.vue'; const { t } = useI18n(); const route = useRoute(); const router = useRouter(); -const stateStore = useStateStore(); const state = useState(); - const user = state.getUser(); const today = ref(Date.vnNew()); const warehousesOptions = ref([]); @@ -145,8 +143,6 @@ onMounted(async () => { await updateWarehouse(warehouseFk.value); }); -onUnmounted(() => (stateStore.rightDrawer = false)); - watch( () => router.currentRoute.value.params.id, (newId) => { @@ -205,8 +201,8 @@ async function updateWarehouse(warehouseFk) { auto-load @on-fetch="(data) => (warehousesOptions = data)" /> - <template v-if="stateStore.isHeaderMounted()"> - <Teleport to="#st-data"> + <VnSubToolbar class="q-mb-md"> + <template #st-data> <div class="row"> <VnSelect :label="t('itemDiary.warehouse')" @@ -235,9 +231,8 @@ async function updateWarehouse(warehouseFk) { @update:model-value="fetchItemBalances" /> </div> - </Teleport> - <Teleport to="#st-actions"> </Teleport> - </template> + </template> + </VnSubToolbar> <QPage class="column items-center q-pa-md"> <QTable :rows="itemBalances" diff --git a/src/pages/Item/Card/ItemLastEntries.vue b/src/pages/Item/Card/ItemLastEntries.vue index 22fb9adc7..d4d0647e3 100644 --- a/src/pages/Item/Card/ItemLastEntries.vue +++ b/src/pages/Item/Card/ItemLastEntries.vue @@ -1,12 +1,10 @@ <script setup> -import { onMounted, computed, onUnmounted, ref, watch } from 'vue'; +import { onMounted, computed, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import { dateRange } from 'src/filters'; import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; - -import { useStateStore } from 'stores/useStateStore'; import { toDateTimeFormat } from 'src/filters/date.js'; import { dashIfEmpty } from 'src/filters'; import { toCurrency } from 'filters/index'; @@ -15,7 +13,6 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; const { t } = useI18n(); const route = useRoute(); -const stateStore = useStateStore(); const exprBuilder = (param, value) => { switch (param) { @@ -180,8 +177,6 @@ onMounted(async () => { updateFilter(); }); }); - -onUnmounted(() => (stateStore.rightDrawer = false)); </script> <template> diff --git a/src/pages/Item/Card/ItemShelving.vue b/src/pages/Item/Card/ItemShelving.vue index 27e265e6b..7ad60c9e0 100644 --- a/src/pages/Item/Card/ItemShelving.vue +++ b/src/pages/Item/Card/ItemShelving.vue @@ -1,19 +1,15 @@ <script setup> -import { onMounted, ref, computed, reactive } from 'vue'; +import { onMounted, ref, computed, reactive, watchEffect } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; - -import VnInput from 'src/components/common/VnInput.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; - import { toDateFormat } from 'src/filters/date.js'; -import { dashIfEmpty } from 'src/filters'; import { useArrayData } from 'src/composables/useArrayData'; import useNotify from 'src/composables/useNotify.js'; import { useVnConfirm } from 'composables/useVnConfirm'; import axios from 'axios'; import { useStateStore } from 'stores/useStateStore'; +import VnTable from 'src/components/VnTable/VnTable.vue'; const stateStore = useStateStore(); @@ -21,8 +17,9 @@ const route = useRoute(); const { t } = useI18n(); const { notify } = useNotify(); const { openConfirmationModal } = useVnConfirm(); - -const rowsSelected = ref([]); +const tableRef = ref(); +const selectedRows = ref([]); +const hasSelectedCards = computed(() => selectedRows.value.length > 0); const exprBuilder = (param, value) => { switch (param) { @@ -36,6 +33,11 @@ const exprBuilder = (param, value) => { }; const params = reactive({ itemFk: route.params.id }); +const filter = reactive({ + where: { + itemFk: route.params.id, + }, +}); const arrayData = useArrayData('ItemShelvings', { url: 'ItemShelvingPlacementSupplyStocks', @@ -44,123 +46,69 @@ const arrayData = useArrayData('ItemShelvings', { }); const rows = computed(() => arrayData.store.data || []); -const applyColumnFilter = async (col) => { - const paramKey = col.columnFilter?.filterParamKey || col.field; - params[paramKey] = col.columnFilter.filterValue; - await arrayData.addFilter({ filter: null, params }); -}; - -const getInputEvents = (col) => { - return col.columnFilter.type === 'select' - ? { 'update:modelValue': () => applyColumnFilter(col) } - : { - 'keyup.enter': () => applyColumnFilter(col), - }; -}; - const columns = computed(() => [ { label: t('shelvings.created'), name: 'created', - field: 'created', align: 'left', - sortable: true, - columnFilter: null, - format: (val) => toDateFormat(val), + columnFilter: false, + format: (row) => toDateFormat(row.created), }, { label: t('shelvings.item'), - name: 'item', - field: 'itemFk', + name: 'itemFk', align: 'left', - sortable: true, - columnFilter: null, + columnFilter: false, }, { label: t('shelvings.concept'), - name: 'concept', + name: 'longName', align: 'left', - sortable: true, - columnFilter: null, + columnFilter: false, }, { label: t('shelvings.parking'), name: 'parking', - field: 'parking', align: 'left', - sortable: true, - format: (val) => dashIfEmpty(val), - columnFilter: { - component: VnSelect, - type: 'select', - filterValue: null, - event: getInputEvents, - attrs: { - url: 'parkings', - fields: ['code'], - 'sort-by': 'code ASC', - 'option-value': 'code', - 'option-label': 'code', - dense: true, - }, + component: 'select', + attrs: { + url: 'parkings', + fields: ['code'], + 'sort-by': 'code ASC', + 'option-value': 'code', + 'option-label': 'code', + dense: true, }, + columnField: { component: null }, }, { label: t('shelvings.shelving'), name: 'shelving', - field: 'shelving', align: 'left', - sortable: true, - format: (val) => dashIfEmpty(val), - columnFilter: { - component: VnSelect, - type: 'select', - filterValue: null, - event: getInputEvents, - attrs: { - url: 'shelvings', - fields: ['code'], - 'sort-by': 'code ASC', - 'option-value': 'code', - 'option-label': 'code', - dense: true, - }, + component: 'select', + attrs: { + url: 'shelvings', + fields: ['code'], + 'sort-by': 'code ASC', + 'option-value': 'code', + 'option-label': 'code', + dense: true, }, + columnField: { component: null }, }, { label: t('shelvings.label'), name: 'label', align: 'left', - sortable: true, - format: (_, row) => (row.stock / row.packing).toFixed(2), - columnFilter: { - component: VnInput, - type: 'text', - filterParamKey: 'label', - filterValue: null, - event: getInputEvents, - attrs: { - dense: true, - }, - }, + columnFilter: { inWhere: true }, + format: (row) => (row.stock / row.packing).toFixed(2), }, { label: t('shelvings.packing'), - field: 'packing', name: 'packing', + attrs: { inWhere: true }, align: 'left', - sortable: true, - columnFilter: { - component: VnInput, - type: 'text', - filterValue: null, - event: getInputEvents, - attrs: { - dense: true, - }, - }, - format: (val) => dashIfEmpty(val), }, ]); @@ -169,15 +117,16 @@ const totalLabels = computed(() => ); const removeLines = async () => { - const itemShelvingIds = rowsSelected.value.map((row) => row.itemShelvingFk); + const itemShelvingIds = selectedRows.value.map((row) => row.itemShelvingFk); await axios.post('ItemShelvings/deleteItemShelvings', { itemShelvingIds }); - rowsSelected.value = []; + selectedRows.value = []; notify('shelvings.shelvingsRemoved', 'positive'); - await arrayData.fetch({ append: false }); + await tableRef.value.reload(); }; onMounted(async () => { await arrayData.fetch({ append: false }); }); +watchEffect(selectedRows); </script> <template> @@ -203,7 +152,7 @@ onMounted(async () => { <QBtn color="primary" icon="delete" - :disabled="!rowsSelected.length" + :disabled="!hasSelectedCards" @click=" openConfirmationModal( t('shelvings.removeConfirmTitle'), @@ -219,41 +168,27 @@ onMounted(async () => { </Teleport> </template> <QPage class="column items-center q-pa-md"> - <QTable - :rows="rows" + <VnTable + ref="tableRef" + data-key="ItemShelving" :columns="columns" - row-key="id" - :pagination="{ rowsPerPage: 0 }" - class="full-width q-mt-md" - selection="multiple" - v-model:selected="rowsSelected" - :no-data-label="t('globals.noResults')" + :url="`ItemShelvingPlacementSupplyStocks`" + :filter="filter" + :expr-builder="exprBuilder" + :right-search="false" + v-model:selected="selectedRows" + :table="{ + 'row-key': 'itemShelvingFk', + selection: 'multiple', + }" + auto-load > - <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 #body-cell-concept="{ row }"> - <QTd @click.stop> - <span class="link">{{ row.longName }}</span> + <template #column-longName="{ row }"> + <span class="link" @click.stop> + {{ row.longName }} <ItemDescriptorProxy :id="row.itemFk" /> - </QTd> + </span> </template> - </QTable> + </VnTable> </QPage> </template> diff --git a/src/pages/Item/Card/ItemSummary.vue b/src/pages/Item/Card/ItemSummary.vue index db90ba06f..7606e6a22 100644 --- a/src/pages/Item/Card/ItemSummary.vue +++ b/src/pages/Item/Card/ItemSummary.vue @@ -89,7 +89,7 @@ const getUrl = (id, param) => `#/Item/${id}/${param}`; <QCard class="vn-one"> <VnTitle :url="getUrl(entityId, 'basic-data')" - :text="t('item.summary.otherData')" + :text="t('item.summary.basicData')" /> <VnLv :label="t('item.summary.intrastatCode')" diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue index a077c72c6..fbb0e01a7 100644 --- a/src/pages/Item/Card/ItemTags.vue +++ b/src/pages/Item/Card/ItemTags.vue @@ -8,7 +8,6 @@ import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import FetchData from 'components/FetchData.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; - import axios from 'axios'; const route = useRoute(); @@ -60,6 +59,10 @@ const insertTag = (rows) => { itemTagsRef.value.formData[itemTagsRef.value.formData.length - 1].priority = getHighestPriority(rows); }; + +const submitTags = async (data) => { + itemTagsRef.value.onSubmit(data); +}; </script> <template> @@ -77,7 +80,6 @@ const insertTag = (rows) => { data-key="ItemTags" model="ItemTags" url="ItemTags" - update-url="Tags/onSubmit" :data-required="{ $index: undefined, itemFk: route.params.id, @@ -147,6 +149,7 @@ const insertTag = (rows) => { v-model="row.value" :label="t('itemTags.value')" :is-clearable="false" + @keyup.enter.stop="submitTags(row)" /> <VnInput :label="t('itemBasicData.relevancy')" @@ -154,6 +157,7 @@ const insertTag = (rows) => { v-model="row.priority" :required="true" :rules="validate('itemTag.priority')" + @keyup.enter.stop="submitTags(row)" /> <div class="row justify-center" style="flex: 0"> <QIcon @@ -189,3 +193,8 @@ const insertTag = (rows) => { </QPage> </div> </template> + +<i18n> +es: + Tags can not be repeated: Las etiquetas no pueden repetirse +</i18n> diff --git a/src/pages/Item/Card/ItemTax.vue b/src/pages/Item/Card/ItemTax.vue index 84b5f63f4..8060481f0 100644 --- a/src/pages/Item/Card/ItemTax.vue +++ b/src/pages/Item/Card/ItemTax.vue @@ -28,7 +28,7 @@ const taxesFilter = { ], }; -const ItemTaxRef = ref(null); +const ItemTaxRef = ref(); const taxesOptions = ref([]); const submitTaxes = async (data) => { @@ -36,7 +36,10 @@ const submitTaxes = async (data) => { id: tax.id, taxClassFk: tax.taxClassFk, })); - + if (payload.some((item) => item.taxClassFk === null)) { + notify(t('Tax class cannot be blank'), 'negative'); + return; + } await axios.post(`Items/updateTaxes`, payload); notify(t('globals.dataSaved'), 'positive'); }; diff --git a/src/pages/Item/ItemFixedPrice.vue b/src/pages/Item/ItemFixedPrice.vue index 8bf5d33bd..09fccfd6d 100644 --- a/src/pages/Item/ItemFixedPrice.vue +++ b/src/pages/Item/ItemFixedPrice.vue @@ -1,5 +1,5 @@ <script setup> -import { onMounted, ref, reactive, onUnmounted, nextTick, computed } from 'vue'; +import { onMounted, ref, onUnmounted, nextTick, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import FetchedTags from 'components/ui/FetchedTags.vue'; @@ -37,11 +37,9 @@ const fixedPrices = ref([]); const warehousesOptions = ref([]); const rowsSelected = ref([]); const itemFixedPriceFilterRef = ref(); -const params = reactive({}); onMounted(async () => { stateStore.rightDrawer = true; - params.warehouseFk = user.value.warehouseFk; }); onUnmounted(() => (stateStore.rightDrawer = false)); @@ -137,8 +135,17 @@ const columns = computed(() => [ ...defaultColumnAttrs, columnClass: 'shrink', component: 'select', - options: warehousesOptions, + columnFilter: { + name: 'warehouseFk', + inWhere: true, + component: 'select', + attrs: { + options: warehousesOptions, + 'option-label': 'name', + 'option-value': 'id', + }, + }, }, { align: 'right', @@ -210,8 +217,6 @@ const getRowUpdateInputEvents = (props, resetMinPrice, inputType = 'text') => { }; const updateMinPrice = async (value, props) => { - // El checkbox hasMinPrice se encuentra en la misma columna que el input hasMinPrice - // Por lo tanto le mandamos otro objeto con las mismas propiedades pero con el campo 'field' cambiado props.row.hasMinPrice = value; await upsertPrice({ row: props.row, @@ -220,12 +225,33 @@ const updateMinPrice = async (value, props) => { }); }; +const validations = ({ row }) => { + const requiredFields = [ + 'itemFk', + 'started', + 'ended', + 'rate2', + 'rate3', + 'warehouseFk', + ]; + const isValid = requiredFields.every( + (field) => row[field] !== null && row[field] !== undefined + ); + return isValid; +}; const upsertPrice = async (props, resetMinPrice = false) => { - const { row } = props; - if (tableRef.value.CrudModelRef.getChanges().updates.length > 0) { - if (resetMinPrice) row.hasMinPrice = 0; - await upsertFixedPrice(row); + const isValid = validations({ ...props }); + if (!isValid) { + return; } + const { row } = props; + const changes = tableRef.value.CrudModelRef.getChanges(); + if (changes?.updates?.length > 0) { + if (resetMinPrice) row.hasMinPrice = 0; + } + if (!changes.updates && !changes.creates) return; + const data = await upsertFixedPrice(row); + tableRef.value.CrudModelRef.formData[props.rowIndex] = data; }; async function upsertFixedPrice(row) { @@ -233,13 +259,6 @@ async function upsertFixedPrice(row) { return data; } -async function saveOnRowChange(row) { - if (rowsSelected.value.length > 1) return; - if (rowsSelected.value[0]?.id === row.id) return; - else if (rowsSelected.value.length === 1) await upsertPrice(rowsSelected.value[0]); - rowsSelected.value = [row]; -} - function checkLastVisibleRow() { let lastVisibleRow = null; @@ -255,39 +274,18 @@ function checkLastVisibleRow() { const addRow = (original = null) => { let copy = null; - if (!original) { - const today = Date.vnNew(); - const millisecsInDay = 86400000; - const daysInWeek = 7; - const nextWeek = new Date(today.getTime() + daysInWeek * millisecsInDay); + const today = Date.vnNew(); + const millisecsInDay = 86400000; + const daysInWeek = 7; + const nextWeek = new Date(today.getTime() + daysInWeek * millisecsInDay); - copy = { - id: 0, - started: today, - ended: nextWeek, - hasMinPrice: 0, - $index: 0, - }; - } else - copy = { - $index: original.$index - 1, - itemFk: original.itemFk, - name: original.name, - subName: original.subName, - value5: original.value5, - value6: original.value6, - value7: original.value7, - value8: original.value8, - value9: original.value9, - value10: original.value10, - warehouseFk: original.warehouseFk, - rate2: original.rate2, - rate3: original.rate3, - hasMinPrice: original.hasMinPrice, - minPrice: original.minPrice, - started: Date.vnNew(), - ended: Date.vnNew(), - }; + copy = { + id: 0, + started: today, + ended: nextWeek, + hasMinPrice: 0, + $index: 0, + }; return { original, copy }; }; @@ -300,7 +298,7 @@ function highlightNewRow({ $index: index }) { row.classList.add('highlight'); setTimeout(() => { row.classList.remove('highlight'); - }, 3000); // Duración de la animación en milisegundos + }, 3000); } } const openEditTableCellDialog = () => { @@ -411,9 +409,13 @@ function handleOnDataSave({ CrudModelRef }) { url="FixedPrices/filter" :order="['itemFk DESC', 'name DESC']" save-url="FixedPrices/crud" - :user-params="{ warehouseFk: user.warehouseFk }" ref="tableRef" dense + :filter="{ + where: { + warehouseFk: user.warehouseFk, + }, + }" :columns="columns" default-mode="table" auto-load @@ -427,7 +429,6 @@ function handleOnDataSave({ CrudModelRef }) { disableInfiniteScroll: true, }" v-model:selected="rowsSelected" - :row-click="saveOnRowChange" :create-as-dialog="false" :create="{ onDataSaved: handleOnDataSave, diff --git a/src/pages/Item/ItemList.vue b/src/pages/Item/ItemList.vue index 30b4893bd..30454a0c3 100644 --- a/src/pages/Item/ItemList.vue +++ b/src/pages/Item/ItemList.vue @@ -55,17 +55,6 @@ const columns = computed(() => [ label: '', name: 'image', align: 'left', - columnField: { - component: VnImg, - attrs: ({ row }) => { - return { - id: row?.id, - zoomResolution: '1600x900', - zoom: true, - class: 'rounded', - }; - }, - }, columnFilter: false, cardVisible: true, }, @@ -184,7 +173,7 @@ const columns = computed(() => [ cardVisible: true, }, { - label: t('globals.origin'), + label: t('item.list.origin'), name: 'origin', align: 'left', component: 'select', @@ -228,7 +217,8 @@ const columns = computed(() => [ }, }, { - label: t('item.list.weightByPiece'), + label: t('item.list.weight'), + toolTip: t('item.list.weightByPiece'), name: 'weightByPiece', align: 'left', component: 'input', @@ -322,7 +312,6 @@ const columns = computed(() => [ ref="tableRef" data-key="ItemList" url="Items/filter" - url-create="Items" :create="{ urlCreate: 'Items', title: t('Create Item'), @@ -333,12 +322,19 @@ const columns = computed(() => [ }" :order="['isActive DESC', 'name', 'id']" :columns="columns" - auto-load redirect="Item" :is-editable="false" :right-search="false" - :filer="itemFilter" + :filter="itemFilter" > + <template #column-image="{ row }"> + <VnImg + :id="row?.id" + zoom-resolution="1600x900" + :zoom="true" + class="rounded" + /> + </template> <template #column-id="{ row }"> <span class="link" @click.stop> {{ row.id }} @@ -348,7 +344,7 @@ const columns = computed(() => [ <template #column-userName="{ row }"> <span class="link" @click.stop> {{ row.userName }} - <WorkerDescriptorProxy :id="row.workerFk" /> + <WorkerDescriptorProxy :id="row.buyerFk" /> </span> </template> <template #column-description="{ row }"> diff --git a/src/pages/Item/ItemListFilter.vue b/src/pages/Item/ItemListFilter.vue index c8357ba33..484265b49 100644 --- a/src/pages/Item/ItemListFilter.vue +++ b/src/pages/Item/ItemListFilter.vue @@ -199,7 +199,17 @@ onMounted(async () => { dense outlined rounded - /> + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel>{{ + t(`params.${scope.opt?.name}`) + }}</QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> </QItemSection> </QItem> <QItem> @@ -434,6 +444,13 @@ en: description: Description name: Name id: Id + Accessories: Accessories + Artificial: Artificial + Flower: Flower + Fruit: Fruit + Green: Green + Handmade: Handmade + Plant: Plant es: More fields: Más campos params: @@ -450,4 +467,11 @@ es: description: Descripción name: Nombre id: Id + Accessories: Accesorios + Artificial: Artificial + Flower: Flor + Fruit: Fruta + Green: Verde + Handmade: Hecho a mano + Plant: Planta </i18n> diff --git a/src/pages/Item/ItemRequest.vue b/src/pages/Item/ItemRequest.vue index 4f037529a..734fe26de 100644 --- a/src/pages/Item/ItemRequest.vue +++ b/src/pages/Item/ItemRequest.vue @@ -5,13 +5,16 @@ import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.v import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import { useStateStore } from 'stores/useStateStore'; import { useArrayData } from 'composables/useArrayData'; -import { dashIfEmpty, toCurrency } from 'filters/index'; +import { toCurrency } from 'filters/index'; import useNotify from 'src/composables/useNotify.js'; import axios from 'axios'; import ItemRequestDenyForm from './ItemRequestDenyForm.vue'; import { toDate } from 'src/filters'; import VnTable from 'components/VnTable/VnTable.vue'; import VnInput from 'src/components/common/VnInput.vue'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +import ItemRequestFilter from './ItemRequestFilter.vue'; +import RightMenu from 'src/components/common/RightMenu.vue'; const { t } = useI18n(); const { notify } = useNotify(); const stateStore = useStateStore(); @@ -228,6 +231,11 @@ onMounted(async () => { </script> <template> + <RightMenu> + <template #right-panel> + <ItemRequestFilter data-key="itemRequest" /> + </template> + </RightMenu> <VnTable ref="tableRef" data-key="itemRequest" @@ -239,6 +247,7 @@ onMounted(async () => { auto-load :disable-option="{ card: true }" chip-locale="item.params" + :right-search="false" > <template #column-ticketFk="{ row }"> <span class="link"> @@ -306,30 +315,28 @@ onMounted(async () => { /> </template> <template #column-denyOptions="{ row, rowIndex }"> - <QTd class="sticky no-padding"> - <QIcon - v-if="row.response?.length" - name="insert_drive_file" - color="primary" - size="sm" - > - <QTooltip> - {{ row.response }} - </QTooltip> - </QIcon> - <QIcon - v-if="row.isOk == null" - name="thumb_down" - color="primary" - size="sm" - class="fill-icon" - @click="showDenyRequestForm(row.id, rowIndex)" - > - <QTooltip> - {{ t('Discard') }} - </QTooltip> - </QIcon> - </QTd> + <QIcon + v-if="row.response?.length" + name="insert_drive_file" + color="primary" + size="sm" + > + <QTooltip> + {{ row.response }} + </QTooltip> + </QIcon> + <QIcon + v-if="row.isOk == null" + name="thumb_down" + color="primary" + size="sm" + class="fill-icon" + @click="showDenyRequestForm(row.id, rowIndex)" + > + <QTooltip> + {{ t('Discard') }} + </QTooltip> + </QIcon> </template> </VnTable> <QDialog ref="denyFormRef" transition-show="scale" transition-hide="scale"> diff --git a/src/pages/Item/ItemRequestFilter.vue b/src/pages/Item/ItemRequestFilter.vue index 64bc0e575..ea1a6e760 100644 --- a/src/pages/Item/ItemRequestFilter.vue +++ b/src/pages/Item/ItemRequestFilter.vue @@ -1,5 +1,5 @@ <script setup> -import { ref } from 'vue'; +import { onMounted, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { dateRange } from 'src/filters'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; @@ -7,6 +7,8 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import VnInput from 'src/components/common/VnInput.vue'; import FetchData from 'components/FetchData.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; +import { useArrayData } from 'src/composables/useArrayData'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const props = defineProps({ @@ -21,7 +23,8 @@ const stateOptions = [ { code: 'accepted', name: t('accepted') }, { code: 'denied', name: t('denied') }, ]; - +const arrayData = useArrayData(props.dataKey); +const fieldFiltersValues = ref([]); const itemTypesOptions = ref([]); const warehousesOptions = ref([]); @@ -56,6 +59,19 @@ const decrement = (paramsObj, key) => { paramsObj[key]--; }; + +onMounted(async () => { + if (arrayData.store?.userParams) { + fieldFiltersValues.value = Object.entries(arrayData.store.userParams).map( + ([key, value]) => ({ + name: key, + value, + selectedField: { name: key, label: t(`params.${key}`) }, + }) + ); + } + exprBuilder('state', arrayData.store?.userParams?.state); +}); </script> <template> @@ -145,33 +161,17 @@ const decrement = (paramsObj, key) => { </QItem> <QItem> <QItemSection> - <VnSelect + <VnSelectWorker :label="t('params.requesterFk')" v-model="params.requesterFk" @update:model-value="searchFn()" - url="Workers/search" :fields="['id', 'name']" - order="name ASC" :params="{ departmentCodes: ['VT'] }" - 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?.nickname }}, - {{ scope.opt?.code }}</QItemLabel - > - </QItemSection> - </QItem> - </template> - </VnSelect> + /> </QItemSection> </QItem> <QItem> diff --git a/src/pages/Item/ItemType/ItemTypeSearchbar.vue b/src/pages/Item/ItemType/ItemTypeSearchbar.vue index 87903a517..749033d43 100644 --- a/src/pages/Item/ItemType/ItemTypeSearchbar.vue +++ b/src/pages/Item/ItemType/ItemTypeSearchbar.vue @@ -10,7 +10,6 @@ const { t } = useI18n(); url="ItemTypes" :label="t('Search item type')" :info="t('Search itemType by id, name or code')" - search-url="table" /> </template> <i18n> diff --git a/src/pages/Item/ItemTypeList.vue b/src/pages/Item/ItemTypeList.vue index 149de482d..4cea931e2 100644 --- a/src/pages/Item/ItemTypeList.vue +++ b/src/pages/Item/ItemTypeList.vue @@ -6,6 +6,7 @@ import VnTable from 'components/VnTable/VnTable.vue'; import FetchData from 'components/FetchData.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import ItemTypeFilter from './ItemType/ItemTypeFilter.vue'; +import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue'; const { t } = useI18n(); const tableRef = ref(); @@ -31,13 +32,14 @@ const columns = computed(() => [ { align: 'left', name: 'name', - label: t('name'), + label: t('globals.name'), cardVisible: true, create: true, }, { align: 'left', label: t('worker'), + name: 'workerFk', create: true, component: 'select', attrs: { @@ -45,20 +47,20 @@ const columns = computed(() => [ optionLabel: 'nickname', optionValue: 'id', }, + format: (row) => row.worker?.user?.name, cardVisible: true, - visible: true, - columnField: { - component: 'userLink', - attrs: ({ row }) => { - return { - workerId: row?.worker?.id, - name: row.worker?.user?.name, - defaultName: true, - }; - }, - }, + columnField: { component: null }, columnFilter: { - name: 'workerFk', + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + where: { role: 'buyer' }, + optionFilter: 'firstName', + optionLabel: 'name', + optionValue: 'id', + useLike: false, + }, + inWhere: true, }, }, { @@ -135,24 +137,27 @@ const columns = computed(() => [ :columns="columns" auto-load :right-search="false" - :is-editable="false" - :use-model="true" redirect="item/item-type" - /> + > + <template #column-workerFk="{ row }"> + <span class="link" @click.stop> + {{ row.worker?.user?.name }} + <WorkerDescriptorProxy :id="row.workerFk" /> + </span> + </template> + </VnTable> </template> <i18n> es: id: Id code: Código - name: Nombre worker: Trabajador ItemCategory: Reino Temperature: Temperatura Create ItemTypes: Crear familia en: code: Code - name: Name worker: Worker ItemCategory: ItemCategory Temperature: Temperature diff --git a/src/pages/Item/locale/en.yml b/src/pages/Item/locale/en.yml index 78a1c3ff0..9b667fcaa 100644 --- a/src/pages/Item/locale/en.yml +++ b/src/pages/Item/locale/en.yml @@ -95,6 +95,15 @@ item: mine: For me state: State myTeam: My team + shipped: Shipped + description: Description + quantity: Quantity + price: Price + item: Item + achieved: Achieved + concept: Concept + denyOptions: Deny + scopeDays: Scope days searchbar: label: Search item descriptor: @@ -112,7 +121,7 @@ item: title: All its properties will be copied subTitle: Do you want to clone this item? list: - id: Identifier + id: Id grouping: Grouping packing: Packing description: Description @@ -122,8 +131,9 @@ item: intrastat: Intrastat isActive: Active size: Size - origin: Origin + origin: Orig. userName: Buyer + weight: Weight weightByPiece: Weight/Piece stemMultiplier: Multiplier producer: Producer diff --git a/src/pages/Item/locale/es.yml b/src/pages/Item/locale/es.yml index 5498f4458..eb3ddd4de 100644 --- a/src/pages/Item/locale/es.yml +++ b/src/pages/Item/locale/es.yml @@ -97,6 +97,15 @@ item: mine: Para mi state: Estado myTeam: Mi equipo + shipped: Enviado + description: Descripción + quantity: Cantidad + price: Precio + item: Artículo + achieved: Conseguido + concept: Concepto + denyOptions: Denegado + scopeDays: Días en adelante searchbar: label: Buscar artículo descriptor: @@ -114,7 +123,7 @@ item: title: Todas sus propiedades serán copiadas subTitle: ¿Desea clonar este artículo? list: - id: Identificador + id: Id grouping: Grouping packing: Packing description: Descripción @@ -124,7 +133,8 @@ item: intrastat: Intrastat isActive: Activo size: Medida - origin: Origen + origin: Orig. + weight: Peso weightByPiece: Peso (gramos)/tallo userName: Comprador stemMultiplier: Multiplicador diff --git a/src/pages/Monitor/Ticket/MonitorTicketFilter.vue b/src/pages/Monitor/Ticket/MonitorTicketFilter.vue index 3247da014..8377d73ef 100644 --- a/src/pages/Monitor/Ticket/MonitorTicketFilter.vue +++ b/src/pages/Monitor/Ticket/MonitorTicketFilter.vue @@ -9,6 +9,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import FetchData from 'src/components/FetchData.vue'; import { dateRange } from 'src/filters'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; defineProps({ dataKey: { type: String, required: true } }); const { t, te } = useI18n(); @@ -59,7 +60,11 @@ const getLocale = (label) => { </template> <template #customTags="{ params, searchFn, formatFn }"> <VnFilterPanelChip - v-if="params.scopeDays !== null" + v-if=" + params.scopeDays !== undefined || + params.scopeDays !== '' || + params.scopeDays !== null + " removable @remove="handleScopeDays(params, null, searchFn)" > @@ -108,33 +113,16 @@ const getLocale = (label) => { </QItem> <QItem> <QItemSection> - <VnSelect + <VnSelectWorker outlined dense rounded :label="t('globals.params.salesPersonFk')" v-model="params.salesPersonFk" - url="Workers/search" :params="{ departmentCodes: ['VT'] }" - is-outlined - option-value="id" - option-label="name" :no-one="true" > - <template #option="{ opt, itemProps }"> - <QItem v-bind="itemProps"> - <QItemSection> - <QItemLabel>{{ opt.name }}</QItemLabel> - <QItemLabel - v-if="opt.code" - class="text-grey text-caption" - > - {{ `${opt.nickname}, ${opt.code}` }} - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> + </VnSelectWorker> </QItemSection> </QItem> <QItem> @@ -197,6 +185,18 @@ const getLocale = (label) => { /> </QItemSection> </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('globals.params.countryFk')" + v-model="params.countryFk" + url="Countries" + /> + </QItemSection> + </QItem> <QItem> <QItemSection> <VnSelect diff --git a/src/pages/Order/Card/OrderCatalog.vue b/src/pages/Order/Card/OrderCatalog.vue index a71065521..453037f15 100644 --- a/src/pages/Order/Card/OrderCatalog.vue +++ b/src/pages/Order/Card/OrderCatalog.vue @@ -1,7 +1,7 @@ <script setup> import { useStateStore } from 'stores/useStateStore'; import { useRoute, useRouter } from 'vue-router'; -import { onMounted, onUnmounted, ref, computed, watch } from 'vue'; +import { onMounted, ref, computed, watch, provide } from 'vue'; import axios from 'axios'; import { useI18n } from 'vue-i18n'; import VnPaginate from 'src/components/ui/VnPaginate.vue'; @@ -18,6 +18,7 @@ const dataKey = 'OrderCatalogList'; const arrayData = useArrayData(dataKey); const store = arrayData.store; const tags = ref([]); +const itemRefs = ref({}); let catalogParams = { orderFk: route.params.id, @@ -29,8 +30,6 @@ onMounted(() => { checkOrderConfirmation(); }); -onUnmounted(() => (stateStore.rightDrawer = false)); - async function checkOrderConfirmation() { const response = await axios.get(`Orders/${route.params.id}`); if (response.data.isConfirmed === 1) { @@ -76,6 +75,19 @@ watch( }, { immediate: true } ); +const onItemSaved = (updatedItem) => { + requestAnimationFrame(() => { + scrollToItem(updatedItem.items[0].itemFk); + }); +}; + +const scrollToItem = async (id) => { + const element = itemRefs.value[id]?.$el; + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } +}; +provide('onItemSaved', onItemSaved); </script> <template> @@ -115,6 +127,7 @@ watch( <CatalogItem v-for="row in rows" :key="row.id" + :ref="(el) => (itemRefs[row.id] = el)" :item="row" is-catalog class="fill-icon" diff --git a/src/pages/Order/Card/OrderCatalogItemDialog.vue b/src/pages/Order/Card/OrderCatalogItemDialog.vue index 09a25fcb8..b1cd8ed6b 100644 --- a/src/pages/Order/Card/OrderCatalogItemDialog.vue +++ b/src/pages/Order/Card/OrderCatalogItemDialog.vue @@ -1,41 +1,53 @@ <script setup> -import toCurrency from '../../../filters/toCurrency'; -import { ref } from 'vue'; +import toCurrency from 'src/filters/toCurrency'; +import { inject, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; import { useRoute } from 'vue-router'; import useNotify from 'composables/useNotify'; import { useArrayData } from 'composables/useArrayData'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; const { t } = useI18n(); const { notify } = useNotify(); const emit = defineEmits(['added']); const route = useRoute(); const props = defineProps({ - prices: { + item: { type: Array, required: true, }, }); - -const fields = ref((props.prices || []).map((item) => ({ ...item, quantity: 0 }))); +const onItemSaved = inject('onItemSaved'); +const prices = ref((props.item.prices || []).map((item) => ({ ...item, quantity: 0 }))); const descriptorData = useArrayData('orderData'); const isLoading = ref(false); const addToOrder = async () => { if (isLoading.value) return; isLoading.value = true; - const items = (fields.value || []).filter((item) => Number(item.quantity) > 0); + const items = (prices.value || []).filter((item) => Number(item.quantity) > 0); await axios.post('/OrderRows/addToOrder', { items, orderFk: Number(route.params.id), }); notify(t('globals.dataSaved'), 'positive'); - emit('added'); - descriptorData.fetch({}); + await descriptorData.fetch({}); + onItemSaved({ ...props, items, saved: true }); + emit('added', items); isLoading.value = false; }; const canAddToOrder = () => { - return (fields.value || []).some((item) => Number(item.quantity) > 0); + let canAddToOrder = (prices.value || []).some((price) => Number(price.quantity) > 0); + if (canAddToOrder) { + const excedQuantity = prices.value.reduce( + (acc, { quantity }) => acc + quantity, + 0 + ); + if (excedQuantity > props.item.available) { + canAddToOrder = false; + } + } + return canAddToOrder; }; </script> @@ -44,30 +56,33 @@ const canAddToOrder = () => { <QForm @submit="addToOrder"> <QMarkupTable class="shadow-0"> <tbody> - <tr v-for="item in fields" :key="item.warehouse"> + <tr v-for="price in prices" :key="price.warehouse"> <td class="text-bold q-pr-md td" style="width: 35%"> - {{ item.warehouse }} + {{ price.warehouse }} </td> <td class="text-right" style="width: 35%"> <span class="link" - @click=" + @click.shift=" () => { - item.quantity += item.grouping; + price.quantity -= price.grouping; + } + " + @click.exact=" + () => { + price.quantity += price.grouping; } " > - {{ item.grouping }} + {{ price.grouping }} </span> - x {{ toCurrency(item.price) }} + x {{ toCurrency(price.price) }} </td> <td class="text-right"> - <QInput - v-model.number="item.quantity" - type="number" - :step="item.grouping" + <VnInputNumber + v-model.number="price.quantity" + :step="price.grouping" min="0" - dense /> </td> </tr> diff --git a/src/pages/Order/Card/OrderCreateDialog.vue b/src/pages/Order/Card/OrderCreateDialog.vue index c78b04d7f..9a53b9e30 100644 --- a/src/pages/Order/Card/OrderCreateDialog.vue +++ b/src/pages/Order/Card/OrderCreateDialog.vue @@ -1,9 +1,8 @@ <script setup> import { useRouter } from 'vue-router'; -import { reactive, onMounted, ref } from 'vue'; +import { reactive, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; -import { useState } from 'composables/useState'; import FormModelPopup from 'components/FormModelPopup.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelect from 'components/common/VnSelect.vue'; @@ -11,29 +10,12 @@ import VnInputDate from 'components/common/VnInputDate.vue'; import { useDialogPluginComponent } from 'quasar'; const { t } = useI18n(); -const state = useState(); const ORDER_MODEL = 'order'; const router = useRouter(); const agencyList = ref([]); -const addressList = ref([]); defineEmits(['confirm', ...useDialogPluginComponent.emits]); -const fetchAddressList = async (addressId) => { - const { data } = await axios.get('addresses', { - params: { - filter: JSON.stringify({ - fields: ['id', 'nickname', 'street', 'city'], - where: { id: addressId }, - }), - }, - }); - addressList.value = data; - if (addressList.value?.length === 1) { - state.get(ORDER_MODEL).addressId = addressList.value[0].id; - } -}; - const fetchAgencyList = async (landed, addressFk) => { if (!landed || !addressFk) { return; @@ -59,17 +41,9 @@ const initialFormState = reactive({ clientFk: $props.clientFk, }); -const onClientChange = async (clientId = $props.clientFk) => { - const { data } = await axios.get(`Clients/${clientId}`); - await fetchAddressList(data.defaultAddressFk); -}; - async function onDataSaved(_, id) { await router.push({ path: `/order/${id}/catalog` }); } -onMounted(async () => { - await onClientChange(); -}); </script> <template> @@ -90,10 +64,9 @@ onMounted(async () => { option-value="id" option-label="name" :filter="{ - fields: ['id', 'name', 'defaultAddressFk'], + fields: ['id', 'name'], }" hide-selected - @update:model-value="onClientChange" > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -110,7 +83,7 @@ onMounted(async () => { :label="t('order.form.addressFk')" v-model="data.addressId" url="addresses" - :fields="['id', 'nickname', 'defaultAddressFk', 'street', 'city']" + :fields="['id', 'nickname', 'street', 'city']" sort-by="id" option-value="id" option-label="street" diff --git a/src/pages/Order/Card/OrderFilter.vue b/src/pages/Order/Card/OrderFilter.vue index 917369919..dc86600ac 100644 --- a/src/pages/Order/Card/OrderFilter.vue +++ b/src/pages/Order/Card/OrderFilter.vue @@ -6,6 +6,7 @@ import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnSelect from 'components/common/VnSelect.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import VnInput from 'components/common/VnInput.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const props = defineProps({ @@ -61,28 +62,16 @@ const sourceList = ref([]); outlined rounded /> - <VnSelect - :label="t('salesPerson')" + <VnSelectWorker + :label="t('globals.salesPerson')" v-model="params.workerFk" - url="Workers/search" - :filter="{ departmentCodes: ['VT'] }" - sort-by="nickname ASC" - option-label="nickname" + :params="{ + departmentCodes: ['VT'], + }" dense outlined rounded - > - <template #option="{ itemProps, opt }"> - <QItem v-bind="itemProps"> - <QItemSection> - <QItemLabel>{{ opt.name }}</QItemLabel> - <QItemLabel caption> - {{ opt.nickname }},{{ opt.code }} - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> + /> <VnInputDate v-model="params.from" :label="t('fromLanded')" diff --git a/src/pages/Order/OrderList.vue b/src/pages/Order/OrderList.vue index c47a9b2ec..dfeb0f6e2 100644 --- a/src/pages/Order/OrderList.vue +++ b/src/pages/Order/OrderList.vue @@ -1,7 +1,7 @@ <script setup> import axios from 'axios'; import { useI18n } from 'vue-i18n'; -import { computed, onMounted, ref } from 'vue'; +import { computed, ref, onMounted } from 'vue'; import { dashIfEmpty, toCurrency, toDate } from 'src/filters'; import OrderSummary from 'pages/Order/Card/OrderSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; @@ -15,14 +15,13 @@ import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vu import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue'; import { toDateTimeFormat } from 'src/filters/date'; import { useRoute } from 'vue-router'; -import dataByOrder from 'src/utils/dataByOrder'; const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const tableRef = ref(); const agencyList = ref([]); -const addressesList = ref([]); const route = useRoute(); +const addressOptions = ref([]); const columns = computed(() => [ { align: 'left', @@ -148,16 +147,12 @@ onMounted(() => { const id = JSON.parse(clientId); fetchClientAddress(id.clientFk); }); + async function fetchClientAddress(id, formData = {}) { - const { data } = await axios.get(`Clients/${id}`, { - params: { - filter: { - order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'], - include: { relation: 'addresses' }, - }, - }, - }); - addressesList.value = data.addresses; + const { data } = await axios.get( + `Clients/${id}/addresses?filter[order]=isActive DESC` + ); + addressOptions.value = data; formData.addressId = data.defaultAddressFk; fetchAgencies(formData); } @@ -168,7 +163,7 @@ async function fetchAgencies({ landed, addressId }) { const { data } = await axios.get('Agencies/landsThatDay', { params: { addressFk: addressId, landed }, }); - agencyList.value = dataByOrder(data, 'agencyMode ASC'); + agencyList.value = data; } const getDateColor = (date) => { @@ -252,34 +247,29 @@ const getDateColor = (date) => { </VnSelect> <VnSelect v-model="data.addressId" - :options="addressesList" + :options="addressOptions" :label="t('module.address')" option-value="id" option-label="nickname" @update:model-value="() => fetchAgencies(data)" > <template #option="scope"> - <QItem - v-bind="scope.itemProps" - :class="{ disabled: !scope.opt.isActive }" - > - <QItemSection style="min-width: min-content" avatar> - <QIcon - v-if=" - scope.opt.isActive && data.addressId === scope.opt.id - " - size="sm" - color="grey" - name="star" - class="fill-icon" - /> - </QItemSection> + <QItem v-bind="scope.itemProps"> <QItemSection> - <QItemLabel> - {{ scope.opt.nickname }} - </QItemLabel> - <QItemLabel caption> - {{ `${scope.opt.street}, ${scope.opt.city}` }} + <QItemLabel + :class="{ + 'color-vn-label': !scope.opt?.isActive, + }" + > + {{ + `${ + !scope.opt?.isActive + ? t('basicData.inactive') + : '' + } ` + }} + {{ scope.opt?.nickname }}: {{ scope.opt?.street }}, + {{ scope.opt?.city }} </QItemLabel> </QItemSection> </QItem> diff --git a/src/pages/Route/Card/RouteFilter.vue b/src/pages/Route/Card/RouteFilter.vue index a6cd149f1..6f65313d3 100644 --- a/src/pages/Route/Card/RouteFilter.vue +++ b/src/pages/Route/Card/RouteFilter.vue @@ -4,6 +4,7 @@ import VnFilterPanel from 'components/ui/VnFilterPanel.vue'; import VnSelect from 'components/common/VnSelect.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import VnInput from 'components/common/VnInput.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const props = defineProps({ @@ -31,29 +32,13 @@ const emit = defineEmits(['search']); <template #body="{ params }"> <QItem class="q-my-sm"> <QItemSection> - <VnSelect - :label="t('Worker')" + <VnSelectWorker v-model="params.workerFk" - url="Workers/search" - sort-by="nickname ASC" - option-value="id" - option-label="nickname" dense outlined rounded :input-debounce="0" - > - <template #option="{ itemProps, opt }"> - <QItem v-bind="itemProps"> - <QItemSection> - <QItemLabel>{{ opt.name }}</QItemLabel> - <QItemLabel caption> - {{ opt.nickname }},{{ opt.code }} - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> + /> </QItemSection> </QItem> <QItem class="q-my-sm"> diff --git a/src/pages/Route/Card/RouteForm.vue b/src/pages/Route/Card/RouteForm.vue index 8c89718fa..aa5caf1ef 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 VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const route = useRoute(); @@ -94,26 +95,7 @@ const onSave = (data, response) => { > <template #form="{ data }"> <VnRow> - <VnSelect - :label="t('Worker')" - v-model="data.workerFk" - url="Workers/search" - sort-by="nickname ASC" - option-value="id" - option-label="nickname" - :input-debounce="0" - > - <template #option="{ itemProps, opt }"> - <QItem v-bind="itemProps"> - <QItemSection> - <QItemLabel>{{ opt.name }}</QItemLabel> - <QItemLabel caption> - {{ opt.nickname }}, {{ opt.code }} - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> + <VnSelectWorker v-model="data.workerFk" /> <VnSelect :label="t('Vehicle')" v-model="data.vehicleFk" diff --git a/src/pages/Route/RouteExtendedList.vue b/src/pages/Route/RouteExtendedList.vue index dbf646935..38e907ce0 100644 --- a/src/pages/Route/RouteExtendedList.vue +++ b/src/pages/Route/RouteExtendedList.vue @@ -223,10 +223,10 @@ function navigate(id) { router.push({ path: `/route/${id}` }); } -const cloneRoutes = () => { +const cloneRoutes = async () => { if (!selectedRows.value.length || !startingDate.value) return; - axios.post('Routes/clone', { - created: startingDate.value, + await axios.post('Routes/clone', { + dated: startingDate.value, ids: selectedRows.value.map((row) => row?.id), }); startingDate.value = null; @@ -274,7 +274,6 @@ const openTicketsDialog = (id) => { <QCardSection> <p class="text-h6 q-ma-none">{{ t('route.Select the starting date') }}</p> </QCardSection> - <QCardSection class="q-pt-none"> <VnInputDate :label="t('route.Stating date')" diff --git a/src/pages/Shelving/ShelvingList.vue b/src/pages/Shelving/ShelvingList.vue index d29f6ff15..a6ea50cf4 100644 --- a/src/pages/Shelving/ShelvingList.vue +++ b/src/pages/Shelving/ShelvingList.vue @@ -2,7 +2,7 @@ import VnPaginate from 'components/ui/VnPaginate.vue'; import { useStateStore } from 'stores/useStateStore'; import { useI18n } from 'vue-i18n'; -import { onMounted, onUnmounted } from 'vue'; +import { onMounted } from 'vue'; import CardList from 'components/ui/CardList.vue'; import VnLv from 'components/ui/VnLv.vue'; import { useRouter } from 'vue-router'; @@ -21,7 +21,6 @@ const filter = { }; onMounted(() => (stateStore.rightDrawer = true)); -onUnmounted(() => (stateStore.rightDrawer = false)); function navigate(id) { router.push({ path: `/shelving/${id}` }); diff --git a/src/pages/Supplier/Card/SupplierBasicData.vue b/src/pages/Supplier/Card/SupplierBasicData.vue index 70f6432dd..842109656 100644 --- a/src/pages/Supplier/Card/SupplierBasicData.vue +++ b/src/pages/Supplier/Card/SupplierBasicData.vue @@ -5,6 +5,7 @@ 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(); @@ -30,31 +31,11 @@ const companySizes = [ :rules="validate('supplier.nickname')" clearable /> - <VnSelect - :label="t('supplier.basicData.workerFk')" + <VnSelectWorker v-model="data.workerFk" - url="Workers/search" - sort-by="nickname ASC" + has-info="Responsible for approving invoices" :rules="validate('supplier.workerFk')" - > - <template #append> - <QIcon name="info" class="cursor-pointer"> - <QTooltip>{{ - t('Responsible for approving invoices') - }}</QTooltip> - </QIcon> - </template> - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel>{{ scope.opt?.name }}</QItemLabel> - <QItemLabel caption> - {{ scope.opt?.nickname }}, {{ scope.opt?.id }} - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> + /> <VnSelect :label="t('supplier.basicData.size')" v-model="data.companySize" @@ -102,6 +83,5 @@ const companySizes = [ <i18n> es: - Responsible for approving invoices: Responsable de aprobar las facturas Small(1-5), Medium(6-50), Big(> 50): Pequeño(1-5), Mediano(6-50), Grande(> 50) </i18n> diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicData.vue b/src/pages/Ticket/Card/BasicData/TicketBasicData.vue index ab96a6e75..0f6cc5772 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicData.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicData.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; +import { ref, computed, onMounted, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; @@ -134,8 +134,6 @@ onMounted(() => { loadDefaultTicketAction(); ticketHaveNegatives(); }); - -onUnmounted(() => (stateStore.rightDrawer = false)); </script> <template> diff --git a/src/pages/Ticket/Card/TicketComponents.vue b/src/pages/Ticket/Card/TicketComponents.vue index b88dd89e8..8fab1968b 100644 --- a/src/pages/Ticket/Card/TicketComponents.vue +++ b/src/pages/Ticket/Card/TicketComponents.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'; +import { ref, computed, onMounted, watch, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; @@ -168,8 +168,6 @@ const getTicketVolume = async () => { onMounted(() => { stateStore.rightDrawer = true; }); - -onUnmounted(() => (stateStore.rightDrawer = false)); </script> <template> diff --git a/src/pages/Ticket/Card/TicketCreateTracking.vue b/src/pages/Ticket/Card/TicketCreateTracking.vue index 3ea762c6c..5c1e916f2 100644 --- a/src/pages/Ticket/Card/TicketCreateTracking.vue +++ b/src/pages/Ticket/Card/TicketCreateTracking.vue @@ -9,6 +9,7 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import FetchData from 'components/FetchData.vue'; import { useState } from 'src/composables/useState'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const emit = defineEmits(['onRequestCreated']); @@ -46,29 +47,7 @@ const onStateFkChange = (formData) => (formData.userFk = user.value.id); option-label="name" option-value="id" /> - <VnSelect - :label="t('expedition.worker')" - v-model="data.userFk" - url="Workers/search" - fields=" ['id', 'name']" - sort-by="name ASC" - hide-selected - option-label="name" - option-value="id" - > - <template #option="{ opt, itemProps }"> - <QItem v-bind="itemProps"> - <QItemSection> - <QItemLabel> - {{ opt.name }} - </QItemLabel> - <QItemLabel caption> - {{ opt.nickname }}, {{ opt.code }} - </QItemLabel> - </QItemSection> - </QItem> - </template></VnSelect - > + <VnSelectWorker v-model="data.userFk" :fields="['id', 'name']" /> </VnRow> </template> </FormModelPopup> diff --git a/src/pages/Ticket/Card/TicketDescriptorMenu.vue b/src/pages/Ticket/Card/TicketDescriptorMenu.vue index 60a703f84..38e2af612 100644 --- a/src/pages/Ticket/Card/TicketDescriptorMenu.vue +++ b/src/pages/Ticket/Card/TicketDescriptorMenu.vue @@ -1,6 +1,6 @@ <script setup> import axios from 'axios'; -import { computed, ref, toRefs } from 'vue'; +import { computed, onMounted, ref, toRefs, watch } from 'vue'; import { useQuasar } from 'quasar'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; @@ -24,6 +24,15 @@ const props = defineProps({ }, }); +onMounted(() => { + restoreTicket(); +}); + +watch( + () => props.ticket, + () => restoreTicket +); + const { push, currentRoute } = useRouter(); const { dialog, notify } = useQuasar(); const { t } = useI18n(); @@ -42,6 +51,7 @@ const hasPdf = ref(); const weight = ref(); const hasDocuwareFile = ref(); const quasar = useQuasar(); +const canRestoreTicket = ref(false); const actions = { clone: async () => { const opts = { message: t('Ticket cloned'), type: 'positive' }; @@ -373,6 +383,54 @@ async function uploadDocuware(force) { if (data) notify({ message: t('PDF sent!'), type: 'positive' }); } + +const restoreTicket = async () => { + const filter = { + fields: ['id', 'originFk', 'creationDate', 'newInstance'], + where: { + originFk: ticketId.value, + newInstance: { like: '%"isDeleted":true%' }, + }, + order: 'creationDate DESC', + limit: 1, + }; + const params = { filter: JSON.stringify(filter) }; + + const { data } = await axios.get(`TicketLogs`, { params }); + + if (data && data.length) { + const now = Date.vnNew(); + const maxDate = new Date(data[0].creationDate); + maxDate.setHours(maxDate.getHours() + 1); + if (now <= maxDate) { + return (canRestoreTicket.value = true); + } + return (canRestoreTicket.value = false); + } + return (canRestoreTicket.value = false); +}; + +async function openRestoreConfirmation(force) { + if (!force) + return quasar + .dialog({ + component: VnConfirm, + componentProps: { + title: t('Are you sure you want to restore the ticket?'), + message: t('You are going to restore this ticket'), + }, + }) + .onOk(async () => { + ticketToRestore(); + }); +} + +async function ticketToRestore() { + const { data } = await axios.post(`Tickets/${ticketId.value}/restore`); + if (data) { + notify({ message: t('Ticket restored'), type: 'positive' }); + } +} </script> <template> <FetchData @@ -560,6 +618,12 @@ async function uploadDocuware(force) { </QItemSection> <QItemSection>{{ t('Show Proforma') }}</QItemSection> </QItem> + <QItem v-if="canRestoreTicket" @click="openRestoreConfirmation()" v-ripple clickable> + <QItemSection avatar> + <QIcon name="restore" /> + </QItemSection> + <QItemSection>{{ t('Restore ticket') }}</QItemSection> + </QItem> <QItem v-if="isEditable" @click="showChangeTimeDialog = !showChangeTimeDialog" @@ -746,4 +810,8 @@ es: You are going to delete this ticket: Vas a eliminar este ticket as PDF signed: como PDF firmado Are you sure you want to replace this delivery note?: ¿Seguro que quieres reemplazar este albarán? + Restore ticket: Restaurar ticket + Are you sure you want to restore the ticket?: ¿Seguro que quieres restaurar el ticket? + You are going to restore this ticket: Vas a restaurar este ticket + Ticket restored: Ticket restaurado </i18n> diff --git a/src/pages/Ticket/Card/TicketExpedition.vue b/src/pages/Ticket/Card/TicketExpedition.vue index b7f1f4dd0..38010a997 100644 --- a/src/pages/Ticket/Card/TicketExpedition.vue +++ b/src/pages/Ticket/Card/TicketExpedition.vue @@ -1,5 +1,5 @@ <script setup> -import { onMounted, ref, computed, onUnmounted } from 'vue'; +import { onMounted, ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; @@ -197,8 +197,6 @@ onMounted(async () => { const filteredColumns = columns.value.filter((col) => col.name !== 'history'); allColumnNames.value = filteredColumns.map((col) => col.name); }); - -onUnmounted(() => (stateStore.rightDrawer = false)); </script> <template> diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index b534170c9..a7e0f6171 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -1,5 +1,5 @@ <script setup> -import { onMounted, ref, computed, onUnmounted, watch } from 'vue'; +import { onMounted, ref, computed, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRouter, useRoute } from 'vue-router'; import { useQuasar } from 'quasar'; @@ -421,8 +421,6 @@ onMounted(async () => { getConfig(); }); -onUnmounted(() => (stateStore.rightDrawer = false)); - const items = ref([]); const newRow = ref({}); @@ -770,7 +768,7 @@ watch( </template> <template #column-item="{ row }"> <div class="row column full-width justify-between items-start"> - {{ row?.item?.name }} + {{ row?.concept }} <div v-if="row?.item?.subName" class="subName"> {{ row?.item?.subName.toUpperCase() }} </div> diff --git a/src/pages/Ticket/Card/TicketSaleMoreActions.vue b/src/pages/Ticket/Card/TicketSaleMoreActions.vue index bd2099756..4cc96e9e2 100644 --- a/src/pages/Ticket/Card/TicketSaleMoreActions.vue +++ b/src/pages/Ticket/Card/TicketSaleMoreActions.vue @@ -11,7 +11,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import useNotify from 'src/composables/useNotify.js'; import axios from 'axios'; import { toDateFormat } from 'src/filters/date'; -import { useRole } from 'src/composables/useRole'; +import { useAcl } from 'src/composables/useAcl'; import { useVnConfirm } from 'composables/useVnConfirm'; const emit = defineEmits(['updateDiscounts', 'getMana', 'refreshTable']); @@ -48,7 +48,7 @@ const { push } = useRouter(); const { t } = useI18n(); const { dialog } = useQuasar(); const { notify } = useNotify(); -const role = useRole(); +const acl = useAcl(); const btnDropdownRef = ref(null); const { openConfirmationModal } = useVnConfirm(); @@ -58,8 +58,10 @@ const isClaimable = computed(() => { if (ticket.value) { const landedPlusWeek = new Date(ticket.value.landed); landedPlusWeek.setDate(landedPlusWeek.getDate() + 7); - const hasClaimManagerRole = role.hasAny('claimManager'); - return landedPlusWeek >= Date.vnNew() || hasClaimManagerRole; + const createAfterDeadline = acl.hasAny([ + { model: 'Claim', props: 'createAfterDeadline', accessType: 'WRITE' }, + ]); + return landedPlusWeek >= Date.vnNew() || createAfterDeadline; } return false; }); diff --git a/src/pages/Ticket/Card/TicketVolume.vue b/src/pages/Ticket/Card/TicketVolume.vue index 7aeeec952..20eced98e 100644 --- a/src/pages/Ticket/Card/TicketVolume.vue +++ b/src/pages/Ticket/Card/TicketVolume.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'; +import { ref, computed, onMounted, watch, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; @@ -90,8 +90,6 @@ const applyVolumes = async (salesData) => { }; onMounted(() => (stateStore.rightDrawer = true)); - -onUnmounted(() => (stateStore.rightDrawer = false)); </script> <template> diff --git a/src/pages/Ticket/TicketAdvanceFilter.vue b/src/pages/Ticket/TicketAdvanceFilter.vue index a1d301f35..6528bf380 100644 --- a/src/pages/Ticket/TicketAdvanceFilter.vue +++ b/src/pages/Ticket/TicketAdvanceFilter.vue @@ -57,7 +57,6 @@ onMounted(async () => await getItemPackingTypes()); search-url="advanceTickets" :data-key="props.dataKey" :search-button="true" - :hidden-tags="['search']" :unremovable-params="['warehouseFk', 'dateFuture', 'dateToAdvance']" > <template #tags="{ tag, formatFn }"> diff --git a/src/pages/Ticket/TicketFutureFilter.vue b/src/pages/Ticket/TicketFutureFilter.vue index ffe967272..d28b0af71 100644 --- a/src/pages/Ticket/TicketFutureFilter.vue +++ b/src/pages/Ticket/TicketFutureFilter.vue @@ -59,7 +59,6 @@ onMounted(async () => { /> <VnFilterPanel :data-key="props.dataKey" - :hidden-tags="['search']" :un-removable-params="['warehouseFk', 'originScopeDays ', 'futureScopeDays']" > <template #tags="{ tag, formatFn }"> diff --git a/src/pages/Ticket/TicketWeekly.vue b/src/pages/Ticket/TicketWeekly.vue index 306f414df..0e18fe028 100644 --- a/src/pages/Ticket/TicketWeekly.vue +++ b/src/pages/Ticket/TicketWeekly.vue @@ -1,5 +1,5 @@ <script setup> -import { onMounted, ref, computed, onUnmounted } from 'vue'; +import { onMounted, ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectCache from 'src/components/common/VnSelectCache.vue'; @@ -170,8 +170,6 @@ onMounted(async () => { const filteredColumns = columns.value.filter((col) => col.name !== 'actions'); allColumnNames.value = filteredColumns.map((col) => col.name); }); - -onUnmounted(() => (stateStore.rightDrawer = false)); </script> <template> diff --git a/src/pages/Travel/ExtraCommunity.vue b/src/pages/Travel/ExtraCommunity.vue index 9286f5e3e..675a44979 100644 --- a/src/pages/Travel/ExtraCommunity.vue +++ b/src/pages/Travel/ExtraCommunity.vue @@ -57,7 +57,7 @@ const travelKgPercentages = ref([]); const tableColumnComponents = { id: { component: QBtn, - attrs: { flat: true, color: 'primary' }, + attrs: { flat: true, color: 'primary', dense: true }, }, cargoSupplierNickname: { component: QBtn, @@ -178,6 +178,7 @@ const columns = computed(() => [ align: 'left', showValue: false, sortable: true, + style: 'min-width: 170px;', }, { label: t('globals.packages'), @@ -237,7 +238,7 @@ const columns = computed(() => [ format: (value) => toDate(value), }, { - label: t('globals.wareHhuseIn'), + label: t('globals.warehouseIn'), field: 'warehouseInName', name: 'warehouseInName', align: 'left', @@ -506,7 +507,7 @@ const getColor = (percentage) => { :key="col.name" :props="props" @click="stopEventPropagation($event, col)" - auto-width + :style="col.style" > <component :is="tableColumnComponents[col.name].component" @@ -581,7 +582,7 @@ const getColor = (percentage) => { }" > <QTd> - <QBtn flat class="link">{{ entry.id }} </QBtn> + <QBtn dense flat class="link">{{ entry.id }} </QBtn> <EntryDescriptorProxy :id="entry.id" /> </QTd> <QTd> @@ -637,6 +638,18 @@ const getColor = (percentage) => { :deep(.q-table) { border-collapse: collapse; + + th { + padding: 0; + } + tbody tr td { + &:nth-child(1) { + max-width: 65px; + } + &:nth-child(4) { + padding: 0; + } + } } .q-td :deep(input) { @@ -684,7 +697,6 @@ const getColor = (percentage) => { width: max-content; } </style> - <i18n> en: searchExtraCommunity: Search for extra community shipping diff --git a/src/pages/Worker/Card/WorkerBalance.vue b/src/pages/Worker/Card/WorkerBalance.vue index 25ab92c9b..95e0b986e 100644 --- a/src/pages/Worker/Card/WorkerBalance.vue +++ b/src/pages/Worker/Card/WorkerBalance.vue @@ -15,6 +15,9 @@ const columns = computed(() => [ name: 'paymentDate', label: t('worker.balance.tableVisibleColumns.paymentDate'), create: true, + columnCreate: { + required: true, + }, component: 'date', field: 'paymentDate', cardVisible: true, @@ -24,6 +27,9 @@ const columns = computed(() => [ name: 'incomeTypeFk', label: t('worker.balance.tableVisibleColumns.incomeType'), create: true, + columnCreate: { + required: true, + }, component: 'select', attrs: { options: payrollComponents, @@ -37,6 +43,9 @@ const columns = computed(() => [ name: 'debit', label: t('worker.balance.tableVisibleColumns.debit'), create: true, + columnCreate: { + required: true, + }, component: 'input', field: 'debit', cardVisible: true, @@ -46,6 +55,9 @@ const columns = computed(() => [ name: 'credit', label: t('worker.balance.tableVisibleColumns.credit'), create: true, + columnCreate: { + required: true, + }, component: 'input', field: 'credit', cardVisible: true, diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index 13f9e9795..73ea34fe9 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -206,6 +206,8 @@ const handlePhotoUpdated = (evt = false) => { <i18n> es: + Go to client: Ir a cliente + Go to user: Ir al usuario Click to allow the user to be disabled: Marcar para deshabilitar Click to exclude the user from getting disabled: Marcar para no deshabilitar </i18n> diff --git a/src/pages/Worker/Card/WorkerPda.vue b/src/pages/Worker/Card/WorkerPda.vue index 94f4e0d95..3ceee2493 100644 --- a/src/pages/Worker/Card/WorkerPda.vue +++ b/src/pages/Worker/Card/WorkerPda.vue @@ -133,6 +133,7 @@ function reloadData() { option-value="id" id="deviceProductionFk" hide-selected + data-cy="pda-dialog-select" > <template #option="scope"> <QItem v-bind="scope.itemProps"> diff --git a/src/pages/Worker/WorkerCreate.vue b/src/pages/Worker/WorkerCreate.vue index 5676837dd..a4c6c2a06 100644 --- a/src/pages/Worker/WorkerCreate.vue +++ b/src/pages/Worker/WorkerCreate.vue @@ -14,6 +14,7 @@ import FormModel from 'components/FormModel.vue'; import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue'; import VnRadio from 'src/components/common/VnRadio.vue'; import { useState } from 'src/composables/useState'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const user = useState().getUser(); @@ -149,27 +150,11 @@ async function autofillBic(worker) { hide-selected :rules="validate('Worker.company')" /> - <VnSelect + <VnSelectWorker :label="t('worker.summary.boss')" v-model="data.bossFk" - url="Workers/search" - option-value="id" - option-label="name" - hide-selected :rules="validate('Worker.boss')" - > - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel>{{ scope.opt.name }}</QItemLabel> - <QItemLabel caption - >{{ scope.opt.nickname }}, - {{ scope.opt.code }} - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> + /> </VnRow> <VnRow> <VnInput diff --git a/src/pages/Worker/WorkerList.vue b/src/pages/Worker/WorkerList.vue index 81b231710..bd1c27938 100644 --- a/src/pages/Worker/WorkerList.vue +++ b/src/pages/Worker/WorkerList.vue @@ -18,6 +18,7 @@ import RightMenu from 'src/components/common/RightMenu.vue'; import WorkerFilter from './WorkerFilter.vue'; import { useState } from 'src/composables/useState'; import axios from 'axios'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const tableRef = ref(); @@ -260,26 +261,10 @@ async function autofillBic(worker) { option-label="code" hide-selected /> - <VnSelect + <VnSelectWorker :label="t('worker.summary.boss')" v-model="data.bossFk" - url="Workers/search" - option-value="id" - option-label="name" - hide-selected - > - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel>{{ scope.opt.name }}</QItemLabel> - <QItemLabel caption - >{{ scope.opt.nickname }}, - {{ scope.opt.code }} - </QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> + /> </VnRow> <VnRow> <VnInput v-model="data.fi" :label="t('worker.create.fi')" /> @@ -376,6 +361,7 @@ async function autofillBic(worker) { <i18n> es: + Create worker: Crear trabajador Search worker: Buscar trabajador You can search by worker id or name: Puedes buscar por id o nombre del trabajador </i18n> diff --git a/src/pages/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue index 535f2393d..731e03ba7 100644 --- a/src/pages/Zone/Card/ZoneBasicData.vue +++ b/src/pages/Zone/Card/ZoneBasicData.vue @@ -32,7 +32,12 @@ const agencyOptions = ref([]); <FormModel :url="`Zones/${route.params.id}`" auto-load model="zone"> <template #form="{ data, validate }"> <VnRow> - <VnInput :label="t('Name')" clearable v-model="data.name" /> + <VnInput + data-cy="zone-basic-data-name" + :label="t('Name')" + clearable + v-model="data.name" + /> </VnRow> <VnRow> diff --git a/src/pages/Zone/Card/ZoneEvents.vue b/src/pages/Zone/Card/ZoneEvents.vue index 6d5b37096..0685c264f 100644 --- a/src/pages/Zone/Card/ZoneEvents.vue +++ b/src/pages/Zone/Card/ZoneEvents.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, onUnmounted } from 'vue'; +import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import ZoneEventsPanel from './ZoneEventsPanel.vue'; @@ -40,8 +40,6 @@ const onZoneEventFormClose = () => { showZoneEventForm.value = false; zoneEventsFormProps.value = {}; }; - -onUnmounted(() => (stateStore.rightDrawer = false)); </script> <template> diff --git a/src/pages/Zone/ZoneFilterPanel.vue b/src/pages/Zone/ZoneFilterPanel.vue index 55d21756d..3a35527ab 100644 --- a/src/pages/Zone/ZoneFilterPanel.vue +++ b/src/pages/Zone/ZoneFilterPanel.vue @@ -28,11 +28,7 @@ const agencies = ref([]); @on-fetch="(data) => (agencies = data)" auto-load /> - <VnFilterPanel - :data-key="props.dataKey" - :search-button="true" - :hidden-tags="['search']" - > + <VnFilterPanel :data-key="props.dataKey" :search-button="true"> <template #tags="{ tag }"> <div class="q-gutter-x-xs"> <strong>{{ t(`filterPanel.${tag.label}`) }}: </strong> diff --git a/src/router/modules/travel.js b/src/router/modules/travel.js index 627692be8..dff693d2f 100644 --- a/src/router/modules/travel.js +++ b/src/router/modules/travel.js @@ -75,9 +75,9 @@ export default { }, { name: 'TravelHistory', - path: 'history', + path: 'log', meta: { - title: 'history', + title: 'log', icon: 'history', }, component: () => import('src/pages/Travel/Card/TravelLog.vue'), diff --git a/src/router/modules/zone.js b/src/router/modules/zone.js index 1f27cc76f..c5ebe762e 100644 --- a/src/router/modules/zone.js +++ b/src/router/modules/zone.js @@ -106,7 +106,7 @@ export default { }, { name: 'ZoneHistory', - path: 'history', + path: 'log', meta: { title: 'log', icon: 'history', diff --git a/test/cypress/integration/client/clientBasicData.spec.js b/test/cypress/integration/client/clientBasicData.spec.js index efaad33c2..bed28dc22 100644 --- a/test/cypress/integration/client/clientBasicData.spec.js +++ b/test/cypress/integration/client/clientBasicData.spec.js @@ -7,8 +7,8 @@ describe('Client basic data', () => { }); it('Should load layout', () => { cy.get('.q-card').should('be.visible'); - cy.dataCy('customerPhone').filter('input').should('be.visible'); - cy.dataCy('customerPhone').filter('input').type('123456789'); + cy.dataCy('customerPhone').find('input').should('be.visible'); + cy.dataCy('customerPhone').find('input').type('123456789'); cy.get('.q-btn-group > .q-btn--standard').click(); cy.intercept('PATCH', '/api/Clients/1102', (req) => { const { body } = req; diff --git a/test/cypress/integration/client/clientList.spec.js b/test/cypress/integration/client/clientList.spec.js index e89b5fc77..ce07deb16 100644 --- a/test/cypress/integration/client/clientList.spec.js +++ b/test/cypress/integration/client/clientList.spec.js @@ -22,10 +22,10 @@ describe('Client list', () => { const data = { Name: { val: `Name ${randomInt}` }, 'Social name': { val: `TEST ${randomInt}` }, - 'Tax number': { val: `20852${randomInt.length}3Z` }, + 'Tax number': { val: `20852${randomInt}3Z` }, 'Web user': { val: `user_test_${randomInt}` }, Street: { val: `C/ STREET ${randomInt}` }, - Email: { val: 'user.test@1.com' }, + Email: { val: `user.test${randomInt}@cypress.com` }, 'Sales person': { val: 'employee', type: 'select' }, Location: { val: '46000, Valencia(Province one), España', type: 'select' }, 'Business type': { val: 'Otros', type: 'select' }, @@ -34,7 +34,7 @@ describe('Client list', () => { cy.get('.q-mt-lg > .q-btn--standard').click(); - cy.checkNotification('Data saved'); + cy.checkNotification('Data created'); cy.url().should('include', '/summary'); }); it('Client list search client', () => { diff --git a/test/cypress/integration/item/ItemFixedPrice.spec.js b/test/cypress/integration/item/ItemFixedPrice.spec.js new file mode 100644 index 000000000..824ecf7a0 --- /dev/null +++ b/test/cypress/integration/item/ItemFixedPrice.spec.js @@ -0,0 +1,63 @@ +/// <reference types="cypress" /> +function goTo(n = 1) { + return `.q-virtual-scroll__content > :nth-child(${n})`; +} +const firstRow = goTo(); +`.q-virtual-scroll__content > :nth-child(2)`; +describe('Handle Items FixedPrice', () => { + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit('/#/item/fixed-price', { timeout: 5000 }); + cy.waitForElement('.q-table'); + cy.get( + '.q-header > .q-toolbar > :nth-child(1) > .q-btn__content > .q-icon' + ).click(); + }); + it('filter', function () { + cy.get('.category-filter > :nth-child(1) > .q-btn__content > .q-icon').click(); + cy.selectOption('.list > :nth-child(2)', 'Alstroemeria'); + cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click(); + + cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.selectOption(`${firstRow} > :nth-child(2)`, '#13'); + cy.get(`${firstRow} > :nth-child(4)`).find('input').type(1); + cy.get(`${firstRow} > :nth-child(5)`).find('input').type('2'); + cy.selectOption(`${firstRow} > :nth-child(9)`, 'Warehouse One'); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + /* ==== End Cypress Studio ==== */ + }); + it('Create and delete ', function () { + cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click(); + cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.selectOption(`${firstRow} > :nth-child(2)`, '#11'); + cy.get(`${firstRow} > :nth-child(4)`).type('1'); + cy.get(`${firstRow} > :nth-child(5)`).type('2'); + cy.selectOption(`${firstRow} > :nth-child(9)`, 'Warehouse One'); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click(); + cy.get(`${firstRow} > .text-right > .q-btn > .q-btn__content > .q-icon`).click(); + cy.get( + '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block' + ).click(); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + }); + + it('Massive edit', function () { + cy.get(' .bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner ').click(); + cy.get('#subToolbar > .q-btn--standard').click(); + cy.selectOption("[data-cy='field-to-edit']", 'Min price'); + cy.dataCy('value-to-edit').find('input').type('1'); + cy.get('.countLines').should('have.text', ' 1 '); + cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + }); + it('Massive remove', function () { + cy.get(' .bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner ').click(); + cy.get('#subToolbar > .q-btn--flat').click(); + cy.get( + '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block' + ).click(); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + }); +}); diff --git a/test/cypress/integration/ticket/ticketList.spec.js b/test/cypress/integration/ticket/ticketList.spec.js index bbdbcea92..c1d1a0655 100644 --- a/test/cypress/integration/ticket/ticketList.spec.js +++ b/test/cypress/integration/ticket/ticketList.spec.js @@ -37,7 +37,7 @@ describe('TicketList', () => { cy.dataCy('ticketSummary').should('exist'); }); - it('Client list create new client', () => { + it.only('Client list create new client', () => { cy.dataCy('vnTableCreateBtn').should('exist'); cy.dataCy('vnTableCreateBtn').click(); const data = { @@ -47,7 +47,8 @@ describe('TicketList', () => { Landed: { val: '01-01-2024', type: 'date' }, }; cy.fillInForm(data); - cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.dataCy('Agency_select').click(); + cy.dataCy('FormModelPopup_save').click(); cy.checkNotification('Data created'); cy.url().should('match', /\/ticket\/\d+\/summary/); }); diff --git a/test/cypress/integration/worker/workerPda.spec.js b/test/cypress/integration/worker/workerPda.spec.js index fe8efa834..dc1ca6224 100644 --- a/test/cypress/integration/worker/workerPda.spec.js +++ b/test/cypress/integration/worker/workerPda.spec.js @@ -1,6 +1,5 @@ describe('WorkerPda', () => { - const deviceProductionField = - '.vn-row > .q-field > .q-field__inner > .q-field__control > .q-field__control-container'; + const select = '[data-cy="pda-dialog-select"]'; beforeEach(() => { cy.viewport(1920, 1080); cy.login('developer'); @@ -9,7 +8,8 @@ describe('WorkerPda', () => { it('assign pda', () => { cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); - cy.get(deviceProductionField).type('{downArrow}{enter}'); + cy.get(select).click(); + cy.get(select).type('{downArrow}{enter}'); cy.get('.q-notification__message').should('have.text', 'Data created'); }); diff --git a/test/cypress/integration/zone/zoneBasicData.spec.js b/test/cypress/integration/zone/zoneBasicData.spec.js index c6151a49b..6229039b7 100644 --- a/test/cypress/integration/zone/zoneBasicData.spec.js +++ b/test/cypress/integration/zone/zoneBasicData.spec.js @@ -1,5 +1,6 @@ describe('ZoneBasicData', () => { const notification = '.q-notification__message'; + const priceBasicData = '[data-cy="Price_input"]'; beforeEach(() => { cy.viewport(1280, 720); @@ -8,14 +9,20 @@ describe('ZoneBasicData', () => { }); it('should throw an error if the name is empty', () => { - cy.get('.q-card > :nth-child(1)').clear(); + cy.get('[data-cy="zone-basic-data-name"] input').type('{selectall}{backspace}'); cy.get('.q-btn-group > .q-btn--standard').click(); cy.get(notification).should('contains.text', "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(notification).should('contains.text', '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(notification).should('contains.text', 'Data saved'); + cy.checkNotification('Data saved'); }); }); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 21121d9df..2b13a7144 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -110,14 +110,14 @@ Cypress.Commands.add('fillInForm', (obj, form = '.q-form > .q-card') => { const { type, val } = field; switch (type) { case 'select': - cy.wrap(el).type(val); + cy.get(el).click(); cy.get('.q-menu .q-item').contains(val).click(); break; case 'date': - cy.wrap(el).type(val.split('-').join('')); + cy.get(el).type(val.split('-').join('')); break; case 'time': - cy.wrap(el).click(); + cy.get(el).click(); cy.get('.q-time .q-time__clock').contains(val.h).click(); cy.get('.q-time .q-time__clock').contains(val.m).click(); cy.get('.q-time .q-time__link').contains(val.x).click(); diff --git a/test/vitest/helper.js b/test/vitest/helper.js index 4bfae5dc8..ce057c7c3 100644 --- a/test/vitest/helper.js +++ b/test/vitest/helper.js @@ -44,7 +44,18 @@ vi.mock('vue-router', () => ({ vi.mock('axios'); vi.spyOn(useValidator, 'useValidator').mockImplementation(() => { - return { validate: vi.fn() }; + return { + validate: vi.fn(), + validations: () => ({ + format: vi.fn(), + presence: vi.fn(), + required: vi.fn(), + length: vi.fn(), + numericality: vi.fn(), + min: vi.fn(), + custom: vi.fn(), + }), + }; }); class FormDataMock {