0
0
Fork 0

Merge branch 'dev' into 7196-cjsToEsm

This commit is contained in:
Javi Gallego 2024-06-27 06:20:03 +00:00
commit a8c3f1d9e5
124 changed files with 7723 additions and 2278 deletions

View File

@ -1,11 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { Notify } from 'quasar';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { Router } from 'src/router'; import { Router } from 'src/router';
import { i18n } from './i18n'; import useNotify from 'src/composables/useNotify.js';
const session = useSession(); const session = useSession();
const { t } = i18n.global; const { notify } = useNotify();
axios.defaults.baseURL = '/api/'; axios.defaults.baseURL = '/api/';
@ -27,10 +26,7 @@ const onResponse = (response) => {
const isSaveRequest = method === 'patch'; const isSaveRequest = method === 'patch';
if (isSaveRequest) { if (isSaveRequest) {
Notify.create({ notify('globals.dataSaved', 'positive');
message: t('globals.dataSaved'),
type: 'positive',
});
} }
return response; return response;
@ -67,10 +63,7 @@ const onResponseError = (error) => {
return Promise.reject(error); return Promise.reject(error);
} }
Notify.create({ notify(message, 'negative');
message: t(message),
type: 'negative',
});
return Promise.reject(error); return Promise.reject(error);
}; };

View File

