From cc7841bd1f865c9f0845304287ad98a3207e2c68 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Thu, 7 May 2026 16:37:46 -0300 Subject: [PATCH] MelissaConversas: a11y + perf tagsForThread + DRY (channelMeta + KANBAN_COLUMNS shared) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A11y no parent: - aria-label em botoes icon-only do header (Recarregar dinamico, Buscar compact, Close); tooltip vira title que SR ignora - aria-hidden=true em icones decorativos (header title, search input, subheader info-circle, kanban col head, empty state, button icons) - aria-busy reativo no mw-col__body durante loading - aria-label dinamico no count do kanban ("3 conversas em Urgente") - aria-expanded + aria-controls no menu mobile button - aria-label no input de busca - role=note no subheader explicativo - :inert="(drawerOpen && isMobile) || null" no
— focus trap real: drawer aberto torna conteudo de fundo inerte (boolean attr via || null pra Vue 3.4 serializar correto) A11y no Sidebar: - aria-hidden=true em todos icones decorativos restantes (filter title icons, list/bell/user/user-minus, channel icons, filter-slash, etc) Perf — tagsForThread cacheado: - Antes era chamado in-template (2x por card, recriava array a cada render). Agora tagsByThreadKey computed Map: lookup O(1) por card, recompute so quando threadTagsMap ou tagById muda. EMPTY_TAGS frozen evita criar arrays novos pra threads sem tags. DRY — channelMeta + KANBAN_COLUMNS shared: - src/utils/channelMeta.js (novo): CHANNEL_OPTIONS frozen + channelIcon + channelLabel. Antes channelIcon estava em 3 lugares (parent, Sidebar, Card); CHANNEL_OPTIONS em 2 (parent, Sidebar). Agora 1. - useConversations.js: exporta KANBAN_COLUMNS frozen (metadata canonica: key + label + icon + color). Antes parent+Sidebar tinham copias locais de 8 linhas cada + composable tinha KANBAN_ORDER separado. Agora KANBAN_ORDER deriva de KANBAN_COLUMNS. Drift eliminado: 3 fontes -> 1 pra channelIcon, 2 -> 1 pra CHANNEL_OPTIONS, 2 -> 1 pra KANBAN_COLUMNS (KANBAN_ORDER ainda interno ao composable mas derivado). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/composables/useConversations.js | 12 ++- src/layout/melissa/MelissaConversas.vue | 74 +++++++++++++------ src/layout/melissa/MelissaConversasCard.vue | 10 +-- .../melissa/MelissaConversasSidebar.vue | 55 +++++--------- src/utils/channelMeta.js | 34 +++++++++ 5 files changed, 116 insertions(+), 69 deletions(-) create mode 100644 src/utils/channelMeta.js diff --git a/src/composables/useConversations.js b/src/composables/useConversations.js index 6c8d896..7148e6c 100644 --- a/src/composables/useConversations.js +++ b/src/composables/useConversations.js @@ -19,7 +19,17 @@ import { ref, computed, onUnmounted } from 'vue'; import { supabase } from '@/lib/supabase/client'; import { useTenantStore } from '@/stores/tenantStore'; -const KANBAN_ORDER = ['urgent', 'awaiting_us', 'awaiting_patient', 'resolved']; +// Metadata canonica das colunas do kanban — fonte unica consumida pelo +// SFC parent (kanban grid central) e pelo MelissaConversasSidebar +// (resumo "Por status"). Antes, parent + sidebar tinham copias locais +// de KANBAN_COLUMNS e o composable separadamente tinha KANBAN_ORDER. +export const KANBAN_COLUMNS = Object.freeze([ + Object.freeze({ key: 'urgent', label: 'Urgente', icon: 'pi pi-exclamation-triangle', color: 'red' }), + Object.freeze({ key: 'awaiting_us', label: 'Aguardando resposta', icon: 'pi pi-inbox', color: 'amber' }), + Object.freeze({ key: 'awaiting_patient', label: 'Aguardando paciente', icon: 'pi pi-hourglass', color: 'blue' }), + Object.freeze({ key: 'resolved', label: 'Resolvido', icon: 'pi pi-check', color: 'emerald' }) +]); +const KANBAN_ORDER = KANBAN_COLUMNS.map((c) => c.key); function sanitizeSearch(raw) { if (typeof raw !== 'string') return ''; diff --git a/src/layout/melissa/MelissaConversas.vue b/src/layout/melissa/MelissaConversas.vue index 350fb5b..affe5f2 100644 --- a/src/layout/melissa/MelissaConversas.vue +++ b/src/layout/melissa/MelissaConversas.vue @@ -17,7 +17,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'; import { supabase } from '@/lib/supabase/client'; import Popover from 'primevue/popover'; -import { useConversations } from '@/composables/useConversations'; +import { useConversations, KANBAN_COLUMNS } from '@/composables/useConversations'; import { useConversationTags } from '@/composables/useConversationTags'; import { useTenantStore } from '@/stores/tenantStore'; import { useConversationDrawerStore } from '@/stores/conversationDrawerStore'; @@ -56,9 +56,26 @@ const tagById = computed(() => { for (const t of tagsApi.allTags.value) m[t.id] = t; return m; }); + +// Cache O(1) por thread_key — antes tagsForThread era chamado in-template +// no v-for de cards, recriava array a cada render. Agora computa uma vez +// quando threadTagsMap ou tagById muda (raro), lookup O(1) por card. +const EMPTY_TAGS = Object.freeze([]); +const tagsByThreadKey = computed(() => { + const out = new Map(); + const tags = tagById.value; + for (const [key, ids] of threadTagsMap.value) { + const arr = []; + for (const id of ids) { + const t = tags[id]; + if (t) arr.push(t); + } + out.set(key, arr); + } + return out; +}); function tagsForThread(threadKey) { - const ids = threadTagsMap.value.get(threadKey) || []; - return ids.map((id) => tagById.value[id]).filter(Boolean); + return tagsByThreadKey.value.get(threadKey) || EMPTY_TAGS; } async function reloadThreadTags() { // Tags pra TODOS os threads (nao so filtered) — antes, mudar de filtro @@ -108,14 +125,8 @@ async function loadMemberNames() { // assigneeLabel migrou pra MelissaConversasCard (usa props.memberNameMap). // Constantes -const KANBAN_COLUMNS = [ - { key: 'urgent', label: 'Urgente', icon: 'pi pi-exclamation-triangle', color: 'red' }, - { key: 'awaiting_us', label: 'Aguardando resposta', icon: 'pi pi-inbox', color: 'amber' }, - { key: 'awaiting_patient', label: 'Aguardando paciente', icon: 'pi pi-hourglass', color: 'blue' }, - { key: 'resolved', label: 'Resolvido', icon: 'pi pi-check', color: 'emerald' } -]; - -// CHANNEL_OPTIONS migrou pra MelissaConversasSidebar (uso unico la). +// KANBAN_COLUMNS importado de @/composables/useConversations (fonte unica). +// CHANNEL_OPTIONS movido pra @/utils/channelMeta (compartilhado com Sidebar/Card). // Helpers fmtRelative/channelIcon/truncate/contactLabel migraram pra // MelissaConversasCard (uso unico la). Se aparecer 3o consumidor, @@ -212,18 +223,23 @@ watch(() => tenantStore.activeTenantId, async (newTid, oldTid) => { /> -
+