Visits view

This commit is contained in:
William Buezas 2024-08-08 11:24:43 -03:00
parent 76b99ed293
commit 2a1cd59492
13 changed files with 422 additions and 29 deletions

View File

@ -24,7 +24,7 @@ module.exports = configure(function (ctx) {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// 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
css: ['app.scss', 'width.scss', 'responsive.scss'],

20
src/boot/vnDate.js Normal file
View File

@ -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();
};
});

View File

@ -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>

View File

@ -54,5 +54,6 @@ export default {
controlPanel: 'Panell de control',
adminConnections: 'Connexions',
//
orderLoadedIntoBasket: 'Comanda carregada a la cistella!'
orderLoadedIntoBasket: 'Comanda carregada a la cistella!',
at: 'a les'
};

View File

@ -68,6 +68,7 @@ export default {
adminConnections: 'Connections',
//
orderLoadedIntoBasket: 'Order loaded into basket!',
at: 'at',
orders: 'Orders',
order: 'Pending order',

View File

@ -74,6 +74,7 @@ export default {
adminConnections: 'Conexiones',
//
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
at: 'a las',
orders: 'Pedidos',
order: 'Pedido pendiente',

View File

@ -54,5 +54,6 @@ export default {
controlPanel: 'Panneau de configuration',
adminConnections: 'Connexions',
//
orderLoadedIntoBasket: 'Commande chargée dans le panier!'
orderLoadedIntoBasket: 'Commande chargée dans le panier!',
at: 'à'
};

View File

@ -55,5 +55,6 @@ export default {
controlPanel: 'Painel de controle',
adminConnections: 'Conexões',
//
orderLoadedIntoBasket: 'Pedido carregado na cesta!'
orderLoadedIntoBasket: 'Pedido carregado na cesta!',
at: 'às'
};

View File

@ -189,7 +189,7 @@ const logoutSupplantedUser = async () => {
}
@include mobile {
#actions > div {
#actions {
.q-btn {
border-radius: 50%;
padding: 10px;

View File

@ -14,16 +14,32 @@ export function date(val, format) {
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 formattedString = qdate.formatDate(
timeStamp,
`dddd, D [${t('of')}] MMMM [${t('of')}] YYYY`,
{
const timeFormat = options.showTime
? options.showSeconds
? ` [${t('at')}] hh:mm:ss`
: ` [${t('at')}] hh:mm`
: '';
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;
};

View File

@ -64,16 +64,3 @@ onMounted(async () => getLinks());
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,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>

View File

@ -81,8 +81,8 @@ const routes = [
},
{
name: 'adminVisits',
path: 'admin/visits'
// component: () => import('pages/Admin/VisitsView.vue')
path: 'admin/visits',
component: () => import('pages/Admin/VisitsView.vue')
},
{
name: 'adminNews',