0
0
Fork 0

Merge branch 'dev' into 6321_negative_tickets

This commit is contained in:
Javier Segarra 2024-07-02 12:20:58 +02:00
commit c3d97231d2
175 changed files with 10386 additions and 2605 deletions

View File

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

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,11 +83,15 @@ 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(
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}` () => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`
); ).value;
const componentIsRendered = ref(false); const componentIsRendered = ref(false);
const arrayData = useArrayData(modelValue); const arrayData = useArrayData(modelValue);
const isLoading = ref(false); const isLoading = ref(false);
@ -119,9 +123,10 @@ onMounted(async () => {
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
state.set(modelValue, $props.formInitialData); state.set(modelValue, $props.formInitialData);
if ($props.autoLoad && !$props.formInitialData && $props.url) await fetch(); if (!$props.formInitialData) {
else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data); if ($props.autoLoad && $props.url) await fetch();
else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data);
}
if ($props.observeFormChanges) { if ($props.observeFormChanges) {
watch( watch(
() => formData.value, () => formData.value,
@ -200,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');
@ -245,12 +251,18 @@ function updateAndEmit(evt, val, res) {
emit(evt, state.get(modelValue), res); emit(evt, state.get(modelValue), res);
} }
defineExpose({ save, isLoading, hasChanges }); defineExpose({
save,
isLoading,
hasChanges,
reset,
fetch,
});
</script> </script>
<template> <template>
<div class="column items-center full-width"> <div class="column items-center full-width">
<QForm <QForm
v-if="formData"
@submit="save" @submit="save"
@reset="reset" @reset="reset"
class="q-pa-md" class="q-pa-md"
@ -258,11 +270,13 @@ defineExpose({ save, isLoading, hasChanges });
> >
<QCard> <QCard>
<slot <slot
v-if="formData"
name="form" name="form"
:data="formData" :data="formData"
:validate="validate" :validate="validate"
:filter="filter" :filter="filter"
/> />
<SkeletonForm v-else/>
</QCard> </QCard>
</QForm> </QForm>
</div> </div>
@ -325,7 +339,7 @@ defineExpose({ save, isLoading, hasChanges });
</QBtnGroup> </QBtnGroup>
</div> </div>
</Teleport> </Teleport>
<SkeletonForm v-if="!formData" />
<QInnerLoading <QInnerLoading
:showing="isLoading" :showing="isLoading"
:label="t('globals.pleaseWait')" :label="t('globals.pleaseWait')"

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,65 @@ 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) {
return;
}
}
const { data } = await axios.post('InvoiceOuts/transferInvoice', params);
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 +85,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 +117,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 +132,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 +190,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 +215,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();
@ -78,13 +87,12 @@ const inputRules = [
<template v-if="$slots.prepend" #prepend> <template v-if="$slots.prepend" #prepend>
<slot name="prepend" /> <slot name="prepend" />
</template> </template>
<template #append> <template #append>
<slot name="append" v-if="$slots.append && !$attrs.disabled" /> <slot name="append" v-if="$slots.append && !$attrs.disabled" />
<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

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, onUnmounted } 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';
@ -376,6 +376,10 @@ async function clearFilter() {
} }
setLogTree(); setLogTree();
onUnmounted(() => {
stateStore.rightDrawer = false;
});
</script> </script>
<template> <template>
<FetchData <FetchData

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,
@ -62,6 +61,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();
@ -120,7 +123,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) {
@ -150,6 +155,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>
@ -182,6 +191,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()
@ -115,13 +116,13 @@ const emit = defineEmits(['onFetch']);
</QBtn> </QBtn>
</RouterLink> </RouterLink>
<QBtn <QBtn
v-if="$slots.menu"
color="white" color="white"
dense dense
flat flat
icon="more_vert" icon="more_vert"
round round
size="md" size="md"
:class="{ invisible: !$slots.menu }"
> >
<QTooltip> <QTooltip>
{{ t('components.cardDescriptor.moreOptions') }} {{ t('components.cardDescriptor.moreOptions') }}

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 },
}" }"
> >
@ -159,9 +163,9 @@ function existSummary(routes) {
margin-top: 2px; margin-top: 2px;
.label { .label {
color: var(--vn-label-color); color: var(--vn-label-color);
width: 8em; width: 9em;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: wrap;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-right: 10px; margin-right: 10px;
flex-grow: 0; flex-grow: 0;
@ -183,15 +187,10 @@ function existSummary(routes) {
color: lighten($primary, 20%); color: lighten($primary, 20%);
} }
.q-checkbox { .q-checkbox {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label { & .q-checkbox__label {
margin-left: 31px;
color: var(--vn-text-color); color: var(--vn-text-color);
} }
& .q-checkbox__inner { & .q-checkbox__inner {
position: absolute;
left: 0;
color: var(--vn-label-color); color: var(--vn-label-color);
} }
} }

View File

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

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,14 @@ 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 $props = defineProps({
modelValue: {
type: Object,
default: () => {}
},
dataKey: { dataKey: {
type: String, type: String,
required: true, required: true,
@ -18,11 +21,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 +38,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,50 +60,55 @@ 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 userParams = ref({})
const { navigate } = useRedirect();
onMounted(() => { onMounted(() => {
if (props.params) userParams.value = JSON.parse(JSON.stringify(props.params)); userParams.value = $props.modelValue ?? {}
if (Object.keys(store.userParams).length > 0) {
userParams.value = JSON.parse(JSON.stringify(store.userParams));
}
emit('init', { params: userParams.value }); 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;
userParams.value = { ...userParams.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 = { ...userParams.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: userParams.value });
userParams.value = newParams; userParams.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() {
@ -105,10 +116,9 @@ async function reload() {
const params = Object.values(userParams.value).filter((param) => param); const params = Object.values(userParams.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() {
@ -118,17 +128,18 @@ async function clearFilters() {
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(userParams.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] = userParams.value[key];
} }
userParams.value = {};
userParams.value = { ...newParams }; // Actualizar los params con los removibles userParams.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: userParams.value }); await arrayData.applyFilter({ params: userParams.value });
if (!props.showAll) { if (!$props.showAll) {
store.data = []; store.data = [];
} }
@ -136,36 +147,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(userParams.value)) {
.map(([key, value]) => ({ const value = userParams.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; userParams.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,7 +233,7 @@ 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>
@ -245,7 +252,7 @@ function formatValue(value) {
<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="userParams" :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 +262,7 @@ function formatValue(value) {
dense dense
icon="search" icon="search"
rounded rounded
type="submit" :type="disableSubmitEvent ? 'button' : 'submit'"
unelevated unelevated
/> />
</QItemSection> </QItemSection>
@ -269,7 +276,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

@ -2,6 +2,7 @@
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useClipboard } from 'src/composables/useClipboard'; import { useClipboard } from 'src/composables/useClipboard';
import { computed } from 'vue';
const $props = defineProps({ const $props = defineProps({
label: { type: String, default: null }, label: { type: String, default: null },
@ -24,53 +25,68 @@ function copyValueText() {
}, },
}); });
} }
const val = computed(() => $props.value);
</script> </script>
<style scoped>
.label,
.value {
white-space: pre-line;
word-wrap: break-word;
}
</style>
<template> <template>
<div class="vn-label-value"> <div class="vn-label-value">
<div v-if="$props.label || $slots.label" class="label"> <QCheckbox
<slot name="label"> v-if="typeof value === 'boolean'"
<span>{{ $props.label }}</span> v-model="val"
</slot> :label="label"
</div> disable
<div class="value"> dense
<slot name="value"> />
<span :title="$props.value"> <template v-else>
{{ $props.dash ? dashIfEmpty($props.value) : $props.value }} <div v-if="label || $slots.label" class="label">
</span> <slot name="label">
</slot> <span>{{ label }}</span>
</div> </slot>
<div class="info" v-if="$props.info"> </div>
<QIcon name="info" class="cursor-pointer" size="xs" color="grey"> <div class="value">
<QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]"> <slot name="value">
{{ $props.info }} <span :title="value">
</QTooltip> {{ dash ? dashIfEmpty(value) : value }}
</QIcon> </span>
</div> </slot>
<div class="copy" v-if="$props.copy && $props.value" @click="copyValueText()"> </div>
<QIcon name="Content_Copy" color="primary"> <div class="info" v-if="info">
<QTooltip>{{ t('globals.copyClipboard') }}</QTooltip> <QIcon name="info" class="cursor-pointer" size="xs" color="grey">
</QIcon> <QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]">
</div> {{ info }}
</QTooltip>
</QIcon>
</div>
<div class="copy" v-if="copy && value" @click="copyValueText()">
<QIcon name="Content_Copy" color="primary">
<QTooltip>{{ t('globals.copyClipboard') }}</QTooltip>
</QIcon>
</div>
</template>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.vn-label-value:hover .copy { .vn-label-value {
visibility: visible; &:hover .copy {
cursor: pointer; visibility: visible;
cursor: pointer;
}
.label,
.value {
white-space: pre-line;
word-wrap: break-word;
}
.copy {
visibility: hidden;
}
.info {
margin-left: 5px;
}
} }
.copy {
visibility: hidden; :deep(.q-checkbox.disabled) {
} opacity: 1 !important;
.info {
margin-left: 5px;
} }
.image { .image {

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

@ -62,14 +62,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,
@ -85,12 +90,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();
store.data = props.data; mounted.value = true;
}); });
watch( watch(
@ -100,11 +106,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 });
@ -112,6 +129,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() {
@ -143,7 +161,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);
@ -155,7 +173,7 @@ async function onLoad(index, done) {
done(isDone); done(isDone);
} }
defineExpose({ fetch, addFilter }); defineExpose({ fetch, addFilter, paginate });
</script> </script>
<template> <template>
@ -204,12 +222,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;
} }
@ -145,6 +152,15 @@ select:-webkit-autofill {
color: var(--vn-label-color); color: var(--vn-label-color);
} }
.disabled {
& .q-checkbox__label {
color: var(--vn-text-color);
}
& .q-checkbox__inner {
color: var(--vn-label-color);
}
}
.q-chip, .q-chip,
.q-notification__message, .q-notification__message,
.q-notification__icon { .q-notification__icon {
@ -194,3 +210,26 @@ input::-webkit-inner-spin-button {
filter: brightness(1.1); filter: brightness(1.1);
mix-blend-mode: multiply; mix-blend-mode: multiply;
} }
/* ===== 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

@ -118,6 +118,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
@ -284,8 +285,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
@ -401,6 +402,7 @@ entry:
type: Type type: Type
color: Color color: Color
id: ID id: ID
printedStickers: Printed stickers
notes: notes:
observationType: Observation type observationType: Observation type
descriptor: descriptor:
@ -425,6 +427,7 @@ entry:
buyingValue: Buying value buyingValue: Buying value
freightValue: Freight value freightValue: Freight value
comissionValue: Commission value comissionValue: Commission value
description: Description
packageValue: Package value packageValue: Package value
isIgnored: Is ignored isIgnored: Is ignored
price2: Grouping price2: Grouping
@ -447,6 +450,14 @@ ticket:
notes: Notes notes: Notes
sale: Sale sale: Sale
negative: Tickets negative negative: Tickets negative
volume: Volume
observation: Notes
ticketAdvance: Advance tickets
futureTickets: Future tickets
purchaseRequest: Purchase request
weeklyTickets: Weekly tickets
services: Service
tracking: Tracking
list: list:
nickname: Nickname nickname: Nickname
state: State state: State
@ -848,7 +859,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
@ -918,7 +930,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
@ -996,6 +1025,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

@ -113,12 +113,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
@ -283,7 +285,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
@ -400,6 +402,7 @@ entry:
type: Tipo type: Tipo
color: Color color: Color
id: ID id: ID
printedStickers: Etiquetas impresas
notes: notes:
observationType: Tipo de observación observationType: Tipo de observación
descriptor: descriptor:
@ -424,6 +427,7 @@ entry:
buyingValue: Coste buyingValue: Coste
freightValue: Porte freightValue: Porte
comissionValue: Comisión comissionValue: Comisión
description: Descripción
packageValue: Embalaje packageValue: Embalaje
isIgnored: Ignorado isIgnored: Ignorado
price2: Grouping price2: Grouping
@ -446,6 +450,14 @@ ticket:
notes: Notas notes: Notas
sale: Lineas del pedido sale: Lineas del pedido
negative: Tickets negativos negative: Tickets negativos
volume: Volumen
observation: Notas
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
purchaseRequest: Petición de compra
weeklyTickets: Tickets programados
services: Servicios
tracking: Estados
list: list:
nickname: Alias nickname: Alias
state: Estado state: Estado
@ -844,6 +856,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
@ -904,7 +918,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
@ -982,6 +1013,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

@ -0,0 +1,81 @@
<script setup>
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
const router = useRouter();
const newAccountForm = reactive({
active: true,
});
const rolesOptions = ref([]);
const redirectToAccountBasicData = (_, { id }) => {
router.push({ name: 'AccountBasicData', params: { id } });
};
</script>
<template>
<FetchData
url="VnRoles"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@on-fetch="(data) => (rolesOptions = data)"
auto-load
/>
<FormModelPopup
:title="t('account.card.newUser')"
url-create="VnUsers"
model="users"
:form-initial-data="newAccountForm"
@on-data-saved="redirectToAccountBasicData"
>
<template #form-inputs="{ data, validate }">
<div class="column q-gutter-sm">
<VnInput
v-model="data.name"
:label="t('account.create.name')"
:rules="validate('VnUser.name')"
/>
<VnInput
v-model="data.nickname"
:label="t('account.create.nickname')"
:rules="validate('VnUser.nickname')"
/>
<VnInput
v-model="data.email"
:label="t('account.create.email')"
type="email"
:rules="validate('VnUser.email')"
/>
<VnSelect
:label="t('account.create.role')"
v-model="data.roleFk"
:options="rolesOptions"
option-value="id"
option-label="name"
map-options
hide-selected
:rules="validate('VnUser.roleFk')"
/>
<VnInput
v-model="data.password"
:label="t('account.create.password')"
type="password"
:rules="validate('VnUser.password')"
/>
<QCheckbox
:label="t('account.create.active')"
v-model="data.active"
:toggle-indeterminate="false"
:rules="validate('VnUser.active')"
/>
</div>
</template>
</FormModelPopup>
</template>

View File

@ -0,0 +1,87 @@
<script setup>
import { ref } 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';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
exprBuilder: {
type: Function,
default: null,
},
});
const rolesOptions = ref([]);
</script>
<template>
<FetchData
url="VnRoles"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@on-fetch="(data) => (rolesOptions = data)"
auto-load
/>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:hidden-tags="['search']"
:redirect="false"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`account.card.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QItem class="q-my-sm">
<QItemSection>
<VnInput
:label="t('account.card.name')"
v-model="params.name"
lazy-rules
is-outlined
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
:label="t('account.card.alias')"
v-model="params.nickname"
lazy-rules
is-outlined
/>
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection>
<VnSelect
:label="t('account.card.role')"
v-model="params.roleFk"
@update:model-value="searchFn()"
:options="rolesOptions"
option-value="id"
option-label="name"
emit-value
map-options
use-input
hide-selected
dense
outlined
rounded
:input-debounce="0"
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>

View File

@ -1 +1,144 @@
<template>Account list</template> <script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { computed, ref } from 'vue';
import VnPaginate from 'src/components/ui/VnPaginate.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 AccountSummary from './Card/AccountSummary.vue';
import AccountFilter from './AccountFilter.vue';
import AccountCreate from './AccountCreate.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useStateStore } from 'stores/useStateStore';
import { useRole } from 'src/composables/useRole';
import { QDialog } from 'quasar';
const stateStore = useStateStore();
const router = useRouter();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const accountCreateDialogRef = ref(null);
const showNewUserBtn = computed(() => useRole().hasAny(['itManagement']));
const filter = {
fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } },
};
const exprBuilder = (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? { id: value }
: {
or: [
{ name: { like: `%${value}%` } },
{ nickname: { like: `%${value}%` } },
],
};
case 'name':
case 'nickname':
return { [param]: { like: `%${value}%` } };
case 'roleFk':
return { [param]: value };
}
};
const getApiUrl = () => new URL(window.location).origin;
const navigate = (event, id) => {
if (event.ctrlKey || event.metaKey)
return window.open(`${getApiUrl()}/#/account/${id}/summary`);
router.push({ path: `/account/${id}` });
};
const openCreateModal = () => accountCreateDialogRef.value.show();
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="AccountList"
url="VnUsers/preview"
:expr-builder="exprBuilder"
:label="t('account.search')"
:info="t('account.searchInfo')"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<AccountFilter data-key="AccountList" :expr-builder="exprBuilder" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
:filter="filter"
data-key="AccountList"
url="VnUsers/preview"
auto-load
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:id="row.id"
:key="row.id"
:title="row.nickname"
@click="navigate($event, row.id)"
>
<template #list-items>
<VnLv :label="t('account.card.name')" :value="row.nickname">
</VnLv>
<VnLv
:label="t('account.card.nickname')"
:value="row.username"
>
</VnLv>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, AccountSummary)"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
<QDialog
ref="accountCreateDialogRef"
transition-hide="scale"
transition-show="scale"
>
<AccountCreate />
</QDialog>
<QPageSticky :offset="[20, 20]" v-if="showNewUserBtn">
<QBtn @click="openCreateModal" color="primary" fab icon="add" />
<QTooltip class="text-no-wrap">
{{ t('account.card.newUser') }}
</QTooltip>
</QPageSticky>
</QPage>
</template>

View File

@ -0,0 +1,47 @@
<script setup>
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnSelect from 'src/components/common/VnSelect.vue';
import FormModel from 'components/FormModel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { ref, watch } from 'vue';
const route = useRoute();
const { t } = useI18n();
const formModelRef = ref(null);
const accountFilter = {
where: { id: route.params.id },
fields: ['id', 'email', 'nickname', 'name', 'accountStateFk', 'packages', 'pickup'],
include: [],
};
watch(
() => route.params.id,
() => formModelRef.value.reset()
);
</script>
<template>
<FormModel
ref="formModelRef"
:url="`VnUsers/preview`"
:url-update="`VnUsers/${route.params.id}/update-user`"
:filter="accountFilter"
model="Accounts"
auto-load
@on-data-saved="formModelRef.fetch()"
>
<template #form="{ data }">
<div class="q-gutter-y-sm">
<VnInput v-model="data.name" :label="t('account.card.nickname')" />
<VnInput v-model="data.nickname" :label="t('account.card.alias')" />
<VnInput v-model="data.email" :label="t('account.card.email')" />
<VnSelect
v-model="data.lang"
:options="['es', 'en']"
:label="t('account.card.lang')"
/>
</div>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,34 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnCard from 'components/common/VnCard.vue';
import AccountDescriptor from './AccountDescriptor.vue';
const { t } = useI18n();
const route = useRoute();
const routeName = computed(() => route.name);
const customRouteRedirectName = computed(() => routeName.value);
const searchBarDataKeys = {
AccountSummary: 'AccountSummary',
AccountInheritedRoles: 'AccountInheritedRoles',
AccountMailForwarding: 'AccountMailForwarding',
AccountMailAlias: 'AccountMailAlias',
AccountPrivileges: 'AccountPrivileges',
AccountLog: 'AccountLog',
};
</script>
<template>
<VnCard
data-key="Account"
:descriptor="AccountDescriptor"
:search-data-key="searchBarDataKeys[routeName]"
:search-custom-route-redirect="customRouteRedirectName"
:search-redirect="!!customRouteRedirectName"
:searchbar-label="t('account.search')"
:searchbar-info="t('account.searchInfo')"
/>
</template>

View File

@ -0,0 +1,130 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import AccountDescriptorMenu from './AccountDescriptorMenu.vue';
import FetchData from 'src/components/FetchData.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const route = useRoute();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.nickname, entity.id));
const filter = {
where: { id: entityId },
fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } },
};
const hasAccount = ref(false);
</script>
<template>
<FetchData
:url="`Accounts/${entityId}/exists`"
auto-load
@on-fetch="(data) => (hasAccount = data.exists)"
/>
<CardDescriptor
ref="descriptor"
:url="`VnUsers/preview`"
:filter="filter"
module="Account"
@on-fetch="setData"
data-key="AccountId"
:title="data.title"
:subtitle="data.subtitle"
>
<template #header-extra-action>
<QBtn
round
flat
size="md"
color="white"
icon="face"
:to="{ name: 'AccountList' }"
>
<QTooltip>
{{ t('Go to module index') }}
</QTooltip>
</QBtn>
</template>
<template #menu>
<AccountDescriptorMenu :has-account="hasAccount" />
</template>
<template #before>
<!-- falla id :id="entityId.value" collection="user" size="160x160" -->
<VnImg :id="entityId" collection="user" size="160x160" class="photo">
<template #error>
<div
class="absolute-full picture text-center q-pa-md flex flex-center"
>
<div>
<div class="text-grey-5" style="opacity: 0.4; font-size: 5vh">
<QIcon name="vn:claims" />
</div>
<div class="text-grey-5" style="opacity: 0.4">
{{ t('account.imageNotFound') }}
</div>
</div>
</div>
</template>
</VnImg>
</template>
<template #body="{ entity }">
<VnLv :label="t('account.card.nickname')" :value="entity.nickname" />
<VnLv :label="t('account.card.role')" :value="entity.role.name" />
</template>
<template #actions="{ entity }">
<QCardActions class="q-gutter-x-md">
<QIcon
v-if="!entity.active"
color="primary"
name="vn:disabled"
flat
round
size="sm"
class="fill-icon"
>
<QTooltip>{{ t('account.card.deactivated') }}</QTooltip>
</QIcon>
<QIcon
color="primary"
name="contact_mail"
v-if="entity.hasAccount"
flat
round
size="sm"
class="fill-icon"
>
<QTooltip>{{ t('account.card.enabled') }}</QTooltip>
</QIcon>
</QCardActions>
</template>
</CardDescriptor>
</template>
<style scoped>
.q-item__label {
margin-top: 0;
}
</style>
<i18n>
en:
accountRate: Claming rate
es:
accountRate: Ratio de reclamación
</i18n>

View File

@ -0,0 +1,187 @@
<script setup>
import axios from 'axios';
import { computed, ref, toRefs } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
const quasar = useQuasar();
const $props = defineProps({
hasAccount: {
type: Boolean,
default: false,
required: true,
},
});
const { t } = useI18n();
const { hasAccount } = toRefs($props);
const { openConfirmationModal } = useVnConfirm();
const route = useRoute();
const account = computed(() => useArrayData('AccountId').store.data[0]);
account.value.hasAccount = hasAccount.value;
const entityId = computed(() => +route.params.id);
async function updateStatusAccount(active) {
if (active) {
await axios.post(`Accounts`, { id: entityId.value });
} else {
await axios.delete(`Accounts/${entityId.value}`);
}
account.value.hasAccount = active;
const status = active ? 'enable' : 'disable';
quasar.notify({
message: t(`account.card.${status}Account.success`),
type: 'positive',
});
}
async function updateStatusUser(active) {
await axios.patch(`VnUsers/${entityId.value}`, { active });
account.value.active = active;
const status = active ? 'activate' : 'deactivate';
quasar.notify({
message: t(`account.card.actions.${status}User.success`),
type: 'positive',
});
}
function setPassword() {
quasar.dialog({
component: CustomerChangePassword,
componentProps: {
id: entityId.value,
},
});
}
const showSyncDialog = ref(false);
const syncPassword = ref(null);
const shouldSyncPassword = ref(false);
async function sync() {
const params = { force: true };
if (shouldSyncPassword.value) params.password = syncPassword.value;
await axios.patch(`Accounts/${account.value.name}/sync`, {
params,
});
quasar.notify({
message: t('account.card.actions.sync.success'),
type: 'positive',
});
}
</script>
<template>
<VnConfirm
v-model="showSyncDialog"
:message="t('account.card.actions.sync.message')"
:title="t('account.card.actions.sync.title')"
:promise="sync"
>
<template #customHTML>
{{ shouldSyncPassword }}
<QCheckbox
:label="t('account.card.actions.sync.checkbox')"
v-model="shouldSyncPassword"
class="full-width"
clearable
clear-icon="close"
>
<QIcon style="padding-left: 10px" color="primary" name="info" size="sm">
<QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip>
</QIcon></QCheckbox
>
<QInput
v-if="shouldSyncPassword"
:label="t('login.password')"
v-model="syncPassword"
class="full-width"
clearable
clear-icon="close"
type="password"
/>
</template>
</VnConfirm>
<QItem v-ripple clickable @click="setPassword">
<QItemSection>{{ t('account.card.actions.setPassword') }}</QItemSection>
</QItem>
<QItem
v-if="!account.hasAccount"
v-ripple
clickable
@click="
openConfirmationModal(
t('account.card.actions.enableAccount.title'),
t('account.card.actions.enableAccount.subtitle'),
() => updateStatusAccount(true)
)
"
>
<QItemSection>{{ t('account.card.actions.enableAccount.name') }}</QItemSection>
</QItem>
<QItem
v-if="account.hasAccount"
v-ripple
clickable
@click="
openConfirmationModal(
t('account.card.actions.disableAccount.title'),
t('account.card.actions.disableAccount.subtitle'),
() => updateStatusAccount(false)
)
"
>
<QItemSection>{{ t('account.card.actions.disableAccount.name') }}</QItemSection>
</QItem>
<QItem
v-if="!account.active"
v-ripple
clickable
@click="
openConfirmationModal(
t('account.card.actions.activateUser.title'),
t('account.card.actions.activateUser.title'),
() => updateStatusUser(true)
)
"
>
<QItemSection>{{ t('account.card.actions.activateUser.name') }}</QItemSection>
</QItem>
<QItem
v-if="account.active"
v-ripple
clickable
@click="
openConfirmationModal(
t('account.card.actions.deactivateUser.title'),
t('account.card.actions.deactivateUser.title'),
() => updateStatusUser(false)
)
"
>
<QItemSection>{{ t('account.card.actions.deactivateUser.name') }}</QItemSection>
</QItem>
<QItem v-ripple clickable @click="showSyncDialog = true">
<QItemSection>{{ t('account.card.actions.sync.name') }}</QItemSection>
</QItem>
<QSeparator />
<QItem
@click="
openConfirmationModal(
t('account.card.actions.delete.title'),
t('account.card.actions.delete.subTitle'),
removeAccount
)
"
v-ripple
clickable
>
<QItemSection avatar>
<QIcon name="delete" />
</QItemSection>
<QItemSection>{{ t('account.card.actions.delete.name') }}</QItemSection>
</QItem>
</template>

View File

@ -0,0 +1,7 @@
<script setup>
import InheritedRoles from '../InheritedRoles.vue';
</script>
<template>
<InheritedRoles data-key="AccountInheritedRoles" />
</template>

View File

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

View File

@ -0,0 +1,187 @@
<script setup>
import { computed, ref, watch, onMounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnPaginate from 'components/ui/VnPaginate.vue';
import AccountMailAliasCreateForm from './AccountMailAliasCreateForm.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const { t } = useI18n();
const route = useRoute();
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const paginateRef = ref(null);
const createMailAliasDialogRef = ref(null);
const arrayData = useArrayData('AccountMailAliases');
const store = arrayData.store;
const loading = ref(false);
const hasAccount = ref(false);
const data = computed(() => {
const dataCopy = store.data;
return dataCopy.sort((a, b) => a.alias?.alias.localeCompare(b.alias?.alias));
});
const filter = computed(() => ({
where: { account: route.params.id },
include: {
relation: 'alias',
scope: {
fields: ['id', 'alias', 'description'],
},
},
}));
const urlPath = 'MailAliasAccounts';
const columns = computed(() => [
{
name: 'name',
},
{
name: 'action',
},
]);
const fetchAccountExistence = async () => {
try {
const { data } = await axios.get(`Accounts/${route.params.id}/exists`);
return data.exists;
} catch (error) {
console.error('Error fetching account existence', error);
return false;
}
};
const deleteMailAlias = async (row) => {
try {
await axios.delete(`${urlPath}/${row.id}`);
fetchMailAliases();
notify(t('Unsubscribed from alias!'), 'positive');
} catch (error) {
console.error(error);
}
};
const createMailAlias = async (mailAliasFormData) => {
try {
await axios.post(urlPath, mailAliasFormData);
notify(t('Subscribed to alias!'), 'positive');
fetchMailAliases();
} catch (error) {
console.error(error);
}
};
const fetchMailAliases = async () => {
await nextTick();
paginateRef.value.fetch();
};
const getAccountData = async () => {
loading.value = true;
hasAccount.value = await fetchAccountExistence();
if (!hasAccount.value) {
loading.value = false;
store.data = [];
return;
}
await fetchMailAliases();
loading.value = false;
};
const openCreateMailAliasForm = () => createMailAliasDialogRef.value.show();
watch(
() => route.params.id,
() => {
store.url = urlPath;
store.filter = filter.value;
getAccountData();
}
);
onMounted(async () => await getAccountData());
</script>
<template>
<QPage class="column items-center q-pa-md">
<div class="full-width" style="max-width: 400px">
<QSpinner v-if="loading" color="primary" size="md" />
<VnPaginate
ref="paginateRef"
data-key="AccountMailAliases"
:filter="filter"
:url="urlPath"
auto-load
>
<template #body="{ rows }">
<QTable
v-if="hasAccount && !loading"
:rows="data"
:columns="columns"
hide-header
>
<template #body="{ row, rowIndex }">
<QTr>
<QTd>
<div class="column">
<span>{{ row.alias?.alias }}</span>
<span class="color-vn-label">{{
row.alias?.description
}}</span>
</div>
</QTd>
<QTd style="width: 50px !important">
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click.stop.prevent="
openConfirmationModal(
t('User will be removed from alias'),
t('¿Seguro que quieres continuar?'),
() => deleteMailAlias(row, rows, rowIndex)
)
"
>
<QTooltip>
{{ t('globals.delete') }}
</QTooltip>
</QIcon>
</QTd>
</QTr>
</template>
</QTable>
</template>
</VnPaginate>
<h5 v-if="!hasAccount" class="text-center">
{{ t('account.mailForwarding.accountNotEnabled') }}
</h5>
</div>
<QDialog ref="createMailAliasDialogRef">
<AccountMailAliasCreateForm @on-submit-create-alias="createMailAlias" />
</QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="openCreateMailAliasForm()">
<QTooltip>{{ t('warehouses.add') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>
<i18n>
es:
Unsubscribed from alias!: ¡Desuscrito del alias!
Subscribed to alias!: ¡Suscrito al alias!
User will be removed from alias: El usuario será borrado del alias
Are you sure you want to continue?: ¿Seguro que quieres continuar?
</i18n>

View File

@ -0,0 +1,51 @@
<script setup>
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import FormPopup from 'components/FormPopup.vue';
const emit = defineEmits(['onSubmitCreateAlias']);
const { t } = useI18n();
const route = useRoute();
const aliasFormData = reactive({
mailAlias: null,
account: route.params.id,
});
const aliasOptions = ref([]);
</script>
<template>
<FetchData
url="MailAliases"
:filter="{ fields: ['id', 'alias'], order: 'alias ASC' }"
auto-load
@on-fetch="(data) => (aliasOptions = data)"
/>
<FormPopup
model="ZoneWarehouse"
@on-submit="emit('onSubmitCreateAlias', aliasFormData)"
>
<template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelect
:label="t('account.card.alias')"
v-model="aliasFormData.mailAlias"
:options="aliasOptions"
option-value="id"
option-label="alias"
hide-selected
:required="true"
/>
</div>
</VnRow>
</template>
</FormPopup>
</template>

View File

@ -0,0 +1,159 @@
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import VnRow from 'components/ui/VnRow.vue';
import axios from 'axios';
import { useStateStore } from 'stores/useStateStore';
import useNotify from 'src/composables/useNotify.js';
const { t } = useI18n();
const route = useRoute();
const stateStore = useStateStore();
const { notify } = useNotify();
const initialData = ref({});
const formData = ref({
forwardTo: null,
account: null,
});
const hasAccount = ref(false);
const hasData = ref(false);
const loading = ref(false);
const hasDataChanged = computed(
() =>
formData.value.forwardTo !== initialData.value.forwardTo ||
initialData.value.hasData !== hasData.value
);
const fetchAccountExistence = async () => {
try {
const { data } = await axios.get(`Accounts/${route.params.id}/exists`);
return data.exists;
} catch (error) {
console.error('Error fetching account existence', error);
return false;
}
};
const fetchMailForwards = async () => {
try {
const response = await axios.get(`MailForwards/${route.params.id}`);
return response.data;
} catch (err) {
console.error('Error fetching mail forwards', err);
return null;
}
};
const deleteMailForward = async () => {
try {
await axios.delete(`MailForwards/${route.params.id}`);
formData.value.forwardTo = null;
initialData.value.forwardTo = null;
initialData.value.hasData = hasData.value;
notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error deleting mail forward', err);
}
};
const updateMailForward = async () => {
try {
await axios.patch('MailForwards', formData.value);
initialData.value = { ...formData.value };
initialData.value.hasData = hasData.value;
} catch (err) {
console.error('Error creating mail forward', err);
}
};
const onSubmit = async () => {
if (hasData.value) await updateMailForward();
else await deleteMailForward();
};
const setInitialData = async () => {
loading.value = true;
initialData.value.account = route.params.id;
formData.value.account = route.params.id;
hasAccount.value = await fetchAccountExistence(route.params.id);
if (!hasAccount.value) {
loading.value = false;
return;
}
const result = await fetchMailForwards(route.params.id);
const forwardTo = result ? result.forwardTo : null;
formData.value.forwardTo = forwardTo;
initialData.value.forwardTo = forwardTo;
initialData.value.hasData = hasAccount.value && !!forwardTo;
hasData.value = hasAccount.value && !!forwardTo;
loading.value = false;
};
watch(
() => route.params.id,
() => setInitialData()
);
onMounted(async () => await setInitialData());
</script>
<template>
<div class="flex justify-center">
<QSpinner v-if="loading" color="primary" size="md" />
<QForm
v-else-if="hasAccount"
@submit="onSubmit()"
class="full-width"
style="max-width: 800px"
>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<div>
<QBtnGroup push class="q-gutter-x-sm">
<slot name="moreActions" />
<QBtn
color="primary"
icon="restart_alt"
flat
@click="reset()"
:label="t('globals.reset')"
/>
<QBtn
color="primary"
icon="save"
@click="onSubmit()"
:disable="!hasDataChanged"
:label="t('globals.save')"
/>
</QBtnGroup>
</div>
</Teleport>
<QCard class="q-pa-lg">
<VnRow class="row q-mb-md">
<QCheckbox
v-model="hasData"
:label="t('account.mailForwarding.enableMailForwarding')"
:toggle-indeterminate="false"
/>
</VnRow>
<VnRow v-if="hasData" class="row q-gutter-md q-mb-md">
<VnInput
v-model="formData.forwardTo"
:label="t('account.mailForwarding.forwardingMail')"
:info="t('account.mailForwarding.mailInputInfo')"
>
</VnInput>
</VnRow>
</QCard>
</QForm>
<h5 v-else class="text-center">
{{ t('account.mailForwarding.accountNotEnabled') }}
</h5>
</div>
</template>

View File

@ -0,0 +1,49 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n();
const route = useRoute();
const rolesOptions = ref([]);
const formModelRef = ref();
</script>
<template>
<FetchData
url="VnRoles"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
auto-load
@on-fetch="(data) => (rolesOptions = data)"
/>
<FormModel
ref="formModelRef"
model="AccountPrivileges"
:url="`VnUsers/${route.params.id}`"
:url-create="`VnUsers/${route.params.id}/privileges`"
auto-load
@on-data-saved="formModelRef.fetch()"
>
<template #form="{ data }">
<div class="q-gutter-y-sm">
<QCheckbox
v-model="data.hasGrant"
:label="t('account.card.privileges.delegate')"
/>
<VnSelect
:label="t('account.card.role')"
v-model="data.roleFk"
:options="rolesOptions"
option-value="id"
option-label="name"
hide-selected
:required="true"
/>
</div>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,102 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import { useArrayData } from 'src/composables/useArrayData';
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const { store } = useArrayData('Account');
const account = ref(store.data);
const entityId = computed(() => $props.id || route.params.id);
const filter = {
where: { id: entityId },
fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } },
};
</script>
<template>
<CardSummary
data-key="AccountSummary"
ref="AccountSummary"
url="VnUsers/preview"
:filter="filter"
@on-fetch="(data) => (account = data)"
>
<template #header>{{ account.id }} - {{ account.nickname }}</template>
<template #body>
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<router-link
:to="{ name: 'AccountBasicData', params: { id: entityId } }"
class="header header-link"
>
{{ t('globals.pageTitles.basicData') }}
<QIcon name="open_in_new" />
</router-link>
</QCardSection>
<VnLv :label="t('account.card.nickname')" :value="account.nickname" />
<VnLv :label="t('account.card.role')" :value="account.role.name" />
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss" scoped>
.q-dialog__inner--minimized > div {
max-width: 80%;
}
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
}
.multimedia-container {
flex: 1 0 21%;
}
.multimedia {
transition: all 0.5s;
opacity: 1;
height: 250px;
.q-img {
object-fit: cover;
background-color: black;
}
video {
object-fit: cover;
background-color: black;
}
}
.multimedia:hover {
opacity: 0.5;
}
.close-button {
top: 1%;
right: 10%;
}
.zindex {
z-index: 1;
}
.change-state {
width: 10%;
}
</style>

View File

@ -0,0 +1,104 @@
<script setup>
import { useRoute, useRouter } from 'vue-router';
import { computed, ref, watch } from 'vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useArrayData } from 'composables/useArrayData';
const props = defineProps({
dataKey: { type: String, required: true },
});
const route = useRoute();
const router = useRouter();
const paginateRef = ref(null);
const arrayData = useArrayData(props.dataKey);
const store = arrayData.store;
const data = computed(() => {
const dataCopy = store.data;
return dataCopy.sort((a, b) => a.role?.name.localeCompare(b.role?.name));
});
const filter = computed(() => ({
where: {
prindicpalType: 'USER',
principalId: route.params.id,
},
include: {
relation: 'role',
scope: {
fields: ['id', 'name', 'description'],
},
},
}));
const urlPath = 'RoleMappings';
const columns = computed(() => [
{
name: 'name',
},
]);
watch(
() => route.params.id,
() => {
store.url = urlPath;
store.filter = filter.value;
store.limit = 0;
fetchSubRoles();
}
);
const fetchSubRoles = () => paginateRef.value.fetch();
const redirectToRoleSummary = (id) =>
router.push({ name: 'RoleSummary', params: { id } });
</script>
<template>
<QPage class="column items-center q-pa-md">
<div class="full-width" style="max-width: 400px">
<VnPaginate
ref="paginateRef"
:data-key="dataKey"
:filter="filter"
:url="urlPath"
:limit="0"
auto-load
>
<template #body>
<QTable :rows="data" :columns="columns" hide-header>
<template #body="{ row }">
<QTr
@click="redirectToRoleSummary(row.role?.id)"
class="cursor-pointer"
>
<QTd>
<div class="column">
<span>{{ row.role?.name }}</span>
<span class="color-vn-label">{{
row.role?.description
}}</span>
</div>
</QTd>
</QTr>
</template>
</QTable>
</template>
</VnPaginate>
</div>
</QPage>
</template>
<i18n>
es:
Role removed. Changes will take a while to fully propagate.: Rol eliminado. Los cambios tardaran un tiempo en propagarse completamente.
Role added! Changes will take a while to fully propagate.: ¡Rol añadido! Los cambios tardaran un tiempo en propagarse completamente.
El rol va a ser eliminado: Role will be removed
¿Seguro que quieres continuar?: Are you sure you want to continue?
</i18n>

View File

@ -15,6 +15,10 @@ const $props = defineProps({
required: false, required: false,
default: null, default: null,
}, },
summary: {
type: Object,
default: null,
},
}); });
const route = useRoute(); const route = useRoute();
@ -60,14 +64,14 @@ const removeRole = () => {
<template> <template>
<CardDescriptor <CardDescriptor
ref="descriptor" :url="`VnRoles/${entityId}`"
:url="`VnRoles`"
:filter="filter" :filter="filter"
module="Role" module="Role"
@on-fetch="setData" @on-fetch="setData"
data-key="accountData" data-key="accountData"
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"
:summary="$props.summary"
> >
<template #menu> <template #menu>
<QItem v-ripple clickable @click="removeRole()"> <QItem v-ripple clickable @click="removeRole()">

View File

@ -0,0 +1,17 @@
<script setup>
import RoleDescriptor from './RoleDescriptor.vue';
import RoleSummary from './RoleSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<QPopupProxy>
<RoleDescriptor v-if="$props.id" :id="$props.id" :summary="RoleSummary" />
</QPopupProxy>
</template>

View File

@ -30,6 +30,7 @@ const filter = {
:url="`VnRoles`" :url="`VnRoles`"
:filter="filter" :filter="filter"
@on-fetch="(data) => (role = data)" @on-fetch="(data) => (role = data)"
data-key="RoleSummary"
> >
<template #header> {{ role.id }} - {{ role.name }} </template> <template #header> {{ role.id }} - {{ role.name }} </template>
<template #body> <template #body>

View File

@ -15,24 +15,75 @@ account:
privileges: Privileges privileges: Privileges
mailAlias: Mail Alias mailAlias: Mail Alias
mailForwarding: Mail Forwarding mailForwarding: Mail Forwarding
accountCreate: New user
aliasUsers: Users aliasUsers: Users
card: card:
name: Name name: Name
nickname: User nickname: User
role: Rol role: Role
email: Email email: Email
alias: Alias alias: Alias
lang: Language lang: Language
roleFk: Role
newUser: New user
ticketTracking: Ticket tracking
privileges:
delegate: Can delegate privileges
enabled: Account enabled!
disabled: Account disabled!
willActivated: User will activated
willDeactivated: User will be deactivated
activated: User activated!
deactivated: User deactivated!
actions: actions:
setPassword: Set password setPassword: Set password
disableAccount: disableAccount:
name: Disable account name: Disable account
title: La cuenta será deshabilitada title: The account will be disabled
subtitle: ¿Seguro que quieres continuar? subtitle: Are you sure you want to continue?
disableUser: Disable user success: 'Account disabled!'
sync: Sync enableAccount:
delete: Delete name: Enable account
title: The account will be enabled
subtitle: Are you sure you want to continue?
success: 'Account enabled!'
deactivateUser:
name: Deactivate user
title: The user will be deactivated
subtitle: Are you sure you want to continue?
success: 'User deactivated!'
activateUser:
name: Activate user
title: The user will be disabled
subtitle: Are you sure you want to continue?
success: 'User activated!'
sync:
name: Sync
title: The account will be sync
subtitle: Are you sure you want to continue?
success: 'User synchronized!'
checkbox: Synchronize password
message: Do you want to synchronize user?
tooltip: If password is not specified, just user attributes are synchronized
delete:
name: Delete
title: The account will be deleted
subtitle: Are you sure you want to continue?
success: ''
search: Search user search: Search user
searchInfo: You can search by id, name or nickname
create:
name: Name
nickname: Nickname
email: Email
role: Role
password: Password
active: Active
mailForwarding:
forwardingMail: Forward email
accountNotEnabled: Account not enabled
enableMailForwarding: Enable mail forwarding
mailInputInfo: All emails will be forwarded to the specified address.
role: role:
pageTitles: pageTitles:
inheritedRoles: Inherited Roles inheritedRoles: Inherited Roles

View File

@ -15,6 +15,7 @@ account:
privileges: Privilegios privileges: Privilegios
mailAlias: Alias de correo mailAlias: Alias de correo
mailForwarding: Reenvío de correo mailForwarding: Reenvío de correo
accountCreate: Nuevo usuario
aliasUsers: Usuarios aliasUsers: Usuarios
card: card:
nickname: Usuario nickname: Usuario
@ -22,27 +23,66 @@ account:
role: Rol role: Rol
email: Mail email: Mail
alias: Alias alias: Alias
lang: dioma lang: Idioma
roleFk: Rol
enabled: ¡Cuenta habilitada!
disabled: ¡Cuenta deshabilitada!
willActivated: El usuario será activado
willDeactivated: El usuario será desactivado
activated: ¡Usuario activado!
deactivated: ¡Usuario desactivado!
newUser: Nuevo usuario
privileges:
delegate: Puede delegar privilegios
actions: actions:
setPassword: Establecer contraseña setPassword: Establecer contraseña
disableAccount: disableAccount:
name: Deshabilitar cuenta name: Deshabilitar cuenta
title: La cuenta será deshabilitada title: La cuenta será deshabilitada
subtitle: ¿Seguro que quieres continuar? subtitle: ¿Seguro que quieres continuar?
disableUser: success: '¡Cuenta deshabilitada!'
enableAccount:
name: Habilitar cuenta
title: La cuenta será habilitada
subtitle: ¿Seguro que quieres continuar?
success: '¡Cuenta habilitada!'
deactivateUser:
name: Desactivar usuario name: Desactivar usuario
title: El usuario será deshabilitado title: El usuario será deshabilitado
subtitle: ¿Seguro que quieres continuar? subtitle: ¿Seguro que quieres continuar?
success: '¡Usuario desactivado!'
activateUser:
name: Activar usuario
title: El usuario será activado
subtitle: ¿Seguro que quieres continuar?
success: '¡Usuario activado!'
sync: sync:
name: Sincronizar name: Sincronizar
title: El usuario será sincronizado title: El usuario será sincronizado
subtitle: ¿Seguro que quieres continuar? subtitle: ¿Seguro que quieres continuar?
success: '¡Usuario sincronizado!'
checkbox: Sincronizar contraseña
message: ¿Quieres sincronizar el usuario?
tooltip: Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
delete: delete:
name: Eliminar name: Eliminar
title: El usuario será eliminado title: El usuario será eliminado
subtitle: ¿Seguro que quieres continuar? subtitle: ¿Seguro que quieres continuar?
success: ''
search: Buscar usuario search: Buscar usuario
searchInfo: Puedes buscar por id, nombre o usuario
create:
name: Nombre
nickname: Nombre mostrado
email: Email
role: Rol
password: Contraseña
active: Activo
mailForwarding:
forwardingMail: Dirección de reenvío
accountNotEnabled: Cuenta no habilitada
enableMailForwarding: Habilitar redirección de correo
mailInputInfo: Todos los correos serán reenviados a la dirección especificada, no se mantendrá copia de los mismos en el buzón del usuario.
role: role:
pageTitles: pageTitles:
inheritedRoles: Roles heredados inheritedRoles: Roles heredados

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

@ -143,10 +143,6 @@ function handleLocation(data, location) {
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox
:label="t('Incoterms authorization')"
v-model="data.hasIncoterms"
/>
<QCheckbox <QCheckbox
:label="t('Electronic invoice')" :label="t('Electronic invoice')"
v-model="data.hasElectronicInvoice" v-model="data.hasElectronicInvoice"

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
@ -306,10 +310,8 @@ const creditWarning = computed(() => {
:value="entity.recommendedCredit" :value="entity.recommendedCredit"
/> />
</QCard> </QCard>
<QCard> <QCard class="vn-one">
<div class="header"> <VnTitle :text="t('Latest tickets')" />
{{ t('Latest tickets') }}
</div>
<CustomerSummaryTable /> <CustomerSummaryTable />
</QCard> </QCard>
</template> </template>

View File

@ -28,7 +28,6 @@ const isLoading = ref(false);
const name = ref(null); const name = ref(null);
const usersPreviewRef = ref(null); const usersPreviewRef = ref(null);
const user = ref([]); const user = ref([]);
const userPasswords = ref(0);
const dataChanges = computed(() => { const dataChanges = computed(() => {
return ( return (
@ -45,7 +44,6 @@ const showChangePasswordDialog = () => {
component: CustomerChangePassword, component: CustomerChangePassword,
componentProps: { componentProps: {
id: route.params.id, id: route.params.id,
userPasswords: userPasswords.value,
promise: usersPreviewRef.value.fetch(), promise: usersPreviewRef.value.fetch(),
}, },
}); });
@ -97,11 +95,6 @@ const onSubmit = async () => {
@on-fetch="(data) => (canChangePassword = data)" @on-fetch="(data) => (canChangePassword = data)"
auto-load auto-load
/> />
<FetchData
@on-fetch="(data) => (userPasswords = data[0])"
auto-load
url="UserPasswords"
/>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push class="q-gutter-x-sm"> <QBtnGroup push class="q-gutter-x-sm">

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: () => {},
@ -91,7 +91,6 @@ const tableColumnComponents = {
props: (prop) => ({ props: (prop) => ({
disable: true, disable: true,
'model-value': prop.value, 'model-value': prop.value,
class: 'disabled-checkbox',
}), }),
event: () => {}, event: () => {},
}, },
@ -102,12 +101,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 +121,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 +203,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

@ -9,6 +9,7 @@ import useNotify from 'src/composables/useNotify';
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 FetchData from 'src/components/FetchData.vue';
const { dialogRef } = useDialogPluginComponent(); const { dialogRef } = useDialogPluginComponent();
const { notify } = useNotify(); const { notify } = useNotify();
@ -19,15 +20,12 @@ const $props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
userPasswords: {
type: Object,
required: true,
},
promise: { promise: {
type: Function, type: Function,
required: true, required: true,
}, },
}); });
const userPasswords = ref({});
const closeButton = ref(null); const closeButton = ref(null);
const isLoading = ref(false); const isLoading = ref(false);
@ -60,6 +58,11 @@ const onSubmit = async () => {
<template> <template>
<QDialog ref="dialogRef"> <QDialog ref="dialogRef">
<FetchData
@on-fetch="(data) => (userPasswords = data[0])"
auto-load
url="UserPasswords"
/>
<QCard class="q-pa-lg"> <QCard class="q-pa-lg">
<QCardSection> <QCardSection>
<QForm @submit.prevent="onSubmit"> <QForm @submit.prevent="onSubmit">
@ -71,7 +74,7 @@ const onSubmit = async () => {
<QIcon name="close" size="sm" /> <QIcon name="close" size="sm" />
</span> </span>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md" style="flex-direction: column">
<div class="col"> <div class="col">
<VnInput <VnInput
:label="t('New password')" :label="t('New password')"
@ -84,11 +87,7 @@ const onSubmit = async () => {
<QTooltip> <QTooltip>
{{ {{
t('customer.card.passwordRequirements', { t('customer.card.passwordRequirements', {
length: $props.userPasswords.length, ...userPasswords,
nAlpha: $props.userPasswords.nAlpha,
nDigits: $props.userPasswords.nDigits,
nPunct: $props.userPasswords.nPunct,
nUpper: $props.userPasswords.nUpper,
}) })
}} }}
</QTooltip> </QTooltip>

View File

@ -162,6 +162,7 @@ const navigateToticketSummary = (id) => {
params: { id }, params: { id },
}); });
}; };
const commonColumns = (col) => ['date', 'state', 'total'].includes(col);
</script> </script>
<template> <template>
@ -171,67 +172,68 @@ const navigateToticketSummary = (id) => {
auto-load auto-load
url="Tickets" url="Tickets"
/> />
<QTable <QCard class="vn-one q-py-sm flex justify-between">
:columns="columns" <QTable
:pagination="{ rowsPerPage: 12 }" :columns="columns"
:rows="rows" :pagination="{ rowsPerPage: 12 }"
class="full-width q-mt-md" :rows="rows"
row-key="id" class="full-width"
v-if="rows?.length" row-key="id"
> >
<template #body-cell="props"> <template #body-cell="props">
<QTd :props="props" @click="navigateToticketSummary(props.row.id)"> <QTd :props="props" @click="navigateToticketSummary(props.row.id)">
<QTr :props="props" class="cursor-pointer"> <QTr :props="props" class="cursor-pointer">
<component <component
:is="tableColumnComponents[props.col.name].component" :is="tableColumnComponents[props.col.name].component"
@click="tableColumnComponents[props.col.name].event(props)" @click="tableColumnComponents[props.col.name].event(props)"
class="rounded-borders q-pa-sm" class="rounded-borders"
v-bind="tableColumnComponents[props.col.name].props(props)" v-bind="tableColumnComponents[props.col.name].props(props)"
>
<template
v-if="
props.col.name === 'id' ||
props.col.name === 'nickname' ||
props.col.name === 'agency' ||
props.col.name === 'route' ||
props.col.name === 'packages'
"
> >
{{ props.value }} <template v-if="!commonColumns(props.col.name)">
</template> <span
<template v-if="props.col.name === 'date'"> :class="{
<QBadge class="q-pa-sm" color="warning"> link:
{{ props.value }} props.col.name === 'route' ||
</QBadge> props.col.name === 'nickname',
</template> }"
<template v-if="props.col.name === 'state'"> >
<QBadge :color="setStateColor(props.row)" class="q-pa-sm"> {{ props.value }}
{{ props.value }} </span>
</QBadge> </template>
</template> <template v-if="props.col.name === 'date'">
<template v-if="props.col.name === 'total'"> <QBadge class="q-pa-sm" color="warning">
<QBadge {{ props.value }}
:color="setTotalPriceColor(props.row)" </QBadge>
class="q-pa-sm" </template>
v-if="setTotalPriceColor(props.row)" <template v-if="props.col.name === 'state'">
> <QBadge :color="setStateColor(props.row)" class="q-pa-sm">
{{ toCurrency(props.value) }} {{ props.value }}
</QBadge> </QBadge>
<div v-else>{{ toCurrency(props.value) }}</div> </template>
</template> <template v-if="props.col.name === 'total'">
<CustomerDescriptorProxy <QBadge
:id="props.row.clientFk" :color="setTotalPriceColor(props.row)"
v-if="props.col.name === 'nickname'" class="q-pa-sm"
/> v-if="setTotalPriceColor(props.row)"
<RouteDescriptorProxy >
:id="props.row.routeFk" {{ toCurrency(props.value) }}
v-if="props.col.name === 'route'" </QBadge>
/> <div v-else>{{ toCurrency(props.value) }}</div>
</component> </template>
</QTr> <CustomerDescriptorProxy
</QTd> :id="props.row.clientFk"
</template> v-if="props.col.name === 'nickname'"
</QTable> />
<RouteDescriptorProxy
:id="props.row.routeFk"
v-if="props.col.name === 'route'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
</QCard>
</template> </template>
<i18n> <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([]);
@ -82,6 +81,16 @@ const tableColumnComponents = computed(() => ({
}, },
event: getInputEvents, event: getInputEvents,
}, },
printedStickers: {
component: VnInput,
props: {
type: 'number',
min: 0,
class: 'input-number',
dense: true,
},
event: getInputEvents,
},
weight: { weight: {
component: VnInput, component: VnInput,
props: { props: {
@ -147,7 +156,7 @@ const entriesTableColumns = computed(() => {
return [ return [
{ {
label: t('entry.summary.item'), label: t('entry.summary.item'),
field: 'id', field: 'itemFk',
name: 'item', name: 'item',
align: 'left', align: 'left',
}, },
@ -169,6 +178,12 @@ const entriesTableColumns = computed(() => {
name: 'stickers', name: 'stickers',
align: 'left', align: 'left',
}, },
{
label: t('entry.buys.printedStickers'),
field: 'printedStickers',
name: 'printedStickers',
align: 'left',
},
{ {
label: t('entry.summary.weight'), label: t('entry.summary.weight'),
field: 'weight', field: 'weight',
@ -216,7 +231,6 @@ const entriesTableColumns = computed(() => {
}); });
const copyOriginalRowsData = (rows) => { const copyOriginalRowsData = (rows) => {
// el objetivo de esto es guardar los valores iniciales de todas las rows para evitar guardar cambios si la data no cambió al disparar los eventos
originalRowDataCopy.value = JSON.parse(JSON.stringify(rows)); originalRowDataCopy.value = JSON.parse(JSON.stringify(rows));
}; };
@ -297,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"
@ -386,19 +402,16 @@ const lockIconType = (groupingMode, mode) => {
</template> </template>
<ItemDescriptorProxy <ItemDescriptorProxy
v-if="col.name === 'item'" v-if="col.name === 'item'"
:id="props.row.id" :id="props.row.item.id"
/> />
</component> </component>
</QTd> </QTd>
</QTr> </QTr>
<QTr no-hover> <QTr no-hover class="full-width infoRow" style="column-span: all">
<QTd /> <QTd />
<QTd> <QTd cols>
<span>{{ props.row.item.itemType.code }}</span> <span>{{ props.row.item.itemType.code }}</span>
</QTd> </QTd>
<QTd>
<span>{{ props.row.item.id }}</span>
</QTd>
<QTd> <QTd>
<span>{{ props.row.item.size }}</span> <span>{{ props.row.item.size }}</span>
</QTd> </QTd>
@ -413,10 +426,6 @@ const lockIconType = (groupingMode, mode) => {
<FetchedTags :item="props.row.item" :max-length="5" /> <FetchedTags :item="props.row.item" :max-length="5" />
</QTd> </QTd>
</QTr> </QTr>
<!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys -->
<QTr v-if="props.rowIndex !== rows.length - 1" class="separation-row">
<QTd colspan="12" class="vn-table-separation-row" />
</QTr>
</template> </template>
<template #item="props"> <template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition"> <div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
@ -466,11 +475,13 @@ const lockIconType = (groupingMode, mode) => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.separation-row { .q-table--horizontal-separator tbody tr:nth-child(odd) > td {
background-color: var(--vn-section-color) !important; border-bottom-width: 0px;
border-top-width: 2px;
border-color: var(--vn-text-color);
} }
.grid-style-transition { .infoRow > td {
transition: transform 0.28s, background-color 0.28s; color: var(--vn-label-color);
} }
</style> </style>

View File

@ -135,14 +135,19 @@ watch;
<template #icons> <template #icons>
<QCardActions class="q-gutter-x-md"> <QCardActions class="q-gutter-x-md">
<QIcon <QIcon
v-if="currentEntry.isExcludedFromAvailable" v-if="currentEntry?.isExcludedFromAvailable"
name="vn:inventory" name="vn:inventory"
color="primary" color="primary"
size="xs" size="xs"
> >
<QTooltip>{{ t('Inventory entry') }}</QTooltip> <QTooltip>{{ t('Inventory entry') }}</QTooltip>
</QIcon> </QIcon>
<QIcon v-if="currentEntry.isRaid" name="vn:net" color="primary" size="xs"> <QIcon
v-if="currentEntry?.isRaid"
name="vn:net"
color="primary"
size="xs"
>
<QTooltip>{{ t('Virtual entry') }}</QTooltip> <QTooltip>{{ t('Virtual entry') }}</QTooltip>
</QIcon> </QIcon>
</QCardActions> </QCardActions>

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();
@ -167,7 +168,7 @@ const columns = computed(() => [
}, },
}, },
{ {
label: t('globals.description'), label: t('entry.latestBuys.description'),
field: 'description', field: 'description',
name: 'description', name: 'description',
align: 'left', align: 'left',
@ -653,6 +654,15 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<EntryLatestBuysFilter data-key="EntryLatestBuys" /> <EntryLatestBuysFilter data-key="EntryLatestBuys" />
</template> </template>
</RightMenu> </RightMenu>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<QTable <QTable
:rows="rows" :rows="rows"
@ -686,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

@ -184,13 +184,6 @@ const suppliersOptions = ref([]);
@click="removeTag(index, params, searchFn)" @click="removeTag(index, params, searchFn)"
/> />
</QItem> </QItem>
<QItem class="q-mt-lg">
<QIcon
name="add_circle"
class="filter-icon"
@click="tagValues.push({})"
/>
</QItem>
</template> </template>
</ItemsFilterPanel> </ItemsFilterPanel>
</template> </template>

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

@ -150,21 +150,19 @@ const downloadCSV = async () => {
> >
<template #body-cell-clientId="{ row }"> <template #body-cell-clientId="{ row }">
<QTd> <QTd>
<QBtn flat dense color="blue"> {{ row.clientId }}</QBtn> <QBtn flat dense class="link"> {{ row.clientId }}</QBtn>
<CustomerDescriptorProxy :id="row.clientId" /> <CustomerDescriptorProxy :id="row.clientId" />
</QTd> </QTd>
</template> </template>
<template #body-cell-ticketId="{ row }"> <template #body-cell-ticketId="{ row }">
<QTd> <QTd>
<QBtn flat dense color="blue"> {{ row.ticketFk }}</QBtn> <QBtn flat dense class="link"> {{ row.ticketFk }}</QBtn>
<TicketDescriptorProxy :id="row.ticketFk" /> <TicketDescriptorProxy :id="row.ticketFk" />
</QTd> </QTd>
</template> </template>
<template #body-cell-worker="{ row }"> <template #body-cell-worker="{ row }">
<QTd> <QTd>
<QBtn class="no-uppercase" flat dense color="blue">{{ <QBtn class="no-uppercase link" flat dense>{{ row.workerName }}</QBtn>
row.workerName
}}</QBtn>
<WorkerDescriptorProxy :id="row.comercialId" /> <WorkerDescriptorProxy :id="row.comercialId" />
</QTd> </QTd>
</template> </template>

View File

@ -20,6 +20,7 @@ const props = defineProps({
:data-key="props.dataKey" :data-key="props.dataKey"
:search-button="true" :search-button="true"
:unremovable-params="['from', 'to']" :unremovable-params="['from', 'to']"
:hidden-tags="['from', 'to']"
> >
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">

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,48 @@ 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);
break;
case 'ColorAndPrice':
tagObj.field = 'showOrder, price';
params.orderBy = JSON.stringify(tagObj);
break;
case 'Name':
tagObj.field = 'name';
params.orderBy = JSON.stringify(tagObj);
break;
case 'Price':
tagObj.field = 'price';
params.orderBy = JSON.stringify(tagObj);
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 +181,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 +190,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 +211,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 +304,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 +352,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 +386,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 +422,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 +486,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 +499,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,19 @@ const orderFilter = {
}, },
], ],
}; };
const onClientChange = async (clientId) => {
try {
const { data } = await axios.get(`Clients/${clientId}`);
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 +144,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 +163,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 +180,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 +224,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">

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