0
1
Fork 0
This commit is contained in:
Javier Segarra 2024-12-11 11:51:59 +01:00
commit d6f1c8dc04
52 changed files with 405 additions and 313 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

@ -240,7 +240,7 @@ defineExpose({
flat flat
:disabled="!showBottomActions && !updatedColumns.length" :disabled="!showBottomActions && !updatedColumns.length"
@click="submit()" @click="submit()"
data-testid="formDefaultSaveButton" data-cy="formDefaultSaveButton"
> >
<QTooltip>{{ t('save') }}</QTooltip> <QTooltip>{{ t('save') }}</QTooltip>
</QBtn> </QBtn>

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

@ -91,7 +91,7 @@ async function confirm() {
@click="confirm()" @click="confirm()"
unelevated unelevated
autofocus autofocus
data-testid="confirmDialogButton" data-cy="confirmDialogButton"
/> />
</QCardActions> </QCardActions>
</QCard> </QCard>

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
@ -71,7 +76,7 @@ onMounted(() => {
is-outlined is-outlined
:clearable="false" :clearable="false"
class="searchbar" class="searchbar"
data-testid="searchBar" data-cy="searchBar"
> >
<template #prepend> <template #prepend>
<QIcon name="search" class="cursor-pointer" @click="search()" /> <QIcon name="search" class="cursor-pointer" @click="search()" />

View File

@ -14,7 +14,7 @@ export default function useNotify() {
type, type,
icon: icon || defaultIcons[type], icon: icon || defaultIcons[type],
attrs: { attrs: {
'data-testid': `${type}Notify` 'data-cy': `${type}Notify`
} }
}); });
}; };

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
@ -60,7 +60,7 @@ const logoutSupplantedUser = async () => {
class="q-mr-md" class="q-mr-md"
/> />
<img class="logo q-mr-lg" src="statics/logo-dark.svg" /> <img class="logo q-mr-lg" src="statics/logo-dark.svg" />
<QToolbarTitle data-testid="headerTitle"> <QToolbarTitle data-cy="headerTitle">
{{ customTitle || menuTitle }} {{ customTitle || menuTitle }}
<div v-if="subtitle" class="subtitle text-caption"> <div v-if="subtitle" class="subtitle text-caption">
{{ subtitle }} {{ subtitle }}
@ -84,11 +84,11 @@ const logoutSupplantedUser = async () => {
v-model="leftDrawerOpen" v-model="leftDrawerOpen"
:width="250" :width="250"
show-if-above show-if-above
data-testid="layoutMenuDrawer" data-cy="layoutMenuDrawer"
> >
<div class="user-info"> <div class="user-info">
<div> <div>
<span id="user-name" data-testid="layoutUserName"> <span id="user-name" data-cy="layoutUserName">
{{ mainUser?.nickname }} {{ mainUser?.nickname }}
</span> </span>
<QBtn <QBtn
@ -96,7 +96,7 @@ const logoutSupplantedUser = async () => {
icon="logout" icon="logout"
alt="_Exit" alt="_Exit"
@click="logout()" @click="logout()"
data-testid="logoutButton" data-cy="logoutButton"
> >
<QTooltip>{{ $t('logOut') }}</QTooltip> <QTooltip>{{ $t('logOut') }}</QTooltip>
</QBtn> </QBtn>
@ -105,7 +105,7 @@ const logoutSupplantedUser = async () => {
v-if="supplantedUser" v-if="supplantedUser"
id="supplant" id="supplant"
class="supplant" class="supplant"
data-testid="layoutSupplantedUserName" data-cy="layoutSupplantedUserName"
> >
<span id="supplanted"> <span id="supplanted">
{{ supplantedUser?.nickname }} {{ supplantedUser?.nickname }}
@ -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

@ -119,7 +119,7 @@ onMounted(() => fetchLanguagesSql());
:label="t('nickname')" :label="t('nickname')"
@keyup.enter="updateUserNickname(data.nickname)" @keyup.enter="updateUserNickname(data.nickname)"
@blur="updateUserNickname(data.nickname)" @blur="updateUserNickname(data.nickname)"
data-testid="configViewNickname" data-cy="configViewNickname"
/> />
<VnSelect <VnSelect
v-model="data.lang" v-model="data.lang"
@ -128,7 +128,7 @@ onMounted(() => fetchLanguagesSql());
option-value="code" option-value="code"
:options="langOptions" :options="langOptions"
@update:model-value="updateConfigLang(data.lang)" @update:model-value="updateConfigLang(data.lang)"
data-testid="configViewLang" data-cy="configViewLang"
/> />
</template> </template>
<template #extraForm> <template #extraForm>

View File

@ -93,36 +93,36 @@ onMounted(() => getCountries());
<VnInput <VnInput
v-model="data.nickname" v-model="data.nickname"
:label="t('name')" :label="t('name')"
data-testid="addressFormNickname" data-cy="addressFormNickname"
/> />
<VnInput <VnInput
v-model="data.street" v-model="data.street"
:label="t('address')" :label="t('address')"
data-testid="addressFormStreet" data-cy="addressFormStreet"
/> />
<VnInput <VnInput
v-model="data.city" v-model="data.city"
:label="t('city')" :label="t('city')"
data-testid="addressFormCity" data-cy="addressFormCity"
/> />
<VnInput <VnInput
v-model="data.postalCode" v-model="data.postalCode"
type="number" type="number"
:label="t('postalCode')" :label="t('postalCode')"
data-testid="addressFormPostcode" data-cy="addressFormPostcode"
/> />
<VnSelect <VnSelect
v-model="data.countryFk" v-model="data.countryFk"
:label="t('country')" :label="t('country')"
:options="countriesOptions" :options="countriesOptions"
@update:model-value="data.provinceFk = null" @update:model-value="data.provinceFk = null"
data-testid="addressFormCountry" data-cy="addressFormCountry"
/> />
<VnSelect <VnSelect
v-model="data.provinceFk" v-model="data.provinceFk"
:label="t('province')" :label="t('province')"
:options="provincesOptions" :options="provincesOptions"
data-testid="addressFormProvince" data-cy="addressFormProvince"
/> />
</template> </template>
</VnForm> </VnForm>

View File

@ -98,7 +98,7 @@ onMounted(async () => {
@click="goToAddressDetails()" @click="goToAddressDetails()"
rounded rounded
no-caps no-caps
data-testid="newAddressBtn" data-cy="newAddressBtn"
> >
<QTooltip> <QTooltip>
{{ t('addAddress') }} {{ t('addAddress') }}
@ -110,7 +110,7 @@ onMounted(async () => {
class="rounded-borders shadow-1 shadow-transition" class="rounded-borders shadow-1 shadow-transition"
separator separator
:rows="addresses" :rows="addresses"
data-testid="addressCardList" data-cy="addressCardList"
> >
<CardList <CardList
v-for="(address, index) in addresses" v-for="(address, index) in addresses"
@ -158,7 +158,7 @@ onMounted(async () => {
flat flat
rounded rounded
@click.stop="goToAddressDetails(address.id)" @click.stop="goToAddressDetails(address.id)"
data-testid="editAddressBtn" data-cy="editAddressBtn"
> >
<QTooltip> <QTooltip>
{{ t('editAddress') }} {{ t('editAddress') }}

View File

@ -44,13 +44,13 @@ const onSearch = data => (items.value = data || []);
empty-icon="refresh" empty-icon="refresh"
:loading="loading" :loading="loading"
:rows="items" :rows="items"
data-testid="itemsViewList" data-cy="itemsViewList"
> >
<CardList <CardList
v-for="(item, index) in items" v-for="(item, index) in items"
:key="index" :key="index"
:clickable="false" :clickable="false"
data-testid="itemsViewCard" data-cy="itemsViewCard"
> >
<template #prepend> <template #prepend>
<VnImg <VnImg

View File

@ -106,7 +106,7 @@ onMounted(async () => {
v-model="data.title" v-model="data.title"
:label="t('title')" :label="t('title')"
:clearable="false" :clearable="false"
data-testid="newsTitleInput" data-cy="newsTitleInput"
/> />
<div class="row justify-between q-gutter-x-md"> <div class="row justify-between q-gutter-x-md">
<VnSelect <VnSelect
@ -116,14 +116,14 @@ onMounted(async () => {
option-value="name" option-value="name"
:options="newsTags" :options="newsTags"
class="col" class="col"
data-testid="newsTagSelect" data-cy="newsTagSelect"
/> />
<VnInput <VnInput
v-model="data.priority" v-model="data.priority"
:label="t('priority')" :label="t('priority')"
:clearable="false" :clearable="false"
class="col" class="col"
data-testid="newsPriorityInput" data-cy="newsPriorityInput"
/> />
</div> </div>
<QEditor <QEditor

View File

@ -64,7 +64,7 @@ onMounted(async () => getNews());
:to="{ name: 'adminNewsDetails' }" :to="{ name: 'adminNewsDetails' }"
rounded rounded
no-caps no-caps
data-testid="addNewBtn" data-cy="addNewBtn"
> >
<QTooltip>{{ t('addNew') }}</QTooltip> <QTooltip>{{ t('addNew') }}</QTooltip>
</QBtn> </QBtn>
@ -75,7 +75,7 @@ onMounted(async () => getNews());
v-for="(newsItem, index) in news" v-for="(newsItem, index) in news"
:key="index" :key="index"
:to="{ name: 'adminNewsDetails', params: { id: newsItem.id } }" :to="{ name: 'adminNewsDetails', params: { id: newsItem.id } }"
data-testid="newsCard" data-cy="newsCard"
> >
<template #prepend> <template #prepend>
<VnImg <VnImg
@ -108,7 +108,7 @@ onMounted(async () => getNews());
() => deleteNew(newsItem.id, index) () => deleteNew(newsItem.id, index)
) )
" "
data-testid="deleteNewBtn" data-cy="deleteNewBtn"
> >
<QTooltip>{{ t('remove') }}</QTooltip> <QTooltip>{{ t('remove') }}</QTooltip>
</QBtn> </QBtn>

View File

@ -153,7 +153,7 @@ onMounted(async () => getImageCollections());
option-label="desc" option-label="desc"
option-value="name" option-value="name"
:options="imageCollections" :options="imageCollections"
data-testid="photoCollectionSelect" data-cy="photoCollectionSelect"
/> />
<QUploader <QUploader
ref="fileUploaderRef" ref="fileUploaderRef"
@ -165,19 +165,19 @@ onMounted(async () => getImageCollections());
bordered bordered
hide-upload-btn hide-upload-btn
@added="onFilesAdded" @added="onFilesAdded"
data-testid="photoUploader" data-cy="photoUploader"
> >
<template #list="scope"> <template #list="scope">
<QList <QList
v-if="addedFiles.length" v-if="addedFiles.length"
separator separator
data-testid="photoUploaderList" data-cy="photoUploaderList"
> >
<QItem <QItem
v-for="(file, index) in scope.files" v-for="(file, index) in scope.files"
:key="file.__key" :key="file.__key"
class="flex full-width row items-center justify-center" class="flex full-width row items-center justify-center"
data-testid="photoUploaderItem" data-cy="photoUploaderItem"
> >
<img <img
:src="file.__img.src" :src="file.__img.src"
@ -211,7 +211,7 @@ onMounted(async () => getImageCollections());
].icon ].icon
" "
size="sm" size="sm"
data-testid="photoUploaderItemsStatusIcon" data-cy="photoUploaderItemsStatusIcon"
> >
<QTooltip> <QTooltip>
{{ {{
@ -237,7 +237,7 @@ onMounted(async () => getImageCollections());
round round
icon="delete" icon="delete"
@click="removeFile(file, index)" @click="removeFile(file, index)"
data-testid="photoUploaderItemsDeleteBtn" data-cy="photoUploaderItemsDeleteBtn"
> >
<QTooltip>{{ t('remove') }}</QTooltip> <QTooltip>{{ t('remove') }}</QTooltip>
</QBtn> </QBtn>
@ -257,7 +257,7 @@ onMounted(async () => getImageCollections());
no-caps no-caps
flat flat
@click="clearFiles()" @click="clearFiles()"
data-testid="photoUploaderClearBtn" data-cy="photoUploaderClearBtn"
/> />
<QBtn <QBtn
:label="t('uploadFiles')" :label="t('uploadFiles')"
@ -266,7 +266,7 @@ onMounted(async () => getImageCollections());
flat flat
:disable="!isSubmitable" :disable="!isSubmitable"
@click="onSubmit(data)" @click="onSubmit(data)"
data-testid="photoUploadSubmitBtn" data-cy="photoUploadSubmitBtn"
/> />
</template> </template>
</VnForm> </VnForm>

View File

@ -47,7 +47,7 @@ const supplantUser = async user => {
search-field="user" search-field="user"
@on-search="onSearch" @on-search="onSearch"
@on-search-error="users = []" @on-search-error="users = []"
data-testid="usersViewSearchBar" data-cy="usersViewSearchBar"
/> />
</Teleport> </Teleport>
<QPage class="vn-w-xs"> <QPage class="vn-w-xs">
@ -57,13 +57,13 @@ const supplantUser = async user => {
empty-icon="refresh" empty-icon="refresh"
:loading="loading" :loading="loading"
:rows="users" :rows="users"
data-testid="usersViewList" data-cy="usersViewList"
> >
<CardList <CardList
v-for="(user, index) in users" v-for="(user, index) in users"
:key="index" :key="index"
:to="{ name: 'accessLog', params: { id: user.id } }" :to="{ name: 'accessLog', params: { id: user.id } }"
data-testid="userViewCard" data-cy="userViewCard"
> >
<template #content> <template #content>
<span class="text-bold q-mb-sm"> <span class="text-bold q-mb-sm">
@ -78,7 +78,7 @@ const supplantUser = async user => {
flat flat
rounded rounded
@click.stop.prevent="supplantUser(user.name)" @click.stop.prevent="supplantUser(user.name)"
data-testid="usersViewSupplantUserBtn" data-cy="usersViewSupplantUserBtn"
> >
<QTooltip> <QTooltip>
{{ t('Impersonate user') }} {{ t('Impersonate user') }}

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"
@ -122,7 +122,7 @@ const fetchData = async () => {
:to="{ name: 'confirm', params: { id: orderId } }" :to="{ name: 'confirm', params: { id: orderId } }"
rounded rounded
no-caps no-caps
data-testid="basketToConfirmBtn" data-cy="basketToConfirmBtn"
> >
<QTooltip>{{ t('checkout') }}</QTooltip> <QTooltip>{{ t('checkout') }}</QTooltip>
</QBtn> </QBtn>

View File

@ -19,19 +19,21 @@ const { t } = useI18n();
v-if="viewMode === 'grid'" v-if="viewMode === 'grid'"
v-ripple v-ripple
class="catalog-card" class="catalog-card"
data-testid="catalogCardGrid" data-cy="catalogCardGrid"
> >
<VnImg <VnImg
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"
data-cy="catalogCardImage"
/> />
<div <div
class="column" class="column"
style="height: 205px; padding: 10px" style="height: 205px; padding: 10px"
data-testid="catalogCardGridBody" data-cy="catalogCardGridBody"
> >
<div class="column" style="margin-bottom: auto"> <div class="column" style="margin-bottom: auto">
<div class="text-subtitle2 ellipsis-2-lines"> <div class="text-subtitle2 ellipsis-2-lines">
@ -45,7 +47,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 +77,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 +87,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 +98,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>
@ -107,7 +107,7 @@ const { t } = useI18n();
</div> </div>
</div> </div>
</QCard> </QCard>
<CardList v-else class="vn-w-sm" data-testid="catalogCardList"> <CardList v-else class="vn-w-sm" data-cy="catalogCardList">
<template #prepend> <template #prepend>
<VnImg <VnImg
storage="catalog" storage="catalog"
@ -117,6 +117,8 @@ const { t } = useI18n();
height="105px" height="105px"
rounded-borders="full" rounded-borders="full"
class="q-mr-md" class="q-mr-md"
zoom-size="1600x900"
data-cy="catalogCardImage"
/> />
</template> </template>
<template #content> <template #content>
@ -153,7 +155,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 +166,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 +177,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"
@ -19,7 +26,7 @@
@click="redirectToBasket()" @click="redirectToBasket()"
rounded rounded
no-caps no-caps
data-testid="catalogGoToBasketButton" data-cy="catalogGoToBasketButton"
> >
<QTooltip> <QTooltip>
{{ t('shoppingCart') }} {{ t('shoppingCart') }}
@ -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">
@ -54,7 +61,10 @@
rounded rounded
no-caps no-caps
@click="redirectToCheckout()" @click="redirectToCheckout()"
data-testid="orderModifyButton" data-cy="orderModifyButton"
color="light-green-7"
unelevated
text-color="white"
> >
{{ t('modify') }} {{ t('modify') }}
</QBtn> </QBtn>
@ -80,7 +90,7 @@
:class="{ active: category == cat.id }" :class="{ active: category == cat.id }"
:key="cat.id" :key="cat.id"
@click="selectedCategory = cat.id" @click="selectedCategory = cat.id"
data-testid="catalogCategoryButton" data-cy="catalogCategoryButton"
> >
<img :src="`statics/category/${cat.code}.svg`" /> <img :src="`statics/category/${cat.code}.svg`" />
<QTooltip>{{ cat.name }}</QTooltip> <QTooltip>{{ cat.name }}</QTooltip>
@ -98,7 +108,7 @@
:options="itemFamilies" :options="itemFamilies"
:disable="!category" :disable="!category"
:label="t('family')" :label="t('family')"
data-testid="catalogFamilySelect" data-cy="catalogFamilySelect"
/> />
<VnSelect <VnSelect
v-model="selectedColor" v-model="selectedColor"
@ -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
@ -177,16 +192,17 @@
:item="_item" :item="_item"
:view-mode="viewMode" :view-mode="viewMode"
@click="showItem(_item)" @click="showItem(_item)"
data-cy="catalogCardElement"
/> />
</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"
@ -251,7 +267,7 @@
flat flat
dense dense
@click="onAddLotClick(lot)" @click="onAddLotClick(lot)"
data-testid="addItemQuantityButton" data-cy="addItemQuantityButton"
> >
<QTooltip>{{ t('add') }}</QTooltip> <QTooltip>{{ t('add') }}</QTooltip>
</QBtn> </QBtn>
@ -276,12 +292,13 @@
flat flat
color="white" color="white"
@click="onConfirmClick()" @click="onConfirmClick()"
data-testid="catalogAddToBasketButton" data-cy="catalogAddToBasketButton"
> >
<QTooltip>{{ t('confirm') }}</QTooltip> <QTooltip>{{ t('confirm') }}</QTooltip>
</QBtn> </QBtn>
</div> </div>
</QCard> </QCard>
<QSpinner v-else color="primary" size="3em" :thickness="5" />
</QDialog> </QDialog>
</div> </div>
</template> </template>
@ -744,7 +761,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);
} }
@ -753,11 +771,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 +868,7 @@ const onAddLotClick = async lot => {
}; };
const resetAmounts = () => { const resetAmounts = () => {
selectedItem.value = null;
addedItemsAmountAcc.value = {}; addedItemsAmountAcc.value = {};
amount.value = 0; amount.value = 0;
}; };
@ -1031,6 +1052,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 +1077,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 +1100,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 +1123,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 +1146,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
);
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>
@ -322,7 +350,7 @@ onMounted(async () => {
:flat="isMobile" :flat="isMobile"
contracted contracted
class="default-radius stepper-container" class="default-radius stepper-container"
data-testid="checkoutStepper" data-cy="checkoutStepper"
> >
<QStep <QStep
v-for="(step, stepIndex) in steps[orderForm.method]" v-for="(step, stepIndex) in steps[orderForm.method]"
@ -375,7 +403,7 @@ onMounted(async () => {
<QList <QList
v-if="step.name === 'address'" v-if="step.name === 'address'"
class="vn-w-xs q-gutter-y-sm column" class="vn-w-xs q-gutter-y-sm column"
data-testid="checkoutAddressStep" data-cy="checkoutAddressStep"
> >
<span class="text-h6 step-title"> <span class="text-h6 step-title">
{{ {{
@ -419,7 +447,7 @@ onMounted(async () => {
option-label="description" option-label="description"
option-value="id" option-value="id"
:options="agencies" :options="agencies"
data-testid="agencyStepSelect" data-cy="agencyStepSelect"
/> />
</div> </div>
<div <div
@ -434,7 +462,7 @@ onMounted(async () => {
option-label="description" option-label="description"
option-value="id" option-value="id"
:options="warehouses" :options="warehouses"
data-testid="pickupStepSelect" data-cy="pickupStepSelect"
/> />
</div> </div>
<!-- Confirm step --> <!-- Confirm step -->
@ -461,14 +489,14 @@ onMounted(async () => {
icon="arrow_back" icon="arrow_back"
dense dense
class="left-navigation-button" class="left-navigation-button"
data-testid="checkoutStepperLeftButton" data-cy="checkoutStepperLeftButton"
> >
<QTooltip> <QTooltip>
{{ t(`${step.backButtonLabel || 'back'}`) }} {{ t(`${step.backButtonLabel || 'back'}`) }}
</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="
@ -476,7 +504,8 @@ onMounted(async () => {
" "
dense dense
class="right-navigation-button" class="right-navigation-button"
data-testid="checkoutStepperRightButton" data-cy="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

@ -73,7 +73,7 @@ const onConfirmPay = async () => {
@click="onPayClick()" @click="onPayClick()"
rounded rounded
no-caps no-caps
data-testid="makePaymentButton" data-cy="makePaymentButton"
> >
<QTooltip> <QTooltip>
{{ t('makePayment') }} {{ t('makePayment') }}
@ -126,7 +126,7 @@ const onConfirmPay = async () => {
v-model="showAmountToPayDialog" v-model="showAmountToPayDialog"
message=" " message=" "
:promise="onConfirmPay" :promise="onConfirmPay"
data-testid="payAmountDialog" data-cy="payAmountDialog"
> >
<template #customHTML> <template #customHTML>
<VnInput <VnInput
@ -136,7 +136,7 @@ const onConfirmPay = async () => {
type="number" type="number"
min="0" min="0"
:max="debt * -1" :max="debt * -1"
data-testid="payAmountInput" data-cy="payAmountInput"
> >
<template #append></template> <template #append></template>
</VnInput> </VnInput>

View File

@ -71,7 +71,7 @@ onMounted(async () => {
<template> <template>
<Teleport v-if="isHeaderMounted" to="#actions"> <Teleport v-if="isHeaderMounted" to="#actions">
<QBtn <QBtn
data-testid="pendingOrdersNewOrder" data-cy="pendingOrdersNewOrder"
:to="{ name: 'checkout' }" :to="{ name: 'checkout' }"
icon="add_shopping_cart" icon="add_shopping_cart"
:label="t('newOrder')" :label="t('newOrder')"
@ -84,16 +84,12 @@ onMounted(async () => {
</QBtn> </QBtn>
</Teleport> </Teleport>
<QPage class="vn-w-sm"> <QPage class="vn-w-sm">
<VnList <VnList :rows="orders" :loading="loading" data-cy="pendingOrdersList">
:rows="orders"
:loading="loading"
data-testid="pendingOrdersList"
>
<CardList <CardList
v-for="(order, index) in orders" v-for="(order, index) in orders"
:key="index" :key="index"
:to="{ name: 'basket', params: { id: order.id } }" :to="{ name: 'basket', params: { id: order.id } }"
data-testid="pendingOrderCard" data-cy="pendingOrderCard"
> >
<template #content> <template #content>
<QItemLabel class="text-bold q-mb-sm"> <QItemLabel class="text-bold q-mb-sm">
@ -116,7 +112,7 @@ onMounted(async () => {
() => removeOrder(order.id, index) () => removeOrder(order.id, index)
) )
" "
data-testid="pendingOrderCardDelete" data-cy="pendingOrderCardDelete"
> >
<QTooltip>{{ t('deleteOrder') }}</QTooltip> <QTooltip>{{ t('deleteOrder') }}</QTooltip>
</QBtn> </QBtn>
@ -125,7 +121,7 @@ onMounted(async () => {
flat flat
rounded rounded
@click.stop.prevent="loadOrder(order.id)" @click.stop.prevent="loadOrder(order.id)"
data-testid="addOrderToBasket" data-cy="addOrderToBasket"
> >
<QTooltip>{{ t('loadOrderIntoCart') }}</QTooltip> <QTooltip>{{ t('loadOrderIntoCart') }}</QTooltip>
</QBtn> </QBtn>

View File

@ -111,7 +111,7 @@ const deleteRow = id => {
</QCardSection> </QCardSection>
<QSeparator v-if="showItems" inset /> <QSeparator v-if="showItems" inset />
<QList v-for="(row, index) in rows" :key="index"> <QList v-for="(row, index) in rows" :key="index">
<QItem v-if="row" data-testid="basketItemRow"> <QItem v-if="row" data-cy="basketItemRow">
<QItemSection v-if="canDeleteItems" avatar> <QItemSection v-if="canDeleteItems" avatar>
<QBtn <QBtn
icon="delete" icon="delete"
@ -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

@ -73,17 +73,17 @@ const loginAsGuest = async () => {
v-model="email" v-model="email"
:label="$t('user')" :label="$t('user')"
autofocus autofocus
data-testid="loginUserInput" data-cy="loginUserInput"
/> />
<QInput <QInput
v-model="password" v-model="password"
:label="$t('password')" :label="$t('password')"
:type="!showPwd ? 'password' : 'text'" :type="!showPwd ? 'password' : 'text'"
data-testid="loginPasswordInput" data-cy="loginPasswordInput"
> >
<template #append> <template #append>
<QIcon <QIcon
data-testid="showPasswordIcon" data-cy="showPasswordIcon"
:name="showPwd ? 'visibility_off' : 'visibility'" :name="showPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer" class="cursor-pointer"
@click="showPwd = !showPwd" @click="showPwd = !showPwd"
@ -92,14 +92,14 @@ const loginAsGuest = async () => {
</QInput> </QInput>
<div class="row justify-between text-center"> <div class="row justify-between text-center">
<QCheckbox <QCheckbox
data-testid="rememberCheckbox" data-cy="rememberCheckbox"
v-model="remember" v-model="remember"
:label="$t('remindMe')" :label="$t('remindMe')"
dense dense
class="col" class="col"
/> />
<QSelect <QSelect
data-testid="switchLanguage" data-cy="switchLanguage"
v-model="selectedLocaleValue" v-model="selectedLocaleValue"
:options="localeOptions" :options="localeOptions"
:label="t('language')" :label="t('language')"
@ -126,7 +126,7 @@ const loginAsGuest = async () => {
</div> </div>
<div class="justify-center"> <div class="justify-center">
<QBtn <QBtn
data-testid="loginAsGuestButton" data-cy="loginAsGuestButton"
@click="loginAsGuest()" @click="loginAsGuest()"
:label="$t('logInAsGuest')" :label="$t('logInAsGuest')"
class="full-width" class="full-width"
@ -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-cy="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-cy="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-cy="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 } =
messages.value[locale.value].date;
return { days, months, daysShort, monthsShort };
} }
} }
}); });

View File

@ -29,7 +29,7 @@ describe('NewsView', () => {
cy.dataCy('newsTitleInput').should('exist'); cy.dataCy('newsTitleInput').should('exist');
cy.dataCy('newsTitleInput').find('input').type('Test new'); cy.dataCy('newsTitleInput').find('input').type('Test new');
cy.dataCy('newsTagSelect').should('exist'); cy.dataCy('newsTagSelect').should('exist');
cy.selectOption('[data-testid="newsTagSelect"]', 'Curso'); cy.selectOption('[data-cy="newsTagSelect"]', 'Curso');
cy.dataCy('newsPriorityInput').should('exist'); cy.dataCy('newsPriorityInput').should('exist');
cy.dataCy('newsPriorityInput').find('input').type('2'); cy.dataCy('newsPriorityInput').find('input').type('2');
cy.dataCy('formDefaultSaveButton').should('not.be.disabled'); cy.dataCy('formDefaultSaveButton').should('not.be.disabled');

View File

@ -15,8 +15,8 @@ describe('Photo Uploader Component', () => {
it('should allow selecting a photo collection', () => { it('should allow selecting a photo collection', () => {
// Simular la selección de una colección de fotos // Simular la selección de una colección de fotos
cy.selectOption('[data-testid="photoCollectionSelect"]', 'Enlace'); cy.selectOption('[data-cy="photoCollectionSelect"]', 'Enlace');
cy.getValue('[data-testid="photoCollectionSelect"]').should( cy.getValue('[data-cy="photoCollectionSelect"]').should(
'equal', 'equal',
'Enlace' 'Enlace'
); );

View File

@ -1,21 +1,18 @@
Cypress.Commands.add('addItemToBasketFlow', () => { Cypress.Commands.add('addItemToBasketFlow', () => {
// 1- Seleccionar categoría // 1- Seleccionar categoría
cy.dataCy('catalogCategoryButton').should('exist'); cy.dataCy('catalogCategoryButton').should('exist');
cy.get('[data-testid="catalogCategoryButton"]:first').click(); cy.get('[data-cy="catalogCategoryButton"]:first').click();
// 2- Seleccionar familia // 2- Seleccionar familia
cy.dataCy('catalogFamilySelect').should('exist'); cy.dataCy('catalogFamilySelect').should('exist');
cy.selectOption('[data-testid="catalogFamilySelect"]', 'Anthurium'); cy.selectOption('[data-cy="catalogFamilySelect"]', 'Anthurium');
cy.getValue('[data-testid="catalogFamilySelect"]').should( cy.getValue('[data-cy="catalogFamilySelect"]').should('equal', 'Anthurium');
'equal',
'Anthurium'
);
cy.dataCy('catalogFamilySelect').should('exist'); cy.dataCy('catalogFamilySelect').should('exist');
// 3- Seleccionar item // 3- Seleccionar item
cy.dataCy('catalogCardGridBody').should('exist'); cy.dataCy('catalogCardGridBody').should('exist');
cy.get('[data-testid="catalogCardGridBody"]:first').click(); cy.get('[data-cy="catalogCardGridBody"]:first').click();
// 4- Añadir item al carrito // 4- Añadir item al carrito
cy.dataCy('addItemQuantityButton').should('exist'); cy.dataCy('addItemQuantityButton').should('exist');
cy.get('[data-testid="addItemQuantityButton"]:first').click(); cy.get('[data-cy="addItemQuantityButton"]:first').click();
cy.dataCy('catalogAddToBasketButton').should('exist'); cy.dataCy('catalogAddToBasketButton').should('exist');
cy.dataCy('catalogAddToBasketButton').click(); cy.dataCy('catalogAddToBasketButton').click();
cy.checkNotify('positive', 'Añadido'); cy.checkNotify('positive', 'Añadido');

View File

@ -42,4 +42,14 @@ describe('CatalogView', () => {
cy.dataCy('catalogGoToBasketButton').click(); cy.dataCy('catalogGoToBasketButton').click();
cy.url().should('contain', '/#/ecomerce/basket'); cy.url().should('contain', '/#/ecomerce/basket');
}); });
it('Open item details and image exists', () => {
// cy.resetDB();
cy.login('developer');
cy.createOrderReceiveFlow();
cy.addItemToBasketFlow();
cy.dataCy('catalogCardElement').first().click();
cy.wait(200);
cy.dataCy('catalogCardImage').find('img').should('exist');
});
}); });

View File

@ -1,5 +1,5 @@
const checkoutNextStep = () => { const checkoutNextStep = () => {
cy.dataCy('checkoutStepperRightButton').should('be.visible').click(); cy.dataCy('checkoutStepperRightButton').last().should('be.visible').click();
}; };
Cypress.Commands.add('createOrderReceive', () => { Cypress.Commands.add('createOrderReceive', () => {
@ -29,7 +29,7 @@ Cypress.Commands.add('createOrderReceive', () => {
'¿Cómo quieres recibir el pedido?' '¿Cómo quieres recibir el pedido?'
); );
cy.dataCy('agencyStepSelect').should('exist'); cy.dataCy('agencyStepSelect').should('exist');
cy.selectOption('[data-testid="agencyStepSelect"]', 'Other agency'); cy.selectOption('[data-cy="agencyStepSelect"]', 'Other agency');
checkoutNextStep(); checkoutNextStep();
checkoutNextStep(); checkoutNextStep();
cy.url().should('contain', '/#/ecomerce/catalog'); cy.url().should('contain', '/#/ecomerce/catalog');
@ -69,7 +69,7 @@ Cypress.Commands.add('createOrderPickup', () => {
'¿En qué almacén quieres recoger tu pedido?' '¿En qué almacén quieres recoger tu pedido?'
); );
cy.dataCy('pickupStepSelect').should('exist'); cy.dataCy('pickupStepSelect').should('exist');
cy.selectOption('[data-testid="pickupStepSelect"]', 'Teleportation device'); cy.selectOption('[data-cy="pickupStepSelect"]', 'Teleportation device');
checkoutNextStep(); checkoutNextStep();
checkoutNextStep(); checkoutNextStep();
cy.url().should('contain', '/#/ecomerce/catalog'); cy.url().should('contain', '/#/ecomerce/catalog');

View File

@ -1,7 +1,7 @@
Cypress.Commands.add('changeUserNickname', (oldNickname, newNickname) => { Cypress.Commands.add('changeUserNickname', (oldNickname, newNickname) => {
cy.dataCy('configViewNickname').find('input').should('exist'); cy.dataCy('configViewNickname').find('input').should('exist');
cy.getValue('input[data-testid="configViewNickname"]').should( cy.getValue('input[data-cy="configViewNickname"]').should(
'equal', 'equal',
oldNickname oldNickname
); );

View File

@ -11,9 +11,9 @@ describe('Changes user nickname', () => {
it('changes site lang when changing user lang', () => { it('changes site lang when changing user lang', () => {
cy.dataCy('configViewLang').should('exist'); cy.dataCy('configViewLang').should('exist');
cy.selectOption('[data-testid="configViewLang"]', 'Español'); cy.selectOption('[data-cy="configViewLang"]', 'Español');
cy.dataCy('headerTitle').should('contain', 'Configuración'); cy.dataCy('headerTitle').should('contain', 'Configuración');
cy.selectOption('[data-testid="configViewLang"]', 'English'); cy.selectOption('[data-cy="configViewLang"]', 'English');
cy.dataCy('headerTitle').should('contain', 'Configuration'); cy.dataCy('headerTitle').should('contain', 'Configuration');
}); });
}); });

View File

@ -23,8 +23,8 @@ describe('PendingOrders', () => {
cy.dataCy('addressFormCity').find('input').type(data.city); cy.dataCy('addressFormCity').find('input').type(data.city);
cy.dataCy('addressFormPostcode').find('input').click(); cy.dataCy('addressFormPostcode').find('input').click();
cy.dataCy('addressFormPostcode').find('input').type(data.postcode); cy.dataCy('addressFormPostcode').find('input').type(data.postcode);
cy.selectOption('[data-testid="addressFormCountry"]', 'España'); cy.selectOption('[data-cy="addressFormCountry"]', 'España');
cy.selectOption('[data-testid="addressFormProvince"]', 'Province one'); cy.selectOption('[data-cy="addressFormProvince"]', 'Province one');
}; };
const verifyAddressCardData = data => { const verifyAddressCardData = data => {
@ -63,7 +63,7 @@ describe('PendingOrders', () => {
cy.dataCy('addressCardList') cy.dataCy('addressCardList')
.children() .children()
.last() .last()
.find('[data-testid="editAddressBtn"]') .find('[data-cy="editAddressBtn"]')
.click(); .click();
// Clear form data // Clear form data
cy.get('form input').each(input => { cy.get('form input').each(input => {

View File

@ -40,12 +40,9 @@ Cypress.Commands.add('logout', user => {
Cypress.Commands.add('loginFlow', (user, visitLogin = true) => { Cypress.Commands.add('loginFlow', (user, visitLogin = true) => {
if (visitLogin) cy.visit('/#/login'); if (visitLogin) cy.visit('/#/login');
cy.dataCy('loginUserInput').type(user); cy.dataCy('loginUserInput').type(user);
cy.getValue('[data-testid="loginUserInput"]').should('equal', user); cy.getValue('[data-cy="loginUserInput"]').should('equal', user);
cy.dataCy('loginPasswordInput').type('nightmare'); cy.dataCy('loginPasswordInput').type('nightmare');
cy.getValue('[data-testid="loginPasswordInput"]').should( cy.getValue('[data-cy="loginPasswordInput"]').should('equal', 'nightmare');
'equal',
'nightmare'
);
cy.get('button[type="submit"]').click(); cy.get('button[type="submit"]').click();
cy.url().should('contain', '/#/cms/home'); cy.url().should('contain', '/#/cms/home');
@ -54,7 +51,7 @@ Cypress.Commands.add('loginFlow', (user, visitLogin = true) => {
Cypress.Commands.add('changeLanguage', language => { Cypress.Commands.add('changeLanguage', language => {
const languagesOrder = ['en', 'es', 'ca', 'fr', 'pt']; const languagesOrder = ['en', 'es', 'ca', 'fr', 'pt'];
const index = languagesOrder.indexOf(language); const index = languagesOrder.indexOf(language);
cy.waitForElement('[data-testid="switchLanguage"]'); cy.waitForElement('[data-cy="switchLanguage"]');
cy.dataCy('switchLanguage').click(); cy.dataCy('switchLanguage').click();
cy.get('.q-menu .q-item').eq(index).click(); // Selecciona y hace clic en el tercer elemento "index" de la lista cy.get('.q-menu .q-item').eq(index).click(); // Selecciona y hace clic en el tercer elemento "index" de la lista
}); });

View File

@ -1,5 +1,5 @@
describe('Login Tests', () => { describe('Login Tests', () => {
const rememberCheckbox = '[data-testid="rememberCheckbox"]'; const rememberCheckbox = '[data-cy="rememberCheckbox"]';
beforeEach(() => { beforeEach(() => {
cy.visit('/#/login'); cy.visit('/#/login');

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'
);
});
});

File diff suppressed because one or more lines are too long

View File

@ -36,6 +36,7 @@ requireCommands.keys().forEach(requireCommands);
// Common commands // Common commands
Cypress.Commands.add('selectOption', (selector, option) => { Cypress.Commands.add('selectOption', (selector, option) => {
cy.waitForElement(selector); cy.waitForElement(selector);
cy.wait(400);
cy.get(selector).click(); cy.get(selector).click();
cy.get('.q-menu .q-item').contains(option).click(); cy.get('.q-menu .q-item').contains(option).click();
}); });
@ -55,7 +56,7 @@ Cypress.Commands.add('waitForElement', (element, timeout = 5000) => {
cy.get(element, { timeout }).should('be.visible'); cy.get(element, { timeout }).should('be.visible');
}); });
Cypress.Commands.add('dataCy', (dataTestId, attr = 'data-testid') => { Cypress.Commands.add('dataCy', (dataTestId, attr = 'data-cy') => {
return cy.get(`[${attr}="${dataTestId}"]`); return cy.get(`[${attr}="${dataTestId}"]`);
}); });
@ -80,7 +81,7 @@ Cypress.Commands.add('setSessionStorage', (key, value) => {
}); });
Cypress.Commands.add('resetDB', () => { Cypress.Commands.add('resetDB', () => {
cy.exec('npm run resetDatabase'); cy.exec('pnpm run db');
}); });
Cypress.Commands.add('setConfirmDialog', () => { Cypress.Commands.add('setConfirmDialog', () => {