0
0
Fork 0

Merge pull request '#6825 - VnTable V1' (!396) from 6825-vnTable into dev

Reviewed-on: verdnatura/salix-front#396
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
Reviewed-by: Pablo Natek <pablone@verdnatura.es>
This commit is contained in:
Alex Moreno 2024-06-20 13:08:55 +00:00
commit bcda488aae
35 changed files with 1839 additions and 1611 deletions

View File

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

View File

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

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

View File

@ -97,7 +97,7 @@ const styleAttrs = computed(() => {
<QIcon
name="close"
size="xs"
v-if="hover && value"
v-if="hover && value && !readonly"
@click="onDateUpdate(null)"
></QIcon>
<QIcon name="event" class="cursor-pointer">

View File

@ -16,11 +16,11 @@ const $props = defineProps({
},
optionLabel: {
type: [String],
default: '',
default: 'name',
},
optionValue: {
type: String,
default: '',
default: 'id',
},
optionFilter: {
type: String,
@ -116,7 +116,9 @@ async function fetchFilter(val) {
if (new RegExp(/\d/g).test(val)) key = optionFilter.value ?? optionValue.value;
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) {
@ -178,6 +180,7 @@ watch(modelValue, (newValue) => {
>
<template v-if="isClearable" #append>
<QIcon
v-show="value"
name="close"
@click.stop="value = null"
class="cursor-pointer"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore';
import { buildFilter } from 'filters/filterPanel';
@ -13,6 +13,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const store = arrayDataStore.get(key);
const route = useRoute();
const router = useRouter();
let canceller = null;
const page = ref(1);
@ -22,8 +23,13 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
store.skip = 0;
const query = route.query;
if (query.params) {
store.userParams = JSON.parse(query.params);
const searchUrl = store.searchUrl;
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',
'userFilter',
'exprBuilder',
'searchUrl',
'navigate',
];
if (typeof userOptions === 'object') {
for (const option in userOptions) {
const isEmpty = userOptions[option] == null || userOptions[option] === '';
if (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) {
if (Object.hasOwn(store, option)) {
const defaultOpts = userOptions[option];
store[option] = userOptions.keepOpts?.includes(option)
? Object.assign(defaultOpts, store[option])
@ -87,8 +95,8 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
Object.assign(params, userParams);
store.isLoading = true;
store.currentFilter = params;
store.isLoading = true;
const response = await axios.get(store.url, {
signal: canceller.signal,
params,
@ -118,6 +126,10 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
}
}
function deleteOption(option) {
delete store[option];
}
function cancelRequest() {
if (canceller) {
canceller.abort();
@ -128,7 +140,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function applyFilter({ filter, params }) {
if (filter) store.userFilter = filter;
store.filter = {};
if (params) store.userParams = Object.assign({}, params);
if (params) store.userParams = { ...params };
const response = await fetch({ append: false });
return response;
@ -137,7 +149,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function addFilter({ filter, params }) {
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);
store.userParams = userParams;
@ -149,15 +161,20 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
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) {
for (const param in params) {
if (params[param] === '' || params[param] === null) {
delete store.userParams[param];
delete params[param];
if (store.filter?.where) {
const key = Object.keys(
exprBuilder && exprBuilder(param) ? exprBuilder(param) : param
);
const key = Object.keys(exprBuilder ? exprBuilder(param) : param);
if (key[0]) delete store.filter.where[key[0]];
if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where;
@ -182,22 +199,34 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
}
function updateStateParams() {
const query = {};
if (store.order) query.order = store.order;
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 newUrl = { path: route.path, query: { ...(route.query ?? {}) } };
newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter);
const url = new URL(window.location.href);
const { hash: currentHash } = url;
const [currentRoute] = currentHash.split('?');
if (store.navigate) {
const { customRouteRedirectName, searchText } = store.navigate;
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();
for (const param in query) params.append(param, query[param]);
const to =
store?.data?.length === 1
? path.replace(/\/(list|:id)|-list/, `/${store.data[0].id}`)
: path.replace(/:id.*/, '');
url.hash = currentRoute + '?' + params.toString();
window.history.pushState({}, '', url.hash);
if (route.path != to) {
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);
@ -207,6 +236,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
fetch,
applyFilter,
addFilter,
addFilterWhere,
refresh,
destroy,
loadMore,
@ -214,5 +244,6 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
totalRows,
updateStateParams,
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);
}
.text-primary-light {
color: $primary-light !important;
}
.bg-primary-light {
background: $primary-light !important;
}
.fill-icon {
font-variation-settings: 'FILL' 1;
}
@ -189,3 +196,26 @@ input::-webkit-inner-spin-button {
.q-scrollarea__content {
max-width: 100%;
}
/* ===== Scrollbar CSS ===== /
/ Firefox */
* {
scrollbar-width: auto;
scrollbar-color: var(--vn-label-color) transparent;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-thumb {
background-color: var(--vn-label-color);
border-radius: 10px;
}
*::-webkit-scrollbar-track {
background: transparent;
}

View File

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

View File

@ -279,8 +279,8 @@ customer:
extendedList:
tableVisibleColumns:
id: Identifier
name: Name
socialName: Social name
name: Comercial name
socialName: Business name
fi: Tax number
salesPersonFk: Salesperson
credit: Credit

View File

@ -278,7 +278,7 @@ customer:
extendedList:
tableVisibleColumns:
id: Identificador
name: Nombre
name: Nombre Comercial
socialName: Razón social
fi: NIF / CIF
salesPersonFk: Comercial

View File

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

View File

@ -29,7 +29,7 @@ const zones = ref();
@on-fetch="(data) => (workers = data)"
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 }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>

View File

@ -1,94 +1,439 @@
<script setup>
import { ref, computed, markRaw } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import CustomerFilter from './CustomerFilter.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 VnTable from 'components/VnTable/VnTable.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
import VnSearchbar from 'components/ui/VnSearchbar.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 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();
function navigate(id) {
router.push({ path: `/customer/${id}` });
}
const redirectToCreateView = () => {
router.push({ name: 'CustomerCreate' });
const redirectToCreateView = (row) => {
router.push({
name: 'TicketList',
query: {
params: JSON.stringify({
clientFk: row.id,
}),
},
});
};
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postcode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script>
<template>
<VnSearchbar
:info="t('You can search by customer id or name')"
:label="t('Search customer')"
data-key="CustomerList"
data-key="Customer"
/>
<RightMenu>
<template #right-panel>
<CustomerFilter data-key="CustomerList" />
</template>
</RightMenu>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
auto-load
data-key="CustomerList"
order="id DESC"
url="/Clients/filter"
>
<template #body="{ rows }">
<CardList
:id="row.id"
:key="row.id"
:title="row.name"
@click="navigate(row.id)"
v-for="row of rows"
>
<template #list-items>
<VnLv :label="t('customer.list.email')" :value="row.email" />
<VnLv :value="row.phone">
<template #label>
{{ t('customer.list.phone') }}
<VnLinkPhone :phone-number="row.phone" />
</template>
</VnLv>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openCard')"
@click.stop="navigate(row.id)"
outline
/>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, CustomerSummary)"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
<VnTable
ref="tableRef"
data-key="Customer"
url="Clients/extendedListFilter"
:create="{
urlCreate: 'Clients/createWithUser',
title: 'Create client',
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {
active: true,
isEqualizated: false,
},
}"
order="id DESC"
:columns="columns"
default-mode="table"
redirect="customer"
auto-load
>
<template #more-create-dialog="{ data }">
<VnLocation
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
/>
<QInput v-model="data.userName" :label="t('Web user')" />
<QInput :label="t('Email')" clearable type="email" v-model="data.email">
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip max-width="400px">{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]">
<QBtn @click="redirectToCreateView()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New client') }}
</QTooltip>
</QPageSticky>
</QPage>
</QInput>
</template>
</VnTable>
</template>
<i18n>
es:
Search customer: Buscar cliente
You can search by customer id or name: Puedes buscar por id o nombre del cliente
New client: Nuevo cliente
Web user: Usuario Web
</i18n>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
</style>

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

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

View File

@ -20,8 +20,8 @@ const { t } = useI18n();
const quasar = useQuasar();
const entityId = computed(() => $props.id || route.params.id);
const URL_KEY = 'NotificationSubscriptions';
const active = ref();
const available = ref();
const active = ref(new Map());
const available = ref(new Map());
async function toggleNotification(notification) {
try {
@ -56,6 +56,7 @@ const swapEntry = (from, to, key) => {
};
function setNotifications(data) {
console.log('data: ', data);
active.value = new Map(data.active);
available.value = new Map(data.available);
}

View File

@ -14,7 +14,6 @@ export default {
main: [
'CustomerList',
'CustomerPayments',
'CustomerExtendedList',
'CustomerNotifications',
'CustomerDefaulter',
],
@ -70,18 +69,6 @@ export default {
component: () =>
import('src/pages/Customer/Payments/CustomerPayments.vue'),
},
{
path: 'extendedList',
name: 'CustomerExtendedList',
meta: {
title: 'extendedList',
icon: 'vn:client',
},
component: () =>
import(
'src/pages/Customer/ExtendedList/CustomerExtendedList.vue'
),
},
{
path: 'notifications',
name: 'CustomerNotifications',

View File

@ -21,6 +21,8 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => {
isLoading: false,
userParamsChanged: false,
exprBuilder: null,
searchUrl: 'params',
navigate: null,
};
}

View File

@ -22,7 +22,7 @@ describe('InvoiceInDueDay', () => {
cy.waitForElement('thead');
cy.get(addBtn).click();
cy.saveCard();
cy.get('tbody > :nth-child(1)').should('exist');
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
});

View File

@ -1,31 +1,98 @@
import { describe, expect, it, beforeAll } from 'vitest';
import { axios } from 'app/test/vitest/helper';
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
import { axios, flushPromises } from 'app/test/vitest/helper';
import { useArrayData } from 'composables/useArrayData';
import { useRouter } from 'vue-router';
import * as vueRouter from 'vue-router';
describe('useArrayData', () => {
let arrayData;
beforeAll(() => {
axios.get.mockResolvedValue({ data: [] });
arrayData = useArrayData('InvoiceIn', { url: 'invoice-in/list' });
Object.defineProperty(window.location, 'href', {
writable: true,
value: 'localhost:9000/invoice-in/list',
const filter = '{"order":"","limit":10,"skip":0}';
const params = { supplierFk: 2 };
beforeEach(() => {
vi.spyOn(useRouter(), 'replace');
vi.spyOn(useRouter(), 'push');
});
afterEach(() => {
vi.clearAllMocks();
});
it('should fetch and repalce url with new params', async () => {
vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] });
const arrayData = useArrayData('ArrayData', { url: 'mockUrl' });
arrayData.store.userParams = params;
arrayData.fetch({});
await flushPromises();
const routerReplace = useRouter().replace.mock.calls[0][0];
expect(axios.get.mock.calls[0][1].params).toEqual({
filter,
supplierFk: 2,
});
expect(routerReplace.path).toEqual('mockSection/list');
expect(JSON.parse(routerReplace.query.params)).toEqual(
expect.objectContaining(params)
);
});
it('Should get data and send new URL without keeping parameters, if there is only one record', async () => {
vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }] });
const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} });
arrayData.store.userParams = params;
arrayData.fetch({});
await flushPromises();
const routerPush = useRouter().push.mock.calls[0][0];
expect(axios.get.mock.calls[0][1].params).toEqual({
filter,
supplierFk: 2,
});
expect(routerPush.path).toEqual('mockName/1');
expect(routerPush.query).toBeUndefined();
});
it('Should get data and send new URL keeping parameters, if you have more than one record', async () => {
vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }] });
vi.spyOn(vueRouter, 'useRoute').mockReturnValue({
matched: [],
query: {},
params: {},
meta: { moduleName: 'mockName' },
path: 'mockName/1',
});
vi.spyOn(vueRouter, 'useRouter').mockReturnValue({
push: vi.fn(),
replace: vi.fn(),
currentRoute: {
value: {
params: {
id: 1,
},
meta: { moduleName: 'mockName' },
matched: [{ path: 'mockName/:id' }],
},
},
});
// Mock the window.history.pushState method within useArrayData
window.history.pushState = (data, title, url) => (window.location.href = url);
const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} });
// Mock the URL constructor within useArrayData
global.URL = class URL {
constructor(url) {
this.hash = url.split('localhost:9000/')[1];
}
};
});
arrayData.store.userParams = params;
arrayData.fetch({});
it('should add the params to the url', async () => {
arrayData.store.userParams = { supplierFk: 2 };
arrayData.updateStateParams();
expect(window.location.href).contain('params=%7B%22supplierFk%22%3A2%7D');
await flushPromises();
const routerPush = useRouter().push.mock.calls[0][0];
expect(axios.get.mock.calls[0][1].params).toEqual({
filter,
supplierFk: 2,
});
expect(routerPush.path).toEqual('mockName/');
expect(routerPush.query.params).toBeDefined();
});
});

View File

@ -1,52 +0,0 @@
import { vi, describe, expect, it, beforeEach, beforeAll } from 'vitest';
import useRedirect from 'src/composables/useRedirect';
import { useRouter } from 'vue-router';
vi.mock('vue-router');
describe('useRedirect', () => {
useRouter.mockReturnValue({
push: vi.fn(),
currentRoute: {
value: {
matched: [
{ path: '/' },
{ path: '/customer' },
{ path: '/customer/:id' },
{ path: '/customer/:id/basic-data' },
],
},
},
});
const data = [];
let navigate;
let spy;
beforeAll(() => {
const { navigate: navigateFn } = useRedirect();
navigate = navigateFn;
spy = useRouter().push;
});
beforeEach(() => {
data.length = 0;
spy.mockReset();
});
it('should redirect to list page if there are several results', async () => {
data.push({ id: 1, name: 'employee' }, { id: 2, name: 'boss' });
navigate(data, {});
expect(spy).toHaveBeenCalledWith({ path: '/customer/' });
});
it('should redirect to list page if there is no results', async () => {
navigate(data, {});
expect(spy).toHaveBeenCalledWith({ path: '/customer/' });
});
it('should redirect to basic-data page if there is only one result', async () => {
data.push({ id: 1, name: 'employee' });
navigate(data, {});
expect(spy).toHaveBeenCalledWith({ path: '/customer/1/basic-data' });
});
});

View File

@ -15,16 +15,19 @@ installQuasarPlugin({
});
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false });
const mockPush = vi.fn();
const mockReplace = vi.fn();
vi.mock('vue-router', () => ({
useRouter: () => ({
push: mockPush,
replace: mockReplace,
currentRoute: {
value: {
params: {
id: 1,
},
meta: { moduleName: 'mockName' },
matched: [{ path: 'mockName/list' }],
},
},
}),
@ -33,6 +36,7 @@ vi.mock('vue-router', () => ({
query: {},
params: {},
meta: { moduleName: 'mockName' },
path: 'mockSection/list',
}),
}));