Files
SIRH/frontend/components/AppTopNav.vue
tristan b9c3a8a84f
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
[#SIRH-25] Version mobile (#16)
| 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: #16
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-04-20 10:12:05 +00:00

245 lines
10 KiB
Vue

<template>
<header ref="headerRef" class="border-b border-neutral-200 bg-primary-500 px-4 py-3 text-white lg:p-5">
<div class="flex h-full items-center justify-between lg:justify-end">
<button
type="button"
class="rounded-md p-1 text-white hover:text-neutral-200 lg:hidden"
@click="$emit('toggleSidebar')"
>
<Icon name="mdi:menu" size="28"/>
</button>
<div class="flex gap-4 text-xl text-white lg:gap-6">
<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-2 z-30 w-[calc(100vw-1rem)] max-w-[400px] rounded-md border border-neutral-200 bg-white text-neutral-800 shadow-lg lg:right-[20px]"
:style="{ top: `${navbarBottom + 10}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="hidden self-center sm:block">{{ 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
}>()
defineEmits<{
(event: 'toggleSidebar'): void
}>()
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>