MelissaConversas: a11y + perf tagsForThread + DRY (channelMeta + KANBAN_COLUMNS shared)
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 <section class="mw-page">
— 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) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,17 @@ import { ref, computed, onUnmounted } from 'vue';
|
|||||||
import { supabase } from '@/lib/supabase/client';
|
import { supabase } from '@/lib/supabase/client';
|
||||||
import { useTenantStore } from '@/stores/tenantStore';
|
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) {
|
function sanitizeSearch(raw) {
|
||||||
if (typeof raw !== 'string') return '';
|
if (typeof raw !== 'string') return '';
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
import { supabase } from '@/lib/supabase/client';
|
import { supabase } from '@/lib/supabase/client';
|
||||||
import Popover from 'primevue/popover';
|
import Popover from 'primevue/popover';
|
||||||
import { useConversations } from '@/composables/useConversations';
|
import { useConversations, KANBAN_COLUMNS } from '@/composables/useConversations';
|
||||||
import { useConversationTags } from '@/composables/useConversationTags';
|
import { useConversationTags } from '@/composables/useConversationTags';
|
||||||
import { useTenantStore } from '@/stores/tenantStore';
|
import { useTenantStore } from '@/stores/tenantStore';
|
||||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||||
@@ -56,9 +56,26 @@ const tagById = computed(() => {
|
|||||||
for (const t of tagsApi.allTags.value) m[t.id] = t;
|
for (const t of tagsApi.allTags.value) m[t.id] = t;
|
||||||
return m;
|
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) {
|
function tagsForThread(threadKey) {
|
||||||
const ids = threadTagsMap.value.get(threadKey) || [];
|
return tagsByThreadKey.value.get(threadKey) || EMPTY_TAGS;
|
||||||
return ids.map((id) => tagById.value[id]).filter(Boolean);
|
|
||||||
}
|
}
|
||||||
async function reloadThreadTags() {
|
async function reloadThreadTags() {
|
||||||
// Tags pra TODOS os threads (nao so filtered) — antes, mudar de filtro
|
// 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).
|
// assigneeLabel migrou pra MelissaConversasCard (usa props.memberNameMap).
|
||||||
|
|
||||||
// Constantes
|
// Constantes
|
||||||
const KANBAN_COLUMNS = [
|
// KANBAN_COLUMNS importado de @/composables/useConversations (fonte unica).
|
||||||
{ key: 'urgent', label: 'Urgente', icon: 'pi pi-exclamation-triangle', color: 'red' },
|
// CHANNEL_OPTIONS movido pra @/utils/channelMeta (compartilhado com Sidebar/Card).
|
||||||
{ 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).
|
|
||||||
|
|
||||||
// Helpers fmtRelative/channelIcon/truncate/contactLabel migraram pra
|
// Helpers fmtRelative/channelIcon/truncate/contactLabel migraram pra
|
||||||
// MelissaConversasCard (uso unico la). Se aparecer 3o consumidor,
|
// MelissaConversasCard (uso unico la). Se aparecer 3o consumidor,
|
||||||
@@ -212,18 +223,23 @@ watch(() => tenantStore.activeTenantId, async (newTid, oldTid) => {
|
|||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<section class="mw-page absolute inset-[6px] [bottom:calc(var(--m-dock-h,76px)+6px)] z-40 flex flex-col bg-[var(--m-bg-medium)] backdrop-blur-[32px] backdrop-saturate-[160%] border border-[var(--m-border)] rounded-[18px] shadow-[0_16px_48px_rgba(0,0,0,0.4)] overflow-hidden text-[var(--m-text)] [font-family:'Segoe_UI',system-ui,-apple-system,sans-serif] [animation:mw-page-enter_240ms_cubic-bezier(0.2,0.7,0.3,1)]">
|
<section
|
||||||
|
class="mw-page absolute inset-[6px] [bottom:calc(var(--m-dock-h,76px)+6px)] z-40 flex flex-col bg-[var(--m-bg-medium)] backdrop-blur-[32px] backdrop-saturate-[160%] border border-[var(--m-border)] rounded-[18px] shadow-[0_16px_48px_rgba(0,0,0,0.4)] overflow-hidden text-[var(--m-text)] [font-family:'Segoe_UI',system-ui,-apple-system,sans-serif] [animation:mw-page-enter_240ms_cubic-bezier(0.2,0.7,0.3,1)]"
|
||||||
|
:inert="(drawerOpen && isMobile) || null"
|
||||||
|
>
|
||||||
<header class="mw-page__head flex items-center justify-between px-[18px] py-[14px] border-b border-[var(--m-border)] flex-shrink-0 gap-2.5">
|
<header class="mw-page__head flex items-center justify-between px-[18px] py-[14px] border-b border-[var(--m-border)] flex-shrink-0 gap-2.5">
|
||||||
<button
|
<button
|
||||||
class="mw-menu-btn mw-menu-btn--mobile-only hidden max-[1023px]:inline-flex h-8 items-center gap-1.5 flex-shrink-0 px-[11px] rounded-[9px] cursor-pointer text-[0.78rem] font-semibold text-white bg-[var(--m-accent)] border border-[var(--m-accent)] [font-family:inherit] transition-[background-color,transform] duration-[140ms] hover:bg-[color-mix(in_srgb,var(--m-accent)_88%,white)] hover:-translate-y-px [&>i]:text-[0.85rem]"
|
class="mw-menu-btn mw-menu-btn--mobile-only hidden max-[1023px]:inline-flex h-8 items-center gap-1.5 flex-shrink-0 px-[11px] rounded-[9px] cursor-pointer text-[0.78rem] font-semibold text-white bg-[var(--m-accent)] border border-[var(--m-accent)] [font-family:inherit] transition-[background-color,transform] duration-[140ms] hover:bg-[color-mix(in_srgb,var(--m-accent)_88%,white)] hover:-translate-y-px [&>i]:text-[0.85rem]"
|
||||||
|
:aria-expanded="drawerOpen"
|
||||||
|
aria-controls="mw-mobile-drawer-target"
|
||||||
v-tooltip.bottom="'Filtros & atribuição'"
|
v-tooltip.bottom="'Filtros & atribuição'"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
>
|
>
|
||||||
<i class="pi pi-bars" />
|
<i class="pi pi-bars" aria-hidden="true" />
|
||||||
<span>Menu Conversas</span>
|
<span>Menu Conversas</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="mw-page__title flex-1 min-w-0 flex items-center gap-2.5 text-[1rem] font-medium flex-wrap [&>i:first-child]:max-[1023px]:hidden">
|
<div class="mw-page__title flex-1 min-w-0 flex items-center gap-2.5 text-[1rem] font-medium flex-wrap [&>i:first-child]:max-[1023px]:hidden">
|
||||||
<i class="pi pi-comments text-emerald-300" />
|
<i class="pi pi-comments text-emerald-300" aria-hidden="true" />
|
||||||
<span class="overflow-hidden text-ellipsis whitespace-nowrap max-[1023px]:hidden">Conversas</span>
|
<span class="overflow-hidden text-ellipsis whitespace-nowrap max-[1023px]:hidden">Conversas</span>
|
||||||
<span class="mw-page__count text-[0.7rem] font-semibold text-[var(--m-accent)] bg-[var(--m-accent-soft)] border border-[color-mix(in_srgb,var(--m-accent)_35%,transparent)] px-2 py-0.5 rounded-full">{{ summary.total }}</span>
|
<span class="mw-page__count text-[0.7rem] font-semibold text-[var(--m-accent)] bg-[var(--m-accent-soft)] border border-[color-mix(in_srgb,var(--m-accent)_35%,transparent)] px-2 py-0.5 rounded-full">{{ summary.total }}</span>
|
||||||
<span v-if="summary.unreadTotal > 0" class="mw-page__unread text-[0.7rem] font-semibold text-[rgb(248,113,113)] bg-[rgba(248,113,113,0.12)] border border-[rgba(248,113,113,0.3)] px-2 py-0.5 rounded-full">
|
<span v-if="summary.unreadTotal > 0" class="mw-page__unread text-[0.7rem] font-semibold text-[rgb(248,113,113)] bg-[rgba(248,113,113,0.12)] border border-[rgba(248,113,113,0.3)] px-2 py-0.5 rounded-full">
|
||||||
@@ -232,36 +248,40 @@ watch(() => tenantStore.activeTenantId, async (newTid, oldTid) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mw-page__actions flex items-center gap-2 flex-shrink-0">
|
<div class="mw-page__actions flex items-center gap-2 flex-shrink-0">
|
||||||
<div class="mw-search mw-search--xl-only relative w-[240px] max-[1279px]:hidden">
|
<div class="mw-search mw-search--xl-only relative w-[240px] max-[1279px]:hidden">
|
||||||
<i class="pi pi-search mw-search__icon absolute top-1/2 left-3 -translate-y-1/2 text-[var(--m-text-muted)] text-[0.78rem]" />
|
<i class="pi pi-search mw-search__icon absolute top-1/2 left-3 -translate-y-1/2 text-[var(--m-text-muted)] text-[0.78rem]" aria-hidden="true" />
|
||||||
<input
|
<input
|
||||||
v-model="filters.search"
|
v-model="filters.search"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Buscar paciente, número ou mensagem"
|
placeholder="Buscar paciente, número ou mensagem"
|
||||||
maxlength="120"
|
maxlength="120"
|
||||||
|
aria-label="Buscar paciente, número ou mensagem"
|
||||||
class="mw-search__input w-full h-8 bg-[var(--m-bg-soft)] border border-[var(--m-border)] text-[var(--m-text)] px-3 pl-8 rounded-[9px] text-[0.78rem] [font-family:inherit] outline-none transition-[border-color,background-color] duration-[140ms] focus:bg-[var(--m-bg-soft-hover)] focus:border-[var(--m-border-strong)] placeholder:text-[var(--m-text-faint)]"
|
class="mw-search__input w-full h-8 bg-[var(--m-bg-soft)] border border-[var(--m-border)] text-[var(--m-text)] px-3 pl-8 rounded-[9px] text-[0.78rem] [font-family:inherit] outline-none transition-[border-color,background-color] duration-[140ms] focus:bg-[var(--m-bg-soft-hover)] focus:border-[var(--m-border-strong)] placeholder:text-[var(--m-text-faint)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="mw-head-btn mw-head-btn--compact-only hidden max-[1279px]:grid w-8 h-8 place-items-center bg-[var(--m-bg-soft)] border border-[var(--m-border)] text-[var(--m-text)] rounded-[9px] cursor-pointer [font-family:inherit] transition-colors duration-[140ms] hover:bg-[var(--m-bg-soft-hover)] [&>i]:text-[0.85rem]"
|
class="mw-head-btn mw-head-btn--compact-only hidden max-[1279px]:grid w-8 h-8 place-items-center bg-[var(--m-bg-soft)] border border-[var(--m-border)] text-[var(--m-text)] rounded-[9px] cursor-pointer [font-family:inherit] transition-colors duration-[140ms] hover:bg-[var(--m-bg-soft-hover)] [&>i]:text-[0.85rem]"
|
||||||
|
aria-label="Buscar"
|
||||||
v-tooltip.bottom="'Buscar'"
|
v-tooltip.bottom="'Buscar'"
|
||||||
@click="openActions"
|
@click="openActions"
|
||||||
>
|
>
|
||||||
<i class="pi pi-search" />
|
<i class="pi pi-search" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="mw-head-btn w-8 h-8 grid place-items-center bg-[var(--m-bg-soft)] border border-[var(--m-border)] text-[var(--m-text)] rounded-[9px] cursor-pointer [font-family:inherit] transition-colors duration-[140ms] hover:bg-[var(--m-bg-soft-hover)] [&>i]:text-[0.85rem]"
|
class="mw-head-btn w-8 h-8 grid place-items-center bg-[var(--m-bg-soft)] border border-[var(--m-border)] text-[var(--m-text)] rounded-[9px] cursor-pointer [font-family:inherit] transition-colors duration-[140ms] hover:bg-[var(--m-bg-soft-hover)] [&>i]:text-[0.85rem]"
|
||||||
|
:aria-label="loading ? 'Recarregando…' : 'Recarregar lista'"
|
||||||
v-tooltip.bottom="'Recarregar'"
|
v-tooltip.bottom="'Recarregar'"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="load"
|
@click="load"
|
||||||
>
|
>
|
||||||
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
|
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="mw-close w-8 h-8 grid place-items-center bg-[var(--m-bg-soft)] border border-[var(--m-border)] text-[var(--m-text)] rounded-[9px] cursor-pointer [font-family:inherit] transition-colors duration-[140ms] hover:bg-[var(--m-bg-soft-hover)]"
|
class="mw-close w-8 h-8 grid place-items-center bg-[var(--m-bg-soft)] border border-[var(--m-border)] text-[var(--m-text)] rounded-[9px] cursor-pointer [font-family:inherit] transition-colors duration-[140ms] hover:bg-[var(--m-bg-soft-hover)]"
|
||||||
|
aria-label="Voltar ao resumo (Esc)"
|
||||||
v-tooltip.bottom="'Voltar ao resumo (Esc)'"
|
v-tooltip.bottom="'Voltar ao resumo (Esc)'"
|
||||||
@click="emit('close')"
|
@click="emit('close')"
|
||||||
>
|
>
|
||||||
<i class="pi pi-times" />
|
<i class="pi pi-times" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -282,8 +302,8 @@ watch(() => tenantStore.activeTenantId, async (newTid, oldTid) => {
|
|||||||
|
|
||||||
<!-- Subheader explicativo (blueprint §9) — diferencia de
|
<!-- Subheader explicativo (blueprint §9) — diferencia de
|
||||||
outras páginas Melissa que mostram listas tabulares. -->
|
outras páginas Melissa que mostram listas tabulares. -->
|
||||||
<div class="mw-subheader flex items-start gap-2.5 px-[18px] py-2.5 border-b border-[var(--m-border)] bg-[var(--m-bg-soft)] text-[0.78rem] text-[var(--m-text-muted)] leading-[1.45] flex-shrink-0">
|
<div class="mw-subheader flex items-start gap-2.5 px-[18px] py-2.5 border-b border-[var(--m-border)] bg-[var(--m-bg-soft)] text-[0.78rem] text-[var(--m-text-muted)] leading-[1.45] flex-shrink-0" role="note">
|
||||||
<i class="pi pi-info-circle mw-subheader__icon text-[var(--p-primary-color)] text-[0.92rem] flex-shrink-0 mt-px" />
|
<i class="pi pi-info-circle mw-subheader__icon text-[var(--p-primary-color)] text-[0.92rem] flex-shrink-0 mt-px" aria-hidden="true" />
|
||||||
<span class="mw-subheader__text flex-1 min-w-0 [&>strong]:text-[var(--m-text)] [&>strong]:font-semibold">
|
<span class="mw-subheader__text flex-1 min-w-0 [&>strong]:text-[var(--m-text)] [&>strong]:font-semibold">
|
||||||
CRM de mensagens organizado em <strong>kanban por urgência</strong>:
|
CRM de mensagens organizado em <strong>kanban por urgência</strong>:
|
||||||
Urgente / Aguardando resposta / Aguardando paciente / Resolvido.
|
Urgente / Aguardando resposta / Aguardando paciente / Resolvido.
|
||||||
@@ -315,12 +335,18 @@ watch(() => tenantStore.activeTenantId, async (newTid, oldTid) => {
|
|||||||
>
|
>
|
||||||
<div class="mw-col__head flex items-center justify-between px-3 py-2.5 border-b border-[var(--m-border)] flex-shrink-0">
|
<div class="mw-col__head flex items-center justify-between px-3 py-2.5 border-b border-[var(--m-border)] flex-shrink-0">
|
||||||
<div class="mw-col__title inline-flex items-center gap-1.5 text-[0.78rem] font-semibold [&>i]:text-[0.78rem]">
|
<div class="mw-col__title inline-flex items-center gap-1.5 text-[0.78rem] font-semibold [&>i]:text-[0.78rem]">
|
||||||
<i :class="col.icon" />
|
<i :class="col.icon" aria-hidden="true" />
|
||||||
<span>{{ col.label }}</span>
|
<span>{{ col.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="mw-col__count text-[0.65rem] font-semibold text-[var(--m-text)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center">{{ byKanban[col.key].length }}</span>
|
<span
|
||||||
|
class="mw-col__count text-[0.65rem] font-semibold text-[var(--m-text)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center"
|
||||||
|
:aria-label="`${byKanban[col.key].length} conversas em ${col.label}`"
|
||||||
|
>{{ byKanban[col.key].length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mw-col__body flex-1 overflow-y-auto p-2 flex flex-col gap-2 max-[1023px]:flex-none max-[1023px]:overflow-visible max-[1023px]:min-h-[80px]">
|
<div
|
||||||
|
class="mw-col__body flex-1 overflow-y-auto p-2 flex flex-col gap-2 max-[1023px]:flex-none max-[1023px]:overflow-visible max-[1023px]:min-h-[80px]"
|
||||||
|
:aria-busy="carregandoInicial && !byKanban[col.key].length"
|
||||||
|
>
|
||||||
<!-- Skeleton só na primeira carga, na coluna que estiver vazia.
|
<!-- Skeleton só na primeira carga, na coluna que estiver vazia.
|
||||||
Estrutura inline (nao usa MelissaConversasCard pq o markup
|
Estrutura inline (nao usa MelissaConversasCard pq o markup
|
||||||
eh simplificado pra placeholder visual). -->
|
eh simplificado pra placeholder visual). -->
|
||||||
@@ -342,7 +368,7 @@ watch(() => tenantStore.activeTenantId, async (newTid, oldTid) => {
|
|||||||
v-else-if="!byKanban[col.key].length"
|
v-else-if="!byKanban[col.key].length"
|
||||||
class="mw-col__empty flex flex-col items-center gap-1.5 px-3 py-6 text-[var(--m-text-faint)] text-[0.72rem] italic [&>i]:text-2xl [&>i]:opacity-40"
|
class="mw-col__empty flex flex-col items-center gap-1.5 px-3 py-6 text-[var(--m-text-faint)] text-[0.72rem] italic [&>i]:text-2xl [&>i]:opacity-40"
|
||||||
>
|
>
|
||||||
<i class="pi pi-comments" />
|
<i class="pi pi-comments" aria-hidden="true" />
|
||||||
<span>Nenhuma conversa</span>
|
<span>Nenhuma conversa</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
* - click — pai chama drawerStore.openForThread
|
* - click — pai chama drawerStore.openForThread
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { channelIcon } from '@/utils/channelMeta';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
thread: { type: Object, required: true },
|
thread: { type: Object, required: true },
|
||||||
tags: { type: Array, default: () => [] },
|
tags: { type: Array, default: () => [] },
|
||||||
@@ -35,14 +37,6 @@ const props = defineProps({
|
|||||||
memberNameMap: { type: Object, default: () => ({}) }
|
memberNameMap: { type: Object, default: () => ({}) }
|
||||||
});
|
});
|
||||||
defineEmits(['click']);
|
defineEmits(['click']);
|
||||||
|
|
||||||
// Helpers internos — duplicados do pai pra manter componente autocontido.
|
|
||||||
// channelIcon eh trivial. Se aparecer um terceiro consumidor (alem do pai
|
|
||||||
// e este), vale extrair pra useChannelMeta.
|
|
||||||
function channelIcon(ch) {
|
|
||||||
const map = { whatsapp: 'pi-whatsapp', sms: 'pi-comment', email: 'pi-envelope' };
|
|
||||||
return map[ch] || 'pi-comment';
|
|
||||||
}
|
|
||||||
function truncate(s, n = 80) {
|
function truncate(s, n = 80) {
|
||||||
if (!s) return '';
|
if (!s) return '';
|
||||||
const str = String(s).replace(/\s+/g, ' ').trim();
|
const str = String(s).replace(/\s+/g, ' ').trim();
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
* - unassignedCount: threads sem atribuicao
|
* - unassignedCount: threads sem atribuicao
|
||||||
*/
|
*/
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { CHANNEL_OPTIONS, channelIcon } from '@/utils/channelMeta';
|
||||||
|
import { KANBAN_COLUMNS } from '@/composables/useConversations';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
filters: { type: Object, required: true },
|
filters: { type: Object, required: true },
|
||||||
@@ -36,25 +38,6 @@ const props = defineProps({
|
|||||||
unassignedCount: { type: Number, default: 0 }
|
unassignedCount: { type: Number, default: 0 }
|
||||||
});
|
});
|
||||||
|
|
||||||
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' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const CHANNEL_OPTIONS = [
|
|
||||||
{ label: 'Todos', value: null },
|
|
||||||
{ label: 'WhatsApp', value: 'whatsapp' },
|
|
||||||
{ label: 'SMS', value: 'sms' },
|
|
||||||
{ label: 'E-mail', value: 'email' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function channelIcon(ch) {
|
|
||||||
const map = { whatsapp: 'pi-whatsapp', sms: 'pi-comment', email: 'pi-envelope' };
|
|
||||||
return map[ch] || 'pi-comment';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal — controla render do footer "Limpar filtros"
|
// Internal — controla render do footer "Limpar filtros"
|
||||||
const hasActiveFilters = computed(() => {
|
const hasActiveFilters = computed(() => {
|
||||||
const f = props.filters;
|
const f = props.filters;
|
||||||
@@ -76,7 +59,7 @@ function clearAllFilters() {
|
|||||||
v-if="unlinkedCount > 0"
|
v-if="unlinkedCount > 0"
|
||||||
class="mw-alert flex gap-2.5 mx-3 mt-3 last:mb-3 p-3 rounded-[10px] border border-[rgba(251,191,36,0.3)] bg-[rgba(251,191,36,0.05)] text-[rgb(251,191,36)] flex-shrink-0 shadow-[0_2px_8px_rgba(0,0,0,0.12)] [&>i]:text-[0.85rem] [&>i]:mt-0.5"
|
class="mw-alert flex gap-2.5 mx-3 mt-3 last:mb-3 p-3 rounded-[10px] border border-[rgba(251,191,36,0.3)] bg-[rgba(251,191,36,0.05)] text-[rgb(251,191,36)] flex-shrink-0 shadow-[0_2px_8px_rgba(0,0,0,0.12)] [&>i]:text-[0.85rem] [&>i]:mt-0.5"
|
||||||
>
|
>
|
||||||
<i class="pi pi-exclamation-circle" />
|
<i class="pi pi-exclamation-circle" aria-hidden="true" />
|
||||||
<div>
|
<div>
|
||||||
<div class="mw-alert__title text-[0.78rem] font-semibold">{{ unlinkedCount }} sem paciente vinculado</div>
|
<div class="mw-alert__title text-[0.78rem] font-semibold">{{ unlinkedCount }} sem paciente vinculado</div>
|
||||||
<div class="mw-alert__hint text-[0.7rem] text-[var(--m-text-muted)] mt-0.5">Números de telefone que não batem com pacientes cadastrados.</div>
|
<div class="mw-alert__hint text-[0.7rem] text-[var(--m-text-muted)] mt-0.5">Números de telefone que não batem com pacientes cadastrados.</div>
|
||||||
@@ -86,7 +69,7 @@ function clearAllFilters() {
|
|||||||
<!-- Filtros rápidos -->
|
<!-- Filtros rápidos -->
|
||||||
<div class="mw-w mw-w--side mx-3 mt-3 first:mt-3 flex-shrink-0 shadow-[0_2px_8px_rgba(0,0,0,0.12)] bg-[var(--m-bg-medium)] border border-[var(--m-border)] rounded-xl p-3">
|
<div class="mw-w mw-w--side mx-3 mt-3 first:mt-3 flex-shrink-0 shadow-[0_2px_8px_rgba(0,0,0,0.12)] bg-[var(--m-bg-medium)] border border-[var(--m-border)] rounded-xl p-3">
|
||||||
<div class="mw-w__head flex items-center justify-between mb-2.5">
|
<div class="mw-w__head flex items-center justify-between mb-2.5">
|
||||||
<span class="mw-w__title inline-flex items-center gap-1.5 text-[0.78rem] font-semibold [&>i]:text-[var(--m-text-muted)] [&>i]:text-[0.78rem]"><i class="pi pi-filter" /> Filtros rápidos</span>
|
<span class="mw-w__title inline-flex items-center gap-1.5 text-[0.78rem] font-semibold [&>i]:text-[var(--m-text-muted)] [&>i]:text-[0.78rem]"><i class="pi pi-filter" aria-hidden="true" /> Filtros rápidos</span>
|
||||||
<button
|
<button
|
||||||
v-if="filters.unreadOnly"
|
v-if="filters.unreadOnly"
|
||||||
class="mw-side__clear-inline w-[18px] h-[18px] grid place-items-center bg-transparent border border-[color-mix(in_srgb,rgb(220,38,38)_30%,var(--m-border))] text-[rgb(220,38,38)] rounded cursor-pointer [font-family:inherit] transition-[background-color,border-color] duration-[140ms] hover:bg-[rgba(220,38,38,0.10)] hover:border-[rgba(220,38,38,0.55)] [&>i]:text-[0.6rem]"
|
class="mw-side__clear-inline w-[18px] h-[18px] grid place-items-center bg-transparent border border-[color-mix(in_srgb,rgb(220,38,38)_30%,var(--m-border))] text-[rgb(220,38,38)] rounded cursor-pointer [font-family:inherit] transition-[background-color,border-color] duration-[140ms] hover:bg-[rgba(220,38,38,0.10)] hover:border-[rgba(220,38,38,0.55)] [&>i]:text-[0.6rem]"
|
||||||
@@ -94,7 +77,7 @@ function clearAllFilters() {
|
|||||||
aria-label="Limpar filtro de não lidas"
|
aria-label="Limpar filtro de não lidas"
|
||||||
@click="filters.unreadOnly = false"
|
@click="filters.unreadOnly = false"
|
||||||
>
|
>
|
||||||
<i class="pi pi-times" />
|
<i class="pi pi-times" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mw-side__list flex flex-col gap-1">
|
<div class="mw-side__list flex flex-col gap-1">
|
||||||
@@ -104,7 +87,7 @@ function clearAllFilters() {
|
|||||||
:aria-pressed="!filters.unreadOnly"
|
:aria-pressed="!filters.unreadOnly"
|
||||||
@click="filters.unreadOnly = false"
|
@click="filters.unreadOnly = false"
|
||||||
>
|
>
|
||||||
<i class="pi pi-list" />
|
<i class="pi pi-list" aria-hidden="true" />
|
||||||
<span>Todas</span>
|
<span>Todas</span>
|
||||||
<span class="mw-side__count text-[0.65rem] font-semibold text-[var(--m-text-muted)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center">{{ summary.total }}</span>
|
<span class="mw-side__count text-[0.65rem] font-semibold text-[var(--m-text-muted)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center">{{ summary.total }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -114,7 +97,7 @@ function clearAllFilters() {
|
|||||||
:aria-pressed="filters.unreadOnly"
|
:aria-pressed="filters.unreadOnly"
|
||||||
@click="filters.unreadOnly = !filters.unreadOnly"
|
@click="filters.unreadOnly = !filters.unreadOnly"
|
||||||
>
|
>
|
||||||
<i class="pi pi-bell" />
|
<i class="pi pi-bell" aria-hidden="true" />
|
||||||
<span>Não lidas</span>
|
<span>Não lidas</span>
|
||||||
<span
|
<span
|
||||||
class="mw-side__count text-[0.65rem] font-semibold text-[var(--m-text-muted)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center"
|
class="mw-side__count text-[0.65rem] font-semibold text-[var(--m-text-muted)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center"
|
||||||
@@ -127,7 +110,7 @@ function clearAllFilters() {
|
|||||||
<!-- Atribuição -->
|
<!-- Atribuição -->
|
||||||
<div class="mw-w mw-w--side mx-3 mt-3 flex-shrink-0 shadow-[0_2px_8px_rgba(0,0,0,0.12)] bg-[var(--m-bg-medium)] border border-[var(--m-border)] rounded-xl p-3">
|
<div class="mw-w mw-w--side mx-3 mt-3 flex-shrink-0 shadow-[0_2px_8px_rgba(0,0,0,0.12)] bg-[var(--m-bg-medium)] border border-[var(--m-border)] rounded-xl p-3">
|
||||||
<div class="mw-w__head flex items-center justify-between mb-2.5">
|
<div class="mw-w__head flex items-center justify-between mb-2.5">
|
||||||
<span class="mw-w__title inline-flex items-center gap-1.5 text-[0.78rem] font-semibold [&>i]:text-[var(--m-text-muted)] [&>i]:text-[0.78rem]"><i class="pi pi-user" /> Atribuição</span>
|
<span class="mw-w__title inline-flex items-center gap-1.5 text-[0.78rem] font-semibold [&>i]:text-[var(--m-text-muted)] [&>i]:text-[0.78rem]"><i class="pi pi-user" aria-hidden="true" /> Atribuição</span>
|
||||||
<button
|
<button
|
||||||
v-if="filters.assigned"
|
v-if="filters.assigned"
|
||||||
class="mw-side__clear-inline w-[18px] h-[18px] grid place-items-center bg-transparent border border-[color-mix(in_srgb,rgb(220,38,38)_30%,var(--m-border))] text-[rgb(220,38,38)] rounded cursor-pointer [font-family:inherit] transition-[background-color,border-color] duration-[140ms] hover:bg-[rgba(220,38,38,0.10)] hover:border-[rgba(220,38,38,0.55)] [&>i]:text-[0.6rem]"
|
class="mw-side__clear-inline w-[18px] h-[18px] grid place-items-center bg-transparent border border-[color-mix(in_srgb,rgb(220,38,38)_30%,var(--m-border))] text-[rgb(220,38,38)] rounded cursor-pointer [font-family:inherit] transition-[background-color,border-color] duration-[140ms] hover:bg-[rgba(220,38,38,0.10)] hover:border-[rgba(220,38,38,0.55)] [&>i]:text-[0.6rem]"
|
||||||
@@ -135,7 +118,7 @@ function clearAllFilters() {
|
|||||||
aria-label="Limpar filtro de atribuição"
|
aria-label="Limpar filtro de atribuição"
|
||||||
@click="filters.assigned = null"
|
@click="filters.assigned = null"
|
||||||
>
|
>
|
||||||
<i class="pi pi-times" />
|
<i class="pi pi-times" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mw-side__list flex flex-col gap-1">
|
<div class="mw-side__list flex flex-col gap-1">
|
||||||
@@ -145,7 +128,7 @@ function clearAllFilters() {
|
|||||||
:aria-pressed="!filters.assigned"
|
:aria-pressed="!filters.assigned"
|
||||||
@click="filters.assigned = null"
|
@click="filters.assigned = null"
|
||||||
>
|
>
|
||||||
<i class="pi pi-list" />
|
<i class="pi pi-list" aria-hidden="true" />
|
||||||
<span>Todas</span>
|
<span>Todas</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -154,7 +137,7 @@ function clearAllFilters() {
|
|||||||
:aria-pressed="filters.assigned === 'me'"
|
:aria-pressed="filters.assigned === 'me'"
|
||||||
@click="filters.assigned = 'me'"
|
@click="filters.assigned = 'me'"
|
||||||
>
|
>
|
||||||
<i class="pi pi-user" />
|
<i class="pi pi-user" aria-hidden="true" />
|
||||||
<span>Minhas</span>
|
<span>Minhas</span>
|
||||||
<span class="mw-side__count text-[0.65rem] font-semibold text-[var(--m-text-muted)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center">{{ mineCount }}</span>
|
<span class="mw-side__count text-[0.65rem] font-semibold text-[var(--m-text-muted)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center">{{ mineCount }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -164,7 +147,7 @@ function clearAllFilters() {
|
|||||||
:aria-pressed="filters.assigned === 'unassigned'"
|
:aria-pressed="filters.assigned === 'unassigned'"
|
||||||
@click="filters.assigned = 'unassigned'"
|
@click="filters.assigned = 'unassigned'"
|
||||||
>
|
>
|
||||||
<i class="pi pi-user-minus" />
|
<i class="pi pi-user-minus" aria-hidden="true" />
|
||||||
<span>Não atribuídas</span>
|
<span>Não atribuídas</span>
|
||||||
<span
|
<span
|
||||||
class="mw-side__count text-[0.65rem] font-semibold text-[var(--m-text-muted)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center"
|
class="mw-side__count text-[0.65rem] font-semibold text-[var(--m-text-muted)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center"
|
||||||
@@ -177,7 +160,7 @@ function clearAllFilters() {
|
|||||||
<!-- Por status (kanban resumo — display-only, sem X) -->
|
<!-- Por status (kanban resumo — display-only, sem X) -->
|
||||||
<div class="mw-w mw-w--side mx-3 mt-3 flex-shrink-0 shadow-[0_2px_8px_rgba(0,0,0,0.12)] bg-[var(--m-bg-medium)] border border-[var(--m-border)] rounded-xl p-3">
|
<div class="mw-w mw-w--side mx-3 mt-3 flex-shrink-0 shadow-[0_2px_8px_rgba(0,0,0,0.12)] bg-[var(--m-bg-medium)] border border-[var(--m-border)] rounded-xl p-3">
|
||||||
<div class="mw-w__head flex items-center justify-between mb-2.5">
|
<div class="mw-w__head flex items-center justify-between mb-2.5">
|
||||||
<span class="mw-w__title inline-flex items-center gap-1.5 text-[0.78rem] font-semibold [&>i]:text-[var(--m-text-muted)] [&>i]:text-[0.78rem]"><i class="pi pi-chart-bar" /> Por status</span>
|
<span class="mw-w__title inline-flex items-center gap-1.5 text-[0.78rem] font-semibold [&>i]:text-[var(--m-text-muted)] [&>i]:text-[0.78rem]"><i class="pi pi-chart-bar" aria-hidden="true" /> Por status</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mw-side__list flex flex-col gap-1">
|
<div class="mw-side__list flex flex-col gap-1">
|
||||||
<div
|
<div
|
||||||
@@ -186,7 +169,7 @@ function clearAllFilters() {
|
|||||||
class="mw-side__row flex items-center gap-2.5 px-2.5 py-1.5 rounded-[10px] border border-[var(--m-border)] bg-[var(--m-bg-medium)] text-[0.78rem] [&>span:first-of-type]:flex-1 [&>i]:text-[0.75rem]"
|
class="mw-side__row flex items-center gap-2.5 px-2.5 py-1.5 rounded-[10px] border border-[var(--m-border)] bg-[var(--m-bg-medium)] text-[0.78rem] [&>span:first-of-type]:flex-1 [&>i]:text-[0.75rem]"
|
||||||
:class="`is-${col.color}`"
|
:class="`is-${col.color}`"
|
||||||
>
|
>
|
||||||
<i :class="col.icon" />
|
<i :class="col.icon" aria-hidden="true" />
|
||||||
<span>{{ col.label }}</span>
|
<span>{{ col.label }}</span>
|
||||||
<span class="mw-side__count text-[0.65rem] font-semibold text-[var(--m-text-muted)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center">{{ summary[col.key] || 0 }}</span>
|
<span class="mw-side__count text-[0.65rem] font-semibold text-[var(--m-text-muted)] bg-[var(--m-bg-medium)] px-1.5 py-px rounded-full min-w-[22px] text-center">{{ summary[col.key] || 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +179,7 @@ function clearAllFilters() {
|
|||||||
<!-- Canais -->
|
<!-- Canais -->
|
||||||
<div class="mw-w mw-w--side mx-3 mt-3 last:mb-3 flex-shrink-0 shadow-[0_2px_8px_rgba(0,0,0,0.12)] bg-[var(--m-bg-medium)] border border-[var(--m-border)] rounded-xl p-3">
|
<div class="mw-w mw-w--side mx-3 mt-3 last:mb-3 flex-shrink-0 shadow-[0_2px_8px_rgba(0,0,0,0.12)] bg-[var(--m-bg-medium)] border border-[var(--m-border)] rounded-xl p-3">
|
||||||
<div class="mw-w__head flex items-center justify-between mb-2.5">
|
<div class="mw-w__head flex items-center justify-between mb-2.5">
|
||||||
<span class="mw-w__title inline-flex items-center gap-1.5 text-[0.78rem] font-semibold [&>i]:text-[var(--m-text-muted)] [&>i]:text-[0.78rem]"><i class="pi pi-send" /> Canais</span>
|
<span class="mw-w__title inline-flex items-center gap-1.5 text-[0.78rem] font-semibold [&>i]:text-[var(--m-text-muted)] [&>i]:text-[0.78rem]"><i class="pi pi-send" aria-hidden="true" /> Canais</span>
|
||||||
<button
|
<button
|
||||||
v-if="filters.channel"
|
v-if="filters.channel"
|
||||||
class="mw-side__clear-inline w-[18px] h-[18px] grid place-items-center bg-transparent border border-[color-mix(in_srgb,rgb(220,38,38)_30%,var(--m-border))] text-[rgb(220,38,38)] rounded cursor-pointer [font-family:inherit] transition-[background-color,border-color] duration-[140ms] hover:bg-[rgba(220,38,38,0.10)] hover:border-[rgba(220,38,38,0.55)] [&>i]:text-[0.6rem]"
|
class="mw-side__clear-inline w-[18px] h-[18px] grid place-items-center bg-transparent border border-[color-mix(in_srgb,rgb(220,38,38)_30%,var(--m-border))] text-[rgb(220,38,38)] rounded cursor-pointer [font-family:inherit] transition-[background-color,border-color] duration-[140ms] hover:bg-[rgba(220,38,38,0.10)] hover:border-[rgba(220,38,38,0.55)] [&>i]:text-[0.6rem]"
|
||||||
@@ -204,7 +187,7 @@ function clearAllFilters() {
|
|||||||
aria-label="Limpar filtro de canal"
|
aria-label="Limpar filtro de canal"
|
||||||
@click="filters.channel = null"
|
@click="filters.channel = null"
|
||||||
>
|
>
|
||||||
<i class="pi pi-times" />
|
<i class="pi pi-times" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mw-side__list flex flex-col gap-1">
|
<div class="mw-side__list flex flex-col gap-1">
|
||||||
@@ -216,8 +199,8 @@ function clearAllFilters() {
|
|||||||
:aria-pressed="filters.channel === opt.value"
|
:aria-pressed="filters.channel === opt.value"
|
||||||
@click="filters.channel = opt.value"
|
@click="filters.channel = opt.value"
|
||||||
>
|
>
|
||||||
<i v-if="opt.value" :class="['pi', channelIcon(opt.value)]" />
|
<i v-if="opt.value" :class="['pi', channelIcon(opt.value)]" aria-hidden="true" />
|
||||||
<i v-else class="pi pi-list" />
|
<i v-else class="pi pi-list" aria-hidden="true" />
|
||||||
<span>{{ opt.label }}</span>
|
<span>{{ opt.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,7 +217,7 @@ function clearAllFilters() {
|
|||||||
class="mw-side__clear-all w-full inline-flex items-center justify-center gap-2 px-3 py-2.5 bg-[var(--m-bg-medium)] border border-[var(--m-border)] text-[var(--m-text)] rounded-[10px] cursor-pointer [font-family:inherit] text-[0.78rem] font-semibold transition-[background-color,border-color,color] duration-[140ms] hover:bg-[var(--m-bg-soft-hover)] hover:border-[var(--m-border-strong)] hover:text-[var(--m-text)] [&>i]:text-[0.78rem] [&>i]:text-[var(--m-text-muted)] [&>i]:transition-colors [&>i]:duration-[140ms] hover:[&>i]:text-[var(--m-text)]"
|
class="mw-side__clear-all w-full inline-flex items-center justify-center gap-2 px-3 py-2.5 bg-[var(--m-bg-medium)] border border-[var(--m-border)] text-[var(--m-text)] rounded-[10px] cursor-pointer [font-family:inherit] text-[0.78rem] font-semibold transition-[background-color,border-color,color] duration-[140ms] hover:bg-[var(--m-bg-soft-hover)] hover:border-[var(--m-border-strong)] hover:text-[var(--m-text)] [&>i]:text-[0.78rem] [&>i]:text-[var(--m-text-muted)] [&>i]:transition-colors [&>i]:duration-[140ms] hover:[&>i]:text-[var(--m-text)]"
|
||||||
@click="clearAllFilters"
|
@click="clearAllFilters"
|
||||||
>
|
>
|
||||||
<i class="pi pi-filter-slash" />
|
<i class="pi pi-filter-slash" aria-hidden="true" />
|
||||||
<span>Limpar filtros</span>
|
<span>Limpar filtros</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* channelMeta — fonte unica de canais de comunicacao (WhatsApp/SMS/E-mail).
|
||||||
|
* --------------------------------------------------------------------
|
||||||
|
* Antes, os 3 consumidores (MelissaConversas parent, Sidebar, Card)
|
||||||
|
* tinham copias locais de CHANNEL_OPTIONS + channelIcon. Drift risk
|
||||||
|
* pequeno mas existia — adicionar 'telegram' exigia mexer em N lugares.
|
||||||
|
* Aqui centraliza.
|
||||||
|
*
|
||||||
|
* NAO eh um composable Vue (no useXxx) porque sao constantes puras
|
||||||
|
* e funcoes sem reactivity — arquivo utility eh suficiente.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const CHANNEL_OPTIONS = Object.freeze([
|
||||||
|
Object.freeze({ label: 'Todos', value: null }),
|
||||||
|
Object.freeze({ label: 'WhatsApp', value: 'whatsapp' }),
|
||||||
|
Object.freeze({ label: 'SMS', value: 'sms' }),
|
||||||
|
Object.freeze({ label: 'E-mail', value: 'email' })
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ICON_MAP = Object.freeze({
|
||||||
|
whatsapp: 'pi-whatsapp',
|
||||||
|
sms: 'pi-comment',
|
||||||
|
email: 'pi-envelope'
|
||||||
|
});
|
||||||
|
|
||||||
|
export function channelIcon(channel) {
|
||||||
|
return ICON_MAP[channel] || 'pi-comment';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function channelLabel(channel) {
|
||||||
|
if (!channel) return 'Todos';
|
||||||
|
const opt = CHANNEL_OPTIONS.find((o) => o.value === channel);
|
||||||
|
return opt?.label || channel;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user