Melissa: 6 Pages aplicando blueprint + dialogs unificados + Conversa estilo WhatsApp
Sprint F (05-06). Blueprint tabular aplicado nas 6 paginas restantes;
dialogs harmonizados (FloatLabel + IconField + variant=filled + section
dividers, espelhando PatientsCadastroPage Identidade); ConversationDrawer
repaginado pra visual estilo WhatsApp.
Pages refatoradas (cada uma com subheader, sidebar __scroll + __footer
fixo "Limpar filtros", Xs inline pra zerar filtro individual, mobile
drawer com sticky footer):
- MelissaCompromissos: blueprint mantendo row design original (color
stripe + name + badges + descricao + meta inline). Filtros Status
(Ativos/Inativos) + Tipo (Nativos/Meus). Coluna Acoes frozen 140px
com toggle+pencil+trash.
- MelissaGrupos / MelissaTags: pattern completo + dialog "Pacientes
do grupo/tag" com lista vinculada via patient_group_patient /
patient_patient_tag. Avatar primary nos pacientes, header colorido
com cor da entidade, X de fechar igual .mc-close. Dialog de
criar/editar com FloatLabel + section dividers.
- MelissaMedicos: blueprint + dialog "Pacientes encaminhados" usando
cor primary do tema (medicos nao tem cor propria); dialog de
criar/editar com 4 secoes (Identificacao/Contato/Localizacao/Obs)
espelhando PatientsCadastroPage. Service ja tinha
fetchPatientsByMedicoNome (ILIKE em encaminhado_por).
- MelissaConversas: subheader, sidebar com bg-soft + border-right e
cards com sombra (mw-w--side), Limpar filtros global no footer fixo
(fix bug: filters era ref({...}) e eu lia filters.search direto, agora
usa .value), alerta de unlinked movido pro topo, kanban mobile com
min-height nas colunas pra mostrar mensagens.
- MelissaRecorrencias: subheader, button list de status (Ativas verde/
Encerradas vermelho/Todas) substitui SelectButton, busca por nome do
paciente, footer Limpar filtros, X inline no filtro Status.
ConversationDrawer redesign (WhatsApp-style):
- Header com avatar circular primary + iniciais + numero formatado
- Container de mensagens com bg "papel de parede" (color-mix com bege
esverdeado WA + radial-gradient pattern)
- Bolhas com cantos certos (top-left ou top-right zerado simulando
tail), sombra sutil, cores autenticas (#d9fdd3 light/#005c4b dark
outbound; #fff/#202c33 inbound), detecao dark via :global
- Time HH:MM + status overlay no canto inferior direito DENTRO do
balao; checks azuis quando lida (#53bdeb)
- Compose pill rounded-full + botao Send circular verde #00a884
- Removido fmtDateTime obsoleto (substituido por fmtTimeOnly)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -336,12 +336,19 @@ const KANBAN_COLUMNS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// ── Formatters ─────────────────────────────────────────────
|
// ── Formatters ─────────────────────────────────────────────
|
||||||
function fmtDateTime(iso) {
|
// HH:MM curto pra exibir dentro da bolha (estilo WhatsApp).
|
||||||
|
function fmtTimeOnly(iso) {
|
||||||
if (!iso) return '';
|
if (!iso) return '';
|
||||||
return new Date(iso).toLocaleString('pt-BR', {
|
return new Date(iso).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
|
||||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
}
|
||||||
hour: '2-digit', minute: '2-digit'
|
|
||||||
});
|
// Iniciais pro avatar circular do header.
|
||||||
|
function iniciaisChat(name) {
|
||||||
|
if (!name) return '?';
|
||||||
|
const parts = String(name).trim().split(/\s+/).filter(Boolean);
|
||||||
|
if (!parts.length) return '?';
|
||||||
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||||
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function channelIcon(ch) {
|
function channelIcon(ch) {
|
||||||
@@ -699,11 +706,16 @@ function insertEmoji(emoji) {
|
|||||||
<ConfirmDialog group="conversation-drawer" />
|
<ConfirmDialog group="conversation-drawer" />
|
||||||
<Drawer v-model:visible="isOpen" position="right" class="!w-full md:!w-[520px]">
|
<Drawer v-model:visible="isOpen" position="right" class="!w-full md:!w-[520px]">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div v-if="store.thread" class="flex items-center gap-2">
|
<div v-if="store.thread" class="flex items-center gap-3 min-w-0">
|
||||||
<i :class="['pi', channelIcon(store.thread.channel)]" />
|
<span class="cd-avatar">
|
||||||
<div class="flex flex-col">
|
{{ iniciaisChat(contactLabel()) }}
|
||||||
<span class="font-semibold">{{ contactLabel() }}</span>
|
</span>
|
||||||
<span v-if="store.thread.contact_number" class="text-xs text-[var(--text-color-secondary)]">{{ store.thread.contact_number }}</span>
|
<div class="flex flex-col min-w-0">
|
||||||
|
<span class="font-semibold truncate text-[0.95rem]">{{ contactLabel() }}</span>
|
||||||
|
<span v-if="store.thread.contact_number" class="text-[0.72rem] text-[var(--text-color-secondary)] flex items-center gap-1.5">
|
||||||
|
<i :class="['pi', channelIcon(store.thread.channel), 'text-[0.65rem]']" />
|
||||||
|
<span class="font-mono">{{ formatPhoneShort(store.thread.contact_number) }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1030,8 +1042,8 @@ function insertEmoji(emoji) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mensagens -->
|
<!-- Mensagens — container com bg WhatsApp-like -->
|
||||||
<div ref="messagesContainerRef" class="flex-1 overflow-y-auto flex flex-col gap-2 p-1">
|
<div ref="messagesContainerRef" class="cd-msgs flex-1 overflow-y-auto flex flex-col gap-1.5 p-3">
|
||||||
<div v-if="store.loading" class="text-xs text-[var(--text-color-secondary)] text-center py-6">
|
<div v-if="store.loading" class="text-xs text-[var(--text-color-secondary)] text-center py-6">
|
||||||
<i class="pi pi-spin pi-spinner mr-2" />Carregando mensagens...
|
<i class="pi pi-spin pi-spinner mr-2" />Carregando mensagens...
|
||||||
</div>
|
</div>
|
||||||
@@ -1042,12 +1054,12 @@ function insertEmoji(emoji) {
|
|||||||
<div
|
<div
|
||||||
v-for="m in store.messages"
|
v-for="m in store.messages"
|
||||||
:key="m.id"
|
:key="m.id"
|
||||||
class="flex flex-col gap-0.5"
|
class="cd-bubble-wrap"
|
||||||
:class="m.direction === 'inbound' ? 'items-start' : 'items-end'"
|
:class="m.direction === 'inbound' ? 'cd-bubble-wrap--in' : 'cd-bubble-wrap--out'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="max-w-[85%] px-3 py-2 rounded-lg text-sm whitespace-pre-wrap break-words"
|
class="cd-bubble"
|
||||||
:class="m.direction === 'inbound' ? 'bg-[var(--surface-ground)] rounded-tl-none' : 'bg-emerald-500/10 text-emerald-900 dark:text-emerald-100 rounded-tr-none'"
|
:class="m.direction === 'inbound' ? 'cd-bubble--in' : 'cd-bubble--out'"
|
||||||
>
|
>
|
||||||
<template v-if="m.media_url">
|
<template v-if="m.media_url">
|
||||||
<!-- Loading enquanto resolve signed URL -->
|
<!-- Loading enquanto resolve signed URL -->
|
||||||
@@ -1061,7 +1073,7 @@ function insertEmoji(emoji) {
|
|||||||
:preview="true"
|
:preview="true"
|
||||||
alt="imagem"
|
alt="imagem"
|
||||||
imageClass="max-w-full rounded-md cursor-zoom-in"
|
imageClass="max-w-full rounded-md cursor-zoom-in"
|
||||||
class="mb-1 block"
|
class="mb-1 block cd-bubble__media"
|
||||||
/>
|
/>
|
||||||
<audio
|
<audio
|
||||||
v-else-if="isAudio(m.media_mime)"
|
v-else-if="isAudio(m.media_mime)"
|
||||||
@@ -1074,7 +1086,7 @@ function insertEmoji(emoji) {
|
|||||||
v-else-if="isVideo(m.media_mime)"
|
v-else-if="isVideo(m.media_mime)"
|
||||||
:src="mediaUrls[m.id]"
|
:src="mediaUrls[m.id]"
|
||||||
controls
|
controls
|
||||||
class="max-w-full rounded-md mb-1"
|
class="max-w-full rounded-md mb-1 cd-bubble__media"
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
v-else
|
v-else
|
||||||
@@ -1088,18 +1100,19 @@ function insertEmoji(emoji) {
|
|||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="m.body">{{ m.body }}</div>
|
<div v-if="m.body" class="cd-bubble__body">{{ m.body }}</div>
|
||||||
</div>
|
<!-- Time + status overlay no canto inferior direito (estilo WhatsApp) -->
|
||||||
<div class="text-[0.65rem] text-[var(--text-color-secondary)] opacity-75 px-1 flex items-center gap-1">
|
<div class="cd-bubble__meta">
|
||||||
<span>{{ fmtDateTime(m.created_at) }}</span>
|
<span>{{ fmtTimeOnly(m.created_at) }}</span>
|
||||||
<span v-if="m.direction === 'outbound'" class="flex items-center">
|
<span v-if="m.direction === 'outbound'" class="cd-bubble__status">
|
||||||
<i v-if="m.delivery_status === 'read'" class="pi pi-check-double text-sky-500" v-tooltip.top="'Lida'" />
|
<i v-if="m.delivery_status === 'read'" class="pi pi-check-double cd-bubble__status--read" v-tooltip.top="'Lida'" />
|
||||||
<i v-else-if="m.delivery_status === 'delivered'" class="pi pi-check-double" v-tooltip.top="'Entregue'" />
|
<i v-else-if="m.delivery_status === 'delivered'" class="pi pi-check-double" v-tooltip.top="'Entregue'" />
|
||||||
<i v-else-if="m.delivery_status === 'sent'" class="pi pi-check" v-tooltip.top="'Enviada'" />
|
<i v-else-if="m.delivery_status === 'sent'" class="pi pi-check" v-tooltip.top="'Enviada'" />
|
||||||
<i v-else-if="m.delivery_status === 'failed'" class="pi pi-exclamation-circle text-red-500" v-tooltip.top="'Falhou'" />
|
<i v-else-if="m.delivery_status === 'failed'" class="pi pi-exclamation-circle text-red-500" v-tooltip.top="'Falhou'" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="m.direction === 'inbound' && !m.patient_id" class="italic">· número não vinculado</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span v-if="m.direction === 'inbound' && !m.patient_id" class="cd-bubble__unlinked">não vinculado</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1142,34 +1155,48 @@ function insertEmoji(emoji) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Compose -->
|
<!-- Compose — barra estilo WhatsApp (input pill + send circular) -->
|
||||||
<div v-if="store.thread.channel === 'whatsapp' && store.thread.contact_number" class="border-t border-[var(--surface-border)] pt-2 flex flex-col gap-2">
|
<div v-if="store.thread.channel === 'whatsapp' && store.thread.contact_number" class="cd-compose">
|
||||||
<div class="flex items-end gap-2">
|
<div class="cd-compose__row">
|
||||||
|
<button
|
||||||
|
class="cd-compose__icon-btn"
|
||||||
|
v-tooltip.top="'Emoji'"
|
||||||
|
@click="toggleEmojiPopover"
|
||||||
|
>
|
||||||
|
<i class="pi pi-face-smile" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="cd-compose__icon-btn"
|
||||||
|
v-tooltip.top="'Templates'"
|
||||||
|
@click="openTemplatesPopover"
|
||||||
|
>
|
||||||
|
<i class="pi pi-bookmark" />
|
||||||
|
</button>
|
||||||
<Textarea
|
<Textarea
|
||||||
ref="composeTextareaRef"
|
ref="composeTextareaRef"
|
||||||
v-model="composeText"
|
v-model="composeText"
|
||||||
autoResize
|
autoResize
|
||||||
rows="1"
|
rows="1"
|
||||||
placeholder="Digite sua mensagem... (Enter envia, Shift+Enter quebra linha)"
|
placeholder="Digite uma mensagem"
|
||||||
class="flex-1 !text-sm !resize-none"
|
class="cd-compose__input flex-1"
|
||||||
:disabled="store.sending"
|
:disabled="store.sending"
|
||||||
:maxlength="4000"
|
:maxlength="4000"
|
||||||
@keydown="onComposeKeydown"
|
@keydown="onComposeKeydown"
|
||||||
/>
|
/>
|
||||||
<Button
|
<button
|
||||||
icon="pi pi-send"
|
class="cd-compose__send"
|
||||||
severity="success"
|
:class="{ 'is-disabled': !composeText.trim() || store.sending }"
|
||||||
class="!w-10 !h-10 shrink-0"
|
|
||||||
:loading="store.sending"
|
|
||||||
:disabled="!composeText.trim() || store.sending"
|
:disabled="!composeText.trim() || store.sending"
|
||||||
v-tooltip.top="'Enviar (Enter)'"
|
v-tooltip.top="'Enviar (Enter)'"
|
||||||
@click="sendMessage"
|
@click="sendMessage"
|
||||||
/>
|
>
|
||||||
|
<i v-if="store.sending" class="pi pi-spin pi-spinner" />
|
||||||
|
<i v-else class="pi pi-send" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 text-[var(--text-color-secondary)]">
|
<div class="cd-compose__hint">
|
||||||
<Button icon="pi pi-bookmark" text rounded size="small" class="!w-8 !h-8" v-tooltip.top="'Templates'" @click="openTemplatesPopover" />
|
<span>Enter envia · Shift+Enter quebra linha</span>
|
||||||
<Button icon="pi pi-face-smile" text rounded size="small" class="!w-8 !h-8" v-tooltip.top="'Emoji'" @click="toggleEmojiPopover" />
|
<span class="ml-auto">{{ composeText.length }}/4000</span>
|
||||||
<span class="ml-auto text-[0.65rem] opacity-60">{{ composeText.length }}/4000</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-[0.7rem] text-[var(--text-color-secondary)] italic text-center py-2 border-t border-[var(--surface-border)]">
|
<div v-else class="text-[0.7rem] text-[var(--text-color-secondary)] italic text-center py-2 border-t border-[var(--surface-border)]">
|
||||||
@@ -1214,3 +1241,221 @@ function insertEmoji(emoji) {
|
|||||||
</Popover>
|
</Popover>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ─── Avatar do header (iniciais sobre primary) ─── */
|
||||||
|
.cd-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: var(--p-primary-color);
|
||||||
|
color: var(--p-primary-contrast-color, white);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Container de mensagens (bg estilo "papel de parede" WhatsApp) ─── */
|
||||||
|
/* Light: bege esverdeado clássico do WA. Dark: cinza profundo tipo
|
||||||
|
wallpaper de modo escuro. Adapta via CSS variable `--p-content-background`
|
||||||
|
pra harmonizar com o tema do app. */
|
||||||
|
.cd-msgs {
|
||||||
|
background-color: color-mix(in srgb, var(--p-content-background) 85%, #efeae2);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--p-text-color) 4%, transparent) 1px, transparent 0);
|
||||||
|
background-size: 18px 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0 -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Bolha (wrapper + content + meta) ─── */
|
||||||
|
.cd-bubble-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
.cd-bubble-wrap--in { align-self: flex-start; }
|
||||||
|
.cd-bubble-wrap--out { align-self: flex-end; align-items: flex-end; }
|
||||||
|
|
||||||
|
.cd-bubble {
|
||||||
|
position: relative;
|
||||||
|
padding: 6px 10px 18px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.42;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.13);
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
/* Inbound: surface-card adapta light/dark; canto top-left "achatado" vira o tail */
|
||||||
|
.cd-bubble--in {
|
||||||
|
background: var(--p-surface-0, #ffffff);
|
||||||
|
color: var(--p-text-color);
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
:global(.p-dark) .cd-bubble--in,
|
||||||
|
:global(html.dark) .cd-bubble--in,
|
||||||
|
:global([data-theme="dark"]) .cd-bubble--in,
|
||||||
|
.cd-bubble--in:where(.p-dark *, html.dark *, [data-theme="dark"] *) {
|
||||||
|
background: #202c33;
|
||||||
|
color: rgb(233, 237, 239);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Outbound: verde claro WA (light) / verde escuro WA (dark) */
|
||||||
|
.cd-bubble--out {
|
||||||
|
background: #d9fdd3;
|
||||||
|
color: rgb(17, 27, 33);
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
:global(.p-dark) .cd-bubble--out,
|
||||||
|
:global(html.dark) .cd-bubble--out,
|
||||||
|
:global([data-theme="dark"]) .cd-bubble--out,
|
||||||
|
.cd-bubble--out:where(.p-dark *, html.dark *, [data-theme="dark"] *) {
|
||||||
|
background: #005c4b;
|
||||||
|
color: rgb(233, 237, 239);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cd-bubble__body {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.cd-bubble__media {
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: -2px -4px 6px -4px;
|
||||||
|
max-width: calc(100% + 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Meta (HH:MM + status checks) overlay no canto inferior direito */
|
||||||
|
.cd-bubble__meta {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 4px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
pointer-events: auto;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.cd-bubble--in .cd-bubble__meta { color: rgba(0, 0, 0, 0.45); }
|
||||||
|
.cd-bubble--out .cd-bubble__meta { color: rgba(0, 0, 0, 0.55); }
|
||||||
|
:global(.p-dark) .cd-bubble--in .cd-bubble__meta,
|
||||||
|
:global(html.dark) .cd-bubble--in .cd-bubble__meta,
|
||||||
|
:global([data-theme="dark"]) .cd-bubble--in .cd-bubble__meta,
|
||||||
|
.cd-bubble--in:where(.p-dark *, html.dark *, [data-theme="dark"] *) .cd-bubble__meta {
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
:global(.p-dark) .cd-bubble--out .cd-bubble__meta,
|
||||||
|
:global(html.dark) .cd-bubble--out .cd-bubble__meta,
|
||||||
|
:global([data-theme="dark"]) .cd-bubble--out .cd-bubble__meta,
|
||||||
|
.cd-bubble--out:where(.p-dark *, html.dark *, [data-theme="dark"] *) .cd-bubble__meta {
|
||||||
|
color: rgba(233, 237, 239, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cd-bubble__status > i {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
.cd-bubble__status--read {
|
||||||
|
color: rgb(83, 189, 235) !important; /* azul WA quando lido */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cd-bubble__unlinked {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
color: var(--p-text-muted-color, var(--text-color-secondary));
|
||||||
|
font-style: italic;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Compose bar estilo WhatsApp ─── */
|
||||||
|
.cd-compose {
|
||||||
|
border-top: 1px solid var(--p-content-border-color, var(--surface-border));
|
||||||
|
padding: 8px 4px 6px;
|
||||||
|
background: color-mix(in srgb, var(--p-content-background) 80%, transparent);
|
||||||
|
}
|
||||||
|
.cd-compose__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.cd-compose__icon-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--p-text-muted-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background-color 140ms ease, color 140ms ease;
|
||||||
|
}
|
||||||
|
.cd-compose__icon-btn:hover {
|
||||||
|
background: var(--p-content-hover-background, color-mix(in srgb, var(--p-text-color) 8%, transparent));
|
||||||
|
color: var(--p-text-color);
|
||||||
|
}
|
||||||
|
.cd-compose__icon-btn > i { font-size: 1rem; }
|
||||||
|
|
||||||
|
.cd-compose__input :deep(textarea),
|
||||||
|
.cd-compose__input.p-textarea {
|
||||||
|
border-radius: 22px !important;
|
||||||
|
background: var(--p-surface-0) !important;
|
||||||
|
border: 1px solid var(--p-content-border-color) !important;
|
||||||
|
padding: 9px 16px !important;
|
||||||
|
font-size: 0.88rem !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
resize: none !important;
|
||||||
|
min-height: 40px;
|
||||||
|
max-height: 120px;
|
||||||
|
transition: border-color 140ms ease, box-shadow 140ms ease;
|
||||||
|
}
|
||||||
|
.cd-compose__input :deep(textarea:focus) {
|
||||||
|
border-color: var(--p-primary-color) !important;
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--p-primary-color) 22%, transparent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cd-compose__send {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: #00a884; /* verde send do WA */
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background-color 140ms ease, transform 140ms ease, opacity 140ms ease;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
.cd-compose__send:hover:not(.is-disabled) {
|
||||||
|
background: #06cf9c;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.cd-compose__send.is-disabled,
|
||||||
|
.cd-compose__send:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: var(--p-content-hover-background);
|
||||||
|
color: var(--p-text-muted-color);
|
||||||
|
}
|
||||||
|
.cd-compose__send > i { font-size: 1rem; }
|
||||||
|
|
||||||
|
.cd-compose__hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px 0;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
color: var(--p-text-muted-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -148,6 +148,19 @@ const carregandoInicial = computed(
|
|||||||
|
|
||||||
const unlinkedCount = computed(() => filteredThreads.value.filter((t) => !t.patient_id).length);
|
const unlinkedCount = computed(() => filteredThreads.value.filter((t) => !t.patient_id).length);
|
||||||
|
|
||||||
|
// ── "Limpar filtros" global (footer fixo da sidebar) ─────────────
|
||||||
|
// `filters` é um ref({...}) (vide useConversations.js). No script
|
||||||
|
// preciso acessar via .value; no template o auto-unwrap cuida.
|
||||||
|
const hasActiveFilters = computed(() =>
|
||||||
|
!!(filters.value.search || filters.value.unreadOnly || filters.value.assigned || filters.value.channel)
|
||||||
|
);
|
||||||
|
function clearAllFilters() {
|
||||||
|
filters.value.search = '';
|
||||||
|
filters.value.unreadOnly = false;
|
||||||
|
filters.value.assigned = null;
|
||||||
|
filters.value.channel = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Popover de Ações (compact)
|
// Popover de Ações (compact)
|
||||||
const actionsPopRef = ref(null);
|
const actionsPopRef = ref(null);
|
||||||
function openActions(e) { actionsPopRef.value?.toggle(e); }
|
function openActions(e) { actionsPopRef.value?.toggle(e); }
|
||||||
@@ -266,119 +279,172 @@ watch(() => tenantStore.activeTenantId, async () => {
|
|||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
<!-- Subheader explicativo (blueprint §9) — diferencia de
|
||||||
|
outras páginas Melissa que mostram listas tabulares. -->
|
||||||
|
<div class="mw-subheader">
|
||||||
|
<i class="pi pi-info-circle mw-subheader__icon" />
|
||||||
|
<span class="mw-subheader__text">
|
||||||
|
CRM de mensagens organizado em <strong>kanban por urgência</strong>:
|
||||||
|
Urgente / Aguardando resposta / Aguardando paciente / Resolvido.
|
||||||
|
Click num card abre a conversa no painel lateral.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mw-body">
|
<div class="mw-body">
|
||||||
<!-- ═══ COL 1: Filtros + atribuição + canais + status ═══ -->
|
<!-- ═══ COL 1: Filtros + atribuição + canais + status ═══ -->
|
||||||
<Teleport to="#mw-mobile-drawer-target" :disabled="!isMobile">
|
<Teleport to="#mw-mobile-drawer-target" :disabled="!isMobile">
|
||||||
<aside class="mw-side">
|
<aside class="mw-side">
|
||||||
<!-- Filtros rápidos -->
|
<div class="mw-side__scroll">
|
||||||
<div class="mw-w">
|
<!-- Alerta unlinked — no topo pra ficar bem visível
|
||||||
<div class="mw-w__head">
|
(números de telefone sem paciente vinculado). -->
|
||||||
<span class="mw-w__title"><i class="pi pi-filter" /> Filtros rápidos</span>
|
<div v-if="unlinkedCount > 0" class="mw-alert">
|
||||||
</div>
|
<i class="pi pi-exclamation-circle" />
|
||||||
<div class="mw-side__list">
|
<div>
|
||||||
<button
|
<div class="mw-alert__title">{{ unlinkedCount }} sem paciente vinculado</div>
|
||||||
class="mw-side__item"
|
<div class="mw-alert__hint">Números de telefone que não batem com pacientes cadastrados.</div>
|
||||||
:class="{ 'is-active': !filters.unreadOnly && !filters.channel }"
|
|
||||||
@click="filters.unreadOnly = false; filters.channel = null; filters.search = ''"
|
|
||||||
>
|
|
||||||
<i class="pi pi-list" />
|
|
||||||
<span>Todas</span>
|
|
||||||
<span class="mw-side__count">{{ summary.total }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="mw-side__item"
|
|
||||||
:class="{ 'is-active': filters.unreadOnly, 'is-warn': summary.unreadTotal > 0 }"
|
|
||||||
@click="filters.unreadOnly = !filters.unreadOnly"
|
|
||||||
>
|
|
||||||
<i class="pi pi-bell" />
|
|
||||||
<span>Não lidas</span>
|
|
||||||
<span class="mw-side__count" :class="{ 'is-danger': summary.unreadTotal > 0 }">{{ summary.unreadTotal }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Atribuição -->
|
|
||||||
<div class="mw-w">
|
|
||||||
<div class="mw-w__head">
|
|
||||||
<span class="mw-w__title"><i class="pi pi-user" /> Atribuição</span>
|
|
||||||
</div>
|
|
||||||
<div class="mw-side__list">
|
|
||||||
<button
|
|
||||||
class="mw-side__item"
|
|
||||||
:class="{ 'is-active': !filters.assigned }"
|
|
||||||
@click="filters.assigned = null"
|
|
||||||
>
|
|
||||||
<i class="pi pi-list" />
|
|
||||||
<span>Todas</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="mw-side__item"
|
|
||||||
:class="{ 'is-active': filters.assigned === 'me' }"
|
|
||||||
@click="filters.assigned = 'me'"
|
|
||||||
>
|
|
||||||
<i class="pi pi-user" />
|
|
||||||
<span>Minhas</span>
|
|
||||||
<span class="mw-side__count">{{ mineCount }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="mw-side__item"
|
|
||||||
:class="{ 'is-active': filters.assigned === 'unassigned' }"
|
|
||||||
@click="filters.assigned = 'unassigned'"
|
|
||||||
>
|
|
||||||
<i class="pi pi-user-minus" />
|
|
||||||
<span>Não atribuídas</span>
|
|
||||||
<span class="mw-side__count" :class="{ 'is-warn': unassignedCount > 0 }">{{ unassignedCount }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Por status (kanban resumo) -->
|
|
||||||
<div class="mw-w">
|
|
||||||
<div class="mw-w__head">
|
|
||||||
<span class="mw-w__title"><i class="pi pi-chart-bar" /> Por status</span>
|
|
||||||
</div>
|
|
||||||
<div class="mw-side__list">
|
|
||||||
<div
|
|
||||||
v-for="col in KANBAN_COLUMNS"
|
|
||||||
:key="col.key"
|
|
||||||
class="mw-side__row"
|
|
||||||
:class="`is-${col.color}`"
|
|
||||||
>
|
|
||||||
<i :class="col.icon" />
|
|
||||||
<span>{{ col.label }}</span>
|
|
||||||
<span class="mw-side__count">{{ summary[col.key] || 0 }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtros rápidos -->
|
||||||
|
<div class="mw-w mw-w--side">
|
||||||
|
<div class="mw-w__head">
|
||||||
|
<span class="mw-w__title"><i class="pi pi-filter" /> Filtros rápidos</span>
|
||||||
|
<button
|
||||||
|
v-if="filters.unreadOnly"
|
||||||
|
class="mw-side__clear-inline"
|
||||||
|
v-tooltip.top="'Limpar filtro de não lidas'"
|
||||||
|
aria-label="Limpar filtro de não lidas"
|
||||||
|
@click="filters.unreadOnly = false"
|
||||||
|
>
|
||||||
|
<i class="pi pi-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mw-side__list">
|
||||||
|
<button
|
||||||
|
class="mw-side__item"
|
||||||
|
:class="{ 'is-active': !filters.unreadOnly }"
|
||||||
|
@click="filters.unreadOnly = false"
|
||||||
|
>
|
||||||
|
<i class="pi pi-list" />
|
||||||
|
<span>Todas</span>
|
||||||
|
<span class="mw-side__count">{{ summary.total }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mw-side__item"
|
||||||
|
:class="{ 'is-active': filters.unreadOnly, 'is-warn': summary.unreadTotal > 0 }"
|
||||||
|
@click="filters.unreadOnly = !filters.unreadOnly"
|
||||||
|
>
|
||||||
|
<i class="pi pi-bell" />
|
||||||
|
<span>Não lidas</span>
|
||||||
|
<span class="mw-side__count" :class="{ 'is-danger': summary.unreadTotal > 0 }">{{ summary.unreadTotal }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Atribuição -->
|
||||||
|
<div class="mw-w mw-w--side">
|
||||||
|
<div class="mw-w__head">
|
||||||
|
<span class="mw-w__title"><i class="pi pi-user" /> Atribuição</span>
|
||||||
|
<button
|
||||||
|
v-if="filters.assigned"
|
||||||
|
class="mw-side__clear-inline"
|
||||||
|
v-tooltip.top="'Limpar filtro de atribuição'"
|
||||||
|
aria-label="Limpar filtro de atribuição"
|
||||||
|
@click="filters.assigned = null"
|
||||||
|
>
|
||||||
|
<i class="pi pi-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mw-side__list">
|
||||||
|
<button
|
||||||
|
class="mw-side__item"
|
||||||
|
:class="{ 'is-active': !filters.assigned }"
|
||||||
|
@click="filters.assigned = null"
|
||||||
|
>
|
||||||
|
<i class="pi pi-list" />
|
||||||
|
<span>Todas</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mw-side__item"
|
||||||
|
:class="{ 'is-active': filters.assigned === 'me' }"
|
||||||
|
@click="filters.assigned = 'me'"
|
||||||
|
>
|
||||||
|
<i class="pi pi-user" />
|
||||||
|
<span>Minhas</span>
|
||||||
|
<span class="mw-side__count">{{ mineCount }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mw-side__item"
|
||||||
|
:class="{ 'is-active': filters.assigned === 'unassigned' }"
|
||||||
|
@click="filters.assigned = 'unassigned'"
|
||||||
|
>
|
||||||
|
<i class="pi pi-user-minus" />
|
||||||
|
<span>Não atribuídas</span>
|
||||||
|
<span class="mw-side__count" :class="{ 'is-warn': unassignedCount > 0 }">{{ unassignedCount }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Por status (kanban resumo — display-only, sem X) -->
|
||||||
|
<div class="mw-w mw-w--side">
|
||||||
|
<div class="mw-w__head">
|
||||||
|
<span class="mw-w__title"><i class="pi pi-chart-bar" /> Por status</span>
|
||||||
|
</div>
|
||||||
|
<div class="mw-side__list">
|
||||||
|
<div
|
||||||
|
v-for="col in KANBAN_COLUMNS"
|
||||||
|
:key="col.key"
|
||||||
|
class="mw-side__row"
|
||||||
|
:class="`is-${col.color}`"
|
||||||
|
>
|
||||||
|
<i :class="col.icon" />
|
||||||
|
<span>{{ col.label }}</span>
|
||||||
|
<span class="mw-side__count">{{ summary[col.key] || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Canais -->
|
||||||
|
<div class="mw-w mw-w--side">
|
||||||
|
<div class="mw-w__head">
|
||||||
|
<span class="mw-w__title"><i class="pi pi-send" /> Canais</span>
|
||||||
|
<button
|
||||||
|
v-if="filters.channel"
|
||||||
|
class="mw-side__clear-inline"
|
||||||
|
v-tooltip.top="'Limpar filtro de canal'"
|
||||||
|
aria-label="Limpar filtro de canal"
|
||||||
|
@click="filters.channel = null"
|
||||||
|
>
|
||||||
|
<i class="pi pi-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mw-side__list">
|
||||||
|
<button
|
||||||
|
v-for="opt in CHANNEL_OPTIONS"
|
||||||
|
:key="String(opt.value)"
|
||||||
|
class="mw-side__item"
|
||||||
|
:class="{ 'is-active': filters.channel === opt.value }"
|
||||||
|
@click="filters.channel = opt.value"
|
||||||
|
>
|
||||||
|
<i v-if="opt.value" :class="['pi', channelIcon(opt.value)]" />
|
||||||
|
<i v-else class="pi pi-list" />
|
||||||
|
<span>{{ opt.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Canais -->
|
<!-- Footer fixo: "Limpar filtros" global (zera busca,
|
||||||
<div class="mw-w">
|
unread, atribuição e canal de uma vez). -->
|
||||||
<div class="mw-w__head">
|
<Transition name="mw-clear">
|
||||||
<span class="mw-w__title"><i class="pi pi-send" /> Canais</span>
|
<div v-if="hasActiveFilters" class="mw-side__footer">
|
||||||
</div>
|
<button class="mw-side__clear-all" @click="clearAllFilters">
|
||||||
<div class="mw-side__list">
|
<i class="pi pi-filter-slash" />
|
||||||
<button
|
<span>Limpar filtros</span>
|
||||||
v-for="opt in CHANNEL_OPTIONS"
|
|
||||||
:key="String(opt.value)"
|
|
||||||
class="mw-side__item"
|
|
||||||
:class="{ 'is-active': filters.channel === opt.value }"
|
|
||||||
@click="filters.channel = opt.value"
|
|
||||||
>
|
|
||||||
<i v-if="opt.value" :class="['pi', channelIcon(opt.value)]" />
|
|
||||||
<i v-else class="pi pi-list" />
|
|
||||||
<span>{{ opt.label }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Transition>
|
||||||
|
|
||||||
<!-- Alerta unlinked -->
|
|
||||||
<div v-if="unlinkedCount > 0" class="mw-alert">
|
|
||||||
<i class="pi pi-exclamation-circle" />
|
|
||||||
<div>
|
|
||||||
<div class="mw-alert__title">{{ unlinkedCount }} sem paciente vinculado</div>
|
|
||||||
<div class="mw-alert__hint">Números de telefone que não batem com pacientes cadastrados.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
@@ -611,42 +677,166 @@ watch(() => tenantStore.activeTenantId, async () => {
|
|||||||
}
|
}
|
||||||
.mw-menu-btn > i { font-size: 0.85rem; }
|
.mw-menu-btn > i { font-size: 0.85rem; }
|
||||||
|
|
||||||
/* Body */
|
/* Subheader explicativo (blueprint §9) */
|
||||||
|
.mw-subheader {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-bottom: 1px solid var(--m-border);
|
||||||
|
background: var(--m-bg-soft);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.mw-subheader__icon {
|
||||||
|
color: var(--p-primary-color);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.mw-subheader__text { flex: 1; min-width: 0; }
|
||||||
|
.mw-subheader__text strong { color: var(--m-text); font-weight: 600; }
|
||||||
|
|
||||||
|
/* Body — sem padding/gap; a sidebar tem bg+border-right próprios e o
|
||||||
|
main column controla seu padding interno. Espelha o pattern usado
|
||||||
|
em MelissaGrupos / MelissaTags / MelissaMedicos. */
|
||||||
.mw-body {
|
.mw-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
gap: 12px;
|
gap: 0;
|
||||||
padding: 12px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Aside */
|
/* Aside — 2 zonas: __scroll (cards) + __footer (Limpar filtros fixo).
|
||||||
|
bg colorido próprio (--m-bg-soft) + border-right pra separar
|
||||||
|
visualmente da coluna principal. */
|
||||||
.mw-side {
|
.mw-side {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
background: var(--m-bg-soft);
|
||||||
|
border-right: 1px solid var(--m-border);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.mw-side__scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--m-border-strong) transparent;
|
scrollbar-color: var(--m-border-strong) transparent;
|
||||||
}
|
}
|
||||||
.mw-side::-webkit-scrollbar { width: 5px; }
|
.mw-side__scroll::-webkit-scrollbar { width: 5px; }
|
||||||
.mw-side::-webkit-scrollbar-thumb {
|
.mw-side__scroll::-webkit-scrollbar-thumb {
|
||||||
background: var(--m-border-strong);
|
background: var(--m-border-strong);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Footer fixo no bottom da sidebar (fora do scroll dos filter cards).
|
||||||
|
Aparece com fade+collapse quando algum filtro está ativo. */
|
||||||
|
.mw-side__footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--m-bg-soft);
|
||||||
|
border-top: 1px solid var(--m-border);
|
||||||
|
}
|
||||||
|
.mw-side__clear-all {
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
background: var(--m-bg-medium);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
color: var(--m-text);
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
|
||||||
|
}
|
||||||
|
.mw-side__clear-all:hover {
|
||||||
|
background: var(--m-bg-soft-hover);
|
||||||
|
border-color: var(--m-border-strong);
|
||||||
|
color: var(--m-text);
|
||||||
|
}
|
||||||
|
.mw-side__clear-all > i {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
transition: color 140ms ease;
|
||||||
|
}
|
||||||
|
.mw-side__clear-all:hover > i { color: var(--m-text); }
|
||||||
|
|
||||||
|
/* X inline ao lado do título de cada filter card — limpa o filtro
|
||||||
|
individual. Espelha o pattern do MelissaPacientes/Grupos/Tags. */
|
||||||
|
.mw-side__clear-inline {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid color-mix(in srgb, rgb(220, 38, 38) 30%, var(--m-border));
|
||||||
|
color: rgb(220, 38, 38);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 140ms ease, border-color 140ms ease;
|
||||||
|
}
|
||||||
|
.mw-side__clear-inline:hover {
|
||||||
|
background: rgba(220, 38, 38, 0.10);
|
||||||
|
border-color: rgba(220, 38, 38, 0.55);
|
||||||
|
}
|
||||||
|
.mw-side__clear-inline > i { font-size: 0.6rem; }
|
||||||
|
|
||||||
|
/* Transition do footer "Limpar filtros" */
|
||||||
|
.mw-clear-enter-active,
|
||||||
|
.mw-clear-leave-active {
|
||||||
|
transition: opacity 220ms ease, transform 220ms ease, max-height 240ms ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.mw-clear-enter-from,
|
||||||
|
.mw-clear-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
.mw-clear-enter-to,
|
||||||
|
.mw-clear-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
max-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Card-base — alinhado com .ma-w / .mp-w / .mcr-w: surface --m-bg-medium
|
/* Card-base — alinhado com .ma-w / .mp-w / .mcr-w: surface --m-bg-medium
|
||||||
pra destacar do bg da página/dialog (ambos --m-bg-soft). */
|
pra destacar do bg da sidebar (--m-bg-soft). */
|
||||||
.mw-w {
|
.mw-w {
|
||||||
background: var(--m-bg-medium);
|
background: var(--m-bg-medium);
|
||||||
border: 1px solid var(--m-border);
|
border: 1px solid var(--m-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
.mw-w__head { margin-bottom: 10px; }
|
/* Modifier pros cards dentro da .mw-side — margem lateral + sombra
|
||||||
|
sutil pra elevar sobre o bg da sidebar. Espelha .mc-w--side, .mt-w--side. */
|
||||||
|
.mw-w--side {
|
||||||
|
margin: 12px 12px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
.mw-w--side:last-of-type { margin-bottom: 12px; }
|
||||||
|
.mw-w__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
.mw-w__title {
|
.mw-w__title {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -734,12 +924,16 @@ watch(() => tenantStore.activeTenantId, async () => {
|
|||||||
.mw-alert {
|
.mw-alert {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
margin: 12px 12px 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||||
background: rgba(251, 191, 36, 0.05);
|
background: rgba(251, 191, 36, 0.05);
|
||||||
color: rgb(251, 191, 36);
|
color: rgb(251, 191, 36);
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
.mw-alert:last-child { margin-bottom: 12px; }
|
||||||
.mw-alert > i { font-size: 0.85rem; margin-top: 2px; }
|
.mw-alert > i { font-size: 0.85rem; margin-top: 2px; }
|
||||||
.mw-alert__title {
|
.mw-alert__title {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
@@ -751,13 +945,15 @@ watch(() => tenantStore.activeTenantId, async () => {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main */
|
/* Main — recebe padding interno (o body não tem mais padding/gap;
|
||||||
|
sidebar fica colada à esquerda com border-right). */
|
||||||
.mw-main {
|
.mw-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Kanban */
|
/* Kanban */
|
||||||
@@ -1009,11 +1205,40 @@ watch(() => tenantStore.activeTenantId, async () => {
|
|||||||
background: var(--m-border-strong);
|
background: var(--m-border-strong);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
/* Sidebar teleportada pro drawer — perde bg/border-right (o drawer
|
||||||
|
já tem chrome próprio) + cards perdem margem lateral (drawer já
|
||||||
|
tem padding). Footer vira sticky no bottom do drawer. */
|
||||||
.mw-mobile-drawer__scroll .mw-side {
|
.mw-mobile-drawer__scroll .mw-side {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.mw-mobile-drawer__scroll .mw-side__scroll {
|
||||||
|
flex: none;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.mw-mobile-drawer__scroll .mw-w--side {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.mw-mobile-drawer__scroll .mw-w--side:last-of-type { margin-bottom: 0; }
|
||||||
|
.mw-mobile-drawer__scroll .mw-alert { margin: 0; }
|
||||||
|
.mw-mobile-drawer__scroll .mw-side__footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
margin: 8px -12px -24px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--m-bg-medium);
|
||||||
|
border-top: 1px solid var(--m-border);
|
||||||
|
backdrop-filter: blur(24px) saturate(160%);
|
||||||
|
-webkit-backdrop-filter: blur(24px) saturate(160%);
|
||||||
|
z-index: 5;
|
||||||
}
|
}
|
||||||
.mw-mobile-drawer__backdrop {
|
.mw-mobile-drawer__backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -1035,12 +1260,32 @@ watch(() => tenantStore.activeTenantId, async () => {
|
|||||||
.mw-kanban { grid-template-columns: repeat(2, 1fr); }
|
.mw-kanban { grid-template-columns: repeat(2, 1fr); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══ Mobile (<lg) — drawer + kanban 1-col ═══ */
|
/* ═══ Mobile (<lg) — drawer + kanban 1-col stacked ═══
|
||||||
|
Em mobile o kanban vira flex column (stacked) e o scroll passa a ser
|
||||||
|
global no .mw-main (não interno por coluna). Cada .mw-col cresce com
|
||||||
|
o conteúdo + min-height pra empty state ter altura visível. */
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
.mw-body { flex-direction: column; padding: 8px; }
|
.mw-body { flex-direction: column; padding: 0; }
|
||||||
.mw-main { width: 100%; }
|
.mw-main {
|
||||||
.mw-kanban { grid-template-columns: 1fr; }
|
width: 100%;
|
||||||
.mw-col { min-height: auto; }
|
padding: 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.mw-kanban {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: none;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.mw-col {
|
||||||
|
flex: none;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
.mw-col__body {
|
||||||
|
flex: none;
|
||||||
|
overflow: visible;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
.mw-page__title > span:first-of-type { display: none; }
|
.mw-page__title > span:first-of-type { display: none; }
|
||||||
.mw-menu-btn--mobile-only { display: inline-flex; }
|
.mw-menu-btn--mobile-only { display: inline-flex; }
|
||||||
}
|
}
|
||||||
|
|||||||
+1840
-228
File diff suppressed because it is too large
Load Diff
+1934
-268
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,6 @@
|
|||||||
*/
|
*/
|
||||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import Popover from 'primevue/popover';
|
|
||||||
import { supabase } from '@/lib/supabase/client';
|
import { supabase } from '@/lib/supabase/client';
|
||||||
import { useTenantStore } from '@/stores/tenantStore';
|
import { useTenantStore } from '@/stores/tenantStore';
|
||||||
// Dialog/SelectButton/Button/Tag/ProgressBar/Avatar/Select: auto-import via PrimeVueResolver
|
// Dialog/SelectButton/Button/Tag/ProgressBar/Avatar/Select: auto-import via PrimeVueResolver
|
||||||
@@ -49,16 +48,46 @@ const sessionsMap = ref({}); // ruleId → AgendaEvento[]
|
|||||||
const expandedId = ref(null);
|
const expandedId = ref(null);
|
||||||
|
|
||||||
const filterStatus = ref('ativo');
|
const filterStatus = ref('ativo');
|
||||||
const statusOptions = [
|
|
||||||
{ label: 'Ativas', value: 'ativo' },
|
// Button list de status (icons + cores). Ativa = verde / Encerrada = vermelho.
|
||||||
{ label: 'Encerradas', value: 'cancelado' },
|
// Todas = neutral. O blueprint pede botões coloridos por status (§7).
|
||||||
{ label: 'Todas', value: 'all' }
|
const STATUS_FILTER_OPTIONS = [
|
||||||
|
{ key: 'ativo', label: 'Ativas', icon: 'pi pi-check-circle' },
|
||||||
|
{ key: 'cancelado', label: 'Encerradas', icon: 'pi pi-times-circle' },
|
||||||
|
{ key: 'all', label: 'Todas', icon: 'pi pi-list' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const busca = ref('');
|
||||||
|
|
||||||
|
function setStatusFilter(s) {
|
||||||
|
filterStatus.value = s;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasActiveFilters = computed(() =>
|
||||||
|
!!(busca.value || filterStatus.value !== 'ativo')
|
||||||
|
);
|
||||||
|
function clearAllFilters() {
|
||||||
|
busca.value = '';
|
||||||
|
if (filterStatus.value !== 'ativo') {
|
||||||
|
filterStatus.value = 'ativo';
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const carregandoInicial = computed(
|
const carregandoInicial = computed(
|
||||||
() => loading.value && rules.value.length === 0
|
() => loading.value && rules.value.length === 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Filtragem client-side por nome do paciente (status é server-side via load).
|
||||||
|
const filteredRules = computed(() => {
|
||||||
|
const q = String(busca.value || '').trim().toLowerCase();
|
||||||
|
if (!q) return rules.value;
|
||||||
|
return rules.value.filter((r) =>
|
||||||
|
String(r._patient?.nome_completo || '').toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Data load ───────────────────────────────────────────────
|
// ── Data load ───────────────────────────────────────────────
|
||||||
async function init() {
|
async function init() {
|
||||||
const { data } = await supabase.auth.getUser();
|
const { data } = await supabase.auth.getUser();
|
||||||
@@ -325,10 +354,6 @@ function toggleExpand(ruleId) {
|
|||||||
expandedId.value = expandedId.value === ruleId ? null : ruleId;
|
expandedId.value = expandedId.value === ruleId ? null : ruleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Popover de Ações (mobile compact) ──────────────────────
|
|
||||||
const actionsPopRef = ref(null);
|
|
||||||
function openActions(e) { actionsPopRef.value?.toggle(e); }
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||||
_mqMobile = window.matchMedia('(max-width: 1023px)');
|
_mqMobile = window.matchMedia('(max-width: 1023px)');
|
||||||
@@ -378,18 +403,11 @@ onBeforeUnmount(() => {
|
|||||||
<span>Menu Recorrências</span>
|
<span>Menu Recorrências</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="mr-page__title">
|
<div class="mr-page__title">
|
||||||
<i class="pi pi-sync text-indigo-300" />
|
<i class="pi pi-sync mr-page__title-icon" />
|
||||||
<span>Recorrências</span>
|
<span>Recorrências</span>
|
||||||
<span class="mr-page__count">{{ rules.length }}</span>
|
<span class="mr-page__count">{{ filteredRules.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-page__actions">
|
<div class="mr-page__actions">
|
||||||
<button
|
|
||||||
class="mr-head-btn mr-head-btn--compact-only"
|
|
||||||
v-tooltip.bottom="'Filtros'"
|
|
||||||
@click="openActions"
|
|
||||||
>
|
|
||||||
<i class="pi pi-sliders-h" />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="mr-head-btn"
|
class="mr-head-btn"
|
||||||
v-tooltip.bottom="'Recarregar'"
|
v-tooltip.bottom="'Recarregar'"
|
||||||
@@ -404,77 +422,110 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Popover ref="actionsPopRef" class="mr-actions-pop">
|
<!-- Subheader explicativo (blueprint §9) -->
|
||||||
<div class="mr-actions">
|
<div class="mr-subheader">
|
||||||
<div class="mr-actions__group">
|
<i class="pi pi-info-circle mr-subheader__icon" />
|
||||||
<div class="mr-actions__label">Status</div>
|
<span class="mr-subheader__text">
|
||||||
<SelectButton
|
Séries de sessões que se repetem (semanal, quinzenal, dias específicos).
|
||||||
v-model="filterStatus"
|
Acompanhe o <strong>progresso</strong> de cada uma e marque como
|
||||||
:options="statusOptions"
|
<strong>encerrada</strong> quando o paciente terminar o tratamento.
|
||||||
optionLabel="label"
|
</span>
|
||||||
optionValue="value"
|
</div>
|
||||||
:allowEmpty="false"
|
|
||||||
size="small"
|
|
||||||
class="w-full"
|
|
||||||
@change="load"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<div class="mr-body">
|
<div class="mr-body">
|
||||||
<!-- ═══ COL 1: Stats + filtros ═══════════════════════════════ -->
|
<!-- ═══ COL 1: Stats + filtros ═══════════════════════════════ -->
|
||||||
<Teleport to="#mr-mobile-drawer-target" :disabled="!isMobile">
|
<Teleport to="#mr-mobile-drawer-target" :disabled="!isMobile">
|
||||||
<aside class="mr-side">
|
<aside class="mr-side">
|
||||||
<div class="mr-w">
|
<div class="mr-side__scroll">
|
||||||
<div class="mr-w__head">
|
<!-- Stats -->
|
||||||
<span class="mr-w__title"><i class="pi pi-chart-bar" /> Estatísticas</span>
|
<div class="mr-w mr-w--side">
|
||||||
</div>
|
<div class="mr-w__head">
|
||||||
<div class="mr-stats">
|
<span class="mr-w__title"><i class="pi pi-chart-bar" /> Estatísticas</span>
|
||||||
<template v-if="carregandoInicial">
|
</div>
|
||||||
<div v-for="i in 4" :key="`stsk-${i}`" class="mr-stat mr-stat--skeleton" aria-busy="true">
|
<div class="mr-stats">
|
||||||
<div class="mr-stat__val melissa-skeleton melissa-skeleton--number" />
|
<template v-if="carregandoInicial">
|
||||||
<div class="mr-stat__lbl melissa-skeleton melissa-skeleton--text" style="width: 60%; margin-top: 6px;" />
|
<div v-for="i in 4" :key="`stsk-${i}`" class="mr-stat mr-stat--skeleton" aria-busy="true">
|
||||||
|
<div class="mr-stat__val melissa-skeleton melissa-skeleton--number" />
|
||||||
|
<div class="mr-stat__lbl melissa-skeleton melissa-skeleton--text" style="width: 60%; margin-top: 6px;" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-for="s in aggregateStats"
|
||||||
|
v-else
|
||||||
|
:key="s.key"
|
||||||
|
class="mr-stat"
|
||||||
|
:class="`is-${s.cls}`"
|
||||||
|
>
|
||||||
|
<div class="mr-stat__val">{{ s.value }}</div>
|
||||||
|
<div class="mr-stat__lbl">{{ s.label }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<div
|
</div>
|
||||||
v-for="s in aggregateStats"
|
|
||||||
v-else
|
<!-- Filtro de status (button list colorido) -->
|
||||||
:key="s.key"
|
<div class="mr-w mr-w--side">
|
||||||
class="mr-stat"
|
<div class="mr-w__head">
|
||||||
:class="`is-${s.cls}`"
|
<span class="mr-w__title"><i class="pi pi-filter" /> Status</span>
|
||||||
>
|
<button
|
||||||
<div class="mr-stat__val">{{ s.value }}</div>
|
v-if="filterStatus !== 'ativo'"
|
||||||
<div class="mr-stat__lbl">{{ s.label }}</div>
|
class="mr-side__clear-inline"
|
||||||
|
v-tooltip.top="'Voltar pro filtro padrão (Ativas)'"
|
||||||
|
aria-label="Voltar pro filtro padrão"
|
||||||
|
@click="setStatusFilter('ativo')"
|
||||||
|
>
|
||||||
|
<i class="pi pi-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mr-side__list">
|
||||||
|
<button
|
||||||
|
v-for="o in STATUS_FILTER_OPTIONS"
|
||||||
|
:key="o.key"
|
||||||
|
class="mr-side__item"
|
||||||
|
:class="[`is-status-${o.key}`, { 'is-active': filterStatus === o.key }]"
|
||||||
|
@click="setStatusFilter(o.key)"
|
||||||
|
>
|
||||||
|
<i :class="o.icon" />
|
||||||
|
<span>{{ o.label }}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mr-w mr-w--side-only">
|
<!-- Footer fixo: Limpar filtros (Transition fade+collapse) -->
|
||||||
<div class="mr-w__head">
|
<Transition name="mr-clear">
|
||||||
<span class="mr-w__title"><i class="pi pi-filter" /> Filtros</span>
|
<div v-if="hasActiveFilters" class="mr-side__footer">
|
||||||
|
<button class="mr-side__clear-all" @click="clearAllFilters">
|
||||||
|
<i class="pi pi-filter-slash" />
|
||||||
|
<span>Limpar filtros</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-side__filters">
|
</Transition>
|
||||||
<div>
|
|
||||||
<div class="mr-side__label">Status</div>
|
|
||||||
<SelectButton
|
|
||||||
v-model="filterStatus"
|
|
||||||
:options="statusOptions"
|
|
||||||
optionLabel="label"
|
|
||||||
optionValue="value"
|
|
||||||
:allowEmpty="false"
|
|
||||||
size="small"
|
|
||||||
class="w-full"
|
|
||||||
@change="load"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<!-- ═══ COL 2: Lista de regras ═══════════════════════════════ -->
|
<!-- ═══ COL 2: Lista de regras ═══════════════════════════════ -->
|
||||||
<div class="mr-main">
|
<div class="mr-main">
|
||||||
|
<!-- Toolbar: busca por nome do paciente -->
|
||||||
|
<div class="mr-toolbar">
|
||||||
|
<div class="mr-search">
|
||||||
|
<i class="pi pi-search mr-search__icon" />
|
||||||
|
<input
|
||||||
|
v-model="busca"
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar por nome do paciente…"
|
||||||
|
class="mr-search__input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="busca"
|
||||||
|
class="mr-search__clear"
|
||||||
|
v-tooltip.bottom="'Limpar busca'"
|
||||||
|
@click="busca = ''"
|
||||||
|
>
|
||||||
|
<i class="pi pi-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mr-list">
|
<div class="mr-list">
|
||||||
<!-- Skeletons (blueprint §9) -->
|
<!-- Skeletons (blueprint §9) -->
|
||||||
<template v-if="carregandoInicial">
|
<template v-if="carregandoInicial">
|
||||||
@@ -490,11 +541,14 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else-if="!rules.length" class="mr-empty">
|
<div v-else-if="!filteredRules.length" class="mr-empty">
|
||||||
<i class="pi pi-calendar-times mr-empty__icon" />
|
<i class="pi pi-calendar-times mr-empty__icon" />
|
||||||
<div class="mr-empty__title">Nenhuma série encontrada</div>
|
<div class="mr-empty__title">Nenhuma série encontrada</div>
|
||||||
<div class="mr-empty__hint">
|
<div class="mr-empty__hint">
|
||||||
<template v-if="filterStatus === 'ativo'">
|
<template v-if="busca">
|
||||||
|
Nenhum paciente corresponde à busca. Ajuste ou limpe os filtros.
|
||||||
|
</template>
|
||||||
|
<template v-else-if="filterStatus === 'ativo'">
|
||||||
Crie sessões recorrentes na agenda pra vê-las aqui.
|
Crie sessões recorrentes na agenda pra vê-las aqui.
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -503,7 +557,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else v-for="rule in rules" :key="rule.id" class="mr-card">
|
<div v-else v-for="rule in filteredRules" :key="rule.id" class="mr-card">
|
||||||
<!-- Head: paciente + descrição + período -->
|
<!-- Head: paciente + descrição + período -->
|
||||||
<div class="mr-card__head">
|
<div class="mr-card__head">
|
||||||
<span class="mr-card__avatar">
|
<span class="mr-card__avatar">
|
||||||
@@ -637,6 +691,10 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.mr-page__title-icon {
|
||||||
|
color: var(--p-primary-color);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
.mr-page__title > span:not(.mr-page__count) {
|
.mr-page__title > span:not(.mr-page__count) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -699,40 +757,159 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
.mr-menu-btn > i { font-size: 0.85rem; }
|
.mr-menu-btn > i { font-size: 0.85rem; }
|
||||||
|
|
||||||
/* Body */
|
/* Subheader (blueprint §9) */
|
||||||
|
.mr-subheader {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-bottom: 1px solid var(--m-border);
|
||||||
|
background: var(--m-bg-soft);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.mr-subheader__icon {
|
||||||
|
color: var(--p-primary-color);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.mr-subheader__text { flex: 1; min-width: 0; }
|
||||||
|
.mr-subheader__text strong { color: var(--m-text); font-weight: 600; }
|
||||||
|
|
||||||
|
/* Body — sidebar fica colada à esquerda com border-right; main com
|
||||||
|
padding interno. Espelha Grupos/Tags/Médicos/Conversas. */
|
||||||
.mr-body {
|
.mr-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
gap: 12px;
|
gap: 0;
|
||||||
padding: 12px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Aside */
|
/* Aside — 2 zonas: __scroll (cards) + __footer (Limpar filtros fixo) */
|
||||||
.mr-side {
|
.mr-side {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
background: var(--m-bg-soft);
|
||||||
|
border-right: 1px solid var(--m-border);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.mr-side__scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--m-border-strong) transparent;
|
scrollbar-color: var(--m-border-strong) transparent;
|
||||||
}
|
}
|
||||||
.mr-side::-webkit-scrollbar { width: 5px; }
|
.mr-side__scroll::-webkit-scrollbar { width: 5px; }
|
||||||
.mr-side::-webkit-scrollbar-thumb {
|
.mr-side__scroll::-webkit-scrollbar-thumb {
|
||||||
background: var(--m-border-strong);
|
background: var(--m-border-strong);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr-w {
|
/* Footer fixo: Limpar filtros (espelha Grupos/Tags/Médicos/Conversas) */
|
||||||
|
.mr-side__footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12px;
|
||||||
background: var(--m-bg-soft);
|
background: var(--m-bg-soft);
|
||||||
|
border-top: 1px solid var(--m-border);
|
||||||
|
}
|
||||||
|
.mr-side__clear-all {
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
background: var(--m-bg-medium);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
color: var(--m-text);
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
|
||||||
|
}
|
||||||
|
.mr-side__clear-all:hover {
|
||||||
|
background: var(--m-bg-soft-hover);
|
||||||
|
border-color: var(--m-border-strong);
|
||||||
|
color: var(--m-text);
|
||||||
|
}
|
||||||
|
.mr-side__clear-all > i {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
transition: color 140ms ease;
|
||||||
|
}
|
||||||
|
.mr-side__clear-all:hover > i { color: var(--m-text); }
|
||||||
|
|
||||||
|
/* X inline ao lado do título do filter card */
|
||||||
|
.mr-side__clear-inline {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid color-mix(in srgb, rgb(220, 38, 38) 30%, var(--m-border));
|
||||||
|
color: rgb(220, 38, 38);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 140ms ease, border-color 140ms ease;
|
||||||
|
}
|
||||||
|
.mr-side__clear-inline:hover {
|
||||||
|
background: rgba(220, 38, 38, 0.10);
|
||||||
|
border-color: rgba(220, 38, 38, 0.55);
|
||||||
|
}
|
||||||
|
.mr-side__clear-inline > i { font-size: 0.6rem; }
|
||||||
|
|
||||||
|
/* Transition do footer */
|
||||||
|
.mr-clear-enter-active,
|
||||||
|
.mr-clear-leave-active {
|
||||||
|
transition: opacity 220ms ease, transform 220ms ease, max-height 240ms ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.mr-clear-enter-from,
|
||||||
|
.mr-clear-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
.mr-clear-enter-to,
|
||||||
|
.mr-clear-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
max-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr-w {
|
||||||
|
background: var(--m-bg-medium);
|
||||||
border: 1px solid var(--m-border);
|
border: 1px solid var(--m-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
.mr-w__head { margin-bottom: 10px; }
|
/* Modifier pros cards dentro da sidebar — margem lateral + sombra */
|
||||||
|
.mr-w--side {
|
||||||
|
margin: 12px 12px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
.mr-w--side:last-of-type { margin-bottom: 12px; }
|
||||||
|
|
||||||
|
.mr-w__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
.mr-w__title {
|
.mr-w__title {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -748,7 +925,7 @@ onBeforeUnmount(() => {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.mr-stat {
|
.mr-stat {
|
||||||
background: var(--m-bg-medium);
|
background: var(--m-bg-soft);
|
||||||
border: 1px solid var(--m-border);
|
border: 1px solid var(--m-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
@@ -765,15 +942,80 @@ onBeforeUnmount(() => {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
.mr-stat.is-ok .mr-stat__val { color: rgb(74, 222, 128); }
|
.mr-stat.is-ok .mr-stat__val { color: rgb(22, 163, 74); }
|
||||||
|
|
||||||
.mr-side__label {
|
/* Filter button list (blueprint §8) */
|
||||||
text-transform: uppercase;
|
.mr-side__list {
|
||||||
letter-spacing: 0.12em;
|
display: flex;
|
||||||
color: var(--m-text-muted);
|
flex-direction: column;
|
||||||
font-size: 0.62rem;
|
gap: 4px;
|
||||||
font-weight: 600;
|
}
|
||||||
margin-bottom: 6px;
|
.mr-side__item {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--m-text);
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
text-align: left;
|
||||||
|
transition: background-color 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
|
||||||
|
}
|
||||||
|
.mr-side__item > i {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
width: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status: Ativas verde / Encerradas vermelho / Todas neutral */
|
||||||
|
.mr-side__item.is-status-ativo {
|
||||||
|
background: rgba(22, 163, 74, 0.05);
|
||||||
|
border-color: rgba(22, 163, 74, 0.18);
|
||||||
|
}
|
||||||
|
.mr-side__item.is-status-ativo > i { color: rgb(22, 163, 74); }
|
||||||
|
.mr-side__item.is-status-ativo:hover {
|
||||||
|
background: rgba(22, 163, 74, 0.10);
|
||||||
|
border-color: rgba(22, 163, 74, 0.30);
|
||||||
|
}
|
||||||
|
.mr-side__item.is-active.is-status-ativo {
|
||||||
|
background: rgba(22, 163, 74, 0.16);
|
||||||
|
border-color: rgba(22, 163, 74, 0.55);
|
||||||
|
box-shadow: 0 0 0 1px rgba(22, 163, 74, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr-side__item.is-status-cancelado {
|
||||||
|
background: rgba(220, 38, 38, 0.05);
|
||||||
|
border-color: rgba(220, 38, 38, 0.18);
|
||||||
|
}
|
||||||
|
.mr-side__item.is-status-cancelado > i { color: rgb(220, 38, 38); }
|
||||||
|
.mr-side__item.is-status-cancelado:hover {
|
||||||
|
background: rgba(220, 38, 38, 0.10);
|
||||||
|
border-color: rgba(220, 38, 38, 0.30);
|
||||||
|
}
|
||||||
|
.mr-side__item.is-active.is-status-cancelado {
|
||||||
|
background: rgba(220, 38, 38, 0.16);
|
||||||
|
border-color: rgba(220, 38, 38, 0.55);
|
||||||
|
box-shadow: 0 0 0 1px rgba(220, 38, 38, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr-side__item.is-status-all {
|
||||||
|
background: var(--m-bg-soft);
|
||||||
|
border-color: var(--m-border);
|
||||||
|
}
|
||||||
|
.mr-side__item.is-status-all > i { color: var(--m-text-muted); }
|
||||||
|
.mr-side__item.is-status-all:hover {
|
||||||
|
background: var(--m-bg-soft-hover);
|
||||||
|
border-color: var(--m-border-strong);
|
||||||
|
}
|
||||||
|
.mr-side__item.is-active.is-status-all {
|
||||||
|
background: var(--m-bg-soft-hover);
|
||||||
|
border-color: var(--m-border-strong);
|
||||||
|
box-shadow: 0 0 0 1px var(--m-border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main */
|
/* Main */
|
||||||
@@ -782,9 +1024,70 @@ onBeforeUnmount(() => {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 12px;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toolbar com busca */
|
||||||
|
.mr-toolbar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.mr-search {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.mr-search__icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.mr-search__input {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--m-bg-medium);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
color: var(--m-text);
|
||||||
|
padding: 9px 36px 9px 34px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
transition: background-color 140ms ease, border-color 140ms ease;
|
||||||
|
}
|
||||||
|
.mr-search__input::placeholder { color: var(--m-text-faint); }
|
||||||
|
.mr-search__input:focus {
|
||||||
|
border-color: var(--m-border-strong);
|
||||||
|
}
|
||||||
|
.mr-search__clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 140ms ease, color 140ms ease;
|
||||||
|
}
|
||||||
|
.mr-search__clear:hover {
|
||||||
|
background: var(--m-bg-soft-hover);
|
||||||
|
color: var(--m-text);
|
||||||
|
}
|
||||||
|
.mr-search__clear > i { font-size: 0.7rem; }
|
||||||
.mr-list {
|
.mr-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 4px 4px;
|
padding: 0 4px 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1049,27 +1352,6 @@ onBeforeUnmount(() => {
|
|||||||
color: var(--m-text-muted);
|
color: var(--m-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Popover de Ações */
|
|
||||||
.mr-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
min-width: 240px;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
.mr-actions__group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.mr-actions__label {
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.14em;
|
|
||||||
color: var(--text-color-secondary, var(--m-text-faint));
|
|
||||||
font-size: 0.62rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Drawer mobile (blueprint §6) */
|
/* Drawer mobile (blueprint §6) */
|
||||||
.mr-mobile-drawer {
|
.mr-mobile-drawer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -1110,6 +1392,31 @@ onBeforeUnmount(() => {
|
|||||||
height: auto;
|
height: auto;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.mr-mobile-drawer__scroll .mr-side__scroll {
|
||||||
|
flex: none;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.mr-mobile-drawer__scroll .mr-w--side {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.mr-mobile-drawer__scroll .mr-w--side:last-of-type { margin-bottom: 0; }
|
||||||
|
.mr-mobile-drawer__scroll .mr-side__footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
margin: 8px -12px -24px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--m-bg-medium);
|
||||||
|
border-top: 1px solid var(--m-border);
|
||||||
|
backdrop-filter: blur(24px) saturate(160%);
|
||||||
|
-webkit-backdrop-filter: blur(24px) saturate(160%);
|
||||||
|
z-index: 5;
|
||||||
}
|
}
|
||||||
.mr-mobile-drawer__backdrop {
|
.mr-mobile-drawer__backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -1124,15 +1431,10 @@ onBeforeUnmount(() => {
|
|||||||
.mr-drawer-fade-enter-from,
|
.mr-drawer-fade-enter-from,
|
||||||
.mr-drawer-fade-leave-to { opacity: 0; }
|
.mr-drawer-fade-leave-to { opacity: 0; }
|
||||||
|
|
||||||
/* Compact (<xl) */
|
|
||||||
@media (max-width: 1279px) {
|
|
||||||
.mr-head-btn--compact-only { display: grid; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile (<lg) */
|
/* Mobile (<lg) */
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
.mr-body { flex-direction: column; padding: 8px; }
|
.mr-body { flex-direction: column; padding: 0; }
|
||||||
.mr-main { width: 100%; }
|
.mr-main { width: 100%; padding: 8px; }
|
||||||
.mr-page__title > span:first-of-type { display: none; }
|
.mr-page__title > span:first-of-type { display: none; }
|
||||||
.mr-menu-btn--mobile-only { display: inline-flex; }
|
.mr-menu-btn--mobile-only { display: inline-flex; }
|
||||||
.mr-card__foot { gap: 4px; }
|
.mr-card__foot { gap: 4px; }
|
||||||
|
|||||||
+1803
-245
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user