feat(sidebar) : migration du layout vers MalioSidebar (footer timer + version, logo Malio)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+103
-131
@@ -1,107 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-screen overflow-hidden">
|
<div class="h-screen overflow-hidden">
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<!-- Mobile sidebar overlay -->
|
<MalioSidebar
|
||||||
<Transition name="sidebar-overlay">
|
v-model="ui.sidebarCollapsed"
|
||||||
<div
|
:sections="mergedSections"
|
||||||
v-if="ui.sidebarOpen"
|
:sidebar-class="ui.sidebarCollapsed ? '' : 'w-[232px]'"
|
||||||
class="fixed inset-0 z-40 bg-black/50 lg:hidden"
|
|
||||||
@click="ui.closeMobileSidebar()"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<aside
|
|
||||||
class="fixed inset-y-0 left-0 z-50 flex h-full flex-shrink-0 flex-col border-r border-neutral-200 bg-tertiary-500 transition-transform duration-300 lg:static lg:z-auto lg:translate-x-0"
|
|
||||||
:class="[
|
|
||||||
ui.sidebarCollapsed ? 'lg:w-16' : 'lg:w-64',
|
|
||||||
ui.sidebarOpen ? 'w-64 translate-x-0' : '-translate-x-full',
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center overflow-hidden" :class="sidebarIsCollapsed ? 'justify-center p-3' : 'justify-between'">
|
<template #logo>
|
||||||
<img
|
<img src="/LOGO_MALIO.png" alt="Malio"/>
|
||||||
v-if="!sidebarIsCollapsed"
|
|
||||||
src="/malio.png"
|
|
||||||
alt="Logo"
|
|
||||||
class="w-auto"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
src="/LOGO_CARRE.png"
|
|
||||||
alt="Logo"
|
|
||||||
class="w-[46px] h-[55px]"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="mr-2 rounded-md p-2 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-700 transition-colors lg:hidden"
|
|
||||||
@click="ui.closeMobileSidebar()"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:close" size="20" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<nav class="flex-1 overflow-hidden" :class="sidebarIsCollapsed ? 'px-1 pb-6' : 'px-4 pb-6'">
|
|
||||||
<!-- Sections dynamiques (/api/sidebar) : navigation globale + sections gated par rôle -->
|
|
||||||
<template v-for="(section, sIndex) in translatedSections" :key="section.label">
|
|
||||||
<p v-if="!sidebarIsCollapsed" class="px-4 pt-5 pb-1 text-xs font-semibold uppercase tracking-wider text-neutral-400">
|
|
||||||
{{ section.label }}
|
|
||||||
</p>
|
|
||||||
<div v-else class="mx-2 my-3 border-t border-secondary-500" />
|
|
||||||
<SidebarLink
|
|
||||||
v-for="item in section.items"
|
|
||||||
:key="item.to"
|
|
||||||
:to="item.to"
|
|
||||||
:icon="item.icon"
|
|
||||||
:label="item.label"
|
|
||||||
:collapsed="sidebarIsCollapsed"
|
|
||||||
@click="ui.closeMobileSidebar()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Items conservés côté client, insérés après la 1re section (cf. décision 3) -->
|
|
||||||
<template v-if="sIndex === 0">
|
|
||||||
<!-- Contextuel projet -->
|
|
||||||
<template v-if="currentProjectId">
|
|
||||||
<SidebarLink :to="`/projects/${currentProjectId}`" icon="mdi:view-column-outline" label="Kanban" :collapsed="sidebarIsCollapsed" sub exact @click="ui.closeMobileSidebar()" />
|
|
||||||
<SidebarLink :to="`/projects/${currentProjectId}/groups`" icon="mdi:tag-multiple-outline" label="Groupes" :collapsed="sidebarIsCollapsed" sub @click="ui.closeMobileSidebar()" />
|
|
||||||
<SidebarLink :to="`/projects/${currentProjectId}/archives`" icon="mdi:archive-outline" label="Archives" :collapsed="sidebarIsCollapsed" sub @click="ui.closeMobileSidebar()" />
|
|
||||||
</template>
|
</template>
|
||||||
<!-- Feature-flag : Documents -->
|
<template #logo-collapsed>
|
||||||
<SidebarLink v-if="isDocumentsVisible" to="/documents" icon="mdi:folder-network-outline" :label="$t('sharedFiles.sidebar.title')" :collapsed="sidebarIsCollapsed" @click="ui.closeMobileSidebar()" />
|
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio"/>
|
||||||
<!-- Feature-flag : Mail + badge -->
|
|
||||||
<div v-if="isMailVisible" class="relative">
|
|
||||||
<SidebarLink to="/mail" icon="mdi:email-outline" :label="$t('mail.sidebar.title')" :collapsed="sidebarIsCollapsed" @click="ui.closeMobileSidebar()" />
|
|
||||||
<span
|
|
||||||
v-if="mailStore.globalUnreadCount > 0"
|
|
||||||
class="pointer-events-none absolute right-3 top-1/2 flex h-5 min-w-5 -translate-y-1/2 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-bold text-white"
|
|
||||||
:class="{ 'right-1 top-1 translate-y-0': sidebarIsCollapsed }"
|
|
||||||
:aria-label="`${mailStore.globalUnreadCount} messages non lus`"
|
|
||||||
>
|
|
||||||
{{ mailStore.globalUnreadCount > 99 ? '99+' : mailStore.globalUnreadCount }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- User-flag : Mes absences (isEmployee — non couvert par le gate rôle) -->
|
|
||||||
<SidebarLink v-if="isEmployee" to="/absences" icon="mdi:umbrella-beach-outline" label="Mes absences" :collapsed="sidebarIsCollapsed" @click="ui.closeMobileSidebar()" />
|
|
||||||
</template>
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<SidebarTimer :collapsed="false" />
|
||||||
|
<p v-if="version" class="text-center text-sm font-bold">v {{ version }}</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
<template #footer-collapsed>
|
||||||
|
<SidebarTimer :collapsed="true" />
|
||||||
<div class="px-4 py-3">
|
</template>
|
||||||
<SidebarTimer :collapsed="sidebarIsCollapsed" />
|
</MalioSidebar>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-center p-4">
|
|
||||||
<p v-if="!sidebarIsCollapsed" class="font-bold">v {{ version }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Collapse toggle button centered vertically on the sidebar edge -->
|
|
||||||
<button
|
|
||||||
class="absolute top-1/2 -right-4 z-10 hidden h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full border border-neutral-200 bg-white text-neutral-400 shadow-sm hover:text-neutral-700 transition-colors lg:flex"
|
|
||||||
:title="ui.sidebarCollapsed ? 'Ouvrir le menu' : 'Réduire le menu'"
|
|
||||||
@click="ui.toggleSidebar()"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
:name="ui.sidebarCollapsed ? 'mdi:chevron-right' : 'mdi:chevron-left'"
|
|
||||||
size="18"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div class="h-full flex-1 flex flex-col min-h-0 min-w-0">
|
<div class="h-full flex-1 flex flex-col min-h-0 min-w-0">
|
||||||
<AppTopNav :user="auth.user" />
|
<AppTopNav :user="auth.user" />
|
||||||
@@ -138,23 +58,6 @@ const route = useRoute()
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { sections } = useSidebar()
|
const { sections } = useSidebar()
|
||||||
|
|
||||||
// `/mail` est déclaré dans config/sidebar.php pour le gating module (disabledRoutes),
|
|
||||||
// mais son rendu visuel + badge non-lus est géré manuellement ci-dessous (feature-flag Mail).
|
|
||||||
// On le filtre des sections dynamiques pour éviter un doublon dans la nav.
|
|
||||||
const translatedSections = computed(() =>
|
|
||||||
sections.value.map((section) => ({
|
|
||||||
label: t(section.label),
|
|
||||||
icon: section.icon,
|
|
||||||
items: section.items
|
|
||||||
.filter((item) => item.to !== '/mail')
|
|
||||||
.map((item) => ({
|
|
||||||
label: t(item.label),
|
|
||||||
to: item.to,
|
|
||||||
icon: item.icon,
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
const isEmployee = computed(() => Boolean(auth.user?.isEmployee))
|
const isEmployee = computed(() => Boolean(auth.user?.isEmployee))
|
||||||
|
|
||||||
const isMailVisible = computed(() => {
|
const isMailVisible = computed(() => {
|
||||||
@@ -165,12 +68,6 @@ const isMailVisible = computed(() => {
|
|||||||
const { enabled: shareEnabled, ensureLoaded: ensureShareStatus } = useShareStatus()
|
const { enabled: shareEnabled, ensureLoaded: ensureShareStatus } = useShareStatus()
|
||||||
const isDocumentsVisible = computed(() => shareEnabled.value === true)
|
const isDocumentsVisible = computed(() => shareEnabled.value === true)
|
||||||
|
|
||||||
// On mobile, sidebar is always expanded (not collapsed icon mode)
|
|
||||||
const sidebarIsCollapsed = computed(() => {
|
|
||||||
if (ui.sidebarOpen) return false
|
|
||||||
return ui.sidebarCollapsed
|
|
||||||
})
|
|
||||||
|
|
||||||
// Close mobile sidebar on route change
|
// Close mobile sidebar on route change
|
||||||
watch(() => route.path, () => {
|
watch(() => route.path, () => {
|
||||||
ui.closeMobileSidebar()
|
ui.closeMobileSidebar()
|
||||||
@@ -181,6 +78,92 @@ const currentProjectId = computed(() => {
|
|||||||
return match ? match[1] : null
|
return match ? match[1] : null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type MalioItem = { label: string; to: string; exact?: boolean }
|
||||||
|
type MalioSection = { label: string; icon: string; items: MalioItem[] }
|
||||||
|
|
||||||
|
// Ordre d'affichage canonique des sections.
|
||||||
|
const SECTION_ORDER = [
|
||||||
|
'sidebar.general.section',
|
||||||
|
'sidebar.tools.section',
|
||||||
|
'sidebar.admin.section',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
// Icônes de secours pour les sections créées côté client (absentes du backend,
|
||||||
|
// ex. module mail off mais partage actif → section Outils à recréer).
|
||||||
|
const SECTION_ICON: Record<string, string> = {
|
||||||
|
'sidebar.general.section': 'mdi:view-dashboard-outline',
|
||||||
|
'sidebar.tools.section': 'mdi:tools',
|
||||||
|
'sidebar.admin.section': 'mdi:cog-outline',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items rendus côté client (dépendent d'un état runtime ignoré du backend).
|
||||||
|
function clientItemsFor(key: string): MalioItem[] {
|
||||||
|
if (key === 'sidebar.general.section') {
|
||||||
|
const items: MalioItem[] = []
|
||||||
|
if (currentProjectId.value) {
|
||||||
|
const id = currentProjectId.value
|
||||||
|
items.push({ label: t('sidebar.project.kanban'), to: `/projects/${id}`, exact: true })
|
||||||
|
items.push({ label: t('sidebar.project.groups'), to: `/projects/${id}/groups` })
|
||||||
|
items.push({ label: t('sidebar.project.archives'), to: `/projects/${id}/archives` })
|
||||||
|
}
|
||||||
|
if (isEmployee.value) {
|
||||||
|
items.push({ label: t('sidebar.general.myAbsences'), to: '/absences' })
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
if (key === 'sidebar.tools.section') {
|
||||||
|
const items: MalioItem[] = []
|
||||||
|
if (isMailVisible.value) {
|
||||||
|
const n = mailStore.globalUnreadCount
|
||||||
|
const suffix = n > 0 ? ` (${n > 99 ? '99+' : n})` : ''
|
||||||
|
items.push({ label: `${t('mail.sidebar.title')}${suffix}`, to: '/mail' })
|
||||||
|
}
|
||||||
|
if (isDocumentsVisible.value) {
|
||||||
|
items.push({ label: t('sharedFiles.sidebar.title'), to: '/documents' })
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedSections = computed<MalioSection[]>(() => {
|
||||||
|
// 1. Sections backend (déjà filtrées par permissions), mail retiré (ré-injecté côté client).
|
||||||
|
const backend = new Map<string, MalioSection>()
|
||||||
|
for (const section of sections.value) {
|
||||||
|
backend.set(section.label, {
|
||||||
|
label: t(section.label),
|
||||||
|
icon: section.icon,
|
||||||
|
items: section.items
|
||||||
|
.filter((item) => item.to !== '/mail')
|
||||||
|
.map((item) => ({ label: t(item.label), to: item.to })),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fusion dans l'ordre canonique.
|
||||||
|
const result: MalioSection[] = []
|
||||||
|
for (const key of SECTION_ORDER) {
|
||||||
|
const base = backend.get(key)
|
||||||
|
const extra = clientItemsFor(key)
|
||||||
|
if (base) {
|
||||||
|
base.items.push(...extra)
|
||||||
|
if (base.items.length > 0) {
|
||||||
|
result.push(base)
|
||||||
|
}
|
||||||
|
} else if (extra.length > 0) {
|
||||||
|
result.push({ label: t(key), icon: SECTION_ICON[key] ?? '', items: extra })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Garde-fou : toute section backend hors ordre canonique n'est pas perdue.
|
||||||
|
for (const [key, section] of backend) {
|
||||||
|
if (!(SECTION_ORDER as readonly string[]).includes(key) && section.items.length > 0) {
|
||||||
|
result.push(section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
const timerStore = useTimerStore()
|
const timerStore = useTimerStore()
|
||||||
|
|
||||||
const baseTitle = ref('Lesstime')
|
const baseTitle = ref('Lesstime')
|
||||||
@@ -268,14 +251,3 @@ function onCompleteSaved() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.sidebar-overlay-enter-active,
|
|
||||||
.sidebar-overlay-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
.sidebar-overlay-enter-from,
|
|
||||||
.sidebar-overlay-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user