#7136 - Enable paginate event in VnSelectFilter #255

Closed
jsegarra wants to merge 86 commits from 7136_vnselectFilter_paginate into dev
322 changed files with 24160 additions and 11122 deletions
Showing only changes of commit 4e6dfbb868 - Show all commits

View File

@ -5,12 +5,17 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2420.01]
## [2418.01]
## [2416.01] - 2024-04-18
### Added
- (Worker) => Se crea la sección Taquilla
- (General) => Se mantiene el filtro lateral en cualquier parte de la seccíon.
### Fixed
- (General) => Se vuelven a mostrar los parámetros en la url al aplicar un filtro

View File

@ -1,6 +1,6 @@
{
"name": "salix-front",
"version": "24.18.0",
"version": "24.22.0",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",

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)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: ['i18n', 'axios', 'vnDate', 'validations', 'quasar.defaults'],
boot: ['i18n', 'axios', 'vnDate', 'validations', 'quasar', 'quasar.defaults'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'],

View File

@ -3,17 +3,12 @@ import { reactive, ref, onMounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import FormModelPopup from './FormModelPopup.vue';
const props = defineProps({
showEntityField: {
type: Boolean,
default: true,
},
});
defineProps({ showEntityField: { type: Boolean, default: true } });
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
@ -26,7 +21,7 @@ const bankEntityFormData = reactive({
});
const countriesFilter = {
fields: ['id', 'country', 'code'],
fields: ['id', 'name', 'code'],
};
const countriesOptions = ref([]);
@ -58,32 +53,28 @@ onMounted(async () => {
>
<template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('name')"
v-model="data.name"
:required="true"
:rules="validate('bankEntity.name')"
/>
</div>
<div class="col">
<VnInput
ref="bicInputRef"
:label="t('swift')"
v-model="data.bic"
:required="true"
:rules="validate('bankEntity.bic')"
/>
</div>
<VnInput
:label="t('name')"
v-model="data.name"
:required="true"
:rules="validate('bankEntity.name')"
/>
<VnInput
ref="bicInputRef"
:label="t('swift')"
v-model="data.bic"
:required="true"
:rules="validate('bankEntity.bic')"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
<VnSelect
:label="t('country')"
v-model="data.countryFk"
:options="countriesOptions"
option-value="id"
option-label="country"
option-label="name"
hide-selected
:required="true"
:rules="validate('bankEntity.countryFk')"

View File

@ -52,9 +52,7 @@ onMounted(() => {
</span>
<h1 class="title">{{ t('New department') }}</h1>
<VnRow class="row q-gutter-md q-mb-md" style="min-width: 250px">
<div class="col">
<VnInput :label="t('Name')" v-model="data.name" />
</div>
<VnInput :label="t('Name')" v-model="data.name" />
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn

View File

@ -5,7 +5,7 @@ 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 VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from './FormModelPopup.vue';
import VnInputDate from './common/VnInputDate.vue';
@ -72,69 +72,57 @@ const onDataSaved = async (formData, requestResponse) => {
{{ 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>
<VnSelect
: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>
</VnSelect>
<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>
<VnSelect
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.clientFk"
@update:model-value="data.ticketFk = null"
/>
<VnInputDate :label="t('Max date')" v-model="data.maxShipped" />
</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>
<VnSelect
:label="t('Serial')"
:options="invoiceOutSerialsOptions"
hide-selected
option-label="description"
option-value="code"
v-model="data.serial"
:required="true"
/>
<VnSelect
:label="t('Area')"
:options="taxAreasOptions"
hide-selected
option-label="code"
option-value="code"
v-model="data.taxArea"
:required="true"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from './FormModelPopup.vue';
@ -40,24 +40,20 @@ const onDataSaved = (dataSaved) => {
>
<template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Name')"
v-model="data.name"
:rules="validate('city.name')"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Province')"
:options="provincesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
:rules="validate('city.provinceFk')"
/>
</div>
<VnInput
:label="t('Name')"
v-model="data.name"
:rules="validate('city.name')"
/>
<VnSelect
:label="t('Province')"
:options="provincesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
:rules="validate('city.provinceFk')"
/>
</VnRow>
</template>
</FormModelPopup>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import CreateNewCityForm from './CreateNewCityForm.vue';
import CreateNewProvinceForm from './CreateNewProvinceForm.vue';
@ -30,21 +30,21 @@ const townsLocationOptions = ref([]);
const onDataSaved = (formData) => {
const newPostcode = {
...formData
...formData,
};
const townObject = townsLocationOptions.value.find(
({id}) => id === formData.townFk
({ id }) => id === formData.townFk
);
newPostcode.town = townObject?.name;
const provinceObject = provincesOptions.value.find(
({id}) => id === formData.provinceFk
({ id }) => id === formData.provinceFk
);
newPostcode.province = provinceObject?.name;
const countryObject = countriesOptions.value.find(
({id}) => id === formData.countryFk
({ id }) => id === formData.countryFk
);
newPostcode.country = countryObject?.country;
emit('onDataSaved', newPostcode);
emit('onDataSaved', newPostcode);
};
const onCityCreated = async ({ name, provinceFk }, formData) => {
@ -92,63 +92,55 @@ const onProvinceCreated = async ({ name }, formData) => {
>
<template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Postcode')"
v-model="data.code"
:rules="validate('postcode.code')"
/>
</div>
<div class="col">
<VnSelectDialog
:label="t('City')"
:options="townsLocationOptions"
v-model="data.townFk"
hide-selected
option-label="name"
option-value="id"
:rules="validate('postcode.city')"
:roles-allowed-to-create="['deliveryAssistant']"
>
<template #form>
<CreateNewCityForm
@on-data-saved="onCityCreated($event, data)"
/>
</template>
</VnSelectDialog>
</div>
<VnInput
:label="t('Postcode')"
v-model="data.code"
:rules="validate('postcode.code')"
/>
<VnSelectDialog
:label="t('City')"
:options="townsLocationOptions"
v-model="data.townFk"
hide-selected
option-label="name"
option-value="id"
:rules="validate('postcode.city')"
:roles-allowed-to-create="['deliveryAssistant']"
>
<template #form>
<CreateNewCityForm @on-data-saved="onCityCreated($event, data)" />
</template>
</VnSelectDialog>
</VnRow>
<VnRow class="row q-gutter-md q-mb-xl">
<div class="col">
<VnSelectDialog
:label="t('Province')"
:options="provincesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
:rules="validate('postcode.provinceFk')"
:roles-allowed-to-create="['deliveryAssistant']"
>
<template #form>
<CreateNewProvinceForm
@on-data-saved="onProvinceCreated($event, data)"
/>
</template>
</VnSelectDialog>
</div>
<div class="col">
<VnSelectFilter
:label="t('Country')"
:options="countriesOptions"
hide-selected
option-label="country"
option-value="id"
v-model="data.countryFk"
:rules="validate('postcode.countryFk')"
/>
</div> </VnRow
></template>
<VnSelectDialog
:label="t('Province')"
:options="provincesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.provinceFk"
:rules="validate('postcode.provinceFk')"
:roles-allowed-to-create="['deliveryAssistant']"
>
<template #form>
<CreateNewProvinceForm
@on-data-saved="onProvinceCreated($event, data)"
/>
</template> </VnSelectDialog
></VnRow>
<VnRow class="row q-gutter-md q-mb-xl"
><VnSelect
:label="t('Country')"
:options="countriesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.countryFk"
:rules="validate('postcode.countryFk')"
/>
</VnRow>
</template>
</FormModelPopup>
</template>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from './FormModelPopup.vue';
@ -40,24 +40,20 @@ const onDataSaved = (dataSaved) => {
>
<template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Name')"
v-model="data.name"
:rules="validate('province.name')"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Autonomy')"
:options="autonomiesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.autonomyFk"
:rules="validate('province.autonomyFk')"
/>
</div>
<VnInput
:label="t('Name')"
v-model="data.name"
:rules="validate('province.name')"
/>
<VnSelect
:label="t('Autonomy')"
:options="autonomiesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.autonomyFk"
:rules="validate('province.autonomyFk')"
/>
</VnRow>
</template>
</FormModelPopup>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from './FormModelPopup.vue';
@ -54,51 +54,42 @@ const onDataSaved = (dataSaved) => {
>
<template #form-inputs="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Identifier')"
v-model="data.thermographId"
:required="true"
:rules="validate('thermograph.id')"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Model')"
:options="thermographsModels"
hide-selected
option-label="value"
option-value="value"
v-model="data.model"
:required="true"
:rules="validate('thermograph.model')"
/>
</div>
<VnInput
:label="t('Identifier')"
v-model="data.thermographId"
:required="true"
:rules="validate('thermograph.id')"
/>
<VnSelect
:label="t('Model')"
:options="thermographsModels"
hide-selected
option-label="value"
option-value="value"
v-model="data.model"
:required="true"
:rules="validate('thermograph.model')"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-xl">
<div class="col">
<VnSelectFilter
:label="t('Warehouse')"
:options="warehousesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.warehouseId"
:required="true"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Temperature')"
:options="temperaturesOptions"
hide-selected
option-label="name"
option-value="code"
v-model="data.temperatureFk"
:required="true"
/>
</div>
<VnSelect
:label="t('Warehouse')"
:options="warehousesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.warehouseId"
:required="true"
/>
<VnSelect
:label="t('Temperature')"
:options="temperaturesOptions"
hide-selected
option-label="name"
option-value="code"
v-model="data.temperatureFk"
:required="true"
/>
</VnRow>
</template>
</FormModelPopup>

View File

@ -81,6 +81,7 @@ defineExpose({
hasChanges,
saveChanges,
getChanges,
formData,
});
async function fetch(data) {
@ -124,11 +125,16 @@ async function onSubmit() {
});
}
isLoading.value = true;
await saveChanges();
await saveChanges($props.saveFn ? formData.value : null);
}
async function saveChanges(data) {
if ($props.saveFn) return $props.saveFn(data, getChanges);
if ($props.saveFn) {
$props.saveFn(data, getChanges);
isLoading.value = false;
hasChanges.value = false;
return;
}
const changes = data || getChanges();
try {
await axios.post($props.saveUrl || $props.url + '/crud', changes);

View File

@ -2,7 +2,7 @@
import { reactive, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
@ -246,61 +246,55 @@ const makeRequest = async () => {
<div class="column">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QOptionGroup
:options="uploadMethodsOptions"
type="radio"
v-model="uploadMethodSelected"
/>
</div>
<QOptionGroup
:options="uploadMethodsOptions"
type="radio"
v-model="uploadMethodSelected"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QFile
v-if="uploadMethodSelected === 'computer'"
ref="inputFileRef"
:label="t('File')"
:multiple="false"
v-model="newPhoto.files"
@update:model-value="updatePhotoPreview($event)"
:accept="allowedContentTypes"
class="required cursor-pointer"
>
<template #append>
<QIcon
name="vn:attach"
class="cursor-pointer q-mr-sm"
@click="openInputFile()"
>
<!-- <QTooltip>{{ t('globals.selectFile') }}</QTooltip> -->
</QIcon>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
t('globals.allowedFilesText', {
allowedContentTypes: allowedContentTypes,
})
}}</QTooltip>
</QIcon>
</template>
</QFile>
<VnInput
v-if="uploadMethodSelected === 'URL'"
v-model="newPhoto.url"
@update:model-value="updatePhotoPreview($event)"
placeholder="https://"
/>
</div>
<QFile
v-if="uploadMethodSelected === 'computer'"
ref="inputFileRef"
:label="t('File')"
:multiple="false"
v-model="newPhoto.files"
@update:model-value="updatePhotoPreview($event)"
:accept="allowedContentTypes"
class="required cursor-pointer"
>
<template #append>
<QIcon
name="vn:attach"
class="cursor-pointer q-mr-sm"
@click="openInputFile()"
>
<!-- <QTooltip>{{ t('globals.selectFile') }}</QTooltip> -->
</QIcon>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
t('globals.allowedFilesText', {
allowedContentTypes: allowedContentTypes,
})
}}</QTooltip>
</QIcon>
</template>
</QFile>
<VnInput
v-if="uploadMethodSelected === 'URL'"
v-model="newPhoto.url"
@update:model-value="updatePhotoPreview($event)"
placeholder="https://"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Orientation')"
:options="viewportTypes"
hide-selected
option-label="description"
v-model="viewportSelection"
/>
</div>
<VnSelect
:label="t('Orientation')"
:options="viewportTypes"
hide-selected
option-label="description"
v-model="viewportSelection"
/>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn

View File

@ -1,9 +1,12 @@
<script setup>
import { ref, reactive } from 'vue';
import { ref, markRaw } from 'vue';
import { useI18n } from 'vue-i18n';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnRow from 'components/ui/VnRow.vue';
import { QCheckbox } from 'quasar';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
@ -28,11 +31,16 @@ const $props = defineProps({
const { t } = useI18n();
const { notify } = useNotify();
const formData = reactive({
field: null,
newValue: null,
});
const inputs = {
input: markRaw(VnInput),
number: markRaw(VnInput),
date: markRaw(VnInputDate),
checkbox: markRaw(QCheckbox),
select: markRaw(VnSelect),
};
const newValue = ref(null);
const selectedField = ref(null);
const closeButton = ref(null);
const isLoading = ref(false);
@ -47,8 +55,8 @@ const submitData = async () => {
isLoading.value = true;
const rowsToEdit = $props.rows.map((row) => ({ id: row.id, itemFk: row.itemFk }));
const payload = {
field: formData.field,
newValue: formData.newValue,
field: selectedField.value.field,
newValue: newValue.value,
lines: rowsToEdit,
};
@ -75,19 +83,20 @@ const closeForm = () => {
<span class="countLines">{{ ` ${rows.length} ` }}</span>
<span class="title">{{ t('buy(s)') }}</span>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Field to edit')"
:options="fieldsOptions"
hide-selected
option-label="label"
option-value="field"
v-model="formData.field"
/>
</div>
<div class="col">
<VnInput :label="t('Value')" v-model="formData.newValue" />
</div>
<VnSelect
:label="t('Field to edit')"
:options="fieldsOptions"
hide-selected
option-label="label"
v-model="selectedField"
/>
<component
:is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}"
v-model="newValue"
:label="t('Value')"
style="width: 200px"
/>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn

View File

@ -1,21 +1,26 @@
<script setup>
import { ref, reactive, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnSelect from 'components/common/VnSelect.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import axios from 'axios';
import { dashIfEmpty } from 'src/filters';
const props = defineProps({
url: {
type: String,
required: true,
},
});
const emit = defineEmits(['itemSelected']);
const { t } = useI18n();
const route = useRoute();
const itemFilter = {
include: [
@ -73,7 +78,7 @@ const tableColumns = computed(() => [
{
label: t('entry.buys.color'),
name: 'ink',
field: 'inkName',
field: (row) => row?.ink?.name,
align: 'left',
},
]);
@ -100,7 +105,7 @@ const fetchResults = async () => {
}
filter.where = where;
const { data } = await axios.get(`Entries/${route.params.id}/lastItemBuys`, {
const { data } = await axios.get(props.url, {
params: { filter: JSON.stringify(filter) },
});
tableRows.value = data;
@ -147,48 +152,32 @@ const selectItem = ({ id }) => {
</span>
<h1 class="title">{{ t('Filter item') }}</h1>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('entry.buys.name')"
v-model="itemFilterParams.name"
/>
</div>
<div class="col">
<VnInput
:label="t('entry.buys.size')"
v-model="itemFilterParams.size"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('entry.buys.producer')"
:options="producersOptions"
hide-selected
option-label="name"
option-value="id"
v-model="itemFilterParams.producerFk"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('entry.buys.type')"
:options="ItemTypesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="itemFilterParams.typeFk"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('entry.buys.color')"
:options="InksOptions"
hide-selected
option-label="name"
option-value="id"
v-model="itemFilterParams.inkFk"
/>
</div>
<VnInput :label="t('entry.buys.name')" v-model="itemFilterParams.name" />
<VnInput :label="t('entry.buys.size')" v-model="itemFilterParams.size" />
<VnSelect
:label="t('entry.buys.producer')"
:options="producersOptions"
hide-selected
option-label="name"
option-value="id"
v-model="itemFilterParams.producerFk"
/>
<VnSelect
:label="t('entry.buys.type')"
:options="ItemTypesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="itemFilterParams.typeFk"
/>
<VnSelect
:label="t('entry.buys.color')"
:options="InksOptions"
hide-selected
option-label="name"
option-value="id"
v-model="itemFilterParams.inkFk"
/>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn

View File

@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnSelect from 'components/common/VnSelect.vue';
import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue';
import axios from 'axios';
@ -145,48 +145,38 @@ const selectTravel = ({ id }) => {
</span>
<h1 class="title">{{ t('Filter travels') }}</h1>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('entry.basicData.agency')"
:options="agenciesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="travelFilterParams.agencyModeFk"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('entry.basicData.warehouseOut')"
:options="warehousesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="travelFilterParams.warehouseOutFk"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('entry.basicData.warehouseIn')"
:options="warehousesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="travelFilterParams.warehouseInFk"
/>
</div>
<div class="col">
<VnInputDate
:label="t('entry.basicData.shipped')"
v-model="travelFilterParams.shipped"
/>
</div>
<div class="col">
<VnInputDate
:label="t('entry.basicData.landed')"
v-model="travelFilterParams.landed"
/>
</div>
<VnSelect
:label="t('entry.basicData.agency')"
:options="agenciesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="travelFilterParams.agencyModeFk"
/>
<VnSelect
:label="t('entry.basicData.warehouseOut')"
:options="warehousesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="travelFilterParams.warehouseOutFk"
/>
<VnSelect
:label="t('entry.basicData.warehouseIn')"
:options="warehousesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="travelFilterParams.warehouseInFk"
/>
<VnInputDate
:label="t('entry.basicData.shipped')"
v-model="travelFilterParams.shipped"
/>
<VnInputDate
:label="t('entry.basicData.landed')"
v-model="travelFilterParams.landed"
/>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn

View File

@ -102,7 +102,7 @@ onMounted(async () => {
});
onBeforeRouteLeave((to, from, next) => {
if (hasChanges.value)
if (hasChanges.value && $props.observeFormChanges)
quasar.dialog({
component: VnConfirm,
componentProps: {
@ -156,9 +156,12 @@ const startFormWatcher = () => {
async function fetch() {
try {
const { data } = await axios.get($props.url, {
let { data } = await axios.get($props.url, {
params: { filter: JSON.stringify($props.filter) },
});
if (Array.isArray(data)) data = data[0] ?? {};
state.set($props.model, data);
originalData.value = data && JSON.parse(JSON.stringify(data));

View File

@ -6,7 +6,7 @@ import FormModel from 'components/FormModel.vue';
const emit = defineEmits(['onDataSaved']);
const $props = defineProps({
defineProps({
title: {
type: String,
default: '',
@ -76,25 +76,25 @@ defineExpose({
<p>{{ subtitle }}</p>
<slot name="form-inputs" :data="data" :validate="validate" />
<div class="q-mt-lg row justify-end">
<QBtn
:label="t('globals.save')"
:title="t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<QBtn
:label="t('globals.cancel')"
:title="t('globals.cancel')"
type="reset"
color="primary"
flat
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
v-close-popup
/>
<QBtn
:label="t('globals.save')"
:title="t('globals.save')"
type="submit"
color="primary"
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
/>
</div>
</template>
</FormModel>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
const emit = defineEmits(['onSubmit']);
const $props = defineProps({
defineProps({
title: {
type: String,
default: '',
@ -56,14 +56,6 @@ const closeForm = () => {
<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')"
@ -74,6 +66,14 @@ const closeForm = () => {
:loading="isLoading"
v-close-popup
/>
<QBtn
v-if="defaultSubmitButton"
:label="customSubmitButtonLabel || t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<slot name="customButtons" />
</div>
</QCard>

View File

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

View File

@ -2,7 +2,7 @@
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import FormModelPopup from './FormModelPopup.vue';
@ -50,16 +50,15 @@ const onDataSaved = (data) => {
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('Type the visible quantity')"
v-model.number="data.quantity"
/>
</div>
<QInput
:label="t('Type the visible quantity')"
v-model.number="data.quantity"
autofocus
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
<VnSelect
:label="t('Warehouse')"
v-model="data.warehouseFk"
:options="warehousesOptions"

View File

@ -5,7 +5,7 @@ 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 VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
@ -83,74 +83,66 @@ const transferInvoice = async () => {
>
<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>
<VnSelect
: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>
</VnSelect>
<VnSelect
:label="t('Rectificative type')"
:options="rectificativeTypeOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.cplusRectificationTypeFk"
:required="true"
/>
</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>
<VnSelect
: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>
</VnSelect>
<VnSelect
:label="t('Type')"
:options="invoiceCorrectionTypesOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.invoiceCorrectionTypeFk"
:required="true"
/>
</VnRow>
</template>
</FormPopup>

View File

@ -7,7 +7,7 @@ import axios from 'axios';
import { useState } from 'src/composables/useState';
import { useSession } from 'src/composables/useSession';
import { localeEquivalence } from 'src/i18n/index';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
@ -172,42 +172,57 @@ function copyUserToken() {
<QSeparator inset class="q-mx-lg" />
<div class="col q-gutter-xs q-pa-md">
<VnRow>
<VnSelectFilter
<VnSelect
:label="t('components.userPanel.localWarehouse')"
v-model="user.localWarehouseFk"
:options="warehousesData"
option-label="name"
option-value="id"
/>
<VnSelectFilter
:label="t('components.userPanel.localBank')"
input-debounce="0"
hide-selected
/>
<VnSelect
:label="t('components.userPanel.localBank')"
v-model="user.localBankFk"
:options="accountBankData"
option-label="bank"
option-value="id"
></VnSelectFilter>
input-debounce="0"
hide-selected
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>
{{ `${opt.id}: ${opt.bank}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow>
<VnRow>
<VnSelectFilter
<VnSelect
:label="t('components.userPanel.localCompany')"
hide-selected
v-model="user.companyFk"
v-model="user.localCompanyFk"
:options="companiesData"
option-label="code"
option-value="id"
input-debounce="0"
/>
<VnSelectFilter
<VnSelect
:label="t('components.userPanel.userWarehouse')"
hide-selected
v-model="user.warehouseFk"
:options="warehousesData"
option-label="name"
option-value="id"
input-debounce="0"
/>
</VnRow>
<VnRow>
<VnSelectFilter
<VnSelect
:label="t('components.userPanel.userCompany')"
hide-selected
v-model="user.companyFk"
@ -215,6 +230,8 @@ function copyUserToken() {
option-label="code"
option-value="id"
style="flex: 0"
dense
input-debounce="0"
/>
</VnRow>
</div>

View File

@ -0,0 +1,55 @@
<script setup>
import { ref, onMounted, useSlots } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
const slots = useSlots();
const hasContent = ref(false);
const rightPanel = ref(null);
onMounted(() => {
rightPanel.value = document.querySelector('#right-panel');
if (rightPanel.value.childNodes.length) hasContent.value = true;
// Check if there's content to display
const observer = new MutationObserver(() => {
hasContent.value = rightPanel.value.childNodes.length;
});
if (rightPanel.value)
observer.observe(rightPanel.value, {
subtree: true,
childList: true,
attributes: true,
});
if (!slots['right-panel'] && !hasContent.value) stateStore.rightDrawer = false;
});
const { t } = useI18n();
const stateStore = useStateStore();
</script>
<template>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
v-if="hasContent || $slots['right-panel']"
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<div id="right-panel"></div>
<slot v-if="!hasContent" name="right-panel" />
</QScrollArea>
</QDrawer>
</template>

View File

@ -29,10 +29,12 @@ async function confirm() {
const response = { address: address.value };
if (props.promise) {
isLoading.value = true;
const { address: _address, ...restData } = props.data;
try {
Object.assign(response, restData);
const dataCopy = JSON.parse(JSON.stringify({ ...props.data }));
delete dataCopy.address;
Object.assign(response, dataCopy);
await props.promise(response);
} finally {
isLoading.value = false;

View File

@ -0,0 +1,90 @@
<script setup>
import { onBeforeMount, computed, watchEffect } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
import VnSubToolbar from '../ui/VnSubToolbar.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import LeftMenu from 'components/LeftMenu.vue';
import RightMenu from 'components/common/RightMenu.vue';
const props = defineProps({
dataKey: { type: String, required: true },
baseUrl: { type: String, default: undefined },
customUrl: { type: String, default: undefined },
filter: { type: Object, default: () => {} },
descriptor: { type: Object, required: true },
filterPanel: { type: Object, default: undefined },
searchDataKey: { type: String, default: undefined },
searchUrl: { type: String, default: undefined },
searchbarLabel: { type: String, default: '' },
searchbarInfo: { type: String, default: '' },
});
const stateStore = useStateStore();
const route = useRoute();
const url = computed(() => {
if (props.baseUrl) return `${props.baseUrl}/${route.params.id}`;
return props.customUrl;
});
const arrayData = useArrayData(props.dataKey, {
url: url.value,
filter: props.filter,
});
onBeforeMount(async () => {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: false });
});
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${route.params.id}`;
await arrayData.fetch({ append: false });
}
});
}
watchEffect(() => {
if (Array.isArray(arrayData.store.data))
arrayData.store.data = arrayData.store.data[0];
});
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar" v-if="props.searchDataKey">
<slot name="searchbar">
<VnSearchbar
:data-key="props.searchDataKey"
:url="props.searchUrl"
:label="props.searchbarLabel"
:info="props.searchbarInfo"
/>
</slot>
</Teleport>
<slot v-else name="searchbar" />
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<RightMenu>
<template #right-panel v-if="props.filterPanel">
<component :is="props.filterPanel" :data-key="props.searchDataKey" />
</template>
</RightMenu>
</template>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]">
<RouterView />
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -6,7 +6,7 @@ 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 VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
@ -123,7 +123,7 @@ function addDefaultData(data) {
<div class="q-gutter-y-ms">
<VnRow>
<VnInput :label="t('globals.reference')" v-model="dms.reference" />
<VnSelectFilter
<VnSelect
:label="t('globals.company')"
v-model="dms.companyFk"
:options="companies"
@ -133,7 +133,7 @@ function addDefaultData(data) {
/>
</VnRow>
<VnRow>
<VnSelectFilter
<VnSelect
:label="t('globals.warehouse')"
v-model="dms.warehouseFk"
:options="warehouses"
@ -141,7 +141,7 @@ function addDefaultData(data) {
option-label="name"
input-debounce="0"
/>
<VnSelectFilter
<VnSelect
:label="t('globals.type')"
v-model="dms.dmsTypeFk"
:options="dmsTypes"

View File

@ -187,7 +187,7 @@ const columns = computed(() => [
downloadFile(
prop.row.id,
$props.downloadModel,
null,
undefined,
prop.row.download
),
},

View File

@ -52,6 +52,13 @@ const focus = () => {
defineExpose({
focus,
});
const inputRules = [
(val) => {
const { min } = vnInputRef.value.$attrs;
if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min });
},
];
</script>
<template>
@ -68,12 +75,15 @@ defineExpose({
:class="{ required: $attrs.required }"
@keyup.enter="onEnterPress()"
:clearable="false"
:rules="inputRules"
:lazy-rules="true"
hide-bottom-space
>
<template v-if="$slots.prepend" #prepend>
<slot name="prepend" />
</template>
<template #append>
<template #append v-if="!!$attrs.disabled">
<slot name="append" v-if="$slots.append" />
<QIcon
name="close"
@ -85,3 +95,9 @@ defineExpose({
</QInput>
</div>
</template>
<i18n>
en:
inputMin: Must be more than {value}
es:
inputMin: Debe ser mayor a {value}
</i18n>

View File

@ -15,6 +15,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
emitDateFormat: {
type: Boolean,
default: false,
},
});
const hover = ref(false);
@ -37,7 +41,10 @@ const value = computed({
return props.modelValue;
},
set(value) {
emit('update:modelValue', joinDateAndTime(value, time.value));
emit(
'update:modelValue',
props.emitDateFormat ? new Date(value) : joinDateAndTime(value, time.value)
);
},
});

View File

@ -12,7 +12,7 @@ import { useValidator } from 'src/composables/useValidator';
import VnAvatar from '../ui/VnAvatar.vue';
import VnJsonValue from '../common/VnJsonValue.vue';
import FetchData from '../FetchData.vue';
import VnSelectFilter from './VnSelectFilter.vue';
import VnSelect from './VnSelect.vue';
import VnUserLink from '../ui/VnUserLink.vue';
const stateStore = useStateStore();
@ -421,12 +421,13 @@ setLogTree();
>
<div class="timeline">
<div class="user-avatar">
<VnUserLink :worker-id="userLog.user.id">
<VnUserLink :worker-id="userLog?.user?.id">
<template #link>
<VnAvatar
:class="{ 'cursor-pointer': userLog.user.id }"
:worker-id="userLog.user.id"
:title="userLog.user.nickname"
:class="{ 'cursor-pointer': userLog?.user?.id }"
:worker-id="userLog?.user?.id"
:title="userLog?.user?.nickname"
:show-letter="!userLog?.user"
size="lg"
/>
</template>
@ -659,7 +660,7 @@ setLogTree();
</QInput>
</QItem>
<QItem>
<VnSelectFilter
<VnSelect
class="full-width"
:label="t('globals.entity')"
v-model="selectedFilters.changedModel"
@ -689,7 +690,7 @@ setLogTree();
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers && userRadio !== null">
<VnSelectFilter
<VnSelect
class="full-width"
:label="t('globals.user')"
v-model="userSelect"
@ -713,7 +714,7 @@ setLogTree();
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</VnSelect>
</QItemSection>
</QItem>
<QItem class="q-mt-sm">

View File

@ -69,7 +69,7 @@ const $props = defineProps({
});
const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const requiredFieldRule = (val) => val ?? t('globals.fieldRequired');
const { optionLabel, optionValue, options, modelValue } = toRefs($props);
const myOptions = ref([]);
@ -202,8 +202,10 @@ async function onScroll(scrollEv) {
hide-selected
fill-input
ref="vnSelectRef"
lazy-rules
:class="{ required: $attrs.required }"
:rules="$attrs.required ? [requiredFieldRule] : null"
virtual-scroll-slice-size="options.length"
>
<template v-if="isClearable" #append>
<QIcon

View File

@ -1,7 +1,7 @@
<script setup>
import { ref, computed } from 'vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import { useRole } from 'src/composables/useRole';
@ -52,7 +52,7 @@ const toggleForm = () => {
</script>
<template>
<VnSelectFilter v-model="value" :options="options" v-bind="$attrs">
<VnSelect v-model="value" :options="options" v-bind="$attrs">
<template v-if="isAllowedToCreate" #append>
<QIcon
@click.stop.prevent="toggleForm()"
@ -72,7 +72,7 @@ const toggleForm = () => {
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData" :key="slotName" />
</template>
</VnSelectFilter>
</VnSelect>
</template>
<style lang="scss" scoped>

View File

@ -199,9 +199,10 @@ es:
templates:
pendingPayment: 'Su pedido está pendiente de pago.
Por favor, entre en la página web y efectue el pago con tarjeta. Muchas gracias.'
minAmount: 'Es necesario un importe mínimo de 50 (Sin IVA) en su pedido
{ orderId } del día { shipped } para recibirlo sin portes adicionales.'
orderChanges: 'Pedido {orderId} día { shipped }: { changes }'
minAmount: 'Te recordamos que tu pedido {orderId} es inferior a 50.
Te recomendamos amplíes para no generar costes extra, provocarán un incremento de tu tarifa.
¡Un saludo!'
orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }'
en: Inglés
es: Español
fr: Francés
@ -215,11 +216,12 @@ fr:
Message: Message
messageTooltip: Les caractères spéciaux comme les accents comptent comme plusieurs
templates:
pendingPayment: 'Votre commande est en attente de paiement.
Veuillez vous connecter sur le site web et effectuer le paiement par carte. Merci beaucoup.'
minAmount: 'Un montant minimum de 50 (TVA non incluse) est requis pour votre commande
{ orderId } du { shipped } afin de la recevoir sans frais de port supplémentaires.'
orderChanges: 'Commande { orderId } du { shipped }: { changes }'
pendingPayment: 'Verdnatura : Commande en attente de règlement. Veuillez régler votre commande avant 9h.
Sinon elle sera décalée en fonction de vos jours de livraison . Merci'
minAmount: 'Verdnatura vous rappelle :
Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }.
Merci.'
orderChanges: 'Commande {orderId} livraison {landing} indisponible/s. Désolés pour le dérangement.'
en: Anglais
es: Espagnol
fr: Français
@ -236,8 +238,8 @@ pt:
pendingPayment: 'Seu pedido está pendente de pagamento.
Por favor, acesse o site e faça o pagamento com cartão. Muito obrigado.'
minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido
{ orderId } do dia { shipped } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } dia { shipped }: { changes }'
{ orderId } do dia { landing } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } com chegada dia { landing }: { changes }'
en: Inglês
es: Espanhol
fr: Francês

View File

@ -1,5 +1,5 @@
<script setup>
import { onBeforeMount, useSlots, watch, computed, ref } from 'vue';
import { onBeforeMount, watch, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData';
@ -38,7 +38,6 @@ const $props = defineProps({
});
const state = useState();
const slots = useSlots();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const arrayData = useArrayData($props.dataKey || $props.module, {
@ -47,7 +46,7 @@ const arrayData = useArrayData($props.dataKey || $props.module, {
skip: 0,
});
const { store } = arrayData;
const entity = computed(() =>Array.isArray( store.data) ? store.data[0] : store.data);
const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
const isLoading = ref(false);
defineExpose({
@ -55,14 +54,12 @@ defineExpose({
});
onBeforeMount(async () => {
await getData();
watch(
() => $props.url,
async () => await getData()
);
watch($props, async () => await getData());
});
async function getData() {
store.url = $props.url;
store.filter = $props.filter ?? {};
isLoading.value = true;
try {
const { data } = await arrayData.fetch({ append: false, updateRouter: false });
@ -117,7 +114,7 @@ const emit = defineEmits(['onFetch']);
icon="more_vert"
round
size="md"
:class="{ invisible: !slots.menu }"
:class="{ invisible: !$slots.menu }"
>
<QTooltip>
{{ t('components.cardDescriptor.moreOptions') }}

View File

@ -15,7 +15,7 @@ const props = defineProps({
default: null,
},
entityId: {
type: Number,
type: [Number, String],
default: null,
},
dataKey: {
@ -32,7 +32,7 @@ const arrayData = useArrayData(props.dataKey || route.meta.moduleName, {
skip: 0,
});
const { store } = arrayData;
const entity = computed(() => Array.isArray(store.data) ? store.data[0] : store.data);
const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
const isLoading = ref(false);
defineExpose({
@ -48,11 +48,19 @@ onBeforeMount(async () => {
async function fetch() {
store.url = props.url;
store.filter = props.filter ?? {};
isLoading.value = true;
const { data } = await arrayData.fetch({ append: false, updateRouter: false });
emit('onFetch', data);
emit('onFetch', Array.isArray(data) ? data[0] : data);
isLoading.value = false;
}
const showRedirectToSummaryIcon = computed(() => {
const routeExists = route.matched.some(
(route) => route.name === `${route.meta.moduleName}Summary`
);
return !isSummary.value && route.meta.moduleName && routeExists;
});
</script>
<template>
@ -63,7 +71,7 @@ async function fetch() {
<div class="summaryHeader bg-primary q-pa-sm text-weight-bolder">
<slot name="header-left">
<router-link
v-if="!isSummary && route.meta.moduleName"
v-if="showRedirectToSummaryIcon"
class="header link"
:to="{
name: `${route.meta.moduleName}Summary`,
@ -134,6 +142,9 @@ async function fetch() {
box-shadow: none;
.vn-label-value {
&.negative > .value span {
color: $alert;
}
display: flex;
flex-direction: row;
margin-top: 2px;

View File

@ -12,12 +12,23 @@ const $props = defineProps({
type: Boolean,
default: false,
},
viewCustomization: {
type: String,
default: '',
},
});
const $q = useQuasar();
// El objetivo de asignar las clases de personalización desde el wrapper es no tener conflictos entre vistas que usen el mismo componente
const viewCustomizationClasses = {
workerCalendar: 'worker-calendar-customizations',
};
const containerClasses = computed(() => {
const classes = ['main-container-background'];
if (viewCustomizationClasses[$props.viewCustomization])
classes.push(viewCustomizationClasses[$props.viewCustomization]);
if ($props.bordered) classes.push('--bordered');
if ($props.transparentBackground) classes.push('transparent-background');
else classes.push($q.dark.isActive ? '--dark' : '--light');
@ -33,6 +44,47 @@ const containerClasses = computed(() => {
</template>
<style lang="scss">
@import '../../css/quasar.variables.scss';
:root {
// Cambia los colores del día actual del calendario por los de salix
--calendar-border-current-dark: #84d0e2 2px solid;
--calendar-border-current: #84d0e2 2px solid;
--calendar-current-color-dark: #84d0e2;
// Colores de fondo del calendario en dark mode
--calendar-outside-background-dark: #222;
--calendar-background-dark: #222;
}
// Clases para modificar el color de fecha seleccionada en componente QCalendarMonth
.q-dark div .q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}
.q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}
.q-calendar-month__head--weekday {
// Transforma los nombres de los días de la semana a mayúsculas
text-transform: capitalize;
}
.transparent-background {
--calendar-background-dark: transparent;
--calendar-background: transparent;
--calendar-outside-background-dark: transparent;
}
.q-calendar__button {
&:hover {
background-color: var(--vn-accent-color);
cursor: pointer;
}
}
.main-container-background {
--calendar-current-background-dark: transparent;
@ -45,14 +97,64 @@ const containerClasses = computed(() => {
}
&.--bordered {
border: 1px solid black;
border: 1px solid #222;
}
}
.transparent-background {
--calendar-background-dark: transparent;
--calendar-background: transparent;
--calendar-outside-background-dark: transparent;
.worker-calendar-customizations {
.q-calendar__button {
width: 32px;
height: 32px;
font-size: 13px;
&:hover {
background-color: var(--vn-accent-color);
cursor: pointer;
}
}
.q-calendar-month__week--days > div:nth-child(6),
.q-calendar-month__week--days > div:nth-child(7) {
// Cambia el color de los días sábado y domingo
color: #777777;
}
.q-calendar-month__week--wrapper {
margin-bottom: 4px;
}
.q-calendar-month__workweek {
height: 32px;
display: flex;
justify-content: center;
}
.q-calendar__button--bordered {
color: $info !important;
}
.q-calendar-month__day--content {
position: absolute;
top: 1;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
.q-outside .calendar-event {
display: none;
}
.q-calendar-month__workweek,
.q-calendar-month__head--workweek,
.q-calendar-month__head--weekday.q-calendar__center.q-calendar__ellipsis {
text-transform: capitalize;
color: #777;
font-weight: bold;
font-size: 0.8rem;
text-align: center;
}
}
.nav-container {

View File

@ -1,28 +1,16 @@
<template>
<div class="q-pa-md">
<div class="row q-gutter-md q-mb-md">
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
</div>
<div class="row q-gutter-md">
<QSkeleton type="QBtn" />

View File

@ -3,46 +3,36 @@
<QSkeleton type="rect" square />
</div>
<div class="row q-pa-md q-col-gutter-md q-mb-md">
<div class="col">
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
</div>
<div class="col">
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
</div>
<div class="col">
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
</div>
<div class="col">
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
</div>
<div class="col">
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
</div>
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="rect" class="q-mb-md" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
<QSkeleton type="text" square />
</div>
</template>

View File

@ -1,45 +1,20 @@
<template>
<div class="q-pa-md w">
<div class="row q-gutter-md q-mb-md">
<div class="col-1">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
<div class="col">
<QSkeleton type="rect" square />
</div>
</div>
<div class="row q-gutter-md q-mb-md" v-for="n in 5" :key="n">
<div class="col-1">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<div class="col">
<QSkeleton type="QInput" square />
</div>
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
</div>
<div class="row q-gutter-md q-mb-md" v-for="n in 5" :key="n">
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
</div>
</div>
</template>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router';
import toDate from 'filters/toDate';
import useRedirect from 'src/composables/useRedirect';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
const { t } = useI18n();
@ -56,6 +56,7 @@ const arrayData = useArrayData(props.dataKey, {
const route = useRoute();
const store = arrayData.store;
const userParams = ref({});
const { navigate } = useRedirect();
onMounted(() => {
if (props.params) userParams.value = JSON.parse(JSON.stringify(props.params));
@ -79,6 +80,7 @@ watch(
const isLoading = ref(false);
async function search() {
store.filter.where = {};
isLoading.value = true;
const params = { ...userParams.value };
store.userParamsChanged = true;
@ -91,6 +93,7 @@ async function search() {
isLoading.value = false;
emit('search');
navigate(store.data, {});
}
async function reload() {
@ -101,6 +104,7 @@ async function reload() {
if (!props.showAll && !params.length) store.data = [];
isLoading.value = false;
emit('refresh');
navigate(store.data, {});
}
async function clearFilters() {
@ -146,7 +150,7 @@ const customTags = computed(() =>
async function remove(key) {
userParams.value[key] = null;
await search();
await arrayData.applyFilter({ params: userParams.value });
emit('remove', key);
}

View File

@ -1,21 +1,16 @@
<script setup>
import { useI18n } from 'vue-i18n';
const props = defineProps({
phoneNumber: { type: [String, Number], default: null },
});
const { t } = useI18n();
defineProps({ phoneNumber: { type: [String, Number], default: null } });
</script>
<template>
<QBtn
v-if="props.phoneNumber"
v-if="phoneNumber"
flat
round
icon="phone"
size="sm"
color="primary"
padding="none"
:href="`sip:${props.phoneNumber}`"
:href="`sip:${phoneNumber}`"
@click.stop
/>
</template>
<style scoped></style>

View File

@ -32,7 +32,7 @@ async function insert() {
<template>
<QCard class="q-pa-xs q-mb-xl full-width" v-if="$props.addNote">
<QCardSection horizontal>
<VnAvatar :descriptor="false" :worker-id="1" size="md" />
<VnAvatar :worker-id="currentUser.id" size="md" />
<div class="full-width row justify-between q-pa-xs">
<VnUserLink :name="t('New note')" :worker-id="currentUser.id" />
{{ t('globals.now') }}
@ -78,8 +78,8 @@ async function insert() {
<TransitionGroup name="list" tag="div" class="column items-center full-width">
<QCard
class="q-pa-xs q-mb-sm full-width"
v-for="note in rows"
:key="note.id"
v-for="(note, index) in rows"
:key="note.id ?? index"
>
<QCardSection horizontal>
<VnAvatar

View File

@ -42,6 +42,10 @@ const props = defineProps({
type: Object,
default: null,
},
keepOpts: {
type: Array,
default: () => [],
},
offset: {
type: Number,
default: 0,
@ -76,8 +80,8 @@ const arrayData = useArrayData(props.dataKey, {
order: props.order,
userParams: props.userParams,
exprBuilder: props.exprBuilder,
keepOpts: props.keepOpts,
});
const hasMoreData = ref();
const store = arrayData.store;
onMounted(() => {
@ -96,8 +100,10 @@ const addFilter = async (filter, params) => {
};
async function fetch() {
store.filter.skip = 0;
store.skip = 0;
await arrayData.fetch({ append: false });
if (!arrayData.hasMoreData.value) {
if (!store.hasMoreData) {
isLoading.value = false;
}
emit('onFetch', store.data);
@ -110,8 +116,8 @@ async function paginate() {
isLoading.value = true;
await arrayData.loadMore();
if (!arrayData.hasMoreData.value) {
if (store.userParamsChanged) arrayData.hasMoreData.value = true;
if (!store.hasMoreData) {
if (store.userParamsChanged) store.hasMoreData = true;
store.userParamsChanged = false;
endPagination();
return;
@ -132,9 +138,7 @@ function endPagination() {
emit('onPaginate');
}
async function onLoad(index, done) {
if (!store.data) {
return done();
}
if (!store.data) return done();
if (store.data.length === 0 || !props.url) return done(false);
@ -142,7 +146,7 @@ async function onLoad(index, done) {
await paginate();
let isDone = false;
if (store.userParamsChanged) isDone = !arrayData.hasMoreData.value;
if (store.userParamsChanged) isDone = !store.hasMoreData;
done(isDone);
}
@ -182,13 +186,12 @@ defineExpose({ fetch, addFilter });
</QCard>
</div>
</div>
<QInfiniteScroll
v-if="store.data"
@load="onLoad"
:offset="offset"
:disable="disableInfiniteScroll || !arrayData.hasMoreData"
class="full-width"
:disable="disableInfiniteScroll || !store.hasMoreData"
v-bind="$attrs"
>
<slot name="body" :rows="store.data"></slot>
@ -196,7 +199,10 @@ defineExpose({ fetch, addFilter });
<QSpinner color="orange" size="md" />
</div>
</QInfiniteScroll>
<div v-if="!isLoading && hasMoreData" class="w-full flex justify-center q-mt-md">
<div
v-if="!isLoading && store.hasMoreData"
class="w-full flex justify-center q-mt-md"
>
<QBtn color="primary" :label="t('Load more data')" @click="paginate()" />
</div>
</template>

View File

@ -1,11 +1,13 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
import VnInput from 'src/components/common/VnInput.vue';
import useRedirect from 'src/composables/useRedirect';
import { useI18n } from 'vue-i18n';
const quasar = useQuasar();
const { t } = useI18n();
const props = defineProps({
dataKey: {
@ -65,10 +67,10 @@ const props = defineProps({
},
});
const router = useRouter();
const arrayData = useArrayData(props.dataKey, { ...props });
const { store } = arrayData;
const searchText = ref('');
const { navigate } = useRedirect();
onMounted(() => {
const params = store.userParams;
@ -81,34 +83,20 @@ async function search() {
const staticParams = Object.entries(store.userParams).filter(
([key, value]) => value && (props.staticParams || []).includes(key)
);
store.skip = 0;
await arrayData.applyFilter({
params: {
...Object.fromEntries(staticParams),
search: searchText.value,
},
});
if (!props.redirect) return;
if (props.customRouteRedirectName)
return router.push({
name: props.customRouteRedirectName,
params: { id: searchText.value },
});
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const [, moduleName] = path.split('/');
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 });
navigate(store.data, {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
});
}
</script>
@ -117,7 +105,7 @@ async function search() {
<VnInput
id="searchbar"
v-model="searchText"
:placeholder="props.label"
:placeholder="t(props.label)"
dense
standout
autofocus
@ -131,19 +119,12 @@ async function search() {
/>
</template>
<template #append>
<QIcon
v-if="searchText !== ''"
name="close"
@click="searchText = ''"
class="cursor-pointer"
/>
<QIcon
v-if="props.info && $q.screen.gt.xs"
name="info"
class="cursor-info"
>
<QTooltip>{{ props.info }}</QTooltip>
<QTooltip>{{ t(props.info) }}</QTooltip>
</QIcon>
</template>
</VnInput>

View File

@ -1,11 +1,27 @@
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { onMounted, onUnmounted, ref } from 'vue';
import { useStateStore } from 'stores/useStateStore';
const stateStore = useStateStore();
const actions = ref(null);
const data = ref(null);
const opts = { subtree: true, childList: true, attributes: true };
const hasContent = ref(false);
onMounted(() => {
stateStore.toggleSubToolbar();
actions.value = document.querySelector('#st-actions');
data.value = document.querySelector('#st-data');
if (!actions.value && !data.value) return;
// Check if there's content to display
const observer = new MutationObserver(
() =>
(hasContent.value =
actions.value.childNodes.length + data.value.childNodes.length)
);
if (actions.value) observer.observe(actions.value, opts);
if (data.value) observer.observe(data.value, opts);
});
onUnmounted(() => {
@ -14,7 +30,10 @@ onUnmounted(() => {
</script>
<template>
<QToolbar class="bg-vn-section-color justify-end sticky">
<QToolbar
class="justify-end sticky"
v-show="hasContent || $slots['st-actions'] || $slots['st-data']"
>
<slot name="st-data">
<div id="st-data"></div>
</slot>

View File

@ -90,7 +90,7 @@ const onNodeCreated = async () => {
await fetchNodeLeaves(creationNodeSelectedId.value);
};
onMounted(async (n) => {
onMounted(async () => {
const tree = [...state.get('Tree'), 1];
const lastStateTree = state.get('TreeState');
if (tree) {

View File

@ -1,6 +1,5 @@
<script setup>
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import { useI18n } from 'vue-i18n';
const $props = defineProps({

View File

@ -0,0 +1,11 @@
export function getDateQBadgeColor(date) {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0);
let comparation = today - timeTicket;
if (comparation == 0) return 'warning';
if (comparation < 0) return 'negative';
}

View File

@ -9,12 +9,9 @@ const arrayDataStore = useArrayDataStore();
export function useArrayData(key, userOptions) {
if (!key) throw new Error('ArrayData: A key is required to use this composable');
if (!arrayDataStore.get(key)) {
arrayDataStore.set(key);
}
if (!arrayDataStore.get(key)) arrayDataStore.set(key);
const store = arrayDataStore.get(key);
const hasMoreData = ref(false);
const route = useRoute();
let canceller = null;
@ -22,6 +19,7 @@ export function useArrayData(key, userOptions) {
onMounted(() => {
setOptions();
store.skip = 0;
const query = route.query;
if (query.params) {
@ -29,9 +27,7 @@ export function useArrayData(key, userOptions) {
}
});
if (key && userOptions) {
setOptions();
}
if (key && userOptions) setOptions();
function setOptions() {
const allowedOptions = [
@ -51,7 +47,10 @@ export function useArrayData(key, userOptions) {
if (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) {
store[option] = userOptions[option];
const defaultOpts = userOptions[option];
store[option] = userOptions.keepOpts?.includes(option)
? Object.assign(defaultOpts, store[option])
: defaultOpts;
}
}
}
@ -95,8 +94,7 @@ export function useArrayData(key, userOptions) {
});
const { limit } = filter;
hasMoreData.value = limit && response.data.length >= limit;
store.hasMoreData = hasMoreData.value;
store.hasMoreData = limit && response.data.length >= limit;
if (append) {
if (!store.data) store.data = [];
@ -155,9 +153,10 @@ export function useArrayData(key, userOptions) {
delete store.userParams[param];
delete params[param];
if (store.filter?.where) {
delete store.filter.where[
Object.keys(exprBuilder ? exprBuilder(param) : param)[0]
];
const key = Object.keys(
exprBuilder && exprBuilder(param) ? exprBuilder(param) : param
);
if (key[0]) delete store.filter.where[key[0]];
if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where;
}
@ -168,7 +167,7 @@ export function useArrayData(key, userOptions) {
}
async function loadMore() {
if (!hasMoreData.value && !store.hasMoreData) return;
if (!store.hasMoreData) return;
store.skip = store.limit * page.value;
page.value += 1;
@ -210,7 +209,6 @@ export function useArrayData(key, userOptions) {
destroy,
loadMore,
store,
hasMoreData,
totalRows,
updateStateParams,
isLoading,

View File

@ -0,0 +1,25 @@
import { useRouter } from 'vue-router';
export default function useRedirect() {
const router = useRouter();
const navigate = (data, { customRouteRedirectName, searchText }) => {
if (customRouteRedirectName)
return router.push({
name: customRouteRedirectName,
params: { id: searchText },
});
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const to =
data.length === 1
? path.replace(/\/(list|:id)|-list/, `/${data[0].id}`)
: path.replace(/:id.*/, '');
router.push({ path: to });
};
return { navigate };
}

View File

@ -3,11 +3,14 @@ import { useRole } from './useRole';
import { useUserConfig } from './useUserConfig';
import axios from 'axios';
import useNotify from './useNotify';
import { useTokenConfig } from './useTokenConfig';
const TOKEN_MULTIMEDIA = 'tokenMultimedia';
const TOKEN = 'token';
export function useSession() {
const { notify } = useNotify();
let isCheckingToken = false;
let intervalId = null;
function getToken() {
const localToken = localStorage.getItem(TOKEN);
@ -22,10 +25,24 @@ export function useSession() {
return localTokenMultimedia || sessionTokenMultimedia || '';
}
function setToken(data) {
const storage = data.keepLogin ? localStorage : sessionStorage;
function setSession(data) {
let keepLogin = data.keepLogin;
const storage = keepLogin ? localStorage : sessionStorage;
storage.setItem(TOKEN, data.token);
storage.setItem(TOKEN_MULTIMEDIA, data.tokenMultimedia);
storage.setItem('created', data.created);
storage.setItem('ttl', data.ttl);
sessionStorage.setItem('keepLogin', keepLogin);
}
function keepLogin() {
return sessionStorage.getItem('keepLogin');
}
function setToken({ token, tokenMultimedia }) {
const storage = keepLogin() ? localStorage : sessionStorage;
storage.setItem(TOKEN, token);
storage.setItem(TOKEN_MULTIMEDIA, tokenMultimedia);
}
async function destroyToken(url, storage, key) {
if (storage.getItem(key)) {
@ -45,11 +62,15 @@ export function useSession() {
tokenMultimedia: 'Accounts/logout',
token: 'VnUsers/logout',
};
const storage = keepLogin() ? localStorage : sessionStorage;
for (const [key, url] of Object.entries(tokens)) {
await destroyToken(url, localStorage, key);
await destroyToken(url, sessionStorage, key);
await destroyToken(url, storage, key);
}
localStorage.clear();
sessionStorage.clear();
const { setUser } = useState();
setUser({
@ -59,22 +80,75 @@ export function useSession() {
lang: '',
darkMode: null,
});
stopRenewer();
}
async function login(token, tokenMultimedia, keepLogin) {
setToken({ token, tokenMultimedia, keepLogin });
async function login(data) {
setSession(data);
await useRole().fetch();
await useUserConfig().fetch();
await useTokenConfig().fetch();
startInterval();
}
function isLoggedIn() {
const localToken = localStorage.getItem(TOKEN);
const sessionToken = sessionStorage.getItem(TOKEN);
startInterval();
return !!(localToken || sessionToken);
}
function startInterval() {
stopRenewer();
const renewPeriod = +sessionStorage.getItem('renewPeriod');
if (!renewPeriod) return;
intervalId = setInterval(() => checkValidity(), renewPeriod * 1000);
}
function stopRenewer() {
clearInterval(intervalId);
}
async function renewToken() {
const _token = getToken();
const token = await axios.post('VnUsers/renewToken', {
headers: { Authorization: _token },
});
const _tokenMultimedia = getTokenMultimedia();
const tokenMultimedia = await axios.post('VnUsers/renewToken', {
headers: { Authorization: _tokenMultimedia },
});
setToken({ token: token.data.id, tokenMultimedia: tokenMultimedia.data.id });
}
async function checkValidity() {
const { getTokenConfig } = useState();
const tokenConfig = getTokenConfig() ?? sessionStorage.getItem('tokenConfig');
const storage = keepLogin() ? localStorage : sessionStorage;
const created = +storage.getItem('created');
const ttl = +storage.getItem('ttl');
if (isCheckingToken || !created) return;
isCheckingToken = true;
const renewPeriodInSeconds = Math.min(ttl, tokenConfig.value.renewPeriod) * 1000;
const maxDate = created + renewPeriodInSeconds;
const now = new Date().getTime();
if (isNaN(renewPeriodInSeconds) || now <= maxDate) {
return (isCheckingToken = false);
}
await renewToken();
isCheckingToken = false;
}
return {
getToken,
getTokenMultimedia,
@ -82,5 +156,8 @@ export function useSession() {
destroy,
login,
isLoggedIn,
checkValidity,
setSession,
renewToken,
};
}

View File

@ -13,34 +13,19 @@ const user = ref({
});
const roles = ref([]);
const tokenConfig = ref({});
const drawer = ref(true);
const headerMounted = ref(false);
export function useState() {
function getUser() {
return computed(() => {
return {
id: user.value.id,
name: user.value.name,
nickname: user.value.nickname,
lang: user.value.lang,
darkMode: user.value.darkMode,
companyFk: user.value.companyFk,
warehouseFk: user.value.warehouseFk,
};
return user.value;
});
}
function setUser(data) {
user.value = {
id: data.id,
name: data.name,
nickname: data.nickname,
lang: data.lang,
darkMode: data.darkMode,
companyFk: data.companyFk,
warehouseFk: data.warehouseFk,
};
user.value = data;
}
function getRoles() {
@ -52,6 +37,15 @@ export function useState() {
function setRoles(data) {
roles.value = data;
}
function getTokenConfig() {
return computed(() => {
return tokenConfig.value;
});
}
function setTokenConfig(data) {
tokenConfig.value = data;
}
function set(name, data) {
state.value[name] = ref(data);
@ -70,6 +64,8 @@ export function useState() {
setUser,
getRoles,
setRoles,
getTokenConfig,
setTokenConfig,
set,
get,
unset,

View File

@ -0,0 +1,28 @@
import axios from 'axios';
import { useState } from './useState';
import useNotify from './useNotify';
export function useTokenConfig() {
const state = useState();
const { notify } = useNotify();
async function fetch() {
try {
const { data } = await axios.get('AccessTokenConfigs/findOne', {
filter: { fields: ['renewInterval', 'renewPeriod'] },
});
if (!data) return;
state.setTokenConfig(data);
sessionStorage.setItem('renewPeriod', data.renewPeriod);
return data;
} catch (error) {
notify('errors.tokenConfig', 'negative');
console.error('Error fetching token config:', error);
}
}
return {
fetch,
state,
};
}

View File

@ -119,6 +119,11 @@ select:-webkit-autofill {
font-variation-settings: 'FILL' 1;
}
.fill-icon-on-hover:hover {
font-variation-settings: 'FILL' 1;
cursor: pointer;
}
.vn-table-separation-row {
height: 16px !important;
background-color: var(--vn-section-color) !important;
@ -136,6 +141,10 @@ select:-webkit-autofill {
background-color: var(--vn-section-color);
}
.tr-header {
color: var(--vn-label-color);
}
.q-chip,
.q-notification__message,
.q-notification__icon {
@ -170,13 +179,6 @@ input::-webkit-inner-spin-button {
-moz-appearance: none;
}
// Clases para modificar el color de fecha seleccionada en componente QCalendarMonth
.q-dark div .q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}
.q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
.q-scrollarea__content {
max-width: 100%;
}

View File

@ -91,3 +91,42 @@ export function toDateTimeFormat(date, showSeconds = false) {
second: showSeconds ? '2-digit' : undefined,
});
}
/**
* Converts seconds to a formatted string representing hours and minutes (hh:mm).
* @param {number} seconds - The number of seconds to convert.
* @param {boolean} includeHSuffix - Optional parameter indicating whether to include "h." after the hour.
* @returns {string} A string representing the time in the format "hh:mm" with optional "h." suffix.
*/
export function secondsToHoursMinutes(seconds, includeHSuffix = true) {
if (!seconds) return includeHSuffix ? '00:00 h.' : '00:00';
const hours = Math.floor(seconds / 3600);
const remainingMinutes = seconds % 3600;
const minutes = Math.floor(remainingMinutes / 60);
const formattedHours = hours < 10 ? '0' + hours : hours;
const formattedMinutes = minutes < 10 ? '0' + minutes : minutes;
// Append "h." if includeHSuffix is true
const suffix = includeHSuffix ? ' h.' : '';
// Return formatted string
return formattedHours + ':' + formattedMinutes + suffix;
}
export function getTimeDifferenceWithToday(date) {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
date = new Date(date);
date.setHours(0, 0, 0, 0);
return today - date;
}
export function isLower(date) {
return getTimeDifferenceWithToday(date) > 0;
}
export function isBigger(date) {
return getTimeDifferenceWithToday(date) < 0;
}

View File

@ -28,9 +28,11 @@ globals:
reset: Reset
close: Close
cancel: Cancel
clone: Clone
confirm: Confirm
assign: Assign
back: Back
downloadPdf: Download PDF
yes: 'Yes'
no: 'No'
noChanges: No changes to save
@ -83,21 +85,33 @@ globals:
selectFile: Select a file
copyClipboard: Copy on clipboard
salesPerson: SalesPerson
send: Send
code: Code
pageTitles:
summary: Summary
basicData: Basic data
log: Logs
parkingList: Parkings list
agencyList: Agencies list
agency: Agency
workCenters: Work centers
modes: Modes
zones: Zones
zonesList: Zones
deliveryList: Delivery days
upcomingList: Upcoming deliveries
created: Created
worker: Worker
now: Now
name: Name
new: New
errors:
statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred
statusBadGateway: It seems that the server has fall down
statusGatewayTimeout: Could not contact the server
userConfig: Error fetching user config
tokenConfig: Error fetching token config
writeRequest: The requested operation could not be completed
login:
title: Login
@ -258,6 +272,7 @@ customer:
tableVisibleColumns:
id: Identifier
name: Name
socialName: Social name
fi: Tax number
salesPersonFk: Salesperson
credit: Credit
@ -417,6 +432,7 @@ ticket:
boxing: Boxing
sms: Sms
notes: Notes
sale: Sale
list:
nickname: Nickname
state: State
@ -512,7 +528,7 @@ claim:
records: records
card:
claimId: Claim ID
assignedTo: Assigned
attendedBy: Attended by
created: Created
state: State
ticketId: Ticket ID
@ -550,6 +566,7 @@ claim:
responsible: Responsible
worker: Worker
redelivery: Redelivery
changeState: Change state
basicData:
customer: Customer
assignedTo: Assigned
@ -811,6 +828,8 @@ worker:
pbx: Private Branch Exchange
log: Log
calendar: Calendar
timeControl: Time control
locker: Locker
list:
name: Name
email: Email
@ -980,7 +999,7 @@ supplier:
billingData: Billing data
payMethod: Pay method
payDeadline: Pay deadline
payDay: Día de pago
payDay: Pay day
account: Account
fiscalData: Fiscal data
sageTaxType: Sage tax type
@ -1117,8 +1136,19 @@ item:
list: List
diary: Diary
tags: Tags
create: Create
buyRequest: Buy requests
fixedPrice: Fixed prices
wasteBreakdown: Waste breakdown
itemCreate: New item
barcode: Barcodes
tax: Tax
log: Log
botanical: Botanical
shelving: Shelving
itemTypeCreate: New item type
family: Item Type
lastEntries: Last entries
descriptor:
item: Item
buyer: Buyer
@ -1146,6 +1176,15 @@ item:
stemMultiplier: Multiplier
producer: Producer
landed: Landed
fixedPrice:
itemId: Item ID
groupingPrice: Grouping price
packingPrice: Packing price
hasMinPrice: Has min price
minPrice: Min price
started: Started
ended: Ended
warehouse: Warehouse
create:
name: Name
tag: Tag
@ -1153,8 +1192,77 @@ item:
type: Type
intrastat: Intrastat
origin: Origin
buyRequest:
ticketId: 'Ticket ID'
shipped: 'Shipped'
requester: 'Requester'
requested: 'Requested'
price: 'Price'
attender: 'Atender'
item: 'Item'
achieved: 'Achieved'
concept: 'Concept'
state: 'State'
summary:
basicData: 'Basic data'
otherData: 'Other data'
description: 'Description'
tax: 'Tax'
tags: 'Tags'
botanical: 'Botanical'
barcode: 'Barcode'
name: 'Nombre'
completeName: 'Nombre completo'
family: 'Familia'
size: 'Medida'
origin: 'Origen'
stems: 'Tallos'
multiplier: 'Multiplicador'
buyer: 'Comprador'
doPhoto: 'Do photo'
intrastatCode: 'Código intrastat'
intrastat: 'Intrastat'
ref: 'Referencia'
relevance: 'Relevancia'
weight: 'Peso (gramos)/tallo'
units: 'Unidades/caja'
expense: 'Gasto'
generic: 'Genérico'
recycledPlastic: 'Plástico reciclado'
nonRecycledPlastic: 'Plástico no reciclado'
minSalesQuantity: 'Cantidad mínima de venta'
genus: 'Genus'
specie: 'Specie'
item/itemType:
pageTitles:
itemType: Item type
basicData: Basic data
summary: Summary
monitor:
pageTitles:
monitors: Monitors
list: List
components:
topbar: {}
itemsFilterPanel:
typeFk: Type
tag: Tag
value: Value
# ItemFixedPriceFilter
buyerFk: Buyer
warehouseFk: Warehouse
started: From
ended: To
mine: For me
hasMinPrice: Minimum price
# LatestBuysFilter
salesPersonFk: Buyer
supplierFk: Supplier
from: From
to: To
active: Is active
visible: Is visible
floramondo: Is floramondo
userPanel:
copyToken: Token copied to clipboard
settings: Settings

View File

@ -28,6 +28,7 @@ globals:
reset: Restaurar
close: Cerrar
cancel: Cancelar
clone: Clonar
confirm: Confirmar
assign: Asignar
back: Volver
@ -58,6 +59,7 @@ globals:
amount: Importe
packages: Bultos
download: Descargar
downloadPdf: Descargar PDF
selectRows: 'Seleccionar las { numberRows } filas(s)'
allRows: 'Todo { numberRows } filas(s)'
markAll: Marcar todo
@ -83,21 +85,33 @@ globals:
selectFile: Seleccione un fichero
copyClipboard: Copiar en portapapeles
salesPerson: Comercial
send: Enviar
code: Código
pageTitles:
summary: Resumen
basicData: Datos básicos
log: Historial
parkingList: Listado de parkings
agencyList: Listado de agencias
agency: Agencia
workCenters: Centros de trabajo
modes: Modos
zones: Zonas
zonesList: Zonas
deliveryList: Días de entrega
upcomingList: Próximos repartos
created: Fecha creación
worker: Trabajador
now: Ahora
name: Nombre
new: Nuevo
errors:
statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor
statusBadGateway: Parece ser que el servidor ha caído
statusGatewayTimeout: No se ha podido contactar con el servidor
userConfig: Error al obtener configuración de usuario
tokenConfig: Error al obtener configuración de token
writeRequest: No se pudo completar la operación solicitada
login:
title: Inicio de sesión
@ -256,6 +270,7 @@ customer:
tableVisibleColumns:
id: Identificador
name: Nombre
socialName: Razón social
fi: NIF / CIF
salesPersonFk: Comercial
credit: Crédito
@ -415,6 +430,7 @@ ticket:
boxing: Encajado
sms: Sms
notes: Notas
sale: Lineas del pedido
list:
nickname: Alias
state: Estado
@ -510,7 +526,7 @@ claim:
records: registros
card:
claimId: ID reclamación
assignedTo: Asignada a
attendedBy: Atendida por
created: Creada
state: Estado
ticketId: ID ticket
@ -548,6 +564,7 @@ claim:
responsible: Responsable
worker: Trabajador
redelivery: Devolución
changeState: Cambiar estado
basicData:
customer: Cliente
assignedTo: Asignada a
@ -809,6 +826,8 @@ worker:
pbx: Centralita
log: Historial
calendar: Calendario
timeControl: Control de horario
locker: Taquilla
list:
name: Nombre
email: Email
@ -1116,8 +1135,19 @@ item:
list: Listado
diary: Histórico
tags: Etiquetas
fixedPrice: Precios fijados
buyRequest: Peticiones de compra
wasteBreakdown: Deglose de mermas
itemCreate: Nuevo artículo
basicData: 'Datos básicos'
tax: 'IVA'
botanical: 'Botánico'
barcode: 'Código de barras'
log: Historial
shelving: Carros
itemTypeCreate: Nueva familia
family: Familia
lastEntries: Últimas entradas
descriptor:
item: Artículo
buyer: Comprador
@ -1145,6 +1175,15 @@ item:
stemMultiplier: Multiplicador
producer: Productor
landed: F. entrega
fixedPrice:
itemId: ID Artículo
groupingPrice: Precio grouping
packingPrice: Precio packing
hasMinPrice: Tiene precio mínimo
minPrice: Precio min
started: Inicio
ended: Fin
warehouse: Almacén
create:
name: Nombre
tag: Etiqueta
@ -1152,8 +1191,83 @@ item:
type: Tipo
intrastat: Intrastat
origin: Origen
summary:
basicData: 'Datos básicos'
otherData: 'Otros datos'
description: 'Descripción'
tax: 'IVA'
tags: 'Etiquetas'
botanical: 'Botánico'
barcode: 'Código de barras'
name: 'Nombre'
completeName: 'Nombre completo'
family: 'Familia'
size: 'Medida'
origin: 'Origen'
stems: 'Tallos'
multiplier: 'Multiplicador'
buyer: 'Comprador'
doPhoto: 'Hacer foto'
intrastatCode: 'Código intrastat'
intrastat: 'Intrastat'
ref: 'Referencia'
relevance: 'Relevancia'
weight: 'Peso (gramos)/tallo'
units: 'Unidades/caja'
expense: 'Gasto'
generic: 'Genérico'
recycledPlastic: 'Plástico reciclado'
nonRecycledPlastic: 'Plástico no reciclado'
minSalesQuantity: 'Cantidad mínima de venta'
genus: 'Genus'
specie: 'Specie'
buyRequest:
ticketId: 'ID Ticket'
shipped: 'F. envío'
requester: 'Solicitante'
requested: 'Solicitado'
price: 'Precio'
attender: 'Comprador'
item: 'Artículo'
achieved: 'Conseguido'
concept: 'Concepto'
state: 'Estado'
item/itemType:
pageTitles:
itemType: Familia
basicData: Datos básicos
summary: Resumen
zone:
pageTitles:
zones: Zona
zonesList: Zonas
deliveryList: Días de entrega
upcomingList: Próximos repartos
monitor:
pageTitles:
monitors: Monitores
list: Listado
components:
topbar: {}
itemsFilterPanel:
typeFk: Tipo
tag: Etiqueta
value: Valor
# ItemFixedPriceFilter
buyerFk: Comprador
warehouseFk: Almacén
started: Desde
ended: Hasta
mine: Para mi
hasMinPrice: Precio mínimo
# LatestBuysFilter
salesPersonFk: Comprador
supplierFk: Proveedor
from: Desde
to: Hasta
active: Activo
visible: Visible
floramondo: Floramondo
userPanel:
copyToken: Token copiado al portapapeles
settings: Configuración

View File

@ -0,0 +1,79 @@
<script setup>
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const router = useRouter();
const stateStore = useStateStore();
function navigate(id) {
router.push({ path: `/agency/${id}` });
}
function exprBuilder(param, value) {
if (!value) return;
if (param !== 'search') return;
if (!isNaN(value)) return { id: value };
return { name: { like: `%${value}%` } };
}
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
:info="t('You can search by name')"
:label="t('Search agency')"
data-key="AgencyList"
url="Agencies"
/>
</Teleport>
</template>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
data-key="AgencyList"
url="Agencies"
order="name"
:expr-builder="exprBuilder"
auto-load
>
<template #body="{ rows }">
<CardList
:id="row.id"
:key="row.id"
:title="row.name"
@click="navigate(row.id)"
v-for="row of rows"
>
<template #list-items>
<QCheckbox
:label="t('isOwn')"
v-model="row.isOwn"
:disable="true"
/>
<QCheckbox
:label="t('isAnyVolumeAllowed')"
v-model="row.isAnyVolumeAllowed"
:disable="true"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage>
</template>
<i18n>
es:
isOwn: Tiene propietario
isAnyVolumeAllowed: Permite cualquier volumen
Search agency: Buscar agencia
You can search by name: Puedes buscar por nombre
en:
isOwn: Has owner
isAnyVolumeAllowed: Allows any volume
</i18n>

View File

@ -0,0 +1,52 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n();
const route = useRoute();
const routeId = route.params?.id || null;
const warehouses = ref([]);
</script>
<template>
<FetchData
url="warehouses"
sort-by="name"
@on-fetch="(data) => (warehouses = data)"
auto-load
/>
<FormModel :url="`Agencies/${routeId}`" model="agency" auto-load>
<template #form="{ data }">
<VnRow>
<VnInput v-model="data.name" :label="t('globals.name')" />
</VnRow>
<VnRow>
<VnSelect
v-model="data.warehouseFk"
option-value="id"
option-label="name"
:label="t('globals.warehouse')"
:options="warehouses"
use-input
input-debounce="0"
:is-clearable="false"
/>
</VnRow>
<VnRow>
<QCheckbox :label="t('agency.isOwn')" v-model="data.isOwn" />
</VnRow>
<VnRow>
<QCheckbox
:label="t('agency.isAnyVolumeAllowed')"
v-model="data.isAnyVolumeAllowed"
/>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,15 @@
<script setup>
import AgencyDescriptor from 'pages/Agency/Card/AgencyDescriptor.vue';
import VnCard from 'components/common/VnCard.vue';
</script>
<template>
<VnCard
data-key="Agency"
base-url="Agencies"
:descriptor="AgencyDescriptor"
search-data-key="AgencyList"
search-url="Agencies"
searchbar-label="agency.searchBar.label"
searchbar-info="agency.searchBar.info"
/>
</template>

View File

@ -0,0 +1,35 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'components/ui/VnLv.vue';
const props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const { t } = useI18n();
const route = useRoute();
const entityId = computed(() => props.id || route.params.id);
const { store } = useArrayData('Parking');
const card = computed(() => store.data);
</script>
<template>
<CardDescriptor
module="Agency"
data-key="Agency"
:url="`Agencies/${entityId}`"
:title="card?.name"
:subtitle="props.id"
>
<template #body="{ entity: agency }">
<VnLv :label="t('globals.name')" :value="agency.name" />
</template>
</CardDescriptor>
</template>

View File

@ -0,0 +1,6 @@
<script setup>
import VnLog from 'src/components/common/VnLog.vue';
</script>
<template>
<VnLog model="Agency" url="/AgencyLogs" />
</template>

View File

@ -0,0 +1,60 @@
<script setup>
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardList from 'src/components/ui/CardList.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnLv from 'src/components/ui/VnLv.vue';
const { t } = useI18n();
const route = useRoute();
const routeId = route.params.id;
</script>
<template>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
data-key="AgencyModes"
:url="`AgencyModes`"
:filter="{ where: { agencyFk: routeId } }"
order="name"
auto-load
>
<template #body="{ rows }">
<CardList
:id="row.id"
:key="row.id"
:title="row.name"
v-for="row of rows"
>
<template #list-items>
<VnLv
:label="t('globals.description')"
:value="row?.description"
/>
<VnLv
:label="t('deliveryMethod')"
:value="row?.deliveryMethodFk"
/>
<VnLv label="m3" :value="row?.m3" />
<VnLv :label="t('inflation')" :value="row?.inflation" />
<VnLv :label="t('globals.code')" :value="row?.code" />
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage>
</template>
<i18n>
es:
isOwn: Tiene propietario
isAnyVolumeAllowed: Permite cualquier volumen
Search agency: Buscar agencia
You can search by name: Puedes buscar por nombre
deliveryMethod: Método de entrega
inflation: Inflación
en:
isOwn: Has owner
isAnyVolumeAllowed: Allows any volume
</i18n>

View File

@ -0,0 +1,42 @@
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'components/ui/VnLv.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
const $props = defineProps({ id: { type: Number, default: 0 } });
const { t } = useI18n();
const entityId = computed(() => $props.id || useRoute().params.id);
</script>
<template>
<div class="q-pa-md">
<CardSummary :url="`Agencies/${entityId}`">
<template #header="{ entity: agency }">{{ agency.name }}</template>
<template #body="{ entity: agency }">
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<VnTitle
:url="`#/agency/${entityId}/basic-data`"
:text="t('globals.pageTitles.basicData')"
/>
</QCardSection>
<VnLv :label="t('globals.name')" :value="agency.name" />
<QCheckbox
:label="t('agency.isOwn')"
v-model="agency.isOwn"
:disable="true"
/>
<QCheckbox
:label="t('agency.isAnyVolumeAllowed')"
v-model="agency.isAnyVolumeAllowed"
:disable="true"
/>
</QCard>
</template>
</CardSummary>
</div>
</template>

View File

@ -0,0 +1,136 @@
<script setup>
import axios from 'axios';
import { useQuasar } from 'quasar';
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue';
import FormModelPopup from 'src/components/FormModelPopup.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n();
const route = useRoute();
const paginate = ref();
const dialog = ref();
const routeId = computed(() => route.params.id);
const quasar = useQuasar();
const initialData = computed(() => {
return {
agencyFk: routeId.value,
workCenterFk: null,
};
});
async function deleteWorCenter(id) {
try {
await axios.delete(`AgencyWorkCenters/${id}`).then(async () => {
quasar.notify({
message: t('agency.notification.removeItem'),
type: 'positive',
});
});
} catch (error) {
quasar.notify({
message: t('agency.notification.removeItemError'),
type: 'error',
});
}
paginate.value.fetch();
}
</script>
<template>
<div class="centerCard">
<FetchData
url="workCenters"
sort-by="name"
@on-fetch="(data) => (warehouses = data)"
auto-load
/>
<VnPaginate
ref="paginate"
data-key="AgencyWorkCenters"
url="AgencyWorkCenters"
:filter="{ where: { agencyFk: routeId } }"
order="id"
auto-load
>
<template #body="{ rows }">
<QCard
flat
bordered
:key="row.id"
v-for="row of rows"
class="card q-pa-md q-mb-sm"
>
<QItem>
<QItemSection side-left>
<VnLv :value="row?.workCenter?.name" icon="delete" />
</QItemSection>
<QItemSection side>
<QBtn
@click.stop="deleteWorCenter(row.id)"
icon="delete"
color="primary"
round
flat
/>
</QItemSection>
</QItem>
</QCard>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="dialog.show()" color="primary" fab icon="add">
<QDialog ref="dialog">
<FormModelPopup
:title="t('Add work center')"
url-create="AgencyWorkCenters"
model="AgencyWorkCenter"
:form-initial-data="initialData"
@on-data-saved="paginate.fetch()"
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
v-model="data.workCenterFk"
option-value="id"
option-label="name"
:label="t('workCenter')"
:options="warehouses"
use-input
input-debounce="0"
:is-clearable="false"
/>
</VnRow>
</template>
</FormModelPopup>
</QDialog>
</QBtn>
<QTooltip>
{{ t('globals.new') }}
</QTooltip>
</QPageSticky>
</template>
<style lang="scss" scoped>
.centerCard {
padding: 5%;
width: 100%;
max-width: 50%;
margin: 0 auto;
}
</style>
<i18n>
es:
workCenter removed successfully: Centro de trabajo eliminado correctamente
This workCenter is already assigned to this agency: Este workCenter ya está asignado a esta agencia
Add work center: Añadir centro de trabajo
workCenter: Centro de trabajo
</i18n>

View File

@ -0,0 +1,11 @@
agency:
isOwn: Own
isAnyVolumeAllowed: Any volume allowed
notification:
removeItemError: Error removing agency
removeItem: WorkCenter removed successfully
pageTitles:
agency: Agency
searchBar:
info: You can search by agency code
label: Search agency...

View File

@ -0,0 +1,12 @@
agency:
isOwn: Propio
isAnyVolumeAllowed: Cualquier volumen
removeItem: Agencia eliminada correctamente
notification:
removeItemError: Error al eliminar la agencia
removeItem: Centro de trabajo eliminado correctamente
pageTitles:
agency: Agencia
searchBar:
info: Puedes buscar por nombre o id
label: Buscar agencia...

View File

@ -2,22 +2,21 @@
import { ref, computed, onMounted } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import axios from 'axios';
import { useStateStore } from 'src/stores/useStateStore';
import { toDate, toPercentage, toCurrency } from 'filters/index';
import { tMobile } from 'src/composables/tMobile';
import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import { useArrayData } from 'composables/useArrayData';
const { t } = useI18n();
const quasar = useQuasar();
const route = useRoute();
const router = useRouter();
const stateStore = computed(() => useStateStore());
const claim = ref(null);
const claimRef = ref();
@ -38,10 +37,11 @@ const marker_labels = [
{ value: DEFAULT_MAX_RESPONSABILITY, label: t('claim.summary.person') },
];
const multiplicatorValue = ref();
const loading = ref(false);
const columns = computed(() => [
{
name: 'Id',
name: 'id',
label: t('Id item'),
field: (row) => row.itemFk,
},
@ -119,7 +119,7 @@ async function updateDestinations(claimDestinationFk) {
async function updateDestination(claimDestinationFk, row, options = {}) {
if (claimDestinationFk) {
await axios.post('Claims/updateClaimDestination', {
await post('Claims/updateClaimDestination', {
claimDestinationFk,
rows: Array.isArray(row) ? row : [row],
});
@ -128,7 +128,7 @@ async function updateDestination(claimDestinationFk, row, options = {}) {
}
async function regularizeClaim() {
await axios.post(`Claims/${claimId}/regularizeClaim`);
await post(`Claims/${claimId}/regularizeClaim`);
await claimRef.value.fetch();
await arrayData.fetch({ append: false });
quasar.notify({
@ -147,7 +147,7 @@ async function onUpdateGreugeAccept() {
const freightPickUpPrice =
(await axios.get(`GreugeConfigs/findOne`)).data.freightPickUpPrice *
multiplicatorValue.value;
await axios.post(`Greuges`, {
await post(`Greuges`, {
clientFk: claim.value.clientFk,
description: `${t('ClaimGreugeDescription')} ${claimId}`.toUpperCase(),
amount: freightPickUpPrice,
@ -166,14 +166,22 @@ async function save(data) {
}
async function importToNewRefundTicket() {
const query = `ClaimBeginnings/${claimId}/importToNewRefundTicket`;
await axios.post(query);
claimActionsForm.value.reload();
await post(`ClaimBeginnings/${claimId}/importToNewRefundTicket`);
await claimActionsForm.value.reload();
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
}
async function post(query, params) {
loading.value = true;
try {
await axios.post(query, params);
} finally {
loading.value = false;
}
}
</script>
<template>
<FetchData
@ -193,30 +201,7 @@ async function importToNewRefundTicket() {
auto-load
@on-fetch="(data) => (destinationTypes = data)"
/>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer
v-model="stateStore.rightDrawer"
side="right"
:width="300"
show-if-above
v-if="claim"
>
<Teleport to="#right-panel" v-if="stateStore.isHeaderMounted() && claim">
<QCard class="totalClaim q-my-md q-pa-sm no-box-shadow">
{{ `${t('Total claimed')}: ${toCurrency(totalClaimed)}` }}
</QCard>
@ -266,7 +251,7 @@ async function importToNewRefundTicket() {
v-model="multiplicatorValue"
/>
</QCard>
</QDrawer>
</Teleport>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()"> </Teleport>
<CrudModel
v-if="claim"
@ -280,6 +265,7 @@ async function importToNewRefundTicket() {
:default-save="false"
:default-reset="false"
@on-fetch="setData"
:limit="0"
auto-load
>
<template #body>
@ -292,6 +278,14 @@ async function importToNewRefundTicket() {
v-model:selected="selectedRows"
:grid="$q.screen.lt.md"
>
<template #body-cell-id="{ value }">
<QTd align="center">
<span class="link">
{{ value }}
<ItemDescriptorProxy :id="value" />
</span>
</QTd>
</template>
<template #body-cell-ticket="{ value }">
<QTd align="center">
<span class="link">
@ -302,7 +296,7 @@ async function importToNewRefundTicket() {
</template>
<template #body-cell-destination="{ row }">
<QTd>
<VnSelectFilter
<VnSelect
v-model="row.claimDestinationFk"
:options="destinationTypes"
option-label="description"
@ -344,7 +338,7 @@ async function importToNewRefundTicket() {
</QItemSection>
<QItemSection side>
<QItemLabel v-if="column.name === 'destination'">
<VnSelectFilter
<VnSelect
v-model="props.row.claimDestinationFk"
:options="destinationTypes"
option-label="description"
@ -383,6 +377,7 @@ async function importToNewRefundTicket() {
icon="check"
@click="regularizeClaim"
:disable="claim.claimStateFk == resolvedStateId"
:loading="loading"
/>
<QBtn
@ -394,6 +389,7 @@ async function importToNewRefundTicket() {
:title="t('Change destination')"
icon="swap_horiz"
@click="dialogDestination = !dialogDestination"
:loading="loading"
/>
<QBtn
color="primary"
@ -404,6 +400,7 @@ async function importToNewRefundTicket() {
icon="Upload"
@click="importToNewRefundTicket"
:disable="claim.claimStateFk == resolvedStateId"
:loading="loading"
/>
</template>
</CrudModel>
@ -418,7 +415,7 @@ async function importToNewRefundTicket() {
</QItem>
</QCardSection>
<QItemSection>
<VnSelectFilter
<VnSelect
class="q-pa-sm"
v-model="claimDestinationFk"
:options="destinationTypes"

View File

@ -2,7 +2,7 @@
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
@ -37,23 +37,19 @@ const claimFilter = {
],
};
const workers = ref([]);
const workersCopy = ref([]);
const claimStates = ref([]);
const claimStatesCopy = ref([]);
const optionsList = ref([]);
function setWorkers(data) {
workers.value = data;
workersCopy.value = data;
}
const workersOptions = ref([]);
function setClaimStates(data) {
claimStates.value = data;
claimStatesCopy.value = data;
}
let optionsList;
async function getEnumValues() {
optionsList = [{ id: null, description: t('claim.basicData.null') }];
optionsList.value = [{ id: null, description: t('claim.basicData.null') }];
const { data } = await axios.get(`Applications/get-enum-values`, {
params: {
schema: 'vn',
@ -62,29 +58,11 @@ async function getEnumValues() {
},
});
for (let value of data)
optionsList.push({ id: value, description: t(`claim.basicData.${value}`) });
optionsList.value.push({ id: value, description: t(`claim.basicData.${value}`) });
}
getEnumValues();
const workerFilter = {
options: workers,
filterFn: (options, value) => {
const search = value.toLowerCase();
if (value === '') return workersCopy.value;
return options.value.filter((row) => {
const id = row.id;
const name = row.name.toLowerCase();
const idMatches = id == search;
const nameMatches = name.indexOf(search) > -1;
return idMatches || nameMatches;
});
},
};
const statesFilter = {
options: claimStates,
filterFn: (options, value) => {
@ -104,7 +82,7 @@ const statesFilter = {
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="setWorkers"
@on-fetch="(data) => (workersOptions = data)"
auto-load
/>
<FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load />
@ -117,86 +95,71 @@ const statesFilter = {
>
<template #form="{ data, validate, filter }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.client.name"
:label="t('claim.basicData.customer')"
disable
/>
</div>
<div class="col">
<VnInputDate
v-model="data.created"
:label="t('claim.basicData.created')"
/>
</div>
<VnInput
v-model="data.client.name"
:label="t('claim.basicData.customer')"
disable
/>
<VnInputDate
v-model="data.created"
:label="t('claim.basicData.created')"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QSelect
v-model="data.workerFk"
:options="workers"
option-value="id"
option-label="name"
emit-value
:label="t('claim.basicData.assignedTo')"
map-options
use-input
@filter="(value, update) => filter(value, update, workerFilter)"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
<template #before>
<QAvatar color="orange">
<QImg
v-if="data.workerFk"
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`"
spinner-color="white"
/>
</QAvatar>
</template>
</QSelect>
</div>
<div class="col">
<QSelect
v-model="data.claimStateFk"
:options="claimStates"
option-value="id"
option-label="description"
emit-value
:label="t('claim.basicData.state')"
map-options
use-input
@filter="(value, update) => filter(value, update, statesFilter)"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
</QSelect>
</div>
<VnSelect
:label="t('claim.basicData.assignedTo')"
v-model="data.workerFk"
:options="workersOptions"
option-value="id"
option-label="name"
emit-value
auto-load
:rules="validate('claim.claimStateFk')"
>
<template #before>
<QAvatar color="orange">
<QImg
v-if="data.workerFk"
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`"
spinner-color="white"
/>
</QAvatar>
</template>
</VnSelect>
<QSelect
v-model="data.claimStateFk"
:options="claimStates"
option-value="id"
option-label="description"
emit-value
:label="t('claim.basicData.state')"
map-options
use-input
@filter="(value, update) => filter(value, update, statesFilter)"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
</QSelect>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
v-model.number="data.packages"
:label="t('globals.packages')"
:rules="validate('claim.packages')"
type="number"
/>
</div>
<div class="col">
<QSelect
v-model="data.pickup"
:options="optionsList"
option-value="id"
option-label="description"
emit-value
:label="t('claim.basicData.pickup')"
map-options
use-input
:input-debounce="0"
>
</QSelect>
</div>
<QInput
v-model.number="data.packages"
:label="t('globals.packages')"
:rules="validate('claim.packages')"
type="number"
/>
<QSelect
v-model="data.pickup"
:options="optionsList"
option-value="id"
option-label="description"
emit-value
:label="t('claim.basicData.pickup')"
map-options
use-input
:input-debounce="0"
>
</QSelect>
</VnRow>
</template>
</FormModel>

View File

@ -1,46 +1,17 @@
<script setup>
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n';
import VnCard from 'components/common/VnCard.vue';
import ClaimDescriptor from './ClaimDescriptor.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
import ClaimFilter from '../ClaimFilter.vue';
</script>
<template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="ClaimList"
url="Claims/filter"
:label="t('Search claim')"
:info="t('You can search by claim id or customer name')"
/>
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<ClaimDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
<VnCard
data-key="Claim"
base-url="Claims"
:descriptor="ClaimDescriptor"
:filter-panel="ClaimFilter"
search-data-key="ClaimList"
search-url="Claims/filter"
searchbar-label="Search claim"
searchbar-info="You can search by claim id or customer name"
/>
</template>
<i18n>
es:
Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
Details: Detalles
Notes: Notas
Action: Acción
</i18n>

View File

@ -11,6 +11,7 @@ import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { getUrl } from 'src/composables/getUrl';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
const $props = defineProps({
id: {
@ -73,8 +74,9 @@ const filter = {
const STATE_COLOR = {
pending: 'warning',
managed: 'info',
incomplete: 'info',
resolved: 'positive',
canceled: 'negative',
};
function stateColor(code) {
return STATE_COLOR[code];
@ -127,17 +129,24 @@ onMounted(async () => {
</VnLv>
<VnLv
v-if="entity.worker"
:label="t('claim.card.assignedTo')"
:label="t('claim.card.attendedBy')"
:value="entity.worker.user.name"
>
<template #value>
<VnUserLink
:name="entity.worker.user.name"
:name="entity.worker.user.nickname"
:worker-id="entity.worker.id"
/>
</template>
</VnLv>
<VnLv :label="t('claim.card.zone')" :value="entity.ticket?.zone?.name" />
<VnLv :label="t('claim.card.zone')">
<template #value>
<span class="link">
{{ entity.ticket?.zone?.name }}
<ZoneDescriptorProxy :id="entity.ticket?.zone?.id" />
</span>
</template>
</VnLv>
<VnLv
:label="t('claim.card.province')"
:value="entity.ticket?.address?.province?.name"

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import CrudModel from 'components/CrudModel.vue';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnSelect from 'components/common/VnSelect.vue';
import { tMobile } from 'composables/tMobile';
const route = useRoute();
@ -161,7 +161,7 @@ const columns = computed(() => [
auto-width
@keyup.ctrl.enter.stop="claimDevelopmentForm.saveChanges()"
>
<VnSelectFilter
<VnSelect
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
@ -181,7 +181,7 @@ const columns = computed(() => [
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</VnSelect>
</QTd>
</template>
<template #item="props">
@ -198,7 +198,7 @@ const columns = computed(() => [
<QList dense>
<QItem v-for="col in props.cols" :key="col.name">
<QItemSection>
<VnSelectFilter
<VnSelect
:label="col.label"
v-model="props.row[col.model]"
:options="col.options"

View File

@ -45,20 +45,25 @@ async function onFetchClaim(data) {
const amount = ref();
const amountClaimed = ref();
async function onFetch(rows) {
async function onFetch(rows, newRows) {
if (newRows) rows = newRows;
amount.value = 0;
amountClaimed.value = 0;
if (!rows || !rows.length) return;
amount.value = rows.reduce(
(accumulator, { sale }) => accumulator + sale.price * sale.quantity,
0
);
for (const row of rows) {
const { sale } = row;
amount.value = amount.value + totalRow(sale);
const price = row.quantity * sale.price;
const discount = (sale.discount * price) / 100;
amountClaimed.value = amountClaimed.value + (price - discount);
}
}
amountClaimed.value = rows.reduce(
(accumulator, line) => accumulator + line.sale.price * line.quantity,
0
);
function totalRow({ price, quantity, discount }) {
const amount = price * quantity;
const appliedDiscount = (discount * amount) / 100;
return amount - appliedDiscount;
}
const columns = computed(() => [
@ -102,12 +107,7 @@ const columns = computed(() => [
{
name: 'total',
label: t('Total'),
field: ({ sale }) => {
const amount = sale.price * sale.quantity;
const appliedDiscount = (sale.discount * amount) / 100;
return amount - appliedDiscount;
},
field: ({ sale }) => totalRow(sale),
format: (value) => toCurrency(value),
sortable: true,
},
@ -129,6 +129,7 @@ async function updateDiscount({ saleFk, discount, canceller }) {
await axios.post(query, body, {
signal: canceller.signal,
});
await claimLinesForm.value.reload();
}
function onUpdateDiscount(response) {
@ -151,8 +152,11 @@ function showImportDialog() {
.onOk(() => claimLinesForm.value.reload());
}
function saveWhenHasChanges() {
claimLinesForm.value.getChanges().updates && claimLinesForm.value.onSubmit();
async function saveWhenHasChanges() {
if (claimLinesForm.value.getChanges().updates) {
await claimLinesForm.value.onSubmit();
await claimLinesForm.value.reload();
}
}
</script>
<template>
@ -188,7 +192,6 @@ function saveWhenHasChanges() {
save-url="ClaimBeginnings/crud"
:filter="linesFilter"
@on-fetch="onFetch"
@save-changes="onFetch"
v-model:selected="selected"
:default-save="false"
:default-reset="false"

View File

@ -35,6 +35,7 @@ const columns = computed(() => [
label: t('Quantity'),
field: (row) => row.quantity,
sortable: true,
default: 0,
},
{
name: 'description',
@ -75,7 +76,6 @@ async function importLines() {
const body = sales.map((row) => ({
claimFk: route.params.id,
saleFk: row.saleFk,
quantity: row.quantity,
}));
canceller = new AbortController();

View File

@ -16,7 +16,7 @@ const claimId = computed(() => $props.id || route.params.id);
const claimFilter = {
where: { claimFk: claimId.value },
fields: ['created', 'workerFk', 'text'],
fields: ['id', 'created', 'workerFk', 'text'],
include: {
relation: 'worker',
scope: {

View File

@ -1,6 +1,6 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toDate, toCurrency } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue';
@ -13,8 +13,11 @@ import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import axios from 'axios';
import dashIfEmpty from 'src/filters/dashIfEmpty';
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
@ -27,7 +30,7 @@ const $props = defineProps({
});
const entityId = computed(() => $props.id || route.params.id);
const ClaimStates = ref([]);
const claimUrl = ref();
const salixUrl = ref();
const claimDmsRef = ref();
@ -99,8 +102,9 @@ const detailsColumns = ref([
const STATE_COLOR = {
pending: 'warning',
managed: 'info',
incomplete: 'info',
resolved: 'positive',
canceled: 'negative',
};
function stateColor(code) {
return STATE_COLOR[code];
@ -162,6 +166,10 @@ function openDialog(dmsId) {
multimediaSlide.value = dmsId;
multimediaDialog.value = true;
}
async function changeState(value) {
await axios.patch(`Claims/updateClaim/${entityId.value}`, { claimStateFk: value });
router.go(route.fullPath);
}
</script>
<template>
@ -171,6 +179,7 @@ function openDialog(dmsId) {
@on-fetch="(data) => setClaimDms(data)"
ref="claimDmsRef"
/>
<FetchData url="ClaimStates" @on-fetch="(data) => (ClaimStates = data)" auto-load />
<CardSummary
ref="summary"
:url="`Claims/${entityId}/getSummary`"
@ -180,6 +189,36 @@ function openDialog(dmsId) {
<template #header="{ entity: { claim } }">
{{ claim.id }} - {{ claim.client.name }} ({{ claim.client.id }})
</template>
<template #header-right>
<QBtnDropdown
side
top
color="black"
text-color="white"
:label="t('ticket.summary.changeState')"
>
<QList>
<QVirtualScroll
style="max-height: 300px"
:items="ClaimStates"
separator
v-slot="{ item, index }"
>
<QItem
:key="index"
dense
clickable
v-close-popup
@click="changeState(item.id)"
>
<QItemSection>
<QItemLabel>{{ item.description }}</QItemLabel>
</QItemSection>
</QItem>
</QVirtualScroll>
</QList>
</QBtnDropdown>
</template>
<template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one">
<VnTitle
@ -223,7 +262,7 @@ function openDialog(dmsId) {
</VnLv>
<VnLv
:label="t('claim.basicData.pickup')"
:value="t(`claim.basicData.${claim.pickup}`)"
:value="`${dashIfEmpty(claim.pickup)}`"
/>
</QCard>
<QCard class="vn-three">
@ -447,4 +486,8 @@ function openDialog(dmsId) {
.zindex {
z-index: 1;
}
.change-state {
width: 10%;
}
</style>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
@ -64,7 +64,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelectFilter
<VnSelect
:label="t('Salesperson')"
v-model="params.salesPersonFk"
@update:model-value="searchFn()"
@ -87,7 +87,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelectFilter
<VnSelect
:label="t('Attender')"
v-model="params.attenderFk"
@update:model-value="searchFn()"
@ -110,7 +110,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelectFilter
<VnSelect
:label="t('Responsible')"
v-model="params.claimResponsibleFk"
@update:model-value="searchFn()"
@ -133,7 +133,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="states">
<VnSelectFilter
<VnSelect
:label="t('State')"
v-model="params.claimStateFk"
@update:model-value="searchFn()"
@ -149,6 +149,15 @@ const states = ref();
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
v-model="params.myTeam"
:label="t('myTeam')"
toggle-indeterminate
/>
</QItemSection>
</QItem>
<QSeparator />
<QExpansionItem :label="t('More options')" expand-separator>
<!-- <QItem>
@ -192,6 +201,7 @@ en:
claimResponsibleFk: Responsible
claimStateFk: State
created: Created
myTeam: My team
es:
params:
search: Contiene
@ -211,4 +221,5 @@ es:
Item: Artículo
Created: Creada
More options: Más opciones
myTeam: Mi equipo
</i18n>

View File

@ -12,7 +12,6 @@ import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorP
import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ClaimSummary from './Card/ClaimSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { getUrl } from 'src/composables/getUrl';
const stateStore = useStateStore();
const router = useRouter();

View File

@ -0,0 +1,2 @@
Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente

View File

@ -202,9 +202,9 @@ const toCustomerAddressEdit = (addressId) => {
<div v-if="item.observations.length">
<div
:key="index"
:key="obIndex"
class="flex q-mb-sm"
v-for="(observation, index) in item.observations"
v-for="(observation, obIndex) in item.observations"
>
<div class="text-weight-bold q-mr-sm">
{{ observation.observationType.description }}:

View File

@ -11,14 +11,18 @@ import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore';
import { useValidator } from 'src/composables/useValidator';
import { usePrintService } from 'src/composables/usePrintService';
import { useSession } from 'src/composables/useSession';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CustomerNewPayment from 'src/pages/Customer/components/CustomerNewPayment.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
const session = useSession();
const tokenMultimedia = session.getTokenMultimedia();
const { sendEmail } = usePrintService();
const { t } = useI18n();
@ -188,6 +192,11 @@ const saveFieldValue = async (row) => {
const sendEmailAction = () => {
sendEmail(`Suppliers/${route.params.id}/campaign-metrics-email`);
};
const showBalancePdf = (balance) => {
const url = `api/InvoiceOuts/${balance.id}/download?access_token=${tokenMultimedia}`;
window.open(url, '_blank');
};
</script>
<template>
@ -257,17 +266,28 @@ const sendEmailAction = () => {
<QTd align="center">
<QIcon
@click.stop="showDialog = true"
class="q-ml-md"
class="q-ml-md fill-icon"
color="primary"
name="outgoing_mail"
size="sm"
style="font-variation-settings: 'FILL' 1"
v-if="row.isCompensation"
>
<QTooltip>
{{ t('Send compensation') }}
</QTooltip>
</QIcon>
<QIcon
@click="showBalancePdf(row)"
class="q-ml-md fill-icon"
color="primary"
name="cloud_download"
size="sm"
v-if="row.hasPdf"
>
<QTooltip>
{{ t('globals.downloadPdf') }}
</QTooltip>
</QIcon>
<QDialog v-model="showDialog">
<QCard class="q-pa-sm">
@ -316,7 +336,7 @@ const sendEmailAction = () => {
<QDrawer :width="256" show-if-above side="right" v-model="stateStore.rightDrawer">
<div class="q-mt-xl q-px-md">
<VnSelectFilter
<VnSelect
:label="t('Company')"
:options="companiesOptions"
@update:model-value="updateCompanyId($event)"

View File

@ -60,139 +60,123 @@ const filterOptions = {
<FormModel :url="`Clients/${route.params.id}`" auto-load model="customer">
<template #form="{ data, validate, filter }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Comercial name')"
:rules="validate('client.socialName')"
autofocus
clearable
v-model="data.name"
/>
</div>
<div class="col">
<QSelect
:input-debounce="0"
:label="t('customer.basicData.businessType')"
:options="businessTypes"
:rules="validate('client.businessTypeFk')"
emit-value
map-options
option-label="description"
option-value="code"
v-model="data.businessTypeFk"
/>
</div>
<VnInput
:label="t('Comercial name')"
:rules="validate('client.socialName')"
autofocus
clearable
v-model="data.name"
/>
<QSelect
:input-debounce="0"
:label="t('customer.basicData.businessType')"
:options="businessTypes"
:rules="validate('client.businessTypeFk')"
emit-value
map-options
option-label="description"
option-value="code"
v-model="data.businessTypeFk"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('customer.basicData.contact')"
:rules="validate('client.contact')"
clearable
v-model="data.contact"
/>
</div>
<div class="col">
<VnInput
:label="t('customer.basicData.email')"
:rules="validate('client.email')"
clearable
type="email"
v-model="data.email"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</VnInput>
</div>
<VnInput
:label="t('customer.basicData.contact')"
:rules="validate('client.contact')"
clearable
v-model="data.contact"
/>
<VnInput
:label="t('customer.basicData.email')"
:rules="validate('client.email')"
clearable
type="email"
v-model="data.email"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</VnInput>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('customer.basicData.phone')"
:rules="validate('client.phone')"
clearable
v-model="data.phone"
/>
</div>
<div class="col">
<VnInput
:label="t('customer.basicData.mobile')"
:rules="validate('client.mobile')"
clearable
v-model="data.mobile"
/>
</div>
<VnInput
:label="t('customer.basicData.phone')"
:rules="validate('client.phone')"
clearable
v-model="data.phone"
/>
<VnInput
:label="t('customer.basicData.mobile')"
:rules="validate('client.mobile')"
clearable
v-model="data.mobile"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<!-- @filter="(value, update) => filter(value, update, filterOptions)" -->
<VnSelectFilter
:input-debounce="0"
:label="t('customer.basicData.salesPerson')"
:rules="validate('client.salesPersonFk')"
emit-value
map-options
option-label="name"
option-value="id"
use-input
v-model="data.salesPersonFk"
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
>
<template #prepend>
<QAvatar color="orange">
<QImg
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
v-if="data.salesPersonFk"
/>
</QAvatar>
</template>
</VnSelectFilter>
</div>
<div class="col">
<QSelect
:input-debounce="0"
:label="t('customer.basicData.contactChannel')"
:options="contactChannels"
:rules="validate('client.contactChannelFk')"
emit-value
map-options
option-label="name"
option-value="id"
v-model="data.contactChannelFk"
/>
</div>
<!-- @filter="(value, update) => filter(value, update, filterOptions)" -->
<QSelect
:input-debounce="0"
:label="t('customer.basicData.salesPerson')"
:options="workers"
:rules="validate('client.salesPersonFk')"
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
emit-value
map-options
option-label="name"
option-value="id"
use-input
v-model="data.salesPersonFk"
>
<template #prepend>
<QAvatar color="orange">
<QImg
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
v-if="data.salesPersonFk"
/>
</QAvatar>
</template>
</QSelect>
<QSelect
v-model="data.contactChannelFk"
:options="contactChannels"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.contactChannel')"
map-options
:rules="validate('client.contactChannelFk')"
:input-debounce="0"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QSelect
:input-debounce="0"
:label="t('customer.basicData.previousClient')"
:options="clients"
:rules="validate('client.transferorFk')"
emit-value
map-options
option-label="name"
option-value="id"
v-model="data.transferorFk"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
t(
'In case of a company succession, specify the grantor company'
)
}}</QTooltip>
</QIcon>
</template>
</QSelect>
</div>
<QSelect
:input-debounce="0"
:label="t('customer.basicData.previousClient')"
:options="clients"
:rules="validate('client.transferorFk')"
emit-value
map-options
option-label="name"
option-value="id"
v-model="data.transferorFk"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
t(
'In case of a company succession, specify the grantor company'
)
}}</QTooltip>
</QIcon>
</template>
</QSelect>
</VnRow>
</template>
</FormModel>

View File

@ -7,7 +7,7 @@ import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue';
@ -48,74 +48,56 @@ const getBankEntities = (data, formData) => {
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Billing data')"
:options="payMethods"
hide-selected
option-label="name"
option-value="id"
v-model="data.payMethod"
/>
</div>
<div class="col">
<VnInput :label="t('Due day')" clearable v-model="data.dueDay" />
</div>
<VnSelect
:label="t('Billing data')"
:options="payMethods"
hide-selected
option-label="name"
option-value="id"
v-model="data.payMethod"
/>
<VnInput :label="t('Due day')" clearable v-model="data.dueDay" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput :label="t('IBAN')" clearable v-model="data.iban">
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{ t('components.iban_tooltip') }}</QTooltip>
</QIcon>
</template>
</VnInput>
</div>
<div class="col">
<VnSelectDialog
:label="t('Swift / BIC')"
:options="bankEntitiesOptions"
:roles-allowed-to-create="['salesAssistant', 'hr']"
:rules="validate('Worker.bankEntity')"
hide-selected
option-label="name"
option-value="id"
v-model="data.bankEntityFk"
>
<template #form>
<CreateBankEntityForm
@on-data-saved="getBankEntities($event, data)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel
>{{ scope.opt.bic }}
{{ scope.opt.name }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectDialog>
</div>
<VnInput :label="t('IBAN')" clearable v-model="data.iban">
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{ t('components.iban_tooltip') }}</QTooltip>
</QIcon>
</template>
</VnInput>
<VnSelectDialog
:label="t('Swift / BIC')"
:options="bankEntitiesOptions"
:roles-allowed-to-create="['salesAssistant', 'hr']"
:rules="validate('Worker.bankEntity')"
hide-selected
option-label="name"
option-value="id"
v-model="data.bankEntityFk"
>
<template #form>
<CreateBankEntityForm
@on-data-saved="getBankEntities($event, data)"
/>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection v-if="scope.opt">
<QItemLabel
>{{ scope.opt.bic }} {{ scope.opt.name }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelectDialog>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Received LCR')" v-model="data.hasLcr" />
</div>
<div class="col">
<QCheckbox
:label="t('VNL core received')"
v-model="data.hasCoreVnl"
/>
</div>
<div class="col">
<QCheckbox :label="t('VNL B2B received')" v-model="data.hasSepaVnl" />
</div>
<QCheckbox :label="t('Received LCR')" v-model="data.hasLcr" />
<QCheckbox :label="t('VNL core received')" v-model="data.hasCoreVnl" />
<QCheckbox :label="t('VNL B2B received')" v-model="data.hasSepaVnl" />
</VnRow>
</template>
</FormModel>

View File

@ -1,43 +1,17 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import VnCard from 'components/common/VnCard.vue';
import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
import CustomerFilter from '../CustomerFilter.vue';
</script>
<template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="CustomerList"
url="Clients/filter"
:label="t('Search customer')"
:info="t('You can search by customer id or name')"
/>
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<CustomerDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
<VnCard
data-key="Client"
base-url="Clients"
:descriptor="CustomerDescriptor"
:filter-panel="CustomerFilter"
search-data-key="CustomerList"
search-url="Clients/filter"
searchbar-label="Search customer"
searchbar-info="You can search by customer id or name"
/>
</template>
<i18n>
es:
Search customer: Buscar cliente
You can search by customer id or name: Puedes buscar por id o nombre del cliente
</i18n>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from 'vue';
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
@ -11,10 +11,6 @@ const route = useRoute();
const { t } = useI18n();
const customerContactsRef = ref(null);
onMounted(() => {
if (customerContactsRef.value) customerContactsRef.value.reload();
});
</script>
<template>
<div class="full-width flex justify-center">
@ -30,6 +26,7 @@ onMounted(() => {
model="CustomerContacts"
ref="customerContactsRef"
url="ClientContacts"
auto-load
>
<template #body="{ rows }">
<QCard class="q-pl-lg q-py-md">

View File

@ -105,7 +105,7 @@ const updateData = () => {
color="primary"
name="lock"
size="md"
style="font-variation-settings: 'FILL' 1"
class="fill-icon"
>
<QTooltip>{{ t('Close contract') }}</QTooltip>
</QIcon>

View File

@ -7,7 +7,7 @@ import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
const { t } = useI18n();
@ -41,93 +41,75 @@ function handleLocation(data, location) {
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Social name')"
:required="true"
:rules="validate('client.socialName')"
clearable
v-model="data.socialName"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{ t('onlyLetters') }}</QTooltip>
</QIcon>
</template>
</VnInput>
</div>
<div class="col">
<VnInput :label="t('Tax number')" clearable v-model="data.fi" />
</div>
<VnInput
:label="t('Social name')"
:required="true"
:rules="validate('client.socialName')"
clearable
v-model="data.socialName"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{ t('onlyLetters') }}</QTooltip>
</QIcon>
</template>
</VnInput>
<VnInput :label="t('Tax number')" clearable v-model="data.fi" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput :label="t('Street')" clearable v-model="data.street" />
</div>
<VnInput :label="t('Street')" clearable v-model="data.street" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Sage tax type')"
:options="typesTaxes"
hide-selected
option-label="vat"
option-value="id"
v-model="data.sageTaxTypeFk"
/>
</div>
<div class="col">
<VnSelectFilter
:label="t('Sage transaction type')"
:options="typesTransactions"
hide-selected
option-label="transaction"
option-value="id"
v-model="data.sageTransactionTypeFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.name }}</QItemLabel>
<QItemLabel caption>
{{ `${scope.opt.id}: ${scope.opt.transaction}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<VnSelect
:label="t('Sage tax type')"
:options="typesTaxes"
hide-selected
option-label="vat"
option-value="id"
v-model="data.sageTaxTypeFk"
/>
<VnSelect
:label="t('Sage transaction type')"
:options="typesTransactions"
hide-selected
option-label="transaction"
option-value="id"
v-model="data.sageTransactionTypeFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.name }}</QItemLabel>
<QItemLabel caption>
{{ `${scope.opt.id}: ${scope.opt.transaction}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnLocation
:rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.postcode"
@update:model-value="(location) => handleLocation(data, location)"
>
</VnLocation>
</div>
<VnLocation
:rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.postcode"
@update:model-value="(location) => handleLocation(data, location)"
>
</VnLocation>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Active')" v-model="data.isActive" />
</div>
<div class="col">
<QCheckbox :label="t('Frozen')" v-model="data.isFreezed" />
</div>
<VnRow>
<QCheckbox :label="t('Active')" v-model="data.isActive" />
<QCheckbox :label="t('Frozen')" v-model="data.isFreezed" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" />
</div>
<div class="col">
<VnRow>
<QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" />
<div>
<QCheckbox :label="t('Vies')" v-model="data.isVies" />
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>
@ -137,23 +119,16 @@ function handleLocation(data, location) {
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox
:label="t('Notify by email')"
v-model="data.isToBeMailed"
/>
</div>
<div class="col">
<QCheckbox
:label="t('Invoice by address')"
v-model="data.hasToInvoiceByAddress"
/>
</div>
<VnRow>
<QCheckbox :label="t('Notify by email')" v-model="data.isToBeMailed" />
<QCheckbox
:label="t('Invoice by address')"
v-model="data.hasToInvoiceByAddress"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnRow>
<div>
<QCheckbox
:label="t('Is equalizated')"
v-model="data.isEqualizated"
@ -164,27 +139,18 @@ function handleLocation(data, location) {
</QTooltip>
</QIcon>
</div>
<div class="col">
<QCheckbox
:label="t('Verified data')"
v-model="data.isTaxDataChecked"
/>
</div>
<QCheckbox :label="t('Verified data')" v-model="data.isTaxDataChecked" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox
:label="t('Incoterms authorization')"
v-model="data.hasIncoterms"
/>
</div>
<div class="col">
<QCheckbox
:label="t('Electronic invoice')"
v-model="data.hasElectronicInvoice"
/>
</div>
<VnRow>
<QCheckbox
:label="t('Incoterms authorization')"
v-model="data.hasIncoterms"
/>
<QCheckbox
:label="t('Electronic invoice')"
v-model="data.hasElectronicInvoice"
/>
</VnRow>
</template>
</FormModel>

View File

@ -1,20 +1,17 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import { QBtn } from 'quasar';
import { useStateStore } from 'src/stores/useStateStore';
import { toCurrency } from 'src/filters';
import { toDateTimeFormat } from 'src/filters/date';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const stateStore = computed(() => useStateStore());
const rows = ref([]);
const totalAmount = ref(0);
@ -105,28 +102,40 @@ const columns = computed(() => [
const setRows = (data) => {
rows.value = data;
totalAmount.value = data.reduce((accumulator, currentValue) => {
return accumulator + currentValue.amount;
}, 0);
};
const toCustomerGreugeCreate = () => {
router.push({ name: 'CustomerGreugeCreate' });
totalAmount.value = data.reduce((acc, { amount = 0 }) => acc + amount, 0);
};
</script>
<template>
<FetchData :filter="filter" @on-fetch="setRows" auto-load url="greuges" />
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="300" show-if-above>
<QCard class="full-width q-pa-sm">
<h6 class="flex justify-end q-my-lg q-pr-lg" v-if="totalAmount">
<span class="color-vn-label q-mr-md">{{ t('Total') }}:</span>
{{ toCurrency(totalAmount) }}
</h6>
<QSkeleton v-else type="QInput" square />
</QCard>
</QDrawer>
<div class="full-width flex justify-center">
<QPage class="card-width q-pa-lg">
<QCard class="full-width q-pa-sm" v-if="totalAmount">
<h6 class="flex justify-end q-my-lg q-pr-lg">
<span class="color-vn-label q-mr-md">{{ t('Total') }}:</span>
{{ toCurrency(totalAmount) }}
</h6>
</QCard>
<QCard class="q-pa-sm q-mt-md">
<QTable
:columns="columns"
@ -164,7 +173,7 @@ const toCustomerGreugeCreate = () => {
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerGreugeCreate()" color="primary" fab icon="add" />
<QBtn color="primary" fab icon="add" :to="{ name: 'CustomerGreugeCreate' }" />
<QTooltip>
{{ t('New greuge') }}
</QTooltip>

View File

@ -8,7 +8,7 @@ import { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n();
const route = useRoute();
@ -139,7 +139,7 @@ const setInq = (value, status) => {
</QIcon>
</template>
</VnInput>
<VnSelectFilter
<VnSelect
:label="t('Entity')"
:options="[]"
class="q-mt-md"
@ -179,7 +179,7 @@ const setInq = (value, status) => {
/>
</div>
<VnSelectFilter
<VnSelect
:label="t('User')"
:options="[]"
class="q-mt-sm"

View File

@ -11,7 +11,6 @@ import useNotify from 'src/composables/useNotify';
import { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue';

View File

@ -7,7 +7,6 @@ import axios from 'axios';
import { toCurrency, toDateHourMinSec } from 'src/filters';
import FetchData from 'components/FetchData.vue';
import CustomerCloseIconTooltip from '../components/CustomerCloseIconTooltip.vue';
import CustomerCheckIconTooltip from '../components/CustomerCheckIconTooltip.vue';

View File

@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
@ -52,90 +52,70 @@ const businessTypesRef = ref();
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput :label="t('Comercial name')" v-model="data.name" />
</div>
<div class="col">
<VnSelectFilter
:label="t('Salesperson')"
:options="workersOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.salesPersonFk"
/>
</div>
<QInput :label="t('Comercial name')" v-model="data.name" />
<VnSelect
:label="t('Salesperson')"
:options="workersOptions"
hide-selected
option-label="name"
option-value="id"
v-model="data.salesPersonFk"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('Business type')"
:options="businessTypesOptions"
hide-selected
option-label="description"
option-value="code"
v-model="data.businessTypeFk"
:fetch-ref="businessTypesRef"
/>
</div>
<div class="col">
<QInput v-model="data.fi" :label="t('Tax number')" />
</div>
<VnSelect
:label="t('Business type')"
:options="businessTypesOptions"
hide-selected
option-label="description"
option-value="code"
v-model="data.businessTypeFk"
:fetch-ref="businessTypesRef"
/>
<QInput v-model="data.fi" :label="t('Tax number')" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('Business name')"
:rules="validate('client.socialName')"
v-model="data.socialName"
/>
</div>
<QInput
:label="t('Business name')"
:rules="validate('client.socialName')"
v-model="data.socialName"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('Street')"
:rules="validate('client.street')"
v-model="data.street"
/>
</div>
<QInput
:label="t('Street')"
:rules="validate('client.street')"
v-model="data.street"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnLocation
:rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="
(location) => handleLocation(data, location)
"
>
</VnLocation>
</div>
<VnLocation
:rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
>
</VnLocation>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput v-model="data.userName" :label="t('Web user')" />
</div>
<div class="col">
<QInput
:label="t('Email')"
:rules="validate('client.email')"
clearable
type="email"
v-model="data.email"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip max-width="400px">{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</QInput>
</div>
<QInput v-model="data.userName" :label="t('Web user')" />
<QInput
:label="t('Email')"
:rules="validate('client.email')"
clearable
type="email"
v-model="data.email"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip max-width="400px">{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</QInput>
</VnRow>
<QCheckbox
:label="t('Is equalizated')"

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
@ -69,7 +69,7 @@ const zones = ref();
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelectFilter
<VnSelect
:label="t('Salesperson')"
v-model="params.salesPersonFk"
@update:model-value="searchFn()"
@ -92,7 +92,7 @@ const zones = ref();
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="provinces">
<VnSelectFilter
<VnSelect
:label="t('Province')"
v-model="params.provinceFk"
@update:model-value="searchFn()"
@ -139,7 +139,7 @@ const zones = ref();
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="zones">
<VnSelectFilter
<VnSelect
:label="t('Zone')"
v-model="params.zoneFk"
@update:model-value="searchFn()"

View File

@ -15,10 +15,3 @@ const stateStore = useStateStore();
<RouterView></RouterView>
</QPageContainer>
</template>
<style lang="scss">
#searchbar,
.search-panel {
width: 400px;
}
</style>

View File

@ -3,10 +3,10 @@ import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { QBtn, QCheckbox, useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency, toDate } from 'filters/index';
import FetchData from 'components/FetchData.vue';
import { toCurrency, toDate, dateRange } from 'filters/index';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import CustomerNotificationsFilter from './CustomerDefaulterFilter.vue';
import CustomerBalanceDueTotal from './CustomerBalanceDueTotal.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
@ -14,13 +14,14 @@ import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.v
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue';
import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue';
const stateStore = useStateStore();
const { t } = useI18n();
const { t, locale } = useI18n();
const quasar = useQuasar();
const dataRef = ref(null);
const balanceDueTotal = ref(0);
const selected = ref([]);
const rows = ref([]);
const tableColumnComponents = {
client: {
@ -167,39 +168,61 @@ const columns = computed(() => [
},
]);
const setRows = (data) => {
rows.value = data;
balanceDueTotal.value = data.reduce((accumulator, currentValue) => {
return accumulator + (currentValue['amount'] || 0);
}, 0);
};
const viewAddObservation = (rowsSelected) => {
quasar.dialog({
component: CustomerDefaulterAddObservation,
componentProps: {
clients: rowsSelected,
promise: refreshData,
promise: async () => await dataRef.value.fetch(),
},
});
};
const refreshData = () => {
setRows();
const onFetch = (data) => {
for (const element of data) element.isWorker = element.businessTypeFk === 'worker';
balanceDueTotal.value = data.reduce((acc, { amount = 0 }) => acc + amount, 0);
};
const onFetch = (data) => {
for (const element of data) {
element.isWorker = element.businessTypeFk === 'worker';
function exprBuilder(param, value) {
switch (param) {
case 'clientFk':
return { [`d.${param}`]: value?.id };
case 'creditInsurance':
case 'amount':
case 'workerFk':
case 'countryFk':
case 'payMethod':
case 'salesPersonFk':
return { [`d.${param}`]: value };
case 'date':
return { 'd.created': { between: dateRange(value) } };
case 'defaulterSinced':
return { 'd.defaulterSinced': { between: dateRange(value) } };
}
rows.value = data;
};
}
</script>
<template>
<FetchData :filter="filter" @on-fetch="onFetch" auto-load url="Defaulters/filter" />
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer side="right" :width="256" show-if-above>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<CustomerNotificationsFilter data-key="CustomerDefaulter" />
</QScrollArea>
@ -214,69 +237,107 @@ const onFetch = (data) => {
icon="vn:notes"
:disabled="!selected.length"
@click.stop="viewAddObservation(selected)"
/>
>
<QTooltip>{{ t('Add observation') }}</QTooltip>
</QBtn>
</div>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<QTable
:columns="columns"
:rows="rows"
class="full-width q-mt-md"
row-key="clientFk"
selection="multiple"
v-model:selected="selected"
<VnPaginate
ref="dataRef"
@on-fetch="onFetch"
data-key="CustomerDefaulter"
:filter="filter"
:expr-builder="exprBuilder"
auto-load
url="Defaulters/filter"
>
<template #header="props">
<QTr :props="props" class="bg">
<QTh>
<QCheckbox v-model="props.selected" />
</QTh>
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
<QTooltip v-if="col.tooltip">{{ col.tooltip }}</QTooltip>
</QTh>
</QTr>
</template>
<template #body="{ rows }">
<div class="q-pa-md">
<QTable
:columns="columns"
:rows="rows"
class="full-width q-mt-md"
row-key="clientFk"
selection="multiple"
v-model:selected="selected"
>
<template #header="props">
<QTr :props="props" class="bg">
<QTh>
<QCheckbox v-model="props.selected" />
</QTh>
<QTh
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ t(col.label) }}
<QTooltip v-if="col.tooltip">{{
col.tooltip
}}</QTooltip>
</QTh>
</QTr>
</template>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
>
<template v-if="props.col.name !== 'isWorker'">
<div v-if="props.col.name === 'lastObservation'">
<VnInput
type="textarea"
v-model="props.value"
autogrow
/>
</div>
<div v-else>{{ props.value }}</div>
</template>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="
tableColumnComponents[props.col.name]
.component
"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(
props
)
"
@click="
tableColumnComponents[props.col.name].event(
props
)
"
>
<template v-if="props.col.name !== 'isWorker'">
<div
v-if="
props.col.name === 'lastObservation'
"
>
<VnInput
type="textarea"
v-model="props.value"
autogrow
:disable="true"
/>
</div>
<div v-else>{{ props.value }}</div>
</template>
<WorkerDescriptorProxy
:id="props.row.salesPersonFk"
v-if="props.col.name === 'salesPerson'"
/>
<WorkerDescriptorProxy
:id="props.row.workerFk"
v-if="props.col.name === 'author'"
/>
<CustomerDescriptorProxy
:id="props.row.clientFk"
v-if="props.col.name === 'client'"
/>
</component>
</QTr>
</QTd>
<WorkerDescriptorProxy
:id="props.row.salesPersonFk"
v-if="props.col.name === 'salesPerson'"
/>
<WorkerDescriptorProxy
:id="props.row.workerFk"
v-if="props.col.name === 'author'"
/>
<CustomerDescriptorProxy
:id="props.row.clientFk"
v-if="props.col.name === 'client'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
</div>
</template>
</QTable>
</VnPaginate>
</QPage>
</template>
@ -289,6 +350,7 @@ const onFetch = (data) => {
<i18n>
es:
Add observation: Añadir observación
Client: Cliente
Is worker: Es trabajador
Salesperson: Comercial

View File

@ -61,13 +61,11 @@ const onSubmit = async () => {
}}
</div>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QInput
:label="t('Message')"
type="textarea"
v-model="newObservation"
/>
</div>
<QInput
:label="t('Message')"
type="textarea"
v-model="newObservation"
/>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn

View File

@ -6,7 +6,7 @@ import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnSelect from 'components/common/VnSelect.vue';
const { t } = useI18n();
const props = defineProps({
@ -48,7 +48,7 @@ const authors = ref();
<template #body="{ params }">
<QItem class="q-mb-sm q-mt-sm">
<QItemSection v-if="clients">
<VnSelectFilter
<VnSelect
:input-debounce="0"
:label="t('Client')"
:options="clients"
@ -57,7 +57,7 @@ const authors = ref();
hide-selected
map-options
option-label="name"
option-value="clientTypeFk"
option-value="id"
outlined
rounded
use-input
@ -71,7 +71,7 @@ const authors = ref();
<QItem class="q-mb-sm">
<QItemSection v-if="salespersons">
<VnSelectFilter
<VnSelect
:input-debounce="0"
:label="t('Salesperson')"
:options="salespersons"
@ -94,7 +94,7 @@ const authors = ref();
<QItem class="q-mb-sm">
<QItemSection v-if="countries">
<VnSelectFilter
<VnSelect
:input-debounce="0"
:label="t('Country')"
:options="countries"
@ -139,7 +139,7 @@ const authors = ref();
<QItem class="q-mb-sm">
<QItemSection v-if="authors">
<VnSelectFilter
<VnSelect
:input-debounce="0"
:label="t('Author')"
:options="authors"
@ -162,7 +162,7 @@ const authors = ref();
<QItem class="q-mb-sm">
<QItemSection>
<VnInput
<VnInputDate
:label="t('L. O. Date')"
clearable
is-outlined

View File

@ -10,7 +10,7 @@ import CustomerExtendedListActions from './CustomerExtendedListActions.vue';
import CustomerExtendedListFilter from './CustomerExtendedListFilter.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters';
@ -37,8 +37,6 @@ onMounted(() => {
allColumnNames.value = filteredColumns.map((col) => col.name);
});
const rows = computed(() => arrayData.value.store.data);
const selectedCustomerId = ref(0);
const selectedSalesPersonId = ref(0);
const allColumnNames = ref([]);
@ -70,6 +68,11 @@ const tableColumnComponents = {
props: () => {},
event: () => {},
},
socialName: {
component: 'span',
props: () => {},
event: () => {},
},
fi: {
component: 'span',
props: () => {},
@ -283,6 +286,12 @@ const columns = computed(() => [
label: t('customer.extendedList.tableVisibleColumns.name'),
name: 'name',
},
{
align: 'left',
field: 'socialName',
label: t('customer.extendedList.tableVisibleColumns.socialName'),
name: 'socialName',
},
{
align: 'left',
field: 'fi',
@ -485,6 +494,23 @@ const selectSalesPersonId = (id) => (selectedSalesPersonId.value = id);
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport></template
>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<CustomerExtendedListFilter
@ -495,7 +521,7 @@ const selectSalesPersonId = (id) => (selectedSalesPersonId.value = id);
</QScrollArea>
</QDrawer>
<VnSubToolbar>
<template #st-actions>
<template #st-data>
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="clientsDetail"
@ -508,58 +534,97 @@ const selectSalesPersonId = (id) => (selectedSalesPersonId.value = id);
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<QTable
:columns="columns"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
:visible-columns="visibleColumns"
@row-click="(evt, row, id) => navigateToTravelId(row.id)"
<VnPaginate
data-key="CustomerExtendedList"
url="Clients/extendedListFilter"
auto-load
>
<template #body-cell="{ col, value }">
<QTd @click="stopEventPropagation($event, col)">
{{ value }}
</QTd>
</template>
<template #body-cell-id="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
<template #body="{ rows }">
<div class="q-pa-md">
<QTable
:columns="columns"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
:visible-columns="visibleColumns"
@row-click="(evt, row, id) => navigateToTravelId(row.id)"
>
<CustomerDescriptorProxy :id="props.row.id" />
{{ props.row.id }}
</component>
</QTd>
<template #body-cell="{ col, value }">
<QTd @click="stopEventPropagation($event, col)">
{{ value }}
</QTd>
</template>
<template #body-cell-customerStatus="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
</component>
</QTd>
</template>
<template #body-cell-id="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
<CustomerDescriptorProxy :id="props.row.id" />
{{ props.row.id }}
</component>
</QTd>
</template>
<template #body-cell-salesPersonFk="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
v-if="props.row.salesPerson"
class="col-content"
:is="tableColumnComponents[props.col.name].component"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
<WorkerDescriptorProxy
:id="props.row.salesPersonFk"
/>
{{ props.row.salesPerson }}
</component>
<span class="col-content" v-else>-</span>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
/>
</QTd>
</template>
</QTable>
</div>
</template>
<template #body-cell-salesPersonFk="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
v-if="props.row.salesPerson"
class="col-content"
:is="tableColumnComponents[props.col.name].component"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
>
<WorkerDescriptorProxy :id="props.row.salesPersonFk" />
{{ props.row.salesPerson }}
</component>
<span class="col-content" v-else>-</span>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
/>
</QTd>
</template>
</QTable>
</VnPaginate>
</QPage>
</template>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import { dateRange } from 'src/filters';
@ -169,7 +169,7 @@ const shouldRenderColumn = (colName) => {
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="clients">
<VnSelectFilter
<VnSelect
:label="t('Social name')"
v-model="params.socialName"
@update:model-value="searchFn()"
@ -201,7 +201,7 @@ const shouldRenderColumn = (colName) => {
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelectFilter
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.salesPersonFk')
"
@ -270,7 +270,7 @@ const shouldRenderColumn = (colName) => {
</QItem>
<QItem v-if="shouldRenderColumn('countryFk')">
<QItemSection>
<VnSelectFilter
<VnSelect
:label="t('customer.extendedList.tableVisibleColumns.countryFk')"
v-model="params.countryFk"
@update:model-value="searchFn()"
@ -287,7 +287,7 @@ const shouldRenderColumn = (colName) => {
</QItem>
<QItem v-if="shouldRenderColumn('provinceFk')">
<QItemSection>
<VnSelectFilter
<VnSelect
:label="t('customer.extendedList.tableVisibleColumns.provinceFk')"
v-model="params.provinceFk"
@update:model-value="searchFn()"
@ -342,7 +342,7 @@ const shouldRenderColumn = (colName) => {
</QItem>
<QItem v-if="shouldRenderColumn('businessTypeFk')">
<QItemSection>
<VnSelectFilter
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.businessTypeFk')
"
@ -361,7 +361,7 @@ const shouldRenderColumn = (colName) => {
</QItem>
<QItem v-if="shouldRenderColumn('payMethodFk')">
<QItemSection>
<VnSelectFilter
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.payMethodFk')
"
@ -380,7 +380,7 @@ const shouldRenderColumn = (colName) => {
</QItem>
<QItem v-if="shouldRenderColumn('sageTaxTypeFk')">
<QItemSection>
<VnSelectFilter
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk')
"
@ -399,7 +399,7 @@ const shouldRenderColumn = (colName) => {
</QItem>
<QItem v-if="shouldRenderColumn('sageTransactionTypeFk')">
<QItemSection>
<VnSelectFilter
<VnSelect
:label="
t(
'customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'

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