Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #6 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
234 lines
9.6 KiB
Vue
234 lines
9.6 KiB
Vue
<template>
|
|
<header ref="headerRef" 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-6 text-xl text-white">
|
|
<div v-if="isAdmin" ref="bellRoot" class="relative">
|
|
<button type="button" class="relative self-center cursor-pointer flex items-center" @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="fixed right-[20px] z-30 w-[400px] rounded-md border border-neutral-200 bg-white text-neutral-800 shadow-lg"
|
|
:style="{ top: `${navbarBottom + 20}px` }"
|
|
>
|
|
<div class="px-3 pt-3 pb-6 text-xl font-semibold">
|
|
Notifications
|
|
</div>
|
|
<div class="flex gap-6 px-3 pb-2 border-b border-black">
|
|
<button
|
|
type="button"
|
|
class="border-b-2 cursor-pointer text-[18px]"
|
|
:class="activeNotifTab === 'unread' ? 'border-black font-semibold text-black' : 'border-transparent text-black hover:text-primary-500'"
|
|
@click="switchNotifTab('unread')"
|
|
>
|
|
Non lues
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="border-b-2 cursor-pointer text-[18px]"
|
|
:class="activeNotifTab === 'history' ? 'border-black font-semibold text-black' : 'border-transparent text-black hover:text-primary-500'"
|
|
@click="switchNotifTab('history')"
|
|
>
|
|
Historique
|
|
</button>
|
|
</div>
|
|
<div v-if="isLoadingNotifications" class="px-3 py-3 text-sm text-neutral-500">
|
|
Chargement...
|
|
</div>
|
|
<div v-else-if="displayedNotifications.length === 0" class="px-3 py-3 text-sm text-neutral-500">
|
|
Aucune notification.
|
|
</div>
|
|
<div v-else class="max-h-80 overflow-auto">
|
|
<NuxtLink
|
|
:to="notification.target"
|
|
v-for="notification in displayedNotifications"
|
|
:key="notification.id"
|
|
class="flex gap-5 items-center border-b border-black px-3 py-4 last:border-b-0 relative hover:bg-tertiary-500"
|
|
:class="notification.isRead ? '' : 'bg-tertiary-500'"
|
|
>
|
|
<div class="rounded-full h-[46px] w-[46px] min-w-[46px] bg-primary-500"></div>
|
|
<div class="flex flex-col min-w-0 text-[16px]">
|
|
<p class="text-black"><span class="font-semibold capitalize">{{ notification.actorName }}</span> {{ notification.message }}</p>
|
|
<p class="text-black">{{ formatTimeAgo(notification.createdAt) }} - {{ notification.category }}</p>
|
|
</div>
|
|
<span v-if="!notification.isRead" class="absolute right-4 bg-primary-500 h-4 w-4 rounded-full"></span>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div ref="userMenuRoot" class="relative flex gap-4">
|
|
<button type="button" class="flex items-center gap-4 cursor-pointer" @click="toggleUserMenu">
|
|
<Icon name="mdi:account-circle-outline" class="self-center" size="36"/>
|
|
<p class="self-center">{{ user?.username }}</p>
|
|
</button>
|
|
<div
|
|
v-if="isUserMenuOpen"
|
|
class="fixed right-[20px] z-30 w-60 rounded-md border border-neutral-200 bg-white text-[16px] text-black font-semibold shadow-lg"
|
|
:style="{ top: `${navbarBottom + 20}px` }"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="block w-full px-3 py-4 text-left hover:bg-tertiary-500 border-b border-black"
|
|
>
|
|
Mon profil
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="block w-full px-3 py-4 text-left hover:bg-tertiary-500 flex justify-between items-center"
|
|
@click="handleLogout"
|
|
>
|
|
<p>Déconnexion</p>
|
|
<Icon name="mdi:logout-variant" size="20"/>
|
|
</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, listTodayNotifications, listHistoryNotifications, markAllNotificationsRead} from '~/services/notifications'
|
|
|
|
defineProps<{
|
|
user?: User
|
|
}>()
|
|
|
|
const formatTimeAgo = (dateString: string): string => {
|
|
const date = new Date(dateString)
|
|
const now = new Date()
|
|
const diffMs = now.getTime() - date.getTime()
|
|
const diffMinutes = Math.floor(diffMs / 60000)
|
|
if (diffMinutes < 1) return "À l'instant"
|
|
if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes > 1 ? 's' : ''}`
|
|
const diffHours = Math.floor(diffMinutes / 60)
|
|
if (diffHours < 24) return `${diffHours} heure${diffHours > 1 ? 's' : ''}`
|
|
const diffDays = Math.floor(diffHours / 24)
|
|
return `${diffDays} jour${diffDays > 1 ? 's' : ''}`
|
|
}
|
|
|
|
const auth = useAuthStore()
|
|
const route = useRoute()
|
|
const headerRef = ref<HTMLElement | null>(null)
|
|
const bellRoot = ref<HTMLElement | null>(null)
|
|
const userMenuRoot = ref<HTMLElement | null>(null)
|
|
const isUserMenuOpen = ref(false)
|
|
const navbarBottom = ref(0)
|
|
|
|
const updateNavbarBottom = () => {
|
|
if (headerRef.value) {
|
|
navbarBottom.value = headerRef.value.getBoundingClientRect().bottom
|
|
}
|
|
}
|
|
const todayNotifications = ref<NotificationItem[]>([])
|
|
const historyNotifications = ref<NotificationItem[]>([])
|
|
const isNotificationsOpen = ref(false)
|
|
const isLoadingNotifications = ref(false)
|
|
const activeNotifTab = ref<'unread' | 'history'>('unread')
|
|
const unreadCount = computed(() => todayNotifications.value.length)
|
|
const displayedNotifications = computed(() => activeNotifTab.value === 'unread' ? todayNotifications.value : historyNotifications.value)
|
|
const isAdmin = computed(() => auth.user?.roles?.includes('ROLE_ADMIN') ?? false)
|
|
|
|
const toggleUserMenu = () => {
|
|
updateNavbarBottom()
|
|
isUserMenuOpen.value = !isUserMenuOpen.value
|
|
}
|
|
|
|
const handleLogout = async () => {
|
|
await auth.logout()
|
|
await navigateTo('/login')
|
|
}
|
|
|
|
const loadTodayNotifications = async () => {
|
|
todayNotifications.value = await listTodayNotifications()
|
|
}
|
|
|
|
const loadHistoryNotifications = async () => {
|
|
historyNotifications.value = await listHistoryNotifications()
|
|
}
|
|
|
|
const loadNotifications = async () => {
|
|
isLoadingNotifications.value = true
|
|
try {
|
|
await loadTodayNotifications()
|
|
} finally {
|
|
isLoadingNotifications.value = false
|
|
}
|
|
}
|
|
|
|
const switchNotifTab = async (tab: 'unread' | 'history') => {
|
|
activeNotifTab.value = tab
|
|
isLoadingNotifications.value = true
|
|
try {
|
|
if (tab === 'history') {
|
|
await loadHistoryNotifications()
|
|
} else {
|
|
await loadTodayNotifications()
|
|
}
|
|
} finally {
|
|
isLoadingNotifications.value = false
|
|
}
|
|
}
|
|
|
|
const closeNotifications = async () => {
|
|
if (!isNotificationsOpen.value) return
|
|
isNotificationsOpen.value = false
|
|
if (todayNotifications.value.length > 0) {
|
|
await markAllNotificationsRead()
|
|
todayNotifications.value = []
|
|
}
|
|
}
|
|
|
|
const toggleNotifications = async () => {
|
|
if (isNotificationsOpen.value) {
|
|
await closeNotifications()
|
|
return
|
|
}
|
|
|
|
updateNavbarBottom()
|
|
activeNotifTab.value = 'unread'
|
|
isNotificationsOpen.value = true
|
|
await loadNotifications()
|
|
}
|
|
|
|
const handleClickOutside = async (event: MouseEvent) => {
|
|
const target = event.target as Node | null
|
|
if (!target) return
|
|
if (bellRoot.value && !bellRoot.value.contains(target)) {
|
|
await closeNotifications()
|
|
}
|
|
if (userMenuRoot.value && !userMenuRoot.value.contains(target)) {
|
|
isUserMenuOpen.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
updateNavbarBottom()
|
|
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>
|