Files
Lesstime/frontend/app/layouts/default.vue
T

249 lines
8.0 KiB
Vue

<template>
<div class="h-screen overflow-hidden">
<div class="flex h-full">
<MalioSidebar
v-model="ui.sidebarCollapsed"
:sections="mergedSections"
:sidebar-class="ui.sidebarCollapsed ? '' : 'w-[232px]'"
>
<template #logo>
<img src="/LOGO_MALIO.png" alt="Malio"/>
</template>
<template #logo-collapsed>
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio"/>
</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 #footer-collapsed>
<SidebarTimer :collapsed="true" />
</template>
</MalioSidebar>
<div class="h-full flex-1 flex flex-col min-h-0 min-w-0">
<AppTopNav :user="auth.user" />
<main class="flex flex-1 flex-col overflow-y-auto overflow-x-hidden bg-white px-4 pb-24 sm:px-6 lg:px-12 xl:px-11">
<slot/>
</main>
</div>
</div>
<TimeEntryDrawer
v-model="completeDrawerOpen"
:entry="timerStore.pendingCompleteEntry"
:users="refData.users"
:projects="refData.projects"
:tags="refData.tags"
@saved="onCompleteSaved"
/>
</div>
</template>
<script setup lang="ts">
import type { UserData } from '~/services/dto/user-data'
import type { Project } from '~/modules/project-management/services/dto/project'
import type { TaskTag } from '~/modules/project-management/services/dto/task-tag'
import { useAppVersion } from '~/composables/useAppVersion'
import type { HydraCollection } from '~/utils/api'
import { extractHydraMembers } from '~/utils/api'
const auth = useAuthStore()
const ui = useUiStore()
const mailStore = useMailStore()
const {version} = useAppVersion()
const route = useRoute()
const { t } = useI18n()
const { sections } = useSidebar()
const isEmployee = computed(() => Boolean(auth.user?.isEmployee))
const isMailVisible = computed(() => {
const roles: string[] = auth.user?.roles ?? []
return roles.includes('ROLE_USER') || roles.includes('ROLE_ADMIN')
})
const { enabled: shareEnabled, ensureLoaded: ensureShareStatus } = useShareStatus()
const isDocumentsVisible = computed(() => shareEnabled.value === true)
const currentProjectId = computed(() => {
const match = route.path.match(/^\/projects\/(\d+)/)
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 baseTitle = ref('Lesstime')
useHead({
titleTemplate: (title) => {
baseTitle.value = title || 'Lesstime'
return title || 'Lesstime'
},
})
watch(
[() => timerStore.elapsedFormatted, () => timerStore.isRunning, () => timerStore.activeEntry?.title],
([elapsed, running, label]) => {
if (import.meta.server) return
const base = baseTitle.value
if (running) {
document.title = label ? `${base} | ${elapsed} · ${label}` : `${base} | ${elapsed}`
} else {
document.title = base
}
},
)
onMounted(() => {
timerStore.fetchActive()
if (isMailVisible.value) {
mailStore.startPolling()
}
ensureShareStatus()
})
watch(() => auth.user, (user) => {
if (!user) {
mailStore.stopPolling()
} else {
if (isMailVisible.value) {
mailStore.startPolling()
}
ensureShareStatus()
}
})
const completeDrawerOpen = ref(false)
const refData = reactive({
users: [] as UserData[],
projects: [] as Project[],
tags: [] as TaskTag[],
loaded: false,
})
async function loadRefData() {
if (refData.loaded) return
const api = useApi()
const [usersData, projectsData, typesData] = await Promise.all([
api.get<HydraCollection<UserData>>('/users'),
api.get<HydraCollection<Project>>('/projects'),
api.get<HydraCollection<TaskTag>>('/task_tags'),
])
refData.users = extractHydraMembers(usersData)
refData.projects = extractHydraMembers(projectsData)
refData.tags = extractHydraMembers(typesData)
refData.loaded = true
}
watch(() => timerStore.pendingCompleteEntry, async (entry) => {
if (entry) {
await loadRefData()
completeDrawerOpen.value = true
}
})
watch(completeDrawerOpen, (open) => {
if (!open) {
nextTick(() => {
timerStore.clearPendingEntry()
})
}
})
function onCompleteSaved() {
completeDrawerOpen.value = false
nextTick(() => {
timerStore.clearPendingEntry()
})
}
</script>