0
0
Fork 0

refactor: refs #6767 fix all conflicts

This commit is contained in:
Jon Elias 2024-04-26 09:47:44 +02:00
commit e6dc7b9862
231 changed files with 21852 additions and 9158 deletions

View File

@ -5,6 +5,8 @@ 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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ 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';
@ -73,7 +73,8 @@ onMounted(async () => {
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<div class="col">
<VnSelect
:label="t('country')"
v-model="data.countryFk"
:options="countriesOptions"
@ -83,6 +84,7 @@ onMounted(async () => {
:required="true"
:rules="validate('bankEntity.countryFk')"
/>
</div>
<div v-if="showEntityField" class="col">
<VnInput
:label="t('id')"

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,7 +72,7 @@ const onDataSaved = async (formData, requestResponse) => {
{{ t('Invoicing in progress...') }}
</span>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<VnSelect
:label="t('Ticket')"
:options="ticketsOptions"
hide-selected
@ -85,17 +85,15 @@ const onDataSaved = async (formData, requestResponse) => {
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{
scope.opt?.nickname
}}</QItemLabel>
<QItemLabel caption>{{ scope.opt?.nickname }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</VnSelect>
<span class="row items-center" style="max-width: max-content">{{
t('Or')
}}</span>
<VnSelectFilter
<VnSelect
:label="t('Client')"
:options="clientsOptions"
hide-selected
@ -107,7 +105,7 @@ const onDataSaved = async (formData, requestResponse) => {
<VnInputDate :label="t('Max date')" v-model="data.maxShipped" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<VnSelect
:label="t('Serial')"
:options="invoiceOutSerialsOptions"
hide-selected
@ -116,7 +114,7 @@ const onDataSaved = async (formData, requestResponse) => {
v-model="data.serial"
:required="true"
/>
<VnSelectFilter
<VnSelect
:label="t('Area')"
:options="taxAreasOptions"
hide-selected

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';
@ -45,7 +45,7 @@ const onDataSaved = (dataSaved) => {
v-model="data.name"
:rules="validate('city.name')"
/>
<VnSelectFilter
<VnSelect
:label="t('Province')"
:options="provincesOptions"
hide-selected

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,7 +30,7 @@ const townsLocationOptions = ref([]);
const onDataSaved = (formData) => {
const newPostcode = {
...formData
...formData,
};
const townObject = townsLocationOptions.value.find(
({ id }) => id === formData.townFk
@ -108,9 +108,7 @@ const onProvinceCreated = async ({ name }, formData) => {
:roles-allowed-to-create="['deliveryAssistant']"
>
<template #form>
<CreateNewCityForm
@on-data-saved="onCityCreated($event, data)"
/>
<CreateNewCityForm @on-data-saved="onCityCreated($event, data)" />
</template>
</VnSelectDialog>
</VnRow>
@ -131,7 +129,7 @@ const onProvinceCreated = async ({ name }, formData) => {
/>
</template>
</VnSelectDialog>
<VnSelectFilter
<VnSelect
:label="t('Country')"
:options="countriesOptions"
hide-selected

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';
@ -45,7 +45,7 @@ const onDataSaved = (dataSaved) => {
v-model="data.name"
:rules="validate('province.name')"
/>
<VnSelectFilter
<VnSelect
:label="t('Autonomy')"
:options="autonomiesOptions"
hide-selected

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';
@ -60,7 +60,7 @@ const onDataSaved = (dataSaved) => {
:required="true"
:rules="validate('thermograph.id')"
/>
<VnSelectFilter
<VnSelect
:label="t('Model')"
:options="thermographsModels"
hide-selected
@ -72,7 +72,7 @@ const onDataSaved = (dataSaved) => {
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-xl">
<VnSelectFilter
<VnSelect
:label="t('Warehouse')"
:options="warehousesOptions"
hide-selected
@ -81,7 +81,7 @@ const onDataSaved = (dataSaved) => {
v-model="data.warehouseId"
:required="true"
/>
<VnSelectFilter
<VnSelect
:label="t('Temperature')"
:options="temperaturesOptions"
hide-selected

View File

@ -24,6 +24,10 @@ const $props = defineProps({
type: String,
default: '',
},
limit: {
type: Number,
default: 20,
},
saveUrl: {
type: String,
default: null,
@ -76,6 +80,7 @@ defineExpose({
reset,
hasChanges,
saveChanges,
getChanges,
});
async function fetch(data) {
@ -260,6 +265,7 @@ watch(formUrl, async () => {
<template>
<VnPaginate
:url="url"
:limit="limit"
v-bind="$attrs"
@on-fetch="fetch"
:skeleton="false"

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';
@ -288,7 +288,7 @@ const makeRequest = async () => {
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<VnSelect
:label="t('Orientation')"
:options="viewportTypes"
hide-selected

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,15 +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">
<VnSelectFilter
<VnSelect
:label="t('Field to edit')"
:options="fieldsOptions"
hide-selected
option-label="label"
option-value="field"
v-model="formData.field"
v-model="selectedField"
/>
<component
:is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}"
v-model="newValue"
:label="t('Value')"
style="width: 200px"
/>
<VnInput :label="t('Value')" v-model="formData.newValue" />
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn

View File

@ -6,7 +6,7 @@ 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';
@ -147,15 +147,9 @@ const selectItem = ({ id }) => {
</span>
<h1 class="title">{{ t('Filter item') }}</h1>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
:label="t('entry.buys.name')"
v-model="itemFilterParams.name"
/>
<VnInput
:label="t('entry.buys.size')"
v-model="itemFilterParams.size"
/>
<VnSelectFilter
<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
@ -163,7 +157,7 @@ const selectItem = ({ id }) => {
option-value="id"
v-model="itemFilterParams.producerFk"
/>
<VnSelectFilter
<VnSelect
:label="t('entry.buys.type')"
:options="ItemTypesOptions"
hide-selected
@ -171,7 +165,7 @@ const selectItem = ({ id }) => {
option-value="id"
v-model="itemFilterParams.typeFk"
/>
<VnSelectFilter
<VnSelect
:label="t('entry.buys.color')"
:options="InksOptions"
hide-selected

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,7 +145,7 @@ const selectTravel = ({ id }) => {
</span>
<h1 class="title">{{ t('Filter travels') }}</h1>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<VnSelect
:label="t('entry.basicData.agency')"
:options="agenciesOptions"
hide-selected
@ -153,7 +153,7 @@ const selectTravel = ({ id }) => {
option-value="id"
v-model="travelFilterParams.agencyModeFk"
/>
<VnSelectFilter
<VnSelect
:label="t('entry.basicData.warehouseOut')"
:options="warehousesOptions"
hide-selected
@ -161,7 +161,7 @@ const selectTravel = ({ id }) => {
option-value="id"
v-model="travelFilterParams.warehouseOutFk"
/>
<VnSelectFilter
<VnSelect
:label="t('entry.basicData.warehouseIn')"
:options="warehousesOptions"
hide-selected

View File

@ -81,6 +81,7 @@ const emit = defineEmits(['onFetch', 'onDataSaved']);
const componentIsRendered = ref(false);
onMounted(async () => {
originalData.value = $props.formInitialData;
nextTick(() => {
componentIsRendered.value = true;
});
@ -101,7 +102,7 @@ onMounted(async () => {
});
onBeforeRouteLeave((to, from, next) => {
if (hasChanges.value)
if (hasChanges.value && $props.observeFormChanges)
quasar.dialog({
component: VnConfirm,
componentProps: {
@ -126,7 +127,7 @@ const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({ ...$props.formInitialData });
const originalData = ref({});
const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({
@ -154,14 +155,18 @@ const startFormWatcher = () => {
};
async function fetch() {
try {
const { data } = await axios.get($props.url, {
params: { filter: JSON.stringify($props.filter) },
});
state.set($props.model, data);
originalData.value = data && JSON.parse(JSON.stringify(data));
emit('onFetch', state.get($props.model));
} catch (error) {
state.set($props.model, {});
originalData.value = {};
}
}
async function save() {

View File

@ -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

@ -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 VnSelectFilter from 'components/common/VnSelectFilter.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>
<VnSelectFilter
: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>
</VnSelectFilter>
</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">
<VnSelectFilter
: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">
<VnSelectFilter
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

@ -1,6 +1,6 @@
<script setup>
import axios from 'axios';
import { onMounted, ref } from 'vue';
import { onMounted, ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { QSeparator, useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
@ -22,6 +22,8 @@ const props = defineProps({
},
});
const expansionItemElements = reactive({});
onMounted(async () => {
await navigation.fetchPinned();
getRoutes();
@ -108,6 +110,10 @@ async function togglePinned(item, event) {
type: 'positive',
});
}
const handleItemExpansion = (itemName) => {
expansionItemElements[itemName].scrollToLastElement();
};
</script>
<template>
@ -213,8 +219,12 @@ async function togglePinned(item, event) {
:icon="item.icon"
:label="t(item.title)"
:content-inset-level="0.5"
@after-show="handleItemExpansion(item.name)"
>
<LeftMenuItemGroup :item="item" />
<LeftMenuItemGroup
:ref="(el) => (expansionItemElements[item.name] = el)"
:item="item"
/>
</QExpansionItem>
</QList>
</template>

View File

@ -1,6 +1,7 @@
<script setup>
import { computed } from 'vue';
import { computed, ref } from 'vue';
import LeftMenuItem from './LeftMenuItem.vue';
import { elementIsVisibleInViewport } from 'src/composables/elementIsVisibleInViewport';
const props = defineProps({
item: {
@ -13,10 +14,27 @@ const props = defineProps({
},
});
const groupEnd = ref(null);
const scrollToLastElement = () => {
if (groupEnd.value && !elementIsVisibleInViewport(groupEnd.value)) {
groupEnd.value.scrollIntoView({
behavior: 'smooth',
block: 'end',
});
}
};
const item = computed(() => props.item); // eslint-disable-line vue/no-dupe-keys
defineExpose({
scrollToLastElement,
});
</script>
<template>
<template v-for="section in item.children" :key="section.name">
<LeftMenuItem :item="section" />
</template>
<div ref="groupEnd" />
</template>

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';
@ -53,10 +53,12 @@ const onDataSaved = (data) => {
<QInput
:label="t('Type the visible quantity')"
v-model.number="data.quantity"
autofocus
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<div class="col">
<VnSelect
:label="t('Warehouse')"
v-model="data.warehouseFk"
:options="warehousesOptions"
@ -64,6 +66,7 @@ const onDataSaved = (data) => {
option-label="name"
hide-selected
/>
</div>
</VnRow>
</template>
</FormModelPopup>

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,7 +83,7 @@ const transferInvoice = async () => {
>
<template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<VnSelect
:label="t('Client')"
:options="clientsOptions"
hide-selected
@ -102,8 +102,8 @@ const transferInvoice = async () => {
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
<VnSelectFilter
</VnSelect>
<VnSelect
:label="t('Rectificative type')"
:options="rectificativeTypeOptions"
hide-selected
@ -114,7 +114,7 @@ const transferInvoice = async () => {
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<VnSelect
:label="t('Class')"
:options="siiTypeInvoiceOutsOptions"
hide-selected
@ -133,8 +133,8 @@ const transferInvoice = async () => {
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
<VnSelectFilter
</VnSelect>
<VnSelect
:label="t('Type')"
:options="invoiceCorrectionTypesOptions"
hide-selected

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,24 +172,24 @@ 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
<VnSelect
:label="t('components.userPanel.localBank')"
hide-selected
v-model="user.localBankFk"
:options="accountBankData"
option-label="bank"
option-value="id"
></VnSelectFilter>
></VnSelect>
</VnRow>
<VnRow>
<VnSelectFilter
<VnSelect
:label="t('components.userPanel.localCompany')"
hide-selected
v-model="user.companyFk"
@ -197,7 +197,7 @@ function copyUserToken() {
option-label="code"
option-value="id"
/>
<VnSelectFilter
<VnSelect
:label="t('components.userPanel.userWarehouse')"
hide-selected
v-model="user.warehouseFk"
@ -207,7 +207,7 @@ function copyUserToken() {
/>
</VnRow>
<VnRow>
<VnSelectFilter
<VnSelect
:label="t('components.userPanel.userCompany')"
hide-selected
v-model="user.companyFk"

View File

@ -0,0 +1,78 @@
<script setup>
import { onBeforeMount, computed } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { useI18n } from 'vue-i18n';
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';
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 },
searchbarDataKey: { type: String, default: undefined },
searchbarUrl: { type: String, default: undefined },
searchbarLabel: { type: String, default: '' },
searchbarInfo: { type: String, default: '' },
});
const { t } = useI18n();
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 });
}
});
}
</script>
<template>
<Teleport
to="#searchbar"
v-if="stateStore.isHeaderMounted() && props.searchbarDataKey"
>
<VnSearchbar
:data-key="props.searchbarDataKey"
:url="props.searchbarUrl"
:label="t(props.searchbarLabel)"
:info="t(props.searchbarInfo)"
/>
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<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

@ -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();
@ -659,7 +659,7 @@ setLogTree();
</QInput>
</QItem>
<QItem>
<VnSelectFilter
<VnSelect
class="full-width"
:label="t('globals.entity')"
v-model="selectedFilters.changedModel"
@ -689,7 +689,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 +713,7 @@ setLogTree();
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</VnSelect>
</QItemSection>
</QItem>
<QItem class="q-mt-sm">

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

@ -21,7 +21,8 @@ const props = defineProps({
},
template: {
type: String,
required: true,
required: false,
default: '',
},
locale: {
type: String,
@ -49,7 +50,7 @@ updateMessage();
function updateMessage() {
const params = props.data;
const key = `templates['${props.template}']`;
const key = props.template ? `templates['${props.template}']` : '';
message.value = t(key, params, { locale: locale.value });
}
@ -104,15 +105,14 @@ async function send() {
map-options
:input-debounce="0"
rounded
outlined
dense
/>
</QCardSection>
<QCardSection class="q-pb-xs">
<VnInput :label="t('Phone')" v-model="phone" is-outlined />
<VnInput :label="t('Phone')" v-model="phone" />
</QCardSection>
<QCardSection class="q-pb-xs">
<VnInput v-model="subject" :label="t('Subject')" is-outlined />
<VnInput v-model="subject" :label="t('Subject')" />
</QCardSection>
<QCardSection class="q-mb-md" q-input>
<QInput
@ -125,7 +125,6 @@ async function send() {
:bottom-slots="true"
:rules="[(value) => value.length < maxLength || 'Error!']"
stack-label
outlined
autofocus
>
<template #append>
@ -135,6 +134,11 @@ async function send() {
@click="message = ''"
class="cursor-pointer"
/>
<QIcon name="info" class="cursor-pointer">
<QTooltip>
{{ t('messageTooltip') }}
</QTooltip>
</QIcon>
</template>
<template #counter>
<QChip :color="color" dense>
@ -184,18 +188,20 @@ en:
es: Spanish
fr: French
pt: Portuguese
messageTooltip: Special characters like accents counts as multiple
es:
Send SMS: Enviar SMS
Language: Idioma
Phone: Móvil
Subject: Asunto
Message: Mensaje
messageTooltip: Carácteres especiales como acentos cuentan como varios
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 }'
{ orderId } con llegada { landing } para recibirlo sin portes adicionales.'
orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }'
en: Inglés
es: Español
fr: Francés
@ -207,12 +213,14 @@ fr:
Phone: Mobile
Subject: Affaire
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
@ -224,12 +232,13 @@ pt:
Phone: Móvel
Subject: Assunto
Message: Mensagem
messageTooltip: Caracteres especiais como acentos contam como vários
templates:
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,9 +48,10 @@ 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;
}
</script>

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,11 +1,12 @@
<script setup>
import VnAvatar from 'src/components/ui/VnAvatar.vue';
import { toDateHour } from 'src/filters';
import { toDateHourMin } from 'src/filters';
import { ref } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n';
import VnPaginate from './VnPaginate.vue';
import VnUserLink from '../ui/VnUserLink.vue';
import { useState } from 'src/composables/useState';
const $props = defineProps({
url: { type: String, default: null },
@ -13,8 +14,10 @@ const $props = defineProps({
body: { type: Object, default: () => {} },
addNote: { type: Boolean, default: false },
});
const { t } = useI18n();
const noteModal = ref(false);
const state = useState();
const currentUser = ref(state.getUser());
const newNote = ref('');
const vnPaginateRef = ref();
@ -22,98 +25,83 @@ async function insert() {
const body = $props.body;
Object.assign(body, { text: newNote.value });
await axios.post($props.url, body);
vnPaginateRef.value.fetch();
await vnPaginateRef.value.fetch();
newNote.value = '';
}
</script>
<template>
<div class="column items-center full-height full-width">
<VnPaginate
:data-key="$props.url"
:url="$props.url"
order="created DESC"
:limit="20"
:filter="$props.filter"
auto-load
ref="vnPaginateRef"
>
<template #body="{ rows }">
<div class="column items-center full-width">
<QCard
class="q-pa-xs q-mb-sm full-width"
v-for="(note, index) in rows"
:key="index"
>
<QCard class="q-pa-xs q-mb-xl full-width" v-if="$props.addNote">
<QCardSection horizontal>
<slot name="picture">
<VnAvatar
:descriptor="false"
:worker-id="note.workerFk"
size="md"
/>
</slot>
<VnAvatar :worker-id="currentUser.id" size="md" />
<div class="full-width row justify-between q-pa-xs">
<VnUserLink
:name="`${note.worker.user.nickname}`"
:worker-id="note.worker.id"
/>
<slot name="actions">
{{ toDateHour(note.created) }}
</slot>
<VnUserLink :name="t('New note')" :worker-id="currentUser.id" />
{{ t('globals.now') }}
</div>
</QCardSection>
<QCardSection class="q-pa-xs q-my-none q-py-none">
<slot name="text">
{{ note.text }}
</slot>
</QCardSection>
</QCard>
</div>
</template>
</VnPaginate>
<QPageSticky position="bottom-right" :offset="[25, 25]" v-if="addNote">
<QBtn color="primary" icon="add" size="lg" round @click="noteModal = true" />
</QPageSticky>
<QDialog v-model="noteModal" @hide="newNote = ''">
<QCard>
<QCardSection>
<QItem class="q-px-none">
<span class="text-primary text-h6 full-width">
<QIcon name="draft" class="q-mr-xs" />
{{ t('Add note') }}
</span>
<QBtn icon="close" flat round dense v-close-popup />
</QItem>
</QCardSection>
<QCardSection>
<QCardSection class="q-pa-xs q-my-none q-py-none" horizontal>
<QInput
autofocus
v-model="newNote"
class="full-width"
type="textarea"
:label="t('Add note here...')"
filled
size="lg"
autogrow
v-model="newNote"
></QInput>
</QCardSection>
<QCardActions class="justify-end q-mr-sm">
<QBtn
autofocus
@keyup.ctrl.enter.stop="insert"
clearable
>
<template #append
><QBtn
:title="t('Save (ctrl + Enter)')"
icon="save"
color="primary"
flat
:label="t('globals.close')"
color="primary"
v-close-popup
/>
<QBtn
:label="t('globals.save')"
color="primary"
v-close-popup
@click="insert"
/>
</QCardActions>
</template>
</QInput>
</QCardSection>
</QCard>
</QDialog>
<VnPaginate
:data-key="$props.url"
:url="$props.url"
order="created DESC"
:limit="0"
:filter="$props.filter"
auto-load
ref="vnPaginateRef"
class="show"
v-bind="$attrs"
>
<template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width">
<QCard
class="q-pa-xs q-mb-sm full-width"
v-for="(note, index) in rows"
:key="note.id ?? index"
>
<QCardSection horizontal>
<VnAvatar
:descriptor="false"
:worker-id="note.workerFk"
size="md"
/>
<div class="full-width row justify-between q-pa-xs">
<VnUserLink
:name="`${note.worker.user.nickname}`"
:worker-id="note.worker.id"
/>
{{ toDateHourMin(note.created) }}
</div>
</QCardSection>
<QCardSection class="q-pa-xs q-my-none q-py-none">
{{ note.text }}
</QCardSection>
</QCard>
</TransitionGroup>
</template>
</VnPaginate>
</template>
<style lang="scss" scoped>
.q-card {
@ -128,9 +116,20 @@ async function insert() {
.q-dialog .q-card {
width: 400px;
}
.list-enter-active,
.list-leave-active {
transition: all 1s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
background-color: $primary;
}
</style>
<i18n>
es:
Add note here...: Añadir nota aquí...
Add note: Añadir nota
New note: Nueva nota
Save (ctrl + Enter): Guardar (Ctrl + Intro)
</i18n>

View File

@ -77,7 +77,6 @@ const arrayData = useArrayData(props.dataKey, {
userParams: props.userParams,
exprBuilder: props.exprBuilder,
});
const hasMoreData = ref();
const store = arrayData.store;
onMounted(() => {
@ -97,7 +96,7 @@ const addFilter = async (filter, params) => {
async function fetch() {
await arrayData.fetch({ append: false });
if (!arrayData.hasMoreData.value) {
if (!store.hasMoreData) {
isLoading.value = false;
}
emit('onFetch', store.data);
@ -110,8 +109,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 +131,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 +139,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 +179,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 +192,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

@ -131,13 +131,6 @@ 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"

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

@ -0,0 +1,6 @@
export const elementIsVisibleInViewport = (el) => {
const { top, left, bottom, right } = el.getBoundingClientRect();
const { innerHeight, innerWidth } = window;
return top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth;
};

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 = [
@ -96,9 +92,8 @@ export function useArrayData(key, userOptions) {
});
const { limit } = filter;
store.hasMoreData = limit && response.data.length >= limit;
hasMoreData.value = response.data.length >= limit;
store.hasMoreData = hasMoreData.value;
if (append) {
if (!store.data) store.data = [];
for (const row of response.data) store.data.push(row);
@ -156,9 +151,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;
}
@ -169,7 +165,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;
@ -211,7 +207,6 @@ export function useArrayData(key, userOptions) {
destroy,
loadMore,
store,
hasMoreData,
totalRows,
updateStateParams,
isLoading,

View File

@ -75,6 +75,10 @@ select:-webkit-autofill {
background-color: #666666;
}
.color-vn-label {
color: var(--vn-label);
}
.color-vn-text {
color: var(--vn-text-color);
}
@ -94,6 +98,10 @@ select:-webkit-autofill {
border-radius: 8px;
}
.card-width {
width: 770px;
}
.vn-card-list {
width: 100%;
max-width: 60em;
@ -111,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;
@ -128,6 +141,19 @@ select:-webkit-autofill {
background-color: var(--vn-section-color);
}
.q-checkbox {
& .q-checkbox__label {
color: var(--vn-text-color);
}
& .q-checkbox__inner {
color: var(--vn-label-color);
}
}
.tr-header {
color: var(--vn-label-color);
}
.q-chip,
.q-notification__message,
.q-notification__icon {
@ -162,13 +188,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

@ -80,7 +80,7 @@
<glyph unicode="&#xe94b;" glyph-name="consignatarios" d="M409.6-64v349.867h204.8v-349.867h256v563.2h153.6l-512 460.8-512-460.8h153.6v-563.2h256z" />
<glyph unicode="&#xe94c;" glyph-name="control" d="M418.133 315.733l-128-123.733 256-256 469.333 469.333-128 128-341.333-341.333zM546.133 311.467l34.133 34.133h-68.267zM230.4 128l-59.733 64 153.6 153.6h-68.267v102.4h426.667l204.8 204.8 85.333-85.333v187.733c0 55.467-46.933 102.4-102.4 102.4h-213.333c-21.333 59.733-76.8 102.4-145.067 102.4s-123.733-42.667-145.067-102.4h-213.333c-55.467 0-102.4-46.933-102.4-102.4v-716.8c0-55.467 46.933-102.4 102.4-102.4h273.067l-196.267 192zM512 857.6c29.867 0 51.2-21.333 51.2-51.2s-21.333-51.2-51.2-51.2-51.2 21.333-51.2 51.2c0 29.867 21.333 51.2 51.2 51.2zM256 652.8h512v-102.4h-512v102.4zM665.6-64h204.8c55.467 0 102.4 46.933 102.4 102.4v204.8l-307.2-307.2z" />
<glyph unicode="&#xe94d;" glyph-name="credit" d="M921.6 849.067h-819.2c-55.467 0-102.4-42.667-102.4-98.133v-601.6c0-55.467 46.933-102.4 102.4-102.4h819.2c55.467 0 102.4 42.667 102.4 102.4v601.6c0 55.467-46.933 98.133-102.4 98.133zM921.6 145.067h-819.2v302.933h819.2v-302.933zM921.6 648.533h-819.2v102.4h819.2v-102.4z" />
<glyph unicode="&#xe94e;" glyph-name="deaulter" d="M677.973-64c-30.72 35.84-61.867 70.827-91.307 107.52-40.96 51.2-80.64 103.253-121.173 154.88-16.64 21.333-21.76 20.48-30.72-4.693-13.227-36.693-25.6-73.387-40.107-109.653-5.12-12.8-13.227-26.88-24.32-34.56-51.627-34.987-104.107-69.12-157.867-100.693-10.667-6.4-30.72-5.547-41.813 0.853-8.107 4.693-12.373 23.893-11.093 35.84 0.853 8.96 11.093 19.627 19.627 25.6 39.253 26.453 78.933 51.627 119.040 76.8 18.347 11.52 30.293 26.027 35.84 47.787 12.373 48.213 27.307 95.573 39.253 143.36 8.533 33.707 26.88 58.88 56.32 77.227 40.533 25.173 80.64 52.053 120.747 78.507 6.4 4.267 10.24 11.52 15.36 17.493-7.253 2.56-14.933 7.253-22.187 6.827-75.52-6.4-151.467-13.227-226.987-20.48-2.133 0-4.693-0.853-6.827-0.853-22.613-1.707-39.253 10.24-40.96 29.867s12.373 33.707 35.413 35.84c45.227 4.267 90.88 8.96 136.107 12.8 65.707 5.547 131.84 10.667 197.547 15.36 26.027 1.707 53.76-21.76 67.413-55.467 9.813-23.893 5.12-46.080-18.347-65.28-49.92-40.107-100.693-78.933-151.040-118.187-23.040-17.92-23.893-23.467-6.4-46.507 58.453-78.080 116.48-156.587 174.933-234.667 27.307-36.693 25.173-50.773-12.373-75.52-5.12 0-9.813 0-14.080 0zM791.893 649.813c-43.093 1.28-76.373-31.573-77.227-75.52-0.853-44.373 29.44-76.8 72.107-77.653 45.227-1.28 77.653 29.44 78.080 73.813 0.427 45.227-29.44 78.080-72.96 79.36zM671.147 737.707c0-72.107-34.133-136.107-87.467-176.64l-235.52-21.76c-72.107 36.693-122.027 111.787-122.027 198.4 0 122.88 99.84 222.293 222.72 222.293 122.453 0 222.293-99.413 222.293-222.293zM592.213 680.533l-50.347 18.347c-2.133-8.533-5.12-16.213-9.813-22.613-5.12-6.4-10.24-11.947-16.213-17.067-5.973-4.267-12.373-8.107-19.2-11.093s-13.653-4.693-20.053-5.547c-17.92-2.987-33.707-0.427-48.64 6.827s-26.88 18.347-36.693 32.853l76.373 12.373 7.253 32.427-97.28-15.787c-1.28 5.547-2.987 11.093-3.84 16.64l-0.853 4.267 99.413 16.213 7.253 32.427-106.667-17.493c0.853 9.387 2.987 17.493 6.4 26.027 3.84 8.533 8.107 16.213 14.080 23.040 5.547 6.827 12.8 12.373 21.333 17.067s17.92 8.107 28.587 9.813c6.827 1.28 13.227 1.707 20.907 1.28s14.507-1.707 21.333-3.84c6.827-2.133 13.653-5.973 20.053-10.24 5.973-4.693 11.947-11.093 17.493-18.773l38.827 37.973c-13.227 17.92-30.293 31.147-52.053 39.253-21.76 8.533-46.080 10.667-73.387 6.4-19.627-2.987-36.267-9.387-51.2-17.92-14.507-8.533-26.88-19.2-37.547-32-10.667-12.373-18.773-26.027-23.893-40.96-5.547-14.507-8.96-29.867-9.813-45.653l-21.76-3.84-7.253-32.427 29.013 4.693 0.427-2.987c1.28-6.827 2.56-12.8 4.267-18.347l-23.467-3.84-8.107-32.427 43.52 7.253c6.827-13.653 15.787-26.027 26.027-36.693 10.24-11.52 22.187-20.48 35.413-27.733 13.227-7.68 27.307-12.8 42.667-15.787s31.573-3.413 47.36-0.853c12.373 2.133 24.32 5.12 35.84 10.667s22.613 11.52 32.853 19.2c10.24 8.107 18.347 16.64 26.027 26.453 6.827 9.387 12.373 20.48 15.36 32.427z" />
<glyph unicode="&#xe94e;" glyph-name="defaulter" d="M677.973-64c-30.72 35.84-61.867 70.827-91.307 107.52-40.96 51.2-80.64 103.253-121.173 154.88-16.64 21.333-21.76 20.48-30.72-4.693-13.227-36.693-25.6-73.387-40.107-109.653-5.12-12.8-13.227-26.88-24.32-34.56-51.627-34.987-104.107-69.12-157.867-100.693-10.667-6.4-30.72-5.547-41.813 0.853-8.107 4.693-12.373 23.893-11.093 35.84 0.853 8.96 11.093 19.627 19.627 25.6 39.253 26.453 78.933 51.627 119.040 76.8 18.347 11.52 30.293 26.027 35.84 47.787 12.373 48.213 27.307 95.573 39.253 143.36 8.533 33.707 26.88 58.88 56.32 77.227 40.533 25.173 80.64 52.053 120.747 78.507 6.4 4.267 10.24 11.52 15.36 17.493-7.253 2.56-14.933 7.253-22.187 6.827-75.52-6.4-151.467-13.227-226.987-20.48-2.133 0-4.693-0.853-6.827-0.853-22.613-1.707-39.253 10.24-40.96 29.867s12.373 33.707 35.413 35.84c45.227 4.267 90.88 8.96 136.107 12.8 65.707 5.547 131.84 10.667 197.547 15.36 26.027 1.707 53.76-21.76 67.413-55.467 9.813-23.893 5.12-46.080-18.347-65.28-49.92-40.107-100.693-78.933-151.040-118.187-23.040-17.92-23.893-23.467-6.4-46.507 58.453-78.080 116.48-156.587 174.933-234.667 27.307-36.693 25.173-50.773-12.373-75.52-5.12 0-9.813 0-14.080 0zM791.893 649.813c-43.093 1.28-76.373-31.573-77.227-75.52-0.853-44.373 29.44-76.8 72.107-77.653 45.227-1.28 77.653 29.44 78.080 73.813 0.427 45.227-29.44 78.080-72.96 79.36zM671.147 737.707c0-72.107-34.133-136.107-87.467-176.64l-235.52-21.76c-72.107 36.693-122.027 111.787-122.027 198.4 0 122.88 99.84 222.293 222.72 222.293 122.453 0 222.293-99.413 222.293-222.293zM592.213 680.533l-50.347 18.347c-2.133-8.533-5.12-16.213-9.813-22.613-5.12-6.4-10.24-11.947-16.213-17.067-5.973-4.267-12.373-8.107-19.2-11.093s-13.653-4.693-20.053-5.547c-17.92-2.987-33.707-0.427-48.64 6.827s-26.88 18.347-36.693 32.853l76.373 12.373 7.253 32.427-97.28-15.787c-1.28 5.547-2.987 11.093-3.84 16.64l-0.853 4.267 99.413 16.213 7.253 32.427-106.667-17.493c0.853 9.387 2.987 17.493 6.4 26.027 3.84 8.533 8.107 16.213 14.080 23.040 5.547 6.827 12.8 12.373 21.333 17.067s17.92 8.107 28.587 9.813c6.827 1.28 13.227 1.707 20.907 1.28s14.507-1.707 21.333-3.84c6.827-2.133 13.653-5.973 20.053-10.24 5.973-4.693 11.947-11.093 17.493-18.773l38.827 37.973c-13.227 17.92-30.293 31.147-52.053 39.253-21.76 8.533-46.080 10.667-73.387 6.4-19.627-2.987-36.267-9.387-51.2-17.92-14.507-8.533-26.88-19.2-37.547-32-10.667-12.373-18.773-26.027-23.893-40.96-5.547-14.507-8.96-29.867-9.813-45.653l-21.76-3.84-7.253-32.427 29.013 4.693 0.427-2.987c1.28-6.827 2.56-12.8 4.267-18.347l-23.467-3.84-8.107-32.427 43.52 7.253c6.827-13.653 15.787-26.027 26.027-36.693 10.24-11.52 22.187-20.48 35.413-27.733 13.227-7.68 27.307-12.8 42.667-15.787s31.573-3.413 47.36-0.853c12.373 2.133 24.32 5.12 35.84 10.667s22.613 11.52 32.853 19.2c10.24 8.107 18.347 16.64 26.027 26.453 6.827 9.387 12.373 20.48 15.36 32.427z" />
<glyph unicode="&#xe94f;" glyph-name="deletedTicket" d="M160.672 85.696h693.248v639.776c0 0-2.016 234.528-349.696 234.528s-343.552-234.528-343.552-234.528v-639.776zM291.328 652.704h170.976v152.256h102.336v-152.256h171.008v-102.336h-171.008v-356.96h-102.336v356.96h-170.976v102.336zM64 61.056v-123.456h899.008v123.456h-899.008z" />
<glyph unicode="&#xe950;" glyph-name="deleteline" d="M354.133 192l-98.133 98.133 157.867 153.6-157.867 157.867 98.133 102.4 157.867-157.867 157.867 153.6 98.133-98.133-157.867-157.867 157.867-153.6-98.133-98.133-157.867 157.867-157.867-157.867zM780.8 507.733l-64-64 59.733-55.467h247.467v119.467h-243.2zM307.2 443.733l-64 64h-243.2v-119.467h251.733l55.467 55.467z" />
<glyph unicode="&#xe951;" glyph-name="delivery" d="M789.333 264.533c-55.467 0-102.4-46.933-102.4-102.4s46.933-102.4 102.4-102.4 102.4 46.933 102.4 102.4c0 59.733-46.933 102.4-102.4 102.4zM789.333 110.933c-29.867 0-51.2 21.333-51.2 51.2s21.333 51.2 51.2 51.2 51.2-21.333 51.2-51.2c0-25.6-25.6-51.2-51.2-51.2zM251.733 264.533c-55.467 0-102.4-46.933-102.4-102.4s46.933-102.4 102.4-102.4c55.467 0 102.4 46.933 102.4 102.4 0 59.733-46.933 102.4-102.4 102.4zM251.733 110.933c-29.867 0-51.2 21.333-51.2 51.2s21.333 51.2 51.2 51.2c29.867 0 51.2-21.333 51.2-51.2 0-25.6-25.6-51.2-51.2-51.2zM1006.933 537.6l-196.267 192c-12.8 12.8-29.867 17.067-46.933 17.067h-98.133v38.4c0 25.6-21.333 51.2-51.2 51.2h-563.2c-29.867 0-51.2-21.333-51.2-51.2v-554.667c0-29.867 25.6-51.2 51.2-51.2h68.267c8.533 64 64 115.2 132.267 115.2 64 0 123.733-51.2 132.267-115.2h268.8c8.533 64 64 115.2 132.267 115.2s128-51.2 136.533-115.2h51.2c29.867 0 51.2 25.6 51.2 51.2v260.267c0 17.067-8.533 34.133-17.067 46.933zM725.333 682.667c0 4.267 4.267 8.533 8.533 8.533h34.133c0 0 4.267 0 4.267-4.267l153.6-145.067c4.267 0 0-12.8-4.267-12.8h-187.733c-8.533 0-8.533 4.267-8.533 8.533v145.067zM311.467 597.333c0 46.933 29.867 85.333 59.733 93.867 4.267 0 4.267 0 8.533 0l98.133 12.8v-51.2c0-46.933-29.867-85.333-59.733-93.867-4.267 0-4.267 0-8.533 0l-98.133-17.067v55.467zM311.467 516.267l46.933 8.533c17.067 4.267 29.867-17.067 29.867-38.4l4.267-29.867-51.2-4.267c-17.067-4.267-29.867 12.8-29.867 38.4v25.6zM149.333 597.333v51.2l85.333 12.8c34.133 4.267 55.467-25.6 55.467-72.533v-51.2l-85.333-12.8c-34.133 0-59.733 29.867-55.467 72.533zM285.867 512v-38.4c0-34.133-21.333-64-42.667-68.267h-4.267l-72.533-8.533v38.4c0 34.133 21.333 64 42.667 68.267h4.267l72.533 8.533z" />

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,8 @@
font-display: block;
}
[class^="icon-"], [class*=" icon-"] {
[class^='icon-'],
[class*=' icon-'] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icon' !important;
speak: never;
@ -26,377 +27,392 @@
}
.icon-100:before {
content: "\e926";
content: '\e926';
}
.icon-Client_unpaid:before {
content: "\e925";
content: '\e925';
}
.icon-Client_unpaid:before {
content: '\e925';
}
.icon-History:before {
content: "\e964";
content: '\e964';
}
.icon-Person:before {
content: "\e984";
content: '\e984';
}
.icon-accessory:before {
content: "\e948";
content: '\e948';
}
.icon-account:before {
content: "\e927";
content: '\e927';
}
.icon-actions:before {
content: "\e928";
content: '\e928';
}
.icon-addperson:before {
content: "\e929";
content: '\e929';
}
.icon-agency:before {
content: "\e92a";
content: '\e92a';
}
.icon-agency:before {
content: '\e92a';
}
.icon-agency-term:before {
content: "\e92b";
content: '\e92b';
}
.icon-albaran:before {
content: "\e92c";
content: '\e92c';
}
.icon-albaran:before {
content: '\e92c';
}
.icon-anonymous:before {
content: "\e92d";
content: '\e92d';
}
.icon-apps:before {
content: "\e92e";
content: '\e92e';
}
.icon-artificial:before {
content: "\e92f";
content: '\e92f';
}
.icon-attach:before {
content: "\e930";
content: '\e930';
}
.icon-barcode:before {
content: "\e932";
content: '\e932';
}
.icon-basket:before {
content: "\e933";
content: '\e933';
}
.icon-basketadd:before {
content: "\e934";
content: '\e934';
}
.icon-bin:before {
content: "\e935";
content: '\e935';
}
.icon-botanical:before {
content: "\e936";
content: '\e936';
}
.icon-bucket:before {
content: "\e937";
content: '\e937';
}
.icon-buscaman:before {
content: "\e938";
content: '\e938';
}
.icon-buyrequest:before {
content: "\e939";
content: '\e939';
}
.icon-calc_volum:before {
content: "\e93a";
content: '\e93a';
}
.icon-calendar:before {
content: "\e940";
content: '\e940';
}
.icon-catalog:before {
content: "\e941";
content: '\e941';
}
.icon-claims:before {
content: "\e942";
content: '\e942';
}
.icon-client:before {
content: "\e943";
content: '\e943';
}
.icon-clone:before {
content: "\e945";
content: '\e945';
}
.icon-columnadd:before {
content: "\e946";
content: '\e946';
}
.icon-columndelete:before {
content: "\e947";
content: '\e947';
}
.icon-components:before {
content: "\e949";
content: '\e949';
}
.icon-consignatarios:before {
content: "\e94b";
content: '\e94b';
}
.icon-control:before {
content: "\e94c";
content: '\e94c';
}
.icon-credit:before {
content: "\e94d";
content: '\e94d';
}
.icon-deaulter:before {
content: "\e94e";
.icon-defaulter:before {
content: '\e94e';
}
.icon-deletedTicket:before {
content: "\e94f";
content: '\e94f';
}
.icon-deleteline:before {
content: "\e950";
content: '\e950';
}
.icon-delivery:before {
content: "\e951";
content: '\e951';
}
.icon-deliveryprices:before {
content: "\e952";
content: '\e952';
}
.icon-details:before {
content: "\e954";
content: '\e954';
}
.icon-dfiscales:before {
content: "\e955";
content: '\e955';
}
.icon-disabled:before {
content: "\e965";
content: '\e965';
}
.icon-doc:before {
content: "\e956";
content: '\e956';
}
.icon-entry:before {
content: "\e958";
content: '\e958';
}
.icon-exit:before {
content: "\e959";
content: '\e959';
}
.icon-eye:before {
content: "\e95a";
content: '\e95a';
}
.icon-fixedPrice:before {
content: "\e95b";
content: '\e95b';
}
.icon-flower:before {
content: "\e95c";
content: '\e95c';
}
.icon-frozen:before {
content: "\e95d";
content: '\e95d';
}
.icon-fruit:before {
content: "\e95e";
content: '\e95e';
}
.icon-funeral:before {
content: "\e95f";
content: '\e95f';
}
.icon-grafana:before {
content: "\e931";
content: '\e931';
}
.icon-grafana:before {
content: '\e931';
}
.icon-greenery:before {
content: "\e91e";
content: '\e91e';
}
.icon-greuge:before {
content: "\e960";
content: '\e960';
}
.icon-grid:before {
content: "\e961";
content: '\e961';
}
.icon-handmade:before {
content: "\e94a";
content: '\e94a';
}
.icon-handmadeArtificial:before {
content: "\e962";
content: '\e962';
}
.icon-headercol:before {
content: "\e963";
content: '\e963';
}
.icon-info:before {
content: "\e966";
content: '\e966';
}
.icon-inventory:before {
content: "\e967";
content: '\e967';
}
.icon-invoice:before {
content: "\e969";
content: '\e969';
}
.icon-invoice-in:before {
content: "\e96a";
content: '\e96a';
}
.icon-invoice-in-create:before {
content: "\e96b";
content: '\e96b';
}
.icon-invoice-out:before {
content: "\e96c";
content: '\e96c';
}
.icon-isTooLittle:before {
content: "\e96e";
content: '\e96e';
}
.icon-item:before {
content: "\e96f";
content: '\e96f';
}
.icon-languaje:before {
content: "\e912";
content: '\e912';
}
.icon-lines:before {
content: "\e971";
content: '\e971';
}
.icon-linesprepaired:before {
content: "\e972";
content: '\e972';
}
.icon-link-to-corrected:before {
content: "\e900";
content: '\e900';
}
.icon-link-to-correcting:before {
content: "\e906";
content: '\e906';
}
.icon-logout:before {
content: "\e90a";
content: '\e90a';
}
.icon-mana:before {
content: "\e974";
content: '\e974';
}
.icon-mandatory:before {
content: "\e975";
content: '\e975';
}
.icon-net:before {
content: "\e976";
content: '\e976';
}
.icon-newalbaran:before {
content: "\e977";
content: '\e977';
}
.icon-niche:before {
content: "\e979";
content: '\e979';
}
.icon-no036:before {
content: "\e97a";
content: '\e97a';
}
.icon-noPayMethod:before {
content: "\e97b";
content: '\e97b';
}
.icon-notes:before {
content: "\e97c";
content: '\e97c';
}
.icon-noweb:before {
content: "\e97e";
content: '\e97e';
}
.icon-onlinepayment:before {
content: "\e97f";
content: '\e97f';
}
.icon-package:before {
content: "\e980";
content: '\e980';
}
.icon-payment:before {
content: "\e982";
content: '\e982';
}
.icon-pbx:before {
content: "\e983";
content: '\e983';
}
.icon-pets:before {
content: "\e985";
content: '\e985';
}
.icon-photo:before {
content: "\e986";
content: '\e986';
}
.icon-plant:before {
content: "\e987";
content: '\e987';
}
.icon-polizon:before {
content: "\e989";
content: '\e989';
}
.icon-preserved:before {
content: "\e98a";
content: '\e98a';
}
.icon-recovery:before {
content: "\e98b";
content: '\e98b';
}
.icon-regentry:before {
content: "\e901";
content: '\e901';
}
.icon-reserva:before {
content: "\e902";
content: '\e902';
}
.icon-revision:before {
content: "\e903";
content: '\e903';
}
.icon-risk:before {
content: "\e904";
content: '\e904';
}
.icon-services:before {
content: "\e905";
content: '\e905';
}
.icon-settings:before {
content: "\e907";
content: '\e907';
}
.icon-shipment:before {
content: "\e908";
content: '\e908';
}
.icon-sign:before {
content: "\e909";
content: '\e909';
}
.icon-sms:before {
content: "\e90b";
content: '\e90b';
}
.icon-solclaim:before {
content: "\e90c";
content: '\e90c';
}
.icon-solunion:before {
content: "\e90d";
content: '\e90d';
}
.icon-splitline:before {
content: "\e90e";
content: '\e90e';
}
.icon-splur:before {
content: "\e90f";
content: '\e90f';
}
.icon-stowaway:before {
content: "\e910";
content: '\e910';
}
.icon-supplier:before {
content: "\e911";
content: '\e911';
}
.icon-supplierfalse:before {
content: "\e913";
content: '\e913';
}
.icon-tags:before {
content: "\e914";
content: '\e914';
}
.icon-tax:before {
content: "\e915";
content: '\e915';
}
.icon-thermometer:before {
content: "\e916";
content: '\e916';
}
.icon-ticket:before {
content: "\e917";
content: '\e917';
}
.icon-ticketAdd:before {
content: "\e918";
content: '\e918';
}
.icon-traceability:before {
content: "\e919";
content: '\e919';
}
.icon-transaction:before {
content: "\e93b";
content: '\e93b';
}
.icon-transaction:before {
content: '\e93b';
}
.icon-treatments:before {
content: "\e91c";
content: '\e91c';
}
.icon-trolley:before {
content: "\e91a";
content: '\e91a';
}
.icon-troncales:before {
content: "\e91b";
content: '\e91b';
}
.icon-unavailable:before {
content: "\e91d";
content: '\e91d';
}
.icon-volume:before {
content: "\e91f";
content: '\e91f';
}
.icon-wand:before {
content: "\e920";
content: '\e920';
}
.icon-web:before {
content: "\e921";
content: '\e921';
}
.icon-wiki:before {
content: "\e922";
content: '\e922';
}
.icon-worker:before {
content: "\e923";
content: '\e923';
}
.icon-zone:before {
content: "\e924";
content: '\e924';
}

View File

@ -26,11 +26,11 @@ export function isValidDate(date) {
* // returns "02/12/2022"
* toDateFormat(new Date(2022, 11, 2));
*/
export function toDateFormat(date) {
export function toDateFormat(date, locale = 'es-ES') {
if (!isValidDate(date)) {
return '';
}
return new Date(date).toLocaleDateString('es-ES', {
return new Date(date).toLocaleDateString(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@ -56,7 +56,7 @@ export function toTimeFormat(date, showSeconds = false) {
if (!isValidDate(date)) {
return '';
}
return new Date(date).toLocaleDateString('es-ES', {
return new Date(date).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit',
second: showSeconds ? '2-digit' : undefined,
@ -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

@ -1,7 +1,8 @@
import toLowerCase from './toLowerCase';
import toDate from './toDate';
import toDateString from './toDateString';
import toDateHour from './toDateHour';
import toDateHourMin from './toDateHourMin';
import toDateHourMinSec from './toDateHourMinSec';
import toRelativeDate from './toRelativeDate';
import toCurrency from './toCurrency';
import toPercentage from './toPercentage';
@ -16,7 +17,8 @@ export {
toDate,
toHour,
toDateString,
toDateHour,
toDateHourMin,
toDateHourMinSec,
toRelativeDate,
toCurrency,
toPercentage,

View File

@ -0,0 +1,11 @@
export default function toDateHourMin(date) {
const dateHour = new Date(date).toLocaleDateString('es-ES', {
timeZone: 'Europe/Madrid',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
return dateHour;
}

View File

@ -1,4 +1,4 @@
export default function toDateHour(date) {
export default function toDateHourMinSec(date) {
const dateHour = new Date(date).toLocaleDateString('es-ES', {
timeZone: 'Europe/Madrid',
year: 'numeric',

View File

@ -28,6 +28,7 @@ globals:
reset: Reset
close: Close
cancel: Cancel
clone: Clone
confirm: Confirm
assign: Assign
back: Back
@ -67,6 +68,7 @@ globals:
reason: reason
noResults: No results
system: System
notificationSent: Notification sent
warehouse: Warehouse
company: Company
fieldRequired: Field required
@ -82,6 +84,7 @@ globals:
selectFile: Select a file
copyClipboard: Copy on clipboard
salesPerson: SalesPerson
send: Send
code: Code
pageTitles:
summary: Summary
@ -90,6 +93,7 @@ globals:
parkingList: Parkings list
created: Created
worker: Worker
now: Now
errors:
statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred
@ -150,6 +154,13 @@ customer:
creditContracts: Credit contracts
creditOpinion: Credit opinion
others: Others
samples: Samples
consumption: Consumption
mandates: Mandates
contacts: Contacts
webPayment: Web payment
fileManagement: File management
unpaid: Unpaid
list:
phone: Phone
email: Email
@ -160,14 +171,18 @@ customer:
customerId: Claim ID
salesPerson: Sales person
credit: Credit
risk: Risk
securedCredit: Secured credit
payMethod: Pay method
debt: Debt
isDisabled: Customer is disabled
isFrozen: Customer is frozen
isFrozen: Customer frozen
hasDebt: Customer has debt
notChecked: Customer not checked
isDisabled: Customer inactive
notChecked: Customer no checked
webAccountInactive: Web account inactive
noWebAccess: Web access is disabled
businessType: Business type
passwordRequirements: 'The password must have at least { length } length characters, {nAlpha} alphabetic characters, {nUpper} capital letters, {nDigits} digits and {nPunct} symbols (Ex: $%&.)\n'
businessTypeFk: Business type
summary:
basicData: Basic data
@ -227,17 +242,20 @@ customer:
recoverySince: Recovery since
businessType: Business Type
city: City
descriptorInfo: Invoices minus payments plus orders not yet
rating: Rating
recommendCredit: Recommended credit
basicData:
socialName: Fiscal name
businessType: Business type
contact: Contact
youCanSaveMultipleEmails: You can save multiple emails
email: Email
phone: Phone
mobile: Mobile
salesPerson: Sales person
contactChannel: Contact channel
previousClient: Previous client
extendedList:
tableVisibleColumns:
id: Identifier
@ -467,6 +485,12 @@ ticket:
weight: Weight
goTo: Go to
summaryAmount: Summary
create:
client: Client
address: Address
landed: Landed
warehouse: Warehouse
agency: Agency
claim:
pageTitles:
claims: Claims
@ -789,6 +813,7 @@ worker:
pbx: Private Branch Exchange
log: Log
calendar: Calendar
timeControl: Time control
list:
name: Name
email: Email
@ -1095,8 +1120,12 @@ item:
list: List
diary: Diary
tags: Tags
create: Create
buyRequest: Buy requests
fixedPrice: Fixed prices
wasteBreakdown: Waste breakdown
itemCreate: New item
log: Log
descriptor:
item: Item
buyer: Buyer
@ -1124,6 +1153,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
@ -1131,8 +1169,68 @@ 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'
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
@ -67,6 +68,7 @@ globals:
reason: motivo
noResults: Sin resultados
system: Sistema
notificationSent: Notificación enviada
warehouse: Almacén
company: Empresa
fieldRequired: Campo requerido
@ -82,6 +84,7 @@ globals:
selectFile: Seleccione un fichero
copyClipboard: Copiar en portapapeles
salesPerson: Comercial
send: Enviar
code: Código
pageTitles:
summary: Resumen
@ -90,6 +93,7 @@ globals:
parkingList: Listado de parkings
created: Fecha creación
worker: Trabajador
now: Ahora
errors:
statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor
@ -149,6 +153,13 @@ customer:
creditContracts: Contratos de crédito
creditOpinion: Opinión de crédito
others: Otros
samples: Plantillas
consumption: Consumo
mandates: Mandatos
contacts: Contactos
webPayment: Pago web
fileManagement: Gestión documental
unpaid: Impago
list:
phone: Teléfono
email: Email
@ -158,14 +169,18 @@ customer:
customerId: ID cliente
salesPerson: Comercial
credit: Crédito
risk: Riesgo
securedCredit: Crédito asegurado
payMethod: Método de pago
debt: Riesgo
isDisabled: El cliente está desactivado
isFrozen: El cliente está congelado
hasDebt: El cliente tiene riesgo
notChecked: El cliente no está comprobado
isFrozen: Cliente congelado
hasDebt: Cliente con riesgo
isDisabled: Cliente inactivo
notChecked: Cliente no comprobado
webAccountInactive: Sin acceso web
noWebAccess: El acceso web está desactivado
businessType: Tipo de negocio
passwordRequirements: 'La contraseña debe tener al menos { length } caracteres de longitud, {nAlpha} caracteres alfabéticos, {nUpper} letras mayúsculas, {nDigits} dígitos y {nPunct} símbolos (Ej: $%&.)'
businessTypeFk: Tipo de negocio
summary:
basicData: Datos básicos
@ -225,17 +240,20 @@ customer:
recoverySince: Recobro desde
businessType: Tipo de negocio
city: Población
descriptorInfo: Facturas menos recibos mas pedidos sin facturar
rating: Clasificación
recommendCredit: Crédito recomendado
basicData:
socialName: Nombre fiscal
businessType: Tipo de negocio
contact: Contacto
youCanSaveMultipleEmails: Puede guardar varios correos electrónicos encadenándolos mediante comas sin espacios{','} ejemplo{':'} user{'@'}dominio{'.'}com, user2{'@'}dominio{'.'}com siendo el primer correo electrónico el principal
email: Email
phone: Teléfono
mobile: Móvil
salesPerson: Comercial
contactChannel: Canal de contacto
previousClient: Cliente anterior
extendedList:
tableVisibleColumns:
id: Identificador
@ -465,6 +483,12 @@ ticket:
weight: Peso
goTo: Ir a
summaryAmount: Resumen
create:
client: Cliente
address: Dirección
landed: F. entrega
warehouse: Almacén
agency: Agencia
claim:
pageTitles:
claims: Reclamaciones
@ -787,6 +811,7 @@ worker:
pbx: Centralita
log: Historial
calendar: Calendario
timeControl: Control de horario
list:
name: Nombre
email: Email
@ -1094,8 +1119,15 @@ 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
descriptor:
item: Artículo
buyer: Comprador
@ -1123,6 +1155,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
@ -1130,8 +1171,68 @@ 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'
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

@ -9,7 +9,7 @@ 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 VnSelect from 'src/components/common/VnSelect.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import { useArrayData } from 'composables/useArrayData';
@ -302,7 +302,7 @@ async function importToNewRefundTicket() {
</template>
<template #body-cell-destination="{ row }">
<QTd>
<VnSelectFilter
<VnSelect
v-model="row.claimDestinationFk"
:options="destinationTypes"
option-label="description"
@ -344,7 +344,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"
@ -418,7 +418,7 @@ async function importToNewRefundTicket() {
</QItem>
</QCardSection>
<QItemSection>
<VnSelectFilter
<VnSelect
class="q-pa-sm"
v-model="claimDestinationFk"
:options="destinationTypes"

View File

@ -1,46 +1,15 @@
<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();
</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')"
<VnCard
data-key="Claim"
base-url="Claims"
:descriptor="ClaimDescriptor"
searchbar-data-key="ClaimList"
searchbar-url="Claims/filter"
searchbar-label="Search claim"
searchbar-info="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>
</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

@ -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

@ -121,11 +121,6 @@ async function fetchMana() {
mana.value = response.data;
}
async function updateQuantity({ id, quantity }) {
if (!id) return;
await axios.patch(`ClaimBeginnings/${id}`, { quantity });
}
async function updateDiscount({ saleFk, discount, canceller }) {
const body = { salesIds: [saleFk], newDiscount: discount };
const claimId = claim.value.ticketFk;
@ -155,6 +150,10 @@ function showImportDialog() {
})
.onOk(() => claimLinesForm.value.reload());
}
function saveWhenHasChanges() {
claimLinesForm.value.getChanges().updates && claimLinesForm.value.onSubmit();
}
</script>
<template>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()">
@ -181,8 +180,7 @@ function showImportDialog() {
@on-fetch="onFetchClaim"
auto-load
/>
<div class="column items-center">
<div class="list">
<div class="q-pa-md">
<CrudModel
data-key="ClaimLines"
ref="claimLinesForm"
@ -195,6 +193,7 @@ function showImportDialog() {
:default-save="false"
:default-reset="false"
auto-load
:limit="0"
>
<template #body="{ rows }">
<QTable
@ -206,26 +205,15 @@ function showImportDialog() {
v-model:selected="selected"
:grid="$q.screen.lt.md"
>
<template #body-cell-claimed="{ row, value }">
<template #body-cell-claimed="{ row }">
<QTd auto-width align="right" class="text-primary">
<span>{{ value }}</span>
<QPopupEdit
v-model="row.quantity"
v-slot="scope"
:title="t('Claimed quantity')"
@update:model-value="updateQuantity(row)"
buttons
>
<QInput
v-model="scope.value"
v-model="row.quantity"
type="number"
dense
autofocus
@keyup.enter="scope.set"
@focus="($event) => $event.target.select()"
@keyup.enter="saveWhenHasChanges()"
@blur="saveWhenHasChanges()"
/>
</QPopupEdit>
</QTd>
</template>
<template #body-cell-description="{ row, value }">
@ -272,32 +260,18 @@ function showImportDialog() {
</QItemLabel>
</QItemSection>
<QItemSection side>
<template
v-if="column.name === 'claimed'"
>
<template v-if="column.name === 'claimed'">
<QItemLabel class="text-primary">
{{ column.value }}
<QPopupEdit
v-model="props.row.quantity"
v-slot="scope"
:title="t('Claimed quantity')"
@update:model-value="
updateQuantity(props.row)
"
buttons
>
<QInput
v-model="scope.value"
v-model="props.row.quantity"
type="number"
dense
autofocus
@keyup.enter="scope.set"
@focus="
($event) =>
$event.target.select()
@keyup.enter="
saveWhenHasChanges()
"
@blur="saveWhenHasChanges()"
/>
</QPopupEdit>
</QItemLabel>
</template>
<template
@ -336,7 +310,6 @@ function showImportDialog() {
</template>
</CrudModel>
</div>
</div>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="showImportDialog()" />

View File

@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import { toDate, toCurrency, toPercentage } from 'filters/index';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import axios from 'axios';
defineEmits([...useDialogPluginComponent.emits]);
@ -34,6 +35,7 @@ const columns = computed(() => [
label: t('Quantity'),
field: (row) => row.quantity,
sortable: true,
default: 0,
},
{
name: 'description',
@ -74,7 +76,6 @@ async function importLines() {
const body = sales.map((row) => ({
claimFk: route.params.id,
saleFk: row.saleFk,
quantity: row.quantity,
}));
canceller = new AbortController();
@ -118,7 +119,6 @@ function cancel() {
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<QTable
class="my-sticky-header-table"
:columns="columns"
:rows="claimableSales"
row-key="saleFk"
@ -126,7 +126,14 @@ function cancel() {
v-model:selected="selected"
square
flat
/>
>
<template #body-cell-description="{ row, value }">
<QTd auto-width align="right" class="link">
{{ value }}
<ItemDescriptorProxy :id="row.itemFk"></ItemDescriptorProxy>
</QTd>
</template>
</QTable>
<QSeparator />
<QCardActions align="right">
<QBtn :label="t('globals.cancel')" color="primary" flat @click="cancel" />
@ -148,33 +155,6 @@ function cancel() {
}
</style>
<style lang="scss">
.my-sticky-header-table {
height: 400px;
thead tr th {
position: sticky;
z-index: 1;
}
thead tr:first-child th {
/* this is when the loading indicator appears */
top: 0;
}
&.q-table--loading thead tr:last-child th {
/* height of all previous header rows */
top: 48px;
}
// /* prevent scrolling behind sticky top row on focus */
tbody {
/* height of all previous header rows */
scroll-margin-top: 48px;
}
}
</style>
<i18n>
es:
Available sales lines: Líneas de venta disponibles

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: {
@ -38,10 +38,11 @@ const body = {
</script>
<template>
<VnNotes
style="overflow-y: auto"
:add-note="$props.addNote"
url="claimObservations"
:add-note="$props.addNote"
:filter="claimFilter"
:body="body"
v-bind="$attrs"
style="overflow-y: auto"
/>
</template>

View File

@ -222,8 +222,8 @@ function openDialog(dmsId) {
</template>
</VnLv>
<VnLv
:label="t('claim.summary.pickup')"
:value="t(`claim.summary.${claim.pickup}`)"
:label="t('claim.basicData.pickup')"
:value="t(`claim.basicData.${claim.pickup}`)"
/>
</QCard>
<QCard class="vn-three">
@ -280,6 +280,48 @@ function openDialog(dmsId) {
</template>
</QTable>
</QCard>
<QCard class="vn-two" v-if="claimDms.length > 0">
<VnTitle
:url="`#/claim/${entityId}/photos`"
:text="t('claim.summary.photos')"
/>
<div class="container">
<div
class="multimedia-container"
v-for="(media, index) of claimDms"
:key="index"
>
<div class="relative-position">
<QIcon
name="play_circle"
color="primary"
size="xl"
class="absolute-center zindex"
v-if="media.isVideo"
@click.stop="openDialog(media.dmsFk)"
>
<QTooltip>Video</QTooltip>
</QIcon>
<QCard class="multimedia relative-position">
<QImg
:src="media.url"
class="rounded-borders cursor-pointer fit"
@click="openDialog(media.dmsFk)"
v-if="!media.isVideo"
>
</QImg>
<video
:src="media.url"
class="rounded-borders cursor-pointer fit"
muted="muted"
v-if="media.isVideo"
@click="openDialog(media.dmsFk)"
/>
</QCard>
</div>
</div>
</div>
</QCard>
<QCard class="vn-two" v-if="developments.length > 0">
<VnTitle
:url="claimUrl + 'development'"
@ -302,49 +344,6 @@ function openDialog(dmsId) {
</template>
</QTable>
</QCard>
<QCard class="vn-max" v-if="claimDms.length > 0">
<VnTitle
:url="`#/claim/${entityId}/photos`"
:text="t('claim.summary.photos')"
/>
<div class="container">
<div
class="multimedia-container"
v-for="(media, index) of claimDms"
:key="index"
>
<div class="relative-position">
<QIcon
name="play_circle"
color="primary"
size="xl"
class="absolute-center zindex"
v-if="media.isVideo"
@click.stop="openDialog(media.dmsFk)"
>
<QTooltip>Video</QTooltip>header
</QIcon>
<QCard class="multimedia relative-position">
<QImg
:src="media.url"
class="rounded-borders cursor-pointer fit"
@click="openDialog(media.dmsFk)"
v-if="!media.isVideo"
>
</QImg>
<video
:src="media.url"
class="rounded-borders cursor-pointer fit"
muted="muted"
v-if="media.isVideo"
@click="openDialog(media.dmsFk)"
/>
</QCard>
</div>
</div>
</div>
</QCard>
<QCard class="vn-max">
<VnTitle :url="claimUrl + 'action'" :text="t('claim.summary.actions')" />
<div id="slider-container" class="q-px-xl q-py-md">

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()"

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

@ -1,18 +1,21 @@
<script setup>
import { ref } from 'vue';
import { onBeforeMount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import FetchData from 'components/FetchData.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const addresses = ref([]);
const client = ref(null);
const provincesLocation = ref([]);
const consigneeFilter = {
const addressFilter = {
fields: [
'id',
'isDefaultAddress',
@ -44,6 +47,42 @@ const consigneeFilter = {
],
};
onBeforeMount(() => {
const { id } = route.params;
getAddressesData(id);
getClientData(id);
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
getAddressesData(newValue);
getClientData(newValue);
}
);
const getAddressesData = async (id) => {
try {
const { data } = await axios.get(`Clients/${id}/addresses`, {
params: { filter: JSON.stringify(addressFilter) },
});
addresses.value = data;
sortAddresses();
} catch (error) {
return error;
}
};
const getClientData = async (id) => {
try {
const { data } = await axios.get(`Clients/${id}`);
client.value = data;
} catch (error) {
return error;
}
};
const setProvince = (provinceFk) => {
const result = provincesLocation.value.filter(
(province) => province.id === provinceFk
@ -51,16 +90,38 @@ const setProvince = (provinceFk) => {
return result[0]?.name || '';
};
const toCustomerConsigneeCreate = () => {
router.push({ name: 'CustomerConsigneeCreate' });
const isDefaultAddress = (address) => {
return client?.value?.defaultAddressFk === address.id ? 1 : 0;
};
const toCustomerConsigneeEdit = (consigneeId) => {
const setDefault = (address) => {
const url = `Clients/${route.params.id}`;
const payload = { defaultAddressFk: address.id };
axios.patch(url, payload).then((res) => {
if (res.data) {
client.value.defaultAddressFk = res.data.defaultAddressFk;
sortAddresses();
}
});
};
const sortAddresses = () => {
if (!client.value || !addresses.value) return;
addresses.value = addresses.value.sort((a, b) => {
return isDefaultAddress(b) - isDefaultAddress(a);
});
};
const toCustomerAddressCreate = () => {
router.push({ name: 'CustomerAddressCreate' });
};
const toCustomerAddressEdit = (addressId) => {
router.push({
name: 'CustomerConsigneeEdit',
name: 'CustomerAddressEdit',
params: {
id: route.params.id,
consigneeId,
addressId,
},
});
};
@ -73,26 +134,41 @@ const toCustomerConsigneeEdit = (consigneeId) => {
url="Provinces/location"
/>
<QCard class="q-pa-lg">
<VnPaginate
data-key="CustomerConsignees"
:url="`Clients/${route.params.id}/addresses`"
order="id"
auto-load
:filter="consigneeFilter"
>
<template #body="{ rows }">
<QCard
v-for="(item, index) in rows"
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg" v-if="addresses.length">
<QCardSection>
<div
v-for="(item, index) in addresses"
:key="index"
class="address-card"
:class="{
'consignees-card': true,
'q-mb-md': index < rows.length - 1,
'q-mb-md': index < addresses.length - 1,
'item-disabled': !item.isActive,
}"
@click="toCustomerConsigneeEdit(item.id)"
@click="toCustomerAddressEdit(item.id)"
>
<div class="q-ml-xs q-mr-md flex items-center">
<QIcon name="star" size="md" color="primary" />
<QIcon
:style="{
'font-variation-settings': `'FILL' ${isDefaultAddress(
item
)}`,
}"
color="primary"
name="star"
size="md"
@click.stop="!isDefaultAddress(item) && setDefault(item)"
>
<QTooltip>
{{
t(
isDefaultAddress(item)
? 'Default address'
: 'Set as default'
)
}}
</QTooltip>
</QIcon>
</div>
<div>
<div class="text-weight-bold q-mb-sm">
@ -136,12 +212,13 @@ const toCustomerConsigneeEdit = (consigneeId) => {
<div>{{ observation.description }}</div>
</div>
</div>
</div>
</QCardSection>
</QCard>
</template>
</VnPaginate>
</QCard>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerConsigneeCreate()" color="primary" fab icon="add" />
<QBtn @click.stop="toCustomerAddressCreate()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New consignee') }}
</QTooltip>
@ -149,8 +226,8 @@ const toCustomerConsigneeEdit = (consigneeId) => {
</template>
<style lang="scss" scoped>
.consignees-card {
border: 2px solid var(--vn-accent-color);
.address-card {
border: 2px solid var(--vn-light-gray);
border-radius: 10px;
padding: 10px;
display: flex;
@ -160,6 +237,10 @@ const toCustomerConsigneeEdit = (consigneeId) => {
background-color: var(--vn-accent-color);
}
}
.item-disabled {
opacity: 0.6;
}
</style>
<i18n>
@ -167,4 +248,6 @@ es:
Is equalizated: Recargo de equivalencia
Is logiflora allowed: Compra directa en Holanda
New consignee: Nuevo consignatario
Default address: Consignatario predeterminado
Set as default: Establecer como predeterminado
</i18n>

View File

@ -1,145 +1,102 @@
<script setup>
import { computed, ref } from 'vue';
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { date, QCheckbox, QBtn, useQuasar } from 'quasar';
import axios from 'axios';
import { QCheckbox, QBtn, useQuasar } from 'quasar';
import { toCurrency } from 'src/filters';
import { toCurrency, toDate, toDateHourMin } from 'src/filters';
import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore';
import { useValidator } from 'src/composables/useValidator';
import { usePrintService } from 'src/composables/usePrintService';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.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 { sendEmail } = usePrintService();
const { t } = useI18n();
const route = useRoute();
const { validate } = useValidator();
const quasar = useQuasar();
const route = useRoute();
const state = useState();
const stateStore = useStateStore();
const user = state.getUser();
const clientRisks = ref(null);
const companiesOptions = ref([]);
const companyId = ref(442);
const rows = ref(null);
const workerId = ref(0);
const receiptsRef = ref(null);
const clientRisksRef = ref(null);
const companiesOptions = ref([]);
const companyId = ref(null);
const receiptsRef = ref(null);
const receiptsData = ref([]);
const filterCompanies = { order: ['code'] };
const params = {
clientId: `${route.params.id}`,
companyId: companyId.value,
filter: { limit: 20 },
const userParams = {
clientId: route.params.id,
companyId: user.value.companyFk,
};
const filter = {
include: { relation: 'company', scope: { fields: ['code'] } },
where: { clientFk: `${route.params.id}`, companyFk: companyId.value },
};
const tableColumnComponents = {
payed: {
component: 'span',
props: () => {},
event: () => {},
},
created: {
component: 'span',
props: () => {},
event: () => {},
},
userName: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => {
workerId.value = prop.row.clientFk;
},
},
description: {
component: 'span',
props: () => {},
event: () => {},
},
bankFk: {
component: 'span',
props: () => {},
event: () => {},
},
debit: {
component: 'span',
props: () => {},
event: () => {},
},
credit: {
component: 'span',
props: () => {},
event: () => {},
},
balance: {
component: 'span',
props: () => {},
event: () => {},
},
isConciliate: {
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
event: () => {},
},
where: { clientFk: route.params.id, companyFk: user.value.companyFk },
};
const columns = computed(() => [
{
align: 'left',
field: 'payed',
format: (value) => date.formatDate(value, 'DD/MM/YYYY'),
format: (value) => toDate(value),
label: t('Date'),
name: 'payed',
name: 'date',
},
{
align: 'left',
field: 'created',
format: (value) => date.formatDate(value, 'DD/MM/YYYY hh:mm'),
format: (value) => toDateHourMin(value),
label: t('Creation date'),
name: 'created',
name: 'creationDate',
},
{
align: 'left',
field: 'userName',
label: t('Employee'),
name: 'userName',
name: 'employee',
},
{
align: 'left',
field: 'description',
label: t('Reference'),
name: 'description',
name: 'reference',
},
{
align: 'left',
field: 'bankFk',
label: t('Bank'),
name: 'bankFk',
name: 'bank',
},
{
align: 'left',
align: 'right',
field: 'debit',
format: (value) => value && toCurrency(value),
label: t('Debit'),
name: 'debit',
},
{
align: 'left',
align: 'right',
field: 'credit',
format: (value) => toCurrency(value),
format: (value) => value && toCurrency(value),
label: t('Havings'),
name: 'credit',
name: 'havings',
},
{
align: 'left',
field: (value) => value.debit - value.credit,
format: (value) => toCurrency(value),
align: 'right',
field: 'balance',
format: (value) => value && toCurrency(value),
label: t('Balance'),
name: 'balance',
},
@ -147,16 +104,58 @@ const columns = computed(() => [
align: 'left',
field: 'isConciliate',
label: t('Conciliated'),
name: 'isConciliate',
name: 'conciliated',
},
{
align: 'left',
field: 'totalWithVat',
label: '',
name: 'actions',
},
]);
const getData = () => {
onBeforeMount(() => {
stateStore.rightDrawer = true;
companyId.value = user.value.companyFk;
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
userParams.clientId = newValue;
filter.where.clientFk = newValue;
getData();
}
);
const getData = () => {
receiptsRef.value?.fetch();
clientRisksRef.value?.fetch();
};
const getCurrentBalance = () => {
const currentBalance = clientRisks.value.find((balance) => {
return balance.companyFk === companyId.value;
});
return currentBalance && currentBalance.amount;
};
const onFetch = (balances) => {
balances.forEach((balance, index) => {
if (index === 0) {
balance.balance = getCurrentBalance();
} else {
let previousBalance = balances[index - 1];
balance.balance =
previousBalance.balance -
(previousBalance.debit - previousBalance.credit);
}
});
receiptsData.value = balances;
};
const showNewPaymentDialog = () => {
quasar.dialog({
component: CustomerNewPayment,
@ -169,25 +168,29 @@ const showNewPaymentDialog = () => {
};
const updateCompanyId = (id) => {
if (id) companyId.value = id;
if (id) {
companyId.value = id;
userParams.companyId = id;
filter.where.companyFk = id;
}
getData();
};
const saveFieldValue = async (row) => {
try {
const payload = { description: row.description };
await axios.patch(`Receipts/${row.id}`, payload);
} catch (err) {
return err;
}
};
const sendEmailAction = () => {
sendEmail(`Suppliers/${route.params.id}/campaign-metrics-email`);
};
</script>
<template>
<FetchData
:filter="filterCompanies"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
url="Companies"
/>
<FetchData
:params="params"
@on-fetch="(data) => (rows = data)"
auto-load
ref="receiptsRef"
url="Receipts/filter"
/>
<FetchData
:filter="filter"
@on-fetch="(data) => (clientRisks = data)"
@ -195,40 +198,125 @@ const updateCompanyId = (id) => {
ref="clientRisksRef"
url="ClientRisks"
/>
<FetchData
:filter="filterCompanies"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
url="Companies"
/>
<VnPaginate
auto-load
data-key="CustomerBalance"
url="Receipts/filter"
:user-params="userParams"
ref="receiptsRef"
@on-fetch="onFetch"
>
<template #body="{ rows }">
<QTable
:columns="columns"
:no-data-label="t('globals.noResults')"
:rows-per-page-options="[0]"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<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 !== 'isConciliate'">
{{ props.value }}
<template #body-cell-employee="{ row }">
<QTd auto-width @click.stop>
<QBtn color="blue" flat no-caps>{{ row.userName }}</QBtn>
<WorkerDescriptorProxy :id="row.clientFk" />
</QTd>
</template>
<WorkerDescriptorProxy :id="workerId" />
</component>
</QTr>
<template #body-cell-reference="{ row }">
<QTd auto-width @click.stop v-if="row.isInvoice">
<QBtn color="blue" dense flat>
{{ t('bill', { ref: row.description }) }}
</QBtn>
<InvoiceOutDescriptorProxy :id="row.id" />
</QTd>
<QTd v-else>
<VnInput
@keyup.enter="saveFieldValue(row)"
autofocus
clearable
dense
v-model="row.description"
/>
</QTd>
</template>
<template #body-cell-conciliated="{ row }">
<QTd align="center">
<QCheckbox :model-value="row.isConciliate === 1" disable />
</QTd>
</template>
<template #body-cell-actions="{ row }">
<QTd align="center">
<QIcon
@click.stop="showDialog = true"
class="q-ml-md"
color="primary"
name="outgoing_mail"
size="sm"
style="font-variation-settings: 'FILL' 1"
v-if="row.isCompensation"
>
<QTooltip>
{{ t('Send compensation') }}
</QTooltip>
</QIcon>
<QDialog v-model="showDialog">
<QCard class="q-pa-sm">
<QCardSection>
<span
ref="closeButton"
class="flex justify-end color-vn-label"
v-close-popup
>
<QIcon name="close" size="sm" />
</span>
<div class="text-h6">
{{ t('Send compensation') }}
</div>
</QCardSection>
<QCardSection>
<div>
{{
t(
'Do you want to report compensation to the client by mail?'
)
}}
</div>
</QCardSection>
<QCardActions class="flex justify-end q-mb-sm">
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="t('globals.save')"
@click="sendEmailAction"
class="q-ml-sm"
color="primary"
/>
</QCardActions>
</QCard>
</QDialog>
</QTd>
</template>
</QTable>
<h5 class="flex justify-center label-color" v-else>
{{ t('globals.noResults') }}
</h5>
</template>
</VnPaginate>
<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)"
@ -236,10 +324,11 @@ const updateCompanyId = (id) => {
option-label="code"
option-value="id"
v-model="companyId"
:rules="validate('entry.companyFk')"
/>
</div>
<QCard class="q-ma-md q-pa-md q-mt-lg" v-if="rows?.length">
<QCard class="q-ma-md q-pa-md q-mt-lg" v-if="receiptsData?.length">
<QCardSection>
<div class="flex justify-center text-subtitle1 text-bold">
{{ t('Total by company') }}
@ -265,6 +354,8 @@ const updateCompanyId = (id) => {
</template>
<i18n>
en:
bill: 'N/INV {ref}'
es:
Company: Empresa
Total by company: Total por empresa
@ -273,9 +364,12 @@ es:
Creation date: Fecha de creación
Employee: Empleado
Reference: Referencia
bill: 'N/FRA {ref}'
Bank: Caja
Debit: Debe
Havings: Haber
Balance: Balance
Conciliated: Conciliado
Send compensation: Enviar compensación
Do you want to report compensation to the client by mail?: ¿Desea informar de la compensación al cliente por correo?
</i18n>

View File

@ -60,77 +60,93 @@ const filterOptions = {
@on-fetch="(data) => (businessTypes = data)"
auto-load
/>
<fetch-data
:filter="filter"
@on-fetch="(data) => (clients = data)"
auto-load
url="Clients"
/>
<FormModel :url="`Clients/${route.params.id}`" model="customer" auto-load>
<FormModel :url="`Clients/${route.params.id}`" auto-load model="customer">
<template #form="{ data, validate, filter }">
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
v-model="data.socialName"
:label="t('customer.basicData.socialName')"
:label="t('Comercial name')"
:rules="validate('client.socialName')"
autofocus
clearable
v-model="data.name"
/>
<QSelect
v-model="data.businessTypeFk"
:options="businessTypes"
option-value="code"
option-label="description"
emit-value
:label="t('customer.basicData.businessType')"
map-options
:rules="validate('client.businessTypeFk')"
: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">
<VnInput
v-model="data.contact"
:label="t('customer.basicData.contact')"
:rules="validate('client.contact')"
clearable
v-model="data.contact"
/>
<VnInput
v-model="data.email"
type="email"
: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">
<VnInput
v-model="data.phone"
:label="t('customer.basicData.phone')"
:rules="validate('client.phone')"
clearable
v-model="data.phone"
/>
<VnInput
v-model="data.mobile"
:label="t('customer.basicData.mobile')"
:rules="validate('client.mobile')"
clearable
v-model="data.mobile"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<QSelect
v-model="data.salesPersonFk"
:options="workers"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.salesPerson')"
map-options
use-input
@filter="(value, update) => filter(value, update, filterOptions)"
:rules="validate('client.salesPersonFk')"
:input-debounce="0"
:label="t('customer.basicData.salesPerson')"
:options="workers"
:rules="validate('client.salesPersonFk')"
@filter="(value, update) => filter(value, update, filterOptions)"
emit-value
map-options
option-label="name"
option-value="id"
use-input
v-model="data.salesPersonFk"
>
<template #prepend>
<QAvatar color="orange">
<QImg
v-if="data.salesPersonFk"
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
v-if="data.salesPersonFk"
/>
</QAvatar>
</template>
@ -147,6 +163,35 @@ const filterOptions = {
:input-debounce="0"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<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>
</template>
<i18n>
es:
In case of a company succession, specify the grantor company: En el caso de que haya habido una sucesión de empresa, indicar la empresa cedente
Comercial name: Nombre comercial
</i18n>

View File

@ -1,15 +1,13 @@
<script setup>
import { onMounted, ref } from 'vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
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';
@ -26,8 +24,9 @@ const filter = {
limit: 30,
};
const getBankEntities = () => {
const getBankEntities = (data, formData) => {
bankEntitiesRef.value.fetch();
formData.bankEntityFk = Number(data.id);
};
</script>
@ -49,7 +48,7 @@ const getBankEntities = () => {
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<VnSelect
:label="t('Billing data')"
:options="payMethods"
hide-selected
@ -57,15 +56,11 @@ const getBankEntities = () => {
option-value="id"
v-model="data.payMethod"
/>
<VnInput
:label="t('Due day')"
:rules="validate('client.socialName')"
v-model="data.dueDay"
/>
<VnInput :label="t('Due day')" clearable v-model="data.dueDay" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput :label="t('IBAN')" v-model="data.iban">
<VnInput :label="t('IBAN')" clearable v-model="data.iban">
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{ t('components.iban_tooltip') }}</QTooltip>
@ -83,14 +78,15 @@ const getBankEntities = () => {
v-model="data.bankEntityFk"
>
<template #form>
<CreateBankEntityForm @on-data-saved="getBankEntities()" />
<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
>{{ scope.opt.bic }} {{ scope.opt.name }}</QItemLabel
>
</QItemSection>
</QItem>
@ -100,10 +96,7 @@ const getBankEntities = () => {
<VnRow class="row q-gutter-md q-mb-md">
<QCheckbox :label="t('Received LCR')" v-model="data.hasLcr" />
<QCheckbox
:label="t('VNL core received')"
v-model="data.hasCoreVnl"
/>
<QCheckbox :label="t('VNL core received')" v-model="data.hasCoreVnl" />
<QCheckbox :label="t('VNL B2B received')" v-model="data.hasSepaVnl" />
</VnRow>
</template>

View File

@ -1,45 +1,15 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
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 route = useRoute();
const { t } = useI18n();
</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')"
<VnCard
data-key="Client"
base-url="Clients"
:descriptor="CustomerDescriptor"
searchbar-data-key="CustomerList"
searchbar-url="Clients/filter"
searchbar-label="Search customer"
searchbar-info="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>
</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

@ -0,0 +1,16 @@
<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
<template>
<h5 class="flex justify-center color-vn-label">
{{ t('Enter a new search') }}
</h5>
</template>
<i18n>
es:
Enter a new search: Introduce una nueva búsqueda
</i18n>

View File

@ -0,0 +1,87 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CrudModel from 'components/CrudModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
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">
<QPage class="card-width q-pa-lg">
<CrudModel
:data-required="{ clientFk: route.params.id }"
:default-remove="false"
:filter="{
fields: ['id', 'name', 'phone', 'clientFk'],
where: { clientFk: route.params.id },
}"
data-key="CustomerContacts"
model="CustomerContacts"
ref="customerContactsRef"
url="ClientContacts"
>
<template #body="{ rows }">
<QCard class="q-pl-lg q-py-md">
<VnRow
v-for="(row, index) in rows"
:key="index"
class="row q-gutter-md q-mb-md"
>
<div class="col">
<VnInput :label="t('Name')" v-model="row.name" />
</div>
<div class="col">
<VnInput :label="t('Phone')" v-model="row.phone" />
</div>
<div class="col-1 row justify-center items-center">
<QIcon
@click="customerContactsRef.remove([row])"
class="cursor-pointer"
color="primary"
name="delete"
size="sm"
>
<QTooltip>
{{ t('Remove contact') }}
</QTooltip>
</QIcon>
</div>
</VnRow>
<VnRow>
<QIcon
@click="customerContactsRef.insert()"
class="cursor-pointer"
color="primary"
name="add"
size="sm"
>
<QTooltip>
{{ t('Add contact') }}
</QTooltip>
</QIcon>
</VnRow>
</QCard>
</template>
</CrudModel>
</QPage>
</div>
</template>
<i18n>
es:
Name: Nombre
Phone: Teléfono
Remove contact: Quitar contacto
Add contact: Añadir contacto
</i18n>

View File

@ -1,3 +1,233 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import ModalCloseContract from 'src/pages/Customer/components/ModalCloseContract.vue';
import { toDate } from 'src/filters';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const quasar = useQuasar();
const vnPaginateRef = ref(null);
const showQPageSticky = ref(true);
const filter = {
order: 'finished ASC, started DESC',
include: [
{
relation: 'insurances',
scope: {
fields: ['id', 'credit', 'created', 'grade'],
order: 'created DESC',
limit: 2,
},
},
],
where: { client: `${route.params.id}` },
};
const fetch = (data) => {
data.forEach((element) => {
if (!element.finished) {
showQPageSticky.value = false;
return;
}
});
};
const toCustomerCreditContractsCreate = () => {
router.push({ name: 'CustomerCreditContractsCreate' });
};
const openDialog = (item) => {
quasar.dialog({
component: ModalCloseContract,
componentProps: {
id: item.id,
promise: updateData,
},
});
};
const openViewCredit = (credit) => {
router.push({
name: 'CustomerCreditContractsInsurance',
params: {
creditId: credit.id,
},
});
};
const updateData = () => {
vnPaginateRef.value?.fetch();
};
</script>
<template>
<div class="flex justify-center">Customer credit contracts</div>
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<VnPaginate
:filter="filter"
@on-fetch="fetch"
auto-load
data-key="CustomerCreditContracts"
order="id DESC"
ref="vnPaginateRef"
url="CreditClassifications"
>
<template #body="{ rows }">
<div v-if="rows.length">
<QCard
v-for="(item, index) in rows"
:key="index"
:class="{
'customer-card': true,
'q-mb-md': index < rows.length - 1,
'is-active': !item.finished,
}"
>
<QCardSection
class="full-width flex justify-between q-py-none"
>
<div class="width-state flex">
<div
class="flex items-center cursor-pointer q-mr-md"
v-if="!item.finished"
>
<QIcon
@click.stop="openDialog(item)"
color="primary"
name="lock"
size="md"
style="font-variation-settings: 'FILL' 1"
>
<QTooltip>{{ t('Close contract') }}</QTooltip>
</QIcon>
</div>
<div>
<div class="flex q-mb-xs">
<div class="q-mr-sm color-vn-label">
{{ t('Since') }}:
</div>
<div class="text-weight-bold">
{{ toDate(item.started) }}
</div>
</div>
<div class="flex">
<div class="q-mr-sm color-vn-label">
{{ t('To') }}:
</div>
<div class="text-weight-bold">
{{ toDate(item.finished) }}
</div>
</div>
</div>
</div>
<QSeparator vertical />
<div class="width-data flex">
<div
class="full-width flex justify-between items-center"
v-if="item?.insurances.length"
>
<div class="flex">
<div class="color-vn-label q-mr-xs">
{{ t('Credit') }}:
</div>
<div class="text-weight-bold">
{{ item.insurances[0].credit }}
</div>
</div>
<div class="flex">
<div class="color-vn-label q-mr-xs">
{{ t('Grade') }}:
</div>
<div class="text-weight-bold">
{{ item.insurances[0].grade || '-' }}
</div>
</div>
<div class="flex">
<div class="color-vn-label q-mr-xs">
{{ t('Date') }}:
</div>
<div class="text-weight-bold">
{{ toDate(item.insurances[0].created) }}
</div>
</div>
<div class="flex items-center cursor-pointer">
<QIcon
@click.stop="openViewCredit(item)"
color="primary"
name="preview"
size="md"
>
<QTooltip>{{
t('View credits')
}}</QTooltip>
</QIcon>
</div>
</div>
</div>
</QCardSection>
</QCard>
</div>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</template>
</VnPaginate>
</QCard>
</div>
<QPageSticky :offset="[18, 18]" v-if="showQPageSticky">
<QBtn
@click.stop="toCustomerCreditContractsCreate()"
color="primary"
fab
icon="add"
/>
<QTooltip>
{{ t('New contract') }}
</QTooltip>
</QPageSticky>
</template>
<style lang="scss" scoped>
.customer-card {
border: 2px solid var(--vn-light-gray);
border-radius: 10px;
padding: 10px;
display: flex;
justify-content: space-between;
}
.is-active {
background-color: var(--vn-light-gray);
}
.width-state {
width: 30%;
}
.width-data {
width: 65%;
}
</style>
<i18n>
es:
Close contract: Cerrar contrato
Since: Desde
To: Hasta
Credit: Crédito
Grade: Grade
Date: Fecha
View credits: Ver créditos
Created: Fecha creación
New contract: Nuevo contrato
</i18n>

View File

@ -1,3 +0,0 @@
<template>
<div class="flex justify-center">Credit management</div>
</template>

View File

@ -1,3 +1,142 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { QBtn } from 'quasar';
import { toCurrency, toDateHourMin } from 'src/filters';
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 WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const { t } = useI18n();
const route = useRoute();
const clientInformasRef = ref(null);
const rows = ref([]);
const filter = {
include: [
{
relation: 'worker',
scope: {
fields: ['id'],
include: { relation: 'user', scope: { fields: ['nickname'] } },
},
},
],
where: { clientFk: route.params.id },
order: ['created DESC'],
limit: 20,
};
const columns = computed(() => [
{
align: 'left',
field: 'created',
format: (value) => toDateHourMin(value),
label: t('Since'),
name: 'since',
},
{
align: 'left',
field: (row) => row.worker.user.nickname,
label: t('Employee'),
name: 'employee',
},
{
align: 'right',
field: 'rating',
label: t('Rating'),
name: 'rating',
},
{
align: 'right',
field: 'recommendedCredit',
format: (value) => toCurrency(value),
label: t('Recommended credit'),
name: 'recommendedCredit',
},
]);
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
filter.where.clientFk = newValue;
clientInformasRef.value?.fetch();
}
);
</script>
<template>
<div class="flex justify-center">Customer credit opinion</div>
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
ref="clientInformasRef"
url="ClientInformas"
/>
<FormModel
:form-initial-data="{}"
:observe-form-changes="false"
:url-create="`Clients/${route.params.id}/setRating`"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Rating')"
clearable
type="number"
v-model.number="data.rating"
/>
</div>
<div class="col">
<VnInput
:label="t('Recommended credit')"
clearable
type="number"
v-model.number="data.recommendedCredit"
/>
</div>
</VnRow>
</template>
</FormModel>
<div class="full-width flex justify-center" v-if="rows.length">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 0 }"
:rows="rows"
hide-bottom
row-key="id"
v-model:selected="selected"
class="card-width q-px-lg"
>
<template #body-cell-employee="{ row }">
<QTd auto-width @click.stop>
<QBtn color="blue" flat no-caps>{{ row.worker.user.nickname }}</QBtn>
<WorkerDescriptorProxy :id="row.clientFk" />
</QTd>
</template>
</QTable>
</div>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</template>
<i18n>
es:
Rating: Clasificación
Recommended credit: Crédito recomendado
Since: Desde
Employee: Empleado
</i18n>

View File

@ -1,26 +1,21 @@
<script setup>
import { ref, computed, onBeforeMount } from 'vue';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { date, QBtn } from 'quasar';
import { QBtn } from 'quasar';
import { toCurrency } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency, toDateHourMin } from 'src/filters';
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 = useStateStore();
const arrayData = ref(null);
const workerId = ref(0);
const rows = computed(() => arrayData.value.store.data);
const rows = ref([]);
onBeforeMount(async () => {
const filter = {
include: [
{
@ -31,17 +26,10 @@ onBeforeMount(async () => {
},
},
],
where: { clientFk: `${route.params.id}` },
where: { clientFk: route.params.id },
order: ['created DESC'],
limit: 20,
};
arrayData.value = useArrayData('CustomerCreditsCard', {
url: 'ClientCredits',
filter,
});
await arrayData.value.fetch({ append: false });
stateStore.rightDrawer = true;
});
const tableColumnComponents = {
created: {
@ -51,10 +39,8 @@ const tableColumnComponents = {
},
employee: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => {
selectWorkerId(prop.row.clientFk);
},
props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: () => {},
},
amount: {
component: 'span',
@ -69,7 +55,7 @@ const columns = computed(() => [
field: 'created',
label: t('Since'),
name: 'created',
format: (value) => date.formatDate(value, 'DD/MM/YYYY hh:mm:ss'),
format: (value) => toDateHourMin(value),
},
{
align: 'left',
@ -86,35 +72,58 @@ const columns = computed(() => [
},
]);
const selectWorkerId = (id) => {
workerId.value = id;
};
const toCustomerCreditCreate = () => {
router.push({ name: 'CustomerCreditCreate' });
};
</script>
<template>
<QPage class="column items-center q-pa-md">
<QTable :columns="columns" :rows="rows" class="full-width q-mt-md" row-key="id">
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="ClientCredits"
/>
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="tableColumnComponents[props.col.name].component"
@click="tableColumnComponents[props.col.name].event(props)"
@click="
tableColumnComponents[props.col.name].event(props)
"
class="rounded-borders q-pa-sm"
v-bind="tableColumnComponents[props.col.name].props(props)"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
>
{{ props.value }}
<WorkerDescriptorProxy :id="workerId" />
<WorkerDescriptorProxy
:id="props.row.workerFk"
v-if="props.col.name === 'employee'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
</QPage>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</QCard>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerCreditCreate()" color="primary" fab icon="add" />

View File

@ -2,11 +2,15 @@
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters';
import { toCurrency, toDate } from 'src/filters';
import useCardDescription from 'src/composables/useCardDescription';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import VnUserLink from 'src/components/ui/VnUserLink.vue';
import CustomerDescriptorMenu from './CustomerDescriptorMenu.vue';
const $props = defineProps({
id: {
@ -19,8 +23,10 @@ const $props = defineProps({
default: null,
},
});
const route = useRoute();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
@ -39,6 +45,23 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
:summary="$props.summary"
data-key="customerData"
>
<template #header-extra-action>
<QBtn
round
flat
size="sm"
icon="vn:Person"
color="white"
:to="{ name: 'CustomerList' }"
>
<QTooltip>
{{ t('Go to module index') }}
</QTooltip>
</QBtn>
</template>
<template #menu="{ entity }">
<CustomerDescriptorMenu :customer="entity" />
</template>
<template #body="{ entity }">
<VnLv :label="t('customer.card.payMethod')" :value="entity.payMethod.name" />
@ -63,23 +86,26 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
/>
</template>
<template #icons="{ entity }">
<QCardActions>
<QCardActions class="q-gutter-x-md">
<QIcon
v-if="entity.isActive == false"
v-if="!entity.isActive"
name="vn:disabled"
size="xs"
color="primary"
>
<QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip>
</QIcon>
<QIcon
v-if="entity.isFreezed == true"
name="vn:frozen"
size="xs"
color="primary"
>
<QIcon v-if="entity.isFreezed" name="vn:frozen" size="xs" color="primary">
<QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip>
</QIcon>
<QIcon
v-if="!entity.account.active"
color="primary"
name="vn:noweb"
size="xs"
>
<QTooltip>{{ t('customer.card.webAccountInactive') }}</QTooltip>
</QIcon>
<QIcon
v-if="entity.debt > entity.credit"
name="vn:risk"
@ -89,17 +115,41 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
<QTooltip>{{ t('customer.card.hasDebt') }}</QTooltip>
</QIcon>
<QIcon
v-if="entity.isTaxDataChecked == false"
v-if="!entity.isTaxDataChecked"
name="vn:no036"
size="xs"
color="primary"
>
<QTooltip>{{ t('customer.card.notChecked') }}</QTooltip>
</QIcon>
<QBtn
v-if="entity.unpaid"
flat
size="sm"
icon="vn:Client_unpaid"
color="primary"
:to="{ name: 'CustomerUnpaid' }"
>
<QTooltip>
{{ t('Unpaid') }}
<br />
{{
t('unpaidDated', {
dated: toDate(entity.unpaid.dated),
})
}}
<br />
{{
t('unpaidAmount', {
amount: toCurrency(entity.unpaid.amount),
})
}}
</QTooltip>
</QBtn>
</QCardActions>
</template>
<template #actions="{ entity }">
<QCardActions>
<QCardActions class="flex justify-center">
<QBtn
:to="{
name: 'TicketList',
@ -109,7 +159,7 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
icon="vn:ticket"
color="primary"
>
<QTooltip>{{ t('ticketList') }}</QTooltip>
<QTooltip>{{ t('Customer ticket list') }}</QTooltip>
</QBtn>
<QBtn
:to="{
@ -120,21 +170,40 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
icon="vn:invoice-out"
color="primary"
>
<QTooltip>{{ t('invoiceOutList') }}</QTooltip>
<QTooltip>{{ t('Customer invoice out list') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'OrderCreate',
query: { clientFk: entity.id },
}"
size="md"
icon="vn:basketadd"
color="primary"
>
<QTooltip>{{ t('New order') }}</QTooltip>
</QBtn>
<QBtn size="md" icon="face" color="primary">
<!-- TODO:: Redirigir a la vista de usuario cuando exista -->
<QTooltip>{{ t('Go to user') }}</QTooltip>
</QBtn>
</QCardActions>
</template>
</CardDescriptor>
</template>
<i18n>
{
"en": {
"ticketList": "Customer ticket list",
"invoiceOutList": "Customer invoice out list"
},
"es": {
"ticketList": "Listado de tickets del cliente",
"invoiceOutList": "Listado de facturas del cliente"
}
}
en:
unpaidDated: 'Date {dated}'
unpaidAmount: 'Amount {amount}'
es:
Go to module index: Ir al índice del módulo
Customer ticket list: Listado de tickets del cliente
Customer invoice out list: Listado de facturas del cliente
New order: Nuevo pedido
Go to user: Ir al usuario
Customer unpaid: Cliente impago
Unpaid: Impagado
unpaidDated: 'Fecha {dated}'
unpaidAmount: 'Importe {amount}'
</i18n>

View File

@ -0,0 +1,68 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
import { useQuasar } from 'quasar';
import useNotify from 'src/composables/useNotify';
import VnSmsDialog from 'src/components/common/VnSmsDialog.vue';
const $props = defineProps({
customer: {
type: Object,
required: true,
},
});
const { notify } = useNotify();
const { t } = useI18n();
const quasar = useQuasar();
const route = useRoute();
const showSmsDialog = () => {
quasar.dialog({
component: VnSmsDialog,
componentProps: {
phone: $props.customer.phone || $props.customer.mobile,
promise: sendSms,
},
});
};
const sendSms = async (payload) => {
payload.destinationFk = route.params.id;
try {
await axios.post(`Clients/${route.params.id}/sendSms`, payload);
notify('globals.notificationSent', 'positive');
} catch (error) {
notify(error.message, 'positive');
}
};
</script>
<template>
<QItem v-ripple clickable>
<QItemSection>
<RouterLink
:to="{
name: 'TicketCreate',
query: { clientFk: customer.id },
}"
class="color-vn-text"
>
{{ t('Simple ticket') }}
</RouterLink>
</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection>
</QItem>
</template>
<i18n>
es:
Simple ticket: Ticket simple
Send SMS: Enviar SMS
</i18n>

View File

@ -0,0 +1,259 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { QBadge, QBtn, QCheckbox } from 'quasar';
import { downloadFile } from 'src/composables/downloadFile';
import { toDateTimeFormat } from 'src/filters/date';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerFileManagementActions from 'src/pages/Customer/components/CustomerFileManagementActions.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const ClientDmsRef = ref(null);
const rows = ref([]);
const filter = {
include: {
relation: 'dms',
scope: {
fields: [
'dmsTypeFk',
'reference',
'hardCopyNumber',
'workerFk',
'description',
'hasFile',
'file',
'created',
],
include: [
{ relation: 'dmsType', scope: { fields: ['name'] } },
{
relation: 'worker',
scope: {
fields: ['id'],
include: { relation: 'user', scope: { fields: ['name'] } },
},
},
],
},
},
where: { clientFk: route.params.id },
order: ['dmsFk DESC'],
limit: 20,
};
const tableColumnComponents = {
id: {
component: 'span',
props: () => {},
event: () => {},
},
type: {
component: 'span',
props: () => {},
event: () => {},
},
order: {
component: QBadge,
props: () => {},
event: () => {},
},
reference: {
component: 'span',
props: () => {},
event: () => {},
},
description: {
component: 'span',
props: () => {},
event: () => {},
},
original: {
component: QCheckbox,
props: (prop) => ({
disable: true,
'model-value': Boolean(prop.value),
}),
event: () => {},
},
file: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: ({ row }) => downloadFile(row.dmsFk),
},
employee: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: () => {},
},
created: {
component: 'span',
props: () => {},
event: () => {},
},
actions: {
component: CustomerFileManagementActions,
props: (prop) => ({
id: prop.row.dmsFk,
promise: setData,
}),
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: ({ dms }) => dms.id,
label: t('Id'),
name: 'id',
},
{
align: 'left',
field: ({ dms }) => dms.dmsType.name,
label: t('Type'),
name: 'type',
},
{
align: 'left',
field: ({ dms }) => dms.hardCopyNumber,
label: t('Order'),
name: 'order',
},
{
align: 'left',
field: ({ dms }) => dms.reference,
label: t('Reference'),
name: 'reference',
},
{
align: 'left',
field: ({ dms }) => dms.description,
label: t('Description'),
name: 'description',
},
{
align: 'left',
field: ({ dms }) => dms.hasFile,
label: t('Original'),
name: 'original',
},
{
align: 'left',
field: ({ dms }) => dms.file,
label: t('File'),
name: 'file',
},
{
align: 'left',
field: ({ dms }) => dms.worker.user.name,
label: t('Employee'),
name: 'employee',
},
{
align: 'left',
field: (value) => value.dms.created,
label: t('Created'),
name: 'created',
format: (value) => toDateTimeFormat(value),
},
{
align: 'right',
field: 'actions',
label: '',
name: 'actions',
},
]);
const setData = () => {
ClientDmsRef.value.fetch();
};
const toCustomerFileManagementCreate = () => {
router.push({ name: 'CustomerFileManagementCreate' });
};
</script>
<template>
<FetchData
ref="ClientDmsRef"
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="ClientDms"
/>
<QPage class="column items-center q-pa-md">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="
props.col.name === 'order' && !props.value
? 'span'
: tableColumnComponents[props.col.name].component
"
@click="tableColumnComponents[props.col.name].event(props)"
class="col-content"
v-bind="tableColumnComponents[props.col.name].props(props)"
>
<template v-if="props.col.name !== 'original'">
{{ props.value }}
</template>
<WorkerDescriptorProxy
:id="props.row.dms.workerFk"
v-if="props.col.name === 'employee'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</QPage>
<QPageSticky :offset="[18, 18]">
<QBtn
@click.stop="toCustomerFileManagementCreate()"
color="primary"
fab
icon="add"
/>
<QTooltip>
{{ t('Upload file') }}
</QTooltip>
</QPageSticky>
</template>
<i18n>
es:
Id: Id
Type: Tipo
Order: Orden
Reference: Referencia
Description: Descripción
Original: Original
File: Fichero
Employee: Empleado
Created: Fecha creación
Upload file: Subir fichero
</i18n>

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();
@ -45,17 +45,24 @@ function handleLocation(data, location) {
:label="t('Social name')"
:required="true"
:rules="validate('client.socialName')"
clearable
v-model="data.socialName"
/>
<VnInput :label="t('Tax number')" v-model="data.fi" />
>
<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">
<VnInput :label="t('Street')" v-model="data.street" />
<VnInput :label="t('Street')" clearable v-model="data.street" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<VnSelect
:label="t('Sage tax type')"
:options="typesTaxes"
hide-selected
@ -63,14 +70,25 @@ function handleLocation(data, location) {
option-value="id"
v-model="data.sageTaxTypeFk"
/>
<VnSelectFilter
<VnSelect
:label="t('Sage transaction type')"
:options="typesTransactions"
hide-selected
option-label="vat"
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">
@ -87,19 +105,20 @@ function handleLocation(data, location) {
<VnRow class="row q-gutter-md q-mb-md">
<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">
<QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" />
<QCheckbox :label="t('Vies')" v-model="data.isVies" />
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>
{{ t('whenActivatingIt') }}
</QTooltip>
</QIcon>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<QCheckbox
:label="t('Notify by email')"
v-model="data.isToBeMailed"
/>
<QCheckbox :label="t('Notify by email')" v-model="data.isToBeMailed" />
<QCheckbox
:label="t('Invoice by address')"
v-model="data.hasToInvoiceByAddress"
@ -107,14 +126,13 @@ function handleLocation(data, location) {
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<QCheckbox
:label="t('Is equalizated')"
v-model="data.isEqualizated"
/>
<QCheckbox
:label="t('Verified data')"
v-model="data.isTaxDataChecked"
/>
<QCheckbox :label="t('Is equalizated')" v-model="data.isEqualizated" />
<QIcon class="cursor-info q-ml-sm" name="info" size="sm">
<QTooltip>
{{ t('inOrderToInvoice') }}
</QTooltip>
</QIcon>
<QCheckbox :label="t('Verified data')" v-model="data.isTaxDataChecked" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
@ -152,4 +170,11 @@ es:
Verified data: Datos comprobados
Incoterms authorization: Autorización incoterms
Electronic invoice: Factura electrónica
onlyLetters: Sólo se pueden usar letras, números y espacios
whenActivatingIt: Al activarlo, no informar el código del país en el campo nif
inOrderToInvoice: Para facturar no se consulta este campo, sino el RE de consignatario. Al modificar este campo si no esta marcada la casilla Facturar por consignatario, se propagará automaticamente el cambio a todos lo consignatarios, en caso contrario preguntará al usuario si quiere o no propagar
en:
onlyLetters: Only letters, numbers and spaces can be used
whenActivatingIt: When activating it, do not enter the country code in the ID field
inOrderToInvoice: In order to invoice, this field is not contulted, but the consignee's ET. When modifiying this field if the invoice by address option is not checked, the change will be automatically propagated to all addresses, otherwise the user will be asked if he wants to propagate it or not
</i18n>

View File

@ -1,27 +1,23 @@
<script setup>
import { ref, computed, onBeforeMount } from 'vue';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { date, QBtn } from 'quasar';
import { QBtn } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from '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 = useStateStore();
const arrayData = ref(null);
const rows = ref([]);
const totalAmount = ref(0);
const workerId = ref(0);
const rows = computed(() => arrayData.value.store.data);
onBeforeMount(async () => {
const filter = {
include: [
{
@ -44,17 +40,6 @@ onBeforeMount(async () => {
limit: 20,
};
arrayData.value = useArrayData('CustomerGreugesCard', {
url: 'greuges',
filter,
});
await arrayData.value.fetch({ append: false });
totalAmount.value = arrayData.value.store.data.reduce((accumulator, currentValue) => {
return accumulator + currentValue.amount;
}, 0);
stateStore.rightDrawer = true;
});
const tableColumnComponents = {
date: {
component: 'span',
@ -63,10 +48,8 @@ const tableColumnComponents = {
},
createdBy: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => {
selectWorkerId(prop.row.clientFk);
},
props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: () => {},
},
comment: {
component: 'span',
@ -91,7 +74,7 @@ const columns = computed(() => [
field: 'shipped',
label: t('Date'),
name: 'date',
format: (value) => date.formatDate(value, 'DD/MM/YYYY hh:mm:ss'),
format: (value) => toDateTimeFormat(value),
},
{
align: 'left',
@ -120,8 +103,11 @@ const columns = computed(() => [
},
]);
const selectWorkerId = (id) => {
workerId.value = id;
const setRows = (data) => {
rows.value = data;
totalAmount.value = data.reduce((accumulator, currentValue) => {
return accumulator + currentValue.amount;
}, 0);
};
const toCustomerGreugeCreate = () => {
@ -130,20 +116,25 @@ const toCustomerGreugeCreate = () => {
</script>
<template>
<QPage class="column items-center q-pa-md">
<QCard class="full-width" v-if="totalAmount">
<FetchData :filter="filter" @on-fetch="setRows" auto-load url="greuges" />
<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="label-color q-mr-md">{{ t('Total') }}:</span>
<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"
:no-data-label="t('globals.noResults')"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
@ -151,23 +142,26 @@ const toCustomerGreugeCreate = () => {
<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)"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
{{ props.value }}
<WorkerDescriptorProxy :id="workerId" />
<WorkerDescriptorProxy
:id="props.row.userFk"
v-if="props.col.name === 'createdBy'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
<QCard class="full-width" v-else>
<h5 class="flex justify-center label-color">
{{ t('globals.noResults') }}
</h5>
</QCard>
</QPage>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerGreugeCreate()" color="primary" fab icon="add" />

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();
@ -124,13 +124,13 @@ const setInq = (value, status) => {
:url="urlClientLogsModels"
/>
<h5 class="flex justify-center label-color">
<h5 class="flex justify-center color-vn-label">
{{ t('globals.noResults') }}
</h5>
<QDrawer :width="256" show-if-above side="right" v-model="stateStore.rightDrawer">
<div class="q-mt-sm q-px-md">
<VnInput :label="t('Search')">
<VnInput :label="t('Search')" clearable>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>
@ -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"
@ -187,7 +187,7 @@ const setInq = (value, status) => {
option-label="name"
option-value="id"
/>
<VnInput :label="t('Changes')" class="q-mt-sm">
<VnInput :label="t('Changes')" clearable class="q-mt-sm">
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>
@ -227,7 +227,7 @@ const setInq = (value, status) => {
</div>
<VnInputDate :label="t('Date')" class="q-mt-sm" />
<VnInput :label="t('To')" class="q-mt-md" />
<VnInput :label="t('To')" clearable class="q-mt-md" />
</div>
</QDrawer>

View File

@ -0,0 +1,135 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { toDateTimeFormat } from 'src/filters/date';
import FetchData from 'components/FetchData.vue';
const { t } = useI18n();
const route = useRoute();
const rows = ref([]);
const filter = {
include: [
{ relation: 'mandateType', scope: { fields: ['id', 'name'] } },
{ relation: 'company', scope: { fields: ['id', 'code'] } },
],
where: { clientFk: route.params.id },
order: ['created DESC'],
limit: 20,
};
const tableColumnComponents = {
id: {
component: 'span',
props: () => {},
event: () => {},
},
company: {
component: 'span',
props: () => {},
event: () => {},
},
type: {
component: 'span',
props: () => {},
event: () => {},
},
registerDate: {
component: 'span',
props: () => {},
event: () => {},
},
endDate: {
component: 'span',
props: () => {},
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: 'id',
label: t('Id'),
name: 'id',
},
{
align: 'left',
field: (row) => row.company.code,
label: t('Company'),
name: 'company',
},
{
align: 'left',
field: (row) => row.mandateType.name,
label: t('Type'),
name: 'type',
},
{
align: 'left',
field: 'created',
label: t('Register date'),
name: 'registerDate',
format: (value) => toDateTimeFormat(value),
},
{
align: 'left',
field: 'finished',
label: t('End date'),
name: 'endDate',
format: (value) => (value ? toDateTimeFormat(value) : '-'),
},
]);
</script>
<template>
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="Mandates"
/>
<QPage class="column items-center q-pa-md">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="tableColumnComponents[props.col.name].component"
@click="tableColumnComponents[props.col.name].event(props)"
class="rounded-borders q-pa-sm"
v-bind="tableColumnComponents[props.col.name].props(props)"
>
{{ props.value }}
</component>
</QTr>
</QTd>
</template>
</QTable>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</QPage>
</template>
<i18n>
es:
Id: Id
Company: Empresa
Type: Tipo
Register date: Fecha alta
End date: Fecha baja
</i18n>

View File

@ -2,7 +2,7 @@
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { date } from 'quasar';
import { toDateTimeFormat } from 'src/filters/date';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
@ -23,10 +23,11 @@ const toCustomerNoteCreate = () => {
</script>
<template>
<QCard class="q-pa-lg">
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<VnPaginate
data-key="CustomerNotes"
:url="'clientObservations'"
url="clientObservations"
auto-load
:filter="noteFilter"
>
@ -35,19 +36,15 @@ const toCustomerNoteCreate = () => {
<QCard
v-for="(item, index) in rows"
:key="index"
:class="{
'q-pa-md': true,
'q-rounded': true,
'custom-border': true,
'q-mb-md': index < rows.length - 1,
}"
class="q-pa-md q-rounded custom-border"
:class="{ 'q-mb-md': index < rows.length - 1 }"
>
<div class="flex justify-between">
<p class="label-color">{{ item.worker.user.nickname }}</p>
<p class="label-color">
{{
date.formatDate(item?.created, 'DD-MM-YYYY HH:mm:ss')
}}
<p class="color-vn-label">
{{ item.worker.user.nickname }}
</p>
<p class="color-vn-label">
{{ toDateTimeFormat(item?.created) }}
</p>
</div>
<h6 class="q-mt-xs q-mb-none">{{ item.text }}</h6>
@ -55,30 +52,19 @@ const toCustomerNoteCreate = () => {
</div>
<div v-else>
<h5 class="flex justify-center label-color">
<h5 class="flex justify-center color-vn-label">
{{ t('globals.noResults') }}
</h5>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn
@click.stop="toCustomerNoteCreate()"
color="primary"
fab
icon="add"
/>
<QTooltip>
{{ t('New consignee') }}
</QTooltip>
</QPageSticky>
</template>
</VnPaginate>
</QCard>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerNoteCreate()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New consignee') }}
{{ t('New note') }}
</QTooltip>
</QPageSticky>
</template>
@ -89,8 +75,9 @@ const toCustomerNoteCreate = () => {
border-radius: 10px;
padding: 10px;
}
.label-color {
color: var(--vn-label-color);
}
</style>
<i18n>
es:
New note: Nueva nota
</i18n>

View File

@ -1,3 +0,0 @@
<template>
<div class="flex justify-center">Others</div>
</template>

View File

@ -1,40 +1,23 @@
<script setup>
import { ref, computed, onBeforeMount } from 'vue';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { date, QBtn } from 'quasar';
import { toCurrency, toDate } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'src/filters';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import FetchData from 'components/FetchData.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const stateStore = useStateStore();
const rows = ref([]);
const arrayData = ref(null);
const workerId = ref(0);
const rows = computed(() => arrayData.value.store.data);
onBeforeMount(async () => {
const filter = {
where: { clientFk: `${route.params.id}` },
where: { clientFk: route.params.id },
order: ['started DESC'],
limit: 20,
};
arrayData.value = useArrayData('CustomerRecoveriesCard', {
url: 'Recoveries',
filter,
});
await arrayData.value.fetch({ append: false });
stateStore.rightDrawer = true;
});
const tableColumnComponents = {
since: {
component: 'span',
@ -64,14 +47,14 @@ const columns = computed(() => [
field: 'started',
label: t('Since'),
name: 'since',
format: (value) => date.formatDate(value, 'DD/MM/YYYY'),
format: (value) => toDate(value),
},
{
align: 'left',
field: 'finished',
label: t('To'),
name: 'to',
format: (value) => date.formatDate(value, 'DD/MM/YYYY'),
format: (value) => toDate(value),
},
{
align: 'left',
@ -94,13 +77,22 @@ const toCustomerRecoverieCreate = () => {
</script>
<template>
<QPage class="column items-center q-pa-md">
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="Recoveries"
/>
<div class="full-width flex justify-center">
<QPage class="card-width q-pa-lg">
<QTable
:columns="columns"
:no-data-label="t('globals.noResults')"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
@ -108,23 +100,21 @@ const toCustomerRecoverieCreate = () => {
<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)"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
{{ props.value }}
<WorkerDescriptorProxy :id="workerId" />
</component>
</QTr>
</QTd>
</template>
</QTable>
<QCard class="full-width" v-else>
<h5 class="flex justify-center label-color">
{{ t('globals.noResults') }}
</h5>
</QCard>
</QPage>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerRecoverieCreate()" color="primary" fab icon="add" />

View File

@ -0,0 +1,144 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { QBtn } from 'quasar';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import { toDateTimeFormat } from 'src/filters/date';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const rows = ref([]);
const filter = {
include: [
{ relation: 'type', scope: { fields: ['code', 'description'] } },
{ relation: 'user', scope: { fields: ['id', 'name'] } },
{ relation: 'company', scope: { fields: ['code'] } },
],
where: { clientFk: route.params.id },
order: ['created DESC'],
limit: 20,
};
const tableColumnComponents = {
sent: {
component: 'span',
props: () => {},
event: () => {},
},
description: {
component: 'span',
props: () => {},
event: () => {},
},
worker: {
component: QBtn,
props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: () => {},
},
company: {
component: 'span',
props: () => {},
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: 'created',
label: t('Sent'),
name: 'sent',
format: (value) => toDateTimeFormat(value),
},
{
align: 'left',
field: (value) => value.type.description,
label: t('Description'),
name: 'description',
},
{
align: 'left',
field: (value) => value.user.name,
label: t('Worker'),
name: 'worker',
},
{
align: 'left',
field: (value) => value.company?.code,
label: t('Company'),
name: 'company',
},
]);
const toCustomerSamplesCreate = () => {
router.push({ name: 'CustomerSamplesCreate' });
};
</script>
<template>
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="ClientSamples"
/>
<div class="full-width flex justify-center">
<QPage class="card-width q-pa-lg">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
:no-data-label="t('globals.noResults')"
>
<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)
"
>
{{ props.value }}
<WorkerDescriptorProxy
:id="props.row.userFk"
v-if="props.col.name === 'worker'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
</QPage>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerSamplesCreate()" color="primary" fab icon="add" />
<QTooltip>
{{ t('Send sample') }}
</QTooltip>
</QPageSticky>
</template>
<i18n>
es:
Sent: Enviado
Description: Descripción
Worker: Trabajador
Company: Empresa
Send sample: Enviar plantilla
</i18n>

View File

@ -7,6 +7,7 @@ import CardSummary from 'components/ui/CardSummary.vue';
import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import CustomerSummaryTable from 'src/pages/Customer/components/CustomerSummaryTable.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
const route = useRoute();
@ -305,9 +306,16 @@ const creditWarning = computed(() => {
:value="entity.recommendedCredit"
/>
</QCard>
<QCard>
<div class="header">
{{ t('Latest tickets') }}
</div>
<CustomerSummaryTable />
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss" scoped>
@media (min-width: $breakpoint-md) {
.summary .vn-one {
@ -315,9 +323,11 @@ const creditWarning = computed(() => {
}
}
</style>
<i18n>
en:
valueInfo: Value from {min} to {max}. The higher the better value
es:
valueInfo: Valor de {min} a {max}. Cuanto más alto, mejor valor
Latest tickets: Últimos tickets
</i18n>

View File

@ -0,0 +1,167 @@
<script setup>
import { computed, onBeforeMount, ref, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'src/components/common/VnInput.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify';
import { useStateStore } from 'stores/useStateStore';
const { t } = useI18n();
const route = useRoute();
const { notify } = useNotify();
const stateStore = useStateStore();
const amountInputRef = ref(null);
const initialDated = Date.vnNew();
const unpaidClient = ref(false);
const isLoading = ref(false);
const amount = ref(null);
const dated = ref(initialDated);
const initialData = ref({
dated: initialDated,
});
const hasChanged = computed(() => {
return (
initialData.value.dated !== dated.value ||
initialData.value.amount !== amount.value
);
});
onBeforeMount(() => {
getData(route.params.id);
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
getData(newValue);
}
);
const getData = async (id) => {
const filter = { where: { clientFk: id } };
try {
const { data } = await axios.get('ClientUnpaids', {
params: { filter: JSON.stringify(filter) },
});
if (data.length) {
setValues(data[0]);
} else {
defaultValues();
}
} catch (error) {
defaultValues();
}
};
const setValues = (data) => {
unpaidClient.value = true;
amount.value = data.amount;
dated.value = data.dated;
initialData.value = data;
};
const defaultValues = () => {
unpaidClient.value = false;
initialData.value.amount = null;
setInitialData();
};
const setInitialData = () => {
amount.value = initialData.value.amount;
dated.value = initialData.value.dated;
};
const onSubmit = async () => {
isLoading.value = true;
const payload = {
amount: amount.value,
clientFk: route.params.id,
dated: dated.value,
};
try {
await axios.patch('ClientUnpaids', payload);
notify('globals.dataSaved', 'positive');
unpaidClient.value = true;
} catch (error) {
notify('errors.create', 'negative');
} finally {
isLoading.value = false;
}
};
watch(
() => unpaidClient.value,
async (val) => {
await nextTick();
if (val) amountInputRef.value.focus();
}
);
</script>
<template>
<Teleport v-if="stateStore?.isSubToolbarShown()" to="#st-actions">
<QBtnGroup push class="q-gutter-x-sm">
<QBtn
:disabled="!hasChanged"
:label="t('globals.reset')"
:loading="isLoading"
@click="setInitialData"
color="primary"
flat
icon="restart_alt"
type="reset"
/>
<QBtn
:disabled="!hasChanged"
:label="t('globals.save')"
:loading="isLoading"
@click="onSubmit"
color="primary"
icon="save"
/>
</QBtnGroup>
</Teleport>
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<QForm>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Unpaid client')" v-model="unpaidClient" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md" v-show="unpaidClient">
<div class="col">
<VnInputDate :label="t('Date')" v-model="dated" />
</div>
<div class="col">
<VnInput
ref="amountInputRef"
:label="t('Amount')"
clearable
type="number"
v-model="amount"
/>
</div>
</VnRow>
</QForm>
</QCard>
</div>
</template>
<i18n>
es:
Unpaid client: Cliente impagado
Date: Fecha
Amount: Importe
</i18n>

View File

@ -1,43 +1,158 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import FormModel from 'components/FormModel.vue';
import axios from 'axios';
import { useQuasar } from 'quasar';
import { useValidator } from 'src/composables/useValidator';
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';
const { notify } = useNotify();
const { t } = useI18n();
const { validate } = useValidator();
const quasar = useQuasar();
const route = useRoute();
const stateStore = useStateStore();
const active = ref(false);
const canChangePassword = ref(0);
const email = ref(null);
const isLoading = ref(false);
const name = ref(null);
const usersPreviewRef = ref(null);
const user = ref([]);
const userPasswords = ref(0);
const dataChanges = computed(() => {
return (
user.value.active !== active.value ||
user.value.email !== email.value ||
user.value.name !== name.value
);
});
const filter = { where: { id: `${route.params.id}` } };
const showChangePasswordDialog = () => {
quasar.dialog({
component: CustomerChangePassword,
componentProps: {
id: route.params.id,
userPasswords: userPasswords.value,
promise: usersPreviewRef.value.fetch(),
},
});
};
const setInitialData = () => {
if (user.value.length) {
active.value = user.value[0].active;
email.value = user.value[0].email;
name.value = user.value[0].name;
}
};
const onSubmit = async () => {
isLoading.value = true;
const payload = {
active: active.value,
email: email.value,
name: name.value,
};
try {
await axios.patch(`Clients/${route.params.id}/updateUser`, payload);
notify('globals.dataSaved', 'positive');
if (usersPreviewRef.value) usersPreviewRef.value.fetch();
} catch (error) {
notify('errors.create', 'negative');
} finally {
isLoading.value = false;
}
};
</script>
<template>
<FormModel
<FetchData
:filter="filter"
:observe-form-changes="false"
:url-update="`Clients/${route.params.id}/updateUser`"
:url="'VnUsers/preview'"
model="client"
>
<template #form="{ data }">
<div
v-for="(item, index) in data"
:key="index"
:class="{
'q-mb-md': index < data.length - 1,
}"
>
<VnRow class="row q-gutter-md q-mb-md">
<QCheckbox
:label="t('Enable web access')"
v-model="item.active"
@on-fetch="
(data) => {
user = data;
setInitialData();
}
"
auto-load
ref="usersPreviewRef"
url="VnUsers/preview"
/>
<FetchData
:url="`Clients/${route.params.id}/hasCustomerRole`"
@on-fetch="(data) => (canChangePassword = data)"
auto-load
/>
<FetchData
@on-fetch="(data) => (userPasswords = data[0])"
auto-load
url="UserPasswords"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput :label="t('User')" v-model="item.name" />
<VnInput :label="t('Recovery email')" v-model="item.email">
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push class="q-gutter-x-sm">
<QBtn
:disabled="isLoading"
:label="t('globals.reset')"
:loading="isLoading"
@click="setInitialData"
color="primary"
flat
icon="restart_alt"
type="reset"
/>
<QBtn
:disabled="isLoading"
:label="t('Change password')"
:loading="isLoading"
@click.stop="showChangePasswordDialog()"
color="primary"
flat
icon="edit"
v-if="canChangePassword"
/>
<QBtn
:disabled="isLoading || !dataChanges"
:label="t('globals.save')"
:loading="isLoading"
@click="onSubmit"
color="primary"
icon="save"
/>
</QBtnGroup>
</Teleport>
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<QCardSection>
<QForm>
<QCheckbox :label="t('Enable web access')" v-model="active" />
<div class="q-px-sm">
<VnInput :label="t('User')" clearable v-model="name" />
<VnInput
:label="t('Recovery email')"
:rules="validate('client.email')"
clearable
type="email"
v-model="email"
class="q-mt-sm"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
@ -48,10 +163,11 @@ const filter = { where: { id: `${route.params.id}` } };
</QIcon>
</template>
</VnInput>
</VnRow>
</div>
</template>
</FormModel>
</QForm>
</QCardSection>
</QCard>
</div>
</template>
<i18n>
@ -60,4 +176,5 @@ es:
User: Usuario
Recovery email: Correo de recuperacion
This email is used for user to regain access their account: Este correo electrónico se usa para que el usuario recupere el acceso a su cuenta
Change password: Cambiar contraseña
</i18n>

View File

@ -0,0 +1,167 @@
<script setup>
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
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';
const { t } = useI18n();
const route = useRoute();
const rows = ref([]);
const filter = {
include: [
{ relation: 'mandateType', scope: { fields: ['id', 'name'] } },
{ relation: 'company', scope: { fields: ['id', 'code'] } },
],
where: { clientFk: null },
order: ['created DESC'],
limit: 20,
};
const tableColumnComponents = {
state: {
component: CustomerCloseIconTooltip,
props: ({ row }) => ({ transaction: row }),
event: () => {},
},
id: {
component: 'span',
props: () => {},
event: () => {},
},
date: {
component: 'span',
props: () => {},
event: () => {},
},
amount: {
component: 'span',
props: () => {},
event: () => {},
},
validate: {
component: CustomerCheckIconTooltip,
props: ({ row }) => ({
transaction: row,
promise: refreshData,
}),
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: '',
label: t('State'),
name: 'state',
},
{
align: 'left',
field: 'id',
label: t('Id'),
name: 'id',
},
{
align: 'left',
field: 'created',
label: t('Date'),
name: 'date',
format: (value) => toDateHourMinSec(value),
},
{
align: 'left',
field: 'amount',
label: t('Amount'),
name: 'amount',
format: (value) => toCurrency(value),
},
{
align: 'left',
field: '',
name: 'validate',
},
]);
onBeforeMount(() => {
getData(route.params.id);
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
getData(newValue);
}
);
const getData = async (id) => {
filter.where.clientFk = id;
try {
const { data } = await axios.get('clients/transactions', {
params: { filter: JSON.stringify(filter) },
});
rows.value = data;
} catch (error) {
return error;
}
};
const refreshData = () => {
getData(route.params.id);
};
</script>
<template>
<div class="full-width flex justify-center">
<QPage class="card-width q-pa-lg">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
v-if="rows?.length"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props">
<component
:is="tableColumnComponents[props.col.name].component"
@click="
tableColumnComponents[props.col.name].event(props)
"
class="rounded-borders q-pa-sm"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
>
{{ props.value }}
</component>
</QTr>
</QTd>
</template>
</QTable>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</QPage>
</div>
</template>
<i18n>
es:
State: Estado
Id: Id
Date: Fecha
Amount: Importe
</i18n>

View File

@ -5,26 +5,14 @@ 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';
const { t } = useI18n();
const newClientForm = reactive({
const initialData = reactive({
active: true,
name: null,
salesPersonFk: null,
businessTypeFk: null,
fi: null,
socialName: null,
street: null,
postcode: null,
city: null,
provinceFk: null,
countryFk: null,
userName: null,
email: null,
isEqualizated: false,
});
@ -55,14 +43,14 @@ function handleLocation(data, location) {
<QPage>
<VnSubToolbar />
<FormModel
:form-initial-data="newClientForm"
:form-initial-data="initialData"
model="client"
url-create="Clients/createWithUser"
>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<QInput :label="t('Comercial name')" v-model="data.name" />
<VnSelectFilter
<VnSelect
:label="t('Salesperson')"
:options="workersOptions"
hide-selected
@ -72,7 +60,7 @@ function handleLocation(data, location) {
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<VnSelect
:label="t('Business type')"
:options="businessTypesOptions"
hide-selected
@ -85,14 +73,14 @@ function handleLocation(data, location) {
<VnRow class="row q-gutter-md q-mb-md">
<QInput
:label="t('Business name')"
:rules="validate('Client.socialName')"
:rules="validate('client.socialName')"
v-model="data.socialName"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<QInput
:label="t('Street')"
:rules="validate('Client.street')"
:rules="validate('client.street')"
v-model="data.street"
/>
</VnRow>
@ -102,20 +90,32 @@ function handleLocation(data, location) {
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="
(location) => handleLocation(data, location)
"
@update:model-value="(location) => handleLocation(data, location)"
>
</VnLocation>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<QInput v-model="data.userName" :label="t('Web user')" />
<QInput v-model="data.email" :label="t('Email')" />
<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')"
v-model="newClientForm.isEqualizated"
v-model="initialData.isEqualizated"
/>
</template>
</FormModel>

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

@ -1,56 +1,52 @@
<script setup>
import { ref, computed, onBeforeMount } from 'vue';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { QBtn, QCheckbox, useQuasar } from 'quasar';
import { toCurrency, toDate } from 'filters/index';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import CustomerNotificationsFilter from './CustomerDefaulterFilter.vue';
import CustomerBalanceDueTotal from './CustomerBalanceDueTotal.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue';
import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue';
const { t } = useI18n();
const stateStore = useStateStore();
const quasar = useQuasar();
const arrayData = ref(null);
const balanceDueTotal = ref(0);
const customerId = ref(0);
const selected = ref([]);
const workerId = ref(0);
const rows = computed(() => arrayData.value.store.data);
const rows = ref([]);
const tableColumnComponents = {
client: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: ({ row }) => selectCustomerId(row.clientFk),
props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: () => {},
},
isWorker: {
component: QCheckbox,
props: ({ row }) => ({
props: (prop) => ({
disable: true,
'model-value': Boolean(row.selected),
'model-value': Boolean(prop.value),
}),
event: () => {},
},
salesperson: {
salesPerson: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: ({ row }) => selectWorkerId(row.salesPersonFk),
props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: () => {},
},
country: {
component: 'span',
props: () => {},
event: () => {},
},
paymentMethod: {
payMethod: {
component: 'span',
props: () => {},
event: () => {},
@ -62,8 +58,8 @@ const tableColumnComponents = {
},
author: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: ({ row }) => selectWorkerId(row.workerFk),
props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: () => {},
},
lastObservation: {
component: 'span',
@ -93,6 +89,7 @@ const columns = computed(() => [
field: 'clientName',
label: t('Client'),
name: 'client',
sortable: true,
},
{
align: 'left',
@ -104,85 +101,77 @@ const columns = computed(() => [
align: 'left',
field: 'salesPersonName',
label: t('Salesperson'),
name: 'salesperson',
name: 'salesPerson',
sortable: true,
},
{
align: 'left',
field: 'country',
label: t('Country'),
name: 'country',
sortable: true,
},
{
align: 'left',
field: 'payMethod',
label: t('P. Method'),
name: 'paymentMethod',
name: 'payMethod',
sortable: true,
tooltip: t('Pay method'),
},
{
align: 'left',
field: ({ amount }) => toCurrency(amount),
label: t('Balance D.'),
name: 'balance',
sortable: true,
tooltip: t('Balance due'),
},
{
align: 'left',
field: 'workerName',
label: t('Author'),
name: 'author',
sortable: true,
tooltip: t('Worker who made the last observation'),
},
{
align: 'left',
field: 'observation',
label: t('Last observation'),
name: 'lastObservation',
sortable: true,
},
{
align: 'left',
field: ({ created }) => toDate(created),
label: t('L. O. Date'),
name: 'date',
sortable: true,
tooltip: t('Last observation date'),
},
{
align: 'left',
field: ({ creditInsurance }) => toCurrency(creditInsurance),
label: t('Credit I.'),
name: 'credit',
sortable: true,
tooltip: t('Credit insurance'),
},
{
align: 'left',
field: ({ defaulterSinced }) => toDate(defaulterSinced),
label: t('From'),
name: 'from',
sortable: true,
},
]);
onBeforeMount(() => {
getArrayData();
});
const getArrayData = async () => {
arrayData.value = useArrayData('CustomerDefaulter', {
url: 'Defaulters/filter',
limit: 0,
});
await arrayData.value.fetch({ append: false });
balanceDueTotal.value = arrayData.value.store.data.reduce(
(accumulator, currentValue) => {
const setRows = (data) => {
rows.value = data;
balanceDueTotal.value = data.reduce((accumulator, currentValue) => {
return accumulator + (currentValue['amount'] || 0);
},
0
);
stateStore.rightDrawer = true;
};
const selectCustomerId = (id) => {
workerId.value = 0;
customerId.value = id;
};
const selectWorkerId = (id) => {
customerId.value = 0;
workerId.value = id;
}, 0);
};
const viewAddObservation = (rowsSelected) => {
@ -196,12 +185,21 @@ const viewAddObservation = (rowsSelected) => {
};
const refreshData = () => {
getArrayData();
setRows();
};
const onFetch = (data) => {
for (const element of data) {
element.isWorker = element.businessTypeFk === 'worker';
}
rows.value = data;
};
</script>
<template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<FetchData :filter="filter" @on-fetch="onFetch" auto-load url="Defaulters/filter" />
<QDrawer side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<CustomerNotificationsFilter data-key="CustomerDefaulter" />
</QScrollArea>
@ -230,6 +228,18 @@ const refreshData = () => {
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">
@ -239,10 +249,29 @@ const refreshData = () => {
v-bind="tableColumnComponents[props.col.name].props(props)"
@click="tableColumnComponents[props.col.name].event(props)"
>
{{ props.value }}
<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>
<WorkerDescriptorProxy v-if="workerId" :id="workerId" />
<CustomerDescriptorProxy v-else :id="customerId" />
<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>
@ -265,10 +294,15 @@ es:
Salesperson: Comercial
Country: País
P. Method: F. Pago
Pay method: Forma de pago
Balance D.: Saldo V.
Balance due: Saldo vencido
Author: Autor
Worker who made the last observation: Trabajador que ha realizado la última observación
Last observation: Última observación
L. O. Date: Fecha Ú. O.
Last observation date: Fecha última observación
Credit I.: Crédito A.
Credit insurance: Crédito asegurado
From: Desde
</i18n>

View File

@ -3,7 +3,9 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useQuasar } from 'quasar';
import { useDialogPluginComponent } from 'quasar';
import useNotify from 'src/composables/useNotify';
import VnRow from 'components/ui/VnRow.vue';
@ -18,8 +20,9 @@ const $props = defineProps({
},
});
const { dialogRef } = useDialogPluginComponent();
const { notify } = useNotify();
const { t } = useI18n();
const quasar = useQuasar();
const newObservation = ref(null);
@ -38,15 +41,9 @@ const onSubmit = async () => {
await $props.promise();
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
notify('globals.dataSaved', 'positive');
} catch (error) {
quasar.notify({
message: t(`${error.message}`),
type: 'negative',
});
notify(error.message, 'negative');
}
};
</script>

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"
@ -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"
@ -119,6 +119,7 @@ const authors = ref();
<QItemSection>
<VnInput
:label="t('P. Method')"
clearable
is-outlined
v-model="params.paymentMethod"
/>
@ -129,6 +130,7 @@ const authors = ref();
<QItemSection>
<VnInput
:label="t('Balance D.')"
clearable
is-outlined
v-model="params.balance"
/>
@ -137,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"
@ -160,7 +162,12 @@ const authors = ref();
<QItem class="q-mb-sm">
<QItemSection>
<VnInput :label="t('L. O. Date')" is-outlined v-model="params.date" />
<VnInput
:label="t('L. O. Date')"
clearable
is-outlined
v-model="params.date"
/>
</QItemSection>
</QItem>
@ -168,6 +175,7 @@ const authors = ref();
<QItemSection>
<VnInput
:label="t('Credit I.')"
clearable
is-outlined
v-model="params.credit"
/>

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'

View File

@ -1,32 +1,17 @@
<script setup>
import { ref, computed, onBeforeMount } from 'vue';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { QBtn } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import CustomerNotificationsFilter from './CustomerNotificationsFilter.vue';
import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
const { t } = useI18n();
const stateStore = useStateStore();
const arrayData = ref(null);
onBeforeMount(async () => {
arrayData.value = useArrayData('CustomerNotifications', {
url: 'Clients',
limit: 0,
});
await arrayData.value.fetch({ append: false });
stateStore.rightDrawer = true;
});
const rows = computed(() => arrayData.value.store.data);
const rows = ref([]);
const selected = ref([]);
const selectedCustomerId = ref(0);
@ -97,7 +82,14 @@ const selectCustomerId = (id) => {
</script>
<template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="Clients"
/>
<QDrawer side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<CustomerNotificationsFilter data-key="CustomerNotifications" />
</QScrollArea>

View File

@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnSelect from 'components/common/VnSelect.vue';
const { t } = useI18n();
const props = defineProps({
@ -40,6 +40,7 @@ const clients = ref();
<QItemSection>
<VnInput
:label="t('Identifier')"
clearable
is-outlined
v-model="params.identifier"
/>
@ -51,7 +52,7 @@ const clients = ref();
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="clients">
<VnSelectFilter
<VnSelect
:input-debounce="0"
:label="t('Social name')"
:options="clients"
@ -75,7 +76,7 @@ const clients = ref();
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="cities">
<VnSelectFilter
<VnSelect
:input-debounce="0"
:label="t('City')"
:options="cities"
@ -96,13 +97,24 @@ const clients = ref();
<QItem class="q-mb-sm">
<QItemSection>
<VnInput :label="t('Phone')" is-outlined v-model="params.phone" />
<VnInput
:label="t('Phone')"
clearable
is-outlined
v-model="params.phone"
/>
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection>
<VnInput :label="t('Email')" is-outlined v-model="params.email" />
<VnInput
:label="t('Email')"
clearable
is-outlined
type="email"
v-model="params.email"
/>
</QItemSection>
</QItem>
<QSeparator />

View File

@ -9,7 +9,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 CustomerCreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import CustomerNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue';
@ -41,9 +41,9 @@ const refreshData = () => {
getCustomsAgents();
};
const toCustomerConsignees = () => {
const toCustomerAddress = () => {
router.push({
name: 'CustomerConsignees',
name: 'CustomerAddress',
params: {
id: route.params.id,
},
@ -70,48 +70,58 @@ function handleLocation(data, location) {
:form-initial-data="formInitialData"
:observe-form-changes="false"
:url-create="urlCreate"
@on-data-saved="toCustomerConsignees()"
@on-data-saved="toCustomerAddress()"
model="client"
>
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerAddress"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<QCheckbox :label="t('Default')" v-model="data.isDefaultAddress" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput :label="t('Consignee')" v-model="data.nickname" />
<VnInput :label="t('Street address')" v-model="data.street" />
<VnInput :label="t('Consignee')" clearable v-model="data.nickname" />
<VnInput :label="t('Street address')" clearable v-model="data.street" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<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">
<VnSelectFilter
<div class="row justify-between q-gutter-md q-mb-md">
<VnSelect
:label="t('Agency')"
:options="agencyModes"
:rules="validate('route.agencyFk')"
hide-selected
option-label="name"
option-value="id"
v-model="data.agencyModeFk"
class="col"
/>
</VnRow>
<VnInput class="col" :label="t('Phone')" clearable v-model="data.phone" />
<VnInput
class="col"
:label="t('Mobile')"
clearable
v-model="data.mobile"
/>
</div>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput :label="t('Phone')" v-model="data.phone" />
<VnInput :label="t('Mobile')" v-model="data.mobile" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<VnSelect
:label="t('Incoterms')"
:options="incoterms"
hide-selected
@ -119,6 +129,7 @@ function handleLocation(data, location) {
option-value="code"
v-model="data.incotermsFk"
/>
<VnSelectDialog
:label="t('Customs agent')"
:options="customsAgents"

View File

@ -1,7 +1,7 @@
<script setup>
import { onBeforeMount, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import VnLocation from 'src/components/common/VnLocation.vue';
@ -9,13 +9,14 @@ 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 CustomerCreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import CustomsNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const urlUpdate = ref('');
const postcodesOptions = ref([]);
@ -24,9 +25,10 @@ const incoterms = ref([]);
const customsAgents = ref([]);
const observationTypes = ref([]);
const notes = ref([]);
const deletes = ref([]);
onBeforeMount(() => {
urlUpdate.value = `Clients/${route.params.id}/updateAddress/${route.params.consigneeId}`;
urlUpdate.value = `Clients/${route.params.id}/updateAddress/${route.params.addressId}`;
});
const getData = async (observations) => {
@ -35,7 +37,7 @@ const getData = async (observations) => {
if (observationTypes.value.length) {
const filter = {
fields: ['id', 'addressFk', 'observationTypeFk', 'description'],
where: { addressFk: `${route.params.consigneeId}` },
where: { addressFk: `${route.params.addressId}` },
};
const { data } = await axios.get('AddressObservations', {
params: { filter: JSON.stringify(filter) },
@ -52,8 +54,9 @@ const getData = async (observations) => {
$isNew: false,
$oldData: null,
$orgIndex: null,
addressFk: `${route.params.consigneeId}`,
addressFk: `${route.params.addressId}`,
description: observation.description,
id: observation.id,
observationTypeFk: type.id,
}
: null;
@ -68,21 +71,39 @@ const addNote = () => {
$isNew: true,
$oldData: null,
$orgIndex: null,
addressFk: `${route.params.consigneeId}`,
addressFk: `${route.params.addressId}`,
description: '',
observationTypeFk: '',
});
};
const deleteNote = (index) => {
const deleteNote = (id, index) => {
deletes.value.push(id);
notes.value.splice(index, 1);
};
const onDataSaved = () => {
const payload = {
creates: notes.value,
const onDataSaved = async () => {
let payload = {};
const creates = notes.value.filter((note) => note.$isNew);
if (creates.length) {
payload.creates = creates;
}
if (deletes.value.length) {
payload.deletes = deletes.value;
}
await axios.post('AddressObservations/crud', payload);
notes.value = [];
deletes.value = [];
toCustomerAddress();
};
axios.post('AddressObservations/crud', payload);
const toCustomerAddress = () => {
router.push({
name: 'CustomerAddress',
params: {
id: route.params.id,
},
});
};
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
@ -110,58 +131,81 @@ function handleLocation(data, location) {
<FormModel
:observe-form-changes="false"
:url-update="urlUpdate"
:url="`Addresses/${route.params.consigneeId}`"
:url="`Addresses/${route.params.addressId}`"
@on-data-saved="onDataSaved()"
auto-load
model="client"
>
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerAddress"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('Enabled')" v-model="data.isActive" />
</div>
<div class="col">
<QCheckbox
:label="t('Is equalizated')"
v-model="data.isEqualizated"
/>
</div>
<div class="col">
<QCheckbox
:label="t('Is Loginflora allowed')"
v-model="data.isLogifloraAllowed"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput :label="t('Consignee')" v-model="data.nickname" />
<VnInput :label="t('Street address')" v-model="data.street" />
<div class="col">
<VnInput :label="t('Consignee')" clearable v-model="data.nickname" />
</div>
<div class="col">
<VnInput :label="t('Street')" clearable v-model="data.street" />
</div>
</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>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
></VnLocation>
</div>
<div class="col">
<VnSelect
:label="t('Agency')"
:options="agencyModes"
:rules="validate('route.agencyFk')"
hide-selected
option-label="name"
option-value="id"
v-model="data.agencyModeFk"
/>
</div>
<div class="col">
<VnInput :label="t('Phone')" clearable v-model="data.phone" />
</div>
<div class="col">
<VnInput :label="t('Mobile')" clearable v-model="data.mobile" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput :label="t('Phone')" v-model="data.phone" />
<VnInput :label="t('Mobile')" v-model="data.mobile" />
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelectFilter
<div class="col">
<VnSelect
:label="t('Incoterms')"
:options="incoterms"
hide-selected
@ -169,6 +213,8 @@ function handleLocation(data, location) {
option-value="code"
v-model="data.incotermsFk"
/>
</div>
<div class="col">
<VnSelectDialog
:label="t('Customs agent')"
:options="customsAgents"
@ -181,6 +227,7 @@ function handleLocation(data, location) {
<CustomsNewCustomsAgent />
</template>
</VnSelectDialog>
</div>
</VnRow>
<h4 class="q-mb-xs">{{ t('Notes') }}</h4>
@ -189,7 +236,8 @@ function handleLocation(data, location) {
class="row q-gutter-md q-mb-md"
v-for="(note, index) in notes"
>
<VnSelectFilter
<div class="col">
<VnSelect
:label="t('Observation type')"
:options="observationTypes"
hide-selected
@ -197,17 +245,25 @@ function handleLocation(data, location) {
option-value="id"
v-model="note.observationTypeFk"
/>
<VnInput :label="t('Description')" v-model="note.description" />
</div>
<div class="col">
<VnInput
:label="t('Description')"
:rules="validate('route.description')"
clearable
v-model="note.description"
/>
</div>
<div class="flex items-center">
<QIcon
@click.stop="deleteNote(index)"
@click.stop="deleteNote(note.id, index)"
class="cursor-pointer"
color="primary"
name="delete"
size="sm"
>
<QTooltip>
{{ t('Remove') }}
{{ t('Remove note') }}
</QTooltip>
</QIcon>
</div>
@ -240,7 +296,7 @@ es:
Is equalizated: Recargo de equivalencia
Is Loginflora allowed: Compra directa en Holanda
Consignee: Consignatario
Street address: Dirección postal
Street: Dirección fiscal
Postcode: Código postal
City: Población
Province: Provincia

View File

@ -0,0 +1,140 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useDialogPluginComponent } from 'quasar';
import useNotify from 'src/composables/useNotify';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { dialogRef } = useDialogPluginComponent();
const { notify } = useNotify();
const { t } = useI18n();
const $props = defineProps({
id: {
type: String,
required: true,
},
userPasswords: {
type: Object,
required: true,
},
promise: {
type: Function,
required: true,
},
});
const closeButton = ref(null);
const isLoading = ref(false);
const newPassword = ref('');
const requestPassword = ref('');
const onSubmit = async () => {
isLoading.value = true;
if (newPassword.value !== requestPassword.value) {
notify(t("Passwords don't match"), 'negative');
isLoading.value = false;
return;
}
const payload = {
newPassword: newPassword.value,
};
try {
await axios.patch(`Clients/${$props.id}/setPassword`, payload);
await $props.promise();
} catch (error) {
notify('errors.create', 'negative');
} finally {
isLoading.value = false;
if (closeButton.value) closeButton.value.click();
}
};
</script>
<template>
<QDialog ref="dialogRef">
<QCard class="q-pa-lg">
<QCardSection>
<QForm @submit.prevent="onSubmit">
<span
ref="closeButton"
class="row justify-end close-icon"
v-close-popup
>
<QIcon name="close" size="sm" />
</span>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('New password')"
clearable
v-model="newPassword"
type="password"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>
{{
t('customer.card.passwordRequirements', {
length: $props.userPasswords.length,
nAlpha: $props.userPasswords.nAlpha,
nDigits: $props.userPasswords.nDigits,
nPunct: $props.userPasswords.nPunct,
nUpper: $props.userPasswords.nUpper,
})
}}
</QTooltip>
</QIcon>
</template>
</VnInput>
</div>
<div class="col">
<VnInput
:label="t('Request password')"
clearable
v-model="requestPassword"
type="password"
/>
</div>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn
:disabled="isLoading"
:label="t('globals.cancel')"
:loading="isLoading"
class="q-ml-sm"
color="primary"
flat
type="reset"
v-close-popup
/>
<QBtn
:disabled="isLoading"
:label="t('Change password')"
:loading="isLoading"
color="primary"
type="submit"
/>
</div>
</QForm>
</QCardSection>
</QCard>
</QDialog>
</template>
<i18n>
es:
New password: Nueva contraseña
Request password: Repetir contraseña
Change password: Cambiar contraseña
Passwords don't match: Las contraseñas no coinciden
</i18n>

View File

@ -0,0 +1,47 @@
<script setup>
import { useI18n } from 'vue-i18n';
import axios from 'axios';
const { t } = useI18n();
const $props = defineProps({
transaction: {
type: Object,
required: true,
},
promise: {
type: Function,
required: true,
},
});
const setClientsConfirmTransaction = async () => {
try {
const payload = { id: $props.transaction.id };
await axios.post('Clients/confirmTransaction', payload);
$props.promise();
} catch (error) {
return error;
}
};
</script>
<template>
<div v-if="!$props?.transaction?.isConfirmed">
<QIcon
@click.stop="setClientsConfirmTransaction"
color="primary"
name="done_all"
size="sm"
class="cursor-pointer"
>
<QTooltip>{{ t('Confirm transaction') }}</QTooltip>
</QIcon>
</div>
</template>
<i18n>
es:
Confirm transaction: Confirmar transacción
</i18n>

View File

@ -0,0 +1,48 @@
<script setup>
import { onBeforeMount, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const tooltip = ref('');
const $props = defineProps({
transaction: {
type: Object,
required: true,
},
});
onBeforeMount(() => {
const errorMessage = $props.transaction.errorMessage
? $props.transaction.errorMessage
: '';
const responseMessage = $props.transaction.responseMessage
? $props.transaction.responseMessage
: '';
tooltip.value = `${errorMessage} ${responseMessage}`;
});
</script>
<template>
<div
v-if="
($props.transaction.errorMessage || $props.transaction.responseMessage) &&
!$props.transaction.isConfirmed
"
>
<QIcon color="negative" name="close" size="sm">
<QTooltip>{{ tooltip }}</QTooltip>
</QIcon>
</div>
<div v-if="$props.transaction.isConfirmed">
<QIcon color="positive" name="check" size="sm">
<QTooltip>{{ t('Confirmed') }}</QTooltip>
</QIcon>
</div>
</template>
<i18n>
es:
Confirmed: Confirmada
</i18n>

View File

@ -0,0 +1,72 @@
<script setup>
import { reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const initialData = reactive({
clientFK: Number(route.params.id),
});
const toCustomerCreditContracts = () => {
router.push({ name: 'CustomerCreditContracts' });
};
</script>
<template>
<FormModel
:form-initial-data="initialData"
:observe-form-changes="false"
url-create="creditClassifications/createWithInsurance"
@on-data-saved="toCustomerCreditContracts()"
>
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerCreditContracts"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
:label="t('Credit')"
clearable
type="number"
v-model.number="data.credit"
/>
</div>
<div class="col">
<VnInput
:label="t('Grade')"
clearable
type="number"
v-model.number="data.grade"
/>
</div>
<div class="col">
<VnInputDate :label="t('Since')" v-model="data.started" />
</div>
</VnRow>
</template>
</FormModel>
</template>
<i18n>
es:
Credit: Crédito
Grade: Grade
Since: Desde
</i18n>

View File

@ -0,0 +1,107 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { toCurrency, toDateHourMinSec } from 'src/filters';
import FetchData from 'components/FetchData.vue';
const { t } = useI18n();
const route = useRoute();
const rows = ref([]);
const filter = {
where: {
creditClassificationFk: `${route.params.creditId}`,
},
limit: 20,
};
const tableColumnComponents = {
created: {
component: 'span',
props: () => {},
event: () => {},
},
grade: {
component: 'span',
props: () => {},
event: () => {},
},
credit: {
component: 'span',
props: () => {},
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: 'created',
format: (value) => toDateHourMinSec(value),
label: t('Created'),
name: 'created',
},
{
align: 'left',
field: 'grade',
label: t('Grade'),
name: 'grade',
},
{
align: 'left',
field: 'credit',
format: (value) => toCurrency(value),
label: t('Credit'),
name: 'credit',
},
]);
</script>
<template>
<FetchData
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="CreditInsurances"
/>
<QPage class="column items-center q-pa-md" v-if="rows.length">
<QTable
:columns="columns"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
>
<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)"
>
{{ props.value }}
</component>
</QTr>
</QTd>
</template>
</QTable>
</QPage>
<h5 class="flex justify-center color-vn-label" v-else>
{{ t('globals.noResults') }}
</h5>
</template>
<i18n>
es:
Created: Fecha creación
Grade: Grade
Credit: Crédito
</i18n>

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