import { defineStore } from 'pinia' import type { MailFolderDto, MailMessageHeaderDto, MailMessageDetailDto, } from '~/services/dto/mail' import { useMailService } from '~/services/mail' const POLL_INTERVAL_MS = 30 * 1000 // 30 secondes export const useMailStore = defineStore('mail', () => { // ─── State ──────────────────────────────────────────────────────────────── /** Liste plate des dossiers (reçue de l'API) */ const folders = ref([]) /** Chemin du dossier actuellement sélectionné */ const selectedFolderPath = ref(null) /** Messages du dossier sélectionné (accumulés pour infinite scroll) */ const messages = ref([]) /** Cursor de pagination pour la page suivante (null = plus de données) */ const messagesCursor = ref(null) /** Chargement en cours (messages) */ const messagesLoading = ref(false) /** ID du message sélectionné pour lecture */ const selectedMessageId = ref(null) /** Détail complet du message sélectionné (body + PJ) */ const selectedMessageDetail = ref(null) /** Chargement du détail en cours */ const detailLoading = ref(false) /** Sync IMAP en cours (déclenchée manuellement) */ const syncing = ref(false) /** Nombre total de messages non lus (toutes boîtes confondues) */ const globalUnreadCount = ref(0) /** Erreur courante (null si aucune) */ const error = ref(null) let pollTimer: ReturnType | null = null // ─── Getters ────────────────────────────────────────────────────────────── /** * Nombre de non-lus dans INBOX uniquement (utilisé dans la sidebar). */ const inboxUnread = computed(() => { const inbox = folders.value.find( (f) => f.path === 'INBOX' || f.path.toUpperCase() === 'INBOX', ) return inbox?.unreadCount ?? 0 }) /** * Construit l'arbre de dossiers depuis la liste plate. * Les dossiers sans parentPath sont à la racine. * Les enfants sont triés alphabétiquement par displayName. */ const folderTree = computed((): MailFolderDto[] => { const map = new Map() const roots: MailFolderDto[] = [] // Initialiser chaque dossier avec children vide folders.value.forEach((folder) => { map.set(folder.path, { ...folder, children: [] }) }) // Construire l'arbre map.forEach((folder) => { if (folder.parentPath && map.has(folder.parentPath)) { const parent = map.get(folder.parentPath)! parent.children = parent.children ?? [] parent.children.push(folder) } else { roots.push(folder) } }) // Trier les enfants alphabétiquement function sortChildren(nodes: MailFolderDto[]): MailFolderDto[] { return nodes .map((n) => ({ ...n, children: n.children ? sortChildren(n.children) : undefined, })) .sort((a, b) => a.displayName.localeCompare(b.displayName, 'fr')) } return sortChildren(roots) }) /** * Indique si le cursor de pagination est disponible (plus de messages à charger). */ const hasMoreMessages = computed(() => messagesCursor.value !== null) // ─── Actions ────────────────────────────────────────────────────────────── /** * Charge la liste des dossiers depuis l'API et met à jour globalUnreadCount. */ async function fetchFolders(): Promise { const service = useMailService() try { folders.value = await service.listFolders() globalUnreadCount.value = folders.value.reduce( (sum, f) => sum + f.unreadCount, 0, ) } catch { // Silently ignore polling errors (ne pas interrompre l'UX) } } /** * Charge les messages du dossier sélectionné. * @param append - Si true, ajoute à la liste existante (infinite scroll). Si false, remplace. */ async function fetchMessages(append = false): Promise { if (!selectedFolderPath.value) return if (messagesLoading.value) return messagesLoading.value = true error.value = null const service = useMailService() try { const cursor = append ? (messagesCursor.value ?? undefined) : undefined const page = await service.listMessages(selectedFolderPath.value, cursor) if (append) { messages.value = [...messages.value, ...page.items] } else { messages.value = page.items } messagesCursor.value = page.nextCursor } catch (err) { error.value = err instanceof Error ? err.message : 'Erreur lors du chargement des messages.' } finally { messagesLoading.value = false } } /** * Sélectionne un dossier et charge ses messages (reset de la pagination). * @param path - Chemin du dossier (ex: "INBOX") */ async function selectFolder(path: string): Promise { if (selectedFolderPath.value === path) return selectedFolderPath.value = path messages.value = [] messagesCursor.value = null selectedMessageId.value = null selectedMessageDetail.value = null await fetchMessages() } /** * Marque un message comme lu ou non-lu. * Met à jour le state local (messages + detail) sans refetch. */ async function markRead(id: number, read: boolean): Promise { const service = useMailService() const updated = await service.markRead(id, read) // Mise à jour optimiste dans la liste const idx = messages.value.findIndex((m) => m.id === id) if (idx !== -1) { messages.value[idx] = { ...messages.value[idx], isRead: updated.isRead } } // Mise à jour dans le détail si ouvert if (selectedMessageDetail.value?.header.id === id) { selectedMessageDetail.value = { ...selectedMessageDetail.value, header: { ...selectedMessageDetail.value.header, isRead: updated.isRead }, } } // Mettre à jour le compteur du dossier await _refreshFolderUnreadCount() } /** * Sélectionne un message et charge son détail complet (body + PJ). * Marque automatiquement le message comme lu si ce n'est pas déjà le cas. * @param id - ID BDD du message */ async function selectMessage(id: number): Promise { if (selectedMessageId.value === id) return selectedMessageId.value = id selectedMessageDetail.value = null detailLoading.value = true const service = useMailService() try { const detail = await service.getMessage(id) selectedMessageDetail.value = detail // Auto-mark as read si nécessaire if (!detail.header.isRead) { await markRead(id, true) } } finally { detailLoading.value = false } } /** * Marque un message comme étoilé ou non-étoilé. * Met à jour le state local sans refetch. */ async function markFlagged(id: number, flagged: boolean): Promise { const service = useMailService() const updated = await service.markFlagged(id, flagged) const idx = messages.value.findIndex((m) => m.id === id) if (idx !== -1) { messages.value[idx] = { ...messages.value[idx], isFlagged: updated.isFlagged } } if (selectedMessageDetail.value?.header.id === id) { selectedMessageDetail.value = { ...selectedMessageDetail.value, header: { ...selectedMessageDetail.value.header, isFlagged: updated.isFlagged }, } } } /** * Déclenche une synchronisation IMAP asynchrone. * Recharge les dossiers après 2s pour refléter les nouveaux messages. */ async function triggerSync(): Promise { if (syncing.value) return syncing.value = true const service = useMailService() try { await service.triggerSync() // Laisser le temps au handler Messenger de traiter setTimeout(async () => { await fetchFolders() if (selectedFolderPath.value) { await fetchMessages(false) } syncing.value = false }, 2000) } catch { syncing.value = false } } /** * Arrête le polling. À appeler au logout. */ function stopPolling(): void { if (pollTimer) { clearInterval(pollTimer) pollTimer = null } } /** * Démarre le polling toutes les 30s pour mettre à jour globalUnreadCount. * À appeler dans app.vue ou le layout default au login. * Idempotent : un seul timer actif à la fois. */ function startPolling(): void { if (pollTimer) return fetchFolders() // Charge immédiatement pollTimer = setInterval(fetchFolders, POLL_INTERVAL_MS) // Cleanup automatique si le scope du store est détruit if (getCurrentScope()) { onScopeDispose(stopPolling) } } /** * Rafraîchit les compteurs non-lus du dossier actuel depuis l'API. * Usage interne — appelé après markRead. */ async function _refreshFolderUnreadCount(): Promise { const service = useMailService() try { const updatedFolders = await service.listFolders() folders.value = updatedFolders globalUnreadCount.value = updatedFolders.reduce( (sum, f) => sum + f.unreadCount, 0, ) } catch { // Silently ignore } } return { // State (readonly pour les consommateurs) folders: readonly(folders), selectedFolderPath: readonly(selectedFolderPath), messages: readonly(messages), messagesCursor: readonly(messagesCursor), messagesLoading: readonly(messagesLoading), selectedMessageId: readonly(selectedMessageId), selectedMessageDetail: readonly(selectedMessageDetail), detailLoading: readonly(detailLoading), syncing: readonly(syncing), globalUnreadCount: readonly(globalUnreadCount), error: readonly(error), // Getters inboxUnread, folderTree, hasMoreMessages, // Actions fetchFolders, selectFolder, fetchMessages, selectMessage, markRead, markFlagged, triggerSync, startPolling, stopPolling, } })