New /activity-log page showing all audit entries across pieces, products and composants. Includes entity type and action filters, expandable diffs, clickable entity links and pagination. Navbar link added under Ressources liées. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
370 lines
12 KiB
Vue
370 lines
12 KiB
Vue
<template>
|
|
<div class="navbar bg-base-100 shadow-lg">
|
|
<div class="navbar-start">
|
|
<!-- Mobile hamburger menu -->
|
|
<div class="dropdown">
|
|
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
|
<IconLucideMenu class="w-5 h-5" aria-hidden="true" />
|
|
</div>
|
|
<ul
|
|
tabindex="0"
|
|
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
|
|
>
|
|
<li class="pt-1 pb-2 lg:hidden">
|
|
<button
|
|
class="w-full flex items-center gap-2 rounded-md px-2 py-1 transition-colors text-base-content hover:bg-primary/10 hover:text-primary"
|
|
@click="$emit('open-settings')"
|
|
>
|
|
<IconLucideSettings class="w-4 h-4" aria-hidden="true" />
|
|
Paramètres d'affichage
|
|
</button>
|
|
</li>
|
|
|
|
<!-- Mobile: simple links -->
|
|
<li v-for="link in simpleLinks" :key="link.to">
|
|
<NuxtLink
|
|
:to="link.to"
|
|
class="rounded-md px-2 py-1 transition-colors"
|
|
:class="linkClass(link)"
|
|
>
|
|
{{ link.label }}
|
|
</NuxtLink>
|
|
</li>
|
|
|
|
<!-- Mobile: dropdown groups -->
|
|
<li
|
|
v-for="group in navGroups"
|
|
:key="group.id + '-mobile'"
|
|
class="mt-1 border-t border-base-200 pt-2"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-center justify-between rounded-md px-2 py-1 text-left transition-colors"
|
|
:class="groupClass(group)"
|
|
:aria-expanded="openDropdown === group.id + '-mobile'"
|
|
@click="toggleDropdown(group.id + '-mobile')"
|
|
@keydown.enter.prevent="toggleDropdown(group.id + '-mobile')"
|
|
@keydown.space.prevent="toggleDropdown(group.id + '-mobile')"
|
|
>
|
|
<span>{{ group.label }}</span>
|
|
<IconLucideChevronRight
|
|
class="h-4 w-4 transition-transform"
|
|
:class="openDropdown === group.id + '-mobile' ? 'rotate-90' : ''"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
<Transition name="nav-dropdown-mobile">
|
|
<ul
|
|
v-if="openDropdown === group.id + '-mobile'"
|
|
class="mt-2 space-y-1 rounded-md border border-base-200 bg-base-100 p-2 shadow-sm overflow-hidden"
|
|
>
|
|
<li v-for="child in group.children" :key="child.to">
|
|
<NuxtLink
|
|
:to="child.to"
|
|
class="rounded-md px-2 py-1 transition-colors block"
|
|
:class="childLinkClass(child)"
|
|
>
|
|
{{ child.label }}
|
|
</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
</Transition>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Logo -->
|
|
<div class="flex items-center space-x-3">
|
|
<div class="avatar">
|
|
<div class="w-14">
|
|
<img
|
|
:src="logoSrc"
|
|
alt="Logo Malio"
|
|
class="h-full w-full object-contain"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<NuxtLink to="/" class="btn btn-ghost text-xl">
|
|
Inventory
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Desktop navbar -->
|
|
<div class="navbar-center hidden lg:flex">
|
|
<ul class="menu menu-horizontal px-1">
|
|
<!-- Desktop: simple links -->
|
|
<li v-for="link in simpleLinks" :key="link.to">
|
|
<NuxtLink
|
|
:to="link.to"
|
|
class="transition-colors px-3 py-2 rounded-md"
|
|
:class="linkClass(link)"
|
|
>
|
|
{{ link.label }}
|
|
</NuxtLink>
|
|
</li>
|
|
|
|
<!-- Desktop: dropdown groups -->
|
|
<li
|
|
v-for="group in navGroups"
|
|
:key="group.id + '-desktop'"
|
|
class="relative"
|
|
@mouseenter="setDropdown(group.id + '-desktop')"
|
|
@mouseleave="scheduleDropdownClose(group.id + '-desktop')"
|
|
@focusin="setDropdown(group.id + '-desktop')"
|
|
@focusout="scheduleDropdownClose(group.id + '-desktop')"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="inline-flex items-center gap-1 rounded-md px-3 py-2 transition-colors"
|
|
:class="groupClass(group)"
|
|
:aria-expanded="openDropdown === group.id + '-desktop'"
|
|
@click="toggleDropdown(group.id + '-desktop')"
|
|
@keydown.enter.prevent="toggleDropdown(group.id + '-desktop')"
|
|
@keydown.space.prevent="toggleDropdown(group.id + '-desktop')"
|
|
>
|
|
{{ group.label }}
|
|
<IconLucideChevronRight
|
|
class="h-4 w-4 transition-transform"
|
|
:class="openDropdown === group.id + '-desktop' ? 'rotate-90' : ''"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
<Transition name="nav-dropdown-desktop">
|
|
<ul
|
|
v-if="openDropdown === group.id + '-desktop'"
|
|
class="absolute left-0 top-full mt-2 w-64 rounded-lg border border-base-200 bg-base-100 p-2 shadow-lg z-50"
|
|
>
|
|
<li v-for="child in group.children" :key="child.to">
|
|
<NuxtLink
|
|
:to="child.to"
|
|
class="block rounded-md px-2 py-1 transition-colors"
|
|
:class="childLinkClass(child)"
|
|
>
|
|
{{ child.label }}
|
|
</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
</Transition>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Navbar end -->
|
|
<div class="navbar-end">
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
class="btn btn-ghost btn-circle hidden lg:inline-flex"
|
|
title="Paramètres d'affichage"
|
|
@click="$emit('open-settings')"
|
|
>
|
|
<IconLucideSettings class="w-5 h-5" aria-hidden="true" />
|
|
</button>
|
|
|
|
<ClientOnly>
|
|
<div v-if="activeProfile" class="dropdown dropdown-end">
|
|
<div
|
|
tabindex="0"
|
|
role="button"
|
|
class="btn btn-ghost btn-circle avatar placeholder"
|
|
>
|
|
<div
|
|
class="bg-secondary text-secondary-content rounded-full w-10 h-10 grid place-items-center"
|
|
>
|
|
<span
|
|
class="flex h-full w-full items-center justify-center text-sm font-semibold leading-none tracking-tight"
|
|
>
|
|
{{ activeProfileInitials }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<ul
|
|
tabindex="0"
|
|
class="menu dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-64"
|
|
>
|
|
<li class="px-2 py-1 text-sm text-base-content/70">
|
|
Connecté en tant que<br />
|
|
<span class="font-semibold text-base-content">{{ activeProfileLabel }}</span>
|
|
</li>
|
|
<li>
|
|
<NuxtLink to="/profiles/manage" class="justify-between">
|
|
Gestion des profils
|
|
<IconLucideChevronRight class="w-4 h-4" aria-hidden="true" />
|
|
</NuxtLink>
|
|
</li>
|
|
<li>
|
|
<button
|
|
type="button"
|
|
class="text-error justify-between"
|
|
@click="$emit('logout')"
|
|
>
|
|
Déconnexion
|
|
<IconLucideLogOut class="w-4 h-4" aria-hidden="true" />
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</ClientOnly>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useRoute } from '#imports'
|
|
import { useNavDropdown } from '~/composables/useNavDropdown'
|
|
import { useProfileSession } from '~/composables/useProfileSession'
|
|
import IconLucideMenu from '~icons/lucide/menu'
|
|
import IconLucideSettings from '~icons/lucide/settings'
|
|
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
|
import IconLucideLogOut from '~icons/lucide/log-out'
|
|
import logoSrc from '~/assets/LOGO_CARRE_BLANC.png'
|
|
|
|
defineEmits<{
|
|
(e: 'open-settings'): void
|
|
(e: 'logout'): void
|
|
}>()
|
|
|
|
interface NavLink {
|
|
to: string
|
|
label: string
|
|
}
|
|
|
|
interface NavGroup {
|
|
id: string
|
|
label: string
|
|
activePaths: string[]
|
|
children: NavLink[]
|
|
}
|
|
|
|
const simpleLinks: NavLink[] = [
|
|
{ to: '/', label: 'Vue d\'ensemble' },
|
|
{ to: '/machines', label: 'Parc Machines' },
|
|
{ to: '/machine-skeleton', label: 'Squelettes de machine' },
|
|
]
|
|
|
|
const navGroups: NavGroup[] = [
|
|
{
|
|
id: 'pieces',
|
|
label: 'Pièces',
|
|
activePaths: ['/piece-category', '/pieces-catalog'],
|
|
children: [
|
|
{ to: '/pieces-catalog', label: 'Catalogue des pièces' },
|
|
{ to: '/piece-category', label: 'Catégorie de pièce' },
|
|
],
|
|
},
|
|
{
|
|
id: 'products',
|
|
label: 'Produits',
|
|
activePaths: ['/product-category', '/product-catalog'],
|
|
children: [
|
|
{ to: '/product-catalog', label: 'Catalogue des produits' },
|
|
{ to: '/product-category', label: 'Catégorie de produit' },
|
|
],
|
|
},
|
|
{
|
|
id: 'component',
|
|
label: 'Composant',
|
|
activePaths: ['/component-category', '/component-catalog'],
|
|
children: [
|
|
{ to: '/component-catalog', label: 'Catalogue des composants' },
|
|
{ to: '/component-category', label: 'Catégorie de composant' },
|
|
],
|
|
},
|
|
{
|
|
id: 'resources',
|
|
label: 'Ressources liées',
|
|
activePaths: ['/sites', '/documents', '/constructeurs', '/activity-log'],
|
|
children: [
|
|
{ to: '/sites', label: 'Sites' },
|
|
{ to: '/documents', label: 'Documents' },
|
|
{ to: '/constructeurs', label: 'Fournisseurs' },
|
|
{ to: '/activity-log', label: 'Journal d\'activité' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const route = useRoute()
|
|
const { openDropdown, setDropdown, scheduleDropdownClose, toggleDropdown } = useNavDropdown()
|
|
const { activeProfile } = useProfileSession()
|
|
|
|
const isActive = (path: string) => {
|
|
if (path === '/') {
|
|
return route.path === '/'
|
|
}
|
|
return route.path.startsWith(path)
|
|
}
|
|
|
|
const isGroupActive = (group: NavGroup) => {
|
|
return group.activePaths.some((path) => isActive(path))
|
|
}
|
|
|
|
const linkClass = (link: NavLink) => {
|
|
return isActive(link.to)
|
|
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
}
|
|
|
|
const groupClass = (group: NavGroup) => {
|
|
return isGroupActive(group)
|
|
? 'bg-primary text-primary-content font-semibold shadow-sm'
|
|
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
}
|
|
|
|
const childLinkClass = (child: NavLink) => {
|
|
return isActive(child.to)
|
|
? 'bg-primary/10 text-primary font-semibold'
|
|
: 'text-base-content hover:bg-primary/10 hover:text-primary'
|
|
}
|
|
|
|
const activeProfileLabel = computed(() => {
|
|
if (!activeProfile.value) {
|
|
return 'Profil inconnu'
|
|
}
|
|
return `${activeProfile.value.firstName} ${activeProfile.value.lastName}`
|
|
})
|
|
|
|
const activeProfileInitials = computed(() => {
|
|
if (!activeProfile.value) {
|
|
return '??'
|
|
}
|
|
const { firstName = '', lastName = '' } = activeProfile.value
|
|
return (
|
|
`${firstName.charAt(0) || ''}${lastName.charAt(0) || ''}`.toUpperCase() || '??'
|
|
)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.nav-dropdown-desktop-enter-active,
|
|
.nav-dropdown-desktop-leave-active {
|
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
}
|
|
.nav-dropdown-desktop-enter-from,
|
|
.nav-dropdown-desktop-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(0.25rem);
|
|
}
|
|
.nav-dropdown-desktop-enter-to,
|
|
.nav-dropdown-desktop-leave-from {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.nav-dropdown-mobile-enter-active,
|
|
.nav-dropdown-mobile-leave-active {
|
|
transition: max-height 0.2s ease, opacity 0.2s ease;
|
|
}
|
|
.nav-dropdown-mobile-enter-from,
|
|
.nav-dropdown-mobile-leave-to {
|
|
max-height: 0;
|
|
opacity: 0;
|
|
}
|
|
.nav-dropdown-mobile-enter-to,
|
|
.nav-dropdown-mobile-leave-from {
|
|
max-height: 12rem;
|
|
opacity: 1;
|
|
}
|
|
</style>
|