diff --git a/.vscode/settings.json b/.vscode/settings.json index ecc1d50d7..5026b7d3b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,6 @@ ], "[vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "cSpell.words": ["axios"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 243d67a34..3316aa441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2352.01] - 2023-12-28 + +### Added +- (carros) => Se añade contador de carros. #6545 +- (Reclamaciones) => Se añade la sección para hacer acciones sobre una reclamación. #5654 +### Changed +### Fixed +- (Reclamaciones) => Se corrige el color de la barra según el tema y el evento de actualziar cantidades #6334 + + ## [2253.01] - 2023-01-05 ### Added diff --git a/package.json b/package.json index 8568d507d..929a7c202 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "23.36.01", + "version": "23.52.01", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", @@ -15,7 +15,7 @@ "test:unit:ci": "vitest run" }, "dependencies": { - "@quasar/cli": "^2.2.1", + "@quasar/cli": "^2.3.0", "@quasar/extras": "^1.16.4", "axios": "^1.4.0", "chromium": "^3.0.3", diff --git a/quasar.config.js b/quasar.config.js index 2d3c71417..755e96bd3 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -66,7 +66,9 @@ module.exports = configure(function (/* ctx */) { // publicPath: '/', // analyze: true, // env: {}, - // rawDefine: {} + rawDefine: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) + }, // ignorePublicFolder: true, // minify: false, // polyfillModulePreload: true, @@ -89,11 +91,12 @@ module.exports = configure(function (/* ctx */) { vitePlugins: [ [ - VueI18nPlugin, + VueI18nPlugin({ + runtimeOnly: false + }), { // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false` // compositionOnly: false, - // you need to set i18n resource including paths ! include: path.resolve(__dirname, './src/i18n/**'), }, diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index 2a4982fce..a5156dc79 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -89,6 +89,7 @@ async function fetch(data) { watch(formData, () => (hasChanges.value = true), { deep: true }); emit('onFetch', data); + return data; } async function reset() { @@ -268,7 +269,7 @@ watch(formUrl, async () => { - + - - + + {{ t('globals.collapseMenu') }} - + - + diff --git a/src/components/ui/CardDescriptor.vue b/src/components/ui/CardDescriptor.vue index f63b75de6..eef2e020b 100644 --- a/src/components/ui/CardDescriptor.vue +++ b/src/components/ui/CardDescriptor.vue @@ -1,8 +1,9 @@ diff --git a/src/components/ui/CardList.vue b/src/components/ui/CardList.vue index ed1df7668..af2398ebd 100644 --- a/src/components/ui/CardList.vue +++ b/src/components/ui/CardList.vue @@ -32,7 +32,7 @@ const $props = defineProps({ gap: 2%; width: 50%; .label { - width: 30%; + width: 35%; color: var(--vn-label); overflow: hidden; text-overflow: ellipsis; diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index d78c3ba13..55e432857 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -41,15 +41,11 @@ onMounted(() => { const isLoading = ref(false); async function search() { - for (const param in userParams.value) { - if (userParams.value[param] === '' || userParams.value[param] === null) { - delete userParams.value[param]; - delete store.userParams[param]; - } - } - const params = { ...userParams.value }; isLoading.value = true; - await arrayData.addFilter({ params }); + const params = { ...userParams.value }; + const { params: newParams } = await arrayData.addFilter({ params }); + userParams.value = newParams; + if (!props.showAll && !Object.values(params).length) store.data = []; isLoading.value = false; @@ -78,10 +74,11 @@ async function clearFilters() { const tags = computed(() => { const params = []; - for (const param in store.userParams) { + for (const param in userParams.value) { + if (!userParams.value[param]) continue; params.push({ label: param, - value: store.userParams[param], + value: userParams.value[param], }); } @@ -89,8 +86,7 @@ const tags = computed(() => { }); async function remove(key) { - delete userParams.value[key]; - delete store.userParams[key]; + userParams.value[key] = null; await search(); } @@ -200,7 +196,7 @@ function formatValue(value) { -es: +es: No filters applied: No se han aplicado filtros Applied filters: Filtros aplicados Remove filters: Eliminar filtros diff --git a/src/components/ui/VnLinkPhone.vue b/src/components/ui/VnLinkPhone.vue new file mode 100644 index 000000000..4445b99c9 --- /dev/null +++ b/src/components/ui/VnLinkPhone.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/src/components/ui/VnPaginate.vue b/src/components/ui/VnPaginate.vue index d21d073f2..c75761462 100644 --- a/src/components/ui/VnPaginate.vue +++ b/src/components/ui/VnPaginate.vue @@ -50,6 +50,10 @@ const props = defineProps({ type: Boolean, default: true, }, + exprBuilder: { + type: Function, + default: null, + }, }); const emit = defineEmits(['onFetch', 'onPaginate']); @@ -68,6 +72,7 @@ const arrayData = useArrayData(props.dataKey, { limit: props.limit, order: props.order, userParams: props.userParams, + exprBuilder: props.exprBuilder, }); const store = arrayData.store; diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js index 9aff0eaa8..6fcca4722 100644 --- a/src/composables/useArrayData.js +++ b/src/composables/useArrayData.js @@ -2,6 +2,7 @@ import { onMounted, ref, computed } from 'vue'; import { useRouter, useRoute } from 'vue-router'; import axios from 'axios'; import { useArrayDataStore } from 'stores/useArrayDataStore'; +import { buildFilter } from 'filters/filterPanel'; const arrayDataStore = useArrayDataStore(); @@ -29,6 +30,10 @@ export function useArrayData(key, userOptions) { } }); + if (key && userOptions) { + setOptions(); + } + function setOptions() { const allowedOptions = [ 'url', @@ -39,10 +44,11 @@ export function useArrayData(key, userOptions) { 'skip', 'userParams', 'userFilter', + 'exprBuilder', ]; if (typeof userOptions === 'object') { for (const option in userOptions) { - const isEmpty = userOptions[option] == null || userOptions[option] == ''; + const isEmpty = userOptions[option] == null || userOptions[option] === ''; if (isEmpty || !allowedOptions.includes(option)) continue; if (Object.prototype.hasOwnProperty.call(store, option)) { @@ -64,16 +70,27 @@ export function useArrayData(key, userOptions) { skip: store.skip, }; - Object.assign(filter, store.userFilter); - Object.assign(store.filter, filter); + let exprFilter; + let userParams = { ...store.userParams }; + if (store?.exprBuilder) { + const where = buildFilter(userParams, (param, value) => { + const res = store.exprBuilder(param, value); + if (res) delete userParams[param]; + return res; + }); + exprFilter = where ? { where } : null; + } + Object.assign(filter, store.userFilter, exprFilter); + Object.assign(store.filter, filter); const params = { filter: JSON.stringify(store.filter), }; - Object.assign(params, store.userParams); + Object.assign(params, userParams); store.isLoading = true; + const response = await axios.get(store.url, { signal: canceller.signal, params, @@ -97,6 +114,7 @@ export function useArrayData(key, userOptions) { store.isLoading = false; canceller = null; + return response; } function destroy() { @@ -121,9 +139,30 @@ export function useArrayData(key, userOptions) { async function addFilter({ filter, params }) { if (filter) store.userFilter = Object.assign(store.userFilter, filter); - if (params) store.userParams = Object.assign(store.userParams, params); + + let userParams = Object.assign({}, store.userParams, params); + userParams = sanitizerParams(userParams, store?.exprBuilder); + + store.userParams = userParams; await fetch({ append: false }); + return { filter, params }; + } + + function sanitizerParams(params) { + for (const param in params) { + if (params[param] === '' || params[param] === null) { + delete store.userParams[param]; + delete params[param]; + if (store.filter?.where) { + delete store.filter.where[Object.keys(store?.exprBuilder(param))[0]]; + if (Object.keys(store.filter.where).length === 0) { + delete store.filter.where; + } + } + } + } + return params; } async function loadMore() { @@ -147,10 +186,11 @@ export function useArrayData(key, userOptions) { if (store.userParams && Object.keys(store.userParams).length !== 0) query.params = JSON.stringify(store.userParams); - router.replace({ - path: route.path, - query: query, - }); + if (router) + router.replace({ + path: route.path, + query: query, + }); } const totalRows = computed(() => (store.data && store.data.length) || 0); diff --git a/src/css/app.scss b/src/css/app.scss index 0f04c9ad8..99170202d 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -45,3 +45,9 @@ body.body--dark { .bg-vn-dark { background-color: var(--vn-dark); } + +.vn-card { + background-color: var(--vn-gray); + color: var(--vn-text); + border-radius: 8px; +} diff --git a/src/filters/filterPanel.js b/src/filters/filterPanel.js new file mode 100644 index 000000000..ee91e6749 --- /dev/null +++ b/src/filters/filterPanel.js @@ -0,0 +1,94 @@ +/** + * Passes a loopback fields filter to an object. + * + * @param {Object} fields The fields object or array + * @return {Object} The fields as object + */ +function fieldsToObject(fields) { + let fieldsObj = {}; + + if (Array.isArray(fields)) { + for (let field of fields) fieldsObj[field] = true; + } else if (typeof fields == 'object') { + for (let field in fields) { + if (fields[field]) fieldsObj[field] = true; + } + } + + return fieldsObj; +} + +/** + * Merges two loopback fields filters. + * + * @param {Object|Array} src The source fields + * @param {Object|Array} dst The destination fields + * @return {Array} The merged fields as an array + */ +function mergeFields(src, dst) { + let fields = {}; + Object.assign(fields, fieldsToObject(src), fieldsToObject(dst)); + return Object.keys(fields); +} + +/** + * Merges two loopback where filters. + * + * @param {Object|Array} src The source where + * @param {Object|Array} dst The destination where + * @return {Array} The merged wheres + */ +function mergeWhere(src, dst) { + let and = []; + if (src) and.push(src); + if (dst) and.push(dst); + return simplifyOperation(and, 'and'); +} + +/** + * Merges two loopback filters returning the merged filter. + * + * @param {Object} src The source filter + * @param {Object} dst The destination filter + * @return {Object} The result filter + */ +function mergeFilters(src, dst) { + let res = Object.assign({}, dst); + + if (!src) return res; + + if (src.fields) res.fields = mergeFields(src.fields, res.fields); + if (src.where) res.where = mergeWhere(res.where, src.where); + if (src.include) res.include = src.include; + if (src.order) res.order = src.order; + if (src.limit) res.limit = src.limit; + if (src.offset) res.offset = src.offset; + if (src.skip) res.skip = src.skip; + + return res; +} + +function simplifyOperation(operation, operator) { + switch (operation.length) { + case 0: + return undefined; + case 1: + return operation[0]; + default: + return { [operator]: operation }; + } +} + +function buildFilter(params, builderFunc) { + let and = []; + + for (let param in params) { + let value = params[param]; + if (value == null) continue; + let expr = builderFunc(param, value); + if (expr) and.push(expr); + } + return simplifyOperation(and, 'and'); +} + +export { fieldsToObject, mergeFields, mergeWhere, mergeFilters, buildFilter }; diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js index 792bd3965..f2b4c9c55 100644 --- a/src/i18n/en/index.js +++ b/src/i18n/en/index.js @@ -45,6 +45,7 @@ export default { today: 'Today', yesterday: 'Yesterday', dateFormat: 'en-GB', + microsip: 'Open in MicroSIP', noSelectedRows: `You don't have any line selected`, }, errors: { @@ -283,6 +284,7 @@ export default { development: 'Development', log: 'Audit logs', notes: 'Notes', + action: 'Action', }, list: { customer: 'Customer', @@ -457,6 +459,7 @@ export default { typesList: 'Types List', typeCreate: 'Create type', typeEdit: 'Edit type', + wagonCounter: 'Trolley counter', }, type: { name: 'Name', diff --git a/src/i18n/es/index.js b/src/i18n/es/index.js index 4ce8c23d6..39d7d7ae0 100644 --- a/src/i18n/es/index.js +++ b/src/i18n/es/index.js @@ -46,6 +46,7 @@ export default { yesterday: 'Ayer', dateFormat: 'es-ES', noSelectedRows: `No tienes ninguna línea seleccionada`, + microsip: 'Abrir en MicroSIP', }, errors: { statusUnauthorized: 'Acceso denegado', @@ -282,6 +283,7 @@ export default { photos: 'Fotos', log: 'Registros de auditoría', notes: 'Notas', + action: 'Acción', }, list: { customer: 'Cliente', @@ -457,6 +459,7 @@ export default { typesList: 'Listado tipos', typeCreate: 'Crear tipo', typeEdit: 'Editar tipo', + wagonCounter: 'Contador de carros', }, type: { name: 'Nombre', diff --git a/src/pages/Claim/Card/ClaimAction.vue b/src/pages/Claim/Card/ClaimAction.vue new file mode 100644 index 000000000..44575f1d6 --- /dev/null +++ b/src/pages/Claim/Card/ClaimAction.vue @@ -0,0 +1,518 @@ + + + (claim = data)" + auto-load + /> + (resolvedStateId = data.id)" + auto-load + :where="{ code: 'resolved' }" + /> + (destinationTypes = data)" + /> + + + + + + {{ t('globals.collapseMenu') }} + + + + + + + + {{ `${t('Total claimed')}: ${toCurrency(totalClaimed)}` }} + + + + + + {{ t('claim.summary.actions') }} + + save({ responsibility: value })" + label-always + color="primary" + markers + :marker-labels="marker_labels" + :min="DEFAULT_MIN_RESPONSABILITY" + :max="DEFAULT_MAX_RESPONSABILITY" + /> + + + + + save({ isChargedToMana: value })" + /> + {{ t('mana') }} + + + + + + + + + + {{ value }} + + + + + + + updateDestination(value, row)" + /> + + + + + {{ toCurrency(value) }} + + + + + {{ toCurrency(value) }} + + + + + + + + + + + + + + + + + {{ column.label }} + + + + + {{ column.value.description }} + + + {{ column.value }} + + + + + + + + + + + + + + + + + + + + + + {{ t('dialog title') }} + + + + + + + + + + + + + + + + + +en: + mana: Is paid with mana + dialog title: Change destination to all selected rows + confirmGreuges: Do you want to insert complaints? + confirmGreugesMessage: Insert complaints into the client's record + +es: + mana: Cargado al maná + Delivered: Descripción + Quantity: Cantidad + Claimed: Rec + Description: Descripción + Price: Precio + Discount: Dto. + Destination: Destino + Landed: F.entrega + Remove line: Eliminar línea + Total claimed: Total reclamado + Regularize: Regularizar + Change destination: Cambiar destino + Import claim: Importar reclamación + dialog title: Cambiar destino en todas las filas seleccionadas + Remove: Eliminar + dialogGreuge title: Insertar greuges en la ficha del cliente + ClaimGreugeDescription: Id reclamación + Id item: Id artículo + confirmGreuges: ¿Desea insertar greuges? + confirmGreugesMessage: Insertar greuges en la ficha del cliente + diff --git a/src/pages/Claim/Card/ClaimCard.vue b/src/pages/Claim/Card/ClaimCard.vue index 03b9889f0..a8c832967 100644 --- a/src/pages/Claim/Card/ClaimCard.vue +++ b/src/pages/Claim/Card/ClaimCard.vue @@ -22,11 +22,6 @@ const $props = defineProps({ const entityId = computed(() => { return $props.id || route.params.id; }); - -let salixUrl; -onMounted(async () => { - salixUrl = await getUrl(`claim/${entityId.value}`); -}); @@ -42,18 +37,6 @@ onMounted(async () => { - - - - - {{ t('Action') }} - - diff --git a/src/pages/Claim/Card/ClaimDescriptor.vue b/src/pages/Claim/Card/ClaimDescriptor.vue index 6933458b9..0aeae3ef5 100644 --- a/src/pages/Claim/Card/ClaimDescriptor.vue +++ b/src/pages/Claim/Card/ClaimDescriptor.vue @@ -62,13 +62,18 @@ const filter = { ], }; +const STATE_COLOR = { + pending: 'positive', + managed: 'warning', + resolved: 'negative', +}; + function stateColor(code) { - if (code === 'pending') return 'positive'; - if (code === 'managed') return 'warning'; - if (code === 'resolved') return 'negative'; + return STATE_COLOR[code]; } const data = ref(useCardDescription()); const setData = (entity) => { + if (!entity) return; data.value = useCardDescription(entity.client.name, entity.id); state.set('ClaimDescriptor', entity); }; @@ -83,6 +88,7 @@ const setData = (entity) => { :title="data.title" :subtitle="data.subtitle" @on-fetch="setData" + data-key="claimData" > @@ -120,16 +126,16 @@ const setData = (entity) => { - {{ entity.client.salesPersonUser.name }} - + {{ entity.client?.salesPersonUser?.name }} + - + diff --git a/src/pages/Claim/Card/ClaimDevelopment.vue b/src/pages/Claim/Card/ClaimDevelopment.vue index 0c83bdadd..0ab3c6c90 100644 --- a/src/pages/Claim/Card/ClaimDevelopment.vue +++ b/src/pages/Claim/Card/ClaimDevelopment.vue @@ -7,6 +7,7 @@ import FetchData from 'components/FetchData.vue'; import VnSelectFilter from 'components/common/VnSelectFilter.vue'; import { getUrl } from 'composables/getUrl'; import { tMobile } from 'composables/tMobile'; +import router from 'src/router'; const route = useRoute(); @@ -19,7 +20,7 @@ const claimResponsibles = ref([]); const claimRedeliveries = ref([]); const workers = ref([]); const selected = ref([]); -const insertButtonRef = ref(); +const saveButtonRef = ref(); let salixUrl; onMounted(async () => { @@ -102,10 +103,6 @@ const columns = computed(() => [ tabIndex: 5, }, ]); - -function goToAction() { - location.href = `${salixUrl}/action`; -} @@ -175,6 +172,7 @@ function goToAction() { :option-label="col.optionLabel" :autofocus="col.tabIndex == 1" input-debounce="0" + hide-selected > @@ -213,6 +211,7 @@ function goToAction() { dense input-debounce="0" :autofocus="col.tabIndex == 1" + hide-selected /> @@ -236,13 +235,11 @@ function goToAction() { @@ -251,9 +248,6 @@ function goToAction() { .grid-style-transition { transition: transform 0.28s, background-color 0.28s; } -.maxwidth { - width: 100%; -} diff --git a/src/pages/Claim/Card/ClaimLines.vue b/src/pages/Claim/Card/ClaimLines.vue index c03291b85..fa7fb123f 100644 --- a/src/pages/Claim/Card/ClaimLines.vue +++ b/src/pages/Claim/Card/ClaimLines.vue @@ -46,7 +46,7 @@ async function onFetchClaim(data) { const amount = ref(0); const amountClaimed = ref(0); async function onFetch(rows) { - if (!rows || rows.length) return; + if (!rows || !rows.length) return; amount.value = rows.reduce( (acumulator, { sale }) => acumulator + sale.price * sale.quantity, 0 @@ -155,7 +155,7 @@ function showImportDialog() { - + {{ t('Amount') }} diff --git a/src/pages/Claim/Card/ClaimPhoto.vue b/src/pages/Claim/Card/ClaimPhoto.vue index 483dbffc1..6ac116ce0 100644 --- a/src/pages/Claim/Card/ClaimPhoto.vue +++ b/src/pages/Claim/Card/ClaimPhoto.vue @@ -26,6 +26,7 @@ const client = ref({}); const inputFile = ref(); const files = ref({}); +const spinnerRef = ref(); const claimDmsRef = ref(); const dmsType = ref({}); const config = ref({}); @@ -118,11 +119,11 @@ async function create() { clientId: client.value.id, }).toUpperCase(), }; - + spinnerRef.value.show(); await axios.post(query, formData, { params: dms, }); - + spinnerRef.value.hide(); quasar.notify({ message: t('globals.dataSaved'), type: 'positive', @@ -234,7 +235,9 @@ function onDrag() { - + + + diff --git a/src/pages/Claim/Card/ClaimSummary.vue b/src/pages/Claim/Card/ClaimSummary.vue index dc5ec9544..64d7b721b 100644 --- a/src/pages/Claim/Card/ClaimSummary.vue +++ b/src/pages/Claim/Card/ClaimSummary.vue @@ -85,10 +85,15 @@ const detailsColumns = ref([ }, ]); +const STATE_COLOR = { + pending: 'positive', + + managed: 'warning', + + resolved: 'negative', +}; function stateColor(code) { - if (code === 'pending') return 'green'; - if (code === 'managed') return 'orange'; - if (code === 'resolved') return 'red'; + return STATE_COLOR[code]; } const developmentColumns = ref([ diff --git a/src/pages/Claim/ClaimFilter.vue b/src/pages/Claim/ClaimFilter.vue index 5918712fd..ac8f4f0db 100644 --- a/src/pages/Claim/ClaimFilter.vue +++ b/src/pages/Claim/ClaimFilter.vue @@ -3,6 +3,7 @@ import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import FetchData from 'components/FetchData.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; +import VnSelectFilter from 'components/common/VnSelectFilter.vue'; const { t } = useI18n(); const props = defineProps({ @@ -60,7 +61,7 @@ const states = ref(); - - - - (data.value = useCardDescription(entity.name, entity :title="data.title" :subtitle="data.subtitle" @on-fetch="setData" + data-key="customerData" > diff --git a/src/pages/Customer/Card/CustomerSummary.vue b/src/pages/Customer/Card/CustomerSummary.vue index 081bdd157..6693274ac 100644 --- a/src/pages/Customer/Card/CustomerSummary.vue +++ b/src/pages/Customer/Card/CustomerSummary.vue @@ -6,6 +6,7 @@ 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'; const route = useRoute(); const { t } = useI18n(); @@ -68,8 +69,18 @@ const creditWarning = computed(() => { - - + + + {{ t('customer.summary.phone') }} + + + + + + {{ t('customer.summary.mobile') }} + + + -import { reactive, watch } from 'vue'; - -const customer = reactive({ - name: '', -}); - -watch(() => customer.name); - - - - - - - - - - - - - - - - - - - - diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index 113e0cc7c..c276b0d3e 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -3,6 +3,7 @@ import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import FetchData from 'components/FetchData.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; +import VnSelectFilter from 'components/common/VnSelectFilter.vue'; const { t } = useI18n(); const props = defineProps({ @@ -63,7 +64,7 @@ const zones = ref(); - - @@ -124,7 +126,7 @@ const zones = ref(); - - + + + {{ t('customer.list.phone') }} + + + (data.value = useCardDescription(entity.ref, entity. :title="data.title" :subtitle="data.subtitle" @on-fetch="setData" + data-key="invoiceOutData" > diff --git a/src/pages/Ticket/Card/TicketDescriptor.vue b/src/pages/Ticket/Card/TicketDescriptor.vue index 641ffee0c..d2a407874 100644 --- a/src/pages/Ticket/Card/TicketDescriptor.vue +++ b/src/pages/Ticket/Card/TicketDescriptor.vue @@ -81,6 +81,7 @@ const setData = (entity) => :filter="filter" :title="data.title" :subtitle="data.subtitle" + data-key="ticketData" @on-fetch="setData" > diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index ce46d1d47..5c4c79a4f 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -10,6 +10,7 @@ import FetchedTags from 'components/ui/FetchedTags.vue'; import InvoiceOutDescriptorProxy from 'pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue'; import VnLv from 'src/components/ui/VnLv.vue'; +import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import { getUrl } from 'src/composables/getUrl'; onUpdated(() => summaryRef.value.fetch()); @@ -172,7 +173,7 @@ async function changeState(value) { :label="t('ticket.summary.agency')" :value="ticket.agencyMode.name" /> - + - + {{ dashIfEmpty(ticket.refFk) }} - - - - + + + {{ t('ticket.summary.consigneePhone') }} + + + + + + {{ t('ticket.summary.consigneeMobile') }} + + + + + + {{ t('ticket.summary.clientPhone') }} + + + + + + {{ t('ticket.summary.clientMobile') }} + + + - + diff --git a/src/pages/Wagon/WagonCounter.vue b/src/pages/Wagon/WagonCounter.vue new file mode 100644 index 000000000..bd5d2ca67 --- /dev/null +++ b/src/pages/Wagon/WagonCounter.vue @@ -0,0 +1,154 @@ + + + + + + + {{ props.title }} + + + + {{ props.count }} + + + + {{ t('Add 30') }} + + + {{ t('Add 10') }} + + + + + {{ t('Subtract 1') }} + + + {{ t('Flush') }} + + + + + + + + + +es: + Subtract 1: Quitar 1 + Add 30: Añadir 30 + Add 10: Añadir 10 + Flush: Vaciar + Are you sure?: ¿Estás seguro? + It will set to 0: Se pondrá a 0 + The counter will be reset to zero: Se pondrá el contador a cero + diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index ba64a5abb..4a1b2a6e8 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -5,7 +5,9 @@ import { useI18n } from 'vue-i18n'; import { useSession } from 'src/composables/useSession'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; +import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import useCardDescription from 'src/composables/useCardDescription'; + const $props = defineProps({ id: { type: Number, @@ -47,19 +49,22 @@ const filter = { ], }; -const sip = computed(() => worker.value.sip && worker.value.sip.extension); +const sip = computed(() => worker.value?.sip && worker.value.sip.extension); function getWorkerAvatar() { const token = getToken(); return `/api/Images/user/160x160/${route.params.id}/download?access_token=${token}`; } const data = ref(useCardDescription()); -const setData = (entity) => - (data.value = useCardDescription(entity.user.nickname, entity.id)); +const setData = (entity) => { + if (!entity) return; + data.value = useCardDescription(entity.user.nickname, entity.id); +}; - - + + - - + + + {{ t('worker.card.phone') }} + + + + + + {{ t('worker.summary.sipExtension') }} + + + diff --git a/src/pages/Worker/Card/WorkerNotificationsManager.vue b/src/pages/Worker/Card/WorkerNotificationsManager.vue index 13c9b3de2..44573adca 100644 --- a/src/pages/Worker/Card/WorkerNotificationsManager.vue +++ b/src/pages/Worker/Card/WorkerNotificationsManager.vue @@ -1,10 +1,12 @@ + - - - - - - {{ t('worker.notificationsManager.activeNotifications') }} - - - - - - - - - - {{ t('worker.notificationsManager.availableNotifications') }} - - - + + + + + - - {{ notification.name }} - {{ - notification.description - }} - - + + + + + + {{ notification.name }} + + {{ notification.description }} + + - - - - - - - + + + + + + + + + + diff --git a/src/pages/Worker/Card/WorkerSummary.vue b/src/pages/Worker/Card/WorkerSummary.vue index 7c8accc5d..970a0dee4 100644 --- a/src/pages/Worker/Card/WorkerSummary.vue +++ b/src/pages/Worker/Card/WorkerSummary.vue @@ -7,6 +7,7 @@ import { getUrl } from 'src/composables/getUrl'; import VnLv from 'src/components/ui/VnLv.vue'; import WorkerDescriptorProxy from './WorkerDescriptorProxy.vue'; import { dashIfEmpty } from 'src/filters'; +import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; const route = useRoute(); const { t } = useI18n(); @@ -91,15 +92,24 @@ const filter = { - - - + + + {{ t('worker.summary.phoneExtension') }} + + + + + + {{ t('worker.summary.entPhone') }} + + + + + + {{ t('worker.summary.personalPhone') }} + + + @@ -109,10 +119,12 @@ const filter = { - + + + {{ t('worker.summary.sipExtension') }} + + + diff --git a/src/router/modules/claim.js b/src/router/modules/claim.js index 9df1dd64e..1dfd75cff 100644 --- a/src/router/modules/claim.js +++ b/src/router/modules/claim.js @@ -19,6 +19,7 @@ export default { 'ClaimLog', 'ClaimNotes', 'ClaimDevelopment', + 'ClaimAction', ], }, children: [ @@ -130,6 +131,15 @@ export default { }, component: () => import('src/pages/Claim/Card/ClaimNotes.vue'), }, + { + name: 'ClaimAction', + path: 'action', + meta: { + title: 'action', + icon: 'vn:actions', + }, + component: () => import('src/pages/Claim/Card/ClaimAction.vue'), + }, ], }, ], diff --git a/src/router/modules/customer.js b/src/router/modules/customer.js index c7e978eec..832a1e0fd 100644 --- a/src/router/modules/customer.js +++ b/src/router/modules/customer.js @@ -10,7 +10,7 @@ export default { component: RouterView, redirect: { name: 'CustomerMain' }, menus: { - main: ['CustomerList', 'CustomerPayments', 'CustomerCreate'], + main: ['CustomerList', 'CustomerPayments'], card: ['CustomerBasicData'], }, children: [ @@ -27,7 +27,7 @@ export default { title: 'list', icon: 'view_list', }, - component: () => import('src/pages/Customer/CustomerList.vue') + component: () => import('src/pages/Customer/CustomerList.vue'), }, { path: 'payments', @@ -36,17 +36,7 @@ export default { title: 'webPayments', icon: 'vn:onlinepayment', }, - component: () => import('src/pages/Customer/CustomerPayments.vue') - }, - { - path: 'create', - name: 'CustomerCreate', - meta: { - title: 'createCustomer', - icon: 'vn:addperson', - roles: ['developer'], - }, - component: () => import('src/pages/Customer/CustomerCreate.vue'), + component: () => import('src/pages/Customer/CustomerPayments.vue'), }, ], }, @@ -63,7 +53,8 @@ export default { title: 'summary', icon: 'launch', }, - component: () => import('src/pages/Customer/Card/CustomerSummary.vue'), + component: () => + import('src/pages/Customer/Card/CustomerSummary.vue'), }, { path: 'basic-data', @@ -72,7 +63,8 @@ export default { title: 'basicData', icon: 'vn:settings', }, - component: () => import('src/pages/Customer/Card/CustomerBasicData.vue'), + component: () => + import('src/pages/Customer/Card/CustomerBasicData.vue'), }, ], }, diff --git a/src/router/modules/wagon.js b/src/router/modules/wagon.js index 3fb808778..238e482dd 100644 --- a/src/router/modules/wagon.js +++ b/src/router/modules/wagon.js @@ -10,7 +10,7 @@ export default { component: RouterView, redirect: { name: 'WagonMain' }, menus: { - main: ['WagonList', 'WagonTypeList'], + main: ['WagonList', 'WagonTypeList', 'WagonCounter'], card: [], }, children: [ @@ -47,6 +47,15 @@ export default { }, component: () => import('src/pages/Wagon/WagonCreate.vue'), }, + { + path: 'counter', + name: 'WagonCounter', + meta: { + title: 'wagonCounter', + icon: 'add_circle', + }, + component: () => import('src/pages/Wagon/WagonCounter.vue'), + }, ], }, { diff --git a/src/router/modules/worker.js b/src/router/modules/worker.js index f79d7f06d..e5ee7c1a2 100644 --- a/src/router/modules/worker.js +++ b/src/router/modules/worker.js @@ -11,7 +11,7 @@ export default { redirect: { name: 'WorkerMain' }, menus: { main: ['WorkerList'], - // card: ['WorkerNotificationsManager'], + card: ['WorkerNotificationsManager'], }, children: [ { @@ -46,15 +46,16 @@ export default { }, component: () => import('src/pages/Worker/Card/WorkerSummary.vue'), }, - // { - // name: 'WorkerNotificationsManager', - // path: 'notifications', - // meta: { - // title: 'notifications', - // icon: 'notifications', - // }, - // component: () => import('src/pages/Worker/Card/WorkerNotificationsManager.vue'), - // }, + { + name: 'WorkerNotificationsManager', + path: 'notifications', + meta: { + title: 'notifications', + icon: 'notifications', + }, + component: () => + import('src/pages/Worker/Card/WorkerNotificationsManager.vue'), + }, ], }, ], diff --git a/src/stores/useArrayDataStore.js b/src/stores/useArrayDataStore.js index f9a32a6fa..223406a33 100644 --- a/src/stores/useArrayDataStore.js +++ b/src/stores/useArrayDataStore.js @@ -18,7 +18,8 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => { skip: 0, order: '', data: ref(), - isLoading: false + isLoading: false, + exprBuilder: null, }; } diff --git a/test/cypress/integration/claimAction.spec.js b/test/cypress/integration/claimAction.spec.js new file mode 100644 index 000000000..f181722fa --- /dev/null +++ b/test/cypress/integration/claimAction.spec.js @@ -0,0 +1,45 @@ +/// +describe('ClaimAction', () => { + const claimId = 2; + + const firstRow = 'tbody > :nth-child(1)'; + const destinationRow = '.q-item__section > .q-field'; + + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/claim/${claimId}/action`); + }); + + it('should import claim', () => { + cy.get('[title="Import claim"]').click(); + }); + + it('should change destination', () => { + const rowData = [true, null, null, 'Bueno']; + cy.fillRow(firstRow, rowData); + }); + + it('should change destination from other button', () => { + const rowData = [true]; + + cy.fillRow(firstRow, rowData); + cy.get('[title="Change destination"]').click(); + cy.selectOption(destinationRow, 'Confeccion'); + cy.get('.q-card > .q-card__actions > .q-btn--standard').click(); + }); + + it('should regularize', () => { + cy.get('[title="Regularize"]').click(); + cy.clickConfirm(); + }); + + it('should remove the line', () => { + cy.fillRow(firstRow, [true]); + cy.removeCard(); + cy.clickConfirm(); + + cy.reload(); + cy.get(firstRow).should('not.exist'); + }); +}); diff --git a/test/cypress/integration/ClaimNotes.spec.js b/test/cypress/integration/claimNotes.spec.js similarity index 90% rename from test/cypress/integration/ClaimNotes.spec.js rename to test/cypress/integration/claimNotes.spec.js index 5b52dd339..0a0f28fe7 100644 --- a/test/cypress/integration/ClaimNotes.spec.js +++ b/test/cypress/integration/claimNotes.spec.js @@ -7,7 +7,7 @@ describe('ClaimNotes', () => { it('should add a new note', () => { const message = 'This is a new message.'; - cy.get('.q-page-sticky button').click(); + cy.get('.q-page-sticky > div > button').click(); cy.get('.q-dialog .q-card__section:nth-child(2)').type(message); cy.get('.q-card__actions button:nth-child(2)').click(); cy.get('.q-card .q-card__section:nth-child(2)') diff --git a/test/cypress/integration/workerNotificationsManager.spec.js b/test/cypress/integration/workerNotificationsManager.spec.js index 4cd54629a..175933277 100644 --- a/test/cypress/integration/workerNotificationsManager.spec.js +++ b/test/cypress/integration/workerNotificationsManager.spec.js @@ -1,36 +1,79 @@ -xdescribe('WorkerNotificationsManager', () => { +describe('WorkerNotificationsManager', () => { + const salesPersonId = 18; + const developerId = 9; + + const activeList = ':nth-child(1) > .q-list'; + const availableList = ':nth-child(2) > .q-list'; + const firstActiveNotification = + ':nth-child(1) > .q-list > :nth-child(1) > .q-item > .q-toggle > .q-toggle__inner'; + const firstAvailableNotification = + ':nth-child(2) > .q-list > :nth-child(1) > .q-item > .q-toggle > .q-toggle__inner'; + beforeEach(() => { - const workerId = 1110; cy.viewport(1280, 720); - cy.login('salesBoss'); - cy.visit(`/#/worker/${workerId}/notifications`); }); - it('should unsubscribe 2 notifications, check the unsubscription has been saved, subscribe to other one and should check the data has been saved', () => { - cy.get('.q-chip').should('have.length', 3); - cy.get('.q-toggle__thumb').eq(0).click(); - cy.get('.q-notification__message').should( - 'have.text', - 'Unsubscribed from the notification' + it('should throw an error if you try to change a notification that is not yours', () => { + cy.login('developer'); + cy.visit(`/#/worker/${salesPersonId}/notifications`); + cy.get(firstAvailableNotification).click(); + cy.notificationHas( + '.q-notification__message', + 'The notification subscription of this worker cant be modified' ); - cy.get('.q-chip > .q-icon').eq(0).click(); + }); - cy.reload(); + it('should active a notification that is yours', () => { + cy.login('developer'); + cy.visit(`/#/worker/${developerId}/notifications`); + cy.waitForElement(activeList); + cy.waitForElement(availableList); - cy.get('.q-chip').should('have.length', 1); - cy.get('.q-toggle__thumb').should('have.length', 3).eq(0).click(); - cy.get('.q-notification__message').should( - 'have.text', - 'Subscribed to the notification' - ); - cy.get('.q-toggle__thumb').should('have.length', 3).eq(1).click(); - cy.get('.q-notification__message').should( - 'have.text', - 'Subscribed to the notification' - ); + cy.get(activeList) + .children() + .its('length') + .then((beforeSize) => { + cy.get(firstAvailableNotification).click(); + cy.get(activeList) + .children() + .should('have.length', beforeSize + 1); + }); + }); - cy.reload(); + it('should deactivate a notification that is yours', () => { + cy.login('developer'); + cy.visit(`/#/worker/${developerId}/notifications`); + cy.waitForElement(activeList); + cy.waitForElement(availableList); - cy.get('.q-chip').should('have.length', 3); + cy.get(availableList) + .children() + .its('length') + .then((beforeSize) => { + cy.get(firstActiveNotification).click(); + cy.get(availableList) + .children() + .should('have.length', beforeSize + 1); + }); + }); + + it('should active a notification if you are their boss', () => { + cy.login('salesBoss'); + cy.visit(`/#/worker/${salesPersonId}/notifications`); + cy.waitForElement(activeList); + cy.waitForElement(availableList); + + cy.get(activeList) + .children() + .its('length') + .then((beforeSize) => { + cy.get(firstAvailableNotification).click(); + cy.get(activeList) + .children() + .should('have.length', beforeSize + 1); + + //Rollback + cy.get(firstActiveNotification).click(); + }); }); }); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 04282f5f8..e57826c88 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -88,7 +88,12 @@ Cypress.Commands.add('addCard', () => { cy.get('.q-page-sticky > div > .q-btn').click(); }); Cypress.Commands.add('clickConfirm', () => { - cy.get('.q-btn--unelevated > .q-btn__content > .block').click(); + cy.waitForElement('.q-dialog__inner > .q-card'); + cy.get('.q-card__actions > .q-btn--unelevated > .q-btn__content > .block').click(); +}); + +Cypress.Commands.add('notificationHas', (selector, text) => { + cy.get(selector).should('have.text', text); }); Cypress.Commands.add('fillRow', (rowSelector, data) => { diff --git a/test/vitest/__tests__/pages/Worker/WorkerNotificationsManager.spec.js b/test/vitest/__tests__/pages/Worker/WorkerNotificationsManager.spec.js index 744346078..35ce91e61 100644 --- a/test/vitest/__tests__/pages/Worker/WorkerNotificationsManager.spec.js +++ b/test/vitest/__tests__/pages/Worker/WorkerNotificationsManager.spec.js @@ -1,15 +1,15 @@ import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; -import { createWrapper, axios } from 'app/test/vitest/helper'; +import { createWrapper } from 'app/test/vitest/helper'; import WorkerNotificationsManager from 'src/pages/Worker/Card/WorkerNotificationsManager.vue'; +import { ref } from 'vue'; describe('WorkerNotificationsManager', () => { let vm; - const entityId = 1110; beforeAll(() => { vm = createWrapper(WorkerNotificationsManager, { - propsData: { - id: entityId, + global: { + stubs: ['CrudModel'], }, }).vm; }); @@ -18,83 +18,16 @@ describe('WorkerNotificationsManager', () => { vi.clearAllMocks(); }); - describe('fetch()', () => { - it('should fetch notification subscriptions and role mappings', async () => { - vi.spyOn(axios, 'get') - .mockResolvedValueOnce({ - data: [ - { - id: 1, - name: 'Name 1', - description: 'Description 1', - notificationFk: 1, - active: true - }, - ], - }); - await vm.fetch(); + describe('swapEntry()', () => { + it('should swap notification', async () => { + const from = ref(new Map()); + const to = ref(new Map()); + from.value.set(1, { notificationFk: 1 }); + to.value.set(2, { notificationFk: 2 }); - expect(axios.get).toHaveBeenCalledWith(`NotificationSubscriptions/${entityId}/getList`); - expect(vm.notifications).toEqual([ - { - id: 1, - notificationFk: 1, - name: 'Name 1', - description: 'Description 1', - active: true, - }, - ]); - }); - }); - - describe('disableNotification()', () => { - it('should disable the notification', async () => { - vi.spyOn(axios, 'delete').mockResolvedValue({ data: { count: 1 } }); - vi.spyOn(vm.quasar, 'notify'); - const subscriptionId = 1; - vm.notifications = [{ id: 1, active: true }]; - - await vm.disableNotification(vm.notifications[0]); - - expect(axios.delete).toHaveBeenCalledWith( - `NotificationSubscriptions/${subscriptionId}` - ); - expect(vm.notifications[0].id).toBeNull(); - expect(vm.notifications[0].id).toBeFalsy(); - expect(vm.quasar.notify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'positive' }) - ); - }); - }); - - describe('toggleNotification()', () => { - it('should activate the notification', async () => { - vi.spyOn(axios, 'post').mockResolvedValue({ - data: { id: 1, notificationFk: 1 }, - }); - vm.notifications = [{ id: null, active: true, notificationFk: 1 }]; - - await vm.toggleNotification(vm.notifications[0]); - - expect(axios.post).toHaveBeenCalledWith('NotificationSubscriptions', { - notificationFk: 1, - userFk: entityId, - }); - expect(vm.notifications[0].id).toBe(1); - expect(vm.notifications[0].active).toBeTruthy(); - expect(vm.quasar.notify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'positive' }) - ); - }); - - it('should disable the notification', async () => { - vi.spyOn(vm, 'disableNotification'); - vm.notifications = [{ id: 1, active: false, notificationFk: 1 }]; - - await vm.toggleNotification(vm.notifications[0]); - - expect(vm.notifications[0].id).toBe(null); - expect(vm.notifications[0].active).toBeFalsy(); + await vm.swapEntry(from.value, to.value, 1); + expect(to.value.size).toBe(2); + expect(from.value.size).toBe(0); }); }); });
+ {{ t('claim.summary.actions') }} +
{{ props.title }}