From 5b6a526932b0bdc232fd8ac8a05f5c0da378f9c3 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Mon, 29 Jan 2024 15:12:30 +0100 Subject: [PATCH 01/11] refs #5509 feat: VnDms feat: EntryDms --- src/components/FormModel.vue | 2 +- src/components/common/VnDms.vue | 75 ++++++++++++++++++++++++++++++ src/components/ui/VnRow.vue | 8 +++- src/components/ui/VnSubToolbar.vue | 2 +- src/i18n/en/index.js | 4 ++ src/i18n/es/index.js | 4 ++ src/pages/Entry/Card/EntryDms.vue | 6 +++ src/router/modules/entry.js | 11 ++++- 8 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 src/components/common/VnDms.vue create mode 100644 src/pages/Entry/Card/EntryDms.vue diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 4ad566bf8..153c086bb 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -128,7 +128,7 @@ async function save() { try { const body = $props.mapper ? $props.mapper(formData.value) : formData.value; - let response + let response; if ($props.urlCreate) { response = await axios.post($props.urlCreate, body); notify('globals.dataCreated', 'positive'); diff --git a/src/components/common/VnDms.vue b/src/components/common/VnDms.vue new file mode 100644 index 000000000..cdc54b786 --- /dev/null +++ b/src/components/common/VnDms.vue @@ -0,0 +1,75 @@ +<script setup> +import { ref } from 'vue'; +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; + +import FetchData from 'components/FetchData.vue'; +import FormModel from 'components/FormModel.vue'; +import VnRow from 'components/ui/VnRow.vue'; +import VnSelectFilter from 'src/components/common/VnSelectFilter.vue'; +import VnInput from 'src/components/common/VnInput.vue'; + +const route = useRoute(); +const { t } = useI18n(); + +const warehouses = ref(); +const companies = ref(); +const dmsTypes = ref(); +</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="(data) => (dmsTypes = data)" auto-load /> + <FormModel + :url="`Dms/${route.params.id}`" + :url-update="`Claims/updateClaim/${route.params.id}`" + model="dms" + > + <template #form="{ data }"> + <div class="q-gutter-y-ms"> + <VnRow> + <VnInput :label="t('Reference')" v-model="data.reference" /> + <VnSelectFilter + :label="t('globals.company')" + v-model="data.companyFk" + :options="companies" + option-value="id" + option-label="code" + input-debounce="0" + /> + </VnRow> + <VnRow> + <VnSelectFilter + :label="t('globals.warehouse')" + v-model="data.warehouseFk" + :options="warehouses" + option-value="id" + option-label="name" + input-debounce="0" + /> + <VnSelectFilter + :label="t('globals.type')" + v-model="data.dmsTypeFk" + :options="dmsTypes" + option-value="id" + option-label="name" + input-debounce="0" + /> + </VnRow> + <VnRow> + <QInput + :label="t('globals.description')" + v-model="data.description" + type="textarea" + /> + </VnRow> + </div> + </template> + </FormModel> +</template> +<style scoped> +.q-gutter-y-ms { + display: grid; + row-gap: 20px; +} +</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/components/ui/VnSubToolbar.vue b/src/components/ui/VnSubToolbar.vue index 81a1820f1..b314716ce 100644 --- a/src/components/ui/VnSubToolbar.vue +++ b/src/components/ui/VnSubToolbar.vue @@ -1,5 +1,5 @@ <script setup> -import { onMounted, onUnmounted, ref } from 'vue'; +import { onMounted, onUnmounted } from 'vue'; import { useStateStore } from 'stores/useStateStore'; const stateStore = useStateStore(); diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js index 1a7a4c27e..2f680280c 100644 --- a/src/i18n/en/index.js +++ b/src/i18n/en/index.js @@ -64,6 +64,9 @@ export default { markAll: 'Mark all', noResults: 'No results', system: 'System', + warehouse: 'Warehouse', + company: 'Company', + type: 'Type', }, errors: { statusUnauthorized: 'Access denied', @@ -265,6 +268,7 @@ export default { basicData: 'Basic data', buys: 'Buys', notes: 'Notes', + dms: 'File management', log: 'Log', }, list: { diff --git a/src/i18n/es/index.js b/src/i18n/es/index.js index 83de42ee0..023c339a4 100644 --- a/src/i18n/es/index.js +++ b/src/i18n/es/index.js @@ -64,6 +64,9 @@ export default { markAll: 'Marcar todo', noResults: 'Sin resultados', system: 'Sistema', + warehouse: 'Almacén', + company: 'Empresa', + type: 'Tipo', }, errors: { statusUnauthorized: 'Acceso denegado', @@ -264,6 +267,7 @@ export default { basicData: 'Datos básicos', buys: 'Compras', notes: 'Notas', + dms: 'Gestión documental', log: 'Historial', }, list: { diff --git a/src/pages/Entry/Card/EntryDms.vue b/src/pages/Entry/Card/EntryDms.vue new file mode 100644 index 000000000..57e6eff53 --- /dev/null +++ b/src/pages/Entry/Card/EntryDms.vue @@ -0,0 +1,6 @@ +<script setup> +import VnDms from 'src/components/common/VnDms.vue'; +</script> +<template> + <VnDms model="Entry" /> +</template> diff --git a/src/router/modules/entry.js b/src/router/modules/entry.js index 8d25a8e0c..3ac12d953 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'], - card: ['EntryBasicData', 'EntryBuys', 'EntryNotes', 'EntryLog'], + card: ['EntryBasicData', 'EntryBuys', 'EntryNotes', 'EntryDms', 'EntryLog'], }, children: [ { @@ -86,6 +86,15 @@ export default { }, component: () => import('src/pages/Entry/Card/EntryNotes.vue'), }, + { + path: 'dms', + name: 'EntryDms', + meta: { + title: 'dms', + icon: 'cloud_upload', + }, + component: () => import('src/pages/Entry/Card/EntryDms.vue'), + }, { path: 'log', name: 'EntryLog', -- 2.40.1 From 64b7a07d4199353861e86fbd3e8bf119e2a77fea Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Mon, 5 Feb 2024 15:04:13 +0100 Subject: [PATCH 02/11] refs #5509 feat: VnDms FormModel --- src/components/FetchData.vue | 7 --- src/components/FormModel.vue | 9 +++- src/components/common/VnDms.vue | 89 ++++++++++++++++++++++++++----- src/pages/Entry/Card/EntryDms.vue | 3 +- 4 files changed, 86 insertions(+), 22 deletions(-) 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 594780220..c3ca8fe98 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -59,6 +59,10 @@ const $props = defineProps({ type: Function, default: null, }, + updateType: { + type: String, + default: 'patch', + }, }); const emit = defineEmits(['onFetch', 'onDataSaved']); @@ -136,7 +140,10 @@ async function save() { response = await axios.post($props.urlCreate, body); notify('globals.dataCreated', 'positive'); } else { - response = await axios.patch($props.urlUpdate || $props.url, body); + response = await axios[$props.updateType]( + $props.urlUpdate || $props.url, + body + ); } emit('onDataSaved', formData.value, response?.data); originalData.value = JSON.parse(JSON.stringify(formData.value)); diff --git a/src/components/common/VnDms.vue b/src/components/common/VnDms.vue index cdc54b786..a3a5dab21 100644 --- a/src/components/common/VnDms.vue +++ b/src/components/common/VnDms.vue @@ -12,26 +12,58 @@ import VnInput from 'src/components/common/VnInput.vue'; const route = useRoute(); const { t } = useI18n(); +const props = defineProps({ + model: { + type: String, + required: true, + }, +}); + const warehouses = ref(); const companies = ref(); const dmsTypes = ref(); +const allowedContentTypes = ref(); +const dms = ref({}); + +function onFileChange(files) { + dms.value.hasFileAttached = !!files; + dms.value.file = files?.name; +} + +function parseDms(data) { + const defaultDms = {}; + + for (let prop in data) { + if (prop.endsWith('Fk')) data[prop.replace('Fk', 'Id')] = data[prop]; + } + console.log(data); + 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="(data) => (dmsTypes = data)" auto-load /> + <FetchData + url="DmsContainers/allowedContentTypes" + @on-fetch="(data) => (allowedContentTypes = data.join(','))" + auto-load + /> <FormModel :url="`Dms/${route.params.id}`" - :url-update="`Claims/updateClaim/${route.params.id}`" + update-type="post" + :url-update="`${props.model}/${route.params.id}/uploadFile`" + @on-fetch="parseDms" model="dms" + :auto-load="!!route.params.id" > - <template #form="{ data }"> + <template #form> <div class="q-gutter-y-ms"> <VnRow> - <VnInput :label="t('Reference')" v-model="data.reference" /> + <VnInput :label="t('Reference')" v-model="dms.reference" /> <VnSelectFilter :label="t('globals.company')" - v-model="data.companyFk" + v-model="dms.companyFk" :options="companies" option-value="id" option-label="code" @@ -41,7 +73,7 @@ const dmsTypes = ref(); <VnRow> <VnSelectFilter :label="t('globals.warehouse')" - v-model="data.warehouseFk" + v-model="dms.warehouseFk" :options="warehouses" option-value="id" option-label="name" @@ -49,20 +81,44 @@ const dmsTypes = ref(); /> <VnSelectFilter :label="t('globals.type')" - v-model="data.dmsTypeFk" + v-model="dms.dmsTypeFk" :options="dmsTypes" option-value="id" option-label="name" input-debounce="0" /> </VnRow> - <VnRow> - <QInput - :label="t('globals.description')" - v-model="data.description" - type="textarea" - /> - </VnRow> + <QInput + :label="t('globals.description')" + v-model="dms.description" + type="textarea" + /> + <QFile + :label="t('entry.buys.file')" + v-model="dms.files" + :multiple="false" + accept=".json" + @update:model-value="onFileChange(dms.files)" + class="required" + :display-value="dms.file" + > + <template #prepend> + <QIcon name="vn:attach" class="cursor-pointer"> + <QTooltip>{{ t('Select a file') }}</QTooltip> + </QIcon> + </template> + <template #append> + <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> </FormModel> @@ -73,3 +129,10 @@ const dmsTypes = ref(); row-gap: 20px; } </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/pages/Entry/Card/EntryDms.vue b/src/pages/Entry/Card/EntryDms.vue index 57e6eff53..696c49a34 100644 --- a/src/pages/Entry/Card/EntryDms.vue +++ b/src/pages/Entry/Card/EntryDms.vue @@ -2,5 +2,6 @@ import VnDms from 'src/components/common/VnDms.vue'; </script> <template> - <VnDms model="Entry" /> + <VnDms model="Clients" /> + <!-- CHANGE ME--> </template> -- 2.40.1 From 18b76e5e129f7bb1b4a510640868d2b713947820 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Thu, 8 Feb 2024 07:25:23 +0100 Subject: [PATCH 03/11] refs #5509 feat: VnDmsList --- src/components/common/VnDms.vue | 33 ++++- src/components/common/VnDmsList.vue | 181 ++++++++++++++++++++++++++++ src/pages/Entry/Card/EntryDms.vue | 4 +- 3 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 src/components/common/VnDmsList.vue diff --git a/src/components/common/VnDms.vue b/src/components/common/VnDms.vue index a3a5dab21..169356152 100644 --- a/src/components/common/VnDms.vue +++ b/src/components/common/VnDms.vue @@ -17,12 +17,17 @@ const props = defineProps({ type: String, required: true, }, + defaultDmsCode: { + type: String, + required: true, + }, }); const warehouses = ref(); const companies = ref(); const dmsTypes = ref(); const allowedContentTypes = ref(); +const config = ref({}); const dms = ref({}); function onFileChange(files) { @@ -39,6 +44,25 @@ function parseDms(data) { console.log(data); dms.value = data; } + +function mapperDms(data) { + const formData = new FormData(); + const { files } = data; + if (files) formData.append(files?.name, files); + console.log('data', data); + delete data.files; + + const dms = { + hasFile: false, + hasFileAttached: false, + reference: data.id, + warehouseId: config.value.warehouseFk, + companyId: config.value.companyFk, + dmsTypeId: data.dmsTypeFk, + description: 'ASD', + }; + return [formData, { params: dms }]; +} </script> <template> <FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load /> @@ -49,6 +73,11 @@ function parseDms(data) { @on-fetch="(data) => (allowedContentTypes = data.join(','))" auto-load /> + <FetchData + url="UserConfigs/getUserConfig" + @on-fetch="(data) => (config = data)" + auto-load + /> <FormModel :url="`Dms/${route.params.id}`" update-type="post" @@ -56,6 +85,7 @@ function parseDms(data) { @on-fetch="parseDms" model="dms" :auto-load="!!route.params.id" + :mapper="mapperDms" > <template #form> <div class="q-gutter-y-ms"> @@ -93,11 +123,12 @@ function parseDms(data) { v-model="dms.description" type="textarea" /> + {{ allowedContentTypes }} <QFile :label="t('entry.buys.file')" v-model="dms.files" :multiple="false" - accept=".json" + :accept="allowedContentTypes" @update:model-value="onFileChange(dms.files)" class="required" :display-value="dms.file" diff --git a/src/components/common/VnDmsList.vue b/src/components/common/VnDmsList.vue new file mode 100644 index 000000000..06078ef17 --- /dev/null +++ b/src/components/common/VnDmsList.vue @@ -0,0 +1,181 @@ +<script setup> +import { ref, computed } from 'vue'; +import { useRoute } from 'vue-router'; +import { useI18n } from 'vue-i18n'; + +import FetchData from 'components/FetchData.vue'; +import FormModel from 'components/FormModel.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 { QCheckbox, QBtn } from 'quasar'; + +const route = useRoute(); +const { t } = useI18n(); +const rows = ref(); + +const $props = defineProps({ + model: { + type: String, + required: true, + }, + defaultDmsCode: { + type: String, + required: true, + }, + entity: { + type: String, + default: 'entryFk', + }, +}); + +const dmsFilter = { + include: { + relation: 'dms', + scope: { + fields: [ + 'dmsTypeFk', + 'reference', + 'hardCopyNumber', + 'workerFk', + 'description', + 'hasFile', + 'file', + 'created', + ], + include: [ + { + relation: 'dmsType', + scope: { + fields: ['name'], + }, + }, + { + relation: 'worker', + scope: { + fields: ['id'], + include: { + relation: 'user', + scope: { + fields: ['name'], + }, + }, + }, + }, + ], + }, + }, +}; + +const columns = computed(() => [ + { + align: 'left', + field: 'id', + label: t('id'), + name: 'id', + component: 'span', + }, + { + align: 'left', + field: 'type', + label: t('type'), + name: 'type', + component: 'span', + }, + { + align: 'left', + field: 'order', + label: t('order'), + name: 'order', + component: 'span', + }, + { + align: 'left', + field: 'reference', + label: t('reference'), + name: 'reference', + component: 'span', + }, + { + align: 'left', + field: 'description', + label: t('description'), + name: 'description', + component: 'span', + }, + { + align: 'left', + field: 'hasFile', + label: t('hasFile'), + name: 'hasFile', + component: QCheckbox, + }, + { + align: 'left', + field: 'file', + label: t('file'), + name: 'file', + component: 'span', + }, + { + align: 'center', + field: 'options', + name: 'options', + }, +]); + +function setData(data) { + const newData = data.map((value) => value.dms); + console.log(newData); + rows.value = newData; +} +</script> +<template> + <FetchData + :url="$props.model" + :where="{ [$props.entity]: route.params.id }" + :filter="dmsFilter" + @on-fetch="setData" + auto-load + /> + <QTable + :columns="columns" + :pagination="{ rowsPerPage: 0 }" + :rows="rows" + class="full-width q-mt-md" + hide-bottom + row-key="clientFk" + selection="multiple" + v-model:selected="selected" + > + <template #body-cell="props"> + <QTd :props="props"> + <QTr :props="props" class="cursor-pointer"> + <component + v-if="props.col.component" + :is="props.col.component" + v-bind="props.col.props && props.col.props(props)" + @click="props.col.event(props)" + > + {{ props.value }} + <!-- <QBtn --> + </component> + </QTr> + </QTd> + </template> + asd + </QTable> +</template> +<style scoped> +.q-gutter-y-ms { + display: grid; + row-gap: 20px; +} +</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/pages/Entry/Card/EntryDms.vue b/src/pages/Entry/Card/EntryDms.vue index 696c49a34..a26811630 100644 --- a/src/pages/Entry/Card/EntryDms.vue +++ b/src/pages/Entry/Card/EntryDms.vue @@ -1,7 +1,7 @@ <script setup> -import VnDms from 'src/components/common/VnDms.vue'; +import VnDmsList from 'src/components/common/VnDmsList.vue'; </script> <template> - <VnDms model="Clients" /> + <VnDmsList model="EntryDms" default-dms-code="entry" /> <!-- CHANGE ME--> </template> -- 2.40.1 From aa4d5bffc36d6c9610de549f4e1d76d591b65b26 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Mon, 12 Feb 2024 15:06:20 +0100 Subject: [PATCH 04/11] refs #5509 feat: VnDms & VnDmsList --- src/components/CrudModel.vue | 17 +- src/components/FormModel.vue | 24 +-- src/components/common/VnDms.vue | 98 +++++++---- src/components/common/VnDmsList.vue | 166 +++++++++++++++--- src/i18n/en/index.js | 17 +- src/i18n/es/index.js | 17 +- src/pages/Claim/ClaimList.vue | 2 +- src/pages/Entry/Card/EntryBuysImport.vue | 2 +- src/pages/Entry/Card/EntryDms.vue | 17 +- src/pages/Entry/Card/EntryNotes.vue | 2 +- src/pages/Entry/EntryLatestBuys.vue | 4 +- src/pages/Order/Card/OrderSummary.vue | 4 +- src/pages/Route/Card/RouteSummary.vue | 2 +- src/pages/Ticket/Card/TicketSummary.vue | 4 +- .../Travel/Card/TravelThermographsForm.vue | 2 +- 15 files changed, 258 insertions(+), 120 deletions(-) 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/FormModel.vue b/src/components/FormModel.vue index 0c669902f..c33835438 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -59,9 +59,9 @@ const $props = defineProps({ type: Function, default: null, }, - updateType: { - type: String, - default: 'patch', + saveFn: { + type: Function, + default: null, }, }); @@ -79,8 +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); + if ($props.formInitialData || !$props.autoLoad) { + state.set($props.model, $props.formInitialData ?? {}); } else { await fetch(); } @@ -142,19 +142,19 @@ 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[$props.updateType]( - $props.urlUpdate || $props.url, + 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) { + console.error(err); notify('errors.create', 'negative'); } isLoading.value = false; diff --git a/src/components/common/VnDms.vue b/src/components/common/VnDms.vue index 169356152..6cdca22c5 100644 --- a/src/components/common/VnDms.vue +++ b/src/components/common/VnDms.vue @@ -1,25 +1,35 @@ <script setup> -import { ref } from 'vue'; +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 FormModel from 'components/FormModel.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({ +const $props = defineProps({ model: { type: String, required: true, }, defaultDmsCode: { type: String, - required: true, + default: null, + }, + formInitialData: { + type: Object, + default: null, + }, + description: { + type: Function, + default: null, }, }); @@ -27,47 +37,67 @@ const warehouses = ref(); const companies = ref(); const dmsTypes = ref(); const allowedContentTypes = ref(); -const config = ref({}); const dms = ref({}); +onMounted(() => defaultData()); function onFileChange(files) { dms.value.hasFileAttached = !!files; dms.value.file = files?.name; } -function parseDms(data) { - const defaultDms = {}; - - for (let prop in data) { - if (prop.endsWith('Fk')) data[prop.replace('Fk', 'Id')] = data[prop]; - } - console.log(data); - dms.value = data; -} - function mapperDms(data) { const formData = new FormData(); const { files } = data; if (files) formData.append(files?.name, files); - console.log('data', data); delete data.files; const dms = { - hasFile: false, - hasFileAttached: false, - reference: data.id, - warehouseId: config.value.warehouseFk, - companyId: config.value.companyFk, + hasFile: !!data.hasFile, + hasFileAttached: data.hasFileAttached, + reference: data.reference, + warehouseId: data.warehouseFk, + companyId: data.companyFk, dmsTypeId: data.dmsTypeFk, - description: 'ASD', + 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, + description: $props.description && $props.description(dms.value), + }); +} + +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="(data) => (dmsTypes = data)" auto-load /> + <FetchData url="DmsTypes" @on-fetch="setDmsTypes" auto-load /> <FetchData url="DmsContainers/allowedContentTypes" @on-fetch="(data) => (allowedContentTypes = data.join(','))" @@ -75,19 +105,16 @@ function mapperDms(data) { /> <FetchData url="UserConfigs/getUserConfig" - @on-fetch="(data) => (config = data)" - auto-load + @on-fetch="addDefaultData" + :auto-load="!$props.formInitialData" /> - <FormModel - :url="`Dms/${route.params.id}`" - update-type="post" - :url-update="`${props.model}/${route.params.id}/uploadFile`" - @on-fetch="parseDms" + <FormModelPopup + :title="t('create')" model="dms" - :auto-load="!!route.params.id" - :mapper="mapperDms" + :form-initial-data="formInitialData" + :save-fn="save" > - <template #form> + <template #form-inputs> <div class="q-gutter-y-ms"> <VnRow> <VnInput :label="t('Reference')" v-model="dms.reference" /> @@ -123,7 +150,6 @@ function mapperDms(data) { v-model="dms.description" type="textarea" /> - {{ allowedContentTypes }} <QFile :label="t('entry.buys.file')" v-model="dms.files" @@ -152,7 +178,7 @@ function mapperDms(data) { /> </div> </template> - </FormModel> + </FormModelPopup> </template> <style scoped> .q-gutter-y-ms { @@ -164,6 +190,6 @@ function mapperDms(data) { en: contentTypesInfo: Allowed file types {allowedContentTypes} es: - contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes} Generate identifier for original file: Generar identificador para archivo original + contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes} </i18n> diff --git a/src/components/common/VnDmsList.vue b/src/components/common/VnDmsList.vue index 06078ef17..f746fa041 100644 --- a/src/components/common/VnDmsList.vue +++ b/src/components/common/VnDmsList.vue @@ -1,31 +1,43 @@ <script setup> import { ref, computed } from 'vue'; -import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; import FetchData from 'components/FetchData.vue'; -import FormModel from 'components/FormModel.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 { QCheckbox, QBtn } from 'quasar'; +import VnDms from 'src/components/common/VnDms.vue'; +import { downloadFile } from 'src/composables/downloadFile'; +import VnConfirm from 'components/ui/VnConfirm.vue'; +import axios from 'axios'; +import { QCheckbox, QBtn, QInput } from 'quasar'; +import { useQuasar } from 'quasar'; 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, }, - entity: { + filter: { type: String, - default: 'entryFk', + required: true, + }, + description: { + type: Function, + required: true, }, }); @@ -42,6 +54,8 @@ const dmsFilter = { 'hasFile', 'file', 'created', + 'companyFk', + 'warehouseFk', ], include: [ { @@ -65,60 +79,69 @@ const dmsFilter = { ], }, }, + order: ['dmsFk DESC'], }; const columns = computed(() => [ { align: 'left', field: 'id', - label: t('id'), + label: t('globals.id'), name: 'id', component: 'span', }, { align: 'left', field: 'type', - label: t('type'), + label: t('globals.type'), name: 'type', - component: 'span', + component: QInput, + props: (prop) => ({ + readonly: true, + borderless: true, + 'model-value': prop.row.dmsType.name, + }), }, { align: 'left', field: 'order', - label: t('order'), + label: t('globals.order'), name: 'order', component: 'span', }, { align: 'left', field: 'reference', - label: t('reference'), + label: t('globals.reference'), name: 'reference', component: 'span', }, { align: 'left', field: 'description', - label: t('description'), + label: t('globals.description'), name: 'description', component: 'span', }, { align: 'left', field: 'hasFile', - label: t('hasFile'), + label: t('globals.original'), name: 'hasFile', component: QCheckbox, + props: (prop) => ({ + disable: true, + 'model-value': Boolean(prop.value), + }), }, { align: 'left', field: 'file', - label: t('file'), + label: t('globals.file'), name: 'file', component: 'span', }, { - align: 'center', field: 'options', name: 'options', }, @@ -126,15 +149,46 @@ const columns = computed(() => [ function setData(data) { const newData = data.map((value) => value.dms); - console.log(newData); 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" - :where="{ [$props.entity]: route.params.id }" :filter="dmsFilter" + :where="{ [$props.filter]: route.params.id }" @on-fetch="setData" auto-load /> @@ -145,26 +199,86 @@ function setData(data) { class="full-width q-mt-md" hide-bottom row-key="clientFk" - selection="multiple" - v-model:selected="selected" + :grid="$q.screen.lt.md" > <template #body-cell="props"> <QTd :props="props"> - <QTr :props="props" class="cursor-pointer"> + <QTr :props="props"> <component v-if="props.col.component" :is="props.col.component" v-bind="props.col.props && props.col.props(props)" - @click="props.col.event(props)" > - {{ props.value }} - <!-- <QBtn --> + <span v-if="props.col.component == 'span'">{{ + props.value + }}</span> </component> </QTr> + + <div class="flex justify-center" v-if="props.col.name == 'options'"> + <QBtn + icon="cloud_download" + flat + color="primary" + @click="downloadFile(props.row.id)" + /> + <QBtn + icon="edit" + flat + color="primary" + @click="showFormDialog(props.row)" + /> + <QBtn + icon="delete" + flat + color="primary" + @click="deleteDms(props.row.id)" + /> + </div> </QTd> </template> - asd + <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()" + > + <QCardSection> + <QCheckbox v-model="props.selected" dense /> + </QCardSection> + <QSeparator /> + <QList dense> + <QItem v-for="col in props.cols" :key="col.name"> + <QItemSection> + <component + v-if="col.component" + :is="col.component" + v-bind="col.props && col.props(props)" + > + <span v-if="col.component == 'span'">{{ + `${col.label}:${col.value}` + }}</span> + </component> + </QItemSection> + </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 { diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js index bd13f7b1d..b8b979961 100644 --- a/src/i18n/en/index.js +++ b/src/i18n/en/index.js @@ -64,7 +64,7 @@ export default { markAll: 'Mark all', requiredField: 'Required field', class: 'clase', - type: 'type', + type: 'Type', reason: 'reason', noResults: 'No results', system: 'System', @@ -72,6 +72,13 @@ export default { 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', }, errors: { statusUnauthorized: 'Access denied', @@ -347,7 +354,6 @@ export default { reference: 'Reference', observations: 'Observations', item: 'Item', - description: 'Description', size: 'Size', packing: 'Packing', grouping: 'Grouping', @@ -362,7 +368,6 @@ export default { }, notes: { observationType: 'Observation type', - description: 'Description', }, descriptor: { agency: 'Agency', @@ -375,7 +380,6 @@ export default { packing: 'Packing', grouping: 'Grouping', quantity: 'Quantity', - description: 'Description', size: 'Size', tags: 'Tags', type: 'Type', @@ -461,7 +465,6 @@ export default { visible: 'Visible', available: 'Available', quantity: 'Quantity', - description: 'Description', price: 'Price', discount: 'Discount', packing: 'Packing', @@ -534,7 +537,6 @@ export default { landed: 'Landed', quantity: 'Quantity', claimed: 'Claimed', - description: 'Description', price: 'Price', discount: 'Discount', total: 'Total', @@ -795,7 +797,6 @@ export default { orderTicketList: 'Order Ticket List', details: 'Details', item: 'Item', - description: 'Description', quantity: 'Quantity', price: 'Price', amount: 'Amount', @@ -1140,7 +1141,6 @@ export default { warehouse: 'Warehouse', travelFileDescription: 'Travel id { travelId }', file: 'File', - description: 'Description', }, }, item: { @@ -1174,7 +1174,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 a171e8e2c..1ac6e535c 100644 --- a/src/i18n/es/index.js +++ b/src/i18n/es/index.js @@ -64,7 +64,7 @@ export default { markAll: 'Marcar todo', requiredField: 'Campo obligatorio', class: 'clase', - type: 'tipo', + type: 'Tipo', reason: 'motivo', noResults: 'Sin resultados', system: 'Sistema', @@ -72,6 +72,13 @@ export default { 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', }, errors: { statusUnauthorized: 'Acceso denegado', @@ -346,7 +353,6 @@ export default { reference: 'Referencia', observations: 'Observaciónes', item: 'Artículo', - description: 'Descripción', size: 'Medida', packing: 'Packing', grouping: 'Grouping', @@ -361,7 +367,6 @@ export default { }, notes: { observationType: 'Tipo de observación', - description: 'Descripción', }, descriptor: { agency: 'Agencia', @@ -374,7 +379,6 @@ export default { packing: 'Packing', grouping: 'Grouping', quantity: 'Cantidad', - description: 'Descripción', size: 'Medida', tags: 'Etiquetas', type: 'Tipo', @@ -460,7 +464,6 @@ export default { visible: 'Visible', available: 'Disponible', quantity: 'Cantidad', - description: 'Descripción', price: 'Precio', discount: 'Descuento', packing: 'Encajado', @@ -533,7 +536,6 @@ export default { landed: 'Entregado', quantity: 'Cantidad', claimed: 'Reclamado', - description: 'Descripción', price: 'Precio', discount: 'Descuento', total: 'Total', @@ -703,7 +705,6 @@ export default { orderTicketList: 'Tickets del pedido', details: 'Detalles', item: 'Item', - description: 'Descripción', quantity: 'Cantidad', price: 'Precio', amount: 'Monto', @@ -1140,7 +1141,6 @@ export default { warehouse: 'Almacén', travelFileDescription: 'Id envío { travelId }', file: 'Fichero', - description: 'Descripción', }, }, item: { @@ -1174,7 +1174,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 e9462e7a9..acb7ec3fd 100644 --- a/src/pages/Claim/ClaimList.vue +++ b/src/pages/Claim/ClaimList.vue @@ -116,7 +116,7 @@ function navigate(id) { outline /> <QBtn - :label="t('components.smartCard.viewDescription')" + :label="t('globals.description')" @click.stop class="bg-vn-dark" outline diff --git a/src/pages/Entry/Card/EntryBuysImport.vue b/src/pages/Entry/Card/EntryBuysImport.vue index 21f0beada..5783826ec 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', diff --git a/src/pages/Entry/Card/EntryDms.vue b/src/pages/Entry/Card/EntryDms.vue index a26811630..5e4d66c0c 100644 --- a/src/pages/Entry/Card/EntryDms.vue +++ b/src/pages/Entry/Card/EntryDms.vue @@ -2,6 +2,19 @@ import VnDmsList from 'src/components/common/VnDmsList.vue'; </script> <template> - <VnDmsList model="EntryDms" default-dms-code="entry" /> - <!-- CHANGE ME--> + <VnDmsList + model="EntryDms" + update-model="EntryDms" + default-dms-code="entry" + filter="entryFk" + :description=" + (data) => t('description', { reference: data.reference, id: data.id }) + " + /> </template> +<i18n> + en: + description: Reference {reference} id {id} + es: + description: Referencia {reference} id {id} +</i18n> 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/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 -- 2.40.1 From 43154084529399c6e0c21132fbfb97213396dbd6 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Tue, 13 Feb 2024 15:01:24 +0100 Subject: [PATCH 05/11] refs #5509 feat: VnDms description --- src/components/common/VnDms.vue | 26 +++-- src/components/common/VnDmsList.vue | 99 +++++++++++-------- src/pages/Entry/Card/EntryDms.vue | 9 -- .../integration/entry/entryDms.spec.js | 13 +++ 4 files changed, 88 insertions(+), 59 deletions(-) create mode 100644 test/cypress/integration/entry/entryDms.spec.js diff --git a/src/components/common/VnDms.vue b/src/components/common/VnDms.vue index 6cdca22c5..99056556d 100644 --- a/src/components/common/VnDms.vue +++ b/src/components/common/VnDms.vue @@ -27,19 +27,20 @@ const $props = defineProps({ type: Object, default: null, }, - description: { - type: Function, - default: null, - }, }); const warehouses = ref(); const companies = ref(); const dmsTypes = ref(); const allowedContentTypes = ref(); +const inputFileRef = ref(); const dms = ref({}); -onMounted(() => defaultData()); +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; @@ -78,7 +79,6 @@ function defaultData() { if ($props.formInitialData) return (dms.value = $props.formInitialData); return addDefaultData({ reference: route.params.id, - description: $props.description && $props.description(dms.value), }); } @@ -151,6 +151,7 @@ function addDefaultData(data) { type="textarea" /> <QFile + ref="inputFileRef" :label="t('entry.buys.file')" v-model="dms.files" :multiple="false" @@ -159,12 +160,14 @@ function addDefaultData(data) { class="required" :display-value="dms.file" > - <template #prepend> - <QIcon name="vn:attach" class="cursor-pointer"> + <template #append> + <QIcon + name="vn:attach" + class="cursor-pointer" + @click="inputFileRef.pickFiles()" + > <QTooltip>{{ t('Select a file') }}</QTooltip> </QIcon> - </template> - <template #append> <QIcon name="info" class="cursor-pointer"> <QTooltip>{{ t('contentTypesInfo', { allowedContentTypes }) @@ -189,7 +192,10 @@ function addDefaultData(data) { <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 index f746fa041..b7e683da9 100644 --- a/src/components/common/VnDmsList.vue +++ b/src/components/common/VnDmsList.vue @@ -2,14 +2,13 @@ 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 { downloadFile } from 'src/composables/downloadFile'; import VnConfirm from 'components/ui/VnConfirm.vue'; -import axios from 'axios'; -import { QCheckbox, QBtn, QInput } from 'quasar'; -import { useQuasar } from 'quasar'; +import { downloadFile } from 'src/composables/downloadFile'; const route = useRoute(); const quasar = useQuasar(); @@ -35,10 +34,6 @@ const $props = defineProps({ type: String, required: true, }, - description: { - type: Function, - required: true, - }, }); const dmsFilter = { @@ -144,6 +139,35 @@ const columns = computed(() => [ { 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), + }, + ], }, ]); @@ -199,7 +223,7 @@ function parseDms(data) { class="full-width q-mt-md" hide-bottom row-key="clientFk" - :grid="$q.screen.lt.md" + :grid="$q.screen.lt.sm" > <template #body-cell="props"> <QTd :props="props"> @@ -216,24 +240,13 @@ function parseDms(data) { </QTr> <div class="flex justify-center" v-if="props.col.name == 'options'"> - <QBtn - icon="cloud_download" - flat - color="primary" - @click="downloadFile(props.row.id)" - /> - <QBtn - icon="edit" - flat - color="primary" - @click="showFormDialog(props.row)" - /> - <QBtn - icon="delete" - flat - color="primary" - @click="deleteDms(props.row.id)" - /> + <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> @@ -244,23 +257,26 @@ function parseDms(data) { flat @keyup.ctrl.enter.stop="claimDevelopmentForm?.saveChanges()" > - <QCardSection> - <QCheckbox v-model="props.selected" dense /> - </QCardSection> <QSeparator /> <QList dense> <QItem v-for="col in props.cols" :key="col.name"> - <QItemSection> - <component - v-if="col.component" - :is="col.component" - v-bind="col.props && col.props(props)" + <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" > - <span v-if="col.component == 'span'">{{ - `${col.label}:${col.value}` - }}</span> - </component> - </QItemSection> + <component + :is="button.component" + v-bind="button.props(col)" + @click="button.click(col)" + /> + </div> + </div> </QItem> </QList> </QCard> @@ -285,6 +301,9 @@ function parseDms(data) { display: grid; row-gap: 20px; } +.labelColor { + color: var(--vn-label); +} </style> <i18n> en: diff --git a/src/pages/Entry/Card/EntryDms.vue b/src/pages/Entry/Card/EntryDms.vue index 5e4d66c0c..bab1ea6c2 100644 --- a/src/pages/Entry/Card/EntryDms.vue +++ b/src/pages/Entry/Card/EntryDms.vue @@ -7,14 +7,5 @@ import VnDmsList from 'src/components/common/VnDmsList.vue'; update-model="EntryDms" default-dms-code="entry" filter="entryFk" - :description=" - (data) => t('description', { reference: data.reference, id: data.id }) - " /> </template> -<i18n> - en: - description: Reference {reference} id {id} - es: - description: Referencia {reference} id {id} -</i18n> diff --git a/test/cypress/integration/entry/entryDms.spec.js b/test/cypress/integration/entry/entryDms.spec.js new file mode 100644 index 000000000..d9f1d77c0 --- /dev/null +++ b/test/cypress/integration/entry/entryDms.spec.js @@ -0,0 +1,13 @@ +describe('WagonTypeCreate', () => { + const entryId = 1; + beforeEach(() => { + cy.viewport(1920, 1080); + cy.login('developer'); + cy.visit(`/#/entry/${entryId}/dms`); + + }); + + it('should create and delete a new wagon type', () => { + cy.addCard() + }); +}); -- 2.40.1 From b3e554d99eef52524bc7e8acc902e970d8278b93 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Thu, 15 Feb 2024 15:00:30 +0100 Subject: [PATCH 06/11] refs #5509 feat: add e2e --- .../integration/entry/entryDms.spec.js | 32 +++++++++++++++++-- test/cypress/support/commands.js | 11 +++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/test/cypress/integration/entry/entryDms.spec.js b/test/cypress/integration/entry/entryDms.spec.js index d9f1d77c0..79a9c5162 100644 --- a/test/cypress/integration/entry/entryDms.spec.js +++ b/test/cypress/integration/entry/entryDms.spec.js @@ -1,5 +1,6 @@ describe('WagonTypeCreate', () => { const entryId = 1; + beforeEach(() => { cy.viewport(1920, 1080); cy.login('developer'); @@ -7,7 +8,34 @@ describe('WagonTypeCreate', () => { }); - it('should create and delete a new wagon type', () => { - cy.addCard() + 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/support/commands.js b/test/cypress/support/commands.js index 6d627e631..a8b4a86f0 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) => { @@ -57,6 +57,12 @@ Cypress.Commands.add('getValue', (selector) => { '> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input' ); } + // Si es un QSelect + if ($el.find('span').length) { + return cy.get( + selector + ' span' + ); + } // 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})`).invoke('text').should('have.value', value) } }); }); -- 2.40.1 From 33eef88825c33bbce5661a165a7c8d94364a9597 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Wed, 21 Feb 2024 14:00:52 +0100 Subject: [PATCH 07/11] refs #5509 fix: e2e --- test/cypress/integration/claim/claimDevelopment.spec.js | 4 ++-- .../integration/invoiceIn/invoiceInIntrastat.spec.js | 2 +- test/cypress/integration/invoiceIn/invoiceInVat.spec.js | 2 +- test/cypress/support/commands.js | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) 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/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 a8b4a86f0..f075d500f 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -55,13 +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'); @@ -138,7 +138,7 @@ Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => { cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`); continue; } - cy.getValue(`:nth-child(${index + 1})`).invoke('text').should('have.value', value) + cy.getValue(`:nth-child(${index + 1})`).should('equal', value) } }); }); -- 2.40.1 From 00bed4fd619652d44bcdd4df8ff0ee4f73e62259 Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Thu, 22 Feb 2024 10:20:44 +0100 Subject: [PATCH 08/11] refs #5509 fix(FormModel): autoLoad && formInitalData --- src/components/FormModel.vue | 5 ++--- src/pages/InvoiceIn/Card/InvoiceInBasicData.vue | 7 ++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index c33835438..504a7acd6 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -79,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(); } diff --git a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue index 2a29a3d0e..306220dd3 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"> -- 2.40.1 From de184e9cf829a60d328946c2a669624096d9ab1e Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Thu, 22 Feb 2024 10:57:11 +0100 Subject: [PATCH 09/11] refs #5509 feat(DMS): change icon --- src/router/modules/entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/router/modules/entry.js b/src/router/modules/entry.js index 339feaa0c..2f6c8cb4c 100644 --- a/src/router/modules/entry.js +++ b/src/router/modules/entry.js @@ -100,7 +100,7 @@ export default { name: 'EntryDms', meta: { title: 'dms', - icon: 'cloud_upload', + icon: 'smb_share', }, component: () => import('src/pages/Entry/Card/EntryDms.vue'), }, -- 2.40.1 From 4988df882c653d54f814c7218c31840b6b134e8f Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Mon, 26 Feb 2024 15:06:49 +0100 Subject: [PATCH 10/11] refs #5509 fix: transaltions --- src/components/EditPictureForm.vue | 2 +- src/components/common/VnDms.vue | 6 +++--- src/i18n/en/index.js | 2 ++ src/i18n/es/index.js | 2 ++ src/pages/Entry/Card/EntryBuysImport.vue | 4 ++-- src/pages/InvoiceIn/Card/InvoiceInBasicData.vue | 5 ++--- 6 files changed, 12 insertions(+), 9 deletions(-) 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/common/VnDms.vue b/src/components/common/VnDms.vue index 99056556d..d2651f5d8 100644 --- a/src/components/common/VnDms.vue +++ b/src/components/common/VnDms.vue @@ -109,7 +109,7 @@ function addDefaultData(data) { :auto-load="!$props.formInitialData" /> <FormModelPopup - :title="t('create')" + :title="formInitialData ? t('globals.edit') : t('globals.create')" model="dms" :form-initial-data="formInitialData" :save-fn="save" @@ -117,7 +117,7 @@ function addDefaultData(data) { <template #form-inputs> <div class="q-gutter-y-ms"> <VnRow> - <VnInput :label="t('Reference')" v-model="dms.reference" /> + <VnInput :label="t('globals.reference')" v-model="dms.reference" /> <VnSelectFilter :label="t('globals.company')" v-model="dms.companyFk" @@ -166,7 +166,7 @@ function addDefaultData(data) { class="cursor-pointer" @click="inputFileRef.pickFiles()" > - <QTooltip>{{ t('Select a file') }}</QTooltip> + <QTooltip>{{ t('globals.selectFile') }}</QTooltip> </QIcon> <QIcon name="info" class="cursor-pointer"> <QTooltip>{{ diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js index 98dabd4f8..ee518418b 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', @@ -79,6 +80,7 @@ export default { order: 'Order', original: 'Original', file: 'File', + selectFile: 'Select a file', }, errors: { statusUnauthorized: 'Access denied', diff --git a/src/i18n/es/index.js b/src/i18n/es/index.js index 36d4662b4..0f65181bd 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', @@ -79,6 +80,7 @@ export default { order: 'Orden', original: 'Original', file: 'Fichero', + selectFile: 'Seleccione un fichero', }, errors: { statusUnauthorized: 'Acceso denegado', diff --git a/src/pages/Entry/Card/EntryBuysImport.vue b/src/pages/Entry/Card/EntryBuysImport.vue index 5783826ec..3e0ac1410 100644 --- a/src/pages/Entry/Card/EntryBuysImport.vue +++ b/src/pages/Entry/Card/EntryBuysImport.vue @@ -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/InvoiceIn/Card/InvoiceInBasicData.vue b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue index 306220dd3..f557c8ef4 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue @@ -514,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"> @@ -623,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"> @@ -692,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 -- 2.40.1 From c8a4854df9b3e890e8b33f817893586fef2ecadf Mon Sep 17 00:00:00 2001 From: alexm <alexm@verdnatura.es> Date: Tue, 27 Feb 2024 08:22:38 +0100 Subject: [PATCH 11/11] refs #5509 fix(vnDms): fix issues --- src/components/FormModel.vue | 2 +- src/components/common/VnDmsList.vue | 8 +++++--- src/i18n/en/index.js | 2 +- src/i18n/es/index.js | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 504a7acd6..912ce8ea7 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -154,7 +154,7 @@ async function save() { hasChanges.value = false; } catch (err) { console.error(err); - notify('errors.create', 'negative'); + notify('errors.writeRequest', 'negative'); } isLoading.value = false; } diff --git a/src/components/common/VnDmsList.vue b/src/components/common/VnDmsList.vue index b7e683da9..5057c0790 100644 --- a/src/components/common/VnDmsList.vue +++ b/src/components/common/VnDmsList.vue @@ -233,9 +233,11 @@ function parseDms(data) { :is="props.col.component" v-bind="props.col.props && props.col.props(props)" > - <span v-if="props.col.component == 'span'">{{ - props.value - }}</span> + <span + v-if="props.col.component == 'span'" + style="white-space: wrap" + >{{ props.value }}</span + > </component> </QTr> diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js index ee518418b..46237c2b9 100644 --- a/src/i18n/en/index.js +++ b/src/i18n/en/index.js @@ -88,7 +88,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', diff --git a/src/i18n/es/index.js b/src/i18n/es/index.js index 0f65181bd..fe82407ad 100644 --- a/src/i18n/es/index.js +++ b/src/i18n/es/index.js @@ -88,7 +88,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', -- 2.40.1