## Objectif Remplacer la sidebar maison par le composant `MalioSidebar` de `@malio/layer-ui` (alignement avec Starseed). ## Changements - **Backend** : `config/sidebar.php` re-catégorisé en **3 groupes** (Général / Outils / Administration). Tous les gates permission/rôle/module **préservés côté serveur** (rien déplacé côté client). - **Frontend** : `app/layouts/default.vue` migré vers `<MalioSidebar>`. Un computed `mergedSections` mappe les sections backend et y fusionne les items contextuels (Kanban/Groupes/Archives sous « Projets », Mes absences, Messagerie avec compteur `(N)`, Documents). - **Footer** : timer (`SidebarTimer`) + version de l'app (masquée en mode replié). - **Logo** : logos Malio repris de Starseed (`LOGO_MALIO.png` / `LOGO_MALIO_COLLAPSED.png`). - **Mobile** : `MalioSidebar` étant toujours visible (pas de tiroir off-canvas), le hamburger pilote désormais le repli ; suppression du code de tiroir mobile mort (`sidebarOpen`/`openMobileSidebar`/`closeMobileSidebar`). - **Nettoyage** : suppression de `SidebarLink.vue` et `LOGO_CARRE.png` (obsolètes). `malio.png` conservé (utilisé par la page login). - **i18n** : nouvelles clés `sidebar.tools.section`, `sidebar.general.myAbsences`, `sidebar.project.kanban|groups|archives` ; `sidebar.general.section` → « Général ». ## Compromis (limites du composant, lib non modifiée) - Pas d'icône par item (uniquement icône de section) — design malioUI, comme Starseed. - Badge mail → suffixe `(N)` dans le libellé. ## Vérifications - Build Nuxt OK (`✨ Build complete!`, exit 0). - Revue par task + revue finale whole-branch : aucun Critical/Important. - Sécurité : filtrage des permissions inchangé (côté serveur). Specs/plan : `docs/superpowers/specs/2026-06-25-malio-sidebar-migration-design.md`, `docs/superpowers/plans/2026-06-25-malio-sidebar-migration.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #26 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
20 KiB
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: propssections(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 surdevelop). - 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 + logiquemergedSections.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
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/*.jsonsi présentes (mêmes clés)
Interfaces:
-
Produces :
/api/sidebarrenvoie des sections dont leslabelsont les cléssidebar.general.section,sidebar.tools.section,sidebar.admin.section. Items inchangés ento; gates (module/roles/permission) inchangés, juste réorganisés. -
Step 1 : Réécrire
config/sidebar.phpen 3 sections
Remplacer le return [...] (lignes 20-44) par :
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|mailetsidebar.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) :
"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
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)
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.)
make cache-clear
Expected : succès, pas d'erreur de parse.
- Step 5 : Commit (sur demande utilisateur)
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.pnget/LOGO_MALIO_COLLAPSED.png. -
Step 1 : Copier les logos depuis Starseed
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
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)
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>) :
<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
translatedSectionsparmergedSectionsdans le<script setup>
Supprimer le computed translatedSections (lignes 144-156) et le remplacer par :
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
})
isDocumentsVisibleexiste déjà (ligne 166).isMailVisible,isEmployee,currentProjectId,sections,mailStore,t,version,uisont 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 :
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
SidebarLinkn'est plus référencé dans ce fichier (le composant Malio le remplace) :
grep -n "SidebarLink" frontend/app/layouts/default.vue
Expected : aucune occurrence.
- Step 4 : Build de vérification
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)
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:
grep -rn "openMobileSidebar\|sidebarOpen\|closeMobileSidebar" frontend/app/components/ frontend/components/ frontend/app/layouts/default.vue
-
Si
MalioSidebargère le responsive et que l'overlay supprimé n'est plus nécessaire : OK. -
Si l'ouverture mobile ne fonctionne plus (ex. AppTopNav appelait
openMobileSidebarpour l'ancien overlay) : adapter sans modifier la lib — a minima conserver le repli/déploiement viaui.sidebarCollapsed, ou conserver un déclencheur. Documenter le choix retenu dans le commit. -
Step 7 : Commit (sur demande utilisateur)
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
grep -rn "SidebarLink" /home/m-tristan/workspace/Lesstime/frontend --include="*.vue" --include="*.ts" | grep -v node_modules
- Si aucune occurrence : supprimer le fichier.
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
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.
git rm frontend/public/malio.png frontend/public/LOGO_CARRE.png
-
Sinon : conserver.
-
Step 3 : Build final
cd /home/m-tristan/workspace/Lesstime/frontend && npm run build
Expected : build réussi.
- Step 4 : Commit (sur demande utilisateur)
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 3clientItemsFor✓ - 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).