diff --git a/CHANGELOG.md b/CHANGELOG.md index 86ffce3fd..8e1d4c433 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,141 @@ +# Version 24.36 - 2024-08-27 + +### Added 🆕 + +- feat(FormModel): trim data by default by:alexm +- feat(orderBasicData): add notes by:alexm +- feat(orderList): correct create order by:alexm +- feat(orderList): use orderFilter and fixed this by:alexm +- feat: #7323 handle workerPhoto (origin/7323_workerPhoto, 7323_workerPhoto) by:Javier Segarra +- feat: add recover password and reset password by:alexm +- feat: refs #7346 add seriaType option by:jgallego +- feat: refs #7346 elimino === by:jgallego +- feat: refs #7346 formdata uses serialType by:jgallego +- feat: refs #7346 refactor by:jgallego +- feat: refs #7346 sonarLint warnings (origin/7346-invoiceOutMultilple, 7346-invoiceOutMultilple) by:jgallego +- feat: refs #7710 uses cloneAll by:jgallego +- fix: refs #7717 fix OrderList table filters' and summary table style by:Jon + +### Changed 📦 + +- feat: refs #7346 refactor by:jgallego +- perf: date fields (mindshore/feature/TicketFutureFilter, feature/TicketFutureFilter) by:Javier Segarra +- perf: refs #7717 right menu filter by:Jon +- perf: use ref at component start by:Javier Segarra +- refactor: refs #7717 delete useless function and import by:Jon +- refactor: refs #7717 deleted useless code by:Jon + +### Fixed 🛠️ + +- feat(orderList): use orderFilter and fixed this by:alexm +- fix(VnTable): orderBy v-model by:alexm +- fix(account_card): redirection by:carlossa +- fix(orderLines): reload when delete and redirect when confirm by:alexm +- fix: #6336 ClaimListStates by:Javier Segarra +- fix: account subsections cards by:carlossa +- fix: duplicate key by:Jon +- fix: order description to vnTable by:alexm +- fix: orderCatalogFilter order by:alexm +- fix: quasar build warnings (6336_claim_fix_states) by:Javier Segarra +- fix: refs #7717 fix OrderList table filters' and summary table style by:Jon +- fix: refs #7717 fix basic data form & minor errors by:Jon +- fix: refs #7717 fix catalog filter, searchbar redirect and search by:Jon +- fix: refs #7717 fix catalog searchbar and worker tests(refs #7323) by:Jon +- fix: refs #7717 fix order sections by:Jon +- fix: refs #7717 fix volume and lines redirect by:Jon +- fix: refs #7717 fixed searchbar filter with rightmenu filters' applied by:Jon +- fix: test by:alexm +- fix: ticketDescriptorMenu by:Javier Segarra +- refs #7355 account fixes by:carlossa + +# Version 24.34 - 2024-08-20 + +### Added 🆕 + +- chore: #6900 order params by:jorgep +- chore: refs #6900 drop console log by:jorgep +- chore: refs #6900 drop vnCurrency by:jorgep +- chore: refs #6900 fix e2e tests by:jorgep +- chore: refs #6900 mv rectificative logic by:jorgep +- chore: refs #6900 responsive code by:jorgep +- chore: refs #7283 drop array types by:jorgep +- chore: refs #7283 drop import by:jorgep +- chore: refs #7283 fix e2e logout by:jorgep +- chore: refs #7283 update VnAvatar title handling by:jorgep +- chore: refs #7323 fix test by:jorgep +- chore: refs #7323 remove unused import by:jorgep +- chore: refs #7323drop commented code by:jorgep +- feat(VnCard): use props searchbar by:alexm +- feat(customer): improve basicData to balance by:alexm +- feat(customer_balance): refs #6943 add functionality from salix by:alexm +- feat(customer_balance): refs #6943 translations by:alexm +- feat: refs #6130 husky commitLint config by:pablone +- feat: refs #6130 husky hooks by:pablone +- feat: refs #6900 add InvoiceInSerial by:jorgep +- feat: refs #6900 add locale by:jorgep +- feat: refs #6900 use VnTable & sort filter fields by:jorgep +- feat: refs #7323 add flex-wrap by:jorgep +- feat: refs #7323 add my account" btn & fix models log selectable by:jorgep +- feat: refs #7323 improve test by:jorgep + +### Changed 📦 + +- refactor(customer_log: use VnLog by:alexm +- refactor(customer_recovery): to vnTable by:alexm +- refactor(customer_webAccess): FormModel by:alexm +- refactor: refs #7283 update avatar size and color by:jorgep + +### Fixed 🛠️ + +- chore: refs #6900 fix e2e tests by:jorgep +- chore: refs #7283 fix e2e logout by:jorgep +- chore: refs #7323 fix test by:jorgep +- feat: refs #7323 add my account" btn & fix models log selectable by:jorgep +- fix #7355 fix acls list by:carlossa +- fix(VnFilterPanel): emit userParams better by:alexm +- fix(claim_summary): url links (HEAD -> 7864_testToMaster_2434, origin/test, origin/7864_testToMaster_2434, test) by:alexm +- fix(customer_sms: fix reload by:alexm +- fix(twoFactor): unify code login and twoFactor by:alexm +- fix: VnCard VnSearchbar props by:alexm +- fix: accountMailAlias by:alexm +- fix: refs #6130 add commit lint modules by:pablone +- fix: refs #6130 pnpm-lock.yml by:pablone +- fix: refs #6900 improve loading by:jorgep +- fix: refs #6900 improve logic (origin/6900-addSerial) by:jorgep +- fix: refs #6900 improve logic by:jorgep +- fix: refs #6900 rectificative btn reactivity by:jorgep +- fix: refs #6900 use type number by:jorgep +- fix: refs #6900 vat & dueday by:jorgep +- fix: refs #6900 vat, dueday & intrastat by:jorgep +- fix: refs #6989 show entity name & default time from config table by:jorgep +- fix: refs #7283 basicData locale by:jorgep +- fix: refs #7283 itemLastEntries filter by:jorgep +- fix: refs #7283 itemTags & VnImg by:jorgep +- fix: refs #7283 locale by:jorgep +- fix: refs #7283 min-width vnImg by:jorgep +- fix: refs #7283 use vnAvatar & add optional zoom by:jorgep +- fix: refs #7283 userPanel pic by:jorgep +- fix: refs #7323 add department popup by:jorgep +- fix: refs #7323 add locale by:jorgep +- fix: refs #7323 css righ menu by:jorgep +- fix: refs #7323 data-key & add select by:jorgep +- fix: refs #7323 load all opts by:jorgep +- fix: refs #7323 righ menu bug by:jorgep +- fix: refs #7323 use global locale by:jorgep +- fix: refs #7323 use workerFilter (origin/7323-warmfix-fixErrors) by:jorgep +- fix: refs #7323 vnsubtoolbar css by:jorgep +- fix: refs #7323 wrong css by:jorgep +- refs #7355 fix Rol, alias by:carlossa +- refs #7355 fix accountAlias by:carlossa +- refs #7355 fix alias summary by:carlossa +- refs #7355 fix conflicts by:carlossa +- refs #7355 fix create Rol by:carlossa +- refs #7355 fix list by:carlossa +- refs #7355 fix lists redirects summary by:carlossa +- refs #7355 fix roles by:carlossa +- refs #7355 fix search exprBuilder by:carlossa +- refs #7355 fix vnTable by:carlossa + # Version 24.32 - 2024-08-06 ### Added 🆕 diff --git a/package.json b/package.json index 72bada823..eaffd8d85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "24.34.0", + "version": "24.40.0", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", @@ -62,4 +62,4 @@ "vite": "^5.1.4", "vitest": "^0.31.1" } -} +} \ No newline at end of file diff --git a/src/boot/keyShortcut.js b/src/boot/keyShortcut.js new file mode 100644 index 000000000..5afb5b74a --- /dev/null +++ b/src/boot/keyShortcut.js @@ -0,0 +1,34 @@ +export default { + mounted: function (el, binding) { + const shortcut = binding.value ?? '+'; + + const { key, ctrl, alt, callback } = + typeof shortcut === 'string' + ? { + key: shortcut, + ctrl: true, + alt: true, + callback: () => + document + .querySelector(`button[shortcut="${shortcut}"]`) + ?.click(), + } + : binding.value; + + const handleKeydown = (event) => { + if (event.key === key && (!ctrl || event.ctrlKey) && (!alt || event.altKey)) { + callback(); + } + }; + + // Attach the event listener to the window + window.addEventListener('keydown', handleKeydown); + + el._handleKeydown = handleKeydown; + }, + unmounted: function (el) { + if (el._handleKeydown) { + window.removeEventListener('keydown', el._handleKeydown); + } + }, +}; diff --git a/src/boot/mainShortcutMixin.js b/src/boot/mainShortcutMixin.js new file mode 100644 index 000000000..8e5f147db --- /dev/null +++ b/src/boot/mainShortcutMixin.js @@ -0,0 +1,38 @@ +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; + + // Resetea la bandera cuando se sueltan las teclas ctrl o alt + if (!ctrlKey || !altKey) { + isNotified = false; + } + }; + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + }, +}; diff --git a/src/boot/quasar.js b/src/boot/quasar.js index a8d9b7ad9..e2035c880 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -1,6 +1,10 @@ import { boot } from 'quasar/wrappers'; import qFormMixin from './qformMixin'; +import mainShortcutMixin from './mainShortcutMixin'; +import keyShortcut from './keyShortcut'; export default boot(({ app }) => { app.mixin(qFormMixin); + app.mixin(mainShortcutMixin); + app.directive('shortcut', keyShortcut); }); diff --git a/src/components/CreateBankEntityForm.vue b/src/components/CreateBankEntityForm.vue index c46ac7752..1117225c7 100644 --- a/src/components/CreateBankEntityForm.vue +++ b/src/components/CreateBankEntityForm.vue @@ -1,5 +1,5 @@ <script setup> -import { reactive, ref, onMounted, nextTick } from 'vue'; +import { reactive, ref, onMounted, nextTick, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import VnInput from 'src/components/common/VnInput.vue'; @@ -7,16 +7,21 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import FetchData from 'components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import FormModelPopup from './FormModelPopup.vue'; +import { useState } from 'src/composables/useState'; defineProps({ showEntityField: { type: Boolean, default: true } }); const emit = defineEmits(['onDataSaved']); const { t } = useI18n(); const bicInputRef = ref(null); +const state = useState(); + +const customer = computed(() => state.get('customer')); + const bankEntityFormData = reactive({ name: null, bic: null, - countryFk: null, + countryFk: customer.value.countryFk, id: null, }); diff --git a/src/components/CreateNewExpenseForm.vue b/src/components/CreateNewExpenseForm.vue new file mode 100644 index 000000000..7088cec03 --- /dev/null +++ b/src/components/CreateNewExpenseForm.vue @@ -0,0 +1,50 @@ +<script setup> +import { useI18n } from 'vue-i18n'; +import VnRow from 'components/ui/VnRow.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import FormModelPopup from './FormModelPopup.vue'; + +const emit = defineEmits(['onDataSaved']); +const { t } = useI18n(); +</script> +<template> + <FormModelPopup + url-create="Expenses" + model="Expense" + :title="t('New expense')" + :form-initial-data="{ id: null, isWithheld: false, name: null }" + @on-data-saved="emit('onDataSaved', $event)" + > + <template #form-inputs="{ data, validate }"> + <VnRow> + <VnInput + :label="`${t('globals.code')}`" + v-model="data.id" + :required="true" + :rules="validate('expense.code')" + /> + <QCheckbox + dense + size="sm" + :label="`${t('It\'s a withholding')}`" + v-model="data.isWithheld" + :rules="validate('expense.isWithheld')" + /> + </VnRow> + <VnRow> + <VnInput + :label="`${t('globals.description')}`" + v-model="data.name" + :required="true" + :rules="validate('expense.description')" + /> + </VnRow> + </template> + </FormModelPopup> +</template> + +<i18n> +es: + New expense: Nuevo gasto + It's a withholding: Es una retención +</i18n> diff --git a/src/components/CreateNewPostcodeForm.vue b/src/components/CreateNewPostcodeForm.vue index a426ac2b4..4c44d29e2 100644 --- a/src/components/CreateNewPostcodeForm.vue +++ b/src/components/CreateNewPostcodeForm.vue @@ -105,7 +105,7 @@ async function setProvince(id, data) { option-label="name" option-value="id" :rules="validate('postcode.city')" - :roles-allowed-to-create="['deliveryAssistant']" + :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" :emit-value="false" clearable > diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index 33c831e3f..0386e037b 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -189,11 +189,11 @@ async function saveChanges(data) { }); } -async function insert() { +async function insert(pushData = $props.dataRequired) { const $index = formData.value.length ? formData.value[formData.value.length - 1].$index + 1 : 0; - formData.value.push(Object.assign({ $index }, $props.dataRequired)); + formData.value.push(Object.assign({ $index }, pushData)); hasChanges.value = true; } diff --git a/src/components/FetchData.vue b/src/components/FetchData.vue index 2a0864d3e..3038aa88e 100644 --- a/src/components/FetchData.vue +++ b/src/components/FetchData.vue @@ -44,7 +44,7 @@ onMounted(async () => { async function fetch(fetchFilter = {}) { try { - const filter = Object.assign(fetchFilter, $props.filter); // eslint-disable-line vue/no-dupe-keys + const filter = { ...fetchFilter, ...$props.filter }; // eslint-disable-line vue/no-dupe-keys if ($props.where && !fetchFilter.where) filter.where = $props.where; if ($props.sortBy) filter.order = $props.sortBy; if ($props.limit) filter.limit = $props.limit; diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index a0f6bf479..05f947cf3 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -22,7 +22,7 @@ const { t } = useI18n(); const { validate } = useValidator(); const { notify } = useNotify(); const route = useRoute(); - +const myForm = ref(null); const $props = defineProps({ url: { type: String, @@ -87,6 +87,10 @@ const $props = defineProps({ type: Boolean, default: false, }, + defaultTrim: { + type: Boolean, + default: true, + }, }); const emit = defineEmits(['onFetch', 'onDataSaved']); const modelValue = computed( @@ -105,11 +109,14 @@ const defaultButtons = computed(() => ({ color: 'primary', icon: 'save', label: 'globals.save', + click: () => myForm.value.submit(), + type: 'submit', }, reset: { color: 'primary', icon: 'restart_alt', label: 'globals.reset', + click: () => reset(), }, ...$props.defaultButtons, })); @@ -195,6 +202,7 @@ async function save() { isLoading.value = true; try { + formData.value = trimData(formData.value); const body = $props.mapper ? $props.mapper(formData.value) : formData.value; const method = $props.urlCreate ? 'post' : 'patch'; const url = @@ -253,6 +261,14 @@ function updateAndEmit(evt, val, res) { emit(evt, state.get(modelValue), res); } +function trimData(data) { + if (!$props.defaultTrim) return data; + for (const key in data) { + if (typeof data[key] == 'string') data[key] = data[key].trim(); + } + return data; +} + defineExpose({ save, isLoading, @@ -263,7 +279,14 @@ defineExpose({ </script> <template> <div class="column items-center full-width"> - <QForm @submit="save" @reset="reset" class="q-pa-md" id="formModel"> + <QForm + ref="myForm" + v-if="formData" + @submit="save" + @reset="reset" + class="q-pa-md" + id="formModel" + > <QCard> <slot v-if="formData" @@ -291,7 +314,7 @@ defineExpose({ :color="defaultButtons.reset.color" :icon="defaultButtons.reset.icon" flat - @click="reset" + @click="defaultButtons.reset.click" :disable="!hasChanges" :title="t(defaultButtons.reset.label)" /> @@ -331,7 +354,7 @@ defineExpose({ :label="tMobile('globals.save')" color="primary" icon="save" - @click="save" + @click="defaultButtons.save.click" :disable="!hasChanges" :title="t(defaultButtons.save.label)" /> diff --git a/src/components/ItemsFilterPanel.vue b/src/components/ItemsFilterPanel.vue index 743c2c9d1..8449f9354 100644 --- a/src/components/ItemsFilterPanel.vue +++ b/src/components/ItemsFilterPanel.vue @@ -159,8 +159,8 @@ const removeTag = (index, params, search) => { /> <VnFilterPanel :data-key="props.dataKey" - :expr-builder="exprBuilder" - :custom-tags="customTags" + :expr-builder="props.exprBuilder" + :custom-tags="props.customTags" > <template #tags="{ tag, formatFn }"> <strong v-if="tag.label === 'categoryFk'"> diff --git a/src/components/LeftMenu.vue b/src/components/LeftMenu.vue index 213c08d7e..03fe11a85 100644 --- a/src/components/LeftMenu.vue +++ b/src/components/LeftMenu.vue @@ -1,6 +1,6 @@ <script setup> import axios from 'axios'; -import { onMounted, ref, reactive } from 'vue'; +import { onMounted, watch, ref, reactive } from 'vue'; import { useI18n } from 'vue-i18n'; import { QSeparator, useQuasar } from 'quasar'; import { useRoute } from 'vue-router'; @@ -29,6 +29,15 @@ onMounted(async () => { getRoutes(); }); +watch( + () => route.matched, + () => { + items.value = []; + getRoutes(); + }, + { deep: true } +); + function findMatches(search, item) { const matches = []; function findRoute(search, item) { diff --git a/src/components/LeftMenuItem.vue b/src/components/LeftMenuItem.vue index f3f2315a3..ab74c1de5 100644 --- a/src/components/LeftMenuItem.vue +++ b/src/components/LeftMenuItem.vue @@ -33,7 +33,12 @@ const itemComputed = computed(() => { <QItemSection avatar v-if="!itemComputed.icon"> <QIcon name="disabled_by_default" /> </QItemSection> - <QItemSection>{{ t(itemComputed.title) }}</QItemSection> + <QItemSection> + {{ t(itemComputed.title) }} + <QTooltip v-if="item.keyBinding"> + {{ 'Ctrl + Alt + ' + item?.keyBinding?.toUpperCase() }} + </QTooltip> + </QItemSection> <QItemSection side> <slot name="side" :item="itemComputed" /> </QItemSection> diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index e80a293c6..00faaebc2 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -24,7 +24,13 @@ const pinnedModulesRef = ref(); <template> <QHeader color="white" elevated> <QToolbar class="q-py-sm q-px-md"> - <QBtn @click="stateStore.toggleLeftDrawer()" icon="menu" round dense flat> + <QBtn + @click="stateStore.toggleLeftDrawer()" + icon="dock_to_right" + round + dense + flat + > <QTooltip bottom anchor="bottom right"> {{ t('globals.collapseMenu') }} </QTooltip> diff --git a/src/components/RefundInvoiceForm.vue b/src/components/RefundInvoiceForm.vue new file mode 100644 index 000000000..c21c892dd --- /dev/null +++ b/src/components/RefundInvoiceForm.vue @@ -0,0 +1,174 @@ +<script setup> +import { ref, reactive } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRouter } from 'vue-router'; +import { useDialogPluginComponent } from 'quasar'; +import VnRow from 'components/ui/VnRow.vue'; +import FetchData from 'components/FetchData.vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import FormPopup from './FormPopup.vue'; +import axios from 'axios'; +import useNotify from 'src/composables/useNotify.js'; + +const $props = defineProps({ + invoiceOutData: { + type: Object, + default: () => {}, + }, +}); + +const { dialogRef } = useDialogPluginComponent(); +const { t } = useI18n(); +const router = useRouter(); +const { notify } = useNotify(); + +const rectificativeTypeOptions = ref([]); +const siiTypeInvoiceOutsOptions = ref([]); +const inheritWarehouse = ref(true); +const invoiceParams = reactive({ + id: $props.invoiceOutData?.id, +}); +const invoiceCorrectionTypesOptions = ref([]); + +const refund = async () => { + const params = { + id: invoiceParams.id, + withWarehouse: invoiceParams.inheritWarehouse, + cplusRectificationTypeFk: invoiceParams.cplusRectificationTypeFk, + siiTypeInvoiceOutFk: invoiceParams.siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk: invoiceParams.invoiceCorrectionTypeFk, + }; + + try { + const { data } = await axios.post('InvoiceOuts/refundAndInvoice', params); + notify(t('Refunded invoice'), 'positive'); + const [id] = data?.refundId || []; + if (id) router.push({ name: 'InvoiceOutSummary', params: { id } }); + } catch (err) { + console.error('Error refunding invoice', err); + } +}; +</script> + +<template> + <FetchData + url="CplusRectificationTypes" + :filter="{ order: 'description' }" + @on-fetch=" + (data) => ( + (rectificativeTypeOptions = data), + (invoiceParams.cplusRectificationTypeFk = data.filter( + (type) => type.description == 'I – Por diferencias' + )[0].id) + ) + " + auto-load + /> + <FetchData + url="SiiTypeInvoiceOuts" + :filter="{ where: { code: { like: 'R%' } } }" + @on-fetch=" + (data) => ( + (siiTypeInvoiceOutsOptions = data), + (invoiceParams.siiTypeInvoiceOutFk = data.filter( + (type) => type.code == 'R4' + )[0].id) + ) + " + auto-load + /> + <FetchData + url="InvoiceCorrectionTypes" + @on-fetch="(data) => (invoiceCorrectionTypesOptions = data)" + auto-load + /> + + <QDialog ref="dialogRef"> + <FormPopup + @on-submit="refund()" + :custom-submit-button-label="t('Accept')" + :default-cancel-button="false" + > + <template #form-inputs> + <VnRow> + <VnSelect + :label="t('Rectificative type')" + :options="rectificativeTypeOptions" + hide-selected + option-label="description" + option-value="id" + v-model="invoiceParams.cplusRectificationTypeFk" + :required="true" + /> + </VnRow> + <VnRow> + <VnSelect + :label="t('Class')" + :options="siiTypeInvoiceOutsOptions" + hide-selected + option-label="description" + option-value="id" + v-model="invoiceParams.siiTypeInvoiceOutFk" + :required="true" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.code }} - + {{ scope.opt?.description }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </VnRow> + + <VnRow> + <VnSelect + :label="t('Type')" + :options="invoiceCorrectionTypesOptions" + hide-selected + option-label="description" + option-value="id" + v-model="invoiceParams.invoiceCorrectionTypeFk" + :required="true" + /> </VnRow + ><VnRow> + <div> + <QCheckbox + :label="t('Inherit warehouse')" + v-model="inheritWarehouse" + /> + <QIcon name="info" class="cursor-info q-ml-sm" size="sm"> + <QTooltip>{{ t('Inherit warehouse tooltip') }}</QTooltip> + </QIcon> + </div> + </VnRow> + </template> + </FormPopup> + </QDialog> +</template> + +<i18n> +en: + Refund invoice: Refund invoice + Rectificative type: Rectificative type + Class: Class + Type: Type + Refunded invoice: Refunded invoice + Inherit warehouse: Inherit the warehouse + Inherit warehouse tooltip: Select this option to inherit the warehouse when refunding the invoice + Accept: Accept + Error refunding invoice: Error refunding invoice +es: + Refund invoice: Abonar factura + Rectificative type: Tipo rectificativa + Class: Clase + Type: Tipo + Refunded invoice: Factura abonada + Inherit warehouse: Heredar el almacén + Inherit warehouse tooltip: Seleccione esta opción para heredar el almacén al abonar la factura. + Accept: Aceptar + Error refunding invoice: Error abonando factura +</i18n> diff --git a/src/components/TransferInvoiceForm.vue b/src/components/TransferInvoiceForm.vue index 17c11d87e..f7050cdba 100644 --- a/src/components/TransferInvoiceForm.vue +++ b/src/components/TransferInvoiceForm.vue @@ -2,13 +2,12 @@ import { ref, reactive } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; -import { useQuasar } from 'quasar'; +import { useQuasar, useDialogPluginComponent } from 'quasar'; import VnConfirm from 'components/ui/VnConfirm.vue'; import VnRow from 'components/ui/VnRow.vue'; import FetchData from 'components/FetchData.vue'; import VnSelect from 'components/common/VnSelect.vue'; import FormPopup from './FormPopup.vue'; -import { useDialogPluginComponent } from 'quasar'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; @@ -18,19 +17,19 @@ const $props = defineProps({ default: () => {}, }, }); + const { dialogRef } = useDialogPluginComponent(); const quasar = useQuasar(); const { t } = useI18n(); const router = useRouter(); const { notify } = useNotify(); -const checked = ref(true); -const transferInvoiceParams = reactive({ - id: $props.invoiceOutData?.id, - refFk: $props.invoiceOutData?.ref, -}); const rectificativeTypeOptions = ref([]); const siiTypeInvoiceOutsOptions = ref([]); +const checked = ref(true); +const transferInvoiceParams = reactive({ + id: $props.invoiceOutData?.id, +}); const invoiceCorrectionTypesOptions = ref([]); const selectedClient = (client) => { @@ -44,10 +43,9 @@ const makeInvoice = async () => { const params = { id: transferInvoiceParams.id, cplusRectificationTypeFk: transferInvoiceParams.cplusRectificationTypeFk, + siiTypeInvoiceOutFk: transferInvoiceParams.siiTypeInvoiceOutFk, invoiceCorrectionTypeFk: transferInvoiceParams.invoiceCorrectionTypeFk, newClientFk: transferInvoiceParams.newClientFk, - refFk: transferInvoiceParams.refFk, - siiTypeInvoiceOutFk: transferInvoiceParams.siiTypeInvoiceOutFk, makeInvoice: checked.value, }; @@ -74,7 +72,7 @@ const makeInvoice = async () => { } } - const { data } = await axios.post('InvoiceOuts/transferInvoice', params); + const { data } = await axios.post('InvoiceOuts/transfer', params); notify(t('Transferred invoice'), 'positive'); const id = data?.[0]; if (id) router.push({ name: 'InvoiceOutSummary', params: { id } }); diff --git a/src/components/UserPanel.vue b/src/components/UserPanel.vue index 2f191691a..98334460a 100644 --- a/src/components/UserPanel.vue +++ b/src/components/UserPanel.vue @@ -164,7 +164,7 @@ const isEmployee = computed(() => useRole().isEmployee()); class="q-mt-sm q-px-md" :to="`/worker/${user.id}`" color="primary" - :label="t('My account')" + :label="t('globals.myAccount')" dense /> <div class="text-subtitle1 q-mt-md"> @@ -270,7 +270,3 @@ const isEmployee = computed(() => useRole().isEmployee()); } } </style> -<i18n> -es: - My account: Mi cuenta -</i18n> diff --git a/src/components/VnSelectProvince.vue b/src/components/VnSelectProvince.vue index ea10ace19..2f08db611 100644 --- a/src/components/VnSelectProvince.vue +++ b/src/components/VnSelectProvince.vue @@ -38,7 +38,7 @@ async function onProvinceCreated(_, data) { hide-selected v-model="provinceFk" :rules="validate && validate('postcode.provinceFk')" - :roles-allowed-to-create="['deliveryAssistant']" + :acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]" > <template #option="{ itemProps, opt }"> <QItem v-bind="itemProps"> diff --git a/src/components/VnTable/VnFilter.vue b/src/components/VnTable/VnFilter.vue index c6d4d8ef2..fd3c29fa3 100644 --- a/src/components/VnTable/VnFilter.vue +++ b/src/components/VnTable/VnFilter.vue @@ -34,7 +34,7 @@ const model = defineModel(undefined, { required: true }); const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl }); const columnFilter = computed(() => $props.column?.columnFilter); -const updateEvent = { 'update:modelValue': addFilter, remove: () => addFilter(null) }; +const updateEvent = { 'update:modelValue': addFilter }; const enterEvent = { 'keyup.enter': () => addFilter(model.value), remove: () => addFilter(null), diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index d6b35d4da..d6008de0b 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -49,6 +49,10 @@ const $props = defineProps({ type: Object, default: null, }, + createAsDialog: { + type: Boolean, + default: true, + }, cardClass: { type: String, default: 'flex-one', @@ -65,9 +69,13 @@ const $props = defineProps({ type: Boolean, default: false, }, + disableInfiniteScroll: { + type: Boolean, + default: false, + }, hasSubToolbar: { type: Boolean, - default: true, + default: null, }, disableOption: { type: Object, @@ -85,6 +93,10 @@ const $props = defineProps({ type: Object, default: () => ({}), }, + crudModel: { + type: Object, + default: () => ({}), + }, tableHeight: { type: String, default: '90vh', @@ -107,7 +119,7 @@ const orders = ref(parseOrder(routeQuery.filter?.order)); const CrudModelRef = ref({}); const showForm = ref(false); const splittedColumns = ref({ columns: [] }); -const columnsVisibilitySkiped = ref(); +const columnsVisibilitySkipped = ref(); const createForm = ref(); const tableModes = [ @@ -135,7 +147,7 @@ onMounted(() => { ? CARD_MODE : $props.defaultMode; stateStore.rightDrawer = true; - columnsVisibilitySkiped.value = [ + columnsVisibilitySkipped.value = [ ...splittedColumns.value.columns .filter((c) => c.visible == false) .map((c) => c.name), @@ -178,10 +190,20 @@ function setUserParams(watchedParams, watchedOrder) { watchedParams = { ...watchedParams, ...where }; delete watchedParams.filter; delete params.value?.filter; - params.value = { ...params.value, ...watchedParams }; + params.value = { ...params.value, ...sanitizer(watchedParams) }; orders.value = parseOrder(order); } +function sanitizer(params) { + for (const [key, value] of Object.entries(params)) { + if (typeof value == 'object') { + const param = Object.values(value)[0]; + if (typeof param == 'string') params[key] = param.replaceAll('%', ''); + } + } + return params; +} + function splitColumns(columns) { splittedColumns.value = { columns: [], @@ -274,10 +296,17 @@ function parseOrder(urlOrders) { const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); defineExpose({ + create: createForm, reload, redirect: redirectFn, selected, + CrudModelRef, }); + +function handleOnDataSaved(_) { + if (_.onDataSaved) _.onDataSaved(this); + else $props.create.onDataSaved(_); +} </script> <template> <QDrawer @@ -315,7 +344,7 @@ defineExpose({ col?.columnFilter !== false && col?.name !== 'tableActions' " - v-model="orders[col.name]" + v-model="orders[col.orderBy ?? col.name]" :name="col.orderBy ?? col.name" :data-key="$attrs['data-key']" :search-url="searchUrl" @@ -332,300 +361,293 @@ defineExpose({ </QScrollArea> </QDrawer> <!-- class in div to fix warn--> - <div class="q-px-md"> - <CrudModel - v-bind="$attrs" - :limit="20" - ref="CrudModelRef" - @on-fetch="(...args) => emit('onFetch', ...args)" - :search-url="searchUrl" - :disable-infinite-scroll="isTableMode" - @save-changes="reload" - :has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable" - :auto-load="hasParams || $attrs['auto-load']" - > - <template - v-for="(_, slotName) in $slots" - #[slotName]="slotData" - :key="slotName" + <CrudModel + v-bind="$attrs" + :class="$attrs['class'] ?? 'q-px-md'" + :limit="$attrs['limit'] ?? 20" + ref="CrudModelRef" + @on-fetch="(...args) => emit('onFetch', ...args)" + :search-url="searchUrl" + :disable-infinite-scroll=" + $attrs['disableInfiniteScroll'] ? isTableMode : !disableInfiniteScroll + " + @save-changes="reload" + :has-sub-toolbar="$props.hasSubToolbar ?? isEditable" + :auto-load="hasParams || $attrs['auto-load']" + > + <template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName"> + <slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" /> + </template> + <template #body="{ rows }"> + <QTable + v-bind="table" + class="vnTable" + :columns="splittedColumns.columns" + :rows="rows" + v-model:selected="selected" + :grid="!isTableMode" + table-header-class="bg-header" + card-container-class="grid-three" + flat + :style="isTableMode && `max-height: ${tableHeight}`" + virtual-scroll + @virtual-scroll=" + (event) => + event.index > rows.length - 2 && + ($props.crudModel?.paginate ?? true) && + CrudModelRef.vnPaginateRef.paginate() + " + @row-click="(_, row) => rowClickFunction && rowClickFunction(row)" + @update:selected="emit('update:selected', $event)" > - <slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" /> - </template> - <template #body="{ rows }"> - <QTable - v-bind="table" - class="vnTable" - :columns="splittedColumns.columns" - :rows="rows" - v-model:selected="selected" - :grid="!isTableMode" - table-header-class="bg-header" - card-container-class="grid-three" - flat - :style="isTableMode && `max-height: ${tableHeight}`" - virtual-scroll - @virtual-scroll=" - (event) => - event.index > rows.length - 2 && - CrudModelRef.vnPaginateRef.paginate() - " - @row-click="(_, row) => rowClickFunction && rowClickFunction(row)" - @update:selected="emit('update:selected', $event)" - > - <template #top-left v-if="!$props.withoutHeader"> - <slot name="top-left"></slot> - </template> - <template #top-right v-if="!$props.withoutHeader"> - <VnVisibleColumn - v-if="isTableMode" - v-model="splittedColumns.columns" - :table-code="tableCode ?? route.name" - :skip="columnsVisibilitySkiped" - /> - <QBtnToggle - v-model="mode" - toggle-color="primary" - class="bg-vn-section-color" - dense - :options="tableModes" - /> - <QBtn - v-if="$props.rightSearch" - icon="filter_alt" - class="bg-vn-section-color q-ml-md" - dense - @click="stateStore.toggleRightDrawer()" - /> - </template> - <template #header-cell="{ col }"> - <QTh v-if="col.visible ?? true"> - <div - class="column self-start q-ml-xs ellipsis" - :class="`text-${col?.align ?? 'left'}`" - :style="$props.columnSearch ? 'height: 75px' : ''" - > - <div - class="row items-center no-wrap" - style="height: 30px" - > - <VnTableOrder - v-model="orders[col.name]" - :name="col.orderBy ?? col.name" - :label="col?.label" - :data-key="$attrs['data-key']" - :search-url="searchUrl" - /> - </div> - <VnTableFilter - v-if="$props.columnSearch" - :column="col" - :show-title="true" + <template #top-left v-if="!$props.withoutHeader"> + <slot name="top-left"></slot> + </template> + <template #top-right v-if="!$props.withoutHeader"> + <VnVisibleColumn + v-if="isTableMode" + v-model="splittedColumns.columns" + :table-code="tableCode ?? route.name" + :skip="columnsVisibilitySkipped" + /> + <QBtnToggle + v-model="mode" + toggle-color="primary" + class="bg-vn-section-color" + dense + :options="tableModes.filter((mode) => !mode.disable)" + /> + <QBtn + v-if="$props.rightSearch" + icon="filter_alt" + class="bg-vn-section-color q-ml-md" + dense + @click="stateStore.toggleRightDrawer()" + /> + </template> + <template #header-cell="{ col }"> + <QTh v-if="col.visible ?? true"> + <div + class="column self-start q-ml-xs ellipsis" + :class="`text-${col?.align ?? 'left'}`" + :style="$props.columnSearch ? 'height: 75px' : ''" + > + <div class="row items-center no-wrap" style="height: 30px"> + <QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip> + <VnTableOrder + v-model="orders[col.orderBy ?? col.name]" + :name="col.orderBy ?? col.name" + :label="col?.label" :data-key="$attrs['data-key']" - v-model="params[columnName(col)]" :search-url="searchUrl" - class="full-width" /> </div> - </QTh> - </template> - <template #header-cell-tableActions> - <QTh auto-width class="sticky" /> - </template> - <template #body-cell-tableStatus="{ col, row }"> - <QTd auto-width :class="getColAlign(col)"> - <VnTableChip - :columns="splittedColumns.columnChips" + <VnTableFilter + v-if="$props.columnSearch" + :column="col" + :show-title="true" + :data-key="$attrs['data-key']" + v-model="params[columnName(col)]" + :search-url="searchUrl" + class="full-width" + /> + </div> + </QTh> + </template> + <template #header-cell-tableActions> + <QTh auto-width class="sticky" /> + </template> + <template #body-cell-tableStatus="{ col, row }"> + <QTd auto-width :class="getColAlign(col)"> + <VnTableChip :columns="splittedColumns.columnChips" :row="row"> + <template #afterChip> + <slot name="afterChip" :row="row"></slot> + </template> + </VnTableChip> + </QTd> + </template> + <template #body-cell="{ col, row, rowIndex }"> + <!-- Columns --> + <QTd + auto-width + class="no-margin q-px-xs" + :class="[getColAlign(col), col.columnClass]" + v-if="col.visible ?? true" + @click.ctrl=" + ($event) => + rowCtrlClickFunction && rowCtrlClickFunction($event, row) + " + > + <slot + :name="`column-${col.name}`" + :col="col" + :row="row" + :row-index="rowIndex" + > + <VnTableColumn + :column="col" :row="row" - > - <template #afterChip> - <slot name="afterChip" :row="row"></slot> - </template> - </VnTableChip> - </QTd> - </template> - <template #body-cell="{ col, row, rowIndex }"> - <!-- Columns --> - <QTd - auto-width - class="no-margin q-px-xs" - :class="[getColAlign(col), col.columnClass]" - v-if="col.visible ?? true" - @click.ctrl=" - ($event) => - rowCtrlClickFunction && - rowCtrlClickFunction($event, row) + :is-editable="col.isEditable ?? isEditable" + v-model="row[col.name]" + component-prop="columnField" + /> + </slot> + </QTd> + </template> + <template #body-cell-tableActions="{ col, row }"> + <QTd + auto-width + :class="getColAlign(col)" + class="sticky no-padding" + @click="stopEventPropagation($event)" + > + <QBtn + v-for="(btn, index) of col.actions" + v-show="btn.show ? btn.show(row) : true" + :key="index" + :title="btn.title" + :icon="btn.icon" + class="q-px-sm text-primary-light" + flat + :style="`visibility: ${ + (btn.show && btn.show(row)) ?? true ? 'visible' : 'hidden' + }`" + @click="btn.action(row)" + /> + </QTd> + </template> + <template #item="{ row, colsMap }"> + <component + :is="$props.redirect ? 'router-link' : 'span'" + :to="`/${$props.redirect}/` + row.id" + > + <QCard + bordered + flat + class="row no-wrap justify-between cursor-pointer" + @click=" + (_, row) => { + $props.rowClick && $props.rowClick(row); + } " > - <slot - :name="`column-${col.name}`" - :col="col" - :row="row" - :row-index="rowIndex" - > - <VnTableColumn - :column="col" - :row="row" - :is-editable="col.isEditable ?? isEditable" - v-model="row[col.name]" - component-prop="columnField" - /> - </slot> - </QTd> - </template> - <template #body-cell-tableActions="{ col, row }"> - <QTd - auto-width - :class="getColAlign(col)" - class="sticky no-padding" - @click="stopEventPropagation($event)" - > - <QBtn - v-for="(btn, index) of col.actions" - :key="index" - :title="btn.title" - :icon="btn.icon" - class="q-px-sm" - flat - :class=" - btn.isPrimary - ? 'text-primary-light' - : 'color-vn-text ' - " - :style="`visibility: ${ - (btn.show && btn.show(row)) ?? true - ? 'visible' - : 'hidden' - }`" - @click="btn.action(row)" - /> - </QTd> - </template> - <template #item="{ row, colsMap }"> - <component - :is="$props.redirect ? 'router-link' : 'span'" - :to="`/${$props.redirect}/` + row.id" - > - <QCard - bordered - flat - class="row no-wrap justify-between cursor-pointer" - @click=" - (_, row) => { - $props.rowClick && $props.rowClick(row); - } - " + <QCardSection + vertical + class="no-margin no-padding" + :class="colsMap.tableActions ? 'w-80' : 'fit'" > + <!-- Chips --> <QCardSection - vertical - class="no-margin no-padding" - :class="colsMap.tableActions ? 'w-80' : 'fit'" + v-if="splittedColumns.chips.length" + class="no-margin q-px-xs q-py-none" > - <!-- Chips --> - <QCardSection - v-if="splittedColumns.chips.length" - class="no-margin q-px-xs q-py-none" + <VnTableChip + :columns="splittedColumns.chips" + :row="row" > - <VnTableChip - :columns="splittedColumns.chips" - :row="row" - > - <template #afterChip> - <slot name="afterChip" :row="row"></slot> - </template> - </VnTableChip> - </QCardSection> - <!-- Title --> - <QCardSection - v-if="splittedColumns.title" - class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis" + <template #afterChip> + <slot name="afterChip" :row="row"></slot> + </template> + </VnTableChip> + </QCardSection> + <!-- Title --> + <QCardSection + v-if="splittedColumns.title" + class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis" + > + <span + :title="row[splittedColumns.title.name]" + @click="stopEventPropagation($event)" + class="cursor-text" > - <span - :title="row[splittedColumns.title.name]" - @click="stopEventPropagation($event)" - class="cursor-text" - > - {{ row[splittedColumns.title.name] }} - </span> - </QCardSection> - <!-- Fields --> - <QCardSection - class="q-pl-sm q-pr-lg q-py-xs" - :class="$props.cardClass" + {{ row[splittedColumns.title.name] }} + </span> + </QCardSection> + <!-- Fields --> + <QCardSection + class="q-pl-sm q-pr-lg q-py-xs" + :class="$props.cardClass" + > + <div + v-for="( + col, index + ) of splittedColumns.cardVisible" + :key="col.name" + class="fields" > - <div - v-for="( - col, index - ) of splittedColumns.cardVisible" - :key="col.name" - class="fields" + <VnLv + :label=" + !col.component && col.label + ? `${col.label}:` + : '' + " > - <VnLv - :label=" - !col.component && col.label - ? `${col.label}:` - : '' - " - > - <template #value> - <span - @click=" - stopEventPropagation($event) - " + <template #value> + <span + @click="stopEventPropagation($event)" + > + <slot + :name="`column-${col.name}`" + :col="col" + :row="row" + :row-index="index" > - <slot - :name="`column-${col.name}`" - :col="col" + <VnTableColumn + :column="col" :row="row" - :row-index="index" - > - <VnTableColumn - :column="col" - :row="row" - :is-editable="false" - v-model="row[col.name]" - component-prop="columnField" - :show-label="true" - /> - </slot> - </span> - </template> - </VnLv> - </div> - </QCardSection> + :is-editable="false" + v-model="row[col.name]" + component-prop="columnField" + :show-label="true" + /> + </slot> + </span> + </template> + </VnLv> + </div> </QCardSection> - <!-- Actions --> - <QCardSection - v-if="colsMap.tableActions" - class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs" - @click="stopEventPropagation($event)" - > - <QBtn - v-for="(btn, index) of splittedColumns.actions - .actions" - :key="index" - :title="btn.title" - :icon="btn.icon" - class="q-pa-xs" - flat - :class=" - btn.isPrimary - ? 'text-primary-light' - : 'color-vn-text ' - " - @click="btn.action(row)" - /> - </QCardSection> - </QCard> - </component> - </template> - </QTable> - </template> - </CrudModel> - </div> - <QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2"> - <QBtn @click="showForm = !showForm" color="primary" fab icon="add" /> + </QCardSection> + <!-- Actions --> + <QCardSection + v-if="colsMap.tableActions" + class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs" + @click="stopEventPropagation($event)" + > + <QBtn + v-for="(btn, index) of splittedColumns.actions + .actions" + :key="index" + :title="btn.title" + :icon="btn.icon" + class="q-pa-xs" + flat + :class=" + btn.isPrimary + ? 'text-primary-light' + : 'color-vn-text ' + " + @click="btn.action(row)" + /> + </QCardSection> + </QCard> + </component> + </template> + </QTable> + </template> + </CrudModel> + <QPageSticky v-if="$props.create" :offset="[20, 20]" style="z-index: 2"> + <QBtn + @click=" + () => + createAsDialog ? (showForm = !showForm) : handleOnDataSaved(create) + " + color="primary" + fab + icon="add" + shortcut="+" + /> <QTooltip> - {{ createForm.title }} + {{ createForm?.title }} </QTooltip> </QPageSticky> <QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> @@ -739,6 +761,7 @@ es: } .q-table__top { top: 0; + padding: 12px 0; } tbody { .q-checkbox { diff --git a/src/components/common/RightMenu.vue b/src/components/common/RightMenu.vue index 732e5367d..3aa1891f9 100644 --- a/src/components/common/RightMenu.vue +++ b/src/components/common/RightMenu.vue @@ -37,7 +37,7 @@ const stateStore = useStateStore(); @click="stateStore.toggleRightDrawer()" round dense - icon="menu" + icon="dock_to_left" > <QTooltip bottom anchor="bottom right"> {{ t('globals.collapseMenu') }} diff --git a/src/components/common/VnCard.vue b/src/components/common/VnCard.vue index 94adbf6cf..7d29da232 100644 --- a/src/components/common/VnCard.vue +++ b/src/components/common/VnCard.vue @@ -1,6 +1,6 @@ <script setup> import { onBeforeMount, computed } from 'vue'; -import { useRoute, onBeforeRouteUpdate } from 'vue-router'; +import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router'; import { useArrayData } from 'src/composables/useArrayData'; import { useStateStore } from 'stores/useStateStore'; import useCardSize from 'src/composables/useCardSize'; @@ -8,7 +8,6 @@ import VnSubToolbar from '../ui/VnSubToolbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue'; import LeftMenu from 'components/LeftMenu.vue'; import RightMenu from 'components/common/RightMenu.vue'; - const props = defineProps({ dataKey: { type: String, required: true }, baseUrl: { type: String, default: undefined }, @@ -18,23 +17,32 @@ const props = defineProps({ filterPanel: { type: Object, default: undefined }, searchDataKey: { type: String, default: undefined }, searchbarProps: { type: Object, default: undefined }, + redirectOnError: { type: Boolean, default: false }, }); const stateStore = useStateStore(); const route = useRoute(); +const router = useRouter(); const url = computed(() => { if (props.baseUrl) return `${props.baseUrl}/${route.params.id}`; return props.customUrl; }); - +const searchRightDataKey = computed(() => { + if (!props.searchDataKey) return route.name; + return props.searchDataKey; +}); const arrayData = useArrayData(props.dataKey, { url: url.value, filter: props.filter, }); onBeforeMount(async () => { - if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; - await arrayData.fetch({ append: false, updateRouter: false }); + try { + if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; + await arrayData.fetch({ append: false, updateRouter: false }); + } catch (e) { + router.push({ name: 'WorkerList' }); + } }); if (props.baseUrl) { @@ -62,17 +70,16 @@ if (props.baseUrl) { <slot name="searchbar" v-if="props.searchDataKey"> <VnSearchbar :data-key="props.searchDataKey" v-bind="props.searchbarProps" /> </slot> - <slot v-else name="searchbar" /> <RightMenu> <template #right-panel v-if="props.filterPanel"> - <component :is="props.filterPanel" :data-key="props.searchDataKey" /> + <component :is="props.filterPanel" :data-key="searchRightDataKey" /> </template> </RightMenu> <QPageContainer> <QPage> <VnSubToolbar /> <div :class="[useCardSize(), $attrs.class]"> - <RouterView /> + <RouterView :key="route.path" /> </div> </QPage> </QPageContainer> diff --git a/src/components/common/VnDmsList.vue b/src/components/common/VnDmsList.vue index c42de6690..16e3c641f 100644 --- a/src/components/common/VnDmsList.vue +++ b/src/components/common/VnDmsList.vue @@ -5,12 +5,14 @@ import { useRoute } from 'vue-router'; import { useQuasar, QCheckbox, QBtn, QInput } from 'quasar'; import axios from 'axios'; +import VnUserLink from '../ui/VnUserLink.vue'; +import { downloadFile } from 'src/composables/downloadFile'; +import VnImg from 'components/ui/VnImg.vue'; import VnPaginate from 'components/ui/VnPaginate.vue'; import VnDms from 'src/components/common/VnDms.vue'; import VnConfirm from 'components/ui/VnConfirm.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; -import VnUserLink from '../ui/VnUserLink.vue'; -import { downloadFile } from 'src/composables/downloadFile'; +import { useSession } from 'src/composables/useSession'; const route = useRoute(); const quasar = useQuasar(); @@ -18,6 +20,7 @@ const { t } = useI18n(); const rows = ref(); const dmsRef = ref(); const formDialog = ref({}); +const token = useSession().getTokenMultimedia(); const $props = defineProps({ model: { @@ -89,6 +92,23 @@ const dmsFilter = { }; const columns = computed(() => [ + { + label: '', + name: 'file', + align: 'left', + component: VnImg, + props: (prop) => { + return { + storage: 'dms', + collection: null, + resolution: null, + id: prop.row.file.split('.')[0], + token: token, + class: 'rounded', + ratio: 1, + }; + }, + }, { align: 'left', field: 'id', @@ -135,19 +155,13 @@ const columns = computed(() => [ field: 'hasFile', label: t('globals.original'), name: 'hasFile', + toolTip: t('The documentation is available in paper form'), component: QCheckbox, props: (prop) => ({ disable: true, 'model-value': Boolean(prop.value), }), }, - { - align: 'left', - field: 'file', - label: t('globals.file'), - name: 'file', - component: 'span', - }, { align: 'left', field: 'worker', @@ -297,6 +311,14 @@ defineExpose({ row-key="clientFk" :grid="$q.screen.lt.sm" > + <template #header="props"> + <QTr :props="props" class="bg"> + <QTh v-for="col in props.cols" :key="col.name" :props="props"> + <QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip + >{{ col.label }} + </QTh> + </QTr> + </template> <template #body-cell="props"> <QTd :props="props"> <QTr :props="props"> @@ -386,10 +408,6 @@ defineExpose({ </QPageSticky> </template> <style scoped> -.q-gutter-y-ms { - display: grid; - row-gap: 20px; -} .labelColor { color: var(--vn-label-color); } @@ -397,8 +415,10 @@ defineExpose({ <i18n> en: contentTypesInfo: Allowed file types {allowedContentTypes} + The documentation is available in paper form: The documentation is available in paper form es: contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes} Generate identifier for original file: Generar identificador para archivo original Upload file: Subir fichero + the documentation is available in paper form: Se tiene la documentación en papel </i18n> diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index 33b97e29d..570d0cbfe 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -1,6 +1,7 @@ <script setup> import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useValidator } from 'src/composables/useValidator'; const emit = defineEmits([ 'update:modelValue', @@ -27,9 +28,11 @@ const $props = defineProps({ default: true, }, }); +const { validations } = useValidator(); const { t } = useI18n(); -const requiredFieldRule = (val) => !!val || t('globals.fieldRequired'); +const requiredFieldRule = (val) => validations().required($attrs.required, val); + const vnInputRef = ref(null); const value = computed({ get() { @@ -57,21 +60,22 @@ const focus = () => { defineExpose({ focus, }); +import { useAttrs } from 'vue'; +const $attrs = useAttrs(); -const inputRules = [ +const mixinRules = [ + requiredFieldRule, + ...($attrs.rules ?? []), (val) => { const { min } = vnInputRef.value.$attrs; + if (!min) return null; if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min }); }, ]; </script> <template> - <div - @mouseover="hover = true" - @mouseleave="hover = false" - :rules="$attrs.required ? [requiredFieldRule] : null" - > + <div @mouseover="hover = true" @mouseleave="hover = false"> <QInput ref="vnInputRef" v-model="value" @@ -80,7 +84,7 @@ const inputRules = [ :class="{ required: $attrs.required }" @keyup.enter="emit('keyup.enter')" :clearable="false" - :rules="inputRules" + :rules="mixinRules" :lazy-rules="true" hide-bottom-space > @@ -88,7 +92,6 @@ const inputRules = [ <slot name="prepend" /> </template> <template #append> - <slot name="append" v-if="$slots.append && !$attrs.disabled" /> <QIcon name="close" size="xs" @@ -100,6 +103,7 @@ const inputRules = [ } " ></QIcon> + <slot name="append" v-if="$slots.append && !$attrs.disabled" /> <QIcon v-if="info" name="info"> <QTooltip max-width="350px"> {{ info }} @@ -115,3 +119,8 @@ const inputRules = [ es: inputMin: Debe ser mayor a {value} </i18n> +<style lang="scss"> +.q-field__append { + padding-inline: 0; +} +</style> diff --git a/src/components/common/VnInputDate.vue b/src/components/common/VnInputDate.vue index 6e57a8a53..96e47d6d7 100644 --- a/src/components/common/VnInputDate.vue +++ b/src/components/common/VnInputDate.vue @@ -3,12 +3,16 @@ import { onMounted, watch, computed, ref } from 'vue'; import { date } from 'quasar'; import { useI18n } from 'vue-i18n'; -const model = defineModel({ type: String }); +const model = defineModel({ type: [String, Date] }); const $props = defineProps({ isOutlined: { type: Boolean, default: false, }, + showEvent: { + type: Boolean, + default: true, + }, }); const { t } = useI18n(); @@ -94,6 +98,7 @@ watch( :class="{ required: $attrs.required }" :rules="$attrs.required ? [requiredFieldRule] : null" :clearable="false" + @click="isPopupOpen = true" > <template #append> <QIcon @@ -111,6 +116,7 @@ watch( " /> <QIcon + v-if="showEvent" name="event" class="cursor-pointer" @click="isPopupOpen = !isPopupOpen" @@ -130,6 +136,7 @@ watch( v-model="popupDate" :landscape="true" :today-btn="true" + :options="$attrs.options" @update:model-value=" (date) => { formattedDate = date; diff --git a/src/components/common/VnInputTime.vue b/src/components/common/VnInputTime.vue index 7ee93de19..b3478bb23 100644 --- a/src/components/common/VnInputTime.vue +++ b/src/components/common/VnInputTime.vue @@ -1,5 +1,5 @@ <script setup> -import { watch, computed, ref, nextTick } from 'vue'; +import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { date } from 'quasar'; @@ -14,13 +14,13 @@ const props = defineProps({ default: false, }, }); +const initialDate = ref(model.value ?? Date.vnNew()); const { t } = useI18n(); const requiredFieldRule = (val) => !!val || t('globals.fieldRequired'); const dateFormat = 'HH:mm'; const isPopupOpen = ref(); const hover = ref(); -const inputRef = ref(); const styleAttrs = computed(() => { return props.isOutlined @@ -50,7 +50,8 @@ const formattedTime = computed({ } if (!props.timeOnly) { const [hh, mm] = time.split(':'); - const date = new Date(model.value ? model.value : null); + + const date = new Date(model.value ? model.value : initialDate.value); date.setHours(hh, mm, 0); time = date?.toISOString(); } @@ -62,37 +63,10 @@ const formattedTime = computed({ function dateToTime(newDate) { return date.formatDate(new Date(newDate), dateFormat); } - -watch( - () => model.value, - (val) => (formattedTime.value = val), - { immediate: true } -); - -watch( - () => formattedTime.value, - async (val) => { - let position = 3; - const input = inputRef.value?.getNativeElement(); - if (!val || !input) return; - - let [hh, mm] = val.split(':'); - hh = parseInt(hh); - if (hh >= 10 || mm != '00') return; - - await nextTick(); - await nextTick(); - if (!hh) position = 0; - input.setSelectionRange(position, position); - }, - { immediate: true } -); </script> - <template> <div @mouseover="hover = true" @mouseleave="hover = false"> <QInput - ref="inputRef" class="vn-input-time" mask="##:##" placeholder="--:--" @@ -102,7 +76,7 @@ watch( style="min-width: 100px" :rules="$attrs.required ? [requiredFieldRule] : null" @click="isPopupOpen = false" - @focus="inputRef.getNativeElement().setSelectionRange(0, 0)" + type="time" > <template #append> <QIcon @@ -149,6 +123,11 @@ watch( border-style: solid; } </style> +<style lang="scss" scoped> +:deep(input[type='time']::-webkit-calendar-picker-indicator) { + display: none; +} +</style> <i18n> es: Open time: Abrir tiempo diff --git a/src/components/common/VnLog.vue b/src/components/common/VnLog.vue index 2163d54d4..ab522cfde 100644 --- a/src/components/common/VnLog.vue +++ b/src/components/common/VnLog.vue @@ -14,6 +14,7 @@ import VnJsonValue from '../common/VnJsonValue.vue'; import FetchData from '../FetchData.vue'; import VnSelect from './VnSelect.vue'; import VnUserLink from '../ui/VnUserLink.vue'; +import VnPaginate from '../ui/VnPaginate.vue'; const stateStore = useStateStore(); const validationsStore = useValidator(); @@ -66,9 +67,10 @@ const filter = { }, }, ], + where: { and: [{ originFk: route.params.id }] }, }; -const workers = ref(); +const paginate = ref(); const actions = ref(); const changeInput = ref(); const searchInput = ref(); @@ -235,9 +237,7 @@ async function openPointRecord(id, modelLog) { const locale = validations[modelLog.model]?.locale || {}; pointRecord.value = parseProps(propNames, locale, data); } -async function setLogTree() { - filter.where = { and: [{ originFk: route.params.id }] }; - const { data } = await getLogs(filter); +async function setLogTree(data) { logTree.value = getLogTree(data); } @@ -266,15 +266,7 @@ async function applyFilter() { filter.where.and.push(selectedFilters.value); } - const { data } = await getLogs(filter); - - logTree.value = getLogTree(data); -} - -async function getLogs(filter) { - return axios.get(props.url ?? `${props.model}Logs`, { - params: { filter: JSON.stringify(filter) }, - }); + paginate.value.fetch(filter); } function setDate(type) { @@ -377,8 +369,6 @@ async function clearFilter() { await applyFilter(); } -setLogTree(); - onUnmounted(() => { stateStore.rightDrawer = false; }); @@ -391,16 +381,6 @@ watch( ); </script> <template> - <FetchData - :url="`${props.model}Logs/${route.params.id}/editors`" - :filter="{ - fields: ['id', 'nickname', 'name', 'image'], - order: 'nickname', - limit: 30, - }" - @on-fetch="(data) => (workers = data)" - auto-load - /> <FetchData :url="`${props.model}Logs/${route.params.id}/models`" :filter="{ order: ['changedModel'] }" @@ -418,231 +398,283 @@ watch( " auto-load /> - <div - class="column items-center logs origin-log q-mt-md" - v-for="(originLog, originLogIndex) in logTree" - :key="originLogIndex" + <VnPaginate + ref="paginate" + :data-key="`${model}Log`" + :url="`${model}Logs`" + :filter="filter" + :skeleton="false" + auto-load + @on-fetch="setLogTree" > - <QItem class="origin-info items-center q-my-md" v-if="logTree.length > 1"> - <h6 class="origin-id text-grey"> - {{ useCapitalize(validations[props.model].locale.name) }} - #{{ originLog.originFk }} - </h6> - <div class="line bg-grey"></div> - </QItem> - <div - class="user-log q-mb-sm" - v-for="(userLog, userIndex) in originLog.logs" - :key="userIndex" - > - <div class="timeline"> - <div class="user-avatar"> - <VnUserLink :worker-id="userLog?.user?.id"> - <template #link> - <VnAvatar - :class="{ 'cursor-pointer': userLog?.user?.id }" - :worker-id="userLog?.user?.id" - :title="userLog?.user?.nickname" - :show-letter="!userLog?.user" - size="lg" - /> - </template> - </VnUserLink> - </div> - <div class="arrow bg-panel" v-if="byRecord"></div> - <div class="line"></div> - </div> - <QList class="user-changes" v-if="userLog"> - <QItem - class="model-log column q-px-none q-py-xs" - v-for="(modelLog, modelLogIndex) in userLog.logs" - :key="modelLogIndex" + <template #body> + <div + class="column items-center logs origin-log q-mt-md" + v-for="(originLog, originLogIndex) in logTree" + :key="originLogIndex" + > + <QItem class="origin-info items-center q-my-md" v-if="logTree.length > 1"> + <h6 class="origin-id text-grey"> + {{ useCapitalize(validations[props.model].locale.name) }} + #{{ originLog.originFk }} + </h6> + <div class="line bg-grey"></div> + </QItem> + <div + class="user-log q-mb-sm" + v-for="(userLog, userIndex) in originLog.logs" + :key="userIndex" > - <QItemSection> - <QItemLabel class="model-info q-mb-xs" v-if="!byRecord"> - <QChip - dense - size="md" - class="model-name q-mr-xs text-white" - v-if=" - !(modelLog.changedModel && modelLog.changedModelId) && - modelLog.model - " - :style="{ - backgroundColor: useColor(modelLog.model), - }" - :title="`${modelLog.model} #${modelLog.id}`" - > - {{ t(modelLog.modelI18n) }} - </QChip> - - <span - class="model-id q-mr-xs" - v-if="modelLog.summaryId" - v-text="`#${modelLog.summaryId}`" - /> - <span - class="model-value" - :title="modelLog.showValue" - v-text="modelLog.showValue" - /> - <QBtn - flat - round - color="grey" - class="q-mr-xs q-ml-auto" - size="sm" - icon="filter_alt" - :title="t('recordChanges')" - @click.stop="filterByRecord(modelLog)" - /> - </QItemLabel> - </QItemSection> - <QItemSection> - <QCard - class="changes-log q-py-none q-mb-xs" - v-for="(log, logIndex) in modelLog.logs" - :key="logIndex" + <div class="timeline"> + <div class="user-avatar"> + <VnUserLink :worker-id="userLog?.user?.id"> + <template #link> + <VnAvatar + :class="{ 'cursor-pointer': userLog?.user?.id }" + :worker-id="userLog?.user?.id" + :title="userLog?.user?.nickname" + :show-letter="!userLog?.user" + size="lg" + /> + </template> + </VnUserLink> + </div> + <div class="arrow bg-panel" v-if="byRecord"></div> + <div class="line"></div> + </div> + <QList class="user-changes" v-if="userLog"> + <QItem + class="model-log column q-px-none q-py-xs" + v-for="(modelLog, modelLogIndex) in userLog.logs" + :key="modelLogIndex" > - <QCardSection class="change-info q-pa-none"> - <QItem - class="q-px-sm q-py-xs justify-between items-center" - > - <div - class="date text-grey text-caption q-mr-sm" - :title=" - date.formatDate( - log.creationDate, - 'DD/MM/YYYY hh:mm:ss' - ) ?? `date:'dd/MM/yyyy HH:mm:ss'` + <QItemSection> + <QItemLabel class="model-info q-mb-xs" v-if="!byRecord"> + <QChip + dense + size="md" + class="model-name q-mr-xs text-white" + v-if=" + !( + modelLog.changedModel && + modelLog.changedModelId + ) && modelLog.model " + :style="{ + backgroundColor: useColor(modelLog.model), + }" + :title="`${modelLog.model} #${modelLog.id}`" > - {{ toRelativeDate(log.creationDate) }} - </div> - <div> - <QBtn - color="grey" - class="pit" - icon="preview" - flat - round - :title="t('pointRecord')" - padding="none" - v-if="log.action != 'insert'" - @click.stop=" - openPointRecord(log.id, modelLog) - " - > - <QPopupProxy> - <QCard v-if="pointRecord"> - <div - class="header q-px-sm q-py-xs q-ma-none text-white text-bold bg-primary" - > - {{ modelLog.modelI18n }} - <span v-if="modelLog.id" - >#{{ modelLog.id }}</span - > - </div> - <QCardSection - class="change-detail q-pa-sm" - > - <QItem - v-for="( - value, index - ) in pointRecord" - :key="index" - class="q-pa-none" - > - <span - class="json-field q-mr-xs text-grey" - :title="value.name" - > - {{ value.nameI18n }}: - </span> - <VnJsonValue - :value="value.val.val" - /> - </QItem> - </QCardSection> - </QCard> - </QPopupProxy> - </QBtn> - <QIcon - class="action q-ml-xs" - :class="actionsClass[log.action]" - :name="actionsIcon[log.action]" - :title=" - t(`actions.${actionsText[log.action]}`) - " - /> - </div> - </QItem> - </QCardSection> - <QCardSection - class="change-detail q-px-sm q-py-xs" - :class="{ expanded: log.expand }" - v-if="log.props.length || log.description" - > - <QIcon - class="cursor-pointer q-mr-md" - color="grey" - name="expand_more" - :title="t('globals.details')" - size="sm" - @click="log.expand = !log.expand" - /> - <span v-if="log.props.length" class="attributes"> - <span v-if="!log.expand" class="q-pa-none text-grey"> - <span - v-for="(prop, propIndex) in log.props" - :key="propIndex" - class="basic-json" - > - <span class="json-field" :title="prop.name"> - {{ prop.nameI18n }}: - </span> - <VnJsonValue :value="prop.val.val" /> - <span v-if="propIndex < log.props.length - 1" - >, - </span> - </span> - </span> + {{ t(modelLog.modelI18n) }} + </QChip> + <span - v-if="log.expand" - class="expanded-json column q-pa-none" - > - <div - v-for="(prop, prop2Index) in log.props" - :key="prop2Index" - class="q-pa-none text-grey" + class="model-id q-mr-xs" + v-if="modelLog.summaryId" + v-text="`#${modelLog.summaryId}`" + /> + <span + class="model-value" + :title="modelLog.showValue" + v-text="modelLog.showValue" + /> + <QBtn + flat + round + color="grey" + class="q-mr-xs q-ml-auto" + size="sm" + icon="filter_alt" + :title="t('recordChanges')" + @click.stop="filterByRecord(modelLog)" + /> + </QItemLabel> + </QItemSection> + <QItemSection> + <QCard + class="changes-log q-py-none q-mb-xs" + v-for="(log, logIndex) in modelLog.logs" + :key="logIndex" + > + <QCardSection class="change-info q-pa-none"> + <QItem + class="q-px-sm q-py-xs justify-between items-center" > - <span class="json-field" :title="prop.name"> - {{ prop.nameI18n }}: - </span> - <VnJsonValue :value="prop.val.val" /> - <span v-if="prop.val.id" class="id-value"> - #{{ prop.val.id }} - </span> - <span v-if="log.action == 'update'"> - ← - <VnJsonValue :value="prop.old.val" /> - <span v-if="prop.old.id" class="id-value"> - #{{ prop.old.id }} + <div + class="date text-grey text-caption q-mr-sm" + :title=" + date.formatDate( + log.creationDate, + 'DD/MM/YYYY hh:mm:ss' + ) ?? `date:'dd/MM/yyyy HH:mm:ss'` + " + > + {{ toRelativeDate(log.creationDate) }} + </div> + <div> + <QBtn + color="grey" + class="pit" + icon="preview" + flat + round + :title="t('pointRecord')" + padding="none" + v-if="log.action != 'insert'" + @click.stop=" + openPointRecord(log.id, modelLog) + " + > + <QPopupProxy> + <QCard v-if="pointRecord"> + <div + class="header q-px-sm q-py-xs q-ma-none text-white text-bold bg-primary" + > + {{ modelLog.modelI18n }} + <span v-if="modelLog.id" + >#{{ + modelLog.id + }}</span + > + </div> + <QCardSection + class="change-detail q-pa-sm" + > + <QItem + v-for="( + value, index + ) in pointRecord" + :key="index" + class="q-pa-none" + > + <span + class="json-field q-mr-xs text-grey" + :title=" + value.name + " + > + {{ + value.nameI18n + }}: + </span> + <VnJsonValue + :value=" + value.val.val + " + /> + </QItem> + </QCardSection> + </QCard> + </QPopupProxy> + </QBtn> + <QIcon + class="action q-ml-xs" + :class="actionsClass[log.action]" + :name="actionsIcon[log.action]" + :title=" + t( + `actions.${ + actionsText[log.action] + }` + ) + " + /> + </div> + </QItem> + </QCardSection> + <QCardSection + class="change-detail q-px-sm q-py-xs" + :class="{ expanded: log.expand }" + v-if="log.props.length || log.description" + > + <QIcon + class="cursor-pointer q-mr-md" + color="grey" + name="expand_more" + :title="t('globals.details')" + size="sm" + @click="log.expand = !log.expand" + /> + <span v-if="log.props.length" class="attributes"> + <span + v-if="!log.expand" + class="q-pa-none text-grey" + > + <span + v-for="(prop, propIndex) in log.props" + :key="propIndex" + class="basic-json" + > + <span + class="json-field" + :title="prop.name" + > + {{ prop.nameI18n }}: + </span> + <VnJsonValue :value="prop.val.val" /> + <span + v-if=" + propIndex < + log.props.length - 1 + " + >, + </span> </span> </span> - </div> - </span> - </span> - <span v-if="!log.props.length" class="description"> - {{ log.description }} - </span> - </QCardSection> - </QCard> - </QItemSection> - </QItem> - </QList> - </div> - </div> + <span + v-if="log.expand" + class="expanded-json column q-pa-none" + > + <div + v-for="( + prop, prop2Index + ) in log.props" + :key="prop2Index" + class="q-pa-none text-grey" + > + <span + class="json-field" + :title="prop.name" + > + {{ prop.nameI18n }}: + </span> + <VnJsonValue :value="prop.val.val" /> + <span + v-if="prop.val.id" + class="id-value" + > + #{{ prop.val.id }} + </span> + <span v-if="log.action == 'update'"> + ← + <VnJsonValue + :value="prop.old.val" + /> + <span + v-if="prop.old.id" + class="id-value" + > + #{{ prop.old.id }} + </span> + </span> + </div> + </span> + </span> + <span + v-if="!log.props.length" + class="description" + > + {{ log.description }} + </span> + </QCardSection> + </QCard> + </QItemSection> + </QItem> + </QList> + </div> + </div> + </template> + </VnPaginate> <Teleport to="#right-panel" v-if="stateStore.isHeaderMounted()"> <QList dense> <QSeparator /> @@ -691,17 +723,16 @@ watch( </QOptionGroup> </QItem> <QItem class="q-mt-sm"> - <QItemSection v-if="!workers"> - <QSkeleton type="QInput" class="full-width" /> - </QItemSection> - <QItemSection v-if="workers && userRadio !== null"> + <QItemSection v-if="userRadio !== null"> <VnSelect class="full-width" :label="t('globals.user')" v-model="userSelect" option-label="name" option-value="id" - :options="workers" + :url="`${model}Logs/${$route.params.id}/editors`" + :fields="['id', 'nickname', 'name', 'image']" + sort-by="nickname" @update:model-value="selectFilter('userSelect')" hide-selected > diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 63a3f088f..1e3a32f48 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -73,6 +73,14 @@ const $props = defineProps({ type: Boolean, default: true, }, + params: { + type: Object, + default: null, + }, + noOne: { + type: Boolean, + default: false, + }, }); const { t } = useI18n(); @@ -85,6 +93,11 @@ 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 value = computed({ get() { @@ -100,9 +113,11 @@ watch(options, (newValue) => { setOptions(newValue); }); -watch(modelValue, (newValue) => { +watch(modelValue, async (newValue) => { if (!myOptions.value.some((option) => option[optionValue.value] == newValue)) - fetchFilter(newValue); + await fetchFilter(newValue); + + if ($props.noOne) myOptions.value.unshift(noOneOpt.value); }); onMounted(() => { @@ -153,13 +168,19 @@ async function fetchFilter(val) { ? optionValue.value : optionFilter.value ?? optionLabel.value); - const defaultWhere = $props.useLike - ? { [key]: { like: `%${val}%` } } - : { [key]: val }; + let defaultWhere = {}; + if ($props.filterOptions.length) { + defaultWhere = $props.filterOptions.reduce((obj, prop) => { + if (!obj.or) obj.or = []; + obj.or.push({ [prop]: getVal(val) }); + return obj; + }, {}); + } else defaultWhere = { [key]: getVal(val) }; const where = { ...(val ? defaultWhere : {}), ...$props.where }; const fetchOptions = { where, include, limit }; if (fields) fetchOptions.fields = fields; if (sortBy) fetchOptions.order = sortBy; + return dataRef.value.fetch(fetchOptions); } @@ -180,6 +201,9 @@ async function filterHandler(val, update) { } else newOptions = filter(val, myOptionsOriginal.value); update( () => { + if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase())) + newOptions.unshift(noOneOpt.value); + myOptions.value = newOptions; }, (ref) => { @@ -194,6 +218,8 @@ async function filterHandler(val, update) { function nullishToTrue(value) { return value ?? true; } + +const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val); </script> <template> @@ -205,6 +231,7 @@ function nullishToTrue(value) { :limit="limit" :sort-by="sortBy" :fields="fields" + :params="params" /> <QSelect v-model="value" diff --git a/src/components/common/VnSelectDialog.vue b/src/components/common/VnSelectDialog.vue index 7f7c29f5d..17f893255 100644 --- a/src/components/common/VnSelectDialog.vue +++ b/src/components/common/VnSelectDialog.vue @@ -1,6 +1,7 @@ <script setup> import { ref, computed } from 'vue'; import { useRole } from 'src/composables/useRole'; +import { useAcl } from 'src/composables/useAcl'; import VnSelect from 'src/components/common/VnSelect.vue'; const emit = defineEmits(['update:modelValue']); @@ -11,6 +12,10 @@ const $props = defineProps({ type: Array, default: () => ['developer'], }, + acls: { + type: Array, + default: () => [], + }, actionIcon: { type: String, default: 'add', @@ -22,15 +27,12 @@ const $props = defineProps({ }); const role = useRole(); -const showForm = ref(false); +const acl = useAcl(); const isAllowedToCreate = computed(() => { + if ($props.acls.length) return acl.hasAny($props.acls); return role.hasAny($props.rolesAllowedToCreate); }); - -const toggleForm = () => { - showForm.value = !showForm.value; -}; </script> <template> @@ -41,7 +43,7 @@ const toggleForm = () => { > <template v-if="isAllowedToCreate" #append> <QIcon - @click.stop.prevent="toggleForm()" + @click.stop.prevent="$refs.dialog.show()" :name="actionIcon" :size="actionIcon === 'add' ? 'xs' : 'sm'" :class="['default-icon', { '--add-icon': actionIcon === 'add' }]" @@ -51,7 +53,7 @@ const toggleForm = () => { > <QTooltip v-if="tooltip">{{ tooltip }}</QTooltip> </QIcon> - <QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> + <QDialog ref="dialog" transition-show="scale" transition-hide="scale"> <slot name="form" /> </QDialog> </template> diff --git a/src/components/ui/CatalogItem.vue b/src/components/ui/CatalogItem.vue index ef722483b..545bfbbb4 100644 --- a/src/components/ui/CatalogItem.vue +++ b/src/components/ui/CatalogItem.vue @@ -127,11 +127,6 @@ const dialog = ref(null); flex-direction: column; gap: 4px; - .subName { - color: var(--vn-label-color); - text-transform: uppercase; - } - p { margin-bottom: 0; } diff --git a/src/components/ui/FetchedTags.vue b/src/components/ui/FetchedTags.vue index beaa85bfe..a0edf85f8 100644 --- a/src/components/ui/FetchedTags.vue +++ b/src/components/ui/FetchedTags.vue @@ -2,10 +2,6 @@ import { computed } from 'vue'; const $props = defineProps({ - maxLength: { - type: Number, - required: true, - }, item: { type: Object, required: true, diff --git a/src/components/ui/VnConfirm.vue b/src/components/ui/VnConfirm.vue index 0480650db..4fa374b62 100644 --- a/src/components/ui/VnConfirm.vue +++ b/src/components/ui/VnConfirm.vue @@ -15,7 +15,7 @@ const props = defineProps({ default: null, }, message: { - type: String, + type: [String, Boolean], default: null, }, data: { @@ -31,11 +31,15 @@ const props = defineProps({ }); defineEmits(['confirm', ...useDialogPluginComponent.emits]); +defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.hide() }); const { dialogRef, onDialogOK } = useDialogPluginComponent(); const title = props.title || t('Confirm'); -const message = props.message || t('Are you sure you want to continue?'); +const message = + props.message || + (props.message !== false ? t('Are you sure you want to continue?') : false); + const isLoading = ref(false); async function confirm() { @@ -61,12 +65,14 @@ async function confirm() { size="xl" v-if="icon" /> - <span class="text-h6 text-grey">{{ title }}</span> + <span class="text-h6">{{ title }}</span> <QSpace /> <QBtn icon="close" :disable="isLoading" flat round dense v-close-popup /> </QCardSection> - <QCardSection class="row items-center"> - <span v-html="message"></span> + <QCardSection class="q-pb-none"> + <span v-if="message !== false" v-html="message" /> + </QCardSection> + <QCardSection class="row items-center q-pt-none"> <slot name="customHTML"></slot> </QCardSection> <QCardActions align="right"> diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index e3019663c..637180c22 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -3,6 +3,7 @@ import { onMounted, ref, computed, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useArrayData } from 'composables/useArrayData'; import { useRoute } from 'vue-router'; +import { date } from 'quasar'; import toDate from 'filters/toDate'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; @@ -24,7 +25,7 @@ const $props = defineProps({ type: Boolean, default: true, }, - unRemovableParams: { + unremovableParams: { type: Array, required: false, default: () => [], @@ -92,16 +93,18 @@ function setUserParams(watchedParams) { const order = watchedParams.filter?.order; delete watchedParams.filter; - userParams.value = { ...userParams.value, ...sanitizer(watchedParams) }; + userParams.value = sanitizer(watchedParams); emit('setUserParams', userParams.value, order); } watch( - () => [route.query[$props.searchUrl], arrayData.store.userParams], - ([newSearchUrl, newUserParams], [oldSearchUrl, oldUserParams]) => { - if (newSearchUrl || oldSearchUrl) setUserParams(newSearchUrl); - if (newUserParams || oldUserParams) setUserParams(newUserParams); - } + () => route.query[$props.searchUrl], + (val, oldValue) => (val || oldValue) && setUserParams(val) +); + +watch( + () => arrayData.store.userParams, + (val, oldValue) => (val || oldValue) && setUserParams(val) ); watch( @@ -111,58 +114,51 @@ watch( const isLoading = ref(false); async function search(evt) { - if (evt && $props.disableSubmitEvent) return; + try { + if (evt && $props.disableSubmitEvent) return; - store.filter.where = {}; - isLoading.value = true; - const filter = { ...userParams.value, ...$props.modelValue }; - store.userParamsChanged = true; - const { params: newParams } = await arrayData.addFilter({ - params: filter, - }); - userParams.value = newParams; + store.filter.where = {}; + isLoading.value = true; + const filter = { ...userParams.value, ...$props.modelValue }; + store.userParamsChanged = true; + const { params: newParams } = await arrayData.addFilter({ + params: filter, + }); + userParams.value = newParams; - if (!$props.showAll && !Object.values(filter).length) store.data = []; - - isLoading.value = false; - emit('search'); -} - -async function reload() { - isLoading.value = true; - const params = Object.values(userParams.value).filter((param) => param); - store.skip = 0; - store.page = 1; - await arrayData.fetch({ append: false }); - if (!$props.showAll && !params.length) store.data = []; - isLoading.value = false; - emit('refresh'); + if (!$props.showAll && !Object.values(filter).length) store.data = []; + emit('search'); + } finally { + isLoading.value = false; + } } async function clearFilters() { - isLoading.value = true; - store.userParamsChanged = true; - arrayData.reset(['skip', 'filter.skip', 'page']); - // Filtrar los params no removibles - const removableFilters = Object.keys(userParams.value).filter((param) => - $props.unRemovableParams.includes(param) - ); - const newParams = {}; - // Conservar solo los params que no son removibles - for (const key of removableFilters) { - newParams[key] = userParams.value[key]; - } - userParams.value = {}; - userParams.value = { ...newParams }; // Actualizar los params con los removibles - await arrayData.applyFilter({ params: userParams.value }); + try { + isLoading.value = true; + store.userParamsChanged = true; + arrayData.reset(['skip', 'filter.skip', 'page']); + // Filtrar los params no removibles + const removableFilters = Object.keys(userParams.value).filter((param) => + $props.unremovableParams.includes(param) + ); + const newParams = {}; + // Conservar solo los params que no son removibles + for (const key of removableFilters) { + newParams[key] = userParams.value[key]; + } + userParams.value = {}; + userParams.value = { ...newParams }; // Actualizar los params con los removibles + await arrayData.applyFilter({ params: userParams.value }); - if (!$props.showAll) { - store.data = []; + if (!$props.showAll) { + store.data = []; + } + emit('clear'); + emit('update:modelValue', userParams.value); + } finally { + isLoading.value = false; } - - isLoading.value = false; - emit('clear'); - emit('update:modelValue', userParams.value); } const tagsList = computed(() => { @@ -176,10 +172,10 @@ const tagsList = computed(() => { }); const tags = computed(() => { - return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key)); + return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.label)); }); const customTags = computed(() => - tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key)) + tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label)) ); async function remove(key) { @@ -190,6 +186,7 @@ async function remove(key) { } function formatValue(value) { + if (value instanceof Date) return date.formatDate(value, 'DD/MM/YYYY'); if (typeof value === 'boolean') return value ? t('Yes') : t('No'); if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value); @@ -198,7 +195,7 @@ function formatValue(value) { function sanitizer(params) { for (const [key, value] of Object.entries(params)) { - if (typeof value == 'object') { + if (value && typeof value === 'object') { const param = Object.values(value)[0]; if (typeof param == 'string') params[key] = param.replaceAll('%', ''); } @@ -225,32 +222,18 @@ function sanitizer(params) { </QItemLabel> </QItemSection> <QItemSection top side> - <div class="q-gutter-xs"> - <QBtn - @click="clearFilters" - color="primary" - dense - flat - icon="filter_list_off" - padding="none" - round - size="sm" - > - <QTooltip>{{ t('Remove filters') }}</QTooltip> - </QBtn> - <QBtn - @click="reload" - color="primary" - dense - flat - icon="refresh" - padding="none" - round - size="sm" - > - <QTooltip>{{ t('Refresh') }}</QTooltip> - </QBtn> - </div> + <QBtn + @click="clearFilters" + color="primary" + dense + flat + icon="filter_list_off" + padding="none" + round + size="sm" + > + <QTooltip>{{ t('Remove filters') }}</QTooltip> + </QBtn> </QItemSection> </QItem> <QItem class="q-mb-sm"> @@ -264,7 +247,7 @@ function sanitizer(params) { <VnFilterPanelChip v-for="chip of tags" :key="chip.label" - :removable="!unRemovableParams.includes(chip.label)" + :removable="!unremovableParams?.includes(chip.label)" @remove="remove(chip.label)" > <slot name="tags" :tag="chip" :format-fn="formatValue"> diff --git a/src/components/ui/VnImg.vue b/src/components/ui/VnImg.vue index 9585b81d8..ceb4e8468 100644 --- a/src/components/ui/VnImg.vue +++ b/src/components/ui/VnImg.vue @@ -39,6 +39,8 @@ const getUrl = (zoom = false) => { const curResolution = zoom ? $props.zoomResolution || $props.resolution : $props.resolution; + if ($props.storage === 'dms') + return `/api/${$props.storage}/${$props.id}/downloadFile?access_token=${token}`; return isEmployee ? `/api/${$props.storage}/${$props.collection}/${curResolution}/${$props.id}/download?access_token=${token}&${timeStamp.value}` : noImage; @@ -52,6 +54,7 @@ defineExpose({ </script> <template> <QImg + :draggable="true" :class="{ zoomIn: zoom }" :src="getUrl()" v-bind="$attrs" @@ -60,10 +63,12 @@ defineExpose({ /> <QDialog v-if="$props.zoom" v-model="show"> <QImg + :draggable="true" :src="getUrl(true)" v-bind="$attrs" spinner-color="primary" class="img_zoom" + :ratio="0" /> </QDialog> </template> diff --git a/src/components/ui/VnPaginate.vue b/src/components/ui/VnPaginate.vue index 0df719c66..79a79c383 100644 --- a/src/components/ui/VnPaginate.vue +++ b/src/components/ui/VnPaginate.vue @@ -10,6 +10,10 @@ const props = defineProps({ type: String, required: true, }, + class: { + type: String, + default: '', + }, autoLoad: { type: Boolean, default: false, @@ -115,8 +119,8 @@ watch( ); watch( - () => [props.url, props.filter], - ([url, filter]) => mounted.value && fetch({ url, filter }) + () => [props.url, props.filter, props.userParams], + ([url, filter, userParams]) => mounted.value && fetch({ url, filter, userParams }) ); const addFilter = async (filter, params) => { @@ -215,18 +219,25 @@ defineExpose({ fetch, addFilter, paginate }); v-if="store.data" @load="onLoad" :offset="offset" - class="full-width" + :class="['full-width', props.class]" :disable="disableInfiniteScroll || !store.hasMoreData" v-bind="$attrs" > <slot name="body" :rows="store.data"></slot> - <div v-if="isLoading" class="info-row q-pa-md text-center"> + <div v-if="isLoading" class="spinner info-row q-pa-md text-center"> <QSpinner color="primary" size="md" /> </div> </QInfiniteScroll> </template> <style lang="scss" scoped> +.spinner { + z-index: 1; + align-content: end; + position: absolute; + bottom: 0; + left: 0; +} .info-row { width: 100%; diff --git a/src/components/ui/VnRow.vue b/src/components/ui/VnRow.vue index 5c01d7216..16bcfab7d 100644 --- a/src/components/ui/VnRow.vue +++ b/src/components/ui/VnRow.vue @@ -1,3 +1,6 @@ +<script setup> +defineProps({ wrap: { type: Boolean, default: false } }); +</script> <template> <div class="vn-row q-gutter-md q-mb-md"> <slot /> @@ -9,10 +12,15 @@ > :deep(*) { flex: 1; } + &[wrap] { + flex-wrap: wrap; + } } @media screen and (max-width: 800px) { .vn-row { - flex-direction: column; + &:not(.wrap) { + flex-direction: column; + } } } </style> diff --git a/src/components/ui/VnSearchbar.vue b/src/components/ui/VnSearchbar.vue index 2053efd62..a78403b5c 100644 --- a/src/components/ui/VnSearchbar.vue +++ b/src/components/ui/VnSearchbar.vue @@ -63,17 +63,13 @@ const props = defineProps({ type: String, default: '', }, - makeFetch: { - type: Boolean, - default: true, - }, - searchUrl: { - type: String, - default: 'params', + whereFilter: { + type: Function, + default: undefined, }, }); -const searchText = ref(''); +const searchText = ref(); let arrayDataProps = { ...props }; if (props.redirect) arrayDataProps = { @@ -104,18 +100,23 @@ onMounted(() => { }); async function search() { - const staticParams = Object.entries(store.userParams).filter( - ([key, value]) => value && (props.staticParams || []).includes(key) - ); + const staticParams = Object.entries(store.userParams); arrayData.reset(['skip', 'page']); - if (props.makeFetch) - await arrayData.applyFilter({ - params: { - ...Object.fromEntries(staticParams), - search: searchText.value, - }, - }); + const filter = { + params: { + ...Object.fromEntries(staticParams), + search: searchText.value, + }, + }; + + if (props.whereFilter) { + filter.filter = { + where: props.whereFilter(searchText.value), + }; + delete filter.params.search; + } + await arrayData.applyFilter(filter); } </script> <template> @@ -123,7 +124,7 @@ async function search() { <QForm @submit="search" id="searchbarForm"> <VnInput id="searchbar" - v-model="searchText" + v-model.trim="searchText" :placeholder="t(props.label)" dense standout diff --git a/src/components/ui/VnSms.vue b/src/components/ui/VnSms.vue index 81058a6cb..bf6e0695e 100644 --- a/src/components/ui/VnSms.vue +++ b/src/components/ui/VnSms.vue @@ -1,5 +1,5 @@ <script setup> -import { watch, computed } from 'vue'; +import { computed } from 'vue'; import { date } from 'quasar'; import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnAvatar from '../ui/VnAvatar.vue'; diff --git a/src/components/ui/VnSubToolbar.vue b/src/components/ui/VnSubToolbar.vue index 5e2412437..5ded4be00 100644 --- a/src/components/ui/VnSubToolbar.vue +++ b/src/components/ui/VnSubToolbar.vue @@ -43,20 +43,9 @@ onBeforeUnmount(() => stateStore.toggleSubToolbar()); </slot> </QToolbar> </template> -<style lang="scss"> -.q-toolbar { - background: var(--vn-section-color); -} -</style> <style lang="scss" scoped> .sticky { position: sticky; - top: 61px; z-index: 1; } -@media (max-width: $breakpoint-sm) { - .sticky { - top: 90px; - } -} </style> diff --git a/src/components/ui/VnUserLink.vue b/src/components/ui/VnUserLink.vue index b04ea7476..00c50ee34 100644 --- a/src/components/ui/VnUserLink.vue +++ b/src/components/ui/VnUserLink.vue @@ -1,20 +1,18 @@ <script setup> import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import { useI18n } from 'vue-i18n'; -const $props = defineProps({ +defineProps({ name: { type: String, default: null }, + tag: { type: String, default: null }, workerId: { type: Number, default: null }, defaultName: { type: Boolean, default: false }, }); - -const { t } = useI18n(); </script> <template> <slot name="link"> - <span :class="{ link: $props.workerId }"> - {{ $props.defaultName ? $props.name ?? t('globals.system') : $props.name }} + <span :class="{ link: workerId }"> + {{ defaultName ? name ?? $t('globals.system') : name }} </span> </slot> - <WorkerDescriptorProxy v-if="$props.workerId" :id="$props.workerId" /> + <WorkerDescriptorProxy v-if="workerId" :id="workerId" /> </template> diff --git a/src/composables/getDateQBadgeColor.js b/src/composables/getDateQBadgeColor.js index be9ef41b5..a91213a0a 100644 --- a/src/composables/getDateQBadgeColor.js +++ b/src/composables/getDateQBadgeColor.js @@ -7,5 +7,5 @@ export function getDateQBadgeColor(date) { let comparation = today - timeTicket; if (comparation == 0) return 'warning'; - if (comparation < 0) return 'negative'; + if (comparation < 0) return 'success'; } diff --git a/src/composables/getTotal.js b/src/composables/getTotal.js new file mode 100644 index 000000000..41c4330c4 --- /dev/null +++ b/src/composables/getTotal.js @@ -0,0 +1,10 @@ +import { toCurrency } from 'src/filters'; + +export function getTotal(rows, key, opts = {}) { + const { currency, cb } = opts; + const total = rows.reduce((acc, row) => acc + +(cb ? cb(row) : row[key] || 0), 0); + + return currency + ? toCurrency(total, currency == 'default' ? undefined : currency) + : total; +} diff --git a/src/composables/useAcl.js b/src/composables/useAcl.js index 46aaa3c25..ede359186 100644 --- a/src/composables/useAcl.js +++ b/src/composables/useAcl.js @@ -16,13 +16,18 @@ export function useAcl() { state.setAcls(acls); } - function hasAny(model, prop, accessType) { - const acls = state.getAcls().value[model]; - if (acls) - return ['*', prop].some((key) => { - const acl = acls[key]; - return acl && (acl['*'] || acl[accessType]); - }); + function hasAny(acls) { + for (const acl of acls) { + let { model, props, accessType } = acl; + const modelAcls = state.getAcls().value[model]; + Array.isArray(props) || (props = [props]); + if (modelAcls) + return ['*', ...props].some((key) => { + const acl = modelAcls[key]; + return acl && (acl['*'] || acl[accessType]); + }); + } + return false; } return { diff --git a/src/composables/usePrintService.js b/src/composables/usePrintService.js index edf9598b2..68a009d7b 100644 --- a/src/composables/usePrintService.js +++ b/src/composables/usePrintService.js @@ -16,7 +16,8 @@ export function usePrintService() { ); } - function openReport(path, params) { + function openReport(path, params, isNewTab = '_self') { + if (typeof params === 'string') params = JSON.parse(params); params = Object.assign( { access_token: getTokenMultimedia(), @@ -25,8 +26,7 @@ export function usePrintService() { ); const query = new URLSearchParams(params).toString(); - - window.open(`api/${path}?${query}`); + window.open(`api/${path}?${query}`, isNewTab); } return { diff --git a/src/composables/useValidator.js b/src/composables/useValidator.js index 5ad96ea1b..7a7032608 100644 --- a/src/composables/useValidator.js +++ b/src/composables/useValidator.js @@ -28,7 +28,7 @@ export function useValidator() { } const { t } = useI18n(); - const validations = function (validation) { + const validations = function (validation = {}) { return { format: (value) => { const { allowNull, with: format, allowBlank } = validation; @@ -40,12 +40,15 @@ export function useValidator() { if (!isValid) return message; }, presence: (value) => { - let message = `Value can't be empty`; + let message = t(`globals.valueCantBeEmpty`); if (validation.message) message = t(validation.message) || validation.message; return !validator.isEmpty(value ? String(value) : '') || message; }, + required: (required, value) => { + return required ? !!value || t('globals.fieldRequired') : null; + }, length: (value) => { const options = { min: validation.min || validation.is, @@ -71,12 +74,17 @@ export function useValidator() { return validator.isInt(value) || 'Value should be integer'; return validator.isNumeric(value) || 'Value should be a number'; }, + min: (value, min) => { + if (min >= 0) + if (Math.floor(value) < min) return t('inputMin', { value: min }); + }, custom: (value) => validation.bindedFunction(value) || 'Invalid value', }; }; return { validate, + validations, models, }; } diff --git a/src/css/app.scss b/src/css/app.scss index 3efe92db4..3c51dc8af 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -37,6 +37,10 @@ a { .link { color: $color-link; cursor: pointer; + + &--white { + color: white; + } } .tx-color-link { @@ -103,10 +107,6 @@ select:-webkit-autofill { border-radius: 8px; } -.card-width { - width: 770px; -} - .vn-card-list { width: 100%; max-width: 60em; @@ -190,6 +190,10 @@ select:-webkit-autofill { font-size: medium; } +.q-toolbar { + background: var(--vn-section-color); +} + .q-card__actions { justify-content: center; } @@ -264,3 +268,15 @@ input::-webkit-inner-spin-button { max-width: 400px; } } +.edit-photo-btn { + position: absolute; + right: 12px; + bottom: 12px; + z-index: 1; + cursor: pointer; +} + +.subName { + color: var(--vn-label-color); + text-transform: uppercase; +} diff --git a/src/filters/date.js b/src/filters/date.js index f9fd1e0b2..058c90060 100644 --- a/src/filters/date.js +++ b/src/filters/date.js @@ -20,21 +20,21 @@ export function isValidDate(date) { * Converts a given date to a specific format. * * @param {number|string|Date} date - The date to be formatted. + * @param {Object} opts - Optional parameters to customize the output format. * @returns {string} The formatted date as a string in 'dd/mm/yyyy' format. If the provided date is not valid, an empty string is returned. * * @example * // returns "02/12/2022" * toDateFormat(new Date(2022, 11, 2)); */ -export function toDateFormat(date, locale = 'es-ES') { - if (!isValidDate(date)) { - return ''; - } - return new Date(date).toLocaleDateString(locale, { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }); +export function toDateFormat(date, locale = 'es-ES', opts = {}) { + if (!isValidDate(date)) return ''; + + const format = Object.assign( + { year: 'numeric', month: '2-digit', day: '2-digit' }, + opts + ); + return new Date(date).toLocaleDateString(locale, format); } /** diff --git a/src/filters/dateRange.js b/src/filters/dateRange.js index 7aa2869e5..4c0cfe654 100644 --- a/src/filters/dateRange.js +++ b/src/filters/dateRange.js @@ -1,7 +1,7 @@ export default function dateRange(value) { const minHour = new Date(value); minHour.setHours(0, 0, 0, 0); - const maxHour = new Date(); + const maxHour = new Date(value); maxHour.setHours(23, 59, 59, 59); return [minHour, maxHour]; diff --git a/src/filters/getParamWhere.js b/src/filters/getParamWhere.js new file mode 100644 index 000000000..48cd9c479 --- /dev/null +++ b/src/filters/getParamWhere.js @@ -0,0 +1,21 @@ +// parsing JSON safely +function parseJSON(str, fallback) { + try { + return JSON.parse(str ?? '{}'); + } catch (e) { + console.error('Error parsing JSON:', e); + return fallback; + } +} +export default function (route, param) { + // catch route query params + const params = parseJSON(route?.query?.params, {}); + + // extract and parse filter from params + const { filter: filterStr = '{}' } = params; + const where = parseJSON(filterStr, {})?.where; + if (where && where[param] !== undefined) { + return where[param]; + } + return null; +} diff --git a/src/filters/index.js b/src/filters/index.js index 940788ed1..5f08f19c7 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -11,6 +11,7 @@ import dashIfEmpty from './dashIfEmpty'; import dateRange from './dateRange'; import toHour from './toHour'; import dashOrCurrency from './dashOrCurrency'; +import getParamWhere from './getParamWhere'; export { toLowerCase, @@ -26,4 +27,5 @@ export { toPercentage, dashIfEmpty, dateRange, + getParamWhere, }; diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index f61dda8a4..c792c8315 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -2,6 +2,7 @@ globals: lang: es: Spanish en: English + quantity: Quantity language: Language entity: Entity user: User @@ -40,6 +41,8 @@ globals: noChanges: No changes to save changesToSave: You have changes pending to save confirmRemove: You are about to delete this row. Are you sure? + rowWillBeRemoved: This row will be removed + sureToContinue: Are you sure you want to continue? rowAdded: Row added rowRemoved: Row removed pleaseWait: Please wait... @@ -67,6 +70,7 @@ globals: allRows: 'All { numberRows } row(s)' markAll: Mark all requiredField: Required field + valueCantBeEmpty: Value cannot be empty class: clase type: Type reason: reason @@ -83,7 +87,7 @@ globals: description: Description id: Id order: Order - original: Original + original: Phys. Doc file: File selectFile: Select a file copyClipboard: Copy on clipboard @@ -93,6 +97,12 @@ globals: since: Since from: From to: To + notes: Notes + refresh: Refresh + item: Item + ticket: Ticket + campaign: Campaign + weight: Weight pageTitles: logIn: Login addressEdit: Update address @@ -120,9 +130,11 @@ globals: notifications: Notifications defaulter: Defaulter customerCreate: New customer + createOrder: New order fiscalData: Fiscal data billingData: Billing data consignees: Consignees + 'address-create': New address notes: Notes credits: Credits greuges: Greuges @@ -255,6 +267,11 @@ globals: twoFactor: Two factor recoverPassword: Recover password resetPassword: Reset password + ticketsMonitor: Tickets monitor + clientsActionsMonitor: Clients and actions + serial: Serial + medical: Mutual + supplier: Supplier created: Created worker: Worker now: Now @@ -267,6 +284,8 @@ globals: title: Unsaved changes will be lost subtitle: Are you sure exit without saving? createInvoiceIn: Create invoice in + myAccount: My account + noOne: No one errors: statusUnauthorized: Access denied statusInternalServerError: An internal server error has ocurred @@ -303,135 +322,6 @@ resetPassword: repeatPassword: Repeat password passwordNotMatch: Passwords don't match passwordChanged: Password changed -customer: - list: - phone: Phone - email: Email - customerOrders: Display customer orders - moreOptions: More options - card: - customerList: Customer list - customerId: Claim ID - salesPerson: Sales person - credit: Credit - risk: Risk - securedCredit: Secured credit - payMethod: Pay method - debt: Debt - isFrozen: Customer frozen - hasDebt: Customer has debt - isDisabled: Customer inactive - notChecked: Customer no checked - webAccountInactive: Web account inactive - noWebAccess: Web access is disabled - businessType: Business type - passwordRequirements: 'The password must have at least { length } length characters, {nAlpha} alphabetic characters, {nUpper} capital letters, {nDigits} digits and {nPunct} symbols (Ex: $%&.)\n' - businessTypeFk: Business type - summary: - basicData: Basic data - fiscalAddress: Fiscal address - fiscalData: Fiscal data - billingData: Billing data - consignee: Default consignee - businessData: Business data - financialData: Financial data - customerId: Customer ID - name: Name - contact: Contact - phone: Phone - mobile: Mobile - email: Email - salesPerson: Sales person - contactChannel: Contact channel - socialName: Social name - fiscalId: Fiscal ID - postcode: Postcode - province: Province - country: Country - street: Address - isEqualizated: Is equalizated - isActive: Is active - invoiceByAddress: Invoice by address - verifiedData: Verified data - hasToInvoice: Has to invoice - notifyByEmail: Notify by email - vies: VIES - payMethod: Pay method - bankAccount: Bank account - dueDay: Due day - hasLcr: Has LCR - hasCoreVnl: Has core VNL - hasB2BVnl: Has B2B VNL - addressName: Address name - addressCity: City - addressStreet: Street - username: Username - webAccess: Web access - totalGreuge: Total greuge - mana: Mana - priceIncreasingRate: Price increasing rate - averageInvoiced: Average invoiced - claimRate: Claming rate - risk: Risk - riskInfo: Invoices minus payments plus orders not yet invoiced - credit: Credit - creditInfo: Company's maximum risk - securedCredit: Secured credit - securedCreditInfo: Solunion's maximum risk - balance: Balance - balanceInfo: Invoices minus payments - balanceDue: Balance due - balanceDueInfo: Deviated invoices minus payments - recoverySince: Recovery since - businessType: Business Type - city: City - descriptorInfo: Invoices minus payments plus orders not yet - rating: Rating - recommendCredit: Recommended credit - basicData: - socialName: Fiscal name - businessType: Business type - contact: Contact - youCanSaveMultipleEmails: You can save multiple emails - email: Email - phone: Phone - mobile: Mobile - salesPerson: Sales person - contactChannel: Contact channel - previousClient: Previous client - extendedList: - tableVisibleColumns: - id: Identifier - name: Name - socialName: Social name - fi: Tax number - salesPersonFk: Salesperson - credit: Credit - creditInsurance: Credit insurance - phone: Phone - mobile: Mobile - street: Street - countryFk: Country - provinceFk: Province - city: City - postcode: Postcode - email: Email - created: Created - businessTypeFk: Business type - payMethodFk: Billing data - sageTaxTypeFk: Sage tax type - sageTransactionTypeFk: Sage tr. type - isActive: Active - isVies: Vies - isTaxDataChecked: Verified data - isEqualizated: Is equalizated - isFreezed: Freezed - hasToInvoice: Invoice - hasToInvoiceByAddress: Invoice by address - isToBeMailed: Mailing - hasLcr: Received LCR - hasCoreVnl: VNL core received - hasSepaVnl: VNL B2B received entry: list: newEntry: New entry @@ -453,6 +343,7 @@ entry: travelFk: Travel isExcludedFromAvailable: Inventory isRaid: Raid + invoiceAmount: Import summary: commission: Commission currency: Currency @@ -689,6 +580,7 @@ invoiceOut: chooseValidClient: Choose a valid client chooseValidCompany: Choose a valid company chooseValidPrinter: Choose a valid printer + chooseValidSerialType: Choose a serial type fillDates: Invoice date and the max date should be filled invoiceDateLessThanMaxDate: Invoice date can not be less than max date invoiceWithFutureDate: Exists an invoice with a future date @@ -743,56 +635,6 @@ parking: searchBar: info: You can search by parking code label: Search parking... -invoiceIn: - list: - ref: Reference - supplier: Supplier - supplierRef: Supplier ref. - serialNumber: Serial number - serial: Serial - file: File - issued: Issued - isBooked: Is booked - awb: AWB - amount: Amount - card: - issued: Issued - amount: Amount - client: Client - company: Company - customerCard: Customer card - ticketList: Ticket List - vat: Vat - dueDay: Due day - intrastat: Intrastat - summary: - supplier: Supplier - supplierRef: Supplier ref. - currency: Currency - docNumber: Doc number - issued: Expedition date - operated: Operation date - bookEntried: Entry date - bookedDate: Booked date - sage: Sage withholding - vat: Undeductible VAT - company: Company - booked: Booked - expense: Expense - taxableBase: Taxable base - rate: Rate - sageVat: Sage vat - sageTransaction: Sage transaction - dueDay: Date - bank: Bank - amount: Amount - foreignValue: Foreign value - dueTotal: Due day - noMatch: Do not match - code: Code - net: Net - stems: Stems - country: Country order: field: salesPersonFk: Sales Person @@ -869,6 +711,7 @@ worker: timeControl: Time control locker: Locker balance: Balance + medical: Medical list: name: Name email: Email @@ -880,6 +723,7 @@ worker: newWorker: New worker card: workerId: Worker ID + user: User name: Name email: Email phone: Phone @@ -948,6 +792,15 @@ worker: amount: Importe remark: Bonficado hasDiploma: Diploma + medical: + tableVisibleColumns: + date: Date + time: Hour + center: Formation Center + invoice: Invoice + amount: Amount + isFit: Fit + remark: Observations imageNotFound: Image not found balance: tableVisibleColumns: @@ -957,6 +810,16 @@ worker: credit: Have concept: Concept wagon: + pageTitles: + wagons: Wagons + wagonsList: Wagons List + wagonCreate: Create wagon + wagonEdit: Edit wagon + typesList: Types List + typeCreate: Create type + typeEdit: Edit type + wagonCounter: Trolley counter + wagonTray: Tray List type: name: Name submit: Submit @@ -1120,9 +983,12 @@ travel: agency: Agency shipped: Shipped landed: Landed + shipHour: Shipment Hour + landHour: Landing Hour warehouseIn: Warehouse in warehouseOut: Warehouse out totalEntries: Total entries + totalEntriesTooltip: Total entries summary: confirmed: Confirmed entryId: Entry Id @@ -1275,6 +1141,7 @@ components: active: Is active visible: Is visible floramondo: Is floramondo + showBadDates: Show future items userPanel: copyToken: Token copied to clipboard settings: Settings diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 4e71a69fb..91ece1b2c 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -3,6 +3,7 @@ globals: es: Español en: Inglés language: Idioma + quantity: Cantidad entity: Entidad user: Usuario details: Detalles @@ -39,6 +40,8 @@ globals: noChanges: Sin cambios que guardar changesToSave: Tienes cambios pendientes de guardar confirmRemove: Vas a eliminar este registro. ¿Continuar? + rowWillBeRemoved: Esta linea se eliminará + sureToContinue: ¿Seguro que quieres continuar? rowAdded: Fila añadida rowRemoved: Fila eliminada pleaseWait: Por favor espera... @@ -76,6 +79,9 @@ globals: warehouse: Almacén company: Empresa fieldRequired: Campo requerido + valueCantBeEmpty: El valor no puede estar vacío + Value can't be blank: El valor no puede estar en blanco + Value can't be null: El valor no puede ser nulo allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }' smsSent: SMS enviado confirmDeletion: Confirmar eliminación @@ -83,7 +89,7 @@ globals: description: Descripción id: Id order: Orden - original: Original + original: Doc. física file: Fichero selectFile: Seleccione un fichero copyClipboard: Copiar en portapapeles @@ -93,6 +99,12 @@ globals: since: Desde from: Desde to: Hasta + notes: Notas + refresh: Actualizar + item: Artículo + ticket: Ticket + campaign: Campaña + weight: Peso pageTitles: logIn: Inicio de sesión addressEdit: Modificar consignatario @@ -115,6 +127,7 @@ globals: inheritedRoles: Roles heredados customers: Clientes customerCreate: Nuevo cliente + createOrder: Nuevo pedido list: Listado webPayments: Pagos Web extendedList: Listado extendido @@ -124,6 +137,7 @@ globals: fiscalData: Datos fiscales billingData: Forma de pago consignees: Consignatarios + 'address-create': Nuevo consignatario notes: Notas credits: Créditos greuges: Greuges @@ -236,7 +250,7 @@ globals: purchaseRequest: Petición de compra weeklyTickets: Tickets programados formation: Formación - locations: Ubicaciones + locations: Localizaciones warehouses: Almacenes roles: Roles connections: Conexiones @@ -257,6 +271,11 @@ globals: twoFactor: Doble factor recoverPassword: Recuperar contraseña resetPassword: Restablecer contraseña + ticketsMonitor: Monitor de tickets + clientsActionsMonitor: Clientes y acciones + serial: Facturas por serie + medical: Mutua + supplier: Proveedor created: Fecha creación worker: Trabajador now: Ahora @@ -269,7 +288,8 @@ globals: title: Los cambios que no haya guardado se perderán subtitle: ¿Seguro que quiere salir sin guardar? createInvoiceIn: Crear factura recibida - + myAccount: Mi cuenta + noOne: Nadie errors: statusUnauthorized: Acceso denegado statusInternalServerError: Ha ocurrido un error interno del servidor @@ -304,134 +324,6 @@ resetPassword: repeatPassword: Repetir contraseña passwordNotMatch: Las contraseñas no coinciden passwordChanged: Contraseña cambiada -customer: - list: - phone: Teléfono - email: Email - customerOrders: Mostrar órdenes del cliente - moreOptions: Más opciones - card: - customerId: ID cliente - salesPerson: Comercial - credit: Crédito - risk: Riesgo - securedCredit: Crédito asegurado - payMethod: Método de pago - debt: Riesgo - isFrozen: Cliente congelado - hasDebt: Cliente con riesgo - isDisabled: Cliente inactivo - notChecked: Cliente no comprobado - webAccountInactive: Sin acceso web - noWebAccess: El acceso web está desactivado - businessType: Tipo de negocio - passwordRequirements: 'La contraseña debe tener al menos { length } caracteres de longitud, {nAlpha} caracteres alfabéticos, {nUpper} letras mayúsculas, {nDigits} dígitos y {nPunct} símbolos (Ej: $%&.)' - businessTypeFk: Tipo de negocio - summary: - basicData: Datos básicos - fiscalAddress: Dirección fiscal - fiscalData: Datos fiscales - billingData: Datos de facturación - consignee: Consignatario pred. - businessData: Datos comerciales - financialData: Datos financieros - customerId: ID cliente - name: Nombre - contact: Contacto - phone: Teléfono - mobile: Móvil - email: Email - salesPerson: Comercial - contactChannel: Canal de contacto - socialName: Razón social - fiscalId: NIF/CIF - postcode: Código postal - province: Provincia - country: País - street: Calle - isEqualizated: Recargo de equivalencia - isActive: Activo - invoiceByAddress: Facturar por consignatario - verifiedData: Datos verificados - hasToInvoice: Facturar - notifyByEmail: Notificar por email - vies: VIES - payMethod: Método de pago - bankAccount: Cuenta bancaria - dueDay: Día de pago - hasLcr: Recibido LCR - hasCoreVnl: Recibido core VNL - hasB2BVnl: Recibido B2B VNL - addressName: Nombre de la dirección - addressCity: Ciudad - addressStreet: Calle - username: Usuario - webAccess: Acceso web - totalGreuge: Greuge total - mana: Maná - priceIncreasingRate: Ratio de incremento de precio - averageInvoiced: Facturación media - claimRate: Ratio de reclamaciones - risk: Riesgo - riskInfo: Facturas menos recibos mas pedidos sin facturar - credit: Crédito - creditInfo: Riesgo máximo asumido por la empresa - securedCredit: Crédito asegurado - securedCreditInfo: Riesgo máximo asumido por Solunion - balance: Balance - balanceInfo: Facturas menos recibos - balanceDue: Saldo vencido - balanceDueInfo: Facturas fuera de plazo menos recibos - recoverySince: Recobro desde - businessType: Tipo de negocio - city: Población - descriptorInfo: Facturas menos recibos mas pedidos sin facturar - rating: Clasificación - recommendCredit: Crédito recomendado - basicData: - socialName: Nombre fiscal - businessType: Tipo de negocio - contact: Contacto - youCanSaveMultipleEmails: Puede guardar varios correos electrónicos encadenándolos mediante comas sin espacios{','} ejemplo{':'} user{'@'}dominio{'.'}com, user2{'@'}dominio{'.'}com siendo el primer correo electrónico el principal - email: Email - phone: Teléfono - mobile: Móvil - salesPerson: Comercial - contactChannel: Canal de contacto - previousClient: Cliente anterior - extendedList: - tableVisibleColumns: - id: Identificador - name: Nombre - socialName: Razón social - fi: NIF / CIF - salesPersonFk: Comercial - credit: Crédito - creditInsurance: Crédito asegurado - phone: Teléfono - mobile: Móvil - street: Dirección fiscal - countryFk: País - provinceFk: Provincia - city: Población - postcode: Código postal - email: Email - created: Fecha creación - businessTypeFk: Tipo de negocio - payMethodFk: Forma de pago - sageTaxTypeFk: Tipo de impuesto Sage - sageTransactionTypeFk: Tipo tr. sage - isActive: Activo - isVies: Vies - isTaxDataChecked: Datos comprobados - isEqualizated: Recargo de equivalencias - isFreezed: Congelado - hasToInvoice: Factura - hasToInvoiceByAddress: Factura por consigna - isToBeMailed: Env. emails - hasLcr: Recibido LCR - hasCoreVnl: Recibido core VNL - hasSepaVnl: Recibido B2B VNL entry: list: newEntry: Nueva entrada @@ -453,6 +345,7 @@ entry: travelFk: Envio isExcludedFromAvailable: Inventario isRaid: Redada + invoiceAmount: Importe summary: commission: Comisión currency: Moneda @@ -696,6 +589,7 @@ invoiceOut: chooseValidClient: Selecciona un cliente válido chooseValidCompany: Selecciona una empresa válida chooseValidPrinter: Selecciona una impresora válida + chooseValidSerialType: Selecciona una tipo de serie válida fillDates: La fecha de la factura y la fecha máxima deben estar completas invoiceDateLessThanMaxDate: La fecha de la factura no puede ser menor que la fecha máxima invoiceWithFutureDate: Existe una factura con una fecha futura @@ -788,54 +682,6 @@ parking: searchBar: info: Puedes buscar por código de parking label: Buscar parking... -invoiceIn: - list: - ref: Referencia - supplier: Proveedor - supplierRef: Ref. proveedor - serialNumber: Num. serie - shortIssued: F. emisión - serial: Serie - file: Fichero - issued: Fecha emisión - isBooked: Conciliada - awb: AWB - amount: Importe - card: - issued: Fecha emisión - amount: Importe - client: Cliente - company: Empresa - customerCard: Ficha del cliente - ticketList: Listado de tickets - vat: Iva - dueDay: Fecha de vencimiento - summary: - supplier: Proveedor - supplierRef: Ref. proveedor - currency: Divisa - docNumber: Número documento - issued: Fecha de expedición - operated: Fecha operación - bookEntried: Fecha asiento - bookedDate: Fecha contable - sage: Retención sage - vat: Iva no deducible - company: Empresa - booked: Contabilizada - expense: Gasto - taxableBase: Base imp. - rate: Tasa - sageTransaction: Sage transación - dueDay: Fecha - bank: Caja - amount: Importe - foreignValue: Divisa - dueTotal: Vencimiento - code: Código - net: Neto - stems: Tallos - country: País department: pageTitles: basicData: Basic data @@ -871,6 +717,8 @@ worker: timeControl: Control de horario locker: Taquilla balance: Balance + formation: Formación + medical: Mutua list: name: Nombre email: Email @@ -882,6 +730,7 @@ worker: newWorker: Nuevo trabajador card: workerId: ID Trabajador + user: Usuario name: Nombre email: Correo personal phone: Teléfono @@ -941,6 +790,15 @@ worker: amount: Importe remark: Bonficado hasDiploma: Diploma + medical: + tableVisibleColumns: + date: Fecha + time: Hora + center: Centro de Formación + invoice: Factura + amount: Importe + isFit: Apto + remark: Observaciones imageNotFound: No se ha encontrado la imagen balance: tableVisibleColumns: @@ -950,6 +808,16 @@ worker: credit: Haber concept: Concepto wagon: + pageTitles: + wagons: Vagones + wagonsList: Listado vagones + wagonCreate: Crear tipo + wagonEdit: Editar tipo + typesList: Listado tipos + typeCreate: Crear tipo + typeEdit: Editar tipo + wagonCounter: Contador de carros + wagonTray: Listado bandejas type: name: Nombre submit: Guardar @@ -1098,11 +966,14 @@ travel: id: Id ref: Referencia agency: Agencia - shipped: Enviado - landed: Llegada - warehouseIn: Almacén de salida - warehouseOut: Almacén de entrada - totalEntries: Total de entradas + shipped: F.envío + shipHour: Hora de envío + landHour: Hora de llegada + landed: F.entrega + warehouseIn: Alm.salida + warehouseOut: Alm.entrada + totalEntries: ∑ + totalEntriesTooltip: Entradas totales summary: confirmed: Confirmado entryId: Id entrada @@ -1253,6 +1124,7 @@ components: active: Activo visible: Visible floramondo: Floramondo + showBadDates: Ver items a futuro userPanel: copyToken: Token copiado al portapapeles settings: Configuración @@ -1267,6 +1139,7 @@ components: clone: Clonar openCard: Ficha openSummary: Detalles + viewSummary: Vista previa cardDescriptor: mainList: Listado principal summary: Resumen diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 88c5ee293..51cd20071 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -5,7 +5,7 @@ const quasar = useQuasar(); </script> <template> - <QLayout view="hHh LpR fFf"> + <QLayout view="hHh LpR fFf" v-shortcut> <Navbar /> <RouterView></RouterView> <QFooter v-if="quasar.platform.is.mobile"></QFooter> diff --git a/src/pages/Account/AccountAcls.vue b/src/pages/Account/AccountAcls.vue index fdb0ecee1..dd93a0cb5 100644 --- a/src/pages/Account/AccountAcls.vue +++ b/src/pages/Account/AccountAcls.vue @@ -6,7 +6,6 @@ import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; import { useQuasar } from 'quasar'; -import FetchData from 'components/FetchData.vue'; import VnTable from 'components/VnTable/VnTable.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnConfirm from 'components/ui/VnConfirm.vue'; @@ -24,7 +23,6 @@ const stateStore = useStateStore(); const quasar = useQuasar(); const tableRef = ref(); -const rolesOptions = ref([]); const exprBuilder = (param, value) => { switch (param) { @@ -143,6 +141,7 @@ const deleteAcl = async ({ id }) => { formInitialData: {}, }" order="id DESC" + :disable-option="{ card: true }" :columns="columns" default-mode="table" :right-search="true" diff --git a/src/pages/Account/AccountAliasList.vue b/src/pages/Account/AccountAliasList.vue index b6f7b219c..c67283297 100644 --- a/src/pages/Account/AccountAliasList.vue +++ b/src/pages/Account/AccountAliasList.vue @@ -21,24 +21,21 @@ const columns = computed(() => [ { align: 'left', name: 'id', - label: t('id'), + label: t('Id'), isId: true, - field: 'id', cardVisible: true, }, { align: 'left', name: 'alias', - label: t('alias'), - field: 'alias', + label: t('Alias'), cardVisible: true, create: true, }, { align: 'left', name: 'description', - label: t('description'), - field: 'description', + label: t('Description'), cardVisible: true, create: true, }, @@ -69,9 +66,17 @@ const columns = computed(() => [ }" order="id DESC" :columns="columns" + :disable-option="{ card: true }" default-mode="table" redirect="account/alias" :is-editable="true" :use-model="true" /> </template> + +<i18n> + es: + Id: Id + Alias: Alias + Description: Descripción +</i18n> diff --git a/src/pages/Account/AccountConnections.vue b/src/pages/Account/AccountConnections.vue index 4d3450665..057745831 100644 --- a/src/pages/Account/AccountConnections.vue +++ b/src/pages/Account/AccountConnections.vue @@ -27,15 +27,15 @@ const filter = { order: 'created DESC', }; -const urlPath = 'AccessTokens'; +const urlPath = 'VnTokens'; const refresh = () => paginateRef.value.fetch(); const navigate = (id) => router.push({ name: 'AccountSummary', params: { id } }); -const killSession = async (id) => { +const killSession = async ({ userId, created }) => { try { - await axios.delete(`${urlPath}/${id}`); + await axios.post(`${urlPath}/killSession`, { userId, created }); paginateRef.value.fetch(); notify(t('Session killed'), 'positive'); } catch (error) { @@ -84,7 +84,7 @@ const killSession = async (id) => { openConfirmationModal( t('Session will be killed'), t('Are you sure you want to continue?'), - () => killSession(row.id) + () => killSession(row) ) " outline diff --git a/src/pages/Account/AccountList.vue b/src/pages/Account/AccountList.vue index cdd88551b..d698596b9 100644 --- a/src/pages/Account/AccountList.vue +++ b/src/pages/Account/AccountList.vue @@ -14,15 +14,23 @@ const columns = computed(() => [ { align: 'left', name: 'id', - label: t('id'), + label: t('Id'), isId: true, field: 'id', cardVisible: true, + columnFilter: { + component: 'select', + name: 'search', + attrs: { + url: 'VnUsers/preview', + fields: ['id', 'name'], + }, + }, }, { align: 'left', name: 'username', - label: t('nickname'), + label: t('Nickname'), isTitle: true, component: 'input', columnField: { @@ -37,7 +45,7 @@ const columns = computed(() => [ { align: 'left', name: 'name', - label: t('name'), + label: t('Name'), component: 'input', columnField: { component: null, @@ -65,6 +73,7 @@ const columns = computed(() => [ title: t('View Summary'), icon: 'preview', action: (row) => viewSummary(row.id, AccountSummary), + isPrimary: true, }, ], }, @@ -108,3 +117,10 @@ const exprBuilder = (param, value) => { :use-model="true" /> </template> + +<i18n> + es: + Id: Id + Nickname: Nickname + Name: Nombre +</i18n> diff --git a/src/pages/Account/Card/AccountCard.vue b/src/pages/Account/Card/AccountCard.vue index a9857b283..67fa15898 100644 --- a/src/pages/Account/Card/AccountCard.vue +++ b/src/pages/Account/Card/AccountCard.vue @@ -15,7 +15,6 @@ const { t } = useI18n(); url: 'VnUsers/preview', label: t('account.search'), info: t('account.searchInfo'), - searchUrl: 'table', }" /> </template> diff --git a/src/pages/Account/Card/AccountDescriptor.vue b/src/pages/Account/Card/AccountDescriptor.vue index bc0c8c713..4571ee8ab 100644 --- a/src/pages/Account/Card/AccountDescriptor.vue +++ b/src/pages/Account/Card/AccountDescriptor.vue @@ -72,7 +72,7 @@ const hasAccount = ref(false); </VnImg> </template> <template #body="{ entity }"> - <VnLv :label="t('account.card.nickname')" :value="entity.nickname" /> + <VnLv :label="t('account.card.nickname')" :value="entity.name" /> <VnLv :label="t('account.card.role')" :value="entity.role.name" /> </template> <template #actions="{ entity }"> diff --git a/src/pages/Account/Card/AccountDescriptorMenu.vue b/src/pages/Account/Card/AccountDescriptorMenu.vue index f67cc0c6b..0e35d25f3 100644 --- a/src/pages/Account/Card/AccountDescriptorMenu.vue +++ b/src/pages/Account/Card/AccountDescriptorMenu.vue @@ -1,15 +1,12 @@ <script setup> import axios from 'axios'; import { computed, ref, toRefs } from 'vue'; -import { useQuasar } from 'quasar'; import { useI18n } from 'vue-i18n'; import { useVnConfirm } from 'composables/useVnConfirm'; import { useRoute } from 'vue-router'; import { useArrayData } from 'src/composables/useArrayData'; -import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; import useNotify from 'src/composables/useNotify.js'; -const quasar = useQuasar(); const $props = defineProps({ hasAccount: { type: Boolean, @@ -35,7 +32,7 @@ async function updateStatusAccount(active) { account.value.hasAccount = active; const status = active ? 'enable' : 'disable'; - quasar.notify({ + notify({ message: t(`account.card.${status}Account.success`), type: 'positive', }); @@ -44,19 +41,11 @@ async function updateStatusUser(active) { await axios.patch(`VnUsers/${entityId.value}`, { active }); account.value.active = active; const status = active ? 'activate' : 'deactivate'; - quasar.notify({ + notify({ message: t(`account.card.actions.${status}User.success`), type: 'positive', }); } -function setPassword() { - quasar.dialog({ - component: CustomerChangePassword, - componentProps: { - id: entityId.value, - }, - }); -} const showSyncDialog = ref(false); const syncPassword = ref(null); const shouldSyncPassword = ref(false); @@ -66,20 +55,11 @@ async function sync() { await axios.patch(`Accounts/${account.value.name}/sync`, { params, }); - quasar.notify({ + notify({ message: t('account.card.actions.sync.success'), type: 'positive', }); } - -const removeAccount = async () => { - try { - await axios.delete(`VnUsers/${account.value.id}`); - notify(t('Account removed'), 'positive'); - } catch (error) { - console.error('Error deleting user', error); - } -}; </script> <template> <VnConfirm @@ -112,24 +92,6 @@ const removeAccount = async () => { /> </template> </VnConfirm> - <!-- <QItem v-ripple clickable @click="setPassword"> - <QItemSection>{{ t('account.card.actions.setPassword') }}</QItemSection> - </QItem> - <QItem - v-if="!account.hasAccount" - v-ripple - clickable - @click=" - openConfirmationModal( - t('account.card.actions.enableAccount.title'), - t('account.card.actions.enableAccount.subtitle'), - () => updateStatusAccount(true) - ) - " - > - <QItemSection>{{ t('account.card.actions.enableAccount.name') }}</QItemSection> - </QItem> --> - <QItem v-if="account.hasAccount" v-ripple @@ -178,10 +140,4 @@ const removeAccount = async () => { </QItem> <QSeparator /> - <!-- <QItem @click="removeAccount(id)" v-ripple clickable> - <QItemSection avatar> - <QIcon name="delete" /> - </QItemSection> - <QItemSection>{{ t('account.card.actions.delete.name') }}</QItemSection> - </QItem> --> </template> diff --git a/src/pages/Account/Card/AccountMailAlias.vue b/src/pages/Account/Card/AccountMailAlias.vue index 594353219..15d03c665 100644 --- a/src/pages/Account/Card/AccountMailAlias.vue +++ b/src/pages/Account/Card/AccountMailAlias.vue @@ -169,7 +169,13 @@ onMounted(async () => await getAccountData(false)); <AccountMailAliasCreateForm @on-submit-create-alias="createMailAlias" /> </QDialog> <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn fab icon="add" color="primary" @click="openCreateMailAliasForm()"> + <QBtn + fab + icon="add" + color="primary" + @click="openCreateMailAliasForm()" + shortcut="+" + > <QTooltip>{{ t('warehouses.add') }}</QTooltip> </QBtn> </QPageSticky> diff --git a/src/pages/Account/Card/AccountPrivileges.vue b/src/pages/Account/Card/AccountPrivileges.vue index 1300f5018..fea57105b 100644 --- a/src/pages/Account/Card/AccountPrivileges.vue +++ b/src/pages/Account/Card/AccountPrivileges.vue @@ -1,5 +1,5 @@ <script setup> -import { ref } from 'vue'; +import { ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; @@ -12,16 +12,21 @@ const route = useRoute(); const rolesOptions = ref([]); const formModelRef = ref(); +watch( + () => route.params.id, + () => formModelRef.value.reset() +); </script> <template> <FetchData url="VnRoles" auto-load @on-fetch="(data) => (rolesOptions = data)" /> <FormModel ref="formModelRef" model="AccountPrivileges" - :url="`VnUsers/${route.params.id}/privileges`" + url="VnUsers/preview" + :filter="{ where: { id: route.params.id } }" :url-create="`VnUsers/${route.params.id}/privileges`" + :id="route.params.id" auto-load - @on-data-saved="formModelRef.fetch()" > <template #form="{ data }"> <div class="q-gutter-y-sm"> diff --git a/src/pages/Account/Card/AccountSummary.vue b/src/pages/Account/Card/AccountSummary.vue index 1901f9cde..5a21e18a5 100644 --- a/src/pages/Account/Card/AccountSummary.vue +++ b/src/pages/Account/Card/AccountSummary.vue @@ -48,7 +48,7 @@ const filter = { <QIcon name="open_in_new" /> </router-link> </QCardSection> - <VnLv :label="t('account.card.nickname')" :value="account.nickname" /> + <VnLv :label="t('account.card.nickname')" :value="account.name" /> <VnLv :label="t('account.card.role')" :value="account.role.name" /> </QCard> </template> diff --git a/src/pages/Account/Role/AccountRoles.vue b/src/pages/Account/Role/AccountRoles.vue index 2f80606b4..ea175d913 100644 --- a/src/pages/Account/Role/AccountRoles.vue +++ b/src/pages/Account/Role/AccountRoles.vue @@ -4,11 +4,9 @@ import { computed, ref } from 'vue'; import VnTable from 'components/VnTable/VnTable.vue'; import { useRoute } from 'vue-router'; import VnSearchbar from 'components/ui/VnSearchbar.vue'; -import { useStateStore } from 'stores/useStateStore'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import RoleSummary from './Card/RoleSummary.vue'; const route = useRoute(); -const stateStore = useStateStore(); const { t } = useI18n(); const $props = defineProps({ id: { @@ -23,24 +21,30 @@ const columns = computed(() => [ { align: 'left', name: 'id', - label: t('id'), + label: t('Id'), isId: true, columnFilter: { inWhere: true, + component: 'select', + name: 'search', + attrs: { + url: 'VnRoles', + fields: ['id', 'name'], + }, }, cardVisible: true, }, { align: 'left', name: 'name', - label: t('name'), + label: t('Name'), cardVisible: true, create: true, }, { align: 'left', name: 'description', - label: t('description'), + label: t('Description'), cardVisible: true, create: true, }, @@ -53,6 +57,7 @@ const columns = computed(() => [ title: t('View Summary'), icon: 'preview', action: (row) => viewSummary(row.id, RoleSummary), + isPrimary: true, }, ], }, @@ -95,8 +100,16 @@ const exprBuilder = (param, value) => { }, }" order="id ASC" + :disable-option="{ card: true }" :columns="columns" default-mode="table" redirect="account/role" /> </template> + +<i18n> + es: + Id: Id + Description: Descripción + Name: Nombre +</i18n> diff --git a/src/pages/Claim/Card/ClaimDevelopment.vue b/src/pages/Claim/Card/ClaimDevelopment.vue index f7e8131c6..61d9f5858 100644 --- a/src/pages/Claim/Card/ClaimDevelopment.vue +++ b/src/pages/Claim/Card/ClaimDevelopment.vue @@ -16,7 +16,6 @@ const claimReasons = ref([]); const claimResults = ref([]); const claimResponsibles = ref([]); const claimRedeliveries = ref([]); -const workers = ref([]); const selected = ref([]); const saveButtonRef = ref(); @@ -82,7 +81,9 @@ const columns = computed(() => [ label: t('Worker'), field: (row) => row.workerFk, sortable: true, - options: workers.value, + url: 'Workers/search', + where: { active: 1 }, + sortBy: 'name ASC', model: 'workerFk', optionValue: 'id', optionLabel: 'nickname', @@ -129,13 +130,6 @@ const columns = computed(() => [ @on-fetch="(data) => (claimRedeliveries = data)" auto-load /> - <FetchData - url="Workers/search" - :where="{ active: 1 }" - order="name ASC" - @on-fetch="(data) => (workers = data)" - auto-load - /> <CrudModel data-key="ClaimDevelopments" url="ClaimDevelopments" @@ -165,6 +159,9 @@ const columns = computed(() => [ > <VnSelect v-model="row[col.model]" + :url="col.url" + :where="col.where" + :sort-by="col.sortBy" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" diff --git a/src/pages/Claim/Card/ClaimLines.vue b/src/pages/Claim/Card/ClaimLines.vue index 52fbfca98..77c7dacf1 100644 --- a/src/pages/Claim/Card/ClaimLines.vue +++ b/src/pages/Claim/Card/ClaimLines.vue @@ -45,7 +45,7 @@ async function onFetchClaim(data) { const amount = ref(); const amountClaimed = ref(); -async function onFetch(rows, newRows) { +function onFetch(rows, newRows) { if (newRows) rows = newRows; amount.value = 0; amountClaimed.value = 0; @@ -155,7 +155,7 @@ function showImportDialog() { async function saveWhenHasChanges() { if (claimLinesForm.value.getChanges().updates) { await claimLinesForm.value.onSubmit(); - await claimLinesForm.value.reload(); + onFetch(claimLinesForm.value.formData); } } </script> @@ -211,7 +211,7 @@ async function saveWhenHasChanges() { <template #body-cell-claimed="{ row }"> <QTd auto-width align="right" class="text-primary"> <QInput - v-model="row.quantity" + v-model.number="row.quantity" type="number" dense @keyup.enter="saveWhenHasChanges()" @@ -266,7 +266,9 @@ async function saveWhenHasChanges() { <template v-if="column.name === 'claimed'"> <QItemLabel class="text-primary"> <QInput - v-model="props.row.quantity" + v-model.number=" + props.row.quantity + " type="number" dense autofocus diff --git a/src/pages/Claim/Card/ClaimSummary.vue b/src/pages/Claim/Card/ClaimSummary.vue index 4d817c3c2..d77f718c6 100644 --- a/src/pages/Claim/Card/ClaimSummary.vue +++ b/src/pages/Claim/Card/ClaimSummary.vue @@ -1,11 +1,10 @@ <script setup> import axios from 'axios'; -import { onMounted, ref, computed } from 'vue'; +import { ref, computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { toDate, toCurrency } from 'src/filters'; import dashIfEmpty from 'src/filters/dashIfEmpty'; -import { getUrl } from 'src/composables/getUrl'; import { useSession } from 'src/composables/useSession'; import VnLv from 'src/components/ui/VnLv.vue'; @@ -36,8 +35,6 @@ const $props = defineProps({ const entityId = computed(() => $props.id || route.params.id); const ClaimStates = ref([]); -const claimUrl = ref(); -const salixUrl = ref(); const claimDmsRef = ref(); const claimDms = ref([]); const multimediaDialog = ref(); @@ -97,8 +94,8 @@ const detailsColumns = ref([ { name: 'total', label: 'claim.total', - field: ({ sale }) => - toCurrency(sale.quantity * sale.price * ((100 - sale.discount) / 100)), + field: (row) => + toCurrency(row.quantity * row.sale.price * ((100 - row.sale.discount) / 100)), sortable: true, }, ]); @@ -152,11 +149,6 @@ const developmentColumns = ref([ }, ]); -onMounted(async () => { - salixUrl.value = await getUrl(''); - claimUrl.value = salixUrl.value + `claim/${entityId.value}/`; -}); - async function getClaimDms() { claimDmsFilter.value.where = { claimFk: entityId.value }; await claimDmsRef.value.fetch(); @@ -177,10 +169,15 @@ function openDialog(dmsId) { multimediaSlide.value = dmsId; multimediaDialog.value = true; } + async function changeState(value) { await axios.patch(`Claims/updateClaim/${entityId.value}`, { claimStateFk: value }); router.go(route.fullPath); } + +function claimUrl(section) { + return '#/claim/' + entityId.value + '/' + section; +} </script> <template> @@ -234,7 +231,7 @@ async function changeState(value) { <template #body="{ entity: { claim, salesClaimed, developments } }"> <QCard class="vn-one" v-if="$route.name != 'ClaimSummary'"> <VnTitle - :url="`#/claim/${entityId}/basic-data`" + :url="claimUrl('basic-data')" :text="t('globals.pageTitles.basicData')" /> <VnLv :label="t('claim.created')" :value="toDate(claim.created)" /> @@ -275,7 +272,7 @@ async function changeState(value) { /> </QCard> <QCard class="vn-two"> - <VnTitle :url="`#/claim/${entityId}/notes`" :text="t('claim.notes')" /> + <VnTitle :url="claimUrl('notes')" :text="t('claim.notes')" /> <ClaimNotes :id="entityId" :add-note="false" @@ -284,7 +281,7 @@ async function changeState(value) { /> </QCard> <QCard class="vn-two" v-if="claimDms?.length"> - <VnTitle :url="`#/claim/${entityId}/photos`" :text="t('claim.photos')" /> + <VnTitle :url="claimUrl('photos')" :text="t('claim.photos')" /> <div class="container max-container-height" style="overflow: auto"> <div class="multimedia-container" @@ -326,7 +323,7 @@ async function changeState(value) { </div> </QCard> <QCard class="vn-max" v-if="salesClaimed.length > 0"> - <VnTitle :url="`#/claim/${entityId}/lines`" :text="t('claim.details')" /> + <VnTitle :url="claimUrl('lines')" :text="t('claim.details')" /> <QTable :columns="detailsColumns" :rows="salesClaimed" @@ -365,7 +362,7 @@ async function changeState(value) { </QTable> </QCard> <QCard class="vn-max" v-if="developments.length > 0"> - <VnTitle :url="claimUrl + 'development'" :text="t('claim.development')" /> + <VnTitle :url="claimUrl('development')" :text="t('claim.development')" /> <QTable :columns="developmentColumns" :rows="developments" @@ -390,7 +387,7 @@ async function changeState(value) { </QTable> </QCard> <QCard class="vn-max"> - <VnTitle :url="claimUrl + 'action'" :text="t('claim.actions')" /> + <VnTitle :url="claimUrl('action')" :text="t('claim.actions')" /> <div id="slider-container" class="q-px-xl q-py-md"> <QSlider v-model="claim.responsibility" diff --git a/src/pages/Claim/ClaimList.vue b/src/pages/Claim/ClaimList.vue index 6fd607da0..6d85817dc 100644 --- a/src/pages/Claim/ClaimList.vue +++ b/src/pages/Claim/ClaimList.vue @@ -50,8 +50,9 @@ const columns = computed(() => [ align: 'left', label: t('claim.attendedBy'), name: 'attendedBy', - cardVisible: true, + orderBy: 'workerFk', columnFilter: { + name: 'attenderFk', component: 'select', attrs: { url: 'Workers/activeWithInheritedRole', @@ -63,6 +64,7 @@ const columns = computed(() => [ optionFilter: 'firstName', }, }, + cardVisible: true, }, { align: 'left', @@ -77,6 +79,9 @@ const columns = computed(() => [ { align: 'left', label: t('claim.state'), + format: ({ stateCode }) => + claimFilterRef.value?.states.find(({ code }) => code === stateCode) + ?.description, name: 'stateCode', chip: { condition: () => true, @@ -96,7 +101,7 @@ const columns = computed(() => [ name: 'tableActions', actions: [ { - title: t('Client ticket list'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row.id, ClaimSummary), }, diff --git a/src/pages/Claim/locale/es.yml b/src/pages/Claim/locale/es.yml index 90bef8e66..052416aa7 100644 --- a/src/pages/Claim/locale/es.yml +++ b/src/pages/Claim/locale/es.yml @@ -42,7 +42,7 @@ claim: pickup: Recoger null: No agency: Agencia - delivery: Entrega + delivery: Reparto fileDescription: 'ID de reclamación {claimId} del cliente {clientName} con ID {clientId}' noData: 'No hay imágenes/videos, haz clic aquí o arrastra y suelta el archivo' dragDrop: Arrastra y suelta aquí diff --git a/src/pages/Customer/Card/CustomerAddress.vue b/src/pages/Customer/Card/CustomerAddress.vue index a97b45a50..166c33e1a 100644 --- a/src/pages/Customer/Card/CustomerAddress.vue +++ b/src/pages/Customer/Card/CustomerAddress.vue @@ -169,6 +169,13 @@ const toCustomerAddressEdit = (addressId) => { {{ item.postalCode }} - {{ item.city }}, {{ item.province.name }} </div> + <div> + {{ item.phone }} + <span v-if="item.mobile" + >, + {{ item.mobile }} + </span> + </div> <div class="flex"> <QCheckbox :label="t('Is equalizated')" @@ -208,7 +215,13 @@ const toCustomerAddressEdit = (addressId) => { </div> <QPageSticky :offset="[18, 18]"> - <QBtn @click.stop="toCustomerAddressCreate()" color="primary" fab icon="add" /> + <QBtn + @click.stop="toCustomerAddressCreate()" + color="primary" + fab + icon="add" + shortcut="+" + /> <QTooltip> {{ t('New consignee') }} </QTooltip> diff --git a/src/pages/Customer/Card/CustomerBalance.vue b/src/pages/Customer/Card/CustomerBalance.vue index 346e76681..2a1991bbd 100644 --- a/src/pages/Customer/Card/CustomerBalance.vue +++ b/src/pages/Customer/Card/CustomerBalance.vue @@ -2,15 +2,15 @@ import { computed, onBeforeMount, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import { useRole } from 'src/composables/useRole'; +import { useAcl } from 'src/composables/useAcl'; import axios from 'axios'; import { useQuasar } from 'quasar'; +import FetchData from 'components/FetchData.vue'; import { toCurrency, toDate, toDateHourMin } from 'src/filters'; import { useState } from 'composables/useState'; import { useStateStore } from 'stores/useStateStore'; import { usePrintService } from 'composables/usePrintService'; -import { useSession } from 'composables/useSession'; import { useVnConfirm } from 'composables/useVnConfirm'; import VnTable from 'components/VnTable/VnTable.vue'; @@ -22,12 +22,10 @@ import CustomerNewPayment from 'src/pages/Customer/components/CustomerNewPayment import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; const { openConfirmationModal } = useVnConfirm(); -const { sendEmail } = usePrintService(); +const { sendEmail, openReport } = usePrintService(); const { t } = useI18n(); -const { hasAny } = useRole(); - -const session = useSession(); -const tokenMultimedia = session.getTokenMultimedia(); +const { hasAny } = useAcl(); +const currentBalance = ref({}); const quasar = useQuasar(); const route = useRoute(); const state = useState(); @@ -36,7 +34,7 @@ const user = state.getUser(); const clientRisk = ref([]); const tableRef = ref(); -const companyId = ref(); +const companyId = ref(user.value.companyFk); const companyLastId = ref(user.value.companyFk); const balances = ref([]); const vnFilterRef = ref({}); @@ -76,14 +74,14 @@ const companyFilterColumn = { const columns = computed(() => [ { - align: 'left', + align: 'right', name: 'payed', label: t('Date'), format: ({ payed }) => toDate(payed), cardVisible: true, }, { - align: 'left', + align: 'right', name: 'created', label: t('Creation date'), format: ({ created }) => toDateHourMin(created), @@ -91,16 +89,10 @@ const columns = computed(() => [ }, { align: 'left', - name: 'workerFk', label: t('Employee'), columnField: { component: 'userLink', - attrs: ({ row }) => { - return { - workerId: row.workerFk, - name: row.userName, - }; - }, + attrs: ({ row }) => ({ workerId: row.workerFk, name: row.userName }), }, cardVisible: true, }, @@ -125,14 +117,14 @@ const columns = computed(() => [ isId: true, }, { - align: 'right', + align: 'left', name: 'credit', label: t('Havings'), format: ({ credit }) => credit && toCurrency(credit), cardVisible: true, }, { - align: 'right', + align: 'left', name: 'balance', label: t('Balance'), format: ({ balance }) => toCurrency(balance), @@ -171,41 +163,15 @@ const columns = computed(() => [ onBeforeMount(() => { stateStore.rightDrawer = true; - companyId.value = user.value.companyFk; }); -async function getClientRisk() { - const { data } = await axios.get(`clientRisks`, { - params: { - filter: JSON.stringify({ - include: { relation: 'company', scope: { fields: ['code'] } }, - where: { clientFk: route.params.id, companyFk: user.value.companyFk }, - }), - }, - }); - clientRisk.value = data; - return clientRisk.value; -} - -async function getCurrentBalance() { - const currentBalance = (await getClientRisk()).find((balance) => { - return balance.companyFk === companyId.value; - }); - return currentBalance && currentBalance.amount; -} - -async function onFetch(data) { - balances.value = []; - for (const [index, balance] of data.entries()) { - if (index === 0) { - balance.balance = await getCurrentBalance(); - continue; - } - const previousBalance = data[index - 1]; - balance.balance = - previousBalance?.balance - (previousBalance?.debit - previousBalance?.credit); +async function getCurrentBalance(data) { + for (const balance of data) { + currentBalance.value[balance.companyFk] = { + code: balance.company.code, + amount: balance.amount, + }; } - balances.value = data; } const showNewPaymentDialog = () => { @@ -220,19 +186,27 @@ const showNewPaymentDialog = () => { }; const showBalancePdf = ({ id }) => { - const url = `api/InvoiceOuts/${id}/download?access_token=${tokenMultimedia}`; - window.open(url, '_blank'); + openReport(`InvoiceOuts/${id}/download`, {}, '_blank'); }; </script> <template> + <FetchData + url="clientRisks" + :filter="{ + include: { relation: 'company', scope: { fields: ['code'] } }, + where: { clientFk: route.params.id, companyFk: companyId }, + }" + auto-load + @on-fetch="getCurrentBalance" + ></FetchData> <VnSubToolbar class="q-mb-md"> <template #st-data> <div class="column justify-center q-px-md q-py-sm"> <span class="text-bold">{{ t('Total by company') }}</span> - <div class="row justify-center" v-if="clientRisk?.length"> - {{ clientRisk[0].company.code }}: - {{ toCurrency(clientRisk[0].amount) }} + <div class="row justify-center"> + {{ currentBalance[companyId]?.code }}: + {{ toCurrency(currentBalance[companyId]?.amount) }} </div> </div> </template> @@ -258,7 +232,7 @@ const showBalancePdf = ({ id }) => { :right-search="false" :is-editable="false" :column-search="false" - @on-fetch="onFetch" + :disable-option="{ card: true }" auto-load > <template #column-balance="{ rowIndex }"> @@ -266,7 +240,7 @@ const showBalancePdf = ({ id }) => { </template> <template #column-description="{ row }"> <div class="link" v-if="row.isInvoice"> - {{ row.description }} + {{ t('bill', { ref: row.description }) }} <InvoiceOutDescriptorProxy :id="row.description" /> </div> <span v-else class="q-pa-xs dotted rounded-borders" :title="row.description"> @@ -284,7 +258,9 @@ const showBalancePdf = ({ id }) => { > <VnInput v-model="scope.value" - :disable="!hasAny(['administrative'])" + :disable=" + !hasAny([{ model: 'Receipt', props: '*', accessType: 'WRITE' }]) + " @keypress.enter="scope.set" autofocus /> @@ -292,7 +268,13 @@ const showBalancePdf = ({ id }) => { </template> </VnTable> <QPageSticky :offset="[18, 18]" style="z-index: 2"> - <QBtn @click.stop="showNewPaymentDialog()" color="primary" fab icon="add" /> + <QBtn + @click.stop="showNewPaymentDialog()" + color="primary" + fab + icon="add" + shortcut="+" + /> <QTooltip> {{ t('New payment') }} </QTooltip> diff --git a/src/pages/Customer/Card/CustomerBasicData.vue b/src/pages/Customer/Card/CustomerBasicData.vue index 87a3b08f7..91d9edc05 100644 --- a/src/pages/Customer/Card/CustomerBasicData.vue +++ b/src/pages/Customer/Card/CustomerBasicData.vue @@ -25,6 +25,7 @@ const title = ref(); /> <FetchData url="BusinessTypes" + :filter="{ fields: ['code', 'description'], order: 'description ASC ' }" @on-fetch="(data) => (businessTypes = data)" auto-load /> @@ -38,7 +39,7 @@ const title = ref(); clearable v-model="data.name" /> - <QSelect + <VnSelect :input-debounce="0" :label="t('customer.basicData.businessType')" :options="businessTypes" @@ -89,15 +90,18 @@ const title = ref(); </VnRow> <VnRow> <VnSelect - url="Workers/activeWithInheritedRole" - :filter="{ where: { role: 'salesPerson' } }" - option-filter="firstName" + url="Workers/search" v-model="data.salesPersonFk" :label="t('customer.basicData.salesPerson')" + :params="{ + departmentCodes: ['VT', 'shopping'], + }" + :fields="['id', 'nickname']" + sort-by="nickname ASC" :rules="validate('client.salesPersonFk')" :use-like="false" - :emit-value="false" - @update:model-value="(val) => (title = val?.nickname)" + emit-value + auto-load > <template #prepend> <VnAvatar @@ -106,8 +110,19 @@ const title = ref(); :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> - <QSelect + <VnSelect v-model="data.contactChannelFk" :options="contactChannels" option-value="id" @@ -120,7 +135,8 @@ const title = ref(); /> </VnRow> <VnRow> - <QSelect + <VnSelect + url="Clients" :input-debounce="0" :label="t('customer.basicData.previousClient')" :options="clients" @@ -129,7 +145,9 @@ const title = ref(); map-options option-label="name" option-value="id" + sort-by="name ASC" v-model="data.transferorFk" + :fields="['id', 'name']" > <template #append> <QIcon name="info" class="cursor-pointer"> @@ -140,7 +158,7 @@ const title = ref(); }}</QTooltip> </QIcon> </template> - </QSelect> + </VnSelect> </VnRow> </template> </FormModel> diff --git a/src/pages/Customer/Card/CustomerBillingData.vue b/src/pages/Customer/Card/CustomerBillingData.vue index b01dc4523..a968d0ec8 100644 --- a/src/pages/Customer/Card/CustomerBillingData.vue +++ b/src/pages/Customer/Card/CustomerBillingData.vue @@ -3,7 +3,6 @@ import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; @@ -14,8 +13,6 @@ import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue'; const { t } = useI18n(); const route = useRoute(); -const payMethods = ref([]); -const bankEntitiesOptions = ref([]); const bankEntitiesRef = ref(null); const filter = { @@ -31,15 +28,6 @@ const getBankEntities = (data, formData) => { </script> <template> - <fetch-data @on-fetch="(data) => (payMethods = data)" auto-load url="PayMethods" /> - <fetch-data - ref="bankEntitiesRef" - @on-fetch="(data) => (bankEntitiesOptions = data)" - :filter="filter" - auto-load - url="BankEntities" - /> - <FormModel :url-update="`Clients/${route.params.id}`" :url="`Clients/${route.params.id}/getCard`" @@ -49,8 +37,9 @@ const getBankEntities = (data, formData) => { <template #form="{ data, validate }"> <VnRow> <VnSelect + auto-load + url="PayMethods" :label="t('Billing data')" - :options="payMethods" hide-selected option-label="name" option-value="id" @@ -69,8 +58,11 @@ const getBankEntities = (data, formData) => { </VnInput> <VnSelectDialog :label="t('Swift / BIC')" - :options="bankEntitiesOptions" - :roles-allowed-to-create="['salesAssistant', 'hr']" + ref="bankEntitiesRef" + :filter="filter" + auto-load + url="BankEntities" + :acls="[{ model: 'BankEntity', props: '*', accessType: 'WRITE' }]" :rules="validate('Worker.bankEntity')" hide-selected option-label="name" @@ -85,9 +77,8 @@ const getBankEntities = (data, formData) => { <template #option="scope"> <QItem v-bind="scope.itemProps"> <QItemSection v-if="scope.opt"> - <QItemLabel - >{{ scope.opt.bic }} {{ scope.opt.name }}</QItemLabel - > + <QItemLabel>{{ scope.opt.bic }} </QItemLabel> + <QItemLabel caption> {{ scope.opt.name }}</QItemLabel> </QItemSection> </QItem> </template> diff --git a/src/pages/Customer/Card/CustomerCard.vue b/src/pages/Customer/Card/CustomerCard.vue index 229946ea2..139917d05 100644 --- a/src/pages/Customer/Card/CustomerCard.vue +++ b/src/pages/Customer/Card/CustomerCard.vue @@ -1,17 +1,23 @@ <script setup> +import { computed } from 'vue'; +import { useRoute } from 'vue-router'; + import VnCard from 'components/common/VnCard.vue'; import CustomerDescriptor from './CustomerDescriptor.vue'; import CustomerFilter from '../CustomerFilter.vue'; +const route = useRoute(); + +const routeName = computed(() => route.name); </script> <template> <VnCard data-key="Client" base-url="Clients" :descriptor="CustomerDescriptor" - :filter-panel="CustomerFilter" + :filter-panel="routeName != 'CustomerConsumption' && CustomerFilter" search-data-key="CustomerList" :searchbar-props="{ - url: 'Clients/extendedListFilter', + url: 'Clients/filter', label: 'Search customer', info: 'You can search by customer id or name', }" diff --git a/src/pages/Customer/Card/CustomerConsumption.vue b/src/pages/Customer/Card/CustomerConsumption.vue index 98a3115da..35f366e47 100644 --- a/src/pages/Customer/Card/CustomerConsumption.vue +++ b/src/pages/Customer/Card/CustomerConsumption.vue @@ -1,17 +1,241 @@ <script setup> +import { ref, computed, onBeforeMount } from 'vue'; +import axios from 'axios'; import { useI18n } from 'vue-i18n'; -import CustomerConsumptionFilter from './CustomerConsumptionFilter.vue'; -import { useStateStore } from 'src/stores/useStateStore'; +import { toDate } from 'src/filters/index'; +import { useRoute } from 'vue-router'; + +import VnTable from 'components/VnTable/VnTable.vue'; +import FetchedTags from 'components/ui/FetchedTags.vue'; +import { useArrayData } from 'src/composables/useArrayData'; +import { usePrintService } from 'src/composables/usePrintService'; +import { useVnConfirm } from 'src/composables/useVnConfirm'; + +const { openConfirmationModal } = useVnConfirm(); +const { openReport, sendEmail } = usePrintService(); +import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; +import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import VnInputDate from 'components/common/VnInputDate.vue'; + +const arrayData = useArrayData('Client'); const { t } = useI18n(); +const route = useRoute(); +const campaignList = ref(); +const showActionBtns = computed(() => handleQueryParams()); +function handleQueryParams() { + const query = getQueryParams(); + return query.from && query.to; +} +const columns = computed(() => [ + { + name: 'search', + align: 'left', + label: t('globals.search'), + visible: false, + }, + { + name: 'itemFk', + align: 'left', + label: t('globals.item'), + columnClass: 'shrink', + cardVisible: true, + columnFilter: { + name: 'itemId', + }, + }, + { + name: 'ticketFk', + align: 'left', + label: t('globals.ticket'), + cardVisible: true, + columnFilter: { + inWhere: true, + }, + }, + { + name: 'shipped', + align: 'left', + label: t('globals.shipped'), + format: ({ shipped }) => toDate(shipped), + columnFilter: false, + cardVisible: true, + }, + { + name: 'description', + align: 'left', + label: t('globals.description'), + columnClass: 'expand', + columnFilter: { + inWhere: true, + }, + }, + { + name: 'quantity', + label: t('globals.quantity'), + cardVisible: true, + columnFilter: { + inWhere: true, + }, + }, + { + name: 'grouped', + label: t('Group by items'), + component: 'checkbox', + visible: false, + orderBy: false, + }, +]); + +onBeforeMount(async () => { + campaignList.value = (await axios('Campaigns/latest')).data; +}); + +function getQueryParams() { + return JSON.parse(route.query.consumption ?? '{}'); +} +function getParams() { + const query = getQueryParams(); + return { + from: query.from, + to: query.to, + recipient: arrayData.store.data.email, + recipientId: arrayData.store.data.id, + }; +} +const userParams = computed(() => { + const minDate = Date.vnNew(); + minDate.setHours(0, 0, 0, 0); + minDate.setMonth(minDate.getMonth() - 2); + + const maxDate = Date.vnNew(); + maxDate.setHours(23, 59, 59, 59); + + return { + campaign: campaignList.value[0]?.id, + from: minDate, + to: maxDate, + }; +}); +const openReportPdf = () => { + openReport(`Clients/${route.params.id}/campaign-metrics-pdf`, getParams()); +}; + +const openSendEmailDialog = async () => { + openConfirmationModal( + t('The consumption report will be sent'), + t('Please, confirm'), + () => sendCampaignMetricsEmail({ address: arrayData.store.data.email }) + ); +}; +const sendCampaignMetricsEmail = ({ address }) => { + sendEmail(`Clients/${route.params.id}/campaign-metrics-email`, { + recipient: address, + ...getParams(), + }); +}; </script> <template> - <Teleport to="#right-panel" v-if="useStateStore().isHeaderMounted()"> - <CustomerConsumptionFilter data-key="CustomerConsumption" /> - </Teleport> + <VnTable + v-if="campaignList" + data-key="CustomerConsumption" + url="Clients/consumption" + :order="['itemTypeFk', 'itemName', 'itemSize', 'description']" + :columns="columns" + search-url="consumption" + :filter="filter" + :user-params="userParams" + :default-remove="false" + :default-reset="false" + :default-save="false" + :has-sub-toolbar="true" + auto-load + > + <template #moreBeforeActions> + <QBtn + color="primary" + flat + icon-right="picture_as_pdf" + @click="openReportPdf()" + :disabled="!showActionBtns" + > + <QTooltip>{{ t('globals.downloadPdf') }}</QTooltip> + </QBtn> + <QBtn + color="primary" + flat + icon-right="email" + @click="openSendEmailDialog()" + :disabled="!showActionBtns" + > + <QTooltip>{{ t('Send to email') }}</QTooltip> + </QBtn> + </template> + <template #column-itemFk="{ row }"> + <span class="link"> + {{ row.itemFk }} + <ItemDescriptorProxy :id="row.itemFk" /> + </span> + </template> + <template #column-ticketFk="{ row }"> + <span class="link"> + {{ row.ticketFk }} + <TicketDescriptorProxy :id="row.ticketFk" /> + </span> + </template> + <template #column-description="{ row }"> + <div>{{ row.concept }}</div> + <div v-if="row.subName" class="subName"> + {{ row.subName }} + </div> + <FetchedTags :item="row" :max-length="3" /> + </template> + <template #moreFilterPanel="{ params }"> + <div class="column no-wrap flex-center q-gutter-y-md q-mt-xs q-pr-xl"> + <VnSelect + v-model="params.campaign" + :options="campaignList" + :label="t('globals.campaign')" + :filled="true" + class="q-px-sm q-pt-none fit" + dense + option-label="code" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.code }} + {{ + new Date(scope.opt?.dated).getFullYear() + }}</QItemLabel + > + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnInputDate + v-model="params.from" + :label="t('globals.from')" + :filled="true" + class="q-px-xs q-pt-none fit" + dense + /> + <VnInputDate + v-model="params.to" + :label="t('globals.to')" + :filled="true" + class="q-px-xs q-pt-none fit" + dense + /> + </div> + </template> + </VnTable> </template> <i18n> es: Enter a new search: Introduce una nueva búsqueda + Group by items: Agrupar por artículos </i18n> diff --git a/src/pages/Customer/Card/CustomerConsumptionFilter.vue b/src/pages/Customer/Card/CustomerConsumptionFilter.vue deleted file mode 100644 index 4d2c5ff3c..000000000 --- a/src/pages/Customer/Card/CustomerConsumptionFilter.vue +++ /dev/null @@ -1,91 +0,0 @@ -<script setup> -import { useI18n } from 'vue-i18n'; -import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import { QItem } from 'quasar'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import { QItemSection } from 'quasar'; - -const { t } = useI18n(); -defineProps({ dataKey: { type: String, required: true } }); -</script> -<template> - <VnFilterPanel :data-key="dataKey" :search-button="true"> - <template #tags="{ tag, formatFn }"> - <div class="q-gutter-x-xs"> - <strong>{{ t(`params.${tag.label}`) }}: </strong> - <span>{{ formatFn(tag.value) }}</span> - </div> - </template> - <template #body="{ params }"> - <QItem> - <QItemSection> - <VnInput - :label="t('params.item')" - v-model="params.itemId" - is-outlined - lazy-rules - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - v-model="params.buyerId" - url="TicketRequests/getItemTypeWorker" - :label="t('params.buyer')" - option-value="id" - option-label="nickname" - dense - outlined - rounded - /> - </QItemSection> - </QItem> - <QItem> - <!--It's required to include the relation category !! There's 413 records in production--> - <QItemSection> - <VnSelect - v-model="params.typeId" - url="ItemTypes" - :label="t('params.type')" - option-label="name" - option-value="id" - dense - outlined - rounded - > - </VnSelect> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - url="ItemCategories" - :label="t('params.category')" - option-label="name" - option-value="id" - v-model="params.categoryId" - dense - outlined - rounded - /> - </QItemSection> - </QItem> - </template> - </VnFilterPanel> -</template> -<i18n> -en: - params: - item: Item id - buyer: Buyer - type: Type - category: Category -es: - params: - item: Id artículo - buyer: Comprador - type: Tipo - category: Categoría -</i18n> diff --git a/src/pages/Customer/Card/CustomerCreditContracts.vue b/src/pages/Customer/Card/CustomerCreditContracts.vue index 568adcf0b..0ff074793 100644 --- a/src/pages/Customer/Card/CustomerCreditContracts.vue +++ b/src/pages/Customer/Card/CustomerCreditContracts.vue @@ -53,6 +53,8 @@ const openDialog = (item) => { promise: updateData, }, }); + updateData(); + showQPageSticky.value = true; }; const openViewCredit = (credit) => { @@ -193,6 +195,7 @@ const updateData = () => { color="primary" fab icon="add" + shortcut="+" /> <QTooltip> {{ t('New contract') }} diff --git a/src/pages/Customer/Card/CustomerCreditOpinion.vue b/src/pages/Customer/Card/CustomerCreditOpinion.vue index 9c060b1a5..92fef1d76 100644 --- a/src/pages/Customer/Card/CustomerCreditOpinion.vue +++ b/src/pages/Customer/Card/CustomerCreditOpinion.vue @@ -1,23 +1,17 @@ <script setup> -import { computed, ref, watch } from 'vue'; +import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import { QBtn } from 'quasar'; - import { toCurrency, toDateHourMin } from 'src/filters'; +import VnTable from 'src/components/VnTable/VnTable.vue'; -import FetchData from 'components/FetchData.vue'; -import FormModel from 'components/FormModel.vue'; -import VnRow from 'components/ui/VnRow.vue'; -import VnInput from 'src/components/common/VnInput.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; const { t } = useI18n(); const route = useRoute(); -const clientInformasRef = ref(null); -const rows = ref([]); +const tableRef = ref(); const filter = { include: [ @@ -37,10 +31,9 @@ const filter = { const columns = computed(() => [ { align: 'left', - field: 'created', - format: (value) => toDateHourMin(value), + format: ({ created }) => toDateHourMin(created), label: t('Since'), - name: 'since', + name: 'created', }, { align: 'left', @@ -51,66 +44,56 @@ const columns = computed(() => [ { align: 'right', field: 'rating', - label: t('Rating'), + label: t('customer.summary.rating'), name: 'rating', + create: true, + columnCreate: { + component: 'number', + autofocus: true, + }, }, { align: 'right', field: 'recommendedCredit', - format: (value) => toCurrency(value), - label: t('Recommended credit'), + format: ({ recommendedCredit }) => toCurrency(recommendedCredit), + label: t('customer.summary.recommendCredit'), name: 'recommendedCredit', + create: true, + columnCreate: { + component: 'number', + autofocus: true, + }, }, ]); - -watch( - () => route.params.id, - (newValue) => { - if (!newValue) return; - filter.where.clientFk = newValue; - clientInformasRef.value?.fetch(); - } -); </script> <template> - <FetchData - :filter="filter" - @on-fetch="(data) => (rows = data)" - auto-load - ref="clientInformasRef" + <VnTable + ref="tableRef" + data-key="ClientInformas" url="ClientInformas" - /> - - <FormModel - :form-initial-data="{}" - :observe-form-changes="false" - :url-create="`Clients/${route.params.id}/setRating`" + :filter="filter" + :order="['created DESC']" + :columns="columns" + :right-search="false" + :is-editable="false" + :use-model="true" + :column-search="false" + :disable-option="{ card: true }" + auto-load + :create="{ + urlCreate: `Clients/${route.params.id}/setRating`, + title: 'Create rating', + onDataSaved: () => tableRef.reload(), + formInitialData: {}, + }" > - <template #form="{ data }"> - <VnRow> - <div class="col"> - <VnInput - :label="t('Rating')" - clearable - type="number" - v-model.number="data.rating" - /> - </div> - <div class="col"> - <VnInput - :label="t('Recommended credit')" - clearable - type="number" - v-model.number="data.recommendedCredit" - /> - </div> - </VnRow> + <template #column-employee="{ row }"> + <span class="link">{{ row.worker.user.nickname }}</span> + <WorkerDescriptorProxy :id="row.worker.id" /> </template> - </FormModel> - - <div class="full-width flex justify-center" v-if="rows.length"> - <QTable + </VnTable> + <!-- <QTable :columns="columns" :pagination="{ rowsPerPage: 0 }" :rows="rows" @@ -120,22 +103,16 @@ watch( class="card-width q-px-lg" > <template #body-cell-employee="{ row }"> - <QTd auto-width @click.stop> - <QBtn color="blue" flat no-caps>{{ row.worker.user.nickname }}</QBtn> + <QTd @click.stop> + <span class="link">{{ row.worker.user.nickname }}</span> <WorkerDescriptorProxy :id="row.clientFk" /> </QTd> </template> - </QTable> - </div> - - <h5 class="flex justify-center color-vn-label" v-else> - {{ t('globals.noResults') }} - </h5> + </QTable> --> </template> <i18n> es: - Rating: Clasificación Recommended credit: Crédito recomendado Since: Desde Employee: Empleado diff --git a/src/pages/Customer/Card/CustomerDescriptor.vue b/src/pages/Customer/Card/CustomerDescriptor.vue index 0e76bcfed..b18f90d20 100644 --- a/src/pages/Customer/Card/CustomerDescriptor.vue +++ b/src/pages/Customer/Card/CustomerDescriptor.vue @@ -3,7 +3,7 @@ import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { toCurrency, toDate } from 'src/filters'; +import { dashIfEmpty, toCurrency, toDate } from 'src/filters'; import useCardDescription from 'src/composables/useCardDescription'; @@ -11,6 +11,10 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue'; +import { useState } from 'src/composables/useState'; +const state = useState(); + +const customer = computed(() => state.get('customer')); const $props = defineProps({ id: { @@ -43,7 +47,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit :subtitle="data.subtitle" @on-fetch="setData" :summary="$props.summary" - data-key="customerData" + data-key="customer" > <template #menu="{ entity }"> <CustomerDescriptorMenu :customer="entity" /> @@ -57,35 +61,46 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit :value="toCurrency(entity.creditInsurance)" /> - <VnLv :label="t('customer.card.debt')" :value="toCurrency(entity.debt)" /> - <VnLv v-if="entity.salesPersonUser" :label="t('customer.card.salesPerson')"> + <VnLv + :label="t('customer.card.debt')" + :value="toCurrency(entity.debt)" + :info="t('customer.summary.riskInfo')" + /> + <VnLv :label="t('customer.card.salesPerson')"> <template #value> <VnUserLink - :name="entity.salesPersonUser?.name" + v-if="entity.salesPersonUser" + :name="entity.salesPersonUser.name" :worker-id="entity.salesPersonFk" /> + <span v-else>{{ dashIfEmpty(entity.salesPersonUser) }}</span> </template> </VnLv> <VnLv :label="t('customer.card.businessTypeFk')" - :value="entity.businessTypeFk" + :value="entity.businessType.description" /> </template> - <template #icons="{ entity }"> - <QCardActions class="q-gutter-x-md"> + <template #icons> + <QCardActions v-if="customer" class="q-gutter-x-md"> <QIcon - v-if="!entity.isActive" + v-if="!customer.isActive" name="vn:disabled" size="xs" color="primary" > <QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip> </QIcon> - <QIcon v-if="entity.isFreezed" name="vn:frozen" size="xs" color="primary"> + <QIcon + v-if="customer.isFreezed" + name="vn:frozen" + size="xs" + color="primary" + > <QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip> </QIcon> <QIcon - v-if="!entity.account.active" + v-if="!customer.account?.active" color="primary" name="vn:noweb" size="xs" @@ -93,7 +108,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit <QTooltip>{{ t('customer.card.webAccountInactive') }}</QTooltip> </QIcon> <QIcon - v-if="entity.debt > entity.credit" + v-if="customer.debt > customer.credit" name="vn:risk" size="xs" color="primary" @@ -101,7 +116,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit <QTooltip>{{ t('customer.card.hasDebt') }}</QTooltip> </QIcon> <QIcon - v-if="!entity.isTaxDataChecked" + v-if="!customer.isTaxDataChecked" name="vn:no036" size="xs" color="primary" @@ -109,7 +124,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit <QTooltip>{{ t('customer.card.notChecked') }}</QTooltip> </QIcon> <QBtn - v-if="entity.unpaid" + v-if="customer.unpaid" flat size="sm" icon="vn:Client_unpaid" @@ -121,13 +136,13 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit <br /> {{ t('unpaidDated', { - dated: toDate(entity.unpaid.dated), + dated: toDate(customer.unpaid.dated), }) }} <br /> {{ t('unpaidAmount', { - amount: toCurrency(entity.unpaid.amount), + amount: toCurrency(customer.unpaid.amount), }) }} </QTooltip> @@ -139,7 +154,13 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit <QBtn :to="{ name: 'TicketList', - query: { table: JSON.stringify({ clientFk: entity.id }) }, + query: { + from: undefined, + to: undefined, + table: JSON.stringify({ + clientFk: entity.id, + }), + }, }" size="md" icon="vn:ticket" @@ -160,23 +181,8 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit </QBtn> <QBtn :to="{ - name: 'OrderCreate', - query: { clientId: entity.id }, - }" - size="md" - icon="vn:basketadd" - color="primary" - > - <QTooltip>{{ t('New order') }}</QTooltip> - </QBtn> - <QBtn - :to="{ - name: 'AccountList', - query: { - table: JSON.stringify({ - filter: { where: { id: entity.id } }, - }), - }, + name: 'AccountSummary', + params: { id: entity.id }, }" size="md" icon="face" @@ -197,7 +203,6 @@ es: Go to module index: Ir al índice del módulo Customer ticket list: Listado de tickets del cliente Customer invoice out list: Listado de facturas del cliente - New order: Nuevo pedido Go to user: Ir al usuario Customer unpaid: Cliente impago Unpaid: Impagado diff --git a/src/pages/Customer/Card/CustomerDescriptorMenu.vue b/src/pages/Customer/Card/CustomerDescriptorMenu.vue index 560ee51c8..89b10a4fe 100644 --- a/src/pages/Customer/Card/CustomerDescriptorMenu.vue +++ b/src/pages/Customer/Card/CustomerDescriptorMenu.vue @@ -8,6 +8,9 @@ import { useQuasar } from 'quasar'; import useNotify from 'src/composables/useNotify'; import VnSmsDialog from 'src/components/common/VnSmsDialog.vue'; +import TicketCreateDialog from 'src/pages/Ticket/TicketCreateDialog.vue'; +import OrderCreateDialog from 'src/pages/Order/Card/OrderCreateDialog.vue'; +import { ref } from 'vue'; const $props = defineProps({ customer: { @@ -40,20 +43,32 @@ const sendSms = async (payload) => { notify(error.message, 'positive'); } }; + +const ticketCreateFormDialog = ref(null); +const openTicketCreateForm = () => { + ticketCreateFormDialog.value.show(); +}; +const orderCreateFormDialog = ref(null); +const openOrderCreateForm = () => { + orderCreateFormDialog.value.show(); +}; </script> <template> - <QItem v-ripple clickable> + <QItem v-ripple clickable @click="openTicketCreateForm()"> <QItemSection> - <RouterLink - :to="{ - name: 'TicketCreate', - query: { clientFk: customer.id }, - }" - class="color-vn-text" - > - {{ t('Simple ticket') }} - </RouterLink> + {{ t('globals.pageTitles.createTicket') }} + <QDialog ref="ticketCreateFormDialog"> + <TicketCreateDialog /> + </QDialog> + </QItemSection> + </QItem> + <QItem v-ripple clickable @click="openOrderCreateForm()"> + <QItemSection> + {{ t('globals.pageTitles.createOrder') }} + <QDialog ref="orderCreateFormDialog"> + <OrderCreateDialog :client-fk="customer.id" /> + </QDialog> </QItemSection> </QItem> <QItem v-ripple clickable> diff --git a/src/pages/Customer/Card/CustomerFiscalData.vue b/src/pages/Customer/Card/CustomerFiscalData.vue index 54b8adb70..d8c07a46f 100644 --- a/src/pages/Customer/Card/CustomerFiscalData.vue +++ b/src/pages/Customer/Card/CustomerFiscalData.vue @@ -93,7 +93,7 @@ function handleLocation(data, location) { <VnRow> <VnLocation :rules="validate('Worker.postcode')" - :roles-allowed-to-create="['deliveryAssistant']" + :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" :location="data" @update:model-value="(location) => handleLocation(data, location)" /> diff --git a/src/pages/Customer/Card/CustomerGreuges.vue b/src/pages/Customer/Card/CustomerGreuges.vue index 12173727f..1d8b8585f 100644 --- a/src/pages/Customer/Card/CustomerGreuges.vue +++ b/src/pages/Customer/Card/CustomerGreuges.vue @@ -5,10 +5,10 @@ import { useRoute } from 'vue-router'; import { toCurrency } from 'src/filters'; import { toDateTimeFormat } from 'src/filters/date'; import VnTable from 'components/VnTable/VnTable.vue'; - +import FetchData from 'components/FetchData.vue'; +const entityId = computed(() => route.params.id); const { t } = useI18n(); const route = useRoute(); -const rows = ref([]); const totalAmount = ref(); const tableRef = ref(); const filter = computed(() => { @@ -28,7 +28,7 @@ const filter = computed(() => { }, ], where: { - clientFk: route.params.id, + clientFk: entityId, }, }; }); @@ -47,7 +47,6 @@ const columns = computed(() => [ }, { align: 'left', - name: 'userFk', label: t('Created by'), component: 'userLink', attrs: ({ row }) => { @@ -73,6 +72,7 @@ const columns = computed(() => [ columnCreate: { component: 'select', url: 'greugeTypes', + sortBy: 'name ASC ', limit: 0, }, }, @@ -84,14 +84,14 @@ const columns = computed(() => [ create: true, }, ]); - -const setRows = (data) => { - rows.value = data; - totalAmount.value = data.reduce((acc, { amount = 0 }) => acc + amount, 0); -}; </script> <template> + <FetchData + :url="`Greuges/${entityId}/sumAmount`" + auto-load + @on-fetch="({ sumAmount }) => (totalAmount = sumAmount)" + ></FetchData> <VnTable ref="tableRef" data-key="Greuges" @@ -104,10 +104,10 @@ const setRows = (data) => { :is-editable="false" :use-model="true" :column-search="false" - @on-fetch="(data) => setRows(data)" + :disable-option="{ card: true }" :create="{ urlCreate: `Greuges`, - title: t('New credit'), + title: t('New greuge'), onDataSaved: () => tableRef.reload(), formInitialData: { shipped: new Date(), clientFk: route.params.id }, }" diff --git a/src/pages/Customer/Card/CustomerMandates.vue b/src/pages/Customer/Card/CustomerMandates.vue index 7af3e5828..248515b4a 100644 --- a/src/pages/Customer/Card/CustomerMandates.vue +++ b/src/pages/Customer/Card/CustomerMandates.vue @@ -1,20 +1,19 @@ <script setup> -import { computed, ref } from 'vue'; +import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import { toDateTimeFormat } from 'src/filters/date'; -import FetchData from 'components/FetchData.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import { dashIfEmpty } from 'src/filters'; const { t } = useI18n(); const route = useRoute(); -const rows = ref([]); - const filter = { include: [ - { relation: 'mandateType', scope: { fields: ['id', 'name'] } }, + { relation: 'mandateType', scope: { fields: ['id', 'code'] } }, { relation: 'company', scope: { fields: ['id', 'code'] } }, ], where: { clientFk: route.params.id }, @@ -22,114 +21,61 @@ const filter = { limit: 20, }; -const tableColumnComponents = { - id: { - component: 'span', - props: () => {}, - event: () => {}, - }, - company: { - component: 'span', - props: () => {}, - event: () => {}, - }, - type: { - component: 'span', - props: () => {}, - event: () => {}, - }, - registerDate: { - component: 'span', - props: () => {}, - event: () => {}, - }, - endDate: { - component: 'span', - props: () => {}, - event: () => {}, - }, -}; - const columns = computed(() => [ { align: 'left', - field: 'id', - label: t('Id'), name: 'id', + label: t('globals.id'), + field: 'id', + isId: true, }, { align: 'left', - field: (row) => row.company.code, - label: t('Company'), + cardVisible: true, + format: ({ company }) => company.code, + label: t('globals.company'), name: 'company', }, { align: 'left', - field: (row) => row.mandateType.name, - label: t('Type'), + cardVisible: true, + format: ({ mandateType }) => mandateType.code, + label: t('globals.type'), name: 'type', }, { align: 'left', - field: 'created', + cardVisible: true, label: t('Register date'), - name: 'registerDate', - format: (value) => toDateTimeFormat(value), + name: 'created', + format: ({ created }) => toDateTimeFormat(created), }, { - align: 'left', - field: 'finished', + align: 'right', + cardVisible: true, + name: 'finished', label: t('End date'), - name: 'endDate', - format: (value) => (value ? toDateTimeFormat(value) : '-'), + format: ({ finished }) => dashIfEmpty(toDateTimeFormat(finished)), }, ]); </script> <template> - <FetchData - :filter="filter" - @on-fetch="(data) => (rows = data)" - auto-load - url="Mandates" - /> - <QPage class="column items-center q-pa-md"> - <QTable + <VnTable + :filter="filter" + auto-load + url="Mandates" :columns="columns" - :pagination="{ rowsPerPage: 12 }" - :rows="rows" class="full-width q-mt-md" - row-key="id" - v-if="rows?.length" - > - <template #body-cell="props"> - <QTd :props="props"> - <QTr :props="props" class="cursor-pointer"> - <component - :is="tableColumnComponents[props.col.name].component" - @click="tableColumnComponents[props.col.name].event(props)" - class="rounded-borders q-pa-sm" - v-bind="tableColumnComponents[props.col.name].props(props)" - > - {{ props.value }} - </component> - </QTr> - </QTd> - </template> - </QTable> - - <h5 class="flex justify-center color-vn-label" v-else> - {{ t('globals.noResults') }} - </h5> + :right-search="false" + :row-click="false" + /> </QPage> </template> <i18n> es: - Id: Id - Company: Empresa - Type: Tipo Register date: Fecha alta End date: Fecha baja </i18n> diff --git a/src/pages/Customer/Card/CustomerRecoveries.vue b/src/pages/Customer/Card/CustomerRecoveries.vue index 8d3d05702..48576ca20 100644 --- a/src/pages/Customer/Card/CustomerRecoveries.vue +++ b/src/pages/Customer/Card/CustomerRecoveries.vue @@ -89,9 +89,10 @@ function setFinished(id) { :columns="columns" :use-model="true" :right-search="false" + :disable-option="{ card: true }" :create="{ urlCreate: 'Recoveries', - title: 'New recovery', + title: t('New recovery'), onDataSaved: () => tableRef.reload(), formInitialData: { clientFk: route.params.id, started: Date.vnNew() }, }" diff --git a/src/pages/Customer/Card/CustomerSamples.vue b/src/pages/Customer/Card/CustomerSamples.vue index de998d8d3..8e2ab92a0 100644 --- a/src/pages/Customer/Card/CustomerSamples.vue +++ b/src/pages/Customer/Card/CustomerSamples.vue @@ -1,19 +1,18 @@ <script setup> import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; -import { useRoute, useRouter } from 'vue-router'; +import { useRoute } from 'vue-router'; -import { QBtn } from 'quasar'; +import { QBtn, useQuasar } from 'quasar'; -import FetchData from 'components/FetchData.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import { toDateTimeFormat } from 'src/filters/date'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import { dashIfEmpty } from 'src/filters'; +import CustomerSamplesCreate from '../components/CustomerSamplesCreate.vue'; const { t } = useI18n(); const route = useRoute(); -const router = useRouter(); - -const rows = ref([]); const filter = { include: [ @@ -26,105 +25,78 @@ const filter = { limit: 20, }; -const tableColumnComponents = { - sent: { - component: 'span', - props: () => {}, - event: () => {}, - }, - description: { - component: 'span', - props: () => {}, - event: () => {}, - }, - worker: { - component: QBtn, - props: () => ({ flat: true, color: 'blue', noCaps: true }), - event: () => {}, - }, - company: { - component: 'span', - props: () => {}, - event: () => {}, - }, -}; - const columns = computed(() => [ { align: 'left', - field: 'created', + name: 'created', label: t('Sent'), - name: 'sent', - format: (value) => toDateTimeFormat(value), + format: ({ created }) => toDateTimeFormat(created), }, { align: 'left', - field: (value) => value.type.description, + format: (row) => row.type.description, label: t('Description'), name: 'description', }, { align: 'left', - field: (value) => value.user.name, label: t('Worker'), - name: 'worker', + columnField: { + component: 'userLink', + attrs: ({ row }) => { + return { + defaultName: true, + workerId: row?.user?.id, + name: row?.user?.name, + }; + }, + }, }, { align: 'left', - field: (value) => value.company?.code, + format: ({ company }) => company?.code ?? dashIfEmpty(company), label: t('Company'), name: 'company', }, ]); +const quasar = useQuasar(); const toCustomerSamplesCreate = () => { - router.push({ name: 'CustomerSamplesCreate' }); + quasar + .dialog({ + component: CustomerSamplesCreate, + }) + .onOk(() => tableRef.value.reload()); }; +const tableRef = ref(); </script> <template> - <FetchData - :filter="filter" - @on-fetch="(data) => (rows = data)" + <VnTable + ref="tableRef" + data-key="ClientSamples" auto-load + :filter="filter" url="ClientSamples" - /> - - <div class="full-width flex justify-center"> - <QPage class="card-width q-pa-lg"> - <QTable - :columns="columns" - :pagination="{ rowsPerPage: 12 }" - :rows="rows" - class="full-width q-mt-md" - row-key="id" - :no-data-label="t('globals.noResults')" - > - <template #body-cell="props"> - <QTd :props="props"> - <QTr :props="props" class="cursor-pointer"> - <component - :is="tableColumnComponents[props.col.name].component" - class="col-content" - v-bind=" - tableColumnComponents[props.col.name].props(props) - " - @click=" - tableColumnComponents[props.col.name].event(props) - " - > - {{ props.value }} - <WorkerDescriptorProxy - :id="props.row.userFk" - v-if="props.col.name === 'worker'" - /> - </component> - </QTr> - </QTd> - </template> - </QTable> - </QPage> - </div> + :columns="columns" + :pagination="{ rowsPerPage: 12 }" + :disable-option="{ card: true }" + :right-search="false" + :rows="rows" + :order="['created DESC']" + class="full-width q-mt-md" + row-key="id" + :create="false" + :no-data-label="t('globals.noResults')" + > + <template #column-worker="{ row }"> + <div v-if="row.user"> + <span class="link">{{ row.user?.name }}</span + ><WorkerDescriptorProxy :id="row.userFk" /> + </div> + <span v-else>{{ dashIfEmpty(row.user) }}</span> + </template> + </VnTable> <QPageSticky :offset="[18, 18]"> <QBtn @click.stop="toCustomerSamplesCreate()" color="primary" fab icon="add" /> diff --git a/src/pages/Customer/Card/CustomerSummary.vue b/src/pages/Customer/Card/CustomerSummary.vue index 15bf19b48..f049426e2 100644 --- a/src/pages/Customer/Card/CustomerSummary.vue +++ b/src/pages/Customer/Card/CustomerSummary.vue @@ -1,10 +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 } 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 CustomerSummaryTable from 'src/pages/Customer/components/CustomerSummaryTable.vue'; @@ -23,11 +24,6 @@ const $props = defineProps({ const entityId = computed(() => $props.id || route.params.id); const customer = computed(() => summary.value.entity); const summary = ref(); -const clientUrl = ref(); - -onMounted(async () => { - clientUrl.value = (await getUrl('client/')) + entityId.value + '/'; -}); const balanceDue = computed(() => { return ( @@ -40,11 +36,11 @@ const balanceDue = computed(() => { const balanceDueWarning = computed(() => (balanceDue.value ? 'negative' : '')); const claimRate = computed(() => { - return customer.value.claimsRatio.claimingRate; + return customer.value.claimsRatio?.claimingRate ?? 0; }); const priceIncreasingRate = computed(() => { - return customer.value.claimsRatio.priceIncreasing / 100; + return customer.value.claimsRatio?.priceIncreasing ?? 0 / 100; }); const debtWarning = computed(() => { @@ -58,6 +54,11 @@ const creditWarning = computed(() => { return tooMuchInsurance || noCreditInsurance ? 'negative' : ''; }); +const sumRisk = ({ clientRisks }) => { + let total = clientRisks.reduce((acc, { amount }) => acc + amount, 0); + + return total; +}; </script> <template> @@ -91,7 +92,13 @@ const creditWarning = computed(() => { <VnLv :label="t('customer.summary.salesPerson')" :value="entity?.salesPersonUser?.name" - /> + > + <template #value> + <VnUserLink + :name="entity.salesPersonUser?.name" + :worker-id="entity.salesPersonFk" + /> </template + ></VnLv> <VnLv :label="t('customer.summary.contactChannel')" :value="entity?.contactChannel?.name" @@ -131,7 +138,7 @@ const creditWarning = computed(() => { :url="`#/customer/${entityId}/fiscal-data`" :text="t('customer.summary.fiscalData')" /> - <VnRow> + <VnRow class="block"> <VnLv :label="t('customer.summary.isEqualizated')" :value="entity.isEqualizated" @@ -140,8 +147,6 @@ const creditWarning = computed(() => { :label="t('customer.summary.isActive')" :value="entity.isActive" /> - </VnRow> - <VnRow> <VnLv :label="t('customer.summary.verifiedData')" :value="entity.isTaxDataChecked" @@ -150,8 +155,6 @@ const creditWarning = computed(() => { :label="t('customer.summary.hasToInvoice')" :value="entity.hasToInvoice" /> - </VnRow> - <VnRow> <VnLv :label="t('customer.summary.notifyByEmail')" :value="entity.isToBeMailed" @@ -162,7 +165,7 @@ const creditWarning = computed(() => { <QCard class="vn-one"> <VnTitle :url="`#/customer/${entityId}/billing-data`" - :text="t('customer.summary.billingData')" + :text="t('customer.summary.payMethodFk')" /> <VnLv :label="t('customer.summary.payMethod')" @@ -170,7 +173,7 @@ const creditWarning = computed(() => { /> <VnLv :label="t('customer.summary.bankAccount')" :value="entity.iban" /> <VnLv :label="t('customer.summary.dueDay')" :value="entity.dueDay" /> - <VnRow class="q-mt-sm" wrap> + <VnRow class="q-mt-sm block"> <VnLv :label="t('customer.summary.hasLcr')" :value="entity.hasLcr" /> <VnLv :label="t('customer.summary.hasCoreVnl')" @@ -185,7 +188,7 @@ const creditWarning = computed(() => { </QCard> <QCard class="vn-one" v-if="entity.defaultAddress"> <VnTitle - :url="`#/customer/${entityId}/consignees`" + :url="`#/customer/${entityId}/address`" :text="t('customer.summary.consignee')" /> <VnLv @@ -217,7 +220,11 @@ const creditWarning = computed(() => { /> </QCard> <QCard class="vn-one" v-if="entity.account"> - <VnTitle :text="t('customer.summary.businessData')" /> + <VnTitle + :url="`${grafanaUrl}/d/adjlxzv5yjt34d/analisis-de-clientes-7c-crm?orgId=1&var-clientFk=${entityId}`" + :text="t('customer.summary.businessData')" + icon="vn:grafana" + /> <VnLv :label="t('customer.summary.totalGreuge')" :value="toCurrency(entity.totalGreuge)" @@ -227,7 +234,6 @@ const creditWarning = computed(() => { :value="toCurrency(entity?.mana?.mana)" /> <VnLv - v-if="entity.claimsRatio" :label="t('customer.summary.priceIncreasingRate')" :value="toPercentage(priceIncreasingRate)" /> @@ -236,15 +242,14 @@ const creditWarning = computed(() => { :value="toCurrency(entity?.averageInvoiced?.invoiced)" /> <VnLv - v-if="entity.claimsRatio" :label="t('customer.summary.claimRate')" :value="toPercentage(claimRate)" /> </QCard> <QCard class="vn-one" v-if="entity.account"> <VnTitle - :url="`https://grafana.verdnatura.es/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk=${entityId}`" - :text="t('customer.summary.financialData')" + :url="`${grafanaUrl}/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk=${entityId}`" + :text="t('customer.summary.payMethodFk')" icon="vn:grafana" /> <VnLv @@ -262,15 +267,13 @@ const creditWarning = computed(() => { /> <VnLv - v-if="entity.creditInsurance" :label="t('customer.summary.securedCredit')" :value="toCurrency(entity.creditInsurance)" :info="t('customer.summary.securedCreditInfo')" /> - <VnLv :label="t('customer.summary.balance')" - :value="toCurrency(entity.sumRisk) || toCurrency(0)" + :value="toCurrency(sumRisk(entity)) || toCurrency(0)" :info="t('customer.summary.balanceInfo')" /> @@ -297,7 +300,7 @@ const creditWarning = computed(() => { :value="entity.recommendedCredit" /> </QCard> - <QCard class="vn-one"> + <QCard> <VnTitle :text="t('Latest tickets')" /> <CustomerSummaryTable /> </QCard> diff --git a/src/pages/Customer/Card/CustomerUnpaid.vue b/src/pages/Customer/Card/CustomerUnpaid.vue index 5b9a6cde2..ad00cbf59 100644 --- a/src/pages/Customer/Card/CustomerUnpaid.vue +++ b/src/pages/Customer/Card/CustomerUnpaid.vue @@ -151,7 +151,10 @@ watch( clearable type="number" v-model="amount" - /> + autofocus + > + <template #append>€</template></VnInput + > </div> </VnRow> </QForm> diff --git a/src/pages/Customer/Card/CustomerWebPayment.vue b/src/pages/Customer/Card/CustomerWebPayment.vue index 13ec6b128..482582078 100644 --- a/src/pages/Customer/Card/CustomerWebPayment.vue +++ b/src/pages/Customer/Card/CustomerWebPayment.vue @@ -5,7 +5,7 @@ import { useRoute } from 'vue-router'; import axios from 'axios'; -import { toCurrency, toDateHourMinSec } from 'src/filters'; +import { toCurrency, toDateHourMin } from 'src/filters'; import CustomerCloseIconTooltip from '../components/CustomerCloseIconTooltip.vue'; import CustomerCheckIconTooltip from '../components/CustomerCheckIconTooltip.vue'; @@ -74,7 +74,7 @@ const columns = computed(() => [ field: 'created', label: t('Date'), name: 'date', - format: (value) => toDateHourMinSec(value), + format: (value) => toDateHourMin(value), }, { align: 'left', diff --git a/src/pages/Customer/CustomerCreate.vue b/src/pages/Customer/CustomerCreate.vue index 193ed59c9..79da63283 100644 --- a/src/pages/Customer/CustomerCreate.vue +++ b/src/pages/Customer/CustomerCreate.vue @@ -86,7 +86,8 @@ function handleLocation(data, location) { <VnRow> <VnLocation :rules="validate('Worker.postcode')" - :roles-allowed-to-create="['deliveryAssistant']" + :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" + v-model="data.location" @update:model-value="(location) => handleLocation(data, location)" > </VnLocation> diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index 1dc0dab43..f6758bf4e 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -2,6 +2,7 @@ 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'; @@ -69,7 +70,7 @@ const columns = computed(() => [ optionFilter: 'firstName', useLike: false, }, - create: true, + create: false, columnField: { component: null, }, @@ -195,6 +196,8 @@ const columns = computed(() => [ component: 'select', attrs: { url: 'BusinessTypes', + fields: ['code', 'description'], + sortBy: 'description ASC ', optionLabel: 'description', optionValue: 'code', }, @@ -353,12 +356,13 @@ const columns = computed(() => [ { title: t('Client ticket list'), icon: 'vn:ticket', - action: redirectToCreateView, + action: redirectToTicketsList, isPrimary: true, }, { - title: t('Client ticket list'), + title: t('components.smartCard.viewSummary'), icon: 'preview', + isPrimary: true, action: (row) => viewSummary(row.id, CustomerSummary), }, ], @@ -366,11 +370,12 @@ const columns = computed(() => [ ]); const { viewSummary } = useSummaryDialog(); -const redirectToCreateView = (row) => { +const redirectToTicketsList = (row) => { router.push({ name: 'TicketList', + query: { - params: JSON.stringify({ + table: JSON.stringify({ clientFk: row.id, }), }, @@ -395,10 +400,10 @@ function handleLocation(data, location) { <VnTable ref="tableRef" data-key="Customer" - url="Clients/extendedListFilter" + url="Clients/filter" :create="{ urlCreate: 'Clients/createWithUser', - title: 'Create client', + title: t('globals.pageTitles.customerCreate'), onDataSaved: ({ id }) => tableRef.redirect(id), formInitialData: { active: true, @@ -411,8 +416,42 @@ function handleLocation(data, location) { auto-load > <template #more-create-dialog="{ data }"> + <VnSelect + url="Workers/search" + v-model="data.salesPersonFk" + :label="t('customer.basicData.salesPerson')" + :params="{ + departmentCodes: ['VT', 'shopping'], + }" + :fields="['id', 'nickname']" + sort-by="nickname ASC" + :use-like="false" + 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 - :roles-allowed-to-create="['deliveryAssistant']" + :acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]" + v-model="data.location" @update:model-value="(location) => handleLocation(data, location)" /> <QInput v-model="data.userName" :label="t('Web user')" /> diff --git a/src/pages/Customer/components/CustomerAddressCreate.vue b/src/pages/Customer/components/CustomerAddressCreate.vue index e37306a94..7826c3579 100644 --- a/src/pages/Customer/components/CustomerAddressCreate.vue +++ b/src/pages/Customer/components/CustomerAddressCreate.vue @@ -57,12 +57,12 @@ function handleLocation(data, location) { </script> <template> - <fetch-data + <FetchData @on-fetch="(data) => (agencyModes = data)" auto-load url="AgencyModes/isActive" /> - <fetch-data @on-fetch="(data) => (incoterms = data)" auto-load url="Incoterms" /> + <FetchData @on-fetch="(data) => (incoterms = data)" auto-load url="Incoterms" /> <FormModel :form-initial-data="formInitialData" @@ -92,7 +92,8 @@ function handleLocation(data, location) { <VnLocation :rules="validate('Worker.postcode')" - :roles-allowed-to-create="['deliveryAssistant']" + :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" + v-model="data.location" @update:model-value="(location) => handleLocation(data, location)" /> diff --git a/src/pages/Customer/components/CustomerAddressEdit.vue b/src/pages/Customer/components/CustomerAddressEdit.vue index 133d2fabc..2252a96dc 100644 --- a/src/pages/Customer/components/CustomerAddressEdit.vue +++ b/src/pages/Customer/components/CustomerAddressEdit.vue @@ -113,18 +113,18 @@ function handleLocation(data, location) { </script> <template> - <fetch-data + <FetchData @on-fetch="(data) => (agencyModes = data)" auto-load url="AgencyModes/isActive" /> - <fetch-data @on-fetch="(data) => (incoterms = data)" auto-load url="Incoterms" /> - <fetch-data + <FetchData @on-fetch="(data) => (incoterms = data)" auto-load url="Incoterms" /> + <FetchData @on-fetch="(data) => (customsAgents = data)" auto-load url="CustomsAgents" /> - <fetch-data @on-fetch="getData" auto-load url="ObservationTypes" /> + <FetchData @on-fetch="getData" auto-load url="ObservationTypes" /> <FormModel :observe-form-changes="false" @@ -176,7 +176,7 @@ function handleLocation(data, location) { <div class="col"> <VnLocation :rules="validate('Worker.postcode')" - :roles-allowed-to-create="['deliveryAssistant']" + :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" :location="{ postcode: data.postalCode, city: data.city, diff --git a/src/pages/Customer/components/CustomerCreditContractsCreate.vue b/src/pages/Customer/components/CustomerCreditContractsCreate.vue index c4434e870..c25e59e1a 100644 --- a/src/pages/Customer/components/CustomerCreditContractsCreate.vue +++ b/src/pages/Customer/components/CustomerCreditContractsCreate.vue @@ -1,5 +1,5 @@ <script setup> -import { reactive } from 'vue'; +import { reactive, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; @@ -10,10 +10,12 @@ import VnInputDate from 'src/components/common/VnInputDate.vue'; const { t } = useI18n(); const route = useRoute(); +const routeId = computed(() => route.params.id); const router = useRouter(); const initialData = reactive({ - clientFK: Number(route.params.id), + started: Date.vnNew(), + clientFk: routeId.value, }); const toCustomerCreditContracts = () => { diff --git a/src/pages/Customer/components/CustomerCreditContractsInsurance.vue b/src/pages/Customer/components/CustomerCreditContractsInsurance.vue index ce880d4b5..70f7cf046 100644 --- a/src/pages/Customer/components/CustomerCreditContractsInsurance.vue +++ b/src/pages/Customer/components/CustomerCreditContractsInsurance.vue @@ -1,47 +1,26 @@ <script setup> -import { computed, ref } from 'vue'; +import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import { toCurrency, toDateHourMinSec } from 'src/filters'; +import { toCurrency, toDate } from 'src/filters'; -import FetchData from 'components/FetchData.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; const { t } = useI18n(); const route = useRoute(); -const rows = ref([]); - const filter = { where: { creditClassificationFk: `${route.params.creditId}`, }, limit: 20, }; - -const tableColumnComponents = { - created: { - component: 'span', - props: () => {}, - event: () => {}, - }, - grade: { - component: 'span', - props: () => {}, - event: () => {}, - }, - credit: { - component: 'span', - props: () => {}, - event: () => {}, - }, -}; - const columns = computed(() => [ { align: 'left', field: 'created', - format: (value) => toDateHourMinSec(value), + format: ({ created }) => toDate(created), label: t('Created'), name: 'created', }, @@ -53,8 +32,7 @@ const columns = computed(() => [ }, { align: 'left', - field: 'credit', - format: (value) => toCurrency(value), + format: ({ credit }) => toCurrency(credit), label: t('Credit'), name: 'credit', }, @@ -62,41 +40,19 @@ const columns = computed(() => [ </script> <template> - <FetchData - :filter="filter" - @on-fetch="(data) => (rows = data)" - auto-load + <VnTable url="CreditInsurances" - /> - - <QPage class="column items-center q-pa-md" v-if="rows.length"> - <QTable - :columns="columns" - :pagination="{ rowsPerPage: 12 }" - :rows="rows" - class="full-width q-mt-md" - row-key="id" - > - <template #body-cell="props"> - <QTd :props="props"> - <QTr :props="props" class="cursor-pointer"> - <component - :is="tableColumnComponents[props.col.name].component" - class="col-content" - v-bind="tableColumnComponents[props.col.name].props(props)" - @click="tableColumnComponents[props.col.name].event(props)" - > - {{ props.value }} - </component> - </QTr> - </QTd> - </template> - </QTable> - </QPage> - - <h5 class="flex justify-center color-vn-label" v-else> - {{ t('globals.noResults') }} - </h5> + ref="tableRef" + data-key="creditInsurances" + :filter="filter" + :columns="columns" + :right-search="false" + :is-editable="false" + :use-model="true" + :column-search="false" + :disable-option="{ card: true }" + auto-load + ></VnTable> </template> <i18n> diff --git a/src/pages/Customer/components/CustomerFileManagementCreate.vue b/src/pages/Customer/components/CustomerFileManagementCreate.vue index 7c15e0b71..f33a47bcc 100644 --- a/src/pages/Customer/components/CustomerFileManagementCreate.vue +++ b/src/pages/Customer/components/CustomerFileManagementCreate.vue @@ -83,35 +83,35 @@ const toCustomerFileManagement = () => { </script> <template> - <fetch-data + <FetchData @on-fetch="(data) => (client = data)" auto-load :url="`Clients/${route.params.id}/getCard`" /> - <fetch-data + <FetchData :filter="filterFindOne" @on-fetch="(data) => (findOne = data)" auto-load url="DmsTypes/findOne" /> - <fetch-data + <FetchData @on-fetch="(data) => (allowedContentTypes = data)" auto-load url="DmsContainers/allowedContentTypes" /> - <fetch-data + <FetchData :filter="filterCompanies" @on-fetch="(data) => (optionsCompanies = data)" auto-load url="Companies" /> - <fetch-data + <FetchData :filter="filterWarehouses" @on-fetch="(data) => (optionsWarehouses = data)" auto-load url="Warehouses" /> - <fetch-data + <FetchData :filter="filterWarehouses" @on-fetch="(data) => (optionsDmsTypes = data)" auto-load diff --git a/src/pages/Customer/components/CustomerFileManagementEdit.vue b/src/pages/Customer/components/CustomerFileManagementEdit.vue index 80eaa44f9..107f41330 100644 --- a/src/pages/Customer/components/CustomerFileManagementEdit.vue +++ b/src/pages/Customer/components/CustomerFileManagementEdit.vue @@ -69,25 +69,25 @@ const toCustomerFileManagement = () => { </script> <template> - <fetch-data :url="`Dms/${route.params.dmsId}`" @on-fetch="setCurrentDms" auto-load /> - <fetch-data + <FetchData :url="`Dms/${route.params.dmsId}`" @on-fetch="setCurrentDms" auto-load /> + <FetchData @on-fetch="(data) => (allowedContentTypes = data)" auto-load url="DmsContainers/allowedContentTypes" /> - <fetch-data + <FetchData :filter="filterCompanies" @on-fetch="(data) => (optionsCompanies = data)" auto-load url="Companies" /> - <fetch-data + <FetchData :filter="filterWarehouses" @on-fetch="(data) => (optionsWarehouses = data)" auto-load url="Warehouses" /> - <fetch-data + <FetchData :filter="filterWarehouses" @on-fetch="(data) => (optionsDmsTypes = data)" auto-load diff --git a/src/pages/Customer/components/CustomerSamplesCreate.vue b/src/pages/Customer/components/CustomerSamplesCreate.vue index 0470dc176..3c9eb856b 100644 --- a/src/pages/Customer/components/CustomerSamplesCreate.vue +++ b/src/pages/Customer/components/CustomerSamplesCreate.vue @@ -1,10 +1,11 @@ <script setup> -import { onBeforeMount, reactive, ref } from 'vue'; +import { computed, onBeforeMount, reactive, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; import axios from 'axios'; -import { useQuasar } from 'quasar'; +import { usePrintService } from 'composables/usePrintService'; +import { useDialogPluginComponent, useQuasar } from 'quasar'; import { useState } from 'src/composables/useState'; import { useValidator } from 'src/composables/useValidator'; @@ -16,7 +17,9 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import CustomerSamplesPreview from 'src/pages/Customer/components/CustomerSamplesPreview.vue'; -import { useStateStore } from 'stores/useStateStore'; +import FormPopup from 'src/components/FormPopup.vue'; + +const { dialogRef, onDialogOK } = useDialogPluginComponent(); const { notify } = useNotify(); const { t } = useI18n(); @@ -26,17 +29,17 @@ const route = useRoute(); const router = useRouter(); const state = useState(); const user = state.getUser(); -const stateStore = useStateStore(); - -const client = ref({}); +const { sendEmail } = usePrintService(); const hasChanged = ref(false); const isLoading = ref(false); -const optionsClientsAddressess = ref([]); -const optionsCompanies = ref([]); +const addressess = ref([]); +const companies = ref([]); const optionsEmailUsers = ref([]); const optionsSamplesVisible = ref([]); const sampleType = ref({ hasPreview: false }); - +const initialData = reactive({}); +const entityId = computed(() => route.params.id); +const customer = computed(() => state.get('customer')); const filterEmailUsers = { where: { userFk: user.value.id } }; const filterClientsAddresses = { include: [ @@ -58,14 +61,13 @@ const filterSamplesVisible = { ], order: ['description'], }; -const initialData = reactive({}); + +defineEmits(['confirm', ...useDialogPluginComponent.emits]); onBeforeMount(async () => { - const { data } = await axios.get(`Clients/1/getCard`); - client.value = data; - initialData.clientFk = route.params?.id; - initialData.recipient = data.email; - initialData.recipientId = data.id; + initialData.clientFk = customer.value.id; + initialData.recipient = customer.value.email; + initialData.recipientId = customer.value.id; }); const setEmailUser = (data) => { @@ -75,7 +77,7 @@ const setEmailUser = (data) => { const setClientsAddresses = (data) => { initialData.addressId = data[0].id; - optionsClientsAddressess.value = data; + addressess.value = data; }; const setSampleType = (sampleId) => { @@ -88,20 +90,6 @@ const setSampleType = (sampleId) => { initialData.companyId = companyFk; }; -const setInitialData = () => { - hasChanged.value = false; - - initialData.addressId = optionsClientsAddressess.value[0].id; - initialData.companyFk = null; - initialData.from = null; - initialData.recipient = client.value.email; - initialData.recipientId = client.value.id; - initialData.replyTo = optionsEmailUsers.value[0]?.email; - initialData.typeFk = ''; - - sampleType.value = {}; -}; - const validateMessage = () => { if (!initialData.recipient) return 'Email cannot be blank'; if (!sampleType.value) return 'Choose a sample'; @@ -120,14 +108,14 @@ const setParams = (params) => { const getPreview = async () => { try { const params = { - recipientId: route.params.id, + recipientId: entityId, }; const validationMessage = validateMessage(); if (validationMessage) return notify(t(validationMessage), 'negative'); setParams(params); - const path = `${sampleType.value.model}/${route.params.id}/${sampleType.value.code}-html`; + const path = `${sampleType.value.model}/${entityId.value}/${sampleType.value.code}-html`; const { data } = await axios.get(path, { params }); if (!data) return; @@ -156,22 +144,33 @@ const onSubmit = async () => { } }; -const onDataSaved = async ({ - addressId, - companyFk, - companyId, - from, - recipient, - replyTo, -}) => { - await axios.post(`Clients/${route.params.id}/incoterms-authorization-email`, { - addressId, - companyFk, - companyId, - from, - recipient, - replyTo, - }); +const getSamples = async () => { + try { + const filter = { where: { id: initialData.typeFk } }; + let { data } = await axios.get('Samples', { + params: { filter: JSON.stringify(filter) }, + }); + return data[0]; + } catch (error) { + notify('errors.create', 'negative'); + } +}; + +const onDataSaved = async () => { + try { + const params = { + recipientId: initialData.recipientId, + recipient: initialData.recipient, + replyTo: initialData.replyTo, + }; + setParams(params); + const samplesData = await getSamples(); + const path = `${samplesData.model}/${entityId.value}/${samplesData.code}-email`; + await sendEmail(path, params); + onDialogOK(params); + } catch (error) { + notify('errors.create', 'negative'); + } toCustomerSamples(); }; @@ -185,73 +184,54 @@ const toCustomerSamples = () => { </script> <template> - <fetch-data + <FetchData :filter="filterEmailUsers" @on-fetch="setEmailUser" auto-load url="EmailUsers" /> - <fetch-data + <FetchData :filter="filterClientsAddresses" - :url="`Clients/${route.params.id}/addresses`" + :url="`Clients/${entityId}/addresses`" @on-fetch="setClientsAddresses" auto-load /> - <fetch-data + <FetchData :filter="filterCompanies" - @on-fetch="(data) => (optionsCompanies = data)" + @on-fetch="(data) => (companies = data)" auto-load url="Companies" /> - <fetch-data + <FetchData :filter="filterSamplesVisible" @on-fetch="(data) => (optionsSamplesVisible = data)" auto-load url="Samples/visible" /> - <Teleport v-if="stateStore?.isSubToolbarShown()" to="#st-actions"> - <QBtnGroup push class="q-gutter-x-sm"> - <QBtn - :label="t('globals.cancel')" - @click="toCustomerSamples" - color="primary" - flat - icon="close" - /> - <QBtn - :disabled="isLoading || !sampleType?.hasPreview" - :label="t('Preview')" - :loading="isLoading" - @click.stop="getPreview()" - color="primary" - flat - icon="preview" - /> - <QBtn - :disabled="!hasChanged" - :label="t('globals.reset')" - :loading="isLoading" - @click="setInitialData" - color="primary" - flat - icon="restart_alt" - type="reset" - /> - <QBtn - :disabled="!hasChanged" - :label="t('globals.save')" - :loading="isLoading" - @click="onSubmit" - color="primary" - icon="save" - /> - </QBtnGroup> - </Teleport> - - <div class="full-width flex justify-center"> - <QCard class="card-width q-pa-lg"> - <QForm> + <QDialog ref="dialogRef"> + <FormPopup + :default-cancel-button="false" + :default-submit-button="false" + @on-submit="onSubmit()" + > + <template #custom-buttons> + <QBtn + :disabled="isLoading || !sampleType?.hasPreview" + :label="t('Preview')" + :loading="isLoading" + @click.stop="getPreview()" + color="primary" + flat + icon="preview" /><QBtn + :disabled="!hasChanged" + :label="t('globals.save')" + :loading="isLoading" + @click="onSubmit" + color="primary" + icon="save" + /></template> + <template #form-inputs> <div class="col"> <VnSelect :label="t('Sample')" @@ -308,7 +288,7 @@ const toCustomerSamples = () => { <div class="col"> <VnSelect :label="t('Company')" - :options="optionsCompanies" + :options="companies" :rules="validate('entry.companyFk')" hide-selected option-label="code" @@ -321,7 +301,7 @@ const toCustomerSamples = () => { <div class="col"> <VnSelect :label="t('Address')" - :options="optionsClientsAddressess" + :options="addressess" hide-selected option-label="nickname" option-value="id" @@ -359,15 +339,15 @@ const toCustomerSamples = () => { required="true" v-model="initialData.from" /> - </div> - </VnRow> - </QForm> - </QCard> - </div> + </div> </VnRow + ></template> + </FormPopup> + </QDialog> </template> <i18n> es: + New sample: Crear plantilla Sample: Plantilla Recipient: Destinatario Reply to: Responder a diff --git a/src/pages/Customer/components/CustomerSummaryTable.vue b/src/pages/Customer/components/CustomerSummaryTable.vue index dc9969b61..374769a57 100644 --- a/src/pages/Customer/components/CustomerSummaryTable.vue +++ b/src/pages/Customer/components/CustomerSummaryTable.vue @@ -1,22 +1,25 @@ <script setup> -import { computed, ref } from 'vue'; +import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; -import { QBtn, date } from 'quasar'; +import { date } from 'quasar'; +import { toDateFormat } from 'src/filters/date.js'; import { toCurrency } from 'src/filters'; -import FetchData from 'components/FetchData.vue'; -import CustomerSummaryTableActions from './CustomerSummaryTableActions.vue'; -import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue'; + +import InvoiceOutDescriptorProxy from 'pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; import RouteDescriptorProxy from 'src/pages/Route/Card/RouteDescriptorProxy.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue'; const { t } = useI18n(); const route = useRoute(); const router = useRouter(); - -const rows = ref([]); +const { viewSummary } = useSummaryDialog(); const filter = { include: [ @@ -32,57 +35,6 @@ const filter = { ], where: { clientFk: route.params.id }, order: ['shipped DESC', 'id'], - limit: 10, -}; - -const tableColumnComponents = { - id: { - component: 'span', - props: () => {}, - event: () => {}, - }, - nickname: { - component: QBtn, - props: () => ({ flat: true, color: 'blue', noCaps: true }), - event: () => {}, - }, - agency: { - component: 'span', - props: () => {}, - event: () => {}, - }, - route: { - component: QBtn, - props: () => ({ flat: true, color: 'blue' }), - event: () => {}, - }, - packages: { - component: 'span', - props: () => {}, - event: () => {}, - }, - date: { - component: 'span', - props: () => {}, - event: () => {}, - }, - state: { - component: 'span', - props: () => {}, - event: () => {}, - }, - total: { - component: 'span', - props: () => {}, - event: () => {}, - }, - actions: { - component: CustomerSummaryTableActions, - props: (prop) => ({ - id: prop.row.id, - }), - event: () => {}, - }, }; const columns = computed(() => [ @@ -94,37 +46,37 @@ const columns = computed(() => [ }, { align: 'left', - field: 'nickname', label: t('Nickname'), name: 'nickname', + columnClass: 'expand', }, { align: 'left', - field: (row) => row?.agencyMode?.name, + format: (row) => row.agencyMode.name, + columnClass: 'expand', label: t('Agency'), - name: 'agency', }, { align: 'left', - field: 'routeFk', + name: 'routeFk', + columnClass: 'shrink', label: t('Route'), - name: 'route', }, { align: 'left', field: 'packages', label: t('Packages'), name: 'packages', + columnClass: 'shrink', }, { align: 'left', - field: (row) => date.formatDate(row?.shipped, 'DD/MM/YYYY'), + format: ({ shipped }) => date.formatDate(shipped, 'DD/MM/YYYY'), label: t('Date'), - name: 'date', + name: 'shipped', }, { align: 'left', - field: (row) => row?.ticketState?.state?.name, label: t('State'), name: 'state', }, @@ -134,11 +86,25 @@ const columns = computed(() => [ label: t('Total'), name: 'total', }, + { - align: 'left', - field: 'totalWithVat', + align: 'right', label: '', - name: 'actions', + name: 'tableActions', + actions: [ + { + title: t('customer.summary.goToLines'), + icon: 'vn:lines', + action: ({ id }) => router.push({ params: { id }, name: 'TicketSale' }), + isPrimary: true, + }, + { + title: t('components.smartCard.viewSummary'), + icon: 'preview', + isPrimary: true, + action: (row) => viewSummary(row.id, TicketSummary), + }, + ], }, ]); @@ -156,84 +122,90 @@ const setTotalPriceColor = (ticket) => { if (total > 0 && total < 50) return 'warning'; }; -const navigateToticketSummary = (id) => { - router.push({ - name: 'TicketSummary', - params: { id }, - }); +const setShippedColor = (date) => { + const today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + + const ticketShipped = new Date(date); + ticketShipped.setHours(0, 0, 0, 0); + + const difference = today - ticketShipped; + + if (difference == 0) return 'warning'; + if (difference < 0) return 'success'; }; -const commonColumns = (col) => ['date', 'state', 'total'].includes(col); </script> <template> - <FetchData + <VnTable + data-key="CustomerTickets" :filter="filter" - @on-fetch="(data) => (rows = data)" - auto-load + :right-search="false" + :column-search="false" url="Tickets" - /> - <QCard class="vn-one q-py-sm flex justify-between"> - <QTable - :columns="columns" - :pagination="{ rowsPerPage: 12 }" - :rows="rows" - class="full-width" - row-key="id" - > - <template #body-cell="props"> - <QTd :props="props" @click="navigateToticketSummary(props.row.id)"> - <QTr :props="props" class="cursor-pointer"> - <component - :is="tableColumnComponents[props.col.name].component" - @click="tableColumnComponents[props.col.name].event(props)" - class="rounded-borders" - v-bind="tableColumnComponents[props.col.name].props(props)" - > - <template v-if="!commonColumns(props.col.name)"> - <span - :class="{ - link: - props.col.name === 'route' || - props.col.name === 'nickname', - }" - > - {{ props.value }} - </span> - </template> - <template v-if="props.col.name === 'date'"> - <QBadge class="q-pa-sm" color="warning"> - {{ props.value }} - </QBadge> - </template> - <template v-if="props.col.name === 'state'"> - <QBadge :color="setStateColor(props.row)" class="q-pa-sm"> - {{ props.value }} - </QBadge> - </template> - <template v-if="props.col.name === 'total'"> - <QBadge - :color="setTotalPriceColor(props.row)" - class="q-pa-sm" - v-if="setTotalPriceColor(props.row)" - > - {{ toCurrency(props.value) }} - </QBadge> - <div v-else>{{ toCurrency(props.value) }}</div> - </template> - <CustomerDescriptorProxy - :id="props.row.clientFk" - v-if="props.col.name === 'nickname'" - /> - <RouteDescriptorProxy - :id="props.row.routeFk" - v-if="props.col.name === 'route'" - /> - </component> - </QTr> - </QTd> - </template> - </QTable> - </QCard> + :columns="columns" + search-url="tickets" + :without-header="true" + auto-load + order="shipped DESC, id" + :disable-option="{ card: true, table: true }" + limit="5" + class="full-width" + :disable-infinite-scroll="true" + > + <template #column-nickname="{ row }"> + <span class="link"> + {{ row.nickname }} + <CustomerDescriptorProxy :id="row.clientFk" /> + </span> + </template> + + <template #column-routeFk="{ row }"> + <span class="link"> + {{ row.routeFk }} + <RouteDescriptorProxy :id="row.routeFk" /> + </span> + </template> + <template #column-total="{ row }"> + <QBadge + class="q-pa-sm" + v-if="setTotalPriceColor(row)" + :color="setTotalPriceColor(row)" + text-color="black" + > + {{ toCurrency(row.totalWithVat) }} + </QBadge> + <span v-else> {{ toCurrency(row.totalWithVat) }}</span> + </template> + <template #column-state="{ row }"> + <span v-if="row.invoiceOut"> + <span :class="{ link: row.invoiceOut.ref }"> + {{ row.invoiceOut.ref }} + <InvoiceOutDescriptorProxy :id="row.invoiceOut.id" /> + </span> + </span> + <QBadge + class="q-pa-sm" + :color="setStateColor(row)" + text-color="black" + v-else-if="setStateColor(row)" + > + {{ row?.ticketState?.state.name }} + </QBadge> + <span v-else> {{ row?.ticketState?.state.name }}</span> + </template> + <template #column-shipped="{ row }"> + <QBadge + class="q-pa-sm" + :color="setShippedColor(row.shipped)" + text-color="black" + v-if="setShippedColor(row.shipped)" + > + {{ toDateFormat(row.shipped) }} + </QBadge> + <span v-else> {{ toDateFormat(row.shipped) }}</span> + </template> + </VnTable> </template> <i18n> diff --git a/src/pages/Customer/components/CustomerSummaryTableActions.vue b/src/pages/Customer/components/CustomerSummaryTableActions.vue deleted file mode 100644 index 6e9038374..000000000 --- a/src/pages/Customer/components/CustomerSummaryTableActions.vue +++ /dev/null @@ -1,44 +0,0 @@ -<script setup> -import { useI18n } from 'vue-i18n'; - -import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue'; - -const { t } = useI18n(); - -defineProps({ - id: { - type: Number, - required: true, - }, -}); - -const { viewSummary } = useSummaryDialog(); -</script> - -<template> - <div> - <QIcon color="primary" name="vn:lines" size="sm"> - <QTooltip> - {{ t('Go to lines') }} - </QTooltip> - </QIcon> - <QIcon - @click.stop="viewSummary(id, TicketSummary)" - class="q-ml-md" - color="primary" - name="preview" - size="sm" - > - <QTooltip> - {{ t('Preview') }} - </QTooltip> - </QIcon> - </div> -</template> - -<i18n> -es: - Go to lines: Ir a lineas - Preview: Vista previa -</i18n> diff --git a/src/pages/Customer/locale/en.yml b/src/pages/Customer/locale/en.yml index 6eb7cfa85..545c3f274 100644 --- a/src/pages/Customer/locale/en.yml +++ b/src/pages/Customer/locale/en.yml @@ -2,3 +2,135 @@ customerFilter: filter: name: Name socialName: Social name +customer: + list: + phone: Phone + email: Email + customerOrders: Display customer orders + moreOptions: More options + card: + customerList: Customer list + customerId: Claim ID + salesPerson: Sales person + credit: Credit + risk: Risk + securedCredit: Secured credit + payMethod: Pay method + debt: Debt + isFrozen: Customer frozen + hasDebt: Customer has debt + isDisabled: Customer inactive + notChecked: Customer no checked + webAccountInactive: Web account inactive + noWebAccess: Web access is disabled + businessType: Business type + passwordRequirements: 'The password must have at least { length } length characters, {nAlpha} alphabetic characters, {nUpper} capital letters, {nDigits} digits and {nPunct} symbols (Ex: $%&.)\n' + businessTypeFk: Business type + summary: + basicData: Basic data + fiscalAddress: Fiscal address + fiscalData: Fiscal data + billingData: Billing data + consignee: Default consignee + businessData: Business data + financialData: Financial data + customerId: Customer ID + name: Name + contact: Contact + phone: Phone + mobile: Mobile + email: Email + salesPerson: Sales person + contactChannel: Contact channel + socialName: Social name + fiscalId: Fiscal ID + postcode: Postcode + province: Province + country: Country + street: Address + isEqualizated: Is equalizated + isActive: Is active + invoiceByAddress: Invoice by address + verifiedData: Verified data + hasToInvoice: Has to invoice + notifyByEmail: Notify by email + vies: VIES + payMethod: Pay method + bankAccount: Bank account + dueDay: Due day + hasLcr: Has LCR + hasCoreVnl: Has core VNL + hasB2BVnl: Has B2B VNL + addressName: Address name + addressCity: City + addressStreet: Street + username: Username + webAccess: Web access + totalGreuge: Total greuge + mana: Mana + priceIncreasingRate: Price increasing rate + averageInvoiced: Average invoiced + claimRate: Claming rate + payMethodFk: Billing data + risk: Risk + maximumRisk: Solunion's maximum risk + riskInfo: Invoices minus payments plus orders not yet invoiced + credit: Credit + creditInfo: Company's maximum risk + securedCredit: Secured credit + securedCreditInfo: Solunion's maximum risk + balance: Balance + balanceInfo: Invoices minus payments + balanceDue: Balance due + balanceDueInfo: Deviated invoices minus payments + recoverySince: Recovery since + businessType: Business Type + city: City + descriptorInfo: Invoices minus payments plus orders not yet + rating: Rating + recommendCredit: Recommended credit + goToLines: Go to lines + basicData: + socialName: Fiscal name + businessType: Business type + contact: Contact + youCanSaveMultipleEmails: You can save multiple emails + email: Email + phone: Phone + mobile: Mobile + salesPerson: Sales person + contactChannel: Contact channel + previousClient: Previous client + extendedList: + tableVisibleColumns: + id: Identifier + name: Name + socialName: Social name + fi: Tax number + salesPersonFk: Salesperson + credit: Credit + creditInsurance: Credit insurance + phone: Phone + mobile: Mobile + street: Street + countryFk: Country + provinceFk: Province + city: City + postcode: Postcode + email: Email + created: Created + businessTypeFk: Business type + payMethodFk: Billing data + sageTaxTypeFk: Sage tax type + sageTransactionTypeFk: Sage tr. type + isActive: Active + isVies: Vies + isTaxDataChecked: Verified data + isEqualizated: Is equalizated + isFreezed: Freezed + hasToInvoice: Invoice + hasToInvoiceByAddress: Invoice by address + isToBeMailed: Mailing + hasLcr: Received LCR + hasCoreVnl: VNL core received + hasSepaVnl: VNL B2B received diff --git a/src/pages/Customer/locale/es.yml b/src/pages/Customer/locale/es.yml index 111696f21..4fcbe3fa2 100644 --- a/src/pages/Customer/locale/es.yml +++ b/src/pages/Customer/locale/es.yml @@ -4,3 +4,134 @@ customerFilter: filter: name: Nombre socialName: Razón Social +customer: + list: + phone: Teléfono + email: Email + customerOrders: Mostrar órdenes del cliente + moreOptions: Más opciones + card: + customerId: ID cliente + salesPerson: Comercial + credit: Crédito + risk: Riesgo + securedCredit: Crédito asegurado + payMethod: Método de pago + debt: Riesgo + isFrozen: Cliente congelado + hasDebt: Cliente con riesgo + isDisabled: Cliente inactivo + notChecked: Cliente no comprobado + webAccountInactive: Sin acceso web + noWebAccess: El acceso web está desactivado + businessType: Tipo de negocio + passwordRequirements: 'La contraseña debe tener al menos { length } caracteres de longitud, {nAlpha} caracteres alfabéticos, {nUpper} letras mayúsculas, {nDigits} dígitos y {nPunct} símbolos (Ej: $%&.)' + businessTypeFk: Tipo de negocio + summary: + basicData: Datos básicos + fiscalAddress: Dirección fiscal + fiscalData: Datos fiscales + billingData: Datos de facturación + consignee: Consignatario pred. + businessData: Datos comerciales + financialData: Datos financieros + customerId: ID cliente + name: Nombre + contact: Contacto + phone: Teléfono + mobile: Móvil + email: Email + salesPerson: Comercial + contactChannel: Canal de contacto + socialName: Razón social + fiscalId: NIF/CIF + postcode: Código postal + province: Provincia + country: País + street: Calle + isEqualizated: Recargo de equivalencia + isActive: Activo + invoiceByAddress: Facturar por consignatario + verifiedData: Datos verificados + hasToInvoice: Facturar + notifyByEmail: Notificar por email + vies: VIES + payMethod: Método de pago + bankAccount: Cuenta bancaria + dueDay: Día de pago + hasLcr: Recibido LCR + hasCoreVnl: Recibido core VNL + hasB2BVnl: Recibido B2B VNL + addressName: Nombre de la dirección + addressCity: Ciudad + addressStreet: Calle + username: Usuario + webAccess: Acceso web + totalGreuge: Greuge total + mana: Maná + priceIncreasingRate: Ratio de incremento de precio + averageInvoiced: Facturación media + claimRate: Ratio de reclamaciones + maximumRisk: Riesgo máximo asumido por Solunion + payMethodFk: Forma de pago + risk: Riesgo + riskInfo: Facturas menos recibos mas pedidos sin facturar + credit: Crédito + creditInfo: Riesgo máximo asumido por la empresa + securedCredit: Crédito asegurado + securedCreditInfo: Riesgo máximo asumido por Solunion + balance: Balance + balanceInfo: Facturas menos recibos + balanceDue: Saldo vencido + balanceDueInfo: Facturas fuera de plazo menos recibos + recoverySince: Recobro desde + businessType: Tipo de negocio + city: Población + descriptorInfo: Facturas menos recibos mas pedidos sin facturar + rating: Clasificación + recommendCredit: Crédito recomendado + goToLines: Ir a líneas + basicData: + socialName: Nombre fiscal + businessType: Tipo de negocio + contact: Contacto + youCanSaveMultipleEmails: Puede guardar varios correos electrónicos encadenándolos mediante comas sin espacios{','} ejemplo{':'} user{'@'}dominio{'.'}com, user2{'@'}dominio{'.'}com siendo el primer correo electrónico el principal + email: Email + phone: Teléfono + mobile: Móvil + salesPerson: Comercial + contactChannel: Canal de contacto + previousClient: Cliente anterior + extendedList: + tableVisibleColumns: + id: Identificador + name: Nombre + socialName: Razón social + fi: NIF / CIF + salesPersonFk: Comercial + credit: Crédito + creditInsurance: Crédito asegurado + phone: Teléfono + mobile: Móvil + street: Dirección fiscal + countryFk: País + provinceFk: Provincia + city: Población + postcode: Código postal + email: Email + created: Fecha creación + businessTypeFk: Tipo de negocio + payMethodFk: Forma de pago + sageTaxTypeFk: Tipo de impuesto Sage + sageTransactionTypeFk: Tipo tr. sage + isActive: Activo + isVies: Vies + isTaxDataChecked: Datos comprobados + isEqualizated: Recargo de equivalencias + isFreezed: Congelado + hasToInvoice: Factura + hasToInvoiceByAddress: Factura por consigna + isToBeMailed: Env. emails + hasLcr: Recibido LCR + hasCoreVnl: Recibido core VNL + hasSepaVnl: Recibido B2B VNL diff --git a/src/pages/Dashboard/DashboardMain.vue b/src/pages/Dashboard/DashboardMain.vue index 6da39ce25..56054156a 100644 --- a/src/pages/Dashboard/DashboardMain.vue +++ b/src/pages/Dashboard/DashboardMain.vue @@ -55,6 +55,15 @@ const pinnedModules = computed(() => navigation.getPinnedModules()); > <div class="text-center text-primary button-text"> {{ t(item.title) }} + <div v-if="item.keyBinding"> + {{ '(' + item.keyBinding + ')' }} + <QTooltip> + {{ + 'Ctrl + Alt + ' + + item?.keyBinding?.toUpperCase() + }} + </QTooltip> + </div> </div> </QBtn> </div> diff --git a/src/pages/Department/Card/DepartmentBasicData.vue b/src/pages/Department/Card/DepartmentBasicData.vue index 4573f7b66..98abfd6b9 100644 --- a/src/pages/Department/Card/DepartmentBasicData.vue +++ b/src/pages/Department/Card/DepartmentBasicData.vue @@ -16,12 +16,12 @@ const workersOptions = ref([]); const clientsOptions = ref([]); </script> <template> - <fetch-data + <FetchData url="Workers/search" @on-fetch="(data) => (workersOptions = data)" auto-load /> - <fetch-data url="Clients" @on-fetch="(data) => (clientsOptions = data)" auto-load /> + <FetchData url="Clients" @on-fetch="(data) => (clientsOptions = data)" auto-load /> <FormModel :url="`Departments/${route.params.id}`" model="department" diff --git a/src/pages/Entry/Card/EntryBasicData.vue b/src/pages/Entry/Card/EntryBasicData.vue index e5d6f50d9..b81b1db22 100644 --- a/src/pages/Entry/Card/EntryBasicData.vue +++ b/src/pages/Entry/Card/EntryBasicData.vue @@ -19,8 +19,6 @@ const { t } = useI18n(); const { hasAny } = useRole(); const isAdministrative = () => hasAny(['administrative']); -const suppliersOptions = ref([]); -const travelsOptions = ref([]); const companiesOptions = ref([]); const currenciesOptions = ref([]); @@ -29,20 +27,6 @@ const onFilterTravelSelected = (formData, id) => { }; </script> <template> - <FetchData - url="Suppliers" - :filter="{ fields: ['id', 'nickname'] }" - order="nickname" - @on-fetch="(data) => (suppliersOptions = data)" - auto-load - /> - <FetchData - url="Travels/filter" - :filter="{ fields: ['id', 'warehouseInName'] }" - order="id" - @on-fetch="(data) => (travelsOptions = data)" - auto-load - /> <FetchData ref="companiesRef" url="Companies" @@ -71,9 +55,10 @@ const onFilterTravelSelected = (formData, id) => { <VnSelect :label="t('entry.basicData.supplier')" v-model="data.supplierFk" - :options="suppliersOptions" + url="Suppliers" option-value="id" option-label="nickname" + :fields="['id', 'nickname']" hide-selected :required="true" map-options @@ -92,7 +77,8 @@ const onFilterTravelSelected = (formData, id) => { <VnSelectDialog :label="t('entry.basicData.travel')" v-model="data.travelFk" - :options="travelsOptions" + url="Travels/filter" + :fields="['id', 'warehouseInName']" option-value="id" option-label="warehouseInName" map-options diff --git a/src/pages/Entry/Card/EntryBuys.vue b/src/pages/Entry/Card/EntryBuys.vue index 6e66f4ce7..ff89faada 100644 --- a/src/pages/Entry/Card/EntryBuys.vue +++ b/src/pages/Entry/Card/EntryBuys.vue @@ -26,7 +26,6 @@ const { notify } = useNotify(); const rowsSelected = ref([]); const entryBuysPaginateRef = ref(null); -const packagingsOptions = ref(null); const originalRowDataCopy = ref(null); const getInputEvents = (colField, props) => { @@ -66,7 +65,10 @@ const tableColumnComponents = computed(() => ({ 'map-options': true, 'use-input': true, 'hide-selected': true, - options: packagingsOptions.value, + url: 'Packagings', + fields: ['id'], + where: { freightItemFk: true }, + 'sort-by': 'id ASC', dense: true, }, event: getInputEvents, @@ -304,13 +306,6 @@ const lockIconType = (groupingMode, mode) => { </script> <template> - <FetchData - ref="expensesRef" - url="Packagings" - :filter="{ fields: ['id'], where: { freightItemFk: true }, order: 'id ASC' }" - auto-load - @on-fetch="(data) => (packagingsOptions = data)" - /> <VnSubToolbar> <template #st-actions> <QBtnGroup push style="column-gap: 10px"> @@ -423,7 +418,7 @@ const lockIconType = (groupingMode, mode) => { <span v-if="props.row.item.subName" class="subName"> {{ props.row.item.subName }} </span> - <FetchedTags :item="props.row.item" :max-length="5" /> + <FetchedTags :item="props.row.item" /> </QTd> </QTr> </template> diff --git a/src/pages/Entry/Card/EntrySummary.vue b/src/pages/Entry/Card/EntrySummary.vue index b32dc70a9..379be1d2f 100644 --- a/src/pages/Entry/Card/EntrySummary.vue +++ b/src/pages/Entry/Card/EntrySummary.vue @@ -319,7 +319,7 @@ const fetchEntryBuys = async () => { <span v-if="row.item.subName" class="subName"> {{ row.item.subName }} </span> - <FetchedTags :item="row.item" :max-length="5" /> + <FetchedTags :item="row.item" /> </QTd> </QTr> <!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys --> diff --git a/src/pages/Entry/EntryFilter.vue b/src/pages/Entry/EntryFilter.vue index 194ce0a28..3b88072fa 100644 --- a/src/pages/Entry/EntryFilter.vue +++ b/src/pages/Entry/EntryFilter.vue @@ -20,7 +20,6 @@ const props = defineProps({ const currenciesOptions = ref([]); const companiesOptions = ref([]); -const suppliersOptions = ref([]); const stateStore = useStateStore(); onMounted(async () => { @@ -45,14 +44,6 @@ onMounted(async () => { @on-fetch="(data) => (currenciesOptions = data)" auto-load /> - <FetchData - url="Suppliers" - :filter="{ fields: ['id', 'nickname', 'name'] }" - order="nickname" - @on-fetch="(data) => (suppliersOptions = data)" - auto-load - /> - <VnFilterPanel :data-key="props.dataKey" :search-button="true"> <template #tags="{ tag, formatFn }"> <div class="q-gutter-x-xs"> @@ -135,9 +126,11 @@ onMounted(async () => { :label="t('params.supplierFk')" v-model="params.supplierFk" @update:model-value="searchFn()" - :options="suppliersOptions" + url="Suppliers" option-value="id" option-label="name" + :fields="['id', 'name', 'nickname']" + sort-by="nickname" hide-selected dense outlined diff --git a/src/pages/Entry/EntryList.vue b/src/pages/Entry/EntryList.vue index bd5ace677..6f7ff1935 100644 --- a/src/pages/Entry/EntryList.vue +++ b/src/pages/Entry/EntryList.vue @@ -7,11 +7,16 @@ import { useStateStore } from 'stores/useStateStore'; import VnTable from 'components/VnTable/VnTable.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import { toDate } from 'src/filters'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import EntrySummary from './Card/EntrySummary.vue'; +import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; +import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue'; const stateStore = useStateStore(); const { t } = useI18n(); const tableRef = ref(); +const { viewSummary } = useSummaryDialog(); const entryFilter = { include: [ { @@ -142,6 +147,12 @@ const columns = computed(() => [ create: true, format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), }, + { + align: 'left', + label: t('entry.list.tableVisibleColumns.invoiceAmount'), + name: 'invoiceAmount', + cardVisible: true, + }, { align: 'left', label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), @@ -168,6 +179,18 @@ const columns = computed(() => [ inWhere: true, }, }, + { + align: 'right', + name: 'tableActions', + actions: [ + { + title: t('components.smartCard.viewSummary'), + icon: 'preview', + action: (row) => viewSummary(row.id, EntrySummary), + isPrimary: true, + }, + ], + }, ]); onMounted(async () => { stateStore.rightDrawer = true; @@ -201,7 +224,20 @@ onMounted(async () => { redirect="entry" auto-load :right-search="false" - /> + > + <template #column-supplierFk="{ row }"> + <span class="link" @click.stop> + {{ row.supplierName }} + <SupplierDescriptorProxy :id="row.supplierFk" /> + </span> + </template> + <template #column-travelFk="{ row }"> + <span class="link" @click.stop> + {{ row.travelRef }} + <TravelDescriptorProxy :id="row.travelFk" /> + </span> + </template> + </VnTable> </template> <i18n> diff --git a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue index ede9af825..045517a3f 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue @@ -223,6 +223,10 @@ async function onSubmit() { autofocus /> </VnRow> + <VnRow> + <VnInputDate :label="t('Entry date')" v-model="data.bookEntried" /> + <VnInputDate :label="t('Accounted date')" v-model="data.booked" /> + </VnRow> <VnRow> <VnSelect :label="t('Undeductible VAT')" @@ -285,10 +289,6 @@ async function onSubmit() { </template> </VnInput> </VnRow> - <VnRow> - <VnInputDate :label="t('Entry date')" v-model="data.bookEntried" /> - <VnInputDate :label="t('Accounted date')" v-model="data.booked" /> - </VnRow> <VnRow> <VnSelect :label="t('Currency')" diff --git a/src/pages/InvoiceIn/Card/InvoiceInCard.vue b/src/pages/InvoiceIn/Card/InvoiceInCard.vue index a95f289b0..0fe2a2368 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInCard.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInCard.vue @@ -3,6 +3,8 @@ import VnCard from 'components/common/VnCard.vue'; import InvoiceInDescriptor from './InvoiceInDescriptor.vue'; import InvoiceInFilter from '../InvoiceInFilter.vue'; import InvoiceInSearchbar from '../InvoiceInSearchbar.vue'; +import { onBeforeRouteUpdate } from 'vue-router'; +import { setRectificative } from '../composables/setRectificative'; const filter = { include: [ @@ -20,6 +22,8 @@ const filter = { { relation: 'currency' }, ], }; + +onBeforeRouteUpdate(async (to) => await setRectificative(to)); </script> <template> <VnCard diff --git a/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue b/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue index cba2a31d2..9bc4856a8 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue @@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; import axios from 'axios'; import { toCurrency, toDate } from 'src/filters'; -import { useRole } from 'src/composables/useRole'; +import { useAcl } from 'src/composables/useAcl'; import { downloadFile } from 'src/composables/downloadFile'; import { useArrayData } from 'src/composables/useArrayData'; import { usePrintService } from 'composables/usePrintService'; @@ -24,7 +24,7 @@ const $props = defineProps({ id: { type: Number, default: null } }); const { push, currentRoute } = useRouter(); const quasar = useQuasar(); -const { hasAny } = useRole(); +const { hasAny } = useAcl(); const { t } = useI18n(); const { openReport, sendEmail } = usePrintService(); const arrayData = useArrayData(); @@ -195,7 +195,8 @@ async function cloneInvoice() { push({ path: `/invoice-in/${data.id}/summary` }); } -const isAdministrative = () => hasAny(['administrative']); +const canEditProp = (props) => + hasAny([{ model: 'InvoiceIn', props, accessType: 'WRITE' }]); const isAgricultural = () => { if (!config.value) return false; @@ -283,7 +284,7 @@ const createInvoiceInCorrection = async () => { <InvoiceInToBook> <template #content="{ book }"> <QItem - v-if="!entity?.isBooked && isAdministrative()" + v-if="!entity?.isBooked && canEditProp('toBook')" v-ripple clickable @click="book(entityId)" @@ -293,7 +294,7 @@ const createInvoiceInCorrection = async () => { </template> </InvoiceInToBook> <QItem - v-if="entity?.isBooked && isAdministrative()" + v-if="entity?.isBooked && canEditProp('toUnbook')" v-ripple clickable @click="triggerMenu('unbook')" @@ -303,7 +304,7 @@ const createInvoiceInCorrection = async () => { </QItemSection> </QItem> <QItem - v-if="isAdministrative()" + v-if="canEditProp('deleteById')" v-ripple clickable @click="triggerMenu('delete')" @@ -311,7 +312,7 @@ const createInvoiceInCorrection = async () => { <QItemSection>{{ t('Delete invoice') }}</QItemSection> </QItem> <QItem - v-if="isAdministrative()" + v-if="canEditProp('clone')" v-ripple clickable @click="triggerMenu('clone')" @@ -356,10 +357,7 @@ const createInvoiceInCorrection = async () => { <template #body="{ entity }"> <VnLv :label="t('invoiceIn.card.issued')" :value="toDate(entity.issued)" /> <VnLv :label="t('invoiceIn.summary.booked')" :value="toDate(entity.booked)" /> - <VnLv - :label="t('invoiceIn.card.amount')" - :value="toCurrency(totalAmount, entity.currency?.code)" - /> + <VnLv :label="t('invoiceIn.card.amount')" :value="toCurrency(totalAmount)" /> <VnLv :label="t('invoiceIn.summary.supplier')"> <template #value> <span class="link"> diff --git a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue index 7dbd0fe9e..1593ea1be 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue @@ -5,10 +5,10 @@ import { useI18n } from 'vue-i18n'; import axios from 'axios'; import { toDate } from 'src/filters'; import { useArrayData } from 'src/composables/useArrayData'; +import { getTotal } from 'src/composables/getTotal'; import CrudModel from 'src/components/CrudModel.vue'; import FetchData from 'src/components/FetchData.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; -import { toCurrency } from 'src/filters'; import useNotify from 'src/composables/useNotify.js'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; @@ -18,6 +18,7 @@ const { notify } = useNotify(); const { t } = useI18n(); const arrayData = useArrayData(); const invoiceIn = computed(() => arrayData.store.data); +const currency = computed(() => invoiceIn.value?.currency?.code); const rowsSelected = ref([]); const banks = ref([]); @@ -71,7 +72,6 @@ async function insert() { await invoiceInFormRef.value.reload(); notify(t('globals.dataSaved'), 'positive'); } -const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, 0); </script> <template> <FetchData @@ -139,9 +139,9 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, <QTd> <VnInputNumber :class="{ - 'no-pointer-events': !isNotEuro(invoiceIn.currency.code), + 'no-pointer-events': !isNotEuro(currency), }" - :disable="!isNotEuro(invoiceIn.currency.code)" + :disable="!isNotEuro(currency)" v-model="row.foreignValue" clearable clear-icon="close" @@ -154,11 +154,17 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, <QTd /> <QTd /> <QTd> - {{ - toCurrency(getTotalAmount(rows), invoiceIn.currency.code) - }} + {{ getTotal(rows, 'amount', { currency: 'default' }) }} + </QTd> + <QTd> + <template v-if="isNotEuro(invoiceIn.currency.code)"> + {{ + getTotal(rows, 'foreignValue', { + currency: invoiceIn.currency.code, + }) + }} + </template> </QTd> - <QTd /> </QTr> </template> <template #item="props"> @@ -208,11 +214,9 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, :label="t('Foreign value')" class="full-width" :class="{ - 'no-pointer-events': !isNotEuro( - invoiceIn.currency.code - ), + 'no-pointer-events': !isNotEuro(currency), }" - :disable="!isNotEuro(invoiceIn.currency.code)" + :disable="!isNotEuro(currency)" v-model="props.row.foreignValue" clearable clear-icon="close" diff --git a/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue b/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue index 481698832..717f30b7f 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue @@ -2,18 +2,15 @@ import { computed, ref } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { toCurrency } from 'src/filters'; +import { getTotal } from 'src/composables/getTotal'; import CrudModel from 'src/components/CrudModel.vue'; import FetchData from 'src/components/FetchData.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; -import { useArrayData } from 'src/composables/useArrayData'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; const { t } = useI18n(); const route = useRoute(); -const arrayData = useArrayData(); -const currency = computed(() => arrayData.store.data?.currency?.code); const invoceInIntrastat = ref([]); const rowsSelected = ref([]); const countries = ref([]); @@ -72,9 +69,6 @@ const columns = computed(() => [ }, ]); -const getTotal = (data, key) => - data.reduce((acc, cur) => acc + +String(cur[key] || 0).replace(',', '.'), 0); - const formatOpt = (row, { model, options }, prop) => { const obj = row[model]; const option = options.find(({ id }) => id == obj); @@ -154,7 +148,7 @@ const formatOpt = (row, { model, options }, prop) => { <QTd /> <QTd /> <QTd> - {{ toCurrency(getTotal(rows, 'amount'), currency) }} + {{ getTotal(rows, 'amount', { currency: 'default' }) }} </QTd> <QTd> {{ getTotal(rows, 'net') }} diff --git a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue index 644b472e2..bf2e7db48 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue @@ -35,7 +35,7 @@ const vatColumns = ref([ name: 'landed', label: 'invoiceIn.summary.taxableBase', field: (row) => row.taxableBase, - format: (value) => toCurrency(value, currency.value), + format: (value) => toCurrency(value), sortable: true, align: 'left', }, @@ -64,7 +64,7 @@ const vatColumns = ref([ name: 'rate', label: 'invoiceIn.summary.rate', field: (row) => taxRate(row.taxableBase, row.taxTypeSage?.rate), - format: (value) => toCurrency(value, currency.value), + format: (value) => toCurrency(value), sortable: true, align: 'left', }, @@ -72,7 +72,7 @@ const vatColumns = ref([ name: 'currency', label: 'invoiceIn.summary.currency', field: (row) => row.foreignValue, - format: (value) => value, + format: (val) => val && toCurrency(val, currency.value), sortable: true, align: 'left', }, @@ -97,7 +97,7 @@ const dueDayColumns = ref([ name: 'amount', label: 'invoiceIn.summary.amount', field: (row) => row.amount, - format: (value) => toCurrency(value, currency.value), + format: (value) => toCurrency(value), sortable: true, align: 'left', }, @@ -105,7 +105,7 @@ const dueDayColumns = ref([ name: 'landed', label: 'invoiceIn.summary.foreignValue', field: (row) => row.foreignValue, - format: (value) => value, + format: (val) => val && toCurrency(val, currency.value), sortable: true, align: 'left', }, @@ -124,7 +124,7 @@ const intrastatColumns = ref([ { name: 'amount', label: 'invoiceIn.summary.amount', - field: (row) => toCurrency(row.amount, currency.value), + field: (row) => toCurrency(row.amount), sortable: true, align: 'left', }, @@ -179,7 +179,6 @@ const getTotalTax = (tax) => const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; </script> - <template> <CardSummary data-key="InvoiceInSummary" @@ -229,10 +228,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; :label="t('invoiceIn.summary.currency')" :value="entity.currency?.code" /> - <VnLv - :label="t('invoiceIn.summary.docNumber')" - :value="`${entity.serial}/${entity.serialNumber}`" - /> + <VnLv :label="t('invoiceIn.serial')" :value="`${entity.serial}`" /> </QCard> <QCard class="vn-one"> <QCardSection class="q-pa-none"> @@ -293,12 +289,9 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; <QCardSection class="q-pa-none"> <VnLv :label="t('invoiceIn.summary.taxableBase')" - :value="toCurrency(entity.totals.totalTaxableBase, currency)" - /> - <VnLv - label="Total" - :value="toCurrency(entity.totals.totalVat, currency)" + :value="toCurrency(entity.totals.totalTaxableBase)" /> + <VnLv label="Total" :value="toCurrency(entity.totals.totalVat)" /> <VnLv :label="t('invoiceIn.summary.dueTotal')"> <template #value> <QChip @@ -311,7 +304,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; : t('invoiceIn.summary.dueTotal') " > - {{ toCurrency(entity.totals.totalDueDay, currency) }} + {{ toCurrency(entity.totals.totalDueDay) }} </QChip> </template> </VnLv> @@ -350,15 +343,17 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; <template #bottom-row> <QTr class="bg"> <QTd></QTd> + <QTd>{{ toCurrency(entity.totals.totalTaxableBase) }}</QTd> + <QTd></QTd> + <QTd></QTd> + <QTd>{{ toCurrency(getTotalTax(entity.invoiceInTax)) }}</QTd> <QTd>{{ - toCurrency(entity.totals.totalTaxableBase, currency) + entity.totals.totalTaxableBaseForeignValue && + toCurrency( + entity.totals.totalTaxableBaseForeignValue, + currency + ) }}</QTd> - <QTd></QTd> - <QTd></QTd> - <QTd>{{ - toCurrency(getTotalTax(entity.invoiceInTax, currency)) - }}</QTd> - <QTd></QTd> </QTr> </template> </QTable> @@ -384,9 +379,17 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; <QTd></QTd> <QTd></QTd> <QTd> - {{ toCurrency(entity.totals.totalDueDay, currency) }} + {{ toCurrency(entity.totals.totalDueDay) }} + </QTd> + <QTd> + {{ + entity.totals.totalDueDayForeignValue && + toCurrency( + entity.totals.totalDueDayForeignValue, + currency + ) + }} </QTd> - <QTd></QTd> </QTr> </template> </QTable> @@ -421,7 +424,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; <template #bottom-row> <QTr class="bg"> <QTd></QTd> - <QTd>{{ toCurrency(intrastatTotals.amount, currency) }}</QTd> + <QTd>{{ toCurrency(intrastatTotals.amount) }}</QTd> <QTd>{{ intrastatTotals.net }}</QTd> <QTd>{{ intrastatTotals.stems }}</QTd> <QTd></QTd> diff --git a/src/pages/InvoiceIn/Card/InvoiceInVat.vue b/src/pages/InvoiceIn/Card/InvoiceInVat.vue index 4dac5058e..d44880937 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInVat.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInVat.vue @@ -2,18 +2,17 @@ import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { useQuasar } from 'quasar'; -import axios from 'axios'; import { useArrayData } from 'src/composables/useArrayData'; +import { getTotal } from 'src/composables/getTotal'; import { toCurrency } from 'src/filters'; import FetchData from 'src/components/FetchData.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import CrudModel from 'src/components/CrudModel.vue'; -import VnInput from 'src/components/common/VnInput.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; +import CreateNewExpenseForm from 'src/components/CreateNewExpenseForm.vue'; const { t } = useI18n(); -const quasar = useQuasar(); const arrayData = useArrayData(); const invoiceIn = computed(() => arrayData.store.data); @@ -23,15 +22,7 @@ const expenses = ref([]); const sageTaxTypes = ref([]); const sageTransactionTypes = ref([]); const rowsSelected = ref([]); -const newExpense = ref({ - code: undefined, - isWithheld: false, - description: undefined, -}); - const invoiceInFormRef = ref(); -const expensesRef = ref(); -const newExpenseRef = ref(); defineProps({ actionIcon: { @@ -56,7 +47,7 @@ const columns = computed(() => [ { name: 'taxablebase', label: t('Taxable base'), - field: (row) => toCurrency(row.taxableBase, currency.value), + field: (row) => row.taxableBase, model: 'taxableBase', sortable: true, tabIndex: 2, @@ -91,7 +82,7 @@ const columns = computed(() => [ label: t('Rate'), sortable: true, tabIndex: 5, - field: (row) => toCurrency(taxRate(row, row.taxTypeSageFk), currency.value), + field: (row) => taxRate(row, row.taxTypeSageFk), align: 'left', }, { @@ -132,40 +123,6 @@ function taxRate(invoiceInTax) { return (taxTypeSage / 100) * taxableBase; } -async function addExpense() { - try { - if (!newExpense.value.code) throw new Error(t(`The code can't be empty`)); - if (isNaN(newExpense.value.code)) - throw new Error(t(`The code have to be a number`)); - if (!newExpense.value.description) - throw new Error(t(`The description can't be empty`)); - - const data = [ - { - id: newExpense.value.code, - isWithheld: newExpense.value.isWithheld, - name: newExpense.value.description, - }, - ]; - - await axios.post(`Expenses`, data); - await expensesRef.value.fetch(); - quasar.notify({ - type: 'positive', - message: t('globals.dataSaved'), - }); - newExpenseRef.value.hide(); - } catch (error) { - quasar.notify({ - type: 'negative', - message: t(`${error.message}`), - }); - } -} -const getTotalTaxableBase = (rows) => - rows.reduce((acc, { taxableBase }) => acc + +(taxableBase || 0), 0); -const getTotalRate = (rows) => rows.reduce((acc, cur) => acc + +taxRate(cur), 0); - const formatOpt = (row, { model, options }, prop) => { const obj = row[model]; const option = options.find(({ id }) => id == obj); @@ -207,46 +164,35 @@ const formatOpt = (row, { model, options }, prop) => { > <template #body-cell-expense="{ row, col }"> <QTd> - <VnSelect + <VnSelectDialog v-model="row[col.model]" :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'name']" + :tooltip="t('Create a new expense')" > <template #option="scope"> <QItem v-bind="scope.itemProps"> {{ `${scope.opt.id}: ${scope.opt.name}` }} </QItem> </template> - <template #append> - <QIcon - name="close" - @click.stop="value = null" - class="cursor-pointer" - size="xs" + <template #form> + <CreateNewExpenseForm + @on-data-saved="$refs.expensesRef.fetch()" /> - <QIcon - @click.stop.prevent="newExpenseRef.show()" - :name="actionIcon" - size="xs" - class="default-icon" - > - <QTooltip> - {{ t('Create expense') }} - </QTooltip> - </QIcon> </template> - </VnSelect> + </VnSelectDialog> </QTd> </template> <template #body-cell-taxablebase="{ row }"> <QTd> + {{ currency }} <VnInputNumber :class="{ - 'no-pointer-events': isNotEuro(invoiceIn.currency.code), + 'no-pointer-events': isNotEuro(currency), }" - :disable="isNotEuro(invoiceIn.currency.code)" + :disable="isNotEuro(currency)" label="" clear-icon="close" v-model="row.taxableBase" @@ -312,9 +258,9 @@ const formatOpt = (row, { model, options }, prop) => { <QTd> <VnInputNumber :class="{ - 'no-pointer-events': !isNotEuro(invoiceIn.currency.code), + 'no-pointer-events': !isNotEuro(currency), }" - :disable="!isNotEuro(invoiceIn.currency.code)" + :disable="!isNotEuro(currency)" v-model="row.foreignValue" /> </QTd> @@ -324,12 +270,24 @@ const formatOpt = (row, { model, options }, prop) => { <QTd /> <QTd /> <QTd> - {{ toCurrency(getTotalTaxableBase(rows), currency) }} + {{ getTotal(rows, 'taxableBase', { currency: 'default' }) }} </QTd> <QTd /> <QTd /> - <QTd> {{ toCurrency(getTotalRate(rows), currency) }}</QTd> - <QTd /> + <QTd> + {{ + getTotal(rows, null, { cb: taxRate, currency: 'default' }) + }}</QTd + > + <QTd> + <template v-if="isNotEuro(invoiceIn.currency.code)"> + {{ + getTotal(rows, 'foreignValue', { + currency: invoiceIn.currency.code, + }) + }} + </template> + </QTd> </QTr> </template> <template #item="props"> @@ -341,7 +299,7 @@ const formatOpt = (row, { model, options }, prop) => { <QSeparator /> <QList> <QItem> - <VnSelect + <VnSelectDialog :label="t('Expense')" class="full-width" v-model="props.row['expenseFk']" @@ -349,24 +307,26 @@ const formatOpt = (row, { model, options }, prop) => { option-value="id" option-label="name" :filter-options="['id', 'name']" + :tooltip="t('Create a new expense')" > <template #option="scope"> <QItem v-bind="scope.itemProps"> {{ `${scope.opt.id}: ${scope.opt.name}` }} </QItem> </template> - </VnSelect> + <template #form> + <CreateNewExpenseForm /> + </template> + </VnSelectDialog> </QItem> <QItem> <VnInputNumber :label="t('Taxable base')" :class="{ - 'no-pointer-events': isNotEuro( - invoiceIn.currency.code - ), + 'no-pointer-events': isNotEuro(currency), }" class="full-width" - :disable="isNotEuro(invoiceIn.currency.code)" + :disable="isNotEuro(currency)" clear-icon="close" v-model="props.row.taxableBase" clearable @@ -427,11 +387,9 @@ const formatOpt = (row, { model, options }, prop) => { :label="t('Foreign value')" class="full-width" :class="{ - 'no-pointer-events': !isNotEuro( - invoiceIn.currency.code - ), + 'no-pointer-events': !isNotEuro(currency), }" - :disable="!isNotEuro(invoiceIn.currency.code)" + :disable="!isNotEuro(currency)" v-model="props.row.foreignValue" /> </QItem> @@ -442,44 +400,6 @@ const formatOpt = (row, { model, options }, prop) => { </QTable> </template> </CrudModel> - <QDialog ref="newExpenseRef"> - <QCard> - <QCardSection class="q-pb-none"> - <QItem class="q-pa-none"> - <span class="text-primary text-h6 full-width"> - <QIcon name="edit" class="q-mr-xs" /> - {{ t('New expense') }} - </span> - <QBtn icon="close" flat round dense v-close-popup /> - </QItem> - </QCardSection> - <QCardSection class="q-pt-none"> - <QItem> - <VnInput - :label="`${t('Code')}*`" - v-model="newExpense.code" - :required="true" - /> - <QCheckbox - dense - size="sm" - :label="`${t('It\'s a withholding')}`" - v-model="newExpense.isWithheld" - /> - </QItem> - <QItem> - <VnInput - :label="`${t('Descripction')}*`" - v-model="newExpense.description" - /> - </QItem> - </QCardSection> - <QCardActions class="justify-end"> - <QBtn flat :label="t('globals.close')" color="primary" v-close-popup /> - <QBtn :label="t('globals.save')" color="primary" @click="addExpense" /> - </QCardActions> - </QCard> - </QDialog> <QPageSticky position="bottom-right" :offset="[25, 25]"> <QBtn color="primary" @@ -487,7 +407,9 @@ const formatOpt = (row, { model, options }, prop) => { size="lg" round @click="invoiceInFormRef.insert()" - /> + > + <QTooltip>{{ t('Add tax') }}</QTooltip> + </QBtn> </QPageSticky> </template> @@ -527,18 +449,11 @@ const formatOpt = (row, { model, options }, prop) => { <i18n> es: Expense: Gasto - Create expense: Crear gasto + Create a new expense: Crear nuevo gasto Add tax: Crear gasto Taxable base: Base imp. Sage tax: Sage iva Sage transaction: Sage transacción Rate: Tasa Foreign value: Divisa - New expense: Nuevo gasto - Code: Código - It's a withholding: Es una retención - Descripction: Descripción - The code can't be empty: El código no puede estar vacío - The description can't be empty: La descripción no puede estar vacía - The code have to be a number: El código debe ser un número. </i18n> diff --git a/src/pages/InvoiceIn/InvoiceInFilter.vue b/src/pages/InvoiceIn/InvoiceInFilter.vue index 2f87c2b2e..bf4e023a9 100644 --- a/src/pages/InvoiceIn/InvoiceInFilter.vue +++ b/src/pages/InvoiceIn/InvoiceInFilter.vue @@ -28,6 +28,16 @@ const activities = ref([]); </div> </template> <template #body="{ params, searchFn }"> + <QItem> + <QItemSection> + <VnInputDate :label="t('From')" v-model="params.from" is-outlined /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInputDate :label="t('To')" v-model="params.to" is-outlined /> + </QItemSection> + </QItem> <QItem> <QItemSection> <VnSelect @@ -64,16 +74,6 @@ const activities = ref([]); /> </QItemSection> </QItem> - <QItem> - <QItemSection> - <VnInput - :label="t('params.serialNumber')" - v-model="params.serialNumber" - is-outlined - lazy-rules - /> - </QItemSection> - </QItem> <QItem> <QItemSection> <VnInput @@ -84,15 +84,6 @@ const activities = ref([]); /> </QItemSection> </QItem> - <QItem> - <QItemSection> - <VnInputDate - :label="t('Issued')" - v-model="params.issued" - is-outlined - /> - </QItemSection> - </QItem> <QItem> <QItemSection> <VnInput @@ -140,22 +131,6 @@ const activities = ref([]); /> </QItemSection> </QItem> - <QExpansionItem :label="t('More options')" expand-separator> - <QItem> - <QItemSection> - <VnInputDate - :label="t('From')" - v-model="params.from" - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInputDate :label="t('To')" v-model="params.to" is-outlined /> - </QItemSection> - </QItem> - </QExpansionItem> </template> </VnFilterPanel> </template> @@ -179,6 +154,7 @@ en: correctedFk: Rectified issued: Issued to: To + from: From awbCode: AWB correctingFk: Rectificative supplierActivityFk: Supplier activity @@ -201,6 +177,8 @@ es: correctedFk: Rectificada correctingFk: Rectificativa supplierActivityFk: Actividad proveedor + from: Desde + to: Hasta From: Desde To: Hasta Amount: Importe diff --git a/src/pages/InvoiceIn/InvoiceInList.vue b/src/pages/InvoiceIn/InvoiceInList.vue index 234cfb50f..0cad09378 100644 --- a/src/pages/InvoiceIn/InvoiceInList.vue +++ b/src/pages/InvoiceIn/InvoiceInList.vue @@ -47,12 +47,6 @@ const cols = computed(() => [ name: 'supplierRef', label: t('invoiceIn.list.supplierRef'), }, - - { - align: 'left', - name: 'serialNumber', - label: t('invoiceIn.list.serialNumber'), - }, { align: 'left', name: 'serial', @@ -141,7 +135,7 @@ const cols = computed(() => [ v-model="data.supplierFk" url="Suppliers" :fields="['id', 'nickname']" - :label="t('Supplier')" + :label="t('globals.supplier')" option-value="id" option-label="nickname" :filter-options="['id', 'name']" @@ -162,7 +156,7 @@ const cols = computed(() => [ /> <VnSelect url="Companies" - :label="t('Company')" + :label="t('globals.company')" :fields="['id', 'code']" v-model="data.companyFk" option-value="id" diff --git a/src/pages/InvoiceIn/Serial/InvoiceInSerial.vue b/src/pages/InvoiceIn/Serial/InvoiceInSerial.vue new file mode 100644 index 000000000..4eb9fa69d --- /dev/null +++ b/src/pages/InvoiceIn/Serial/InvoiceInSerial.vue @@ -0,0 +1,68 @@ +<script setup> +import { ref, computed, onBeforeMount } from 'vue'; +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; +import axios from 'axios'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import RightMenu from 'src/components/common/RightMenu.vue'; +import InvoiceInSerialFilter from './InvoiceInSerialFilter.vue'; + +const { t } = useI18n(); + +const cols = computed(() => [ + { + align: 'left', + name: 'serial', + label: t('Serial'), + columnFilter: false, + }, + { + align: 'left', + name: 'pending', + label: t('Pending'), + columnFilter: false, + }, + { + align: 'left', + name: 'total', + label: 'Total', + columnFilter: false, + }, +]); + +const daysAgo = ref(); + +onBeforeMount(async () => { + const tableParam = useRoute().query.table; + + if (tableParam) daysAgo.value = JSON.parse(tableParam).daysAgo; + else + daysAgo.value = ( + await axios.get('InvoiceInConfigs/findOne', { + params: { filter: { fields: ['daysAgo'] } }, + }) + ).data?.daysAgo; +}); +</script> +<template> + <RightMenu> + <template #right-panel> + <InvoiceInSerialFilter data-key="InvoiceInSerial" /> + </template> + </RightMenu> + <VnTable + v-if="!isNaN(daysAgo)" + data-key="InvoiceInSerial" + url="InvoiceIns/getSerial" + :columns="cols" + :right-search="false" + :user-params="{ daysAgo }" + :disable-option="{ card: true }" + auto-load + /> +</template> +<i18n> +es: + Serial: Serie + Pending: Pendiente +</i18n> diff --git a/src/pages/InvoiceIn/Serial/InvoiceInSerialFilter.vue b/src/pages/InvoiceIn/Serial/InvoiceInSerialFilter.vue new file mode 100644 index 000000000..4f8c9d70b --- /dev/null +++ b/src/pages/InvoiceIn/Serial/InvoiceInSerialFilter.vue @@ -0,0 +1,53 @@ +<script setup> +import { useI18n } from 'vue-i18n'; +import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +defineProps({ dataKey: { type: String, required: true } }); + +const { t } = useI18n(); +</script> +<template> + <VnFilterPanel :data-key="dataKey" :search-button="true"> + <template #tags="{ tag, formatFn }"> + <div class="q-gutter-x-xs"> + <strong>{{ t(`params.${tag.label}`) }}: </strong> + <span>{{ formatFn(tag.value) }}</span> + </div> + </template> + <template #body="{ params }"> + <QItem> + <QItemSection> + <VnInputNumber + v-model="params.daysAgo" + :label="t('params.daysAgo')" + outlined + rounded + dense + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + v-model="params.serial" + :label="t('params.serial')" + outlined + rounded + dense + /> + </QItemSection> + </QItem> + </template> + </VnFilterPanel> +</template> +<i18n> +en: + params: + daysAgo: Last days + serial: serial +es: + params: + daysAgo: Últimos días + serial: serie +</i18n> diff --git a/src/pages/InvoiceIn/composables/setRectificative.js b/src/pages/InvoiceIn/composables/setRectificative.js new file mode 100644 index 000000000..a1afb8fb0 --- /dev/null +++ b/src/pages/InvoiceIn/composables/setRectificative.js @@ -0,0 +1,14 @@ +import axios from 'axios'; + +export async function setRectificative(route) { + const card = route.matched.find((route) => route.name === 'InvoiceInCard'); + const corrective = card.children.find( + (route) => route.name === 'InvoiceInCorrective' + ); + + corrective.meta.hidden = !( + await axios.get('InvoiceInCorrections', { + params: { filter: { where: { correctingFk: route.params.id } } }, + }) + ).data.length; +} diff --git a/src/pages/InvoiceIn/locale/en.yml b/src/pages/InvoiceIn/locale/en.yml new file mode 100644 index 000000000..824fd6e12 --- /dev/null +++ b/src/pages/InvoiceIn/locale/en.yml @@ -0,0 +1,49 @@ +invoiceIn: + serial: Serial + list: + ref: Reference + supplier: Supplier + supplierRef: Supplier ref. + serial: Serial + file: File + issued: Issued + isBooked: Is booked + awb: AWB + amount: Amount + card: + issued: Issued + amount: Amount + client: Client + company: Company + customerCard: Customer card + ticketList: Ticket List + vat: Vat + dueDay: Due day + intrastat: Intrastat + summary: + supplier: Supplier + supplierRef: Supplier ref. + currency: Currency + issued: Expedition date + operated: Operation date + bookEntried: Entry date + bookedDate: Booked date + sage: Sage withholding + vat: Undeductible VAT + company: Company + booked: Booked + expense: Expense + taxableBase: Taxable base + rate: Rate + sageVat: Sage vat + sageTransaction: Sage transaction + dueDay: Date + bank: Bank + amount: Amount + foreignValue: Foreign value + dueTotal: Due day + noMatch: Do not match + code: Code + net: Net + stems: Stems + country: Country diff --git a/src/pages/InvoiceIn/locale/es.yml b/src/pages/InvoiceIn/locale/es.yml new file mode 100644 index 000000000..944708364 --- /dev/null +++ b/src/pages/InvoiceIn/locale/es.yml @@ -0,0 +1,47 @@ +invoiceIn: + serial: Serie + list: + ref: Referencia + supplier: Proveedor + supplierRef: Ref. proveedor + shortIssued: F. emisión + file: Fichero + issued: Fecha emisión + isBooked: Conciliada + awb: AWB + amount: Importe + card: + issued: Fecha emisión + amount: Importe + client: Cliente + company: Empresa + customerCard: Ficha del cliente + ticketList: Listado de tickets + vat: Iva + dueDay: Fecha de vencimiento + summary: + supplier: Proveedor + supplierRef: Ref. proveedor + currency: Divisa + docNumber: Número documento + issued: Fecha de expedición + operated: Fecha operación + bookEntried: Fecha asiento + bookedDate: Fecha contable + sage: Retención sage + vat: Iva no deducible + company: Empresa + booked: Contabilizada + expense: Gasto + taxableBase: Base imp. + rate: Tasa + sageTransaction: Sage transación + dueDay: Fecha + bank: Caja + amount: Importe + foreignValue: Divisa + dueTotal: Vencimiento + code: Código + net: Neto + stems: Tallos + country: País diff --git a/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue b/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue index e524faa24..08b21fb4a 100644 --- a/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue +++ b/src/pages/InvoiceOut/Card/InvoiceOutDescriptorMenu.vue @@ -5,6 +5,7 @@ import { useRouter } from 'vue-router'; import { useQuasar } from 'quasar'; import TransferInvoiceForm from 'src/components/TransferInvoiceForm.vue'; +import RefundInvoiceForm from 'src/components/RefundInvoiceForm.vue'; import SendEmailDialog from 'components/common/SendEmailDialog.vue'; import useNotify from 'src/composables/useNotify'; @@ -40,8 +41,7 @@ const invoiceFormType = ref('pdf'); const defaultEmailAddress = ref($props.invoiceOutData.client?.email); const showInvoicePdf = () => { - const url = `api/InvoiceOuts/${$props.invoiceOutData.id}/download?access_token=${token}`; - window.open(url, '_blank'); + openReport(`InvoiceOuts/${$props.invoiceOutData.id}/download`, {}, '_blank'); }; const showInvoiceCsv = () => { @@ -141,6 +141,15 @@ const showTransferInvoiceForm = async () => { }, }); }; + +const showRefundInvoiceForm = () => { + quasar.dialog({ + component: RefundInvoiceForm, + componentProps: { + invoiceOutData: $props.invoiceOutData, + }, + }); +}; </script> <template> @@ -222,17 +231,20 @@ const showTransferInvoiceForm = async () => { <QItemSection>{{ t('Generate PDF invoice') }}</QItemSection> </QItem> <QItem v-ripple clickable> - <QItemSection>{{ t('Refund...') }}</QItemSection> + <QItemSection>{{ t('Refund') }}</QItemSection> <QItemSection side> <QIcon name="keyboard_arrow_right" /> </QItemSection> <QMenu anchor="top end" self="top start"> <QList> <QItem v-ripple clickable @click="refundInvoice(true)"> - <QItemSection>{{ t('With warehouse') }}</QItemSection> + <QItemSection>{{ t('With warehouse, no invoice') }}</QItemSection> </QItem> <QItem v-ripple clickable @click="refundInvoice(false)"> - <QItemSection>{{ t('Without warehouse') }}</QItemSection> + <QItemSection>{{ t('Without warehouse, no invoice') }}</QItemSection> + </QItem> + <QItem v-ripple clickable @click="showRefundInvoiceForm()"> + <QItemSection>{{ t('Invoiced') }}</QItemSection> </QItem> </QList> </QMenu> @@ -250,13 +262,14 @@ es: Delete invoice: Eliminar factura Book invoice: Asentar factura Generate PDF invoice: Generar PDF factura - Refund...: Abono + Refund: Abono As PDF: como PDF As CSV: como CSV Send PDF: Enviar PDF Send CSV: Enviar CSV - With warehouse: Con almacén - Without warehouse: Sin almacén + With warehouse, no invoice: Con almacén, sin factura + Without warehouse, no invoice: Sin almacén, sin factura + Invoiced: Facturado InvoiceOut deleted: Factura eliminada Confirm deletion: Confirmar eliminación Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura? diff --git a/src/pages/InvoiceOut/InvoiceOutGlobal.vue b/src/pages/InvoiceOut/InvoiceOutGlobal.vue index eecc61bc2..5f2eb3c02 100644 --- a/src/pages/InvoiceOut/InvoiceOutGlobal.vue +++ b/src/pages/InvoiceOut/InvoiceOutGlobal.vue @@ -94,11 +94,13 @@ const selectCustomerId = (id) => { }; const statusText = computed(() => { - return status.value === 'invoicing' - ? `${t(`status.${status.value}`)} ${ - addresses.value[getAddressNumber.value]?.clientId - }` - : t(`status.${status.value}`); + const baseStatus = t(`status.${status.value}`); + const clientId = + status.value === 'invoicing' + ? addresses.value[getAddressNumber.value]?.clientId || '' + : ''; + + return clientId ? `${baseStatus} ${clientId}`.trim() : baseStatus; }); onMounted(() => (stateStore.rightDrawer = true)); diff --git a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue index 23c63ee6a..5bcb21001 100644 --- a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue +++ b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue @@ -20,21 +20,25 @@ const { initialDataLoading, formInitialData, invoicing, status } = const { makeInvoice, setStatusValue } = invoiceOutGlobalStore; const clientsToInvoice = ref('all'); - const companiesOptions = ref([]); - const printersOptions = ref([]); +const serialTypesOptions = ref([]); -const clientsOptions = ref([]); +const handleInvoiceOutSerialsFetch = (data) => { + serialTypesOptions.value = Array.from( + new Set(data.map((item) => item.type).filter((type) => type)) + ); +}; const formData = ref({}); const optionsInitialData = computed(() => { - return ( - companiesOptions.value.length > 0 && - printersOptions.value.length > 0 && - clientsOptions.value.length > 0 - ); + const optionsArrays = [ + companiesOptions.value, + printersOptions.value, + serialTypesOptions.value, + ]; + return optionsArrays.every((arr) => arr.length > 0); }); const getStatus = computed({ @@ -48,7 +52,7 @@ const getStatus = computed({ onMounted(async () => { await invoiceOutGlobalStore.init(); - formData.value = formInitialData.value.invoiceDate; + formData.value = { invoiceDate: formInitialData.value.invoiceDate }; }); </script> @@ -59,8 +63,11 @@ onMounted(async () => { auto-load /> <FetchData url="Printers" @on-fetch="(data) => (printersOptions = data)" auto-load /> - <FetchData url="Clients" @on-fetch="(data) => (clientsOptions = data)" auto-load /> - + <FetchData + url="invoiceOutSerials" + @on-fetch="handleInvoiceOutSerialsFetch" + auto-load + /> <QForm v-if="!initialDataLoading && optionsInitialData" @submit="makeInvoice(formData, clientsToInvoice)" @@ -87,13 +94,34 @@ onMounted(async () => { v-if="clientsToInvoice === 'one'" :label="t('client')" v-model="formData.clientId" - :options="clientsOptions" + url="Clients" option-value="id" option-label="name" hide-selected dense outlined rounded + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + #{{ scope.opt?.id }} {{ scope.opt?.name }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnSelect + :label="t('invoiceOutSerialType')" + v-model="formData.serialType" + :options="serialTypesOptions" + option-value="type" + option-label="type" + hide-selected + dense + outlined + rounded /> <VnInputDate v-model="formData.invoiceDate" @@ -109,9 +137,7 @@ onMounted(async () => { :label="t('company')" v-model="formData.companyFk" :options="companiesOptions" - option-value="id" option-label="code" - hide-selected dense outlined rounded @@ -120,9 +146,6 @@ onMounted(async () => { :label="t('printer')" v-model="formData.printer" :options="printersOptions" - option-value="id" - option-label="name" - hide-selected dense outlined rounded @@ -168,6 +191,7 @@ en: printer: Printer invoiceOut: Invoice out client: Client + invoiceOutSerialType: Serial Type stop: Stop es: @@ -179,5 +203,6 @@ es: printer: Impresora invoiceOut: Facturar client: Cliente + invoiceOutSerialType: Tipo de Serie stop: Parar </i18n> diff --git a/src/pages/InvoiceOut/InvoiceOutList.vue b/src/pages/InvoiceOut/InvoiceOutList.vue index 528cdf234..915b58b15 100644 --- a/src/pages/InvoiceOut/InvoiceOutList.vue +++ b/src/pages/InvoiceOut/InvoiceOutList.vue @@ -1,5 +1,5 @@ <script setup> -import { onMounted, onUnmounted, ref, computed } from 'vue'; +import { onMounted, onUnmounted, ref, computed, watchEffect } from 'vue'; import { useI18n } from 'vue-i18n'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; @@ -12,7 +12,6 @@ import InvoiceOutSummary from './Card/InvoiceOutSummary.vue'; import { toCurrency, toDate } from 'src/filters/index'; import { useStateStore } from 'stores/useStateStore'; import { QBtn } from 'quasar'; -import { watchEffect } from 'vue'; import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue'; const { t } = useI18n(); @@ -20,7 +19,6 @@ const stateStore = useStateStore(); const { viewSummary } = useSummaryDialog(); const tableRef = ref(); const invoiceOutSerialsOptions = ref([]); -const ticketsOptions = ref([]); const customerOptions = ref([]); const selectedRows = ref([]); const hasSelectedCards = computed(() => selectedRows.value.length > 0); @@ -123,7 +121,7 @@ const columns = computed(() => [ name: 'tableActions', actions: [ { - title: t('InvoiceOutSummary'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row.id, InvoiceOutSummary), }, @@ -199,7 +197,7 @@ watchEffect(selectedRows); :url="`${MODEL}/filter`" :create="{ urlCreate: 'InvoiceOuts/createManualInvoice', - title: t('Create Manual Invoice'), + title: t('Create manual invoice'), onDataSaved: ({ id }) => tableRef.redirect(id), formInitialData: { active: true, @@ -222,14 +220,25 @@ watchEffect(selectedRows); </span> </template> <template #more-create-dialog="{ data }"> - <VnSelect - url="Tickets" - v-model="data.ticketFk" - :label="t('invoiceOutList.tableVisibleColumns.ticket')" - :options="ticketsOptions" - option-label="nickname" - option-value="id" - /> + <div class="flex no-wrap flex-center"> + <VnSelect + url="Tickets" + v-model="data.ticketFk" + :label="t('invoiceOutList.tableVisibleColumns.ticket')" + option-label="id" + option-value="id" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> #{{ scope.opt?.id }} </QItemLabel> + <QItemLabel caption>{{ scope.opt?.nickname }}</QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + <span class="q-ml-md">O</span> + </div> <VnSelect url="Clients" v-model="data.clientFk" @@ -238,21 +247,21 @@ watchEffect(selectedRows); option-label="name" option-value="id" /> - <VnInputDate - :label="t('invoiceOutList.tableVisibleColumns.dueDate')" - v-model="data.maxShipped" - /> <VnSelect url="InvoiceOutSerials" - v-model="data.invoiceOutSerial" + v-model="data.serial" :label="t('invoiceOutList.tableVisibleColumns.invoiceOutSerial')" :options="invoiceOutSerialsOptions" option-label="description" option-value="code" /> + <VnInputDate + :label="t('invoiceOutList.tableVisibleColumns.dueDate')" + v-model="data.maxShipped" + /> <VnSelect url="TaxAreas" - v-model="data.area" + v-model="data.taxArea" :label="t('invoiceOutList.tableVisibleColumns.taxArea')" :options="taxAreasOptions" option-label="code" @@ -273,10 +282,12 @@ en: fileAllowed: Successful download of CSV file youCanSearchByInvoiceReference: You can search by invoice reference createInvoice: Make invoice + Create manual invoice: Create manual invoice es: searchInvoice: Buscar factura emitida fileDenied: El navegador denegó la descarga de archivos... fileAllowed: Descarga exitosa de archivo CSV youCanSearchByInvoiceReference: Puedes buscar por referencia de la factura createInvoice: Crear factura + Create manual invoice: Crear factura manual </i18n> diff --git a/src/pages/InvoiceOut/InvoiceOutNegativeBases.vue b/src/pages/InvoiceOut/InvoiceOutNegativeBases.vue index 93b326c59..774b42478 100644 --- a/src/pages/InvoiceOut/InvoiceOutNegativeBases.vue +++ b/src/pages/InvoiceOut/InvoiceOutNegativeBases.vue @@ -74,6 +74,9 @@ const columns = computed(() => [ align: 'left', name: 'amount', label: t('invoiceOutModule.amount'), + columnFilter: { + type: 'number', + }, format: (row) => toCurrency(row.amount), cardVisible: true, }, diff --git a/src/pages/InvoiceOut/InvoiceOutNegativeBasesFilter.vue b/src/pages/InvoiceOut/InvoiceOutNegativeBasesFilter.vue index 2d192ebb3..94eab2aab 100644 --- a/src/pages/InvoiceOut/InvoiceOutNegativeBasesFilter.vue +++ b/src/pages/InvoiceOut/InvoiceOutNegativeBasesFilter.vue @@ -19,7 +19,7 @@ const props = defineProps({ <VnFilterPanel :data-key="props.dataKey" :search-button="true" - :unremovable-params="['from', 'to']" + :un-removable-params="['from', 'to']" :hidden-tags="['from', 'to']" > <template #tags="{ tag, formatFn }"> diff --git a/src/pages/Item/Card/ItemBasicData.vue b/src/pages/Item/Card/ItemBasicData.vue index 89c883295..ae9e45983 100644 --- a/src/pages/Item/Card/ItemBasicData.vue +++ b/src/pages/Item/Card/ItemBasicData.vue @@ -16,7 +16,6 @@ const route = useRoute(); const { t } = useI18n(); const itemTypesOptions = ref([]); -const itemsWithNameOptions = ref([]); const intrastatsOptions = ref([]); const expensesOptions = ref([]); diff --git a/src/pages/Item/Card/ItemBotanical.vue b/src/pages/Item/Card/ItemBotanical.vue index 416c7f78b..0687b8db3 100644 --- a/src/pages/Item/Card/ItemBotanical.vue +++ b/src/pages/Item/Card/ItemBotanical.vue @@ -14,20 +14,9 @@ const route = useRoute(); const { t } = useI18n(); const itemBotanicalsRef = ref(null); -const itemGenusOptions = ref([]); -const itemSpeciesOptions = ref([]); const itemBotanicals = ref([]); let itemBotanicalsForm = reactive({ itemFk: null }); -const onGenusCreated = (response, formData) => { - itemGenusOptions.value = [...itemGenusOptions.value, response]; - formData.genusFk = response.id; -}; - -const onSpecieCreated = (response, formData) => { - itemSpeciesOptions.value = [...itemSpeciesOptions.value, response]; - formData.specieFk = response.id; -}; const entityId = computed(() => { return route.params.id; }); @@ -47,18 +36,6 @@ onMounted(async () => { }" @on-fetch="(data) => (itemBotanicals = data)" /> - <FetchData - url="Genera" - :filter="{ fields: ['id', 'name'], order: 'name ASC' }" - @on-fetch="(data) => (itemGenusOptions = data)" - auto-load - /> - <FetchData - url="Species" - :filter="{ fields: ['id', 'name'], order: 'name ASC' }" - @on-fetch="(data) => (itemSpeciesOptions = data)" - auto-load - /> <FormModel url-update="ItemBotanicals" model="entry" @@ -69,36 +46,35 @@ onMounted(async () => { <template #form="{ data }"> <VnRow> <VnSelectDialog + ref="genusRef" :label="t('Genus')" v-model="data.genusFk" - :options="itemGenusOptions" + url="Genera" option-label="name" option-value="id" + :fields="['id', 'name']" + sort-by="name ASC" hide-selected > <template #form> <CreateGenusForm - @on-data-saved=" - (_, requestResponse) => - onGenusCreated(requestResponse, data) - " + @on-data-saved="(_, res) => (data.genusFk = res.id)" /> </template> </VnSelectDialog> <VnSelectDialog :label="t('Species')" v-model="data.specieFk" - :options="itemSpeciesOptions" + url="Species" option-label="name" option-value="id" + :fields="['id', 'name']" + sort-by="name ASC" hide-selected > <template #form> <CreateSpecieForm - @on-data-saved=" - (_, requestResponse) => - onSpecieCreated(requestResponse, data) - " + @on-data-saved="(_, res) => (data.specieFk = res.id)" /> </template> </VnSelectDialog> diff --git a/src/pages/Item/Card/ItemDescriptor.vue b/src/pages/Item/Card/ItemDescriptor.vue index 8381f0624..baac0c608 100644 --- a/src/pages/Item/Card/ItemDescriptor.vue +++ b/src/pages/Item/Card/ItemDescriptor.vue @@ -10,8 +10,6 @@ import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.v import VnConfirm from 'components/ui/VnConfirm.vue'; import RegularizeStockForm from 'components/RegularizeStockForm.vue'; import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue'; - -import { useState } from 'src/composables/useState'; import useCardDescription from 'src/composables/useCardDescription'; import { getUrl } from 'src/composables/getUrl'; import axios from 'axios'; @@ -35,58 +33,69 @@ const $props = defineProps({ type: Number, default: null, }, + warehouseFk: { + type: Number, + default: null, + }, }); const quasar = useQuasar(); const route = useRoute(); const router = useRouter(); const { t } = useI18n(); -const state = useState(); -const user = state.getUser(); - +const warehouseConfig = ref(null); const entityId = computed(() => { return $props.id || route.params.id; }); + const regularizeStockFormDialog = ref(null); const available = ref(null); const visible = ref(null); -const _warehouseFk = ref(null); const salixUrl = ref(); -const warehouseFk = computed({ - get() { - return _warehouseFk.value; - }, - set(val) { - _warehouseFk.value = val; - if (val) updateStock(); - }, -}); onMounted(async () => { - warehouseFk.value = user.value.warehouseFk; salixUrl.value = await getUrl(''); + await getItemConfigs(); + await updateStock(); }); const data = ref(useCardDescription()); -const setData = (entity) => { - if (!entity) return; - data.value = useCardDescription(entity.name, entity.id); +const setData = async (entity) => { + try { + if (!entity) return; + data.value = useCardDescription(entity.name, entity.id); + await updateStock(); + } catch (err) { + console.error('Error item'); + } }; +const getItemConfigs = async () => { + try { + const { data } = await axios.get('ItemConfigs/findOne'); + if (!data) return; + return (warehouseConfig.value = data.warehouseFk); + } catch (err) { + console.error('Error item'); + } +}; const updateStock = async () => { try { available.value = null; visible.value = null; const params = { - warehouseFk: warehouseFk.value, + warehouseFk: $props.warehouseFk, dated: $props.dated, }; + await getItemConfigs(); + if (!params.warehouseFk) { + params.warehouseFk = warehouseConfig.value; + } const { data } = await axios.get(`Items/${entityId.value}/getVisibleAvailable`, { params, }); - available.value = data.available; visible.value = data.visible; } catch (err) { diff --git a/src/pages/Item/Card/ItemDescriptorImage.vue b/src/pages/Item/Card/ItemDescriptorImage.vue index d923dd28f..a4ef22ce3 100644 --- a/src/pages/Item/Card/ItemDescriptorImage.vue +++ b/src/pages/Item/Card/ItemDescriptorImage.vue @@ -47,8 +47,11 @@ const getWarehouseName = async (warehouseFk) => { const filter = { where: { id: warehouseFk }, }; - - const { data } = await axios.get('Warehouses/findOne', { filter }); + const { data } = await axios.get('Warehouses/findOne', { + params: { + filter: JSON.stringify(filter), + }, + }); if (!data) return; warehouseName.value = data.name; }; @@ -138,14 +141,6 @@ en: </i18n> <style lang="scss" scoped> -.edit-photo-btn { - position: absolute; - right: 12px; - bottom: 12px; - z-index: 1; - cursor: pointer; -} - .separation-borders { border-left: 1px solid $white; border-right: 1px solid $white; diff --git a/src/pages/Item/Card/ItemDescriptorProxy.vue b/src/pages/Item/Card/ItemDescriptorProxy.vue index 2b7b39a65..2ffc9080f 100644 --- a/src/pages/Item/Card/ItemDescriptorProxy.vue +++ b/src/pages/Item/Card/ItemDescriptorProxy.vue @@ -15,6 +15,10 @@ const $props = defineProps({ type: Number, default: null, }, + warehouseFk: { + type: Number, + default: null, + }, }); </script> @@ -26,6 +30,7 @@ const $props = defineProps({ :summary="ItemSummary" :dated="dated" :sale-fk="saleFk" + :warehouse-fk="warehouseFk" /> </QPopupProxy> </template> diff --git a/src/pages/Item/Card/ItemShelving.vue b/src/pages/Item/Card/ItemShelving.vue index 7e7faab36..41cb34c03 100644 --- a/src/pages/Item/Card/ItemShelving.vue +++ b/src/pages/Item/Card/ItemShelving.vue @@ -3,7 +3,6 @@ import { onMounted, ref, computed, reactive } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import FetchData from 'components/FetchData.vue'; 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'; @@ -24,8 +23,6 @@ const { notify } = useNotify(); const { openConfirmationModal } = useVnConfirm(); const rowsSelected = ref([]); -const parkingsOptions = ref([]); -const shelvingsOptions = ref([]); const exprBuilder = (param, value) => { switch (param) { @@ -104,7 +101,9 @@ const columns = computed(() => [ filterValue: null, event: getInputEvents, attrs: { - options: parkingsOptions.value, + url: 'parkings', + fields: ['code'], + 'sort-by': 'code ASC', 'option-value': 'code', 'option-label': 'code', dense: true, @@ -124,7 +123,9 @@ const columns = computed(() => [ filterValue: null, event: getInputEvents, attrs: { - options: shelvingsOptions.value, + url: 'shelvings', + fields: ['code'], + 'sort-by': 'code ASC', 'option-value': 'code', 'option-label': 'code', dense: true, @@ -188,18 +189,6 @@ onMounted(async () => { </script> <template> - <FetchData - url="parkings" - :filter="{ fields: ['code'], order: 'code ASC' }" - auto-load - @on-fetch="(data) => (parkingsOptions = data)" - /> - <FetchData - url="shelvings" - :filter="{ fields: ['code'], order: 'code ASC' }" - auto-load - @on-fetch="(data) => (shelvingsOptions = data)" - /> <template v-if="stateStore.isHeaderMounted()"> <Teleport to="#st-data"> <div class="q-pa-md q-mr-lg q-ma-xs" style="border: 2px solid #222"> @@ -237,7 +226,6 @@ onMounted(async () => { </QBtn> </Teleport> </template> - <QPage class="column items-center q-pa-md"> <QTable :rows="rows" diff --git a/src/pages/Item/Card/ItemSummary.vue b/src/pages/Item/Card/ItemSummary.vue index 7af3e0fa0..7b6015c30 100644 --- a/src/pages/Item/Card/ItemSummary.vue +++ b/src/pages/Item/Card/ItemSummary.vue @@ -7,8 +7,7 @@ import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; - -import { useRole } from 'src/composables/useRole'; +import VnTitle from 'src/components/common/VnTitle.vue'; const $props = defineProps({ id: { @@ -19,23 +18,10 @@ const $props = defineProps({ const route = useRoute(); const { t } = useI18n(); -const roleState = useRole(); const entityId = computed(() => $props.id || route.params.id); - -const isBuyer = computed(() => { - return roleState.hasAny(['buyer']); -}); - -const isReplenisher = computed(() => { - return roleState.hasAny(['replenisher']); -}); - -const isAdministrative = computed(() => { - return roleState.hasAny(['administrative']); -}); +const getUrl = (id, param) => `#/Item/${id}/${param}`; </script> - <template> <CardSummary ref="summary" @@ -44,13 +30,15 @@ const isAdministrative = computed(() => { data-key="ItemSummary" > <template #header-left> - <router-link - v-if="route.name !== 'ItemSummary'" + <QBtn + v-if="$route.name !== 'ItemSummary'" :to="{ name: 'ItemSummary', params: { id: entityId } }" - class="header link" - > - <QIcon name="open_in_new" color="white" size="sm" /> - </router-link> + class="header link--white" + icon="open_in_new" + flat + dense + round + /> </template> <template #header="{ entity: { item } }"> {{ item.id }} - {{ item.name }} @@ -65,15 +53,10 @@ const isAdministrative = computed(() => { /> </QCard> <QCard class="vn-one"> - <component - :is="isBuyer ? 'router-link' : 'span'" - :to="{ name: 'ItemBasicData', params: { id: entityId } }" - class="header" - :class="{ 'header-link': isBuyer }" - > - {{ t('item.summary.basicData') }} - <QIcon v-if="isBuyer" name="open_in_new" /> - </component> + <VnTitle + :url="getUrl(entityId, 'basic-data')" + :text="t('item.summary.basicData')" + /> <VnLv :label="t('item.summary.name')" :value="item.name" /> <VnLv :label="t('item.summary.completeName')" :value="item.longName" /> <VnLv :label="t('item.summary.family')" :value="item.itemType.name" /> @@ -104,15 +87,10 @@ const isAdministrative = computed(() => { </VnLv> </QCard> <QCard class="vn-one"> - <component - :is="isBuyer ? 'router-link' : 'span'" - :to="{ name: 'ItemBasicData', params: { id: entityId } }" - class="header" - :class="{ 'header-link': isBuyer }" - > - {{ t('item.summary.otherData') }} - <QIcon v-if="isBuyer" name="open_in_new" /> - </component> + <VnTitle + :url="getUrl(entityId, 'basic-data')" + :text="t('item.summary.otherData')" + /> <VnLv :label="t('item.summary.intrastatCode')" :value="item.intrastat.id" @@ -137,15 +115,7 @@ const isAdministrative = computed(() => { /> </QCard> <QCard class="vn-one"> - <component - :is="isBuyer || isReplenisher ? 'router-link' : 'span'" - :to="{ name: 'ItemTags', params: { id: entityId } }" - class="header" - :class="{ 'header-link': isBuyer || isReplenisher }" - > - {{ t('item.summary.tags') }} - <QIcon v-if="isBuyer || isReplenisher" name="open_in_new" /> - </component> + <VnTitle :url="getUrl(entityId, 'tags')" :text="t('item.summary.tags')" /> <VnLv v-for="(tag, index) in tags" :key="index" @@ -154,29 +124,14 @@ const isAdministrative = computed(() => { /> </QCard> <QCard class="vn-one" v-if="item.description"> - <component - :is="isBuyer ? 'router-link' : 'span'" - :to="{ name: 'ItemBasicData', params: { id: entityId } }" - class="header" - :class="{ 'header-link': isBuyer }" - > - {{ t('item.summary.description') }} - <QIcon v-if="isBuyer" name="open_in_new" /> - </component> - <p> - {{ item.description }} - </p> + <VnTitle + :url="getUrl(entityId, 'basic-data')" + :text="t('item.summary.description')" + /> + <p v-text="item.description" /> </QCard> <QCard class="vn-one"> - <component - :is="isBuyer || isAdministrative ? 'router-link' : 'span'" - :to="{ name: 'ItemTax', params: { id: entityId } }" - class="header" - :class="{ 'header-link': isBuyer || isAdministrative }" - > - {{ t('item.summary.tax') }} - <QIcon v-if="isBuyer || isAdministrative" name="open_in_new" /> - </component> + <VnTitle :url="getUrl(entityId, 'tax')" :text="t('item.summary.tax')" /> <VnLv v-for="(tax, index) in item.taxes" :key="index" @@ -185,15 +140,10 @@ const isAdministrative = computed(() => { /> </QCard> <QCard class="vn-one"> - <component - :is="isBuyer ? 'router-link' : 'span'" - :to="{ name: 'ItemBotanical', params: { id: entityId } }" - class="header" - :class="{ 'header-link': isBuyer }" - > - {{ t('item.summary.botanical') }} - <QIcon v-if="isBuyer" name="open_in_new" /> - </component> + <VnTitle + :url="getUrl(entityId, 'botanical')" + :text="t('item.summary.botanical')" + /> <VnLv :label="t('item.summary.genus')" :value="botanical?.genus?.name" /> <VnLv :label="t('item.summary.specie')" @@ -201,23 +151,19 @@ const isAdministrative = computed(() => { /> </QCard> <QCard class="vn-one"> - <component - :is="isBuyer || isReplenisher ? 'router-link' : 'span'" - :to="{ name: 'ItemBarcode', params: { id: entityId } }" - class="header" - :class="{ 'header-link': isBuyer || isReplenisher }" - > - {{ t('item.summary.barcode') }} - <QIcon v-if="isBuyer || isReplenisher" name="open_in_new" /> - </component> - <p v-for="(barcode, index) in item.itemBarcode" :key="index"> - {{ barcode.code }} - </p> + <VnTitle + :url="getUrl(entityId, 'barcode')" + :text="t('item.summary.barcode')" + /> + <p + v-for="(barcode, index) in item.itemBarcode" + :key="index" + v-text="barcode.code" + /> </QCard> </template> </CardSummary> </template> - <i18n> en: Este artículo necesita una foto: Este artículo necesita una foto diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue index 40c9941e9..39723ae65 100644 --- a/src/pages/Item/Card/ItemTags.vue +++ b/src/pages/Item/Card/ItemTags.vue @@ -24,6 +24,7 @@ const getSelectedTagValues = async (tag) => { const filter = { fields: ['value'], order: 'value ASC', + limit: 30, }; const params = { filter: JSON.stringify(filter) }; @@ -126,7 +127,7 @@ const insertTag = (rows) => { :key="row.tagFk" :label="t('Value')" v-model="row.value" - :options="valueOptionsMap.get(row.tagFk)" + :url="`Tags/${row.tagFk}/filterValue`" option-label="value" option-value="value" emit-value @@ -135,6 +136,7 @@ const insertTag = (rows) => { :is-clearable="false" :required="false" :rules="validate('itemTag.tagFk')" + :use-like="false" /> <VnInput v-else-if=" diff --git a/src/pages/Item/ItemFixedPrice.vue b/src/pages/Item/ItemFixedPrice.vue index 2ecd1f21b..d91b5189e 100644 --- a/src/pages/Item/ItemFixedPrice.vue +++ b/src/pages/Item/ItemFixedPrice.vue @@ -1,196 +1,190 @@ <script setup> -import { onMounted, ref, reactive, computed, onUnmounted, watch } from 'vue'; +import { onMounted, ref, reactive, onUnmounted, nextTick, computed } from 'vue'; import { useI18n } from 'vue-i18n'; - -import FetchData from 'components/FetchData.vue'; +import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import FetchedTags from 'components/ui/FetchedTags.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import EditTableCellValueForm from 'src/components/EditTableCellValueForm.vue'; import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue'; +import { useQuasar } from 'quasar'; import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue'; - +import { tMobile } from 'src/composables/tMobile'; +import VnConfirm from 'components/ui/VnConfirm.vue'; +import FetchData from 'src/components/FetchData.vue'; import { useStateStore } from 'stores/useStateStore'; -import { dashIfEmpty } from 'src/filters'; +import { toDate } from 'src/filters'; import { useVnConfirm } from 'composables/useVnConfirm'; import { useState } from 'src/composables/useState'; -import { toCurrency } from 'filters/index'; import useNotify from 'src/composables/useNotify.js'; import axios from 'axios'; -import { useArrayData } from 'composables/useArrayData'; import { isLower, isBigger } from 'src/filters/date.js'; import RightMenu from 'src/components/common/RightMenu.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import { QCheckbox } from 'quasar'; +const quasar = useQuasar(); const stateStore = useStateStore(); const { t } = useI18n(); const { openConfirmationModal } = useVnConfirm(); const state = useState(); const { notify } = useNotify(); - +const tableRef = ref(); const editTableCellDialogRef = ref(null); const user = state.getUser(); const fixedPrices = ref([]); -const fixedPricesOriginalData = ref([]); const warehousesOptions = ref([]); const rowsSelected = ref([]); -const exprBuilder = (param, value) => { - switch (param) { - case 'name': - return { 'i.name': { like: `%${value}%` } }; - case 'itemFk': - case 'warehouseFk': - case 'rate2': - case 'rate3': - param = `fp.${param}`; - return { [param]: value }; - case 'minPrice': - param = `i.${param}`; - return { [param]: value }; - } -}; +const itemFixedPriceFilterRef = ref(); const params = reactive({}); -const arrayData = useArrayData('ItemFixedPrices', { - url: 'FixedPrices/filter', - userParams: params, - order: ['name ASC', 'itemFk'], - exprBuilder: exprBuilder, -}); -const store = arrayData.store; - -const fetchFixedPrices = async () => { - await arrayData.fetch({ append: false }); -}; - -const onFixedPricesFetched = (data) => { - fixedPrices.value = data; - // el objetivo de guardar una copia de las rows es evitar guardar cambios si la data no cambió al disparar los eventos - fixedPricesOriginalData.value = JSON.parse(JSON.stringify(data)); -}; - -watch( - () => store.data, - (data) => onFixedPricesFetched(data) -); - -const applyColumnFilter = async (col) => { - try { - const paramKey = col.columnFilter?.filterParamKey || col.field; - params[paramKey] = col.columnFilter.filterValue; - await arrayData.addFilter({ params }); - } catch (err) { - console.error('Error applying column filter', err); - } -}; - -const getColumnInputEvents = (col) => { - return col.columnFilter.type === 'select' - ? { 'update:modelValue': () => applyColumnFilter(col) } - : { - 'keyup.enter': () => applyColumnFilter(col), - }; -}; - -const defaultColumnFilter = { - component: VnInput, - type: 'text', - filterValue: null, - event: getColumnInputEvents, - attrs: { - dense: true, - }, -}; - const defaultColumnAttrs = { align: 'left', sortable: true, }; - +onMounted(async () => { + stateStore.rightDrawer = true; + params.warehouseFk = user.value.warehouseFk; +}); +onUnmounted(() => (stateStore.rightDrawer = false)); const columns = computed(() => [ { label: t('item.fixedPrice.itemId'), name: 'itemId', - field: 'itemFk', ...defaultColumnAttrs, - columnFilter: { - ...defaultColumnFilter, + isId: true, + cardVisible: true, + columnField: { + component: 'input', + type: 'number', }, + columnClass: 'shrink', }, { - label: t('globals.description'), + label: t('globals.name'), field: 'name', name: 'description', ...defaultColumnAttrs, - columnFilter: { - ...defaultColumnFilter, - }, + create: true, + cardVisible: true, }, { label: t('item.fixedPrice.groupingPrice'), field: 'rate2', - name: 'groupingPrice', + name: 'rate2', ...defaultColumnAttrs, - columnFilter: { - ...defaultColumnFilter, + cardVisible: true, + columnField: { + class: 'expand', + component: 'input', + type: 'number', + }, + columnFilter: { + class: 'expand', + component: 'input', + type: 'number', }, - format: (val) => toCurrency(val), }, { label: t('item.fixedPrice.packingPrice'), field: 'rate3', - name: 'packingPrice', + name: 'rate3', ...defaultColumnAttrs, - columnFilter: { - ...defaultColumnFilter, + cardVisible: true, + columnField: { + class: 'expand', + component: 'input', + type: 'number', + }, + columnFilter: { + class: 'expand', + component: 'input', + type: 'number', }, - format: (val) => dashIfEmpty(val), }, { label: t('item.fixedPrice.minPrice'), field: 'minPrice', + columnClass: 'shrink', name: 'minPrice', ...defaultColumnAttrs, + cardVisible: true, + columnField: { + class: 'expand', + component: 'input', + type: 'number', + }, columnFilter: { - ...defaultColumnFilter, + class: 'expand', + component: 'input', + type: 'number', }, }, { label: t('item.fixedPrice.started'), field: 'started', name: 'started', + format: ({ started }) => toDate(started), + cardVisible: true, ...defaultColumnAttrs, - columnFilter: null, + columnField: { + component: 'date', + class: 'shrink', + }, + columnFilter: { + component: 'date', + }, + columnClass: 'expand', }, { label: t('item.fixedPrice.ended'), field: 'ended', name: 'ended', ...defaultColumnAttrs, - columnFilter: null, + cardVisible: true, + columnField: { + component: 'date', + class: 'shrink', + }, + columnFilter: { + component: 'date', + }, + columnClass: 'expand', + format: (row) => toDate(row.ended), }, { label: t('item.fixedPrice.warehouse'), field: 'warehouseFk', - name: 'warehouse', + name: 'warehouseFk', ...defaultColumnAttrs, + columnClass: 'shrink', columnFilter: { - component: VnSelect, - type: 'select', - filterValue: null, - event: getColumnInputEvents, - attrs: { - options: warehousesOptions.value, - 'option-value': 'id', - 'option-label': 'name', - dense: true, - }, + component: 'select', + }, + columnField: { + component: 'select', + class: 'expand', + }, + attrs: { + options: warehousesOptions, }, }, - { name: 'deleteAction', align: 'center' }, + { + align: 'right', + name: 'tableActions', + actions: [ + { + title: t('delete'), + icon: 'delete', + action: (row) => confirmRemove(row), + isPrimary: true, + }, + ], + }, ]); const editTableFieldsOptions = [ @@ -218,15 +212,6 @@ const editTableFieldsOptions = [ type: 'number', }, }, - { - field: 'hasMinPrice', - label: t('item.fixedPrice.hasMinPrice'), - component: 'checkbox', - attrs: { - 'false-value': 0, - 'true-value': 1, - }, - }, { field: 'started', label: t('item.fixedPrice.started'), @@ -248,7 +233,6 @@ const editTableFieldsOptions = [ }, }, ]; - const getRowUpdateInputEvents = (props, resetMinPrice, inputType = 'text') => { return inputType === 'text' ? { @@ -258,91 +242,6 @@ const getRowUpdateInputEvents = (props, resetMinPrice, inputType = 'text') => { : { 'update:modelValue': () => upsertPrice(props, resetMinPrice) }; }; -const validations = (row, rowIndex, col) => { - const isNew = !row.id; - // Si la row no tiene id significa que fue agregada con addRow y no se ha guardado en la base de datos - // Si isNew es falso no se checkea si el valor es igual a la original - if (!isNew) - if (fixedPricesOriginalData.value[rowIndex][col.field] == row[col.field]) - return false; - - const requiredFields = ['itemFk', 'started', 'ended', 'rate2', 'rate3']; - return requiredFields.every( - (field) => row[field] !== null && row[field] !== undefined - ); -}; - -const upsertPrice = async ({ row, col, rowIndex }, resetMinPrice = false) => { - if (!validations(row, rowIndex, col)) return; - - try { - if (resetMinPrice) row.hasMinPrice = 0; - - const { data } = await axios.patch('FixedPrices/upsertFixedPrice', row); - row = data; - fixedPricesOriginalData.value[rowIndex][col.field] = row[col.field]; - } catch (err) { - console.error('Error editing price', err); - } -}; - -const addRow = () => { - if (!fixedPrices.value || fixedPrices.value.length === 0) { - fixedPrices.value = []; - - const today = Date.vnNew(); - const millisecsInDay = 86400000; - const daysInWeek = 7; - const nextWeek = new Date(today.getTime() + daysInWeek * millisecsInDay); - - const newPrice = { - started: today, - ended: nextWeek, - hasMinPrice: 0, - }; - - fixedPricesOriginalData.value.push({ ...newPrice }); - fixedPrices.value.push({ ...newPrice }); - return; - } - - const lastItemCopy = JSON.parse( - JSON.stringify(fixedPrices.value[fixedPrices.value.length - 1]) - ); - delete lastItemCopy.id; - fixedPricesOriginalData.value.push(lastItemCopy); - fixedPrices.value.push(lastItemCopy); -}; - -const openEditTableCellDialog = () => { - editTableCellDialogRef.value.show(); -}; - -const onEditCellDataSaved = async () => { - rowsSelected.value = []; - await fetchFixedPrices(); -}; - -const onWarehousesFetched = (data) => { - warehousesOptions.value = data; - // Actualiza las 'options' del elemento con field 'warehouseFk' en 'editTableFieldsOptions'. - const warehouseField = editTableFieldsOptions.find( - (field) => field.field === 'warehouseFk' - ); - warehouseField.attrs.options = data; -}; - -const removePrice = async (id, rowIndex) => { - try { - await axios.delete(`FixedPrices/${id}`); - fixedPrices.value.splice(rowIndex, 1); - fixedPricesOriginalData.value.splice(rowIndex, 1); - notify(t('globals.dataSaved'), 'positive'); - } catch (err) { - console.error('Error removing price', err); - } -}; - 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 @@ -354,213 +253,361 @@ const updateMinPrice = async (value, props) => { }); }; -onMounted(async () => { - stateStore.rightDrawer = true; - params.warehouseFk = user.value.warehouseFk; - await fetchFixedPrices(); -}); +const upsertPrice = async (props, resetMinPrice = false) => { + try { + const { row } = props; + if (tableRef.value.CrudModelRef.getChanges().updates.length > 0) { + if (resetMinPrice) row.hasMinPrice = 0; + await upsertFixedPrice(row); + } + } catch (err) { + console.error('Error editing price', err); + } +}; -onUnmounted(() => (stateStore.rightDrawer = false)); +async function upsertFixedPrice(row) { + try { + const { data } = await axios.patch('FixedPrices/upsertFixedPrice', row); + return data; + } catch (err) { + console.error('Error editing price', err); + } +} + +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; + + getTableRows().forEach((row, index) => { + const rect = row.getBoundingClientRect(); + if (rect.top >= 0 && rect.bottom <= window.innerHeight) { + lastVisibleRow = index; + } + }); + + return lastVisibleRow; +} + +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); + + 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(), + }; + return { original, copy }; +}; + +const getTableRows = () => + document.getElementsByClassName('q-table')[0].querySelectorAll('tr.cursor-pointer'); + +function highlightNewRow({ $index: index }) { + const row = getTableRows()[index]; + if (row) { + row.classList.add('highlight'); + setTimeout(() => { + row.classList.remove('highlight'); + }, 3000); // Duración de la animación en milisegundos + } +} +const openEditTableCellDialog = () => { + editTableCellDialogRef.value.show(); +}; + +const onEditCellDataSaved = async () => { + rowsSelected.value = []; + tableRef.value.reload(); +}; + +const removeFuturePrice = async () => { + try { + rowsSelected.value.forEach(({ id }) => { + const rowIndex = fixedPrices.value.findIndex(({ id }) => id === id); + removePrice(id, rowIndex); + }); + } catch (err) { + console.error('Error removing price', err); + } +}; + +function confirmRemove(item, isFuture) { + const promise = async () => + isFuture ? removeFuturePrice(item.id) : removePrice(item.id); + quasar.dialog({ + component: VnConfirm, + componentProps: { + title: t('globals.rowWillBeRemoved'), + message: t('globals.confirmDeletion'), + promise, + }, + }); +} + +const removePrice = async (id) => { + try { + await axios.delete(`FixedPrices/${id}`); + notify(t('globals.dataSaved'), 'positive'); + tableRef.value.reload({}); + } catch (err) { + console.error('Error removing price', err); + } +}; +const dateStyle = (date) => + date + ? { + 'bg-color': 'warning', + 'is-outlined': true, + } + : {}; + +function handleOnDataSave({ CrudModelRef }) { + const { original, copy } = addRow(CrudModelRef.formData[checkLastVisibleRow()]); + if (original) { + CrudModelRef.formData.splice(original?.$index ?? 0, 0, copy); + } else { + CrudModelRef.insert(copy); + } + nextTick(() => { + highlightNewRow(original ?? { $index: 0 }); + }); +} </script> <template> <FetchData - url="Warehouses" - :filter="{ order: ['name'] }" + @on-fetch="(data) => (warehousesOptions = data)" auto-load - @on-fetch="(data) => onWarehousesFetched(data)" + url="Warehouses" + :filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }" /> <RightMenu> <template #right-panel> <ItemFixedPriceFilter data-key="ItemFixedPrices" - :warehouses-options="warehousesOptions" + ref="itemFixedPriceFilterRef" /> </template> </RightMenu> - <QPage class="column items-center q-pa-md"> - <QTable - :rows="fixedPrices" + <VnSubToolbar> + <template #st-data> + <QBtn + v-if="rowsSelected.length" + @click="openEditTableCellDialog()" + color="primary" + icon="edit" + > + <QTooltip> + {{ t('Edit fixed price(s)') }} + </QTooltip> + </QBtn> + <QBtn + :label="tMobile('globals.remove')" + color="primary" + icon="delete" + flat + @click="(row) => confirmRemove(row, true)" + :title="t('globals.remove')" + v-if="rowsSelected.length" + /> + </template> + </VnSubToolbar> + <QPage> + <VnTable + @on-fetch=" + (data) => + data.forEach((item) => { + item.hasMinPrice = `${item.hasMinPrice !== 0}`; + }) + " + :default-remove="false" + :default-reset="false" + :default-save="false" + data-key="ItemFixedPrices" + url="FixedPrices/filter" + :order="['itemFk ASC']" + save-url="FixedPrices/crud" + :user-params="{ warehouseFk: user.warehouseFk }" + ref="tableRef" + dense :columns="columns" - row-key="id" - selection="multiple" + default-mode="table" + auto-load + :is-editable="true" + :right-search="false" + :table="{ + 'row-key': 'id', + selection: 'multiple', + }" + :crud-model="{ + paginate: false, + }" v-model:selected="rowsSelected" - :pagination="{ rowsPerPage: 0 }" - class="full-width q-mt-md" - :no-data-label="t('globals.noResults')" + :row-click="saveOnRowChange" + :create-as-dialog="false" + :create="{ + onDataSaved: handleOnDataSave, + }" + :use-model="true" + :disable-option="{ card: true }" > - <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 #header-selection="scope"> + <QCheckbox v-model="scope.selected" /> + </template> + <template #body-selection="scope"> + {{ scope }} + <QCheckbox flat v-model="scope.selected" /> </template> - <template #body-cell-itemId="props"> - <QTd> - <VnSelect - url="Items/withName" - hide-selected - option-label="id" - option-value="id" - v-model="props.row.itemFk" - v-on="getRowUpdateInputEvents(props, true, 'select')" - > - <template #option="scope"> - <QItem v-bind="scope.itemProps"> - <QItemSection> - <QItemLabel> #{{ scope.opt?.id }} </QItemLabel> - <QItemLabel caption>{{ scope.opt?.name }}</QItemLabel> - </QItemSection> - </QItem> - </template> - </VnSelect> - </QTd> + <template #column-itemId="props"> + <VnSelect + style="max-width: 100px" + url="Items/withName" + hide-selected + option-label="id" + option-value="id" + v-model="props.row.itemFk" + v-on="getRowUpdateInputEvents(props, true, 'select')" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> #{{ scope.opt?.id }} </QItemLabel> + <QItemLabel caption>{{ scope.opt?.name }}</QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> </template> - <template #body-cell-description="{ row }"> - <QTd class="col"> - <span class="link"> - {{ row.name }} - </span> - <ItemDescriptorProxy :id="row.itemFk" /> - <FetchedTags :item="row" :max-length="6" /> - </QTd> + <template #column-description="{ row }"> + <span class="link"> + {{ row.name }} + </span> + <span class="subName">{{ row.subName }}</span> + <ItemDescriptorProxy :id="row.itemFk" /> + <FetchedTags style="width: max-content; max-width: 220px" :item="row" /> </template> - <template #body-cell-groupingPrice="props"> - <QTd class="col"> - <VnInput - v-model.number="props.row.rate2" - v-on="getRowUpdateInputEvents(props)" - > - <template #append>€</template> - </VnInput> - </QTd> + <template #column-rate2="props"> + <VnInput + mask="###.##" + v-model.number="props.row.rate2" + v-on="getRowUpdateInputEvents(props)" + > + <template #append>€</template> + </VnInput> </template> - <template #body-cell-packingPrice="props"> - <QTd class="col"> - <VnInput - v-model.number="props.row.rate3" - v-on="getRowUpdateInputEvents(props)" - > - <template #append>€</template> - </VnInput> - </QTd> + <template #column-rate3="props"> + <VnInput + mask="###.##" + v-model.number="props.row.rate3" + v-on="getRowUpdateInputEvents(props)" + > + <template #append>€</template> + </VnInput> </template> - <template #body-cell-minPrice="props"> + <template #column-minPrice="props"> <QTd class="col"> - <div class="row"> + <div class="row" style="width: 115px"> <QCheckbox - class="col" :model-value="props.row.hasMinPrice" @update:model-value="updateMinPrice($event, props)" - :false-value="0" - :true-value="1" - :toggle-indeterminate="false" + :false-value="'false'" + :true-value="'true'" /> <VnInput class="col" - :disable="!props.row.hasMinPrice" + :disable="props.row.hasMinPrice === 1" v-model.number="props.row.minPrice" v-on="getRowUpdateInputEvents(props)" - type="number" - /> + > + <template #append>€</template> + </VnInput> </div> </QTd> </template> - <template #body-cell-started="props"> - <QTd class="col" style="min-width: 160px"> - <VnInputDate - v-model="props.row.started" - v-on="getRowUpdateInputEvents(props, false, 'date')" - v-bind=" - isBigger(props.row.started) - ? { 'bg-color': 'warning', 'is-outlined': true } - : {} - " - /> - </QTd> + <template #column-started="props"> + <VnInputDate + class="vnInputDate" + :show-event="true" + v-model="props.row.started" + v-on="getRowUpdateInputEvents(props, false, 'date')" + v-bind="dateStyle(isBigger(props.row.started))" + /> </template> - <template #body-cell-ended="props"> - <QTd class="col" style="min-width: 150px"> - <VnInputDate - v-model="props.row.ended" - v-on="getRowUpdateInputEvents(props, false, 'date')" - v-bind=" - isLower(props.row.ended) - ? { 'bg-color': 'warning', 'is-outlined': true } - : {} - " - /> - </QTd> + <template #column-ended="props"> + <VnInputDate + class="vnInputDate" + :show-event="true" + v-model="props.row.ended" + v-on="getRowUpdateInputEvents(props, false, 'date')" + v-bind="dateStyle(isLower(props.row.ended))" + /> </template> - <template #body-cell-warehouse="props"> - <QTd class="col"> - <VnSelect - :options="warehousesOptions" - hide-selected - option-label="name" - option-value="id" - v-model="props.row.warehouseFk" - v-on="getRowUpdateInputEvents(props, false, 'select')" - /> - </QTd> + <template #column-warehouseFk="props"> + <VnSelect + style="max-width: 150px" + :options="warehousesOptions" + hide-selected + option-label="name" + option-value="id" + v-model="props.row.warehouseFk" + v-on="getRowUpdateInputEvents(props, false, 'select')" + /> </template> - <template #body-cell-deleteAction="{ row, rowIndex }"> - <QTd class="col"> - <QIcon - name="delete" - size="sm" - class="cursor-pointer fill-icon-on-hover" - color="primary" - @click.stop=" - openConfirmationModal( - t('This row will be removed'), - t('Do you want to clone this item?'), - () => removePrice(row.id, rowIndex) - ) - " - > - <QTooltip class="text-no-wrap"> - {{ t('Delete') }} - </QTooltip> - </QIcon> - </QTd> + <template #column-deleteAction="{ row, rowIndex }"> + <QIcon + name="delete" + size="sm" + class="cursor-pointer fill-icon-on-hover" + color="primary" + @click.stop=" + openConfirmationModal( + t('globals.rowWillBeRemoved'), + t('Do you want to clone this item?'), + () => removePrice(row.id, rowIndex) + ) + " + > + <QTooltip class="text-no-wrap"> + {{ t('globals.delete') }} + </QTooltip> + </QIcon> </template> - <template #bottom-row> - <QTd align="center"> - <QIcon - @click.stop="addRow()" - class="fill-icon-on-hover" - color="primary" - name="add_circle" - size="sm" - > - <QTooltip> - {{ t('Add fixed price') }} - </QTooltip> - </QIcon> - </QTd> - </template> - </QTable> - <QPageSticky v-if="rowsSelected.length" :offset="[20, 20]"> - <QBtn @click="openEditTableCellDialog()" color="primary" fab icon="edit" /> - <QTooltip> - {{ t('Edit fixed price(s)') }} - </QTooltip> - </QPageSticky> + </VnTable> + <QDialog ref="editTableCellDialogRef"> <EditTableCellValueForm edit-url="FixedPrices/editFixedPrice" @@ -571,12 +618,56 @@ onUnmounted(() => (stateStore.rightDrawer = false)); </QDialog> </QPage> </template> +<style lang="scss"> +.q-table th, +.q-table td { + padding-inline: 5px !important; + // text-align: -webkit-right; +} +.q-table tbody td { + max-width: none; + .q-td.col { + & .vnInputDate { + min-width: 90px; + } + & div.row { + & .q-checkbox { + & .q-checkbox__inner { + position: relative !important; + &.q-checkbox__inner--truthy { + color: var(--q-primary); + } + } + } + } + } +} +.q-field__after, +.q-field__append { + padding: 0; +} + +tbody tr.highlight .q-td { + animation: highlight-animation 4s ease-in-out; +} +@keyframes highlight-animation { + 0% { + background-color: $primary-light; + } + 100% { + background-color: transparent; + } +} +.subName { + margin-left: 5%; + font-size: 0.75rem; + text-transform: uppercase; + color: var(--vn-label-color); +} +</style> <i18n> es: Add fixed price: Añadir precio fijado Edit fixed price(s): Editar precio(s) fijado(s) - This row will be removed: Esta linea se eliminará - Are you sure you want to continue?: ¿Seguro que quieres continuar? - Delete: Eliminar </i18n> diff --git a/src/pages/Item/ItemFixedPriceFilter.vue b/src/pages/Item/ItemFixedPriceFilter.vue index 6c847ece8..84eefaed3 100644 --- a/src/pages/Item/ItemFixedPriceFilter.vue +++ b/src/pages/Item/ItemFixedPriceFilter.vue @@ -9,18 +9,29 @@ import ItemsFilterPanel from 'src/components/ItemsFilterPanel.vue'; const { t } = useI18n(); -defineProps({ +const props = defineProps({ dataKey: { type: String, required: true, }, - warehousesOptions: { - type: Array, - default: () => [], - }, }); const itemTypeWorkersOptions = ref([]); +const exprBuilder = (param, value) => { + switch (param) { + case 'name': + return { 'i.name': { like: `%${value}%` } }; + case 'itemFk': + case 'warehouseFk': + case 'rate2': + case 'rate3': + param = `fp.${param}`; + return { [param]: value }; + case 'minPrice': + param = `i.${param}`; + return { [param]: value }; + } +}; </script> <template> @@ -31,7 +42,7 @@ const itemTypeWorkersOptions = ref([]); :filter="{ fields: ['id', 'nickname'], order: 'nickname ASC', limit: 30 }" @on-fetch="(data) => (itemTypeWorkersOptions = data)" /> - <ItemsFilterPanel :data-key="dataKey" :custom-tags="['tags']"> + <ItemsFilterPanel :data-key="props.dataKey" :custom-tags="['tags']"> <template #body="{ params, searchFn }"> <QItem class="q-my-md"> <QItemSection> @@ -52,9 +63,11 @@ const itemTypeWorkersOptions = ref([]); <QItem class="q-my-md"> <QItemSection> <VnSelect + url="Warehouses" + auto-load + :filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }" :label="t('components.itemsFilterPanel.warehouseFk')" v-model="params.warehouseFk" - :options="warehousesOptions" option-label="name" option-value="id" dense @@ -93,8 +106,15 @@ const itemTypeWorkersOptions = ref([]); toggle-indeterminate @update:model-value="searchFn()" /> - </QItemSection> - <QItemSection> + + <QCheckbox + v-model="params.showBadDates" + :label="t(`components.itemsFilterPanel.showBadDates`)" + toggle-indeterminate + @update:model-value="searchFn()" + > + </QCheckbox> + <QCheckbox :label="t('components.itemsFilterPanel.hasMinPrice')" v-model="params.hasMinPrice" diff --git a/src/pages/Item/ItemList.vue b/src/pages/Item/ItemList.vue index f1e3629cd..e3fcceb3a 100644 --- a/src/pages/Item/ItemList.vue +++ b/src/pages/Item/ItemList.vue @@ -517,7 +517,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <template #body-cell-description="{ row }"> <QTd class="col"> <span>{{ row.name }} {{ row.subName }}</span> - <FetchedTags :item="row" :max-length="6" /> + <FetchedTags :item="row" /> </QTd> </template> <template #body-cell-isActive="{ row }"> @@ -562,7 +562,13 @@ onUnmounted(() => (stateStore.rightDrawer = false)); </VnPaginate> <QPageSticky :offset="[20, 20]"> - <QBtn @click="redirectToItemCreate()" color="primary" fab icon="add" /> + <QBtn + @click="redirectToItemCreate()" + color="primary" + fab + icon="add" + shortcut="+" + /> <QTooltip class="text-no-wrap"> {{ t('New item') }} </QTooltip> diff --git a/src/pages/Item/ItemListFilter.vue b/src/pages/Item/ItemListFilter.vue index 62c0c56dc..22dce9c64 100644 --- a/src/pages/Item/ItemListFilter.vue +++ b/src/pages/Item/ItemListFilter.vue @@ -30,7 +30,7 @@ const itemTypesRef = ref(null); const categoriesOptions = ref([]); const itemTypesOptions = ref([]); const buyersOptions = ref([]); -const suppliersOptions = ref([]); +const tagOptions = ref([]); const tagValues = ref([]); const fieldFiltersValues = ref([]); const moreFields = ref([]); @@ -161,12 +161,6 @@ onMounted(async () => { @on-fetch="(data) => (buyersOptions = data)" auto-load /> - <FetchData - url="Suppliers" - :filter="{ fields: ['id', 'name', 'nickname'], order: 'name ASC' }" - @on-fetch="(data) => (suppliersOptions = data)" - auto-load - /> <FetchData url="Tags" :filter="{ fields: ['id', 'name', 'isFree'] }" @@ -261,9 +255,11 @@ onMounted(async () => { :label="t('params.supplierFk')" v-model="params.supplierFk" @update:model-value="searchFn()" - :options="suppliersOptions" + url="Suppliers" option-value="id" option-label="name" + :fields="['id', 'name', 'nickname']" + sort-by="name ASC" hide-selected dense outlined diff --git a/src/pages/Item/ItemRequest.vue b/src/pages/Item/ItemRequest.vue index ae6638953..8a41bbe04 100644 --- a/src/pages/Item/ItemRequest.vue +++ b/src/pages/Item/ItemRequest.vue @@ -1,7 +1,6 @@ <script setup> import { ref, computed, onMounted, onBeforeMount, watch } from 'vue'; import { useI18n } from 'vue-i18n'; -import FetchData from 'components/FetchData.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; @@ -22,7 +21,6 @@ import RightMenu from 'src/components/common/RightMenu.vue'; const { t } = useI18n(); const { notify } = useNotify(); const stateStore = useStateStore(); -const workersOptions = ref([]); let filterParams = ref({}); const denyFormRef = ref(null); const denyRequestId = ref(null); @@ -208,13 +206,6 @@ onBeforeMount(() => { </script> <template> - <FetchData - url="Workers" - :filter="{ where: { role: 'buyer' } }" - order="id" - @on-fetch="(data) => (workersOptions = data)" - auto-load - /> <VnSearchbar data-key="ItemRequests" url="TicketRequests/filter" @@ -268,7 +259,9 @@ onBeforeMount(() => { <QTd> <VnSelect v-model="row.attenderFk" - :options="workersOptions" + :where="{ role: 'buyer' }" + sort-by="id" + url="Workers" hide-selected option-label="firstName" option-value="id" diff --git a/src/pages/Item/ItemRequestFilter.vue b/src/pages/Item/ItemRequestFilter.vue index c9340dc1f..aa07b8d50 100644 --- a/src/pages/Item/ItemRequestFilter.vue +++ b/src/pages/Item/ItemRequestFilter.vue @@ -24,7 +24,6 @@ const stateOptions = [ const itemTypesOptions = ref([]); const warehousesOptions = ref([]); -const workersOptions = ref([]); const exprBuilder = (param, value) => { switch (param) { @@ -72,18 +71,6 @@ const decrement = (paramsObj, key) => { @on-fetch="(data) => (warehousesOptions = data)" auto-load /> - <FetchData - url="Workers/search" - :filter="{ - fields: ['id', 'name'], - order: 'name ASC', - }" - :params="{ - departmentCodes: ['VT'], - }" - @on-fetch="(data) => (workersOptions = data)" - auto-load - /> <VnFilterPanel :data-key="props.dataKey" :search-button="true" @@ -162,7 +149,10 @@ const decrement = (paramsObj, key) => { :label="t('params.requesterFk')" v-model="params.requesterFk" @update:model-value="searchFn()" - :options="workersOptions" + url="Workers/search" + :fields="['id', 'name']" + order="name ASC" + :params="{ departmentCodes: ['VT'] }" option-value="id" option-label="name" hide-selected diff --git a/src/pages/Item/ItemTypeList.vue b/src/pages/Item/ItemTypeList.vue index 125672d60..d874a5dcb 100644 --- a/src/pages/Item/ItemTypeList.vue +++ b/src/pages/Item/ItemTypeList.vue @@ -99,7 +99,13 @@ const exprBuilder = (param, value) => { </div> </QPage> <QPageSticky :offset="[20, 20]"> - <QBtn fab icon="add" color="primary" @click="redirectToCreateView()" /> + <QBtn + fab + icon="add" + color="primary" + @click="redirectToCreateView()" + shortcut="+" + /> <QTooltip> {{ t('New item type') }} </QTooltip> diff --git a/src/pages/Login/LoginMain.vue b/src/pages/Login/LoginMain.vue index c17201969..44b868ebd 100644 --- a/src/pages/Login/LoginMain.vue +++ b/src/pages/Login/LoginMain.vue @@ -1,6 +1,6 @@ <script setup> import { ref } from 'vue'; -import { Notify, useQuasar } from 'quasar'; +import { Notify } from 'quasar'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; @@ -11,7 +11,6 @@ import VnLogo from 'components/ui/VnLogo.vue'; import VnInput from 'src/components/common/VnInput.vue'; import axios from 'axios'; -const quasar = useQuasar(); const session = useSession(); const loginCache = useLogin(); const router = useRouter(); diff --git a/src/pages/Login/ResetPassword.vue b/src/pages/Login/ResetPassword.vue index eff718e97..2751f1ceb 100644 --- a/src/pages/Login/ResetPassword.vue +++ b/src/pages/Login/ResetPassword.vue @@ -33,7 +33,6 @@ async function onSubmit() { }; try { - console.log('newPassword: ', newPassword); await axios.post( 'VnUsers/reset-password', { newPassword: newPassword.value }, diff --git a/src/pages/Monitor/MonitorClients.vue b/src/pages/Monitor/MonitorClients.vue new file mode 100644 index 000000000..ff51e4464 --- /dev/null +++ b/src/pages/Monitor/MonitorClients.vue @@ -0,0 +1,145 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; +import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; +import { toDateFormat } from 'src/filters/date.js'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import VnInputDate from 'src/components/common/VnInputDate.vue'; +import VnRow from 'src/components/ui/VnRow.vue'; +import { dateRange } from 'src/filters'; +const { t } = useI18n(); + +const dates = dateRange(Date.vnNew()); +const from = ref(dates[0]); +const to = ref(dates[1]); + +const filter = computed(() => { + const obj = {}; + const formatFrom = setHours(from.value, 'from'); + const formatTo = setHours(to.value, 'to'); + let stamp; + + if (!formatFrom && formatTo) stamp = { lte: formatTo }; + else if (formatFrom && !formatTo) stamp = { gte: formatFrom }; + else if (formatFrom && formatTo) stamp = { between: [formatFrom, formatTo] }; + + return Object.assign(obj, { where: { 'v.stamp': stamp } }); +}); + +function exprBuilder(param, value) { + switch (param) { + case 'clientFk': + return { [`c.id`]: value }; + case 'salesPersonFk': + return { [`c.${param}`]: value }; + } +} + +function setHours(date, type) { + if (!date) return null; + + const d = new Date(date); + if (type == 'from') d.setHours(0, 0, 0, 0); + else d.setHours(23, 59, 59, 59); + return d; +} + +const columns = computed(() => [ + { + label: t('salesClientsTable.date'), + name: 'dated', + field: 'dated', + align: 'left', + columnFilter: false, + format: (row) => toDateFormat(row.dated, 'es-ES', { year: '2-digit' }), + }, + { + label: t('salesClientsTable.hour'), + name: 'hour', + field: 'hour', + align: 'left', + columnFilter: false, + }, + { + label: t('salesClientsTable.salesPerson'), + name: 'salesPersonFk', + field: 'salesPerson', + align: 'left', + columnField: { + component: null, + }, + optionFilter: 'firstName', + columnFilter: { + component: 'select', + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + sortBy: 'nickname ASC', + where: { role: 'salesPerson' }, + useLike: false, + }, + }, + columnClass: 'no-padding', + }, + { + label: t('salesClientsTable.client'), + field: 'clientName', + name: 'clientFk', + align: 'left', + columnField: { + component: null, + }, + orderBy: 'c.name', + columnFilter: { + component: 'select', + attrs: { + url: 'Clients', + fields: ['id', 'name'], + sortBy: 'name ASC', + }, + }, + columnClass: 'no-padding', + }, +]); +</script> + +<template> + <VnTable + ref="table" + data-key="SalesMonitorClients" + url="SalesMonitors/clientsFilter" + search-url="SalesMonitorClients" + :order="['dated DESC', 'hour DESC']" + :expr-builder="exprBuilder" + :filter="filter" + :offset="50" + auto-load + :columns="columns" + :right-search="false" + default-mode="table" + :disable-option="{ card: true }" + dense + class="q-px-none" + > + <template #top-left> + <VnRow> + <VnInputDate v-model="from" :label="$t('globals.from')" dense /> + <VnInputDate v-model="to" :label="$t('globals.to')" dense /> + </VnRow> + </template> + <template #column-salesPersonFk="{ row }"> + <span class="link" :title="row.salesPerson" v-text="row.salesPerson" /> + <WorkerDescriptorProxy :id="row.salesPersonFk" dense /> + </template> + <template #column-clientFk="{ row }"> + <span class="link" :title="row.clientName" v-text="row.clientName" /> + <CustomerDescriptorProxy :id="row.clientFk" /> + </template> + </VnTable> +</template> +<style lang="scss" scoped> +.full-width .vn-row > * { + flex: 0.4; +} +</style> diff --git a/src/pages/Monitor/MonitorClientsActions.vue b/src/pages/Monitor/MonitorClientsActions.vue new file mode 100644 index 000000000..821773bbf --- /dev/null +++ b/src/pages/Monitor/MonitorClientsActions.vue @@ -0,0 +1,26 @@ +<script setup> +import SalesClientTable from './MonitorClients.vue'; +import SalesOrdersTable from './MonitorOrders.vue'; +import VnRow from 'src/components/ui/VnRow.vue'; +</script> +<template> + <VnRow + class="q-pa-md" + :style="{ 'flex-direction': $q.screen.lt.lg ? 'column' : 'row', gap: '0px' }" + > + <div style="flex: 0.3"> + <span + class="q-ml-md text-body1" + v-text="$t('salesMonitor.clientsOnWebsite')" + /> + <SalesClientTable /> + </div> + <div style="flex: 0.7"> + <span + class="q-ml-md text-body1" + v-text="$t('salesMonitor.recentOrderActions')" + /> + <SalesOrdersTable /> + </div> + </VnRow> +</template> diff --git a/src/pages/Monitor/MonitorList.vue b/src/pages/Monitor/MonitorList.vue deleted file mode 100644 index 4906247e8..000000000 --- a/src/pages/Monitor/MonitorList.vue +++ /dev/null @@ -1,90 +0,0 @@ -<script setup> -import { onMounted, onUnmounted, ref } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import { useStateStore } from 'stores/useStateStore'; -import SalesClientTable from './SalesClientsTable.vue'; -import SalesOrdersTable from './SalesOrdersTable.vue'; -import SalesTicketsTable from './SalesTicketsTable.vue'; -import VnSearchbar from 'components/ui/VnSearchbar.vue'; - -const { t } = useI18n(); -const stateStore = useStateStore(); - -const expanded = ref(true); - -onMounted(async () => { - stateStore.leftDrawer = false; -}); - -onUnmounted(() => (stateStore.leftDrawer = true)); -</script> - -<template> - <template v-if="stateStore.isHeaderMounted()"> - <Teleport to="#searchbar"> - <VnSearchbar - data-key="SalesMonitorTickets" - url="SalesMonitors/salesFilter" - :redirect="false" - :label="t('searchBar.label')" - :info="t('searchBar.info')" - /> - </Teleport> - </template> - <QPage class="column items-center q-pa-md"> - <QCard class="full-width q-mb-lg"> - <QExpansionItem v-model="expanded" dense :duration="150"> - <template v-if="!expanded" #header> - <div class="row full-width"> - <span class="flex col text-body1"> - {{ t('salesMonitor.clientsOnWebsite') }} - </span> - <span class="flex col q-ml-xl text-body1"> - {{ t('salesMonitor.recentOrderActions') }} - </span> - </div> - </template> - <template #default> - <div class="expansion-tables-container"> - <QCardSection class="col"> - <span class="flex col q-mb-sm text-body1"> - {{ t('salesMonitor.clientsOnWebsite') }} - </span> - <SalesClientTable /> - </QCardSection> - <QCardSection class="col"> - <span class="flex col q-mb-sm text-body1"> - {{ t('salesMonitor.recentOrderActions') }} - </span> - <SalesOrdersTable /> - </QCardSection> - </div> - </template> - </QExpansionItem> - </QCard> - <QCard class="full-width"> - <QItem class="justify-between"> - <QItemLabel class="col slider-container"> - <span class="text-body1" - >{{ t('salesMonitor.ticketsMonitor') }} - </span> - <QCardSection class="col" style="padding-inline: 0" - ><SalesTicketsTable /> - </QCardSection> - </QItemLabel> - </QItem> - </QCard> - </QPage> -</template> - -<style lang="scss" scoped> -.expansion-tables-container { - display: flex; - border-top: 1px solid $color-spacer; - - @media (max-width: $breakpoint-md-max) { - flex-direction: column; - } -} -</style> diff --git a/src/pages/Monitor/MonitorOrders.vue b/src/pages/Monitor/MonitorOrders.vue new file mode 100644 index 000000000..eb455a239 --- /dev/null +++ b/src/pages/Monitor/MonitorOrders.vue @@ -0,0 +1,203 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; +import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; +import VnTable from 'components/VnTable/VnTable.vue'; + +import { toDateFormat, toDateTimeFormat } from 'src/filters/date.js'; +import { toCurrency } from 'src/filters'; +import { useVnConfirm } from 'composables/useVnConfirm'; +import axios from 'axios'; + +const { t } = useI18n(); +const { openConfirmationModal } = useVnConfirm(); + +const table = ref(); +const selectedRows = ref([]); + +function exprBuilder(param, value) { + switch (param) { + case 'clientFk': + return { [`c.id`]: value }; + case 'salesPersonFk': + return { [`c.salesPersonFk`]: value }; + } +} + +const columns = computed(() => [ + { + label: t('salesOrdersTable.dateSend'), + name: 'dateSend', + field: 'dateSend', + align: 'left', + orderBy: 'date_send', + columnFilter: false, + }, + { + label: t('salesOrdersTable.dateMake'), + name: 'dateMake', + field: 'dateMake', + align: 'left', + orderBy: 'date_make', + columnFilter: false, + format: (row) => toDateTimeFormat(row.date_make), + }, + { + label: t('salesOrdersTable.client'), + name: 'clientFk', + align: 'left', + columnFilter: { + component: 'select', + attrs: { + url: 'Clients', + fields: ['id', 'name'], + sortBy: 'name ASC', + }, + }, + }, + { + label: t('salesOrdersTable.agency'), + name: 'agencyName', + align: 'left', + columnFilter: false, + }, + { + label: t('salesOrdersTable.salesPerson'), + name: 'salesPersonFk', + align: 'left', + optionFilter: 'firstName', + columnFilter: { + component: 'select', + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + sortBy: 'nickname ASC', + where: { role: 'salesPerson' }, + useLike: false, + }, + }, + }, + { + label: t('salesOrdersTable.import'), + name: 'import', + field: 'import', + align: 'left', + columnFilter: false, + format: (row) => toCurrency(row.import), + }, +]); + +const getBadgeColor = (date) => { + const today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + + const orderLanded = new Date(date); + orderLanded.setHours(0, 0, 0, 0); + + const difference = today - orderLanded; + + if (difference == 0) return 'warning'; + if (difference < 0) return 'success'; + if (difference > 0) return 'alert'; +}; + +const removeOrders = async () => { + try { + const selectedIds = selectedRows.value.map((row) => row.id); + const params = { deletes: selectedIds }; + await axios.post('SalesMonitors/deleteOrders', params); + selectedRows.value = []; + await table.value.reload(); + } catch (err) { + console.error('Error deleting orders', err); + } +}; + +const openTab = (id) => + window.open(`#/order/${id}/summary`, '_blank', 'noopener, noreferrer'); +</script> +<template> + <VnTable + ref="table" + class="q-px-none" + data-key="SalesMonitorOrders" + url="SalesMonitors/ordersFilter" + search-url="SalesMonitorOrders" + order="date_send DESC" + :right-search="false" + :expr-builder="exprBuilder" + auto-load + :columns="columns" + :table="{ + 'row-key': 'id', + selection: 'multiple', + 'hide-bottom': true, + }" + default-mode="table" + :row-click="({ id }) => openTab(id)" + v-model:selected="selectedRows" + :disable-option="{ card: true }" + > + <template #top-left> + <QBtn + icon="refresh" + size="md" + color="primary" + dense + flat + @click="$refs.table.reload()" + > + <QTooltip>{{ $t('globals.refresh') }}</QTooltip> + </QBtn> + <QBtn + v-if="selectedRows.length" + icon="delete" + size="md" + dense + flat + color="primary" + @click=" + openConfirmationModal( + $t('salesOrdersTable.deleteConfirmTitle'), + $t('salesOrdersTable.deleteConfirmMessage'), + removeOrders + ) + " + > + <QTooltip>{{ t('salesOrdersTable.delete') }}</QTooltip> + </QBtn> + </template> + <template #column-dateSend="{ row }"> + <QTd> + <QBadge + :color="getBadgeColor(row.date_send)" + text-color="black" + class="q-pa-sm" + style="font-size: 14px" + > + {{ toDateFormat(row.date_send) }} + </QBadge> + </QTd> + </template> + + <template #column-clientFk="{ row }"> + <QTd @click.stop> + <span class="link" v-text="row.clientName" :title="row.clientName" /> + <CustomerDescriptorProxy :id="row.clientFk" /> + </QTd> + </template> + + <template #column-salesPersonFk="{ row }"> + <QTd @click.stop> + <span class="link" v-text="row.salesPerson" /> + <WorkerDescriptorProxy :id="row.salesPersonFk" dense /> + </QTd> + </template> + </VnTable> +</template> +<style lang="scss" scoped> +.q-td { + max-width: 140px; +} +</style> diff --git a/src/pages/Monitor/SalesClientsTable.vue b/src/pages/Monitor/SalesClientsTable.vue deleted file mode 100644 index 2fb9e8e2f..000000000 --- a/src/pages/Monitor/SalesClientsTable.vue +++ /dev/null @@ -1,147 +0,0 @@ -<script setup> -import { ref, computed, reactive, watch } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import FetchData from 'components/FetchData.vue'; -import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; - -import { toDateFormat } from 'src/filters/date.js'; -import VnTable from 'src/components/VnTable/VnTable.vue'; - -const { t } = useI18n(); - -const paginateRef = ref(null); -const workersActiveOptions = ref([]); -const clientsOptions = ref([]); - -const from = ref(Date.vnNew()); -const to = ref(Date.vnNew()); - -const dateRange = computed(() => { - const minHour = new Date(from.value); - minHour.setHours(0, 0, 0, 0); - const maxHour = new Date(to.value); - maxHour.setHours(23, 59, 59, 59); - return [minHour, maxHour]; -}); - -const filter = reactive({ - where: { - 'v.stamp': { - between: dateRange.value, - }, - }, -}); - -const refetch = async () => await paginateRef.value.fetch(); - -watch(dateRange, (val) => { - filter.where['v.stamp'].between = val; - refetch(); -}); - -function exprBuilder(param, value) { - switch (param) { - case 'clientFk': - return { [`c.id`]: value }; - case 'salesPersonFk': - return { [`c.${param}`]: value }; - } -} - -const params = reactive({}); - -const columns = computed(() => [ - { - label: t('salesClientsTable.date'), - name: 'dated', - field: 'dated', - align: 'left', - columnFilter: null, - sortable: true, - format: (row) => toDateFormat(row.dated), - }, - { - label: t('salesClientsTable.hour'), - name: 'hour', - field: 'hour', - align: 'left', - sortable: true, - }, - { - label: t('salesClientsTable.salesPerson'), - name: 'salesPerson', - field: 'salesPerson', - align: 'left', - sortable: true, - columnField: { - component: null, - }, - }, - { - label: t('salesClientsTable.client'), - field: 'clientName', - name: 'client', - align: 'left', - sortable: true, - columnField: { - component: null, - }, - }, -]); -</script> - -<template> - <FetchData - url="Workers/activeWithInheritedRole" - :filter="{ - fields: ['id', 'nickname'], - order: 'nickname ASC', - where: { role: 'salesPerson' }, - }" - auto-load - @on-fetch="(data) => (workersActiveOptions = data)" - /> - <FetchData - url="Clients" - :filter="{ - fields: ['id', 'name'], - order: 'name ASC', - }" - auto-load - @on-fetch="(data) => (clientsOptions = data)" - /> - <QCard style="max-height: 380px; overflow-y: scroll"> - <VnTable - ref="paginateRef" - data-key="SalesMonitorClients" - url="SalesMonitors/clientsFilter" - :order="['dated DESC', 'hour DESC']" - :limit="6" - :expr-builder="exprBuilder" - :user-params="params" - :filter="filter" - :offset="50" - auto-load - :columns="columns" - :right-search="false" - default-mode="table" - dense - :without-header="true" - > - <template #column-salesPerson="{ row }"> - <QTd> - <span class="link">{{ row.salesPerson }}</span> - <WorkerDescriptorProxy :id="row.salesPersonFk" dense /> - </QTd> - </template> - <template #column-client="{ row }"> - <QTd> - <span class="link">{{ row.clientName }}</span> - <CustomerDescriptorProxy :id="row.clientFk" /> - </QTd> - </template> - </VnTable> - </QCard> -</template> diff --git a/src/pages/Monitor/SalesOrdersTable.vue b/src/pages/Monitor/SalesOrdersTable.vue deleted file mode 100644 index f0c389aa6..000000000 --- a/src/pages/Monitor/SalesOrdersTable.vue +++ /dev/null @@ -1,204 +0,0 @@ -<script setup> -import { ref, computed } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import FetchData from 'components/FetchData.vue'; -import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; -import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; -import VnTable from 'components/VnTable/VnTable.vue'; - -import { toDateFormat, toDateTimeFormat } from 'src/filters/date.js'; -import { toCurrency } from 'src/filters'; -import { useVnConfirm } from 'composables/useVnConfirm'; -import axios from 'axios'; - -const { t } = useI18n(); -const { openConfirmationModal } = useVnConfirm(); - -const paginateRef = ref(null); -const workersActiveOptions = ref([]); -const clientsOptions = ref([]); -const selectedRows = ref([]); - -const dateRange = (value) => { - const minHour = new Date(value); - minHour.setHours(0, 0, 0, 0); - const maxHour = new Date(value); - maxHour.setHours(23, 59, 59, 59); - - return [minHour, maxHour]; -}; - -function exprBuilder(param, value) { - switch (param) { - case 'date_send': - return { - [`o.date_send`]: { - between: dateRange(value), - }, - }; - case 'clientFk': - return { [`c.id`]: value }; - case 'salesPersonFk': - return { [`c.${param}`]: value }; - } -} - -const columns = computed(() => [ - { - label: t('salesOrdersTable.date'), - name: 'date', - field: 'dated', - align: 'left', - sortable: true, - cardVisible: true, - }, - { - label: t('salesOrdersTable.client'), - name: 'client', - align: 'left', - sortable: true, - cardVisible: true, - }, - { - label: t('salesOrdersTable.salesPerson'), - name: 'salesPerson', - align: 'left', - sortable: true, - cardVisible: true, - }, -]); - -const getBadgeColor = (date) => { - const today = Date.vnNew(); - today.setHours(0, 0, 0, 0); - - const orderLanded = new Date(date); - orderLanded.setHours(0, 0, 0, 0); - - const difference = today - orderLanded; - - if (difference == 0) return 'warning'; - if (difference < 0) return 'success'; - if (difference > 0) return 'alert'; -}; - -const removeOrders = async () => { - try { - const selectedIds = selectedRows.value.map((row) => row.id); - const params = { deletes: selectedIds }; - await axios.post('SalesMonitors/deleteOrders', params); - selectedRows.value = []; - await paginateRef.value.fetch(); - } catch (err) { - console.error('Error deleting orders', err); - } -}; - -const redirectToOrderSummary = (orderId) => { - const url = `#/order/${orderId}/summary`; - window.open(url, '_blank'); -}; -</script> - -<template> - <FetchData - url="Workers/activeWithInheritedRole" - :filter="{ - fields: ['id', 'nickname'], - order: 'nickname ASC', - where: { role: 'salesPerson' }, - }" - auto-load - @on-fetch="(data) => (workersActiveOptions = data)" - /> - <FetchData - url="Clients" - :filter="{ - fields: ['id', 'name'], - order: 'name ASC', - }" - auto-load - @on-fetch="(data) => (clientsOptions = data)" - /> - - <VnSubToolbar /> - <QCard style="max-height: 380px; overflow-y: scroll"> - <VnTable - ref="paginateRef" - data-key="SalesMonitorOrders" - url="SalesMonitors/ordersFilter" - order="date_make DESC" - :limit="6" - :right-search="false" - :expr-builder="exprBuilder" - auto-load - :columns="columns" - :table="{ - 'row-key': 'id', - selection: 'multiple', - 'hide-bottom': true, - }" - default-mode="table" - :without-header="false" - @row-click="(_, row) => redirectToOrderSummary(row.id)" - v-model:selected="selectedRows" - > - <template #top-left> - <QBtn - v-if="selectedRows.length > 0" - icon="delete" - size="md" - color="primary" - @click=" - openConfirmationModal( - t('salesOrdersTable.deleteConfirmTitle'), - t('salesOrdersTable.deleteConfirmMessage'), - removeOrders - ) - " - > - <QTooltip>{{ t('salesOrdersTable.delete') }}</QTooltip> - </QBtn> - </template> - <template #column-date="{ row }"> - <QTd> - <QBadge - :color="getBadgeColor(row.date_send)" - text-color="black" - class="q-pa-sm q-mb-md" - style="font-size: 14px" - > - {{ toDateFormat(row.date_send) }} - </QBadge> - <div>{{ toDateTimeFormat(row.date_make) }}</div> - </QTd> - </template> - <template #column-client="{ row }"> - <QTd> - <div class="q-mb-md"> - <span class="link">{{ row.clientName }}</span> - <CustomerDescriptorProxy :id="row.clientFk" /> - </div> - <span> {{ row.agencyName }}</span> - </QTd> - </template> - - <template #column-salesPerson="{ row }"> - <QTd> - <div class="q-mb-md"> - <span class="link">{{ row.salesPerson }}</span> - <WorkerDescriptorProxy :id="row.salesPersonFk" dense /> - </div> - <span>{{ toCurrency(row.import) }}</span> - </QTd> - </template> - </VnTable> - </QCard> -</template> -<style lang="scss" scoped> -.q-td { - color: gray; -} -</style> diff --git a/src/pages/Monitor/Ticket/MonitorTicketFilter.vue b/src/pages/Monitor/Ticket/MonitorTicketFilter.vue new file mode 100644 index 000000000..167a10465 --- /dev/null +++ b/src/pages/Monitor/Ticket/MonitorTicketFilter.vue @@ -0,0 +1,286 @@ +<script setup> +import { ref } from 'vue'; +import { useI18n } from 'vue-i18n'; + +import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; +import VnFilterPanelChip from 'src/components/ui/VnFilterPanelChip.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +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'; + +defineProps({ dataKey: { type: String, required: true } }); +const { t } = useI18n(); +const warehouses = ref(); +const groupedStates = ref(); + +const handleScopeDays = (params, days, callback) => { + const [from, to] = dateRange(Date.vnNew()); + if (!days) { + Object.assign(params, { from, to, scopeDays: 1 }); + } else { + params.from = from; + to.setDate(to.getDate() + days); + params.to = to; + } + if (callback) callback(); +}; +</script> +<template> + <FetchData url="Warehouses" auto-load @on-fetch="(data) => (warehouses = data)" /> + <FetchData + url="AlertLevels" + auto-load + @on-fetch=" + (data) => + (groupedStates = data.map((x) => Object.assign(x, { code: t(x.code) }))) + " + /> + <VnFilterPanel + :data-key="dataKey" + :search-button="true" + :hidden-tags="['from', 'to']" + :custom-tags="['scopeDays']" + :unremovable-params="['from', 'to', 'scopeDays']" + > + <template #tags="{ tag, formatFn }"> + <div class="q-gutter-x-xs"> + <strong v-text="`${t(`params.${tag.label}`)}:`" /> + <span v-text="formatFn(tag.value)" /> + </div> + </template> + <template #customTags="{ params, searchFn, formatFn }"> + <VnFilterPanelChip + v-if="params.scopeDays" + removable + @remove="handleScopeDays(params, null, searchFn)" + > + <strong v-text="`${t(`params.scopeDays`)}:`" /> + <span v-text="formatFn(params.scopeDays)" /> + </VnFilterPanelChip> + </template> + <template #body="{ params }"> + <QItem> + <QItemSection> + <VnInput + :label="t('params.clientFk')" + v-model="params.clientFk" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + :label="t('params.orderFk')" + v-model="params.orderFk" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInputNumber + :label="t('params.scopeDays')" + v-model="params.scopeDays" + is-outlined + @update:model-value="(val) => handleScopeDays(params, val)" + @remove="(val) => handleScopeDays(params, val)" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + :label="t('params.nickname')" + v-model="params.nickname" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.salesPersonFk')" + v-model="params.salesPersonFk" + url="Workers/search" + :params="{ departmentCodes: ['VT'] }" + is-outlined + option-value="code" + 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> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnInput + :label="t('params.refFk')" + v-model="params.refFk" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.agencyModeFk')" + v-model="params.agencyModeFk" + url="AgencyModes/isActive" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.stateFk')" + v-model="params.stateFk" + url="States" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.groupedStates')" + v-model="params.alertLevel" + :options="groupedStates" + option-label="code" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.warehouseFk')" + v-model="params.warehouseFk" + :options="warehouses" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + outlined + dense + rounded + :label="t('params.provinceFk')" + v-model="params.provinceFk" + url="Provinces" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QCheckbox + :label="t('params.myTeam')" + v-model="params.myTeam" + toggle-indeterminate + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QCheckbox + :label="t('params.problems')" + v-model="params.problems" + toggle-indeterminate + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <QCheckbox + :label="t('params.pending')" + v-model="params.pending" + toggle-indeterminate + /> + </QItemSection> + </QItem> + </template> + </VnFilterPanel> +</template> +<i18n> +en: + params: + clientFk: Client id + orderFk: Order id + scopeDays: Days onward + nickname: Nickname + salesPersonFk: Sales person + refFk: Invoice + agencyModeFk: Agency + stateFk: State + groupedStates: Grouped State + warehouseFk: Warehouse + provinceFk: Province + myTeam: My team + problems: With problems + pending: Pending + from: From + to: To + alertLevel: Grouped State + FREE: Free + DELIVERED: Delivered + ON_PREPARATION: On preparation + ON_PREVIOUS: On previous + PACKED: Packed + No one: No one + +es: + params: + clientFk: Id cliente + orderFk: Id cesta + scopeDays: Días en adelante + nickname: Nombre mostrado + salesPersonFk: Comercial + refFk: Factura + agencyModeFk: Agencia + stateFk: Estado + groupedStates: Estado agrupado + warehouseFk: Almacén + provinceFk: Provincia + myTeam: Mi equipo + problems: Con problemas + pending: Pendiente + from: Desde + To: Hasta + alertLevel: Estado agrupado + FREE: Libre + DELIVERED: Servido + ON_PREPARATION: En preparación + ON_PREVIOUS: En previa + PACKED: Encajado +</i18n> diff --git a/src/pages/Monitor/Ticket/MonitorTicketSearchbar.vue b/src/pages/Monitor/Ticket/MonitorTicketSearchbar.vue new file mode 100644 index 000000000..4950ab381 --- /dev/null +++ b/src/pages/Monitor/Ticket/MonitorTicketSearchbar.vue @@ -0,0 +1,12 @@ +<script setup> +import VnSearchbar from 'components/ui/VnSearchbar.vue'; +</script> +<template> + <VnSearchbar + data-key="SalesMonitorTickets" + url="SalesMonitors/salesFilter" + :redirect="false" + :label="$t('searchBar.label')" + :info="$t('searchBar.info')" + /> +</template> diff --git a/src/pages/Monitor/SalesTicketsTable.vue b/src/pages/Monitor/Ticket/MonitorTickets.vue similarity index 62% rename from src/pages/Monitor/SalesTicketsTable.vue rename to src/pages/Monitor/Ticket/MonitorTickets.vue index 1ca68240b..258b5022f 100644 --- a/src/pages/Monitor/SalesTicketsTable.vue +++ b/src/pages/Monitor/Ticket/MonitorTickets.vue @@ -1,43 +1,38 @@ <script setup> -import { ref, computed, onMounted, reactive } from 'vue'; +import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; -import { useRouter } from 'vue-router'; - import FetchData from 'components/FetchData.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; -import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnTable from 'components/VnTable/VnTable.vue'; - import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import { toDateFormat, toTimeFormat } from 'src/filters/date.js'; -import { toCurrency, dateRange } from 'src/filters'; -const DEFAULT_AUTO_REFRESH = 1000; +import { toDateFormat } from 'src/filters/date.js'; +import { toCurrency, dateRange, dashIfEmpty } from 'src/filters'; +import RightMenu from 'src/components/common/RightMenu.vue'; +import MonitorTicketSearchbar from './MonitorTicketSearchbar.vue'; +import MonitorTicketFilter from './MonitorTicketFilter.vue'; + +const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000; // 2min in ms const { t } = useI18n(); const autoRefresh = ref(false); -const router = useRouter(); -const paginateRef = ref(null); -const workersActiveOptions = ref([]); -const provincesOptions = ref([]); -const statesOptions = ref([]); -const zonesOptions = ref([]); +const tableRef = ref(null); +const provinceOpts = ref([]); +const stateOpts = ref([]); +const zoneOpts = ref([]); const visibleColumns = ref([]); -const allColumnNames = ref([]); const { viewSummary } = useSummaryDialog(); +const [from, to] = dateRange(Date.vnNew()); function exprBuilder(param, value) { switch (param) { case 'stateFk': return { 'ts.stateFk': value }; case 'salesPersonFk': - return { 'c.salesPersonFk': value }; + return { 'c.salesPersonFk': !value ? null : value }; case 'provinceFk': return { 'a.provinceFk': value }; case 'theoreticalHour': @@ -54,55 +49,12 @@ function exprBuilder(param, value) { } } -const filter = { order: ['totalProblems DESC'] }; -let params = reactive({}); - -const applyColumnFilter = async (col) => { - try { - const paramKey = col.columnFilter?.filterParamKey || col.field; - params[paramKey] = col.columnFilter.filterValue; - await paginateRef.value.addFilter(null, params); - } catch (err) { - console.error('Error applying column filter', err); - } -}; - -const getInputEvents = (col) => { - return col.columnFilter.type === 'select' || col.columnFilter.type === 'date' - ? { 'update:modelValue': () => applyColumnFilter(col) } - : { - 'keyup.enter': () => applyColumnFilter(col), - }; -}; - -const fetchParams = ($params = {}) => { - const excludedParams = ['search', 'clientFk', 'orderFk', 'refFk', 'scopeDays']; - - const hasExcludedParams = excludedParams.some((param) => { - return $params && $params[param] != undefined; - }); - const hasParams = Object.entries($params).length; - if (!hasParams || !hasExcludedParams) $params.scopeDays = 1; - - if (typeof $params.scopeDays === 'number') { - const from = Date.vnNew(); - from.setHours(0, 0, 0, 0); - - const to = new Date(from.getTime()); - to.setDate(to.getDate() + $params.scopeDays); - to.setHours(23, 59, 59, 999); - - Object.assign($params, { from, to }); - } - return { tableOrder: 'totalProblems DESC', ...$params }; -}; - const columns = computed(() => [ { label: t('salesTicketsTable.problems'), - name: 'problems', + name: 'totalProblems', align: 'left', - sortable: true, + columnFilter: false, attrs: { dense: true, @@ -110,13 +62,12 @@ const columns = computed(() => [ }, { label: t('salesTicketsTable.identifier'), - name: 'identifier', + name: 'id', field: 'id', align: 'left', - sortable: true, columnFilter: { - component: 'input', + component: 'number', name: 'id', attrs: { dense: true, @@ -125,41 +76,41 @@ const columns = computed(() => [ }, { label: t('salesTicketsTable.client'), - name: 'client', + name: 'clientFk', align: 'left', field: 'nickname', - sortable: true, columnFilter: { - component: 'input', - name: 'nickname', + component: 'select', attrs: { - dense: true, + url: 'Clients', + fields: ['id', 'name', 'nickname'], + sortBy: 'name ASC', }, }, }, { label: t('salesTicketsTable.salesPerson'), - name: 'salesPerson', + name: 'salesPersonFk', field: 'userName', align: 'left', - sortable: true, + optionFilter: 'firstName', columnFilter: { component: 'select', - name: 'salesPersonFk', attrs: { - options: workersActiveOptions.value, - 'option-value': 'id', - 'option-label': 'name', - dense: true, + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + sortBy: 'nickname ASC', + where: { role: 'salesPerson' }, + useLike: false, }, }, }, { label: t('salesTicketsTable.date'), - name: 'date', + name: 'shippedDate', style: { 'max-width': '100px' }, align: 'left', - sortable: true, + columnFilter: { component: 'date', name: 'shippedDate', @@ -170,61 +121,39 @@ const columns = computed(() => [ }, { label: t('salesTicketsTable.theoretical'), - name: 'theoretical', + name: 'theoreticalhour', field: 'zoneLanding', align: 'left', - sortable: true, - format: (val) => toTimeFormat(val), - columnFilter: { - component: 'input', - name: 'theoreticalHour', - attrs: { - dense: true, - }, - }, + format: (row) => row.theoreticalhour, + columnFilter: false, }, { label: t('salesTicketsTable.practical'), - name: 'practical', + name: 'practicalHour', field: 'practicalHour', align: 'left', - sortable: true, - columnFilter: { - component: 'input', - name: 'practicalHour', - attrs: { - dense: true, - }, - }, + format: (row) => row.practicalHour, + columnFilter: false, }, { label: t('salesTicketsTable.preparation'), - name: 'preparation', + name: 'preparationHour', field: 'shipped', align: 'left', - sortable: true, - format: (val) => toTimeFormat(val), - columnFilter: { - component: 'input', - name: 'shippedDate', - attrs: { - dense: true, - }, - }, + format: (row) => row.preparationHour, + columnFilter: false, }, - { label: t('salesTicketsTable.province'), - name: 'province', + name: 'provinceFk', field: 'province', align: 'left', - style: { 'max-width': '100px' }, - sortable: true, + format: (row) => row.province, columnFilter: { component: 'select', name: 'provinceFk', attrs: { - options: provincesOptions.value, + options: provinceOpts.value, 'option-value': 'id', 'option-label': 'name', dense: true, @@ -236,12 +165,11 @@ const columns = computed(() => [ name: 'state', align: 'left', style: { 'max-width': '100px' }, - sortable: true, columnFilter: { component: 'select', name: 'stateFk', attrs: { - options: statesOptions.value, + options: stateOpts.value, 'option-value': 'id', 'option-label': 'name', dense: true, @@ -253,10 +181,7 @@ const columns = computed(() => [ name: 'isFragile', field: 'isFragile', align: 'left', - sortable: true, - columnFilter: { - inWhere: true, - }, + columnFilter: false, attrs: { 'checked-icon': 'local_bar', 'unchecked-icon': 'local_bar', @@ -266,14 +191,14 @@ const columns = computed(() => [ }, { label: t('salesTicketsTable.zone'), - name: 'zone', + name: 'zoneFk', align: 'left', - sortable: true, + columnFilter: { component: 'select', name: 'zoneFk', attrs: { - options: zonesOptions.value, + options: zoneOpts.value, 'option-value': 'id', 'option-label': 'name', dense: true, @@ -282,13 +207,13 @@ const columns = computed(() => [ }, { label: t('salesTicketsTable.total'), - name: 'total', + name: 'totalWithVat', field: 'totalWithVat', align: 'left', style: { 'max-width': '75px' }, - sortable: true, + columnFilter: { - component: 'input', + component: 'number', name: 'totalWithVat', attrs: { dense: true, @@ -304,7 +229,7 @@ const columns = computed(() => [ title: t('salesTicketsTable.goToLines'), icon: 'vn:lines', color: 'priamry', - action: (row) => redirectToSales(row.id), + action: (row) => openTab(row.id), isPrimary: true, attrs: { flat: true, @@ -343,18 +268,13 @@ let refreshTimer = null; const autoRefreshHandler = (value) => { if (value) - refreshTimer = setInterval(() => paginateRef.value.fetch(), DEFAULT_AUTO_REFRESH); + refreshTimer = setInterval(() => tableRef.value.reload(), DEFAULT_AUTO_REFRESH); else { clearInterval(refreshTimer); refreshTimer = null; } }; -const redirectToTicketSummary = (id) => { - const url = `#/ticket/${id}/summary`; - window.open(url, '_blank'); -}; - const stateColors = { notice: 'info', success: 'positive', @@ -375,30 +295,10 @@ const formatShippedDate = (date) => { return toDateFormat(_date); }; -const redirectToSales = (id) => { - const url = `#/ticket/${id}/sale`; - window.open(url, '_blank'); -}; - -// onMounted(async () => { -// const filteredColumns = columns.value.filter((col) => col.name !== 'rowActions'); -// allColumnNames.value = filteredColumns.map((col) => col.name); -// params = fetchParams(); -// await paginateRef.value.addFilter(null, params); -// }); +const openTab = (id) => + window.open(`#/ticket/${id}/sale`, '_blank', 'noopener, noreferrer'); </script> - <template> - <FetchData - url="Workers/activeWithInheritedRole" - :filter="{ - fields: ['id', 'nickname'], - order: 'nickname ASC', - where: { role: 'salesPerson' }, - }" - auto-load - @on-fetch="(data) => (workersActiveOptions = data)" - /> <FetchData url="Provinces" :filter="{ @@ -406,7 +306,7 @@ const redirectToSales = (id) => { order: 'name ASC', }" auto-load - @on-fetch="(data) => (provincesOptions = data)" + @on-fetch="(data) => (provinceOpts = data)" /> <FetchData url="States" @@ -415,7 +315,7 @@ const redirectToSales = (id) => { order: 'name ASC', }" auto-load - @on-fetch="(data) => (statesOptions = data)" + @on-fetch="(data) => (stateOpts = data)" /> <FetchData url="Zones" @@ -424,46 +324,60 @@ const redirectToSales = (id) => { order: 'name ASC', }" auto-load - @on-fetch="(data) => (zonesOptions = data)" + @on-fetch="(data) => (zoneOpts = data)" /> + <MonitorTicketSearchbar /> + <RightMenu> + <template #right-panel> + <MonitorTicketFilter data-key="saleMonitorTickets" /> + </template> + </RightMenu> <VnTable - ref="paginateRef" - data-key="SalesMonitorTickets" + ref="tableRef" + data-key="saleMonitorTickets" url="SalesMonitors/salesFilter" - :filter="filter" - :limit="20" + search-url="saleMonitorTickets" :expr-builder="exprBuilder" - :user-params="params" :offset="50" :columns="columns" :visible-columns="visibleColumns" :right-search="false" default-mode="table" auto-load - @row-click="(_, row) => redirectToTicketSummary(row.id)" + :row-click="({ id }) => openTab(id)" + :disable-option="{ card: true }" + :user-params="{ from, to, scopeDays: 1 }" > <template #top-left> - <TableVisibleColumns - :all-columns="allColumnNames" - table-code="ticketsMonitor" - labels-traductions-path="salesTicketsTable" - @on-config-saved="visibleColumns = [...$event, 'rowActions']" - /> + <QBtn + icon="refresh" + size="md" + color="primary" + class="q-mr-sm" + dense + flat + @click="$refs.tableRef.reload()" + > + <QTooltip>{{ $t('globals.refresh') }}</QTooltip> + </QBtn> <QCheckbox v-model="autoRefresh" - :label="t('salesTicketsTable.autoRefresh')" + :label="$t('salesTicketsTable.autoRefresh')" @update:model-value="autoRefreshHandler" - /> + dense + > + <QTooltip>{{ $t('refreshInfo') }}</QTooltip> + </QCheckbox> </template> - <template #column-problems="{ row }"> - <QTd class="no-padding" style="max-width: 50px"> + <template #column-totalProblems="{ row }"> + <QTd class="no-padding" style="max-width: 60px"> <QIcon v-if="row.isTaxDataChecked === 0" name="vn:no036" color="primary" size="xs" > - <QTooltip>{{ t('salesTicketsTable.noVerifiedData') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> </QIcon> <QIcon v-if="row.hasTicketRequest" @@ -471,7 +385,7 @@ const redirectToSales = (id) => { color="primary" size="xs" > - <QTooltip>{{ t('salesTicketsTable.purchaseRequest') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> </QIcon> <QIcon v-if="row.itemShortage" @@ -479,10 +393,10 @@ const redirectToSales = (id) => { color="primary" size="xs" > - <QTooltip>{{ t('salesTicketsTable.notVisible') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip> </QIcon> <QIcon v-if="row.isFreezed" name="vn:frozen" color="primary" size="xs"> - <QTooltip>{{ t('salesTicketsTable.clientFrozen') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip> </QIcon> <QIcon v-if="row.risk" @@ -490,7 +404,9 @@ const redirectToSales = (id) => { :color="row.hasHighRisk ? 'negative' : 'primary'" size="xs" > - <QTooltip>{{ t('salesTicketsTable.risk') }}: {{ row.risk }}</QTooltip> + <QTooltip + >{{ $t('salesTicketsTable.risk') }}: {{ row.risk }}</QTooltip + > </QIcon> <QIcon v-if="row.hasComponentLack" @@ -498,7 +414,7 @@ const redirectToSales = (id) => { color="primary" size="xs" > - <QTooltip>{{ t('salesTicketsTable.componentLack') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip> </QIcon> <QIcon v-if="row.isTooLittle" @@ -506,11 +422,11 @@ const redirectToSales = (id) => { color="primary" size="xs" > - <QTooltip>{{ t('salesTicketsTable.tooLittle') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip> </QIcon> </QTd> </template> - <template #column-identifier="{ row }"> + <template #column-id="{ row }"> <QTd class="no-padding"> <span class="link" @click.stop.prevent> {{ row.id }} @@ -518,19 +434,19 @@ const redirectToSales = (id) => { </span> </QTd> </template> - <template #column-client="{ row }"> - <QTd class="no-padding" @click.stop.prevent> + <template #column-clientFk="{ row }"> + <QTd class="no-padding" @click.stop :title="row.nickname"> <span class="link">{{ row.nickname }}</span> <CustomerDescriptorProxy :id="row.clientFk" /> </QTd> </template> - <template #column-salesPerson="{ row }"> - <QTd class="no-padding" @click.stop.prevent> - <span class="link">{{ row.userName }}</span> + <template #column-salesPersonFk="{ row }"> + <QTd class="no-padding" @click.stop :title="row.userName"> + <span class="link" v-text="dashIfEmpty(row.userName)" /> <WorkerDescriptorProxy :id="row.salesPersonFk" /> </QTd> </template> - <template #column-date="{ row }"> + <template #column-shippedDate="{ row }"> <QTd class="no-padding"> <QBadge v-bind="getBadgeAttrs(row.shippedDate)" @@ -541,6 +457,11 @@ const redirectToSales = (id) => { </QBadge> </QTd> </template> + <template #column-provinceFk="{ row }"> + <QTd class="no-padding"> + <span :title="row.province" v-text="row.province" /> + </QTd> + </template> <template #column-state="{ row }"> <QTd class="no-padding" @click.stop.prevent> <div v-if="row.refFk"> @@ -561,17 +482,17 @@ const redirectToSales = (id) => { <template #column-isFragile="{ row }"> <QTd class="no-padding"> <QIcon v-if="row.isFragile" name="local_bar" color="primary" size="sm"> - <QTooltip>{{ t('salesTicketsTable.isFragile') }}</QTooltip> + <QTooltip>{{ $t('salesTicketsTable.isFragile') }}</QTooltip> </QIcon> </QTd> </template> - <template #column-zone="{ row }"> - <QTd class="no-padding" @click.stop.prevent> + <template #column-zoneFk="{ row }"> + <QTd class="no-padding" @click.stop.prevent :title="row.zoneName"> <span class="link">{{ row.zoneName }}</span> <ZoneDescriptorProxy :id="row.zoneFk" /> </QTd> </template> - <template #column-total="{ row }"> + <template #column-totalWithVat="{ row }"> <QTd class="no-padding"> <QBadge :color="totalPriceColor(row) || 'transparent'" diff --git a/src/pages/Monitor/locale/en.yml b/src/pages/Monitor/locale/en.yml index f58db7854..4cdd245aa 100644 --- a/src/pages/Monitor/locale/en.yml +++ b/src/pages/Monitor/locale/en.yml @@ -11,11 +11,14 @@ salesClientsTable: client: Client salesOrdersTable: delete: Delete - date: Date + dateSend: Send date + dateMake: Make date client: Client salesPerson: Salesperson deleteConfirmTitle: Delete selected elements deleteConfirmMessage: All the selected elements will be deleted. Are you sure you want to continue? + agency: Agency + import: Import salesTicketsTable: autoRefresh: Auto-refresh problems: Problems @@ -43,3 +46,4 @@ salesTicketsTable: searchBar: label: Search tickets info: Search tickets by id or alias +refreshInfo: Toggle auto-refresh every 2 minutes diff --git a/src/pages/Monitor/locale/es.yml b/src/pages/Monitor/locale/es.yml index 918b51813..8087bb444 100644 --- a/src/pages/Monitor/locale/es.yml +++ b/src/pages/Monitor/locale/es.yml @@ -11,11 +11,14 @@ salesClientsTable: client: Cliente salesOrdersTable: delete: Eliminar - date: Fecha + dateSend: Fecha de envío + dateMake: Fecha de realización client: Cliente salesPerson: Comercial deleteConfirmTitle: Eliminar los elementos seleccionados deleteConfirmMessage: Todos los elementos seleccionados serán eliminados. ¿Seguro que quieres continuar? + agency: Agencia + import: Importe salesTicketsTable: autoRefresh: Auto-refresco problems: Problemas @@ -43,3 +46,4 @@ salesTicketsTable: searchBar: label: Buscar tickets info: Buscar tickets por identificador o alias +refreshInfo: Conmuta el refresco automático cada 2 minutos diff --git a/src/pages/Order/Card/OrderForm.vue b/src/pages/Order/Card/OrderBasicData.vue similarity index 85% rename from src/pages/Order/Card/OrderForm.vue rename to src/pages/Order/Card/OrderBasicData.vue index f447950d2..4bc9e2e43 100644 --- a/src/pages/Order/Card/OrderForm.vue +++ b/src/pages/Order/Card/OrderBasicData.vue @@ -1,5 +1,5 @@ <script setup> -import { useRoute, useRouter } from 'vue-router'; +import { useRoute } from 'vue-router'; import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; @@ -7,6 +7,7 @@ import { useState } from 'composables/useState'; import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelect from 'components/common/VnSelect.vue'; +import VnInput from 'components/common/VnInput.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; @@ -15,7 +16,6 @@ const route = useRoute(); const state = useState(); const ORDER_MODEL = 'order'; -const router = useRouter(); const isNew = Boolean(!route.params.id); const clientList = ref([]); const agencyList = ref([]); @@ -64,13 +64,6 @@ const fetchOrderDetails = (order) => { fetchAgencyList(order?.landed, order?.addressFk); }; -const orderMapper = (order) => { - return { - addressId: order.addressFk, - agencyModeId: order.agencyModeFk, - landed: new Date(order.landed).toISOString(), - }; -}; const orderFilter = { include: [ { relation: 'agencyMode', scope: { fields: ['name'] } }, @@ -106,10 +99,6 @@ const onClientChange = async (clientId) => { console.error('Error al cambiar el cliente:', error); } }; - -async function onDataSaved({ id }) { - await router.push({ path: `/order/${id}/catalog` }); -} </script> <template> @@ -117,9 +106,8 @@ async function onDataSaved({ id }) { <div class="q-pa-md"> <FormModel :url="`Orders/${route.params.id}`" - @on-data-saved="onDataSaved" + :url-update="`Orders/${route.params.id}/updateBasicData`" :model="ORDER_MODEL" - :mapper="orderMapper" :filter="orderFilter" @on-fetch="fetchOrderDetails" auto-load @@ -180,8 +168,6 @@ async function onDataSaved({ id }) { () => fetchAgencyList(data.landed, data.addressFk) " /> - </VnRow> - <VnRow> <VnSelect :label="t('order.form.agencyModeFk')" v-model="data.agencyModeFk" @@ -189,9 +175,29 @@ async function onDataSaved({ id }) { option-value="agencyModeFk" option-label="agencyMode" hide-selected - :disable="!agencyList?.length" - > - </VnSelect> + :disable="!agencyList?.length && data.isConfirmed === 1" + clearable + emit-value + map-options + :model-value=" + !data.isConfirmed && + agencyList?.length && + agencyList.some( + (agency) => agency.agencyModeFk === data.agency_id + ) + ? data.agencyModeFk + : null + " + /> + </VnRow> + <VnRow> + <VnInput + :label="t('globals.notes')" + type="textarea" + v-model="data.note" + fill-input + autogrow + /> </VnRow> </template> </FormModel> diff --git a/src/pages/Order/Card/OrderCard.vue b/src/pages/Order/Card/OrderCard.vue index 5b6896656..67c0f1de5 100644 --- a/src/pages/Order/Card/OrderCard.vue +++ b/src/pages/Order/Card/OrderCard.vue @@ -1,16 +1,35 @@ <script setup> +import { computed } from 'vue'; +import { useRoute } from 'vue-router'; import VnCard from 'components/common/VnCard.vue'; import OrderDescriptor from 'pages/Order/Card/OrderDescriptor.vue'; import OrderFilter from './OrderFilter.vue'; import OrderSearchbar from './OrderSearchbar.vue'; +import OrderCatalogFilter from './OrderCatalogFilter.vue'; +const config = { + OrderCatalog: OrderCatalogFilter, +}; +const route = useRoute(); + +const routeName = computed(() => route.name); +const customRouteRedirectName = computed(() => { + const route = config[routeName.value]; + if (route) return null; + return 'OrderList'; +}); +const customFilterPanel = computed(() => { + const filterPanel = config[routeName.value] ?? OrderFilter; + return filterPanel; +}); </script> + <template> <VnCard data-key="Order" base-url="Orders" :descriptor="OrderDescriptor" - :filter-panel="OrderFilter" - search-data-key="OrderList" + :filter-panel="customFilterPanel" + :search-data-key="customRouteRedirectName" > <template #searchbar> <OrderSearchbar /> diff --git a/src/pages/Order/OrderCatalog.vue b/src/pages/Order/Card/OrderCatalog.vue similarity index 77% rename from src/pages/Order/OrderCatalog.vue rename to src/pages/Order/Card/OrderCatalog.vue index 2cf6e1c29..68bf9511f 100644 --- a/src/pages/Order/OrderCatalog.vue +++ b/src/pages/Order/Card/OrderCatalog.vue @@ -1,17 +1,24 @@ <script setup> import { useStateStore } from 'stores/useStateStore'; -import { useRoute } from 'vue-router'; +import { useRoute, useRouter } from 'vue-router'; import { onMounted, onUnmounted, ref } from 'vue'; +import axios from 'axios'; import { useI18n } from 'vue-i18n'; import VnPaginate from 'components/ui/VnPaginate.vue'; import CatalogItem from 'components/ui/CatalogItem.vue'; import OrderCatalogFilter from 'pages/Order/Card/OrderCatalogFilter.vue'; +import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; const route = useRoute(); +const router = useRouter(); const stateStore = useStateStore(); const { t } = useI18n(); +const tags = ref([]); -onMounted(() => (stateStore.rightDrawer = true)); +onMounted(() => { + stateStore.rightDrawer = true; + checkOrderConfirmation(); +}); onUnmounted(() => (stateStore.rightDrawer = false)); const catalogParams = { @@ -19,7 +26,12 @@ const catalogParams = { orderBy: JSON.stringify({ field: 'relevancy DESC, name', way: 'ASC', isTag: false }), }; -const tags = ref([]); +async function checkOrderConfirmation() { + const response = await axios.get(`Orders/${route.params.id}`); + if (response.data.isConfirmed === 1) { + router.push(`/order/${route.params.id}/line`); + } +} function extractTags(items) { const resultTags = []; @@ -52,6 +64,15 @@ function extractValueTags(items) { </script> <template> + <VnSearchbar + data-key="OrderCatalogList" + :user-params="catalogParams" + :static-params="['orderFk', 'orderBy']" + :redirect="false" + url="Orders/CatalogFilter" + :label="t('Search items')" + :info="t('You can search items by name or id')" + /> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QScrollArea class="fit text-grey-8"> <OrderCatalogFilter @@ -68,7 +89,6 @@ function extractValueTags(items) { url="Orders/CatalogFilter" :limit="50" :user-params="catalogParams" - auto-load @on-fetch="extractTags" :update-router="false" > @@ -106,3 +126,8 @@ function extractValueTags(items) { text-align: center; } </style> + +<i18n> +es: + You can search items by name or id: Puedes buscar items por nombre o id +</i18n> diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index c354ec94b..938cc4fe2 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -7,8 +7,8 @@ import FetchData from 'components/FetchData.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnSelect from 'components/common/VnSelect.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; -import { useValidator } from 'src/composables/useValidator'; import VnInput from 'src/components/common/VnInput.vue'; +import getParamWhere from 'src/filters/getParamWhere'; const { t } = useI18n(); @@ -27,19 +27,26 @@ const props = defineProps({ required: true, }, }); - const categoryList = ref(null); -const selectedCategoryFk = ref(null); -const typeList = ref(null); +const selectedCategoryFk = ref(getParamWhere(route, 'categoryFk')); +const typeList = ref([]); const selectedTypeFk = ref(null); -const validationsStore = useValidator(); -const selectedOrder = ref(null); -const selectedOrderField = ref(null); -const moreFields = ref([]); -const moreFieldsOrder = ref([]); const selectedTag = ref(null); const tagValues = ref([{}]); const tagOptions = ref([]); +const vnFilterPanelRef = ref(); +const orderByList = ref([ + { id: 'relevancy DESC, name', name: t('params.relevancy'), priority: 999 }, + { id: 'showOrder, price', name: t('params.colorAndPrice'), priority: 999 }, + { id: 'name', name: t('params.name'), priority: 999 }, + { id: 'price', name: t('params.price'), priority: 999 }, +]); +const orderWayList = ref([ + { id: 'ASC', name: t('params.ASC') }, + { id: 'DESC', name: t('params.DESC') }, +]); +const orderBySelected = ref('relevancy DESC, name'); +const orderWaySelected = ref('ASC'); const createValue = (val, done) => { if (val.length > 2) { @@ -72,7 +79,7 @@ const selectCategory = (params, category, search) => { search(); }; -const loadTypes = async (categoryFk) => { +const loadTypes = async (categoryFk = selectedCategoryFk.value) => { const { data } = await axios.get(`Orders/${route.params.id}/getItemTypeAvailable`, { params: { itemCategoryId: categoryFk }, }); @@ -84,7 +91,14 @@ const selectedCategory = computed(() => (category) => category?.id === selectedCategoryFk.value ) ); - +function filterFn(val, update) { + update(() => { + const needle = val.toLowerCase(); + tagOptions.value = props.tagValue.filter( + (v) => v.toLowerCase().indexOf(needle) > -1 + ); + }); +} const selectedType = computed(() => { return (typeList.value || []).find((type) => type?.id === selectedTypeFk.value); }); @@ -95,7 +109,8 @@ function exprBuilder(param, value) { case 'typeFk': return { [param]: value }; case 'search': - return { 'i.name': { like: `%${value}%` } }; + if (/^\d+$/.test(value)) return { 'i.id': value }; + else return { 'i.name': { like: `%${value}%` } }; } } @@ -132,36 +147,6 @@ const removeTagChip = (selection, params, search) => { search(); }; -const onOrderChange = (value, params) => { - const tagObj = JSON.parse(params.orderBy); - tagObj.way = value.name; - params.orderBy = JSON.stringify(tagObj); -}; - -const onOrderFieldChange = (value, params) => { - const tagObj = JSON.parse(params.orderBy); - switch (value) { - case 'Relevancy': - tagObj.name = value + ' DESC, name'; - params.orderBy = JSON.stringify(tagObj); - break; - case 'ColorAndPrice': - tagObj.name = 'showOrder, price'; - params.orderBy = JSON.stringify(tagObj); - break; - case 'Name': - tagObj.name = 'name'; - params.orderBy = JSON.stringify(tagObj); - break; - case 'Price': - tagObj.name = 'price'; - params.orderBy = JSON.stringify(tagObj); - break; - } -}; - -const _moreFields = ['ASC', 'DESC']; -const _moreFieldsTypes = ['Relevancy', 'ColorAndPrice', 'Name', 'Price']; const setCategoryList = (data) => { categoryList.value = (data || []) .filter((category) => category.display) @@ -169,8 +154,8 @@ const setCategoryList = (data) => { ...category, icon: `vn:${(category.icon || '').split('-')[1]}`, })); - moreFields.value = useLang(_moreFields); - moreFieldsOrder.value = useLang(_moreFieldsTypes); + + selectedCategoryFk.value && loadTypes(); }; const getCategoryClass = (category, params) => { @@ -179,27 +164,22 @@ const getCategoryClass = (category, params) => { } }; -const useLang = (values) => { - const { models } = validationsStore; - const properties = models.Item?.properties || {}; - return values.map((name) => { - let prop = properties[name]; - const label = t(`params.${name}`); - return { - name, - label, - type: prop ? prop.type : null, - }; - }); -}; +function addOrder(value, field, params) { + let { orderBy } = params; + orderBy = JSON.parse(orderBy); + orderBy[field] = value; + params.orderBy = JSON.stringify(orderBy); + vnFilterPanelRef.value.search(); +} </script> <template> <FetchData url="ItemCategories" limit="30" auto-load @on-fetch="setCategoryList" /> <VnFilterPanel + ref="vnFilterPanelRef" :data-key="props.dataKey" :hidden-tags="['orderFk', 'orderBy']" - :unremovable-params="['orderFk', 'orderBy']" + :un-removable-params="['orderFk', 'orderBy']" :expr-builder="exprBuilder" :custom-tags="['tagGroups']" @remove="clearFilter" @@ -289,33 +269,29 @@ const useLang = (values) => { </QItemSection> </QItem> <QSeparator /> - <QItem class="q-my-md"> - <QItemSection> - <VnSelect - :label="t('Order')" - v-model="selectedOrder" - :options="moreFields" - option-label="label" - option-value="way" - dense - outlined - rounded - @update:model-value="(value) => onOrderChange(value, params)" - /> - </QItemSection> - </QItem> <QItem class="q-mb-md"> <QItemSection> <VnSelect :label="t('Order by')" - v-model="selectedOrderField" - :options="moreFieldsOrder" - option-label="label" - option-value="name" + v-model="orderBySelected" + :options="orderByList" dense outlined rounded - @update:model-value="(value) => onOrderFieldChange(value, params)" + @update:model-value="(value) => addOrder(value, 'field', params)" + /> + </QItemSection> + </QItem> + <QItem class="q-my-md"> + <QItemSection> + <VnSelect + :label="t('Order')" + v-model="orderWaySelected" + :options="orderWayList" + dense + outlined + rounded + @update:model-value="(value) => addOrder(value, 'way', params)" /> </QItemSection> </QItem> @@ -352,7 +328,7 @@ const useLang = (values) => { v-if="!selectedTag" :label="t('params.value')" v-model="value.value" - :options="tagValue || []" + :options="tagOptions || []" option-value="value" option-label="value" dense @@ -362,6 +338,8 @@ const useLang = (values) => { use-input class="filter-input" @new-value="createValue" + @filter="filterFn" + @update:model-value="applyTagFilter(params, searchFn)" /> <VnSelect v-else-if="selectedTag === 1" @@ -377,6 +355,7 @@ const useLang = (values) => { use-input class="filter-input" @new-value="createValue" + @update:model-value="applyTagFilter(params, searchFn)" /> <VnInput v-else @@ -386,6 +365,7 @@ const useLang = (values) => { outlined rounded class="filter-input" + @keyup.enter="applyTagFilter(params, searchFn)" /> <QIcon name="delete" @@ -400,21 +380,6 @@ const useLang = (values) => { @click="tagValues.push({})" /> </QItem> - <QItem> - <QItemSection class="q-py-sm"> - <QBtn - :label="t('Search')" - class="full-width" - color="primary" - dense - icon="search" - rounded - type="button" - unelevated - @click.stop="applyTagFilter(params, searchFn)" - /> - </QItemSection> - </QItem> <QSeparator /> </template> </VnFilterPanel> @@ -477,10 +442,10 @@ en: order: Order ASC: Ascendant DESC: Descendant - Relevancy: Relevancy - ColorAndPrice: Color and price - Name: Name - Price: Price + relevancy: Relevancy + colorAndPrice: Color and price + name: Name + price: Price es: params: type: Tipo @@ -490,10 +455,10 @@ es: order: Orden ASC: Ascendiente DESC: Descendiente - Relevancy: Relevancia - ColorAndPrice: Color y precio - Name: Nombre - Price: Precio + relevancy: Relevancia + colorAndPrice: Color y precio + name: Nombre + price: Precio Order: Orden Order by: Ordenar por Plant: Planta diff --git a/src/pages/Order/Card/OrderCatalogItemDialog.vue b/src/pages/Order/Card/OrderCatalogItemDialog.vue index 047816127..46a50c021 100644 --- a/src/pages/Order/Card/OrderCatalogItemDialog.vue +++ b/src/pages/Order/Card/OrderCatalogItemDialog.vue @@ -5,10 +5,12 @@ import { useI18n } from 'vue-i18n'; import axios from 'axios'; import { useRoute } from 'vue-router'; import useNotify from 'composables/useNotify'; +import { useArrayData } from 'composables/useArrayData'; const { t } = useI18n(); -const route = useRoute(); const { notify } = useNotify(); +const emit = defineEmits(['added']); +const route = useRoute(); const props = defineProps({ prices: { type: Array, @@ -16,9 +18,8 @@ const props = defineProps({ }, }); -const emit = defineEmits(['added']); - const fields = ref((props.prices || []).map((item) => ({ ...item, quantity: 0 }))); +const descriptorData = useArrayData('orderData'); const addToOrder = async () => { const items = (fields.value || []).filter((item) => Number(item.quantity) > 0); @@ -28,19 +29,20 @@ const addToOrder = async () => { }); notify(t('globals.dataSaved'), 'positive'); emit('added'); + descriptorData.fetch({}); }; </script> <template> - <div class="container order-catalog-item q-pb-md"> + <div class="container order-catalog-item q-pa-md"> <QForm @submit="addToOrder"> <QMarkupTable class="shadow-0"> <tbody> <tr v-for="item in fields" :key="item.warehouse"> - <td class="text-bold q-py-lg"> + <td class="text-bold q-pr-md td" style="width: 35%"> {{ item.warehouse }} </td> - <td class="text-right"> + <td class="text-right" style="width: 35%"> <span class="link" @click=" @@ -75,8 +77,7 @@ const addToOrder = async () => { </template> <style lang="scss" scoped> -.container { - max-width: 448px; - width: 100%; +.td { + width: 200px; } </style> diff --git a/src/pages/Order/Card/OrderCreateDialog.vue b/src/pages/Order/Card/OrderCreateDialog.vue new file mode 100644 index 000000000..9d5c9281e --- /dev/null +++ b/src/pages/Order/Card/OrderCreateDialog.vue @@ -0,0 +1,220 @@ +<script setup> +import { useRoute, useRouter } from 'vue-router'; +import { onMounted, 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'; +import VnInputDate from 'components/common/VnInputDate.vue'; +import { useDialogPluginComponent } from 'quasar'; +import { reactive } from 'vue'; +import FetchData from 'components/FetchData.vue'; + +const { t } = useI18n(); +const route = useRoute(); +const state = useState(); +const ORDER_MODEL = 'order'; + +const router = useRouter(); +const clientList = ref([]); +const agencyList = ref([]); +const addressList = ref([]); +defineEmits(['confirm', ...useDialogPluginComponent.emits]); + +const fetchAddressList = async (addressId) => { + try { + 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; + } + } catch (err) { + console.error(`Error fetching addresses`, err); + return err.response; + } +}; + +const fetchAgencyList = async (landed, addressFk) => { + if (!landed || !addressFk) { + return; + } + try { + const { data } = await axios.get('Agencies/landsThatDay', { + params: { + addressFk, + landed: new Date(landed).toISOString(), + }, + }); + agencyList.value = data; + } catch (err) { + console.error(`Error fetching agencies`, err); + return err.response; + } +}; + +// const fetchOrderDetails = (order) => { +// fetchAddressList(order?.addressFk); +// fetchAgencyList(order?.landed, order?.addressFk); +// }; +const $props = defineProps({ + clientFk: { + type: Number, + default: null, + }, +}); +const initialFormState = reactive({ + active: true, + addressId: null, + clientFk: $props.clientFk, +}); +// const orderMapper = (order) => { +// return { +// addressId: order.addressFk, +// agencyModeId: order.agencyModeFk, +// landed: new Date(order.landed).toISOString(), +// }; +// }; +// const orderFilter = { +// include: [ +// { relation: 'agencyMode', scope: { fields: ['name'] } }, +// { +// relation: 'address', +// scope: { fields: ['nickname'] }, +// }, +// { relation: 'rows', scope: { fields: ['id'] } }, +// { +// relation: 'client', +// scope: { +// fields: [ +// 'salesPersonFk', +// 'name', +// 'isActive', +// 'isFreezed', +// 'isTaxDataChecked', +// ], +// include: { +// relation: 'salesPersonUser', +// scope: { fields: ['id', 'name'] }, +// }, +// }, +// }, +// ], +// }; + +const onClientChange = async (clientId = $props.clientFk) => { + try { + const { data } = await axios.get(`Clients/${clientId}`); + await fetchAddressList(data.defaultAddressFk); + } catch (error) { + console.error('Error al cambiar el cliente:', error); + } +}; + +async function onDataSaved(_, id) { + await router.push({ path: `/order/${id}/catalog` }); +} +onMounted(async () => { + await onClientChange(); +}); +</script> + +<template> + <FetchData + url="addresses" + @on-fetch="(data) => (clientOptions = data)" + :filter="{ fields: ['id', 'name', 'defaultAddressFk'], order: 'id' }" + auto-load + /> + <FormModelPopup + url-create="Orders/new" + :title="t('Create Order')" + @on-data-saved="onDataSaved" + :model="ORDER_MODEL" + :filter="orderFilter" + :form-initial-data="initialFormState" + > + <template #form-inputs="{ data }"> + <VnRow class="row q-gutter-md q-mb-md"> + <VnSelect + url="Clients" + :label="t('order.form.clientFk')" + v-model="data.clientFk" + option-value="id" + option-label="name" + :filter="{ + fields: ['id', 'name', 'defaultAddressFk'], + }" + hide-selected + @update:model-value="onClientChange" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ `${scope.opt.id}: ${scope.opt.name}` }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnSelect + :label="t('order.form.addressFk')" + v-model="data.addressId" + :options="addressList" + option-value="id" + option-label="street" + hide-selected + :disable="!addressList?.length" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ + `${scope.opt.nickname}: ${scope.opt.street},${scope.opt.city}` + }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </VnRow> + <VnRow class="row q-gutter-md q-mb-md"> + <VnInputDate + placeholder="dd-mm-aaa" + :label="t('order.form.landed')" + v-model="data.landed" + @update:model-value=" + () => fetchAgencyList(data.landed, data.addressId) + " + /> + </VnRow> + <VnRow class="row q-gutter-md q-mb-md"> + <VnSelect + :label="t('order.form.agencyModeFk')" + v-model="data.agencyModeId" + :options="agencyList" + option-value="agencyModeFk" + option-label="agencyMode" + hide-selected + :disable="!agencyList?.length" + > + </VnSelect> + </VnRow> + </template> + </FormModelPopup> +</template> + +<i18n> + es: + No default address found for the client: No hay ninguna dirección asociada a este cliente. +</i18n> diff --git a/src/pages/Order/Card/OrderDescriptor.vue b/src/pages/Order/Card/OrderDescriptor.vue index 4d84a32fc..a035971b0 100644 --- a/src/pages/Order/Card/OrderDescriptor.vue +++ b/src/pages/Order/Card/OrderDescriptor.vue @@ -4,13 +4,13 @@ import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { toCurrency, toDate } from 'src/filters'; import { useState } from 'src/composables/useState'; - import useCardDescription from 'src/composables/useCardDescription'; -import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; + import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import OrderDescriptorMenu from 'pages/Order/Card/OrderDescriptorMenu.vue'; import FetchData from 'components/FetchData.vue'; +import OrderDescriptorMenu from 'pages/Order/Card/OrderDescriptorMenu.vue'; +import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; const DEFAULT_ITEMS = 0; @@ -25,6 +25,8 @@ const $props = defineProps({ const route = useRoute(); const state = useState(); const { t } = useI18n(); +const data = ref(useCardDescription()); +const getTotalRef = ref(); const entityId = computed(() => { return $props.id || route.params.id; @@ -57,11 +59,11 @@ const filter = { ], }; -const data = ref(useCardDescription()); const setData = (entity) => { if (!entity) return; + getTotalRef.value && getTotalRef.value.fetch(); data.value = useCardDescription(entity?.client?.name, entity?.id); - state.set('OrderDescriptor', entity); + state.set('orderData', entity); }; const getConfirmationValue = (isConfirmed) => { @@ -69,13 +71,17 @@ const getConfirmationValue = (isConfirmed) => { }; const total = ref(null); + +function ticketFilter(order) { + return JSON.stringify({ id: order.id }); +} </script> <template> <FetchData + ref="getTotalRef" :url="`Orders/${entityId}/getTotal`" @on-fetch="(response) => (total = response)" - auto-load /> <CardDescriptor ref="descriptor" @@ -120,7 +126,7 @@ const total = ref(null); color="primary" :to="{ name: 'TicketList', - query: { params: JSON.stringify({ orderFk: entity.id }) }, + query: { table: ticketFilter(entity) }, }" > <QTooltip>{{ t('order.summary.orderTicketList') }}</QTooltip> diff --git a/src/pages/Order/Card/OrderDescriptorMenu.vue b/src/pages/Order/Card/OrderDescriptorMenu.vue index 2e36aa3c5..2695da4e5 100644 --- a/src/pages/Order/Card/OrderDescriptorMenu.vue +++ b/src/pages/Order/Card/OrderDescriptorMenu.vue @@ -23,7 +23,7 @@ function confirmRemove() { .dialog({ component: VnConfirm, componentProps: { - title: t('confirmDeletion'), + title: t('globals.confirmDeletion'), message: t('confirmDeletionMessage'), promise: remove, }, @@ -52,12 +52,10 @@ async function remove() { <i18n> en: deleteOrder: Delete order - confirmDeletion: Confirm deletion confirmDeletionMessage: Are you sure you want to delete this order? es: deleteOrder: Eliminar pedido - confirmDeletion: Confirmar eliminación confirmDeletionMessage: Seguro que quieres eliminar este pedido? </i18n> diff --git a/src/pages/Order/Card/OrderFilter.vue b/src/pages/Order/Card/OrderFilter.vue index 347affb04..be47eed54 100644 --- a/src/pages/Order/Card/OrderFilter.vue +++ b/src/pages/Order/Card/OrderFilter.vue @@ -21,15 +21,13 @@ const salesPersonFilter = { fields: ['id', 'nickname'], }; const salesPersonList = ref(null); -const sourceFilter = { fields: ['value'] }; -const sourceList = ref(null); +const sourceList = ref([]); </script> <template> <FetchData url="AgencyModes/isActive" :filter="agencyFilter" - limit="30" sort-by="name ASC" auto-load @on-fetch="(data) => (agencyList = data)" @@ -37,7 +35,6 @@ const sourceList = ref(null); <FetchData url="Workers/search" :filter="salesPersonFilter" - limit="30" sort-by="nickname ASC" @on-fetch="(data) => (salesPersonList = data)" :params="{ departmentCodes: ['VT'] }" @@ -45,8 +42,7 @@ const sourceList = ref(null); /> <FetchData url="Orders/getSourceValues" - :filter="sourceFilter" - limit="30" + :filter="{ fields: ['value'] }" sort-by="value ASC" @on-fetch="(data) => (sourceList = data)" auto-load @@ -59,148 +55,88 @@ const sourceList = ref(null); </div> </template> <template #body="{ params }"> - <QItem> - <QItemSection> - <VnInput - is-outlined - :label="t('customerId')" - v-model="params.clientFk" - lazy-rules - > - <template #prepend> - <QIcon name="badge" size="sm"></QIcon> - </template> - </VnInput> - </QItemSection> - </QItem> - <QItem> - <QItemSection v-if="agencyList"> - <VnSelect - :label="t('agency')" - v-model="params.agencyModeFk" - :options="agencyList" - option-value="id" - option-label="name" - dense - outlined - rounded - emit-value - map-options - use-input - :input-debounce="0" - /> - </QItemSection> - <QItemSection v-else> - <QSkeleton type="QInput" class="full-width" /> - </QItemSection> - </QItem> - <QItem> - <QItemSection v-if="salesPersonList"> - <VnSelect - :label="t('salesPerson')" - v-model="params.workerFk" - :options="salesPersonList" - option-value="id" - option-label="name" - dense - outlined - rounded - emit-value - map-options - use-input - :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> - <QItemSection v-else> - <QSkeleton type="QInput" class="full-width" /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInputDate - v-model="params.from" - :label="t('fromLanded')" - dense - outlined - rounded - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInputDate - v-model="params.to" - :label="t('toLanded')" - dense - outlined - rounded - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnInput - :label="t('orderId')" - v-model="params.orderFk" - lazy-rules - is-outlined - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection v-if="sourceList"> - <VnSelect - :label="t('application')" - v-model="params.sourceApp" - :options="sourceList" - option-label="value" - emit-value - map-options - use-input - dense - outlined - rounded - :input-debounce="0" - /> - </QItemSection> - <QItemSection v-else> - <QSkeleton type="QInput" class="full-width" /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <QCheckbox - v-model="params.myTeam" - :label="t('myTeam')" - toggle-indeterminate - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <QCheckbox - v-model="params.isConfirmed" - :label="t('isConfirmed')" - toggle-indeterminate - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <QCheckbox v-model="params.showEmpty" :label="t('showEmpty')" /> - </QItemSection> - </QItem> + <div class="q-px-md q-gutter-y-sm"> + <VnInput + :label="t('customerId')" + v-model="params.clientFk" + lazy-rules + dense + outlined + rounded + /> + <VnSelect + :label="t('agency')" + v-model="params.agencyModeFk" + :options="agencyList" + :input-debounce="0" + dense + outlined + rounded + /> + <VnSelect + :label="t('salesPerson')" + v-model="params.workerFk" + url="Workers/search" + :filter="{ departmentCodes: ['VT'] }" + sort-by="nickname ASC" + option-label="nickname" + 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')" + dense + outlined + rounded + /> + <VnInputDate + v-model="params.to" + :label="t('toLanded')" + dense + outlined + rounded + /> + <VnInput + :label="t('orderId')" + v-model="params.orderFk" + lazy-rules + is-outlined + /> + <VnSelect + :label="t('application')" + v-model="params.sourceApp" + :options="sourceList" + option-label="value" + dense + outlined + rounded + :input-debounce="0" + /> + <QCheckbox + v-model="params.myTeam" + :label="t('myTeam')" + toggle-indeterminate + /> + <QCheckbox + v-model="params.isConfirmed" + :label="t('isConfirmed')" + toggle-indeterminate + /> + <QCheckbox v-model="params.showEmpty" :label="t('showEmpty')" /> + </div> </template> </VnFilterPanel> </template> diff --git a/src/pages/Order/OrderLines.vue b/src/pages/Order/Card/OrderLines.vue similarity index 69% rename from src/pages/Order/OrderLines.vue rename to src/pages/Order/Card/OrderLines.vue index 9814eaf34..17a157797 100644 --- a/src/pages/Order/OrderLines.vue +++ b/src/pages/Order/Card/OrderLines.vue @@ -1,23 +1,27 @@ <script setup> -import { useRoute } from 'vue-router'; +import { useRoute, useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { ref, computed } from 'vue'; +import { ref, computed, watch } from 'vue'; import { useQuasar } from 'quasar'; +import axios from 'axios'; +import { useStateStore } from 'stores/useStateStore'; +import { useArrayData } from 'composables/useArrayData'; +import { toCurrency, toDate } from 'src/filters'; import VnConfirm from 'components/ui/VnConfirm.vue'; -import { toCurrency, toDate } from 'src/filters'; -import axios from 'axios'; import VnTable from 'src/components/VnTable/VnTable.vue'; import FetchData from 'src/components/FetchData.vue'; import VnImg from 'src/components/ui/VnImg.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import FetchedTags from 'src/components/ui/FetchedTags.vue'; -import { useStateStore } from 'stores/useStateStore'; +import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; +const router = useRouter(); const stateStore = useStateStore(); const route = useRoute(); const { t } = useI18n(); const quasar = useQuasar(); +const descriptorData = useArrayData('orderData'); const componentKey = ref(0); const tableLinesRef = ref(); const order = ref(); @@ -25,6 +29,8 @@ const orderSummary = ref({ total: null, vat: null, }); +const getTotalRef = ref(); +const getVATRef = ref(); const lineFilter = ref({ include: [ @@ -59,6 +65,13 @@ const lineFilter = ref({ fields: ['id', 'name'], }, }, + { + relation: 'order', + scope: { + fields: ['id', 'isConfirmed'], + where: { id: route.params.id }, + }, + }, ], where: { orderFk: route.params.id }, }); @@ -104,6 +117,7 @@ const columns = computed(() => [ component: null, }, format: (row) => row?.item?.name, + columnClass: 'expand', }, { align: 'left', @@ -147,7 +161,6 @@ const columns = computed(() => [ align: 'left', name: 'amount', label: t('lines.amount'), - format: (row) => toCurrency(row.amount), }, { align: 'right', @@ -155,8 +168,9 @@ const columns = computed(() => [ name: 'tableActions', actions: [ { - title: t('delete'), + title: t('Delete'), icon: 'delete', + show: (row) => !row.order.isConfirmed, action: (row) => confirmRemove(row), isPrimary: true, }, @@ -185,6 +199,9 @@ async function remove(item) { type: 'positive', }); tableLinesRef.value.reload(); + descriptorData.fetch({}); + getTotalRef.value.fetch(); + getVATRef.value.fetch(); } async function confirmOrder() { @@ -193,7 +210,22 @@ async function confirmOrder() { message: t('globals.confirm'), type: 'positive', }); + router.push({ + name: 'TicketList', + query: { + table: JSON.stringify({ clientFk: descriptorData.store.data.clientFk }), + }, + }); } + +watch( + () => router.currentRoute.value.params.id, + () => { + lineFilter.value.where.orderFk = router.currentRoute.value.params.id; + + tableLinesRef.value.reload(); + } +); </script> <template> @@ -204,78 +236,85 @@ async function confirmOrder() { auto-load /> <FetchData + ref="getTotalRef" :key="componentKey" :url="`Orders/${route.params.id}/getTotal`" @on-fetch="(data) => (orderSummary.total = data)" auto-load /> <FetchData + ref="getVATRef" :key="componentKey" :url="`Orders/${route.params.id}/getVAT`" @on-fetch="(data) => (orderSummary.vat = data)" auto-load /> <QDrawer side="right" :width="270" v-model="stateStore.rightDrawer"> - <QCard class="order-lines-summary q-pa-lg"> + <QCard + class="order-lines-summary q-pa-lg" + v-if="orderSummary.vat && orderSummary.total" + > <p class="header text-right block"> {{ t('summary') }} </p> <VnLv - v-if="orderSummary.vat && orderSummary.total" :label="t('subtotal') + ': '" :value="toCurrency(orderSummary.total - orderSummary.vat)" /> - <VnLv - v-if="orderSummary.vat" - :label="t('VAT') + ': '" - :value="toCurrency(orderSummary?.vat)" - /> - <VnLv - v-if="orderSummary.total" - :label="t('total') + ': '" - :value="toCurrency(orderSummary?.total)" - /> + <VnLv :label="t('VAT') + ': '" :value="toCurrency(orderSummary?.vat)" /> + <VnLv :label="t('total') + ': '" :value="toCurrency(orderSummary?.total)" /> </QCard> </QDrawer> - <QPage :key="componentKey" class="column items-center"> - <div class="order-list full-width"> - <div v-if="!orderSummary.total" class="no-result"> - {{ t('globals.noResults') }} - </div> - <VnTable - ref="tableLinesRef" - data-key="OrderLines" - url="OrderRows" - :columns="columns" - :right-search="false" - :use-model="true" - auto-load - :user-filter="lineFilter" - > - <template #column-image="{ row }"> - <div class="image-wrapper"> - <VnImg :id="parseInt(row?.item?.image)" class="rounded" /> - </div> - </template> - <template #column-itemFk="{ row }"> - <div class="row column full-width justify-between items-start"> - {{ row?.item?.name }} - <div v-if="row?.item?.subName" class="subName"> - {{ row?.item?.subName.toUpperCase() }} - </div> - </div> - <FetchedTags :item="row?.item" :max-length="6" /> - </template> - </VnTable> - </div> - <QPageSticky :offset="[20, 20]" v-if="!order?.isConfirmed" style="z-index: 2"> - <QBtn fab icon="check" color="primary" @click="confirmOrder()" /> - <QTooltip> - {{ t('confirm') }} - </QTooltip> - </QPageSticky> - </QPage> + <VnTable + ref="tableLinesRef" + data-key="OrderLines" + url="OrderRows" + :columns="columns" + :right-search="false" + :use-model="true" + auto-load + :user-filter="lineFilter" + > + <template #column-image="{ row }"> + <div class="image-wrapper"> + <VnImg :id="parseInt(row?.item?.image)" class="rounded" /> + </div> + </template> + <template #column-id="{ row }"> + <span class="link" @click.stop> + {{ row?.item?.id }} + <ItemDescriptorProxy :id="row?.item?.id" /> + </span> + </template> + <template #column-itemFk="{ row }"> + <div class="row column full-width justify-between items-start"> + {{ row?.item?.name }} + <div v-if="row?.item?.subName" class="subName"> + {{ row?.item?.subName.toUpperCase() }} + </div> + </div> + <FetchedTags :item="row?.item" /> + </template> + <template #column-amount="{ row }"> + {{ toCurrency(row.quantity * row.price) }} + </template> + <template #column-tableActions="{ row }"> + <QIcon + v-if="row.order?.isConfirmed === 0" + name="delete" + icon="delete" + @click="confirmRemove(row)" + class="cursor-pointer" + /> + </template> + </VnTable> + <QPageSticky :offset="[20, 20]" v-if="!order?.isConfirmed" style="z-index: 2"> + <QBtn fab icon="check" color="primary" @click="confirmOrder()" /> + <QTooltip> + {{ t('confirm') }} + </QTooltip> + </QPageSticky> </template> <style lang="scss" scoped> diff --git a/src/pages/Order/Card/OrderSearchbar.vue b/src/pages/Order/Card/OrderSearchbar.vue index a768768a5..fa30a097f 100644 --- a/src/pages/Order/Card/OrderSearchbar.vue +++ b/src/pages/Order/Card/OrderSearchbar.vue @@ -10,7 +10,7 @@ const { t } = useI18n(); data-key="OrderList" url="Orders/filter" :label="t('Search order')" - :info="t('You can search orders by reference')" + :info="t('Search orders by ticket id')" /> </template> @@ -18,5 +18,5 @@ const { t } = useI18n(); <i18n> es: Search order: Buscar orden - You can search orders by reference: Puedes buscar por referencia de la orden + Search orders by ticket id: Buscar pedido por id ticket </i18n> diff --git a/src/pages/Order/Card/OrderSummary.vue b/src/pages/Order/Card/OrderSummary.vue index cb708fb1d..60358f744 100644 --- a/src/pages/Order/Card/OrderSummary.vue +++ b/src/pages/Order/Card/OrderSummary.vue @@ -180,17 +180,19 @@ const detailsColumns = ref([ <ItemDescriptorProxy :id="props.row.item?.id" /> </span> </QTd> - <QTd key="description" :props="props" class="description"> - <div class="name"> - <span>{{ props.row.item.name }}</span> - <span - v-if="props.row.item.subName" - class="subName" - > - {{ props.row.item.subName }} - </span> + <QTd key="description" :props="props"> + <div class="description"> + <div class="name"> + {{ props.row.item.name }} + <span + v-if="props.row.item.subName" + class="subName" + > + {{ props.row.item.subName }} + </span> + </div> </div> - <FetchedTags :item="props.row.item" :max-length="5" /> + <FetchedTags :item="props.row.item" /> </QTd> <QTd key="quantity" :props="props"> {{ props.row.quantity }} @@ -228,24 +230,13 @@ const detailsColumns = ref([ } .description { - display: flex; - flex-direction: column; - justify-content: center; text-align: left; - height: auto; - padding-top: 12px; - padding-bottom: 12px; + padding-top: 15px; + padding-bottom: 15px; .name { - display: flex; - align-items: center; - padding-bottom: 8px; - - & > * { - flex: 1; - } - .subName { + margin-left: 5%; text-transform: uppercase; color: var(--vn-label-color); } diff --git a/src/pages/Order/Card/OrderVolume.vue b/src/pages/Order/Card/OrderVolume.vue new file mode 100644 index 000000000..27ee24197 --- /dev/null +++ b/src/pages/Order/Card/OrderVolume.vue @@ -0,0 +1,156 @@ +<script setup> +import axios from 'axios'; +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; +import { ref, onMounted } from 'vue'; +import { dashIfEmpty } from 'src/filters'; +import { useStateStore } from 'stores/useStateStore'; + +import FetchData from 'components/FetchData.vue'; +import FetchedTags from 'components/ui/FetchedTags.vue'; +import ItemDescriptorProxy from 'pages/Item/Card/ItemDescriptorProxy.vue'; +import VnLv from 'components/ui/VnLv.vue'; +import VnTable from 'components/VnTable/VnTable.vue'; + +const route = useRoute(); +const { t } = useI18n(); +const volumeSummary = ref(null); +const volumeRef = ref(); +const volumes = ref([]); +const volumeFilter = ref({ + include: [ + { + relation: 'item', + }, + ], + where: { orderFk: route.params.id }, +}); + +const columns = [ + { + name: 'itemFk', + label: t('item'), + align: 'left', + }, + { + name: 'description', + label: t('globals.description'), + align: 'left', + }, + { + name: 'quantity', + label: t('quantity'), + align: 'left', + }, + { + name: 'volume', + label: t('volume'), + align: 'left', + }, +]; + +const loadVolumes = async (rows) => { + if (!rows) return; + const { data } = await axios.get(`Orders/${route.params.id}/getVolumes`); + rows.forEach((order) => { + (data.volumes || []).forEach((volume) => { + if (order.itemFk === volume.itemFk) order.volume = volume.volume; + }); + }); + volumes.value = rows; +}; + +const stateStore = useStateStore(); +onMounted(async () => (stateStore.rightDrawer = false)); +</script> + +<template> + <FetchData + :url="`Orders/${route.params.id}/getTotalVolume`" + @on-fetch="(data) => (volumeSummary = data)" + auto-load + /> + <QCard v-if="volumeSummary" class="order-volume-summary q-pa-lg"> + <VnLv :label="t('total')" :value="`${volumeSummary?.totalVolume} m³`" /> + <VnLv + :label="t('boxes')" + :value="`${dashIfEmpty(volumeSummary?.totalBoxes)} U`" + /> + </QCard> + <VnTable + ref="volumeRef" + data-key="OrderCatalogVolume" + url="OrderRows" + :columns="columns" + auto-load + :user-filter="volumeFilter" + order="itemFk" + @on-fetch="(data) => loadVolumes(data)" + :right-search="false" + :column-search="false" + :disable-option="{ card: true }" + > + <template #column-itemFk="{ row }"> + <span class="link"> + {{ row.itemFk }} + <ItemDescriptorProxy :id="row.itemFk" /> + </span> + </template> + <template #column-description="{ row }"> + <div class="row column full-width justify-between items-start"> + {{ row?.item?.name }} + <div v-if="row?.item?.subName" class="subName"> + {{ row?.item?.subName.toUpperCase() }} + </div> + </div> + <FetchedTags :item="row?.item" /> + </template> + <template #column-volume="{ rowIndex }"> + {{ volumes?.[rowIndex]?.volume }} + </template> + </VnTable> +</template> + +<style lang="scss"> +.order-volume-summary { + .vn-label-value { + display: flex; + justify-content: flex-end; + gap: 2%; + + .label { + color: var(--vn-label-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .value { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} + +.subName { + color: var(--vn-label-color); + text-transform: uppercase; +} +</style> +<i18n> +en: + summary: Summary + total: Total + boxes: Boxes + item: Item + quantity: Quantity + volume: m³ per quantity +es: + summary: Resumen + total: Total + boxes: Cajas + item: Artículo + quantity: Cantidad + volume: m³ por cantidad +</i18n> diff --git a/src/pages/Order/OrderList.vue b/src/pages/Order/OrderList.vue index 945f61f3b..d96a33ef5 100644 --- a/src/pages/Order/OrderList.vue +++ b/src/pages/Order/OrderList.vue @@ -3,19 +3,24 @@ import axios from 'axios'; import { useI18n } from 'vue-i18n'; import { computed, ref } from 'vue'; import { dashIfEmpty, toCurrency, toDate } from 'src/filters'; -import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import OrderSummary from 'pages/Order/Card/OrderSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import VnTable from 'src/components/VnTable/VnTable.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; +import OrderSearchbar from './Card/OrderSearchbar.vue'; +import RightMenu from 'src/components/common/RightMenu.vue'; +import OrderFilter from './Card/OrderFilter.vue'; +import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue'; +import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue'; +import { toDateTimeFormat } from 'src/filters/date'; const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const tableRef = ref(); -const clientList = ref([]); const agencyList = ref([]); -const selectedAddress = ref(); +const addressesList = ref([]); +const clientId = ref(); const columns = computed(() => [ { @@ -29,7 +34,7 @@ const columns = computed(() => [ }, { align: 'left', - name: 'clientName', + name: 'clientFk', label: t('module.customer'), isTitle: true, cardVisible: true, @@ -41,20 +46,26 @@ const columns = computed(() => [ columnField: { component: null, }, + format: (row) => row?.clientName, }, { align: 'left', - name: 'name', + name: 'salesPersonFk', label: t('module.salesPerson'), - component: 'select', - attrs: { - url: 'Workers/activeWithInheritedRole', - fields: ['id', 'name'], - where: { role: 'salesPerson' }, - }, - columnField: { - component: null, + columnFilter: { + component: 'select', + inWhere: true, + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + where: { role: 'salesPerson' }, + useLike: false, + optionValue: 'id', + optionLabel: 'name', + optionFilter: 'firstName', + }, }, + format: (row) => row?.name, }, { align: 'left', @@ -67,7 +78,7 @@ const columns = computed(() => [ label: t('module.created'), component: 'date', cardVisible: true, - format: (row) => toDate(row?.landed), + format: (row) => toDateTimeFormat(row?.landed), columnField: { component: null, }, @@ -92,17 +103,22 @@ const columns = computed(() => [ }, { align: 'left', - name: 'agencyName', + name: 'agencyModeFk', label: t('module.agency'), - component: 'select', + format: (row) => row?.agencyName, + columnFilter: { + component: 'select', + attrs: { + url: 'agencyModes', + fields: ['id', 'name'], + find: { + value: 'agencyModeFk', + label: 'agencyName', + }, + }, + }, cardVisible: true, - attrs: { - url: 'Agencies', - fields: ['id', 'name'], - }, - columnField: { - component: null, - }, + columnClass: 'expand', }, { align: 'left', @@ -120,27 +136,52 @@ const columns = computed(() => [ title: t('InvoiceOutSummary'), icon: 'preview', action: (row) => viewSummary(row.id, OrderSummary), + isPrimary: true, }, ], }, ]); -async function fetchClientAddress(id, data) { - const clientData = await axios.get(`Clients/${id}`); - selectedAddress.value = clientData.data.defaultAddressFk; - data.addressId = selectedAddress.value; +async function fetchClientAddress(id, formData) { + const { data } = await axios.get(`Clients/${id}`, { + params: { filter: { include: { relation: 'addresses' } } }, + }); + addressesList.value = data.addresses; + formData.addressId = data.defaultAddressFk; + fetchAgencies(formData); } + +async function fetchAgencies({ landed, addressId }) { + if (!landed || !addressId) return (agencyList.value = []); + + const { data } = await axios.get('Agencies/landsThatDay', { + params: { addressFk: addressId, landed }, + }); + agencyList.value = data; +} + +const getDateColor = (date) => { + const today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + const timeTicket = new Date(date); + timeTicket.setHours(0, 0, 0, 0); + const comparation = today - timeTicket; + if (comparation == 0) return 'bg-warning'; + if (comparation < 0) return 'bg-success'; +}; </script> <template> - <VnSearchbar - data-key="OrderList" - :label="t('Search order')" - :info="t('You can search orders by reference')" - /> + <OrderSearchbar /> + <RightMenu> + <template #right-panel> + <OrderFilter data-key="OrderList" /> + </template> + </RightMenu> <VnTable ref="tableRef" data-key="OrderList" url="Orders/filter" + :order="['landed DESC', 'clientFk ASC', 'id DESC']" :create="{ urlCreate: 'Orders/new', title: 'Create Order', @@ -152,36 +193,68 @@ async function fetchClientAddress(id, data) { addressId: null, }, }" + :user-params="{ showEmpty: false }" + :right-search="false" :columns="columns" redirect="order" - auto-load > + <template #column-clientFk="{ row }"> + <span class="link" @click.stop> + {{ row?.clientName }} + <CustomerDescriptorProxy :id="row?.clientFk" /> + </span> + </template> + <template #column-salesPersonFk="{ row }"> + <span class="link" @click.stop> + {{ row?.name }} + <WorkerDescriptorProxy :id="row?.salesPersonFk" /> + </span> + </template> + <template #column-landed="{ row }"> + <span v-if="getDateColor(row.landed)"> + <QChip :class="getDateColor(row.landed)" dense square> + {{ toDate(row?.landed) }} + </QChip> + </span> + </template> <template #more-create-dialog="{ data }"> <VnSelect url="Clients" - v-model="data.id" + :include="{ relation: 'addresses' }" + v-model="clientId" :label="t('module.customer')" - :options="clientList" - option-value="id" - option-label="name" @update:model-value="(id) => fetchClientAddress(id, data)" /> <VnSelect - url="Clients" - v-model="selectedAddress" + v-model="data.addressId" + :options="addressesList" :label="t('module.address')" - :options="selectedAddress" - option-value="defaultAddressFk" - option-label="street" + option-value="id" + option-label="nickname" + @update:model-value="() => fetchAgencies(data)" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.nickname }}: {{ scope.opt?.street }}, + {{ scope.opt?.city }}</QItemLabel + > + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnInputDate + v-model="data.landed" + :label="t('module.landed')" + @update:model-value="() => fetchAgencies(data)" /> - <VnInputDate v-model="data.landed" :label="t('module.landed')" /> <VnSelect - url="Agencies" v-model="data.agencyModeId" :label="t('module.agency')" :options="agencyList" - option-value="id" - option-label="name" + option-value="agencyModeFk" + option-label="agencyMode" /> </template> </VnTable> diff --git a/src/pages/Order/OrderVolume.vue b/src/pages/Order/OrderVolume.vue deleted file mode 100644 index 35d68de16..000000000 --- a/src/pages/Order/OrderVolume.vue +++ /dev/null @@ -1,156 +0,0 @@ -<script setup> -import { useRoute } from 'vue-router'; -import { useI18n } from 'vue-i18n'; -import { ref } from 'vue'; - -import VnPaginate from 'components/ui/VnPaginate.vue'; -import FetchData from 'components/FetchData.vue'; -import VnLv from 'components/ui/VnLv.vue'; -import CardList from 'components/ui/CardList.vue'; -import FetchedTags from 'components/ui/FetchedTags.vue'; - -import { dashIfEmpty } from 'src/filters'; -import axios from 'axios'; - -const route = useRoute(); -const { t } = useI18n(); -const volumeSummary = ref(null); - -const loadVolumes = async (rows) => { - const { data } = await axios.get(`Orders/${route.params.id}/getVolumes`); - (rows || []).forEach((order) => { - (data.volumes || []).forEach((volume) => { - if (order.itemFk === volume.itemFk) { - order.volume = volume.volume; - } - }); - }); -}; -</script> - -<template> - <FetchData - :url="`Orders/${route.params.id}/getTotalVolume`" - @on-fetch="(data) => (volumeSummary = data)" - auto-load - /> - <QPage class="column items-center q-pa-md"> - <div class="vn-card-list"> - <div - v-if="!volumeSummary?.totalVolume && !volumeSummary?.totalBoxes" - class="no-result" - > - {{ t('globals.noResults') }} - </div> - <QCard v-else class="order-volume-summary q-pa-lg"> - <p class="header text-right block"> - {{ t('summary') }} - </p> - <VnLv :label="t('total')" :value="`${volumeSummary?.totalVolume} m³`" /> - <VnLv - :label="t('boxes')" - :value="`${dashIfEmpty(volumeSummary?.totalBoxes)} U`" - /> - </QCard> - <VnPaginate - data-key="OrderCatalogVolume" - url="OrderRows" - :limit="20" - auto-load - :filter="{ - include: { - relation: 'item', - }, - where: { orderFk: route.params.id }, - }" - order="itemFk" - @on-fetch="(data) => loadVolumes(data)" - > - <template #body="{ rows }"> - <div class="catalog-list q-mt-xl"> - <CardList - v-for="row in rows" - :key="row.id" - :id="row.id" - :title="row?.item?.name" - class="cursor-inherit" - > - <template #list-items> - <div class="q-mb-sm"> - <FetchedTags :item="row.item" :max-length="5" /> - </div> - <VnLv :label="t('item')" :value="row.item.id" /> - <VnLv :label="t('subName')"> - <template #value> - <span class="text-uppercase"> - {{ row.item.subName }} - </span> - </template> - </VnLv> - <VnLv :label="t('quantity')" :value="row.quantity" /> - <VnLv :label="t('volume')" :value="row.volume" /> - </template> - </CardList> - </div> - </template> - </VnPaginate> - </div> - </QPage> -</template> - -<style lang="scss"> -.order-volume-summary { - .vn-label-value { - display: flex; - justify-content: flex-end; - gap: 2%; - - .label { - color: var(--vn-label-color); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .value { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } -} -</style> -<style lang="scss" scoped> -.header { - color: $primary; - font-weight: bold; - margin-bottom: 25px; - font-size: 20px; - display: inline-block; -} - -.no-result { - font-size: 24px; - font-weight: bold; - color: var(--vn-label-color); - text-align: center; -} -</style> -<i18n> -en: - summary: Summary - total: Total - boxes: Boxes - item: Item - subName: Subname - quantity: Quantity - volume: m³ per quantity -es: - summary: Resumen - total: Total - boxes: Cajas - item: Artículo - subName: Subname - quantity: Cantidad - volume: m³ por cantidad -</i18n> diff --git a/src/pages/Route/Card/RouteSummary.vue b/src/pages/Route/Card/RouteSummary.vue index d7a02833e..3f9b1a2a9 100644 --- a/src/pages/Route/Card/RouteSummary.vue +++ b/src/pages/Route/Card/RouteSummary.vue @@ -217,7 +217,7 @@ const ticketColumns = ref([ <template #body-cell-city="{ value, row }"> <QTd auto-width> <span - class="text-primary cursor-pointer" + class="link cursor-pointer" @click="openBuscaman(entity?.route?.vehicleFk, [row])" > {{ value }} @@ -226,7 +226,7 @@ const ticketColumns = ref([ </template> <template #body-cell-client="{ value, row }"> <QTd auto-width> - <span class="text-primary cursor-pointer"> + <span class="link cursor-pointer"> {{ value }} <CustomerDescriptorProxy :id="row?.clientFk" /> </span> @@ -234,7 +234,7 @@ const ticketColumns = ref([ </template> <template #body-cell-ticket="{ value, row }"> <QTd auto-width class="text-center"> - <span class="text-primary cursor-pointer"> + <span class="link cursor-pointer"> {{ value }} <TicketDescriptorProxy :id="row?.id" /> </span> diff --git a/src/pages/Route/RouteList.vue b/src/pages/Route/RouteList.vue index e24ed33ed..1e20df99c 100644 --- a/src/pages/Route/RouteList.vue +++ b/src/pages/Route/RouteList.vue @@ -17,7 +17,9 @@ import RouteFilter from 'pages/Route/Card/RouteFilter.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import VnTable from 'components/VnTable/VnTable.vue'; +import { usePrintService } from 'src/composables/usePrintService'; +const { openReport } = usePrintService(); const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const quasar = useQuasar(); @@ -87,6 +89,7 @@ const columns = computed(() => [ label: 'agencyName', }, }, + columnClass: 'expand', }, { align: 'left', @@ -142,17 +145,9 @@ const columns = computed(() => [ { align: 'center', name: 'm3', - label: 'volume', + label: t('Volume'), cardVisible: true, - }, - { - align: 'left', - name: 'description', - label: t('Description'), - isTitle: true, - create: true, - component: 'input', - field: 'description', + columnClass: 'shrink', }, { align: 'left', @@ -168,12 +163,38 @@ const columns = computed(() => [ component: 'time', columnFilter: false, }, + { + align: 'center', + name: 'kmStart', + label: t('KmStart'), + columnClass: 'shrink', + create: true, + visible: false, + }, + { + align: 'center', + name: 'kmEnd', + label: t('KmEnd'), + columnClass: 'shrink', + create: true, + visible: false, + }, + { + align: 'left', + name: 'description', + label: t('Description'), + isTitle: true, + create: true, + component: 'input', + field: 'description', + }, { align: 'left', name: 'isOk', label: t('Served'), component: 'checkbox', columnFilter: false, + columnClass: 'shrink', }, { align: 'right', @@ -185,7 +206,7 @@ const columns = computed(() => [ action: (row) => openTicketsDialog(row?.id), }, { - title: t('Preview'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row?.id, RouteSummary), }, @@ -216,18 +237,15 @@ const cloneRoutes = () => { const showRouteReport = () => { const ids = selectedRows.value.map((row) => row?.id); const idString = ids.join(','); - let url; - - if (selectedRows.value.length <= 1) { - url = `api/Routes/${idString}/driver-route-pdf?access_token=${session.getTokenMultimedia()}`; - } else { - const params = new URLSearchParams({ - access_token: session.getTokenMultimedia(), + let url = `Routes/${idString}/driver-route-pdf`; + let params = {}; + if (selectedRows.value.length >= 1) { + params = { id: idString, - }); - url = `api/Routes/downloadZip?${params.toString()}`; + }; + url = `Routes/downloadZip`; } - window.open(url, '_blank'); + openReport(url, params, '_blank'); }; function markAsServed() { @@ -368,10 +386,13 @@ es: Worker: Trabajador Agency: Agencia Vehicle: Vehículo + Volume: Volumen Date: Fecha Description: Descripción Hour started: Hora inicio Hour finished: Hora fin + KmStart: Km inicio + KmEnd: Km fin Served: Servida newRoute: Nueva Ruta Clone Selected Routes: Clonar rutas seleccionadas diff --git a/src/pages/Shelving/Card/ShelvingFilter.vue b/src/pages/Shelving/Card/ShelvingFilter.vue index abc91373b..0056ffaec 100644 --- a/src/pages/Shelving/Card/ShelvingFilter.vue +++ b/src/pages/Shelving/Card/ShelvingFilter.vue @@ -3,6 +3,7 @@ import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import FetchData from 'components/FetchData.vue'; import VnFilterPanel from 'components/ui/VnFilterPanel.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; const { t } = useI18n(); const props = defineProps({ @@ -15,25 +16,13 @@ const props = defineProps({ const emit = defineEmits(['search']); const workers = ref(); -const parkings = ref(); function setWorkers(data) { workers.value = data; } - -function setParkings(data) { - parkings.value = data; -} </script> <template> - <FetchData - url="Parkings" - :filter="{ fields: ['id', 'code'] }" - sort-by="code ASC" - @on-fetch="setParkings" - auto-load - /> <FetchData url="Workers/activeWithInheritedRole" :filter="{ where: { role: 'salesPerson' } }" @@ -54,44 +43,36 @@ function setParkings(data) { </template> <template #body="{ params }"> <QItem class="q-my-sm"> - <QItemSection v-if="!parkings"> - <QSkeleton type="QInput" class="full-width" /> - </QItemSection> - <QItemSection v-if="parkings"> - <QSelect + <QItemSection> + <VnSelect + v-model="params.parkingFk" + url="Parkings" + :fields="['id', 'code']" + :label="t('params.parkingFk')" + option-value="id" + option-label="code" + :filter-options="['id', 'code']" dense outlined rounded - :label="t('params.parkingFk')" - v-model="params.parkingFk" - :options="parkings" - option-value="id" - option-label="code" - emit-value - map-options - use-input - :input-debounce="0" + sort-by="code ASC" /> </QItemSection> </QItem> <QItem class="q-mb-sm"> - <QItemSection v-if="!workers"> - <QSkeleton type="QInput" class="full-width" /> - </QItemSection> - <QItemSection v-if="workers"> - <QSelect + <QItemSection> + <VnSelect dense outlined rounded :label="t('params.userFk')" v-model="params.userFk" - :options="workers" + url="Workers/activeWithInheritedRole" option-value="id" - option-label="name" - emit-value - map-options - use-input - :input-debounce="0" + option-label="firstName" + :where="{ role: 'salesPerson' }" + sort-by="firstName ASC" + :use-like="false" /> </QItemSection> </QItem> diff --git a/src/pages/Shelving/Card/ShelvingForm.vue b/src/pages/Shelving/Card/ShelvingForm.vue index dd1c4e4a2..dc0234c22 100644 --- a/src/pages/Shelving/Card/ShelvingForm.vue +++ b/src/pages/Shelving/Card/ShelvingForm.vue @@ -1,18 +1,18 @@ <script setup> import { useI18n } from 'vue-i18n'; -import { ref } from 'vue'; +import { computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import VnRow from 'components/ui/VnRow.vue'; -import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; const { t } = useI18n(); const route = useRoute(); const router = useRouter(); -const shelvingId = route.params?.id || null; -const isNew = Boolean(!shelvingId); +const entityId = computed(() => route.params.id ?? null); +const isNew = Boolean(!entityId.value); const defaultInitialData = { parkingFk: null, priority: null, @@ -20,30 +20,6 @@ const defaultInitialData = { isRecyclable: false, }; -const parkingFilter = { fields: ['id', 'code'] }; -const parkingList = ref([]); -const parkingListCopy = ref([]); - -const setParkingList = (data) => { - parkingList.value = data; - parkingListCopy.value = data; -}; - -const parkingSelectFilter = { - options: parkingList, - filterFn: (options, value) => { - const search = value.trim().toLowerCase(); - - if (!search || search === '') { - return parkingListCopy.value; - } - - return options.value.filter((option) => - option.code.toLowerCase().startsWith(search) - ); - }, -}; - const shelvingFilter = { include: [ { @@ -67,44 +43,33 @@ const onSave = (shelving, newShelving) => { }; </script> <template> - <VnSubToolbar /> - <FetchData - url="Parkings" - :filter="parkingFilter" - @on-fetch="setParkingList" - auto-load - /> + <VnSubToolbar v-if="isNew" /> <FormModel - :url="isNew ? null : `Shelvings/${shelvingId}`" + :url="isNew ? null : `Shelvings/${entityId}`" :url-create="isNew ? 'Shelvings' : null" :observe-form-changes="!isNew" :filter="shelvingFilter" model="shelving" :auto-load="!isNew" - :form-initial-data="defaultInitialData" + :form-initial-data="isNew ? defaultInitialData : null" @on-data-saved="onSave" > - <template #form="{ data, validate, filter }"> + <template #form="{ data, validate }"> <VnRow> <VnInput v-model="data.code" :label="t('shelving.basicData.code')" :rules="validate('Shelving.code')" /> - <QSelect + <VnSelect v-model="data.parkingFk" - :options="parkingList" + url="Parkings" option-value="id" option-label="code" - emit-value + :filter-options="['id', 'code']" + :fields="['id', 'code']" :label="t('shelving.basicData.parking')" - map-options - use-input - @filter=" - (value, update) => filter(value, update, parkingSelectFilter) - " :rules="validate('Shelving.parkingFk')" - :input-debounce="0" /> </VnRow> <VnRow> diff --git a/src/pages/Shelving/ShelvingList.vue b/src/pages/Shelving/ShelvingList.vue index 3f16f8ef7..d29f6ff15 100644 --- a/src/pages/Shelving/ShelvingList.vue +++ b/src/pages/Shelving/ShelvingList.vue @@ -87,7 +87,7 @@ function exprBuilder(param, value) { </div> <QPageSticky :offset="[20, 20]"> <RouterLink :to="{ name: 'ShelvingCreate' }"> - <QBtn fab icon="add" color="primary" /> + <QBtn fab icon="add" color="primary" shortcut="+" /> <QTooltip> {{ t('shelving.list.newShelving') }} </QTooltip> diff --git a/src/pages/Supplier/Card/SupplierAddresses.vue b/src/pages/Supplier/Card/SupplierAddresses.vue index 4b2174d0b..f46a3be19 100644 --- a/src/pages/Supplier/Card/SupplierAddresses.vue +++ b/src/pages/Supplier/Card/SupplierAddresses.vue @@ -87,7 +87,13 @@ const redirectToUpdateView = (addressData) => { </VnPaginate> </div> <QPageSticky :offset="[20, 20]"> - <QBtn fab icon="add" color="primary" @click="redirectToCreateView()" /> + <QBtn + fab + icon="add" + color="primary" + @click="redirectToCreateView()" + shortcut="+" + /> <QTooltip> {{ t('New address') }} </QTooltip> @@ -99,3 +105,4 @@ const redirectToUpdateView = (addressData) => { es: New address: Nueva dirección </i18n> +s diff --git a/src/pages/Supplier/Card/SupplierAddressesCreate.vue b/src/pages/Supplier/Card/SupplierAddressesCreate.vue index df98b6091..290373039 100644 --- a/src/pages/Supplier/Card/SupplierAddressesCreate.vue +++ b/src/pages/Supplier/Card/SupplierAddressesCreate.vue @@ -84,7 +84,7 @@ function handleLocation(data, location) { <VnRow> <VnLocation :rules="validate('Worker.postcode')" - :roles-allowed-to-create="['deliveryAssistant']" + :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" :location=" data.postalCode ? { diff --git a/src/pages/Supplier/Card/SupplierAgencyTerm.vue b/src/pages/Supplier/Card/SupplierAgencyTerm.vue index 26a410e8c..99b672cc4 100644 --- a/src/pages/Supplier/Card/SupplierAgencyTerm.vue +++ b/src/pages/Supplier/Card/SupplierAgencyTerm.vue @@ -109,7 +109,13 @@ const redirectToCreateView = () => { </template> </CrudModel> <QPageSticky :offset="[20, 20]"> - <QBtn fab icon="add" color="primary" @click="redirectToCreateView()" /> + <QBtn + fab + icon="add" + color="primary" + @click="redirectToCreateView()" + shortcut="+" + /> <QTooltip> {{ t('supplier.agencyTerms.addRow') }} </QTooltip> diff --git a/src/pages/Supplier/Card/SupplierConsumption.vue b/src/pages/Supplier/Card/SupplierConsumption.vue index 100a38b2a..8fa6a1e5c 100644 --- a/src/pages/Supplier/Card/SupplierConsumption.vue +++ b/src/pages/Supplier/Card/SupplierConsumption.vue @@ -208,7 +208,7 @@ onMounted(async () => { <QTd no-hover> <span>{{ buy.subName }}</span> - <FetchedTags :item="buy" :max-length="5" /> + <FetchedTags :item="buy" /> </QTd> <QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd> <QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd> diff --git a/src/pages/Supplier/Card/SupplierConsumptionFilter.vue b/src/pages/Supplier/Card/SupplierConsumptionFilter.vue index 3fc61b15e..401bde8fa 100644 --- a/src/pages/Supplier/Card/SupplierConsumptionFilter.vue +++ b/src/pages/Supplier/Card/SupplierConsumptionFilter.vue @@ -1,56 +1,21 @@ <script setup> -import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; - import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInput from 'src/components/common/VnInput.vue'; -import FetchData from 'components/FetchData.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; const { t } = useI18n(); -const props = defineProps({ +defineProps({ dataKey: { type: String, required: true, }, }); - -const buyersOptions = ref([]); -const itemTypesOptions = ref([]); -const itemCategoriesOptions = ref([]); </script> <template> - <FetchData - url="TicketRequests/getItemTypeWorker" - :filter="{ - fields: ['id', 'nickname'], - order: 'nickname ASC', - }" - @on-fetch="(data) => (buyersOptions = data)" - auto-load - /> - <FetchData - url="ItemTypes" - :filter="{ - fields: ['id', 'name', 'categoryFk'], - include: 'category', - order: 'name ASC', - }" - @on-fetch="(data) => (itemTypesOptions = data)" - auto-load - /> - <FetchData - url="ItemCategories" - :filter="{ - fields: ['id', 'name'], - order: 'name ASC', - }" - @on-fetch="(data) => (itemCategoriesOptions = data)" - auto-load - /> - <VnFilterPanel :data-key="props.dataKey" :search-button="true" :redirect="false"> + <VnFilterPanel :data-key="dataKey" :search-button="true" :redirect="false"> <template #tags="{ tag, formatFn }"> <div class="q-gutter-x-xs"> <strong>{{ t(`params.${tag.label}`) }}: </strong> @@ -82,7 +47,9 @@ const itemCategoriesOptions = ref([]); :label="t('params.buyerId')" v-model="params.buyerId" @update:model-value="searchFn()" - :options="buyersOptions" + url="TicketRequests/getItemTypeWorker" + :fields="['id', 'nickname']" + sort-by="nickname ASC" option-value="id" option-label="nickname" hide-selected @@ -98,7 +65,10 @@ const itemCategoriesOptions = ref([]); :label="t('params.typeId')" v-model="params.typeId" @update:model-value="searchFn()" - :options="itemTypesOptions" + url="ItemTypes" + :include="['category']" + :fields="['id', 'name', 'categoryFk']" + sort-by="name ASC" option-value="id" option-label="name" hide-selected @@ -125,7 +95,9 @@ const itemCategoriesOptions = ref([]); :label="t('params.categoryId')" v-model="params.categoryId" @update:model-value="searchFn()" - :options="itemCategoriesOptions" + url="ItemCategories" + :fields="['id', 'name']" + sort-by="name ASC" option-value="id" option-label="name" hide-selected diff --git a/src/pages/Supplier/Card/SupplierFiscalData.vue b/src/pages/Supplier/Card/SupplierFiscalData.vue index c2cb42c6a..60cd6770b 100644 --- a/src/pages/Supplier/Card/SupplierFiscalData.vue +++ b/src/pages/Supplier/Card/SupplierFiscalData.vue @@ -146,7 +146,7 @@ function handleLocation(data, location) { <VnRow> <VnLocation :rules="validate('Worker.postcode')" - :roles-allowed-to-create="['deliveryAssistant']" + :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" :location="{ postcode: data.postCode, city: data.city, diff --git a/src/pages/Supplier/Card/SupplierSummary.vue b/src/pages/Supplier/Card/SupplierSummary.vue index 822586b47..5791db1eb 100644 --- a/src/pages/Supplier/Card/SupplierSummary.vue +++ b/src/pages/Supplier/Card/SupplierSummary.vue @@ -4,13 +4,11 @@ import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardSummary from 'components/ui/CardSummary.vue'; import VnLv from 'src/components/ui/VnLv.vue'; -import { useRole } from 'src/composables/useRole'; import { dashIfEmpty } from 'src/filters'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnTitle from 'src/components/common/VnTitle.vue'; const route = useRoute(); -const roleState = useRole(); const { t } = useI18n(); const $props = defineProps({ @@ -32,13 +30,7 @@ async function setData(data) { } } -const isAdministrative = computed(() => { - return roleState.hasAny(['administrative']); -}); - -function getUrl(section) { - return isAdministrative.value && `#/supplier/${entityId.value}/${section}`; -} +const getUrl = (section) => `#/supplier/${entityId.value}/${section}`; </script> <template> diff --git a/src/pages/Ticket/Card/BasicData/BasicDataTable.vue b/src/pages/Ticket/Card/BasicData/BasicDataTable.vue index 48b8c882f..7f2f100ad 100644 --- a/src/pages/Ticket/Card/BasicData/BasicDataTable.vue +++ b/src/pages/Ticket/Card/BasicData/BasicDataTable.vue @@ -245,7 +245,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <div class="column"> <span>{{ row.item.name }}</span> <span class="color-vn-label">{{ row.item.subName }}</span> - <FetchedTags :item="row.item" :max-length="6" /> + <FetchedTags :item="row.item" /> </div> </QTd> </template> diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue index 2d937346a..28c6fcf15 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue @@ -30,7 +30,6 @@ const { t } = useI18n(); const agencyFetchRef = ref(null); const zonesFetchRef = ref(null); -const clientsOptions = ref([]); const warehousesOptions = ref([]); const companiesOptions = ref([]); const agenciesOptions = ref([]); @@ -273,15 +272,6 @@ const redirectToCustomerAddress = () => { onMounted(() => onFormModelInit()); </script> <template> - <FetchData - url="Clients" - :filter="{ - fields: ['id', 'name'], - order: 'id', - }" - @on-fetch="(data) => (clientsOptions = data)" - auto-load - /> <FetchData url="Warehouses" @on-fetch="(data) => (warehousesOptions = data)" @@ -317,7 +307,9 @@ onMounted(() => onFormModelInit()); v-model="clientId" option-value="id" option-label="name" - :options="clientsOptions" + url="Clients" + :fields="['id', 'name']" + sort-by="id" hide-selected map-options :required="true" diff --git a/src/pages/Ticket/Card/TicketCard.vue b/src/pages/Ticket/Card/TicketCard.vue index cf15cb7fa..73b6f5543 100644 --- a/src/pages/Ticket/Card/TicketCard.vue +++ b/src/pages/Ticket/Card/TicketCard.vue @@ -1,17 +1,11 @@ <script setup> import { useI18n } from 'vue-i18n'; -import { useRoute } from 'vue-router'; -import { computed } from 'vue'; import VnCard from 'components/common/VnCard.vue'; import TicketDescriptor from './TicketDescriptor.vue'; import TicketFilter from '../TicketFilter.vue'; const { t } = useI18n(); -const route = useRoute(); - -const routeName = computed(() => route.name); -const customRouteRedirectName = computed(() => routeName.value); </script> <template> <VnCard @@ -21,7 +15,7 @@ const customRouteRedirectName = computed(() => routeName.value); :descriptor="TicketDescriptor" search-data-key="TicketList" :searchbar-props="{ - customRouteRedirectName, + url: 'Tickets/filter', label: t('card.search'), info: t('card.searchInfo'), }" diff --git a/src/pages/Ticket/Card/TicketComponents.vue b/src/pages/Ticket/Card/TicketComponents.vue index 3954b5a62..6131c92db 100644 --- a/src/pages/Ticket/Card/TicketComponents.vue +++ b/src/pages/Ticket/Card/TicketComponents.vue @@ -310,7 +310,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <div class="column"> <span>{{ row.item.name }}</span> <span class="color-vn-label">{{ row.item.subName }}</span> - <FetchedTags :item="row.item" :max-length="6" /> + <FetchedTags :item="row.item" /> </div> </QTd> </template> diff --git a/src/pages/Ticket/Card/TicketCreateTracking.vue b/src/pages/Ticket/Card/TicketCreateTracking.vue index 87ca8dd3f..d692f550d 100644 --- a/src/pages/Ticket/Card/TicketCreateTracking.vue +++ b/src/pages/Ticket/Card/TicketCreateTracking.vue @@ -17,9 +17,7 @@ const { t } = useI18n(); const state = useState(); const user = state.getUser(); const stateFetchDataRef = ref(null); - const statesOptions = ref([]); -const workersOptions = ref([]); const onStateFkChange = (formData) => (formData.userFk = user.value.id); </script> @@ -30,12 +28,6 @@ const onStateFkChange = (formData) => (formData.userFk = user.value.id); auto-load @on-fetch="(data) => (statesOptions = data)" /> - <FetchData - url="Workers/search" - :filter="{ fields: ['id', 'name'], order: 'name ASC' }" - auto-load - @on-fetch="(data) => (workersOptions = data)" - /> <FormModelPopup :title="t('Create tracking')" url-create="Tickets/state" @@ -57,7 +49,9 @@ const onStateFkChange = (formData) => (formData.userFk = user.value.id); <VnSelect :label="t('tracking.worker')" v-model="data.userFk" - :options="workersOptions" + url="Workers/search" + fields=" ['id', 'name']" + sort-by="name ASC" hide-selected option-label="name" option-value="id" diff --git a/src/pages/Ticket/Card/TicketDescriptorMenu.vue b/src/pages/Ticket/Card/TicketDescriptorMenu.vue index c7784bc2a..4cf4e633f 100644 --- a/src/pages/Ticket/Card/TicketDescriptorMenu.vue +++ b/src/pages/Ticket/Card/TicketDescriptorMenu.vue @@ -9,6 +9,8 @@ import SendEmailDialog from 'components/common/SendEmailDialog.vue'; import VnConfirm from 'components/ui/VnConfirm.vue'; import VnSmsDialog from 'components/common/VnSmsDialog.vue'; import toDate from 'filters/toDate'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +import { useArrayData } from 'src/composables/useArrayData'; const props = defineProps({ ticket: { @@ -21,19 +23,22 @@ const { push, currentRoute } = useRouter(); const { dialog, notify } = useQuasar(); const { t } = useI18n(); const { openReport, sendEmail } = usePrintService(); - +const ticketSummary = useArrayData('TicketSummary'); const ticket = ref(props.ticket); const ticketId = currentRoute.value.params.id; +const weight = ref(); const actions = { clone: async () => { const opts = { message: t('Ticket cloned'), type: 'positive' }; let clonedTicketId; try { - const { data } = await axios.post(`Tickets/${ticketId}/clone`, { - shipped: ticket.value.shipped, + const { data } = await axios.post(`Tickets/cloneAll`, { + ticketsIds: [ticket.value.id], + withWarehouse: true, + negative: false, }); - clonedTicketId = data; + clonedTicketId = data[0].id; } catch (e) { opts.message = t('It was not able to clone the ticket'); opts.type = 'negative'; @@ -44,6 +49,25 @@ const actions = { push({ name: 'TicketSummary', params: { id: clonedTicketId } }); } }, + setWeight: async () => { + try { + const invoiceIds = ( + await axios.post(`Tickets/${ticketId}/setWeight`, { + weight: weight.value, + }) + ).data; + + notify({ message: t('Weight set'), type: 'positive' }); + if (invoiceIds.length) + notify({ + message: t('invoiceIds', { invoiceIds: invoiceIds.join() }), + type: 'positive', + }); + await ticketSummary.fetch({ updateRouter: false }); + } catch (e) { + notify({ message: e.message, type: 'negative' }); + } + }, remove: async () => { try { await axios.post(`Tickets/${ticketId}/setDeleted`); @@ -253,6 +277,12 @@ function openConfirmDialog(callback) { </QItemSection> <QItemSection>{{ t('To clone ticket') }}</QItemSection> </QItem> + <QItem @click="$refs.weightDialog.show()" v-ripple clickable> + <QItemSection avatar> + <QIcon name="weight" /> + </QItemSection> + <QItemSection>{{ t('Set weight') }}</QItemSection> + </QItem> <template v-if="!ticket.isDeleted"> <QSeparator /> <QItem @click="openConfirmDialog('remove')" v-ripple clickable> @@ -262,9 +292,25 @@ function openConfirmDialog(callback) { <QItemSection>{{ t('Delete ticket') }}</QItemSection> </QItem> </template> + <VnConfirm + ref="weightDialog" + :title="t('Set weight')" + :message="t('This ticket may be invoiced, do you want to continue?')" + :promise="actions.setWeight" + > + <template #customHTML> + <VnInputNumber + :label="t('globals.weight')" + v-model="weight" + class="full-width" + /> + </template> + </VnConfirm> </template> - <i18n> +en: + invoiceIds: "Invoices have been generated with the following ids: {invoiceIds}" + es: Open Delivery Note...: Abrir albarán... Send Delivery Note...: Enviar albarán... @@ -282,4 +328,8 @@ es: To clone ticket: Clonar ticket Ticket cloned: Ticked clonado It was not able to clone the ticket: No se pudo clonar el ticket + Set weight: Establecer peso + Weight set: Peso establecido + This ticket may be invoiced, do you want to continue?: Es posible que se facture este ticket, desea continuar? + invoiceIds: "Se han generado las facturas con los siguientes ids: {invoiceIds}" </i18n> diff --git a/src/pages/Ticket/Card/TicketExpedition.vue b/src/pages/Ticket/Card/TicketExpedition.vue index a7eb9e27e..c4ab63b39 100644 --- a/src/pages/Ticket/Card/TicketExpedition.vue +++ b/src/pages/Ticket/Card/TicketExpedition.vue @@ -3,7 +3,6 @@ import { onMounted, ref, computed, onUnmounted, reactive, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import FetchData from 'components/FetchData.vue'; import VnInput from 'src/components/common/VnInput.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import TicketEditManaProxy from './TicketEditMana.vue'; @@ -35,7 +34,6 @@ const selectedExpeditions = ref([]); const allColumnNames = ref([]); const visibleColumns = ref([]); const newTicketWithRoute = ref(false); -const itemsOptions = ref([]); const exprBuilder = (param, value) => { switch (param) { @@ -139,7 +137,9 @@ const columns = computed(() => [ filterValue: null, event: getInputEvents, attrs: { - options: itemsOptions.value, + url: 'Items', + fields: ['id', 'name'], + 'sort-by': 'name ASC', 'option-value': 'id', 'option-label': 'name', dense: true, @@ -268,19 +268,12 @@ onMounted(async () => { stateStore.rightDrawer = true; const filteredColumns = columns.value.filter((col) => col.name !== 'history'); allColumnNames.value = filteredColumns.map((col) => col.name); - // await expeditionsArrayData.fetch({ append: false }); }); onUnmounted(() => (stateStore.rightDrawer = false)); </script> <template> - <FetchData - url="Items" - auto-load - :filter="{ fields: ['id', 'name'], order: 'name ASC' }" - @on-fetch="(data) => (itemsOptions = data)" - /> <VnSubToolbar> <template #st-data> <TableVisibleColumns diff --git a/src/pages/Ticket/Card/TicketPurchaseRequest.vue b/src/pages/Ticket/Card/TicketPurchaseRequest.vue index 281dc46a1..8d84e2b46 100644 --- a/src/pages/Ticket/Card/TicketPurchaseRequest.vue +++ b/src/pages/Ticket/Card/TicketPurchaseRequest.vue @@ -252,7 +252,13 @@ const openCreateModal = () => createTicketRequestDialogRef.value.show(); <TicketCreateRequest @on-request-created="crudModelRef.reload()" /> </QDialog> <QPageSticky :offset="[20, 20]"> - <QBtn @click="openCreateModal()" color="primary" fab icon="add" /> + <QBtn + @click="openCreateModal()" + color="primary" + fab + icon="add" + shortcut="+" + /> <QTooltip class="text-no-wrap"> {{ t('purchaseRequest.newRequest') }} </QTooltip> diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index cbc94b388..2ea12bb05 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -11,7 +11,6 @@ import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import TicketEditManaProxy from './TicketEditMana.vue'; import VnImg from 'src/components/ui/VnImg.vue'; -import RightMenu from 'src/components/common/RightMenu.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import TicketSaleMoreActions from './TicketSaleMoreActions.vue'; import TicketTransfer from './TicketTransfer.vue'; @@ -39,7 +38,6 @@ const ticketConfig = ref(null); const isLocked = ref(false); const isTicketEditable = ref(false); const sales = ref([]); -const itemsWithNameOptions = ref([]); const editableStatesOptions = ref([]); const selectedSales = ref([]); const mana = ref(null); @@ -436,12 +434,6 @@ onUnmounted(() => (stateStore.rightDrawer = false)); auto-load @on-fetch="(data) => (isLocked = data)" /> - <FetchData - url="Items/withName" - :filter="{ fields: ['id', 'name'], order: 'id DESC' }" - auto-load - @on-fetch="(data) => (itemsWithNameOptions = data)" - /> <FetchData url="States/editableStates" :filter="{ fields: ['code', 'name', 'id', 'alertLevel'], order: 'name ASC' }" @@ -627,10 +619,12 @@ onUnmounted(() => (stateStore.rightDrawer = false)); </div> <VnSelect v-else - :options="itemsWithNameOptions" hide-selected option-label="name" option-value="id" + url="Items/withName" + :fields="['id', 'name']" + sort-by="id DESC" @update:model-value="changeQuantity(row)" v-model="row.itemFk" > @@ -662,7 +656,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <div class="column"> <span>{{ row.concept }}</span> <span class="color-vn-label">{{ row.item?.subName }}</span> - <FetchedTags v-if="row.item" :item="row.item" :max-length="6" /> + <FetchedTags v-if="row.item" :item="row.item" /> <QPopupProxy v-if="row.id && isTicketEditable"> <VnInput v-model="row.concept" @change="updateConcept(row)" /> </QPopupProxy> diff --git a/src/pages/Ticket/Card/TicketSaleMoreActions.vue b/src/pages/Ticket/Card/TicketSaleMoreActions.vue index 9ec6b303a..94db67be2 100644 --- a/src/pages/Ticket/Card/TicketSaleMoreActions.vue +++ b/src/pages/Ticket/Card/TicketSaleMoreActions.vue @@ -131,7 +131,11 @@ const createClaim = () => { onCreateClaimAccepted ); else - openConfirmationModal(t('Do you want to create a claim?'), onCreateClaimAccepted); + openConfirmationModal( + t('Do you want to create a claim?'), + false, + onCreateClaimAccepted + ); }; const onCreateClaimAccepted = async () => { @@ -153,14 +157,22 @@ const setReserved = async (reserved) => { }; const createRefund = async (withWarehouse) => { - if (!props.sales) return; + if (!props.ticket) return; - const salesIds = props.sales.map((sale) => sale.id); - const params = { salesIds: salesIds, withWarehouse: withWarehouse, negative: true }; - const { data } = await axios.post('Sales/clone', params); - const [refundTicket] = data; - notify(t('refundTicketCreated', { ticketId: refundTicket.id }), 'positive'); - router.push({ name: 'TicketSale', params: { id: refundTicket.id } }); + const params = { + ticketsIds: [props.ticket.id], + withWarehouse: withWarehouse, + negative: true, + }; + + try { + const { data } = await axios.post('Tickets/cloneAll', params); + const [refundTicket] = data; + notify(t('refundTicketCreated', { ticketId: refundTicket.id }), 'positive'); + router.push({ name: 'TicketSale', params: { id: refundTicket.id } }); + } catch (error) { + console.error(error); + } }; </script> @@ -244,7 +256,7 @@ const createRefund = async (withWarehouse) => { </QItem> <QItem clickable v-ripple> <QItemSection> - <QItemLabel>{{ t('Refund...') }}</QItemLabel> + <QItemLabel>{{ t('Refund') }}</QItemLabel> </QItemSection> <QItemSection side> <QIcon name="keyboard_arrow_right" /> @@ -279,7 +291,7 @@ es: Add claim: Crear reclamación Mark as reserved: Marcar como reservado Unmark as reserved: Desmarcar como reservado - Refund...: Abono... + Refund: Abono with warehouse: con almacén without warehouse: sin almacén Claim out of time: Reclamación fuera de plazo diff --git a/src/pages/Ticket/Card/TicketSaleTracking.vue b/src/pages/Ticket/Card/TicketSaleTracking.vue index d519bc2c6..6978d92c8 100644 --- a/src/pages/Ticket/Card/TicketSaleTracking.vue +++ b/src/pages/Ticket/Card/TicketSaleTracking.vue @@ -26,8 +26,6 @@ const saleTrackingFetchDataRef = ref(null); const sales = ref([]); const saleTrackings = ref([]); const itemShelvignsSales = ref([]); -const shelvingsOptions = ref([]); -const parkingsOptions = ref([]); const saleTrackingUrl = computed(() => `SaleTrackings/${route.params.id}/filter`); const oldQuantity = ref(null); @@ -330,12 +328,6 @@ const qCheckBoxController = (sale, action) => { auto-load @on-fetch="(data) => (sales = data)" /> - <FetchData - url="Shelvings" - auto-load - @on-fetch="(data) => (shelvingsOptions = data)" - /> - <FetchData url="Parkings" auto-load @on-fetch="(data) => (parkingsOptions = data)" /> <QTable :rows="sales" :columns="columns" @@ -420,7 +412,7 @@ const qCheckBoxController = (sale, action) => { <span v-if="row.subName" class="color-vn-label"> {{ row.subName }} </span> - <FetchedTags :item="row" :max-length="6" tag="value" /> + <FetchedTags :item="row" tag="value" /> </div> </QTd> </template> @@ -500,7 +492,7 @@ const qCheckBoxController = (sale, action) => { <template #body-cell-shelving="{ row }"> <QTd auto-width> <VnSelect - :options="shelvingsOptions" + url="Shelvings" hide-selected option-label="code" option-value="code" @@ -513,7 +505,7 @@ const qCheckBoxController = (sale, action) => { <template #body-cell-parking="{ row }"> <QTd auto-width> <VnSelect - :options="parkingsOptions" + url="Parkings" hide-selected option-label="code" option-value="id" diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index 177384663..6af782173 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -31,8 +31,7 @@ const $props = defineProps({ const entityId = computed(() => $props.id || route.params.id); const summaryRef = ref(); -const ticket = ref(); -const salesLines = ref(null); +const ticket = computed(() => summaryRef.value?.entity); const editableStates = ref([]); const ticketUrl = ref(); const grafanaUrl = 'https://grafana.verdnatura.es'; @@ -40,12 +39,6 @@ const grafanaUrl = 'https://grafana.verdnatura.es'; onMounted(async () => { ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/'; }); -async function setData(data) { - if (data) { - ticket.value = data; - salesLines.value = data.sales; - } -} function formattedAddress() { if (!ticket.value) return ''; @@ -89,7 +82,6 @@ async function changeState(value) { <CardSummary ref="summaryRef" :url="`Tickets/${entityId}/summary`" - @on-fetch="(data) => setData(data)" data-key="TicketSummary" > <template #header="{ entity }"> @@ -131,7 +123,7 @@ async function changeState(value) { </QList> </QBtnDropdown> </template> - <template #body> + <template #body="{ entity }"> <QCard class="vn-one"> <VnTitle :url="ticketUrl + 'basic-data/step-one'" @@ -139,57 +131,58 @@ async function changeState(value) { /> <VnLv :label="t('ticket.summary.state')"> <template #value> - <QChip :color="ticket.ticketState?.state?.classColor ?? 'dark'"> - {{ ticket.ticketState?.state?.name }} + <QChip :color="entity.ticketState?.state?.classColor ?? 'dark'"> + {{ entity.ticketState?.state?.name }} </QChip> </template> </VnLv> <VnLv :label="t('ticket.summary.salesPerson')"> <template #value> <VnUserLink - :name="ticket.client?.salesPersonUser?.name" - :worker-id="ticket.client?.salesPersonFk" + :name="entity.client?.salesPersonUser?.name" + :worker-id="entity.client?.salesPersonFk" /> </template> </VnLv> <VnLv :label="t('ticket.summary.agency')" - :value="ticket.agencyMode?.name" + :value="entity.agencyMode?.name" /> - <VnLv :label="t('ticket.summary.zone')" :value="ticket?.zone?.name" /> + <VnLv :label="t('ticket.summary.zone')" :value="entity?.zone?.name" /> <VnLv :label="t('ticket.summary.warehouse')" - :value="ticket.warehouse?.name" + :value="entity.warehouse?.name" /> <VnLv + v-if="ticket?.ticketCollections?.length > 0" :label="t('ticket.summary.collection')" - :value="ticket.ticketCollections[0]?.collectionFk" + :value="ticket?.ticketCollections[0]?.collectionFk" > <template #value> <a - :href="`${grafanaUrl}/d/d552ab74-85b4-4e7f-a279-fab7cd9c6124/control-de-expediciones?orgId=1&var-collectionFk=${ticket.ticketCollections[0]?.collectionFk}`" + :href="`${grafanaUrl}/d/d552ab74-85b4-4e7f-a279-fab7cd9c6124/control-de-expediciones?orgId=1&var-collectionFk=${entity.ticketCollections[0]?.collectionFk}`" target="_blank" class="grafana" > - {{ ticket.ticketCollections[0]?.collectionFk }} + {{ entity.ticketCollections[0]?.collectionFk }} </a> </template> </VnLv> - <VnLv :label="t('ticket.summary.route')" :value="ticket.routeFk" /> + <VnLv :label="t('ticket.summary.route')" :value="entity.routeFk" /> <VnLv :label="t('ticket.summary.invoice')"> <template #value> - <span :class="{ link: ticket.refFk }"> - {{ dashIfEmpty(ticket.refFk) }} + <span :class="{ link: entity.refFk }"> + {{ dashIfEmpty(entity.refFk) }} <InvoiceOutDescriptorProxy - :id="ticket.invoiceOut.id" - v-if="ticket.refFk" + :id="entity.invoiceOut.id" + v-if="entity.refFk" /> </span> </template> </VnLv> <VnLv :label="t('ticket.summary.weight')" - :value="dashIfEmpty(ticket.weight)" + :value="dashIfEmpty(entity.weight)" /> </QCard> <QCard class="vn-one"> @@ -199,35 +192,35 @@ async function changeState(value) { /> <VnLv :label="t('ticket.summary.shipped')" - :value="toDate(ticket.shipped)" + :value="toDate(entity.shipped)" /> <VnLv :label="t('ticket.summary.landed')" - :value="toDate(ticket.landed)" + :value="toDate(entity.landed)" /> - <VnLv :label="t('globals.packages')" :value="ticket.packages" /> - <VnLv :value="ticket.address.phone"> + <VnLv :label="t('globals.packages')" :value="entity.packages" /> + <VnLv :value="entity.address.phone"> <template #label> {{ t('ticket.summary.consigneePhone') }} - <VnLinkPhone :phone-number="ticket.address.phone" /> + <VnLinkPhone :phone-number="entity.address.phone" /> </template> </VnLv> - <VnLv :value="ticket.address.mobile"> + <VnLv :value="entity.address.mobile"> <template #label> {{ t('ticket.summary.consigneeMobile') }} - <VnLinkPhone :phone-number="ticket.address.mobile" /> + <VnLinkPhone :phone-number="entity.address.mobile" /> </template> </VnLv> - <VnLv :value="ticket.client.phone"> + <VnLv :value="entity.client.phone"> <template #label> {{ t('ticket.summary.clientPhone') }} - <VnLinkPhone :phone-number="ticket.client.phone" /> + <VnLinkPhone :phone-number="entity.client.phone" /> </template> </VnLv> - <VnLv :value="ticket.client.mobile"> + <VnLv :value="entity.client.mobile"> <template #label> {{ t('ticket.summary.clientMobile') }} - <VnLinkPhone :phone-number="ticket.client.mobile" /> + <VnLinkPhone :phone-number="entity.client.mobile" /> </template> </VnLv> <VnLv @@ -235,13 +228,13 @@ async function changeState(value) { :value="formattedAddress()" /> </QCard> - <QCard class="vn-one" v-if="ticket.notes.length"> + <QCard class="vn-one" v-if="entity.notes.length"> <VnTitle :url="ticketUrl + 'observation'" :text="t('ticket.pageTitles.notes')" /> <VnLv - v-for="note in ticket.notes" + v-for="note in entity.notes" :key="note.id" :label="note.observationType.description" :value="note.description" @@ -262,15 +255,15 @@ async function changeState(value) { <div class="bodyCard"> <VnLv :label="t('ticket.summary.subtotal')" - :value="toCurrency(ticket.totalWithoutVat)" + :value="toCurrency(entity.totalWithoutVat)" /> <VnLv :label="t('ticket.summary.vat')" - :value="toCurrency(ticket.totalWithVat - ticket.totalWithoutVat)" + :value="toCurrency(entity.totalWithVat - entity.totalWithoutVat)" /> <VnLv :label="t('ticket.summary.total')" - :value="toCurrency(ticket.totalWithVat)" + :value="toCurrency(entity.totalWithVat)" /> </div> </QCard> @@ -279,7 +272,7 @@ async function changeState(value) { :url="ticketUrl + 'sale'" :text="t('ticket.summary.saleLines')" /> - <QTable :rows="ticket.sales" style="text-align: center"> + <QTable :rows="entity.sales" style="text-align: center"> <template #body-cell="{ value }"> <QTd>{{ value }}</QTd> </template> @@ -383,7 +376,11 @@ async function changeState(value) { <QTd> <QBtn class="link" flat> {{ props.row.itemFk }} - <ItemDescriptorProxy :id="props.row.itemFk" /> + <ItemDescriptorProxy + :id="props.row.itemFk" + :sale-fk="props.row.id" + :warehouse-fk="ticket.warehouseFk" + /> </QBtn> </QTd> <QTd>{{ props.row.visible }}</QTd> @@ -399,7 +396,6 @@ async function changeState(value) { <FetchedTags class="fetched-tags" :item="props.row.item" - :max-length="5" ></FetchedTags> </QTd> <QTd>{{ props.row.price }} €</QTd> @@ -420,10 +416,10 @@ async function changeState(value) { </QCard> <QCard class="vn-max" - v-if="ticket.packagings.length > 0 || ticket.services.length > 0" + v-if="entity.packagings.length || entity.services.length" > <VnTitle :url="ticketUrl + 'package'" :text="t('globals.packages')" /> - <QTable :rows="ticket.packagings" flat> + <QTable :rows="entity.packagings" flat> <template #header="props"> <QTr :props="props"> <QTh auto-width>{{ t('ticket.summary.created') }}</QTh> @@ -443,7 +439,7 @@ async function changeState(value) { :url="ticketUrl + 'service'" :text="t('ticket.summary.service')" /> - <QTable :rows="ticket.services" flat> + <QTable :rows="entity.services" flat> <template #header="props"> <QTr :props="props"> <QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh> diff --git a/src/pages/Ticket/Card/TicketTracking.vue b/src/pages/Ticket/Card/TicketTracking.vue index 2ddb278fa..f7cbb14e9 100644 --- a/src/pages/Ticket/Card/TicketTracking.vue +++ b/src/pages/Ticket/Card/TicketTracking.vue @@ -83,6 +83,8 @@ const openCreateModal = () => createTrackingDialogRef.value.show(); :filter="paginateFilter" url="TicketTrackings" auto-load + order="created DESC" + :limit="0" > <template #body="{ rows }"> <QTable @@ -112,7 +114,13 @@ const openCreateModal = () => createTrackingDialogRef.value.show(); <TicketCreateTracking @on-request-created="paginateRef.fetch()" /> </QDialog> <QPageSticky :offset="[20, 20]"> - <QBtn @click="openCreateModal()" color="primary" fab icon="add" /> + <QBtn + @click="openCreateModal()" + color="primary" + fab + icon="add" + shortcut="+" + /> <QTooltip class="text-no-wrap"> {{ t('tracking.addState') }} </QTooltip> diff --git a/src/pages/Ticket/Card/TicketTransfer.vue b/src/pages/Ticket/Card/TicketTransfer.vue index 9a22c764c..1944b80f4 100644 --- a/src/pages/Ticket/Card/TicketTransfer.vue +++ b/src/pages/Ticket/Card/TicketTransfer.vue @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import VnInput from 'src/components/common/VnInput.vue'; +import TicketTransferForm from './TicketTransferForm.vue'; import { toDateFormat } from 'src/filters/date.js'; import axios from 'axios'; @@ -135,9 +136,7 @@ onMounted(() => (_transfer.value = $props.transfer)); :columns="destinationTicketColumns" :title="t('Destination ticket')" row-key="id" - :pagination="{ rowsPerPage: 0 }" class="full-width q-mt-md" - :no-data-label="t('globals.noResults')" > <template #body-cell-address="{ row }"> <QTd @click.stop> @@ -158,29 +157,11 @@ onMounted(() => (_transfer.value = $props.transfer)); </QTd> </template> + <template #no-data> + <TicketTransferForm v-bind="$props" /> + </template> <template #bottom> - <QForm class="q-mt-lg full-width"> - <VnInput - v-model.number="_transfer.ticketId" - :label="t('Transfer to ticket')" - :clearable="false" - > - <template #append> - <QBtn - icon="keyboard_arrow_right" - color="primary" - @click="transferSales(_transfer.ticketId)" - style="width: 30px" - /> - </template> - </VnInput> - <QBtn - :label="t('New ticket')" - color="primary" - class="full-width q-my-lg" - @click="transferSales()" - /> - </QForm> + <TicketTransferForm v-bind="$props" /> </template> </QTable> </QCard> diff --git a/src/pages/Ticket/Card/TicketTransferForm.vue b/src/pages/Ticket/Card/TicketTransferForm.vue new file mode 100644 index 000000000..9507429e6 --- /dev/null +++ b/src/pages/Ticket/Card/TicketTransferForm.vue @@ -0,0 +1,86 @@ +<script setup> +import { ref, onMounted } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRouter } from 'vue-router'; + +import VnInput from 'src/components/common/VnInput.vue'; + +import axios from 'axios'; + +const $props = defineProps({ + mana: { + type: Number, + default: null, + }, + newPrice: { + type: Number, + default: 0, + }, + transfer: { + type: Object, + default: () => {}, + }, + ticket: { + type: Object, + default: () => {}, + }, +}); + +const emit = defineEmits(['refreshData']); + +const router = useRouter(); +const { t } = useI18n(); + +const _transfer = ref(null); + +const transferSales = async (ticketId) => { + const params = { + ticketId: ticketId, + sales: $props.transfer.sales, + }; + + const { data } = await axios.post( + `tickets/${$props.ticket.id}/transferSales`, + params + ); + + if (data && data.id === $props.ticket.id) emit('refreshData'); + else router.push({ name: 'TicketSale', params: { id: data.id } }); +}; + +onMounted(() => (_transfer.value = $props.transfer)); +</script> + +<template> + {{ _transfer }} + <QForm class="q-mt-lg full-width"> + <VnInput + v-model.number="_transfer.ticketId" + :label="t('Transfer to ticket')" + :clearable="false" + > + <template #append> + <QBtn + icon="keyboard_arrow_right" + color="primary" + @click="transferSales(_transfer.ticketId)" + style="width: 30px" + /> + </template> + </VnInput> + <QBtn + :label="t('New ticket')" + color="primary" + class="full-width q-my-lg" + @click="transferSales()" + /> + </QForm> +</template> + +<i18n> +es: + Sales to transfer: Líneas a transferir + Destination ticket: Ticket destinatario + Transfer to ticket: Transferir a ticket + New ticket: Nuevo ticket +</i18n> diff --git a/src/pages/Ticket/Card/TicketVolume.vue b/src/pages/Ticket/Card/TicketVolume.vue index 93da31e53..68d2a1f73 100644 --- a/src/pages/Ticket/Card/TicketVolume.vue +++ b/src/pages/Ticket/Card/TicketVolume.vue @@ -145,7 +145,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <div class="column"> <span>{{ row.item.name }}</span> <span class="color-vn-label">{{ row.item.subName }}</span> - <FetchedTags :item="row.item" :max-length="6" /> + <FetchedTags :item="row.item" /> </div> </QTd> </template> diff --git a/src/pages/Ticket/TicketAdvance.vue b/src/pages/Ticket/TicketAdvance.vue index bf4000fdf..177b3a29b 100644 --- a/src/pages/Ticket/TicketAdvance.vue +++ b/src/pages/Ticket/TicketAdvance.vue @@ -133,6 +133,14 @@ const ticketColumns = computed(() => [ sortable: true, columnFilter: null, }, + { + label: t('advanceTickets.preparation'), + name: 'preparation', + field: 'preparation', + align: 'left', + sortable: true, + columnFilter: null, + }, { label: t('advanceTickets.liters'), name: 'liters', @@ -448,7 +456,7 @@ const handleCloseProgressDialog = () => { const handleCancelProgress = () => (cancelProgress.value = true); onMounted(async () => { - let today = Date.vnNew(); + let today = Date.vnNew().toISOString(); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); userParams.dateFuture = tomorrow; @@ -624,6 +632,7 @@ onMounted(async () => { </QIcon> </QTd> </template> + <template #body-cell-ticketId="{ row }"> <QTd> <QBtn flat class="link"> @@ -635,6 +644,7 @@ onMounted(async () => { <template #body-cell-state="{ row }"> <QTd> <QBadge + v-if="row.state" text-color="black" :color="row.classColor" class="q-ma-none" @@ -642,6 +652,7 @@ onMounted(async () => { > {{ row.state }} </QBadge> + <span v-else> {{ dashIfEmpty(row.state) }}</span> </QTd> </template> <template #body-cell-import="{ row }"> diff --git a/src/pages/Ticket/TicketAdvanceFilter.vue b/src/pages/Ticket/TicketAdvanceFilter.vue index c4548763a..209a1a307 100644 --- a/src/pages/Ticket/TicketAdvanceFilter.vue +++ b/src/pages/Ticket/TicketAdvanceFilter.vue @@ -70,7 +70,6 @@ onMounted(async () => await getItemPackingTypes()); v-model="params.dateFuture" :label="t('params.dateFuture')" is-outlined - @update:model-value="searchFn()" /> </QItemSection> </QItem> @@ -80,7 +79,6 @@ onMounted(async () => await getItemPackingTypes()); v-model="params.dateToAdvance" :label="t('params.dateToAdvance')" is-outlined - @update:model-value="searchFn()" /> </QItemSection> </QItem> @@ -121,10 +119,9 @@ onMounted(async () => await getItemPackingTypes()); <QItem> <QItemSection> <QCheckbox - :label="t('params.itemPackingTypes')" - v-model="params.itemPackingTypes" + :label="t('params.isFullMovable')" + v-model="params.isFullMovable" toggle-indeterminate - :false-value="null" @update:model-value="searchFn()" /> </QItemSection> @@ -157,7 +154,7 @@ en: dateToAdvance: Destination date futureIpt: Origin IPT ipt: Destination IPT - itemPackingTypes: 100% movable + isFullMovable: 100% movable warehouseFk: Warehouse es: Horizontal: Horizontal @@ -168,6 +165,6 @@ es: dateToAdvance: Fecha destino futureIpt: IPT Origen ipt: IPT destino - itemPackingTypes: 100% movible + isFullMovable: 100% movible warehouseFk: Almacén </i18n> diff --git a/src/pages/Ticket/TicketCreateDialog.vue b/src/pages/Ticket/TicketCreateDialog.vue new file mode 100644 index 000000000..1493adc53 --- /dev/null +++ b/src/pages/Ticket/TicketCreateDialog.vue @@ -0,0 +1,226 @@ +<script setup> +import { useRoute, useRouter } from 'vue-router'; +import { onBeforeMount, reactive, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useDialogPluginComponent } from 'quasar'; + +import FormModelPopup from 'components/FormModelPopup.vue'; +import FetchData from 'components/FetchData.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import VnInputDate from 'components/common/VnInputDate.vue'; + +import { useState } from 'composables/useState'; +import axios from 'axios'; + +const { t } = useI18n(); +const route = useRoute(); +const router = useRouter(); +const state = useState(); +const user = state.getUser(); +defineEmits(['confirm', ...useDialogPluginComponent.emits]); + +const initialFormState = reactive({ + clientId: Number(route.query?.clientFk) || null, + addressId: null, + agencyModeId: null, + warehouseId: user.value.warehouseFk, + landed: null, +}); +const clientOptions = ref([]); +const agenciesOptions = ref([]); +const addressesOptions = ref([]); +const warehousesOptions = ref([]); +const selectedClient = ref(null); + +onBeforeMount(async () => { + await onClientSelected(initialFormState); +}); + +const fetchClient = async (formData) => { + try { + const filter = { + include: { + relation: 'defaultAddress', + scope: { + fields: ['id', 'agencyModeFk'], + }, + }, + where: { id: formData.clientId }, + }; + const params = { filter: JSON.stringify(filter) }; + const { data } = await axios.get('Clients', { params }); + const [client] = data; + selectedClient.value = client; + } catch (err) { + console.error('Error fetching client'); + } +}; + +const fetchAddresses = async (formData) => { + try { + if (!formData.clientId) return; + + const filter = { + fields: ['nickname', 'street', 'city', 'id'], + where: { isActive: true }, + order: 'nickname ASC', + }; + const params = { filter: JSON.stringify(filter) }; + const { data } = await axios.get(`Clients/${formData.clientId}/addresses`, { + params, + }); + addressesOptions.value = data; + + const { defaultAddress } = selectedClient.value; + formData.addressId = defaultAddress.id; + } catch (err) { + console.error(`Error fetching addresses`, err); + return err.response; + } +}; + +const onClientSelected = async (formData) => { + await fetchClient(formData); + await fetchAddresses(formData); +}; + +const fetchAvailableAgencies = async (formData) => { + if (!formData.warehouseId || !formData.addressId || !formData.landed) return; + let params = { + warehouseFk: formData.warehouseId, + addressFk: formData.addressId, + landed: formData.landed, + }; + + const { data } = await axios.get('Agencies/getAgenciesWithWarehouse', { params }); + + agenciesOptions.value = data; + + const defaultAgency = agenciesOptions.value.find( + (agency) => + agency.agencyModeFk === selectedClient.value.defaultAddress.agencyModeFk + ); + + if (defaultAgency) formData.agencyModeId = defaultAgency.agencyModeFk; +}; + +const redirectToTicketList = (_, { id }) => { + router.push({ name: 'TicketSummary', params: { id } }); +}; +</script> + +<template> + <FetchData + url="Clients" + @on-fetch="(data) => (clientOptions = data)" + :filter="{ fields: ['id', 'name', 'defaultAddressFk'], order: 'id' }" + auto-load + /> + <FetchData + url="Warehouses" + @on-fetch="(data) => (warehousesOptions = data)" + order="name" + auto-load + /> + + <FormModelPopup + :title="t('globals.pageTitles.createTicket')" + url-create="Tickets/new" + model="ticket" + :form-initial-data="initialFormState" + @on-data-saved="redirectToTicketList" + > + <template #form-inputs="{ data }"> + <VnRow> + <div class="col"> + <VnSelect + :label="t('ticket.create.client')" + v-model="data.clientId" + :options="clientOptions" + option-value="id" + option-label="name" + hide-selected + @update:model-value="(client) => onClientSelected(data)" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt.name }} + </QItemLabel> + <QItemLabel caption> + {{ `#${scope.opt.id}` }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </div> + </VnRow> + <VnRow> + <div class="col"> + <VnSelect + :label="t('ticket.create.address')" + v-model="data.addressId" + :options="addressesOptions" + option-value="id" + option-label="nickname" + hide-selected + :disable="!data.clientId" + @update:model-value="() => fetchAvailableAgencies(data)" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt.nickname }} + </QItemLabel> + <QItemLabel caption> + {{ `${scope.opt.street}, ${scope.opt.city}` }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </div> + </VnRow> + <VnRow> + <div class="col"> + <VnInputDate + placeholder="dd-mm-aaa" + :label="t('ticket.create.landed')" + v-model="data.landed" + @update:model-value="() => fetchAvailableAgencies(data)" + /> + </div> + </VnRow> + <VnRow> + <div class="col"> + <VnSelect + :label="t('ticket.create.warehouse')" + v-model="data.warehouseId" + :options="warehousesOptions" + option-value="id" + option-label="name" + hide-selected + @update:model-value="() => fetchAvailableAgencies(data)" + /> + </div> + </VnRow> + <VnRow> + <div class="col"> + <VnSelect + :label="t('ticket.create.agency')" + v-model="data.agencyModeId" + :options="agenciesOptions" + option-value="agencyModeFk" + option-label="agencyMode" + hide-selected + :disable="!data.clientId || !data.landed || !data.warehouseId" + /> + </div> + </VnRow> + </template> + </FormModelPopup> +</template> diff --git a/src/pages/Ticket/TicketFuture.vue b/src/pages/Ticket/TicketFuture.vue index 2fec6dc18..2078d0595 100644 --- a/src/pages/Ticket/TicketFuture.vue +++ b/src/pages/Ticket/TicketFuture.vue @@ -8,6 +8,8 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; +import RightMenu from 'src/components/common/RightMenu.vue'; +import TicketFutureFilter from './TicketFutureFilter.vue'; import { dashIfEmpty, toCurrency } from 'src/filters'; import { useVnConfirm } from 'composables/useVnConfirm'; @@ -37,9 +39,9 @@ const exprBuilder = (param, value) => { return { liters: value }; case 'lines': return { lines: value }; - case 'ipt': + case 'iptColFilter': return { ipt: { like: `%${value}%` } }; - case 'futureIpt': + case 'futureIptColFilter': return { futureIpt: { like: `%${value}%` } }; case 'totalWithVat': return { totalWithVat: value }; @@ -47,8 +49,8 @@ const exprBuilder = (param, value) => { }; const userParams = reactive({ - futureDated: Date.vnNew(), - originDated: Date.vnNew(), + futureScopeDays: Date.vnNew().toISOString(), + originScopeDays: Date.vnNew().toISOString(), warehouseFk: user.value.warehouseFk, }); @@ -60,8 +62,8 @@ const arrayData = useArrayData('FutureTickets', { const { store } = arrayData; const params = reactive({ - futureDated: Date.vnNew(), - originDated: Date.vnNew(), + futureScopeDays: Date.vnNew(), + originScopeDays: Date.vnNew(), warehouseFk: user.value.warehouseFk, }); @@ -83,6 +85,8 @@ const getInputEvents = (col) => { }; }; +const tickets = computed(() => store.data); + const ticketColumns = computed(() => [ { label: t('futureTickets.problems'), @@ -121,7 +125,7 @@ const ticketColumns = computed(() => [ sortable: true, columnFilter: { component: VnSelect, - filterParamKey: 'ipt', + filterParamKey: 'iptColFilter', type: 'select', filterValue: null, event: getInputEvents, @@ -168,7 +172,7 @@ const ticketColumns = computed(() => [ label: t('futureTickets.availableLines'), name: 'lines', field: 'lines', - align: 'left', + align: 'center', sortable: true, columnFilter: { component: VnInput, @@ -214,7 +218,7 @@ const ticketColumns = computed(() => [ sortable: true, columnFilter: { component: VnSelect, - filterParamKey: 'futureIpt', + filterParamKey: 'futureIptColFilter', type: 'select', filterValue: null, event: getInputEvents, @@ -230,7 +234,7 @@ const ticketColumns = computed(() => [ { label: t('futureTickets.futureState'), name: 'futureState', - align: 'left', + align: 'right', sortable: true, columnFilter: null, format: (val) => dashIfEmpty(val), @@ -305,9 +309,14 @@ onMounted(async () => { </QBtn> </template> </VnSubToolbar> + <RightMenu> + <template #right-panel> + <TicketFutureFilter data-key="FutureTickets" /> + </template> + </RightMenu> <QPage class="column items-center q-pa-md"> <QTable - :rows="store.data" + :rows="tickets" :columns="ticketColumns" row-key="id" selection="multiple" @@ -449,7 +458,7 @@ onMounted(async () => { </QTd> </template> <template #body-cell-shipped="{ row }"> - <QTd> + <QTd class="shipped"> <QBadge text-color="black" :color="getDateQBadgeColor(row.shipped)" @@ -496,7 +505,7 @@ onMounted(async () => { </QTd> </template> <template #body-cell-futureShipped="{ row }"> - <QTd> + <QTd class="shipped"> <QBadge text-color="black" :color="getDateQBadgeColor(row.futureShipped)" @@ -523,6 +532,9 @@ onMounted(async () => { </template> <style scoped lang="scss"> +.shipped { + min-width: 132px; +} .vertical-separator { border-left: 4px solid white !important; } diff --git a/src/pages/Ticket/TicketFutureFilter.vue b/src/pages/Ticket/TicketFutureFilter.vue new file mode 100644 index 000000000..6345f62b3 --- /dev/null +++ b/src/pages/Ticket/TicketFutureFilter.vue @@ -0,0 +1,242 @@ +<script setup> +import { ref } from 'vue'; +import { useI18n } from 'vue-i18n'; + +import FetchData from 'components/FetchData.vue'; +import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import VnInputDate from 'src/components/common/VnInputDate.vue'; +import VnInput from 'src/components/common/VnInput.vue'; + +import axios from 'axios'; +import { onMounted } from 'vue'; + +const { t } = useI18n(); +const props = defineProps({ + dataKey: { + type: String, + required: true, + }, +}); + +const warehousesOptions = ref([]); +const itemPackingTypes = ref([]); +const stateOptions = ref([]); + +const getItemPackingTypes = async () => { + try { + const filter = { + where: { isActive: true }, + }; + const { data } = await axios.get('ItemPackingTypes', { + params: { filter: JSON.stringify(filter) }, + }); + itemPackingTypes.value = data.map((ipt) => ({ + description: t(ipt.description), + code: ipt.code, + })); + } catch (error) { + console.error(error); + } +}; + +const getGroupedStates = async () => { + try { + const { data } = await axios.get('AlertLevels'); + stateOptions.value = data.map((state) => ({ + id: state.id, + name: t(`futureTickets.${state.code}`), + code: state.code, + })); + } catch (error) { + console.error(error); + } +}; + +onMounted(async () => { + getItemPackingTypes(); + getGroupedStates(); +}); +</script> + +<template> + <FetchData + url="Warehouses" + @on-fetch="(data) => (warehousesOptions = data)" + auto-load + /> + <VnFilterPanel + :data-key="props.dataKey" + :hidden-tags="['search']" + :un-removable-params="['warehouseFk', 'originScopeDays ', 'futureScopeDays']" + > + <template #tags="{ tag, formatFn }"> + <div class="q-gutter-x-xs"> + <strong>{{ t(`params.${tag.label}`) }}: </strong> + <span>{{ formatFn(tag.value) }}</span> + </div> + </template> + <template #body="{ params, searchFn }"> + <QItem class="q-my-sm"> + <QItemSection> + <VnInputDate + v-model="params.originScopeDays" + :label="t('params.originScopeDays')" + is-outlined + /> + </QItemSection> + </QItem> + <QItem class="q-my-sm"> + <QItemSection> + <VnInputDate + v-model="params.futureScopeDays" + :label="t('params.futureScopeDays')" + is-outlined + /> + </QItemSection> + </QItem> + <QItem class="q-my-sm"> + <QItemSection> + <VnInput + :label="t('params.litersMax')" + v-model="params.litersMax" + is-outlined + /> + </QItemSection> + </QItem> + <QItem class="q-my-sm"> + <QItemSection> + <VnInput + :label="t('params.linesMax')" + v-model="params.linesMax" + is-outlined + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('params.ipt')" + v-model="params.ipt" + :options="itemPackingTypes" + option-value="code" + option-label="description" + :info="t('iptInfo')" + @update:model-value="searchFn()" + dense + outlined + rounded + > + </VnSelect> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('params.futureIpt')" + v-model="params.futureIpt" + :options="itemPackingTypes" + option-value="code" + option-label="description" + :info="t('iptInfo')" + @update:model-value="searchFn()" + dense + outlined + rounded + > + </VnSelect> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('params.state')" + v-model="params.state" + :options="stateOptions" + option-value="id" + option-label="name" + @update:model-value="searchFn()" + dense + outlined + rounded + > + </VnSelect> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('params.futureState')" + v-model="params.futureState" + :options="stateOptions" + option-value="id" + option-label="name" + @update:model-value="searchFn()" + dense + outlined + rounded + > + </VnSelect> + </QItemSection> + </QItem> + + <QItem> + <QItemSection> + <QCheckbox + :label="t('params.problems')" + v-model="params.problems" + :toggle-indeterminate="false" + @update:model-value="searchFn()" + /> + </QItemSection> + </QItem> + <QItem> + <QItemSection> + <VnSelect + :label="t('params.warehouseFk')" + v-model="params.warehouseFk" + :options="warehousesOptions" + option-value="id" + option-label="name" + @update:model-value="searchFn()" + dense + outlined + rounded + > + </VnSelect> + </QItemSection> + </QItem> + </template> + </VnFilterPanel> +</template> + +<i18n> +en: + iptInfo: IPT + params: + originScopeDays: Origin date + futureScopeDays: Destination date + futureIpt: Destination IPT + ipt: Origin IPT + warehouseFk: Warehouse + litersMax: Max liters + linesMax: Max lines + state: Origin grouped state + futureState: Destination grouped state + problems: With problems +es: + Horizontal: Horizontal + Vertical: Vertical + iptInfo: Encajado + params: + originScopeDays: Fecha origen + futureScopeDays: Fecha destino + futureIpt: IPT destino + ipt: IPT Origen + warehouseFk: Almacén + litersMax: Litros máx. + linesMax: Líneas máx. + state: Estado agrupado origen + futureState: Estado agrupado destino + problems: Con problemas +</i18n> diff --git a/src/pages/Ticket/TicketList.vue b/src/pages/Ticket/TicketList.vue index cbd102317..bd0bda6e3 100644 --- a/src/pages/Ticket/TicketList.vue +++ b/src/pages/Ticket/TicketList.vue @@ -1,6 +1,7 @@ <script setup> import axios from 'axios'; import { computed, ref, onMounted } from 'vue'; +import { useRoute } from 'vue-router'; import { useStateStore } from 'stores/useStateStore'; import { useI18n } from 'vue-i18n'; import { toDate, toCurrency } from 'src/filters/index'; @@ -14,6 +15,7 @@ import VnRow from 'src/components/ui/VnRow.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import TicketFilter from './TicketFilter.vue'; +const route = useRoute(); const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const tableRef = ref(); @@ -23,12 +25,23 @@ const agenciesOptions = ref([]); const selectedClient = ref(); const stateStore = useStateStore(); const from = Date.vnNew(); +from.setHours(0, 0, 0, 0); const to = Date.vnNew(); +to.setHours(23, 59, 0, 0); to.setDate(to.getDate() + 1); const userParams = { - from: from.toISOString(), - to: to.toISOString(), + from: null, + to: null, +}; +// Método para inicializar las variables desde la query string +const initializeFromQuery = () => { + const query = route.query.table ? JSON.parse(route.query.table) : {}; + + // Asigna los valores a las variables correspondientes + from.value = query.from || from.toISOString(); + to.value = query.to || to.toISOString(); + Object.assign(userParams, { from, to }); }; const columns = computed(() => [ @@ -200,23 +213,26 @@ const getColor = (row) => { return row?.classColor ? `bg-${row.classColor}` : 'bg-orange'; }; -onMounted(() => (stateStore.rightDrawer = true)); +onMounted(() => { + initializeFromQuery(); + stateStore.rightDrawer = true; +}); </script> <template> <VnSearchbar - data-key="Tickets" + data-key="Ticket" :label="t('Search ticket')" :info="t('You can search by ticket id or alias')" /> <RightMenu> <template #right-panel> - <TicketFilter data-key="Tickets" /> + <TicketFilter data-key="Ticket" /> </template> </RightMenu> <VnTable ref="tableRef" - data-key="Tickets" + data-key="Ticket" url="Tickets/filter" :create="{ urlCreate: 'Tickets/new', @@ -225,7 +241,7 @@ onMounted(() => (stateStore.rightDrawer = true)); formInitialData: {}, }" default-mode="table" - order="id DESC" + :order="['shippedDate DESC', 'shippedHour ASC', 'zoneLanding ASC', 'id']" :columns="columns" :user-params="userParams" :right-search="false" diff --git a/src/pages/Ticket/locale/en.yml b/src/pages/Ticket/locale/en.yml index 10a8e1fa4..305228669 100644 --- a/src/pages/Ticket/locale/en.yml +++ b/src/pages/Ticket/locale/en.yml @@ -93,6 +93,11 @@ futureTickets: moveTicketSuccess: Tickets moved successfully! searchInfo: Search future tickets by date futureTicket: Future tickets + FREE: Free + ON_PREVIOUS: ON_PREVIOUS + ON_PREPARATION: On preparation + PACKED: Packed + DELIVERED: Delivered expedition: id: Expedition item: Item diff --git a/src/pages/Ticket/locale/es.yml b/src/pages/Ticket/locale/es.yml index a80692bfe..30c5550be 100644 --- a/src/pages/Ticket/locale/es.yml +++ b/src/pages/Ticket/locale/es.yml @@ -105,7 +105,7 @@ advanceTickets: futureLines: Líneas futureImport: Importe advanceTickets: Adelantar tickets con negativos - advanceTicketTitle: Advance tickets + advanceTicketTitle: Adelantar tickets advanceTitleSubtitle: '¿Desea adelantar {selectedTickets} tickets?' noDeliveryZone: No hay una zona de reparto disponible para la fecha de envío seleccionada moveTicketSuccess: 'Tickets movidos correctamente {ticketsNumber}' @@ -140,6 +140,11 @@ futureTickets: moveTicketSuccess: Tickets movidos correctamente searchInfo: Buscar tickets por fecha futureTicket: Tickets a futuro + FREE: Libre + ON_PREVIOUS: ON_PREVIOUS + ON_PREPARATION: En preparación + PACKED: Encajado + DELIVERED: Servido ticketSale: id: Id visible: Visible diff --git a/src/pages/Travel/Card/TravelDescriptorMenuItems.vue b/src/pages/Travel/Card/TravelDescriptorMenuItems.vue index bd2561211..47b6f0aa6 100644 --- a/src/pages/Travel/Card/TravelDescriptorMenuItems.vue +++ b/src/pages/Travel/Card/TravelDescriptorMenuItems.vue @@ -8,7 +8,7 @@ import VnConfirm from 'components/ui/VnConfirm.vue'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; -import { useRole } from 'src/composables/useRole'; +import { useAcl } from 'src/composables/useAcl'; const $props = defineProps({ travel: { @@ -21,7 +21,6 @@ const { t } = useI18n(); const router = useRouter(); const quasar = useQuasar(); const { notify } = useNotify(); -const role = useRole(); const redirectToCreateView = (queryParams) => { router.push({ name: 'TravelCreate', query: { travelData: queryParams } }); @@ -42,9 +41,7 @@ const cloneTravelWithEntries = async () => { } }; -const isBuyer = computed(() => { - return role.hasAny(['buyer']); -}); +const canDelete = computed(() => useAcl().hasAny('Travel','*','WRITE')); const openDeleteEntryDialog = (id) => { quasar @@ -81,7 +78,7 @@ const deleteTravel = async (id) => { </QItemSection> </QItem> <QItem - v-if="isBuyer && travel.totalEntries === 0" + v-if="canDelete && travel.totalEntries === 0" v-ripple clickable @click="openDeleteEntryDialog(travel.id)" diff --git a/src/pages/Travel/Card/TravelThermographs.vue b/src/pages/Travel/Card/TravelThermographs.vue index 9f224154c..6d83581ee 100644 --- a/src/pages/Travel/Card/TravelThermographs.vue +++ b/src/pages/Travel/Card/TravelThermographs.vue @@ -196,6 +196,7 @@ const removeThermograph = async (id) => { icon="add" color="primary" @click="redirectToThermographForm('create')" + shortcut="+" /> <QTooltip class="text-no-wrap"> {{ t('Add thermograph') }} diff --git a/src/pages/Travel/ExtraCommunityFilter.vue b/src/pages/Travel/ExtraCommunityFilter.vue index c51151451..a8bbde75b 100644 --- a/src/pages/Travel/ExtraCommunityFilter.vue +++ b/src/pages/Travel/ExtraCommunityFilter.vue @@ -20,7 +20,6 @@ const props = defineProps({ const warehousesOptions = ref([]); const continentsOptions = ref([]); const agenciesOptions = ref([]); -const suppliersOptions = ref([]); const warehousesByContinent = ref({}); const add = (paramsObj, key) => { @@ -76,12 +75,6 @@ warehouses(); @on-fetch="(data) => (agenciesOptions = data)" auto-load /> - <FetchData - url="Suppliers" - @on-fetch="(data) => (suppliersOptions = data)" - auto-load - /> - <VnFilterPanel :data-key="props.dataKey" :search-button="true"> <template #tags="{ tag, formatFn }"> <div class="q-gutter-x-xs"> @@ -220,7 +213,7 @@ warehouses(); <VnSelect :label="t('globals.pageTitles.supplier')" v-model="params.cargoSupplierFk" - :options="suppliersOptions" + url="Suppliers" option-value="id" option-label="name" hide-selected diff --git a/src/pages/Travel/TravelList.vue b/src/pages/Travel/TravelList.vue index 8989e485c..c7ad908f9 100644 --- a/src/pages/Travel/TravelList.vue +++ b/src/pages/Travel/TravelList.vue @@ -10,6 +10,7 @@ import { computed } from 'vue'; import TravelSummary from './Card/TravelSummary.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue'; import { toDate } from 'src/filters'; +import { getDateQBadgeColor } from 'src/composables/getDateQBadgeColor.js'; const { viewSummary } = useSummaryDialog(); const router = useRouter(); const { t } = useI18n(); @@ -46,14 +47,12 @@ const columns = computed(() => [ name: 'id', label: t('travel.travelList.tableVisibleColumns.id'), isId: true, - field: 'id', cardVisible: true, }, { align: 'left', name: 'ref', label: t('travel.travelList.tableVisibleColumns.ref'), - field: 'ref', component: 'input', columnField: { component: null, @@ -65,7 +64,6 @@ const columns = computed(() => [ align: 'left', name: 'agencyModeFk', label: t('travel.travelList.tableVisibleColumns.agency'), - field: 'agencyModeFk', component: 'select', attrs: { url: 'agencyModes', @@ -78,37 +76,10 @@ const columns = computed(() => [ cardVisible: true, create: true, }, - { - align: 'left', - name: 'shipped', - label: t('travel.travelList.tableVisibleColumns.shipped'), - field: 'shipped', - component: 'date', - columnField: { - component: null, - }, - cardVisible: true, - create: true, - format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.shipped)), - }, - { - align: 'left', - name: 'landed', - label: t('travel.travelList.tableVisibleColumns.landed'), - field: 'landed', - component: 'date', - columnField: { - component: null, - }, - cardVisible: true, - create: true, - format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.landed)), - }, { align: 'left', name: 'warehouseInFk', label: t('travel.travelList.tableVisibleColumns.warehouseIn'), - field: 'warehouseInFk', component: 'select', attrs: { url: 'warehouses', @@ -123,11 +94,28 @@ const columns = computed(() => [ cardVisible: true, create: true, }, + { + align: 'left', + name: 'shipped', + label: t('travel.travelList.tableVisibleColumns.shipped'), + component: 'date', + columnField: { + component: null, + }, + cardVisible: true, + create: true, + format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.shipped)), + }, + { + align: 'left', + name: 'shipmentHour', + label: t('travel.travelList.tableVisibleColumns.shipHour'), + cardVisible: true, + }, { align: 'left', name: 'warehouseOutFk', label: t('travel.travelList.tableVisibleColumns.warehouseOut'), - field: 'warehouseOutFk', component: 'select', attrs: { url: 'warehouses', @@ -140,12 +128,30 @@ const columns = computed(() => [ cardVisible: true, create: true, }, + { + align: 'left', + name: 'landed', + label: t('travel.travelList.tableVisibleColumns.landed'), + component: 'date', + columnField: { + component: null, + }, + cardVisible: true, + create: true, + format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.landed)), + }, + { + align: 'left', + name: 'landingHour', + label: t('travel.travelList.tableVisibleColumns.landHour'), + cardVisible: true, + }, { align: 'left', name: 'totalEntries', label: t('travel.travelList.tableVisibleColumns.totalEntries'), - field: 'totalEntries', component: 'input', + toolTip: t('travel.travelList.tableVisibleColumns.totalEntriesTooltip'), columnField: { component: null, }, @@ -165,13 +171,15 @@ const columns = computed(() => [ }, { title: t('Add entry'), - icon: 'contact_support', + icon: 'vn:ticket', action: redirectCreateEntryView, + isPrimary: true, }, { - title: t('View Summary'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row.id, TravelSummary), + isPrimary: true, }, ], }, @@ -202,12 +210,43 @@ const columns = computed(() => [ redirect="travel" :is-editable="false" :use-model="true" - /> + > + <template #column-shipped="{ row }"> + <QBadge + text-color="black" + v-if="getDateQBadgeColor(row.shipped)" + :color="getDateQBadgeColor(row.shipped)" + > + {{ toDate(row.shipped) }} + </QBadge> + <span v-else>{{ toDate(row.shipped) }}</span> + <QIcon + name="flight_takeoff" + size="sm" + :class="{ 'is-active': row.isDelivered }" + /> + </template> + <template #column-landed="{ row }"> + <QBadge + text-color="black" + v-if="getDateQBadgeColor(row.landed)" + :color="getDateQBadgeColor(row.landed)" + > + {{ toDate(row.landed) }} + </QBadge> + <span v-else>{{ toDate(row.landed) }}</span> + <QIcon + name="flight_land" + size="sm" + :class="{ 'is-active': row.isReceived }" + /> + </template> + </VnTable> </template> <i18n> en: - addEntry: Add entry + Add entry: Add entry searchByIdOrReference: Search by ID or reference es: @@ -215,4 +254,12 @@ es: searchByIdOrReference: Buscar por ID o por referencia You can search by travel id or name: Buscar por envio por id o nombre Search travel: Buscar envio + Clone: Clonar + Add entry: Añadir Entrada </i18n> + +<style lang="scss" scoped> +.is-active { + color: #c8e484; +} +</style> diff --git a/src/pages/Wagon/Type/WagonCreateTray.vue b/src/pages/Wagon/Type/WagonCreateTray.vue new file mode 100644 index 000000000..304fa83ac --- /dev/null +++ b/src/pages/Wagon/Type/WagonCreateTray.vue @@ -0,0 +1,116 @@ +<script setup> +import { ref, watch } from 'vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import FormPopup from 'src/components/FormPopup.vue'; +import VnRow from 'src/components/ui/VnRow.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import FetchData from 'src/components/FetchData.vue'; +import { useI18n } from 'vue-i18n'; +import axios from 'axios'; + +const $props = defineProps({ + entityId: { + type: Number, + required: true, + }, + height: { + type: Number, + default: 0, + }, + color: { + type: Array, + default: () => [], + }, +}); +const entityId = ref($props.entityId); +const selectedTrayColor = ref(); +const trayHeight = ref(); +const wagonColors = ref([]); +const wagonColorTranslated = ref(); +const heights = ref(); +const existingTrayHeight = ref($props.height); +const { t } = useI18n(); + +watch(wagonColors, () => { + wagonColorTranslated.value = wagonColors.value.map((color) => { + return { ...color, name: t(`colors.${color.name}`) }; + }); +}); +const emit = defineEmits(['onSubmit']); + +async function getTrays() { + const { data } = await axios.get('WagonTypeTrays', undefined, { + filter: { wagonTypeFk: entityId.value }, + }); + existingTrayHeight.value = data.filter((item) => item.wagonTypeFk == entityId.value); + heights.value = existingTrayHeight.value.map((item) => item.height); +} + +function onSubmit() { + emit('onSubmit', { + wagonTypeFk: entityId.value, + wagonTypeColorFk: selectedTrayColor.value, + height: trayHeight.value, + }); +} +getTrays(); +</script> + +<template> + <FetchData + url="WagonTypeColors" + @on-fetch="(data) => (wagonColors = data)" + auto-load + /> + <FormPopup + ref="createTrayFormDialogRef" + @on-submit="onSubmit()" + :title="t('Add new tray')" + :entity-id="selectedEntityId" + > + <template #form-inputs> + <VnRow class="row q-gutter-md q-mb-md"> + <VnSelect + v-model="selectedTrayColor" + :options="wagonColorTranslated" + option-label="name" + option-value="id" + id="id" + :label="t('Select a tray color')" + :required="true" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps" clickable> + <QItemSection> + <QItemLabel>{{ scope.opt.name }}</QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnInput v-model="trayHeight" :label="t('Height')" type="number" /> + </VnRow> + </template> + </FormPopup> +</template> + +<i18n> + en: + Select a tray: Select a tray + colors: + white: White + red: Red + green: Green + blue: Blue + es: + Select a tray color: Seleccione un color + Add new tray: Añadir nueva bandeja + Height: Altura + The minimum height between trays is 50cm: La altura mínima entre bandejas es de 50cm + The maximum height of the wagon is 200cm: La altura máxima del vagón es de 200cm + A tray with the same height already exists, try with a different height: Ya existe una bandeja con la misma altura, prueba con una diferente + colors: + white: Blanco + red: Rojo + green: Verde + blue: Azul +</i18n> diff --git a/src/pages/Wagon/Type/WagonTypeCreate.vue b/src/pages/Wagon/Type/WagonTypeCreate.vue deleted file mode 100644 index bc9c1a40c..000000000 --- a/src/pages/Wagon/Type/WagonTypeCreate.vue +++ /dev/null @@ -1,433 +0,0 @@ -<script setup> -import { computed, ref, onMounted, onUpdated } from 'vue'; -import { useRoute, useRouter } from 'vue-router'; -import { useQuasar } from 'quasar'; - -import VnInput from 'src/components/common/VnInput.vue'; - -import { useI18n } from 'vue-i18n'; -import axios from 'axios'; - -onMounted(() => fetch()); -onUpdated(() => fetch()); - -const { t } = useI18n(); -const route = useRoute(); -const quasar = useQuasar(); -const router = useRouter(); -const $props = defineProps({ - id: { - type: Number, - required: false, - default: null, - }, -}); -const entityId = computed(() => $props.id || route.params.id); - -const wagon = ref([]); -const divisible = ref(false); -const name = ref(''); -const colorPickerActive = ref(false); -let originalData = { trays: [] }; -let wagonConfig; -let wagonTypeColors; -let currentTrayColorPicked; - -async function fetch() { - try { - await axios.get('WagonConfigs').then(async (res) => { - if (res.data) { - wagonConfig = res.data[0]; - } - }); - - await axios.get(`WagonTypeColors`).then(async (res) => { - if (res.data) { - wagonTypeColors = res.data; - if (!entityId.value) - wagon.value.push({ - id: 0, - position: 0, - color: { ...wagonTypeColors[0] }, - action: 'add', - }); - else { - await axios - .get(`WagonTypeTrays`, { - params: { filter: { where: { typeFk: entityId.value } } }, - }) - .then(async (res) => { - if (res.data) { - for (let i = 0; i < res.data.length; i++) { - const tray = res.data[i]; - wagon.value.push({ - id: res.data.length - i - 1, - position: tray.height, - color: { - ...wagonTypeColors.find((color) => { - return color.id === tray.colorFk; - }), - }, - action: tray.height == 0 ? 'add' : 'delete', - }); - } - wagon.value.forEach((value) => { - originalData.trays.push({ ...value }); - }); - } - }); - } - } - }); - - if (entityId.value) { - await axios.get(`WagonTypes/${entityId.value}`).then((res) => { - if (res.data) { - originalData.name = name.value = res.data.name; - originalData.divisible = divisible.value = res.data.divisible; - } - }); - } - } catch (e) { - // - } -} - -function addTray() { - if ( - wagon.value.find((tray) => { - return tray.position == null; - }) - ) { - quasar.notify({ - message: t('wagon.warnings.uncompleteTrays'), - type: 'warning', - }); - return; - } - - if (wagon.value.length < wagonConfig.maxTrays) { - wagon.value.unshift({ - id: wagon.value.length, - position: null, - color: { ...wagonTypeColors[0] }, - action: 'delete', - }); - } else { - quasar.notify({ - message: t('wagon.warnings.maxTrays'), - type: 'warning', - }); - } -} - -function deleteTray(trayToDelete) { - wagon.value = wagon.value.filter((tray) => tray.id !== trayToDelete.id); - reorderIds(); -} - -function reorderIds() { - for (let index = wagon.value.length - 1; index >= 0; index--) { - wagon.value[index].id = index; - } -} - -async function onSubmit() { - try { - const path = entityId.value - ? 'WagonTypes/editWagonType' - : 'WagonTypes/createWagonType'; - - const params = { - id: entityId.value, - name: name.value, - divisible: divisible.value, - trays: wagon.value, - }; - - await axios.patch(path, params).then((res) => { - if (res.status == 204) router.push({ path: `/wagon/type/list` }); - }); - } catch (error) { - // - } -} - -function onReset() { - name.value = entityId.value ? originalData.name : null; - divisible.value = entityId.value ? originalData.divisible : false; - wagon.value = entityId.value - ? [...originalData.trays] - : [ - { - id: 0, - position: 0, - color: { ...wagonTypeColors[0] }, - action: 'add', - }, - ]; -} - -function doAction(tray) { - if (tray.action == 'add') { - addTray(); - } else { - deleteTray(tray); - } -} - -function showColorPicker(tray) { - colorPickerActive.value = true; - currentTrayColorPicked = wagon.value.findIndex((val) => { - return val.id === tray.id; - }); -} - -function updateColor(newColor) { - wagon.value[currentTrayColorPicked].color = { - ...wagonTypeColors.find((color) => { - return color.rgb === newColor; - }), - }; -} - -function onPositionBlur(tray) { - if (tray.position) { - if (tray.position == '' || tray.position < 0) { - tray.position = null; - return; - } - tray.position = parseInt(tray.position); - wagon.value.sort((a, b) => b.position - a.position); - reorderIds(); - for (let index = wagon.value.length - 1; index > 0; index--) { - if (exceedMaxHeight(index - 1)) continue; - if ( - wagon.value[index - 1].position - wagon.value[index].position >= - wagonConfig.minHeightBetweenTrays - ) { - continue; - } else { - wagon.value[index - 1].position += - wagonConfig.minHeightBetweenTrays - - (wagon.value[index - 1].position - wagon.value[index].position); - - quasar.notify({ - message: - t('wagon.warnings.minHeightBetweenTrays') + - wagonConfig.minHeightBetweenTrays + - ' cm', - type: 'warning', - }); - - exceedMaxHeight(index - 1); - } - } - } -} - -function exceedMaxHeight(pos) { - if (wagon.value[pos].position > wagonConfig.maxWagonHeight) { - wagon.value.splice(pos, 1); - quasar.notify({ - message: - t('wagon.warnings.maxWagonHeight') + wagonConfig.maxWagonHeight + ' cm', - type: 'warning', - }); - return true; - } - return false; -} -</script> - -<template> - <QPage class="q-pa-sm q-mx-xl"> - <QForm @submit="onSubmit()" @reset="onReset()" class="q-pa-sm"> - <QCard class="q-pa-md"> - <VnInput - filled - v-model="name" - :label="t('wagon.type.name')" - :rules="[(val) => !!val || t('wagon.warnings.nameNotEmpty')]" - /> - <QCheckbox class="q-mb-sm" v-model="divisible" label="Divisible" /> - <div class="wagon-tray q-mx-lg" v-for="tray in wagon" :key="tray.id"> - <div class="position"> - <QInput - autofocus - filled - type="number" - :class="{ isVisible: tray.action == 'add' }" - v-model="tray.position" - @blur="onPositionBlur(tray)" - > - <QTooltip :delay="2000"> - {{ - t('wagon.warnings.minHeightBetweenTrays') + - wagonConfig.minHeightBetweenTrays + - ' cm' - }} - <QSpace /> - {{ - t('wagon.warnings.maxWagonHeight') + - wagonConfig.maxWagonHeight + - ' cm' - }} - </QTooltip> - </QInput> - </div> - <div class="shelving"> - <div class="shelving-half"> - <div class="shelving-up"></div> - <div - class="shelving-down" - :style="{ backgroundColor: tray.color.rgb }" - @click="showColorPicker(tray)" - ></div> - </div> - <div - class="shelving-divisible" - :class="{ isVisible: !divisible }" - ></div> - <div class="shelving-half"> - <div class="shelving-up"></div> - <div - class="shelving-down" - :style="{ backgroundColor: tray.color.rgb }" - @click="showColorPicker(tray)" - ></div> - </div> - </div> - <div class="action-button"> - <QBtn - flat - round - color="primary" - :icon="tray.action" - @click="doAction(tray)" - /> - </div> - </div> - <div class="q-mb-sm wheels"> - <QIcon color="grey-6" name="trip_origin" size="xl" /> - <QIcon color="grey-6" name="trip_origin" size="xl" /> - </div> - <QDialog - v-model="colorPickerActive" - position="right" - :no-backdrop-dismiss="false" - > - <QCard> - <QCardSection> - <div class="text-h6">{{ t('wagon.type.trayColor') }}</div> - </QCardSection> - <QCardSection class="row items-center no-wrap"> - <QColor - flat - v-model="wagon[currentTrayColorPicked].color.rgb" - no-header - no-footer - default-view="palette" - :palette=" - wagonTypeColors.map((color) => { - return color.rgb; - }) - " - @change="updateColor($event)" - /> - <QBtn flat round icon="close" v-close-popup /> - </QCardSection> - </QCard> - </QDialog> - </QCard> - <div class="q-mt-md"> - <QBtn :label="t('wagon.type.submit')" type="submit" color="primary" /> - <QBtn - :label="t('wagon.type.reset')" - type="reset" - color="primary" - flat - class="q-ml-sm" - /> - </div> - </QForm> - </QPage> -</template> - -<style lang="scss" scoped> -.q-page { - display: flex; - justify-content: center; - align-items: flex-start; -} - -.q-form { - width: 70%; -} - -.q-dialog { - .q-card { - width: 100%; - } -} - -.wheels { - margin-left: 5%; - display: flex; - justify-content: space-around; -} - -.wagon-tray { - display: flex; - height: 6rem; - - .position { - width: 20%; - border-right: 1rem solid gray; - display: flex; - align-items: flex-end; - justify-content: flex-end; - padding-right: 1rem; - } - - .shelving { - display: flex; - width: 75%; - - .shelving-half { - width: 50%; - height: 100%; - - .shelving-up { - height: 80%; - width: 100%; - } - - .shelving-down { - height: 20%; - width: 100%; - } - } - - .shelving-divisible { - width: 1%; - height: 100%; - border-left: 0.5rem dashed grey; - border-right: 0.5rem dashed grey; - } - } - - .action-button { - width: 10%; - border-left: 1rem solid gray; - display: flex; - align-items: flex-end; - justify-content: flex-start; - padding-left: 1rem; - } - - .isVisible { - display: none; - } -} -</style> diff --git a/src/pages/Wagon/Type/WagonTypeEdit.vue b/src/pages/Wagon/Type/WagonTypeEdit.vue new file mode 100644 index 000000000..eb8205d72 --- /dev/null +++ b/src/pages/Wagon/Type/WagonTypeEdit.vue @@ -0,0 +1,293 @@ +<script setup> +import { computed, ref, watch } from 'vue'; +import { useRoute } from 'vue-router'; +import { useQuasar } from 'quasar'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; +import FormModel from 'src/components/FormModel.vue'; +import { useI18n } from 'vue-i18n'; +import axios from 'axios'; +import VnPaginate from 'components/ui/VnPaginate.vue'; +import WagonCreateTray from './WagonCreateTray.vue'; + +const { t } = useI18n(); +const { notify } = useQuasar(); +const route = useRoute(); +const entityId = computed(() => route.params.id); +const wagonTrays = ref([]); +const createTrayFormDialogRef = ref(); +const selectedEntityId = ref(); + +async function loadTrays() { + try { + const res = await axios.get('WagonTypeTrays'); + const filteredTrays = res.data.filter( + (tray) => tray.wagonTypeFk === entityId.value + ); + wagonTrays.value = filteredTrays; + return; + } catch (err) { + console.error('Error loading trays:', err); + } +} + +async function addTray(newTray) { + const res = await axios.post(`WagonTypeTrays`, newTray); + wagonTrays.value.push(res.data); + notify({ + message: t(`Tray added successfully`), + type: 'positive', + }); +} + +async function deleteTray(trayToDelete) { + await axios.delete(`WagonTypeTrays/${trayToDelete.id}`); + const index = wagonTrays.value.findIndex((tray) => tray.id === trayToDelete.id); + if (index !== -1) { + wagonTrays.value.splice(index, 1); + } + notify({ + message: t('Tray deleted successfully'), + type: 'positive', + }); +} + +const filter = { + where: { wagonTypeFk: entityId.value }, + include: { + relation: 'color', + scope: { + fields: ['rgb'], + }, + }, +}; + +const formFilter = { where: { wagonTypeFk: entityId.value } }; + +const showCreateTrayForm = (id) => { + selectedEntityId.value = id; + createTrayFormDialogRef.value.show(); +}; + +watch( + () => wagonTrays.value, + async (newVal, oldVal) => { + if (newVal.length !== oldVal.length) { + await loadTrays(); + } + }, + { deep: true } +); +</script> + +<template> + <VnSubToolbar /> + <QPage class="q-pa-sm q-mx-xl"> + <FormModel + :url="`WagonTypes/ ${entityId}`" + :url-update="`WagonTypes/ ${entityId}`" + :filter="formFilter" + model="WagonType" + auto-load + > + <template #form="{ data }"> + <QCard class="q-pa-md"> + <VnInput + filled + v-model="data.name" + :label="t('wagon.type.name')" + :rules="[(val) => !!val || t('wagon.warnings.nameNotEmpty')]" + /> + <QCheckbox + class="q-mb-sm" + v-model="data.divisible" + label="Divisible" + /> + + <VnPaginate + data-key="wagonTypeTray" + url="WagonTypeTrays" + order="id DESC" + :filter="filter" + auto-load + ref="vnPaginateRef" + v-bind="$attrs" + :key="wagonTrays.length" + > + <template #body="{ rows }"> + <div v-for="row in rows" :key="row.id"> + <div class="shelving"></div> + <div class="action-button"> + <div class="wagon-tray q-mx-lg"> + <div class="position"> + <VnInput + borderless + type="number" + disable + class="input-tray q-mb-sm" + :label="t('Height') + ': '" + v-model="row.height" + /> + </div> + <div class="shelving"> + <div class="shelving-half"> + <div class="shelving-up"></div> + <div + class="shelving-down" + :style="{ + backgroundColor: row.color.rgb, + }" + ></div> + </div> + <div + class="shelving-divisible" + :class="{ isVisible: !data.divisible }" + ></div> + <div class="shelving-half"> + <div class="shelving-up"></div> + <div + class="shelving-down" + :style="{ + backgroundColor: row.color.rgb, + }" + ></div> + </div> + </div> + <div class="action-button"> + <QBtn + v-if="row.height === 0" + flat + round + color="primary" + :icon="'add'" + class="btn-tray" + @click="showCreateTrayForm(entityId)" + /> + <QBtn + v-else + flat + round + color="primary" + :icon="'delete'" + class="btn-tray" + @click="deleteTray(row)" + /> + </div> + </div> + </div> + </div> + </template> + </VnPaginate> + <div class="q-mb-sm wheels"> + <QIcon color="grey-6" name="trip_origin" size="xl" /> + <QIcon color="grey-6" name="trip_origin" size="xl" /> + </div> + </QCard> + <QDialog ref="createTrayFormDialogRef"> + <WagonCreateTray :entity-id="entityId" @on-submit="addTray($event)" /> + </QDialog> + </template> + </FormModel> + </QPage> +</template> + +<style lang="scss" scoped> +.q-page { + display: flex; + justify-content: center; + align-items: flex-start; +} + +.q-form { + width: 70%; +} + +.q-dialog { + .q-card { + width: 100%; + } +} + +.wheels { + margin-left: 5%; + display: flex; + justify-content: space-around; +} + +.wagon-tray { + display: flex; + height: 6rem; + + .position { + width: 26%; + border-right: 1rem solid gray; + display: flex; + align-items: flex-start; + justify-content: flex-start; + padding-right: 2rem; + padding-left: 3rem; + } + + .shelving { + display: flex; + width: 54%; + + .shelving-half { + width: 100%; + height: 100%; + + .shelving-up { + height: 80%; + width: 100%; + } + + .shelving-down { + height: 20%; + width: 100%; + } + } + + .shelving-divisible { + width: 1%; + height: 100%; + border-left: 0.5rem dashed grey; + border-right: 0.5rem dashed grey; + } + } + + .action-button { + width: 20%; + border-left: 1rem solid gray; + align-items: baseline; + padding-left: 3rem; + } + + .isVisible { + display: none; + } +} +.btn-tray { + margin-right: 100%; + margin-top: 100%; +} +.input-tray { + margin-top: 100%; + margin-left: 40%; +} +</style> + +<i18n> + en: + tray: Tray + wagonColor: Wagon color + Select a tray: Select a tray + es: + tray: Bandeja + wagonColor: Color de la bandeja + Select a tray: Seleccione una bandeja + Create new Wagon type: Crear nuevo tipo de vagón + Add new tray: Añadir nueva bandeja + Height: Altura + Tray added successfully: Bandeja añadida correctamente + Tray deleted successfully: Bandeja eliminada correctamente +</i18n> diff --git a/src/pages/Wagon/Type/WagonTypeList.vue b/src/pages/Wagon/Type/WagonTypeList.vue index 3ecca1ea3..7615dea02 100644 --- a/src/pages/Wagon/Type/WagonTypeList.vue +++ b/src/pages/Wagon/Type/WagonTypeList.vue @@ -1,4 +1,5 @@ <script setup> +import { ref, computed } from 'vue'; import axios from 'axios'; import { useQuasar } from 'quasar'; import VnPaginate from 'src/components/ui/VnPaginate.vue'; @@ -6,36 +7,40 @@ import { useArrayData } from 'src/composables/useArrayData'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import CardList from 'components/ui/CardList.vue'; +import FormModelPopup from 'src/components/FormModelPopup.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnRow from 'src/components/ui/VnRow.vue'; const quasar = useQuasar(); const arrayData = useArrayData('WagonTypeList'); const store = arrayData.store; -const router = useRouter(); +const dialog = ref(); +const { push } = useRouter(); const { t } = useI18n(); +const paginate = ref(); -function navigate(id) { - router.push({ path: `/wagon/type/${id}/edit` }); +const initialData = computed(() => { + return { + name: null, + }; +}); + +function reloadData() { + initialData.value.name = null; + paginate.value.fetch(); } -function create() { - router.push({ path: `/wagon/type/create` }); +function navigate(id, name) { + push({ path: `/wagon/type/${id}/edit`, query: { name } }); } async function remove(row) { - try { - const id = row.id; - await axios - .delete(`WagonTypes/deleteWagonType`, { params: { id } }) - .then(async () => { - quasar.notify({ - message: t('wagon.type.removeItem'), - type: 'positive', - }); - store.data.splice(store.data.indexOf(row), 1); - }); - } catch (error) { - // - } + await axios.delete(`WagonTypes/${row.id}`); + quasar.notify({ + message: t('wagon.type.removeItem'), + type: 'positive', + }); + store.data.splice(store.data.indexOf(row), 1); } </script> @@ -43,8 +48,9 @@ async function remove(row) { <QPage class="column items-center q-pa-md"> <div class="vn-card-list"> <VnPaginate + ref="paginate" data-key="WagonTypeList" - url="/WagonTypes" + url="WagonTypes" order="id DESC" auto-load > @@ -54,12 +60,19 @@ async function remove(row) { :key="row.id" :title="(row.name || '').toString()" :id="row.id" - @click="navigate(row.id)" + @click="navigate(row.id, row.name)" > + <template #list-items> + <QCheckbox + :label="t('Divisble')" + :model-value="row.divisible" + disable + /> + </template> <template #actions> <QBtn :label="t('components.smartCard.openCard')" - @click.stop="navigate(row.id)" + @click.stop="navigate(row.id, row.name)" outline /> <QBtn @@ -73,8 +86,42 @@ async function remove(row) { </template> </VnPaginate> </div> - <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn @click="create" fab icon="add" color="primary" /> + <QPageSticky :offset="[18, 18]"> + <QBtn @click.stop="dialog.show()" color="primary" fab icon="add" shortcut="+"> + <QDialog ref="dialog"> + <FormModelPopup + :title="t('Create new Wagon type')" + url-create="WagonTypes" + model="WagonType" + :form-initial-data="initialData" + @on-data-saved="reloadData()" + auto-load + > + <template #form-inputs="{ data }"> + <VnRow class="row q-gutter-md q-mb-md"> + <VnInput + filled + v-model="data.name" + :label="t('Name')" + :rules="[(val) => !!val || t('nameNotEmpty')]" + /> + </VnRow> + </template> + </FormModelPopup> + </QDialog> + </QBtn> + <QTooltip> + {{ t('globals.new') }} + </QTooltip> </QPageSticky> </QPage> </template> + +<i18n> +en: + nameNotEmpty: The name cannot be empty +es: + Create new Wagon type: Crear nuevo tipo de vagón + Name: Nombre + nameNotEmpty: El nombre no puede estar vacío +</i18n> diff --git a/src/pages/Wagon/WagonList.vue b/src/pages/Wagon/WagonList.vue index a8b6728c3..c4824b861 100644 --- a/src/pages/Wagon/WagonList.vue +++ b/src/pages/Wagon/WagonList.vue @@ -94,7 +94,7 @@ async function remove(row) { </VnPaginate> </div> <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn @click="create" fab icon="add" color="primary" /> + <QBtn @click="create" fab icon="add" color="primary" shortcut="+" /> </QPageSticky> </QPage> </template> diff --git a/src/pages/Worker/Card/WorkerBasicData.vue b/src/pages/Worker/Card/WorkerBasicData.vue index 82203fc2a..d131fea3e 100644 --- a/src/pages/Worker/Card/WorkerBasicData.vue +++ b/src/pages/Worker/Card/WorkerBasicData.vue @@ -12,9 +12,12 @@ import VnSelect from 'src/components/common/VnSelect.vue'; const route = useRoute(); const { t } = useI18n(); -const workersOptions = ref([]); -const countriesOptions = ref([]); -const educationLevelsOptions = ref([]); +const educationLevels = ref([]); +const countries = ref([]); +const maritalStatus = [ + { code: 'M', name: t('Married') }, + { code: 'S', name: t('Single') }, +]; const workerFilter = { include: [ @@ -29,44 +32,21 @@ const workerFilter = { { relation: 'department', scope: { include: { relation: 'department' } } }, ], }; -const workersFilter = { - fields: ['id', 'nickname'], - order: 'nickname ASC', - limit: 30, -}; -const countriesFilter = { - fields: ['id', 'name', 'code'], - order: 'name ASC', - limit: 30, -}; -const educationLevelsFilter = { fields: ['id', 'name'], order: 'name ASC', limit: 30 }; - -const maritalStatus = [ - { code: 'M', name: t('Married') }, - { code: 'S', name: t('Single') }, -]; </script> <template> <FetchData - :filter="workersFilter" - @on-fetch="(data) => (workersOptions = data)" - auto-load - url="Workers/search" - /> - <FetchData - :filter="countriesFilter" - @on-fetch="(data) => (countriesOptions = data)" - auto-load - url="Countries" - /> - <FetchData - :filter="educationLevelsFilter" - @on-fetch="(data) => (educationLevelsOptions = data)" + :filter="{ fields: ['id', 'name'], order: 'name ASC' }" + @on-fetch="(data) => (educationLevels = data)" auto-load url="EducationLevels" /> - + <FetchData + url="Countries" + :filter="{ fields: ['id', 'name'], order: 'name ASC' }" + @on-fetch="(data) => (countries = data)" + auto-load + /> <FormModel :filter="workerFilter" :url="`Workers/${route.params.id}`" @@ -90,7 +70,7 @@ const maritalStatus = [ <VnRow> <VnSelect :label="t('Boss')" - :options="workersOptions" + url="Workers/search" hide-selected option-label="nickname" option-value="id" @@ -121,7 +101,7 @@ const maritalStatus = [ <VnRow> <VnSelect :label="t('Origin country')" - :options="countriesOptions" + :options="countries" hide-selected option-label="name" option-value="id" @@ -129,7 +109,7 @@ const maritalStatus = [ /> <VnSelect :label="t('Education level')" - :options="educationLevelsOptions" + :options="educationLevels" hide-selected option-label="name" option-value="id" diff --git a/src/pages/Worker/Card/WorkerCalendarItem.vue b/src/pages/Worker/Card/WorkerCalendarItem.vue index 91e118708..05a013d6b 100644 --- a/src/pages/Worker/Card/WorkerCalendarItem.vue +++ b/src/pages/Worker/Card/WorkerCalendarItem.vue @@ -152,7 +152,7 @@ const getEventAttrs = (timestamp) => { if (isFestive) { attrs.class = '--festive'; - attrs.label = event.absenceId ?? timestamp.day; + attrs.label = timestamp.day; } else attrs.class = `--${type}`; return attrs; diff --git a/src/pages/Worker/Card/WorkerCard.vue b/src/pages/Worker/Card/WorkerCard.vue index 0abcdcafd..5f9fa0f8e 100644 --- a/src/pages/Worker/Card/WorkerCard.vue +++ b/src/pages/Worker/Card/WorkerCard.vue @@ -15,5 +15,6 @@ import WorkerFilter from '../WorkerFilter.vue'; label: 'Search worker', info: 'You can search by worker id or name', }" + :redirect-on-error="true" /> </template> diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index 8fbb23ef8..be30537df 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -1,15 +1,15 @@ <script setup> -import { computed, ref, watch } from 'vue'; +import { computed, ref } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import WorkerChangePasswordForm from 'src/pages/Worker/Card/WorkerChangePasswordForm.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import { useState } from 'src/composables/useState'; import axios from 'axios'; import VnImg from 'src/components/ui/VnImg.vue'; +import EditPictureForm from 'components/EditPictureForm.vue'; const $props = defineProps({ id: { @@ -18,6 +18,7 @@ const $props = defineProps({ default: null, }, }); +const image = ref(null); const route = useRoute(); const { t } = useI18n(); @@ -25,6 +26,10 @@ const state = useState(); const user = state.getUser(); const changePasswordFormDialog = ref(null); const cardDescriptorRef = ref(null); +const showEditPhotoForm = ref(false); +const toggleEditPictureForm = () => { + showEditPhotoForm.value = !showEditPhotoForm.value; +}; const entityId = computed(() => { return $props.id || route.params.id; @@ -32,49 +37,6 @@ const entityId = computed(() => { const worker = ref(); const workerExcluded = ref(false); -const filter = { - include: [ - { - relation: 'user', - scope: { - fields: ['email', 'name', 'nickname'], - }, - }, - { - relation: 'department', - scope: { - include: [ - { - relation: 'department', - }, - ], - }, - }, - { - relation: 'sip', - }, - ], -}; - -const sip = ref(null); -watch( - () => [worker.value?.sip?.extension, state.get('extension')], - ([newWorkerSip, newStateExtension], [oldWorkerSip, oldStateExtension]) => { - if (newStateExtension !== oldStateExtension || sip.value === oldStateExtension) { - sip.value = newStateExtension; - } else if (newWorkerSip !== oldWorkerSip && sip.value !== newStateExtension) { - sip.value = newWorkerSip; - } - } -); - -const data = ref(useCardDescription()); -const setData = (entity) => { - if (!entity) return; - data.value = useCardDescription(entity.user?.nickname, entity.id); -}; - -const openChangePasswordForm = () => changePasswordFormDialog.value.show(); const getIsExcluded = async () => { try { @@ -99,7 +61,9 @@ const handleExcluded = async () => { workerExcluded.value = !workerExcluded.value; }; - +const handlePhotoUpdated = (evt = false) => { + image.value.reload(evt); +}; const refetch = async () => await cardDescriptorRef.value.getData(); </script> <template> @@ -107,14 +71,12 @@ const refetch = async () => await cardDescriptorRef.value.getData(); ref="cardDescriptorRef" module="Worker" data-key="workerData" - :url="`Workers/${entityId}`" - :filter="filter" - :title="data.title" - :subtitle="data.subtitle" + url="Workers/descriptor" + :filter="{ where: { id: entityId } }" + title="user.nickname" @on-fetch=" (data) => { worker = data; - setData(data); getIsExcluded(); } " @@ -133,7 +95,7 @@ const refetch = async () => await cardDescriptorRef.value.getData(); v-if="!worker.user.emailVerified && user.id != worker.id" v-ripple clickable - @click="openChangePasswordForm()" + @click="$refs.changePasswordFormDialog.show()" > <QItemSection> {{ t('Change password') }} @@ -144,30 +106,52 @@ const refetch = async () => await cardDescriptorRef.value.getData(); </QItem> </template> <template #before> - <VnImg - :id="parseInt(entityId)" - collection="user" - resolution="520x520" - class="photo" - > - <template #error> - <div - class="absolute-full picture text-center q-pa-md flex flex-center" - > - <div> - <div class="text-grey-5" style="opacity: 0.4; font-size: 5vh"> - <QIcon name="vn:claims" /> - </div> - <div class="text-grey-5" style="opacity: 0.4"> - {{ t('worker.imageNotFound') }} + <div class="relative-position"> + <VnImg + ref="image" + :id="parseInt(entityId)" + collection="user" + resolution="520x520" + class="photo" + > + <template #error> + <div + class="absolute-full picture text-center q-pa-md flex flex-center" + > + <div> + <div + class="text-grey-5" + style="opacity: 0.4; font-size: 5vh" + > + <QIcon name="vn:claims" /> + </div> + <div class="text-grey-5" style="opacity: 0.4"> + {{ t('worker.imageNotFound') }} + </div> </div> </div> - </div> - </template> - </VnImg> + </template> </VnImg + ><QBtn + color="primary" + size="lg" + round + class="edit-photo-btn" + @click="toggleEditPictureForm()" + > + <QIcon name="edit" size="sm" /> + <QDialog ref="editPhotoFormDialog" v-model="showEditPhotoForm"> + <EditPictureForm + collection="user" + :id="entityId" + @close-form="toggleEditPictureForm()" + @on-photo-uploaded="handlePhotoUpdated" + /> + </QDialog> + </QBtn> + </div> </template> <template #body="{ entity }"> - <VnLv :label="t('worker.card.name')" :value="entity.user?.nickname" /> + <VnLv :label="t('worker.card.user')" :value="entity.user?.name" /> <VnLv :label="t('worker.card.email')" :value="entity.user?.email" copy /> <VnLv :label="t('worker.list.department')" @@ -179,10 +163,10 @@ const refetch = async () => await cardDescriptorRef.value.getData(); <VnLinkPhone :phone-number="entity.phone" /> </template> </VnLv> - <VnLv :value="sip"> + <VnLv :value="worker?.sip?.extension"> <template #label> {{ t('worker.summary.sipExtension') }} - <VnLinkPhone v-if="sip" :phone-number="sip" /> + <VnLinkPhone :phone-number="worker?.sip?.extension" /> </template> </VnLv> </template> diff --git a/src/pages/Worker/Card/WorkerDms.vue b/src/pages/Worker/Card/WorkerDms.vue index a4f7ef5a9..e2b62bc4f 100644 --- a/src/pages/Worker/Card/WorkerDms.vue +++ b/src/pages/Worker/Card/WorkerDms.vue @@ -10,6 +10,6 @@ const route = useRoute(); delete-model="WorkerDms" download-model="WorkerDms" default-dms-code="hhrrData" - filter="worker" + filter="wd.workerFk" /> </template> diff --git a/src/pages/Worker/Card/WorkerFormation.vue b/src/pages/Worker/Card/WorkerFormation.vue index 829326898..71c5cba5d 100644 --- a/src/pages/Worker/Card/WorkerFormation.vue +++ b/src/pages/Worker/Card/WorkerFormation.vue @@ -108,7 +108,7 @@ const columns = computed(() => [ :filter="courseFilter" :create="{ urlCreate: 'trainingCourses', - title: 'Create trainingCourse', + title: t('Create training course'), onDataSaved: () => tableRef.reload(), formInitialData: { workerFk: entityId, @@ -122,3 +122,7 @@ const columns = computed(() => [ :use-model="true" /> </template> +<i18n> +es: + Create training course: Crear curso de formación +</i18n> diff --git a/src/pages/Worker/Card/WorkerLocker.vue b/src/pages/Worker/Card/WorkerLocker.vue index f19fc8ae6..4a19e472c 100644 --- a/src/pages/Worker/Card/WorkerLocker.vue +++ b/src/pages/Worker/Card/WorkerLocker.vue @@ -3,13 +3,13 @@ import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; -import { useRole } from 'src/composables/useRole'; +import { useAcl } from 'src/composables/useAcl'; import FormModel from 'components/FormModel.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import { useArrayData } from 'src/composables/useArrayData'; import FetchData from 'components/FetchData.vue'; -const { hasAny } = useRole(); +const { hasAny } = useAcl(); const { t } = useI18n(); const fetchData = ref(); const originaLockerId = ref(); @@ -57,7 +57,11 @@ const init = async (data) => { option-label="code" option-value="id" hide-selected - :readonly="!hasAny(['productionBoss', 'hr'])" + :readonly=" + !hasAny([ + { model: 'Worker', props: '__get__locker', accessType: 'READ' }, + ]) + " /> </template> </FormModel> diff --git a/src/pages/Worker/Card/WorkerMedical.vue b/src/pages/Worker/Card/WorkerMedical.vue new file mode 100644 index 000000000..6bca4ae85 --- /dev/null +++ b/src/pages/Worker/Card/WorkerMedical.vue @@ -0,0 +1,91 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; +import VnTable from 'components/VnTable/VnTable.vue'; +const tableRef = ref(); +const { t } = useI18n(); +const route = useRoute(); +const entityId = computed(() => route.params.id); + +const columns = [ + { + align: 'left', + name: 'date', + label: t('worker.medical.tableVisibleColumns.date'), + create: true, + component: 'date', + }, + { + align: 'left', + name: 'time', + label: t('worker.medical.tableVisibleColumns.time'), + create: true, + component: 'time', + attrs: { + timeOnly: true, + }, + }, + { + align: 'left', + name: 'centerFk', + label: t('worker.medical.tableVisibleColumns.center'), + create: true, + component: 'select', + attrs: { + url: 'medicalCenters', + fields: ['id', 'name'], + }, + }, + { + align: 'left', + name: 'invoice', + label: t('worker.medical.tableVisibleColumns.invoice'), + create: true, + component: 'input', + }, + { + align: 'left', + name: 'amount', + label: t('worker.medical.tableVisibleColumns.amount'), + create: true, + component: 'input', + }, + { + align: 'left', + name: 'isFit', + label: t('worker.medical.tableVisibleColumns.isFit'), + create: true, + component: 'checkbox', + }, + { + align: 'left', + name: 'remark', + label: t('worker.medical.tableVisibleColumns.remark'), + create: true, + component: 'input', + }, +]; +</script> +<template> + <VnTable + ref="tableRef" + data-key="WorkerMedical" + :url="`Workers/${entityId}/medicalReview`" + save-url="MedicalReviews/crud" + :create="{ + urlCreate: 'medicalReviews', + title: t('Create medicalReview'), + onDataSaved: () => tableRef.reload(), + formInitialData: { + workerFk: entityId, + }, + }" + order="date DESC" + :columns="columns" + auto-load + :right-search="false" + :is-editable="true" + :use-model="true" + /> +</template> diff --git a/src/pages/Worker/Card/WorkerPda.vue b/src/pages/Worker/Card/WorkerPda.vue index 4e0abc20c..a53aac270 100644 --- a/src/pages/Worker/Card/WorkerPda.vue +++ b/src/pages/Worker/Card/WorkerPda.vue @@ -116,7 +116,7 @@ function reloadData() { </template> </VnPaginate> <QPageSticky :offset="[18, 18]"> - <QBtn @click.stop="dialog.show()" color="primary" fab icon="add"> + <QBtn @click.stop="dialog.show()" color="primary" fab icon="add" shortcut="+"> <QDialog ref="dialog"> <FormModelPopup :title="t('Add new device')" diff --git a/src/pages/Worker/Card/WorkerSummary.vue b/src/pages/Worker/Card/WorkerSummary.vue index 5c6261b94..0e7bfd863 100644 --- a/src/pages/Worker/Card/WorkerSummary.vue +++ b/src/pages/Worker/Card/WorkerSummary.vue @@ -10,7 +10,6 @@ import CardSummary from 'components/ui/CardSummary.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnTitle from 'src/components/common/VnTitle.vue'; import RoleDescriptorProxy from 'src/pages/Account/Role/Card/RoleDescriptorProxy.vue'; -import VnRow from 'src/components/ui/VnRow.vue'; import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; const route = useRoute(); @@ -35,13 +34,22 @@ const filter = { { relation: 'user', scope: { - fields: ['email', 'name', 'nickname', 'roleFk'], - include: { - relation: 'role', - scope: { - fields: ['name'], + fields: ['name', 'nickname', 'roleFk'], + + include: [ + { + relation: 'role', + scope: { + fields: ['name'], + }, }, - }, + { + relation: 'emailUser', + scope: { + fields: ['email'], + }, + }, + ], }, }, { @@ -93,7 +101,6 @@ const filter = { /> </template> </VnLv> - <VnLv :label="t('worker.list.email')" :value="worker.user.email" copy /> <VnLv :label="t('worker.summary.boss')" link> <template #value> <VnUserLink @@ -139,34 +146,34 @@ const filter = { /> <VnLv :label="t('worker.summary.fi')" :value="worker.fi" /> <VnLv :label="t('worker.summary.birth')" :value="toDate(worker.birth)" /> - <VnRow class="q-mt-sm" wrap> - <VnLv - :label="t('worker.summary.isFreelance')" - :value="worker.isFreelance" - /> - <VnLv - :label="t('worker.summary.isSsDiscounted')" - :value="worker.isSsDiscounted" - /> - <VnLv - :label="t('worker.summary.hasMachineryAuthorized')" - :value="worker.hasMachineryAuthorized" - /> - <VnLv - :label="t('worker.summary.isDisable')" - :value="worker.isDisable" - /> - </VnRow> + <VnLv + :label="t('worker.summary.isFreelance')" + :value="worker.isFreelance" + /> + <VnLv + :label="t('worker.summary.isSsDiscounted')" + :value="worker.isSsDiscounted" + /> + <VnLv + :label="t('worker.summary.hasMachineryAuthorized')" + :value="worker.hasMachineryAuthorized" + /> + <VnLv :label="t('worker.summary.isDisable')" :value="worker.isDisable" /> </QCard> <QCard class="vn-one"> <VnTitle :text="t('worker.summary.userData')" /> - <VnLv :label="t('worker.summary.userId')" :value="worker.user.id" /> - <VnLv :label="t('worker.card.name')" :value="worker.user.nickname" /> + <VnLv :label="t('worker.summary.userId')" :value="worker?.user?.id" /> + <VnLv :label="t('worker.card.name')" :value="worker?.user?.nickname" /> + <VnLv + :label="t('worker.list.email')" + :value="worker.user?.emailUser?.email" + copy + /> <VnLv :label="t('worker.summary.role')"> <template #value> <span class="link"> - {{ worker.user.role.name }} - <RoleDescriptorProxy :id="worker.user.role.id" /> + {{ worker?.user?.role?.name }} + <RoleDescriptorProxy :id="worker?.user?.role?.id" /> </span> </template> </VnLv> diff --git a/src/pages/Worker/Card/WorkerTimeControl.vue b/src/pages/Worker/Card/WorkerTimeControl.vue index 87ff44e63..abf60a078 100644 --- a/src/pages/Worker/Card/WorkerTimeControl.vue +++ b/src/pages/Worker/Card/WorkerTimeControl.vue @@ -13,6 +13,7 @@ import WorkerTimeControlCalendar from 'pages/Worker/Card/WorkerTimeControlCalend import useNotify from 'src/composables/useNotify.js'; import axios from 'axios'; import { useRole } from 'src/composables/useRole'; +import { useAcl } from 'src/composables/useAcl'; import { useWeekdayStore } from 'src/stores/useWeekdayStore'; import { useStateStore } from 'stores/useStateStore'; import { useState } from 'src/composables/useState'; @@ -26,7 +27,6 @@ import { date } from 'quasar'; const route = useRoute(); const { t, locale } = useI18n(); const { notify } = useNotify(); -const { hasAny } = useRole(); const _state = useState(); const user = _state.getUser(); const stateStore = useStateStore(); @@ -34,6 +34,10 @@ const weekdayStore = useWeekdayStore(); const weekDays = ref([]); const { openConfirmationModal } = useVnConfirm(); const { getWeekOfYear } = date; +const defaultDate = computed(() => { + const timestamp = route.query.timestamp; + return timestamp ? new Date(timestamp * 1000) : Date.vnNew(); +}); const workerTimeFormDialogRef = ref(null); const workerTimeReasonFormDialogRef = ref(null); @@ -56,15 +60,17 @@ const workerTimeFormProps = reactive({ // Array utilizado por QCalendar para seleccionar un rango de fechas const selectedCalendarDates = ref([]); // Date formateada para bindear al componente QDate -const selectedDateFormatted = ref(toDateString(Date.vnNew())); +const selectedDateFormatted = ref(toDateString(defaultDate.value)); const arrayData = useArrayData('workerData'); const worker = computed(() => arrayData.store?.data); -const isHr = computed(() => hasAny(['hr'])); +const isHr = computed(() => useRole().hasAny(['hr'])); -const isHimSelf = computed(() => user.value.id === Number(route.params.id)); +const canSend = computed(() => useAcl().hasAny('WorkerTimeControl', 'sendMail', 'WRITE')); + +const isHimself = computed(() => user.value.id === Number(route.params.id)); const columns = computed(() => { return weekdayStore.getLocales?.map((day, index) => { @@ -423,7 +429,7 @@ onBeforeMount(() => { }); onMounted(async () => { - await setDate(Date.vnNew()); + await setDate(defaultDate.value); await getMailStates(selectedDate.value); stateStore.rightDrawer = true; }); @@ -443,7 +449,7 @@ onMounted(async () => { <div> <QBtnGroup push class="q-gutter-x-sm" flat> <QBtn - v-if="isHimSelf && state" + v-if="isHimself && state" :label="t('Satisfied')" color="primary" type="submit" @@ -451,7 +457,7 @@ onMounted(async () => { @click="isSatisfied()" /> <QBtn - v-if="isHimSelf && state" + v-if="isHimself && state" :label="t('Not satisfied')" color="primary" type="submit" @@ -462,14 +468,14 @@ onMounted(async () => { </QBtnGroup> <QBtnGroup push class="q-gutter-x-sm q-ml-none" flat> <QBtn - v-if="reason && state && (isHimSelf || isHr)" + v-if="reason && state && (isHimself || isHr)" :label="t('Reason')" color="primary" type="submit" @click="showReasonForm()" /> <QBtn - v-if="isHr && state !== 'CONFIRMED' && canResend" + v-if="canSend && state !== 'CONFIRMED' && canResend" :label="state ? t('Resend') : t('globals.send')" color="primary" type="submit" @@ -547,9 +553,12 @@ onMounted(async () => { <QTd v-for="(day, index) in props.cols" :key="index" - style="padding: 20px 16px !important" + :style="{ + padding: '20px 16px !important', + 'vertical-align': 'baseline', + }" > - <div class="full-height full-width column items-center"> + <div class="full-width column items-center"> <WorkerTimeHourChip v-for="(hour, ind) in day.dayData?.hours" :key="ind" @@ -596,7 +605,7 @@ onMounted(async () => { <WorkerTimeReasonForm @on-submit="isUnsatisfied($event)" :reason="reason" - :is-him-self="isHimSelf" + :is-himself="isHimself" /> </QDialog> </QPage> @@ -622,6 +631,9 @@ onMounted(async () => { margin-bottom: 0px; } } +:deep(.q-td) { + min-width: 170px; +} </style> <i18n> diff --git a/src/pages/Worker/Card/WorkerTimeReasonForm.vue b/src/pages/Worker/Card/WorkerTimeReasonForm.vue index 5c1ab9118..23bdba15e 100644 --- a/src/pages/Worker/Card/WorkerTimeReasonForm.vue +++ b/src/pages/Worker/Card/WorkerTimeReasonForm.vue @@ -9,7 +9,7 @@ const $props = defineProps({ type: String, default: '', }, - isHimSelf: { + isHimself: { type: Boolean, default: false, }, @@ -40,7 +40,7 @@ const closeForm = () => { v-model="reasonFormData" type="textarea" autogrow - :disable="!isHimSelf" + :disable="!isHimself" /> </template> </FormPopup> diff --git a/src/pages/Worker/WorkerCreate.vue b/src/pages/Worker/WorkerCreate.vue index 5f96c136d..b51209879 100644 --- a/src/pages/Worker/WorkerCreate.vue +++ b/src/pages/Worker/WorkerCreate.vue @@ -2,7 +2,6 @@ import { onBeforeMount, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; -import { useUserConfig } from 'src/composables/useUserConfig'; import VnRow from 'components/ui/VnRow.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; @@ -14,15 +13,25 @@ import FetchData from 'components/FetchData.vue'; 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'; const { t } = useI18n(); +const user = useState().getUser(); const companiesOptions = ref([]); -const workersOptions = ref([]); const payMethodsOptions = ref([]); const bankEntitiesOptions = ref([]); -const formData = ref({ isFreelance: false }); -const defaultPayMethod = ref(0); +const formData = ref({ companyFk: user.value.companyFk, isFreelance: false }); +const defaultPayMethod = ref(); + +onBeforeMount(async () => { + defaultPayMethod.value = ( + await axios.get('WorkerConfigs/findOne', { + params: { field: ['payMethodFk'] }, + }) + ).data.payMethodFk; + formData.value.payMethodFk = defaultPayMethod.value; +}); function handleLocation(data, location) { const { town, code, provinceFk, countryFk } = location ?? {}; @@ -32,16 +41,32 @@ function handleLocation(data, location) { data.countryFk = countryFk; } -onBeforeMount(async () => { - const userInfo = await useUserConfig().fetch(); - formData.value.companyFk = userInfo.companyFk; +function generateCodeUser(worker) { + if (!worker.firstName || !worker.lastNames) return; - const { data } = await axios.get('WorkerConfigs/findOne', { - params: { field: ['payMethodFk'] }, - }); - defaultPayMethod.value = data.payMethodFk; - formData.value.payMethodFk = defaultPayMethod.value; -}); + const totalName = worker.firstName.concat(' ' + worker.lastNames).toLowerCase(); + const totalNameArray = totalName.split(' '); + let newCode = ''; + + for (let part of totalNameArray) newCode += part.charAt(0); + + worker.code = newCode.toUpperCase().slice(0, 3); + worker.name = totalNameArray[0] + newCode.slice(1); + + if (!worker.companyFk) worker.companyFk = user.companyFk; +} + +async function autofillBic(worker) { + if (!worker || !worker.iban) return; + + let bankEntityId = parseInt(worker.iban.substr(4, 4)); + let filter = { where: { id: bankEntityId } }; + + const { data } = await axios.get(`BankEntities`, { params: { filter } }); + const hasData = data && data[0]; + if (hasData) worker.bankEntityFk = data[0].id; + else if (!hasData) worker.bankEntityFk = undefined; +} </script> <template> <FetchData @@ -49,11 +74,6 @@ onBeforeMount(async () => { @on-fetch="(data) => (companiesOptions = data)" auto-load /> - <FetchData - url="Workers/search" - @on-fetch="(data) => (workersOptions = data)" - auto-load - /> <FetchData url="Paymethods" @on-fetch="(data) => (payMethodsOptions = data)" @@ -93,11 +113,13 @@ onBeforeMount(async () => { v-model="data.firstName" :label="t('worker.create.name')" :rules="validate('Worker.firstName')" + @update:model-value="generateCodeUser(data)" /> <VnInput v-model="data.lastNames" :label="t('worker.create.lastName')" :rules="validate('Worker.lastNames')" + @update:model-value="generateCodeUser(data)" /> <VnInput v-model="data.code" @@ -130,7 +152,7 @@ onBeforeMount(async () => { <VnSelect :label="t('worker.create.boss')" v-model="data.bossFk" - :options="workersOptions" + url="Workers/search" option-value="id" option-label="name" hide-selected @@ -203,6 +225,7 @@ onBeforeMount(async () => { :label="t('worker.create.iban')" :rules="validate('Worker.iban')" :disable="formData.isFreelance" + @update:model-value="autofillBic(data)" > <template #append> <QIcon name="info" class="cursor-info"> @@ -220,6 +243,8 @@ onBeforeMount(async () => { :roles-allowed-to-create="['salesAssistant', 'hr']" :rules="validate('Worker.bankEntity')" :disable="formData.isFreelance" + @update:model-value="autofillBic(data)" + :filter-options="['bic', 'name']" > <template #form> <CreateBankEntityForm diff --git a/src/pages/Worker/WorkerFilter.vue b/src/pages/Worker/WorkerFilter.vue index 0853791ef..765241341 100644 --- a/src/pages/Worker/WorkerFilter.vue +++ b/src/pages/Worker/WorkerFilter.vue @@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n'; import FetchData from 'components/FetchData.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnInput from 'src/components/common/VnInput.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; const { t } = useI18n(); const props = defineProps({ @@ -26,7 +27,7 @@ const departments = ref(); <span>{{ formatFn(tag.value) }}</span> </div> </template> - <template #body="{ params, searchFn }"> + <template #body="{ params }"> <QItem> <QItemSection> <VnInput :label="t('FI')" v-model="params.fi" is-outlined @@ -67,20 +68,17 @@ const departments = ref(); <QSkeleton type="QInput" class="full-width" /> </QItemSection> <QItemSection v-if="departments"> - <QSelect + <VnSelect :label="t('Department')" v-model="params.departmentFk" - @update:model-value="searchFn()" :options="departments" option-value="id" option-label="name" emit-value map-options - use-input dense outlined rounded - :input-debounce="0" /> </QItemSection> </QItem> @@ -107,6 +105,7 @@ en: userName: User extension: Extension departmentFk: Department + id: ID es: params: search: Contiene @@ -116,6 +115,7 @@ es: userName: Usuario extension: Extensión departmentFk: Departamento + id: ID FI: NIF First Name: Nombre Last Name: Apellidos diff --git a/src/pages/Worker/WorkerList.vue b/src/pages/Worker/WorkerList.vue index 9d4c3acbf..4c9964c0b 100644 --- a/src/pages/Worker/WorkerList.vue +++ b/src/pages/Worker/WorkerList.vue @@ -1,5 +1,5 @@ <script setup> -import { computed, ref } from 'vue'; +import { onBeforeMount, computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; @@ -14,28 +14,27 @@ import VnLocation from 'src/components/common/VnLocation.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue'; import FetchData from 'src/components/FetchData.vue'; +import RightMenu from 'src/components/common/RightMenu.vue'; +import WorkerFilter from './WorkerFilter.vue'; +import { useState } from 'src/composables/useState'; +import axios from 'axios'; const { t } = useI18n(); const tableRef = ref(); const { viewSummary } = useSummaryDialog(); const companiesOptions = ref([]); -const workersOptions = ref([]); const payMethodsOptions = ref([]); const bankEntitiesOptions = ref([]); const postcodesOptions = ref([]); +const user = useState().getUser(); +const defaultPayMethod = ref(); const columns = computed(() => [ { align: 'left', name: 'id', - label: t('tableColumns.id'), - columnFilter: { - alias: 'w', - inWhere: true, - }, - chip: { - condition: () => true, - }, + label: t('id'), + field: 'id', isId: true, }, { @@ -44,7 +43,7 @@ const columns = computed(() => [ label: t('tableColumns.name'), isTitle: true, columnFilter: { - name: 'search', + name: 'firstName', }, }, { @@ -54,8 +53,7 @@ const columns = computed(() => [ cardVisible: true, columnFilter: { component: 'select', - inWhere: true, - alias: 'wd', + name: 'departmentFk', attrs: { url: 'Departments', }, @@ -79,7 +77,7 @@ const columns = computed(() => [ name: 'tableActions', actions: [ { - title: t('InvoiceOutSummary'), + title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row) => viewSummary(row.id, WorkerSummary), }, @@ -87,6 +85,14 @@ const columns = computed(() => [ }, ]); +onBeforeMount(async () => { + defaultPayMethod.value = ( + await axios.get('WorkerConfigs/findOne', { + params: { field: ['payMethodFk'] }, + }) + ).data?.payMethodFk; +}); + function handleLocation(data, location) { const { town, code, provinceFk, countryFk } = location ?? {}; data.postcode = code; @@ -103,6 +109,31 @@ function uppercaseStreetModel(data) { }, }; } + +function generateCodeUser(worker) { + if (!worker.firstName || !worker.lastNames) return; + + const totalName = worker.firstName.concat(' ' + worker.lastNames).toLowerCase(); + const totalNameArray = totalName.split(' '); + let newCode = ''; + + for (let part of totalNameArray) newCode += part.charAt(0); + + worker.code = newCode.toUpperCase().slice(0, 3); + worker.name = totalNameArray[0] + newCode.slice(1); + + if (!worker.companyFk) worker.companyFk = user.companyFk; +} + +async function autofillBic(worker) { + if (!worker || !worker.iban) return; + + let bankEntityId = parseInt(worker.iban.substr(4, 4)); + let filter = { where: { id: bankEntityId } }; + + const { data } = await axios.get(`BankEntities`, { params: { filter } }); + worker.bankEntityFk = data?.[0]?.id ?? undefined; +} </script> <template> <VnSearchbar @@ -115,11 +146,6 @@ function uppercaseStreetModel(data) { @on-fetch="(data) => (companiesOptions = data)" auto-load /> - <FetchData - url="Workers/search" - @on-fetch="(data) => (workersOptions = data)" - auto-load - /> <FetchData url="Paymethods" @on-fetch="(data) => (payMethodsOptions = data)" @@ -130,7 +156,13 @@ function uppercaseStreetModel(data) { @on-fetch="(data) => (bankEntitiesOptions = data)" auto-load /> + <RightMenu> + <template #right-panel> + <WorkerFilter data-key="Worker" /> + </template> + </RightMenu> <VnTable + v-if="defaultPayMethod" ref="tableRef" data-key="Worker" url="Workers/filter" @@ -139,16 +171,19 @@ function uppercaseStreetModel(data) { title: t('Create worker'), onDataSaved: ({ id }) => tableRef.redirect(id), formInitialData: { + payMethodFk: defaultPayMethod, + companyFk: user.companyFk, isFreelance: false, }, }" :columns="columns" default-mode="table" redirect="worker" + :right-search="false" auto-load > <template #more-create-dialog="{ data }"> - <div class="q-pa-lg full-width" style="max-width: 1200px"> + <div class="q-pa-lg full-width"> <VnRadio v-model="data.isFreelance" :val="false" @@ -162,10 +197,16 @@ function uppercaseStreetModel(data) { @update:model-value="delete data.payMethodFk" /> <VnRow> - <VnInput v-model="data.firstName" :label="t('worker.create.name')" /> + <VnInput + next + v-model="data.firstName" + :label="t('worker.create.name')" + @update:model-value="generateCodeUser(data)" + /> <VnInput v-model="data.lastNames" :label="t('worker.create.lastName')" + @update:model-value="generateCodeUser(data)" /> <VnInput v-model="data.code" :label="t('worker.create.code')" /> </VnRow> @@ -188,7 +229,7 @@ function uppercaseStreetModel(data) { <VnSelect :label="t('worker.create.boss')" v-model="data.bossFk" - :options="workersOptions" + url="Workers/search" option-value="id" option-label="name" hide-selected @@ -221,7 +262,7 @@ function uppercaseStreetModel(data) { </VnRow> <VnRow> <VnLocation - :roles-allowed-to-create="['deliveryAssistant']" + :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" :options="postcodesOptions" @update:model-value="(location) => handleLocation(data, location)" :disable="data.isFreelance" @@ -252,6 +293,7 @@ function uppercaseStreetModel(data) { v-model="data.iban" :label="t('worker.create.iban')" :disable="data.isFreelance" + @update:model-value="autofillBic(data)" > <template #append> <QIcon name="info" class="cursor-info"> @@ -268,8 +310,10 @@ function uppercaseStreetModel(data) { option-label="name" option-value="id" hide-selected - :roles-allowed-to-create="['salesAssistant', 'hr']" + :acls="[{ model: 'BankEntity', props: '*', accessType: 'WRITE' }]" :disable="data.isFreelance" + @update:model-value="autofillBic(data)" + :filter-options="['bic', 'name']" > <template #form> <CreateBankEntityForm diff --git a/src/pages/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue index a4874e5fc..512d07636 100644 --- a/src/pages/Zone/Card/ZoneBasicData.vue +++ b/src/pages/Zone/Card/ZoneBasicData.vue @@ -83,6 +83,7 @@ const agencyOptions = ref([]); :label="t('Price')" type="number" min="0" + required="true" clearable /> <VnInput @@ -95,7 +96,12 @@ const agencyOptions = ref([]); </VnRow> <VnRow> - <VnInput v-model="data.inflation" :label="t('Inflation')" clearable /> + <VnInput + v-model="data.inflation" + :label="t('Inflation')" + type="number" + clearable + /> <QCheckbox v-model="data.isVolumetric" :label="t('Volumetric')" diff --git a/src/pages/Zone/Card/ZoneCard.vue b/src/pages/Zone/Card/ZoneCard.vue index 02ec12fe7..d61c61abf 100644 --- a/src/pages/Zone/Card/ZoneCard.vue +++ b/src/pages/Zone/Card/ZoneCard.vue @@ -2,41 +2,37 @@ import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import { computed } from 'vue'; + import VnCard from 'components/common/VnCard.vue'; import ZoneDescriptor from './ZoneDescriptor.vue'; -import ZoneSearchbar from './ZoneSearchbar.vue'; +import ZoneFilterPanel from '../ZoneFilterPanel.vue'; const { t } = useI18n(); const route = useRoute(); - const routeName = computed(() => route.name); -const customRouteRedirectName = computed(() => { - if (routeName.value === 'ZoneLocations') return null; - return routeName.value; -}); -const searchbarMakeFetch = computed(() => routeName.value !== 'ZoneEvents'); -const searchBarDataKeys = { - ZoneWarehouses: 'ZoneWarehouses', - ZoneSummary: 'ZoneSummary', - ZoneLocations: 'ZoneLocations', - ZoneEvents: 'ZoneEvents', -}; + +function notIsLocations(ifIsFalse, ifIsTrue) { + if (routeName.value != 'ZoneLocations') return ifIsFalse; + return ifIsTrue; +} </script> <template> <VnCard - data-key="Zone" + data-key="zone" + base-url="Zones" :descriptor="ZoneDescriptor" - :search-data-key="searchBarDataKeys[routeName]" :filter-panel="ZoneFilterPanel" + :search-data-key="notIsLocations('ZoneList', 'ZoneLocations')" :searchbar-props="{ url: 'Zones', - label: t('list.searchZone'), + label: notIsLocations(t('list.searchZone'), t('list.searchLocation')), info: t('list.searchInfo'), + whereFilter: notIsLocations((value) => { + return /^\d+$/.test(value) + ? { id: value } + : { name: { like: `%${value}%` } }; + }), }" - > - <template #searchbar> - <ZoneSearchbar /> - </template> - </VnCard> + /> </template> diff --git a/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue b/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue index 8f1168ce9..22d5bcd5e 100644 --- a/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue +++ b/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue @@ -8,13 +8,6 @@ import VnConfirm from 'components/ui/VnConfirm.vue'; import axios from 'axios'; -const $props = defineProps({ - zone: { - type: Object, - default: () => {}, - }, -}); - const { t } = useI18n(); const { push, currentRoute } = useRouter(); const zoneId = currentRoute.value.params.id; @@ -22,32 +15,21 @@ const zoneId = currentRoute.value.params.id; const actions = { clone: async () => { const opts = { message: t('Zone cloned'), type: 'positive' }; - let clonedZoneId; try { - const { data } = await axios.post(`Zones/${zoneId}/clone`, { - shipped: $props.zone.value.shipped, - }); - clonedZoneId = data; + const { data } = await axios.post(`Zones/${zoneId}/clone`, {}); + notify(opts); + push(`/zone/${data.id}/basic-data`); } catch (e) { opts.message = t('It was not able to clone the zone'); opts.type = 'negative'; - } finally { - notify(opts); - - if (clonedZoneId) push({ name: 'ZoneSummary', params: { id: clonedZoneId } }); } }, remove: async () => { try { - await axios.post(`Zones/${zoneId}/setDeleted`); + await axios.post(`Zones/${zoneId}/deleteZone`); notify({ message: t('Zone deleted'), type: 'positive' }); - notify({ - message: t('You can undo this action within the first hour'), - icon: 'info', - }); - push({ name: 'ZoneList' }); } catch (e) { notify({ message: e.message, type: 'negative' }); @@ -64,30 +46,31 @@ function openConfirmDialog(callback) { } </script> <template> - <QItem @click="openConfirmDialog('clone')" v-ripple clickable> - <QItemSection avatar> - <QIcon name="content_copy" /> - </QItemSection> - <QItemSection>{{ t('To clone zone') }}</QItemSection> - </QItem> <QItem @click="openConfirmDialog('remove')" v-ripple clickable> <QItemSection avatar> <QIcon name="delete" /> </QItemSection> <QItemSection>{{ t('deleteZone') }}</QItemSection> </QItem> + <QItem @click="openConfirmDialog('clone')" v-ripple clickable> + <QItemSection avatar> + <QIcon name="content_copy" /> + </QItemSection> + <QItemSection>{{ t('cloneZone') }}</QItemSection> + </QItem> </template> <i18n> en: - deleteZone: Delete zone + deleteZone: Delete + cloneZone: Clone confirmDeletion: Confirm deletion confirmDeletionMessage: Are you sure you want to delete this zone? es: - To clone zone: Clonar zone - deleteZone: Eliminar zona + cloneZone: Clonar + deleteZone: Eliminar confirmDeletion: Confirmar eliminación confirmDeletionMessage: Seguro que quieres eliminar este zona? - + Zone deleted: Zona eliminada </i18n> diff --git a/src/pages/Zone/Card/ZoneEventExclusionForm.vue b/src/pages/Zone/Card/ZoneEventExclusionForm.vue index 721f4bbc3..0ba2e640a 100644 --- a/src/pages/Zone/Card/ZoneEventExclusionForm.vue +++ b/src/pages/Zone/Card/ZoneEventExclusionForm.vue @@ -58,20 +58,12 @@ const arrayData = useArrayData('ZoneEvents'); const exclusionGeoCreate = async () => { try { - if (isNew.value) { - const params = { - zoneFk: parseInt(route.params.id), - date: dated.value, - geoIds: tickedNodes.value, - }; - await axios.post('Zones/exclusionGeo', params); - } else { - const params = { - zoneExclusionFk: props.event?.zoneExclusionFk, - geoIds: tickedNodes.value, - }; - await axios.post('Zones/updateExclusionGeo', params); - } + const params = { + zoneFk: parseInt(route.params.id), + date: dated.value, + geoIds: tickedNodes.value, + }; + await axios.post('Zones/exclusionGeo', params); await refetchEvents(); } catch (err) { console.error('Error creating exclusion geo: ', err); @@ -85,7 +77,7 @@ const exclusionCreate = async () => { { dated: dated.value }, ]); else - await axios.put(`Zones/${route.params.id}/exclusions/${props.event?.id}`, { + await axios.post(`Zones/${route.params.id}/exclusions`, { dated: dated.value, }); @@ -103,8 +95,7 @@ const onSubmit = async () => { const deleteEvent = async () => { try { if (!props.event) return; - const exclusionId = props.event?.zoneExclusionFk || props.event?.id; - await axios.delete(`Zones/${route.params.id}/exclusions/${exclusionId}`); + await axios.delete(`Zones/${route.params.id}/exclusions`); await refetchEvents(); } catch (err) { console.error('Error deleting event: ', err); @@ -141,7 +132,11 @@ onMounted(() => { > <template #form-inputs> <VnRow class="row q-gutter-md q-mb-lg"> - <VnInputDate :label="t('eventsInclusionForm.day')" v-model="dated" /> + <VnInputDate + :label="t('eventsInclusionForm.day')" + v-model="dated" + :model-value="props.date" + /> </VnRow> <div class="column q-gutter-y-sm q-mb-md"> <QRadio diff --git a/src/pages/Zone/Card/ZoneEvents.vue b/src/pages/Zone/Card/ZoneEvents.vue index e4fe5ff22..32a7db4c4 100644 --- a/src/pages/Zone/Card/ZoneEvents.vue +++ b/src/pages/Zone/Card/ZoneEvents.vue @@ -13,8 +13,8 @@ import { reactive } from 'vue'; const { t } = useI18n(); const stateStore = useStateStore(); -const firstDay = ref(null); -const lastDay = ref(null); +const firstDay = ref(); +const lastDay = ref(); const events = ref([]); const formModeName = ref('include'); @@ -52,7 +52,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); @click="stateStore.toggleRightDrawer()" round dense - icon="menu" + icon="dock_to_left" > <QTooltip bottom anchor="bottom right"> {{ t('globals.collapseMenu') }} @@ -102,6 +102,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); color="primary" fab icon="add" + shortcut="+" /> <QTooltip class="text-no-wrap"> {{ t('eventsInclusionForm.addEvent') }} diff --git a/src/pages/Zone/Card/ZoneLocationsTree.vue b/src/pages/Zone/Card/ZoneLocationsTree.vue index 70384a1bb..cb1508ed6 100644 --- a/src/pages/Zone/Card/ZoneLocationsTree.vue +++ b/src/pages/Zone/Card/ZoneLocationsTree.vue @@ -1,10 +1,7 @@ <script setup> import { onMounted, ref, computed, watch, onUnmounted } from 'vue'; -import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import VnInput from 'src/components/common/VnInput.vue'; - import { useState } from 'src/composables/useState'; import axios from 'axios'; import { useArrayData } from 'composables/useArrayData'; @@ -30,7 +27,6 @@ const props = defineProps({ const emit = defineEmits(['update:tickedNodes']); -const { t } = useI18n(); const route = useRoute(); const state = useState(); @@ -186,16 +182,6 @@ onUnmounted(() => { </script> <template> - <VnInput - v-if="showSearchBar" - v-model="store.userParams.search" - :placeholder="t('globals.search')" - @keydown.enter.prevent="reFetch()" - > - <template #prepend> - <QIcon class="cursor-pointer" name="search" /> - </template> - </VnInput> <QTree ref="treeRef" :nodes="nodes" diff --git a/src/pages/Zone/Card/ZoneSearchbar.vue b/src/pages/Zone/Card/ZoneSearchbar.vue index 607057d0b..06168eb62 100644 --- a/src/pages/Zone/Card/ZoneSearchbar.vue +++ b/src/pages/Zone/Card/ZoneSearchbar.vue @@ -19,24 +19,14 @@ const exprBuilder = (param, value) => { agencyModeFk: value, }; case 'search': - if (value) { - if (!isNaN(value)) { - return { id: value }; - } else { - return { - name: { - like: `%${value}%`, - }, - }; - } - } + return /^\d+$/.test(value) ? { id: value } : { name: { like: `%${value}%` } }; } }; </script> <template> <VnSearchbar - data-key="ZoneList" + data-key="Zones" url="Zones" :filter="{ include: { relation: 'agencyMode', scope: { fields: ['name'] } }, diff --git a/src/pages/Zone/Card/ZoneWarehouses.vue b/src/pages/Zone/Card/ZoneWarehouses.vue index 196333d08..6b2933224 100644 --- a/src/pages/Zone/Card/ZoneWarehouses.vue +++ b/src/pages/Zone/Card/ZoneWarehouses.vue @@ -14,7 +14,7 @@ const { t } = useI18n(); const route = useRoute(); const { openConfirmationModal } = useVnConfirm(); -const paginateRef = ref(null); +const paginateRef = ref(); const createWarehouseDialogRef = ref(null); const arrayData = useArrayData('ZoneWarehouses'); @@ -111,7 +111,13 @@ const openCreateWarehouseForm = () => createWarehouseDialogRef.value.show(); <ZoneCreateWarehouse @on-submit-create-warehouse="createZoneWarehouse" /> </QDialog> <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn fab icon="add" color="primary" @click="openCreateWarehouseForm()"> + <QBtn + fab + icon="add" + color="primary" + @click="openCreateWarehouseForm()" + shortcut="+" + > <QTooltip>{{ t('warehouses.add') }}</QTooltip> </QBtn> </QPageSticky> diff --git a/src/pages/Zone/ZoneDeliveryPanel.vue b/src/pages/Zone/ZoneDeliveryPanel.vue index 03f534701..d6c96b935 100644 --- a/src/pages/Zone/ZoneDeliveryPanel.vue +++ b/src/pages/Zone/ZoneDeliveryPanel.vue @@ -1,47 +1,25 @@ <script setup> -import { onMounted, ref, reactive } from 'vue'; +import { onMounted, ref, reactive, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import VnSelect from 'src/components/common/VnSelect.vue'; - import { useArrayData } from 'src/composables/useArrayData'; -import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; -import { watch } from 'vue'; +import FetchData from 'src/components/FetchData.vue'; const { t } = useI18n(); const { notify } = useNotify(); -const deliveryMethodFk = ref(null); -const deliveryMethods = ref([]); +const deliveryMethodFk = ref('delivery'); +const deliveryMethods = ref({}); +const inq = ref([]); const formData = reactive({}); const arrayData = useArrayData('ZoneDeliveryDays', { url: 'Zones/getEvents', }); -const fetchDeliveryMethods = async (filter) => { - try { - const params = { filter: JSON.stringify(filter) }; - const { data } = await axios.get('DeliveryMethods', { params }); - return data.map((deliveryMethod) => deliveryMethod.id); - } catch (err) { - console.error('Error fetching delivery methods: ', err); - } -}; - -watch( - () => deliveryMethodFk.value, - async (val) => { - let filter; - if (val === 'pickUp') filter = { where: { code: 'PICKUP' } }; - else filter = { where: { code: { inq: ['DELIVERY', 'AGENCY'] } } }; - - deliveryMethods.value = await fetchDeliveryMethods(filter); - }, - { immediate: true } -); - +const deliveryMethodsConfig = { pickUp: ['PICKUP'], delivery: ['AGENCY', 'DELIVERY'] }; const fetchData = async (params) => { try { const { data } = params @@ -62,14 +40,38 @@ const onSubmit = async () => { }; onMounted(async () => { - deliveryMethodFk.value = 'delivery'; formData.geoFk = arrayData.store?.userParams?.geoFk; formData.agencyModeFk = arrayData.store?.userParams?.agencyModeFk; if (formData.geoFk || formData.agencyModeFk) await fetchData(); }); +watch( + () => deliveryMethodFk.value, + () => { + inq.value = { + deliveryMethodFk: { inq: deliveryMethods.value[deliveryMethodFk.value] }, + }; + } +); </script> <template> + <FetchData + url="DeliveryMethods" + :fields="['id', 'name', 'deliveryMethodFk']" + @on-fetch=" + (data) => { + Object.entries(deliveryMethodsConfig).forEach(([key, value]) => { + deliveryMethods[key] = data + .filter((code) => value.includes(code.code)) + .map((method) => method.id); + }); + inq = { + deliveryMethodFk: { inq: deliveryMethods[deliveryMethodFk] }, + }; + } + " + auto-load + /> <QForm @submit="onSubmit()" class="q-pa-md"> <div class="column q-gutter-y-sm"> <QRadio @@ -90,7 +92,7 @@ onMounted(async () => { :label="t('deliveryPanel.postcode')" v-model="formData.geoFk" url="Postcodes/location" - :fields="['geoFk', 'code', 'townFk']" + :fields="['geoFk', 'code', 'townFk', 'countryFk']" sort-by="code, townFk" option-value="geoFk" option-label="code" @@ -106,26 +108,35 @@ onMounted(async () => { <QItemLabel>{{ opt.code }}</QItemLabel> <QItemLabel caption >{{ opt.town?.province?.name }}, - {{ opt.town?.province?.country?.country }}</QItemLabel + {{ opt.town?.province?.country?.name }}</QItemLabel > </QItemSection> </QItem> </template> </VnSelect> <VnSelect - :label=" - t( - deliveryMethodFk === 'delivery' - ? 'deliveryPanel.agency' - : 'deliveryPanel.warehouse' - ) - " + data-key="delivery" + v-if="deliveryMethodFk == 'delivery'" + :label="t('deliveryPanel.agency')" v-model="formData.agencyModeFk" url="AgencyModes/isActive" :fields="['id', 'name']" - :where="{ - deliveryMethodFk: { inq: deliveryMethods }, - }" + :where="inq" + sort-by="name ASC" + option-value="id" + option-label="name" + hide-selected + dense + outlined + rounded + /> + <VnSelect + v-else + :label="t('deliveryPanel.warehouse')" + v-model="formData.agencyModeFk" + url="AgencyModes/isActive" + :fields="['id', 'name']" + :where="inq" sort-by="name ASC" option-value="id" option-label="name" diff --git a/src/pages/Zone/ZoneFilterPanel.vue b/src/pages/Zone/ZoneFilterPanel.vue index c84355eb0..25c55d75c 100644 --- a/src/pages/Zone/ZoneFilterPanel.vue +++ b/src/pages/Zone/ZoneFilterPanel.vue @@ -27,6 +27,7 @@ const agencies = ref([]); :data-key="props.dataKey" :search-button="true" :hidden-tags="['search']" + search-url="table" > <template #tags="{ tag }"> <div class="q-gutter-x-xs"> diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue index 0272292f6..d160ea6b5 100644 --- a/src/pages/Zone/ZoneList.vue +++ b/src/pages/Zone/ZoneList.vue @@ -1,74 +1,120 @@ <script setup> import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; -import { onMounted, computed } from 'vue'; +import { computed, ref, onMounted } from 'vue'; +import axios from 'axios'; + import { toCurrency } from 'src/filters'; - -import VnPaginate from 'src/components/ui/VnPaginate.vue'; -import ZoneSummary from 'src/pages/Zone/Card/ZoneSummary.vue'; - -import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { toTimeFormat } from 'src/filters/date'; import { useVnConfirm } from 'composables/useVnConfirm'; import useNotify from 'src/composables/useNotify.js'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useStateStore } from 'stores/useStateStore'; -import axios from 'axios'; +import ZoneSummary from 'src/pages/Zone/Card/ZoneSummary.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import VnInputTime from 'src/components/common/VnInputTime.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import ZoneFilterPanel from './ZoneFilterPanel.vue'; import ZoneSearchbar from './Card/ZoneSearchbar.vue'; -const stateStore = useStateStore(); const { t } = useI18n(); const router = useRouter(); const { notify } = useNotify(); const { viewSummary } = useSummaryDialog(); const { openConfirmationModal } = useVnConfirm(); +const stateStore = useStateStore(); +const tableRef = ref(); +const warehouseOptions = ref([]); -const redirectToZoneSummary = (event, { id }) => { - router.push({ name: 'ZoneSummary', params: { id } }); +const tableFilter = { + include: [ + { + relation: 'agencyMode', + scope: { + fields: ['id', 'name'], + }, + }, + ], }; const columns = computed(() => [ { - name: 'ID', - label: t('list.id'), - field: (row) => row.id, - sortable: true, align: 'left', + name: 'id', + label: t('list.id'), + chip: { + condition: () => true, + }, + isId: true, + columnFilter: { + inWhere: true, + }, }, { + align: 'left', name: 'name', label: t('list.name'), - field: (row) => row.name, - sortable: true, - align: 'left', + isTitle: true, + create: true, + columnFilter: { + optionLabel: 'name', + optionValue: 'id', + }, }, { - name: 'agency', + align: 'left', + name: 'agencyModeFk', label: t('list.agency'), - field: (row) => row?.agencyMode?.name, - sortable: true, - align: 'left', + cardVisible: true, + columnFilter: { + component: 'select', + inWhere: true, + attrs: { + url: 'AgencyModes', + }, + }, + columnField: { + component: null, + }, + format: (row, dashIfEmpty) => dashIfEmpty(row?.agencyMode?.name), }, { - name: 'close', - label: t('list.close'), - field: (row) => (row?.hour ? toTimeFormat(row?.hour) : '-'), - sortable: true, align: 'left', - }, - { name: 'price', label: t('list.price'), - field: (row) => (row?.price ? toCurrency(row.price) : '-'), - sortable: true, - align: 'left', + cardVisible: true, + format: (row) => toCurrency(row.price), + columnFilter: { + inWhere: true, + }, + }, + { + align: 'left', + name: 'hour', + label: t('list.close'), + cardVisible: true, + format: (row) => toTimeFormat(row.hour), + hidden: true, }, { - name: 'actions', - label: '', - sortable: false, align: 'right', + name: 'tableActions', + actions: [ + { + title: t('list.zoneSummary'), + icon: 'preview', + action: (row) => viewSummary(row.id, ZoneSummary), + isPrimary: true, + }, + { + title: t('globals.clone'), + icon: 'vn:clone', + action: (row) => handleClone(row.id), + isPrimary: true, + }, + ], }, ]); @@ -84,6 +130,7 @@ const handleClone = (id) => { () => clone(id) ); }; + onMounted(() => (stateStore.rightDrawer = true)); </script> @@ -91,82 +138,72 @@ onMounted(() => (stateStore.rightDrawer = true)); <ZoneSearchbar /> <RightMenu> <template #right-panel> - <ZoneFilterPanel data-key="ZoneList" :expr-builder="exprBuilder" /> + <ZoneFilterPanel data-key="Zones" /> </template> </RightMenu> - <QPage class="column items-center q-pa-md"> - <div class="vn-card-list"> - <VnPaginate - data-key="ZoneList" - url="Zones" - :filter="{ - include: { relation: 'agencyMode', scope: { fields: ['name'] } }, - }" - :limit="20" - auto-load - > - <template #body="{ rows }"> - <div class="q-pa-md"> - <QTable - :rows="rows" - :columns="columns" - row-key="clientId" - class="full-width" - @row-click="redirectToZoneSummary" - > - <template #header="props"> - <QTr :props="props" class="bg"> - <QTh - v-for="col in props.cols" - :key="col.name" - :props="props" - > - {{ t(col.label) }} - <QTooltip v-if="col.tooltip">{{ - col.tooltip - }}</QTooltip> - </QTh> - </QTr> - </template> - - <template #body-cell="props"> - <QTd :props="props"> - <QTr :props="props" class="cursor-pointer"> - {{ props.value }} - </QTr> - </QTd> - </template> - <template #body-cell-actions="props"> - <QTd :props="props" class="q-gutter-x-sm"> - <QIcon - name="vn:clone" - size="sm" - color="primary" - @click.stop="handleClone(props.row.id)" - > - <QTooltip>{{ t('globals.clone') }}</QTooltip> - </QIcon> - <QIcon - name="preview" - size="sm" - color="primary" - @click.stop=" - viewSummary(props.row.id, ZoneSummary) - " - > - <QTooltip>{{ t('Preview') }}</QTooltip> - </QIcon> - </QTd> - </template> - </QTable> - </div> - </template> - </VnPaginate> - </div> - <QPageSticky position="bottom-right" :offset="[18, 18]"> - <QBtn :to="{ path: `/zone/create` }" fab icon="add" color="primary"> - <QTooltip>{{ t('list.create') }}</QTooltip> - </QBtn> - </QPageSticky> - </QPage> + <VnTable + ref="tableRef" + data-key="Zones" + url="Zones" + :create="{ + urlCreate: 'Zones', + title: t('list.createZone'), + onDataSaved: ({ id }) => tableRef.redirect(`${id}/location`), + formInitialData: {}, + }" + :user-filter="tableFilter" + :columns="columns" + redirect="zone" + :right-search="false" + auto-load + > + <template #more-create-dialog="{ data }"> + <VnSelect + url="AgencyModes" + v-model="data.agencyModeFk" + option-value="id" + option-label="name" + :label="t('list.agency')" + /> + <VnInput + v-model="data.price" + :label="t('list.price')" + min="0" + type="number" + required="true" + /> + <VnInput + v-model="data.bonus" + :label="t('list.bonus')" + min="0" + type="number" + /> + <VnInput + v-model="data.travelingDays" + :label="t('list.travelingDays')" + type="number" + min="0" + /> + <VnInputTime v-model="data.hour" :label="t('list.close')" /> + <VnSelect + url="Warehouses" + v-model="data.warehouseFK" + option-value="id" + option-label="name" + :label="t('list.warehouse')" + :options="warehouseOptions" + /> + <QCheckbox + v-model="data.isVolumetric" + :label="t('list.isVolumetric')" + :toggle-indeterminate="false" + /> + </template> + </VnTable> </template> + +<i18n> +es: + Search zone: Buscar zona + You can search zones by id or name: Puedes buscar zonas por id o nombre +</i18n> diff --git a/src/pages/Zone/locale/en.yml b/src/pages/Zone/locale/en.yml index 31eeb2b7f..2608c071c 100644 --- a/src/pages/Zone/locale/en.yml +++ b/src/pages/Zone/locale/en.yml @@ -18,9 +18,16 @@ list: create: Create zone openSummary: Details searchZone: Search zones + searchLocation: Search locations searchInfo: Search zone by id or name confirmCloneTitle: All it's properties will be copied confirmCloneSubtitle: Do you want to clone this zone? + travelingDays: Traveling days + warehouse: Warehouse + bonus: Bonus + isVolumetric: Volumetric + createZone: Create zone + zoneSummary: Summary create: name: Name warehouse: Warehouse @@ -30,6 +37,8 @@ create: price: Price bonus: Bonus volumetric: Volumetric + itemMaxSize: Max m³ + inflation: Inflation summary: agency: Agency price: Price diff --git a/src/pages/Zone/locale/es.yml b/src/pages/Zone/locale/es.yml index c670c2c08..dd919a0c5 100644 --- a/src/pages/Zone/locale/es.yml +++ b/src/pages/Zone/locale/es.yml @@ -18,9 +18,16 @@ list: create: Crear zona openSummary: Detalles searchZone: Buscar zonas + searchLocation: Buscar localizaciones searchInfo: Buscar zonas por identificador o nombre confirmCloneTitle: Todas sus propiedades serán copiadas confirmCloneSubtitle: ¿Seguro que quieres clonar esta zona? + travelingDays: Días de viaje + warehouse: Almacén + bonus: Bonus + isVolumetric: Volumétrico + createZone: Crear zona + zoneSummary: Resumen create: name: Nombre warehouse: Almacén @@ -30,6 +37,8 @@ create: price: Precio bonus: Bonificación volumetric: Volumétrico + itemMaxSize: Medida máxima + inflation: Inflación summary: agency: Agencia price: Precio diff --git a/src/router/index.js b/src/router/index.js index 686da2dde..18541c0b2 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -60,15 +60,12 @@ export default route(function (/* { store, ssrContext } */) { await useTokenConfig().fetch(); } const matches = to.matched; - const hasRequiredRoles = matches.every((route) => { + const hasRequiredAcls = matches.every((route) => { const meta = route.meta; - if (meta && meta.roles) return useRole().hasAny(meta.roles); - return true; + if (!meta?.acls) return true; + return useAcl().hasAny(meta.acls); }); - - if (!hasRequiredRoles) { - return next({ path: '/' }); - } + if (!hasRequiredAcls) return next({ path: '/' }); } next(); diff --git a/src/router/modules/Supplier.js b/src/router/modules/Supplier.js index b711066b2..143d7c824 100644 --- a/src/router/modules/Supplier.js +++ b/src/router/modules/Supplier.js @@ -7,6 +7,7 @@ export default { title: 'suppliers', icon: 'vn:supplier', moduleName: 'Supplier', + keyBinding: 'p', }, component: RouterView, redirect: { name: 'SupplierMain' }, diff --git a/src/router/modules/account.js b/src/router/modules/account.js index cfec2b95d..7200131da 100644 --- a/src/router/modules/account.js +++ b/src/router/modules/account.js @@ -7,6 +7,7 @@ export default { title: 'users', icon: 'face', moduleName: 'Account', + keyBinding: 'u', }, component: RouterView, redirect: { name: 'AccountMain' }, @@ -79,7 +80,7 @@ export default { meta: { title: 'accounts', icon: 'accessibility', - roles: ['itManagement'], + acls: [{ model: 'Account', props: '*', accessType: '*' }], }, component: () => import('src/pages/Account/AccountAccounts.vue'), }, @@ -89,7 +90,7 @@ export default { meta: { title: 'ldap', icon: 'account_tree', - roles: ['itManagement'], + acls: [{ model: 'LdapConfig', props: '*', accessType: '*' }], }, component: () => import('src/pages/Account/AccountLdap.vue'), }, @@ -99,7 +100,7 @@ export default { meta: { title: 'samba', icon: 'preview', - roles: ['itManagement'], + acls: [{ model: 'SambaConfig', props: '*', accessType: '*' }], }, component: () => import('src/pages/Account/AccountSamba.vue'), }, diff --git a/src/router/modules/claim.js b/src/router/modules/claim.js index cced9e24d..b58a58e8d 100644 --- a/src/router/modules/claim.js +++ b/src/router/modules/claim.js @@ -7,6 +7,7 @@ export default { title: 'claims', icon: 'vn:claims', moduleName: 'Claim', + keyBinding: 'r', }, component: RouterView, redirect: { name: 'ClaimMain' }, @@ -61,7 +62,7 @@ export default { meta: { title: 'basicData', icon: 'vn:settings', - roles: ['salesPerson'], + acls: [{ model: 'Claim', props: 'findById', accessType: 'READ' }], }, component: () => import('src/pages/Claim/Card/ClaimBasicData.vue'), }, @@ -98,7 +99,13 @@ export default { meta: { title: 'development', icon: 'vn:traceability', - roles: ['claimManager'], + acls: [ + { + model: 'ClaimDevelopment', + props: '*', + accessType: 'WRITE', + }, + ], }, component: () => import('src/pages/Claim/Card/ClaimDevelopment.vue'), }, diff --git a/src/router/modules/customer.js b/src/router/modules/customer.js index f364bd862..1b707f1a2 100644 --- a/src/router/modules/customer.js +++ b/src/router/modules/customer.js @@ -7,6 +7,7 @@ export default { title: 'customers', icon: 'vn:client', moduleName: 'Customer', + keyBinding: 'c', }, component: RouterView, redirect: { name: 'CustomerMain' }, diff --git a/src/router/modules/entry.js b/src/router/modules/entry.js index 0d38ed626..365615b87 100644 --- a/src/router/modules/entry.js +++ b/src/router/modules/entry.js @@ -7,6 +7,7 @@ export default { title: 'entries', icon: 'vn:entry', moduleName: 'Entry', + keyBinding: 'e', }, component: RouterView, redirect: { name: 'EntryMain' }, diff --git a/src/router/modules/invoiceIn.js b/src/router/modules/invoiceIn.js index cd8f7de9c..168d64f37 100644 --- a/src/router/modules/invoiceIn.js +++ b/src/router/modules/invoiceIn.js @@ -1,5 +1,5 @@ import { RouterView } from 'vue-router'; - +import { setRectificative } from 'src/pages/InvoiceIn/composables/setRectificative'; export default { path: '/invoice-in', name: 'InvoiceIn', @@ -11,7 +11,7 @@ export default { component: RouterView, redirect: { name: 'InvoiceInMain' }, menus: { - main: ['InvoiceInList'], + main: ['InvoiceInList', 'InvoiceInSerial'], card: [ 'InvoiceInBasicData', 'InvoiceInVat', @@ -37,6 +37,16 @@ export default { }, component: () => import('src/pages/InvoiceIn/InvoiceInList.vue'), }, + { + path: 'serial', + name: 'InvoiceInSerial', + meta: { + title: 'serial', + icon: 'view_list', + }, + component: () => + import('src/pages/InvoiceIn/Serial/InvoiceInSerial.vue'), + }, { path: 'create', name: 'InvoiceInCreare', @@ -53,6 +63,10 @@ export default { path: ':id', component: () => import('src/pages/InvoiceIn/Card/InvoiceInCard.vue'), redirect: { name: 'InvoiceInSummary' }, + beforeEnter: async (to, from, next) => { + await setRectificative(to); + next(); + }, children: [ { name: 'InvoiceInSummary', @@ -70,7 +84,6 @@ export default { meta: { title: 'basicData', icon: 'vn:settings', - roles: ['salesPerson'], }, component: () => import('src/pages/InvoiceIn/Card/InvoiceInBasicData.vue'), diff --git a/src/router/modules/item.js b/src/router/modules/item.js index 4bd5df4e2..48e19dd54 100644 --- a/src/router/modules/item.js +++ b/src/router/modules/item.js @@ -7,6 +7,7 @@ export default { title: 'items', icon: 'vn:item', moduleName: 'Item', + keyBinding: 'a', }, component: RouterView, redirect: { name: 'ItemMain' }, diff --git a/src/router/modules/monitor.js b/src/router/modules/monitor.js index f0db8d3f3..7342a5904 100644 --- a/src/router/modules/monitor.js +++ b/src/router/modules/monitor.js @@ -7,11 +7,12 @@ export default { title: 'monitors', icon: 'grid_view', moduleName: 'Monitor', + keyBinding: 'm', }, component: RouterView, redirect: { name: 'MonitorMain' }, menus: { - main: ['MonitorList'], + main: ['MonitorTickets', 'MonitorClientsActions'], card: [], }, children: [ @@ -19,16 +20,27 @@ export default { path: '', name: 'MonitorMain', component: () => import('src/components/common/VnSectionMain.vue'), - redirect: { name: 'MonitorList' }, + redirect: { name: 'MonitorTickets' }, children: [ { - path: 'list', - name: 'MonitorList', + path: 'tickets', + name: 'MonitorTickets', meta: { - title: 'list', - icon: 'grid_view', + title: 'ticketsMonitor', + icon: 'vn:ticket', }, - component: () => import('src/pages/Monitor/MonitorList.vue'), + component: () => + import('src/pages/Monitor/Ticket/MonitorTickets.vue'), + }, + { + path: 'clients-actions', + name: 'MonitorClientsActions', + meta: { + title: 'clientsActionsMonitor', + icon: 'vn:client', + }, + component: () => + import('src/pages/Monitor/MonitorClientsActions.vue'), }, ], }, diff --git a/src/router/modules/order.js b/src/router/modules/order.js index a2b874cc6..bfa37fce5 100644 --- a/src/router/modules/order.js +++ b/src/router/modules/order.js @@ -7,6 +7,7 @@ export default { title: 'order', icon: 'vn:basket', moduleName: 'Order', + keyBinding: 'o', }, component: RouterView, redirect: { name: 'OrderMain' }, @@ -63,7 +64,7 @@ export default { title: 'basicData', icon: 'vn:settings', }, - component: () => import('src/pages/Order/Card/OrderForm.vue'), + component: () => import('src/pages/Order/Card/OrderBasicData.vue'), }, { name: 'OrderCatalog', @@ -72,7 +73,7 @@ export default { title: 'catalog', icon: 'vn:basket', }, - component: () => import('src/pages/Order/OrderCatalog.vue'), + component: () => import('src/pages/Order/Card/OrderCatalog.vue'), }, { name: 'OrderVolume', @@ -81,7 +82,7 @@ export default { title: 'volume', icon: 'vn:volume', }, - component: () => import('src/pages/Order/OrderVolume.vue'), + component: () => import('src/pages/Order/Card/OrderVolume.vue'), }, { name: 'OrderLines', @@ -90,7 +91,7 @@ export default { title: 'lines', icon: 'vn:lines', }, - component: () => import('src/pages/Order/OrderLines.vue'), + component: () => import('src/pages/Order/Card/OrderLines.vue'), }, ], }, diff --git a/src/router/modules/shelving.js b/src/router/modules/shelving.js index 70145dfb4..b7f50a3b6 100644 --- a/src/router/modules/shelving.js +++ b/src/router/modules/shelving.js @@ -76,7 +76,6 @@ export default { meta: { title: 'basicData', icon: 'vn:settings', - roles: ['salesPerson'], }, component: () => import('pages/Shelving/Card/ShelvingForm.vue'), }, diff --git a/src/router/modules/ticket.js b/src/router/modules/ticket.js index 4074f089f..dcc238f95 100644 --- a/src/router/modules/ticket.js +++ b/src/router/modules/ticket.js @@ -7,6 +7,7 @@ export default { title: 'tickets', icon: 'vn:ticket', moduleName: 'Ticket', + keyBinding: 't', }, component: RouterView, redirect: { name: 'TicketMain' }, @@ -53,7 +54,6 @@ export default { meta: { title: 'createTicket', icon: 'vn:ticketAdd', - roles: ['developer'], }, component: () => import('src/pages/Ticket/TicketCreate.vue'), }, diff --git a/src/router/modules/wagon.js b/src/router/modules/wagon.js index d3d14a888..e25e585eb 100644 --- a/src/router/modules/wagon.js +++ b/src/router/modules/wagon.js @@ -11,7 +11,7 @@ export default { component: RouterView, redirect: { name: 'WagonMain' }, menus: { - main: ['WagonList', 'WagonTypeList', 'WagonCounter'], + main: ['WagonList', 'WagonTypeList', 'WagonCounter', 'WagonTray'], card: [], }, children: [ @@ -81,7 +81,7 @@ export default { title: 'typeCreate', icon: 'create', }, - component: () => import('src/pages/Wagon/Type/WagonTypeCreate.vue'), + component: () => import('src/pages/Wagon/Type/WagonTypeList.vue'), }, { path: ':id/edit', @@ -90,7 +90,7 @@ export default { title: 'typeEdit', icon: 'edit', }, - component: () => import('src/pages/Wagon/Type/WagonTypeCreate.vue'), + component: () => import('src/pages/Wagon/Type/WagonTypeEdit.vue'), }, ], }, diff --git a/src/router/modules/worker.js b/src/router/modules/worker.js index f80df5e06..b2716474b 100644 --- a/src/router/modules/worker.js +++ b/src/router/modules/worker.js @@ -7,6 +7,7 @@ export default { title: 'workers', icon: 'vn:worker', moduleName: 'Worker', + keyBinding: 'w', }, component: RouterView, redirect: { name: 'WorkerMain' }, @@ -25,6 +26,7 @@ export default { 'WorkerLocker', 'WorkerBalance', 'WorkerFormation', + 'WorkerMedical', ], }, children: [ @@ -196,6 +198,15 @@ export default { }, component: () => import('src/pages/Worker/Card/WorkerFormation.vue'), }, + { + name: 'WorkerMedical', + path: 'medical', + meta: { + title: 'medical', + icon: 'medical_information', + }, + component: () => import('src/pages/Worker/Card/WorkerMedical.vue'), + }, ], }, ], diff --git a/src/router/modules/zone.js b/src/router/modules/zone.js index 889b47464..1f27cc76f 100644 --- a/src/router/modules/zone.js +++ b/src/router/modules/zone.js @@ -7,6 +7,7 @@ export default { title: 'zones', icon: 'vn:zone', moduleName: 'Zone', + keyBinding: 'z', }, component: RouterView, redirect: { name: 'ZoneMain' }, @@ -50,33 +51,6 @@ export default { }, component: () => import('src/pages/Zone/ZoneDeliveryDays.vue'), }, - { - path: 'create', - name: 'ZoneCreate', - meta: { - title: 'zoneCreate', - icon: 'create', - }, - component: () => import('src/pages/Zone/ZoneCreate.vue'), - }, - { - path: ':id/edit', - name: 'ZoneEdit', - meta: { - title: 'zoneEdit', - icon: 'edit', - }, - component: () => import('src/pages/Zone/ZoneCreate.vue'), - }, - // { - // path: 'counter', - // name: 'ZoneCounter', - // meta: { - // title: 'zoneCounter', - // icon: 'add_circle', - // }, - // component: () => import('src/pages/Zone/ZoneCounter.vue'), - // }, { name: 'ZoneUpcomingDeliveries', path: 'upcoming-deliveries', diff --git a/src/stores/invoiceOutGlobal.js b/src/stores/invoiceOutGlobal.js index bb9a3d376..42acac013 100644 --- a/src/stores/invoiceOutGlobal.js +++ b/src/stores/invoiceOutGlobal.js @@ -19,6 +19,7 @@ export const useInvoiceOutGlobalStore = defineStore({ maxShipped: null, clientId: null, printer: null, + serialType: null, }, addresses: [], minInvoicingDate: null, @@ -100,6 +101,7 @@ export const useInvoiceOutGlobalStore = defineStore({ maxShipped: new Date(formData.maxShipped), clientId: formData.clientId ? formData.clientId : null, companyFk: formData.companyFk, + serialType: formData.serialType, }; this.validateMakeInvoceParams(params, clientsToInvoice); @@ -152,7 +154,13 @@ export const useInvoiceOutGlobalStore = defineStore({ ); throw new Error('Invoice date in the future'); } - + if (!params.serialType) { + notify( + 'invoiceOut.globalInvoices.errors.chooseValidSerialType', + 'negative' + ); + throw new Error('Invalid Serial Type'); + } if (!params.companyFk) { notify('invoiceOut.globalInvoices.errors.chooseValidCompany', 'negative'); throw new Error('Invalid company'); @@ -180,6 +188,7 @@ export const useInvoiceOutGlobalStore = defineStore({ invoiceDate: new Date(formData.invoiceDate), maxShipped: new Date(formData.maxShipped), companyFk: formData.companyFk, + serialType: formData.serialType, }; this.status = 'invoicing'; @@ -191,12 +200,7 @@ export const useInvoiceOutGlobalStore = defineStore({ this.addressIndex++; this.isInvoicing = false; } catch (err) { - if ( - err && - err.response && - err.response.status >= 400 && - err.response.status < 500 - ) { + if (err?.response?.status >= 400 && err?.response?.status < 500) { this.invoiceClientError(address, err.response?.data?.error?.message); return; } else { @@ -243,7 +247,7 @@ export const useInvoiceOutGlobalStore = defineStore({ params, }); - if (data.data && data.data.error) throw new Error(); + if (data?.data?.error) throw new Error(); const status = exportFile('negativeBases.csv', data, { encoding: 'windows-1252', diff --git a/src/stores/useNavigationStore.js b/src/stores/useNavigationStore.js index 961e80377..4a819bf19 100644 --- a/src/stores/useNavigationStore.js +++ b/src/stores/useNavigationStore.js @@ -2,7 +2,7 @@ import axios from 'axios'; import { ref } from 'vue'; import { defineStore } from 'pinia'; import { toLowerCamel } from 'src/filters'; -import { useRole } from 'src/composables/useRole'; +import { useAcl } from 'src/composables/useAcl'; import routes from 'src/router/modules'; export const useNavigationStore = defineStore('navigationStore', () => { @@ -26,7 +26,7 @@ export const useNavigationStore = defineStore('navigationStore', () => { 'zone', ]; const pinnedModules = ref([]); - const role = useRole(); + const acl = useAcl(); function getModules() { const modulesRoutes = ref([]); @@ -56,6 +56,7 @@ export const useNavigationStore = defineStore('navigationStore', () => { function addMenuItem(module, route, parent) { const { meta } = route; let { menuChildren = null } = meta; + if (meta.hidden) return; if (menuChildren) menuChildren = menuChildren.map(({ name, title, icon }) => ({ name, @@ -63,7 +64,7 @@ export const useNavigationStore = defineStore('navigationStore', () => { title: `globals.pageTitles.${title}`, })); - if (meta && meta.roles && role.hasAny(meta.roles) === false) return; + if (meta && meta.acls && acl.hasAny(meta.acls) === false) return; const item = { name: route.name, @@ -72,6 +73,7 @@ export const useNavigationStore = defineStore('navigationStore', () => { if (meta) { item.title = `globals.pageTitles.${meta.title}`; item.icon = meta.icon; + item.keyBinding = meta.keyBinding; } parent.push(item); diff --git a/test/cypress/integration/claim/claimDevelopment.spec.js b/test/cypress/integration/claim/claimDevelopment.spec.js index 903f58d4b..3b73a24d9 100755 --- a/test/cypress/integration/claim/claimDevelopment.spec.js +++ b/test/cypress/integration/claim/claimDevelopment.spec.js @@ -8,6 +8,8 @@ describe('ClaimDevelopment', () => { cy.viewport(1920, 1080); cy.login('developer'); cy.visit(`/#/claim/${claimId}/development`); + cy.intercept('GET', /\/api\/Workers\/search/).as('workers'); + cy.intercept('GET', /\/api\/Workers\/search/).as('workers'); cy.waitForElement('tbody'); }); @@ -32,10 +34,19 @@ describe('ClaimDevelopment', () => { }); it('should add and remove new line', () => { + cy.wait(['@workers', '@workers']); cy.addCard(); + cy.get(thirdRow).should('exist'); - const rowData = [false, 'Novato', 'Roces', 'Compradores', 'employeeNick', 'Tour']; + const rowData = [ + false, + 'Novato', + 'Roces', + 'Compradores', + 'administrativeNick', + 'Tour', + ]; cy.fillRow(thirdRow, rowData); cy.saveCard(); diff --git a/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js b/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js index 77a11969b..e1939fe5a 100644 --- a/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInBasicData.spec.js @@ -36,8 +36,7 @@ describe('InvoiceInBasicData', () => { }); it('should throw an error creating a new dms if a file is not attached', () => { - cy.get(formInputs).eq(5).click(); - cy.get(formInputs).eq(5).type('{selectall}{backspace}'); + cy.get(formInputs).eq(7).type('{selectall}{backspace}'); cy.get(documentBtns).eq(0).click(); cy.get(dialogActionBtns).eq(1).click(); cy.get('.q-notification__message').should( diff --git a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js index 018ae7a53..b84d743d1 100644 --- a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js @@ -3,13 +3,14 @@ describe('InvoiceInVat', () => { const thirdRow = 'tbody > :nth-child(3)'; const firstLineVat = 'tbody > :nth-child(1) > :nth-child(4)'; const dialogInputs = '.q-dialog label input'; - const dialogBtns = '.q-dialog button'; - const acrossInput = 'tbody tr:nth-child(1) td:nth-child(2) .default-icon'; + const addBtn = 'tbody tr:nth-child(1) td:nth-child(2) .--add-icon'; const randomInt = Math.floor(Math.random() * 100); beforeEach(() => { cy.login('developer'); cy.visit(`/#/invoice-in/1/vat`); + cy.intercept('GET', '/api/InvoiceIns/1/getTotals').as('lastCall'); + cy.wait('@lastCall'); }); it('should edit the sage iva', () => { @@ -26,22 +27,15 @@ describe('InvoiceInVat', () => { }); it('should remove the first line', () => { - cy.removeRow(2); - }); - - it('should throw an error if there are fields undefined', () => { - cy.get(acrossInput).click(); - cy.get(dialogBtns).eq(2).click(); - cy.get('.q-notification__message').should('have.text', "The code can't be empty"); + cy.removeRow(1); }); it('should correctly handle expense addition', () => { - cy.get(acrossInput).click(); + cy.get(addBtn).click(); cy.get(dialogInputs).eq(0).type(randomInt); - cy.get(dialogInputs).eq(1).click(); cy.get(dialogInputs).eq(1).type('This is a dummy expense'); - cy.get(dialogBtns).eq(2).click(); - cy.get('.q-notification__message').should('have.text', 'Data saved'); + cy.get('button[type="submit"]').click(); + cy.get('.q-notification__message').should('have.text', 'Data created'); }); }); diff --git a/test/cypress/integration/outLogin/login.spec.js b/test/cypress/integration/outLogin/login.spec.js index f8a9f5c64..3db223cdb 100755 --- a/test/cypress/integration/outLogin/login.spec.js +++ b/test/cypress/integration/outLogin/login.spec.js @@ -52,9 +52,9 @@ describe('Login', () => { cy.url().should('contain', '/login'); }); - it(`should get redirected to dashboard since employee can't create tickets`, () => { - cy.visit('/#/ticket/create', { failOnStatusCode: false }); - cy.url().should('contain', '/#/login?redirect=/ticket/create'); + it(`should be redirected to dashboard since the employee is not enabled to see ldap`, () => { + cy.visit('/#/account/ldap', { failOnStatusCode: false }); + cy.url().should('contain', '/#/login?redirect=/account/ldap'); cy.get('input[aria-label="Username"]').type('employee'); cy.get('input[aria-label="Password"]').type('nightmare'); cy.get('button[type="submit"]').click(); diff --git a/test/cypress/integration/route/routeList.spec.js b/test/cypress/integration/route/routeList.spec.js index afc0fc395..c9d7147c2 100644 --- a/test/cypress/integration/route/routeList.spec.js +++ b/test/cypress/integration/route/routeList.spec.js @@ -10,12 +10,13 @@ describe('Route', () => { it('Route list create route', () => { cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); - cy.get('input[name="description"]').eq(1).type('routeTestOne{enter}'); + cy.get('input[name="description"]').type('routeTestOne{enter}'); cy.get('.q-notification__message').should('have.text', 'Data created'); cy.url().should('include', '/summary'); }); it('Route list search and edit', () => { + cy.get('#searchbar input').type('{enter}'); cy.get('input[name="description"]').type('routeTestOne{enter}'); cy.get('.q-table tr') .its('length') diff --git a/test/cypress/integration/ticket/ticketDescriptor.spec.js b/test/cypress/integration/ticket/ticketDescriptor.spec.js index fc920f346..516b0f13d 100644 --- a/test/cypress/integration/ticket/ticketDescriptor.spec.js +++ b/test/cypress/integration/ticket/ticketDescriptor.spec.js @@ -1,22 +1,22 @@ /// <reference types="cypress" /> describe('Ticket descriptor', () => { const toCloneOpt = '[role="menu"] .q-list > :nth-child(5)'; + const setWeightOpt = '[role="menu"] .q-list > :nth-child(6)'; const warehouseValue = ':nth-child(1) > :nth-child(6) > .value > span'; const summaryHeader = '.summaryHeader > div'; - + const weight = 25; + const weightValue = ':nth-child(10) > .value > span'; beforeEach(() => { - const ticketId = 1; - cy.login('developer'); - cy.visit(`/#/ticket/${ticketId}/summary`); + cy.viewport(1920, 1080); }); it('should clone the ticket without warehouse', () => { - cy.openLeftMenu(); + cy.visit('/#/ticket/1/summary'); cy.openActionsDescriptor(); cy.get(toCloneOpt).click(); cy.clickConfirm(); - cy.get(warehouseValue).contains('-'); + cy.get(warehouseValue).contains('Warehouse One'); cy.get(summaryHeader) .invoke('text') .then((text) => { @@ -24,4 +24,15 @@ describe('Ticket descriptor', () => { cy.wrap(owner.trim()).should('eq', 'Bruce Wayne (1101)'); }); }); + + it('should set the weight of the ticket', () => { + cy.visit('/#/ticket/10/summary'); + cy.openActionsDescriptor(); + cy.get(setWeightOpt).click(); + cy.intercept('POST', /\/api\/Tickets\/\d+\/setWeight/).as('weight'); + cy.get('.q-dialog input').type(weight); + cy.clickConfirm(); + cy.wait('@weight'); + cy.get(weightValue).contains(weight); + }); }); diff --git a/test/cypress/integration/vnComponent/VnShortcut.spec.js b/test/cypress/integration/vnComponent/VnShortcut.spec.js new file mode 100644 index 000000000..b49b4e964 --- /dev/null +++ b/test/cypress/integration/vnComponent/VnShortcut.spec.js @@ -0,0 +1,33 @@ +/// <reference types="cypress" /> + +describe('VnShortcuts', () => { + const modules = { + item: 'a', + customer: 'c', + ticket: 't', + claim: 'r', + worker: 'w', + monitor: 'm', + order: 'o', + supplier: 'p', + entry: 'e', + zone: 'z', + account: 'u', + }; + beforeEach(() => { + cy.login('developer'); + cy.visit('/'); + }); + + for (const [module, shortcut] of Object.entries(modules)) { + it(`should visit ${module} module`, () => { + cy.get('body').trigger('keydown', { + ctrlKey: true, + altKey: true, + code: `Key${shortcut.toUpperCase()}`, + }); + + cy.url().should('include', module); + }); + } +}); diff --git a/test/cypress/integration/wagonType/wagonTypeCreate.spec.js b/test/cypress/integration/wagonType/wagonTypeCreate.spec.js index bcf7fe841..cd7ffa58f 100644 --- a/test/cypress/integration/wagonType/wagonTypeCreate.spec.js +++ b/test/cypress/integration/wagonType/wagonTypeCreate.spec.js @@ -3,52 +3,15 @@ describe('WagonTypeCreate', () => { cy.viewport(1920, 1080); cy.login('developer'); cy.visit('/#/wagon/type/create'); + cy.waitForElement('.q-page', 6000); }); - function chooseColor(color) { - cy.get('div.shelving-down').eq(1).click(); - cy.get('div.q-color-picker__cube').eq(color).click(); - cy.get('div.q-card__section').find('button').click(); - } - - function addTray(position) { - cy.get('div.action-button').last().find('button').click(); - cy.focused().type(position); - cy.focused().blur(); - } - - it('should create and delete a new wagon type', () => { + it('should create a new wagon type', () => { + cy.get('.q-page-sticky > div > .q-btn').click(); cy.get('input').first().type('Example for testing'); - cy.get('div.q-checkbox__bg').click(); - chooseColor(1); - - // Insert invalid position (not minimal height) - addTray(20); - cy.get('div[role="alert"]').should('exist'); - chooseColor(2); - addTray(150); - chooseColor(3); - addTray(100); - - // Insert invalid position (max height reached) - addTray(210); - cy.get('div[role="alert"]').should('exist'); - - // Save cy.get('button[type="submit"]').click(); - - // Check data has been saved successfully - cy.get(':nth-child(1) > :nth-child(1) > .justify-between > .flex > .title') - .contains('Example for testing') - .click(); - cy.get('input').first().should('have.value', 'Example for testing'); - cy.get('div.wagon-tray').should('have.length', 4); - cy.get('div.position').eq(0).find('input').should('have.value', '150'); - cy.get('div.position').eq(1).find('input').should('have.value', '100'); - cy.get('div.position').eq(2).find('input').should('have.value', '50'); - - // Delete wagon type created - cy.go('back'); + }); + it('delete a wagon type', () => { cy.get( ':nth-child(2) > :nth-child(1) > .card-list-body > .actions > .q-btn--standard' ).click(); diff --git a/test/cypress/integration/wagonType/wagonTypeEdit.spec.js b/test/cypress/integration/wagonType/wagonTypeEdit.spec.js new file mode 100644 index 000000000..6e5816e51 --- /dev/null +++ b/test/cypress/integration/wagonType/wagonTypeEdit.spec.js @@ -0,0 +1,27 @@ +describe('WagonTypeEdit', () => { + const trayColorRow = + '.q-select > .q-field__inner > .q-field__control > .q-field__control-container'; + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit('/#/wagon/type/2/edit'); + }); + + it('should edit the name and the divisible field of the wagon type', () => { + cy.get('.q-card'); + cy.get('input').first().type(' changed'); + cy.get('div.q-checkbox__bg').first().click(); + cy.get('.q-btn--standard').click(); + }); + + it('should create a tray', () => { + cy.get('.action-button > .q-btn > .q-btn__content > .q-icon').click(); + cy.get('input').last().type('150'); + cy.get(trayColorRow).type('{downArrow}{downArrow}{enter}'); + }); + + it('should delete a tray', () => { + cy.get('.action-button > .q-btn > .q-btn__content > .q-icon').first().click(); + cy.reload(); + }); +}); diff --git a/test/cypress/integration/worker/workerCreate.spec.js b/test/cypress/integration/worker/workerCreate.spec.js index c1832ad67..50afe1892 100644 --- a/test/cypress/integration/worker/workerCreate.spec.js +++ b/test/cypress/integration/worker/workerCreate.spec.js @@ -2,6 +2,9 @@ describe('WorkerCreate', () => { const externalRadio = '.q-radio:nth-child(2)'; const notification = '.q-notification__message'; const developerBossId = 120; + const payMethodCross = + '.grid-create .full-width > :nth-child(9) .q-select .q-field__append:not(.q-anchor--skip)'; + const saveBtn = '.q-mt-lg > .q-btn--standard'; const internal = { Fi: { val: '78457139E' }, @@ -36,7 +39,8 @@ describe('WorkerCreate', () => { it('should throw an error if a pay method has not been selected', () => { cy.fillInForm(internal); - cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get(payMethodCross).click(); + cy.get(saveBtn).click(); cy.get(notification).should('contains.text', 'Payment method is required'); }); @@ -45,14 +49,14 @@ describe('WorkerCreate', () => { ...internal, 'Pay method': { val: 'PayMethod one', type: 'select' }, }); - cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get(saveBtn).click(); cy.get(notification).should('contains.text', 'Data created'); }); it('should create an external', () => { cy.get(externalRadio).click(); cy.fillInForm(external); - cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get(saveBtn).click(); cy.get(notification).should('contains.text', 'Data created'); }); }); diff --git a/test/cypress/integration/worker/workerList.spec.js b/test/cypress/integration/worker/workerList.spec.js index de57c9638..8a8bea443 100644 --- a/test/cypress/integration/worker/workerList.spec.js +++ b/test/cypress/integration/worker/workerList.spec.js @@ -1,4 +1,7 @@ describe('WorkerList', () => { + const inputName = '.q-drawer .q-form input[aria-label="First Name"]'; + const searchBtn = '.q-drawer button:nth-child(3)'; + const descriptorTitle = '.descriptor .title span'; beforeEach(() => { cy.viewport(1280, 720); cy.login('developer'); @@ -6,6 +9,11 @@ describe('WorkerList', () => { }); it('should open the worker summary', () => { - cy.get('.q-drawer .q-form input[aria-label="Name"]').type('jessica jones{enter}'); + cy.get(inputName).type('jessica{enter}'); + cy.get(searchBtn).click(); + cy.intercept('GET', /\/api\/Workers\/\d+/).as('worker'); + cy.wait('@worker').then(() => + cy.get(descriptorTitle).should('include.text', 'Jessica') + ); }); }); diff --git a/test/cypress/integration/worker/workerLocker.spec.js b/test/cypress/integration/worker/workerLocker.spec.js index 9a4066f54..8a169dfb2 100644 --- a/test/cypress/integration/worker/workerLocker.spec.js +++ b/test/cypress/integration/worker/workerLocker.spec.js @@ -1,12 +1,12 @@ describe('WorkerLocker', () => { - const workerId = 1109; + const productionId = 49; const lockerCode = '2F'; const input = '.q-card input'; const thirdOpt = '[role="listbox"] .q-item:nth-child(1)'; beforeEach(() => { cy.viewport(1280, 720); cy.login('productionBoss'); - cy.visit(`/#/worker/${workerId}/locker`); + cy.visit(`/#/worker/${productionId}/locker`); }); it('should allocates a locker', () => { diff --git a/test/cypress/integration/zone/zoneBasicData.spec.js b/test/cypress/integration/zone/zoneBasicData.spec.js new file mode 100644 index 000000000..c6151a49b --- /dev/null +++ b/test/cypress/integration/zone/zoneBasicData.spec.js @@ -0,0 +1,21 @@ +describe('ZoneBasicData', () => { + const notification = '.q-notification__message'; + + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit('/#/zone/4/basic-data'); + }); + + it('should throw an error if the name is empty', () => { + cy.get('.q-card > :nth-child(1)').clear(); + cy.get('.q-btn-group > .q-btn--standard').click(); + cy.get(notification).should('contains.text', "can't 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'); + }); +}); diff --git a/test/cypress/integration/zone/zoneCreate.spec.js b/test/cypress/integration/zone/zoneCreate.spec.js new file mode 100644 index 000000000..9618ea846 --- /dev/null +++ b/test/cypress/integration/zone/zoneCreate.spec.js @@ -0,0 +1,38 @@ +describe('ZoneCreate', () => { + const notification = '.q-notification__message'; + + const data = { + Name: { val: 'Zone pickup D' }, + Price: { val: '3' }, + Bonus: { val: '0' }, + 'Traveling days': { val: '0' }, + Warehouse: { val: 'Algemesi', type: 'select' }, + Volumetric: { val: 'true', type: 'checkbox' }, + }; + + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit('/#/zone/list'); + cy.get('.q-page-sticky > div > .q-btn').click(); + }); + + it('should throw an error if an agency has not been selected', () => { + cy.fillInForm({ + ...data, + }); + cy.get('input[aria-label="Close"]').type('10:00'); + cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get(notification).should('contains.text', 'Agency cannot be blank'); + }); + + it('should create a zone', () => { + cy.fillInForm({ + ...data, + Agency: { val: 'inhouse pickup', type: 'select' }, + }); + cy.get('input[aria-label="Close"]').type('10:00'); + cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get(notification).should('contains.text', 'Data created'); + }); +}); diff --git a/test/cypress/integration/zone/zoneList.spec.js b/test/cypress/integration/zone/zoneList.spec.js index f35da7e5f..92c77a2c6 100644 --- a/test/cypress/integration/zone/zoneList.spec.js +++ b/test/cypress/integration/zone/zoneList.spec.js @@ -1,15 +1,18 @@ describe('ZoneList', () => { beforeEach(() => { - cy.viewport(1920, 1080); + cy.viewport(1280, 720); cy.login('developer'); - cy.visit(`/#/zone/list`); + cy.visit('/#/zone/list'); }); - it('should open the details', () => { - cy.get(':nth-child(1) > .text-right > .material-symbols-outlined').click(); + it('should filter by agency', () => { + cy.get( + ':nth-child(1) > .column > .q-field > .q-field__inner > .q-field__control > .q-field__control-container' + ).type('{downArrow}{enter}'); }); - it('should redirect to summary', () => { - cy.waitForElement('.q-page'); - cy.get('tbody > :nth-child(1)').click(); + + it('should open the zone summary', () => { + cy.get('input[aria-label="Name"]').type('zone refund'); + cy.get('.q-scrollarea__content > .q-btn--standard > .q-btn__content').click(); }); }); diff --git a/test/cypress/integration/zone/zoneWarehouse.spec.js b/test/cypress/integration/zone/zoneWarehouse.spec.js new file mode 100644 index 000000000..3ffa3f69d --- /dev/null +++ b/test/cypress/integration/zone/zoneWarehouse.spec.js @@ -0,0 +1,34 @@ +describe('ZoneWarehouse', () => { + const data = { + Warehouse: { val: 'Algemesi', type: 'select' }, + }; + const deviceProductionField = + '.vn-row > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container'; + const dataError = "ER_DUP_ENTRY: Duplicate entry '2-2' for key 'zoneFk'"; + + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit(`/#/zone/2/warehouses`); + }); + + it('should throw an error if the warehouse chosen is already put in the zone', () => { + cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.get(deviceProductionField).click(); + cy.get(deviceProductionField).type('{upArrow}{enter}'); + cy.get('.q-notification__message').should('have.text', dataError); + }); + + it('should create a warehouse', () => { + cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.get(deviceProductionField).click(); + cy.fillInForm(data); + cy.get('.q-mt-lg > .q-btn--standard').click(); + }); + + it('should delete a warehouse', () => { + cy.get('tbody > :nth-child(2) > :nth-child(2) > .q-icon').click(); + cy.get('.q-card__actions > .q-btn--flat > .q-btn__content').click(); + cy.reload(); + }); +}); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 3cf909af5..a9a405313 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -105,6 +105,12 @@ Cypress.Commands.add('fillInForm', (obj, form = '.q-form > .q-card') => { case 'date': cy.wrap(el).type(val.split('-').join('')); break; + case 'time': + cy.wrap(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(); + break; default: cy.wrap(el).type(val); break; diff --git a/test/vitest/__tests__/composables/useAcl.spec.js b/test/vitest/__tests__/composables/useAcl.spec.js index a2b44b5e7..6cb29984c 100644 --- a/test/vitest/__tests__/composables/useAcl.spec.js +++ b/test/vitest/__tests__/composables/useAcl.spec.js @@ -48,40 +48,62 @@ describe('useAcl', () => { describe('hasAny', () => { it('should return false if no roles matched', async () => { - expect(acl.hasAny('Worker', 'updateAttributes', 'WRITE')).toBeFalsy(); + expect( + acl.hasAny([ + { model: 'Worker', props: 'updateAttributes', accessType: 'WRITE' }, + ]) + ).toBeFalsy(); }); it('should return false if no roles matched', async () => { - expect(acl.hasAny('Worker', 'holidays', 'READ')).toBeTruthy(); + expect( + acl.hasAny([{ model: 'Worker', props: 'holidays', accessType: 'READ' }]) + ).toBeTruthy(); }); describe('*', () => { it('should return true if an acl matched', async () => { - expect(acl.hasAny('Address', '*', 'WRITE')).toBeTruthy(); + expect( + acl.hasAny([{ model: 'Address', props: '*', accessType: 'WRITE' }]) + ).toBeTruthy(); }); it('should return false if no acls matched', async () => { - expect(acl.hasAny('Worker', '*', 'READ')).toBeFalsy(); + expect( + acl.hasAny([{ model: 'Worker', props: '*', accessType: 'READ' }]) + ).toBeFalsy(); }); }); describe('$authenticated', () => { it('should return false if no acls matched', async () => { - expect(acl.hasAny('Url', 'getByUser', '*')).toBeFalsy(); + expect( + acl.hasAny([{ model: 'Url', props: 'getByUser', accessType: '*' }]) + ).toBeFalsy(); }); it('should return true if an acl matched', async () => { - expect(acl.hasAny('Url', 'getByUser', 'READ')).toBeTruthy(); + expect( + acl.hasAny([{ model: 'Url', props: 'getByUser', accessType: 'READ' }]) + ).toBeTruthy(); }); }); describe('$everyone', () => { it('should return false if no acls matched', async () => { - expect(acl.hasAny('TpvTransaction', 'start', 'READ')).toBeFalsy(); + expect( + acl.hasAny([ + { model: 'TpvTransaction', props: 'start', accessType: 'READ' }, + ]) + ).toBeFalsy(); }); it('should return false if an acl matched', async () => { - expect(acl.hasAny('TpvTransaction', 'start', 'WRITE')).toBeTruthy(); + expect( + acl.hasAny([ + { model: 'TpvTransaction', props: 'start', accessType: 'WRITE' }, + ]) + ).toBeTruthy(); }); }); }); diff --git a/test/vitest/__tests__/pages/Login/Login.spec.js b/test/vitest/__tests__/pages/Login/Login.spec.js index 5ab4cee9e..e90a8ee53 100644 --- a/test/vitest/__tests__/pages/Login/Login.spec.js +++ b/test/vitest/__tests__/pages/Login/Login.spec.js @@ -44,10 +44,6 @@ describe('Login', () => { it('should not set the token into session if any error occurred', async () => { vi.spyOn(axios, 'post').mockReturnValue({ data: null }); - vi.spyOn(vm.quasar, 'notify'); - await vm.onSubmit(); - - expect(vm.quasar.notify).not.toHaveBeenCalled(); }); }); diff --git a/test/vitest/__tests__/pages/Wagons/WagonTypeCreate.spec.js b/test/vitest/__tests__/pages/Wagons/WagonTypeCreate.spec.js deleted file mode 100644 index 60c199b73..000000000 --- a/test/vitest/__tests__/pages/Wagons/WagonTypeCreate.spec.js +++ /dev/null @@ -1,271 +0,0 @@ -import { axios, createWrapper } from 'app/test/vitest/helper'; -import WagonTypeCreate from 'pages/Wagon/Type/WagonTypeCreate.vue'; -import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; - -describe('WagonTypeCreate', () => { - let vmCreate, vmEdit; - const entityId = 1; - - beforeAll(() => { - vmEdit = createWrapper(WagonTypeCreate, {propsData: { - id: entityId, - }}).vm; - vmCreate = createWrapper(WagonTypeCreate).vm; - vmEdit.wagonConfig = vmCreate.wagonConfig = {maxTrays: 2 ,minHeightBetweenTrays: 50, maxWagonHeight: 200 }; - vmEdit.wagonTypeColors = vmCreate.wagonTypeColors = [{id: 1, color:'white', rgb:'#000000'}]; - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - describe('addTray()', () => { - it('should throw message if there are uncomplete trays', async () => { - vi.spyOn(vmEdit.quasar, 'notify'); - vmEdit.wagon = [{ - id: 1, - position: null, - color: vmEdit.wagonTypeColors[0] - }]; - - await vmEdit.addTray(); - - expect(vmEdit.quasar.notify).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'warning', - }) - ); - }); - - it('should create a new tray if the limit has not been reached', async () => { - vmEdit.wagon = [{ - id: 1, - position: 0, - color: vmEdit.wagonTypeColors[0] - }]; - - await vmEdit.addTray(); - expect(vmEdit.wagon.length).toEqual(2); - }); - - it('should throw message if there are uncomplete trays', async () => { - vi.spyOn(vmEdit.quasar, 'notify'); - vmEdit.wagon = [{ - id: 1, - position: 0, - color: vmEdit.wagonTypeColors[0] - },{ - id: 2, - position: 50, - color: vmEdit.wagonTypeColors[0] - }]; - - await vmEdit.addTray(); - - expect(vmEdit.quasar.notify).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'warning', - }) - ); - }); - }); - - describe('deleteTray() reorderIds()', () => { - it('should delete a tray and reorder the ids', async () => { - const trayToDelete = { - id: 1, - position: 0, - color: vmEdit.wagonTypeColors[0] - }; - const trayMaintained = { - id: 2, - position: 50, - color: vmEdit.wagonTypeColors[0] - }; - vmEdit.wagon = [trayToDelete,trayMaintained]; - - await vmEdit.deleteTray(trayToDelete); - - expect(vmEdit.wagon.length).toEqual(1); - expect(vmEdit.wagon[0].id).toEqual(0); - expect(vmEdit.wagon[0].position).toEqual(50); - - }); - }); - - describe('onSubmit()', () => { - it('should make a patch to editWagonType if have id', async () => { - vi.spyOn(axios, 'patch').mockResolvedValue({ data: true }); - const wagon = { - id: entityId, - name: "Mock name", - divisible: true, - trays: [{ - id: 1, - position: 0, - color: vmEdit.wagonTypeColors[0] - }] - } - vmEdit.name = wagon.name; - vmEdit.divisible = wagon.divisible; - vmEdit.wagon = wagon.trays; - - await vmEdit.onSubmit(); - - expect(axios.patch).toHaveBeenCalledWith( - `WagonTypes/editWagonType`, wagon - ); - }); - - it('should make a patch to createtWagonType if not have id', async () => { - vi.spyOn(axios, 'patch').mockResolvedValue({ data: true }); - const wagon = { - name: "Mock name", - divisible: true, - trays: [{ - id: 1, - position: 0, - color: vmCreate.wagonTypeColors[0] - }] - } - vmCreate.name = wagon.name; - vmCreate.divisible = wagon.divisible; - vmCreate.wagon = wagon.trays; - - await vmCreate.onSubmit(); - - expect(axios.patch).toHaveBeenCalledWith( - `WagonTypes/createWagonType`, wagon - ); - }); - }); - - describe('onReset()', () => { - it('should reset if have id', async () => { - vmEdit.name = 'Changed name'; - vmEdit.divisible = false; - vmEdit.wagon = []; - vmEdit.originalData = { - name: 'Original name', - divisible: true, - trays: [{ - id: 1, - position: 0, - color: vmEdit.wagonTypeColors[0] - },{ - id: 2, - position: 50, - color: vmEdit.wagonTypeColors[0] - }] - }; - - vmEdit.onReset(); - - expect(vmEdit.name).toEqual(vmEdit.originalData.name); - expect(vmEdit.divisible).toEqual(vmEdit.originalData.divisible); - expect(vmEdit.wagon).toEqual(vmEdit.originalData.trays); - }); - - it('should reset if not have id', async () => { - vmCreate.name = 'Changed name'; - vmCreate.divisible = false; - vmCreate.wagon = []; - - vmCreate.onReset(); - - expect(vmCreate.name).toEqual(null); - expect(vmCreate.divisible).toEqual(false); - expect(vmCreate.wagon.length).toEqual(1); - }); - }); - - describe('onPositionBlur()', () => { - it('should set position null if position is negative', async () => { - const negativeTray = { - id: 1, - position: -1, - color: vmCreate.wagonTypeColors[0] - }; - - vmCreate.onPositionBlur(negativeTray); - - expect(negativeTray.position).toEqual(null); - }); - - it('should set position and reorder array', async () => { - const trays = [{ - id: 0, - position: 100, - color: vmCreate.wagonTypeColors[0] - },{ - id: 1, - position: 0, - color: vmCreate.wagonTypeColors[0] - }]; - const newTray = { - id: 2, - position: 50, - color: vmCreate.wagonTypeColors[0] - }; - trays.push(newTray); - vmCreate.wagon = trays; - - vmCreate.onPositionBlur(newTray); - - expect(vmCreate.wagon[0].position).toEqual(100); - expect(vmCreate.wagon[1].position).toEqual(50); - expect(vmCreate.wagon[2].position).toEqual(0); - }); - - it('should throw message if not have min height between trays and should set new adequate positions', async () => { - vi.spyOn(vmCreate.quasar, 'notify'); - const trays = [{ - id: 0, - position: 0, - color: vmCreate.wagonTypeColors[0] - }]; - const newTray = { - id: 1, - position: 20, - color: vmCreate.wagonTypeColors[0] - }; - trays.push(newTray); - vmCreate.wagon = trays; - - vmCreate.onPositionBlur(newTray); - - expect(vmCreate.wagon[0].position).toEqual(50); - expect(vmCreate.wagon[1].position).toEqual(0); - expect(vmCreate.quasar.notify).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'warning', - }) - ); - }); - - it('should throw message if max height has been exceed', async () => { - vi.spyOn(vmCreate.quasar, 'notify'); - const trays = [{ - id: 0, - position: 0, - color: vmCreate.wagonTypeColors[0] - }]; - const newTray = { - id: 1, - position: 210, - color: vmCreate.wagonTypeColors[0] - }; - trays.push(newTray); - vmCreate.wagon = trays; - - vmCreate.onPositionBlur(newTray); - - expect(vmCreate.wagon.length).toEqual(1); - expect(vmCreate.quasar.notify).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'warning', - }) - ); - }); - }); -});