Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d95f9e782 | |||
| c766e76624 |
+9
-2
@@ -26,7 +26,14 @@ return [
|
|||||||
['label' => 'sidebar.general.myTasks', 'to' => '/my-tasks', 'icon' => 'mdi:clipboard-check-outline', 'module' => 'project-management', 'permission' => 'project-management.tasks.view'],
|
['label' => 'sidebar.general.myTasks', 'to' => '/my-tasks', 'icon' => 'mdi:clipboard-check-outline', 'module' => 'project-management', 'permission' => 'project-management.tasks.view'],
|
||||||
['label' => 'sidebar.general.projects', 'to' => '/projects', 'icon' => 'mdi:folder-outline', 'module' => 'project-management', 'permission' => 'project-management.projects.view'],
|
['label' => 'sidebar.general.projects', 'to' => '/projects', 'icon' => 'mdi:folder-outline', 'module' => 'project-management', 'permission' => 'project-management.projects.view'],
|
||||||
['label' => 'sidebar.general.timeTracking', 'to' => '/time-tracking', 'icon' => 'mdi:calendar-edit-outline', 'module' => 'time-tracking', 'permission' => 'time-tracking.entries.view'],
|
['label' => 'sidebar.general.timeTracking', 'to' => '/time-tracking', 'icon' => 'mdi:calendar-edit-outline', 'module' => 'time-tracking', 'permission' => 'time-tracking.entries.view'],
|
||||||
// Gating module uniquement (cf. en-tête) : rendu visuel + badge gérés côté layout.
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'sidebar.tools.section',
|
||||||
|
'icon' => 'mdi:tools',
|
||||||
|
'items' => [
|
||||||
|
// Gating module uniquement : rendu visuel + badge non-lus gérés côté layout
|
||||||
|
// (filtré de translatedSections puis ré-injecté avec suffixe (N)).
|
||||||
['label' => 'sidebar.general.mail', 'to' => '/mail', 'icon' => 'mdi:email-outline', 'module' => 'mail'],
|
['label' => 'sidebar.general.mail', 'to' => '/mail', 'icon' => 'mdi:email-outline', 'module' => 'mail'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -37,8 +44,8 @@ return [
|
|||||||
'items' => [
|
'items' => [
|
||||||
['label' => 'sidebar.admin.teamAbsences', 'to' => '/team-absences', 'icon' => 'mdi:calendar-account-outline', 'module' => 'absence'],
|
['label' => 'sidebar.admin.teamAbsences', 'to' => '/team-absences', 'icon' => 'mdi:calendar-account-outline', 'module' => 'absence'],
|
||||||
['label' => 'sidebar.admin.directory', 'to' => '/directory', 'icon' => 'mdi:card-account-details-outline', 'module' => 'directory'],
|
['label' => 'sidebar.admin.directory', 'to' => '/directory', 'icon' => 'mdi:card-account-details-outline', 'module' => 'directory'],
|
||||||
['label' => 'sidebar.admin.administration', 'to' => '/admin', 'icon' => 'mdi:cog-outline', 'permission' => 'core.users.view'],
|
|
||||||
['label' => 'sidebar.admin.reporting', 'to' => '/reporting', 'icon' => 'mdi:chart-line', 'module' => 'reporting', 'permission' => 'reporting.view'],
|
['label' => 'sidebar.admin.reporting', 'to' => '/reporting', 'icon' => 'mdi:chart-line', 'module' => 'reporting', 'permission' => 'reporting.view'],
|
||||||
|
['label' => 'sidebar.admin.administration', 'to' => '/admin', 'icon' => 'mdi:cog-outline', 'permission' => 'core.users.view'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.4.39'
|
app.version: '0.4.40'
|
||||||
|
|||||||
@@ -0,0 +1,484 @@
|
|||||||
|
# Migration sidebar vers MalioSidebar — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Remplacer la sidebar maison de Lesstime par le composant `MalioSidebar` de `@malio/layer-ui`, en 3 groupes (Général / Outils / Administration), avec timer + version dans le footer et le logo Malio de Starseed.
|
||||||
|
|
||||||
|
**Architecture:** Modèle backend-driven conservé — `config/sidebar.php` filtré par `SidebarProvider` (permissions/rôles/modules côté serveur), exposé via `/api/sidebar`, consommé par `useSidebar()`. Le layout `default.vue` mappe ces sections vers le format `MalioSidebar` et fusionne les items contextuels rendus côté client (Kanban/Groupes/Archives, Documents, Mail+badge, Mes absences).
|
||||||
|
|
||||||
|
**Tech Stack:** Nuxt 4 (SPA), Vue 3 `<script setup>` TS, Pinia, `@malio/layer-ui` ^1.7.16, i18n (@nuxtjs/i18n), Symfony 8 / API Platform 4 (backend config PHP).
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- **Ne jamais modifier `@malio/layer-ui`** (lib externe). Source de référence en lecture seule : `frontend/node_modules/@malio/layer-ui/app/components/malio/sidebar/Sidebar.vue`.
|
||||||
|
- `MalioSidebar` : props `sections` (requis), `modelValue` (v-model collapse bool), `sidebarClass`, `toggleClass`. Item = `{ label: string; to: string; exact?: boolean }` (pas d'icône ni de badge par item). Section = `{ label?: string; icon?: string; items: SidebarItem[] }`. Slots : `#logo`, `#logo-collapsed`, `#footer`, `#footer-collapsed`.
|
||||||
|
- **TypeScript strict** ; indentation **4 espaces** (frontend).
|
||||||
|
- Backend : `declare(strict_types=1)` en tête des fichiers PHP.
|
||||||
|
- Commits format projet : `type(scope) : message` (espaces autour du `:`), types autorisés minuscules (`feat`, `fix`, `refactor`, `chore`, …). **Ne committer que sur demande de l'utilisateur** (règle CLAUDE.md). Travailler sur une branche dédiée (pas directement sur `develop`).
|
||||||
|
- **Pas de runner de test frontend** dans ce projet → vérification par `npm run build` (Nuxt, échoue sur erreur TS/template) + **QA manuelle navigateur** (`make dev-nuxt`, port 3002). Ne PAS introduire de framework de test (hors scope).
|
||||||
|
- Décisions validées : 3 groupes ; badge mail = **suffixe `(N)`** sur le label.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- `config/sidebar.php` — **Modify** : re-catégorisation en 3 sections.
|
||||||
|
- `frontend/i18n/locales/fr.json` — **Modify** : clés de sections/items.
|
||||||
|
- `frontend/i18n/locales/*.json` (autres langues présentes) — **Modify si existantes** : mêmes clés.
|
||||||
|
- `frontend/public/LOGO_MALIO.png` — **Create** (copie Starseed).
|
||||||
|
- `frontend/public/LOGO_MALIO_COLLAPSED.png` — **Create** (copie Starseed).
|
||||||
|
- `frontend/app/layouts/default.vue` — **Modify** : réécriture du template sidebar + logique `mergedSections`.
|
||||||
|
- `frontend/components/ui/SidebarLink.vue` — **Possible delete** (si plus aucun usage après migration).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 0 : Branche de travail
|
||||||
|
|
||||||
|
**Files:** aucun (git).
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Créer la branche depuis `develop`**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/m-tristan/workspace/Lesstime
|
||||||
|
git checkout develop && git pull --ff-only
|
||||||
|
git checkout -b feat/malio-sidebar
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected : sur la branche `feat/malio-sidebar`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1 : Backend — re-catégorisation `config/sidebar.php` + i18n
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `config/sidebar.php`
|
||||||
|
- Modify: `frontend/i18n/locales/fr.json`
|
||||||
|
- Modify: autres `frontend/i18n/locales/*.json` si présentes (mêmes clés)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces : `/api/sidebar` renvoie des sections dont les `label` sont les clés `sidebar.general.section`, `sidebar.tools.section`, `sidebar.admin.section`. Items inchangés en `to` ; gates (`module`/`roles`/`permission`) inchangés, juste réorganisés.
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Réécrire `config/sidebar.php` en 3 sections**
|
||||||
|
|
||||||
|
Remplacer le `return [...]` (lignes 20-44) par :
|
||||||
|
|
||||||
|
```php
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'label' => 'sidebar.general.section',
|
||||||
|
'icon' => 'mdi:view-dashboard-outline',
|
||||||
|
'items' => [
|
||||||
|
['label' => 'sidebar.general.dashboard', 'to' => '/', 'icon' => 'mdi:view-dashboard-outline'],
|
||||||
|
['label' => 'sidebar.general.myTasks', 'to' => '/my-tasks', 'icon' => 'mdi:clipboard-check-outline', 'module' => 'project-management', 'permission' => 'project-management.tasks.view'],
|
||||||
|
['label' => 'sidebar.general.projects', 'to' => '/projects', 'icon' => 'mdi:folder-outline', 'module' => 'project-management', 'permission' => 'project-management.projects.view'],
|
||||||
|
['label' => 'sidebar.general.timeTracking', 'to' => '/time-tracking', 'icon' => 'mdi:calendar-edit-outline', 'module' => 'time-tracking', 'permission' => 'time-tracking.entries.view'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'sidebar.tools.section',
|
||||||
|
'icon' => 'mdi:tools',
|
||||||
|
'items' => [
|
||||||
|
// Gating module uniquement : rendu visuel + badge non-lus gérés côté layout
|
||||||
|
// (filtré de translatedSections puis ré-injecté avec suffixe (N)).
|
||||||
|
['label' => 'sidebar.general.mail', 'to' => '/mail', 'icon' => 'mdi:email-outline', 'module' => 'mail'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'sidebar.admin.section',
|
||||||
|
'icon' => 'mdi:cog-outline',
|
||||||
|
'roles' => ['ROLE_ADMIN'],
|
||||||
|
'items' => [
|
||||||
|
['label' => 'sidebar.admin.teamAbsences', 'to' => '/team-absences', 'icon' => 'mdi:calendar-account-outline', 'module' => 'absence'],
|
||||||
|
['label' => 'sidebar.admin.directory', 'to' => '/directory', 'icon' => 'mdi:card-account-details-outline', 'module' => 'directory'],
|
||||||
|
['label' => 'sidebar.admin.reporting', 'to' => '/reporting', 'icon' => 'mdi:chart-line', 'module' => 'reporting', 'permission' => 'reporting.view'],
|
||||||
|
['label' => 'sidebar.admin.administration', 'to' => '/admin', 'icon' => 'mdi:cog-outline', 'permission' => 'core.users.view'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
> Mettre aussi à jour le commentaire d'en-tête si nécessaire (le bloc décrivant Mail/contextuels reste valable).
|
||||||
|
|
||||||
|
- [ ] **Step 2 : Mettre à jour les clés i18n FR**
|
||||||
|
|
||||||
|
Dans `frontend/i18n/locales/fr.json`, bloc `sidebar` :
|
||||||
|
- `sidebar.general.section` : remplacer la valeur par `"Général"`.
|
||||||
|
- Ajouter `sidebar.tools.section` : `"Outils"`.
|
||||||
|
- Conserver `sidebar.general.dashboard|myTasks|projects|timeTracking|mail` et `sidebar.admin.*`.
|
||||||
|
- Ajouter les clés pour items client (utilisées en Task 3) :
|
||||||
|
- `sidebar.general.myAbsences` : `"Mes absences"`
|
||||||
|
- `sidebar.project.kanban` : `"Kanban"`
|
||||||
|
- `sidebar.project.groups` : `"Groupes"`
|
||||||
|
- `sidebar.project.archives` : `"Archives"`
|
||||||
|
|
||||||
|
Résultat attendu du bloc (extrait) :
|
||||||
|
|
||||||
|
```json
|
||||||
|
"sidebar": {
|
||||||
|
"general": {
|
||||||
|
"section": "Général",
|
||||||
|
"dashboard": "Tableau de bord",
|
||||||
|
"myTasks": "Mes tâches",
|
||||||
|
"projects": "Projets",
|
||||||
|
"timeTracking": "Suivi de temps",
|
||||||
|
"mail": "Messagerie",
|
||||||
|
"myAbsences": "Mes absences"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"section": "Outils"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"kanban": "Kanban",
|
||||||
|
"groups": "Groupes",
|
||||||
|
"archives": "Archives"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"section": "Administration",
|
||||||
|
"teamAbsences": "Absences équipe",
|
||||||
|
"directory": "Répertoire",
|
||||||
|
"administration": "Administration",
|
||||||
|
"reporting": "Rapports"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3 : Répliquer les clés dans les autres locales si présentes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls /home/m-tristan/workspace/Lesstime/frontend/i18n/locales/
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour chaque fichier autre que `fr.json`, ajouter `tools.section`, `general.myAbsences`, `project.kanban|groups|archives` et ajuster `general.section`. S'il n'existe que `fr.json`, ne rien faire de plus.
|
||||||
|
|
||||||
|
- [ ] **Step 4 : Vérifier `/api/sidebar` (admin)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -i php-lesstime-fpm php -r 'var_dump(require "/var/www/config/sidebar.php");' | head -5
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected : le fichier PHP se parse sans erreur (3 entrées de premier niveau). (Le chemin exact dans le container peut différer — sinon, vérifier via `make cache-clear` qui échouerait sur une erreur de syntaxe PHP.)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make cache-clear
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected : succès, pas d'erreur de parse.
|
||||||
|
|
||||||
|
- [ ] **Step 5 : Commit (sur demande utilisateur)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add config/sidebar.php frontend/i18n/locales/
|
||||||
|
git commit -m "refactor(sidebar) : re-catégorisation en 3 groupes (Général / Outils / Administration)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2 : Frontend — assets logo
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `frontend/public/LOGO_MALIO.png`
|
||||||
|
- Create: `frontend/public/LOGO_MALIO_COLLAPSED.png`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces : assets statiques servis à `/LOGO_MALIO.png` et `/LOGO_MALIO_COLLAPSED.png`.
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Copier les logos depuis Starseed**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp /home/m-tristan/workspace/Starseed/frontend/public/LOGO_MALIO.png \
|
||||||
|
/home/m-tristan/workspace/Lesstime/frontend/public/LOGO_MALIO.png
|
||||||
|
cp /home/m-tristan/workspace/Starseed/frontend/public/LOGO_MALIO_COLLAPSED.png \
|
||||||
|
/home/m-tristan/workspace/Lesstime/frontend/public/LOGO_MALIO_COLLAPSED.png
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 : Vérifier**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la /home/m-tristan/workspace/Lesstime/frontend/public/LOGO_MALIO*.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected : deux fichiers présents (~5.8K et ~2.2K).
|
||||||
|
|
||||||
|
- [ ] **Step 3 : Commit (sur demande utilisateur)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/public/LOGO_MALIO.png frontend/public/LOGO_MALIO_COLLAPSED.png
|
||||||
|
git commit -m "chore(sidebar) : ajout des logos Malio (déplié / replié)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3 : Frontend — migration du layout vers `MalioSidebar`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/app/layouts/default.vue`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes : `useSidebar().sections` (clés i18n des Task 1), `useUiStore().sidebarCollapsed`, `SidebarTimer` (`:collapsed`), `useAppVersion().version`, `useMailStore().globalUnreadCount`, `useShareStatus()`, `auth.user.isEmployee`, `auth.user.roles`, `useI18n().t`.
|
||||||
|
- Produces : layout rendant `<MalioSidebar>`.
|
||||||
|
|
||||||
|
> Ce task est une réécriture cohérente d'un seul fichier : la sidebar doit rester fonctionnelle (toutes features préservées) à la fin du task. On ne committe pas d'état intermédiaire cassé.
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Remplacer le bloc `<aside>…</aside>` (lignes 13-104) par `<MalioSidebar>`**
|
||||||
|
|
||||||
|
Nouveau template de la zone sidebar (remplace l'overlay mobile lignes 5-11 **et** l'`<aside>`) :
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<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>
|
||||||
|
```
|
||||||
|
|
||||||
|
Le bloc `<div class="h-full flex-1 …">` (AppTopNav + `<main>` + `<slot/>`) et le `<TimeEntryDrawer>` restent **inchangés**.
|
||||||
|
|
||||||
|
- [ ] **Step 2 : Remplacer la logique `translatedSections` par `mergedSections` dans le `<script setup>`**
|
||||||
|
|
||||||
|
Supprimer le computed `translatedSections` (lignes 144-156) et le remplacer par :
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
> `isDocumentsVisible` existe déjà (ligne 166). `isMailVisible`, `isEmployee`, `currentProjectId`, `sections`, `mailStore`, `t`, `version`, `ui` sont déjà déclarés — ne pas les redéclarer.
|
||||||
|
|
||||||
|
- [ ] **Step 3 : Nettoyer le `<script>` et les imports devenus inutiles**
|
||||||
|
|
||||||
|
- Supprimer `sidebarIsCollapsed` (computed lignes 169-172) **si** plus utilisé après suppression de l'`<aside>` (l'était pour le rendu manuel). Vérifier qu'aucune autre référence ne subsiste :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n "sidebarIsCollapsed" frontend/app/layouts/default.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
S'il ne reste aucune occurrence hors déclaration, supprimer le computed.
|
||||||
|
|
||||||
|
- Conserver `watch(() => route.path, () => { ui.closeMobileSidebar() })` (fermeture mobile sur navigation).
|
||||||
|
- Vérifier que `SidebarLink` n'est plus référencé dans ce fichier (le composant Malio le remplace) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n "SidebarLink" frontend/app/layouts/default.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected : aucune occurrence.
|
||||||
|
|
||||||
|
- [ ] **Step 4 : Build de vérification**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/m-tristan/workspace/Lesstime/frontend && npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected : build Nuxt réussi, **aucune erreur TypeScript** ni de template. (Si `mergedSections`/types invalides, le build échoue ici.)
|
||||||
|
|
||||||
|
- [ ] **Step 5 : QA manuelle (dev server)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev-nuxt # port 3002
|
||||||
|
```
|
||||||
|
|
||||||
|
Vérifier en **admin** (`admin`/`admin`) :
|
||||||
|
- 3 groupes : Général, Outils, Administration.
|
||||||
|
- Général : Tableau de bord, Mes tâches, Projets, Suivi de temps.
|
||||||
|
- En ouvrant un projet (`/projects/<id>`) : Kanban/Groupes/Archives apparaissent dans Général ; Kanban actif uniquement sur la page kanban (exact).
|
||||||
|
- Outils : Messagerie (+ `(N)` si non-lus), Documents (si partage activé).
|
||||||
|
- Administration : Absences équipe, Répertoire, Rapports, Administration.
|
||||||
|
- Footer : timer cliquable (start/stop) + `v <version>` ; en replié, le timer reste (icône) et la version disparaît.
|
||||||
|
- Logo Malio déplié + replié (collapsed via toggle du composant).
|
||||||
|
- Route active surlignée ; pas de doublon `/mail`.
|
||||||
|
|
||||||
|
Vérifier en **utilisateur non-admin** (`alice`/`alice`) :
|
||||||
|
- **Pas** de groupe Administration.
|
||||||
|
- Items gated par permission absents si l'utilisateur n'a pas la permission.
|
||||||
|
- Mes absences visible uniquement si `isEmployee`.
|
||||||
|
|
||||||
|
- [ ] **Step 6 : Vérifier le comportement mobile (largeur < lg)**
|
||||||
|
|
||||||
|
Réduire la fenêtre / activer le responsive devtools.
|
||||||
|
- Vérifier l'ouverture/fermeture de la sidebar sur mobile.
|
||||||
|
- Vérifier le bouton hamburger éventuel de `AppTopNav` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "openMobileSidebar\|sidebarOpen\|closeMobileSidebar" frontend/app/components/ frontend/components/ frontend/app/layouts/default.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
- Si `MalioSidebar` gère le responsive et que l'overlay supprimé n'est plus nécessaire : OK.
|
||||||
|
- Si l'ouverture mobile ne fonctionne plus (ex. AppTopNav appelait `openMobileSidebar` pour l'ancien overlay) : adapter **sans modifier la lib** — a minima conserver le repli/déploiement via `ui.sidebarCollapsed`, ou conserver un déclencheur. Documenter le choix retenu dans le commit.
|
||||||
|
|
||||||
|
- [ ] **Step 7 : Commit (sur demande utilisateur)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/app/layouts/default.vue
|
||||||
|
git commit -m "feat(sidebar) : migration du layout vers MalioSidebar (footer timer + version, logo Malio)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4 : Nettoyage des éléments obsolètes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Possible delete: `frontend/components/ui/SidebarLink.vue`
|
||||||
|
- Possible delete: anciens logos `frontend/public/malio.png`, `frontend/public/LOGO_CARRE.png`
|
||||||
|
|
||||||
|
**Interfaces:** aucun (suppression sûre uniquement si zéro référence).
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Vérifier les usages restants de `SidebarLink`**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "SidebarLink" /home/m-tristan/workspace/Lesstime/frontend --include="*.vue" --include="*.ts" | grep -v node_modules
|
||||||
|
```
|
||||||
|
|
||||||
|
- Si **aucune** occurrence : supprimer le fichier.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git rm frontend/components/ui/SidebarLink.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
- Si encore référencé ailleurs : **ne pas supprimer**, laisser tel quel.
|
||||||
|
|
||||||
|
- [ ] **Step 2 : Vérifier les usages des anciens logos**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "malio.png\|LOGO_CARRE.png" /home/m-tristan/workspace/Lesstime/frontend --include="*.vue" --include="*.ts" --include="*.css" | grep -v node_modules
|
||||||
|
```
|
||||||
|
|
||||||
|
- Si **aucune** occurrence : supprimer les deux PNG.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git rm frontend/public/malio.png frontend/public/LOGO_CARRE.png
|
||||||
|
```
|
||||||
|
|
||||||
|
- Sinon : conserver.
|
||||||
|
|
||||||
|
- [ ] **Step 3 : Build final**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/m-tristan/workspace/Lesstime/frontend && npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected : build réussi.
|
||||||
|
|
||||||
|
- [ ] **Step 4 : Commit (sur demande utilisateur)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore(sidebar) : suppression des composants/assets obsolètes de l'ancienne sidebar"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review (auteur du plan)
|
||||||
|
|
||||||
|
**Spec coverage :**
|
||||||
|
- Remplacement par MalioSidebar → Task 3 ✓
|
||||||
|
- Permissions serveur préservées → Task 1 (gates inchangés) + Task 3 (mail filtré/ré-injecté, garde-fou sections) ✓
|
||||||
|
- 3 groupes Général/Outils/Administration → Task 1 + Task 3 (ordre canonique) ✓
|
||||||
|
- Footer timer + version → Task 3 Step 1 ✓
|
||||||
|
- Logo Malio Starseed → Task 2 + Task 3 ✓
|
||||||
|
- Items contextuels (Kanban/Groupes/Archives, Documents, Mes absences) → Task 3 `clientItemsFor` ✓
|
||||||
|
- Badge mail = suffixe `(N)` → Task 3 `clientItemsFor` ✓
|
||||||
|
- Mobile → Task 3 Step 6 ✓
|
||||||
|
- Nettoyage → Task 4 ✓
|
||||||
|
|
||||||
|
**Placeholder scan :** pas de TBD ; les branches conditionnelles de suppression (Task 4) et d'adaptation mobile (Task 3 Step 6) sont des décisions binaires basées sur un `grep`, pas des placeholders.
|
||||||
|
|
||||||
|
**Type consistency :** `MalioItem`/`MalioSection` définis une fois (Task 3) et utilisés de façon cohérente ; `clientItemsFor`/`mergedSections`/`SECTION_ORDER`/`SECTION_ICON` cohérents. Items produits conformes au type attendu par `MalioSidebar` (`{label, to, exact?}`).
|
||||||
|
|
||||||
|
**Réserve connue :** absence de runner de test FE → vérification par build + QA manuelle (assumé, conforme à l'état du repo).
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
# Migration de la sidebar vers `MalioSidebar` (@malio/layer-ui)
|
||||||
|
|
||||||
|
**Date** : 2026-06-25
|
||||||
|
**Statut** : Design validé
|
||||||
|
**Scope** : Frontend (layout) + backend (config sidebar) + assets
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
La sidebar actuelle de Lesstime est un `<aside>` fait main dans
|
||||||
|
`frontend/app/layouts/default.vue`, qui itère sur les sections renvoyées par
|
||||||
|
`/api/sidebar` et rend chaque item via le composant maison `SidebarLink`. Le
|
||||||
|
timer et la version sont empilés en bas du `<aside>`, le toggle collapse et
|
||||||
|
l'overlay mobile sont gérés manuellement.
|
||||||
|
|
||||||
|
La librairie `@malio/layer-ui` (mise à jour) fournit désormais un composant
|
||||||
|
`MalioSidebar`. Le projet **Starseed** a déjà effectué cette migration sur une
|
||||||
|
architecture identique (`config/sidebar.php` → `SidebarProvider` → composable
|
||||||
|
`useSidebar` → layout). Cette spec applique la même migration à Lesstime, avec
|
||||||
|
trois spécificités Lesstime : footer (timer + version), re-catégorisation des
|
||||||
|
onglets, et plusieurs items contextuels rendus côté client.
|
||||||
|
|
||||||
|
On **ne modifie pas** la lib `@malio/layer-ui` (règle CLAUDE.md).
|
||||||
|
|
||||||
|
## Objectifs
|
||||||
|
|
||||||
|
1. Remplacer le `<aside>` maison par `<MalioSidebar>`.
|
||||||
|
2. Préserver le filtrage des permissions/rôles/modules **côté serveur**.
|
||||||
|
3. Re-catégoriser la navigation en 3 groupes : **Général / Outils / Administration**.
|
||||||
|
4. Mettre le timer et la version dans le **footer** du composant.
|
||||||
|
5. Reprendre le **logo Malio** de Starseed.
|
||||||
|
|
||||||
|
## Décisions validées
|
||||||
|
|
||||||
|
- **Catégorisation** : 3 groupes (option B).
|
||||||
|
- **Badge mail** : le compteur de non-lus devient un **suffixe sur le label**
|
||||||
|
(`Messagerie (3)`), faute de slot badge/icône par item dans `MalioSidebar`.
|
||||||
|
|
||||||
|
## Contraintes du composant `MalioSidebar`
|
||||||
|
|
||||||
|
Source : `frontend/node_modules/@malio/layer-ui/app/components/malio/sidebar/Sidebar.vue`.
|
||||||
|
|
||||||
|
- **Props** : `sections` (requis), `modelValue` (v-model collapse, bool),
|
||||||
|
`id`, `sidebarClass`, `toggleClass`.
|
||||||
|
- **Types** :
|
||||||
|
- `SidebarItem = { label: string; to: string; exact?: boolean }`
|
||||||
|
- `SidebarSection = { label?: string; icon?: string; items: SidebarItem[] }`
|
||||||
|
- **Slots** : `#logo`, `#logo-collapsed`, `#footer`, `#footer-collapsed`.
|
||||||
|
- **Events** : `update:modelValue(boolean)`.
|
||||||
|
- **Item** : pas d'icône par item ni de badge — uniquement l'icône de section.
|
||||||
|
Route active = match exact ou par préfixe (`exact: true` pour exact strict).
|
||||||
|
- Largeurs fixes : 232px (déplié) / 72px (replié). Toggle interne.
|
||||||
|
|
||||||
|
### Conséquences (compromis assumés)
|
||||||
|
|
||||||
|
- Perte de l'**icône par item** (design malioUI = texte + icône de section).
|
||||||
|
Starseed fonctionne ainsi.
|
||||||
|
- Le **badge mail** ne peut pas être une pastille → suffixe `(N)` dans le label.
|
||||||
|
|
||||||
|
## Architecture cible
|
||||||
|
|
||||||
|
Modèle **backend-driven** conservé (sécurité serveur intacte). Le frontend
|
||||||
|
mappe les sections renvoyées par `/api/sidebar` vers le format `MalioSidebar`
|
||||||
|
et **fusionne** les items contextuels (qui dépendent d'un état runtime non
|
||||||
|
connu du backend).
|
||||||
|
|
||||||
|
### 1. Backend — `config/sidebar.php`
|
||||||
|
|
||||||
|
Re-catégorisation en 3 sections (gates inchangés, juste réorganisés) :
|
||||||
|
|
||||||
|
```
|
||||||
|
GÉNÉRAL (sidebar.general.section, icon mdi:view-dashboard-outline)
|
||||||
|
Tableau de bord / —
|
||||||
|
Mes tâches /my-tasks module project-management, perm tasks.view
|
||||||
|
Projets /projects module project-management, perm projects.view
|
||||||
|
Suivi de temps /time-tracking module time-tracking, perm entries.view
|
||||||
|
|
||||||
|
OUTILS (sidebar.tools.section, icon mdi:tools)
|
||||||
|
Messagerie /mail module mail
|
||||||
|
(filtré du rendu backend côté front, ré-injecté avec badge)
|
||||||
|
|
||||||
|
ADMINISTRATION (sidebar.admin.section, icon mdi:cog-outline, roles [ROLE_ADMIN])
|
||||||
|
Absences équipe /team-absences module absence
|
||||||
|
Répertoire /directory module directory
|
||||||
|
Rapports /reporting module reporting, perm reporting.view
|
||||||
|
Administration /admin perm core.users.view
|
||||||
|
```
|
||||||
|
|
||||||
|
> `/mail` reste déclaré pour le gating module (`disabledRoutes`), mais est
|
||||||
|
> filtré des sections rendues et ré-injecté côté client avec son badge, comme
|
||||||
|
> aujourd'hui.
|
||||||
|
|
||||||
|
### 2. i18n — `frontend/i18n/locales/fr.json`
|
||||||
|
|
||||||
|
- Renommer `sidebar.general.section` : « Gestion de projet » → « Général ».
|
||||||
|
- Ajouter `sidebar.tools.section` : « Outils ».
|
||||||
|
- Conserver les clés d'items existantes. Items client : réutiliser les clés
|
||||||
|
existantes quand elles existent (`sharedFiles.sidebar.title` pour Documents,
|
||||||
|
`mail.sidebar.title`/`sidebar.general.mail` pour Messagerie) ; ajouter une
|
||||||
|
clé pour « Mes absences » (aujourd'hui en dur) et pour les contextuels
|
||||||
|
(Kanban/Groupes/Archives, aujourd'hui en dur) si on souhaite les traduire,
|
||||||
|
sinon conserver les libellés en dur actuels.
|
||||||
|
|
||||||
|
### 3. Frontend — `frontend/app/layouts/default.vue`
|
||||||
|
|
||||||
|
Réécriture du template autour de `<MalioSidebar>` :
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<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>
|
||||||
|
<SidebarTimer :collapsed="false" />
|
||||||
|
<p class="font-bold">v {{ version }}</p>
|
||||||
|
</template>
|
||||||
|
<template #footer-collapsed>
|
||||||
|
<SidebarTimer :collapsed="true" />
|
||||||
|
</template>
|
||||||
|
</MalioSidebar>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Computed `mergedSections`** : construit les sections finales dans l'ordre
|
||||||
|
canonique `[général, outils, administration]`.
|
||||||
|
|
||||||
|
Logique de fusion :
|
||||||
|
1. Partir des sections backend (déjà filtrées), mappées en
|
||||||
|
`{ label: t(label), icon, items: items.filter(to !== '/mail').map({label: t, to}) }`.
|
||||||
|
2. Définir une table `clientItems` indexée par clé de section :
|
||||||
|
- `sidebar.general.section` → (si `currentProjectId`) Kanban (`exact`),
|
||||||
|
Groupes, Archives ; puis (si `isEmployee`) Mes absences.
|
||||||
|
- `sidebar.tools.section` → (si `isMailVisible`) Messagerie avec label
|
||||||
|
`Messagerie` + suffixe `(N)` quand `mailStore.globalUnreadCount > 0`
|
||||||
|
(`99+` au-delà) ; puis (si `shareEnabled`) Documents.
|
||||||
|
3. Pour chaque section backend, **append** ses items client.
|
||||||
|
4. Si une clé de `clientItems` produit des items mais que la section
|
||||||
|
correspondante n'est **pas** présente dans la réponse backend (ex. module
|
||||||
|
mail off mais partage on → pas de section « Outils » côté backend), **créer**
|
||||||
|
la section côté front (label + icône depuis une table locale).
|
||||||
|
5. **Supprimer** les sections finales sans items.
|
||||||
|
6. Trier selon l'ordre canonique des clés.
|
||||||
|
|
||||||
|
Le reste du `<script>` (timer title watchers, `refData`/`TimeEntryDrawer`,
|
||||||
|
polling mail, `ensureShareStatus`, `currentProjectId`, `isEmployee`,
|
||||||
|
`isMailVisible`, `shareEnabled`) est **conservé tel quel**.
|
||||||
|
|
||||||
|
### 4. Mobile
|
||||||
|
|
||||||
|
Starseed a **supprimé l'overlay mobile custom** et ne garde que
|
||||||
|
`watch(route) → ui.closeMobileSidebar()`. On s'aligne : suppression du markup
|
||||||
|
overlay (`ui.sidebarOpen`, `.sidebar-overlay`) si `MalioSidebar` gère le
|
||||||
|
responsive. **À vérifier à l'implémentation** : comportement mobile réel du
|
||||||
|
composant ; si l'ouverture mobile n'est pas couverte, adapter a minima sans
|
||||||
|
modifier la lib.
|
||||||
|
|
||||||
|
### 5. Assets — logo
|
||||||
|
|
||||||
|
Copier depuis Starseed vers `frontend/public/` :
|
||||||
|
- `LOGO_MALIO.png` (128×44)
|
||||||
|
- `LOGO_MALIO_COLLAPSED.png` (34×40)
|
||||||
|
|
||||||
|
Les anciens `/malio.png` et `/LOGO_CARRE.png` ne sont plus référencés par le
|
||||||
|
layout (les laisser ou les retirer si plus aucun usage — à vérifier).
|
||||||
|
|
||||||
|
## Composants / éléments réutilisés
|
||||||
|
|
||||||
|
- `SidebarTimer` (`components/ui/SidebarTimer.vue`) : inchangé, déjà piloté par
|
||||||
|
`:collapsed`.
|
||||||
|
- `useAppVersion()` : inchangé.
|
||||||
|
- `useSidebar()` : inchangé.
|
||||||
|
- `usePermissions()` : inchangé (le filtrage permission reste backend ; les
|
||||||
|
flags client `isEmployee`/`isMailVisible`/`shareEnabled` restent locaux).
|
||||||
|
|
||||||
|
## Éléments supprimés
|
||||||
|
|
||||||
|
- Le `<aside>` manuel et son markup (logo, nav, toggle, overlay) dans
|
||||||
|
`default.vue`.
|
||||||
|
- L'usage de `SidebarLink` dans le layout (le composant peut rester s'il est
|
||||||
|
utilisé ailleurs — à vérifier ; sinon suppression possible).
|
||||||
|
|
||||||
|
## Critères d'acceptation
|
||||||
|
|
||||||
|
1. La sidebar est rendue par `<MalioSidebar>`.
|
||||||
|
2. 3 groupes : Général, Outils, Administration (Administration visible
|
||||||
|
uniquement pour `ROLE_ADMIN` / permissions, comme avant).
|
||||||
|
3. Toutes les permissions/rôles/modules sont respectés à l'identique (aucune
|
||||||
|
régression de visibilité pour user/admin).
|
||||||
|
4. Items contextuels présents : Kanban/Groupes/Archives (dans un projet),
|
||||||
|
Documents (partage activé), Mes absences (employé).
|
||||||
|
5. Messagerie affiche `(N)` quand il y a des non-lus.
|
||||||
|
6. Footer : timer fonctionnel + version (version masquée en replié).
|
||||||
|
7. Logo Malio de Starseed affiché (déplié + replié).
|
||||||
|
8. Collapse/expand et route active fonctionnent.
|
||||||
|
9. Pas de doublon `/mail`. Pas de section vide affichée.
|
||||||
|
10. Build Nuxt OK, pas d'erreur TS.
|
||||||
|
|
||||||
|
## Hors scope
|
||||||
|
|
||||||
|
- Refonte du `SiteSelector` (n'existe pas dans Lesstime).
|
||||||
|
- Modification de la lib `@malio/layer-ui`.
|
||||||
|
- Changement du modèle de permissions backend.
|
||||||
+125
-139
@@ -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"
|
</template>
|
||||||
src="/malio.png"
|
<template #logo-collapsed>
|
||||||
alt="Logo"
|
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio"/>
|
||||||
class="w-auto"
|
</template>
|
||||||
/>
|
<template #footer>
|
||||||
<img
|
<div class="flex flex-col gap-2">
|
||||||
v-else
|
<SidebarTimer :collapsed="false" />
|
||||||
src="/LOGO_CARRE.png"
|
<p v-if="version" class="text-center text-sm font-bold">v {{ version }}</p>
|
||||||
alt="Logo"
|
</div>
|
||||||
class="w-[46px] h-[55px]"
|
</template>
|
||||||
/>
|
<template #footer-collapsed>
|
||||||
<button
|
<SidebarTimer :collapsed="true" />
|
||||||
class="mr-2 rounded-md p-2 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-700 transition-colors lg:hidden"
|
</template>
|
||||||
@click="ui.closeMobileSidebar()"
|
</MalioSidebar>
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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,22 +68,116 @@ 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
|
|
||||||
watch(() => route.path, () => {
|
|
||||||
ui.closeMobileSidebar()
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentProjectId = computed(() => {
|
const currentProjectId = computed(() => {
|
||||||
const match = route.path.match(/^\/projects\/(\d+)/)
|
const match = route.path.match(/^\/projects\/(\d+)/)
|
||||||
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 timerStore = useTimerStore()
|
||||||
|
|
||||||
const baseTitle = ref('Lesstime')
|
const baseTitle = ref('Lesstime')
|
||||||
@@ -268,14 +265,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>
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
<div class="flex h-full items-center justify-between">
|
<div class="flex h-full items-center justify-between">
|
||||||
<MalioButtonIcon
|
<MalioButtonIcon
|
||||||
icon="mdi:menu"
|
icon="mdi:menu"
|
||||||
aria-label="Menu"
|
aria-label="Replier ou déplier le menu"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon-size="24"
|
icon-size="24"
|
||||||
button-class="lg:hidden text-white hover:bg-primary-600"
|
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">
|
<div class="hidden items-center gap-2 lg:flex">
|
||||||
<h1 class="text-lg font-bold tracking-tight">Lesstime</h1>
|
<h1 class="text-lg font-bold tracking-tight">Lesstime</h1>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -349,21 +349,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"myTasks": "Mes tâches",
|
|
||||||
"general": {
|
"general": {
|
||||||
"section": "Gestion de projet",
|
"section": "Général",
|
||||||
"dashboard": "Tableau de bord",
|
"dashboard": "Tableau de bord",
|
||||||
"myTasks": "Mes tâches",
|
"myTasks": "Mes tâches",
|
||||||
"projects": "Projets",
|
"projects": "Projets",
|
||||||
"timeTracking": "Suivi de temps",
|
"timeTracking": "Suivi de temps",
|
||||||
"mail": "Messagerie"
|
"mail": "Messagerie",
|
||||||
|
"myAbsences": "Mes absences"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"section": "Outils"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"kanban": "Kanban",
|
||||||
|
"groups": "Groupes",
|
||||||
|
"archives": "Archives"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"section": "Administration",
|
"section": "Administration",
|
||||||
"teamAbsences": "Absences équipe",
|
"teamAbsences": "Absences équipe",
|
||||||
"administration": "Administration",
|
|
||||||
"directory": "Répertoire",
|
"directory": "Répertoire",
|
||||||
"reporting": "Rapports"
|
"reporting": "Rapports",
|
||||||
|
"administration": "Administration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reporting": {
|
"reporting": {
|
||||||
|
|||||||
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 |
@@ -1,6 +1,5 @@
|
|||||||
export const useUiStore = defineStore('ui', () => {
|
export const useUiStore = defineStore('ui', () => {
|
||||||
const sidebarCollapsed = ref(false)
|
const sidebarCollapsed = ref(false)
|
||||||
const sidebarOpen = ref(false)
|
|
||||||
const darkMode = ref(false)
|
const darkMode = ref(false)
|
||||||
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
@@ -45,13 +44,5 @@ export const useUiStore = defineStore('ui', () => {
|
|||||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMobileSidebar() {
|
return { sidebarCollapsed, darkMode, toggleSidebar, toggleDarkMode }
|
||||||
sidebarOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeMobileSidebar() {
|
|
||||||
sidebarOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return { sidebarCollapsed, sidebarOpen, darkMode, toggleSidebar, openMobileSidebar, closeMobileSidebar, toggleDarkMode }
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user