#7750 - first-beta-review #95

Merged
jsegarra merged 26 commits from wbuezas/hedera-web-mindshore:first-beta-review into beta 2024-12-09 23:18:07 +00:00
27 changed files with 280 additions and 209 deletions

View File

@ -6,31 +6,30 @@ Hedera is the main web page for Verdnatura.
Required dependencies. Required dependencies.
* PHP >= 7.0 - PHP >= 7.0
* Node.js >= 18.0 - Node.js >= 18.0
Launch application for development. Launch application for development.
``` ```
$ quasar dev $ quasar dev
``` ```
Launch Salix backend. Launch Salix backend.
```
npm run salix
``` ```
Launch legacy PHP backend. pnpm run back
```
npm run back
``` ```
Run server side method from command line. Run server side method from command line.
``` ```
php hedera-web.php -m method_path php hedera-web.php -m method_path
``` ```
## Built with ## Built with
* [Webpack](https://webpack.js.org/) - [Webpack](https://webpack.js.org/)
* [MooTools](https://mootools.net/) - [MooTools](https://mootools.net/)
* [TinyMCE](https://www.tinymce.com/) - [TinyMCE](https://www.tinymce.com/)

View File

@ -3,6 +3,7 @@ import { Connection } from '../js/db/connection';
import { useUserStore } from 'stores/user'; import { useUserStore } from 'stores/user';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useAppStore } from 'src/stores/app';
const { notify } = useNotify(); const { notify } = useNotify();
// Be careful when using SSR for cross-request state pollution // Be careful when using SSR for cross-request state pollution
@ -37,9 +38,11 @@ const onResponseError = error => {
export default boot(({ app }) => { export default boot(({ app }) => {
const userStore = useUserStore(); const userStore = useUserStore();
const appStore = useAppStore();
function addToken(config) { function addToken(config) {
if (userStore.token) { if (userStore.token) {
config.headers.Authorization = userStore.token; config.headers.Authorization = userStore.token;
config.headers['Accept-Language'] = appStore.siteLang;
} }
return config; return config;
} }

View File

@ -129,17 +129,20 @@ async function filterHandler(val, update) {
if (!$props.defaultFilter) return update(); if (!$props.defaultFilter) return update();
const newOptions = filter(val, myOptionsOriginal.value); const newOptions = filter(val, myOptionsOriginal.value);
update(
() => { setTimeout(() => {
myOptions.value = newOptions; update(
}, () => {
ref => { myOptions.value = newOptions;
if (val !== '' && ref.options.length > 0) { },
ref.setOptionIndex(-1); ref => {
ref.moveOptionSelection(1, true); if (val !== '' && ref.options.length > 0) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
} }
} );
); }, 300);
} }
</script> </script>
@ -178,6 +181,13 @@ async function filterHandler(val, update) {
> >
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" /> <slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template> </template>
<template #no-option>
<QItem>
<QItemSection class="text-grey">
{{ t('emptyList') }}
</QItemSection>
</QItem>
</template>
</QSelect> </QSelect>
</template> </template>

View File

@ -100,26 +100,21 @@ const onSubmit = async () => {
en-US: en-US:
name: Name name: Name
file: File file: File
send: Send
imageAdded: Image added successfully imageAdded: Image added successfully
es-ES: es-ES:
name: Nombre name: Nombre
file: Archivo file: Archivo
send: Enviar
imageAdded: Imagen añadida correctamente imageAdded: Imagen añadida correctamente
ca-ES: ca-ES:
name: Nom name: Nom
file: Arxiu file: Arxiu
send: Enviar
imageAdded: Imatge afegida correctament imageAdded: Imatge afegida correctament
fr-FR: fr-FR:
name: Nom name: Nom
file: Fichier file: Fichier
send: Envoyer
imageAdded: Image ajoutée correctement imageAdded: Image ajoutée correctement
pt-PT: pt-PT:
name: Nome name: Nome
file: Arquivo file: Arquivo
send: Enviar
imageAdded: Imagen adicionada corretamente imageAdded: Imagen adicionada corretamente
</i18n> </i18n>

View File

@ -67,6 +67,10 @@ 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 zoomUrl = computed(() => {
return `${props.baseURL ?? app.imageUrl}/${props.storage}/${props.zoomSize}/${props.id}`;
});
const rounded = computed(() => { const rounded = computed(() => {
const roundedMap = { const roundedMap = {
none: '', none: '',
@ -114,7 +118,7 @@ const rounded = computed(() => {
<QDialog v-if="props.zoomSize" v-model="showZoom"> <QDialog v-if="props.zoomSize" v-model="showZoom">
<QImg <QImg
:src="url" :src="zoomUrl"
size="full" size="full"
class="img_zoom" class="img_zoom"
v-bind="$attrs" v-bind="$attrs"

View File

@ -6,10 +6,6 @@ defineProps({
type: String, type: String,
default: 'emptyList' default: 'emptyList'
}, },
emptyIcon: {
type: String,
default: 'block'
},
rows: { rows: {
type: Array, type: Array,
default: () => [] default: () => []
@ -29,7 +25,6 @@ const { t } = useI18n();
v-if="!rows?.length" v-if="!rows?.length"
class="flex items-center q-pa-md justify-center items-center" class="flex items-center q-pa-md justify-center items-center"
> >
<QIcon :name="emptyIcon" size="sm" class="q-mr-sm" />
{{ t(emptyMessage) }} {{ t(emptyMessage) }}
</span> </span>
<QSpinner v-if="loading" color="primary" size="3em" :thickness="2" /> <QSpinner v-if="loading" color="primary" size="3em" :thickness="2" />

View File

@ -40,6 +40,11 @@ const search = async () => {
query: searchTerm.value ? { search: searchTerm.value } : {} query: searchTerm.value ? { search: searchTerm.value } : {}
}); });
if (!searchTerm.value) {
emit('onSearchError');
return;
}
if (props.sqlQuery) { if (props.sqlQuery) {
data = await jApi.query(props.sqlQuery, { data = await jApi.query(props.sqlQuery, {
[props.searchField]: searchTerm.value [props.searchField]: searchTerm.value

View File

@ -134,6 +134,7 @@ export default {
minQuantity: 'Quantitat mínima', minQuantity: 'Quantitat mínima',
introduceSearchTerm: 'Introdueix un terme de cerca', introduceSearchTerm: 'Introdueix un terme de cerca',
noOrdersFound: `No s'han trobat comandes`, noOrdersFound: `No s'han trobat comandes`,
send: 'Enviar',
// 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

@ -168,6 +168,7 @@ export default {
minQuantity: 'Minimum quantity', minQuantity: 'Minimum quantity',
introduceSearchTerm: 'Enter a search term', introduceSearchTerm: 'Enter a search term',
noOrdersFound: 'No orders found', noOrdersFound: 'No orders found',
send: 'Send',
// 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

@ -167,6 +167,7 @@ export default {
minQuantity: 'Cantidad mínima', minQuantity: 'Cantidad mínima',
introduceSearchTerm: 'Introduce un término de búsqueda', introduceSearchTerm: 'Introduce un término de búsqueda',
noOrdersFound: 'No se encontrado pedidos', noOrdersFound: 'No se encontrado pedidos',
send: 'Enviar',
// 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

@ -135,6 +135,7 @@ export default {
minQuantity: 'Quantité minimum', minQuantity: 'Quantité minimum',
introduceSearchTerm: 'Entrez un terme de recherche', introduceSearchTerm: 'Entrez un terme de recherche',
noOrdersFound: 'Aucune commande trouvée', noOrdersFound: 'Aucune commande trouvée',
send: 'Envoyer',
// 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

@ -133,6 +133,7 @@ export default {
minQuantity: 'Quantidade mínima', minQuantity: 'Quantidade mínima',
introduceSearchTerm: 'Digite um termo de pesquisa', introduceSearchTerm: 'Digite um termo de pesquisa',
noOrdersFound: 'Nenhum pedido encontrado', noOrdersFound: 'Nenhum pedido encontrado',
send: 'Enviar',
// 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

@ -3,13 +3,11 @@
id="bg" id="bg"
class="fullscreen row justify-center items-center layout-view scroll" class="fullscreen row justify-center items-center layout-view scroll"
> >
<div class="column q-pa-md row items-center justify-center"> <QPageContainer class="column q-pa-md row items-center justify-center">
<router-view v-slot="{ Component }"> <transition>
<transition> <router-view />
<component :is="Component" /> </transition>
</transition> </QPageContainer>
</router-view>
</div>
</QLayout> </QLayout>
</template> </template>

View File

@ -47,7 +47,7 @@ const logoutSupplantedUser = async () => {
</script> </script>
<template> <template>
<QLayout view="hhh Lpr fFf"> <QLayout view="hhh LpR fFf">
<QHeader> <QHeader>
<QToolbar> <QToolbar>
<QBtn <QBtn
@ -216,6 +216,10 @@ const logoutSupplantedUser = async () => {
padding: 16px; padding: 16px;
} }
div .q-drawer-container {
padding: 0 !important;
}
@include mobile { @include mobile {
#actions { #actions {
.q-btn { .q-btn {

View File

@ -109,12 +109,12 @@ const fetchData = async () => {
</QBtn> </QBtn>
<QBtn <QBtn
icon="shopping_bag" icon="shopping_bag"
:label="t('catalog')" :label="t('titles.Catalog')"
:to="{ name: 'catalog' }" :to="{ name: 'catalog' }"
rounded rounded
no-caps no-caps
> >
<QTooltip>{{ t('catalog') }}</QTooltip> <QTooltip>{{ t('titles.Catalog') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
icon="shopping_cart_checkout" icon="shopping_cart_checkout"

View File

@ -25,8 +25,9 @@ const { t } = useI18n();
storage="catalog" storage="catalog"
size="200x200" size="200x200"
:id="item.image" :id="item.image"
height="210px" height="190px"
rounded="bottom" rounded="bottom"
zoom-size="1600x900"
/> />
<div <div
class="column" class="column"
@ -45,7 +46,7 @@ const { t } = useI18n();
</span> </span>
<span> #{{ item.id }}</span> <span> #{{ item.id }}</span>
</div> </div>
<div class="tags q-pt-xs text-caption"> <div class="tags text-caption">
<div <div
v-for="(tag, index) in item.previewTags" v-for="(tag, index) in item.previewTags"
:key="index" :key="index"
@ -75,7 +76,7 @@ const { t } = useI18n();
<div class="row justify-between items-cente q-gutter-x-xs"> <div class="row justify-between items-cente q-gutter-x-xs">
<QBadge <QBadge
:label="`x${item.grouping}`" :label="`x${item.grouping}`"
color="grey" color="grey-4"
class="col-2 justify-end text-body2" class="col-2 justify-end text-body2"
> >
<QTooltip> <QTooltip>
@ -85,8 +86,7 @@ const { t } = useI18n();
<QBadge <QBadge
outline outline
:label="item.available" :label="item.available"
color="accent" color="grey-6"
text-color="black"
class="col justify-end text-body2" class="col justify-end text-body2"
> >
<QTooltip> <QTooltip>
@ -97,7 +97,6 @@ const { t } = useI18n();
outline outline
:label="currency(item.price)" :label="currency(item.price)"
color="accent" color="accent"
text-color="black"
class="col justify-end text-body2" class="col justify-end text-body2"
> >
<QTooltip> <QTooltip>
@ -117,6 +116,7 @@ const { t } = useI18n();
height="105px" height="105px"
rounded-borders="full" rounded-borders="full"
class="q-mr-md" class="q-mr-md"
zoom-size="1600x900"
/> />
</template> </template>
<template #content> <template #content>
@ -153,7 +153,8 @@ const { t } = useI18n();
<div class="row justify-end items-center q-gutter-x-xs q-mt-sm"> <div class="row justify-end items-center q-gutter-x-xs q-mt-sm">
<QBadge <QBadge
:label="`x${item.grouping}`" :label="`x${item.grouping}`"
color="grey" color="grey-4"
text-color="black"
class="col-2 justify-end text-body2" class="col-2 justify-end text-body2"
> >
<QTooltip> <QTooltip>
@ -163,8 +164,7 @@ const { t } = useI18n();
<QBadge <QBadge
outline outline
:label="item.available" :label="item.available"
color="accent" color="grey-6"
text-color="black"
class="col-3 justify-end text-body2" class="col-3 justify-end text-body2"
> >
<QTooltip> <QTooltip>
@ -175,7 +175,6 @@ const { t } = useI18n();
outline outline
:label="currency(item.price)" :label="currency(item.price)"
color="accent" color="accent"
text-color="black"
class="col-3 justify-end text-body2" class="col-3 justify-end text-body2"
> >
<QTooltip> <QTooltip>

View File

@ -1,7 +1,14 @@
<template> <template>
<Teleport v-if="isHeaderMounted" to="#actions"> <Teleport v-if="isHeaderMounted" to="#actions">
<div class="row"> <div class="row">
<VnSearchBar :search-term="search" @on-search-error="items = []" /> <VnSearchBar
@on-search-error="
() => {
items = [];
search = '';
}
"
/>
<QBtn <QBtn
:icon="viewTypeButtonContent.icon" :icon="viewTypeButtonContent.icon"
:label="viewTypeButtonContent.label" :label="viewTypeButtonContent.label"
@ -36,7 +43,7 @@
/> />
</div> </div>
</Teleport> </Teleport>
<div style="padding-bottom: 5em"> <div>
<QDrawer v-model="rightDrawerOpen" side="right" :width="250" persistent> <QDrawer v-model="rightDrawerOpen" side="right" :width="250" persistent>
<div class="q-pa-md"> <div class="q-pa-md">
<div class="basket-info q-gutter-y-sm"> <div class="basket-info q-gutter-y-sm">
@ -55,6 +62,9 @@
no-caps no-caps
@click="redirectToCheckout()" @click="redirectToCheckout()"
data-testid="orderModifyButton" data-testid="orderModifyButton"
color="light-green-7"
unelevated
text-color="white"
> >
{{ t('modify') }} {{ t('modify') }}
</QBtn> </QBtn>
@ -132,6 +142,12 @@
:disable="!category" :disable="!category"
:label="t('category')" :label="t('category')"
/> />
<div
v-if="isSomeFilterSelected"
class="q-mt-md text-grey-7"
>
{{ t('orderBy') }}
</div>
<VnSelect <VnSelect
v-if="isSomeFilterSelected" v-if="isSomeFilterSelected"
v-model="selectedOrderBy" v-model="selectedOrderBy"
@ -139,7 +155,7 @@
option-value="value" option-value="value"
option-label="label" option-label="label"
:is-clearable="false" :is-clearable="false"
:label="t('orderBy')" :label="t('sort')"
/> />
</div> </div>
<span <span
@ -153,7 +169,7 @@
<div <div
:class=" :class="
viewMode === 'grid' viewMode === 'grid'
? 'q-pa-md row justify-center q-gutter-md' ? ' row justify-center q-gutter-md'
: 'column items-center' : 'column items-center'
" "
> >
@ -167,7 +183,6 @@
v-else-if="!items || !items.length || !isSomeFilterSelected" v-else-if="!items || !items.length || !isSomeFilterSelected"
class="text-subtitle1 text-grey-7 q-pa-md" class="text-subtitle1 text-grey-7 q-pa-md"
> >
<QIcon name="refresh" size="sm" class="q-mr-sm"></QIcon>
<span>{{ t('pleaseSetFilter') }}</span> <span>{{ t('pleaseSetFilter') }}</span>
</div> </div>
<CatalogCard <CatalogCard
@ -180,13 +195,13 @@
/> />
</div> </div>
<QDialog v-model="showItemDialog" @hide="resetAmounts()"> <QDialog v-model="showItemDialog" @hide="resetAmounts()">
<QCard style="width: 25em" class="column"> <QCard v-if="selectedItem" style="width: 25em" class="column">
<div class="q-pa-md relative-position"> <div class="q-pa-md relative-position">
<div class="q-mb-md" style="display: flex"> <div class="q-mb-md" style="display: flex">
<VnImg <VnImg
storage="catalog" storage="catalog"
size="200x200" size="200x200"
:id="'asd'" :id="selectedItem.image"
width="112px" width="112px"
height="112px" height="112px"
rounded="bottom" rounded="bottom"
@ -282,6 +297,7 @@
</QBtn> </QBtn>
</div> </div>
</QCard> </QCard>
<QSpinner v-else color="primary" size="3em" :thickness="5" />
</QDialog> </QDialog>
</div> </div>
</template> </template>
@ -744,7 +760,8 @@ const getSubcategories = async () => {
DROP TEMPORARY TABLE tmp.itemAvailable;`, DROP TEMPORARY TABLE tmp.itemAvailable;`,
{ orderId: basketOrderId.value } { orderId: basketOrderId.value }
); );
itemSubcategories.value = res.results[1].data; const filtered = res.results[1].data.filter(item => item.category);
itemSubcategories.value = filtered.map(i => i.category);
} catch (error) { } catch (error) {
console.error('Error getting subcategories:', error); console.error('Error getting subcategories:', error);
Review

En lilium hemos quitado los trycatch porque si la peticion falla, el usuario no se entera.
Ya tenemos el controlador de axios, no?

En lilium hemos quitado los trycatch porque si la peticion falla, el usuario no se entera. Ya tenemos el controlador de axios, no?
Review

Si, tenemos el controlador de axios

Si, tenemos el controlador de axios
} }
@ -753,11 +770,13 @@ const getSubcategories = async () => {
const showItem = async item => { const showItem = async item => {
if (checkGuest()) return; if (checkGuest()) return;
const itemLots = await calcItem(item.id); showItemDialog.value = true;
const tags = await getItemTags(item.id); const [itemLots, tags] = await Promise.all([
calcItem(item.id),
getItemTags(item.id)
]);
item.lots = itemLots; item.lots = itemLots;
item.tags = tags; item.tags = tags;
showItemDialog.value = true;
selectedItem.value = item; selectedItem.value = item;
}; };
@ -848,6 +867,7 @@ const onAddLotClick = async lot => {
}; };
const resetAmounts = () => { const resetAmounts = () => {
selectedItem.value = null;
addedItemsAmountAcc.value = {}; addedItemsAmountAcc.value = {};
amount.value = 0; amount.value = 0;
}; };
@ -1031,6 +1051,8 @@ en-US:
filterBy: Filter by filterBy: Filter by
chooseCategory: Choose a category chooseCategory: Choose a category
youMustBeLoggedIn: You must be a registered user youMustBeLoggedIn: You must be a registered user
sort: Order
amountNotAvailable: Amount not available
es-ES: es-ES:
category: Categoría category: Categoría
deleteFilter: Quitar filtro deleteFilter: Quitar filtro
@ -1054,6 +1076,8 @@ es-ES:
filterBy: Filtrar por filterBy: Filtrar por
chooseCategory: Elige una categoría chooseCategory: Elige una categoría
youMustBeLoggedIn: Debes estar registrado como usuario youMustBeLoggedIn: Debes estar registrado como usuario
sort: Ordenar
amountNotAvailable: Cantidad no disponible
ca-ES: ca-ES:
category: Categoría category: Categoría
deleteFilter: Eliminar filtro deleteFilter: Eliminar filtro
@ -1075,6 +1099,8 @@ ca-ES:
filterBy: Filtrar per filterBy: Filtrar per
chooseCategory: Tria una categoria chooseCategory: Tria una categoria
youMustBeLoggedIn: Has d'estar registrat com a usuari youMustBeLoggedIn: Has d'estar registrat com a usuari
sort: Ordenar
amountNotAvailable: Quantitat no disponible
fr-FR: fr-FR:
category: Catégorie category: Catégorie
deleteFilter: Supprimer le filtre deleteFilter: Supprimer le filtre
@ -1096,6 +1122,8 @@ fr-FR:
filterBy: Filtrer par filterBy: Filtrer par
chooseCategory: Choisissez une catégorie chooseCategory: Choisissez une catégorie
youMustBeLoggedIn: Vous devez être un utilisateur enregistré youMustBeLoggedIn: Vous devez être un utilisateur enregistré
sort: Trier
amountNotAvailable: Quantité non disponible
pt-PT: pt-PT:
category: Categoria category: Categoria
deleteFilter: Apagar filtro deleteFilter: Apagar filtro
@ -1117,4 +1145,6 @@ pt-PT:
filterBy: Filtrar por filterBy: Filtrar por
chooseCategory: Escolha uma categoria chooseCategory: Escolha uma categoria
youMustBeLoggedIn: Deves estar registrado como usuario youMustBeLoggedIn: Deves estar registrado como usuario
sort: Ordenar
amountNotAvailable: Quantidade não disponível
</i18n> </i18n>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted, inject, computed } from 'vue'; import { ref, onMounted, inject, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
@ -28,6 +28,7 @@ const agencies = ref([]);
const warehouses = ref([]); const warehouses = ref([]);
const currentStep = ref('method'); const currentStep = ref('method');
const id = route.params.id; const id = route.params.id;
const defaultValues = ref(null);
const orderForm = ref({ const orderForm = ref({
method: 'AGENCY', method: 'AGENCY',
date: formatDate(Date.vnNew(), 'YYYY/MM/DD'), date: formatDate(Date.vnNew(), 'YYYY/MM/DD'),
@ -179,6 +180,15 @@ const getAgencies = async () => {
} }
); );
agencies.value = results[1].data; agencies.value = results[1].data;
if (agencies.value && agencies.value.length && defaultValues.value) {
const found = agencies.value.find(
agency => agency.id === defaultValues.value.defaultAgencyFk
);
Review

Duda, si no se encuentra no debería mostrar mensaje de error al usuario?
Cambiar found por agency

Duda, si no se encuentra no debería mostrar mensaje de error al usuario? Cambiar found por agency
Review

No creo que se deba mostrar un error por esto, supongo que por algo el hedera viejo no lo hacía

No creo que se deba mostrar un error por esto, supongo que por algo el hedera viejo no lo hacía
if (found)
orderForm.value.agency = defaultValues.value.defaultAgencyFk;
}
} catch (error) { } catch (error) {
console.error('Error getting agencies:', error); console.error('Error getting agencies:', error);
} }
@ -284,6 +294,13 @@ const submit = async () => {
} }
}; };
const getDefaultValues = async () => {
return await jApi.query(
`SELECT deliveryMethod, agencyModeFk, addressFk, defaultAgencyFk
FROM myBasketDefaults`
);
};
onMounted(async () => { onMounted(async () => {
today.value = Date.vnNew(); today.value = Date.vnNew();
today.value.setHours(0, 0, 0, 0); today.value.setHours(0, 0, 0, 0);
@ -305,10 +322,21 @@ onMounted(async () => {
orderForm.value.agency = order.agencyModeFk; orderForm.value.agency = order.agencyModeFk;
orderForm.value.address = order.addressFk; orderForm.value.address = order.addressFk;
} }
} else {
const [_defaultValues] = await getDefaultValues();
if (_defaultValues) defaultValues.value = _defaultValues;
} }
getAddresses(); getAddresses();
}); });
watch(
() => orderForm.value.method,
() => {
orderForm.value.address = '';
orderForm.value.agency = '';
}
);
</script> </script>
<template> <template>
@ -468,7 +496,7 @@ onMounted(async () => {
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
v-if="showNavigationButtons" v-if="showNavigationButtons || currentStep === 'confirm'"
@click="onNextStep(stepIndex)" @click="onNextStep(stepIndex)"
:color="currentStep === 'confirm' ? 'accent ' : 'primary'" :color="currentStep === 'confirm' ? 'accent ' : 'primary'"
:icon=" :icon="
@ -477,6 +505,7 @@ onMounted(async () => {
dense dense
class="right-navigation-button" class="right-navigation-button"
data-testid="checkoutStepperRightButton" data-testid="checkoutStepperRightButton"
:loading="loading"
> >
<QTooltip> <QTooltip>
{{ t(`${step.nextButtonLabel || 'next'}`) }} {{ t(`${step.nextButtonLabel || 'next'}`) }}
@ -491,7 +520,7 @@ onMounted(async () => {
@import 'src/css/responsive'; @import 'src/css/responsive';
.step-title { .step-title {
min-width: 100%; max-width: 90%;
margin-bottom: 16px; margin-bottom: 16px;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
@ -523,7 +552,7 @@ onMounted(async () => {
.left-navigation-button { .left-navigation-button {
position: absolute; position: absolute;
left: 5px; left: 5px;
top: 50%; top: 25px;
@include mobile { @include mobile {
top: 35%; top: 35%;
} }
@ -532,7 +561,7 @@ onMounted(async () => {
.right-navigation-button { .right-navigation-button {
position: absolute; position: absolute;
right: 5px; right: 5px;
top: 50%; top: 25px;
@include mobile { @include mobile {
top: 35%; top: 35%;
} }

View File

@ -288,9 +288,8 @@ onMounted(async () => {
> >
<div <div
v-if="!transferAccounts.length" v-if="!transferAccounts.length"
class="row items-center justify-center q-pa-md bg-red" class="row items-center justify-center q-pa-md"
> >
<QIcon class="q-mr-md" name="block" size="sm" />
<span>{{ t('emptyList') }}</span> <span>{{ t('emptyList') }}</span>
</div> </div>
<QList> <QList>

View File

@ -172,7 +172,6 @@ const deleteRow = id => {
class="row items-center justify-center q-pa-md" class="row items-center justify-center q-pa-md"
style="margin-top: 32px" style="margin-top: 32px"
> >
<QIcon class="q-mr-md" name="block" size="sm" />
<span>{{ t('emptyList') }}</span> <span>{{ t('emptyList') }}</span>
</div> </div>
</QCard> </QCard>

View File

@ -136,8 +136,11 @@ const loginAsGuest = async () => {
outline outline
/> />
</div> </div>
<p class="password-forgotten text-center q-mt-lg"> <p
<router-link to="/remember-password" class="link"> class="password-forgotten text-center q-mt-lg"
data-testid="recoverPasswordViewLink"
>
<router-link :to="{ name: 'recoverPassword' }" class="link">
{{ $t('haveForgottenPassword') }} {{ $t('haveForgottenPassword') }}
</router-link> </router-link>
</p> </p>

View File

@ -0,0 +1,99 @@
<script setup>
import { ref } from 'vue';
import { api } from 'boot/axios';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import useNotify from 'src/composables/useNotify.js';
import VnInput from 'src/components/common/VnInput.vue';
const user = ref('');
const router = useRouter();
const { t } = useI18n();
const { notify } = useNotify();
const onSend = async () => {
const params = {
user: user.value,
app: 'hedera'
};
await api.post('VnUsers/recoverPassword', params);
notify(t('weHaveSentEmailToRecover'), 'positive');
router.push('/login');
};
</script>
<template>
<QPage class="text-center">
<div>
<QIcon
name="contact_support"
class="block q-mx-auto text-accent"
style="font-size: 120px"
/>
</div>
<div>
<QForm @submit="onSend" class="q-gutter-y-md text-grey-8">
<VnInput
v-model="user"
:label="t('user')"
autofocus
data-testid="recoverPasswordUserInput"
/>
<div class="q-mt-lg">
{{ t('weSendEmail') }}
</div>
<div>
<QBtn
type="submit"
:label="t('send')"
class="full-width q-mt-md"
color="primary"
rounded
no-caps
unelevated
data-testid="recoverPasswordSubmitButton"
/>
<div class="text-center q-mt-md">
<router-link to="/login" class="link">
{{ t('back') }}
</router-link>
</div>
</div>
</QForm>
</div>
</QPage>
</template>
<style lang="scss" scoped>
.q-btn {
height: 50px;
}
</style>
<i18n lang="yaml">
en-US:
inputEmail: Input email
rememberPassword: Rememeber password
weSendEmail: We will sent you an email to recover your password
weHaveSentEmailToRecover: We've sent you an email where you can recover your password
es-ES:
inputEmail: Introduce el correo electrónico
rememberPassword: Recordar contraseña
weSendEmail: Te enviaremos un correo para restablecer tu contraseña
weHaveSentEmailToRecover: Te hemos enviado un correo donde podrás recuperar tu contraseña
ca-ES:
inputEmail: Introdueix el correu electrònic
rememberPassword: Recordar contrasenya
weSendEmail: T'enviarem un correu per restablir la teva contrasenya
weHaveSentEmailToRecover: T'hem enviat un correu on podràs recuperar la teva contrasenya
fr-FR:
inputEmail: Entrez l'email
rememberPassword: Se souvenir du mot de passe
weSendEmail: Nous vous enverrons un e-mail pour récupérer votre mot de passe
weHaveSentEmailToRecover: Nous vous avons envoyé un e-mail vous pouvez récupérer votre mot de passe
pr-BR:
inputEmail: Digite o e-mail
rememberPassword: Lembrar senha
weSendEmail: Enviaremos um e-mail para recuperar sua senha
weHaveSentEmailToRecover: Enviamos um e-mail onde você pode recuperar sua senha
</i18n>

View File

@ -1,114 +0,0 @@
<template>
<div class="text-center">
<div>
<QIcon
name="contact_support"
class="block q-mx-auto text-accent"
style="font-size: 120px"
/>
</div>
<div>
<QForm
@submit="onSend"
class="q-gutter-y-md text-grey-8"
>
<div class="text-h5">
<div>
{{ $t('dontWorry') }}
</div>
<div>
{{ $t('fillData') }}
</div>
</div>
<QInput
v-model="email"
:label="$t('user')"
:rules="[val => !!val || $t('inputEmail')]"
autofocus
/>
<div class="q-mt-lg">
{{ $t('weSendEmail') }}
</div>
<div>
<QBtn
type="submit"
:label="$t('send')"
class="full-width q-mt-md"
color="primary"
rounded
no-caps
unelevated
/>
<div class="text-center q-mt-md">
<router-link
to="/login"
class="link"
>
{{ $t('return') }}
</router-link>
</div>
</div>
</QForm>
</div>
</div>
</template>
<style lang="scss" scoped>
#image {
height: 190px;
}
.q-btn {
height: 50px;
}
a {
color: inherit;
font-size: 0.8rem;
}
</style>
<script>
export default {
name: 'VnRememberPasword',
data() {
return {
email: ''
};
},
methods: {
async onSend() {
const params = {
email: this.email
};
await this.$axios.post('Users/reset', params);
this.$q.notify({
message: this.$t('weHaveSentEmailToRecover'),
type: 'positive'
});
this.$router.push('/login');
}
}
};
</script>
<i18n lang="yaml">
en-US:
user: User
inputEmail: Input email
rememberPassword: Rememeber password
dontWorry: Don't worry!
fillData: Fill the data
weSendEmail: We will sent you an email to recover your password
weHaveSentEmailToRecover: We've sent you an email where you can recover your password
send: Send
return: Return
es-ES:
user: Usuario
inputEmail: Introduce el correo electrónico
rememberPassword: Recordar contraseña
dontWorry: ¡No te preocupes!
fillData: Rellena los datos
weSendEmail: Te enviaremos un correo para restablecer tu contraseña
weHaveSentEmailToRecover: Te hemos enviado un correo donde podrás recuperar tu contraseña
send: Enviar
return: Volver
</i18n>

View File

@ -39,10 +39,11 @@ export default route(function (/* { store, ssrContext } */) {
Router.beforeEach((to, from, next) => { Router.beforeEach((to, from, next) => {
const userStore = useUserStore(); const userStore = useUserStore();
const allowedRoutes = ['login', 'recoverPassword'];
if ( if (
!userStore.storage.getItem('token') && !userStore.storage.getItem('token') &&
to.name !== 'login' && !allowedRoutes.includes(to.name) &&
!userStore.isGuest !userStore.isGuest
) { ) {
return next({ name: 'login' }); return next({ name: 'login' });

View File

@ -5,17 +5,17 @@ const routes = [
children: [ children: [
{ {
name: 'login', name: 'login',
path: '/login/:email?', path: '',
component: () => import('pages/Login/LoginView.vue') component: () => import('pages/Login/LoginView.vue')
}, },
{ {
name: 'rememberPassword', name: 'recoverPassword',
path: '/remember-password', path: 'recover',
component: () => import('pages/Login/RememberPassword.vue') component: () => import('pages/Login/RecoverPassword.vue')
}, },
{ {
name: 'resetPassword', name: 'resetPassword',
path: '/reset-password', path: 'reset',
component: () => import('pages/Login/ResetPassword.vue') component: () => import('pages/Login/ResetPassword.vue')
} }
] ]

View File

@ -20,12 +20,7 @@ export const useAppStore = defineStore('hedera', {
menuEssentialLinks: [], menuEssentialLinks: [],
hiddenMenuLinks: new Set(['Reports']), hiddenMenuLinks: new Set(['Reports']),
basketOrderId: null, basketOrderId: null,
localeDates: {
days: [],
months: [],
daysShort: [],
monthsShort: []
},
siteLang: null, siteLang: null,
localeOptions: [ localeOptions: [
{ label: t('langs.en'), lang: 'en-US', value: 'en' }, { label: t('langs.en'), lang: 'en-US', value: 'en' },
@ -66,20 +61,9 @@ export const useAppStore = defineStore('hedera', {
this.$patch({ imageUrl }); this.$patch({ imageUrl });
}, },
getLocaleDates() {
const { messages, locale } = i18n.global;
this.localeDates = {
days: messages.value[locale.value].date.days,
months: messages.value[locale.value].date.months,
daysShort: messages.value[locale.value].date.daysShort,
monthsShort: messages.value[locale.value].date.monthsShort
};
},
async init() { async init() {
this.updateSiteLocale(localStorage.getItem('siteLang') || 'es-ES'); this.updateSiteLocale(localStorage.getItem('siteLang') || 'es-ES');
this.getBasketOrderId(); this.getBasketOrderId();
this.getLocaleDates();
}, },
getBasketOrderId() { getBasketOrderId() {
@ -187,6 +171,12 @@ export const useAppStore = defineStore('hedera', {
isDesktop() { isDesktop() {
const $q = useQuasar(); const $q = useQuasar();
return $q?.screen?.width > 1024; return $q?.screen?.width > 1024;
},
localeDates() {
const { messages, locale } = i18n.global;
const { days, months, daysShort, monthsShort } =
jsegarra marked this conversation as resolved Outdated

dale una vuelta porque esto se puede simplificar mucho

dale una vuelta porque esto se puede simplificar mucho

Simplificado

Simplificado
messages.value[locale.value].date;
return { days, months, daysShort, monthsShort };
} }
} }
}); });

View File

@ -0,0 +1,18 @@
describe('Login Tests', () => {
beforeEach(() => {
cy.visit('/#/login');
});
it('should ssend recover email', () => {
cy.dataCy('recoverPasswordViewLink').should('exist');
cy.dataCy('recoverPasswordViewLink').click();
cy.dataCy('recoverPasswordUserInput').find('input').should('exist');
cy.dataCy('recoverPasswordUserInput').find('input').type('developer');
cy.dataCy('recoverPasswordSubmitButton').should('exist');
cy.dataCy('recoverPasswordSubmitButton').click();
cy.checkNotify(
'positive',
'Te hemos enviado un correo donde podrás recuperar tu contraseña'
);
});
});