#6943 Customer FiscalData already exists #1365

Closed
jsegarra wants to merge 7 commits from 6943_customerFD_alreadyExists into dev
230 changed files with 5056 additions and 3366 deletions
Showing only changes of commit 921f3b6985 - Show all commits

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules

File diff suppressed because it is too large Load Diff

107
Jenkinsfile vendored
View File

@ -1,6 +1,7 @@
#!/usr/bin/env groovy
def PROTECTED_BRANCH
def IS_LATEST
def BRANCH_ENV = [
test: 'test',
@ -10,17 +11,19 @@ def BRANCH_ENV = [
node {
stage('Setup') {
env.FRONT_REPLICAS = 1
env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev'
PROTECTED_BRANCH = [
'dev',
'test',
'master',
'main',
'beta'
].contains(env.BRANCH_NAME)
]
// https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
IS_PROTECTED_BRANCH = PROTECTED_BRANCH.contains(env.BRANCH_NAME)
IS_LATEST = ['master', 'main'].contains(env.BRANCH_NAME)
// https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
echo "NODE_NAME: ${env.NODE_NAME}"
echo "WORKSPACE: ${env.WORKSPACE}"
@ -33,7 +36,7 @@ node {
props.each {key, value -> echo "${key}: ${value}" }
}
if (PROTECTED_BRANCH) {
if (IS_PROTECTED_BRANCH) {
configFileProvider([
configFile(fileId: "salix-front.branch.${env.BRANCH_NAME}",
variable: 'BRANCH_PROPS_FILE')
@ -58,6 +61,19 @@ pipeline {
PROJECT_NAME = 'lilium'
}
stages {
stage('Version') {
when {
expression { IS_PROTECTED_BRANCH }
}
steps {
script {
def packageJson = readJSON file: 'package.json'
def version = "${packageJson.version}-build${env.BUILD_ID}"
writeFile(file: 'VERSION.txt', text: version)
echo "VERSION: ${version}"
}
}
}
stage('Install') {
environment {
NODE_ENV = ""
@ -68,48 +84,85 @@ pipeline {
}
stage('Test') {
when {
expression { !PROTECTED_BRANCH }
expression { !IS_PROTECTED_BRANCH }
}
environment {
NODE_ENV = ""
NODE_ENV = ''
CI = 'true'
TZ = 'Europe/Madrid'
}
steps {
sh 'pnpm run test:unit:ci'
}
post {
always {
junit(
testResults: 'junitresults.xml',
allowEmptyResults: true
)
parallel {
stage('Unit') {
steps {
sh 'pnpm run test:unit:ci'
}
post {
always {
junit(
testResults: 'junit/vitest.xml',
allowEmptyResults: true
)
}
}
}
stage('E2E') {
environment {
CREDENTIALS = credentials('docker-registry')
COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}"
COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ."
}
steps {
script {
sh 'rm junit/e2e-*.xml || true'
env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev'
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs')
sh "docker-compose ${env.COMPOSE_PARAMS} up -d"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ") {
sh 'cypress run --browser chromium || true'
}
}
}
post {
always {
sh "docker-compose ${env.COMPOSE_PARAMS} down -v"
junit(
testResults: 'junit/e2e-*.xml',
allowEmptyResults: true
)
}
}
}
}
}
stage('Build') {
when {
expression { PROTECTED_BRANCH }
expression { IS_PROTECTED_BRANCH }
}
environment {
CREDENTIALS = credentials('docker-registry')
VERSION = readFile 'VERSION.txt'
}
steps {
sh 'quasar build'
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
sh 'quasar build'
def baseImage = "salix-frontend:${env.VERSION}"
def image = docker.build(baseImage, ".")
docker.withRegistry("https://${env.REGISTRY}", 'docker-registry') {
image.push()
image.push(env.BRANCH_NAME)
if (IS_LATEST) image.push('latest')
}
}
dockerBuild()
}
}
stage('Deploy') {
when {
expression { PROTECTED_BRANCH }
expression { IS_PROTECTED_BRANCH }
}
environment {
VERSION = readFile 'VERSION.txt'
}
steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
withKubeConfig([
serverUrl: "$KUBERNETES_API",
credentialsId: 'kubernetes',

View File

@ -1,12 +1,37 @@
import { defineConfig } from 'cypress';
// https://docs.cypress.io/app/tooling/reporters
// https://docs.cypress.io/app/references/configuration
// https://www.npmjs.com/package/cypress-mochawesome-reporter
let urlHost, reporter, reporterOptions;
if (process.env.CI) {
urlHost = 'front';
reporter = 'junit';
reporterOptions = {
mochaFile: 'junit/e2e-[hash].xml',
toConsole: false,
};
} else {
urlHost = 'localhost';
reporter = 'cypress-mochawesome-reporter';
reporterOptions = {
charts: true,
reportPageTitle: 'Cypress Inline Reporter',
reportFilename: '[status]_[datetime]-report',
embeddedScreenshots: true,
reportDir: 'test/cypress/reports',
inlineAssets: true,
};
}
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:9000/',
experimentalStudio: true,
baseUrl: `http://${urlHost}:9000`,
experimentalStudio: false,
defaultCommandTimeout: 10000,
trashAssetsBeforeRuns: false,
requestTimeout: 10000,
responseTimeout: 30000,
pageLoadTimeout: 60000,
defaultBrowser: 'chromium',
fixturesFolder: 'test/cypress/fixtures',
screenshotsFolder: 'test/cypress/screenshots',
supportFile: 'test/cypress/support/index.js',
@ -14,29 +39,19 @@ export default defineConfig({
downloadsFolder: 'test/cypress/downloads',
video: false,
specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: false,
watchForFileChanges: false,
reporter: 'cypress-mochawesome-reporter',
reporterOptions: {
charts: true,
reportPageTitle: 'Cypress Inline Reporter',
reportFilename: '[status]_[datetime]-report',
embeddedScreenshots: true,
reportDir: 'test/cypress/reports',
inlineAssets: true,
},
experimentalRunAllSpecs: true,
watchForFileChanges: true,
reporter,
reporterOptions,
component: {
componentFolder: 'src',
testFiles: '**/*.spec.js',
supportFile: 'test/cypress/support/unit.js',
},
setupNodeEvents: async (on, config) => {
const plugin = await import('cypress-mochawesome-reporter/plugin');
plugin.default(on);
return config;
},
viewportWidth: 1280,
viewportHeight: 720,
},
experimentalMemoryManagement: true,
defaultCommandTimeout: 10000,
numTestsKeptInMemory: 2,
});

View File

@ -1,7 +0,0 @@
version: '3.7'
services:
main:
image: registry.verdnatura.es/salix-frontend:${VERSION:?}
build:
context: .
dockerfile: ./Dockerfile

45
docs/Dockerfile.dev Normal file
View File

@ -0,0 +1,45 @@
FROM debian:12.9-slim
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg2 \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g corepack@0.31.0 \
&& corepack enable pnpm \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update \
&& apt-get -y --no-install-recommends install \
apt-utils \
chromium \
libasound2 \
libgbm-dev \
libgtk-3-0 \
libgtk2.0-0 \
libnotify-dev \
libnss3 \
libxss1 \
libxtst6 \
xauth \
xvfb \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd -r -g 1000 app \
&& useradd -r -u 1000 -g app -m -d /home/app app
USER app
ENV SHELL=bash
ENV PNPM_HOME="/home/app/.local/share/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN pnpm setup \
&& pnpm install --global cypress@13.6.6 \
&& cypress install
WORKDIR /app

View File

@ -1,6 +1,6 @@
{
"name": "salix-front",
"version": "25.08.0",
"version": "25.10.0",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",
@ -71,4 +71,4 @@
"vite": "^6.0.11",
"vitest": "^0.31.1"
}
}
}

View File

@ -1,6 +1,6 @@
export default [
{
path: '/api',
rule: { target: 'http://0.0.0.0:3000' },
rule: { target: 'http://127.0.0.1:3000' },
},
];

View File

@ -11,6 +11,7 @@
import { configure } from 'quasar/wrappers';
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import path from 'path';
const target = `http://${process.env.CI ? 'back' : 'localhost'}:3000`;
export default configure(function (/* ctx */) {
return {
@ -108,13 +109,17 @@ export default configure(function (/* ctx */) {
},
proxy: {
'/api': {
target: 'http://0.0.0.0:3000',
target: target,
logLevel: 'debug',
changeOrigin: true,
secure: false,
},
},
open: false,
allowedHosts: [
'front', // Agrega este nombre de host
'localhost', // Opcional, para pruebas locales
],
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework

View File

@ -9,19 +9,19 @@ export default {
if (!form) return;
try {
const inputsFormCard = form.querySelectorAll(
`input:not([disabled]):not([type="checkbox"])`
`input:not([disabled]):not([type="checkbox"])`,
);
if (inputsFormCard.length) {
focusFirstInput(inputsFormCard[0]);
}
const textareas = document.querySelectorAll(
'textarea:not([disabled]), [contenteditable]:not([disabled])'
'textarea:not([disabled]), [contenteditable]:not([disabled])',
);
if (textareas.length) {
focusFirstInput(textareas[textareas.length - 1]);
}
const inputs = document.querySelectorAll(
'form#formModel input:not([disabled]):not([type="checkbox"])'
'form#formModel input:not([disabled]):not([type="checkbox"])',
);
const input = inputs[0];
if (!input) return;
@ -30,22 +30,5 @@ export default {
} catch (error) {
console.error(error);
}
form.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter' && !that.$attrs['prevent-submit']) {
const input = evt.target;
if (input.type == 'textarea' && evt.shiftKey) {
evt.preventDefault();
let { selectionStart, selectionEnd } = input;
input.value =
input.value.substring(0, selectionStart) +
'\n' +
input.value.substring(selectionEnd);
selectionStart = selectionEnd = selectionStart + 1;
return;
}
evt.preventDefault();
that.onSubmit();
}
});
},
};

View File

@ -2,7 +2,6 @@
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectProvince from 'src/components/VnSelectProvince.vue';
@ -21,14 +20,11 @@ const postcodeFormData = reactive({
provinceFk: null,
townFk: null,
});
const townsFetchDataRef = ref(false);
const townFilter = ref({});
const countriesRef = ref(false);
const provincesOptions = ref([]);
const townsOptions = ref([]);
const town = ref({});
const countryFilter = ref({});
function onDataSaved(formData) {
const newPostcode = {
@ -51,7 +47,6 @@ async function setCountry(countryFk, data) {
data.townFk = null;
data.provinceFk = null;
data.countryFk = countryFk;
await fetchTowns();
}
// Province
@ -60,22 +55,11 @@ async function setProvince(id, data) {
const newProvince = provincesOptions.value.find((province) => province.id == id);
if (newProvince) data.countryFk = newProvince.countryFk;
postcodeFormData.provinceFk = id;
await fetchTowns();
}
async function onProvinceCreated(data) {
postcodeFormData.provinceFk = data.id;
}
function provinceByCountry(countryFk = postcodeFormData.countryFk) {
return provincesOptions.value
.filter((province) => province.countryFk === countryFk)
.map(({ id }) => id);
}
// Town
async function handleTowns(data) {
townsOptions.value = data;
}
function setTown(newTown, data) {
town.value = newTown;
data.provinceFk = newTown?.provinceFk ?? newTown;
@ -88,18 +72,6 @@ async function onCityCreated(newTown, formData) {
formData.townFk = newTown;
setTown(newTown, formData);
}
async function fetchTowns(countryFk = postcodeFormData.countryFk) {
if (!countryFk) return;
const provinces = postcodeFormData.provinceFk
? [postcodeFormData.provinceFk]
: provinceByCountry();
townFilter.value.where = {
provinceFk: {
inq: provinces,
},
};
await townsFetchDataRef.value?.fetch();
}
async function filterTowns(name) {
if (name !== '') {
@ -108,22 +80,11 @@ async function filterTowns(name) {
like: `%${name}%`,
},
};
await townsFetchDataRef.value?.fetch();
}
}
</script>
<template>
<FetchData
ref="townsFetchDataRef"
:sort-by="['name ASC']"
:limit="30"
:filter="townFilter"
@on-fetch="handleTowns"
auto-load
url="Towns/location"
/>
<FormModelPopup
url-create="postcodes"
model="postcode"
@ -149,14 +110,13 @@ async function filterTowns(name) {
@filter="filterTowns"
:tooltip="t('Create city')"
v-model="data.townFk"
:options="townsOptions"
option-label="name"
option-value="id"
url="Towns/location"
:rules="validate('postcode.city')"
:acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
:emit-value="false"
required
data-cy="locationTown"
sort-by="name ASC"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
@ -197,16 +157,12 @@ async function filterTowns(name) {
/>
<VnSelect
ref="countriesRef"
:limit="30"
:filter="countryFilter"
:sort-by="['name ASC']"
auto-load
url="Countries"
required
:label="t('Country')"
hide-selected
option-label="name"
option-value="id"
v-model="data.countryFk"
:rules="validate('postcode.countryFk')"
@update:model-value="(value) => setCountry(value, data)"

View File

@ -62,12 +62,9 @@ const where = computed(() => {
auto-load
:where="where"
url="Autonomies/location"
:sort-by="['name ASC']"
:limit="30"
sort-by="name ASC"
:label="t('Autonomy')"
hide-selected
option-label="name"
option-value="id"
v-model="data.autonomyFk"
:rules="validate('province.autonomyFk')"
>

View File

@ -42,7 +42,6 @@ const itemFilter = {
const itemFilterParams = reactive({});
const closeButton = ref(null);
const isLoading = ref(false);
const producersOptions = ref([]);
const ItemTypesOptions = ref([]);
const InksOptions = ref([]);
const tableRows = ref([]);
@ -121,23 +120,17 @@ const selectItem = ({ id }) => {
</script>
<template>
<FetchData
url="Producers"
@on-fetch="(data) => (producersOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
auto-load
/>
<FetchData
url="ItemTypes"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
order="name"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
order="name ASC"
@on-fetch="(data) => (ItemTypesOptions = data)"
auto-load
/>
<FetchData
url="Inks"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
order="name"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
order="name ASC"
@on-fetch="(data) => (InksOptions = data)"
auto-load
/>
@ -152,11 +145,11 @@ const selectItem = ({ id }) => {
<VnInput :label="t('entry.buys.size')" v-model="itemFilterParams.size" />
<VnSelect
:label="t('globals.producer')"
:options="producersOptions"
hide-selected
option-label="name"
option-value="id"
v-model="itemFilterParams.producerFk"
url="Producers"
:fields="['id', 'name']"
sort-by="name ASC"
/>
<VnSelect
:label="t('globals.type')"

View File

@ -124,7 +124,7 @@ const selectTravel = ({ id }) => {
<FetchData
url="AgencyModes"
@on-fetch="(data) => (agenciesOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
auto-load
/>
<FetchData

View File

@ -1,6 +1,6 @@
<script setup>
import axios from 'axios';
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
import { onMounted, onUnmounted, computed, ref, watch, nextTick, useAttrs } from 'vue';
import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
@ -12,6 +12,7 @@ import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData';
import { getDifferences, getUpdatedValues } from 'src/filters';
const { push } = useRouter();
const quasar = useQuasar();
@ -22,6 +23,7 @@ const { validate } = useValidator();
const { notify } = useNotify();
const route = useRoute();
const myForm = ref(null);
const attrs = useAttrs();
const $props = defineProps({
url: {
type: String,
@ -98,6 +100,10 @@ const $props = defineProps({
type: [String, Boolean],
default: '800px',
},
onDataSaved: {
type: Function,
default: () => {},
},
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
@ -110,14 +116,14 @@ const isLoading = ref(false);
const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges);
const originalData = computed(() => state.get(modelValue));
const formData = ref({});
const formData = ref();
const defaultButtons = computed(() => ({
save: {
dataCy: 'saveDefaultBtn',
color: 'primary',
icon: 'save',
label: 'globals.save',
click: () => myForm.value.submit(),
click: async () => await save(),
type: 'submit',
},
reset: {
@ -138,7 +144,8 @@ onMounted(async () => {
if (!$props.formInitialData) {
if ($props.autoLoad && $props.url) await fetch();
else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data);
else if (arrayData.store.data)
updateAndEmit('onFetch', { val: arrayData.store.data });
}
if ($props.observeFormChanges) {
watch(
@ -158,7 +165,7 @@ onMounted(async () => {
if (!$props.url)
watch(
() => arrayData.store.data,
(val) => updateAndEmit('onFetch', val),
(val) => updateAndEmit('onFetch', { val }),
);
watch(
@ -204,7 +211,7 @@ async function fetch() {
});
if (Array.isArray(data)) data = data[0] ?? {};
updateAndEmit('onFetch', data);
updateAndEmit('onFetch', { val: data });
} catch (e) {
state.set(modelValue, {});
throw e;
@ -231,7 +238,11 @@ async function save() {
if ($props.hasConfirmModal) return;
Review

El await en saveFn al abrir el popup no se espera a que confirmes para continuar?

El await en saveFn al abrir el popup no se espera a que confirmes para continuar?
Review

Si te refieres a la linea 236, la función saveFn apunta a formCustomFn y ahi es donde se gestiona la petición, por tanto termina la implicación de FormModel

Si te refieres a la linea 236, la función saveFn apunta a formCustomFn y ahi es donde se gestiona la petición, por tanto termina la implicación de FormModel
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
updateAndEmit('onDataSaved', formData.value, response?.data);
updateAndEmit('onDataSaved', {
val: formData.value,
res: response?.data,
old: originalData.value,
});
if ($props.reload) await arrayData.fetch({});
hasChanges.value = false;
} finally {
@ -246,7 +257,7 @@ async function saveAndGo() {
function reset() {
formData.value = JSON.parse(JSON.stringify(originalData.value));
updateAndEmit('onFetch', originalData.value);
updateAndEmit('onFetch', { val: originalData.value });
if ($props.observeFormChanges) {
hasChanges.value = false;
isResetting.value = true;
@ -268,11 +279,11 @@ function filter(value, update, filterOptions) {
);
}
function updateAndEmit(evt, val, res) {
function updateAndEmit(evt, { val, res, old } = { val: null, res: null, old: null }) {
state.set(modelValue, val);
if (!$props.url) arrayData.store.data = val;
emit(evt, state.get(modelValue), res);
emit(evt, state.get(modelValue), res, old);
}
function trimData(data) {
@ -282,6 +293,27 @@ function trimData(data) {
}
return data;
}
function onBeforeSave(formData, originalData) {
return getUpdatedValues(
Object.keys(getDifferences(formData, originalData)),
formData,
);
}
async function onKeyup(evt) {
if (evt.key === 'Enter' && !('prevent-submit' in attrs)) {
const input = evt.target;
if (input.type == 'textarea' && evt.shiftKey) {
let { selectionStart, selectionEnd } = input;
input.value =
input.value.substring(0, selectionStart) +
'\n' +
input.value.substring(selectionEnd);
selectionStart = selectionEnd = selectionStart + 1;
return;
}
await save();
}
}
defineExpose({
save,
@ -297,12 +329,13 @@ defineExpose({
<QForm
ref="myForm"
v-if="formData"
@submit="save"
@submit.prevent
@keyup.prevent="onKeyup"
@reset="reset"
class="q-pa-md"
:style="maxWidth ? 'max-width: ' + maxWidth : ''"
id="formModel"
:prevent-submit="$attrs['prevent-submit']"
:mapper="onBeforeSave"
>
<QCard>
<slot

View File

@ -1,12 +1,13 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ref, computed, useAttrs, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import FormModel from 'components/FormModel.vue';
const emit = defineEmits(['onDataSaved', 'onDataCanceled']);
defineProps({
const props = defineProps({
title: {
type: String,
default: '',
@ -22,17 +23,28 @@ defineProps({
});
const { t } = useI18n();
const attrs = useAttrs();
const state = useState();
const formModelRef = ref(null);
const closeButton = ref(null);
const isSaveAndContinue = ref(false);
const onDataSaved = (formData, requestResponse) => {
if (closeButton.value && isSaveAndContinue) closeButton.value.click();
const isSaveAndContinue = ref(props.showSaveAndContinueBtn);
const isLoading = computed(() => formModelRef.value?.isLoading);
const reset = computed(() => formModelRef.value?.reset);
const onDataSaved = async (formData, requestResponse) => {
if (!isSaveAndContinue.value) closeButton.value?.click();
if (isSaveAndContinue.value) {
await nextTick();
state.set(attrs.model, attrs.formInitialData);
}
isSaveAndContinue.value = props.showSaveAndContinueBtn;
emit('onDataSaved', formData, requestResponse);
};
const isLoading = computed(() => formModelRef.value?.isLoading);
const reset = computed(() => formModelRef.value?.reset);
const onClick = async (saveAndContinue) => {
isSaveAndContinue.value = saveAndContinue;
await formModelRef.value.save();
};
defineExpose({
isLoading,
@ -58,19 +70,6 @@ defineExpose({
<p>{{ subtitle }}</p>
<slot name="form-inputs" :data="data" :validate="validate" />
<div class="q-mt-lg row justify-end">
<QBtn
v-if="showSaveAndContinueBtn"
:label="t('globals.isSaveAndContinue')"
:title="t('globals.isSaveAndContinue')"
type="submit"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
data-cy="FormModelPopup_isSaveAndContinue"
z-max
@click="() => (isSaveAndContinue = true)"
/>
<QBtn
:label="t('globals.cancel')"
:title="t('globals.cancel')"
@ -82,24 +81,31 @@ defineExpose({
data-cy="FormModelPopup_cancel"
v-close-popup
z-max
@click="
() => {
isSaveAndContinue = false;
emit('onDataCanceled');
}
"
@click="emit('onDataCanceled')"
/>
<QBtn
:flat="showSaveAndContinueBtn"
:label="t('globals.save')"
:title="t('globals.save')"
type="submit"
@click="onClick(false)"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
data-cy="FormModelPopup_save"
z-max
@click="() => (isSaveAndContinue = false)"
/>
<QBtn
v-if="showSaveAndContinueBtn"
:label="t('globals.isSaveAndContinue')"
:title="t('globals.isSaveAndContinue')"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
data-cy="FormModelPopup_isSaveAndContinue"
z-max
@click="onClick(true)"
/>
</div>
</template>

View File

@ -121,23 +121,25 @@ const removeTag = (index, params, search) => {
applyTags(params, search);
};
const setCategoryList = (data) => {
categoryList.value = (data || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
categoryList.value = (data || []).map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
fetchItemTypes();
};
</script>
<template>
<FetchData url="ItemCategories" limit="30" auto-load @on-fetch="setCategoryList" />
<FetchData
url="ItemCategories"
auto-load
@on-fetch="setCategoryList"
:where="{ display: { neq: 0 } }"
/>
<FetchData
url="Tags"
:filter="{ fields: ['id', 'name', 'isFree'] }"
auto-load
limit="30"
@on-fetch="(data) => (tagOptions = data)"
/>
<VnFilterPanel
@ -195,8 +197,6 @@ const setCategoryList = (data) => {
:label="t('components.itemsFilterPanel.typeFk')"
v-model="params.typeFk"
:options="itemTypesOptions"
option-value="id"
option-label="name"
dense
outlined
rounded
@ -234,7 +234,6 @@ const setCategoryList = (data) => {
:label="t('globals.tag')"
v-model="value.selectedTag"
:options="tagOptions"
option-label="name"
dense
outlined
rounded

View File

@ -85,7 +85,15 @@ const refresh = () => window.location.reload();
</QTooltip>
<PinnedModules ref="pinnedModulesRef" />
</QBtn>
<QBtn class="q-pa-none" rounded dense flat no-wrap id="user">
<QBtn
class="q-pa-none"
rounded
dense
flat
no-wrap
id="user"
data-cy="userPanel_btn"
>
<VnAvatar
:worker-id="user.id"
:title="user.name"

View File

@ -1,8 +1,22 @@
<script setup>
import { toCurrency } from 'src/filters';
defineProps({ row: { type: Object, required: true } });
</script>
<template>
<span class="q-gutter-x-xs">
<router-link
v-if="row.claim?.claimFk"
:to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }"
class="link"
>
<QIcon name="vn:claims" size="xs">
<QTooltip>
{{ t('ticketSale.claim') }}:
{{ row.claim?.claimFk }}
</QTooltip>
</QIcon>
</router-link>
<QIcon
v-if="row?.risk"
name="vn:risk"
@ -10,7 +24,8 @@ defineProps({ row: { type: Object, required: true } });
size="xs"
>
<QTooltip>
{{ $t('salesTicketsTable.risk') }}: {{ row.risk - row.credit }}
{{ $t('salesTicketsTable.risk') }}:
{{ toCurrency(row.risk - row.credit) }}
</QTooltip>
</QIcon>
<QIcon
@ -53,7 +68,7 @@ defineProps({ row: { type: Object, required: true } });
<QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip>
</QIcon>
<QIcon
v-if="!row?.isTaxDataChecked === 0"
v-if="row?.isTaxDataChecked !== 0"
name="vn:no036"
color="primary"
size="xs"

View File

@ -1,6 +1,6 @@
<script setup>
import { markRaw, computed } from 'vue';
import { QIcon, QCheckbox, QToggle } from 'quasar';
import { QIcon, QToggle } from 'quasar';
import { dashIfEmpty } from 'src/filters';
import VnSelect from 'components/common/VnSelect.vue';

View File

@ -91,7 +91,6 @@ const components = {
event: updateEvent,
attrs: {
...defaultAttrs,
style: 'min-width: 150px',
},
forceAttrs,
},
@ -152,7 +151,7 @@ const onTabPressed = async () => {
};
</script>
<template>
<div v-if="showFilter" class="full-width flex-center" style="overflow: hidden">
<div v-if="showFilter" class="full-width" style="overflow: hidden">
<VnColumn
:column="$props.column"
default="input"

View File

@ -23,6 +23,10 @@ const $props = defineProps({
type: Boolean,
default: false,
},
align: {
type: String,
default: 'end',
},
});
const hover = ref();
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
@ -46,16 +50,27 @@ async function orderBy(name, direction) {
}
defineExpose({ orderBy });
function textAlignToFlex(textAlign) {
return `justify-content: ${
{
'text-center': 'center',
'text-left': 'start',
'text-right': 'end',
}[textAlign] || 'start'
};`;
}
</script>
<template>
<div
@mouseenter="hover = true"
@mouseleave="hover = false"
@click="orderBy(name, model?.direction)"
class="row items-center no-wrap cursor-pointer title"
class="items-center no-wrap cursor-pointer title"
:style="textAlignToFlex(align)"
>
<span :title="label">{{ label }}</span>
<sup v-if="name && model?.index">
<div v-if="name && model?.index">
<QChip
:label="!vertical ? model?.index : ''"
:icon="
@ -92,20 +107,16 @@ defineExpose({ orderBy });
/>
</div>
</QChip>
</sup>
</div>
</div>
</template>
<style lang="scss" scoped>
.title {
display: flex;
justify-content: center;
align-items: center;
height: 30px;
width: 100%;
color: var(--vn-label-color);
}
sup {
vertical-align: super; /* Valor predeterminado */
/* También puedes usar otros valores como "baseline", "top", "text-top", etc. */
white-space: nowrap;
}
</style>

View File

@ -10,14 +10,15 @@ import {
render,
inject,
useAttrs,
nextTick,
} from 'vue';
import { useArrayData } from 'src/composables/useArrayData';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useQuasar, date } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { useFilterParams } from 'src/composables/useFilterParams';
import { dashIfEmpty } from 'src/filters';
import { dashIfEmpty, toDate } from 'src/filters';
import CrudModel from 'src/components/CrudModel.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
@ -30,6 +31,7 @@ import VnLv from 'components/ui/VnLv.vue';
import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
import VnTableFilter from './VnTableFilter.vue';
import { getColAlign } from 'src/composables/getColAlign';
import RightMenu from '../common/RightMenu.vue';
const arrayData = useArrayData(useAttrs()['data-key']);
const $props = defineProps({
@ -49,10 +51,6 @@ const $props = defineProps({
type: Boolean,
default: true,
},
rightSearchIcon: {
type: Boolean,
default: true,
},
rowClick: {
type: [Function, Boolean],
default: null,
@ -136,6 +134,10 @@ const $props = defineProps({
createComplement: {
type: Object,
},
dataCy: {
type: String,
default: 'vn-table',
},
});
const { t } = useI18n();
@ -164,7 +166,6 @@ const app = inject('app');
const editingRow = ref(null);
const editingField = ref(null);
const isTableMode = computed(() => mode.value == TABLE_MODE);
const showRightIcon = computed(() => $props.rightSearch || $props.rightSearchIcon);
const selectRegex = /select/;
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
const tableModes = [
@ -252,7 +253,9 @@ function splitColumns(columns) {
col.columnFilter = { inWhere: true, ...col.columnFilter };
splittedColumns.value.columns.push(col);
}
// Status column
splittedColumns.value.create = createOrderSort(splittedColumns.value.create);
if (splittedColumns.value.chips.length) {
splittedColumns.value.columnChips = splittedColumns.value.chips.filter(
(c) => !c.isId,
@ -268,6 +271,24 @@ function splitColumns(columns) {
}
}
function createOrderSort(columns) {
const orderedColumn = columns
.map((column, index) =>
column.createOrder !== undefined ? { ...column, originalIndex: index } : null,
)
.filter((item) => item !== null);
orderedColumn.sort((a, b) => a.createOrder - b.createOrder);
const filteredColumns = columns.filter((col) => col.createOrder === undefined);
orderedColumn.forEach((col) => {
filteredColumns.splice(col.createOrder, 0, col);
});
return filteredColumns;
}
const rowClickFunction = computed(() => {
if ($props.rowClick != undefined) return $props.rowClick;
if ($props.redirect) return ({ id }) => redirectFn(id);
@ -313,8 +334,14 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
if (evt?.shiftKey && added) {
const rowIndex = selectedRows[0].$index;
const selectedIndexes = new Set(selected.value.map((row) => row.$index));
for (const row of rows) {
if (row.$index == rowIndex) break;
const minIndex = selectedIndexes.size
? Math.min(...selectedIndexes, rowIndex)
: 0;
const maxIndex = Math.max(...selectedIndexes, rowIndex);
for (let i = minIndex; i <= maxIndex; i++) {
const row = rows[i];
if (row.$index == rowIndex) continue;
if (!selectedIndexes.has(row.$index)) {
selected.value.push(row);
selectedIndexes.add(row.$index);
@ -337,14 +364,14 @@ function hasEditableFormat(column) {
const clickHandler = async (event) => {
const clickedElement = event.target.closest('td');
const isDateElement = event.target.closest('.q-date');
const isTimeElement = event.target.closest('.q-time');
const isQSelectDropDown = event.target.closest('.q-select__dropdown-icon');
if (isDateElement || isTimeElement) return;
if (isDateElement || isTimeElement || isQSelectDropDown) return;
if (clickedElement === null) {
destroyInput(editingRow.value, editingField.value);
await destroyInput(editingRow.value, editingField.value);
return;
}
const rowIndex = clickedElement.getAttribute('data-row-index');
@ -352,19 +379,19 @@ const clickHandler = async (event) => {
const column = $props.columns.find((col) => col.name === colField);
if (editingRow.value !== null && editingField.value !== null) {
if (editingRow.value === rowIndex && editingField.value === colField) {
return;
}
if (editingRow.value == rowIndex && editingField.value == colField) return;
destroyInput(editingRow.value, editingField.value);
await destroyInput(editingRow.value, editingField.value);
}
if (isEditableColumn(column))
if (isEditableColumn(column)) {
await renderInput(Number(rowIndex), colField, clickedElement);
}
};
async function handleTabKey(event, rowIndex, colField) {
if (editingRow.value == rowIndex && editingField.value == colField)
destroyInput(editingRow.value, editingField.value);
await destroyInput(editingRow.value, editingField.value);
const direction = event.shiftKey ? -1 : 1;
const { nextRowIndex, nextColumnName } = await handleTabNavigation(
@ -411,20 +438,14 @@ async function renderInput(rowId, field, clickedElement) {
focusOnMount: true,
eventHandlers: {
'update:modelValue': async (value) => {
if (isSelect) {
row[column.name] = value[column.attrs?.optionValue ?? 'id'];
row[column?.name + 'TextValue'] =
value[column.attrs?.optionLabel ?? 'name'];
await column?.cellEvent?.['update:modelValue']?.(
value,
oldValue,
row,
);
if (isSelect && value) {
await updateSelectValue(value, column, row, oldValue);
} else row[column.name] = value;
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
},
keyup: async (event) => {
if (event.key === 'Enter') handleBlur(rowId, field, clickedElement);
if (event.key === 'Enter')
await destroyInput(rowId, field, clickedElement);
},
keydown: async (event) => {
switch (event.key) {
@ -433,7 +454,7 @@ async function renderInput(rowId, field, clickedElement) {
event.stopPropagation();
break;
case 'Escape':
destroyInput(rowId, field, clickedElement);
await destroyInput(rowId, field, clickedElement);
break;
default:
break;
@ -448,16 +469,31 @@ async function renderInput(rowId, field, clickedElement) {
node.appContext = app._context;
render(node, clickedElement);
if (['checkbox', 'toggle', undefined].includes(column?.component))
if (['toggle'].includes(column?.component))
node.el?.querySelector('span > div').focus();
if (['checkbox', undefined].includes(column?.component))
node.el?.querySelector('span > div > div').focus();
}
function destroyInput(rowIndex, field, clickedElement) {
async function updateSelectValue(value, column, row, oldValue) {
row[column.name] = value[column.attrs?.optionValue ?? 'id'];
row[column?.name + 'VnTableTextValue'] = value[column.attrs?.optionLabel ?? 'name'];
if (column?.attrs?.find?.label)
row[column?.attrs?.find?.label] = value[column.attrs?.optionLabel ?? 'name'];
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
}
async function destroyInput(rowIndex, field, clickedElement) {
if (!clickedElement)
clickedElement = document.querySelector(
`[data-row-index="${rowIndex}"][data-col-field="${field}"]`,
);
if (clickedElement) {
await nextTick();
render(null, clickedElement);
Array.from(clickedElement.childNodes).forEach((child) => {
child.style.visibility = 'visible';
@ -469,10 +505,6 @@ function destroyInput(rowIndex, field, clickedElement) {
editingField.value = null;
}
function handleBlur(rowIndex, field, clickedElement) {
destroyInput(rowIndex, field, clickedElement);
}
async function handleTabNavigation(rowIndex, colName, direction) {
const columns = $props.columns;
const totalColumns = columns.length;
@ -488,9 +520,7 @@ async function handleTabNavigation(rowIndex, colName, direction) {
if (isEditableColumn(columns[newColumnIndex])) break;
} while (iterations < totalColumns);
if (iterations >= totalColumns) {
return;
}
if (iterations >= totalColumns + 1) return;
if (direction === 1 && newColumnIndex <= currentColumnIndex) {
rowIndex++;
@ -519,27 +549,83 @@ function getToggleIcon(value) {
}
function formatColumnValue(col, row, dashIfEmpty) {
if (col?.format) {
if (selectRegex.test(col?.component) && row[col?.name + 'TextValue']) {
return dashIfEmpty(row[col?.name + 'TextValue']);
if (col?.format || row[col?.name + 'VnTableTextValue']) {
if (selectRegex.test(col?.component) && row[col?.name + 'VnTableTextValue']) {
return dashIfEmpty(row[col?.name + 'VnTableTextValue']);
} else {
return col.format(row, dashIfEmpty);
}
} else {
return dashIfEmpty(row[col?.name]);
}
if (col?.component === 'date') return dashIfEmpty(toDate(row[col?.name]));
if (col?.component === 'time')
return row[col?.name] >= 5
? dashIfEmpty(date.formatDate(new Date(row[col?.name]), 'HH:mm'))
: row[col?.name];
if (selectRegex.test(col?.component) && $props.isEditable) {
const { find, url } = col.attrs;
const urlRelation = url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1);
if (col?.attrs.options) {
const find = col?.attrs.options.find((option) => option.id === row[col.name]);
if (!col.attrs?.optionLabel || !find) return dashIfEmpty(row[col?.name]);
return dashIfEmpty(find[col.attrs?.optionLabel ?? 'name']);
}
if (typeof row[urlRelation] == 'object') {
if (typeof find == 'object')
return dashIfEmpty(row[urlRelation][find?.label ?? 'name']);
return dashIfEmpty(row[urlRelation][col?.attrs.optionLabel ?? 'name']);
}
if (typeof row[urlRelation] == 'string') return dashIfEmpty(row[urlRelation]);
}
return dashIfEmpty(row[col?.name]);
}
const checkbox = ref(null);
function cardClick(_, row) {
if ($props.redirect) router.push({ path: `/${$props.redirect}/${row.id}` });
}
function removeTextValue(data, getChanges) {
let changes = data.updates;
if (!changes) return data;
for (const change of changes) {
for (const key in change.data) {
if (key.endsWith('VnTableTextValue')) {
delete change.data[key];
}
}
}
data.updates = changes.filter((change) => Object.keys(change.data).length > 0);
if ($attrs?.beforeSaveFn) data = $attrs.beforeSaveFn(data, getChanges);
return data;
}
function handleRowClick(event, row) {
if (event.ctrlKey) return rowCtrlClickFunction.value(event, row);
if (rowClickFunction.value) rowClickFunction.value(row);
}
const rowCtrlClickFunction = computed(() => {
if ($props.rowCtrlClick != undefined) return $props.rowCtrlClick;
if ($props.redirect)
return (evt, { id }) => {
stopEventPropagation(evt);
window.open(`/#/${$props.redirect}/${id}`, '_blank');
};
return () => {};
});
</script>
<template>
<QDrawer
v-if="$props.rightSearch"
v-model="stateStore.rightDrawer"
side="right"
:width="256"
:overlay="$props.overlay"
>
<QScrollArea class="fit">
<RightMenu v-if="$props.rightSearch" :overlay="overlay">
<template #right-panel>
<VnTableFilter
:data-key="$attrs['data-key']"
:columns="columns"
@ -553,8 +639,8 @@ const checkbox = ref(null);
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
</VnTableFilter>
</QScrollArea>
</QDrawer>
</template>
</RightMenu>
<CrudModel
v-bind="$attrs"
:class="$attrs['class'] ?? 'q-px-md'"
@ -563,6 +649,7 @@ const checkbox = ref(null);
@on-fetch="(...args) => emit('onFetch', ...args)"
:search-url="searchUrl"
:disable-infinite-scroll="isTableMode"
:before-save-fn="removeTextValue"
@save-changes="reload"
:has-sub-toolbar="$props.hasSubToolbar ?? isEditable"
:auto-load="hasParams || $attrs['auto-load']"
@ -590,9 +677,10 @@ const checkbox = ref(null);
:style="isTableMode && `max-height: ${tableHeight}`"
:virtual-scroll="isTableMode"
@virtual-scroll="handleScroll"
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
@row-click="(event, row) => handleRowClick(event, row)"
@update:selected="emit('update:selected', $event)"
@selection="(details) => handleSelection(details, rows)"
:hide-selected-banner="true"
>
<template #top-left v-if="!$props.withoutHeader">
<slot name="top-left"> </slot>
@ -606,35 +694,28 @@ const checkbox = ref(null);
:skip="columnsVisibilitySkipped"
/>
<QBtnToggle
v-if="!tableModes.some((mode) => mode.disable)"
v-model="mode"
toggle-color="primary"
class="bg-vn-section-color"
dense
:options="tableModes.filter((mode) => !mode.disable)"
/>
<QBtn
v-if="showRightIcon"
icon="filter_alt"
class="bg-vn-section-color q-ml-sm"
dense
@click="stateStore.toggleRightDrawer()"
/>
</template>
<template #header-cell="{ col }">
<QTh
v-if="col.visible ?? true"
v-bind:class="col.headerClass"
class="body-cell"
:style="col?.width ? `max-width: ${col?.width}` : ''"
style="padding: inherit"
>
<div
class="no-padding"
:style="
withFilters && $props.columnSearch ? 'height: 75px' : ''
"
:style="[
withFilters && $props.columnSearch ? 'height: 75px' : '',
]"
>
<div class="text-center" style="height: 30px">
<div style="height: 30px">
<QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip>
<VnTableOrder
v-model="orders[col.orderBy ?? col.name]"
@ -642,6 +723,7 @@ const checkbox = ref(null);
:label="col?.labelAbbreviation ?? col?.label"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
:align="getColAlign(col)"
/>
</div>
<VnFilter
@ -723,7 +805,11 @@ const checkbox = ref(null);
<span
v-else
:class="hasEditableFormat(col)"
:style="col?.style ? col.style(row) : null"
:style="
typeof col?.style == 'function'
? col.style(row)
: col?.style
"
style="bottom: 0"
>
{{ formatColumnValue(col, row, dashIfEmpty) }}
@ -750,7 +836,7 @@ const checkbox = ref(null);
flat
dense
:class="
btn.isPrimary ? 'text-primary-light' : 'color-vn-text '
btn.isPrimary ? 'text-primary-light' : 'color-vn-label'
"
:style="`visibility: ${
((btn.show && btn.show(row)) ?? true)
@ -758,23 +844,19 @@ const checkbox = ref(null);
: 'hidden'
}`"
@click="btn.action(row)"
:data-cy="btn?.name ?? `tableAction-${index}`"
/>
</QTd>
</template>
<template #item="{ row, colsMap }">
<component
:is="$props.redirect ? 'router-link' : 'span'"
:to="`/${$props.redirect}/` + row.id"
v-bind:is="'div'"
@click="(event) => cardClick(event, row)"
>
<QCard
bordered
flat
class="row no-wrap justify-between cursor-pointer q-pa-sm"
@click="
(_, row) => {
$props.rowClick && $props.rowClick(row);
}
"
style="height: 100%"
>
<QCardSection
@ -811,7 +893,7 @@ const checkbox = ref(null);
</QCardSection>
<!-- Fields -->
<QCardSection
class="q-pl-sm q-pr-lg q-py-xs"
class="q-pl-sm q-py-xs"
:class="$props.cardClass"
>
<div
@ -858,13 +940,14 @@ const checkbox = ref(null);
:key="index"
:title="btn.title"
:icon="btn.icon"
data-cy="cardBtn"
class="q-pa-xs"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
: 'color-vn-label'
"
flat
@click="btn.action(row)"
/>
</QCardSection>
@ -931,14 +1014,6 @@ const checkbox = ref(null);
transition-show="scale"
transition-hide="scale"
:full-width="createComplement?.isFullWidth ?? false"
@before-hide="
() => {
if (createRef.isSaveAndContinue) {
showForm = true;
createForm.formInitialData = { ...create.formInitialData };
}
}
"
data-cy="vn-table-create-dialog"
>
<FormModelPopup
@ -949,7 +1024,10 @@ const checkbox = ref(null);
>
<template #form-inputs="{ data }">
<div :style="createComplement?.containerStyle">
<div>
<div
:style="createComplement?.previousStyle"
v-if="!quasar.screen.xs"
>
<slot name="previous-create-dialog" :data="data" />
</div>
<div class="grid-create" :style="createComplement?.columnGridStyle">
@ -962,7 +1040,10 @@ const checkbox = ref(null);
:label="column.label"
>
<VnColumn
:column="column"
:column="{
...column,
...{ disable: column?.createDisable ?? false },
}"
:row="{}"
default="input"
v-model="data[column.name]"
@ -1022,8 +1103,8 @@ es:
}
.body-cell {
padding-left: 2px !important;
padding-right: 2px !important;
padding-left: 4px !important;
padding-right: 4px !important;
position: relative;
}
.bg-chip-secondary {
@ -1042,8 +1123,8 @@ es:
.grid-three {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
max-width: 100%;
grid-template-columns: repeat(auto-fit, minmax(300px, max-content));
width: 100%;
grid-gap: 20px;
margin: 0 auto;
}
@ -1117,6 +1198,7 @@ es:
.vn-label-value {
display: flex;
flex-direction: row;
align-items: center;
color: var(--vn-text-color);
.value {
overflow: hidden;

View File

@ -27,30 +27,58 @@ describe('VnTable', () => {
beforeEach(() => (vm.selected = []));
describe('handleSelection()', () => {
const rows = [{ $index: 0 }, { $index: 1 }, { $index: 2 }];
const selectedRows = [{ $index: 1 }];
it('should add rows to selected when shift key is pressed and rows are added except last one', () => {
const rows = [
{ $index: 0 },
{ $index: 1 },
{ $index: 2 },
{ $index: 3 },
{ $index: 4 },
];
it('should add rows to selected when shift key is pressed and rows are added in ascending order', () => {
const selectedRows = [{ $index: 1 }];
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows
rows,
);
expect(vm.selected).toEqual([{ $index: 0 }]);
});
it('should add rows to selected when shift key is pressed and rows are added in descending order', () => {
const selectedRows = [{ $index: 3 }];
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows,
);
expect(vm.selected).toEqual([{ $index: 0 }, { $index: 1 }, { $index: 2 }]);
});
it('should not add rows to selected when shift key is not pressed', () => {
const selectedRows = [{ $index: 1 }];
vm.handleSelection(
{ evt: { shiftKey: false }, added: true, rows: selectedRows },
rows
rows,
);
expect(vm.selected).toEqual([]);
});
it('should not add rows to selected when rows are not added', () => {
const selectedRows = [{ $index: 1 }];
vm.handleSelection(
{ evt: { shiftKey: true }, added: false, rows: selectedRows },
rows
rows,
);
expect(vm.selected).toEqual([]);
});
it('should add all rows between the smallest and largest selected indexes', () => {
vm.selected = [{ $index: 1 }, { $index: 3 }];
const selectedRows = [{ $index: 4 }];
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows,
);
expect(vm.selected).toEqual([{ $index: 1 }, { $index: 3 }, { $index: 2 }]);
});
});
});

View File

@ -57,6 +57,7 @@ describe('FormModel', () => {
vm.state.set(model, formInitialData);
expect(vm.hasChanges).toBe(false);
await vm.$nextTick();
vm.formData.mockKey = 'newVal';
await vm.$nextTick();
expect(vm.hasChanges).toBe(true);
@ -94,8 +95,12 @@ describe('FormModel', () => {
it('should call axios.patch with the right data', async () => {
const spy = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} });
const { vm } = mount({ propsData: { url, model } });
vm.formData.mockKey = 'newVal';
vm.formData = {};
await vm.$nextTick();
vm.formData = { mockKey: 'newVal' };
await vm.$nextTick();
await vm.save();
expect(spy).toHaveBeenCalled();
vm.formData.mockKey = 'mockVal';

View File

@ -11,6 +11,13 @@ const stateStore = useStateStore();
const slots = useSlots();
const hasContent = useHasContent('#right-panel');
defineProps({
overlay: {
type: Boolean,
default: false,
},
});
onMounted(() => {
if ((!slots['right-panel'] && !hasContent.value) || quasar.platform.is.mobile)
stateStore.rightDrawer = false;
@ -34,7 +41,12 @@ onMounted(() => {
</QBtn>
</div>
</Teleport>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256">
<QDrawer
v-model="stateStore.rightDrawer"
side="right"
:width="256"
:overlay="overlay"
>
<QScrollArea class="fit">
<div id="right-panel"></div>
<slot v-if="!hasContent" name="right-panel" />

View File

@ -39,6 +39,13 @@ onBeforeMount(async () => {
});
onBeforeRouteUpdate(async (to, from) => {
if (hasRouteParam(to.params)) {
const { matched } = router.currentRoute.value;
const { name } = matched.at(-3);
if (name) {
router.push({ name, params: to.params });
}
}
const id = to.params.id;
if (id !== from.params.id) await fetch(id, true);
});
@ -50,6 +57,9 @@ async function fetch(id, append = false) {
else arrayData.store.url = props.url.replace(regex, `/${id}`);
await arrayData.fetch({ append, updateRouter: false });
}
function hasRouteParam(params, valueToCheck = ':addressId') {
return Object.values(params).includes(valueToCheck);
}
</script>
<template>
<Teleport to="#left-panel" v-if="stateStore.isHeaderMounted()">

View File

@ -27,7 +27,7 @@ const checkboxModel = computed({
</script>
<template>
<div>
<QCheckbox v-bind="$attrs" v-on="$attrs" v-model="checkboxModel" />
<QCheckbox v-bind="$attrs" v-model="checkboxModel" />
<QIcon
v-if="info"
v-bind="$attrs"

View File

@ -2,7 +2,7 @@
const $props = defineProps({
colors: {
type: String,
default: '{"value":[]}',
default: '{"value": []}',
},
});
@ -11,7 +11,7 @@ const maxHeight = 30;
const colorHeight = maxHeight / colorArray?.length;
</script>
<template>
<div class="color-div" :style="{ height: `${maxHeight}px` }">
<div v-if="colors" class="color-div" :style="{ height: `${maxHeight}px` }">
<div
v-for="(color, index) in colorArray"
:key="index"

View File

@ -48,7 +48,8 @@ function toValueAttrs(attrs) {
<span
v-for="toComponent of componentArray"
:key="toComponent.name"
class="column flex-center fit"
class="column fit"
:class="toComponent?.component == 'checkbox' ? 'flex-center' : ''"
>
<component
v-if="toComponent?.component"

View File

@ -107,6 +107,7 @@ const manageDate = (date) => {
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
hide-bottom-space
:data-cy="$attrs.dataCy ?? $attrs.label + '_inputDate'"
>
<template #append>
<QIcon

View File

@ -85,6 +85,7 @@ const handleModelValue = (data) => {
:tooltip="t('Create new location')"
:rules="mixinRules"
:lazy-rules="true"
required
>
<template #form>
<CreateNewPostcode

View File

@ -641,15 +641,7 @@ watch(
>
{{ prop.nameI18n }}:
</span>
<VnJsonValue :value="prop.val.val" />
<span
v-if="prop.val.id"
class="id-value"
>
#{{ prop.val.id }}
</span>
<span v-if="log.action == 'update'">
<VnJsonValue
:value="prop.old.val"
/>
@ -659,6 +651,26 @@ watch(
>
#{{ prop.old.id }}
</span>
<VnJsonValue
:value="prop.val.val"
/>
<span
v-if="prop.val.id"
class="id-value"
>
#{{ prop.val.id }}
</span>
</span>
<span v-else="prop.old.val">
<VnJsonValue
:value="prop.val.val"
/>
<span
v-if="prop.old.id"
class="id-value"
>#{{ prop.old.id }}</span
>
</span>
</div>
</span>

View File

@ -302,8 +302,6 @@ defineExpose({ opts: myOptions, vnSelectRef });
function handleKeyDown(event) {
if (event.key === 'Tab' && !event.shiftKey) {
event.preventDefault();
const inputValue = vnSelectRef.value?.inputValue;
if (inputValue) {

View File

@ -6,6 +6,7 @@ import { useArrayData } from 'composables/useArrayData';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useState } from 'src/composables/useState';
import { useRoute } from 'vue-router';
import { useClipboard } from 'src/composables/useClipboard';
import VnMoreOptions from './VnMoreOptions.vue';
const $props = defineProps({
@ -29,10 +30,6 @@ const $props = defineProps({
type: String,
default: null,
},
module: {
type: String,
default: null,
},
summary: {
type: Object,
default: null,
@ -46,6 +43,7 @@ const $props = defineProps({
const state = useState();
const route = useRoute();
const { t } = useI18n();
const { copyText } = useClipboard();
const { viewSummary } = useSummaryDialog();
let arrayData;
let store;
@ -78,6 +76,15 @@ onBeforeMount(async () => {
);
});
const routeName = computed(() => {
const DESCRIPTOR_PROXY = 'DescriptorProxy';
let name = $props.dataKey;
if ($props.dataKey.includes(DESCRIPTOR_PROXY)) {
name = name.split(DESCRIPTOR_PROXY)[0];
}
return `${name}Summary`;
});
async function getData() {
store.url = $props.url;
store.filter = $props.filter ?? {};
@ -103,6 +110,14 @@ function getValueFromPath(path) {
return current;
}
function copyIdText(id) {
copyText(id, {
component: {
copyValue: id,
},
});
}
const emit = defineEmits(['onFetch']);
const iconModule = computed(() => route.matched[1].meta.icon);
@ -148,7 +163,7 @@ const toModule = computed(() =>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
<RouterLink :to="{ name: `${module}Summary`, params: { id: entity.id } }">
<RouterLink :to="{ name: routeName, params: { id: entity.id } }">
<QBtn
class="link"
color="white"
@ -184,10 +199,23 @@ const toModule = computed(() =>
</slot>
</div>
</QItemLabel>
<QItem dense>
<QItemLabel class="subtitle" caption>
<QItem>
<QItemLabel class="subtitle">
#{{ getValueFromPath(subtitle) ?? entity.id }}
</QItemLabel>
<QBtn
round
flat
dense
size="sm"
icon="content_copy"
color="primary"
@click.stop="copyIdText(entity.id)"
>
<QTooltip>
{{ t('globals.copyId') }}
</QTooltip>
</QBtn>
</QItem>
</QList>
<div class="list-box q-mt-xs">
@ -197,7 +225,7 @@ const toModule = computed(() =>
<div class="icons">
<slot name="icons" :entity="entity" />
</div>
<div class="actions justify-center">
<div class="actions justify-center" data-cy="descriptor_actions">
<slot name="actions" :entity="entity" />
</div>
<slot name="after" />
@ -294,3 +322,11 @@ const toModule = computed(() =>
}
}
</style>
<i18n>
en:
globals:
copyId: Copy ID
es:
globals:
copyId: Copiar ID
</i18n>

View File

@ -204,8 +204,9 @@ async function search() {
}
:deep(.q-field--focused) {
.q-icon {
color: black;
.q-icon,
.q-placeholder {
color: var(--vn-black-text-color);
}
}

View File

@ -53,3 +53,8 @@ const manaCode = ref(props.manaCode);
/>
</div>
</template>
<i18n>
es:
Promotion mana: Maná promoción
Claim mana: Maná reclamación
</i18n>

View File

@ -0,0 +1,66 @@
import { describe, it, expect, vi } from 'vitest';
import { useRequired } from '../useRequired';
vi.mock('../useValidator', () => ({
useValidator: () => ({
validations: () => ({
required: vi.fn((isRequired, val) => {
if (!isRequired) return true;
return val !== null && val !== undefined && val !== '';
}),
}),
}),
}));
describe('useRequired', () => {
it('should detect required when attr is boolean true', () => {
const attrs = { required: true };
const { isRequired } = useRequired(attrs);
expect(isRequired).toBe(true);
});
it('should detect required when attr is boolean false', () => {
const attrs = { required: false };
const { isRequired } = useRequired(attrs);
expect(isRequired).toBe(false);
});
it('should detect required when attr exists without value', () => {
const attrs = { required: '' };
const { isRequired } = useRequired(attrs);
expect(isRequired).toBe(true);
});
it('should return false when required attr does not exist', () => {
const attrs = { someOtherAttr: 'value' };
const { isRequired } = useRequired(attrs);
expect(isRequired).toBe(false);
});
describe('requiredFieldRule', () => {
it('should validate required field with value', () => {
const attrs = { required: true };
const { requiredFieldRule } = useRequired(attrs);
expect(requiredFieldRule('some value')).toBe(true);
});
it('should validate required field with empty value', () => {
const attrs = { required: true };
const { requiredFieldRule } = useRequired(attrs);
expect(requiredFieldRule('')).toBe(false);
});
it('should pass validation when field is not required', () => {
const attrs = { required: false };
const { requiredFieldRule } = useRequired(attrs);
expect(requiredFieldRule('')).toBe(true);
});
it('should handle null and undefined values', () => {
const attrs = { required: true };
const { requiredFieldRule } = useRequired(attrs);
expect(requiredFieldRule(null)).toBe(false);
expect(requiredFieldRule(undefined)).toBe(false);
});
});
});

View File

@ -29,7 +29,6 @@ export async function checkEntryLock(entryFk, userFk) {
.dialog({
component: VnConfirm,
componentProps: {
'data-cy': 'entry-lock-confirm',
title: t('entry.lock.title'),
message: t('entry.lock.message', {
userName: data?.user?.nickname,

View File

@ -1,13 +1,14 @@
export function getColAlign(col) {
let align;
switch (col.component) {
case 'time':
case 'date':
case 'select':
align = 'left';
break;
case 'number':
align = 'right';
break;
case 'date':
case 'checkbox':
align = 'center';
break;

View File

@ -11,6 +11,7 @@ export async function useCau(res, message) {
const { config, headers, request, status, statusText, data } = res || {};
const { params, url, method, signal, headers: confHeaders } = config || {};
const { message: resMessage, code, name } = data?.error || {};
delete confHeaders?.Authorization;
const additionalData = {
path: location.hash,
@ -40,7 +41,7 @@ export async function useCau(res, message) {
handler: async () => {
const locale = i18n.global.t;
const reason = ref(
code == 'ACCESS_DENIED' ? locale('cau.askPrivileges') : ''
code == 'ACCESS_DENIED' ? locale('cau.askPrivileges') : '',
);
openConfirmationModal(
locale('cau.title'),
@ -59,10 +60,9 @@ export async function useCau(res, message) {
'onUpdate:modelValue': (val) => (reason.value = val),
label: locale('cau.inputLabel'),
class: 'full-width',
required: true,
autofocus: true,
},
}
},
);
},
},

View File

@ -2,14 +2,10 @@ import { useValidator } from 'src/composables/useValidator';
export function useRequired($attrs) {
const { validations } = useValidator();
const hasRequired = Object.keys($attrs).includes('required');
let isRequired = false;
if (hasRequired) {
const required = $attrs['required'];
if (typeof required === 'boolean') {
isRequired = required;
}
}
const isRequired =
typeof $attrs['required'] === 'boolean'
? $attrs['required']
: Object.keys($attrs).includes('required');
const requiredFieldRule = (val) => validations().required(isRequired, val);
return {

View File

@ -335,3 +335,7 @@ input::-webkit-inner-spin-button {
border: 1px solid;
box-shadow: 0 4px 6px #00000000;
}
.containerShrinked {
width: 80%;
}

View File

@ -3,6 +3,8 @@ import { useI18n } from 'vue-i18n';
export default function (value, options = {}) {
if (!value) return;
if (!isValidDate(value)) return null;
if (!options.dateStyle && !options.timeStyle) {
options.day = '2-digit';
options.month = '2-digit';
@ -10,7 +12,12 @@ export default function (value, options = {}) {
}
const { locale } = useI18n();
const date = new Date(value);
const newDate = new Date(value);
return new Intl.DateTimeFormat(locale.value, options).format(date);
return new Intl.DateTimeFormat(locale.value, options).format(newDate);
}
// handle 0000-00-00
function isValidDate(date) {
const parsedDate = new Date(date);
return parsedDate instanceof Date && !isNaN(parsedDate.getTime());
}

View File

@ -49,6 +49,7 @@ globals:
rowRemoved: Row removed
pleaseWait: Please wait...
noPinnedModules: You don't have any pinned modules
enterToConfirm: Press Enter to confirm
summary:
basicData: Basic data
daysOnward: Days onward
@ -152,10 +153,12 @@ globals:
maxTemperature: Max
minTemperature: Min
changePass: Change password
setPass: Set password
deleteConfirmTitle: Delete selected elements
changeState: Change state
raid: 'Raid {daysInForward} days'
isVies: Vies
noData: No data available
pageTitles:
logIn: Login
addressEdit: Update address
@ -691,8 +694,10 @@ worker:
machine: Machine
business:
tableVisibleColumns:
id: ID
started: Start Date
ended: End Date
hourlyLabor: Time sheet
company: Company
reasonEnd: Reason for Termination
department: Department
@ -700,6 +705,7 @@ worker:
calendarType: Work Calendar
workCenter: Work Center
payrollCategories: Contract Category
workerBusinessAgreementName: Agreement
occupationCode: Contribution Code
rate: Rate
businessType: Contract Type
@ -829,6 +835,8 @@ travel:
CloneTravelAndEntries: Clone travel and his entries
deleteTravel: Delete travel
AddEntry: Add entry
availabled: Availabled
availabledHour: Availabled hour
thermographs: Thermographs
hb: HB
basicData:

View File

@ -51,6 +51,7 @@ globals:
pleaseWait: Por favor espera...
noPinnedModules: No has fijado ningún módulo
split: Split
enterToConfirm: Pulsa Enter para confirmar
summary:
basicData: Datos básicos
daysOnward: Días adelante
@ -156,10 +157,12 @@ globals:
maxTemperature: Máx
minTemperature: Mín
changePass: Cambiar contraseña
setPass: Establecer contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado
raid: 'Redada {daysInForward} días'
isVies: Vies
noData: Datos no disponibles
pageTitles:
logIn: Inicio de sesión
addressEdit: Modificar consignatario
@ -767,8 +770,10 @@ worker:
concept: Concepto
business:
tableVisibleColumns:
id: Id
started: Fecha inicio
ended: Fecha fin
hourlyLabor: Ficha
company: Empresa
reasonEnd: Motivo finalización
department: Departamento
@ -779,12 +784,13 @@ worker:
occupationCode: Cotización
rate: Tarifa
businessType: Contrato
workerBusinessAgreementName: Convenio
amount: Salario
basicSalary: Salario transportistas
notes: Notas
operator:
numberOfWagons: Número de vagones
train: tren
train: Tren
itemPackingType: Tipo de embalaje
warehouse: Almacén
sector: Sector
@ -838,6 +844,7 @@ supplier:
verified: Verificado
isActive: Está activo
billingData: Forma de pago
financialData: Datos financieros
payDeadline: Plazo de pago
payDay: Día de pago
account: Cuenta
@ -915,6 +922,8 @@ travel:
deleteTravel: Eliminar envío
AddEntry: Añadir entrada
thermographs: Termógrafos
availabled: F. Disponible
availabledHour: Hora Disponible
hb: HB
basicData:
daysInForward: Desplazamiento automatico (redada)

View File

@ -51,7 +51,6 @@ const removeAlias = () => {
<CardDescriptor
ref="descriptor"
:url="`MailAliases/${entityId}`"
module="Alias"
data-key="Alias"
title="alias"
>

View File

@ -24,7 +24,6 @@ onMounted(async () => {
ref="descriptor"
:url="`VnUsers/preview`"
:filter="{ ...filter, where: { id: entityId } }"
module="Account"
data-key="Account"
title="nickname"
>

View File

@ -25,16 +25,23 @@ const $props = defineProps({
const { t } = useI18n();
const { hasAccount } = toRefs($props);
const { openConfirmationModal } = useVnConfirm();
const arrayData = useArrayData('Account');
const route = useRoute();
const router = useRouter();
const state = useState();
const user = state.getUser();
const { notify } = useQuasar();
const account = computed(() => useArrayData('Account').store.data[0]);
const account = computed(() => arrayData.store.data);
account.value.hasAccount = hasAccount.value;
const entityId = computed(() => +route.params.id);
const hasitManagementAccess = ref();
const hasSysadminAccess = ref();
const isHimself = computed(() => user.value.id === account.value.id);
const url = computed(() =>
isHimself.value
? 'Accounts/change-password'
: `Accounts/${entityId.value}/setPassword`,
);
async function updateStatusAccount(active) {
if (active) {
@ -107,11 +114,8 @@ onMounted(() => {
:ask-old-pass="askOldPass"
:submit-fn="
async (newPassword, oldPassword) => {
await axios.patch(`Accounts/change-password`, {
userId: entityId,
newPassword,
oldPassword,
});
const body = isHimself ? { userId: entityId, oldPassword } : {};
await axios.patch(url, { ...body, newPassword });
}
"
/>
@ -150,21 +154,16 @@ onMounted(() => {
t('account.card.actions.disableAccount.title'),
t('account.card.actions.disableAccount.subtitle'),
() => deleteAccount(),
() => deleteAccount(),
)
"
>
<QItemSection>{{ t('globals.delete') }}</QItemSection>
</QItem>
<QItem
v-if="hasSysadminAccess"
v-ripple
clickable
@click="user.id === account.id ? onChangePass(true) : onChangePass(false)"
>
<QItemSection v-if="user.id === account.id">
{{ t('globals.changePass') }}
<QItem v-if="hasSysadminAccess || isHimself" v-ripple clickable>
<QItemSection @click="onChangePass(isHimself)">
{{ isHimself ? t('globals.changePass') : t('globals.setPass') }}
</QItemSection>
<QItemSection v-else>{{ t('globals.setPass') }}</QItemSection>
</QItem>
<QItem
v-if="!account.hasAccount && hasSysadminAccess"
@ -175,6 +174,7 @@ onMounted(() => {
t('account.card.actions.enableAccount.title'),
t('account.card.actions.enableAccount.subtitle'),
() => updateStatusAccount(true),
() => updateStatusAccount(true),
)
"
>
@ -189,6 +189,7 @@ onMounted(() => {
t('account.card.actions.disableAccount.title'),
t('account.card.actions.disableAccount.subtitle'),
() => updateStatusAccount(false),
() => updateStatusAccount(false),
)
"
>
@ -204,6 +205,7 @@ onMounted(() => {
t('account.card.actions.activateUser.title'),
t('account.card.actions.activateUser.title'),
() => updateStatusUser(true),
() => updateStatusUser(true),
)
"
>
@ -218,6 +220,7 @@ onMounted(() => {
t('account.card.actions.deactivateUser.title'),
t('account.card.actions.deactivateUser.title'),
() => updateStatusUser(false),
() => updateStatusUser(false),
)
"
>

View File

@ -35,7 +35,6 @@ const removeRole = async () => {
<CardDescriptor
url="VnRoles"
:filter="{ where: { id: entityId } }"
module="Role"
data-key="Role"
:summary="$props.summary"
>

View File

@ -27,6 +27,7 @@ const claimActionsForm = ref();
const rows = ref([]);
const selectedRows = ref([]);
const destinationTypes = ref([]);
const shelvings = ref([]);
const totalClaimed = ref(null);
const DEFAULT_MAX_RESPONSABILITY = 5;
const DEFAULT_MIN_RESPONSABILITY = 1;
@ -56,6 +57,12 @@ const columns = computed(() => [
field: (row) => row.claimDestinationFk,
align: 'left',
},
{
name: 'shelving',
label: t('shelvings.shelving'),
field: (row) => row.shelvingFk,
align: 'left',
},
{
name: 'Landed',
label: t('Landed'),
@ -125,6 +132,10 @@ async function updateDestination(claimDestinationFk, row, options = {}) {
options.reload && claimActionsForm.value.reload();
}
}
async function updateShelving(shelvingFk, row) {
await axios.patch(`ClaimEnds/${row.id}`, { shelvingFk });
claimActionsForm.value.reload();
}
async function regularizeClaim() {
await post(`Claims/${claimId}/regularizeClaim`);
@ -200,6 +211,7 @@ async function post(query, params) {
auto-load
@on-fetch="(data) => (destinationTypes = data)"
/>
<FetchData url="Shelvings" auto-load @on-fetch="(data) => (shelvings = data)" />
<RightMenu v-if="claim">
<template #right-panel>
<QCard class="totalClaim q-my-md q-pa-sm no-box-shadow">
@ -312,6 +324,20 @@ async function post(query, params) {
/>
</QTd>
</template>
<template #body-cell-shelving="{ row }">
<QTd>
<VnSelect
v-model="row.shelvingFk"
:options="shelvings"
option-label="code"
option-value="id"
style="width: 100px"
hide-selected
@update:model-value="(value) => updateShelving(value, row)"
/>
</QTd>
</template>
<template #body-cell-price="{ value }">
<QTd align="center">
{{ toCurrency(value) }}
@ -354,7 +380,7 @@ async function post(query, params) {
(value) =>
updateDestination(
value,
props.row
props.row,
)
"
/>
@ -371,6 +397,17 @@ async function post(query, params) {
</QTable>
</template>
<template #moreBeforeActions>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:label="tMobile('Import claim')"
:title="t('Import claim')"
icon="Download"
@click="importToNewRefundTicket"
:disable="claim.claimStateFk == resolvedStateId"
:loading="loading"
/>
<QBtn
color="primary"
text-color="white"
@ -394,17 +431,6 @@ async function post(query, params) {
@click="dialogDestination = !dialogDestination"
:loading="loading"
/>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:label="tMobile('Import claim')"
:title="t('Import claim')"
icon="Upload"
@click="importToNewRefundTicket"
:disable="claim.claimStateFk == resolvedStateId"
:loading="loading"
/>
</template>
</CrudModel>
<QDialog v-model="dialogDestination">

View File

@ -40,7 +40,7 @@ const workersOptions = ref([]);
</VnRow>
<VnRow>
<VnSelect
:label="t('claim.assignedTo')"
:label="t('claim.attendedBy')"
v-model="data.workerFk"
:options="workersOptions"
option-value="id"

View File

@ -46,7 +46,6 @@ onMounted(async () => {
<CardDescriptor
:url="`Claims/${entityId}`"
:filter="filter"
module="Claim"
title="client.name"
data-key="Claim"
>

View File

@ -190,7 +190,7 @@ async function saveWhenHasChanges() {
ref="claimLinesForm"
:url="`Claims/${route.params.id}/lines`"
save-url="ClaimBeginnings/crud"
:filter="linesFilter"
:user-filter="linesFilter"
@on-fetch="onFetch"
v-model:selected="selected"
:default-save="false"

View File

@ -156,7 +156,6 @@ function onDrag() {
url="Claims"
:filter="claimDmsFilter"
@on-fetch="([data]) => setClaimDms(data)"
limit="20"
auto-load
ref="claimDmsRef"
/>

View File

@ -233,20 +233,27 @@ function claimUrl(section) {
<ClaimDescriptorMenu :claim="entity.claim" />
</template>
<template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one" v-if="$route.name != 'ClaimSummary'">
<QCard class="vn-one">
<VnTitle
:url="claimUrl('basic-data')"
:text="t('globals.pageTitles.basicData')"
/>
<VnLv :label="t('claim.created')" :value="toDate(claim.created)" />
<VnLv :label="t('claim.state')">
<VnLv
v-if="$route.name != 'ClaimSummary'"
:label="t('claim.created')"
:value="toDate(claim.created)"
/>
<VnLv v-if="$route.name != 'ClaimSummary'" :label="t('claim.state')">
<template #value>
<QChip :color="stateColor(claim.claimState.code)" dense>
{{ claim.claimState.description }}
</QChip>
</template>
</VnLv>
<VnLv :label="t('globals.salesPerson')">
<VnLv
v-if="$route.name != 'ClaimSummary'"
:label="t('globals.salesPerson')"
>
<template #value>
<VnUserLink
:name="claim.client?.salesPersonUser?.name"
@ -254,7 +261,7 @@ function claimUrl(section) {
/>
</template>
</VnLv>
<VnLv :label="t('claim.attendedBy')">
<VnLv v-if="$route.name != 'ClaimSummary'" :label="t('claim.attendedBy')">
<template #value>
<VnUserLink
:name="claim.worker?.user?.nickname"
@ -262,7 +269,7 @@ function claimUrl(section) {
/>
</template>
</VnLv>
<VnLv :label="t('claim.customer')">
<VnLv v-if="$route.name != 'ClaimSummary'" :label="t('claim.customer')">
<template #value>
<span class="link cursor-pointer">
{{ claim.client?.name }}
@ -274,6 +281,11 @@ function claimUrl(section) {
:label="t('claim.pickup')"
:value="`${dashIfEmpty(claim.pickup)}`"
/>
<VnLv
:label="t('globals.packages')"
:value="`${dashIfEmpty(claim.packages)}`"
:translation="(value) => t(`claim.packages`)"
/>
</QCard>
<QCard class="vn-two">
<VnTitle :url="claimUrl('notes')" :text="t('claim.notes')" />

View File

@ -19,30 +19,36 @@ const columns = [
name: 'itemFk',
label: t('Id item'),
columnFilter: false,
align: 'left',
align: 'right',
},
{
name: 'ticketFk',
label: t('Ticket'),
columnFilter: false,
align: 'left',
align: 'right',
},
{
name: 'claimDestinationFk',
label: t('Destination'),
columnFilter: false,
align: 'left',
align: 'right',
},
{
name: 'shelvingCode',
label: t('Shelving'),
columnFilter: false,
align: 'right',
},
{
name: 'landed',
label: t('Landed'),
format: (row) => toDate(row.landed),
align: 'left',
align: 'center',
},
{
name: 'quantity',
label: t('Quantity'),
align: 'left',
align: 'right',
},
{
name: 'concept',
@ -52,18 +58,18 @@ const columns = [
{
name: 'price',
label: t('Price'),
align: 'left',
align: 'right',
},
{
name: 'discount',
label: t('Discount'),
format: ({ discount }) => toPercentage(discount / 100),
align: 'left',
align: 'right',
},
{
name: 'total',
label: t('Total'),
align: 'left',
align: 'right',
},
];
</script>

View File

@ -1,8 +1,6 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
@ -14,15 +12,14 @@ const props = defineProps({
type: String,
required: true,
},
states: {
type: Array,
default: () => [],
},
});
const states = ref([]);
defineExpose({ states });
</script>
<template>
<FetchData url="ClaimStates" @on-fetch="(data) => (states = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
@ -109,7 +106,6 @@ defineExpose({ states });
:label="t('claim.zone')"
v-model="params.zoneFk"
url="Zones"
:use-like="false"
outlined
rounded
dense

View File

@ -10,12 +10,13 @@ import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnTable from 'src/components/VnTable/VnTable.vue';
import ZoneDescriptorProxy from '../Zone/Card/ZoneDescriptorProxy.vue';
import VnSection from 'src/components/common/VnSection.vue';
import FetchData from 'src/components/FetchData.vue';
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const dataKey = 'ClaimList';
const claimFilterRef = ref();
const states = ref([]);
const columns = computed(() => [
{
align: 'left',
@ -81,8 +82,7 @@ const columns = computed(() => [
align: 'left',
label: t('claim.state'),
format: ({ stateCode }) =>
claimFilterRef.value?.states.find(({ code }) => code === stateCode)
?.description,
states.value?.find(({ code }) => code === stateCode)?.description,
name: 'stateCode',
chip: {
condition: () => true,
@ -92,7 +92,7 @@ const columns = computed(() => [
name: 'claimStateFk',
component: 'select',
attrs: {
options: claimFilterRef.value?.states,
options: states.value,
optionLabel: 'description',
},
},
@ -125,6 +125,7 @@ const STATE_COLOR = {
</script>
<template>
<FetchData url="ClaimStates" @on-fetch="(data) => (states = data)" auto-load />
<VnSection
:data-key="dataKey"
:columns="columns"
@ -135,7 +136,7 @@ const STATE_COLOR = {
}"
>
<template #advanced-menu>
<ClaimFilter data-key="ClaimList" ref="claimFilterRef" />
<ClaimFilter :data-key ref="claimFilterRef" :states />
</template>
<template #body>
<VnTable

View File

@ -13,7 +13,6 @@ claim:
province: Province
zone: Zone
customerId: client ID
assignedTo: Assigned
created: Created
details: Details
item: Item

View File

@ -13,7 +13,6 @@ claim:
province: Provincia
zone: Zona
customerId: ID de cliente
assignedTo: Asignado a
created: Creado
details: Detalles
item: Artículo

View File

@ -17,8 +17,7 @@ const bankEntitiesRef = ref(null);
const filter = {
fields: ['id', 'bic', 'name'],
order: 'bic ASC',
limit: 30,
order: 'bic ASC'
};
const getBankEntities = (data, formData) => {

View File

@ -232,7 +232,6 @@ const updateDateParams = (value, params) => {
:include="'category'"
:sortBy="'name ASC'"
dense
@update:model-value="(data) => updateDateParams(data, params)"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -254,7 +253,6 @@ const updateDateParams = (value, params) => {
:fields="['id', 'name']"
:sortBy="'name ASC'"
dense
@update:model-value="(data) => updateDateParams(data, params)"
/>
<VnSelect
v-model="params.campaign"
@ -303,12 +301,14 @@ en:
valentinesDay: Valentine's Day
mothersDay: Mother's Day
allSaints: All Saints' Day
frenchMothersDay: Mother's Day in France
es:
Enter a new search: Introduce una nueva búsqueda
Group by items: Agrupar por artículos
valentinesDay: Día de San Valentín
mothersDay: Día de la Madre
allSaints: Día de Todos los Santos
frenchMothersDay: (Francia) Día de la Madre
Campaign consumption: Consumo campaña
Campaign: Campaña
From: Desde

View File

@ -55,7 +55,6 @@ const debtWarning = computed(() => {
<template>
<CardDescriptor
module="Customer"
:url="`Clients/${entityId}/getCard`"
:summary="$props.summary"
data-key="Customer"

View File

@ -2,7 +2,9 @@
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useQuasar } from 'quasar';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import FetchData from 'src/components/FetchData.vue';
import FormModel from 'src/components/FormModel.vue';
import VnRow from 'src/components/ui/VnRow.vue';
@ -10,11 +12,15 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
import { getDifferences, getUpdatedValues } from 'src/filters';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
const quasar = useQuasar();
const { t } = useI18n();
const route = useRoute();
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const typesTaxes = ref([]);
const typesTransactions = ref([]);
@ -30,7 +36,7 @@ const formModelRef = ref(null);
const hasChangedTaxData = ref(false);
async function formCustomFn(data) {
const { email, phone, mobile } = data;
const { email, phone, mobile } = data;

que @alexm confirme técnicamente el enfoque i en caso que este correcto, hazle test de front.

que @alexm confirme técnicamente el enfoque i en caso que este correcto, hazle test de front.

Este caso es muy concreto por eso la nueva prop en FormModel
No puede usarse lo que hay porque al darle a guardar tiene que hacer una comprobación previa(beforeSave), y según eso ejecutar una cosa después del guardado onDataSaved.
Me parece complicado y fácil de entender, el separar un flujo en dos funciones

Este caso es muy concreto por eso la nueva prop en FormModel No puede usarse lo que hay porque al darle a guardar tiene que hacer una comprobación previa(beforeSave), y según eso ejecutar una cosa después del guardado onDataSaved. Me parece complicado y fácil de entender, el separar un flujo en dos funciones

Okey, ya lo tengo. Cuando me confirmes hago e2e

Okey, ya lo tengo. Cuando me confirmes hago e2e
const hasContactData = email || phone || mobile;
if (hasChangedTaxData.value && hasContactData) await checkExistingClient(data);
@ -44,7 +50,7 @@ async function checkExistingClient({ email, phone, mobile, id }) {
if (phone) findParams.push({ phone: phone });
if (mobile) findParams.push({ mobile: mobile });
const filter = encodeURIComponent(
const filter = encodeURIComponent(
JSON.stringify({
where: {
and: [{ or: findParams }, { id: { neq: id } }],
@ -56,18 +62,49 @@ async function checkExistingClient({ email, phone, mobile, id }) {
const { data: exist } = await axios.get(query);
if (!exist) confirm(data);
else {
Review

aqui llamas con data primero y luego con body, imagino que lo habrás probado, de donde sale body?

aqui llamas con _data_ primero y luego con _body_, imagino que lo habrás probado, de donde sale body?
openConfirmationModal(
openConfirmationModal(
t('Found a client with this data'),
`${t('foundClient_before')} <a class="link" href="#!/client/${exist.id}/summary" target="_blank">${exist.id}</a> ${t('foundClient_after')}`,
() => confirm(data),
null,
);
}
}
}
async function confirm(data) {
async function confirm(data) {
await axios.patch(`Clients/${route.params.id}/updateFiscalData`, data);
}
function onBeforeSave(formData, originalData) {
return getUpdatedValues(
Object.keys(getDifferences(formData, originalData)),
formData,
);
}
async function checkEtChanges(data, _, originalData) {
const equalizatedHasChanged = originalData.isEqualizated != data.isEqualizated;
const hasToInvoiceByAddress =
originalData.hasToInvoiceByAddress || data.hasToInvoiceByAddress;
if (equalizatedHasChanged && hasToInvoiceByAddress) {
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('You changed the equalization tax'),
message: t('Do you want to spread the change?'),
promise: () => acceptPropagate(data),
},
});
} else if (equalizatedHasChanged) {
await acceptPropagate(data);
}
}
async function acceptPropagate({ isEqualizated }) {
await axios.patch(`Clients/${route.params.id}/addressesPropagateRe`, {
isEqualizated,
});
notify(t('Equivalent tax spreaded'), 'warning');
}
</script>
<template>
@ -84,6 +121,9 @@ async function checkExistingClient({ email, phone, mobile, id }) {
model="Customer"
:save-fn="formCustomFn"
prevent-submit
:mapper="onBeforeSave"
observe-form-changes
@on-data-saved="checkEtChanges"
>
<template #form="{ data, validate }">
<VnRow>
@ -196,7 +236,6 @@ async function checkExistingClient({ email, phone, mobile, id }) {
</VnRow>
</template>
</FormModel>
</template>
<i18n>
@ -226,6 +265,9 @@ es:
whenActivatingIt: Al activarlo, no informar el código del país en el campo nif
inOrderToInvoice: Para facturar no se consulta este campo, sino el RE de consignatario. Al modificar este campo si no esta marcada la casilla Facturar por consignatario, se propagará automaticamente el cambio a todos lo consignatarios, en caso contrario preguntará al usuario si quiere o no propagar
Daily invoice: Facturación diaria
Equivalent tax spreaded: Recargo de equivalencia propagado
You changed the equalization tax: Has cambiado el recargo de equivalencia
Do you want to spread the change?: ¿Deseas propagar el cambio a sus consignatarios?
en:
Found a client with this data: A client with this data has been found
foundClient_before: The client with id

View File

@ -270,7 +270,7 @@ const sumRisk = ({ clientRisks }) => {
<VnTitle
target="_blank"
:url="`${grafanaUrl}/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk=${entityId}`"
:text="t('customer.summary.payMethodFk')"
:text="t('customer.summary.financialData')"
icon="vn:grafana"
/>
<VnLv

View File

@ -143,6 +143,7 @@ const exprBuilder = (param, value) => {
outlined
rounded
auto-load
sortBy="name ASC"
/></QItemSection>
</QItem>
<QItem class="q-mb-sm">

View File

@ -78,10 +78,20 @@ const columns = computed(() => [
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
fields: ['id', 'name', 'firstName'],
where: { role: 'salesPerson' },
optionFilter: 'firstName',
},
columnFilter: {
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name', 'firstName'],
where: { role: 'salesPerson' },
optionLabel: 'firstName',
optionValue: 'id',
},
},
create: false,
columnField: {
component: null,

View File

@ -87,7 +87,7 @@ onMounted(async () => {
<FetchData
url="Campaigns/latest"
@on-fetch="(data) => (campaignsOptions = data)"
:filter="{ fields: ['id', 'code', 'dated'], order: 'code ASC', limit: 30 }"
:filter="{ fields: ['id', 'code', 'dated'], order: 'code ASC' }"
auto-load
/>
<FetchData

View File

@ -98,7 +98,6 @@ function onAgentCreated({ id, fiscalName }, data) {
:rules="validate('Worker.postcode')"
:acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
v-model="data.location"
:required="true"
@update:model-value="(location) => handleLocation(data, location)"
/>

View File

@ -96,11 +96,11 @@ const updateObservations = async (payload) => {
await axios.post('AddressObservations/crud', payload);
notes.value = [];
deletes.value = [];
toCustomerAddress();
};
async function updateAll({ data, payload }) {
await updateObservations(payload);
await updateAddress(data);
toCustomerAddress();
}
function getPayload() {
return {
@ -137,15 +137,12 @@ async function handleDialog(data) {
.onOk(async () => {
await updateAddressTicket();
await updateAll(body);
toCustomerAddress();
})
.onCancel(async () => {
await updateAll(body);
toCustomerAddress();
});
} else {
updateAll(body);
toCustomerAddress();
await updateAll(body);
}
}
@ -236,7 +233,7 @@ function handleLocation(data, location) {
postcode: data.postalCode,
city: data.city,
province: data.province,
country: data.province.country,
country: data.province?.country,
}"
@update:model-value="(location) => handleLocation(data, location)"
></VnLocation>

View File

@ -77,24 +77,23 @@ onBeforeMount(() => {
function setPaymentType(accounting) {
if (!accounting) return;
accountingType.value = accounting.accountingType;
initialData.description = [];
initialData.payed = Date.vnNew();
isCash.value = accountingType.value.code == 'cash';
viewReceipt.value = isCash.value;
if (accountingType.value.daysInFuture)
initialData.payed.setDate(
initialData.payed.getDate() + accountingType.value.daysInFuture
initialData.payed.getDate() + accountingType.value.daysInFuture,
);
maxAmount.value = accountingType.value && accountingType.value.maxAmount;
if (accountingType.value.code == 'compensation')
return (initialData.description = '');
if (accountingType.value.receiptDescription)
initialData.description.push(accountingType.value.receiptDescription);
if (initialData.description) initialData.description.push(initialData.description);
initialData.description = initialData.description.join(', ');
let descriptions = [];
if (accountingType.value.receiptDescription)
descriptions.push(accountingType.value.receiptDescription);
if (initialData.description) descriptions.push(initialData.description);
initialData.description = descriptions.join(', ');
}
const calculateFromAmount = (event) => {
@ -114,7 +113,7 @@ function onBeforeSave(data) {
if (isCash.value && shouldSendEmail.value && !data.email)
return notify(t('There is no assigned email for this client'), 'negative');
data.bankFk = data.bankFk.id;
data.bankFk = data.bankFk?.id;
return data;
}
@ -189,7 +188,7 @@ async function getAmountPaid() {
:url-create="urlCreate"
:mapper="onBeforeSave"
@on-data-saved="onDataSaved"
:prevent-submit="true"
prevent-submit
>
<template #form="{ data, validate }">
<span ref="closeButton" class="row justify-end close-icon" v-close-popup>

View File

@ -18,6 +18,7 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import CustomerSamplesPreview from 'src/pages/Customer/components/CustomerSamplesPreview.vue';
import FormPopup from 'src/components/FormPopup.vue';
import { useArrayData } from 'src/composables/useArrayData';
const { dialogRef, onDialogOK } = useDialogPluginComponent();
@ -39,7 +40,7 @@ const optionsSamplesVisible = ref([]);
const sampleType = ref({ hasPreview: false });
const initialData = reactive({});
const entityId = computed(() => route.params.id);
const customer = computed(() => state.get('Customer'));
const customer = computed(() => useArrayData('Customer').store?.data);
const filterEmailUsers = { where: { userFk: user.value.id } };
const filterClientsAddresses = {
include: [
@ -65,9 +66,9 @@ const filterSamplesVisible = {
defineEmits(['confirm', ...useDialogPluginComponent.emits]);
onBeforeMount(async () => {
initialData.clientFk = customer.value.id;
initialData.recipient = customer.value.email;
initialData.recipientId = customer.value.id;
initialData.clientFk = customer.value?.id;
initialData.recipient = customer.value?.email;
initialData.recipientId = customer.value?.id;
});
const setEmailUser = (data) => {

View File

@ -16,7 +16,6 @@ import ItemDescriptor from 'src/pages/Item/Card/ItemDescriptor.vue';
import axios from 'axios';
import VnSelectEnum from 'src/components/common/VnSelectEnum.vue';
import { checkEntryLock } from 'src/composables/checkEntryLock';
import SkeletonDescriptor from 'src/components/ui/SkeletonDescriptor.vue';
const $props = defineProps({
id: {
@ -55,6 +54,7 @@ const columns = [
toggleIndeterminate: false,
},
create: true,
createOrder: 12,
width: '25px',
},
{
@ -62,9 +62,10 @@ const columns = [
name: 'workerFk',
component: 'select',
attrs: {
url: 'Workers/search',
url: 'TicketRequests/getItemTypeWorker',
fields: ['id', 'nickname'],
optionLabel: 'nickname',
sortBy: 'nickname ASC',
optionValue: 'id',
},
visible: false,
@ -88,22 +89,13 @@ const columns = [
isEditable: false,
columnFilter: false,
},
{
name: 'entryFk',
isId: true,
visible: false,
isEditable: false,
disable: true,
create: true,
columnFilter: false,
},
{
align: 'center',
label: 'Id',
name: 'itemFk',
component: 'number',
isEditable: false,
width: '40px',
width: '35px',
},
{
labelAbbreviation: '',
@ -111,7 +103,7 @@ const columns = [
name: 'hex',
columnSearch: false,
isEditable: false,
width: '5px',
width: '9px',
component: 'select',
attrs: {
url: 'Inks',
@ -138,6 +130,7 @@ const columns = [
name: 'itemFk',
visible: false,
create: true,
createOrder: 0,
columnFilter: false,
},
{
@ -156,11 +149,13 @@ const columns = [
{
align: 'center',
labelAbbreviation: t('Sti.'),
label: t('Printed Stickers/Stickers'),
label: t('Stickers'),
toolTip: t('Printed Stickers/Stickers'),
name: 'stickers',
component: 'number',
component: 'input',
create: true,
createOrder: 1,
attrs: {
positive: false,
},
@ -179,8 +174,9 @@ const columns = [
component: 'select',
attrs: {
url: 'packagings',
fields: ['id', 'volume'],
fields: ['id'],
optionLabel: 'id',
optionValue: 'id',
},
create: true,
width: '40px',
@ -192,10 +188,10 @@ const columns = [
component: 'number',
create: true,
width: '35px',
format: (row) => parseFloat(row['weight']).toFixed(1),
},
{
align: 'center',
labelAbbreviation: 'Pack',
labelAbbreviation: 'P',
label: 'Packing',
toolTip: 'Packing',
name: 'packing',
@ -209,7 +205,7 @@ const columns = [
row['amount'] = row['quantity'] * row['buyingValue'];
},
},
width: '35px',
width: '30px',
style: (row) => {
if (row.groupingMode === 'grouping')
return { color: 'var(--vn-label-color)' };
@ -229,7 +225,7 @@ const columns = [
indeterminateValue: null,
},
size: 'xs',
width: '30px',
width: '25px',
create: true,
rightFilter: false,
getIcon: (value) => {
@ -245,12 +241,12 @@ const columns = [
},
{
align: 'center',
labelAbbreviation: 'Group',
labelAbbreviation: 'G',
label: 'Grouping',
toolTip: 'Grouping',
name: 'grouping',
component: 'number',
width: '35px',
width: '30px',
create: true,
style: (row) => {
if (row.groupingMode === 'packing') return { color: 'var(--vn-label-color)' };
@ -271,6 +267,7 @@ const columns = [
},
width: '45px',
create: true,
createOrder: 3,
style: getQuantityStyle,
},
{
@ -280,6 +277,7 @@ const columns = [
toolTip: t('Buying value'),
name: 'buyingValue',
create: true,
createOrder: 2,
component: 'number',
attrs: {
positive: false,
@ -290,6 +288,7 @@ const columns = [
},
},
width: '45px',
format: (row) => parseFloat(row['buyingValue']).toFixed(3),
},
{
align: 'center',
@ -301,6 +300,7 @@ const columns = [
positive: false,
},
isEditable: false,
format: (row) => parseFloat(row['amount']).toFixed(2),
style: getAmountStyle,
},
{
@ -310,14 +310,17 @@ const columns = [
toolTip: t('Package'),
name: 'price2',
component: 'number',
createDisable: true,
width: '35px',
create: true,
format: (row) => parseFloat(row['price2']).toFixed(2),
},
{
align: 'center',
label: t('Box'),
name: 'price3',
component: 'number',
createDisable: true,
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
row['price2'] = row['price2'] * (value / oldValue);
@ -325,25 +328,7 @@ const columns = [
},
width: '35px',
create: true,
},
{
align: 'center',
labelAbbreviation: 'Min.',
label: t('Minimum price'),
toolTip: t('Minimum price'),
name: 'minPrice',
component: 'number',
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
await axios.patch(`Items/${row['itemFk']}`, {
minPrice: value,
});
},
},
width: '35px',
style: (row) => {
if (!row?.hasMinPrice) return { color: 'var(--vn-label-color)' };
},
format: (row) => parseFloat(row['price3']).toFixed(2),
},
{
align: 'center',
@ -364,6 +349,26 @@ const columns = [
},
width: '25px',
},
{
align: 'center',
labelAbbreviation: 'Min.',
label: t('Minimum price'),
toolTip: t('Minimum price'),
name: 'minPrice',
component: 'number',
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
await axios.patch(`Items/${row['itemFk']}`, {
minPrice: value,
});
},
},
width: '35px',
style: (row) => {
if (!row?.hasMinPrice) return { color: 'var(--vn-label-color)' };
},
format: (row) => parseFloat(row['minPrice']).toFixed(2),
},
{
align: 'center',
labelAbbreviation: t('P.Sen'),
@ -373,6 +378,9 @@ const columns = [
component: 'number',
isEditable: false,
width: '40px',
style: () => {
return { color: 'var(--vn-label-color)' };
},
},
{
align: 'center',
@ -412,6 +420,9 @@ const columns = [
component: 'input',
isEditable: false,
width: '35px',
style: () => {
return { color: 'var(--vn-label-color)' };
},
},
];
@ -497,14 +508,15 @@ async function setBuyUltimate(itemFk, data) {
},
});
const buyUltimateData = buyUltimate.data[0];
if (!buyUltimateData) return;
const allowedKeys = columns
.filter((col) => col.create === true)
.map((col) => col.name);
allowedKeys.forEach((key) => {
if (buyUltimateData.hasOwnProperty(key) && key !== 'entryFk') {
data[key] = buyUltimateData[key];
if (buyUltimateData?.hasOwnProperty(key) && key !== 'entryFk') {
if (!['stickers', 'quantity'].includes(key)) data[key] = buyUltimateData[key];
}
});
}
@ -518,7 +530,7 @@ onMounted(() => {
<Teleport to="#st-data" v-if="stateStore?.isSubToolbarShown() && editableMode">
<QBtnGroup push style="column-gap: 1px">
<QBtnDropdown
icon="exposure_neg_1"
label="+/-"
color="primary"
flat
:title="t('Invert quantity value')"
@ -533,7 +545,7 @@ onMounted(() => {
@click="invertQuantitySign(selectedRows, -1)"
data-cy="set-negative-quantity"
>
<span style="font-size: medium">-1</span>
<span style="font-size: large">-</span>
</QBtn>
</QItemSection>
</QItem>
@ -544,7 +556,7 @@ onMounted(() => {
@click="invertQuantitySign(selectedRows, 1)"
data-cy="set-positive-quantity"
>
<span style="font-size: medium">1</span>
<span style="font-size: large">+</span>
</QBtn>
</QItemSection>
</QItem>
@ -558,11 +570,11 @@ onMounted(() => {
:disable="!selectedRows.length"
data-cy="check-buy-amount"
>
<QTooltip>{{}}</QTooltip>
<QList>
<QItem>
<QItemSection>
<QBtn
size="sm"
icon="check"
flat
@click="setIsChecked(selectedRows, true)"
@ -573,6 +585,7 @@ onMounted(() => {
<QItem>
<QItemSection>
<QBtn
size="sm"
icon="close"
flat
@click="setIsChecked(selectedRows, false)"
@ -595,7 +608,7 @@ onMounted(() => {
ref="entryBuysRef"
data-key="EntryBuys"
:url="`Entries/${entityId}/getBuyList`"
order="name DESC"
search-url="EntryBuys"
save-url="Buys/crud"
:disable-option="{ card: true }"
v-model:selected="selectedRows"
@ -625,21 +638,25 @@ onMounted(() => {
isFullWidth: true,
containerStyle: {
display: 'flex',
'flex-wrap': 'wrap',
gap: '16px',
position: 'relative',
height: '450px',
},
columnGridStyle: {
'max-width': '50%',
flex: 1,
'margin-right': '30px',
flex: 1,
},
previousStyle: {
'max-width': '30%',
height: '500px',
},
displayPrevious: true,
}"
:is-editable="editableMode"
:without-header="!editableMode"
:with-filters="editableMode"
:right-search="editableMode"
:right-search-icon="true"
:row-click="false"
:columns="columns"
:beforeSaveFn="beforeSave"
@ -648,6 +665,7 @@ onMounted(() => {
auto-load
footer
data-cy="entry-buys"
overlay
>
<template #column-hex="{ row }">
<VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" />
@ -662,7 +680,7 @@ onMounted(() => {
<FetchedTags :item="row" :columns="3" />
</template>
<template #column-stickers="{ row }">
<span class="editable-text">
<span :class="editableMode ? 'editable-text' : ''">
<span style="color: var(--vn-label-color)">
{{ row.printedStickers }}
</span>
@ -693,20 +711,36 @@ onMounted(() => {
</template>
<template #column-create-itemFk="{ data }">
<VnSelect
url="Items"
url="Items/search"
v-model="data.itemFk"
:label="t('Article')"
:fields="['id', 'name']"
:fields="['id', 'name', 'size', 'producerName']"
:filter-options="['id', 'name', 'size', 'producerName']"
option-label="name"
option-value="id"
@update:modelValue="
async (value) => {
setBuyUltimate(value, data);
await setBuyUltimate(value, data);
}
"
:required="true"
data-cy="itemFk-create-popup"
/>
sort-by="nickname DESC"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt.name }}
</QItemLabel>
<QItemLabel caption>
#{{ scope.opt.id }}, {{ scope.opt?.size }},
{{ scope.opt?.producerName }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template>
<template #column-create-groupingMode="{ data }">
<VnSelectEnum
@ -720,9 +754,14 @@ onMounted(() => {
/>
</template>
<template #previous-create-dialog="{ data }">
<div style="position: absolute">
<div
style="position: absolute"
:class="{ 'centered-container': !data.itemFk }"
>
<ItemDescriptor :id="data.itemFk" v-if="data.itemFk" />
<SkeletonDescriptor v-if="!data.itemFk" :has-image="true" />
<div v-else>
<span>{{ t('globals.noData') }}</span>
</div>
</div>
</template>
</VnTable>
@ -744,6 +783,7 @@ es:
Com.: Ref.
Comment: Referencia
Minimum price: Precio mínimo
Stickers: Etiquetas
Printed Stickers/Stickers: Etiquetas impresas/Etiquetas
Cost: Cost.
Buying value: Coste
@ -761,7 +801,12 @@ es:
Check buy amount: Marcar como correcta la cantidad de compra
</i18n>
<style lang="scss" scoped>
.test {
.centered-container {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
width: 40%;
height: 100%;
}
</style>

View File

@ -147,7 +147,6 @@ async function deleteEntry() {
<template>
<CardDescriptor
ref="entryDescriptorRef"
module="Entry"
:url="`Entries/${entityId}`"
:userFilter="entryFilter"
title="supplier.nickname"

View File

@ -65,7 +65,7 @@ const entriesTableColumns = computed(() => [
]);
function downloadCSV(rows) {
const headers = ['id', 'itemFk', 'name', 'stickers', 'packing', 'comment'];
const headers = ['id', 'itemFk', 'name', 'stickers', 'packing', 'grouping', 'comment'];
const csvRows = rows.map((row) => {
const buy = row;
@ -77,6 +77,7 @@ function downloadCSV(rows) {
item.name || '',
buy.stickers,
buy.packing,
buy.grouping,
item.comment || '',
].join(',');
});

View File

@ -248,7 +248,7 @@ const entryFilterPanel = ref();
<i18n>
en:
params:
isExcludedFromAvailable: Inventory
isExcludedFromAvailable: Is excluded
isOrdered: Ordered
isReceived: Received
isConfirmed: Confirmed

View File

@ -1,8 +1,6 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
import VnSelect from 'components/common/VnSelect.vue';
@ -18,18 +16,10 @@ defineProps({
},
});
const itemTypeWorkersOptions = ref([]);
const tagValues = ref([]);
</script>
<template>
<FetchData
url="TicketRequests/getItemTypeWorker"
limit="30"
auto-load
:filter="{ fields: ['id', 'nickname'], order: 'nickname ASC', limit: 30 }"
@on-fetch="(data) => (itemTypeWorkersOptions = data)"
/>
<ItemsFilterPanel :data-key="dataKey" :custom-tags="['tags']">
<template #body="{ params, searchFn }">
<QItem class="q-my-md">
@ -37,9 +27,10 @@ const tagValues = ref([]);
<VnSelect
:label="t('components.itemsFilterPanel.salesPersonFk')"
v-model="params.salesPersonFk"
:options="itemTypeWorkersOptions"
option-value="id"
url="TicketRequests/getItemTypeWorker"
option-label="nickname"
:fields="['id', 'nickname']"
sort-by="nickname ASC"
dense
outlined
rounded
@ -52,8 +43,9 @@ const tagValues = ref([]);
<QItemSection>
<VnSelectSupplier
v-model="params.supplierFk"
@update:model-value="searchFn()"
hide-selected
url="Suppliers"
:fields="['id', 'name', 'nickname']"
sort-by="name ASC"
dense
outlined
rounded

View File

@ -11,6 +11,8 @@ import VnTable from 'components/VnTable/VnTable.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue';
import { toDate } from 'src/filters';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import EntrySummary from './Card/EntrySummary.vue';
const { t } = useI18n();
const tableRef = ref();
@ -18,6 +20,7 @@ const defaultEntry = ref({});
const state = useState();
const user = state.getUser();
const dataKey = 'EntryList';
const { viewSummary } = useSummaryDialog();
const entryQueryFilter = {
include: [
@ -44,28 +47,32 @@ const entryQueryFilter = {
const columns = computed(() => [
{
label: 'Ex',
labelAbbreviation: 'Ex',
label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'),
toolTip: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'),
name: 'isExcludedFromAvailable',
component: 'checkbox',
width: '35px',
},
{
label: 'Pe',
labelAbbreviation: 'Pe',
label: t('entry.list.tableVisibleColumns.isOrdered'),
toolTip: t('entry.list.tableVisibleColumns.isOrdered'),
name: 'isOrdered',
component: 'checkbox',
width: '35px',
},
{
label: 'Le',
labelAbbreviation: 'LE',
label: t('entry.list.tableVisibleColumns.isConfirmed'),
toolTip: t('entry.list.tableVisibleColumns.isConfirmed'),
name: 'isConfirmed',
component: 'checkbox',
width: '35px',
},
{
label: 'Re',
labelAbbreviation: 'Re',
label: t('entry.list.tableVisibleColumns.isReceived'),
toolTip: t('entry.list.tableVisibleColumns.isReceived'),
name: 'isReceived',
component: 'checkbox',
@ -89,6 +96,7 @@ const columns = computed(() => [
chip: {
condition: () => true,
},
width: '50px',
},
{
label: t('entry.list.tableVisibleColumns.supplierFk'),
@ -99,8 +107,10 @@ const columns = computed(() => [
attrs: {
url: 'suppliers',
fields: ['id', 'name'],
where: { order: 'name DESC' },
},
format: (row, dashIfEmpty) => dashIfEmpty(row.supplierName),
width: '110px',
},
{
align: 'left',
@ -124,6 +134,7 @@ const columns = computed(() => [
label: 'AWB',
name: 'awbCode',
component: 'input',
width: '100px',
},
{
align: 'left',
@ -160,6 +171,7 @@ const columns = computed(() => [
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.warehouseOutName),
width: '65px',
},
{
align: 'left',
@ -175,20 +187,23 @@ const columns = computed(() => [
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.warehouseInName),
width: '65px',
},
{
align: 'left',
labelAbbreviation: t('Type'),
label: t('entry.list.tableVisibleColumns.entryTypeDescription'),
toolTip: t('entry.list.tableVisibleColumns.entryTypeDescription'),
name: 'entryTypeCode',
cardVisible: true,
},
{
name: 'dated',
label: t('entry.list.tableVisibleColumns.dated'),
component: 'date',
cardVisible: false,
visible: false,
create: true,
component: 'select',
attrs: {
url: 'entryTypes',
fields: ['code', 'description'],
optionValue: 'code',
optionLabel: 'description',
},
width: '65px',
format: (row, dashIfEmpty) => dashIfEmpty(row.entryTypeDescription),
},
{
name: 'companyFk',
@ -210,6 +225,19 @@ const columns = computed(() => [
visible: false,
create: true,
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
isPrimary: true,
action: (row) => viewSummary(row.id, EntrySummary, 'xlg-width'),
},
],
},
]);
function getBadgeAttrs(row) {
const date = row.landed;
@ -220,7 +248,8 @@ function getBadgeAttrs(row) {
let timeDiff = today - timeTicket;
if (timeDiff > 0) return { color: 'warning', 'text-color': 'black' };
if (timeDiff > 0) return { color: 'info', 'text-color': 'black' };
if (timeDiff < 0) return { color: 'warning', 'text-color': 'black' };
switch (row.entryTypeCode) {
case 'regularization':
case 'life':
@ -245,7 +274,6 @@ function getBadgeAttrs(row) {
default:
break;
}
if (timeDiff < 0) return { color: 'info', 'text-color': 'black' };
return { color: 'transparent' };
}
@ -255,16 +283,7 @@ onBeforeMount(async () => {
</script>
<template>
<VnSection
:data-key="dataKey"
prefix="entry"
url="Entries/filter"
:array-data-props="{
url: 'Entries/filter',
order: 'landed DESC',
userFilter: EntryFilter,
}"
>
<VnSection :data-key="dataKey" prefix="entry">
<template #advanced-menu>
<EntryFilter :data-key="dataKey" />
</template>
@ -273,6 +292,7 @@ onBeforeMount(async () => {
v-if="defaultEntry.defaultSupplierFk"
ref="tableRef"
:data-key="dataKey"
search-url="EntryList"
url="Entries/filter"
:filter="entryQueryFilter"
order="landed DESC"
@ -328,4 +348,5 @@ es:
Search entries: Buscar entradas
You can search by entry reference: Puedes buscar por referencia de la entrada
Create entry: Crear entrada
Type: Tipo
</i18n>

View File

@ -19,6 +19,7 @@ const { t } = useI18n();
const quasar = useQuasar();
const state = useState();
const user = state.getUser();
const footer = ref({ bought: 0, reserve: 0 });
const columns = computed(() => [
{
align: 'left',
@ -34,18 +35,18 @@ const columns = computed(() => [
label: t('entryStockBought.buyer'),
isTitle: true,
component: 'select',
isEditable: false,
cardVisible: true,
create: true,
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
where: { role: 'buyer' },
optionFilter: 'firstName',
optionLabel: 'name',
url: 'TicketRequests/getItemTypeWorker',
fields: ['id', 'nickname'],
optionLabel: 'nickname',
sortBy: 'nickname ASC',
optionValue: 'id',
useLike: false,
},
columnFilter: false,
width: '50px',
},
{
align: 'center',
@ -55,6 +56,8 @@ const columns = computed(() => [
create: true,
component: 'number',
summation: true,
width: '50px',
format: ({ reserve }, dashIfEmpty) => dashIfEmpty(round(reserve)),
},
{
align: 'center',
@ -62,6 +65,7 @@ const columns = computed(() => [
name: 'bought',
summation: true,
cardVisible: true,
style: ({ reserve, bought }) => boughtStyle(bought, reserve),
columnFilter: false,
},
{
@ -78,6 +82,7 @@ const columns = computed(() => [
actions: [
{
title: t('entryStockBought.viewMoreDetails'),
name: 'searchBtn',
icon: 'search',
isPrimary: true,
action: (row) => {
@ -132,20 +137,20 @@ function openDialog() {
}
function setFooter(data) {
const footer = {
bought: 0,
reserve: 0,
};
footer.value = { bought: 0, reserve: 0 };
data.forEach((row) => {
footer.bought += row?.bought;
footer.reserve += row?.reserve;
footer.value.bought += row?.bought;
footer.value.reserve += row?.reserve;
});
tableRef.value.footer = footer;
}
function round(value) {
return Math.round(value * 100) / 100;
}
function boughtStyle(bought, reserve) {
return reserve < bought ? { color: 'var(--q-negative)' } : '';
}
</script>
<template>
<VnSubToolbar>
@ -158,7 +163,7 @@ function round(value) {
@on-fetch="
(data) => {
travel = data.find(
(data) => data.warehouseIn?.code.toLowerCase() === 'vnh'
(data) => data.warehouseIn?.code.toLowerCase() === 'vnh',
);
}
"
@ -179,6 +184,7 @@ function round(value) {
@click="openDialog()"
:title="t('entryStockBought.editTravel')"
color="primary"
data-cy="edit-travel"
/>
</div>
</VnRow>
@ -239,31 +245,22 @@ function round(value) {
table-height="80vh"
auto-load
:column-search="false"
:without-header="true"
>
<template #column-workerFk="{ row }">
<span class="link" @click.stop>
{{ row?.worker?.user?.name }}
{{ row?.worker?.user?.nickname }}
<WorkerDescriptorProxy :id="row?.workerFk" />
</span>
</template>
<template #column-bought="{ row }">
<span :class="{ 'text-negative': row.reserve < row.bought }">
{{ row?.bought }}
</span>
</template>
<template #column-footer-reserve>
<span>
{{ round(tableRef.footer.reserve) }}
{{ round(footer.reserve) }}
</span>
</template>
<template #column-footer-bought>
<span
:class="{
'text-negative':
tableRef.footer.reserve < tableRef.footer.bought,
}"
>
{{ round(tableRef.footer.bought) }}
<span :style="boughtStyle(footer?.bought, footer?.reserve)">
{{ round(footer.bought) }}
</span>
</template>
</VnTable>
@ -279,10 +276,11 @@ function round(value) {
justify-content: center;
}
.column {
min-width: 35%;
margin-top: 5%;
display: flex;
flex-direction: column;
align-items: center;
min-width: 35%;
}
.text-negative {
color: $negative !important;

View File

@ -14,14 +14,14 @@ const $props = defineProps({
required: true,
},
dated: {
type: Date,
type: [Date, String],
required: true,
},
});
const customUrl = `StockBoughts/getStockBoughtDetail?workerFk=${$props.workerFk}&dated=${$props.dated}`;
const columns = [
{
align: 'left',
align: 'right',
label: t('Entry'),
name: 'entryFk',
isTitle: true,
@ -29,7 +29,7 @@ const columns = [
columnFilter: false,
},
{
align: 'left',
align: 'right',
name: 'itemFk',
label: t('Item'),
columnFilter: false,
@ -44,21 +44,21 @@ const columns = [
cardVisible: true,
},
{
align: 'left',
align: 'right',
name: 'volume',
label: t('Volume'),
columnFilter: false,
cardVisible: true,
},
{
align: 'left',
align: 'right',
label: t('Packaging'),
name: 'packagingFk',
columnFilter: false,
cardVisible: true,
},
{
align: 'left',
align: 'right',
label: 'Packing',
name: 'packing',
columnFilter: false,
@ -73,12 +73,14 @@ const columns = [
ref="tableRef"
data-key="StockBoughtsDetail"
:url="customUrl"
order="itemName DESC"
order="volume DESC"
:columns="columns"
:right-search="false"
:disable-infinite-scroll="true"
:disable-option="{ card: true }"
:limit="0"
:without-header="true"
:with-filters="false"
auto-load
>
<template #column-entryFk="{ row }">
@ -99,16 +101,14 @@ const columns = [
</template>
<style lang="css" scoped>
.container {
max-width: 50vw;
max-width: 100%;
width: 50%;
overflow: auto;
justify-content: center;
align-items: center;
margin: auto;
background-color: var(--vn-section-color);
padding: 4px;
}
.container > div > div > .q-table__top.relative-position.row.items-center {
background-color: red !important;
padding: 2%;
}
</style>
<i18n>

View File

@ -90,7 +90,6 @@ async function setInvoiceCorrection(id) {
<template>
<CardDescriptor
ref="cardDescriptorRef"
module="InvoiceIn"
data-key="InvoiceIn"
:url="`InvoiceIns/${entityId}`"
:filter="filter"

View File

@ -7,7 +7,6 @@ import { toDate } from 'src/filters';
import { useArrayData } from 'src/composables/useArrayData';
import { getTotal } from 'src/composables/getTotal';
import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import useNotify from 'src/composables/useNotify.js';
import VnInputDate from 'src/components/common/VnInputDate.vue';
@ -22,7 +21,6 @@ const invoiceIn = computed(() => arrayData.store.data);
const currency = computed(() => invoiceIn.value?.currency?.code);
const rowsSelected = ref([]);
const banks = ref([]);
const invoiceInFormRef = ref();
const invoiceId = +route.params.id;
const filter = { where: { invoiceInFk: invoiceId } };
@ -41,10 +39,9 @@ const columns = computed(() => [
name: 'bank',
label: t('Bank'),
field: (row) => row.bankFk,
options: banks.value,
model: 'bankFk',
optionValue: 'id',
optionLabel: 'bank',
url: 'Accountings',
sortable: true,
tabIndex: 2,
align: 'left',
@ -82,12 +79,6 @@ onBeforeMount(async () => {
});
</script>
<template>
<FetchData
url="Accountings"
auto-load
limit="30"
@on-fetch="(data) => (banks = data)"
/>
<CrudModel
v-if="invoiceIn"
ref="invoiceInFormRef"
@ -117,9 +108,9 @@ onBeforeMount(async () => {
<QTd>
<VnSelect
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:url="col.url"
:option-label="col.optionLabel"
:option-value="col.optionValue"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -193,8 +184,7 @@ onBeforeMount(async () => {
:label="t('Bank')"
class="full-width"
v-model="props.row['bankFk']"
:options="banks"
option-value="id"
url="Accountings"
option-label="bank"
>
<template #option="scope">

View File

@ -185,6 +185,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
data-key="InvoiceInSummary"
:url="`InvoiceIns/${entityId}/summary`"
@on-fetch="(data) => init(data)"
module-name="InvoiceIn"
>
<template #header="{ entity }">
<div>{{ entity.id }} - {{ entity.supplier?.name }}</div>

View File

@ -202,7 +202,7 @@ function setCursor(ref) {
:option-label="col.optionLabel"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
@keydown.tab="
@keydown.tab.prevent="
autocompleteExpense(
$event,
row,

View File

@ -36,7 +36,6 @@ function ticketFilter(invoice) {
<template>
<CardDescriptor
ref="descriptor"
module="InvoiceOut"
:url="`InvoiceOuts/${entityId}`"
:filter="filter"
title="ref"

View File

@ -103,7 +103,7 @@ const refundInvoice = async (withWarehouse) => {
t('refundInvoiceSuccessMessage', {
refundTicket: data[0].id,
}),
'positive'
'positive',
);
};
@ -124,6 +124,13 @@ const showRefundInvoiceForm = () => {
},
});
};
const showExportationLetter = () => {
openReport(`InvoiceOuts/${$props.invoiceOutData.ref}/exportation-pdf`, {
recipientId: $props.invoiceOutData.client.id,
refFk: $props.invoiceOutData.ref,
});
};
</script>
<template>
@ -156,10 +163,14 @@ const showRefundInvoiceForm = () => {
<QMenu anchor="top end" self="top start">
<QList>
<QItem v-ripple clickable @click="showSendInvoiceDialog('pdf')">
<QItemSection>{{ t('Send PDF') }}</QItemSection>
<QItemSection data-cy="InvoiceOutDescriptorMenuSendPdfOption">
{{ t('Send PDF') }}
</QItemSection>
</QItem>
<QItem v-ripple clickable @click="showSendInvoiceDialog('csv')">
<QItemSection>{{ t('Send CSV') }}</QItemSection>
<QItemSection data-cy="InvoiceOutDescriptorMenuSendCsvOption">
{{ t('Send CSV') }}
</QItemSection>
</QItem>
</QList>
</QMenu>
@ -172,7 +183,7 @@ const showRefundInvoiceForm = () => {
t('Confirm deletion'),
t('Are you sure you want to delete this invoice?'),
deleteInvoice,
redirectToInvoiceOutList
redirectToInvoiceOutList,
)
"
>
@ -185,7 +196,7 @@ const showRefundInvoiceForm = () => {
openConfirmationModal(
'',
t('Are you sure you want to book this invoice?'),
bookInvoice
bookInvoice,
)
"
>
@ -198,7 +209,7 @@ const showRefundInvoiceForm = () => {
openConfirmationModal(
t('Generate PDF invoice document'),
t('Are you sure you want to generate/regenerate the PDF invoice?'),
generateInvoicePdf
generateInvoicePdf,
)
"
>
@ -226,6 +237,14 @@ const showRefundInvoiceForm = () => {
{{ t('Create a single ticket with all the content of the current invoice') }}
</QTooltip>
</QItem>
<QItem
v-if="$props.invoiceOutData.serial === 'E'"
v-ripple
clickable
@click="showExportationLetter()"
>
<QItemSection>{{ t('Show CITES letter') }}</QItemSection>
</QItem>
</template>
<i18n>
@ -255,7 +274,7 @@ es:
Create a single ticket with all the content of the current invoice: Crear un ticket único con todo el contenido de la factura actual
refundInvoiceSuccessMessage: Se ha creado el siguiente ticket de abono {refundTicket}
The email can't be empty: El email no puede estar vacío
Show CITES letter: Ver carta CITES
en:
refundInvoiceSuccessMessage: The following refund ticket have been created {refundTicket}
</i18n>

View File

@ -125,7 +125,7 @@ const ticketsColumns = ref([
:value="toDate(invoiceOut.issued)"
/>
<VnLv
:label="t('invoiceOut.summary.dued')"
:label="t('invoiceOut.summary.expirationDate')"
:value="toDate(invoiceOut.dued)"
/>
<VnLv :label="t('globals.created')" :value="toDate(invoiceOut.created)" />

View File

@ -22,7 +22,7 @@ const states = ref();
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<strong>{{ t(`invoiceOut.params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
@ -84,15 +84,6 @@ const states = ref();
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
v-model="params.issued"
:label="t('Issued')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
@ -110,37 +101,3 @@ const states = ref();
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
search: Contains
clientFk: Customer
fi: FI
amount: Amount
min: Min
max: Max
hasPdf: Has PDF
issued: Issued
created: Created
dued: Dued
es:
params:
search: Contiene
clientFk: Cliente
fi: CIF
amount: Importe
min: Min
max: Max
hasPdf: Tiene PDF
issued: Emitida
created: Creada
dued: Vencida
Customer ID: ID cliente
FI: CIF
Amount: Importe
Has PDF: Tiene PDF
Issued: Fecha emisión
Created: Fecha creación
Dued: Fecha vencimiento
</i18n>

View File

@ -21,7 +21,6 @@ import VnSection from 'src/components/common/VnSection.vue';
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const tableRef = ref();
const invoiceOutSerialsOptions = ref([]);
const customerOptions = ref([]);
const selectedRows = ref([]);
const hasSelectedCards = computed(() => selectedRows.value.length > 0);
@ -71,14 +70,6 @@ const columns = computed(() => [
inWhere: true,
},
},
{
align: 'left',
name: 'issued',
label: t('invoiceOut.summary.issued'),
component: 'date',
format: (row) => toDate(row.issued),
columnField: { component: null },
},
{
align: 'left',
name: 'clientFk',
@ -376,7 +367,6 @@ watchEffect(selectedRows);
url="InvoiceOutSerials"
v-model="data.serial"
:label="t('invoiceOutModule.serial')"
:options="invoiceOutSerialsOptions"
option-label="description"
option-value="code"
option-filter

View File

@ -10,6 +10,8 @@ import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vu
import TicketDescriptorProxy from '../Ticket/Card/TicketDescriptorProxy.vue';
import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import InvoiceOutNegativeBasesFilter from './InvoiceOutNegativeBasesFilter.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
const { t } = useI18n();
const tableRef = ref();
@ -97,16 +99,19 @@ const columns = computed(() => [
align: 'left',
name: 'isActive',
label: t('invoiceOut.negativeBases.active'),
component: 'checkbox',
},
{
align: 'left',
name: 'hasToInvoice',
label: t('invoiceOut.negativeBases.hasToInvoice'),
component: 'checkbox',
},
{
align: 'left',
name: 'hasVerifiedData',
name: 'isTaxDataChecked',
label: t('invoiceOut.negativeBases.verifiedData'),
component: 'checkbox',
},
{
align: 'left',
@ -142,7 +147,7 @@ const downloadCSV = async () => {
await invoiceOutGlobalStore.getNegativeBasesCsv(
userParams.from,
userParams.to,
filterParams
filterParams,
);
};
</script>
@ -154,6 +159,11 @@ const downloadCSV = async () => {
</QBtn>
</template>
</VnSubToolbar>
<RightMenu>
<template #right-panel>
<InvoiceOutNegativeBasesFilter data-key="negativeFilter" />
</template>
</RightMenu>
<VnTable
ref="tableRef"
data-key="negativeFilter"
@ -174,6 +184,7 @@ const downloadCSV = async () => {
auto-load
:is-editable="false"
:use-model="true"
:right-search="false"
>
<template #column-clientId="{ row }">
<span class="link" @click.stop>

View File

@ -2,9 +2,10 @@
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 VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
const { t } = useI18n();
const props = defineProps({
@ -24,11 +25,11 @@ const props = defineProps({
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<strong>{{ t(`invoiceOut.params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params }">
<template #body="{ params, searchFn }">
<QItem>
<QItemSection>
<VnInputDate
@ -49,38 +50,70 @@ const props = defineProps({
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.company"
<VnSelect
url="Companies"
:label="t('globals.company')"
is-outlined
/>
v-model="params.company"
option-label="code"
option-value="code"
dense
outlined
rounded
@update:model-value="searchFn()"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.code }}
</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
<VnSelect
url="Countries"
:label="t('globals.params.countryFk')"
v-model="params.country"
:label="t('globals.country')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.clientId"
:label="t('invoiceOut.negativeBases.clientId')"
is-outlined
/>
option-label="name"
option-value="name"
outlined
dense
rounded
@update:model-value="searchFn()"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.name }}
</QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.clientSocialName"
<VnSelect
url="Clients"
:label="t('globals.client')"
is-outlined
v-model="params.clientId"
outlined
dense
rounded
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
@ -90,15 +123,18 @@ const props = defineProps({
v-model="params.amount"
:label="t('globals.amount')"
is-outlined
:positive="false"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.comercialName"
<VnSelectWorker
:label="t('invoiceOut.negativeBases.comercial')"
v-model="params.workerName"
option-value="name"
is-outlined
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>

View File

@ -2,9 +2,10 @@ invoiceOut:
search: Search invoice
searchInfo: You can search by invoice reference
params:
id: ID
company: Company
country: Country
clientId: Client ID
clientId: Client
clientSocialName: Client
taxableBase: Base
ticketFk: Ticket
@ -12,6 +13,19 @@ invoiceOut:
hasToInvoice: Has to invoice
hasVerifiedData: Verified data
workerName: Worker
isTaxDataChecked: Verified data
amount: Amount
clientFk: Client
companyFk: Company
created: Created
dued: Dued
customsAgentFk: Custom Agent
ref: Reference
fi: FI
min: Min
max: Max
hasPdf: Has PDF
search: Contains
card:
issued: Issued
customerCard: Customer card
@ -19,6 +33,7 @@ invoiceOut:
summary:
issued: Issued
dued: Due
expirationDate: Expiration date
booked: Booked
taxBreakdown: Tax breakdown
taxableBase: Taxable base
@ -52,7 +67,7 @@ invoiceOut:
active: Active
hasToInvoice: Has to Invoice
verifiedData: Verified Data
comercial: Commercial
comercial: Sales person
errors:
downloadCsvFailed: CSV download failed
invoiceOutModule:

View File

@ -2,9 +2,10 @@ invoiceOut:
search: Buscar factura emitida
searchInfo: Puedes buscar por referencia de la factura
params:
id: ID
company: Empresa
country: País
clientId: ID del cliente
clientId: Cliente
clientSocialName: Cliente
taxableBase: Base
ticketFk: Ticket
@ -12,6 +13,19 @@ invoiceOut:
hasToInvoice: Debe facturar
hasVerifiedData: Datos verificados
workerName: Comercial
isTaxDataChecked: Datos comprobados
amount: Importe
clientFk: Cliente
companyFk: Empresa
created: Creada
dued: Vencida
customsAgentFk: Agente aduanas
ref: Referencia
fi: CIF
min: Min
max: Max
hasPdf: Tiene PDF
search: Contiene
card:
issued: Fecha emisión
customerCard: Ficha del cliente
@ -19,6 +33,7 @@ invoiceOut:
summary:
issued: Fecha
dued: Fecha límite
expirationDate: Fecha vencimiento
booked: Contabilizada
taxBreakdown: Desglose impositivo
taxableBase: Base imp.

View File

@ -92,7 +92,6 @@ const updateStock = async () => {
<template>
<CardDescriptor
data-key="Item"
module="Item"
:summary="$props.summary"
:url="`Items/${entityId}/getCard`"
@on-fetch="setData"
@ -121,22 +120,9 @@ const updateStock = async () => {
</template>
</VnLv>
<VnLv :label="t('globals.producer')" :value="dashIfEmpty(entity.subName)" />
<VnLv
v-if="entity.value5"
:label="t('item.descriptor.color')"
:value="entity.value5"
>
</VnLv>
<VnLv
v-if="entity.value6"
:label="t('item.descriptor.category')"
:value="entity.value6"
/>
<VnLv
v-if="entity.value7"
:label="t('item.list.stems')"
:value="entity.value7"
/>
<VnLv v-if="entity?.value5" :label="entity?.tag5" :value="entity.value5" />
<VnLv v-if="entity?.value6" :label="entity?.tag6" :value="entity.value6" />
<VnLv v-if="entity?.value7" :label="entity?.tag7" :value="entity.value7" />
</template>
<template #icons="{ entity }">
<QCardActions v-if="entity" class="q-gutter-x-md">

View File

@ -12,7 +12,7 @@ import FetchData from 'components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import { toDateFormat } from 'src/filters/date.js';
import { toDateTimeFormat } from 'src/filters/date.js';
import { dashIfEmpty } from 'src/filters';
import { date } from 'quasar';
import { useState } from 'src/composables/useState';
@ -27,7 +27,7 @@ const user = state.getUser();
const today = Date.vnNew();
today.setHours(0, 0, 0, 0);
const warehousesOptions = ref([]);
const itemBalances = computed(() => arrayDataItemBalances.store.data);
const itemBalances = computed(() => arrayDataItemBalances.store.data || []);
const where = computed(() => arrayDataItemBalances.store.filter.where || {});
const showWhatsBeforeInventory = ref(false);
const inventoriedDate = ref(null);
@ -143,7 +143,12 @@ onMounted(async () => {
const fetchItemBalances = async () => await arrayDataItemBalances.fetch({});
const getBadgeAttrs = (_date) => {
const isSameDate = date.isSameDate(today, _date);
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(_date);
timeTicket.setHours(0, 0, 0, 0);
const isSameDate = date.isSameDate(today, timeTicket);
const attrs = {
'text-color': isSameDate ? 'black' : 'white',
color: isSameDate ? 'warning' : 'transparent',
@ -244,7 +249,7 @@ async function updateWarehouse(warehouseFk) {
dense
style="font-size: 14px"
>
{{ toDateFormat(row.shipped) }}
{{ toDateTimeFormat(row.shipped) }}
</QBadge>
</QTd>
</template>
@ -313,8 +318,8 @@ async function updateWarehouse(warehouseFk) {
row.lineFk == row.lastPreparedLineFk
? 'black'
: row.balance < 0
? 'negative'
: ''
? 'negative'
: ''
"
dense
style="font-size: 14px"

View File

@ -11,7 +11,6 @@ import { toCurrency } from 'filters/index';
import { useArrayData } from 'composables/useArrayData';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
const { t } = useI18n();
const route = useRoute();
const from = ref();
@ -41,7 +40,7 @@ const itemLastEntries = ref([]);
const columns = computed(() => [
{
label: 'Nv',
label: 'NV',
name: 'ig',
align: 'center',
},
@ -70,6 +69,7 @@ const columns = computed(() => [
field: 'reference',
align: 'center',
format: (_, row) => toCurrency(row.price2) + ' / ' + toCurrency(row.price3),
style: (row) => highlightedRow(row),
},
{
label: t('lastEntries.printedStickers'),
@ -84,6 +84,7 @@ const columns = computed(() => [
field: 'stickers',
align: 'center',
format: (val) => dashIfEmpty(val),
style: (row) => highlightedRow(row),
},
{
label: 'Packing',
@ -102,12 +103,14 @@ const columns = computed(() => [
name: 'stems',
field: 'stems',
align: 'center',
style: (row) => highlightedRow(row),
},
{
label: t('lastEntries.quantity'),
name: 'quantity',
field: 'quantity',
align: 'center',
style: (row) => highlightedRow(row),
},
{
label: t('lastEntries.cost'),
@ -120,12 +123,14 @@ const columns = computed(() => [
name: 'weight',
field: 'weight',
align: 'center',
style: (row) => highlightedRow(row),
},
{
label: t('lastEntries.cube'),
name: 'cube',
field: 'packagingFk',
align: 'center',
style: (row) => highlightedRow(row),
},
{
label: t('lastEntries.supplier'),
@ -208,6 +213,14 @@ onMounted(async () => {
function getBadgeClass(groupingMode, expectedGrouping) {
return groupingMode === expectedGrouping ? 'accent-badge' : 'simple-badge';
}
function highlightedRow(row) {
return row?.isInventorySupplier
? {
'background-color': 'var(--vn-section-hover-color)',
}
: '';
}
</script>
<template>
<VnSubToolbar>
@ -236,7 +249,7 @@ function getBadgeClass(groupingMode, expectedGrouping) {
:no-data-label="t('globals.noResults')"
>
<template #body-cell-ig="{ row }">
<QTd class="text-center">
<QTd class="text-center" :style="highlightedRow(row)">
<QIcon
:name="row.isIgnored ? 'check_box' : 'check_box_outline_blank'"
style="color: var(--vn-label-color)"
@ -245,38 +258,38 @@ function getBadgeClass(groupingMode, expectedGrouping) {
</QTd>
</template>
<template #body-cell-warehouse="{ row }">
<QTd>
<QTd :style="highlightedRow(row)">
<span>{{ row.warehouse }}</span>
</QTd>
</template>
<template #body-cell-date="{ row }">
<QTd class="text-center">
<QTd class="text-center" :style="highlightedRow(row)">
<VnDateBadge :date="row.landed" />
</QTd>
</template>
<template #body-cell-entry="{ row }">
<QTd @click.stop>
<QTd @click.stop :style="highlightedRow(row)">
<div class="full-width flex justify-center">
<EntryDescriptorProxy :id="row.entryFk" class="q-ma-none" dense />
<span class="link">{{ row.entryFk }}</span>
</div>
</QTd>
</template>
<template #body-cell-pvp="{ value }">
<QTd @click.stop class="text-center">
<template #body-cell-pvp="{ row, value }">
<QTd @click.stop class="text-center" :style="highlightedRow(row)">
<span> {{ value }}</span>
<QTooltip> {{ t('lastEntries.grouping') }}/Packing </QTooltip></QTd
>
<QTooltip> {{ t('lastEntries.grouping') }}/Packing </QTooltip>
</QTd>
</template>
<template #body-cell-printedStickers="{ row }">
<QTd @click.stop class="text-center">
<QTd @click.stop class="text-center" :style="highlightedRow(row)">
<span style="color: var(--vn-label-color)">
{{ row.printedStickers }}</span
>
</QTd>
</template>
<template #body-cell-packing="{ row }">
<QTd @click.stop>
<QTd @click.stop :style="highlightedRow(row)">
<QBadge
class="center-content"
:class="getBadgeClass(row.groupingMode, 'packing')"
@ -288,7 +301,7 @@ function getBadgeClass(groupingMode, expectedGrouping) {
</QTd>
</template>
<template #body-cell-grouping="{ row }">
<QTd @click.stop>
<QTd @click.stop :style="highlightedRow(row)">
<QBadge
class="center-content"
:class="getBadgeClass(row.groupingMode, 'grouping')"
@ -300,7 +313,7 @@ function getBadgeClass(groupingMode, expectedGrouping) {
</QTd>
</template>
<template #body-cell-cost="{ row }">
<QTd @click.stop class="text-center">
<QTd @click.stop class="text-center" :style="highlightedRow(row)">
<span>
{{ toCurrency(row.cost, 'EUR', 3) }}
<QTooltip>
@ -319,7 +332,7 @@ function getBadgeClass(groupingMode, expectedGrouping) {
</QTd>
</template>
<template #body-cell-supplier="{ row }">
<QTd @click.stop>
<QTd @click.stop :style="highlightedRow(row)">
<div class="full-width flex justify-left">
<QBadge
:class="
@ -354,7 +367,6 @@ function getBadgeClass(groupingMode, expectedGrouping) {
.th :first-child {
.td {
text-align: center;
background-color: red;
}
}
.accent-badge {

View File

@ -110,10 +110,16 @@ const columns = computed(() => [
attrs: { inWhere: true },
align: 'left',
},
{
label: t('globals.visible'),
name: 'stock',
attrs: { inWhere: true },
align: 'left',
},
]);
const totalLabels = computed(() =>
rows.value.reduce((acc, row) => acc + row.stock / row.packing, 0).toFixed(2)
rows.value.reduce((acc, row) => acc + row.stock / row.packing, 0).toFixed(2),
);
const removeLines = async () => {
@ -157,7 +163,7 @@ watchEffect(selectedRows);
openConfirmationModal(
t('shelvings.removeConfirmTitle'),
t('shelvings.removeConfirmSubtitle'),
removeLines
removeLines,
)
"
>

View File

@ -87,7 +87,7 @@ const insertTag = (rows) => {
tagFk: undefined,
}"
:default-remove="false"
:filter="{
:user-filter="{
fields: ['id', 'itemFk', 'tagFk', 'value', 'priority'],
where: { itemFk: route.params.id },
include: {
@ -119,6 +119,7 @@ const insertTag = (rows) => {
"
:required="true"
:rules="validate('itemTag.tagFk')"
:data-cy="`tag${row?.tag?.name}`"
/>
<VnSelect
v-if="row.tag?.isFree === false"
@ -145,6 +146,7 @@ const insertTag = (rows) => {
:label="t('itemTags.value')"
:is-clearable="false"
@keyup.enter.stop="(data) => itemTagsRef.onSubmit(data)"
:data-cy="`tag${row?.tag?.name}Value`"
/>
<VnInput
:label="t('itemBasicData.relevancy')"
@ -162,6 +164,7 @@ const insertTag = (rows) => {
name="delete"
size="sm"
dense
:data-cy="`deleteTag${row?.tag?.name}`"
>
<QTooltip>
{{ t('itemTags.removeTag') }}
@ -177,6 +180,7 @@ const insertTag = (rows) => {
icon="add"
v-shortcut="'+'"
fab
data-cy="createNewTag"
>
<QTooltip>
{{ t('itemTags.addTag') }}

View File

@ -1,8 +1,6 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSelect from 'components/common/VnSelect.vue';
import ItemsFilterPanel from 'src/components/ItemsFilterPanel.vue';
@ -16,17 +14,9 @@ const props = defineProps({
},
});
const itemTypeWorkersOptions = ref([]);
</script>
<template>
<FetchData
url="TicketRequests/getItemTypeWorker"
limit="30"
auto-load
:filter="{ fields: ['id', 'nickname'], order: 'nickname ASC', limit: 30 }"
@on-fetch="(data) => (itemTypeWorkersOptions = data)"
/>
<ItemsFilterPanel :data-key="props.dataKey" :custom-tags="['tags']">
<template #body="{ params, searchFn }">
<QItem class="q-my-md">
@ -34,14 +24,15 @@ const itemTypeWorkersOptions = ref([]);
<VnSelect
:label="t('params.buyerFk')"
v-model="params.buyerFk"
:options="itemTypeWorkersOptions"
option-value="id"
url="TicketRequests/getItemTypeWorker"
:fields="['id', 'nickname']"
option-label="nickname"
dense
outlined
rounded
use-input
@update:model-value="searchFn()"
sort-by="nickname ASC"
/>
</QItemSection>
</QItem>
@ -50,11 +41,10 @@ const itemTypeWorkersOptions = ref([]);
<VnSelect
url="Warehouses"
auto-load
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
:fields="['id', 'name']"
sort-by="name ASC"
:label="t('params.warehouseFk')"
v-model="params.warehouseFk"
option-label="name"
option-value="id"
dense
outlined
rounded

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