ADD: Notificaciones en tiempo real (#3)
* ADD: Avances * ADD: Usuarios conectados en tiempo real
This commit is contained in:
parent
d7d83b69a0
commit
24edbfebb4
@ -2,7 +2,7 @@
|
||||
"name": "notsoweb.frontend",
|
||||
"copyright": "Notsoweb Software Inc.",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"version": "0.9.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import { onBeforeMount, onMounted } from 'vue';
|
||||
import { bootPermissions } from '@Plugins/RolePermission.js';
|
||||
import { onMounted } from 'vue';
|
||||
import useDarkMode from '@Stores/DarkMode'
|
||||
import useLeftSidebar from '@Stores/LeftSidebar'
|
||||
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
||||
import useNotifier from '@Stores/Notifier'
|
||||
|
||||
import Header from '../Skeleton/Header.vue';
|
||||
import LeftSidebar from '../Skeleton/Sidebar/Left.vue';
|
||||
@ -13,6 +13,7 @@ import NotificationSidebar from '../Skeleton/Sidebar/Notification.vue';
|
||||
const darkMode = useDarkMode();
|
||||
const leftSidebar = useLeftSidebar();
|
||||
const notificationSidebar = useNotificationSidebar();
|
||||
const notifier = useNotifier();
|
||||
|
||||
/** Propiedades */
|
||||
defineProps({
|
||||
@ -20,13 +21,11 @@ defineProps({
|
||||
});
|
||||
|
||||
/** Ciclos */
|
||||
onBeforeMount(() => {
|
||||
bootPermissions()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
leftSidebar.boot()
|
||||
darkMode.boot()
|
||||
leftSidebar.boot();
|
||||
darkMode.boot();
|
||||
notifier.boot();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ defineProps({
|
||||
{{ title }}
|
||||
</p>
|
||||
<p v-if="subtitle"
|
||||
class="text-sm text-primary-on dark:text-primary-dark-on"
|
||||
class="text-sm text-gray-50"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
|
||||
@ -13,10 +13,7 @@ const emit = defineEmits([
|
||||
const props = defineProps({
|
||||
editable: Boolean,
|
||||
show: Boolean,
|
||||
title: {
|
||||
default: Lang('details'),
|
||||
type: String
|
||||
}
|
||||
title: String
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -25,7 +22,7 @@ const props = defineProps({
|
||||
<template #title>
|
||||
<p
|
||||
class="font-bold text-xl"
|
||||
v-text="title"
|
||||
v-text="title ?? $t('details')"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
|
||||
@ -24,6 +24,12 @@ const query = ref('');
|
||||
const search = () => {
|
||||
emit('search', query.value);
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
query.value = '';
|
||||
|
||||
search();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="title" class="flex w-full justify-center">
|
||||
@ -35,12 +41,19 @@ const search = () => {
|
||||
<div class="flex w-full justify-between items-center border-y-2 border-page-t dark:border-page-dt">
|
||||
<div>
|
||||
<div class="relative py-1 z-0">
|
||||
<div @click="search" class="absolute inset-y-0 right-2 flex items-center pl-3 cursor-pointer text-gray-700 hover:scale-110 hover:text-danger">
|
||||
<div @click="search" class="absolute inset-y-0 right-2 flex items-center pl-3 cursor-pointer">
|
||||
<GoogleIcon
|
||||
:title="$t('search')"
|
||||
class="text-xl"
|
||||
class="text-xl text-gray-700 hover:scale-110 hover:text-danger"
|
||||
name="search"
|
||||
/>
|
||||
<GoogleIcon
|
||||
v-show="query"
|
||||
:title="$t('clear')"
|
||||
class="text-xl text-gray-700 hover:scale-110 hover:text-danger"
|
||||
name="close"
|
||||
@click="clear"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
id="search"
|
||||
@ -54,7 +67,7 @@ const search = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-sm" id="buttons">
|
||||
<div class="flex items-center space-x-1 text-sm" id="buttons">
|
||||
<slot />
|
||||
<RouterLink :to="$view({name:'index'})">
|
||||
<IconButton
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
<script setup>
|
||||
import { users } from '@Plugins/AuthUsers'
|
||||
import { hasPermission } from '@Plugins/RolePermission'
|
||||
import { logout } from '@Services/Page';
|
||||
import useDarkMode from '@Stores/DarkMode'
|
||||
import useLeftSidebar from '@Stores/LeftSidebar'
|
||||
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
||||
import useNotifier from '@Stores/Notifier'
|
||||
import useLoader from '@Stores/Loader';
|
||||
|
||||
import Loader from '@Shared/Loader.vue';
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
import Dropdown from '../Dropdown.vue';
|
||||
import DropdownLink from '../DropdownLink.vue';
|
||||
@ -19,6 +23,8 @@ const darkMode = useDarkMode()
|
||||
const leftSidebar = useLeftSidebar()
|
||||
const notificationSidebar = useNotificationSidebar()
|
||||
const notifier = useNotifier()
|
||||
const loader = useLoader()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -36,6 +42,19 @@ const notifier = useNotifier()
|
||||
/>
|
||||
<div class="flex w-fit justify-end items-center h-14 header-right">
|
||||
<ul class="flex items-center space-x-2">
|
||||
<li v-if="loader.isProcessing" class="flex items-center">
|
||||
<Loader />
|
||||
</li>
|
||||
<li v-if="hasPermission('users.online')">
|
||||
<RouterLink :to="{ name: 'admin.users.online' }" class="flex items-center">
|
||||
<GoogleIcon
|
||||
class="text-xl mt-1"
|
||||
name="connect_without_contact"
|
||||
:title="$t('notifications.title')"
|
||||
/>
|
||||
<span class="text-xs">{{ users.length - 1 }}</span>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<GoogleIcon
|
||||
class="text-xl mt-1"
|
||||
|
||||
@ -1,15 +1,23 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
||||
import useNotifier from '@Stores/Notifier'
|
||||
|
||||
import ModalController from '@Controllers/ModalController.js';
|
||||
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
import Item from './Notification/Item.vue';
|
||||
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||
import ShowView from '@Holos/Skeleton/Sidebar/Notification/Show.vue';
|
||||
|
||||
/** Definidores */
|
||||
const notifier = useNotifier();
|
||||
const notificationSidebar = useNotificationSidebar()
|
||||
|
||||
/** Controladores */
|
||||
const Modal = new ModalController();
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(['open']);
|
||||
|
||||
@ -18,10 +26,9 @@ const props = defineProps({
|
||||
sidebar: Boolean
|
||||
});
|
||||
|
||||
/** Ciclos */
|
||||
onMounted(() => {
|
||||
notifier.boot();
|
||||
});
|
||||
const showModal = ref(Modal.showModal);
|
||||
const modelModal = ref(Modal.modelModal);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -37,9 +44,14 @@ onMounted(() => {
|
||||
<div class="flex flex-col h-full p-2 md:w-64">
|
||||
<div class="flex h-full flex-col w-[15.5rem] justify-between rounded-lg overflow-y-auto overflow-x-hidden bg-primary/70 text-primary-t dark:bg-primary-d/70 dark:text-primary-dt">
|
||||
<div class="flex justify-between px-2 items-center">
|
||||
<h4 class="text-md text-center py-1 font-semibold">
|
||||
<div class="py-1">
|
||||
<h4 class="text-md font-semibold">
|
||||
{{ $t('notifications.title') }} <span class="text-xs">({{ notifier.counter }})</span>
|
||||
</h4>
|
||||
<h4 class="text-xs font-semibold" v-if="notifier.unreadClosedCounter > 0">
|
||||
{{ $t('notifications.unreadClosed') }} <span class="text-xs"> ({{ notifier.unreadClosedCounter }})</span>
|
||||
</h4>
|
||||
</div>
|
||||
<GoogleIcon
|
||||
name="close"
|
||||
class="text-primary-t dark:text-primary-dt cursor-pointer"
|
||||
@ -47,15 +59,35 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
<div class="flex h-full flex-col space-y-1">
|
||||
<ul class="px-2 space-y-2 h-[calc(100vh-6.5rem)] overflow-y-auto">
|
||||
<ul class="px-2 space-y-1 overflow-y-auto"
|
||||
:class="{
|
||||
'h-[calc(100vh-10rem)]': notifier.unreadClosedCounter > 0,
|
||||
'h-[calc(100vh-9rem)]': notifier.unreadClosedCounter === 0
|
||||
}"
|
||||
>
|
||||
<Item v-for="notification in notifier.notifications"
|
||||
:key="notification.id"
|
||||
:notification="notification"
|
||||
@openModal="Modal.switchShowModal(notification)"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex justify-center items-center pb-1">
|
||||
<RouterLink :to="$view({ name: 'profile.notifications.index' })">
|
||||
<PrimaryButton type="button" @click="notificationSidebar.close()">
|
||||
{{ $t('notifications.seeAll') }}
|
||||
</PrimaryButton>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ShowView
|
||||
:show="showModal"
|
||||
:model="modelModal"
|
||||
@close="Modal.switchShowModal"
|
||||
@reload="notifier.getUpdates()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -6,6 +6,11 @@ import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
/** Definidores */
|
||||
const notifier = useNotifier();
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits([
|
||||
'openModal'
|
||||
]);
|
||||
|
||||
/** Propiedades */
|
||||
defineProps({
|
||||
notification: Object,
|
||||
@ -22,12 +27,12 @@ defineProps({
|
||||
<GoogleIcon
|
||||
name="close"
|
||||
class="text-xs text-white cursor-pointer"
|
||||
@click="notifier.readNotification(notification.id)"
|
||||
@click="notifier.closeNotification(notification.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<div class="w-10 space-y-0">
|
||||
<div class="flex w-full cursor-pointer">
|
||||
<div class="w-10 space-y-0" @click="emit('openModal', notification)">
|
||||
<template v-if="notification.user">
|
||||
<div class="w-10 h-10 bg-transparent rounded-full flex items-center justify-center">
|
||||
<img v-if="notification.user"
|
||||
@ -56,6 +61,10 @@ defineProps({
|
||||
v-text="notification.data.title"
|
||||
class="text-sm font-medium truncate"
|
||||
/>
|
||||
<div
|
||||
v-text="notification.data.description"
|
||||
class="text-xs w-40 font-thin truncate"
|
||||
/>
|
||||
<div v-if="notification.user"
|
||||
v-text="`~ ${notification.user.name} ${notification.user.paternal}`"
|
||||
class="text-xs text-gray-400 truncate"
|
||||
|
||||
74
src/components/Holos/Skeleton/Sidebar/Notification/Show.vue
Normal file
74
src/components/Holos/Skeleton/Sidebar/Notification/Show.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onUpdated } from 'vue';
|
||||
import { getDateTime } from '@Controllers/DateController';
|
||||
import useNotifier from '@Stores/Notifier';
|
||||
|
||||
import Header from '@Holos/Modal/Elements/Header.vue';
|
||||
import ShowModal from '@Holos/Modal/Show.vue';
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
|
||||
/** Definidores */
|
||||
const notifier = useNotifier();
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits([
|
||||
'close',
|
||||
'reload'
|
||||
]);
|
||||
|
||||
/** Propiedades */
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
model: Object
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
if(!props.model.read_at && props.show) {
|
||||
notifier.readNotification(props.model.id);
|
||||
}
|
||||
|
||||
if(!props.model.read_at && !props.show) {
|
||||
emit('reload');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ShowModal
|
||||
:show="show"
|
||||
@close="$emit('close')"
|
||||
>
|
||||
<Header
|
||||
:title="model.data.title"
|
||||
>
|
||||
</Header>
|
||||
<div class="py-2 border-b">
|
||||
<div class="flex w-full px-4 py-2">
|
||||
<GoogleIcon
|
||||
class="text-xl text-success"
|
||||
name="contact_mail"
|
||||
/>
|
||||
<div class="pl-3">
|
||||
<p class="font-bold text-lg leading-none pb-2">
|
||||
{{ $t('details') }}
|
||||
</p>
|
||||
<div class="flex flex-col">
|
||||
<b>{{ $t('description') }}: </b>
|
||||
{{ model.data.description }}
|
||||
</div>
|
||||
<div v-if="model.data.message" class="flex flex-col">
|
||||
<b>{{ $t('message') }}: </b>
|
||||
{{ model.data.message }}
|
||||
</div>
|
||||
<p>
|
||||
<b>{{ $t('created_at') }}: </b>
|
||||
{{ getDateTime(model.created_at) }}
|
||||
</p>
|
||||
<p v-if="model.read_at">
|
||||
<b>{{ $t('read_at') }}: </b>
|
||||
{{ getDateTime(model.read_at) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ShowModal>
|
||||
</template>
|
||||
@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import GoogleIcon from '../Shared/GoogleIcon.vue';
|
||||
import Loader from '../Shared/Loader.vue';
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits([
|
||||
@ -9,6 +10,7 @@ const emit = defineEmits([
|
||||
/** Propiedades */
|
||||
const props = defineProps({
|
||||
items: Object,
|
||||
processing: Boolean
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -16,13 +18,13 @@ const props = defineProps({
|
||||
<section class="pb-2">
|
||||
<div class="w-full overflow-hidden rounded-md shadow-lg">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<table v-if="!processing" class="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<slot name="head" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="">
|
||||
<tbody>
|
||||
<template v-if="items?.total > 0">
|
||||
<slot
|
||||
name="body"
|
||||
@ -36,6 +38,24 @@ const props = defineProps({
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<table v-else class="animate-pulse w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="100%" class="h-8 text-center">
|
||||
<div class="flex items-center justify-center">
|
||||
<Loader />
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="100%" class="table-item h-7 text-center">
|
||||
<div class="w-full h-4 bg-secondary/50 rounded-md"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -25,9 +25,6 @@ const props = defineProps({
|
||||
name="body"
|
||||
:items="items"
|
||||
/>
|
||||
<tr>
|
||||
<slot name="empty" />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
6
src/components/Shared/Loader.vue
Normal file
6
src/components/Shared/Loader.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<svg class="animate-spin -ml-1 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,90 +0,0 @@
|
||||
import { ref } from 'vue';
|
||||
import { api } from '@Services/Api.js';
|
||||
|
||||
/**
|
||||
* Controlador simple de las bandejas
|
||||
*/
|
||||
class SearcherController
|
||||
{
|
||||
route = '';
|
||||
params = {};
|
||||
query = ref('');
|
||||
|
||||
constructor({ route, model, params = {} }) {
|
||||
this.route = route;
|
||||
this.model = ref(model);
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Búsqueda simple
|
||||
*/
|
||||
search = (q = '', params) => {
|
||||
this.query.value = q;
|
||||
api.get(this._getRoute(), {
|
||||
params: {
|
||||
q: this.query.value,
|
||||
...params
|
||||
},
|
||||
onSuccess: (r) => {
|
||||
this.model.value = r.users;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Paginación simple
|
||||
*/
|
||||
withPagination = (page, params) => {
|
||||
api.get(this._getRoute(), {
|
||||
params: {
|
||||
page,
|
||||
...params
|
||||
},
|
||||
onSuccess: (r) => {
|
||||
this.model.value = r.users;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Búsqueda con Paginación en tablas
|
||||
*/
|
||||
searchWithPagination = (page, params) => {
|
||||
api.get(page, {
|
||||
params: {
|
||||
q: this.query.value,
|
||||
...params
|
||||
},
|
||||
onSuccess: (r) => {
|
||||
this.model.value = r.users;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Búsqueda con Paginación en bandejas
|
||||
*/
|
||||
searchWithInboxPagination = (page, params) => {
|
||||
api.get(page, {
|
||||
params: {
|
||||
q: this.query.value,
|
||||
...params
|
||||
},
|
||||
onSuccess: (r) => {
|
||||
this.model.value = r.users;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la ruta según los parámetros
|
||||
*/
|
||||
_getRoute = () => {
|
||||
return (this.params)
|
||||
? route(this.route, this.params)
|
||||
: route(this.route);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearcherController;
|
||||
23
src/index.js
23
src/index.js
@ -4,37 +4,46 @@ import axios from 'axios';
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
import { useRoute, ZiggyVue } from 'ziggy-js';
|
||||
import { i18n, lang } from '@/lang/i18n.js';
|
||||
import { i18n, lang } from '@Lang/i18n.js';
|
||||
import router from '@Router/Index'
|
||||
import Notify from '@Plugins/Notify'
|
||||
import { bootPermissions } from '@Plugins/RolePermission';
|
||||
import TailwindScreen from '@Plugins/TailwindScreen'
|
||||
import { pagePlugin } from '@Services/Page';
|
||||
import { reloadApp, view } from '@Services/Page';
|
||||
|
||||
import App from '@Layouts/AppLayout.vue'
|
||||
import Error503 from '@Pages/Errors/503.vue'
|
||||
|
||||
// Configurar axios
|
||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
// Crear instancias globales
|
||||
// Elementos globales
|
||||
window.axios = axios;
|
||||
window.Lang = lang;
|
||||
window.Notify = new Notify();
|
||||
window.TwScreen = new TailwindScreen();
|
||||
|
||||
async function boot() {
|
||||
let initRoutes = false;
|
||||
|
||||
// Iniciar rutas
|
||||
try {
|
||||
const routes = await axios.get(import.meta.env.VITE_API_URL + '/api/resources/routes');
|
||||
|
||||
// Iniciar rutas
|
||||
window.Ziggy = routes.data;
|
||||
window.route = useRoute();
|
||||
window.view = view;
|
||||
initRoutes = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Failed to load routes');
|
||||
window.Notify.error(window.Lang('server.api.noAvailable'));
|
||||
}
|
||||
|
||||
if(initRoutes) {
|
||||
// Iniciar permisos
|
||||
await bootPermissions();
|
||||
|
||||
// Iniciar broadcast
|
||||
if(import.meta.env.VITE_REVERB_ACTIVE === 'true') {
|
||||
await import('@Services/Broadcast')
|
||||
}
|
||||
@ -48,6 +57,10 @@ async function boot() {
|
||||
.use(router)
|
||||
.use(ZiggyVue)
|
||||
.mount('#app');
|
||||
} else {
|
||||
createApp(Error503)
|
||||
.mount('#app');
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar aplicación
|
||||
|
||||
@ -202,17 +202,21 @@ export default {
|
||||
import: 'Importar',
|
||||
items: 'Elementos',
|
||||
maternal:'Apellido materno',
|
||||
message:'Mensaje',
|
||||
menu:'Menú',
|
||||
name:'Nombre',
|
||||
noRecords:'Sin registros',
|
||||
notification:'Notificación',
|
||||
notifications: {
|
||||
unreadClosed:'Ocultas',
|
||||
readed:'Marcar como leído',
|
||||
deleted:'Notificación eliminada',
|
||||
description:'Notificaciones del usuario',
|
||||
notFound:'Notificación no encontrada',
|
||||
title:'Notificaciones',
|
||||
seeAll:'Ver todas',
|
||||
},
|
||||
omitted:'Omitida',
|
||||
password:'Contraseña',
|
||||
passwordConfirmation:'Confirmar contraseña',
|
||||
passwordCurrent:'Contraseña actual',
|
||||
@ -246,6 +250,7 @@ export default {
|
||||
},
|
||||
profile:'Perfil',
|
||||
readed:'Leído',
|
||||
read_at:'Fecha leído',
|
||||
register: {
|
||||
create: {
|
||||
onError: 'Error al crear el registro',
|
||||
@ -300,6 +305,11 @@ export default {
|
||||
search:'Buscar',
|
||||
selected: 'Seleccionado',
|
||||
select: 'Seleccionar',
|
||||
server: {
|
||||
api: {
|
||||
noAvailable: 'No se encontró el servidor API.'
|
||||
}
|
||||
},
|
||||
session: {
|
||||
closed: 'Sesión cerrada',
|
||||
},
|
||||
@ -354,6 +364,7 @@ export default {
|
||||
start: 'Hora inicial',
|
||||
end: 'Hora final',
|
||||
},
|
||||
title: 'Título',
|
||||
total: 'Total',
|
||||
unknown:'Desconocido',
|
||||
update:'Actualizar',
|
||||
@ -390,6 +401,11 @@ export default {
|
||||
},
|
||||
title:'Roles de usuario',
|
||||
},
|
||||
online: {
|
||||
description: 'Lista de usuarios conectados al sistema.',
|
||||
title: 'Usuarios conectados',
|
||||
count: 'Usuarios conectados.',
|
||||
},
|
||||
menu:'Menú de usuario',
|
||||
select:'Seleccionar un usuario',
|
||||
settings:'Ajustes del usuario',
|
||||
|
||||
@ -1,13 +1,25 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import useLoader from '@Stores/Loader';
|
||||
import { hasPermission } from '@Plugins/RolePermission';
|
||||
|
||||
import Layout from '@Holos/Layout/App.vue';
|
||||
import Link from '@Holos/Skeleton/Sidebar/Link.vue';
|
||||
import Section from '@Holos/Skeleton/Sidebar/Section.vue';
|
||||
|
||||
/** Definidores */
|
||||
const loader = useLoader()
|
||||
|
||||
/** Propiedades */
|
||||
defineProps({
|
||||
title: String,
|
||||
});
|
||||
|
||||
/** Ciclos */
|
||||
onMounted(() => {
|
||||
loader.boot()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -28,8 +40,12 @@ defineProps({
|
||||
to="profile.show"
|
||||
/>
|
||||
</Section>
|
||||
<Section :name="$t('admin.title')">
|
||||
<Section
|
||||
v-if="hasPermission('users.index')"
|
||||
:name="$t('admin.title')"
|
||||
>
|
||||
<Link
|
||||
v-if="hasPermission('users.index')"
|
||||
icon="people"
|
||||
name="users.title"
|
||||
to="admin.users.index"
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { can, apiTo, viewTo } from './Module'
|
||||
import { api } from '@Services/Api';
|
||||
import { useSearcher } from '@Services/Api';
|
||||
import { users } from '@Plugins/AuthUsers'
|
||||
|
||||
import ModalController from '@Controllers/ModalController.js';
|
||||
import SearcherController from '@Controllers/SearcherController.js';
|
||||
|
||||
import IconButton from '@Holos/Button/Icon.vue'
|
||||
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
||||
@ -23,27 +23,23 @@ const modelModal = ref(Modal.modelModal);
|
||||
|
||||
const models = ref([]);
|
||||
|
||||
const Searcher = new SearcherController({
|
||||
route: 'users.index',
|
||||
model: models
|
||||
const searcher = useSearcher({
|
||||
url: route('users.index'),
|
||||
onSuccess: (r) => models.value = r.models,
|
||||
onError: () => models.value = []
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
function load() {
|
||||
api.get(apiTo('index'), {
|
||||
onSuccess: (r) => models.value = r.users
|
||||
});
|
||||
}
|
||||
|
||||
/** Ciclos */
|
||||
onMounted(() => load());
|
||||
onMounted(() => {
|
||||
searcher.search();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SearcherHead
|
||||
:title="$t('users.title')"
|
||||
@search="Searcher.search"
|
||||
@search="(x) => searcher.search(x)"
|
||||
>
|
||||
<RouterLink
|
||||
v-if="can('create')"
|
||||
@ -60,7 +56,8 @@ onMounted(() => load());
|
||||
<div class="pt-2 w-full">
|
||||
<Table
|
||||
:items="models"
|
||||
@send-pagination="Searcher.searchWithPagination"
|
||||
@send-pagination="searcher.pagination"
|
||||
:processing="searcher.processing"
|
||||
>
|
||||
<template #head>
|
||||
<th v-text="$t('user')" />
|
||||
@ -166,7 +163,7 @@ onMounted(() => load());
|
||||
:show="destroyModal"
|
||||
:to="(user) => apiTo('destroy', { user })"
|
||||
@close="Modal.switchDestroyModal"
|
||||
@update="load"
|
||||
@update="searcher.search()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
150
src/pages/Admin/Users/Online.vue
Normal file
150
src/pages/Admin/Users/Online.vue
Normal file
@ -0,0 +1,150 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { can, apiTo, viewTo } from './Module'
|
||||
import { users } from '@Plugins/AuthUsers'
|
||||
|
||||
import ModalController from '@Controllers/ModalController.js';
|
||||
|
||||
import IconButton from '@Holos/Button/Icon.vue'
|
||||
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
||||
import Header from '@Holos/PageHeader.vue';
|
||||
import Table from '@Holos/TableSimple.vue';
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
import ShowView from './Modals/Show.vue';
|
||||
|
||||
/** Controladores */
|
||||
const Modal = new ModalController();
|
||||
|
||||
/** Propiedades */
|
||||
const destroyModal = ref(Modal.destroyModal);
|
||||
const showModal = ref(Modal.showModal);
|
||||
const modelModal = ref(Modal.modelModal);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Header
|
||||
:title="$t('users.online.title')"
|
||||
>
|
||||
<RouterLink
|
||||
v-if="can('create')"
|
||||
:to="viewTo({ name: 'create' })"
|
||||
>
|
||||
<IconButton
|
||||
class="text-white"
|
||||
icon="add"
|
||||
:title="$t('crud.create')"
|
||||
filled
|
||||
/>
|
||||
</RouterLink>
|
||||
</Header>
|
||||
<p class="mt-2">{{ $t('users.online.description') }} {{ users.length - 1 }} {{ $t('users.online.count') }}</p>
|
||||
<div class="w-full -mt-2">
|
||||
<Table
|
||||
:items="users"
|
||||
>
|
||||
<template #head>
|
||||
<th v-text="$t('profile')" class="w-10" />
|
||||
<th v-text="$t('name')" />
|
||||
<th v-text="$t('contact')" />
|
||||
<th
|
||||
v-text="$t('actions')"
|
||||
class="w-32 text-center"
|
||||
/>
|
||||
</template>
|
||||
<template #body="{items}">
|
||||
<template v-for="model in items">
|
||||
<tr v-if="model.id != 1">
|
||||
<td class="table-item border">
|
||||
<img :src="model.profile_photo_url" alt="Profile photo" class="w-10 h-10 rounded-full">
|
||||
</td>
|
||||
<td class="table-item border">
|
||||
{{ `${model.full_name}` }}
|
||||
</td>
|
||||
<td class="table-item border">
|
||||
<p>
|
||||
<a
|
||||
class="hover:underline"
|
||||
target="_blank"
|
||||
:href="`mailto:${model.email}`"
|
||||
>
|
||||
{{ model.email }}
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="model.phone" class="font-semibold text-xs">
|
||||
<b>Teléfono: </b>
|
||||
<a
|
||||
class="hover:underline"
|
||||
target="_blank"
|
||||
:href="`tel:${model.phone}`"
|
||||
>
|
||||
{{ model.phone }}
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
<td class="table-item">
|
||||
<div class="table-actions">
|
||||
<GoogleIcon
|
||||
class="btn-icon"
|
||||
name="visibility"
|
||||
:title="$t('crud.show')"
|
||||
@click="Modal.switchShowModal(model)"
|
||||
outline
|
||||
/>
|
||||
<RouterLink
|
||||
v-if="can('edit')"
|
||||
class="h-fit"
|
||||
:to="viewTo({ name: 'edit', params: { id: model.id } })"
|
||||
>
|
||||
<GoogleIcon
|
||||
class="btn-icon"
|
||||
name="edit"
|
||||
:title="$t('crud.edit')"
|
||||
outline
|
||||
/>
|
||||
</RouterLink>
|
||||
<GoogleIcon
|
||||
v-if="can('destroy')"
|
||||
class="btn-icon"
|
||||
name="delete"
|
||||
:title="$t('crud.destroy')"
|
||||
@click="Modal.switchDestroyModal(model)"
|
||||
outline
|
||||
/>
|
||||
<RouterLink
|
||||
v-if="can('settings')"
|
||||
class="h-fit"
|
||||
:to="viewTo({ name: 'settings', params: { id: model.id } })"
|
||||
>
|
||||
<GoogleIcon
|
||||
class="btn-icon"
|
||||
name="settings"
|
||||
:title="$t('setting')"
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<ShowView
|
||||
v-if="can('index')"
|
||||
:show="showModal"
|
||||
:model="modelModal"
|
||||
@close="Modal.switchShowModal"
|
||||
/>
|
||||
<DestroyView
|
||||
v-if="can('destroy')"
|
||||
:model="modelModal"
|
||||
:show="destroyModal"
|
||||
:to="(user) => apiTo('destroy', { user })"
|
||||
@close="Modal.switchDestroyModal"
|
||||
@update="searcher.search()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
14
src/pages/Errors/404.vue
Normal file
14
src/pages/Errors/404.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="relative flex items-top justify-center min-h-[calc(100vh-5rem)] sm:items-center sm:pt-0">
|
||||
<div class="max-w-xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="flex items-center pt-8 sm:justify-start sm:pt-0">
|
||||
<div class="px-4 text-lg text-gray-500 border-r border-gray-400 tracking-wider">
|
||||
404
|
||||
</div>
|
||||
<div class="ml-4 text-lg text-gray-500 uppercase tracking-wider">
|
||||
Not Found
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
14
src/pages/Errors/502.vue
Normal file
14
src/pages/Errors/502.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="relative flex items-top justify-center min-h-[calc(100vh-5rem)] sm:items-center sm:pt-0">
|
||||
<div class="max-w-xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="flex items-center pt-8 sm:justify-start sm:pt-0">
|
||||
<div class="px-4 text-lg text-gray-500 border-r border-gray-400 tracking-wider">
|
||||
502
|
||||
</div>
|
||||
<div class="ml-4 text-lg text-gray-500 uppercase tracking-wider">
|
||||
Bad Gateway
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
14
src/pages/Errors/503.vue
Normal file
14
src/pages/Errors/503.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="relative flex items-top justify-center min-h-[calc(100vh-5rem)] sm:items-center sm:pt-0">
|
||||
<div class="max-w-xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="flex items-center pt-8 sm:justify-start sm:pt-0">
|
||||
<div class="px-4 text-lg text-gray-500 border-r border-gray-400 tracking-wider">
|
||||
503
|
||||
</div>
|
||||
<div class="ml-4 text-lg text-gray-500 uppercase tracking-wider">
|
||||
Service Unavailable
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
140
src/pages/Profile/Notifications/Index.vue
Normal file
140
src/pages/Profile/Notifications/Index.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useSearcher } from '@Services/Api';
|
||||
|
||||
import ModalController from '@Controllers/ModalController.js';
|
||||
import { getDateTime } from '@Controllers/DateController.js';
|
||||
|
||||
import SearcherHead from '@Holos/Searcher.vue';
|
||||
import Table from '@Holos/Table.vue';
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
import IconButton from '@Holos/Button/Icon.vue';
|
||||
import ShowView from '@Holos/Skeleton/Sidebar/Notification/Show.vue';
|
||||
|
||||
/** Controladores */
|
||||
const Modal = new ModalController();
|
||||
|
||||
/** Propiedades */
|
||||
// const destroyModal = ref(Modal.destroyModal);
|
||||
const showModal = ref(Modal.showModal);
|
||||
const modelModal = ref(Modal.modelModal);
|
||||
|
||||
const models = ref([]);
|
||||
|
||||
const searcher = useSearcher({
|
||||
url: route('system.notifications.all'),
|
||||
onSuccess: (r) => models.value = r.models,
|
||||
onError: () => models.value = []
|
||||
});
|
||||
|
||||
/** Ciclos */
|
||||
onMounted(() => {
|
||||
searcher.search();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SearcherHead
|
||||
:title="$t('notifications.title')"
|
||||
@search="(x) => searcher.search(x)"
|
||||
>
|
||||
<IconButton
|
||||
icon="refresh"
|
||||
:title="$t('notifications.unreadClosed')"
|
||||
@click="searcher.search()"
|
||||
/>
|
||||
</SearcherHead>
|
||||
<div class="pt-2 w-full">
|
||||
<Table
|
||||
:items="models"
|
||||
@send-pagination="searcher.pagination"
|
||||
:processing="searcher.processing"
|
||||
>
|
||||
<template #head>
|
||||
<th v-text="$t('title')" />
|
||||
<th v-text="$t('description')" />
|
||||
<th
|
||||
v-text="$t('date')"
|
||||
class="w-40 text-center"
|
||||
/>
|
||||
<th
|
||||
v-text="$t('status')"
|
||||
class="w-32 text-center"
|
||||
/>
|
||||
<th
|
||||
v-text="$t('actions')"
|
||||
class="w-32 text-center"
|
||||
/>
|
||||
</template>
|
||||
<template #body="{items}">
|
||||
<tr v-for="model in items">
|
||||
<td class="table-item border">
|
||||
{{ model.data.title }}
|
||||
</td>
|
||||
<td class="table-item border">
|
||||
{{ model.data.description }}
|
||||
</td>
|
||||
<td class="table-item border">
|
||||
{{ getDateTime(model.created_at) }}
|
||||
</td>
|
||||
<td class="table-item border">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="w-2 h-2 rounded-full" :class="model.read_at ? 'bg-success' : 'bg-danger'"></div>
|
||||
<span class="ml-2">{{ model.read_at ? $t('readed') : (model.is_closed ? $t('omitted') : $t('unreaded')) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="table-item">
|
||||
<div class="table-actions">
|
||||
<GoogleIcon
|
||||
class="btn-icon"
|
||||
name="visibility"
|
||||
:title="$t('crud.show')"
|
||||
@click="Modal.switchShowModal(model)"
|
||||
outline
|
||||
/>
|
||||
<!-- <GoogleIcon
|
||||
v-if="can('destroy')"
|
||||
class="btn-icon"
|
||||
name="delete"
|
||||
:title="$t('crud.destroy')"
|
||||
@click="Modal.switchDestroyModal(model)"
|
||||
outline
|
||||
/> -->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template #empty>
|
||||
<td class="table-item border">
|
||||
<div class="flex items-center text-sm">
|
||||
<p class="font-semibold">
|
||||
{{ $t('registers.empty') }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="table-item border">-</td>
|
||||
<td class="table-item border">-</td>
|
||||
<td class="table-item border">-</td>
|
||||
<td class="table-item border">-</td>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<ShowView
|
||||
:show="showModal"
|
||||
:model="modelModal"
|
||||
@close="Modal.switchShowModal"
|
||||
@reload="searcher.search()"
|
||||
/>
|
||||
<!-- <DestroyView
|
||||
v-if="can('destroy')"
|
||||
:model="modelModal"
|
||||
:show="destroyModal"
|
||||
:to="(user) => apiTo('destroy', { user })"
|
||||
@close="Modal.switchDestroyModal"
|
||||
@update="getNotifications"
|
||||
/> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
21
src/pages/Profile/Notifications/Module.js
Normal file
21
src/pages/Profile/Notifications/Module.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { lang } from '@Lang/i18n';
|
||||
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||
|
||||
// Ruta API
|
||||
const apiTo = (name, params = {}) => route(`users.${name}`, params)
|
||||
|
||||
// Ruta visual
|
||||
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `admin.users.${name}`, params, query })
|
||||
|
||||
// Obtener traducción del componente
|
||||
const transl = (str) => lang(`users.${str}`)
|
||||
|
||||
// Determina si un usuario puede hacer algo no en base a los permisos
|
||||
const can = (permission) => hasPermission(`users.${permission}`)
|
||||
|
||||
export {
|
||||
can,
|
||||
viewTo,
|
||||
apiTo,
|
||||
transl
|
||||
}
|
||||
26
src/plugins/AuthUsers.js
Normal file
26
src/plugins/AuthUsers.js
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Usuarios autenticados
|
||||
*/
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
const users = ref([]);
|
||||
|
||||
function boot(x) {
|
||||
users.value = x;
|
||||
}
|
||||
|
||||
function addUser(user) {
|
||||
users.value.push(user);
|
||||
}
|
||||
|
||||
function removeUser(user) {
|
||||
users.value = users.value.filter(u => u.id !== user.id);
|
||||
}
|
||||
|
||||
export {
|
||||
users,
|
||||
boot,
|
||||
addUser,
|
||||
removeUser
|
||||
};
|
||||
@ -1,4 +1,3 @@
|
||||
import { lang } from '@Lang/i18n';
|
||||
import toastr from 'toastr';
|
||||
|
||||
class Notify {
|
||||
|
||||
@ -22,16 +22,23 @@ const hasPermission = (can) => {
|
||||
}
|
||||
|
||||
const bootPermissions = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!permissionsInit.value) {
|
||||
api.get(route('user.permissions'), {
|
||||
onSuccess: (res) => {
|
||||
loadPermissions(res.permissions)
|
||||
|
||||
resolve(true)
|
||||
},
|
||||
onFinish: () => {
|
||||
permissionsInit.value = true;
|
||||
},
|
||||
onError: () => {
|
||||
reject(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetPermissions = () => {
|
||||
@ -47,8 +54,13 @@ const loadPermissions = (permissionList = []) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getAllPermissions = () => {
|
||||
return allPermissions.value;
|
||||
}
|
||||
|
||||
export {
|
||||
bootPermissions,
|
||||
hasPermission,
|
||||
resetPermissions
|
||||
resetPermissions,
|
||||
getAllPermissions
|
||||
};
|
||||
@ -1,4 +1,13 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { hasPermission } from '@Plugins/RolePermission';
|
||||
|
||||
function can(next, can) {
|
||||
if (!hasPermission(can)) {
|
||||
next({ name: '404' });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
@ -9,8 +18,23 @@ const router = createRouter({
|
||||
component: () => import('@Pages/Dashboard/Index.vue')
|
||||
}, {
|
||||
path: '/profile',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'profile.show',
|
||||
component: () => import('@Pages/Profile/Show.vue')
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'profile.notifications.index',
|
||||
component: () => import('@Pages/Profile/Notifications/Index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}, {
|
||||
path: '/admin',
|
||||
children: [
|
||||
@ -20,24 +44,39 @@ const router = createRouter({
|
||||
{
|
||||
path: '',
|
||||
name: 'admin.users.index',
|
||||
beforeEnter: (to, from, next) => can(next, 'users.index'),
|
||||
component: () => import('@Pages/Admin/Users/Index.vue')
|
||||
},
|
||||
{
|
||||
path: 'online',
|
||||
name: 'admin.users.online',
|
||||
beforeEnter: (to, from, next) => can(next, 'users.online'),
|
||||
component: () => import('@Pages/Admin/Users/Online.vue')
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
name: 'admin.users.create',
|
||||
beforeEnter: (to, from, next) => can(next, 'users.create'),
|
||||
component: () => import('@Pages/Admin/Users/Create.vue')
|
||||
}, {
|
||||
path: ':id/edit',
|
||||
name: 'admin.users.edit',
|
||||
beforeEnter: (to, from, next) => can(next, 'users.edit'),
|
||||
component: () => import('@Pages/Admin/Users/Edit.vue')
|
||||
}, {
|
||||
path: ':id/settings',
|
||||
name: 'admin.users.settings',
|
||||
beforeEnter: (to, from, next) => can(next, 'users.settings'),
|
||||
component: () => import('@Pages/Admin/Users/Settings.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: '404',
|
||||
component: () => import('@Pages/Errors/404.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@ -244,6 +244,11 @@ const api = {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Instancia de la API
|
||||
*/
|
||||
const useApi = () => reactive(api);
|
||||
|
||||
/**
|
||||
* Instancia de la API para formularios
|
||||
*/
|
||||
@ -462,7 +467,7 @@ const useSearcher = (options = {
|
||||
async load({
|
||||
url,
|
||||
apiToken = token.value,
|
||||
filters = {}
|
||||
filters,
|
||||
}) {
|
||||
this.errors = {};
|
||||
this.processing = true;
|
||||
@ -478,7 +483,7 @@ const useSearcher = (options = {
|
||||
method: 'get',
|
||||
url,
|
||||
params: {
|
||||
query: this.query,
|
||||
q: this.query,
|
||||
...filters
|
||||
},
|
||||
headers: {
|
||||
@ -531,35 +536,46 @@ const useSearcher = (options = {
|
||||
|
||||
this.processing = false;
|
||||
},
|
||||
pagination(url, filters = {}) {
|
||||
pagination(url, filter = {}) {
|
||||
console.log(url, filter)
|
||||
this.load({
|
||||
url,
|
||||
filters
|
||||
filters : {
|
||||
...options.filters,
|
||||
...filter,
|
||||
}
|
||||
})
|
||||
},
|
||||
search(q, filters = {}) {
|
||||
search(q = '', filter = {}) {
|
||||
this.query = q
|
||||
this.load({
|
||||
url,
|
||||
filters
|
||||
url: options.url,
|
||||
filters : {
|
||||
...options.filters,
|
||||
...filter,
|
||||
}
|
||||
})
|
||||
},
|
||||
refresh(filters = {}) {
|
||||
refresh(filter = {}) {
|
||||
this.load({
|
||||
url,
|
||||
filters
|
||||
url: options.url,
|
||||
filters : {
|
||||
...options.filters,
|
||||
...filter,
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export {
|
||||
api,
|
||||
token,
|
||||
closeSession,
|
||||
hasToken,
|
||||
useForm,
|
||||
useSearcher,
|
||||
defineApiToken,
|
||||
defineCsrfToken,
|
||||
resetApiToken
|
||||
defineApiToken,
|
||||
hasToken,
|
||||
resetApiToken,
|
||||
useApi,
|
||||
useForm,
|
||||
useSearcher
|
||||
}
|
||||
35
src/stores/Loader.js
Normal file
35
src/stores/Loader.js
Normal file
@ -0,0 +1,35 @@
|
||||
import axios from 'axios';
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
// Almacén del modo oscuro
|
||||
const useLoader = defineStore('loader', {
|
||||
state: () => ({
|
||||
processing: false
|
||||
}),
|
||||
getters: {
|
||||
isProcessing(state) {
|
||||
return state.processing
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
boot() {
|
||||
axios.interceptors.request.use((config) => {
|
||||
this.processing = true
|
||||
return config;
|
||||
}, (error) => {
|
||||
this.processing = false
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
axios.interceptors.response.use((response) => {
|
||||
this.processing = false
|
||||
return response;
|
||||
}, (error) => {
|
||||
this.processing = false
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default useLoader
|
||||
@ -1,6 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '@Services/Api'
|
||||
import { page } from '@Services/Page'
|
||||
import { hasPermission } from '@Plugins/RolePermission'
|
||||
import { boot as bootAuthUsers, addUser, removeUser } from '@Plugins/AuthUsers'
|
||||
|
||||
/** Propiedades */
|
||||
const hasNotifications = import.meta.env.VITE_REVERB_ACTIVE === 'true';
|
||||
@ -9,6 +11,7 @@ const hasNotifications = import.meta.env.VITE_REVERB_ACTIVE === 'true';
|
||||
const useNotifier = defineStore('notifier', {
|
||||
state: () => ({
|
||||
counter: 0,
|
||||
unreadClosedCounter: 0,
|
||||
notifications: [],
|
||||
isStarted: false,
|
||||
user_id: 0,
|
||||
@ -21,7 +24,7 @@ const useNotifier = defineStore('notifier', {
|
||||
|
||||
this.subscribeGLobalNotifications();
|
||||
this.subscribeUserNotifications();
|
||||
|
||||
this.suscribeAuthUsers();
|
||||
this.isStarted = true;
|
||||
|
||||
this.getUpdates();
|
||||
@ -48,6 +51,23 @@ const useNotifier = defineStore('notifier', {
|
||||
unsubscribeGlobalNotifications() {
|
||||
Echo.leave('Global');
|
||||
},
|
||||
// Usuarios logueados
|
||||
suscribeAuthUsers() {
|
||||
if(hasPermission('users.index')) {
|
||||
Echo.join('online')
|
||||
.here((users) => {
|
||||
bootAuthUsers(users);
|
||||
})
|
||||
.joining((user) => {
|
||||
addUser(user);
|
||||
})
|
||||
.leaving((user) => {
|
||||
removeUser(user);
|
||||
});
|
||||
} else {
|
||||
Echo.join('online');
|
||||
}
|
||||
},
|
||||
// Notificaciones del usuario
|
||||
subscribeUserNotifications() {
|
||||
Echo.private(`App.Models.User.${this.user_id}`)
|
||||
@ -61,13 +81,26 @@ const useNotifier = defineStore('notifier', {
|
||||
},
|
||||
readNotification(id) {
|
||||
api.post(route('system.notifications.read'), {
|
||||
id,
|
||||
data: {
|
||||
id
|
||||
},
|
||||
onSuccess: res => {
|
||||
this.getUpdates();
|
||||
},
|
||||
onFailed: res => {
|
||||
this.getUpdates();
|
||||
}
|
||||
})
|
||||
},
|
||||
closeNotification(id) {
|
||||
api.post(route('system.notifications.close'), {
|
||||
data: {
|
||||
id
|
||||
},
|
||||
onSuccess: res => {
|
||||
Notify.success(Lang('notifications.readed'))
|
||||
this.getUpdates();
|
||||
},
|
||||
onFailed: res => {
|
||||
Notify.error(Lang('error'))
|
||||
this.getUpdates();
|
||||
}
|
||||
})
|
||||
@ -75,8 +108,9 @@ const useNotifier = defineStore('notifier', {
|
||||
getUpdates() {
|
||||
api.get(route('system.notifications.all-unread'), {
|
||||
onSuccess: res => {
|
||||
this.counter = res.data.total;
|
||||
this.notifications = res.data.notifications;
|
||||
this.counter = res.total;
|
||||
this.unreadClosedCounter = res.unread_closed;
|
||||
this.notifications = res.notifications;
|
||||
},
|
||||
onFailed: res => {
|
||||
console.log('error', res)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user