@ -27,7 +27,6 @@ export default {
this.$el.addEventListener('keyup', function (evt) { this.$el.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter') { if (evt.key === 'Enter') {
const input = evt.target; const input = evt.target;
console.log('input', input);
if (input.type == 'textarea' && evt.shiftKey) { if (input.type == 'textarea' && evt.shiftKey) {
evt.preventDefault(); evt.preventDefault();
let { selectionStart, selectionEnd } = input; let { selectionStart, selectionEnd } = input;

View File

@ -67,6 +67,10 @@ const $props = defineProps({
default: '', default: '',
description: 'It is used for redirect on click "save and continue"', description: 'It is used for redirect on click "save and continue"',
}, },
hasSubtoolbar: {
type: Boolean,
default: true,
},
}); });
const isLoading = ref(false); const isLoading = ref(false);
@ -75,6 +79,7 @@ const originalData = ref();
const vnPaginateRef = ref(); const vnPaginateRef = ref();
const formData = ref(); const formData = ref();
const saveButtonRef = ref(null); const saveButtonRef = ref(null);
const watchChanges = ref();
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -89,6 +94,7 @@ defineExpose({
saveChanges, saveChanges,
getChanges, getChanges,
formData, formData,
vnPaginateRef,
}); });
async function fetch(data) { async function fetch(data) {
@ -97,19 +103,26 @@ async function fetch(data) {
data.map((d) => (d.$index = $index++)); data.map((d) => (d.$index = $index++));
} }
originalData.value = data && JSON.parse(JSON.stringify(data)); resetData(data);
formData.value = data && JSON.parse(JSON.stringify(data));
watch(formData, () => (hasChanges.value = true), { deep: true });
emit('onFetch', data); emit('onFetch', data);
return data; return data;
} }
function resetData(data) {
if (!data) return;
originalData.value = JSON.parse(JSON.stringify(data));
formData.value = JSON.parse(JSON.stringify(data));
if (watchChanges.value) watchChanges.value(); //destoy watcher
watchChanges.value = watch(formData, () => (hasChanges.value = true), { deep: true });
}
async function reset() { async function reset() {
await fetch(originalData.value); await fetch(originalData.value);
hasChanges.value = false; hasChanges.value = false;
} }
// eslint-disable-next-line vue/no-dupe-keys
function filter(value, update, filterOptions) { function filter(value, update, filterOptions) {
update( update(
() => { () => {
@ -135,7 +148,7 @@ async function onSubmit() {
await saveChanges($props.saveFn ? formData.value : null); await saveChanges($props.saveFn ? formData.value : null);
} }
async function onSumbitAndGo() { async function onSubmitAndGo() {
await onSubmit(); await onSubmit();
push({ path: $props.goTo }); push({ path: $props.goTo });
} }
@ -271,8 +284,9 @@ function isEmpty(obj) {
if (obj.length > 0) return false; if (obj.length > 0) return false;
} }
async function reload() { async function reload(params) {
vnPaginateRef.value.fetch(); const data = await vnPaginateRef.value.fetch(params);
fetch(data);
} }
watch(formUrl, async () => { watch(formUrl, async () => {
@ -284,10 +298,11 @@ watch(formUrl, async () => {
<VnPaginate <VnPaginate
:url="url" :url="url"
:limit="limit" :limit="limit"
v-bind="$attrs"
@on-fetch="fetch" @on-fetch="fetch"
@on-change="resetData"
:skeleton="false" :skeleton="false"
ref="vnPaginateRef" ref="vnPaginateRef"
v-bind="$attrs"
> >
<template #body v-if="formData"> <template #body v-if="formData">
<slot <slot
@ -298,8 +313,8 @@ watch(formUrl, async () => {
></slot> ></slot>
</template> </template>
</VnPaginate> </VnPaginate>
<SkeletonTable v-if="!formData" /> <SkeletonTable v-if="!formData" :columns="$attrs.columns?.length" />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubtoolbar">
<QBtnGroup push style="column-gap: 10px"> <QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" /> <slot name="moreBeforeActions" />
<QBtn <QBtn
@ -324,7 +339,7 @@ watch(formUrl, async () => {
/> />
<QBtnDropdown <QBtnDropdown
v-if="$props.goTo && $props.defaultSave" v-if="$props.goTo && $props.defaultSave"
@click="onSumbitAndGo" @click="onSubmitAndGo"
:label="tMobile('globals.saveAndContinue')" :label="tMobile('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')" :title="t('globals.saveAndContinue')"
:disable="!hasChanges" :disable="!hasChanges"

View File

@ -83,6 +83,10 @@ const $props = defineProps({
default: '', default: '',
description: 'It is used for redirect on click "save and continue"', description: 'It is used for redirect on click "save and continue"',
}, },
reload: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed( const modelValue = computed(
@ -201,6 +205,7 @@ async function save() {
if ($props.urlCreate) notify('globals.dataCreated', 'positive'); if ($props.urlCreate) notify('globals.dataCreated', 'positive');
updateAndEmit('onDataSaved', formData.value, response?.data); updateAndEmit('onDataSaved', formData.value, response?.data);
if ($props.reload) await arrayData.fetch({});
} catch (err) { } catch (err) {
console.error(err); console.error(err);
notify('errors.writeRequest', 'negative'); notify('errors.writeRequest', 'negative');

View File

@ -2,7 +2,8 @@
import { ref, reactive } from 'vue'; import { ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
@ -18,33 +19,68 @@ const $props = defineProps({
}, },
}); });
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
const checked = ref(true);
const transferInvoiceParams = reactive({ const transferInvoiceParams = reactive({
id: $props.invoiceOutData?.id, id: $props.invoiceOutData?.id,
refFk: $props.invoiceOutData?.ref, refFk: $props.invoiceOutData?.ref,
}); });
const closeButton = ref(null);
const clientsOptions = ref([]);
const rectificativeTypeOptions = ref([]); const rectificativeTypeOptions = ref([]);
const siiTypeInvoiceOutsOptions = ref([]); const siiTypeInvoiceOutsOptions = ref([]);
const invoiceCorrectionTypesOptions = ref([]); const invoiceCorrectionTypesOptions = ref([]);
const closeForm = () => { const selectedClient = (client) => {
if (closeButton.value) closeButton.value.click(); transferInvoiceParams.selectedClientData = client;
}; };
const transferInvoice = async () => { const makeInvoice = async () => {
const hasToInvoiceByAddress =
transferInvoiceParams.selectedClientData.hasToInvoiceByAddress;
const params = {
id: transferInvoiceParams.id,
cplusRectificationTypeFk: transferInvoiceParams.cplusRectificationTypeFk,
invoiceCorrectionTypeFk: transferInvoiceParams.invoiceCorrectionTypeFk,
newClientFk: transferInvoiceParams.newClientFk,
refFk: transferInvoiceParams.refFk,
siiTypeInvoiceOutFk: transferInvoiceParams.siiTypeInvoiceOutFk,
makeInvoice: checked.value,
};
try { try {
const { data } = await axios.post( if (checked.value && hasToInvoiceByAddress) {
'InvoiceOuts/transferInvoice', const response = await new Promise((resolve) => {
transferInvoiceParams quasar
); .dialog({
component: VnConfirm,
componentProps: {
title: t('Bill destination client'),
message: t('transferInvoiceInfo'),
},
})
.onOk(() => {
resolve(true);
})
.onCancel(() => {
resolve(false);
});
});
if (!response) {
console.log('entra cuando no checkbox');
return;
}
}
console.log('params: ', params);
const { data } = await axios.post('InvoiceOuts/transferInvoice', params);
console.log('data: ', data);
notify(t('Transferred invoice'), 'positive'); notify(t('Transferred invoice'), 'positive');
closeForm(); const id = data?.[0];
router.push('InvoiceOutSummary', { id: data.id }); if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
} catch (err) { } catch (err) {
console.error('Error transfering invoice', err); console.error('Error transfering invoice', err);
} }
@ -52,22 +88,30 @@ const transferInvoice = async () => {
</script> </script>
<template> <template>
<FetchData
url="Clients"
@on-fetch="(data) => (clientsOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'id', limit: 30 }"
auto-load
/>
<FetchData <FetchData
url="CplusRectificationTypes" url="CplusRectificationTypes"
:filter="{ order: 'description' }" :filter="{ order: 'description' }"
@on-fetch="(data) => (rectificativeTypeOptions = data)" @on-fetch="
(data) => (
(rectificativeTypeOptions = data),
(transferInvoiceParams.cplusRectificationTypeFk = data.filter(
(type) => type.description == 'I Por diferencias'
)[0].id)
)
"
auto-load auto-load
/> />
<FetchData <FetchData
url="SiiTypeInvoiceOuts" url="SiiTypeInvoiceOuts"
:filter="{ where: { code: { like: 'R%' } } }" :filter="{ where: { code: { like: 'R%' } } }"
@on-fetch="(data) => (siiTypeInvoiceOutsOptions = data)" @on-fetch="
(data) => (
(siiTypeInvoiceOutsOptions = data),
(transferInvoiceParams.siiTypeInvoiceOutFk = data.filter(
(type) => type.code == 'R4'
)[0].id)
)
"
auto-load auto-load
/> />
<FetchData <FetchData
@ -76,7 +120,7 @@ const transferInvoice = async () => {
auto-load auto-load
/> />
<FormPopup <FormPopup
@on-submit="transferInvoice()" @on-submit="makeInvoice()"
:title="t('Transfer invoice')" :title="t('Transfer invoice')"
:custom-submit-button-label="t('Transfer client')" :custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false" :default-cancel-button="false"
@ -91,13 +135,18 @@ const transferInvoice = async () => {
option-value="id" option-value="id"
v-model="transferInvoiceParams.newClientFk" v-model="transferInvoiceParams.newClientFk"
:required="true" :required="true"
url="Clients"
:fields="['id', 'name', 'hasToInvoiceByAddress']"
auto-load
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem
v-bind="scope.itemProps"
@click="selectedClient(scope.opt)"
>
<QItemSection> <QItemSection>
<QItemLabel> <QItemLabel>
#{{ scope.opt?.id }} - #{{ scope.opt?.id }} - {{ scope.opt?.name }}
{{ scope.opt?.name }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -144,11 +193,23 @@ const transferInvoice = async () => {
:required="true" :required="true"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div>
<QCheckbox :label="t('Bill destination client')" v-model="checked" />
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('transferInvoiceInfo') }}</QTooltip>
</QIcon>
</div>
</VnRow>
</template> </template>
</FormPopup> </FormPopup>
</template> </template>
<i18n> <i18n>
en:
checkInfo: New tickets from the destination customer will be generated in the consignee by default.
transferInvoiceInfo: Destination customer is marked to bill in the consignee
confirmTransferInvoice: The destination customer has selected to bill in the consignee, do you want to continue?
es: es:
Transfer invoice: Transferir factura Transfer invoice: Transferir factura
Transfer client: Transferir cliente Transfer client: Transferir cliente
@ -157,4 +218,7 @@ es:
Class: Clase Class: Clase
Type: Tipo Type: Tipo
Transferred invoice: Factura transferida Transferred invoice: Factura transferida
Bill destination client: Facturar cliente destino
transferInvoiceInfo: Los nuevos tickets del cliente destino, serán generados en el consignatario por defecto.
confirmTransferInvoice: El cliente destino tiene marcado facturar por consignatario, desea continuar?
</i18n> </i18n>

View File

@ -11,6 +11,7 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { useClipboard } from 'src/composables/useClipboard'; import { useClipboard } from 'src/composables/useClipboard';
import VnImg from 'src/components/ui/VnImg.vue';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
@ -47,7 +48,6 @@ const darkMode = computed({
}); });
const user = state.getUser(); const user = state.getUser();
const token = session.getTokenMultimedia();
const warehousesData = ref(); const warehousesData = ref();
const companiesData = ref(); const companiesData = ref();
const accountBankData = ref(); const accountBankData = ref();
@ -149,10 +149,7 @@ function saveUserData(param, value) {
<div class="col column items-center q-mb-sm"> <div class="col column items-center q-mb-sm">
<QAvatar size="80px"> <QAvatar size="80px">
<QImg <VnImg :id="user.id" collection="user" size="160x160" />
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="white"
/>
</QAvatar> </QAvatar>
<div class="text-subtitle1 q-mt-md"> <div class="text-subtitle1 q-mt-md">

View File

@ -0,0 +1,55 @@
<script setup>
defineProps({
columns: {
type: Array,
required: true,
},
row: {
type: Object,
default: null,
},
});
function stopEventPropagation(event) {
event.preventDefault();
event.stopPropagation();
}
</script>
<template>
<slot name="beforeChip" :row="row"></slot>
<span
v-for="col of columns"
:key="col.name"
@click="stopEventPropagation"
class="cursor-text"
>
<QChip
v-if="col.chip.condition(row[col.name], row)"
:title="col.label"
:class="[
col.chip.color
? col.chip.color(row)
: !col.chip.icon && 'bg-chip-secondary',
col.chip.icon && 'q-px-none',
]"
dense
square
>
<span v-if="!col.chip.icon">{{ row[col.name] }}</span>
<QIcon v-else :name="col.chip.icon" color="primary-light" />
</QChip>
</span>
<slot name="afterChip" :row="row"></slot>
</template>
<style lang="scss">
.bg-chip-secondary {
background-color: var(--vn-page-color);
color: var(--vn-text-color);
}
.cursor-text {
pointer-events: all;
cursor: text;
user-select: all;
}
</style>

View File

@ -0,0 +1,161 @@
<script setup>
import { markRaw, computed, defineModel } from 'vue';
import { QIcon, QCheckbox } from 'quasar';
import { dashIfEmpty } from 'src/filters';
/* basic input */
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnComponent from 'components/common/VnComponent.vue';
const model = defineModel(undefined, { required: true });
const $props = defineProps({
column: {
type: Object,
required: true,
},
row: {
type: Object,
default: () => {},
},
default: {
type: [Object, String],
default: null,
},
componentProp: {
type: String,
default: null,
},
isEditable: {
type: Boolean,
default: true,
},
components: {
type: Object,
default: null,
},
showLabel: {
type: Boolean,
default: null,
},
});
const defaultComponents = {
input: {
component: markRaw(VnInput),
attrs: {
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
number: {
component: markRaw(VnInput),
attrs: {
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
date: {
component: markRaw(VnInputDate),
attrs: {
readonly: true,
disable: !$props.isEditable,
style: 'min-width: 125px',
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
checkbox: {
component: markRaw(QCheckbox),
attrs: (prop) => {
const defaultAttrs = {
disable: !$props.isEditable,
'model-value': Boolean(prop),
class: 'no-padding',
};
if (typeof prop == 'number') {
defaultAttrs['true-value'] = 1;
defaultAttrs['false-value'] = 0;
}
return defaultAttrs;
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
select: {
component: markRaw(VnSelect),
attrs: {
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
icon: {
component: markRaw(QIcon),
},
};
const value = computed(() => {
return $props.column.format
? $props.column.format($props.row, dashIfEmpty)
: dashIfEmpty($props.row[$props.column.name]);
});
const col = computed(() => {
let newColumn = { ...$props.column };
const specific = newColumn[$props.componentProp];
if (specific) {
newColumn = {
...newColumn,
...specific,
...specific.attrs,
...specific.forceAttrs,
};
}
if (
(newColumn.name.startsWith('is') || newColumn.name.startsWith('has')) &&
!newColumn.component
)
newColumn.component = 'checkbox';
if ($props.default && !newColumn.component) newColumn.component = $props.default;
return newColumn;
});
const components = computed(() => $props.components ?? defaultComponents);
</script>
<template>
<div class="row no-wrap fit">
<VnComponent
v-if="col.before"
:prop="col.before"
:components="components"
:value="model"
v-model="model"
/>
<VnComponent
v-if="col.component"
:prop="col"
:components="components"
:value="model"
v-model="model"
/>
<span :title="value" v-else>{{ value }}</span>
<VnComponent
v-if="col.after"
:prop="col.after"
:components="components"
:value="model"
v-model="model"
/>
</div>
</template>

View File

@ -0,0 +1,146 @@
<script setup>
import { markRaw, computed, defineModel } from 'vue';
import { QCheckbox } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
/* basic input */
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue';
const $props = defineProps({
column: {
type: Object,
required: true,
},
showTitle: {
type: Boolean,
default: false,
},
dataKey: {
type: String,
required: true,
},
searchUrl: {
type: String,
default: 'params',
},
});
const model = defineModel(undefined, { required: true });
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
const columnFilter = computed(() => $props.column?.columnFilter);
const updateEvent = { 'update:modelValue': addFilter };
const enterEvent = {
'keyup.enter': () => addFilter(model.value),
remove: () => addFilter(null),
};
const defaultAttrs = {
filled: !$props.showTitle,
class: 'q-px-sm q-pb-xs q-pt-none',
dense: true,
};
const forceAttrs = {
label: $props.showTitle ? '' : $props.column.label,
};
const components = {
input: {
component: markRaw(VnInput),
event: enterEvent,
attrs: {
...defaultAttrs,
clearable: true,
},
forceAttrs,
},
number: {
component: markRaw(VnInput),
event: enterEvent,
attrs: {
...defaultAttrs,
clearable: true,
},
forceAttrs,
},
date: {
component: markRaw(VnInputDate),
event: updateEvent,
attrs: {
...defaultAttrs,
style: 'min-width: 150px',
},
forceAttrs,
},
checkbox: {
component: markRaw(QCheckbox),
event: updateEvent,
attrs: {
dense: true,
class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs',
'toggle-indeterminate': true,
},
forceAttrs,
},
select: {
component: markRaw(VnSelect),
event: updateEvent,
attrs: {
class: 'q-px-md q-pb-xs q-pt-none',
dense: true,
filled: !$props.showTitle,
},
forceAttrs,
},
};
async function addFilter(value) {
value ??= undefined;
if (value && typeof value === 'object') value = model.value;
value = value === '' ? undefined : value;
let field = columnFilter.value?.name ?? $props.column.name;
if (columnFilter.value?.inWhere) {
if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field;
return await arrayData.addFilterWhere({ [field]: value });
}
await arrayData.addFilter({ params: { [field]: value } });
}
function alignRow() {
switch ($props.column.align) {
case 'left':
return 'justify-start items-start';
case 'right':
return 'justify-end items-end';
default:
return 'flex-center';
}
}
const showFilter = computed(
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions'
);
</script>
<template>
<div
v-if="showTitle"
class="q-pt-sm q-px-sm ellipsis"
:class="`text-${column?.align ?? 'left'}`"
:style="!showFilter ? { 'min-height': 72 + 'px' } : ''"
>
{{ column?.label }}
</div>
<div v-if="showFilter" class="full-width" :class="alignRow()">
<VnTableColumn
:column="$props.column"
default="input"
v-model="model"
:components="components"
component-prop="columnFilter"
/>
</div>
</template>

View File

@ -0,0 +1,628 @@
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import FormModelPopup from 'components/FormModelPopup.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnLv from 'components/ui/VnLv.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue';
import VnTableFilter from 'components/VnTable/VnFilter.vue';
import VnTableChip from 'components/VnTable/VnChip.vue';
const $props = defineProps({
columns: {
type: Array,
required: true,
},
defaultMode: {
type: String,
default: 'card', // 'table', 'card'
},
columnSearch: {
type: Boolean,
default: true,
},
rightSearch: {
type: Boolean,
default: true,
},
rowClick: {
type: Function,
default: null,
},
redirect: {
type: String,
default: null,
},
create: {
type: Object,
default: null,
},
cardClass: {
type: String,
default: 'flex-one',
},
searchUrl: {
type: String,
default: 'table',
},
isEditable: {
type: Boolean,
default: false,
},
useModel: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
const stateStore = useStateStore();
const route = useRoute();
const router = useRouter();
const quasar = useQuasar();
const DEFAULT_MODE = 'card';
const TABLE_MODE = 'table';
const mode = ref(DEFAULT_MODE);
const selected = ref([]);
const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}');
const params = ref({ ...routeQuery, ...routeQuery.filter?.where });
const CrudModelRef = ref({});
const showForm = ref(false);
const splittedColumns = ref({ columns: [] });
const tableModes = [
{
icon: 'view_column',
title: t('table view'),
value: TABLE_MODE,
},
{
icon: 'grid_view',
title: t('grid view'),
value: DEFAULT_MODE,
},
];
onMounted(() => {
mode.value = quasar.platform.is.mobile ? DEFAULT_MODE : $props.defaultMode;
stateStore.rightDrawer = true;
setUserParams(route.query[$props.searchUrl]);
});
watch(
() => $props.columns,
(value) => splitColumns(value),
{ immediate: true }
);
watch(
() => route.query[$props.searchUrl],
(val) => setUserParams(val)
);
function setUserParams(watchedParams) {
if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
const where = JSON.parse(watchedParams?.filter)?.where;
watchedParams = { ...watchedParams, ...where };
delete watchedParams.filter;
params.value = { ...params.value, ...watchedParams };
}
function splitColumns(columns) {
splittedColumns.value = {
columns: [],
chips: [],
create: [],
visible: [],
};
for (const col of columns) {
if (col.name == 'tableActions') splittedColumns.value.actions = col;
if (col.chip) splittedColumns.value.chips.push(col);
if (col.isTitle) splittedColumns.value.title = col;
if (col.create) splittedColumns.value.create.push(col);
if (col.cardVisible) splittedColumns.value.visible.push(col);
if ($props.isEditable && col.disable == null) col.disable = false;
if ($props.useModel) col.columnFilter = { ...col.columnFilter, inWhere: true };
splittedColumns.value.columns.push(col);
}
// Status column
if (splittedColumns.value.chips.length) {
splittedColumns.value.columnChips = splittedColumns.value.chips.filter(
(c) => !c.isId
);
if (splittedColumns.value.columnChips.length)
splittedColumns.value.columns.unshift({
align: 'left',
label: t('status'),
name: 'tableStatus',
columnFilter: false,
});
}
}
const rowClickFunction = computed(() => {
if ($props.rowClick) return $props.rowClick;
if ($props.redirect) return ({ id }) => redirectFn(id);
return () => {};
});
function redirectFn(id) {
router.push({ path: `/${$props.redirect}/${id}` });
}
function stopEventPropagation(event) {
event.preventDefault();
event.stopPropagation();
}
function reload(params) {
CrudModelRef.value.reload(params);
}
function columnName(col) {
const column = { ...col, ...col.columnFilter };
let name = column.name;
if (column.alias) name = column.alias + '.' + name;
return name;
}
function getColAlign(col) {
return 'text-' + (col.align ?? 'left')
}
defineExpose({
reload,
redirect: redirectFn,
});
</script>
<template>
<QDrawer
v-if="$props.rightSearch"
v-model="stateStore.rightDrawer"
side="right"
:width="256"
show-if-above
>
<QScrollArea class="fit">
<VnFilterPanel
:data-key="$attrs['data-key']"
:search-button="true"
v-model="params"
:disable-submit-event="true"
:search-url="searchUrl"
>
<template #body>
<VnTableFilter
:column="col"
:data-key="$attrs['data-key']"
v-for="col of splittedColumns.columns"
:key="col.id"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</template>
<slot
name="moreFilterPanel"
:params="params"
:columns="splittedColumns.columns"
/>
</VnFilterPanel>
</QScrollArea>
</QDrawer>
<!-- class in div to fix warn-->
<div class="q-px-md">
<CrudModel
v-bind="$attrs"
:limit="20"
ref="CrudModelRef"
:search-url="searchUrl"
:disable-infinite-scroll="mode == TABLE_MODE"
@save-changes="reload"
:has-subtoolbar="isEditable"
>
<template #body="{ rows }">
<QTable
v-bind="$attrs['QTable']"
class="vnTable"
:columns="splittedColumns.columns"
:rows="rows"
v-model:selected="selected"
:grid="mode != TABLE_MODE"
table-header-class="bg-header"
card-container-class="grid-three"
flat
:style="mode == TABLE_MODE && 'max-height: 90vh'"
virtual-scroll
@virtual-scroll="
(event) =>
event.index > rows.length - 2 &&
CrudModelRef.vnPaginateRef.paginate()
"
@row-click="(_, row) => rowClickFunction(row)"
>
<template #top-left>
<slot name="top-left"></slot>
</template>
<template #top-right>
<!-- <QBtn
icon="visibility"
title="asd"
class="bg-vn-section-color q-mr-md"
dense
v-if="mode == 'table'"
/> -->
<QBtnToggle
v-model="mode"
toggle-color="primary"
class="bg-vn-section-color"
dense
:options="tableModes"
/>
<QBtn
icon="filter_alt"
title="asd"
class="bg-vn-section-color q-ml-md"
dense
@click="stateStore.toggleRightDrawer()"
/>
</template>
<template #header-cell="{ col }">
<QTh
auto-width
style="min-width: 100px"
v-if="$props.columnSearch"
>
<VnTableFilter
:column="col"
:show-title="true"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</QTh>
</template>
<template #header-cell-tableActions>
<QTh auto-width class="sticky" />
</template>
<template #body-cell-tableStatus="{ col, row }">
<QTd auto-width :class="getColAlign(col)">
<VnTableChip
:columns="splittedColumns.columnChips"
:row="row"
>
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QTd>
</template>
<template #body-cell="{ col, row }">
<!-- Columns -->
<QTd
auto-width
class="no-margin q-px-xs"
:class="getColAlign(col)"
>
<VnTableColumn
:column="col"
:row="row"
:is-editable="false"
v-model="row[col.name]"
component-prop="columnField"
/>
</QTd>
</template>
<template #body-cell-tableActions="{ col, row }">
<QTd
auto-width
:class="getColAlign(col)"
class="sticky no-padding"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of col.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-px-sm"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
"
@click="btn.action(row)"
/>
</QTd>
</template>
<template #item="{ row, colsMap }">
<component
:is="$props.redirect ? 'router-link' : 'span'"
:to="`/${$props.redirect}/` + row.id"
>
<QCard
bordered
flat
class="row no-wrap justify-between cursor-pointer"
@click="
(_, row) => {
$props.rowClick && $props.rowClick(row);
}
"
>
<QCardSection
vertical
class="no-margin no-padding"
:class="colsMap.tableActions ? 'w-80' : 'fit'"
>
<!-- Chips -->
<QCardSection
v-if="splittedColumns.chips.length"
class="no-margin q-px-xs q-py-none"
>
<VnTableChip
:columns="splittedColumns.chips"
:row="row"
>
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QCardSection>
<!-- Title -->
<QCardSection
v-if="splittedColumns.title"
class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis"
>
<span
:title="row[splittedColumns.title.name]"
@click="stopEventPropagation($event)"
class="cursor-text"
>
{{ row[splittedColumns.title.name] }}
</span>
</QCardSection>
<!-- Fields -->
<QCardSection
class="q-pl-sm q-pr-lg q-py-xs"
:class="$props.cardClass"
>
<div
v-for="col of splittedColumns.visible"
:key="col.name"
class="fields"
>
<VnLv
:label="
!col.component &&
col.label &&
`${col.label}:`
"
>
<template #value>
<span
@click="
stopEventPropagation($event)
"
>
<VnTableColumn
:column="col"
:row="row"
:is-editable="false"
v-model="row[col.name]"
component-prop="columnField"
:show-label="true"
/>
</span>
</template>
</VnLv>
</div>
</QCardSection>
</QCardSection>
<!-- Actions -->
<QCardSection
v-if="colsMap.tableActions"
class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of splittedColumns.actions
.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-pa-xs"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
"
@click="btn.action(row)"
/>
</QCardSection>
</QCard>
</component>
</template>
</QTable>
</template>
</CrudModel>
</div>
<QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2">
<QBtn @click="showForm = !showForm" color="primary" fab icon="add" />
<QTooltip>
{{ create.title }}
</QTooltip>
</QPageSticky>
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
<FormModelPopup
v-bind="create"
:model="$attrs['data-key'] + 'Create'"
@on-data-saved="(_, res) => create.onDataSaved(res)"
>
<template #form-inputs="{ data }">
<div class="grid-create">
<VnTableColumn
v-for="column of splittedColumns.create"
:key="column.name"
:column="column"
:row="{}"
default="input"
v-model="data[column.name]"
:show-label="true"
/>
<slot name="more-create-dialog" :data="data" />
</div>
</template>
</FormModelPopup>
</QDialog>
</template>
<i18n>
en:
status: Status
es:
status: Estados
</i18n>
<style lang="scss">
.bg-chip-secondary {
background-color: var(--vn-page-color);
color: var(--vn-text-color);
}
.bg-header {
background-color: #5d5d5d;
color: var(--vn-text-color);
}
.q-table--dark .q-table__bottom,
.q-table--dark thead,
.q-table--dark tr,
.q-table--dark th,
.q-table--dark td {
border-color: #222222;
}
.q-table__container > div:first-child {
background-color: var(--vn-page-color);
}
.grid-three {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, max-content));
max-width: 100%;
grid-gap: 20px;
margin: 0 auto;
}
.grid-create {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
max-width: 100%;
grid-gap: 20px;
margin: 0 auto;
}
.flex-one {
display: flex;
flex-flow: row wrap;
div.fields {
width: 100%;
.vn-label-value {
display: flex;
gap: 2%;
}
}
}
.q-table th {
padding: 0;
}
.vnTable {
thead tr th {
position: sticky;
z-index: 2;
}
thead tr:first-child th {
top: 0;
}
.q-table__top {
top: 0;
}
tbody {
.q-checkbox {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label {
margin-left: 31px;
color: var(--vn-text-color);
}
& .q-checkbox__inner {
position: absolute;
left: 0;
color: var(--vn-label-color);
}
}
}
.sticky {
position: sticky;
right: 0;
}
td.sticky {
background-color: var(--q-dark);
z-index: 1;
}
}
.vn-label-value {
display: flex;
flex-direction: row;
color: var(--vn-text-color);
.value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: all;
cursor: text;
user-select: all;
}
}
.cardEllipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.grid-two {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
max-width: 100%;
margin: 0 auto;
overflow: scroll;
white-space: wrap;
width: 100%;
}
.w-80 {
width: 80%;
}
.w-20 {
width: 20%;
}
.cursor-text {
pointer-events: all;
cursor: text;
user-select: all;
}
</style>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { onBeforeMount, computed, watchEffect } from 'vue'; import { onBeforeMount, computed } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router'; import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize'; import useCardSize from 'src/composables/useCardSize';
@ -41,20 +41,6 @@ onBeforeMount(async () => {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
}); });
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${to.params.id}`;
await arrayData.fetch({ append: false });
}
});
}
watchEffect(() => {
if (Array.isArray(arrayData.store.data))
arrayData.store.data = arrayData.store.data[0];
});
</script> </script>
<template> <template>
<QDrawer <QDrawer

View File

@ -0,0 +1,60 @@
<script setup>
import { computed, defineModel } from 'vue';
const model = defineModel(undefined, { required: true });
const $props = defineProps({
prop: {
type: Object,
required: true,
},
components: {
type: Object,
default: () => {},
},
value: {
type: [Object, Number, String],
default: () => {},
},
});
const componentArray = computed(() => {
if (typeof $props.prop === 'object') return [$props.prop];
return $props.prop;
});
function mix(toComponent) {
const { component, attrs, event } = toComponent;
const customComponent = $props.components[component];
return {
component: customComponent?.component ?? component,
attrs: {
...toValueAttrs(attrs),
...toValueAttrs(customComponent?.attrs),
...toComponent,
...toValueAttrs(customComponent?.forceAttrs),
},
event: event ?? customComponent?.event,
};
}
function toValueAttrs(attrs) {
if (!attrs) return;
return typeof attrs == 'function' ? attrs($props.value) : attrs;
}
</script>
<template>
<span
v-for="toComponent of componentArray"
:key="toComponent.name"
class="column flex-center fit"
>
<component
v-if="toComponent?.component"
:is="mix(toComponent).component"
v-bind="mix(toComponent).attrs"
v-on="mix(toComponent).event ?? {}"
v-model="model"
class="fit"
/>
</span>
</template>

View File

@ -2,7 +2,12 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const emit = defineEmits(['update:modelValue', 'update:options', 'keyup.enter']); const emit = defineEmits([
'update:modelValue',
'update:options',
'keyup.enter',
'remove',
]);
const $props = defineProps({ const $props = defineProps({
modelValue: { modelValue: {
@ -17,6 +22,10 @@ const $props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
clearable: {
type: Boolean,
default: true,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -83,7 +92,7 @@ const inputRules = [
<QIcon <QIcon
name="close" name="close"
size="xs" size="xs"
v-if="hover && value && !$attrs.disabled" v-if="hover && value && !$attrs.disabled && $props.clearable"
@click="value = null" @click="value = null"
></QIcon> ></QIcon>
<QIcon v-if="info" name="info"> <QIcon v-if="info" name="info">

View File

@ -1,5 +1,6 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import isValidDate from 'filters/isValidDate'; import isValidDate from 'filters/isValidDate';
const props = defineProps({ const props = defineProps({
@ -24,6 +25,9 @@ const hover = ref(false);
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const joinDateAndTime = (date, time) => { const joinDateAndTime = (date, time) => {
if (!date) { if (!date) {
return null; return null;
@ -91,13 +95,15 @@ const styleAttrs = computed(() => {
readonly readonly
:model-value="displayDate(value)" :model-value="displayDate(value)"
v-bind="{ ...$attrs, ...styleAttrs }" v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: $attrs.required }"
:rules="$attrs.required ? [requiredFieldRule] : null"
@click="isPopupOpen = true" @click="isPopupOpen = true"
> >
<template #append> <template #append>
<QIcon <QIcon
name="close" name="close"
size="xs" size="xs"
v-if="hover && value" v-if="hover && value && !readonly"
@click="onDateUpdate(null)" @click="onDateUpdate(null)"
></QIcon> ></QIcon>
<QIcon name="event" class="cursor-pointer"> <QIcon name="event" class="cursor-pointer">

View File

@ -17,8 +17,9 @@ const props = defineProps({
default: false, default: false,
}, },
}); });
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const value = computed({ const value = computed({
get() { get() {
@ -71,6 +72,8 @@ const styleAttrs = computed(() => {
readonly readonly
:model-value="formatTime(value)" :model-value="formatTime(value)"
v-bind="{ ...$attrs, ...styleAttrs }" v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: $attrs.required }"
:rules="$attrs.required ? [requiredFieldRule] : null"
@click="isPopupOpen = true" @click="isPopupOpen = true"
> >
<template #append> <template #append>

View File

@ -0,0 +1,23 @@
<script setup>
defineProps({
title: { type: String, default: null },
content: { type: [String, Number], default: null },
});
</script>
<template>
<QPopupProxy>
<QCard>
<slot name="title">
<div
class="header q-px-sm q-py-xs q-ma-none text-white text-bold bg-primary"
v-text="title"
/>
</slot>
<slot name="content">
<QCardSection class="change-detail q-pa-sm">
{{ content }}
</QCardSection>
</slot>
</QCard>
</QPopupProxy>
</template>

View File

@ -0,0 +1,97 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
const { t } = useI18n();
const $props = defineProps({
progress: {
type: Number, //Progress value (1.0 > x > 0.0)
required: true,
},
showDialog: {
type: Boolean,
required: true,
},
cancelled: {
type: Boolean,
required: false,
default: false,
},
});
const emit = defineEmits(['cancel', 'close']);
const dialogRef = ref(null);
const _showDialog = computed({
get: () => $props.showDialog,
set: (value) => {
if (value) dialogRef.value.show();
},
});
const _progress = computed(() => $props.progress);
const progressLabel = computed(() => `${Math.round($props.progress * 100)}%`);
const cancel = () => {
dialogRef.value.hide();
emit('cancel');
};
</script>
<template>
<QDialog ref="dialogRef" v-model="_showDialog" @hide="onDialogHide">
<QCard class="full-width dialog">
<QCardSection class="row">
<span class="text-h6">{{ t('Progress') }}</span>
<QSpace />
<QBtn icon="close" flat round dense @click="emit('close')" />
</QCardSection>
<QCardSection>
<div class="column">
<span>{{ t('Total progress') }}:</span>
<QLinearProgress
size="30px"
:value="_progress"
color="primary"
stripe
class="q-mt-sm q-mb-md"
>
<div class="absolute-full flex flex-center">
<QBadge
v-if="cancelled"
text-color="white"
color="negative"
:label="t('Cancelled')"
/>
<span v-else class="text-white text-subtitle1">
{{ progressLabel }}
</span>
</div>
</QLinearProgress>
<slot />
</div>
</QCardSection>
<QCardActions align="right">
<QBtn
v-if="!cancelled && progress < 1"
type="button"
flat
class="text-primary"
@click="cancel()"
>
{{ t('globals.cancel') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
</template>
<i18n>
es:
Progress: Progreso
Total progress: Progreso total
Cancelled: Cancelado
</i18n>

View File

@ -1,6 +1,5 @@
<script setup> <script setup>
import { ref, toRefs, computed, watch } from 'vue'; import { ref, toRefs, computed, watch, onMounted } from 'vue';
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
const emit = defineEmits(['update:modelValue', 'update:options']); const emit = defineEmits(['update:modelValue', 'update:options']);
@ -16,11 +15,11 @@ const $props = defineProps({
}, },
optionLabel: { optionLabel: {
type: [String], type: [String],
default: '', default: 'name',
}, },
optionValue: { optionValue: {
type: String, type: String,
default: '', default: 'id',
}, },
optionFilter: { optionFilter: {
type: String, type: String,
@ -58,6 +57,10 @@ const $props = defineProps({
type: [Number, String], type: [Number, String],
default: '30', default: '30',
}, },
focusOnMount: {
type: Boolean,
default: false,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -116,7 +119,9 @@ async function fetchFilter(val) {
if (new RegExp(/\d/g).test(val)) key = optionFilter.value ?? optionValue.value; if (new RegExp(/\d/g).test(val)) key = optionFilter.value ?? optionValue.value;
const where = { ...{ [key]: { like: `%${val}%` } }, ...$props.where }; const where = { ...{ [key]: { like: `%${val}%` } }, ...$props.where };
return dataRef.value.fetch({ fields, where, order: sortBy, limit }); const fetchOptions = { where, order: sortBy, limit };
if (fields) fetchOptions.fields = fields;
return dataRef.value.fetch(fetchOptions);
} }
async function filterHandler(val, update) { async function filterHandler(val, update) {
@ -146,6 +151,10 @@ watch(modelValue, (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue)) if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue); fetchFilter(newValue);
}); });
onMounted(async () => {
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
</script> </script>
<template> <template>
@ -178,6 +187,7 @@ watch(modelValue, (newValue) => {
> >
<template v-if="isClearable" #append> <template v-if="isClearable" #append>
<QIcon <QIcon
v-show="value"
name="close" name="close"
@click.stop="value = null" @click.stop="value = null"
class="cursor-pointer" class="cursor-pointer"

View File

@ -184,6 +184,7 @@ en:
minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order
{ orderId } of { shipped } to receive it without additional shipping costs.' { orderId } of { shipped } to receive it without additional shipping costs.'
orderChanges: 'Order {orderId} of { shipped }: { changes }' orderChanges: 'Order {orderId} of { shipped }: { changes }'
productNotAvailable: 'Verdnatura communicates: Your order {ticketFk} with reception date on {landed}. {notAvailables} not available. Sorry for the inconvenience.'
en: English en: English
es: Spanish es: Spanish
fr: French fr: French
@ -203,6 +204,7 @@ es:
Te recomendamos amplíes para no generar costes extra, provocarán un incremento de tu tarifa. Te recomendamos amplíes para no generar costes extra, provocarán un incremento de tu tarifa.
¡Un saludo!' ¡Un saludo!'
orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }' orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }'
productNotAvailable: 'Verdnatura le comunica: Pedido {ticketFk} con fecha de recepción {landed}. {notAvailables} no disponible/s. Disculpe las molestias.'
en: Inglés en: Inglés
es: Español es: Español
fr: Francés fr: Francés
@ -222,6 +224,7 @@ fr:
Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }. Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }.
Merci.' Merci.'
orderChanges: 'Commande {orderId} livraison {landing} indisponible/s. Désolés pour le dérangement.' orderChanges: 'Commande {orderId} livraison {landing} indisponible/s. Désolés pour le dérangement.'
productNotAvailable: 'Verdnatura communique : Votre commande {ticketFk} avec date de réception le {landed}. {notAvailables} non disponible. Nous sommes désolés pour les inconvénients.'
en: Anglais en: Anglais
es: Espagnol es: Espagnol
fr: Français fr: Français
@ -240,6 +243,7 @@ pt:
minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido
{ orderId } do dia { landing } para recebê-lo sem custos de envio adicionais.' { orderId } do dia { landing } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } com chegada dia { landing }: { changes }' orderChanges: 'Pedido { orderId } com chegada dia { landing }: { changes }'
productNotAvailable: 'Verdnatura comunica: Seu pedido {ticketFk} com data de recepção em {landed}. {notAvailables} não disponível/eis. Desculpe pelo transtorno.'
en: Inglês en: Inglês
es: Espanhol es: Espanhol
fr: Francês fr: Francês

View File

@ -39,6 +39,7 @@ const $props = defineProps({
}); });
const state = useState(); const state = useState();
const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
let arrayData; let arrayData;
@ -57,7 +58,7 @@ onBeforeMount(async () => {
store = arrayData.store; store = arrayData.store;
entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data)); entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
// It enables to load data only once if the module is the same as the dataKey // It enables to load data only once if the module is the same as the dataKey
if ($props.dataKey !== useRoute().meta.moduleName) await getData(); if ($props.dataKey !== route.meta.moduleName || !route.params.id) await getData();
watch( watch(
() => [$props.url, $props.filter], () => [$props.url, $props.filter],
async () => await getData() async () => await getData()

View File

@ -22,11 +22,15 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
moduleName: {
type: String,
default: null,
},
}); });
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
const route = useRoute(); const route = useRoute();
const isSummary = ref(); const isSummary = ref();
const arrayData = useArrayData(props.dataKey || route.meta.moduleName, { const arrayData = useArrayData(props.dataKey, {
url: props.url, url: props.url,
filter: props.filter, filter: props.filter,
skip: 0, skip: 0,
@ -83,7 +87,7 @@ function existSummary(routes) {
v-if="showRedirectToSummaryIcon" v-if="showRedirectToSummaryIcon"
class="header link" class="header link"
:to="{ :to="{
name: `${route.meta.moduleName}Summary`, name: `${moduleName ?? route.meta.moduleName}Summary`,
params: { id: entityId || entity.id }, params: { id: entityId || entity.id },
}" }"
> >

View File

@ -1,25 +1,38 @@
<script setup>
defineProps({
columns: {
type: Number,
default: 6,
},
});
</script>
<template> <template>
<div class="q-pa-md w"> <div class="q-pa-md q-mx-md container">
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md justify-around no-wrap">
<QSkeleton type="rect" square /> <QSkeleton type="rect" square v-for="n in columns" :key="n" class="column" />
<QSkeleton type="rect" square /> </div>
<QSkeleton type="rect" square /> <div
<QSkeleton type="rect" square /> class="row q-gutter-md q-mb-md justify-around no-wrap"
<QSkeleton type="rect" square /> v-for="n in 5"
<QSkeleton type="rect" square /> :key="n"
</div> >
<div class="row q-gutter-md q-mb-md" v-for="n in 5" :key="n"> <QSkeleton
<QSkeleton type="QInput" square /> type="QInput"
<QSkeleton type="QInput" square /> square
<QSkeleton type="QInput" square /> v-for="m in columns"
<QSkeleton type="QInput" square /> :key="m"
<QSkeleton type="QInput" square /> class="column"
<QSkeleton type="QInput" square /> />
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.w { .container {
width: 80vw; width: 100%;
overflow-x: hidden;
}
.column {
flex-shrink: 0;
width: 200px;
} }
</style> </style>

View File

@ -4,11 +4,11 @@ import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import toDate from 'filters/toDate'; import toDate from 'filters/toDate';
import useRedirect from 'src/composables/useRedirect';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const params = defineModel({ default: {}, required: true, type: Object });
const $props = defineProps({
dataKey: { dataKey: {
type: String, type: String,
required: true, required: true,
@ -18,11 +18,6 @@ const props = defineProps({
required: false, required: false,
default: false, default: false,
}, },
params: {
type: Object,
required: false,
default: null,
},
showAll: { showAll: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -40,12 +35,20 @@ const props = defineProps({
}, },
hiddenTags: { hiddenTags: {
type: Array, type: Array,
default: () => [], default: () => ['filter'],
}, },
customTags: { customTags: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
disableSubmitEvent: {
type: Boolean,
default: false,
},
searchUrl: {
type: String,
default: 'params',
},
redirect: { redirect: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -54,61 +57,64 @@ const props = defineProps({
const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']); const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']);
const arrayData = useArrayData(props.dataKey, { const arrayData = useArrayData($props.dataKey, {
exprBuilder: props.exprBuilder, exprBuilder: $props.exprBuilder,
searchUrl: $props.searchUrl,
navigate: $props.redirect ? {} : null,
}); });
const route = useRoute(); const route = useRoute();
const store = arrayData.store; const store = arrayData.store;
const userParams = ref({});
const { navigate } = useRedirect();
onMounted(() => { onMounted(() => {
if (props.params) userParams.value = JSON.parse(JSON.stringify(props.params)); emit('init', { params: params.value });
if (Object.keys(store.userParams).length > 0) {
userParams.value = JSON.parse(JSON.stringify(store.userParams));
}
emit('init', { params: userParams.value });
}); });
function setUserParams(watchedParams) {
if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
watchedParams = { ...watchedParams, ...watchedParams.filter?.where };
delete watchedParams.filter;
params.value = { ...params.value, ...watchedParams };
}
watch( watch(
() => route.query.params, () => route.query[$props.searchUrl],
(val) => { (val) => setUserParams(val)
if (!val) { );
userParams.value = {};
} else { watch(
const parsedParams = JSON.parse(val); () => arrayData.store.userParams,
userParams.value = { ...parsedParams }; (val) => setUserParams(val)
}
}
); );
const isLoading = ref(false); const isLoading = ref(false);
async function search() { async function search(evt) {
if (evt && $props.disableSubmitEvent) return;
store.filter.where = {}; store.filter.where = {};
isLoading.value = true; isLoading.value = true;
const params = { ...userParams.value }; const filter = { ...params.value };
store.userParamsChanged = true; store.userParamsChanged = true;
store.filter.skip = 0; store.filter.skip = 0;
store.skip = 0; store.skip = 0;
const { params: newParams } = await arrayData.addFilter({ params }); const { params: newParams } = await arrayData.addFilter({ params: params.value });
userParams.value = newParams; params.value = newParams;
if (!props.showAll && !Object.values(params).length) store.data = []; if (!$props.showAll && !Object.values(filter).length) store.data = [];
isLoading.value = false; isLoading.value = false;
emit('search'); emit('search');
if (props.redirect) navigate(store.data, {});
} }
async function reload() { async function reload() {
isLoading.value = true; isLoading.value = true;
const params = Object.values(userParams.value).filter((param) => param); const params = Object.values(params.value).filter((param) => param);
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!props.showAll && !params.length) store.data = []; if (!$props.showAll && !params.length) store.data = [];
isLoading.value = false; isLoading.value = false;
emit('refresh'); emit('refresh');
if (props.redirect) navigate(store.data, {});
} }
async function clearFilters() { async function clearFilters() {
@ -117,18 +123,19 @@ async function clearFilters() {
store.filter.skip = 0; store.filter.skip = 0;
store.skip = 0; store.skip = 0;
// Filtrar los params no removibles // Filtrar los params no removibles
const removableFilters = Object.keys(userParams.value).filter((param) => const removableFilters = Object.keys(params.value).filter((param) =>
props.unremovableParams.includes(param) $props.unremovableParams.includes(param)
); );
const newParams = {}; const newParams = {};
// Conservar solo los params que no son removibles // Conservar solo los params que no son removibles
for (const key of removableFilters) { for (const key of removableFilters) {
newParams[key] = userParams.value[key]; newParams[key] = params.value[key];
} }
userParams.value = { ...newParams }; // Actualizar los params con los removibles params.value = {};
await arrayData.applyFilter({ params: userParams.value }); params.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: params.value });
if (!props.showAll) { if (!$props.showAll) {
store.data = []; store.data = [];
} }
@ -136,36 +143,32 @@ async function clearFilters() {
emit('clear'); emit('clear');
} }
const tagsList = computed(() => const tagsList = computed(() => {
Object.entries(userParams.value) const tagList = [];
.filter(([key, value]) => value && !(props.hiddenTags || []).includes(key)) for (const key of Object.keys(params.value)) {
.map(([key, value]) => ({ const value = params.value[key];
label: key, if (value == null || ($props.hiddenTags || []).includes(key)) continue;
value: value, tagList.push({ label: key, value });
})) }
); return tagList;
});
const tags = computed(() => const tags = computed(() => {
tagsList.value.filter((tag) => !(props.customTags || []).includes(tag.label)) return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key));
); });
const customTags = computed(() => const customTags = computed(() =>
tagsList.value.filter((tag) => (props.customTags || []).includes(tag.label)) tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key))
); );
async function remove(key) { async function remove(key) {
userParams.value[key] = null; params.value[key] = undefined;
await arrayData.applyFilter({ params: userParams.value }); search();
emit('remove', key); emit('remove', key);
} }
function formatValue(value) { function formatValue(value) {
if (typeof value === 'boolean') { if (typeof value === 'boolean') return value ? t('Yes') : t('No');
return value ? t('Yes') : t('No'); if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value);
}
if (isNaN(value) && !isNaN(Date.parse(value))) {
return toDate(value);
}
return `"${value}"`; return `"${value}"`;
} }
@ -226,14 +229,14 @@ function formatValue(value) {
<slot name="tags" :tag="chip" :format-fn="formatValue"> <slot name="tags" :tag="chip" :format-fn="formatValue">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ chip.label }}:</strong> <strong>{{ chip.label }}:</strong>
<span>"{{ chip.value }}"</span> <span>"{{ formatValue(chip.value) }}"</span>
</div> </div>
</slot> </slot>
</VnFilterPanelChip> </VnFilterPanelChip>
<slot <slot
v-if="$slots.customTags" v-if="$slots.customTags"
name="customTags" name="customTags"
:params="userParams" :params="params"
:tags="customTags" :tags="customTags"
:format-fn="formatValue" :format-fn="formatValue"
:search-fn="search" :search-fn="search"
@ -243,9 +246,9 @@ function formatValue(value) {
<QSeparator /> <QSeparator />
</QList> </QList>
<QList dense class="list q-gutter-y-sm q-mt-sm"> <QList dense class="list q-gutter-y-sm q-mt-sm">
<slot name="body" :params="userParams" :search-fn="search"></slot> <slot name="body" :params="params" :search-fn="search"></slot>
</QList> </QList>
<template v-if="props.searchButton"> <template v-if="$props.searchButton">
<QItem> <QItem>
<QItemSection class="q-py-sm"> <QItemSection class="q-py-sm">
<QBtn <QBtn
@ -255,7 +258,7 @@ function formatValue(value) {
dense dense
icon="search" icon="search"
rounded rounded
type="submit" :type="disableSubmitEvent ? 'button' : 'submit'"
unelevated unelevated
/> />
</QItemSection> </QItemSection>
@ -269,7 +272,6 @@ function formatValue(value) {
color="primary" color="primary"
/> />
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.list { .list {
width: 256px; width: 256px;

View File

@ -0,0 +1,68 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useSession } from 'src/composables/useSession';
const $props = defineProps({
storage: {
type: [String, Number],
default: 'Images',
},
collection: {
type: String,
default: 'catalog',
},
size: {
type: String,
default: '200x200',
},
zoomSize: {
type: String,
required: false,
default: 'lg',
},
id: {
type: Number,
required: true,
},
});
const show = ref(false);
const token = useSession().getTokenMultimedia();
const timeStamp = ref(`timestamp=${Date.now()}`);
const url = computed(
() =>
`/api/${$props.storage}/${$props.collection}/${$props.size}/${$props.id}/download?access_token=${token}&${timeStamp.value}`
);
const reload = () => {
timeStamp.value = `timestamp=${Date.now()}`;
};
defineExpose({
reload,
});
onMounted(() => {});
</script>
<template>
<QImg :src="url" v-bind="$attrs" @click="show = !show" spinner-color="primary" />
<QDialog v-model="show" v-if="$props.zoomSize">
<QImg
:src="url"
size="full"
class="img_zoom"
v-bind="$attrs"
spinner-color="primary"
/>
</QDialog>
</template>
<style lang="scss" scoped>
.q-img {
cursor: zoom-in;
min-width: 50px;
}
.rounded {
border-radius: 50%;
}
.img_zoom {
border-radius: 0%;
}
</style>

View File

@ -21,7 +21,7 @@ const currentUser = ref(state.getUser());
const newNote = ref(''); const newNote = ref('');
const vnPaginateRef = ref(); const vnPaginateRef = ref();
function handleKeyUp(event) { function handleKeyUp(event) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
if (!event.shiftKey) insert(); if (!event.shiftKey) insert();
} }
@ -78,6 +78,7 @@ async function insert() {
ref="vnPaginateRef" ref="vnPaginateRef"
class="show" class="show"
v-bind="$attrs" v-bind="$attrs"
search-url="notes"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width"> <TransitionGroup name="list" tag="div" class="column items-center full-width">

View File

@ -58,14 +58,19 @@ const props = defineProps({
type: Function, type: Function,
default: null, default: null,
}, },
searchUrl: {
type: String,
default: null,
},
disableInfiniteScroll: { disableInfiniteScroll: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}); });
const emit = defineEmits(['onFetch', 'onPaginate']); const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']);
const isLoading = ref(false); const isLoading = ref(false);
const mounted = ref(false);
const pagination = ref({ const pagination = ref({
sortBy: props.order, sortBy: props.order,
rowsPerPage: props.limit, rowsPerPage: props.limit,
@ -81,11 +86,13 @@ const arrayData = useArrayData(props.dataKey, {
userParams: props.userParams, userParams: props.userParams,
exprBuilder: props.exprBuilder, exprBuilder: props.exprBuilder,
keepOpts: props.keepOpts, keepOpts: props.keepOpts,
searchUrl: props.searchUrl,
}); });
const store = arrayData.store; const store = arrayData.store;
onMounted(() => { onMounted(async () => {
if (props.autoLoad) fetch(); if (props.autoLoad) await fetch();
mounted.value = true;
}); });
watch( watch(
@ -95,11 +102,22 @@ watch(
} }
); );
watch(
() => store.data,
(data) => emit('onChange', data)
);
watch(
() => props.url,
(url) => fetch({ url })
);
const addFilter = async (filter, params) => { const addFilter = async (filter, params) => {
await arrayData.addFilter({ filter, params }); await arrayData.addFilter({ filter, params });
}; };
async function fetch() { async function fetch(params) {
useArrayData(props.dataKey, params);
store.filter.skip = 0; store.filter.skip = 0;
store.skip = 0; store.skip = 0;
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
@ -107,6 +125,7 @@ async function fetch() {
isLoading.value = false; isLoading.value = false;
} }
emit('onFetch', store.data); emit('onFetch', store.data);
return store.data;
} }
async function paginate() { async function paginate() {
@ -138,7 +157,7 @@ function endPagination() {
emit('onPaginate'); emit('onPaginate');
} }
async function onLoad(index, done) { async function onLoad(index, done) {
if (!store.data) return done(); if (!store.data || !mounted.value) return done();
if (store.data.length === 0 || !props.url) return done(false); if (store.data.length === 0 || !props.url) return done(false);
@ -150,7 +169,7 @@ async function onLoad(index, done) {
done(isDone); done(isDone);
} }
defineExpose({ fetch, addFilter }); defineExpose({ fetch, addFilter, paginate });
</script> </script>
<template> <template>
@ -199,12 +218,6 @@ defineExpose({ fetch, addFilter });
<QSpinner color="orange" size="md" /> <QSpinner color="orange" size="md" />
</div> </div>
</QInfiniteScroll> </QInfiniteScroll>
<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> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -3,7 +3,6 @@ import { onMounted, ref, watch } from 'vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import useRedirect from 'src/composables/useRedirect';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'src/stores/useStateStore'; import { useStateStore } from 'src/stores/useStateStore';
@ -18,17 +17,14 @@ const props = defineProps({
}, },
label: { label: {
type: String, type: String,
required: false,
default: 'Search', default: 'Search',
}, },
info: { info: {
type: String, type: String,
required: false,
default: '', default: '',
}, },
redirect: { redirect: {
type: Boolean, type: Boolean,
required: false,
default: true, default: true,
}, },
url: { url: {
@ -73,10 +69,20 @@ const props = defineProps({
}, },
}); });
let arrayData = useArrayData(props.dataKey, { ...props });
let store = arrayData.store;
const searchText = ref(''); const searchText = ref('');
const { navigate } = useRedirect(); let arrayDataProps = { ...props };
if (props.redirect)
arrayDataProps = {
...props,
...{
navigate: {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
},
},
};
let arrayData = useArrayData(props.dataKey, arrayDataProps);
let store = arrayData.store;
watch( watch(
() => props.dataKey, () => props.dataKey,
@ -106,13 +112,6 @@ async function search() {
search: searchText.value, search: searchText.value,
}, },
}); });
if (!props.redirect) return;
navigate(store.data, {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
});
} }
</script> </script>
<template> <template>

View File

@ -1,5 +1,5 @@
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore'; import { useArrayDataStore } from 'stores/useArrayDataStore';
import { buildFilter } from 'filters/filterPanel'; import { buildFilter } from 'filters/filterPanel';
@ -13,6 +13,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const store = arrayDataStore.get(key); const store = arrayDataStore.get(key);
const route = useRoute(); const route = useRoute();
const router = useRouter();
let canceller = null; let canceller = null;
const page = ref(1); const page = ref(1);
@ -22,8 +23,13 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
store.skip = 0; store.skip = 0;
const query = route.query; const query = route.query;
if (query.params) { const searchUrl = store.searchUrl;
store.userParams = JSON.parse(query.params); if (query[searchUrl]) {
const params = JSON.parse(query[searchUrl]);
const filter = params?.filter;
delete params.filter;
store.userParams = { ...params, ...store.userParams };
store.userFilter = { ...JSON.parse(filter), ...store.userFilter };
} }
}); });
@ -40,13 +46,15 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
'userParams', 'userParams',
'userFilter', 'userFilter',
'exprBuilder', 'exprBuilder',
'searchUrl',
'navigate',
]; ];
if (typeof userOptions === 'object') { if (typeof userOptions === 'object') {
for (const option in userOptions) { for (const option in userOptions) {
const isEmpty = userOptions[option] == null || userOptions[option] === ''; const isEmpty = userOptions[option] == null || userOptions[option] === '';
if (isEmpty || !allowedOptions.includes(option)) continue; if (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) { if (Object.hasOwn(store, option)) {
const defaultOpts = userOptions[option]; const defaultOpts = userOptions[option];
store[option] = userOptions.keepOpts?.includes(option) store[option] = userOptions.keepOpts?.includes(option)
? Object.assign(defaultOpts, store[option]) ? Object.assign(defaultOpts, store[option])
@ -65,7 +73,6 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const filter = { const filter = {
order: store.order, order: store.order,
limit: store.limit, limit: store.limit,
skip: store.skip,
}; };
let exprFilter; let exprFilter;
@ -80,15 +87,15 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
} }
Object.assign(filter, store.userFilter, exprFilter); Object.assign(filter, store.userFilter, exprFilter);
Object.assign(store.filter, filter); Object.assign(store.filter, { ...filter, skip: store.skip });
const params = { const params = {
filter: JSON.stringify(store.filter), filter: JSON.stringify(store.filter),
}; };
Object.assign(params, userParams); Object.assign(params, userParams);
store.isLoading = true;
store.currentFilter = params; store.currentFilter = params;
store.isLoading = true;
const response = await axios.get(store.url, { const response = await axios.get(store.url, {
signal: canceller.signal, signal: canceller.signal,
params, params,
@ -118,6 +125,10 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
} }
} }
function deleteOption(option) {
delete store[option];
}
function cancelRequest() { function cancelRequest() {
if (canceller) { if (canceller) {
canceller.abort(); canceller.abort();
@ -128,7 +139,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function applyFilter({ filter, params }) { async function applyFilter({ filter, params }) {
if (filter) store.userFilter = filter; if (filter) store.userFilter = filter;
store.filter = {}; store.filter = {};
if (params) store.userParams = Object.assign({}, params); if (params) store.userParams = { ...params };
const response = await fetch({ append: false }); const response = await fetch({ append: false });
return response; return response;
@ -137,7 +148,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function addFilter({ filter, params }) { async function addFilter({ filter, params }) {
if (filter) store.userFilter = Object.assign(store.userFilter, filter); if (filter) store.userFilter = Object.assign(store.userFilter, filter);
let userParams = Object.assign({}, store.userParams, params); let userParams = { ...store.userParams, ...params };
userParams = sanitizerParams(userParams, store?.exprBuilder); userParams = sanitizerParams(userParams, store?.exprBuilder);
store.userParams = userParams; store.userParams = userParams;
@ -149,15 +160,20 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
return { filter, params }; return { filter, params };
} }
async function addFilterWhere(where) {
const storedFilter = { ...store.userFilter };
if (!storedFilter?.where) storedFilter.where = {};
where = { ...storedFilter.where, ...where };
await addFilter({ filter: { where } });
}
function sanitizerParams(params, exprBuilder) { function sanitizerParams(params, exprBuilder) {
for (const param in params) { for (const param in params) {
if (params[param] === '' || params[param] === null) { if (params[param] === '' || params[param] === null) {
delete store.userParams[param]; delete store.userParams[param];
delete params[param]; delete params[param];
if (store.filter?.where) { if (store.filter?.where) {
const key = Object.keys( const key = Object.keys(exprBuilder ? exprBuilder(param) : param);
exprBuilder && exprBuilder(param) ? exprBuilder(param) : param
);
if (key[0]) delete store.filter.where[key[0]]; if (key[0]) delete store.filter.where[key[0]];
if (Object.keys(store.filter.where).length === 0) { if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where; delete store.filter.where;
@ -182,22 +198,34 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
} }
function updateStateParams() { function updateStateParams() {
const query = {}; const newUrl = { path: route.path, query: { ...(route.query ?? {}) } };
if (store.order) query.order = store.order; newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter);
if (store.limit) query.limit = store.limit;
if (store.skip) query.skip = store.skip;
if (store.userParams && Object.keys(store.userParams).length !== 0)
query.params = JSON.stringify(store.userParams);
const url = new URL(window.location.href); if (store.navigate) {
const { hash: currentHash } = url; const { customRouteRedirectName, searchText } = store.navigate;
const [currentRoute] = currentHash.split('?'); if (customRouteRedirectName)
return router.push({
name: customRouteRedirectName,
params: { id: searchText },
});
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const params = new URLSearchParams(); const to =
for (const param in query) params.append(param, query[param]); store?.data?.length === 1
? path.replace(/\/(list|:id)|-list/, `/${store.data[0].id}`)
: path.replace(/:id.*/, '');
url.hash = currentRoute + '?' + params.toString(); if (route.path != to) {
window.history.pushState({}, '', url.hash); const pushUrl = { path: to };
if (to.endsWith('/list') || to.endsWith('/'))
pushUrl.query = newUrl.query;
destroy();
return router.push(pushUrl);
}
}
router.replace(newUrl);
} }
const totalRows = computed(() => (store.data && store.data.length) || 0); const totalRows = computed(() => (store.data && store.data.length) || 0);
@ -207,6 +235,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
fetch, fetch,
applyFilter, applyFilter,
addFilter, addFilter,
addFilterWhere,
refresh, refresh,
destroy, destroy,
loadMore, loadMore,
@ -214,5 +243,6 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
totalRows, totalRows,
updateStateParams, updateStateParams,
isLoading, isLoading,
deleteOption,
}; };
} }

View File

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

View File

@ -115,6 +115,13 @@ select:-webkit-autofill {
background-color: var(--vn-accent-color); background-color: var(--vn-accent-color);
} }
.text-primary-light {
color: $primary-light !important;
}
.bg-primary-light {
background: $primary-light !important;
}
.fill-icon { .fill-icon {
font-variation-settings: 'FILL' 1; font-variation-settings: 'FILL' 1;
} }
@ -189,3 +196,26 @@ input::-webkit-inner-spin-button {
.q-scrollarea__content { .q-scrollarea__content {
max-width: 100%; max-width: 100%;
} }
/* ===== Scrollbar CSS ===== /
/ Firefox */
* {
scrollbar-width: auto;
scrollbar-color: var(--vn-label-color) transparent;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-thumb {
background-color: var(--vn-label-color);
border-radius: 10px;
}
*::-webkit-scrollbar-track {
background: transparent;
}

View File

@ -28,7 +28,7 @@ $color-link: #66bfff;
$color-spacer-light: #a3a3a31f; $color-spacer-light: #a3a3a31f;
$color-spacer: #7979794d; $color-spacer: #7979794d;
$border-thin-light: 1px solid $color-spacer-light; $border-thin-light: 1px solid $color-spacer-light;
$primary-light: lighten($primary, 35%); $primary-light: #f5b351;
$dark-shadow-color: black; $dark-shadow-color: black;
$layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d; $layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d;
$spacing-md: 16px; $spacing-md: 16px;

View File

@ -8,11 +8,8 @@ export default function (value, fractionSize = 2) {
const options = { const options = {
style: 'percent', style: 'percent',
minimumFractionDigits: fractionSize, minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize maximumFractionDigits: fractionSize,
}; };
return new Intl.NumberFormat(locale, options) return new Intl.NumberFormat(locale, options).format(parseFloat(value));
.format(parseFloat(value)); }
}

View File

@ -113,6 +113,7 @@ globals:
name: Name name: Name
new: New new: New
comment: Comment comment: Comment
observations: Observations
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred statusInternalServerError: An internal server error has ocurred
@ -279,8 +280,8 @@ customer:
extendedList: extendedList:
tableVisibleColumns: tableVisibleColumns:
id: Identifier id: Identifier
name: Name name: Comercial name
socialName: Social name socialName: Business name
fi: Tax number fi: Tax number
salesPersonFk: Salesperson salesPersonFk: Salesperson
credit: Credit credit: Credit
@ -443,6 +444,10 @@ ticket:
sms: Sms sms: Sms
notes: Notes notes: Notes
sale: Sale sale: Sale
ticketAdvance: Advance tickets
futureTickets: Future tickets
purchaseRequest: Purchase request
weeklyTickets: Weekly tickets
list: list:
nickname: Nickname nickname: Nickname
state: State state: State
@ -844,7 +849,8 @@ worker:
calendar: Calendar calendar: Calendar
timeControl: Time control timeControl: Time control
locker: Locker locker: Locker
balance: Balance
formation: Formation
list: list:
name: Name name: Name
email: Email email: Email
@ -914,7 +920,24 @@ worker:
payMethods: Pay method payMethods: Pay method
iban: IBAN iban: IBAN
bankEntity: Swift / BIC bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: Image not found imageNotFound: Image not found
balance:
tableVisibleColumns:
paymentDate: Date
incomeType: Type
debit: Debt
credit: Have
concept: Concept
wagon: wagon:
pageTitles: pageTitles:
wagons: Wagons wagons: Wagons
@ -992,6 +1015,18 @@ route:
shipped: Preparation date shipped: Preparation date
viewCmr: View CMR viewCmr: View CMR
downloadCmrs: Download CMRs downloadCmrs: Download CMRs
columnLabels:
Id: Id
vehicle: Vehicle
description: Description
isServed: Served
worker: Worker
date: Date
started: Started
actions: Actions
agency: Agency
volume: Volume
finished: Finished
supplier: supplier:
pageTitles: pageTitles:
suppliers: Suppliers suppliers: Suppliers

View File

@ -107,12 +107,14 @@ globals:
aliasUsers: Usuarios aliasUsers: Usuarios
subRoles: Subroles subRoles: Subroles
inheritedRoles: Roles heredados inheritedRoles: Roles heredados
workers: Trabajadores
created: Fecha creación created: Fecha creación
worker: Trabajador worker: Trabajador
now: Ahora now: Ahora
name: Nombre name: Nombre
new: Nuevo new: Nuevo
comment: Comentario comment: Comentario
observations: Observaciones
errors: errors:
statusUnauthorized: Acceso denegado statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor statusInternalServerError: Ha ocurrido un error interno del servidor
@ -277,7 +279,7 @@ customer:
extendedList: extendedList:
tableVisibleColumns: tableVisibleColumns:
id: Identificador id: Identificador
name: Nombre name: Nombre Comercial
socialName: Razón social socialName: Razón social
fi: NIF / CIF fi: NIF / CIF
salesPersonFk: Comercial salesPersonFk: Comercial
@ -441,6 +443,10 @@ ticket:
sms: Sms sms: Sms
notes: Notas notes: Notas
sale: Lineas del pedido sale: Lineas del pedido
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
purchaseRequest: Petición de compra
weeklyTickets: Tickets programados
list: list:
nickname: Alias nickname: Alias
state: Estado state: Estado
@ -839,6 +845,8 @@ worker:
calendar: Calendario calendar: Calendario
timeControl: Control de horario timeControl: Control de horario
locker: Taquilla locker: Taquilla
balance: Balance
formation: Formación
list: list:
name: Nombre name: Nombre
email: Email email: Email
@ -899,7 +907,24 @@ worker:
payMethods: Método de pago payMethods: Método de pago
iban: IBAN iban: IBAN
bankEntity: Swift / BIC bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: No se ha encontrado la imagen imageNotFound: No se ha encontrado la imagen
balance:
tableVisibleColumns:
paymentDate: Fecha
incomeType: Tipo
debit: Debe
credit: Haber
concept: Concepto
wagon: wagon:
pageTitles: pageTitles:
wagons: Vagones wagons: Vagones
@ -977,6 +1002,18 @@ route:
shipped: Fecha preparación shipped: Fecha preparación
viewCmr: Ver CMR viewCmr: Ver CMR
downloadCmrs: Descargar CMRs downloadCmrs: Descargar CMRs
columnLabels:
Id: Id
vehicle: Vehículo
description: Descripción
isServed: Servida
worker: Trabajador
date: Fecha
started: Iniciada
actions: Acciones
agency: Agencia
volume: Volumen
finished: Finalizada
supplier: supplier:
pageTitles: pageTitles:
suppliers: Proveedores suppliers: Proveedores

View File

@ -3,7 +3,6 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';

View File

@ -6,8 +6,8 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue';
import { useSession } from 'src/composables/useSession';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -19,7 +19,6 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
@ -31,10 +30,6 @@ const filter = {
fields: ['id', 'nickname', 'name', 'role'], fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } }, include: { relation: 'role', scope: { fields: ['id', 'name'] } },
}; };
function getAccountAvatar() {
const token = getTokenMultimedia();
return `/api/Images/user/160x160/${entityId.value}/download?access_token=${token}`;
}
const hasAccount = ref(false); const hasAccount = ref(false);
</script> </script>
@ -72,7 +67,8 @@ const hasAccount = ref(false);
<AccountDescriptorMenu :has-account="hasAccount" /> <AccountDescriptorMenu :has-account="hasAccount" />
</template> </template>
<template #before> <template #before>
<QImg :src="getAccountAvatar()" class="photo"> <!-- falla id :id="entityId.value" collection="user" size="160x160" -->
<VnImg :id="entityId" collection="user" size="160x160" class="photo">
<template #error> <template #error>
<div <div
class="absolute-full picture text-center q-pa-md flex flex-center" class="absolute-full picture text-center q-pa-md flex flex-center"
@ -87,7 +83,7 @@ const hasAccount = ref(false);
</div> </div>
</div> </div>
</template> </template>
</QImg> </VnImg>
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('account.card.nickname')" :value="entity.nickname" /> <VnLv :label="t('account.card.nickname')" :value="entity.nickname" />

View File

@ -30,6 +30,7 @@ const filter = {
<template> <template>
<CardSummary <CardSummary
data-key="AccountSummary"
ref="AccountSummary" ref="AccountSummary"
url="VnUsers/preview" url="VnUsers/preview"
:filter="filter" :filter="filter"

View File

@ -33,7 +33,6 @@ function exprBuilder(param, value) {
url="Agencies" url="Agencies"
order="name" order="name"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<CardList <CardList

View File

@ -14,7 +14,7 @@ const entityId = computed(() => $props.id || useRoute().params.id);
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<CardSummary :url="`Agencies/${entityId}`"> <CardSummary :url="`Agencies/${entityId}`" data-key="Agency">
<template #header="{ entity: agency }">{{ agency.name }}</template> <template #header="{ entity: agency }">{{ agency.name }}</template>
<template #body="{ entity: agency }"> <template #body="{ entity: agency }">
<QCard class="vn-one"> <QCard class="vn-one">

View File

@ -10,32 +10,13 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import axios from 'axios'; import axios from 'axios';
import { useSession } from 'src/composables/useSession'; // import { useSession } from 'src/composables/useSession';
import VnImg from 'src/components/ui/VnImg.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession(); // const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); // const token = getTokenMultimedia();
const claimFilter = {
fields: [
'id',
'clientFk',
'created',
'workerFk',
'claimStateFk',
'packages',
'pickup',
],
include: [
{
relation: 'client',
scope: {
fields: ['name'],
},
},
],
};
const claimStates = ref([]); const claimStates = ref([]);
const claimStatesCopy = ref([]); const claimStatesCopy = ref([]);
@ -87,11 +68,10 @@ const statesFilter = {
/> />
<FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load /> <FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load />
<FormModel <FormModel
:url="`Claims/${route.params.id}`" model="Claim"
:url-update="`Claims/updateClaim/${route.params.id}`" :url-update="`Claims/updateClaim/${route.params.id}`"
:filter="claimFilter"
model="claim"
auto-load auto-load
:reload="true"
> >
<template #form="{ data, validate, filter }"> <template #form="{ data, validate, filter }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
@ -118,9 +98,11 @@ const statesFilter = {
> >
<template #before> <template #before>
<QAvatar color="orange"> <QAvatar color="orange">
<QImg <VnImg
v-if="data.workerFk" v-if="data.workerFk"
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`" :size="'160x160'"
:id="data.workerFk"
collection="user"
spinner-color="white" spinner-color="white"
/> />
</QAvatar> </QAvatar>

View File

@ -2,6 +2,7 @@
import VnCard from 'components/common/VnCard.vue'; import VnCard from 'components/common/VnCard.vue';
import ClaimDescriptor from './ClaimDescriptor.vue'; import ClaimDescriptor from './ClaimDescriptor.vue';
import ClaimFilter from '../ClaimFilter.vue'; import ClaimFilter from '../ClaimFilter.vue';
import filter from './ClaimFilter.js';
</script> </script>
<template> <template>
<VnCard <VnCard
@ -13,5 +14,6 @@ import ClaimFilter from '../ClaimFilter.vue';
search-url="Claims/filter" search-url="Claims/filter"
searchbar-label="Search claim" searchbar-label="Search claim"
searchbar-info="You can search by claim id or customer name" searchbar-info="You can search by claim id or customer name"
:filter="filter"
/> />
</template> </template>

View File

@ -12,6 +12,7 @@ import useCardDescription from 'src/composables/useCardDescription';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import filter from './ClaimFilter.js';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -29,49 +30,6 @@ const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const filter = {
include: [
{
relation: 'client',
scope: {
include: [
{ relation: 'salesPersonUser' },
{
relation: 'claimsRatio',
scope: {
fields: ['claimingRate'],
limit: 1,
},
},
],
},
},
{
relation: 'claimState',
},
{
relation: 'ticket',
scope: {
include: [
{ relation: 'zone' },
{
relation: 'address',
scope: {
include: { relation: 'province' },
},
},
],
},
},
{
relation: 'worker',
scope: {
include: { relation: 'user' },
},
},
],
};
const STATE_COLOR = { const STATE_COLOR = {
pending: 'warning', pending: 'warning',
incomplete: 'info', incomplete: 'info',
@ -101,7 +59,7 @@ onMounted(async () => {
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"
@on-fetch="setData" @on-fetch="setData"
data-key="claimData" data-key="Claim"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">
<ClaimDescriptorMenu :claim="entity" /> <ClaimDescriptorMenu :claim="entity" />

View File

@ -0,0 +1,52 @@
export default {
fields: [
'id',
'clientFk',
'created',
'workerFk',
'claimStateFk',
'packages',
'pickup',
'ticketFk',
],
include: [
{
relation: 'client',
scope: {
include: [
{ relation: 'salesPersonUser' },
{
relation: 'claimsRatio',
scope: {
fields: ['claimingRate'],
limit: 1,
},
},
],
},
},
{
relation: 'claimState',
},
{
relation: 'ticket',
scope: {
include: [
{ relation: 'zone' },
{
relation: 'address',
scope: {
include: { relation: 'province' },
},
},
],
},
},
{
relation: 'worker',
scope: {
include: { relation: 'user' },
},
},
],
};

View File

@ -185,6 +185,7 @@ async function changeState(value) {
:url="`Claims/${entityId}/getSummary`" :url="`Claims/${entityId}/getSummary`"
:entity-id="entityId" :entity-id="entityId"
@on-fetch="getClaimDms" @on-fetch="getClaimDms"
data-key="claimSummary"
> >
<template #header="{ entity: { claim } }"> <template #header="{ entity: { claim } }">
{{ claim.id }} - {{ claim.client.name }} ({{ claim.client.id }}) {{ claim.id }} - {{ claim.client.name }} ({{ claim.client.id }})

View File

@ -3,16 +3,14 @@ import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const workers = ref([]); const workers = ref([]);
const workersCopy = ref([]); const workersCopy = ref([]);
@ -143,10 +141,11 @@ const filterOptions = {
> >
<template #prepend> <template #prepend>
<QAvatar color="orange"> <QAvatar color="orange">
<QImg <VnImg
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
v-if="data.salesPersonFk" v-if="data.salesPersonFk"
:id="user.id"
collection="user"
spinner-color="white"
/> />
</QAvatar> </QAvatar>
</template> </template>

View File

@ -10,7 +10,7 @@ import CustomerFilter from '../CustomerFilter.vue';
:descriptor="CustomerDescriptor" :descriptor="CustomerDescriptor"
:filter-panel="CustomerFilter" :filter-panel="CustomerFilter"
search-data-key="CustomerList" search-data-key="CustomerList"
search-url="Clients/filter" search-url="Clients/extendedListFilter"
searchbar-label="Search customer" searchbar-label="Search customer"
searchbar-info="You can search by customer id or name" searchbar-info="You can search by customer id or name"
/> />

View File

@ -61,7 +61,11 @@ const creditWarning = computed(() => {
</script> </script>
<template> <template>
<CardSummary ref="summary" :url="`Clients/${entityId}/summary`"> <CardSummary
ref="summary"
:url="`Clients/${entityId}/summary`"
data-key="CustomerSummary"
>
<template #body="{ entity }"> <template #body="{ entity }">
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle <VnTitle

View File

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

View File

@ -1,94 +1,439 @@
<script setup> <script setup>
import { ref, computed, markRaw } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnLocation from 'src/components/common/VnLocation.vue';
import CustomerFilter from './CustomerFilter.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import CustomerSummary from './Card/CustomerSummary.vue'; import CustomerSummary from './Card/CustomerSummary.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { toDate } from 'src/filters';
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter();
const postcodesOptions = ref([]);
const tableRef = ref();
const columns = computed(() => [
{
align: 'left',
name: 'id',
label: t('customer.extendedList.tableVisibleColumns.id'),
chip: {
condition: () => true,
},
isId: true,
columnFilter: {
component: 'select',
name: 'search',
attrs: {
url: 'Clients',
fields: ['id', 'name'],
},
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.name'),
name: 'name',
isTitle: true,
create: true,
},
{
align: 'left',
name: 'socialName',
label: t('customer.extendedList.tableVisibleColumns.socialName'),
isTitle: true,
create: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.fi'),
name: 'fi',
create: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.salesPersonFk'),
name: 'salesPersonFk',
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
where: { role: 'salesPerson' },
},
create: true,
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.salesPerson),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.credit'),
name: 'credit',
component: 'number',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.creditInsurance'),
name: 'creditInsurance',
component: 'number',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.phone'),
name: 'phone',
cardVisible: true,
columnFilter: {
component: 'number',
},
columnField: {
component: null,
after: {
component: markRaw(VnLinkPhone),
attrs: (prop) => {
return {
'phone-number': prop,
};
},
},
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.mobile'),
name: 'mobile',
cardVisible: true,
columnFilter: {
component: 'number',
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.street'),
name: 'street',
create: true,
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.countryFk'),
name: 'countryFk',
columnFilter: {
component: 'select',
inWhere: true,
alias: 'c',
attrs: {
url: 'Countries',
},
},
format: (row, dashIfEmpty) => dashIfEmpty(row.country),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.provinceFk'),
name: 'provinceFk',
component: 'select',
attrs: {
url: 'Provinces',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.province),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.city'),
name: 'city',
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.postcode'),
name: 'postcode',
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.email'),
name: 'email',
cardVisible: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.created'),
name: 'created',
format: ({ created }) => toDate(created),
component: 'date',
columnFilter: {
alias: 'c',
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.businessTypeFk'),
name: 'businessTypeFk',
create: true,
component: 'select',
attrs: {
url: 'BusinessTypes',
optionLabel: 'description',
optionValue: 'code',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.businessType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.payMethodFk'),
name: 'payMethodFk',
columnFilter: {
component: 'select',
attrs: {
url: 'PayMethods',
},
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.payMethod),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk'),
name: 'sageTaxTypeFk',
columnFilter: {
component: 'select',
attrs: {
optionLabel: 'vat',
url: 'SageTaxTypes',
},
alias: 'sti',
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.sageTaxType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'),
name: 'sageTransactionTypeFk',
columnFilter: {
component: 'select',
attrs: {
optionLabel: 'transaction',
url: 'SageTransactionTypes',
},
alias: 'stt',
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.sageTransactionType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isActive'),
name: 'isActive',
chip: {
color: null,
condition: (value) => !value,
icon: 'vn:disabled',
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isVies'),
name: 'isVies',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isTaxDataChecked'),
name: 'isTaxDataChecked',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isEqualizated'),
name: 'isEqualizated',
create: true,
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isFreezed'),
name: 'isFreezed',
chip: {
color: null,
condition: (value) => value,
icon: 'vn:frozen',
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoice'),
name: 'hasToInvoice',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoiceByAddress'),
name: 'hasToInvoiceByAddress',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isToBeMailed'),
name: 'isToBeMailed',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasLcr'),
name: 'hasLcr',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasCoreVnl'),
name: 'hasCoreVnl',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasSepaVnl'),
name: 'hasSepaVnl',
columnFilter: {
inWhere: true,
},
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('Client ticket list'),
icon: 'vn:ticket',
action: redirectToCreateView,
isPrimary: true,
},
{
title: t('Client ticket list'),
icon: 'preview',
action: (row) => viewSummary(row.id, CustomerSummary),
},
],
},
]);
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const redirectToCreateView = (row) => {
function navigate(id) { router.push({
router.push({ path: `/customer/${id}` }); name: 'TicketList',
} query: {
params: JSON.stringify({
const redirectToCreateView = () => { clientFk: row.id,
router.push({ name: 'CustomerCreate' }); }),
},
});
}; };
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postcode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script> </script>
<template> <template>
<VnSearchbar <VnSearchbar
:info="t('You can search by customer id or name')" :info="t('You can search by customer id or name')"
:label="t('Search customer')" :label="t('Search customer')"
data-key="CustomerList" data-key="Customer"
/> />
<RightMenu> <VnTable
<template #right-panel> ref="tableRef"
<CustomerFilter data-key="CustomerList" /> data-key="Customer"
</template> url="Clients/extendedListFilter"
</RightMenu> :create="{
<QPage class="column items-center q-pa-md"> urlCreate: 'Clients/createWithUser',
<div class="vn-card-list"> title: 'Create client',
<VnPaginate onDataSaved: ({ id }) => tableRef.redirect(id),
auto-load formInitialData: {
data-key="CustomerList" active: true,
order="id DESC" isEqualizated: false,
url="/Clients/filter" },
> }"
<template #body="{ rows }"> order="id DESC"
<CardList :columns="columns"
:id="row.id" default-mode="table"
:key="row.id" redirect="customer"
:title="row.name" auto-load
@click="navigate(row.id)" >
v-for="row of rows" <template #more-create-dialog="{ data }">
> <VnLocation
<template #list-items> :roles-allowed-to-create="['deliveryAssistant']"
<VnLv :label="t('customer.list.email')" :value="row.email" /> :options="postcodesOptions"
<VnLv :value="row.phone"> v-model="data.location"
<template #label> @update:model-value="(location) => handleLocation(data, location)"
{{ t('customer.list.phone') }} />
<VnLinkPhone :phone-number="row.phone" /> <QInput v-model="data.userName" :label="t('Web user')" />
</template> <QInput :label="t('Email')" clearable type="email" v-model="data.email">
</VnLv> <template #append>
</template> <QIcon name="info" class="cursor-info">
<template #actions> <QTooltip max-width="400px">{{
<QBtn t('customer.basicData.youCanSaveMultipleEmails')
:label="t('components.smartCard.openCard')" }}</QTooltip>
@click.stop="navigate(row.id)" </QIcon>
outline
/>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, CustomerSummary)"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
</template> </template>
</VnPaginate> </QInput>
</div> </template>
<QPageSticky :offset="[20, 20]"> </VnTable>
<QBtn @click="redirectToCreateView()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New client') }}
</QTooltip>
</QPageSticky>
</QPage>
</template> </template>
<i18n> <i18n>
es: es:
Search customer: Buscar cliente Web user: Usuario Web
You can search by customer id or name: Puedes buscar por id o nombre del cliente
New client: Nuevo cliente
</i18n> </i18n>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
</style>

View File

@ -22,7 +22,7 @@ const balanceDueTotal = ref(0);
const selected = ref([]); const selected = ref([]);
const tableColumnComponents = { const tableColumnComponents = {
client: { clientFk: {
component: QBtn, component: QBtn,
props: () => ({ flat: true, class: 'link', noCaps: true }), props: () => ({ flat: true, class: 'link', noCaps: true }),
event: () => {}, event: () => {},
@ -40,7 +40,7 @@ const tableColumnComponents = {
props: () => ({ flat: true, class: 'link', noCaps: true }), props: () => ({ flat: true, class: 'link', noCaps: true }),
event: () => {}, event: () => {},
}, },
department: { departmentName: {
component: 'span', component: 'span',
props: () => {}, props: () => {},
event: () => {}, event: () => {},
@ -102,12 +102,12 @@ const columns = computed(() => [
align: 'left', align: 'left',
field: 'clientName', field: 'clientName',
label: t('Client'), label: t('Client'),
name: 'client', name: 'clientFk',
sortable: true, sortable: true,
}, },
{ {
align: 'left', align: 'left',
field: 'isWorker', field: ({ isWorker }) => Boolean(isWorker),
label: t('Is worker'), label: t('Is worker'),
name: 'isWorker', name: 'isWorker',
}, },
@ -122,7 +122,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
field: 'departmentName', field: 'departmentName',
label: t('Department'), label: t('Department'),
name: 'department', name: 'departmentName',
sortable: true, sortable: true,
}, },
{ {
@ -204,48 +204,24 @@ const viewAddObservation = (rowsSelected) => {
}); });
}; };
const departments = ref(new Map());
const onFetch = async (data) => { const onFetch = async (data) => {
const salesPersonFks = data.map((item) => item.salesPersonFk);
const departmentNames = salesPersonFks.map(async (salesPersonFk) => {
try {
const { data: workerDepartment } = await axios.get(
`WorkerDepartments/${salesPersonFk}`
);
const { data: department } = await axios.get(
`Departments/${workerDepartment.departmentFk}`
);
departments.value.set(salesPersonFk, department.name);
} catch (error) {
console.error('Err: ', error);
}
});
const recoveryData = await axios.get('Recoveries'); const recoveryData = await axios.get('Recoveries');
const recoveries = recoveryData.data.map(({ clientFk, finished }) => ({ const recoveries = recoveryData.data.map(({ clientFk, finished }) => ({
clientFk, clientFk,
finished, finished,
})); }));
await Promise.all(departmentNames);
data.forEach((item) => { data.forEach((item) => {
item.departmentName = departments.value.get(item.salesPersonFk);
item.isWorker = item.businessTypeFk === 'worker';
const recovery = recoveries.find(({ clientFk }) => clientFk === item.clientFk); const recovery = recoveries.find(({ clientFk }) => clientFk === item.clientFk);
item.finished = recovery?.finished === null; item.finished = recovery?.finished === null;
}); });
for (const element of data) element.isWorker = element.businessTypeFk === 'worker';
balanceDueTotal.value = data.reduce((acc, { amount = 0 }) => acc + amount, 0); balanceDueTotal.value = data.reduce((acc, { amount = 0 }) => acc + amount, 0);
}; };
function exprBuilder(param, value) { function exprBuilder(param, value) {
switch (param) { switch (param) {
case 'clientFk': case 'clientFk':
return { [`d.${param}`]: value?.id }; return { [`d.${param}`]: value };
case 'creditInsurance': case 'creditInsurance':
case 'amount': case 'amount':
case 'workerFk': case 'workerFk':

View File

@ -1,7 +1,6 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
@ -16,14 +15,13 @@ const props = defineProps({
}, },
}); });
const clients = ref();
const salespersons = ref(); const salespersons = ref();
const countries = ref(); const countries = ref();
const authors = ref(); const authors = ref();
const departments = ref();
</script> </script>
<template> <template>
<FetchData @on-fetch="(data) => (clients = data)" auto-load url="Clients" />
<FetchData <FetchData
:filter="{ where: { role: 'salesPerson' } }" :filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (salespersons = data)" @on-fetch="(data) => (salespersons = data)"
@ -36,6 +34,7 @@ const authors = ref();
auto-load auto-load
url="Workers/activeWithInheritedRole" url="Workers/activeWithInheritedRole"
/> />
<FetchData @on-fetch="(data) => (departments = data)" auto-load url="Departments" />
<VnFilterPanel :data-key="props.dataKey" :search-button="true"> <VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
@ -47,29 +46,22 @@ const authors = ref();
<template #body="{ params, searchFn }"> <template #body="{ params, searchFn }">
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection v-if="clients"> <VnSelect
<VnSelect :label="t('Client')"
:label="t('Client')" url="Clients"
:options="clients" dense
dense option-label="name"
emit-value option-value="id"
hide-selected outlined
map-options rounded
option-label="name" emit-value
option-value="id" hide-selected
outlined map-options
rounded v-model="params.clientFk"
use-input use-input
v-model="params.clientFk" @update:model-value="searchFn()"
@update:model-value="searchFn()" />
auto-load
/>
</QItemSection>
<QItemSection v-else>
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
</QItem> </QItem>
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection v-if="salespersons"> <QItemSection v-if="salespersons">
<VnSelect <VnSelect
@ -93,6 +85,29 @@ const authors = ref();
<QSkeleton class="full-width" type="QInput" /> <QSkeleton class="full-width" type="QInput" />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="departments">
<VnSelect
:input-debounce="0"
:label="t('Departments')"
:options="departments"
dense
emit-value
hide-selected
map-options
option-label="name"
option-value="id"
outlined
rounded
use-input
v-model="params.departmentFk"
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection v-else>
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
</QItem>
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection v-if="countries"> <QItemSection v-if="countries">
@ -104,7 +119,7 @@ const authors = ref();
emit-value emit-value
hide-selected hide-selected
map-options map-options
option-label="country" option-label="name"
option-value="id" option-value="id"
outlined outlined
rounded rounded

View File

@ -1,619 +0,0 @@
<script setup>
import { ref, computed, onBeforeMount, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { QBtn, QIcon } from 'quasar';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import CustomerExtendedListActions from './CustomerExtendedListActions.vue';
import CustomerExtendedListFilter from './CustomerExtendedListFilter.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters';
import RightMenu from 'src/components/common/RightMenu.vue';
const { t } = useI18n();
const router = useRouter();
const stateStore = useStateStore();
const arrayData = ref(null);
onBeforeMount(async () => {
arrayData.value = useArrayData('CustomerExtendedList', {
url: 'Clients/extendedListFilter',
limit: 0,
});
await arrayData.value.fetch({ append: false });
stateStore.rightDrawer = true;
});
onMounted(() => {
const filteredColumns = columns.value.filter(
(col) => col.name !== 'actions' && col.name !== 'customerStatus'
);
allColumnNames.value = filteredColumns.map((col) => col.name);
});
const selectedCustomerId = ref(0);
const selectedSalesPersonId = ref(0);
const allColumnNames = ref([]);
const visibleColumns = ref([]);
const tableColumnComponents = {
customerStatus: {
component: QIcon,
props: (prop) => ({
name: !prop.row.isActive
? 'vn:disabled'
: prop.row.isActive && prop.row.isFreezed
? 'vn:frozen'
: '',
color: 'primary',
size: 'sm',
}),
event: () => {},
},
id: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => {
selectCustomerId(prop.row.id);
},
},
name: {
component: 'span',
props: () => {},
event: () => {},
},
socialName: {
component: 'span',
props: () => {},
event: () => {},
},
fi: {
component: 'span',
props: () => {},
event: () => {},
},
salesPersonFk: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => selectSalesPersonId(prop.row.salesPersonFk),
},
credit: {
component: 'span',
props: () => {},
event: () => {},
},
creditInsurance: {
component: 'span',
props: () => {},
event: () => {},
},
phone: {
component: 'span',
props: () => {},
event: () => {},
},
mobile: {
component: 'span',
props: () => {},
event: () => {},
},
street: {
component: 'span',
props: () => {},
event: () => {},
},
countryFk: {
component: 'span',
props: () => {},
event: () => {},
},
provinceFk: {
component: 'span',
props: () => {},
event: () => {},
},
city: {
component: 'span',
props: () => {},
event: () => {},
},
postcode: {
component: 'span',
props: () => {},
event: () => {},
},
email: {
component: 'span',
props: () => {},
event: () => {},
},
created: {
component: 'span',
props: () => {},
event: () => {},
},
businessTypeFk: {
component: 'span',
props: () => {},
event: () => {},
},
payMethodFk: {
component: 'span',
props: () => {},
event: () => {},
},
sageTaxTypeFk: {
component: 'span',
props: () => {},
event: () => {},
},
sageTransactionTypeFk: {
component: 'span',
props: () => {},
event: () => {},
},
isActive: {
component: QIcon,
props: (prop) => ({
name: prop.row.isActive ? 'check' : 'close',
color: prop.row.isActive ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isVies: {
component: QIcon,
props: (prop) => ({
name: prop.row.isVies ? 'check' : 'close',
color: prop.row.isVies ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isTaxDataChecked: {
component: QIcon,
props: (prop) => ({
name: prop.row.isTaxDataChecked ? 'check' : 'close',
color: prop.row.isTaxDataChecked ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isEqualizated: {
component: QIcon,
props: (prop) => ({
name: prop.row.isEqualizated ? 'check' : 'close',
color: prop.row.isEqualizated ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isFreezed: {
component: QIcon,
props: (prop) => ({
name: prop.row.isFreezed ? 'check' : 'close',
color: prop.row.isFreezed ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasToInvoice: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasToInvoice ? 'check' : 'close',
color: prop.row.hasToInvoice ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasToInvoiceByAddress: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasToInvoiceByAddress ? 'check' : 'close',
color: prop.row.hasToInvoiceByAddress ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isToBeMailed: {
component: QIcon,
props: (prop) => ({
name: prop.row.isToBeMailed ? 'check' : 'close',
color: prop.row.isToBeMailed ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasLcr: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasLcr ? 'check' : 'close',
color: prop.row.hasLcr ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasCoreVnl: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasCoreVnl ? 'check' : 'close',
color: prop.row.hasCoreVnl ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasSepaVnl: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasSepaVnl ? 'check' : 'close',
color: prop.row.hasSepaVnl ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
actions: {
component: CustomerExtendedListActions,
props: (prop) => ({
id: prop.row.id,
}),
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: '',
label: '',
name: 'customerStatus',
format: () => ' ',
},
{
align: 'left',
field: 'id',
label: t('customer.extendedList.tableVisibleColumns.id'),
name: 'id',
},
{
align: 'left',
field: 'name',
label: t('customer.extendedList.tableVisibleColumns.name'),
name: 'name',
},
{
align: 'left',
field: 'socialName',
label: t('customer.extendedList.tableVisibleColumns.socialName'),
name: 'socialName',
},
{
align: 'left',
field: 'fi',
label: t('customer.extendedList.tableVisibleColumns.fi'),
name: 'fi',
},
{
align: 'left',
field: 'salesPerson',
label: t('customer.extendedList.tableVisibleColumns.salesPersonFk'),
name: 'salesPersonFk',
},
{
align: 'left',
field: 'credit',
label: t('customer.extendedList.tableVisibleColumns.credit'),
name: 'credit',
},
{
align: 'left',
field: 'creditInsurance',
label: t('customer.extendedList.tableVisibleColumns.creditInsurance'),
name: 'creditInsurance',
},
{
align: 'left',
field: 'phone',
label: t('customer.extendedList.tableVisibleColumns.phone'),
name: 'phone',
},
{
align: 'left',
field: 'mobile',
label: t('customer.extendedList.tableVisibleColumns.mobile'),
name: 'mobile',
},
{
align: 'left',
field: 'street',
label: t('customer.extendedList.tableVisibleColumns.street'),
name: 'street',
},
{
align: 'left',
field: 'country',
label: t('customer.extendedList.tableVisibleColumns.countryFk'),
name: 'countryFk',
},
{
align: 'left',
field: 'province',
label: t('customer.extendedList.tableVisibleColumns.provinceFk'),
name: 'provinceFk',
},
{
align: 'left',
field: 'city',
label: t('customer.extendedList.tableVisibleColumns.city'),
name: 'city',
},
{
align: 'left',
field: 'postcode',
label: t('customer.extendedList.tableVisibleColumns.postcode'),
name: 'postcode',
},
{
align: 'left',
field: 'email',
label: t('customer.extendedList.tableVisibleColumns.email'),
name: 'email',
},
{
align: 'left',
field: 'created',
label: t('customer.extendedList.tableVisibleColumns.created'),
name: 'created',
format: (value) => toDate(value),
},
{
align: 'left',
field: 'businessType',
label: t('customer.extendedList.tableVisibleColumns.businessTypeFk'),
name: 'businessTypeFk',
},
{
align: 'left',
field: 'payMethod',
label: t('customer.extendedList.tableVisibleColumns.payMethodFk'),
name: 'payMethodFk',
},
{
align: 'left',
field: 'sageTaxType',
label: t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk'),
name: 'sageTaxTypeFk',
},
{
align: 'left',
field: 'sageTransactionType',
label: t('customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'),
name: 'sageTransactionTypeFk',
},
{
align: 'left',
field: 'isActive',
label: t('customer.extendedList.tableVisibleColumns.isActive'),
name: 'isActive',
format: () => ' ',
},
{
align: 'left',
field: 'isVies',
label: t('customer.extendedList.tableVisibleColumns.isVies'),
name: 'isVies',
format: () => ' ',
},
{
align: 'left',
field: 'isTaxDataChecked',
label: t('customer.extendedList.tableVisibleColumns.isTaxDataChecked'),
name: 'isTaxDataChecked',
format: () => ' ',
},
{
align: 'left',
field: 'isEqualizated',
label: t('customer.extendedList.tableVisibleColumns.isEqualizated'),
name: 'isEqualizated',
format: () => ' ',
},
{
align: 'left',
field: 'isFreezed',
label: t('customer.extendedList.tableVisibleColumns.isFreezed'),
name: 'isFreezed',
format: () => ' ',
},
{
align: 'left',
field: 'hasToInvoice',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoice'),
name: 'hasToInvoice',
format: () => ' ',
},
{
align: 'left',
field: 'hasToInvoiceByAddress',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoiceByAddress'),
name: 'hasToInvoiceByAddress',
format: () => ' ',
},
{
align: 'left',
field: 'isToBeMailed',
label: t('customer.extendedList.tableVisibleColumns.isToBeMailed'),
name: 'isToBeMailed',
format: () => ' ',
},
{
align: 'left',
field: 'hasLcr',
label: t('customer.extendedList.tableVisibleColumns.hasLcr'),
name: 'hasLcr',
format: () => ' ',
},
{
align: 'left',
field: 'hasCoreVnl',
label: t('customer.extendedList.tableVisibleColumns.hasCoreVnl'),
name: 'hasCoreVnl',
format: () => ' ',
},
{
align: 'left',
field: 'hasSepaVnl',
label: t('customer.extendedList.tableVisibleColumns.hasSepaVnl'),
name: 'hasSepaVnl',
format: () => ' ',
},
{
align: 'right',
field: 'actions',
label: '',
name: 'actions',
},
]);
const stopEventPropagation = (event, col) => {
if (!['id', 'salesPersonFk'].includes(col.name)) return;
event.preventDefault();
event.stopPropagation();
};
const navigateToTravelId = (id) => router.push({ path: `/customer/${id}` });
const selectCustomerId = (id) => (selectedCustomerId.value = id);
const selectSalesPersonId = (id) => (selectedSalesPersonId.value = id);
</script>
<template>
<RightMenu>
<template #right-panel>
<CustomerExtendedListFilter
v-if="visibleColumns.length !== 0"
data-key="CustomerExtendedList"
:visible-columns="visibleColumns"
/>
</template>
</RightMenu>
<VnSubToolbar>
<template #st-data>
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="clientsDetail"
labels-traductions-path="customer.extendedList.tableVisibleColumns"
@on-config-saved="
visibleColumns = ['customerStatus', ...$event, 'actions']
"
/>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<VnPaginate
data-key="CustomerExtendedList"
url="Clients/extendedListFilter"
auto-load
>
<template #body="{ rows }">
<div class="q-pa-md">
<QTable
:columns="columns"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
:visible-columns="visibleColumns"
@row-click="(evt, row, id) => navigateToTravelId(row.id)"
>
<template #body-cell="{ col, value }">
<QTd @click="stopEventPropagation($event, col)">
{{ value }}
</QTd>
</template>
<template #body-cell-customerStatus="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
</component>
</QTd>
</template>
<template #body-cell-id="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
<CustomerDescriptorProxy :id="props.row.id" />
{{ props.row.id }}
</component>
</QTd>
</template>
<template #body-cell-salesPersonFk="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
v-if="props.row.salesPerson"
class="col-content"
:is="tableColumnComponents[props.col.name].component"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
<WorkerDescriptorProxy
:id="props.row.salesPersonFk"
/>
{{ props.row.salesPerson }}
</component>
<span class="col-content" v-else>-</span>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
/>
</QTd>
</template>
</QTable>
</div>
</template>
</VnPaginate>
</QPage>
</template>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
</style>

View File

@ -1,60 +0,0 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import CustomerSummary from '../Card/CustomerSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
const { t } = useI18n();
const router = useRouter();
const { viewSummary } = useSummaryDialog();
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
const redirectToCreateView = () => {
router.push({
name: 'TicketList',
query: {
params: JSON.stringify({
clientFk: $props.id,
}),
},
});
};
</script>
<template>
<div>
<QIcon
@click.stop="redirectToCreateView"
color="primary"
name="vn:ticket"
size="sm"
>
<QTooltip>
{{ t('Client ticket list') }}
</QTooltip>
</QIcon>
<QIcon
@click.stop="viewSummary($props.id, CustomerSummary)"
class="q-ml-md"
color="primary"
name="preview"
size="sm"
>
<QTooltip>
{{ t('Preview') }}
</QTooltip>
</QIcon>
</div>
</template>
<i18n>
es:
Client ticket list: Listado de tickets del cliente
Preview: Vista previa
</i18n>

View File

@ -1,571 +0,0 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.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';
const props = defineProps({
dataKey: {
type: String,
required: true,
},
visibleColumns: {
type: Array,
required: true,
},
});
const { t } = useI18n();
const clients = ref();
const workers = ref();
const countriesOptions = ref([]);
const provincesOptions = ref([]);
const paymethodsOptions = ref([]);
const businessTypesOptions = ref([]);
const sageTaxTypesOptions = ref([]);
const sageTransactionTypesOptions = ref([]);
const visibleColumnsSet = computed(() => new Set(props.visibleColumns));
const exprBuilder = (param, value) => {
switch (param) {
case 'created':
return {
'c.created': {
between: dateRange(value),
},
};
case 'id':
case 'name':
case 'socialName':
case 'fi':
case 'credit':
case 'creditInsurance':
case 'phone':
case 'mobile':
case 'street':
case 'city':
case 'postcode':
case 'email':
case 'isActive':
case 'isVies':
case 'isTaxDataChecked':
case 'isEqualizated':
case 'isFreezed':
case 'hasToInvoice':
case 'hasToInvoiceByAddress':
case 'isToBeMailed':
case 'hasSepaVnl':
case 'hasLcr':
case 'hasCoreVnl':
case 'countryFk':
case 'provinceFk':
case 'salesPersonFk':
case 'businessTypeFk':
case 'payMethodFk':
case 'sageTaxTypeFk':
case 'sageTransactionTypeFk':
return { [`c.${param}`]: value };
}
};
const shouldRenderColumn = (colName) => {
return visibleColumnsSet.value.has(colName);
};
</script>
<template>
<FetchData
url="Clients"
:filter="{ where: { role: 'socialName' } }"
@on-fetch="(data) => (clients = data)"
auto-load
/>
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<FetchData
url="Countries"
:filter="{ fields: ['id', 'country'], order: 'country ASC' }"
@on-fetch="(data) => (countriesOptions = data)"
auto-load
/>
<FetchData
ref="provincesFetchDataRef"
@on-fetch="(data) => (provincesOptions = data)"
auto-load
url="Provinces"
/>
<FetchData
url="Paymethods"
@on-fetch="(data) => (paymethodsOptions = data)"
auto-load
/>
<FetchData
url="BusinessTypes"
@on-fetch="(data) => (businessTypesOptions = data)"
auto-load
/>
<FetchData
url="SageTaxTypes"
auto-load
@on-fetch="(data) => (sageTaxTypesOptions = data)"
/>
<FetchData
url="sageTransactionTypes"
auto-load
@on-fetch="(data) => (sageTransactionTypesOptions = data)"
/>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:expr-builder="exprBuilder"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong
>{{ t(`customer.extendedList.tableVisibleColumns.${tag.label}`) }}:
</strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QItem v-if="shouldRenderColumn('id')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.id')"
v-model="params.id"
is-outlined
clearable
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('name')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.name')"
v-model="params.name"
is-outlined
/>
</QItemSection>
</QItem>
<!-- <QItem class="q-mb-sm">
<QItemSection v-if="!clients">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="clients">
<VnSelect
:label="t('Social name')"
v-model="params.socialName"
@update:model-value="searchFn()"
:options="clients"
option-value="socialName"
option-label="socialName"
emit-value
map-options
use-input
hide-selected
dense
outlined
rounded
:input-debounce="0"
/>
</QItemSection>
</QItem> -->
<QItem v-if="shouldRenderColumn('fi')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.fi')"
v-model="params.fi"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('salesPersonFk')">
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.salesPersonFk')
"
v-model="params.salesPersonFk"
@update:model-value="searchFn()"
:options="workers"
option-value="id"
option-label="name"
emit-value
map-options
use-input
hide-selected
dense
outlined
rounded
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('credit')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.credit')"
v-model="params.credit"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('creditInsurance')">
<QItemSection>
<VnInput
:label="
t('customer.extendedList.tableVisibleColumns.creditInsurance')
"
v-model="params.creditInsurance"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('phone')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.phone')"
v-model="params.phone"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('mobile')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.mobile')"
v-model="params.mobile"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('street')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.street')"
v-model="params.street"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('countryFk')">
<QItemSection>
<VnSelect
:label="t('customer.extendedList.tableVisibleColumns.countryFk')"
v-model="params.countryFk"
@update:model-value="searchFn()"
:options="countriesOptions"
option-value="id"
option-label="country"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('provinceFk')">
<QItemSection>
<VnSelect
:label="t('customer.extendedList.tableVisibleColumns.provinceFk')"
v-model="params.provinceFk"
@update:model-value="searchFn()"
:options="provincesOptions"
option-value="id"
option-label="name"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('city')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.city')"
v-model="params.city"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('postcode')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.postcode')"
v-model="params.postcode"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('email')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.email')"
v-model="params.email"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('created')">
<QItemSection>
<VnInputDate
v-model="params.created"
:label="t('customer.extendedList.tableVisibleColumns.created')"
@update:model-value="searchFn()"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('businessTypeFk')">
<QItemSection>
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.businessTypeFk')
"
v-model="params.businessTypeFk"
:options="businessTypesOptions"
@update:model-value="searchFn()"
option-value="code"
option-label="description"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('payMethodFk')">
<QItemSection>
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.payMethodFk')
"
v-model="params.payMethodFk"
:options="paymethodsOptions"
@update:model-value="searchFn()"
option-value="id"
option-label="name"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('sageTaxTypeFk')">
<QItemSection>
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk')
"
v-model="params.sageTaxTypeFk"
@update:model-value="searchFn()"
:options="sageTaxTypesOptions"
option-value="id"
option-label="vat"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('sageTransactionTypeFk')">
<QItemSection>
<VnSelect
:label="
t(
'customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'
)
"
v-model="params.sageTransactionTypeFk"
@update:model-value="searchFn()"
:options="sageTransactionTypesOptions"
option-value="id"
option-label="transaction"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('isActive') || shouldRenderColumn('isVies')">
<QItemSection v-if="shouldRenderColumn('isActive')">
<QCheckbox
v-model="params.isActive"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.isActive')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('isVies')">
<QCheckbox
v-model="params.isVies"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.isVies')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="
shouldRenderColumn('isEqualizated') ||
shouldRenderColumn('isTaxDataChecked')
"
>
<QItemSection v-if="shouldRenderColumn('isTaxDataChecked')">
<QCheckbox
v-model="params.isTaxDataChecked"
@update:model-value="searchFn()"
:label="
t(
'customer.extendedList.tableVisibleColumns.isTaxDataChecked'
)
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('isEqualizated')">
<QCheckbox
v-model="params.isEqualizated"
@update:model-value="searchFn()"
:label="
t('customer.extendedList.tableVisibleColumns.isEqualizated')
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="
shouldRenderColumn('hasToInvoice') || shouldRenderColumn('isFreezed')
"
>
<QItemSection v-if="shouldRenderColumn('isFreezed')">
<QCheckbox
v-model="params.isFreezed"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.isFreezed')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('hasToInvoice')">
<QCheckbox
v-model="params.hasToInvoice"
@update:model-value="searchFn()"
:label="
t('customer.extendedList.tableVisibleColumns.hasToInvoice')
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="
shouldRenderColumn('isToBeMailed') ||
shouldRenderColumn('hasToInvoiceByAddress')
"
>
<QItemSection v-if="shouldRenderColumn('hasToInvoiceByAddress')">
<QCheckbox
v-model="params.hasToInvoiceByAddress"
@update:model-value="searchFn()"
:label="
t(
'customer.extendedList.tableVisibleColumns.hasToInvoiceByAddress'
)
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('isToBeMailed')">
<QCheckbox
v-model="params.isToBeMailed"
@update:model-value="searchFn()"
:label="
t('customer.extendedList.tableVisibleColumns.isToBeMailed')
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="shouldRenderColumn('hasLcr') || shouldRenderColumn('hasCoreVnl')"
>
<QItemSection v-if="shouldRenderColumn('hasLcr')">
<QCheckbox
v-model="params.hasLcr"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.hasLcr')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('hasCoreVnl')">
<QCheckbox
v-model="params.hasCoreVnl"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.hasCoreVnl')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('hasSepaVnl')">
<QItemSection>
<QCheckbox
v-model="params.hasSepaVnl"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.hasSepaVnl')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QSeparator />
</template>
</VnFilterPanel>
</template>
<i18n>
es:
Social name: Razón social
</i18n>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import DepartmentDescriptor from './DepartmentDescriptor.vue'; import DepartmentDescriptor from './DepartmentDescriptor.vue';
import DepartmentSummaryDialog from './DepartmentSummaryDialog.vue'; import DepartmentSummary from './DepartmentSummary.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -15,7 +15,7 @@ const $props = defineProps({
<DepartmentDescriptor <DepartmentDescriptor
v-if="$props.id" v-if="$props.id"
:id="$props.id" :id="$props.id"
:summary="DepartmentSummaryDialog" :summary="DepartmentSummary"
/> />
</QPopupProxy> </QPopupProxy>
</template> </template>

View File

@ -32,6 +32,7 @@ onMounted(async () => {
:url="`Departments/${entityId}`" :url="`Departments/${entityId}`"
class="full-width" class="full-width"
style="max-width: 900px" style="max-width: 900px"
module-name="Department"
> >
<template #header="{ entity }"> <template #header="{ entity }">
<div>{{ entity.name }}</div> <div>{{ entity.name }}</div>

View File

@ -11,9 +11,9 @@ import VnInput from 'src/components/common/VnInput.vue';
import FetchedTags from 'components/ui/FetchedTags.vue'; import FetchedTags from 'components/ui/FetchedTags.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
@ -22,7 +22,6 @@ const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore();
const { notify } = useNotify(); const { notify } = useNotify();
const rowsSelected = ref([]); const rowsSelected = ref([]);
@ -312,20 +311,22 @@ const lockIconType = (groupingMode, mode) => {
auto-load auto-load
@on-fetch="(data) => (packagingsOptions = data)" @on-fetch="(data) => (packagingsOptions = data)"
/> />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <VnSubToolbar>
<QBtnGroup push style="column-gap: 10px"> <template #st-actions>
<slot name="moreBeforeActions" /> <QBtnGroup push style="column-gap: 10px">
<QBtn <slot name="moreBeforeActions" />
:label="t('globals.remove')" <QBtn
color="primary" :label="t('globals.remove')"
icon="delete" color="primary"
flat icon="delete"
@click="openRemoveDialog()" flat
:disable="!rowsSelected?.length" @click="openRemoveDialog()"
:title="t('globals.remove')" :disable="!rowsSelected?.length"
/> :title="t('globals.remove')"
</QBtnGroup> />
</Teleport> </QBtnGroup>
</template>
</VnSubToolbar>
<VnPaginate <VnPaginate
ref="entryBuysPaginateRef" ref="entryBuysPaginateRef"
data-key="EntryBuys" data-key="EntryBuys"

View File

@ -161,6 +161,7 @@ const fetchEntryBuys = async () => {
ref="summaryRef" ref="summaryRef"
:url="`Entries/${entityId}/getEntry`" :url="`Entries/${entityId}/getEntry`"
@on-fetch="(data) => setEntryData(data)" @on-fetch="(data) => setEntryData(data)"
data-key="EntrySummary"
> >
<template #header-left> <template #header-left>
<router-link <router-link

View File

@ -16,14 +16,15 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
import { useSession } from 'composables/useSession'; // import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const router = useRouter(); const router = useRouter();
const { getTokenMultimedia } = useSession(); // const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); // const token = getTokenMultimedia();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
@ -695,14 +696,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</template> </template>
<template #body-cell-picture="{ row }"> <template #body-cell-picture="{ row }">
<QTd> <QTd>
<QImg <VnImg :id="row.itemFk" size="50x50" class="image" />
:src="`/api/Images/catalog/50x50/${row.itemFk}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
height="50px"
width="50px"
class="image"
/>
</QTd> </QTd>
</template> </template>
<template #body-cell-itemFk="{ row }"> <template #body-cell-itemFk="{ row }">

View File

@ -10,8 +10,10 @@ import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue'; import VnCurrency from 'src/components/common/VnCurrency.vue';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import useNotify from 'src/composables/useNotify.js';
const route = useRoute(); const route = useRoute();
const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const arrayData = useArrayData(); const arrayData = useArrayData();
const invoiceIn = computed(() => arrayData.store.data); const invoiceIn = computed(() => arrayData.store.data);
@ -69,6 +71,7 @@ const isNotEuro = (code) => code != 'EUR';
async function insert() { async function insert() {
await axios.post('/InvoiceInDueDays/new', { id: +invoiceId }); await axios.post('/InvoiceInDueDays/new', { id: +invoiceId });
await invoiceInFormRef.value.reload(); await invoiceInFormRef.value.reload();
notify(t('globals.dataSaved'), 'positive');
} }
const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, 0); const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, 0);
</script> </script>

View File

@ -106,6 +106,7 @@ const ticketsColumns = ref([
ref="summary" ref="summary"
:url="`InvoiceOuts/${entityId}/summary`" :url="`InvoiceOuts/${entityId}/summary`"
:entity-id="entityId" :entity-id="entityId"
data-key="InvoiceOutSummary"
> >
<template #header="{ entity: { invoiceOut } }"> <template #header="{ entity: { invoiceOut } }">
<div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div> <div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div>

View File

@ -13,7 +13,6 @@ import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import { useSession } from 'src/composables/useSession';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import axios from 'axios'; import axios from 'axios';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
@ -42,14 +41,12 @@ const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const image = ref(null);
const regularizeStockFormDialog = ref(null); const regularizeStockFormDialog = ref(null);
const item = ref(null); const item = ref(null);
const available = ref(null); const available = ref(null);
@ -67,17 +64,10 @@ const warehouseFk = computed({
}); });
onMounted(async () => { onMounted(async () => {
await getItemAvatar();
warehouseFk.value = user.value.warehouseFk; warehouseFk.value = user.value.warehouseFk;
salixUrl.value = await getUrl(''); salixUrl.value = await getUrl('');
}); });
const getItemAvatar = async () => {
const token = getTokenMultimedia();
const timeStamp = `timestamp=${Date.now()}`;
image.value = `/api/Images/catalog/200x200/${entityId.value}/download?access_token=${token}&${timeStamp}`;
};
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => { const setData = (entity) => {
if (!entity) return; if (!entity) return;

View File

@ -3,8 +3,7 @@ import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import EditPictureForm from 'components/EditPictureForm.vue'; import EditPictureForm from 'components/EditPictureForm.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import { useSession } from 'src/composables/useSession';
import axios from 'axios'; import axios from 'axios';
const $props = defineProps({ const $props = defineProps({
@ -27,19 +26,12 @@ const $props = defineProps({
}); });
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const image = ref(null); const image = ref(null);
const editPhotoFormDialog = ref(null); const editPhotoFormDialog = ref(null);
const showEditPhotoForm = ref(false); const showEditPhotoForm = ref(false);
const warehouseName = ref(null); const warehouseName = ref(null);
const getItemAvatar = async () => {
const token = getTokenMultimedia();
const timeStamp = `timestamp=${Date.now()}`;
image.value = `/api/Images/catalog/200x200/${$props.entityId}/download?access_token=${token}&${timeStamp}`;
};
const toggleEditPictureForm = () => { const toggleEditPictureForm = () => {
showEditPhotoForm.value = !showEditPhotoForm.value; showEditPhotoForm.value = !showEditPhotoForm.value;
}; };
@ -62,14 +54,17 @@ const getWarehouseName = async (warehouseFk) => {
}; };
onMounted(async () => { onMounted(async () => {
getItemAvatar();
getItemConfigs(); getItemConfigs();
}); });
const handlePhotoUpdated = (evt = false) => {
image.value.reload(evt);
};
</script> </script>
<template> <template>
<div class="relative-position"> <div class="relative-position">
<QImg :src="image" spinner-color="primary" style="min-height: 256px"> <VnImg ref="image" :id="$props.entityId" @refresh="handlePhotoUpdated(true)">
<template #error> <template #error>
<div class="absolute-full picture text-center q-pa-md flex flex-center"> <div class="absolute-full picture text-center q-pa-md flex flex-center">
<div> <div>
@ -82,7 +77,7 @@ onMounted(async () => {
</div> </div>
</div> </div>
</template> </template>
</QImg> </VnImg>
<QBtn <QBtn
v-if="showEditButton" v-if="showEditButton"
color="primary" color="primary"
@ -97,7 +92,7 @@ onMounted(async () => {
collection="catalog" collection="catalog"
:id="entityId" :id="entityId"
@close-form="toggleEditPictureForm()" @close-form="toggleEditPictureForm()"
@on-photo-uploaded="getItemAvatar()" @on-photo-uploaded="handlePhotoUpdated"
/> />
</QDialog> </QDialog>
</QBtn> </QBtn>

View File

@ -33,7 +33,6 @@ const user = state.getUser();
const fixedPrices = ref([]); const fixedPrices = ref([]);
const fixedPricesOriginalData = ref([]); const fixedPricesOriginalData = ref([]);
const warehousesOptions = ref([]); const warehousesOptions = ref([]);
const itemsWithNameOptions = ref([]);
const rowsSelected = ref([]); const rowsSelected = ref([]);
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
@ -371,12 +370,6 @@ onUnmounted(() => (stateStore.rightDrawer = false));
auto-load auto-load
@on-fetch="(data) => onWarehousesFetched(data)" @on-fetch="(data) => onWarehousesFetched(data)"
/> />
<FetchData
url="Items/withName"
:filter="{ fields: ['id', 'name'], order: 'id DESC' }"
auto-load
@on-fetch="(data) => (itemsWithNameOptions = data)"
/>
<RightMenu> <RightMenu>
<template #right-panel> <template #right-panel>
<ItemFixedPriceFilter <ItemFixedPriceFilter
@ -419,7 +412,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<template #body-cell-itemId="props"> <template #body-cell-itemId="props">
<QTd> <QTd>
<VnSelect <VnSelect
:options="itemsWithNameOptions" url="Items/withName"
hide-selected hide-selected
option-label="id" option-label="id"
option-value="id" option-value="id"
@ -562,7 +555,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</QTd> </QTd>
</template> </template>
</QTable> </QTable>
<QPageSticky v-if="rowsSelected.length > 0" :offset="[20, 20]"> <QPageSticky v-if="rowsSelected.length" :offset="[20, 20]">
<QBtn @click="openEditTableCellDialog()" color="primary" fab icon="edit" /> <QBtn @click="openEditTableCellDialog()" color="primary" fab icon="edit" />
<QTooltip> <QTooltip>
{{ t('Edit fixed price(s)') }} {{ t('Edit fixed price(s)') }}

View File

@ -16,17 +16,15 @@ import ItemListFilter from './ItemListFilter.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { toDateFormat } from 'src/filters/date.js'; import { toDateFormat } from 'src/filters/date.js';
import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import axios from 'axios'; import axios from 'axios';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const router = useRouter(); const router = useRouter();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
@ -491,10 +489,9 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</template> </template>
<template #body-cell-picture="{ row }"> <template #body-cell-picture="{ row }">
<QTd> <QTd>
<QImg <VnImg
:src="`/api/Images/catalog/50x50/${row.id}/download?access_token=${token}`" size="50x50"
spinner-color="primary" :id="row.id"
:ratio="1"
height="50px" height="50px"
width="50px" width="50px"
class="image" class="image"

View File

@ -3,14 +3,15 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import VnInput from 'components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import { useValidator } from 'src/composables/useValidator';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const props = defineProps({ const props = defineProps({
dataKey: { dataKey: {
@ -21,32 +22,34 @@ const props = defineProps({
type: Array, type: Array,
required: true, required: true,
}, },
tagValue: {
type: Array,
required: true,
},
}); });
const categoryList = ref(null); const categoryList = ref(null);
const selectedCategoryFk = ref(null); const selectedCategoryFk = ref(null);
const typeList = ref(null); const typeList = ref(null);
const selectedTypeFk = ref(null); const selectedTypeFk = ref(null);
const validationsStore = useValidator();
const selectedOrder = ref(null);
const selectedOrderField = ref(null);
const moreFields = ref([]);
const moreFieldsOrder = ref([]);
const createValue = (val, done) => {
if (val.length > 2) {
if (!tagOptions.value.includes(val)) {
done(tagOptions.value, 'add-unique');
}
tagValues.value.push({ value: val });
}
};
const resetCategory = () => { const resetCategory = () => {
selectedCategoryFk.value = null; selectedCategoryFk.value = null;
typeList.value = null; typeList.value = null;
}; };
const selectedOrder = ref(null);
const orderList = [
{ way: 'ASC', name: 'Ascendant' },
{ way: 'DESC', name: 'Descendant' },
];
const selectedOrderField = ref(null);
const OrderFields = [
{ field: 'relevancy DESC, name', name: 'Relevancy', priority: 999 },
{ field: 'showOrder, price', name: 'Color and price', priority: 999 },
{ field: 'name', name: 'Name', priority: 999 },
{ field: 'price', name: 'Price', priority: 999 },
];
const clearFilter = (key) => { const clearFilter = (key) => {
if (key === 'categoryFk') { if (key === 'categoryFk') {
resetCategory(); resetCategory();
@ -72,21 +75,6 @@ const loadTypes = async (categoryFk) => {
typeList.value = data; typeList.value = data;
}; };
const onFilterInit = async ({ params }) => {
if (params.typeFk) {
selectedTypeFk.value = params.typeFk;
}
if (params.categoryFk) {
await loadTypes(params.categoryFk);
selectedCategoryFk.value = params.categoryFk;
}
if (params.orderBy) {
orderByParam.value = JSON.parse(params.orderBy);
selectedOrder.value = orderByParam.value?.way;
selectedOrderField.value = orderByParam.value?.field;
}
};
const selectedCategory = computed(() => const selectedCategory = computed(() =>
(categoryList.value || []).find( (categoryList.value || []).find(
(category) => category?.id === selectedCategoryFk.value (category) => category?.id === selectedCategoryFk.value
@ -109,10 +97,7 @@ function exprBuilder(param, value) {
const selectedTag = ref(null); const selectedTag = ref(null);
const tagValues = ref([{}]); const tagValues = ref([{}]);
const tagOptions = ref(null); const tagOptions = ref([]);
const isButtonDisabled = computed(
() => !selectedTag.value || tagValues.value.some((item) => !item.value)
);
const applyTagFilter = (params, search) => { const applyTagFilter = (params, search) => {
if (!tagValues.value?.length) { if (!tagValues.value?.length) {
@ -125,12 +110,12 @@ const applyTagFilter = (params, search) => {
} }
params.tagGroups.push( params.tagGroups.push(
JSON.stringify({ JSON.stringify({
values: tagValues.value, values: tagValues.value.filter((obj) => Object.keys(obj).length > 0),
tagSelection: { tagSelection: {
...selectedTag.value, ...selectedTag.value,
orgShowField: selectedTag.value.name, orgShowField: selectedTag?.value?.name,
}, },
tagFk: selectedTag.value.tagFk, tagFk: selectedTag?.value?.tagFk,
}) })
); );
search(); search();
@ -147,20 +132,52 @@ const removeTagChip = (selection, params, search) => {
search(); search();
}; };
const orderByParam = ref(null); const onOrderChange = (value, params) => {
const tagObj = JSON.parse(params.orderBy);
const onOrderFieldChange = (value, params, search) => { tagObj.way = value.name;
const orderBy = Object.assign({}, orderByParam.value, { field: value.field }); params.orderBy = JSON.stringify(tagObj);
params.orderBy = JSON.stringify(orderBy);
search();
}; };
const onOrderChange = (value, params, search) => { const onOrderFieldChange = (value, params) => {
const orderBy = Object.assign({}, orderByParam.value, { way: value.way }); const tagObj = JSON.parse(params.orderBy); // esto donde va
params.orderBy = JSON.stringify(orderBy); const fields = {
search(); Relevancy: (value) => value + ' DESC, name',
ColorAndPrice: 'showOrder, price',
Name: 'name',
Price: 'price',
};
let tagField = fields[value];
if (!tagField) return;
if (typeof tagField === 'function') tagField = tagField(value);
tagObj.field = tagField;
params.orderBy = JSON.stringify(tagObj);
switch (value) {
case 'Relevancy':
tagObj.field = value + ' DESC, name';
params.orderBy = JSON.stringify(tagObj);
console.log('params: ', params);
break;
case 'ColorAndPrice':
tagObj.field = 'showOrder, price';
params.orderBy = JSON.stringify(tagObj);
console.log('params: ', params);
break;
case 'Name':
tagObj.field = 'name';
params.orderBy = JSON.stringify(tagObj);
console.log('params: ', params);
break;
case 'Price':
tagObj.field = 'price';
params.orderBy = JSON.stringify(tagObj);
console.log('params: ', params);
break;
}
}; };
const _moreFields = ['ASC', 'DESC'];
const _moreFieldsTypes = ['Relevancy', 'ColorAndPrice', 'Name', 'Price'];
const setCategoryList = (data) => { const setCategoryList = (data) => {
categoryList.value = (data || []) categoryList.value = (data || [])
.filter((category) => category.display) .filter((category) => category.display)
@ -168,6 +185,8 @@ const setCategoryList = (data) => {
...category, ...category,
icon: `vn:${(category.icon || '').split('-')[1]}`, icon: `vn:${(category.icon || '').split('-')[1]}`,
})); }));
moreFields.value = useLang(_moreFields);
moreFieldsOrder.value = useLang(_moreFieldsTypes);
}; };
const getCategoryClass = (category, params) => { const getCategoryClass = (category, params) => {
@ -175,6 +194,20 @@ const getCategoryClass = (category, params) => {
return 'active'; return 'active';
} }
}; };
const useLang = (values) => {
const { models } = validationsStore;
const properties = models.Item?.properties || {};
return values.map((name) => {
let prop = properties[name];
const label = t(`params.${name}`);
return {
name,
label,
type: prop ? prop.type : null,
};
});
};
</script> </script>
<template> <template>
@ -182,10 +215,11 @@ const getCategoryClass = (category, params) => {
<VnFilterPanel <VnFilterPanel
:data-key="props.dataKey" :data-key="props.dataKey"
:hidden-tags="['orderFk', 'orderBy']" :hidden-tags="['orderFk', 'orderBy']"
:unremovable-params="['orderFk', 'orderBy']"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
:custom-tags="['tagGroups']" :custom-tags="['tagGroups']"
@init="onFilterInit"
@remove="clearFilter" @remove="clearFilter"
:redirect="false"
> >
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<strong v-if="tag.label === 'categoryFk'"> <strong v-if="tag.label === 'categoryFk'">
@ -274,40 +308,29 @@ const getCategoryClass = (category, params) => {
<QItem class="q-my-md"> <QItem class="q-my-md">
<QItemSection> <QItemSection>
<VnSelect <VnSelect
:label="t('params.order')" :label="t('Order')"
v-model="selectedOrder" v-model="selectedOrder"
:options="orderList || []" :options="moreFields"
option-value="way" option-label="label"
option-label="name"
dense dense
outlined outlined
rounded rounded
:emit-value="false" @update:model-value="(value) => onOrderChange(value, params)"
use-input
:is-clearable="false"
@update:model-value="
(value) => onOrderChange(value, params, searchFn)
"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem class="q-mb-md"> <QItem class="q-mb-md">
<QItemSection> <QItemSection>
<VnSelect <VnSelect
:label="t('params.order')" :label="t('Order by')"
v-model="selectedOrderField" v-model="selectedOrderField"
:options="OrderFields || []" :options="moreFieldsOrder"
option-value="field" option-label="label"
option-label="name" option-value="name"
dense dense
outlined outlined
rounded rounded
:emit-value="false" @update:model-value="(value) => onOrderFieldChange(value, params)"
use-input
:is-clearable="false"
@update:model-value="
(value) => onOrderFieldChange(value, params, searchFn)
"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -333,15 +356,30 @@ const getCategoryClass = (category, params) => {
:key="value" :key="value"
class="q-mt-md filter-value" class="q-mt-md filter-value"
> >
<VnInput <FetchData
v-if="selectedTag?.isFree" v-if="selectedTag"
v-model="value.value" :url="`Tags/${selectedTag}/filterValue`"
:label="t('params.value')" limit="30"
is-outlined auto-load
class="filter-input" @on-fetch="(data) => (tagOptions = data)"
/> />
<VnSelect <VnSelect
v-else v-if="!selectedTag"
:label="t('params.value')"
v-model="value.value"
:options="tagValue || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
class="filter-input"
@new-value="createValue"
/>
<VnSelect
v-else-if="selectedTag === 1"
:label="t('params.value')" :label="t('params.value')"
v-model="value.value" v-model="value.value"
:options="tagOptions || []" :options="tagOptions || []"
@ -352,18 +390,18 @@ const getCategoryClass = (category, params) => {
rounded rounded
emit-value emit-value
use-input use-input
:disable="!selectedTag" class="filter-input"
@new-value="createValue"
/>
<VnInput
v-else
:label="t('params.value')"
v-model="value.value"
dense
outlined
rounded
class="filter-input" class="filter-input"
/> />
<FetchData
v-if="selectedTag && !selectedTag.isFree"
:url="`Tags/${selectedTag?.id}/filterValue`"
limit="30"
auto-load
@on-fetch="(data) => (tagOptions = data)"
/>
<QIcon <QIcon
name="delete" name="delete"
class="filter-icon" class="filter-icon"
@ -388,7 +426,6 @@ const getCategoryClass = (category, params) => {
rounded rounded
type="button" type="button"
unelevated unelevated
:disable="isButtonDisabled"
@click.stop="applyTagFilter(params, searchFn)" @click.stop="applyTagFilter(params, searchFn)"
/> />
</QItemSection> </QItemSection>
@ -453,6 +490,12 @@ en:
tag: Tag tag: Tag
value: Value value: Value
order: Order order: Order
ASC: Ascendant
DESC: Descendant
Relevancy: Relevancy
ColorAndPrice: Color and price
Name: Name
Price: Price
es: es:
params: params:
type: Tipo type: Tipo
@ -460,6 +503,14 @@ es:
tag: Etiqueta tag: Etiqueta
value: Valor value: Valor
order: Orden order: Orden
ASC: Ascendiente
DESC: Descendiente
Relevancy: Relevancia
ColorAndPrice: Color y precio
Name: Nombre
Price: Precio
Order: Orden
Order by: Ordenar por
Plant: Planta Plant: Planta
Flower: Flor Flower: Flor
Handmade: Confección Handmade: Confección

View File

@ -3,16 +3,14 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnLv from 'components/ui/VnLv.vue'; import VnLv from 'components/ui/VnLv.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue'; import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import { useSession } from 'composables/useSession';
import toCurrency from '../../../filters/toCurrency'; import toCurrency from '../../../filters/toCurrency';
const DEFAULT_PRICE_KG = 0; const DEFAULT_PRICE_KG = 0;
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const { t } = useI18n(); const { t } = useI18n();
defineProps({ defineProps({
@ -29,14 +27,7 @@ const dialog = ref(null);
<div class="container order-catalog-item overflow-hidden"> <div class="container order-catalog-item overflow-hidden">
<QCard class="card shadow-6"> <QCard class="card shadow-6">
<div class="img-wrapper"> <div class="img-wrapper">
<QImg <VnImg :id="item.id" class="image" />
:src="`/api/Images/catalog/200x200/${item.id}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
height="192"
width="192"
class="image"
/>
<div v-if="item.hex" class="item-color-container"> <div v-if="item.hex" class="item-color-container">
<div <div
class="item-color" class="item-color"
@ -59,7 +50,10 @@ const dialog = ref(null);
</template> </template>
<div class="footer"> <div class="footer">
<div class="price"> <div class="price">
<p>{{ item.available }} {{ t('to') }} {{ item.price }}</p> <p>
{{ item.available }} {{ t('to') }}
{{ toCurrency(item.price) }}
</p>
<QIcon name="add_circle" class="icon"> <QIcon name="add_circle" class="icon">
<QTooltip>{{ t('globals.add') }}</QTooltip> <QTooltip>{{ t('globals.add') }}</QTooltip>
<QPopupProxy ref="dialog"> <QPopupProxy ref="dialog">

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
@ -16,6 +16,7 @@ const route = useRoute();
const state = useState(); const state = useState();
const ORDER_MODEL = 'order'; const ORDER_MODEL = 'order';
const router = useRouter();
const isNew = Boolean(!route.params.id); const isNew = Boolean(!route.params.id);
const initialFormState = reactive({ const initialFormState = reactive({
clientFk: null, clientFk: null,
@ -26,22 +27,19 @@ const initialFormState = reactive({
const clientList = ref([]); const clientList = ref([]);
const agencyList = ref([]); const agencyList = ref([]);
const addressList = ref([]); const addressList = ref([]);
const clientId = ref(null);
const onClientsFetched = async (data) => { const onClientsFetched = (data) => {
try { clientList.value = data;
clientList.value = data; initialFormState.clientFk = Number(route.query?.clientFk) || null;
initialFormState.clientFk = Number(route.query?.clientFk) || null; clientId.value = initialFormState.clientFk;
if (initialFormState.clientFk) { const client = clientList.value.find(
const { defaultAddressFk } = clientList.value.find( (client) => client.id === initialFormState.clientFk
(client) => client.id === initialFormState.clientFk );
); if (!client?.defaultAddressFk)
throw new Error(t(`No default address found for the client`));
if (defaultAddressFk) await fetchAddressList(defaultAddressFk); fetchAddressList(client.defaultAddressFk);
}
} catch (err) {
console.error('Error fetching clients', err);
}
}; };
const fetchAddressList = async (addressId) => { const fetchAddressList = async (addressId) => {
@ -55,7 +53,6 @@ const fetchAddressList = async (addressId) => {
}, },
}); });
addressList.value = data; addressList.value = data;
// Set address by default
if (addressList.value?.length === 1) { if (addressList.value?.length === 1) {
state.get(ORDER_MODEL).addressFk = addressList.value[0].id; state.get(ORDER_MODEL).addressFk = addressList.value[0].id;
} }
@ -121,6 +118,21 @@ const orderFilter = {
}, },
], ],
}; };
const onClientChange = async (clientId) => {
try {
const { data } = await axios.get(`Clients/${clientId}`);
console.log('info cliente: ', data);
await fetchAddressList(data.defaultAddressFk);
} catch (error) {
console.error('Error al cambiar el cliente:', error);
}
};
async function onDataSaved(data) {
await router.push({ path: `/order/${data}/catalog` });
}
</script> </script>
<template> <template>
@ -134,13 +146,15 @@ const orderFilter = {
<div class="q-pa-md"> <div class="q-pa-md">
<FormModel <FormModel
:url="!isNew ? `Orders/${route.params.id}` : null" :url="!isNew ? `Orders/${route.params.id}` : null"
:url-create="isNew ? 'Orders/new' : null" url-create="Orders/new"
@on-data-saved="onDataSaved"
:model="ORDER_MODEL" :model="ORDER_MODEL"
:form-initial-data="isNew ? initialFormState : null" :form-initial-data="isNew ? initialFormState : null"
:observe-form-changes="!isNew" :observe-form-changes="!isNew"
:mapper="isNew ? orderMapper : null" :mapper="isNew ? orderMapper : null"
:filter="orderFilter" :filter="orderFilter"
@on-fetch="fetchOrderDetails" @on-fetch="fetchOrderDetails"
auto-load
> >
<template #form="{ data }"> <template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
@ -151,9 +165,7 @@ const orderFilter = {
option-value="id" option-value="id"
option-label="name" option-label="name"
hide-selected hide-selected
@update:model-value=" @update:model-value="onClientChange"
(client) => fetchAddressList(client.defaultAddressFk)
"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -170,12 +182,10 @@ const orderFilter = {
v-model="data.addressFk" v-model="data.addressFk"
:options="addressList" :options="addressList"
option-value="id" option-value="id"
option-label="nickname" option-label="street"
hide-selected hide-selected
:disable="!addressList?.length" :disable="!addressList?.length"
@update:model-value=" @update:model-value="onAddressChange"
() => fetchAgencyList(data.landed, data.addressFk)
"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -216,3 +226,8 @@ const orderFilter = {
</FormModel> </FormModel>
</div> </div>
</template> </template>
<i18n>
es:
No default address found for the client: No hay ninguna dirección asociada a este cliente.
</i18n>

View File

@ -51,7 +51,11 @@ const detailsColumns = ref([
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<CardSummary ref="summary" :url="`Orders/${entityId}/summary`"> <CardSummary
ref="summary"
:url="`Orders/${entityId}/summary`"
data-key="OrderSummary"
>
<template #header="{ entity }"> <template #header="{ entity }">
{{ t('order.summary.basket') }} #{{ entity?.id }} - {{ t('order.summary.basket') }} #{{ entity?.id }} -
{{ entity?.client?.name }} ({{ entity?.clientFk }}) {{ entity?.client?.name }} ({{ entity?.clientFk }})

View File

@ -4,7 +4,6 @@ import { useRoute } from 'vue-router';
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import OrderCatalogItem from 'pages/Order/Card/OrderCatalogItem.vue'; import OrderCatalogItem from 'pages/Order/Card/OrderCatalogItem.vue';
import OrderCatalogFilter from 'pages/Order/Card/OrderCatalogFilter.vue'; import OrderCatalogFilter from 'pages/Order/Card/OrderCatalogFilter.vue';
@ -35,38 +34,31 @@ function extractTags(items) {
}); });
}); });
tags.value = resultTags; tags.value = resultTags;
extractValueTags(items);
}
const tagValue = ref([]);
function extractValueTags(items) {
const resultValueTags = items.flatMap((x) =>
Object.keys(x)
.filter((k) => /^value\d+$/.test(k))
.map((v) => x[v])
.filter((v) => v)
.sort()
);
tagValue.value = resultValueTags;
} }
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="OrderCatalogList"
url="Orders/CatalogFilter"
:limit="50"
:user-params="catalogParams"
:static-params="['orderFk', 'orderBy']"
:redirect="false"
/>
</Teleport>
<Teleport v-if="stateStore.isHeaderMounted()" to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click.stop="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit text-grey-8">
<OrderCatalogFilter data-key="OrderCatalogList" :tags="tags" /> <OrderCatalogFilter
data-key="OrderCatalogList"
:tag-value="tagValue"
:tags="tags"
/>
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">

View File

@ -7,19 +7,17 @@ import { useQuasar } from 'quasar';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnLv from 'components/ui/VnLv.vue'; import VnLv from 'components/ui/VnLv.vue';
import CardList from 'components/ui/CardList.vue';
import FetchedTags from 'components/ui/FetchedTags.vue'; import FetchedTags from 'components/ui/FetchedTags.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import VnImg from 'components/ui/VnImg.vue';
import { toCurrency, toDate } from 'src/filters'; import { toCurrency, toDate } from 'src/filters';
import { useSession } from 'composables/useSession';
import axios from 'axios'; import axios from 'axios';
import ItemDescriptorProxy from '../Item/Card/ItemDescriptorProxy.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const quasar = useQuasar(); const quasar = useQuasar();
const token = getTokenMultimedia();
const orderSummary = ref({ const orderSummary = ref({
total: null, total: null,
vat: null, vat: null,
@ -61,6 +59,56 @@ async function confirmOrder() {
type: 'positive', type: 'positive',
}); });
} }
const detailsColumns = ref([
{
name: 'img',
label: '',
field: (row) => row?.item?.id,
},
{
name: 'item',
label: t('order.summary.item'),
field: (row) => row?.item?.id,
sortable: true,
},
{
name: 'description',
label: t('globals.description'),
field: (row) => row?.item?.name,
},
{
name: 'warehouse',
label: t('warehouse'),
field: (row) => row?.warehouse?.name,
sortable: true,
},
{
name: 'shipped',
label: t('shipped'),
field: (row) => toDate(row?.shipped),
},
{
name: 'quantity',
label: t('order.summary.quantity'),
field: (row) => row?.quantity,
},
{
name: 'price',
label: t('order.summary.price'),
field: (row) => toCurrency(row?.price),
},
{
name: 'amount',
label: t('order.summary.amount'),
field: (row) => toCurrency(row?.quantity * row?.price),
},
{
name: 'actions',
label: '',
field: (row) => row?.id,
},
]);
</script> </script>
<template> <template>
@ -83,30 +131,33 @@ async function confirmOrder() {
auto-load auto-load
/> />
<QPage :key="componentKey" class="column items-center q-pa-md"> <QPage :key="componentKey" class="column items-center q-pa-md">
<div class="vn-card-list"> <div class="order-list full-width">
<div v-if="!orderSummary.total" class="no-result"> <div v-if="!orderSummary.total" class="no-result">
{{ t('globals.noResults') }} {{ t('globals.noResults') }}
</div> </div>
<QCard v-else class="order-lines-summary q-pa-lg">
<p class="header text-right block"> <QDrawer side="right" :width="270" show-if-above>
{{ t('summary') }} <QCard class="order-lines-summary q-pa-lg">
</p> <p class="header text-right block">
<VnLv {{ t('summary') }}
v-if="orderSummary.vat && orderSummary.total" </p>
:label="t('subtotal')" <VnLv
:value="toCurrency(orderSummary.total - orderSummary.vat)" v-if="orderSummary.vat && orderSummary.total"
/> :label="t('subtotal')"
<VnLv :value="toCurrency(orderSummary.total - orderSummary.vat)"
v-if="orderSummary.vat" />
:label="t('VAT')" <VnLv
:value="toCurrency(orderSummary?.vat)" v-if="orderSummary.vat"
/> :label="t('VAT')"
<VnLv :value="toCurrency(orderSummary?.vat)"
v-if="orderSummary.total" />
:label="t('total')" <VnLv
:value="toCurrency(orderSummary?.total)" v-if="orderSummary.total"
/> :label="t('total')"
</QCard> :value="toCurrency(orderSummary?.total)"
/>
</QCard>
</QDrawer>
<VnPaginate <VnPaginate
data-key="OrderLines" data-key="OrderLines"
url="OrderRows" url="OrderRows"
@ -125,74 +176,71 @@ async function confirmOrder() {
}" }"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<div class="catalog-list q-mt-xl"> <div class="q-pa-md">
<CardList <QTable
v-for="row in rows" :columns="detailsColumns"
:key="row.id" :rows="rows"
:id="row.id" flat
:title="row?.item?.name" class="full-width"
class="cursor-inherit" style="text-align: center"
> >
<template #title> <template #header="props">
<div class="flex items-center"> <QTr class="tr-header" :props="props">
<div class="image-wrapper q-mr-md"> <QTh
<QImg v-for="col in props.cols"
:src="`/api/Images/catalog/50x50/${row?.item?.id}/download?access_token=${token}`" :key="col.name"
spinner-color="primary" :props="props"
:ratio="1" style="text-align: center"
height="50"
width="50"
class="image"
/>
</div>
<div
class="title text-primary text-weight-bold text-h5"
> >
{{ row?.item?.name }} {{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #body-cell-img="{ value }">
<QTd>
<div class="image-wrapper">
<VnImg :id="value" class="rounded" />
</div> </div>
<QChip class="q-chip-color" outline size="sm"> </QTd>
{{ t('ID') }}: {{ row.id }}
</QChip>
</div>
</template> </template>
<template #list-items> <template #body-cell-item="{ value }">
<div class="q-mb-sm"> <QTd class="item">
<span class="text-uppercase subname"> <span class="link">
{{ row.item.subName }} <QBtn flat>
{{ value }}
</QBtn>
<ItemDescriptorProxy :id="value" />
</span> </span>
<FetchedTags :item="row.item" :max-length="5" /> </QTd>
</div>
<VnLv :label="t('item')" :value="String(row.item.id)" />
<VnLv
:label="t('warehouse')"
:value="row.warehouse.name"
/>
<VnLv
:label="t('shipped')"
:value="toDate(row.shipped)"
/>
<VnLv
:label="t('quantity')"
:value="String(row.quantity)"
/>
<VnLv
:label="t('price')"
:value="toCurrency(row.price)"
/>
<VnLv
:label="t('amount')"
:value="toCurrency(row.price * row.quantity)"
/>
</template> </template>
<template #actions v-if="!order?.isConfirmed"> <template #body-cell-description="{ row, value }">
<QBtn <QTd>
:label="t('remove')" <div
@click.stop="confirmRemove(row)" class="row column full-width justify-between items-start"
color="primary" >
style="margin-top: 15px" {{ value }}
/> <div v-if="value" class="subName">
{{ value.toUpperCase() }}
</div>
</div>
<FetchedTags :item="row.item" :max-length="6" />
</QTd>
</template> </template>
</CardList>
<template #body-cell-actions="{ value }">
<QTd>
<QIcon
name="delete"
color="primary"
size="sm"
class="cursor-pointer"
@click.stop="confirmRemove(value)"
>
<QTooltip>{{ t('Remove thermograph') }}</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
</div> </div>
</template> </template>
</VnPaginate> </VnPaginate>
@ -239,14 +287,7 @@ async function confirmOrder() {
.image-wrapper { .image-wrapper {
height: 50px; height: 50px;
width: 50px; width: 50px;
margin-left: 30%;
.image {
border-radius: 50%;
}
}
.subname {
color: var(--vn-label-color);
} }
.no-result { .no-result {
@ -255,6 +296,11 @@ async function confirmOrder() {
color: var(--vn-label-color); color: var(--vn-label-color);
text-align: center; text-align: center;
} }
.subName {
text-transform: uppercase;
color: var(--vn-label-color);
}
</style> </style>
<i18n> <i18n>
en: en:

View File

@ -30,6 +30,7 @@ const filter = {
:url="`Parkings/${entityId}`" :url="`Parkings/${entityId}`"
:filter="filter" :filter="filter"
@on-fetch="(data) => (parking = data)" @on-fetch="(data) => (parking = data)"
data-key="Parking"
> >
<template #header>{{ parking.code }}</template> <template #header>{{ parking.code }}</template>
<template #body> <template #body>

View File

@ -240,4 +240,5 @@ es:
From: Desde From: Desde
To: Hasta To: Hasta
Served: Servida Served: Servida
Days Onward: Días en adelante
</i18n> </i18n>

View File

@ -3,14 +3,15 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'components/ui/VnLv.vue';
import { QIcon } from 'quasar'; import { QIcon } from 'quasar';
import { dashIfEmpty, toCurrency, toDate, toHour } from 'src/filters'; import { dashIfEmpty, toCurrency, toDate, toHour } from 'src/filters';
import { openBuscaman } from 'src/utils/buscaman';
import CardSummary from 'components/ui/CardSummary.vue';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import { openBuscaman } from 'src/utils/buscaman'; import VnLv from 'components/ui/VnLv.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -122,13 +123,20 @@ const ticketColumns = ref([
ref="summary" ref="summary"
:url="`Routes/${entityId}/summary`" :url="`Routes/${entityId}/summary`"
:entity-id="entityId" :entity-id="entityId"
data-key="RouteSummary"
> >
<template #header="{ entity }"> <template #header="{ entity }">
<span>{{ `${entity?.route.id} - ${entity?.route?.description}` }}</span> <span>{{ `${entity?.route.id} - ${entity?.route?.description}` }}</span>
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<QCard class="vn-max">
<VnTitle
:url="`#/route/${entityId}/basic-data`"
:text="t('globals.pageTitles.basicData')"
/>
</QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<VnLv :label="t('ID')" :value="entity?.route.id" />
<VnLv <VnLv
:label="t('route.summary.date')" :label="t('route.summary.date')"
:value="toDate(entity?.route.created)" :value="toDate(entity?.route.created)"
@ -153,24 +161,6 @@ const ticketColumns = ref([
:label="t('route.summary.cost')" :label="t('route.summary.cost')"
:value="toCurrency(entity.route?.cost)" :value="toCurrency(entity.route?.cost)"
/> />
</QCard>
<QCard class="vn-one">
<VnLv
:label="t('route.summary.started')"
:value="toHour(entity?.route.started)"
/>
<VnLv
:label="t('route.summary.finished')"
:value="toHour(entity?.route.finished)"
/>
<VnLv
:label="t('route.summary.kmStart')"
:value="dashIfEmpty(entity?.route?.kmStart)"
/>
<VnLv
:label="t('route.summary.kmEnd')"
:value="dashIfEmpty(entity?.route?.kmEnd)"
/>
<VnLv <VnLv
:label="t('route.summary.volume')" :label="t('route.summary.volume')"
:value="`${dashIfEmpty(entity?.route?.m3)} / ${dashIfEmpty( :value="`${dashIfEmpty(entity?.route?.m3)} / ${dashIfEmpty(
@ -192,19 +182,32 @@ const ticketColumns = ref([
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<div class="header"> <VnLv
{{ t('globals.description') }} :label="t('route.summary.started')"
</div> :value="toHour(entity?.route.started)"
<p> />
{{ dashIfEmpty(entity?.route?.description) }} <VnLv
</p> :label="t('route.summary.finished')"
:value="toHour(entity?.route.finished)"
/>
<VnLv
:label="t('route.summary.kmStart')"
:value="dashIfEmpty(entity?.route?.kmStart)"
/>
<VnLv
:label="t('route.summary.kmEnd')"
:value="dashIfEmpty(entity?.route?.kmEnd)"
/>
<VnLv
:label="t('globals.description')"
:value="dashIfEmpty(entity?.route?.description)"
/>
</QCard> </QCard>
<QCard class="vn-max"> <QCard class="vn-max">
<a class="header" :href="`#/route/${entityId}/tickets`"> <VnTitle
{{ t('route.summary.tickets') }} :url="`#/route/${entityId}/tickets`"
<QIcon name="open_in_new" color="primary" /> :text="t('route.summary.tickets')"
</a> />
<QTable <QTable
:columns="ticketColumns" :columns="ticketColumns"
:rows="entity?.tickets" :rows="entity?.tickets"

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onBeforeMount, computed, ref } from 'vue'; import { onBeforeMount, onMounted, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Notify } from 'quasar'; import { Notify } from 'quasar';
import axios from 'axios'; import axios from 'axios';
@ -10,10 +10,12 @@ import CmrFilter from './CmrFilter.vue';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import { useStateStore } from 'src/stores/useStateStore';
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession(); const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); const token = getTokenMultimedia();
const state = useStateStore();
const selected = ref([]); const selected = ref([]);
const warehouses = ref([]); const warehouses = ref([]);
@ -81,6 +83,9 @@ onBeforeMount(async () => {
const { data } = await axios.get('Warehouses'); const { data } = await axios.get('Warehouses');
warehouses.value = data; warehouses.value = data;
}); });
onMounted(() => (state.rightDrawer = true));
function getApiUrl() { function getApiUrl() {
return new URL(window.location).origin; return new URL(window.location).origin;
} }
@ -109,13 +114,7 @@ function downloadPdfs() {
</RightMenu> </RightMenu>
<div class="column items-center"> <div class="column items-center">
<div class="list"> <div class="list">
<VnPaginate <VnPaginate data-key="CmrList" :url="`Routes/cmrs`" order="cmrFk DESC">
data-key="CmrList"
:url="`Routes/cmrs`"
order="cmrFk DESC"
limit="null"
auto-load
>
<template #body="{ rows }"> <template #body="{ rows }">
<QTable <QTable
:columns="columns" :columns="columns"

View File

@ -67,6 +67,7 @@ const filter = {
}, },
}, },
], ],
where: { id: entityId },
}; };
const openAddStopDialog = () => { const openAddStopDialog = () => {
@ -84,7 +85,7 @@ const openAddStopDialog = () => {
<CardSummary <CardSummary
data-key="RoadmapSummary" data-key="RoadmapSummary"
ref="summary" ref="summary"
:url="`Roadmaps/${entityId}`" :url="`Roadmaps`"
:filter="filter" :filter="filter"
> >
<template #header-left> <template #header-left>

View File

@ -39,7 +39,7 @@ const selectedRows = ref([]);
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'ID', name: 'ID',
label: t('ID'), label: 'Id',
field: (row) => row.routeFk, field: (row) => row.routeFk,
sortable: true, sortable: true,
align: 'left', align: 'left',
@ -117,7 +117,9 @@ const columns = computed(() => [
const refreshKey = ref(0); const refreshKey = ref(0);
const total = computed(() => selectedRows.value.reduce((item) => item?.price || 0, 0)); const total = computed(() => {
return selectedRows.value.reduce((sum, item) => sum + item.price, 0);
});
const openDmsUploadDialog = async () => { const openDmsUploadDialog = async () => {
dmsDialog.value.rowsToCreateInvoiceIn = selectedRows.value dmsDialog.value.rowsToCreateInvoiceIn = selectedRows.value
@ -212,7 +214,6 @@ function navigateToRouteSummary(event, row) {
data-key="RouteAutonomousList" data-key="RouteAutonomousList"
url="AgencyTerms/filter" url="AgencyTerms/filter"
:limit="20" :limit="20"
auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<div class="q-pa-md"> <div class="q-pa-md">
@ -306,6 +307,13 @@ function navigateToRouteSummary(event, row) {
cursor: pointer; cursor: pointer;
} }
} }
th:last-child,
td:last-child {
background-color: var(--vn-section-color);
position: sticky;
right: 0;
}
</style> </style>
<i18n> <i18n>
es: es:

View File

@ -1,40 +1,50 @@
<script setup> <script setup>
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { dashIfEmpty, toHour } from 'src/filters'; import { dashIfEmpty, toHour } from 'src/filters';
import VnSelect from 'components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import { useValidator } from 'composables/useValidator'; import { useValidator } from 'composables/useValidator';
import { useSession } from 'composables/useSession';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useArrayData } from 'composables/useArrayData';
import { useQuasar } from 'quasar';
import axios from 'axios';
import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import FetchData from 'components/FetchData.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import RouteSummary from 'pages/Route/Card/RouteSummary.vue';
import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import VnInputTime from 'components/common/VnInputTime.vue'; import VnInputTime from 'components/common/VnInputTime.vue';
import axios from 'axios'; import VnLv from 'src/components/ui/VnLv.vue';
import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import RouteSummary from 'pages/Route/Card/RouteSummary.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useSession } from 'composables/useSession';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue';
import { useQuasar } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue';
const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator(); const { validate } = useValidator();
const { viewSummary } = useSummaryDialog();
const quasar = useQuasar(); const quasar = useQuasar();
const session = useSession(); const session = useSession();
const { viewSummary } = useSummaryDialog(); const paginate = ref();
const visibleColumns = ref([]); const visibleColumns = ref([]);
const selectedRows = ref([]); const selectedRows = ref([]);
const workers = ref([]);
const agencyList = ref([]);
const vehicleList = ref([]);
const allColumnNames = ref([]);
const confirmationDialog = ref(false);
const startingDate = ref(null);
const refreshKey = ref(0);
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'ID', name: 'Id',
label: t('ID'), label: t('Id'),
field: (row) => row.id, field: (row) => row.id,
sortable: true, sortable: true,
align: 'center', align: 'center',
@ -109,14 +119,12 @@ const columns = computed(() => [
align: 'right', align: 'right',
}, },
]); ]);
const arrayData = useArrayData('EntryLatestBuys', { const arrayData = useArrayData('EntryLatestBuys', {
url: 'Buys/latestBuysFilter', url: 'Buys/latestBuysFilter',
order: ['itemFk DESC'], order: ['itemFk DESC'],
}); });
const refreshKey = ref(0);
const workers = ref([]);
const agencyList = ref([]);
const vehicleList = ref([]);
const updateRoute = async (route) => { const updateRoute = async (route) => {
try { try {
return await axios.patch(`Routes/${route.id}`, route); return await axios.patch(`Routes/${route.id}`, route);
@ -124,9 +132,6 @@ const updateRoute = async (route) => {
return err; return err;
} }
}; };
const allColumnNames = ref([]);
const confirmationDialog = ref(false);
const startingDate = ref(null);
const cloneRoutes = () => { const cloneRoutes = () => {
axios.post('Routes/clone', { axios.post('Routes/clone', {
@ -135,6 +140,7 @@ const cloneRoutes = () => {
}); });
refreshKey.value++; refreshKey.value++;
startingDate.value = null; startingDate.value = null;
paginate.value.fetch();
}; };
const showRouteReport = () => { const showRouteReport = () => {
@ -154,15 +160,13 @@ const showRouteReport = () => {
window.open(url, '_blank'); window.open(url, '_blank');
}; };
const markAsServed = () => { function markAsServed() {
selectedRows.value.forEach((row) => { selectedRows.value.forEach(async (row) => {
if (row?.id) { if (row?.id) await axios.patch(`Routes/${row?.id}`, { isOk: true });
axios.patch(`Routes/${row?.id}`, { isOk: true });
}
}); });
refreshKey.value++; refreshKey.value++;
startingDate.value = null; startingDate.value = null;
}; }
const openTicketsDialog = (id) => { const openTicketsDialog = (id) => {
if (!id) { if (!id) {
@ -179,11 +183,9 @@ const openTicketsDialog = (id) => {
}; };
onMounted(async () => { onMounted(async () => {
stateStore.rightDrawer = true;
allColumnNames.value = columns.value.map((col) => col.name); allColumnNames.value = columns.value.map((col) => col.name);
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
}); });
onUnmounted(() => (stateStore.rightDrawer = false));
</script> </script>
<template> <template>
@ -210,6 +212,10 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<QBtn flat :label="t('Cancel')" v-close-popup class="text-primary" /> <QBtn flat :label="t('Cancel')" v-close-popup class="text-primary" />
<QBtn color="primary" v-close-popup @click="cloneRoutes"> <QBtn color="primary" v-close-popup @click="cloneRoutes">
{{ t('globals.clone') }} {{ t('globals.clone') }}
<VnLv
:label="t('route.summary.packages')"
:value="getTotalPackages(entity.tickets)"
/>
</QBtn> </QBtn>
</QCardActions> </QCardActions>
</QCard> </QCard>
@ -228,7 +234,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
class="LeftIcon" class="LeftIcon"
:all-columns="allColumnNames" :all-columns="allColumnNames"
table-code="routesList" table-code="routesList"
labels-traductions-path="globals" labels-traductions-path="route.columnLabels"
@on-config-saved="visibleColumns = [...$event]" @on-config-saved="visibleColumns = [...$event]"
/> />
</template> </template>
@ -256,7 +262,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
color="primary" color="primary"
class="q-mr-sm" class="q-mr-sm"
:disable="!selectedRows?.length" :disable="!selectedRows?.length"
@click="markAsServed" @click="markAsServed()"
> >
<QTooltip>{{ t('Mark as served') }}</QTooltip> <QTooltip>{{ t('Mark as served') }}</QTooltip>
</QBtn> </QBtn>
@ -269,7 +275,6 @@ onUnmounted(() => (stateStore.rightDrawer = false));
url="Routes/filter" url="Routes/filter"
:order="['created ASC', 'started ASC', 'id ASC']" :order="['created ASC', 'started ASC', 'id ASC']"
:limit="20" :limit="20"
auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<div class="q-pa-md route-table"> <div class="q-pa-md route-table">
@ -500,7 +505,6 @@ en:
hourStarted: Started hour hourStarted: Started hour
hourFinished: Finished hour hourFinished: Finished hour
es: es:
ID: ID
Worker: Trabajador Worker: Trabajador
Agency: Agencia Agency: Agencia
Vehicle: Vehículo Vehicle: Vehículo
@ -521,4 +525,6 @@ es:
Summary: Resumen Summary: Resumen
Route is closed: La ruta está cerrada Route is closed: La ruta está cerrada
Route is not served: La ruta no está servida Route is not served: La ruta no está servida
hourStarted: Hora de inicio
hourFinished: Hora de fin
</i18n> </i18n>

View File

@ -128,8 +128,8 @@ function confirmRemove() {
.onOk(() => refreshKey.value++); .onOk(() => refreshKey.value++);
} }
function navigateToRoadmapSummary(event, row) { function navigateToRoadmapSummary(_, { id }) {
router.push({ name: 'RoadmapSummary', params: { id: row.id } }); router.push({ name: 'RoadmapSummary', params: { id } });
} }
</script> </script>
@ -193,7 +193,6 @@ function navigateToRoadmapSummary(event, row) {
url="Roadmaps" url="Roadmaps"
:limit="20" :limit="20"
:filter="filter" :filter="filter"
auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<div class="q-pa-md"> <div class="q-pa-md">

View File

@ -141,7 +141,7 @@ const setOrderedPriority = async () => {
}; };
const sortRoutes = async () => { const sortRoutes = async () => {
await axios.get(`Routes/${route.params?.id}/guessPriority/`); await axios.patch(`Routes/${route.params?.id}/guessPriority/`);
refreshKey.value++; refreshKey.value++;
}; };

View File

@ -36,7 +36,12 @@ const filter = {
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<CardSummary ref="summary" :url="`Shelvings/${entityId}`" :filter="filter"> <CardSummary
ref="summary"
:url="`Shelvings/${entityId}`"
:filter="filter"
data-key="ShelvingSummary"
>
<template #header="{ entity }"> <template #header="{ entity }">
<div>{{ entity.code }}</div> <div>{{ entity.code }}</div>
</template> </template>

View File

@ -104,10 +104,14 @@ const totalEntryPrice = (rows) => {
for (const row of rows) { for (const row of rows) {
let total = 0; let total = 0;
let quantity = 0; let quantity = 0;
for (const buy of row.buys) {
total = total + buy.total; if (row.buys) {
quantity = quantity + buy.quantity; for (const buy of row.buys) {
total = total + buy.total;
quantity = quantity + buy.quantity;
}
} }
row.total = total; row.total = total;
row.quantity = quantity; row.quantity = quantity;
totalPrice = totalPrice + total; totalPrice = totalPrice + total;

View File

@ -50,7 +50,7 @@ const itemCategoriesOptions = ref([]);
@on-fetch="(data) => (itemCategoriesOptions = data)" @on-fetch="(data) => (itemCategoriesOptions = data)"
auto-load auto-load
/> />
<VnFilterPanel :data-key="props.dataKey" :search-button="true"> <VnFilterPanel :data-key="props.dataKey" :search-button="true" :redirect="false">
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong> <strong>{{ t(`params.${tag.label}`) }}: </strong>

View File

@ -48,6 +48,7 @@ function getUrl(section) {
ref="summaryRef" ref="summaryRef"
:url="`Suppliers/${entityId}/getSummary`" :url="`Suppliers/${entityId}/getSummary`"
@on-fetch="(data) => setData(data)" @on-fetch="(data) => setData(data)"
data-key="SupplierSummary"
> >
<template #header> <template #header>
<span>{{ supplier.name }} - {{ supplier.id }}</span> <span>{{ supplier.name }} - {{ supplier.id }}</span>

View File

@ -0,0 +1,263 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import FetchData from 'components/FetchData.vue';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'filters/index';
import { useRole } from 'src/composables/useRole';
const $props = defineProps({
formData: {
type: Object,
required: true,
},
haveNegatives: {
type: Boolean,
required: true,
default: false,
},
});
const emit = defineEmits(['updateForm', 'update:haveNegatives']);
const stateStore = useStateStore();
const { t } = useI18n();
const { hasAny } = useRole();
const _ticketData = ref($props.formData);
const ticketUpdateActions = ref(null);
const haveNegatives = computed({
get: () => $props.haveNegatives,
set: (val) => emit('update:haveNegatives', val),
});
const rows = computed(() => _ticketData.value?.sale?.items || []);
watch(
() => _ticketData.value,
(val) => emit('updateForm', val),
{ deep: true }
);
const columns = computed(() => [
{
required: true,
label: t('basicData.item'),
name: 'item',
align: 'left',
format: (val) => val.name,
},
{
required: true,
label: t('basicData.description'),
name: 'description',
align: 'left',
hidden: true,
},
{
label: t('basicData.movable'),
name: 'movable',
align: 'left',
},
{
required: true,
label: t('basicData.quantity'),
name: 'quantity',
field: 'quantity',
align: 'left',
},
{
required: true,
label: t('basicData.pricePPU'),
name: 'price',
field: 'price',
align: 'left',
format: (val) => toCurrency(val),
},
{
required: true,
label: t('basicData.newPricePPU'),
name: 'newPrice',
field: (row) => row.component.newPrice,
align: 'left',
format: (val) => toCurrency(val),
},
{
required: true,
label: t('basicData.difference'),
name: 'difference',
field: (row) => row.component.difference,
align: 'left',
format: (val) => toCurrency(val),
},
]);
const loadDefaultTicketAction = () => {
const isSalesAssistant = hasAny(['salesAssistant']);
_ticketData.value.option = isSalesAssistant ? 'mana' : 'renewPrices';
};
const totalPrice = computed(() => {
return rows.value.reduce((acc, item) => acc + item.price * item.quantity, 0);
});
const totalNewPrice = computed(() => {
return rows.value.reduce(
(acc, item) => acc + item.component.newPrice * item.quantity,
0
);
});
const totalDifference = computed(() => {
return rows.value.reduce((acc, item) => acc + item.component?.difference || 0, 0);
});
const showMovablecolumn = computed(() => (haveDifferences.value > 0 ? ['movable'] : []));
const haveDifferences = computed(() => _ticketData.value.sale?.haveDifferences);
const ticketHaveNegatives = () => {
let _haveNegatives = false;
let haveNotNegatives = false;
_ticketData.value.withoutNegatives = false;
_ticketData.value?.sale?.items.forEach((item) => {
if (item.quantity > item.movable) _haveNegatives = true;
else haveNotNegatives = true;
});
haveNegatives.value = _haveNegatives && haveNotNegatives && haveDifferences.value;
if (haveNegatives.value) _ticketData.value.withoutNegatives = true;
};
onMounted(() => {
stateStore.rightDrawer = true;
loadDefaultTicketAction();
ticketHaveNegatives();
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
url="TicketUpdateActions"
@on-fetch="(data) => (ticketUpdateActions = data)"
auto-load
/>
<RightMenu>
<template #right-panel>
<QCard
class="q-pa-md q-mb-md q-ma-md color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('basicData.total') }}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<span>
{{ t('basicData.price') }}:
{{ toCurrency(totalPrice) }}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<span>
{{ t('basicData.newPrice') }}: {{ toCurrency(totalNewPrice) }}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<span>
{{ t('basicData.difference') }}: {{ toCurrency(totalDifference) }}
</span>
</QCardSection>
</QCard>
<QCard
v-if="totalDifference"
class="q-pa-md q-mb-md q-ma-md color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('basicData.chargeDifference') }}
</span>
</QCardSection>
<QCardSection
v-for="(action, index) in ticketUpdateActions"
:key="index"
horizontal
>
<QRadio
v-model="_ticketData.option"
:val="action.code"
:label="action.description"
dense
/>
</QCardSection>
</QCard>
<QCard
v-if="haveNegatives"
class="q-pa-md q-mb-md q-ma-md color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal class="flex row items-center">
<QCheckbox
:label="t('basicData.withoutNegatives')"
v-model="_ticketData.withoutNegatives"
:toggle-indeterminate="false"
/>
<QIcon name="info" size="xs" class="q-ml-sm">
<QTooltip max-width="350px">
{{ t('basicData.withoutNegativesInfo') }}
</QTooltip>
</QIcon>
</QCardSection>
</QCard>
</template>
</RightMenu>
<QTable
:visible-columns="showMovablecolumn"
:rows="rows"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
flat
>
<template #body-cell-item="{ row }">
<QTd>
<QBtn flat color="primary">
{{ row.itemFk }}
<ItemDescriptorProxy :id="row.itemFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-description="{ row }">
<QTd>
<div class="column">
<span>{{ row.item.name }}</span>
<span class="color-vn-label">{{ row.item.subName }}</span>
<FetchedTags :item="row.item" :max-length="6" />
</div>
</QTd>
</template>
<template #body-cell-movable="{ row }">
<QTd>
<QBadge
v-if="_ticketData?.sale?.haveDifferences"
:text-color="row.quantity > row.movable ? 'black' : 'white'"
:color="row.quantity > row.movable ? 'negative' : 'transparent'"
:label="row.movable"
/>
</QTd>
</template>
</QTable>
</template>

View File

@ -0,0 +1,468 @@
<script setup>
import { ref, computed, onMounted, watch } 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 VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import { toTimeFormat } from 'filters/date.js';
const $props = defineProps({
formData: {
type: Object,
required: true,
default: () => ({}),
},
});
const emit = defineEmits(['updateForm']);
const { notify } = useNotify();
const router = useRouter();
const { t } = useI18n();
const agencyFetchRef = ref(null);
const zonesFetchRef = ref(null);
const clientsOptions = ref([]);
const warehousesOptions = ref([]);
const companiesOptions = ref([]);
const agenciesOptions = ref([]);
const zonesOptions = ref([]);
const addresses = ref([]);
const formData = ref($props.formData);
watch(
() => formData.value,
(val) => emit('updateForm', val),
{ deep: true }
);
const agencyByWarehouseFilter = computed(() => ({
fields: ['id', 'name'],
order: 'name ASC',
where: {
warehouseFk: warehouseId.value,
},
}));
const zonesFilter = computed(() => ({
fields: ['id', 'name'],
order: 'name ASC',
where: formData.value?.agencyModeFk
? {
shipped: formData.value?.shipped,
addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk,
}
: {},
}));
const getLanded = async (params) => {
try {
const validParams =
shipped.value && addressId.value && agencyModeId.value && warehouseId.value;
if (!validParams) return;
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');
}
};
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(`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');
}
};
const onChangeZone = async (zoneId) => {
try {
formData.value.agencyModeFk = null;
const { data } = await axios.get(`Zones/${zoneId}`);
formData.value.agencyModeFk = data.agencyModeFk;
} catch (error) {
console.error(error);
}
};
const onChangeAddress = async (addressId) => {
try {
formData.value.nickname = null;
const { data } = await axios.get(`Addresses/${addressId}`);
formData.value.nickname = data.nickname;
} catch (error) {
console.error(error);
}
};
const getClientDefaultAddress = async (clientId) => {
try {
const { data } = await axios.get(`Clients/${clientId}`);
if (data) addressId.value = data.defaultAddressFk;
} catch (error) {
console.error(error);
}
};
const clientAddressesList = async (value) => {
let filter = {
include: [
{
relation: 'province',
scope: {
fields: ['name'],
},
},
{
relation: 'agencyMode',
scope: {
fields: ['name'],
},
},
],
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`Clients/${value}/addresses`, { params });
if (data) addresses.value = data;
};
const addressId = computed({
get: () => formData.value?.addressFk,
set: (val) => {
if (val != formData.value?.addressFk) {
formData.value.addressFk = val;
onChangeAddress(val);
getShipped({
landed: formData.value?.landed,
addressFk: val,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk,
});
}
},
});
const clientId = computed({
get: () => formData.value?.clientFk,
set: (val) => {
formData.value.clientFk = val;
formData.value.addressFk = null;
if (!val) return;
getClientDefaultAddress(val);
clientAddressesList(val);
},
});
const landed = computed({
get: () => formData.value?.landed,
set: (val) => {
formData.value.landed = val;
getShipped({
landed: val,
addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk,
});
},
});
const agencyModeId = computed({
get: () => formData.value.agencyModeFk,
set: (val) => {
if (val != formData.value.agencyModeFk) {
formData.value.agencyModeFk = val;
if (!val) return;
const agencyMode = agenciesOptions.value.find((a) => a.id == val);
formData.value.warehouseFk = agencyMode.warehouseFk;
getLanded({
shipped: formData.value?.shipped,
addressFk: formData.value?.addressFk,
agencyModeFk: val,
warehouseFk: formData.value?.warehouseFk,
});
}
},
});
const zoneId = computed({
get: () => formData.value?.zoneFk,
set: (val) => {
if (val != formData.value?.zoneFk) {
formData.value.zoneFk = val;
onChangeZone(val);
}
},
});
const warehouseId = computed({
get: () => formData.value?.warehouseFk,
set: (val) => {
if (val != formData.value?.warehouseFk) {
formData.value.warehouseFk = val;
getShipped({
landed: formData.value?.landed,
addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: val,
}).then(() => {
if (zoneId.value == null) formData.value.agencyModeFk = null;
});
}
},
});
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);
};
const redirectToCustomerAddress = () => {
router.push({
name: 'CustomerAddressEditCard',
params: { id: clientId.value, addressId: addressId.value },
});
};
onMounted(() => onFormModelInit());
</script>
<template>
<FetchData
url="Clients"
:filter="{
fields: ['id', 'name'],
order: 'id',
}"
@on-fetch="(data) => (clientsOptions = data)"
auto-load
/>
<FetchData
url="Warehouses"
@on-fetch="(data) => (warehousesOptions = data)"
auto-load
/>
<FetchData
url="Companies"
:filter="{
fields: ['id', 'code'],
order: 'code ASC',
}"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
/>
<FetchData
ref="agencyFetchRef"
url="AgencyModes/byWarehouse"
:filter="agencyByWarehouseFilter"
@on-fetch="(data) => (agenciesOptions = data)"
auto-load
/>
<FetchData
ref="zonesFetchRef"
url="Zones/includingExpired"
:filter="zonesFilter"
@on-fetch="(data) => (zonesOptions = data)"
auto-load
/>
<QForm>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
:label="t('basicData.client')"
v-model="clientId"
option-value="id"
option-label="name"
:options="clientsOptions"
hide-selected
map-options
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
>#{{ scope.opt?.id }} {{ scope.opt?.name }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
:label="t('basicData.warehouse')"
v-model="warehouseId"
option-value="id"
option-label="name"
:options="warehousesOptions"
hide-selected
map-options
:required="true"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
:label="t('basicData.address')"
v-model="addressId"
option-value="id"
option-label="nickname"
:options="addresses"
hide-selected
map-options
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
:class="{
'color-vn-label': !scope.opt?.isActive,
}"
>
{{
`${
!scope.opt?.isActive
? t('basicData.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>
<template #append>
<QIcon
name="edit"
color="primary"
size="sm"
class="fill-icon cursor-pointer"
@click.stop="redirectToCustomerAddress()"
>
<QTooltip>{{ t('basicData.editAddress') }}</QTooltip>
</QIcon>
</template>
</VnSelect>
<VnInput
:label="t('basicData.alias')"
v-model="formData.nickname"
:required="true"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md no-wrap">
<VnSelect
:label="t('basicData.company')"
v-model="formData.companyFk"
option-value="id"
option-label="code"
:options="companiesOptions"
hide-selected
map-options
:required="true"
/>
<VnSelect
:label="t('basicData.agency')"
v-model="agencyModeId"
option-value="id"
option-label="name"
:options="agenciesOptions"
hide-selected
map-options
@focus="agencyFetchRef.fetch()"
/>
<VnSelect
:label="t('basicData.zone')"
v-model="zoneId"
option-value="id"
option-label="name"
:options="zonesOptions"
hide-selected
map-options
:required="true"
@focus="zonesFetchRef.fetch()"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
>{{ scope.opt?.name }} - Max.
{{ toTimeFormat(scope.opt?.hour) }}
h.</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInputDate
:label="t('basicData.shipped')"
v-model="formData.shipped"
:required="true"
/>
<VnInputTime
:label="t('basicData.shippedHour')"
v-model="formData.shipped"
:required="true"
/>
<VnInputDate
:label="t('basicData.landed')"
v-model="formData.landed"
:required="true"
/>
</VnRow>
</QForm>
</template>

View File

@ -0,0 +1,195 @@
<script setup>
import { ref, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import BasicDataTable from './BasicDataTable.vue';
import TicketBasicDataForm from './TicketBasicDataForm.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const { notify } = useNotify();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const stepperRef = ref(null);
const { openConfirmationModal } = useVnConfirm();
const step = ref(1);
const formData = ref({});
const initialDataLoaded = ref(false);
const haveNegatives = ref(false);
const ticketFilter = {
include: [
{ relation: 'address' },
{
relation: 'client',
scope: {
fields: [
'salesPersonFk',
'name',
'isActive',
'isFreezed',
'isTaxDataChecked',
'credit',
'email',
'phone',
'mobile',
'hasElectronicInvoice',
],
include: {
relation: 'salesPersonUser',
scope: { fields: ['id', 'name'] },
},
},
},
{ relation: 'invoiceOut' },
],
};
const getTicketData = async () => {
const params = { filter: JSON.stringify(ticketFilter) };
const { data } = await axios.get(`tickets/${route.params.id}`, { params });
formData.value = data;
initialDataLoaded.value = true;
};
const isFormInvalid = () => {
return (
!formData.value.clientFk ||
!formData.value.addressFk ||
!formData.value.agencyModeFk ||
!formData.value.companyFk ||
!formData.value.shipped ||
!formData.value.landed ||
!formData.value.zoneFk
);
};
const getPriceDifference = async () => {
try {
const params = {
landed: formData.value.landed,
addressId: formData.value.addressFk,
agencyModeId: formData.value.agencyModeFk,
zoneId: formData.value.zoneFk,
warehouseId: formData.value.warehouseFk,
shipped: formData.value.shipped,
};
const { data } = await axios.post(
`tickets/${formData.value.id}/priceDifference`,
params
);
formData.value.sale = data;
} catch (error) {
console.error(error);
}
};
const submit = async () => {
try {
if (!formData.value.option)
return notify(t('basicData.chooseAnOption'), 'negative');
const params = {
clientFk: formData.value.clientFk,
nickname: formData.value.nickname,
agencyModeFk: formData.value.agencyModeFk,
addressFk: formData.value.addressFk,
zoneFk: formData.value.zoneFk,
warehouseFk: formData.value.warehouseFk,
companyFk: formData.value.companyFk,
shipped: formData.value.shipped,
landed: formData.value.landed,
isDeleted: formData.value.isDeleted,
option: formData.value.option,
isWithoutNegatives: formData.value.withoutNegatives,
withWarningAccept: formData.value.withWarningAccept,
keepPrice: false,
};
const { data } = await axios.post(
`tickets/${formData.value.id}/componentUpdate`,
params
);
if (!data) return;
const ticketToMove = data.id;
notify(t('basicData.unroutedTicket'), 'positive');
router.push({ name: 'TicketSummary', params: { id: ticketToMove } });
} catch (error) {
console.error(error);
}
};
const submitWithNegatives = async () => {
formData.value.withWarningAccept = true;
submit();
};
const onNextStep = async () => {
if (step.value === 1) {
if (isFormInvalid())
return notify(t('basicData.someFieldsAreInvalid'), 'negative');
await getPriceDifference();
stepperRef.value.next();
} else if (step.value === 2) {
if (haveNegatives.value && !formData.value.withoutNegatives)
openConfirmationModal(
t('basicData.negativesConfirmTitle'),
t('basicData.negativesConfirmMessage'),
submitWithNegatives
);
else submit();
}
};
onBeforeMount(async () => await getTicketData());
</script>
<template>
<QStepper
v-model="step"
ref="stepperRef"
color="primary"
animated
keep-alive
style="max-width: 800px; margin: auto"
>
<QStep :name="1" :title="t('globals.pageTitles.basicData')" :done="step > 1">
<TicketBasicDataForm
v-if="initialDataLoaded"
@update-form="($event) => (formData = $event)"
:form-data="formData"
/>
</QStep>
<QStep :name="2" :title="t('basicData.priceDifference')">
<BasicDataTable
:form-data="formData"
v-model:haveNegatives="haveNegatives"
@update-form="($event) => (formData = $event)"
/>
</QStep>
<template #navigation>
<QStepperNavigation class="flex justify-between">
<QBtn
flat
color="primary"
@click="stepperRef.previous()"
:label="t('basicData.back')"
class="q-ml-sm"
:class="{ invisible: step === 1 }"
/>
<QBtn
@click="onNextStep()"
color="primary"
:label="step === 2 ? t('basicData.finalize') : t('basicData.next')"
/>
</QStepperNavigation>
</template>
</QStepper>
</template>

View File

@ -1,3 +0,0 @@
<template>
<QCard>Basic Data</QCard>
</template>

View File

@ -1,17 +1,30 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { computed } from 'vue';
import VnCard from 'components/common/VnCard.vue'; import VnCard from 'components/common/VnCard.vue';
import TicketDescriptor from './TicketDescriptor.vue'; import TicketDescriptor from './TicketDescriptor.vue';
import TicketFilter from '../TicketFilter.vue'; import TicketFilter from '../TicketFilter.vue';
const { t } = useI18n();
const route = useRoute();
const routeName = computed(() => route.name);
const searchBarDataKeys = {
TicketSummary: 'TicketSummary',
TicketSale: 'TicketSale',
TicketPurchaseRequest: 'TicketPurchaseRequest',
};
</script> </script>
<template> <template>
<VnCard <VnCard
data-key="Ticket" data-key="Ticket"
base-url="Tickets"
:descriptor="TicketDescriptor"
:filter-panel="TicketFilter" :filter-panel="TicketFilter"
search-data-key="TicketList" :descriptor="TicketDescriptor"
search-url="Tickets/filter" :search-data-key="searchBarDataKeys[routeName]"
searchbar-label="Search ticket" :search-custom-route-redirect="routeName"
searchbar-info="You can search by ticket id or alias" :searchbar-label="t('card.search')"
:searchbar-info="t('card.searchInfo')"
/> />
</template> </template>

View File

@ -0,0 +1,69 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
const emit = defineEmits(['onRequestCreated']);
const route = useRoute();
const { t } = useI18n();
const attendersOptions = ref([]);
</script>
<template>
<FetchData
url="TicketRequests/getItemTypeWorker"
:filter="{ fields: ['id', 'nickname'], order: 'nickname ASC' }"
auto-load
@on-fetch="(data) => (attendersOptions = data)"
/>
<FormModelPopup
:title="t('Create request')"
url-create="TicketRequests"
model="CreateTicketRequest"
:form-initial-data="{ ticketFk: route.params.id }"
@on-data-saved="() => emit('onRequestCreated')"
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
v-model="data.description"
:label="t('purchaseRequest.description')"
/>
<VnSelect
:label="t('purchaseRequest.atender')"
v-model="data.attenderFk"
:options="attendersOptions"
hide-selected
option-label="nickname"
option-value="id"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
v-model="data.quantity"
:label="t('purchaseRequest.quantity')"
type="number"
min="1"
/>
<VnInput
v-model="data.price"
:label="t('purchaseRequest.price')"
type="number"
min="0"
/>
</VnRow>
</template>
</FormModelPopup>
</template>
<i18n>
es:
Create request: Crear petición de compra
</i18n>

View File

@ -0,0 +1,96 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters';
const $props = defineProps({
mana: {
type: Number,
default: null,
},
newPrice: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['save', 'cancel']);
const { t } = useI18n();
const QPopupProxyRef = ref(null);
const save = () => {
emit('save');
QPopupProxyRef.value.hide();
};
const cancel = () => {
emit('cancel');
QPopupProxyRef.value.hide();
};
</script>
<template>
<QPopupProxy ref="QPopupProxyRef">
<div class="container">
<QSpinner v-if="!mana" color="orange" size="md" />
<div v-else>
<div class="header">Mana: {{ toCurrency(mana) }}</div>
<div class="q-pa-md">
<slot />
<div v-if="newPrice" class="column items-center q-mt-lg">
<span class="text-primary">{{ t('New price') }}</span>
<span class="text-subtitle1">
{{ toCurrency($props.newPrice) }}
</span>
</div>
</div>
</div>
<div class="row">
<QBtn
color="primary"
class="no-border-radius"
dense
style="width: 50%"
@click="cancel()"
>
{{ t('globals.cancel') }}
</QBtn>
<QBtn
color="primary"
class="no-border-radius"
dense
style="width: 50%"
@click="save()"
>
{{ t('globals.save') }}
</QBtn>
</div>
</div>
</QPopupProxy>
</template>
<style lang="scss" scoped>
.container {
background-color: $dark;
width: 230px;
}
.header {
height: 54px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: $primary;
font-size: 1.2rem;
font-weight: bold;
min-width: 230px;
}
</style>
<i18n>
es:
New price: Nuevo precio
</i18n>

View File

@ -0,0 +1,10 @@
<script setup>
import VnLog from 'src/components/common/VnLog.vue';
import { useRoute } from 'vue-router';
const route = useRoute();
</script>
<template>
<VnLog model="Ticket" url="/TicketLogs" :key="route.params.id"></VnLog>
</template>

View File

@ -0,0 +1,267 @@
<script setup>
import { ref, computed, watch, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import CrudModel from 'src/components/CrudModel.vue';
import TicketCreateRequest from './TicketCreateRequest.vue';
import { dashIfEmpty } from 'src/filters';
import { toDateFormat } from 'src/filters/date.js';
const route = useRoute();
const { t } = useI18n();
const createTicketRequestDialogRef = ref(null);
const crudModelRef = ref(null);
watch(
() => route.params.id,
async (val) => {
crudModelFilter.where.ticketFk = val;
crudModelRef.value.reload();
}
);
const crudModelFilter = reactive({
include: [
{
relation: 'atender',
scope: {
include: {
relation: 'user',
scope: {
fields: ['nickname'],
},
},
},
},
{
relation: 'requester',
scope: {
include: {
relation: 'user',
scope: {
fields: ['nickname'],
},
},
},
},
{
relation: 'sale',
},
],
fields: [
'id',
'description',
'created',
'requesterFk',
'attenderFk',
'quantity',
'price',
'saleFk',
'isOk',
],
order: ['created ASC'],
where: {
ticketFk: route.params.id,
},
});
const columns = computed(() => [
{
label: t('purchaseRequest.id'),
name: 'id',
field: 'id',
align: 'left',
columnFilter: null,
},
{
label: t('purchaseRequest.description'),
name: 'description',
field: 'description',
align: 'left',
format: (val) => dashIfEmpty(val),
},
{
label: t('purchaseRequest.created'),
name: 'created',
field: 'created',
align: 'left',
format: (val) => toDateFormat(val),
},
{
label: t('purchaseRequest.requester'),
name: 'requester',
align: 'left',
sortable: true,
},
{
label: t('purchaseRequest.atender'),
name: 'atender',
align: 'left',
},
{
label: t('purchaseRequest.quantity'),
name: 'quantity',
align: 'left',
},
{
label: t('purchaseRequest.price'),
name: 'price',
align: 'left',
},
{
label: t('purchaseRequest.saleFk'),
name: 'saleFk',
align: 'left',
},
{
label: t('purchaseRequest.state'),
name: 'state',
field: 'isOk',
align: 'left',
format: (val) => t(getRequestState(val)),
},
{
label: '',
name: 'actions',
align: 'left',
columnFilter: null,
},
]);
const getRequestState = (state) => {
switch (state) {
case null:
return 'New';
case false:
return 'Denied';
case true:
return 'Acepted';
}
};
const isEditable = (isOk) => isOk !== null;
const removeLine = async (row) => crudModelRef.value.remove([row]);
const openCreateModal = () => createTicketRequestDialogRef.value.show();
</script>
<template>
<QPage class="column items-center q-pa-md">
<CrudModel
data-key="PurchaseRequests"
url="TicketRequests"
ref="crudModelRef"
:filter="crudModelFilter"
:order="['created ASC']"
:default-remove="false"
:default-save="false"
:default-reset="false"
:limit="0"
auto-load
>
<template #body="{ rows }">
<QTable
:rows="rows"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
@row-click="(_, row) => redirectToTicketSummary(row.ticketFk)"
>
<template #body-cell-description="{ row }">
<QTd @click.stop>
<VnInput
v-model="row.description"
@blur="crudModelRef.saveChanges()"
:disable="isEditable(row.isOk)"
/>
</QTd>
</template>
<template #body-cell-requester="{ row }">
<QTd @click.stop>
<QBtn flat color="primary">
{{ row.requester?.user?.nickname }}
<WorkerDescriptorProxy :id="row.requesterFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-atender="{ row }">
<QTd @click.stop>
<QBtn flat color="primary">
{{ row.atender?.user?.nickname }}
<WorkerDescriptorProxy :id="row.attenderFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-quantity="{ row }">
<QTd @click.stop>
<VnInput
v-model="row.quantity"
@blur="crudModelRef.saveChanges()"
:disable="isEditable(row.isOk)"
/>
</QTd>
</template>
<template #body-cell-price="{ row }">
<QTd @click.stop>
<VnInput
v-model="row.price"
@blur="crudModelRef.saveChanges()"
:disable="isEditable(row.isOk)"
/>
</QTd>
</template>
<template #body-cell-saleFk="{ row }">
<QTd @click.stop>
<QBtn v-if="row.saleFk" flat color="primary">
{{ row.sale.itemFk }}
<ItemDescriptorProxy :id="row.sale.itemFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-actions="{ row }">
<QTd>
<QIcon
@click.stop="removeLine(row)"
class="q-ml-sm cursor-pointer"
color="primary"
name="delete"
size="sm"
>
<QTooltip>
{{ t('globals.delete') }}
</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
</template>
</CrudModel>
<QDialog
ref="createTicketRequestDialogRef"
transition-show="scale"
transition-hide="scale"
>
<TicketCreateRequest @on-request-created="crudModelRef.reload()" />
</QDialog>
<QPageSticky :offset="[20, 20]">
<QBtn @click="openCreateModal()" color="primary" fab icon="add" />
<QTooltip class="text-no-wrap">
{{ t('purchaseRequest.newRequest') }}
</QTooltip>
</QPageSticky>
</QPage>
</template>
<i18n>
es:
New: Nueva
Denied: Denegada
Accepted: Aceptada
</i18n>

View File

@ -1 +1,779 @@
<template>Ticket sale</template> <script setup>
import { onMounted, ref, computed, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import TicketEditManaProxy from './TicketEditMana.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import TicketSaleMoreActions from './TicketSaleMoreActions.vue';
import TicketTransfer from './TicketTransfer.vue';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency, toPercentage, dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { useVnConfirm } from 'composables/useVnConfirm';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const route = useRoute();
const router = useRouter();
const stateStore = useStateStore();
const { t } = useI18n();
const { notify } = useNotify();
const { openConfirmationModal } = useVnConfirm();
const editPriceProxyRef = ref(null);
const stateBtnDropdownRef = ref(null);
const arrayData = useArrayData('ticketData');
const { store } = arrayData;
const ticketConfig = ref(null);
const isLocked = ref(false);
const isTicketEditable = ref(false);
const sales = ref([]);
const itemsWithNameOptions = ref([]);
const editableStatesOptions = ref([]);
const selectedSales = ref([]);
const mana = ref(null);
const manaCode = ref('mana');
const ticketState = computed(() => store.data?.ticketState?.state?.code);
const transfer = ref({
lastActiveTickets: [],
sales: [],
});
watch(
() => route.params.id,
async () => await getSales()
);
const columns = computed(() => [
{
label: '',
name: 'statusIcons',
align: 'left',
},
{
label: '',
name: 'picture',
align: 'left',
},
{
label: t('ticketSale.visible'),
name: 'visible',
field: 'visible',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.available'),
name: 'available',
field: 'available',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.id'),
name: 'itemFk',
field: 'itemFk',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.quantity'),
name: 'quantity',
field: 'quantity',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.item'),
name: 'item',
field: 'item',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.price'),
name: 'price',
field: 'price',
align: 'left',
sortable: true,
format: (val) => toCurrency(val),
},
{
label: t('ticketSale.discount'),
name: 'discount',
field: 'discount',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.amount'),
name: 'amount',
field: 'amount',
align: 'left',
sortable: true,
format: (val) => toCurrency(val),
},
{
label: t('ticketSale.packaging'),
name: 'itemPackingTypeFk',
field: 'item',
align: 'left',
sortable: true,
format: (val) => dashIfEmpty(val?.itemPackingTypeFk),
},
{
label: '',
name: 'history',
align: 'left',
columnFilter: null,
},
]);
const getConfig = async () => {
try {
let filter = {
fields: ['daysForWarningClaim'],
};
const { data } = await axios.get(`TicketConfigs`, { filter });
ticketConfig.value = data;
} catch (err) {
console.error('Error getting ticket config', err);
}
};
const onSalesFetched = (salesData) => {
sales.value = salesData;
for (let sale of salesData) sale.amount = getSaleTotal(sale);
};
const getSales = async () => {
try {
const { data } = await axios.get(`Tickets/${route.params.id}/getSales`);
onSalesFetched(data);
} catch (err) {
console.error('Error fetching sales', err);
}
};
const getSaleTotal = (sale) => {
if (sale.quantity == null || sale.price == null) return null;
const price = sale.quantity * sale.price;
const discount = (sale.discount * price) / 100;
return price - discount;
};
const resetChanges = async () => {
arrayData.fetch({ append: false });
getSales();
};
const updateQuantity = async (sale) => {
try {
const payload = { quantity: sale.quantity };
await axios.post(`Sales/${sale.id}/updateQuantity`, payload);
notify('globals.dataSaved', 'positive');
} catch (err) {
console.error('Error updating quantity', err);
}
};
const addSale = async (sale) => {
try {
const payload = {
barcode: sale.itemFk,
quantity: sale.quantity,
};
const { data } = await axios.post(`tickets/${route.params.id}/addSale`, payload);
if (!data) return;
const newSale = data;
sale.id = newSale.id;
sale.image = newSale.item.image;
sale.subName = newSale.item.subName;
sale.concept = newSale.concept;
sale.quantity = newSale.quantity;
sale.discount = newSale.discount;
sale.price = newSale.price;
sale.item = newSale.item;
notify('globals.dataSaved', 'positive');
} catch (err) {
console.error('Error adding sale', err);
}
};
const changeQuantity = (sale) => {
if (
!sale.itemFk ||
sale.quantity == null ||
edit.value?.oldQuantity === sale.quantity
)
return;
if (!sale.id) return addSale(sale);
updateQuantity(sale);
};
const updateConcept = async (sale) => {
try {
const data = { newConcept: sale.concept };
await axios.post(`Sales/${sale.id}/updateConcept`, data);
notify('globals.dataSaved', 'positive');
} catch (err) {
console.error('Error updating concept', err);
}
};
const DEFAULT_EDIT = {
price: null,
discount: null,
sale: null,
sales: null,
oldQuantity: null,
};
const edit = ref({ ...DEFAULT_EDIT });
const usesMana = ref(null);
const getUsesMana = async () => {
const { data } = await axios.get('Sales/usesMana');
usesMana.value = data;
};
const getMana = async () => {
const { data } = await axios.get(`Tickets/${route.params.id}/getSalesPersonMana`);
mana.value = data;
await getUsesMana();
};
const selectedValidSales = computed(() => {
if (!sales.value) return;
return selectedSales.value.filter((sale) => sale.id != undefined);
});
const onOpenEditPricePopover = async (sale) => {
await getMana();
edit.value = {
sale: JSON.parse(JSON.stringify(sale)),
price: sale.price,
};
};
const onOpenEditDiscountPopover = async (sale) => {
await getMana();
if (isLocked.value) return;
if (sale) {
edit.value = {
sale: JSON.parse(JSON.stringify(sale)),
discount: sale.discount,
};
} else {
edit.value = {
discount: null,
sales: selectedValidSales.value,
};
}
};
const updatePrice = async (sale) => {
try {
const newPrice = edit.value.price;
if (newPrice != null && newPrice != sale.price) {
await axios.post(`Sales/${sale.id}/updatePrice`, { newPrice });
sale.price = newPrice;
edit.value = { ...DEFAULT_EDIT };
notify('globals.dataSaved', 'positive');
}
await getMana();
} catch (err) {
console.error('Error updating price', err);
}
};
const changeDiscount = (sale) => {
const newDiscount = edit.value.discount;
if (newDiscount != null && newDiscount != sale.discount) updateDiscount([sale]);
};
const updateDiscount = async (sales, newDiscount = null) => {
const saleIds = sales.map((sale) => sale.id);
const _newDiscount = newDiscount || edit.value.discount;
const params = {
salesIds: saleIds,
newDiscount: _newDiscount,
manaCode: manaCode.value,
};
await axios.post(`Tickets/${route.params.id}/updateDiscount`, params);
notify('globals.dataSaved', 'positive');
for (let sale of sales) sale.discount = _newDiscount;
edit.value = { ...DEFAULT_EDIT };
};
const getNewPrice = computed(() => {
if (edit.value?.sale) {
const sale = edit.value.sale;
let newDiscount = sale.discount;
let newPrice = edit.value.price || sale.price;
if (edit.value.discount != null) newDiscount = edit.value.discount;
if (edit.value.price != null) newPrice = edit.value.price;
const price = sale.quantity * newPrice;
const discount = (newDiscount * price) / 100;
return price - discount;
}
return 0;
});
const newOrderFromTicket = async () => {
try {
const { data } = await axios.post(`Orders/newFromTicket`, {
ticketFk: Number(route.params.id),
});
const routeData = router.resolve({ name: 'OrderCatalog', params: { id: data } });
window.open(routeData.href, '_blank');
} catch (err) {
console.error('Error creating new order', err);
}
};
const goToLog = (saleId) => {
router.push({
name: 'TicketLog',
params: {
originId: route.params.id,
changedModel: 'Sale',
changedModelId: saleId,
},
});
};
const changeTicketState = async (val) => {
try {
stateBtnDropdownRef.value.hide();
const params = { ticketFk: route.params.id, code: val };
await axios.post('Tickets/state', params);
notify('globals.dataSaved', 'positive');
await resetChanges();
} catch (err) {
console.error('Error changing ticket state', err);
}
};
const removeSelectedSales = () => {
selectedSales.value.forEach((sale) => {
const index = sales.value.indexOf(sale);
sales.value.splice(index, 1);
});
};
const removeSales = async () => {
try {
const params = { sales: selectedValidSales.value, ticketId: store.data.id };
await axios.post('Sales/deleteSales', params);
removeSelectedSales();
notify('globals.dataSaved', 'positive');
} catch (err) {
console.error('Error deleting sales', err);
}
};
const insertRow = () => sales.value.push({ ...DEFAULT_EDIT });
const setTransferParams = async () => {
try {
const checkedSales = JSON.parse(JSON.stringify(selectedSales.value));
transfer.value = {
lastActiveTickets: [],
sales: checkedSales,
};
const params = { ticketId: store.data.id };
const { data } = await axios.get(
`clients/${store.data.clientFk}/lastActiveTickets`,
{
params,
}
);
transfer.value.lastActiveTickets = data;
} catch (err) {
console.error('Error setting transfer params', err);
}
};
onMounted(async () => {
stateStore.rightDrawer = true;
getConfig();
getSales();
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
:url="`Tickets/${route.params.id}/isEditable`"
auto-load
@on-fetch="(data) => (isTicketEditable = data)"
/>
<FetchData
:url="`Tickets/${route.params.id}/isLocked`"
auto-load
@on-fetch="(data) => (isLocked = data)"
/>
<FetchData
url="Items/withName"
:filter="{ fields: ['id', 'name'], order: 'id DESC' }"
auto-load
@on-fetch="(data) => (itemsWithNameOptions = data)"
/>
<FetchData
url="States/editableStates"
:filter="{ fields: ['code', 'name', 'id', 'alertLevel'], order: 'name ASC' }"
auto-load
@on-fetch="(data) => (editableStatesOptions = data)"
/>
<VnSubToolbar>
<template #st-actions>
<QBtnGroup push class="q-gutter-x-sm" flat>
<QBtn
:label="t('ticketSale.ok')"
color="primary"
:disable="!isTicketEditable || ticketState === 'OK'"
@click="changeTicketState('OK')"
>
<QTooltip>{{ t(`Change ticket state to 'Ok'`) }}</QTooltip>
</QBtn>
<QBtnDropdown
ref="stateBtnDropdownRef"
color="primary"
:label="t('ticketSale.state')"
:disable="!isTicketEditable"
>
<VnSelect
:options="editableStatesOptions"
hide-selected
option-label="name"
option-value="code"
hide-dropdown-icon
focus-on-mount
@update:model-value="changeTicketState"
/>
</QBtnDropdown>
<TicketSaleMoreActions
:ticket="store.data"
:is-ticket-editable="isTicketEditable"
:sales="selectedValidSales"
:disable="!selectedSales.length"
:mana="mana"
:ticket-config="ticketConfig"
@get-mana="getMana()"
@update-discounts="updateDiscount"
/>
<QBtn
color="primary"
icon="delete"
:disable="!isTicketEditable || !selectedSales.length"
@click="
openConfirmationModal(
t('Continue anyway?'),
t('You are going to delete lines of the ticket'),
removeSales
)
"
>
<QTooltip>{{ t('Remove lines') }}</QTooltip>
</QBtn>
<QBtn
color="primary"
icon="vn:splitline"
:disable="!isTicketEditable || !selectedSales.length"
@click="setTransferParams()"
>
<QTooltip>{{ t('Transfer lines') }}</QTooltip>
<TicketTransfer
:transfer="transfer"
:ticket="store.data"
@refresh-data="resetChanges()"
/>
</QBtn>
</QBtnGroup>
</template>
</VnSubToolbar>
<RightMenu>
<template #right-panel>
<div
class="q-pa-md q-mb-md q-ma-md color-vn-text"
style="border: 2px solid black"
>
<QCardSection class="justify-center text-subtitle1" horizontal>
<span class="q-mr-xs color-vn-label"
>{{ t('ticketSale.subtotal') }}:
</span>
<span>{{ toCurrency(store.data?.totalWithoutVat) }}</span>
</QCardSection>
<QCardSection class="justify-center text-subtitle1" horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketSale.tax') }}:
</span>
<span>{{
toCurrency(store.data?.totalWithVat - store.data?.totalWithoutVat)
}}</span>
</QCardSection>
<QCardSection
class="justify-center text-weight-bold text-subtitle1"
horizontal
>
<span class="q-mr-xs color-vn-label">
{{ t('ticketSale.total') }}:
</span>
<span>{{ toCurrency(store.data?.totalWithVat) }}</span>
</QCardSection>
</div>
</template>
</RightMenu>
<QTable
:rows="sales"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
selection="multiple"
v-model:selected="selectedSales"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-statusIcons="{ row }">
<QTd class="q-gutter-x-xs">
<router-link
v-if="row.claim?.claimFk"
:to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }"
>
<QIcon color="primary" name="vn:claims" size="xs">
<QTooltip>
{{ t('ticketSale.claim') }}:
{{ row.claim?.claimFk }}
</QTooltip>
</QIcon>
</router-link>
<QIcon v-if="row.visible < 0" color="primary" name="warning" size="xs">
<QTooltip>
{{ t('ticketSale.visible') }}: {{ row.visible || 0 }}
</QTooltip>
</QIcon>
<QIcon v-if="row.reserved" color="primary" name="vn:reserva" size="xs">
<QTooltip>
{{ t('ticketSale.reserved') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.itemShortage"
color="primary"
name="vn:unavailable"
size="xs"
>
<QTooltip>
{{ t('ticketSale.noVisible') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.hasComponentLack"
color="primary"
name="vn:components"
size="xs"
>
<QTooltip>
{{ t('ticketSale.hasComponentLack') }}
</QTooltip>
</QIcon>
</QTd>
</template>
<template #body-cell-picture="{ row }">
<QTd>
<div class="image-wrapper">
<VnImg :id="row.itemFk" class="rounded" />
</div>
</QTd>
</template>
<template #body-cell-visible="{ row }">
<QTd @click.stop>
<QBadge :color="row.visible < 0 ? 'alert' : 'transparent'" dense>
{{ row.visible }}
</QBadge>
</QTd>
</template>
<template #body-cell-available="{ row }">
<QTd @click.stop>
<QBadge :color="row.available < 0 ? 'alert' : 'transparent'" dense>
{{ row.available }}
</QBadge>
</QTd>
</template>
<template #body-cell-itemFk="{ row }">
<QTd @click.stop>
<div v-if="row.id">
<QBtn flat color="primary" dense>
{{ row.itemFk }}
</QBtn>
<ItemDescriptorProxy :id="row.itemFk" />
</div>
<VnSelect
v-else
:options="itemsWithNameOptions"
hide-selected
option-label="name"
option-value="id"
@update:model-value="changeQuantity(row)"
v-model="row.itemFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.name }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-quantity="{ row }">
<QTd @click.stop>
<VnInput
v-if="isTicketEditable"
v-model.number="row.quantity"
@keyup.enter="changeQuantity(row)"
@blur="changeQuantity(row)"
@focus="edit.oldQuantity = row.quantity"
/>
<span v-else>{{ row.quantity }}</span>
</QTd>
</template>
<template #body-cell-item="{ row }">
<QTd class="col">
<div class="column">
<span>{{ row.concept }}</span>
<span class="color-vn-label">{{ row.item?.subName }}</span>
<FetchedTags v-if="row.item" :item="row.item" :max-length="6" />
<QPopupProxy v-if="row.id && isTicketEditable">
<VnInput v-model="row.concept" @change="updateConcept(row)" />
</QPopupProxy>
</div>
</QTd>
</template>
<template #body-cell-price="{ row }">
<QTd>
<template v-if="isTicketEditable && row.id">
<QBtn flat color="primary" dense @click="onOpenEditPricePopover(row)">
{{ toCurrency(row.price) }}
</QBtn>
<TicketEditManaProxy
ref="editPriceProxyRef"
:mana="mana"
:new-price="getNewPrice"
@save="updatePrice(row)"
>
<VnInput
v-model.number="edit.price"
:label="t('ticketSale.price')"
type="number"
/>
</TicketEditManaProxy>
</template>
<span v-else>{{ toCurrency(row.price) }}</span>
</QTd>
</template>
<template #body-cell-discount="{ row }">
<QTd>
<template v-if="!isLocked && row.id">
<QBtn
flat
color="primary"
dense
@click="onOpenEditDiscountPopover(row)"
>
{{ toPercentage(row.discount / 100) }}
</QBtn>
<TicketEditManaProxy
:mana="mana"
:new-price="getNewPrice"
@save="changeDiscount(row)"
>
<VnInput
v-model.number="edit.discount"
:label="t('ticketSale.discount')"
type="number"
/>
</TicketEditManaProxy>
</template>
<span v-else>{{ toPercentage(row.discount / 100) }}</span>
</QTd>
</template>
<template #body-cell-history="{ row }">
<QTd>
<QBtn
v-if="row.$hasLogs"
@click.stop="goToLog(row.id)"
color="primary"
icon="history"
size="md"
flat
>
<QTooltip class="text-no-wrap">
{{ t('ticketSale.history') }}
</QTooltip>
</QBtn>
</QTd>
</template>
<template #bottom-row>
<QBtn
class="cursor-pointer fill-icon q-ml-md q-my-lg"
color="primary"
icon="add_circle"
size="md"
round
flat
:disable="!isTicketEditable"
@click="insertRow()"
>
<QTooltip>
{{ t('Add item') }}
</QTooltip>
</QBtn>
</template>
</QTable>
<QPageSticky :offset="[20, 20]">
<QBtn @click="newOrderFromTicket()" color="primary" fab icon="add" />
<QTooltip class="text-no-wrap">
{{ t('Add item to basket') }}
</QTooltip>
</QPageSticky>
</template>
<i18n>
es:
New item: Nuevo artículo
Add item to basket: Añadir artículo a la cesta
Change ticket state to 'Ok': Cambiar estado del ticket a 'Ok'
Remove lines: Eliminar líneas
Continue anyway?: ¿Continuar de todas formas?
You are going to delete lines of the ticket: Vas a eliminar lineas del ticket
Add item: Añadir artículo
Select lines to see the options: Selecciona líneas para ver las opciones
Transfer lines: Transferir líneas
</i18n>

View File

@ -0,0 +1,289 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router';
import VnSmsDialog from 'components/common/VnSmsDialog.vue';
import TicketEditManaProxy from './TicketEditMana.vue';
import VnInput from 'src/components/common/VnInput.vue';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
import { toDateFormat } from 'src/filters/date';
import { useRole } from 'src/composables/useRole';
import { useVnConfirm } from 'composables/useVnConfirm';
const emit = defineEmits(['updateDiscounts', 'getMana']);
const props = defineProps({
disable: {
type: Boolean,
default: false,
},
isTicketEditable: {
type: Boolean,
default: false,
},
ticket: {
type: Object,
required: true,
default: () => {},
},
sales: {
type: Array,
default: () => [],
},
mana: {
type: Number,
default: null,
},
ticketConfig: {
type: Array,
default: () => [],
},
});
const router = useRouter();
const { t } = useI18n();
const { dialog } = useQuasar();
const { notify } = useNotify();
const role = useRole();
const btnDropdownRef = ref(null);
const { openConfirmationModal } = useVnConfirm();
const newDiscount = ref(null);
const ticket = computed(() => props.ticket);
const isClaimable = computed(() => {
if (ticket.value) {
const landedPlusWeek = new Date(ticket.value.landed);
landedPlusWeek.setDate(landedPlusWeek.getDate() + 7);
const hasClaimManagerRole = role.hasAny('claimManager');
return landedPlusWeek >= Date.vnNew() || hasClaimManagerRole;
}
return false;
});
const hasReserves = computed(() => props.sales.some((sale) => sale.reserved == true));
const sendSms = async (params) => {
await axios.post(`Tickets/${ticket.value.id}/sendSms`, params);
notify(t('SMS sent'), 'positive');
};
const showSmsDialog = (template) => {
const address = ticket.value.address;
const client = ticket.value.client;
const phone = address.mobile || address.phone || client.mobile || client.phone;
const items = props.sales.map((sale) => {
return `${sale.quantity} ${sale.concept}`;
});
const notAvailables = items.join(', ');
const data = {
ticketId: ticket.value.id,
destinationFk: ticket.value.clientFk,
destination: phone,
ticketFk: ticket.value.id,
created: ticket.value.updated,
landed: toDateFormat(ticket.value.landed),
notAvailables,
};
dialog({
component: VnSmsDialog,
componentProps: {
phone: phone,
template: template,
locale: client?.user?.lang ?? 'default_locale',
data: data,
promise: sendSms,
},
});
};
const calculateSalePrice = async () => {
if (!props.sales) return;
await axios.post(`Sales/recalculatePrice`, props.sales);
notify(t('globals.dataSaved'), 'positive');
};
const changeMultipleDiscount = () => {
const hasChanges = props.sales.some((sale) => {
return sale.discount != newDiscount.value;
});
if (newDiscount.value != null && hasChanges)
emit('updateDiscounts', props.sales, newDiscount.value);
btnDropdownRef.value.hide();
};
const createClaim = () => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const timeDifference = today.getTime() - new Date(ticket.value.landed).getTime();
const pastDays = Math.floor(timeDifference / 86400000);
if (pastDays >= props.ticketConfig[0].daysForWarningClaim)
openConfirmationModal(
t('Claim out of time'),
t('Do you want to continue?'),
onCreateClaimAccepted
);
else
openConfirmationModal(t('Do you want to create a claim?'), onCreateClaimAccepted);
};
const onCreateClaimAccepted = async () => {
try {
const params = { ticketId: ticket.value.id, sales: props.sales };
const { data } = await axios.post(`Claims/createFromSales`, params);
router.push({ name: 'ClaimBasicData', params: { id: data.id } });
} catch (error) {
console.error('Error creating claim: ', error);
}
};
const setReserved = async (reserved) => {
const params = { ticketId: ticket.value.id, sales: props.sales, reserved: reserved };
await axios.post(`Sales/reserve`, params);
props.sales.forEach((sale) => {
sale.reserved = reserved;
});
};
const createRefund = async (withWarehouse) => {
if (!props.sales) return;
const salesIds = props.sales.map((sale) => sale.id);
const params = { salesIds: salesIds, withWarehouse: withWarehouse, negative: true };
const { data } = await axios.post('Sales/clone', params);
const [refundTicket] = data;
notify(t('refundTicketCreated', { ticketId: refundTicket.id }), 'positive');
router.push({ name: 'TicketSale', params: { id: refundTicket.id } });
};
</script>
<template>
<QBtnDropdown
ref="btnDropdownRef"
color="primary"
:label="t('ticketSale.more')"
:disable="disable"
>
<template #label>
<QTooltip>{{ t('Select lines to see the options') }}</QTooltip>
</template>
<QList>
<QItem
v-if="ticket"
clickable
v-close-popup
v-ripple
@click="showSmsDialog('productNotAvailable')"
>
<QItemSection>
<QItemLabel>{{ t('Send shortage SMS') }}</QItemLabel>
</QItemSection>
</QItem>
<QItem
v-if="isTicketEditable"
clickable
v-close-popup
v-ripple
@click="calculateSalePrice()"
>
<QItemSection>
<QItemLabel>{{ t('Recalculate price') }}</QItemLabel>
</QItemSection>
</QItem>
<QItem clickable v-ripple @click="emit('getMana')">
<QItemSection>
<QItemLabel>{{ t('Update discount') }}</QItemLabel>
</QItemSection>
<TicketEditManaProxy :mana="props.mana" @save="changeMultipleDiscount()">
<VnInput
v-model.number="newDiscount"
:label="t('ticketSale.discount')"
type="number"
/>
</TicketEditManaProxy>
</QItem>
<QItem
v-if="isClaimable"
clickable
v-close-popup
v-ripple
@click="createClaim()"
>
<QItemSection>
<QItemLabel>{{ t('Add claim') }}</QItemLabel>
</QItemSection>
</QItem>
<QItem
v-if="isTicketEditable"
clickable
v-close-popup
v-ripple
@click="setReserved(true)"
>
<QItemSection>
<QItemLabel>{{ t('Mark as reserved') }}</QItemLabel>
</QItemSection>
</QItem>
<QItem
v-if="isTicketEditable && hasReserves"
clickable
v-close-popup
v-ripple
@click="setReserved(false)"
>
<QItemSection>
<QItemLabel>{{ t('Unmark as reserved') }}</QItemLabel>
</QItemSection>
</QItem>
<QItem clickable v-ripple>
<QItemSection>
<QItemLabel>{{ t('Refund...') }}</QItemLabel>
</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start" auto-close bordered>
<QList>
<QItem v-ripple clickable @click="createRefund(true)">
<QItemSection>
{{ t('with warehouse') }}
</QItemSection>
</QItem>
<QItem v-ripple clickable @click="createRefund(false)">
<QItemSection>
{{ t('without warehouse') }}
</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem>
</QList>
</QBtnDropdown>
</template>
<i18n>
en:
refundTicketCreated: 'The following refund ticket have been created {ticketId}'
es:
SMS sent: SMS enviado
Send shortage SMS: Enviar SMS faltas
Recalculate price: Recalcular precio
Update discount: Actualizar descuento
Add claim: Crear reclamación
Mark as reserved: Marcar como reservado
Unmark as reserved: Desmarcar como reservado
Refund...: Abono...
with warehouse: con almacén
without warehouse: sin almacén
Claim out of time: Reclamación fuera de plazo
Do you want to continue?: ¿Desea continuar?
Do you want to create a claim?: ¿Quieres crear una reclamación?
refundTicketCreated: 'The following refund ticket have been created: {ticketId}'
</i18n>

View File

@ -90,6 +90,7 @@ async function changeState(value) {
ref="summaryRef" ref="summaryRef"
:url="`Tickets/${entityId}/summary`" :url="`Tickets/${entityId}/summary`"
@on-fetch="(data) => setData(data)" @on-fetch="(data) => setData(data)"
data-key="TicketSummary"
> >
<template #header="{ entity }"> <template #header="{ entity }">
<div> <div>

View File

@ -0,0 +1,196 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import { toDateFormat } from 'src/filters/date.js';
import axios from 'axios';
const $props = defineProps({
mana: {
type: Number,
default: null,
},
newPrice: {
type: Number,
default: 0,
},
transfer: {
type: Object,
default: () => {},
},
ticket: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['refreshData']);
const router = useRouter();
const { t } = useI18n();
const QPopupProxyRef = ref(null);
const _transfer = ref(null);
const transferLinesColumns = computed(() => [
{
label: t('ticketSale.id'),
name: 'itemFk',
field: 'itemFk',
align: 'left',
},
{
label: t('ticketSale.item'),
name: 'item',
field: 'concept',
align: 'left',
},
{
label: t('ticketSale.quantity'),
name: 'quantity',
field: 'quantity',
align: 'left',
},
]);
const destinationTicketColumns = computed(() => [
{
label: t('ticketSale.id'),
name: 'id',
field: 'id',
align: 'left',
},
{
label: t('ticketSale.shipped'),
name: 'item',
field: 'shipped',
align: 'left',
format: (val) => toDateFormat(val),
},
{
label: t('ticketSale.agency'),
name: 'agency',
field: 'agencyName',
align: 'left',
},
{
label: t('ticketSale.address'),
name: 'address',
field: 'address',
align: 'left',
},
]);
const transferSales = async (ticketId) => {
const params = {
ticketId: ticketId,
sales: $props.transfer.sales,
};
const { data } = await axios.post(
`tickets/${$props.ticket.id}/transferSales`,
params
);
if (data && data.id === $props.ticket.id) emit('refreshData');
else router.push({ name: 'TicketSale', params: { id: data.id } });
};
onMounted(() => (_transfer.value = $props.transfer));
</script>
<template>
<QPopupProxy ref="QPopupProxyRef">
<QCard class="q-px-md" style="display: flex">
<QTable
v-if="transfer.sales"
:rows="transfer.sales"
:columns="transferLinesColumns"
:title="t('Sales to transfer')"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-quantity="{ row }">
<QTd @click.stop>
<VnInput
v-model.number="row.quantity"
:clearable="false"
@keyup.enter="changeQuantity(row)"
@blur="changeQuantity(row)"
@focus="edit.oldQuantity = row.quantity"
style="max-width: 60px"
/>
</QTd>
</template>
</QTable>
<QSeparator vertical spaced />
<QTable
v-if="transfer.lastActiveTickets"
:rows="transfer.lastActiveTickets"
:columns="destinationTicketColumns"
:title="t('Destination ticket')"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-address="{ row }">
<QTd @click.stop>
<span>
{{ row.nickname }}
{{ row.name }}
{{ row.street }}
{{ row.postalCode }}
{{ row.city }}
</span>
<QTooltip>
{{ row.nickname }}
{{ row.name }}
{{ row.street }}
{{ row.postalCode }}
{{ row.city }}
</QTooltip>
</QTd>
</template>
<template #bottom>
<QForm class="q-mt-lg full-width">
<VnInput
v-model.number="_transfer.ticketId"
:label="t('Transfer to ticket')"
:clearable="false"
>
<template #append>
<QBtn
icon="keyboard_arrow_right"
color="primary"
@click="transferSales(_transfer.ticketId)"
style="width: 30px"
/>
</template>
</VnInput>
<QBtn
:label="t('New ticket')"
color="primary"
class="full-width q-my-lg"
@click="transferSales()"
/>
</QForm>
</template>
</QTable>
</QCard>
</QPopupProxy>
</template>
<i18n>
es:
Sales to transfer: Líneas a transferir
Destination ticket: Ticket destinatario
Transfer to ticket: Transferir a ticket
New ticket: Nuevo ticket
</i18n>

View File

@ -0,0 +1,710 @@
<script setup>
import { onMounted, ref, computed, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnProgress from 'src/components/common/VnProgressModal.vue';
import { dashIfEmpty, toCurrency } from 'src/filters';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import { useState } from 'src/composables/useState';
import { toDateFormat } from 'src/filters/date.js';
import axios from 'axios';
const state = useState();
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const user = state.getUser();
const itemPackingTypesOptions = ref([]);
const zonesOptions = ref([]);
const selectedTickets = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'id':
case 'futureId':
case 'liters':
case 'futureLiters':
case 'lines':
case 'futureLines':
case 'totalWithVat':
case 'futureTotalWithVat':
case 'futureZone':
case 'notMovableLines':
case 'futureZoneFk':
return { [param]: value };
case 'ipt':
return { ipt: { like: `%${value}%` } };
case 'futureIpt':
return { futureIpt: { like: `%${value}%` } };
}
};
const userParams = reactive({});
const arrayData = useArrayData('AdvanceTickets', {
url: 'Tickets/getTicketsAdvance',
userParams: userParams,
exprBuilder: exprBuilder,
});
const { store } = arrayData;
const tickets = computed(() =>
(store.data || []).map((ticket, index) => ({ ...ticket, index: index }))
);
const applyColumnFilter = async (col) => {
try {
const paramKey = col.columnFilter?.filterParamKey || col.field;
userParams[paramKey] = col.columnFilter.filterValue;
await arrayData.addFilter({ params: userParams });
} catch (err) {
console.error('Error applying column filter', err);
}
};
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const ticketColumns = computed(() => [
{
label: '',
name: 'icons',
align: 'left',
columnFilter: null,
},
{
label: t('advanceTickets.ticketId'),
name: 'ticketId',
align: 'center',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
filterParamKey: 'id',
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('advanceTickets.ipt'),
name: 'ipt',
field: 'ipt',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
filterParamKey: 'ipt',
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemPackingTypesOptions.value,
'option-value': 'code',
'option-label': 'description',
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.state'),
name: 'state',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('advanceTickets.liters'),
name: 'liters',
field: 'liters',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('advanceTickets.lines'),
name: 'lines',
field: 'lines',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.import'),
field: 'import',
name: 'import',
align: 'left',
sortable: true,
},
{
label: t('advanceTickets.futureId'),
name: 'futureId',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
filterParamKey: 'futureId',
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('advanceTickets.futureIpt'),
name: 'futureIpt',
field: 'futureIpt',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
filterParamKey: 'futureIpt',
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemPackingTypesOptions.value,
'option-value': 'code',
'option-label': 'description',
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.futureState'),
name: 'futureState',
align: 'left',
sortable: true,
columnFilter: null,
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.futureLiters'),
name: 'futureLiters',
field: 'futureLiters',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.futureZone'),
name: 'futureZoneName',
field: 'futureZoneName',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
type: 'select',
filterValue: null,
filterParamKey: 'futureZoneFk',
event: getInputEvents,
attrs: {
options: zonesOptions.value,
'option-value': 'id',
'option-label': 'name',
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.notMovableLines'),
name: 'notMovableLines',
field: 'notMovableLines',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.futureLines'),
name: 'futureLines',
field: 'futureLines',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.futureImport'),
name: 'futureImport',
align: 'left',
sortable: true,
columnFilter: null,
},
]);
const isLessThan50 = (totalWithVat) =>
parseInt(totalWithVat) > 0 && parseInt(totalWithVat) < 50;
const totalPriceColor = (totalWithVat) =>
isLessThan50(totalWithVat) ? 'warning' : 'transparent';
const getLanded = async (params) => {
try {
const query = `Agencies/getLanded`;
const { data } = await axios.get(query, { params });
if (!data) return;
return data;
} catch (error) {
notify(t('advanceTickets.noDeliveryZone'), 'negative');
console.error('Error getting landed', error);
}
};
const requestComponentUpdate = async (ticket, isWithoutNegatives) => {
const query = `tickets/${ticket.futureId}/componentUpdate`;
if (!ticket.landed) {
const newLanded = await getLanded({
shipped: userParams.dateToAdvance,
addressFk: ticket.futureAddressFk,
agencyModeFk: ticket.agencyModeFk ?? ticket.futureAgencyModeFk,
warehouseFk: ticket.futureWarehouseFk,
});
if (!newLanded) {
notify(t('advanceTickets.noDeliveryZone'), 'negative');
return;
}
ticket.landed = newLanded.landed;
ticket.zoneFk = newLanded.zoneFk;
}
const params = {
clientFk: ticket.futureClientFk,
nickname: ticket.nickname,
agencyModeFk: ticket.agencyModeFk ?? ticket.futureAgencyModeFk,
addressFk: ticket.futureAddressFk,
zoneFk: ticket.zoneFk ?? ticket.futureZoneFk,
warehouseFk: ticket.futureWarehouseFk,
companyFk: ticket.futureCompanyFk,
shipped: userParams.dateToAdvance,
landed: ticket.landed,
isDeleted: false,
isWithoutNegatives,
newTicket: ticket.id ?? undefined,
keepPrice: true,
};
return { query, params };
};
const moveTicketsAdvance = async () => {
try {
let ticketsToMove = [];
for (const ticket of selectedTickets.value) {
if (!ticket.id) {
try {
const { query, params } = await requestComponentUpdate(ticket, false);
axios.post(query, params);
} catch (e) {
console.error('Error moving ticket', e);
}
continue;
}
ticketsToMove.push({
originId: ticket.futureId,
destinationId: ticket.id,
originShipped: ticket.futureShipped,
destinationShipped: ticket.shipped,
workerFk: ticket.workerFk,
});
}
const params = { tickets: ticketsToMove };
await axios.post('Tickets/merge', params);
arrayData.fetch({ append: false });
selectedTickets.value = [];
if (ticketsToMove.length)
notify(t('advanceTickets.moveTicketSuccess'), 'positive');
} catch (error) {
console.error('Error moving tickets', error);
}
};
const progressLength = ref(0);
const progressPercentage = computed(() => {
if (progressLength.value === 0 || selectedTickets.value.length === 0) return 0;
return progressLength.value / selectedTickets.value.length;
});
const splitErrors = ref([]);
const showProgressDialog = ref(false);
const cancelProgress = ref(false);
const progressAdd = () => {
progressLength.value++;
if (progressLength.value === selectedTickets.value.length) {
notify(
t('advanceTickets.moveTicketSuccess', {
ticketsNumber: progressLength.value - splitErrors.value.length,
}),
'positive'
);
}
};
const splitTickets = async () => {
try {
showProgressDialog.value = true;
for (const ticket of selectedTickets.value) {
if (cancelProgress.value) break;
try {
const { query, params } = await requestComponentUpdate(ticket, true);
await axios.post(query, params);
progressAdd(ticket.futureId);
} catch (error) {
splitErrors.value.push({
id: ticket.futureId,
reason: error.response?.data?.error?.message,
});
progressAdd(ticket.futureId);
}
}
} catch (error) {
console.error('Error splitting tickets', error);
} finally {
arrayData.fetch({ append: false });
}
};
const resetProgressData = () => {
if (cancelProgress.value) cancelProgress.value = false;
progressLength.value = 0;
splitErrors.value = [];
selectedTickets.value = [];
};
const handleCloseProgressDialog = () => {
showProgressDialog.value = false;
resetProgressData();
};
const handleCancelProgress = () => (cancelProgress.value = true);
onMounted(async () => {
let today = Date.vnNew();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
userParams.dateFuture = tomorrow;
userParams.dateToAdvance = today;
userParams.warehouseFk = user.value.warehouseFk;
await arrayData.addFilter({ userParams });
});
</script>
<template>
<FetchData
url="itemPackingTypes"
:filter="{
fields: ['code', 'description'],
order: 'description ASC',
where: { isActive: true },
}"
auto-load
@on-fetch="(data) => (itemPackingTypesOptions = data)"
/>
<FetchData
url="Zones"
:filter="{
fields: ['id', 'name'],
order: 'name ASC',
}"
auto-load
@on-fetch="(data) => (zonesOptions = data)"
/>
<VnSearchbar
data-key="WeeklyTickets"
:label="t('weeklyTickets.search')"
:info="t('weeklyTickets.searchInfo')"
/>
<VnSubToolbar>
<template #st-data>
<QBtn
icon="keyboard_double_arrow_left"
color="primary"
class="q-mr-sm"
:disable="!selectedTickets.length"
@click.stop="
openConfirmationModal(
t('advanceTickets.advanceTicketTitle'),
t(`advanceTickets.advanceTitleSubtitle`, {
selectedTickets: selectedTickets.length,
}),
moveTicketsAdvance
)
"
>
<QTooltip>
{{ t('advanceTickets.advanceTickets') }}
</QTooltip>
</QBtn>
<QBtn
icon="alt_route"
color="primary"
:disable="!selectedTickets.length"
@click.stop="
openConfirmationModal(
t('advanceTickets.advanceWithoutNegativeTitle'),
t(`advanceTickets.advanceWithoutNegativeSubtitle`, {
selectedTickets: selectedTickets.length,
}),
splitTickets
)
"
>
<QTooltip>
{{ t('advanceTickets.advanceTicketsWithoutNegatives') }}
</QTooltip>
</QBtn>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<QTable
:rows="tickets"
:columns="ticketColumns"
row-key="index"
selection="multiple"
v-model:selected="selectedTickets"
:pagination="{ rowsPerPage: 0 }"
:no-data-label="t('globals.noResults')"
style="max-width: 99%"
>
<template #header="props">
<QTr :props="props">
<QTh
class="horizontal-separator text-uppercase color-vn-label"
colspan="7"
translate
>
{{ t('advanceTickets.destination') }}
{{ toDateFormat(userParams.dateToAdvance) }}
</QTh>
<QTh
class="horizontal-separator text-uppercase color-vn-label"
colspan="9"
translate
>
{{ t('advanceTickets.origin') }}
{{ toDateFormat(userParams.dateFuture) }}
</QTh>
</QTr>
<QTr>
<QTh>
<QCheckbox v-model="props.selected" />
</QTh>
<QTh
v-for="(col, index) in ticketColumns"
:key="index"
:class="{ 'vertical-separator': col.name === 'futureId' }"
>
{{ col.label }}
</QTh>
</QTr>
</template>
<template #top-row="{ cols }">
<QTr>
<QTd />
<QTd
v-for="(col, index) in cols"
:key="index"
style="max-width: 100px"
>
<component
:is="col.columnFilter.component"
v-if="col.columnFilter"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #header-cell-availableLines="{ col }">
<QTh class="vertical-separator">
{{ col.label }}
</QTh>
</template>
<template #body-cell-icons="{ row }">
<QTd class="q-gutter-x-xs">
<QIcon
v-if="row.futureAgency !== row.agency && row.agency"
color="primary"
name="vn:agency-term"
size="xs"
>
<QTooltip class="column">
<span>
{{
t('advanceTickets.originAgency', {
agency: row.futureAgency,
})
}}
</span>
<span>
{{
t('advanceTickets.destinationAgency', {
agency: row.agency,
})
}}
</span>
</QTooltip>
</QIcon>
</QTd>
</template>
<template #body-cell-ticketId="{ row }">
<QTd>
<QBtn flat color="primary">
{{ row.id }}
<TicketDescriptorProxy :id="row.id" />
</QBtn>
</QTd>
</template>
<template #body-cell-state="{ row }">
<QTd>
<QBadge
text-color="black"
:color="row.classColor"
class="q-ma-none"
dense
>
{{ row.state }}
</QBadge>
</QTd>
</template>
<template #body-cell-import="{ row }">
<QTd>
<QBadge
:text-color="isLessThan50(row.totalWithVat) ? 'black' : 'white'"
:color="totalPriceColor(row.totalWithVat)"
class="q-ma-none"
dense
>
{{ toCurrency(row.totalWithVat || 0) }}
</QBadge>
</QTd>
</template>
<template #body-cell-futureId="{ row }">
<QTd class="vertical-separator">
<QBtn flat color="primary" dense>
{{ row.futureId }}
<TicketDescriptorProxy :id="row.futureId" />
</QBtn>
</QTd>
</template>
<template #body-cell-futureState="{ row }">
<QTd>
<QBadge
text-color="black"
:color="row.futureClassColor"
class="q-ma-none"
dense
>
{{ row.futureState }}
</QBadge>
</QTd>
</template>
<template #body-cell-futureImport="{ row }">
<QTd>
<QBadge
:text-color="
isLessThan50(row.futureTotalWithVat) ? 'black' : 'white'
"
:color="totalPriceColor(row.futureTotalWithVat)"
class="q-ma-none"
dense
>
{{ toCurrency(row.futureTotalWithVat || 0) }}
</QBadge>
</QTd>
</template>
</QTable>
<VnProgress
:progress="progressPercentage"
:cancelled="cancelProgress"
v-model:show-dialog="showProgressDialog"
@cancel="handleCancelProgress()"
@close="handleCloseProgressDialog()"
>
<div v-if="splitErrors.length" class="column">
<span>{{ t('advanceTickets.errorsList') }}:</span>
<span v-for="(error, index) in splitErrors" :key="index">
{{ error.id }}: {{ error.reason }}
</span>
</div>
</VnProgress>
</QPage>
</template>
<style scoped lang="scss">
.vertical-separator {
border-left: 4px solid white !important;
}
.horizontal-separator {
border-bottom: 4px solid white !important;
}
</style>

View File

@ -0,0 +1,533 @@
<script setup>
import { onMounted, ref, computed, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { dashIfEmpty, toCurrency } from 'src/filters';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData';
import { getDateQBadgeColor } from 'src/composables/getDateQBadgeColor.js';
import useNotify from 'src/composables/useNotify.js';
import { useState } from 'src/composables/useState';
import { toDateTimeFormat } from 'src/filters/date.js';
import axios from 'axios';
const state = useState();
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const user = state.getUser();
const itemPackingTypesOptions = ref([]);
const selectedTickets = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'id':
return { id: value };
case 'futureId':
return { futureId: value };
case 'liters':
return { liters: value };
case 'lines':
return { lines: value };
case 'ipt':
return { ipt: { like: `%${value}%` } };
case 'futureIpt':
return { futureIpt: { like: `%${value}%` } };
case 'totalWithVat':
return { totalWithVat: value };
}
};
const userParams = reactive({
futureDated: Date.vnNew(),
originDated: Date.vnNew(),
warehouseFk: user.value.warehouseFk,
});
const arrayData = useArrayData('FutureTickets', {
url: 'Tickets/getTicketsFuture',
userParams: userParams,
exprBuilder: exprBuilder,
});
const { store } = arrayData;
const params = reactive({
futureDated: Date.vnNew(),
originDated: Date.vnNew(),
warehouseFk: user.value.warehouseFk,
});
const applyColumnFilter = async (col) => {
try {
const paramKey = col.columnFilter?.filterParamKey || col.field;
params[paramKey] = col.columnFilter.filterValue;
await arrayData.addFilter({ params });
} catch (err) {
console.error('Error applying column filter', err);
}
};
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const ticketColumns = computed(() => [
{
label: t('futureTickets.problems'),
name: 'problems',
align: 'left',
columnFilter: null,
},
{
label: t('futureTickets.ticketId'),
name: 'ticketId',
align: 'center',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
filterParamKey: 'id',
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('futureTickets.shipped'),
name: 'shipped',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('futureTickets.ipt'),
name: 'ipt',
field: 'ipt',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
filterParamKey: 'ipt',
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemPackingTypesOptions.value,
'option-value': 'code',
'option-label': 'description',
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('futureTickets.state'),
name: 'state',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('futureTickets.liters'),
name: 'liters',
field: 'liters',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('futureTickets.import'),
field: 'import',
name: 'import',
align: 'left',
sortable: true,
},
{
label: t('futureTickets.availableLines'),
name: 'lines',
field: 'lines',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('futureTickets.futureId'),
name: 'futureId',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
filterParamKey: 'futureId',
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('futureTickets.futureShipped'),
name: 'futureShipped',
align: 'left',
sortable: true,
columnFilter: null,
format: (val) => dashIfEmpty(val),
},
{
label: t('futureTickets.futureIpt'),
name: 'futureIpt',
field: 'futureIpt',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
filterParamKey: 'futureIpt',
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemPackingTypesOptions.value,
'option-value': 'code',
'option-label': 'description',
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('futureTickets.futureState'),
name: 'futureState',
align: 'left',
sortable: true,
columnFilter: null,
format: (val) => dashIfEmpty(val),
},
]);
const isLessThan50 = (totalWithVat) =>
parseInt(totalWithVat) > 0 && parseInt(totalWithVat) < 50;
const totalPriceColor = (totalWithVat) =>
isLessThan50(totalWithVat) ? 'warning' : 'transparent';
const moveTicketsFuture = async () => {
try {
const ticketsToMove = selectedTickets.value.map((ticket) => ({
originId: ticket.id,
destinationId: ticket.futureId,
originShipped: ticket.shipped,
destinationShipped: ticket.futureShipped,
workerFk: ticket.workerFk,
}));
let params = { tickets: ticketsToMove };
await axios.post('Tickets/merge', params);
notify(t('futureTickets.moveTicketSuccess'), 'positive');
selectedTickets.value = [];
arrayData.fetch({ append: false });
} catch (error) {
console.error('Error moving tickets to future', error);
}
};
onMounted(async () => {
await arrayData.fetch({ append: false });
});
</script>
<template>
<FetchData
url="itemPackingTypes"
:filter="{
fields: ['code', 'description'],
order: 'description ASC',
where: { isActive: true },
}"
auto-load
@on-fetch="(data) => (itemPackingTypesOptions = data)"
/>
<VnSearchbar
data-key="FutureTickets"
:label="t('Search ticket')"
:info="t('futureTickets.searchInfo')"
/>
<VnSubToolbar>
<template #st-data>
<QBtn
icon="keyboard_double_arrow_right"
color="primary"
:disable="!selectedTickets.length"
@click.stop="
openConfirmationModal(
t('futureTickets.moveTicketTitle'),
t(`futureTickets.moveTicketDialogSubtitle`, {
selectedTickets: selectedTickets.length,
}),
moveTicketsFuture
)
"
>
<QTooltip>
{{ t('futureTickets.futureTicket') }}
</QTooltip>
</QBtn>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<QTable
:rows="store.data"
:columns="ticketColumns"
row-key="id"
selection="multiple"
v-model:selected="selectedTickets"
:pagination="{ rowsPerPage: 0 }"
:no-data-label="t('globals.noResults')"
style="max-width: 99%"
>
<template #header="props">
<QTr>
<QTh class="horizontal-separator" />
<QTh
class="horizontal-separator text-uppercase color-vn-label"
colspan="8"
translate
>
{{ t('futureTickets.origin') }}
</QTh>
<QTh
class="horizontal-separator text-uppercase color-vn-label"
colspan="4"
translate
>
{{ t('futureTickets.destination') }}
</QTh>
</QTr>
<QTr>
<QTh>
<QCheckbox v-model="props.selected" />
</QTh>
<QTh
v-for="(col, index) in ticketColumns"
:key="index"
:class="{ 'vertical-separator': col.name === 'futureId' }"
>
{{ col.label }}
</QTh>
</QTr>
</template>
<template #top-row="{ cols }">
<QTr>
<QTd />
<QTd
v-for="(col, index) in cols"
:key="index"
style="max-width: 100px"
>
<component
:is="col.columnFilter.component"
v-if="col.columnFilter"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #header-cell-availableLines="{ col }">
<QTh class="vertical-separator">
{{ col.label }}
</QTh>
</template>
<template #body-cell-problems="{ row }">
<QTd class="q-gutter-x-xs">
<QIcon
v-if="row.isTaxDataChecked === 0"
color="primary"
name="vn:no036"
size="xs"
>
<QTooltip>
{{ t('futureTickets.noVerified') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.hasTicketRequest"
color="primary"
name="vn:buyrequest"
size="xs"
>
<QTooltip>
{{ t('futureTickets.purchaseRequest') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.itemShortage"
color="primary"
name="vn:unavailable"
size="xs"
>
<QTooltip>
{{ t('futureTickets.noVisible') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.isFreezed"
color="primary"
name="vn:frozen"
size="xs"
>
<QTooltip>
{{ t('futureTickets.clientFrozen') }}
</QTooltip>
</QIcon>
<QIcon v-if="row.risk" color="primary" name="vn:risk" size="xs">
<QTooltip>
{{ t('futureTickets.risk') }}: {{ row.risk }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.hasComponentLack"
color="primary"
name="vn:components"
size="xs"
>
<QTooltip>
{{ t('futureTickets.componentLack') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.hasRounding"
color="primary"
name="sync_problem"
size="xs"
>
<QTooltip>
{{ t('futureTickets.rounding') }}
</QTooltip>
</QIcon>
</QTd>
</template>
<template #body-cell-ticketId="{ row }">
<QTd>
<QBtn flat color="primary">
{{ row.id }}
<TicketDescriptorProxy :id="row.id" />
</QBtn>
</QTd>
</template>
<template #body-cell-shipped="{ row }">
<QTd>
<QBadge
text-color="black"
:color="getDateQBadgeColor(row.shipped)"
class="q-ma-none"
>
{{ toDateTimeFormat(row.shipped) }}
</QBadge>
</QTd>
</template>
<template #body-cell-state="{ row }">
<QTd>
<QBadge
text-color="black"
:color="row.classColor"
class="q-ma-none"
dense
>
{{ row.state }}
</QBadge>
</QTd>
</template>
<template #body-cell-import="{ row }">
<QTd>
<QBadge
:text-color="
totalPriceColor(row.totalWithVat) === 'warning'
? 'black'
: 'white'
"
:color="totalPriceColor(row.totalWithVat)"
class="q-ma-none"
dense
>
{{ toCurrency(row.totalWithVat || 0) }}
</QBadge>
</QTd>
</template>
<template #body-cell-futureId="{ row }">
<QTd class="vertical-separator">
<QBtn flat color="primary" dense>
{{ row.futureId }}
<TicketDescriptorProxy :id="row.futureId" />
</QBtn>
</QTd>
</template>
<template #body-cell-futureShipped="{ row }">
<QTd>
<QBadge
text-color="black"
:color="getDateQBadgeColor(row.futureShipped)"
class="q-ma-none"
>
{{ toDateTimeFormat(row.futureShipped) }}
</QBadge>
</QTd>
</template>
<template #body-cell-futureState="{ row }">
<QTd>
<QBadge
text-color="black"
:color="row.futureClassColor"
class="q-ma-none"
dense
>
{{ row.futureState }}
</QBadge>
</QTd>
</template>
</QTable>
</QPage>
</template>
<style scoped lang="scss">
.vertical-separator {
border-left: 4px solid white !important;
}
.horizontal-separator {
border-bottom: 4px solid white !important;
}
</style>

View File

@ -0,0 +1,326 @@
<script setup>
import { onMounted, ref, computed, reactive, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { dashIfEmpty } from 'src/filters';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const router = useRouter();
const stateStore = useStateStore();
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const paginateRef = ref(null);
const agencyModesOptions = ref([]);
const visibleColumns = ref([]);
const allColumnNames = ref([]);
const arrayData = useArrayData('WeeklyTickets');
const { store } = arrayData;
const weekdays = [
{ id: 0, name: t('weekdays.mon') },
{ id: 1, name: t('weekdays.tue') },
{ id: 2, name: t('weekdays.wed') },
{ id: 3, name: t('weekdays.thu') },
{ id: 4, name: t('weekdays.fri') },
{ id: 5, name: t('weekdays.sat') },
{ id: 6, name: t('weekdays.sun') },
];
const exprBuilder = (param, value) => {
switch (param) {
case 'clientName':
return { 'c.name': value };
case 'nickName':
return { 'u.name': value };
}
};
const params = reactive({});
const applyColumnFilter = async (col) => {
try {
const paramKey = col.columnFilter?.filterParamKey || col.field;
params[paramKey] = col.columnFilter.filterValue;
await paginateRef.value.addFilter(null, params);
} catch (err) {
console.error('Error applying column filter', err);
}
};
const getInputEvents = (col) => ({ 'keyup.enter': () => applyColumnFilter(col) });
const columns = computed(() => [
{
label: t('weeklyTickets.id'),
name: 'id',
field: 'ticketFk',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('weeklyTickets.client'),
name: 'client',
field: 'clientName',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('weeklyTickets.shipment'),
name: 'shipment',
field: 'weekDay',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('weeklyTickets.agency'),
name: 'agency',
field: 'agencyModeFk',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('weeklyTickets.warehouse'),
name: 'warehouse',
field: 'warehouseName',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('weeklyTickets.salesperson'),
field: 'salesperson',
name: 'salesperson',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
filterParamKey: 'nickName',
attrs: {
dense: true,
},
},
},
{
label: '',
name: 'actions',
align: 'left',
columnFilter: null,
},
]);
const redirectToTicketSummary = (ticketFk) =>
router.push({ name: 'TicketSummary', params: { id: ticketFk } });
const deleteWeekly = async (ticketFk) => {
try {
await axios.delete(`TicketWeeklies/${ticketFk}`);
notify(t('globals.dataSaved'), 'positive');
const ticketIndex = store.data.findIndex((e) => e.ticketFk == ticketFk);
store.data.splice(ticketIndex, 1);
} catch (err) {
console.error('Error deleting weekly', err);
}
};
const onUpdate = async (ticketFk, field, value) => {
try {
const params = { ticketFk, [field]: value };
await axios.patch('TicketWeeklies', params);
} catch (err) {
console.error('Error updating weekly', err);
}
};
onMounted(async () => {
stateStore.rightDrawer = true;
const filteredColumns = columns.value.filter((col) => col.name !== 'actions');
allColumnNames.value = filteredColumns.map((col) => col.name);
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
url="AgencyModes/isActive"
:filter="{ fields: ['id', 'name'], order: 'name' }"
auto-load
@on-fetch="(data) => (agencyModesOptions = data)"
/>
<VnSearchbar
data-key="WeeklyTickets"
:label="t('weeklyTickets.search')"
:info="t('weeklyTickets.searchInfo')"
/>
<VnSubToolbar>
<template #st-data>
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="itemsIndex"
labels-traductions-path="weeklyTickets"
@on-config-saved="visibleColumns = [...$event, 'actions']"
/>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<VnPaginate
ref="paginateRef"
data-key="WeeklyTickets"
url="TicketWeeklies/filter"
:order="['weekDay', 'ticketFk']"
:limit="20"
:expr-builder="exprBuilder"
:user-params="params"
:offset="50"
auto-load
>
<template #body="{ rows }">
<QTable
:rows="rows"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:visible-columns="visibleColumns"
:no-data-label="t('globals.noResults')"
@row-click="(_, row) => redirectToTicketSummary(row.ticketFk)"
>
<template #top-row="{ cols }">
<QTr>
<QTd
v-for="(col, index) in cols"
:key="index"
style="max-width: 100px"
>
<component
:is="col.columnFilter.component"
v-if="col.columnFilter"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #body-cell-id="{ row }">
<QTd @click.stop>
<QBtn flat color="primary">
{{ row.ticketFk }}
<TicketDescriptorProxy :id="row.ticketFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-salesperson="{ row }">
<QTd @click.stop>
<QBtn flat color="primary">
{{ row.userName }}
<WorkerDescriptorProxy :id="row.workerFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-client="{ row }">
<QTd @click.stop>
<QBtn flat color="primary" dense>
{{ row.clientName }}
<CustomerDescriptorProxy :id="row.clientFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-shipment="{ row }">
<QTd @click.stop>
<VnSelect
:options="weekdays"
hide-selected
option-label="name"
option-value="id"
v-model="row.weekDay"
@update:model-value="
onUpdate(row.ticketFk, 'weekDay', $event)
"
/>
</QTd>
</template>
<template #body-cell-agency="{ row }">
<QTd @click.stop>
<VnSelect
:options="agencyModesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="row.agencyModeFk"
@update:model-value="
onUpdate(row.ticketFk, 'agencyModeFk', $event)
"
/>
</QTd>
</template>
<template #body-cell-actions="{ row }">
<QTd>
<QIcon
@click.stop="
openConfirmationModal(
t('You are going to delete this weekly ticket'),
t(
'This ticket will be removed from weekly tickets! Continue anyway?'
),
() => deleteWeekly(row.ticketFk)
)
"
class="q-ml-sm cursor-pointer"
color="primary"
name="delete"
size="sm"
>
<QTooltip>
{{ t('globals.delete') }}
</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
</template>
</VnPaginate>
</QPage>
</template>
<i18n>
es:
You are going to delete this weekly ticket: Vas a eliminar este ticket programado
This ticket will be removed from weekly tickets! Continue anyway?: Este ticket se eliminará de tickets programados! ¿Continuar de todas formas?
</i18n>

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