143 lines
4.6 KiB
Vue
143 lines
4.6 KiB
Vue
<template>
|
|
<header class="border-b border-neutral-200 bg-primary-500 p-5 text-white">
|
|
<div class="flex h-full items-center justify-end">
|
|
<div class="flex gap-12 text-xl text-white">
|
|
<div v-if="isAdmin" ref="bellRoot" class="relative">
|
|
<button type="button" class="relative self-center cursor-pointer" @click="toggleNotifications">
|
|
<Icon name="mdi:bell-plus" size="36" />
|
|
<span
|
|
v-if="unreadCount > 0"
|
|
class="absolute -right-1 -top-1 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-bold text-white"
|
|
>
|
|
{{ unreadCount }}
|
|
</span>
|
|
</button>
|
|
|
|
<div
|
|
v-if="isNotificationsOpen"
|
|
class="absolute right-0 top-full z-30 mt-2 w-80 rounded-md border border-neutral-200 bg-white text-neutral-800 shadow-lg"
|
|
>
|
|
<div class="border-b border-neutral-200 px-3 py-2 text-sm font-semibold">
|
|
Notifications
|
|
</div>
|
|
<div v-if="isLoadingNotifications" class="px-3 py-3 text-sm text-neutral-500">
|
|
Chargement...
|
|
</div>
|
|
<div v-else-if="notifications.length === 0" class="px-3 py-3 text-sm text-neutral-500">
|
|
Aucune notification.
|
|
</div>
|
|
<div v-else class="max-h-80 overflow-auto">
|
|
<div
|
|
v-for="notification in notifications"
|
|
:key="notification.id"
|
|
class="border-b border-neutral-100 px-3 py-2 last:border-b-0"
|
|
>
|
|
<p class="text-sm font-semibold text-neutral-900">{{ notification.title }}</p>
|
|
<p class="text-xs text-neutral-600">{{ notification.message }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="group relative flex gap-4">
|
|
<Icon name="mdi:account-circle-outline" class="self-center cursor-pointer" size="36" />
|
|
<p class="self-center cursor-pointer">{{ user?.username }}</p>
|
|
<div class="invisible absolute right-0 top-full z-20 mt-2 w-44 rounded-md border border-neutral-200 bg-white py-1 text-sm text-neutral-800 opacity-0 shadow-lg transition-all group-hover:visible group-hover:opacity-100">
|
|
<button
|
|
type="button"
|
|
class="block w-full px-3 py-2 text-left hover:bg-neutral-100"
|
|
>
|
|
Mon profil
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="block w-full px-3 py-2 text-left hover:bg-neutral-100"
|
|
@click="handleLogout"
|
|
>
|
|
Déconnexion
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { User } from '~/services/dto/user'
|
|
import type { NotificationItem } from '~/services/dto/notification'
|
|
import { listUnreadNotifications, markAllNotificationsRead } from '~/services/notifications'
|
|
|
|
defineProps<{
|
|
user?: User
|
|
}>()
|
|
|
|
const auth = useAuthStore()
|
|
const route = useRoute()
|
|
const bellRoot = ref<HTMLElement | null>(null)
|
|
const notifications = ref<NotificationItem[]>([])
|
|
const isNotificationsOpen = ref(false)
|
|
const isLoadingNotifications = ref(false)
|
|
const unreadCount = computed(() => notifications.value.length)
|
|
const isAdmin = computed(() => auth.user?.roles?.includes('ROLE_ADMIN') ?? false)
|
|
|
|
const handleLogout = async () => {
|
|
await auth.logout()
|
|
await navigateTo('/login')
|
|
}
|
|
|
|
const loadNotifications = async () => {
|
|
isLoadingNotifications.value = true
|
|
try {
|
|
notifications.value = await listUnreadNotifications()
|
|
} finally {
|
|
isLoadingNotifications.value = false
|
|
}
|
|
}
|
|
|
|
const closeNotifications = async () => {
|
|
if (!isNotificationsOpen.value) return
|
|
isNotificationsOpen.value = false
|
|
if (notifications.value.length > 0) {
|
|
await markAllNotificationsRead()
|
|
notifications.value = []
|
|
}
|
|
}
|
|
|
|
const toggleNotifications = async () => {
|
|
if (isNotificationsOpen.value) {
|
|
await closeNotifications()
|
|
return
|
|
}
|
|
|
|
isNotificationsOpen.value = true
|
|
await loadNotifications()
|
|
}
|
|
|
|
const handleClickOutside = async (event: MouseEvent) => {
|
|
const target = event.target as Node | null
|
|
if (!target || !bellRoot.value) return
|
|
if (!bellRoot.value.contains(target)) {
|
|
await closeNotifications()
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (isAdmin.value) {
|
|
await loadNotifications()
|
|
}
|
|
document.addEventListener('click', handleClickOutside)
|
|
})
|
|
|
|
watch(
|
|
() => route.fullPath,
|
|
async () => {
|
|
if (!isAdmin.value) return
|
|
await loadNotifications()
|
|
}
|
|
)
|
|
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener('click', handleClickOutside)
|
|
})
|
|
</script>
|