Modulo Administración #78
|
@ -24,7 +24,7 @@ module.exports = configure(function (ctx) {
|
||||||
// app boot file (/src/boot)
|
// app boot file (/src/boot)
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/boot-files
|
// https://v2.quasar.dev/quasar-cli-webpack/boot-files
|
||||||
boot: ['i18n', 'axios', 'error-handler', 'app'],
|
boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'],
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
|
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
|
||||||
css: ['app.scss', 'width.scss', 'responsive.scss'],
|
css: ['app.scss', 'width.scss', 'responsive.scss'],
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
|
||||||
|
export default boot(() => {
|
||||||
|
Date.vnUTC = () => {
|
||||||
|
const env = process.env.NODE_ENV;
|
||||||
|
if (!env || env === 'development') {
|
||||||
|
return new Date(Date.UTC(2001, 0, 1, 11));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
Date.vnNew = () => {
|
||||||
|
return new Date(Date.vnUTC());
|
||||||
|
};
|
||||||
|
|
||||||
|
Date.vnNow = () => {
|
||||||
|
return new Date(Date.vnUTC()).getTime();
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1,174 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, watch, computed, ref } from 'vue';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const model = defineModel({ type: String });
|
||||||
|
const props = defineProps({
|
||||||
|
isOutlined: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const requiredFieldRule = val => !!val || t('globals.fieldRequired');
|
||||||
|
|
||||||
|
const dateFormat = 'DD/MM/YYYY';
|
||||||
|
const isPopupOpen = ref();
|
||||||
|
const hover = ref();
|
||||||
|
const mask = ref();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// fix quasar bug
|
||||||
|
mask.value = '##/##/####';
|
||||||
|
});
|
||||||
|
|
||||||
|
const styleAttrs = computed(() => {
|
||||||
|
return props.isOutlined
|
||||||
|
? {
|
||||||
|
dense: true,
|
||||||
|
outlined: true,
|
||||||
|
rounded: true
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedDate = computed({
|
||||||
|
get() {
|
||||||
|
if (!model.value) return model.value;
|
||||||
|
return date.formatDate(new Date(model.value), dateFormat);
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (value === model.value) return;
|
||||||
|
let newDate;
|
||||||
|
if (value) {
|
||||||
|
// parse input
|
||||||
|
if (value.includes('/')) {
|
||||||
|
if (value.length === 6) {
|
||||||
|
value = value + new Date().getFullYear();
|
||||||
|
}
|
||||||
|
if (value.length >= 10) {
|
||||||
|
if (value.at(2) === '/') {
|
||||||
|
value = value.split('/').reverse().join('/');
|
||||||
|
}
|
||||||
|
value = date.formatDate(
|
||||||
|
new Date(value).toISOString(),
|
||||||
|
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [year, month, day] = value.split('-').map(e => parseInt(e));
|
||||||
|
newDate = new Date(year, month - 1, day);
|
||||||
|
if (model.value) {
|
||||||
|
const orgDate =
|
||||||
|
model.value instanceof Date
|
||||||
|
? model.value
|
||||||
|
: new Date(model.value);
|
||||||
|
|
||||||
|
newDate.setHours(
|
||||||
|
orgDate.getHours(),
|
||||||
|
orgDate.getMinutes(),
|
||||||
|
orgDate.getSeconds(),
|
||||||
|
orgDate.getMilliseconds()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isNaN(newDate)) model.value = newDate.toISOString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const popupDate = computed(() =>
|
||||||
|
model.value
|
||||||
|
? date.formatDate(new Date(model.value), 'YYYY/MM/DD')
|
||||||
|
: model.value
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => model.value,
|
||||||
|
val => (formattedDate.value = val),
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div @mouseover="hover = true" @mouseleave="hover = false">
|
||||||
|
<QInput
|
||||||
|
v-model="formattedDate"
|
||||||
|
class="vn-input-date"
|
||||||
|
:mask="mask"
|
||||||
|
placeholder="dd/mm/aaaa"
|
||||||
|
v-bind="{ ...$attrs, ...styleAttrs }"
|
||||||
|
:class="{ required: $attrs.required }"
|
||||||
|
:rules="$attrs.required ? [requiredFieldRule] : null"
|
||||||
|
:clearable="false"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<QIcon
|
||||||
|
name="close"
|
||||||
|
size="xs"
|
||||||
|
v-if="
|
||||||
|
($attrs.clearable == undefined || $attrs.clearable) &&
|
||||||
|
hover &&
|
||||||
|
model &&
|
||||||
|
!$attrs.disable
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
model = null;
|
||||||
|
isPopupOpen = false;
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<QIcon
|
||||||
|
name="event"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="isPopupOpen = !isPopupOpen"
|
||||||
|
:title="t('openDate')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<QMenu
|
||||||
|
transition-show="scale"
|
||||||
|
transition-hide="scale"
|
||||||
|
v-model="isPopupOpen"
|
||||||
|
anchor="bottom left"
|
||||||
|
self="top start"
|
||||||
|
:no-focus="true"
|
||||||
|
:no-parent-event="true"
|
||||||
|
>
|
||||||
|
<QDate
|
||||||
|
v-model="popupDate"
|
||||||
|
:landscape="true"
|
||||||
|
:today-btn="true"
|
||||||
|
color="accent"
|
||||||
|
@update:model-value="
|
||||||
|
date => {
|
||||||
|
formattedDate = date;
|
||||||
|
isPopupOpen = false;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</QMenu>
|
||||||
|
</QInput>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
.vn-input-date.q-field--standard.q-field--readonly .q-field__control:before {
|
||||||
|
border-bottom-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vn-input-date.q-field--outlined.q-field--readonly .q-field__control:before {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
en-US:
|
||||||
|
openDate: Open date
|
||||||
|
es-ES:
|
||||||
|
openDate: Abrir fecha
|
||||||
|
ca-ES:
|
||||||
|
openDate: Obrir data
|
||||||
|
fr-FR:
|
||||||
|
openDate: Ouvrir la date
|
||||||
|
pt-PT:
|
||||||
|
openDate: Abrir data
|
||||||
|
</i18n>
|
|
@ -54,5 +54,6 @@ export default {
|
||||||
controlPanel: 'Panell de control',
|
controlPanel: 'Panell de control',
|
||||||
adminConnections: 'Connexions',
|
adminConnections: 'Connexions',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Comanda carregada a la cistella!'
|
orderLoadedIntoBasket: 'Comanda carregada a la cistella!',
|
||||||
|
at: 'a les'
|
||||||
};
|
};
|
||||||
|
|
|
@ -68,6 +68,7 @@ export default {
|
||||||
adminConnections: 'Connections',
|
adminConnections: 'Connections',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Order loaded into basket!',
|
orderLoadedIntoBasket: 'Order loaded into basket!',
|
||||||
|
at: 'at',
|
||||||
|
|
||||||
orders: 'Orders',
|
orders: 'Orders',
|
||||||
order: 'Pending order',
|
order: 'Pending order',
|
||||||
|
|
|
@ -74,6 +74,7 @@ export default {
|
||||||
adminConnections: 'Conexiones',
|
adminConnections: 'Conexiones',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
|
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
|
||||||
|
at: 'a las',
|
||||||
|
|
||||||
orders: 'Pedidos',
|
orders: 'Pedidos',
|
||||||
order: 'Pedido pendiente',
|
order: 'Pedido pendiente',
|
||||||
|
|
|
@ -54,5 +54,6 @@ export default {
|
||||||
controlPanel: 'Panneau de configuration',
|
controlPanel: 'Panneau de configuration',
|
||||||
adminConnections: 'Connexions',
|
adminConnections: 'Connexions',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Commande chargée dans le panier!'
|
orderLoadedIntoBasket: 'Commande chargée dans le panier!',
|
||||||
|
at: 'à'
|
||||||
};
|
};
|
||||||
|
|
|
@ -55,5 +55,6 @@ export default {
|
||||||
controlPanel: 'Painel de controle',
|
controlPanel: 'Painel de controle',
|
||||||
adminConnections: 'Conexões',
|
adminConnections: 'Conexões',
|
||||||
//
|
//
|
||||||
orderLoadedIntoBasket: 'Pedido carregado na cesta!'
|
orderLoadedIntoBasket: 'Pedido carregado na cesta!',
|
||||||
|
at: 'às'
|
||||||
};
|
};
|
||||||
|
|
|
@ -189,7 +189,7 @@ const logoutSupplantedUser = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
#actions > div {
|
#actions {
|
||||||
.q-btn {
|
.q-btn {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
|
@ -14,16 +14,32 @@ export function date(val, format) {
|
||||||
return qdate.formatDate(val, format, i18n.global.tm('date'));
|
return qdate.formatDate(val, format, i18n.global.tm('date'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatDateTitle = timeStamp => {
|
/**
|
||||||
|
* @param {Date} timeStamp - La marca de tiempo que se va a formatear. Si no se proporciona, la función devolverá una cadena vacía.
|
||||||
|
* @param {Object} options - Un objeto que contiene las opciones de formato.
|
||||||
|
* @param {boolean} options.showTime - Indica si se debe mostrar la hora en el formato de la fecha.
|
||||||
|
* @param {boolean} options.showSeconds - Indica si se deben mostrar los segundos en el formato de la hora. Solo se aplica si showTime es true.
|
||||||
|
* @param {boolean} options.shortDay - Indica si se debe usar una versión corta del día (por ejemplo, "Mon" en lugar de "Monday").
|
||||||
|
* @returns {string} La fecha formateada como un título.
|
||||||
|
*/
|
||||||
|
export const formatStringDate = (timeStamp, options) => {
|
||||||
|
if (!timeStamp) return '';
|
||||||
|
|
||||||
const { t, messages, locale } = i18n.global;
|
const { t, messages, locale } = i18n.global;
|
||||||
const formattedString = qdate.formatDate(
|
|
||||||
timeStamp,
|
const timeFormat = options.showTime
|
||||||
`dddd, D [${t('of')}] MMMM [${t('of')}] YYYY`,
|
? options.showSeconds
|
||||||
{
|
? ` [${t('at')}] hh:mm:ss`
|
||||||
days: messages.value[locale.value].date.days,
|
: ` [${t('at')}] hh:mm`
|
||||||
months: messages.value[locale.value].date.months
|
: '';
|
||||||
}
|
const day = options.shortDay ? 'dd' : 'dddd';
|
||||||
);
|
|
||||||
|
const formatString = `${day}, D [${t('of')}] MMMM [${t('of')}] YYYY${timeFormat}`;
|
||||||
|
|
||||||
|
const formattedString = qdate.formatDate(timeStamp, formatString, {
|
||||||
|
days: messages.value[locale.value].date.days,
|
||||||
|
months: messages.value[locale.value].date.months
|
||||||
|
});
|
||||||
return formattedString;
|
return formattedString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -64,16 +64,3 @@ onMounted(async () => getLinks());
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject, watch, computed } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { date as qdate } from 'quasar';
|
||||||
|
|
||||||
|
import VnInputDate from 'src/components/common/VnInputDate.vue';
|
||||||
|
|
||||||
|
import { formatStringDate } from 'src/lib/filters.js';
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const jApi = inject('jApi');
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const from = ref(Date.vnNew(route.query.from) || Date.vnNew());
|
||||||
|
const to = ref(Date.vnNew(route.query.to) || Date.vnNew());
|
||||||
|
const visitsData = ref(null);
|
||||||
|
|
||||||
|
const getVisits = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const [visitsResponse] = await jApi.query(
|
||||||
|
`SELECT browser,
|
||||||
|
MIN(CAST(version AS DECIMAL(4,1))) minVersion,
|
||||||
|
MAX(CAST(version AS DECIMAL(4,1))) maxVersion,
|
||||||
|
MAX(c.stamp) lastVisit,
|
||||||
|
COUNT(DISTINCT c.id) visits,
|
||||||
|
SUM(a.firstAccessFk = c.id AND v.firstAgentFk = a.id) newVisits
|
||||||
|
FROM visitUser e
|
||||||
|
JOIN visitAccess c ON c.id = e.accessFk
|
||||||
|
JOIN visitAgent a ON a.id = c.agentFk
|
||||||
|
JOIN visit v ON v.id = a.visitFk
|
||||||
|
WHERE c.stamp BETWEEN TIMESTAMP(#from,'00:00:00') AND TIMESTAMP(#to,'23:59:59')
|
||||||
|
GROUP BY browser ORDER BY visits DESC`,
|
||||||
|
{
|
||||||
|
from: qdate.formatDate(from.value, 'YYYY-MM-DD'),
|
||||||
|
to: qdate.formatDate(to.value, 'YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
visitsData.value = visitsResponse;
|
||||||
|
loading.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting visits:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const visitsCardText = computed(
|
||||||
|
() =>
|
||||||
|
`${visitsData?.value?.visits || 0} ${t('visits')}, ${visitsData?.value?.newVisits || 0} ${t('news')}`
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => from.value, () => to.value],
|
||||||
|
async () => {
|
||||||
|
await router.replace({
|
||||||
|
query: {
|
||||||
|
from: qdate.formatDate(from.value, 'YYYY-MM-DD'),
|
||||||
|
to: qdate.formatDate(to.value, 'YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await getVisits();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport v-if="isHeaderMounted" to="#actions">
|
||||||
|
<QBtn
|
||||||
|
:label="t('refresh')"
|
||||||
|
icon="refresh"
|
||||||
|
@click="getVisits()"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
class="q-mr-sm"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
:label="t('connections')"
|
||||||
|
icon="visibility"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
:to="{ name: 'adminConnections' }"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<QPage class="vn-w-xs column">
|
||||||
|
<QCard class="column q-pa-lg q-mb-md">
|
||||||
|
<VnInputDate :label="t('from')" v-model="from" class="q-mb-sm" />
|
||||||
|
<VnInputDate :label="t('to')" v-model="to" />
|
||||||
|
</QCard>
|
||||||
|
<QCard v-if="!loading" class="q-pa-lg flex q-mb-md">
|
||||||
|
<span class="full-width text-right text-h6">
|
||||||
|
{{ visitsCardText }}
|
||||||
|
</span>
|
||||||
|
</QCard>
|
||||||
|
<QCard v-if="!loading" class="q-pa-lg column">
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
visitsData?.browser &&
|
||||||
|
visitsData?.minVersion &&
|
||||||
|
visitsData?.maxVersion
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ visitsData?.browser }} - {{ visitsData?.minVersion }} -
|
||||||
|
{{ visitsData?.maxVersion }}
|
||||||
|
</span>
|
||||||
|
<span>{{ visitsCardText }}</span>
|
||||||
|
<span v-if="visitsData">
|
||||||
|
{{
|
||||||
|
formatStringDate(visitsData.lastVisit, {
|
||||||
|
showTime: true,
|
||||||
|
showSeconds: true,
|
||||||
|
shortDay: true
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</QCard>
|
||||||
|
<QSpinner
|
||||||
|
v-else
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
:thickness="2"
|
||||||
|
style="margin: 0 auto"
|
||||||
|
/>
|
||||||
|
</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:
|
||||||
|
from: From
|
||||||
|
to: To
|
||||||
|
visits: Visits
|
||||||
|
news: New
|
||||||
|
connections: Connections
|
||||||
|
refresh: Refresh
|
||||||
|
es-ES:
|
||||||
|
from: Desde
|
||||||
|
to: Hasta
|
||||||
|
visits: Visitas
|
||||||
|
news: Nuevas
|
||||||
|
connections: Conexiones
|
||||||
|
refresh: Actualizar
|
||||||
|
ca-ES:
|
||||||
|
from: Desde
|
||||||
|
to: Fins
|
||||||
|
visits: Visites
|
||||||
|
news: Noves
|
||||||
|
connections: Connexions
|
||||||
|
refresh: Actualitzar
|
||||||
|
fr-FR:
|
||||||
|
from: À partir de
|
||||||
|
to: Jusqu'à
|
||||||
|
visits: Visites
|
||||||
|
news: Nouveau
|
||||||
|
connections: Connexions
|
||||||
|
refresh: Rafraîchir
|
||||||
|
pt-PT:
|
||||||
|
from: Desde
|
||||||
|
to: Até
|
||||||
|
visits: Visitas
|
||||||
|
news: Novo
|
||||||
|
connections: Conexões
|
||||||
|
refresh: Atualizar
|
||||||
|
</i18n>
|
|
@ -81,8 +81,8 @@ const routes = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'adminVisits',
|
name: 'adminVisits',
|
||||||
path: 'admin/visits'
|
path: 'admin/visits',
|
||||||
// component: () => import('pages/Admin/VisitsView.vue')
|
component: () => import('pages/Admin/VisitsView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'adminNews',
|
name: 'adminNews',
|
||||||
|
|
Loading…
Reference in New Issue