diff --git a/package.json b/package.json index cdb185ba1..4668d2d56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "24.26.2", + "version": "24.28.1", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 426d99b9a..05c63d563 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -87,7 +87,7 @@ const $props = defineProps({ const emit = defineEmits(['onFetch', 'onDataSaved']); const modelValue = computed( () => $props.model ?? `formModel_${route?.meta?.title ?? route.name}` -); +).value; const componentIsRendered = ref(false); const arrayData = useArrayData(modelValue); const isLoading = ref(false); @@ -119,9 +119,10 @@ onMounted(async () => { // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla state.set(modelValue, $props.formInitialData); - if ($props.autoLoad && !$props.formInitialData && $props.url) await fetch(); - else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data); - + if (!$props.formInitialData) { + if ($props.autoLoad && $props.url) await fetch(); + else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data); + } if ($props.observeFormChanges) { watch( () => formData.value, @@ -245,7 +246,13 @@ function updateAndEmit(evt, val, res) { emit(evt, state.get(modelValue), res); } -defineExpose({ save, isLoading, hasChanges }); +defineExpose({ + save, + isLoading, + hasChanges, + reset, + fetch, +}); </script> <template> <div class="column items-center full-width"> diff --git a/src/components/VnTable/VnChip.vue b/src/components/VnTable/VnChip.vue index 2569eec37..74207b943 100644 --- a/src/components/VnTable/VnChip.vue +++ b/src/components/VnTable/VnChip.vue @@ -16,10 +16,11 @@ function stopEventPropagation(event) { } </script> <template> + <slot name="beforeChip" :row="row"></slot> <span v-for="col of columns" :key="col.name" - @click="stopEventPropagation($event)" + @click="stopEventPropagation" class="cursor-text" > <QChip diff --git a/src/components/VnTable/VnColumn.vue b/src/components/VnTable/VnColumn.vue index 6a52e0158..6cd62d83e 100644 --- a/src/components/VnTable/VnColumn.vue +++ b/src/components/VnTable/VnColumn.vue @@ -9,7 +9,7 @@ import VnInput from 'components/common/VnInput.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import VnComponent from 'components/common/VnComponent.vue'; -const model = defineModel(); +const model = defineModel(undefined, { required: true }); const $props = defineProps({ column: { type: Object, @@ -17,7 +17,7 @@ const $props = defineProps({ }, row: { type: Object, - required: true, + default: () => {}, }, default: { type: [Object, String], diff --git a/src/components/VnTable/VnFilter.vue b/src/components/VnTable/VnFilter.vue index c2fadbb95..3d489cf73 100644 --- a/src/components/VnTable/VnFilter.vue +++ b/src/components/VnTable/VnFilter.vue @@ -27,7 +27,7 @@ const $props = defineProps({ default: 'params', }, }); -const model = defineModel(); +const model = defineModel(undefined, { required: true }); const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl }); const columnFilter = computed(() => $props.column?.columnFilter); @@ -36,45 +36,44 @@ const enterEvent = { 'keyup.enter': () => addFilter(model.value), remove: () => addFilter(null), }; + +const defaultAttrs = { + filled: !$props.showTitle, + class: 'q-px-sm q-pb-xs q-pt-none', + dense: true, +}; + +const forceAttrs = { + label: $props.showTitle ? '' : $props.column.label, +}; + const components = { input: { component: markRaw(VnInput), event: enterEvent, attrs: { - class: 'q-px-sm q-pb-xs q-pt-none', - dense: true, - filled: !$props.showTitle, + ...defaultAttrs, clearable: true, }, - forceAttrs: { - label: $props.showTitle ? '' : $props.column.label, - }, + forceAttrs, }, number: { component: markRaw(VnInput), event: enterEvent, attrs: { - dense: true, - class: 'q-px-sm q-pb-xs q-pt-none', + ...defaultAttrs, clearable: true, - filled: !$props.showTitle, - }, - forceAttrs: { - label: $props.showTitle ? '' : $props.column.label, }, + forceAttrs, }, date: { component: markRaw(VnInputDate), event: updateEvent, attrs: { - dense: true, - class: 'q-px-sm q-pb-xs q-pt-none', - filled: !$props.showTitle, + ...defaultAttrs, style: 'min-width: 150px', }, - forceAttrs: { - label: $props.showTitle ? '' : $props.column.label, - }, + forceAttrs, }, checkbox: { component: markRaw(QCheckbox), @@ -84,9 +83,7 @@ const components = { class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs', 'toggle-indeterminate': true, }, - forceAttrs: { - label: $props.showTitle ? '' : $props.column.label, - }, + forceAttrs, }, select: { component: markRaw(VnSelect), @@ -96,9 +93,7 @@ const components = { dense: true, filled: !$props.showTitle, }, - forceAttrs: { - label: $props.showTitle ? '' : $props.column.label, - }, + forceAttrs, }, }; @@ -116,32 +111,36 @@ async function addFilter(value) { } function alignRow() { - if ($props.column.align == 'left') return 'justify-start items-start'; - if ($props.column.align == 'right') return 'justify-end items-end'; - return 'flex-center'; + switch ($props.column.align) { + case 'left': + return 'justify-start items-start'; + case 'right': + return 'justify-end items-end'; + default: + return 'flex-center'; + } } + +const showFilter = computed( + () => $props.column?.columnFilter !== false && $props.column.name != 'tableActions' +); </script> <template> <div - v-if="showTitle && column" + v-if="showTitle" class="q-pt-sm q-px-sm ellipsis" - :class="`text-${column.align ?? 'left'}`" + :class="`text-${column?.align ?? 'left'}`" + :style="!showFilter ? { 'min-height': 72 + 'px' } : ''" > - {{ column.label }} + {{ column?.label }} </div> - <div - v-if="columnFilter !== false && column.name != 'tableActions'" - class="full-width" - :class="alignRow()" - > + <div v-if="showFilter" class="full-width" :class="alignRow()"> <VnTableColumn :column="$props.column" - :row="{}" default="input" v-model="model" :components="components" component-prop="columnFilter" /> </div> - <div v-else-if="showTitle" style="height: 45px"><!-- fixme! --></div> </template> diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 262f85f96..493f1480e 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -66,7 +66,9 @@ const route = useRoute(); const router = useRouter(); const quasar = useQuasar(); -const mode = ref('card'); +const DEFAULT_MODE = 'card'; +const TABLE_MODE = 'table'; +const mode = ref(DEFAULT_MODE); const selected = ref([]); const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}'); const params = ref({ ...routeQuery, ...routeQuery.filter?.where }); @@ -77,17 +79,17 @@ const tableModes = [ { icon: 'view_column', title: t('table view'), - value: 'table', + value: TABLE_MODE, }, { icon: 'grid_view', title: t('grid view'), - value: 'card', + value: DEFAULT_MODE, }, ]; onMounted(() => { - mode.value = quasar.platform.is.mobile ? 'card' : $props.defaultMode; + mode.value = quasar.platform.is.mobile ? DEFAULT_MODE : $props.defaultMode; stateStore.rightDrawer = true; setUserParams(route.query[$props.searchUrl]); }); @@ -172,6 +174,9 @@ function columnName(col) { return name; } +function getColAlign(col) { + return 'text-' + (col.align ?? 'left') +} defineExpose({ reload, redirect: redirectFn, @@ -218,22 +223,22 @@ defineExpose({ :limit="20" ref="CrudModelRef" :search-url="searchUrl" - :disable-infinite-scroll="mode == 'table'" + :disable-infinite-scroll="mode == TABLE_MODE" @save-changes="reload" :has-subtoolbar="isEditable" > <template #body="{ rows }"> <QTable - v-bind="$attrs" + v-bind="$attrs['QTable']" class="vnTable" :columns="splittedColumns.columns" :rows="rows" v-model:selected="selected" - :grid="mode != 'table'" + :grid="mode != TABLE_MODE" table-header-class="bg-header" card-container-class="grid-three" flat - :style="mode == 'table' && 'max-height: 90vh'" + :style="mode == TABLE_MODE && 'max-height: 90vh'" virtual-scroll @virtual-scroll=" (event) => @@ -287,7 +292,7 @@ defineExpose({ <QTh auto-width class="sticky" /> </template> <template #body-cell-tableStatus="{ col, row }"> - <QTd auto-width :class="`text-${col.align ?? 'left'}`"> + <QTd auto-width :class="getColAlign(col)"> <VnTableChip :columns="splittedColumns.columnChips" :row="row" @@ -303,7 +308,7 @@ defineExpose({ <QTd auto-width class="no-margin q-px-xs" - :class="`text-${col.align ?? 'left'}`" + :class="getColAlign(col)" > <VnTableColumn :column="col" @@ -317,7 +322,7 @@ defineExpose({ <template #body-cell-tableActions="{ col, row }"> <QTd auto-width - :class="`text-${col.align ?? 'left'}`" + :class="getColAlign(col)" class="sticky no-padding" @click="stopEventPropagation($event)" > diff --git a/src/components/common/VnCard.vue b/src/components/common/VnCard.vue index 98c079239..8517525df 100644 --- a/src/components/common/VnCard.vue +++ b/src/components/common/VnCard.vue @@ -1,5 +1,5 @@ <script setup> -import { computed } from 'vue'; +import { onBeforeMount, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useArrayData } from 'src/composables/useArrayData'; import { useStateStore } from 'stores/useStateStore'; @@ -32,10 +32,15 @@ const url = computed(() => { return props.customUrl; }); -useArrayData(props.dataKey, { +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 }); +}); </script> <template> <QDrawer diff --git a/src/components/common/VnComponent.vue b/src/components/common/VnComponent.vue index 56b43a4db..318b5ee5f 100644 --- a/src/components/common/VnComponent.vue +++ b/src/components/common/VnComponent.vue @@ -1,7 +1,7 @@ <script setup> import { computed, defineModel } from 'vue'; -const model = defineModel(); +const model = defineModel(undefined, { required: true }); const $props = defineProps({ prop: { type: Object, diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index 26c4a9a73..07e82abed 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -83,7 +83,6 @@ const inputRules = [ <template v-if="$slots.prepend" #prepend> <slot name="prepend" /> </template> - <template #append> <slot name="append" v-if="$slots.append && !$attrs.disabled" /> <QIcon diff --git a/src/components/common/VnLog.vue b/src/components/common/VnLog.vue index 9d672bc3f..61436b7e8 100644 --- a/src/components/common/VnLog.vue +++ b/src/components/common/VnLog.vue @@ -1,5 +1,5 @@ <script setup> -import { ref } from 'vue'; +import { ref, onUnmounted } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import axios from 'axios'; @@ -376,6 +376,10 @@ async function clearFilter() { } setLogTree(); + +onUnmounted(() => { + stateStore.rightDrawer = false; +}); </script> <template> <FetchData diff --git a/src/components/ui/CardDescriptor.vue b/src/components/ui/CardDescriptor.vue index b83cca3f4..b2084479d 100644 --- a/src/components/ui/CardDescriptor.vue +++ b/src/components/ui/CardDescriptor.vue @@ -115,13 +115,13 @@ const emit = defineEmits(['onFetch']); </QBtn> </RouterLink> <QBtn + v-if="$slots.menu" color="white" dense flat icon="more_vert" round size="md" - :class="{ invisible: !$slots.menu }" > <QTooltip> {{ t('components.cardDescriptor.moreOptions') }} diff --git a/src/components/ui/CardSummary.vue b/src/components/ui/CardSummary.vue index ae9a43578..cab5b98be 100644 --- a/src/components/ui/CardSummary.vue +++ b/src/components/ui/CardSummary.vue @@ -159,9 +159,9 @@ function existSummary(routes) { margin-top: 2px; .label { color: var(--vn-label-color); - width: 8em; + width: 9em; overflow: hidden; - white-space: nowrap; + white-space: wrap; text-overflow: ellipsis; margin-right: 10px; flex-grow: 0; diff --git a/src/components/ui/VnConfirm.vue b/src/components/ui/VnConfirm.vue index f8715f685..0480650db 100644 --- a/src/components/ui/VnConfirm.vue +++ b/src/components/ui/VnConfirm.vue @@ -67,6 +67,7 @@ async function confirm() { </QCardSection> <QCardSection class="row items-center"> <span v-html="message"></span> + <slot name="customHTML"></slot> </QCardSection> <QCardActions align="right"> <QBtn diff --git a/src/components/ui/VnImg.vue b/src/components/ui/VnImg.vue new file mode 100644 index 000000000..37072a69e --- /dev/null +++ b/src/components/ui/VnImg.vue @@ -0,0 +1,60 @@ +<script setup> +import { ref, computed, onMounted } from 'vue'; +import { useSession } from 'src/composables/useSession'; + +const $props = defineProps({ + collection: { + type: [String, Number], + default: 'Images', + }, + size: { + type: String, + default: '200x200', + }, + zoomSize: { + type: String, + required: true, + default: 'lg', + }, + id: { + type: Boolean, + default: false, + }, +}); +const show = ref(false); +const token = useSession().getTokenMultimedia(); +const timeStamp = ref(`timestamp=${Date.now()}`); +const url = computed( + () => + `/api/${$props.collection}/catalog/${$props.size}/${$props.id}/download?access_token=${token}&${timeStamp.value}` +); +const emits = defineEmits(['refresh']); +const reload = (emit = false) => { + timeStamp.value = `timestamp=${Date.now()}`; +}; +defineExpose({ + reload, +}); + +onMounted(() => {}); +</script> +<template> + <QImg :src="url" v-bind="$attrs" @click="show = !show" spinner-color="primary" /> + <QDialog v-model="show" v-if="$props.zoomSize"> + <QImg :src="url" class="img_zoom" v-bind="$attrs" spinner-color="primary" /> + </QDialog> +</template> + +<style lang="scss" scoped> +.q-img { + cursor: zoom-in; +} +.rounded { + border-radius: 50%; +} +.img_zoom { + width: 100%; + height: auto; + border-radius: 0%; +} +</style> diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index cb6f002b7..e8575abbb 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -421,6 +421,7 @@ entry: buyingValue: Buying value freightValue: Freight value comissionValue: Commission value + description: Description packageValue: Package value isIgnored: Is ignored price2: Grouping @@ -998,6 +999,18 @@ route: shipped: Preparation date viewCmr: View CMR downloadCmrs: Download CMRs + columnLabels: + Id: Id + vehicle: Vehicle + description: Description + isServed: Served + worker: Worker + date: Date + started: Started + actions: Actions + agency: Agency + volume: Volume + finished: Finished supplier: pageTitles: suppliers: Suppliers diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index ecc06cb3d..7dc11c06e 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -107,6 +107,7 @@ globals: aliasUsers: Usuarios subRoles: Subroles inheritedRoles: Roles heredados + workers: Trabajadores created: Fecha creación worker: Trabajador now: Ahora @@ -419,6 +420,7 @@ entry: buyingValue: Coste freightValue: Porte comissionValue: Comisión + description: Descripción packageValue: Embalaje isIgnored: Ignorado price2: Grouping @@ -984,6 +986,18 @@ route: shipped: Fecha preparación viewCmr: Ver CMR downloadCmrs: Descargar CMRs + columnLabels: + Id: Id + vehicle: Vehículo + description: Descripción + isServed: Servida + worker: Trabajador + date: Fecha + started: Iniciada + actions: Acciones + agency: Agencia + volume: Volumen + finished: Finalizada supplier: pageTitles: suppliers: Proveedores diff --git a/src/pages/Account/AccountCreate.vue b/src/pages/Account/AccountCreate.vue new file mode 100644 index 000000000..1c0f9fee6 --- /dev/null +++ b/src/pages/Account/AccountCreate.vue @@ -0,0 +1,81 @@ +<script setup> +import { reactive, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRouter } from 'vue-router'; + +import FormModelPopup from 'components/FormModelPopup.vue'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import FetchData from 'components/FetchData.vue'; +import VnInput from 'src/components/common/VnInput.vue'; + +const { t } = useI18n(); +const router = useRouter(); + +const newAccountForm = reactive({ + active: true, +}); +const rolesOptions = ref([]); + +const redirectToAccountBasicData = (_, { id }) => { + router.push({ name: 'AccountBasicData', params: { id } }); +}; +</script> + +<template> + <FetchData + url="VnRoles" + :filter="{ fields: ['id', 'name'], order: 'name ASC' }" + @on-fetch="(data) => (rolesOptions = data)" + auto-load + /> + <FormModelPopup + :title="t('account.card.newUser')" + url-create="VnUsers" + model="users" + :form-initial-data="newAccountForm" + @on-data-saved="redirectToAccountBasicData" + > + <template #form-inputs="{ data, validate }"> + <div class="column q-gutter-sm"> + <VnInput + v-model="data.name" + :label="t('account.create.name')" + :rules="validate('VnUser.name')" + /> + <VnInput + v-model="data.nickname" + :label="t('account.create.nickname')" + :rules="validate('VnUser.nickname')" + /> + <VnInput + v-model="data.email" + :label="t('account.create.email')" + type="email" + :rules="validate('VnUser.email')" + /> + <VnSelect + :label="t('account.create.role')" + v-model="data.roleFk" + :options="rolesOptions" + option-value="id" + option-label="name" + map-options + hide-selected + :rules="validate('VnUser.roleFk')" + /> + <VnInput + v-model="data.password" + :label="t('account.create.password')" + type="password" + :rules="validate('VnUser.password')" + /> + <QCheckbox + :label="t('account.create.active')" + v-model="data.active" + :toggle-indeterminate="false" + :rules="validate('VnUser.active')" + /> + </div> + </template> + </FormModelPopup> +</template> diff --git a/src/pages/Account/AccountFilter.vue b/src/pages/Account/AccountFilter.vue new file mode 100644 index 000000000..784c925bc --- /dev/null +++ b/src/pages/Account/AccountFilter.vue @@ -0,0 +1,87 @@ +<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 VnInput from 'src/components/common/VnInput.vue'; + +const { t } = useI18n(); +const props = defineProps({ + dataKey: { + type: String, + required: true, + }, + exprBuilder: { + type: Function, + default: null, + }, +}); + +const rolesOptions = ref([]); +</script> + +<template> + <FetchData + url="VnRoles" + :filter="{ fields: ['id', 'name'], order: 'name ASC' }" + @on-fetch="(data) => (rolesOptions = data)" + auto-load + /> + <VnFilterPanel + :data-key="props.dataKey" + :search-button="true" + :hidden-tags="['search']" + :redirect="false" + > + <template #tags="{ tag, formatFn }"> + <div class="q-gutter-x-xs"> + <strong>{{ t(`account.card.${tag.label}`) }}: </strong> + <span>{{ formatFn(tag.value) }}</span> + </div> + </template> + <template #body="{ params, searchFn }"> + <QItem class="q-my-sm"> + <QItemSection> + <VnInput + :label="t('account.card.name')" + v-model="params.name" + lazy-rules + is-outlined + /> + </QItemSection> + </QItem> + <QItem class="q-my-sm"> + <QItemSection> + <VnInput + :label="t('account.card.alias')" + v-model="params.nickname" + lazy-rules + is-outlined + /> + </QItemSection> + </QItem> + <QItem class="q-mb-sm"> + <QItemSection> + <VnSelect + :label="t('account.card.role')" + v-model="params.roleFk" + @update:model-value="searchFn()" + :options="rolesOptions" + option-value="id" + option-label="name" + emit-value + map-options + use-input + hide-selected + dense + outlined + rounded + :input-debounce="0" + /> + </QItemSection> + </QItem> + </template> + </VnFilterPanel> +</template> diff --git a/src/pages/Account/AccountList.vue b/src/pages/Account/AccountList.vue index 4cf27607a..dee019fed 100644 --- a/src/pages/Account/AccountList.vue +++ b/src/pages/Account/AccountList.vue @@ -1 +1,144 @@ -<template>Account list</template> +<script setup> +import { useI18n } from 'vue-i18n'; +import { useRouter } from 'vue-router'; +import { computed, ref } from 'vue'; + +import VnPaginate from 'src/components/ui/VnPaginate.vue'; +import VnSearchbar from 'components/ui/VnSearchbar.vue'; +import VnLv from 'src/components/ui/VnLv.vue'; +import CardList from 'src/components/ui/CardList.vue'; +import AccountSummary from './Card/AccountSummary.vue'; +import AccountFilter from './AccountFilter.vue'; +import AccountCreate from './AccountCreate.vue'; + +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import { useStateStore } from 'stores/useStateStore'; +import { useRole } from 'src/composables/useRole'; +import { QDialog } from 'quasar'; + +const stateStore = useStateStore(); +const router = useRouter(); +const { t } = useI18n(); +const { viewSummary } = useSummaryDialog(); +const accountCreateDialogRef = ref(null); +const showNewUserBtn = computed(() => useRole().hasAny(['itManagement'])); + +const filter = { + fields: ['id', 'nickname', 'name', 'role'], + include: { relation: 'role', scope: { fields: ['id', 'name'] } }, +}; + +const exprBuilder = (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { + or: [ + { name: { like: `%${value}%` } }, + { nickname: { like: `%${value}%` } }, + ], + }; + case 'name': + case 'nickname': + return { [param]: { like: `%${value}%` } }; + case 'roleFk': + return { [param]: value }; + } +}; + +const getApiUrl = () => new URL(window.location).origin; + +const navigate = (event, id) => { + if (event.ctrlKey || event.metaKey) + return window.open(`${getApiUrl()}/#/account/${id}/summary`); + router.push({ path: `/account/${id}` }); +}; + +const openCreateModal = () => accountCreateDialogRef.value.show(); +</script> + +<template> + <template v-if="stateStore.isHeaderMounted()"> + <Teleport to="#searchbar"> + <VnSearchbar + data-key="AccountList" + url="VnUsers/preview" + :expr-builder="exprBuilder" + :label="t('account.search')" + :info="t('account.searchInfo')" + /> + </Teleport> + <Teleport to="#actions-append"> + <div class="row q-gutter-x-sm"> + <QBtn + flat + @click="stateStore.toggleRightDrawer()" + round + dense + icon="menu" + > + <QTooltip bottom anchor="bottom right"> + {{ t('globals.collapseMenu') }} + </QTooltip> + </QBtn> + </div> + </Teleport> + </template> + <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> + <QScrollArea class="fit text-grey-8"> + <AccountFilter data-key="AccountList" :expr-builder="exprBuilder" /> + </QScrollArea> + </QDrawer> + <QPage class="column items-center q-pa-md"> + <div class="vn-card-list"> + <VnPaginate + :filter="filter" + data-key="AccountList" + url="VnUsers/preview" + auto-load + > + <template #body="{ rows }"> + <CardList + v-for="row of rows" + :id="row.id" + :key="row.id" + :title="row.nickname" + @click="navigate($event, row.id)" + > + <template #list-items> + <VnLv :label="t('account.card.name')" :value="row.nickname"> + </VnLv> + <VnLv + :label="t('account.card.nickname')" + :value="row.username" + > + </VnLv> + </template> + <template #actions> + <QBtn + :label="t('components.smartCard.openSummary')" + @click.stop="viewSummary(row.id, AccountSummary)" + color="primary" + style="margin-top: 15px" + /> + </template> + </CardList> + </template> + </VnPaginate> + </div> + <QDialog + ref="accountCreateDialogRef" + transition-hide="scale" + transition-show="scale" + > + <AccountCreate /> + </QDialog> + <QPageSticky :offset="[20, 20]" v-if="showNewUserBtn"> + <QBtn @click="openCreateModal" color="primary" fab icon="add" /> + <QTooltip class="text-no-wrap"> + {{ t('account.card.newUser') }} + </QTooltip> + </QPageSticky> + </QPage> +</template> diff --git a/src/pages/Account/Card/AccountBasicData.vue b/src/pages/Account/Card/AccountBasicData.vue new file mode 100644 index 000000000..3a9d5c9bf --- /dev/null +++ b/src/pages/Account/Card/AccountBasicData.vue @@ -0,0 +1,48 @@ +<script setup> +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; +import VnSelect from 'src/components/common/VnSelect.vue'; +import FormModel from 'components/FormModel.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import { ref, watch } from 'vue'; + +const route = useRoute(); +const { t } = useI18n(); +const formModelRef = ref(null); + +const accountFilter = { + where: { id: route.params.id }, + fields: ['id', 'email', 'nickname', 'name', 'accountStateFk', 'packages', 'pickup'], + include: [], +}; + +watch( + () => route.params.id, + () => formModelRef.value.reset() +); +</script> +<template> + <FormModel + ref="formModelRef" + :url="`VnUsers/preview`" + :url-update="`VnUsers/${route.params.id}/update-user`" + :filter="accountFilter" + model="Accounts" + auto-load + @on-data-saved="formModelRef.fetch()" + > + <template #form="{ data }"> + <div class="q-gutter-y-sm"> + <VnInput v-model="data.name" :label="t('account.card.nickname')" /> + <VnInput v-model="data.nickname" :label="t('account.card.alias')" /> + <VnInput v-model="data.email" :label="t('account.card.email')" /> + <VnSelect + v-model="data.lang" + :options="['es', 'en']" + :label="t('account.card.lang')" + /> + </div> + </template> + </FormModel> +</template> diff --git a/src/pages/Account/Card/AccountCard.vue b/src/pages/Account/Card/AccountCard.vue new file mode 100644 index 000000000..e4db3ee2b --- /dev/null +++ b/src/pages/Account/Card/AccountCard.vue @@ -0,0 +1,34 @@ +<script setup> +import { computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; + +import VnCard from 'components/common/VnCard.vue'; +import AccountDescriptor from './AccountDescriptor.vue'; + +const { t } = useI18n(); +const route = useRoute(); + +const routeName = computed(() => route.name); +const customRouteRedirectName = computed(() => routeName.value); +const searchBarDataKeys = { + AccountSummary: 'AccountSummary', + AccountInheritedRoles: 'AccountInheritedRoles', + AccountMailForwarding: 'AccountMailForwarding', + AccountMailAlias: 'AccountMailAlias', + AccountPrivileges: 'AccountPrivileges', + AccountLog: 'AccountLog', +}; +</script> + +<template> + <VnCard + data-key="Account" + :descriptor="AccountDescriptor" + :search-data-key="searchBarDataKeys[routeName]" + :search-custom-route-redirect="customRouteRedirectName" + :search-redirect="!!customRouteRedirectName" + :searchbar-label="t('account.search')" + :searchbar-info="t('account.searchInfo')" + /> +</template> diff --git a/src/pages/Account/Card/AccountDescriptor.vue b/src/pages/Account/Card/AccountDescriptor.vue new file mode 100644 index 000000000..2ff8afa33 --- /dev/null +++ b/src/pages/Account/Card/AccountDescriptor.vue @@ -0,0 +1,134 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; +import CardDescriptor from 'components/ui/CardDescriptor.vue'; +import VnLv from 'src/components/ui/VnLv.vue'; +import useCardDescription from 'src/composables/useCardDescription'; +import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; +import { useSession } from 'src/composables/useSession'; +import FetchData from 'src/components/FetchData.vue'; + +const $props = defineProps({ + id: { + type: Number, + required: false, + default: null, + }, +}); + +const route = useRoute(); +const { t } = useI18n(); +const { getTokenMultimedia } = useSession(); +const entityId = computed(() => { + return $props.id || route.params.id; +}); +const data = ref(useCardDescription()); +const setData = (entity) => (data.value = useCardDescription(entity.nickname, entity.id)); + +const filter = { + where: { id: entityId }, + fields: ['id', 'nickname', 'name', 'role'], + include: { relation: 'role', scope: { fields: ['id', 'name'] } }, +}; +function getAccountAvatar() { + const token = getTokenMultimedia(); + return `/api/Images/user/160x160/${entityId.value}/download?access_token=${token}`; +} +const hasAccount = ref(false); +</script> + +<template> + <FetchData + :url="`Accounts/${entityId}/exists`" + auto-load + @on-fetch="(data) => (hasAccount = data.exists)" + /> + <CardDescriptor + ref="descriptor" + :url="`VnUsers/preview`" + :filter="filter" + module="Account" + @on-fetch="setData" + data-key="AccountId" + :title="data.title" + :subtitle="data.subtitle" + > + <template #header-extra-action> + <QBtn + round + flat + size="md" + color="white" + icon="face" + :to="{ name: 'AccountList' }" + > + <QTooltip> + {{ t('Go to module index') }} + </QTooltip> + </QBtn> + </template> + <template #menu> + <AccountDescriptorMenu :has-account="hasAccount" /> + </template> + <template #before> + <QImg :src="getAccountAvatar()" 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('account.imageNotFound') }} + </div> + </div> + </div> + </template> + </QImg> + </template> + <template #body="{ entity }"> + <VnLv :label="t('account.card.nickname')" :value="entity.nickname" /> + <VnLv :label="t('account.card.role')" :value="entity.role.name" /> + </template> + <template #actions="{ entity }"> + <QCardActions class="q-gutter-x-md"> + <QIcon + v-if="!entity.active" + color="primary" + name="vn:disabled" + flat + round + size="sm" + class="fill-icon" + > + <QTooltip>{{ t('account.card.deactivated') }}</QTooltip> + </QIcon> + <QIcon + color="primary" + name="contact_mail" + v-if="entity.hasAccount" + flat + round + size="sm" + class="fill-icon" + > + <QTooltip>{{ t('account.card.enabled') }}</QTooltip> + </QIcon> + </QCardActions> + </template> + </CardDescriptor> +</template> +<style scoped> +.q-item__label { + margin-top: 0; +} +</style> +<i18n> + en: + accountRate: Claming rate + es: + accountRate: Ratio de reclamación +</i18n> diff --git a/src/pages/Account/Card/AccountDescriptorMenu.vue b/src/pages/Account/Card/AccountDescriptorMenu.vue new file mode 100644 index 000000000..60510394d --- /dev/null +++ b/src/pages/Account/Card/AccountDescriptorMenu.vue @@ -0,0 +1,187 @@ +<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'; + +const quasar = useQuasar(); +const $props = defineProps({ + hasAccount: { + type: Boolean, + default: false, + required: true, + }, +}); +const { t } = useI18n(); +const { hasAccount } = toRefs($props); +const { openConfirmationModal } = useVnConfirm(); +const route = useRoute(); + +const account = computed(() => useArrayData('AccountId').store.data[0]); +account.value.hasAccount = hasAccount.value; +const entityId = computed(() => +route.params.id); + +async function updateStatusAccount(active) { + if (active) { + await axios.post(`Accounts`, { id: entityId.value }); + } else { + await axios.delete(`Accounts/${entityId.value}`); + } + + account.value.hasAccount = active; + const status = active ? 'enable' : 'disable'; + quasar.notify({ + message: t(`account.card.${status}Account.success`), + type: 'positive', + }); +} +async function updateStatusUser(active) { + await axios.patch(`VnUsers/${entityId.value}`, { active }); + account.value.active = active; + const status = active ? 'activate' : 'deactivate'; + quasar.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); +async function sync() { + const params = { force: true }; + if (shouldSyncPassword.value) params.password = syncPassword.value; + await axios.patch(`Accounts/${account.value.name}/sync`, { + params, + }); + quasar.notify({ + message: t('account.card.actions.sync.success'), + type: 'positive', + }); +} +</script> +<template> + <VnConfirm + v-model="showSyncDialog" + :message="t('account.card.actions.sync.message')" + :title="t('account.card.actions.sync.title')" + :promise="sync" + > + <template #customHTML> + {{ shouldSyncPassword }} + <QCheckbox + :label="t('account.card.actions.sync.checkbox')" + v-model="shouldSyncPassword" + class="full-width" + clearable + clear-icon="close" + > + <QIcon style="padding-left: 10px" color="primary" name="info" size="sm"> + <QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip> + </QIcon></QCheckbox + > + <QInput + v-if="shouldSyncPassword" + :label="t('login.password')" + v-model="syncPassword" + class="full-width" + clearable + clear-icon="close" + type="password" + /> + </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 + clickable + @click=" + openConfirmationModal( + t('account.card.actions.disableAccount.title'), + t('account.card.actions.disableAccount.subtitle'), + () => updateStatusAccount(false) + ) + " + > + <QItemSection>{{ t('account.card.actions.disableAccount.name') }}</QItemSection> + </QItem> + + <QItem + v-if="!account.active" + v-ripple + clickable + @click=" + openConfirmationModal( + t('account.card.actions.activateUser.title'), + t('account.card.actions.activateUser.title'), + () => updateStatusUser(true) + ) + " + > + <QItemSection>{{ t('account.card.actions.activateUser.name') }}</QItemSection> + </QItem> + <QItem + v-if="account.active" + v-ripple + clickable + @click=" + openConfirmationModal( + t('account.card.actions.deactivateUser.title'), + t('account.card.actions.deactivateUser.title'), + () => updateStatusUser(false) + ) + " + > + <QItemSection>{{ t('account.card.actions.deactivateUser.name') }}</QItemSection> + </QItem> + <QItem v-ripple clickable @click="showSyncDialog = true"> + <QItemSection>{{ t('account.card.actions.sync.name') }}</QItemSection> + </QItem> + + <QSeparator /> + <QItem + @click=" + openConfirmationModal( + t('account.card.actions.delete.title'), + t('account.card.actions.delete.subTitle'), + removeAccount + ) + " + 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/AccountInheritedRoles.vue b/src/pages/Account/Card/AccountInheritedRoles.vue new file mode 100644 index 000000000..530a35449 --- /dev/null +++ b/src/pages/Account/Card/AccountInheritedRoles.vue @@ -0,0 +1,7 @@ +<script setup> +import InheritedRoles from '../InheritedRoles.vue'; +</script> + +<template> + <InheritedRoles data-key="AccountInheritedRoles" /> +</template> diff --git a/src/pages/Account/Card/AccountLog.vue b/src/pages/Account/Card/AccountLog.vue new file mode 100644 index 000000000..0f6cfb93f --- /dev/null +++ b/src/pages/Account/Card/AccountLog.vue @@ -0,0 +1,6 @@ +<script setup> +import VnLog from 'src/components/common/VnLog.vue'; +</script> +<template> + <VnLog model="User" /> +</template> diff --git a/src/pages/Account/Card/AccountMailAlias.vue b/src/pages/Account/Card/AccountMailAlias.vue new file mode 100644 index 000000000..99ce3ab22 --- /dev/null +++ b/src/pages/Account/Card/AccountMailAlias.vue @@ -0,0 +1,187 @@ +<script setup> +import { computed, ref, watch, onMounted, nextTick } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; + +import VnPaginate from 'components/ui/VnPaginate.vue'; +import AccountMailAliasCreateForm from './AccountMailAliasCreateForm.vue'; + +import { useVnConfirm } from 'composables/useVnConfirm'; +import { useArrayData } from 'composables/useArrayData'; +import useNotify from 'src/composables/useNotify.js'; +import axios from 'axios'; + +const { t } = useI18n(); +const route = useRoute(); +const { openConfirmationModal } = useVnConfirm(); +const { notify } = useNotify(); + +const paginateRef = ref(null); +const createMailAliasDialogRef = ref(null); + +const arrayData = useArrayData('AccountMailAliases'); +const store = arrayData.store; + +const loading = ref(false); +const hasAccount = ref(false); +const data = computed(() => { + const dataCopy = store.data; + return dataCopy.sort((a, b) => a.alias?.alias.localeCompare(b.alias?.alias)); +}); + +const filter = computed(() => ({ + where: { account: route.params.id }, + include: { + relation: 'alias', + scope: { + fields: ['id', 'alias', 'description'], + }, + }, +})); + +const urlPath = 'MailAliasAccounts'; + +const columns = computed(() => [ + { + name: 'name', + }, + { + name: 'action', + }, +]); + +const fetchAccountExistence = async () => { + try { + const { data } = await axios.get(`Accounts/${route.params.id}/exists`); + return data.exists; + } catch (error) { + console.error('Error fetching account existence', error); + return false; + } +}; + +const deleteMailAlias = async (row) => { + try { + await axios.delete(`${urlPath}/${row.id}`); + fetchMailAliases(); + notify(t('Unsubscribed from alias!'), 'positive'); + } catch (error) { + console.error(error); + } +}; + +const createMailAlias = async (mailAliasFormData) => { + try { + await axios.post(urlPath, mailAliasFormData); + notify(t('Subscribed to alias!'), 'positive'); + fetchMailAliases(); + } catch (error) { + console.error(error); + } +}; + +const fetchMailAliases = async () => { + await nextTick(); + paginateRef.value.fetch(); +}; + +const getAccountData = async () => { + loading.value = true; + hasAccount.value = await fetchAccountExistence(); + if (!hasAccount.value) { + loading.value = false; + store.data = []; + return; + } + await fetchMailAliases(); + loading.value = false; +}; + +const openCreateMailAliasForm = () => createMailAliasDialogRef.value.show(); + +watch( + () => route.params.id, + () => { + store.url = urlPath; + store.filter = filter.value; + getAccountData(); + } +); + +onMounted(async () => await getAccountData()); +</script> + +<template> + <QPage class="column items-center q-pa-md"> + <div class="full-width" style="max-width: 400px"> + <QSpinner v-if="loading" color="primary" size="md" /> + <VnPaginate + ref="paginateRef" + data-key="AccountMailAliases" + :filter="filter" + :url="urlPath" + auto-load + > + <template #body="{ rows }"> + <QTable + v-if="hasAccount && !loading" + :rows="data" + :columns="columns" + hide-header + > + <template #body="{ row, rowIndex }"> + <QTr> + <QTd> + <div class="column"> + <span>{{ row.alias?.alias }}</span> + <span class="color-vn-label">{{ + row.alias?.description + }}</span> + </div> + </QTd> + <QTd style="width: 50px !important"> + <QIcon + name="delete" + size="sm" + class="cursor-pointer" + color="primary" + @click.stop.prevent=" + openConfirmationModal( + t('User will be removed from alias'), + t('¿Seguro que quieres continuar?'), + () => deleteMailAlias(row, rows, rowIndex) + ) + " + > + <QTooltip> + {{ t('globals.delete') }} + </QTooltip> + </QIcon> + </QTd> + </QTr> + </template> + </QTable> + </template> + </VnPaginate> + <h5 v-if="!hasAccount" class="text-center"> + {{ t('account.mailForwarding.accountNotEnabled') }} + </h5> + </div> + <QDialog ref="createMailAliasDialogRef"> + <AccountMailAliasCreateForm @on-submit-create-alias="createMailAlias" /> + </QDialog> + <QPageSticky position="bottom-right" :offset="[18, 18]"> + <QBtn fab icon="add" color="primary" @click="openCreateMailAliasForm()"> + <QTooltip>{{ t('warehouses.add') }}</QTooltip> + </QBtn> + </QPageSticky> + </QPage> +</template> + +<i18n> +es: + Unsubscribed from alias!: ¡Desuscrito del alias! + Subscribed to alias!: ¡Suscrito al alias! + User will be removed from alias: El usuario será borrado del alias + Are you sure you want to continue?: ¿Seguro que quieres continuar? +</i18n> diff --git a/src/pages/Account/Card/AccountMailAliasCreateForm.vue b/src/pages/Account/Card/AccountMailAliasCreateForm.vue new file mode 100644 index 000000000..8f6d57091 --- /dev/null +++ b/src/pages/Account/Card/AccountMailAliasCreateForm.vue @@ -0,0 +1,51 @@ +<script setup> +import { reactive, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; + +import VnSelect from 'src/components/common/VnSelect.vue'; +import FetchData from 'components/FetchData.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import FormPopup from 'components/FormPopup.vue'; + +const emit = defineEmits(['onSubmitCreateAlias']); + +const { t } = useI18n(); +const route = useRoute(); + +const aliasFormData = reactive({ + mailAlias: null, + account: route.params.id, +}); + +const aliasOptions = ref([]); +</script> + +<template> + <FetchData + url="MailAliases" + :filter="{ fields: ['id', 'alias'], order: 'alias ASC' }" + auto-load + @on-fetch="(data) => (aliasOptions = data)" + /> + <FormPopup + model="ZoneWarehouse" + @on-submit="emit('onSubmitCreateAlias', aliasFormData)" + > + <template #form-inputs> + <VnRow class="row q-gutter-md q-mb-md"> + <div class="col"> + <VnSelect + :label="t('account.card.alias')" + v-model="aliasFormData.mailAlias" + :options="aliasOptions" + option-value="id" + option-label="alias" + hide-selected + :required="true" + /> + </div> + </VnRow> + </template> + </FormPopup> +</template> diff --git a/src/pages/Account/Card/AccountMailForwarding.vue b/src/pages/Account/Card/AccountMailForwarding.vue new file mode 100644 index 000000000..24b30f2b3 --- /dev/null +++ b/src/pages/Account/Card/AccountMailForwarding.vue @@ -0,0 +1,159 @@ +<script setup> +import { ref, onMounted, watch, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; + +import VnInput from 'src/components/common/VnInput.vue'; +import VnRow from 'components/ui/VnRow.vue'; + +import axios from 'axios'; +import { useStateStore } from 'stores/useStateStore'; +import useNotify from 'src/composables/useNotify.js'; + +const { t } = useI18n(); +const route = useRoute(); +const stateStore = useStateStore(); +const { notify } = useNotify(); + +const initialData = ref({}); +const formData = ref({ + forwardTo: null, + account: null, +}); + +const hasAccount = ref(false); +const hasData = ref(false); +const loading = ref(false); +const hasDataChanged = computed( + () => + formData.value.forwardTo !== initialData.value.forwardTo || + initialData.value.hasData !== hasData.value +); + +const fetchAccountExistence = async () => { + try { + const { data } = await axios.get(`Accounts/${route.params.id}/exists`); + return data.exists; + } catch (error) { + console.error('Error fetching account existence', error); + return false; + } +}; + +const fetchMailForwards = async () => { + try { + const response = await axios.get(`MailForwards/${route.params.id}`); + return response.data; + } catch (err) { + console.error('Error fetching mail forwards', err); + return null; + } +}; + +const deleteMailForward = async () => { + try { + await axios.delete(`MailForwards/${route.params.id}`); + formData.value.forwardTo = null; + initialData.value.forwardTo = null; + initialData.value.hasData = hasData.value; + notify(t('globals.dataSaved'), 'positive'); + } catch (err) { + console.error('Error deleting mail forward', err); + } +}; + +const updateMailForward = async () => { + try { + await axios.patch('MailForwards', formData.value); + initialData.value = { ...formData.value }; + initialData.value.hasData = hasData.value; + } catch (err) { + console.error('Error creating mail forward', err); + } +}; + +const onSubmit = async () => { + if (hasData.value) await updateMailForward(); + else await deleteMailForward(); +}; + +const setInitialData = async () => { + loading.value = true; + initialData.value.account = route.params.id; + formData.value.account = route.params.id; + hasAccount.value = await fetchAccountExistence(route.params.id); + if (!hasAccount.value) { + loading.value = false; + return; + } + + const result = await fetchMailForwards(route.params.id); + const forwardTo = result ? result.forwardTo : null; + formData.value.forwardTo = forwardTo; + initialData.value.forwardTo = forwardTo; + + initialData.value.hasData = hasAccount.value && !!forwardTo; + hasData.value = hasAccount.value && !!forwardTo; + loading.value = false; +}; + +watch( + () => route.params.id, + () => setInitialData() +); + +onMounted(async () => await setInitialData()); +</script> +<template> + <div class="flex justify-center"> + <QSpinner v-if="loading" color="primary" size="md" /> + <QForm + v-else-if="hasAccount" + @submit="onSubmit()" + class="full-width" + style="max-width: 800px" + > + <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> + <div> + <QBtnGroup push class="q-gutter-x-sm"> + <slot name="moreActions" /> + <QBtn + color="primary" + icon="restart_alt" + flat + @click="reset()" + :label="t('globals.reset')" + /> + <QBtn + color="primary" + icon="save" + @click="onSubmit()" + :disable="!hasDataChanged" + :label="t('globals.save')" + /> + </QBtnGroup> + </div> + </Teleport> + <QCard class="q-pa-lg"> + <VnRow class="row q-mb-md"> + <QCheckbox + v-model="hasData" + :label="t('account.mailForwarding.enableMailForwarding')" + :toggle-indeterminate="false" + /> + </VnRow> + <VnRow v-if="hasData" class="row q-gutter-md q-mb-md"> + <VnInput + v-model="formData.forwardTo" + :label="t('account.mailForwarding.forwardingMail')" + :info="t('account.mailForwarding.mailInputInfo')" + > + </VnInput> + </VnRow> + </QCard> + </QForm> + <h5 v-else class="text-center"> + {{ t('account.mailForwarding.accountNotEnabled') }} + </h5> + </div> +</template> diff --git a/src/pages/Account/Card/AccountPrivileges.vue b/src/pages/Account/Card/AccountPrivileges.vue new file mode 100644 index 000000000..f1f24f19b --- /dev/null +++ b/src/pages/Account/Card/AccountPrivileges.vue @@ -0,0 +1,49 @@ +<script setup> +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 VnSelect from 'src/components/common/VnSelect.vue'; + +const { t } = useI18n(); +const route = useRoute(); + +const rolesOptions = ref([]); +const formModelRef = ref(); +</script> +<template> + <FetchData + url="VnRoles" + :filter="{ fields: ['id', 'name'], order: 'name ASC' }" + auto-load + @on-fetch="(data) => (rolesOptions = data)" + /> + <FormModel + ref="formModelRef" + model="AccountPrivileges" + :url="`VnUsers/${route.params.id}`" + :url-create="`VnUsers/${route.params.id}/privileges`" + auto-load + @on-data-saved="formModelRef.fetch()" + > + <template #form="{ data }"> + <div class="q-gutter-y-sm"> + <QCheckbox + v-model="data.hasGrant" + :label="t('account.card.privileges.delegate')" + /> + <VnSelect + :label="t('account.card.role')" + v-model="data.roleFk" + :options="rolesOptions" + option-value="id" + option-label="name" + hide-selected + :required="true" + /> + </div> + </template> + </FormModel> +</template> diff --git a/src/pages/Account/Card/AccountSummary.vue b/src/pages/Account/Card/AccountSummary.vue new file mode 100644 index 000000000..1c7f79f0e --- /dev/null +++ b/src/pages/Account/Card/AccountSummary.vue @@ -0,0 +1,101 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; + +import CardSummary from 'components/ui/CardSummary.vue'; +import VnLv from 'src/components/ui/VnLv.vue'; + +import { useArrayData } from 'src/composables/useArrayData'; + +const route = useRoute(); +const { t } = useI18n(); + +const $props = defineProps({ + id: { + type: Number, + default: 0, + }, +}); +const { store } = useArrayData('Account'); +const account = ref(store.data); + +const entityId = computed(() => $props.id || route.params.id); +const filter = { + where: { id: entityId }, + fields: ['id', 'nickname', 'name', 'role'], + include: { relation: 'role', scope: { fields: ['id', 'name'] } }, +}; +</script> + +<template> + <CardSummary + ref="AccountSummary" + url="VnUsers/preview" + :filter="filter" + @on-fetch="(data) => (account = data)" + > + <template #header>{{ account.id }} - {{ account.nickname }}</template> + <template #body> + <QCard class="vn-one"> + <QCardSection class="q-pa-none"> + <router-link + :to="{ name: 'AccountBasicData', params: { id: entityId } }" + class="header header-link" + > + {{ t('globals.pageTitles.basicData') }} + <QIcon name="open_in_new" /> + </router-link> + </QCardSection> + <VnLv :label="t('account.card.nickname')" :value="account.nickname" /> + <VnLv :label="t('account.card.role')" :value="account.role.name" /> + </QCard> + </template> + </CardSummary> +</template> + +<style lang="scss" scoped> +.q-dialog__inner--minimized > div { + max-width: 80%; +} +.container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 15px; +} +.multimedia-container { + flex: 1 0 21%; +} +.multimedia { + transition: all 0.5s; + opacity: 1; + height: 250px; + + .q-img { + object-fit: cover; + background-color: black; + } + video { + object-fit: cover; + background-color: black; + } +} + +.multimedia:hover { + opacity: 0.5; +} + +.close-button { + top: 1%; + right: 10%; +} + +.zindex { + z-index: 1; +} + +.change-state { + width: 10%; +} +</style> diff --git a/src/pages/Account/InheritedRoles.vue b/src/pages/Account/InheritedRoles.vue new file mode 100644 index 000000000..41e718bb5 --- /dev/null +++ b/src/pages/Account/InheritedRoles.vue @@ -0,0 +1,104 @@ +<script setup> +import { useRoute, useRouter } from 'vue-router'; +import { computed, ref, watch } from 'vue'; + +import VnPaginate from 'components/ui/VnPaginate.vue'; + +import { useArrayData } from 'composables/useArrayData'; + +const props = defineProps({ + dataKey: { type: String, required: true }, +}); + +const route = useRoute(); +const router = useRouter(); + +const paginateRef = ref(null); + +const arrayData = useArrayData(props.dataKey); +const store = arrayData.store; + +const data = computed(() => { + const dataCopy = store.data; + return dataCopy.sort((a, b) => a.role?.name.localeCompare(b.role?.name)); +}); + +const filter = computed(() => ({ + where: { + prindicpalType: 'USER', + principalId: route.params.id, + }, + include: { + relation: 'role', + scope: { + fields: ['id', 'name', 'description'], + }, + }, +})); + +const urlPath = 'RoleMappings'; + +const columns = computed(() => [ + { + name: 'name', + }, +]); + +watch( + () => route.params.id, + () => { + store.url = urlPath; + store.filter = filter.value; + store.limit = 0; + fetchSubRoles(); + } +); + +const fetchSubRoles = () => paginateRef.value.fetch(); + +const redirectToRoleSummary = (id) => + router.push({ name: 'RoleSummary', params: { id } }); +</script> + +<template> + <QPage class="column items-center q-pa-md"> + <div class="full-width" style="max-width: 400px"> + <VnPaginate + ref="paginateRef" + :data-key="dataKey" + :filter="filter" + :url="urlPath" + :limit="0" + auto-load + > + <template #body> + <QTable :rows="data" :columns="columns" hide-header> + <template #body="{ row }"> + <QTr + @click="redirectToRoleSummary(row.role?.id)" + class="cursor-pointer" + > + <QTd> + <div class="column"> + <span>{{ row.role?.name }}</span> + <span class="color-vn-label">{{ + row.role?.description + }}</span> + </div> + </QTd> + </QTr> + </template> + </QTable> + </template> + </VnPaginate> + </div> + </QPage> +</template> + +<i18n> +es: + Role removed. Changes will take a while to fully propagate.: Rol eliminado. Los cambios tardaran un tiempo en propagarse completamente. + Role added! Changes will take a while to fully propagate.: ¡Rol añadido! Los cambios tardaran un tiempo en propagarse completamente. + El rol va a ser eliminado: Role will be removed + ¿Seguro que quieres continuar?: Are you sure you want to continue? +</i18n> diff --git a/src/pages/Account/locale/en.yml b/src/pages/Account/locale/en.yml index dca9b45d9..c7220d7c9 100644 --- a/src/pages/Account/locale/en.yml +++ b/src/pages/Account/locale/en.yml @@ -15,24 +15,75 @@ account: privileges: Privileges mailAlias: Mail Alias mailForwarding: Mail Forwarding + accountCreate: New user aliasUsers: Users card: name: Name nickname: User - role: Rol + role: Role email: Email alias: Alias lang: Language + roleFk: Role + newUser: New user + ticketTracking: Ticket tracking + privileges: + delegate: Can delegate privileges + enabled: Account enabled! + disabled: Account disabled! + willActivated: User will activated + willDeactivated: User will be deactivated + activated: User activated! + deactivated: User deactivated! actions: setPassword: Set password disableAccount: name: Disable account - title: La cuenta será deshabilitada - subtitle: ¿Seguro que quieres continuar? - disableUser: Disable user - sync: Sync - delete: Delete + title: The account will be disabled + subtitle: Are you sure you want to continue? + success: 'Account disabled!' + enableAccount: + name: Enable account + title: The account will be enabled + subtitle: Are you sure you want to continue? + success: 'Account enabled!' + deactivateUser: + name: Deactivate user + title: The user will be deactivated + subtitle: Are you sure you want to continue? + success: 'User deactivated!' + activateUser: + name: Activate user + title: The user will be disabled + subtitle: Are you sure you want to continue? + success: 'User activated!' + sync: + name: Sync + title: The account will be sync + subtitle: Are you sure you want to continue? + success: 'User synchronized!' + checkbox: Synchronize password + message: Do you want to synchronize user? + tooltip: If password is not specified, just user attributes are synchronized + delete: + name: Delete + title: The account will be deleted + subtitle: Are you sure you want to continue? + success: '' search: Search user + searchInfo: You can search by id, name or nickname + create: + name: Name + nickname: Nickname + email: Email + role: Role + password: Password + active: Active + mailForwarding: + forwardingMail: Forward email + accountNotEnabled: Account not enabled + enableMailForwarding: Enable mail forwarding + mailInputInfo: All emails will be forwarded to the specified address. role: pageTitles: inheritedRoles: Inherited Roles diff --git a/src/pages/Account/locale/es.yml b/src/pages/Account/locale/es.yml index 896cc8ea9..fcc4ce1c8 100644 --- a/src/pages/Account/locale/es.yml +++ b/src/pages/Account/locale/es.yml @@ -15,6 +15,7 @@ account: privileges: Privilegios mailAlias: Alias de correo mailForwarding: Reenvío de correo + accountCreate: Nuevo usuario aliasUsers: Usuarios card: nickname: Usuario @@ -22,27 +23,66 @@ account: role: Rol email: Mail alias: Alias - lang: dioma + lang: Idioma + roleFk: Rol + enabled: ¡Cuenta habilitada! + disabled: ¡Cuenta deshabilitada! + willActivated: El usuario será activado + willDeactivated: El usuario será desactivado + activated: ¡Usuario activado! + deactivated: ¡Usuario desactivado! + newUser: Nuevo usuario + privileges: + delegate: Puede delegar privilegios actions: setPassword: Establecer contraseña disableAccount: name: Deshabilitar cuenta title: La cuenta será deshabilitada subtitle: ¿Seguro que quieres continuar? - disableUser: + success: '¡Cuenta deshabilitada!' + enableAccount: + name: Habilitar cuenta + title: La cuenta será habilitada + subtitle: ¿Seguro que quieres continuar? + success: '¡Cuenta habilitada!' + deactivateUser: name: Desactivar usuario title: El usuario será deshabilitado subtitle: ¿Seguro que quieres continuar? + success: '¡Usuario desactivado!' + activateUser: + name: Activar usuario + title: El usuario será activado + subtitle: ¿Seguro que quieres continuar? + success: '¡Usuario activado!' sync: name: Sincronizar title: El usuario será sincronizado subtitle: ¿Seguro que quieres continuar? + success: '¡Usuario sincronizado!' + checkbox: Sincronizar contraseña + message: ¿Quieres sincronizar el usuario? + tooltip: Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario delete: name: Eliminar title: El usuario será eliminado subtitle: ¿Seguro que quieres continuar? - + success: '' search: Buscar usuario + searchInfo: Puedes buscar por id, nombre o usuario + create: + name: Nombre + nickname: Nombre mostrado + email: Email + role: Rol + password: Contraseña + active: Activo + mailForwarding: + forwardingMail: Dirección de reenvío + accountNotEnabled: Cuenta no habilitada + enableMailForwarding: Habilitar redirección de correo + mailInputInfo: Todos los correos serán reenviados a la dirección especificada, no se mantendrá copia de los mismos en el buzón del usuario. role: pageTitles: inheritedRoles: Roles heredados diff --git a/src/pages/Agency/AgencyList.vue b/src/pages/Agency/AgencyList.vue index de335738d..ec6506ba0 100644 --- a/src/pages/Agency/AgencyList.vue +++ b/src/pages/Agency/AgencyList.vue @@ -33,7 +33,6 @@ function exprBuilder(param, value) { url="Agencies" order="name" :expr-builder="exprBuilder" - auto-load > <template #body="{ rows }"> <CardList diff --git a/src/pages/Customer/Card/CustomerCard.vue b/src/pages/Customer/Card/CustomerCard.vue index 98842a0c7..17f123e7b 100644 --- a/src/pages/Customer/Card/CustomerCard.vue +++ b/src/pages/Customer/Card/CustomerCard.vue @@ -10,7 +10,7 @@ import CustomerFilter from '../CustomerFilter.vue'; :descriptor="CustomerDescriptor" :filter-panel="CustomerFilter" search-data-key="CustomerList" - search-url="Clients/filter" + search-url="Clients/extendedListFilter" searchbar-label="Search customer" searchbar-info="You can search by customer id or name" /> diff --git a/src/pages/Customer/Card/CustomerFiscalData.vue b/src/pages/Customer/Card/CustomerFiscalData.vue index deaaefc50..d8592421f 100644 --- a/src/pages/Customer/Card/CustomerFiscalData.vue +++ b/src/pages/Customer/Card/CustomerFiscalData.vue @@ -143,10 +143,6 @@ function handleLocation(data, location) { </VnRow> <VnRow> - <QCheckbox - :label="t('Incoterms authorization')" - v-model="data.hasIncoterms" - /> <QCheckbox :label="t('Electronic invoice')" v-model="data.hasElectronicInvoice" diff --git a/src/pages/Customer/Card/CustomerSummary.vue b/src/pages/Customer/Card/CustomerSummary.vue index f386b0359..5a003dc85 100644 --- a/src/pages/Customer/Card/CustomerSummary.vue +++ b/src/pages/Customer/Card/CustomerSummary.vue @@ -306,10 +306,8 @@ const creditWarning = computed(() => { :value="entity.recommendedCredit" /> </QCard> - <QCard> - <div class="header"> - {{ t('Latest tickets') }} - </div> + <QCard class="vn-one"> + <VnTitle :text="t('Latest tickets')" /> <CustomerSummaryTable /> </QCard> </template> diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index 9e534235c..33659dd77 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -28,7 +28,6 @@ const isLoading = ref(false); const name = ref(null); const usersPreviewRef = ref(null); const user = ref([]); -const userPasswords = ref(0); const dataChanges = computed(() => { return ( @@ -45,7 +44,6 @@ const showChangePasswordDialog = () => { component: CustomerChangePassword, componentProps: { id: route.params.id, - userPasswords: userPasswords.value, promise: usersPreviewRef.value.fetch(), }, }); @@ -97,11 +95,6 @@ const onSubmit = async () => { @on-fetch="(data) => (canChangePassword = data)" auto-load /> - <FetchData - @on-fetch="(data) => (userPasswords = data[0])" - auto-load - url="UserPasswords" - /> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <QBtnGroup push class="q-gutter-x-sm"> diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index 206832f46..043bcdadb 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -29,7 +29,7 @@ const zones = ref(); @on-fetch="(data) => (workers = data)" auto-load /> - <VnFilterPanel :data-key="props.dataKey" :search-button="true"> + <VnFilterPanel :data-key="props.dataKey" :search-button="true" search-url="table"> <template #tags="{ tag, formatFn }"> <div class="q-gutter-x-xs"> <strong>{{ t(`params.${tag.label}`) }}: </strong> diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index 7eb3cefd0..ccd53e8b2 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import VnTable from 'components/VnTable/VnTable.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; +import VnSearchbar from 'components/ui/VnSearchbar.vue'; import CustomerSummary from './Card/CustomerSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; @@ -382,9 +383,14 @@ function handleLocation(data, location) { </script> <template> + <VnSearchbar + :info="t('You can search by customer id or name')" + :label="t('Search customer')" + data-key="Customer" + /> <VnTable ref="tableRef" - data-key="CustomerExtendedList" + data-key="Customer" url="Clients/extendedListFilter" :create="{ urlCreate: 'Clients/createWithUser', diff --git a/src/pages/Customer/components/CustomerChangePassword.vue b/src/pages/Customer/components/CustomerChangePassword.vue index f0be5e510..1bfc5e103 100644 --- a/src/pages/Customer/components/CustomerChangePassword.vue +++ b/src/pages/Customer/components/CustomerChangePassword.vue @@ -9,6 +9,7 @@ import useNotify from 'src/composables/useNotify'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; +import FetchData from 'src/components/FetchData.vue'; const { dialogRef } = useDialogPluginComponent(); const { notify } = useNotify(); @@ -19,15 +20,12 @@ const $props = defineProps({ type: String, required: true, }, - userPasswords: { - type: Object, - required: true, - }, promise: { type: Function, required: true, }, }); +const userPasswords = ref({}); const closeButton = ref(null); const isLoading = ref(false); @@ -60,6 +58,11 @@ const onSubmit = async () => { <template> <QDialog ref="dialogRef"> + <FetchData + @on-fetch="(data) => (userPasswords = data[0])" + auto-load + url="UserPasswords" + /> <QCard class="q-pa-lg"> <QCardSection> <QForm @submit.prevent="onSubmit"> @@ -71,7 +74,7 @@ const onSubmit = async () => { <QIcon name="close" size="sm" /> </span> - <VnRow class="row q-gutter-md q-mb-md"> + <VnRow class="row q-gutter-md q-mb-md" style="flex-direction: column"> <div class="col"> <VnInput :label="t('New password')" @@ -84,11 +87,7 @@ const onSubmit = async () => { <QTooltip> {{ t('customer.card.passwordRequirements', { - length: $props.userPasswords.length, - nAlpha: $props.userPasswords.nAlpha, - nDigits: $props.userPasswords.nDigits, - nPunct: $props.userPasswords.nPunct, - nUpper: $props.userPasswords.nUpper, + ...userPasswords, }) }} </QTooltip> diff --git a/src/pages/Customer/components/CustomerSummaryTable.vue b/src/pages/Customer/components/CustomerSummaryTable.vue index 6a33ebc88..dc9969b61 100644 --- a/src/pages/Customer/components/CustomerSummaryTable.vue +++ b/src/pages/Customer/components/CustomerSummaryTable.vue @@ -162,6 +162,7 @@ const navigateToticketSummary = (id) => { params: { id }, }); }; +const commonColumns = (col) => ['date', 'state', 'total'].includes(col); </script> <template> @@ -171,67 +172,68 @@ const navigateToticketSummary = (id) => { auto-load url="Tickets" /> - <QTable - :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" @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 q-pa-sm" - v-bind="tableColumnComponents[props.col.name].props(props)" - > - <template - v-if=" - props.col.name === 'id' || - props.col.name === 'nickname' || - props.col.name === 'agency' || - props.col.name === 'route' || - props.col.name === 'packages' - " + <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)" > - {{ props.value }} - </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> + <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> </template> <i18n> diff --git a/src/pages/Entry/Card/EntryBuys.vue b/src/pages/Entry/Card/EntryBuys.vue index b08c40810..6e66f4ce7 100644 --- a/src/pages/Entry/Card/EntryBuys.vue +++ b/src/pages/Entry/Card/EntryBuys.vue @@ -11,9 +11,9 @@ import VnInput from 'src/components/common/VnInput.vue'; import FetchedTags from 'components/ui/FetchedTags.vue'; import VnConfirm from 'components/ui/VnConfirm.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; +import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import { useQuasar } from 'quasar'; -import { useStateStore } from 'stores/useStateStore'; import { toCurrency } from 'src/filters'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; @@ -22,7 +22,6 @@ const quasar = useQuasar(); const route = useRoute(); const router = useRouter(); const { t } = useI18n(); -const stateStore = useStateStore(); const { notify } = useNotify(); const rowsSelected = ref([]); @@ -312,20 +311,22 @@ const lockIconType = (groupingMode, mode) => { auto-load @on-fetch="(data) => (packagingsOptions = data)" /> - <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> - <QBtnGroup push style="column-gap: 10px"> - <slot name="moreBeforeActions" /> - <QBtn - :label="t('globals.remove')" - color="primary" - icon="delete" - flat - @click="openRemoveDialog()" - :disable="!rowsSelected?.length" - :title="t('globals.remove')" - /> - </QBtnGroup> - </Teleport> + <VnSubToolbar> + <template #st-actions> + <QBtnGroup push style="column-gap: 10px"> + <slot name="moreBeforeActions" /> + <QBtn + :label="t('globals.remove')" + color="primary" + icon="delete" + flat + @click="openRemoveDialog()" + :disable="!rowsSelected?.length" + :title="t('globals.remove')" + /> + </QBtnGroup> + </template> + </VnSubToolbar> <VnPaginate ref="entryBuysPaginateRef" data-key="EntryBuys" diff --git a/src/pages/Entry/Card/EntryDescriptor.vue b/src/pages/Entry/Card/EntryDescriptor.vue index 807ccdae4..3c925ead6 100644 --- a/src/pages/Entry/Card/EntryDescriptor.vue +++ b/src/pages/Entry/Card/EntryDescriptor.vue @@ -135,14 +135,19 @@ watch; <template #icons> <QCardActions class="q-gutter-x-md"> <QIcon - v-if="currentEntry.isExcludedFromAvailable" + v-if="currentEntry?.isExcludedFromAvailable" name="vn:inventory" color="primary" size="xs" > <QTooltip>{{ t('Inventory entry') }}</QTooltip> </QIcon> - <QIcon v-if="currentEntry.isRaid" name="vn:net" color="primary" size="xs"> + <QIcon + v-if="currentEntry?.isRaid" + name="vn:net" + color="primary" + size="xs" + > <QTooltip>{{ t('Virtual entry') }}</QTooltip> </QIcon> </QCardActions> diff --git a/src/pages/Entry/EntryLatestBuys.vue b/src/pages/Entry/EntryLatestBuys.vue index cae59207b..291b828c9 100644 --- a/src/pages/Entry/EntryLatestBuys.vue +++ b/src/pages/Entry/EntryLatestBuys.vue @@ -167,7 +167,7 @@ const columns = computed(() => [ }, }, { - label: t('globals.description'), + label: t('entry.latestBuys.description'), field: 'description', name: 'description', align: 'left', @@ -653,6 +653,15 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <EntryLatestBuysFilter data-key="EntryLatestBuys" /> </template> </RightMenu> + <Teleport to="#actions-append"> + <div class="row q-gutter-x-sm"> + <QBtn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu"> + <QTooltip bottom anchor="bottom right"> + {{ t('globals.collapseMenu') }} + </QTooltip> + </QBtn> + </div> + </Teleport> <QPage class="column items-center q-pa-md"> <QTable :rows="rows" diff --git a/src/pages/Entry/EntryLatestBuysFilter.vue b/src/pages/Entry/EntryLatestBuysFilter.vue index f83bb167a..f147a3c6f 100644 --- a/src/pages/Entry/EntryLatestBuysFilter.vue +++ b/src/pages/Entry/EntryLatestBuysFilter.vue @@ -184,13 +184,6 @@ const suppliersOptions = ref([]); @click="removeTag(index, params, searchFn)" /> </QItem> - <QItem class="q-mt-lg"> - <QIcon - name="add_circle" - class="filter-icon" - @click="tagValues.push({})" - /> - </QItem> </template> </ItemsFilterPanel> </template> diff --git a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue index 9325a5b41..d2b43c57a 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDueDay.vue @@ -10,8 +10,10 @@ import FetchData from 'src/components/FetchData.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnCurrency from 'src/components/common/VnCurrency.vue'; import { toCurrency } from 'src/filters'; +import useNotify from 'src/composables/useNotify.js'; const route = useRoute(); +const { notify } = useNotify(); const { t } = useI18n(); const arrayData = useArrayData(); const invoiceIn = computed(() => arrayData.store.data); @@ -69,6 +71,7 @@ const isNotEuro = (code) => code != 'EUR'; async function insert() { await axios.post('/InvoiceInDueDays/new', { id: +invoiceId }); await invoiceInFormRef.value.reload(); + notify(t('globals.dataSaved'), 'positive'); } const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, 0); </script> diff --git a/src/pages/Item/Card/ItemDescriptorImage.vue b/src/pages/Item/Card/ItemDescriptorImage.vue index f16a57548..aceede880 100644 --- a/src/pages/Item/Card/ItemDescriptorImage.vue +++ b/src/pages/Item/Card/ItemDescriptorImage.vue @@ -3,8 +3,7 @@ import { ref, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; import EditPictureForm from 'components/EditPictureForm.vue'; - -import { useSession } from 'src/composables/useSession'; +import VnImg from 'src/components/ui/VnImg.vue'; import axios from 'axios'; const $props = defineProps({ @@ -27,19 +26,12 @@ const $props = defineProps({ }); const { t } = useI18n(); -const { getTokenMultimedia } = useSession(); const image = ref(null); const editPhotoFormDialog = ref(null); const showEditPhotoForm = ref(false); const warehouseName = ref(null); -const getItemAvatar = async () => { - const token = getTokenMultimedia(); - const timeStamp = `timestamp=${Date.now()}`; - image.value = `/api/Images/catalog/200x200/${$props.entityId}/download?access_token=${token}&${timeStamp}`; -}; - const toggleEditPictureForm = () => { showEditPhotoForm.value = !showEditPhotoForm.value; }; @@ -62,14 +54,17 @@ const getWarehouseName = async (warehouseFk) => { }; onMounted(async () => { - getItemAvatar(); getItemConfigs(); }); + +const handlePhotoUpdated = (evt = false) => { + image.value.reload(evt); +}; </script> <template> <div class="relative-position"> - <QImg :src="image" spinner-color="primary" style="min-height: 256px"> + <VnImg ref="image" :id="$props.entityId" @refresh="handlePhotoUpdated(true)"> <template #error> <div class="absolute-full picture text-center q-pa-md flex flex-center"> <div> @@ -82,7 +77,7 @@ onMounted(async () => { </div> </div> </template> - </QImg> + </VnImg> <QBtn v-if="showEditButton" color="primary" @@ -97,7 +92,7 @@ onMounted(async () => { collection="catalog" :id="entityId" @close-form="toggleEditPictureForm()" - @on-photo-uploaded="getItemAvatar()" + @on-photo-uploaded="handlePhotoUpdated" /> </QDialog> </QBtn> diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index 04748a8c8..97422084b 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -3,14 +3,15 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import axios from 'axios'; - -import VnInput from 'components/common/VnInput.vue'; 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'; const { t } = useI18n(); + const route = useRoute(); const props = defineProps({ dataKey: { @@ -21,32 +22,34 @@ const props = defineProps({ type: Array, required: true, }, + tagValue: { + type: Array, + required: true, + }, }); const categoryList = ref(null); const selectedCategoryFk = ref(null); const typeList = ref(null); const selectedTypeFk = ref(null); - +const validationsStore = useValidator(); +const selectedOrder = ref(null); +const selectedOrderField = ref(null); +const moreFields = ref([]); +const moreFieldsOrder = ref([]); +const createValue = (val, done) => { + if (val.length > 2) { + if (!tagOptions.value.includes(val)) { + done(tagOptions.value, 'add-unique'); + } + tagValues.value.push({ value: val }); + } +}; const resetCategory = () => { selectedCategoryFk.value = null; typeList.value = null; }; -const selectedOrder = ref(null); -const orderList = [ - { way: 'ASC', name: 'Ascendant' }, - { way: 'DESC', name: 'Descendant' }, -]; - -const selectedOrderField = ref(null); -const OrderFields = [ - { field: 'relevancy DESC, name', name: 'Relevancy', priority: 999 }, - { field: 'showOrder, price', name: 'Color and price', priority: 999 }, - { field: 'name', name: 'Name', priority: 999 }, - { field: 'price', name: 'Price', priority: 999 }, -]; - const clearFilter = (key) => { if (key === 'categoryFk') { resetCategory(); @@ -72,21 +75,6 @@ const loadTypes = async (categoryFk) => { typeList.value = data; }; -const onFilterInit = async ({ params }) => { - if (params.typeFk) { - selectedTypeFk.value = params.typeFk; - } - if (params.categoryFk) { - await loadTypes(params.categoryFk); - selectedCategoryFk.value = params.categoryFk; - } - if (params.orderBy) { - orderByParam.value = JSON.parse(params.orderBy); - selectedOrder.value = orderByParam.value?.way; - selectedOrderField.value = orderByParam.value?.field; - } -}; - const selectedCategory = computed(() => (categoryList.value || []).find( (category) => category?.id === selectedCategoryFk.value @@ -109,10 +97,7 @@ function exprBuilder(param, value) { const selectedTag = ref(null); const tagValues = ref([{}]); -const tagOptions = ref(null); -const isButtonDisabled = computed( - () => !selectedTag.value || tagValues.value.some((item) => !item.value) -); +const tagOptions = ref([]); const applyTagFilter = (params, search) => { if (!tagValues.value?.length) { @@ -125,12 +110,12 @@ const applyTagFilter = (params, search) => { } params.tagGroups.push( JSON.stringify({ - values: tagValues.value, + values: tagValues.value.filter((obj) => Object.keys(obj).length > 0), tagSelection: { ...selectedTag.value, - orgShowField: selectedTag.value.name, + orgShowField: selectedTag?.value?.name, }, - tagFk: selectedTag.value.tagFk, + tagFk: selectedTag?.value?.tagFk, }) ); search(); @@ -147,20 +132,52 @@ const removeTagChip = (selection, params, search) => { search(); }; -const orderByParam = ref(null); - -const onOrderFieldChange = (value, params, search) => { - const orderBy = Object.assign({}, orderByParam.value, { field: value.field }); - params.orderBy = JSON.stringify(orderBy); - search(); +const onOrderChange = (value, params) => { + const tagObj = JSON.parse(params.orderBy); + tagObj.way = value.name; + params.orderBy = JSON.stringify(tagObj); }; -const onOrderChange = (value, params, search) => { - const orderBy = Object.assign({}, orderByParam.value, { way: value.way }); - params.orderBy = JSON.stringify(orderBy); - search(); +const onOrderFieldChange = (value, params) => { + const tagObj = JSON.parse(params.orderBy); // esto donde va + const fields = { + Relevancy: (value) => value + ' DESC, name', + ColorAndPrice: 'showOrder, price', + Name: 'name', + Price: 'price', + }; + let tagField = fields[value]; + if (!tagField) return; + + if (typeof tagField === 'function') tagField = tagField(value); + tagObj.field = tagField; + params.orderBy = JSON.stringify(tagObj); + switch (value) { + case 'Relevancy': + tagObj.field = value + ' DESC, name'; + params.orderBy = JSON.stringify(tagObj); + console.log('params: ', params); + break; + case 'ColorAndPrice': + tagObj.field = 'showOrder, price'; + params.orderBy = JSON.stringify(tagObj); + console.log('params: ', params); + break; + case 'Name': + tagObj.field = 'name'; + params.orderBy = JSON.stringify(tagObj); + console.log('params: ', params); + break; + case 'Price': + tagObj.field = 'price'; + params.orderBy = JSON.stringify(tagObj); + console.log('params: ', params); + break; + } }; +const _moreFields = ['ASC', 'DESC']; +const _moreFieldsTypes = ['Relevancy', 'ColorAndPrice', 'Name', 'Price']; const setCategoryList = (data) => { categoryList.value = (data || []) .filter((category) => category.display) @@ -168,6 +185,8 @@ const setCategoryList = (data) => { ...category, icon: `vn:${(category.icon || '').split('-')[1]}`, })); + moreFields.value = useLang(_moreFields); + moreFieldsOrder.value = useLang(_moreFieldsTypes); }; const getCategoryClass = (category, params) => { @@ -175,6 +194,20 @@ const getCategoryClass = (category, params) => { return 'active'; } }; + +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, + }; + }); +}; </script> <template> @@ -182,9 +215,9 @@ const getCategoryClass = (category, params) => { <VnFilterPanel :data-key="props.dataKey" :hidden-tags="['orderFk', 'orderBy']" + :unremovable-params="['orderFk', 'orderBy']" :expr-builder="exprBuilder" :custom-tags="['tagGroups']" - @init="onFilterInit" @remove="clearFilter" > <template #tags="{ tag, formatFn }"> @@ -274,40 +307,29 @@ const getCategoryClass = (category, params) => { <QItem class="q-my-md"> <QItemSection> <VnSelect - :label="t('params.order')" + :label="t('Order')" v-model="selectedOrder" - :options="orderList || []" - option-value="way" - option-label="name" + :options="moreFields" + option-label="label" dense outlined rounded - :emit-value="false" - use-input - :is-clearable="false" - @update:model-value=" - (value) => onOrderChange(value, params, searchFn) - " + @update:model-value="(value) => onOrderChange(value, params)" /> </QItemSection> </QItem> <QItem class="q-mb-md"> <QItemSection> <VnSelect - :label="t('params.order')" + :label="t('Order by')" v-model="selectedOrderField" - :options="OrderFields || []" - option-value="field" - option-label="name" + :options="moreFieldsOrder" + option-label="label" + option-value="name" dense outlined rounded - :emit-value="false" - use-input - :is-clearable="false" - @update:model-value=" - (value) => onOrderFieldChange(value, params, searchFn) - " + @update:model-value="(value) => onOrderFieldChange(value, params)" /> </QItemSection> </QItem> @@ -333,15 +355,30 @@ const getCategoryClass = (category, params) => { :key="value" class="q-mt-md filter-value" > - <VnInput - v-if="selectedTag?.isFree" - v-model="value.value" - :label="t('params.value')" - is-outlined - class="filter-input" + <FetchData + v-if="selectedTag" + :url="`Tags/${selectedTag}/filterValue`" + limit="30" + auto-load + @on-fetch="(data) => (tagOptions = data)" /> <VnSelect - v-else + v-if="!selectedTag" + :label="t('params.value')" + v-model="value.value" + :options="tagValue || []" + option-value="value" + option-label="value" + dense + outlined + rounded + emit-value + use-input + class="filter-input" + @new-value="createValue" + /> + <VnSelect + v-else-if="selectedTag === 1" :label="t('params.value')" v-model="value.value" :options="tagOptions || []" @@ -352,18 +389,18 @@ const getCategoryClass = (category, params) => { rounded emit-value use-input - :disable="!selectedTag" + class="filter-input" + @new-value="createValue" + /> + <VnInput + v-else + :label="t('params.value')" + v-model="value.value" + dense + outlined + rounded class="filter-input" /> - - <FetchData - v-if="selectedTag && !selectedTag.isFree" - :url="`Tags/${selectedTag?.id}/filterValue`" - limit="30" - auto-load - @on-fetch="(data) => (tagOptions = data)" - /> - <QIcon name="delete" class="filter-icon" @@ -388,7 +425,6 @@ const getCategoryClass = (category, params) => { rounded type="button" unelevated - :disable="isButtonDisabled" @click.stop="applyTagFilter(params, searchFn)" /> </QItemSection> @@ -453,6 +489,12 @@ en: tag: Tag value: Value order: Order + ASC: Ascendant + DESC: Descendant + Relevancy: Relevancy + ColorAndPrice: Color and price + Name: Name + Price: Price es: params: type: Tipo @@ -460,6 +502,14 @@ es: tag: Etiqueta value: Valor order: Orden + ASC: Ascendiente + DESC: Descendiente + Relevancy: Relevancia + ColorAndPrice: Color y precio + Name: Nombre + Price: Precio + Order: Orden + Order by: Ordenar por Plant: Planta Flower: Flor Handmade: Confección diff --git a/src/pages/Order/Card/OrderCatalogItem.vue b/src/pages/Order/Card/OrderCatalogItem.vue index 0e1005493..34e22915d 100644 --- a/src/pages/Order/Card/OrderCatalogItem.vue +++ b/src/pages/Order/Card/OrderCatalogItem.vue @@ -3,16 +3,14 @@ import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import VnLv from 'components/ui/VnLv.vue'; +import VnImg from 'src/components/ui/VnImg.vue'; import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; -import { useSession } from 'composables/useSession'; import toCurrency from '../../../filters/toCurrency'; const DEFAULT_PRICE_KG = 0; -const { getTokenMultimedia } = useSession(); -const token = getTokenMultimedia(); const { t } = useI18n(); defineProps({ @@ -29,14 +27,7 @@ const dialog = ref(null); <div class="container order-catalog-item overflow-hidden"> <QCard class="card shadow-6"> <div class="img-wrapper"> - <QImg - :src="`/api/Images/catalog/200x200/${item.id}/download?access_token=${token}`" - spinner-color="primary" - :ratio="1" - height="192" - width="192" - class="image" - /> + <VnImg :id="item.id" class="image" /> <div v-if="item.hex" class="item-color-container"> <div class="item-color" @@ -59,7 +50,10 @@ const dialog = ref(null); </template> <div class="footer"> <div class="price"> - <p>{{ item.available }} {{ t('to') }} {{ item.price }}</p> + <p> + {{ item.available }} {{ t('to') }} + {{ toCurrency(item.price) }} + </p> <QIcon name="add_circle" class="icon"> <QTooltip>{{ t('globals.add') }}</QTooltip> <QPopupProxy ref="dialog"> diff --git a/src/pages/Order/Card/OrderForm.vue b/src/pages/Order/Card/OrderForm.vue index 378073897..20b29cd9c 100644 --- a/src/pages/Order/Card/OrderForm.vue +++ b/src/pages/Order/Card/OrderForm.vue @@ -1,5 +1,5 @@ <script setup> -import { useRoute } from 'vue-router'; +import { useRoute, useRouter } from 'vue-router'; import { reactive, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; @@ -16,6 +16,7 @@ const route = useRoute(); const state = useState(); const ORDER_MODEL = 'order'; +const router = useRouter(); const isNew = Boolean(!route.params.id); const initialFormState = reactive({ clientFk: null, @@ -26,22 +27,19 @@ const initialFormState = reactive({ const clientList = ref([]); const agencyList = ref([]); const addressList = ref([]); +const clientId = ref(null); -const onClientsFetched = async (data) => { - try { - clientList.value = data; - initialFormState.clientFk = Number(route.query?.clientFk) || null; +const onClientsFetched = (data) => { + clientList.value = data; + initialFormState.clientFk = Number(route.query?.clientFk) || null; + clientId.value = initialFormState.clientFk; - if (initialFormState.clientFk) { - const { defaultAddressFk } = clientList.value.find( - (client) => client.id === initialFormState.clientFk - ); - - if (defaultAddressFk) await fetchAddressList(defaultAddressFk); - } - } catch (err) { - console.error('Error fetching clients', err); - } + const client = clientList.value.find( + (client) => client.id === initialFormState.clientFk + ); + if (!client?.defaultAddressFk) + throw new Error(t(`No default address found for the client`)); + fetchAddressList(client.defaultAddressFk); }; const fetchAddressList = async (addressId) => { @@ -55,7 +53,6 @@ const fetchAddressList = async (addressId) => { }, }); addressList.value = data; - // Set address by default if (addressList.value?.length === 1) { state.get(ORDER_MODEL).addressFk = addressList.value[0].id; } @@ -121,6 +118,21 @@ const orderFilter = { }, ], }; + +const onClientChange = async (clientId) => { + try { + const { data } = await axios.get(`Clients/${clientId}`); + console.log('info cliente: ', data); + + await fetchAddressList(data.defaultAddressFk); + } catch (error) { + console.error('Error al cambiar el cliente:', error); + } +}; + +async function onDataSaved(data) { + await router.push({ path: `/order/${data}/catalog` }); +} </script> <template> @@ -134,13 +146,15 @@ const orderFilter = { <div class="q-pa-md"> <FormModel :url="!isNew ? `Orders/${route.params.id}` : null" - :url-create="isNew ? 'Orders/new' : null" + url-create="Orders/new" + @on-data-saved="onDataSaved" :model="ORDER_MODEL" :form-initial-data="isNew ? initialFormState : null" :observe-form-changes="!isNew" :mapper="isNew ? orderMapper : null" :filter="orderFilter" @on-fetch="fetchOrderDetails" + auto-load > <template #form="{ data }"> <VnRow class="row q-gutter-md q-mb-md"> @@ -151,9 +165,7 @@ const orderFilter = { option-value="id" option-label="name" hide-selected - @update:model-value=" - (client) => fetchAddressList(client.defaultAddressFk) - " + @update:model-value="onClientChange" > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -170,12 +182,10 @@ const orderFilter = { v-model="data.addressFk" :options="addressList" option-value="id" - option-label="nickname" + option-label="street" hide-selected :disable="!addressList?.length" - @update:model-value=" - () => fetchAgencyList(data.landed, data.addressFk) - " + @update:model-value="onAddressChange" > <template #option="scope"> <QItem v-bind="scope.itemProps"> @@ -216,3 +226,8 @@ const orderFilter = { </FormModel> </div> </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/OrderCatalog.vue b/src/pages/Order/OrderCatalog.vue index 1ed03c47d..1d97663d0 100644 --- a/src/pages/Order/OrderCatalog.vue +++ b/src/pages/Order/OrderCatalog.vue @@ -4,7 +4,6 @@ import { useRoute } from 'vue-router'; import { onMounted, onUnmounted, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import VnPaginate from 'components/ui/VnPaginate.vue'; -import VnSearchbar from 'components/ui/VnSearchbar.vue'; import OrderCatalogItem from 'pages/Order/Card/OrderCatalogItem.vue'; import OrderCatalogFilter from 'pages/Order/Card/OrderCatalogFilter.vue'; @@ -35,38 +34,31 @@ function extractTags(items) { }); }); tags.value = resultTags; + extractValueTags(items); +} + +const tagValue = ref([]); + +function extractValueTags(items) { + const resultValueTags = items.flatMap((x) => + Object.keys(x) + .filter((k) => /^value\d+$/.test(k)) + .map((v) => x[v]) + .filter((v) => v) + .sort() + ); + tagValue.value = resultValueTags; } </script> <template> - <Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> - <VnSearchbar - data-key="OrderCatalogList" - url="Orders/CatalogFilter" - :limit="50" - :user-params="catalogParams" - :static-params="['orderFk', 'orderBy']" - :redirect="false" - /> - </Teleport> - <Teleport v-if="stateStore.isHeaderMounted()" to="#actions-append"> - <div class="row q-gutter-x-sm"> - <QBtn - flat - @click.stop="stateStore.toggleRightDrawer()" - round - dense - icon="menu" - > - <QTooltip bottom anchor="bottom right"> - {{ t('globals.collapseMenu') }} - </QTooltip> - </QBtn> - </div> - </Teleport> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QScrollArea class="fit text-grey-8"> - <OrderCatalogFilter data-key="OrderCatalogList" :tags="tags" /> + <OrderCatalogFilter + data-key="OrderCatalogList" + :tag-value="tagValue" + :tags="tags" + /> </QScrollArea> </QDrawer> <QPage class="column items-center q-pa-md"> diff --git a/src/pages/Order/OrderLines.vue b/src/pages/Order/OrderLines.vue index f089c0e85..6758fb170 100644 --- a/src/pages/Order/OrderLines.vue +++ b/src/pages/Order/OrderLines.vue @@ -7,19 +7,17 @@ import { useQuasar } from 'quasar'; 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 VnConfirm from 'components/ui/VnConfirm.vue'; +import VnImg from 'components/ui/VnImg.vue'; import { toCurrency, toDate } from 'src/filters'; -import { useSession } from 'composables/useSession'; import axios from 'axios'; +import ItemDescriptorProxy from '../Item/Card/ItemDescriptorProxy.vue'; const route = useRoute(); const { t } = useI18n(); -const { getTokenMultimedia } = useSession(); const quasar = useQuasar(); -const token = getTokenMultimedia(); const orderSummary = ref({ total: null, vat: null, @@ -61,6 +59,56 @@ async function confirmOrder() { type: 'positive', }); } + +const detailsColumns = ref([ + { + name: 'img', + label: '', + field: (row) => row?.item?.id, + }, + { + name: 'item', + label: t('order.summary.item'), + field: (row) => row?.item?.id, + sortable: true, + }, + { + name: 'description', + label: t('globals.description'), + field: (row) => row?.item?.name, + }, + { + name: 'warehouse', + label: t('warehouse'), + field: (row) => row?.warehouse?.name, + sortable: true, + }, + { + name: 'shipped', + label: t('shipped'), + field: (row) => toDate(row?.shipped), + }, + { + name: 'quantity', + label: t('order.summary.quantity'), + field: (row) => row?.quantity, + }, + { + name: 'price', + label: t('order.summary.price'), + field: (row) => toCurrency(row?.price), + }, + { + name: 'amount', + label: t('order.summary.amount'), + field: (row) => toCurrency(row?.quantity * row?.price), + }, + { + name: 'actions', + label: '', + field: (row) => row?.id, + }, +]); </script> <template> @@ -83,30 +131,33 @@ async function confirmOrder() { auto-load /> <QPage :key="componentKey" class="column items-center q-pa-md"> - <div class="vn-card-list"> + <div class="order-list full-width"> <div v-if="!orderSummary.total" class="no-result"> {{ t('globals.noResults') }} </div> - <QCard v-else class="order-lines-summary q-pa-lg"> - <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)" - /> - </QCard> + + <QDrawer side="right" :width="270" show-if-above> + <QCard class="order-lines-summary q-pa-lg"> + <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)" + /> + </QCard> + </QDrawer> <VnPaginate data-key="OrderLines" url="OrderRows" @@ -125,74 +176,71 @@ async function confirmOrder() { }" > <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" + <div class="q-pa-md"> + <QTable + :columns="detailsColumns" + :rows="rows" + flat + class="full-width" + style="text-align: center" > - <template #title> - <div class="flex items-center"> - <div class="image-wrapper q-mr-md"> - <QImg - :src="`/api/Images/catalog/50x50/${row?.item?.id}/download?access_token=${token}`" - spinner-color="primary" - :ratio="1" - height="50" - width="50" - class="image" - /> - </div> - <div - class="title text-primary text-weight-bold text-h5" + <template #header="props"> + <QTr class="tr-header" :props="props"> + <QTh + v-for="col in props.cols" + :key="col.name" + :props="props" + style="text-align: center" > - {{ row?.item?.name }} + {{ t(col.label) }} + </QTh> + </QTr> + </template> + <template #body-cell-img="{ value }"> + <QTd> + <div class="image-wrapper"> + <VnImg :id="value" class="rounded" /> </div> - <QChip class="q-chip-color" outline size="sm"> - {{ t('ID') }}: {{ row.id }} - </QChip> - </div> + </QTd> </template> - <template #list-items> - <div class="q-mb-sm"> - <span class="text-uppercase subname"> - {{ row.item.subName }} + <template #body-cell-item="{ value }"> + <QTd class="item"> + <span class="link"> + <QBtn flat> + {{ value }} + </QBtn> + <ItemDescriptorProxy :id="value" /> </span> - <FetchedTags :item="row.item" :max-length="5" /> - </div> - <VnLv :label="t('item')" :value="String(row.item.id)" /> - <VnLv - :label="t('warehouse')" - :value="row.warehouse.name" - /> - <VnLv - :label="t('shipped')" - :value="toDate(row.shipped)" - /> - <VnLv - :label="t('quantity')" - :value="String(row.quantity)" - /> - <VnLv - :label="t('price')" - :value="toCurrency(row.price)" - /> - <VnLv - :label="t('amount')" - :value="toCurrency(row.price * row.quantity)" - /> + </QTd> </template> - <template #actions v-if="!order?.isConfirmed"> - <QBtn - :label="t('remove')" - @click.stop="confirmRemove(row)" - color="primary" - style="margin-top: 15px" - /> + <template #body-cell-description="{ row, value }"> + <QTd> + <div + class="row column full-width justify-between items-start" + > + {{ value }} + <div v-if="value" class="subName"> + {{ value.toUpperCase() }} + </div> + </div> + <FetchedTags :item="row.item" :max-length="6" /> + </QTd> </template> - </CardList> + + <template #body-cell-actions="{ value }"> + <QTd> + <QIcon + name="delete" + color="primary" + size="sm" + class="cursor-pointer" + @click.stop="confirmRemove(value)" + > + <QTooltip>{{ t('Remove thermograph') }}</QTooltip> + </QIcon> + </QTd> + </template> + </QTable> </div> </template> </VnPaginate> @@ -239,14 +287,7 @@ async function confirmOrder() { .image-wrapper { height: 50px; width: 50px; - - .image { - border-radius: 50%; - } -} - -.subname { - color: var(--vn-label-color); + margin-left: 30%; } .no-result { @@ -255,6 +296,11 @@ async function confirmOrder() { color: var(--vn-label-color); text-align: center; } + +.subName { + text-transform: uppercase; + color: var(--vn-label-color); +} </style> <i18n> en: diff --git a/src/pages/Route/Card/RouteFilter.vue b/src/pages/Route/Card/RouteFilter.vue index 050e7c71d..f0215370f 100644 --- a/src/pages/Route/Card/RouteFilter.vue +++ b/src/pages/Route/Card/RouteFilter.vue @@ -240,4 +240,5 @@ es: From: Desde To: Hasta Served: Servida + Days Onward: Días en adelante </i18n> diff --git a/src/pages/Route/Card/RouteSummary.vue b/src/pages/Route/Card/RouteSummary.vue index ac03a8231..653508fe8 100644 --- a/src/pages/Route/Card/RouteSummary.vue +++ b/src/pages/Route/Card/RouteSummary.vue @@ -3,14 +3,15 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useStateStore } from 'stores/useStateStore'; -import CardSummary from 'components/ui/CardSummary.vue'; -import VnLv from 'components/ui/VnLv.vue'; import { QIcon } from 'quasar'; import { dashIfEmpty, toCurrency, toDate, toHour } from 'src/filters'; +import { openBuscaman } from 'src/utils/buscaman'; +import CardSummary from 'components/ui/CardSummary.vue'; import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; -import { openBuscaman } from 'src/utils/buscaman'; +import VnLv from 'components/ui/VnLv.vue'; +import VnTitle from 'src/components/common/VnTitle.vue'; const $props = defineProps({ id: { @@ -127,8 +128,14 @@ const ticketColumns = ref([ <span>{{ `${entity?.route.id} - ${entity?.route?.description}` }}</span> </template> <template #body="{ entity }"> + <QCard class="vn-max"> + <VnTitle + :url="`#/route/${entityId}/basic-data`" + :text="t('globals.pageTitles.basicData')" + /> + </QCard> + <QCard class="vn-one"> - <VnLv :label="t('ID')" :value="entity?.route.id" /> <VnLv :label="t('route.summary.date')" :value="toDate(entity?.route.created)" @@ -153,24 +160,6 @@ const ticketColumns = ref([ :label="t('route.summary.cost')" :value="toCurrency(entity.route?.cost)" /> - </QCard> - <QCard class="vn-one"> - <VnLv - :label="t('route.summary.started')" - :value="toHour(entity?.route.started)" - /> - <VnLv - :label="t('route.summary.finished')" - :value="toHour(entity?.route.finished)" - /> - <VnLv - :label="t('route.summary.kmStart')" - :value="dashIfEmpty(entity?.route?.kmStart)" - /> - <VnLv - :label="t('route.summary.kmEnd')" - :value="dashIfEmpty(entity?.route?.kmEnd)" - /> <VnLv :label="t('route.summary.volume')" :value="`${dashIfEmpty(entity?.route?.m3)} / ${dashIfEmpty( @@ -192,19 +181,32 @@ const ticketColumns = ref([ /> </QCard> <QCard class="vn-one"> - <div class="header"> - {{ t('globals.description') }} - </div> - <p> - {{ dashIfEmpty(entity?.route?.description) }} - </p> + <VnLv + :label="t('route.summary.started')" + :value="toHour(entity?.route.started)" + /> + <VnLv + :label="t('route.summary.finished')" + :value="toHour(entity?.route.finished)" + /> + <VnLv + :label="t('route.summary.kmStart')" + :value="dashIfEmpty(entity?.route?.kmStart)" + /> + <VnLv + :label="t('route.summary.kmEnd')" + :value="dashIfEmpty(entity?.route?.kmEnd)" + /> + <VnLv + :label="t('globals.description')" + :value="dashIfEmpty(entity?.route?.description)" + /> </QCard> - <QCard class="vn-max"> - <a class="header" :href="`#/route/${entityId}/tickets`"> - {{ t('route.summary.tickets') }} - <QIcon name="open_in_new" color="primary" /> - </a> + <VnTitle + :url="`#/route/${entityId}/tickets`" + :text="t('route.summary.tickets')" + /> <QTable :columns="ticketColumns" :rows="entity?.tickets" diff --git a/src/pages/Route/Cmr/CmrList.vue b/src/pages/Route/Cmr/CmrList.vue index cbfc3751a..07479a88d 100644 --- a/src/pages/Route/Cmr/CmrList.vue +++ b/src/pages/Route/Cmr/CmrList.vue @@ -1,7 +1,8 @@ <script setup> -import { computed, ref } from 'vue'; +import { onBeforeMount, computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { Notify } from 'quasar'; +import axios from 'axios'; import VnPaginate from 'components/ui/VnPaginate.vue'; import { useSession } from 'src/composables/useSession'; import { toDate } from 'filters/index'; @@ -29,7 +30,6 @@ const columns = computed(() => [ field: (row) => row.hasCmrDms, align: 'center', sortable: true, - headerStyle: 'padding-left: 35px', }, { name: 'ticketFk', @@ -62,7 +62,6 @@ const columns = computed(() => [ field: (row) => toDate(row.shipped), align: 'center', sortable: true, - headerStyle: 'padding-left: 33px', }, { name: 'warehouseFk', @@ -77,6 +76,11 @@ const columns = computed(() => [ field: (row) => row.cmrFk, }, ]); + +onBeforeMount(async () => { + const { data } = await axios.get('Warehouses'); + warehouses.value = data; +}); function getApiUrl() { return new URL(window.location).origin; } @@ -105,13 +109,7 @@ function downloadPdfs() { </RightMenu> <div class="column items-center"> <div class="list"> - <VnPaginate - data-key="CmrList" - :url="`Routes/cmrs`" - order="cmrFk DESC" - limit="null" - auto-load - > + <VnPaginate data-key="CmrList" :url="`Routes/cmrs`" order="cmrFk DESC"> <template #body="{ rows }"> <QTable :columns="columns" @@ -187,7 +185,6 @@ function downloadPdfs() { </QPageSticky> </div> </template> - <style lang="scss" scoped> .list { padding-top: 15px; @@ -204,4 +201,10 @@ function downloadPdfs() { #false { background-color: $negative; } +:deep(.q-table th) { + max-width: 80px; +} +:deep(.q-table th:nth-child(3)) { + max-width: 100px; +} </style> diff --git a/src/pages/Route/Roadmap/RoadmapSummary.vue b/src/pages/Route/Roadmap/RoadmapSummary.vue index e9969c2f7..6c397bcc1 100644 --- a/src/pages/Route/Roadmap/RoadmapSummary.vue +++ b/src/pages/Route/Roadmap/RoadmapSummary.vue @@ -67,6 +67,7 @@ const filter = { }, }, ], + where: { id: entityId }, }; const openAddStopDialog = () => { @@ -84,7 +85,7 @@ const openAddStopDialog = () => { <CardSummary data-key="RoadmapSummary" ref="summary" - :url="`Roadmaps/${entityId}`" + :url="`Roadmaps`" :filter="filter" > <template #header-left> diff --git a/src/pages/Route/RouteAutonomous.vue b/src/pages/Route/RouteAutonomous.vue index f704d2aff..daffcb3f2 100644 --- a/src/pages/Route/RouteAutonomous.vue +++ b/src/pages/Route/RouteAutonomous.vue @@ -39,7 +39,7 @@ const selectedRows = ref([]); const columns = computed(() => [ { name: 'ID', - label: t('ID'), + label: 'Id', field: (row) => row.routeFk, sortable: true, align: 'left', @@ -117,7 +117,9 @@ const columns = computed(() => [ const refreshKey = ref(0); -const total = computed(() => selectedRows.value.reduce((item) => item?.price || 0, 0)); +const total = computed(() => { + return selectedRows.value.reduce((sum, item) => sum + item.price, 0); +}); const openDmsUploadDialog = async () => { dmsDialog.value.rowsToCreateInvoiceIn = selectedRows.value @@ -212,7 +214,6 @@ function navigateToRouteSummary(event, row) { data-key="RouteAutonomousList" url="AgencyTerms/filter" :limit="20" - auto-load > <template #body="{ rows }"> <div class="q-pa-md"> @@ -306,6 +307,13 @@ function navigateToRouteSummary(event, row) { cursor: pointer; } } + +th:last-child, +td:last-child { + background-color: var(--vn-section-color); + position: sticky; + right: 0; +} </style> <i18n> es: diff --git a/src/pages/Route/RouteList.vue b/src/pages/Route/RouteList.vue index 77c3bdb4c..edec43fec 100644 --- a/src/pages/Route/RouteList.vue +++ b/src/pages/Route/RouteList.vue @@ -1,40 +1,51 @@ <script setup> -import VnPaginate from 'components/ui/VnPaginate.vue'; import { useStateStore } from 'stores/useStateStore'; import { useI18n } from 'vue-i18n'; -import { computed, onMounted, onUnmounted, ref } from 'vue'; +import { computed, onMounted, ref } from 'vue'; import { dashIfEmpty, toHour } from 'src/filters'; -import VnSelect from 'components/common/VnSelect.vue'; -import FetchData from 'components/FetchData.vue'; import { useValidator } from 'composables/useValidator'; +import { useSession } from 'composables/useSession'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import { useArrayData } from 'composables/useArrayData'; +import { useQuasar } from 'quasar'; + +import axios from 'axios'; +import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue'; +import FetchData from 'components/FetchData.vue'; +import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue'; +import RouteSummary from 'pages/Route/Card/RouteSummary.vue'; +import RouteFilter from 'pages/Route/Card/RouteFilter.vue'; +import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue'; +import RightMenu from 'src/components/common/RightMenu.vue'; + +import VnPaginate from 'components/ui/VnPaginate.vue'; +import VnSelect from 'components/common/VnSelect.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import VnInput from 'components/common/VnInput.vue'; import VnInputTime from 'components/common/VnInputTime.vue'; -import axios from 'axios'; -import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue'; -import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue'; -import RouteFilter from 'pages/Route/Card/RouteFilter.vue'; -import RouteSummary from 'pages/Route/Card/RouteSummary.vue'; +import VnLv from 'src/components/ui/VnLv.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; -import { useSession } from 'composables/useSession'; -import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue'; -import { useQuasar } from 'quasar'; -import { useArrayData } from 'composables/useArrayData'; -import RightMenu from 'src/components/common/RightMenu.vue'; -const stateStore = useStateStore(); const { t } = useI18n(); const { validate } = useValidator(); +const { viewSummary } = useSummaryDialog(); const quasar = useQuasar(); const session = useSession(); -const { viewSummary } = useSummaryDialog(); +const paginate = ref(); const visibleColumns = ref([]); const selectedRows = ref([]); +const workers = ref([]); +const agencyList = ref([]); +const vehicleList = ref([]); +const allColumnNames = ref([]); +const confirmationDialog = ref(false); +const startingDate = ref(null); +const refreshKey = ref(0); + const columns = computed(() => [ { - name: 'ID', - label: t('ID'), + name: 'Id', + label: t('Id'), field: (row) => row.id, sortable: true, align: 'center', @@ -109,14 +120,12 @@ const columns = computed(() => [ align: 'right', }, ]); + const arrayData = useArrayData('EntryLatestBuys', { url: 'Buys/latestBuysFilter', order: ['itemFk DESC'], }); -const refreshKey = ref(0); -const workers = ref([]); -const agencyList = ref([]); -const vehicleList = ref([]); + const updateRoute = async (route) => { try { return await axios.patch(`Routes/${route.id}`, route); @@ -124,9 +133,6 @@ const updateRoute = async (route) => { return err; } }; -const allColumnNames = ref([]); -const confirmationDialog = ref(false); -const startingDate = ref(null); const cloneRoutes = () => { axios.post('Routes/clone', { @@ -135,6 +141,7 @@ const cloneRoutes = () => { }); refreshKey.value++; startingDate.value = null; + paginate.value.fetch(); }; const showRouteReport = () => { @@ -154,15 +161,13 @@ const showRouteReport = () => { window.open(url, '_blank'); }; -const markAsServed = () => { - selectedRows.value.forEach((row) => { - if (row?.id) { - axios.patch(`Routes/${row?.id}`, { isOk: true }); - } +function markAsServed() { + selectedRows.value.forEach(async (row) => { + if (row?.id) await axios.patch(`Routes/${row?.id}`, { isOk: true }); }); refreshKey.value++; startingDate.value = null; -}; +} const openTicketsDialog = (id) => { if (!id) { @@ -179,11 +184,9 @@ const openTicketsDialog = (id) => { }; onMounted(async () => { - stateStore.rightDrawer = true; allColumnNames.value = columns.value.map((col) => col.name); await arrayData.fetch({ append: false }); }); -onUnmounted(() => (stateStore.rightDrawer = false)); </script> <template> @@ -210,6 +213,10 @@ onUnmounted(() => (stateStore.rightDrawer = false)); <QBtn flat :label="t('Cancel')" v-close-popup class="text-primary" /> <QBtn color="primary" v-close-popup @click="cloneRoutes"> {{ t('globals.clone') }} + <VnLv + :label="t('route.summary.packages')" + :value="getTotalPackages(entity.tickets)" + /> </QBtn> </QCardActions> </QCard> @@ -228,7 +235,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); class="LeftIcon" :all-columns="allColumnNames" table-code="routesList" - labels-traductions-path="globals" + labels-traductions-path="route.columnLabels" @on-config-saved="visibleColumns = [...$event]" /> </template> @@ -256,7 +263,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); color="primary" class="q-mr-sm" :disable="!selectedRows?.length" - @click="markAsServed" + @click="markAsServed()" > <QTooltip>{{ t('Mark as served') }}</QTooltip> </QBtn> @@ -269,7 +276,6 @@ onUnmounted(() => (stateStore.rightDrawer = false)); url="Routes/filter" :order="['created ASC', 'started ASC', 'id ASC']" :limit="20" - auto-load > <template #body="{ rows }"> <div class="q-pa-md route-table"> @@ -500,7 +506,6 @@ en: hourStarted: Started hour hourFinished: Finished hour es: - ID: ID Worker: Trabajador Agency: Agencia Vehicle: Vehículo @@ -521,4 +526,6 @@ es: Summary: Resumen Route is closed: La ruta está cerrada Route is not served: La ruta no está servida + hourStarted: Hora de inicio + hourFinished: Hora de fin </i18n> diff --git a/src/pages/Route/RouteRoadmap.vue b/src/pages/Route/RouteRoadmap.vue index cecc2b2c3..f3c0505c6 100644 --- a/src/pages/Route/RouteRoadmap.vue +++ b/src/pages/Route/RouteRoadmap.vue @@ -129,7 +129,7 @@ function confirmRemove() { } function navigateToRoadmapSummary(event, row) { - router.push({ name: 'RoadmapSummary', params: { id: row.id } }); + router.push({ name: 'RoadmapSummary', params: { id: 1 } }); } </script> @@ -193,7 +193,6 @@ function navigateToRoadmapSummary(event, row) { url="Roadmaps" :limit="20" :filter="filter" - auto-load > <template #body="{ rows }"> <div class="q-pa-md"> diff --git a/src/pages/Route/RouteTickets.vue b/src/pages/Route/RouteTickets.vue index ba3e855d6..fc9087032 100644 --- a/src/pages/Route/RouteTickets.vue +++ b/src/pages/Route/RouteTickets.vue @@ -141,7 +141,7 @@ const setOrderedPriority = async () => { }; const sortRoutes = async () => { - await axios.get(`Routes/${route.params?.id}/guessPriority/`); + await axios.patch(`Routes/${route.params?.id}/guessPriority/`); refreshKey.value++; }; diff --git a/src/pages/Supplier/Card/SupplierDescriptor.vue b/src/pages/Supplier/Card/SupplierDescriptor.vue index b8479d8f6..4ef76c2f8 100644 --- a/src/pages/Supplier/Card/SupplierDescriptor.vue +++ b/src/pages/Supplier/Card/SupplierDescriptor.vue @@ -17,6 +17,10 @@ const $props = defineProps({ required: false, default: null, }, + summary: { + type: Object, + default: null, + }, }); const route = useRoute(); @@ -106,6 +110,7 @@ const getEntryQueryParams = (supplier) => { :filter="filter" @on-fetch="setData" data-key="supplier" + :summary="$props.summary" > <template #header-extra-action> <QBtn diff --git a/src/pages/Supplier/Card/SupplierDescriptorProxy.vue b/src/pages/Supplier/Card/SupplierDescriptorProxy.vue index b730a39dd..6311939b8 100644 --- a/src/pages/Supplier/Card/SupplierDescriptorProxy.vue +++ b/src/pages/Supplier/Card/SupplierDescriptorProxy.vue @@ -1,5 +1,6 @@ <script setup> import SupplierDescriptor from './SupplierDescriptor.vue'; +import SupplierSummary from './SupplierSummary.vue'; const $props = defineProps({ id: { @@ -11,6 +12,6 @@ const $props = defineProps({ <template> <QPopupProxy> - <SupplierDescriptor v-if="$props.id" :id="$props.id" /> + <SupplierDescriptor v-if="$props.id" :id="$props.id" :summary="SupplierSummary" /> </QPopupProxy> </template> diff --git a/src/pages/Travel/Card/TravelBasicData.vue b/src/pages/Travel/Card/TravelBasicData.vue index f4e97c239..1eb9bbc0f 100644 --- a/src/pages/Travel/Card/TravelBasicData.vue +++ b/src/pages/Travel/Card/TravelBasicData.vue @@ -24,7 +24,7 @@ const agenciesOptions = ref([]); <FormModel :url="`Travels/${route.params.id}`" :url-update="`Travels/${route.params.id}`" - model="travel" + model="Travel" auto-load > <template #form="{ data }"> diff --git a/src/pages/Travel/Card/TravelCard.vue b/src/pages/Travel/Card/TravelCard.vue index 1d591f064..bf7e6d57a 100644 --- a/src/pages/Travel/Card/TravelCard.vue +++ b/src/pages/Travel/Card/TravelCard.vue @@ -1,7 +1,40 @@ <script setup> import VnCard from 'components/common/VnCard.vue'; import TravelDescriptor from './TravelDescriptor.vue'; + +const filter = { + fields: [ + 'id', + 'ref', + 'shipped', + 'landed', + 'totalEntries', + 'warehouseInFk', + 'warehouseOutFk', + 'cargoSupplierFk', + 'agencyModeFk', + ], + include: [ + { + relation: 'warehouseIn', + scope: { + fields: ['name'], + }, + }, + { + relation: 'warehouseOut', + scope: { + fields: ['name'], + }, + }, + ], +}; </script> <template> - <VnCard data-key="Travel" base-url="Travels" :descriptor="TravelDescriptor" /> + <VnCard + data-key="Travel" + :filter="filter" + base-url="Travels" + :descriptor="TravelDescriptor" + /> </template> diff --git a/src/pages/Travel/Card/TravelDescriptor.vue b/src/pages/Travel/Card/TravelDescriptor.vue index c7501b1d4..6d3707828 100644 --- a/src/pages/Travel/Card/TravelDescriptor.vue +++ b/src/pages/Travel/Card/TravelDescriptor.vue @@ -1,5 +1,5 @@ <script setup> -import { ref, computed } from 'vue'; +import { computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; @@ -7,7 +7,6 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import TravelDescriptorMenuItems from './TravelDescriptorMenuItems.vue'; -import useCardDescription from 'src/composables/useCardDescription'; import { toDate } from 'src/filters'; const $props = defineProps({ @@ -52,23 +51,15 @@ const filter = { const entityId = computed(() => { return $props.id || route.params.id; }); - -const data = ref(useCardDescription()); - -const setData = (entity) => { - data.value = useCardDescription(entity.ref, entity.id); -}; </script> <template> <CardDescriptor module="Travel" :url="`Travels/${entityId}`" - :title="data.title" - :subtitle="data.subtitle" + title="ref" :filter="filter" - @on-fetch="setData" - data-key="travelData" + data-key="Travel" > <template #header-extra-action> <QBtn diff --git a/src/pages/Travel/Card/TravelDescriptorMenuItems.vue b/src/pages/Travel/Card/TravelDescriptorMenuItems.vue index 17b9333ca..1bb80ff01 100644 --- a/src/pages/Travel/Card/TravelDescriptorMenuItems.vue +++ b/src/pages/Travel/Card/TravelDescriptorMenuItems.vue @@ -32,10 +32,11 @@ const cloneTravel = () => { redirectToCreateView(stringifiedTravelData); }; -const cloneTravelWithEntries = () => { +const cloneTravelWithEntries = async () => { try { - axios.post(`Travels/${$props.travel.id}/cloneWithEntries`); + const { data } = await axios.post(`Travels/${$props.travel.id}/cloneWithEntries`); notify('globals.dataSaved', 'positive'); + router.push({ name: 'TravelBasicData', params: { id: data.id } }); } catch (err) { console.err('Error cloning travel with entries'); } diff --git a/src/pages/Travel/Card/TravelSummary.vue b/src/pages/Travel/Card/TravelSummary.vue index 7fc92e7b6..91b36f0cf 100644 --- a/src/pages/Travel/Card/TravelSummary.vue +++ b/src/pages/Travel/Card/TravelSummary.vue @@ -8,7 +8,6 @@ import VnLv from 'src/components/ui/VnLv.vue'; import VnTitle from 'src/components/common/VnTitle.vue'; import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue'; import FetchData from 'src/components/FetchData.vue'; -import TravelDescriptorMenuItems from './TravelDescriptorMenuItems.vue'; import { toDate, toCurrency } from 'src/filters'; import axios from 'axios'; @@ -222,6 +221,8 @@ async function setTravelData(travelData) { console.error(`Error setting travel data`, err); } } + +const getLink = (param) => `#/travel/${entityId.value}/${param}`; </script> <template> @@ -240,21 +241,15 @@ async function setTravelData(travelData) { <template #header> <span>{{ travel.ref }} - {{ travel.id }}</span> </template> - <template #header-right> - <QBtn color="white" dense flat icon="more_vert" round size="md"> - <QTooltip> - {{ t('components.cardDescriptor.moreOptions') }} - </QTooltip> - <QMenu> - <QList> - <TravelDescriptorMenuItems :travel="travel" /> - </QList> - </QMenu> - </QBtn> - </template> <template #body> <QCard class="vn-one"> + <QCardSection class="q-pa-none"> + <VnTitle + :url="getLink('basic-data')" + :text="t('travel.pageTitles.basicData')" + /> + </QCardSection> <VnLv :label="t('globals.shipped')" :value="toDate(travel.shipped)" /> <VnLv :label="t('globals.wareHouseOut')" @@ -267,6 +262,12 @@ async function setTravelData(travelData) { /> </QCard> <QCard class="vn-one"> + <QCardSection class="q-pa-none"> + <VnTitle + :url="getLink('basic-data')" + :text="t('travel.pageTitles.basicData')" + /> + </QCardSection> <VnLv :label="t('globals.landed')" :value="toDate(travel.landed)" /> <VnLv :label="t('globals.wareHouseIn')" @@ -279,12 +280,18 @@ async function setTravelData(travelData) { /> </QCard> <QCard class="vn-one"> + <QCardSection class="q-pa-none"> + <VnTitle + :url="getLink('basic-data')" + :text="t('travel.pageTitles.basicData')" + /> + </QCardSection> <VnLv :label="t('globals.agency')" :value="travel.agency?.name" /> <VnLv :label="t('globals.reference')" :value="travel.ref" /> <VnLv label="m³" :value="travel.m3" /> <VnLv :label="t('globals.totalEntries')" :value="travel.totalEntries" /> </QCard> - <QCard class="full-width" v-if="entriesTableRows.length > 0"> + <QCard class="full-width"> <VnTitle :text="t('travel.summary.entries')" /> <QTable :rows="entriesTableRows" @@ -299,13 +306,15 @@ async function setTravelData(travelData) { </QTh> </QTr> </template> - <template #body-cell-isConfirmed="{ col, value }"> + <template #body-cell-isConfirmed="{ col, row }"> <QTd> - <QIcon + <QCheckbox v-if="col.name === 'isConfirmed'" - :name="value ? 'check' : 'close'" - :color="value ? 'positive' : 'negative'" - size="sm" + :label="t('travel.summary.received')" + :true-value="1" + :false-value="0" + v-model="row[col.name]" + :disable="true" /> </QTd> </template> diff --git a/src/pages/Travel/ExtraCommunity.vue b/src/pages/Travel/ExtraCommunity.vue index 607ecb560..639c7d894 100644 --- a/src/pages/Travel/ExtraCommunity.vue +++ b/src/pages/Travel/ExtraCommunity.vue @@ -53,6 +53,7 @@ const draggedRowIndex = ref(null); const targetRowIndex = ref(null); const entryRowIndex = ref(null); const draggedEntry = ref(null); +const travelKgPercentages = ref([]); const tableColumnComponents = { id: { @@ -88,6 +89,10 @@ const tableColumnComponents = { component: 'span', attrs: {}, }, + percentage: { + component: 'span', + attrs: {}, + }, kg: { component: VnInput, attrs: { dense: true, type: 'number', min: 0, class: 'input-number' }, @@ -179,6 +184,14 @@ const columns = computed(() => [ showValue: true, sortable: true, }, + { + label: '%', + field: '', + name: 'percentage', + align: 'center', + showValue: false, + sortable: true, + }, { label: t('kg'), field: 'kg', @@ -278,6 +291,8 @@ const saveFieldValue = async (val, field, index) => { await axios.patch(`Travels/${id}`, params); // Actualizar la copia de los datos originales con el nuevo valor originalRowDataCopy.value[index][field] = val; + + await arrayData.fetch({ append: false }); } catch (err) { console.error('Error updating travel'); } @@ -302,6 +317,11 @@ onMounted(async () => { landedTo.value.setDate(landedTo.value.getDate() + 7); landedTo.value.setHours(23, 59, 59, 59); + const { data } = await axios.get('TravelKgPercentages', { + params: { filter: JSON.stringify({ order: 'value DESC' }) }, + }); + + travelKgPercentages.value = data; await getData(); }); @@ -419,6 +439,11 @@ const handleDragScroll = (event) => { stopScroll(); } }; + +const getColor = (percentage) => { + for (const { value, className } of travelKgPercentages.value) + if (percentage > value) return className; +}; </script> <template> @@ -460,7 +485,7 @@ const handleDragScroll = (event) => { <template #body="props"> <QTr :props="props" - class="cursor-pointer bg-vn-primary-row" + class="cursor-pointer bg-travel" @click="navigateToTravelId(props.row.id)" @dragenter="handleDragEnter($event, props.rowIndex)" @dragover.prevent @@ -494,18 +519,32 @@ const handleDragScroll = (event) => { : {} " > - <template v-if="col.showValue"> - <span - :class="[ - 'text-left', - { - 'supplier-name': - col.name === 'cargoSupplierNickname', - }, - ]" - >{{ col.value }}</span - > - </template> + <QChip + v-if="col.name === 'percentage'" + :label=" + props.row.percentageKg + ? `${props.row.percentageKg}%` + : '-' + " + class="text-left q-py-xs q-px-sm" + :color="getColor(props.row.percentageKg)" + /> + <span + v-else-if="col.showValue" + :class="[ + 'text-left', + { + 'supplier-name': + col.name === 'cargoSupplierNickname', + }, + { + link: ['id', 'cargoSupplierNickname'].includes( + col.name + ), + }, + ]" + v-text="col.value" + /> <!-- Main Row Descriptors --> <TravelDescriptorProxy v-if="col.name === 'id'" @@ -539,11 +578,11 @@ const handleDragScroll = (event) => { }" > <QTd> - <QBtn flat color="primary">{{ entry.id }} </QBtn> + <QBtn flat class="link">{{ entry.id }} </QBtn> <EntryDescriptorProxy :id="entry.id" /> </QTd> <QTd> - <QBtn flat color="primary" dense>{{ entry.supplierName }}</QBtn> + <QBtn flat class="link" dense>{{ entry.supplierName }}</QBtn> <SupplierDescriptorProxy :id="entry.supplierFk" /> </QTd> <QTd /> @@ -556,6 +595,7 @@ const handleDragScroll = (event) => { <QTd> <span>{{ entry.stickers }}</span> </QTd> + <QTd /> <QTd></QTd> <QTd> <span>{{ entry.loadedkg }}</span> @@ -574,10 +614,23 @@ const handleDragScroll = (event) => { </template> <style scoped lang="scss"> +.q-chip { + color: var(--vn-text-color); +} + :deep(.q-table) { border-collapse: collapse; } +.q-td :deep(input) { + font-weight: bold; +} + +.bg-travel { + background-color: var(--vn-page-color); + border-bottom: 2px solid $primary; +} + .dashed-border { &.--left { border-left: 1px dashed #ccc; diff --git a/src/pages/Travel/TravelCreate.vue b/src/pages/Travel/TravelCreate.vue index 53c8d402d..09bf58765 100644 --- a/src/pages/Travel/TravelCreate.vue +++ b/src/pages/Travel/TravelCreate.vue @@ -1,6 +1,6 @@ <script setup> import { useI18n } from 'vue-i18n'; -import { reactive, ref, onBeforeMount } from 'vue'; +import { ref, onBeforeMount } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import FetchData from 'components/FetchData.vue'; @@ -15,29 +15,19 @@ const { t } = useI18n(); const route = useRoute(); const router = useRouter(); -const newTravelForm = reactive({ - ref: null, - agencyModeFk: null, - shipped: null, - landed: null, - warehouseOutFk: null, - warehouseInFk: null, -}); - const agenciesOptions = ref([]); const warehousesOptions = ref([]); const viewAction = ref(); +const newTravelForm = ref({}); onBeforeMount(() => { - // Esto nos permite decirle a FormModel si queremos observar los cambios o no - // Ya que si queremos clonar queremos que nos permita guardar inmediatamente sin realizar cambios en el form viewAction.value = route.query.travelData ? 'clone' : 'create'; if (route.query.travelData) { const travelData = JSON.parse(route.query.travelData); - for (let key in newTravelForm) { - newTravelForm[key] = travelData[key]; - } + + newTravelForm.value = { ...newTravelForm.value, ...travelData }; + delete newTravelForm.value.id; } }); @@ -60,8 +50,8 @@ const redirectToTravelBasicData = (_, { id }) => { <QPage> <VnSubToolbar /> <FormModel - url-update="Travels" - model="travel" + url-create="Travels" + model="travelCreate" :form-initial-data="newTravelForm" :observe-form-changes="viewAction === 'create'" @on-data-saved="redirectToTravelBasicData" diff --git a/src/pages/Worker/Card/WorkerNotificationsManager.vue b/src/pages/Worker/Card/WorkerNotificationsManager.vue index 44573adca..8699392e0 100644 --- a/src/pages/Worker/Card/WorkerNotificationsManager.vue +++ b/src/pages/Worker/Card/WorkerNotificationsManager.vue @@ -20,8 +20,8 @@ const { t } = useI18n(); const quasar = useQuasar(); const entityId = computed(() => $props.id || route.params.id); const URL_KEY = 'NotificationSubscriptions'; -const active = ref(); -const available = ref(); +const active = ref(new Map()); +const available = ref(new Map()); async function toggleNotification(notification) { try { @@ -56,6 +56,7 @@ const swapEntry = (from, to, key) => { }; function setNotifications(data) { + console.log('data: ', data); active.value = new Map(data.active); available.value = new Map(data.available); } diff --git a/src/router/modules/account.js b/src/router/modules/account.js index f325a8dcd..6f3f8c25b 100644 --- a/src/router/modules/account.js +++ b/src/router/modules/account.js @@ -21,7 +21,14 @@ export default { 'AccountAcls', 'AccountConnections', ], - card: [], + card: [ + 'AccountBasicData', + 'AccountInheritedRoles', + 'AccountMailForwarding', + 'AccountMailAlias', + 'AccountPrivileges', + 'AccountLog', + ], }, children: [ { @@ -112,5 +119,81 @@ export default { }, ], }, + { + name: 'AccountCard', + path: ':id', + component: () => import('src/pages/Account/Card/AccountCard.vue'), + redirect: { name: 'AccountSummary' }, + children: [ + { + name: 'AccountSummary', + path: 'summary', + meta: { + title: 'summary', + icon: 'launch', + }, + component: () => import('src/pages/Account/Card/AccountSummary.vue'), + }, + { + name: 'AccountBasicData', + path: 'basic-data', + meta: { + title: 'basicData', + icon: 'vn:settings', + }, + component: () => + import('src/pages/Account/Card/AccountBasicData.vue'), + }, + { + name: 'AccountInheritedRoles', + path: 'inherited-roles', + meta: { + title: 'inheritedRoles', + icon: 'group', + }, + component: () => + import('src/pages/Account/Card/AccountInheritedRoles.vue'), + }, + { + name: 'AccountMailForwarding', + path: 'mail-forwarding', + meta: { + title: 'mailForwarding', + icon: 'forward', + }, + component: () => + import('src/pages/Account/Card/AccountMailForwarding.vue'), + }, + { + name: 'AccountMailAlias', + path: 'mail-alias', + meta: { + title: 'mailAlias', + icon: 'email', + }, + component: () => + import('src/pages/Account/Card/AccountMailAlias.vue'), + }, + { + name: 'AccountPrivileges', + path: 'privileges', + meta: { + title: 'privileges', + icon: 'badge', + }, + component: () => + import('src/pages/Account/Card/AccountPrivileges.vue'), + }, + { + name: 'AccountLog', + path: 'log', + meta: { + title: 'log', + icon: 'history', + }, + component: () => import('src/pages/Account/Card/AccountLog.vue'), + }, + ], + }, ], }; diff --git a/test/cypress/integration/VnLocation.spec.js b/test/cypress/integration/VnLocation.spec.js index 84b2086cc..6719d8391 100644 --- a/test/cypress/integration/VnLocation.spec.js +++ b/test/cypress/integration/VnLocation.spec.js @@ -2,8 +2,7 @@ const locationOptions = '[role="listbox"] > div.q-virtual-scroll__content > .q-i describe('VnLocation', () => { const dialogInputs = '.q-dialog label input'; describe('Worker Create', () => { - const inputLocation = - '.q-form .q-card > :nth-child(3) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container'; + const inputLocation = '.q-form input[aria-label="Location"]'; beforeEach(() => { cy.viewport(1280, 720); cy.login('developer'); @@ -25,9 +24,6 @@ describe('VnLocation', () => { cy.get(inputLocation).clear(); cy.get(inputLocation).type('ecuador'); cy.get(locationOptions).should('have.length.at.least', 1); - cy.get( - '.q-form .q-card > :nth-child(3) > .q-field > .q-field__inner > .q-field__control > :nth-child(3) > .q-icon' - ).click(); }); }); describe('Fiscal-data', () => { @@ -38,9 +34,7 @@ describe('VnLocation', () => { cy.waitForElement('.q-form'); }); it('Create postCode', function () { - cy.get( - ':nth-child(6) > .q-field > .q-field__inner > .q-field__control > :nth-child(2) > .q-icon' - ).click(); + cy.get('.q-form > .q-card > .vn-row:nth-child(6) .--add-icon').click(); cy.get('.q-card > h1').should('have.text', 'New postcode'); cy.get(dialogInputs).eq(0).clear('12'); cy.get(dialogInputs).eq(0).type('1234453'); diff --git a/test/cypress/integration/invoiceIn/invoiceInDueDay.spec.js b/test/cypress/integration/invoiceIn/invoiceInDueDay.spec.js index 124b60c34..5a5becd22 100644 --- a/test/cypress/integration/invoiceIn/invoiceInDueDay.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInDueDay.spec.js @@ -22,7 +22,7 @@ describe('InvoiceInDueDay', () => { cy.waitForElement('thead'); cy.get(addBtn).click(); - cy.saveCard(); + cy.get('tbody > :nth-child(1)').should('exist'); cy.get('.q-notification__message').should('have.text', 'Data saved'); }); }); diff --git a/test/cypress/integration/worker/workerPda.spec.js b/test/cypress/integration/worker/workerPda.spec.js index 4184735ae..fe8efa834 100644 --- a/test/cypress/integration/worker/workerPda.spec.js +++ b/test/cypress/integration/worker/workerPda.spec.js @@ -10,7 +10,6 @@ describe('WorkerPda', () => { it('assign pda', () => { cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); cy.get(deviceProductionField).type('{downArrow}{enter}'); - cy.get('.vn-row > #simSerialNumber').type('123{enter}'); cy.get('.q-notification__message').should('have.text', 'Data created'); }); diff --git a/test/vitest/__tests__/composables/useArrayData.spec.js b/test/vitest/__tests__/composables/useArrayData.spec.js index ae0ca7368..5e4d12560 100644 --- a/test/vitest/__tests__/composables/useArrayData.spec.js +++ b/test/vitest/__tests__/composables/useArrayData.spec.js @@ -1,31 +1,98 @@ -import { describe, expect, it, beforeAll } from 'vitest'; -import { axios } from 'app/test/vitest/helper'; +import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'; +import { axios, flushPromises } from 'app/test/vitest/helper'; import { useArrayData } from 'composables/useArrayData'; +import { useRouter } from 'vue-router'; +import * as vueRouter from 'vue-router'; describe('useArrayData', () => { - let arrayData; - beforeAll(() => { - axios.get.mockResolvedValue({ data: [] }); - arrayData = useArrayData('InvoiceIn', { url: 'invoice-in/list' }); - Object.defineProperty(window.location, 'href', { - writable: true, - value: 'localhost:9000/invoice-in/list', + const filter = '{"order":"","limit":10,"skip":0}'; + const params = { supplierFk: 2 }; + beforeEach(() => { + vi.spyOn(useRouter(), 'replace'); + vi.spyOn(useRouter(), 'push'); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should fetch and repalce url with new params', async () => { + vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] }); + + const arrayData = useArrayData('ArrayData', { url: 'mockUrl' }); + + arrayData.store.userParams = params; + arrayData.fetch({}); + + await flushPromises(); + const routerReplace = useRouter().replace.mock.calls[0][0]; + + expect(axios.get.mock.calls[0][1].params).toEqual({ + filter, + supplierFk: 2, + }); + expect(routerReplace.path).toEqual('mockSection/list'); + expect(JSON.parse(routerReplace.query.params)).toEqual( + expect.objectContaining(params) + ); + }); + + it('Should get data and send new URL without keeping parameters, if there is only one record', async () => { + vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }] }); + + const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} }); + + arrayData.store.userParams = params; + arrayData.fetch({}); + + await flushPromises(); + const routerPush = useRouter().push.mock.calls[0][0]; + + expect(axios.get.mock.calls[0][1].params).toEqual({ + filter, + supplierFk: 2, + }); + expect(routerPush.path).toEqual('mockName/1'); + expect(routerPush.query).toBeUndefined(); + }); + + it('Should get data and send new URL keeping parameters, if you have more than one record', async () => { + vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }] }); + + vi.spyOn(vueRouter, 'useRoute').mockReturnValue({ + matched: [], + query: {}, + params: {}, + meta: { moduleName: 'mockName' }, + path: 'mockName/1', + }); + vi.spyOn(vueRouter, 'useRouter').mockReturnValue({ + push: vi.fn(), + replace: vi.fn(), + currentRoute: { + value: { + params: { + id: 1, + }, + meta: { moduleName: 'mockName' }, + matched: [{ path: 'mockName/:id' }], + }, + }, }); - // Mock the window.history.pushState method within useArrayData - window.history.pushState = (data, title, url) => (window.location.href = url); + const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} }); - // Mock the URL constructor within useArrayData - global.URL = class URL { - constructor(url) { - this.hash = url.split('localhost:9000/')[1]; - } - }; - }); + arrayData.store.userParams = params; + arrayData.fetch({}); - it('should add the params to the url', async () => { - arrayData.store.userParams = { supplierFk: 2 }; - arrayData.updateStateParams(); - expect(window.location.href).contain('params=%7B%22supplierFk%22%3A2%7D'); + await flushPromises(); + const routerPush = useRouter().push.mock.calls[0][0]; + + expect(axios.get.mock.calls[0][1].params).toEqual({ + filter, + supplierFk: 2, + }); + expect(routerPush.path).toEqual('mockName/'); + expect(routerPush.query.params).toBeDefined(); }); }); diff --git a/test/vitest/helper.js b/test/vitest/helper.js index 4eeea25a8..d6721d817 100644 --- a/test/vitest/helper.js +++ b/test/vitest/helper.js @@ -15,16 +15,19 @@ installQuasarPlugin({ }); const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false }); const mockPush = vi.fn(); +const mockReplace = vi.fn(); vi.mock('vue-router', () => ({ useRouter: () => ({ push: mockPush, + replace: mockReplace, currentRoute: { value: { params: { id: 1, }, meta: { moduleName: 'mockName' }, + matched: [{ path: 'mockName/list' }], }, }, }), @@ -33,6 +36,7 @@ vi.mock('vue-router', () => ({ query: {}, params: {}, meta: { moduleName: 'mockName' }, + path: 'mockSection/list', }), }));