diff --git a/docs/superpowers/plans/2026-05-19-mail-phase7-admin-polish.md b/docs/superpowers/plans/2026-05-19-mail-phase7-admin-polish.md new file mode 100644 index 0000000..516bfd7 --- /dev/null +++ b/docs/superpowers/plans/2026-05-19-mail-phase7-admin-polish.md @@ -0,0 +1,526 @@ +# Mail Integration — Phase 7 : Admin Config + Sidebar + Polish + +> **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:** Finaliser l'intégration mail avec l'UI admin de configuration, le lien sidebar avec badge unread temps réel (polling 30s), et la documentation utilisateur/opérationnelle finale. + +**Architecture:** Onglet `AdminMailTab.vue` calqué sur `AdminZimbraTab.vue` (form IMAP/SMTP/credentials, bouton test connexion). Lien sidebar dans `layouts/default.vue` (visible ROLE_USER+ROLE_ADMIN seulement, masqué ROLE_CLIENT pur). Polling start au login / stop au logout via layout. Documentation finale dans `docs/` + section README mail. + +**Tech Stack:** Nuxt 4, Vue 3 Composition API, @malio/layer-ui, Pinia (useMailStore). + +--- + +## Fichiers créés / modifiés + +| Fichier | Action | +|---------|--------| +| `frontend/components/admin/AdminMailTab.vue` | **Créer** | +| `frontend/pages/admin.vue` | **Modifier** (ajout onglet mail) | +| `frontend/layouts/default.vue` | **Modifier** (lien sidebar + polling lifecycle) | +| `frontend/i18n/locales/fr.json` | **Modifier** (clés mail.admin.* + mail.sidebar.*) | +| `frontend/i18n/locales/en.json` | **Modifier si présent** | +| `docs/mail-cron-setup.md` | **Modifier** (enrichir checklist prod + sécurité) | +| `docs/mail-integration.md` | **Créer** (doc complète intégration) | + +--- + +## Task 1 : Composant `AdminMailTab.vue` + +**Fichier cible :** `frontend/components/admin/AdminMailTab.vue` + +**Modèle de référence :** `frontend/components/admin/AdminZimbraTab.vue` — reproduire exactement le même pattern (reactive form, hasPassword, isSaving/isTesting, loadSettings onMounted, handleSave/handleTest). + +**Service à utiliser :** `useMailService()` depuis `~/services/mail` — méthodes `getConfiguration`, `updateConfiguration`, `testConfiguration`. + +**DTOs :** `MailConfigurationDto`, `MailConfigurationUpdateDto`, `MailTestConnectionResultDto` depuis `~/services/dto/mail`. + +### Étapes + +- [ ] Créer `frontend/components/admin/AdminMailTab.vue` +- [ ] Déclarer le reactive form avec tous les champs de `MailConfigurationDto` (sauf `hasPassword`, qui est en lecture seule) : + ``` + protocol: '' (lecture seule "imap" en MVP — champ disabled) + imapHost: '' + imapPort: 993 (default OVH) + imapEncryption: 'ssl' (default OVH) + smtpHost: '' + smtpPort: 465 (default OVH) + smtpEncryption: 'ssl' (default OVH) + username: '' + password: '' (write-only — jamais pré-rempli) + sentFolderPath: '' (ex: "Sent Messages" ou "INBOX.Sent") + enabled: false + ``` +- [ ] `hasPassword` : `ref(false)` — alimenté par `getConfiguration().hasPassword` +- [ ] `isSaving` : `ref(false)`, `isTesting` : `ref(false)` +- [ ] `testResult` : `ref(null)` — réinitialisé à null au handleSave +- [ ] `loadSettings()` : + ```ts + async function loadSettings(): Promise { + const config = await getConfiguration() + form.protocol = config.protocol ?? 'imap' + form.imapHost = config.imapHost ?? '' + form.imapPort = config.imapPort ?? 993 + form.imapEncryption = config.imapEncryption ?? 'ssl' + form.smtpHost = config.smtpHost ?? '' + form.smtpPort = config.smtpPort ?? 465 + form.smtpEncryption = config.smtpEncryption ?? 'ssl' + form.username = config.username ?? '' + form.sentFolderPath = config.sentFolderPath ?? '' + form.enabled = config.enabled + hasPassword.value = config.hasPassword + // password jamais pré-rempli + } + ``` +- [ ] `handleSave()` : construit un `MailConfigurationUpdateDto` — inclure `password` uniquement si `form.password` est non-vide, sinon omettre le champ. Après save réussi : `hasPassword.value = result.hasPassword`, vider `form.password`, `testResult.value = null` +- [ ] `handleTest()` : appelle `testConfiguration()`, `testResult.value = result.ok`. Le champ `result.error` est affiché en sous-texte si `testResult.value === false` +- [ ] Template — sections IMAP et SMTP avec labels traduits : + - Titre `h2` : `$t('mail.admin.title')` + - Section IMAP (`fieldset` ou `div` avec titre `$t('mail.admin.imapSection')`) : + - `MalioInputText` pour `imapHost` + helper text `$t('mail.admin.ovhDefaultsHelp')` sous le champ (texte gris : `ssl0.ovh.net`) + - `input[type=number]` natif pour `imapPort` (MalioInputText n'accepte pas les number — voir convention CLAUDE.md) + - `select` natif pour `imapEncryption` (options : `ssl`, `tls`, `none`) + - Section SMTP (`$t('mail.admin.smtpSection')`) : + - `MalioInputText` pour `smtpHost` + - `input[type=number]` natif pour `smtpPort` + - `select` natif pour `smtpEncryption` (options : `ssl`, `tls`, `none`) + - Credentials : + - `MalioInputText` pour `username` + - `MalioInputPassword` pour `password` + indicateur `hasPassword` (même pattern que `AdminZimbraTab.vue` : `

