Modulo Administración #78
|
@ -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'],
|
||||
|
|
|
@ -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',
|
||||
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',
|
||||
//
|
||||
orderLoadedIntoBasket: 'Order loaded into basket!',
|
||||
at: 'at',
|
||||
|
||||
orders: 'Orders',
|
||||
order: 'Pending order',
|
||||
|
|
|
@ -74,6 +74,7 @@ export default {
|
|||
adminConnections: 'Conexiones',
|
||||
//
|
||||
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
|
||||
at: 'a las',
|
||||
|
||||
orders: 'Pedidos',
|
||||
order: 'Pedido pendiente',
|
||||
|
|
|
@ -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: 'à'
|
||||
};
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
|
|
@ -189,7 +189,7 @@ const logoutSupplantedUser = async () => {
|
|||
}
|
||||
|
||||
@include mobile {
|
||||
#actions > div {
|
||||
#actions {
|
||||
.q-btn {
|
||||
border-radius: 50%;
|
||||
padding: 10px;
|
||||
|
|
|
@ -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`,
|
||||
{
|
||||
days: messages.value[locale.value].date.days,
|
||||
months: messages.value[locale.value].date.months
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
path: 'admin/visits'
|
||||
// component: () => import('pages/Admin/VisitsView.vue')
|
||||
path: 'admin/visits',
|
||||
component: () => import('pages/Admin/VisitsView.vue')
|
||||
},
|
||||
{
|
||||
name: 'adminNews',
|
||||
|
|
Loading…
Reference in New Issue