refs #5509 feat: VnDms & VnDmsList

This commit is contained in:
Alex Moreno 2024-02-12 15:06:20 +01:00
parent cb6ba483d0
commit aa4d5bffc3
15 changed files with 258 additions and 120 deletions

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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 {

View File

@ -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',

View File

@ -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',

View File

@ -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

View File

@ -44,7 +44,7 @@ const columns = computed(() => [
align: 'left',
},
{
label: t('entry.buys.description'),
label: t('globals.description'),
name: 'description',
field: 'description',
align: 'left',

View File

@ -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>

View File

@ -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')"
/>

View File

@ -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') },

View File

@ -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>

View File

@ -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) }}

View File

@ -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>

View File

@ -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