hedera-web/src/pages/Ecomerce/CatalogView.vue

1203 lines
34 KiB
Vue

<script setup>
import { useI18n } from 'vue-i18n';
import {
inject,
onBeforeMount,
ref,
computed,
watch,
onBeforeUnmount
} from 'vue';
import { useRoute, useRouter } from 'vue-router';
import VnImg from 'src/components/ui/VnImg.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CatalogCard from 'src/pages/Ecomerce/CatalogCard.vue';
import VnSearchBar from 'src/components/ui/VnSearchBar.vue';
import { useAppStore } from 'stores/app';
import { useUserStore } from 'stores/user';
import { storeToRefs } from 'pinia';
import { formatDateTitle, currency } from 'src/lib/filters.js';
import useNotify from 'src/composables/useNotify.js';
import debounce from 'src/utils/debouncer.js';
import { fetch } from 'src/composables/serviceUtils';
const jApi = inject('jApi');
const api = inject('api');
const { t } = useI18n();
const appStore = useAppStore();
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const { isHeaderMounted, rightDrawerOpen, basketOrderId, isDesktop } =
storeToRefs(appStore);
const { isGuest } = storeToRefs(userStore);
const { notify } = useNotify();
const order = ref(null);
const items = ref([]);
const selectedItem = ref(null);
const showItemDialog = ref(false);
const loading = ref(false);
const viewMode = ref('grid');
// Filters options
const categories = ref([]);
const itemColors = ref([]);
const itemFamilies = ref([]);
const itemProducers = ref([]);
const itemOrigins = ref([]);
const itemSubcategories = ref([]);
const search = ref(null);
// Filters values
const category = ref(null);
const type = ref(null);
const color = ref(null);
const producer = ref(null);
const origin = ref(null);
const subcategory = ref(null);
// Order by options
const orderBy = ref({
field: 'relevancy DESC, name',
way: 'ASC',
isTag: false
});
const orderByOptions = ref([
{
label: t('relevancy'),
value: {
field: 'relevancy DESC, name',
way: 'ASC',
isTag: false
}
},
{
label: t('name'),
value: {
field: 'name',
way: 'ASC',
isTag: false
}
},
{
label: t('lowerSize'),
value: {
field: 'i.size',
way: 'ASC',
isTag: false
}
},
{
label: t('higherSize'),
value: {
field: 'i.size',
way: 'DESC',
isTag: false
}
},
{
label: t('lowerPrice'),
value: {
field: 'price',
way: 'ASC',
isTag: false
}
},
{
label: t('higherPrice'),
value: {
field: 'price',
way: 'DESC',
isTag: false
}
},
{
label: t('available'),
value: {
field: 'available',
way: 'ASC',
isTag: false
}
},
{
label: t('color'),
value: {
field: 'i.inkFk',
way: 'ASC',
isTag: false
}
},
{
label: t('producer'),
value: {
field: 'producerFk',
way: 'ASC',
isTag: false
}
},
{
label: t('origin'),
value: {
field: 'originFk',
way: 'ASC',
isTag: false
}
},
{
label: t('category'),
value: {
field: 'categoryFk',
way: 'ASC',
isTag: false
}
}
]);
const selectedCategory = computed({
get() {
return category.value;
},
async set(value) {
category.value = value;
onCategoryChange();
if (!value) return;
debouncedGetFilters();
debouncedGetItems();
refreshTitle();
}
});
const selectedType = computed({
get() {
return type.value;
},
set(value) {
type.value = value;
router.push({
params: { category: category.value, type: type.value },
query: { ...route.query }
});
if (!selectedCategory.value) return;
debouncedGetFilters();
debouncedGetItems();
refreshTitle();
}
});
const selectedColor = computed({
get() {
return color.value;
},
set(value) {
color.value = value;
if (!selectedCategory.value) return;
debouncedGetFilters();
debouncedGetItems();
}
});
const selectedProducer = computed({
get() {
return producer.value;
},
set(value) {
producer.value = value;
if (!value || !selectedCategory.value) return;
debouncedGetFilters();
debouncedGetItems();
}
});
const selectedOrigin = computed({
get() {
return origin.value;
},
set(value) {
origin.value = value;
if (!value || !selectedCategory.value) return;
debouncedGetFilters();
debouncedGetItems();
}
});
const selectedSubcategory = computed({
get() {
return subcategory.value;
},
set(value) {
subcategory.value = value;
if (!value && !selectedCategory.value) return;
debouncedGetFilters();
debouncedGetItems();
}
});
const selectedOrderBy = computed({
get() {
return orderBy.value;
},
set(value) {
if (value) {
orderBy.value = value;
} else {
orderBy.value = {
field: 'relevancy DESC, name',
way: 'ASC',
isTag: false
};
}
debouncedGetItems();
}
});
const queryFilter = computed(() => {
const filters = [];
// Filtro principal
if (selectedCategory.value) {
filters.push(`(t.categoryFk = ${selectedCategory.value})`);
}
// Filtro búsqueda
if (search.value) {
filters.push(
`((i.longName LIKE '%${search.value}%') OR (i.subname LIKE '%${search.value}%'))`
);
}
const otherFilters = {
'i.typeFk': selectedType.value,
'i.inkFk': selectedColor?.value ? `'${selectedColor.value}'` : null,
'i.producerFk': selectedProducer.value,
'i.originFk': selectedOrigin.value
};
// Añadir solo aquellos filtros que tengan valor
Object.entries(otherFilters).forEach(([key, value]) => {
if (value) {
filters.push(`(${key} = ${value})`);
}
});
// Concatenar todos los filtros
return filters.join(' AND ');
});
const isSomeFilterSelected = computed(() => {
return (
search.value ||
selectedType.value ||
selectedColor.value ||
selectedProducer.value ||
selectedOrigin.value ||
selectedSubcategory.value
);
});
const viewTypeButtonContent = computed(() => {
return {
label: t(viewMode.value === 'list' ? 'gridView' : 'listView'),
icon: viewMode.value === 'list' ? 'grid_on' : 'view_list'
};
});
const checkGuest = () => {
if (isGuest.value) {
notify(t('youMustBeLoggedIn'), 'negative');
return true;
}
return false;
};
const getFilters = async () => {
const promises = [
getItemFamilies(),
getItemColors(),
getProducers(),
getOrigins(),
getSubcategories()
];
await Promise.allSettled(promises);
};
const getItemExprBuilder = (param, value) => {
if (param === 'orderFk' || param === 'orderBy') return;
else return { [param]: value };
};
const getItems = async () => {
try {
if (!basketOrderId.value || !isSomeFilterSelected.value) return;
loading.value = true;
const params = {
orderFk: basketOrderId.value,
orderBy: JSON.stringify({
field: selectedOrderBy.value.field,
way: selectedOrderBy.value.way,
isTag: selectedOrderBy.value.isTag
}),
typeFk: selectedType.value,
categoryFk: selectedCategory.value,
inkFk: selectedColor.value,
producerFk: selectedProducer.value,
originFk: selectedOrigin.value
};
const { data } = await fetch({
url: 'Orders/catalogFilter',
params,
exprBuilder: getItemExprBuilder
});
items.value = data;
await onItemsFetched();
loading.value = false;
} catch (error) {
console.error('Error getting items:', error);
}
};
const debouncedGetItems = debounce(getItems, 400);
const debouncedGetFilters = debounce(getFilters, 400);
const getOrder = async () => {
try {
if (!basketOrderId.value) return;
const filter = {
include: [
{
relation: 'address',
scope: {
fields: ['nickname', 'city', 'street']
}
}
],
id: basketOrderId.value,
fields: ['id', 'sent', 'addressFk', 'agencyModeFk', 'nickname']
};
const { data } = await api.get('Orders/filter', {
params: {
filter: JSON.stringify(filter),
orderFk: basketOrderId.value
}
});
order.value = data[0];
} catch (error) {
console.error('Error getting order:', error);
}
};
const getCategories = async () => {
try {
const { data } = await api.get('itemCategories', {
params: {
filter: JSON.stringify({
order: 'display ASC',
include: {
relation: 'itemTypes',
scope: {
fields: ['code', 'name']
}
}
})
}
});
categories.value = data.map(category => {
return {
...category,
icon: category.icon.split('-')[1]
};
});
} catch (error) {
console.error('Error getting categories:', error);
}
};
const getItemFamilies = async () => {
try {
if (!selectedCategory.value || !basketOrderId.value) return;
const { data } = await api.get(
`Orders/${basketOrderId.value}/getItemTypeAvailable`,
{
params: { itemCategoryId: selectedCategory.value }
}
);
itemFamilies.value = data;
} catch (error) {
console.error('Error getting available items:', error);
}
};
const getItemColors = async () => {
try {
if (!selectedCategory.value || !basketOrderId.value) return;
const { data } = await api.get('Orders/itemsColorsAvailable', {
params: {
orderFk: basketOrderId.value,
whereFilter: queryFilter.value
}
});
itemColors.value = data;
} catch (error) {
console.error('Error getting items colors:', error);
}
};
const onItemsFetched = async () => {
items.value.forEach(item => {
const previewTags = [
{ name: item.tag5, value: item.value5 },
{ name: item.tag6, value: item.value6 },
{ name: item.tag7, value: item.value7 }
];
item.previewTags = previewTags;
});
};
const getProducers = async () => {
try {
const { data } = await api.get('Orders/itemsProducersAvailable', {
params: {
orderFk: basketOrderId.value,
whereFilter: queryFilter.value
}
});
itemProducers.value = data;
} catch (error) {
console.error('Error getting producers:', error);
}
};
const getOrigins = async () => {
try {
const { data } = await api.get('Orders/itemsOriginsAvailable', {
params: {
orderFk: basketOrderId.value,
whereFilter: queryFilter.value
}
});
itemOrigins.value = data;
} catch (error) {
console.error('Error getting origins:', error);
}
};
const getSubcategories = async () => {
try {
const { data } = await api.get('Orders/itemsSubcategoriesAvailable', {
params: {
orderFk: basketOrderId.value,
whereFilter: queryFilter.value
}
});
const filtered = data.filter(item => item.category);
itemSubcategories.value = filtered.map(i => i.category);
} catch (error) {
console.error('Error getting subcategories:', error);
}
};
const showItem = async item => {
if (checkGuest()) return;
showItemDialog.value = true;
const [itemLots, tags] = await Promise.all([
calcItem(item.id),
getItemTags(item.id)
]);
item.lots = itemLots;
item.tags = tags;
selectedItem.value = item;
};
const removeCategory = () => {
selectedCategory.value = null;
onCategoryChange();
};
const onCategoryChange = () => {
selectedType.value = null;
selectedColor.value = null;
selectedProducer.value = null;
selectedOrigin.value = null;
selectedSubcategory.value = null;
search.value = '';
items.value = [];
router.push({ params: { category: category.value, type: null } });
};
const getItemTags = async itemFk => {
try {
const { data } = await api.get('ItemTags', {
params: {
filter: JSON.stringify({
where: {
itemFk
},
include: [
{
relation: 'tag',
scope: {
fields: ['name']
}
}
]
})
}
});
return data.map(tag => {
return {
name: tag?.tag?.name,
value: tag.value
};
});
} catch (error) {
console.error('Error getting available items:', error);
}
};
const calcItem = async itemId => {
try {
const { data } = await api.get('Orders/calcCatalogFromItem', {
params: {
orderFk: basketOrderId.value,
itemId
}
});
return data;
} catch (error) {
console.error('Error getting items:', error);
}
};
const addedItemsAmountAcc = ref({});
const amount = ref(0);
const onAddLotClick = async lot => {
try {
const { grouping, warehouseFk, available } = lot;
let lotAmount = addedItemsAmountAcc.value[warehouseFk];
if (lotAmount === undefined) lotAmount = 0;
if (lotAmount < available) {
let newAmount = lotAmount + grouping;
if (newAmount > available) newAmount = available;
addedItemsAmountAcc.value[warehouseFk] = newAmount;
amount.value += newAmount - lotAmount;
} else {
notify(t('amountNotAvailable'), 'negative');
}
} catch (error) {
console.error('Error adding item to basket:', error);
}
};
const resetAmounts = () => {
selectedItem.value = null;
addedItemsAmountAcc.value = {};
amount.value = 0;
};
const addItemToOrder = async params => {
try {
await api.post('applications/myOrder_addItem/execute-proc', {
schema: 'hedera',
params: [
params.orderFk,
params.warehouse,
params.item,
params.amount
]
});
} catch (error) {
console.error('Error adding item to basket:', error);
throw error;
}
};
const onConfirmClick = async params => {
try {
let amountSum = 0;
const addItemPromises = [];
for (const warehouse in addedItemsAmountAcc.value) {
const amount = addedItemsAmountAcc.value[warehouse];
amountSum += amount;
const params = {
orderFk: basketOrderId.value,
warehouse,
item: selectedItem.value.id,
amount
};
addItemPromises.push(addItemToOrder(params));
}
if (amountSum > 0) {
await Promise.all(addItemPromises);
notify(
`${t('added')} ${amountSum} ${selectedItem.value.name}`,
'positive'
);
}
showItemDialog.value = false;
} catch (error) {
console.error('Error adding item to basket:', error);
}
};
const refreshTitle = () => {
const { meta } = route;
const title = t(meta.title);
let subtitle;
let customTitle;
if (selectedCategory.value) {
const _category = categories.value.find(
i => i.id === selectedCategory.value
);
if (_category) {
const categoryName = _category.name;
customTitle = categoryName;
if (selectedType.value) {
const _type = itemFamilies.value.find(
i => i.id === selectedType.value
);
if (_type) {
subtitle = categoryName;
customTitle = _type.name;
}
}
}
}
appStore.$patch({ title, subtitle, customTitle });
};
const onViewModeClick = () => {
viewMode.value = viewMode.value === 'list' ? 'grid' : 'list';
};
const redirectToCheckout = () => {
if (checkGuest()) return;
router.push({
name: 'checkout',
params: { id: basketOrderId.value },
query: { continue: 'catalog' }
});
};
const redirectToBasket = () => {
if (checkGuest()) return;
router.push({ name: 'basket' });
};
const toggleRightDrawer = () => {
rightDrawerOpen.value = !rightDrawerOpen.value;
};
watch(
() => route.query.search,
val => {
if (val) {
search.value = val;
debouncedGetItems();
}
}
);
onBeforeMount(async () => {
if (!isGuest.value) {
await appStore.check('catalog');
} else {
const resultSet = await jApi.execQuery(
'CALL myOrder_configureForGuest(@orderId); SELECT @orderId;'
);
resultSet.fetchResult();
appStore.basketOrderId = resultSet.fetchValue();
}
await getOrder();
await getCategories();
if (route.params.category)
selectedCategory.value = Number(route.params.category);
if (route.params.type) selectedType.value = Number(route.params.type);
});
onBeforeUnmount(() => appStore.resetCustomTitle());
</script>
<template>
<Teleport v-if="isHeaderMounted" to="#actions">
<div class="row">
<VnSearchBar
@on-search-error="
() => {
items = [];
search = '';
}
"
/>
<QBtn
:icon="viewTypeButtonContent.icon"
:label="viewTypeButtonContent.label"
@click="onViewModeClick()"
rounded
no-caps
>
<QTooltip>
{{ viewTypeButtonContent.label }}
</QTooltip>
</QBtn>
<QBtn
icon="shopping_cart_checkout"
:label="t('shoppingCart')"
@click="redirectToBasket()"
rounded
no-caps
data-cy="catalogGoToBasketButton"
>
<QTooltip>
{{ t('shoppingCart') }}
</QTooltip>
</QBtn>
<QBtn
v-if="!isDesktop"
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleRightDrawer()"
/>
</div>
</Teleport>
<div>
<QDrawer v-model="rightDrawerOpen" side="right" :width="250" persistent>
<div class="q-pa-md">
<div class="basket-info q-gutter-y-sm">
<span v-if="order?.nickname">{{ order.nickname }}</span>
<span v-if="order?.sent">
{{
formatDateTitle(order.sent, {
shortDay: true,
shortMonth: true,
includeOfString: true
})
}}
</span>
<QBtn
rounded
no-caps
@click="redirectToCheckout()"
data-cy="orderModifyButton"
color="light-green-7"
unelevated
text-color="white"
>
{{ t('modify') }}
</QBtn>
</div>
<div class="q-mt-md">
<div class="q-mb-xs text-grey-7">
{{ t('category') }}
<QIcon
v-if="category"
style="font-size: 1.3em"
name="cancel"
class="cursor-pointer"
:title="t('deleteFilter')"
@click="removeCategory()"
/>
</div>
<div class="categories">
<QBtn
flat
round
class="category q-pa-sm"
v-for="cat in categories"
:class="{ active: category == cat.id }"
:key="cat.id"
@click="selectedCategory = cat.id"
data-cy="catalogCategoryButton"
>
<img :src="`statics/category/${cat.icon}.svg`" />
<QTooltip>{{ cat.name }}</QTooltip>
</QBtn>
</div>
</div>
<div class="q-mt-md" v-if="selectedCategory">
<div class="q-mb-xs text-grey-7">
{{ t('filterBy') }}
</div>
<VnSelect
v-model="selectedType"
option-value="id"
option-label="name"
:options="itemFamilies"
:disable="!category"
:label="t('family')"
data-cy="catalogFamilySelect"
/>
<VnSelect
v-model="selectedColor"
option-value="id"
option-label="name"
:options="itemColors"
:disable="!category"
:label="t('color')"
/>
<VnSelect
v-model="selectedProducer"
option-value="id"
option-label="name"
:options="itemProducers"
:disable="!category"
:label="t('producer')"
/>
<VnSelect
v-model="selectedOrigin"
option-value="id"
option-label="name"
:options="itemOrigins"
:disable="!category"
:label="t('origin')"
/>
<VnSelect
v-model="selectedSubcategory"
option-value="id"
option-label="name"
:options="itemSubcategories"
:disable="!category"
:label="t('category')"
/>
<div
v-if="isSomeFilterSelected"
class="q-mt-md text-grey-7"
>
{{ t('orderBy') }}
</div>
<VnSelect
v-if="isSomeFilterSelected"
v-model="selectedOrderBy"
:options="orderByOptions"
option-value="value"
option-label="label"
:is-clearable="false"
:label="t('sort')"
/>
</div>
<span
v-else
class="flex full-width justify-center q-pt-lg text-h6 text-grey-7"
>
{{ t('chooseCategory') }}
</span>
</div>
</QDrawer>
<div
:class="
viewMode === 'grid'
? ' row justify-center q-gutter-md'
: 'column items-center'
"
>
<QSpinner
v-if="loading"
color="primary"
size="3em"
:thickness="2"
/>
<div
v-else-if="!items || !items.length || !isSomeFilterSelected"
class="text-subtitle1 text-grey-7 q-pa-md"
>
<span>{{ t('pleaseSetFilter') }}</span>
</div>
<CatalogCard
v-else
v-for="_item in items"
:key="_item.id"
:item="_item"
:view-mode="viewMode"
@click="showItem(_item)"
data-cy="catalogCardElement"
/>
</div>
<QDialog v-model="showItemDialog" @hide="resetAmounts()">
<QCard v-if="selectedItem" style="width: 25em" class="column">
<div class="q-pa-md relative-position">
<div class="q-mb-md" style="display: flex">
<VnImg
storage="catalog"
size="200x200"
:id="selectedItem.image"
width="112px"
height="112px"
rounded="bottom"
class="q-mr-md"
/>
<div class="column">
<div class="text-subtitle2">
{{ selectedItem.name }}
</div>
<span
class="text-subtitle2 text-grey-7 text-uppercase"
>
{{ selectedItem.subName }}
</span>
<span class="text-grey-7">
#{{ selectedItem.id }}</span
>
</div>
</div>
<div class="text-caption">
<div
v-for="(tag, index) in selectedItem.tags"
:key="index"
class="row"
>
<span class="text-grey-7" style="width: 35%">
{{ tag.name }}
</span>
<span>{{ tag.value }}</span>
</div>
</div>
<div
v-if="selectedItem.minQuantity"
class="row justify-end q-px-md absolute-bottom-right q-pa-md"
>
<QIcon
name="production_quantity_limits"
size="xs"
color="negative"
>
<QTooltip>
{{ t('minQuantity') }}
</QTooltip>
</QIcon>
<span class="text-negative text-caption">{{
selectedItem.minQuantity
}}</span>
</div>
</div>
<div
v-for="(lot, index) in selectedItem.lots"
:key="index"
class="column"
>
<QSeparator />
<div class="row items-center q-px-md q-py-xs">
<span class="col">{{ currency(lot.price) }}</span>
<span class="col"> x{{ lot.grouping }}</span>
<QBtn
icon="add"
round
flat
dense
@click="onAddLotClick(lot)"
data-cy="addItemQuantityButton"
>
<QTooltip>{{ t('add') }}</QTooltip>
</QBtn>
</div>
</div>
<div class="row items-center justify-between q-pa-md bg-black">
<QBtn
icon="delete"
round
dense
flat
color="white"
@click="resetAmounts()"
>
<QTooltip>{{ t('delete') }}</QTooltip>
</QBtn>
<span class="text-center text-white">{{ amount }}</span>
<QBtn
icon="check"
round
dense
flat
color="white"
@click="onConfirmClick()"
data-cy="catalogAddToBasketButton"
>
<QTooltip>{{ t('confirm') }}</QTooltip>
</QBtn>
</div>
</QCard>
<QSpinner v-else color="primary" size="3em" :thickness="5" />
</QDialog>
</div>
</template>
<style lang="scss" scoped>
.basket-info {
display: flex;
flex-direction: column;
background-color: #8cc63f;
color: white;
padding: 17px 28px;
border-radius: 7px;
text-align: center;
}
.categories {
display: flex;
flex-wrap: wrap;
justify-content: center;
.category {
width: 54px;
&.active {
background: rgba(0, 0, 0, 0.08);
}
& > img {
height: 40px;
width: 40px;
}
}
}
</style>
<i18n lang="yaml">
en-US:
category: Category
deleteFilter: Delete filter
family: Family
color: Color
producer: Producer
origin: Origin
pleaseSetFilter: Choose a filter from the right menu
orderBy: Order by
relevancy: Relevancy
name: Name
lowerSize: Lower size
higherSize: Higher size
lowerPrice: Lower price
higherPrice: Higher price
add: Add
added: Added
listView: List view
gridView: Grid view
filterBy: Filter by
chooseCategory: Choose a category
youMustBeLoggedIn: You must be a registered user
sort: Order
amountNotAvailable: Amount not available
es-ES:
category: Categoría
deleteFilter: Quitar filtro
family: Familia
search: Buscar
color: Color
producer: Productor
origin: Origen
pleaseSetFilter: Elige un filtro en el menú de la derecha
orderBy: Ordernar por
relevancy: Relevancia
name: Nombre
lowerSize: Medida más pequeña
higherSize: Medida más grande
lowerPrice: Precio más bajo
higherPrice: Precio más alto
add: Añadir
added: Añadido
listView: Vista de lista
gridView: Vista de rejilla
filterBy: Filtrar por
chooseCategory: Elige una categoría
youMustBeLoggedIn: Debes estar registrado como usuario
sort: Ordenar
amountNotAvailable: Cantidad no disponible
ca-ES:
category: Categoría
deleteFilter: Eliminar filtro
family: Família
color: Color
producer: Productor
origin: Origen
pleaseSetFilter: Tria un filtre en el menú de la dreta
orderBy: Ordenar per
relevancy: Relevància
name: Nom
lowerSize: Mida més petita
higherSize: Mida més gran
lowerPrice: Preu més baix
higherPrice: Preu més alt
add: Afegir
listView: Vista de llista
gridView: Vista de graella
filterBy: Filtrar per
chooseCategory: Tria una categoria
youMustBeLoggedIn: Has d'estar registrat com a usuari
sort: Ordenar
amountNotAvailable: Quantitat no disponible
fr-FR:
category: Catégorie
deleteFilter: Supprimer le filtre
family: Famille
color: Couleur
producer: Producteur
origin: Origine
pleaseSetFilter: Choisissez un filtre dans le menu de droite
orderBy: Trier par
relevancy: Pertinence
name: Nom
lowerSize: Taille le plus bas
higherSize: Taille le plus élevé
lowerPrice: Prix le plus bas
higherPrice: Prix le plus élevé
add: Ajouter
listView: Vue en liste
gridView: Vue en grille
filterBy: Filtrer par
chooseCategory: Choisissez une catégorie
youMustBeLoggedIn: Vous devez être un utilisateur enregistré
sort: Trier
amountNotAvailable: Quantité non disponible
pt-PT:
category: Categoria
deleteFilter: Apagar filtro
family: Família
color: Cor
producer: Produtor
origin: Origem
pleaseSetFilter: Escolha um filtro no menu à direita
orderBy: Ordenar por
relevancy: Relevância
name: Nome
lowerSize: Tamanho menor
higherSize: Tamanho maior
lowerPrice: Preço mais baixo
higherPrice: Preço mais alto
add: Adicionar
listView: Vista de lista
gridView: Vista de grade
filterBy: Filtrar por
chooseCategory: Escolha uma categoria
youMustBeLoggedIn: Deves estar registrado como usuario
sort: Ordenar
amountNotAvailable: Quantidade não disponível
</i18n>