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
33 changed files with 945 additions and 288 deletions
Showing only changes of commit 76b99ed293 - Show all commits

View File

@ -1,10 +1,10 @@
import { boot } from 'quasar/wrappers'
import { appStore } from 'stores/app'
import { userStore } from 'stores/user'
import { boot } from 'quasar/wrappers';
import { useAppStore } from 'stores/app';
import { useUserStore } from 'stores/user';
export default boot(({ app }) => {
const props = app.config.globalProperties
props.$app = appStore()
props.$user = userStore()
props.$actions = document.createElement('div')
})
const props = app.config.globalProperties;
const userStore = useUserStore();
props.$app = useAppStore();
props.$user = userStore.user;
});

View File

@ -1,6 +1,6 @@
import { boot } from 'quasar/wrappers';
import { Connection } from '../js/db/connection';
import { userStore } from 'stores/user';
import { useUserStore } from 'stores/user';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
@ -36,10 +36,10 @@ const onResponseError = error => {
};
export default boot(({ app }) => {
const user = userStore();
const userStore = useUserStore();
function addToken(config) {
if (user.token) {
config.headers.Authorization = user.token;
if (userStore.token) {
config.headers.Authorization = userStore.token;
}
return config;
}

View File

@ -2,6 +2,8 @@
import { ref, inject, onMounted, computed, Teleport } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
import useNotify from 'src/composables/useNotify.js';
import {
generateUpdateSqlQuery,
@ -81,6 +83,8 @@ const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const jApi = inject('jApi');
const { notify } = useNotify();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const loading = ref(false);
const formData = ref({});
@ -191,13 +195,14 @@ defineExpose({
</span>
<slot name="form" :data="formData" />
<component
v-if="isHeaderMounted"
:is="showBottomActions ? 'div' : Teleport"
:to="$actions"
to="#actions"
class="flex row justify-end q-gutter-x-sm"
:class="{ 'q-mt-md': showBottomActions }"
>
<QBtn
v-if="defaultActions"
v-if="defaultActions && showBottomActions"
:label="t('cancel')"
:icon="showBottomActions ? undefined : 'check'"
rounded

View File

@ -9,7 +9,7 @@ const emit = defineEmits([
'remove'
]);
const $props = defineProps({
const props = defineProps({
modelValue: {
type: [String, Number],
default: null
@ -33,7 +33,7 @@ const requiredFieldRule = val => !!val || t('globals.fieldRequired');
const vnInputRef = ref(null);
const value = computed({
get() {
return $props.modelValue;
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
@ -41,7 +41,7 @@ const value = computed({
});
const hover = ref(false);
const styleAttrs = computed(() => {
return $props.isOutlined
return props.isOutlined
? { dense: true, outlined: true, rounded: true }
: {};
});
@ -88,9 +88,7 @@ const inputRules = [
<template #append>
<slot v-if="$slots.append && !$attrs.disabled" name="append" />
<QIcon
v-if="
hover && value && !$attrs.disabled && $props.clearable
"
v-if="hover && value && !$attrs.disabled && props.clearable"
name="close"
size="xs"
@click="

View File

@ -0,0 +1,44 @@
<script setup>
const props = defineProps({
clickable: { type: Boolean, default: true },
rounded: { type: Boolean, default: true }
});
const emit = defineEmits(['click']);
const handleClick = () => {
if (props.clickable) {
emit('click');
}
};
</script>
<template>
<QItem
v-bind="$attrs"
v-ripple="clickable"
:clickable="clickable"
class="full-width row items-center justify-between card no-border-radius bg-white"
:class="{ 'cursor-pointer': clickable, 'no-radius': !rounded }"
@click="handleClick()"
>
<QItemSection class="no-padding">
<div class="row no-wrap">
<slot name="prepend" />
<div class="column full-width">
<slot name="content" />
</div>
</div>
</QItemSection>
<QItemSection class="no-padding" side>
<slot name="actions" />
</QItemSection>
</QItem>
</template>
<style lang="scss" scoped>
.card {
border-bottom: 1px solid $gray-light;
padding: 20px;
}
</style>

View File

@ -5,8 +5,10 @@ import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import VnForm from 'src/components/common/VnForm.vue';
import { userStore as useUserStore } from 'stores/user';
import { useUserStore } from 'stores/user';
import useNotify from 'src/composables/useNotify.js';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const props = defineProps({
verificationToken: {
@ -24,6 +26,8 @@ const { t } = useI18n();
const api = inject('api');
const userStore = useUserStore();
const { notify } = useNotify();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const oldPasswordRef = ref(null);
const newPasswordRef = ref(null);
@ -33,7 +37,7 @@ const repeatPassword = ref('');
const passwordRequirements = ref(null);
const formData = ref({
userId: userStore.id,
userId: userStore.user.id,
oldPassword: '',
newPassword: ''
});
@ -75,7 +79,7 @@ const getPasswordRequirements = async () => {
};
const login = async () => {
await userStore.login(userStore.name, formData.value.newPassword);
await userStore.login(userStore.user.name, formData.value.newPassword);
};
const onPasswordChanged = async () => {
@ -112,40 +116,40 @@ onMounted(async () => {
v-model="formData.oldPassword"
:type="!showOldPwd ? 'password' : 'text'"
:label="t('oldPassword')"
>
>
<template #append>
<QIcon
:name="showOldPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showOldPwd = !showOldPwd"
/>
</template>
</VnInput>
<QIcon
:name="showOldPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showOldPwd = !showOldPwd"
/>
</template>
</VnInput>
<VnInput
ref="newPasswordRef"
v-model="formData.newPassword"
:type="!showNewPwd ? 'password' : 'text'"
:label="t('newPassword')"
><template #append>
<QIcon
:name="showNewPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showNewPwd = !showNewPwd"
/>
</template></VnInput>
><template #append>
<QIcon
:name="showNewPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showNewPwd = !showNewPwd"
/> </template
></VnInput>
<VnInput
v-model="repeatPassword"
:type="!showCopyPwd ? 'password' : 'text'"
:label="t('repeatPassword')"
><template #append>
<QIcon
:name="showCopyPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showCopyPwd = !showCopyPwd"
/>
</template></VnInput>
><template #append>
<QIcon
:name="showCopyPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="showCopyPwd = !showCopyPwd"
/> </template
></VnInput>
</template>
<template #actions>
<template v-if="isHeaderMounted" #actions>
<QBtn
:label="t('requirements')"
rounded

View File

@ -1,8 +1,8 @@
<script setup>
import { ref, computed } from 'vue';
import { appStore } from 'stores/app';
import { useAppStore } from 'stores/app';
const $props = defineProps({
const props = defineProps({
baseURL: {
type: String,
default: null
@ -23,23 +23,27 @@ const $props = defineProps({
id: {
type: Number,
required: true
},
rounded: {
type: Boolean,
default: false
}
});
const app = appStore();
const app = useAppStore();
const show = ref(false);
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>
<template>
<QImg
:class="{ zoomIn: $props.zoomSize }"
:class="{ zoomIn: props.zoomSize, rounded: props.rounded }"
:src="url"
v-bind="$attrs"
@click="show = !show"
spinner-color="primary"
/>
<QDialog v-model="show" v-if="$props.zoomSize">
<QDialog v-model="show" v-if="props.zoomSize">
<QImg
:src="url"
size="full"

View File

@ -1,8 +1,8 @@
// app global css in SCSS form
@font-face {
font-family: Poppins;
src: url(./poppins.ttf) format('truetype');
font-family: Poppins;
src: url(./poppins.ttf) format('truetype');
}
@font-face {
font-family: 'Open Sans';
@ -36,3 +36,9 @@ a.link {
.q-page-sticky.fixed-bottom-right {
margin: 18px;
}
.no-border-radius {
border-radius: 0 !important;
}
.no-padding {
padding: 0 !important;
}

View File

@ -15,7 +15,7 @@
$primary: #1a1a1a;
$secondary: #26a69a;
$accent: #8cc63f;
$gray-light: #ddd;
$dark: #1d1d1d;
$dark-page: #121212;

58
src/i18n/ca-ES/index.js Normal file
View File

@ -0,0 +1,58 @@
export default {
date: {
days: [
'Diumenge',
'Dilluns',
'Dimarts',
'Dimecres',
'Dijous',
'Divendres',
'Dissabte'
],
daysShort: ['Dg', 'Dl', 'Dt', 'Dc', 'Dj', 'Dv', 'Ds'],
months: [
'Gener',
'Febrer',
'Març',
'Abril',
'Maig',
'Juny',
'Juliol',
'Agost',
'Setembre',
'Octubre',
'Novembre',
'Desembre'
],
shortMonths: [
'Gen',
'Feb',
'Mar',
'Abr',
'Mai',
'Jun',
'Jul',
'Ago',
'Set',
'Oct',
'Nov',
'Des'
]
},
of: 'de',
// Menu
home: 'Inici',
catalog: 'Catàleg',
pendingOrders: 'Comandes pendents',
confirmedOrders: 'Comandes confirmades',
invoices: 'Factures',
agencyPackages: 'Paquets per agència',
accountConfig: 'Configuració',
addressesList: 'Adreces',
addressDetails: 'Configuració',
checkout: 'Configurar encàrrec',
controlPanel: 'Panell de control',
adminConnections: 'Connexions',
//
orderLoadedIntoBasket: 'Comanda carregada a la cistella!'
};

View File

@ -56,6 +56,19 @@ export default {
// menu
home: 'Home',
catalog: 'Catalog',
pendingOrders: 'Pending orders',
confirmedOrders: 'Confirmed orders',
invoices: 'Invoices',
agencyPackages: 'Bundles by agency',
accountConfig: 'Configuration',
addressesList: 'Addresses',
addressDetails: 'Configuration',
checkout: 'Configure order',
controlPanel: 'Control panel',
adminConnections: 'Connections',
//
orderLoadedIntoBasket: 'Order loaded into basket!',
orders: 'Orders',
order: 'Pending order',
ticket: 'Order',
@ -76,5 +89,6 @@ export default {
addressEdit: 'Edit address',
dataSaved: 'Data saved',
save: 'Save',
cancel: 'Cancel'
cancel: 'Cancel',
of: 'of'
};

View File

@ -1,6 +1,3 @@
// This is just an example,
// so you can safely delete all default props below
export default {
failed: 'Acción fallida',
success: 'Acción exitosa',
@ -65,6 +62,19 @@ export default {
// Menu
home: 'Inicio',
catalog: 'Catálogo',
pendingOrders: 'Pedidos pendientes',
confirmedOrders: 'Pedidos confirmados',
invoices: 'Facturas',
agencyPackages: 'Bultos por agencia',
accountConfig: 'Configuración',
addressesList: 'Direcciones',
addressDetails: 'Configuración',
checkout: 'Configurar pedido',
controlPanel: 'Panel de control',
adminConnections: 'Conexiones',
//
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
orders: 'Pedidos',
order: 'Pedido pendiente',
ticket: 'Pedido',
@ -94,5 +104,6 @@ export default {
addressEdit: 'Editar dirección',
dataSaved: 'Datos guardados',
save: 'Guardar',
cancel: 'Cancelar'
cancel: 'Cancelar',
of: 'de'
};

58
src/i18n/fr-FR/index.js Normal file
View File

@ -0,0 +1,58 @@
export default {
date: {
days: [
'Dimanche',
'Lundi',
'Mardi',
'Mercredi',
'Jeudi',
'Vendredi',
'Samedi'
],
daysShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
months: [
'Janvier',
'Février',
'Mars',
'Avril',
'Mai',
'Juin',
'Juillet',
'Août',
'Septembre',
'Octobre',
'Novembre',
'Décembre'
],
shortMonths: [
'Jan',
'Fév',
'Mar',
'Avr',
'Mai',
'Juin',
'Juil',
'Aoû',
'Sep',
'Oct',
'Nov',
'Déc'
]
},
of: 'de',
// Menu
home: 'Accueil',
catalog: 'Catalogue',
pendingOrders: 'Commandes en attente',
confirmedOrders: 'Commandes confirmées',
invoices: 'Factures',
agencyPackages: 'Liste par agence',
accountConfig: 'Configuration',
addressesList: 'Adresses',
addressDetails: 'Configuration',
checkout: "Définissez l'ordre",
controlPanel: 'Panneau de configuration',
adminConnections: 'Connexions',
//
orderLoadedIntoBasket: 'Commande chargée dans le panier!'
};

View File

@ -1,7 +1,13 @@
import enUS from './en-US'
import esES from './es-ES'
import enUS from './en-US';
import esES from './es-ES';
import frFR from './fr-FR';
import ptPT from './pt-PT';
import caES from './ca-ES';
export default {
'en-US': enUS,
'es-ES': esES
}
'es-ES': esES,
'fr-FR': frFR,
'pt-PT': ptPT,
'ca-ES': caES
};

59
src/i18n/pt-PT/index.js Normal file
View File

@ -0,0 +1,59 @@
export default {
date: {
days: [
'Domingo',
'Segunda-feira',
'Terça-feira',
'Quarta-feira',
'Quinta-feira',
'Sexta-feira',
'Sábado'
],
daysShort: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'],
months: [
'Janeiro',
'Fevereiro',
'Março',
'Abril',
'Maio',
'Junho',
'Julho',
'Agosto',
'Setembro',
'Outubro',
'Novembro',
'Dezembro'
],
shortMonths: [
'Jan',
'Fev',
'Mar',
'Abr',
'Mai',
'Jun',
'Jul',
'Ago',
'Set',
'Out',
'Nov',
'Dez'
]
},
of: 'de',
// Menu
home: 'Principio',
catalog: 'Catálogo',
pendingOrders: 'Pedidos pendentes',
confirmedOrders: 'Pedidos confirmados',
invoices: 'Facturas',
agencyPackages: 'Bultos por agencia',
accountConfig: 'Configuração',
addressesList: 'Moradas',
addressDetails: 'Configuração',
checkout: 'Configurar encomenda',
controlPanel: 'Painel de controle',
adminConnections: 'Conexões',
//
orderLoadedIntoBasket: 'Pedido carregado na cesta!'
};

View File

@ -1,3 +1,45 @@
<script setup>
import { ref, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router';
import { useUserStore } from 'stores/user';
import { useAppStore } from 'stores/app';
const router = useRouter();
const userStore = useUserStore();
const appStore = useAppStore();
const { user, supplantedUser } = storeToRefs(userStore);
const { menuEssentialLinks, title, subtitle, useRightDrawer, rightDrawerOpen } =
storeToRefs(appStore);
const actions = ref(null);
const leftDrawerOpen = ref(false);
const toggleLeftDrawer = () => {
leftDrawerOpen.value = !leftDrawerOpen.value;
};
onMounted(async () => {
appStore.isHeaderMounted = true;
await userStore.fetchUser();
await appStore.loadConfig();
await userStore.supplantInit();
await appStore.getMenuLinks();
});
const logout = async () => {
await userStore.logout();
router.push('/login');
};
const logoutSupplantedUser = async () => {
await userStore.logoutSupplantedUser();
await appStore.getMenuLinks();
};
</script>
<template>
<QLayout view="lHh Lpr lFf">
<QHeader>
@ -11,15 +53,15 @@
@click="toggleLeftDrawer"
/>
<QToolbarTitle>
{{ $app.title }}
<div v-if="$app.subtitle" class="subtitle text-caption">
{{ $app.subtitle }}
{{ title }}
<div v-if="subtitle" class="subtitle text-caption">
{{ subtitle }}
</div>
</QToolbarTitle>
<div id="actions" ref="actions"></div>
<div id="actions" ref="actions" class="flex items-center"></div>
<QBtn
v-if="$app.useRightDrawer"
@click="$app.rightDrawerOpen = !$app.rightDrawerOpen"
v-if="useRightDrawer"
@click="rightDrawerOpen = !rightDrawerOpen"
aria-label="Menu"
flat
dense
@ -35,15 +77,22 @@
</QToolbar>
<div class="user-info">
<div>
<span id="user-name">{{ user.nickname }}</span>
<span id="user-name">{{ user?.nickname }}</span>
<QBtn flat icon="logout" alt="_Exit" @click="logout()" />
</div>
<div id="supplant" class="supplant">
<span id="supplanted">{{ supplantedUser }}</span>
<QBtn flat icon="logout" alt="_Exit" />
<div v-if="supplantedUser" id="supplant" class="supplant">
<span id="supplanted">
{{ supplantedUser?.nickname }}
</span>
<QBtn
flat
icon="logout"
alt="_Exit"
@click="logoutSupplantedUser()"
/>
</div>
</div>
<QList v-for="item in essentialLinks" :key="item.id">
<QList v-for="item in menuEssentialLinks" :key="item.id">
<QItem v-if="!item.childs" :to="`/${item.path}`">
<QItemSection>
<QItemLabel>{{ item.description }}</QItemLabel>
@ -118,12 +167,7 @@
}
}
&.supplant {
display: none;
border-top: none;
&.show {
display: flex;
}
}
}
}
@ -143,10 +187,7 @@
.q-page-container > * {
padding: 16px;
}
#actions > div {
display: flex;
align-items: center;
}
@include mobile {
#actions > div {
.q-btn {
@ -166,71 +207,6 @@
}
</style>
<script>
import { defineComponent, ref } from 'vue';
import { userStore } from 'stores/user';
export default defineComponent({
name: 'MainLayout',
props: {},
setup() {
const leftDrawerOpen = ref(false);
return {
user: userStore(),
supplantedUser: ref(''),
essentialLinks: ref(null),
leftDrawerOpen,
toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
};
},
async mounted() {
this.$refs.actions.appendChild(this.$actions);
await this.user.loadData();
await this.$app.loadConfig();
await this.fetchData();
},
methods: {
async fetchData() {
const sections = await this.$jApi.query('SELECT * FROM myMenu');
const sectionMap = new Map();
for (const section of sections) {
sectionMap.set(section.id, section);
}
const sectionTree = [];
for (const section of sections) {
const parent = section.parentFk;
if (parent) {
const parentSection = sectionMap.get(parent);
if (!parentSection) continue;
let childs = parentSection.childs;
if (!childs) {
childs = parentSection.childs = [];
}
childs.push(section);
} else {
sectionTree.push(section);
}
}
this.essentialLinks = sectionTree;
},
async logout() {
this.user.logout();
this.$router.push('/login');
}
}
});
</script>
<i18n lang="yaml">
en-US:
visitor: Visitor

View File

@ -1,73 +1,87 @@
import { date as qdate, format } from 'quasar'
const { pad } = format
import { i18n } from 'src/boot/i18n';
import { date as qdate, format } from 'quasar';
const { pad } = format;
export function currency (val) {
return typeof val === 'number' ? val.toFixed(2) + '€' : val
export function currency(val) {
return typeof val === 'number' ? val.toFixed(2) + '€' : val;
}
export function date (val, format) {
if (val == null) return val
export function date(val, format) {
if (val == null) return val;
if (!(val instanceof Date)) {
val = new Date(val)
val = new Date(val);
}
return qdate.formatDate(val, format, window.i18n.tm('date'))
return qdate.formatDate(val, format, i18n.global.tm('date'));
}
export function relDate (val) {
if (val == null) return val
export const formatDateTitle = timeStamp => {
const { t, messages, locale } = i18n.global;
const formattedString = qdate.formatDate(
timeStamp,
`dddd, D [${t('of')}] MMMM [${t('of')}] YYYY`,
{
days: messages.value[locale.value].date.days,
months: messages.value[locale.value].date.months
}
);
return formattedString;
};
export function relDate(val) {
if (val == null) return val;
if (!(val instanceof Date)) {
val = new Date(val)
val = new Date(val);
}
const dif = qdate.getDateDiff(new Date(), val, 'days')
let day
const dif = qdate.getDateDiff(new Date(), val, 'days');
let day;
switch (dif) {
case 0:
day = 'today'
break
day = 'today';
break;
case 1:
day = 'yesterday'
break
day = 'yesterday';
break;
case -1:
day = 'tomorrow'
break
day = 'tomorrow';
break;
}
if (day) {
day = window.i18n.t(day)
day = i18n.global.t(day);
} else {
if (dif > 0 && dif <= 7) {
day = qdate.formatDate(val, 'ddd', window.i18n.tm('date'))
day = qdate.formatDate(val, 'ddd', i18n.global.tm('date'));
} else {
day = qdate.formatDate(val, 'ddd, MMMM Do', window.i18n.tm('date'))
day = qdate.formatDate(val, 'ddd, MMMM Do', i18n.global.tm('date'));
}
}
return day
return day;
}
export function relTime (val) {
if (val == null) return val
export function relTime(val) {
if (val == null) return val;
if (!(val instanceof Date)) {
val = new Date(val)
val = new Date(val);
}
return relDate(val) + ' ' + qdate.formatDate(val, 'H:mm:ss')
return relDate(val) + ' ' + qdate.formatDate(val, 'H:mm:ss');
}
export function elapsedTime (val) {
if (val == null) return val
export function elapsedTime(val) {
if (val == null) return val;
if (!(val instanceof Date)) {
val = new Date(val)
val = new Date(val);
}
const now = new Date().getTime()
val = Math.floor((now - val.getTime()) / 1000)
const now = new Date().getTime();
val = Math.floor((now - val.getTime()) / 1000);
const hours = Math.floor(val / 3600)
val -= hours * 3600
const minutes = Math.floor(val / 60)
val -= minutes * 60
const seconds = val
const hours = Math.floor(val / 3600);
val -= hours * 3600;
const minutes = Math.floor(val / 60);
val -= minutes * 60;
const seconds = val;
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}`
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}`;
}

View File

@ -6,17 +6,21 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import VnForm from 'src/components/common/VnForm.vue';
import ChangePasswordForm from 'src/components/ui/ChangePasswordForm.vue';
import { userStore as useUserStore } from 'stores/user';
import { useUserStore } from 'stores/user';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { t } = useI18n();
const jApi = inject('jApi');
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const vnFormRef = ref(null);
const changePasswordFormDialog = ref(null);
const showChangePasswordForm = ref(false);
const langOptions = ref([]);
const pks = computed(() => ({ id: userStore.id }));
const pks = computed(() => ({ id: userStore.user.id }));
const fetchConfigDataSql = {
query: `
SELECT u.id, u.name, u.email, u.nickname,
@ -44,13 +48,13 @@ onMounted(() => fetchLanguagesSql());
<template>
<QPage>
<QPage class="q-pa-md flex justify-center">
<Teleport :to="$actions">
<Teleport v-if="isHeaderMounted" to="#actions">
<QBtn
:label="t('addresses')"
icon="location_on"
rounded
no-caps
:to="{ name: 'AddressesList' }"
:to="{ name: 'addressesList' }"
/>
<QBtn
:label="t('changePassword')"

View File

@ -7,10 +7,15 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnForm from 'src/components/common/VnForm.vue';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const router = useRouter();
const route = useRoute();
const { t } = useI18n();
const jApi = inject('jApi');
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const vnFormRef = ref(null);
const countriesOptions = ref([]);
@ -32,7 +37,7 @@ watch(
async val => await getProvinces(val)
);
const goBack = () => router.push({ name: 'AddressesList' });
const goBack = () => router.push({ name: 'addressesList' });
const getCountries = async () => {
countriesOptions.value = await jApi.query(
@ -56,7 +61,7 @@ onMounted(() => getCountries());
<template>
<QPage class="q-pa-md flex justify-center">
<Teleport :to="$actions">
<Teleport v-if="isHeaderMounted" to="#actions">
<QBtn
:label="t('back')"
icon="close"

View File

@ -5,19 +5,23 @@ import { useRouter } from 'vue-router';
import useNotify from 'src/composables/useNotify.js';
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const router = useRouter();
const jApi = inject('jApi');
const { notify } = useNotify();
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const addresses = ref([]);
const defaultAddress = ref(null);
const clientId = ref(null);
const goToAddressDetails = (id = 0) =>
router.push({ name: 'AddressDetails', params: { id } });
router.push({ name: 'addressDetails', params: { id } });
const getDefaultAddress = async () => {
try {
@ -84,7 +88,7 @@ onMounted(async () => {
</script>
<template>
<Teleport :to="$actions">
<Teleport v-if="isHeaderMounted" to="#actions">
<QBtn
:label="t('addAddress')"
icon="add"

View File

@ -0,0 +1,168 @@
<script setup>
import { ref, onMounted, inject, onBeforeUnmount } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardList from 'src/components/ui/CardList.vue';
import { date as qdate } from 'quasar';
import { useUserStore } from 'stores/user';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const { t } = useI18n();
const jApi = inject('jApi');
const router = useRouter();
const userStore = useUserStore();
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
const connections = ref([]);
const loading = ref(false);
const intervalId = ref(null);
const getConnections = async () => {
try {
loading.value = true;
connections.value = await jApi.query(
`SELECT vu.userFk userId, vu.stamp, u.nickname, s.lastUpdate,
a.platform, a.browser, a.version, u.name user
FROM userSession s
JOIN visitUser vu ON vu.id = s.userVisitFk
JOIN visitAccess ac ON ac.id = vu.accessFk
JOIN visitAgent a ON a.id = ac.agentFk
JOIN visit v ON v.id = a.visitFk
JOIN account.user u ON u.id = vu.userFk
ORDER BY lastUpdate DESC`
);
loading.value = false;
} catch (error) {
console.error('Error getting connections:', error);
}
};
const supplantUser = async user => {
try {
await userStore.supplantUser(user);
await appStore.getMenuLinks();
router.push({ name: 'confirmedOrders' });
} catch (error) {
console.error('Error supplanting user:', error);
}
};
onMounted(async () => {
getConnections();
intervalId.value = setInterval(getConnections, 60000);
});
onBeforeUnmount(() => clearInterval(intervalId.value));
</script>
<template>
<Teleport v-if="isHeaderMounted" to="#actions">
<div class="flex">
<QBtn
:label="t('refresh')"
icon="refresh"
@click="getConnections()"
rounded
no-caps
class="q-mr-sm"
/>
<QBadge class="q-pa-sm" v-if="connections.length" color="blue">
{{ connections?.length }} {{ t('connections') }}
</QBadge>
</div>
</Teleport>
<QPage class="vn-w-xs">
<QList class="flex justify-center">
<QSpinner
v-if="loading"
color="primary"
size="3em"
:thickness="2"
/>
<CardList
v-else
v-for="(connection, index) in connections"
:key="index"
>
<template #content>
<span class="text-bold q-mb-sm">
{{ connection.nickname }}
</span>
<span>
{{
qdate.formatDate(connection.stamp, 'dd, hh:mm:ss A')
}}
-
{{
qdate.formatDate(
connection.lastUpdate,
'hh:mm:ss A'
)
}}</span
>
<span
v-if="
connection.platform &&
connection.browser &&
connection.version
"
>
{{ connection.platform }} - {{ connection.browser }} -
{{ connection.version }}
</span>
</template>
<template #actions>
<QBtn
icon="people"
flat
rounded
@click="supplantUser(connection.user)"
/>
</template>
</CardList>
</QList>
</QPage>
</template>
<style lang="scss" scoped>
.card-container {
width: 140px;
height: 170px;
padding: 15px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.card-title {
font-size: 0.7rem;
font-weight: bold;
}
.card-description {
font-size: 0.65rem;
text-align: center;
}
</style>
<i18n lang="yaml">
en-US:
refresh: Refresh
connections: Connections
es-ES:
refresh: Actualizar
connections: Conexiones
ca-ES:
refresh: Actualitzar
connections: Connexions
fr-FR:
refresh: Rafraîchir
connections: Connexions
pt-PT:
refresh: Atualizar
connections: Conexões
</i18n>

View File

@ -0,0 +1,79 @@
<script setup>
import { ref, onMounted, inject } from 'vue';
const jApi = inject('jApi');
const links = ref([]);
const getLinks = async () => {
try {
links.value = await jApi.query(
`SELECT image, name, description, link FROM link
ORDER BY name`
);
} catch (error) {
console.error('Error getting links:', error);
}
};
onMounted(async () => getLinks());
</script>
<template>
<QPage>
<QList class="flex justify-center">
<QItem
v-for="(link, index) in links"
:key="index"
:href="link.link"
target="_blank"
class="flex"
>
<QCard class="card-container">
<QImg
:src="`http://cdn.verdnatura.es/image/link/full/${link.image}`"
width="60px"
height="60px"
/>
<span class="card-title q-mt-md">{{ link.name }}</span>
<p class="card-description">{{ link.description }}</p>
</QCard>
</QItem>
</QList>
</QPage>
</template>
<style lang="scss" scoped>
.card-container {
width: 140px;
height: 170px;
padding: 15px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.card-title {
font-size: 0.7rem;
font-weight: bold;
}
.card-description {
font-size: 0.65rem;
text-align: center;
}
</style>
<i18n lang="yaml">
en-US:
addAddress: Add address
es-ES:
addAddress: Añadir dirección
ca-ES:
addAddress: Afegir adreça
fr-FR:
addAddress: Ajouter une adresse
pt-PT:
addAddress: Adicionar Morada
</i18n>

View File

@ -0,0 +1,36 @@
<script setup>
import { ref, onMounted, inject } from 'vue';
const jApi = inject('jApi');
const users = ref([]);
const getUsers = async () => {
try {
users.value = await jApi.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 }
);
} catch (error) {
console.error('Error getting users:', error);
}
};
onMounted(async () => getUsers());
</script>
<template>
<QPage>
<QList class="flex justify-center">
<!-- TODO: WIP -->
</QList>
</QPage>
</template>
<style lang="scss" scoped></style>
<i18n lang="yaml"></i18n>

View File

@ -1,5 +1,5 @@
<template>
<Teleport :to="$actions">
<Teleport v-if="isHeaderMounted" to="#actions">
<QInput
:placeholder="$t('search')"
v-model="search"
@ -346,11 +346,18 @@
import { date, currency } from 'src/lib/filters.js';
import { date as qdate } from 'quasar';
import axios from 'axios';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
const CancelToken = axios.CancelToken;
export default {
name: 'HederaCatalog',
setup() {
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
return { isHeaderMounted };
},
data() {
return {
uid: 0,

View File

@ -1,5 +1,5 @@
<template>
<Teleport :to="$actions">
<Teleport v-if="isHeaderMounted" to="#actions">
<QSelect
v-model="year"
:options="years"
@ -64,9 +64,16 @@
<script>
import { date, currency } from 'src/lib/filters.js';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
export default {
name: 'OrdersPendingIndex',
setup() {
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
return { isHeaderMounted };
},
data() {
const curYear = new Date().getFullYear();
const years = [];
@ -102,12 +109,12 @@ export default {
},
async mounted() {
await this.loadData();
await this.fetchUser();
},
watch: {
async year() {
await this.loadData();
await this.fetchUser();
}
},
@ -115,7 +122,7 @@ export default {
date,
currency,
async loadData() {
async fetchUser() {
const params = {
from: new Date(this.year, 0),
to: new Date(this.year, 11, 31, 23, 59, 59)

View File

@ -1,5 +1,5 @@
<template>
<Teleport :to="$actions">
<Teleport v-if="isHeaderMounted" to="#actions">
<div class="balance">
<span class="label">{{ $t('balance') }}</span>
<span class="amount" :class="{ negative: debt < 0 }">
@ -95,9 +95,16 @@
<script>
import { date, currency } from 'src/lib/filters.js';
import { tpvStore } from 'stores/tpv';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
export default {
name: 'OrdersPendingIndex',
setup() {
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
return { isHeaderMounted };
},
data() {
return {
orders: null,

View File

@ -1,5 +1,5 @@
<template>
<Teleport :to="$actions">
<Teleport v-if="isHeaderMounted" to="#actions">
<QBtn
icon="print"
:label="$t('printDeliveryNote')"
@ -82,10 +82,16 @@
<script>
import { date, currency } from 'src/lib/filters.js';
import { useAppStore } from 'stores/app';
import { storeToRefs } from 'pinia';
export default {
name: 'OrdersConfirmedView',
setup() {
const appStore = useAppStore();
const { isHeaderMounted } = storeToRefs(appStore);
return { isHeaderMounted };
},
data() {
return {
ticket: {},

View File

@ -1,13 +1,13 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { userStore } from 'stores/user';
import { useUserStore } from 'stores/user';
import { onMounted, ref } from 'vue';
import useNotify from 'src/composables/useNotify.js';
import { useRouter, useRoute } from 'vue-router';
const { notify } = useNotify();
const t = useI18n();
const user = userStore();
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const email = ref(null);
@ -29,7 +29,7 @@ onMounted(() => {
}
});
async function onLogin() {
await user.login(email.value, password.value, remember.value);
await userStore.login(email.value, password.value, remember.value);
router.push('/');
}
</script>
@ -57,31 +57,30 @@ async function onLogin() {
/>
</template>
</QInput>
<div class=" text-center"> <QCheckbox
v-model="remember"
:label="$t('remindMe')"
dense
/> <QBtn
id="switchLanguage"
:label="$t('language')"
icon="translate"
color="primary"
size="sm"
flat
rounded
>
<QMenu auto-close>
<QList dense v-for="lang in langs" :key="lang">
<QItem
disabled
v-ripple
clickable
>
{{ $t(`langs.${lang}`) }}
</QItem>
</QList>
</QMenu>
</QBtn></div>
<div class="text-center">
<QCheckbox
v-model="remember"
:label="$t('remindMe')"
dense
/>
<QBtn
id="switchLanguage"
:label="$t('language')"
icon="translate"
color="primary"
size="sm"
flat
rounded
>
<QMenu auto-close>
<QList dense v-for="lang in langs" :key="lang">
<QItem disabled v-ripple clickable>
{{ $t(`langs.${lang}`) }}
</QItem>
</QList>
</QMenu>
</QBtn>
</div>
</div>
<div class="justify-center">
<QBtn

View File

@ -1,12 +1,13 @@
import { route } from 'quasar/wrappers'
import { appStore } from 'stores/app'
import { route } from 'quasar/wrappers';
import { useAppStore } from 'stores/app';
import {
createRouter,
createMemoryHistory,
createWebHistory,
createWebHashHistory
} from 'vue-router'
import routes from './routes'
} from 'vue-router';
import routes from './routes';
import { i18n } from 'src/boot/i18n';
/*
* If not building with SSR mode, you can
@ -22,7 +23,7 @@ export default route(function (/* { store, ssrContext } */) {
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory
: createWebHashHistory;
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
@ -34,18 +35,18 @@ export default route(function (/* { store, ssrContext } */) {
history: createHistory(
process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE
)
})
});
Router.afterEach((to, from) => {
if (from.name === to.name) return
const app = appStore()
if (from.name === to.name) return;
const app = useAppStore();
app.$patch({
title: window.i18n.t(to.name || 'home'),
title: i18n.global.t(to.name || 'home'),
subtitle: null,
useRightDrawer: false,
rightDrawerOpen: true
})
})
});
});
return Router
})
return Router;
});

View File

@ -4,7 +4,7 @@ const routes = [
component: () => import('layouts/LoginLayout.vue'),
children: [
{
name: 'Login',
name: 'login',
path: '/login/:email?',
component: () => import('pages/Login/LoginView.vue')
},
@ -35,19 +35,9 @@ const routes = [
component: () => import('src/pages/Cms/HomeView.vue')
},
{
name: 'orders',
name: 'confirmedOrders',
path: '/ecomerce/orders',
component: () => import('pages/Ecomerce/Orders.vue')
},
{
name: 'ticket',
path: '/ecomerce/ticket/:id',
component: () => import('pages/Ecomerce/Ticket.vue')
},
{
name: 'invoices',
path: '/ecomerce/invoices',
component: () => import('pages/Ecomerce/Invoices.vue')
component: () => import('src/pages/Ecomerce/Orders.vue')
},
{
name: 'catalog',
@ -55,24 +45,59 @@ const routes = [
component: () => import('pages/Ecomerce/Catalog.vue')
},
{
name: 'packages',
name: 'agencyPackages',
path: '/agencies/packages',
component: () => import('src/pages/Agencies/PackagesView.vue')
},
{
name: 'Account',
name: 'accountConfig',
path: '/account/conf',
component: () => import('pages/Account/AccountConfig.vue')
},
{
name: 'AddressesList',
name: 'addressesList',
path: '/account/address-list',
component: () => import('pages/Account/AddressList.vue')
},
{
name: 'AddressDetails',
name: 'addressDetails',
path: '/account/address/:id?',
component: () => import('pages/Account/AddressDetails.vue')
},
{
name: 'controlPanel',
path: 'admin/links',
component: () => import('pages/Admin/LinksView.vue')
},
{
name: 'adminUsers',
path: 'admin/users',
component: () => import('pages/Admin/UsersView.vue')
},
{
name: 'adminConnections',
path: 'admin/connections',
component: () => import('pages/Admin/ConnectionsView.vue')
},
{
name: 'adminVisits',
path: 'admin/visits'
// component: () => import('pages/Admin/VisitsView.vue')
},
{
name: 'adminNews',
path: 'admin/news'
// component: () => import('pages/Admin/NewsView.vue')
},
{
name: 'adminPhotos',
path: 'admin/photos'
// component: () => import('pages/Admin/PhotosView.vue')
},
{
name: 'adminItems',
path: 'admin/items'
// component: () => import('pages/Admin/ItemsView.vue')
}
]
},

View File

@ -1,19 +1,46 @@
import { defineStore } from 'pinia'
import { jApi } from 'boot/axios'
import { defineStore } from 'pinia';
import { jApi } from 'boot/axios';
export const appStore = defineStore('hedera', {
export const useAppStore = defineStore('hedera', {
state: () => ({
title: null,
subtitle: null,
imageUrl: '',
useRightDrawer: false,
rightDrawerOpen: false
rightDrawerOpen: false,
isHeaderMounted: false,
menuEssentialLinks: []
}),
actions: {
async loadConfig () {
const imageUrl = await jApi.getValue('SELECT url FROM imageConfig')
this.$patch({ imageUrl })
async getMenuLinks() {
const sections = await jApi.query('SELECT * FROM myMenu');
const sectionMap = new Map();
for (const section of sections) {
sectionMap.set(section.id, section);
}
const sectionTree = [];
for (const section of sections) {
const parent = section.parentFk;
if (parent) {
const parentSection = sectionMap.get(parent);
if (!parentSection) continue;
let childs = parentSection.childs;
if (!childs) {
childs = parentSection.childs = [];
}
childs.push(section);
} else {
sectionTree.push(section);
}
}
this.menuEssentialLinks = sectionTree;
},
async loadConfig() {
const imageUrl = await jApi.getValue('SELECT url FROM imageConfig');
this.$patch({ imageUrl });
}
}
})
});

View File

@ -1,5 +1,5 @@
import { store } from 'quasar/wrappers'
import { createPinia } from 'pinia'
import { store } from 'quasar/wrappers';
import { createPinia } from 'pinia';
/*
* If not building with SSR mode, you can
@ -11,10 +11,10 @@ import { createPinia } from 'pinia'
*/
export default store((/* { ssrContext } */) => {
const pinia = createPinia()
const pinia = createPinia();
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
return pinia
})
return pinia;
});

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import { api, jApi } from 'boot/axios';
export const userStore = defineStore('user', {
export const useUserStore = defineStore('user', {
state: () => {
const token =
sessionStorage.getItem('vnToken') ||
@ -9,10 +9,9 @@ export const userStore = defineStore('user', {
return {
token,
id: null,
name: null,
nickname: null,
isGuest: false
isGuest: false,
user: null,
supplantedUser: null
};
},
@ -21,6 +20,11 @@ export const userStore = defineStore('user', {
},
actions: {
async getToken() {
this.token =
sessionStorage.getItem('vnToken') ||
localStorage.getItem('vnToken');
},
async login(user, password, remember) {
const params = { user, password };
const res = await api.post('Accounts/login', params);
@ -36,7 +40,6 @@ export const userStore = defineStore('user', {
name: user
});
},
async logout() {
if (this.token != null) {
try {
@ -48,16 +51,38 @@ export const userStore = defineStore('user', {
this.$reset();
},
async loadData() {
const userData = await jApi.getObject(
'SELECT id, nickname, name FROM account.myUser'
);
async fetchUser(userType = 'user') {
try {
const userData = await jApi.getObject(
'SELECT id, nickname, name FROM account.myUser'
);
this.$patch({ [userType]: userData });
} catch (error) {
console.error('Error fetching user: ', error);
}
},
this.$patch({
id: userData.id,
nickname: userData.nickname,
name: userData.name
async supplantUser(supplantUser) {
const json = await jApi.send('client/supplant', {
supplantUser
});
this.token = json;
sessionStorage.setItem('supplantUser', supplantUser);
await this.fetchUser('supplantedUser');
},
async supplantInit() {
const user = sessionStorage.getItem('supplantUser');
if (user == null) return;
await this.supplantUser(user);
},
async logoutSupplantedUser() {
sessionStorage.removeItem('supplantUser');
this.supplantedUser = null;
await api.post('Accounts/logout');
this.getToken();
await this.fetchUser();
}
}
});