Merge branch 'dev' into 6999-AddTabToFilter
gitea/salix-front/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Jon Elias 2024-11-19 09:21:10 +00:00
commit 5acce4d61d
73 changed files with 735 additions and 696 deletions

View File

@ -1,6 +1,6 @@
{
"name": "salix-front",
"version": "24.44.0",
"version": "24.50.0",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",
@ -64,4 +64,4 @@
"vite": "^5.1.4",
"vitest": "^0.31.1"
}
}
}

View File

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

View File

@ -77,7 +77,7 @@ const isLoading = ref(false);
const hasChanges = ref(false);
const originalData = ref();
const vnPaginateRef = ref();
const formData = ref();
const formData = ref([]);
const saveButtonRef = ref(null);
const watchChanges = ref();
const formUrl = computed(() => $props.url);

View File

@ -22,7 +22,7 @@ const props = defineProps({
default: 'main',
},
});
const initialized = ref(false);
const items = ref([]);
const expansionItemElements = reactive({});
const pinnedModules = computed(() => {
@ -34,18 +34,26 @@ const search = ref(null);
const filteredItems = computed(() => {
if (!search.value) return items.value;
const normalizedSearch = normalize(search.value);
return items.value.filter((item) => {
const locale = t(item.title).toLowerCase();
return locale.includes(search.value.toLowerCase());
const locale = normalize(t(item.title));
return locale.includes(normalizedSearch);
});
});
const filteredPinnedModules = computed(() => {
if (!search.value) return pinnedModules.value;
const normalizedSearch = search.value
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
const map = new Map();
for (const [key, pinnedModule] of pinnedModules.value) {
const locale = t(pinnedModule.title).toLowerCase();
if (locale.includes(search.value.toLowerCase())) map.set(key, pinnedModule);
const locale = t(pinnedModule.title)
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
if (locale.includes(normalizedSearch)) map.set(key, pinnedModule);
}
return map;
});
@ -53,11 +61,13 @@ const filteredPinnedModules = computed(() => {
onMounted(async () => {
await navigation.fetchPinned();
getRoutes();
initialized.value = true;
});
watch(
() => route.matched,
() => {
if (!initialized.value) return;
items.value = [];
getRoutes();
},
@ -147,6 +157,13 @@ async function togglePinned(item, event) {
const handleItemExpansion = (itemName) => {
expansionItemElements[itemName].scrollToLastElement();
};
function normalize(text) {
return text
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
}
</script>
<template>

View File

@ -25,7 +25,7 @@ const $props = defineProps({
},
searchUrl: {
type: String,
default: 'params',
default: 'table',
},
});

View File

@ -17,7 +17,7 @@ const $props = defineProps({
},
searchUrl: {
type: String,
default: 'params',
default: 'table',
},
vertical: {
type: Boolean,

View File

@ -162,9 +162,7 @@ onMounted(() => {
: $props.defaultMode;
stateStore.rightDrawer = quasar.screen.gt.xs;
columnsVisibilitySkipped.value = [
...splittedColumns.value.columns
.filter((c) => c.visible == false)
.map((c) => c.name),
...splittedColumns.value.columns.filter((c) => !c.visible).map((c) => c.name),
...['tableActions'],
];
createForm.value = $props.create;
@ -237,7 +235,7 @@ function splitColumns(columns) {
if (col.create) splittedColumns.value.create.push(col);
if (col.cardVisible) splittedColumns.value.cardVisible.push(col);
if ($props.isEditable && col.disable == null) col.disable = false;
if ($props.useModel && col.columnFilter != false)
if ($props.useModel && col.columnFilter !== false)
col.columnFilter = { inWhere: true, ...col.columnFilter };
splittedColumns.value.columns.push(col);
}

View File

@ -1,8 +1,7 @@
<script setup>
import { onMounted, watch, computed, ref } from 'vue';
import { onMounted, watch, computed, ref, useAttrs } from 'vue';
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useAttrs } from 'vue';
import VnDate from './VnDate.vue';
import { useRequired } from 'src/composables/useRequired';

View File

@ -2,5 +2,12 @@
const model = defineModel({ type: Boolean, required: true });
</script>
<template>
<QRadio v-model="model" v-bind="$attrs" dense :dark="true" class="q-mr-sm" />
<QRadio
v-model="model"
v-bind="$attrs"
dense
:dark="true"
class="q-mr-sm"
size="xs"
/>
</template>

View File

@ -1,7 +1,7 @@
<script setup>
import { ref, toRefs, computed, watch, onMounted, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue';
import { useArrayData } from 'src/composables/useArrayData';
import { useRequired } from 'src/composables/useRequired';
import dataByOrder from 'src/utils/dataByOrder';
@ -90,6 +90,10 @@ const $props = defineProps({
type: Boolean,
default: false,
},
dataKey: {
type: String,
default: null,
},
});
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
@ -98,14 +102,14 @@ const { optionLabel, optionValue, optionFilter, optionFilterValue, options, mode
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const vnSelectRef = ref();
const dataRef = ref();
const lastVal = ref();
const noOneText = t('globals.noOne');
const noOneOpt = ref({
[optionValue.value]: false,
[optionLabel.value]: noOneText,
});
const isLoading = ref(false);
const useURL = computed(() => $props.url);
const value = computed({
get() {
return $props.modelValue;
@ -129,11 +133,18 @@ watch(modelValue, async (newValue) => {
onMounted(() => {
setOptions(options.value);
if ($props.url && $props.modelValue && !findKeyInOptions())
if (useURL.value && $props.modelValue && !findKeyInOptions())
fetchFilter($props.modelValue);
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
defineExpose({ opts: myOptions });
const arrayDataKey =
$props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label);
const arrayData = useArrayData(arrayDataKey, { url: $props.url, searchUrl: false });
function findKeyInOptions() {
if (!$props.options) return;
return filter($props.modelValue, $props.options)?.length;
@ -168,7 +179,7 @@ function filter(val, options) {
}
async function fetchFilter(val) {
if (!$props.url || !dataRef.value) return;
if (!$props.url) return;
const { fields, include, sortBy, limit } = $props;
const key =
@ -190,8 +201,8 @@ async function fetchFilter(val) {
const fetchOptions = { where, include, limit };
if (fields) fetchOptions.fields = fields;
if (sortBy) fetchOptions.order = sortBy;
return dataRef.value.fetch(fetchOptions);
arrayData.reset(['skip', 'filter.skip', 'page']);
return (await arrayData.applyFilter({ filter: fetchOptions }))?.data;
}
async function filterHandler(val, update) {
@ -231,20 +242,23 @@ function nullishToTrue(value) {
const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
defineExpose({ opts: myOptions });
async function onScroll({ to, direction, from, index }) {
const lastIndex = myOptions.value.length - 1;
if (from === 0 && index === 0) return;
if (!useURL.value && !$props.fetchRef) return;
if (direction === 'decrease') return;
if (to === lastIndex && arrayData.store.hasMoreData && !isLoading.value) {
isLoading.value = true;
await arrayData.loadMore();
setOptions(arrayData.store.data);
vnSelectRef.value.scrollTo(lastIndex);
isLoading.value = false;
}
}
</script>
<template>
<FetchData
ref="dataRef"
:url="$props.url"
@on-fetch="(data) => setOptions(data)"
:where="where || { [optionValue]: value }"
:limit="limit"
:sort-by="sortBy"
:fields="fields"
:params="params"
/>
<QSelect
v-model="value"
:options="myOptions"
@ -263,6 +277,9 @@ defineExpose({ opts: myOptions });
:rules="mixinRules"
virtual-scroll-slice-size="options.length"
hide-bottom-space
:input-debounce="useURL ? '300' : '0'"
:loading="isLoading"
@virtual-scroll="onScroll"
>
<template v-if="isClearable" #append>
<QIcon

View File

@ -83,7 +83,7 @@ async function fetch() {
<slot name="header" :entity="entity" dense>
<VnLv :label="`${entity.id} -`" :value="entity.name" />
</slot>
<slot name="header-right">
<slot name="header-right" :entity="entity">
<span></span>
</slot>
</div>

View File

@ -37,7 +37,7 @@ const $props = defineProps({
},
hiddenTags: {
type: Array,
default: () => ['filter', 'search', 'or', 'and'],
default: () => ['filter', 'or', 'and'],
},
customTags: {
type: Array,
@ -49,7 +49,7 @@ const $props = defineProps({
},
searchUrl: {
type: String,
default: 'params',
default: 'table',
},
redirect: {
type: Boolean,
@ -61,7 +61,6 @@ const emit = defineEmits([
'update:modelValue',
'refresh',
'clear',
'search',
'init',
'remove',
'setUserParams',

View File

@ -17,19 +17,12 @@ const config = reactive({
const type = Object.keys(config).find((key) => key in useAttrs()) || 'sip';
onBeforeMount(async () => {
let url;
let { channel } = config[type];
if (type === 'say-simple') {
url = (await axios.get('SaySimpleConfigs/findOne')).data.url;
if (!channel)
channel = (
await axios.get('SaySimpleCountries/findOne', {
params: {
filter: { fields: ['channel'], where: { countryFk: 0 } },
},
})
).data?.channel;
const { url, defaultChannel } = (await axios.get('SaySimpleConfigs/findOne'))
.data;
if (!channel) channel = defaultChannel;
config[
type

View File

@ -65,13 +65,9 @@ onBeforeRouteLeave((to, from, next) => {
auto-load
@on-fetch="(data) => (observationTypes = data)"
/>
<QCard class="q-pa-xs q-mb-xl full-width" v-if="$props.addNote">
<QCard class="q-pa-xs q-mb-lg full-width" v-if="$props.addNote">
<QCardSection horizontal>
<VnAvatar :worker-id="currentUser.id" size="md" />
<div class="full-width row justify-between q-pa-xs">
<VnUserLink :name="t('New note')" :worker-id="currentUser.id" />
{{ t('globals.now') }}
</div>
{{ t('New note') }}
</QCardSection>
<QCardSection class="q-px-xs q-my-none q-py-none">
<VnRow class="full-width">
@ -144,7 +140,7 @@ onBeforeRouteLeave((to, from, next) => {
<div class="full-width row justify-between q-pa-xs">
<div>
<VnUserLink
:name="`${note.worker.user.nickname}`"
:name="`${note.worker.user.name}`"
:worker-id="note.worker.id"
/>
<QBadge

View File

@ -44,7 +44,7 @@ const props = defineProps({
},
limit: {
type: Number,
default: 10,
default: 20,
},
userParams: {
type: Object,
@ -100,7 +100,7 @@ const arrayData = useArrayData(props.dataKey, {
const store = arrayData.store;
onMounted(async () => {
if (props.autoLoad) await fetch();
if (props.autoLoad && !store.data?.length) await fetch();
mounted.value = true;
});
@ -115,7 +115,11 @@ watch(
watch(
() => store.data,
(data) => emit('onChange', data)
(data) => {
if (!mounted.value) return;
emit('onChange', data);
},
{ immediate: true }
);
watch(

View File

@ -1,5 +1,5 @@
<template>
<div class="vn-row q-gutter-md q-mb-md">
<div class="vn-row q-gutter-md">
<slot />
</div>
</template>
@ -18,6 +18,9 @@
&:not(.wrap) {
flex-direction: column;
}
&[fixed] {
flex-direction: row;
}
}
}
</style>

View File

@ -45,7 +45,7 @@ const props = defineProps({
},
limit: {
type: Number,
default: 10,
default: 20,
},
userParams: {
type: Object,

View File

@ -75,18 +75,10 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
limit: store.limit,
};
let exprFilter;
let userParams = { ...store.userParams };
if (store?.exprBuilder) {
const where = buildFilter(userParams, (param, value) => {
const res = store.exprBuilder(param, value);
if (res) delete userParams[param];
return res;
});
exprFilter = where ? { where } : null;
}
Object.assign(filter, store.userFilter, exprFilter);
Object.assign(filter, store.userFilter);
let where;
if (filter?.where || store.filter?.where)
where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {});
@ -96,11 +88,28 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
Object.assign(params, userParams);
params.filter.skip = store.skip;
if (store.order && store.order.length) params.filter.order = store.order;
if (store?.order && typeof store?.order == 'string') store.order = [store.order];
if (store.order?.length) params.filter.order = [...store.order];
else delete params.filter.order;
store.currentFilter = JSON.parse(JSON.stringify(params));
delete store.currentFilter.filter.include;
store.currentFilter.filter = JSON.stringify(store.currentFilter.filter);
let exprFilter;
if (store?.exprBuilder) {
exprFilter = buildFilter(params, (param, value) => {
if (param == 'filter') return;
const res = store.exprBuilder(param, value);
if (res) delete params[param];
return res;
});
}
if (params.filter.where || exprFilter)
params.filter.where = { ...params.filter.where, ...exprFilter };
params.filter = JSON.stringify(params.filter);
store.currentFilter = params;
store.isLoading = true;
const response = await axios.get(store.url, {
signal: canceller.signal,
@ -247,6 +256,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
}
function updateStateParams() {
if (!route?.path) return;
const newUrl = { path: route.path, query: { ...(route.query ?? {}) } };
if (store?.searchUrl)
newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter);
@ -270,7 +280,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const pushUrl = { path: to };
if (to.endsWith('/list') || to.endsWith('/'))
pushUrl.query = newUrl.query;
destroy();
else destroy();
return router.push(pushUrl);
}
}

View File

@ -0,0 +1,8 @@
import { openURL } from 'quasar';
const defaultWindowFeatures = {
noopener: true,
noreferrer: true,
};
export default function (url, windowFeatures = defaultWindowFeatures, fn = undefined) {
openURL(url, fn, windowFeatures);
}

View File

@ -9,7 +9,7 @@ function parseJSON(str, fallback) {
}
export default function (route, param) {
// catch route query params
const params = parseJSON(route?.query?.params, {});
const params = parseJSON(route?.query?.table, {});
// extract and parse filter from params
const { filter: filterStr = '{}' } = params;

View File

@ -506,6 +506,7 @@ invoiceOut:
invoiceWithFutureDate: Exists an invoice with a future date
noTicketsToInvoice: There are not tickets to invoice
criticalInvoiceError: 'Critical invoicing error, process stopped'
invalidSerialTypeForAll: The serial type must be global when invoicing all clients
table:
addressId: Address id
streetAddress: Street
@ -857,6 +858,7 @@ components:
downloadFile: Download file
openCard: View
openSummary: Summary
viewSummary: Summary
cardDescriptor:
mainList: Main list
summary: Summary

View File

@ -509,6 +509,7 @@ invoiceOut:
invoiceWithFutureDate: Existe una factura con una fecha futura
noTicketsToInvoice: No existen tickets para facturar
criticalInvoiceError: Error crítico en la facturación proceso detenido
invalidSerialTypeForAll: El tipo de serie debe ser global cuando se facturan todos los clientes
table:
addressId: Id dirección
streetAddress: Dirección fiscal

View File

@ -104,7 +104,7 @@ const exprBuilder = (param, value) => {
<template>
<VnSearchbar
data-key="AccountUsers"
data-key="AccountList"
:expr-builder="exprBuilder"
:label="t('account.search')"
:info="t('account.searchInfo')"
@ -112,12 +112,12 @@ const exprBuilder = (param, value) => {
/>
<RightMenu>
<template #right-panel>
<AccountFilter data-key="AccountUsers" />
<AccountFilter data-key="AccountList" />
</template>
</RightMenu>
<VnTable
ref="tableRef"
data-key="AccountUsers"
data-key="AccountList"
url="VnUsers/preview"
:filter="filter"
order="id DESC"

View File

@ -82,14 +82,14 @@ const exprBuilder = (param, value) => {
<template>
<VnSearchbar
data-key="Roles"
data-key="AccountRolesList"
:expr-builder="exprBuilder"
:label="t('role.searchRoles')"
:info="t('role.searchInfo')"
/>
<VnTable
ref="tableRef"
data-key="Roles"
data-key="AccountRolesList"
:url="`VnRoles`"
:create="{
urlCreate: 'VnRoles',

View File

@ -9,7 +9,7 @@ const { t } = useI18n();
<VnCard
data-key="Role"
:descriptor="RoleDescriptor"
search-data-key="AccountRoles"
search-data-key="AccountRolesList"
:searchbar-props="{
url: 'VnRoles',
label: t('role.searchRoles'),

View File

@ -23,7 +23,7 @@ defineExpose({ states });
<template>
<FetchData url="ClaimStates" @on-fetch="(data) => (states = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true" search-url="table">
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>

View File

@ -37,6 +37,9 @@ const entityId = computed(() => {
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity?.name, entity?.id));
const debtWarning = computed(() => {
return customer.value?.debt > customer.value?.credit ? 'negative' : 'primary';
});
</script>
<template>
@ -117,7 +120,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
v-if="customer.debt > customer.credit"
name="vn:risk"
size="xs"
color="primary"
:color="debtWarning"
>
<QTooltip>{{ t('customer.card.hasDebt') }}</QTooltip>
</QIcon>
@ -174,23 +177,6 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
>
<QTooltip>{{ t('Customer ticket list') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'TicketList',
query: {
table: JSON.stringify({
clientFk: entity.id,
}),
createForm: JSON.stringify({ clientId: entity.id }),
},
}"
size="md"
color="primary"
target="_blank"
icon="vn:ticketAdd"
>
<QTooltip>{{ t('New ticket') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'InvoiceOutList',
@ -202,23 +188,6 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
>
<QTooltip>{{ t('Customer invoice out list') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'OrderList',
query: {
table: JSON.stringify({
clientFk: entity.id,
}),
createForm: JSON.stringify({ clientFk: entity.id }),
},
}"
size="md"
target="_blank"
icon="vn:basketadd"
color="primary"
>
<QTooltip>{{ t('New order') }}</QTooltip>
</QBtn>
<QBtn
:to="{
name: 'AccountSummary',

View File

@ -6,8 +6,8 @@ import axios from 'axios';
import { useQuasar } from 'quasar';
import useNotify from 'src/composables/useNotify';
import VnSmsDialog from 'src/components/common/VnSmsDialog.vue';
import useOpenURL from 'src/composables/useOpenURL';
const $props = defineProps({
customer: {
@ -15,7 +15,6 @@ const $props = defineProps({
required: true,
},
});
const { notify } = useNotify();
const { t } = useI18n();
const quasar = useQuasar();
@ -40,9 +39,42 @@ const sendSms = async (payload) => {
notify(error.message, 'positive');
}
};
const openCreateForm = (type) => {
const query = {
table: {
clientFk: $props.customer.id,
},
createForm: {
addressId: $props.customer.defaultAddressFk,
},
};
const clientFk = {
ticket: 'clientId',
order: 'clientFk',
};
const key = clientFk[type];
if (!key) return;
query.createForm[key] = $props.customer.id;
const params = Object.entries(query)
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
.join('&');
useOpenURL(`/#/${type}/list?${params}`);
};
</script>
<template>
<QItem v-ripple clickable @click="openCreateForm('ticket')">
<QItemSection>
{{ t('globals.pageTitles.createTicket') }}
</QItemSection>
</QItem>
<QItem v-ripple clickable @click="openCreateForm('order')">
<QItemSection>
{{ t('globals.pageTitles.createOrder') }}
</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection>
</QItem>

View File

@ -4,7 +4,7 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { toCurrency, toPercentage, toDate } from 'src/filters';
import { toCurrency, toPercentage, toDate, dashOrCurrency } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue';
import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue';
@ -27,21 +27,16 @@ const $props = defineProps({
const entityId = computed(() => $props.id || route.params.id);
const customer = computed(() => summary.value.entity);
const summary = ref();
const clientUrl = ref();
onMounted(async () => {
clientUrl.value = (await getUrl('client/')) + entityId.value + '/';
});
const defaulterAmount = computed(() => customer.value.defaulters[0]?.amount);
const balanceDue = computed(() => {
return (
customer.value &&
customer.value.defaulters.length &&
customer.value.defaulters[0].amount
);
const amount = defaulterAmount.value;
if (!amount || amount < 0) {
return null;
}
return amount;
});
const balanceDueWarning = computed(() => (balanceDue.value ? 'negative' : ''));
const balanceDueWarning = computed(() => (defaulterAmount.value ? 'negative' : ''));
const claimRate = computed(() => {
return customer.value.claimsRatio?.claimingRate ?? 0;
@ -305,7 +300,7 @@ const sumRisk = ({ clientRisks }) => {
<VnLv
v-if="entity.defaulters"
:label="t('customer.summary.balanceDue')"
:value="toCurrency(balanceDue)"
:value="dashOrCurrency(balanceDue)()"
:class="balanceDueWarning"
:info="t('customer.summary.balanceDueInfo')"
/>

View File

@ -2,10 +2,9 @@
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 VnRow from 'components/ui/VnRow.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify';
import { useStateStore } from 'stores/useStateStore';

View File

@ -11,10 +11,24 @@ defineProps({
required: true,
},
});
const handleSalesModelValue = (val) => ({
or: [
{ id: val },
{ name: val },
{ nickname: { like: '%' + val + '%' } },
{ code: { like: `${val}%` } },
],
});
const exprBuilder = (param, value) => {
return {
and: [{ active: { neq: false } }, handleSalesModelValue(value)],
};
};
</script>
<template>
<VnFilterPanel :data-key="dataKey" :search-button="true" search-url="table">
<VnFilterPanel :data-key="dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
@ -52,14 +66,18 @@ defineProps({
<QItem class="q-mb-sm">
<QItemSection>
<VnSelect
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
url="Workers/search"
:params="{
departmentCodes: ['VT'],
}"
auto-load
:label="t('Salesperson')"
:expr-builder="exprBuilder"
v-model="params.salesPersonFk"
@update:model-value="searchFn()"
option-value="id"
option-label="name"
sort-by="nickname ASC"
emit-value
map-options
use-input
@ -68,7 +86,18 @@ defineProps({
outlined
rounded
:input-debounce="0"
/>
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.nickname }},{{ opt.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template></VnSelect
>
</QItemSection>
</QItem>
<QItem class="q-mb-sm">

View File

@ -394,16 +394,16 @@ function handleLocation(data, location) {
<VnSearchbar
:info="t('You can search by customer id or name')"
:label="t('Search customer')"
data-key="Customer"
data-key="CustomerList"
/>
<RightMenu>
<template #right-panel>
<CustomerFilter data-key="Customer" />
<CustomerFilter data-key="CustomerList" />
</template>
</RightMenu>
<VnTable
ref="tableRef"
data-key="Customer"
data-key="CustomerList"
url="Clients/filter"
:create="{
urlCreate: 'Clients/createWithUser',

View File

@ -23,6 +23,7 @@ const incoterms = ref([]);
const customsAgents = ref([]);
const observationTypes = ref([]);
const notes = ref([]);
let originalNotes = [];
const deletes = ref([]);
onBeforeMount(() => {
@ -42,7 +43,8 @@ const getData = async (observations) => {
});
if (data.length) {
notes.value = data
originalNotes = data;
notes.value = originalNotes
.map((observation) => {
const type = observationTypes.value.find(
(type) => type.id === observation.observationTypeFk
@ -81,14 +83,24 @@ const deleteNote = (id, index) => {
};
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;
}
let payload = {
creates: notes.value.filter((note) => note.$isNew),
deletes: deletes.value,
updates: notes.value
.filter((note) =>
originalNotes.some(
(oNote) =>
oNote.id === note.id &&
(note.description !== oNote.description ||
note.observationTypeFk !== oNote.observationTypeFk)
)
)
.map((note) => ({
data: note,
where: { id: note.id },
})),
};
await axios.post('AddressObservations/crud', payload);
notes.value = [];
deletes.value = [];

View File

@ -194,14 +194,14 @@ const getItemPackagingType = (ticketSales) => {
redirect="ticket"
>
<template #column-nickname="{ row }">
<span class="link">
<span class="link" @click.stop>
{{ row.nickname }}
<CustomerDescriptorProxy :id="row.clientFk" />
</span>
</template>
<template #column-routeFk="{ row }">
<span class="link">
<span class="link" @click.stop>
{{ row.routeFk }}
<RouteDescriptorProxy :id="row.routeFk" />
</span>
@ -218,7 +218,7 @@ const getItemPackagingType = (ticketSales) => {
<span v-else> {{ toCurrency(row.totalWithVat) }}</span>
</template>
<template #column-state="{ row }">
<span v-if="row.invoiceOut">
<span v-if="row.invoiceOut" @click.stop>
<span :class="{ link: row.invoiceOut.ref }">
{{ row.invoiceOut.ref }}
<InvoiceOutDescriptorProxy :id="row.invoiceOut.id" />

View File

@ -99,7 +99,7 @@ const travelDialogRef = ref(false);
const tableRef = ref();
const travel = ref(null);
const userParams = ref({
dated: Date.vnNew(),
dated: Date.vnNew().toJSON(),
});
const filter = ref({
@ -219,6 +219,7 @@ function round(value) {
data-key="StockBoughts"
url="StockBoughts/getStockBought"
save-url="StockBoughts/crud"
search-url="StockBoughts"
order="reserve DESC"
:right-search="false"
:is-editable="true"

View File

@ -18,7 +18,7 @@ const $props = defineProps({
required: true,
},
});
const customUrl = `StockBoughts/getStockBoughtDetail?workerFk=${$props.workerFk}&date=${$props.dated}`;
const customUrl = `StockBoughts/getStockBoughtDetail?workerFk=${$props.workerFk}&dated=${$props.dated}`;
const columns = [
{
align: 'left',

View File

@ -27,7 +27,7 @@ onMounted(async () => {
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
search-url="table"
search-url="StockBoughts"
@set-user-params="setUserParams"
>
<template #tags="{ tag, formatFn }">
@ -36,12 +36,19 @@ onMounted(async () => {
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params }">
<template #body="{ params, searchFn }">
<QItem class="q-my-sm">
<QItemSection>
<VnInputDate
id="date"
v-model="params.dated"
@update:model-value="
(value) => {
params.dated = value;
setUserParams(params);
searchFn();
}
"
:label="t('Date')"
is-outlined
/>

View File

@ -122,7 +122,7 @@ const cols = computed(() => [
:columns="cols"
:right-search="false"
:disable-option="{ card: true }"
:auto-load="!!$route.query.params"
:auto-load="!!$route.query.table"
>
<template #column-supplierFk="{ row }">
<span class="link" @click.stop>

View File

@ -183,7 +183,7 @@ onMounted(async () => {
<i18n>
en:
invoiceDate: Invoice date
maxShipped: Max date
maxShipped: Max date ticket
allClients: All clients
oneClient: One client
company: Company
@ -195,7 +195,7 @@ en:
es:
invoiceDate: Fecha de factura
maxShipped: Fecha límite
maxShipped: Fecha límite ticket
allClients: Todos los clientes
oneClient: Un solo cliente
company: Empresa

View File

@ -6,15 +6,19 @@ import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { usePrintService } from 'composables/usePrintService';
import VnTable from 'components/VnTable/VnTable.vue';
import { usePrintService } from 'src/composables/usePrintService';
import VnTable from 'src/components/VnTable/VnTable.vue';
import InvoiceOutSummary from './Card/InvoiceOutSummary.vue';
import { toCurrency, toDate } from 'src/filters/index';
import { useStateStore } from 'stores/useStateStore';
import { QBtn } from 'quasar';
import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
import axios from 'axios';
import RightMenu from 'src/components/common/RightMenu.vue';
import InvoiceOutFilter from './InvoiceOutFilter.vue';
import VnRow from 'src/components/ui/VnRow.vue';
import VnRadio from 'src/components/common/VnRadio.vue';
import VnInput from 'src/components/common/VnInput.vue';
import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
const { t } = useI18n();
const stateStore = useStateStore();
@ -26,99 +30,86 @@ const selectedRows = ref([]);
const hasSelectedCards = computed(() => selectedRows.value.length > 0);
const MODEL = 'InvoiceOuts';
const { openReport } = usePrintService();
const addressOptions = ref([]);
const selectedOption = ref('ticket');
async function fetchClientAddress(id) {
const { data } = await axios.get(
`Clients/${id}/addresses?filter[order]=isActive DESC`
);
addressOptions.value = data;
}
const exprBuilder = (_, value) => {
return {
or: [{ code: value }, { description: { like: `%${value}%` } }],
};
};
const columns = computed(() => [
{
align: 'center',
name: 'id',
label: t('invoiceOutList.tableVisibleColumns.id'),
chip: {
condition: () => true,
},
chip: { condition: () => true },
isId: true,
columnFilter: {
name: 'search',
},
columnFilter: { name: 'search' },
},
{
align: 'left',
name: 'ref',
label: t('invoiceOutList.tableVisibleColumns.ref'),
label: t('globals.reference'),
isTitle: true,
component: 'select',
attrs: {
url: MODEL,
optionLabel: 'ref',
optionValue: 'id',
},
columnField: {
component: null,
},
attrs: { url: MODEL, optionLabel: 'ref', optionValue: 'id' },
columnField: { component: null },
},
{
align: 'left',
name: 'Issued',
label: t('invoiceOutList.tableVisibleColumns.issued'),
name: 'issued',
label: t('invoiceOut.summary.issued'),
component: 'date',
format: (row) => toDate(row.issued),
columnField: {
component: null,
},
columnField: { component: null },
},
{
align: 'left',
name: 'clientFk',
label: t('invoiceOutModule.customer'),
label: t('globals.client'),
cardVisible: true,
component: 'select',
attrs: {
url: 'Clients',
fields: ['id', 'name'],
},
columnField: {
component: null,
},
attrs: { url: 'Clients', fields: ['id', 'name'] },
columnField: { component: null },
},
{
align: 'left',
name: 'companyCode',
label: t('invoiceOutModule.company'),
label: t('globals.company'),
cardVisible: true,
component: 'select',
attrs: {
url: 'Companies',
optionLabel: 'code',
optionValue: 'id',
},
columnField: {
component: null,
},
attrs: { url: 'Companies', optionLabel: 'code', optionValue: 'id' },
columnField: { component: null },
},
{
align: 'left',
name: 'amount',
label: t('invoiceOutModule.amount'),
label: t('globals.amount'),
cardVisible: true,
format: (row) => toCurrency(row.amount),
},
{
align: 'left',
name: 'created',
label: t('invoiceOutList.tableVisibleColumns.created'),
label: t('globals.created'),
component: 'date',
columnField: {
component: null,
},
columnField: { component: null },
format: (row) => toDate(row.created),
},
{
align: 'left',
name: 'dued',
label: t('invoiceOutList.tableVisibleColumns.dueDate'),
label: t('invoiceOut.summary.dued'),
component: 'date',
columnField: {
component: null,
},
columnField: { component: null },
format: (row) => toDate(row.dued),
},
{
@ -128,11 +119,12 @@ const columns = computed(() => [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
isPrimary: true,
action: (row) => viewSummary(row.id, InvoiceOutSummary),
},
{
title: t('DownloadPdf'),
icon: 'vn:ticket',
title: t('globals.downloadPdf'),
icon: 'cloud_download',
isPrimary: true,
action: (row) => openPdf(row.id),
},
@ -181,12 +173,12 @@ watchEffect(selectedRows);
<template>
<VnSearchbar
:info="t('youCanSearchByInvoiceReference')"
:label="t('searchInvoice')"
data-key="invoiceOut"
:label="t('Search invoice')"
data-key="invoiceOutList"
/>
<RightMenu>
<template #right-panel>
<InvoiceOutFilter data-key="invoiceOut" />
<InvoiceOutFilter data-key="invoiceOutList" />
</template>
</RightMenu>
<VnSubToolbar>
@ -197,21 +189,19 @@ watchEffect(selectedRows);
@click="downloadPdf()"
:disable="!hasSelectedCards"
>
<QTooltip>{{ t('globals.downloadPdf') }}</QTooltip>
<QTooltip>{{ t('downloadPdf') }}</QTooltip>
</QBtn>
</template>
</VnSubToolbar>
<VnTable
ref="tableRef"
data-key="invoiceOut"
data-key="invoiceOutList"
:url="`${MODEL}/filter`"
:create="{
urlCreate: 'InvoiceOuts/createManualInvoice',
title: t('Create manual invoice'),
title: t('createManualInvoice'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {
active: true,
},
formInitialData: { active: true },
}"
:right-search="false"
v-model:selected="selectedRows"
@ -231,74 +221,203 @@ watchEffect(selectedRows);
</span>
</template>
<template #more-create-dialog="{ data }">
<div class="flex no-wrap flex-center">
<VnSelect
url="Tickets"
v-model="data.ticketFk"
:label="t('invoiceOutList.tableVisibleColumns.ticket')"
option-label="id"
option-value="id"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.nickname }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<span class="q-ml-md">O</span>
<div class="row q-col-gutter-xs">
<div class="col-12">
<div class="q-col-gutter-xs">
<VnRow fixed>
<VnRadio
v-model="selectedOption"
val="ticket"
:label="t('globals.ticket')"
class="q-my-none q-mr-md"
/>
<VnInput
v-show="selectedOption === 'ticket'"
v-model="data.ticketFk"
:label="t('globals.ticket')"
style="flex: 1"
/>
<div
class="row q-col-gutter-xs q-ml-none"
v-show="selectedOption !== 'ticket'"
>
<div class="col">
<VnSelect
v-model="data.clientFk"
:label="t('globals.client')"
url="Clients"
:options="customerOptions"
option-label="name"
option-value="id"
@update:model-value="fetchClientAddress"
>
<template #option="scope">
<QItem
v-bind="scope.itemProps"
@click="selectedClient(scope.opt)"
>
<QItemSection>
<QItemLabel>
#{{ scope.opt?.id }} -
{{ scope.opt?.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</div>
<div class="col">
<VnSelect
v-model="data.addressFk"
:label="t('ticket.summary.consignee')"
:options="addressOptions"
option-label="nickname"
option-value="id"
v-if="
data.clientFk &&
selectedOption === 'consignatario'
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
:class="{
'color-vn-label':
!scope.opt?.isActive,
}"
>
{{
`${
!scope.opt?.isActive
? t('inactive')
: ''
} `
}}
<span>
{{
scope.opt?.nickname
}}</span
>
<span
v-if="
scope.opt?.province ||
scope.opt?.city ||
scope.opt?.street
"
>, {{ scope.opt?.street }},
{{ scope.opt?.city }},
{{
scope.opt?.province?.name
}}
-
{{
scope.opt?.agencyMode
?.name
}}</span
>
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</div>
</div>
</VnRow>
<VnRow fixed>
<VnRadio
v-model="selectedOption"
val="cliente"
:label="t('globals.client')"
class="q-my-none q-mr-md"
/>
</VnRow>
<VnRow fixed>
<VnRadio
v-model="selectedOption"
val="consignatario"
:label="t('ticket.summary.consignee')"
class="q-my-none q-mr-md"
/>
</VnRow>
</div>
</div>
<div class="full-width">
<VnRow class="row q-col-gutter-xs">
<VnSelect
url="InvoiceOutSerials"
v-model="data.serial"
:label="t('invoiceIn.serial')"
:options="invoiceOutSerialsOptions"
option-label="description"
option-value="code"
option-filter
:expr-builder="exprBuilder"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.code }} -
{{ scope.opt?.description }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnInputDate
:label="t('invoiceOut.summary.dued')"
v-model="data.maxShipped"
/>
</VnRow>
<VnRow class="row q-col-gutter-xs">
<VnSelect
url="TaxAreas"
v-model="data.taxArea"
:label="t('invoiceOutList.tableVisibleColumns.taxArea')"
:options="taxAreasOptions"
option-label="code"
option-value="code"
/>
<VnInput
v-model="data.reference"
:label="t('globals.reference')"
/>
</VnRow>
</div>
</div>
<VnSelect
url="Clients"
v-model="data.clientFk"
:label="t('invoiceOutModule.customer')"
:options="customerOptions"
option-label="name"
option-value="id"
/>
<VnSelect
url="InvoiceOutSerials"
v-model="data.serial"
:label="t('invoiceOutList.tableVisibleColumns.invoiceOutSerial')"
:options="invoiceOutSerialsOptions"
option-label="description"
option-value="code"
/>
<VnInputDate
:label="t('invoiceOutList.tableVisibleColumns.dueDate')"
v-model="data.maxShipped"
/>
<VnSelect
url="TaxAreas"
v-model="data.taxArea"
:label="t('invoiceOutList.tableVisibleColumns.taxArea')"
:options="taxAreasOptions"
option-label="code"
option-value="code"
/>
<QInput
v-model="data.reference"
:label="t('invoiceOutList.tableVisibleColumns.ref')"
/>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
#formModel .vn-row {
min-height: 45px;
.q-radio {
align-self: flex-end;
flex: 0.3;
}
> .q-input,
> .q-select {
flex: 0.75;
}
}
</style>
<i18n>
en:
searchInvoice: Search issued invoice
fileDenied: Browser denied file download...
fileAllowed: Successful download of CSV file
youCanSearchByInvoiceReference: You can search by invoice reference
createInvoice: Make invoice
Create manual invoice: Create manual invoice
es:
searchInvoice: Buscar factura emitida
fileDenied: El navegador denegó la descarga de archivos...
fileAllowed: Descarga exitosa de archivo CSV
youCanSearchByInvoiceReference: Puedes buscar por referencia de la factura
createInvoice: Crear factura
Create manual invoice: Crear factura manual
en:
invoiceId: Invoice ID
youCanSearchByInvoiceReference: You can search by invoice reference
createManualInvoice: Create Manual Invoice
inactive: (Inactive)
es:
invoiceId: ID de factura
youCanSearchByInvoiceReference: Puedes buscar por referencia de la factura
createManualInvoice: Crear factura manual
inactive: (Inactivo)
</i18n>

View File

@ -2,6 +2,7 @@ invoiceOutModule:
customer: Client
amount: Amount
company: Company
address: Address
invoiceOutList:
tableVisibleColumns:
id: ID
@ -15,11 +16,11 @@ invoiceOutList:
DownloadPdf: Download PDF
InvoiceOutSummary: Summary
negativeBases:
country: Country
clientId: Client ID
base: Base
ticketId: Ticket
active: Active
hasToInvoice: Has to invoice
verifiedData: Verified data
commercial: Commercial
country: Country
clientId: Client ID
base: Base
ticketId: Ticket
active: Active
hasToInvoice: Has to invoice
verifiedData: Verified data
commercial: Commercial

View File

@ -4,6 +4,7 @@ invoiceOutModule:
customer: Cliente
amount: Importe
company: Empresa
address: Consignatario
invoiceOutList:
tableVisibleColumns:
id: ID

View File

@ -64,8 +64,7 @@ const columns = computed(() => [
},
{
label: t('globals.name'),
field: 'name',
name: 'description',
name: 'name',
...defaultColumnAttrs,
create: true,
cardVisible: true,
@ -426,7 +425,7 @@ function handleOnDataSave({ CrudModelRef }) {
:default-save="false"
data-key="ItemFixedPrices"
url="FixedPrices/filter"
:order="['description DESC']"
:order="['itemFk DESC', 'name DESC']"
save-url="FixedPrices/crud"
:user-params="{ warehouseFk: user.warehouseFk }"
ref="tableRef"
@ -480,7 +479,7 @@ function handleOnDataSave({ CrudModelRef }) {
</template>
</VnSelect>
</template>
<template #column-description="{ row }">
<template #column-name="{ row }">
<span class="link">
{{ row.name }}
</span>

View File

@ -17,21 +17,6 @@ const props = defineProps({
});
const itemTypeWorkersOptions = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'name':
return { 'i.name': { like: `%${value}%` } };
case 'itemFk':
case 'warehouseFk':
case 'rate2':
case 'rate3':
param = `fp.${param}`;
return { [param]: value };
case 'minPrice':
param = `i.${param}`;
return { [param]: value };
}
};
</script>
<template>
@ -66,7 +51,7 @@ const exprBuilder = (param, value) => {
url="Warehouses"
auto-load
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
:label="t('components.itemsFilterPanel.warehouseFk')"
:label="t('globals.warehouse')"
v-model="params.warehouseFk"
option-label="name"
option-value="id"

View File

@ -166,6 +166,7 @@ onMounted(() => {
:expr-builder="exprBuilder"
:custom-tags="['tagGroups', 'categoryFk']"
:redirect="false"
search-url="params"
>
<template #tags="{ tag, formatFn }">
<strong v-if="tag.label === 'typeFk'">

View File

@ -1,6 +1,6 @@
<script setup>
import { useRouter } from 'vue-router';
import { onMounted, ref } from 'vue';
import { reactive, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useState } from 'composables/useState';
@ -9,7 +9,6 @@ import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import { useDialogPluginComponent } from 'quasar';
import { reactive } from 'vue';
const { t } = useI18n();
const state = useState();
@ -58,10 +57,6 @@ const fetchAgencyList = async (landed, addressFk) => {
}
};
// const fetchOrderDetails = (order) => {
// fetchAddressList(order?.addressFk);
// fetchAgencyList(order?.landed, order?.addressFk);
// };
const $props = defineProps({
clientFk: {
type: Number,
@ -73,39 +68,6 @@ const initialFormState = reactive({
addressId: null,
clientFk: $props.clientFk,
});
// const orderMapper = (order) => {
// return {
// addressId: order.addressFk,
// agencyModeId: order.agencyModeFk,
// landed: new Date(order.landed).toISOString(),
// };
// };
// const orderFilter = {
// include: [
// { relation: 'agencyMode', scope: { fields: ['name'] } },
// {
// relation: 'address',
// scope: { fields: ['nickname'] },
// },
// { relation: 'rows', scope: { fields: ['id'] } },
// {
// relation: 'client',
// scope: {
// fields: [
// 'salesPersonFk',
// 'name',
// 'isActive',
// 'isFreezed',
// 'isTaxDataChecked',
// ],
// include: {
// relation: 'salesPersonUser',
// scope: { fields: ['id', 'name'] },
// },
// },
// },
// ],
// };
const onClientChange = async (clientId = $props.clientFk) => {
try {

View File

@ -111,6 +111,7 @@ const columns = computed(() => [
component: 'select',
attrs: {
url: 'Items',
sortBy: 'name ASC ',
fields: ['id', 'name', 'subName'],
},
columnField: {

View File

@ -1,7 +1,7 @@
<script setup>
import axios from 'axios';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { dashIfEmpty, toCurrency, toDate } from 'src/filters';
import OrderSummary from 'pages/Order/Card/OrderSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
@ -14,7 +14,6 @@ import OrderFilter from './Card/OrderFilter.vue';
import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue';
import { toDateTimeFormat } from 'src/filters/date';
import { onMounted } from 'vue';
import { useRoute } from 'vue-router';
const { t } = useI18n();
@ -142,8 +141,13 @@ const columns = computed(() => [
],
},
]);
async function fetchClientAddress(id, formData) {
onMounted(() => {
if (!route.query.createForm) return;
const clientId = route.query.createForm;
const id = JSON.parse(clientId);
fetchClientAddress(id.clientFk);
});
async function fetchClientAddress(id, formData = {}) {
const { data } = await axios.get(`Clients/${id}`, {
params: { filter: { include: { relation: 'addresses' } } },
});
@ -170,13 +174,6 @@ const getDateColor = (date) => {
if (comparation == 0) return 'bg-warning';
if (comparation < 0) return 'bg-success';
};
onMounted(() => {
if (!route.query.createForm) return;
const clientId = route.query.createForm;
const id = JSON.parse(clientId);
fetchClientAddress(id.clientFk, id);
});
</script>
<template>
<OrderSearchbar />
@ -194,7 +191,7 @@ onMounted(() => {
urlCreate: 'Orders/new',
title: t('module.cerateOrder'),
onDataSaved: (url) => {
tableRef.redirect(url);
tableRef.redirect(`${url}/catalog`);
},
formInitialData: {
active: true,

View File

@ -116,7 +116,7 @@ function getApiUrl() {
return new URL(window.location).origin;
}
function getCmrUrl(value) {
return `${getApiUrl()}/api/Routes/${value}/cmr?access_token=${token}`;
return `${getApiUrl()}/api/Cmrs/${value}/print?access_token=${token}`;
}
function downloadPdfs() {
if (!selectedRows.value.length) {
@ -129,7 +129,7 @@ function downloadPdfs() {
let cmrs = [];
for (let value of selectedRows.value) cmrs.push(value.cmrFk);
// prettier-ignore
return window.open(`${getApiUrl()}/api/Routes/downloadCmrsZip?ids=${cmrs.join(',')}&access_token=${token}`);
return window.open(`${getApiUrl()}/api/Cmrs/downloadZip?ids=${cmrs.join(',')}&access_token=${token}`);
}
</script>
<template>
@ -149,7 +149,7 @@ function downloadPdfs() {
<VnTable
ref="tableRef"
data-key="CmrList"
url="Routes/cmrs"
url="Cmrs/filter"
:columns="columns"
:right-search="true"
default-mode="table"

View File

@ -16,12 +16,9 @@ import { useAcl } from 'src/composables/useAcl';
import { useValidator } from 'src/composables/useValidator';
import { toTimeFormat } from 'filters/date.js';
const $props = defineProps({
formData: {
type: Object,
required: true,
default: () => ({}),
},
const formData = defineModel({
type: Object,
required: true,
});
const emit = defineEmits(['updateForm']);
@ -40,7 +37,6 @@ const agenciesOptions = ref([]);
const zonesOptions = ref([]);
const addresses = ref([]);
const zoneSelectRef = ref();
const formData = ref($props.formData);
watch(
() => formData.value,
@ -69,47 +65,28 @@ const zoneWhere = computed(() => {
: {};
});
const getLanded = async (params) => {
try {
const validParams =
shipped.value && addressId.value && agencyModeId.value && warehouseId.value;
if (!validParams) return;
async function getLanded(params) {
getDate(`Agencies/getLanded`, params);
}
formData.value.zoneFk = null;
zonesOptions.value = [];
const { data } = await axios.get(`Agencies/getLanded`, { params });
if (data) {
formData.value.zoneFk = data.zoneFk;
formData.value.landed = data.landed;
formData.value.shipped = params.shipped;
}
} catch (error) {
console.error(error);
notify(t('basicData.noDeliveryZoneAvailable'), 'negative');
async function getShipped(params) {
getDate(`Agencies/getShipped`, params);
}
async function getDate(query, params) {
for (const param in params) {
if (!params[param]) return;
}
};
const getShipped = async (params) => {
try {
const validParams =
landed.value && addressId.value && agencyModeId.value && warehouseId.value;
if (!validParams) return;
formData.value.zoneFk = null;
zonesOptions.value = [];
const { data } = await axios.get(query, { params });
if (!data) return notify(t('basicData.noDeliveryZoneAvailable'), 'negative');
formData.value.zoneFk = null;
zonesOptions.value = [];
const { data } = await axios.get(`Agencies/getShipped`, { params });
if (data) {
formData.value.zoneFk = data.zoneFk;
formData.value.landed = params.landed;
formData.value.shipped = data.shipped;
} else {
notify(t('basicData.noDeliveryZoneAvailable'), 'negative');
}
} catch (error) {
console.error(error);
notify(t('basicData.noDeliveryZoneAvailable'), 'negative');
}
};
formData.value.zoneFk = data.zoneFk;
if (data.landed) formData.value.landed = data.landed;
if (data.shipped) formData.value.shipped = data.shipped;
}
const onChangeZone = async (zoneId) => {
formData.value.agencyModeFk = null;
@ -177,18 +154,26 @@ const clientId = computed({
},
});
const landed = computed({
get: () => formData.value?.landed,
set: (val) => {
formData.value.landed = val;
getShipped({
landed: val,
function addDateParams(obj) {
return {
...obj,
...{
addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk,
});
},
});
},
};
}
async function setLanded(landed) {
if (!landed) return;
getShipped(addDateParams({ landed }));
}
async function setShipped(shipped) {
if (!shipped) return;
getLanded(addDateParams({ shipped }));
}
const agencyModeId = computed({
get: () => formData.value.agencyModeFk,
@ -236,21 +221,6 @@ const warehouseId = computed({
},
});
const shipped = computed({
get: () => formData.value?.shipped,
set: (val) => {
if (new Date(formData.value?.shipped).toDateString() != val.toDateString())
val.setHours(0, 0, 0, 0);
formData.value.shipped = val;
getLanded({
shipped: val,
addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk,
});
},
});
const onFormModelInit = () => {
if (formData.value?.clientFk) clientAddressesList(formData.value?.clientFk);
};
@ -451,18 +421,21 @@ async function getZone(options) {
v-model="formData.shipped"
:required="true"
:rules="validate('ticketList.shipped')"
@update:model-value="setShipped"
/>
<VnInputTime
:label="t('basicData.shippedHour')"
v-model="formData.shipped"
:required="true"
:rules="validate('basicData.shippedHour')"
@update:model-value="setShipped"
/>
<VnInputDate
:label="t('basicData.landed')"
v-model="formData.landed"
:required="true"
:rules="validate('basicData.landed')"
@update:model-value="setLanded"
/>
</VnRow>
</QForm>

View File

@ -158,7 +158,7 @@ onBeforeMount(async () => await getTicketData());
<TicketBasicDataForm
v-if="initialDataLoaded"
@update-form="($event) => (formData = $event)"
:form-data="formData"
v-model="formData"
/>
</QStep>
<QStep :name="2" :title="t('basicData.priceDifference')">

View File

@ -1,9 +1,9 @@
<script setup>
import axios from 'axios';
import { ref, toRefs } from 'vue';
import { computed, ref, toRefs } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useRouter } from 'vue-router';
import { usePrintService } from 'composables/usePrintService';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
@ -23,7 +23,6 @@ const props = defineProps({
required: true,
},
});
const route = useRoute();
const { push, currentRoute } = useRouter();
const { dialog, notify } = useQuasar();
@ -31,7 +30,7 @@ const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const ticketSummary = useArrayData('TicketSummary');
const { ticket } = toRefs(props);
const ticketId = currentRoute.value.params.id;
const ticketId = computed(() => props.ticket.id ?? currentRoute.value.params.id);
const client = ref();
const showTransferDialog = ref(false);
const showTurnDialog = ref(false);
@ -68,7 +67,7 @@ const actions = {
setWeight: async () => {
try {
const invoiceIds = (
await axios.post(`Tickets/${ticketId}/setWeight`, {
await axios.post(`Tickets/${ticketId.value}/setWeight`, {
weight: weight.value,
})
).data;
@ -86,7 +85,7 @@ const actions = {
},
remove: async () => {
try {
await axios.post(`Tickets/${ticketId}/setDeleted`);
await axios.post(`Tickets/${ticketId.value}/setDeleted`);
notify({ message: t('Ticket deleted'), type: 'positive' });
notify({
@ -176,14 +175,14 @@ function showSmsDialog(template, customData) {
}
async function showSmsDialogWithChanges() {
const query = `TicketLogs/${ticketId}/getChanges`;
const query = `TicketLogs/${ticketId.value}/getChanges`;
const response = await axios.get(query);
showSmsDialog('orderChanges', { changes: response.data });
}
async function sendSms(body) {
await axios.post(`Tickets/${ticketId}/sendSms`, body);
await axios.post(`Tickets/${ticketId.value}/sendSms`, body);
notify({
message: 'Notification sent',
type: 'positive',
@ -256,7 +255,10 @@ async function transferClient(client) {
clientFk: client,
};
const { data } = await axios.patch(`Tickets/${ticketId}/transferClient`, params);
const { data } = await axios.patch(
`Tickets/${ticketId.value}/transferClient`,
params
);
if (data) window.location.reload();
}
@ -296,7 +298,10 @@ async function changeShippedHour(time) {
shipped: time,
};
const { data } = await axios.post(`Tickets/${ticketId}/updateEditableTicket`, params);
const { data } = await axios.post(
`Tickets/${ticketId.value}/updateEditableTicket`,
params
);
if (data) window.location.reload();
}
@ -313,7 +318,7 @@ function openRecalculateDialog() {
}
async function recalculateComponents() {
await axios.post(`Tickets/${ticketId}/recalculateComponents`);
await axios.post(`Tickets/${ticketId.value}/recalculateComponents`);
notify({
message: t('Data saved'),
type: 'positive',
@ -336,11 +341,11 @@ async function handleInvoiceOutData() {
}
async function docuwareDownload() {
await axios.get(`Tickets/${ticketId}/docuwareDownload`);
await axios.get(`Tickets/${ticketId.value}/docuwareDownload`);
}
async function hasDocuware() {
const { data } = await axios.post(`Docuwares/${ticketId}/checkFile`, {
const { data } = await axios.post(`Docuwares/${ticketId.value}/checkFile`, {
fileCabinet: 'deliveryNote',
signed: true,
});
@ -371,11 +376,7 @@ async function uploadDocuware(force) {
</script>
<template>
<FetchData
:url="
route.path.startsWith('/ticket')
? `Tickets/${ticketId}/isEditable`
: `Tickets/${ticket}/isEditable`
"
:url="`Tickets/${ticketId}/isEditable`"
auto-load
@on-fetch="handleFetchData"
/>
@ -396,8 +397,6 @@ async function uploadDocuware(force) {
<VnSelect
url="Clients"
:fields="['id', 'name']"
option-label="name"
option-value="id"
v-model="client"
:label="t('Client')"
auto-load

View File

@ -268,6 +268,7 @@ onMounted(() => (stateStore.rightDrawer = false));
:label="t('basicData.price')"
type="number"
min="0"
step="any"
/>
</template>
</VnTable>

View File

@ -94,25 +94,32 @@ const columns = computed(() => [
},
{
align: 'left',
label: t('ticketList.id'),
label: t('globals.id'),
name: 'itemFk',
},
{
align: 'left',
label: t('basicData.quantity'),
label: t('globals.quantity'),
name: 'quantity',
format: (row) => toCurrency(row.quantity),
},
{
align: 'left',
label: t('basicData.item'),
label: t('globals.item'),
name: 'item',
format: (row) => row?.item?.name,
columnClass: 'expand',
},
{
align: 'left',
label: t('basicData.price'),
label: t('globals.size'),
name: 'size',
format: (row) => row?.item?.size,
columnClass: 'expand',
},
{
align: 'left',
label: t('globals.price'),
name: 'price',
format: (row) => toCurrency(row.price),
},
@ -124,7 +131,7 @@ const columns = computed(() => [
},
{
align: 'left',
label: t('ticketList.amount'),
label: t('globals.amount'),
name: 'amount',
format: (row) => parseInt(row.amount * row.quantity),
},

View File

@ -20,6 +20,7 @@ import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnToSummary from 'src/components/ui/VnToSummary.vue';
import TicketDescriptorMenu from './TicketDescriptorMenu.vue';
const route = useRoute();
const { notify } = useNotify();
@ -116,24 +117,36 @@ function toTicketUrl(section) {
{{ entity.nickname }}
</div>
</template>
<template #header-right>
<QBtnDropdown
ref="stateBtnDropdownRef"
color="black"
text-color="white"
:label="t('globals.changeState')"
:disable="!isEditable()"
>
<VnSelect
:options="editableStates"
hide-selected
option-label="name"
option-value="code"
hide-dropdown-icon
focus-on-mount
@update:model-value="changeState"
/>
</QBtnDropdown>
<template #header-right="{ entity }">
<div>
<QBtnDropdown
ref="stateBtnDropdownRef"
color="black"
text-color="white"
:label="t('globals.changeState')"
:disable="!isEditable()"
>
<VnSelect
:options="editableStates"
hide-selected
option-label="name"
option-value="code"
hide-dropdown-icon
focus-on-mount
@update:model-value="changeState"
/>
</QBtnDropdown>
<QBtn color="white" dense flat icon="more_vert" round size="md">
<QTooltip>
{{ t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu>
<QList>
<TicketDescriptorMenu :ticket="entity" />
</QList>
</QMenu>
</QBtn>
</div>
</template>
<template #body="{ entity }">
<QCard class="vn-one">

View File

@ -47,7 +47,7 @@ const getGroupedStates = (data) => {
/>
<FetchData url="AgencyModes" @on-fetch="(data) => (agencies = data)" auto-load />
<FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true" search-url="table">
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>

View File

@ -1,7 +1,7 @@
<script setup>
import axios from 'axios';
import { computed, ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
@ -18,12 +18,13 @@ import RightMenu from 'src/components/common/RightMenu.vue';
import TicketFilter from './TicketFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FetchData from 'src/components/FetchData.vue';
import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
import ZoneDescriptorProxy from '../Zone/Card/ZoneDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import { toTimeFormat } from 'src/filters/date';
import InvoiceOutDescriptorProxy from '../InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const tableRef = ref();
@ -40,16 +41,18 @@ from.setDate(from.getDate() - 7);
const to = Date.vnNew();
to.setHours(23, 59, 0, 0);
to.setDate(to.getDate() + 1);
const userParams = {
from: null,
to: null,
};
// Método para inicializar las variables desde la query string
onMounted(() => {
initializeFromQuery();
stateStore.rightDrawer = true;
if (!route.query.createForm) return;
onClientSelected(JSON.parse(route.query.createForm));
});
const initializeFromQuery = () => {
const query = route.query.table ? JSON.parse(route.query.table) : {};
// Asigna los valores a las variables correspondientes
from.value = query.from || from.toISOString();
to.value = query.to || to.toISOString();
Object.assign(userParams, { from, to });
@ -206,13 +209,19 @@ const columns = computed(() => [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
isPrimary: true,
action: (row) => viewSummary(row.id, TicketSummary),
action: (row, evt) => {
if (evt && evt.ctrlKey) {
const url = router.resolve({
params: { id: row.id },
name: 'TicketCard',
}).href;
window.open(url, '_blank');
} else viewSummary(row.id, TicketSummary);
},
},
],
},
]);
function redirectToLines(id) {
const url = `#/ticket/${id}/sale`;
window.open(url, '_blank');
@ -301,11 +310,6 @@ const getDateColor = (date) => {
if (comparation < 0) return 'bg-success';
};
onMounted(() => {
initializeFromQuery();
stateStore.rightDrawer = true;
});
async function makeInvoice(ticket) {
const ticketsIds = ticket.map((item) => item.id);
const { data } = await axios.post(`Tickets/invoiceTicketsAndPdf`, { ticketsIds });
@ -454,24 +458,24 @@ function setReference(data) {
auto-load
/>
<VnSearchbar
data-key="Ticket"
data-key="TicketList"
:label="t('Search ticket')"
:info="t('You can search by ticket id or alias')"
/>
<RightMenu>
<template #right-panel>
<TicketFilter data-key="Ticket" />
<TicketFilter data-key="TicketList" />
</template>
</RightMenu>
<VnTable
ref="tableRef"
data-key="Ticket"
data-key="TicketList"
url="Tickets/filter"
:create="{
urlCreate: 'Tickets/new',
title: t('ticketList.createTicket'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {},
formInitialData: { clientId: null },
}"
default-mode="table"
:order="['shippedDate DESC', 'shippedHour ASC', 'zoneLanding ASC', 'id']"
@ -573,17 +577,17 @@ function setReference(data) {
</span>
</template>
<template #column-stateFk="{ row }">
<span v-if="getColor(row)">
<QChip :class="getColor(row)" dense square>
{{ row.state }}
</QChip>
</span>
<span v-else-if="row.state === 'Entregado'">
<span v-if="row.refFk">
<span class="link" @click.stop>
{{ row.refFk }}
<InvoiceOutDescriptorProxy :id="row.invoiceOutId" />
</span>
</span>
<span v-else-if="getColor(row)">
<QChip :class="getColor(row)" dense square>
{{ row.state }}
</QChip>
</span>
<span v-else>
{{ row.state }}
</span>
@ -615,6 +619,7 @@ function setReference(data) {
option-value="id"
option-label="name"
hide-selected
required
@update:model-value="(client) => onClientSelected(data)"
:sort-by="'id ASC'"
>
@ -641,6 +646,7 @@ function setReference(data) {
option-label="nickname"
hide-selected
map-options
required
:disable="!data.clientId"
:sort-by="'isActive DESC'"
@update:model-value="() => fetchAvailableAgencies(data)"
@ -691,6 +697,7 @@ function setReference(data) {
option-value="id"
option-label="name"
hide-selected
required
@update:model-value="() => fetchAvailableAgencies(data)"
/>
</div>
@ -704,7 +711,6 @@ function setReference(data) {
option-value="agencyModeFk"
option-label="agencyMode"
hide-selected
:disable="!data.clientId || !data.landed || !data.warehouseId"
/>
</div>
</VnRow>
@ -840,7 +846,14 @@ function setReference(data) {
</QTooltip>
</QPageSticky>
</template>
<style scoped>
.disabled,
.disabled *,
[disabled],
[disabled] * {
cursor: pointer !important;
}
</style>
<i18n>
es:
Search ticket: Buscar ticket

View File

@ -138,6 +138,7 @@ ticketSale:
ok: Ok
more: Más
address: Consignatario
size: Medida
ticketComponents:
serie: Serie
components: Componentes

View File

@ -19,7 +19,7 @@ const router = useRouter();
const { t } = useI18n();
const { notify } = useNotify();
const thermographPaginateRef = ref(null);
const thermographPaginateRef = ref();
const warehouses = ref([]);
const thermographFilter = {
@ -145,7 +145,6 @@ const removeThermograph = async (id) => {
data-key="TravelThermographs"
url="TravelThermographs"
:filter="thermographFilter"
:params="{ travelFk: id }"
auto-load
>
<template #body="{ rows }">

View File

@ -205,6 +205,7 @@ const onThermographCreated = async (data) => {
}"
sort-by="thermographFk ASC"
option-label="thermographFk"
option-filter-value="thermographFk"
:disable="viewAction === 'edit'"
:tooltip="t('New thermograph')"
:roles-allowed-to-create="['logistic']"

View File

@ -24,7 +24,7 @@ defineExpose({ states });
<template>
<FetchData url="warehouses" @on-fetch="(data) => (states = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true" search-url="table">
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>

View File

@ -14,6 +14,7 @@ import WorkerFilter from '../WorkerFilter.vue';
url: 'Workers/filter',
label: 'Search worker',
info: 'You can search by worker id or name',
order: 'id DESC',
}"
:redirect-on-error="true"
/>

View File

@ -70,7 +70,7 @@ function setNotifications(data) {
:default-reset="false"
:default-remove="false"
:default-save="false"
@on-fetch="setNotifications"
@on-fetch="(data) => data && setNotifications(data)"
search-url="notifications"
>
<template #body>

View File

@ -20,12 +20,13 @@ function notIsLocations(ifIsFalse, ifIsTrue) {
<template>
<VnCard
data-key="zone"
base-url="Zones"
:base-url="notIsLocations('Zones', undefined)"
:descriptor="ZoneDescriptor"
:filter-panel="ZoneFilterPanel"
:search-data-key="notIsLocations('ZoneList', 'ZoneLocations')"
:filter-panel="notIsLocations(ZoneFilterPanel, undefined)"
:search-data-key="notIsLocations('ZoneList', undefined)"
:custom-url="`Zones/${route.params?.id}/getLeaves`"
:searchbar-props="{
url: 'Zones',
url: notIsLocations('Zones', 'ZoneLocations'),
label: notIsLocations(t('list.searchZone'), t('list.searchLocation')),
info: t('list.searchInfo'),
whereFilter: notIsLocations((value) => {

View File

@ -5,6 +5,7 @@ import VnInput from 'src/components/common/VnInput.vue';
import { useState } from 'src/composables/useState';
import axios from 'axios';
import { useArrayData } from 'composables/useArrayData';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
const props = defineProps({
rootLabel: {
@ -33,22 +34,23 @@ const state = useState();
const treeRef = ref();
const expanded = ref([]);
const arrayData = useArrayData('ZoneLocations', {
url: `Zones/${route.params.id}/getLeaves`,
const datakey = 'ZoneLocations';
const url = computed(() => `Zones/${route.params.id}/getLeaves`);
const arrayData = useArrayData(datakey, {
url: url.value,
});
const { store } = arrayData;
const storeData = computed(() => store.data);
const nodes = ref([
{
id: null,
name: props.rootLabel,
sons: true,
tickable: false,
noTick: true,
children: [{}],
},
]);
const defaultNode = {
id: null,
name: props.rootLabel,
sons: true,
tickable: false,
noTick: true,
children: [{}],
};
const nodes = ref([defaultNode]);
const _tickedNodes = computed({
get: () => props.tickedNodes,
@ -128,6 +130,7 @@ function getNodeIds(node) {
watch(storeData, async (val) => {
// Se triggerea cuando se actualiza el store.data, el cual es el resultado del fetch de la searchbar
if (!nodes.value[0]) nodes.value = [defaultNode];
nodes.value[0].childs = [...val];
const fetchedNodeKeys = val.flatMap(getNodeIds);
state.set('Tree', [...fetchedNodeKeys]);
@ -193,6 +196,7 @@ onUnmounted(() => {
<QBtn color="primary" icon="search" dense flat @click="reFetch()" />
</template>
</VnInput>
<VnSearchbar :data-key="datakey" :url="url" :redirect="false" />
<QTree
ref="treeRef"
:nodes="nodes"

View File

@ -26,7 +26,7 @@ const exprBuilder = (param, value) => {
<template>
<VnSearchbar
data-key="Zones"
data-key="ZonesList"
url="Zones"
:filter="{
include: { relation: 'agencyMode', scope: { fields: ['name'] } },

View File

@ -105,11 +105,14 @@ watch(
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection v-if="opt.code">
<QItemLabel>{{ opt.code }}</QItemLabel>
<QItemLabel caption
>{{ opt.town?.province?.name }},
{{ opt.town?.province?.country?.name }}</QItemLabel
>
<QItemLabel>
{{ `${opt.code}, ${opt.town?.name}` }}
</QItemLabel>
<QItemLabel caption>
{{
`${opt.town?.province?.name}, ${opt.town?.province?.country?.name}`
}}
</QItemLabel>
</QItemSection>
</QItem>
</template>

View File

@ -22,12 +22,16 @@ const agencies = ref([]);
</script>
<template>
<FetchData url="agencies" @on-fetch="(data) => (agencies = data)" auto-load />
<FetchData
url="AgencyModes"
:filter="{ fields: ['id', 'name'] }"
@on-fetch="(data) => (agencies = data)"
auto-load
/>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:hidden-tags="['search']"
search-url="table"
>
<template #tags="{ tag }">
<div class="q-gutter-x-xs">

View File

@ -73,6 +73,7 @@ const columns = computed(() => [
inWhere: true,
attrs: {
url: 'AgencyModes',
fields: ['id', 'name'],
},
},
columnField: {
@ -138,12 +139,12 @@ onMounted(() => (stateStore.rightDrawer = true));
<ZoneSearchbar />
<RightMenu>
<template #right-panel>
<ZoneFilterPanel data-key="Zones" />
<ZoneFilterPanel data-key="ZonesList" />
</template>
</RightMenu>
<VnTable
ref="tableRef"
data-key="Zones"
data-key="ZonesList"
url="Zones"
:create="{
urlCreate: 'Zones',

View File

@ -113,7 +113,7 @@ export default {
name: 'SupplierAccounts',
meta: {
title: 'accounts',
icon: 'vn:account',
icon: 'vn:credit',
},
component: () =>
import('src/pages/Supplier/Card/SupplierAccounts.vue'),

View File

@ -162,6 +162,15 @@ export const useInvoiceOutGlobalStore = defineStore({
);
throw new Error('Invalid Serial Type');
}
if (clientsToInvoice === 'all' && params.serialType !== 'global') {
notify(
'invoiceOut.globalInvoices.errors.invalidSerialTypeForAll',
'negative'
);
throw new Error('For "all" clients, the serialType must be "global"');
}
if (!params.companyFk) {
notify('invoiceOut.globalInvoices.errors.chooseValidCompany', 'negative');
throw new Error('Invalid company');

View File

@ -8,7 +8,7 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => {
userFilter: {},
userParams: {},
url: '',
limit: 10,
limit: 20,
skip: 0,
order: '',
isLoading: false,

View File

@ -5,7 +5,7 @@ import { useRouter } from 'vue-router';
import * as vueRouter from 'vue-router';
describe('useArrayData', () => {
const filter = '{"limit":10,"skip":0}';
const filter = '{"limit":20,"skip":0}';
const params = { supplierFk: 2 };
beforeEach(() => {
vi.spyOn(useRouter(), 'replace');