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",
|
"name": "notsoweb.frontend",
|
||||||
"copyright": "Notsoweb Software Inc.",
|
"copyright": "Notsoweb Software Inc.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.9.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeMount, onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { bootPermissions } from '@Plugins/RolePermission.js';
|
|
||||||
import useDarkMode from '@Stores/DarkMode'
|
import useDarkMode from '@Stores/DarkMode'
|
||||||
import useLeftSidebar from '@Stores/LeftSidebar'
|
import useLeftSidebar from '@Stores/LeftSidebar'
|
||||||
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
||||||
|
import useNotifier from '@Stores/Notifier'
|
||||||
|
|
||||||
import Header from '../Skeleton/Header.vue';
|
import Header from '../Skeleton/Header.vue';
|
||||||
import LeftSidebar from '../Skeleton/Sidebar/Left.vue';
|
import LeftSidebar from '../Skeleton/Sidebar/Left.vue';
|
||||||
@ -13,6 +13,7 @@ import NotificationSidebar from '../Skeleton/Sidebar/Notification.vue';
|
|||||||
const darkMode = useDarkMode();
|
const darkMode = useDarkMode();
|
||||||
const leftSidebar = useLeftSidebar();
|
const leftSidebar = useLeftSidebar();
|
||||||
const notificationSidebar = useNotificationSidebar();
|
const notificationSidebar = useNotificationSidebar();
|
||||||
|
const notifier = useNotifier();
|
||||||
|
|
||||||
/** Propiedades */
|
/** Propiedades */
|
||||||
defineProps({
|
defineProps({
|
||||||
@ -20,13 +21,11 @@ defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** Ciclos */
|
/** Ciclos */
|
||||||
onBeforeMount(() => {
|
|
||||||
bootPermissions()
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(()=> {
|
onMounted(() => {
|
||||||
leftSidebar.boot()
|
leftSidebar.boot();
|
||||||
darkMode.boot()
|
darkMode.boot();
|
||||||
|
notifier.boot();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ defineProps({
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="subtitle"
|
<p v-if="subtitle"
|
||||||
class="text-sm text-primary-on dark:text-primary-dark-on"
|
class="text-sm text-gray-50"
|
||||||
>
|
>
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -13,10 +13,7 @@ const emit = defineEmits([
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
editable: Boolean,
|
editable: Boolean,
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
title: {
|
title: String
|
||||||
default: Lang('details'),
|
|
||||||
type: String
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -25,7 +22,7 @@ const props = defineProps({
|
|||||||
<template #title>
|
<template #title>
|
||||||
<p
|
<p
|
||||||
class="font-bold text-xl"
|
class="font-bold text-xl"
|
||||||
v-text="title"
|
v-text="title ?? $t('details')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|||||||
@ -24,6 +24,12 @@ const query = ref('');
|
|||||||
const search = () => {
|
const search = () => {
|
||||||
emit('search', query.value);
|
emit('search', query.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
query.value = '';
|
||||||
|
|
||||||
|
search();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-if="title" class="flex w-full justify-center">
|
<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 class="flex w-full justify-between items-center border-y-2 border-page-t dark:border-page-dt">
|
||||||
<div>
|
<div>
|
||||||
<div class="relative py-1 z-0">
|
<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
|
<GoogleIcon
|
||||||
:title="$t('search')"
|
:title="$t('search')"
|
||||||
class="text-xl"
|
class="text-xl text-gray-700 hover:scale-110 hover:text-danger"
|
||||||
name="search"
|
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>
|
</div>
|
||||||
<input
|
<input
|
||||||
id="search"
|
id="search"
|
||||||
@ -54,7 +67,7 @@ const search = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 />
|
<slot />
|
||||||
<RouterLink :to="$view({name:'index'})">
|
<RouterLink :to="$view({name:'index'})">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { users } from '@Plugins/AuthUsers'
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission'
|
||||||
import { logout } from '@Services/Page';
|
import { logout } from '@Services/Page';
|
||||||
import useDarkMode from '@Stores/DarkMode'
|
import useDarkMode from '@Stores/DarkMode'
|
||||||
import useLeftSidebar from '@Stores/LeftSidebar'
|
import useLeftSidebar from '@Stores/LeftSidebar'
|
||||||
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
||||||
import useNotifier from '@Stores/Notifier'
|
import useNotifier from '@Stores/Notifier'
|
||||||
|
import useLoader from '@Stores/Loader';
|
||||||
|
|
||||||
|
import Loader from '@Shared/Loader.vue';
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
import Dropdown from '../Dropdown.vue';
|
import Dropdown from '../Dropdown.vue';
|
||||||
import DropdownLink from '../DropdownLink.vue';
|
import DropdownLink from '../DropdownLink.vue';
|
||||||
@ -19,6 +23,8 @@ const darkMode = useDarkMode()
|
|||||||
const leftSidebar = useLeftSidebar()
|
const leftSidebar = useLeftSidebar()
|
||||||
const notificationSidebar = useNotificationSidebar()
|
const notificationSidebar = useNotificationSidebar()
|
||||||
const notifier = useNotifier()
|
const notifier = useNotifier()
|
||||||
|
const loader = useLoader()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -36,6 +42,19 @@ const notifier = useNotifier()
|
|||||||
/>
|
/>
|
||||||
<div class="flex w-fit justify-end items-center h-14 header-right">
|
<div class="flex w-fit justify-end items-center h-14 header-right">
|
||||||
<ul class="flex items-center space-x-2">
|
<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">
|
<li class="flex items-center">
|
||||||
<GoogleIcon
|
<GoogleIcon
|
||||||
class="text-xl mt-1"
|
class="text-xl mt-1"
|
||||||
|
|||||||
@ -1,15 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
||||||
import useNotifier from '@Stores/Notifier'
|
import useNotifier from '@Stores/Notifier'
|
||||||
|
|
||||||
|
import ModalController from '@Controllers/ModalController.js';
|
||||||
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
import Item from './Notification/Item.vue';
|
import Item from './Notification/Item.vue';
|
||||||
|
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||||
|
import ShowView from '@Holos/Skeleton/Sidebar/Notification/Show.vue';
|
||||||
|
|
||||||
/** Definidores */
|
/** Definidores */
|
||||||
const notifier = useNotifier();
|
const notifier = useNotifier();
|
||||||
const notificationSidebar = useNotificationSidebar()
|
const notificationSidebar = useNotificationSidebar()
|
||||||
|
|
||||||
|
/** Controladores */
|
||||||
|
const Modal = new ModalController();
|
||||||
|
|
||||||
/** Eventos */
|
/** Eventos */
|
||||||
const emit = defineEmits(['open']);
|
const emit = defineEmits(['open']);
|
||||||
|
|
||||||
@ -18,10 +26,9 @@ const props = defineProps({
|
|||||||
sidebar: Boolean
|
sidebar: Boolean
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Ciclos */
|
const showModal = ref(Modal.showModal);
|
||||||
onMounted(() => {
|
const modelModal = ref(Modal.modelModal);
|
||||||
notifier.boot();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -37,9 +44,14 @@ onMounted(() => {
|
|||||||
<div class="flex flex-col h-full p-2 md:w-64">
|
<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 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">
|
<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>
|
{{ $t('notifications.title') }} <span class="text-xs">({{ notifier.counter }})</span>
|
||||||
</h4>
|
</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
|
<GoogleIcon
|
||||||
name="close"
|
name="close"
|
||||||
class="text-primary-t dark:text-primary-dt cursor-pointer"
|
class="text-primary-t dark:text-primary-dt cursor-pointer"
|
||||||
@ -47,15 +59,35 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-full flex-col space-y-1">
|
<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"
|
<Item v-for="notification in notifier.notifications"
|
||||||
:key="notification.id"
|
:key="notification.id"
|
||||||
:notification="notification"
|
:notification="notification"
|
||||||
|
@openModal="Modal.switchShowModal(notification)"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<ShowView
|
||||||
|
:show="showModal"
|
||||||
|
:model="modelModal"
|
||||||
|
@close="Modal.switchShowModal"
|
||||||
|
@reload="notifier.getUpdates()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -6,6 +6,11 @@ import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|||||||
/** Definidores */
|
/** Definidores */
|
||||||
const notifier = useNotifier();
|
const notifier = useNotifier();
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'openModal'
|
||||||
|
]);
|
||||||
|
|
||||||
/** Propiedades */
|
/** Propiedades */
|
||||||
defineProps({
|
defineProps({
|
||||||
notification: Object,
|
notification: Object,
|
||||||
@ -22,12 +27,12 @@ defineProps({
|
|||||||
<GoogleIcon
|
<GoogleIcon
|
||||||
name="close"
|
name="close"
|
||||||
class="text-xs text-white cursor-pointer"
|
class="text-xs text-white cursor-pointer"
|
||||||
@click="notifier.readNotification(notification.id)"
|
@click="notifier.closeNotification(notification.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full cursor-pointer">
|
||||||
<div class="w-10 space-y-0">
|
<div class="w-10 space-y-0" @click="emit('openModal', notification)">
|
||||||
<template v-if="notification.user">
|
<template v-if="notification.user">
|
||||||
<div class="w-10 h-10 bg-transparent rounded-full flex items-center justify-center">
|
<div class="w-10 h-10 bg-transparent rounded-full flex items-center justify-center">
|
||||||
<img v-if="notification.user"
|
<img v-if="notification.user"
|
||||||
@ -56,6 +61,10 @@ defineProps({
|
|||||||
v-text="notification.data.title"
|
v-text="notification.data.title"
|
||||||
class="text-sm font-medium truncate"
|
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"
|
<div v-if="notification.user"
|
||||||
v-text="`~ ${notification.user.name} ${notification.user.paternal}`"
|
v-text="`~ ${notification.user.name} ${notification.user.paternal}`"
|
||||||
class="text-xs text-gray-400 truncate"
|
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>
|
<script setup>
|
||||||
import GoogleIcon from '../Shared/GoogleIcon.vue';
|
import GoogleIcon from '../Shared/GoogleIcon.vue';
|
||||||
|
import Loader from '../Shared/Loader.vue';
|
||||||
|
|
||||||
/** Eventos */
|
/** Eventos */
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@ -9,6 +10,7 @@ const emit = defineEmits([
|
|||||||
/** Propiedades */
|
/** Propiedades */
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
items: Object,
|
items: Object,
|
||||||
|
processing: Boolean
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -16,13 +18,13 @@ const props = defineProps({
|
|||||||
<section class="pb-2">
|
<section class="pb-2">
|
||||||
<div class="w-full overflow-hidden rounded-md shadow-lg">
|
<div class="w-full overflow-hidden rounded-md shadow-lg">
|
||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
<table class="w-full">
|
<table v-if="!processing" class="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<slot name="head" />
|
<slot name="head" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="">
|
<tbody>
|
||||||
<template v-if="items?.total > 0">
|
<template v-if="items?.total > 0">
|
||||||
<slot
|
<slot
|
||||||
name="body"
|
name="body"
|
||||||
@ -36,6 +38,24 @@ const props = defineProps({
|
|||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -25,9 +25,6 @@ const props = defineProps({
|
|||||||
name="body"
|
name="body"
|
||||||
:items="items"
|
:items="items"
|
||||||
/>
|
/>
|
||||||
<tr>
|
|
||||||
<slot name="empty" />
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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;
|
|
||||||
25
src/index.js
25
src/index.js
@ -4,37 +4,46 @@ import axios from 'axios';
|
|||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { useRoute, ZiggyVue } from 'ziggy-js';
|
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 router from '@Router/Index'
|
||||||
import Notify from '@Plugins/Notify'
|
import Notify from '@Plugins/Notify'
|
||||||
|
import { bootPermissions } from '@Plugins/RolePermission';
|
||||||
import TailwindScreen from '@Plugins/TailwindScreen'
|
import TailwindScreen from '@Plugins/TailwindScreen'
|
||||||
import { pagePlugin } from '@Services/Page';
|
import { pagePlugin } from '@Services/Page';
|
||||||
import { reloadApp,view } from '@Services/Page';
|
import { reloadApp, view } from '@Services/Page';
|
||||||
|
|
||||||
import App from '@Layouts/AppLayout.vue'
|
import App from '@Layouts/AppLayout.vue'
|
||||||
|
import Error503 from '@Pages/Errors/503.vue'
|
||||||
|
|
||||||
// Configurar axios
|
// Configurar axios
|
||||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
||||||
// Crear instancias globales
|
// Elementos globales
|
||||||
window.axios = axios;
|
window.axios = axios;
|
||||||
window.Lang = lang;
|
window.Lang = lang;
|
||||||
window.Notify = new Notify();
|
window.Notify = new Notify();
|
||||||
window.TwScreen = new TailwindScreen();
|
window.TwScreen = new TailwindScreen();
|
||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
|
let initRoutes = false;
|
||||||
|
|
||||||
|
// Iniciar rutas
|
||||||
try {
|
try {
|
||||||
const routes = await axios.get(import.meta.env.VITE_API_URL + '/api/resources/routes');
|
const routes = await axios.get(import.meta.env.VITE_API_URL + '/api/resources/routes');
|
||||||
|
|
||||||
// Iniciar rutas
|
|
||||||
window.Ziggy = routes.data;
|
window.Ziggy = routes.data;
|
||||||
window.route = useRoute();
|
window.route = useRoute();
|
||||||
window.view = view;
|
window.view = view;
|
||||||
|
initRoutes = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
window.Notify.error(window.Lang('server.api.noAvailable'));
|
||||||
alert('Failed to load routes');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(initRoutes) {
|
||||||
|
// Iniciar permisos
|
||||||
|
await bootPermissions();
|
||||||
|
|
||||||
|
// Iniciar broadcast
|
||||||
if(import.meta.env.VITE_REVERB_ACTIVE === 'true') {
|
if(import.meta.env.VITE_REVERB_ACTIVE === 'true') {
|
||||||
await import('@Services/Broadcast')
|
await import('@Services/Broadcast')
|
||||||
}
|
}
|
||||||
@ -48,6 +57,10 @@ async function boot() {
|
|||||||
.use(router)
|
.use(router)
|
||||||
.use(ZiggyVue)
|
.use(ZiggyVue)
|
||||||
.mount('#app');
|
.mount('#app');
|
||||||
|
} else {
|
||||||
|
createApp(Error503)
|
||||||
|
.mount('#app');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iniciar aplicación
|
// Iniciar aplicación
|
||||||
|
|||||||
@ -202,17 +202,21 @@ export default {
|
|||||||
import: 'Importar',
|
import: 'Importar',
|
||||||
items: 'Elementos',
|
items: 'Elementos',
|
||||||
maternal:'Apellido materno',
|
maternal:'Apellido materno',
|
||||||
|
message:'Mensaje',
|
||||||
menu:'Menú',
|
menu:'Menú',
|
||||||
name:'Nombre',
|
name:'Nombre',
|
||||||
noRecords:'Sin registros',
|
noRecords:'Sin registros',
|
||||||
notification:'Notificación',
|
notification:'Notificación',
|
||||||
notifications: {
|
notifications: {
|
||||||
|
unreadClosed:'Ocultas',
|
||||||
readed:'Marcar como leído',
|
readed:'Marcar como leído',
|
||||||
deleted:'Notificación eliminada',
|
deleted:'Notificación eliminada',
|
||||||
description:'Notificaciones del usuario',
|
description:'Notificaciones del usuario',
|
||||||
notFound:'Notificación no encontrada',
|
notFound:'Notificación no encontrada',
|
||||||
title:'Notificaciones',
|
title:'Notificaciones',
|
||||||
|
seeAll:'Ver todas',
|
||||||
},
|
},
|
||||||
|
omitted:'Omitida',
|
||||||
password:'Contraseña',
|
password:'Contraseña',
|
||||||
passwordConfirmation:'Confirmar contraseña',
|
passwordConfirmation:'Confirmar contraseña',
|
||||||
passwordCurrent:'Contraseña actual',
|
passwordCurrent:'Contraseña actual',
|
||||||
@ -246,6 +250,7 @@ export default {
|
|||||||
},
|
},
|
||||||
profile:'Perfil',
|
profile:'Perfil',
|
||||||
readed:'Leído',
|
readed:'Leído',
|
||||||
|
read_at:'Fecha leído',
|
||||||
register: {
|
register: {
|
||||||
create: {
|
create: {
|
||||||
onError: 'Error al crear el registro',
|
onError: 'Error al crear el registro',
|
||||||
@ -300,6 +305,11 @@ export default {
|
|||||||
search:'Buscar',
|
search:'Buscar',
|
||||||
selected: 'Seleccionado',
|
selected: 'Seleccionado',
|
||||||
select: 'Seleccionar',
|
select: 'Seleccionar',
|
||||||
|
server: {
|
||||||
|
api: {
|
||||||
|
noAvailable: 'No se encontró el servidor API.'
|
||||||
|
}
|
||||||
|
},
|
||||||
session: {
|
session: {
|
||||||
closed: 'Sesión cerrada',
|
closed: 'Sesión cerrada',
|
||||||
},
|
},
|
||||||
@ -354,6 +364,7 @@ export default {
|
|||||||
start: 'Hora inicial',
|
start: 'Hora inicial',
|
||||||
end: 'Hora final',
|
end: 'Hora final',
|
||||||
},
|
},
|
||||||
|
title: 'Título',
|
||||||
total: 'Total',
|
total: 'Total',
|
||||||
unknown:'Desconocido',
|
unknown:'Desconocido',
|
||||||
update:'Actualizar',
|
update:'Actualizar',
|
||||||
@ -390,6 +401,11 @@ export default {
|
|||||||
},
|
},
|
||||||
title:'Roles de usuario',
|
title:'Roles de usuario',
|
||||||
},
|
},
|
||||||
|
online: {
|
||||||
|
description: 'Lista de usuarios conectados al sistema.',
|
||||||
|
title: 'Usuarios conectados',
|
||||||
|
count: 'Usuarios conectados.',
|
||||||
|
},
|
||||||
menu:'Menú de usuario',
|
menu:'Menú de usuario',
|
||||||
select:'Seleccionar un usuario',
|
select:'Seleccionar un usuario',
|
||||||
settings:'Ajustes del usuario',
|
settings:'Ajustes del usuario',
|
||||||
|
|||||||
@ -1,13 +1,25 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import useLoader from '@Stores/Loader';
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission';
|
||||||
|
|
||||||
import Layout from '@Holos/Layout/App.vue';
|
import Layout from '@Holos/Layout/App.vue';
|
||||||
import Link from '@Holos/Skeleton/Sidebar/Link.vue';
|
import Link from '@Holos/Skeleton/Sidebar/Link.vue';
|
||||||
import Section from '@Holos/Skeleton/Sidebar/Section.vue';
|
import Section from '@Holos/Skeleton/Sidebar/Section.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const loader = useLoader()
|
||||||
|
|
||||||
/** Propiedades */
|
/** Propiedades */
|
||||||
defineProps({
|
defineProps({
|
||||||
title: String,
|
title: String,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
loader.boot()
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -28,8 +40,12 @@ defineProps({
|
|||||||
to="profile.show"
|
to="profile.show"
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
<Section :name="$t('admin.title')">
|
<Section
|
||||||
|
v-if="hasPermission('users.index')"
|
||||||
|
:name="$t('admin.title')"
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
|
v-if="hasPermission('users.index')"
|
||||||
icon="people"
|
icon="people"
|
||||||
name="users.title"
|
name="users.title"
|
||||||
to="admin.users.index"
|
to="admin.users.index"
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { can, apiTo, viewTo } from './Module'
|
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 ModalController from '@Controllers/ModalController.js';
|
||||||
import SearcherController from '@Controllers/SearcherController.js';
|
|
||||||
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue'
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
||||||
@ -23,27 +23,23 @@ const modelModal = ref(Modal.modelModal);
|
|||||||
|
|
||||||
const models = ref([]);
|
const models = ref([]);
|
||||||
|
|
||||||
const Searcher = new SearcherController({
|
const searcher = useSearcher({
|
||||||
route: 'users.index',
|
url: route('users.index'),
|
||||||
model: models
|
onSuccess: (r) => models.value = r.models,
|
||||||
|
onError: () => models.value = []
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
function load() {
|
|
||||||
api.get(apiTo('index'), {
|
|
||||||
onSuccess: (r) => models.value = r.users
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ciclos */
|
/** Ciclos */
|
||||||
onMounted(() => load());
|
onMounted(() => {
|
||||||
|
searcher.search();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SearcherHead
|
<SearcherHead
|
||||||
:title="$t('users.title')"
|
:title="$t('users.title')"
|
||||||
@search="Searcher.search"
|
@search="(x) => searcher.search(x)"
|
||||||
>
|
>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="can('create')"
|
v-if="can('create')"
|
||||||
@ -60,7 +56,8 @@ onMounted(() => load());
|
|||||||
<div class="pt-2 w-full">
|
<div class="pt-2 w-full">
|
||||||
<Table
|
<Table
|
||||||
:items="models"
|
:items="models"
|
||||||
@send-pagination="Searcher.searchWithPagination"
|
@send-pagination="searcher.pagination"
|
||||||
|
:processing="searcher.processing"
|
||||||
>
|
>
|
||||||
<template #head>
|
<template #head>
|
||||||
<th v-text="$t('user')" />
|
<th v-text="$t('user')" />
|
||||||
@ -166,7 +163,7 @@ onMounted(() => load());
|
|||||||
:show="destroyModal"
|
:show="destroyModal"
|
||||||
:to="(user) => apiTo('destroy', { user })"
|
:to="(user) => apiTo('destroy', { user })"
|
||||||
@close="Modal.switchDestroyModal"
|
@close="Modal.switchDestroyModal"
|
||||||
@update="load"
|
@update="searcher.search()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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';
|
import toastr from 'toastr';
|
||||||
|
|
||||||
class Notify {
|
class Notify {
|
||||||
|
|||||||
@ -22,16 +22,23 @@ const hasPermission = (can) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bootPermissions = () => {
|
const bootPermissions = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
if (!permissionsInit.value) {
|
if (!permissionsInit.value) {
|
||||||
api.get(route('user.permissions'), {
|
api.get(route('user.permissions'), {
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
loadPermissions(res.permissions)
|
loadPermissions(res.permissions)
|
||||||
|
|
||||||
|
resolve(true)
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
permissionsInit.value = true;
|
permissionsInit.value = true;
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
reject(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetPermissions = () => {
|
const resetPermissions = () => {
|
||||||
@ -47,8 +54,13 @@ const loadPermissions = (permissionList = []) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAllPermissions = () => {
|
||||||
|
return allPermissions.value;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
bootPermissions,
|
bootPermissions,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
resetPermissions
|
resetPermissions,
|
||||||
|
getAllPermissions
|
||||||
};
|
};
|
||||||
@ -1,4 +1,13 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
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({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
@ -9,8 +18,23 @@ const router = createRouter({
|
|||||||
component: () => import('@Pages/Dashboard/Index.vue')
|
component: () => import('@Pages/Dashboard/Index.vue')
|
||||||
}, {
|
}, {
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
name: 'profile.show',
|
name: 'profile.show',
|
||||||
component: () => import('@Pages/Profile/Show.vue')
|
component: () => import('@Pages/Profile/Show.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'notifications',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'profile.notifications.index',
|
||||||
|
component: () => import('@Pages/Profile/Notifications/Index.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
}, {
|
}, {
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
children: [
|
children: [
|
||||||
@ -20,24 +44,39 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: 'admin.users.index',
|
name: 'admin.users.index',
|
||||||
|
beforeEnter: (to, from, next) => can(next, 'users.index'),
|
||||||
component: () => import('@Pages/Admin/Users/Index.vue')
|
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',
|
path: 'create',
|
||||||
name: 'admin.users.create',
|
name: 'admin.users.create',
|
||||||
|
beforeEnter: (to, from, next) => can(next, 'users.create'),
|
||||||
component: () => import('@Pages/Admin/Users/Create.vue')
|
component: () => import('@Pages/Admin/Users/Create.vue')
|
||||||
}, {
|
}, {
|
||||||
path: ':id/edit',
|
path: ':id/edit',
|
||||||
name: 'admin.users.edit',
|
name: 'admin.users.edit',
|
||||||
|
beforeEnter: (to, from, next) => can(next, 'users.edit'),
|
||||||
component: () => import('@Pages/Admin/Users/Edit.vue')
|
component: () => import('@Pages/Admin/Users/Edit.vue')
|
||||||
}, {
|
}, {
|
||||||
path: ':id/settings',
|
path: ':id/settings',
|
||||||
name: 'admin.users.settings',
|
name: 'admin.users.settings',
|
||||||
|
beforeEnter: (to, from, next) => can(next, 'users.settings'),
|
||||||
component: () => import('@Pages/Admin/Users/Settings.vue')
|
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
|
* Instancia de la API para formularios
|
||||||
*/
|
*/
|
||||||
@ -462,7 +467,7 @@ const useSearcher = (options = {
|
|||||||
async load({
|
async load({
|
||||||
url,
|
url,
|
||||||
apiToken = token.value,
|
apiToken = token.value,
|
||||||
filters = {}
|
filters,
|
||||||
}) {
|
}) {
|
||||||
this.errors = {};
|
this.errors = {};
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
@ -478,7 +483,7 @@ const useSearcher = (options = {
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
url,
|
url,
|
||||||
params: {
|
params: {
|
||||||
query: this.query,
|
q: this.query,
|
||||||
...filters
|
...filters
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
@ -531,35 +536,46 @@ const useSearcher = (options = {
|
|||||||
|
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
},
|
},
|
||||||
pagination(url, filters = {}) {
|
pagination(url, filter = {}) {
|
||||||
|
console.log(url, filter)
|
||||||
this.load({
|
this.load({
|
||||||
url,
|
url,
|
||||||
filters
|
filters : {
|
||||||
|
...options.filters,
|
||||||
|
...filter,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
search(q, filters = {}) {
|
search(q = '', filter = {}) {
|
||||||
this.query = q
|
this.query = q
|
||||||
this.load({
|
this.load({
|
||||||
url,
|
url: options.url,
|
||||||
filters
|
filters : {
|
||||||
|
...options.filters,
|
||||||
|
...filter,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
refresh(filters = {}) {
|
refresh(filter = {}) {
|
||||||
this.load({
|
this.load({
|
||||||
url,
|
url: options.url,
|
||||||
filters
|
filters : {
|
||||||
|
...options.filters,
|
||||||
|
...filter,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export {
|
export {
|
||||||
api,
|
api,
|
||||||
token,
|
token,
|
||||||
closeSession,
|
closeSession,
|
||||||
hasToken,
|
|
||||||
useForm,
|
|
||||||
useSearcher,
|
|
||||||
defineApiToken,
|
|
||||||
defineCsrfToken,
|
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 { defineStore } from 'pinia'
|
||||||
import { api } from '@Services/Api'
|
import { api } from '@Services/Api'
|
||||||
import { page } from '@Services/Page'
|
import { page } from '@Services/Page'
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission'
|
||||||
|
import { boot as bootAuthUsers, addUser, removeUser } from '@Plugins/AuthUsers'
|
||||||
|
|
||||||
/** Propiedades */
|
/** Propiedades */
|
||||||
const hasNotifications = import.meta.env.VITE_REVERB_ACTIVE === 'true';
|
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', {
|
const useNotifier = defineStore('notifier', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
counter: 0,
|
counter: 0,
|
||||||
|
unreadClosedCounter: 0,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
isStarted: false,
|
isStarted: false,
|
||||||
user_id: 0,
|
user_id: 0,
|
||||||
@ -21,7 +24,7 @@ const useNotifier = defineStore('notifier', {
|
|||||||
|
|
||||||
this.subscribeGLobalNotifications();
|
this.subscribeGLobalNotifications();
|
||||||
this.subscribeUserNotifications();
|
this.subscribeUserNotifications();
|
||||||
|
this.suscribeAuthUsers();
|
||||||
this.isStarted = true;
|
this.isStarted = true;
|
||||||
|
|
||||||
this.getUpdates();
|
this.getUpdates();
|
||||||
@ -48,6 +51,23 @@ const useNotifier = defineStore('notifier', {
|
|||||||
unsubscribeGlobalNotifications() {
|
unsubscribeGlobalNotifications() {
|
||||||
Echo.leave('Global');
|
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
|
// Notificaciones del usuario
|
||||||
subscribeUserNotifications() {
|
subscribeUserNotifications() {
|
||||||
Echo.private(`App.Models.User.${this.user_id}`)
|
Echo.private(`App.Models.User.${this.user_id}`)
|
||||||
@ -61,13 +81,26 @@ const useNotifier = defineStore('notifier', {
|
|||||||
},
|
},
|
||||||
readNotification(id) {
|
readNotification(id) {
|
||||||
api.post(route('system.notifications.read'), {
|
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 => {
|
onSuccess: res => {
|
||||||
Notify.success(Lang('notifications.readed'))
|
|
||||||
this.getUpdates();
|
this.getUpdates();
|
||||||
},
|
},
|
||||||
onFailed: res => {
|
onFailed: res => {
|
||||||
Notify.error(Lang('error'))
|
|
||||||
this.getUpdates();
|
this.getUpdates();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -75,8 +108,9 @@ const useNotifier = defineStore('notifier', {
|
|||||||
getUpdates() {
|
getUpdates() {
|
||||||
api.get(route('system.notifications.all-unread'), {
|
api.get(route('system.notifications.all-unread'), {
|
||||||
onSuccess: res => {
|
onSuccess: res => {
|
||||||
this.counter = res.data.total;
|
this.counter = res.total;
|
||||||
this.notifications = res.data.notifications;
|
this.unreadClosedCounter = res.unread_closed;
|
||||||
|
this.notifications = res.notifications;
|
||||||
},
|
},
|
||||||
onFailed: res => {
|
onFailed: res => {
|
||||||
console.log('error', res)
|
console.log('error', res)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user