Modulo Administración #78

Merged
jsegarra merged 19 commits from wbuezas/hedera-web-mindshore:feature/Administracion into 4922-vueMigration 2024-08-23 19:29:46 +00:00
14 changed files with 526 additions and 43 deletions
Showing only changes of commit 2fb892c71a - Show all commits

View File

@ -130,24 +130,28 @@ onMounted(async () => {
v-model="formData.newPassword" v-model="formData.newPassword"
:type="!showNewPwd ? 'password' : 'text'" :type="!showNewPwd ? 'password' : 'text'"
:label="t('newPassword')" :label="t('newPassword')"
><template #append> >
<template #append>
<QIcon <QIcon
:name="showNewPwd ? 'visibility_off' : 'visibility'" :name="showNewPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer" class="cursor-pointer"
@click="showNewPwd = !showNewPwd" @click="showNewPwd = !showNewPwd"
/> </template />
></VnInput> </template>
</VnInput>
<VnInput <VnInput
v-model="repeatPassword" v-model="repeatPassword"
:type="!showCopyPwd ? 'password' : 'text'" :type="!showCopyPwd ? 'password' : 'text'"
:label="t('repeatPassword')" :label="t('repeatPassword')"
><template #append> >
<template #append>
<QIcon <QIcon
:name="showCopyPwd ? 'visibility_off' : 'visibility'" :name="showCopyPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer" class="cursor-pointer"
@click="showCopyPwd = !showCopyPwd" @click="showCopyPwd = !showCopyPwd"
/> </template />
></VnInput> </template>
</VnInput>
</template> </template>
<template v-if="isHeaderMounted" #actions> <template v-if="isHeaderMounted" #actions>
<QBtn <QBtn

View File

@ -0,0 +1,121 @@
<script setup>
import { ref, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import useNotify from 'src/composables/useNotify.js';
const props = defineProps({
schema: {
type: String,
default: ''
},
imageName: {
type: String,
default: ''
}
});
const emit = defineEmits(['close']);
const api = inject('api');
const { t } = useI18n();
const { notify } = useNotify();
const inputFileRef = ref(null);
const loading = ref(false);
const name = ref(props.imageName ?? '');
const file = ref(null);
const onSubmit = async () => {
try {
loading.value = true;
const formData = new FormData();
formData.append('name', name.value);
formData.append('image', file.value);
formData.append('schema', props.schema);
formData.append('srv', 'json:image/upload');
await api({
method: 'post',
url: location.origin,
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
});
notify(t('imageAdded'), 'positive');
emit('close');
} catch (error) {
console.error('Error uploading image:', error);
} finally {
loading.value = false;
}
};
</script>
<template>
<QForm @submit="onSubmit">
<QCard class="q-pa-lg">
<VnInput v-model="name" :label="t('name')" />
<QFile
ref="inputFileRef"
:label="t('file')"
v-model="file"
:multiple="false"
class="q-mb-xs"
>
<template #append>
<QIcon
name="attach_file"
class="cursor-pointer"
@click="inputFileRef.pickFiles()"
/>
</template>
</QFile>
<div class="flex row justify-end q-gutter-x-sm">
<QSpinner
v-if="loading"
color="primary"
size="3em"
:thickness="2"
/>
<QBtn
v-else
type="submit"
:label="t('send')"
flat
class="q-mt-md"
rounded
/>
</div>
</QCard>
</QForm>
</template>
<style lang="scss" scoped></style>
<i18n lang="yaml">
en-US:
name: Name
file: File
send: Send
es-ES:
name: Nombre
file: Archivo
send: Enviar
ca-ES:
name: Nom
file: Arxiu
send: Enviar
fr-FR:
name: Nom
file: Fichier
send: Envoyer
pt-PT:
name: Nome
file: Arquivo
send: Enviar
</i18n>

View File

@ -2,6 +2,8 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useAppStore } from 'stores/app'; import { useAppStore } from 'stores/app';
import ImageEditor from 'src/components/ui/ImageEditor.vue';
const props = defineProps({ const props = defineProps({
baseURL: { baseURL: {
type: String, type: String,
@ -27,23 +29,67 @@ const props = defineProps({
rounded: { rounded: {
type: Boolean, type: Boolean,
default: false default: false
},
fullRounded: {
type: Boolean,
default: false
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '100%'
},
editable: {
type: Boolean,
default: false
},
editSchema: {
type: String,
default: ''
},
editImageName: {
type: String,
default: ''
} }
}); });
const app = useAppStore(); const app = useAppStore();
const show = ref(false); const showZoom = ref(false);
const showEditForm = ref(false);
const url = computed(() => { const url = computed(() => {
return `${props.baseURL ?? app.imageUrl}/${props.storage}/${props.size}/${props.id}`; return `${props.baseURL ?? app.imageUrl}/${props.storage}/${props.size}/${props.id}`;
}); });
</script> </script>
<template> <template>
<QImg <div class="relative-position main-image-container">
:class="{ zoomIn: props.zoomSize, rounded: props.rounded }" <QBtn
:src="url" v-if="props.editable"
v-bind="$attrs" icon="add_a_photo"
@click="show = !show" class="show-edit-button absolute-top-left"
spinner-color="primary" round
/> text-color="black"
<QDialog v-model="show" v-if="props.zoomSize"> @click.stop="showEditForm = !showEditForm"
/>
<QImg
:class="{
zoomIn: props.zoomSize,
rounded: props.rounded,
'full-rounded': props.fullRounded
}"
class="main-image"
:src="url"
v-bind="$attrs"
@click="showZoom = !showZoom"
spinner-color="primary"
:width="props.width"
:height="props.height"
/>
</div>
<QDialog v-if="props.zoomSize" v-model="showZoom">
<QImg <QImg
:src="url" :src="url"
size="full" size="full"
@ -52,17 +98,47 @@ const url = computed(() => {
spinner-color="primary" spinner-color="primary"
/> />
</QDialog> </QDialog>
<QDialog v-if="props.editable" v-model="showEditForm">
<ImageEditor
class="all-pointer-events"
:schema="props.editSchema"
:imageName="props.editImageName"
@close="showEditForm = false"
/>
</QDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-img { .main-image-container {
&:hover {
.show-edit-button {
visibility: visible !important;
}
.main-image {
filter: brightness(80%);
}
}
}
.main-image {
&.zoomIn { &.zoomIn {
cursor: zoom-in; cursor: zoom-in;
} }
min-width: 50px; min-width: 50px;
} }
.show-edit-button {
visibility: hidden;
cursor: pointer;
background-color: $gray-light;
z-index: 1;
}
.rounded { .rounded {
border-radius: 50%; border-radius: 0.6em;
}
.full-rounded {
border-radius: 50px;
} }
.img_zoom { .img_zoom {
border-radius: 0%; border-radius: 0%;

View File

@ -0,0 +1,91 @@
<script setup>
import { onMounted, ref, inject } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
const props = defineProps({
searchFn: {
type: Function,
default: null
},
placeholder: {
type: String,
default: 'Search'
},
sqlQuery: {
type: String,
default: null
},
searchField: {
type: String,
default: 'search'
}
});
const emit = defineEmits(['onSearch', 'onSearchError']);
const jApi = inject('jApi');
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const searchTerm = ref('');
const search = async () => {
try {
let data = null;
router.replace({
query: searchTerm.value ? { search: searchTerm.value } : {}
});
if (props.sqlQuery) {
data = await jApi.query(props.sqlQuery, {
[props.searchField]: searchTerm.value
});
} else if (props.searchFn) {
data = props.searchFn(searchTerm.value);
}
emit('onSearch', data);
} catch (error) {
console.error('Error searching:', error);
emit('onSearchError');
}
};
onMounted(() => {
if (route.query.search) {
searchTerm.value = route.query.search;
search();
}
});
</script>
<template>
<VnInput
v-model="searchTerm"
@keyup.enter="search()"
:placeholder="props.placeholder || t('search')"
bg-color="white"
isOutlined
:clearable="false"
>
<template #append>
<QIcon name="search" class="cursor-pointer" @click="search()" />
</template>
</VnInput>
</template>
<i18n lang="yaml">
en-US:
search: Search
es-ES:
search: Buscar
ca-ES:
search: Cercar
fr-FR:
search: Rechercher
pt-PT:
search: Pesquisar
</i18n>

View File

@ -53,7 +53,12 @@ export default {
checkout: 'Configurar encàrrec', checkout: 'Configurar encàrrec',
controlPanel: 'Panell de control', controlPanel: 'Panell de control',
adminConnections: 'Connexions', adminConnections: 'Connexions',
adminItems: 'Articles',
adminVisits: 'Visites',
adminUsers: "Gestió d'usuaris",
adminPhotos: 'Imatges',
// //
orderLoadedIntoBasket: 'Comanda carregada a la cistella!', orderLoadedIntoBasket: 'Comanda carregada a la cistella!',
at: 'a les' at: 'a les',
imageAdded: 'Imatge afegida correctament'
}; };

View File

@ -66,9 +66,14 @@ export default {
checkout: 'Configure order', checkout: 'Configure order',
controlPanel: 'Control panel', controlPanel: 'Control panel',
adminConnections: 'Connections', adminConnections: 'Connections',
adminItems: 'Items',
adminVisits: 'Visits',
adminUsers: 'User management',
adminPhotos: 'Images',
// //
orderLoadedIntoBasket: 'Order loaded into basket!', orderLoadedIntoBasket: 'Order loaded into basket!',
at: 'at', at: 'at',
imageAdded: 'Image added successfully',
orders: 'Orders', orders: 'Orders',
order: 'Pending order', order: 'Pending order',

View File

@ -72,6 +72,10 @@ export default {
checkout: 'Configurar pedido', checkout: 'Configurar pedido',
controlPanel: 'Panel de control', controlPanel: 'Panel de control',
adminConnections: 'Conexiones', adminConnections: 'Conexiones',
adminItems: 'Artículos',
adminVisits: 'Visitas',
adminUsers: 'Gestión de usuarios',
adminPhotos: 'Imágenes',
// //
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!', orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
at: 'a las', at: 'a las',

View File

@ -53,7 +53,12 @@ export default {
checkout: "Définissez l'ordre", checkout: "Définissez l'ordre",
controlPanel: 'Panneau de configuration', controlPanel: 'Panneau de configuration',
adminConnections: 'Connexions', adminConnections: 'Connexions',
adminItems: 'Articles',
adminVisits: 'Visites',
adminUsers: 'Gestion des utilisateurs',
adminPhotos: 'Images',
// //
orderLoadedIntoBasket: 'Commande chargée dans le panier!', orderLoadedIntoBasket: 'Commande chargée dans le panier!',
at: 'à' at: 'à',
imageAdded: 'Image ajoutée correctement'
}; };

View File

@ -54,7 +54,12 @@ export default {
checkout: 'Configurar encomenda', checkout: 'Configurar encomenda',
controlPanel: 'Painel de controle', controlPanel: 'Painel de controle',
adminConnections: 'Conexões', adminConnections: 'Conexões',
adminItems: 'Artigos',
adminVisits: 'Visitas',
adminUsers: 'Gestão de usuários',
adminPhotos: 'Imagens',
// //
orderLoadedIntoBasket: 'Pedido carregado na cesta!', orderLoadedIntoBasket: 'Pedido carregado na cesta!',
at: 'às' at: 'às',
imageAdded: 'Imagen adicionada corretamente'
}; };

View File

@ -50,6 +50,7 @@ const supplantUser = async user => {
console.error('Error supplanting user:', error); console.error('Error supplanting user:', error);
} }
}; };
onMounted(async () => { onMounted(async () => {
getConnections(); getConnections();
intervalId.value = setInterval(getConnections, 60000); intervalId.value = setInterval(getConnections, 60000);

View File

@ -0,0 +1,100 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import CardList from 'src/components/ui/CardList.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import VnSearchBar from 'src/components/ui/VnSearchBar.vue';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const { t } = useI18n();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const loading = ref(false);
const items = ref([]);
const query = `SELECT i.id, i.longName, i.size, i.category,
i.value5, i.value6, i.value7,
i.image, im.updated
FROM vn.item i
LEFT JOIN image im
ON im.collectionFk = 'catalog'
AND im.name = i.image
WHERE i.longName LIKE CONCAT('%', #search, '%')
OR i.id = #search
ORDER BY i.longName LIMIT 50`;
const onSearch = data => (items.value = data || []);
</script>
<template>
<Teleport v-if="isHeaderMounted" to="#actions">
<VnSearchBar
:sqlQuery="query"
@onSearch="onSearch"
@onSearchError="items = []"
/>
</Teleport>
<QPage class="vn-w-xs">
<QList class="flex justify-center">
<span v-if="!loading && !items.length" class="flex items-center">
<QIcon name="refresh" size="sm" class="q-mr-sm" />
{{ t('introduceSearchTerm') }}
</span>
<QSpinner
v-if="loading"
color="primary"
size="3em"
:thickness="2"
/>
<CardList
v-else
v-for="(item, index) in items"
:key="index"
:clickable="false"
>
<template #prepend>
<VnImg
storage="catalog"
size="200x200"
:id="item.id"
width="80px"
height="80px"
class="q-mr-md"
rounded
editable
editSchema="catalog"
:editImageName="item.image"
/>
</template>
<template #content>
<span class="text-bold q-mb-sm">
{{ item.longName }}
</span>
<span>
{{ item.value5 }} {{ item.value6 }}
{{ item.value7 }}
</span>
<span>{{ item.id }}</span>
<span>{{ item.image }}</span>
</template>
</CardList>
</QList>
</QPage>
</template>
<i18n lang="yaml">
en-US:
introduceSearchTerm: Enter a search term
es-ES:
introduceSearchTerm: Introduce un término de búsqueda
ca-ES:
introduceSearchTerm: Introdueix un terme de cerca
fr-FR:
introduceSearchTerm: Entrez un terme de recherche
pt-PT:
introduceSearchTerm: Digite um termo de pesquisa
</i18n>

View File

View File

@ -1,36 +1,102 @@
<script setup> <script setup>
import { ref, onMounted, inject } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
const jApi = inject('jApi'); import CardList from 'src/components/ui/CardList.vue';
import VnSearchBar from 'src/components/ui/VnSearchBar.vue';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
import { useUserStore } from 'stores/user';
import useNotify from 'src/composables/useNotify.js';
const { t } = useI18n();
const router = useRouter();
const userStore = useUserStore();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const { notify } = useNotify();
const loading = ref(false);
const users = ref([]); const users = ref([]);
const getUsers = async () => { const query = `SELECT u.id, u.name, u.nickname, u.active
FROM account.user u
WHERE u.name LIKE CONCAT('%', #user, '%')
OR u.nickname LIKE CONCAT('%', #user, '%')
OR u.id = #user
ORDER BY u.name LIMIT 200`;
const onSearch = data => (users.value = data || []);
const supplantUser = async user => {
try { try {
users.value = await jApi.query( await userStore.supplantUser(user);
`SELECT u.id, u.name, u.nickname, u.active await appStore.getMenuLinks();
FROM account.user u router.push({ name: 'confirmedOrders' });
WHERE u.name LIKE CONCAT('%', #user, '%')
OR u.nickname LIKE CONCAT('%', #user, '%')
OR u.id = #user
ORDER BY u.name LIMIT 200`,
{ user: 9 }
);
} catch (error) { } catch (error) {
console.error('Error getting users:', error); console.error('Error supplanting user:', error);
notify(error.message, 'negative');
} }
}; };
onMounted(async () => getUsers());
</script> </script>
<template> <template>
<QPage> <Teleport v-if="isHeaderMounted" to="#actions">
<VnSearchBar
:sqlQuery="query"
searchField="user"
@onSearch="onSearch"
@onSearchError="users = []"
/>
</Teleport>
<QPage class="vn-w-xs">
<QList class="flex justify-center"> <QList class="flex justify-center">
<!-- TODO: WIP --> <span v-if="!loading && !users.length" class="flex items-center">
<QIcon name="refresh" size="sm" class="q-mr-sm" />
{{ t('noData') }}
</span>
<QSpinner
v-if="loading"
color="primary"
size="3em"
:thickness="2"
/>
<CardList
v-else
v-for="(user, index) in users"
:key="index"
:clickable="false"
>
<template #content>
<span class="text-bold q-mb-sm">
{{ user.nickname }}
</span>
<span>#{{ user.id }} - {{ user.name }} </span>
</template>
<template #actions>
<QBtn
icon="people"
Review

tooltip

tooltip
Review
053b9f845787404b46118f2a99ebacd3602f753e
flat
rounded
@click="supplantUser(user.name)"
/>
</template>
</CardList>
</QList> </QList>
</QPage> </QPage>
</template> </template>
<style lang="scss" scoped></style> <i18n lang="yaml">
en-US:
<i18n lang="yaml"></i18n> noData: No data
es-ES:
noData: Sin datos
ca-ES:
noData: Sense dades
fr-FR:
noData: Aucune donnée
pt-PT:
noData: Sem dados
</i18n>

View File

@ -91,13 +91,13 @@ const routes = [
}, },
{ {
name: 'adminPhotos', name: 'adminPhotos',
path: 'admin/photos' path: 'admin/photos',
// component: () => import('pages/Admin/PhotosView.vue') component: () => import('pages/Admin/PhotosView.vue')
}, },
{ {
name: 'adminItems', name: 'adminItems',
path: 'admin/items' path: 'admin/items',
// component: () => import('pages/Admin/ItemsView.vue') component: () => import('pages/Admin/ItemsView.vue')
} }
] ]
}, },