MelissaConversas: refator + extracoes + migracao Tailwind

useConversations: debounce 300ms no realtime load (sem isso, clinica
ativa fazia SELECT 500 por mensagem); expose currentUserId no return
(antes SFC + composable faziam 2 round-trips a auth.getUser); cleanup
do timer no unsubscribeRealtime.

MelissaConversas: bug fixes de loading
- reloadThreadTags lê de threads.value (universal, nao filtered) — antes
  tags piscavam a cada flick de filtro
- watch(threads) com debounce 200ms substitui watch(filteredThreads.length)
  — antes recarregava todas as tags em cada char digitado
- Promise.all no mount sem race com currentUserId (reloadThreadTags
  removido daqui — vem via watch automatic)
- watch drawer.isOpen: await load() antes (antes load+reload em paralelo
  liam threads velhas)
- watch tenantStore com token monotonico (race A→B→A)
- supabase.auth.getUser local removido (usa currentUserId do composable)

Extracoes:
- MelissaConversasSidebar.vue: aside col-1 (alerta unlinked + 4 grupos
  de filtros + footer "Limpar filtros" com Vue Transition). filters
  passado como prop e mutado direto. KANBAN_COLUMNS/CHANNEL_OPTIONS/
  channelIcon/hasActiveFilters/clearAllFilters movidos pra dentro.
  Tailwind nas bases; state modifiers .is-active/.is-warn/.is-danger/
  .is-{red,amber,blue,emerald} ficam scoped (cores fixas por status).
- MelissaConversasCard.vue: card do kanban (head/msg/tags/foot).
  channelIcon/truncate/contactLabel/fmtRelative/assigneeLabel movidos.
  aria-label, aria-pressed, aria-hidden em icones decorativos.
  Tailwind no template; .is-mine do assignee fica scoped.

Tailwind no resto do parent: containers (.mw-page + animation), header
(.mw-page__head/title/count/unread/actions), search (.mw-search* +
--xl-only via max-[1279px]:hidden), close/head-btn/menu-btn (incluindo
--compact-only e --mobile-only via hidden + max-[XXX]:grid/inline-flex),
subheader, body/main/kanban/col/col__head/title/count/body/empty,
mobile drawer + backdrop. 2 media queries inteiras eliminadas
(@media max-width 1279/1023). State modifiers de kanban color
(.mw-col.is-{color}) ficam scoped — 12 regras com cores fixas RGB
seriam ruidosas inline. Cross-teleport :deep(.mw-side*) preservado.

MelissaConversas: 1293 -> 465 linhas (-828, -64%)
  script: 198 -> 195 (logica essencial preservada)
  template: 278 -> 143 (49% reducao via componentizacao)
  style: 761 -> 99 (87% reducao — so keyframes, kanban color states,
                     scrollbars, cross-teleport :deep, Vue Transitions)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-07 13:51:05 -03:00
parent ef3e160b36
commit 250e946084
4 changed files with 620 additions and 986 deletions
+29 -4
View File
@@ -45,6 +45,21 @@ export function useConversations() {
let realtimeChannel = null;
// Debounce do refetch disparado por realtime — sem isso, clínica ativa
// recebendo 10 mensagens/min faz 10 SELECT 500 na lista de threads por
// minuto. 300ms agrupa rajadas (digita-rápido, mensagens em sequência)
// sem fazer o user esperar visualmente. A nova mensagem em si vai pro
// threadMessages direto (acima); load() é só pra atualizar contadores
// e preview do thread na lista — pode esperar.
let _loadDebounceTimer = null;
function _scheduleLoad() {
if (_loadDebounceTimer) clearTimeout(_loadDebounceTimer);
_loadDebounceTimer = setTimeout(() => {
_loadDebounceTimer = null;
load();
}, 300);
}
async function load() {
const tenantId = tenantStore.activeTenantId;
if (!tenantId) {
@@ -90,8 +105,8 @@ export function useConversations() {
filter: `tenant_id=eq.${tenantId}`
},
(payload) => {
// refetch da lista (view agrega tudo)
load();
// refetch da lista (view agrega tudo) — debounced
_scheduleLoad();
// se o drawer esta aberto numa thread desta msg, appenda
const newMsg = payload.new;
if (currentThread.value && messageBelongsToThread(newMsg, currentThread.value)) {
@@ -109,7 +124,7 @@ export function useConversations() {
filter: `tenant_id=eq.${tenantId}`
},
(payload) => {
load();
_scheduleLoad();
const updated = payload.new;
if (currentThread.value && messageBelongsToThread(updated, currentThread.value)) {
const idx = threadMessages.value.findIndex((m) => m.id === updated.id);
@@ -125,6 +140,12 @@ export function useConversations() {
supabase.removeChannel(realtimeChannel);
realtimeChannel = null;
}
// Cancela refetch agendado — se desmontar entre o trigger e o
// debounce dispara, callback rodaria em ref morta.
if (_loadDebounceTimer) {
clearTimeout(_loadDebounceTimer);
_loadDebounceTimer = null;
}
}
onUnmounted(() => unsubscribeRealtime());
@@ -263,6 +284,10 @@ export function useConversations() {
threadLoading,
loadThreadMessages,
markThreadRead,
setKanbanStatus
setKanbanStatus,
// Exposto pra evitar que cada consumidor chame supabase.auth.getUser()
// por conta propria — antes, MelissaConversas + composable faziam 2
// round-trips ao auth no mesmo mount.
currentUserId
};
}