0
0
Fork 0

Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 4988-agencySection

This commit is contained in:
Pablo Natek 2024-04-03 08:40:15 +02:00
commit 6c39c7da97
258 changed files with 15132 additions and 11920 deletions

View File

@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2416.01] - 2024-04-18
### Added
## [2414.01] - 2024-04-04
### Added
- (Tickets) => Se añade la opción de clonar ticket. #6951
- (Parking) => Se añade la sección Parking. #5186
### Changed
### Fixed
- (General) => Se corrige la redirección cuando hay 1 solo registro y cuando se aplica un filtro diferente al id al hacer una búsqueda general. #6893
## [2400.01] - 2024-01-04 ## [2400.01] - 2024-01-04
### Added ### Added

View File

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

130
Jenkinsfile vendored
View File

@ -1,99 +1,119 @@
#!/usr/bin/env groovy #!/usr/bin/env groovy
def PROTECTED_BRANCH
def BRANCH_ENV = [
test: 'test',
master: 'production'
]
node {
stage('Setup') {
env.FRONT_REPLICAS = 1
env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev'
PROTECTED_BRANCH = [
'dev',
'test',
'master'
].contains(env.BRANCH_NAME)
// https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
echo "NODE_NAME: ${env.NODE_NAME}"
echo "WORKSPACE: ${env.WORKSPACE}"
configFileProvider([
configFile(fileId: 'salix-front.properties',
variable: 'PROPS_FILE')
]) {
def props = readProperties file: PROPS_FILE
props.each {key, value -> env."${key}" = value }
props.each {key, value -> echo "${key}: ${value}" }
}
if (PROTECTED_BRANCH) {
configFileProvider([
configFile(fileId: "salix-front.branch.${env.BRANCH_NAME}",
variable: 'BRANCH_PROPS_FILE')
]) {
def props = readProperties file: BRANCH_PROPS_FILE
props.each {key, value -> env."${key}" = value }
props.each {key, value -> echo "${key}: ${value}" }
}
}
}
}
pipeline { pipeline {
agent any agent any
options { options {
disableConcurrentBuilds() disableConcurrentBuilds()
} }
tools {
nodejs 'node-v18'
}
environment { environment {
PROJECT_NAME = 'lilium' PROJECT_NAME = 'lilium'
STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}" STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}"
} }
stages { stages {
stage('Checkout') {
steps {
script {
switch (env.BRANCH_NAME) {
case 'master':
env.NODE_ENV = 'production'
env.FRONT_REPLICAS = 2
break
case 'test':
env.NODE_ENV = 'test'
env.FRONT_REPLICAS = 1
break
}
}
setEnv()
}
}
stage('Install') { stage('Install') {
environment { environment {
NODE_ENV = "" NODE_ENV = ""
} }
steps { steps {
nodejs('node-v18') { sh 'pnpm install --prefer-offline'
sh 'npm install --no-audit --prefer-offline'
}
} }
} }
stage('Test') { stage('Test') {
when { not { anyOf { when {
branch 'test' expression { !PROTECTED_BRANCH }
branch 'master' }
}}}
environment { environment {
NODE_ENV = "" NODE_ENV = ""
} }
parallel { steps {
stage('Frontend') { sh 'pnpm run test:unit:ci'
steps { }
nodejs('node-v18') { post {
sh 'npm run test:unit:ci' always {
} junit(
} testResults: 'junitresults.xml',
allowEmptyResults: true
)
} }
} }
} }
stage('Build') { stage('Build') {
when { anyOf { when {
branch 'test' expression { PROTECTED_BRANCH }
branch 'master' }
}}
environment { environment {
CREDENTIALS = credentials('docker-registry') CREDENTIALS = credentials('docker-registry')
} }
steps { steps {
nodejs('node-v18') { sh 'quasar build'
sh 'quasar build' script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
} }
dockerBuild() dockerBuild()
} }
} }
stage('Deploy') { stage('Deploy') {
when { anyOf { when {
branch 'test' expression { PROTECTED_BRANCH }
branch 'master' }
}}
environment { environment {
DOCKER_HOST = "${env.SWARM_HOST}" DOCKER_HOST = "${env.SWARM_HOST}"
} }
steps { steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
}
sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}" sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}"
} }
} }
} }
post {
always {
script {
if (!['master', 'test'].contains(env.BRANCH_NAME)) {
try {
junit 'junitresults.xml'
junit 'junit.xml'
} catch (e) {
echo e.toString()
}
}
}
}
}
} }

View File

@ -5,7 +5,7 @@ Lilium frontend
## Install the dependencies ## Install the dependencies
```bash ```bash
npm install pnpm install
``` ```
### Install quasar cli ### Install quasar cli
@ -23,13 +23,13 @@ quasar dev
### Run unit tests ### Run unit tests
```bash ```bash
npm run test:unit pnpm run test:unit
``` ```
### Run e2e tests ### Run e2e tests
```bash ```bash
npm run test:e2e pnpm run test:e2e
``` ```
### Build the app for production ### Build the app for production

BIN
bun.lockb Executable file

Binary file not shown.

9264
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

