CambiosSolicitadosEntries #201

Merged
alexm merged 17 commits from :CambiosSolicitadosEntries into dev 2024-03-14 06:39:45 +00:00
121 changed files with 3588 additions and 1642 deletions
Showing only changes of commit f26d14e875 - Show all commits

View File

@ -1,5 +1,6 @@
FROM node:stretch-slim
RUN npm install -g @quasar/cli
RUN corepack enable pnpm
RUN pnpm install -g @quasar/cli
WORKDIR /app
COPY dist/spa ./
CMD ["quasar", "serve", "./", "--history", "--hostname", "0.0.0.0"]

18
Jenkinsfile vendored
View File

@ -62,7 +62,7 @@ pipeline {
NODE_ENV = ""
}
steps {
sh 'npm install --prefer-offline'
sh 'pnpm install --prefer-offline'
}
}
stage('Test') {
@ -73,18 +73,14 @@ pipeline {
NODE_ENV = ""
}
steps {
sh 'npm run test:unit:ci'
sh 'pnpm run test:unit:ci'
}
post {
post {
always {
script {
try {
junit 'junitresults.xml'
junit 'junit.xml'
} catch (e) {
echo e.toString()
}
}
junit(
testResults: 'junitresults.xml',
allowEmptyResults: true
)
}
}
}

View File

@ -1,10 +1,11 @@
{
"name": "salix-front",
"version": "24.10.0",
"version": "24.12.0",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",
"private": true,
"packageManager": "pnpm@8.15.1",
"scripts": {
"lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
@ -16,12 +17,12 @@
},
"dependencies": {
"@quasar/cli": "^2.3.0",
"@quasar/extras": "^1.16.4",
"@quasar/extras": "^1.16.9",
"axios": "^1.4.0",
"chromium": "^3.0.3",
"croppie": "^2.6.5",
"pinia": "^2.1.3",
"quasar": "^2.12.0",
"quasar": "^2.14.5",
"validator": "^13.9.0",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
@ -30,11 +31,11 @@
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^0.8.1",
"@pinia/testing": "^0.1.2",
"@quasar/app-vite": "^1.4.3",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.3.0",
"@vue/test-utils": "^2.3.2",
"@quasar/app-vite": "^1.7.3",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0",
"@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.14",
"cypress": "^12.13.0",
"cypress": "^13.6.6",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-cypress": "^2.13.3",
@ -50,8 +51,8 @@
"bun": ">= 1.0.25"
},
"overrides": {
"@vitejs/plugin-vue": "^4.0.0",
"vite": "^4.3.5",
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.1.4",
"vitest": "^0.31.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -67,7 +67,7 @@ module.exports = configure(function (/* ctx */) {
// analyze: true,
// env: {},
rawDefine: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
// ignorePublicFolder: true,
// minify: false,
@ -92,7 +92,7 @@ module.exports = configure(function (/* ctx */) {
vitePlugins: [
[
VueI18nPlugin({
runtimeOnly: false
runtimeOnly: false,
}),
{
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
@ -123,9 +123,6 @@ module.exports = configure(function (/* ctx */) {
framework: {
config: {
config: {
brand: {
primary: 'orange',
},
dark: 'auto',
},
},

View File

@ -1,5 +1,5 @@
<script setup>
import { reactive, ref } from 'vue';
import { reactive, ref, onMounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
@ -16,9 +16,8 @@ const props = defineProps({
});
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const bicInputRef = ref(null);
const bankEntityFormData = reactive({
name: null,
bic: null,
@ -32,9 +31,14 @@ const countriesFilter = {
const countriesOptions = ref([]);
const onDataSaved = (data) => {
emit('onDataSaved', data);
const onDataSaved = (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
};
onMounted(async () => {
await nextTick();
bicInputRef.value.focus();
});
</script>
<template>
@ -50,7 +54,7 @@ const onDataSaved = (data) => {
:title="t('title')"
:subtitle="t('subtitle')"
:form-initial-data="bankEntityFormData"
@on-data-saved="onDataSaved($event)"
@on-data-saved="onDataSaved"
>
<template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
@ -64,6 +68,7 @@ const onDataSaved = (data) => {
</div>
<div class="col">
<VnInput
ref="bicInputRef"
:label="t('swift')"
v-model="data.bic"
:required="true"

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

@ -71,13 +71,9 @@ const closeForm = () => {
<span ref="closeButton" class="close-icon" v-close-popup>
<QIcon name="close" size="sm" />
</span>
<h1 class="title">
{{
t('editBuyTitle', {
buysAmount: rows.length,
})
}}
</h1>
<span class="title">{{ t('Edit') }}</span>
<span class="countLines">{{ ` ${rows.length} ` }}</span>
<span class="title">{{ t('buy(s)') }}</span>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
@ -94,23 +90,23 @@ const closeForm = () => {
</div>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn
:label="t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<QBtn
:label="t('globals.cancel')"
type="reset"
color="primary"
flat
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
v-close-popup
/>
<QBtn
:label="t('globals.save')"
type="submit"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
/>
</div>
</QCard>
</QForm>
@ -129,13 +125,18 @@ const closeForm = () => {
right: 20px;
cursor: pointer;
}
.countLines {
font-size: 24px;
color: $primary;
font-weight: bold;
}
</style>
<i18n>
en:
editBuyTitle: Edit {buysAmount} buy(s)
es:
editBuyTitle: Editar {buysAmount} compra(s)
Field to edit: Campo a editar
Value: Valor
</i18n>
es:
Edit: Editar
buy(s): compra(s)
Field to edit: Campo a editar
Value: Valor
</i18n>

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,8 @@ 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';
import { tMobile } from 'src/composables/tMobile';
const quasar = useQuasar();
const state = useState();
@ -41,6 +44,10 @@ const $props = defineProps({
type: Boolean,
default: true,
},
defaultButtons: {
type: Object,
default: () => {},
},
autoLoad: {
type: Boolean,
default: false,
@ -63,6 +70,10 @@ const $props = defineProps({
type: Boolean,
default: true,
},
saveFn: {
type: Function,
default: null,
},
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
@ -79,9 +90,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();
}
@ -94,6 +104,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(() => {
// Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas.
if (hasChanges.value) {
@ -110,7 +133,19 @@ const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({ ...$props.formInitialData });
const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({
save: {
color: 'primary',
icon: 'restart_alt',
label: 'globals.save',
},
reset: {
color: 'primary',
icon: 'save',
label: 'globals.reset',
},
...$props.defaultButtons,
}));
const startFormWatcher = () => {
watch(
() => formData.value,
@ -122,10 +157,6 @@ const startFormWatcher = () => {
);
};
function tMobile(...args) {
if (!quasar.platform.is.mobile) return t(...args);
}
async function fetch() {
const { data } = await axios.get($props.url, {
params: { filter: JSON.stringify($props.filter) },
@ -147,17 +178,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;
}
@ -221,21 +255,21 @@ watch(formUrl, async () => {
<QBtnGroup push class="q-gutter-x-sm">
<slot name="moreActions" />
<QBtn
:label="tMobile('globals.reset')"
color="primary"
icon="restart_alt"
:label="tMobile(defaultButtons.reset.label)"
:color="defaultButtons.reset.color"
:icon="defaultButtons.reset.icon"
flat
@click="reset"
:disable="!hasChanges"
:title="t('globals.reset')"
:title="t(defaultButtons.reset.label)"
/>
<QBtn
:label="tMobile('globals.save')"
color="primary"
icon="save"
:label="tMobile(defaultButtons.save.label)"
:color="defaultButtons.save.color"
:icon="defaultButtons.save.icon"
@click="save"
:disable="!hasChanges"
:title="t('globals.save')"
:title="t(defaultButtons.save.label)"
/>
</QBtnGroup>
</div>
@ -258,3 +292,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

@ -42,8 +42,8 @@ const { t } = useI18n();
const closeButton = ref(null);
const isLoading = ref(false);
const onDataSaved = (dataSaved) => {
emit('onDataSaved', dataSaved);
const onDataSaved = (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
closeForm();
};
@ -59,7 +59,7 @@ const closeForm = () => {
:default-actions="false"
:url-create="urlCreate"
:model="model"
@on-data-saved="onDataSaved($event)"
@on-data-saved="onDataSaved"
>
<template #form="{ data, validate }">
<span ref="closeButton" class="close-icon" v-close-popup>

View File

@ -0,0 +1,34 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useCapitalize } from 'src/composables/useCapitalize';
import VnInput from 'src/components/common/VnInput.vue';
const props = defineProps({
modelValue: { type: String, default: '' },
});
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']);
const amount = computed({
get() {
return props.modelValue;
},
set(val) {
emit('update:modelValue', val);
},
});
</script>
<template>
<VnInput
v-model="amount"
type="number"
step="any"
:label="useCapitalize(t('amount'))"
/>
</template>
<i18n>
es:
amount: importe
</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

@ -1,5 +1,5 @@
<script setup>
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const emit = defineEmits(['update:modelValue', 'update:options', 'keyup.enter']);
@ -17,7 +17,7 @@ const $props = defineProps({
const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const vnInputRef = ref(null);
const value = computed({
get() {
return $props.modelValue;
@ -40,6 +40,14 @@ const styleAttrs = computed(() => {
const onEnterPress = () => {
emit('keyup.enter');
};
const focus = () => {
vnInputRef.value.focus();
};
defineExpose({
focus,
});
</script>
<template>

View File

@ -1,6 +1,7 @@
<script setup>
import { computed, ref } from 'vue';
import { toDate } from 'src/filters';
import VnInput from 'components/common/VnInput.vue';
import isValidDate from "filters/isValidDate";
const props = defineProps({
modelValue: {
@ -17,12 +18,25 @@ const props = defineProps({
},
});
const emit = defineEmits(['update:modelValue']);
const joinDateAndTime = (date, time) => {
if (!date) {
return null;
}
if (!time) {
return new Date(date).toISOString();
}
const [year, month, day] = date.split('/');
return new Date(`${year}-${month}-${day}T${time}`).toISOString();
};
const time = computed(() => (props.modelValue ? props.modelValue.split('T')?.[1] : null));
const value = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value ? new Date(value).toISOString() : null);
emit('update:modelValue', joinDateAndTime(value, time.value));
},
});
@ -40,6 +54,16 @@ const formatDate = (dateString) => {
date.getDate()
)}`;
};
const displayDate = (dateString) => {
if (!dateString || !isValidDate(dateString)) {
return '';
}
return new Date(dateString).toLocaleDateString([], {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
};
const styleAttrs = computed(() => {
return props.isOutlined
@ -53,12 +77,12 @@ const styleAttrs = computed(() => {
</script>
<template>
<QInput
<VnInput
class="vn-input-date"
rounded
readonly
:model-value="toDate(value)"
:model-value="displayDate(value)"
v-bind="{ ...$attrs, ...styleAttrs }"
readonly
@click="isPopupOpen = true"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
@ -76,7 +100,7 @@ const styleAttrs = computed(() => {
</QPopupProxy>
</QIcon>
</template>
</QInput>
</VnInput>
</template>
<style lang="scss">

View File

@ -1,8 +1,8 @@
<script setup>
import { computed, ref } from 'vue';
import { toHour } from 'src/filters';
import { useI18n } from 'vue-i18n';
import isValidDate from 'filters/isValidDate';
import VnInput from "components/common/VnInput.vue";
const props = defineProps({
modelValue: {
@ -26,8 +26,8 @@ const value = computed({
},
set(value) {
const [hours, minutes] = value.split(':');
const date = new Date();
date.setUTCHours(
const date = new Date(props.modelValue);
date.setHours(
Number.parseInt(hours) || 0,
Number.parseInt(minutes) || 0,
0,
@ -37,6 +37,7 @@ const value = computed({
},
});
const isPopupOpen = ref(false);
const onDateUpdate = (date) => {
internalValue.value = date;
};
@ -45,7 +46,7 @@ const save = () => {
value.value = internalValue.value;
};
const formatTime = (dateString) => {
if (!isValidDate(dateString)) {
if (!dateString || !isValidDate(dateString)) {
return '';
}
@ -70,16 +71,17 @@ const styleAttrs = computed(() => {
</script>
<template>
<QInput
<VnInput
class="vn-input-time"
rounded
readonly
:model-value="toHour(value)"
:model-value="formatTime(value)"
v-bind="{ ...$attrs, ...styleAttrs }"
@click="isPopupOpen = true"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
v-model="isPopupOpen"
cover
transition-show="scale"
transition-hide="scale"
@ -109,7 +111,7 @@ const styleAttrs = computed(() => {
</QPopupProxy>
</QIcon>
</template>
</QInput>
</VnInput>
</template>
<style lang="scss">

View File

@ -1036,7 +1036,6 @@ en:
claimStateFk: Claim State
workerFk: Worker
clientFk: Customer
rma: RMA
responsibility: Responsibility
packages: Packages
es:
@ -1046,7 +1045,7 @@ es:
tooltips:
search: Buscar por identificador o concepto
changes: Buscar por cambios. Los atributos deben buscarse por su nombre interno, para obtenerlo situar el cursor sobre el atributo.
Audit logs: Registros de auditoría
Audit logs: Historial
Property: Propiedad
Before: Antes
After: Después
@ -1076,7 +1075,6 @@ es:
claimStateFk: Estado de la reclamación
workerFk: Trabajador
clientFk: Cliente
rma: RMA
responsibility: Responsabilidad
packages: Bultos
</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,11 +174,10 @@ 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.'
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.'
orderChanges: 'Order {orderId} of { shipped }: { changes }'
en: English
@ -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
@ -205,7 +193,7 @@ es:
templates:
pendingPayment: 'Su pedido está pendiente de pago.
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.'
orderChanges: 'Pedido {orderId} día { shipped }: { changes }'
en: Inglés
@ -222,7 +210,7 @@ fr:
templates:
pendingPayment: 'Votre commande est en attente de paiement.
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.'
orderChanges: 'Commande { orderId } du { shipped }: { changes }'
en: Anglais
@ -239,7 +227,7 @@ pt:
templates:
pendingPayment: 'Seu pedido está pendente de pagamento.
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.'
orderChanges: 'Pedido { orderId } dia { shipped }: { changes }'
en: Inglês

View File

@ -175,6 +175,7 @@ const emit = defineEmits(['onFetch']);
<style lang="scss">
.body {
background-color: var(--vn-gray);
.text-h5 {
padding-top: 5px;
padding-bottom: 5px;
@ -192,7 +193,8 @@ const emit = defineEmits(['onFetch']);
.label {
color: var(--vn-label);
font-size: 12px;
::after {
&:not(:has(a))::after {
content: ':';
}
}
@ -227,8 +229,6 @@ const emit = defineEmits(['onFetch']);
margin-bottom: 15px;
}
.list-box {
background-color: var(--vn-gray);
.q-item__label {
color: var(--vn-label);
}

View File

@ -1,11 +1,10 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import axios from 'axios';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
onMounted(() => fetch());
const entity = ref();
const props = defineProps({
url: {
@ -16,14 +15,25 @@ const props = defineProps({
type: Object,
default: null,
},
entityId: {
type: Number,
default: null,
},
});
const emit = defineEmits(['onFetch']);
const route = useRoute();
const isSummary = ref();
defineExpose({
entity,
fetch,
});
onMounted(() => {
isSummary.value = String(route.path).endsWith('/summary');
fetch();
});
async function fetch() {
const params = {};
@ -46,9 +56,19 @@ watch(props, async () => {
<QCard class="cardSummary">
<SkeletonSummary v-if="!entity" />
<template v-if="entity">
<div class="summaryHeader bg-primary q-pa-md text-weight-bolder">
<div class="summaryHeader bg-primary q-pa-sm text-weight-bolder">
<slot name="header-left">
<span></span>
<router-link
v-if="!isSummary && route.meta.moduleName"
class="header link"
:to="{
name: `${route.meta.moduleName}Summary`,
params: { id: entityId || entity.id },
}"
>
<QIcon name="open_in_new" color="white" size="sm" />
</router-link>
<span v-else></span>
</slot>
<slot name="header" :entity="entity">
<VnLv :label="`${entity.id} -`" :value="entity.name" />
@ -85,8 +105,8 @@ watch(props, async () => {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
gap: 15px;
padding: 15px;
gap: 10px;
padding: 10px;
background-color: var(--vn-gray);
> .q-card.vn-one {
@ -105,14 +125,14 @@ watch(props, async () => {
> .q-card {
width: 100%;
background-color: var(--vn-gray);
padding: 15px;
padding: 7px;
font-size: 16px;
min-width: 275px;
.vn-label-value {
display: flex;
flex-direction: row;
margin-top: 5px;
margin-top: 2px;
.label {
color: var(--vn-label);
width: 8em;
@ -131,13 +151,26 @@ watch(props, async () => {
.header {
color: $primary;
font-weight: bold;
margin-bottom: 25px;
margin-bottom: 10px;
font-size: 20px;
display: inline-block;
}
.header.link:hover {
color: lighten($primary, 20%);
}
.q-checkbox {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label {
margin-left: 31px;
color: var(--vn-text);
}
& .q-checkbox__inner {
position: absolute;
left: 0;
color: var(--vn-label);
}
}
}
}

View File

@ -13,12 +13,49 @@ defineProps({
<template>
<div class="fetchedTags">
<div class="wrap">
<div class="inline-tag" :class="{ empty: !$props.item.value5 }">{{ $props.item.value5 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value6 }">{{ $props.item.value6 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value7 }">{{ $props.item.value7 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value8 }">{{ $props.item.value8 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value9 }">{{ $props.item.value9 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value10 }">{{ $props.item.value10 }}</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.value5 }"
:title="$props.item.tag5 + ': ' + $props.item.value5"
>
{{ $props.item.value5 }}
</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.tag6 }"
:title="$props.item.tag6 + ': ' + $props.item.value6"
>
{{ $props.item.value6 }}
</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.value7 }"
:title="$props.item.tag7 + ': ' + $props.item.value7"
>
{{ $props.item.value7 }}
</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.value8 }"
:title="$props.item.tag8 + ': ' + $props.item.value8"
>
{{ $props.item.value8 }}
</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.value9 }"
:title="$props.item.tag9 + ': ' + $props.item.value9"
>
{{ $props.item.value9 }}
</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.value10 }"
:title="$props.item.tag10 + ': ' + $props.item.value10"
>
{{ $props.item.value10 }}
</div>
</div>
</div>
</template>

View File

@ -1,8 +1,8 @@
<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 +13,8 @@ const $props = defineProps({
dash: { type: Boolean, default: true },
copy: { type: Boolean, default: false },
});
const isBooleanValue = computed(() => typeof $props.value === 'boolean');
const { t } = useI18n();
const { copyText } = useClipboard();
function copyValueText() {
@ -40,36 +40,36 @@ function copyValueText() {
</slot>
</div>
<div class="value">
<span v-if="isBooleanValue">
<QIcon
:name="$props.value ? `check` : `close`"
:color="$props.value ? `positive` : `negative`"
size="sm"
/>
</span>
<slot v-else name="value">
<slot name="value">
<span :title="$props.value">
{{ $props.dash ? dashIfEmpty($props.value) : $props.value }}
</span>
</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

@ -8,7 +8,6 @@ import VnPaginate from './VnPaginate.vue';
import VnUserLink from '../ui/VnUserLink.vue';
const $props = defineProps({
id: { type: String, required: true },
url: { type: String, default: null },
filter: { type: Object, default: () => {} },
body: { type: Object, default: () => {} },
@ -28,7 +27,7 @@ async function insert() {
}
</script>
<template>
<div class="column items-center full-height">
<div class="column items-center full-height full-width">
<VnPaginate
:data-key="$props.url"
:url="$props.url"
@ -39,28 +38,38 @@ async function insert() {
ref="vnPaginateRef"
>
<template #body="{ rows }">
<QCard class="q-pa-xs q-mb-md" v-for="(note, index) in rows" :key="index">
<QCardSection horizontal>
<slot name="picture">
<VnAvatar :descriptor="false" :worker-id="note.workerFk" />
</slot>
<QItem class="full-width justify-between items-start">
<VnUserLink
:name="`${note.worker.user.nickname}`"
:worker-id="note.worker.id"
/>
<slot name="actions">
{{ toDateHour(note.created) }}
<div class="column items-center full-width">
<QCard
class="q-pa-xs q-mb-sm full-width"
v-for="(note, index) in rows"
:key="index"
>
<QCardSection horizontal>
<slot name="picture">
<VnAvatar
:descriptor="false"
:worker-id="note.workerFk"
size="md"
/>
</slot>
</QItem>
</QCardSection>
<QCardSection class="q-pa-sm">
<slot name="text">
{{ note.text }}
</slot>
</QCardSection>
</QCard>
<div class="full-width row justify-between q-pa-xs">
<VnUserLink
:name="`${note.worker.user.nickname}`"
:worker-id="note.worker.id"
/>
<slot name="actions">
{{ toDateHour(note.created) }}
</slot>
</div>
</QCardSection>
<QCardSection class="q-pa-xs q-my-none q-py-none">
<slot name="text">
{{ note.text }}
</slot>
</QCardSection>
</QCard>
</div>
</template>
</VnPaginate>
<QPageSticky position="bottom-right" :offset="[25, 25]" v-if="addNote">
@ -108,8 +117,10 @@ async function insert() {
</template>
<style lang="scss" scoped>
.q-card {
max-width: 80em;
width: 90%;
@media (max-width: $breakpoint-sm) {
width: 100%;
}
&__section {
word-wrap: break-word;
}

View File

@ -138,7 +138,7 @@ async function onLoad(...params) {
</script>
<template>
<div>
<div class="full-width">
<div
v-if="!props.autoLoad && !store.data && !isLoading"
class="info-row q-pa-md text-center"
@ -175,6 +175,7 @@ async function onLoad(...params) {
@load="onLoad"
:offset="offset"
class="full-width full-height"
v-bind="$attrs"
>
<slot name="body" :rows="store.data"></slot>
<div v-if="isLoading" class="info-row q-pa-md text-center">

View File

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

View File

@ -81,8 +81,9 @@ onMounted(() => {
});
async function search() {
const staticParams = Object.entries(store.userParams)
.filter(([key, value]) => value && (props.staticParams || []).includes(key));
const staticParams = Object.entries(store.userParams).filter(
([key, value]) => value && (props.staticParams || []).includes(key)
);
await arrayData.applyFilter({
params: {
...Object.fromEntries(staticParams),
@ -117,7 +118,12 @@ async function search() {
autofocus
>
<template #prepend>
<QIcon name="search" v-if="!quasar.platform.is.mobile" />
<QIcon
v-if="!quasar.platform.is.mobile"
class="cursor-pointer"
name="search"
@click="search"
/>
</template>
<template #append>
<QIcon
@ -155,11 +161,9 @@ async function search() {
.cursor-info {
cursor: help;
}
.body--light #searchbar {
#searchbar {
.q-field--standout.q-field--highlighted .q-field__control {
background-color: $grey-7;
color: #333;
background-color: var(--vn-text);
}
}
</style>

View File

@ -0,0 +1,7 @@
import { useQuasar } from 'quasar';
export default function() {
const quasar = useQuasar();
return quasar.screen.gt.xs ? 'q-pa-md': 'q-pa-xs';
}

View File

@ -1,17 +1,59 @@
// app global css in SCSS form
@import './icons.scss';
body.body--light {
--fount-color: black;
--vn-sectionColor: #ffffff;
--vn-pageColor: #e0e0e0;
background-color: var(--vn-pageColor);
.q-header .q-toolbar {
color: var(--fount-color);
}
--vn-text: var(--fount-color);
--vn-gray: var(--vn-sectionColor);
--vn-label: #5f5f5f;
--vn-dark: var(--vn-sectionColor);
--vn-light-gray: #e7e3e3;
}
body.body--dark {
--vn-pageColor: #222;
--vn-SectionColor: #3c3b3b;
background-color: var(--vn-pageColor);
--vn-text: white;
--vn-gray: var(--vn-SectionColor);
--vn-label: #a8a8a8;
--vn-dark: var(--vn-SectionColor);
--vn-light-gray: #424242;
}
a {
text-decoration: none;
}
.link {
color: $primary;
color: $color-link;
cursor: pointer;
}
.tx-color-link {
color: $color-link !important;
}
.header-link {
color: $color-link !important;
cursor: pointer;
border-bottom: solid $primary;
border-width: 2px;
width: 100%;
.q-icon {
float: right;
}
text-transform: uppercase;
}
.link:hover {
color: $orange-4;
text-decoration: underline;
}
// Removes chrome autofill background
@ -24,26 +66,6 @@ select:-webkit-autofill {
background-clip: text !important;
}
body.body--light {
.q-header .q-toolbar {
background-color: $white;
color: #555;
}
--vn-text: #000000;
--vn-gray: #f5f5f5;
--vn-label: #5f5f5f;
--vn-dark: white;
--vn-light-gray: #e7e3e3;
}
body.body--dark {
--vn-text: #ffffff;
--vn-gray: #313131;
--vn-label: #a8a8a8;
--vn-dark: #292929;
--vn-light-gray: #424242;
}
.bg-vn-dark {
background-color: var(--vn-dark);
}

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
@font-face {
font-family: 'icon';
src: url('fonts/icon.eot?7zbcv0');
src: url('fonts/icon.eot?7zbcv0#iefix') format('embedded-opentype'),
url('fonts/icon.ttf?7zbcv0') format('truetype'),
url('fonts/icon.woff?7zbcv0') format('woff'),
url('fonts/icon.svg?7zbcv0#icon') format('svg');
src: url('fonts/icon.eot?2omjsr');
src: url('fonts/icon.eot?2omjsr#iefix') format('embedded-opentype'),
url('fonts/icon.ttf?2omjsr') format('truetype'),
url('fonts/icon.woff?2omjsr') format('woff'),
url('fonts/icon.svg?2omjsr#icon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@ -28,6 +28,9 @@
.icon-100:before {
content: "\e926";
}
.icon-Client_unpaid:before {
content: "\e925";
}
.icon-History:before {
content: "\e964";
}
@ -46,9 +49,15 @@
.icon-addperson:before {
content: "\e929";
}
.icon-agency:before {
content: "\e92a";
}
.icon-agency-term:before {
content: "\e92b";
}
.icon-albaran:before {
content: "\e92c";
}
.icon-anonymous:before {
content: "\e92d";
}
@ -172,6 +181,9 @@
.icon-funeral:before {
content: "\e95f";
}
.icon-grafana:before {
content: "\e931";
}
.icon-greenery:before {
content: "\e91e";
}
@ -355,6 +367,9 @@
.icon-traceability:before {
content: "\e919";
}
.icon-transaction:before {
content: "\e93b";
}
.icon-treatments:before {
content: "\e91c";
}

View File

@ -11,26 +11,32 @@
// It's highly recommended to change the default colors
// to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website.
// Tip: to add new colors https://quasar.dev/style/color-palette/#adding-your-own-colors
$primary: #ec8916;
$primary-light: lighten($primary, 35%);
$secondary: #26a69a;
$accent: #9c27b0;
$white: #fff;
$secondary: $primary;
$positive: #21ba45;
$negative: #c10015;
$info: #31ccec;
$warning: #f2c037;
$vnColor: #8ebb27;
// Pendiente de cuadrar con la base de datos
$success: $positive;
$alert: $negative;
$white: #fff;
$dark: #3c3b3b;
// custom
$color-link: #66bfff;
$color-spacer-light: #a3a3a31f;
$color-spacer: #7979794d;
$border-thin-light: 1px solid $color-spacer-light;
$primary-light: lighten($primary, 35%);
$dark-shadow-color: black;
$layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d;
$spacing-md: 16px;
.bg-success {
background-color: $positive;
}
.bg-notice {
background-color: $info;
}
@ -40,12 +46,3 @@ $alert: $negative;
.bg-alert {
background-color: $negative;
}
$color-spacer-light: rgba(255, 255, 255, 0.12);
$color-spacer: rgba(255, 255, 255, 0.3);
$border-thin-light: 1px solid $color-spacer-light;
$dark-shadow-color: #000;
$dark: #292929;
$layout-shadow-dark: 0 0 10px 2px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.24);
$spacing-md: 16px;

93
src/filters/date.js Normal file
View File

@ -0,0 +1,93 @@
/**
* Checks if a given date is valid.
*
* @param {number|string|Date} date - The date to be checked.
* @returns {boolean} True if the date is valid, false otherwise.
*
* @example
* // returns true
* isValidDate(new Date());
*
* @example
* // returns false
* isValidDate('invalid date');
*/
export function isValidDate(date) {
return date && !isNaN(new Date(date).getTime());
}
/**
* Converts a given date to a specific format.
*
* @param {number|string|Date} date - The date to be formatted.
* @returns {string} The formatted date as a string in 'dd/mm/yyyy' format. If the provided date is not valid, an empty string is returned.
*
* @example
* // returns "02/12/2022"
* toDateFormat(new Date(2022, 11, 2));
*/
export function toDateFormat(date) {
if (!isValidDate(date)) {
return '';
}
return new Date(date).toLocaleDateString('es-ES', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
}
/**
* Converts a given date to a specific time format.
*
* @param {number|string|Date} date - The date to be formatted.
* @param {boolean} [showSeconds=false] - Whether to include seconds in the output format.
* @returns {string} The formatted time as a string in 'hh:mm:ss' format. If the provided date is not valid, an empty string is returned. If showSeconds is false, seconds are not included in the output format.
*
* @example
* // returns "00:00"
* toTimeFormat(new Date(2022, 11, 2));
*
* @example
* // returns "00:00:00"
* toTimeFormat(new Date(2022, 11, 2), true);
*/
export function toTimeFormat(date, showSeconds = false) {
if (!isValidDate(date)) {
return '';
}
return new Date(date).toLocaleDateString('es-ES', {
hour: '2-digit',
minute: '2-digit',
second: showSeconds ? '2-digit' : undefined,
});
}
/**
* Converts a given date to a specific date and time format.
*
* @param {number|string|Date} date - The date to be formatted.
* @param {boolean} [showSeconds=false] - Whether to include seconds in the output format.
* @returns {string} The formatted date as a string in 'dd/mm/yyyy, hh:mm:ss' format. If the provided date is not valid, an empty string is returned. If showSeconds is false, seconds are not included in the output format.
*
* @example
* // returns "02/12/2022, 00:00"
* toDateTimeFormat(new Date(2022, 11, 2));
*
* @example
* // returns "02/12/2022, 00:00:00"
* toDateTimeFormat(new Date(2022, 11, 2), true);
*/
export function toDateTimeFormat(date, showSeconds = false) {
if (!isValidDate(date)) {
return '';
}
return new Date(date).toLocaleDateString('es-ES', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: showSeconds ? '2-digit' : undefined,
});
}

View File

@ -24,12 +24,14 @@ export default {
dataCreated: 'Data created',
add: 'Add',
create: 'Create',
edit: 'Edit',
save: 'Save',
remove: 'Remove',
reset: 'Reset',
close: 'Close',
cancel: 'Cancel',
confirm: 'Confirm',
assign: 'Assign',
back: 'Back',
yes: 'Yes',
no: 'No',
@ -64,12 +66,24 @@ export default {
markAll: 'Mark all',
requiredField: 'Required field',
class: 'clase',
type: 'type',
type: 'Type',
reason: 'reason',
noResults: 'No results',
system: 'System',
warehouse: 'Warehouse',
company: 'Company',
fieldRequired: 'Field required',
allowedFilesText: 'Allowed file types: { allowedContentTypes }',
confirmDeletion: 'Confirm deletion',
confirmDeletionMessage: 'Are you sure you want to delete this?',
description: 'Description',
id: 'Id',
order: 'Order',
original: 'Original',
file: 'File',
selectFile: 'Select a file',
copyClipboard: 'Copy on clipboard',
salesPerson: 'SalesPerson',
},
errors: {
statusUnauthorized: 'Access denied',
@ -77,7 +91,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',
@ -160,13 +174,14 @@ export default {
hasDebt: 'Customer has debt',
notChecked: 'Customer not checked',
noWebAccess: 'Web access is disabled',
businessTypeFk: 'Business type',
},
summary: {
basicData: 'Basic data',
fiscalAddress: 'Fiscal address',
fiscalData: 'Fiscal data',
billingData: 'Billing data',
consignee: 'Consignee',
consignee: 'Default consignee',
businessData: 'Business data',
financialData: 'Financial data',
customerId: 'Customer ID',
@ -219,6 +234,8 @@ export default {
recoverySince: 'Recovery since',
businessType: 'Business Type',
city: 'City',
rating: 'Rating',
recommendCredit: 'Recommended credit',
},
basicData: {
socialName: 'Fiscal name',
@ -273,6 +290,7 @@ export default {
basicData: 'Basic data',
buys: 'Buys',
notes: 'Notes',
dms: 'File management',
log: 'Log',
create: 'Create',
latestBuys: 'Latest buys',
@ -344,7 +362,6 @@ export default {
reference: 'Reference',
observations: 'Observations',
item: 'Item',
description: 'Description',
size: 'Size',
packing: 'Packing',
grouping: 'Grouping',
@ -359,7 +376,6 @@ export default {
},
notes: {
observationType: 'Observation type',
description: 'Description',
},
descriptor: {
agency: 'Agency',
@ -372,7 +388,6 @@ export default {
packing: 'Packing',
grouping: 'Grouping',
quantity: 'Quantity',
description: 'Description',
size: 'Size',
tags: 'Tags',
type: 'Type',
@ -425,6 +440,7 @@ export default {
shipped: 'Shipped',
warehouse: 'Warehouse',
customerCard: 'Customer card',
alias: 'Alias',
},
boxing: {
expedition: 'Expedition',
@ -458,7 +474,6 @@ export default {
visible: 'Visible',
available: 'Available',
quantity: 'Quantity',
description: 'Description',
price: 'Price',
discount: 'Discount',
packing: 'Packing',
@ -483,11 +498,9 @@ export default {
claims: 'Claims',
list: 'List',
createClaim: 'Create claim',
rmaList: 'RMA',
summary: 'Summary',
basicData: 'Basic Data',
lines: 'Lines',
rma: 'RMA',
photos: 'Photos',
development: 'Development',
log: 'Audit logs',
@ -504,10 +517,6 @@ export default {
code: 'Code',
records: 'records',
},
rma: {
user: 'User',
created: 'Created',
},
card: {
claimId: 'Claim ID',
assignedTo: 'Assigned',
@ -516,6 +525,8 @@ export default {
ticketId: 'Ticket ID',
customerSummary: 'Customer summary',
claimedTicket: 'Claimed ticket',
saleTracking: 'Sale tracking',
ticketTracking: 'Ticket tracking',
commercial: 'Commercial',
province: 'Province',
zone: 'Zone',
@ -531,7 +542,6 @@ export default {
landed: 'Landed',
quantity: 'Quantity',
claimed: 'Claimed',
description: 'Description',
price: 'Price',
discount: 'Discount',
total: 'Total',
@ -547,7 +557,6 @@ export default {
responsible: 'Responsible',
worker: 'Worker',
redelivery: 'Redelivery',
returnOfMaterial: 'RMA',
},
basicData: {
customer: 'Customer',
@ -555,7 +564,6 @@ export default {
created: 'Created',
state: 'State',
picked: 'Picked',
returnOfMaterial: 'Return of material authorization (RMA)',
},
photo: {
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}',
@ -793,7 +801,6 @@ export default {
orderTicketList: 'Order Ticket List',
details: 'Details',
item: 'Item',
description: 'Description',
quantity: 'Quantity',
price: 'Price',
amount: 'Amount',
@ -827,6 +834,7 @@ export default {
notifications: 'Notifications',
workerCreate: 'New worker',
department: 'Department',
pda: 'PDA',
},
list: {
name: 'Name',
@ -868,6 +876,13 @@ export default {
subscribed: 'Subscribed to the notification',
unsubscribed: 'Unsubscribed from the notification',
},
pda: {
newPDA: 'New PDA',
currentPDA: 'Current PDA',
model: 'Model',
serialNumber: 'Serial number',
removePDA: 'Deallocate PDA',
},
create: {
name: 'Name',
lastName: 'Last name',
@ -1138,7 +1153,6 @@ export default {
warehouse: 'Warehouse',
travelFileDescription: 'Travel id { travelId }',
file: 'File',
description: 'Description',
},
},
item: {
@ -1172,7 +1186,6 @@ export default {
clone: 'Clone',
openCard: 'View',
openSummary: 'Summary',
viewDescription: 'Description',
},
cardDescriptor: {
mainList: 'Main list',

View File

@ -24,12 +24,14 @@ export default {
dataCreated: 'Datos creados',
add: 'Añadir',
create: 'Crear',
edit: 'Modificar',
save: 'Guardar',
remove: 'Eliminar',
reset: 'Restaurar',
close: 'Cerrar',
cancel: 'Cancelar',
confirm: 'Confirmar',
assign: 'Asignar',
back: 'Volver',
yes: 'Si',
no: 'No',
@ -64,12 +66,24 @@ export default {
markAll: 'Marcar todo',
requiredField: 'Campo obligatorio',
class: 'clase',
type: 'tipo',
type: 'Tipo',
reason: 'motivo',
noResults: 'Sin resultados',
system: 'Sistema',
warehouse: 'Almacén',
company: 'Empresa',
fieldRequired: 'Campo requerido',
allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }',
confirmDeletion: 'Confirmar eliminación',
confirmDeletionMessage: '¿Seguro que quieres eliminar?',
description: 'Descripción',
id: 'Id',
order: 'Orden',
original: 'Original',
file: 'Fichero',
selectFile: 'Seleccione un fichero',
copyClipboard: 'Copiar en portapapeles',
salesPerson: 'Comercial',
},
errors: {
statusUnauthorized: 'Acceso denegado',
@ -77,7 +91,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',
@ -159,13 +173,14 @@ export default {
hasDebt: 'El cliente tiene riesgo',
notChecked: 'El cliente no está comprobado',
noWebAccess: 'El acceso web está desactivado',
businessTypeFk: 'Tipo de negocio',
},
summary: {
basicData: 'Datos básicos',
fiscalAddress: 'Dirección fiscal',
fiscalData: 'Datos fiscales',
billingData: 'Datos de facturación',
consignee: 'Consignatario',
consignee: 'Consignatario pred.',
businessData: 'Datos comerciales',
financialData: 'Datos financieros',
customerId: 'ID cliente',
@ -218,6 +233,8 @@ export default {
recoverySince: 'Recobro desde',
businessType: 'Tipo de negocio',
city: 'Población',
rating: 'Clasificación',
recommendCredit: 'Crédito recomendado',
},
basicData: {
socialName: 'Nombre fiscal',
@ -272,6 +289,7 @@ export default {
basicData: 'Datos básicos',
buys: 'Compras',
notes: 'Notas',
dms: 'Gestión documental',
log: 'Historial',
create: 'Crear',
latestBuys: 'Últimas compras',
@ -292,8 +310,8 @@ export default {
reference: 'Referencia',
invoiceNumber: 'Núm. factura',
ordered: 'Pedida',
confirmed: 'Confirmado',
booked: 'Asentado',
confirmed: 'Confirmada',
booked: 'Contabilizada',
raid: 'Redada',
excludedFromAvailable: 'Inventario',
travelReference: 'Referencia',
@ -343,7 +361,6 @@ export default {
reference: 'Referencia',
observations: 'Observaciónes',
item: 'Artículo',
description: 'Descripción',
size: 'Medida',
packing: 'Packing',
grouping: 'Grouping',
@ -358,7 +375,6 @@ export default {
},
notes: {
observationType: 'Tipo de observación',
description: 'Descripción',
},
descriptor: {
agency: 'Agencia',
@ -371,7 +387,6 @@ export default {
packing: 'Packing',
grouping: 'Grouping',
quantity: 'Cantidad',
description: 'Descripción',
size: 'Medida',
tags: 'Etiquetas',
type: 'Tipo',
@ -424,6 +439,7 @@ export default {
shipped: 'Enviado',
warehouse: 'Almacén',
customerCard: 'Ficha del cliente',
alias: 'Alias',
},
boxing: {
expedition: 'Expedición',
@ -457,7 +473,6 @@ export default {
visible: 'Visible',
available: 'Disponible',
quantity: 'Cantidad',
description: 'Descripción',
price: 'Precio',
discount: 'Descuento',
packing: 'Encajado',
@ -482,14 +497,12 @@ export default {
claims: 'Reclamaciones',
list: 'Listado',
createClaim: 'Crear reclamación',
rmaList: 'RMA',
summary: 'Resumen',
basicData: 'Datos básicos',
lines: 'Líneas',
rma: 'RMA',
development: 'Trazabilidad',
photos: 'Fotos',
log: 'Registros de auditoría',
log: 'Historial',
notes: 'Notas',
action: 'Acción',
},
@ -503,10 +516,6 @@ export default {
code: 'Código',
records: 'registros',
},
rma: {
user: 'Usuario',
created: 'Creado',
},
card: {
claimId: 'ID reclamación',
assignedTo: 'Asignada a',
@ -515,6 +524,8 @@ export default {
ticketId: 'ID ticket',
customerSummary: 'Resumen del cliente',
claimedTicket: 'Ticket reclamado',
saleTracking: 'Líneas preparadas',
ticketTracking: 'Estados del ticket',
commercial: 'Comercial',
province: 'Provincia',
zone: 'Zona',
@ -530,7 +541,6 @@ export default {
landed: 'Entregado',
quantity: 'Cantidad',
claimed: 'Reclamado',
description: 'Descripción',
price: 'Precio',
discount: 'Descuento',
total: 'Total',
@ -546,7 +556,6 @@ export default {
responsible: 'Responsable',
worker: 'Trabajador',
redelivery: 'Devolución',
returnOfMaterial: 'RMA',
},
basicData: {
customer: 'Cliente',
@ -554,7 +563,6 @@ export default {
created: 'Creada',
state: 'Estado',
picked: 'Recogida',
returnOfMaterial: 'Autorización de retorno de materiales (RMA)',
},
photo: {
fileDescription:
@ -701,7 +709,6 @@ export default {
orderTicketList: 'Tickets del pedido',
details: 'Detalles',
item: 'Item',
description: 'Descripción',
quantity: 'Cantidad',
price: 'Precio',
amount: 'Monto',
@ -714,7 +721,7 @@ export default {
create: 'Crear',
summary: 'Resumen',
basicData: 'Datos básicos',
log: 'Registros de auditoría',
log: 'Historial',
},
list: {
parking: 'Parking',
@ -746,7 +753,7 @@ export default {
dueDay: 'Vencimiento',
intrastat: 'Intrastat',
corrective: 'Rectificativa',
log: 'Registros de auditoría',
log: 'Historial',
},
list: {
ref: 'Referencia',
@ -827,6 +834,7 @@ export default {
notifications: 'Notificaciones',
workerCreate: 'Nuevo trabajador',
department: 'Departamentos',
pda: 'PDA',
},
list: {
name: 'Nombre',
@ -868,6 +876,13 @@ export default {
subscribed: 'Se ha suscrito a la notificación',
unsubscribed: 'Se ha dado de baja de la notificación',
},
pda: {
newPDA: 'Nueva PDA',
currentPDA: 'PDA Actual',
model: 'Modelo',
serialNumber: 'Número de serie',
removePDA: 'Desasignar PDA',
},
create: {
name: 'Nombre',
lastName: 'Apellido',
@ -1138,7 +1153,6 @@ export default {
warehouse: 'Almacén',
travelFileDescription: 'Id envío { travelId }',
file: 'Fichero',
description: 'Descripción',
},
},
item: {
@ -1172,7 +1186,6 @@ export default {
clone: 'Clonar',
openCard: 'Ficha',
openSummary: 'Detalles',
viewDescription: 'Descripción',
},
cardDescriptor: {
mainList: 'Listado principal',

View File

@ -40,7 +40,7 @@ const langs = ['en', 'es'];
<template>
<QLayout view="hHh LpR fFf">
<QHeader reveal class="bg-dark">
<QHeader reveal class="bg-vn-dark">
<QToolbar class="justify-end">
<QBtn
id="switchLanguage"

View File

@ -24,7 +24,6 @@ const claimFilter = {
'workerFk',
'claimStateFk',
'packages',
'rma',
'hasToPickUp',
],
include: [
@ -169,13 +168,6 @@ const statesFilter = {
type="number"
/>
</div>
<div class="col">
<VnInput
v-model="data.rma"
:label="t('claim.basicData.returnOfMaterial')"
:rules="validate('claim.rma')"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">

View File

@ -5,6 +5,7 @@ import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n';
import ClaimDescriptor from './ClaimDescriptor.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
@ -28,7 +29,9 @@ const { t } = useI18n();
<QPageContainer>
<QPage>
<VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -105,7 +105,6 @@ onMounted(async () => {
<ClaimDescriptorMenu :claim="entity" />
</template>
<template #body="{ entity }">
<VnLv :label="t('claim.card.created')" :value="toDate(entity.created)" />
<VnLv v-if="entity.claimState" :label="t('claim.card.state')">
<template #value>
<QBadge :color="stateColor(entity.claimState.code)" dense>
@ -113,13 +112,13 @@ onMounted(async () => {
</QBadge>
</template>
</VnLv>
<VnLv :label="t('claim.card.ticketId')">
<VnLv :label="t('claim.card.created')" :value="toDate(entity.created)" />
<VnLv :label="t('claim.card.commercial')">
<template #value>
<span class="link">
{{ entity.ticketFk }}
<TicketDescriptorProxy :id="entity.ticketFk" />
</span>
<VnUserLink
:name="entity.client?.salesPersonUser?.name"
:worker-id="entity.client?.salesPersonFk"
/>
</template>
</VnLv>
<VnLv
@ -134,19 +133,20 @@ onMounted(async () => {
/>
</template>
</VnLv>
<VnLv :label="t('claim.card.commercial')">
<template #value>
<VnUserLink
:name="entity.client?.salesPersonUser?.name"
:worker-id="entity.client?.salesPersonFk"
/>
</template>
</VnLv>
<VnLv :label="t('claim.card.zone')" :value="entity.ticket?.zone?.name" />
<VnLv
:label="t('claim.card.province')"
:value="entity.ticket?.address?.province?.name"
/>
<VnLv :label="t('claim.card.zone')" :value="entity.ticket?.zone?.name" />
<VnLv :label="t('claim.card.ticketId')">
<template #value>
<span class="link">
{{ entity.ticketFk }}
<TicketDescriptorProxy :id="entity.ticketFk" />
</span>
</template>
</VnLv>
<VnLv
:label="t('claimRate')"
:value="toPercentage(entity.client?.claimsRatio?.claimingRate)"
@ -176,6 +176,7 @@ onMounted(async () => {
color="primary"
:href="salixUrl + 'ticket/' + entity.ticketFk + '/sale-tracking'"
>
<QTooltip>{{ t('claim.card.saleTracking') }}</QTooltip>
</QBtn>
<QBtn
size="md"
@ -183,6 +184,7 @@ onMounted(async () => {
color="primary"
:href="salixUrl + 'ticket/' + entity.ticketFk + '/tracking/index'"
>
<QTooltip>{{ t('claim.card.ticketTracking') }}</QTooltip>
</QBtn>
</QCardActions>
</template>

View File

@ -1,4 +1,5 @@
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useState } from 'src/composables/useState';
import VnNotes from 'src/components/ui/VnNotes.vue';
@ -6,14 +7,15 @@ import VnNotes from 'src/components/ui/VnNotes.vue';
const route = useRoute();
const state = useState();
const user = state.getUser();
const id = route.params.id;
const $props = defineProps({
id: { type: Number, default: null },
addNote: { type: Boolean, default: true },
});
const claimId = computed(() => $props.id || route.params.id);
const claimFilter = {
where: { claimFk: id },
where: { claimFk: claimId.value },
fields: ['created', 'workerFk', 'text'],
include: {
relation: 'worker',
@ -30,19 +32,16 @@ const claimFilter = {
};
const body = {
claimFk: id,
claimFk: claimId.value,
workerFk: user.value.id,
};
</script>
<template>
<div class="column items-center">
<VnNotes
style="overflow-y: scroll"
:add-note="$props.addNote"
:id="id"
url="claimObservations"
:filter="claimFilter"
:body="body"
/>
</div>
<VnNotes
style="overflow-y: auto"
:add-note="$props.addNote"
url="claimObservations"
:filter="claimFilter"
:body="body"
/>
</template>

View File

@ -1,145 +0,0 @@
<script setup>
import axios from 'axios';
import { watch, ref, computed, onUnmounted, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import CrudModel from 'components/CrudModel.vue';
import { useState } from 'src/composables/useState';
import { toDate } from 'src/filters';
const quasar = useQuasar();
const state = useState();
const { t } = useI18n();
const selected = ref([]);
const claimRmaRef = ref();
const claim = computed(() => state.get('ClaimDescriptor'));
const claimRmaFilter = {
include: {
relation: 'worker',
scope: {
include: {
relation: 'user',
},
},
},
order: 'created DESC',
where: {
code: claim.value?.rma,
},
};
async function addRow() {
if (!claim.value.rma) {
return quasar.notify({
message: `This claim is not associated to any RMA`,
type: 'negative',
});
}
const formData = {
code: claim.value.rma,
};
await axios.post(`ClaimRmas`, formData);
await claimRmaRef.value.reload();
quasar.notify({
type: 'positive',
message: t('globals.rowAdded'),
icon: 'check',
});
}
onMounted(() => {
if (claim.value) claimRmaRef.value.reload();
});
watch(
claim,
() => {
claimRmaRef.value.reload();
},
{ deep: true }
);
</script>
<template>
<div class="column items-center">
<div class="list">
<CrudModel
data-key="ClaimRma"
url="ClaimRmas"
model="ClaimRma"
:filter="claimRmaFilter"
v-model:selected="selected"
ref="claimRmaRef"
:default-save="false"
:default-reset="false"
:default-remove="false"
>
<template #body="{ rows }">
<QCard>
<template v-for="(row, index) of rows" :key="row.id">
<QItem class="q-pa-none items-start">
<QItemSection class="q-pa-md">
<QList>
<QItem class="q-pa-none">
<QItemSection>
<QItemLabel caption>
{{ t('claim.rma.user') }}
</QItemLabel>
<QItemLabel>
{{ row?.worker?.user?.name }}
</QItemLabel>
</QItemSection>
</QItem>
<QItem class="q-pa-none">
<QItemSection>
<QItemLabel caption>
{{ t('claim.rma.created') }}
</QItemLabel>
<QItemLabel>
{{
toDate(row.created, {
timeStyle: 'medium',
})
}}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QItemSection>
<QCardActions vertical class="justify-between">
<QBtn
flat
round
color="orange"
icon="vn:bin"
@click="claimRmaRef.remove([row])"
>
<QTooltip>{{ t('globals.remove') }}</QTooltip>
</QBtn>
</QCardActions>
</QItem>
<QSeparator v-if="index !== rows.length - 1" />
</template>
</QCard>
</template>
</CrudModel>
</div>
</div>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="addRow()" />
</QPageSticky>
</template>
<style lang="scss" scoped>
.list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
This claim is not associated to any RMA: Esta reclamación no está asociada a ninguna ARM
</i18n>

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, ref, computed, watch } from 'vue';
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toDate, toCurrency } from 'src/filters';
@ -70,7 +70,7 @@ const detailsColumns = ref([
},
{
name: 'description',
label: 'claim.summary.description',
label: 'globals.description',
field: (row) => row.sale.concept,
},
{
@ -172,6 +172,7 @@ function openDialog(dmsId) {
<CardSummary
ref="summary"
:url="`Claims/${entityId}/getSummary`"
:entity-id="entityId"
@on-fetch="getClaimDms"
>
<template #header="{ entity: { claim } }">
@ -179,9 +180,9 @@ function openDialog(dmsId) {
</template>
<template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one">
<a class="header" :href="`#/claim/${entityId}/basic-data`">
<a class="header header-link" :href="`#/claim/${entityId}/basic-data`">
{{ t('claim.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<VnLv
:label="t('claim.summary.created')"
@ -194,19 +195,19 @@ function openDialog(dmsId) {
</QChip>
</template>
</VnLv>
<VnLv :label="t('claim.summary.assignedTo')">
<VnLv :label="t('globals.salesPerson')">
<template #value>
<VnUserLink
:name="claim.worker?.user?.nickname"
:worker-id="claim.workerFk"
:name="claim.client?.salesPersonUser?.name"
:worker-id="claim.client?.salesPersonFk"
/>
</template>
</VnLv>
<VnLv :label="t('claim.summary.attendedBy')">
<template #value>
<VnUserLink
:name="claim.client?.salesPersonUser?.name"
:worker-id="claim.client?.salesPersonFk"
:name="claim.worker?.user?.nickname"
:worker-id="claim.workerFk"
/>
</template>
</VnLv>
@ -218,26 +219,37 @@ function openDialog(dmsId) {
/>
</template>
</VnLv>
<VnLv :label="t('claim.summary.returnOfMaterial')" :value="claim.rma" />
<QCheckbox
:align-items="right"
:label="t('claim.basicData.picked')"
v-model="claim.hasToPickUp"
:disable="true"
/>
</QCard>
<QCard class="vn-three claimVnNotes full-height">
<a class="header" :href="`#/claim/${entityId}/notes`">
<QCard class="vn-three">
<a class="header header-link" :href="`#/claim/${entityId}/notes`">
{{ t('claim.summary.notes') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<ClaimNotes :add-note="false" style="height: 350px" order="created ASC" />
<ClaimNotes
:id="entityId"
:add-note="false"
style="max-height: 300px"
order="created ASC"
/>
</QCard>
<QCard class="vn-two" v-if="salesClaimed.length > 0">
<a class="header" :href="`#/claim/${entityId}/lines`">
<a class="header header-link" :href="`#/claim/${entityId}/lines`">
{{ t('claim.summary.details') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<QTable :columns="detailsColumns" :rows="salesClaimed" flat>
<QTable
:columns="detailsColumns"
:rows="salesClaimed"
flat
dense
:rows-per-page-options="[0]"
hide-bottom
>
<template #header="props">
<QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
@ -268,11 +280,19 @@ function openDialog(dmsId) {
</QTable>
</QCard>
<QCard class="vn-two" v-if="developments.length > 0">
<a class="header" :href="claimUrl + 'development'">
<a class="header header-link" :href="claimUrl + 'development'">
{{ t('claim.summary.development') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<QTable :columns="developmentColumns" :rows="developments" flat>
<QTable
:columns="developmentColumns"
:rows="developments"
flat
dense
:rows-per-page-options="[0]"
hide-bottom
>
<template #header="props">
<QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
@ -283,9 +303,9 @@ function openDialog(dmsId) {
</QTable>
</QCard>
<QCard class="vn-max" v-if="claimDms.length > 0">
<a class="header" :href="`#/claim/${entityId}/photos`">
<a class="header header-link" :href="`#/claim/${entityId}/photos`">
{{ t('claim.summary.photos') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<div class="container">
<div
@ -302,7 +322,7 @@ function openDialog(dmsId) {
v-if="media.isVideo"
@click.stop="openDialog(media.dmsFk)"
>
<QTooltip>Video</QTooltip>
<QTooltip>Video</QTooltip>header
</QIcon>
<QCard class="multimedia relative-position">
<QImg
@ -326,9 +346,9 @@ function openDialog(dmsId) {
</QCard>
<QCard class="vn-max">
<a class="header" :href="claimUrl + 'action'">
<a class="header header-link" :href="claimUrl + 'action'">
{{ t('claim.summary.actions') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" class="link" />
</a>
<div id="slider-container" class="q-px-xl q-py-md">
<QSlider
@ -336,7 +356,7 @@ function openDialog(dmsId) {
label
:label-value="t('claim.summary.responsibility')"
label-always
color="primary"
color="var()"
markers
:marker-labels="[
{ value: 1, label: t('claim.summary.company') },
@ -390,13 +410,7 @@ function openDialog(dmsId) {
</template>
</CardSummary>
</template>
<style lang="scss">
.claimVnNotes {
.q-card {
max-width: 100%;
}
}
</style>
<style lang="scss" scoped>
.q-dialog__inner--minimized > div {
max-width: 80%;
@ -406,7 +420,6 @@ function openDialog(dmsId) {
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
flex-basis: 30%;
}
.multimedia-container {
flex: 1 0 21%;

View File

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

View File

@ -1,171 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { useArrayData } from 'src/composables/useArrayData';
import axios from 'axios';
const quasar = useQuasar();
const { t } = useI18n();
const arrayData = useArrayData('ClaimRmaList');
const isLoading = ref(false);
const input = ref();
const newRma = ref({
code: '',
crated: Date.vnNew(),
});
function onInputUpdate(value) {
newRma.value.code = value.toUpperCase();
}
async function submit() {
const formData = newRma.value;
if (formData.code === '') return;
isLoading.value = true;
await axios.post('ClaimRmas', formData);
await arrayData.refresh();
isLoading.value = false;
input.value.$el.focus();
newRma.value = {
code: '',
created: Date.vnNew(),
};
}
function confirm(id) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
data: { id },
promise: remove,
},
})
.onOk(async () => await arrayData.refresh());
}
async function remove({ id }) {
await axios.delete(`ClaimRmas/${id}`);
quasar.notify({
type: 'positive',
message: t('globals.rowRemoved'),
});
}
</script>
<template>
<QPage class="column items-center q-pa-md sticky">
<QPageSticky expand position="top" :offset="[16, 16]">
<QCard class="card q-pa-md">
<QForm @submit="submit">
<VnInput
ref="input"
v-model="newRma.code"
:label="t('claim.rmaList.code')"
@update:model-value="onInputUpdate"
class="q-mb-md"
:readonly="isLoading"
:loading="isLoading"
autofocus
/>
<div class="text-caption">
{{ arrayData.totalRows }} {{ t('claim.rmaList.records') }}
</div>
</QForm>
</QCard>
</QPageSticky>
<div class="vn-card-list">
<VnPaginate
data-key="ClaimRmaList"
url="ClaimRmas"
order="id DESC"
:offset="50"
auto-load
>
<template #body="{ rows }">
<QCard class="card">
<template v-if="isLoading">
<QItem class="q-pa-none items-start">
<QItemSection class="q-pa-md">
<QList>
<QItem class="q-pa-none">
<QItemSection>
<QItemLabel caption>
<QSkeleton />
</QItemLabel>
<QItemLabel>
<QSkeleton type="text" />
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QItemSection>
<QCardActions vertical class="justify-between">
<QSkeleton
type="circle"
class="q-mb-md"
size="40px"
/>
</QCardActions>
</QItem>
<QSeparator />
</template>
<template v-for="row of rows" :key="row.id">
<QItem class="q-pa-none items-start">
<QItemSection class="q-pa-md">
<QList>
<QItem class="q-pa-none">
<QItemSection>
<QItemLabel caption>{{
t('claim.rmaList.code')
}}</QItemLabel>
<QItemLabel>{{ row.code }}</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QItemSection>
<QCardActions vertical class="justify-between">
<QBtn
flat
round
color="primary"
icon="vn:bin"
@click="confirm(row.id)"
>
<QTooltip>{{ t('globals.remove') }}</QTooltip>
</QBtn>
</QCardActions>
</QItem>
<QSeparator />
</template>
</QCard>
</template>
</VnPaginate>
</div>
</QPage>
</template>
<style lang="scss" scoped>
.sticky {
padding-top: 156px;
}
.card {
width: 100%;
max-width: 60em;
}
.q-page-sticky {
z-index: 2998;
}
</style>

View File

@ -6,6 +6,7 @@ import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const route = useRoute();
@ -30,7 +31,9 @@ const { t } = useI18n();
<QPageContainer>
<QPage>
<VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -40,6 +40,15 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
data-key="customerData"
>
<template #body="{ entity }">
<VnLv :label="t('customer.card.payMethod')" :value="entity.payMethod.name" />
<VnLv :label="t('customer.card.credit')" :value="toCurrency(entity.credit)" />
<VnLv
:label="t('customer.card.securedCredit')"
:value="toCurrency(entity.creditInsurance)"
/>
<VnLv :label="t('customer.card.debt')" :value="toCurrency(entity.debt)" />
<VnLv v-if="entity.salesPersonUser" :label="t('customer.card.salesPerson')">
<template #value>
<VnUserLink
@ -48,13 +57,10 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
/>
</template>
</VnLv>
<VnLv :label="t('customer.card.credit')" :value="toCurrency(entity.credit)" />
<VnLv
:label="t('customer.card.securedCredit')"
:value="toCurrency(entity.creditInsurance)"
:label="t('customer.card.businessTypeFk')"
:value="entity.businessTypeFk"
/>
<VnLv :label="t('customer.card.payMethod')" :value="entity.payMethod.name" />
<VnLv :label="t('customer.card.debt')" :value="toCurrency(entity.debt)" />
</template>
<template #icons="{ entity }">
<QCardActions>

View File

@ -62,9 +62,9 @@ const creditWarning = computed(() => {
<CardSummary ref="summary" :url="`Clients/${entityId}/summary`">
<template #body="{ entity }">
<QCard class="vn-one">
<a class="header" :href="clientUrl + `basic-data`">
<a class="header header-link" :href="`#/customer/${entityId}/basic-data`">
{{ t('customer.summary.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<VnLv :label="t('customer.summary.customerId')" :value="entity.id" />
<VnLv :label="t('customer.summary.name')" :value="entity.name" />
@ -96,9 +96,12 @@ const creditWarning = computed(() => {
/>
</QCard>
<QCard class="vn-one">
<a class="header" :href="clientUrl + `fiscal-data`">
<a
class="header header-link"
:href="`#/customer/${entityId}/fiscal-data`"
>
{{ t('customer.summary.fiscalAddress') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<VnLv
:label="t('customer.summary.socialName')"
@ -121,37 +124,58 @@ const creditWarning = computed(() => {
<VnLv :label="t('customer.summary.street')" :value="entity.street" />
</QCard>
<QCard class="vn-one">
<a class="header link" :href="clientUrl + `fiscal-data`" link>
{{ t('customer.summary.fiscalAddress') }}
<QIcon name="open_in_new" color="primary" />
<a
class="header header-link"
:href="`#/customer/${entityId}/fiscal-data`"
link
>
{{ t('customer.summary.fiscalData') }}
<QIcon name="open_in_new" />
</a>
<VnLv
<QCheckbox
:label="t('customer.summary.isEqualizated')"
:value="entity.isEqualizated"
v-model="entity.isEqualizated"
:disable="true"
/>
<VnLv :label="t('customer.summary.isActive')" :value="entity.isActive" />
<VnLv
<QCheckbox
:label="t('customer.summary.isActive')"
v-model="entity.isActive"
:disable="true"
/>
<QCheckbox
:label="t('customer.summary.invoiceByAddress')"
:value="entity.hasToInvoiceByAddress"
v-model="entity.hasToInvoiceByAddress"
:disable="true"
/>
<VnLv
<QCheckbox
:label="t('customer.summary.verifiedData')"
:value="entity.isTaxDataChecked"
v-model="entity.isTaxDataChecked"
:disable="true"
/>
<VnLv
<QCheckbox
:label="t('customer.summary.hasToInvoice')"
:value="entity.hasToInvoice"
v-model="entity.hasToInvoice"
:disable="true"
/>
<VnLv
<QCheckbox
:label="t('customer.summary.notifyByEmail')"
:value="entity.isToBeMailed"
v-model="entity.isToBeMailed"
:disable="true"
/>
<QCheckbox
:label="t('customer.summary.vies')"
v-model="entity.isVies"
:disable="true"
/>
<VnLv :label="t('customer.summary.vies')" :value="entity.isVies" />
</QCard>
<QCard class="vn-one">
<a class="header link" :href="clientUrl + `billing-data`" link>
<a
class="header header-link"
:href="`#/customer/${entityId}/billing-data`"
link
>
{{ t('customer.summary.billingData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<VnLv
:label="t('customer.summary.payMethod')"
@ -159,20 +183,32 @@ const creditWarning = computed(() => {
/>
<VnLv :label="t('customer.summary.bankAccount')" :value="entity.iban" />
<VnLv :label="t('customer.summary.dueDay')" :value="entity.dueDay" />
<VnLv :label="t('customer.summary.hasLcr')" :value="entity.hasLcr" />
<VnLv
:label="t('customer.summary.hasCoreVnl')"
:value="entity.hasCoreVnl"
<QCheckbox
style="padding: 0"
:label="t('customer.summary.hasLcr')"
v-model="entity.hasLcr"
:disable="true"
/>
<VnLv
<QCheckbox
:label="t('customer.summary.hasCoreVnl')"
v-model="entity.hasCoreVnl"
:disable="true"
/>
<QCheckbox
:label="t('customer.summary.hasB2BVnl')"
:value="entity.hasSepaVnl"
v-model="entity.hasSepaVnl"
:disable="true"
/>
</QCard>
<QCard class="vn-one" v-if="entity.defaultAddress">
<a class="header link" :href="clientUrl + `address/index`" link>
<a
class="header header-link"
:href="`#/customer/${entityId}/consignees`"
link
>
{{ t('customer.summary.consignee') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<VnLv
:label="t('customer.summary.addressName')"
@ -188,21 +224,22 @@ const creditWarning = computed(() => {
/>
</QCard>
<QCard class="vn-one" v-if="entity.account">
<a class="header link" :href="clientUrl + `web-access`">
<a class="header header-link" :href="`#/customer/${entityId}/web-access`">
{{ t('customer.summary.webAccess') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<VnLv
:label="t('customer.summary.username')"
:value="entity.account.name"
/>
<VnLv
<QCheckbox
:label="t('customer.summary.webAccess')"
:value="entity.account.active"
v-model="entity.account.active"
:disable="true"
/>
</QCard>
<QCard class="vn-one" v-if="entity.account">
<div class="header">
<div class="header header-link">
{{ t('customer.summary.businessData') }}
</div>
<VnLv
@ -230,12 +267,12 @@ const creditWarning = computed(() => {
</QCard>
<QCard class="vn-one" v-if="entity.account">
<a
class="header link"
class="header header-link"
:href="`https://grafana.verdnatura.es/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk=${entityId}`"
link
>
{{ t('customer.summary.financialData') }}
<QIcon name="vn:grafana" color="primary" />
<QIcon name="vn:grafana" />
</a>
<VnLv
:label="t('customer.summary.risk')"
@ -276,7 +313,30 @@ const creditWarning = computed(() => {
:label="t('customer.summary.recoverySince')"
:value="toDate(entity.recovery.started)"
/>
<VnLv
:label="t('customer.summary.rating')"
:value="entity.rating"
:info="t('valueInfo', { min: 1, max: 20 })"
/>
<VnLv
:label="t('customer.summary.recommendCredit')"
:value="entity.recommendedCredit"
/>
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss" scoped>
@media (min-width: $breakpoint-md) {
.summary .vn-one {
min-width: 300px;
}
}
</style>
<i18n>
en:
valueInfo: Value from {min} to {max}. The higher the better value
es:
valueInfo: Valor de {min} a {max}. Cuanto más alto, mejor valor
</i18n>

View File

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

View File

@ -1,9 +1,9 @@
<script setup>
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
const { t } = useI18n();
const props = defineProps({
@ -12,10 +12,6 @@ const props = defineProps({
required: true,
},
});
function isValidNumber(value) {
return /^(\d|\d+(\.|,)?\d+)$/.test(value);
}
</script>
<template>
@ -51,28 +47,9 @@ function isValidNumber(value) {
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('Amount')"
v-model="params.amount"
is-outlined
@update:model-value="
(value) => {
if (value.includes(','))
params.amount = params.amount.replace(',', '.');
}
"
:rules="[
(val) => isValidNumber(val) || !val || 'Please type a number',
]"
lazy-rules
>
<template #prepend>
<QIcon name="euro" size="sm" />
</template>
</VnInput>
<VnCurrency v-model="params.amount" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate v-model="params.from" :label="t('From')" is-outlined />

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 toCustomerConsignees = () => {
},
});
};
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
@ -113,83 +91,17 @@ const toCustomerConsignees = () => {
<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>
</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>
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
></VnLocation>
</div>
</VnRow>
<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">
<VnSelectFilter
:label="t('Agency')"

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import { useRoute } 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';
@ -17,13 +17,8 @@ import CustomsNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCus
const { t } = useI18n();
const route = useRoute();
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([]);
@ -34,14 +29,6 @@ onBeforeMount(() => {
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) => {
observationTypes.value = observations;
@ -97,26 +84,16 @@ const onDataSaved = () => {
};
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>
<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
@ -168,83 +145,17 @@ const onDataSaved = () => {
<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>
</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>
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
></VnLocation>
</div>
</VnRow>
<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">
<VnSelectFilter
:label="t('Agency')"

View File

@ -112,7 +112,7 @@ const onDataSaved = async () => {
:filter="filterBanks"
@on-fetch="(data) => (bankOptions = data)"
auto-load
url="Banks"
url="Accountings"
/>
<fetch-data
:filter="filterClientFindOne"

View File

@ -36,9 +36,12 @@ onMounted(async () => {
</template>
<template #body="{ entity: department }">
<QCard class="column">
<a class="header" :href="department + `basic-data`">
<a
class="header header-link"
:href="`#/department/department/${entityId}/basic-data`"
>
{{ t('Basic data') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<div class="full-width row wrap justify-between content-between">
<div class="column" style="min-width: 50%">

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

@ -7,6 +7,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import EntryDescriptor from './EntryDescriptor.vue';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
const { t } = useI18n();
const stateStore = useStateStore();
@ -33,7 +34,9 @@ const stateStore = useStateStore();
<QPage>
<VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

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

View File

@ -39,30 +39,47 @@ onMounted(async () => {
const tableColumnComponents = {
quantity: {
component: () => 'span',
props: () => {},
},
stickers: {
component: () => 'span',
props: () => {},
event: () => {},
},
packagingFk: {
component: () => 'span',
props: () => {},
event: () => {},
},
weight: {
component: () => 'span',
props: () => {},
event: () => {},
},
packing: {
component: () => 'span',
props: () => {},
event: () => {},
},
grouping: {
component: () => 'span',
props: () => {},
event: () => {},
},
buyingValue: {
component: () => 'span',
props: () => {},
event: () => {},
},
amount: {
component: () => 'span',
props: () => {},
event: () => {},
},
pvp: {
component: () => 'span',
props: () => {},
event: () => {},
},
};
@ -160,152 +177,121 @@ const fetchEntryBuys = async () => {
<template #header>
<span>{{ entry.id }} - {{ entry.supplier.nickname }}</span>
</template>
<template #body>
<QCard class="vn-one">
<router-link
:to="{ name: 'EntryBasicData', params: { id: entityId } }"
class="header link"
class="header header-link"
>
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</router-link>
<VnRow>
<div class="col">
<VnLv
:label="t('entry.summary.commission')"
:value="entry.commission"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.currency')"
:value="entry.currency.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.company')"
:value="entry.company.code"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.reference')"
:value="entry.reference"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.invoiceNumber')"
:value="entry.invoiceNumber"
/>
</div>
</VnRow>
<VnLv :label="t('entry.summary.commission')" :value="entry.commission" />
<VnLv :label="t('entry.summary.currency')" :value="entry.currency.name" />
<VnLv :label="t('entry.summary.company')" :value="entry.company.code" />
<VnLv :label="t('entry.summary.reference')" :value="entry.reference" />
<VnLv
:label="t('entry.summary.invoiceNumber')"
:value="entry.invoiceNumber"
/>
</QCard>
<QCard class="vn-one">
<router-link
:to="{ name: 'EntryBasicData', params: { id: entityId } }"
class="header link"
class="header header-link"
>
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</router-link>
<VnRow>
<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>
<VnLv :label="t('entry.summary.travelReference')">
<template #value>
<span class="link">
{{ entry.travel.ref }}
<TravelDescriptorProxy :id="entry.travel.id" />
</span>
</template>
</VnLv>
<VnLv
:label="t('entry.summary.travelAgency')"
:value="entry.travel.agency.name"
/>
<VnLv
:label="t('entry.summary.travelShipped')"
:value="toDate(entry.travel.shipped)"
/>
<VnLv
:label="t('entry.summary.travelWarehouseOut')"
:value="entry.travel.warehouseOut.name"
/>
<QCheckbox
:label="t('entry.summary.travelDelivered')"
v-model="entry.travel.isDelivered"
:disable="true"
/>
<VnLv
:label="t('entry.summary.travelLanded')"
:value="toDate(entry.travel.landed)"
/>
<VnLv
:label="t('entry.summary.travelWarehouseIn')"
:value="entry.travel.warehouseIn.name"
/>
<QCheckbox
:label="t('entry.summary.travelReceived')"
v-model="entry.travel.isReceived"
:disable="true"
/>
</QCard>
<QCard class="vn-one">
<router-link
:to="{ name: 'TravelSummary', params: { id: entry.travel.id } }"
class="header link"
class="header header-link"
>
{{ t('Travel data') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</router-link>
<VnRow>
<div class="col">
<VnLv :label="t('entry.summary.travelReference')">
<template #value>
<span class="link">
{{ entry.travel.ref }}
<TravelDescriptorProxy :id="entry.travel.id" />
</span>
</template>
</VnLv>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelAgency')"
:value="entry.travel.agency.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelShipped')"
:value="toDate(entry.travel.shipped)"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelWarehouseOut')"
:value="entry.travel.warehouseOut.name"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelDelivered')"
:value="entry.travel.isDelivered"
/>
</div>
<div class="col">
<VnLv
:label="t('entry.summary.travelLanded')"
:value="toDate(entry.travel.landed)"
/>
</div>
<div class="col">
<VnLv
: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>
<QCheckbox
:label="t('entry.summary.ordered')"
v-model="entry.isOrdered"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.confirmed')"
v-model="entry.isConfirmed"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.booked')"
v-model="entry.isBooked"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.raid')"
v-model="entry.isRaid"
:disable="true"
/>
<QCheckbox
:label="t('entry.summary.excludedFromAvailable')"
v-model="entry.isExcludedFromAvailable"
:disable="true"
/>
</QCard>
<QCard class="vn-two" style="min-width: 100%">
jsegarra marked this conversation as resolved
Review

En esta seccion en salix hay 3 columnas/cards, en lilium debe ser igual.
Hay una pequeña modificación:
Lo que ahora hay en la card datos basicos se divide en dos columnas
card 1(Link a basic data): Comisión, Moneda, Empresa ,Referencia, Núm. factura
card 2(Link a basic data): Pedida, Confirmado, Asentado, Redada, Inventario
card 3(Link a travel): Esta seccion igual que esta son los datos de envio

En esta seccion en salix hay 3 columnas/cards, en lilium debe ser igual. Hay una pequeña modificación: Lo que ahora hay en la card datos basicos se divide en dos columnas card 1(Link a basic data): Comisión, Moneda, Empresa ,Referencia, Núm. factura card 2(Link a basic data): Pedida, Confirmado, Asentado, Redada, Inventario card 3(Link a travel): Esta seccion igual que esta son los datos de envio
Review

Implementado!

Commit: 84a0be0a5c

Implementado! Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/84a0be0a5c787448d1c34f459bbae4fc166fe977
Review

Resuelvo conversación porque lo he visto bien y he compartido la evidencia del cambio con Javi

Resuelvo conversación porque lo he visto bien y he compartido la evidencia del cambio con Javi
<a class="header">
<a class="header header-link">
{{ t('entry.summary.buys') }}
<QIcon name="open_in_new" />
</a>
<QTable
:rows="entryBuys"
@ -318,7 +304,10 @@ const fetchEntryBuys = async () => {
<QTr no-hover>
<QTd v-for="col in cols" :key="col.name">
<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
v-if="
@ -368,5 +357,4 @@ const fetchEntryBuys = async () => {
<i18n>
es:
Travel data: Datos envío
</i18n>

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { onMounted, ref, computed, reactive, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
@ -8,11 +8,16 @@ import FetchedTags from 'components/ui/FetchedTags.vue';
import EntryDescriptorProxy from './Card/EntryDescriptorProxy.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import EditTableCellValueForm from 'src/components/EditTableCellValueForm.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import EntryLatestBuysFilter from './EntryLatestBuysFilter.vue';
import ItemDescriptorProxy from '../Item/Card/ItemDescriptorProxy.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDate, toCurrency } from 'src/filters';
import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
const router = useRouter();
const session = useSession();
@ -21,11 +26,72 @@ const stateStore = useStateStore();
const { t } = useI18n();
const rowsFetchDataRef = ref(null);
const itemTypesOptions = ref([]);
const originsOptions = ref([]);
const itemFamiliesOptions = ref([]);
const intrastatOptions = ref([]);
const packagingsOptions = ref([]);
const editTableCellDialogRef = ref(null);
const visibleColumns = ref([]);
const allColumnNames = ref([]);
const rows = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'id':
case 'size':
case 'weightByPiece':
case 'isActive':
case 'family':
case 'minPrice':
case 'packingOut':
return { [`i.${param}`]: value };
case 'name':
case 'description':
return { [`i.${param}`]: { like: `%${value}%` } };
case 'code':
return { 'it.code': value };
case 'intrastat':
return { 'intr.description': value };
case 'origin':
return { 'ori.code': value };
case 'landing':
return { [`lb.${param}`]: value };
case 'packing':
case 'grouping':
case 'quantity':
case 'entryFk':
case 'buyingValue':
case 'freightValue':
case 'comissionValue':
case 'packageValue':
case 'isIgnored':
case 'price2':
case 'price3':
case 'ektFk':
case 'weight':
case 'packagingFk':
return { [`b.${param}`]: value };
}
};
const params = reactive({});
const arrayData = useArrayData('EntryLatestBuys', {
url: 'Buys/latestBuysFilter',
order: ['itemFk DESC'],
exprBuilder: exprBuilder,
});
const store = arrayData.store;
const rows = computed(() => store.data);
const rowsSelected = ref([]);
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const columns = computed(() => [
{
label: t('entry.latestBuys.picture'),
@ -37,12 +103,32 @@ const columns = computed(() => [
name: 'itemFk',
field: 'itemFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.packing'),
field: 'packing',
name: 'packing',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -50,6 +136,16 @@ const columns = computed(() => [
field: 'grouping',
name: 'grouping',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -57,12 +153,32 @@ const columns = computed(() => [
field: 'quantity',
name: 'quantity',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.description'),
label: t('globals.description'),
field: 'description',
name: 'description',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -70,35 +186,104 @@ const columns = computed(() => [
field: 'size',
name: 'size',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.tags'),
name: 'tags',
align: 'left',
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.type'),
field: 'code',
name: 'type',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemTypesOptions.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
},
{
label: t('entry.latestBuys.intrastat'),
field: 'intrastat',
name: 'intrastat',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: intrastatOptions.value,
'option-value': 'description',
'option-label': 'description',
dense: true,
},
},
},
{
label: t('entry.latestBuys.origin'),
field: 'origin',
name: 'origin',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: originsOptions.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
},
{
label: t('entry.latestBuys.weightByPiece'),
field: 'weightByPiece',
name: 'weightByPiece',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -106,24 +291,67 @@ const columns = computed(() => [
field: 'isActive',
jgallego marked this conversation as resolved Outdated

en esta seccion hay que añadir el filtro lateral de busqueda igual que esta en salix.

en esta seccion hay que añadir el filtro lateral de busqueda igual que esta en salix.
https://redmine.verdnatura.es/issues/6537#note-6

Buenas @jgallego por nuestra parte lo teniamos en visto esto en esta tarea: https://mindshore-verdnatura.atlassian.net/jira/software/projects/SCRUM/boards/1/backlog?selectedIssue=SCRUM-162&text=entry

Si te parece lo mandamos en una PR aparte

Buenas @jgallego por nuestra parte lo teniamos en visto esto en esta tarea: https://mindshore-verdnatura.atlassian.net/jira/software/projects/SCRUM/boards/1/backlog?selectedIssue=SCRUM-162&text=entry Si te parece lo mandamos en una PR aparte

perfecto, resuelvo conversacion

perfecto, resuelvo conversacion
name: 'isActive',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.family'),
field: 'family',
name: 'family',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemFamiliesOptions.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
},
{
label: t('entry.latestBuys.entryFk'),
field: 'entryFk',
name: 'entryFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.buyingValue'),
field: 'buyingValue',
name: 'buyingValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -131,6 +359,16 @@ const columns = computed(() => [
field: 'freightValue',
name: 'freightValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -138,6 +376,16 @@ const columns = computed(() => [
field: 'comissionValue',
name: 'comissionValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -145,6 +393,16 @@ const columns = computed(() => [
field: 'packageValue',
name: 'packageValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -152,12 +410,33 @@ const columns = computed(() => [
field: 'isIgnored',
name: 'isIgnored',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.price2'),
field: 'price2',
name: 'price2',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -165,6 +444,16 @@ const columns = computed(() => [
field: 'price3',
name: 'price3',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -172,6 +461,16 @@ const columns = computed(() => [
field: 'minPrice',
name: 'minPrice',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
},
{
@ -179,6 +478,16 @@ const columns = computed(() => [
field: 'ektFk',
name: 'ektFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -186,18 +495,51 @@ const columns = computed(() => [
field: 'weight',
name: 'weight',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('entry.latestBuys.packagingFk'),
field: 'packagingFk',
name: 'packagingFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: packagingsOptions.value,
'option-value': 'id',
'option-label': 'id',
dense: true,
},
},
},
{
label: t('entry.latestBuys.packingOut'),
field: 'packingOut',
name: 'packingOut',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
@ -205,6 +547,16 @@ const columns = computed(() => [
field: 'landing',
name: 'landing',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toDate(val),
},
]);
@ -214,7 +566,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') },
@ -234,20 +586,55 @@ const redirectToEntryBuys = (entryFk) => {
router.push({ name: 'EntryBuys', params: { id: entryFk } });
};
const applyColumnFilter = async (col) => {
try {
params[col.field] = col.columnFilter.filterValue;
await arrayData.addFilter({ params });
} catch (err) {
console.error('Error applying column filter', err);
}
};
onMounted(async () => {
stateStore.rightDrawer = true;
const filteredColumns = columns.value.filter((col) => col.name !== 'picture');
allColumnNames.value = filteredColumns.map((col) => col.name);
await arrayData.fetch({ append: false });
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
ref="rowsFetchDataRef"
url="Buys/latestBuysFilter"
:filter="{ order: 'itemFk DESC', limit: 20 }"
@on-fetch="(data) => (rows = data)"
url="ItemTypes"
:filter="{ fields: ['code'], order: 'code ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (itemTypesOptions = data)"
/>
<FetchData
url="Origins"
:filter="{ fields: ['code'], order: 'code ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (originsOptions = data)"
/>
<FetchData
url="ItemFamilies"
:filter="{ fields: ['code'], order: 'code ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (itemFamiliesOptions = data)"
/>
<FetchData
url="Packagings"
:filter="{ fields: ['id'], order: 'id ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (packagingsOptions = data)"
/>
<FetchData
url="Intrastats"
:filter="{ fields: ['description'], order: 'description ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (intrastatOptions = data)"
/>
<QToolbar class="bg-vn-dark justify-end">
<div id="st-data">
@ -261,19 +648,43 @@ onMounted(async () => {
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<EntryLatestBuysFilter data-key="EntryLatestBuys" :tags="tags" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">
<QTable
:rows="rows"
:columns="columns"
hide-bottom
selection="multiple"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:visible-columns="visibleColumns"
v-model:selected="rowsSelected"
:no-data-label="t('globals.noResults')"
@row-click="(_, row) => redirectToEntryBuys(row.entryFk)"
>
<template #top-row="{ cols }">
<QTr>
<QTd />
<QTd
v-for="(col, index) in cols"
:key="index"
style="max-width: 100px"
>
<component
:is="col.columnFilter.component"
v-if="col.name !== 'picture'"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #body-cell-picture="{ row }">
<QTd>
<QImg
@ -291,6 +702,7 @@ onMounted(async () => {
<QBtn flat color="primary">
{{ row.itemFk }}
</QBtn>
<ItemDescriptorProxy :id="row.itemFk" />
</QTd>
</template>
<template #body-cell-tags="{ row }">

View File

@ -0,0 +1,474 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import axios from 'axios';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const itemCategories = ref([]);
const selectedCategoryFk = ref(null);
const selectedTypeFk = ref(null);
const itemTypesOptions = ref([]);
const itemTypeWorkersOptions = ref([]);
const suppliersOptions = ref([]);
const tagOptions = ref([]);
const tagValues = ref([]);
const categoryList = computed(() => {
return (itemCategories.value || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
});
const selectedCategory = computed(() =>
(itemCategories.value || []).find(
(category) => category?.id === selectedCategoryFk.value
)
);
const selectedType = computed(() => {
return (itemTypesOptions.value || []).find(
(type) => type?.id === selectedTypeFk.value
);
});
const selectCategory = async (params, categoryId, search) => {
if (params.categoryFk === categoryId) {
resetCategory(params);
search();
return;
}
selectedCategoryFk.value = categoryId;
params.categoryFk = categoryId;
await fetchItemTypes(categoryId);
search();
};
const resetCategory = (params) => {
selectedCategoryFk.value = null;
itemTypesOptions.value = null;
if (params) {
params.categoryFk = null;
params.typeFk = null;
}
};
const applyTags = (params, search) => {
params.tags = tagValues.value
.filter((tag) => tag.selectedTag && tag.value)
.map((tag) => ({
tagFk: tag.selectedTag.id,
tagName: tag.selectedTag.name,
value: tag.value,
}));
search();
};
const fetchItemTypes = async (id) => {
try {
const filter = {
fields: ['id', 'name', 'categoryFk'],
where: { categoryFk: id },
include: 'category',
order: 'name ASC',
};
const { data } = await axios.get('ItemTypes', {
params: { filter: JSON.stringify(filter) },
});
itemTypesOptions.value = data;
} catch (err) {
console.error('Error fetching item types', err);
}
};
const getCategoryClass = (category, params) => {
if (category.id === params?.categoryFk) {
return 'active';
}
};
const getSelectedTagValues = async (tag) => {
try {
tag.value = null;
const filter = {
fields: ['value'],
order: 'value ASC',
limit: 30,
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`Tags/${tag.selectedTag.id}/filterValue`, {
params,
});
tag.valueOptions = data;
} catch (err) {
console.error('Error getting selected tag values');
}
};
const removeTag = (index, params, search) => {
(tagValues.value || []).splice(index, 1);
applyTags(params, search);
};
</script>
<template>
<FetchData
url="ItemCategories"
limit="30"
auto-load
@on-fetch="(data) => (itemCategories = data)"
/>
<FetchData
url="TicketRequests/getItemTypeWorker"
limit="30"
auto-load
:filter="{ fields: ['id', 'nickname'], order: 'nickname ASC', limit: 30 }"
@on-fetch="(data) => (itemTypeWorkersOptions = data)"
/>
<FetchData
url="Suppliers"
limit="30"
auto-load
:filter="{ fields: ['id', 'name', 'nickname'], order: 'name ASC', limit: 30 }"
@on-fetch="(data) => (suppliersOptions = data)"
/>
<FetchData
url="Tags"
:filter="{ fields: ['id', 'name', 'isFree'] }"
auto-load
limit="30"
@on-fetch="(data) => (tagOptions = data)"
/>
<VnFilterPanel
:data-key="props.dataKey"
:expr-builder="exprBuilder"
:custom-tags="['tags']"
@init="onFilterInit"
@remove="clearFilter"
>
<template #tags="{ tag, formatFn }">
<strong v-if="tag.label === 'categoryFk'">
{{ t(selectedCategory?.name || '') }}
</strong>
<strong v-else-if="tag.label === 'typeFk'">
{{ t(selectedType?.name || '') }}
</strong>
<div v-else class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #customTags="{ tags: customTags, params }">
<template v-for="tag in customTags" :key="tag.label">
<VnFilterPanelChip
v-for="chip in tag.value"
:key="chip"
removable
@remove="removeTagChip(chip, params, searchFn)"
>
<div class="q-gutter-x-xs">
<strong>{{ chip.tagName }}: </strong>
<span>"{{ chip.value }}"</span>
</div>
</VnFilterPanelChip>
</template>
</template>
<template #body="{ params, searchFn }">
<QItem class="category-filter q-mt-md">
<QBtn
dense
flat
round
v-for="category in categoryList"
:key="category.name"
:class="['category', getCategoryClass(category, params)]"
:icon="category.icon"
@click="selectCategory(params, category.id, searchFn)"
>
<QTooltip>
{{ t(category.name) }}
</QTooltip>
</QBtn>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnSelectFilter
:label="t('params.typeFk')"
v-model="params.typeFk"
:options="itemTypesOptions"
option-value="id"
option-label="name"
dense
outlined
rounded
use-input
:disable="!selectedCategoryFk"
@update:model-value="
(value) => {
selectedTypeFk = value;
searchFn();
}
"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.categoryName }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
<QSeparator />
<QItem class="q-my-md">
<QItemSection>
<VnSelectFilter
:label="t('params.salesPersonFk')"
v-model="params.salesPersonFk"
:options="itemTypeWorkersOptions"
option-value="id"
option-label="nickname"
dense
outlined
rounded
use-input
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnSelectFilter
:label="t('params.supplier')"
v-model="params.supplierFk"
:options="suppliersOptions"
option-value="id"
option-label="name"
dense
outlined
rounded
use-input
@update:model-value="searchFn()"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.nickname }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnInputDate
:label="t('params.from')"
v-model="params.from"
is-outlined
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnInputDate
:label="t('params.to')"
v-model="params.to"
is-outlined
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.active')"
v-model="params.active"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection>
<QCheckbox
:label="t('params.visible')"
v-model="params.visible"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.floramondo')"
v-model="params.floramondo"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem
v-for="(value, index) in tagValues"
:key="value"
class="q-mt-md filter-value"
>
<QItemSection class="col">
<VnSelectFilter
:label="t('params.tag')"
v-model="value.selectedTag"
:options="tagOptions"
option-label="name"
dense
outlined
rounded
:emit-value="false"
use-input
:is-clearable="false"
@update:model-value="getSelectedTagValues(value)"
/>
</QItemSection>
<QItemSection class="col">
<VnSelectFilter
v-if="!value?.selectedTag?.isFree && value.valueOptions"
:label="t('params.value')"
v-model="value.value"
:options="value.valueOptions || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
:disable="!value"
:is-clearable="false"
class="filter-input"
@update:model-value="applyTags(params, searchFn)"
/>
<VnInput
v-else
v-model="value.value"
:label="t('params.value')"
:disable="!value"
is-outlined
class="filter-input"
:is-clearable="false"
@keyup.enter="applyTags(params, searchFn)"
/>
</QItemSection>
<QIcon
name="delete"
class="filter-icon"
@click="removeTag(index, params, searchFn)"
/>
</QItem>
<QItem class="q-mt-lg">
<QIcon
name="add_circle"
class="filter-icon"
@click="tagValues.push({})"
/>
</QItem>
</template>
</VnFilterPanel>
</template>
<style lang="scss" scoped>
.category-filter {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
.category {
padding: 8px;
width: 60px;
height: 60px;
font-size: 1.4rem;
background-color: var(--vn-light-gray);
&.active {
background-color: $primary;
}
}
}
.filter-icon {
font-size: 24px;
color: $primary;
padding: 0 4px;
cursor: pointer;
}
.filter-input {
flex-shrink: 1;
min-width: 0;
}
.filter-value {
display: flex;
align-items: center;
}
</style>
<i18n>
en:
params:
supplier: Supplier
from: From
to: To
active: Is active
visible: Is visible
floramondo: Is floramondo
salesPersonFk: Buyer
categoryFk: Category
typeFk: Type
tag: Tag
value: Value
es:
params:
supplier: Proveedor
from: Desde
to: Hasta
active: Activo
visible: Visible
floramondo: Floramondo
salesPersonFk: Comprador
categoryFk: Categoría
typeFk: Tipo
tag: Etiqueta
value: Valor
Plant: Planta
Flower: Flor
Handmade: Confección
Green: Verde
Accessories: Complemento
Fruit: Fruta
</i18n>

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

@ -8,6 +8,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData';
import { onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
@ -74,7 +75,9 @@ watch(
<QPageContainer>
<QPage>
<VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -8,6 +8,7 @@ import { useArrayData } from 'src/composables/useArrayData';
import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
const route = useRoute();
const { t } = useI18n();
@ -74,7 +75,12 @@ async function insert() {
}
</script>
<template>
<FetchData url="Banks" auto-load limit="30" @on-fetch="(data) => (banks = data)" />
<FetchData
url="Accountings"
auto-load
limit="30"
@on-fetch="(data) => (banks = data)"
/>
<CrudModel
v-if="invoiceIn"
ref="invoiceInFormRef"
@ -158,7 +164,12 @@ async function insert() {
</template>
<template #body-cell-amount="{ row }">
<QTd>
<QInput v-model="row.amount" clearable clear-icon="close" />
<VnCurrency
v-model="row.amount"
:is-outlined="false"
clearable
clear-icon="close"
/>
</QTd>
</template>
<template #body-cell-foreignvalue="{ row }">

View File

@ -209,9 +209,9 @@ function getLink(param) {
<!--Basic Data-->
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a class="header" :href="getLink('basic-data')">
<a class="header header-link" :href="getLink('basic-data')">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
</QCardSection>
<VnLv
@ -233,9 +233,9 @@ function getLink(param) {
</QCard>
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a class="header" :href="getLink('basic-data')">
<a class="header header-link" :href="getLink('basic-data')">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
</QCardSection>
<VnLv
@ -258,9 +258,9 @@ function getLink(param) {
</QCard>
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a class="header" :href="getLink('basic-data')">
<a class="header header-link" :href="getLink('basic-data')">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
</QCardSection>
<VnLv
@ -275,16 +275,17 @@ function getLink(param) {
:label="t('invoiceIn.summary.company')"
:value="invoiceIn.company?.code"
/>
<VnLv
<QCheckbox
:label="t('invoiceIn.summary.booked')"
:value="invoiceIn.isBooked"
v-model="invoiceIn.isBooked"
:disable="true"
/>
</QCard>
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a class="header" :href="getLink('basic-data')">
<a class="header header-link" :href="getLink('basic-data')">
{{ t('invoiceIn.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
</QCardSection>
<QCardSection class="q-pa-none">
@ -318,9 +319,9 @@ function getLink(param) {
</QCard>
<!--Vat-->
<QCard v-if="invoiceIn.invoiceInTax.length">
<a class="header" :href="getLink('vat')">
<a class="header header-link" :href="getLink('vat')">
{{ t('invoiceIn.card.vat') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<QTable
:columns="vatColumns"
@ -351,9 +352,9 @@ function getLink(param) {
</QCard>
<!--Due Day-->
<QCard v-if="invoiceIn.invoiceInDueDay.length">
<a class="header" :href="getLink('due-day')">
<a class="header header-link" :href="getLink('due-day')">
{{ t('invoiceIn.card.dueDay') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<QTable
class="full-width"
@ -381,9 +382,9 @@ function getLink(param) {
</QCard>
<!--Intrastat-->
<QCard v-if="invoiceIn.invoiceInIntrastat.length">
<a class="header" :href="getUrl('intrastat')">
<a class="header header-link" :href="getLink('intrastat')">
{{ t('invoiceIn.card.intrastat') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<QTable
:columns="intrastatColumns"

View File

@ -9,6 +9,7 @@ import { toCurrency } from 'src/filters';
import FetchData from 'src/components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
const route = useRoute();
const { t } = useI18n();
@ -225,7 +226,7 @@ async function addExpense() {
</template>
<template #body-cell-taxablebase="{ row }">
<QTd>
<QInput
<VnCurrency
:class="{
'no-pointer-events': isNotEuro(invoiceIn.currency.code),
}"
@ -234,11 +235,7 @@ async function addExpense() {
clear-icon="close"
v-model="row.taxableBase"
clearable
>
<template #prepend>
<QIcon name="euro" size="xs" flat />
</template>
</QInput>
/>
</QTd>
</template>
<template #body-cell-sageiva="{ row, col }">
@ -328,7 +325,7 @@ async function addExpense() {
</VnSelectFilter>
</QItem>
<QItem>
<QInput
<VnCurrency
:label="t('Taxable base')"
:class="{
'no-pointer-events': isNotEuro(
@ -340,11 +337,7 @@ async function addExpense() {
clear-icon="close"
v-model="props.row.taxableBase"
clearable
>
<template #append>
<QIcon name="euro" size="xs" flat />
</template>
</QInput>
/>
</QItem>
<QItem>
<VnSelectFilter

View File

@ -8,6 +8,7 @@ import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import { useCapitalize } from 'src/composables/useCapitalize';
import VnCurrency from 'src/components/common/VnCurrency.vue';
const { t } = useI18n();
const props = defineProps({
@ -137,16 +138,7 @@ const suppliersRef = ref();
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('Amount')"
v-model="params.amount"
is-outlined
lazy-rules
>
<template #prepend>
<QIcon name="euro" size="sm"></QIcon>
</template>
</VnInput>
<VnCurrency v-model="params.amount" is-outlined />
</QItemSection>
</QItem>
<QItem class="q-mb-md">

View File

@ -5,6 +5,7 @@ import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
@ -28,7 +29,9 @@ const { t } = useI18n();
<QPageContainer>
<QPage>
<VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -95,15 +95,20 @@ const ticketsColumns = ref([
</script>
<template>
<CardSummary ref="summary" :url="`InvoiceOuts/${entityId}/summary`">
<CardSummary
ref="summary"
:url="`InvoiceOuts/${entityId}/summary`"
:entity-id="entityId"
>
<template #header="{ entity: { invoiceOut } }">
<div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div>
</template>
<template #body="{ entity: { invoiceOut } }">
<QCard class="vn-one">
<div class="header">
<a class="header header-link">
{{ t('invoiceOut.pageTitles.basicData') }}
</div>
<QIcon name="open_in_new" />
</a>
<VnLv
:label="t('invoiceOut.summary.issued')"
:value="toDate(invoiceOut.issued)"
@ -126,9 +131,10 @@ const ticketsColumns = ref([
/>
</QCard>
<QCard class="vn-three">
<div class="header">
<a class="header header-link">
{{ t('invoiceOut.summary.taxBreakdown') }}
</div>
<QIcon name="open_in_new" />
</a>
<QTable :columns="taxColumns" :rows="invoiceOut.taxesBreakdown" flat>
<template #header="props">
<QTr :props="props">
@ -140,9 +146,10 @@ const ticketsColumns = ref([
</QTable>
</QCard>
<QCard class="vn-three">
<div class="header">
<a class="header header-link">
{{ t('invoiceOut.summary.tickets') }}
</div>
<QIcon name="open_in_new" />
</a>
<QTable v-if="tickets" :columns="ticketsColumns" :rows="tickets" flat>
<template #header="props">
<QTr :props="props">

View File

@ -6,6 +6,7 @@ import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
const { t } = useI18n();
const props = defineProps({
@ -57,7 +58,11 @@ function setWorkers(data) {
</QItem>
<QItem>
<QItemSection>
<VnInput :label="t('Amount')" v-model="params.amount" is-outlined />
<VnCurrency
:label="t('Amount')"
v-model="params.amount"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>

View File

@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
const { t } = useI18n();
const props = defineProps({
@ -84,7 +85,7 @@ const props = defineProps({
</QItem>
<QItem>
<QItemSection>
<VnInput
<VnCurrency
v-model="params.amount"
:label="t('invoiceOut.negativeBases.amount')"
is-outlined

View File

@ -4,6 +4,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import ItemDescriptor from './ItemDescriptor.vue';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
</script>
@ -19,7 +20,9 @@ const stateStore = useStateStore();
<QPage>
<VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -69,12 +69,12 @@ async function onSubmit() {
<template>
<QForm @submit="onSubmit" class="q-gutter-y-md q-pa-lg formCard">
<VnLogo alt="Logo" fit="contain" :ratio="16 / 9" class="q-mb-md" />
<VnInput
v-model="username"
:label="t('login.username')"
lazy-rules
:rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]"
color="primary"
/>
<VnInput
type="password"
@ -82,9 +82,8 @@ async function onSubmit() {
:label="t('login.password')"
lazy-rules
:rules="[(val) => (val && val.length > 0) || t('login.fieldRequired')]"
class="red"
/>
<QToggle v-model="keepLogin" :label="t('login.keepLogin')" />
<div>
<QBtn
:label="t('login.submit')"
@ -95,6 +94,7 @@ async function onSubmit() {
unelevated
/>
</div>
<QToggle v-model="keepLogin" :label="t('login.keepLogin')" />
</QForm>
</template>
@ -104,6 +104,9 @@ async function onSubmit() {
min-width: 300px;
}
.q-input {
color: $primary;
}
@media (max-width: $breakpoint-xs-max) {
.formCard {
min-width: 100%;

View File

@ -99,6 +99,8 @@ onMounted(async () => {
</i18n>
<style lang="scss">
$vnColor: #8ebb27;
.formCard {
max-width: 1500px;
min-width: 700px;

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

@ -132,12 +132,11 @@ const openBuscaman = async (route, ticket) => {
<template>
<div class="q-pa-md">
<CardSummary ref="summary" :url="`Routes/${entityId}/summary`">
<template #header-left>
<RouterLink :to="{ name: `RouteSummary`, params: { id: entityId } }">
<QIcon name="open_in_new" color="white" size="sm" />
</RouterLink>
</template>
<CardSummary
ref="summary"
:url="`Routes/${entityId}/summary`"
:entity-id="entityId"
>
<template #header="{ entity }">
<span>{{ `${entity?.route.id} - ${entity?.route?.description}` }}</span>
</template>
@ -199,7 +198,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: {
@ -71,11 +71,11 @@ const filter = {
<template #body="{ entity }">
<QCard class="vn-one">
<RouterLink
class="header"
class="header header-link"
:to="{ name: 'ShelvingBasicData', params: { id: entityId } }"
>
{{ t('shelving.pageTitles.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</RouterLink>
<VnLv :label="t('shelving.summary.code')" :value="entity.code" />
<VnLv

View File

@ -18,13 +18,16 @@ const quasar = useQuasar();
const { notify } = useNotify();
const route = useRoute();
const { t } = useI18n();
const bankEntitiesRef = ref(null);
const supplier = ref(null);
const supplierAccountRef = ref(null);
const wireTransferFk = ref(null);
const bankEntitiesOptions = ref([]);
const onBankEntityCreated = (data) => {
bankEntitiesOptions.value.push(data);
const onBankEntityCreated = async (dataSaved, rowData) => {
await bankEntitiesRef.value.fetch();
rowData.bankEntityFk = dataSaved.id;
};
const onChangesSaved = () => {
@ -63,6 +66,7 @@ onMounted(() => {
</script>
<template>
<FetchData
ref="bankEntitiesRef"
url="BankEntities"
@on-fetch="(data) => (bankEntitiesOptions = data)"
auto-load
@ -114,13 +118,16 @@ onMounted(() => {
:label="t('worker.create.bankEntity')"
v-model="row.bankEntityFk"
:options="bankEntitiesOptions"
option-label="name"
option-label="bic"
option-value="id"
hide-selected
>
<template #form>
<CreateBankEntityForm
@on-data-saved="onBankEntityCreated($event)"
@on-data-saved="
(_, requestResponse) =>
onBankEntityCreated(requestResponse, row)
"
:show-entity-field="false"
/>
</template>

View File

@ -83,8 +83,13 @@ const redirectToUpdateView = (addressData) => {
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="redirectToCreateView()" />
<QTooltip>
{{ t('supplier.list.newSupplier') }}
{{ t('New address') }}
</QTooltip>
</QPageSticky>
</QPage>
</template>
<i18n>
es:
New address: Nueva dirección
</i18n>

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

@ -26,6 +26,7 @@ const workersOptions = ref([]);
:url-update="`Suppliers/${route.params.id}`"
model="supplier"
auto-load
:clear-store-on-unmount="false"
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">

View File

@ -33,6 +33,7 @@ const formatPayDems = (data) => {
:url-update="`Suppliers/${route.params.id}`"
model="supplier"
auto-load
:clear-store-on-unmount="false"
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">

View File

@ -5,6 +5,7 @@ import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import LeftMenu from 'components/LeftMenu.vue';
import SupplierDescriptor from './SupplierDescriptor.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
@ -30,7 +31,9 @@ const { t } = useI18n();
<QPageContainer>
<QPage>
<VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
@ -11,6 +11,15 @@ const route = useRoute();
const { t } = useI18n();
const supplierContactRef = ref(null);
const insertRow = () => {
supplierContactRef.value.insert();
nextTick(() => {
const inputs = document.querySelectorAll('[input-name-focusable]');
const lastInput = inputs[inputs.length - 1];
if (lastInput) lastInput.focus();
});
};
onMounted(() => {
if (supplierContactRef.value) supplierContactRef.value.reload();
});
@ -38,6 +47,7 @@ onMounted(() => {
<VnRow class="row q-gutter-md">
<div class="col">
<VnInput
input-name-focusable
:label="t('supplier.contacts.name')"
v-model="row.name"
/>
@ -92,7 +102,7 @@ onMounted(() => {
size="sm"
class="cursor-pointer"
color="primary"
@click="supplierContactRef.insert()"
@click="insertRow()"
>
<QTooltip>
{{ t('Add contact') }}

View File

@ -9,6 +9,7 @@ import VnLv from 'src/components/ui/VnLv.vue';
import { toDateString } from 'src/filters';
import useCardDescription from 'src/composables/useCardDescription';
import { getUrl } from 'src/composables/getUrl';
import { useState } from 'src/composables/useState';
const $props = defineProps({
id: {
@ -21,6 +22,7 @@ const $props = defineProps({
const route = useRoute();
const { t } = useI18n();
const url = ref();
const state = useState();
const filter = {
fields: [
@ -71,6 +73,8 @@ const setData = (entity) => {
data.value = useCardDescription(entity.ref, entity.id);
};
const supplier = computed(() => state.get('supplier'));
const getEntryQueryParams = (supplier) => {
if (!supplier) return null;
@ -101,7 +105,7 @@ const getEntryQueryParams = (supplier) => {
:subtitle="data.subtitle"
:filter="filter"
@on-fetch="setData"
data-key="Supplier"
data-key="supplier"
>
<template #header-extra-action>
<QBtn
@ -133,10 +137,10 @@ const getEntryQueryParams = (supplier) => {
<VnLv :label="t('supplier.summary.payDay')" :value="entity.payDay" />
<VnLv :label="t('supplier.summary.account')" :value="entity.account" />
</template>
<template #icons="{ entity }">
<QCardActions class="q-gutter-x-md">
<template #icons>
<QCardActions v-if="supplier" class="q-gutter-x-md">
<QIcon
v-if="!entity.isActive"
v-if="!supplier.isActive"
name="vn:disabled"
color="primary"
size="xs"
@ -144,7 +148,7 @@ const getEntryQueryParams = (supplier) => {
<QTooltip>{{ t('Inactive supplier') }}</QTooltip>
</QIcon>
<QIcon
v-if="!entity.isSerious"
v-if="!supplier.isSerious"
name="vn:supplierfalse"
color="primary"
size="xs"
@ -167,6 +171,7 @@ const getEntryQueryParams = (supplier) => {
<QTooltip>{{ t('All entries with current supplier') }}</QTooltip>
</QBtn>
<QBtn
v-if="entity.client?.fi"
:to="{
name: 'CustomerCard',
params: { id: entity.client?.id },

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;
@ -53,6 +53,7 @@ function handleLocation(data, location) {
:url-update="`Suppliers/${route.params.id}/updateFiscalData`"
model="supplier"
auto-load
:clear-store-on-unmount="false"
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">

View File

@ -51,21 +51,20 @@ const isAdministrative = computed(() => {
:url="`Suppliers/${entityId}/getSummary`"
@on-fetch="(data) => setData(data)"
>
<template #header-left>
<a v-if="isAdministrative" class="header link" :href="supplierUrl">
<QIcon name="open_in_new" color="white" size="sm" />
</a>
</template>
<template #header>
<span>{{ supplier.name }} - {{ supplier.id }}</span>
</template>
<template #body>
<QCard class="vn-one">
<a v-if="isAdministrative" class="header link" :href="supplierUrl">
<router-link
v-if="isAdministrative"
class="header link"
:to="{ name: 'SupplierBasicData', params: { id: entityId } }"
>
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QIcon name="open_in_new" />
</router-link>
<span v-else> {{ t('globals.summary.basicData') }}</span>
<VnLv label="Id" :value="supplier.id" />
<VnLv label="Alias" :value="supplier.nickname" />
@ -82,33 +81,26 @@ const isAdministrative = computed(() => {
<span> {{ dashIfEmpty(supplier.note) }} </span>
</template>
</VnLv>
<VnLv :label="t('supplier.summary.verified')" class="q-mb-xs">
<template #value>
<QCheckbox
v-model="supplier.isSerious"
dense
disable
class="full-width q-mb-xs"
/>
</template>
</VnLv>
<VnLv :label="t('supplier.summary.isActive')" class="q-mb-xs">
<template #value>
<QCheckbox
v-model="supplier.isActive"
dense
disable
class="full-width q-mb-xs"
/>
</template>
</VnLv>
<QCheckbox
:label="t('supplier.summary.verified')"
v-model="supplier.isSerious"
:disable="true"
/>
<QCheckbox
:label="t('supplier.summary.isActive')"
v-model="supplier.isActive"
:disable="true"
/>
</QCard>
<QCard class="vn-one">
<a v-if="isAdministrative" class="header link" :href="supplierUrl">
<router-link
v-if="isAdministrative"
class="header link"
:to="{ name: 'SupplierBillingData', params: { id: entityId } }"
>
{{ t('supplier.summary.billingData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QIcon name="open_in_new" />
</router-link>
<span v-else> {{ t('supplier.summary.billingData') }}</span>
<VnLv
:label="t('supplier.summary.payMethod')"
@ -124,10 +116,14 @@ const isAdministrative = computed(() => {
<VnLv :label="t('supplier.summary.account')" :value="supplier.account" />
</QCard>
<QCard class="vn-one">
<a v-if="isAdministrative" class="header link" :href="supplierUrl">
<router-link
v-if="isAdministrative"
class="header link"
:to="{ name: 'SupplierFiscalData', params: { id: entityId } }"
>
{{ t('supplier.summary.fiscalData') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QIcon name="open_in_new" />
</router-link>
<span v-else> {{ t('supplier.summary.fiscalData') }}</span>
<VnLv
:label="t('supplier.summary.sageTaxType')"
@ -155,10 +151,14 @@ const isAdministrative = computed(() => {
/>
</QCard>
<QCard class="vn-one">
<a v-if="isAdministrative" class="header link" :href="supplierUrl">
<router-link
v-if="isAdministrative"
class="header link"
:to="{ name: 'SupplierFiscalData', params: { id: entityId } }"
>
{{ t('supplier.summary.fiscalAddress') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QIcon name="open_in_new" />
</router-link>
<span v-else> {{ t('supplier.summary.fiscalAddress') }}</span>
<VnLv :label="t('supplier.summary.socialName')" :value="supplier.name" />
<VnLv :label="t('supplier.summary.taxNumber')" :value="supplier.nif" />

View File

@ -42,7 +42,7 @@ const redirectToCreateView = () => {
</QScrollArea>
</QDrawer>
<div class="vn-card-list">
<VnPaginate data-key="SuppliersList" url="Suppliers/filter" auto-load>
<VnPaginate data-key="SuppliersList" url="Suppliers/filter">
<template #body="{ rows }">
<CardList
v-for="row of rows"

View File

@ -5,6 +5,7 @@ import TicketDescriptor from './TicketDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
@ -29,7 +30,9 @@ const { t } = useI18n();
<QPage>
<VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -88,14 +88,6 @@ const setData = (entity) =>
<TicketDescriptorMenu :ticket="entity" />
</template>
<template #body="{ entity }">
<VnLv v-if="entity.ticketState" :label="t('ticket.card.state')">
<template #value>
<QBadge :color="entity.ticketState.state.classColor">
{{ entity.ticketState.state.name }}
</QBadge>
</template>
</VnLv>
<VnLv :label="t('ticket.card.shipped')" :value="toDate(entity.shipped)" />
<VnLv :label="t('ticket.card.customerId')">
<template #value>
<span class="link">
@ -104,6 +96,13 @@ const setData = (entity) =>
</span>
</template>
</VnLv>
<VnLv v-if="entity.ticketState" :label="t('ticket.card.state')">
<template #value>
<QBadge :color="entity.ticketState.state.classColor">
{{ entity.ticketState.state.name }}
</QBadge>
</template>
</VnLv>
<VnLv :label="t('ticket.summary.salesPerson')">
<template #value>
<VnUserLink
@ -112,12 +111,14 @@ const setData = (entity) =>
/>
</template>
</VnLv>
<VnLv :label="t('ticket.card.warehouse')" :value="entity.warehouse?.name" />
<VnLv :label="t('ticket.card.shipped')" :value="toDate(entity.shipped)" />
<VnLv
v-if="entity.agencyMode"
:label="t('ticket.card.agency')"
:value="entity.agencyMode.name"
/>
<VnLv :label="t('ticket.card.warehouse')" :value="entity.warehouse?.name" />
<VnLv :label="t('ticket.card.alias')" :value="entity.nickname" />
</template>
<template #icons="{ entity }">
<QCardActions>

View File

@ -147,9 +147,9 @@ async function changeState(value) {
</div>
</QCard>
<QCard class="vn-one">
<a class="header link" :href="ticketUrl + 'basic-data/step-one'">
<a class="header header-link" :href="ticketUrl + 'basic-data/step-one'">
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<VnLv :label="t('ticket.summary.state')">
<template #value>
@ -193,9 +193,9 @@ async function changeState(value) {
/>
</QCard>
<QCard class="vn-one">
<a class="header link" :href="ticketUrl + 'basic-data/step-one'">
<a class="header header-link" :href="ticketUrl + 'basic-data/step-one'">
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<VnLv
:label="t('ticket.summary.shipped')"
@ -236,9 +236,9 @@ async function changeState(value) {
/>
</QCard>
<QCard class="vn-one">
<a class="header link" :href="ticketUrl + 'observation'">
<a class="header header-link" :href="ticketUrl + 'observation'">
{{ t('ticket.pageTitles.notes') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<VnLv
v-for="note in ticket.notes"
@ -258,9 +258,9 @@ async function changeState(value) {
</VnLv>
</QCard>
<QCard class="vn-max">
<a class="header link" :href="ticketUrl + 'sale'">
<a class="header header-link" :href="ticketUrl + 'sale'">
{{ t('ticket.summary.saleLines') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<QTable :rows="ticket.sales">
<template #header="props">
@ -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>
@ -396,9 +396,9 @@ async function changeState(value) {
class="vn-max"
v-if="ticket.packagings.length > 0 || ticket.services.length > 0"
>
<a class="header link" :href="ticketUrl + 'package'">
<a class="header header-link" :href="ticketUrl + 'package'">
{{ t('globals.packages') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<QTable :rows="ticket.packagings" flat>
<template #header="props">
@ -417,15 +417,15 @@ async function changeState(value) {
</template>
</QTable>
<a class="header link q-mt-xl" :href="ticketUrl + 'service'">
<a class="header header-link q-mt-xl" :href="ticketUrl + 'service'">
{{ t('ticket.summary.service') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</a>
<QTable :rows="ticket.services" flat>
<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

@ -3,6 +3,7 @@ import { useStateStore } from 'stores/useStateStore';
import TravelDescriptor from './TravelDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
</script>
@ -17,7 +18,9 @@ const stateStore = useStateStore();
<QPageContainer>
<QPage>
<VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -236,19 +236,9 @@ async function setTravelData(travelData) {
:url="`Travels/${entityId}/getTravel`"
@on-fetch="(data) => setTravelData(data)"
>
<template #header-left>
<router-link
class="header link"
:to="{ name: 'TravelSummary', params: { id: entityId } }"
>
<QIcon name="open_in_new" color="white" size="sm" />
<QTooltip>{{ t('travel.pageTitles.summary') }}</QTooltip>
</router-link>
</template>
<template #header>
<span>{{ travel.ref }} - {{ travel.id }}</span>
</template>
<template #header-right>
<QBtn color="white" dense flat icon="more_vert" round size="md">
<QTooltip>
@ -269,15 +259,11 @@ async function setTravelData(travelData) {
:label="t('globals.wareHouseOut')"
:value="travel.warehouseOut?.name"
/>
<VnLv :label="t('travel.summary.delivered')" class="q-mb-xs">
<template #value>
<QIcon
:name="travel.isDelivered ? 'check' : 'close'"
:color="travel.isDelivered ? 'positive' : 'negative'"
size="sm"
/>
</template>
</VnLv>
<QCheckbox
:label="t('travel.summary.delivered')"
v-model="travel.isDelivered"
:disable="true"
/>
</QCard>
<QCard class="vn-one">
<VnLv :label="t('globals.landed')" :value="toDate(travel.landed)" />
@ -285,15 +271,11 @@ async function setTravelData(travelData) {
:label="t('globals.wareHouseIn')"
:value="travel.warehouseIn?.name"
/>
<VnLv :label="t('travel.summary.received')" class="q-mb-xs">
<template #value>
<QIcon
:name="travel.isReceived ? 'check' : 'close'"
:color="travel.isReceived ? 'positive' : 'negative'"
size="sm"
/>
</template>
</VnLv>
<QCheckbox
:label="t('travel.summary.received')"
v-model="travel.isReceived"
:disable="true"
/>
</QCard>
<QCard class="vn-one">
<VnLv :label="t('globals.agency')" :value="travel.agency?.name" />
@ -302,7 +284,7 @@ async function setTravelData(travelData) {
<VnLv :label="t('globals.totalEntries')" :value="travel.totalEntries" />
</QCard>
<QCard class="full-width" v-if="entriesTableRows.length > 0">
<span class="header">
<span class="header header-link">
{{ t('travel.summary.entries') }}
</span>
<QTable
@ -361,14 +343,14 @@ async function setTravelData(travelData) {
<QCard class="full-width" v-if="thermographs.length > 0">
<RouterLink
class="header"
class="header header-link"
:to="{
name: 'TravelThermographsIndex',
params: { id: travel.id },
}"
>
{{ t('travel.summary.thermographs') }}
<QIcon name="open_in_new" color="primary" />
<QIcon name="open_in_new" />
</RouterLink>
<QTable
:rows="thermographs"

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

@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
import LeftMenu from 'components/LeftMenu.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const route = useRoute();
@ -17,7 +18,9 @@ const { t } = useI18n();
</QDrawer>
<QPageContainer>
<QPage>
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -5,6 +5,7 @@ import WorkerDescriptor from './WorkerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
@ -29,7 +30,9 @@ const { t } = useI18n();
<QPage>
<VnSubToolbar />
<div class="q-pa-md"><RouterView></RouterView></div>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>

Some files were not shown because too many files have changed in this diff Show More