Merge branch 'dev' of https: refs #8440//gitea.verdnatura.es/verdnatura/salix-front into 8440-createVehicleNotes
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details

This commit is contained in:
Jose Antonio Tubau 2025-03-05 07:19:11 +01:00
commit 25ffdb94e6
169 changed files with 2172 additions and 1011 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -1,3 +1,41 @@
# Version 25.08 - 2025-03-04
### Added 🆕
- feat: add order for table (origin/8681_ticketAdvance_updates) by:Javier Segarra
- feat: detect when is descriptor proxy by:Javier Segarra
- feat: refs #7356 update CrudModel by:Javier Segarra
- feat: refs #8242 remove teleport by:Javier Segarra
- feat: refs #8242 use stateStore by:Javier Segarra
- fix: fixed negative bases style by:Jon
- fix: fixed style when clicking on icons by:Jon
- refactor: refs #6897 remove debug logs and unused style (origin/6897-fixSomeCaus) by:pablone
- style: refs #7356 eslint format by:Javier Segarra
### Changed 📦
- perf: refs #7356 minor changes (origin/7356_ticketService) by:Javier Segarra
- refactor: refs #6897 remove debug logs and unused style (origin/6897-fixSomeCaus) by:pablone
- refactor: refs #6897 update component props and attributes for consistency and improved functionality (origin/6897-fixMinorIssues) by:pablone
- refactor: refs #6897 update component props and improve UI handling in Entry pages by:pablone
- refactor: refs #6897 update VnTable components for improved value handling and UI adjustments (origin/6897-minorFixes) by:pablone
- refactor: refs #8697 simplify date handling in ItemDiary component by:pablone
### Fixed 🛠️
- fix: add datakey by:Javier Segarra
- fix: fixed account descriptor menu and created e2e by:Jon
- fix: fixed negative bases style by:Jon
- fix: fixed style when clicking on icons by:Jon
- fix: refs #6553 workerBusiness (origin/6553-fixWorkerBusinessV2) by:carlossa
- fix: refs #6553 workerBusiness v3 by:carlossa
- fix: refs #6897 prevent default event behavior in autocompleteExpense function by:pablone
- fix: refs #7356 chaining params by:Javier Segarra
- fix: refs #7356 ticketService by:Javier Segarra
- fix: refs #8242 workerDepartmentTree bug (origin/8242_leftMenu_responsive) by:Javier Segarra
- fix: workerBasicData by:carlossa
- Revert "revert 1015acefb7e400be2d8b5958dba69b4d98276b34" (origin/fix_revert_revert, fix_revert_revert) by:alexm
# Version 25.06 - 2025-02-18 # Version 25.06 - 2025-02-18
### Added 🆕 ### Added 🆕

108
Jenkinsfile vendored
View File

