Files
Lesstime/docs/superpowers/plans/2026-06-25-malio-sidebar-migration.md
T
tristan c766e76624
Auto Tag Develop / tag (push) Successful in 8s
feat(sidebar) : migration vers MalioSidebar — 3 groupes, footer timer+version, logo (LST-71) (#26)
## 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>
2026-06-25 15:30:23 +00:00

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 : 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.phpModify : re-catégorisation en 3 sections.
  • frontend/i18n/locales/fr.jsonModify : clés de sections/items.
  • frontend/i18n/locales/*.json (autres langues présentes) — Modify si existantes : mêmes clés.
  • frontend/public/LOGO_MALIO.pngCreate (copie Starseed).
  • frontend/public/LOGO_MALIO_COLLAPSED.pngCreate (copie Starseed).
  • frontend/app/layouts/default.vueModify : réécriture du template sidebar + logique mergedSections.
  • frontend/components/ui/SidebarLink.vuePossible 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/*.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 :

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) :

"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)"

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

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 translatedSections par mergedSections dans 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
})

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 :

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) :
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 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)

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 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).