diff --git a/Jenkinsfile b/Jenkinsfile index 0027e1cbe..7d2957a1c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -75,16 +75,12 @@ pipeline { steps { sh 'pnpm run test:unit:ci' } - post { + post { always { - script { - try { - junit 'junitresults.xml' - junit 'junit.xml' - } catch (e) { - echo e.toString() - } - } + junit( + testResults: 'junitresults.xml', + allowEmptyResults: true + ) } } } diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index 9bb05d439..c8fa5809c 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -176,8 +176,8 @@ async function remove(data) { .dialog({ component: VnConfirm, componentProps: { - title: t('confirmDeletion'), - message: t('confirmDeletionMessage'), + title: t('globals.confirmDeletion'), + message: t('globals.confirmDeletionMessage'), newData, ids, }, @@ -317,16 +317,3 @@ watch(formUrl, async () => { color="primary" /> </template> - -<i18n> - { - "en": { - "confirmDeletion": "Confirm deletion", - "confirmDeletionMessage": "Are you sure you want to delete this?" - }, - "es": { - "confirmDeletion": "Confirmar eliminación", - "confirmDeletionMessage": "Seguro que quieres eliminar?" - } - } -</i18n> diff --git a/src/components/EditPictureForm.vue b/src/components/EditPictureForm.vue index 9f69896b5..3d7f3615b 100644 --- a/src/components/EditPictureForm.vue +++ b/src/components/EditPictureForm.vue @@ -272,7 +272,7 @@ const makeRequest = async () => { class="cursor-pointer q-mr-sm" @click="openInputFile()" > - <!-- <QTooltip>{{ t('Select a file') }}</QTooltip> --> + <!-- <QTooltip>{{ t('globals.selectFile') }}</QTooltip> --> </QIcon> <QIcon name="info" class="cursor-pointer"> <QTooltip>{{ diff --git a/src/components/FetchData.vue b/src/components/FetchData.vue index 4f5d7a57d..5b3dcbea7 100644 --- a/src/components/FetchData.vue +++ b/src/components/FetchData.vue @@ -59,11 +59,4 @@ async function fetch(fetchFilter = {}) { // } } - -const render = () => { - return h('div', []); -}; </script> -<template> - <render /> -</template> diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 9fd16088c..912ce8ea7 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -59,6 +59,10 @@ const $props = defineProps({ type: Function, default: null, }, + saveFn: { + type: Function, + default: null, + }, }); const emit = defineEmits(['onFetch', 'onDataSaved']); @@ -75,9 +79,8 @@ onMounted(async () => { }); // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla - if ($props.formInitialData && !$props.autoLoad) { - state.set($props.model, $props.formInitialData); - } else { + state.set($props.model, $props.formInitialData ?? {}); + if ($props.autoLoad && !$props.formInitialData) { await fetch(); } @@ -138,17 +141,20 @@ async function save() { try { const body = $props.mapper ? $props.mapper(formData.value) : formData.value; let response; - if ($props.urlCreate) { - response = await axios.post($props.urlCreate, body); - notify('globals.dataCreated', 'positive'); - } else { - response = await axios.patch($props.urlUpdate || $props.url, body); - } + if ($props.saveFn) response = await $props.saveFn(body); + else + response = await axios[$props.urlCreate ? 'post' : 'patch']( + $props.urlCreate || $props.urlUpdate || $props.url, + body + ); + if ($props.urlCreate) notify('globals.dataCreated', 'positive'); + emit('onDataSaved', formData.value, response?.data); originalData.value = JSON.parse(JSON.stringify(formData.value)); hasChanges.value = false; } catch (err) { - notify('errors.create', 'negative'); + console.error(err); + notify('errors.writeRequest', 'negative'); } isLoading.value = false; } diff --git a/src/components/common/VnDms.vue b/src/components/common/VnDms.vue new file mode 100644 index 000000000..d2651f5d8 --- /dev/null +++ b/src/components/common/VnDms.vue @@ -0,0 +1,201 @@ +<script setup> +import { ref, onMounted } from 'vue'; +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; +import axios from 'axios'; + +import FetchData from 'components/FetchData.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import VnSelectFilter from 'src/components/common/VnSelectFilter.vue'; +import VnInput from 'src/components/common/VnInput.vue'; +import FormModelPopup from 'components/FormModelPopup.vue'; + +const route = useRoute(); +const { t } = useI18n(); +const emit = defineEmits(['onDataSaved']); + +const $props = defineProps({ + model: { + type: String, + required: true, + }, + defaultDmsCode: { + type: String, + default: null, + }, + formInitialData: { + type: Object, + default: null, + }, +}); + +const warehouses = ref(); +const companies = ref(); +const dmsTypes = ref(); +const allowedContentTypes = ref(); +const inputFileRef = ref(); +const dms = ref({}); + +onMounted(() => { + defaultData(); + if (!$props.formInitialData) + dms.value.description = t($props.model + 'Description', dms.value); +}); +function onFileChange(files) { + dms.value.hasFileAttached = !!files; + dms.value.file = files?.name; +} + +function mapperDms(data) { + const formData = new FormData(); + const { files } = data; + if (files) formData.append(files?.name, files); + delete data.files; + + const dms = { + hasFile: !!data.hasFile, + hasFileAttached: data.hasFileAttached, + reference: data.reference, + warehouseId: data.warehouseFk, + companyId: data.companyFk, + dmsTypeId: data.dmsTypeFk, + description: data.description, + }; + return [formData, { params: dms }]; +} + +function getUrl() { + if ($props.formInitialData) return 'dms/' + $props.formInitialData.id + '/updateFile'; + return `${$props.model}/${route.params.id}/uploadFile`; +} + +async function save() { + const body = mapperDms(dms.value); + await axios.post(getUrl(), body[0], body[1]); + emit('onDataSaved', body[1].params); +} + +function defaultData() { + if ($props.formInitialData) return (dms.value = $props.formInitialData); + return addDefaultData({ + reference: route.params.id, + }); +} + +function setDmsTypes(data) { + dmsTypes.value = data; + if (!$props.formInitialData && $props.defaultDmsCode) { + const { id } = data.find((dmsType) => dmsType.code == $props.defaultDmsCode); + addDefaultData({ dmsTypeFk: id }); + } +} + +function addDefaultData(data) { + Object.assign(dms.value, data); +} +</script> +<template> + <FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load /> + <FetchData url="Companies" @on-fetch="(data) => (companies = data)" auto-load /> + <FetchData url="DmsTypes" @on-fetch="setDmsTypes" auto-load /> + <FetchData + url="DmsContainers/allowedContentTypes" + @on-fetch="(data) => (allowedContentTypes = data.join(','))" + auto-load + /> + <FetchData + url="UserConfigs/getUserConfig" + @on-fetch="addDefaultData" + :auto-load="!$props.formInitialData" + /> + <FormModelPopup + :title="formInitialData ? t('globals.edit') : t('globals.create')" + model="dms" + :form-initial-data="formInitialData" + :save-fn="save" + > + <template #form-inputs> + <div class="q-gutter-y-ms"> + <VnRow> + <VnInput :label="t('globals.reference')" v-model="dms.reference" /> + <VnSelectFilter + :label="t('globals.company')" + v-model="dms.companyFk" + :options="companies" + option-value="id" + option-label="code" + input-debounce="0" + /> + </VnRow> + <VnRow> + <VnSelectFilter + :label="t('globals.warehouse')" + v-model="dms.warehouseFk" + :options="warehouses" + option-value="id" + option-label="name" + input-debounce="0" + /> + <VnSelectFilter + :label="t('globals.type')" + v-model="dms.dmsTypeFk" + :options="dmsTypes" + option-value="id" + option-label="name" + input-debounce="0" + /> + </VnRow> + <QInput + :label="t('globals.description')" + v-model="dms.description" + type="textarea" + /> + <QFile + ref="inputFileRef" + :label="t('entry.buys.file')" + v-model="dms.files" + :multiple="false" + :accept="allowedContentTypes" + @update:model-value="onFileChange(dms.files)" + class="required" + :display-value="dms.file" + > + <template #append> + <QIcon + name="vn:attach" + class="cursor-pointer" + @click="inputFileRef.pickFiles()" + > + <QTooltip>{{ t('globals.selectFile') }}</QTooltip> + </QIcon> + <QIcon name="info" class="cursor-pointer"> + <QTooltip>{{ + t('contentTypesInfo', { allowedContentTypes }) + }}</QTooltip> + </QIcon> + </template> + </QFile> + <QCheckbox + v-model="dms.hasFile" + :label="t('Generate identifier for original file')" + /> + </div> + </template> + </FormModelPopup> +</template> +<style scoped> +.q-gutter-y-ms { + display: grid; + row-gap: 20px; +} +</style> +<i18n> +en: + contentTypesInfo: Allowed file types {allowedContentTypes} + EntryDmsDescription: Reference {reference} +es: + Generate identifier for original file: Generar identificador para archivo original + contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes} + EntryDmsDescription: Referencia {reference} + +</i18n> diff --git a/src/components/common/VnDmsList.vue b/src/components/common/VnDmsList.vue new file mode 100644 index 000000000..5057c0790 --- /dev/null +++ b/src/components/common/VnDmsList.vue @@ -0,0 +1,316 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; +import { useQuasar, QCheckbox, QBtn, QInput } from 'quasar'; +import axios from 'axios'; + +import FetchData from 'components/FetchData.vue'; +import VnDms from 'src/components/common/VnDms.vue'; +import VnConfirm from 'components/ui/VnConfirm.vue'; +import { downloadFile } from 'src/composables/downloadFile'; + +const route = useRoute(); +const quasar = useQuasar(); +const { t } = useI18n(); +const rows = ref(); +const dmsRef = ref(); +const formDialog = ref({}); + +const $props = defineProps({ + model: { + type: String, + required: true, + }, + updateModel: { + type: String, + default: null, + }, + defaultDmsCode: { + type: String, + required: true, + }, + filter: { + type: String, + required: true, + }, +}); + +const dmsFilter = { + include: { + relation: 'dms', + scope: { + fields: [ + 'dmsTypeFk', + 'reference', + 'hardCopyNumber', + 'workerFk', + 'description', + 'hasFile', + 'file', + 'created', + 'companyFk', + 'warehouseFk', + ], + include: [ + { + relation: 'dmsType', + scope: { + fields: ['name'], + }, + }, + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { + fields: ['name'], + }, + }, + }, + }, + ], + }, + }, + order: ['dmsFk DESC'], +}; + +const columns = computed(() => [ + { + align: 'left', + field: 'id', + label: t('globals.id'), + name: 'id', + component: 'span', + }, + { + align: 'left', + field: 'type', + label: t('globals.type'), + name: 'type', + component: QInput, + props: (prop) => ({ + readonly: true, + borderless: true, + 'model-value': prop.row.dmsType.name, + }), + }, + { + align: 'left', + field: 'order', + label: t('globals.order'), + name: 'order', + component: 'span', + }, + { + align: 'left', + field: 'reference', + label: t('globals.reference'), + name: 'reference', + component: 'span', + }, + { + align: 'left', + field: 'description', + label: t('globals.description'), + name: 'description', + component: 'span', + }, + { + align: 'left', + field: 'hasFile', + label: t('globals.original'), + name: 'hasFile', + component: QCheckbox, + props: (prop) => ({ + disable: true, + 'model-value': Boolean(prop.value), + }), + }, + { + align: 'left', + field: 'file', + label: t('globals.file'), + name: 'file', + component: 'span', + }, + { + field: 'options', + name: 'options', + components: [ + { + component: QBtn, + props: () => ({ + icon: 'cloud_download', + flat: true, + color: 'primary', + }), + click: (prop) => downloadFile(prop.row.id), + }, + { + component: QBtn, + props: () => ({ + icon: 'edit', + flat: true, + color: 'primary', + }), + click: (prop) => showFormDialog(prop.row), + }, + { + component: QBtn, + props: () => ({ + icon: 'delete', + flat: true, + color: 'primary', + }), + click: (prop) => deleteDms(prop.row.id), + }, + ], + }, +]); + +function setData(data) { + const newData = data.map((value) => value.dms); + rows.value = newData; +} + +function deleteDms(dmsFk) { + quasar + .dialog({ + component: VnConfirm, + componentProps: { + title: t('globals.confirmDeletion'), + message: t('globals.confirmDeletionMessage'), + }, + }) + .onOk(async () => { + await axios.post(`${$props.model}/${dmsFk}/removeFile`); + const index = rows.value.findIndex((row) => row.id == dmsFk); + rows.value.splice(index, 1); + }); +} + +function showFormDialog(dms) { + if (dms) dms = parseDms(dms); + formDialog.value = { + show: true, + dms, + }; +} + +function parseDms(data) { + for (let prop in data) { + if (prop.endsWith('Fk')) data[prop.replace('Fk', 'Id')] = data[prop]; + } + return data; +} +</script> +<template> + <FetchData + ref="dmsRef" + :url="$props.model" + :filter="dmsFilter" + :where="{ [$props.filter]: route.params.id }" + @on-fetch="setData" + auto-load + /> + <QTable + :columns="columns" + :pagination="{ rowsPerPage: 0 }" + :rows="rows" + class="full-width q-mt-md" + hide-bottom + row-key="clientFk" + :grid="$q.screen.lt.sm" + > + <template #body-cell="props"> + <QTd :props="props"> + <QTr :props="props"> + <component + v-if="props.col.component" + :is="props.col.component" + v-bind="props.col.props && props.col.props(props)" + > + <span + v-if="props.col.component == 'span'" + style="white-space: wrap" + >{{ props.value }}</span + > + </component> + </QTr> + + <div class="flex justify-center" v-if="props.col.name == 'options'"> + <div v-for="button of props.col.components" :key="button.id"> + <component + :is="button.component" + v-bind="button.props(props)" + @click="button.click(props)" + /> + </div> + </div> + </QTd> + </template> + <template #item="props"> + <div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition"> + <QCard + bordered + flat + @keyup.ctrl.enter.stop="claimDevelopmentForm?.saveChanges()" + > + <QSeparator /> + <QList dense> + <QItem v-for="col in props.cols" :key="col.name"> + <div v-if="col.name != 'options'" class="row"> + <span class="labelColor">{{ col.label }}:</span> + <span>{{ col.value }}</span> + </div> + <div v-if="col.name == 'options'" class="row"> + <div + v-for="button of col.components" + :key="button.id" + class="row" + > + <component + :is="button.component" + v-bind="button.props(col)" + @click="button.click(col)" + /> + </div> + </div> + </QItem> + </QList> + </QCard> + </div> + </template> + </QTable> + <QDialog v-model="formDialog.show"> + <VnDms + :model="updateModel ?? model" + :default-dms-code="defaultDmsCode" + :form-initial-data="formDialog.dms" + @on-data-saved="dmsRef.fetch()" + :description="$props.description" + /> + </QDialog> + <QPageSticky position="bottom-right" :offset="[25, 25]"> + <QBtn fab color="primary" icon="add" @click="showFormDialog()" /> + </QPageSticky> +</template> +<style scoped> +.q-gutter-y-ms { + display: grid; + row-gap: 20px; +} +.labelColor { + color: var(--vn-label); +} +</style> +<i18n> +en: + contentTypesInfo: Allowed file types {allowedContentTypes} +es: + contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes} + Generate identifier for original file: Generar identificador para archivo original +</i18n> diff --git a/src/components/ui/VnLv.vue b/src/components/ui/VnLv.vue index 0e4a055eb..72c05ae6a 100644 --- a/src/components/ui/VnLv.vue +++ b/src/components/ui/VnLv.vue @@ -1,8 +1,9 @@ <script setup> import { computed } from 'vue'; import { dashIfEmpty } from 'src/filters'; - +import { useI18n } from 'vue-i18n'; import { useClipboard } from 'src/composables/useClipboard'; + const $props = defineProps({ label: { type: String, default: null }, value: { @@ -13,8 +14,9 @@ const $props = defineProps({ dash: { type: Boolean, default: true }, copy: { type: Boolean, default: false }, }); -const isBooleanValue = computed(() => typeof $props.value === 'boolean'); +const { t } = useI18n(); +const isBooleanValue = computed(() => typeof $props.value === 'boolean'); const { copyText } = useClipboard(); function copyValueText() { @@ -54,22 +56,29 @@ function copyValueText() { </slot> </div> <div class="info" v-if="$props.info"> - <QIcon name="info"> + <QIcon name="info" class="cursor-pointer" size="xs" color="grey"> <QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]"> {{ $props.info }} </QTooltip> </QIcon> </div> <div class="copy" v-if="$props.copy && $props.value" @click="copyValueText()"> - <QIcon name="Content_Copy" color="primary" /> + <QIcon name="Content_Copy" color="primary"> + <QTooltip>{{ t('globals.copyClipboard') }}</QTooltip> + </QIcon> </div> </div> </template> <style lang="scss" scoped> +.vn-label-value:hover .copy { + visibility: visible; + cursor: pointer; +} .copy { - &:hover { - cursor: pointer; - } + visibility: hidden; +} +.info { + margin-left: 5px; } </style> diff --git a/src/components/ui/VnRow.vue b/src/components/ui/VnRow.vue index c3c951528..e13730e62 100644 --- a/src/components/ui/VnRow.vue +++ b/src/components/ui/VnRow.vue @@ -1,12 +1,16 @@ <template> - <div id="row"> + <div id="row" class="q-gutter-md"> <slot></slot> </div> </template> <style lang="scss" scopped> +#row { + display: grid; + grid-template-columns: 1fr 1fr; +} @media screen and (max-width: 800px) { #row { - flex-direction: column; + grid-template-columns: 1fr; } } </style> diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js index 9e0ad7c9b..0cc91c88d 100644 --- a/src/i18n/en/index.js +++ b/src/i18n/en/index.js @@ -24,6 +24,7 @@ export default { dataCreated: 'Data created', add: 'Add', create: 'Create', + edit: 'Edit', save: 'Save', remove: 'Remove', reset: 'Reset', @@ -64,12 +65,23 @@ export default { markAll: 'Mark all', requiredField: 'Required field', class: 'clase', - type: 'type', + type: 'Type', reason: 'reason', noResults: 'No results', system: 'System', + warehouse: 'Warehouse', + company: 'Company', fieldRequired: 'Field required', allowedFilesText: 'Allowed file types: { allowedContentTypes }', + confirmDeletion: 'Confirm deletion', + confirmDeletionMessage: 'Are you sure you want to delete this?', + description: 'Description', + id: 'Id', + order: 'Order', + original: 'Original', + file: 'File', + selectFile: 'Select a file', + copyClipboard: 'Copy on clipboard', }, errors: { statusUnauthorized: 'Access denied', @@ -77,7 +89,7 @@ export default { statusBadGateway: 'It seems that the server has fall down', statusGatewayTimeout: 'Could not contact the server', userConfig: 'Error fetching user config', - create: 'Error during creation', + writeRequest: 'The requested operation could not be completed', }, login: { title: 'Login', @@ -166,7 +178,7 @@ export default { fiscalAddress: 'Fiscal address', fiscalData: 'Fiscal data', billingData: 'Billing data', - consignee: 'Consignee', + consignee: 'Default consignee', businessData: 'Business data', financialData: 'Financial data', customerId: 'Customer ID', @@ -219,6 +231,8 @@ export default { recoverySince: 'Recovery since', businessType: 'Business Type', city: 'City', + rating: 'Rating', + recommendCredit: 'Recommended credit', }, basicData: { socialName: 'Fiscal name', @@ -273,6 +287,7 @@ export default { basicData: 'Basic data', buys: 'Buys', notes: 'Notes', + dms: 'File management', log: 'Log', create: 'Create', latestBuys: 'Latest buys', @@ -344,7 +359,6 @@ export default { reference: 'Reference', observations: 'Observations', item: 'Item', - description: 'Description', size: 'Size', packing: 'Packing', grouping: 'Grouping', @@ -359,7 +373,6 @@ export default { }, notes: { observationType: 'Observation type', - description: 'Description', }, descriptor: { agency: 'Agency', @@ -372,7 +385,6 @@ export default { packing: 'Packing', grouping: 'Grouping', quantity: 'Quantity', - description: 'Description', size: 'Size', tags: 'Tags', type: 'Type', @@ -458,7 +470,6 @@ export default { visible: 'Visible', available: 'Available', quantity: 'Quantity', - description: 'Description', price: 'Price', discount: 'Discount', packing: 'Packing', @@ -531,7 +542,6 @@ export default { landed: 'Landed', quantity: 'Quantity', claimed: 'Claimed', - description: 'Description', price: 'Price', discount: 'Discount', total: 'Total', @@ -793,7 +803,6 @@ export default { orderTicketList: 'Order Ticket List', details: 'Details', item: 'Item', - description: 'Description', quantity: 'Quantity', price: 'Price', amount: 'Amount', @@ -1138,7 +1147,6 @@ export default { warehouse: 'Warehouse', travelFileDescription: 'Travel id { travelId }', file: 'File', - description: 'Description', }, }, item: { @@ -1172,7 +1180,6 @@ export default { clone: 'Clone', openCard: 'View', openSummary: 'Summary', - viewDescription: 'Description', }, cardDescriptor: { mainList: 'Main list', diff --git a/src/i18n/es/index.js b/src/i18n/es/index.js index 6083dfad7..7369721e6 100644 --- a/src/i18n/es/index.js +++ b/src/i18n/es/index.js @@ -24,6 +24,7 @@ export default { dataCreated: 'Datos creados', add: 'Añadir', create: 'Crear', + edit: 'Modificar', save: 'Guardar', remove: 'Eliminar', reset: 'Restaurar', @@ -64,12 +65,23 @@ export default { markAll: 'Marcar todo', requiredField: 'Campo obligatorio', class: 'clase', - type: 'tipo', + type: 'Tipo', reason: 'motivo', noResults: 'Sin resultados', system: 'Sistema', + warehouse: 'Almacén', + company: 'Empresa', fieldRequired: 'Campo requerido', allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }', + confirmDeletion: 'Confirmar eliminación', + confirmDeletionMessage: '¿Seguro que quieres eliminar?', + description: 'Descripción', + id: 'Id', + order: 'Orden', + original: 'Original', + file: 'Fichero', + selectFile: 'Seleccione un fichero', + copyClipboard: 'Copiar en portapapeles', }, errors: { statusUnauthorized: 'Acceso denegado', @@ -77,7 +89,7 @@ export default { statusBadGateway: 'Parece ser que el servidor ha caído', statusGatewayTimeout: 'No se ha podido contactar con el servidor', userConfig: 'Error al obtener configuración de usuario', - create: 'Error al crear', + writeRequest: 'No se pudo completar la operación solicitada', }, login: { title: 'Inicio de sesión', @@ -165,7 +177,7 @@ export default { fiscalAddress: 'Dirección fiscal', fiscalData: 'Datos fiscales', billingData: 'Datos de facturación', - consignee: 'Consignatario', + consignee: 'Consignatario pred.', businessData: 'Datos comerciales', financialData: 'Datos financieros', customerId: 'ID cliente', @@ -218,6 +230,8 @@ export default { recoverySince: 'Recobro desde', businessType: 'Tipo de negocio', city: 'Población', + rating: 'Clasificación', + recommendCredit: 'Crédito recomendado', }, basicData: { socialName: 'Nombre fiscal', @@ -272,6 +286,7 @@ export default { basicData: 'Datos básicos', buys: 'Compras', notes: 'Notas', + dms: 'Gestión documental', log: 'Historial', create: 'Crear', latestBuys: 'Últimas compras', @@ -343,7 +358,6 @@ export default { reference: 'Referencia', observations: 'Observaciónes', item: 'Artículo', - description: 'Descripción', size: 'Medida', packing: 'Packing', grouping: 'Grouping', @@ -358,7 +372,6 @@ export default { }, notes: { observationType: 'Tipo de observación', - description: 'Descripción', }, descriptor: { agency: 'Agencia', @@ -371,7 +384,6 @@ export default { packing: 'Packing', grouping: 'Grouping', quantity: 'Cantidad', - description: 'Descripción', size: 'Medida', tags: 'Etiquetas', type: 'Tipo', @@ -457,7 +469,6 @@ export default { visible: 'Visible', available: 'Disponible', quantity: 'Cantidad', - description: 'Descripción', price: 'Precio', discount: 'Descuento', packing: 'Encajado', @@ -530,7 +541,6 @@ export default { landed: 'Entregado', quantity: 'Cantidad', claimed: 'Reclamado', - description: 'Descripción', price: 'Precio', discount: 'Descuento', total: 'Total', @@ -701,7 +711,6 @@ export default { orderTicketList: 'Tickets del pedido', details: 'Detalles', item: 'Item', - description: 'Descripción', quantity: 'Cantidad', price: 'Precio', amount: 'Monto', @@ -1138,7 +1147,6 @@ export default { warehouse: 'Almacén', travelFileDescription: 'Id envío { travelId }', file: 'Fichero', - description: 'Descripción', }, }, item: { @@ -1172,7 +1180,6 @@ export default { clone: 'Clonar', openCard: 'Ficha', openSummary: 'Detalles', - viewDescription: 'Descripción', }, cardDescriptor: { mainList: 'Listado principal', diff --git a/src/pages/Claim/ClaimList.vue b/src/pages/Claim/ClaimList.vue index 322055b13..fd0d2561f 100644 --- a/src/pages/Claim/ClaimList.vue +++ b/src/pages/Claim/ClaimList.vue @@ -116,7 +116,13 @@ function navigate(event, id) { </template> <template #actions> <QBtn - :label="t('components.smartCard.viewDescription')" + :label="t('components.smartCard.openCard')" + @click.stop="navigate(row.id)" + class="bg-vn-dark" + outline + /> + <QBtn + :label="t('globals.description')" @click.stop class="bg-vn-dark" outline diff --git a/src/pages/Customer/Card/CustomerSummary.vue b/src/pages/Customer/Card/CustomerSummary.vue index c46b8a8de..5591fd15c 100644 --- a/src/pages/Customer/Card/CustomerSummary.vue +++ b/src/pages/Customer/Card/CustomerSummary.vue @@ -122,7 +122,7 @@ const creditWarning = computed(() => { </QCard> <QCard class="vn-one"> <a class="header link" :href="clientUrl + `fiscal-data`" link> - {{ t('customer.summary.fiscalAddress') }} + {{ t('customer.summary.fiscalData') }} <QIcon name="open_in_new" color="primary" /> </a> <VnLv @@ -235,7 +235,8 @@ const creditWarning = computed(() => { link > {{ t('customer.summary.financialData') }} - <QIcon name="vn:grafana" color="primary" /> + <QIcon name="open_in_new" color="primary" /> + <!-- Pendiente de añadir el icono <QIcon name="vn:grafana" color="primary" /> --> </a> <VnLv :label="t('customer.summary.risk')" @@ -276,7 +277,30 @@ const creditWarning = computed(() => { :label="t('customer.summary.recoverySince')" :value="toDate(entity.recovery.started)" /> + <VnLv + :label="t('customer.summary.rating')" + :value="entity.rating" + :info="t('valueInfo', { min: 1, max: 20 })" + /> + + <VnLv + :label="t('customer.summary.recommendCredit')" + :value="entity.recommendedCredit" + /> </QCard> </template> </CardSummary> </template> +<style lang="scss" scoped> +@media (min-width: $breakpoint-md) { + .summary .vn-one { + min-width: 300px; + } +} +</style> +<i18n> +en: + valueInfo: Value from {min} to {max}. The higher the better value +es: + valueInfo: Valor de {min} a {max}. Cuanto más alto, mejor valor +</i18n> diff --git a/src/pages/Customer/ExtendedList/CustomerExtendedList.vue b/src/pages/Customer/ExtendedList/CustomerExtendedList.vue index 69effe88e..9d98f479c 100644 --- a/src/pages/Customer/ExtendedList/CustomerExtendedList.vue +++ b/src/pages/Customer/ExtendedList/CustomerExtendedList.vue @@ -13,7 +13,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import { useArrayData } from 'composables/useArrayData'; import { useStateStore } from 'stores/useStateStore'; -import { dashIfEmpty, toDate } from 'src/filters'; +import { toDate } from 'src/filters'; const { t } = useI18n(); const router = useRouter(); @@ -477,17 +477,11 @@ const stopEventPropagation = (event, col) => { event.stopPropagation(); }; -const navigateToTravelId = (id) => { - router.push({ path: `/customer/${id}` }); -}; +const navigateToTravelId = (id) => router.push({ path: `/customer/${id}` }); -const selectCustomerId = (id) => { - selectedCustomerId.value = id; -}; +const selectCustomerId = (id) => (selectedCustomerId.value = id); -const selectSalesPersonId = (id) => { - selectedSalesPersonId.value = id; -}; +const selectSalesPersonId = (id) => (selectedSalesPersonId.value = id); </script> <template> @@ -521,37 +515,50 @@ const selectSalesPersonId = (id) => { class="full-width q-mt-md" row-key="id" :visible-columns="visibleColumns" + @row-click="(evt, row, id) => navigateToTravelId(row.id)" > - <template #body="props"> - <QTr - :props="props" - @click="navigateToTravelId(props.row.id)" - class="cursor-pointer" - > - <QTd - v-for="col in props.cols" - :key="col.name" - :props="props" - @click="stopEventPropagation($event, col)" + <template #body-cell="{ col, value }"> + <QTd @click="stopEventPropagation($event, col)"> + {{ value }} + </QTd> + </template> + <template #body-cell-id="props"> + <QTd @click="stopEventPropagation($event, props.col)"> + <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)" > - <component - :is="tableColumnComponents[col.name].component" - class="col-content" - v-bind="tableColumnComponents[col.name].props(props)" - @click="tableColumnComponents[col.name].event(props)" - > - {{ dashIfEmpty(col.value) }} - <WorkerDescriptorProxy - v-if="props.row.salesPersonFk" - :id="selectedSalesPersonId" - /> - <CustomerDescriptorProxy - v-if="props.row.id" - :id="selectedCustomerId" - /> - </component> - </QTd> - </QTr> + <CustomerDescriptorProxy :id="props.row.id" /> + {{ props.row.id }} + </component> + </QTd> + </template> + <template #body-cell-salesPersonFk="props"> + <QTd @click="stopEventPropagation($event, props.col)"> + <component + v-if="props.row.salesPerson" + class="col-content" + :is="tableColumnComponents[props.col.name].component" + v-bind="tableColumnComponents[props.col.name].props(props)" + @click="tableColumnComponents[props.col.name].event(props)" + > + <WorkerDescriptorProxy :id="props.row.salesPersonFk" /> + {{ props.row.salesPerson }} + </component> + <span class="col-content" v-else>-</span> + </QTd> + </template> + <template #body-cell-actions="props"> + <QTd @click="stopEventPropagation($event, props.col)"> + <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)" + /> + </QTd> </template> </QTable> </QPage> diff --git a/src/pages/Entry/Card/EntryBuysImport.vue b/src/pages/Entry/Card/EntryBuysImport.vue index 21f0beada..3e0ac1410 100644 --- a/src/pages/Entry/Card/EntryBuysImport.vue +++ b/src/pages/Entry/Card/EntryBuysImport.vue @@ -44,7 +44,7 @@ const columns = computed(() => [ align: 'left', }, { - label: t('entry.buys.description'), + label: t('globals.description'), name: 'description', field: 'description', align: 'left', @@ -214,7 +214,7 @@ const redirectToBuysView = () => { class="cursor-pointer" @click="inputFileRef.pickFiles()" > - <QTooltip>{{ t('Select a file') }}</QTooltip> + <QTooltip>{{ t('globals.selectFile') }}</QTooltip> </QIcon> </template> </QFile> @@ -292,6 +292,6 @@ const redirectToBuysView = () => { <i18n> es: - Select a file: Selecciona un fichero + globals.selectFile: Selecciona un fichero Some of the imported buys does not have an item: Algunas de las compras importadas no tienen un artículo </i18n> diff --git a/src/pages/Entry/Card/EntryDms.vue b/src/pages/Entry/Card/EntryDms.vue new file mode 100644 index 000000000..bab1ea6c2 --- /dev/null +++ b/src/pages/Entry/Card/EntryDms.vue @@ -0,0 +1,11 @@ +<script setup> +import VnDmsList from 'src/components/common/VnDmsList.vue'; +</script> +<template> + <VnDmsList + model="EntryDms" + update-model="EntryDms" + default-dms-code="entry" + filter="entryFk" + /> +</template> diff --git a/src/pages/Entry/Card/EntryNotes.vue b/src/pages/Entry/Card/EntryNotes.vue index f56e59253..0d2e5e51a 100644 --- a/src/pages/Entry/Card/EntryNotes.vue +++ b/src/pages/Entry/Card/EntryNotes.vue @@ -63,7 +63,7 @@ onMounted(() => { </div> <div class="col"> <VnInput - :label="t('entry.notes.description')" + :label="t('globals.description')" v-model="row.description" :rules="validate('EntryObservation.description')" /> diff --git a/src/pages/Entry/EntryLatestBuys.vue b/src/pages/Entry/EntryLatestBuys.vue index f4a423f3b..09a6a2f27 100644 --- a/src/pages/Entry/EntryLatestBuys.vue +++ b/src/pages/Entry/EntryLatestBuys.vue @@ -59,7 +59,7 @@ const columns = computed(() => [ align: 'left', }, { - label: t('entry.latestBuys.description'), + label: t('globals.description'), field: 'description', name: 'description', align: 'left', @@ -214,7 +214,7 @@ const editTableCellFormFieldsOptions = [ { field: 'grouping', label: t('entry.latestBuys.grouping') }, { field: 'packageValue', label: t('entry.latestBuys.packageValue') }, { field: 'weight', label: t('entry.latestBuys.weight') }, - { field: 'description', label: t('entry.latestBuys.description') }, + { field: 'description', label: t('globals.description') }, { field: 'size', label: t('entry.latestBuys.size') }, { field: 'weightByPiece', label: t('entry.latestBuys.weightByPiece') }, { field: 'packingOut', label: t('entry.latestBuys.packingOut') }, diff --git a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue index 2a29a3d0e..f557c8ef4 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue @@ -174,7 +174,12 @@ async function upsert() { @on-fetch="(data) => (userConfig = data)" auto-load /> - <FormModel v-if="invoiceIn" :url="`InvoiceIns/${route.params.id}`" model="invoiceIn"> + <FormModel + v-if="invoiceIn" + :url="`InvoiceIns/${route.params.id}`" + model="invoiceIn" + :auto-load="true" + > <template #form="{ data }"> <div class="row q-gutter-md q-mb-md"> <div class="col"> @@ -509,7 +514,7 @@ async function upsert() { @click="inputFileRef.pickFiles()" > <QTooltip> - {{ t('Select a file') }} + {{ t('globals.selectFile') }} </QTooltip> </QBtn> <QBtn icon="info" flat round padding="xs"> @@ -618,7 +623,7 @@ async function upsert() { @click="inputFileRef.pickFiles()" > <QTooltip> - {{ t('Select a file') }} + {{ t('globals.selectFile') }} </QTooltip> </QBtn> <QBtn icon="info" flat round padding="xs"> @@ -687,7 +692,6 @@ async function upsert() { Generate identifier for original file: Generar identificador para archivo original File: Fichero Create document: Crear documento - Select a file: Seleccione un fichero Allowed content types: Tipos de archivo permitidos The company can't be empty: La empresa no puede estar vacía The warehouse can't be empty: El almacén no puede estar vacío diff --git a/src/pages/Order/Card/OrderSummary.vue b/src/pages/Order/Card/OrderSummary.vue index 9b26891a1..f9704a480 100644 --- a/src/pages/Order/Card/OrderSummary.vue +++ b/src/pages/Order/Card/OrderSummary.vue @@ -31,7 +31,7 @@ const detailsColumns = ref([ }, { name: 'description', - label: t('order.summary.description'), + label: t('globals.description'), field: (row) => row?.item?.name, }, { @@ -167,7 +167,7 @@ const detailsColumns = ref([ <template #header="props"> <QTr :props="props"> <QTh auto-width>{{ t('order.summary.item') }}</QTh> - <QTh>{{ t('order.summary.description') }}</QTh> + <QTh>{{ t('globals.description') }}</QTh> <QTh auto-width>{{ t('order.summary.quantity') }}</QTh> <QTh auto-width>{{ t('order.summary.price') }}</QTh> <QTh auto-width>{{ t('order.summary.amount') }}</QTh> diff --git a/src/pages/Route/Card/RouteSummary.vue b/src/pages/Route/Card/RouteSummary.vue index a10ca088e..df4495d3a 100644 --- a/src/pages/Route/Card/RouteSummary.vue +++ b/src/pages/Route/Card/RouteSummary.vue @@ -199,7 +199,7 @@ const openBuscaman = async (route, ticket) => { </QCard> <QCard class="vn-one"> <div class="header"> - {{ t('route.summary.description') }} + {{ t('globals.description') }} </div> <p> {{ dashIfEmpty(entity?.route?.description) }} diff --git a/src/pages/Ticket/Card/TicketSummary.vue b/src/pages/Ticket/Card/TicketSummary.vue index fe7dcee9a..f3e01d06b 100644 --- a/src/pages/Ticket/Card/TicketSummary.vue +++ b/src/pages/Ticket/Card/TicketSummary.vue @@ -270,7 +270,7 @@ async function changeState(value) { <QTh auto-width>{{ t('ticket.summary.visible') }}</QTh> <QTh auto-width>{{ t('ticket.summary.available') }}</QTh> <QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh> - <QTh auto-width>{{ t('ticket.summary.description') }}</QTh> + <QTh auto-width>{{ t('globals.description') }}</QTh> <QTh auto-width>{{ t('ticket.summary.price') }}</QTh> <QTh auto-width>{{ t('ticket.summary.discount') }}</QTh> <QTh auto-width>{{ t('globals.amount') }}</QTh> @@ -425,7 +425,7 @@ async function changeState(value) { <template #header="props"> <QTr :props="props"> <QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh> - <QTh auto-width>{{ t('ticket.summary.description') }}</QTh> + <QTh auto-width>{{ t('globals.description') }}</QTh> <QTh auto-width>{{ t('ticket.summary.price') }}</QTh> <QTh auto-width>{{ t('ticket.summary.taxClass') }}</QTh> <QTh auto-width>{{ t('globals.amount') }}</QTh> diff --git a/src/pages/Travel/Card/TravelThermographsForm.vue b/src/pages/Travel/Card/TravelThermographsForm.vue index 6758cb6ff..4462846cb 100644 --- a/src/pages/Travel/Card/TravelThermographsForm.vue +++ b/src/pages/Travel/Card/TravelThermographsForm.vue @@ -300,7 +300,7 @@ const onThermographCreated = async (data) => { <VnRow v-if="viewAction === 'edit'" class="row q-gutter-md q-mb-md"> <div class="col"> <QInput - :label="t('travel.thermographs.description')" + :label="t('globals.description')" type="textarea" v-model="thermographForm.description" fill-input diff --git a/src/router/modules/entry.js b/src/router/modules/entry.js index b3ab05a08..2f6c8cb4c 100644 --- a/src/router/modules/entry.js +++ b/src/router/modules/entry.js @@ -11,7 +11,7 @@ export default { redirect: { name: 'EntryMain' }, menus: { main: ['EntryList', 'EntryLatestBuys'], - card: ['EntryBasicData', 'EntryBuys', 'EntryNotes', 'EntryLog'], + card: ['EntryBasicData', 'EntryBuys', 'EntryNotes', 'EntryDms', 'EntryLog'], }, children: [ { @@ -95,6 +95,15 @@ export default { }, component: () => import('src/pages/Entry/Card/EntryNotes.vue'), }, + { + path: 'dms', + name: 'EntryDms', + meta: { + title: 'dms', + icon: 'smb_share', + }, + component: () => import('src/pages/Entry/Card/EntryDms.vue'), + }, { path: 'log', name: 'EntryLog', diff --git a/test/cypress/integration/claim/claimDevelopment.spec.js b/test/cypress/integration/claim/claimDevelopment.spec.js index 88ccbfab8..26c7ee196 100755 --- a/test/cypress/integration/claim/claimDevelopment.spec.js +++ b/test/cypress/integration/claim/claimDevelopment.spec.js @@ -13,7 +13,7 @@ describe('ClaimDevelopment', () => { it('should reset line', () => { cy.selectOption(firstLineReason, 'Novato'); cy.resetCard(); - cy.getValue(firstLineReason).should('have.value', 'Prisas'); + cy.getValue(firstLineReason).should('equal', 'Prisas'); }); it('should edit line', () => { @@ -23,7 +23,7 @@ describe('ClaimDevelopment', () => { cy.login('developer'); cy.visit(`/#/claim/${claimId}/development`); - cy.getValue(firstLineReason).should('have.value', 'Novato'); + cy.getValue(firstLineReason).should('equal', 'Novato'); //Restart data cy.selectOption(firstLineReason, 'Prisas'); diff --git a/test/cypress/integration/entry/entryDms.spec.js b/test/cypress/integration/entry/entryDms.spec.js new file mode 100644 index 000000000..79a9c5162 --- /dev/null +++ b/test/cypress/integration/entry/entryDms.spec.js @@ -0,0 +1,41 @@ +describe('WagonTypeCreate', () => { + const entryId = 1; + + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/entry/${entryId}/dms`); + + }); + + it('should create edit and remove new dms', () => { + cy.addRow(); + cy.get('.icon-attach').click() + cy.get('.q-file').selectFile('test/cypress/fixtures/image.jpg', { + force: true, + }); + + cy.get("tbody > tr").then((value) => { + //Create and check if exist new row + let newFileTd = Cypress.$(value).length; + cy.get('.q-btn--standard > .q-btn__content > .block').click(); + expect(value).to.have.length(newFileTd++); + const newRowSelector = `tbody > :nth-child(${newFileTd})` + cy.waitForElement(newRowSelector); + + //Edit new dms + const u = undefined; + cy.validateRow(newRowSelector, [u,u,u,u,'ENTRADA ID 1']) + cy.get(`tbody :nth-child(${newFileTd}) > .text-right > .flex > :nth-child(2) > .q-btn > .q-btn__content > .q-icon`).click(); + }) + // cy.log('newFileTd', newFileTd) + + // //Create and check if exist new row + // cy.log('newFileTd:', newFileTd); + // cy.get(`tbody :nth-child(${newFileTd}) > .text-right > .flex > :nth-child(2) > .q-btn > .q-btn__content > .q-icon`).click() + + // cy.get(`tbody :nth-child(${newFileTd}) > :nth-child(5) > .q-tr > :nth-child(1) > span`).then((value) => { + // cy.log(value) + // }); + }); +}); diff --git a/test/cypress/integration/invoiceIn/invoiceInIntrastat.spec.js b/test/cypress/integration/invoiceIn/invoiceInIntrastat.spec.js index 5024b2f1c..306c0b8c0 100644 --- a/test/cypress/integration/invoiceIn/invoiceInIntrastat.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInIntrastat.spec.js @@ -18,7 +18,7 @@ describe('InvoiceInIntrastat', () => { cy.visit(`/#/invoice-in/1/intrastat`); cy.getValue(firstLineCode).should( - 'have.value', + 'equal', 'Plantas vivas: Esqueje/injerto, Vid' ); }); diff --git a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js index 26c7750ad..811374b98 100644 --- a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js @@ -21,7 +21,7 @@ describe('InvoiceInVat', () => { cy.saveCard(); cy.visit(`/#/invoice-in/1/vat`); - cy.getValue(firstLineVat).should('have.value', 'H.P. IVA 21% CEE'); + cy.getValue(firstLineVat).should('equal', 'H.P. IVA 21% CEE'); }); it('should add a new row', () => { diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 6d627e631..f075d500f 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -42,7 +42,7 @@ Cypress.Commands.add('login', (user) => { }); Cypress.Commands.add('waitForElement', (element) => { - cy.get(element, { timeout: 2000 }).should('be.visible'); + cy.get(element, { timeout: 5000 }).should('be.visible'); }); Cypress.Commands.add('getValue', (selector) => { @@ -55,7 +55,13 @@ Cypress.Commands.add('getValue', (selector) => { return cy.get( selector + '> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input' - ); + ).invoke('val') + } + // Si es un QSelect + if ($el.find('span').length) { + return cy.get( + selector + ' span' + ).then(($span) => { return $span[0].innerText }) } // Puedes añadir un log o lanzar un error si el elemento no es reconocido cy.log('Elemento no soportado'); @@ -126,12 +132,13 @@ Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => { cy.get(rowSelector).within(() => { for (const [index, value] of expectedValues.entries()) { cy.log('CHECKING ', index, value); + if(value === undefined) continue if (typeof value == 'boolean') { const prefix = value ? '' : 'not.'; cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`); continue; } - cy.getValue(`:nth-child(${index + 1})`).should('have.value', value); + cy.getValue(`:nth-child(${index + 1})`).should('equal', value) } }); });