{{ $t('mail.admin.passwordSet') }}

`) + - `MalioInputText` pour `sentFolderPath` (placeholder: `Sent Messages`) + - `label` + checkbox natif pour `enabled` : `$t('mail.admin.enabled')` + - Boutons côte à côte : + - `MalioButton` submit `$t('mail.admin.save')` `:disabled="isSaving"` → `handleSave` + - `MalioButton` variant tertiary `$t('mail.admin.test')` `:disabled="isTesting"` → `handleTest` + - Résultat test : `

` coloré vert/rouge selon valeur — si false ET `testError`, afficher `testError` sous le résultat +- [ ] `onMounted(() => { loadSettings() })` +- [ ] Vérifier indentation 4 espaces, pas d'imports inutilisés, TypeScript strict + +--- + +## Task 2 : Intégration `AdminMailTab` dans `pages/admin.vue` + +**Fichier cible :** `frontend/pages/admin.vue` + +Le pattern actuel utilise un tableau `tabs as const` + `activeTab` ref + v-if par composant. Il suffit d'ajouter l'entrée mail à la fin. + +### Étapes + +- [ ] Ouvrir `frontend/pages/admin.vue` +- [ ] Dans le tableau `tabs`, ajouter à la fin : + ```ts + { key: 'mail', label: 'Mail' }, + ``` + Remarque : les labels dans `tabs` sont des string litéraux inline (cf. autres onglets comme `'Zimbra'`), pas de i18n ici. +- [ ] Le type `TabKey` est inféré automatiquement via `typeof tabs[number]['key']` — pas de changement nécessaire +- [ ] Dans le template, après ``, ajouter : + ```html + + ``` +- [ ] Vérifier que Nuxt auto-importe `AdminMailTab` (fichier dans `components/admin/` → auto-import OK) +- [ ] Test manuel : naviguer vers `/admin`, cliquer l'onglet "Mail", vérifier que le form se charge sans erreur 403 si connecté ROLE_ADMIN + +--- + +## Task 3 : Lien sidebar dans `layouts/default.vue` + +**Fichier cible :** `frontend/layouts/default.vue` + +Le composant `SidebarLink` accepte `to`, `icon`, `label`, `collapsed`. Il n'a pas de prop `badge` native — vérifier dans `@malio/layer-ui/COMPONENTS.md` si une prop badge existe. Si non, wrapper manuel avec un `

` + badge absolu. + +### Étapes + +- [ ] Lire `frontend/node_modules/@malio/layer-ui/COMPONENTS.md` pour vérifier les props de `SidebarLink` (présence prop `badge` ou `badgeCount`) +- [ ] **Cas A — SidebarLink a une prop badge :** + Utiliser directement : + ```html + + ``` +- [ ] **Cas B — SidebarLink n'a pas de prop badge (plus probable) :** + Wrapper avec badge manuel : + ```html +
+ + + {{ mailStore.globalUnreadCount > 99 ? '99+' : mailStore.globalUnreadCount }} + +
+ ``` +- [ ] Dans `')) + }) + ``` + Résultat attendu : chaîne sans `