Merge branch 'test' of https://gitea.verdnatura.es/verdnatura/salix-front
gitea/salix-front/pipeline/head This commit looks good Details

This commit is contained in:
Alex Moreno 2024-03-07 07:57:32 +01:00
commit 212fe0e5b4
52 changed files with 7144 additions and 10007 deletions

View File

@ -2,4 +2,4 @@ FROM node:stretch-slim
RUN npm install -g @quasar/cli RUN npm install -g @quasar/cli
WORKDIR /app WORKDIR /app
COPY dist/spa ./ COPY dist/spa ./
CMD ["quasar", "serve", "./", "--history", "--hostname", "0.0.0.0"] CMD ["quasar", "serve", "./", "--history", "--hostname", "0.0.0.0"]

130
Jenkinsfile vendored
View File

@ -1,99 +1,119 @@
#!/usr/bin/env groovy #!/usr/bin/env groovy
def PROTECTED_BRANCH
def BRANCH_ENV = [
test: 'test',
master: 'production'
]
node {
stage('Setup') {
env.FRONT_REPLICAS = 1
env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev'
PROTECTED_BRANCH = [
'dev',
'test',
'master'
].contains(env.BRANCH_NAME)
// https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
echo "NODE_NAME: ${env.NODE_NAME}"
echo "WORKSPACE: ${env.WORKSPACE}"
configFileProvider([
configFile(fileId: 'salix-front.properties',
variable: 'PROPS_FILE')
]) {
def props = readProperties file: PROPS_FILE
props.each {key, value -> env."${key}" = value }
props.each {key, value -> echo "${key}: ${value}" }
}
if (PROTECTED_BRANCH) {
configFileProvider([
configFile(fileId: "salix-front.branch.${env.BRANCH_NAME}",
variable: 'BRANCH_PROPS_FILE')
]) {
def props = readProperties file: BRANCH_PROPS_FILE
props.each {key, value -> env."${key}" = value }
props.each {key, value -> echo "${key}: ${value}" }
}
}
}
}
pipeline { pipeline {
agent any agent any
options { options {
disableConcurrentBuilds() disableConcurrentBuilds()
} }
tools {
nodejs 'node-v18'
}
environment { environment {
PROJECT_NAME = 'lilium' PROJECT_NAME = 'lilium'
STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}" STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}"
} }
stages { stages {
stage('Checkout') {
steps {
script {
switch (env.BRANCH_NAME) {
case 'master':
env.NODE_ENV = 'production'
env.FRONT_REPLICAS = 2
break
case 'test':
env.NODE_ENV = 'test'
env.FRONT_REPLICAS = 1
break
}
}
setEnv()
}
}
stage('Install') { stage('Install') {
environment { environment {
NODE_ENV = "" NODE_ENV = ""
} }
steps { steps {
nodejs('node-v18') { sh 'npm install --prefer-offline'
sh 'npm install --no-audit --prefer-offline'
}
} }
} }
stage('Test') { stage('Test') {
when { not { anyOf { when {
branch 'test' expression { !PROTECTED_BRANCH }
branch 'master' }
}}}
environment { environment {
NODE_ENV = "" NODE_ENV = ""
} }
parallel { steps {
stage('Frontend') { sh 'npm run test:unit:ci'
steps { }
nodejs('node-v18') { post {
sh 'npm run test:unit:ci' always {
} junit(
} testResults: 'junitresults.xml',
allowEmptyResults: true
)
} }
} }
} }
stage('Build') { stage('Build') {
when { anyOf { when {
branch 'test' expression { PROTECTED_BRANCH }
branch 'master' }
}}
environment { environment {
CREDENTIALS = credentials('docker-registry') CREDENTIALS = credentials('docker-registry')
} }
steps { steps {
nodejs('node-v18') { sh 'quasar build'
sh 'quasar build' script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
} }
dockerBuild() dockerBuild()
} }
} }
stage('Deploy') { stage('Deploy') {
when { anyOf { when {
branch 'test' expression { PROTECTED_BRANCH }
branch 'master' }
}}
environment { environment {
DOCKER_HOST = "${env.SWARM_HOST}" DOCKER_HOST = "${env.SWARM_HOST}"
} }
steps { steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
}
sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}" sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}"
} }
} }
} }
post {
always {
script {
if (!['master', 'test'].contains(env.BRANCH_NAME)) {
try {
junit 'junitresults.xml'
junit 'junit.xml'
} catch (e) {
echo e.toString()
}
}
}
}
}
} }

View File

@ -5,13 +5,13 @@ Lilium frontend
## Install the dependencies ## Install the dependencies
```bash ```bash
npm install bun install
``` ```
### Install quasar cli ### Install quasar cli
```bash ```bash
sudo npm install -g @quasar/cli sudo bun install -g @quasar/cli
``` ```
### Start the app in development mode (hot-code reloading, error reporting, etc.) ### Start the app in development mode (hot-code reloading, error reporting, etc.)
@ -23,7 +23,7 @@ quasar dev
### Run unit tests ### Run unit tests
```bash ```bash
npm run test:unit bun run test:unit
``` ```
### Run e2e tests ### Run e2e tests

BIN
bun.lockb Executable file

Binary file not shown.

