Merge branch 'develop' into fix/project-creation-workflow
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 1m28s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m40s

This commit is contained in:
2026-06-26 14:50:08 +00:00
60 changed files with 3552 additions and 767 deletions
+126 -141
View File
@@ -1,112 +1,31 @@
<template>
<div class="h-screen overflow-hidden">
<div class="flex h-full">
<!-- Mobile sidebar overlay -->
<Transition name="sidebar-overlay">
<div
v-if="ui.sidebarOpen"
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',
]"
<MalioSidebar
v-model="ui.sidebarCollapsed"
:sections="mergedSections"
:sidebar-class="ui.sidebarCollapsed ? '' : 'w-[232px]'"
>
<div class="flex items-center overflow-hidden" :class="sidebarIsCollapsed ? 'justify-center p-3' : 'justify-between'">
<img
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>
<!-- Feature-flag : Documents -->
<SidebarLink v-if="isDocumentsVisible" to="/documents" icon="mdi:folder-network-outline" :label="$t('sharedFiles.sidebar.title')" :collapsed="sidebarIsCollapsed" @click="ui.closeMobileSidebar()" />
<!-- 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>
</nav>
<div class="px-4 py-3">
<SidebarTimer :collapsed="sidebarIsCollapsed" />
</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>
<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-8 lg:px-16">
<div aria-hidden="true" class="pointer-events-none sticky top-0 z-30 h-8 flex-shrink-0 bg-white sm:h-12" />
<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>
@@ -139,23 +58,6 @@ const route = useRoute()
const { t } = useI18n()
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 isMailVisible = computed(() => {
@@ -166,22 +68,116 @@ const isMailVisible = computed(() => {
const { enabled: shareEnabled, ensureLoaded: ensureShareStatus } = useShareStatus()
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
watch(() => route.path, () => {
ui.closeMobileSidebar()
})
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',
}
// Item client avec ancre optionnelle : `after` = `to` de l'item après lequel l'insérer
// (sinon ajouté en fin de section).
type ClientItem = MalioItem & { after?: string }
// Items rendus côté client (dépendent d'un état runtime ignoré du backend).
function clientItemsFor(key: string): ClientItem[] {
if (key === 'sidebar.general.section') {
const items: ClientItem[] = []
if (currentProjectId.value) {
const id = currentProjectId.value
// Insérés juste sous « Projets », dans l'ordre via ancres chaînées.
items.push({ label: t('sidebar.project.kanban'), to: `/projects/${id}`, exact: true, after: '/projects' })
items.push({ label: t('sidebar.project.groups'), to: `/projects/${id}/groups`, after: `/projects/${id}` })
items.push({ label: t('sidebar.project.archives'), to: `/projects/${id}/archives`, after: `/projects/${id}/groups` })
}
if (isEmployee.value) {
items.push({ label: t('sidebar.general.myAbsences'), to: '/absences' })
}
return items
}
if (key === 'sidebar.tools.section') {
const items: ClientItem[] = []
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 []
}
// Insère les items client après leur ancre (`after`), sinon en fin de liste.
function mergeClientItems(base: MalioItem[], extra: ClientItem[]): MalioItem[] {
const result = [...base]
for (const { after, ...item } of extra) {
const idx = after ? result.findIndex((i) => i.to === after) : -1
if (idx !== -1) {
result.splice(idx + 1, 0, item)
} else {
result.push(item)
}
}
return result
}
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 = mergeClientItems(base.items, extra)
if (base.items.length > 0) {
result.push(base)
}
} else if (extra.length > 0) {
result.push({ label: t(key), icon: SECTION_ICON[key] ?? '', items: mergeClientItems([], 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')
@@ -269,14 +265,3 @@ function onCompleteSaved() {
})
}
</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>
+2 -2
View File
@@ -3,11 +3,11 @@
<div class="flex h-full items-center justify-between">
<MalioButtonIcon
icon="mdi:menu"
aria-label="Menu"
aria-label="Replier ou déplier le menu"
variant="ghost"
icon-size="24"
button-class="lg:hidden text-white hover:bg-primary-600"
@click="ui.openMobileSidebar()"
@click="ui.toggleSidebar()"
/>
<div class="hidden items-center gap-2 lg:flex">
<h1 class="text-lg font-bold tracking-tight">Lesstime</h1>
+25
View File
@@ -0,0 +1,25 @@
<template>
<!-- Entête de page standard : source unique du style des titres.
Toujours sticky en haut du <main> scrollable : reste visible au scroll.
Fond blanc + pt-[38px]/pb-[30px] (au lieu de marges) pour que le contenu
défilant soit masqué sous l'entête (espaces haut ET bas compris) et que
l'entête soit collée sous l'AppTopNav sans trou.
Slots :
- défaut : texte du titre
- #actions : boutons à droite du titre
- #subheader : barre de filtres / onglets rendue SOUS le titre, dans le
même bloc sticky (reste donc collée avec le titre). La
marge titre -> sous-entête est portée par le contenu passé
(ex. mt-4) pour laisser chaque page régler son cas. -->
<div class="sticky top-0 z-20 bg-white pt-[38px] pb-[30px]">
<div class="flex items-center justify-between gap-4">
<h1 class="text-[30px] font-semibold text-primary-500">
<slot/>
</h1>
<div v-if="$slots.actions" class="shrink-0">
<slot name="actions"/>
</div>
</div>
<slot name="subheader"/>
</div>
</template>
-52
View File
@@ -1,52 +0,0 @@
<template>
<NuxtLink
:to="to"
class="group/link relative flex items-center transition-colors hover:text-primary-500"
:class="linkClasses"
:active-class="exact ? '' : activeClass"
:exact-active-class="exact ? activeClass : ''"
>
<Icon :name="icon" :size="sub ? '20' : '24'" class="flex-shrink-0" />
<span
v-if="!collapsed"
class="self-baseline whitespace-nowrap overflow-hidden transition-opacity duration-300"
:class="sub ? 'text-sm' : 'text-md'"
>
{{ label }}
</span>
<div
v-if="collapsed"
class="pointer-events-none absolute left-full z-50 ml-2 rounded-md bg-neutral-800 px-2 py-1 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/link:pointer-events-auto group-hover/link:opacity-100 whitespace-nowrap"
>
{{ label }}
</div>
</NuxtLink>
</template>
<script setup lang="ts">
const props = defineProps<{
to: string
icon: string
label: string
collapsed: boolean
sub?: boolean
exact?: boolean
}>()
const activeClass = computed(() => {
if (props.collapsed) {
return '!text-primary-500 bg-primary-500/10'
}
return '!text-primary-500 bg-tertiary-500'
})
const linkClasses = computed(() => {
if (props.collapsed) {
return 'justify-center w-10 h-10 mx-auto my-1 p-2 rounded-lg text-neutral-600 hover:text-primary-500 hover:bg-primary-500/10'
}
if (props.sub) {
return 'gap-3 px-4 py-2 pl-12 text-sm font-semibold text-neutral-700'
}
return 'gap-3 px-4 py-3 text-md font-semibold text-neutral-700'
})
</script>
+13 -5
View File
@@ -349,21 +349,29 @@
}
},
"sidebar": {
"myTasks": "Mes tâches",
"general": {
"section": "Gestion de projet",
"section": "Général",
"dashboard": "Tableau de bord",
"myTasks": "Mes tâches",
"projects": "Projets",
"timeTracking": "Suivi de temps",
"mail": "Messagerie"
"mail": "Messagerie",
"myAbsences": "Mes absences"
},
"tools": {
"section": "Outils"
},
"project": {
"kanban": "Kanban",
"groups": "Groupes",
"archives": "Archives"
},
"admin": {
"section": "Administration",
"teamAbsences": "Absences équipe",
"administration": "Administration",
"directory": "Répertoire",
"reporting": "Rapports"
"reporting": "Rapports",
"administration": "Administration"
}
},
"reporting": {
+14 -10
View File
@@ -1,15 +1,18 @@
<template>
<div class="flex flex-col gap-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-neutral-900">{{ $t('absences.title') }}</h1>
<MalioButton
:label="$t('absences.newRequest')"
icon-name="mdi:plus"
icon-position="left"
@click="requestDrawerOpen = true"
/>
</div>
<div>
<PageHeader>
{{ $t('absences.title') }}
<template #actions>
<MalioButton
:label="$t('absences.newRequest')"
icon-name="mdi:plus"
icon-position="left"
@click="requestDrawerOpen = true"
/>
</template>
</PageHeader>
<div class="flex flex-col gap-6">
<AbsenceBalanceCards :balances="balances" />
<!-- Filters -->
@@ -65,6 +68,7 @@
:can-cancel="selected?.status === 'pending'"
@cancelled="reload"
/>
</div>
</div>
</template>
@@ -1,9 +1,10 @@
<template>
<div class="flex flex-col gap-6">
<h1 class="text-2xl font-bold text-neutral-900">
<div>
<PageHeader>
{{ $t("absences.teamTitle") }}
</h1>
</PageHeader>
<div class="flex flex-col gap-6">
<!-- KPIs -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="rounded-lg border border-neutral-200 bg-white p-4">
@@ -189,6 +190,7 @@
:user="selectedEmployee"
@saved="loadEmployees"
/>
</div>
</div>
</template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<NuxtLayout name="default">
<div class="mx-auto max-w-lg px-4 py-10">
<h1 class="mb-8 text-2xl font-bold text-neutral-900">{{ $t('profile.title') }}</h1>
<PageHeader>{{ $t('profile.title') }}</PageHeader>
<div class="flex flex-col items-center gap-6 rounded-xl border border-neutral-200 bg-white p-8 shadow-sm">
<!-- Current avatar -->
@@ -1,10 +1,13 @@
<template>
<div class="flex flex-col gap-6">
<div class="flex items-center gap-3 pt-4">
<MalioButtonIcon icon="mdi:arrow-left" :aria-label="$t('common.back')" @click="goBack" />
<h1 class="text-2xl font-bold text-neutral-900">{{ client?.name ?? '…' }}</h1>
</div>
<div>
<PageHeader>
<span class="inline-flex items-center gap-3">
<MalioButtonIcon icon="mdi:arrow-left" :aria-label="$t('common.back')" @click="goBack" />
{{ client?.name ?? '…' }}
</span>
</PageHeader>
<div class="flex flex-col gap-6">
<p v-if="loading">{{ $t('common.loading') }}</p>
<template v-else-if="client">
<MalioTabList v-model="activeTab" :tabs="tabs">
@@ -111,6 +114,7 @@
</template>
</MalioTabList>
</template>
</div>
</div>
</template>
@@ -1,9 +1,10 @@
<template>
<div class="flex flex-col gap-6">
<h1 class="text-2xl font-bold text-neutral-900">
<div>
<PageHeader>
{{ $t('directory.title') }}
</h1>
</PageHeader>
<div class="flex flex-col gap-6">
<MalioTabList v-model="activeTab" :tabs="tabs">
<!-- Clients -->
<template #clients>
@@ -171,6 +172,7 @@
:message="deleteModalMessage"
@confirm="confirmDelete"
/>
</div>
</div>
</template>
@@ -1,10 +1,13 @@
<template>
<div class="flex flex-col gap-6">
<div class="flex items-center gap-3 pt-4">
<MalioButtonIcon icon="mdi:arrow-left" :aria-label="$t('common.back')" @click="goBack" />
<h1 class="text-2xl font-bold text-neutral-900">{{ prestataire?.name ?? '…' }}</h1>
</div>
<div>
<PageHeader>
<span class="inline-flex items-center gap-3">
<MalioButtonIcon icon="mdi:arrow-left" :aria-label="$t('common.back')" @click="goBack" />
{{ prestataire?.name ?? '…' }}
</span>
</PageHeader>
<div class="flex flex-col gap-6">
<p v-if="loading">{{ $t('common.loading') }}</p>
<template v-else-if="prestataire">
<MalioTabList v-model="activeTab" :tabs="tabs">
@@ -111,6 +114,7 @@
</template>
</MalioTabList>
</template>
</div>
</div>
</template>
@@ -1,10 +1,13 @@
<template>
<div class="flex flex-col gap-6">
<div class="flex items-center gap-3 pt-4">
<MalioButtonIcon icon="mdi:arrow-left" :aria-label="$t('common.back')" @click="goBack" />
<h1 class="text-2xl font-bold text-neutral-900">{{ prospect?.company ?? '…' }}</h1>
</div>
<div>
<PageHeader>
<span class="inline-flex items-center gap-3">
<MalioButtonIcon icon="mdi:arrow-left" :aria-label="$t('common.back')" @click="goBack" />
{{ prospect?.company ?? '…' }}
</span>
</PageHeader>
<div class="flex flex-col gap-6">
<p v-if="loading">{{ $t('common.loading') }}</p>
<template v-else-if="prospect">
<MalioTabList v-model="activeTab" :tabs="tabs">
@@ -126,6 +129,7 @@
</template>
</MalioTabList>
</template>
</div>
</div>
</template>
+6 -4
View File
@@ -95,11 +95,13 @@ function handleTaskLinked(_taskId: number): void {
<template>
<div class="flex h-full flex-col overflow-hidden">
<div class="flex flex-shrink-0 items-center justify-between border-b border-neutral-200 bg-white px-4 py-3">
<h1 class="text-lg font-semibold text-neutral-900">
<div class="flex-shrink-0">
<PageHeader>
{{ t('mail.title') }}
</h1>
<MailRefreshButton />
<template #actions>
<MailRefreshButton />
</template>
</PageHeader>
</div>
<div class="flex flex-1 overflow-hidden">
@@ -355,9 +355,9 @@ onMounted(async () => {
<template>
<div class="min-w-0">
<!-- Header + Filters -->
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
<div class="flex items-center justify-between gap-3">
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('myTasks.title') }}</h1>
<PageHeader>
{{ $t('myTasks.title') }}
<template #actions>
<div class="flex items-center gap-2">
<MalioButton
icon-name="mdi:plus"
@@ -378,78 +378,79 @@ onMounted(async () => {
<Icon name="mdi:format-list-bulleted" size="20" />
</button>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-3">
<MalioSelect
v-model="selectedProjectId"
:options="projectOptions"
label="Projet"
:empty-option-label="$t('myTasks.allProjects')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedGroupId"
:options="groupOptions"
label="Groupe"
:empty-option-label="$t('myTasks.allGroups')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedTagId"
:options="tagOptions"
label="Type"
:empty-option-label="$t('myTasks.allTypes')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedPriorityId"
:options="priorityOptions"
label="Priorité"
:empty-option-label="$t('myTasks.allPriorities')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedEffortId"
:options="effortOptions"
label="Effort"
:empty-option-label="$t('myTasks.allEfforts')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedAssigneeId"
:options="assigneeOptions"
label="Assigné"
:empty-option-label="$t('myTasks.allAssignees')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="sortById"
:options="sortOptions"
:label="$t('myTasks.sortBy')"
:empty-option-label="$t('myTasks.sortDefault')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
</div>
</div>
</template>
<template #subheader>
<div class="mt-4 flex flex-wrap gap-3">
<MalioSelect
v-model="selectedProjectId"
:options="projectOptions"
label="Projet"
:empty-option-label="$t('myTasks.allProjects')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedGroupId"
:options="groupOptions"
label="Groupe"
:empty-option-label="$t('myTasks.allGroups')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedTagId"
:options="tagOptions"
label="Type"
:empty-option-label="$t('myTasks.allTypes')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedPriorityId"
:options="priorityOptions"
label="Priorité"
:empty-option-label="$t('myTasks.allPriorities')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedEffortId"
:options="effortOptions"
label="Effort"
:empty-option-label="$t('myTasks.allEfforts')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedAssigneeId"
:options="assigneeOptions"
label="Assigné"
:empty-option-label="$t('myTasks.allAssignees')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="sortById"
:options="sortOptions"
:label="$t('myTasks.sortBy')"
:empty-option-label="$t('myTasks.sortDefault')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
</div>
</template>
</PageHeader>
<!-- Kanban View grouped by canonical category -->
<div v-if="viewMode === 'kanban'">
<div class="mt-6 flex h-[calc(100vh-260px)] gap-3 overflow-x-auto pb-4">
<div class="flex h-[calc(100vh-260px)] gap-3 overflow-x-auto pb-4">
<div
v-for="cat in CATEGORIES"
:key="cat"
@@ -509,7 +510,7 @@ onMounted(async () => {
</div>
<!-- List View -->
<div v-if="viewMode === 'list'" class="mt-6 flex flex-col gap-2.5 rounded-[10px] bg-tertiary-500 p-2.5">
<div v-if="viewMode === 'list'" class="flex flex-col gap-2.5 rounded-[10px] bg-tertiary-500 p-2.5">
<TaskBulkActions
:selected-count="selectedTaskIds.size"
:total-count="tasks.length"
@@ -1,20 +1,19 @@
<template>
<div>
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
<div class="flex items-center justify-between">
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ project?.name ?? '' }} {{ $t('archive.title') }}</h1>
</div>
<div class="mt-4">
<MalioSelect
v-model="selectedGroupId"
:options="groupFilterOptions"
label="Groupe"
empty-option-label="Tous les groupes"
group-class="w-64"
/>
</div>
</div>
<PageHeader>
{{ project?.name ?? '' }} {{ $t('archive.title') }}
<template #subheader>
<div class="mt-4">
<MalioSelect
v-model="selectedGroupId"
:options="groupFilterOptions"
label="Groupe"
empty-option-label="Tous les groupes"
group-class="w-64"
/>
</div>
</template>
</PageHeader>
<div>
<p v-if="filteredTasks.length === 0" class="text-sm text-neutral-400">
@@ -1,10 +1,6 @@
<template>
<div>
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
<div class="flex items-center justify-between">
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ project?.name ?? '' }} Groupes</h1>
</div>
</div>
<PageHeader>{{ project?.name ?? '' }} Groupes</PageHeader>
<div>
<ProjectGroupTab :project-id="projectId" />
@@ -1,8 +1,8 @@
<template>
<div class="min-w-0">
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
<div class="flex items-center justify-between gap-3">
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ project?.name ?? '' }}</h1>
<PageHeader>
{{ project?.name ?? '' }}
<template #actions>
<div class="flex items-center gap-2">
<MalioButton
icon-name="mdi:plus"
@@ -30,66 +30,67 @@
@click="projectDrawerOpen = true"
/>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-3">
<MalioSelect
v-model="selectedGroupId"
:options="groupFilterOptions"
label="Groupe"
empty-option-label="Tous les groupes"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedTagId"
:options="tagFilterOptions"
label="Tags"
empty-option-label="Tous les tags"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedAssigneeId"
:options="userFilterOptions"
label="User"
empty-option-label="Tous les users"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-if="viewMode === 'list'"
v-model="selectedStatusId"
:options="statusFilterOptions"
label="Status"
empty-option-label="Tous les status"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedPriorityId"
:options="priorityFilterOptions"
label="Priorité"
empty-option-label="Toutes"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedEffortId"
:options="effortFilterOptions"
label="Effort"
empty-option-label="Tous"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
</div>
</div>
</template>
<template #subheader>
<div class="mt-4 flex flex-wrap gap-3">
<MalioSelect
v-model="selectedGroupId"
:options="groupFilterOptions"
label="Groupe"
empty-option-label="Tous les groupes"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedTagId"
:options="tagFilterOptions"
label="Tags"
empty-option-label="Tous les tags"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedAssigneeId"
:options="userFilterOptions"
label="User"
empty-option-label="Tous les users"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-if="viewMode === 'list'"
v-model="selectedStatusId"
:options="statusFilterOptions"
label="Status"
empty-option-label="Tous les status"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedPriorityId"
:options="priorityFilterOptions"
label="Priorité"
empty-option-label="Toutes"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedEffortId"
:options="effortFilterOptions"
label="Effort"
empty-option-label="Tous"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
</div>
</template>
</PageHeader>
<!-- Kanban -->
<div v-if="viewMode === 'kanban'" class="mt-6 flex h-[calc(100vh-200px)] gap-3 overflow-x-auto pb-4">
@@ -1,8 +1,8 @@
<template>
<div>
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
<div class="flex flex-wrap items-center justify-between gap-3">
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('projects.title') }}</h1>
<PageHeader>
{{ $t('projects.title') }}
<template #actions>
<div class="flex items-center gap-2 sm:gap-3">
<MalioButton
variant="tertiary"
@@ -23,8 +23,8 @@
<span class="sm:hidden">{{ $t('projects.addProjectShort') }}</span>
</MalioButton>
</div>
</div>
</div>
</template>
</PageHeader>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div
+52 -53
View File
@@ -1,60 +1,59 @@
<template>
<div>
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">
{{ $t('reporting.title') }}
</h1>
<!-- Filters -->
<div class="mt-4 flex flex-wrap items-end gap-3">
<MalioSelect
v-model="selectedPeriod"
:options="periodOptions"
:label="$t('reporting.filters.period')"
group-class="!w-48"
text-field="text-sm"
text-value="text-sm"
/>
<div class="w-40">
<label class="mb-1 block text-sm font-medium text-neutral-700">
{{ $t('reporting.filters.from') }}
</label>
<MalioDate
v-model="customFrom"
:disabled="selectedPeriod !== 'custom'"
group-class="w-full"
<PageHeader>
{{ $t('reporting.title') }}
<template #subheader>
<!-- Filters -->
<div class="mt-4 flex flex-wrap items-end gap-3">
<MalioSelect
v-model="selectedPeriod"
:options="periodOptions"
:label="$t('reporting.filters.period')"
group-class="!w-48"
text-field="text-sm"
text-value="text-sm"
/>
<div class="w-40">
<label class="mb-1 block text-sm font-medium text-neutral-700">
{{ $t('reporting.filters.from') }}
</label>
<MalioDate
v-model="customFrom"
:disabled="selectedPeriod !== 'custom'"
group-class="w-full"
/>
</div>
<div class="w-40">
<label class="mb-1 block text-sm font-medium text-neutral-700">
{{ $t('reporting.filters.to') }}
</label>
<MalioDate
v-model="customTo"
:disabled="selectedPeriod !== 'custom'"
group-class="w-full"
/>
</div>
<MalioSelect
v-model="selectedProjectId"
:options="projectOptions"
:label="$t('reporting.filters.project')"
:empty-option-label="$t('reporting.filters.allProjects')"
group-class="!w-44"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedUserId"
:options="userOptions"
:label="$t('reporting.filters.user')"
:empty-option-label="$t('reporting.filters.allUsers')"
group-class="!w-44"
text-field="text-sm"
text-value="text-sm"
/>
</div>
<div class="w-40">
<label class="mb-1 block text-sm font-medium text-neutral-700">
{{ $t('reporting.filters.to') }}
</label>
<MalioDate
v-model="customTo"
:disabled="selectedPeriod !== 'custom'"
group-class="w-full"
/>
</div>
<MalioSelect
v-model="selectedProjectId"
:options="projectOptions"
:label="$t('reporting.filters.project')"
:empty-option-label="$t('reporting.filters.allProjects')"
group-class="!w-44"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedUserId"
:options="userOptions"
:label="$t('reporting.filters.user')"
:empty-option-label="$t('reporting.filters.allUsers')"
group-class="!w-44"
text-field="text-sm"
text-value="text-sm"
/>
</div>
</div>
</template>
</PageHeader>
<!-- Loading -->
<div v-if="isLoading" class="mt-12 flex items-center justify-center">
@@ -1,101 +1,104 @@
<template>
<div class="flex min-h-0 flex-1 flex-col">
<div ref="pageHeaderEl" class="sticky top-8 z-20 flex-shrink-0 bg-white pb-4 sm:top-12">
<div class="flex items-center justify-between gap-3">
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">Suivi des temps</h1>
<MalioButton
icon-name="mdi:plus"
icon-position="left"
button-class="shrink-0"
@click="openCreateDrawer()"
>
<span class="hidden sm:inline">Ajouter une Activité</span>
<span class="sm:hidden">Activité</span>
</MalioButton>
</div>
<div class="relative z-30 mt-4 flex flex-wrap items-center gap-3 sm:gap-4">
<div class="flex shrink-0 items-center gap-1 h-8">
<MalioButtonIcon
icon="mdi:chevron-left"
aria-label="Précédent"
variant="ghost"
@click="navigatePrev"
/>
<DateFilter v-model="selectedDateFilter" :picker-mode="viewMode === 'day' ? 'day' : 'week'" />
<h2 class="shrink-0 whitespace-nowrap text-lg font-bold leading-8 text-orange-500">
{{ currentMonthLabel }}
</h2>
<MalioButtonIcon
icon="mdi:chevron-right"
aria-label="Suivant"
variant="ghost"
@click="navigateNext"
/>
</div>
<div class="flex items-center rounded-full bg-neutral-100 p-1">
<button
v-for="mode in (['week', 'day', 'list'] as const)"
:key="mode"
class="rounded-full px-4 py-1.5 text-sm font-semibold transition-all"
:class="viewMode === mode
? 'bg-primary-500 text-white shadow-sm'
: 'text-neutral-500 hover:text-neutral-700'"
@click="viewMode = mode"
<div ref="pageHeaderEl" class="flex-shrink-0">
<PageHeader>
Suivi des temps
<template #actions>
<MalioButton
icon-name="mdi:plus"
icon-position="left"
button-class="shrink-0"
@click="openCreateDrawer()"
>
{{ mode === 'week' ? 'Semaine' : mode === 'day' ? 'Jour' : 'Liste' }}
</button>
</div>
<span class="hidden sm:inline">Ajouter une Activité</span>
<span class="sm:hidden">Activité</span>
</MalioButton>
</template>
<template #subheader>
<div class="relative z-30 mt-4 flex flex-wrap items-center gap-3 sm:gap-4">
<div class="flex shrink-0 items-center gap-1 h-8">
<MalioButtonIcon
icon="mdi:chevron-left"
aria-label="Précédent"
variant="ghost"
@click="navigatePrev"
/>
<DateFilter v-model="selectedDateFilter" :picker-mode="viewMode === 'day' ? 'day' : 'week'" />
<h2 class="shrink-0 whitespace-nowrap text-lg font-bold leading-8 text-orange-500">
{{ currentMonthLabel }}
</h2>
<MalioButtonIcon
icon="mdi:chevron-right"
aria-label="Suivant"
variant="ghost"
@click="navigateNext"
/>
</div>
<div class="[&>div]:!mt-0">
<MalioSelect
v-model="selectedUserId"
:options="userOptions"
group-class="!w-36 sm:!w-44"
text-field="text-sm"
text-value="text-sm"
label="User"
empty-option-label="Tous"
/>
</div>
<div class="flex items-center rounded-full bg-neutral-100 p-1">
<button
v-for="mode in (['week', 'day', 'list'] as const)"
:key="mode"
class="rounded-full px-4 py-1.5 text-sm font-semibold transition-all"
:class="viewMode === mode
? 'bg-primary-500 text-white shadow-sm'
: 'text-neutral-500 hover:text-neutral-700'"
@click="viewMode = mode"
>
{{ mode === 'week' ? 'Semaine' : mode === 'day' ? 'Jour' : 'Liste' }}
</button>
</div>
<div class="[&>div]:!mt-0">
<MalioSelect
v-model="selectedProjectId"
:options="projectOptions"
empty-option-label="Tous"
label="Projet"
group-class="!w-36 sm:!w-44"
text-field="text-sm"
text-value="text-sm"
/>
</div>
<div class="[&>div]:!mt-0">
<MalioSelect
v-model="selectedUserId"
:options="userOptions"
group-class="!w-36 sm:!w-44"
text-field="text-sm"
text-value="text-sm"
label="User"
empty-option-label="Tous"
/>
</div>
<div class="[&>div]:!mt-0">
<MalioSelect
v-model="selectedTagId"
:options="tagOptions"
empty-option-label="Tous"
label="Tag"
group-class="!w-36 sm:!w-44"
text-field="text-sm"
text-value="text-sm"
/>
</div>
<div class="[&>div]:!mt-0">
<MalioSelect
v-model="selectedProjectId"
:options="projectOptions"
empty-option-label="Tous"
label="Projet"
group-class="!w-36 sm:!w-44"
text-field="text-sm"
text-value="text-sm"
/>
</div>
<MalioButton
:label="$t('timeEntries.export')"
variant="secondary"
icon-name="mdi:download"
icon-position="left"
button-class="w-auto px-4"
@click="exportDrawerOpen = true"
/>
</div>
<div class="[&>div]:!mt-0">
<MalioSelect
v-model="selectedTagId"
:options="tagOptions"
empty-option-label="Tous"
label="Tag"
group-class="!w-36 sm:!w-44"
text-field="text-sm"
text-value="text-sm"
/>
</div>
<MalioButton
:label="$t('timeEntries.export')"
variant="secondary"
icon-name="mdi:download"
icon-position="left"
button-class="w-auto px-4"
@click="exportDrawerOpen = true"
/>
</div>
</template>
</PageHeader>
</div>
<div class="relative z-0 mt-4 -mb-24 min-h-0 flex-1">
<div class="relative z-0 -mb-24 min-h-0 flex-1">
<TimeEntryList
v-if="viewMode === 'list'"
:entries="filteredEntries"
+20
View File
@@ -33,6 +33,9 @@ export default defineNuxtConfig({
'nuxt-toast',
'@nuxtjs/i18n',
'@nuxt/icon',
// Error tracking GlitchTip : charge le module uniquement si un DSN est fourni
// (donc seulement au build prod). En dev, aucun DSN => zero impact.
...(process.env.NUXT_PUBLIC_SENTRY_DSN ? ['@sentry/nuxt/module'] : []),
],
dir: {
layouts: 'app/layouts',
@@ -56,6 +59,23 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE,
sentry: {
// DSN du projet GlitchTip "lesstime-front" (vide => SDK inerte).
dsn: process.env.NUXT_PUBLIC_SENTRY_DSN || '',
environment: process.env.NODE_ENV || 'development',
},
},
},
// Upload des source maps vers GlitchTip (stacktraces lisibles cote front).
// Ne s'execute au build que si un token d'upload est fourni.
sourcemap: { client: 'hidden' },
sentry: {
sourceMapsUploadOptions: {
url: process.env.SENTRY_URL,
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
},
},
devServer: {
+890 -136
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -11,11 +11,12 @@
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
},
"dependencies": {
"@malio/layer-ui": "^1.7.5",
"@malio/layer-ui": "^1.7.16",
"@nuxt/icon": "^2.2.1",
"@nuxtjs/i18n": "^10.2.3",
"@nuxtjs/tailwindcss": "^6.14.0",
"@pinia/nuxt": "^0.11.3",
"@sentry/nuxt": "^10.61.0",
"@tailwindcss/typography": "^0.5.19",
"@vuepic/vue-datepicker": "^12.1.0",
"chart.js": "^4.5.1",
+20 -19
View File
@@ -1,24 +1,25 @@
<template>
<div>
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">Administration</h1>
<div class="mt-6 border-b border-neutral-200 overflow-x-auto">
<nav class="flex gap-4 sm:gap-6">
<button
v-for="tab in visibleTabs"
:key="tab.key"
class="whitespace-nowrap px-1 pb-3 text-sm font-semibold transition"
:class="activeTab === tab.key
? 'border-b-2 border-primary-500 text-primary-500'
: 'text-neutral-500 hover:text-neutral-700'"
@click="activeTab = tab.key"
>
{{ tab.label }}
</button>
</nav>
</div>
</div>
<PageHeader>
Administration
<template #subheader>
<div class="mt-6 border-b border-neutral-200 overflow-x-auto">
<nav class="flex gap-4 sm:gap-6">
<button
v-for="tab in visibleTabs"
:key="tab.key"
class="whitespace-nowrap px-1 pb-3 text-sm font-semibold transition"
:class="activeTab === tab.key
? 'border-b-2 border-primary-500 text-primary-500'
: 'text-neutral-500 hover:text-neutral-700'"
@click="activeTab = tab.key"
>
{{ tab.label }}
</button>
</nav>
</div>
</template>
</PageHeader>
<div>
<AdminWorkflowTab v-if="activeTab === 'workflows'" />
+3 -3
View File
@@ -1,9 +1,9 @@
<template>
<div>
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('sharedFiles.title') }}</h1>
<PageHeader>{{ $t('sharedFiles.title') }}</PageHeader>
<!-- Fil d'Ariane -->
<nav class="mt-4 flex flex-wrap items-center gap-1 text-sm text-neutral-500">
<nav class="flex flex-wrap items-center gap-1 text-sm text-neutral-500">
<button class="hover:text-primary-500" @click="openPath('')">{{ $t('sharedFiles.root') }}</button>
<template v-for="crumb in breadcrumb" :key="crumb.path">
<span>/</span>
@@ -12,7 +12,7 @@
</nav>
<!-- Filtre local + rechargement -->
<div class="mt-4 flex items-center gap-2">
<div class="flex items-center gap-2">
<div class="max-w-sm flex-1">
<MalioInputText
v-model="filter"
+35 -34
View File
@@ -506,39 +506,40 @@ const lineOptions = {
<template>
<div>
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('dashboard.title') }}</h1>
<!-- Filters -->
<div class="mt-4 flex flex-wrap gap-3">
<MalioSelect
v-model="selectedPeriod"
:options="periodOptions"
:label="$t('dashboard.filters.period')"
group-class="!w-48"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedProjectId"
:options="projectOptions"
:label="$t('dashboard.filters.project')"
:empty-option-label="$t('dashboard.filters.allProjects')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedUserId"
:options="userOptions"
:label="$t('dashboard.filters.user')"
:empty-option-label="$t('dashboard.filters.allUsers')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
</div>
</div>
<PageHeader>
{{ $t('dashboard.title') }}
<template #subheader>
<!-- Filters -->
<div class="mt-4 flex flex-wrap gap-3">
<MalioSelect
v-model="selectedPeriod"
:options="periodOptions"
:label="$t('dashboard.filters.period')"
group-class="!w-48"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedProjectId"
:options="projectOptions"
:label="$t('dashboard.filters.project')"
:empty-option-label="$t('dashboard.filters.allProjects')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
<MalioSelect
v-model="selectedUserId"
:options="userOptions"
:label="$t('dashboard.filters.user')"
:empty-option-label="$t('dashboard.filters.allUsers')"
group-class="!w-40"
text-field="text-sm"
text-value="text-sm"
/>
</div>
</template>
</PageHeader>
<!-- Loading -->
<div v-if="isLoading" class="mt-12 flex items-center justify-center">
@@ -547,7 +548,7 @@ const lineOptions = {
<template v-else>
<!-- KPI Cards -->
<div class="mt-6 grid grid-cols-2 gap-4 lg:grid-cols-4">
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
<div class="rounded-xl border border-neutral-100 bg-neutral-50 p-5">
<p class="text-xs font-medium uppercase tracking-wider text-neutral-400">
{{ $t('dashboard.stats.hoursPeriod') }}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+18
View File
@@ -0,0 +1,18 @@
import * as Sentry from '@sentry/nuxt'
// Init Sentry cote client (SPA). Le DSN provient du build prod (NUXT_PUBLIC_SENTRY_DSN).
// Si le DSN est vide (dev), Sentry.init est un no-op : rien n'est envoye.
const config = useRuntimeConfig()
const dsn = config.public.sentry?.dsn
if (dsn) {
Sentry.init({
dsn,
environment: config.public.sentry?.environment,
// Pas d'APM/tracing (hors perimetre ticket #146) : on ne remonte que les erreurs.
tracesSampleRate: 0,
// Pas de session replay (volume).
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
})
}
+1 -10
View File
@@ -1,6 +1,5 @@
export const useUiStore = defineStore('ui', () => {
const sidebarCollapsed = ref(false)
const sidebarOpen = ref(false)
const darkMode = ref(false)
if (import.meta.client) {
@@ -45,13 +44,5 @@ export const useUiStore = defineStore('ui', () => {
sidebarCollapsed.value = !sidebarCollapsed.value
}
function openMobileSidebar() {
sidebarOpen.value = true
}
function closeMobileSidebar() {
sidebarOpen.value = false
}
return { sidebarCollapsed, sidebarOpen, darkMode, toggleSidebar, openMobileSidebar, closeMobileSidebar, toggleDarkMode }
return { sidebarCollapsed, darkMode, toggleSidebar, toggleDarkMode }
})