@ -1,6 +1,7 @@
#!/usr/bin/env groovy #!/usr/bin/env groovy
def PROTECTED_BRANCH def PROTECTED_BRANCH
def IS_LATEST
def BRANCH_ENV = [ def BRANCH_ENV = [
test: 'test', test: 'test',
@ -10,19 +11,22 @@ def BRANCH_ENV = [
node { node {
stage('Setup') { stage('Setup') {
env.FRONT_REPLICAS = 1
env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev' env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev'
PROTECTED_BRANCH = [ PROTECTED_BRANCH = [
'dev', 'dev',
'test', 'test',
'master', 'master',
'main',
'beta' '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 "NODE_NAME: ${env.NODE_NAME}"
echo "WORKSPACE: ${env.WORKSPACE}" echo "WORKSPACE: ${env.WORKSPACE}"
echo "CHANGE_TARGET: ${env.CHANGE_TARGET}"
configFileProvider([ configFileProvider([
configFile(fileId: 'salix-front.properties', configFile(fileId: 'salix-front.properties',
@ -33,7 +37,7 @@ node {
props.each {key, value -> echo "${key}: ${value}" } props.each {key, value -> echo "${key}: ${value}" }
} }
if (PROTECTED_BRANCH) { if (IS_PROTECTED_BRANCH) {
configFileProvider([ configFileProvider([
configFile(fileId: "salix-front.branch.${env.BRANCH_NAME}", configFile(fileId: "salix-front.branch.${env.BRANCH_NAME}",
variable: 'BRANCH_PROPS_FILE') variable: 'BRANCH_PROPS_FILE')
@ -58,6 +62,19 @@ pipeline {
PROJECT_NAME = 'lilium' PROJECT_NAME = 'lilium'
} }
stages { 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') { stage('Install') {
environment { environment {
NODE_ENV = "" NODE_ENV = ""
@ -68,48 +85,85 @@ pipeline {
} }
stage('Test') { stage('Test') {
when { when {
expression { !PROTECTED_BRANCH } expression { !IS_PROTECTED_BRANCH }
} }
environment { environment {
NODE_ENV = "" NODE_ENV = ''
CI = 'true'
TZ = 'Europe/Madrid'
} }
steps { parallel {
sh 'pnpm run test:unit:ci' stage('Unit') {
} steps {
post { sh 'pnpm run test:front:ci'
always { }
junit( post {
testResults: 'junitresults.xml', always {
allowEmptyResults: true 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') { stage('Build') {
when { when {
expression { PROTECTED_BRANCH } expression { IS_PROTECTED_BRANCH }
} }
environment { environment {
CREDENTIALS = credentials('docker-registry') VERSION = readFile 'VERSION.txt'
} }
steps { steps {
sh 'quasar build'
script { script {
def packageJson = readJSON file: 'package.json' sh 'quasar build'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
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') { stage('Deploy') {
when { when {
expression { PROTECTED_BRANCH } expression { IS_PROTECTED_BRANCH }
}
environment {
VERSION = readFile 'VERSION.txt'
} }
steps { steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
withKubeConfig([ withKubeConfig([
serverUrl: "$KUBERNETES_API", serverUrl: "$KUBERNETES_API",
credentialsId: 'kubernetes', credentialsId: 'kubernetes',

View File

@ -23,7 +23,7 @@ quasar dev
### Run unit tests ### Run unit tests
```bash ```bash
pnpm run test:unit pnpm run test:front
``` ```
### Run e2e tests ### Run e2e tests

View File

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

View File

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

45
docs/Dockerfile.dev Normal file
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "25.10.0", "version": "25.12.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -14,8 +14,8 @@
"test:e2e": "cypress open", "test:e2e": "cypress open",
"test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run", "test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest", "test:front": "vitest",
"test:unit:ci": "vitest run", "test:front:ci": "vitest run",
"commitlint": "commitlint --edit", "commitlint": "commitlint --edit",
"prepare": "npx husky install", "prepare": "npx husky install",
"addReferenceTag": "node .husky/addReferenceTag.js", "addReferenceTag": "node .husky/addReferenceTag.js",
@ -71,4 +71,4 @@
"vite": "^6.0.11", "vite": "^6.0.11",
"vitest": "^0.31.1" "vitest": "^0.31.1"
} }
} }

View File

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

View File

@ -11,6 +11,7 @@
import { configure } from 'quasar/wrappers'; import { configure } from 'quasar/wrappers';
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import path from 'path'; import path from 'path';
const target = `http://${process.env.CI ? 'back' : 'localhost'}:3000`;
export default configure(function (/* ctx */) { export default configure(function (/* ctx */) {
return { return {
@ -108,13 +109,17 @@ export default configure(function (/* ctx */) {
}, },
proxy: { proxy: {
'/api': { '/api': {
target: 'http://0.0.0.0:3000', target: target,
logLevel: 'debug', logLevel: 'debug',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },
}, },
open: 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 // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework

View File

@ -184,8 +184,11 @@ async function saveChanges(data) {
if ($props.beforeSaveFn) { if ($props.beforeSaveFn) {
changes = await $props.beforeSaveFn(changes, getChanges); changes = await $props.beforeSaveFn(changes, getChanges);
} }
try { try {
if (changes?.creates?.length === 0 && changes?.updates?.length === 0) {
return;
}
await axios.post($props.saveUrl || $props.url + '/crud', changes); await axios.post($props.saveUrl || $props.url + '/crud', changes);
} finally { } finally {
isLoading.value = false; isLoading.value = false;

View File

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

View File

@ -12,6 +12,7 @@ import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue'; import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile'; import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { getDifferences, getUpdatedValues } from 'src/filters';
const { push } = useRouter(); const { push } = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
@ -95,6 +96,10 @@ const $props = defineProps({
type: [String, Boolean], type: [String, Boolean],
default: '800px', default: '800px',
}, },
onDataSaved: {
type: Function,
default: () => {},
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed( const modelValue = computed(
@ -284,7 +289,12 @@ function trimData(data) {
} }
return data; return data;
} }
function onBeforeSave(formData, originalData) {
return getUpdatedValues(
Object.keys(getDifferences(formData, originalData)),
formData,
);
}
async function onKeyup(evt) { async function onKeyup(evt) {
if (evt.key === 'Enter' && !('prevent-submit' in attrs)) { if (evt.key === 'Enter' && !('prevent-submit' in attrs)) {
const input = evt.target; const input = evt.target;
@ -321,6 +331,7 @@ defineExpose({
class="q-pa-md" class="q-pa-md"
:style="maxWidth ? 'max-width: ' + maxWidth : ''" :style="maxWidth ? 'max-width: ' + maxWidth : ''"
id="formModel" id="formModel"
:mapper="onBeforeSave"
> >
<QCard> <QCard>
<slot <slot

View File

@ -1,12 +1,13 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, useAttrs, nextTick } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
const emit = defineEmits(['onDataSaved', 'onDataCanceled']); const emit = defineEmits(['onDataSaved', 'onDataCanceled']);
defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
default: '', default: '',
@ -22,12 +23,21 @@ defineProps({
}); });
const { t } = useI18n(); const { t } = useI18n();
const attrs = useAttrs();
const state = useState();
const formModelRef = ref(null); const formModelRef = ref(null);
const closeButton = ref(null); const closeButton = ref(null);
const isSaveAndContinue = ref(false); const isSaveAndContinue = ref(props.showSaveAndContinueBtn);
const onDataSaved = (formData, requestResponse) => { const isLoading = computed(() => formModelRef.value?.isLoading);
if (closeButton.value && !isSaveAndContinue.value) closeButton.value.click(); const reset = computed(() => formModelRef.value?.reset);
const onDataSaved = async (formData, requestResponse) => {
if (!isSaveAndContinue.value) closeButton.value?.click();
if (isSaveAndContinue.value) {
await nextTick();
state.set(attrs.model, attrs.formInitialData);
}
isSaveAndContinue.value = props.showSaveAndContinueBtn;
emit('onDataSaved', formData, requestResponse); emit('onDataSaved', formData, requestResponse);
}; };
@ -36,9 +46,6 @@ const onClick = async (saveAndContinue) => {
await formModelRef.value.save(); await formModelRef.value.save();
}; };
const isLoading = computed(() => formModelRef.value?.isLoading);
const reset = computed(() => formModelRef.value?.reset);
defineExpose({ defineExpose({
isLoading, isLoading,
onDataSaved, onDataSaved,
@ -74,10 +81,7 @@ defineExpose({
data-cy="FormModelPopup_cancel" data-cy="FormModelPopup_cancel"
v-close-popup v-close-popup
z-max z-max
@click=" @click="emit('onDataCanceled')"
isSaveAndContinue = false;
emit('onDataCanceled');
"
/> />
<QBtn <QBtn
:flat="showSaveAndContinueBtn" :flat="showSaveAndContinueBtn"

View File

@ -85,7 +85,15 @@ const refresh = () => window.location.reload();
</QTooltip> </QTooltip>
<PinnedModules ref="pinnedModulesRef" /> <PinnedModules ref="pinnedModulesRef" />
</QBtn> </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 <VnAvatar
:worker-id="user.id" :worker-id="user.id"
:title="user.name" :title="user.name"

View File

@ -18,14 +18,25 @@ defineProps({ row: { type: Object, required: true } });
</QIcon> </QIcon>
</router-link> </router-link>
<QIcon <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" name="vn:risk"
:color="row.hasHighRisk ? 'negative' : 'primary'" :color="row.hasHighRisk ? 'negative' : 'primary'"
size="xs" size="xs"
> >
<QTooltip> <QTooltip>
{{ $t('salesTicketsTable.risk') }}: {{ $t('salesTicketsTable.risk') }}:
{{ toCurrency(row.risk - row.credit) }} {{ toCurrency(row.risk - (row.credit ?? 0)) }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
@ -67,12 +78,7 @@ defineProps({ row: { type: Object, required: true } });
> >
<QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip> <QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon v-if="row?.isTaxDataChecked" name="vn:no036" color="primary" size="xs">
v-if="row?.isTaxDataChecked !== 0"
name="vn:no036"
color="primary"
size="xs"
>
<QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip> <QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip>
</QIcon> </QIcon>
<QIcon v-if="row?.isFreezed" name="vn:frozen" color="primary" size="xs"> <QIcon v-if="row?.isFreezed" name="vn:frozen" color="primary" size="xs">

View File

@ -91,7 +91,6 @@ const components = {
event: updateEvent, event: updateEvent,
attrs: { attrs: {
...defaultAttrs, ...defaultAttrs,
style: 'min-width: 150px',
}, },
forceAttrs, forceAttrs,
}, },

View File

@ -31,6 +31,7 @@ import VnLv from 'components/ui/VnLv.vue';
import VnTableOrder from 'src/components/VnTable/VnOrder.vue'; import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
import VnTableFilter from './VnTableFilter.vue'; import VnTableFilter from './VnTableFilter.vue';
import { getColAlign } from 'src/composables/getColAlign'; import { getColAlign } from 'src/composables/getColAlign';
import RightMenu from '../common/RightMenu.vue';
const arrayData = useArrayData(useAttrs()['data-key']); const arrayData = useArrayData(useAttrs()['data-key']);
const $props = defineProps({ const $props = defineProps({
@ -50,14 +51,14 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
rightSearchIcon: {
type: Boolean,
default: true,
},
rowClick: { rowClick: {
type: [Function, Boolean], type: [Function, Boolean],
default: null, default: null,
}, },
rowCtrlClick: {
type: [Function, Boolean],
default: null,
},
redirect: { redirect: {
type: String, type: String,
default: null, default: null,
@ -137,6 +138,10 @@ const $props = defineProps({
createComplement: { createComplement: {
type: Object, type: Object,
}, },
dataCy: {
type: String,
default: 'vn-table',
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -252,7 +257,9 @@ function splitColumns(columns) {
col.columnFilter = { inWhere: true, ...col.columnFilter }; col.columnFilter = { inWhere: true, ...col.columnFilter };
splittedColumns.value.columns.push(col); splittedColumns.value.columns.push(col);
} }
// Status column
splittedColumns.value.create = createOrderSort(splittedColumns.value.create);
if (splittedColumns.value.chips.length) { if (splittedColumns.value.chips.length) {
splittedColumns.value.columnChips = splittedColumns.value.chips.filter( splittedColumns.value.columnChips = splittedColumns.value.chips.filter(
(c) => !c.isId, (c) => !c.isId,
@ -268,6 +275,24 @@ function splitColumns(columns) {
} }
} }
function createOrderSort(columns) {
const orderedColumn = columns
.map((column, index) =>
column.createOrder !== undefined ? { ...column, originalIndex: index } : null,
)
.filter((item) => item !== null);
orderedColumn.sort((a, b) => a.createOrder - b.createOrder);
const filteredColumns = columns.filter((col) => col.createOrder === undefined);
orderedColumn.forEach((col) => {
filteredColumns.splice(col.createOrder, 0, col);
});
return filteredColumns;
}
const rowClickFunction = computed(() => { const rowClickFunction = computed(() => {
if ($props.rowClick != undefined) return $props.rowClick; if ($props.rowClick != undefined) return $props.rowClick;
if ($props.redirect) return ({ id }) => redirectFn(id); if ($props.redirect) return ({ id }) => redirectFn(id);
@ -313,8 +338,14 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
if (evt?.shiftKey && added) { if (evt?.shiftKey && added) {
const rowIndex = selectedRows[0].$index; const rowIndex = selectedRows[0].$index;
const selectedIndexes = new Set(selected.value.map((row) => row.$index)); const selectedIndexes = new Set(selected.value.map((row) => row.$index));
for (const row of rows) { const minIndex = selectedIndexes.size
if (row.$index == rowIndex) break; ? 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)) { if (!selectedIndexes.has(row.$index)) {
selected.value.push(row); selected.value.push(row);
selectedIndexes.add(row.$index); selectedIndexes.add(row.$index);
@ -337,12 +368,11 @@ function hasEditableFormat(column) {
const clickHandler = async (event) => { const clickHandler = async (event) => {
const clickedElement = event.target.closest('td'); const clickedElement = event.target.closest('td');
const isDateElement = event.target.closest('.q-date'); const isDateElement = event.target.closest('.q-date');
const isTimeElement = event.target.closest('.q-time'); const isTimeElement = event.target.closest('.q-time');
const isQselectDropDown = event.target.closest('.q-select__dropdown-icon'); const isQSelectDropDown = event.target.closest('.q-select__dropdown-icon');
if (isDateElement || isTimeElement || isQselectDropDown) return; if (isDateElement || isTimeElement || isQSelectDropDown) return;
if (clickedElement === null) { if (clickedElement === null) {
await destroyInput(editingRow.value, editingField.value); await destroyInput(editingRow.value, editingField.value);
@ -413,20 +443,13 @@ async function renderInput(rowId, field, clickedElement) {
eventHandlers: { eventHandlers: {
'update:modelValue': async (value) => { 'update:modelValue': async (value) => {
if (isSelect && value) { if (isSelect && value) {
row[column.name] = value[column.attrs?.optionValue ?? 'id']; await updateSelectValue(value, column, row, oldValue);
row[column?.name + 'TextValue'] =
value[column.attrs?.optionLabel ?? 'name'];
await column?.cellEvent?.['update:modelValue']?.(
value,
oldValue,
row,
);
} else row[column.name] = value; } else row[column.name] = value;
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row); await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
}, },
keyup: async (event) => { keyup: async (event) => {
if (event.key === 'Enter') if (event.key === 'Enter')
await destroyInput(rowIndex, field, clickedElement); await destroyInput(rowId, field, clickedElement);
}, },
keydown: async (event) => { keydown: async (event) => {
switch (event.key) { switch (event.key) {
@ -457,6 +480,17 @@ async function renderInput(rowId, field, clickedElement) {
node.el?.querySelector('span > div > div').focus(); 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) { async function destroyInput(rowIndex, field, clickedElement) {
if (!clickedElement) if (!clickedElement)
clickedElement = document.querySelector( clickedElement = document.querySelector(
@ -519,9 +553,9 @@ function getToggleIcon(value) {
} }
function formatColumnValue(col, row, dashIfEmpty) { function formatColumnValue(col, row, dashIfEmpty) {
if (col?.format) { if (col?.format || row[col?.name + 'VnTableTextValue']) {
if (selectRegex.test(col?.component) && row[col?.name + 'TextValue']) { if (selectRegex.test(col?.component) && row[col?.name + 'VnTableTextValue']) {
return dashIfEmpty(row[col?.name + 'TextValue']); return dashIfEmpty(row[col?.name + 'VnTableTextValue']);
} else { } else {
return col.format(row, dashIfEmpty); return col.format(row, dashIfEmpty);
} }
@ -554,19 +588,48 @@ function formatColumnValue(col, row, dashIfEmpty) {
} }
return dashIfEmpty(row[col?.name]); return dashIfEmpty(row[col?.name]);
} }
function cardClick(_, row) { function cardClick(_, row) {
if ($props.redirect) router.push({ path: `/${$props.redirect}/${row.id}` }); 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> </script>
<template> <template>
<QDrawer <RightMenu v-if="$props.rightSearch" :overlay="overlay">
v-if="$props.rightSearch" <template #right-panel>
v-model="stateStore.rightDrawer"
side="right"
:width="256"
:overlay="$props.overlay"
>
<QScrollArea class="fit">
<VnTableFilter <VnTableFilter
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
:columns="columns" :columns="columns"
@ -580,8 +643,8 @@ function cardClick(_, row) {
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" /> <slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template> </template>
</VnTableFilter> </VnTableFilter>
</QScrollArea> </template>
</QDrawer> </RightMenu>
<CrudModel <CrudModel
v-bind="$attrs" v-bind="$attrs"
:class="$attrs['class'] ?? 'q-px-md'" :class="$attrs['class'] ?? 'q-px-md'"
@ -590,6 +653,7 @@ function cardClick(_, row) {
@on-fetch="(...args) => emit('onFetch', ...args)" @on-fetch="(...args) => emit('onFetch', ...args)"
:search-url="searchUrl" :search-url="searchUrl"
:disable-infinite-scroll="isTableMode" :disable-infinite-scroll="isTableMode"
:before-save-fn="removeTextValue"
@save-changes="reload" @save-changes="reload"
:has-sub-toolbar="$props.hasSubToolbar ?? isEditable" :has-sub-toolbar="$props.hasSubToolbar ?? isEditable"
:auto-load="hasParams || $attrs['auto-load']" :auto-load="hasParams || $attrs['auto-load']"
@ -617,10 +681,11 @@ function cardClick(_, row) {
:style="isTableMode && `max-height: ${tableHeight}`" :style="isTableMode && `max-height: ${tableHeight}`"
:virtual-scroll="isTableMode" :virtual-scroll="isTableMode"
@virtual-scroll="handleScroll" @virtual-scroll="handleScroll"
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)" @row-click="(event, row) => handleRowClick(event, row)"
@update:selected="emit('update:selected', $event)" @update:selected="emit('update:selected', $event)"
@selection="(details) => handleSelection(details, rows)" @selection="(details) => handleSelection(details, rows)"
:hide-selected-banner="true" :hide-selected-banner="true"
:data-cy="$props.dataCy ?? 'vnTable'"
> >
<template #top-left v-if="!$props.withoutHeader"> <template #top-left v-if="!$props.withoutHeader">
<slot name="top-left"> </slot> <slot name="top-left"> </slot>
@ -634,6 +699,7 @@ function cardClick(_, row) {
:skip="columnsVisibilitySkipped" :skip="columnsVisibilitySkipped"
/> />
<QBtnToggle <QBtnToggle
v-if="!tableModes.some((mode) => mode.disable)"
v-model="mode" v-model="mode"
toggle-color="primary" toggle-color="primary"
class="bg-vn-section-color" class="bg-vn-section-color"
@ -953,14 +1019,6 @@ function cardClick(_, row) {
transition-show="scale" transition-show="scale"
transition-hide="scale" transition-hide="scale"
:full-width="createComplement?.isFullWidth ?? false" :full-width="createComplement?.isFullWidth ?? false"
@before-hide="
() => {
if (createRef.isSaveAndContinue) {
showForm = true;
createForm.formInitialData = { ...create.formInitialData };
}
}
"
data-cy="vn-table-create-dialog" data-cy="vn-table-create-dialog"
> >
<FormModelPopup <FormModelPopup
@ -971,7 +1029,10 @@ function cardClick(_, row) {
> >
<template #form-inputs="{ data }"> <template #form-inputs="{ data }">
<div :style="createComplement?.containerStyle"> <div :style="createComplement?.containerStyle">
<div> <div
:style="createComplement?.previousStyle"
v-if="!quasar.screen.xs"
>
<slot name="previous-create-dialog" :data="data" /> <slot name="previous-create-dialog" :data="data" />
</div> </div>
<div class="grid-create" :style="createComplement?.columnGridStyle"> <div class="grid-create" :style="createComplement?.columnGridStyle">
@ -984,7 +1045,10 @@ function cardClick(_, row) {
:label="column.label" :label="column.label"
> >
<VnColumn <VnColumn
:column="column" :column="{
...column,
...{ disable: column?.createDisable ?? false },
}"
:row="{}" :row="{}"
default="input" default="input"
v-model="data[column.name]" v-model="data[column.name]"

View File

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

View File

@ -30,8 +30,8 @@ describe('CrudModel', () => {
saveFn: '', saveFn: '',
}, },
}); });
wrapper=wrapper.wrapper; wrapper = wrapper.wrapper;
vm=wrapper.vm; vm = wrapper.vm;
}); });
beforeEach(() => { beforeEach(() => {
@ -143,14 +143,14 @@ describe('CrudModel', () => {
}); });
it('should return true if object is empty', async () => { it('should return true if object is empty', async () => {
dummyObj ={}; dummyObj = {};
result = vm.isEmpty(dummyObj); result = vm.isEmpty(dummyObj);
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false if object is not empty', async () => { 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); result = vm.isEmpty(dummyObj);
expect(result).toBe(false); expect(result).toBe(false);
@ -158,29 +158,31 @@ describe('CrudModel', () => {
it('should return true if array is empty', async () => { it('should return true if array is empty', async () => {
dummyArray = []; dummyArray = [];
result = vm.isEmpty(dummyArray); result = vm.isEmpty(dummyArray);
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false if array is not empty', async () => { it('should return false if array is not empty', async () => {
dummyArray = [1,2,3]; dummyArray = [1, 2, 3];
result = vm.isEmpty(dummyArray); result = vm.isEmpty(dummyArray);
expect(result).toBe(false); expect(result).toBe(false);
}) });
}); });
describe('resetData()', () => { describe('resetData()', () => {
it('should add $index to elements in data[] and sets originalData and formData with data', async () => { it('should add $index to elements in data[] and sets originalData and formData with data', async () => {
data = [{ data = [
name: 'Tony', {
lastName: 'Stark', name: 'Tony',
age: 42, lastName: 'Stark',
}]; age: 42,
},
];
vm.resetData(data); vm.resetData(data);
expect(vm.originalData).toEqual(data); expect(vm.originalData).toEqual(data);
expect(vm.originalData[0].$index).toEqual(0); expect(vm.originalData[0].$index).toEqual(0);
expect(vm.formData).toEqual(data); expect(vm.formData).toEqual(data);
@ -200,7 +202,7 @@ describe('CrudModel', () => {
lastName: 'Stark', lastName: 'Stark',
age: 42, age: 42,
}; };
vm.resetData(data); vm.resetData(data);
expect(vm.originalData).toEqual(data); expect(vm.originalData).toEqual(data);
@ -210,17 +212,19 @@ describe('CrudModel', () => {
}); });
describe('saveChanges()', () => { describe('saveChanges()', () => {
data = [{ data = [
name: 'Tony', {
lastName: 'Stark', name: 'Tony',
age: 42, lastName: 'Stark',
}]; age: 42,
},
];
it('should call saveFn if exists', async () => { it('should call saveFn if exists', async () => {
await wrapper.setProps({ saveFn: vi.fn() }); await wrapper.setProps({ saveFn: vi.fn() });
vm.saveChanges(data); vm.saveChanges(data);
expect(vm.saveFn).toHaveBeenCalledOnce(); expect(vm.saveFn).toHaveBeenCalledOnce();
expect(vm.isLoading).toBe(false); expect(vm.isLoading).toBe(false);
expect(vm.hasChanges).toBe(false); expect(vm.hasChanges).toBe(false);
@ -229,13 +233,15 @@ describe('CrudModel', () => {
}); });
it("should use default url if there's not saveFn", async () => { it("should use default url if there's not saveFn", async () => {
const postMock =vi.spyOn(axios, 'post'); const postMock = vi.spyOn(axios, 'post');
vm.formData = [{ vm.formData = [
name: 'Bruce', {
lastName: 'Wayne', name: 'Bruce',
age: 45, lastName: 'Wayne',
}] age: 45,
},
];
await vm.saveChanges(data); await vm.saveChanges(data);

View File

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

View File

@ -1,10 +1,9 @@
<script setup> <script setup>
import { onBeforeMount } from 'vue'; import { onBeforeMount } from 'vue';
import { useRouter, onBeforeRouteUpdate } from 'vue-router'; import { useRouter, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize'; import useCardSize from 'src/composables/useCardSize';
import LeftMenu from 'components/LeftMenu.vue';
import VnSubToolbar from '../ui/VnSubToolbar.vue'; import VnSubToolbar from '../ui/VnSubToolbar.vue';
const props = defineProps({ const props = defineProps({
@ -27,7 +26,13 @@ const arrayData = useArrayData(props.dataKey, {
oneRecord: true, oneRecord: true,
}); });
onBeforeRouteLeave(() => {
stateStore.cardDescriptorChangeValue(null);
});
onBeforeMount(async () => { onBeforeMount(async () => {
stateStore.cardDescriptorChangeValue(props.descriptor);
const route = router.currentRoute.value; const route = router.currentRoute.value;
try { try {
await fetch(route.params.id); await fetch(route.params.id);
@ -62,11 +67,6 @@ function hasRouteParam(params, valueToCheck = ':addressId') {
} }
</script> </script>
<template> <template>
<Teleport to="#left-panel" v-if="stateStore.isHeaderMounted()">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</Teleport>
<VnSubToolbar /> <VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]"> <div :class="[useCardSize(), $attrs.class]">
<RouterView :key="$route.path" /> <RouterView :key="$route.path" />

View File

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

View File

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

View File

@ -12,7 +12,7 @@ const $props = defineProps({
}, },
}); });
onMounted( onMounted(
() => (stateStore.leftDrawer = useQuasar().screen.gt.xs ? $props.leftDrawer : false) () => (stateStore.leftDrawer = useQuasar().screen.gt.xs ? $props.leftDrawer : false),
); );
const teleportRef = ref({}); const teleportRef = ref({});
@ -35,8 +35,14 @@ onMounted(() => {
<template> <template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit text-grey-8">
<div id="left-panel" ref="teleportRef"></div> <div id="left-panel" ref="teleportRef">
<LeftMenu v-if="!hasContent" /> <template v-if="stateStore.cardDescriptor">
<component :is="stateStore.cardDescriptor" />
<QSeparator />
<LeftMenu source="card" />
</template>
<template v-else> <LeftMenu /></template>
</div>
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>

View File

@ -76,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() { async function getData() {
store.url = $props.url; store.url = $props.url;
store.filter = $props.filter ?? {}; store.filter = $props.filter ?? {};
@ -154,9 +163,7 @@ const toModule = computed(() =>
{{ t('components.smartCard.openSummary') }} {{ t('components.smartCard.openSummary') }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<RouterLink <RouterLink :to="{ name: routeName, params: { id: entity.id } }">
:to="{ name: `${dataKey}Summary`, params: { id: entity.id } }"
>
<QBtn <QBtn
class="link" class="link"
color="white" color="white"
@ -196,7 +203,6 @@ const toModule = computed(() =>
<QItemLabel class="subtitle"> <QItemLabel class="subtitle">
#{{ getValueFromPath(subtitle) ?? entity.id }} #{{ getValueFromPath(subtitle) ?? entity.id }}
</QItemLabel> </QItemLabel>
<QBtn <QBtn
round round
flat flat
@ -210,7 +216,6 @@ const toModule = computed(() =>
{{ t('globals.copyId') }} {{ t('globals.copyId') }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<!-- </QItemLabel> -->
</QItem> </QItem>
</QList> </QList>
<div class="list-box q-mt-xs"> <div class="list-box q-mt-xs">
@ -220,7 +225,7 @@ const toModule = computed(() =>
<div class="icons"> <div class="icons">
<slot name="icons" :entity="entity" /> <slot name="icons" :entity="entity" />
</div> </div>
<div class="actions justify-center"> <div class="actions justify-center" data-cy="descriptor_actions">
<slot name="actions" :entity="entity" /> <slot name="actions" :entity="entity" />
</div> </div>
<slot name="after" /> <slot name="after" />

View File

@ -27,6 +27,7 @@ const $attrs = computed(() => {
const $props = defineProps({ const $props = defineProps({
url: { type: String, default: null }, url: { type: String, default: null },
saveUrl: { type: String, default: null }, saveUrl: { type: String, default: null },
userFilter: { type: Object, default: () => {} },
filter: { type: Object, default: () => {} }, filter: { type: Object, default: () => {} },
body: { type: Object, default: () => {} }, body: { type: Object, default: () => {} },
addNote: { type: Boolean, default: false }, addNote: { type: Boolean, default: false },
@ -183,7 +184,8 @@ function fetchData([data]) {
:url="$props.url" :url="$props.url"
order="created DESC" order="created DESC"
:limit="0" :limit="0"
:user-filter="$props.filter" :user-filter="userFilter"
:filter="filter"
auto-load auto-load
ref="vnPaginateRef" ref="vnPaginateRef"
class="show" class="show"

View File

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

View File

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

View File

@ -153,6 +153,7 @@ globals:
maxTemperature: Max maxTemperature: Max
minTemperature: Min minTemperature: Min
changePass: Change password changePass: Change password
setPass: Set password
deleteConfirmTitle: Delete selected elements deleteConfirmTitle: Delete selected elements
changeState: Change state changeState: Change state
raid: 'Raid {daysInForward} days' raid: 'Raid {daysInForward} days'
@ -693,8 +694,10 @@ worker:
machine: Machine machine: Machine
business: business:
tableVisibleColumns: tableVisibleColumns:
id: ID
started: Start Date started: Start Date
ended: End Date ended: End Date
hourlyLabor: Time sheet
company: Company company: Company
reasonEnd: Reason for Termination reasonEnd: Reason for Termination
department: Department department: Department
@ -702,6 +705,7 @@ worker:
calendarType: Work Calendar calendarType: Work Calendar
workCenter: Work Center workCenter: Work Center
payrollCategories: Contract Category payrollCategories: Contract Category
workerBusinessAgreementName: Agreement
occupationCode: Contribution Code occupationCode: Contribution Code
rate: Rate rate: Rate
businessType: Contract Type businessType: Contract Type

View File

@ -157,6 +157,7 @@ globals:
maxTemperature: Máx maxTemperature: Máx
minTemperature: Mín minTemperature: Mín
changePass: Cambiar contraseña changePass: Cambiar contraseña
setPass: Establecer contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado changeState: Cambiar estado
raid: 'Redada {daysInForward} días' raid: 'Redada {daysInForward} días'
@ -769,8 +770,10 @@ worker:
concept: Concepto concept: Concepto
business: business:
tableVisibleColumns: tableVisibleColumns:
id: Id
started: Fecha inicio started: Fecha inicio
ended: Fecha fin ended: Fecha fin
hourlyLabor: Ficha
company: Empresa company: Empresa
reasonEnd: Motivo finalización reasonEnd: Motivo finalización
department: Departamento department: Departamento
@ -781,12 +784,13 @@ worker:
occupationCode: Cotización occupationCode: Cotización
rate: Tarifa rate: Tarifa
businessType: Contrato businessType: Contrato
workerBusinessAgreementName: Convenio
amount: Salario amount: Salario
basicSalary: Salario transportistas basicSalary: Salario transportistas
notes: Notas notes: Notas
operator: operator:
numberOfWagons: Número de vagones numberOfWagons: Número de vagones
train: tren train: Tren
itemPackingType: Tipo de embalaje itemPackingType: Tipo de embalaje
warehouse: Almacén warehouse: Almacén
sector: Sector sector: Sector

View File

@ -25,12 +25,13 @@ const $props = defineProps({
const { t } = useI18n(); const { t } = useI18n();
const { hasAccount } = toRefs($props); const { hasAccount } = toRefs($props);
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const arrayData = useArrayData('Account');
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const { notify } = useQuasar(); const { notify } = useQuasar();
const account = computed(() => useArrayData('Account').store.data[0]); const account = computed(() => arrayData.store.data);
account.value.hasAccount = hasAccount.value; account.value.hasAccount = hasAccount.value;
const entityId = computed(() => +route.params.id); const entityId = computed(() => +route.params.id);
const hasitManagementAccess = ref(); const hasitManagementAccess = ref();
@ -39,7 +40,7 @@ const isHimself = computed(() => user.value.id === account.value.id);
const url = computed(() => const url = computed(() =>
isHimself.value isHimself.value
? 'Accounts/change-password' ? 'Accounts/change-password'
: `Accounts/${entityId.value}/setPassword` : `Accounts/${entityId.value}/setPassword`,
); );
async function updateStatusAccount(active) { async function updateStatusAccount(active) {
@ -153,6 +154,7 @@ onMounted(() => {
t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.title'),
t('account.card.actions.disableAccount.subtitle'), t('account.card.actions.disableAccount.subtitle'),
() => deleteAccount(), () => deleteAccount(),
() => deleteAccount(),
) )
" "
> >
@ -172,6 +174,7 @@ onMounted(() => {
t('account.card.actions.enableAccount.title'), t('account.card.actions.enableAccount.title'),
t('account.card.actions.enableAccount.subtitle'), t('account.card.actions.enableAccount.subtitle'),
() => updateStatusAccount(true), () => updateStatusAccount(true),
() => updateStatusAccount(true),
) )
" "
> >
@ -186,6 +189,7 @@ onMounted(() => {
t('account.card.actions.disableAccount.title'), t('account.card.actions.disableAccount.title'),
t('account.card.actions.disableAccount.subtitle'), t('account.card.actions.disableAccount.subtitle'),
() => updateStatusAccount(false), () => updateStatusAccount(false),
() => updateStatusAccount(false),
) )
" "
> >
@ -201,6 +205,7 @@ onMounted(() => {
t('account.card.actions.activateUser.title'), t('account.card.actions.activateUser.title'),
t('account.card.actions.activateUser.title'), t('account.card.actions.activateUser.title'),
() => updateStatusUser(true), () => updateStatusUser(true),
() => updateStatusUser(true),
) )
" "
> >
@ -215,6 +220,7 @@ onMounted(() => {
t('account.card.actions.deactivateUser.title'), t('account.card.actions.deactivateUser.title'),
t('account.card.actions.deactivateUser.title'), t('account.card.actions.deactivateUser.title'),
() => updateStatusUser(false), () => updateStatusUser(false),
() => updateStatusUser(false),
) )
" "
> >

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, useAttrs } from 'vue'; import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import VnNotes from 'src/components/ui/VnNotes.vue'; import VnNotes from 'src/components/ui/VnNotes.vue';
@ -7,7 +7,6 @@ import VnNotes from 'src/components/ui/VnNotes.vue';
const route = useRoute(); const route = useRoute();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const $attrs = useAttrs();
const $props = defineProps({ const $props = defineProps({
id: { type: [Number, String], default: null }, id: { type: [Number, String], default: null },
@ -15,24 +14,21 @@ const $props = defineProps({
}); });
const claimId = computed(() => $props.id || route.params.id); const claimId = computed(() => $props.id || route.params.id);
const claimFilter = computed(() => { const claimFilter = {
return { fields: ['id', 'created', 'workerFk', 'text'],
where: { claimFk: claimId.value }, include: {
fields: ['id', 'created', 'workerFk', 'text'], relation: 'worker',
include: { scope: {
relation: 'worker', fields: ['id', 'firstName', 'lastName'],
scope: { include: {
fields: ['id', 'firstName', 'lastName'], relation: 'user',
include: { scope: {
relation: 'user', fields: ['id', 'nickname', 'name'],
scope: {
fields: ['id', 'nickname', 'name'],
},
}, },
}, },
}, },
}; },
}); };
const body = { const body = {
claimFk: claimId.value, claimFk: claimId.value,
@ -43,7 +39,8 @@ const body = {
<VnNotes <VnNotes
url="claimObservations" url="claimObservations"
:add-note="$props.addNote" :add-note="$props.addNote"
:filter="claimFilter" :user-filter="claimFilter"
:filter="{ where: { claimFk: claimId } }"
:body="body" :body="body"
v-bind="$attrs" v-bind="$attrs"
style="overflow-y: auto" style="overflow-y: auto"

View File

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

View File

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

View File

@ -106,7 +106,6 @@ const props = defineProps({
:label="t('claim.zone')" :label="t('claim.zone')"
v-model="params.zoneFk" v-model="params.zoneFk"
url="Zones" url="Zones"
:use-like="false"
outlined outlined
rounded rounded
dense dense

View File

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

View File

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

View File

@ -1,28 +1,15 @@
<script setup> <script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import VnNotes from 'src/components/ui/VnNotes.vue'; import VnNotes from 'src/components/ui/VnNotes.vue';
const route = useRoute();
const noteFilter = computed(() => {
return {
order: 'created DESC',
where: {
clientFk: `${route.params.id}`,
},
};
});
</script> </script>
<template> <template>
<VnNotes <VnNotes
url="clientObservations" url="clientObservations"
:add-note="true" :add-note="true"
:filter="noteFilter" :filter="{ where: { clientFk: $route.params.id } }"
:body="{ clientFk: route.params.id }" :body="{ clientFk: $route.params.id }"
style="overflow-y: auto" style="overflow-y: auto"
:select-type="true" :select-type="true"
required required
order="created DESC"
/> />
</template> </template>

View File

@ -325,7 +325,7 @@ const sumRisk = ({ clientRisks }) => {
</QCard> </QCard>
<QCard class="vn-max"> <QCard class="vn-max">
<VnTitle :text="t('Latest tickets')" /> <VnTitle :text="t('Latest tickets')" />
<CustomerSummaryTable /> <CustomerSummaryTable :id="entityId" />
</QCard> </QCard>
</template> </template>
</CardSummary> </CardSummary>

View File

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

View File

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

View File

@ -77,7 +77,6 @@ onBeforeMount(() => {
function setPaymentType(accounting) { function setPaymentType(accounting) {
if (!accounting) return; if (!accounting) return;
accountingType.value = accounting.accountingType; accountingType.value = accounting.accountingType;
initialData.description = []; initialData.description = [];
initialData.payed = Date.vnNew(); initialData.payed = Date.vnNew();
isCash.value = accountingType.value.code == 'cash'; isCash.value = accountingType.value.code == 'cash';
@ -87,14 +86,14 @@ function setPaymentType(accounting) {
initialData.payed.getDate() + accountingType.value.daysInFuture, initialData.payed.getDate() + accountingType.value.daysInFuture,
); );
maxAmount.value = accountingType.value && accountingType.value.maxAmount; maxAmount.value = accountingType.value && accountingType.value.maxAmount;
if (accountingType.value.code == 'compensation') if (accountingType.value.code == 'compensation')
return (initialData.description = ''); 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) => { const calculateFromAmount = (event) => {

View File

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

View File

@ -20,7 +20,12 @@ const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const $props = defineProps({
id: {
type: Number,
default: null,
},
});
const filter = { const filter = {
include: [ include: [
{ {
@ -43,7 +48,7 @@ const filter = {
}, },
}, },
], ],
where: { clientFk: route.params.id }, where: { clientFk: $props.id ?? route.params.id },
order: ['shipped DESC', 'id'], order: ['shipped DESC', 'id'],
limit: 30, limit: 30,
}; };

View File

@ -17,9 +17,9 @@ describe('getAddresses', () => {
expect(axios.get).toHaveBeenCalledWith(`Clients/${clientId}/addresses`, { expect(axios.get).toHaveBeenCalledWith(`Clients/${clientId}/addresses`, {
params: { params: {
filter: JSON.stringify({ filter: JSON.stringify({
fields: ['nickname', 'street', 'city', 'id'], fields: ['nickname', 'street', 'city', 'id', 'isActive'],
where: { isActive: true }, where: { isActive: true },
order: 'nickname ASC', order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'],
}), }),
}, },
}); });
@ -30,4 +30,4 @@ describe('getAddresses', () => {
expect(axios.get).not.toHaveBeenCalled(); expect(axios.get).not.toHaveBeenCalled();
}); });
}); });

View File

@ -1,15 +1,15 @@
import axios from 'axios'; import axios from 'axios';
export async function getAddresses(clientId, _filter = {}) { export async function getAddresses(clientId, _filter = {}) {
if (!clientId) return; if (!clientId) return;
const filter = { const filter = {
..._filter, ..._filter,
fields: ['nickname', 'street', 'city', 'id'], fields: ['nickname', 'street', 'city', 'id', 'isActive'],
where: { isActive: true }, where: { isActive: true },
order: 'nickname ASC', order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'],
}; };
const params = { filter: JSON.stringify(filter) }; const params = { filter: JSON.stringify(filter) };
return await axios.get(`Clients/${clientId}/addresses`, { return await axios.get(`Clients/${clientId}/addresses`, {
params, params,
}); });
}; }

View File

@ -54,6 +54,7 @@ const columns = [
toggleIndeterminate: false, toggleIndeterminate: false,
}, },
create: true, create: true,
createOrder: 12,
width: '25px', width: '25px',
}, },
{ {
@ -61,9 +62,10 @@ const columns = [
name: 'workerFk', name: 'workerFk',
component: 'select', component: 'select',
attrs: { attrs: {
url: 'Workers/search', url: 'TicketRequests/getItemTypeWorker',
fields: ['id', 'nickname'], fields: ['id', 'nickname'],
optionLabel: 'nickname', optionLabel: 'nickname',
sortBy: 'nickname ASC',
optionValue: 'id', optionValue: 'id',
}, },
visible: false, visible: false,
@ -87,15 +89,6 @@ const columns = [
isEditable: false, isEditable: false,
columnFilter: false, columnFilter: false,
}, },
{
name: 'entryFk',
isId: true,
visible: false,
isEditable: false,
disable: true,
create: true,
columnFilter: false,
},
{ {
align: 'center', align: 'center',
label: 'Id', label: 'Id',
@ -137,6 +130,7 @@ const columns = [
name: 'itemFk', name: 'itemFk',
visible: false, visible: false,
create: true, create: true,
createOrder: 0,
columnFilter: false, columnFilter: false,
}, },
{ {
@ -160,6 +154,8 @@ const columns = [
name: 'stickers', name: 'stickers',
component: 'input', component: 'input',
create: true, create: true,
createOrder: 1,
attrs: { attrs: {
positive: false, positive: false,
}, },
@ -271,6 +267,7 @@ const columns = [
}, },
width: '45px', width: '45px',
create: true, create: true,
createOrder: 3,
style: getQuantityStyle, style: getQuantityStyle,
}, },
{ {
@ -280,6 +277,7 @@ const columns = [
toolTip: t('Buying value'), toolTip: t('Buying value'),
name: 'buyingValue', name: 'buyingValue',
create: true, create: true,
createOrder: 2,
component: 'number', component: 'number',
attrs: { attrs: {
positive: false, positive: false,
@ -312,6 +310,7 @@ const columns = [
toolTip: t('Package'), toolTip: t('Package'),
name: 'price2', name: 'price2',
component: 'number', component: 'number',
createDisable: true,
width: '35px', width: '35px',
create: true, create: true,
format: (row) => parseFloat(row['price2']).toFixed(2), format: (row) => parseFloat(row['price2']).toFixed(2),
@ -321,6 +320,7 @@ const columns = [
label: t('Box'), label: t('Box'),
name: 'price3', name: 'price3',
component: 'number', component: 'number',
createDisable: true,
cellEvent: { cellEvent: {
'update:modelValue': async (value, oldValue, row) => { 'update:modelValue': async (value, oldValue, row) => {
row['price2'] = row['price2'] * (value / oldValue); row['price2'] = row['price2'] * (value / oldValue);
@ -508,13 +508,14 @@ async function setBuyUltimate(itemFk, data) {
}, },
}); });
const buyUltimateData = buyUltimate.data[0]; const buyUltimateData = buyUltimate.data[0];
if (!buyUltimateData) return;
const allowedKeys = columns const allowedKeys = columns
.filter((col) => col.create === true) .filter((col) => col.create === true)
.map((col) => col.name); .map((col) => col.name);
allowedKeys.forEach((key) => { allowedKeys.forEach((key) => {
if (buyUltimateData.hasOwnProperty(key) && key !== 'entryFk') { if (buyUltimateData?.hasOwnProperty(key) && key !== 'entryFk') {
if (!['stickers', 'quantity'].includes(key)) data[key] = buyUltimateData[key]; if (!['stickers', 'quantity'].includes(key)) data[key] = buyUltimateData[key];
} }
}); });
@ -607,6 +608,7 @@ onMounted(() => {
ref="entryBuysRef" ref="entryBuysRef"
data-key="EntryBuys" data-key="EntryBuys"
:url="`Entries/${entityId}/getBuyList`" :url="`Entries/${entityId}/getBuyList`"
search-url="EntryBuys"
save-url="Buys/crud" save-url="Buys/crud"
:disable-option="{ card: true }" :disable-option="{ card: true }"
v-model:selected="selectedRows" v-model:selected="selectedRows"
@ -636,22 +638,24 @@ onMounted(() => {
isFullWidth: true, isFullWidth: true,
containerStyle: { containerStyle: {
display: 'flex', display: 'flex',
'flex-wrap': 'wrap',
gap: '16px', gap: '16px',
position: 'relative', position: 'relative',
height: '450px',
}, },
columnGridStyle: { columnGridStyle: {
'max-width': '50%', 'max-width': '50%',
flex: 1,
'margin-right': '30px', 'margin-right': '30px',
flex: 1,
}, },
previousStyle: {
'max-width': '30%',
height: '500px',
},
displayPrevious: true,
}" }"
:is-editable="editableMode" :is-editable="editableMode"
:without-header="!editableMode" :without-header="!editableMode"
:with-filters="editableMode" :with-filters="editableMode"
:right-search="true" :right-search="editableMode"
:right-search-icon="true"
:row-click="false" :row-click="false"
:columns="columns" :columns="columns"
:beforeSaveFn="beforeSave" :beforeSaveFn="beforeSave"
@ -660,6 +664,7 @@ onMounted(() => {
auto-load auto-load
footer footer
data-cy="entry-buys" data-cy="entry-buys"
overlay
> >
<template #column-hex="{ row }"> <template #column-hex="{ row }">
<VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" /> <VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" />

View File

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

View File

@ -145,6 +145,7 @@ const entryFilterPanel = ref();
v-model="params.agencyModeId" v-model="params.agencyModeId"
@update:model-value="searchFn()" @update:model-value="searchFn()"
url="AgencyModes" url="AgencyModes"
sort-by="name ASC"
:fields="['id', 'name']" :fields="['id', 'name']"
hide-selected hide-selected
dense dense
@ -248,7 +249,7 @@ const entryFilterPanel = ref();
<i18n> <i18n>
en: en:
params: params:
isExcludedFromAvailable: Inventory isExcludedFromAvailable: Is excluded
isOrdered: Ordered isOrdered: Ordered
isReceived: Received isReceived: Received
isConfirmed: Confirmed isConfirmed: Confirmed

View File

@ -11,6 +11,8 @@ import VnTable from 'components/VnTable/VnTable.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue'; import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import EntrySummary from './Card/EntrySummary.vue';
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref(); const tableRef = ref();
@ -18,6 +20,7 @@ const defaultEntry = ref({});
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const dataKey = 'EntryList'; const dataKey = 'EntryList';
const { viewSummary } = useSummaryDialog();
const entryQueryFilter = { const entryQueryFilter = {
include: [ include: [
@ -222,6 +225,19 @@ const columns = computed(() => [
visible: false, visible: false,
create: true, create: true,
}, },
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
isPrimary: true,
action: (row) => viewSummary(row.id, EntrySummary, 'xlg-width'),
},
],
},
]); ]);
function getBadgeAttrs(row) { function getBadgeAttrs(row) {
const date = row.landed; const date = row.landed;
@ -267,16 +283,7 @@ onBeforeMount(async () => {
</script> </script>
<template> <template>
<VnSection <VnSection :data-key="dataKey" prefix="entry">
:data-key="dataKey"
prefix="entry"
url="Entries/filter"
:array-data-props="{
url: 'Entries/filter',
order: 'landed DESC',
userFilter: EntryFilter,
}"
>
<template #advanced-menu> <template #advanced-menu>
<EntryFilter :data-key="dataKey" /> <EntryFilter :data-key="dataKey" />
</template> </template>
@ -285,6 +292,7 @@ onBeforeMount(async () => {
v-if="defaultEntry.defaultSupplierFk" v-if="defaultEntry.defaultSupplierFk"
ref="tableRef" ref="tableRef"
:data-key="dataKey" :data-key="dataKey"
search-url="EntryList"
url="Entries/filter" url="Entries/filter"
:filter="entryQueryFilter" :filter="entryQueryFilter"
order="landed DESC" order="landed DESC"

View File

@ -19,6 +19,7 @@ const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const footer = ref({ bought: 0, reserve: 0 });
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
@ -38,16 +39,14 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
create: true, create: true,
attrs: { attrs: {
url: 'Workers/activeWithInheritedRole', url: 'TicketRequests/getItemTypeWorker',
fields: ['id', 'name', 'nickname'], fields: ['id', 'nickname'],
where: { role: 'buyer' },
optionFilter: 'firstName',
optionLabel: 'nickname', optionLabel: 'nickname',
sortBy: 'nickname ASC',
optionValue: 'id', optionValue: 'id',
useLike: false,
}, },
columnFilter: false, columnFilter: false,
width: '70px', width: '50px',
}, },
{ {
align: 'center', align: 'center',
@ -58,6 +57,7 @@ const columns = computed(() => [
component: 'number', component: 'number',
summation: true, summation: true,
width: '50px', width: '50px',
format: ({ reserve }, dashIfEmpty) => dashIfEmpty(round(reserve)),
}, },
{ {
align: 'center', align: 'center',
@ -65,6 +65,7 @@ const columns = computed(() => [
name: 'bought', name: 'bought',
summation: true, summation: true,
cardVisible: true, cardVisible: true,
style: ({ reserve, bought }) => boughtStyle(bought, reserve),
columnFilter: false, columnFilter: false,
}, },
{ {
@ -95,7 +96,6 @@ const columns = computed(() => [
}, },
}, },
], ],
'data-cy': 'table-actions',
}, },
]); ]);
@ -137,20 +137,20 @@ function openDialog() {
} }
function setFooter(data) { function setFooter(data) {
const footer = { footer.value = { bought: 0, reserve: 0 };
bought: 0,
reserve: 0,
};
data.forEach((row) => { data.forEach((row) => {
footer.bought += row?.bought; footer.value.bought += row?.bought;
footer.reserve += row?.reserve; footer.value.reserve += row?.reserve;
}); });
tableRef.value.footer = footer;
} }
function round(value) { function round(value) {
return Math.round(value * 100) / 100; return Math.round(value * 100) / 100;
} }
function boughtStyle(bought, reserve) {
return reserve < bought ? { color: 'var(--q-negative)' } : '';
}
</script> </script>
<template> <template>
<VnSubToolbar> <VnSubToolbar>
@ -253,24 +253,14 @@ function round(value) {
<WorkerDescriptorProxy :id="row?.workerFk" /> <WorkerDescriptorProxy :id="row?.workerFk" />
</span> </span>
</template> </template>
<template #column-bought="{ row }">
<span :class="{ 'text-negative': row.reserve < row.bought }">
{{ row?.bought }}
</span>
</template>
<template #column-footer-reserve> <template #column-footer-reserve>
<span> <span>
{{ round(tableRef.footer.reserve) }} {{ round(footer.reserve) }}
</span> </span>
</template> </template>
<template #column-footer-bought> <template #column-footer-bought>
<span <span :style="boughtStyle(footer?.bought, footer?.reserve)">
:class="{ {{ round(footer.bought) }}
'text-negative':
tableRef.footer.reserve < tableRef.footer.bought,
}"
>
{{ round(tableRef.footer.bought) }}
</span> </span>
</template> </template>
</VnTable> </VnTable>
@ -286,7 +276,7 @@ function round(value) {
justify-content: center; justify-content: center;
} }
.column { .column {
min-width: 40%; min-width: 35%;
margin-top: 5%; margin-top: 5%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -14,7 +14,7 @@ const $props = defineProps({
required: true, required: true,
}, },
dated: { dated: {
type: Date, type: [Date, String],
required: true, required: true,
}, },
}); });

View File

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

View File

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

View File

@ -163,10 +163,14 @@ const showExportationLetter = () => {
<QMenu anchor="top end" self="top start"> <QMenu anchor="top end" self="top start">
<QList> <QList>
<QItem v-ripple clickable @click="showSendInvoiceDialog('pdf')"> <QItem v-ripple clickable @click="showSendInvoiceDialog('pdf')">
<QItemSection>{{ t('Send PDF') }}</QItemSection> <QItemSection data-cy="InvoiceOutDescriptorMenuSendPdfOption">
{{ t('Send PDF') }}
</QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable @click="showSendInvoiceDialog('csv')"> <QItem v-ripple clickable @click="showSendInvoiceDialog('csv')">
<QItemSection>{{ t('Send CSV') }}</QItemSection> <QItemSection data-cy="InvoiceOutDescriptorMenuSendCsvOption">
{{ t('Send CSV') }}
</QItemSection>
</QItem> </QItem>
</QList> </QList>
</QMenu> </QMenu>

View File

@ -21,7 +21,6 @@ import VnSection from 'src/components/common/VnSection.vue';
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const tableRef = ref(); const tableRef = ref();
const invoiceOutSerialsOptions = ref([]);
const customerOptions = ref([]); const customerOptions = ref([]);
const selectedRows = ref([]); const selectedRows = ref([]);
const hasSelectedCards = computed(() => selectedRows.value.length > 0); const hasSelectedCards = computed(() => selectedRows.value.length > 0);
@ -368,7 +367,6 @@ watchEffect(selectedRows);
url="InvoiceOutSerials" url="InvoiceOutSerials"
v-model="data.serial" v-model="data.serial"
:label="t('invoiceOutModule.serial')" :label="t('invoiceOutModule.serial')"
:options="invoiceOutSerialsOptions"
option-label="description" option-label="description"
option-value="code" option-value="code"
option-filter option-filter

View File

@ -2,6 +2,7 @@ invoiceOut:
search: Search invoice search: Search invoice
searchInfo: You can search by invoice reference searchInfo: You can search by invoice reference
params: params:
id: ID
company: Company company: Company
country: Country country: Country
clientId: Client clientId: Client
@ -24,6 +25,7 @@ invoiceOut:
min: Min min: Min
max: Max max: Max
hasPdf: Has PDF hasPdf: Has PDF
search: Contains
card: card:
issued: Issued issued: Issued
customerCard: Customer card customerCard: Customer card

View File

@ -2,6 +2,7 @@ invoiceOut:
search: Buscar factura emitida search: Buscar factura emitida
searchInfo: Puedes buscar por referencia de la factura searchInfo: Puedes buscar por referencia de la factura
params: params:
id: ID
company: Empresa company: Empresa
country: País country: País
clientId: Cliente clientId: Cliente
@ -24,6 +25,7 @@ invoiceOut:
min: Min min: Min
max: Max max: Max
hasPdf: Tiene PDF hasPdf: Tiene PDF
search: Contiene
card: card:
issued: Fecha emisión issued: Fecha emisión
customerCard: Ficha del cliente customerCard: Ficha del cliente

View File

@ -120,22 +120,9 @@ const updateStock = async () => {
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('globals.producer')" :value="dashIfEmpty(entity.subName)" /> <VnLv :label="t('globals.producer')" :value="dashIfEmpty(entity.subName)" />
<VnLv <VnLv v-if="entity?.value5" :label="entity?.tag5" :value="entity.value5" />
v-if="entity.value5" <VnLv v-if="entity?.value6" :label="entity?.tag6" :value="entity.value6" />
:label="t('item.descriptor.color')" <VnLv v-if="entity?.value7" :label="entity?.tag7" :value="entity.value7" />
:value="entity.value5"
>
</VnLv>
<VnLv
v-if="entity.value6"
:label="t('item.descriptor.category')"
:value="entity.value6"
/>
<VnLv
v-if="entity.value7"
:label="t('item.list.stems')"
:value="entity.value7"
/>
</template> </template>
<template #icons="{ entity }"> <template #icons="{ entity }">
<QCardActions v-if="entity" class="q-gutter-x-md"> <QCardActions v-if="entity" class="q-gutter-x-md">

View File

@ -12,7 +12,7 @@ import FetchData from 'components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue';
import { toDateFormat } from 'src/filters/date.js'; import { toDateTimeFormat } from 'src/filters/date.js';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { date } from 'quasar'; import { date } from 'quasar';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
@ -27,7 +27,7 @@ const user = state.getUser();
const today = Date.vnNew(); const today = Date.vnNew();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
const warehousesOptions = ref([]); const warehousesOptions = ref([]);
const itemBalances = computed(() => arrayDataItemBalances.store.data); const itemBalances = computed(() => arrayDataItemBalances.store.data || []);
const where = computed(() => arrayDataItemBalances.store.filter.where || {}); const where = computed(() => arrayDataItemBalances.store.filter.where || {});
const showWhatsBeforeInventory = ref(false); const showWhatsBeforeInventory = ref(false);
const inventoriedDate = ref(null); const inventoriedDate = ref(null);
@ -143,7 +143,12 @@ onMounted(async () => {
const fetchItemBalances = async () => await arrayDataItemBalances.fetch({}); const fetchItemBalances = async () => await arrayDataItemBalances.fetch({});
const getBadgeAttrs = (_date) => { const getBadgeAttrs = (_date) => {
const isSameDate = date.isSameDate(today, _date); let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(_date);
timeTicket.setHours(0, 0, 0, 0);
const isSameDate = date.isSameDate(today, timeTicket);
const attrs = { const attrs = {
'text-color': isSameDate ? 'black' : 'white', 'text-color': isSameDate ? 'black' : 'white',
color: isSameDate ? 'warning' : 'transparent', color: isSameDate ? 'warning' : 'transparent',
@ -153,15 +158,10 @@ const getBadgeAttrs = (_date) => {
const scrollToToday = async () => { const scrollToToday = async () => {
await nextTick(); await nextTick();
const todayCell = document.querySelector(`td[data-date="${today.toISOString()}"]`); const todayCell = document.querySelector(
if (todayCell) { `td[data-date="${date.formatDate(today, 'YYYY-MM-DD')}"]`,
todayCell.scrollIntoView({ behavior: 'smooth', block: 'center' }); );
} if (todayCell) todayCell.scrollIntoView({ behavior: 'smooth', block: 'center' });
};
const formatDateForAttribute = (dateValue) => {
if (dateValue instanceof Date) return date.formatDate(dateValue, 'YYYY-MM-DD');
return dateValue;
}; };
async function updateWarehouse(warehouseFk) { async function updateWarehouse(warehouseFk) {
@ -237,14 +237,14 @@ async function updateWarehouse(warehouseFk) {
</QTd> </QTd>
</template> </template>
<template #body-cell-date="{ row }"> <template #body-cell-date="{ row }">
<QTd @click.stop :data-date="formatDateForAttribute(row.shipped)"> <QTd @click.stop :data-date="row?.shipped.substring(0, 10)">
<QBadge <QBadge
v-bind="getBadgeAttrs(row.shipped)" v-bind="getBadgeAttrs(row.shipped)"
class="q-ma-none" class="q-ma-none"
dense dense
style="font-size: 14px" style="font-size: 14px"
> >
{{ toDateFormat(row.shipped) }} {{ toDateTimeFormat(row.shipped) }}
</QBadge> </QBadge>
</QTd> </QTd>
</template> </template>
@ -313,8 +313,8 @@ async function updateWarehouse(warehouseFk) {
row.lineFk == row.lastPreparedLineFk row.lineFk == row.lastPreparedLineFk
? 'black' ? 'black'
: row.balance < 0 : row.balance < 0
? 'negative' ? 'negative'
: '' : ''
" "
dense dense
style="font-size: 14px" style="font-size: 14px"

View File

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

View File

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

View File

@ -17,6 +17,7 @@ import MonitorTicketFilter from './MonitorTicketFilter.vue';
import TicketProblems from 'src/components/TicketProblems.vue'; import TicketProblems from 'src/components/TicketProblems.vue';
import VnDateBadge from 'src/components/common/VnDateBadge.vue'; import VnDateBadge from 'src/components/common/VnDateBadge.vue';
import { useStateStore } from 'src/stores/useStateStore'; import { useStateStore } from 'src/stores/useStateStore';
import useOpenURL from 'src/composables/useOpenURL';
const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000; const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000;
const { t } = useI18n(); const { t } = useI18n();
@ -321,8 +322,7 @@ const totalPriceColor = (ticket) => {
if (total > 0 && total < 50) return 'warning'; if (total > 0 && total < 50) return 'warning';
}; };
const openTab = (id) => const openTab = (id) => useOpenURL(`#/ticket/${id}/sale`);
window.open(`#/ticket/${id}/sale`, '_blank', 'noopener, noreferrer');
</script> </script>
<template> <template>
<FetchData <FetchData
@ -397,6 +397,7 @@ const openTab = (id) =>
default-mode="table" default-mode="table"
auto-load auto-load
:row-click="({ id }) => openTab(id)" :row-click="({ id }) => openTab(id)"
:row-ctrl-click="(_, { id }) => openTab(id)"
:disable-option="{ card: true }" :disable-option="{ card: true }"
:user-params="{ from, to, scopeDays: 0 }" :user-params="{ from, to, scopeDays: 0 }"
> >

View File

@ -22,7 +22,7 @@ salesTicketsTable:
notVisible: Not visible notVisible: Not visible
purchaseRequest: Purchase request purchaseRequest: Purchase request
clientFrozen: Client frozen clientFrozen: Client frozen
risk: Risk risk: Excess risk
componentLack: Component lack componentLack: Component lack
tooLittle: Ticket too little tooLittle: Ticket too little
identifier: Identifier identifier: Identifier

View File

@ -22,7 +22,7 @@ salesTicketsTable:
notVisible: No visible notVisible: No visible
purchaseRequest: Petición de compra purchaseRequest: Petición de compra
clientFrozen: Cliente congelado clientFrozen: Cliente congelado
risk: Riesgo risk: Exceso de riesgo
componentLack: Faltan componentes componentLack: Faltan componentes
tooLittle: Ticket demasiado pequeño tooLittle: Ticket demasiado pequeño
identifier: Identificador identifier: Identificador

View File

@ -10,6 +10,7 @@ import OrderCatalogFilter from 'src/pages/Order/Card/OrderCatalogFilter.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import { onUnmounted } from 'vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -23,16 +24,40 @@ const catalogParams = {
const arrayData = useArrayData(dataKey, { const arrayData = useArrayData(dataKey, {
url: 'Orders/CatalogFilter', url: 'Orders/CatalogFilter',
userParams: catalogParams, userParams: catalogParams,
exprBuilder,
searchUrl: 'table',
}); });
const store = arrayData.store; const store = arrayData.store;
const tags = ref([]); const tags = ref([]);
const itemRefs = ref({}); const itemRefs = ref({});
onMounted(() => { onMounted(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
checkOrderConfirmation(); checkOrderConfirmation();
if (
arrayData.store.userParams &&
Object.keys(arrayData.store.userParams).some((key) => !key.startsWith('order'))
) {
await arrayData.fetch({});
}
}); });
onUnmounted(() => {
arrayData.destroy();
});
function exprBuilder(param, value) {
switch (param) {
case 'categoryFk':
case 'typeFk':
return { [param]: value };
case 'search':
if (/^\d+$/.test(value)) return { 'i.id': value };
else return { 'i.name': { like: `%${value}%` } };
}
}
async function checkOrderConfirmation() { async function checkOrderConfirmation() {
const response = await axios.get(`Orders/${route.params.id}`); const response = await axios.get(`Orders/${route.params.id}`);
if (response.data.isConfirmed === 1) { if (response.data.isConfirmed === 1) {
@ -96,6 +121,7 @@ watch(
:tag-value="tagValue" :tag-value="tagValue"
:tags="tags" :tags="tags"
:initial-catalog-params="catalogParams" :initial-catalog-params="catalogParams"
:arrayData
/> />
</template> </template>
</RightMenu> </RightMenu>

View File

@ -24,6 +24,10 @@ const props = defineProps({
type: Array, type: Array,
required: true, required: true,
}, },
arrayData: {
type: Object,
required: true,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -74,17 +78,6 @@ const loadTypes = async (id) => {
typeList.value = data; typeList.value = data;
}; };
function exprBuilder(param, value) {
switch (param) {
case 'categoryFk':
case 'typeFk':
return { [param]: value };
case 'search':
if (/^\d+$/.test(value)) return { 'i.id': value };
else return { 'i.name': { like: `%${value}%` } };
}
}
const applyTags = (tagInfo, params, search) => { const applyTags = (tagInfo, params, search) => {
if (!tagInfo || !tagInfo.values.length) { if (!tagInfo || !tagInfo.values.length) {
params.tagGroups = null; params.tagGroups = null;
@ -152,9 +145,8 @@ function addOrder(value, field, params) {
:data-key="props.dataKey" :data-key="props.dataKey"
:hidden-tags="['filter', 'orderFk', 'orderBy']" :hidden-tags="['filter', 'orderFk', 'orderBy']"
:unremovable-params="['orderFk', 'orderBy']" :unremovable-params="['orderFk', 'orderBy']"
:expr-builder="exprBuilder"
:custom-tags="['tagGroups', 'categoryFk']" :custom-tags="['tagGroups', 'categoryFk']"
:redirect="false" :arrayData
> >
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<strong v-if="tag.label === 'typeFk' && typeList"> <strong v-if="tag.label === 'typeFk' && typeList">

View File

@ -155,11 +155,23 @@ onMounted(() => {
}); });
async function fetchClientAddress(id, formData = {}) { async function fetchClientAddress(id, formData = {}) {
const { data } = await axios.get( const { data } = await axios.get(`Clients/${id}/addresses`, {
`Clients/${id}/addresses?filter[order]=isActive DESC` params: {
); filter: JSON.stringify({
include: [
{
relation: 'client',
scope: {
fields: ['defaultAddressFk'],
},
},
],
order: ['isActive DESC'],
}),
},
});
addressOptions.value = data; addressOptions.value = data;
formData.addressId = data.defaultAddressFk; formData.addressId = data[0].client.defaultAddressFk;
fetchAgencies(formData); fetchAgencies(formData);
} }
@ -167,7 +179,13 @@ async function fetchAgencies({ landed, addressId }) {
if (!landed || !addressId) return (agencyList.value = []); if (!landed || !addressId) return (agencyList.value = []);
const { data } = await axios.get('Agencies/landsThatDay', { const { data } = await axios.get('Agencies/landsThatDay', {
params: { addressFk: addressId, landed }, params: {
filter: JSON.stringify({
order: ['agencyMode DESC', 'agencyModeFk ASC'],
}),
addressFk: addressId,
landed,
},
}); });
agencyList.value = data; agencyList.value = data;
} }
@ -258,6 +276,7 @@ const getDateColor = (date) => {
</template> </template>
</VnSelect> </VnSelect>
<VnSelect <VnSelect
:disable="!data.clientFk"
v-model="data.addressId" v-model="data.addressId"
:options="addressOptions" :options="addressOptions"
:label="t('module.address')" :label="t('module.address')"
@ -284,6 +303,9 @@ const getDateColor = (date) => {
{{ scope.opt?.street }}, {{ scope.opt?.street }},
{{ scope.opt?.city }} {{ scope.opt?.city }}
</QItemLabel> </QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.id}` }}
</QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>

View File

@ -27,14 +27,17 @@ describe('getAgencies', () => {
landed: 'true', landed: 'true',
}; };
const filter = { const filter = {
fields: ['nickname', 'street', 'city', 'id'], fields: ['name', 'street', 'city', 'id'],
where: { isActive: true }, where: { isActive: true },
order: 'nickname ASC', order: ['name ASC'],
}; };
await getAgencies(formData, null, filter); await getAgencies(formData, null, filter);
expect(axios.get).toHaveBeenCalledWith('Agencies/getAgenciesWithWarehouse', generateParams(formData, filter)); expect(axios.get).toHaveBeenCalledWith(
'Agencies/getAgenciesWithWarehouse',
generateParams(formData, filter),
);
}); });
it('should not call API when formData is missing required landed field', async () => { it('should not call API when formData is missing required landed field', async () => {
@ -64,19 +67,19 @@ describe('getAgencies', () => {
it('should return options and agency when default agency is found', async () => { it('should return options and agency when default agency is found', async () => {
const formData = { warehouseId: '123', addressId: '456', landed: 'true' }; const formData = { warehouseId: '123', addressId: '456', landed: 'true' };
const client = { defaultAddress: { agencyModeFk: 'Agency1' } }; const client = { defaultAddress: { agencyModeFk: 'Agency1' } };
const { options, agency } = await getAgencies(formData, client); const { options, agency } = await getAgencies(formData, client);
expect(options).toEqual(response.data); expect(options).toEqual(response.data);
expect(agency).toEqual(response.data[0]); expect(agency).toEqual(response.data[0]);
}); });
it('should return options and agency when client is not provided', async () => { it('should return options and agency when client is not provided', async () => {
const formData = { warehouseId: '123', addressId: '456', landed: 'true' }; const formData = { warehouseId: '123', addressId: '456', landed: 'true' };
const { options, agency } = await getAgencies(formData); const { options, agency } = await getAgencies(formData);
expect(options).toEqual(response.data); expect(options).toEqual(response.data);
expect(agency).toBeNull(); expect(agency).toBeNull();
}); });
}); });

View File

@ -1,14 +1,14 @@
import axios from 'axios'; import axios from 'axios';
import agency from 'src/router/modules/agency';
export async function getAgencies(formData, client, _filter = {}) { export async function getAgencies(formData, client, _filter = {}) {
if (!formData.warehouseId || !formData.addressId || !formData.landed) return; if (!formData.warehouseId || !formData.addressId || !formData.landed) return;
const filter = { const filter = {
..._filter ..._filter,
order: ['name ASC'],
}; };
let defaultAgency = null; let agency = null;
let params = { let params = {
filter: JSON.stringify(filter), filter: JSON.stringify(filter),
warehouseFk: formData.warehouseId, warehouseFk: formData.warehouseId,
@ -16,11 +16,15 @@ export async function getAgencies(formData, client, _filter = {}) {
landed: formData.landed, landed: formData.landed,
}; };
const { data } = await axios.get('Agencies/getAgenciesWithWarehouse', { params }); const { data: options } = await axios.get('Agencies/getAgenciesWithWarehouse', {
params,
});
if(data && client) { if (options && client) {
defaultAgency = data.find((agency) => agency.agencyModeFk === client.defaultAddress.agencyModeFk ); agency = options.find(
}; ({ agencyModeFk }) => agencyModeFk === client.defaultAddress.agencyModeFk,
);
return {options: data, agency: defaultAgency} }
return { options, agency };
} }

View File

@ -44,8 +44,7 @@ const exprBuilder = (param, value) => {
<template> <template>
<FetchData <FetchData
url="AgencyModes" url="AgencyModes"
:filter="{ fields: ['id', 'name'] }" :filter="{ fields: ['id', 'name'], order: ['name ASC'] }"
sort-by="name ASC"
@on-fetch="(data) => (agencyList = data)" @on-fetch="(data) => (agencyList = data)"
auto-load auto-load
/> />

View File

@ -180,6 +180,7 @@ const onDmsSaved = async (dms, response) => {
rows: dmsDialog.value.rowsToCreateInvoiceIn, rows: dmsDialog.value.rowsToCreateInvoiceIn,
dms: response.data, dms: response.data,
}); });
notify(t('Data saved'), 'positive');
} }
dmsDialog.value.show = false; dmsDialog.value.show = false;
dmsDialog.value.initialForm = null; dmsDialog.value.initialForm = null;
@ -243,7 +244,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</template> </template>
<template #column-invoiceInFk="{ row }"> <template #column-invoiceInFk="{ row }">
<span class="link" @click.stop> <span class="link" @click.stop>
{{ row.invoiceInFk }} {{ row.supplierRef }}
<InvoiceInDescriptorProxy v-if="row.invoiceInFk" :id="row.invoiceInFk" /> <InvoiceInDescriptorProxy v-if="row.invoiceInFk" :id="row.invoiceInFk" />
</span> </span>
</template> </template>

View File

@ -3,7 +3,7 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { dashIfEmpty, toDate, toHour } from 'src/filters'; import { toDate, toHour } from 'src/filters';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { usePrintService } from 'src/composables/usePrintService'; import { usePrintService } from 'src/composables/usePrintService';
@ -335,7 +335,6 @@ const openTicketsDialog = (id) => {
<QBtn <QBtn
icon="vn:clone" icon="vn:clone"
color="primary" color="primary"
flat
class="q-mr-sm" class="q-mr-sm"
:disable="!selectedRows?.length" :disable="!selectedRows?.length"
@click="confirmationDialog = true" @click="confirmationDialog = true"
@ -345,7 +344,6 @@ const openTicketsDialog = (id) => {
<QBtn <QBtn
icon="cloud_download" icon="cloud_download"
color="primary" color="primary"
flat
class="q-mr-sm" class="q-mr-sm"
:disable="!selectedRows?.length" :disable="!selectedRows?.length"
@click="showRouteReport" @click="showRouteReport"
@ -355,7 +353,6 @@ const openTicketsDialog = (id) => {
<QBtn <QBtn
icon="check" icon="check"
color="primary" color="primary"
flat
class="q-mr-sm" class="q-mr-sm"
:disable="!selectedRows?.length" :disable="!selectedRows?.length"
@click="markAsServed()" @click="markAsServed()"

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref, markRaw } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { toHour } from 'src/filters'; import { toHour } from 'src/filters';
@ -8,6 +8,7 @@ import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnSection from 'src/components/common/VnSection.vue'; import VnSection from 'src/components/common/VnSection.vue';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
@ -38,17 +39,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
name: 'workerFk', name: 'workerFk',
label: t('route.Worker'), label: t('route.Worker'),
component: 'select', component: markRaw(VnSelectWorker),
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
useLike: false,
optionFilter: 'firstName',
find: {
value: 'workerFk',
label: 'workerUserName',
},
},
create: true, create: true,
cardVisible: true, cardVisible: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef),
@ -59,6 +50,10 @@ const columns = computed(() => [
name: 'agencyName', name: 'agencyName',
label: t('route.Agency'), label: t('route.Agency'),
cardVisible: true, cardVisible: true,
},
{
label: t('route.Agency'),
name: 'agencyModeFk',
component: 'select', component: 'select',
attrs: { attrs: {
url: 'agencyModes', url: 'agencyModes',
@ -69,14 +64,19 @@ const columns = computed(() => [
}, },
}, },
create: true, create: true,
columnClass: 'expand',
columnFilter: false, columnFilter: false,
visible: false,
}, },
{ {
align: 'left', align: 'left',
name: 'vehiclePlateNumber', name: 'vehiclePlateNumber',
label: t('route.Vehicle'), label: t('route.Vehicle'),
cardVisible: true, cardVisible: true,
},
{
name: 'vehicleFk',
label: t('route.Vehicle'),
cardVisible: true,
component: 'select', component: 'select',
attrs: { attrs: {
url: 'vehicles', url: 'vehicles',
@ -90,6 +90,7 @@ const columns = computed(() => [
}, },
create: true, create: true,
columnFilter: false, columnFilter: false,
visible: false,
}, },
{ {
align: 'left', align: 'left',
@ -156,6 +157,7 @@ const columns = computed(() => [
<VnTable <VnTable
:data-key :data-key
:columns="columns" :columns="columns"
ref="tableRef"
:right-search="false" :right-search="false"
redirect="route" redirect="route"
:create="{ :create="{

View File

@ -22,7 +22,12 @@ const links = {
}; };
</script> </script>
<template> <template>
<CardSummary data-key="Vehicle" :url="`Vehicles/${entityId}`" :filter="VehicleFilter"> <CardSummary
data-key="Vehicle"
:url="`Vehicles/${entityId}`"
module-name="Vehicle"
:filter="VehicleFilter"
>
<template #header="{ entity }"> <template #header="{ entity }">
<div>{{ entity.id }} - {{ entity.numberPlate }}</div> <div>{{ entity.id }} - {{ entity.numberPlate }}</div>
</template> </template>

View File

@ -45,8 +45,6 @@ const filter = {
:label="t('parking.sector')" :label="t('parking.sector')"
:value="entity.sector?.description" :value="entity.sector?.description"
/> />
<VnLv :label="t('parking.row')" :value="entity.row" />
<VnLv :label="t('parking.column')" :value="entity.column" />
</QCard> </QCard>
</template> </template>
</CardSummary> </CardSummary>

View File

@ -1,19 +1,15 @@
<script setup> <script setup>
import { onMounted, onUnmounted } from 'vue'; import { computed, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import CardList from 'components/ui/CardList.vue';
import VnLv from 'components/ui/VnLv.vue';
import ParkingFilter from './ParkingFilter.vue';
import ParkingSummary from './Card/ParkingSummary.vue';
import exprBuilder from './ParkingExprBuilder.js';
import VnSection from 'src/components/common/VnSection.vue'; import VnSection from 'src/components/common/VnSection.vue';
import ParkingFilter from './ParkingFilter.vue';
import exprBuilder from './ParkingExprBuilder.js';
import ParkingSummary from './Card/ParkingSummary.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const { push } = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const dataKey = 'ParkingList'; const dataKey = 'ParkingList';
@ -24,7 +20,48 @@ onUnmounted(() => (stateStore.rightDrawer = false));
const filter = { const filter = {
fields: ['id', 'sectorFk', 'code', 'pickingOrder'], fields: ['id', 'sectorFk', 'code', 'pickingOrder'],
}; };
const columns = computed(() => [
{
align: 'left',
name: 'code',
label: t('globals.code'),
isId: true,
isTitle: true,
columnFilter: false,
sortable: true,
},
{
align: 'left',
name: 'sector',
label: t('parking.sector'),
format: (val) => val.sector.description ?? '',
sortable: true,
cardVisible: true,
},
{
align: 'left',
name: 'pickingOrder',
label: t('parking.pickingOrder'),
sortable: true,
cardVisible: true,
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
action: (row) => viewSummary(row.id, ParkingSummary),
isPrimary: true,
},
],
},
]);
</script> </script>
<template> <template>
<VnSection <VnSection
:data-key="dataKey" :data-key="dataKey"
@ -40,41 +77,24 @@ const filter = {
<ParkingFilter data-key="ParkingList" /> <ParkingFilter data-key="ParkingList" />
</template> </template>
<template #body> <template #body>
<QPage class="column items-center q-pa-md"> <VnTable
<div class="vn-card-list"> :data-key="dataKey"
<VnPaginate :data-key="dataKey"> :columns="columns"
<template #body="{ rows }"> is-editable="false"
<CardList :right-search="false"
v-for="row of rows" :use-model="true"
:key="row.id" :disable-option="{ table: true }"
:id="row.id" redirect="shelving/parking"
:title="row.code" default-mode="card"
@click=" >
push({ path: `/shelving/parking/${row.id}/summary` }) <template #actions="{ row }">
" <QBtn
> :label="t('components.smartCard.openSummary')"
<template #list-items> @click.stop="viewSummary(row.id, ParkingSummary)"
<VnLv color="primary"
label="Sector" />
:value="row.sector?.description" </template>
/> </VnTable>
<VnLv
:label="t('parking.pickingOrder')"
:value="row.pickingOrder"
/>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, ParkingSummary)"
color="primary"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage>
</template> </template>
</VnSection> </VnSection>
</template> </template>

View File

@ -11,6 +11,11 @@ export default {
'isSerious', 'isSerious',
'isTrucker', 'isTrucker',
'account', 'account',
'workerFk',
'note',
'isReal',
'isPayMethodChecked',
'companySize',
], ],
include: [ include: [
{ {

View File

@ -6,7 +6,10 @@ import VnSection from 'src/components/common/VnSection.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import SupplierSummary from './Card/SupplierSummary.vue';
const { viewSummary } = useSummaryDialog();
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref(); const tableRef = ref();
const dataKey = 'SupplierList'; const dataKey = 'SupplierList';
@ -103,6 +106,19 @@ const columns = computed(() => [
}, },
}, },
}, },
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
isPrimary: true,
action: (row) => viewSummary(row.id, SupplierSummary, 'md-width'),
},
],
},
]); ]);
</script> </script>
<template> <template>

View File

@ -93,9 +93,9 @@ function ticketFilter(ticket) {
<VnLv :label="t('globals.warehouse')" :value="entity.warehouse?.name" /> <VnLv :label="t('globals.warehouse')" :value="entity.warehouse?.name" />
<VnLv :label="t('globals.alias')" :value="entity.nickname" /> <VnLv :label="t('globals.alias')" :value="entity.nickname" />
</template> </template>
<template #icons> <template #icons="{ entity }">
<QCardActions class="q-gutter-x-xs"> <QCardActions class="q-gutter-x-xs">
<TicketProblems :row="problems" /> <TicketProblems :row="{ ...entity?.client, ...problems }" />
</QCardActions> </QCardActions>
</template> </template>
<template #actions="{ entity }"> <template #actions="{ entity }">

View File

@ -1,32 +1,26 @@
<script setup> <script setup>
import { ref } from 'vue'; import axios from 'axios';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import VnUsesMana from 'components/ui/VnUsesMana.vue'; import VnUsesMana from 'components/ui/VnUsesMana.vue';
const $props = defineProps({ const $props = defineProps({
mana: {
type: Number,
default: null,
},
newPrice: { newPrice: {
type: Number, type: Number,
default: 0, default: 0,
}, },
usesMana: {
type: Boolean,
default: false,
},
manaCode: {
type: String,
default: 'mana',
},
sale: { sale: {
type: Object, type: Object,
default: null, default: null,
}, },
}); });
const route = useRoute();
const mana = ref(null);
const usesMana = ref(false);
const emit = defineEmits(['save', 'cancel']); const emit = defineEmits(['save', 'cancel']);
const { t } = useI18n(); const { t } = useI18n();
@ -38,32 +32,47 @@ const save = (sale = $props.sale) => {
QPopupProxyRef.value.hide(); QPopupProxyRef.value.hide();
}; };
const getMana = async () => {
const { data } = await axios.get(`Tickets/${route.params.id}/getSalesPersonMana`);
mana.value = data;
await getUsesMana();
};
const getUsesMana = async () => {
const { data } = await axios.get('Sales/usesMana');
usesMana.value = data;
};
const cancel = () => { const cancel = () => {
emit('cancel'); emit('cancel');
QPopupProxyRef.value.hide(); QPopupProxyRef.value.hide();
}; };
const hasMana = computed(() => typeof mana.value === 'number');
defineExpose({ save }); defineExpose({ save });
</script> </script>
<template> <template>
<QPopupProxy ref="QPopupProxyRef" data-cy="ticketEditManaProxy"> <QPopupProxy
ref="QPopupProxyRef"
@before-show="getMana"
data-cy="ticketEditManaProxy"
>
<div class="container"> <div class="container">
<QSpinner v-if="!mana" color="primary" size="md" /> <div class="header">Mana: {{ toCurrency(mana) }}</div>
<div v-else> <QSpinner v-if="!hasMana" color="primary" size="md" />
<div class="header">Mana: {{ toCurrency(mana) }}</div> <div class="q-pa-md" v-else>
<div class="q-pa-md"> <slot :popup="QPopupProxyRef" />
<slot :popup="QPopupProxyRef" /> <div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm">
<div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm"> <VnUsesMana :mana-code="manaCode" />
<VnUsesMana :mana-code="manaCode" /> </div>
</div> <div v-if="newPrice" class="column items-center q-mt-lg">
<div v-if="newPrice" class="column items-center q-mt-lg"> <span class="text-primary">{{ t('New price') }}</span>
<span class="text-primary">{{ t('New price') }}</span> <span class="text-subtitle1">
<span class="text-subtitle1"> {{ toCurrency($props.newPrice) }}
{{ toCurrency($props.newPrice) }} </span>
</span>
</div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<QBtn <QBtn
color="primary" color="primary"

View File

@ -37,7 +37,6 @@ const expeditionStateTypes = ref([]);
const expeditionsFilter = computed(() => ({ const expeditionsFilter = computed(() => ({
where: { ticketFk: route.params.id }, where: { ticketFk: route.params.id },
order: ['created DESC'],
})); }));
const ticketArrayData = useArrayData('Ticket'); const ticketArrayData = useArrayData('Ticket');
@ -105,6 +104,9 @@ const columns = computed(() => [
name: 'created', name: 'created',
align: 'left', align: 'left',
cardVisible: true, cardVisible: true,
columnFilter: {
component: 'date',
},
format: (row) => toDateTimeFormat(row.created), format: (row) => toDateTimeFormat(row.created),
}, },
{ {
@ -201,7 +203,7 @@ const getExpeditionState = async (expedition) => {
const openGrafana = (expeditionFk) => { const openGrafana = (expeditionFk) => {
useOpenURL( useOpenURL(
`https://grafana.verdnatura.es/d/de1njb6p5answd/control-de-expediciones?orgId=1&var-expeditionFk=${expeditionFk}` `https://grafana.verdnatura.es/d/de1njb6p5answd/control-de-expediciones?orgId=1&var-expeditionFk=${expeditionFk}`,
); );
}; };
@ -287,7 +289,7 @@ onMounted(async () => {
openConfirmationModal( openConfirmationModal(
'', '',
t('expedition.removeExpeditionSubtitle'), t('expedition.removeExpeditionSubtitle'),
deleteExpedition deleteExpedition,
) )
" "
> >
@ -302,7 +304,6 @@ onMounted(async () => {
url="Expeditions/filter" url="Expeditions/filter"
search-url="expeditions" search-url="expeditions"
:columns="columns" :columns="columns"
:filter="expeditionsFilter"
v-model:selected="selectedRows" v-model:selected="selectedRows"
:table="{ :table="{
'row-key': 'id', 'row-key': 'id',
@ -316,11 +317,14 @@ onMounted(async () => {
return { id: value }; return { id: value };
case 'packageItemName': case 'packageItemName':
return { packagingItemFk: value }; return { packagingItemFk: value };
case 'created':
return { 'e.created': { gte: value } };
} }
} }
" "
:redirect="false" :redirect="false"
order="created DESC" order="created DESC"
:filter="expeditionsFilter"
> >
<template #column-freightItemName="{ row }"> <template #column-freightItemName="{ row }">
<span class="link" @click.stop> <span class="link" @click.stop>

View File

@ -45,7 +45,6 @@ const isTicketEditable = ref(false);
const sales = ref([]); const sales = ref([]);
const editableStatesOptions = ref([]); const editableStatesOptions = ref([]);
const selectedSales = ref([]); const selectedSales = ref([]);
const mana = ref(null);
const manaCode = ref('mana'); const manaCode = ref('mana');
const ticketState = computed(() => store.data?.ticketState?.state?.code); const ticketState = computed(() => store.data?.ticketState?.state?.code);
const transfer = ref({ const transfer = ref({
@ -175,17 +174,21 @@ const getSaleTotal = (sale) => {
return price - discount; return price - discount;
}; };
const getRowUpdateInputEvents = (sale) => ({
'keyup.enter': () => {
changeQuantity(sale);
},
blur: () => {
changeQuantity(sale);
},
});
const resetChanges = async () => { const resetChanges = async () => {
arrayData.fetch({ append: false }); arrayData.fetch({ append: false });
tableRef.value.reload(); tableRef.value.reload();
}; };
const rowToUpdate = ref(null);
const changeQuantity = async (sale) => { const changeQuantity = async (sale) => {
if ( if (!sale.itemFk || sale.quantity == null || sale?.originalQuantity === sale.quantity)
!sale.itemFk ||
sale.quantity == null ||
edit.value?.oldQuantity === sale.quantity
)
return; return;
if (!sale.id) return addSale(sale); if (!sale.id) return addSale(sale);
@ -197,13 +200,10 @@ const changeQuantity = async (sale) => {
const updateQuantity = async (sale) => { const updateQuantity = async (sale) => {
try { try {
let { quantity, id } = sale; let { quantity, id } = sale;
if (!rowToUpdate.value) return;
rowToUpdate.value = null;
sale.isNew = false; sale.isNew = false;
const params = { quantity: quantity }; await axios.post(`Sales/${id}/updateQuantity`, { quantity });
await axios.post(`Sales/${id}/updateQuantity`, params);
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
tableRef.value.reload(); resetChanges();
} catch (e) { } catch (e) {
const { quantity } = tableRef.value.CrudModelRef.originalData.find( const { quantity } = tableRef.value.CrudModelRef.originalData.find(
(s) => s.id === sale.id, (s) => s.id === sale.id,
@ -247,7 +247,7 @@ const updateConcept = async (sale) => {
const data = { newConcept: sale.concept }; const data = { newConcept: sale.concept };
await axios.post(`Sales/${sale.id}/updateConcept`, data); await axios.post(`Sales/${sale.id}/updateConcept`, data);
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
tableRef.value.reload(); resetChanges();
}; };
const DEFAULT_EDIT = { const DEFAULT_EDIT = {
@ -258,18 +258,6 @@ const DEFAULT_EDIT = {
oldQuantity: null, oldQuantity: null,
}; };
const edit = ref({ ...DEFAULT_EDIT }); const edit = ref({ ...DEFAULT_EDIT });
const usesMana = ref(null);
const getUsesMana = async () => {
const { data } = await axios.get('Sales/usesMana');
usesMana.value = data;
};
const getMana = async () => {
const { data } = await axios.get(`Tickets/${route.params.id}/getSalesPersonMana`);
mana.value = data;
await getUsesMana();
};
const selectedValidSales = computed(() => { const selectedValidSales = computed(() => {
if (!sales.value) return; if (!sales.value) return;
@ -277,7 +265,6 @@ const selectedValidSales = computed(() => {
}); });
const onOpenEditPricePopover = async (sale) => { const onOpenEditPricePopover = async (sale) => {
await getMana();
edit.value = { edit.value = {
sale: JSON.parse(JSON.stringify(sale)), sale: JSON.parse(JSON.stringify(sale)),
price: sale.price, price: sale.price,
@ -285,7 +272,6 @@ const onOpenEditPricePopover = async (sale) => {
}; };
const onOpenEditDiscountPopover = async (sale) => { const onOpenEditDiscountPopover = async (sale) => {
await getMana();
if (isLocked.value) return; if (isLocked.value) return;
if (sale) { if (sale) {
edit.value = { edit.value = {
@ -306,14 +292,13 @@ const changePrice = async (sale) => {
await confirmUpdate(() => updatePrice(sale, newPrice)); await confirmUpdate(() => updatePrice(sale, newPrice));
} else updatePrice(sale, newPrice); } else updatePrice(sale, newPrice);
} }
await getMana();
}; };
const updatePrice = async (sale, newPrice) => { const updatePrice = async (sale, newPrice) => {
await axios.post(`Sales/${sale.id}/updatePrice`, { newPrice }); await axios.post(`Sales/${sale.id}/updatePrice`, { newPrice });
sale.price = newPrice; sale.price = newPrice;
edit.value = { ...DEFAULT_EDIT }; edit.value = { ...DEFAULT_EDIT };
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
tableRef.value.reload(); resetChanges();
}; };
const changeDiscount = async (sale) => { const changeDiscount = async (sale) => {
@ -345,7 +330,7 @@ const updateDiscount = async (sales, newDiscount = null) => {
}; };
await axios.post(`Tickets/${route.params.id}/updateDiscount`, params); await axios.post(`Tickets/${route.params.id}/updateDiscount`, params);
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
tableRef.value.reload(); resetChanges();
}; };
const getNewPrice = computed(() => { const getNewPrice = computed(() => {
@ -413,7 +398,7 @@ const removeSales = async () => {
await axios.post('Sales/deleteSales', params); await axios.post('Sales/deleteSales', params);
removeSelectedSales(); removeSelectedSales();
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
window.location.reload(); resetChanges();
}; };
const setTransferParams = async () => { const setTransferParams = async () => {
@ -599,9 +584,7 @@ watch(
:is-ticket-editable="isTicketEditable" :is-ticket-editable="isTicketEditable"
:sales="selectedValidSales" :sales="selectedValidSales"
:disable="!hasSelectedRows" :disable="!hasSelectedRows"
:mana="mana"
:ticket-config="ticketConfig" :ticket-config="ticketConfig"
@get-mana="getMana()"
@update-discounts="updateDiscounts" @update-discounts="updateDiscounts"
@refresh-table="resetChanges" @refresh-table="resetChanges"
/> />
@ -772,9 +755,7 @@ watch(
v-if="row.isNew || isTicketEditable" v-if="row.isNew || isTicketEditable"
type="number" type="number"
v-model.number="row.quantity" v-model.number="row.quantity"
@blur="changeQuantity(row)" v-on="getRowUpdateInputEvents(row)"
@keyup.enter.stop="changeQuantity(row)"
@update:model-value="() => (rowToUpdate = row)"
@focus="edit.oldQuantity = row.quantity" @focus="edit.oldQuantity = row.quantity"
/> />
<span v-else>{{ row.quantity }}</span> <span v-else>{{ row.quantity }}</span>
@ -786,7 +767,6 @@ watch(
</QBtn> </QBtn>
<TicketEditManaProxy <TicketEditManaProxy
ref="editPriceProxyRef" ref="editPriceProxyRef"
:mana="mana"
:sale="row" :sale="row"
:new-price="getNewPrice" :new-price="getNewPrice"
@save="changePrice" @save="changePrice"
@ -809,10 +789,8 @@ watch(
<TicketEditManaProxy <TicketEditManaProxy
ref="editManaProxyRef" ref="editManaProxyRef"
:mana="mana"
:sale="row" :sale="row"
:new-price="getNewPrice" :new-price="getNewPrice"
:uses-mana="usesMana"
:mana-code="manaCode" :mana-code="manaCode"
@save="changeDiscount" @save="changeDiscount"
> >

View File

@ -34,10 +34,6 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
mana: {
type: Number,
default: null,
},
ticketConfig: { ticketConfig: {
type: Array, type: Array,
default: () => [], default: () => [],
@ -50,6 +46,7 @@ const { dialog } = useQuasar();
const { notify } = useNotify(); const { notify } = useNotify();
const acl = useAcl(); const acl = useAcl();
const btnDropdownRef = ref(null); const btnDropdownRef = ref(null);
const editManaProxyRef = ref(null);
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const newDiscount = ref(null); const newDiscount = ref(null);
@ -131,13 +128,13 @@ const createClaim = () => {
openConfirmationModal( openConfirmationModal(
t('Claim out of time'), t('Claim out of time'),
t('Do you want to continue?'), t('Do you want to continue?'),
onCreateClaimAccepted onCreateClaimAccepted,
); );
else else
openConfirmationModal( openConfirmationModal(
t('Do you want to create a claim?'), t('Do you want to create a claim?'),
false, false,
onCreateClaimAccepted onCreateClaimAccepted,
); );
}; };
@ -216,8 +213,14 @@ const createRefund = async (withWarehouse) => {
<QItemSection> <QItemSection>
<QItemLabel>{{ t('Update discount') }}</QItemLabel> <QItemLabel>{{ t('Update discount') }}</QItemLabel>
</QItemSection> </QItemSection>
<TicketEditManaProxy :mana="props.mana" @save="changeMultipleDiscount()"> <TicketEditManaProxy
ref="editManaProxyRef"
:sale="row"
@save="changeMultipleDiscount"
>
<VnInput <VnInput
autofocus
@keyup.enter.stop="() => editManaProxyRef.save(row)"
v-model.number="newDiscount" v-model.number="newDiscount"
:label="t('ticketSale.discount')" :label="t('ticketSale.discount')"
type="number" type="number"

View File

@ -31,7 +31,7 @@ const oldQuantity = ref(null);
watch( watch(
() => route.params.id, () => route.params.id,
async () => nextTick(async () => await saleTrackingFetchDataRef.value.fetch()) async () => nextTick(async () => await saleTrackingFetchDataRef.value.fetch()),
); );
const columns = computed(() => [ const columns = computed(() => [
@ -212,7 +212,7 @@ const updateShelving = async (sale) => {
const { data: patchResponseData } = await axios.patch( const { data: patchResponseData } = await axios.patch(
`ItemShelvings/${sale.itemShelvingFk}`, `ItemShelvings/${sale.itemShelvingFk}`,
params params,
); );
const filter = { const filter = {
fields: ['parkingFk'], fields: ['parkingFk'],
@ -385,7 +385,7 @@ const qCheckBoxController = (sale, action) => {
</template> </template>
<template #body-cell-parking="{ row }"> <template #body-cell-parking="{ row }">
<QTd style="width: 10%"> <QTd style="width: 10%">
{{ dashIfEmpty(row.parkingFk) }} {{ dashIfEmpty(row.parkingCode) }}
</QTd> </QTd>
</template> </template>
<template #body-cell-actions="{ row }"> <template #body-cell-actions="{ row }">

View File

@ -121,6 +121,50 @@ async function handleSave() {
isSaving.value = false; isSaving.value = false;
} }
} }
function validateFields(item) {
// Only validate fields that are being updated
const shouldExist = (field) => !isUpdate || field in item;
if (!shouldExist('ticketServiceTypeFk') && !item.ticketServiceTypeFk) {
notify('Description is required', 'negative');
return false;
}
if (!shouldExist('quantity') && (!item.quantity || item.quantity <= 0)) {
notify('Quantity must be greater than 0', 'negative');
return false;
}
if (!shouldExist('price') && (!item.price || item.price < 0)) {
notify('Price must be valid', 'negative');
return false;
}
return true;
}
function beforeSave(data) {
const { creates = [], updates = [] } = data;
const validData = { creates: [], updates: [] };
// Validate creates
if (creates.length) {
for (const create of creates) {
create.ticketFk = route.params.id;
if (validateFields(create)) {
validData.creates.push(create);
}
}
}
// Validate updates
if (updates.length) {
for (const update of updates) {
validData.updates.push(update);
}
}
return validData;
}
</script> </script>
<template> <template>
@ -141,6 +185,7 @@ async function handleSave() {
v-model:selected="selected" v-model:selected="selected"
:order="['description ASC']" :order="['description ASC']"
:default-remove="false" :default-remove="false"
:beforeSaveFn="beforeSave"
> >
<template #moreBeforeActions> <template #moreBeforeActions>
<QBtn <QBtn
@ -170,6 +215,7 @@ async function handleSave() {
option-value="id" option-value="id"
hide-selected hide-selected
sort-by="name ASC" sort-by="name ASC"
:required="true"
> >
<template #form> <template #form>
<TicketCreateServiceType <TicketCreateServiceType
@ -185,6 +231,7 @@ async function handleSave() {
:label="col.label" :label="col.label"
v-model.number="row.quantity" v-model.number="row.quantity"
type="number" type="number"
:required="true"
min="0" min="0"
:info="t('service.quantityInfo')" :info="t('service.quantityInfo')"
/> />
@ -196,6 +243,7 @@ async function handleSave() {
:label="col.label" :label="col.label"
v-model.number="row.price" v-model.number="row.price"
type="number" type="number"
:required="true"
min="0" min="0"
@keyup.enter="handleSave" @keyup.enter="handleSave"
/> />

View File

@ -46,6 +46,15 @@ const descriptorData = useArrayData('Ticket');
onMounted(async () => { onMounted(async () => {
ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/'; ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/';
}); });
const formattedAddress = computed(() => {
if (!ticket.value) return '';
const address = ticket.value.address;
const postcode = address.postalCode;
const province = address.province ? `(${address.province.name})` : '';
return `${address.street} - ${postcode} - ${address.city} ${province}`;
});
function isEditable() { function isEditable() {
try { try {
@ -238,7 +247,7 @@ onMounted(async () => {
/> />
<VnLv <VnLv
:label="t('ticket.summary.consigneeStreet')" :label="t('ticket.summary.consigneeStreet')"
:value="entity.address?.street" :value="formattedAddress"
/> />
</QCard> </QCard>
<QCard class="vn-one" v-if="entity.notes.length"> <QCard class="vn-one" v-if="entity.notes.length">

View File

@ -42,7 +42,7 @@ const transferRef = ref(null);
/> />
</div> </div>
<div v-else> <div style="display: flex; flex-direction: row" v-else>
<TicketTransfer <TicketTransfer
ref="transferRef" ref="transferRef"
:ticket="$props.ticket" :ticket="$props.ticket"

View File

@ -142,7 +142,7 @@ onMounted(() => (stateStore.rightDrawer = true));
<template #column-concept="{ row }"> <template #column-concept="{ row }">
<span>{{ row.item.name }}</span> <span>{{ row.item.name }}</span>
<span class="color-vn-label q-pl-md">{{ row.item.subName }}</span> <span class="color-vn-label q-pl-md">{{ row.item.subName }}</span>
<FetchedTags :item="row.item" /> <FetchedTags :item="row.item" :columns="6" />
</template> </template>
<template #column-volume="{ rowIndex }"> <template #column-volume="{ rowIndex }">
<span>{{ packingTypeVolume?.[rowIndex]?.volume }}</span> <span>{{ packingTypeVolume?.[rowIndex]?.volume }}</span>

View File

@ -456,6 +456,7 @@ watch(
:pagination="{ rowsPerPage: 0 }" :pagination="{ rowsPerPage: 0 }"
:no-data-label="t('globals.noResults')" :no-data-label="t('globals.noResults')"
:right-search="false" :right-search="false"
:order="['futureTotalWithVat ASC']"
auto-load auto-load
:disable-option="{ card: true }" :disable-option="{ card: true }"
> >

View File

@ -46,7 +46,12 @@ const getGroupedStates = (data) => {
" "
auto-load auto-load
/> />
<FetchData url="AgencyModes" @on-fetch="(data) => (agencies = data)" auto-load /> <FetchData
url="AgencyModes"
:filter="{ fields: ['id', 'name'], order: ['name ASC'] }"
@on-fetch="(data) => (agencies = data)"
auto-load
/>
<FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load /> <FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true"> <VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
@ -74,10 +79,20 @@ const getGroupedStates = (data) => {
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate v-model="params.from" :label="t('From')" is-outlined /> <VnInputDate
v-model="params.from"
:label="t('From')"
is-outlined
data-cy="From_date"
/>
</QItemSection> </QItemSection>
<QItemSection> <QItemSection>
<VnInputDate v-model="params.to" :label="t('To')" is-outlined /> <VnInputDate
v-model="params.to"
:label="t('To')"
is-outlined
data-cy="To_date"
/>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem>
@ -241,8 +256,6 @@ const getGroupedStates = (data) => {
v-model="params.agencyModeFk" v-model="params.agencyModeFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
:options="agencies" :options="agencies"
option-value="id"
option-label="name"
emit-value emit-value
map-options map-options
use-input use-input
@ -293,6 +306,7 @@ en:
clientFk: Customer clientFk: Customer
orderFk: Order orderFk: Order
from: From from: From
shipped: Shipped
to: To to: To
salesPersonFk: Salesperson salesPersonFk: Salesperson
stateFk: State stateFk: State
@ -320,6 +334,7 @@ es:
clientFk: Cliente clientFk: Cliente
orderFk: Pedido orderFk: Pedido
from: Desde from: Desde
shipped: F. envío
to: Hasta to: Hasta
salesPersonFk: Comercial salesPersonFk: Comercial
stateFk: Estado stateFk: Estado

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { computed, ref, onBeforeMount } from 'vue'; import { computed, ref, onBeforeMount, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -69,6 +69,8 @@ const companiesOptions = ref([]);
const accountingOptions = ref([]); const accountingOptions = ref([]);
const amountToReturn = ref(); const amountToReturn = ref();
const dataKey = 'TicketList'; const dataKey = 'TicketList';
const filterPanelRef = ref(null);
const formInitialData = ref({});
const columns = computed(() => [ const columns = computed(() => [
{ {
@ -108,25 +110,27 @@ const columns = computed(() => [
}, },
{ {
align: 'left', align: 'left',
name: 'shippedDate', name: 'shipped',
cardVisible: true, cardVisible: true,
label: t('ticketList.shipped'), label: t('ticketList.shipped'),
columnFilter: { columnFilter: {
component: 'date', component: 'date',
alias: 't',
inWhere: true,
}, },
format: ({ shippedDate }) => toDate(shippedDate), format: ({ shippedDate }) => toDate(shippedDate),
}, },
{ {
align: 'left', align: 'left',
name: 'shipped', name: 'shipped',
component: 'time',
columnFilter: false,
label: t('ticketList.hour'), label: t('ticketList.hour'),
format: (row) => toTimeFormat(row.shipped), format: (row) => toTimeFormat(row.shipped),
}, },
{ {
align: 'left', align: 'left',
name: 'zoneLanding', name: 'zoneLanding',
component: 'time',
columnFilter: false,
label: t('ticketList.closure'), label: t('ticketList.closure'),
format: (row, dashIfEmpty) => dashIfEmpty(toTimeFormat(row.zoneLanding)), format: (row, dashIfEmpty) => dashIfEmpty(toTimeFormat(row.zoneLanding)),
}, },
@ -146,9 +150,16 @@ const columns = computed(() => [
}, },
{ {
align: 'left', align: 'left',
name: 'province', name: 'provinceFk',
label: t('ticketList.province'), label: t('ticketList.province'),
columnClass: 'expand', component: 'select',
attrs: {
url: 'Provinces',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.province),
}, },
{ {
align: 'left', align: 'left',
@ -182,9 +193,19 @@ const columns = computed(() => [
}, },
{ {
align: 'left', align: 'left',
name: 'warehouse', name: 'warehouseFk',
label: t('ticketList.warehouse'), label: t('globals.warehouse'),
columnClass: 'expand', component: 'select',
attrs: {
url: 'warehouses',
fields: ['id', 'name'],
},
format: (row) => row.warehouse,
columnField: {
component: null,
},
cardVisible: false,
create: false,
}, },
{ {
align: 'left', align: 'left',
@ -216,6 +237,7 @@ const columns = computed(() => [
{ {
title: t('components.smartCard.viewSummary'), title: t('components.smartCard.viewSummary'),
icon: 'preview', icon: 'preview',
isPrimary: true,
action: (row, evt) => { action: (row, evt) => {
if (evt && evt.ctrlKey) { if (evt && evt.ctrlKey) {
const url = router.resolve({ const url = router.resolve({
@ -253,7 +275,7 @@ const fetchAvailableAgencies = async (formData) => {
const { options, agency } = response; const { options, agency } = response;
if (options) agenciesOptions.value = options; if (options) agenciesOptions.value = options;
if (agency) formData.agencyModeId = agency; if (agency) formData.agencyModeId = agency.agencyModeFk;
}; };
const fetchClient = async (formData) => { const fetchClient = async (formData) => {
@ -423,6 +445,22 @@ function setReference(data) {
dialogData.value.value.description = newDescription; dialogData.value.value.description = newDescription;
} }
watch(
() => route.query.table,
(newValue) => {
if (newValue) {
const clientId = +JSON.parse(newValue)?.clientFk;
if (!clientId) return;
formInitialData.value = {
clientId,
};
if (tableRef.value) tableRef.value.create.formInitialData = { clientId };
onClientSelected({ clientId });
}
},
{ immediate: true },
);
</script> </script>
<template> <template>
@ -457,7 +495,7 @@ function setReference(data) {
urlCreate: 'Tickets/new', urlCreate: 'Tickets/new',
title: t('ticketList.createTicket'), title: t('ticketList.createTicket'),
onDataSaved: ({ id }) => tableRef.redirect(id), onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: { clientId: null }, formInitialData,
}" }"
default-mode="table" default-mode="table"
:columns="columns" :columns="columns"
@ -539,11 +577,9 @@ function setReference(data) {
:label="t('ticketList.client')" :label="t('ticketList.client')"
v-model="data.clientId" v-model="data.clientId"
:options="clientsOptions" :options="clientsOptions"
option-value="id"
option-label="name"
hide-selected hide-selected
required required
@update:model-value="(client) => onClientSelected(data)" @update:model-value="() => onClientSelected(data)"
:sort-by="'id ASC'" :sort-by="'id ASC'"
> >
<template #option="scope"> <template #option="scope">
@ -565,7 +601,6 @@ function setReference(data) {
:label="t('basicData.address')" :label="t('basicData.address')"
v-model="data.addressId" v-model="data.addressId"
:options="addressesOptions" :options="addressesOptions"
option-value="id"
option-label="nickname" option-label="nickname"
hide-selected hide-selected
map-options map-options
@ -611,6 +646,9 @@ function setReference(data) {
{{ scope.opt?.city }} {{ scope.opt?.city }}
</span> </span>
</QItemLabel> </QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.id}` }}
</QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
@ -634,8 +672,6 @@ function setReference(data) {
:label="t('globals.warehouse')" :label="t('globals.warehouse')"
v-model="data.warehouseId" v-model="data.warehouseId"
:options="warehousesOptions" :options="warehousesOptions"
option-value="id"
option-label="name"
hide-selected hide-selected
required required
@update:model-value="() => fetchAvailableAgencies(data)" @update:model-value="() => fetchAvailableAgencies(data)"
@ -695,7 +731,6 @@ function setReference(data) {
:label="t('ticketList.company')" :label="t('ticketList.company')"
v-model="dialogData.companyFk" v-model="dialogData.companyFk"
:options="companiesOptions" :options="companiesOptions"
option-value="id"
option-label="code" option-label="code"
hide-selected hide-selected
> >
@ -706,7 +741,6 @@ function setReference(data) {
:label="t('ticketList.bank')" :label="t('ticketList.bank')"
v-model="dialogData.bankFk" v-model="dialogData.bankFk"
:options="accountingOptions" :options="accountingOptions"
option-value="id"
option-label="bank" option-label="bank"
hide-selected hide-selected
@update:model-value="setReference" @update:model-value="setReference"

View File

@ -73,6 +73,7 @@ warehouses();
/> />
<FetchData <FetchData
url="AgencyModes" url="AgencyModes"
:filter="{ fields: ['id', 'name'], order: ['name ASC'] }"
@on-fetch="(data) => (agenciesOptions = data)" @on-fetch="(data) => (agenciesOptions = data)"
auto-load auto-load
/> />

View File

@ -39,6 +39,7 @@ const redirectToTravelBasicData = (_, { id }) => {
<template> <template>
<FetchData <FetchData
url="AgencyModes" url="AgencyModes"
:filter="{ fields: ['id', 'name'], order: ['name ASC'] }"
@on-fetch="(data) => (agenciesOptions = data)" @on-fetch="(data) => (agenciesOptions = data)"
auto-load auto-load
/> />

View File

@ -52,9 +52,8 @@ defineExpose({ states });
v-model="params.agencyModeFk" v-model="params.agencyModeFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
url="agencyModes" url="agencyModes"
sort-by="name ASC"
:use-like="false" :use-like="false"
option-value="id"
option-label="name"
option-filter="name" option-filter="name"
dense dense
outlined outlined

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, nextTick } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
@ -17,6 +17,12 @@ const maritalStatus = [
{ code: 'M', name: t('Married') }, { code: 'M', name: t('Married') },
{ code: 'S', name: t('Single') }, { code: 'S', name: t('Single') },
]; ];
async function setAdvancedSummary(data) {
const advanced = (await useAdvancedSummary('Workers', data.id)) ?? {};
Object.assign(form.value.formData, advanced);
await nextTick();
if (form.value) form.value.hasChanges = false;
}
</script> </script>
<template> <template>
<FetchData <FetchData
@ -36,18 +42,22 @@ const maritalStatus = [
:url-update="`Workers/${$route.params.id}`" :url-update="`Workers/${$route.params.id}`"
auto-load auto-load
model="Worker" model="Worker"
@on-fetch=" @on-fetch="setAdvancedSummary"
async (data) => {
Object.assign(data, (await useAdvancedSummary('Workers', data.id)) ?? {});
await $nextTick();
if (form) form.hasChanges = false;
}
"
> >
<template #form="{ data }"> <template #form="{ data }">
<VnRow> <VnRow>
<VnInput :label="t('Name')" clearable v-model="data.firstName" /> <VnInput
<VnInput :label="t('Last name')" clearable v-model="data.lastName" /> :label="t('Name')"
clearable
v-model="data.firstName"
:required="true"
/>
<VnInput
:label="t('Last name')"
clearable
v-model="data.lastName"
:required="true"
/>
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnInput v-model="data.phone" :label="t('Business phone')" clearable /> <VnInput v-model="data.phone" :label="t('Business phone')" clearable />

View File

@ -3,7 +3,7 @@ import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import { toDate } from 'src/filters'; import { dashIfEmpty, toDate } from 'src/filters';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import axios from 'axios'; import axios from 'axios';
@ -15,7 +15,7 @@ const quasar = useQuasar();
async function reactivateWorker() { async function reactivateWorker() {
const hasToReactive = tableRef.value.CrudModelRef.formData.find( const hasToReactive = tableRef.value.CrudModelRef.formData.find(
(data) => !data.ended (data) => !data.ended,
); );
if (hasToReactive) { if (hasToReactive) {
quasar quasar
@ -35,28 +35,44 @@ async function reactivateWorker() {
} }
} }
const columns = computed(() => [ const columns = computed(() => [
{
name: 'id',
label: t('Id'),
align: 'left',
isId: true,
cardVisible: true,
width: '40px',
},
{
name: 'isHourlyLabor',
label: t('worker.business.tableVisibleColumns.hourlyLabor'),
align: 'left',
component: 'checkbox',
cardVisible: true,
width: '60px',
},
{ {
name: 'started', name: 'started',
label: t('worker.business.tableVisibleColumns.started'), label: t('worker.business.tableVisibleColumns.started'),
align: 'left',
format: ({ started }) => toDate(started), format: ({ started }) => toDate(started),
component: 'date', component: 'date',
cardVisible: true, cardVisible: true,
create: true, create: true,
width: '90px',
}, },
{ {
name: 'ended', name: 'ended',
label: t('worker.business.tableVisibleColumns.ended'), label: t('worker.business.tableVisibleColumns.ended'),
align: 'left',
format: ({ ended }) => toDate(ended), format: ({ ended }) => toDate(ended),
component: 'date', component: 'date',
cardVisible: true, cardVisible: true,
create: true, create: true,
width: '90px',
}, },
{ {
label: t('worker.business.tableVisibleColumns.company'), label: t('worker.business.tableVisibleColumns.company'),
align: 'left', toolTip: t('worker.business.tableVisibleColumns.company'),
name: 'companyCodeFk', name: 'companyCodeFk',
component: 'select', component: 'select',
attrs: { attrs: {
@ -65,23 +81,23 @@ const columns = computed(() => [
optionLabel: 'code', optionLabel: 'code',
optionValue: 'code', optionValue: 'code',
}, },
cardVisible: true,
create: true, create: true,
width: '60px',
}, },
{ {
align: 'left',
name: 'reasonEndFk', name: 'reasonEndFk',
component: 'select', component: 'select',
label: t('worker.business.tableVisibleColumns.reasonEnd'), label: t('worker.business.tableVisibleColumns.reasonEnd'),
toolTip: t('worker.business.tableVisibleColumns.reasonEnd'),
attrs: { attrs: {
url: 'BusinessReasonEnds', url: 'BusinessReasonEnds',
fields: ['id', 'reason'], fields: ['id', 'reason'],
optionLabel: 'reason', optionLabel: 'reason',
}, },
cardVisible: true, cardVisible: true,
format: ({ reason }, dashIfEmpty) => dashIfEmpty(reason),
}, },
{ {
align: 'left',
name: 'departmentFk', name: 'departmentFk',
component: 'select', component: 'select',
label: t('worker.business.tableVisibleColumns.department'), label: t('worker.business.tableVisibleColumns.department'),
@ -89,15 +105,19 @@ const columns = computed(() => [
url: 'Departments', url: 'Departments',
fields: ['id', 'name'], fields: ['id', 'name'],
optionLabel: 'name', optionLabel: 'name',
optionValue: 'id',
}, },
cardVisible: true, cardVisible: true,
create: true, create: true,
width: '80px',
format: ({ departmentName }, dashIfEmpty) => dashIfEmpty(departmentName),
}, },
{ {
align: 'left', align: 'left',
name: 'workerBusinessProfessionalCategoryFk', name: 'workerBusinessProfessionalCategoryFk',
component: 'select', component: 'select',
label: t('worker.business.tableVisibleColumns.professionalCategory'), label: t('worker.business.tableVisibleColumns.professionalCategory'),
toolTip: t('worker.business.tableVisibleColumns.professionalCategory'),
attrs: { attrs: {
url: 'WorkerBusinessProfessionalCategories', url: 'WorkerBusinessProfessionalCategories',
fields: ['id', 'description', 'code'], fields: ['id', 'description', 'code'],
@ -105,6 +125,9 @@ const columns = computed(() => [
}, },
cardVisible: true, cardVisible: true,
create: true, create: true,
width: '100px',
format: ({ professionalDescription }, dashIfEmpty) =>
dashIfEmpty(professionalDescription),
}, },
{ {
align: 'left', align: 'left',
@ -118,6 +141,8 @@ const columns = computed(() => [
}, },
cardVisible: true, cardVisible: true,
create: true, create: true,
format: ({ calendarTypeDescription }, dashIfEmpty) =>
dashIfEmpty(calendarTypeDescription),
}, },
{ {
align: 'left', align: 'left',
@ -131,6 +156,8 @@ const columns = computed(() => [
}, },
cardVisible: true, cardVisible: true,
create: true, create: true,
width: '100px',
format: ({ workCenterName }, dashIfEmpty) => dashIfEmpty(workCenterName),
}, },
{ {
align: 'left', align: 'left',
@ -144,6 +171,7 @@ const columns = computed(() => [
}, },
cardVisible: true, cardVisible: true,
create: true, create: true,
format: ({ payrollDescription }, dashIfEmpty) => dashIfEmpty(payrollDescription),
}, },
{ {
align: 'left', align: 'left',
@ -157,6 +185,7 @@ const columns = computed(() => [
}, },
cardVisible: true, cardVisible: true,
create: true, create: true,
format: ({ occupationName }, dashIfEmpty) => dashIfEmpty(occupationName),
}, },
{ {
align: 'left', align: 'left',
@ -165,6 +194,7 @@ const columns = computed(() => [
component: 'input', component: 'input',
cardVisible: true, cardVisible: true,
create: true, create: true,
width: '50px',
}, },
{ {
align: 'left', align: 'left',
@ -177,6 +207,22 @@ const columns = computed(() => [
}, },
cardVisible: true, cardVisible: true,
create: true, create: true,
format: ({ workerBusinessTypeName }, dashIfEmpty) =>
dashIfEmpty(workerBusinessTypeName),
},
{
align: 'left',
name: 'workerBusinessAgreementFk',
label: t('worker.business.tableVisibleColumns.workerBusinessAgreementName'),
component: 'select',
attrs: {
url: 'WorkerBusinessAgreements',
fields: ['id', 'name'],
},
cardVisible: true,
create: true,
format: ({ workerBusinessAgreementName }, dashIfEmpty) =>
dashIfEmpty(workerBusinessAgreementName),
}, },
{ {
align: 'left', align: 'left',
@ -185,6 +231,7 @@ const columns = computed(() => [
component: 'input', component: 'input',
cardVisible: true, cardVisible: true,
create: true, create: true,
width: '70px',
}, },
{ {
align: 'left', align: 'left',
@ -193,6 +240,7 @@ const columns = computed(() => [
component: 'input', component: 'input',
cardVisible: true, cardVisible: true,
create: true, create: true,
width: '70px',
}, },
{ {
name: 'notes', name: 'notes',
@ -208,27 +256,27 @@ const columns = computed(() => [
<VnTable <VnTable
ref="tableRef" ref="tableRef"
data-key="WorkerBusiness" data-key="WorkerBusiness"
:url="`Workers/${entityId}/Business`" :url="`Workers/${entityId}/getWorkerBusiness`"
save-url="/Businesses/crud" save-url="/Businesses/crud"
:create="{ :create="{
urlCreate: `Workers/${entityId}/Business`, urlCreate: `Workers/${entityId}/Business`,
title: 'Create business', title: t('Create business'),
onDataSaved: () => tableRef.reload(), onDataSaved: () => tableRef.reload(),
formInitialData: {}, formInitialData: {},
}" }"
order="id DESC" order="id DESC"
:columns="columns" :columns="columns"
default-mode="card"
auto-load auto-load
:disable-option="{ table: true }" :disable-option="{ card: true }"
:right-search="false" :right-search="false"
card-class="grid-two q-gutter-x-xl q-gutter-y-md q-pr-lg q-py-lg"
:is-editable="true" :is-editable="true"
:use-model="true" :use-model="true"
:right-search-icon="false"
@save-changes="(data) => reactivateWorker(data)" @save-changes="(data) => reactivateWorker(data)"
/> />
</template> </template>
<i18n> <i18n>
es: es:
Do you want to reactivate the user?: desea reactivar el usuario? Do you want to reactivate the user?: desea reactivar el usuario?
Create business: Crear contrato
</i18n> </i18n>

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