9264
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.8.0", "version": "24.10.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -46,7 +46,8 @@
"engines": { "engines": {
"node": "^20 || ^18 || ^16", "node": "^20 || ^18 || ^16",
"npm": ">= 8.1.2", "npm": ">= 8.1.2",
"yarn": ">= 1.21.1" "yarn": ">= 1.21.1",
"bun": ">= 1.0.25"
}, },
"overrides": { "overrides": {
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",

5654
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -176,8 +176,8 @@ async function remove(data) {
.dialog({ .dialog({
component: VnConfirm, component: VnConfirm,
componentProps: { componentProps: {
title: t('confirmDeletion'), title: t('globals.confirmDeletion'),
message: t('confirmDeletionMessage'), message: t('globals.confirmDeletionMessage'),
newData, newData,
ids, ids,
}, },
@ -317,16 +317,3 @@ watch(formUrl, async () => {
color="primary" color="primary"
/> />
</template> </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

@ -272,7 +272,7 @@ const makeRequest = async () => {
class="cursor-pointer q-mr-sm" class="cursor-pointer q-mr-sm"
@click="openInputFile()" @click="openInputFile()"
> >
<!-- <QTooltip>{{ t('Select a file') }}</QTooltip> --> <!-- <QTooltip>{{ t('globals.selectFile') }}</QTooltip> -->
</QIcon> </QIcon>
<QIcon name="info" class="cursor-pointer"> <QIcon name="info" class="cursor-pointer">
<QTooltip>{{ <QTooltip>{{

View File

@ -59,11 +59,4 @@ async function fetch(fetchFilter = {}) {
// //
} }
} }
const render = () => {
return h('div', []);
};
</script> </script>
<template>
<render />
</template>

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue'; import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
@ -8,6 +9,7 @@ import { useStateStore } from 'stores/useStateStore';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import SkeletonForm from 'components/ui/SkeletonForm.vue'; import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const state = useState(); const state = useState();
@ -59,6 +61,10 @@ const $props = defineProps({
type: Function, type: Function,
default: null, default: null,
}, },
saveFn: {
type: Function,
default: null,
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
@ -75,9 +81,8 @@ onMounted(async () => {
}); });
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
if ($props.formInitialData && !$props.autoLoad) { state.set($props.model, $props.formInitialData);
state.set($props.model, $props.formInitialData); if ($props.autoLoad && !$props.formInitialData) {
} else {
await fetch(); await fetch();
} }
@ -90,6 +95,19 @@ onMounted(async () => {
} }
}); });
onBeforeRouteLeave((to, from, next) => {
if (!hasChanges.value) next();
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('Unsaved changes will be lost'),
message: t('Are you sure exit without saving?'),
promise: () => next(),
},
});
});
onUnmounted(() => { onUnmounted(() => {
state.unset($props.model); state.unset($props.model);
}); });
@ -138,17 +156,20 @@ async function save() {
try { try {
const body = $props.mapper ? $props.mapper(formData.value) : formData.value; const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
let response; let response;
if ($props.urlCreate) { if ($props.saveFn) response = await $props.saveFn(body);
response = await axios.post($props.urlCreate, body); else
notify('globals.dataCreated', 'positive'); response = await axios[$props.urlCreate ? 'post' : 'patch'](
} else { $props.urlCreate || $props.urlUpdate || $props.url,
response = await axios.patch($props.urlUpdate || $props.url, body); body
} );
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
emit('onDataSaved', formData.value, response?.data); emit('onDataSaved', formData.value, response?.data);
originalData.value = JSON.parse(JSON.stringify(formData.value)); originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false; hasChanges.value = false;
} catch (err) { } catch (err) {
notify('errors.create', 'negative'); console.error(err);
notify('errors.writeRequest', 'negative');
} }
isLoading.value = false; isLoading.value = false;
} }
@ -249,3 +270,8 @@ watch(formUrl, async () => {
padding: 32px; padding: 32px;
} }
</style> </style>
<i18n>
es:
Unsaved changes will be lost: Los cambios que no haya guardado se perderán
Are you sure exit without saving?: ¿Seguro que quiere salir sin guardar?
</i18n>

View File

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

View File

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

View File

@ -94,16 +94,6 @@ async function send() {
<QSpace /> <QSpace />
<QBtn icon="close" :disable="isLoading" flat round dense v-close-popup /> <QBtn icon="close" :disable="isLoading" flat round dense v-close-popup />
</QCardSection> </QCardSection>
<QCardSection v-if="props.locale">
<QBanner class="bg-amber text-white" rounded dense>
<template #avatar>
<QIcon name="warning" />
</template>
<span
v-html="t('CustomerDefaultLanguage', { locale: t(props.locale) })"
></span>
</QBanner>
</QCardSection>
<QCardSection class="q-pb-xs"> <QCardSection class="q-pb-xs">
<QSelect <QSelect
:label="t('Language')" :label="t('Language')"
@ -184,11 +174,10 @@ async function send() {
<i18n> <i18n>
en: en:
CustomerDefaultLanguage: This customer uses <strong>{locale}</strong> as their default language
templates: templates:
pendingPayment: 'Your order is pending of payment. pendingPayment: 'Your order is pending of payment.
Please, enter the website and make the payment with a credit card. Thank you.' Please, enter the website and make the payment with a credit card. Thank you.'
minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order
{ orderId } of { shipped } to receive it without additional shipping costs.' { orderId } of { shipped } to receive it without additional shipping costs.'
orderChanges: 'Order {orderId} of { shipped }: { changes }' orderChanges: 'Order {orderId} of { shipped }: { changes }'
en: English en: English
@ -197,7 +186,6 @@ en:
pt: Portuguese pt: Portuguese
es: es:
Send SMS: Enviar SMS Send SMS: Enviar SMS
CustomerDefaultLanguage: Este cliente utiliza <strong>{locale}</strong> como idioma por defecto
Language: Idioma Language: Idioma
Phone: Móvil Phone: Móvil
Subject: Asunto Subject: Asunto
@ -205,7 +193,7 @@ es:
templates: templates:
pendingPayment: 'Su pedido está pendiente de pago. pendingPayment: 'Su pedido está pendiente de pago.
Por favor, entre en la página web y efectue el pago con tarjeta. Muchas gracias.' Por favor, entre en la página web y efectue el pago con tarjeta. Muchas gracias.'
minAmount: 'Es necesario un importe mínimo de 50 (Sin IVA) en su pedido minAmount: 'Es necesario un importe mínimo de 50 (Sin IVA) en su pedido
{ orderId } del día { shipped } para recibirlo sin portes adicionales.' { orderId } del día { shipped } para recibirlo sin portes adicionales.'
orderChanges: 'Pedido {orderId} día { shipped }: { changes }' orderChanges: 'Pedido {orderId} día { shipped }: { changes }'
en: Inglés en: Inglés
@ -222,7 +210,7 @@ fr:
templates: templates:
pendingPayment: 'Votre commande est en attente de paiement. pendingPayment: 'Votre commande est en attente de paiement.
Veuillez vous connecter sur le site web et effectuer le paiement par carte. Merci beaucoup.' Veuillez vous connecter sur le site web et effectuer le paiement par carte. Merci beaucoup.'
minAmount: 'Un montant minimum de 50 (TVA non incluse) est requis pour votre commande minAmount: 'Un montant minimum de 50 (TVA non incluse) est requis pour votre commande
{ orderId } du { shipped } afin de la recevoir sans frais de port supplémentaires.' { orderId } du { shipped } afin de la recevoir sans frais de port supplémentaires.'
orderChanges: 'Commande { orderId } du { shipped }: { changes }' orderChanges: 'Commande { orderId } du { shipped }: { changes }'
en: Anglais en: Anglais
@ -239,7 +227,7 @@ pt:
templates: templates:
pendingPayment: 'Seu pedido está pendente de pagamento. pendingPayment: 'Seu pedido está pendente de pagamento.
Por favor, acesse o site e faça o pagamento com cartão. Muito obrigado.' Por favor, acesse o site e faça o pagamento com cartão. Muito obrigado.'
minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido
{ orderId } do dia { shipped } para recebê-lo sem custos de envio adicionais.' { orderId } do dia { shipped } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } dia { shipped }: { changes }' orderChanges: 'Pedido { orderId } dia { shipped }: { changes }'
en: Inglês en: Inglês

View File

@ -188,16 +188,18 @@ const emit = defineEmits(['onFetch']);
.label { .label {
color: var(--vn-label); color: var(--vn-label);
font-size: 12px; font-size: 12px;
width: 47%; ::after {
content: ':';
}
} }
.value { .value {
color: var(--vn-text); color: var(--vn-text);
font-size: 14px; font-size: 14px;
margin-left: 12px; margin-left: 12px;
width: 47%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
text-align: left;
} }
.info { .info {
margin-left: 5px; margin-left: 5px;

View File

@ -1,7 +1,8 @@
<script setup> <script setup>
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router';
import toDate from 'filters/toDate'; import toDate from 'filters/toDate';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
@ -52,6 +53,7 @@ const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']);
const arrayData = useArrayData(props.dataKey, { const arrayData = useArrayData(props.dataKey, {
exprBuilder: props.exprBuilder, exprBuilder: props.exprBuilder,
}); });
const route = useRoute();
const store = arrayData.store; const store = arrayData.store;
const userParams = ref({}); const userParams = ref({});
@ -63,6 +65,18 @@ onMounted(() => {
emit('init', { params: userParams.value }); emit('init', { params: userParams.value });
}); });
watch(
() => route.query.params,
(val) => {
if (!val) {
userParams.value = {};
} else {
const parsedParams = JSON.parse(val);
userParams.value = { ...parsedParams };
}
}
);
const isLoading = ref(false); const isLoading = ref(false);
async function search() { async function search() {
isLoading.value = true; isLoading.value = true;

View File

@ -1,8 +1,9 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { useI18n } from 'vue-i18n';
import { useClipboard } from 'src/composables/useClipboard'; import { useClipboard } from 'src/composables/useClipboard';
const $props = defineProps({ const $props = defineProps({
label: { type: String, default: null }, label: { type: String, default: null },
value: { value: {
@ -13,8 +14,9 @@ const $props = defineProps({
dash: { type: Boolean, default: true }, dash: { type: Boolean, default: true },
copy: { type: Boolean, default: false }, 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(); const { copyText } = useClipboard();
function copyValueText() { function copyValueText() {
@ -54,22 +56,29 @@ function copyValueText() {
</slot> </slot>
</div> </div>
<div class="info" v-if="$props.info"> <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]"> <QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]">
{{ $props.info }} {{ $props.info }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
</div> </div>
<div class="copy" v-if="$props.copy && $props.value" @click="copyValueText()"> <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>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.vn-label-value:hover .copy {
visibility: visible;
cursor: pointer;
}
.copy { .copy {
&:hover { visibility: hidden;
cursor: pointer; }
} .info {
margin-left: 5px;
} }
</style> </style>

View File

@ -1,9 +1,15 @@
<template> <template>
<div id="row"> <div id="row" class="q-gutter-md q-mb-md">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<style lang="scss" scopped> <style lang="scss" scopped>
#row {
display: flex;
> * {
flex: 1;
}
}
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
#row { #row {
flex-direction: column; flex-direction: column;

View File

@ -1,20 +1,21 @@
<script setup> <script setup>
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useState } from 'src/composables/useState';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue';
import CreateDepartmentChild from '../CreateDepartmentChild.vue'; import CreateDepartmentChild from '../CreateDepartmentChild.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useRouter } from 'vue-router';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
const state = useState();
const router = useRouter();
const treeRef = ref(null); const treeRef = ref();
const showCreateNodeFormVal = ref(false); const showCreateNodeFormVal = ref(false);
const creationNodeSelectedId = ref(null); const creationNodeSelectedId = ref(null);
const expanded = ref([]); const expanded = ref([]);
@ -24,30 +25,35 @@ const nodes = ref([{ id: null, name: t('Departments'), sons: true, children: [{}
const fetchedChildrensSet = ref(new Set()); const fetchedChildrensSet = ref(new Set());
const onNodeExpanded = (nodeKeysArray) => { const onNodeExpanded = (nodeKeysArray) => {
// Verificar si el nodo ya fue expandido
if (!fetchedChildrensSet.value.has(nodeKeysArray.at(-1))) { if (!fetchedChildrensSet.value.has(nodeKeysArray.at(-1))) {
fetchedChildrensSet.value.add(nodeKeysArray.at(-1)); fetchedChildrensSet.value.add(nodeKeysArray.at(-1));
fetchNodeLeaves(nodeKeysArray.at(-1)); // Llamar a la función para obtener los nodos hijos fetchNodeLeaves(nodeKeysArray.at(-1));
} }
state.set('Tree', nodeKeysArray);
}; };
const fetchNodeLeaves = async (nodeKey) => { const fetchNodeLeaves = async (nodeKey) => {
try { try {
const node = treeRef.value.getNodeByKey(nodeKey); const node = treeRef.value?.getNodeByKey(nodeKey);
if (!node || node.sons === 0) return; if (!node || node.sons === 0) return;
const params = { parentId: node.id }; const params = { parentId: node.id };
const response = await axios.get('/departments/getLeaves', { params }); const response = await axios.get('/departments/getLeaves', { params });
// Si hay datos en la respuesta y tiene hijos, agregarlos al nodo actual
if (response.data) { if (response.data) {
node.children = response.data; node.children = response.data.map((n) => {
node.children.forEach((node) => { const hasChildrens = n.sons > 0;
if (node.sons) node.children = [{}];
n.children = hasChildrens ? [{}] : null;
n.clickable = true;
return n;
}); });
} }
state.set('Tree', node);
} catch (err) { } catch (err) {
console.error('Error fetching department leaves'); console.error('Error fetching department leaves', err);
throw new Error(); throw new Error();
} }
}; };
@ -84,10 +90,49 @@ const onNodeCreated = async () => {
await fetchNodeLeaves(creationNodeSelectedId.value); await fetchNodeLeaves(creationNodeSelectedId.value);
}; };
const redirectToDepartmentSummary = (id) => { onMounted(async (n) => {
if (!id) return; const tree = [...state.get('Tree'), 1];
router.push({ name: 'DepartmentSummary', params: { id } }); const lastStateTree = state.get('TreeState');
}; if (tree) {
for (let n of tree) {
await fetchNodeLeaves(n);
}
expanded.value = tree;
if (lastStateTree) {
tree.push(lastStateTree);
await fetchNodeLeaves(lastStateTree);
}
}
setTimeout(() => {
if (lastStateTree) {
document.getElementById(lastStateTree).scrollIntoView();
}
}, 1000);
});
function handleEvent(type, event, node) {
const isParent = node.sons > 0;
const lastId = isParent ? node.id : node.parentFk;
switch (type) {
case 'path':
state.set('TreeState', lastId);
node.id && router.push({ path: `/department/department/${node.id}/summary` });
break;
case 'tab':
state.set('TreeState', lastId);
node.id &&
window.open(`#/department/department/${node.id}/summary`, '_blank');
break;
default:
node.id &&
router.push({ path: `#/department/department/${node.id}/summary` });
break;
}
}
</script> </script>
<template> <template>
@ -99,15 +144,27 @@ const redirectToDepartmentSummary = (id) => {
label-key="name" label-key="name"
v-model:expanded="expanded" v-model:expanded="expanded"
@update:expanded="onNodeExpanded($event)" @update:expanded="onNodeExpanded($event)"
:default-expand-all="true"
> >
<template #default-header="{ node }"> <template #default-header="{ node }">
<div <div
class="row justify-between full-width q-pr-md cursor-pointer" :id="node.id"
@click.stop="redirectToDepartmentSummary(node.id)" class="qtr row justify-between full-width q-pr-md cursor-pointer"
> >
<span class="text-uppercase"> <div>
{{ node.name }} <span
</span> @click="handleEvent('row', $event, node)"
class="cursor-pointer"
>
{{ node.name }}
<DepartmentDescriptorProxy :id="node.id" />
</span>
</div>
<div
@click.stop.exact="handleEvent('path', $event, node)"
@click.ctrl.stop="handleEvent('tab', $event, node)"
style="flex-grow: 1; width: 10px"
></div>
<div class="row justify-between" style="max-width: max-content"> <div class="row justify-between" style="max-width: max-content">
<QIcon <QIcon
v-if="node.id" v-if="node.id"
@ -149,6 +206,11 @@ const redirectToDepartmentSummary = (id) => {
</QCard> </QCard>
</template> </template>
<style lang="scss" scoped>
span {
color: $primary;
}
</style>
<i18n> <i18n>
es: es:
Departments: Departamentos Departments: Departamentos

View File

@ -13,6 +13,7 @@
// Tip: Use the "Theme Builder" on Quasar's documentation website. // Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary: #ec8916; $primary: #ec8916;
$primary-light: lighten($primary, 35%);
$secondary: #26a69a; $secondary: #26a69a;
$accent: #9c27b0; $accent: #9c27b0;
$white: #fff; $white: #fff;

View File

@ -24,6 +24,7 @@ export default {
dataCreated: 'Data created', dataCreated: 'Data created',
add: 'Add', add: 'Add',
create: 'Create', create: 'Create',
edit: 'Edit',
save: 'Save', save: 'Save',
remove: 'Remove', remove: 'Remove',
reset: 'Reset', reset: 'Reset',
@ -64,12 +65,23 @@ export default {
markAll: 'Mark all', markAll: 'Mark all',
requiredField: 'Required field', requiredField: 'Required field',
class: 'clase', class: 'clase',
type: 'type', type: 'Type',
reason: 'reason', reason: 'reason',
noResults: 'No results', noResults: 'No results',
system: 'System', system: 'System',
warehouse: 'Warehouse',
company: 'Company',
fieldRequired: 'Field required', fieldRequired: 'Field required',
allowedFilesText: 'Allowed file types: { allowedContentTypes }', 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: { errors: {
statusUnauthorized: 'Access denied', statusUnauthorized: 'Access denied',
@ -77,7 +89,7 @@ export default {
statusBadGateway: 'It seems that the server has fall down', statusBadGateway: 'It seems that the server has fall down',
statusGatewayTimeout: 'Could not contact the server', statusGatewayTimeout: 'Could not contact the server',
userConfig: 'Error fetching user config', userConfig: 'Error fetching user config',
create: 'Error during creation', writeRequest: 'The requested operation could not be completed',
}, },
login: { login: {
title: 'Login', title: 'Login',
@ -166,7 +178,7 @@ export default {
fiscalAddress: 'Fiscal address', fiscalAddress: 'Fiscal address',
fiscalData: 'Fiscal data', fiscalData: 'Fiscal data',
billingData: 'Billing data', billingData: 'Billing data',
consignee: 'Consignee', consignee: 'Default consignee',
businessData: 'Business data', businessData: 'Business data',
financialData: 'Financial data', financialData: 'Financial data',
customerId: 'Customer ID', customerId: 'Customer ID',
@ -219,6 +231,8 @@ export default {
recoverySince: 'Recovery since', recoverySince: 'Recovery since',
businessType: 'Business Type', businessType: 'Business Type',
city: 'City', city: 'City',
rating: 'Rating',
recommendCredit: 'Recommended credit',
}, },
basicData: { basicData: {
socialName: 'Fiscal name', socialName: 'Fiscal name',
@ -273,6 +287,7 @@ export default {
basicData: 'Basic data', basicData: 'Basic data',
buys: 'Buys', buys: 'Buys',
notes: 'Notes', notes: 'Notes',
dms: 'File management',
log: 'Log', log: 'Log',
create: 'Create', create: 'Create',
latestBuys: 'Latest buys', latestBuys: 'Latest buys',
@ -344,7 +359,6 @@ export default {
reference: 'Reference', reference: 'Reference',
observations: 'Observations', observations: 'Observations',
item: 'Item', item: 'Item',
description: 'Description',
size: 'Size', size: 'Size',
packing: 'Packing', packing: 'Packing',
grouping: 'Grouping', grouping: 'Grouping',
@ -359,7 +373,6 @@ export default {
}, },
notes: { notes: {
observationType: 'Observation type', observationType: 'Observation type',
description: 'Description',
}, },
descriptor: { descriptor: {
agency: 'Agency', agency: 'Agency',
@ -372,7 +385,6 @@ export default {
packing: 'Packing', packing: 'Packing',
grouping: 'Grouping', grouping: 'Grouping',
quantity: 'Quantity', quantity: 'Quantity',
description: 'Description',
size: 'Size', size: 'Size',
tags: 'Tags', tags: 'Tags',
type: 'Type', type: 'Type',
@ -458,7 +470,6 @@ export default {
visible: 'Visible', visible: 'Visible',
available: 'Available', available: 'Available',
quantity: 'Quantity', quantity: 'Quantity',
description: 'Description',
price: 'Price', price: 'Price',
discount: 'Discount', discount: 'Discount',
packing: 'Packing', packing: 'Packing',
@ -531,7 +542,6 @@ export default {
landed: 'Landed', landed: 'Landed',
quantity: 'Quantity', quantity: 'Quantity',
claimed: 'Claimed', claimed: 'Claimed',
description: 'Description',
price: 'Price', price: 'Price',
discount: 'Discount', discount: 'Discount',
total: 'Total', total: 'Total',
@ -793,7 +803,6 @@ export default {
orderTicketList: 'Order Ticket List', orderTicketList: 'Order Ticket List',
details: 'Details', details: 'Details',
item: 'Item', item: 'Item',
description: 'Description',
quantity: 'Quantity', quantity: 'Quantity',
price: 'Price', price: 'Price',
amount: 'Amount', amount: 'Amount',
@ -1138,7 +1147,6 @@ export default {
warehouse: 'Warehouse', warehouse: 'Warehouse',
travelFileDescription: 'Travel id { travelId }', travelFileDescription: 'Travel id { travelId }',
file: 'File', file: 'File',
description: 'Description',
}, },
}, },
item: { item: {
@ -1172,7 +1180,6 @@ export default {
clone: 'Clone', clone: 'Clone',
openCard: 'View', openCard: 'View',
openSummary: 'Summary', openSummary: 'Summary',
viewDescription: 'Description',
}, },
cardDescriptor: { cardDescriptor: {
mainList: 'Main list', mainList: 'Main list',

View File

@ -24,6 +24,7 @@ export default {
dataCreated: 'Datos creados', dataCreated: 'Datos creados',
add: 'Añadir', add: 'Añadir',
create: 'Crear', create: 'Crear',
edit: 'Modificar',
save: 'Guardar', save: 'Guardar',
remove: 'Eliminar', remove: 'Eliminar',
reset: 'Restaurar', reset: 'Restaurar',
@ -64,12 +65,23 @@ export default {
markAll: 'Marcar todo', markAll: 'Marcar todo',
requiredField: 'Campo obligatorio', requiredField: 'Campo obligatorio',
class: 'clase', class: 'clase',
type: 'tipo', type: 'Tipo',
reason: 'motivo', reason: 'motivo',
noResults: 'Sin resultados', noResults: 'Sin resultados',
system: 'Sistema', system: 'Sistema',
warehouse: 'Almacén',
company: 'Empresa',
fieldRequired: 'Campo requerido', fieldRequired: 'Campo requerido',
allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }', 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: { errors: {
statusUnauthorized: 'Acceso denegado', statusUnauthorized: 'Acceso denegado',
@ -77,7 +89,7 @@ export default {
statusBadGateway: 'Parece ser que el servidor ha caído', statusBadGateway: 'Parece ser que el servidor ha caído',
statusGatewayTimeout: 'No se ha podido contactar con el servidor', statusGatewayTimeout: 'No se ha podido contactar con el servidor',
userConfig: 'Error al obtener configuración de usuario', userConfig: 'Error al obtener configuración de usuario',
create: 'Error al crear', writeRequest: 'No se pudo completar la operación solicitada',
}, },
login: { login: {
title: 'Inicio de sesión', title: 'Inicio de sesión',
@ -165,7 +177,7 @@ export default {
fiscalAddress: 'Dirección fiscal', fiscalAddress: 'Dirección fiscal',
fiscalData: 'Datos fiscales', fiscalData: 'Datos fiscales',
billingData: 'Datos de facturación', billingData: 'Datos de facturación',
consignee: 'Consignatario', consignee: 'Consignatario pred.',
businessData: 'Datos comerciales', businessData: 'Datos comerciales',
financialData: 'Datos financieros', financialData: 'Datos financieros',
customerId: 'ID cliente', customerId: 'ID cliente',
@ -218,6 +230,8 @@ export default {
recoverySince: 'Recobro desde', recoverySince: 'Recobro desde',
businessType: 'Tipo de negocio', businessType: 'Tipo de negocio',
city: 'Población', city: 'Población',
rating: 'Clasificación',
recommendCredit: 'Crédito recomendado',
}, },
basicData: { basicData: {
socialName: 'Nombre fiscal', socialName: 'Nombre fiscal',
@ -272,6 +286,7 @@ export default {
basicData: 'Datos básicos', basicData: 'Datos básicos',
buys: 'Compras', buys: 'Compras',
notes: 'Notas', notes: 'Notas',
dms: 'Gestión documental',
log: 'Historial', log: 'Historial',
create: 'Crear', create: 'Crear',
latestBuys: 'Últimas compras', latestBuys: 'Últimas compras',
@ -343,7 +358,6 @@ export default {
reference: 'Referencia', reference: 'Referencia',
observations: 'Observaciónes', observations: 'Observaciónes',
item: 'Artículo', item: 'Artículo',
description: 'Descripción',
size: 'Medida', size: 'Medida',
packing: 'Packing', packing: 'Packing',
grouping: 'Grouping', grouping: 'Grouping',
@ -358,7 +372,6 @@ export default {
}, },
notes: { notes: {
observationType: 'Tipo de observación', observationType: 'Tipo de observación',
description: 'Descripción',
}, },
descriptor: { descriptor: {
agency: 'Agencia', agency: 'Agencia',
@ -371,7 +384,6 @@ export default {
packing: 'Packing', packing: 'Packing',
grouping: 'Grouping', grouping: 'Grouping',
quantity: 'Cantidad', quantity: 'Cantidad',
description: 'Descripción',
size: 'Medida', size: 'Medida',
tags: 'Etiquetas', tags: 'Etiquetas',
type: 'Tipo', type: 'Tipo',
@ -457,7 +469,6 @@ export default {
visible: 'Visible', visible: 'Visible',
available: 'Disponible', available: 'Disponible',
quantity: 'Cantidad', quantity: 'Cantidad',
description: 'Descripción',
price: 'Precio', price: 'Precio',
discount: 'Descuento', discount: 'Descuento',
packing: 'Encajado', packing: 'Encajado',
@ -530,7 +541,6 @@ export default {
landed: 'Entregado', landed: 'Entregado',
quantity: 'Cantidad', quantity: 'Cantidad',
claimed: 'Reclamado', claimed: 'Reclamado',
description: 'Descripción',
price: 'Precio', price: 'Precio',
discount: 'Descuento', discount: 'Descuento',
total: 'Total', total: 'Total',
@ -701,7 +711,6 @@ export default {
orderTicketList: 'Tickets del pedido', orderTicketList: 'Tickets del pedido',
details: 'Detalles', details: 'Detalles',
item: 'Item', item: 'Item',
description: 'Descripción',
quantity: 'Cantidad', quantity: 'Cantidad',
price: 'Precio', price: 'Precio',
amount: 'Monto', amount: 'Monto',
@ -1138,7 +1147,6 @@ export default {
warehouse: 'Almacén', warehouse: 'Almacén',
travelFileDescription: 'Id envío { travelId }', travelFileDescription: 'Id envío { travelId }',
file: 'Fichero', file: 'Fichero',
description: 'Descripción',
}, },
}, },
item: { item: {
@ -1172,7 +1180,6 @@ export default {
clone: 'Clonar', clone: 'Clonar',
openCard: 'Ficha', openCard: 'Ficha',
openSummary: 'Detalles', openSummary: 'Detalles',
viewDescription: 'Descripción',
}, },
cardDescriptor: { cardDescriptor: {
mainList: 'Listado principal', mainList: 'Listado principal',

View File

@ -116,7 +116,13 @@ function navigate(event, id) {
</template> </template>
<template #actions> <template #actions>
<QBtn <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 @click.stop
class="bg-vn-dark" class="bg-vn-dark"
outline outline

View File

@ -122,7 +122,7 @@ const creditWarning = computed(() => {
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<a class="header link" :href="clientUrl + `fiscal-data`" link> <a class="header link" :href="clientUrl + `fiscal-data`" link>
{{ t('customer.summary.fiscalAddress') }} {{ t('customer.summary.fiscalData') }}
<QIcon name="open_in_new" color="primary" /> <QIcon name="open_in_new" color="primary" />
</a> </a>
<VnLv <VnLv
@ -235,7 +235,8 @@ const creditWarning = computed(() => {
link link
> >
{{ t('customer.summary.financialData') }} {{ 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> </a>
<VnLv <VnLv
:label="t('customer.summary.risk')" :label="t('customer.summary.risk')"
@ -276,7 +277,30 @@ const creditWarning = computed(() => {
:label="t('customer.summary.recoverySince')" :label="t('customer.summary.recoverySince')"
:value="toDate(entity.recovery.started)" :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> </QCard>
</template> </template>
</CardSummary> </CardSummary>
</template> </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>

View File

@ -13,7 +13,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { dashIfEmpty, toDate } from 'src/filters'; import { toDate } from 'src/filters';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
@ -477,17 +477,11 @@ const stopEventPropagation = (event, col) => {
event.stopPropagation(); event.stopPropagation();
}; };
const navigateToTravelId = (id) => { const navigateToTravelId = (id) => router.push({ path: `/customer/${id}` });
router.push({ path: `/customer/${id}` });
};
const selectCustomerId = (id) => { const selectCustomerId = (id) => (selectedCustomerId.value = id);
selectedCustomerId.value = id;
};
const selectSalesPersonId = (id) => { const selectSalesPersonId = (id) => (selectedSalesPersonId.value = id);
selectedSalesPersonId.value = id;
};
</script> </script>
<template> <template>
@ -521,37 +515,50 @@ const selectSalesPersonId = (id) => {
class="full-width q-mt-md" class="full-width q-mt-md"
row-key="id" row-key="id"
:visible-columns="visibleColumns" :visible-columns="visibleColumns"
@row-click="(evt, row, id) => navigateToTravelId(row.id)"
> >
<template #body="props"> <template #body-cell="{ col, value }">
<QTr <QTd @click="stopEventPropagation($event, col)">
:props="props" {{ value }}
@click="navigateToTravelId(props.row.id)" </QTd>
class="cursor-pointer" </template>
> <template #body-cell-id="props">
<QTd <QTd @click="stopEventPropagation($event, props.col)">
v-for="col in props.cols" <component
:key="col.name" :is="tableColumnComponents[props.col.name].component"
:props="props" class="col-content"
@click="stopEventPropagation($event, col)" v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
> >
<component <CustomerDescriptorProxy :id="props.row.id" />
:is="tableColumnComponents[col.name].component" {{ props.row.id }}
class="col-content" </component>
v-bind="tableColumnComponents[col.name].props(props)" </QTd>
@click="tableColumnComponents[col.name].event(props)" </template>
> <template #body-cell-salesPersonFk="props">
{{ dashIfEmpty(col.value) }} <QTd @click="stopEventPropagation($event, props.col)">
<WorkerDescriptorProxy <component
v-if="props.row.salesPersonFk" v-if="props.row.salesPerson"
:id="selectedSalesPersonId" class="col-content"
/> :is="tableColumnComponents[props.col.name].component"
<CustomerDescriptorProxy v-bind="tableColumnComponents[props.col.name].props(props)"
v-if="props.row.id" @click="tableColumnComponents[props.col.name].event(props)"
:id="selectedCustomerId" >
/> <WorkerDescriptorProxy :id="props.row.salesPersonFk" />
</component> {{ props.row.salesPerson }}
</QTd> </component>
</QTr> <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> </template>
</QTable> </QTable>
</QPage> </QPage>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import VnLocation from 'src/components/common/VnLocation.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
@ -20,13 +20,9 @@ const router = useRouter();
const formInitialData = reactive({ isDefaultAddress: false }); const formInitialData = reactive({ isDefaultAddress: false });
const townsFetchDataRef = ref(null);
const postcodeFetchDataRef = ref(null);
const urlCreate = ref(''); const urlCreate = ref('');
const postcodesOptions = ref([]); const postcodesOptions = ref([]);
const citiesLocationOptions = ref([]);
const provincesLocationOptions = ref([]);
const agencyModes = ref([]); const agencyModes = ref([]);
const incoterms = ref([]); const incoterms = ref([]);
const customsAgents = ref([]); const customsAgents = ref([]);
@ -36,14 +32,6 @@ onBeforeMount(() => {
getCustomsAgents(); getCustomsAgents();
}); });
const onPostcodeCreated = async ({ code, provinceFk, townFk }, formData) => {
await postcodeFetchDataRef.value.fetch();
await townsFetchDataRef.value.fetch();
formData.postalCode = code;
formData.provinceFk = provinceFk;
formData.city = citiesLocationOptions.value.find((town) => town.id === townFk).name;
};
const getCustomsAgents = async () => { const getCustomsAgents = async () => {
const { data } = await axios.get('CustomsAgents'); const { data } = await axios.get('CustomsAgents');
customsAgents.value = data; customsAgents.value = data;
@ -61,26 +49,16 @@ const toCustomerConsignees = () => {
}, },
}); });
}; };
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postalCode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script> </script>
<template> <template>
<FetchData
@on-fetch="(data) => (postcodesOptions = data)"
auto-load
ref="postcodeFetchDataRef"
url="Postcodes/location"
/>
<FetchData
@on-fetch="(data) => (citiesLocationOptions = data)"
auto-load
ref="townsFetchDataRef"
url="Towns/location"
/>
<FetchData
@on-fetch="(data) => (provincesLocationOptions = data)"
auto-load
url="Provinces/location"
/>
<fetch-data <fetch-data
@on-fetch="(data) => (agencyModes = data)" @on-fetch="(data) => (agencyModes = data)"
auto-load auto-load
@ -113,83 +91,17 @@ const toCustomerConsignees = () => {
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnSelectDialog <VnLocation
:label="t('Postcode')"
:options="postcodesOptions"
:roles-allowed-to-create="['deliveryAssistant']"
:rules="validate('Worker.postcode')" :rules="validate('Worker.postcode')"
hide-selected :roles-allowed-to-create="['deliveryAssistant']"
option-label="code" :options="postcodesOptions"
option-value="code" v-model="data.location"
v-model="data.postalCode" @update:model-value="(location) => handleLocation(data, location)"
> ></VnLocation>
<template #form>
<CustomerCreateNewPostcode
@on-data-saved="onPostcodeCreated($event, data)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel>{{ scope.opt.code }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt.code }} -
{{ scope.opt.town.name }}
({{ scope.opt.town.province.name }},
{{ scope.opt.town.province.country.country }})
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectDialog>
</div>
<div class="col">
<!-- ciudades -->
<VnSelectFilter
:label="t('City')"
:options="citiesLocationOptions"
hide-selected
option-label="name"
option-value="name"
v-model="data.city"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.name }}</QItemLabel>
<QItemLabel caption>
{{
`${scope.opt.name}, ${scope.opt.province.name} (${scope.opt.province.country.country})`
}}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div> </div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Province')"
:options="provincesLocationOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.name} (${scope.opt.country.country})`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col"> <div class="col">
<VnSelectFilter <VnSelectFilter
:label="t('Agency')" :label="t('Agency')"

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import VnLocation from 'src/components/common/VnLocation.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
@ -17,13 +17,8 @@ import CustomsNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCus
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const townsFetchDataRef = ref(null);
const postcodeFetchDataRef = ref(null);
const urlUpdate = ref(''); const urlUpdate = ref('');
const postcodesOptions = ref([]); const postcodesOptions = ref([]);
const citiesLocationOptions = ref([]);
const provincesLocationOptions = ref([]);
const agencyModes = ref([]); const agencyModes = ref([]);
const incoterms = ref([]); const incoterms = ref([]);
const customsAgents = ref([]); const customsAgents = ref([]);
@ -34,14 +29,6 @@ onBeforeMount(() => {
urlUpdate.value = `Clients/${route.params.id}/updateAddress/${route.params.consigneeId}`; urlUpdate.value = `Clients/${route.params.id}/updateAddress/${route.params.consigneeId}`;
}); });
const onPostcodeCreated = async ({ code, provinceFk, townFk }, formData) => {
await postcodeFetchDataRef.value.fetch();
await townsFetchDataRef.value.fetch();
formData.postalCode = code;
formData.provinceFk = provinceFk;
formData.city = citiesLocationOptions.value.find((town) => town.id === townFk).name;
};
const getData = async (observations) => { const getData = async (observations) => {
observationTypes.value = observations; observationTypes.value = observations;
@ -97,26 +84,16 @@ const onDataSaved = () => {
}; };
axios.post('AddressObservations/crud', payload); axios.post('AddressObservations/crud', payload);
}; };
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postalCode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script> </script>
<template> <template>
<FetchData
ref="postcodeFetchDataRef"
@on-fetch="(data) => (postcodesOptions = data)"
auto-load
url="Postcodes/location"
/>
<FetchData
ref="townsFetchDataRef"
@on-fetch="(data) => (citiesLocationOptions = data)"
auto-load
url="Towns/location"
/>
<FetchData
@on-fetch="(data) => (provincesLocationOptions = data)"
auto-load
url="Provinces/location"
/>
<fetch-data <fetch-data
@on-fetch="(data) => (agencyModes = data)" @on-fetch="(data) => (agencyModes = data)"
auto-load auto-load
@ -168,83 +145,17 @@ const onDataSaved = () => {
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnSelectDialog <VnLocation
:label="t('Postcode')"
:options="postcodesOptions"
:roles-allowed-to-create="['deliveryAssistant']"
:rules="validate('Worker.postcode')" :rules="validate('Worker.postcode')"
hide-selected :roles-allowed-to-create="['deliveryAssistant']"
option-label="code" :options="postcodesOptions"
option-value="code" v-model="data.location"
v-model="data.postalCode" @update:model-value="(location) => handleLocation(data, location)"
> ></VnLocation>
<template #form>
<CustomerCreateNewPostcode
@on-data-saved="onPostcodeCreated($event, data)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel>{{ scope.opt.code }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt.code }} -
{{ scope.opt.town.name }}
({{ scope.opt.town.province.name }},
{{ scope.opt.town.province.country.country }})
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectDialog>
</div>
<div class="col">
<!-- ciudades -->
<VnSelectFilter
:label="t('City')"
:options="citiesLocationOptions"
hide-selected
option-label="name"
option-value="name"
v-model="data.city"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.name }}</QItemLabel>
<QItemLabel caption>
{{
`${scope.opt.name}, ${scope.opt.province.name} (${scope.opt.province.country.country})`
}}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div> </div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Province')"
:options="provincesLocationOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
`${scope.opt.name} (${scope.opt.country.country})`
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col"> <div class="col">
<VnSelectFilter <VnSelectFilter
:label="t('Agency')" :label="t('Agency')"

View File

@ -0,0 +1,21 @@
<script setup>
import DepartmentDescriptor from './DepartmentDescriptor.vue';
import DepartmentSummaryDialog from './DepartmentSummaryDialog.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<QPopupProxy>
<DepartmentDescriptor
v-if="$props.id"
:id="$props.id"
:summary="DepartmentSummaryDialog"
/>
</QPopupProxy>
</template>

View File

@ -0,0 +1,29 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import DepartmentSummary from './DepartmentSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<QDialog ref="dialogRef" @hide="onDialogHide">
<DepartmentSummary v-if="$props.id" :id="$props.id" />
</QDialog>
</template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -44,7 +44,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
}, },
{ {
label: t('entry.buys.description'), label: t('globals.description'),
name: 'description', name: 'description',
field: 'description', field: 'description',
align: 'left', align: 'left',
@ -214,7 +214,7 @@ const redirectToBuysView = () => {
class="cursor-pointer" class="cursor-pointer"
@click="inputFileRef.pickFiles()" @click="inputFileRef.pickFiles()"
> >
<QTooltip>{{ t('Select a file') }}</QTooltip> <QTooltip>{{ t('globals.selectFile') }}</QTooltip>
</QIcon> </QIcon>
</template> </template>
</QFile> </QFile>
@ -292,6 +292,6 @@ const redirectToBuysView = () => {
<i18n> <i18n>
es: 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 Some of the imported buys does not have an item: Algunas de las compras importadas no tienen un artículo
</i18n> </i18n>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

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

View File

@ -50,25 +50,23 @@ onMounted(() => {
:key="index" :key="index"
class="row q-gutter-md q-mb-md" class="row q-gutter-md q-mb-md"
> >
<div class="col-3"> <VnSelectFilter
<VnSelectFilter :label="t('entry.notes.observationType')"
:label="t('entry.notes.observationType')" v-model="row.observationTypeFk"
v-model="row.observationTypeFk" :options="entryObservationsOptions"
:options="entryObservationsOptions" :disable="!!row.id"
:disable="!!row.id" option-label="description"
option-label="description" option-value="id"
option-value="id" hide-selected
hide-selected />
/>
</div> <VnInput
<div class="col"> :label="t('globals.description')"
<VnInput v-model="row.description"
:label="t('entry.notes.description')" :rules="validate('EntryObservation.description')"
v-model="row.description" />
:rules="validate('EntryObservation.description')"
/> <div class="row justify-center items-center">
</div>
<div class="col-1 row justify-center items-center">
<QIcon <QIcon
name="delete" name="delete"
size="sm" size="sm"
@ -82,19 +80,17 @@ onMounted(() => {
</QIcon> </QIcon>
</div> </div>
</VnRow> </VnRow>
<VnRow> <QIcon
<QIcon name="add"
name="add" size="sm"
size="sm" class="cursor-pointer"
class="cursor-pointer" color="primary"
color="primary" @click="entryObservationsRef.insert()"
@click="entryObservationsRef.insert()" >
> <QTooltip>
<QTooltip> {{ t('Add note') }}
{{ t('Add note') }} </QTooltip>
</QTooltip> </QIcon>
</QIcon>
</VnRow>
</QCard> </QCard>
</template> </template>
</CrudModel> </CrudModel>

View File

@ -39,30 +39,47 @@ onMounted(async () => {
const tableColumnComponents = { const tableColumnComponents = {
quantity: { quantity: {
component: () => 'span', component: () => 'span',
props: () => {},
}, },
stickers: { stickers: {
component: () => 'span', component: () => 'span',
props: () => {},
event: () => {},
}, },
packagingFk: { packagingFk: {
component: () => 'span', component: () => 'span',
props: () => {},
event: () => {},
}, },
weight: { weight: {
component: () => 'span', component: () => 'span',
props: () => {},
event: () => {},
}, },
packing: { packing: {
component: () => 'span', component: () => 'span',
props: () => {},
event: () => {},
}, },
grouping: { grouping: {
component: () => 'span', component: () => 'span',
props: () => {},
event: () => {},
}, },
buyingValue: { buyingValue: {
component: () => 'span', component: () => 'span',
props: () => {},
event: () => {},
}, },
amount: { amount: {
component: () => 'span', component: () => 'span',
props: () => {},
event: () => {},
}, },
pvp: { pvp: {
component: () => 'span', component: () => 'span',
props: () => {},
event: () => {},
}, },
}; };
@ -162,125 +179,82 @@ const fetchEntryBuys = async () => {
{{ t('globals.summary.basicData') }} {{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" color="primary" /> <QIcon name="open_in_new" color="primary" />
</a> </a>
<VnRow>
<div class="col"> <VnLv :label="t('entry.summary.commission')" :value="entry.commission" />
<VnLv
:label="t('entry.summary.commission')" <VnLv :label="t('entry.summary.currency')" :value="entry.currency.name" />
:value="entry.commission"
/> <VnLv :label="t('entry.summary.company')" :value="entry.company.code" />
</div>
<div class="col"> <VnLv :label="t('entry.summary.reference')" :value="entry.reference" />
<VnLv
:label="t('entry.summary.currency')" <VnLv
:value="entry.currency.name" :label="t('entry.summary.invoiceNumber')"
/> :value="entry.invoiceNumber"
</div> />
<div class="col">
<VnLv <VnLv :label="t('entry.summary.ordered')" :value="entry.isOrdered" />
:label="t('entry.summary.company')"
:value="entry.company.code" <VnLv :label="t('entry.summary.confirmed')" :value="entry.isConfirmed" />
/>
</div> <VnLv :label="t('entry.summary.booked')" :value="entry.isBooked" />
<div class="col">
<VnLv <VnLv :label="t('entry.summary.raid')" :value="entry.isRaid" />
:label="t('entry.summary.reference')"
:value="entry.reference" <VnLv
/> :label="t('entry.summary.excludedFromAvailable')"
</div> :value="entry.isExcludedFromAvailable"
<div class="col"> />
<VnLv
:label="t('entry.summary.invoiceNumber')"
:value="entry.invoiceNumber"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.ordered')"
:value="entry.isOrdered"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.confirmed')"
:value="entry.isConfirmed"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.booked')"
:value="entry.isBooked"
/>
</div>
<div class="col">
<VnLv :label="t('entry.summary.raid')" :value="entry.isRaid" />
</div>
<div class="col">
<VnLv
:label="t('entry.summary.excludedFromAvailable')"
:value="entry.isExcludedFromAvailable"
/>
</div>
</VnRow>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<a class="header link" :href="entryUrl"> <a class="header link" :href="entryUrl">
{{ t('Travel data') }} {{ t('Travel data') }}
<QIcon name="open_in_new" color="primary" /> <QIcon name="open_in_new" color="primary" />
</a> </a>
<VnRow>
<div class="col"> <VnLv :label="t('entry.summary.travelReference')">
<VnLv :label="t('entry.summary.travelReference')"> <template #value>
<template #value> <span class="link">
<span class="link"> {{ entry.travel.ref }}
{{ entry.travel.ref }} <TravelDescriptorProxy :id="entry.travel.id" />
<TravelDescriptorProxy :id="entry.travel.id" /> </span>
</span> </template>
</template> </VnLv>
</VnLv>
</div> <VnLv
<div class="col"> :label="t('entry.summary.travelAgency')"
<VnLv :value="entry.travel.agency.name"
:label="t('entry.summary.travelAgency')" />
:value="entry.travel.agency.name"
/> <VnLv
</div> :label="t('entry.summary.travelShipped')"
<div class="col"> :value="toDate(entry.travel.shipped)"
<VnLv />
:label="t('entry.summary.travelShipped')"
:value="toDate(entry.travel.shipped)" <VnLv
/> :label="t('entry.summary.travelWarehouseOut')"
</div> :value="entry.travel.warehouseOut.name"
<div class="col"> />
<VnLv
:label="t('entry.summary.travelWarehouseOut')" <VnLv
:value="entry.travel.warehouseOut.name" :label="t('entry.summary.travelDelivered')"
/> :value="entry.travel.isDelivered"
</div> />
<div class="col">
<VnLv <VnLv
:label="t('entry.summary.travelDelivered')" :label="t('entry.summary.travelLanded')"
:value="entry.travel.isDelivered" :value="toDate(entry.travel.landed)"
/> />
</div>
<div class="col"> <VnLv
<VnLv :label="t('entry.summary.travelWarehouseIn')"
:label="t('entry.summary.travelLanded')" :value="entry.travel.warehouseIn.name"
:value="toDate(entry.travel.landed)" />
/>
</div> <VnLv
<div class="col"> :label="t('entry.summary.travelReceived')"
<VnLv :value="entry.travel.isReceived"
:label="t('entry.summary.travelWarehouseIn')" />
:value="entry.travel.warehouseIn.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelReceived')"
:value="entry.travel.isReceived"
/>
</div>
</VnRow>
</QCard> </QCard>
<QCard class="vn-two" style="min-width: 100%"> <QCard class="vn-two" style="min-width: 100%">
<a class="header"> <a class="header">
@ -297,7 +271,10 @@ const fetchEntryBuys = async () => {
<QTr no-hover> <QTr no-hover>
<QTd v-for="col in cols" :key="col.name"> <QTd v-for="col in cols" :key="col.name">
<component <component
:is="tableColumnComponents[col.name].component()" :is="tableColumnComponents[col.name].component(props)"
v-bind="tableColumnComponents[col.name].props(props)"
@click="tableColumnComponents[col.name].event(props)"
class="col-content"
> >
<template <template
v-if=" v-if="

View File

@ -59,7 +59,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
}, },
{ {
label: t('entry.latestBuys.description'), label: t('globals.description'),
field: 'description', field: 'description',
name: 'description', name: 'description',
align: 'left', align: 'left',
@ -214,7 +214,7 @@ const editTableCellFormFieldsOptions = [
{ field: 'grouping', label: t('entry.latestBuys.grouping') }, { field: 'grouping', label: t('entry.latestBuys.grouping') },
{ field: 'packageValue', label: t('entry.latestBuys.packageValue') }, { field: 'packageValue', label: t('entry.latestBuys.packageValue') },
{ field: 'weight', label: t('entry.latestBuys.weight') }, { 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: 'size', label: t('entry.latestBuys.size') },
{ field: 'weightByPiece', label: t('entry.latestBuys.weightByPiece') }, { field: 'weightByPiece', label: t('entry.latestBuys.weightByPiece') },
{ field: 'packingOut', label: t('entry.latestBuys.packingOut') }, { field: 'packingOut', label: t('entry.latestBuys.packingOut') },

View File

@ -174,7 +174,12 @@ async function upsert() {
@on-fetch="(data) => (userConfig = data)" @on-fetch="(data) => (userConfig = data)"
auto-load 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 }"> <template #form="{ data }">
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
@ -509,7 +514,7 @@ async function upsert() {
@click="inputFileRef.pickFiles()" @click="inputFileRef.pickFiles()"
> >
<QTooltip> <QTooltip>
{{ t('Select a file') }} {{ t('globals.selectFile') }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<QBtn icon="info" flat round padding="xs"> <QBtn icon="info" flat round padding="xs">
@ -618,7 +623,7 @@ async function upsert() {
@click="inputFileRef.pickFiles()" @click="inputFileRef.pickFiles()"
> >
<QTooltip> <QTooltip>
{{ t('Select a file') }} {{ t('globals.selectFile') }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<QBtn icon="info" flat round padding="xs"> <QBtn icon="info" flat round padding="xs">
@ -687,7 +692,6 @@ async function upsert() {
Generate identifier for original file: Generar identificador para archivo original Generate identifier for original file: Generar identificador para archivo original
File: Fichero File: Fichero
Create document: Crear documento Create document: Crear documento
Select a file: Seleccione un fichero
Allowed content types: Tipos de archivo permitidos Allowed content types: Tipos de archivo permitidos
The company can't be empty: La empresa no puede estar vacía 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 The warehouse can't be empty: El almacén no puede estar vacío

View File

@ -31,7 +31,7 @@ const detailsColumns = ref([
}, },
{ {
name: 'description', name: 'description',
label: t('order.summary.description'), label: t('globals.description'),
field: (row) => row?.item?.name, field: (row) => row?.item?.name,
}, },
{ {
@ -167,7 +167,7 @@ const detailsColumns = ref([
<template #header="props"> <template #header="props">
<QTr :props="props"> <QTr :props="props">
<QTh auto-width>{{ t('order.summary.item') }}</QTh> <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.quantity') }}</QTh>
<QTh auto-width>{{ t('order.summary.price') }}</QTh> <QTh auto-width>{{ t('order.summary.price') }}</QTh>
<QTh auto-width>{{ t('order.summary.amount') }}</QTh> <QTh auto-width>{{ t('order.summary.amount') }}</QTh>

View File

@ -199,7 +199,7 @@ const openBuscaman = async (route, ticket) => {
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<div class="header"> <div class="header">
{{ t('route.summary.description') }} {{ t('globals.description') }}
</div> </div>
<p> <p>
{{ dashIfEmpty(entity?.route?.description) }} {{ dashIfEmpty(entity?.route?.description) }}

View File

@ -6,7 +6,7 @@ import { useStateStore } from 'stores/useStateStore';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'components/ui/VnLv.vue'; import VnLv from 'components/ui/VnLv.vue';
import ShelvingFilter from 'pages/Shelving/Card/ShelvingFilter.vue'; import ShelvingFilter from 'pages/Shelving/Card/ShelvingFilter.vue';
import VnUserLink from "components/ui/VnUserLink.vue"; import VnUserLink from 'components/ui/VnUserLink.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {

View File

@ -10,6 +10,7 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import CustomerCreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue'; import CustomerCreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -55,27 +56,16 @@ onMounted(() => {
updateAddressForm(addressData); updateAddressForm(addressData);
} }
}); });
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postalCode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script> </script>
<template> <template>
<FetchData
ref="postcodeFetchDataRef"
url="Postcodes/location"
@on-fetch="(data) => (postcodesOptions = data)"
auto-load
/>
<FetchData
ref="provincesFetchDataRef"
@on-fetch="(data) => (provincesOptions = data)"
auto-load
url="Provinces"
/>
<FetchData
ref="townsFetchDataRef"
@on-fetch="(data) => (townsLocationOptions = data)"
auto-load
url="Towns/location"
/>
<QPage> <QPage>
<FormModel <FormModel
model="supplierAddresses" model="supplierAddresses"
@ -104,59 +94,15 @@ onMounted(() => {
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<VnSelectDialog <VnLocation
v-model="data.postalCode" :rules="validate('Worker.postcode')"
:label="t('supplier.addresses.postcode')"
:rules="validate('supplierAddress.postcode')"
:roles-allowed-to-create="['deliveryAssistant']" :roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions" :options="postcodesOptions"
option-label="code" v-model="data.location"
option-value="code" @update:model-value="
hide-selected (location) => handleLocation(data, location)
> "
<template #form> ></VnLocation>
<CustomerCreateNewPostcode
@on-data-saved="onPostcodeCreated($event)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel>{{ scope.opt.code }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt.code }} -
{{ scope.opt.town.name }} ({{
scope.opt.town.province.name
}},
{{
scope.opt.town.province.country.country
}})</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectDialog>
</div>
<div class="col">
<VnSelectFilter
:label="t('supplier.addresses.city')"
:options="townsLocationOptions"
v-model="data.city"
hide-selected
option-label="name"
option-value="id"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('supplier.addresses.province')"
:options="provincesOptions"
hide-selected
map-options
option-label="name"
option-value="id"
v-model="data.provinceFk"
/>
</div> </div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">

View File

@ -21,7 +21,7 @@ const postcodesOptions = ref([]);
function handleLocation(data, location) { function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {}; const { town, code, provinceFk, countryFk } = location ?? {};
data.postcode = code; data.postCode = code;
data.city = town; data.city = town;
data.provinceFk = provinceFk; data.provinceFk = provinceFk;
data.countryFk = countryFk; data.countryFk = countryFk;

View File

@ -270,7 +270,7 @@ async function changeState(value) {
<QTh auto-width>{{ t('ticket.summary.visible') }}</QTh> <QTh auto-width>{{ t('ticket.summary.visible') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.available') }}</QTh> <QTh auto-width>{{ t('ticket.summary.available') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.quantity') }}</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.price') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.discount') }}</QTh> <QTh auto-width>{{ t('ticket.summary.discount') }}</QTh>
<QTh auto-width>{{ t('globals.amount') }}</QTh> <QTh auto-width>{{ t('globals.amount') }}</QTh>
@ -425,7 +425,7 @@ async function changeState(value) {
<template #header="props"> <template #header="props">
<QTr :props="props"> <QTr :props="props">
<QTh auto-width>{{ t('ticket.summary.quantity') }}</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.price') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.taxClass') }}</QTh> <QTh auto-width>{{ t('ticket.summary.taxClass') }}</QTh>
<QTh auto-width>{{ t('globals.amount') }}</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"> <VnRow v-if="viewAction === 'edit'" class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<QInput <QInput
:label="t('travel.thermographs.description')" :label="t('globals.description')"
type="textarea" type="textarea"
v-model="thermographForm.description" v-model="thermographForm.description"
fill-input fill-input

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed, watch } from 'vue';
import { QBtn, QField, QPopupEdit } from 'quasar'; import { QBtn } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -46,7 +46,12 @@ const arrayData = useArrayData('ExtraCommunity', {
}, },
}); });
const rows = computed(() => arrayData.store.data || []); const rows = ref([]);
const originalRowDataCopy = ref([]);
const draggedRowIndex = ref(null);
const targetRowIndex = ref(null);
const entryRowIndex = ref(null);
const draggedEntry = ref(null);
const tableColumnComponents = { const tableColumnComponents = {
id: { id: {
@ -55,7 +60,12 @@ const tableColumnComponents = {
}, },
cargoSupplierNickname: { cargoSupplierNickname: {
component: QBtn, component: QBtn,
attrs: { flat: true, color: 'primary', dense: true }, attrs: {
flat: true,
color: 'primary',
dense: true,
class: 'supplier-name-button',
},
}, },
agencyModeName: { agencyModeName: {
component: 'span', component: 'span',
@ -66,16 +76,24 @@ const tableColumnComponents = {
attrs: {}, attrs: {},
}, },
ref: { ref: {
component: QField, component: VnInput,
attrs: { readonly: true, dense: true, class: 'cursor-pointer' }, attrs: { dense: true },
event: (val, field, rowIndex) => ({
'keyup.enter': () => saveFieldValue(val, field, rowIndex),
blur: () => saveFieldValue(val, field, rowIndex),
}),
}, },
stickers: { stickers: {
component: 'span', component: 'span',
attrs: {}, attrs: {},
}, },
kg: { kg: {
component: QField, component: VnInput,
attrs: { readonly: true, dense: true, class: 'cursor-pointer' }, attrs: { dense: true, type: 'number', min: 0, class: 'input-number' },
event: (val, field, rowIndex) => ({
'keyup.enter': () => saveFieldValue(val, field, rowIndex),
blur: () => saveFieldValue(val, field, rowIndex),
}),
}, },
loadedKg: { loadedKg: {
component: 'span', component: 'span',
@ -199,7 +217,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
showValue: true, showValue: true,
sortable: true, sortable: true,
format: (value) => toDate(value.substring(0, 10)), format: (value) => toDate(value),
}, },
{ {
label: t('globals.wareHouseIn'), label: t('globals.wareHouseIn'),
@ -216,7 +234,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
showValue: true, showValue: true,
sortable: true, sortable: true,
format: (value) => toDate(value.substring(0, 10)), format: (value) => toDate(value),
}, },
]); ]);
@ -224,6 +242,22 @@ async function getData() {
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
} }
const onStoreDataChange = () => {
const newData = JSON.parse(JSON.stringify(arrayData.store.data)) || [];
rows.value = newData;
// el objetivo de esto es guardar una copia de los valores iniciales de todas las rows para corroborar si la data cambio antes de guardar los cambios
originalRowDataCopy.value = JSON.parse(JSON.stringify(newData));
};
watch(
arrayData.store,
() => {
if (!arrayData.store.data) return;
onStoreDataChange();
},
{ deep: true, immediate: true }
);
const openReportPdf = () => { const openReportPdf = () => {
const params = { const params = {
...arrayData.store.userParams, ...arrayData.store.userParams,
@ -235,9 +269,14 @@ const openReportPdf = () => {
const saveFieldValue = async (val, field, index) => { const saveFieldValue = async (val, field, index) => {
try { try {
// Evitar la solicitud de guardado si el valor no ha cambiado
if (originalRowDataCopy.value[index][field] == val) return;
const id = rows.value[index].id; const id = rows.value[index].id;
const params = { [field]: val }; const params = { [field]: val };
await axios.patch(`Travels/${id}`, params); await axios.patch(`Travels/${id}`, params);
// Actualizar la copia de los datos originales con el nuevo valor
originalRowDataCopy.value[index][field] = val;
} catch (err) { } catch (err) {
console.error('Error updating travel'); console.error('Error updating travel');
} }
@ -248,6 +287,7 @@ const navigateToTravelId = (id) => {
}; };
const stopEventPropagation = (event, col) => { const stopEventPropagation = (event, col) => {
// Detener la propagación del evento de los siguientes elementos para evitar el click sobre la row que dispararía la función navigateToTravelId
if (!['ref', 'id', 'cargoSupplierNickname', 'kg'].includes(col.name)) return; if (!['ref', 'id', 'cargoSupplierNickname', 'kg'].includes(col.name)) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -263,6 +303,121 @@ onMounted(async () => {
landedTo.value.setHours(23, 59, 59, 59); landedTo.value.setHours(23, 59, 59, 59);
await getData(); await getData();
}); });
// Handler del evento @dragstart (inicio del drag) y guarda información inicial
const handleDragStart = (event, rowIndex, entryIndex) => {
draggedRowIndex.value = rowIndex;
entryRowIndex.value = entryIndex;
event.dataTransfer.effectAllowed = 'move';
};
// Handler del evento @dragenter (cuando haces drag sobre une elemento y lo arrastras sobre un posible target de drop) y actualiza el targetIndex
const handleDragEnter = (_, targetIndex) => {
targetRowIndex.value = targetIndex;
};
const saveRowDrop = async (targetRowIndex) => {
const entryId = draggedEntry.value.id;
const travelId = rows.value[targetRowIndex].id;
await axios.patch(`Entries/${entryId}`, { travelFk: travelId });
};
const moveRow = async (draggedRowIndex, targetRowIndex, entryIndex) => {
try {
if (draggedRowIndex === targetRowIndex) return;
// Remover entry de la row original
draggedEntry.value = rows.value[draggedRowIndex].entries.splice(entryIndex, 1)[0];
//Si la row de destino por alguna razón no tiene la propiedad entry la creamos
if (!rows.value[targetRowIndex].entries) rows.value[targetRowIndex].entries = [];
// Añadir entry a la row de destino
rows.value[targetRowIndex].entries.push(draggedEntry.value);
await saveRowDrop(targetRowIndex);
} catch (err) {
cleanDragAndDropData();
console.error('Error moving row', err);
}
};
// Handler de cuando haces un drop tanto dentro como fuera de la tabla para limpiar acciones y data
const handleDragEnd = () => {
stopScroll();
cleanDragAndDropData();
};
// Handler del evento @drop (cuando soltas el elemento draggeado sobre un target)
const handleDrop = () => {
if (
!draggedRowIndex.value &&
draggedRowIndex.value !== 0 &&
!targetRowIndex.value &&
draggedRowIndex.value !== 0
)
return;
moveRow(draggedRowIndex.value, targetRowIndex.value, entryRowIndex.value);
handleDragEnd();
};
const cleanDragAndDropData = () => {
draggedRowIndex.value = null;
targetRowIndex.value = null;
entryRowIndex.value = null;
draggedEntry.value = null;
};
const scrollInterval = ref(null);
const startScroll = (direction) => {
// Iniciar el scroll en la dirección especificada
if (!scrollInterval.value) {
scrollInterval.value = requestAnimationFrame(() => scroll(direction));
}
};
const stopScroll = () => {
if (scrollInterval.value) {
cancelAnimationFrame(scrollInterval.value);
scrollInterval.value = null;
}
};
const scroll = (direction) => {
// Controlar el desplazamiento en la dirección especificada
const yOffset = direction === 'up' ? -2 : 2;
window.scrollBy(0, yOffset);
const windowHeight = window.innerHeight;
const documentHeight = document.body.offsetHeight;
// Verificar si se alcanzaron los límites de la ventana para detener el desplazamiento
if (
(direction === 'up' && window.scrollY > 0) ||
(direction === 'down' && windowHeight + window.scrollY < documentHeight)
) {
scrollInterval.value = requestAnimationFrame(() => scroll(direction));
} else {
stopScroll();
}
};
// Handler del scroll mientras se hace el drag de una row
const handleDragScroll = (event) => {
// Obtener la posición y dimensiones del cursor
const y = event.clientY;
const windowHeight = window.innerHeight;
// Verificar si el cursor está cerca del borde superior o inferior de la ventana
const nearTop = y < 150;
const nearBottom = y > windowHeight - 100;
if (nearTop) {
startScroll('up');
} else if (nearBottom) {
startScroll('down');
} else {
stopScroll();
}
};
</script> </script>
<template> <template>
@ -302,59 +457,60 @@ onMounted(async () => {
row-key="clientId" row-key="clientId"
:pagination="{ rowsPerPage: 0 }" :pagination="{ rowsPerPage: 0 }"
class="full-width" class="full-width"
table-style="user-select: none;"
@drag="handleDragScroll($event)"
@dragend="handleDragEnd($event)"
:separator="!targetRowIndex && targetRowIndex !== 0 ? 'horizontal' : 'none'"
> >
<template #body="props"> <template #body="props">
<QTr <QTr
:props="props" :props="props"
@click="navigateToTravelId(props.row.id)"
class="cursor-pointer bg-vn-primary-row" class="cursor-pointer bg-vn-primary-row"
@click="navigateToTravelId(props.row.id)"
@dragenter="handleDragEnter($event, props.rowIndex)"
@dragover.prevent
@drop="handleDrop()"
:class="{
'dashed-border --top --left --right':
targetRowIndex === props.rowIndex,
'--bottom':
targetRowIndex === props.rowIndex &&
(!props.row.entries || props.row.entries.length === 0),
}"
> >
<QTd <QTd
v-for="col in props.cols" v-for="col in props.cols"
:key="col.name" :key="col.name"
:props="props" :props="props"
@click="stopEventPropagation($event, col)" @click="stopEventPropagation($event, col)"
auto-width
> >
<component <component
:is="tableColumnComponents[col.name].component" :is="tableColumnComponents[col.name].component"
v-bind="tableColumnComponents[col.name].attrs" v-bind="tableColumnComponents[col.name].attrs"
v-model="rows[props.rowIndex][col.field]"
v-on="
tableColumnComponents[col.name].event
? tableColumnComponents[col.name].event(
rows[props.rowIndex][col.field],
col.field,
props.rowIndex
)
: {}
"
> >
<!-- Editable 'ref' and 'kg' QField slot -->
<template
v-if="col.name === 'ref' || col.name === 'kg'"
#control
>
<div
class="self-center full-width no-outline"
tabindex="0"
>
{{ col.value }}
</div>
<QPopupEdit
:key="col.name"
v-model="col.value"
label-set="Save"
label-cancel="Close"
>
<VnInput
v-model="rows[props.pageIndex][col.field]"
dense
autofocus
@keyup.enter="
saveFieldValue(
rows[props.pageIndex][col.field],
col.field,
props.rowIndex
)
"
/>
</QPopupEdit>
</template>
<template v-if="col.showValue"> <template v-if="col.showValue">
{{ col.value }} <span
:class="[
'text-left',
{
'supplier-name':
col.name === 'cargoSupplierNickname',
},
]"
>{{ col.value }}</span
>
</template> </template>
<!-- Main Row Descriptors --> <!-- Main Row Descriptors -->
<TravelDescriptorProxy <TravelDescriptorProxy
v-if="col.name === 'id'" v-if="col.name === 'id'"
@ -367,13 +523,27 @@ onMounted(async () => {
</component> </component>
</QTd> </QTd>
</QTr> </QTr>
<QTr <QTr
v-for="entry in props.row.entries" v-for="(entry, index) in props.row.entries"
:key="entry.id" :key="index"
:props="props" :props="props"
class="bg-vn-secondary-row" class="bg-vn-secondary-row cursor-pointer"
@dragstart="handleDragStart($event, props.rowIndex, index)"
@dragenter="handleDragEnter($event, props.rowIndex)"
@dragover.prevent
@drop="handleDrop()"
:draggable="true"
:class="{
'dragged-row':
entryRowIndex === index && props.rowIndex === draggedRowIndex,
'dashed-border --left --right': targetRowIndex === props.rowIndex,
'--bottom':
targetRowIndex === props.rowIndex &&
index === props.row.entries.length - 1,
}"
> >
<QTd class="row justify-center"> <QTd>
<QBtn flat color="primary">{{ entry.id }} </QBtn> <QBtn flat color="primary">{{ entry.id }} </QBtn>
<EntryDescriptorProxy :id="entry.id" /> <EntryDescriptorProxy :id="entry.id" />
</QTd> </QTd>
@ -381,33 +551,75 @@ onMounted(async () => {
<QBtn flat color="primary" dense>{{ entry.supplierName }}</QBtn> <QBtn flat color="primary" dense>{{ entry.supplierName }}</QBtn>
<SupplierDescriptorProxy :id="entry.supplierFk" /> <SupplierDescriptorProxy :id="entry.supplierFk" />
</QTd> </QTd>
<QTd />
<QTd>
<span>{{ toCurrency(entry.invoiceAmount) }}</span>
</QTd>
<QTd>
<span>{{ entry.reference }}</span>
</QTd>
<QTd>
<span>{{ entry.stickers }}</span>
</QTd>
<QTd></QTd> <QTd></QTd>
<QTd <QTd>
><span>{{ toCurrency(entry.invoiceAmount) }}</span></QTd <span>{{ entry.loadedkg }}</span>
> </QTd>
<QTd <QTd>
><span>{{ entry.reference }}</span></QTd <span>{{ entry.volumeKg }}</span>
> </QTd>
<QTd <QTd />
><span>{{ entry.stickers }}</span></QTd <QTd />
> <QTd />
<QTd></QTd> <QTd />
<QTd
><span>{{ entry.loadedkg }}</span></QTd
>
<QTd
><span>{{ entry.volumeKg }}</span></QTd
>
<QTd></QTd>
<QTd></QTd>
<QTd></QTd>
<QTd></QTd>
</QTr> </QTr>
</template> </template>
</QTable> </QTable>
</QPage> </QPage>
</template> </template>
<style scoped lang="scss">
:deep(.q-table) {
border-collapse: collapse;
}
.dashed-border {
&.--left {
border-left: 1px dashed #ccc;
}
&.--right {
border-right: 1px dashed #ccc;
}
&.--top {
border-top: 1px dashed #ccc;
}
&.--bottom {
border-bottom: 1px dashed #ccc;
}
}
.dragged-row {
background-color: $primary-light;
}
.supplier-name {
display: flex;
max-width: 150px;
@media (max-width: $breakpoint-md-max) {
max-width: 100px;
}
}
.supplier-name-button {
white-space: normal;
width: max-content;
}
</style>
<i18n> <i18n>
en: en:
searchExtraCommunity: Search for extra community shipping searchExtraCommunity: Search for extra community shipping

View File

@ -65,7 +65,7 @@ const decrement = (paramsObj, key) => {
<span>{{ formatFn(tag.value) }}</span> <span>{{ formatFn(tag.value) }}</span>
</div> </div>
</template> </template>
<template #body="{ params }"> <template #body="{ params, searchFn }">
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInput label="id" v-model="params.id" is-outlined /> <VnInput label="id" v-model="params.id" is-outlined />
@ -116,6 +116,7 @@ const decrement = (paramsObj, key) => {
<VnSelectFilter <VnSelectFilter
:label="t('params.agencyModeFk')" :label="t('params.agencyModeFk')"
v-model="params.agencyModeFk" v-model="params.agencyModeFk"
@update:model-value="searchFn()"
:options="agenciesOptions" :options="agenciesOptions"
option-value="agencyFk" option-value="agencyFk"
option-label="name" option-label="name"
@ -129,8 +130,9 @@ const decrement = (paramsObj, key) => {
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
v-model="params.shippedFrom"
:label="t('params.shippedFrom')" :label="t('params.shippedFrom')"
v-model="params.shippedFrom"
@update:model-value="searchFn()"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -138,8 +140,9 @@ const decrement = (paramsObj, key) => {
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
v-model="params.landedTo"
:label="t('params.landedTo')" :label="t('params.landedTo')"
v-model="params.landedTo"
@update:model-value="searchFn()"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -149,6 +152,7 @@ const decrement = (paramsObj, key) => {
<VnSelectFilter <VnSelectFilter
:label="t('params.warehouseOutFk')" :label="t('params.warehouseOutFk')"
v-model="params.warehouseOutFk" v-model="params.warehouseOutFk"
@update:model-value="searchFn()"
:options="warehousesOptions" :options="warehousesOptions"
option-value="id" option-value="id"
option-label="name" option-label="name"
@ -164,6 +168,7 @@ const decrement = (paramsObj, key) => {
<VnSelectFilter <VnSelectFilter
:label="t('params.warehouseInFk')" :label="t('params.warehouseInFk')"
v-model="params.warehouseInFk" v-model="params.warehouseInFk"
@update:model-value="searchFn()"
:options="warehousesOptions" :options="warehousesOptions"
option-value="id" option-value="id"
option-label="name" option-label="name"
@ -179,6 +184,7 @@ const decrement = (paramsObj, key) => {
<VnSelectFilter <VnSelectFilter
:label="t('supplier.pageTitles.supplier')" :label="t('supplier.pageTitles.supplier')"
v-model="params.cargoSupplierFk" v-model="params.cargoSupplierFk"
@update:model-value="searchFn()"
:options="suppliersOptions" :options="suppliersOptions"
option-value="id" option-value="id"
option-label="name" option-label="name"
@ -194,6 +200,7 @@ const decrement = (paramsObj, key) => {
<VnSelectFilter <VnSelectFilter
:label="t('params.continent')" :label="t('params.continent')"
v-model="params.continent" v-model="params.continent"
@update:model-value="searchFn()"
:options="continentsOptions" :options="continentsOptions"
option-value="code" option-value="code"
option-label="name" option-label="name"
@ -208,19 +215,6 @@ const decrement = (paramsObj, key) => {
</VnFilterPanel> </VnFilterPanel>
</template> </template>
<style scoped>
.input-number >>> input[type='number'] {
-moz-appearance: textfield;
}
.input-number >>> input::-webkit-outer-spin-button,
.input-number >>> input::-webkit-inner-spin-button {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
</style>
<i18n> <i18n>
en: en:
params: params:

View File

@ -78,7 +78,7 @@ const filter = {
<VnLv :label="t('worker.card.name')" :value="worker.user.nickname" /> <VnLv :label="t('worker.card.name')" :value="worker.user.nickname" />
<VnLv <VnLv
:label="t('worker.list.department')" :label="t('worker.list.department')"
:value="worker.department.department.name" :value="worker.department?.department?.name"
/> />
<VnLv :label="t('worker.list.email')" :value="worker.user.email" copy /> <VnLv :label="t('worker.list.email')" :value="worker.user.email" copy />
<VnLv :label="t('worker.summary.boss')" link> <VnLv :label="t('worker.summary.boss')" link>
@ -102,10 +102,10 @@ const filter = {
<VnLinkPhone :phone-number="worker.phone" /> <VnLinkPhone :phone-number="worker.phone" />
</template> </template>
</VnLv> </VnLv>
<VnLv :value="worker.client.phone"> <VnLv :value="worker.client?.phone">
<template #label> <template #label>
{{ t('worker.summary.personalPhone') }} {{ t('worker.summary.personalPhone') }}
<VnLinkPhone :phone-number="worker.client.phone" /> <VnLinkPhone :phone-number="worker.client?.phone" />
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('worker.summary.locker')" :value="worker.locker" /> <VnLv :label="t('worker.summary.locker')" :value="worker.locker" />

View File

@ -11,7 +11,7 @@ export default {
redirect: { name: 'EntryMain' }, redirect: { name: 'EntryMain' },
menus: { menus: {
main: ['EntryList', 'EntryLatestBuys'], main: ['EntryList', 'EntryLatestBuys'],
card: ['EntryBasicData', 'EntryBuys', 'EntryNotes', 'EntryLog'], card: ['EntryBasicData', 'EntryBuys', 'EntryNotes', 'EntryDms', 'EntryLog'],
}, },
children: [ children: [
{ {
@ -95,6 +95,15 @@ export default {
}, },
component: () => import('src/pages/Entry/Card/EntryNotes.vue'), 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', path: 'log',
name: 'EntryLog', name: 'EntryLog',

View File

@ -13,7 +13,7 @@ describe('ClaimDevelopment', () => {
it('should reset line', () => { it('should reset line', () => {
cy.selectOption(firstLineReason, 'Novato'); cy.selectOption(firstLineReason, 'Novato');
cy.resetCard(); cy.resetCard();
cy.getValue(firstLineReason).should('have.value', 'Prisas'); cy.getValue(firstLineReason).should('equal', 'Prisas');
}); });
it('should edit line', () => { it('should edit line', () => {
@ -23,7 +23,7 @@ describe('ClaimDevelopment', () => {
cy.login('developer'); cy.login('developer');
cy.visit(`/#/claim/${claimId}/development`); cy.visit(`/#/claim/${claimId}/development`);
cy.getValue(firstLineReason).should('have.value', 'Novato'); cy.getValue(firstLineReason).should('equal', 'Novato');
//Restart data //Restart data
cy.selectOption(firstLineReason, 'Prisas'); cy.selectOption(firstLineReason, 'Prisas');

View File

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

View File

@ -18,7 +18,7 @@ describe('InvoiceInIntrastat', () => {
cy.visit(`/#/invoice-in/1/intrastat`); cy.visit(`/#/invoice-in/1/intrastat`);
cy.getValue(firstLineCode).should( cy.getValue(firstLineCode).should(
'have.value', 'equal',
'Plantas vivas: Esqueje/injerto, Vid' 'Plantas vivas: Esqueje/injerto, Vid'
); );
}); });

View File

@ -21,7 +21,7 @@ describe('InvoiceInVat', () => {
cy.saveCard(); cy.saveCard();
cy.visit(`/#/invoice-in/1/vat`); 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', () => { it('should add a new row', () => {

View File

@ -42,7 +42,7 @@ Cypress.Commands.add('login', (user) => {
}); });
Cypress.Commands.add('waitForElement', (element) => { 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) => { Cypress.Commands.add('getValue', (selector) => {
@ -55,7 +55,13 @@ Cypress.Commands.add('getValue', (selector) => {
return cy.get( return cy.get(
selector + selector +
'> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input' '> .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 // Puedes añadir un log o lanzar un error si el elemento no es reconocido
cy.log('Elemento no soportado'); cy.log('Elemento no soportado');
@ -126,12 +132,13 @@ Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
cy.get(rowSelector).within(() => { cy.get(rowSelector).within(() => {
for (const [index, value] of expectedValues.entries()) { for (const [index, value] of expectedValues.entries()) {
cy.log('CHECKING ', index, value); cy.log('CHECKING ', index, value);
if(value === undefined) continue
if (typeof value == 'boolean') { if (typeof value == 'boolean') {
const prefix = value ? '' : 'not.'; const prefix = value ? '' : 'not.';
cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`); cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`);
continue; continue;
} }
cy.getValue(`:nth-child(${index + 1})`).should('have.value', value); cy.getValue(`:nth-child(${index + 1})`).should('equal', value)
} }
}); });
}); });