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/common/VnInput.vue b/src/components/common/VnInput.vue index c84a55122..0171f0c8d 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -78,7 +78,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/i18n/locale/en.yml b/src/i18n/locale/en.yml index 5ef3a5472..6dd464ac5 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 @@ -991,6 +992,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 cfadd57b3..e5a27f3ac 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 @@ -976,6 +978,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/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/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/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/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/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/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'); });