6098
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot) // app boot file (/src/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files // https://v2.quasar.dev/quasar-cli/boot-files
boot: ['i18n', 'axios', 'vnDate', 'validations'], boot: ['i18n', 'axios', 'vnDate', 'validations', 'quasar.defaults'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'], css: ['app.scss'],
@ -67,7 +67,7 @@ module.exports = configure(function (/* ctx */) {
// analyze: true, // analyze: true,
// env: {}, // env: {},
rawDefine: { rawDefine: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}, },
// ignorePublicFolder: true, // ignorePublicFolder: true,
// minify: false, // minify: false,
@ -92,7 +92,7 @@ module.exports = configure(function (/* ctx */) {
vitePlugins: [ vitePlugins: [
[ [
VueI18nPlugin({ VueI18nPlugin({
runtimeOnly: false runtimeOnly: false,
}), }),
{ {
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false` // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
@ -117,15 +117,13 @@ module.exports = configure(function (/* ctx */) {
secure: false, secure: false,
}, },
}, },
open: false,
}, },
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: { framework: {
config: { config: {
config: { config: {
brand: {
primary: 'orange',
},
dark: 'auto', dark: 'auto',
}, },
}, },

View File

@ -11,7 +11,7 @@ axios.defaults.baseURL = '/api/';
const onRequest = (config) => { const onRequest = (config) => {
const token = session.getToken(); const token = session.getToken();
if (token.length && config.headers) { if (token.length && !config.headers.Authorization) {
config.headers.Authorization = token; config.headers.Authorization = token;
} }

View File

@ -0,0 +1,5 @@
import { QTable } from 'quasar';
import setDefault from './setDefault';
setDefault(QTable, 'pagination', { rowsPerPage: 0 });
setDefault(QTable, 'hidePagination', true);

View File

@ -0,0 +1,18 @@
export default function (component, key, value) {
const prop = component.props[key];
switch (typeof prop) {
case 'object':
prop.default = value;
break;
case 'function':
component.props[key] = {
type: prop,
default: value,
};
break;
case 'undefined':
throw new Error('unknown prop: ' + key);
default:
throw new Error('unhandled type: ' + typeof prop);
}
}

21
src/boot/qformMixin.js Normal file
View File

@ -0,0 +1,21 @@
import { getCurrentInstance } from 'vue';
const filterAvailableInput = element => element.classList.contains('q-field__native') && !element.disabled
const filterAvailableText = element => element.__vueParentComponent.type.name === 'QInput' && element.__vueParentComponent?.attrs?.class !== 'vn-input-date';
export default {
mounted: function () {
const vm = getCurrentInstance();
if (vm.type.name === 'QForm')
if (!['searchbarForm','filterPanelForm'].includes(this.$el?.id)) {
// AUTOFOCUS
const elementsArray = Array.from(this.$el.elements);
const firstInputElement = elementsArray.filter(filterAvailableInput).find(filterAvailableText);
if (firstInputElement) {
firstInputElement.focus();
}
}
},
};

View File

@ -0,0 +1 @@
export * from './defaults/qTable';

6
src/boot/quasar.js Normal file
View File

@ -0,0 +1,6 @@
import { boot } from 'quasar/wrappers';
import qFormMixin from './qformMixin';
export default boot(({ app }) => {
app.mixin(qFormMixin);
});

View File

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

View File

@ -0,0 +1,174 @@
<script setup>
import { reactive, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from './FormModelPopup.vue';
import VnInputDate from './common/VnInputDate.vue';
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const router = useRouter();
const manualInvoiceFormData = reactive({
maxShipped: Date.vnNew(),
});
const formModelPopupRef = ref();
const invoiceOutSerialsOptions = ref([]);
const taxAreasOptions = ref([]);
const ticketsOptions = ref([]);
const clientsOptions = ref([]);
const isLoading = computed(() => formModelPopupRef.value?.isLoading);
const onDataSaved = async (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
if (requestResponse && requestResponse.id)
router.push({ name: 'InvoiceOutSummary', params: { id: requestResponse.id } });
};
</script>
<template>
<FetchData
url="InvoiceOutSerials"
:filter="{ where: { code: { neq: 'R' } }, order: ['code'] }"
@on-fetch="(data) => (invoiceOutSerialsOptions = data)"
auto-load
/>
<FetchData
url="TaxAreas"
:filter="{ order: ['code'] }"
@on-fetch="(data) => (taxAreasOptions = data)"
auto-load
/>
<FetchData
url="Tickets"
:filter="{ fields: ['id', 'nickname'], order: 'shipped DESC', limit: 30 }"
@on-fetch="(data) => (ticketsOptions = data)"
auto-load
/>
<FetchData
url="Clients"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
@on-fetch="(data) => (clientsOptions = data)"
auto-load
/>
<FormModelPopup
ref="formModelPopupRef"
:title="t('Create manual invoice')"
url-create="InvoiceOuts/createManualInvoice"
model="invoiceOut"
:form-initial-data="manualInvoiceFormData"
@on-data-saved="onDataSaved"
>
<template #form-inputs="{ data }">
<span v-if="isLoading" class="text-primary invoicing-text">
<QIcon name="warning" class="fill-icon q-mr-sm" size="md" />
{{ t('Invoicing in progress...') }}
</span>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Ticket')"
:options="ticketsOptions"
hide-selected
option-label="id"
option-value="id"
v-model="data.ticketFk"
@update:model-value="data.clientFk = null"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{
scope.opt?.nickname
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<span class="row items-center" style="max-width: max-content">{{
t('Or')
}}</span>
<div class="col">
<VnSelectFilter
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.clientFk"
@update:model-value="data.ticketFk = null"
/>
</div>
<div class="col">
<VnInputDate :label="t('Max date')" v-model="data.maxShipped" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Serial')"
:options="invoiceOutSerialsOptions"
hide-selected
option-label="description"
option-value="code"
v-model="data.serial"
:required="true"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Area')"
:options="taxAreasOptions"
hide-selected
option-label="code"
option-value="code"
v-model="data.taxArea"
:required="true"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
:label="t('Reference')"
type="textarea"
v-model="data.reference"
fill-input
autogrow
/>
</VnRow>
</template>
</FormModelPopup>
</template>
<style lang="scss" scoped>
.invoicing-text {
display: flex;
justify-content: center;
align-items: center;
color: $primary;
font-size: 24px;
margin-bottom: 8px;
}
</style>
<i18n>
es:
Create manual invoice: Crear factura manual
Ticket: Ticket
Client: Cliente
Max date: Fecha límite
Serial: Serie
Area: Area
Reference: Referencia
Or: O
Invoicing in progress...: Facturación en progreso...
</i18n>

View File

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

View File

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

View File

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

View File

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

View File

@ -202,7 +202,6 @@ const selectItem = ({ id }) => {
<QTable <QTable
:columns="tableColumns" :columns="tableColumns"
:rows="tableRows" :rows="tableRows"
:pagination="{ rowsPerPage: 0 }"
:loading="loading" :loading="loading"
:hide-header="!tableRows || !tableRows.length > 0" :hide-header="!tableRows || !tableRows.length > 0"
:no-data-label="t('Enter a new search')" :no-data-label="t('Enter a new search')"

View File

@ -200,7 +200,6 @@ const selectTravel = ({ id }) => {
<QTable <QTable
:columns="tableColumns" :columns="tableColumns"
:rows="tableRows" :rows="tableRows"
:pagination="{ rowsPerPage: 0 }"
:loading="loading" :loading="loading"
:hide-header="!tableRows || !tableRows.length > 0" :hide-header="!tableRows || !tableRows.length > 0"
:no-data-label="t('Enter a new search')" :no-data-label="t('Enter a new search')"

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue'; import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
@ -8,6 +9,8 @@ import { useStateStore } from 'stores/useStateStore';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import SkeletonForm from 'components/ui/SkeletonForm.vue'; import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile';
const quasar = useQuasar(); const quasar = useQuasar();
const state = useState(); const state = useState();
@ -41,6 +44,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
defaultButtons: {
type: Object,
default: () => {},
},
autoLoad: { autoLoad: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -59,14 +66,18 @@ const $props = defineProps({
type: Function, type: Function,
default: null, default: null,
}, },
clearStoreOnUnmount: {
type: Boolean,
default: true,
},
saveFn: {
type: Function,
default: null,
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
defineExpose({
save,
});
const componentIsRendered = ref(false); const componentIsRendered = ref(false);
onMounted(async () => { onMounted(async () => {
@ -75,9 +86,8 @@ onMounted(async () => {
}); });
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
if ($props.formInitialData && !$props.autoLoad) { state.set($props.model, $props.formInitialData);
state.set($props.model, $props.formInitialData); if ($props.autoLoad && !$props.formInitialData) {
} else {
await fetch(); await fetch();
} }
@ -90,8 +100,26 @@ onMounted(async () => {
} }
}); });
onBeforeRouteLeave((to, from, next) => {
if (!hasChanges.value) next();
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('Unsaved changes will be lost'),
message: t('Are you sure exit without saving?'),
promise: () => next(),
},
});
});
onUnmounted(() => { onUnmounted(() => {
state.unset($props.model); // Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas.
if (hasChanges.value) {
state.set($props.model, originalData.value);
return;
}
if ($props.clearStoreOnUnmount) state.unset($props.model);
}); });
const isLoading = ref(false); const isLoading = ref(false);
@ -101,7 +129,19 @@ const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({ ...$props.formInitialData }); const originalData = ref({ ...$props.formInitialData });
const formData = computed(() => state.get($props.model)); const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({
save: {
color: 'primary',
icon: 'restart_alt',
label: 'globals.save',
},
reset: {
color: 'primary',
icon: 'save',
label: 'globals.reset',
},
...$props.defaultButtons,
}));
const startFormWatcher = () => { const startFormWatcher = () => {
watch( watch(
() => formData.value, () => formData.value,
@ -113,10 +153,6 @@ const startFormWatcher = () => {
); );
}; };
function tMobile(...args) {
if (!quasar.platform.is.mobile) return t(...args);
}
async function fetch() { async function fetch() {
const { data } = await axios.get($props.url, { const { data } = await axios.get($props.url, {
params: { filter: JSON.stringify($props.filter) }, params: { filter: JSON.stringify($props.filter) },
@ -138,17 +174,20 @@ async function save() {
try { try {
const body = $props.mapper ? $props.mapper(formData.value) : formData.value; const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
let response; let response;
if ($props.urlCreate) { if ($props.saveFn) response = await $props.saveFn(body);
response = await axios.post($props.urlCreate, body); else
notify('globals.dataCreated', 'positive'); response = await axios[$props.urlCreate ? 'post' : 'patch'](
} else { $props.urlCreate || $props.urlUpdate || $props.url,
response = await axios.patch($props.urlUpdate || $props.url, body); body
} );
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
emit('onDataSaved', formData.value, response?.data); emit('onDataSaved', formData.value, response?.data);
originalData.value = JSON.parse(JSON.stringify(formData.value)); originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false; hasChanges.value = false;
} catch (err) { } catch (err) {
notify('errors.create', 'negative'); console.error(err);
notify('errors.writeRequest', 'negative');
} }
isLoading.value = false; isLoading.value = false;
} }
@ -184,6 +223,11 @@ watch(formUrl, async () => {
reset(); reset();
fetch(); fetch();
}); });
defineExpose({
save,
isLoading,
});
</script> </script>
<template> <template>
<div class="column items-center full-width"> <div class="column items-center full-width">
@ -212,21 +256,21 @@ watch(formUrl, async () => {
<QBtnGroup push class="q-gutter-x-sm"> <QBtnGroup push class="q-gutter-x-sm">
<slot name="moreActions" /> <slot name="moreActions" />
<QBtn <QBtn
:label="tMobile('globals.reset')" :label="tMobile(defaultButtons.reset.label)"
color="primary" :color="defaultButtons.reset.color"
icon="restart_alt" :icon="defaultButtons.reset.icon"
flat flat
@click="reset" @click="reset"
:disable="!hasChanges" :disable="!hasChanges"
:title="t('globals.reset')" :title="t(defaultButtons.reset.label)"
/> />
<QBtn <QBtn
:label="tMobile('globals.save')" :label="tMobile(defaultButtons.save.label)"
color="primary" :color="defaultButtons.save.color"
icon="save" :icon="defaultButtons.save.icon"
@click="save" @click="save"
:disable="!hasChanges" :disable="!hasChanges"
:title="t('globals.save')" :title="t(defaultButtons.save.label)"
/> />
</QBtnGroup> </QBtnGroup>
</div> </div>
@ -249,3 +293,8 @@ watch(formUrl, async () => {
padding: 32px; padding: 32px;
} }
</style> </style>
<i18n>
es:
Unsaved changes will be lost: Los cambios que no haya guardado se perderán
Are you sure exit without saving?: ¿Seguro que quiere salir sin guardar?
</i18n>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
@ -39,27 +39,34 @@ const $props = defineProps({
const { t } = useI18n(); const { t } = useI18n();
const formModelRef = ref(null);
const closeButton = ref(null); const closeButton = ref(null);
const isLoading = ref(false);
const onDataSaved = (dataSaved) => { const onDataSaved = (formData, requestResponse) => {
emit('onDataSaved', dataSaved); emit('onDataSaved', formData, requestResponse);
closeForm(); closeForm();
}; };
const closeForm = () => { const isLoading = computed(() => formModelRef.value?.isLoading);
const closeForm = async () => {
if (closeButton.value) closeButton.value.click(); if (closeButton.value) closeButton.value.click();
}; };
defineExpose({
isLoading,
});
</script> </script>
<template> <template>
<FormModel <FormModel
ref="formModelRef"
:form-initial-data="formInitialData" :form-initial-data="formInitialData"
:observe-form-changes="false" :observe-form-changes="false"
:default-actions="false" :default-actions="false"
:url-create="urlCreate" :url-create="urlCreate"
:model="model" :model="model"
@on-data-saved="onDataSaved($event)" @on-data-saved="onDataSaved"
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<span ref="closeButton" class="close-icon" v-close-popup> <span ref="closeButton" class="close-icon" v-close-popup>

View File

@ -0,0 +1,96 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const emit = defineEmits(['onSubmit']);
const $props = defineProps({
title: {
type: String,
default: '',
},
subtitle: {
type: String,
default: '',
},
defaultSubmitButton: {
type: Boolean,
default: true,
},
defaultCancelButton: {
type: Boolean,
default: true,
},
customSubmitButtonLabel: {
type: String,
default: '',
},
});
const { t } = useI18n();
const closeButton = ref(null);
const isLoading = ref(false);
const onSubmit = () => {
emit('onSubmit');
closeForm();
};
const closeForm = () => {
if (closeButton.value) closeButton.value.click();
};
</script>
<template>
<QForm
@submit="onSubmit($event)"
class="all-pointer-events full-width"
style="max-width: 800px"
>
<QCard class="q-pa-lg">
<span ref="closeButton" class="close-icon" v-close-popup>
<QIcon name="close" size="sm" />
</span>
<h1 class="title">{{ title }}</h1>
<p>{{ subtitle }}</p>
<slot name="form-inputs" />
<div class="q-mt-lg row justify-end">
<QBtn
v-if="defaultSubmitButton"
:label="customSubmitButtonLabel || t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<QBtn
v-if="defaultCancelButton"
:label="t('globals.cancel')"
color="primary"
flat
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
v-close-popup
/>
<slot name="customButtons" />
</div>
</QCard>
</QForm>
</template>
<style lang="scss" scoped>
.title {
font-size: 17px;
font-weight: bold;
line-height: 20px;
}
.close-icon {
position: absolute;
top: 20px;
right: 20px;
cursor: pointer;
}
</style>

View File

@ -234,6 +234,6 @@ async function togglePinned(item, event) {
max-width: 256px; max-width: 256px;
} }
.header { .header {
color: #999999; color: var(--vn-label-color);
} }
</style> </style>

View File

@ -2,7 +2,7 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t, te } = useI18n();
const props = defineProps({ const props = defineProps({
item: { item: {
@ -11,19 +11,30 @@ const props = defineProps({
}, },
}); });
const item = computed(() => props.item); // eslint-disable-line vue/no-dupe-keys const itemComputed = computed(() => {
const item = JSON.parse(JSON.stringify(props.item));
const [, , section] = item.title.split('.');
if (!te(item.title)) item.title = t(`globals.pageTitles.${section}`);
return item;
});
</script> </script>
<template> <template>
<QItem active-class="text-primary" :to="{ name: item.name }" clickable v-ripple> <QItem
<QItemSection avatar v-if="item.icon"> active-class="text-primary"
<QIcon :name="item.icon" /> :to="{ name: itemComputed.name }"
clickable
v-ripple
>
<QItemSection avatar v-if="itemComputed.icon">
<QIcon :name="itemComputed.icon" />
</QItemSection> </QItemSection>
<QItemSection avatar v-if="!item.icon"> <QItemSection avatar v-if="!itemComputed.icon">
<QIcon name="disabled_by_default" /> <QIcon name="disabled_by_default" />
</QItemSection> </QItemSection>
<QItemSection>{{ t(item.title) }}</QItemSection> <QItemSection>{{ t(itemComputed.title) }}</QItemSection>
<QItemSection side> <QItemSection side>
<slot name="side" :item="item" /> <slot name="side" :item="itemComputed" />
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>

View File

@ -10,12 +10,12 @@ import UserPanel from 'components/UserPanel.vue';
import VnBreadcrumbs from './common/VnBreadcrumbs.vue'; import VnBreadcrumbs from './common/VnBreadcrumbs.vue';
const { t } = useI18n(); const { t } = useI18n();
const session = useSession();
const stateStore = useStateStore(); const stateStore = useStateStore();
const quasar = useQuasar(); const quasar = useQuasar();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const token = session.getToken(); const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const appName = 'Lilium'; const appName = 'Lilium';
onMounted(() => stateStore.setMounted()); onMounted(() => stateStore.setMounted());
@ -106,7 +106,7 @@ const pinnedModulesRef = ref();
width: max-content; width: max-content;
} }
.q-header { .q-header {
background-color: var(--vn-dark); background-color: var(--vn-section-color);
} }
</style> </style>
<i18n> <i18n>

View File

@ -0,0 +1,168 @@
<script setup>
import { ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({
invoiceOutData: {
type: Object,
default: () => {},
},
});
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const transferInvoiceParams = reactive({
id: $props.invoiceOutData?.id,
refFk: $props.invoiceOutData?.ref,
});
const closeButton = ref(null);
const clientsOptions = ref([]);
const rectificativeTypeOptions = ref([]);
const siiTypeInvoiceOutsOptions = ref([]);
const invoiceCorrectionTypesOptions = ref([]);
const closeForm = () => {
if (closeButton.value) closeButton.value.click();
};
const transferInvoice = async () => {
try {
const { data } = await axios.post(
'InvoiceOuts/transferInvoice',
transferInvoiceParams
);
notify(t('Transferred invoice'), 'positive');
closeForm();
router.push('InvoiceOutSummary', { id: data.id });
} catch (err) {
console.error('Error transfering invoice', err);
}
};
</script>
<template>
<FetchData
url="Clients"
@on-fetch="(data) => (clientsOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'id', limit: 30 }"
auto-load
/>
<FetchData
url="CplusRectificationTypes"
:filter="{ order: 'description' }"
@on-fetch="(data) => (rectificativeTypeOptions = data)"
auto-load
/>
<FetchData
url="SiiTypeInvoiceOuts"
:filter="{ where: { code: { like: 'R%' } } }"
@on-fetch="(data) => (siiTypeInvoiceOutsOptions = data)"
auto-load
/>
<FetchData
url="InvoiceCorrectionTypes"
@on-fetch="(data) => (invoiceCorrectionTypesOptions = data)"
auto-load
/>
<FormPopup
@on-submit="transferInvoice()"
:title="t('Transfer invoice')"
:custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false"
>
<template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
v-model="transferInvoiceParams.newClientFk"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
#{{ scope.opt?.id }} -
{{ scope.opt?.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Rectificative type')"
:options="rectificativeTypeOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.cplusRectificationTypeFk"
:required="true"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Class')"
:options="siiTypeInvoiceOutsOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.siiTypeInvoiceOutFk"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.code }} -
{{ scope.opt?.description }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('Type')"
:options="invoiceCorrectionTypesOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.invoiceCorrectionTypeFk"
:required="true"
/>
</div>
</VnRow>
</template>
</FormPopup>
</template>
<i18n>
es:
Transfer invoice: Transferir factura
Transfer client: Transferir cliente
Client: Cliente
Rectificative type: Tipo rectificativa
Class: Clase
Type: Tipo
Transferred invoice: Factura transferida
</i18n>

View File

@ -7,12 +7,16 @@ import axios from 'axios';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { localeEquivalence } from 'src/i18n/index'; import { localeEquivalence } from 'src/i18n/index';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
const router = useRouter(); const router = useRouter();
const { t, locale } = useI18n(); const { t, locale } = useI18n();
import { useClipboard } from 'src/composables/useClipboard'; import { useClipboard } from 'src/composables/useClipboard';
import { ref } from 'vue';
const { copyText } = useClipboard(); const { copyText } = useClipboard();
const userLocale = computed({ const userLocale = computed({
get() { get() {
@ -44,7 +48,10 @@ const darkMode = computed({
}); });
const user = state.getUser(); const user = state.getUser();
const token = session.getToken(); const token = session.getTokenMultimedia();
const warehousesData = ref();
const companiesData = ref();
const accountBankData = ref();
onMounted(async () => { onMounted(async () => {
updatePreferences(); updatePreferences();
@ -87,10 +94,28 @@ function copyUserToken() {
</script> </script>
<template> <template>
<QMenu anchor="bottom left"> <FetchData
url="Warehouses"
order="name"
@on-fetch="(data) => (warehousesData = data)"
auto-load
/>
<FetchData
url="Companies"
order="name"
@on-fetch="(data) => (companiesData = data)"
auto-load
/>
<FetchData
url="Accountings"
order="name"
@on-fetch="(data) => (accountBankData = data)"
auto-load
/>
<QMenu anchor="bottom left" class="bg-vn-section-color">
<div class="row no-wrap q-pa-md"> <div class="row no-wrap q-pa-md">
<div class="column panel"> <div class="col column">
<div class="text-h6 q-mb-md"> <div class="text-h6 q-ma-sm q-mb-none">
{{ t('components.userPanel.settings') }} {{ t('components.userPanel.settings') }}
</div> </div>
<QToggle <QToggle
@ -114,7 +139,7 @@ function copyUserToken() {
<QSeparator vertical inset class="q-mx-lg" /> <QSeparator vertical inset class="q-mx-lg" />
<div class="column items-center panel"> <div class="col column items-center q-mb-sm">
<QAvatar size="80px"> <QAvatar size="80px">
<QImg <QImg
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`" :src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
@ -131,7 +156,6 @@ function copyUserToken() {
> >
@{{ user.name }} @{{ user.name }}
</div> </div>
<QBtn <QBtn
id="logout" id="logout"
color="orange" color="orange"
@ -141,17 +165,63 @@ function copyUserToken() {
icon="logout" icon="logout"
@click="logout()" @click="logout()"
v-close-popup v-close-popup
dense
/> />
</div> </div>
</div> </div>
<QSeparator inset class="q-mx-lg" />
<div class="col q-gutter-xs q-pa-md">
<VnRow>
<VnSelectFilter
:label="t('components.userPanel.localWarehouse')"
v-model="user.localWarehouseFk"
:options="warehousesData"
option-label="name"
option-value="id"
/>
<VnSelectFilter
:label="t('components.userPanel.localBank')"
hide-selected
v-model="user.localBankFk"
:options="accountBankData"
option-label="bank"
option-value="id"
></VnSelectFilter>
</VnRow>
<VnRow>
<VnSelectFilter
:label="t('components.userPanel.localCompany')"
hide-selected
v-model="user.companyFk"
:options="companiesData"
option-label="code"
option-value="id"
/>
<VnSelectFilter
:label="t('components.userPanel.userWarehouse')"
hide-selected
v-model="user.warehouseFk"
:options="warehousesData"
option-label="name"
option-value="id"
/>
</VnRow>
<VnRow>
<VnSelectFilter
:label="t('components.userPanel.userCompany')"
hide-selected
v-model="user.companyFk"
:options="companiesData"
option-label="code"
option-value="id"
style="flex: 0"
/>
</VnRow>
</div>
</QMenu> </QMenu>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.panel {
width: 150px;
}
.copyText { .copyText {
&:hover { &:hover {
cursor: alias; cursor: alias;

View File

@ -0,0 +1,156 @@
<script setup>
import {useDialogPluginComponent} from 'quasar';
import {useI18n} from 'vue-i18n';
import {computed, ref} from 'vue';
import VnInput from 'components/common/VnInput.vue';
import axios from 'axios';
import useNotify from "composables/useNotify";
const MESSAGE_MAX_LENGTH = 160;
const {t} = useI18n();
const {notify} = useNotify();
const props = defineProps({
title: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
destination: {
type: String,
required: true,
},
destinationFk: {
type: String,
required: true,
},
data: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits([...useDialogPluginComponent.emits, 'sent']);
const {dialogRef, onDialogHide} = useDialogPluginComponent();
const smsRules = [
(val) => (val && val.length > 0) || t("The message can't be empty"),
(val) =>
(val && new Blob([val]).size <= MESSAGE_MAX_LENGTH) ||
t("The message it's too long"),
];
const message = ref('');
const charactersRemaining = computed(
() => MESSAGE_MAX_LENGTH - new Blob([message.value]).size
);
const charactersChipColor = computed(() => {
if (charactersRemaining.value < 0) {
return 'negative';
}
if (charactersRemaining.value <= 25) {
return 'warning';
}
return 'primary';
});
const onSubmit = async () => {
if (!props.destination) {
throw new Error(`The destination can't be empty`);
}
if (!message.value) {
throw new Error(`The message can't be empty`);
}
if (charactersRemaining.value < 0) {
throw new Error(`The message it's too long`);
}
const response = await axios.post(props.url, {
destination: props.destination,
destinationFk: props.destinationFk,
message: message.value,
...props.data,
});
if (response.data) {
emit('sent', response.data);
notify('globals.smsSent', 'positive');
}
emit('ok', response.data);
emit('hide', response.data);
};
</script>
<template>
<QDialog ref="dialogRef" @hide="onDialogHide">
<QCard class="full-width dialog">
<QCardSection class="row">
<span v-if="title" class="text-h6">{{ title }}</span>
<QSpace />
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<QForm @submit="onSubmit">
<QCardSection>
<VnInput
v-model="message"
type="textarea"
:rules="smsRules"
:label="t('Message')"
:placeholder="t('Message')"
:rows="5"
required
clearable
no-error-icon
>
<template #append>
<QIcon name="info">
<QTooltip>
{{
t(
'Special characters like accents counts as a multiple'
)
}}
</QTooltip>
</QIcon>
</template>
</VnInput>
<p class="q-mb-none q-mt-md">
{{ t('Characters remaining') }}:
<QChip :color="charactersChipColor">
{{ charactersRemaining }}
</QChip>
</p>
</QCardSection>
<QCardActions align="right">
<QBtn type="button" flat v-close-popup class="text-primary">
{{ t('globals.cancel') }}
</QBtn>
<QBtn type="submit" color="primary">{{ t('Send') }}</QBtn>
</QCardActions>
</QForm>
</QCard>
</QDialog>
</template>
<style lang="scss" scoped>
.dialog {
max-width: 450px;
}
</style>
<i18n>
es:
Message: Mensaje
Send: Enviar
Characters remaining: Carácteres restantes
Special characters like accents counts as a multiple: Carácteres especiales como los acentos cuentan como varios
The destination can't be empty: El destinatario no puede estar vacio
The message can't be empty: El mensaje no puede estar vacio
The message it's too long: El mensaje es demasiado largo
</i18n>

View File

@ -5,16 +5,16 @@ import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useCamelCase } from 'src/composables/useCamelCase'; import { useCamelCase } from 'src/composables/useCamelCase';
const router = useRouter(); const { currentRoute } = useRouter();
const quasar = useQuasar(); const { screen } = useQuasar();
const { t } = useI18n(); const { t, te } = useI18n();
let matched = ref([]); let matched = ref([]);
let breadcrumbs = ref([]); let breadcrumbs = ref([]);
let root = ref(null); let root = ref(null);
watchEffect(() => { watchEffect(() => {
matched.value = router.currentRoute.value.matched.filter( matched.value = currentRoute.value.matched.filter(
(matched) => Object.keys(matched.meta).length (matched) => Object.keys(matched.meta).length
); );
breadcrumbs.value.length = 0; breadcrumbs.value.length = 0;
@ -34,13 +34,17 @@ function getBreadcrumb(param) {
icon: param.meta.icon, icon: param.meta.icon,
path: param.path, path: param.path,
root: root.value, root: root.value,
locale: t(`globals.pageTitles.${param.meta.title}`),
}; };
if (quasar.screen.gt.sm) { if (screen.gt.sm) {
breadcrumb.name = param.name; breadcrumb.name = param.name;
breadcrumb.title = useCamelCase(param.meta.title); breadcrumb.title = useCamelCase(param.meta.title);
} }
const moduleLocale = `${breadcrumb.root}.pageTitles.${breadcrumb.title}`;
if (te(moduleLocale)) breadcrumb.locale = t(moduleLocale);
return breadcrumb; return breadcrumb;
} }
</script> </script>
@ -50,7 +54,7 @@ function getBreadcrumb(param) {
v-for="(breadcrumb, index) of breadcrumbs" v-for="(breadcrumb, index) of breadcrumbs"
:key="index" :key="index"
:icon="breadcrumb.icon" :icon="breadcrumb.icon"
:label="t(`${breadcrumb.root}.pageTitles.${breadcrumb.title}`)" :label="breadcrumb.locale"
:to="breadcrumb.path" :to="breadcrumb.path"
/> />
</QBreadcrumbs> </QBreadcrumbs>
@ -71,7 +75,7 @@ function getBreadcrumb(param) {
} }
&--last, &--last,
&__separator { &__separator {
color: var(--vn-label); color: var(--vn-label-color);
} }
} }
@media (max-width: $breakpoint-md) { @media (max-width: $breakpoint-md) {

View File

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

View File

@ -0,0 +1,208 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
const route = useRoute();
const { t } = useI18n();
const emit = defineEmits(['onDataSaved']);
const $props = defineProps({
model: {
type: String,
required: true,
},
defaultDmsCode: {
type: String,
default: null,
},
formInitialData: {
type: Object,
default: null,
},
url: {
type: String,
default: null,
},
});
const warehouses = ref();
const companies = ref();
const dmsTypes = ref();
const allowedContentTypes = ref();
const inputFileRef = ref();
const dms = ref({});
onMounted(() => {
defaultData();
if (!$props.formInitialData)
dms.value.description = t($props.model + 'Description', dms.value);
});
function onFileChange(files) {
dms.value.hasFileAttached = !!files;
dms.value.file = files?.name;
}
function mapperDms(data) {
const formData = new FormData();
const { files } = data;
if (files) formData.append(files?.name, files);
delete data.files;
const dms = {
hasFile: !!data.hasFile,
hasFileAttached: data.hasFileAttached,
reference: data.reference,
warehouseId: data.warehouseFk,
companyId: data.companyFk,
dmsTypeId: data.dmsTypeFk,
description: data.description,
};
return [formData, { params: dms }];
}
function getUrl() {
if ($props.url) return $props.url;
if ($props.formInitialData) return 'dms/' + $props.formInitialData.id + '/updateFile';
return `${$props.model}/${route.params.id}/uploadFile`;
}
async function save() {
const body = mapperDms(dms.value);
const response = await axios.post(getUrl(), body[0], body[1]);
emit('onDataSaved', body[1].params, response);
}
function defaultData() {
if ($props.formInitialData) return (dms.value = $props.formInitialData);
return addDefaultData({
reference: route.params.id,
});
}
function setDmsTypes(data) {
dmsTypes.value = data;
if (!$props.formInitialData && $props.defaultDmsCode) {
const { id } = data.find((dmsType) => dmsType.code == $props.defaultDmsCode);
addDefaultData({ dmsTypeFk: id });
}
}
function addDefaultData(data) {
Object.assign(dms.value, data);
}
</script>
<template>
<FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load />
<FetchData url="Companies" @on-fetch="(data) => (companies = data)" auto-load />
<FetchData url="DmsTypes" @on-fetch="setDmsTypes" auto-load />
<FetchData
url="DmsContainers/allowedContentTypes"
@on-fetch="(data) => (allowedContentTypes = data.join(','))"
auto-load
/>
<FetchData
url="UserConfigs/getUserConfig"
@on-fetch="addDefaultData"
:auto-load="!$props.formInitialData"
/>
<FormModelPopup
:title="formInitialData ? t('globals.edit') : t('globals.create')"
model="dms"
:form-initial-data="formInitialData ?? {}"
:save-fn="save"
>
<template #form-inputs>
<div class="q-gutter-y-ms">
<VnRow>
<VnInput :label="t('globals.reference')" v-model="dms.reference" />
<VnSelectFilter
:label="t('globals.company')"
v-model="dms.companyFk"
:options="companies"
option-value="id"
option-label="code"
input-debounce="0"
/>
</VnRow>
<VnRow>
<VnSelectFilter
:label="t('globals.warehouse')"
v-model="dms.warehouseFk"
:options="warehouses"
option-value="id"
option-label="name"
input-debounce="0"
/>
<VnSelectFilter
:label="t('globals.type')"
v-model="dms.dmsTypeFk"
:options="dmsTypes"
option-value="id"
option-label="name"
input-debounce="0"
/>
</VnRow>
<QInput
:label="t('globals.description')"
v-model="dms.description"
type="textarea"
/>
<QFile
ref="inputFileRef"
:label="t('entry.buys.file')"
v-model="dms.files"
:multiple="false"
:accept="allowedContentTypes"
@update:model-value="onFileChange(dms.files)"
class="required"
:display-value="dms.file"
>
<template #append>
<QIcon
name="vn:attach"
class="cursor-pointer"
@click="inputFileRef.pickFiles()"
>
<QTooltip>{{ t('globals.selectFile') }}</QTooltip>
</QIcon>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
t('contentTypesInfo', { allowedContentTypes })
}}</QTooltip>
</QIcon>
</template>
</QFile>
<QCheckbox
v-model="dms.hasFile"
:label="t('Generate identifier for original file')"
/>
</div>
</template>
</FormModelPopup>
</template>
<style scoped>
.q-gutter-y-ms {
display: grid;
row-gap: 20px;
}
</style>
<i18n>
en:
contentTypesInfo: Allowed file types {allowedContentTypes}
EntryDmsDescription: Reference {reference}
SupplierDmsDescription: Reference {reference}
es:
Generate identifier for original file: Generar identificador para archivo original
contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes}
EntryDmsDescription: Referencia {reference}
SupplierDmsDescription: Referencia {reference}
</i18n>

View File

@ -0,0 +1,315 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useQuasar, QCheckbox, QBtn, QInput } from 'quasar';
import axios from 'axios';
import FetchData from 'components/FetchData.vue';
import VnDms from 'src/components/common/VnDms.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import { downloadFile } from 'src/composables/downloadFile';
const route = useRoute();
const quasar = useQuasar();
const { t } = useI18n();
const rows = ref();
const dmsRef = ref();
const formDialog = ref({});
const $props = defineProps({
model: {
type: String,
required: true,
},
updateModel: {
type: String,
default: null,
},
defaultDmsCode: {
type: String,
required: true,
},
filter: {
type: String,
required: true,
},
});
const dmsFilter = {
include: {
relation: 'dms',
scope: {
fields: [
'dmsTypeFk',
'reference',
'hardCopyNumber',
'workerFk',
'description',
'hasFile',
'file',
'created',
'companyFk',
'warehouseFk',
],
include: [
{
relation: 'dmsType',
scope: {
fields: ['name'],
},
},
{
relation: 'worker',
scope: {
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['name'],
},
},
},
},
],
},
},
order: ['dmsFk DESC'],
};
const columns = computed(() => [
{
align: 'left',
field: 'id',
label: t('globals.id'),
name: 'id',
component: 'span',
},
{
align: 'left',
field: 'type',
label: t('globals.type'),
name: 'type',
component: QInput,
props: (prop) => ({
readonly: true,
borderless: true,
'model-value': prop.row.dmsType.name,
}),
},
{
align: 'left',
field: 'order',
label: t('globals.order'),
name: 'order',
component: 'span',
},
{
align: 'left',
field: 'reference',
label: t('globals.reference'),
name: 'reference',
component: 'span',
},
{
align: 'left',
field: 'description',
label: t('globals.description'),
name: 'description',
component: 'span',
},
{
align: 'left',
field: 'hasFile',
label: t('globals.original'),
name: 'hasFile',
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
},
{
align: 'left',
field: 'file',
label: t('globals.file'),
name: 'file',
component: 'span',
},
{
field: 'options',
name: 'options',
components: [
{
component: QBtn,
props: () => ({
icon: 'cloud_download',
flat: true,
color: 'primary',
}),
click: (prop) => downloadFile(prop.row.id),
},
{
component: QBtn,
props: () => ({
icon: 'edit',
flat: true,
color: 'primary',
}),
click: (prop) => showFormDialog(prop.row),
},
{
component: QBtn,
props: () => ({
icon: 'delete',
flat: true,
color: 'primary',
}),
click: (prop) => deleteDms(prop.row.id),
},
],
},
]);
function setData(data) {
const newData = data.map((value) => value.dms);
rows.value = newData;
}
function deleteDms(dmsFk) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('globals.confirmDeletion'),
message: t('globals.confirmDeletionMessage'),
},
})
.onOk(async () => {
await axios.post(`${$props.model}/${dmsFk}/removeFile`);
const index = rows.value.findIndex((row) => row.id == dmsFk);
rows.value.splice(index, 1);
});
}
function showFormDialog(dms) {
if (dms) dms = parseDms(dms);
formDialog.value = {
show: true,
dms,
};
}
function parseDms(data) {
for (let prop in data) {
if (prop.endsWith('Fk')) data[prop.replace('Fk', 'Id')] = data[prop];
}
return data;
}
</script>
<template>
<FetchData
ref="dmsRef"
:url="$props.model"
:filter="dmsFilter"
:where="{ [$props.filter]: route.params.id }"
@on-fetch="setData"
auto-load
/>
<QTable
:columns="columns"
:rows="rows"
class="full-width q-mt-md"
hide-bottom
row-key="clientFk"
:grid="$q.screen.lt.sm"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props">
<component
v-if="props.col.component"
:is="props.col.component"
v-bind="props.col.props && props.col.props(props)"
>
<span
v-if="props.col.component == 'span'"
style="white-space: wrap"
>{{ props.value }}</span
>
</component>
</QTr>
<div class="flex justify-center" v-if="props.col.name == 'options'">
<div v-for="button of props.col.components" :key="button.id">
<component
:is="button.component"
v-bind="button.props(props)"
@click="button.click(props)"
/>
</div>
</div>
</QTd>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard
bordered
flat
@keyup.ctrl.enter.stop="claimDevelopmentForm?.saveChanges()"
>
<QSeparator />
<QList dense>
<QItem v-for="col in props.cols" :key="col.name">
<div v-if="col.name != 'options'" class="row">
<span class="labelColor">{{ col.label }}:</span>
<span>{{ col.value }}</span>
</div>
<div v-if="col.name == 'options'" class="row">
<div
v-for="button of col.components"
:key="button.id"
class="row"
>
<component
:is="button.component"
v-bind="button.props(col)"
@click="button.click(col)"
/>
</div>
</div>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
<QDialog v-model="formDialog.show">
<VnDms
:model="updateModel ?? model"
:default-dms-code="defaultDmsCode"
:form-initial-data="formDialog.dms"
@on-data-saved="dmsRef.fetch()"
:description="$props.description"
/>
</QDialog>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="showFormDialog()" />
</QPageSticky>
</template>
<style scoped>
.q-gutter-y-ms {
display: grid;
row-gap: 20px;
}
.labelColor {
color: var(--vn-label-color);
}
</style>
<i18n>
en:
contentTypesInfo: Allowed file types {allowedContentTypes}
es:
contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes}
Generate identifier for original file: Generar identificador para archivo original
</i18n>

View File

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

View File

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

View File

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

View File

@ -403,7 +403,7 @@ setLogTree();
auto-load auto-load
/> />
<div <div
class="column items-center logs origin-log" class="column items-center logs origin-log q-mt-md"
v-for="(originLog, originLogIndex) in logTree" v-for="(originLog, originLogIndex) in logTree"
:key="originLogIndex" :key="originLogIndex"
> >
@ -819,7 +819,7 @@ setLogTree();
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-card { .q-card {
background-color: var(--vn-gray); background-color: var(--vn-section-color);
} }
.q-item { .q-item {
min-height: 0px; min-height: 0px;
@ -836,7 +836,7 @@ setLogTree();
max-width: 400px; max-width: 400px;
& > .header { & > .header {
color: $dark; color: var(--vn-section-color);
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -916,7 +916,7 @@ setLogTree();
font-style: italic; font-style: italic;
} }
.model-id { .model-id {
color: var(--vn-label); color: var(--vn-label-color);
font-size: 0.9rem; font-size: 0.9rem;
} }
.q-btn { .q-btn {
@ -942,7 +942,7 @@ setLogTree();
} }
.change-info { .change-info {
overflow: hidden; overflow: hidden;
background-color: var(--vn-dark); background-color: var(--vn-section-color);
& > .date { & > .date {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
@ -981,7 +981,7 @@ setLogTree();
position: relative; position: relative;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
background-color: var(--vn-gray); background-color: var(--vn-section-color);
white-space: nowrap; white-space: nowrap;
box-sizing: border-box; box-sizing: border-box;
& > .q-icon { & > .q-icon {
@ -1036,7 +1036,6 @@ en:
claimStateFk: Claim State claimStateFk: Claim State
workerFk: Worker workerFk: Worker
clientFk: Customer clientFk: Customer
rma: RMA
responsibility: Responsibility responsibility: Responsibility
packages: Packages packages: Packages
es: es:
@ -1046,7 +1045,7 @@ es:
tooltips: tooltips:
search: Buscar por identificador o concepto search: Buscar por identificador o concepto
changes: Buscar por cambios. Los atributos deben buscarse por su nombre interno, para obtenerlo situar el cursor sobre el atributo. changes: Buscar por cambios. Los atributos deben buscarse por su nombre interno, para obtenerlo situar el cursor sobre el atributo.
Audit logs: Registros de auditoría Audit logs: Historial
Property: Propiedad Property: Propiedad
Before: Antes Before: Antes
After: Después After: Después
@ -1076,7 +1075,6 @@ es:
claimStateFk: Estado de la reclamación claimStateFk: Estado de la reclamación
workerFk: Trabajador workerFk: Trabajador
clientFk: Cliente clientFk: Cliente
rma: RMA
responsibility: Responsabilidad responsibility: Responsabilidad
packages: Bultos packages: Bultos
</i18n> </i18n>

View File

@ -59,6 +59,9 @@ const toggleForm = () => {
:name="actionIcon" :name="actionIcon"
:size="actionIcon === 'add' ? 'xs' : 'sm'" :size="actionIcon === 'add' ? 'xs' : 'sm'"
:class="['default-icon', { '--add-icon': actionIcon === 'add' }]" :class="['default-icon', { '--add-icon': actionIcon === 'add' }]"
:style="{
'font-variation-settings': `'FILL' ${1}`,
}"
> >
<QTooltip v-if="tooltip">{{ tooltip }}</QTooltip> <QTooltip v-if="tooltip">{{ tooltip }}</QTooltip>
</QIcon> </QIcon>
@ -79,7 +82,7 @@ const toggleForm = () => {
border-radius: 50px; border-radius: 50px;
&.--add-icon { &.--add-icon {
color: var(--vn-text); color: var(--vn-text-color);
background-color: $primary; background-color: $primary;
} }
} }

View File

@ -51,8 +51,8 @@ const $props = defineProps({
default: null, default: null,
}, },
limit: { limit: {
type: Number, type: [Number, String],
default: 30, default: '30',
}, },
}); });
@ -151,7 +151,7 @@ watch(modelValue, (newValue) => {
@on-fetch="(data) => setOptions(data)" @on-fetch="(data) => setOptions(data)"
:where="where || { [optionValue]: value }" :where="where || { [optionValue]: value }"
:limit="limit" :limit="limit"
:order-by="orderBy" :sort-by="sortBy"
:fields="fields" :fields="fields"
/> />
<QSelect <QSelect

View File

@ -94,16 +94,6 @@ async function send() {
<QSpace /> <QSpace />
<QBtn icon="close" :disable="isLoading" flat round dense v-close-popup /> <QBtn icon="close" :disable="isLoading" flat round dense v-close-popup />
</QCardSection> </QCardSection>
<QCardSection v-if="props.locale">
<QBanner class="bg-amber text-white" rounded dense>
<template #avatar>
<QIcon name="warning" />
</template>
<span
v-html="t('CustomerDefaultLanguage', { locale: t(props.locale) })"
></span>
</QBanner>
</QCardSection>
<QCardSection class="q-pb-xs"> <QCardSection class="q-pb-xs">
<QSelect <QSelect
:label="t('Language')" :label="t('Language')"
@ -184,7 +174,6 @@ async function send() {
<i18n> <i18n>
en: en:
CustomerDefaultLanguage: This customer uses <strong>{locale}</strong> as their default language
templates: templates:
pendingPayment: 'Your order is pending of payment. pendingPayment: 'Your order is pending of payment.
Please, enter the website and make the payment with a credit card. Thank you.' Please, enter the website and make the payment with a credit card. Thank you.'
@ -197,7 +186,6 @@ en:
pt: Portuguese pt: Portuguese
es: es:
Send SMS: Enviar SMS Send SMS: Enviar SMS
CustomerDefaultLanguage: Este cliente utiliza <strong>{locale}</strong> como idioma por defecto
Language: Idioma Language: Idioma
Phone: Móvil Phone: Móvil
Subject: Asunto Subject: Asunto

View File

@ -0,0 +1,25 @@
<script setup>
const $props = defineProps({
url: { type: String, default: null },
text: { type: String, default: null },
icon: { type: String, default: 'open_in_new' },
});
</script>
<template>
<div class="titleBox">
<div class="header-link">
<a :href="$props.url" :class="$props.url ? 'link' : 'color-vn-text'">
{{ $props.text }}
<QIcon v-if="url" :name="$props.icon" />
</a>
</div>
</div>
</template>
<style scoped lang="scss">
a {
font-size: large;
}
.titleBox {
padding-bottom: 2%;
}
</style>

View File

@ -1,9 +1,10 @@
<script setup> <script setup>
import { onMounted, useSlots, watch, computed, ref } from 'vue'; import { onBeforeMount, useSlots, watch, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue'; import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useState } from 'src/composables/useState';
const $props = defineProps({ const $props = defineProps({
url: { url: {
@ -35,35 +36,37 @@ const $props = defineProps({
default: null, default: null,
}, },
}); });
const state = useState();
const slots = useSlots(); const slots = useSlots();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const entity = computed(() => useArrayData($props.dataKey).store.data); const arrayData = useArrayData($props.dataKey || $props.module, {
url: $props.url,
filter: $props.filter,
skip: 0,
});
const { store } = arrayData;
const entity = computed(() => store.data);
const isLoading = ref(false); const isLoading = ref(false);
defineExpose({ defineExpose({
getData, getData,
}); });
onMounted(async () => { onBeforeMount(async () => {
await getData(); await getData();
watch( watch(
() => $props.url, () => $props.url,
async (newUrl, lastUrl) => { async () => await getData()
if (newUrl == lastUrl) return;
await getData();
}
); );
}); });
async function getData() { async function getData() {
const arrayData = useArrayData($props.dataKey, { store.url = $props.url;
url: $props.url,
filter: $props.filter,
skip: 0,
});
isLoading.value = true; isLoading.value = true;
try { try {
const { data } = await arrayData.fetch({ append: false, updateRouter: false }); const { data } = await arrayData.fetch({ append: false, updateRouter: false });
state.set($props.dataKey, data);
emit('onFetch', data); emit('onFetch', data);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -171,6 +174,7 @@ const emit = defineEmits(['onFetch']);
<style lang="scss"> <style lang="scss">
.body { .body {
background-color: var(--vn-section-color);
.text-h5 { .text-h5 {
padding-top: 5px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
@ -186,18 +190,21 @@ const emit = defineEmits(['onFetch']);
display: flex; display: flex;
padding: 2px 16px; padding: 2px 16px;
.label { .label {
color: var(--vn-label); color: var(--vn-label-color);
font-size: 12px; font-size: 12px;
width: 47%;
&:not(:has(a))::after {
content: ':';
}
} }
.value { .value {
color: var(--vn-text); color: var(--vn-text-color);
font-size: 14px; font-size: 14px;
margin-left: 12px; margin-left: 12px;
width: 47%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
text-align: left;
} }
.info { .info {
margin-left: 5px; margin-left: 5px;
@ -216,15 +223,13 @@ const emit = defineEmits(['onFetch']);
} }
} }
.subtitle { .subtitle {
color: var(--vn-text); color: var(--vn-text-color);
font-size: 16px; font-size: 16px;
margin-bottom: 15px; margin-bottom: 15px;
} }
.list-box { .list-box {
background-color: var(--vn-gray);
.q-item__label { .q-item__label {
color: var(--vn-label); color: var(--vn-label-color);
} }
} }
.descriptor { .descriptor {

View File

@ -61,7 +61,7 @@ const toggleCardCheck = (item) => {
} }
.q-chip-color { .q-chip-color {
color: var(--vn-label); color: var(--vn-label-color) !important;
} }
.card-list-body { .card-list-body {
@ -75,7 +75,7 @@ const toggleCardCheck = (item) => {
width: 50%; width: 50%;
.label { .label {
width: 35%; width: 35%;
color: var(--vn-label); color: var(--vn-label-color);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -117,7 +117,7 @@ const toggleCardCheck = (item) => {
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.card:hover { .card:hover {
background-color: var(--vn-gray); background-color: var(--vn-section-color);
} }
.list-items { .list-items {
width: 75%; width: 75%;

View File

@ -1,12 +1,10 @@
<script setup> <script setup>
import { onMounted, ref, watch } from 'vue'; import { ref, computed, watch, onBeforeMount } from 'vue';
import axios from 'axios'; import { useRoute } from 'vue-router';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue'; import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import { useArrayData } from 'src/composables/useArrayData';
onMounted(() => fetch());
const entity = ref();
const props = defineProps({ const props = defineProps({
url: { url: {
type: String, type: String,
@ -16,39 +14,65 @@ const props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
entityId: {
type: Number,
default: null,
},
dataKey: {
type: String,
default: '',
},
}); });
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
const route = useRoute();
const isSummary = ref();
const arrayData = useArrayData(props.dataKey || route.meta.moduleName, {
url: props.url,
filter: props.filter,
skip: 0,
});
const { store } = arrayData;
const entity = computed(() => store.data);
const isLoading = ref(false);
defineExpose({ defineExpose({
entity, entity,
fetch, fetch,
}); });
async function fetch() { onBeforeMount(async () => {
const params = {}; isSummary.value = String(route.path).endsWith('/summary');
await fetch();
if (props.filter) params.filter = JSON.stringify(props.filter); watch(props, async () => await fetch());
const { data } = await axios.get(props.url, { params });
entity.value = data;
emit('onFetch', data);
}
watch(props, async () => {
entity.value = null;
fetch();
}); });
async function fetch() {
store.url = props.url;
isLoading.value = true;
const { data } = await arrayData.fetch({ append: false, updateRouter: false });
emit('onFetch', data);
isLoading.value = false;
}
</script> </script>
<template> <template>
<div class="summary container"> <div class="summary container">
<QCard class="cardSummary"> <QCard class="cardSummary">
<SkeletonSummary v-if="!entity" /> <SkeletonSummary v-if="!entity || isLoading" />
<template v-if="entity"> <template v-if="entity && !isLoading">
<div class="summaryHeader bg-primary q-pa-md text-weight-bolder"> <div class="summaryHeader bg-primary q-pa-sm text-weight-bolder">
<slot name="header-left"> <slot name="header-left">
<span></span> <router-link
v-if="!isSummary && route.meta.moduleName"
class="header link"
:to="{
name: `${route.meta.moduleName}Summary`,
params: { id: entityId || entity.id },
}"
>
<QIcon name="open_in_new" color="white" size="sm" />
</router-link>
<span v-else></span>
</slot> </slot>
<slot name="header" :entity="entity"> <slot name="header" :entity="entity">
<VnLv :label="`${entity.id} -`" :value="entity.name" /> <VnLv :label="`${entity.id} -`" :value="entity.name" />
@ -85,9 +109,9 @@ watch(props, async () => {
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-evenly; justify-content: space-evenly;
gap: 15px; gap: 10px;
padding: 15px; padding: 10px;
background-color: var(--vn-gray); background-color: var(--vn-section-color);
> .q-card.vn-one { > .q-card.vn-one {
flex: 1; flex: 1;
@ -104,17 +128,17 @@ watch(props, async () => {
> .q-card { > .q-card {
width: 100%; width: 100%;
background-color: var(--vn-gray); background-color: var(--vn-section-color);
padding: 15px; padding: 7px;
font-size: 16px; font-size: 16px;
min-width: 275px; min-width: 275px;
.vn-label-value { .vn-label-value {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-top: 5px; margin-top: 2px;
.label { .label {
color: var(--vn-label); color: var(--vn-label-color);
width: 8em; width: 8em;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
@ -124,20 +148,33 @@ watch(props, async () => {
flex-shrink: 0; flex-shrink: 0;
} }
.value { .value {
color: var(--vn-text); color: var(--vn-text-color);
overflow: hidden; overflow: hidden;
} }
} }
.header { .header {
color: $primary; color: $primary;
font-weight: bold; font-weight: bold;
margin-bottom: 25px; margin-bottom: 10px;
font-size: 20px; font-size: 20px;
display: inline-block; display: inline-block;
} }
.header.link:hover { .header.link:hover {
color: lighten($primary, 20%); color: lighten($primary, 20%);
} }
.q-checkbox {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label {
margin-left: 31px;
color: var(--vn-text-color);
}
& .q-checkbox__inner {
position: absolute;
left: 0;
color: var(--vn-label-color);
}
}
} }
} }

View File

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

View File

@ -10,8 +10,8 @@ const $props = defineProps({
size: { type: String, default: null }, size: { type: String, default: null },
title: { type: String, default: null }, title: { type: String, default: null },
}); });
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const { t } = useI18n(); const { t } = useI18n();
const title = computed(() => $props.title ?? t('globals.system')); const title = computed(() => $props.title ?? t('globals.system'));

View File

@ -1,7 +1,8 @@
<script setup> <script setup>
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router';
import toDate from 'filters/toDate'; import toDate from 'filters/toDate';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
@ -52,6 +53,7 @@ const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']);
const arrayData = useArrayData(props.dataKey, { const arrayData = useArrayData(props.dataKey, {
exprBuilder: props.exprBuilder, exprBuilder: props.exprBuilder,
}); });
const route = useRoute();
const store = arrayData.store; const store = arrayData.store;
const userParams = ref({}); const userParams = ref({});
@ -63,6 +65,18 @@ onMounted(() => {
emit('init', { params: userParams.value }); emit('init', { params: userParams.value });
}); });
watch(
() => route.query.params,
(val) => {
if (!val) {
userParams.value = {};
} else {
const parsedParams = JSON.parse(val);
userParams.value = { ...parsedParams };
}
}
);
const isLoading = ref(false); const isLoading = ref(false);
async function search() { async function search() {
isLoading.value = true; isLoading.value = true;
@ -150,7 +164,7 @@ function formatValue(value) {
</script> </script>
<template> <template>
<QForm @submit="search"> <QForm @submit="search" id="filterPanelForm">
<QList dense> <QList dense>
<QItem class="q-mt-xs"> <QItem class="q-mt-xs">
<QItemSection top> <QItemSection top>

View File

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

View File

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

View File

@ -44,7 +44,7 @@ const props = defineProps({
}, },
offset: { offset: {
type: Number, type: Number,
default: 500, default: 0,
}, },
skeleton: { skeleton: {
type: Boolean, type: Boolean,
@ -54,6 +54,10 @@ const props = defineProps({
type: Function, type: Function,
default: null, default: null,
}, },
disableInfiniteScroll: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['onFetch', 'onPaginate']); const emit = defineEmits(['onFetch', 'onPaginate']);
@ -122,10 +126,11 @@ async function paginate() {
emit('onPaginate'); emit('onPaginate');
} }
async function onLoad(...params) { async function onLoad(index, done) {
if (!store.data) return; if (!store.data) {
return done();
}
const done = params[1];
if (store.data.length === 0 || !props.url) return done(false); if (store.data.length === 0 || !props.url) return done(false);
pagination.value.page = pagination.value.page + 1; pagination.value.page = pagination.value.page + 1;
@ -138,7 +143,7 @@ async function onLoad(...params) {
</script> </script>
<template> <template>
<div> <div class="full-width">
<div <div
v-if="!props.autoLoad && !store.data && !isLoading" v-if="!props.autoLoad && !store.data && !isLoading"
class="info-row q-pa-md text-center" class="info-row q-pa-md text-center"
@ -174,7 +179,9 @@ async function onLoad(...params) {
v-if="store.data" v-if="store.data"
@load="onLoad" @load="onLoad"
:offset="offset" :offset="offset"
class="full-width full-height" :disable="disableInfiniteScroll || !arrayData.hasMoreData.value"
class="full-width"
v-bind="$attrs"
> >
<slot name="body" :rows="store.data"></slot> <slot name="body" :rows="store.data"></slot>
<div v-if="isLoading" class="info-row q-pa-md text-center"> <div v-if="isLoading" class="info-row q-pa-md text-center">

View File

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

View File

@ -1,11 +1,9 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import VnInput from 'src/components/common/VnInput.vue';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import VnInput from 'src/components/common/VnInput.vue';
const quasar = useQuasar(); const quasar = useQuasar();
@ -68,9 +66,8 @@ const props = defineProps({
}); });
const router = useRouter(); const router = useRouter();
const route = useRoute();
const arrayData = useArrayData(props.dataKey, { ...props }); const arrayData = useArrayData(props.dataKey, { ...props });
const store = arrayData.store; const { store } = arrayData;
const searchText = ref(''); const searchText = ref('');
onMounted(() => { onMounted(() => {
@ -81,8 +78,9 @@ onMounted(() => {
}); });
async function search() { async function search() {
const staticParams = Object.entries(store.userParams) const staticParams = Object.entries(store.userParams).filter(
.filter(([key, value]) => value && (props.staticParams || []).includes(key)); ([key, value]) => value && (props.staticParams || []).includes(key)
);
await arrayData.applyFilter({ await arrayData.applyFilter({
params: { params: {
...Object.fromEntries(staticParams), ...Object.fromEntries(staticParams),
@ -91,23 +89,31 @@ async function search() {
}); });
if (!props.redirect) return; if (!props.redirect) return;
if (props.customRouteRedirectName) { if (props.customRouteRedirectName)
router.push({ return router.push({
name: props.customRouteRedirectName, name: props.customRouteRedirectName,
params: { id: searchText.value }, params: { id: searchText.value },
}); });
return;
}
const { matched: matches } = route; const { matched: matches } = router.currentRoute.value;
const { path } = matches[matches.length - 1]; const { path } = matches.at(-1);
const newRoute = path.replace(':id', searchText.value); const [, moduleName] = path.split('/');
await router.push(newRoute);
if (!store.data.length || store.data.length > 1)
return router.push({ path: `/${moduleName}/list` });
const targetId = store.data[0].id;
let targetUrl;
if (path.endsWith('/list')) targetUrl = path.replace('/list', `/${targetId}/summary`);
else if (path.includes(':id')) targetUrl = path.replace(':id', targetId);
await router.push({ path: targetUrl });
} }
</script> </script>
<template> <template>
<QForm @submit="search"> <QForm @submit="search" id="searchbarForm">
<VnInput <VnInput
id="searchbar" id="searchbar"
v-model="searchText" v-model="searchText"
@ -117,7 +123,12 @@ async function search() {
autofocus autofocus
> >
<template #prepend> <template #prepend>
<QIcon name="search" v-if="!quasar.platform.is.mobile" /> <QIcon
v-if="!quasar.platform.is.mobile"
class="cursor-pointer"
name="search"
@click="search"
/>
</template> </template>
<template #append> <template #append>
<QIcon <QIcon
@ -155,11 +166,14 @@ async function search() {
.cursor-info { .cursor-info {
cursor: help; cursor: help;
} }
#searchbar {
.body--light #searchbar {
.q-field--standout.q-field--highlighted .q-field__control { .q-field--standout.q-field--highlighted .q-field__control {
background-color: $grey-7; background-color: white;
color: #333; color: black;
.q-field__native,
.q-icon {
color: black !important;
}
} }
} }
</style> </style>

View File

@ -14,7 +14,7 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<QToolbar class="bg-vn-dark justify-end sticky"> <QToolbar class="justify-end sticky">
<slot name="st-data"> <slot name="st-data">
<div id="st-data"></div> <div id="st-data"></div>
</slot> </slot>

View File

@ -1,20 +1,21 @@
<script setup> <script setup>
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useState } from 'src/composables/useState';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue';
import CreateDepartmentChild from '../CreateDepartmentChild.vue'; import CreateDepartmentChild from '../CreateDepartmentChild.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useRouter } from 'vue-router';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
const state = useState();
const router = useRouter();
const treeRef = ref(null); const treeRef = ref();
const showCreateNodeFormVal = ref(false); const showCreateNodeFormVal = ref(false);
const creationNodeSelectedId = ref(null); const creationNodeSelectedId = ref(null);
const expanded = ref([]); const expanded = ref([]);
@ -24,30 +25,35 @@ const nodes = ref([{ id: null, name: t('Departments'), sons: true, children: [{}
const fetchedChildrensSet = ref(new Set()); const fetchedChildrensSet = ref(new Set());
const onNodeExpanded = (nodeKeysArray) => { const onNodeExpanded = (nodeKeysArray) => {
// Verificar si el nodo ya fue expandido
if (!fetchedChildrensSet.value.has(nodeKeysArray.at(-1))) { if (!fetchedChildrensSet.value.has(nodeKeysArray.at(-1))) {
fetchedChildrensSet.value.add(nodeKeysArray.at(-1)); fetchedChildrensSet.value.add(nodeKeysArray.at(-1));
fetchNodeLeaves(nodeKeysArray.at(-1)); // Llamar a la función para obtener los nodos hijos fetchNodeLeaves(nodeKeysArray.at(-1));
} }
state.set('Tree', nodeKeysArray);
}; };
const fetchNodeLeaves = async (nodeKey) => { const fetchNodeLeaves = async (nodeKey) => {
try { try {
const node = treeRef.value.getNodeByKey(nodeKey); const node = treeRef.value?.getNodeByKey(nodeKey);
if (!node || node.sons === 0) return; if (!node || node.sons === 0) return;
const params = { parentId: node.id }; const params = { parentId: node.id };
const response = await axios.get('/departments/getLeaves', { params }); const response = await axios.get('/departments/getLeaves', { params });
// Si hay datos en la respuesta y tiene hijos, agregarlos al nodo actual
if (response.data) { if (response.data) {
node.children = response.data; node.children = response.data.map((n) => {
node.children.forEach((node) => { const hasChildrens = n.sons > 0;
if (node.sons) node.children = [{}];
n.children = hasChildrens ? [{}] : null;
n.clickable = true;
return n;
}); });
} }
state.set('Tree', node);
} catch (err) { } catch (err) {
console.error('Error fetching department leaves'); console.error('Error fetching department leaves', err);
throw new Error(); throw new Error();
} }
}; };
@ -84,10 +90,49 @@ const onNodeCreated = async () => {
await fetchNodeLeaves(creationNodeSelectedId.value); await fetchNodeLeaves(creationNodeSelectedId.value);
}; };
const redirectToDepartmentSummary = (id) => { onMounted(async (n) => {
if (!id) return; const tree = [...state.get('Tree'), 1];
router.push({ name: 'DepartmentSummary', params: { id } }); const lastStateTree = state.get('TreeState');
}; if (tree) {
for (let n of tree) {
await fetchNodeLeaves(n);
}
expanded.value = tree;
if (lastStateTree) {
tree.push(lastStateTree);
await fetchNodeLeaves(lastStateTree);
}
}
setTimeout(() => {
if (lastStateTree) {
document.getElementById(lastStateTree).scrollIntoView();
}
}, 1000);
});
function handleEvent(type, event, node) {
const isParent = node.sons > 0;
const lastId = isParent ? node.id : node.parentFk;
switch (type) {
case 'path':
state.set('TreeState', lastId);
node.id && router.push({ path: `/department/department/${node.id}/summary` });
break;
case 'tab':
state.set('TreeState', lastId);
node.id &&
window.open(`#/department/department/${node.id}/summary`, '_blank');
break;
default:
node.id &&
router.push({ path: `#/department/department/${node.id}/summary` });
break;
}
}
</script> </script>
<template> <template>
@ -99,15 +144,27 @@ const redirectToDepartmentSummary = (id) => {
label-key="name" label-key="name"
v-model:expanded="expanded" v-model:expanded="expanded"
@update:expanded="onNodeExpanded($event)" @update:expanded="onNodeExpanded($event)"
:default-expand-all="true"
> >
<template #default-header="{ node }"> <template #default-header="{ node }">
<div <div
class="row justify-between full-width q-pr-md cursor-pointer" :id="node.id"
@click.stop="redirectToDepartmentSummary(node.id)" class="qtr row justify-between full-width q-pr-md cursor-pointer"
> >
<span class="text-uppercase"> <div>
{{ node.name }} <span
</span> @click="handleEvent('row', $event, node)"
class="cursor-pointer"
>
{{ node.name }}
<DepartmentDescriptorProxy :id="node.id" />
</span>
</div>
<div
@click.stop.exact="handleEvent('path', $event, node)"
@click.ctrl.stop="handleEvent('tab', $event, node)"
style="flex-grow: 1; width: 10px"
></div>
<div class="row justify-between" style="max-width: max-content"> <div class="row justify-between" style="max-width: max-content">
<QIcon <QIcon
v-if="node.id" v-if="node.id"
@ -149,6 +206,11 @@ const redirectToDepartmentSummary = (id) => {
</QCard> </QCard>
</template> </template>
<style lang="scss" scoped>
span {
color: $primary;
}
</style>
<i18n> <i18n>
es: es:
Departments: Departamentos Departments: Departamentos

View File

@ -1,8 +1,8 @@
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { getUrl } from './getUrl'; import { getUrl } from './getUrl';
const session = useSession(); const {getTokenMultimedia} = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
export async function downloadFile(dmsId) { export async function downloadFile(dmsId) {
let appUrl = await getUrl('', 'lilium'); let appUrl = await getUrl('', 'lilium');

View File

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

View File

@ -4,7 +4,7 @@ import { useQuasar } from 'quasar';
export function usePrintService() { export function usePrintService() {
const quasar = useQuasar(); const quasar = useQuasar();
const { getToken } = useSession(); const { getTokenMultimedia } = useSession();
function sendEmail(path, params) { function sendEmail(path, params) {
return axios.post(path, params).then(() => return axios.post(path, params).then(() =>
@ -19,7 +19,7 @@ export function usePrintService() {
function openReport(path, params) { function openReport(path, params) {
params = Object.assign( params = Object.assign(
{ {
access_token: getToken(), access_token: getTokenMultimedia(),
}, },
params params
); );

View File

@ -1,30 +1,56 @@
import { useState } from './useState'; import { useState } from './useState';
import { useRole } from './useRole'; import { useRole } from './useRole';
import { useUserConfig } from './useUserConfig'; import { useUserConfig } from './useUserConfig';
import axios from 'axios';
import useNotify from './useNotify';
export function useSession() { export function useSession() {
const { notify } = useNotify();
function getToken() { function getToken() {
const localToken = localStorage.getItem('token'); const localToken = localStorage.getItem('token');
const sessionToken = sessionStorage.getItem('token'); const sessionToken = sessionStorage.getItem('token');
return localToken || sessionToken || ''; return localToken || sessionToken || '';
} }
function getTokenMultimedia() {
const localTokenMultimedia = localStorage.getItem('tokenMultimedia');
const sessionTokenMultimedia = sessionStorage.getItem('tokenMultimedia');
return localTokenMultimedia || sessionTokenMultimedia || '';
}
function setToken(data) { function setToken(data) {
if (data.keepLogin) { if (data.keepLogin) {
localStorage.setItem('token', data.token); localStorage.setItem('token', data.token);
localStorage.setItem('tokenMultimedia', data.tokenMultimedia);
} else { } else {
sessionStorage.setItem('token', data.token); sessionStorage.setItem('token', data.token);
sessionStorage.setItem('tokenMultimedia', data.tokenMultimedia);
} }
} }
async function destroyToken(url, storage, key) {
function destroy() { if (storage.getItem(key)) {
if (localStorage.getItem('token')) try {
localStorage.removeItem('token') await axios.post(url, null, {
headers: { Authorization: storage.getItem(key) },
if (sessionStorage.getItem('token')) });
sessionStorage.removeItem('token'); } catch (error) {
notify('errors.statusUnauthorized', 'negative');
} finally {
storage.removeItem(key);
}
}
}
async function destroy() {
const tokens = {
tokenMultimedia: 'Accounts/logout',
token: 'VnUsers/logout',
};
for (const [key, url] of Object.entries(tokens)) {
await destroyToken(url, localStorage, key);
await destroyToken(url, sessionStorage, key);
}
const { setUser } = useState(); const { setUser } = useState();
@ -37,8 +63,8 @@ export function useSession() {
}); });
} }
async function login(token, keepLogin) { async function login(token, tokenMultimedia, keepLogin) {
setToken({ token, keepLogin }); setToken({ token, tokenMultimedia, keepLogin });
await useRole().fetch(); await useRole().fetch();
await useUserConfig().fetch(); await useUserConfig().fetch();
@ -53,6 +79,7 @@ export function useSession() {
return { return {
getToken, getToken,
getTokenMultimedia,
setToken, setToken,
destroy, destroy,
login, login,

View File

@ -0,0 +1,23 @@
import VnConfirm from 'components/ui/VnConfirm.vue';
import { useQuasar } from 'quasar';
export function useVnConfirm() {
const quasar = useQuasar();
const openConfirmationModal = (title, message, promise, successFn) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: title,
message: message,
promise: promise,
},
})
.onOk(async () => {
if (successFn) successFn();
});
};
return { openConfirmationModal };
}

View File

@ -1,55 +1,84 @@
// app global css in SCSS form // app global css in SCSS form
@import './icons.scss'; @import './icons.scss';
body.body--light {
--font-color: black;
--vn-section-color: #e0e0e0;
--vn-page-color: #ffffff;
--vn-text-color: var(--font-color);
--vn-label-color: #5f5f5f;
--vn-accent-color: #e7e3e3;
background-color: var(--vn-page-color);
.q-header .q-toolbar {
color: var(--font-color);
}
.q-card,
.q-table,
.q-table__bottom,
.q-drawer {
background-color: var(--vn-section-color);
}
}
body.body--dark {
--vn-section-color: #403c3c;
--vn-text-color: white;
--vn-label-color: #a8a8a8;
--vn-accent-color: #424242;
background-color: #222;
}
a { a {
text-decoration: none; text-decoration: none;
} }
.link { .link {
color: $primary; color: $color-link;
cursor: pointer; cursor: pointer;
} }
.tx-color-link {
color: $color-link !important;
}
.tx-color-font {
color: $color-link !important;
}
.header-link {
color: $color-link !important;
cursor: pointer;
border-bottom: solid $primary;
border-width: 2px;
width: 100%;
.q-icon {
float: right;
}
text-transform: uppercase;
}
.link:hover { .link:hover {
color: $orange-4; text-decoration: underline;
} }
// Removes chrome autofill background // Removes chrome autofill background
input:-webkit-autofill, input:-webkit-autofill,
select:-webkit-autofill { select:-webkit-autofill {
color: var(--vn-text); color: var(--vn-text-color);
font-family: $typography-font-family; font-family: $typography-font-family;
-webkit-text-fill-color: var(--vn-text); -webkit-text-fill-color: var(--vn-text-color);
-webkit-background-clip: text !important; -webkit-background-clip: text !important;
background-clip: text !important; background-clip: text !important;
} }
body.body--light { .bg-vn-section-color {
.q-header .q-toolbar { background-color: var(--vn-section-color);
background-color: $white;
color: #555;
}
--vn-text: #000000;
--vn-gray: #f5f5f5;
--vn-label: #5f5f5f;
--vn-dark: white;
--vn-light-gray: #e7e3e3;
}
body.body--dark {
--vn-text: #ffffff;
--vn-gray: #313131;
--vn-label: #a8a8a8;
--vn-dark: #292929;
--vn-light-gray: #424242;
}
.bg-vn-dark {
background-color: var(--vn-dark);
} }
.color-vn-text { .color-vn-text {
color: var(--vn-text); color: var(--vn-text-color);
} }
.color-vn-white { .color-vn-white {
@ -57,8 +86,8 @@ body.body--dark {
} }
.vn-card { .vn-card {
background-color: var(--vn-gray); background-color: var(--vn-section-color);
color: var(--vn-text); color: var(--vn-text-color);
border-radius: 8px; border-radius: 8px;
} }
@ -68,11 +97,20 @@ body.body--dark {
} }
.bg-vn-primary-row { .bg-vn-primary-row {
background-color: var(--vn-dark); background-color: var(--vn-section-color);
} }
.bg-vn-secondary-row { .bg-vn-secondary-row {
background-color: var(--vn-light-gray); background-color: var(--vn-accent-color);
}
.fill-icon {
font-variation-settings: 'FILL' 1;
}
.vn-table-separation-row {
height: 16px !important;
background-color: var(--vn-section-color) !important;
} }
/* Estilo para el asterisco en campos requeridos */ /* Estilo para el asterisco en campos requeridos */
@ -80,6 +118,10 @@ body.body--dark {
content: ' *'; content: ' *';
} }
.q-chip {
color: black;
}
input[type='number'] { input[type='number'] {
-moz-appearance: textfield; -moz-appearance: textfield;
} }

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

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

View File

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

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

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

View File

@ -24,12 +24,14 @@ export default {
dataCreated: 'Data created', dataCreated: 'Data created',
add: 'Add', add: 'Add',
create: 'Create', create: 'Create',
edit: 'Edit',
save: 'Save', save: 'Save',
remove: 'Remove', remove: 'Remove',
reset: 'Reset', reset: 'Reset',
close: 'Close', close: 'Close',
cancel: 'Cancel', cancel: 'Cancel',
confirm: 'Confirm', confirm: 'Confirm',
assign: 'Assign',
back: 'Back', back: 'Back',
yes: 'Yes', yes: 'Yes',
no: 'No', no: 'No',
@ -64,12 +66,32 @@ export default {
markAll: 'Mark all', markAll: 'Mark all',
requiredField: 'Required field', requiredField: 'Required field',
class: 'clase', class: 'clase',
type: 'type', type: 'Type',
reason: 'reason', reason: 'reason',
noResults: 'No results', noResults: 'No results',
system: 'System', system: 'System',
warehouse: 'Warehouse',
company: 'Company',
fieldRequired: 'Field required', fieldRequired: 'Field required',
allowedFilesText: 'Allowed file types: { allowedContentTypes }', allowedFilesText: 'Allowed file types: { allowedContentTypes }',
smsSent: 'SMS sent',
confirmDeletion: 'Confirm deletion',
confirmDeletionMessage: 'Are you sure you want to delete this?',
description: 'Description',
id: 'Id',
order: 'Order',
original: 'Original',
file: 'File',
selectFile: 'Select a file',
copyClipboard: 'Copy on clipboard',
salesPerson: 'SalesPerson',
code: 'Code',
pageTitles: {
summary: 'Summary',
basicData: 'Basic data',
log: 'Logs',
parkingList: 'Parkings list',
},
}, },
errors: { errors: {
statusUnauthorized: 'Access denied', statusUnauthorized: 'Access denied',
@ -77,7 +99,7 @@ export default {
statusBadGateway: 'It seems that the server has fall down', statusBadGateway: 'It seems that the server has fall down',
statusGatewayTimeout: 'Could not contact the server', statusGatewayTimeout: 'Could not contact the server',
userConfig: 'Error fetching user config', userConfig: 'Error fetching user config',
create: 'Error during creation', writeRequest: 'The requested operation could not be completed',
}, },
login: { login: {
title: 'Login', title: 'Login',
@ -160,13 +182,14 @@ export default {
hasDebt: 'Customer has debt', hasDebt: 'Customer has debt',
notChecked: 'Customer not checked', notChecked: 'Customer not checked',
noWebAccess: 'Web access is disabled', noWebAccess: 'Web access is disabled',
businessTypeFk: 'Business type',
}, },
summary: { summary: {
basicData: 'Basic data', basicData: 'Basic data',
fiscalAddress: 'Fiscal address', fiscalAddress: 'Fiscal address',
fiscalData: 'Fiscal data', fiscalData: 'Fiscal data',
billingData: 'Billing data', billingData: 'Billing data',
consignee: 'Consignee', consignee: 'Default consignee',
businessData: 'Business data', businessData: 'Business data',
financialData: 'Financial data', financialData: 'Financial data',
customerId: 'Customer ID', customerId: 'Customer ID',
@ -219,6 +242,8 @@ export default {
recoverySince: 'Recovery since', recoverySince: 'Recovery since',
businessType: 'Business Type', businessType: 'Business Type',
city: 'City', city: 'City',
rating: 'Rating',
recommendCredit: 'Recommended credit',
}, },
basicData: { basicData: {
socialName: 'Fiscal name', socialName: 'Fiscal name',
@ -273,6 +298,7 @@ export default {
basicData: 'Basic data', basicData: 'Basic data',
buys: 'Buys', buys: 'Buys',
notes: 'Notes', notes: 'Notes',
dms: 'File management',
log: 'Log', log: 'Log',
create: 'Create', create: 'Create',
latestBuys: 'Latest buys', latestBuys: 'Latest buys',
@ -344,7 +370,6 @@ export default {
reference: 'Reference', reference: 'Reference',
observations: 'Observations', observations: 'Observations',
item: 'Item', item: 'Item',
description: 'Description',
size: 'Size', size: 'Size',
packing: 'Packing', packing: 'Packing',
grouping: 'Grouping', grouping: 'Grouping',
@ -359,7 +384,6 @@ export default {
}, },
notes: { notes: {
observationType: 'Observation type', observationType: 'Observation type',
description: 'Description',
}, },
descriptor: { descriptor: {
agency: 'Agency', agency: 'Agency',
@ -372,7 +396,6 @@ export default {
packing: 'Packing', packing: 'Packing',
grouping: 'Grouping', grouping: 'Grouping',
quantity: 'Quantity', quantity: 'Quantity',
description: 'Description',
size: 'Size', size: 'Size',
tags: 'Tags', tags: 'Tags',
type: 'Type', type: 'Type',
@ -425,6 +448,7 @@ export default {
shipped: 'Shipped', shipped: 'Shipped',
warehouse: 'Warehouse', warehouse: 'Warehouse',
customerCard: 'Customer card', customerCard: 'Customer card',
alias: 'Alias',
}, },
boxing: { boxing: {
expedition: 'Expedition', expedition: 'Expedition',
@ -458,7 +482,6 @@ export default {
visible: 'Visible', visible: 'Visible',
available: 'Available', available: 'Available',
quantity: 'Quantity', quantity: 'Quantity',
description: 'Description',
price: 'Price', price: 'Price',
discount: 'Discount', discount: 'Discount',
packing: 'Packing', packing: 'Packing',
@ -476,6 +499,7 @@ export default {
request: 'Request', request: 'Request',
weight: 'Weight', weight: 'Weight',
goTo: 'Go to', goTo: 'Go to',
summaryAmount: 'Summary',
}, },
}, },
claim: { claim: {
@ -483,11 +507,9 @@ export default {
claims: 'Claims', claims: 'Claims',
list: 'List', list: 'List',
createClaim: 'Create claim', createClaim: 'Create claim',
rmaList: 'RMA',
summary: 'Summary', summary: 'Summary',
basicData: 'Basic Data', basicData: 'Basic Data',
lines: 'Lines', lines: 'Lines',
rma: 'RMA',
photos: 'Photos', photos: 'Photos',
development: 'Development', development: 'Development',
log: 'Audit logs', log: 'Audit logs',
@ -504,10 +526,6 @@ export default {
code: 'Code', code: 'Code',
records: 'records', records: 'records',
}, },
rma: {
user: 'User',
created: 'Created',
},
card: { card: {
claimId: 'Claim ID', claimId: 'Claim ID',
assignedTo: 'Assigned', assignedTo: 'Assigned',
@ -516,6 +534,8 @@ export default {
ticketId: 'Ticket ID', ticketId: 'Ticket ID',
customerSummary: 'Customer summary', customerSummary: 'Customer summary',
claimedTicket: 'Claimed ticket', claimedTicket: 'Claimed ticket',
saleTracking: 'Sale tracking',
ticketTracking: 'Ticket tracking',
commercial: 'Commercial', commercial: 'Commercial',
province: 'Province', province: 'Province',
zone: 'Zone', zone: 'Zone',
@ -531,7 +551,6 @@ export default {
landed: 'Landed', landed: 'Landed',
quantity: 'Quantity', quantity: 'Quantity',
claimed: 'Claimed', claimed: 'Claimed',
description: 'Description',
price: 'Price', price: 'Price',
discount: 'Discount', discount: 'Discount',
total: 'Total', total: 'Total',
@ -547,7 +566,6 @@ export default {
responsible: 'Responsible', responsible: 'Responsible',
worker: 'Worker', worker: 'Worker',
redelivery: 'Redelivery', redelivery: 'Redelivery',
returnOfMaterial: 'RMA',
}, },
basicData: { basicData: {
customer: 'Customer', customer: 'Customer',
@ -555,7 +573,6 @@ export default {
created: 'Created', created: 'Created',
state: 'State', state: 'State',
picked: 'Picked', picked: 'Picked',
returnOfMaterial: 'Return of material authorization (RMA)',
}, },
photo: { photo: {
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}', fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}',
@ -583,6 +600,7 @@ export default {
company: 'Company', company: 'Company',
dued: 'Due date', dued: 'Due date',
shortDued: 'Due date', shortDued: 'Due date',
amount: 'Amount',
}, },
card: { card: {
issued: 'Issued', issued: 'Issued',
@ -616,7 +634,7 @@ export default {
fillDates: 'Invoice date and the max date should be filled', fillDates: 'Invoice date and the max date should be filled',
invoiceDateLessThanMaxDate: 'Invoice date can not be less than max date', invoiceDateLessThanMaxDate: 'Invoice date can not be less than max date',
invoiceWithFutureDate: 'Exists an invoice with a future date', invoiceWithFutureDate: 'Exists an invoice with a future date',
noTicketsToInvoice: 'There are not clients to invoice', noTicketsToInvoice: 'There are not tickets to invoice',
criticalInvoiceError: 'Critical invoicing error, process stopped', criticalInvoiceError: 'Critical invoicing error, process stopped',
}, },
table: { table: {
@ -676,6 +694,19 @@ export default {
recyclable: 'Recyclable', recyclable: 'Recyclable',
}, },
}, },
parking: {
pickingOrder: 'Picking order',
sector: 'Sector',
row: 'Row',
column: 'Column',
pageTitles: {
parking: 'Parking',
},
searchBar: {
info: 'You can search by parking code',
label: 'Search parking...',
},
},
invoiceIn: { invoiceIn: {
pageTitles: { pageTitles: {
invoiceIns: 'Invoices In', invoiceIns: 'Invoices In',
@ -793,7 +824,6 @@ export default {
orderTicketList: 'Order Ticket List', orderTicketList: 'Order Ticket List',
details: 'Details', details: 'Details',
item: 'Item', item: 'Item',
description: 'Description',
quantity: 'Quantity', quantity: 'Quantity',
price: 'Price', price: 'Price',
amount: 'Amount', amount: 'Amount',
@ -827,6 +857,8 @@ export default {
notifications: 'Notifications', notifications: 'Notifications',
workerCreate: 'New worker', workerCreate: 'New worker',
department: 'Department', department: 'Department',
pda: 'PDA',
log: 'Log',
}, },
list: { list: {
name: 'Name', name: 'Name',
@ -868,6 +900,13 @@ export default {
subscribed: 'Subscribed to the notification', subscribed: 'Subscribed to the notification',
unsubscribed: 'Unsubscribed from the notification', unsubscribed: 'Unsubscribed from the notification',
}, },
pda: {
newPDA: 'New PDA',
currentPDA: 'Current PDA',
model: 'Model',
serialNumber: 'Serial number',
removePDA: 'Deallocate PDA',
},
create: { create: {
name: 'Name', name: 'Name',
lastName: 'Last name', lastName: 'Last name',
@ -933,6 +972,22 @@ export default {
uncompleteTrays: 'There are incomplete trays', uncompleteTrays: 'There are incomplete trays',
}, },
}, },
'route/roadmap': {
pageTitles: {
roadmap: 'Roadmap',
summary: 'Summary',
basicData: 'Basic Data',
stops: 'Stops',
},
},
roadmap: {
pageTitles: {
roadmap: 'Roadmap',
summary: 'Summary',
basicData: 'Basic Data',
stops: 'Stops',
},
},
route: { route: {
pageTitles: { pageTitles: {
routes: 'Routes', routes: 'Routes',
@ -942,6 +997,11 @@ export default {
create: 'Create', create: 'Create',
basicData: 'Basic Data', basicData: 'Basic Data',
summary: 'Summary', summary: 'Summary',
RouteRoadmap: 'Roadmaps',
RouteRoadmapCreate: 'Create roadmap',
tickets: 'Tickets',
log: 'Log',
autonomous: 'Autonomous',
}, },
cmr: { cmr: {
list: { list: {
@ -976,6 +1036,7 @@ export default {
addresses: 'Addresses', addresses: 'Addresses',
consumption: 'Consumption', consumption: 'Consumption',
agencyTerm: 'Agency agreement', agencyTerm: 'Agency agreement',
dms: 'File management',
}, },
list: { list: {
payMethod: 'Pay method', payMethod: 'Pay method',
@ -1139,7 +1200,6 @@ export default {
warehouse: 'Warehouse', warehouse: 'Warehouse',
travelFileDescription: 'Travel id { travelId }', travelFileDescription: 'Travel id { travelId }',
file: 'File', file: 'File',
description: 'Description',
}, },
}, },
item: { item: {
@ -1167,13 +1227,17 @@ export default {
copyToken: 'Token copied to clipboard', copyToken: 'Token copied to clipboard',
settings: 'Settings', settings: 'Settings',
logOut: 'Log Out', logOut: 'Log Out',
localWarehouse: 'Local warehouse',
localBank: 'Local bank',
localCompany: 'Local company',
userWarehouse: 'User warehouse',
userCompany: 'User company',
}, },
smartCard: { smartCard: {
downloadFile: 'Download file', downloadFile: 'Download file',
clone: 'Clone', clone: 'Clone',
openCard: 'View', openCard: 'View',
openSummary: 'Summary', openSummary: 'Summary',
viewDescription: 'Description',
}, },
cardDescriptor: { cardDescriptor: {
mainList: 'Main list', mainList: 'Main list',

View File

@ -24,12 +24,14 @@ export default {
dataCreated: 'Datos creados', dataCreated: 'Datos creados',
add: 'Añadir', add: 'Añadir',
create: 'Crear', create: 'Crear',
edit: 'Modificar',
save: 'Guardar', save: 'Guardar',
remove: 'Eliminar', remove: 'Eliminar',
reset: 'Restaurar', reset: 'Restaurar',
close: 'Cerrar', close: 'Cerrar',
cancel: 'Cancelar', cancel: 'Cancelar',
confirm: 'Confirmar', confirm: 'Confirmar',
assign: 'Asignar',
back: 'Volver', back: 'Volver',
yes: 'Si', yes: 'Si',
no: 'No', no: 'No',
@ -64,12 +66,32 @@ export default {
markAll: 'Marcar todo', markAll: 'Marcar todo',
requiredField: 'Campo obligatorio', requiredField: 'Campo obligatorio',
class: 'clase', class: 'clase',
type: 'tipo', type: 'Tipo',
reason: 'motivo', reason: 'motivo',
noResults: 'Sin resultados', noResults: 'Sin resultados',
system: 'Sistema', system: 'Sistema',
warehouse: 'Almacén',
company: 'Empresa',
fieldRequired: 'Campo requerido', fieldRequired: 'Campo requerido',
allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }', allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }',
smsSent: 'SMS enviado',
confirmDeletion: 'Confirmar eliminación',
confirmDeletionMessage: '¿Seguro que quieres eliminar?',
description: 'Descripción',
id: 'Id',
order: 'Orden',
original: 'Original',
file: 'Fichero',
selectFile: 'Seleccione un fichero',
copyClipboard: 'Copiar en portapapeles',
salesPerson: 'Comercial',
code: 'Código',
pageTitles: {
summary: 'Resumen',
basicData: 'Datos básicos',
log: 'Historial',
parkingList: 'Listado de parkings',
},
}, },
errors: { errors: {
statusUnauthorized: 'Acceso denegado', statusUnauthorized: 'Acceso denegado',
@ -77,7 +99,7 @@ export default {
statusBadGateway: 'Parece ser que el servidor ha caído', statusBadGateway: 'Parece ser que el servidor ha caído',
statusGatewayTimeout: 'No se ha podido contactar con el servidor', statusGatewayTimeout: 'No se ha podido contactar con el servidor',
userConfig: 'Error al obtener configuración de usuario', userConfig: 'Error al obtener configuración de usuario',
create: 'Error al crear', writeRequest: 'No se pudo completar la operación solicitada',
}, },
login: { login: {
title: 'Inicio de sesión', title: 'Inicio de sesión',
@ -159,13 +181,14 @@ export default {
hasDebt: 'El cliente tiene riesgo', hasDebt: 'El cliente tiene riesgo',
notChecked: 'El cliente no está comprobado', notChecked: 'El cliente no está comprobado',
noWebAccess: 'El acceso web está desactivado', noWebAccess: 'El acceso web está desactivado',
businessTypeFk: 'Tipo de negocio',
}, },
summary: { summary: {
basicData: 'Datos básicos', basicData: 'Datos básicos',
fiscalAddress: 'Dirección fiscal', fiscalAddress: 'Dirección fiscal',
fiscalData: 'Datos fiscales', fiscalData: 'Datos fiscales',
billingData: 'Datos de facturación', billingData: 'Datos de facturación',
consignee: 'Consignatario', consignee: 'Consignatario pred.',
businessData: 'Datos comerciales', businessData: 'Datos comerciales',
financialData: 'Datos financieros', financialData: 'Datos financieros',
customerId: 'ID cliente', customerId: 'ID cliente',
@ -218,6 +241,8 @@ export default {
recoverySince: 'Recobro desde', recoverySince: 'Recobro desde',
businessType: 'Tipo de negocio', businessType: 'Tipo de negocio',
city: 'Población', city: 'Población',
rating: 'Clasificación',
recommendCredit: 'Crédito recomendado',
}, },
basicData: { basicData: {
socialName: 'Nombre fiscal', socialName: 'Nombre fiscal',
@ -272,6 +297,7 @@ export default {
basicData: 'Datos básicos', basicData: 'Datos básicos',
buys: 'Compras', buys: 'Compras',
notes: 'Notas', notes: 'Notas',
dms: 'Gestión documental',
log: 'Historial', log: 'Historial',
create: 'Crear', create: 'Crear',
latestBuys: 'Últimas compras', latestBuys: 'Últimas compras',
@ -292,8 +318,8 @@ export default {
reference: 'Referencia', reference: 'Referencia',
invoiceNumber: 'Núm. factura', invoiceNumber: 'Núm. factura',
ordered: 'Pedida', ordered: 'Pedida',
confirmed: 'Confirmado', confirmed: 'Confirmada',
booked: 'Asentado', booked: 'Contabilizada',
raid: 'Redada', raid: 'Redada',
excludedFromAvailable: 'Inventario', excludedFromAvailable: 'Inventario',
travelReference: 'Referencia', travelReference: 'Referencia',
@ -343,7 +369,6 @@ export default {
reference: 'Referencia', reference: 'Referencia',
observations: 'Observaciónes', observations: 'Observaciónes',
item: 'Artículo', item: 'Artículo',
description: 'Descripción',
size: 'Medida', size: 'Medida',
packing: 'Packing', packing: 'Packing',
grouping: 'Grouping', grouping: 'Grouping',
@ -358,7 +383,6 @@ export default {
}, },
notes: { notes: {
observationType: 'Tipo de observación', observationType: 'Tipo de observación',
description: 'Descripción',
}, },
descriptor: { descriptor: {
agency: 'Agencia', agency: 'Agencia',
@ -371,7 +395,6 @@ export default {
packing: 'Packing', packing: 'Packing',
grouping: 'Grouping', grouping: 'Grouping',
quantity: 'Cantidad', quantity: 'Cantidad',
description: 'Descripción',
size: 'Medida', size: 'Medida',
tags: 'Etiquetas', tags: 'Etiquetas',
type: 'Tipo', type: 'Tipo',
@ -424,6 +447,7 @@ export default {
shipped: 'Enviado', shipped: 'Enviado',
warehouse: 'Almacén', warehouse: 'Almacén',
customerCard: 'Ficha del cliente', customerCard: 'Ficha del cliente',
alias: 'Alias',
}, },
boxing: { boxing: {
expedition: 'Expedición', expedition: 'Expedición',
@ -457,7 +481,6 @@ export default {
visible: 'Visible', visible: 'Visible',
available: 'Disponible', available: 'Disponible',
quantity: 'Cantidad', quantity: 'Cantidad',
description: 'Descripción',
price: 'Precio', price: 'Precio',
discount: 'Descuento', discount: 'Descuento',
packing: 'Encajado', packing: 'Encajado',
@ -475,6 +498,7 @@ export default {
request: 'Petición de compra', request: 'Petición de compra',
weight: 'Peso', weight: 'Peso',
goTo: 'Ir a', goTo: 'Ir a',
summaryAmount: 'Resumen',
}, },
}, },
claim: { claim: {
@ -482,14 +506,12 @@ export default {
claims: 'Reclamaciones', claims: 'Reclamaciones',
list: 'Listado', list: 'Listado',
createClaim: 'Crear reclamación', createClaim: 'Crear reclamación',
rmaList: 'RMA',
summary: 'Resumen', summary: 'Resumen',
basicData: 'Datos básicos', basicData: 'Datos básicos',
lines: 'Líneas', lines: 'Líneas',
rma: 'RMA',
development: 'Trazabilidad', development: 'Trazabilidad',
photos: 'Fotos', photos: 'Fotos',
log: 'Registros de auditoría', log: 'Historial',
notes: 'Notas', notes: 'Notas',
action: 'Acción', action: 'Acción',
}, },
@ -503,10 +525,6 @@ export default {
code: 'Código', code: 'Código',
records: 'registros', records: 'registros',
}, },
rma: {
user: 'Usuario',
created: 'Creado',
},
card: { card: {
claimId: 'ID reclamación', claimId: 'ID reclamación',
assignedTo: 'Asignada a', assignedTo: 'Asignada a',
@ -515,6 +533,8 @@ export default {
ticketId: 'ID ticket', ticketId: 'ID ticket',
customerSummary: 'Resumen del cliente', customerSummary: 'Resumen del cliente',
claimedTicket: 'Ticket reclamado', claimedTicket: 'Ticket reclamado',
saleTracking: 'Líneas preparadas',
ticketTracking: 'Estados del ticket',
commercial: 'Comercial', commercial: 'Comercial',
province: 'Provincia', province: 'Provincia',
zone: 'Zona', zone: 'Zona',
@ -530,7 +550,6 @@ export default {
landed: 'Entregado', landed: 'Entregado',
quantity: 'Cantidad', quantity: 'Cantidad',
claimed: 'Reclamado', claimed: 'Reclamado',
description: 'Descripción',
price: 'Precio', price: 'Precio',
discount: 'Descuento', discount: 'Descuento',
total: 'Total', total: 'Total',
@ -546,7 +565,6 @@ export default {
responsible: 'Responsable', responsible: 'Responsable',
worker: 'Trabajador', worker: 'Trabajador',
redelivery: 'Devolución', redelivery: 'Devolución',
returnOfMaterial: 'RMA',
}, },
basicData: { basicData: {
customer: 'Cliente', customer: 'Cliente',
@ -554,7 +572,6 @@ export default {
created: 'Creada', created: 'Creada',
state: 'Estado', state: 'Estado',
picked: 'Recogida', picked: 'Recogida',
returnOfMaterial: 'Autorización de retorno de materiales (RMA)',
}, },
photo: { photo: {
fileDescription: fileDescription:
@ -583,6 +600,7 @@ export default {
company: 'Empresa', company: 'Empresa',
dued: 'Fecha vencimineto', dued: 'Fecha vencimineto',
shortDued: 'F. vencimiento', shortDued: 'F. vencimiento',
amount: 'Importe',
}, },
card: { card: {
issued: 'Fecha emisión', issued: 'Fecha emisión',
@ -618,7 +636,7 @@ export default {
invoiceDateLessThanMaxDate: invoiceDateLessThanMaxDate:
'La fecha de la factura no puede ser menor que la fecha máxima', 'La fecha de la factura no puede ser menor que la fecha máxima',
invoiceWithFutureDate: 'Existe una factura con una fecha futura', invoiceWithFutureDate: 'Existe una factura con una fecha futura',
noTicketsToInvoice: 'No hay clientes para facturar', noTicketsToInvoice: 'No existen tickets para facturar',
criticalInvoiceError: 'Error crítico en la facturación, proceso detenido', criticalInvoiceError: 'Error crítico en la facturación, proceso detenido',
}, },
table: { table: {
@ -701,7 +719,6 @@ export default {
orderTicketList: 'Tickets del pedido', orderTicketList: 'Tickets del pedido',
details: 'Detalles', details: 'Detalles',
item: 'Item', item: 'Item',
description: 'Descripción',
quantity: 'Cantidad', quantity: 'Cantidad',
price: 'Precio', price: 'Precio',
amount: 'Monto', amount: 'Monto',
@ -714,7 +731,7 @@ export default {
create: 'Crear', create: 'Crear',
summary: 'Resumen', summary: 'Resumen',
basicData: 'Datos básicos', basicData: 'Datos básicos',
log: 'Registros de auditoría', log: 'Historial',
}, },
list: { list: {
parking: 'Parking', parking: 'Parking',
@ -735,6 +752,18 @@ export default {
recyclable: 'Reciclable', recyclable: 'Reciclable',
}, },
}, },
parking: {
pickingOrder: 'Orden de recogida',
row: 'Fila',
column: 'Columna',
pageTitles: {
parking: 'Parking',
},
searchBar: {
info: 'Puedes buscar por código de parking',
label: 'Buscar parking...',
},
},
invoiceIn: { invoiceIn: {
pageTitles: { pageTitles: {
invoiceIns: 'Fact. recibidas', invoiceIns: 'Fact. recibidas',
@ -746,7 +775,7 @@ export default {
dueDay: 'Vencimiento', dueDay: 'Vencimiento',
intrastat: 'Intrastat', intrastat: 'Intrastat',
corrective: 'Rectificativa', corrective: 'Rectificativa',
log: 'Registros de auditoría', log: 'Historial',
}, },
list: { list: {
ref: 'Referencia', ref: 'Referencia',
@ -827,6 +856,8 @@ export default {
notifications: 'Notificaciones', notifications: 'Notificaciones',
workerCreate: 'Nuevo trabajador', workerCreate: 'Nuevo trabajador',
department: 'Departamentos', department: 'Departamentos',
pda: 'PDA',
log: 'Historial',
}, },
list: { list: {
name: 'Nombre', name: 'Nombre',
@ -868,6 +899,13 @@ export default {
subscribed: 'Se ha suscrito a la notificación', subscribed: 'Se ha suscrito a la notificación',
unsubscribed: 'Se ha dado de baja de la notificación', unsubscribed: 'Se ha dado de baja de la notificación',
}, },
pda: {
newPDA: 'Nueva PDA',
currentPDA: 'PDA Actual',
model: 'Modelo',
serialNumber: 'Número de serie',
removePDA: 'Desasignar PDA',
},
create: { create: {
name: 'Nombre', name: 'Nombre',
lastName: 'Apellido', lastName: 'Apellido',
@ -933,6 +971,22 @@ export default {
uncompleteTrays: 'Hay bandejas sin completar', uncompleteTrays: 'Hay bandejas sin completar',
}, },
}, },
'route/roadmap': {
pageTitles: {
roadmap: 'Troncales',
summary: 'Resumen',
basicData: 'Datos básicos',
stops: 'Paradas',
},
},
roadmap: {
pageTitles: {
roadmap: 'Troncales',
summary: 'Resumen',
basicData: 'Datos básicos',
stops: 'Paradas',
},
},
route: { route: {
pageTitles: { pageTitles: {
routes: 'Rutas', routes: 'Rutas',
@ -941,7 +995,12 @@ export default {
RouteList: 'Listado', RouteList: 'Listado',
create: 'Crear', create: 'Crear',
basicData: 'Datos básicos', basicData: 'Datos básicos',
summary: 'Summary', summary: 'Resumen',
RouteRoadmap: 'Troncales',
RouteRoadmapCreate: 'Crear troncal',
tickets: 'Tickets',
log: 'Historial',
autonomous: 'Autónomos',
}, },
cmr: { cmr: {
list: { list: {
@ -976,6 +1035,7 @@ export default {
addresses: 'Direcciones', addresses: 'Direcciones',
consumption: 'Consumo', consumption: 'Consumo',
agencyTerm: 'Acuerdo agencia', agencyTerm: 'Acuerdo agencia',
dms: 'Gestión documental',
}, },
list: { list: {
payMethod: 'Método de pago', payMethod: 'Método de pago',
@ -1139,7 +1199,6 @@ export default {
warehouse: 'Almacén', warehouse: 'Almacén',
travelFileDescription: 'Id envío { travelId }', travelFileDescription: 'Id envío { travelId }',
file: 'Fichero', file: 'Fichero',
description: 'Descripción',
}, },
}, },
item: { item: {
@ -1167,13 +1226,17 @@ export default {
copyToken: 'Token copiado al portapapeles', copyToken: 'Token copiado al portapapeles',
settings: 'Configuración', settings: 'Configuración',
logOut: 'Cerrar sesión', logOut: 'Cerrar sesión',
localWarehouse: 'Almacén local',
localBank: 'Banco local',
localCompany: 'Empresa local',
userWarehouse: 'Almacén del usuario',
userCompany: 'Empresa del usuario',
}, },
smartCard: { smartCard: {
downloadFile: 'Descargar archivo', downloadFile: 'Descargar archivo',
clone: 'Clonar', clone: 'Clonar',
openCard: 'Ficha', openCard: 'Ficha',
openSummary: 'Detalles', openSummary: 'Detalles',
viewDescription: 'Descripción',
}, },
cardDescriptor: { cardDescriptor: {
mainList: 'Listado principal', mainList: 'Listado principal',

View File

@ -11,5 +11,3 @@ const quasar = useQuasar();
<QFooter v-if="quasar.platform.is.mobile"></QFooter> <QFooter v-if="quasar.platform.is.mobile"></QFooter>
</QLayout> </QLayout>
</template> </template>
<style lang="scss" scoped></style>

View File

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

View File

@ -291,8 +291,6 @@ async function importToNewRefundTicket() {
selection="multiple" selection="multiple"
v-model:selected="selectedRows" v-model:selected="selectedRows"
:grid="$q.screen.lt.md" :grid="$q.screen.lt.md"
:pagination="{ rowsPerPage: 0 }"
:hide-bottom="true"
> >
<template #body-cell-ticket="{ value }"> <template #body-cell-ticket="{ value }">
<QTd align="center"> <QTd align="center">

View File

@ -13,8 +13,8 @@ import { useSession } from 'src/composables/useSession';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const claimFilter = { const claimFilter = {
fields: [ fields: [
@ -24,7 +24,6 @@ const claimFilter = {
'workerFk', 'workerFk',
'claimStateFk', 'claimStateFk',
'packages', 'packages',
'rma',
'hasToPickUp', 'hasToPickUp',
], ],
include: [ include: [
@ -169,13 +168,6 @@ const statesFilter = {
type="number" type="number"
/> />
</div> </div>
<div class="col">
<VnInput
v-model="data.rma"
:label="t('claim.basicData.returnOfMaterial')"
:rules="validate('claim.rma')"
/>
</div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">

View File

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

View File

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

View File

@ -150,10 +150,8 @@ const columns = computed(() => [
<QTable <QTable
:columns="columns" :columns="columns"
:rows="rows" :rows="rows"
:pagination="{ rowsPerPage: 0 }"
row-key="$index" row-key="$index"
selection="multiple" selection="multiple"
hide-pagination
v-model:selected="selected" v-model:selected="selected"
:grid="$q.screen.lt.md" :grid="$q.screen.lt.md"
table-header-class="text-left" table-header-class="text-left"

View File

@ -161,7 +161,7 @@ function showImportDialog() {
<div class="row q-gutter-md"> <div class="row q-gutter-md">
<div> <div>
{{ t('Amount') }} {{ t('Amount') }}
<QChip :dense="$q.screen.lt.sm"> <QChip :dense="$q.screen.lt.sm" text-color="white">
{{ toCurrency(amount) }} {{ toCurrency(amount) }}
</QChip> </QChip>
</div> </div>
@ -201,11 +201,9 @@ function showImportDialog() {
:columns="columns" :columns="columns"
:rows="rows" :rows="rows"
:dense="$q.screen.lt.md" :dense="$q.screen.lt.md"
:pagination="{ rowsPerPage: 0 }"
row-key="id" row-key="id"
selection="multiple" selection="multiple"
v-model:selected="selected" v-model:selected="selected"
hide-pagination
:grid="$q.screen.lt.md" :grid="$q.screen.lt.md"
> >
<template #body-cell-claimed="{ row, value }"> <template #body-cell-claimed="{ row, value }">

View File

@ -121,7 +121,6 @@ function cancel() {
class="my-sticky-header-table" class="my-sticky-header-table"
:columns="columns" :columns="columns"
:rows="claimableSales" :rows="claimableSales"
:pagination="{ rowsPerPage: 10 }"
row-key="saleFk" row-key="saleFk"
selection="multiple" selection="multiple"
v-model:selected="selected" v-model:selected="selected"

View File

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

View File

@ -11,8 +11,8 @@ import FetchData from 'components/FetchData.vue';
const router = useRouter(); const router = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const claimId = computed(() => router.currentRoute.value.params.id); const claimId = computed(() => router.currentRoute.value.params.id);

View File

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

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, ref, computed, watch } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
@ -11,11 +11,12 @@ import VnLv from 'src/components/ui/VnLv.vue';
import ClaimNotes from 'src/pages/Claim/Card/ClaimNotes.vue'; import ClaimNotes from 'src/pages/Claim/Card/ClaimNotes.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -35,7 +36,6 @@ const claimDmsFilter = ref({
relation: 'dms', relation: 'dms',
}, },
], ],
where: { claimFk: entityId.value },
}); });
onMounted(async () => { onMounted(async () => {
@ -71,7 +71,7 @@ const detailsColumns = ref([
}, },
{ {
name: 'description', name: 'description',
label: 'claim.summary.description', label: 'globals.description',
field: (row) => row.sale.concept, field: (row) => row.sale.concept,
}, },
{ {
@ -141,6 +141,11 @@ const claimDms = ref([]);
const multimediaDialog = ref(); const multimediaDialog = ref();
const multimediaSlide = ref(); const multimediaSlide = ref();
async function getClaimDms() {
claimDmsFilter.value.where = { claimFk: entityId.value };
await claimDmsRef.value.fetch();
}
function setClaimDms(data) { function setClaimDms(data) {
claimDms.value = []; claimDms.value = [];
data.forEach((media) => { data.forEach((media) => {
@ -163,19 +168,23 @@ function openDialog(dmsId) {
url="ClaimDms" url="ClaimDms"
:filter="claimDmsFilter" :filter="claimDmsFilter"
@on-fetch="(data) => setClaimDms(data)" @on-fetch="(data) => setClaimDms(data)"
limit="20"
ref="claimDmsRef" ref="claimDmsRef"
/> />
<CardSummary ref="summary" :url="`Claims/${entityId}/getSummary`"> <CardSummary
ref="summary"
:url="`Claims/${entityId}/getSummary`"
:entity-id="entityId"
@on-fetch="getClaimDms"
>
<template #header="{ entity: { claim } }"> <template #header="{ entity: { claim } }">
{{ claim.id }} - {{ claim.client.name }} {{ claim.id }} - {{ claim.client.name }}
</template> </template>
<template #body="{ entity: { claim, salesClaimed, developments } }"> <template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one"> <QCard class="vn-one">
<a class="header" :href="`#/claim/${entityId}/basic-data`"> <VnTitle
{{ t('claim.pageTitles.basicData') }} :url="`#/claim/${entityId}/basic-data`"
<QIcon name="open_in_new" color="primary" /> :text="t('claim.pageTitles.basicData')"
</a> />
<VnLv <VnLv
:label="t('claim.summary.created')" :label="t('claim.summary.created')"
:value="toDate(claim.created)" :value="toDate(claim.created)"
@ -187,19 +196,19 @@ function openDialog(dmsId) {
</QChip> </QChip>
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('claim.summary.assignedTo')"> <VnLv :label="t('globals.salesPerson')">
<template #value> <template #value>
<VnUserLink <VnUserLink
:name="claim.worker?.user?.nickname" :name="claim.client?.salesPersonUser?.name"
:worker-id="claim.workerFk" :worker-id="claim.client?.salesPersonFk"
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('claim.summary.attendedBy')"> <VnLv :label="t('claim.summary.attendedBy')">
<template #value> <template #value>
<VnUserLink <VnUserLink
:name="claim.client?.salesPersonUser?.name" :name="claim.worker?.user?.nickname"
:worker-id="claim.client?.salesPersonFk" :worker-id="claim.workerFk"
/> />
</template> </template>
</VnLv> </VnLv>
@ -211,26 +220,37 @@ function openDialog(dmsId) {
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('claim.summary.returnOfMaterial')" :value="claim.rma" />
<QCheckbox <QCheckbox
:align-items="right"
:label="t('claim.basicData.picked')" :label="t('claim.basicData.picked')"
v-model="claim.hasToPickUp" v-model="claim.hasToPickUp"
:disable="true"
/> />
</QCard> </QCard>
<QCard class="vn-three claimVnNotes full-height"> <QCard class="vn-three">
<a class="header" :href="`#/claim/${entityId}/notes`"> <VnTitle
{{ t('claim.summary.notes') }} :url="`#/claim/${entityId}/notes`"
<QIcon name="open_in_new" color="primary" /> :text="t('claim.summary.notes')"
</a> />
<ClaimNotes :add-note="false" style="height: 350px" order="created ASC" /> <ClaimNotes
:id="entityId"
:add-note="false"
style="max-height: 300px"
order="created ASC"
/>
</QCard> </QCard>
<QCard class="vn-two" v-if="salesClaimed.length > 0"> <QCard class="vn-two" v-if="salesClaimed.length > 0">
<a class="header" :href="`#/claim/${entityId}/lines`"> <VnTitle
{{ t('claim.summary.details') }} :url="`#/claim/${entityId}/lines`"
<QIcon name="open_in_new" color="primary" /> :text="t('claim.summary.details')"
</a> />
<QTable :columns="detailsColumns" :rows="salesClaimed" flat> <QTable
:columns="detailsColumns"
:rows="salesClaimed"
flat
dense
:rows-per-page-options="[0]"
hide-bottom
>
<template #header="props"> <template #header="props">
<QTr :props="props"> <QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props"> <QTh v-for="col in props.cols" :key="col.name" :props="props">
@ -252,7 +272,8 @@ function openDialog(dmsId) {
> >
<ItemDescriptorProxy <ItemDescriptorProxy
v-if="col.name == 'description'" v-if="col.name == 'description'"
:id="2" :id="props.row.sale.itemFk"
:sale-fk="props.row.saleFk"
></ItemDescriptorProxy> ></ItemDescriptorProxy>
</QTh> </QTh>
</QTr> </QTr>
@ -260,11 +281,18 @@ function openDialog(dmsId) {
</QTable> </QTable>
</QCard> </QCard>
<QCard class="vn-two" v-if="developments.length > 0"> <QCard class="vn-two" v-if="developments.length > 0">
<a class="header" :href="claimUrl + 'development'"> <VnTitle
{{ t('claim.summary.development') }} :url="claimUrl + 'development'"
<QIcon name="open_in_new" color="primary" /> :text="t('claim.summary.development')"
</a> />
<QTable :columns="developmentColumns" :rows="developments" flat> <QTable
:columns="developmentColumns"
:rows="developments"
flat
dense
:rows-per-page-options="[0]"
hide-bottom
>
<template #header="props"> <template #header="props">
<QTr :props="props"> <QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props"> <QTh v-for="col in props.cols" :key="col.name" :props="props">
@ -274,12 +302,11 @@ function openDialog(dmsId) {
</template> </template>
</QTable> </QTable>
</QCard> </QCard>
<QCard class="vn-max" v-if="claimDms.length > 0"> <QCard class="vn-max" v-if="claimDms.length > 0">
<a class="header" :href="`#/claim/${entityId}/photos`"> <VnTitle
{{ t('claim.summary.photos') }} :url="`#/claim/${entityId}/photos`"
<QIcon name="open_in_new" color="primary" /> :text="t('claim.summary.photos')"
</a> />
<div class="container"> <div class="container">
<div <div
class="multimedia-container" class="multimedia-container"
@ -295,7 +322,7 @@ function openDialog(dmsId) {
v-if="media.isVideo" v-if="media.isVideo"
@click.stop="openDialog(media.dmsFk)" @click.stop="openDialog(media.dmsFk)"
> >
<QTooltip>Video</QTooltip> <QTooltip>Video</QTooltip>header
</QIcon> </QIcon>
<QCard class="multimedia relative-position"> <QCard class="multimedia relative-position">
<QImg <QImg
@ -319,17 +346,14 @@ function openDialog(dmsId) {
</QCard> </QCard>
<QCard class="vn-max"> <QCard class="vn-max">
<a class="header" :href="claimUrl + 'action'"> <VnTitle :url="claimUrl + 'action'" :text="t('claim.summary.actions')" />
{{ t('claim.summary.actions') }}
<QIcon name="open_in_new" color="primary" />
</a>
<div id="slider-container" class="q-px-xl q-py-md"> <div id="slider-container" class="q-px-xl q-py-md">
<QSlider <QSlider
v-model="claim.responsibility" v-model="claim.responsibility"
label label
:label-value="t('claim.summary.responsibility')" :label-value="t('claim.summary.responsibility')"
label-always label-always
color="primary" color="var()"
markers markers
:marker-labels="[ :marker-labels="[
{ value: 1, label: t('claim.summary.company') }, { value: 1, label: t('claim.summary.company') },
@ -383,13 +407,7 @@ function openDialog(dmsId) {
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss">
.claimVnNotes {
.q-card {
max-width: 100%;
}
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-dialog__inner--minimized > div { .q-dialog__inner--minimized > div {
max-width: 80%; max-width: 80%;
@ -399,7 +417,6 @@ function openDialog(dmsId) {
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 15px; gap: 15px;
flex-basis: 30%;
} }
.multimedia-container { .multimedia-container {
flex: 1 0 21%; flex: 1 0 21%;

View File

@ -108,7 +108,11 @@ function navigate(event, id) {
/> />
<VnLv :label="t('claim.list.state')"> <VnLv :label="t('claim.list.state')">
<template #value> <template #value>
<QBadge :color="stateColor(row.stateCode)" dense> <QBadge
text-color="black"
:color="stateColor(row.stateCode)"
dense
>
{{ row.stateDescription }} {{ row.stateDescription }}
</QBadge> </QBadge>
</template> </template>
@ -116,9 +120,8 @@ function navigate(event, id) {
</template> </template>
<template #actions> <template #actions>
<QBtn <QBtn
:label="t('components.smartCard.viewDescription')" :label="t('globals.description')"
@click.stop @click.stop
class="bg-vn-dark"
outline outline
style="margin-top: 15px" style="margin-top: 15px"
> >

View File

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

View File

@ -198,7 +198,6 @@ const updateCompanyId = (id) => {
<QTable <QTable
:columns="columns" :columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows" :rows="rows"
class="full-width q-mt-md" class="full-width q-mt-md"
row-key="id" row-key="id"

View File

@ -11,8 +11,8 @@ import VnInput from 'src/components/common/VnInput.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const session = useSession(); const { getTokenMultimedia } = useSession();
const token = session.getToken(); const token = getTokenMultimedia();
const workers = ref([]); const workers = ref([]);
const workersCopy = ref([]); const workersCopy = ref([]);

View File

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

View File

@ -150,14 +150,14 @@ const toCustomerConsigneeEdit = (consigneeId) => {
<style lang="scss" scoped> <style lang="scss" scoped>
.consignees-card { .consignees-card {
border: 2px solid var(--vn-light-gray); border: 2px solid var(--vn-accent-color);
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
display: flex; display: flex;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: var(--vn-light-gray); background-color: var(--vn-accent-color);
} }
} }
</style> </style>

View File

@ -97,13 +97,7 @@ const toCustomerCreditCreate = () => {
<template> <template>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<QTable <QTable :columns="columns" :rows="rows" class="full-width q-mt-md" row-key="id">
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
>
<template #body-cell="props"> <template #body-cell="props">
<QTd :props="props"> <QTd :props="props">
<QTr :props="props" class="cursor-pointer"> <QTr :props="props" class="cursor-pointer">

View File

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

View File

@ -140,7 +140,6 @@ const toCustomerGreugeCreate = () => {
<QTable <QTable
:columns="columns" :columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows" :rows="rows"
class="full-width q-mt-md" class="full-width q-mt-md"
row-key="id" row-key="id"
@ -180,13 +179,13 @@ const toCustomerGreugeCreate = () => {
<style lang="scss"> <style lang="scss">
.consignees-card { .consignees-card {
border: 2px solid var(--vn-light-gray); border: 2px solid var(--vn-accent-color);
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
} }
.label-color { .label-color {
color: var(--vn-label); color: var(--vn-label-color);
} }
</style> </style>

View File

@ -85,12 +85,12 @@ const toCustomerNoteCreate = () => {
<style lang="scss"> <style lang="scss">
.custom-border { .custom-border {
border: 2px solid var(--vn-light-gray); border: 2px solid var(--vn-accent-color);
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
} }
.label-color { .label-color {
color: var(--vn-label); color: var(--vn-label-color);
} }
</style> </style>

View File

@ -97,7 +97,6 @@ const toCustomerRecoverieCreate = () => {
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<QTable <QTable
:columns="columns" :columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows" :rows="rows"
class="full-width q-mt-md" class="full-width q-mt-md"
row-key="id" row-key="id"
@ -137,13 +136,13 @@ const toCustomerRecoverieCreate = () => {
<style lang="scss"> <style lang="scss">
.consignees-card { .consignees-card {
border: 2px solid var(--vn-light-gray); border: 2px solid var(--vn-accent-color);
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
} }
.label-color { .label-color {
color: var(--vn-label); color: var(--vn-label-color);
} }
</style> </style>

View File

@ -7,6 +7,7 @@ import CardSummary from 'components/ui/CardSummary.vue';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -62,10 +63,10 @@ const creditWarning = computed(() => {
<CardSummary ref="summary" :url="`Clients/${entityId}/summary`"> <CardSummary ref="summary" :url="`Clients/${entityId}/summary`">
<template #body="{ entity }"> <template #body="{ entity }">
<QCard class="vn-one"> <QCard class="vn-one">
<a class="header" :href="clientUrl + `basic-data`"> <VnTitle
{{ t('customer.summary.basicData') }} :url="`#/customer/${entityId}/basic-data`"
<QIcon name="open_in_new" color="primary" /> :text="t('customer.summary.basicData')"
</a> />
<VnLv :label="t('customer.summary.customerId')" :value="entity.id" /> <VnLv :label="t('customer.summary.customerId')" :value="entity.id" />
<VnLv :label="t('customer.summary.name')" :value="entity.name" /> <VnLv :label="t('customer.summary.name')" :value="entity.name" />
<VnLv :label="t('customer.summary.contact')" :value="entity.contact" /> <VnLv :label="t('customer.summary.contact')" :value="entity.contact" />
@ -96,10 +97,10 @@ const creditWarning = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<a class="header" :href="clientUrl + `fiscal-data`"> <VnTitle
{{ t('customer.summary.fiscalAddress') }} :url="`#/customer/${entityId}/fiscal-data`"
<QIcon name="open_in_new" color="primary" /> :text="t('customer.summary.fiscalAddress')"
</a> />
<VnLv <VnLv
:label="t('customer.summary.socialName')" :label="t('customer.summary.socialName')"
:value="entity.socialName" :value="entity.socialName"
@ -121,59 +122,80 @@ const creditWarning = computed(() => {
<VnLv :label="t('customer.summary.street')" :value="entity.street" /> <VnLv :label="t('customer.summary.street')" :value="entity.street" />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<a class="header link" :href="clientUrl + `fiscal-data`" link> <VnTitle
{{ t('customer.summary.fiscalAddress') }} :url="`#/customer/${entityId}/fiscal-data`"
<QIcon name="open_in_new" color="primary" /> :text="t('customer.summary.fiscalData')"
</a> />
<VnLv <QCheckbox
:label="t('customer.summary.isEqualizated')" :label="t('customer.summary.isEqualizated')"
:value="entity.isEqualizated" v-model="entity.isEqualizated"
:disable="true"
/> />
<VnLv :label="t('customer.summary.isActive')" :value="entity.isActive" /> <QCheckbox
<VnLv :label="t('customer.summary.isActive')"
v-model="entity.isActive"
:disable="true"
/>
<QCheckbox
:label="t('customer.summary.invoiceByAddress')" :label="t('customer.summary.invoiceByAddress')"
:value="entity.hasToInvoiceByAddress" v-model="entity.hasToInvoiceByAddress"
:disable="true"
/> />
<VnLv <QCheckbox
:label="t('customer.summary.verifiedData')" :label="t('customer.summary.verifiedData')"
:value="entity.isTaxDataChecked" v-model="entity.isTaxDataChecked"
:disable="true"
/> />
<VnLv <QCheckbox
:label="t('customer.summary.hasToInvoice')" :label="t('customer.summary.hasToInvoice')"
:value="entity.hasToInvoice" v-model="entity.hasToInvoice"
:disable="true"
/> />
<VnLv <QCheckbox
:label="t('customer.summary.notifyByEmail')" :label="t('customer.summary.notifyByEmail')"
:value="entity.isToBeMailed" v-model="entity.isToBeMailed"
:disable="true"
/>
<QCheckbox
:label="t('customer.summary.vies')"
v-model="entity.isVies"
:disable="true"
/> />
<VnLv :label="t('customer.summary.vies')" :value="entity.isVies" />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<a class="header link" :href="clientUrl + `billing-data`" link> <VnTitle
{{ t('customer.summary.billingData') }} :url="`#/customer/${entityId}/billing-data`"
<QIcon name="open_in_new" color="primary" /> :text="t('customer.summary.billingData')"
</a> />
<VnLv <VnLv
:label="t('customer.summary.payMethod')" :label="t('customer.summary.payMethod')"
:value="entity.payMethod.name" :value="entity.payMethod.name"
/> />
<VnLv :label="t('customer.summary.bankAccount')" :value="entity.iban" /> <VnLv :label="t('customer.summary.bankAccount')" :value="entity.iban" />
<VnLv :label="t('customer.summary.dueDay')" :value="entity.dueDay" /> <VnLv :label="t('customer.summary.dueDay')" :value="entity.dueDay" />
<VnLv :label="t('customer.summary.hasLcr')" :value="entity.hasLcr" /> <QCheckbox
<VnLv style="padding: 0"
:label="t('customer.summary.hasCoreVnl')" :label="t('customer.summary.hasLcr')"
:value="entity.hasCoreVnl" v-model="entity.hasLcr"
:disable="true"
/> />
<VnLv <QCheckbox
:label="t('customer.summary.hasCoreVnl')"
v-model="entity.hasCoreVnl"
:disable="true"
/>
<QCheckbox
:label="t('customer.summary.hasB2BVnl')" :label="t('customer.summary.hasB2BVnl')"
:value="entity.hasSepaVnl" v-model="entity.hasSepaVnl"
:disable="true"
/> />
</QCard> </QCard>
<QCard class="vn-one" v-if="entity.defaultAddress"> <QCard class="vn-one" v-if="entity.defaultAddress">
<a class="header link" :href="clientUrl + `address/index`" link> <VnTitle
{{ t('customer.summary.consignee') }} :url="`#/customer/${entityId}/consignees`"
<QIcon name="open_in_new" color="primary" /> :text="t('customer.summary.consignee')"
</a> />
<VnLv <VnLv
:label="t('customer.summary.addressName')" :label="t('customer.summary.addressName')"
:value="entity.defaultAddress.nickname" :value="entity.defaultAddress.nickname"
@ -188,23 +210,22 @@ const creditWarning = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one" v-if="entity.account"> <QCard class="vn-one" v-if="entity.account">
<a class="header link" :href="clientUrl + `web-access`"> <VnTitle
{{ t('customer.summary.webAccess') }} :url="`#/customer/${entityId}/web-access`"
<QIcon name="open_in_new" color="primary" /> :text="t('customer.summary.webAccess')"
</a> />
<VnLv <VnLv
:label="t('customer.summary.username')" :label="t('customer.summary.username')"
:value="entity.account.name" :value="entity.account.name"
/> />
<VnLv <QCheckbox
:label="t('customer.summary.webAccess')" :label="t('customer.summary.webAccess')"
:value="entity.account.active" v-model="entity.account.active"
:disable="true"
/> />
</QCard> </QCard>
<QCard class="vn-one" v-if="entity.account"> <QCard class="vn-one" v-if="entity.account">
<div class="header"> <VnTitle :text="t('customer.summary.businessData')" />
{{ t('customer.summary.businessData') }}
</div>
<VnLv <VnLv
:label="t('customer.summary.totalGreuge')" :label="t('customer.summary.totalGreuge')"
:value="toCurrency(entity.totalGreuge)" :value="toCurrency(entity.totalGreuge)"
@ -229,14 +250,11 @@ const creditWarning = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one" v-if="entity.account"> <QCard class="vn-one" v-if="entity.account">
<a <VnTitle
class="header link" :url="`https://grafana.verdnatura.es/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk=${entityId}`"
:href="`https://grafana.verdnatura.es/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk=${entityId}`" :text="t('customer.summary.financialData')"
link icon="vn:grafana"
> />
{{ t('customer.summary.financialData') }}
<QIcon name="vn:grafana" color="primary" />
</a>
<VnLv <VnLv
:label="t('customer.summary.risk')" :label="t('customer.summary.risk')"
:value="toCurrency(entity?.debt?.debt)" :value="toCurrency(entity?.debt?.debt)"
@ -276,7 +294,30 @@ const creditWarning = computed(() => {
:label="t('customer.summary.recoverySince')" :label="t('customer.summary.recoverySince')"
:value="toDate(entity.recovery.started)" :value="toDate(entity.recovery.started)"
/> />
<VnLv
:label="t('customer.summary.rating')"
:value="entity.rating"
:info="t('valueInfo', { min: 1, max: 20 })"
/>
<VnLv
:label="t('customer.summary.recommendCredit')"
:value="entity.recommendedCredit"
/>
</QCard> </QCard>
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss" scoped>
@media (min-width: $breakpoint-md) {
.summary .vn-one {
min-width: 300px;
}
}
</style>
<i18n>
en:
valueInfo: Value from {min} to {max}. The higher the better value
es:
valueInfo: Valor de {min} a {max}. Cuanto más alto, mejor valor
</i18n>

View File

@ -84,7 +84,6 @@ const redirectToCreateView = () => {
<QBtn <QBtn
:label="t('components.smartCard.openCard')" :label="t('components.smartCard.openCard')"
@click.stop="navigate(row.id)" @click.stop="navigate(row.id)"
class="bg-vn-dark"
outline outline
/> />
<QBtn <QBtn

View File

@ -30,16 +30,16 @@ const { t } = useI18n();
border: 1px solid black; border: 1px solid black;
} }
.title_balance { .title_balance {
color: var(--vn-text); color: var(--vn-text-color);
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
.key_balance { .key_balance {
color: var(--vn-label); color: var(--vn-label-color);
margin-bottom: 0; margin-bottom: 0;
} }
.value_balance { .value_balance {
color: var(--vn-text); color: var(--vn-text-color);
margin-bottom: 0; margin-bottom: 0;
} }
</style> </style>

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