Catalog view #87

Merged
jsegarra merged 14 commits from wbuezas/hedera-web-mindshore:feature/Catalog into 4922-vueMigration 2024-10-01 19:27:15 +00:00
19 changed files with 1377 additions and 740 deletions

View File

@ -28,13 +28,9 @@ const props = defineProps({
type: Number, type: Number,
required: true required: true
}, },
rounded: { roundedBorders: {
type: Boolean, type: String,
default: false default: 'none'
},
fullRounded: {
type: Boolean,
default: false
}, },
width: { width: {
type: String, type: String,
@ -70,6 +66,17 @@ const showEditForm = ref(false);
const url = computed(() => { const url = computed(() => {
return `${props.baseURL ?? app.imageUrl}/${props.storage}/${props.size}/${props.id}`; return `${props.baseURL ?? app.imageUrl}/${props.storage}/${props.size}/${props.id}`;
}); });
const rounded = computed(() => {
const roundedMap = {
none: '',
default: 'rounded',
full: 'full-rounded',
top: 'top-rounded',
bottom: 'bottom-rounded'
};
return roundedMap[props.roundedBorders];
});
</script> </script>
<template> <template>
<div class="relative-position main-image-container"> <div class="relative-position main-image-container">
@ -85,11 +92,7 @@ const url = computed(() => {
<QTooltip>{{ t('addOrEditImage') }}</QTooltip> <QTooltip>{{ t('addOrEditImage') }}</QTooltip>
</QBtn> </QBtn>
<QImg <QImg
:class="{ :class="[rounded, { zoomIn: props.zoomSize }]"
zoomIn: props.zoomSize,
rounded: props.rounded,
'full-rounded': props.fullRounded
}"
class="main-image" class="main-image"
:src="url" :src="url"
v-bind="$attrs" v-bind="$attrs"
@ -161,9 +164,19 @@ const url = computed(() => {
.rounded { .rounded {
border-radius: 0.6em; border-radius: 0.6em;
} }
.full-rounded { .full-rounded {
border-radius: 50px; border-radius: 50px;
} }
.rounded-bottom {
border-radius: 0.6em 0.6em 0 0;
}
.rounded-top {
border-radius: 0 0 0.6em 0.6em;
}
.img_zoom { .img_zoom {
border-radius: 0%; border-radius: 0%;
} }

View File

@ -12,7 +12,7 @@ const props = defineProps({
}, },
placeholder: { placeholder: {
type: String, type: String,
default: 'Search' default: ''
}, },
sqlQuery: { sqlQuery: {
type: String, type: String,

View File

@ -103,6 +103,9 @@ export default {
user: 'Usuari', user: 'Usuari',
password: 'Contrasenya', password: 'Contrasenya',
modify: 'Modificar', modify: 'Modificar',
shoppingCart: 'Cistella de la compra',
available: 'Disponible',
minQuantity: 'Quantitat mínima',
// Image related translations // Image related translations
'Cant lock cache': 'No es pot bloquejar la memòria cau', 'Cant lock cache': 'No es pot bloquejar la memòria cau',
'Bad file format': 'Format de fitxer no reconegut', 'Bad file format': 'Format de fitxer no reconegut',

View File

@ -136,6 +136,9 @@ export default {
remindMe: 'Remember me', remindMe: 'Remember me',
password: 'Password', password: 'Password',
modify: 'Modify', modify: 'Modify',
shoppingCart: 'Shopping cart',
available: 'Available',
minQuantity: 'Minimum quantity',
// Image related translations // Image related translations
'Cant lock cache': 'The cache could not be blocked', 'Cant lock cache': 'The cache could not be blocked',
'Bad file format': 'Unrecognized file format', 'Bad file format': 'Unrecognized file format',

View File

@ -135,6 +135,9 @@ export default {
cancel: 'Cancelar', cancel: 'Cancelar',
of: 'de', of: 'de',
modify: 'Modificar', modify: 'Modificar',
shoppingCart: 'Cesta de la compra',
available: 'Disponible',
minQuantity: 'Cantidad mínima',
// Image related translations // Image related translations
'Cant lock cache': 'La caché no pudo ser bloqueada', 'Cant lock cache': 'La caché no pudo ser bloqueada',
'Bad file format': 'Formato de archivo no reconocido', 'Bad file format': 'Formato de archivo no reconocido',

View File

@ -103,6 +103,9 @@ export default {
user: 'Utilisateur', user: 'Utilisateur',
password: 'Mot de passe', password: 'Mot de passe',
modify: 'Modifier', modify: 'Modifier',
shoppingCart: 'Panier',
available: 'Disponible',
minQuantity: 'Quantité minimum',
// Image related translations // Image related translations
'Cant lock cache': "Le cache n'a pas pu être verrouillé", 'Cant lock cache': "Le cache n'a pas pu être verrouillé",
'Bad file format': 'Format de fichier non reconnu', 'Bad file format': 'Format de fichier non reconnu',

View File

@ -101,6 +101,9 @@ export default {
user: 'Utilizador', user: 'Utilizador',
password: 'Senha', password: 'Senha',
modify: 'Modificar', modify: 'Modificar',
shoppingCart: 'Cesta da compra',
available: 'Disponível',
minQuantity: 'Quantidade mínima',
// Image related translations // Image related translations
'Cant lock cache': 'O cache não pôde ser bloqueado', 'Cant lock cache': 'O cache não pôde ser bloqueado',
'Bad file format': 'Formato de arquivo inválido', 'Bad file format': 'Formato de arquivo inválido',

View File

@ -104,10 +104,7 @@ onMounted(async () => {
</QBtn> </QBtn>
</Teleport> </Teleport>
<QPage class="vn-w-sm"> <QPage class="vn-w-sm">
<QList <QList class="rounded-borders shadow-1 shadow-transition" separator>
class="rounded-borders shadow-1 shadow-transition"
separator
>
<CardList <CardList
v-for="(address, index) in addresses" v-for="(address, index) in addresses"
:key="index" :key="index"

View File

@ -33,9 +33,9 @@ const onSearch = data => (items.value = data || []);
<template> <template>
<Teleport v-if="isHeaderMounted" to="#actions"> <Teleport v-if="isHeaderMounted" to="#actions">
<VnSearchBar <VnSearchBar
:sqlQuery="query" :sql-query="query"
@onSearch="onSearch" @on-search="onSearch"
@onSearchError="items = []" @on-search-error="items = []"
/> />
</Teleport> </Teleport>
<QPage class="vn-w-xs"> <QPage class="vn-w-xs">
@ -66,8 +66,8 @@ const onSearch = data => (items.value = data || []);
class="q-mr-md" class="q-mr-md"
rounded rounded
editable editable
editSchema="catalog" edit-schema="catalog"
:editImageName="item.image" :edit-image-name="item.image"
/> />
</template> </template>
<template #content> <template #content>

View File

@ -1,706 +0,0 @@
<template>
<Teleport v-if="isHeaderMounted" to="#actions">
<QInput
:placeholder="$t('search')"
v-model="search"
debounce="500"
class="search q-mr-sm"
rounded
dark
dense
standout
>
<template #prepend>
<QIcon v-if="search === ''" name="search" />
<QIcon
v-else
name="clear"
class="cursor-pointer"
@click="search = ''"
/>
</template>
</QInput>
<QBtn
:icon="$t(viewMode == 'list' ? 'view_list' : 'grid_on')"
:label="$t(viewMode == 'list' ? 'listView' : 'gridView')"
@click="onViewModeClick()"
rounded
no-caps
/>
</Teleport>
<div style="padding-bottom: 5em">
<QDrawer v-model="$app.rightDrawerOpen" side="right" :width="250">
<div class="q-pa-md">
<div class="basket-info">
<p>{{ date(new Date()) }}</p>
<p>
{{ $t('warehouse') }}
{{ 'Algemesi' }}
</p>
<QBtn
flat
rounded
no-caps
:to="{
name: 'checkout',
params: { id: appStore.basketOrderId },
query: { continue: 'catalog' }
}"
>
{{ $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="
$router.push({ params: { category: null } })
"
/>
</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"
:title="cat.name"
:to="{ params: { category: cat.id, type: null } }"
>
<img :src="`statics/category/${cat.code}.svg`" />
</QBtn>
</div>
</div>
<div class="q-mt-md" v-if="category || search">
<div class="q-mb-xs text-grey-7">
{{ $t('filterBy') }}
</div>
<QSelect
v-model="type"
option-value="id"
option-label="name"
:options="types"
:disable="!category"
clearable
:label="$t('family')"
@filter="filterType"
@input="
$router.push({ params: { type: type && type.id } })
"
/>
<QSelect
v-model="order"
input-debounce="0"
:options="orderOptions"
:label="$t('orderBy')"
/>
</div>
</div>
<div class="q-pa-md" v-if="typeId || search">
<div class="q-mb-md" v-for="tag in tags" :key="tag.uid">
<div class="q-mb-xs text-caption text-grey-7">
{{ tag.name }}
<QIcon
v-if="tag.hasFilter"
style="font-size: 1.3em"
name="cancel"
:title="$t('deleteFilter')"
class="cursor-pointer"
@click="onResetTagFilterClick(tag)"
/>
</div>
<div v-if="!tag.useRange">
<div
v-for="value in tag.values.slice(0, tag.showCount)"
:key="value"
>
<QCheckbox
v-model="tag.filter"
:dense="true"
:val="value"
:label="value"
@input="onCheck(tag)"
/>
</div>
<div v-if="tag.values.length > tag.showCount">
<span
class="cursor-pointer text-blue"
@click="tag.showCount = Infinity"
>
<QIcon name="keyboard_arrow_down" />
{{ $t('viewMore') }}
</span>
</div>
<div v-if="tag.showCount == Infinity">
<span
class="cursor-pointer text-blue"
@click="tag.showCount = tag.initialCount"
>
<QIcon name="keyboard_arrow_up" />
{{ $t('viewLess') }}
</span>
</div>
</div>
<div class="q-mx-md">
<QRange
class="q-mt-lg"
v-if="tag.useRange"
v-model="tag.filter"
:min="tag.min"
:max="tag.max"
:step="tag.step"
:color="tag.hasFilter ? 'primary' : 'grey-6'"
@input="onRangeChange(tag, true)"
@change="onRangeChange(tag)"
label-always
markers
snap
/>
</div>
</div>
</div>
</QDrawer>
<QInfiniteScroll
@load="onLoad"
scroll-taget="html"
:offset="800"
:disable="disableScroll"
>
<div class="q-pa-md row justify-center q-gutter-md">
<QSpinner v-if="isLoading" color="primary" size="50px" />
<div
v-if="items && !items.length"
class="text-subtitle1 text-grey-7 q-pa-md"
>
{{ $t('noItemsFound') }}
</div>
<div
v-if="!items && !isLoading"
class="text-subtitle1 text-grey-7 q-pa-md"
>
{{ $t('pleaseSetFilter') }}
</div>
<QCard class="my-card" v-for="_item in items" :key="_item.id">
<img
:src="`${$imageBase}/catalog/200x200/${_item.image}`"
/>
<QCardSection>
<div class="name text-subtitle1">
{{ _item.longName }}
</div>
<div
class="sub-name text-uppercase text-subtitle1 text-grey-7 ellipsize q-pt-xs"
>
{{ _item.subName }}
</div>
<div class="tags q-pt-xs">
<div v-for="tag in _item.tags" :key="tag.tagFk">
<span class="text-grey-7">{{
tag.tag.name
}}</span>
{{ tag.value }}
</div>
</div>
</QCardSection>
<QCardActions class="actions justify-between">
<div class="q-pl-sm">
<span class="available bg-green text-white">{{
_item.available
}}</span>
{{ $t('from') }}
<span class="price">{{
currency(_item.buy?.price3)
}}</span>
</div>
<QBtn
icon="add_shopping_cart"
:title="$t('buy')"
@click="showItem(_item)"
flat
/>
</QCardActions>
</QCard>
</div>
<template #loading>
<div class="row justify-center q-my-md">
<QSpinner color="primary" name="dots" size="40px" />
</div>
</template>
</QInfiniteScroll>
<QDialog v-model="showItemDialog">
<QCard style="width: 25em">
<QImg
:src="`${$imageBase}/catalog/200x200/${item.image}`"
:ratio="5 / 3"
>
<div class="absolute-bottom text-center q-pa-xs">
<div class="text-subtitle1">
{{ item.longName }}
</div>
</div>
</QImg>
<QCardSection>
<div
class="text-uppercase text-subtitle1 text-grey-7 ellipsize"
>
{{ item.subName }}
</div>
<div class="text-grey-7">#{{ item.id }}</div>
</QCardSection>
<QCardSection>
<div v-for="tag in item.tags" :key="tag.tagFk">
<span class="text-grey-7">{{ tag.tag.name }}</span>
{{ tag.value }}
</div>
</QCardSection>
<QCardActions align="right">
<QBtn @click="showItemDialog = false" flat>
{{ $t('cancel') }}
</QBtn>
<QBtn @click="showItemDialog = false" flat>
{{ $t('accept') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
<QPageSticky>
<QBtn
fab
to="/ecomerce/basket"
icon="shopping_cart"
color="accent"
:title="$t('shoppingCart')"
/>
</QPageSticky>
</div>
</template>
<style lang="scss" scoped>
.search {
max-width: 250px;
}
.basket-info {
background-color: #8cc63f;
color: white;
padding: 17px 28px;
border-radius: 7px;
text-align: center;
& > p {
margin: 0;
padding: 0.4em 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.categories {
margin: 0 auto;
width: 220px;
.category {
width: 55px;
&.active {
background: rgba(0, 0, 0, 0.08);
}
& > img {
height: 40px;
width: 40px;
}
}
}
.tags {
max-height: 4.6em;
overflow: hidden;
}
.available {
padding: 0.15em;
border-radius: 0.2em;
font-size: 1.3em;
}
.price {
font-size: 1.3em;
}
.my-card {
width: 100%;
max-width: 17.5em;
height: 32.5em;
overflow: hidden;
.name,
.sub-name {
line-height: 1.3em;
}
.ellipsize {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.description {
height: 40px;
overflow: hidden;
}
}
</style>
<script>
import { date, currency, formatDate } from 'src/lib/filters.js';
import axios from 'axios';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const CancelToken = axios.CancelToken;
export default {
name: 'HederaCatalog',
setup() {
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
return { isHeaderMounted, appStore };
},
data() {
return {
uid: 0,
search: '',
orderDate: formatDate(new Date(), 'YYYY/MM/DD'),
category: null,
categories: [],
type: null,
typeId: null,
types: [],
orgTypes: [],
item: {},
showItemDialog: false,
tags: [],
isLoading: false,
items: null,
limit: null,
pageSize: 30,
maxTags: 5,
disableScroll: true,
viewMode: 'list',
order: {
label: this.$t('relevancy'),
value: 'relevancy DESC, longName'
},
orderOptions: [
{
label: this.$t('relevancy'),
value: 'relevancy DESC, longName'
},
{
label: this.$t('name'),
value: 'longName'
},
{
label: this.$t('siceAsc'),
value: 'size ASC'
},
{
label: this.$t('sizeDesc'),
value: 'size DESC'
},
{
label: this.$t('priceAsc'),
value: 'price ASC'
},
{
label: this.$t('priceDesc'),
value: 'price DESC'
},
{
label: this.$t('available'),
value: 'available'
}
]
};
},
created() {
this.$app.useRightDrawer = true;
},
async beforeMount() {
const isGuest = false; // TODO: Integrate isGuest logic
if (!isGuest) {
this.appStore.check('catalog');
}
},
async mounted() {
this.categories = await this.$jApi.query(
`SELECT c.id, l.name, c.color, c.code
FROM vn.itemCategory c
JOIN vn.itemCategoryL10n l ON l.id = c.id
WHERE c.display
ORDER BY display`
);
this.onRouteChange(this.$route);
},
beforeUnmount() {
this.clearTimeoutAndRequest();
},
beforeRouteUpdate(to, from, next) {
this.onRouteChange(to);
next();
},
watch: {
categories() {
this.refreshTitle();
},
orgTypes() {
this.refreshTitle();
},
order() {
this.loadItems();
},
date() {
this.loadItems();
},
async category(value) {
this.orgTypes = [];
if (!value) return;
const res = await this.$jApi.execQuery(
`CALL myOrder_getAvailable(${this.appStore.basketOrderId});
SELECT DISTINCT t.id, l.name
FROM vn.item i
JOIN vn.itemType t ON t.id = i.typeFk
JOIN tmp.itemAvailable a ON a.id = i.id
JOIN vn.itemTypeL10n l ON l.id = t.id
WHERE t.\`order\` >= 0
AND t.categoryFk = #category
ORDER BY t.\`order\`, l.name;
DROP TEMPORARY TABLE tmp.itemAvailable;`,
{ category: value }
);
res.fetch();
this.orgTypes = res.fetchData();
},
search(value) {
const location = { params: this.$route.params };
if (value) location.query = { search: value };
this.$router.push(location);
}
},
methods: {
date,
currency,
onViewModeClick() {
this.viewMode = this.viewMode === 'list' ? 'grid' : 'list';
},
onRouteChange(route) {
let { category, type } = route.params;
category = parseInt(category) || null;
type = parseInt(type) || null;
this.category = category;
this.typeId = category ? type : null;
this.search = route.query.search || '';
this.tags = [];
this.refreshTitle();
this.loadItems();
},
refreshTitle() {
let title = this.$t(this.$router.currentRoute.value.name);
let subtitle;
if (this.category) {
const category =
this.categories.find(i => i.id === this.category) || {};
title = category.name;
}
if (this.typeId) {
this.type = this.orgTypes.find(i => i.id === this.typeId);
subtitle = title;
title = this.type && this.type.name;
} else {
this.type = null;
}
this.$app.$patch({ title, subtitle });
},
clearTimeoutAndRequest() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (this.source) {
this.source.cancel();
this.source = null;
}
},
loadItemsDelayed() {
this.clearTimeoutAndRequest();
this.timeout = setTimeout(() => this.loadItems(), 500);
},
loadItems() {
this.items = null;
this.isLoading = true;
this.limit = this.pageSize;
this.disableScroll = false;
this.isLoading = false;
// this.loadItemsBase().finally(() => (this.isLoading = false))
},
onLoad(index, done) {
if (this.isLoading) return done();
this.limit += this.pageSize;
done();
// this.loadItemsBase().finally(done)
},
loadItemsBase() {
this.clearTimeoutAndRequest();
if (!(this.category || this.typeId || this.search)) {
this.tags = [];
return Promise.resolve(true);
}
const tagFilter = [];
for (const tag of this.tags) {
if (tag.hasFilter) {
tagFilter.push({
tagFk: tag.id,
values: tag.filter
});
}
}
this.source = CancelToken.source();
const params = {
dated: this.orderDate,
typeFk: this.typeId,
categoryFk: this.category,
search: this.search,
order: this.order.value,
limit: this.limit,
tagFilter
};
const config = {
params,
cancelToken: this.source.token
};
return this.$axios
.get('Items/catalog', config)
.then(res => this.onItemsGet(res))
.catch(err => this.onItemsError(err))
.finally(() => (this.cancel = null));
},
onItemsError(err) {
if (err.__CANCEL__) return;
this.disableScroll = true;
throw err;
},
onItemsGet(res) {
for (const tag of res.data.tags) {
tag.uid = this.uid++;
if (tag.filter) {
tag.hasFilter = true;
tag.useRange = tag.filter.max || tag.filter.min;
} else {
tag.useRange =
tag.isQuantitative && tag.values.length > this.maxTags;
this.resetTagFilter(tag);
}
if (tag.values) {
tag.initialCount = this.maxTags;
if (Array.isArray(tag.filter)) {
tag.initialCount = Math.max(
tag.initialCount,
tag.filter.length
);
}
tag.showCount = tag.initialCount;
}
}
this.items = res.data.items;
this.tags = res.data.tags;
this.disableScroll = this.items.length < this.limit;
},
onRangeChange(tag, delay) {
tag.hasFilter = true;
if (!delay) this.loadItems();
else this.loadItemsDelayed();
},
onCheck(tag) {
tag.hasFilter = tag.filter.length > 0;
this.loadItems();
},
resetTagFilter(tag) {
tag.hasFilter = false;
if (tag.useRange) {
tag.filter = {
min: tag.min,
max: tag.max
};
} else {
tag.filter = [];
}
},
onResetTagFilterClick(tag) {
this.resetTagFilter(tag);
this.loadItems();
},
filterType(val, update) {
if (val === '') {
update(() => {
this.types = this.orgTypes;
});
} else {
update(() => {
const needle = val.toLowerCase();
this.types = this.orgTypes.filter(
type => type.name.toLowerCase().indexOf(needle) > -1
);
});
}
},
showItem(item) {
this.item = item;
this.showItemDialog = true;
const conf = this.$state.catalogConfig;
const params = {
dated: this.orderDate,
addressFk: conf.addressFk,
agencyModeFk: conf.agencyModeFk
};
this.$axios
.get(`Items/${item.id}/calcCatalog`, { params })
.then(res => (this.lots = res.data));
}
}
};
</script>
<i18n lang="yaml">
es-ES:
gridView: Vista de rejilla
listView: Vista de lista
shoppingCart: Cesta de la compra
warehouse: Almacén
agency: Agencia
modify: Modificar
category: Categoría
deleteFilter: Quitar filtro
filterBy: Filtrar por
family: Familia
orderBy: Ordernar por
pleaseSetFilter: Elige un filtro en el menú de la derecha
search: Buscar
</i18n>

View File

@ -0,0 +1,207 @@
<script setup>
import { useI18n } from 'vue-i18n';
import VnImg from 'src/components/ui/VnImg.vue';
import CardList from 'src/components/ui/CardList.vue';
import { currency } from 'src/lib/filters.js';
defineProps({
item: { type: Object, default: () => {} },
viewMode: { type: String, default: 'grid' }
});
const { t } = useI18n();
</script>
<template>
<QCard v-if="viewMode === 'grid'" v-ripple class="catalog-card">
<VnImg
storage="catalog"
size="200x200"
:id="item.image"
height="210px"
rounded="bottom"
/>
<div class="column" style="height: 205px; padding: 10px">
<div class="column" style="margin-bottom: auto">
<div class="text-subtitle2 ellipsis-2-lines">
{{ item.item }}
</div>
<div
class="row justify-between text-uppercase text-subtitle1 text-grey-7"
>
<span class="text-subtitle2">
{{ item.subName }}
</span>
<span> #{{ item.id }}</span>
</div>
<div class="tags q-pt-xs text-caption">
<div
v-for="(tag, index) in item.previewTags"

Echo en falta el carro en rojo que representa la cantidad mínima
x1, representa el empaquetado mínimo. Esta traducción no está presente en el componente vue

Echo en falta el carro en rojo que representa la cantidad mínima x1, representa el empaquetado mínimo. Esta traducción no está presente en el componente vue

minQuantity handleado.

Commit: 0f5014088d

minQuantity handleado. Commit: https://gitea.verdnatura.es/verdnatura/hedera-web/commit/0f5014088d670c3e5cdbf63913672f14c6c3a4bf
:key="index"
class="full-width row"
>
<span class="text-grey-7 col">
{{ tag.name }}
</span>
<span class="col ellipsis">{{ tag.value }}</span>
</div>
</div>
<div v-if="item.minQuantity" class="row justify-end">
<QIcon
name="production_quantity_limits"
size="xs"
color="negative"
>
<QTooltip>
{{ t('minQuantity') }}
</QTooltip>
</QIcon>
<span class="text-negative text-caption">{{
item.minQuantity
}}</span>
</div>
</div>
<div class="row justify-between items-cente q-gutter-x-xs">
<QBadge
:label="`x${item.grouping}`"
color="grey"
class="col-2 justify-end text-body2"
>
<QTooltip>
{{ t('minGrouping') }}
</QTooltip>
</QBadge>
<QBadge
outline
:label="item.available"
color="accent"
text-color="black"
class="col justify-end text-body2"
>
<QTooltip>
{{ t('available') }}
</QTooltip>
</QBadge>
<QBadge
outline
:label="currency(item.price)"
color="accent"
text-color="black"
class="col justify-end text-body2"
>
<QTooltip>
{{ t('groupingPrice') }}
</QTooltip>
</QBadge>
</div>
</div>
</QCard>
<CardList v-else class="vn-w-sm">
<template #prepend>
<VnImg
storage="catalog"
size="200x200"
:id="item.image"
width="105px"
height="105px"
rounded-borders="full"
class="q-mr-md"
/>
</template>
<template #content>
<span class="ellipsis-2-lines">
{{ item.item }}
</span>
<div class="row justify-between text-uppercase text-grey-7">
<span>{{ item.subName }}</span>
<span>#{{ item.id }}</span>
</div>
<div class="full-width row">
<span
v-for="(tag, index) in item.previewTags"
:key="index"
class="text-grey-7 text-caption q-mr-sm"
>
{{ tag.value }}
</span>
</div>
<div v-if="item.minQuantity" class="row justify-end">
<QIcon
name="production_quantity_limits"
size="xs"
color="negative"
>
<QTooltip>
{{ t('minQuantity') }}
</QTooltip>
</QIcon>
<span class="text-negative text-caption">{{
item.minQuantity
}}</span>
</div>
<div class="row justify-end items-center q-gutter-x-xs q-mt-sm">
<QBadge
:label="`x${item.grouping}`"
color="grey"
class="col-2 justify-end text-body2"
>
<QTooltip>
{{ t('minGrouping') }}
</QTooltip>
</QBadge>
<QBadge
outline
:label="item.available"
color="accent"
text-color="black"
class="col-3 justify-end text-body2"
>
<QTooltip>
{{ t('available') }}
</QTooltip>
</QBadge>
<QBadge
outline
:label="currency(item.price)"
color="accent"
text-color="black"
class="col-3 justify-end text-body2"
>
<QTooltip>
{{ t('groupingPrice') }}
</QTooltip>
</QBadge>
</div>
</template>
</CardList>
</template>
<style lang="scss" scoped>
.catalog-card {
display: flex;
flex-direction: column;
width: 210px;
overflow: hidden;
cursor: pointer;
}
</style>
<i18n lang="yaml">
en-US:
groupingPrice: Price per group
minGrouping: Minimum packing
es-ES:
groupingPrice: Precio por grupo
minGrouping: Cantidad mínima
ca-ES:
groupingPrice: Preu per grup
minGrouping: Empaquetament mínim
fr-FR:
groupingPrice: Prix par groupe
minGrouping: Emballage minimum
pt-PT:
groupingPrice: Preço por grupo
minGrouping: Embalagem mínima
</i18n>

File diff suppressed because it is too large Load Diff

View File

@ -174,7 +174,6 @@ en-US:
startOrder: Start order startOrder: Start order
noOrdersFound: No orders found noOrdersFound: No orders found
makePayment: Make payment makePayment: Make payment
shoppingCart: Shopping cart
balance: 'Balance:' balance: 'Balance:'
paymentInfo: >- paymentInfo: >-
The amount shown is your slope (negative) or favorable balance today, it The amount shown is your slope (negative) or favorable balance today, it
@ -187,7 +186,6 @@ es-ES:
startOrder: Empezar pedido startOrder: Empezar pedido
noOrdersFound: No se encontrado pedidos noOrdersFound: No se encontrado pedidos
makePayment: Realizar pago makePayment: Realizar pago
shoppingCart: Cesta de la compra
balance: 'Saldo:' balance: 'Saldo:'
paymentInfo: >- paymentInfo: >-
La cantidad mostrada es tu saldo pendiente (negativa) o favorable a día de La cantidad mostrada es tu saldo pendiente (negativa) o favorable a día de
@ -201,7 +199,6 @@ ca-ES:
startOrder: Començar encàrrec startOrder: Començar encàrrec
noOrdersFound: No s'han trobat comandes noOrdersFound: No s'han trobat comandes
makePayment: Realitzar pagament makePayment: Realitzar pagament
shoppingCart: Cistella de la compra
balance: 'Saldo:' balance: 'Saldo:'
paymentInfo: >- paymentInfo: >-
La quantitat mostrada és el teu saldo pendent (negatiu) o favorable a dia La quantitat mostrada és el teu saldo pendent (negatiu) o favorable a dia
@ -215,7 +212,6 @@ fr-FR:
startOrder: Acheter startOrder: Acheter
noOrdersFound: Aucune commande trouvée noOrdersFound: Aucune commande trouvée
makePayment: Effectuer un paiement makePayment: Effectuer un paiement
shoppingCart: Panier
balance: 'Balance:' balance: 'Balance:'
paymentInfo: >- paymentInfo: >-
Le montant indiqué est votre pente (négative) ou balance favorable Le montant indiqué est votre pente (négative) ou balance favorable
@ -229,7 +225,6 @@ pt-PT:
startOrder: Iniciar encomenda startOrder: Iniciar encomenda
noOrdersFound: Nenhum pedido encontrado noOrdersFound: Nenhum pedido encontrado
makePayment: Realizar pagamento makePayment: Realizar pagamento
shoppingCart: Cesta da compra
balance: 'Saldo:' balance: 'Saldo:'
paymentInfo: >- paymentInfo: >-
A quantidade mostrada é seu saldo pendente (negativo) ou favorável a dia de A quantidade mostrada é seu saldo pendente (negativo) ou favorável a dia de

View File

@ -66,7 +66,7 @@ const deleteRow = id => {
<template> <template>
<QCard class="vn-w-sm" style="padding: 32px"> <QCard class="vn-w-sm" style="padding: 32px">
<QCardSection class="no-padding q-mb-md"> <QCardSection class="no-padding q-mb-md">
<div class="text-h6">#{{ ticket.id }}</div> <div class="text-h6 text-bold">#{{ ticket.id }}</div>
</QCardSection> </QCardSection>
<QCardSection class="no-padding q-mb-md q-gutter-y-xs"> <QCardSection class="no-padding q-mb-md q-gutter-y-xs">
<div class="text-subtitle1 text-bold"> <div class="text-subtitle1 text-bold">

View File

@ -41,12 +41,23 @@ onMounted(() => {
password.value.focus(); password.value.focus();
} }
}); });
async function onLogin() {
await userStore.login(email.value, password.value, remember.value); const onLogin = async () => {
await userStore.fetchUser(); await userStore.fetchUser();
await userStore.updateUserLang(selectedLocaleValue.value); await userStore.updateUserLang(selectedLocaleValue.value);
await router.push('/'); await router.push('/');
} };
const login = async () => {
await userStore.login(email.value, password.value, remember.value);
await onLogin();
};
const loginAsGuest = async () => {
userStore.isGuest = true;
localStorage.setItem('hederaGuest', true);
await onLogin();
};
</script> </script>
<template> <template>
@ -56,7 +67,7 @@ async function onLogin() {
<img src="statics/logo.svg" alt="Verdnatura" class="block" /> <img src="statics/logo.svg" alt="Verdnatura" class="block" />
</router-link> </router-link>
</div> </div>
<QForm @submit="onLogin" class="q-gutter-y-md"> <QForm @submit="login()" class="q-gutter-y-md">
<div class="q-gutter-y-sm"> <div class="q-gutter-y-sm">
<QInput v-model="email" :label="$t('user')" autofocus /> <QInput v-model="email" :label="$t('user')" autofocus />
<QInput <QInput
@ -106,7 +117,7 @@ async function onLogin() {
</div> </div>
<div class="justify-center"> <div class="justify-center">
<QBtn <QBtn
to="/" @click="loginAsGuest()"
:label="$t('logInAsGuest')" :label="$t('logInAsGuest')"
class="full-width" class="full-width"
color="primary" color="primary"

View File

@ -75,7 +75,7 @@ const routes = [
meta: { meta: {
title: 'Catalog' title: 'Catalog'
}, },
component: () => import('pages/Ecomerce/Catalog.vue') component: () => import('pages/Ecomerce/CatalogView.vue')
}, },
{ {
name: 'basket', name: 'basket',

View File

@ -139,6 +139,11 @@ export const useAppStore = defineStore('hedera', {
unloadOrder() { unloadOrder() {
localStorage.removeItem(storageOrderName); localStorage.removeItem(storageOrderName);
this.basketOrderId = null; this.basketOrderId = null;
},
onLogout() {
this.unloadOrder();
this.menuEssentialLinks = [];
} }
}, },
getters: { getters: {

View File

@ -2,6 +2,7 @@ import { defineStore } from 'pinia';
import { api, jApi } from 'boot/axios'; import { api, jApi } from 'boot/axios';
import { i18n } from 'src/boot/i18n'; import { i18n } from 'src/boot/i18n';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useAppStore } from 'src/stores/app.js';
const { t } = i18n.global; const { t } = i18n.global;
const { notify } = useNotify(); const { notify } = useNotify();
@ -35,6 +36,7 @@ export const useUserStore = defineStore('user', {
actions: { actions: {
async init() { async init() {
this.isGuest = localStorage.getItem('hederaGuest') || false;
await this.fetchUser(); await this.fetchUser();
await this.supplantInit(); await this.supplantInit();
this.updateSiteLocale(); this.updateSiteLocale();
@ -71,6 +73,8 @@ export const useUserStore = defineStore('user', {
sessionStorage.removeItem('vnToken'); sessionStorage.removeItem('vnToken');
} }
this.$reset(); this.$reset();
localStorage.removeItem('hederaGuest');
useAppStore().onLogout();
}, },
async fetchUser(userType = 'user') { async fetchUser(userType = 'user') {

10
src/utils/debouncer.js Normal file
View File

@ -0,0 +1,10 @@
export default function debounce(callback, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
// eslint-disable-next-line
callback(...args);
}, delay);
};
}