# Mail Integration — Phase 6 : Intégration Tâches

> **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:** Brancher l'intégration mail ↔ tâches : modal "Créer tâche depuis mail" (pré-remplie subject+body), modal "Lier mail à tâche existante" (autocomplete), nouvel onglet "Mails" dans `TaskModal.vue` listant les mails liés.

**Architecture:** 3 nouveaux composants modals sous `components/mail/`, modification de `TaskModal.vue` pour ajouter un onglet `mails` à côté des onglets `details` et `planning` existants, branchement des handlers placeholders dans `pages/mail.vue`. Pas de nouveau backend (endpoints Phase 3 déjà en place).

**Contexte codebase important :**
- Il n'y a pas de `TaskDrawer.vue` — le composant principal est `TaskModal.vue` (`frontend/components/task/TaskModal.vue`), qui contient un système d'onglets `details` / `planning`.
- `TaskModal.vue` est ouvert via `v-model: boolean` (prop `modelValue`) et reçoit la tâche complète via la prop `task: Task | null`.
- Le service `useMailService()` expose déjà : `createTaskFromMail(mailId, input)`, `linkTask(mailId, taskId)`, `listMailsForTask(taskId)`.
- `MailCreateTaskInput` est `{ projectId: number; taskGroupId?: number | null; priority?: string | null }` — le backend dérive titre/description du mail, le frontend passe uniquement les affectations.
- `useProjectService().getAll()`, `useTaskGroupService().getByProject(projectId)`, `useTaskPriorityService().getAll()` sont les services de sélection.
- Le projet est SPA (SSR off) — pas de guard `import.meta.client` nécessaire pour DOM.
- Pattern modal existant : `<Teleport to="body">` + `<Transition>` + backdrop click pour fermer (voir `TaskModal.vue`).
- `MalioSelect` requiert `{ label: string, value: number | null }` — utiliser `<select>` natif pour les enums string (ex: priority qui est une string).

**Tech Stack:** Nuxt 4, Vue 3 Composition API + `<script setup lang="ts">`, Pinia, @malio/layer-ui (MalioButton, MalioButtonIcon, MalioSelect), Tailwind CSS, @nuxt/icon.

