Modulo Administración #78
|
@ -130,24 +130,28 @@ onMounted(async () => {
|
|||
v-model="formData.newPassword"
|
||||
:type="!showNewPwd ? 'password' : 'text'"
|
||||
:label="t('newPassword')"
|
||||
><template #append>
|
||||
>
|
||||
<template #append>
|
||||
<QIcon
|
||||
:name="showNewPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="showNewPwd = !showNewPwd"
|
||||
/> </template
|
||||
></VnInput>
|
||||
/>
|
||||
</template>
|
||||
</VnInput>
|
||||
<VnInput
|
||||
v-model="repeatPassword"
|
||||
:type="!showCopyPwd ? 'password' : 'text'"
|
||||
:label="t('repeatPassword')"
|
||||
><template #append>
|
||||
>
|
||||
<template #append>
|
||||
<QIcon
|
||||
:name="showCopyPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="showCopyPwd = !showCopyPwd"
|
||||
/> </template
|
||||
></VnInput>
|
||||
/>
|
||||
</template>
|
||||
</VnInput>
|
||||
</template>
|
||||
<template v-if="isHeaderMounted" #actions>
|
||||
<QBtn
|
||||
|
|
|
@ -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>
|
|
@ -2,6 +2,8 @@
|
|||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from 'stores/app';
|
||||
|
||||
import ImageEditor from 'src/components/ui/ImageEditor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
baseURL: {
|
||||
type: String,
|
||||
|
@ -27,23 +29,67 @@ const props = defineProps({
|
|||
rounded: {
|
||||
type: Boolean,
|
||||
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 show = ref(false);
|
||||
const showZoom = ref(false);
|
||||
const showEditForm = ref(false);
|
||||
const url = computed(() => {
|
||||
return `${props.baseURL ?? app.imageUrl}/${props.storage}/${props.size}/${props.id}`;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="relative-position main-image-container">
|
||||
<QBtn
|
||||
v-if="props.editable"
|
||||
icon="add_a_photo"
|
||||
class="show-edit-button absolute-top-left"
|
||||
round
|
||||
text-color="black"
|
||||
@click.stop="showEditForm = !showEditForm"
|
||||
/>
|
||||
<QImg
|
||||
:class="{ zoomIn: props.zoomSize, rounded: props.rounded }"
|
||||
:class="{
|
||||
zoomIn: props.zoomSize,
|
||||
rounded: props.rounded,
|
||||
'full-rounded': props.fullRounded
|
||||
}"
|
||||
class="main-image"
|
||||
:src="url"
|
||||
v-bind="$attrs"
|
||||
@click="show = !show"
|
||||
@click="showZoom = !showZoom"
|
||||
spinner-color="primary"
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
/>
|
||||
<QDialog v-model="show" v-if="props.zoomSize">
|
||||
</div>
|
||||
|
||||
<QDialog v-if="props.zoomSize" v-model="showZoom">
|
||||
<QImg
|
||||
:src="url"
|
||||
size="full"
|
||||
|
@ -52,17 +98,47 @@ const url = computed(() => {
|
|||
spinner-color="primary"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.q-img {
|
||||
.main-image-container {
|
||||
&:hover {
|
||||
.show-edit-button {
|
||||
visibility: visible !important;
|
||||
}
|
||||
.main-image {
|
||||
filter: brightness(80%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-image {
|
||||
&.zoomIn {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.show-edit-button {
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
background-color: $gray-light;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 50%;
|
||||
border-radius: 0.6em;
|
||||
}
|
||||
.full-rounded {
|
||||
border-radius: 50px;
|
||||
}
|
||||
.img_zoom {
|
||||
border-radius: 0%;
|
||||
|
|
|
@ -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>
|
|
@ -53,7 +53,12 @@ export default {
|
|||
checkout: 'Configurar encàrrec',
|
||||
controlPanel: 'Panell de control',
|
||||
adminConnections: 'Connexions',
|
||||
adminItems: 'Articles',
|
||||
adminVisits: 'Visites',
|
||||
adminUsers: "Gestió d'usuaris",
|
||||
adminPhotos: 'Imatges',
|
||||
//
|
||||
orderLoadedIntoBasket: 'Comanda carregada a la cistella!',
|
||||
at: 'a les'
|
||||
at: 'a les',
|
||||
imageAdded: 'Imatge afegida correctament'
|
||||
};
|
||||
|
|
|
@ -66,9 +66,14 @@ export default {
|
|||
checkout: 'Configure order',
|
||||
controlPanel: 'Control panel',
|
||||
adminConnections: 'Connections',
|
||||
adminItems: 'Items',
|
||||
adminVisits: 'Visits',
|
||||
adminUsers: 'User management',
|
||||
adminPhotos: 'Images',
|
||||
//
|
||||
orderLoadedIntoBasket: 'Order loaded into basket!',
|
||||
at: 'at',
|
||||
imageAdded: 'Image added successfully',
|
||||
|
||||
orders: 'Orders',
|
||||
order: 'Pending order',
|
||||
|
|
|
@ -72,6 +72,10 @@ export default {
|
|||
checkout: 'Configurar pedido',
|
||||
controlPanel: 'Panel de control',
|
||||
adminConnections: 'Conexiones',
|
||||
adminItems: 'Artículos',
|
||||
adminVisits: 'Visitas',
|
||||
adminUsers: 'Gestión de usuarios',
|
||||
adminPhotos: 'Imágenes',
|
||||
//
|
||||
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
|
||||
at: 'a las',
|
||||
|
|
|
@ -53,7 +53,12 @@ export default {
|
|||
checkout: "Définissez l'ordre",
|
||||
controlPanel: 'Panneau de configuration',
|
||||
adminConnections: 'Connexions',
|
||||
adminItems: 'Articles',
|
||||
adminVisits: 'Visites',
|
||||
adminUsers: 'Gestion des utilisateurs',
|
||||
adminPhotos: 'Images',
|
||||
//
|
||||
orderLoadedIntoBasket: 'Commande chargée dans le panier!',
|
||||
at: 'à'
|
||||
at: 'à',
|
||||
imageAdded: 'Image ajoutée correctement'
|
||||
};
|
||||
|
|
|
@ -54,7 +54,12 @@ export default {
|
|||
checkout: 'Configurar encomenda',
|
||||
controlPanel: 'Painel de controle',
|
||||
adminConnections: 'Conexões',
|
||||
adminItems: 'Artigos',
|
||||
adminVisits: 'Visitas',
|
||||
adminUsers: 'Gestão de usuários',
|
||||
adminPhotos: 'Imagens',
|
||||
//
|
||||
orderLoadedIntoBasket: 'Pedido carregado na cesta!',
|
||||
at: 'às'
|
||||
at: 'às',
|
||||
imageAdded: 'Imagen adicionada corretamente'
|
||||
};
|
||||
|
|
|
@ -50,6 +50,7 @@ const supplantUser = async user => {
|
|||
console.error('Error supplanting user:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
getConnections();
|
||||
intervalId.value = setInterval(getConnections, 60000);
|
||||
|
|
|
@ -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>
|
|
@ -1,36 +1,102 @@
|
|||
<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 getUsers = async () => {
|
||||
try {
|
||||
users.value = await jApi.query(
|
||||
`SELECT u.id, u.name, u.nickname, u.active
|
||||
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`,
|
||||
{ user: 9 }
|
||||
);
|
||||
ORDER BY u.name LIMIT 200`;
|
||||
|
||||
const onSearch = data => (users.value = data || []);
|
||||
|
||||
const supplantUser = async user => {
|
||||
try {
|
||||
await userStore.supplantUser(user);
|
||||
await appStore.getMenuLinks();
|
||||
router.push({ name: 'confirmedOrders' });
|
||||
} catch (error) {
|
||||
console.error('Error getting users:', error);
|
||||
console.error('Error supplanting user:', error);
|
||||
notify(error.message, 'negative');
|
||||
}
|
||||
};
|
||||
onMounted(async () => getUsers());
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<!-- 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"
|
||||
|
||||
flat
|
||||
rounded
|
||||
@click="supplantUser(user.name)"
|
||||
/>
|
||||
</template>
|
||||
</CardList>
|
||||
</QList>
|
||||
</QPage>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
<i18n lang="yaml"></i18n>
|
||||
<i18n lang="yaml">
|
||||
en-US:
|
||||
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>
|
||||
|
|
|
@ -91,13 +91,13 @@ const routes = [
|
|||
},
|
||||
{
|
||||
name: 'adminPhotos',
|
||||
path: 'admin/photos'
|
||||
// component: () => import('pages/Admin/PhotosView.vue')
|
||||
path: 'admin/photos',
|
||||
component: () => import('pages/Admin/PhotosView.vue')
|
||||
},
|
||||
{
|
||||
name: 'adminItems',
|
||||
path: 'admin/items'
|
||||
// component: () => import('pages/Admin/ItemsView.vue')
|
||||
path: 'admin/items',
|
||||
component: () => import('pages/Admin/ItemsView.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue
tooltip
053b9f8457