fix/OrderCatalog #893

Merged
jsegarra merged 11 commits from wbuezas/salix-front-mindshore-fork2:fix/OrderCatalog into dev 2024-11-12 12:01:43 +00:00
6 changed files with 360 additions and 196 deletions

View File

@ -119,7 +119,7 @@ watch(options, (newValue) => {
});
watch(modelValue, async (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
if (!myOptions?.value?.some((option) => option[optionValue.value] == newValue))
await fetchFilter(newValue);
if ($props.noOne) myOptions.value.unshift(noOneOpt.value);

View File

@ -57,7 +57,6 @@ const $props = defineProps({
},
});
defineExpose({ search, sanitizer });
const emit = defineEmits([
'update:modelValue',
'refresh',
@ -170,9 +169,29 @@ const tagsList = computed(() => {
return tagList;
});
const formatTags = (tags) => {
jsegarra marked this conversation as resolved
Review

La IA me ha propiesto esta solucion
const formatTags = (tags) => { return tags.flatMap((tag) => tag.label === 'and' ? tag.value.flatMap(item => Object.entries(item).map(([key, value]) => ({ label: key, value })) ) : [tag] ); };

Con reduce es mas complejo

La IA me ha propiesto esta solucion `const formatTags = (tags) => { return tags.flatMap((tag) => tag.label === 'and' ? tag.value.flatMap(item => Object.entries(item).map(([key, value]) => ({ label: key, value })) ) : [tag] ); };` Con reduce es mas complejo
Review

Decidí no aplicar la sugerencia ya que considero que complejiza y hace el codigo más dificil de leer / mantener.

Decidí no aplicar la sugerencia ya que considero que complejiza y hace el codigo más dificil de leer / mantener.
const formattedTags = [];
tags.forEach((tag) => {
if (tag.label === 'and') {
tag.value.forEach((item) => {
for (const key in item) {
formattedTags.push({ label: key, value: item[key] });
}
});
} else {
formattedTags.push(tag);
}
});
return formattedTags;
};
const tags = computed(() => {
return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.label));
const filteredTags = tagsList.value.filter(
(tag) => !($props.customTags || []).includes(tag.label)
);
return formatTags(filteredTags);
});
const customTags = computed(() =>
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label))
);
@ -193,13 +212,20 @@ function formatValue(value) {
function sanitizer(params) {
for (const [key, value] of Object.entries(params)) {
if (value && typeof value === 'object') {
if (key === 'and' && Array.isArray(value)) {
value.forEach((item) => {
Object.assign(params, item);
});
delete params[key];
} else if (value && typeof value === 'object') {
const param = Object.values(value)[0];
if (typeof param == 'string') params[key] = param.replaceAll('%', '');
}
}
return params;
}
defineExpose({ search, sanitizer, userParams });
</script>
<template>

View File

@ -10,11 +10,19 @@ function parseJSON(str, fallback) {
export default function (route, param) {
// catch route query params
const params = parseJSON(route?.query?.params, {});
// extract and parse filter from params
const { filter: filterStr = '{}' } = params;
const where = parseJSON(filterStr, {})?.where;
if (where && where[param] !== undefined) {
if (where && !param) {
return where;
} else if (where && where.and) {
const foundParam = where.and.find((p) => p[param]);
if (foundParam) {
return foundParam[param];
}
} else if (where && where[param]) {
return where[param];
}
return null;

View File

@ -0,0 +1,163 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
const props = defineProps({
tags: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['applyTags']);
const { t } = useI18n();
const tagValues = ref([{}]);
const tagOptions = ref([]);
const selectedTag = ref(null);
const applyTags = () => {
if (tagValues.value.some((tag) => !tag.value)) return;
const tagInfo = {
values: [...tagValues.value],
tagFk: selectedTag?.value?.id,
tagSelection: {
name: selectedTag?.value?.name,
},
};
emit('applyTags', tagInfo);
};
const removeTagGroupParam = (valIndex = null) => {
if (!valIndex) {
tagValues.value = [{}];
} else {
(tagValues.value || []).splice(valIndex, 1);
}
};
const getSelectedTagValues = async (tag) => {
try {
if (!tag?.id) return;
const filter = {
fields: ['value'],
order: 'value ASC',
limit: 30,
};
const url = `Tags/${tag?.id}/filterValue`;
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(url, {
params,
});
tagOptions.value = data;
} catch (err) {
console.error('Error getting selected tag values');
}
};
</script>
<template>
<QForm @submit="applyTags(tagValues)" class="all-pointer-events">
<QCard class="q-pa-sm column q-pa-lg">
<QBtn
round
color="primary"
style="position: absolute; z-index: 1; right: 0; top: 0"
jsegarra marked this conversation as resolved Outdated

porque pones un valor por defecto si ya tiene el default?

porque pones un valor por defecto si ya tiene el default?

Buena observación, había quedado de antes que componentice esta parte.

Commit: 498a52a3e5

Buena observación, había quedado de antes que componentice esta parte. Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/498a52a3e5a78c4e8c8f7c58a3cc4b655ca33cac
icon="search"
type="submit"
>
</QBtn>
<VnSelect
:label="t('params.tag')"
v-model="selectedTag"
:options="props.tags"
option-value="id"
option-label="name"
dense
outlined
class="q-mb-md"
rounded
:emit-value="false"
use-input
@update:model-value="($event) => getSelectedTagValues($event)"
/>
<QBtn
icon="add_circle"
shortcut="+"
flat
class="filter-icon q-mb-md"
size="md"
dense
:disabled="!selectedTag || !tagValues[0].value"
@click="tagValues.unshift({})"
/>
<div
v-for="(value, index) in tagValues"
:key="value"
class="filter-value column align-left"
>
<div class="col row q-mb-md">
<VnSelect
v-if="!selectedTag?.isFree && tagOptions"
:label="t('components.itemsFilterPanel.value')"
v-model="value.value"
:options="tagOptions || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
:disable="!value || !selectedTag"
:is-clearable="false"
class="col"
/>
<VnInput
v-else
v-model="value.value"
:label="t('components.itemsFilterPanel.value')"
:disable="!value"
is-outlined
:is-clearable="false"
class="col"
/>
<QBtn
icon="delete"
size="md"
outlined
dense
rounded
flat
class="filter-icon col-2"
:disabled="!value.value"
@click="removeTagGroupParam(index)"
/>
</div>
</div>
</QCard>
</QForm>
</template>
<style scoped lang="scss">
.filter-icon {
font-size: 24px;
color: $primary;
padding: 0 4px;
cursor: pointer;
}
</style>
<i18n>
en:
params:
tag: Tag
es:
params:
tag: Etiqueta
</i18n>

View File

@ -1,30 +1,57 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import { useRoute, useRouter } from 'vue-router';
import { onMounted, onUnmounted, ref } from 'vue';
import { onBeforeMount, onMounted, onUnmounted, ref, computed, watch } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue';
import CatalogItem from 'components/ui/CatalogItem.vue';
import OrderCatalogFilter from 'pages/Order/Card/OrderCatalogFilter.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import getParamWhere from 'src/filters/getParamWhere';
import { useArrayData } from 'composables/useArrayData';
const route = useRoute();
const router = useRouter();
const stateStore = useStateStore();
const { t } = useI18n();
const arrayData = useArrayData('OrderCatalogList');
const store = arrayData.store;
const showFilter = ref(null);
const tags = ref([]);
let catalogParams = {
orderFk: route.params.id,
orderBy: JSON.stringify({ field: 'relevancy DESC, name', way: 'ASC', isTag: false }),
};
onBeforeMount(() => {
const whereParams = getParamWhere(route);
if (whereParams) {
const formattedWhereParams = {};
if (whereParams.and) {
whereParams.and.forEach((item) => {
Object.assign(formattedWhereParams, item);
});
} else {
Object.assign(formattedWhereParams, whereParams);
}
catalogParams = {
...catalogParams,
...formattedWhereParams,
};
} else {
showFilter.value = true;
}
});
onMounted(() => {
stateStore.rightDrawer = true;
checkOrderConfirmation();
});
onUnmounted(() => (stateStore.rightDrawer = false));
const catalogParams = {
orderFk: route.params.id,
orderBy: JSON.stringify({ field: 'relevancy DESC, name', way: 'ASC', isTag: false }),
};
onUnmounted(() => (stateStore.rightDrawer = false));
async function checkOrderConfirmation() {
const response = await axios.get(`Orders/${route.params.id}`);
@ -34,6 +61,7 @@ async function checkOrderConfirmation() {
}
function extractTags(items) {
if (!items || !items.length) return;
const resultTags = [];
(items || []).forEach((item) => {
(item.tags || []).forEach((tag) => {
@ -61,6 +89,15 @@ function extractValueTags(items) {
);
tagValue.value = resultValueTags;
}
const autoLoad = computed(() => !!catalogParams.categoryFk);
watch(
() => store.data,
(val) => {
extractTags(val);
},
{ immediate: true }
);
</script>
<template>
@ -74,11 +111,12 @@ function extractValueTags(items) {
:info="t('You can search items by name or id')"
/>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<QScrollArea v-if="showFilter" class="fit text-grey-8">
<OrderCatalogFilter
data-key="OrderCatalogList"
:tag-value="tagValue"
:tags="tags"
:initial-catalog-params="catalogParams"
/>
</QScrollArea>
</QDrawer>
@ -89,8 +127,9 @@ function extractValueTags(items) {
url="Orders/CatalogFilter"
:limit="50"
:user-params="catalogParams"
@on-fetch="extractTags"
@on-fetch="showFilter = true"
:update-router="false"
:auto-load="autoLoad"
>
<template #body="{ rows }">
<div class="catalog-list">

View File

@ -1,5 +1,5 @@
<script setup>
import { computed, ref } from 'vue';
import { computed, ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
@ -9,10 +9,9 @@ import VnSelect from 'components/common/VnSelect.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import VnInput from 'src/components/common/VnInput.vue';
import getParamWhere from 'src/filters/getParamWhere';
import CatalogFilterValueDialog from 'src/pages/Order/Card/CatalogFilterValueDialog.vue';
import { useArrayData } from 'composables/useArrayData';
const { t } = useI18n();
const route = useRoute();
const props = defineProps({
dataKey: {
type: String,
@ -27,13 +26,18 @@ const props = defineProps({
required: true,
},
});
const { t } = useI18n();
const route = useRoute();
const arrayData = useArrayData(props.dataKey);
const categoryList = ref(null);
const selectedCategoryFk = ref(getParamWhere(route, 'categoryFk'));
const selectedCategoryFk = ref(null);
const typeList = ref([]);
const selectedTypeFk = ref(null);
const selectedTag = ref(null);
const tagValues = ref([{}]);
const tagOptions = ref([]);
const generalSearchParam = ref(null);
const vnFilterPanelRef = ref();
const orderByList = ref([
{ id: 'relevancy DESC, name', name: t('params.relevancy'), priority: 999 },
@ -48,32 +52,24 @@ const orderWayList = ref([
const orderBySelected = ref('relevancy DESC, name');
const orderWaySelected = ref('ASC');
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 = (params, search) => {
selectedCategoryFk.value = null;
typeList.value = null;
};
const clearFilter = (key) => {
if (key === 'categoryFk') {
resetCategory();
}
params.categoryFk = null;
params.typeFk = null;
arrayData.store.userFilter = null;
removeTagGroupParam(params, search);
};
const selectCategory = (params, category, search) => {
if (params.categoryFk === category?.id) {
resetCategory();
resetCategory(params, search);
params.categoryFk = null;
} else {
selectedCategoryFk.value = category?.id;
params.categoryFk = category?.id;
params.typeFk = null;
selectedTypeFk.value = null;
loadTypes(category?.id);
}
search();
@ -86,19 +82,12 @@ const loadTypes = async (categoryFk = selectedCategoryFk.value) => {
typeList.value = data;
};
const selectedCategory = computed(() =>
(categoryList.value || []).find(
const selectedCategory = computed(() => {
return (categoryList.value || []).find(
(category) => category?.id === selectedCategoryFk.value
)
);
function filterFn(val, update) {
update(() => {
const needle = val.toLowerCase();
tagOptions.value = props.tagValue.filter(
(v) => v.toLowerCase().indexOf(needle) > -1
);
});
}
);
});
const selectedType = computed(() => {
return (typeList.value || []).find((type) => type?.id === selectedTypeFk.value);
});
@ -114,37 +103,26 @@ function exprBuilder(param, value) {
}
}
const applyTagFilter = (params, search) => {
if (!tagValues.value?.length) {
const applyTags = (tagInfo, params, search) => {
if (!tagInfo || !tagInfo.values.length) {
params.tagGroups = null;
search();
return;
}
if (!params.tagGroups) {
params.tagGroups = [];
}
params.tagGroups.push(
JSON.stringify({
values: tagValues.value.filter((obj) => Object.keys(obj).length > 0),
tagSelection: {
...selectedTag.value,
orgShowField: selectedTag?.value?.name,
},
tagFk: selectedTag?.value?.tagFk,
})
);
if (!params.tagGroups) params.tagGroups = [];
params.tagGroups.push(tagInfo);
search();
selectedTag.value = null;
tagValues.value = [{}];
};
const removeTagChip = (selection, params, search) => {
if (params.tagGroups) {
params.tagGroups = (params.tagGroups || []).filter(
(value) => value !== selection
);
const removeTagGroupParam = (params, search, valIndex = null) => {
if (!valIndex) {
params.tagGroups = null;
search();
} else {
params.tagGroups.splice(valIndex, 1);
search();
}
search();
};
const setCategoryList = (data) => {
@ -171,6 +149,11 @@ function addOrder(value, field, params) {
params.orderBy = JSON.stringify(orderBy);
vnFilterPanelRef.value.search();
}
onMounted(() => {
selectedCategoryFk.value = getParamWhere(route, 'categoryFk');
selectedTypeFk.value = getParamWhere(route, 'typeFk');
});
</script>
<template>
@ -181,15 +164,11 @@ function addOrder(value, field, params) {
:hidden-tags="['orderFk', 'orderBy']"
:un-removable-params="['orderFk', 'orderBy']"
:expr-builder="exprBuilder"
:custom-tags="['tagGroups']"
@remove="clearFilter"
:custom-tags="['tagGroups', 'categoryFk']"
:redirect="false"
>
<template #tags="{ tag, formatFn }">
<strong v-if="tag.label === 'categoryFk'">
{{ t(selectedCategory?.name || '') }}
</strong>
<strong v-else-if="tag.label === 'typeFk'">
<strong v-if="tag.label === 'typeFk'">
{{ t(selectedType?.name || '') }}
</strong>
<div v-else class="q-gutter-x-xs">
@ -198,22 +177,33 @@ function addOrder(value, field, params) {
</div>
</template>
<template #customTags="{ tags: customTags, params, searchFn }">
<template v-for="tag in customTags" :key="tag.label">
<template v-if="tag.label === 'tagGroups'">
<VnFilterPanelChip
v-for="chip in tag.value"
:key="chip"
removable
@remove="removeTagChip(chip, params, searchFn)"
>
<strong> {{ JSON.parse(chip).tagSelection?.name }}: </strong>
<span>{{
(JSON.parse(chip).values || [])
.map((item) => item.value)
.join(' | ')
}}</span>
</VnFilterPanelChip>
</template>
<template v-for="customTag in customTags" :key="customTag.label">
<VnFilterPanelChip
v-for="(tag, valIndex) in Array.isArray(customTag.value)
? customTag.value
: 1"
:key="valIndex"
removable
@remove="
customTag.label === 'categoryFk'
? resetCategory(params, searchFn)
: removeTagGroupParam(params, searchFn, valIndex)
"
>
<strong v-if="customTag.label === 'categoryFk'">
{{ t(selectedCategory?.name || '') }}
</strong>
<strong v-if="tag?.tagSelection?.name" class="q-mr-xs">
jsegarra marked this conversation as resolved
Review

ufff 2 veces la misma comprobacion...

ufff 2 veces la misma comprobacion...
{{ tag.tagSelection.name }}:
</strong>
<span>
{{
(tag?.values || [])
.map((item) => `"${item.value}"`)
.join(', ')
}}
</span>
</VnFilterPanelChip>
</template>
</template>
<template #body="{ params, searchFn }">
@ -297,91 +287,46 @@ function addOrder(value, field, params) {
</QItemSection>
</QItem>
<QSeparator />
<QItem class="q-mt-md">
<QItemSection>
<VnSelect
:label="t('params.tag')"
v-model="selectedTag"
:options="props.tags || []"
option-value="id"
option-label="name"
dense
outlined
rounded
:emit-value="false"
use-input
/>
</QItemSection>
</QItem>
<QItem
v-for="(value, index) in tagValues"
:key="value"
class="q-mt-md filter-value"
>
<FetchData
v-if="selectedTag"
:url="`Tags/${selectedTag}/filterValue`"
limit="30"
auto-load
@on-fetch="(data) => (tagOptions = data)"
/>
<VnSelect
v-if="!selectedTag"
:label="t('params.value')"
v-model="value.value"
:options="tagOptions || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
class="filter-input"
@new-value="createValue"
@filter="filterFn"
@update:model-value="applyTagFilter(params, searchFn)"
/>
<VnSelect
v-else-if="selectedTag === 1"
:label="t('params.value')"
v-model="value.value"
:options="tagOptions || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
class="filter-input"
@new-value="createValue"
@update:model-value="applyTagFilter(params, searchFn)"
/>
<VnInput
v-else
:label="t('params.value')"
v-model="value.value"
dense
outlined
rounded
class="filter-input"
@keyup.enter="applyTagFilter(params, searchFn)"
/>
<QIcon
name="delete"
class="filter-icon"
@click="(tagValues || []).splice(index, 1)"
/>
</QItem>
<QItem class="q-mt-lg">
<QBtn
icon="add_circle"
shortcut="+"
flat
class="filter-icon"
@click="tagValues.push({})"
/>
<VnSelect
:label="t('components.itemsFilterPanel.value')"
:options="props.tagValue"
dense
outlined
rounded
:is-clearable="false"
v-model="generalSearchParam"
@update:model-value="
applyTags(
{ values: [{ value: generalSearchParam }] },
params,
searchFn
)
"
>
<template #prepend>
<QIcon name="search" />
</template>
<template #after>
<QBtn
icon="add_circle"
shortcut="+"
flat
color="primary"
size="md"
/>
<QPopupProxy>
<CatalogFilterValueDialog
style="display: inline-block"
:tags="tags"
@apply-tags="
($event) => applyTags($event, params, searchFn)
jsegarra marked this conversation as resolved Outdated

Prueba con QPopupProxy mejor que dialogo porque así se abre en la misma región y no hay que mover tanto el ratón y vista.
También nos facilita copiar y pegar valores

Prueba con QPopupProxy mejor que dialogo porque así se abre en la misma región y no hay que mover tanto el ratón y vista. También nos facilita copiar y pegar valores

Coincido, QPopupProxy aplicado.

Commit: 52a2250acc

Coincido, `QPopupProxy` aplicado. Commit: https://gitea.verdnatura.es/verdnatura/salix-front/commit/52a2250acc721f80a9a5e5d85dd4b9bad99ea1df
"
/>
</QPopupProxy>
</template>
</VnSelect>
</QItem>
<QSeparator />
</template>
@ -416,23 +361,6 @@ function addOrder(value, field, params) {
cursor: pointer;
}
}
.filter-icon {
font-size: 24px;
color: $primary;
padding: 0 4px;
cursor: pointer;
}
.filter-input {
flex-shrink: 1;
min-width: 0;
}
.filter-value {
display: flex;
align-items: center;
}
</style>
<i18n>