Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 8422-createVehicleDms
gitea/salix-front/pipeline/pr-dev This commit looks good
Details
gitea/salix-front/pipeline/pr-dev This commit looks good
Details
This commit is contained in:
commit
3570556c47
|
@ -0,0 +1 @@
|
|||
node_modules
|
3133
CHANGELOG.md
3133
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env groovy
|
||||
|
||||
def PROTECTED_BRANCH
|
||||
def IS_LATEST
|
||||
|
||||
def BRANCH_ENV = [
|
||||
test: 'test',
|
||||
|
@ -10,19 +11,22 @@ 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}"
|
||||
echo "CHANGE_TARGET: ${env.CHANGE_TARGET}"
|
||||
|
||||
configFileProvider([
|
||||
configFile(fileId: 'salix-front.properties',
|
||||
|
@ -33,7 +37,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 +62,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 +85,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:front: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',
|
||||
|
|
|
@ -23,7 +23,7 @@ quasar dev
|
|||
### Run unit tests
|
||||
|
||||
```bash
|
||||
pnpm run test:unit
|
||||
pnpm run test:front
|
||||
```
|
||||
|
||||
### Run e2e tests
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
version: '3.7'
|
||||
services:
|
||||
main:
|
||||
image: registry.verdnatura.es/salix-frontend:${VERSION:?}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
|
@ -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
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "salix-front",
|
||||
"version": "25.08.0",
|
||||
"version": "25.12.0",
|
||||
"description": "Salix frontend",
|
||||
"productName": "Salix",
|
||||
"author": "Verdnatura",
|
||||
|
@ -14,8 +14,8 @@
|
|||
"test:e2e": "cypress open",
|
||||
"test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run",
|
||||
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
|
||||
"test:unit": "vitest",
|
||||
"test:unit:ci": "vitest run",
|
||||
"test:front": "vitest",
|
||||
"test:front:ci": "vitest run",
|
||||
"commitlint": "commitlint --edit",
|
||||
"prepare": "npx husky install",
|
||||
"addReferenceTag": "node .husky/addReferenceTag.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export default [
|
||||
{
|
||||
path: '/api',
|
||||
rule: { target: 'http://0.0.0.0:3000' },
|
||||
rule: { target: 'http://127.0.0.1:3000' },
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -51,4 +51,5 @@ export default boot(({ app }) => {
|
|||
|
||||
await useCau(response, message);
|
||||
};
|
||||
app.provide('app', app);
|
||||
});
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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')"
|
||||
>
|
||||
|
|
|
@ -64,6 +64,10 @@ const $props = defineProps({
|
|||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
beforeSaveFn: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
goTo: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
@ -176,8 +180,15 @@ async function saveChanges(data) {
|
|||
hasChanges.value = false;
|
||||
return;
|
||||
}
|
||||
const changes = data || getChanges();
|
||||
let changes = data || getChanges();
|
||||
if ($props.beforeSaveFn) {
|
||||
changes = await $props.beforeSaveFn(changes, getChanges);
|
||||
}
|
||||
try {
|
||||
if (changes?.creates?.length === 0 && changes?.updates?.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await axios.post($props.saveUrl || $props.url + '/crud', changes);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
|
@ -229,12 +240,12 @@ async function remove(data) {
|
|||
componentProps: {
|
||||
title: t('globals.confirmDeletion'),
|
||||
message: t('globals.confirmDeletionMessage'),
|
||||
newData,
|
||||
data: { deletes: ids },
|
||||
ids,
|
||||
promise: saveChanges,
|
||||
},
|
||||
})
|
||||
.onOk(async () => {
|
||||
await saveChanges({ deletes: ids });
|
||||
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
|
||||
fetch(newData);
|
||||
});
|
||||
|
@ -374,6 +385,8 @@ watch(formUrl, async () => {
|
|||
@click="onSubmit"
|
||||
:disable="!hasChanges"
|
||||
:title="t('globals.save')"
|
||||
v-shortcut="'s'"
|
||||
shortcut="s"
|
||||
data-cy="crudModelDefaultSaveBtn"
|
||||
/>
|
||||
<slot name="moreAfterActions" />
|
||||
|
|
|
@ -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')"
|
||||
|
|
|
@ -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
|
||||
|
@ -181,6 +181,7 @@ const selectTravel = ({ id }) => {
|
|||
color="primary"
|
||||
:disabled="isLoading"
|
||||
:loading="isLoading"
|
||||
data-cy="save-filter-travel-form"
|
||||
/>
|
||||
</div>
|
||||
<QTable
|
||||
|
@ -191,9 +192,10 @@ const selectTravel = ({ id }) => {
|
|||
:no-data-label="t('Enter a new search')"
|
||||
class="q-mt-lg"
|
||||
@row-click="(_, row) => selectTravel(row)"
|
||||
data-cy="table-filter-travel-form"
|
||||
>
|
||||
<template #body-cell-id="{ row }">
|
||||
<QTd auto-width @click.stop>
|
||||
<QTd auto-width @click.stop data-cy="travelFk-travel-form">
|
||||
<QBtn flat color="blue">{{ row.id }}</QBtn>
|
||||
<TravelDescriptorProxy :id="row.id" />
|
||||
</QTd>
|
||||
|
|
|
@ -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,
|
||||
|
@ -94,6 +96,10 @@ const $props = defineProps({
|
|||
type: [String, Boolean],
|
||||
default: '800px',
|
||||
},
|
||||
onDataSaved: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['onFetch', 'onDataSaved']);
|
||||
const modelValue = computed(
|
||||
|
@ -106,14 +112,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: {
|
||||
|
@ -134,7 +140,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(
|
||||
|
@ -154,7 +161,7 @@ onMounted(async () => {
|
|||
if (!$props.url)
|
||||
watch(
|
||||
() => arrayData.store.data,
|
||||
(val) => updateAndEmit('onFetch', val),
|
||||
(val) => updateAndEmit('onFetch', { val }),
|
||||
);
|
||||
|
||||
watch(
|
||||
|
@ -200,7 +207,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;
|
||||
|
@ -227,7 +234,11 @@ async function save() {
|
|||
|
||||
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 {
|
||||
|
@ -242,7 +253,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;
|
||||
|
@ -264,11 +275,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) {
|
||||
|
@ -278,6 +289,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,
|
||||
|
@ -293,12 +325,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
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<script setup>
|
||||
import { ref, computed } 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: '',
|
||||
|
@ -15,23 +16,41 @@ defineProps({
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showSaveAndContinueBtn: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const attrs = useAttrs();
|
||||
const state = useState();
|
||||
const formModelRef = ref(null);
|
||||
const closeButton = ref(null);
|
||||
const isSaveAndContinue = ref(props.showSaveAndContinueBtn);
|
||||
const isLoading = computed(() => formModelRef.value?.isLoading);
|
||||
const reset = computed(() => formModelRef.value?.reset);
|
||||
|
||||
const onDataSaved = (formData, requestResponse) => {
|
||||
if (closeButton.value) closeButton.value.click();
|
||||
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 onClick = async (saveAndContinue) => {
|
||||
isSaveAndContinue.value = saveAndContinue;
|
||||
await formModelRef.value.save();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
isLoading,
|
||||
onDataSaved,
|
||||
isSaveAndContinue,
|
||||
reset,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -59,15 +78,16 @@ defineExpose({
|
|||
flat
|
||||
:disabled="isLoading"
|
||||
:loading="isLoading"
|
||||
@click="emit('onDataCanceled')"
|
||||
v-close-popup
|
||||
data-cy="FormModelPopup_cancel"
|
||||
v-close-popup
|
||||
z-max
|
||||
@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"
|
||||
|
@ -75,6 +95,18 @@ defineExpose({
|
|||
data-cy="FormModelPopup_save"
|
||||
z-max
|
||||
/>
|
||||
<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>
|
||||
</FormModel>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -26,6 +26,7 @@ const itemComputed = computed(() => {
|
|||
:to="{ name: itemComputed.name }"
|
||||
clickable
|
||||
v-ripple
|
||||
:data-cy="`${itemComputed.name}-menu-item`"
|
||||
>
|
||||
<QItemSection avatar v-if="itemComputed.icon">
|
||||
<QIcon :name="itemComputed.icon" />
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,16 +1,42 @@
|
|||
<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"
|
||||
v-if="row?.reserved"
|
||||
color="primary"
|
||||
name="vn:reserva"
|
||||
size="xs"
|
||||
data-cy="ticketSaleReservedIcon"
|
||||
>
|
||||
<QTooltip>
|
||||
{{ t('ticketSale.reserved') }}
|
||||
</QTooltip>
|
||||
</QIcon>
|
||||
<QIcon
|
||||
v-if="row?.hasRisk"
|
||||
name="vn:risk"
|
||||
:color="row.hasHighRisk ? 'negative' : 'primary'"
|
||||
size="xs"
|
||||
>
|
||||
<QTooltip>
|
||||
{{ $t('salesTicketsTable.risk') }}: {{ row.risk - row.credit }}
|
||||
{{ $t('salesTicketsTable.risk') }}:
|
||||
{{ toCurrency(row.risk - (row.credit ?? 0)) }}
|
||||
</QTooltip>
|
||||
</QIcon>
|
||||
<QIcon
|
||||
|
@ -52,12 +78,7 @@ defineProps({ row: { type: Object, required: true } });
|
|||
>
|
||||
<QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip>
|
||||
</QIcon>
|
||||
<QIcon
|
||||
v-if="!row?.isTaxDataChecked === 0"
|
||||
name="vn:no036"
|
||||
color="primary"
|
||||
size="xs"
|
||||
>
|
||||
<QIcon v-if="row?.isTaxDataChecked" name="vn:no036" color="primary" size="xs">
|
||||
<QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip>
|
||||
</QIcon>
|
||||
<QIcon v-if="row?.isFreezed" name="vn:frozen" color="primary" size="xs">
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<script setup>
|
||||
import { markRaw, computed } from 'vue';
|
||||
import { QIcon, QCheckbox } from 'quasar';
|
||||
import { QIcon, QToggle } from 'quasar';
|
||||
import { dashIfEmpty } from 'src/filters';
|
||||
|
||||
/* basic input */
|
||||
import VnSelect from 'components/common/VnSelect.vue';
|
||||
import VnSelectCache from 'components/common/VnSelectCache.vue';
|
||||
import VnInput from 'components/common/VnInput.vue';
|
||||
|
@ -12,8 +11,11 @@ import VnInputDate from 'components/common/VnInputDate.vue';
|
|||
import VnInputTime from 'components/common/VnInputTime.vue';
|
||||
import VnComponent from 'components/common/VnComponent.vue';
|
||||
import VnUserLink from 'components/ui/VnUserLink.vue';
|
||||
import VnSelectEnum from '../common/VnSelectEnum.vue';
|
||||
import VnCheckbox from '../common/VnCheckbox.vue';
|
||||
|
||||
const model = defineModel(undefined, { required: true });
|
||||
const emit = defineEmits(['blur']);
|
||||
const $props = defineProps({
|
||||
column: {
|
||||
type: Object,
|
||||
|
@ -39,10 +41,18 @@ const $props = defineProps({
|
|||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
eventHandlers: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const defaultSelect = {
|
||||
|
@ -99,7 +109,8 @@ const defaultComponents = {
|
|||
},
|
||||
},
|
||||
checkbox: {
|
||||
component: markRaw(QCheckbox),
|
||||
ref: 'checkbox',
|
||||
component: markRaw(VnCheckbox),
|
||||
attrs: ({ model }) => {
|
||||
const defaultAttrs = {
|
||||
disable: !$props.isEditable,
|
||||
|
@ -115,6 +126,10 @@ const defaultComponents = {
|
|||
},
|
||||
forceAttrs: {
|
||||
label: $props.showLabel && $props.column.label,
|
||||
autofocus: true,
|
||||
},
|
||||
events: {
|
||||
blur: () => emit('blur'),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
|
@ -125,12 +140,19 @@ const defaultComponents = {
|
|||
component: markRaw(VnSelect),
|
||||
...defaultSelect,
|
||||
},
|
||||
selectEnum: {
|
||||
component: markRaw(VnSelectEnum),
|
||||
...defaultSelect,
|
||||
},
|
||||
icon: {
|
||||
component: markRaw(QIcon),
|
||||
},
|
||||
userLink: {
|
||||
component: markRaw(VnUserLink),
|
||||
},
|
||||
toggle: {
|
||||
component: markRaw(QToggle),
|
||||
},
|
||||
};
|
||||
|
||||
const value = computed(() => {
|
||||
|
@ -160,7 +182,28 @@ const col = computed(() => {
|
|||
return newColumn;
|
||||
});
|
||||
|
||||
const components = computed(() => $props.components ?? defaultComponents);
|
||||
const components = computed(() => {
|
||||
const sourceComponents = $props.components ?? defaultComponents;
|
||||
|
||||
return Object.keys(sourceComponents).reduce((acc, key) => {
|
||||
const component = sourceComponents[key];
|
||||
|
||||
if (!component || typeof component !== 'object') {
|
||||
acc[key] = component;
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[key] = {
|
||||
...component,
|
||||
attrs: {
|
||||
...(component.attrs || {}),
|
||||
autofocus: $props.autofocus,
|
||||
},
|
||||
event: { ...component?.event, ...$props?.eventHandlers },
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="row no-wrap">
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
<script setup>
|
||||
import { markRaw, computed } from 'vue';
|
||||
import { QCheckbox } from 'quasar';
|
||||
import { QCheckbox, QToggle } from 'quasar';
|
||||
import { useArrayData } from 'composables/useArrayData';
|
||||
|
||||
/* basic input */
|
||||
import VnSelect from 'components/common/VnSelect.vue';
|
||||
import VnInput from 'components/common/VnInput.vue';
|
||||
import VnInputDate from 'components/common/VnInputDate.vue';
|
||||
import VnInputTime from 'components/common/VnInputTime.vue';
|
||||
import VnTableColumn from 'components/VnTable/VnColumn.vue';
|
||||
import VnColumn from 'components/VnTable/VnColumn.vue';
|
||||
|
||||
const $props = defineProps({
|
||||
column: {
|
||||
|
@ -27,6 +25,10 @@ const $props = defineProps({
|
|||
type: String,
|
||||
default: 'table',
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
defineExpose({ addFilter, props: $props });
|
||||
|
@ -34,7 +36,7 @@ defineExpose({ addFilter, props: $props });
|
|||
const model = defineModel(undefined, { required: true });
|
||||
const arrayData = useArrayData(
|
||||
$props.dataKey,
|
||||
$props.searchUrl ? { searchUrl: $props.searchUrl } : null
|
||||
$props.searchUrl ? { searchUrl: $props.searchUrl } : null,
|
||||
);
|
||||
const columnFilter = computed(() => $props.column?.columnFilter);
|
||||
|
||||
|
@ -46,19 +48,18 @@ const enterEvent = {
|
|||
|
||||
const defaultAttrs = {
|
||||
filled: !$props.showTitle,
|
||||
class: 'q-px-xs q-pb-xs q-pt-none fit',
|
||||
dense: true,
|
||||
};
|
||||
|
||||
const forceAttrs = {
|
||||
label: $props.showTitle ? '' : columnFilter.value?.label ?? $props.column.label,
|
||||
label: $props.showTitle ? '' : (columnFilter.value?.label ?? $props.column.label),
|
||||
};
|
||||
|
||||
const selectComponent = {
|
||||
component: markRaw(VnSelect),
|
||||
event: updateEvent,
|
||||
attrs: {
|
||||
class: 'q-px-sm q-pb-xs q-pt-none fit',
|
||||
class: `q-pt-none fit ${$props.customClass}`,
|
||||
dense: true,
|
||||
filled: !$props.showTitle,
|
||||
},
|
||||
|
@ -90,7 +91,6 @@ const components = {
|
|||
event: updateEvent,
|
||||
attrs: {
|
||||
...defaultAttrs,
|
||||
style: 'min-width: 150px',
|
||||
},
|
||||
forceAttrs,
|
||||
},
|
||||
|
@ -109,14 +109,24 @@ const components = {
|
|||
component: markRaw(QCheckbox),
|
||||
event: updateEvent,
|
||||
attrs: {
|
||||
dense: true,
|
||||
class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs fit',
|
||||
class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit',
|
||||
'toggle-indeterminate': true,
|
||||
size: 'sm',
|
||||
},
|
||||
forceAttrs,
|
||||
},
|
||||
select: selectComponent,
|
||||
rawSelect: selectComponent,
|
||||
toggle: {
|
||||
component: markRaw(QToggle),
|
||||
event: updateEvent,
|
||||
attrs: {
|
||||
class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit',
|
||||
'toggle-indeterminate': true,
|
||||
size: 'sm',
|
||||
},
|
||||
forceAttrs,
|
||||
},
|
||||
};
|
||||
|
||||
async function addFilter(value, name) {
|
||||
|
@ -132,19 +142,8 @@ async function addFilter(value, name) {
|
|||
await arrayData.addFilter({ params: { [field]: value } });
|
||||
}
|
||||
|
||||
function alignRow() {
|
||||
switch ($props.column.align) {
|
||||
case 'left':
|
||||
return 'justify-start items-start';
|
||||
case 'right':
|
||||
return 'justify-end items-end';
|
||||
default:
|
||||
return 'flex-center';
|
||||
}
|
||||
}
|
||||
|
||||
const showFilter = computed(
|
||||
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions'
|
||||
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions',
|
||||
);
|
||||
|
||||
const onTabPressed = async () => {
|
||||
|
@ -152,13 +151,8 @@ const onTabPressed = async () => {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-if="showFilter"
|
||||
class="full-width"
|
||||
:class="alignRow()"
|
||||
style="max-height: 45px; overflow: hidden"
|
||||
>
|
||||
<VnTableColumn
|
||||
<div v-if="showFilter" class="full-width" style="overflow: hidden">
|
||||
<VnColumn
|
||||
:column="$props.column"
|
||||
default="input"
|
||||
v-model="model"
|
||||
|
@ -168,3 +162,8 @@ const onTabPressed = async () => {
|
|||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
label.vn-label-padding > .q-field__inner > .q-field__control {
|
||||
padding: inherit !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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 });
|
||||
|
@ -41,55 +45,78 @@ async function orderBy(name, direction) {
|
|||
break;
|
||||
}
|
||||
if (!direction) return await arrayData.deleteOrder(name);
|
||||
|
||||
await arrayData.addOrder(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"
|
||||
class="items-center no-wrap cursor-pointer title"
|
||||
:style="textAlignToFlex(align)"
|
||||
>
|
||||
<span :title="label">{{ label }}</span>
|
||||
<QChip
|
||||
v-if="name"
|
||||
:label="!vertical ? model?.index : ''"
|
||||
:icon="
|
||||
(model?.index || hover) && !vertical
|
||||
? model?.direction == 'DESC'
|
||||
? 'arrow_downward'
|
||||
: 'arrow_upward'
|
||||
: undefined
|
||||
"
|
||||
:size="vertical ? '' : 'sm'"
|
||||
:class="[
|
||||
model?.index ? 'color-vn-text' : 'bg-transparent',
|
||||
vertical ? 'q-px-none' : '',
|
||||
]"
|
||||
class="no-box-shadow"
|
||||
:clickable="true"
|
||||
style="min-width: 40px"
|
||||
>
|
||||
<div
|
||||
class="column flex-center"
|
||||
v-if="vertical"
|
||||
:style="!model?.index && 'color: #5d5d5d'"
|
||||
<div v-if="name && model?.index">
|
||||
<QChip
|
||||
:label="!vertical ? model?.index : ''"
|
||||
:icon="
|
||||
(model?.index || hover) && !vertical
|
||||
? model?.direction == 'DESC'
|
||||
? 'arrow_downward'
|
||||
: 'arrow_upward'
|
||||
: undefined
|
||||
"
|
||||
:size="vertical ? '' : 'sm'"
|
||||
:class="[
|
||||
model?.index ? 'color-vn-text' : 'bg-transparent',
|
||||
vertical ? 'q-px-none' : '',
|
||||
]"
|
||||
class="no-box-shadow"
|
||||
:clickable="true"
|
||||
style="min-width: 40px; max-height: 30px"
|
||||
>
|
||||
{{ model?.index }}
|
||||
<QIcon
|
||||
:name="
|
||||
model?.index
|
||||
? model?.direction == 'DESC'
|
||||
? 'arrow_downward'
|
||||
: 'arrow_upward'
|
||||
: 'swap_vert'
|
||||
"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
</QChip>
|
||||
<div
|
||||
class="column flex-center"
|
||||
v-if="vertical"
|
||||
:style="!model?.index && 'color: #5d5d5d'"
|
||||
>
|
||||
{{ model?.index }}
|
||||
<QIcon
|
||||
:name="
|
||||
model?.index
|
||||
? model?.direction == 'DESC'
|
||||
? 'arrow_downward'
|
||||
: 'arrow_upward'
|
||||
: 'swap_vert'
|
||||
"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
</QChip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
color: var(--vn-label-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,22 +1,39 @@
|
|||
<script setup>
|
||||
import { ref, onBeforeMount, onMounted, computed, watch, useAttrs } from 'vue';
|
||||
import {
|
||||
ref,
|
||||
onBeforeMount,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
computed,
|
||||
watch,
|
||||
h,
|
||||
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, toDate } from 'src/filters';
|
||||
|
||||
import CrudModel from 'src/components/CrudModel.vue';
|
||||
import FormModelPopup from 'components/FormModelPopup.vue';
|
||||
|
||||
import VnTableColumn from 'components/VnTable/VnColumn.vue';
|
||||
import VnColumn from 'components/VnTable/VnColumn.vue';
|
||||
import VnFilter from 'components/VnTable/VnFilter.vue';
|
||||
import VnTableChip from 'components/VnTable/VnChip.vue';
|
||||
import VnVisibleColumn from 'src/components/VnTable/VnVisibleColumn.vue';
|
||||
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({
|
||||
columns: {
|
||||
type: Array,
|
||||
|
@ -34,10 +51,6 @@ const $props = defineProps({
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rightSearchIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rowClick: {
|
||||
type: [Function, Boolean],
|
||||
default: null,
|
||||
|
@ -114,7 +127,23 @@ const $props = defineProps({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withFilters: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
createComplement: {
|
||||
type: Object,
|
||||
},
|
||||
dataCy: {
|
||||
type: String,
|
||||
default: 'vn-table',
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const stateStore = useStateStore();
|
||||
const route = useRoute();
|
||||
|
@ -132,10 +161,17 @@ const showForm = ref(false);
|
|||
const splittedColumns = ref({ columns: [] });
|
||||
const columnsVisibilitySkipped = ref();
|
||||
const createForm = ref();
|
||||
const createRef = ref(null);
|
||||
const tableRef = ref();
|
||||
const params = ref(useFilterParams($attrs['data-key']).params);
|
||||
const orders = ref(useFilterParams($attrs['data-key']).orders);
|
||||
const app = inject('app');
|
||||
|
||||
const editingRow = ref(null);
|
||||
const editingField = ref(null);
|
||||
const isTableMode = computed(() => mode.value == TABLE_MODE);
|
||||
const selectRegex = /select/;
|
||||
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
|
||||
const tableModes = [
|
||||
{
|
||||
icon: 'view_column',
|
||||
|
@ -156,7 +192,8 @@ onBeforeMount(() => {
|
|||
hasParams.value = urlParams && Object.keys(urlParams).length !== 0;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
if ($props.isEditable) document.addEventListener('click', clickHandler);
|
||||
mode.value =
|
||||
quasar.platform.is.mobile && !$props.disableOption?.card
|
||||
? CARD_MODE
|
||||
|
@ -178,14 +215,25 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
|
||||
onUnmounted(async () => {
|
||||
if ($props.isEditable) document.removeEventListener('click', clickHandler);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => $props.columns,
|
||||
(value) => splitColumns(value),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const isTableMode = computed(() => mode.value == TABLE_MODE);
|
||||
const showRightIcon = computed(() => $props.rightSearch || $props.rightSearchIcon);
|
||||
defineExpose({
|
||||
create: createForm,
|
||||
reload,
|
||||
redirect: redirectFn,
|
||||
selected,
|
||||
CrudModelRef,
|
||||
params,
|
||||
tableRef,
|
||||
});
|
||||
|
||||
function splitColumns(columns) {
|
||||
splittedColumns.value = {
|
||||
|
@ -209,7 +257,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,
|
||||
|
@ -225,22 +275,30 @@ 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);
|
||||
return () => {};
|
||||
});
|
||||
|
||||
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 () => {};
|
||||
});
|
||||
|
||||
function redirectFn(id) {
|
||||
router.push({ path: `/${$props.redirect}/${id}` });
|
||||
}
|
||||
|
@ -262,21 +320,6 @@ function columnName(col) {
|
|||
return name;
|
||||
}
|
||||
|
||||
function getColAlign(col) {
|
||||
return 'text-' + (col.align ?? 'left');
|
||||
}
|
||||
|
||||
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
|
||||
defineExpose({
|
||||
create: createForm,
|
||||
reload,
|
||||
redirect: redirectFn,
|
||||
selected,
|
||||
CrudModelRef,
|
||||
params,
|
||||
tableRef,
|
||||
});
|
||||
|
||||
function handleOnDataSaved(_) {
|
||||
if (_.onDataSaved) _.onDataSaved({ CrudModelRef: CrudModelRef.value });
|
||||
else $props.create.onDataSaved(_);
|
||||
|
@ -295,8 +338,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);
|
||||
|
@ -304,16 +353,283 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isEditableColumn(column) {
|
||||
const isEditableCol = column?.isEditable ?? true;
|
||||
const isVisible = column?.visible ?? true;
|
||||
const hasComponent = column?.component;
|
||||
|
||||
return $props.isEditable && isVisible && hasComponent && isEditableCol;
|
||||
}
|
||||
|
||||
function hasEditableFormat(column) {
|
||||
if (isEditableColumn(column)) return 'editable-text';
|
||||
}
|
||||
|
||||
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 || isQSelectDropDown) return;
|
||||
|
||||
if (clickedElement === null) {
|
||||
await destroyInput(editingRow.value, editingField.value);
|
||||
return;
|
||||
}
|
||||
const rowIndex = clickedElement.getAttribute('data-row-index');
|
||||
const colField = clickedElement.getAttribute('data-col-field');
|
||||
const column = $props.columns.find((col) => col.name === colField);
|
||||
|
||||
if (editingRow.value !== null && editingField.value !== null) {
|
||||
if (editingRow.value == rowIndex && editingField.value == colField) return;
|
||||
|
||||
await destroyInput(editingRow.value, editingField.value);
|
||||
}
|
||||
|
||||
if (isEditableColumn(column)) {
|
||||
await renderInput(Number(rowIndex), colField, clickedElement);
|
||||
}
|
||||
};
|
||||
|
||||
async function handleTabKey(event, rowIndex, colField) {
|
||||
if (editingRow.value == rowIndex && editingField.value == colField)
|
||||
await destroyInput(editingRow.value, editingField.value);
|
||||
|
||||
const direction = event.shiftKey ? -1 : 1;
|
||||
const { nextRowIndex, nextColumnName } = await handleTabNavigation(
|
||||
rowIndex,
|
||||
colField,
|
||||
direction,
|
||||
);
|
||||
|
||||
if (nextRowIndex < 0 || nextRowIndex >= arrayData.store.data.length) return;
|
||||
|
||||
event.preventDefault();
|
||||
await renderInput(nextRowIndex, nextColumnName, null);
|
||||
}
|
||||
|
||||
async function renderInput(rowId, field, clickedElement) {
|
||||
editingField.value = field;
|
||||
editingRow.value = rowId;
|
||||
|
||||
const originalColumn = $props.columns.find((col) => col.name === field);
|
||||
const column = { ...originalColumn, ...{ label: '' } };
|
||||
const row = CrudModelRef.value.formData[rowId];
|
||||
const oldValue = CrudModelRef.value.formData[rowId][column?.name];
|
||||
|
||||
if (!clickedElement)
|
||||
clickedElement = document.querySelector(
|
||||
`[data-row-index="${rowId}"][data-col-field="${field}"]`,
|
||||
);
|
||||
|
||||
Array.from(clickedElement.childNodes).forEach((child) => {
|
||||
child.style.visibility = 'hidden';
|
||||
child.style.position = 'relative';
|
||||
});
|
||||
|
||||
const isSelect = selectRegex.test(column?.component);
|
||||
if (isSelect) column.attrs = { ...column.attrs, 'emit-value': false };
|
||||
|
||||
const node = h(VnColumn, {
|
||||
row: row,
|
||||
class: 'temp-input',
|
||||
column: column,
|
||||
modelValue: row[column.name],
|
||||
componentProp: 'columnField',
|
||||
autofocus: true,
|
||||
focusOnMount: true,
|
||||
eventHandlers: {
|
||||
'update:modelValue': async (value) => {
|
||||
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')
|
||||
await destroyInput(rowId, field, clickedElement);
|
||||
},
|
||||
keydown: async (event) => {
|
||||
switch (event.key) {
|
||||
case 'Tab':
|
||||
await handleTabKey(event, rowId, field);
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case 'Escape':
|
||||
await destroyInput(rowId, field, clickedElement);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
click: (event) => {
|
||||
column?.cellEvent?.['click']?.(event, row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
node.appContext = app._context;
|
||||
render(node, clickedElement);
|
||||
|
||||
if (['toggle'].includes(column?.component))
|
||||
node.el?.querySelector('span > div').focus();
|
||||
|
||||
if (['checkbox', undefined].includes(column?.component))
|
||||
node.el?.querySelector('span > div > div').focus();
|
||||
}
|
||||
|
||||
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';
|
||||
child.style.position = '';
|
||||
});
|
||||
}
|
||||
if (editingRow.value !== rowIndex || editingField.value !== field) return;
|
||||
editingRow.value = null;
|
||||
editingField.value = null;
|
||||
}
|
||||
|
||||
async function handleTabNavigation(rowIndex, colName, direction) {
|
||||
const columns = $props.columns;
|
||||
const totalColumns = columns.length;
|
||||
let currentColumnIndex = columns.findIndex((col) => col.name === colName);
|
||||
|
||||
let iterations = 0;
|
||||
let newColumnIndex = currentColumnIndex;
|
||||
|
||||
do {
|
||||
iterations++;
|
||||
newColumnIndex = (newColumnIndex + direction + totalColumns) % totalColumns;
|
||||
|
||||
if (isEditableColumn(columns[newColumnIndex])) break;
|
||||
} while (iterations < totalColumns);
|
||||
|
||||
if (iterations >= totalColumns + 1) return;
|
||||
|
||||
if (direction === 1 && newColumnIndex <= currentColumnIndex) {
|
||||
rowIndex++;
|
||||
} else if (direction === -1 && newColumnIndex >= currentColumnIndex) {
|
||||
rowIndex--;
|
||||
}
|
||||
return { nextRowIndex: rowIndex, nextColumnName: columns[newColumnIndex].name };
|
||||
}
|
||||
|
||||
function getCheckboxIcon(value) {
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
return value ? 'check' : 'close';
|
||||
case 'number':
|
||||
return value === 0 ? 'close' : 'check';
|
||||
case 'undefined':
|
||||
return 'indeterminate_check_box';
|
||||
default:
|
||||
return 'indeterminate_check_box';
|
||||
}
|
||||
}
|
||||
|
||||
function getToggleIcon(value) {
|
||||
if (value === null) return 'help_outline';
|
||||
return value ? 'toggle_on' : 'toggle_off';
|
||||
}
|
||||
|
||||
function formatColumnValue(col, row, dashIfEmpty) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
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"
|
||||
show-if-above
|
||||
>
|
||||
<QScrollArea class="fit">
|
||||
<RightMenu v-if="$props.rightSearch" :overlay="overlay">
|
||||
<template #right-panel>
|
||||
<VnTableFilter
|
||||
:data-key="$attrs['data-key']"
|
||||
:columns="columns"
|
||||
|
@ -327,16 +643,17 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
|
||||
</template>
|
||||
</VnTableFilter>
|
||||
</QScrollArea>
|
||||
</QDrawer>
|
||||
</template>
|
||||
</RightMenu>
|
||||
<CrudModel
|
||||
v-bind="$attrs"
|
||||
:class="$attrs['class'] ?? 'q-px-md'"
|
||||
:limit="$attrs['limit'] ?? 20"
|
||||
:limit="$attrs['limit'] ?? 100"
|
||||
ref="CrudModelRef"
|
||||
@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']"
|
||||
|
@ -348,8 +665,12 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
<QTable
|
||||
ref="tableRef"
|
||||
v-bind="table"
|
||||
class="vnTable"
|
||||
:class="{ 'last-row-sticky': $props.footer }"
|
||||
:class="[
|
||||
'vnTable',
|
||||
table ? 'selection-cell' : '',
|
||||
$props.footer ? 'last-row-sticky' : '',
|
||||
]"
|
||||
wrap-cells
|
||||
:columns="splittedColumns.columns"
|
||||
:rows="rows"
|
||||
v-model:selected="selected"
|
||||
|
@ -360,12 +681,14 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
: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"
|
||||
:data-cy="$props.dataCy ?? 'vnTable'"
|
||||
>
|
||||
<template #top-left v-if="!$props.withoutHeader">
|
||||
<slot name="top-left"></slot>
|
||||
<slot name="top-left"> </slot>
|
||||
</template>
|
||||
<template #top-right v-if="!$props.withoutHeader">
|
||||
<slot name="top-right"></slot>
|
||||
|
@ -376,49 +699,50 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
: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"
|
||||
:style="col.headerStyle"
|
||||
:class="col.headerClass"
|
||||
v-bind:class="col.headerClass"
|
||||
class="body-cell"
|
||||
:style="col?.width ? `max-width: ${col?.width}` : ''"
|
||||
>
|
||||
<div
|
||||
class="column ellipsis"
|
||||
:class="`text-${col?.align ?? 'left'}`"
|
||||
:style="$props.columnSearch ? 'height: 75px' : ''"
|
||||
class="no-padding"
|
||||
:style="[
|
||||
withFilters && $props.columnSearch ? 'height: 75px' : '',
|
||||
]"
|
||||
>
|
||||
<div class="row items-center no-wrap" style="height: 30px">
|
||||
<div style="height: 30px">
|
||||
<QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip>
|
||||
<VnTableOrder
|
||||
v-model="orders[col.orderBy ?? col.name]"
|
||||
:name="col.orderBy ?? col.name"
|
||||
:label="col?.label"
|
||||
:label="col?.labelAbbreviation ?? col?.label"
|
||||
:data-key="$attrs['data-key']"
|
||||
:search-url="searchUrl"
|
||||
:align="getColAlign(col)"
|
||||
/>
|
||||
</div>
|
||||
<VnFilter
|
||||
v-if="$props.columnSearch"
|
||||
v-if="
|
||||
$props.columnSearch &&
|
||||
col.columnSearch !== false &&
|
||||
withFilters
|
||||
"
|
||||
:column="col"
|
||||
:show-title="true"
|
||||
:data-key="$attrs['data-key']"
|
||||
v-model="params[columnName(col)]"
|
||||
:search-url="searchUrl"
|
||||
class="full-width"
|
||||
customClass="header-filter"
|
||||
/>
|
||||
</div>
|
||||
</QTh>
|
||||
|
@ -436,32 +760,67 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
</QTd>
|
||||
</template>
|
||||
<template #body-cell="{ col, row, rowIndex }">
|
||||
<!-- Columns -->
|
||||
<QTd
|
||||
auto-width
|
||||
class="no-margin"
|
||||
:class="[getColAlign(col), col.columnClass]"
|
||||
:style="col.style"
|
||||
class="no-margin q-px-xs"
|
||||
v-if="col.visible ?? true"
|
||||
@click.ctrl="
|
||||
($event) =>
|
||||
rowCtrlClickFunction && rowCtrlClickFunction($event, row)
|
||||
"
|
||||
:style="{
|
||||
'max-width': col?.width ?? false,
|
||||
position: 'relative',
|
||||
}"
|
||||
:class="[
|
||||
col.columnClass,
|
||||
'body-cell no-margin no-padding',
|
||||
getColAlign(col),
|
||||
]"
|
||||
:data-row-index="rowIndex"
|
||||
:data-col-field="col?.name"
|
||||
>
|
||||
<slot
|
||||
:name="`column-${col.name}`"
|
||||
:col="col"
|
||||
:row="row"
|
||||
:row-index="rowIndex"
|
||||
<div
|
||||
class="no-padding no-margin peter"
|
||||
style="
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
"
|
||||
>
|
||||
<VnTableColumn
|
||||
:column="col"
|
||||
<slot
|
||||
:name="`column-${col.name}`"
|
||||
:col="col"
|
||||
:row="row"
|
||||
:is-editable="col.isEditable ?? isEditable"
|
||||
v-model="row[col.name]"
|
||||
component-prop="columnField"
|
||||
/>
|
||||
</slot>
|
||||
:row-index="rowIndex"
|
||||
>
|
||||
<QIcon
|
||||
v-if="col?.component === 'toggle'"
|
||||
:name="
|
||||
col?.getIcon
|
||||
? col.getIcon(row[col?.name])
|
||||
: getToggleIcon(row[col?.name])
|
||||
"
|
||||
style="color: var(--vn-text-color)"
|
||||
:class="hasEditableFormat(col)"
|
||||
size="14px"
|
||||
/>
|
||||
<QIcon
|
||||
v-else-if="col?.component === 'checkbox'"
|
||||
:name="getCheckboxIcon(row[col?.name])"
|
||||
style="color: var(--vn-text-color)"
|
||||
:class="hasEditableFormat(col)"
|
||||
size="14px"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
:class="hasEditableFormat(col)"
|
||||
:style="
|
||||
typeof col?.style == 'function'
|
||||
? col.style(row)
|
||||
: col?.style
|
||||
"
|
||||
style="bottom: 0"
|
||||
>
|
||||
{{ formatColumnValue(col, row, dashIfEmpty) }}
|
||||
</span>
|
||||
</slot>
|
||||
</div>
|
||||
</QTd>
|
||||
</template>
|
||||
<template #body-cell-tableActions="{ col, row }">
|
||||
|
@ -482,7 +841,7 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
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)
|
||||
|
@ -490,23 +849,19 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
: '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
|
||||
|
@ -543,7 +898,7 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
</QCardSection>
|
||||
<!-- Fields -->
|
||||
<QCardSection
|
||||
class="q-pl-sm q-pr-lg q-py-xs"
|
||||
class="q-pl-sm q-py-xs"
|
||||
:class="$props.cardClass"
|
||||
>
|
||||
<div
|
||||
|
@ -564,7 +919,7 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
:row="row"
|
||||
:row-index="index"
|
||||
>
|
||||
<VnTableColumn
|
||||
<VnColumn
|
||||
:column="col"
|
||||
:row="row"
|
||||
:is-editable="false"
|
||||
|
@ -590,13 +945,14 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
: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>
|
||||
|
@ -604,14 +960,17 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
</component>
|
||||
</template>
|
||||
<template #bottom-row="{ cols }" v-if="$props.footer">
|
||||
<QTr v-if="rows.length" style="height: 30px">
|
||||
<QTr v-if="rows.length" style="height: 45px">
|
||||
<QTh v-if="table.selection" />
|
||||
<QTh
|
||||
v-for="col of cols.filter((cols) => cols.visible ?? true)"
|
||||
:key="col?.id"
|
||||
class="text-center"
|
||||
:class="getColAlign(col)"
|
||||
>
|
||||
<slot :name="`column-footer-${col.name}`" />
|
||||
<slot
|
||||
:name="`column-footer-${col.name}`"
|
||||
:isEditableColumn="isEditableColumn(col)"
|
||||
/>
|
||||
</QTh>
|
||||
</QTr>
|
||||
</template>
|
||||
|
@ -655,32 +1014,51 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
|
|||
{{ createForm?.title }}
|
||||
</QTooltip>
|
||||
</QPageSticky>
|
||||
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
|
||||
<QDialog
|
||||
v-model="showForm"
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
:full-width="createComplement?.isFullWidth ?? false"
|
||||
data-cy="vn-table-create-dialog"
|
||||
>
|
||||
<FormModelPopup
|
||||
ref="createRef"
|
||||
v-bind="createForm"
|
||||
:model="$attrs['data-key'] + 'Create'"
|
||||
@on-data-saved="(_, res) => createForm.onDataSaved(res)"
|
||||
>
|
||||
<template #form-inputs="{ data }">
|
||||
<div class="grid-create">
|
||||
<slot
|
||||
v-for="column of splittedColumns.create"
|
||||
:key="column.name"
|
||||
:name="`column-create-${column.name}`"
|
||||
:data="data"
|
||||
:column-name="column.name"
|
||||
:label="column.label"
|
||||
<div :style="createComplement?.containerStyle">
|
||||
<div
|
||||
:style="createComplement?.previousStyle"
|
||||
v-if="!quasar.screen.xs"
|
||||
>
|
||||
<VnTableColumn
|
||||
:column="column"
|
||||
:row="{}"
|
||||
default="input"
|
||||
v-model="data[column.name]"
|
||||
:show-label="true"
|
||||
component-prop="columnCreate"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="more-create-dialog" :data="data" />
|
||||
<slot name="previous-create-dialog" :data="data" />
|
||||
</div>
|
||||
<div class="grid-create" :style="createComplement?.columnGridStyle">
|
||||
<slot
|
||||
v-for="column of splittedColumns.create"
|
||||
:key="column.name"
|
||||
:name="`column-create-${column.name}`"
|
||||
:data="data"
|
||||
:column-name="column.name"
|
||||
:label="column.label"
|
||||
>
|
||||
<VnColumn
|
||||
:column="{
|
||||
...column,
|
||||
...{ disable: column?.createDisable ?? false },
|
||||
}"
|
||||
:row="{}"
|
||||
default="input"
|
||||
v-model="data[column.name]"
|
||||
:show-label="true"
|
||||
component-prop="columnCreate"
|
||||
:data-cy="`${column.name}-create-popup`"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="more-create-dialog" :data="data" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FormModelPopup>
|
||||
|
@ -698,6 +1076,42 @@ es:
|
|||
</i18n>
|
||||
|
||||
<style lang="scss">
|
||||
.selection-cell {
|
||||
table td:first-child {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
.side-padding {
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
.editable-text:hover {
|
||||
border-bottom: 1px dashed var(--q-primary);
|
||||
@extend .side-padding;
|
||||
}
|
||||
.editable-text {
|
||||
border-bottom: 1px dashed var(--vn-label-color);
|
||||
@extend .side-padding;
|
||||
}
|
||||
.cell-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding-top: 0px !important;
|
||||
}
|
||||
.q-field--labeled .q-field__native,
|
||||
.q-field--labeled .q-field__prefix,
|
||||
.q-field--labeled .q-field__suffix {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.body-cell {
|
||||
padding-left: 4px !important;
|
||||
padding-right: 4px !important;
|
||||
position: relative;
|
||||
}
|
||||
.bg-chip-secondary {
|
||||
background-color: var(--vn-page-color);
|
||||
color: var(--vn-text-color);
|
||||
|
@ -714,8 +1128,8 @@ es:
|
|||
|
||||
.grid-three {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, max-content));
|
||||
max-width: 100%;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, max-content));
|
||||
width: 100%;
|
||||
grid-gap: 20px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
@ -723,7 +1137,6 @@ es:
|
|||
.grid-create {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
|
||||
max-width: 100%;
|
||||
grid-gap: 20px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
@ -739,7 +1152,9 @@ es:
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.q-table tbody tr td {
|
||||
position: relative;
|
||||
}
|
||||
.q-table {
|
||||
th {
|
||||
padding: 0;
|
||||
|
@ -788,6 +1203,7 @@ es:
|
|||
.vn-label-value {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
color: var(--vn-text-color);
|
||||
.value {
|
||||
overflow: hidden;
|
||||
|
@ -839,4 +1255,15 @@ es:
|
|||
.q-table__middle.q-virtual-scroll.q-virtual-scroll--vertical.scroll {
|
||||
background-color: var(--vn-section-color);
|
||||
}
|
||||
.temp-input {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
label.header-filter > .q-field__inner > .q-field__control {
|
||||
padding: inherit;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,25 +29,29 @@ function columnName(col) {
|
|||
<VnFilterPanel v-bind="$attrs" :search-button="true" :disable-submit-event="true">
|
||||
<template #body="{ params, orders, searchFn }">
|
||||
<div
|
||||
class="row no-wrap flex-center"
|
||||
class="container"
|
||||
v-for="col of columns.filter((c) => c.columnFilter ?? true)"
|
||||
:key="col.id"
|
||||
>
|
||||
<VnFilter
|
||||
ref="tableFilterRef"
|
||||
:column="col"
|
||||
:data-key="$attrs['data-key']"
|
||||
v-model="params[columnName(col)]"
|
||||
:search-url="searchUrl"
|
||||
/>
|
||||
<VnTableOrder
|
||||
v-if="col?.columnFilter !== false && col?.name !== 'tableActions'"
|
||||
v-model="orders[col.orderBy ?? col.name]"
|
||||
:name="col.orderBy ?? col.name"
|
||||
:data-key="$attrs['data-key']"
|
||||
:search-url="searchUrl"
|
||||
:vertical="true"
|
||||
/>
|
||||
<div class="filter">
|
||||
<VnFilter
|
||||
ref="tableFilterRef"
|
||||
:column="col"
|
||||
:data-key="$attrs['data-key']"
|
||||
v-model="params[columnName(col)]"
|
||||
:search-url="searchUrl"
|
||||
/>
|
||||
</div>
|
||||
<div class="order">
|
||||
<VnTableOrder
|
||||
v-if="col?.columnFilter !== false && col?.name !== 'tableActions'"
|
||||
v-model="orders[col.orderBy ?? col.name]"
|
||||
:name="col.orderBy ?? col.name"
|
||||
:data-key="$attrs['data-key']"
|
||||
:search-url="searchUrl"
|
||||
:vertical="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<slot
|
||||
name="moreFilterPanel"
|
||||
|
@ -68,3 +72,21 @@ function columnName(col) {
|
|||
</template>
|
||||
</VnFilterPanel>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 45px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter {
|
||||
width: 70%;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
.order {
|
||||
width: 10%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -32,16 +32,21 @@ const areAllChecksMarked = computed(() => {
|
|||
|
||||
function setUserConfigViewData(data, isLocal) {
|
||||
if (!data) return;
|
||||
// Importante: El name de las columnas de la tabla debe conincidir con el name de las variables que devuelve la view config
|
||||
if (!isLocal) localColumns.value = [];
|
||||
// Array to Object
|
||||
|
||||
const skippeds = $props.skip.reduce((a, v) => ({ ...a, [v]: v }), {});
|
||||
|
||||
for (let column of columns.value) {
|
||||
const { label, name } = column;
|
||||
const { label, name, labelAbbreviation } = column;
|
||||
if (skippeds[name]) continue;
|
||||
column.visible = data[name] ?? true;
|
||||
if (!isLocal) localColumns.value.push({ name, label, visible: column.visible });
|
||||
if (!isLocal)
|
||||
localColumns.value.push({
|
||||
name,
|
||||
label,
|
||||
labelAbbreviation,
|
||||
visible: column.visible,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,7 +157,11 @@ onMounted(async () => {
|
|||
<QCheckbox
|
||||
v-for="col in localColumns"
|
||||
:key="col.name"
|
||||
:label="col.label ?? col.name"
|
||||
:label="
|
||||
col?.labelAbbreviation
|
||||
? col.labelAbbreviation + ` (${col.label ?? col.name})`
|
||||
: (col.label ?? col.name)
|
||||
"
|
||||
v-model="col.visible"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -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 }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,8 +30,8 @@ describe('CrudModel', () => {
|
|||
saveFn: '',
|
||||
},
|
||||
});
|
||||
wrapper=wrapper.wrapper;
|
||||
vm=wrapper.vm;
|
||||
wrapper = wrapper.wrapper;
|
||||
vm = wrapper.vm;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -143,14 +143,14 @@ describe('CrudModel', () => {
|
|||
});
|
||||
|
||||
it('should return true if object is empty', async () => {
|
||||
dummyObj ={};
|
||||
result = vm.isEmpty(dummyObj);
|
||||
dummyObj = {};
|
||||
result = vm.isEmpty(dummyObj);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if object is not empty', async () => {
|
||||
dummyObj = {a:1, b:2, c:3};
|
||||
dummyObj = { a: 1, b: 2, c: 3 };
|
||||
result = vm.isEmpty(dummyObj);
|
||||
|
||||
expect(result).toBe(false);
|
||||
|
@ -158,29 +158,31 @@ describe('CrudModel', () => {
|
|||
|
||||
it('should return true if array is empty', async () => {
|
||||
dummyArray = [];
|
||||
result = vm.isEmpty(dummyArray);
|
||||
result = vm.isEmpty(dummyArray);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should return false if array is not empty', async () => {
|
||||
dummyArray = [1,2,3];
|
||||
dummyArray = [1, 2, 3];
|
||||
result = vm.isEmpty(dummyArray);
|
||||
|
||||
expect(result).toBe(false);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetData()', () => {
|
||||
it('should add $index to elements in data[] and sets originalData and formData with data', async () => {
|
||||
data = [{
|
||||
name: 'Tony',
|
||||
lastName: 'Stark',
|
||||
age: 42,
|
||||
}];
|
||||
data = [
|
||||
{
|
||||
name: 'Tony',
|
||||
lastName: 'Stark',
|
||||
age: 42,
|
||||
},
|
||||
];
|
||||
|
||||
vm.resetData(data);
|
||||
|
||||
|
||||
expect(vm.originalData).toEqual(data);
|
||||
expect(vm.originalData[0].$index).toEqual(0);
|
||||
expect(vm.formData).toEqual(data);
|
||||
|
@ -200,7 +202,7 @@ describe('CrudModel', () => {
|
|||
lastName: 'Stark',
|
||||
age: 42,
|
||||
};
|
||||
|
||||
|
||||
vm.resetData(data);
|
||||
|
||||
expect(vm.originalData).toEqual(data);
|
||||
|
@ -210,17 +212,19 @@ describe('CrudModel', () => {
|
|||
});
|
||||
|
||||
describe('saveChanges()', () => {
|
||||
data = [{
|
||||
name: 'Tony',
|
||||
lastName: 'Stark',
|
||||
age: 42,
|
||||
}];
|
||||
data = [
|
||||
{
|
||||
name: 'Tony',
|
||||
lastName: 'Stark',
|
||||
age: 42,
|
||||
},
|
||||
];
|
||||
|
||||
it('should call saveFn if exists', async () => {
|
||||
await wrapper.setProps({ saveFn: vi.fn() });
|
||||
|
||||
vm.saveChanges(data);
|
||||
|
||||
|
||||
expect(vm.saveFn).toHaveBeenCalledOnce();
|
||||
expect(vm.isLoading).toBe(false);
|
||||
expect(vm.hasChanges).toBe(false);
|
||||
|
@ -229,13 +233,15 @@ describe('CrudModel', () => {
|
|||
});
|
||||
|
||||
it("should use default url if there's not saveFn", async () => {
|
||||
const postMock =vi.spyOn(axios, 'post');
|
||||
|
||||
vm.formData = [{
|
||||
name: 'Bruce',
|
||||
lastName: 'Wayne',
|
||||
age: 45,
|
||||
}]
|
||||
const postMock = vi.spyOn(axios, 'post');
|
||||
|
||||
vm.formData = [
|
||||
{
|
||||
name: 'Bruce',
|
||||
lastName: 'Wayne',
|
||||
age: 45,
|
||||
},
|
||||
];
|
||||
|
||||
await vm.saveChanges(data);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,61 +1,65 @@
|
|||
import { vi, describe, expect, it, beforeEach, beforeAll, afterEach } from 'vitest';
|
||||
import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest';
|
||||
import { createWrapper } from 'app/test/vitest/helper';
|
||||
import UserPanel from 'src/components/UserPanel.vue';
|
||||
import axios from 'axios';
|
||||
import { useState } from 'src/composables/useState';
|
||||
|
||||
vi.mock('src/utils/quasarLang', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('UserPanel', () => {
|
||||
let wrapper;
|
||||
let vm;
|
||||
let state;
|
||||
let wrapper;
|
||||
let vm;
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper(UserPanel, {});
|
||||
state = useState();
|
||||
state.setUser({
|
||||
id: 115,
|
||||
name: 'itmanagement',
|
||||
nickname: 'itManagementNick',
|
||||
lang: 'en',
|
||||
darkMode: false,
|
||||
companyFk: 442,
|
||||
warehouseFk: 1,
|
||||
});
|
||||
wrapper = wrapper.wrapper;
|
||||
vm = wrapper.vm;
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper(UserPanel, {});
|
||||
state = useState();
|
||||
state.setUser({
|
||||
id: 115,
|
||||
name: 'itmanagement',
|
||||
nickname: 'itManagementNick',
|
||||
lang: 'en',
|
||||
darkMode: false,
|
||||
companyFk: 442,
|
||||
warehouseFk: 1,
|
||||
});
|
||||
wrapper = wrapper.wrapper;
|
||||
vm = wrapper.vm;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch warehouses data on mounted', async () => {
|
||||
const fetchData = wrapper.findComponent({ name: 'FetchData' });
|
||||
expect(fetchData.props('url')).toBe('Warehouses');
|
||||
expect(fetchData.props('autoLoad')).toBe(true);
|
||||
});
|
||||
it('should fetch warehouses data on mounted', async () => {
|
||||
const fetchData = wrapper.findComponent({ name: 'FetchData' });
|
||||
expect(fetchData.props('url')).toBe('Warehouses');
|
||||
expect(fetchData.props('autoLoad')).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle dark mode correctly and update preferences', async () => {
|
||||
await vm.saveDarkMode(true);
|
||||
expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true });
|
||||
expect(vm.user.darkMode).toBe(true);
|
||||
vm.updatePreferences();
|
||||
expect(vm.darkMode).toBe(true);
|
||||
});
|
||||
it('should toggle dark mode correctly and update preferences', async () => {
|
||||
await vm.saveDarkMode(true);
|
||||
expect(axios.patch).toHaveBeenCalledWith('/UserConfigs/115', { darkMode: true });
|
||||
expect(vm.user.darkMode).toBe(true);
|
||||
await vm.updatePreferences();
|
||||
expect(vm.darkMode).toBe(true);
|
||||
});
|
||||
|
||||
it('should change user language and update preferences', async () => {
|
||||
const userLanguage = 'es';
|
||||
await vm.saveLanguage(userLanguage);
|
||||
expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage });
|
||||
expect(vm.user.lang).toBe(userLanguage);
|
||||
vm.updatePreferences();
|
||||
expect(vm.locale).toBe(userLanguage);
|
||||
});
|
||||
it('should change user language and update preferences', async () => {
|
||||
const userLanguage = 'es';
|
||||
await vm.saveLanguage(userLanguage);
|
||||
expect(axios.patch).toHaveBeenCalledWith('/VnUsers/115', { lang: userLanguage });
|
||||
expect(vm.user.lang).toBe(userLanguage);
|
||||
await vm.updatePreferences();
|
||||
expect(vm.locale).toBe(userLanguage);
|
||||
});
|
||||
|
||||
it('should update user data', async () => {
|
||||
const key = 'name';
|
||||
const value = 'itboss';
|
||||
await vm.saveUserData(key, value);
|
||||
expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value });
|
||||
});
|
||||
});
|
||||
it('should update user data', async () => {
|
||||
const key = 'name';
|
||||
const value = 'itboss';
|
||||
await vm.saveUserData(key, value);
|
||||
expect(axios.post).toHaveBeenCalledWith('UserConfigs/setUserConfig', { [key]: value });
|
||||
});
|
||||
});
|
|
@ -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" />
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<script setup>
|
||||
import { onBeforeMount } from 'vue';
|
||||
import { useRouter, onBeforeRouteUpdate } from 'vue-router';
|
||||
import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router';
|
||||
import { useArrayData } from 'src/composables/useArrayData';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import useCardSize from 'src/composables/useCardSize';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
import VnSubToolbar from '../ui/VnSubToolbar.vue';
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -27,7 +26,13 @@ const arrayData = useArrayData(props.dataKey, {
|
|||
oneRecord: true,
|
||||
});
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
stateStore.cardDescriptorChangeValue(null);
|
||||
});
|
||||
|
||||
onBeforeMount(async () => {
|
||||
stateStore.cardDescriptorChangeValue(props.descriptor);
|
||||
|
||||
const route = router.currentRoute.value;
|
||||
try {
|
||||
await fetch(route.params.id);
|
||||
|
@ -39,6 +44,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,13 +62,11 @@ 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()">
|
||||
<component :is="descriptor" />
|
||||
<QSeparator />
|
||||
<LeftMenu source="card" />
|
||||
</Teleport>
|
||||
<VnSubToolbar />
|
||||
<div :class="[useCardSize(), $attrs.class]">
|
||||
<RouterView :key="$route.path" />
|
||||
|
|
|
@ -1,28 +1,38 @@
|
|||
<script setup>
|
||||
import { defineModel } from 'vue';
|
||||
|
||||
const modelValue = defineModel({ type: Boolean, default: false });
|
||||
import { computed } from 'vue';
|
||||
|
||||
const model = defineModel({ type: [Number, Boolean] });
|
||||
const $props = defineProps({
|
||||
info: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
const checkboxModel = computed({
|
||||
get() {
|
||||
if (typeof model.value === 'number') {
|
||||
return model.value !== 0;
|
||||
}
|
||||
return model.value;
|
||||
},
|
||||
set(value) {
|
||||
if (typeof model.value === 'number') {
|
||||
model.value = value ? 1 : 0;
|
||||
} else {
|
||||
model.value = value;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<QCheckbox
|
||||
<QCheckbox v-bind="$attrs" v-model="checkboxModel" />
|
||||
<QIcon
|
||||
v-if="info"
|
||||
v-bind="$attrs"
|
||||
v-on="$attrs"
|
||||
v-model="modelValue"
|
||||
/>
|
||||
<QIcon
|
||||
v-if="info"
|
||||
v-bind="$attrs"
|
||||
class="cursor-info q-ml-sm"
|
||||
name="info"
|
||||
class="cursor-info q-ml-sm"
|
||||
name="info"
|
||||
size="sm"
|
||||
>
|
||||
<QTooltip>
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<script setup>
|
||||
const $props = defineProps({
|
||||
colors: {
|
||||
type: String,
|
||||
default: '{"value": []}',
|
||||
},
|
||||
});
|
||||
|
||||
const colorArray = JSON.parse($props.colors)?.value;
|
||||
const maxHeight = 30;
|
||||
const colorHeight = maxHeight / colorArray?.length;
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="colors" class="color-div" :style="{ height: `${maxHeight}px` }">
|
||||
<div
|
||||
v-for="(color, index) in colorArray"
|
||||
:key="index"
|
||||
:style="{
|
||||
backgroundColor: `#${color}`,
|
||||
height: `${colorHeight}px`,
|
||||
}"
|
||||
>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.color-div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
|
@ -17,6 +17,8 @@ const $props = defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['blur']);
|
||||
|
||||
const componentArray = computed(() => {
|
||||
if (typeof $props.prop === 'object') return [$props.prop];
|
||||
return $props.prop;
|
||||
|
@ -46,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"
|
||||
|
@ -54,6 +57,7 @@ function toValueAttrs(attrs) {
|
|||
v-bind="mix(toComponent).attrs"
|
||||
v-on="mix(toComponent).event ?? {}"
|
||||
v-model="model"
|
||||
@blur="emit('blur')"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -11,6 +11,7 @@ const emit = defineEmits([
|
|||
'update:options',
|
||||
'keyup.enter',
|
||||
'remove',
|
||||
'blur',
|
||||
]);
|
||||
|
||||
const $props = defineProps({
|
||||
|
@ -136,6 +137,7 @@ const handleUppercase = () => {
|
|||
:type="$attrs.type"
|
||||
:class="{ required: isRequired }"
|
||||
@keyup.enter="emit('keyup.enter')"
|
||||
@blur="emit('blur')"
|
||||
@keydown="handleKeydown"
|
||||
:clearable="false"
|
||||
:rules="mixinRules"
|
||||
|
@ -143,7 +145,7 @@ const handleUppercase = () => {
|
|||
hide-bottom-space
|
||||
:data-cy="$attrs.dataCy ?? $attrs.label + '_input'"
|
||||
>
|
||||
<template #prepend>
|
||||
<template #prepend v-if="$slots.prepend">
|
||||
<slot name="prepend" />
|
||||
</template>
|
||||
<template #append>
|
||||
|
@ -168,11 +170,11 @@ const handleUppercase = () => {
|
|||
}
|
||||
"
|
||||
></QIcon>
|
||||
|
||||
|
||||
<QIcon
|
||||
name="match_case"
|
||||
size="xs"
|
||||
v-if="!$attrs.disabled && !($attrs.readonly) && $props.uppercase"
|
||||
v-if="!$attrs.disabled && !$attrs.readonly && $props.uppercase"
|
||||
@click="handleUppercase"
|
||||
class="uppercase-icon"
|
||||
>
|
||||
|
@ -180,7 +182,7 @@ const handleUppercase = () => {
|
|||
{{ t('Convert to uppercase') }}
|
||||
</QTooltip>
|
||||
</QIcon>
|
||||
|
||||
|
||||
<slot name="append" v-if="$slots.append && !$attrs.disabled" />
|
||||
<QIcon v-if="info" name="info">
|
||||
<QTooltip max-width="350px">
|
||||
|
@ -194,13 +196,15 @@ const handleUppercase = () => {
|
|||
|
||||
<style>
|
||||
.uppercase-icon {
|
||||
transition: color 0.3s, transform 0.2s;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.3s,
|
||||
transform 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.uppercase-icon:hover {
|
||||
color: #ed9937;
|
||||
transform: scale(1.2);
|
||||
color: #ed9937;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
</style>
|
||||
<i18n>
|
||||
|
@ -214,4 +218,4 @@ const handleUppercase = () => {
|
|||
maxLength: El valor excede los {value} carácteres
|
||||
inputMax: Debe ser menor a {value}
|
||||
Convert to uppercase: Convertir a mayúsculas
|
||||
</i18n>
|
||||
</i18n>
|
||||
|
|
|
@ -42,7 +42,7 @@ const formattedDate = computed({
|
|||
if (value.at(2) == '/') value = value.split('/').reverse().join('/');
|
||||
value = date.formatDate(
|
||||
new Date(value).toISOString(),
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ',
|
||||
);
|
||||
}
|
||||
const [year, month, day] = value.split('-').map((e) => parseInt(e));
|
||||
|
@ -55,7 +55,7 @@ const formattedDate = computed({
|
|||
orgDate.getHours(),
|
||||
orgDate.getMinutes(),
|
||||
orgDate.getSeconds(),
|
||||
orgDate.getMilliseconds()
|
||||
orgDate.getMilliseconds(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ const formattedDate = computed({
|
|||
});
|
||||
|
||||
const popupDate = computed(() =>
|
||||
model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value
|
||||
model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value,
|
||||
);
|
||||
onMounted(() => {
|
||||
// fix quasar bug
|
||||
|
@ -73,7 +73,7 @@ onMounted(() => {
|
|||
watch(
|
||||
() => model.value,
|
||||
(val) => (formattedDate.value = val),
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const styleAttrs = computed(() => {
|
||||
|
@ -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
|
||||
|
|
|
@ -8,6 +8,7 @@ defineProps({
|
|||
});
|
||||
|
||||
const model = defineModel({ type: [Number, String] });
|
||||
const emit = defineEmits(['blur']);
|
||||
</script>
|
||||
<template>
|
||||
<VnInput
|
||||
|
@ -24,5 +25,6 @@ const model = defineModel({ type: [Number, String] });
|
|||
model = parseFloat(val).toFixed(decimalPlaces);
|
||||
}
|
||||
"
|
||||
@blur="emit('blur')"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -85,6 +85,7 @@ const handleModelValue = (data) => {
|
|||
:tooltip="t('Create new location')"
|
||||
:rules="mixinRules"
|
||||
:lazy-rules="true"
|
||||
required
|
||||
>
|
||||
<template #form>
|
||||
<CreateNewPostcode
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -12,7 +12,7 @@ const $props = defineProps({
|
|||
},
|
||||
});
|
||||
onMounted(
|
||||
() => (stateStore.leftDrawer = useQuasar().screen.gt.xs ? $props.leftDrawer : false)
|
||||
() => (stateStore.leftDrawer = useQuasar().screen.gt.xs ? $props.leftDrawer : false),
|
||||
);
|
||||
|
||||
const teleportRef = ref({});
|
||||
|
@ -35,8 +35,14 @@ onMounted(() => {
|
|||
<template>
|
||||
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||
<QScrollArea class="fit text-grey-8">
|
||||
<div id="left-panel" ref="teleportRef"></div>
|
||||
<LeftMenu v-if="!hasContent" />
|
||||
<div id="left-panel" ref="teleportRef">
|
||||
<template v-if="stateStore.cardDescriptor">
|
||||
<component :is="stateStore.cardDescriptor" />
|
||||
<QSeparator />
|
||||
<LeftMenu source="card" />
|
||||
</template>
|
||||
<template v-else> <LeftMenu /></template>
|
||||
</div>
|
||||
</QScrollArea>
|
||||
</QDrawer>
|
||||
<QPageContainer>
|
||||
|
|
|
@ -14,7 +14,7 @@ const $props = defineProps({
|
|||
},
|
||||
});
|
||||
const options = ref([]);
|
||||
|
||||
const emit = defineEmits(['blur']);
|
||||
onBeforeMount(async () => {
|
||||
const { url, optionValue, optionLabel } = useAttrs();
|
||||
const findBy = $props.find ?? url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1);
|
||||
|
@ -35,5 +35,5 @@ onBeforeMount(async () => {
|
|||
});
|
||||
</script>
|
||||
<template>
|
||||
<VnSelect v-bind="$attrs" :options="$attrs.options ?? options" />
|
||||
<VnSelect v-bind="$attrs" :options="$attrs.options ?? options" @blur="emit('blur')" />
|
||||
</template>
|
||||
|
|
|
@ -37,7 +37,6 @@ const isAllowedToCreate = computed(() => {
|
|||
|
||||
defineExpose({ vnSelectDialogRef: select });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VnSelect
|
||||
ref="select"
|
||||
|
@ -67,7 +66,6 @@ defineExpose({ vnSelectDialogRef: select });
|
|||
</template>
|
||||
</VnSelect>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.default-icon {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<script setup>
|
||||
import VnSelectDialog from './VnSelectDialog.vue';
|
||||
import FilterTravelForm from 'src/components/FilterTravelForm.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { toDate } from 'src/filters';
|
||||
const { t } = useI18n();
|
||||
|
||||
const $props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
onFilterTravelSelected: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<VnSelectDialog
|
||||
:label="t('entry.basicData.travel')"
|
||||
v-bind="$attrs"
|
||||
url="Travels/filter"
|
||||
:fields="['id', 'warehouseInName']"
|
||||
option-value="id"
|
||||
option-label="warehouseInName"
|
||||
map-options
|
||||
hide-selected
|
||||
:required="true"
|
||||
action-icon="filter_alt"
|
||||
:roles-allowed-to-create="['buyer']"
|
||||
>
|
||||
<template #form>
|
||||
<FilterTravelForm @travel-selected="onFilterTravelSelected(data, $event)" />
|
||||
</template>
|
||||
<template #option="scope">
|
||||
<QItem v-bind="scope.itemProps">
|
||||
<QItemSection>
|
||||
<QItemLabel>
|
||||
{{ scope.opt?.agencyModeName }} -
|
||||
{{ scope.opt?.warehouseInName }}
|
||||
({{ toDate(scope.opt?.shipped) }}) →
|
||||
{{ scope.opt?.warehouseOutName }}
|
||||
({{ toDate(scope.opt?.landed) }})
|
||||
</QItemLabel>
|
||||
</QItemSection>
|
||||
</QItem>
|
||||
</template>
|
||||
</VnSelectDialog>
|
||||
</template>
|
|
@ -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>
|
||||
|
|
|
@ -1,53 +1,32 @@
|
|||
<script setup>
|
||||
defineProps({
|
||||
hasImage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div id="descriptor-skeleton">
|
||||
<div id="descriptor-skeleton" class="bg-vn-page">
|
||||
<div class="row justify-between q-pa-sm">
|
||||
<QSkeleton square size="40px" />
|
||||
<QSkeleton square size="40px" />
|
||||
<QSkeleton square height="40px" width="20px" />
|
||||
<QSkeleton square size="30px" v-for="i in 3" :key="i" />
|
||||
</div>
|
||||
<div class="col justify-between q-pa-sm q-gutter-y-xs">
|
||||
<QSkeleton square height="40px" width="150px" />
|
||||
<QSkeleton square height="30px" width="70px" />
|
||||
<div class="q-pa-xs" v-if="hasImage">
|
||||
<QSkeleton square height="200px" width="100%" />
|
||||
</div>
|
||||
<div class="col q-pl-sm q-pa-sm q-mb-md">
|
||||
<div class="row justify-between">
|
||||
<QSkeleton type="text" square height="30px" width="20%" />
|
||||
<QSkeleton type="text" square height="30px" width="60%" />
|
||||
</div>
|
||||
<div class="row justify-between">
|
||||
<QSkeleton type="text" square height="30px" width="20%" />
|
||||
<QSkeleton type="text" square height="30px" width="60%" />
|
||||
</div>
|
||||
<div class="row justify-between">
|
||||
<QSkeleton type="text" square height="30px" width="20%" />
|
||||
<QSkeleton type="text" square height="30px" width="60%" />
|
||||
</div>
|
||||
<div class="row justify-between">
|
||||
<QSkeleton type="text" square height="30px" width="20%" />
|
||||
<QSkeleton type="text" square height="30px" width="60%" />
|
||||
</div>
|
||||
<div class="row justify-between">
|
||||
<QSkeleton type="text" square height="30px" width="20%" />
|
||||
<QSkeleton type="text" square height="30px" width="60%" />
|
||||
</div>
|
||||
<div class="row justify-between">
|
||||
<QSkeleton type="text" square height="30px" width="20%" />
|
||||
<QSkeleton type="text" square height="30px" width="60%" />
|
||||
<div class="col justify-between q-pa-md q-gutter-y-xs">
|
||||
<QSkeleton square height="25px" width="150px" />
|
||||
<QSkeleton square height="15px" width="70px" />
|
||||
</div>
|
||||
<div class="q-pl-sm q-pa-sm q-mb-md">
|
||||
<div class="row q-gutter-x-sm q-pa-none q-ma-none" v-for="i in 5" :key="i">
|
||||
<QSkeleton type="text" square height="20px" width="30%" />
|
||||
<QSkeleton type="text" square height="20px" width="60%" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<QCardActions>
|
||||
<QSkeleton size="40px" />
|
||||
<QSkeleton size="40px" />
|
||||
<QSkeleton size="40px" />
|
||||
<QSkeleton size="40px" />
|
||||
<QSkeleton size="40px" />
|
||||
<QCardActions class="q-gutter-x-sm justify-between">
|
||||
<QSkeleton size="40px" v-for="i in 5" :key="i" />
|
||||
</QCardActions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#descriptor-skeleton .q-card__actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -82,7 +82,7 @@ function cancel() {
|
|||
@click="cancel()"
|
||||
/>
|
||||
</QCardSection>
|
||||
<QCardSection class="q-pb-none">
|
||||
<QCardSection class="q-pb-none" data-cy="VnConfirm_message">
|
||||
<span v-if="message !== false" v-html="message" />
|
||||
</QCardSection>
|
||||
<QCardSection class="row items-center q-pt-none">
|
||||
|
@ -95,6 +95,7 @@ function cancel() {
|
|||
:disable="isLoading"
|
||||
flat
|
||||
@click="cancel()"
|
||||
data-cy="VnConfirm_cancel"
|
||||
/>
|
||||
<QBtn
|
||||
:label="t('globals.confirm')"
|
||||
|
|
|
@ -293,6 +293,9 @@ const getLocale = (label) => {
|
|||
/>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.q-field__label.no-pointer-events.absolute.ellipsis {
|
||||
margin-left: 6px !important;
|
||||
}
|
||||
.list {
|
||||
width: 256px;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<QTooltip>
|
||||
{{ $t('components.cardDescriptor.moreOptions') }}
|
||||
</QTooltip>
|
||||
<QMenu ref="menuRef">
|
||||
<QMenu ref="menuRef" data-cy="descriptor-more-opts-menu">
|
||||
<QList>
|
||||
<slot name="menu" :menu-ref="$refs.menuRef" />
|
||||
</QList>
|
||||
|
|
|
@ -18,20 +18,16 @@ import VnInput from 'components/common/VnInput.vue';
|
|||
|
||||
const emit = defineEmits(['onFetch']);
|
||||
|
||||
const originalAttrs = useAttrs();
|
||||
|
||||
const $attrs = computed(() => {
|
||||
const { style, ...rest } = originalAttrs;
|
||||
return rest;
|
||||
});
|
||||
const $attrs = useAttrs();
|
||||
|
||||
const isRequired = computed(() => {
|
||||
return Object.keys($attrs).includes('required')
|
||||
return Object.keys($attrs).includes('required');
|
||||
});
|
||||
|
||||
const $props = defineProps({
|
||||
url: { type: String, default: null },
|
||||
saveUrl: {type: String, default: null},
|
||||
saveUrl: { type: String, default: null },
|
||||
userFilter: { type: Object, default: () => {} },
|
||||
filter: { type: Object, default: () => {} },
|
||||
body: { type: Object, default: () => {} },
|
||||
addNote: { type: Boolean, default: false },
|
||||
|
@ -65,7 +61,7 @@ async function insert() {
|
|||
}
|
||||
|
||||
function confirmAndUpdate() {
|
||||
if(!newNote.text && originalText)
|
||||
if (!newNote.text && originalText)
|
||||
quasar
|
||||
.dialog({
|
||||
component: VnConfirm,
|
||||
|
@ -88,11 +84,17 @@ async function update() {
|
|||
...body,
|
||||
...{ notes: newNote.text },
|
||||
};
|
||||
await axios.patch(`${$props.saveUrl ?? `${$props.url}/${$props.body.workerFk}`}`, newBody);
|
||||
await axios.patch(
|
||||
`${$props.saveUrl ?? `${$props.url}/${$props.body.workerFk}`}`,
|
||||
newBody,
|
||||
);
|
||||
}
|
||||
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
if ((newNote.text && !$props.justInput) || (newNote.text !== originalText) && $props.justInput)
|
||||
if (
|
||||
(newNote.text && !$props.justInput) ||
|
||||
(newNote.text !== originalText && $props.justInput)
|
||||
)
|
||||
quasar.dialog({
|
||||
component: VnConfirm,
|
||||
componentProps: {
|
||||
|
@ -104,12 +106,11 @@ onBeforeRouteLeave((to, from, next) => {
|
|||
else next();
|
||||
});
|
||||
|
||||
function fetchData([ data ]) {
|
||||
function fetchData([data]) {
|
||||
newNote.text = data?.notes;
|
||||
originalText = data?.notes;
|
||||
emit('onFetch', data);
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<FetchData
|
||||
|
@ -126,8 +127,8 @@ function fetchData([ data ]) {
|
|||
@on-fetch="fetchData"
|
||||
auto-load
|
||||
/>
|
||||
<QCard
|
||||
class="q-pa-xs q-mb-lg full-width"
|
||||
<QCard
|
||||
class="q-pa-xs q-mb-lg full-width"
|
||||
:class="{ 'just-input': $props.justInput }"
|
||||
v-if="$props.addNote || $props.justInput"
|
||||
>
|
||||
|
@ -179,7 +180,8 @@ function fetchData([ data ]) {
|
|||
:url="$props.url"
|
||||
order="created DESC"
|
||||
:limit="0"
|
||||
:user-filter="$props.filter"
|
||||
:user-filter="userFilter"
|
||||
:filter="filter"
|
||||
auto-load
|
||||
ref="vnPaginateRef"
|
||||
class="show"
|
||||
|
@ -218,7 +220,7 @@ function fetchData([ data ]) {
|
|||
>
|
||||
{{
|
||||
observationTypes.find(
|
||||
(ot) => ot.id === note.observationTypeFk
|
||||
(ot) => ot.id === note.observationTypeFk,
|
||||
)?.description
|
||||
}}
|
||||
</QBadge>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,3 +53,8 @@ const manaCode = ref(props.manaCode);
|
|||
/>
|
||||
</div>
|
||||
</template>
|
||||
<i18n>
|
||||
es:
|
||||
Promotion mana: Maná promoción
|
||||
Claim mana: Maná reclamación
|
||||
</i18n>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
import { useQuasar } from 'quasar';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import VnConfirm from 'components/ui/VnConfirm.vue';
|
||||
|
||||
export async function checkEntryLock(entryFk, userFk) {
|
||||
const { t } = useI18n();
|
||||
const quasar = useQuasar();
|
||||
const { push } = useRouter();
|
||||
const { data } = await axios.get(`Entries/${entryFk}`, {
|
||||
params: {
|
||||
filter: JSON.stringify({
|
||||
fields: ['id', 'locked', 'lockerUserFk'],
|
||||
include: { relation: 'user', scope: { fields: ['id', 'nickname'] } },
|
||||
}),
|
||||
},
|
||||
});
|
||||
const entryConfig = await axios.get('EntryConfigs/findOne');
|
||||
|
||||
if (data?.lockerUserFk && data?.locked) {
|
||||
const now = new Date(Date.vnNow()).getTime();
|
||||
const lockedTime = new Date(data.locked).getTime();
|
||||
const timeDiff = (now - lockedTime) / 1000;
|
||||
const isMaxTimeLockExceeded = entryConfig.data.maxLockTime > timeDiff;
|
||||
|
||||
if (data?.lockerUserFk !== userFk && isMaxTimeLockExceeded) {
|
||||
quasar
|
||||
.dialog({
|
||||
component: VnConfirm,
|
||||
componentProps: {
|
||||
title: t('entry.lock.title'),
|
||||
message: t('entry.lock.message', {
|
||||
userName: data?.user?.nickname,
|
||||
time: timeDiff / 60,
|
||||
}),
|
||||
},
|
||||
})
|
||||
.onOk(
|
||||
async () =>
|
||||
await axios.patch(`Entries/${entryFk}`, {
|
||||
locked: Date.vnNow(),
|
||||
lockerUserFk: userFk,
|
||||
}),
|
||||
)
|
||||
.onCancel(() => {
|
||||
push({ path: `summary` });
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await axios
|
||||
.patch(`Entries/${entryFk}`, {
|
||||
locked: Date.vnNow(),
|
||||
lockerUserFk: userFk,
|
||||
})
|
||||
.then(
|
||||
quasar.notify({
|
||||
message: t('entry.lock.success'),
|
||||
color: 'positive',
|
||||
group: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
export function getColAlign(col) {
|
||||
let align;
|
||||
switch (col.component) {
|
||||
case 'time':
|
||||
case 'date':
|
||||
case 'select':
|
||||
align = 'left';
|
||||
break;
|
||||
case 'number':
|
||||
align = 'right';
|
||||
break;
|
||||
case 'checkbox':
|
||||
align = 'center';
|
||||
break;
|
||||
default:
|
||||
align = col?.align;
|
||||
}
|
||||
|
||||
if (/^is[A-Z]/.test(col.name) || /^has[A-Z]/.test(col.name)) align = 'center';
|
||||
|
||||
return 'text-' + (align ?? 'center');
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -21,7 +21,10 @@ body.body--light {
|
|||
.q-header .q-toolbar {
|
||||
color: var(--vn-text-color);
|
||||
}
|
||||
|
||||
--vn-color-negative: $negative;
|
||||
}
|
||||
|
||||
body.body--dark {
|
||||
--vn-header-color: #5d5d5d;
|
||||
--vn-page-color: #222;
|
||||
|
@ -37,6 +40,8 @@ body.body--dark {
|
|||
--vn-text-color-contrast: black;
|
||||
|
||||
background-color: var(--vn-page-color);
|
||||
|
||||
--vn-color-negative: $negative;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -75,7 +80,6 @@ a {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
// Removes chrome autofill background
|
||||
input:-webkit-autofill,
|
||||
select:-webkit-autofill {
|
||||
color: var(--vn-text-color);
|
||||
|
@ -149,11 +153,6 @@ select:-webkit-autofill {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vn-table-separation-row {
|
||||
height: 16px !important;
|
||||
background-color: var(--vn-section-color) !important;
|
||||
}
|
||||
|
||||
/* Estilo para el asterisco en campos requeridos */
|
||||
.q-field.required .q-field__label:after {
|
||||
content: ' *';
|
||||
|
@ -276,8 +275,6 @@ input::-webkit-inner-spin-button {
|
|||
font-size: 11pt;
|
||||
}
|
||||
td {
|
||||
font-size: 11pt;
|
||||
border-top: 1px solid var(--vn-page-color);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
}
|
||||
|
@ -321,9 +318,6 @@ input::-webkit-inner-spin-button {
|
|||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.row > .column:has(.q-checkbox) {
|
||||
max-width: fit-content;
|
||||
}
|
||||
.q-field__inner {
|
||||
.q-field__control {
|
||||
min-height: auto !important;
|
||||
|
@ -341,3 +335,7 @@ input::-webkit-inner-spin-button {
|
|||
border: 1px solid;
|
||||
box-shadow: 0 4px 6px #00000000;
|
||||
}
|
||||
|
||||
.containerShrinked {
|
||||
width: 80%;
|
||||
}
|
||||
|
|
|
@ -30,7 +30,9 @@ $color-spacer: #7979794d;
|
|||
$border-thin-light: 1px solid $color-spacer-light;
|
||||
$primary-light: #f5b351;
|
||||
$dark-shadow-color: black;
|
||||
$layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d;
|
||||
$layout-shadow-dark:
|
||||
0 0 10px 2px #00000033,
|
||||
0 0px 10px #0000003d;
|
||||
$spacing-md: 16px;
|
||||
$color-font-secondary: #777;
|
||||
$width-xs: 400px;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ globals:
|
|||
reset: Reset
|
||||
close: Close
|
||||
cancel: Cancel
|
||||
isSaveAndContinue: Save and continue
|
||||
clone: Clone
|
||||
confirm: Confirm
|
||||
assign: Assign
|
||||
|
@ -48,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
|
||||
|
@ -151,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
|
||||
|
@ -408,6 +412,106 @@ cau:
|
|||
subtitle: By sending this ticket, all the data related to the error, the section, the user, etc., are already sent.
|
||||
inputLabel: Explain why this error should not appear
|
||||
askPrivileges: Ask for privileges
|
||||
entry:
|
||||
list:
|
||||
newEntry: New entry
|
||||
tableVisibleColumns:
|
||||
isExcludedFromAvailable: Exclude from inventory
|
||||
isOrdered: Ordered
|
||||
isConfirmed: Ready to label
|
||||
isReceived: Received
|
||||
isRaid: Raid
|
||||
landed: Date
|
||||
supplierFk: Supplier
|
||||
reference: Ref/Alb/Guide
|
||||
invoiceNumber: Invoice
|
||||
agencyModeId: Agency
|
||||
isBooked: Booked
|
||||
companyFk: Company
|
||||
evaNotes: Notes
|
||||
warehouseOutFk: Origin
|
||||
warehouseInFk: Destiny
|
||||
entryTypeDescription: Entry type
|
||||
invoiceAmount: Import
|
||||
travelFk: Travel
|
||||
summary:
|
||||
invoiceAmount: Amount
|
||||
commission: Commission
|
||||
currency: Currency
|
||||
invoiceNumber: Invoice number
|
||||
ordered: Ordered
|
||||
booked: Booked
|
||||
excludedFromAvailable: Inventory
|
||||
travelReference: Reference
|
||||
travelAgency: Agency
|
||||
travelShipped: Shipped
|
||||
travelDelivered: Delivered
|
||||
travelLanded: Landed
|
||||
travelReceived: Received
|
||||
buys: Buys
|
||||
stickers: Stickers
|
||||
package: Package
|
||||
packing: Pack.
|
||||
grouping: Group.
|
||||
buyingValue: Buying value
|
||||
import: Import
|
||||
pvp: PVP
|
||||
basicData:
|
||||
travel: Travel
|
||||
currency: Currency
|
||||
commission: Commission
|
||||
observation: Observation
|
||||
booked: Booked
|
||||
excludedFromAvailable: Inventory
|
||||
buys:
|
||||
observations: Observations
|
||||
packagingFk: Box
|
||||
color: Color
|
||||
printedStickers: Printed stickers
|
||||
notes:
|
||||
observationType: Observation type
|
||||
latestBuys:
|
||||
tableVisibleColumns:
|
||||
image: Picture
|
||||
itemFk: Item ID
|
||||
weightByPiece: Weight/Piece
|
||||
isActive: Active
|
||||
family: Family
|
||||
entryFk: Entry
|
||||
freightValue: Freight value
|
||||
comissionValue: Commission value
|
||||
packageValue: Package value
|
||||
isIgnored: Is ignored
|
||||
price2: Grouping
|
||||
price3: Packing
|
||||
minPrice: Min
|
||||
ektFk: Ekt
|
||||
packingOut: Package out
|
||||
landing: Landing
|
||||
isExcludedFromAvailable: Exclude from inventory
|
||||
isRaid: Raid
|
||||
invoiceNumber: Invoice
|
||||
reference: Ref/Alb/Guide
|
||||
params:
|
||||
isExcludedFromAvailable: Excluir del inventario
|
||||
isOrdered: Pedida
|
||||
isConfirmed: Lista para etiquetar
|
||||
isReceived: Recibida
|
||||
isRaid: Redada
|
||||
landed: Fecha
|
||||
supplierFk: Proveedor
|
||||
invoiceNumber: Nº Factura
|
||||
reference: Ref/Alb/Guía
|
||||
agencyModeId: Agencia
|
||||
isBooked: Asentado
|
||||
companyFk: Empresa
|
||||
travelFk: Envio
|
||||
evaNotes: Notas
|
||||
warehouseOutFk: Origen
|
||||
warehouseInFk: Destino
|
||||
entryTypeDescription: Tipo entrada
|
||||
invoiceAmount: Importe
|
||||
dated: Fecha
|
||||
ticket:
|
||||
params:
|
||||
ticketFk: Ticket ID
|
||||
|
@ -590,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
|
||||
|
@ -599,6 +705,7 @@ worker:
|
|||
calendarType: Work Calendar
|
||||
workCenter: Work Center
|
||||
payrollCategories: Contract Category
|
||||
workerBusinessAgreementName: Agreement
|
||||
occupationCode: Contribution Code
|
||||
rate: Rate
|
||||
businessType: Contract Type
|
||||
|
@ -728,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:
|
||||
|
|
|
@ -33,6 +33,7 @@ globals:
|
|||
reset: Restaurar
|
||||
close: Cerrar
|
||||
cancel: Cancelar
|
||||
isSaveAndContinue: Guardar y continuar
|
||||
clone: Clonar
|
||||
confirm: Confirmar
|
||||
assign: Asignar
|
||||
|
@ -50,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
|
||||
|
@ -57,8 +59,8 @@ globals:
|
|||
today: Hoy
|
||||
yesterday: Ayer
|
||||
dateFormat: es-ES
|
||||
microsip: Abrir en MicroSIP
|
||||
noSelectedRows: No tienes ninguna línea seleccionada
|
||||
microsip: Abrir en MicroSIP
|
||||
downloadCSVSuccess: Descarga de CSV exitosa
|
||||
reference: Referencia
|
||||
agency: Agencia
|
||||
|
@ -155,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
|
||||
|
@ -402,6 +406,87 @@ cau:
|
|||
subtitle: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc
|
||||
inputLabel: Explique el motivo por el que no deberia aparecer este fallo
|
||||
askPrivileges: Solicitar permisos
|
||||
entry:
|
||||
list:
|
||||
newEntry: Nueva entrada
|
||||
tableVisibleColumns:
|
||||
isExcludedFromAvailable: Excluir del inventario
|
||||
isOrdered: Pedida
|
||||
isConfirmed: Lista para etiquetar
|
||||
isReceived: Recibida
|
||||
isRaid: Redada
|
||||
landed: Fecha
|
||||
supplierFk: Proveedor
|
||||
invoiceNumber: Nº Factura
|
||||
reference: Ref/Alb/Guía
|
||||
agencyModeId: Agencia
|
||||
isBooked: Asentado
|
||||
companyFk: Empresa
|
||||
travelFk: Envio
|
||||
evaNotes: Notas
|
||||
warehouseOutFk: Origen
|
||||
warehouseInFk: Destino
|
||||
entryTypeDescription: Tipo entrada
|
||||
invoiceAmount: Importe
|
||||
summary:
|
||||
invoiceAmount: Importe
|
||||
commission: Comisión
|
||||
currency: Moneda
|
||||
invoiceNumber: Núm. factura
|
||||
ordered: Pedida
|
||||
booked: Contabilizada
|
||||
excludedFromAvailable: Inventario
|
||||
travelReference: Referencia
|
||||
travelAgency: Agencia
|
||||
travelShipped: F. envio
|
||||
travelWarehouseOut: Alm. salida
|
||||
travelDelivered: Enviada
|
||||
travelLanded: F. entrega
|
||||
travelReceived: Recibida
|
||||
buys: Compras
|
||||
stickers: Etiquetas
|
||||
package: Embalaje
|
||||
packing: Pack.
|
||||
grouping: Group.
|
||||
buyingValue: Coste
|
||||
import: Importe
|
||||
pvp: PVP
|
||||
basicData:
|
||||
travel: Envío
|
||||
currency: Moneda
|
||||
observation: Observación
|
||||
commission: Comisión
|
||||
booked: Asentado
|
||||
excludedFromAvailable: Inventario
|
||||
buys:
|
||||
observations: Observaciónes
|
||||
packagingFk: Embalaje
|
||||
color: Color
|
||||
printedStickers: Etiquetas impresas
|
||||
notes:
|
||||
observationType: Tipo de observación
|
||||
latestBuys:
|
||||
tableVisibleColumns:
|
||||
image: Foto
|
||||
itemFk: Id Artículo
|
||||
weightByPiece: Peso (gramos)/tallo
|
||||
isActive: Activo
|
||||
family: Familia
|
||||
entryFk: Entrada
|
||||
freightValue: Porte
|
||||
comissionValue: Comisión
|
||||
packageValue: Embalaje
|
||||
isIgnored: Ignorado
|
||||
price2: Grouping
|
||||
price3: Packing
|
||||
minPrice: Min
|
||||
ektFk: Ekt
|
||||
packingOut: Embalaje envíos
|
||||
landing: Llegada
|
||||
isExcludedFromAvailable: Excluir del inventario
|
||||
isRaid: Redada
|
||||
invoiceNumber: Nº Factura
|
||||
reference: Ref/Alb/Guía
|
||||
ticket:
|
||||
params:
|
||||
ticketFk: ID de ticket
|
||||
|
@ -493,15 +578,11 @@ ticket:
|
|||
consigneeStreet: Dirección
|
||||
create:
|
||||
address: Dirección
|
||||
order:
|
||||
field:
|
||||
salesPersonFk: Comercial
|
||||
form:
|
||||
clientFk: Cliente
|
||||
addressFk: Dirección
|
||||
agencyModeFk: Agencia
|
||||
list:
|
||||
newOrder: Nuevo Pedido
|
||||
invoiceOut:
|
||||
card:
|
||||
issued: Fecha emisión
|
||||
customerCard: Ficha del cliente
|
||||
ticketList: Listado de tickets
|
||||
summary:
|
||||
issued: Fecha
|
||||
dued: Fecha límite
|
||||
|
@ -512,6 +593,71 @@ order:
|
|||
fee: Cuota
|
||||
tickets: Tickets
|
||||
totalWithVat: Importe
|
||||
globalInvoices:
|
||||
errors:
|
||||
chooseValidClient: Selecciona un cliente válido
|
||||
chooseValidCompany: Selecciona una empresa válida
|
||||
chooseValidPrinter: Selecciona una impresora válida
|
||||
chooseValidSerialType: Selecciona una tipo de serie válida
|
||||
fillDates: La fecha de la factura y la fecha máxima deben estar completas
|
||||
invoiceDateLessThanMaxDate: La fecha de la factura no puede ser menor que la fecha máxima
|
||||
invoiceWithFutureDate: Existe una factura con una fecha futura
|
||||
noTicketsToInvoice: No existen tickets para facturar
|
||||
criticalInvoiceError: Error crítico en la facturación proceso detenido
|
||||
invalidSerialTypeForAll: El tipo de serie debe ser global cuando se facturan todos los clientes
|
||||
table:
|
||||
addressId: Id dirección
|
||||
streetAddress: Dirección fiscal
|
||||
statusCard:
|
||||
percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}'
|
||||
pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs'
|
||||
negativeBases:
|
||||
clientId: Id cliente
|
||||
base: Base
|
||||
active: Activo
|
||||
hasToInvoice: Facturar
|
||||
verifiedData: Datos comprobados
|
||||
comercial: Comercial
|
||||
errors:
|
||||
downloadCsvFailed: Error al descargar CSV
|
||||
order:
|
||||
field:
|
||||
salesPersonFk: Comercial
|
||||
form:
|
||||
clientFk: Cliente
|
||||
addressFk: Dirección
|
||||
agencyModeFk: Agencia
|
||||
list:
|
||||
newOrder: Nuevo Pedido
|
||||
summary:
|
||||
basket: Cesta
|
||||
notConfirmed: No confirmada
|
||||
created: Creado
|
||||
createdFrom: Creado desde
|
||||
address: Dirección
|
||||
total: Total
|
||||
vat: IVA
|
||||
state: Estado
|
||||
alias: Alias
|
||||
items: Artículos
|
||||
orderTicketList: Tickets del pedido
|
||||
amount: Monto
|
||||
confirm: Confirmar
|
||||
confirmLines: Confirmar lineas
|
||||
shelving:
|
||||
list:
|
||||
parking: Parking
|
||||
priority: Prioridad
|
||||
newShelving: Nuevo Carro
|
||||
summary:
|
||||
recyclable: Reciclable
|
||||
parking:
|
||||
pickingOrder: Orden de recogida
|
||||
row: Fila
|
||||
column: Columna
|
||||
searchBar:
|
||||
info: Puedes buscar por código de parking
|
||||
label: Buscar parking...
|
||||
department:
|
||||
chat: Chat
|
||||
bossDepartment: Jefe de departamento
|
||||
|
@ -624,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
|
||||
|
@ -636,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
|
||||
|
@ -695,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
|
||||
|
@ -772,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)
|
||||
|
|
|
@ -51,7 +51,6 @@ const removeAlias = () => {
|
|||
<CardDescriptor
|
||||
ref="descriptor"
|
||||
:url="`MailAliases/${entityId}`"
|
||||
module="Alias"
|
||||
data-key="Alias"
|
||||
title="alias"
|
||||
>
|
||||
|
|
|
@ -24,7 +24,6 @@ onMounted(async () => {
|
|||
ref="descriptor"
|
||||
:url="`VnUsers/preview`"
|
||||
:filter="{ ...filter, where: { id: entityId } }"
|
||||
module="Account"
|
||||
data-key="Account"
|
||||
title="nickname"
|
||||
>
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
"
|
||||
>
|
||||
|
|
|
@ -35,7 +35,6 @@ const removeRole = async () => {
|
|||
<CardDescriptor
|
||||
url="VnRoles"
|
||||
:filter="{ where: { id: entityId } }"
|
||||
module="Role"
|
||||
data-key="Role"
|
||||
:summary="$props.summary"
|
||||
>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -46,7 +46,6 @@ onMounted(async () => {
|
|||
<CardDescriptor
|
||||
:url="`Claims/${entityId}`"
|
||||
:filter="filter"
|
||||
module="Claim"
|
||||
title="client.name"
|
||||
data-key="Claim"
|
||||
>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { computed, useAttrs } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useState } from 'src/composables/useState';
|
||||
import VnNotes from 'src/components/ui/VnNotes.vue';
|
||||
|
@ -7,7 +7,6 @@ import VnNotes from 'src/components/ui/VnNotes.vue';
|
|||
const route = useRoute();
|
||||
const state = useState();
|
||||
const user = state.getUser();
|
||||
const $attrs = useAttrs();
|
||||
|
||||
const $props = defineProps({
|
||||
id: { type: [Number, String], default: null },
|
||||
|
@ -15,24 +14,21 @@ const $props = defineProps({
|
|||
});
|
||||
const claimId = computed(() => $props.id || route.params.id);
|
||||
|
||||
const claimFilter = computed(() => {
|
||||
return {
|
||||
where: { claimFk: claimId.value },
|
||||
fields: ['id', 'created', 'workerFk', 'text'],
|
||||
include: {
|
||||
relation: 'worker',
|
||||
scope: {
|
||||
fields: ['id', 'firstName', 'lastName'],
|
||||
include: {
|
||||
relation: 'user',
|
||||
scope: {
|
||||
fields: ['id', 'nickname', 'name'],
|
||||
},
|
||||
const claimFilter = {
|
||||
fields: ['id', 'created', 'workerFk', 'text'],
|
||||
include: {
|
||||
relation: 'worker',
|
||||
scope: {
|
||||
fields: ['id', 'firstName', 'lastName'],
|
||||
include: {
|
||||
relation: 'user',
|
||||
scope: {
|
||||
fields: ['id', 'nickname', 'name'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const body = {
|
||||
claimFk: claimId.value,
|
||||
|
@ -43,7 +39,8 @@ const body = {
|
|||
<VnNotes
|
||||
url="claimObservations"
|
||||
:add-note="$props.addNote"
|
||||
:filter="claimFilter"
|
||||
:user-filter="claimFilter"
|
||||
:filter="{ where: { claimFk: claimId } }"
|
||||
:body="body"
|
||||
v-bind="$attrs"
|
||||
style="overflow-y: auto"
|
||||
|
|
|
@ -156,7 +156,6 @@ function onDrag() {
|
|||
url="Claims"
|
||||
:filter="claimDmsFilter"
|
||||
@on-fetch="([data]) => setClaimDms(data)"
|
||||
limit="20"
|
||||
auto-load
|
||||
ref="claimDmsRef"
|
||||
/>
|
||||
|
|
|
@ -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')" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -13,7 +13,6 @@ claim:
|
|||
province: Province
|
||||
zone: Zone
|
||||
customerId: client ID
|
||||
assignedTo: Assigned
|
||||
created: Created
|
||||
details: Details
|
||||
item: Item
|
||||
|
|
|
@ -13,7 +13,6 @@ claim:
|
|||
province: Provincia
|
||||
zone: Zona
|
||||
customerId: ID de cliente
|
||||
assignedTo: Asignado a
|
||||
created: Creado
|
||||
details: Detalles
|
||||
item: Artículo
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -218,7 +218,7 @@ const updateDateParams = (value, params) => {
|
|||
<div v-if="row.subName" class="subName">
|
||||
{{ row.subName }}
|
||||
</div>
|
||||
<FetchedTags :item="row" :max-length="3" />
|
||||
<FetchedTags :item="row" />
|
||||
</template>
|
||||
<template #moreFilterPanel="{ params }">
|
||||
<div class="column no-wrap flex-center q-gutter-y-md q-mt-xs q-pr-xl">
|
||||
|
@ -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
|
||||
|
|
|
@ -55,7 +55,6 @@ const debtWarning = computed(() => {
|
|||
|
||||
<template>
|
||||
<CardDescriptor
|
||||
module="Customer"
|
||||
:url="`Clients/${entityId}/getCard`"
|
||||
:summary="$props.summary"
|
||||
data-key="Customer"
|
||||
|
@ -119,14 +118,6 @@ const debtWarning = computed(() => {
|
|||
>
|
||||
<QTooltip>{{ t('Allowed substitution') }}</QTooltip>
|
||||
</QIcon>
|
||||
<QIcon
|
||||
v-if="customer?.isFreezed"
|
||||
name="vn:frozen"
|
||||
size="xs"
|
||||
color="primary"
|
||||
>
|
||||
<QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip>
|
||||
</QIcon>
|
||||
<QIcon
|
||||
v-if="!entity.account?.active"
|
||||
color="primary"
|
||||
|
@ -151,6 +142,14 @@ const debtWarning = computed(() => {
|
|||
>
|
||||
<QTooltip>{{ t('customer.card.notChecked') }}</QTooltip>
|
||||
</QIcon>
|
||||
<QIcon
|
||||
v-if="entity?.isFreezed"
|
||||
name="vn:frozen"
|
||||
size="xs"
|
||||
color="primary"
|
||||
>
|
||||
<QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip>
|
||||
</QIcon>
|
||||
<QBtn
|
||||
v-if="entity.unpaid"
|
||||
flat
|
||||
|
@ -164,13 +163,13 @@ const debtWarning = computed(() => {
|
|||
<br />
|
||||
{{
|
||||
t('unpaidDated', {
|
||||
dated: toDate(customer.unpaid?.dated),
|
||||
dated: toDate(entity.unpaid?.dated),
|
||||
})
|
||||
}}
|
||||
<br />
|
||||
{{
|
||||
t('unpaidAmount', {
|
||||
amount: toCurrency(customer.unpaid?.amount),
|
||||
amount: toCurrency(entity.unpaid?.amount),
|
||||
})
|
||||
}}
|
||||
</QTooltip>
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
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 'components/FetchData.vue';
|
||||
import FormModel from 'components/FormModel.vue';
|
||||
import VnRow from 'components/ui/VnRow.vue';
|
||||
|
@ -10,9 +13,13 @@ 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 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 { notify } = useNotify();
|
||||
|
||||
const typesTaxes = ref([]);
|
||||
const typesTransactions = ref([]);
|
||||
|
@ -24,6 +31,37 @@ function handleLocation(data, location) {
|
|||
data.provinceFk = provinceFk;
|
||||
data.countryFk = countryFk;
|
||||
}
|
||||
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>
|
||||
|
@ -37,6 +75,9 @@ function handleLocation(data, location) {
|
|||
:url-update="`Clients/${route.params.id}/updateFiscalData`"
|
||||
auto-load
|
||||
model="Customer"
|
||||
:mapper="onBeforeSave"
|
||||
observe-form-changes
|
||||
@on-data-saved="checkEtChanges"
|
||||
>
|
||||
<template #form="{ data, validate }">
|
||||
<VnRow>
|
||||
|
@ -114,7 +155,7 @@ function handleLocation(data, location) {
|
|||
<VnCheckbox
|
||||
v-model="data.isVies"
|
||||
:label="t('globals.isVies')"
|
||||
:info="t('whenActivatingIt')"
|
||||
:info="t('whenActivatingIt')"
|
||||
/>
|
||||
</VnRow>
|
||||
|
||||
|
@ -127,7 +168,7 @@ function handleLocation(data, location) {
|
|||
</VnRow>
|
||||
|
||||
<VnRow>
|
||||
<VnCheckbox
|
||||
<VnCheckbox
|
||||
v-model="data.isEqualizated"
|
||||
:label="t('Is equalizated')"
|
||||
:info="t('inOrderToInvoice')"
|
||||
|
@ -172,6 +213,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:
|
||||
onlyLetters: Only letters, numbers and spaces can be used
|
||||
whenActivatingIt: When activating it, do not enter the country code in the ID field
|
||||
|
|
|
@ -1,28 +1,15 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import VnNotes from 'src/components/ui/VnNotes.vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const noteFilter = computed(() => {
|
||||
return {
|
||||
order: 'created DESC',
|
||||
where: {
|
||||
clientFk: `${route.params.id}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VnNotes
|
||||
url="clientObservations"
|
||||
:add-note="true"
|
||||
:filter="noteFilter"
|
||||
:body="{ clientFk: route.params.id }"
|
||||
:filter="{ where: { clientFk: $route.params.id } }"
|
||||
:body="{ clientFk: $route.params.id }"
|
||||
style="overflow-y: auto"
|
||||
:select-type="true"
|
||||
required
|
||||
order="created DESC"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -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
|
||||
|
@ -325,7 +325,7 @@ const sumRisk = ({ clientRisks }) => {
|
|||
</QCard>
|
||||
<QCard class="vn-max">
|
||||
<VnTitle :text="t('Latest tickets')" />
|
||||
<CustomerSummaryTable />
|
||||
<CustomerSummaryTable :id="entityId" />
|
||||
</QCard>
|
||||
</template>
|
||||
</CardSummary>
|
||||
|
|
|
@ -143,6 +143,7 @@ const exprBuilder = (param, value) => {
|
|||
outlined
|
||||
rounded
|
||||
auto-load
|
||||
sortBy="name ASC"
|
||||
/></QItemSection>
|
||||
</QItem>
|
||||
<QItem class="q-mb-sm">
|
||||
|
|
|
@ -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,
|
||||
|
@ -264,6 +274,7 @@ const columns = computed(() => [
|
|||
align: 'left',
|
||||
name: 'isActive',
|
||||
label: t('customer.summary.isActive'),
|
||||
component: 'checkbox',
|
||||
chip: {
|
||||
color: null,
|
||||
condition: (value) => !value,
|
||||
|
@ -302,6 +313,7 @@ const columns = computed(() => [
|
|||
align: 'left',
|
||||
name: 'isFreezed',
|
||||
label: t('customer.extendedList.tableVisibleColumns.isFreezed'),
|
||||
component: 'checkbox',
|
||||
chip: {
|
||||
color: null,
|
||||
condition: (value) => value,
|
||||
|
|
|
@ -9,7 +9,7 @@ import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.v
|
|||
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
|
||||
import VnInput from 'src/components/common/VnInput.vue';
|
||||
import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue';
|
||||
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue';
|
||||
import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue';
|
||||
import VnTable from 'src/components/VnTable/VnTable.vue';
|
||||
import { useArrayData } from 'src/composables/useArrayData';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)"
|
||||
/>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -20,7 +20,12 @@ const { t } = useI18n();
|
|||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { viewSummary } = useSummaryDialog();
|
||||
|
||||
const $props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
const filter = {
|
||||
include: [
|
||||
{
|
||||
|
@ -43,7 +48,7 @@ const filter = {
|
|||
},
|
||||
},
|
||||
],
|
||||
where: { clientFk: route.params.id },
|
||||
where: { clientFk: $props.id ?? route.params.id },
|
||||
order: ['shipped DESC', 'id'],
|
||||
limit: 30,
|
||||
};
|
||||
|
|
|
@ -17,9 +17,9 @@ describe('getAddresses', () => {
|
|||
expect(axios.get).toHaveBeenCalledWith(`Clients/${clientId}/addresses`, {
|
||||
params: {
|
||||
filter: JSON.stringify({
|
||||
fields: ['nickname', 'street', 'city', 'id'],
|
||||
fields: ['nickname', 'street', 'city', 'id', 'isActive'],
|
||||
where: { isActive: true },
|
||||
order: 'nickname ASC',
|
||||
order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
@ -30,4 +30,4 @@ describe('getAddresses', () => {
|
|||
|
||||
expect(axios.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import axios from 'axios';
|
||||
|
||||
export async function getAddresses(clientId, _filter = {}) {
|
||||
export async function getAddresses(clientId, _filter = {}) {
|
||||
if (!clientId) return;
|
||||
const filter = {
|
||||
..._filter,
|
||||
fields: ['nickname', 'street', 'city', 'id'],
|
||||
fields: ['nickname', 'street', 'city', 'id', 'isActive'],
|
||||
where: { isActive: true },
|
||||
order: 'nickname ASC',
|
||||
order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'],
|
||||
};
|
||||
const params = { filter: JSON.stringify(filter) };
|
||||
return await axios.get(`Clients/${clientId}/addresses`, {
|
||||
params,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRole } from 'src/composables/useRole';
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { checkEntryLock } from 'src/composables/checkEntryLock';
|
||||
import FetchData from 'components/FetchData.vue';
|
||||
import FormModel from 'components/FormModel.vue';
|
||||
import VnRow from 'components/ui/VnRow.vue';
|
||||
import VnInput from 'src/components/common/VnInput.vue';
|
||||
import VnSelect from 'src/components/common/VnSelect.vue';
|
||||
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
|
||||
import FilterTravelForm from 'src/components/FilterTravelForm.vue';
|
||||
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
|
||||
import { toDate } from 'src/filters';
|
||||
import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue';
|
||||
import VnSelectSupplier from 'src/components/common/VnSelectSupplier.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const { hasAny } = useRole();
|
||||
const isAdministrative = () => hasAny(['administrative']);
|
||||
const state = useState();
|
||||
const user = state.getUser().fn();
|
||||
|
||||
const companiesOptions = ref([]);
|
||||
const currenciesOptions = ref([]);
|
||||
|
||||
const onFilterTravelSelected = (formData, id) => {
|
||||
formData.travelFk = id;
|
||||
};
|
||||
onMounted(() => {
|
||||
checkEntryLock(route.params.id, user.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -52,46 +54,24 @@ const onFilterTravelSelected = (formData, id) => {
|
|||
>
|
||||
<template #form="{ data }">
|
||||
<VnRow>
|
||||
<VnSelectTravelExtended
|
||||
:data="data"
|
||||
v-model="data.travelFk"
|
||||
:onFilterTravelSelected="(data, result) => (data.travelFk = result)"
|
||||
/>
|
||||
<VnSelectSupplier
|
||||
v-model="data.supplierFk"
|
||||
hide-selected
|
||||
:required="true"
|
||||
map-options
|
||||
/>
|
||||
<VnSelectDialog
|
||||
:label="t('entry.basicData.travel')"
|
||||
v-model="data.travelFk"
|
||||
url="Travels/filter"
|
||||
:fields="['id', 'warehouseInName']"
|
||||
option-value="id"
|
||||
option-label="warehouseInName"
|
||||
map-options
|
||||
hide-selected
|
||||
:required="true"
|
||||
action-icon="filter_alt"
|
||||
>
|
||||
<template #form>
|
||||
<FilterTravelForm
|
||||
@travel-selected="onFilterTravelSelected(data, $event)"
|
||||
/>
|
||||
</template>
|
||||
<template #option="scope">
|
||||
<QItem v-bind="scope.itemProps">
|
||||
<QItemSection>
|
||||
<QItemLabel>
|
||||
{{ scope.opt?.agencyModeName }} -
|
||||
{{ scope.opt?.warehouseInName }}
|
||||
({{ toDate(scope.opt?.shipped) }}) →
|
||||
{{ scope.opt?.warehouseOutName }}
|
||||
({{ toDate(scope.opt?.landed) }})
|
||||
</QItemLabel>
|
||||
</QItemSection>
|
||||
</QItem>
|
||||
</template>
|
||||
</VnSelectDialog>
|
||||
</VnRow>
|
||||
<VnRow>
|
||||
<VnInput v-model="data.reference" :label="t('globals.reference')" />
|
||||
<VnInputNumber
|
||||
v-model="data.invoiceAmount"
|
||||
:label="t('entry.summary.invoiceAmount')"
|
||||
:positive="false"
|
||||
/>
|
||||
</VnRow>
|
||||
<VnRow>
|
||||
<VnInput
|
||||
|
@ -113,8 +93,7 @@ const onFilterTravelSelected = (formData, id) => {
|
|||
<VnInputNumber
|
||||
:label="t('entry.summary.commission')"
|
||||
v-model="data.commission"
|
||||
step="1"
|
||||
autofocus
|
||||
:step="1"
|
||||
:positive="false"
|
||||
/>
|
||||
<VnSelect
|
||||
|
@ -161,7 +140,7 @@ const onFilterTravelSelected = (formData, id) => {
|
|||
:label="t('entry.summary.excludedFromAvailable')"
|
||||
/>
|
||||
<QCheckbox
|
||||
v-if="isAdministrative()"
|
||||
:disable="!isAdministrative()"
|
||||
v-model="data.isBooked"
|
||||
:label="t('entry.basicData.booked')"
|
||||
/>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue