diff --git a/src/components/CreateBankEntityForm.vue b/src/components/CreateBankEntityForm.vue index c46ac7752..1117225c7 100644 --- a/src/components/CreateBankEntityForm.vue +++ b/src/components/CreateBankEntityForm.vue @@ -1,5 +1,5 @@ <script setup> -import { reactive, ref, onMounted, nextTick } from 'vue'; +import { reactive, ref, onMounted, nextTick, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import VnInput from 'src/components/common/VnInput.vue'; @@ -7,16 +7,21 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import FetchData from 'components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import FormModelPopup from './FormModelPopup.vue'; +import { useState } from 'src/composables/useState'; defineProps({ showEntityField: { type: Boolean, default: true } }); const emit = defineEmits(['onDataSaved']); const { t } = useI18n(); const bicInputRef = ref(null); +const state = useState(); + +const customer = computed(() => state.get('customer')); + const bankEntityFormData = reactive({ name: null, bic: null, - countryFk: null, + countryFk: customer.value.countryFk, id: null, }); diff --git a/src/components/FetchData.vue b/src/components/FetchData.vue index 2a0864d3e..3038aa88e 100644 --- a/src/components/FetchData.vue +++ b/src/components/FetchData.vue @@ -44,7 +44,7 @@ onMounted(async () => { async function fetch(fetchFilter = {}) { try { - const filter = Object.assign(fetchFilter, $props.filter); // eslint-disable-line vue/no-dupe-keys + const filter = { ...fetchFilter, ...$props.filter }; // eslint-disable-line vue/no-dupe-keys if ($props.where && !fetchFilter.where) filter.where = $props.where; if ($props.sortBy) filter.order = $props.sortBy; if ($props.limit) filter.limit = $props.limit; diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 899e4d000..0ed3de261 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -69,9 +69,13 @@ const $props = defineProps({ type: Boolean, default: false, }, + disableInfiniteScroll: { + type: Boolean, + default: false, + }, hasSubToolbar: { type: Boolean, - default: true, + default: null, }, disableOption: { type: Object, @@ -299,7 +303,7 @@ defineExpose({ CrudModelRef, }); -function handleOnDataSaved(_, res) { +function handleOnDataSaved(_) { if (_.onDataSaved) _.onDataSaved(this); else $props.create.onDataSaved(_); } @@ -357,17 +361,18 @@ function handleOnDataSaved(_, res) { </QScrollArea> </QDrawer> <!-- class in div to fix warn--> - <CrudModel v-bind="$attrs" :class="$attrs['class'] ?? 'q-px-md'" - :limit="20" + :limit="$attrs['limit'] ?? 20" ref="CrudModelRef" @on-fetch="(...args) => emit('onFetch', ...args)" :search-url="searchUrl" - :disable-infinite-scroll="isTableMode" + :disable-infinite-scroll=" + $attrs['disableInfiniteScroll'] ? isTableMode : disableInfiniteScroll + " @save-changes="reload" - :has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable" + :has-sub-toolbar="$props.hasSubToolbar ?? isEditable" :auto-load="hasParams || $attrs['auto-load']" > <template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName"> @@ -633,7 +638,7 @@ function handleOnDataSaved(_, res) { </QTable> </template> </CrudModel> - <QPageSticky :offset="[20, 20]" style="z-index: 2"> + <QPageSticky v-if="$props.create" :offset="[20, 20]" style="z-index: 2"> <QBtn @click=" () => @@ -645,7 +650,7 @@ function handleOnDataSaved(_, res) { shortcut="+" /> <QTooltip> - {{ createForm.title }} + {{ createForm?.title }} </QTooltip> </QPageSticky> <QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> @@ -759,6 +764,7 @@ es: } .q-table__top { top: 0; + padding: 12px 0; } tbody { .q-checkbox { diff --git a/src/components/common/VnCard.vue b/src/components/common/VnCard.vue index 5cee06582..7d29da232 100644 --- a/src/components/common/VnCard.vue +++ b/src/components/common/VnCard.vue @@ -79,7 +79,7 @@ if (props.baseUrl) { <QPage> <VnSubToolbar /> <div :class="[useCardSize(), $attrs.class]"> - <RouterView :key="route.fullPath" /> + <RouterView :key="route.path" /> </div> </QPage> </QPageContainer> diff --git a/src/components/common/VnSelectDialog.vue b/src/components/common/VnSelectDialog.vue index 219e6dc98..17f893255 100644 --- a/src/components/common/VnSelectDialog.vue +++ b/src/components/common/VnSelectDialog.vue @@ -28,7 +28,6 @@ const $props = defineProps({ const role = useRole(); const acl = useAcl(); -const showForm = ref(false); const isAllowedToCreate = computed(() => { if ($props.acls.length) return acl.hasAny($props.acls); diff --git a/src/components/ui/CatalogItem.vue b/src/components/ui/CatalogItem.vue index ef722483b..545bfbbb4 100644 --- a/src/components/ui/CatalogItem.vue +++ b/src/components/ui/CatalogItem.vue @@ -127,11 +127,6 @@ const dialog = ref(null); flex-direction: column; gap: 4px; - .subName { - color: var(--vn-label-color); - text-transform: uppercase; - } - p { margin-bottom: 0; } diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index cebdc4bbf..637180c22 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -3,6 +3,7 @@ import { onMounted, ref, computed, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useArrayData } from 'composables/useArrayData'; import { useRoute } from 'vue-router'; +import { date } from 'quasar'; import toDate from 'filters/toDate'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; @@ -185,6 +186,7 @@ async function remove(key) { } function formatValue(value) { + if (value instanceof Date) return date.formatDate(value, 'DD/MM/YYYY'); if (typeof value === 'boolean') return value ? t('Yes') : t('No'); if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value); diff --git a/src/components/ui/VnImg.vue b/src/components/ui/VnImg.vue index 9585b81d8..7dbc3ac2a 100644 --- a/src/components/ui/VnImg.vue +++ b/src/components/ui/VnImg.vue @@ -52,6 +52,7 @@ defineExpose({ </script> <template> <QImg + :draggable="true" :class="{ zoomIn: zoom }" :src="getUrl()" v-bind="$attrs" @@ -60,6 +61,7 @@ defineExpose({ /> <QDialog v-if="$props.zoom" v-model="show"> <QImg + :draggable="true" :src="getUrl(true)" v-bind="$attrs" spinner-color="primary" diff --git a/src/components/ui/VnUserLink.vue b/src/components/ui/VnUserLink.vue index b04ea7476..00c50ee34 100644 --- a/src/components/ui/VnUserLink.vue +++ b/src/components/ui/VnUserLink.vue @@ -1,20 +1,18 @@ <script setup> import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; -import { useI18n } from 'vue-i18n'; -const $props = defineProps({ +defineProps({ name: { type: String, default: null }, + tag: { type: String, default: null }, workerId: { type: Number, default: null }, defaultName: { type: Boolean, default: false }, }); - -const { t } = useI18n(); </script> <template> <slot name="link"> - <span :class="{ link: $props.workerId }"> - {{ $props.defaultName ? $props.name ?? t('globals.system') : $props.name }} + <span :class="{ link: workerId }"> + {{ defaultName ? name ?? $t('globals.system') : name }} </span> </slot> - <WorkerDescriptorProxy v-if="$props.workerId" :id="$props.workerId" /> + <WorkerDescriptorProxy v-if="workerId" :id="workerId" /> </template> diff --git a/src/composables/usePrintService.js b/src/composables/usePrintService.js index edf9598b2..ff43c65a1 100644 --- a/src/composables/usePrintService.js +++ b/src/composables/usePrintService.js @@ -17,6 +17,7 @@ export function usePrintService() { } function openReport(path, params) { + if (typeof params === 'string') params = JSON.parse(params); params = Object.assign( { access_token: getTokenMultimedia(), diff --git a/src/css/app.scss b/src/css/app.scss index ecffd576c..3c51dc8af 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -275,3 +275,8 @@ input::-webkit-inner-spin-button { z-index: 1; cursor: pointer; } + +.subName { + color: var(--vn-label-color); + text-transform: uppercase; +} diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index da86399af..d29b864b9 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -2,6 +2,7 @@ globals: lang: es: Spanish en: English + quantity: Quantity language: Language entity: Entity user: User @@ -98,6 +99,9 @@ globals: to: To notes: Notes refresh: Refresh + item: Item + ticket: Ticket + campaign: Campaign weight: Weight pageTitles: logIn: Login @@ -125,6 +129,7 @@ globals: notifications: Notifications defaulter: Defaulter customerCreate: New customer + createOrder: New order fiscalData: Fiscal data billingData: Billing data consignees: Consignees @@ -315,135 +320,6 @@ resetPassword: repeatPassword: Repeat password passwordNotMatch: Passwords don't match passwordChanged: Password changed -customer: - list: - phone: Phone - email: Email - customerOrders: Display customer orders - moreOptions: More options - card: - customerList: Customer list - customerId: Claim ID - salesPerson: Sales person - credit: Credit - risk: Risk - securedCredit: Secured credit - payMethod: Pay method - debt: Debt - isFrozen: Customer frozen - hasDebt: Customer has debt - isDisabled: Customer inactive - notChecked: Customer no checked - webAccountInactive: Web account inactive - noWebAccess: Web access is disabled - businessType: Business type - passwordRequirements: 'The password must have at least { length } length characters, {nAlpha} alphabetic characters, {nUpper} capital letters, {nDigits} digits and {nPunct} symbols (Ex: $%&.)\n' - businessTypeFk: Business type - summary: - basicData: Basic data - fiscalAddress: Fiscal address - fiscalData: Fiscal data - billingData: Billing data - consignee: Default consignee - businessData: Business data - financialData: Financial data - customerId: Customer ID - name: Name - contact: Contact - phone: Phone - mobile: Mobile - email: Email - salesPerson: Sales person - contactChannel: Contact channel - socialName: Social name - fiscalId: Fiscal ID - postcode: Postcode - province: Province - country: Country - street: Address - isEqualizated: Is equalizated - isActive: Is active - invoiceByAddress: Invoice by address - verifiedData: Verified data - hasToInvoice: Has to invoice - notifyByEmail: Notify by email - vies: VIES - payMethod: Pay method - bankAccount: Bank account - dueDay: Due day - hasLcr: Has LCR - hasCoreVnl: Has core VNL - hasB2BVnl: Has B2B VNL - addressName: Address name - addressCity: City - addressStreet: Street - username: Username - webAccess: Web access - totalGreuge: Total greuge - mana: Mana - priceIncreasingRate: Price increasing rate - averageInvoiced: Average invoiced - claimRate: Claming rate - risk: Risk - riskInfo: Invoices minus payments plus orders not yet invoiced - credit: Credit - creditInfo: Company's maximum risk - securedCredit: Secured credit - securedCreditInfo: Solunion's maximum risk - balance: Balance - balanceInfo: Invoices minus payments - balanceDue: Balance due - balanceDueInfo: Deviated invoices minus payments - recoverySince: Recovery since - businessType: Business Type - city: City - descriptorInfo: Invoices minus payments plus orders not yet - rating: Rating - recommendCredit: Recommended credit - basicData: - socialName: Fiscal name - businessType: Business type - contact: Contact - youCanSaveMultipleEmails: You can save multiple emails - email: Email - phone: Phone - mobile: Mobile - salesPerson: Sales person - contactChannel: Contact channel - previousClient: Previous client - extendedList: - tableVisibleColumns: - id: Identifier - name: Name - socialName: Social name - fi: Tax number - salesPersonFk: Salesperson - credit: Credit - creditInsurance: Credit insurance - phone: Phone - mobile: Mobile - street: Street - countryFk: Country - provinceFk: Province - city: City - postcode: Postcode - email: Email - created: Created - businessTypeFk: Business type - payMethodFk: Billing data - sageTaxTypeFk: Sage tax type - sageTransactionTypeFk: Sage tr. type - isActive: Active - isVies: Vies - isTaxDataChecked: Verified data - isEqualizated: Is equalizated - isFreezed: Freezed - hasToInvoice: Invoice - hasToInvoiceByAddress: Invoice by address - isToBeMailed: Mailing - hasLcr: Received LCR - hasCoreVnl: VNL core received - hasSepaVnl: VNL B2B received entry: list: newEntry: New entry diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 5e8efff1e..3d905509d 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -3,6 +3,7 @@ globals: es: Español en: Inglés language: Idioma + quantity: Cantidad entity: Entidad user: Usuario details: Detalles @@ -100,6 +101,9 @@ globals: to: Hasta notes: Notas refresh: Actualizar + item: Artículo + ticket: Ticket + campaign: Campaña weight: Peso pageTitles: logIn: Inicio de sesión @@ -122,6 +126,7 @@ globals: inheritedRoles: Roles heredados customers: Clientes customerCreate: Nuevo cliente + createOrder: Nuevo pedido list: Listado webPayments: Pagos Web extendedList: Listado extendido @@ -317,134 +322,6 @@ resetPassword: repeatPassword: Repetir contraseña passwordNotMatch: Las contraseñas no coinciden passwordChanged: Contraseña cambiada -customer: - list: - phone: Teléfono - email: Email - customerOrders: Mostrar órdenes del cliente - moreOptions: Más opciones - card: - customerId: ID cliente - salesPerson: Comercial - credit: Crédito - risk: Riesgo - securedCredit: Crédito asegurado - payMethod: Método de pago - debt: Riesgo - isFrozen: Cliente congelado - hasDebt: Cliente con riesgo - isDisabled: Cliente inactivo - notChecked: Cliente no comprobado - webAccountInactive: Sin acceso web - noWebAccess: El acceso web está desactivado - businessType: Tipo de negocio - passwordRequirements: 'La contraseña debe tener al menos { length } caracteres de longitud, {nAlpha} caracteres alfabéticos, {nUpper} letras mayúsculas, {nDigits} dígitos y {nPunct} símbolos (Ej: $%&.)' - businessTypeFk: Tipo de negocio - summary: - basicData: Datos básicos - fiscalAddress: Dirección fiscal - fiscalData: Datos fiscales - billingData: Datos de facturación - consignee: Consignatario pred. - businessData: Datos comerciales - financialData: Datos financieros - customerId: ID cliente - name: Nombre - contact: Contacto - phone: Teléfono - mobile: Móvil - email: Email - salesPerson: Comercial - contactChannel: Canal de contacto - socialName: Razón social - fiscalId: NIF/CIF - postcode: Código postal - province: Provincia - country: País - street: Calle - isEqualizated: Recargo de equivalencia - isActive: Activo - invoiceByAddress: Facturar por consignatario - verifiedData: Datos verificados - hasToInvoice: Facturar - notifyByEmail: Notificar por email - vies: VIES - payMethod: Método de pago - bankAccount: Cuenta bancaria - dueDay: Día de pago - hasLcr: Recibido LCR - hasCoreVnl: Recibido core VNL - hasB2BVnl: Recibido B2B VNL - addressName: Nombre de la dirección - addressCity: Ciudad - addressStreet: Calle - username: Usuario - webAccess: Acceso web - totalGreuge: Greuge total - mana: Maná - priceIncreasingRate: Ratio de incremento de precio - averageInvoiced: Facturación media - claimRate: Ratio de reclamaciones - risk: Riesgo - riskInfo: Facturas menos recibos mas pedidos sin facturar - credit: Crédito - creditInfo: Riesgo máximo asumido por la empresa - securedCredit: Crédito asegurado - securedCreditInfo: Riesgo máximo asumido por Solunion - balance: Balance - balanceInfo: Facturas menos recibos - balanceDue: Saldo vencido - balanceDueInfo: Facturas fuera de plazo menos recibos - recoverySince: Recobro desde - businessType: Tipo de negocio - city: Población - descriptorInfo: Facturas menos recibos mas pedidos sin facturar - rating: Clasificación - recommendCredit: Crédito recomendado - basicData: - socialName: Nombre fiscal - businessType: Tipo de negocio - contact: Contacto - youCanSaveMultipleEmails: Puede guardar varios correos electrónicos encadenándolos mediante comas sin espacios{','} ejemplo{':'} user{'@'}dominio{'.'}com, user2{'@'}dominio{'.'}com siendo el primer correo electrónico el principal - email: Email - phone: Teléfono - mobile: Móvil - salesPerson: Comercial - contactChannel: Canal de contacto - previousClient: Cliente anterior - extendedList: - tableVisibleColumns: - id: Identificador - name: Nombre - socialName: Razón social - fi: NIF / CIF - salesPersonFk: Comercial - credit: Crédito - creditInsurance: Crédito asegurado - phone: Teléfono - mobile: Móvil - street: Dirección fiscal - countryFk: País - provinceFk: Provincia - city: Población - postcode: Código postal - email: Email - created: Fecha creación - businessTypeFk: Tipo de negocio - payMethodFk: Forma de pago - sageTaxTypeFk: Tipo de impuesto Sage - sageTransactionTypeFk: Tipo tr. sage - isActive: Activo - isVies: Vies - isTaxDataChecked: Datos comprobados - isEqualizated: Recargo de equivalencias - isFreezed: Congelado - hasToInvoice: Factura - hasToInvoiceByAddress: Factura por consigna - isToBeMailed: Env. emails - hasLcr: Recibido LCR - hasCoreVnl: Recibido core VNL - hasSepaVnl: Recibido B2B VNL entry: list: newEntry: Nueva entrada diff --git a/src/pages/Customer/Card/CustomerBalance.vue b/src/pages/Customer/Card/CustomerBalance.vue index 95f5f0981..70747bb3f 100644 --- a/src/pages/Customer/Card/CustomerBalance.vue +++ b/src/pages/Customer/Card/CustomerBalance.vue @@ -95,12 +95,7 @@ const columns = computed(() => [ label: t('Employee'), columnField: { component: 'userLink', - attrs: ({ row }) => { - return { - workerId: row.workerFk, - name: row.userName, - }; - }, + attrs: ({ row }) => ({ workerId: row.workerFk, tag: row.userName }), }, cardVisible: true, }, @@ -259,6 +254,7 @@ const showBalancePdf = ({ id }) => { :is-editable="false" :column-search="false" @on-fetch="onFetch" + :disable-option="{ card: true }" auto-load > <template #column-balance="{ rowIndex }"> diff --git a/src/pages/Customer/Card/CustomerBasicData.vue b/src/pages/Customer/Card/CustomerBasicData.vue index a6150bb3f..8cc59ef3c 100644 --- a/src/pages/Customer/Card/CustomerBasicData.vue +++ b/src/pages/Customer/Card/CustomerBasicData.vue @@ -25,6 +25,7 @@ const title = ref(); /> <FetchData url="BusinessTypes" + :filter="{ fields: ['code', 'description'], order: 'description ASC ' }" @on-fetch="(data) => (businessTypes = data)" auto-load /> @@ -38,7 +39,7 @@ const title = ref(); clearable v-model="data.name" /> - <QSelect + <VnSelect :input-debounce="0" :label="t('customer.basicData.businessType')" :options="businessTypes" @@ -89,20 +90,18 @@ const title = ref(); </VnRow> <VnRow> <VnSelect - url="Workers/activeWithInheritedRole" - :filter="{ where: { role: 'salesPerson' } }" - option-filter="firstName" + url="Workers/search" v-model="data.salesPersonFk" :label="t('customer.basicData.salesPerson')" + :params="{ + departmentCodes: ['VT', 'shopping'], + }" + :fields="['id', 'nickname']" + sort-by="nickname ASC" :rules="validate('client.salesPersonFk')" :use-like="false" - :emit-value="false" - @update:model-value=" - (val) => { - title = val?.nickname; - data.salesPersonFk = val?.id; - } - " + emit-value + auto-load > <template #prepend> <VnAvatar @@ -111,8 +110,19 @@ const title = ref(); :title="title" /> </template> + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel>{{ scope.opt?.name }}</QItemLabel> + <QItemLabel caption + >{{ scope.opt?.nickname }}, + {{ scope.opt?.code }}</QItemLabel + > + </QItemSection> + </QItem> + </template> </VnSelect> - <QSelect + <VnSelect v-model="data.contactChannelFk" :options="contactChannels" option-value="id" @@ -125,7 +135,9 @@ const title = ref(); /> </VnRow> <VnRow> - <QSelect + <VnSelect + url="Clients" + :where="{ id: { neq: $route.params.id } }" :input-debounce="0" :label="t('customer.basicData.previousClient')" :options="clients" @@ -134,7 +146,9 @@ const title = ref(); map-options option-label="name" option-value="id" + sort-by="name ASC" v-model="data.transferorFk" + :fields="['id', 'name']" > <template #append> <QIcon name="info" class="cursor-pointer"> @@ -145,7 +159,7 @@ const title = ref(); }}</QTooltip> </QIcon> </template> - </QSelect> + </VnSelect> </VnRow> </template> </FormModel> diff --git a/src/pages/Customer/Card/CustomerBillingData.vue b/src/pages/Customer/Card/CustomerBillingData.vue index 495b871df..5eeaea50b 100644 --- a/src/pages/Customer/Card/CustomerBillingData.vue +++ b/src/pages/Customer/Card/CustomerBillingData.vue @@ -31,8 +31,8 @@ const getBankEntities = (data, formData) => { </script> <template> - <fetch-data @on-fetch="(data) => (payMethods = data)" auto-load url="PayMethods" /> - <fetch-data + <FetchData @on-fetch="(data) => (payMethods = data)" auto-load url="PayMethods" /> + <FetchData ref="bankEntitiesRef" @on-fetch="(data) => (bankEntitiesOptions = data)" :filter="filter" @@ -85,9 +85,8 @@ const getBankEntities = (data, formData) => { <template #option="scope"> <QItem v-bind="scope.itemProps"> <QItemSection v-if="scope.opt"> - <QItemLabel - >{{ scope.opt.bic }} {{ scope.opt.name }}</QItemLabel - > + <QItemLabel>{{ scope.opt.bic }} </QItemLabel> + <QItemLabel caption> {{ scope.opt.name }}</QItemLabel> </QItemSection> </QItem> </template> diff --git a/src/pages/Customer/Card/CustomerCard.vue b/src/pages/Customer/Card/CustomerCard.vue index 229946ea2..139917d05 100644 --- a/src/pages/Customer/Card/CustomerCard.vue +++ b/src/pages/Customer/Card/CustomerCard.vue @@ -1,17 +1,23 @@ <script setup> +import { computed } from 'vue'; +import { useRoute } from 'vue-router'; + import VnCard from 'components/common/VnCard.vue'; import CustomerDescriptor from './CustomerDescriptor.vue'; import CustomerFilter from '../CustomerFilter.vue'; +const route = useRoute(); + +const routeName = computed(() => route.name); </script> <template> <VnCard data-key="Client" base-url="Clients" :descriptor="CustomerDescriptor" - :filter-panel="CustomerFilter" + :filter-panel="routeName != 'CustomerConsumption' && CustomerFilter" search-data-key="CustomerList" :searchbar-props="{ - url: 'Clients/extendedListFilter', + url: 'Clients/filter', label: 'Search customer', info: 'You can search by customer id or name', }" diff --git a/src/pages/Customer/Card/CustomerConsumption.vue b/src/pages/Customer/Card/CustomerConsumption.vue index 4d3da1116..35f366e47 100644 --- a/src/pages/Customer/Card/CustomerConsumption.vue +++ b/src/pages/Customer/Card/CustomerConsumption.vue @@ -1,15 +1,241 @@ <script setup> -import CustomerConsumptionFilter from './CustomerConsumptionFilter.vue'; -import { useStateStore } from 'src/stores/useStateStore'; +import { ref, computed, onBeforeMount } from 'vue'; +import axios from 'axios'; +import { useI18n } from 'vue-i18n'; +import { toDate } from 'src/filters/index'; +import { useRoute } from 'vue-router'; + +import VnTable from 'components/VnTable/VnTable.vue'; +import FetchedTags from 'components/ui/FetchedTags.vue'; +import { useArrayData } from 'src/composables/useArrayData'; +import { usePrintService } from 'src/composables/usePrintService'; +import { useVnConfirm } from 'src/composables/useVnConfirm'; + +const { openConfirmationModal } = useVnConfirm(); +const { openReport, sendEmail } = usePrintService(); +import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; +import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import VnInputDate from 'components/common/VnInputDate.vue'; + +const arrayData = useArrayData('Client'); +const { t } = useI18n(); +const route = useRoute(); +const campaignList = ref(); +const showActionBtns = computed(() => handleQueryParams()); +function handleQueryParams() { + const query = getQueryParams(); + return query.from && query.to; +} +const columns = computed(() => [ + { + name: 'search', + align: 'left', + label: t('globals.search'), + visible: false, + }, + { + name: 'itemFk', + align: 'left', + label: t('globals.item'), + columnClass: 'shrink', + cardVisible: true, + columnFilter: { + name: 'itemId', + }, + }, + { + name: 'ticketFk', + align: 'left', + label: t('globals.ticket'), + cardVisible: true, + columnFilter: { + inWhere: true, + }, + }, + { + name: 'shipped', + align: 'left', + label: t('globals.shipped'), + format: ({ shipped }) => toDate(shipped), + columnFilter: false, + cardVisible: true, + }, + { + name: 'description', + align: 'left', + label: t('globals.description'), + columnClass: 'expand', + columnFilter: { + inWhere: true, + }, + }, + { + name: 'quantity', + label: t('globals.quantity'), + cardVisible: true, + columnFilter: { + inWhere: true, + }, + }, + { + name: 'grouped', + label: t('Group by items'), + component: 'checkbox', + visible: false, + orderBy: false, + }, +]); + +onBeforeMount(async () => { + campaignList.value = (await axios('Campaigns/latest')).data; +}); + +function getQueryParams() { + return JSON.parse(route.query.consumption ?? '{}'); +} +function getParams() { + const query = getQueryParams(); + return { + from: query.from, + to: query.to, + recipient: arrayData.store.data.email, + recipientId: arrayData.store.data.id, + }; +} +const userParams = computed(() => { + const minDate = Date.vnNew(); + minDate.setHours(0, 0, 0, 0); + minDate.setMonth(minDate.getMonth() - 2); + + const maxDate = Date.vnNew(); + maxDate.setHours(23, 59, 59, 59); + + return { + campaign: campaignList.value[0]?.id, + from: minDate, + to: maxDate, + }; +}); +const openReportPdf = () => { + openReport(`Clients/${route.params.id}/campaign-metrics-pdf`, getParams()); +}; + +const openSendEmailDialog = async () => { + openConfirmationModal( + t('The consumption report will be sent'), + t('Please, confirm'), + () => sendCampaignMetricsEmail({ address: arrayData.store.data.email }) + ); +}; +const sendCampaignMetricsEmail = ({ address }) => { + sendEmail(`Clients/${route.params.id}/campaign-metrics-email`, { + recipient: address, + ...getParams(), + }); +}; </script> <template> - <Teleport to="#right-panel" v-if="useStateStore().isHeaderMounted()"> - <CustomerConsumptionFilter data-key="CustomerConsumption" /> - </Teleport> + <VnTable + v-if="campaignList" + data-key="CustomerConsumption" + url="Clients/consumption" + :order="['itemTypeFk', 'itemName', 'itemSize', 'description']" + :columns="columns" + search-url="consumption" + :filter="filter" + :user-params="userParams" + :default-remove="false" + :default-reset="false" + :default-save="false" + :has-sub-toolbar="true" + auto-load + > + <template #moreBeforeActions> + <QBtn + color="primary" + flat + icon-right="picture_as_pdf" + @click="openReportPdf()" + :disabled="!showActionBtns" + > + <QTooltip>{{ t('globals.downloadPdf') }}</QTooltip> + </QBtn> + <QBtn + color="primary" + flat + icon-right="email" + @click="openSendEmailDialog()" + :disabled="!showActionBtns" + > + <QTooltip>{{ t('Send to email') }}</QTooltip> + </QBtn> + </template> + <template #column-itemFk="{ row }"> + <span class="link"> + {{ row.itemFk }} + <ItemDescriptorProxy :id="row.itemFk" /> + </span> + </template> + <template #column-ticketFk="{ row }"> + <span class="link"> + {{ row.ticketFk }} + <TicketDescriptorProxy :id="row.ticketFk" /> + </span> + </template> + <template #column-description="{ row }"> + <div>{{ row.concept }}</div> + <div v-if="row.subName" class="subName"> + {{ row.subName }} + </div> + <FetchedTags :item="row" :max-length="3" /> + </template> + <template #moreFilterPanel="{ params }"> + <div class="column no-wrap flex-center q-gutter-y-md q-mt-xs q-pr-xl"> + <VnSelect + v-model="params.campaign" + :options="campaignList" + :label="t('globals.campaign')" + :filled="true" + class="q-px-sm q-pt-none fit" + dense + option-label="code" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt?.code }} + {{ + new Date(scope.opt?.dated).getFullYear() + }}</QItemLabel + > + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnInputDate + v-model="params.from" + :label="t('globals.from')" + :filled="true" + class="q-px-xs q-pt-none fit" + dense + /> + <VnInputDate + v-model="params.to" + :label="t('globals.to')" + :filled="true" + class="q-px-xs q-pt-none fit" + dense + /> + </div> + </template> + </VnTable> </template> <i18n> es: Enter a new search: Introduce una nueva búsqueda + Group by items: Agrupar por artículos </i18n> diff --git a/src/pages/Customer/Card/CustomerConsumptionFilter.vue b/src/pages/Customer/Card/CustomerConsumptionFilter.vue deleted file mode 100644 index 4d2c5ff3c..000000000 --- a/src/pages/Customer/Card/CustomerConsumptionFilter.vue +++ /dev/null @@ -1,91 +0,0 @@ -<script setup> -import { useI18n } from 'vue-i18n'; -import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import { QItem } from 'quasar'; -import VnSelect from 'src/components/common/VnSelect.vue'; -import { QItemSection } from 'quasar'; - -const { t } = useI18n(); -defineProps({ dataKey: { type: String, required: true } }); -</script> -<template> - <VnFilterPanel :data-key="dataKey" :search-button="true"> - <template #tags="{ tag, formatFn }"> - <div class="q-gutter-x-xs"> - <strong>{{ t(`params.${tag.label}`) }}: </strong> - <span>{{ formatFn(tag.value) }}</span> - </div> - </template> - <template #body="{ params }"> - <QItem> - <QItemSection> - <VnInput - :label="t('params.item')" - v-model="params.itemId" - is-outlined - lazy-rules - /> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - v-model="params.buyerId" - url="TicketRequests/getItemTypeWorker" - :label="t('params.buyer')" - option-value="id" - option-label="nickname" - dense - outlined - rounded - /> - </QItemSection> - </QItem> - <QItem> - <!--It's required to include the relation category !! There's 413 records in production--> - <QItemSection> - <VnSelect - v-model="params.typeId" - url="ItemTypes" - :label="t('params.type')" - option-label="name" - option-value="id" - dense - outlined - rounded - > - </VnSelect> - </QItemSection> - </QItem> - <QItem> - <QItemSection> - <VnSelect - url="ItemCategories" - :label="t('params.category')" - option-label="name" - option-value="id" - v-model="params.categoryId" - dense - outlined - rounded - /> - </QItemSection> - </QItem> - </template> - </VnFilterPanel> -</template> -<i18n> -en: - params: - item: Item id - buyer: Buyer - type: Type - category: Category -es: - params: - item: Id artículo - buyer: Comprador - type: Tipo - category: Categoría -</i18n> diff --git a/src/pages/Customer/Card/CustomerCreditContracts.vue b/src/pages/Customer/Card/CustomerCreditContracts.vue index 12719b2cf..0ff074793 100644 --- a/src/pages/Customer/Card/CustomerCreditContracts.vue +++ b/src/pages/Customer/Card/CustomerCreditContracts.vue @@ -53,6 +53,8 @@ const openDialog = (item) => { promise: updateData, }, }); + updateData(); + showQPageSticky.value = true; }; const openViewCredit = (credit) => { diff --git a/src/pages/Customer/Card/CustomerCreditOpinion.vue b/src/pages/Customer/Card/CustomerCreditOpinion.vue index 9c060b1a5..6a839ff3f 100644 --- a/src/pages/Customer/Card/CustomerCreditOpinion.vue +++ b/src/pages/Customer/Card/CustomerCreditOpinion.vue @@ -1,23 +1,17 @@ <script setup> -import { computed, ref, watch } from 'vue'; +import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import { QBtn } from 'quasar'; - import { toCurrency, toDateHourMin } from 'src/filters'; +import VnTable from 'src/components/VnTable/VnTable.vue'; -import FetchData from 'components/FetchData.vue'; -import FormModel from 'components/FormModel.vue'; -import VnRow from 'components/ui/VnRow.vue'; -import VnInput from 'src/components/common/VnInput.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; const { t } = useI18n(); const route = useRoute(); -const clientInformasRef = ref(null); -const rows = ref([]); +const tableRef = ref(); const filter = { include: [ @@ -37,10 +31,9 @@ const filter = { const columns = computed(() => [ { align: 'left', - field: 'created', - format: (value) => toDateHourMin(value), + format: ({ created }) => toDateHourMin(created), label: t('Since'), - name: 'since', + name: 'created', }, { align: 'left', @@ -53,64 +46,54 @@ const columns = computed(() => [ field: 'rating', label: t('Rating'), name: 'rating', + create: true, + columnCreate: { + component: 'number', + autofocus: true, + }, }, { align: 'right', field: 'recommendedCredit', - format: (value) => toCurrency(value), + format: ({ recommendedCredit }) => toCurrency(recommendedCredit), label: t('Recommended credit'), name: 'recommendedCredit', + create: true, + columnCreate: { + component: 'number', + autofocus: true, + }, }, ]); - -watch( - () => route.params.id, - (newValue) => { - if (!newValue) return; - filter.where.clientFk = newValue; - clientInformasRef.value?.fetch(); - } -); </script> <template> - <FetchData - :filter="filter" - @on-fetch="(data) => (rows = data)" - auto-load - ref="clientInformasRef" + <VnTable + ref="tableRef" + data-key="ClientInformas" url="ClientInformas" - /> - - <FormModel - :form-initial-data="{}" - :observe-form-changes="false" - :url-create="`Clients/${route.params.id}/setRating`" + :filter="filter" + :order="['created DESC']" + :columns="columns" + :right-search="false" + :is-editable="false" + :use-model="true" + :column-search="false" + :disable-option="{ card: true }" + auto-load + :create="{ + urlCreate: `Clients/${route.params.id}/setRating`, + title: 'Create rating', + onDataSaved: () => tableRef.reload(), + formInitialData: {}, + }" > - <template #form="{ data }"> - <VnRow> - <div class="col"> - <VnInput - :label="t('Rating')" - clearable - type="number" - v-model.number="data.rating" - /> - </div> - <div class="col"> - <VnInput - :label="t('Recommended credit')" - clearable - type="number" - v-model.number="data.recommendedCredit" - /> - </div> - </VnRow> + <template #column-employee="{ row }"> + <span class="link" @click.stop>{{ row.worker.user.nickname }}</span> + <WorkerDescriptorProxy :id="row.clientFk" /> </template> - </FormModel> - - <div class="full-width flex justify-center" v-if="rows.length"> - <QTable + </VnTable> + <!-- <QTable :columns="columns" :pagination="{ rowsPerPage: 0 }" :rows="rows" @@ -120,17 +103,12 @@ watch( class="card-width q-px-lg" > <template #body-cell-employee="{ row }"> - <QTd auto-width @click.stop> - <QBtn color="blue" flat no-caps>{{ row.worker.user.nickname }}</QBtn> + <QTd @click.stop> + <span class="link">{{ row.worker.user.nickname }}</span> <WorkerDescriptorProxy :id="row.clientFk" /> </QTd> </template> - </QTable> - </div> - - <h5 class="flex justify-center color-vn-label" v-else> - {{ t('globals.noResults') }} - </h5> + </QTable> --> </template> <i18n> diff --git a/src/pages/Customer/Card/CustomerDescriptor.vue b/src/pages/Customer/Card/CustomerDescriptor.vue index 0e76bcfed..b18f90d20 100644 --- a/src/pages/Customer/Card/CustomerDescriptor.vue +++ b/src/pages/Customer/Card/CustomerDescriptor.vue @@ -3,7 +3,7 @@ import { ref, computed } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; -import { toCurrency, toDate } from 'src/filters'; +import { dashIfEmpty, toCurrency, toDate } from 'src/filters'; import useCardDescription from 'src/composables/useCardDescription'; @@ -11,6 +11,10 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue'; import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue'; +import { useState } from 'src/composables/useState'; +const state = useState(); + +const customer = computed(() => state.get('customer')); const $props = defineProps({ id: { @@ -43,7 +47,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit :subtitle="data.subtitle" @on-fetch="setData" :summary="$props.summary" - data-key="customerData" + data-key="customer" > <template #menu="{ entity }"> <CustomerDescriptorMenu :customer="entity" /> @@ -57,35 +61,46 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit :value="toCurrency(entity.creditInsurance)" /> - <VnLv :label="t('customer.card.debt')" :value="toCurrency(entity.debt)" /> - <VnLv v-if="entity.salesPersonUser" :label="t('customer.card.salesPerson')"> + <VnLv + :label="t('customer.card.debt')" + :value="toCurrency(entity.debt)" + :info="t('customer.summary.riskInfo')" + /> + <VnLv :label="t('customer.card.salesPerson')"> <template #value> <VnUserLink - :name="entity.salesPersonUser?.name" + v-if="entity.salesPersonUser" + :name="entity.salesPersonUser.name" :worker-id="entity.salesPersonFk" /> + <span v-else>{{ dashIfEmpty(entity.salesPersonUser) }}</span> </template> </VnLv> <VnLv :label="t('customer.card.businessTypeFk')" - :value="entity.businessTypeFk" + :value="entity.businessType.description" /> </template> - <template #icons="{ entity }"> - <QCardActions class="q-gutter-x-md"> + <template #icons> + <QCardActions v-if="customer" class="q-gutter-x-md"> <QIcon - v-if="!entity.isActive" + v-if="!customer.isActive" name="vn:disabled" size="xs" color="primary" > <QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip> </QIcon> - <QIcon v-if="entity.isFreezed" name="vn:frozen" size="xs" color="primary"> + <QIcon + v-if="customer.isFreezed" + name="vn:frozen" + size="xs" + color="primary" + > <QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip> </QIcon> <QIcon - v-if="!entity.account.active" + v-if="!customer.account?.active" color="primary" name="vn:noweb" size="xs" @@ -93,7 +108,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit <QTooltip>{{ t('customer.card.webAccountInactive') }}</QTooltip> </QIcon> <QIcon - v-if="entity.debt > entity.credit" + v-if="customer.debt > customer.credit" name="vn:risk" size="xs" color="primary" @@ -101,7 +116,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit <QTooltip>{{ t('customer.card.hasDebt') }}</QTooltip> </QIcon> <QIcon - v-if="!entity.isTaxDataChecked" + v-if="!customer.isTaxDataChecked" name="vn:no036" size="xs" color="primary" @@ -109,7 +124,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit <QTooltip>{{ t('customer.card.notChecked') }}</QTooltip> </QIcon> <QBtn - v-if="entity.unpaid" + v-if="customer.unpaid" flat size="sm" icon="vn:Client_unpaid" @@ -121,13 +136,13 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit <br /> {{ t('unpaidDated', { - dated: toDate(entity.unpaid.dated), + dated: toDate(customer.unpaid.dated), }) }} <br /> {{ t('unpaidAmount', { - amount: toCurrency(entity.unpaid.amount), + amount: toCurrency(customer.unpaid.amount), }) }} </QTooltip> @@ -139,7 +154,13 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit <QBtn :to="{ name: 'TicketList', - query: { table: JSON.stringify({ clientFk: entity.id }) }, + query: { + from: undefined, + to: undefined, + table: JSON.stringify({ + clientFk: entity.id, + }), + }, }" size="md" icon="vn:ticket" @@ -160,23 +181,8 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit </QBtn> <QBtn :to="{ - name: 'OrderCreate', - query: { clientId: entity.id }, - }" - size="md" - icon="vn:basketadd" - color="primary" - > - <QTooltip>{{ t('New order') }}</QTooltip> - </QBtn> - <QBtn - :to="{ - name: 'AccountList', - query: { - table: JSON.stringify({ - filter: { where: { id: entity.id } }, - }), - }, + name: 'AccountSummary', + params: { id: entity.id }, }" size="md" icon="face" @@ -197,7 +203,6 @@ es: Go to module index: Ir al índice del módulo Customer ticket list: Listado de tickets del cliente Customer invoice out list: Listado de facturas del cliente - New order: Nuevo pedido Go to user: Ir al usuario Customer unpaid: Cliente impago Unpaid: Impagado diff --git a/src/pages/Customer/Card/CustomerDescriptorMenu.vue b/src/pages/Customer/Card/CustomerDescriptorMenu.vue index 560ee51c8..89b10a4fe 100644 --- a/src/pages/Customer/Card/CustomerDescriptorMenu.vue +++ b/src/pages/Customer/Card/CustomerDescriptorMenu.vue @@ -8,6 +8,9 @@ import { useQuasar } from 'quasar'; import useNotify from 'src/composables/useNotify'; import VnSmsDialog from 'src/components/common/VnSmsDialog.vue'; +import TicketCreateDialog from 'src/pages/Ticket/TicketCreateDialog.vue'; +import OrderCreateDialog from 'src/pages/Order/Card/OrderCreateDialog.vue'; +import { ref } from 'vue'; const $props = defineProps({ customer: { @@ -40,20 +43,32 @@ const sendSms = async (payload) => { notify(error.message, 'positive'); } }; + +const ticketCreateFormDialog = ref(null); +const openTicketCreateForm = () => { + ticketCreateFormDialog.value.show(); +}; +const orderCreateFormDialog = ref(null); +const openOrderCreateForm = () => { + orderCreateFormDialog.value.show(); +}; </script> <template> - <QItem v-ripple clickable> + <QItem v-ripple clickable @click="openTicketCreateForm()"> <QItemSection> - <RouterLink - :to="{ - name: 'TicketCreate', - query: { clientFk: customer.id }, - }" - class="color-vn-text" - > - {{ t('Simple ticket') }} - </RouterLink> + {{ t('globals.pageTitles.createTicket') }} + <QDialog ref="ticketCreateFormDialog"> + <TicketCreateDialog /> + </QDialog> + </QItemSection> + </QItem> + <QItem v-ripple clickable @click="openOrderCreateForm()"> + <QItemSection> + {{ t('globals.pageTitles.createOrder') }} + <QDialog ref="orderCreateFormDialog"> + <OrderCreateDialog :client-fk="customer.id" /> + </QDialog> </QItemSection> </QItem> <QItem v-ripple clickable> diff --git a/src/pages/Customer/Card/CustomerGreuges.vue b/src/pages/Customer/Card/CustomerGreuges.vue index 12173727f..1c78392e7 100644 --- a/src/pages/Customer/Card/CustomerGreuges.vue +++ b/src/pages/Customer/Card/CustomerGreuges.vue @@ -47,7 +47,6 @@ const columns = computed(() => [ }, { align: 'left', - name: 'userFk', label: t('Created by'), component: 'userLink', attrs: ({ row }) => { @@ -73,6 +72,7 @@ const columns = computed(() => [ columnCreate: { component: 'select', url: 'greugeTypes', + sortBy: 'name ASC ', limit: 0, }, }, @@ -105,9 +105,10 @@ const setRows = (data) => { :use-model="true" :column-search="false" @on-fetch="(data) => setRows(data)" + :disable-option="{ card: true }" :create="{ urlCreate: `Greuges`, - title: t('New credit'), + title: t('New greuge'), onDataSaved: () => tableRef.reload(), formInitialData: { shipped: new Date(), clientFk: route.params.id }, }" diff --git a/src/pages/Customer/Card/CustomerRecoveries.vue b/src/pages/Customer/Card/CustomerRecoveries.vue index 8d3d05702..3ea8998e9 100644 --- a/src/pages/Customer/Card/CustomerRecoveries.vue +++ b/src/pages/Customer/Card/CustomerRecoveries.vue @@ -89,6 +89,7 @@ function setFinished(id) { :columns="columns" :use-model="true" :right-search="false" + :disable-option="{ card: true }" :create="{ urlCreate: 'Recoveries', title: 'New recovery', diff --git a/src/pages/Customer/Card/CustomerSamples.vue b/src/pages/Customer/Card/CustomerSamples.vue index de998d8d3..49697aab7 100644 --- a/src/pages/Customer/Card/CustomerSamples.vue +++ b/src/pages/Customer/Card/CustomerSamples.vue @@ -3,18 +3,18 @@ import { ref, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; -import { QBtn } from 'quasar'; +import { QBtn, useQuasar } from 'quasar'; -import FetchData from 'components/FetchData.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import { toDateTimeFormat } from 'src/filters/date'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import { dashIfEmpty } from 'src/filters'; +import CustomerSamplesCreate from '../components/CustomerSamplesCreate.vue'; const { t } = useI18n(); const route = useRoute(); const router = useRouter(); -const rows = ref([]); - const filter = { include: [ { relation: 'type', scope: { fields: ['code', 'description'] } }, @@ -26,105 +26,67 @@ const filter = { limit: 20, }; -const tableColumnComponents = { - sent: { - component: 'span', - props: () => {}, - event: () => {}, - }, - description: { - component: 'span', - props: () => {}, - event: () => {}, - }, - worker: { - component: QBtn, - props: () => ({ flat: true, color: 'blue', noCaps: true }), - event: () => {}, - }, - company: { - component: 'span', - props: () => {}, - event: () => {}, - }, -}; - const columns = computed(() => [ { align: 'left', - field: 'created', + name: 'created', label: t('Sent'), - name: 'sent', - format: (value) => toDateTimeFormat(value), + format: ({ created }) => toDateTimeFormat(created), }, { align: 'left', - field: (value) => value.type.description, + format: (row) => row.type.description, label: t('Description'), name: 'description', }, { align: 'left', - field: (value) => value.user.name, label: t('Worker'), name: 'worker', }, { align: 'left', - field: (value) => value.company?.code, + format: ({ company }) => company?.code ?? dashIfEmpty(company), label: t('Company'), name: 'company', }, ]); +const quasar = useQuasar(); const toCustomerSamplesCreate = () => { - router.push({ name: 'CustomerSamplesCreate' }); + quasar + .dialog({ + component: CustomerSamplesCreate, + }) + .onOk(() => tableRef.value.reload()); }; +const tableRef = ref(); </script> <template> - <FetchData - :filter="filter" - @on-fetch="(data) => (rows = data)" + <VnTable + ref="tableRef" + data-key="ClientSamples" auto-load + :filter="filter" url="ClientSamples" - /> - - <div class="full-width flex justify-center"> - <QPage class="card-width q-pa-lg"> - <QTable - :columns="columns" - :pagination="{ rowsPerPage: 12 }" - :rows="rows" - class="full-width q-mt-md" - row-key="id" - :no-data-label="t('globals.noResults')" - > - <template #body-cell="props"> - <QTd :props="props"> - <QTr :props="props" class="cursor-pointer"> - <component - :is="tableColumnComponents[props.col.name].component" - class="col-content" - v-bind=" - tableColumnComponents[props.col.name].props(props) - " - @click=" - tableColumnComponents[props.col.name].event(props) - " - > - {{ props.value }} - <WorkerDescriptorProxy - :id="props.row.userFk" - v-if="props.col.name === 'worker'" - /> - </component> - </QTr> - </QTd> - </template> - </QTable> - </QPage> - </div> + :columns="columns" + :pagination="{ rowsPerPage: 12 }" + :disable-option="{ card: true }" + :rows="rows" + class="full-width q-mt-md" + row-key="id" + :create="false" + :no-data-label="t('globals.noResults')" + > + <template #column-worker="{ row }"> + <div v-if="row.user"> + <span class="link">{{ row.user?.name }}</span + ><WorkerDescriptorProxy :id="row.userFk" /> + </div> + <span v-else>{{ dashIfEmpty(row.user) }}</span> + </template> + </VnTable> <QPageSticky :offset="[18, 18]"> <QBtn @click.stop="toCustomerSamplesCreate()" color="primary" fab icon="add" /> diff --git a/src/pages/Customer/Card/CustomerSummary.vue b/src/pages/Customer/Card/CustomerSummary.vue index da50ba239..f049426e2 100644 --- a/src/pages/Customer/Card/CustomerSummary.vue +++ b/src/pages/Customer/Card/CustomerSummary.vue @@ -1,10 +1,11 @@ <script setup> -import { computed, ref, onMounted } from 'vue'; +import { computed, ref } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; +import VnUserLink from 'src/components/ui/VnUserLink.vue'; + import { toCurrency, toPercentage, toDate } from 'src/filters'; import CardSummary from 'components/ui/CardSummary.vue'; -import { getUrl } from 'src/composables/getUrl'; import VnLv from 'src/components/ui/VnLv.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import CustomerSummaryTable from 'src/pages/Customer/components/CustomerSummaryTable.vue'; @@ -23,11 +24,6 @@ const $props = defineProps({ const entityId = computed(() => $props.id || route.params.id); const customer = computed(() => summary.value.entity); const summary = ref(); -const clientUrl = ref(); - -onMounted(async () => { - clientUrl.value = (await getUrl('client/')) + entityId.value + '/'; -}); const balanceDue = computed(() => { return ( @@ -40,11 +36,11 @@ const balanceDue = computed(() => { const balanceDueWarning = computed(() => (balanceDue.value ? 'negative' : '')); const claimRate = computed(() => { - return customer.value.claimsRatio.claimingRate; + return customer.value.claimsRatio?.claimingRate ?? 0; }); const priceIncreasingRate = computed(() => { - return customer.value.claimsRatio.priceIncreasing / 100; + return customer.value.claimsRatio?.priceIncreasing ?? 0 / 100; }); const debtWarning = computed(() => { @@ -58,6 +54,11 @@ const creditWarning = computed(() => { return tooMuchInsurance || noCreditInsurance ? 'negative' : ''; }); +const sumRisk = ({ clientRisks }) => { + let total = clientRisks.reduce((acc, { amount }) => acc + amount, 0); + + return total; +}; </script> <template> @@ -91,7 +92,13 @@ const creditWarning = computed(() => { <VnLv :label="t('customer.summary.salesPerson')" :value="entity?.salesPersonUser?.name" - /> + > + <template #value> + <VnUserLink + :name="entity.salesPersonUser?.name" + :worker-id="entity.salesPersonFk" + /> </template + ></VnLv> <VnLv :label="t('customer.summary.contactChannel')" :value="entity?.contactChannel?.name" @@ -131,7 +138,7 @@ const creditWarning = computed(() => { :url="`#/customer/${entityId}/fiscal-data`" :text="t('customer.summary.fiscalData')" /> - <VnRow> + <VnRow class="block"> <VnLv :label="t('customer.summary.isEqualizated')" :value="entity.isEqualizated" @@ -140,8 +147,6 @@ const creditWarning = computed(() => { :label="t('customer.summary.isActive')" :value="entity.isActive" /> - </VnRow> - <VnRow> <VnLv :label="t('customer.summary.verifiedData')" :value="entity.isTaxDataChecked" @@ -150,8 +155,6 @@ const creditWarning = computed(() => { :label="t('customer.summary.hasToInvoice')" :value="entity.hasToInvoice" /> - </VnRow> - <VnRow> <VnLv :label="t('customer.summary.notifyByEmail')" :value="entity.isToBeMailed" @@ -162,7 +165,7 @@ const creditWarning = computed(() => { <QCard class="vn-one"> <VnTitle :url="`#/customer/${entityId}/billing-data`" - :text="t('customer.summary.billingData')" + :text="t('customer.summary.payMethodFk')" /> <VnLv :label="t('customer.summary.payMethod')" @@ -170,7 +173,7 @@ const creditWarning = computed(() => { /> <VnLv :label="t('customer.summary.bankAccount')" :value="entity.iban" /> <VnLv :label="t('customer.summary.dueDay')" :value="entity.dueDay" /> - <VnRow class="q-mt-sm" wrap> + <VnRow class="q-mt-sm block"> <VnLv :label="t('customer.summary.hasLcr')" :value="entity.hasLcr" /> <VnLv :label="t('customer.summary.hasCoreVnl')" @@ -185,7 +188,7 @@ const creditWarning = computed(() => { </QCard> <QCard class="vn-one" v-if="entity.defaultAddress"> <VnTitle - :url="`#/customer/${entityId}/consignees`" + :url="`#/customer/${entityId}/address`" :text="t('customer.summary.consignee')" /> <VnLv @@ -218,7 +221,7 @@ const creditWarning = computed(() => { </QCard> <QCard class="vn-one" v-if="entity.account"> <VnTitle - :url="`https://grafana.verdnatura.es/d/adjlxzv5yjt34d/analisis-de-clientes-7c-crm?orgId=1&var-clientFk=${entityId}`" + :url="`${grafanaUrl}/d/adjlxzv5yjt34d/analisis-de-clientes-7c-crm?orgId=1&var-clientFk=${entityId}`" :text="t('customer.summary.businessData')" icon="vn:grafana" /> @@ -231,7 +234,6 @@ const creditWarning = computed(() => { :value="toCurrency(entity?.mana?.mana)" /> <VnLv - v-if="entity.claimsRatio" :label="t('customer.summary.priceIncreasingRate')" :value="toPercentage(priceIncreasingRate)" /> @@ -240,15 +242,14 @@ const creditWarning = computed(() => { :value="toCurrency(entity?.averageInvoiced?.invoiced)" /> <VnLv - v-if="entity.claimsRatio" :label="t('customer.summary.claimRate')" :value="toPercentage(claimRate)" /> </QCard> <QCard class="vn-one" v-if="entity.account"> <VnTitle - :url="`https://grafana.verdnatura.es/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk=${entityId}`" - :text="t('customer.summary.financialData')" + :url="`${grafanaUrl}/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk=${entityId}`" + :text="t('customer.summary.payMethodFk')" icon="vn:grafana" /> <VnLv @@ -266,15 +267,13 @@ const creditWarning = computed(() => { /> <VnLv - v-if="entity.creditInsurance" :label="t('customer.summary.securedCredit')" :value="toCurrency(entity.creditInsurance)" :info="t('customer.summary.securedCreditInfo')" /> - <VnLv :label="t('customer.summary.balance')" - :value="toCurrency(entity.sumRisk) || toCurrency(0)" + :value="toCurrency(sumRisk(entity)) || toCurrency(0)" :info="t('customer.summary.balanceInfo')" /> @@ -301,7 +300,7 @@ const creditWarning = computed(() => { :value="entity.recommendedCredit" /> </QCard> - <QCard class="vn-one"> + <QCard> <VnTitle :text="t('Latest tickets')" /> <CustomerSummaryTable /> </QCard> diff --git a/src/pages/Customer/Card/CustomerUnpaid.vue b/src/pages/Customer/Card/CustomerUnpaid.vue index 5b9a6cde2..ad00cbf59 100644 --- a/src/pages/Customer/Card/CustomerUnpaid.vue +++ b/src/pages/Customer/Card/CustomerUnpaid.vue @@ -151,7 +151,10 @@ watch( clearable type="number" v-model="amount" - /> + autofocus + > + <template #append>€</template></VnInput + > </div> </VnRow> </QForm> diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index a38f8d1a8..f6758bf4e 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -2,6 +2,7 @@ import { ref, computed, markRaw } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; +import VnSelect from 'src/components/common/VnSelect.vue'; import VnTable from 'components/VnTable/VnTable.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue'; @@ -69,7 +70,7 @@ const columns = computed(() => [ optionFilter: 'firstName', useLike: false, }, - create: true, + create: false, columnField: { component: null, }, @@ -195,6 +196,8 @@ const columns = computed(() => [ component: 'select', attrs: { url: 'BusinessTypes', + fields: ['code', 'description'], + sortBy: 'description ASC ', optionLabel: 'description', optionValue: 'code', }, @@ -353,12 +356,13 @@ const columns = computed(() => [ { title: t('Client ticket list'), icon: 'vn:ticket', - action: redirectToCreateView, + action: redirectToTicketsList, isPrimary: true, }, { title: t('components.smartCard.viewSummary'), icon: 'preview', + isPrimary: true, action: (row) => viewSummary(row.id, CustomerSummary), }, ], @@ -366,11 +370,12 @@ const columns = computed(() => [ ]); const { viewSummary } = useSummaryDialog(); -const redirectToCreateView = (row) => { +const redirectToTicketsList = (row) => { router.push({ name: 'TicketList', + query: { - params: JSON.stringify({ + table: JSON.stringify({ clientFk: row.id, }), }, @@ -395,10 +400,10 @@ function handleLocation(data, location) { <VnTable ref="tableRef" data-key="Customer" - url="Clients/extendedListFilter" + url="Clients/filter" :create="{ urlCreate: 'Clients/createWithUser', - title: 'Create client', + title: t('globals.pageTitles.customerCreate'), onDataSaved: ({ id }) => tableRef.redirect(id), formInitialData: { active: true, @@ -411,6 +416,39 @@ function handleLocation(data, location) { auto-load > <template #more-create-dialog="{ data }"> + <VnSelect + url="Workers/search" + v-model="data.salesPersonFk" + :label="t('customer.basicData.salesPerson')" + :params="{ + departmentCodes: ['VT', 'shopping'], + }" + :fields="['id', 'nickname']" + sort-by="nickname ASC" + :use-like="false" + emit-value + auto-load + > + <template #prepend> + <VnAvatar + :worker-id="data.salesPersonFk" + color="primary" + :title="title" + /> + </template> + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel>{{ scope.opt?.name }}</QItemLabel> + <QItemLabel caption + >{{ scope.opt?.nickname }}, + {{ scope.opt?.code }}</QItemLabel + > + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnLocation :acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]" v-model="data.location" diff --git a/src/pages/Customer/components/CustomerAddressCreate.vue b/src/pages/Customer/components/CustomerAddressCreate.vue index 88204cbaa..7826c3579 100644 --- a/src/pages/Customer/components/CustomerAddressCreate.vue +++ b/src/pages/Customer/components/CustomerAddressCreate.vue @@ -57,12 +57,12 @@ function handleLocation(data, location) { </script> <template> - <fetch-data + <FetchData @on-fetch="(data) => (agencyModes = data)" auto-load url="AgencyModes/isActive" /> - <fetch-data @on-fetch="(data) => (incoterms = data)" auto-load url="Incoterms" /> + <FetchData @on-fetch="(data) => (incoterms = data)" auto-load url="Incoterms" /> <FormModel :form-initial-data="formInitialData" diff --git a/src/pages/Customer/components/CustomerAddressEdit.vue b/src/pages/Customer/components/CustomerAddressEdit.vue index 0be7e475a..0aa46f85d 100644 --- a/src/pages/Customer/components/CustomerAddressEdit.vue +++ b/src/pages/Customer/components/CustomerAddressEdit.vue @@ -113,18 +113,18 @@ function handleLocation(data, location) { </script> <template> - <fetch-data + <FetchData @on-fetch="(data) => (agencyModes = data)" auto-load url="AgencyModes/isActive" /> - <fetch-data @on-fetch="(data) => (incoterms = data)" auto-load url="Incoterms" /> - <fetch-data + <FetchData @on-fetch="(data) => (incoterms = data)" auto-load url="Incoterms" /> + <FetchData @on-fetch="(data) => (customsAgents = data)" auto-load url="CustomsAgents" /> - <fetch-data @on-fetch="getData" auto-load url="ObservationTypes" /> + <FetchData @on-fetch="getData" auto-load url="ObservationTypes" /> <FormModel :observe-form-changes="false" diff --git a/src/pages/Customer/components/CustomerCreditContractsCreate.vue b/src/pages/Customer/components/CustomerCreditContractsCreate.vue index c4434e870..c25e59e1a 100644 --- a/src/pages/Customer/components/CustomerCreditContractsCreate.vue +++ b/src/pages/Customer/components/CustomerCreditContractsCreate.vue @@ -1,5 +1,5 @@ <script setup> -import { reactive } from 'vue'; +import { reactive, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; @@ -10,10 +10,12 @@ import VnInputDate from 'src/components/common/VnInputDate.vue'; const { t } = useI18n(); const route = useRoute(); +const routeId = computed(() => route.params.id); const router = useRouter(); const initialData = reactive({ - clientFK: Number(route.params.id), + started: Date.vnNew(), + clientFk: routeId.value, }); const toCustomerCreditContracts = () => { diff --git a/src/pages/Customer/components/CustomerCreditContractsInsurance.vue b/src/pages/Customer/components/CustomerCreditContractsInsurance.vue index ce880d4b5..70f7cf046 100644 --- a/src/pages/Customer/components/CustomerCreditContractsInsurance.vue +++ b/src/pages/Customer/components/CustomerCreditContractsInsurance.vue @@ -1,47 +1,26 @@ <script setup> -import { computed, ref } from 'vue'; +import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; -import { toCurrency, toDateHourMinSec } from 'src/filters'; +import { toCurrency, toDate } from 'src/filters'; -import FetchData from 'components/FetchData.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; const { t } = useI18n(); const route = useRoute(); -const rows = ref([]); - const filter = { where: { creditClassificationFk: `${route.params.creditId}`, }, limit: 20, }; - -const tableColumnComponents = { - created: { - component: 'span', - props: () => {}, - event: () => {}, - }, - grade: { - component: 'span', - props: () => {}, - event: () => {}, - }, - credit: { - component: 'span', - props: () => {}, - event: () => {}, - }, -}; - const columns = computed(() => [ { align: 'left', field: 'created', - format: (value) => toDateHourMinSec(value), + format: ({ created }) => toDate(created), label: t('Created'), name: 'created', }, @@ -53,8 +32,7 @@ const columns = computed(() => [ }, { align: 'left', - field: 'credit', - format: (value) => toCurrency(value), + format: ({ credit }) => toCurrency(credit), label: t('Credit'), name: 'credit', }, @@ -62,41 +40,19 @@ const columns = computed(() => [ </script> <template> - <FetchData - :filter="filter" - @on-fetch="(data) => (rows = data)" - auto-load + <VnTable url="CreditInsurances" - /> - - <QPage class="column items-center q-pa-md" v-if="rows.length"> - <QTable - :columns="columns" - :pagination="{ rowsPerPage: 12 }" - :rows="rows" - class="full-width q-mt-md" - row-key="id" - > - <template #body-cell="props"> - <QTd :props="props"> - <QTr :props="props" class="cursor-pointer"> - <component - :is="tableColumnComponents[props.col.name].component" - class="col-content" - v-bind="tableColumnComponents[props.col.name].props(props)" - @click="tableColumnComponents[props.col.name].event(props)" - > - {{ props.value }} - </component> - </QTr> - </QTd> - </template> - </QTable> - </QPage> - - <h5 class="flex justify-center color-vn-label" v-else> - {{ t('globals.noResults') }} - </h5> + ref="tableRef" + data-key="creditInsurances" + :filter="filter" + :columns="columns" + :right-search="false" + :is-editable="false" + :use-model="true" + :column-search="false" + :disable-option="{ card: true }" + auto-load + ></VnTable> </template> <i18n> diff --git a/src/pages/Customer/components/CustomerFileManagementCreate.vue b/src/pages/Customer/components/CustomerFileManagementCreate.vue index 7c15e0b71..f33a47bcc 100644 --- a/src/pages/Customer/components/CustomerFileManagementCreate.vue +++ b/src/pages/Customer/components/CustomerFileManagementCreate.vue @@ -83,35 +83,35 @@ const toCustomerFileManagement = () => { </script> <template> - <fetch-data + <FetchData @on-fetch="(data) => (client = data)" auto-load :url="`Clients/${route.params.id}/getCard`" /> - <fetch-data + <FetchData :filter="filterFindOne" @on-fetch="(data) => (findOne = data)" auto-load url="DmsTypes/findOne" /> - <fetch-data + <FetchData @on-fetch="(data) => (allowedContentTypes = data)" auto-load url="DmsContainers/allowedContentTypes" /> - <fetch-data + <FetchData :filter="filterCompanies" @on-fetch="(data) => (optionsCompanies = data)" auto-load url="Companies" /> - <fetch-data + <FetchData :filter="filterWarehouses" @on-fetch="(data) => (optionsWarehouses = data)" auto-load url="Warehouses" /> - <fetch-data + <FetchData :filter="filterWarehouses" @on-fetch="(data) => (optionsDmsTypes = data)" auto-load diff --git a/src/pages/Customer/components/CustomerFileManagementEdit.vue b/src/pages/Customer/components/CustomerFileManagementEdit.vue index 80eaa44f9..107f41330 100644 --- a/src/pages/Customer/components/CustomerFileManagementEdit.vue +++ b/src/pages/Customer/components/CustomerFileManagementEdit.vue @@ -69,25 +69,25 @@ const toCustomerFileManagement = () => { </script> <template> - <fetch-data :url="`Dms/${route.params.dmsId}`" @on-fetch="setCurrentDms" auto-load /> - <fetch-data + <FetchData :url="`Dms/${route.params.dmsId}`" @on-fetch="setCurrentDms" auto-load /> + <FetchData @on-fetch="(data) => (allowedContentTypes = data)" auto-load url="DmsContainers/allowedContentTypes" /> - <fetch-data + <FetchData :filter="filterCompanies" @on-fetch="(data) => (optionsCompanies = data)" auto-load url="Companies" /> - <fetch-data + <FetchData :filter="filterWarehouses" @on-fetch="(data) => (optionsWarehouses = data)" auto-load url="Warehouses" /> - <fetch-data + <FetchData :filter="filterWarehouses" @on-fetch="(data) => (optionsDmsTypes = data)" auto-load diff --git a/src/pages/Customer/components/CustomerSamplesCreate.vue b/src/pages/Customer/components/CustomerSamplesCreate.vue index 79f4fe449..3c9eb856b 100644 --- a/src/pages/Customer/components/CustomerSamplesCreate.vue +++ b/src/pages/Customer/components/CustomerSamplesCreate.vue @@ -1,11 +1,11 @@ <script setup> -import { onBeforeMount, reactive, ref } from 'vue'; +import { computed, onBeforeMount, reactive, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; import axios from 'axios'; import { usePrintService } from 'composables/usePrintService'; -import { useQuasar } from 'quasar'; +import { useDialogPluginComponent, useQuasar } from 'quasar'; import { useState } from 'src/composables/useState'; import { useValidator } from 'src/composables/useValidator'; @@ -17,7 +17,9 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import CustomerSamplesPreview from 'src/pages/Customer/components/CustomerSamplesPreview.vue'; -import { useStateStore } from 'stores/useStateStore'; +import FormPopup from 'src/components/FormPopup.vue'; + +const { dialogRef, onDialogOK } = useDialogPluginComponent(); const { notify } = useNotify(); const { t } = useI18n(); @@ -27,17 +29,17 @@ const route = useRoute(); const router = useRouter(); const state = useState(); const user = state.getUser(); -const stateStore = useStateStore(); const { sendEmail } = usePrintService(); -const client = ref({}); const hasChanged = ref(false); const isLoading = ref(false); -const optionsClientsAddressess = ref([]); -const optionsCompanies = ref([]); +const addressess = ref([]); +const companies = ref([]); const optionsEmailUsers = ref([]); const optionsSamplesVisible = ref([]); const sampleType = ref({ hasPreview: false }); - +const initialData = reactive({}); +const entityId = computed(() => route.params.id); +const customer = computed(() => state.get('customer')); const filterEmailUsers = { where: { userFk: user.value.id } }; const filterClientsAddresses = { include: [ @@ -59,14 +61,13 @@ const filterSamplesVisible = { ], order: ['description'], }; -const initialData = reactive({}); + +defineEmits(['confirm', ...useDialogPluginComponent.emits]); onBeforeMount(async () => { - const { data } = await axios.get(`Clients/1/getCard`); - client.value = data; - initialData.clientFk = route.params?.id; - initialData.recipient = data.email; - initialData.recipientId = data.id; + initialData.clientFk = customer.value.id; + initialData.recipient = customer.value.email; + initialData.recipientId = customer.value.id; }); const setEmailUser = (data) => { @@ -76,7 +77,7 @@ const setEmailUser = (data) => { const setClientsAddresses = (data) => { initialData.addressId = data[0].id; - optionsClientsAddressess.value = data; + addressess.value = data; }; const setSampleType = (sampleId) => { @@ -89,20 +90,6 @@ const setSampleType = (sampleId) => { initialData.companyId = companyFk; }; -const setInitialData = () => { - hasChanged.value = false; - - initialData.addressId = optionsClientsAddressess.value[0].id; - initialData.companyFk = null; - initialData.from = null; - initialData.recipient = client.value.email; - initialData.recipientId = client.value.id; - initialData.replyTo = optionsEmailUsers.value[0]?.email; - initialData.typeFk = ''; - - sampleType.value = {}; -}; - const validateMessage = () => { if (!initialData.recipient) return 'Email cannot be blank'; if (!sampleType.value) return 'Choose a sample'; @@ -121,14 +108,14 @@ const setParams = (params) => { const getPreview = async () => { try { const params = { - recipientId: route.params.id, + recipientId: entityId, }; const validationMessage = validateMessage(); if (validationMessage) return notify(t(validationMessage), 'negative'); setParams(params); - const path = `${sampleType.value.model}/${route.params.id}/${sampleType.value.code}-html`; + const path = `${sampleType.value.model}/${entityId.value}/${sampleType.value.code}-html`; const { data } = await axios.get(path, { params }); if (!data) return; @@ -169,7 +156,6 @@ const getSamples = async () => { } }; -getSamples(); const onDataSaved = async () => { try { const params = { @@ -179,8 +165,9 @@ const onDataSaved = async () => { }; setParams(params); const samplesData = await getSamples(); - const path = `${samplesData.model}/${route.params.id}/${samplesData.code}-email`; + const path = `${samplesData.model}/${entityId.value}/${samplesData.code}-email`; await sendEmail(path, params); + onDialogOK(params); } catch (error) { notify('errors.create', 'negative'); } @@ -197,73 +184,54 @@ const toCustomerSamples = () => { </script> <template> - <fetch-data + <FetchData :filter="filterEmailUsers" @on-fetch="setEmailUser" auto-load url="EmailUsers" /> - <fetch-data + <FetchData :filter="filterClientsAddresses" - :url="`Clients/${route.params.id}/addresses`" + :url="`Clients/${entityId}/addresses`" @on-fetch="setClientsAddresses" auto-load /> - <fetch-data + <FetchData :filter="filterCompanies" - @on-fetch="(data) => (optionsCompanies = data)" + @on-fetch="(data) => (companies = data)" auto-load url="Companies" /> - <fetch-data + <FetchData :filter="filterSamplesVisible" @on-fetch="(data) => (optionsSamplesVisible = data)" auto-load url="Samples/visible" /> - <Teleport v-if="stateStore?.isSubToolbarShown()" to="#st-actions"> - <QBtnGroup push class="q-gutter-x-sm"> - <QBtn - :label="t('globals.cancel')" - @click="toCustomerSamples" - color="primary" - flat - icon="close" - /> - <QBtn - :disabled="isLoading || !sampleType?.hasPreview" - :label="t('Preview')" - :loading="isLoading" - @click.stop="getPreview()" - color="primary" - flat - icon="preview" - /> - <QBtn - :disabled="!hasChanged" - :label="t('globals.reset')" - :loading="isLoading" - @click="setInitialData" - color="primary" - flat - icon="restart_alt" - type="reset" - /> - <QBtn - :disabled="!hasChanged" - :label="t('globals.save')" - :loading="isLoading" - @click="onSubmit" - color="primary" - icon="save" - /> - </QBtnGroup> - </Teleport> - - <div class="full-width flex justify-center"> - <QCard class="card-width q-pa-lg"> - <QForm> + <QDialog ref="dialogRef"> + <FormPopup + :default-cancel-button="false" + :default-submit-button="false" + @on-submit="onSubmit()" + > + <template #custom-buttons> + <QBtn + :disabled="isLoading || !sampleType?.hasPreview" + :label="t('Preview')" + :loading="isLoading" + @click.stop="getPreview()" + color="primary" + flat + icon="preview" /><QBtn + :disabled="!hasChanged" + :label="t('globals.save')" + :loading="isLoading" + @click="onSubmit" + color="primary" + icon="save" + /></template> + <template #form-inputs> <div class="col"> <VnSelect :label="t('Sample')" @@ -320,7 +288,7 @@ const toCustomerSamples = () => { <div class="col"> <VnSelect :label="t('Company')" - :options="optionsCompanies" + :options="companies" :rules="validate('entry.companyFk')" hide-selected option-label="code" @@ -333,7 +301,7 @@ const toCustomerSamples = () => { <div class="col"> <VnSelect :label="t('Address')" - :options="optionsClientsAddressess" + :options="addressess" hide-selected option-label="nickname" option-value="id" @@ -371,15 +339,15 @@ const toCustomerSamples = () => { required="true" v-model="initialData.from" /> - </div> - </VnRow> - </QForm> - </QCard> - </div> + </div> </VnRow + ></template> + </FormPopup> + </QDialog> </template> <i18n> es: + New sample: Crear plantilla Sample: Plantilla Recipient: Destinatario Reply to: Responder a diff --git a/src/pages/Customer/components/CustomerSummaryTable.vue b/src/pages/Customer/components/CustomerSummaryTable.vue index dc9969b61..87e4cffa7 100644 --- a/src/pages/Customer/components/CustomerSummaryTable.vue +++ b/src/pages/Customer/components/CustomerSummaryTable.vue @@ -1,22 +1,25 @@ <script setup> -import { computed, ref } from 'vue'; +import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; -import { QBtn, date } from 'quasar'; +import { date } from 'quasar'; +import { toDateFormat } from 'src/filters/date.js'; import { toCurrency } from 'src/filters'; -import FetchData from 'components/FetchData.vue'; -import CustomerSummaryTableActions from './CustomerSummaryTableActions.vue'; -import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; +import { useSummaryDialog } from 'src/composables/useSummaryDialog'; +import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue'; + +import InvoiceOutDescriptorProxy from 'pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; import RouteDescriptorProxy from 'src/pages/Route/Card/RouteDescriptorProxy.vue'; +import VnTable from 'src/components/VnTable/VnTable.vue'; +import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue'; const { t } = useI18n(); const route = useRoute(); const router = useRouter(); - -const rows = ref([]); +const { viewSummary } = useSummaryDialog(); const filter = { include: [ @@ -32,57 +35,6 @@ const filter = { ], where: { clientFk: route.params.id }, order: ['shipped DESC', 'id'], - limit: 10, -}; - -const tableColumnComponents = { - id: { - component: 'span', - props: () => {}, - event: () => {}, - }, - nickname: { - component: QBtn, - props: () => ({ flat: true, color: 'blue', noCaps: true }), - event: () => {}, - }, - agency: { - component: 'span', - props: () => {}, - event: () => {}, - }, - route: { - component: QBtn, - props: () => ({ flat: true, color: 'blue' }), - event: () => {}, - }, - packages: { - component: 'span', - props: () => {}, - event: () => {}, - }, - date: { - component: 'span', - props: () => {}, - event: () => {}, - }, - state: { - component: 'span', - props: () => {}, - event: () => {}, - }, - total: { - component: 'span', - props: () => {}, - event: () => {}, - }, - actions: { - component: CustomerSummaryTableActions, - props: (prop) => ({ - id: prop.row.id, - }), - event: () => {}, - }, }; const columns = computed(() => [ @@ -94,37 +46,37 @@ const columns = computed(() => [ }, { align: 'left', - field: 'nickname', label: t('Nickname'), name: 'nickname', + columnClass: 'expand', }, { align: 'left', - field: (row) => row?.agencyMode?.name, + format: (row) => row.agencyMode.name, + columnClass: 'expand', label: t('Agency'), - name: 'agency', }, { align: 'left', - field: 'routeFk', + name: 'routeFk', + columnClass: 'shrink', label: t('Route'), - name: 'route', }, { align: 'left', field: 'packages', label: t('Packages'), name: 'packages', + columnClass: 'shrink', }, { align: 'left', - field: (row) => date.formatDate(row?.shipped, 'DD/MM/YYYY'), + format: ({ shipped }) => date.formatDate(shipped, 'DD/MM/YYYY'), label: t('Date'), - name: 'date', + name: 'shipped', }, { align: 'left', - field: (row) => row?.ticketState?.state?.name, label: t('State'), name: 'state', }, @@ -134,11 +86,25 @@ const columns = computed(() => [ label: t('Total'), name: 'total', }, + { - align: 'left', - field: 'totalWithVat', + align: 'right', label: '', - name: 'actions', + name: 'tableActions', + actions: [ + { + title: t('customer.summary.goToLines'), + icon: 'vn:lines', + action: ({ id }) => router.push({ params: { id }, name: 'TicketSale' }), + isPrimary: true, + }, + { + title: t('components.smartCard.viewSummary'), + icon: 'preview', + isPrimary: true, + action: (row) => viewSummary(row.id, TicketSummary), + }, + ], }, ]); @@ -156,84 +122,89 @@ const setTotalPriceColor = (ticket) => { if (total > 0 && total < 50) return 'warning'; }; -const navigateToticketSummary = (id) => { - router.push({ - name: 'TicketSummary', - params: { id }, - }); +const setShippedColor = (date) => { + const today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + + const ticketShipped = new Date(date); + ticketShipped.setHours(0, 0, 0, 0); + + const difference = today - ticketShipped; + + if (difference == 0) return 'warning'; + if (difference < 0) return 'success'; }; -const commonColumns = (col) => ['date', 'state', 'total'].includes(col); </script> <template> - <FetchData + <VnTable + data-key="CustomerTickets" :filter="filter" - @on-fetch="(data) => (rows = data)" - auto-load + :right-search="false" + :column-search="false" url="Tickets" - /> - <QCard class="vn-one q-py-sm flex justify-between"> - <QTable - :columns="columns" - :pagination="{ rowsPerPage: 12 }" - :rows="rows" - class="full-width" - row-key="id" - > - <template #body-cell="props"> - <QTd :props="props" @click="navigateToticketSummary(props.row.id)"> - <QTr :props="props" class="cursor-pointer"> - <component - :is="tableColumnComponents[props.col.name].component" - @click="tableColumnComponents[props.col.name].event(props)" - class="rounded-borders" - v-bind="tableColumnComponents[props.col.name].props(props)" - > - <template v-if="!commonColumns(props.col.name)"> - <span - :class="{ - link: - props.col.name === 'route' || - props.col.name === 'nickname', - }" - > - {{ props.value }} - </span> - </template> - <template v-if="props.col.name === 'date'"> - <QBadge class="q-pa-sm" color="warning"> - {{ props.value }} - </QBadge> - </template> - <template v-if="props.col.name === 'state'"> - <QBadge :color="setStateColor(props.row)" class="q-pa-sm"> - {{ props.value }} - </QBadge> - </template> - <template v-if="props.col.name === 'total'"> - <QBadge - :color="setTotalPriceColor(props.row)" - class="q-pa-sm" - v-if="setTotalPriceColor(props.row)" - > - {{ toCurrency(props.value) }} - </QBadge> - <div v-else>{{ toCurrency(props.value) }}</div> - </template> - <CustomerDescriptorProxy - :id="props.row.clientFk" - v-if="props.col.name === 'nickname'" - /> - <RouteDescriptorProxy - :id="props.row.routeFk" - v-if="props.col.name === 'route'" - /> - </component> - </QTr> - </QTd> - </template> - </QTable> - </QCard> + :columns="columns" + search-url="tickets" + :without-header="true" + auto-load + order="shipped DESC, id" + :disable-option="{ card: true, table: true }" + limit="5" + class="full-width" + > + <template #column-nickname="{ row }"> + <span class="link"> + {{ row.nickname }} + <CustomerDescriptorProxy :id="row.clientFk" /> + </span> + </template> + + <template #column-routeFk="{ row }"> + <span class="link"> + {{ row.routeFk }} + <RouteDescriptorProxy :id="row.routeFk" /> + </span> + </template> + <template #column-total="{ row }"> + <QBadge + class="q-pa-sm" + v-if="setTotalPriceColor(row)" + :color="setTotalPriceColor(row)" + text-color="black" + > + {{ toCurrency(row.totalWithVat) }} + </QBadge> + <span v-else> {{ toCurrency(row.totalWithVat) }}</span> + </template> + <template #column-state="{ row }"> + <span v-if="row.invoiceOut"> + <span :class="{ link: row.invoiceOut.ref }"> + {{ row.invoiceOut.ref }} + <InvoiceOutDescriptorProxy :id="row.invoiceOut.id" /> + </span> + </span> + <QBadge + class="q-pa-sm" + :color="setStateColor(row)" + text-color="black" + v-else-if="setStateColor(row)" + > + {{ row?.ticketState?.state.name }} + </QBadge> + <span v-else> {{ row?.ticketState?.state.name }}</span> + </template> + <template #column-shipped="{ row }"> + <QBadge + class="q-pa-sm" + :color="setShippedColor(row.shipped)" + text-color="black" + v-if="setShippedColor(row.shipped)" + > + {{ toDateFormat(row.shipped) }} + </QBadge> + <span v-else> {{ toDateFormat(row.shipped) }}</span> + </template> + </VnTable> </template> <i18n> diff --git a/src/pages/Customer/components/CustomerSummaryTableActions.vue b/src/pages/Customer/components/CustomerSummaryTableActions.vue deleted file mode 100644 index 6e9038374..000000000 --- a/src/pages/Customer/components/CustomerSummaryTableActions.vue +++ /dev/null @@ -1,44 +0,0 @@ -<script setup> -import { useI18n } from 'vue-i18n'; - -import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue'; - -const { t } = useI18n(); - -defineProps({ - id: { - type: Number, - required: true, - }, -}); - -const { viewSummary } = useSummaryDialog(); -</script> - -<template> - <div> - <QIcon color="primary" name="vn:lines" size="sm"> - <QTooltip> - {{ t('Go to lines') }} - </QTooltip> - </QIcon> - <QIcon - @click.stop="viewSummary(id, TicketSummary)" - class="q-ml-md" - color="primary" - name="preview" - size="sm" - > - <QTooltip> - {{ t('Preview') }} - </QTooltip> - </QIcon> - </div> -</template> - -<i18n> -es: - Go to lines: Ir a lineas - Preview: Vista previa -</i18n> diff --git a/src/pages/Customer/locale/en.yml b/src/pages/Customer/locale/en.yml index 6eb7cfa85..545c3f274 100644 --- a/src/pages/Customer/locale/en.yml +++ b/src/pages/Customer/locale/en.yml @@ -2,3 +2,135 @@ customerFilter: filter: name: Name socialName: Social name +customer: + list: + phone: Phone + email: Email + customerOrders: Display customer orders + moreOptions: More options + card: + customerList: Customer list + customerId: Claim ID + salesPerson: Sales person + credit: Credit + risk: Risk + securedCredit: Secured credit + payMethod: Pay method + debt: Debt + isFrozen: Customer frozen + hasDebt: Customer has debt + isDisabled: Customer inactive + notChecked: Customer no checked + webAccountInactive: Web account inactive + noWebAccess: Web access is disabled + businessType: Business type + passwordRequirements: 'The password must have at least { length } length characters, {nAlpha} alphabetic characters, {nUpper} capital letters, {nDigits} digits and {nPunct} symbols (Ex: $%&.)\n' + businessTypeFk: Business type + summary: + basicData: Basic data + fiscalAddress: Fiscal address + fiscalData: Fiscal data + billingData: Billing data + consignee: Default consignee + businessData: Business data + financialData: Financial data + customerId: Customer ID + name: Name + contact: Contact + phone: Phone + mobile: Mobile + email: Email + salesPerson: Sales person + contactChannel: Contact channel + socialName: Social name + fiscalId: Fiscal ID + postcode: Postcode + province: Province + country: Country + street: Address + isEqualizated: Is equalizated + isActive: Is active + invoiceByAddress: Invoice by address + verifiedData: Verified data + hasToInvoice: Has to invoice + notifyByEmail: Notify by email + vies: VIES + payMethod: Pay method + bankAccount: Bank account + dueDay: Due day + hasLcr: Has LCR + hasCoreVnl: Has core VNL + hasB2BVnl: Has B2B VNL + addressName: Address name + addressCity: City + addressStreet: Street + username: Username + webAccess: Web access + totalGreuge: Total greuge + mana: Mana + priceIncreasingRate: Price increasing rate + averageInvoiced: Average invoiced + claimRate: Claming rate + payMethodFk: Billing data + risk: Risk + maximumRisk: Solunion's maximum risk + riskInfo: Invoices minus payments plus orders not yet invoiced + credit: Credit + creditInfo: Company's maximum risk + securedCredit: Secured credit + securedCreditInfo: Solunion's maximum risk + balance: Balance + balanceInfo: Invoices minus payments + balanceDue: Balance due + balanceDueInfo: Deviated invoices minus payments + recoverySince: Recovery since + businessType: Business Type + city: City + descriptorInfo: Invoices minus payments plus orders not yet + rating: Rating + recommendCredit: Recommended credit + goToLines: Go to lines + basicData: + socialName: Fiscal name + businessType: Business type + contact: Contact + youCanSaveMultipleEmails: You can save multiple emails + email: Email + phone: Phone + mobile: Mobile + salesPerson: Sales person + contactChannel: Contact channel + previousClient: Previous client + extendedList: + tableVisibleColumns: + id: Identifier + name: Name + socialName: Social name + fi: Tax number + salesPersonFk: Salesperson + credit: Credit + creditInsurance: Credit insurance + phone: Phone + mobile: Mobile + street: Street + countryFk: Country + provinceFk: Province + city: City + postcode: Postcode + email: Email + created: Created + businessTypeFk: Business type + payMethodFk: Billing data + sageTaxTypeFk: Sage tax type + sageTransactionTypeFk: Sage tr. type + isActive: Active + isVies: Vies + isTaxDataChecked: Verified data + isEqualizated: Is equalizated + isFreezed: Freezed + hasToInvoice: Invoice + hasToInvoiceByAddress: Invoice by address + isToBeMailed: Mailing + hasLcr: Received LCR + hasCoreVnl: VNL core received + hasSepaVnl: VNL B2B received diff --git a/src/pages/Customer/locale/es.yml b/src/pages/Customer/locale/es.yml index 111696f21..4fcbe3fa2 100644 --- a/src/pages/Customer/locale/es.yml +++ b/src/pages/Customer/locale/es.yml @@ -4,3 +4,134 @@ customerFilter: filter: name: Nombre socialName: Razón Social +customer: + list: + phone: Teléfono + email: Email + customerOrders: Mostrar órdenes del cliente + moreOptions: Más opciones + card: + customerId: ID cliente + salesPerson: Comercial + credit: Crédito + risk: Riesgo + securedCredit: Crédito asegurado + payMethod: Método de pago + debt: Riesgo + isFrozen: Cliente congelado + hasDebt: Cliente con riesgo + isDisabled: Cliente inactivo + notChecked: Cliente no comprobado + webAccountInactive: Sin acceso web + noWebAccess: El acceso web está desactivado + businessType: Tipo de negocio + passwordRequirements: 'La contraseña debe tener al menos { length } caracteres de longitud, {nAlpha} caracteres alfabéticos, {nUpper} letras mayúsculas, {nDigits} dígitos y {nPunct} símbolos (Ej: $%&.)' + businessTypeFk: Tipo de negocio + summary: + basicData: Datos básicos + fiscalAddress: Dirección fiscal + fiscalData: Datos fiscales + billingData: Datos de facturación + consignee: Consignatario pred. + businessData: Datos comerciales + financialData: Datos financieros + customerId: ID cliente + name: Nombre + contact: Contacto + phone: Teléfono + mobile: Móvil + email: Email + salesPerson: Comercial + contactChannel: Canal de contacto + socialName: Razón social + fiscalId: NIF/CIF + postcode: Código postal + province: Provincia + country: País + street: Calle + isEqualizated: Recargo de equivalencia + isActive: Activo + invoiceByAddress: Facturar por consignatario + verifiedData: Datos verificados + hasToInvoice: Facturar + notifyByEmail: Notificar por email + vies: VIES + payMethod: Método de pago + bankAccount: Cuenta bancaria + dueDay: Día de pago + hasLcr: Recibido LCR + hasCoreVnl: Recibido core VNL + hasB2BVnl: Recibido B2B VNL + addressName: Nombre de la dirección + addressCity: Ciudad + addressStreet: Calle + username: Usuario + webAccess: Acceso web + totalGreuge: Greuge total + mana: Maná + priceIncreasingRate: Ratio de incremento de precio + averageInvoiced: Facturación media + claimRate: Ratio de reclamaciones + maximumRisk: Riesgo máximo asumido por Solunion + payMethodFk: Forma de pago + risk: Riesgo + riskInfo: Facturas menos recibos mas pedidos sin facturar + credit: Crédito + creditInfo: Riesgo máximo asumido por la empresa + securedCredit: Crédito asegurado + securedCreditInfo: Riesgo máximo asumido por Solunion + balance: Balance + balanceInfo: Facturas menos recibos + balanceDue: Saldo vencido + balanceDueInfo: Facturas fuera de plazo menos recibos + recoverySince: Recobro desde + businessType: Tipo de negocio + city: Población + descriptorInfo: Facturas menos recibos mas pedidos sin facturar + rating: Clasificación + recommendCredit: Crédito recomendado + goToLines: Ir a líneas + basicData: + socialName: Nombre fiscal + businessType: Tipo de negocio + contact: Contacto + youCanSaveMultipleEmails: Puede guardar varios correos electrónicos encadenándolos mediante comas sin espacios{','} ejemplo{':'} user{'@'}dominio{'.'}com, user2{'@'}dominio{'.'}com siendo el primer correo electrónico el principal + email: Email + phone: Teléfono + mobile: Móvil + salesPerson: Comercial + contactChannel: Canal de contacto + previousClient: Cliente anterior + extendedList: + tableVisibleColumns: + id: Identificador + name: Nombre + socialName: Razón social + fi: NIF / CIF + salesPersonFk: Comercial + credit: Crédito + creditInsurance: Crédito asegurado + phone: Teléfono + mobile: Móvil + street: Dirección fiscal + countryFk: País + provinceFk: Provincia + city: Población + postcode: Código postal + email: Email + created: Fecha creación + businessTypeFk: Tipo de negocio + payMethodFk: Forma de pago + sageTaxTypeFk: Tipo de impuesto Sage + sageTransactionTypeFk: Tipo tr. sage + isActive: Activo + isVies: Vies + isTaxDataChecked: Datos comprobados + isEqualizated: Recargo de equivalencias + isFreezed: Congelado + hasToInvoice: Factura + hasToInvoiceByAddress: Factura por consigna + isToBeMailed: Env. emails + hasLcr: Recibido LCR + hasCoreVnl: Recibido core VNL + hasSepaVnl: Recibido B2B VNL diff --git a/src/pages/Dashboard/DashboardMain.vue b/src/pages/Dashboard/DashboardMain.vue index a339120e2..56054156a 100644 --- a/src/pages/Dashboard/DashboardMain.vue +++ b/src/pages/Dashboard/DashboardMain.vue @@ -60,7 +60,7 @@ const pinnedModules = computed(() => navigation.getPinnedModules()); <QTooltip> {{ 'Ctrl + Alt + ' + - item.keyBinding.toUpperCase() + item?.keyBinding?.toUpperCase() }} </QTooltip> </div> diff --git a/src/pages/Department/Card/DepartmentBasicData.vue b/src/pages/Department/Card/DepartmentBasicData.vue index 4573f7b66..98abfd6b9 100644 --- a/src/pages/Department/Card/DepartmentBasicData.vue +++ b/src/pages/Department/Card/DepartmentBasicData.vue @@ -16,12 +16,12 @@ const workersOptions = ref([]); const clientsOptions = ref([]); </script> <template> - <fetch-data + <FetchData url="Workers/search" @on-fetch="(data) => (workersOptions = data)" auto-load /> - <fetch-data url="Clients" @on-fetch="(data) => (clientsOptions = data)" auto-load /> + <FetchData url="Clients" @on-fetch="(data) => (clientsOptions = data)" auto-load /> <FormModel :url="`Departments/${route.params.id}`" model="department" diff --git a/src/pages/Login/ResetPassword.vue b/src/pages/Login/ResetPassword.vue index eff718e97..2751f1ceb 100644 --- a/src/pages/Login/ResetPassword.vue +++ b/src/pages/Login/ResetPassword.vue @@ -33,7 +33,6 @@ async function onSubmit() { }; try { - console.log('newPassword: ', newPassword); await axios.post( 'VnUsers/reset-password', { newPassword: newPassword.value }, diff --git a/src/pages/Order/Card/OrderCreateDialog.vue b/src/pages/Order/Card/OrderCreateDialog.vue new file mode 100644 index 000000000..9d5c9281e --- /dev/null +++ b/src/pages/Order/Card/OrderCreateDialog.vue @@ -0,0 +1,220 @@ +<script setup> +import { useRoute, useRouter } from 'vue-router'; +import { onMounted, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import axios from 'axios'; +import { useState } from 'composables/useState'; +import FormModelPopup from 'components/FormModelPopup.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import VnInputDate from 'components/common/VnInputDate.vue'; +import { useDialogPluginComponent } from 'quasar'; +import { reactive } from 'vue'; +import FetchData from 'components/FetchData.vue'; + +const { t } = useI18n(); +const route = useRoute(); +const state = useState(); +const ORDER_MODEL = 'order'; + +const router = useRouter(); +const clientList = ref([]); +const agencyList = ref([]); +const addressList = ref([]); +defineEmits(['confirm', ...useDialogPluginComponent.emits]); + +const fetchAddressList = async (addressId) => { + try { + const { data } = await axios.get('addresses', { + params: { + filter: JSON.stringify({ + fields: ['id', 'nickname', 'street', 'city'], + where: { id: addressId }, + }), + }, + }); + addressList.value = data; + if (addressList.value?.length === 1) { + state.get(ORDER_MODEL).addressId = addressList.value[0].id; + } + } catch (err) { + console.error(`Error fetching addresses`, err); + return err.response; + } +}; + +const fetchAgencyList = async (landed, addressFk) => { + if (!landed || !addressFk) { + return; + } + try { + const { data } = await axios.get('Agencies/landsThatDay', { + params: { + addressFk, + landed: new Date(landed).toISOString(), + }, + }); + agencyList.value = data; + } catch (err) { + console.error(`Error fetching agencies`, err); + return err.response; + } +}; + +// const fetchOrderDetails = (order) => { +// fetchAddressList(order?.addressFk); +// fetchAgencyList(order?.landed, order?.addressFk); +// }; +const $props = defineProps({ + clientFk: { + type: Number, + default: null, + }, +}); +const initialFormState = reactive({ + active: true, + addressId: null, + clientFk: $props.clientFk, +}); +// const orderMapper = (order) => { +// return { +// addressId: order.addressFk, +// agencyModeId: order.agencyModeFk, +// landed: new Date(order.landed).toISOString(), +// }; +// }; +// const orderFilter = { +// include: [ +// { relation: 'agencyMode', scope: { fields: ['name'] } }, +// { +// relation: 'address', +// scope: { fields: ['nickname'] }, +// }, +// { relation: 'rows', scope: { fields: ['id'] } }, +// { +// relation: 'client', +// scope: { +// fields: [ +// 'salesPersonFk', +// 'name', +// 'isActive', +// 'isFreezed', +// 'isTaxDataChecked', +// ], +// include: { +// relation: 'salesPersonUser', +// scope: { fields: ['id', 'name'] }, +// }, +// }, +// }, +// ], +// }; + +const onClientChange = async (clientId = $props.clientFk) => { + try { + const { data } = await axios.get(`Clients/${clientId}`); + await fetchAddressList(data.defaultAddressFk); + } catch (error) { + console.error('Error al cambiar el cliente:', error); + } +}; + +async function onDataSaved(_, id) { + await router.push({ path: `/order/${id}/catalog` }); +} +onMounted(async () => { + await onClientChange(); +}); +</script> + +<template> + <FetchData + url="addresses" + @on-fetch="(data) => (clientOptions = data)" + :filter="{ fields: ['id', 'name', 'defaultAddressFk'], order: 'id' }" + auto-load + /> + <FormModelPopup + url-create="Orders/new" + :title="t('Create Order')" + @on-data-saved="onDataSaved" + :model="ORDER_MODEL" + :filter="orderFilter" + :form-initial-data="initialFormState" + > + <template #form-inputs="{ data }"> + <VnRow class="row q-gutter-md q-mb-md"> + <VnSelect + url="Clients" + :label="t('order.form.clientFk')" + v-model="data.clientFk" + option-value="id" + option-label="name" + :filter="{ + fields: ['id', 'name', 'defaultAddressFk'], + }" + hide-selected + @update:model-value="onClientChange" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ `${scope.opt.id}: ${scope.opt.name}` }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + <VnSelect + :label="t('order.form.addressFk')" + v-model="data.addressId" + :options="addressList" + option-value="id" + option-label="street" + hide-selected + :disable="!addressList?.length" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ + `${scope.opt.nickname}: ${scope.opt.street},${scope.opt.city}` + }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </VnRow> + <VnRow class="row q-gutter-md q-mb-md"> + <VnInputDate + placeholder="dd-mm-aaa" + :label="t('order.form.landed')" + v-model="data.landed" + @update:model-value=" + () => fetchAgencyList(data.landed, data.addressId) + " + /> + </VnRow> + <VnRow class="row q-gutter-md q-mb-md"> + <VnSelect + :label="t('order.form.agencyModeFk')" + v-model="data.agencyModeId" + :options="agencyList" + option-value="agencyModeFk" + option-label="agencyMode" + hide-selected + :disable="!agencyList?.length" + > + </VnSelect> + </VnRow> + </template> + </FormModelPopup> +</template> + +<i18n> + es: + No default address found for the client: No hay ninguna dirección asociada a este cliente. +</i18n> diff --git a/src/pages/Supplier/Card/SupplierConsumptionFilter.vue b/src/pages/Supplier/Card/SupplierConsumptionFilter.vue index 3fc61b15e..401bde8fa 100644 --- a/src/pages/Supplier/Card/SupplierConsumptionFilter.vue +++ b/src/pages/Supplier/Card/SupplierConsumptionFilter.vue @@ -1,56 +1,21 @@ <script setup> -import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; - import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInput from 'src/components/common/VnInput.vue'; -import FetchData from 'components/FetchData.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; const { t } = useI18n(); -const props = defineProps({ +defineProps({ dataKey: { type: String, required: true, }, }); - -const buyersOptions = ref([]); -const itemTypesOptions = ref([]); -const itemCategoriesOptions = ref([]); </script> <template> - <FetchData - url="TicketRequests/getItemTypeWorker" - :filter="{ - fields: ['id', 'nickname'], - order: 'nickname ASC', - }" - @on-fetch="(data) => (buyersOptions = data)" - auto-load - /> - <FetchData - url="ItemTypes" - :filter="{ - fields: ['id', 'name', 'categoryFk'], - include: 'category', - order: 'name ASC', - }" - @on-fetch="(data) => (itemTypesOptions = data)" - auto-load - /> - <FetchData - url="ItemCategories" - :filter="{ - fields: ['id', 'name'], - order: 'name ASC', - }" - @on-fetch="(data) => (itemCategoriesOptions = data)" - auto-load - /> - <VnFilterPanel :data-key="props.dataKey" :search-button="true" :redirect="false"> + <VnFilterPanel :data-key="dataKey" :search-button="true" :redirect="false"> <template #tags="{ tag, formatFn }"> <div class="q-gutter-x-xs"> <strong>{{ t(`params.${tag.label}`) }}: </strong> @@ -82,7 +47,9 @@ const itemCategoriesOptions = ref([]); :label="t('params.buyerId')" v-model="params.buyerId" @update:model-value="searchFn()" - :options="buyersOptions" + url="TicketRequests/getItemTypeWorker" + :fields="['id', 'nickname']" + sort-by="nickname ASC" option-value="id" option-label="nickname" hide-selected @@ -98,7 +65,10 @@ const itemCategoriesOptions = ref([]); :label="t('params.typeId')" v-model="params.typeId" @update:model-value="searchFn()" - :options="itemTypesOptions" + url="ItemTypes" + :include="['category']" + :fields="['id', 'name', 'categoryFk']" + sort-by="name ASC" option-value="id" option-label="name" hide-selected @@ -125,7 +95,9 @@ const itemCategoriesOptions = ref([]); :label="t('params.categoryId')" v-model="params.categoryId" @update:model-value="searchFn()" - :options="itemCategoriesOptions" + url="ItemCategories" + :fields="['id', 'name']" + sort-by="name ASC" option-value="id" option-label="name" hide-selected diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index d19c1be49..6af782173 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -154,8 +154,9 @@ async function changeState(value) { :value="entity.warehouse?.name" /> <VnLv + v-if="ticket?.ticketCollections?.length > 0" :label="t('ticket.summary.collection')" - :value="entity.ticketCollections[0]?.collectionFk" + :value="ticket?.ticketCollections[0]?.collectionFk" > <template #value> <a diff --git a/src/pages/Ticket/Card/TicketTransfer.vue b/src/pages/Ticket/Card/TicketTransfer.vue index 9a22c764c..1944b80f4 100644 --- a/src/pages/Ticket/Card/TicketTransfer.vue +++ b/src/pages/Ticket/Card/TicketTransfer.vue @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import VnInput from 'src/components/common/VnInput.vue'; +import TicketTransferForm from './TicketTransferForm.vue'; import { toDateFormat } from 'src/filters/date.js'; import axios from 'axios'; @@ -135,9 +136,7 @@ onMounted(() => (_transfer.value = $props.transfer)); :columns="destinationTicketColumns" :title="t('Destination ticket')" row-key="id" - :pagination="{ rowsPerPage: 0 }" class="full-width q-mt-md" - :no-data-label="t('globals.noResults')" > <template #body-cell-address="{ row }"> <QTd @click.stop> @@ -158,29 +157,11 @@ onMounted(() => (_transfer.value = $props.transfer)); </QTd> </template> + <template #no-data> + <TicketTransferForm v-bind="$props" /> + </template> <template #bottom> - <QForm class="q-mt-lg full-width"> - <VnInput - v-model.number="_transfer.ticketId" - :label="t('Transfer to ticket')" - :clearable="false" - > - <template #append> - <QBtn - icon="keyboard_arrow_right" - color="primary" - @click="transferSales(_transfer.ticketId)" - style="width: 30px" - /> - </template> - </VnInput> - <QBtn - :label="t('New ticket')" - color="primary" - class="full-width q-my-lg" - @click="transferSales()" - /> - </QForm> + <TicketTransferForm v-bind="$props" /> </template> </QTable> </QCard> diff --git a/src/pages/Ticket/Card/TicketTransferForm.vue b/src/pages/Ticket/Card/TicketTransferForm.vue new file mode 100644 index 000000000..9507429e6 --- /dev/null +++ b/src/pages/Ticket/Card/TicketTransferForm.vue @@ -0,0 +1,86 @@ +<script setup> +import { ref, onMounted } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRouter } from 'vue-router'; + +import VnInput from 'src/components/common/VnInput.vue'; + +import axios from 'axios'; + +const $props = defineProps({ + mana: { + type: Number, + default: null, + }, + newPrice: { + type: Number, + default: 0, + }, + transfer: { + type: Object, + default: () => {}, + }, + ticket: { + type: Object, + default: () => {}, + }, +}); + +const emit = defineEmits(['refreshData']); + +const router = useRouter(); +const { t } = useI18n(); + +const _transfer = ref(null); + +const transferSales = async (ticketId) => { + const params = { + ticketId: ticketId, + sales: $props.transfer.sales, + }; + + const { data } = await axios.post( + `tickets/${$props.ticket.id}/transferSales`, + params + ); + + if (data && data.id === $props.ticket.id) emit('refreshData'); + else router.push({ name: 'TicketSale', params: { id: data.id } }); +}; + +onMounted(() => (_transfer.value = $props.transfer)); +</script> + +<template> + {{ _transfer }} + <QForm class="q-mt-lg full-width"> + <VnInput + v-model.number="_transfer.ticketId" + :label="t('Transfer to ticket')" + :clearable="false" + > + <template #append> + <QBtn + icon="keyboard_arrow_right" + color="primary" + @click="transferSales(_transfer.ticketId)" + style="width: 30px" + /> + </template> + </VnInput> + <QBtn + :label="t('New ticket')" + color="primary" + class="full-width q-my-lg" + @click="transferSales()" + /> + </QForm> +</template> + +<i18n> +es: + Sales to transfer: Líneas a transferir + Destination ticket: Ticket destinatario + Transfer to ticket: Transferir a ticket + New ticket: Nuevo ticket +</i18n> diff --git a/src/pages/Ticket/TicketCreateDialog.vue b/src/pages/Ticket/TicketCreateDialog.vue new file mode 100644 index 000000000..1493adc53 --- /dev/null +++ b/src/pages/Ticket/TicketCreateDialog.vue @@ -0,0 +1,226 @@ +<script setup> +import { useRoute, useRouter } from 'vue-router'; +import { onBeforeMount, reactive, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useDialogPluginComponent } from 'quasar'; + +import FormModelPopup from 'components/FormModelPopup.vue'; +import FetchData from 'components/FetchData.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import VnInputDate from 'components/common/VnInputDate.vue'; + +import { useState } from 'composables/useState'; +import axios from 'axios'; + +const { t } = useI18n(); +const route = useRoute(); +const router = useRouter(); +const state = useState(); +const user = state.getUser(); +defineEmits(['confirm', ...useDialogPluginComponent.emits]); + +const initialFormState = reactive({ + clientId: Number(route.query?.clientFk) || null, + addressId: null, + agencyModeId: null, + warehouseId: user.value.warehouseFk, + landed: null, +}); +const clientOptions = ref([]); +const agenciesOptions = ref([]); +const addressesOptions = ref([]); +const warehousesOptions = ref([]); +const selectedClient = ref(null); + +onBeforeMount(async () => { + await onClientSelected(initialFormState); +}); + +const fetchClient = async (formData) => { + try { + const filter = { + include: { + relation: 'defaultAddress', + scope: { + fields: ['id', 'agencyModeFk'], + }, + }, + where: { id: formData.clientId }, + }; + const params = { filter: JSON.stringify(filter) }; + const { data } = await axios.get('Clients', { params }); + const [client] = data; + selectedClient.value = client; + } catch (err) { + console.error('Error fetching client'); + } +}; + +const fetchAddresses = async (formData) => { + try { + if (!formData.clientId) return; + + const filter = { + fields: ['nickname', 'street', 'city', 'id'], + where: { isActive: true }, + order: 'nickname ASC', + }; + const params = { filter: JSON.stringify(filter) }; + const { data } = await axios.get(`Clients/${formData.clientId}/addresses`, { + params, + }); + addressesOptions.value = data; + + const { defaultAddress } = selectedClient.value; + formData.addressId = defaultAddress.id; + } catch (err) { + console.error(`Error fetching addresses`, err); + return err.response; + } +}; + +const onClientSelected = async (formData) => { + await fetchClient(formData); + await fetchAddresses(formData); +}; + +const fetchAvailableAgencies = async (formData) => { + if (!formData.warehouseId || !formData.addressId || !formData.landed) return; + let params = { + warehouseFk: formData.warehouseId, + addressFk: formData.addressId, + landed: formData.landed, + }; + + const { data } = await axios.get('Agencies/getAgenciesWithWarehouse', { params }); + + agenciesOptions.value = data; + + const defaultAgency = agenciesOptions.value.find( + (agency) => + agency.agencyModeFk === selectedClient.value.defaultAddress.agencyModeFk + ); + + if (defaultAgency) formData.agencyModeId = defaultAgency.agencyModeFk; +}; + +const redirectToTicketList = (_, { id }) => { + router.push({ name: 'TicketSummary', params: { id } }); +}; +</script> + +<template> + <FetchData + url="Clients" + @on-fetch="(data) => (clientOptions = data)" + :filter="{ fields: ['id', 'name', 'defaultAddressFk'], order: 'id' }" + auto-load + /> + <FetchData + url="Warehouses" + @on-fetch="(data) => (warehousesOptions = data)" + order="name" + auto-load + /> + + <FormModelPopup + :title="t('globals.pageTitles.createTicket')" + url-create="Tickets/new" + model="ticket" + :form-initial-data="initialFormState" + @on-data-saved="redirectToTicketList" + > + <template #form-inputs="{ data }"> + <VnRow> + <div class="col"> + <VnSelect + :label="t('ticket.create.client')" + v-model="data.clientId" + :options="clientOptions" + option-value="id" + option-label="name" + hide-selected + @update:model-value="(client) => onClientSelected(data)" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt.name }} + </QItemLabel> + <QItemLabel caption> + {{ `#${scope.opt.id}` }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </div> + </VnRow> + <VnRow> + <div class="col"> + <VnSelect + :label="t('ticket.create.address')" + v-model="data.addressId" + :options="addressesOptions" + option-value="id" + option-label="nickname" + hide-selected + :disable="!data.clientId" + @update:model-value="() => fetchAvailableAgencies(data)" + > + <template #option="scope"> + <QItem v-bind="scope.itemProps"> + <QItemSection> + <QItemLabel> + {{ scope.opt.nickname }} + </QItemLabel> + <QItemLabel caption> + {{ `${scope.opt.street}, ${scope.opt.city}` }} + </QItemLabel> + </QItemSection> + </QItem> + </template> + </VnSelect> + </div> + </VnRow> + <VnRow> + <div class="col"> + <VnInputDate + placeholder="dd-mm-aaa" + :label="t('ticket.create.landed')" + v-model="data.landed" + @update:model-value="() => fetchAvailableAgencies(data)" + /> + </div> + </VnRow> + <VnRow> + <div class="col"> + <VnSelect + :label="t('ticket.create.warehouse')" + v-model="data.warehouseId" + :options="warehousesOptions" + option-value="id" + option-label="name" + hide-selected + @update:model-value="() => fetchAvailableAgencies(data)" + /> + </div> + </VnRow> + <VnRow> + <div class="col"> + <VnSelect + :label="t('ticket.create.agency')" + v-model="data.agencyModeId" + :options="agenciesOptions" + option-value="agencyModeFk" + option-label="agencyMode" + hide-selected + :disable="!data.clientId || !data.landed || !data.warehouseId" + /> + </div> + </VnRow> + </template> + </FormModelPopup> +</template> diff --git a/src/pages/Ticket/TicketList.vue b/src/pages/Ticket/TicketList.vue index cbd102317..f3e0e5c06 100644 --- a/src/pages/Ticket/TicketList.vue +++ b/src/pages/Ticket/TicketList.vue @@ -14,6 +14,8 @@ import VnRow from 'src/components/ui/VnRow.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import TicketFilter from './TicketFilter.vue'; +import { useRoute } from 'vue-router'; +const route = useRoute(); const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const tableRef = ref(); @@ -27,8 +29,17 @@ const to = Date.vnNew(); to.setDate(to.getDate() + 1); const userParams = { - from: from.toISOString(), - to: to.toISOString(), + from: null, + to: null, +}; +// Método para inicializar las variables desde la query string +const initializeFromQuery = () => { + const query = route.query.table ? JSON.parse(route.query.table) : {}; + + // Asigna los valores a las variables correspondientes + from.value = query.from || from.toISOString(); + to.value = query.to || to.toISOString(); + Object.assign(userParams, { from, to }); }; const columns = computed(() => [ @@ -200,7 +211,10 @@ const getColor = (row) => { return row?.classColor ? `bg-${row.classColor}` : 'bg-orange'; }; -onMounted(() => (stateStore.rightDrawer = true)); +onMounted(() => { + initializeFromQuery(); + stateStore.rightDrawer = true; +}); </script> <template>