[notifications] Implemented

This commit is contained in:
Ferdinand Thiessen 2021-03-29 07:35:23 +02:00
parent c362843c8e
commit 852b1dad03
6 changed files with 132 additions and 10 deletions

View File

@ -313,7 +313,7 @@ async function getBackend() {
*/ */
export default boot(async ({ router, app }) => { export default boot(async ({ router, app }) => {
const backend = await getBackend(); const backend = await getBackend();
if (!backend) { if (backend === null) {
void router.push({ name: 'error' }); void router.push({ name: 'error' });
return; return;
} }

View File

@ -0,0 +1,45 @@
<template>
<q-card bordered class="row q-ma-xs q-pa-xs" style="position: relative">
<div class="col-12 text-weight-light">{{ dateString }}</div>
<div class="col-12">{{ modelValue.text }}</div>
<q-btn
round
dense
icon="close"
size="sm"
color="negative"
class="q-ma-xs"
style="position: absolute; top: 0; right: 0"
@click="remove"
/>
</q-card>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue';
import { formatDateTime } from 'src/utils/datetime';
export default defineComponent({
name: 'Notification',
props: {
modelValue: {
required: true,
type: Object as PropType<FG.Notification>,
},
},
emits: {
remove: (id: number) => !!id,
},
setup(props, { emit }) {
const dateString = computed(() => formatDateTime(props.modelValue.time, true, true));
function remove() {
emit('remove', props.modelValue.id);
}
return { dateString, remove };
},
});
</script>
<style scoped></style>

View File

@ -1,11 +1,10 @@
declare namespace FG { declare namespace FG {
interface Session { interface Notification {
expires: Date; id: number;
token: string; plugin: string;
lifetime: number; text: string;
browser: string; data?: unknown;
platform: string; time: Date;
userid: string;
} }
interface User { interface User {
userid: string; userid: string;
@ -18,6 +17,14 @@ declare namespace FG {
permissions?: Array<string>; permissions?: Array<string>;
avatar_url?: string; avatar_url?: string;
} }
interface Session {
expires: Date;
token: string;
lifetime: number;
browser: string;
platform: string;
userid: string;
}
type Permission = string; type Permission = string;
interface Role { interface Role {
id: number; id: number;
@ -69,6 +76,7 @@ declare namespace FG {
} }
interface Service { interface Service {
userid: string; userid: string;
is_backup: boolean;
value: number; value: number;
} }
interface Drink { interface Drink {

View File

@ -21,6 +21,25 @@
</q-toolbar-title> </q-toolbar-title>
<!-- Hier kommen die Shortlinks hin --> <!-- Hier kommen die Shortlinks hin -->
<q-btn icon="message" flat dense
><q-badge color="negative" floating>{{ notifications.length }}</q-badge>
<q-menu style="max-height: 400px; overflow: auto">
<q-btn
v-if="noPermission"
label="Benachrichtigungen erlauben"
@click="requestPermission"
/>
<template v-if="notifications.length > 0">
<Notification
v-for="(notification, index) in notifications"
:key="index"
:model-value="notification"
@remove="remove"
/>
</template>
<div v-else class="q-pa-sm">Keine neuen Benachrichtigungen</div>
</q-menu>
</q-btn>
<shortcut-link <shortcut-link
v-for="(shortcut, index) in shortcuts" v-for="(shortcut, index) in shortcuts"
:key="'shortcut' + index" :key="'shortcut' + index"
@ -84,8 +103,9 @@
<script lang="ts"> <script lang="ts">
import EssentialLink from 'src/components/navigation/EssentialLink.vue'; import EssentialLink from 'src/components/navigation/EssentialLink.vue';
import ShortcutLink from 'src/components/navigation/ShortcutLink.vue'; import ShortcutLink from 'src/components/navigation/ShortcutLink.vue';
import Notification from 'src/components/Notification.vue';
import { Screen } from 'quasar'; import { Screen } from 'quasar';
import { defineComponent, ref, inject, computed } from 'vue'; import { defineComponent, ref, inject, computed, onBeforeMount } from 'vue';
import { useMainStore } from 'src/store'; import { useMainStore } from 'src/store';
import { FG_Plugin } from 'src/plugins'; import { FG_Plugin } from 'src/plugins';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -100,7 +120,7 @@ const essentials: FG_Plugin.MenuLink[] = [
export default defineComponent({ export default defineComponent({
name: 'MainLayout', name: 'MainLayout',
components: { EssentialLink, ShortcutLink }, components: { EssentialLink, ShortcutLink, Notification },
setup() { setup() {
const router = useRouter(); const router = useRouter();
const mainStore = useMainStore(); const mainStore = useMainStore();
@ -108,6 +128,16 @@ export default defineComponent({
const leftDrawer = ref(false); const leftDrawer = ref(false);
const shortcuts = flaschengeist?.shortcuts; const shortcuts = flaschengeist?.shortcuts;
const mainLinks = flaschengeist?.menuLinks; const mainLinks = flaschengeist?.menuLinks;
const notifications = computed(() => mainStore.notifications.slice().reverse());
const noPermission = ref(window.Notification.permission !== 'granted');
function requestPermission() {
void window.Notification.requestPermission().then(
(p) => (noPermission.value = p !== 'granted')
);
}
onBeforeMount(() => window.setInterval(() => void mainStore.loadNotifications(), 30000));
const leftDrawerOpen = computed({ const leftDrawerOpen = computed({
get: () => (leftDrawer.value || Screen.gt.sm ? true : false), get: () => (leftDrawer.value || Screen.gt.sm ? true : false),
@ -131,6 +161,10 @@ export default defineComponent({
void mainStore.logout(); void mainStore.logout();
} }
async function remove(id: number) {
await mainStore.removeNotification(id);
}
return { return {
essentials, essentials,
leftDrawerOpen, leftDrawerOpen,
@ -138,6 +172,10 @@ export default defineComponent({
leftDrawerClicker, leftDrawerClicker,
logout, logout,
mainLinks, mainLinks,
notifications,
noPermission,
remove,
requestPermission,
shortcuts, shortcuts,
subLinks, subLinks,
}; };

View File

@ -84,6 +84,7 @@ export default defineComponent({
async function enrollForJob() { async function enrollForJob() {
const newService: FG.Service = { const newService: FG.Service = {
userid: mainStore.currentUser.userid, userid: mainStore.currentUser.userid,
is_backup: false,
value: 1, value: 1,
}; };
try { try {
@ -104,6 +105,7 @@ export default defineComponent({
async function signOutFromJob() { async function signOutFromJob() {
const newService: FG.Service = { const newService: FG.Service = {
userid: mainStore.currentUser.userid, userid: mainStore.currentUser.userid,
is_backup: false,
value: -1, value: -1,
}; };
try { try {

View File

@ -41,6 +41,7 @@ export const useMainStore = defineStore({
state: () => ({ state: () => ({
session: loadCurrentSession(), session: loadCurrentSession(),
user: loadUser(), user: loadUser(),
notifications: [] as Array<FG.Notification>,
}), }),
getters: { getters: {
@ -123,5 +124,33 @@ export const useMainStore = defineStore({
response && 'status' in response ? (<AxiosResponse>response).status : false response && 'status' in response ? (<AxiosResponse>response).status : false
); );
}, },
async loadNotifications() {
const params =
this.notifications.length > 0
? { from: this.notifications[this.notifications.length - 1].time }
: {};
const { data } = await api.get<FG.Notification[]>('/notifications', { params: params });
data.forEach((n) => {
n.time = new Date(n.time);
if (window.Notification.permission === 'granted')
new window.Notification(n.text, {
timestamp: n.time.getTime(),
});
});
this.notifications.push(...data);
},
async removeNotification(id: number) {
const idx = this.notifications.findIndex((n) => n.id === id);
if (idx >= 0)
try {
this.notifications.splice(idx, 1);
await api.delete(`/notifications/${id}`);
} catch (error) {
if (this.notifications.length > idx)
this.notifications.splice(idx, this.notifications.length - idx - 1);
}
},
}, },
}); });