**Branche cible :** `feat/mail-integration` (vérifier qu'elle est active avant de commencer).

**Fichiers créés/modifiés par le codeur :**

| Fichier | Action |
|---|---|
| `frontend/components/mail/MailCreateTaskModal.vue` | Créer |
| `frontend/components/mail/MailLinkTaskModal.vue` | Créer |
| `frontend/components/mail/MailPickerModal.vue` | Créer |
| `frontend/components/task/TaskModal.vue` | Modifier (ajout onglet `mails`) |
| `frontend/pages/mail.vue` | Modifier (brancher handlers placeholders) |
| `frontend/i18n/locales/fr.json` | Modifier (ajout sous-clés `mail.createTaskModal`, `mail.linkTaskModal`, `mail.pickerModal`, `mail.taskTab`) |
| `frontend/i18n/locales/en.json` | Modifier si le fichier existe |

---

### Task 1 : Vérification de l'environnement

- [ ] **Step 1 : Vérifier la branche active**

    ```bash
    git -C /home/r-dev/malio-dev/Lesstime branch --show-current
    ```

    Attendu : `feat/mail-integration`. Si non :

    ```bash
    git -C /home/r-dev/malio-dev/Lesstime checkout feat/mail-integration
    ```

- [ ] **Step 2 : Vérifier que les fichiers Phase 5 existent**

    ```bash
    ls -la /home/r-dev/malio-dev/Lesstime/frontend/pages/mail.vue \
           /home/r-dev/malio-dev/Lesstime/frontend/components/mail/MailMessageViewer.vue \
           /home/r-dev/malio-dev/Lesstime/frontend/services/mail.ts \
           /home/r-dev/malio-dev/Lesstime/frontend/services/dto/mail.ts
    ```

    Attendu : les 4 fichiers présents. Si absents, implémenter Phase 5 d'abord.

- [ ] **Step 3 : Vérifier le baseline TypeScript**

    ```bash
    cd /home/r-dev/malio-dev/Lesstime/frontend && npx tsc --noEmit 2>&1 | head -30
    ```

    Noter les erreurs existantes (si any) pour les distinguer des erreurs Phase 6.

---

### Task 2 : Clés i18n Phase 6 — `frontend/i18n/locales/fr.json`

Ajouter les sous-clés dans le bloc `mail` existant (après la clé `task` déjà présente en Phase 5).

- [ ] **Step 1 : Ajouter les sous-clés dans `fr.json`**

    Dans le bloc `"mail": { ... }`, ajouter après `"task": { ... }` :

    ```json
    "createTaskModal": {
        "title": "Créer une tâche depuis ce mail",
        "submit": "Créer la tâche",
        "projectLabel": "Projet *",
        "projectPlaceholder": "Sélectionner un projet",
        "groupLabel": "Groupe (optionnel)",
        "groupPlaceholder": "Aucun groupe",
        "priorityLabel": "Priorité (optionnelle)",
        "priorityPlaceholder": "Aucune priorité",
        "titleHint": "Le titre sera rempli depuis le sujet du mail.",
        "descriptionHint": "La description sera remplie depuis le corps du mail."
    },
    "linkTaskModal": {
        "title": "Lier à une tâche existante",
        "submit": "Lier la tâche",
        "searchPlaceholder": "Rechercher une tâche par titre…",
        "projectFilter": "Filtrer par projet",
        "projectAll": "Tous les projets",
        "empty": "Aucune tâche correspondante.",
        "loading": "Recherche en cours…"
    },
    "pickerModal": {
        "title": "Lier un mail à cette tâche",
        "searchPlaceholder": "Rechercher un mail (sujet, expéditeur)…",
        "empty": "Aucun mail correspondant.",
        "loading": "Chargement des mails…",
        "submit": "Lier ce mail"
    },
    "taskTab": {
        "title": "Mails",
        "empty": "Aucun mail lié à cette tâche.",
        "linkButton": "Lier un mail",
        "openInMailer": "Ouvrir dans la messagerie",
        "unlinkConfirm": "Délier ce mail ?"
    }
    ```

- [ ] **Step 2 : Si `en.json` existe, ajouter les traductions anglaises**

    ```bash
    ls /home/r-dev/malio-dev/Lesstime/frontend/i18n/locales/
    ```

    Si `en.json` existe, ajouter dans le bloc `"mail"` :

    ```json
    "createTaskModal": {
        "title": "Create a task from this mail",
        "submit": "Create task",
        "projectLabel": "Project *",
        "projectPlaceholder": "Select a project",
        "groupLabel": "Group (optional)",
        "groupPlaceholder": "No group",
        "priorityLabel": "Priority (optional)",
        "priorityPlaceholder": "No priority",
        "titleHint": "Title will be filled from the mail subject.",
        "descriptionHint": "Description will be filled from the mail body."
    },
    "linkTaskModal": {
        "title": "Link to an existing task",
        "submit": "Link task",
        "searchPlaceholder": "Search a task by title…",
        "projectFilter": "Filter by project",
        "projectAll": "All projects",
        "empty": "No matching task.",
        "loading": "Searching…"
    },
    "pickerModal": {
        "title": "Link a mail to this task",
        "searchPlaceholder": "Search a mail (subject, sender)…",
        "empty": "No matching mail.",
        "loading": "Loading mails…",
        "submit": "Link this mail"
    },
    "taskTab": {
        "title": "Mails",
        "empty": "No mail linked to this task.",
        "linkButton": "Link a mail",
        "openInMailer": "Open in mailer",
        "unlinkConfirm": "Unlink this mail?"
    }
    ```

- [ ] **Step 3 : Valider la syntaxe JSON**

    ```bash
    node -e "JSON.parse(require('fs').readFileSync('/home/r-dev/malio-dev/Lesstime/frontend/i18n/locales/fr.json', 'utf8')); console.log('JSON OK')"
    ```

    Attendu : `JSON OK`. Corriger la virgule manquante si erreur.

- [ ] **Step 4 : Commit**

    ```bash
    git -C /home/r-dev/malio-dev/Lesstime add frontend/i18n/locales/fr.json frontend/i18n/locales/en.json 2>/dev/null; true
    git -C /home/r-dev/malio-dev/Lesstime commit -m "feat(mail) : clés i18n Phase 6 — createTaskModal, linkTaskModal, pickerModal, taskTab"
    ```

---

### Task 3 : Composant `MailCreateTaskModal.vue`

Modal pour créer une tâche depuis un mail. Le backend (`POST /api/mail/messages/{id}/create-task`) dérive automatiquement le titre (subject) et la description (body plain text) — le frontend passe uniquement l'affectation (projet, groupe, priorité).

Le composant charge les projets au `onMounted`, puis charge les groupes quand le projet est sélectionné, et les priorités une seule fois. La priorité est une string (en IRI ou slug selon le backend) — utiliser `<select>` natif (pas `MalioSelect`) car les values sont des strings ou null.

- [ ] **Step 1 : Créer `frontend/components/mail/MailCreateTaskModal.vue`**

    ```vue
    <script setup lang="ts">
    import type { MailMessageDetailDto } from '~/services/dto/mail'
    import type { Task } from '~/services/dto/task'
    import type { Project } from '~/services/dto/project'
    import type { TaskGroup } from '~/services/dto/task-group'
    import type { TaskPriority } from '~/services/dto/task-priority'
    import { useMailService } from '~/services/mail'
    import { useProjectService } from '~/services/projects'
    import { useTaskGroupService } from '~/services/task-groups'
    import { useTaskPriorityService } from '~/services/task-priorities'

    const props = defineProps<{
        /** v-model: true = modal ouvert */
        modelValue: boolean
        /** ID BDD du message source */
        messageId: number
        /** Détail du message (pour afficher sujet/expéditeur en lecture seule) */
        messageDetail: MailMessageDetailDto | null
    }>()

    const emit = defineEmits<{
        'update:modelValue': [value: boolean]
        /** Émis après création réussie — payload = tâche créée */
        created: [task: Task]
    }>()

    const { t } = useI18n()
    const mailService = useMailService()
    const projectService = useProjectService()
    const taskGroupService = useTaskGroupService()
    const priorityService = useTaskPriorityService()

    // ─── État formulaire ──────────────────────────────────────────────────────

    const projectId = ref<number | null>(null)
    const taskGroupId = ref<number | null>(null)
    const priorityId = ref<number | null>(null)
    const isSubmitting = ref(false)
    const touchedProject = ref(false)

    // ─── Données de référence ─────────────────────────────────────────────────

    const projects = ref<Project[]>([])
    const groups = ref<TaskGroup[]>([])
    const priorities = ref<TaskPriority[]>([])
    const loadingGroups = ref(false)

    const projectOptions = computed(() =>
        projects.value.map(p => ({ label: p.name, value: p.id }))
    )

    const groupOptions = computed(() => [
        { label: t('mail.createTaskModal.groupPlaceholder'), value: null },
        ...groups.value.filter(g => !g.archived).map(g => ({ label: g.title, value: g.id })),
    ])

    // ─── Chargement initial ───────────────────────────────────────────────────

    onMounted(async () => {
        const [projs, prios] = await Promise.all([
            projectService.getAll({ archived: false }),
            priorityService.getAll(),
        ])
        projects.value = projs
        priorities.value = prios
    })

    // Recharger les groupes quand le projet change
    watch(projectId, async (pid) => {
        taskGroupId.value = null
        groups.value = []
        if (!pid) return
        loadingGroups.value = true
        try {
            groups.value = await taskGroupService.getByProject(pid)
        } finally {
            loadingGroups.value = false
        }
    })

    // Reset formulaire à l'ouverture
    watch(() => props.modelValue, (open) => {
        if (open) {
            projectId.value = null
            taskGroupId.value = null
            priorityId.value = null
            touchedProject.value = false
        }
    })

    // ─── Actions ──────────────────────────────────────────────────────────────

    function close(): void {
        emit('update:modelValue', false)
    }

    async function handleSubmit(): Promise<void> {
        touchedProject.value = true
        if (!projectId.value) return

        isSubmitting.value = true
        try {
            const task = await mailService.createTaskFromMail(props.messageId, {
                projectId: projectId.value,
                taskGroupId: taskGroupId.value ?? undefined,
                priority: priorityId.value ? `/api/task_priorities/${priorityId.value}` : undefined,
            })
            emit('created', task)
            close()
        } finally {
            isSubmitting.value = false
        }
    }
    </script>

    <template>
        <Teleport v-if="modelValue" to="body">
            <Transition name="mail-modal" appear>
                <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
                    <!-- Backdrop -->
                    <div
                        class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
                        @click="close"
                    />

                    <!-- Modal -->
                    <div
                        class="relative z-10 w-full max-w-lg rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 overflow-hidden"
                        style="max-height: min(90vh, 640px)"
                    >
                        <!-- Header -->
                        <div class="flex items-center justify-between border-b border-neutral-100 bg-neutral-50/80 px-6 py-4">
                            <h2 class="text-base font-bold text-neutral-900">
                                {{ t('mail.createTaskModal.title') }}
                            </h2>
                            <MalioButtonIcon
                                icon="mdi:close"
                                aria-label="Fermer"
                                variant="ghost"
                                icon-size="20"
                                @click="close"
                            />
                        </div>

                        <!-- Corps -->
                        <div class="overflow-y-auto px-6 py-5 space-y-5">
                            <!-- Info mail source (lecture seule) -->
                            <div
                                v-if="messageDetail"
                                class="rounded-lg border border-neutral-200 bg-neutral-50 px-4 py-3 text-sm"
                            >
                                <p class="font-medium text-neutral-800 truncate">
                                    {{ messageDetail.header.subject ?? t('mail.noSubject') }}
                                </p>
                                <p class="mt-0.5 text-xs text-neutral-500 truncate">
                                    {{ messageDetail.header.fromName ?? messageDetail.header.fromEmail }}
                                </p>
                                <p class="mt-2 text-xs text-neutral-400 italic">
                                    {{ t('mail.createTaskModal.titleHint') }}
                                </p>
                                <p class="text-xs text-neutral-400 italic">
                                    {{ t('mail.createTaskModal.descriptionHint') }}
                                </p>
                            </div>

                            <!-- Sélection projet -->
                            <div>
                                <MalioSelect
                                    v-model="projectId"
                                    :options="projectOptions"
                                    :label="t('mail.createTaskModal.projectLabel')"
                                    :empty-option-label="t('mail.createTaskModal.projectPlaceholder')"
                                    min-width="w-full"
                                />
                                <p
                                    v-if="touchedProject && !projectId"
                                    class="mt-1 text-xs text-red-500"
                                >
                                    {{ t('mail.createTaskModal.projectLabel').replace(' *', '') }} requis
                                </p>
                            </div>

                            <!-- Sélection groupe (optionnel, chargé après projet) -->
                            <div v-if="projectId">
                                <MalioSelect
                                    v-model="taskGroupId"
                                    :options="groupOptions"
                                    :label="t('mail.createTaskModal.groupLabel')"
                                    :empty-option-label="t('mail.createTaskModal.groupPlaceholder')"
                                    min-width="w-full"
                                    :disabled="loadingGroups"
                                />
                            </div>

                            <!-- Sélection priorité (optionnelle) — MalioSelect car les values sont number | null -->
                            <div>
                                <MalioSelect
                                    v-model="priorityId"
                                    :options="[
                                        { label: t('mail.createTaskModal.priorityPlaceholder'), value: null },
                                        ...priorities.map(p => ({ label: p.label, value: p.id }))
                                    ]"
                                    :label="t('mail.createTaskModal.priorityLabel')"
                                    :empty-option-label="t('mail.createTaskModal.priorityPlaceholder')"
                                    min-width="w-full"
                                />
                            </div>
                        </div>

                        <!-- Footer -->
                        <div class="flex justify-end gap-3 border-t border-neutral-100 px-6 py-4">
                            <MalioButton
                                variant="tertiary"
                                label="Annuler"
                                button-class="w-auto px-4"
                                @click="close"
                            />
                            <MalioButton
                                :label="t('mail.createTaskModal.submit')"
                                button-class="w-auto px-6"
                                :disabled="isSubmitting"
                                @click="handleSubmit"
                            />
                        </div>
                    </div>
                </div>
            </Transition>
        </Teleport>
    </template>

    <style scoped>
    .mail-modal-enter-active,
    .mail-modal-leave-active {
        transition: opacity 0.2s ease;
    }

    .mail-modal-enter-active > div:last-child,
    .mail-modal-leave-active > div:last-child {
        transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease;
    }

    .mail-modal-enter-from,
    .mail-modal-leave-to {
        opacity: 0;
    }

    .mail-modal-enter-from > div:last-child {
        transform: scale(0.95) translateY(8px);
        opacity: 0;
    }
    </style>
    ```

    Points clés :
    - La priorité passe en IRI `/api/task_priorities/{id}` dans le payload — conforme au pattern `TaskModal.vue`.
    - `taskGroupId: undefined` (pas `null`) si non sélectionné, pour que le backend ignore le champ (le type `MailCreateTaskInput` le déclare optionnel).
    - Le backdrop click ferme la modal (même pattern que `TaskModal.vue`).
    - Le composant ne pré-remplit PAS le titre/description : c'est le backend qui le fait depuis le mail, le frontend ne gère que les affectations.

- [ ] **Step 2 : Vérification TypeScript**

    ```bash
    cd /home/r-dev/malio-dev/Lesstime/frontend && npx tsc --noEmit 2>&1 | grep "MailCreateTaskModal" | head -10
    ```

    Attendu : aucune erreur.

- [ ] **Step 3 : Commit**

    ```bash
    git -C /home/r-dev/malio-dev/Lesstime add frontend/components/mail/MailCreateTaskModal.vue
    git -C /home/r-dev/malio-dev/Lesstime commit -m "feat(mail) : MailCreateTaskModal — picker projet/groupe/priorité, appel createTaskFromMail"
    ```

---

### Task 4 : Composant `MailLinkTaskModal.vue`

Modal pour lier un mail à une tâche existante. Autocomplete en input + dropdown filtré. Debounce 300ms sur la recherche. Filtre optionnel par projet. Statut archivé exclu (paramètre `archived: false`).

Le service `useTaskService().getFiltered()` accepte des params libres — utiliser `{ title: searchTerm, archived: false }` + éventuellement `project: '/api/projects/{id}'`.

- [ ] **Step 1 : Créer `frontend/components/mail/MailLinkTaskModal.vue`**

    ```vue
    <script setup lang="ts">
    import type { Task } from '~/services/dto/task'
    import type { Project } from '~/services/dto/project'
    import { useMailService } from '~/services/mail'
    import { useTaskService } from '~/services/tasks'
    import { useProjectService } from '~/services/projects'

    const props = defineProps<{
        modelValue: boolean
        /** ID BDD du message à lier */
        messageId: number
    }>()

    const emit = defineEmits<{
        'update:modelValue': [value: boolean]
        /** Émis après liaison réussie — payload = id de la tâche liée */
        linked: [taskId: number]
    }>()

    const { t } = useI18n()
    const mailService = useMailService()
    const taskService = useTaskService()
    const projectService = useProjectService()

    // ─── État recherche ───────────────────────────────────────────────────────

    const searchQuery = ref('')
    const filterProjectId = ref<number | null>(null)
    const results = ref<Task[]>([])
    const selectedTask = ref<Task | null>(null)
    const isLoading = ref(false)
    const isSubmitting = ref(false)

    // ─── Projets pour le filtre ───────────────────────────────────────────────

    const projects = ref<Project[]>([])

    const projectFilterOptions = computed(() => [
        { label: t('mail.linkTaskModal.projectAll'), value: null },
        ...projects.value.map(p => ({ label: p.name, value: p.id })),
    ])

    onMounted(async () => {
        projects.value = await projectService.getAll({ archived: false })
    })

    // ─── Debounce recherche ───────────────────────────────────────────────────

    let debounceTimer: ReturnType<typeof setTimeout> | null = null

    watch([searchQuery, filterProjectId], () => {
        selectedTask.value = null
        if (debounceTimer) clearTimeout(debounceTimer)
        debounceTimer = setTimeout(() => {
            void runSearch()
        }, 300)
    })

    async function runSearch(): Promise<void> {
        const q = searchQuery.value.trim()
        if (!q && !filterProjectId.value) {
            results.value = []
            return
        }
        isLoading.value = true
        try {
            const params: Record<string, string | number | boolean | string[]> = {
                archived: false,
            }
            if (q) params['title'] = q
            if (filterProjectId.value) params['project'] = `/api/projects/${filterProjectId.value}`
            results.value = await taskService.getFiltered(params)
        } finally {
            isLoading.value = false
        }
    }

    // ─── Reset à l'ouverture ──────────────────────────────────────────────────

    watch(() => props.modelValue, (open) => {
        if (open) {
            searchQuery.value = ''
            filterProjectId.value = null
            results.value = []
            selectedTask.value = null
        }
    })

    onBeforeUnmount(() => {
        if (debounceTimer) clearTimeout(debounceTimer)
    })

    // ─── Actions ──────────────────────────────────────────────────────────────

    function close(): void {
        emit('update:modelValue', false)
    }

    function selectTask(task: Task): void {
        selectedTask.value = task
    }

    async function handleSubmit(): Promise<void> {
        if (!selectedTask.value) return
        isSubmitting.value = true
        try {
            await mailService.linkTask(props.messageId, selectedTask.value.id)
            emit('linked', selectedTask.value.id)
            close()
        } finally {
            isSubmitting.value = false
        }
    }
    </script>

    <template>
        <Teleport v-if="modelValue" to="body">
            <Transition name="mail-modal" appear>
                <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
                    <div
                        class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
                        @click="close"
                    />

                    <div
                        class="relative z-10 w-full max-w-lg rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 overflow-hidden"
                        style="max-height: min(90vh, 640px)"
                    >
                        <!-- Header -->
                        <div class="flex items-center justify-between border-b border-neutral-100 bg-neutral-50/80 px-6 py-4">
                            <h2 class="text-base font-bold text-neutral-900">
                                {{ t('mail.linkTaskModal.title') }}
                            </h2>
                            <MalioButtonIcon
                                icon="mdi:close"
                                aria-label="Fermer"
                                variant="ghost"
                                icon-size="20"
                                @click="close"
                            />
                        </div>

                        <!-- Corps -->
                        <div class="overflow-y-auto px-6 py-5 space-y-4">
                            <!-- Filtre projet -->
                            <MalioSelect
                                v-model="filterProjectId"
                                :options="projectFilterOptions"
                                :label="t('mail.linkTaskModal.projectFilter')"
                                :empty-option-label="t('mail.linkTaskModal.projectAll')"
                                min-width="w-full"
                            />

                            <!-- Recherche tâche -->
                            <div>
                                <label class="mb-1 block text-sm font-medium text-neutral-700">
                                    {{ t('mail.linkTaskModal.title') }}
                                </label>
                                <input
                                    v-model="searchQuery"
                                    type="text"
                                    :placeholder="t('mail.linkTaskModal.searchPlaceholder')"
                                    class="w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
                                />
                            </div>

                            <!-- Résultats -->
                            <div class="max-h-64 overflow-y-auto rounded-md border border-neutral-200">
                                <!-- Chargement -->
                                <div
                                    v-if="isLoading"
                                    class="flex items-center justify-center py-6 text-sm text-neutral-400"
                                >
                                    <Icon name="material-symbols:progress-activity" size="18" class="mr-2 animate-spin" />
                                    {{ t('mail.linkTaskModal.loading') }}
                                </div>

                                <!-- Vide -->
                                <div
                                    v-else-if="!isLoading && results.length === 0 && (searchQuery.trim() || filterProjectId)"
                                    class="py-6 text-center text-sm text-neutral-400 italic"
                                >
                                    {{ t('mail.linkTaskModal.empty') }}
                                </div>

                                <!-- Liste résultats -->
                                <button
                                    v-for="task in results"
                                    :key="task.id"
                                    type="button"
                                    class="flex w-full items-start gap-3 px-4 py-3 text-left text-sm transition-colors hover:bg-neutral-50"
                                    :class="selectedTask?.id === task.id
                                        ? 'bg-primary-50 border-l-2 border-primary-500'
                                        : 'border-l-2 border-transparent'"
                                    @click="selectTask(task)"
                                >
                                    <Icon
                                        name="material-symbols:task-outline"
                                        size="16"
                                        class="mt-0.5 flex-shrink-0 text-neutral-400"
                                    />
                                    <div class="min-w-0 flex-1">
                                        <p class="truncate font-medium text-neutral-800">
                                            {{ task.title }}
                                        </p>
                                        <p
                                            v-if="task.project"
                                            class="truncate text-xs text-neutral-500"
                                        >
                                            {{ task.project.name }}
                                            <span v-if="task.project.code && task.number">
                                                — {{ task.project.code }}-{{ task.number }}
                                            </span>
                                        </p>
                                    </div>
                                    <Icon
                                        v-if="selectedTask?.id === task.id"
                                        name="material-symbols:check-circle"
                                        size="16"
                                        class="flex-shrink-0 text-primary-500"
                                    />
                                </button>
                            </div>
                        </div>

                        <!-- Footer -->
                        <div class="flex justify-end gap-3 border-t border-neutral-100 px-6 py-4">
                            <MalioButton
                                variant="tertiary"
                                label="Annuler"
                                button-class="w-auto px-4"
                                @click="close"
                            />
                            <MalioButton
                                :label="t('mail.linkTaskModal.submit')"
                                button-class="w-auto px-6"
                                :disabled="!selectedTask || isSubmitting"
                                @click="handleSubmit"
                            />
                        </div>
                    </div>
                </div>
            </Transition>
        </Teleport>
    </template>

    <style scoped>
    .mail-modal-enter-active,
    .mail-modal-leave-active {
        transition: opacity 0.2s ease;
    }

    .mail-modal-enter-active > div:last-child,
    .mail-modal-leave-active > div:last-child {
        transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease;
    }

    .mail-modal-enter-from,
    .mail-modal-leave-to {
        opacity: 0;
    }

    .mail-modal-enter-from > div:last-child {
        transform: scale(0.95) translateY(8px);
        opacity: 0;
    }
    </style>
    ```

    Points clés :
    - Debounce 300ms — le timer est nettoyé dans `onBeforeUnmount` pour éviter les fuites.
    - Sélection mono-tâche (click toggle sélection visuelle, un seul peut être sélectionné à la fois).
    - Le bouton "Lier" est désactivé si aucune tâche sélectionnée ou si soumission en cours.
    - La recherche ne se déclenche PAS si query vide ET pas de filtre projet (évite de charger toutes les tâches au montage).

- [ ] **Step 2 : Vérification TypeScript**

    ```bash
    cd /home/r-dev/malio-dev/Lesstime/frontend && npx tsc --noEmit 2>&1 | grep "MailLinkTaskModal" | head -10
    ```

    Attendu : aucune erreur.

- [ ] **Step 3 : Commit**

    ```bash
    git -C /home/r-dev/malio-dev/Lesstime add frontend/components/mail/MailLinkTaskModal.vue
    git -C /home/r-dev/malio-dev/Lesstime commit -m "feat(mail) : MailLinkTaskModal — autocomplete tâches, filtre projet, debounce 300ms"
    ```

---

### Task 5 : Composant `MailPickerModal.vue`

Modal utilisé depuis l'onglet "Mails" de `TaskModal.vue` pour lier un mail existant à la tâche ouverte. Charge les messages récents depuis le dossier INBOX (ou le dernier dossier sélectionné dans le store), affiche une liste filtrable, appelle `mailService.linkTask(messageId, taskId)`.

- [ ] **Step 1 : Créer `frontend/components/mail/MailPickerModal.vue`**

    ```vue
    <script setup lang="ts">
    import type { MailMessageHeaderDto } from '~/services/dto/mail'
    import { useMailService } from '~/services/mail'
    import { useMailStore } from '~/stores/mail'

    const props = defineProps<{
        modelValue: boolean
        /** ID de la tâche cible (destinataire du lien) */
        taskId: number
    }>()

    const emit = defineEmits<{
        'update:modelValue': [value: boolean]
        /** Émis après liaison réussie — payload = id du message lié */
        linked: [messageId: number]
    }>()

    const { t } = useI18n()
    const mailService = useMailService()
    const mailStore = useMailStore()

    // ─── État ─────────────────────────────────────────────────────────────────

    const searchQuery = ref('')
    const allMessages = ref<MailMessageHeaderDto[]>([])
    const selectedMessage = ref<MailMessageHeaderDto | null>(null)
    const isLoading = ref(false)
    const isSubmitting = ref(false)

    // ─── Filtrage local (pas d'appel API par frappe — les messages sont déjà chargés) ──

    const filteredMessages = computed(() => {
        const q = searchQuery.value.toLowerCase().trim()
        if (!q) return allMessages.value
        return allMessages.value.filter(
            (m) =>
                (m.subject ?? '').toLowerCase().includes(q) ||
                (m.fromName ?? '').toLowerCase().includes(q) ||
                (m.fromEmail ?? '').toLowerCase().includes(q),
        )
    })

    // ─── Chargement à l'ouverture ─────────────────────────────────────────────

    watch(() => props.modelValue, async (open) => {
        if (!open) return
        searchQuery.value = ''
        selectedMessage.value = null
        isLoading.value = true
        try {
            // Utiliser les messages déjà dans le store si disponibles (dossier courant)
            // Sinon charger depuis INBOX
            const folderPath = mailStore.selectedFolderPath ?? 'INBOX'
            const page = await mailService.listMessages(folderPath, undefined, 50)
            allMessages.value = page.items
        } finally {
            isLoading.value = false
        }
    })

    // ─── Actions ──────────────────────────────────────────────────────────────

    function close(): void {
        emit('update:modelValue', false)
    }

    function selectMessage(msg: MailMessageHeaderDto): void {
        selectedMessage.value = msg
    }

    async function handleSubmit(): Promise<void> {
        if (!selectedMessage.value) return
        isSubmitting.value = true
        try {
            await mailService.linkTask(selectedMessage.value.id, props.taskId)
            emit('linked', selectedMessage.value.id)
            close()
        } finally {
            isSubmitting.value = false
        }
    }

    // ─── Formatage ────────────────────────────────────────────────────────────

    function formatDate(iso: string | null): string {
        if (!iso) return ''
        return new Date(iso).toLocaleDateString('fr', {
            day: '2-digit',
            month: 'short',
            year: 'numeric',
        })
    }
    </script>

    <template>
        <Teleport v-if="modelValue" to="body">
            <Transition name="mail-modal" appear>
                <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
                    <div
                        class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
                        @click="close"
                    />

                    <div
                        class="relative z-10 w-full max-w-lg rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 overflow-hidden"
                        style="max-height: min(90vh, 640px)"
                    >
                        <!-- Header -->
                        <div class="flex items-center justify-between border-b border-neutral-100 bg-neutral-50/80 px-6 py-4">
                            <h2 class="text-base font-bold text-neutral-900">
                                {{ t('mail.pickerModal.title') }}
                            </h2>
                            <MalioButtonIcon
                                icon="mdi:close"
                                aria-label="Fermer"
                                variant="ghost"
                                icon-size="20"
                                @click="close"
                            />
                        </div>

                        <!-- Corps -->
                        <div class="overflow-y-auto px-6 py-5 space-y-4">
                            <!-- Recherche locale -->
                            <input
                                v-model="searchQuery"
                                type="text"
                                :placeholder="t('mail.pickerModal.searchPlaceholder')"
                                class="w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
                            />

                            <!-- Résultats -->
                            <div class="max-h-80 overflow-y-auto rounded-md border border-neutral-200 divide-y divide-neutral-100">
                                <!-- Chargement -->
                                <div
                                    v-if="isLoading"
                                    class="flex items-center justify-center py-8 text-sm text-neutral-400"
                                >
                                    <Icon name="material-symbols:progress-activity" size="18" class="mr-2 animate-spin" />
                                    {{ t('mail.pickerModal.loading') }}
                                </div>

                                <!-- Vide -->
                                <div
                                    v-else-if="filteredMessages.length === 0"
                                    class="py-8 text-center text-sm text-neutral-400 italic"
                                >
                                    {{ t('mail.pickerModal.empty') }}
                                </div>

                                <!-- Liste -->
                                <button
                                    v-for="msg in filteredMessages"
                                    :key="msg.id"
                                    type="button"
                                    class="flex w-full items-start gap-3 px-4 py-3 text-left text-sm transition-colors hover:bg-neutral-50"
                                    :class="selectedMessage?.id === msg.id
                                        ? 'bg-primary-50 border-l-2 border-primary-500'
                                        : 'border-l-2 border-transparent'"
                                    @click="selectMessage(msg)"
                                >
                                    <Icon
                                        name="material-symbols:mail-outline"
                                        size="16"
                                        class="mt-0.5 flex-shrink-0 text-neutral-400"
                                    />
                                    <div class="min-w-0 flex-1">
                                        <p class="truncate font-medium text-neutral-800">
                                            {{ msg.subject ?? t('mail.noSubject') }}
                                        </p>
                                        <p class="flex items-center gap-2 text-xs text-neutral-500">
                                            <span class="truncate">{{ msg.fromName ?? msg.fromEmail }}</span>
                                            <span class="flex-shrink-0">·</span>
                                            <span class="flex-shrink-0">{{ formatDate(msg.sentAt ?? msg.receivedAt) }}</span>
                                        </p>
                                    </div>
                                    <Icon
                                        v-if="selectedMessage?.id === msg.id"
                                        name="material-symbols:check-circle"
                                        size="16"
                                        class="flex-shrink-0 text-primary-500"
                                    />
                                </button>
                            </div>
                        </div>

                        <!-- Footer -->
                        <div class="flex justify-end gap-3 border-t border-neutral-100 px-6 py-4">
                            <MalioButton
                                variant="tertiary"
                                label="Annuler"
                                button-class="w-auto px-4"
                                @click="close"
                            />
                            <MalioButton
                                :label="t('mail.pickerModal.submit')"
                                button-class="w-auto px-6"
                                :disabled="!selectedMessage || isSubmitting"
                                @click="handleSubmit"
                            />
                        </div>
                    </div>
                </div>
            </Transition>
        </Teleport>
    </template>

    <style scoped>
    .mail-modal-enter-active,
    .mail-modal-leave-active {
        transition: opacity 0.2s ease;
    }

    .mail-modal-enter-active > div:last-child,
    .mail-modal-leave-active > div:last-child {
        transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease;
    }

    .mail-modal-enter-from,
    .mail-modal-leave-to {
        opacity: 0;
    }

    .mail-modal-enter-from > div:last-child {
        transform: scale(0.95) translateY(8px);
        opacity: 0;
    }
    </style>
    ```

    Points clés :
    - Le filtrage est **local** (sur les 50 messages chargés) — pas de debounce nécessaire, réactif instantané.
    - La note sur `linkTask(messageId, taskId)` : l'ordre des arguments est `(mailId, taskId)` dans `useMailService`. Ici on passe `selectedMessage.value.id` (mailId) et `props.taskId`.
    - Utilise `mailStore.selectedFolderPath` pour charger depuis le dossier courant (meilleure UX si l'utilisateur vient de la page mail). Fallback sur `INBOX`.

- [ ] **Step 2 : Vérification TypeScript**

    ```bash
    cd /home/r-dev/malio-dev/Lesstime/frontend && npx tsc --noEmit 2>&1 | grep "MailPickerModal" | head -10
    ```

    Attendu : aucune erreur.

- [ ] **Step 3 : Commit**

    ```bash
    git -C /home/r-dev/malio-dev/Lesstime add frontend/components/mail/MailPickerModal.vue
    git -C /home/r-dev/malio-dev/Lesstime commit -m "feat(mail) : MailPickerModal — sélection mail depuis dossier courant, liaison taskId"
    ```

---

### Task 6 : Onglet "Mails" dans `TaskModal.vue`

Ajouter un troisième onglet `mails` dans `TaskModal.vue`. Cet onglet est visible uniquement pour les utilisateurs non-ROLE_CLIENT (check `authStore.user?.roles`). Il charge les mails liés via `mailService.listMailsForTask(task.id)` à l'ouverture de l'onglet, affiche une liste compacte cliquable, propose un bouton "Lier un mail" qui ouvre `MailPickerModal`.

**Important :** `TaskModal.vue` est un composant lourd. Modifier avec précision — ne pas altérer les onglets `details` et `planning` existants.

- [ ] **Step 1 : Lire le fichier actuel pour localiser les points de modification**

    Lire `frontend/components/task/TaskModal.vue` pour identifier :
    1. La ligne `const activeTab = ref<'details' | 'planning'>('details')` — à étendre au type union.
    2. La nav `<nav class="flex gap-6">` et sa boucle `v-for="tab in ['details', 'planning']"` — à étendre.
    3. La section `<div v-show="activeTab === 'planning'">` — après laquelle insérer l'onglet mails.

- [ ] **Step 2 : Modifier le type `activeTab` et ajouter les imports**

    Dans la section `<script setup lang="ts">` (après le bloc de la section `<template>`), modifier la ligne :

    ```typescript
    const activeTab = ref<'details' | 'planning'>('details')
    ```

    en :

    ```typescript
    const activeTab = ref<'details' | 'planning' | 'mails'>('details')
    ```

    Ajouter les imports nécessaires (après les imports existants) :

    ```typescript
    import { useMailService } from '~/services/mail'
    import type { MailMessageHeaderDto } from '~/services/dto/mail'
    ```

    Ajouter les variables d'état pour l'onglet mails (après `const activeTab = ref<...>`) :

    ```typescript
    const mailService = useMailService()
    const linkedMails = ref<MailMessageHeaderDto[]>([])
    const mailsLoading = ref(false)
    const showMailPickerModal = ref(false)

    const isMailUser = computed(() =>
        !(authStore.user?.roles?.includes('ROLE_CLIENT') === true
        && authStore.user?.roles?.includes('ROLE_ADMIN') !== true)
    )
    ```

    Ajouter la fonction de chargement des mails :

    ```typescript
    async function loadLinkedMails(): Promise<void> {
        if (!props.task || !isMailUser.value) return
        mailsLoading.value = true
        try {
            linkedMails.value = await mailService.listMailsForTask(props.task.id)
        } catch {
            linkedMails.value = []
        } finally {
            mailsLoading.value = false
        }
    }
    ```

    Étendre le watcher existant `watch(() => props.modelValue, ...)` pour charger les mails quand l'onglet mails est actif, OU ajouter un watcher sur `activeTab` :

    ```typescript
    watch(activeTab, async (tab) => {
        if (tab === 'mails' && props.task) {
            await loadLinkedMails()
        }
    })
    ```

    Ajouter le handler de liaison réussie (appelé par `MailPickerModal`) :

    ```typescript
    async function handleMailLinked(): Promise<void> {
        showMailPickerModal.value = false
        await loadLinkedMails()
    }
    ```

    Ajouter la fonction de formatage de date (utilisée dans l'onglet) :

    ```typescript
    function formatMailDate(iso: string | null): string {
        if (!iso) return ''
        return new Date(iso).toLocaleDateString('fr', {
            day: '2-digit',
            month: 'short',
        })
    }
    ```

- [ ] **Step 3 : Modifier le template — nav des onglets**

    Remplacer la boucle d'onglets :

    ```html
    <button
        v-for="tab in ['details', 'planning']"
        :key="tab"
        type="button"
        class="px-1 pb-3 text-sm font-semibold transition"
        :class="activeTab === tab
            ? 'border-b-2 border-primary-500 text-primary-500'
            : 'text-neutral-500 hover:text-neutral-700'"
        @click="activeTab = tab as 'details' | 'planning'"
    >
        {{ $t(`tasks.${tab}Tab`) }}
    </button>
    ```

    par :

    ```html
    <button
        v-for="tab in availableTabs"
        :key="tab"
        type="button"
        class="px-1 pb-3 text-sm font-semibold transition"
        :class="activeTab === tab
            ? 'border-b-2 border-primary-500 text-primary-500'
            : 'text-neutral-500 hover:text-neutral-700'"
        @click="activeTab = tab as 'details' | 'planning' | 'mails'"
    >
        {{ tab === 'mails' ? $t('mail.taskTab.title') : $t(`tasks.${tab}Tab`) }}
    </button>
    ```

    Et ajouter le computed `availableTabs` dans le script :

    ```typescript
    const availableTabs = computed(() => {
        const base: Array<'details' | 'planning' | 'mails'> = ['details', 'planning']
        if (isEditing.value && isMailUser.value) base.push('mails')
        return base
    })
    ```

    Note : l'onglet "Mails" n'apparaît qu'en mode édition (tâche existante avec ID) et uniquement pour les non-ROLE_CLIENT.

- [ ] **Step 4 : Ajouter le contenu de l'onglet mails dans le template**

    Après la balise fermante `</div>` du `<div v-show="activeTab === 'planning'">`, ajouter :

    ```html
    <!-- Onglet Mails -->
    <div v-show="activeTab === 'mails'" class="space-y-4">
        <!-- Chargement -->
        <div v-if="mailsLoading" class="flex items-center justify-center py-8">
            <Icon name="material-symbols:progress-activity" size="24" class="animate-spin text-neutral-400" />
        </div>

        <!-- Vide -->
        <div
            v-else-if="linkedMails.length === 0"
            class="flex flex-col items-center justify-center gap-3 py-8 text-center"
        >
            <Icon name="material-symbols:mail-outline" size="32" class="text-neutral-300" />
            <p class="text-sm text-neutral-400 italic">{{ $t('mail.taskTab.empty') }}</p>
        </div>

        <!-- Liste mails liés -->
        <div v-else class="divide-y divide-neutral-100 rounded-lg border border-neutral-200">
            <NuxtLink
                v-for="mail in linkedMails"
                :key="mail.id"
                :to="`/mail?messageId=${mail.id}`"
                class="flex items-start gap-3 px-4 py-3 text-sm transition-colors hover:bg-neutral-50"
                :title="$t('mail.taskTab.openInMailer')"
            >
                <Icon
                    name="material-symbols:mail-outline"
                    size="16"
                    class="mt-0.5 flex-shrink-0 text-neutral-400"
                />
                <div class="min-w-0 flex-1">
                    <p class="truncate font-medium text-neutral-800">
                        {{ mail.subject ?? $t('mail.noSubject') }}
                    </p>
                    <p class="flex items-center gap-2 text-xs text-neutral-500">
                        <span class="truncate">{{ mail.fromName ?? mail.fromEmail }}</span>
                        <span>·</span>
                        <span class="flex-shrink-0">{{ formatMailDate(mail.sentAt ?? mail.receivedAt) }}</span>
                    </p>
                </div>
                <Icon
                    name="material-symbols:open-in-new"
                    size="14"
                    class="flex-shrink-0 text-neutral-300"
                />
            </NuxtLink>
        </div>

        <!-- Bouton lier un mail -->
        <div class="pt-2">
            <MalioButton
                :label="$t('mail.taskTab.linkButton')"
                variant="secondary"
                icon-name="material-symbols:link"
                icon-position="left"
                :icon-size="14"
                button-class="w-auto"
                @click="showMailPickerModal = true"
            />
        </div>

        <!-- Modal picker mail -->
        <MailPickerModal
            v-if="task"
            v-model="showMailPickerModal"
            :task-id="task.id"
            @linked="handleMailLinked"
        />
    </div>
    ```

- [ ] **Step 5 : Reset de l'onglet actif à l'ouverture**

    Dans le watcher existant `watch(() => props.modelValue, async (open) => { if (open) { activeTab.value = 'details' ... } })`, l'onglet est déjà resetté à `'details'` — vérifier que c'est bien le cas (ne pas modifier cette ligne).

    Ajouter le reset de `linkedMails` :

    ```typescript
    // À l'intérieur du bloc `if (open) {`, après `activeTab.value = 'details'`
    linkedMails.value = []
    ```

- [ ] **Step 6 : Vérification TypeScript**

    ```bash
    cd /home/r-dev/malio-dev/Lesstime/frontend && npx tsc --noEmit 2>&1 | grep -E "TaskModal|mail" | head -20
    ```

    Attendu : aucune nouvelle erreur.

- [ ] **Step 7 : Commit**

    ```bash
    git -C /home/r-dev/malio-dev/Lesstime add frontend/components/task/TaskModal.vue
    git -C /home/r-dev/malio-dev/Lesstime commit -m "feat(mail) : onglet Mails dans TaskModal — liste mails liés, bouton lier, MailPickerModal"
    ```

---

### Task 7 : Brancher les handlers dans `pages/mail.vue`

Remplacer les placeholders `console.warn` par l'ouverture des modals. Ajouter les refs `showCreateTaskModal` et `showLinkTaskModal` avec les states associés.

- [ ] **Step 1 : Modifier `frontend/pages/mail.vue`**

    Dans la section `<script setup lang="ts">`, ajouter les imports :

    ```typescript
    import type { Task } from '~/services/dto/task'
    ```

    Ajouter les refs d'état des modals (après les handlers existants) :

    ```typescript
    // ─── Modals Phase 6 ────────────────────────────────────────────────────────

    const showCreateTaskModal = ref(false)
    const showLinkTaskModal = ref(false)
    const activeMailIdForModal = ref<number | null>(null)
    ```

    Remplacer les fonctions placeholder :

    ```typescript
    // Avant (à supprimer) :
    function handleCreateTask(mailId: number): void {
        console.warn('[mail] handleCreateTask mailId=', mailId, '— modal à implémenter en Phase 6')
    }

    function handleLinkTask(mailId: number): void {
        console.warn('[mail] handleLinkTask mailId=', mailId, '— modal à implémenter en Phase 6')
    }
    ```

    ```typescript
    // Après (à insérer à la place) :
    function handleCreateTask(mailId: number): void {
        activeMailIdForModal.value = mailId
        showCreateTaskModal.value = true
    }

    function handleLinkTask(mailId: number): void {
        activeMailIdForModal.value = mailId
        showLinkTaskModal.value = true
    }

    function handleTaskCreated(task: Task): void {
        showCreateTaskModal.value = false
        // La tâche est créée et liée côté backend — pas d'action supplémentaire côté page mail
        // Le toast de succès est déjà géré par useMailService.createTaskFromMail (toastSuccessKey)
    }

    function handleTaskLinked(taskId: number): void {
        showLinkTaskModal.value = false
        // Idem — toast géré par useMailService.linkTask
    }
    ```

    Dans le `<template>`, ajouter les modals juste avant la balise `</div>` de fermeture principale :

    ```html
    <!-- Modal créer tâche depuis mail -->
    <MailCreateTaskModal
        v-if="activeMailIdForModal !== null"
        v-model="showCreateTaskModal"
        :message-id="activeMailIdForModal"
        :message-detail="selectedMessageDetail"
        @created="handleTaskCreated"
    />

    <!-- Modal lier mail à tâche -->
    <MailLinkTaskModal
        v-if="activeMailIdForModal !== null"
        v-model="showLinkTaskModal"
        :message-id="activeMailIdForModal"
        @linked="handleTaskLinked"
    />
    ```

- [ ] **Step 2 : Vérification TypeScript**

    ```bash
    cd /home/r-dev/malio-dev/Lesstime/frontend && npx tsc --noEmit 2>&1 | grep "mail.vue" | head -10
    ```

    Attendu : aucune erreur.

- [ ] **Step 3 : Commit**

    ```bash
    git -C /home/r-dev/malio-dev/Lesstime add frontend/pages/mail.vue
    git -C /home/r-dev/malio-dev/Lesstime commit -m "feat(mail) : pages/mail.vue — branche handlers Phase 6 (MailCreateTaskModal + MailLinkTaskModal)"
    ```

---

### Task 8 : Validation TypeScript globale + tests manuels end-to-end

- [ ] **Step 1 : TypeScript strict — zéro erreur Phase 6**

    ```bash
    cd /home/r-dev/malio-dev/Lesstime/frontend && npx tsc --noEmit 2>&1
    ```

    Attendu : sortie vide. Erreurs fréquentes à anticiper :

    - `Type '"mails"' is not assignable to type '"details" | "planning"'` → vérifier que le type union du ref est bien `'details' | 'planning' | 'mails'` ET que le cast `as 'details' | 'planning' | 'mails'` est appliqué dans le handler click de la nav.
    - `Property 'id' does not exist on type 'Task | null'` → le `v-if="task"` dans le template assure que `task` est non-null dans la portée — TypeScript peut ne pas l'inférer dans le script ; utiliser `props.task?.id` ou un guard explicite.
    - `Cannot find name 'MailPickerModal'` → Nuxt auto-importe les composants de `components/` — vérifier le nom de fichier (PascalCase, chemin `components/mail/MailPickerModal.vue`).
    - `Module '"~/services/mail"' has no exported member 'useMailService'` → vérifier l'import dans `TaskModal.vue` ; le service est un named export.

- [ ] **Step 2 : Démarrer le serveur dev**

    ```bash
    cd /home/r-dev/malio-dev/Lesstime && make dev-nuxt
    ```

    Attendu : `Nuxt ready` sur `http://localhost:3002`. Aucune erreur de compilation Nuxt.

- [ ] **Step 3 : Workflow 1 — Créer une tâche depuis un mail**

    - [ ] Se connecter avec `alice` / `alice` (ROLE_USER)
    - [ ] Naviguer vers `http://localhost:3002/mail`
    - [ ] Sélectionner un dossier et un message (si backend Phase 3 dispo) ou forcer via DevTools (injecter `selectedMessageDetail` dans le store Pinia)
    - [ ] Cliquer "Créer une tâche" dans `MailMessageViewer`
    - [ ] Vérifier : `MailCreateTaskModal` s'ouvre, affiche le sujet + expéditeur en lecture seule
    - [ ] Sélectionner un projet (ex : le projet SIRH des fixtures)
    - [ ] Optionnel : sélectionner un groupe
    - [ ] Cliquer "Créer la tâche"
    - [ ] Vérifier : toast succès "Tâche créée depuis le mail." + modal se ferme
    - [ ] Naviguer vers le projet correspondant → la nouvelle tâche apparaît avec titre = subject du mail
    - [ ] Ouvrir la tâche → onglet "Mails" → le mail source apparaît dans la liste

- [ ] **Step 4 : Workflow 2 — Lier une tâche existante à un mail**

    - [ ] Sur la page `/mail`, sélectionner un message
    - [ ] Cliquer "Lier à une tâche" dans `MailMessageViewer`
    - [ ] Vérifier : `MailLinkTaskModal` s'ouvre
    - [ ] Taper quelques lettres dans la recherche → les tâches s'affichent avec debounce
    - [ ] Sélectionner une tâche (ex : "Réunion de suivi hebdomadaire" depuis les fixtures)
    - [ ] Cliquer "Lier la tâche"
    - [ ] Vérifier : toast succès "Mail lié à la tâche." + modal se ferme
    - [ ] Ouvrir la tâche liée → onglet "Mails" → le mail apparaît dans la liste

- [ ] **Step 5 : Workflow 3 — Lier un mail depuis l'onglet Mails du TaskModal**

    - [ ] Naviguer vers un projet, ouvrir une tâche via TaskModal
    - [ ] Cliquer sur l'onglet "Mails" (visible uniquement si ROLE_USER/ROLE_ADMIN)
    - [ ] Vérifier : l'onglet charge et affiche "Aucun mail lié" si vide
    - [ ] Cliquer "Lier un mail"
    - [ ] Vérifier : `MailPickerModal` s'ouvre, charge les mails du dossier courant (ou INBOX)
    - [ ] Rechercher/sélectionner un mail, cliquer "Lier ce mail"
    - [ ] Vérifier : toast + liste rafraîchie dans l'onglet "Mails"

- [ ] **Step 6 : Workflow 4 — Navigation depuis l'onglet Mails vers /mail**

    - [ ] Dans l'onglet "Mails" du TaskModal, cliquer sur un mail lié
    - [ ] Vérifier : navigation vers `/mail?messageId={id}` + le mail s'ouvre automatiquement (deep-link Phase 5)

- [ ] **Step 7 : Vérification ROLE_CLIENT**

    - [ ] Se connecter avec `client-liot` / `client`
    - [ ] Naviguer vers `/mail` → redirect `/portal` (Phase 5)
    - [ ] Via le portal, ouvrir un ticket client → TaskModal ne doit pas afficher l'onglet "Mails" (computed `availableTabs` l'exclut pour ROLE_CLIENT)

- [ ] **Step 8 : Commit de correction si nécessaire**

    ```bash
    git -C /home/r-dev/malio-dev/Lesstime add \
        frontend/components/mail/ \
        frontend/components/task/TaskModal.vue \
        frontend/pages/mail.vue
    git -C /home/r-dev/malio-dev/Lesstime commit -m "fix(mail) : corrections post-test Phase 6 (TypeScript + comportement modals)"
    ```

---

## Exigences techniques

- `<script setup lang="ts">` partout — aucun Options API
- **4 espaces d'indentation** — convention Lesstime
- **TypeScript strict** : 0 erreur `npx tsc --noEmit` à la fin de chaque tâche
- **Modals** : pattern `<Teleport to="body">` + `<Transition name="mail-modal">` + backdrop click (identique à `TaskModal.vue`)
- **`MailCreateTaskInput`** : `priority` est transmis en IRI `/api/task_priorities/{id}` (string), pas en id numérique — conforme au pattern existant dans `TaskModal.vue`
- **Autocomplete** : debounce 300ms dans `MailLinkTaskModal` — timer nettoyé dans `onBeforeUnmount`
- **Filtrage mail** : local dans `MailPickerModal` (liste chargée une fois, filtrée en computed) — pas d'appel API par frappe
- **ROLE_CLIENT** : l'onglet "Mails" dans `TaskModal` est exclu via `availableTabs` computed — double protection (backend bloque déjà les endpoints mail pour ROLE_CLIENT)
- **Toast** : géré automatiquement par `useApi` via `toastSuccessKey` dans les méthodes du service — ne pas dupliquer dans les composants
- **NuxtLink** pour la navigation vers `/mail?messageId=X` depuis l'onglet "Mails" (pas de `router.push`) — meilleure accessibilité
- **`v-if="task"` dans TaskModal** : utiliser `props.task` (non null en mode édition) avec guard explicite pour les appels service
- **Format commits** : `feat(mail) : <message>` (espace avant `:`), 1 commit par composant/fichier principal
- **Pas de `console.warn`** dans le code final — supprimer les placeholders Phase 5

---

## Fichiers créés/modifiés — récapitulatif

| Fichier | Type | Action |
|---|---|---|
| `frontend/components/mail/MailCreateTaskModal.vue` | Composant Vue | Créer |
| `frontend/components/mail/MailLinkTaskModal.vue` | Composant Vue | Créer |
| `frontend/components/mail/MailPickerModal.vue` | Composant Vue | Créer |
| `frontend/components/task/TaskModal.vue` | Composant Vue | Modifier (onglet mails, state, watcher) |
| `frontend/pages/mail.vue` | Page Nuxt | Modifier (handlers + modals dans template) |
| `frontend/i18n/locales/fr.json` | i18n | Modifier (sous-clés `mail.createTaskModal`, `mail.linkTaskModal`, `mail.pickerModal`, `mail.taskTab`) |
| `frontend/i18n/locales/en.json` | i18n | Modifier si existant |

**Fichiers Phase 4/5 utilisés (ne pas modifier sauf indication) :**

| Fichier | Utilisation dans Phase 6 |
|---|---|
| `frontend/services/mail.ts` | `createTaskFromMail`, `linkTask`, `listMailsForTask` |
| `frontend/services/dto/mail.ts` | `MailMessageDetailDto`, `MailMessageHeaderDto`, `MailCreateTaskInput` |
| `frontend/services/tasks.ts` | `getFiltered()` dans `MailLinkTaskModal` |
| `frontend/services/projects.ts` | `getAll()` dans `MailCreateTaskModal`, `MailLinkTaskModal` |
| `frontend/services/task-groups.ts` | `getByProject()` dans `MailCreateTaskModal` |
| `frontend/services/task-priorities.ts` | `getAll()` dans `MailCreateTaskModal` |
| `frontend/stores/mail.ts` | `selectedFolderPath` dans `MailPickerModal` |
| `frontend/components/mail/MailMessageViewer.vue` | Émet `createTask` / `linkTask` (déjà câblé en Phase 5) |

---

## Output attendu — critères d'acceptation Phase 6

| Critère | Vérification |
|---|---|
| TypeScript strict : 0 erreur | `cd frontend && npx tsc --noEmit` → sortie vide |
| Workflow 1 complet | Mail → "Créer tâche" → tâche créée avec subject comme titre → visible dans onglet Mails du TaskModal |
| Workflow 2 complet | Mail → "Lier à une tâche" → sélection + liaison → mail visible dans onglet Mails |
| Workflow 3 complet | TaskModal onglet Mails → "Lier un mail" → MailPickerModal → liaison → liste rafraîchie |
| Workflow 4 complet | Onglet Mails → click mail → navigation `/mail?messageId=X` → deep-link fonctionne |
| ROLE_CLIENT isolé | Onglet "Mails" absent pour ROLE_CLIENT (computed `availableTabs`) |
| Aucun `console.warn` résiduel | DevTools console → aucun warn `[mail]` Phase 5 |
| Pas d'erreur Vue/Nuxt | DevTools console → aucune erreur rouge |

La Phase 7 (Admin config + sidebar + polish) peut commencer dès que tous ces critères sont satisfaits.
