PR-CUSTOMER #186

Merged
jsegarra merged 105 commits from :PR-CUSTOMER into dev 2024-04-19 15:55:53 +00:00
45 changed files with 6680 additions and 487 deletions
Showing only changes of commit a0f9c811ff - Show all commits

View File

@ -1,6 +1,5 @@
FROM node:stretch-slim
RUN curl -fsSL https://bun.sh/install | bash
RUN bun install -g @quasar/cli
RUN npm install -g @quasar/cli
WORKDIR /app
COPY dist/spa ./
CMD ["quasar", "serve", "./", "--history", "--hostname", "0.0.0.0"]

38
Jenkinsfile vendored
View File

@ -9,7 +9,7 @@ def BRANCH_ENV = [
node {
stage('Setup') {
env.BACK_REPLICAS = 1
env.FRONT_REPLICAS = 1
env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev'
PROTECTED_BRANCH = [
@ -62,7 +62,7 @@ pipeline {
NODE_ENV = ""
}
steps {
sh 'bun install --no-audit --prefer-offline'
sh 'npm install --prefer-offline'
}
}
stage('Test') {
@ -72,11 +72,15 @@ pipeline {
environment {
NODE_ENV = ""
}
parallel {
stage('Frontend') {
steps {
sh 'bun run test:unit:ci'
}
steps {
sh 'npm run test:unit:ci'
}
post {
always {
junit(
testResults: 'junitresults.xml',
allowEmptyResults: true
)
}
}
}
@ -89,7 +93,11 @@ pipeline {
}
steps {
sh 'quasar build'
// dockerBuild()
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
}
dockerBuild()
}
}
stage('Deploy') {
@ -108,18 +116,4 @@ pipeline {
}
}
}
post {
always {
script {
if (!PROTECTED_BRANCH) {
try {
junit 'junitresults.xml'
junit 'junit.xml'
} catch (e) {
echo e.toString()
}
}
}
}
}
}

BIN
bun.lockb

Binary file not shown.

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({
component: VnConfirm,
componentProps: {
title: t('confirmDeletion'),
message: t('confirmDeletionMessage'),
title: t('globals.confirmDeletion'),
message: t('globals.confirmDeletionMessage'),
newData,
ids,
},
@ -317,16 +317,3 @@ watch(formUrl, async () => {
color="primary"
/>
</template>
<i18n>
{
"en": {
"confirmDeletion": "Confirm deletion",
"confirmDeletionMessage": "Are you sure you want to delete this?"
},
"es": {
"confirmDeletion": "Confirmar eliminación",
"confirmDeletionMessage": "Seguro que quieres eliminar?"
}
}
</i18n>

View File

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

View File

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

View File

@ -1,6 +1,7 @@
<script setup>
import axios from 'axios';
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState';
@ -8,6 +9,7 @@ import { useStateStore } from 'stores/useStateStore';
import { useValidator } from 'src/composables/useValidator';
import useNotify from 'src/composables/useNotify.js';
import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue';
const quasar = useQuasar();
const state = useState();
@ -59,6 +61,10 @@ const $props = defineProps({
type: Function,
default: null,
},
saveFn: {
type: Function,
default: null,
},
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
@ -76,9 +82,8 @@ onMounted(async () => {
});
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
if ($props.formInitialData && !$props.autoLoad) {
state.set($props.model, $props.formInitialData);
} else {
state.set($props.model, $props.formInitialData ?? {});
if ($props.autoLoad && !$props.formInitialData) {
await fetch();
}
jsegarra marked this conversation as resolved Outdated

Eliminar el import de VnRow

Eliminar el import de VnRow

Mmmm... No veo un import de VnRow en el componente FormModel

Mmmm... No veo un import de `VnRow` en el componente `FormModel`
@ -91,6 +96,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(() => {
state.unset($props.model);
});
@ -143,17 +161,20 @@ async function save() {
try {
const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
let response;
if ($props.urlCreate) {
response = await axios.post($props.urlCreate, body);
notify('globals.dataCreated', 'positive');
} else {
response = await axios.patch($props.urlUpdate || $props.url, body);
}
if ($props.saveFn) response = await $props.saveFn(body);
else
response = await axios[$props.urlCreate ? 'post' : 'patch'](
$props.urlCreate || $props.urlUpdate || $props.url,
body
);
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
emit('onDataSaved', formData.value, response?.data);
originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false;
} catch (err) {
notify('errors.create', 'negative');
console.error(err);
notify('errors.writeRequest', 'negative');
}
isLoading.value = false;
}
@ -254,3 +275,8 @@ watch(formUrl, async () => {
padding: 32px;
}
</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 />
<QBtn icon="close" :disable="isLoading" flat round dense v-close-popup />
</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">
<QSelect
:label="t('Language')"
@ -184,7 +174,6 @@ async function send() {
<i18n>
en:
CustomerDefaultLanguage: This customer uses <strong>{locale}</strong> as their default language
templates:
pendingPayment: 'Your order is pending of payment.
Please, enter the website and make the payment with a credit card. Thank you.'
@ -197,7 +186,6 @@ en:
pt: Portuguese
es:
Send SMS: Enviar SMS
CustomerDefaultLanguage: Este cliente utiliza <strong>{locale}</strong> como idioma por defecto
Language: Idioma
Phone: Móvil
Subject: Asunto

View File

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

View File

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

View File

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

View File

@ -1,20 +1,21 @@
<script setup>
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useState } from 'src/composables/useState';
import { useQuasar } from 'quasar';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue';
import CreateDepartmentChild from '../CreateDepartmentChild.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import { useRouter } from 'vue-router';
const quasar = useQuasar();
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const state = useState();
const router = useRouter();
const treeRef = ref(null);
const treeRef = ref();
const showCreateNodeFormVal = ref(false);
const creationNodeSelectedId = ref(null);
const expanded = ref([]);
@ -24,30 +25,35 @@ const nodes = ref([{ id: null, name: t('Departments'), sons: true, children: [{}
const fetchedChildrensSet = ref(new Set());
const onNodeExpanded = (nodeKeysArray) => {
// Verificar si el nodo ya fue expandido
if (!fetchedChildrensSet.value.has(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) => {
try {
const node = treeRef.value.getNodeByKey(nodeKey);
const node = treeRef.value?.getNodeByKey(nodeKey);
if (!node || node.sons === 0) return;
const params = { parentId: node.id };
const response = await axios.get('/departments/getLeaves', { params });
// Si hay datos en la respuesta y tiene hijos, agregarlos al nodo actual
if (response.data) {
node.children = response.data;
node.children.forEach((node) => {
if (node.sons) node.children = [{}];
node.children = response.data.map((n) => {
const hasChildrens = n.sons > 0;
n.children = hasChildrens ? [{}] : null;
n.clickable = true;
return n;
});
}
state.set('Tree', node);
} catch (err) {
console.error('Error fetching department leaves');
console.error('Error fetching department leaves', err);
throw new Error();
}
};
@ -84,10 +90,49 @@ const onNodeCreated = async () => {
await fetchNodeLeaves(creationNodeSelectedId.value);
};
const redirectToDepartmentSummary = (id) => {
if (!id) return;
router.push({ name: 'DepartmentSummary', params: { id } });
};
onMounted(async (n) => {
const tree = [...state.get('Tree'), 1];
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>
<template>
@ -99,15 +144,27 @@ const redirectToDepartmentSummary = (id) => {
label-key="name"
v-model:expanded="expanded"
@update:expanded="onNodeExpanded($event)"
:default-expand-all="true"
>
<template #default-header="{ node }">
<div
class="row justify-between full-width q-pr-md cursor-pointer"
@click.stop="redirectToDepartmentSummary(node.id)"
:id="node.id"
class="qtr row justify-between full-width q-pr-md cursor-pointer"
>
<span class="text-uppercase">
{{ node.name }}
</span>
<div>
<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">
<QIcon
v-if="node.id"
@ -149,6 +206,11 @@ const redirectToDepartmentSummary = (id) => {
</QCard>
</template>
<style lang="scss" scoped>
span {
color: $primary;
}
</style>
<i18n>
es:
Departments: Departamentos

View File

@ -24,6 +24,7 @@ export default {
dataCreated: 'Data created',
add: 'Add',
create: 'Create',
edit: 'Edit',
save: 'Save',
remove: 'Remove',
reset: 'Reset',
@ -64,13 +65,24 @@ export default {
markAll: 'Mark all',
requiredField: 'Required field',
class: 'clase',
type: 'type',
type: 'Type',
reason: 'reason',
noResults: 'No results',
system: 'System',
notificationSent: 'Notification sent',
warehouse: 'Warehouse',
company: 'Company',
fieldRequired: 'Field required',
allowedFilesText: 'Allowed file types: { allowedContentTypes }',
confirmDeletion: 'Confirm deletion',
confirmDeletionMessage: 'Are you sure you want to delete this?',
description: 'Description',
id: 'Id',
order: 'Order',
original: 'Original',
file: 'File',
selectFile: 'Select a file',
copyClipboard: 'Copy on clipboard',
},
errors: {
statusUnauthorized: 'Access denied',
@ -78,7 +90,7 @@ export default {
statusBadGateway: 'It seems that the server has fall down',
statusGatewayTimeout: 'Could not contact the server',
userConfig: 'Error fetching user config',
create: 'Error during creation',
writeRequest: 'The requested operation could not be completed',
},
login: {
title: 'Login',
@ -179,7 +191,7 @@ export default {
fiscalAddress: 'Fiscal address',
fiscalData: 'Fiscal data',
billingData: 'Billing data',
consignee: 'Consignee',
consignee: 'Default consignee',
businessData: 'Business data',
financialData: 'Financial data',
customerId: 'Customer ID',
@ -233,6 +245,8 @@ export default {
businessType: 'Business Type',
city: 'City',
descriptorInfo: 'Invoices minus payments plus orders not yet',
rating: 'Rating',
recommendCredit: 'Recommended credit',
},
basicData: {
socialName: 'Fiscal name',
@ -289,6 +303,7 @@ export default {
basicData: 'Basic data',
buys: 'Buys',
notes: 'Notes',
dms: 'File management',
log: 'Log',
create: 'Create',
latestBuys: 'Latest buys',
@ -360,7 +375,6 @@ export default {
reference: 'Reference',
observations: 'Observations',
item: 'Item',
description: 'Description',
size: 'Size',
packing: 'Packing',
grouping: 'Grouping',
@ -375,7 +389,6 @@ export default {
},
notes: {
observationType: 'Observation type',
description: 'Description',
},
descriptor: {
agency: 'Agency',
@ -388,7 +401,6 @@ export default {
packing: 'Packing',
grouping: 'Grouping',
quantity: 'Quantity',
description: 'Description',
size: 'Size',
tags: 'Tags',
type: 'Type',
@ -474,7 +486,6 @@ export default {
visible: 'Visible',
available: 'Available',
quantity: 'Quantity',
description: 'Description',
price: 'Price',
discount: 'Discount',
packing: 'Packing',
@ -554,7 +565,6 @@ export default {
landed: 'Landed',
quantity: 'Quantity',
claimed: 'Claimed',
description: 'Description',
price: 'Price',
discount: 'Discount',
total: 'Total',
@ -816,7 +826,6 @@ export default {
orderTicketList: 'Order Ticket List',
details: 'Details',
item: 'Item',
description: 'Description',
quantity: 'Quantity',
price: 'Price',
amount: 'Amount',
@ -1161,7 +1170,6 @@ export default {
warehouse: 'Warehouse',
travelFileDescription: 'Travel id { travelId }',
file: 'File',
description: 'Description',
},
},
item: {
@ -1195,7 +1203,6 @@ export default {
clone: 'Clone',
openCard: 'View',
openSummary: 'Summary',
viewDescription: 'Description',
},
cardDescriptor: {
mainList: 'Main list',

View File

@ -24,6 +24,7 @@ export default {
dataCreated: 'Datos creados',
add: 'Añadir',
create: 'Crear',
edit: 'Modificar',
save: 'Guardar',
remove: 'Eliminar',
reset: 'Restaurar',
@ -64,13 +65,24 @@ export default {
markAll: 'Marcar todo',
requiredField: 'Campo obligatorio',
class: 'clase',
type: 'tipo',
type: 'Tipo',
reason: 'motivo',
noResults: 'Sin resultados',
system: 'Sistema',
notificationSent: 'Notificación enviada',
warehouse: 'Almacén',
company: 'Empresa',
fieldRequired: 'Campo requerido',
allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }',
confirmDeletion: 'Confirmar eliminación',
confirmDeletionMessage: '¿Seguro que quieres eliminar?',
description: 'Descripción',
id: 'Id',
order: 'Orden',
original: 'Original',
file: 'Fichero',
selectFile: 'Seleccione un fichero',
copyClipboard: 'Copiar en portapapeles',
},
errors: {
statusUnauthorized: 'Acceso denegado',
@ -78,7 +90,7 @@ export default {
statusBadGateway: 'Parece ser que el servidor ha caído',
statusGatewayTimeout: 'No se ha podido contactar con el servidor',
userConfig: 'Error al obtener configuración de usuario',
create: 'Error al crear',
writeRequest: 'No se pudo completar la operación solicitada',
},
login: {
title: 'Inicio de sesión',
@ -178,7 +190,7 @@ export default {
fiscalAddress: 'Dirección fiscal',
fiscalData: 'Datos fiscales',
billingData: 'Datos de facturación',
consignee: 'Consignatario',
consignee: 'Consignatario pred.',
businessData: 'Datos comerciales',
financialData: 'Datos financieros',
customerId: 'ID cliente',
@ -232,6 +244,8 @@ export default {
businessType: 'Tipo de negocio',
city: 'Población',
descriptorInfo: 'Facturas menos recibos mas pedidos sin facturar',
rating: 'Clasificación',
recommendCredit: 'Crédito recomendado',
},
basicData: {
socialName: 'Nombre fiscal',
@ -289,6 +303,7 @@ export default {
basicData: 'Datos básicos',
buys: 'Compras',
notes: 'Notas',
dms: 'Gestión documental',
log: 'Historial',
create: 'Crear',
latestBuys: 'Últimas compras',
@ -360,7 +375,6 @@ export default {
reference: 'Referencia',
observations: 'Observaciónes',
item: 'Artículo',
description: 'Descripción',
size: 'Medida',
packing: 'Packing',
grouping: 'Grouping',
@ -375,7 +389,6 @@ export default {
},
notes: {
observationType: 'Tipo de observación',
description: 'Descripción',
},
descriptor: {
agency: 'Agencia',
@ -388,7 +401,6 @@ export default {
packing: 'Packing',
grouping: 'Grouping',
quantity: 'Cantidad',
description: 'Descripción',
size: 'Medida',
tags: 'Etiquetas',
type: 'Tipo',
@ -474,7 +486,6 @@ export default {
visible: 'Visible',
available: 'Disponible',
quantity: 'Cantidad',
description: 'Descripción',
price: 'Precio',
discount: 'Descuento',
packing: 'Encajado',
@ -554,7 +565,6 @@ export default {
landed: 'Entregado',
quantity: 'Cantidad',
claimed: 'Reclamado',
description: 'Descripción',
price: 'Precio',
discount: 'Descuento',
total: 'Total',
@ -725,7 +735,6 @@ export default {
orderTicketList: 'Tickets del pedido',
details: 'Detalles',
item: 'Item',
description: 'Descripción',
quantity: 'Cantidad',
price: 'Precio',
amount: 'Monto',
@ -1162,7 +1171,6 @@ export default {
warehouse: 'Almacén',
travelFileDescription: 'Id envío { travelId }',
file: 'Fichero',
description: 'Descripción',
},
},
item: {
@ -1196,7 +1204,6 @@ export default {
clone: 'Clonar',
openCard: 'Ficha',
openSummary: 'Detalles',
viewDescription: 'Descripción',
},
cardDescriptor: {
mainList: 'Listado principal',

View File

@ -35,7 +35,6 @@ const claimDmsFilter = ref({
relation: 'dms',
},
],
where: { claimFk: entityId.value },
});
onMounted(async () => {
@ -141,6 +140,11 @@ const claimDms = ref([]);
const multimediaDialog = ref();
const multimediaSlide = ref();
async function getClaimDms() {
claimDmsFilter.value.where = { claimFk: entityId.value };
await claimDmsRef.value.fetch();
}
function setClaimDms(data) {
claimDms.value = [];
data.forEach((media) => {
@ -163,10 +167,13 @@ function openDialog(dmsId) {
url="ClaimDms"
:filter="claimDmsFilter"
@on-fetch="(data) => setClaimDms(data)"
limit="20"
ref="claimDmsRef"
/>
<CardSummary ref="summary" :url="`Claims/${entityId}/getSummary`">
<CardSummary
ref="summary"
:url="`Claims/${entityId}/getSummary`"
@on-fetch="getClaimDms"
>
<template #header="{ entity: { claim } }">
{{ claim.id }} - {{ claim.client.name }}
</template>
@ -252,7 +259,8 @@ function openDialog(dmsId) {
>
<ItemDescriptorProxy
v-if="col.name == 'description'"
:id="2"
:id="props.row.id"
:sale-fk="props.row.saleFk"
></ItemDescriptorProxy>
</QTh>
</QTr>
@ -274,7 +282,6 @@ function openDialog(dmsId) {
</template>
</QTable>
</QCard>
<QCard class="vn-max" v-if="claimDms.length > 0">
<a class="header" :href="`#/claim/${entityId}/photos`">
{{ t('claim.summary.photos') }}

View File

@ -116,7 +116,13 @@ function navigate(event, id) {
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.viewDescription')"
:label="t('components.smartCard.openCard')"
@click.stop="navigate(row.id)"
class="bg-vn-dark"
outline
/>
<QBtn
:label="t('globals.description')"
@click.stop
class="bg-vn-dark"
outline

View File

@ -123,7 +123,7 @@ const creditWarning = computed(() => {
</QCard>
<QCard class="vn-one">
<a class="header link" :href="clientUrl + `fiscal-data`" link>
{{ t('customer.summary.fiscalAddress') }}
{{ t('customer.summary.fiscalData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<VnLv
@ -236,7 +236,8 @@ const creditWarning = computed(() => {
link
>
{{ t('customer.summary.financialData') }}
<QIcon name="vn:grafana" color="primary" />
<QIcon name="open_in_new" color="primary" />
<!-- Pendiente de añadir el icono <QIcon name="vn:grafana" color="primary" /> -->
</a>
<VnLv
:label="t('customer.summary.risk')"
@ -277,6 +278,16 @@ const creditWarning = computed(() => {
:label="t('customer.summary.recoverySince')"
:value="toDate(entity.recovery.started)"
/>
<VnLv
:label="t('customer.summary.rating')"
:value="entity.rating"

falta traduccion

falta traduccion

Corregido: 3bcd4984f5

Corregido: 3bcd4984f5

Lo veo OK

Lo veo OK
:info="t('valueInfo', { min: 1, max: 20 })"
/>
<VnLv
:label="t('customer.summary.recommendCredit')"
:value="entity.recommendedCredit"
/>
</QCard>
<QCard>
<div class="header">
@ -288,7 +299,18 @@ const creditWarning = computed(() => {
</CardSummary>
</template>
<style lang="scss" scoped>
@media (min-width: $breakpoint-md) {
.summary .vn-one {
min-width: 300px;
}
}
</style>
<i18n>
en:
valueInfo: Value from {min} to {max}. The higher the better value
es:
valueInfo: Valor de {min} a {max}. Cuanto más alto, mejor valor
Latest tickets: Últimos tickets
</i18n>

View File

@ -4,10 +4,6 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { QBtn, QIcon } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { dashIfEmpty, toDate } from 'src/filters';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
@ -16,6 +12,9 @@ import CustomerExtendedListFilter from './CustomerExtendedListFilter.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters';
const { t } = useI18n();
const router = useRouter();
const stateStore = useStateStore();
@ -466,17 +465,11 @@ const stopEventPropagation = (event, col) => {
event.stopPropagation();
};
const navigateToTravelId = (id) => {
router.push({ path: `/customer/${id}` });
};
const navigateToTravelId = (id) => router.push({ path: `/customer/${id}` });
const selectCustomerId = (id) => {
selectedCustomerId.value = id;
};
const selectCustomerId = (id) => (selectedCustomerId.value = id);
const selectSalesPersonId = (id) => {
selectedSalesPersonId.value = id;
};
const selectSalesPersonId = (id) => (selectedSalesPersonId.value = id);
</script>
<template>
@ -517,37 +510,50 @@ const selectSalesPersonId = (id) => {
class="full-width q-mt-md"
row-key="id"
:visible-columns="visibleColumns"
@row-click="(evt, row, id) => navigateToTravelId(row.id)"
>
<template #body="props">
<QTr
:props="props"
@click="navigateToTravelId(props.row.id)"
class="cursor-pointer"
>
<QTd
v-for="col in props.cols"
:key="col.name"
:props="props"
@click="stopEventPropagation($event, col)"
<template #body-cell="{ col, value }">
<QTd @click="stopEventPropagation($event, col)">
{{ value }}
</QTd>
</template>
<template #body-cell-id="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
>
<component
:is="tableColumnComponents[col.name].component"
class="col-content"
v-bind="tableColumnComponents[col.name].props(props)"
@click="tableColumnComponents[col.name].event(props)"
>
{{ dashIfEmpty(col.value) }}
<WorkerDescriptorProxy
v-if="props.row.salesPersonFk"
:id="selectedSalesPersonId"
/>
<CustomerDescriptorProxy
v-if="props.row.id"
:id="selectedCustomerId"
/>
</component>
</QTd>
</QTr>
<CustomerDescriptorProxy :id="props.row.id" />
{{ props.row.id }}
</component>
</QTd>
</template>
<template #body-cell-salesPersonFk="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
v-if="props.row.salesPerson"
class="col-content"
:is="tableColumnComponents[props.col.name].component"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
>
<WorkerDescriptorProxy :id="props.row.salesPersonFk" />
{{ props.row.salesPerson }}
</component>
<span class="col-content" v-else>-</span>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
/>
</QTd>
</template>
</QTable>
</QPage>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import VnLocation from 'src/components/common/VnLocation.vue';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
@ -20,13 +20,9 @@ const router = useRouter();
const formInitialData = reactive({ isDefaultAddress: false });
const townsFetchDataRef = ref(null);
const postcodeFetchDataRef = ref(null);
const urlCreate = ref('');
const postcodesOptions = ref([]);
const citiesLocationOptions = ref([]);
const provincesLocationOptions = ref([]);
const agencyModes = ref([]);
const incoterms = ref([]);
const customsAgents = ref([]);
@ -36,14 +32,6 @@ onBeforeMount(() => {
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 { data } = await axios.get('CustomsAgents');
customsAgents.value = data;
@ -61,26 +49,16 @@ const toCustomerAddress = () => {
},
});
};
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postalCode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script>
<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
@on-fetch="(data) => (agencyModes = data)"
auto-load
@ -127,85 +105,14 @@ const toCustomerAddress = () => {
jsegarra marked this conversation as resolved Outdated

El layout debe tener(salto de linea = 1 fila)
1 checkbox
2 inputs
El componente VnLocation ocupa toda la fila
3 inputs
1 inputs y un input con el botón de crear

El layout debe tener(salto de linea = 1 fila) 1 checkbox 2 inputs El componente VnLocation ocupa toda la fila 3 inputs 1 inputs y un input con el botón de crear

Corregido: 38d3d49ecb

Corregido: 38d3d49ecb
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectDialog
:label="t('Postcode')"
:options="postcodesOptions"
:roles-allowed-to-create="['deliveryAssistant']"
<VnLocation
:rules="validate('Worker.postcode')"
hide-selected
option-label="code"
option-value="code"
v-model="data.postalCode"
>
<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>
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
></VnLocation>
</div>
<div class="col">
<!-- ciudades -->
<VnSelectFilter
:label="t('City')"
:options="citiesLocationOptions"
:rules="validate('postcode.city')"
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 class="col">
<VnSelectFilter
:label="t('Province')"
:options="provincesLocationOptions"
:rules="validate('city.provinceFk')"
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>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Agency')"

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import VnLocation from 'src/components/common/VnLocation.vue';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
@ -18,13 +18,8 @@ const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const townsFetchDataRef = ref(null);
const postcodeFetchDataRef = ref(null);
const urlUpdate = ref('');
const postcodesOptions = ref([]);
const citiesLocationOptions = ref([]);
const provincesLocationOptions = ref([]);
const agencyModes = ref([]);
const incoterms = ref([]);
const customsAgents = ref([]);
@ -36,14 +31,6 @@ onBeforeMount(() => {
urlUpdate.value = `Clients/${route.params.id}/updateAddress/${route.params.addressId}`;
});
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) => {
observationTypes.value = observations;
@ -118,26 +105,16 @@ const toCustomerAddress = () => {
},
});
};
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postalCode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script>
<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
@on-fetch="(data) => (agencyModes = data)"
auto-load
@ -199,84 +176,14 @@ const toCustomerAddress = () => {
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectDialog
:label="t('Postcode')"
:options="postcodesOptions"
:roles-allowed-to-create="['deliveryAssistant']"
<VnLocation
:rules="validate('Worker.postcode')"
hide-selected
option-label="code"
option-value="code"
v-model="data.postalCode"
>
<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>
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
></VnLocation>
</div>
<div class="col">
<VnSelectFilter
:label="t('City')"
:options="citiesLocationOptions"
:rules="validate('postcode.city')"
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 class="col">
<VnSelectFilter
:label="t('Province')"
:options="provincesLocationOptions"
:rules="validate('city.provinceFk')"
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>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
: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',
},
{
label: t('entry.buys.description'),
label: t('globals.description'),
name: 'description',
field: 'description',
align: 'left',
@ -214,7 +214,7 @@ const redirectToBuysView = () => {
class="cursor-pointer"
@click="inputFileRef.pickFiles()"
>
<QTooltip>{{ t('Select a file') }}</QTooltip>
<QTooltip>{{ t('globals.selectFile') }}</QTooltip>
</QIcon>
</template>
</QFile>
@ -292,6 +292,6 @@ const redirectToBuysView = () => {
<i18n>
es:
Select a file: Selecciona un fichero
globals.selectFile: Selecciona un fichero
Some of the imported buys does not have an item: Algunas de las compras importadas no tienen un artículo
</i18n>

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

@ -63,7 +63,7 @@ onMounted(() => {
</div>
<div class="col">
<VnInput
:label="t('entry.notes.description')"
:label="t('globals.description')"
v-model="row.description"
:rules="validate('EntryObservation.description')"
/>

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script setup>
import { computed, ref, onMounted, onUnmounted } from 'vue';
import { computed, ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
@ -14,6 +14,7 @@ import EditPictureForm from 'components/EditPictureForm.vue';
import { useState } from 'src/composables/useState';
import useCardDescription from 'src/composables/useCardDescription';
import { useSession } from 'src/composables/useSession';
import { getUrl } from 'src/composables/getUrl';
import axios from 'axios';
const $props = defineProps({
@ -30,6 +31,10 @@ const $props = defineProps({
type: String,
default: null,
},
saleFk: {
type: Number,
default: null,
},
});
const quasar = useQuasar();
@ -51,6 +56,7 @@ const available = ref(null);
const visible = ref(null);
const _warehouseFk = ref(null);
const warehouseText = ref(null);
const salixUrl = ref();
const warehouseFk = computed({
get() {
return _warehouseFk.value;
@ -69,6 +75,7 @@ const showEditPhotoForm = ref(false);
onMounted(async () => {
await getItemAvatar();
warehouseFk.value = user.value.warehouseFk;
salixUrl.value = await getUrl('');
});
const getItemAvatar = async () => {
@ -276,7 +283,16 @@ const openCloneDialog = async () => {
<template #actions="{}">
<QCardActions class="row justify-center">
<QBtn
:to="{ name: 'ItemDiary' }"
:href="
salixUrl +
'item/' +
entityId +
'/diary?' +
'warehouseFk=' +
warehouseFk +
'&lineFk=' +
$props.saleFk
"
size="md"
icon="vn:transaction"
color="primary"

View File

@ -11,6 +11,10 @@ const $props = defineProps({
type: String,
default: null,
},
saleFk: {
type: Number,
default: null,
},
});
</script>
@ -21,6 +25,7 @@ const $props = defineProps({
:id="$props.id"
:summary="ItemSummary"
:dated="dated"
:sale-fk="saleFk"
/>
</QPopupProxy>
</template>

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import { useStateStore } from 'stores/useStateStore';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'components/ui/VnLv.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({
id: {

View File

@ -10,6 +10,7 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import VnRow from 'components/ui/VnRow.vue';
import CustomerCreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
const { t } = useI18n();
const route = useRoute();
@ -55,27 +56,16 @@ onMounted(() => {
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>
<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>
<FormModel
model="supplierAddresses"
@ -104,59 +94,15 @@ onMounted(() => {
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectDialog
v-model="data.postalCode"
:label="t('supplier.addresses.postcode')"
:rules="validate('supplierAddress.postcode')"
<VnLocation
:rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
option-label="code"
option-value="code"
hide-selected
>
<template #form>
<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"
/>
v-model="data.location"
@update:model-value="
(location) => handleLocation(data, location)
"
></VnLocation>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">

View File

@ -21,7 +21,7 @@ const postcodesOptions = ref([]);
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postcode = code;
data.postCode = code;
data.city = town;
data.provinceFk = provinceFk;
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.available') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.description') }}</QTh>
<QTh auto-width>{{ t('globals.description') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.price') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.discount') }}</QTh>
<QTh auto-width>{{ t('globals.amount') }}</QTh>
@ -425,7 +425,7 @@ async function changeState(value) {
<template #header="props">
<QTr :props="props">
<QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.description') }}</QTh>
<QTh auto-width>{{ t('globals.description') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.price') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.taxClass') }}</QTh>
<QTh auto-width>{{ t('globals.amount') }}</QTh>

View File

@ -300,7 +300,7 @@ const onThermographCreated = async (data) => {
<VnRow v-if="viewAction === 'edit'" class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('travel.thermographs.description')"
:label="t('globals.description')"
type="textarea"
v-model="thermographForm.description"
fill-input

View File

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

View File

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

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.getValue(firstLineCode).should(
'have.value',
'equal',
'Plantas vivas: Esqueje/injerto, Vid'
);
});

View File

@ -21,7 +21,7 @@ describe('InvoiceInVat', () => {
cy.saveCard();
cy.visit(`/#/invoice-in/1/vat`);
cy.getValue(firstLineVat).should('have.value', 'H.P. IVA 21% CEE');
cy.getValue(firstLineVat).should('equal', 'H.P. IVA 21% CEE');
});
it('should add a new row', () => {

View File

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