diff --git a/src/components/LeftMenu.vue b/src/components/LeftMenu.vue index 6e0ae5907..b02c0d014 100644 --- a/src/components/LeftMenu.vue +++ b/src/components/LeftMenu.vue @@ -34,18 +34,26 @@ const search = ref(null); const filteredItems = computed(() => { if (!search.value) return items.value; + const normalizedSearch = normalize(search.value); return items.value.filter((item) => { - const locale = t(item.title).toLowerCase(); - return locale.includes(search.value.toLowerCase()); + const locale = normalize(t(item.title)); + return locale.includes(normalizedSearch); }); }); const filteredPinnedModules = computed(() => { if (!search.value) return pinnedModules.value; + const normalizedSearch = search.value + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase(); const map = new Map(); for (const [key, pinnedModule] of pinnedModules.value) { - const locale = t(pinnedModule.title).toLowerCase(); - if (locale.includes(search.value.toLowerCase())) map.set(key, pinnedModule); + const locale = t(pinnedModule.title) + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase(); + if (locale.includes(normalizedSearch)) map.set(key, pinnedModule); } return map; }); @@ -147,6 +155,13 @@ async function togglePinned(item, event) { const handleItemExpansion = (itemName) => { expansionItemElements[itemName].scrollToLastElement(); }; + +function normalize(text) { + return text + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase(); +} </script> <template> diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 4622f1cb0..227ff9465 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -1,7 +1,7 @@ <script setup> import { ref, toRefs, computed, watch, onMounted, useAttrs } from 'vue'; import { useI18n } from 'vue-i18n'; -import FetchData from 'src/components/FetchData.vue'; +import { useArrayData } from 'src/composables/useArrayData'; import { useRequired } from 'src/composables/useRequired'; import dataByOrder from 'src/utils/dataByOrder'; @@ -90,6 +90,10 @@ const $props = defineProps({ type: Boolean, default: false, }, + dataKey: { + type: String, + default: null, + }, }); const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])]; @@ -98,14 +102,14 @@ const { optionLabel, optionValue, optionFilter, optionFilterValue, options, mode const myOptions = ref([]); const myOptionsOriginal = ref([]); const vnSelectRef = ref(); -const dataRef = ref(); const lastVal = ref(); const noOneText = t('globals.noOne'); const noOneOpt = ref({ [optionValue.value]: false, [optionLabel.value]: noOneText, }); - +const isLoading = ref(false); +const useURL = computed(() => $props.url); const value = computed({ get() { return $props.modelValue; @@ -129,11 +133,18 @@ watch(modelValue, async (newValue) => { onMounted(() => { setOptions(options.value); - if ($props.url && $props.modelValue && !findKeyInOptions()) + if (useURL.value && $props.modelValue && !findKeyInOptions()) fetchFilter($props.modelValue); if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300); }); +defineExpose({ opts: myOptions }); + +const arrayDataKey = + $props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label); + +const arrayData = useArrayData(arrayDataKey, { url: $props.url, searchUrl: false }); + function findKeyInOptions() { if (!$props.options) return; return filter($props.modelValue, $props.options)?.length; @@ -168,7 +179,7 @@ function filter(val, options) { } async function fetchFilter(val) { - if (!$props.url || !dataRef.value) return; + if (!$props.url) return; const { fields, include, sortBy, limit } = $props; const key = @@ -190,8 +201,8 @@ async function fetchFilter(val) { const fetchOptions = { where, include, limit }; if (fields) fetchOptions.fields = fields; if (sortBy) fetchOptions.order = sortBy; - - return dataRef.value.fetch(fetchOptions); + arrayData.reset(['skip', 'filter.skip', 'page']); + return (await arrayData.applyFilter({ filter: fetchOptions }))?.data; } async function filterHandler(val, update) { @@ -231,20 +242,23 @@ function nullishToTrue(value) { const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val); -defineExpose({ opts: myOptions }); +async function onScroll({ to, direction, from, index }) { + const lastIndex = myOptions.value.length - 1; + + if (from === 0 && index === 0) return; + if (!useURL.value && !$props.fetchRef) return; + if (direction === 'decrease') return; + if (to === lastIndex && arrayData.store.hasMoreData && !isLoading.value) { + isLoading.value = true; + await arrayData.loadMore(); + setOptions(arrayData.store.data); + vnSelectRef.value.scrollTo(lastIndex); + isLoading.value = false; + } +} </script> <template> - <FetchData - ref="dataRef" - :url="$props.url" - @on-fetch="(data) => setOptions(data)" - :where="where || { [optionValue]: value }" - :limit="limit" - :sort-by="sortBy" - :fields="fields" - :params="params" - /> <QSelect v-model="value" :options="myOptions" @@ -263,6 +277,9 @@ defineExpose({ opts: myOptions }); :rules="mixinRules" virtual-scroll-slice-size="options.length" hide-bottom-space + :input-debounce="useURL ? '300' : '0'" + :loading="isLoading" + @virtual-scroll="onScroll" > <template v-if="isClearable" #append> <QIcon diff --git a/src/components/ui/VnLinkPhone.vue b/src/components/ui/VnLinkPhone.vue index 3b63889e1..4c045968f 100644 --- a/src/components/ui/VnLinkPhone.vue +++ b/src/components/ui/VnLinkPhone.vue @@ -17,19 +17,12 @@ const config = reactive({ const type = Object.keys(config).find((key) => key in useAttrs()) || 'sip'; onBeforeMount(async () => { - let url; let { channel } = config[type]; if (type === 'say-simple') { - url = (await axios.get('SaySimpleConfigs/findOne')).data.url; - if (!channel) - channel = ( - await axios.get('SaySimpleCountries/findOne', { - params: { - filter: { fields: ['channel'], where: { countryFk: 0 } }, - }, - }) - ).data?.channel; + const { url, defaultChannel } = (await axios.get('SaySimpleConfigs/findOne')) + .data; + if (!channel) channel = defaultChannel; config[ type diff --git a/src/components/ui/VnNotes.vue b/src/components/ui/VnNotes.vue index b395b3934..dbcb2f3fe 100644 --- a/src/components/ui/VnNotes.vue +++ b/src/components/ui/VnNotes.vue @@ -65,13 +65,9 @@ onBeforeRouteLeave((to, from, next) => { auto-load @on-fetch="(data) => (observationTypes = data)" /> - <QCard class="q-pa-xs q-mb-xl full-width" v-if="$props.addNote"> + <QCard class="q-pa-xs q-mb-lg full-width" v-if="$props.addNote"> <QCardSection horizontal> - <VnAvatar :worker-id="currentUser.id" size="md" /> - <div class="full-width row justify-between q-pa-xs"> - <VnUserLink :name="t('New note')" :worker-id="currentUser.id" /> - {{ t('globals.now') }} - </div> + {{ t('New note') }} </QCardSection> <QCardSection class="q-px-xs q-my-none q-py-none"> <VnRow class="full-width"> @@ -144,7 +140,7 @@ onBeforeRouteLeave((to, from, next) => { <div class="full-width row justify-between q-pa-xs"> <div> <VnUserLink - :name="`${note.worker.user.nickname}`" + :name="`${note.worker.user.name}`" :worker-id="note.worker.id" /> <QBadge diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js index 747c6ab64..9348793d2 100644 --- a/src/composables/useArrayData.js +++ b/src/composables/useArrayData.js @@ -247,6 +247,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) { } function updateStateParams() { + if (!route?.path) return; const newUrl = { path: route.path, query: { ...(route.query ?? {}) } }; if (store?.searchUrl) newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter); diff --git a/src/composables/useOpenURL.js b/src/composables/useOpenURL.js new file mode 100644 index 000000000..008774c20 --- /dev/null +++ b/src/composables/useOpenURL.js @@ -0,0 +1,8 @@ +import { openURL } from 'quasar'; +const defaultWindowFeatures = { + noopener: true, + noreferrer: true, +}; +export default function (url, windowFeatures = defaultWindowFeatures, fn = undefined) { + openURL(url, fn, windowFeatures); +} diff --git a/src/pages/Customer/Card/CustomerDescriptor.vue b/src/pages/Customer/Card/CustomerDescriptor.vue index 974b05181..e46d2cb29 100644 --- a/src/pages/Customer/Card/CustomerDescriptor.vue +++ b/src/pages/Customer/Card/CustomerDescriptor.vue @@ -174,23 +174,6 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit > <QTooltip>{{ t('Customer ticket list') }}</QTooltip> </QBtn> - <QBtn - :to="{ - name: 'TicketList', - query: { - table: JSON.stringify({ - clientFk: entity.id, - }), - createForm: JSON.stringify({ clientId: entity.id }), - }, - }" - size="md" - color="primary" - target="_blank" - icon="vn:ticketAdd" - > - <QTooltip>{{ t('New ticket') }}</QTooltip> - </QBtn> <QBtn :to="{ name: 'InvoiceOutList', @@ -202,23 +185,6 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit > <QTooltip>{{ t('Customer invoice out list') }}</QTooltip> </QBtn> - <QBtn - :to="{ - name: 'OrderList', - query: { - table: JSON.stringify({ - clientFk: entity.id, - }), - createForm: JSON.stringify({ clientFk: entity.id }), - }, - }" - size="md" - target="_blank" - icon="vn:basketadd" - color="primary" - > - <QTooltip>{{ t('New order') }}</QTooltip> - </QBtn> <QBtn :to="{ name: 'AccountSummary', diff --git a/src/pages/Customer/Card/CustomerDescriptorMenu.vue b/src/pages/Customer/Card/CustomerDescriptorMenu.vue index 7af415820..aeaeaef57 100644 --- a/src/pages/Customer/Card/CustomerDescriptorMenu.vue +++ b/src/pages/Customer/Card/CustomerDescriptorMenu.vue @@ -6,8 +6,8 @@ import axios from 'axios'; import { useQuasar } from 'quasar'; import useNotify from 'src/composables/useNotify'; - import VnSmsDialog from 'src/components/common/VnSmsDialog.vue'; +import useOpenURL from 'src/composables/useOpenURL'; const $props = defineProps({ customer: { @@ -15,7 +15,6 @@ const $props = defineProps({ required: true, }, }); - const { notify } = useNotify(); const { t } = useI18n(); const quasar = useQuasar(); @@ -40,9 +39,42 @@ const sendSms = async (payload) => { notify(error.message, 'positive'); } }; + +const openCreateForm = (type) => { + const query = { + table: { + clientFk: $props.customer.id, + }, + createForm: { + addressId: $props.customer.defaultAddressFk, + }, + }; + const clientFk = { + ticket: 'clientId', + order: 'clientFk', + }; + const key = clientFk[type]; + if (!key) return; + query.createForm[key] = $props.customer.id; + + const params = Object.entries(query) + .map(([key, value]) => `${key}=${JSON.stringify(value)}`) + .join('&'); + useOpenURL(`/#/${type}/list?${params}`); +}; </script> <template> + <QItem v-ripple clickable @click="openCreateForm('ticket')"> + <QItemSection> + {{ t('globals.pageTitles.createTicket') }} + </QItemSection> + </QItem> + <QItem v-ripple clickable @click="openCreateForm('order')"> + <QItemSection> + {{ t('globals.pageTitles.createOrder') }} + </QItemSection> + </QItem> <QItem v-ripple clickable> <QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection> </QItem> diff --git a/src/pages/Customer/Card/CustomerUnpaid.vue b/src/pages/Customer/Card/CustomerUnpaid.vue index d7f933a7f..ef3ff3b94 100644 --- a/src/pages/Customer/Card/CustomerUnpaid.vue +++ b/src/pages/Customer/Card/CustomerUnpaid.vue @@ -2,10 +2,9 @@ import { computed, onBeforeMount, ref, watch, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; - import VnInputDate from 'components/common/VnInputDate.vue'; import VnInput from 'src/components/common/VnInput.vue'; - +import VnRow from 'components/ui/VnRow.vue'; import axios from 'axios'; import useNotify from 'src/composables/useNotify'; import { useStateStore } from 'stores/useStateStore'; diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index ae3a42514..c3f4d52e8 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -53,7 +53,7 @@ defineProps({ <QItemSection> <VnSelect url="Workers/activeWithInheritedRole" - :filter="{ where: { role: 'salesPerson' } }" + :where="{ role: 'salesPerson' }" auto-load :label="t('Salesperson')" v-model="params.salesPersonFk" @@ -67,7 +67,6 @@ defineProps({ dense outlined rounded - :input-debounce="0" /> </QItemSection> </QItem> diff --git a/src/pages/Customer/components/CustomerSummaryTable.vue b/src/pages/Customer/components/CustomerSummaryTable.vue index 1c0dfd2ce..c1ba506fd 100644 --- a/src/pages/Customer/components/CustomerSummaryTable.vue +++ b/src/pages/Customer/components/CustomerSummaryTable.vue @@ -194,14 +194,14 @@ const getItemPackagingType = (ticketSales) => { redirect="ticket" > <template #column-nickname="{ row }"> - <span class="link"> + <span class="link" @click.stop> {{ row.nickname }} <CustomerDescriptorProxy :id="row.clientFk" /> </span> </template> <template #column-routeFk="{ row }"> - <span class="link"> + <span class="link" @click.stop> {{ row.routeFk }} <RouteDescriptorProxy :id="row.routeFk" /> </span> @@ -218,7 +218,7 @@ const getItemPackagingType = (ticketSales) => { <span v-else> {{ toCurrency(row.totalWithVat) }}</span> </template> <template #column-state="{ row }"> - <span v-if="row.invoiceOut"> + <span v-if="row.invoiceOut" @click.stop> <span :class="{ link: row.invoiceOut.ref }"> {{ row.invoiceOut.ref }} <InvoiceOutDescriptorProxy :id="row.invoiceOut.id" /> diff --git a/src/pages/Entry/EntryStockBought.vue b/src/pages/Entry/EntryStockBought.vue index 7ae6901d3..3f0cd2d99 100644 --- a/src/pages/Entry/EntryStockBought.vue +++ b/src/pages/Entry/EntryStockBought.vue @@ -99,7 +99,7 @@ const travelDialogRef = ref(false); const tableRef = ref(); const travel = ref(null); const userParams = ref({ - dated: Date.vnNew(), + dated: Date.vnNew().toJSON(), }); const filter = ref({ @@ -219,6 +219,7 @@ function round(value) { data-key="StockBoughts" url="StockBoughts/getStockBought" save-url="StockBoughts/crud" + search-url="StockBoughts" order="reserve DESC" :right-search="false" :is-editable="true" diff --git a/src/pages/Entry/EntryStockBoughtDetail.vue b/src/pages/Entry/EntryStockBoughtDetail.vue index 0fd775ee6..812171825 100644 --- a/src/pages/Entry/EntryStockBoughtDetail.vue +++ b/src/pages/Entry/EntryStockBoughtDetail.vue @@ -18,7 +18,7 @@ const $props = defineProps({ required: true, }, }); -const customUrl = `StockBoughts/getStockBoughtDetail?workerFk=${$props.workerFk}&date=${$props.dated}`; +const customUrl = `StockBoughts/getStockBoughtDetail?workerFk=${$props.workerFk}&dated=${$props.dated}`; const columns = [ { align: 'left', diff --git a/src/pages/Entry/EntryStockBoughtFilter.vue b/src/pages/Entry/EntryStockBoughtFilter.vue index 7694cfe6c..e59332064 100644 --- a/src/pages/Entry/EntryStockBoughtFilter.vue +++ b/src/pages/Entry/EntryStockBoughtFilter.vue @@ -27,7 +27,7 @@ onMounted(async () => { <VnFilterPanel :data-key="props.dataKey" :search-button="true" - search-url="table" + search-url="StockBoughts" @set-user-params="setUserParams" > <template #tags="{ tag, formatFn }"> @@ -36,12 +36,19 @@ onMounted(async () => { <span>{{ formatFn(tag.value) }}</span> </div> </template> - <template #body="{ params }"> + <template #body="{ params, searchFn }"> <QItem class="q-my-sm"> <QItemSection> <VnInputDate id="date" v-model="params.dated" + @update:model-value=" + (value) => { + params.dated = value; + setUserParams(params); + searchFn(); + } + " :label="t('Date')" is-outlined /> diff --git a/src/pages/Route/Cmr/CmrList.vue b/src/pages/Route/Cmr/CmrList.vue index ede271960..b3eaf3b48 100644 --- a/src/pages/Route/Cmr/CmrList.vue +++ b/src/pages/Route/Cmr/CmrList.vue @@ -116,7 +116,7 @@ function getApiUrl() { return new URL(window.location).origin; } function getCmrUrl(value) { - return `${getApiUrl()}/api/Routes/${value}/cmr?access_token=${token}`; + return `${getApiUrl()}/api/Cmrs/${value}/print?access_token=${token}`; } function downloadPdfs() { if (!selectedRows.value.length) { @@ -129,7 +129,7 @@ function downloadPdfs() { let cmrs = []; for (let value of selectedRows.value) cmrs.push(value.cmrFk); // prettier-ignore - return window.open(`${getApiUrl()}/api/Routes/downloadCmrsZip?ids=${cmrs.join(',')}&access_token=${token}`); + return window.open(`${getApiUrl()}/api/Cmrs/downloadZip?ids=${cmrs.join(',')}&access_token=${token}`); } </script> <template> @@ -149,7 +149,7 @@ function downloadPdfs() { <VnTable ref="tableRef" data-key="CmrList" - url="Routes/cmrs" + url="Cmrs/filter" :columns="columns" :right-search="true" default-mode="table" diff --git a/src/pages/Ticket/Card/TicketPurchaseRequest.vue b/src/pages/Ticket/Card/TicketPurchaseRequest.vue index 7715e9e21..fdc35d369 100644 --- a/src/pages/Ticket/Card/TicketPurchaseRequest.vue +++ b/src/pages/Ticket/Card/TicketPurchaseRequest.vue @@ -268,6 +268,7 @@ onMounted(() => (stateStore.rightDrawer = false)); :label="t('basicData.price')" type="number" min="0" + step="any" /> </template> </VnTable> diff --git a/src/pages/Ticket/TicketList.vue b/src/pages/Ticket/TicketList.vue index a3876a1a6..0685217ac 100644 --- a/src/pages/Ticket/TicketList.vue +++ b/src/pages/Ticket/TicketList.vue @@ -45,6 +45,13 @@ const userParams = { from: null, to: null, }; + +onMounted(() => { + initializeFromQuery(); + stateStore.rightDrawer = true; + if (!route.query.createForm) return; + onClientSelected(JSON.parse(route.query.createForm)); +}); // Método para inicializar las variables desde la query string const initializeFromQuery = () => { const query = route.query.table ? JSON.parse(route.query.table) : {}; @@ -301,11 +308,6 @@ const getDateColor = (date) => { if (comparation < 0) return 'bg-success'; }; -onMounted(() => { - initializeFromQuery(); - stateStore.rightDrawer = true; -}); - async function makeInvoice(ticket) { const ticketsIds = ticket.map((item) => item.id); const { data } = await axios.post(`Tickets/invoiceTicketsAndPdf`, { ticketsIds }); diff --git a/src/pages/Zone/ZoneDeliveryPanel.vue b/src/pages/Zone/ZoneDeliveryPanel.vue index bb92ccc6a..255c891a1 100644 --- a/src/pages/Zone/ZoneDeliveryPanel.vue +++ b/src/pages/Zone/ZoneDeliveryPanel.vue @@ -105,11 +105,14 @@ watch( <template #option="{ itemProps, opt }"> <QItem v-bind="itemProps"> <QItemSection v-if="opt.code"> - <QItemLabel>{{ opt.code }}</QItemLabel> - <QItemLabel caption - >{{ opt.town?.province?.name }}, - {{ opt.town?.province?.country?.name }}</QItemLabel - > + <QItemLabel> + {{ `${opt.code}, ${opt.town?.name}` }} + </QItemLabel> + <QItemLabel caption> + {{ + `${opt.town?.province?.name}, ${opt.town?.province?.country?.name}` + }} + </QItemLabel> </QItemSection> </QItem> </template> diff --git a/src/pages/Zone/ZoneFilterPanel.vue b/src/pages/Zone/ZoneFilterPanel.vue index efe710360..25d6c340f 100644 --- a/src/pages/Zone/ZoneFilterPanel.vue +++ b/src/pages/Zone/ZoneFilterPanel.vue @@ -22,7 +22,12 @@ const agencies = ref([]); </script> <template> - <FetchData url="agencies" @on-fetch="(data) => (agencies = data)" auto-load /> + <FetchData + url="AgencyModes" + :filter="{ fields: ['id', 'name'] }" + @on-fetch="(data) => (agencies = data)" + auto-load + /> <VnFilterPanel :data-key="props.dataKey" :search-button="true" diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue index 0e9bed29d..698e5ed6d 100644 --- a/src/pages/Zone/ZoneList.vue +++ b/src/pages/Zone/ZoneList.vue @@ -73,6 +73,7 @@ const columns = computed(() => [ inWhere: true, attrs: { url: 'AgencyModes', + fields: ['id', 'name'], }, }, columnField: {