melissa/layout: topbar->tray bottom-right + dock 4 builtins + mobile collapse
Tray no canto inferior direito (substitui o topbar band do topo):
busca + plan-DEV + bell + ajuda + cog. Sibling de .melissa-dock
(fora de .win11-summary) pra ficar sempre interativo mesmo com
secao aberta (que aplica blur+pointer-none). z-index 66 (acima
do dock=65). Em <md (768px) collapse parcial — bell/help/cog/
plan-DEV somem e viram popup vertical no botao ⋮; dot vermelho
no ⋮ quando ha notificacoes nao lidas. Search sempre visivel.
Dock: 4 builtins na ordem Agenda · Pacientes · WhatsApp · Financeiro
(antes so Agenda+WhatsApp). MRU (max 3) ganha @media (max-width:
767px) display:none — utility 'hidden' do Tailwind perdia pro
.dock-pin{display:grid} por ordem de carga. Divisor entre builtins
e pins user some em mobile se so houver MRU (que ja esta oculto).
Wire-ups das commits anteriores:
- ref melissaBuscaRef + provide('openMelissaBusca') pra acoes
contextuais futuras (botao tray chama direto via ref)
- @goto-date no <MelissaBusca> -> onBuscaGotoDate via _callOnAgenda
- @iniciar-cronometro no <MelissaTimelineHoje> -> handler que abre
o cronometro com sessionPlan + autostart; opcao (b) "ja ativo"
mostra toast warn sem trocar paciente
- Card "Proximo paciente" troca CTA pra "Iniciar cronometro" quando
emCurso E tem patient_id; @open chama o mesmo handler do timeline
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -575,9 +575,50 @@ function setPreset(name) {
|
|||||||
|
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────
|
||||||
// Settings popover (canto superior direito)
|
// Settings popover (canto inferior direito — vive na .melissa-tray)
|
||||||
// ───────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────
|
||||||
const settingsOpen = ref(false);
|
const settingsOpen = ref(false);
|
||||||
|
const cogBtnEl = ref(null);
|
||||||
|
|
||||||
|
// "More" tray popup — visivel so em mobile (<md). Collapse de bell/
|
||||||
|
// help/cog/plan-DEV num menu vertical pra economizar largura.
|
||||||
|
const trayMoreOpen = ref(false);
|
||||||
|
const trayMoreBtnEl = ref(null);
|
||||||
|
|
||||||
|
// Fechar ao clicar fora: listener so existe enquanto o popover esta
|
||||||
|
// aberto. mousedown (capture) fecha antes do click chegar — mas o
|
||||||
|
// proprio cog precisa ser ignorado, senao fecha aqui e o @click do
|
||||||
|
// botao re-abre na sequencia.
|
||||||
|
function onSettingsDocMouseDown(e) {
|
||||||
|
if (!settingsOpen.value) return;
|
||||||
|
const t = e.target;
|
||||||
|
if (!(t instanceof Element)) return;
|
||||||
|
if (t.closest('.mp-panel')) return; // clique dentro do panel
|
||||||
|
if (cogBtnEl.value?.contains(t)) return; // clique no proprio cog
|
||||||
|
settingsOpen.value = false;
|
||||||
|
}
|
||||||
|
watch(settingsOpen, (open) => {
|
||||||
|
if (open) document.addEventListener('mousedown', onSettingsDocMouseDown, true);
|
||||||
|
else document.removeEventListener('mousedown', onSettingsDocMouseDown, true);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => document.removeEventListener('mousedown', onSettingsDocMouseDown, true));
|
||||||
|
|
||||||
|
// Mesmo padrao pro popup "More" do tray em mobile: ignora o proprio
|
||||||
|
// botao trigger (senao fecha + reabre no click) e fecha em qualquer
|
||||||
|
// outro lugar fora do panel.
|
||||||
|
function onTrayMoreDocMouseDown(e) {
|
||||||
|
if (!trayMoreOpen.value) return;
|
||||||
|
const t = e.target;
|
||||||
|
if (!(t instanceof Element)) return;
|
||||||
|
if (t.closest('.melissa-tray__more-panel')) return;
|
||||||
|
if (trayMoreBtnEl.value?.contains(t)) return;
|
||||||
|
trayMoreOpen.value = false;
|
||||||
|
}
|
||||||
|
watch(trayMoreOpen, (open) => {
|
||||||
|
if (open) document.addEventListener('mousedown', onTrayMoreDocMouseDown, true);
|
||||||
|
else document.removeEventListener('mousedown', onTrayMoreDocMouseDown, true);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => document.removeEventListener('mousedown', onTrayMoreDocMouseDown, true));
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────
|
||||||
// Timeline horizontal — range/eco/posicoes/auto-scroll/cursor "Agora"
|
// Timeline horizontal — range/eco/posicoes/auto-scroll/cursor "Agora"
|
||||||
@@ -587,10 +628,19 @@ const settingsOpen = ref(false);
|
|||||||
// Pai so passa eventos brutos + workRules/settings/feriados via props,
|
// Pai so passa eventos brutos + workRules/settings/feriados via props,
|
||||||
// e recebe @evento (pra abrir dialog) + @clear-filter (pra limpar tipo).
|
// e recebe @evento (pra abrir dialog) + @clear-filter (pra limpar tipo).
|
||||||
|
|
||||||
// Contagens por tipo + frase resumo do dia
|
// Contagens por tipo + frase resumo do dia. Pra sessao tambem quebro
|
||||||
|
// por status (cancelado/remarcado) pra montar o sufixo "(x foi cancelado,
|
||||||
|
// x foi remarcado)" depois do chip de atendimentos.
|
||||||
const contagensDia = computed(() => {
|
const contagensDia = computed(() => {
|
||||||
const c = { sessao: 0, supervisao: 0, reuniao: 0 };
|
const c = { sessao: 0, supervisao: 0, reuniao: 0, sessaoCancelada: 0, sessaoRemarcada: 0 };
|
||||||
for (const ev of eventosHojeReais.value) c[ev.tipo] = (c[ev.tipo] || 0) + 1;
|
for (const ev of eventosHojeReais.value) {
|
||||||
|
c[ev.tipo] = (c[ev.tipo] || 0) + 1;
|
||||||
|
if (ev.tipo === 'sessao') {
|
||||||
|
const s = String(ev.status || '').toLowerCase();
|
||||||
|
if (s === 'cancelado' || s === 'cancelada') c.sessaoCancelada += 1;
|
||||||
|
else if (s === 'remarcado') c.sessaoRemarcada += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
return c;
|
return c;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -598,11 +648,34 @@ function pluralizar(n, singular, plural) {
|
|||||||
return `${n} ${n === 1 ? singular : plural}`;
|
return `${n} ${n === 1 ? singular : plural}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sufixo "(1 foi cancelado, 2 foram remarcados)" depois do chip de
|
||||||
|
// atendimentos quando houver sessoes cancel/remarcado no dia.
|
||||||
|
function _statusSuffix(qtdCancel, qtdRemarc) {
|
||||||
|
const partes = [];
|
||||||
|
if (qtdCancel > 0) {
|
||||||
|
partes.push(qtdCancel === 1
|
||||||
|
? `${qtdCancel} foi cancelado`
|
||||||
|
: `${qtdCancel} foram cancelados`);
|
||||||
|
}
|
||||||
|
if (qtdRemarc > 0) {
|
||||||
|
partes.push(qtdRemarc === 1
|
||||||
|
? `${qtdRemarc} foi remarcado`
|
||||||
|
: `${qtdRemarc} foram remarcados`);
|
||||||
|
}
|
||||||
|
return partes.length ? ` (${partes.join(', ')})` : '';
|
||||||
|
}
|
||||||
|
|
||||||
// Partes estruturadas pro template renderizar cada contagem como link clicável
|
// Partes estruturadas pro template renderizar cada contagem como link clicável
|
||||||
const resumoPartes = computed(() => {
|
const resumoPartes = computed(() => {
|
||||||
const c = contagensDia.value;
|
const c = contagensDia.value;
|
||||||
const partes = [];
|
const partes = [];
|
||||||
if (c.sessao > 0) partes.push({ tipo: 'sessao', text: pluralizar(c.sessao, 'atendimento', 'atendimentos') });
|
if (c.sessao > 0) {
|
||||||
|
partes.push({
|
||||||
|
tipo: 'sessao',
|
||||||
|
text: pluralizar(c.sessao, 'atendimento', 'atendimentos'),
|
||||||
|
suffix: _statusSuffix(c.sessaoCancelada, c.sessaoRemarcada)
|
||||||
|
});
|
||||||
|
}
|
||||||
if (c.supervisao > 0) partes.push({ tipo: 'supervisao', text: pluralizar(c.supervisao, 'supervisão', 'supervisões') });
|
if (c.supervisao > 0) partes.push({ tipo: 'supervisao', text: pluralizar(c.supervisao, 'supervisão', 'supervisões') });
|
||||||
if (c.reuniao > 0) partes.push({ tipo: 'reuniao', text: pluralizar(c.reuniao, 'reunião', 'reuniões') });
|
if (c.reuniao > 0) partes.push({ tipo: 'reuniao', text: pluralizar(c.reuniao, 'reunião', 'reuniões') });
|
||||||
return partes;
|
return partes;
|
||||||
@@ -1745,6 +1818,22 @@ function _callOnAgenda(action) {
|
|||||||
if (secaoAberta.value !== 'agenda') abrirSecao('agenda');
|
if (secaoAberta.value !== 'agenda') abrirSecao('agenda');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MelissaBusca @goto-date — usuario digitou "hoje"/"20/06" na busca
|
||||||
|
// global. Abre a agenda se fechada (via _callOnAgenda que enfileira a
|
||||||
|
// action ate o ref aparecer) e chama gotoDate exposto pela MelissaAgenda.
|
||||||
|
function onBuscaGotoDate(date) {
|
||||||
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return;
|
||||||
|
_callOnAgenda((agenda) => agenda.gotoDate?.(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ref + provide pra qualquer secao filha pedir pra abrir a busca global
|
||||||
|
// programaticamente. UI nao tem mais botao por secao (lupa unica fica
|
||||||
|
// na .melissa-tray), mas o inject permanece exposto pra acoes contextuais
|
||||||
|
// futuras (ex: "buscar paciente" num componente filho que quer abrir
|
||||||
|
// o spotlight com query pre-preenchida).
|
||||||
|
const melissaBuscaRef = ref(null);
|
||||||
|
provide('openMelissaBusca', () => melissaBuscaRef.value?.openDialog?.());
|
||||||
|
|
||||||
function onAbrirProntuario() {
|
function onAbrirProntuario() {
|
||||||
const ev = eventoSelecionado.value;
|
const ev = eventoSelecionado.value;
|
||||||
if (!ev?.patient_id) {
|
if (!ev?.patient_id) {
|
||||||
@@ -1987,6 +2076,30 @@ const { toqueTermino, testarToque } = useMelissaToques('sino');
|
|||||||
function abrirCronometro() {
|
function abrirCronometro() {
|
||||||
cronoRef.value?.abrir();
|
cronoRef.value?.abrir();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Click no botao ⏱ de um evento da timeline (ou no CTA do card
|
||||||
|
// "Proximo paciente" quando o evento esta em curso). Pre-seleciona
|
||||||
|
// o paciente + autostart. Se ja houver cronometro rodando de outro
|
||||||
|
// paciente, mostra toast sem trocar (opcao b decidida 2026-05-22).
|
||||||
|
function onIniciarCronometroFromEvento(ev) {
|
||||||
|
if (!ev?.patient_id) return;
|
||||||
|
// Plano programado: horario original do evento na agenda. So passa se
|
||||||
|
// os campos forem numericos validos — abrir() sanitiza de novo internamente.
|
||||||
|
const sessionPlan = (typeof ev.startH === 'number' && typeof ev.endH === 'number')
|
||||||
|
? { startH: ev.startH, endH: ev.endH }
|
||||||
|
: null;
|
||||||
|
const ret = cronoRef.value?.abrir({ pacienteId: ev.patient_id, autostart: true, sessionPlan });
|
||||||
|
if (ret && !ret.opened && ret.alreadyRunning && !ret.samePaciente) {
|
||||||
|
const atualNome = pacientesReais.value.find((p) => String(p.id) === String(ret.pacienteId))?.nome
|
||||||
|
|| 'outro paciente';
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Cronômetro já ativo',
|
||||||
|
detail: `Sessão de ${atualNome} em andamento. Pare o cronômetro atual antes de iniciar outro.`,
|
||||||
|
life: 3500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
function fecharCronometro() {
|
function fecharCronometro() {
|
||||||
cronoRef.value?.fechar();
|
cronoRef.value?.fechar();
|
||||||
}
|
}
|
||||||
@@ -2347,71 +2460,6 @@ function onKeydown(e) {
|
|||||||
<!-- PLANO DE TRÁS — Resumo (recebe blur quando workspace abre) -->
|
<!-- PLANO DE TRÁS — Resumo (recebe blur quando workspace abre) -->
|
||||||
<!-- ═══════════════════════════════════════════════════════ -->
|
<!-- ═══════════════════════════════════════════════════════ -->
|
||||||
<div class="win11-summary" :class="{ 'is-behind': summaryDimmed }">
|
<div class="win11-summary" :class="{ 'is-behind': summaryDimmed }">
|
||||||
<!-- Faixa de fundo do topbar — gradiente horizontal
|
|
||||||
(cor solida na direita -> transparente na esquerda)
|
|
||||||
pra dar legibilidade aos icones sem virar barra solida.
|
|
||||||
Cor flipa com light/dark via --m-band. -->
|
|
||||||
<div class="melissa-topbar-band" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
<!-- Topbar Melissa (canto sup. direito): plan-DEV + notificações
|
|
||||||
+ ajuda + cog. Os 3 primeiros vêm do AppTopbar — replicados
|
|
||||||
aqui porque a rota /melissa é fullscreen e não monta o
|
|
||||||
AppLayout. Drawer de notificações também é montado abaixo
|
|
||||||
(AjudaDrawer já é global no App.vue). -->
|
|
||||||
<div class="absolute top-5 right-5 z-30 flex items-center gap-2">
|
|
||||||
<!-- Plan switcher DEV (só aparece em dev / com flag) -->
|
|
||||||
<button
|
|
||||||
v-if="showPlanDevMenu"
|
|
||||||
ref="planBtn"
|
|
||||||
class="glass-btn w-10 h-10 grid place-items-center"
|
|
||||||
:disabled="planMenuLoading || trocandoPlano"
|
|
||||||
title="Plano (DEV)"
|
|
||||||
@click="openPlanMenu"
|
|
||||||
>
|
|
||||||
<i v-if="planMenuLoading || trocandoPlano" class="pi pi-spin pi-spinner text-white/90 text-base" />
|
|
||||||
<i v-else class="pi pi-sliders-h text-white/90 text-base" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Notificações -->
|
|
||||||
<button
|
|
||||||
class="glass-btn w-10 h-10 grid place-items-center relative"
|
|
||||||
title="Notificações"
|
|
||||||
@click="notificationStore.drawerOpen = true"
|
|
||||||
>
|
|
||||||
<i class="pi pi-bell text-white/90 text-base" />
|
|
||||||
<span
|
|
||||||
v-if="notificationStore.unreadCount > 0"
|
|
||||||
class="m-topbar-badge"
|
|
||||||
>{{ notificationStore.unreadCount > 99 ? '99+' : notificationStore.unreadCount }}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Ajuda -->
|
|
||||||
<button
|
|
||||||
class="glass-btn w-10 h-10 grid place-items-center"
|
|
||||||
:class="{ 'glass-btn--active': ajudaDrawerOpen }"
|
|
||||||
:title="ajudaDrawerOpen ? 'Fechar ajuda' : 'Ajuda'"
|
|
||||||
@click="toggleAjuda"
|
|
||||||
>
|
|
||||||
<i class="pi pi-question-circle text-white/90 text-base" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Cog (settings popover) — existente -->
|
|
||||||
<button
|
|
||||||
class="glass-btn w-10 h-10 grid place-items-center"
|
|
||||||
title="Personalizar"
|
|
||||||
@click="settingsOpen = !settingsOpen"
|
|
||||||
>
|
|
||||||
<i class="pi pi-cog text-white/90 text-base" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Transition name="settings-pop">
|
|
||||||
<MelissaSettingsPanel
|
|
||||||
v-if="settingsOpen"
|
|
||||||
@close="settingsOpen = false"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Conteúdo central -->
|
<!-- Conteúdo central -->
|
||||||
<div class="win11-summary__inner">
|
<div class="win11-summary__inner">
|
||||||
<!-- Bloco hero: relógio + data + saudação + resumo do dia -->
|
<!-- Bloco hero: relógio + data + saudação + resumo do dia -->
|
||||||
@@ -2428,6 +2476,7 @@ function onKeydown(e) {
|
|||||||
|
|
||||||
<!-- Busca rápida (entre o "Hoje há" e a timeline) -->
|
<!-- Busca rápida (entre o "Hoje há" e a timeline) -->
|
||||||
<MelissaBusca
|
<MelissaBusca
|
||||||
|
ref="melissaBuscaRef"
|
||||||
class="mt-8"
|
class="mt-8"
|
||||||
:pacientes="pacientesReais"
|
:pacientes="pacientesReais"
|
||||||
:eventos="eventosHojeReais"
|
:eventos="eventosHojeReais"
|
||||||
@@ -2436,6 +2485,7 @@ function onKeydown(e) {
|
|||||||
@evento="abrirEvento"
|
@evento="abrirEvento"
|
||||||
@documento="(d) => d?.patient_id ? router.push({ path: '/melissa/paciente', query: { id: String(d.patient_id), tab: 'documentos' } }) : abrirSecao('pacientes')"
|
@documento="(d) => d?.patient_id ? router.push({ path: '/melissa/paciente', query: { id: String(d.patient_id), tab: 'documentos' } }) : abrirSecao('pacientes')"
|
||||||
@intake="() => abrirSecao('cadastros-recebidos')"
|
@intake="() => abrirSecao('cadastros-recebidos')"
|
||||||
|
@goto-date="onBuscaGotoDate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Timeline horizontal + vertical (responsivo) -->
|
<!-- Timeline horizontal + vertical (responsivo) -->
|
||||||
@@ -2448,6 +2498,7 @@ function onKeydown(e) {
|
|||||||
:filtro-tipo="filtroTipo"
|
:filtro-tipo="filtroTipo"
|
||||||
@evento="abrirEvento"
|
@evento="abrirEvento"
|
||||||
@clear-filter="limparFiltro"
|
@clear-filter="limparFiltro"
|
||||||
|
@iniciar-cronometro="onIniciarCronometroFromEvento"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Cards (catálogo + ativos + layout switchável) -->
|
<!-- Cards (catálogo + ativos + layout switchável) -->
|
||||||
@@ -2460,14 +2511,24 @@ function onKeydown(e) {
|
|||||||
:class="cardsLayout === 'linha-unica' ? 'cards-row--linha' : 'cards-row--wrap'"
|
:class="cardsLayout === 'linha-unica' ? 'cards-row--linha' : 'cards-row--wrap'"
|
||||||
>
|
>
|
||||||
<template v-for="cardId in cardsAtivos" :key="cardId">
|
<template v-for="cardId in cardsAtivos" :key="cardId">
|
||||||
<!-- Próximo paciente -->
|
<!-- Próximo paciente. Se o evento esta em curso E tem
|
||||||
|
paciente, action vira "Iniciar cronômetro" pra
|
||||||
|
facilitar o fluxo "estou comecando a sessao agora". -->
|
||||||
<MelissaCard
|
<MelissaCard
|
||||||
v-if="cardId === 'proximo-paciente'"
|
v-if="cardId === 'proximo-paciente'"
|
||||||
icon="pi pi-user"
|
icon="pi pi-user"
|
||||||
icon-color="text-emerald-300"
|
icon-color="text-emerald-300"
|
||||||
title="Próximo paciente"
|
title="Próximo paciente"
|
||||||
:action-title="proximoPaciente ? 'Abrir sessão' : 'Abrir Pacientes'"
|
:action-title="proximoPaciente
|
||||||
@open="proximoPaciente ? abrirEvento(proximoPaciente.ev) : abrirSecao('pacientes')"
|
? (proximoPaciente.emCurso && proximoPaciente.ev?.patient_id
|
||||||
|
? 'Iniciar cronômetro'
|
||||||
|
: 'Abrir sessão')
|
||||||
|
: 'Abrir Pacientes'"
|
||||||
|
@open="proximoPaciente
|
||||||
|
? (proximoPaciente.emCurso && proximoPaciente.ev?.patient_id
|
||||||
|
? onIniciarCronometroFromEvento(proximoPaciente.ev)
|
||||||
|
: abrirEvento(proximoPaciente.ev))
|
||||||
|
: abrirSecao('pacientes')"
|
||||||
>
|
>
|
||||||
<div v-if="proximoPaciente" class="flex items-center gap-3">
|
<div v-if="proximoPaciente" class="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
@@ -2613,14 +2674,163 @@ function onKeydown(e) {
|
|||||||
<span class="psi-kbd" aria-hidden="true">Ctrl \</span>
|
<span class="psi-kbd" aria-hidden="true">Ctrl \</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════ -->
|
||||||
|
<!-- TRAY Melissa (system tray win11-style, canto inf. direito) -->
|
||||||
|
<!-- busca + plan-DEV + notificações + ajuda + cog. Sibling de -->
|
||||||
|
<!-- .dock (fora de .win11-summary) pra ficar sempre interativo,-->
|
||||||
|
<!-- mesmo com secao aberta (que aplica blur+pointer-none na -->
|
||||||
|
<!-- summary). AjudaDrawer ja e global no App.vue. -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════ -->
|
||||||
|
<div class="melissa-tray">
|
||||||
|
<!-- Busca global (Ctrl+K) — afordancia visivel pra mouse/touch.
|
||||||
|
Centraliza o ponto de acesso entre seccoes (em vez de
|
||||||
|
cada toolbar ter o proprio botao). -->
|
||||||
|
<button
|
||||||
|
class="glass-btn w-10 h-10 grid place-items-center"
|
||||||
|
v-tooltip.top="'Buscar (Ctrl+K)'"
|
||||||
|
aria-label="Busca global"
|
||||||
|
@click="melissaBuscaRef?.openDialog?.()"
|
||||||
|
>
|
||||||
|
<i class="pi pi-search text-white/90 text-base" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Plan switcher DEV (só aparece em dev / com flag).
|
||||||
|
Em mobile (<md) some — vai pro popup do "more". -->
|
||||||
|
<button
|
||||||
|
v-if="showPlanDevMenu"
|
||||||
|
ref="planBtn"
|
||||||
|
class="glass-btn w-10 h-10 hidden md:grid place-items-center"
|
||||||
|
:disabled="planMenuLoading || trocandoPlano"
|
||||||
|
v-tooltip.top="'Plano (DEV)'"
|
||||||
|
@click="openPlanMenu"
|
||||||
|
>
|
||||||
|
<i v-if="planMenuLoading || trocandoPlano" class="pi pi-spin pi-spinner text-white/90 text-base" />
|
||||||
|
<i v-else class="pi pi-sliders-h text-white/90 text-base" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Notificações — em mobile (<md) some, vai pro popup. -->
|
||||||
|
<button
|
||||||
|
class="glass-btn w-10 h-10 hidden md:grid place-items-center relative"
|
||||||
|
v-tooltip.top="'Notificações'"
|
||||||
|
@click="notificationStore.drawerOpen = true"
|
||||||
|
>
|
||||||
|
<i class="pi pi-bell text-white/90 text-base" />
|
||||||
|
<span
|
||||||
|
v-if="notificationStore.unreadCount > 0"
|
||||||
|
class="m-topbar-badge"
|
||||||
|
>{{ notificationStore.unreadCount > 99 ? '99+' : notificationStore.unreadCount }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Ajuda (conteudo muda com a rota via AjudaDrawer global).
|
||||||
|
data-ajuda-toggle: marca pro AjudaDrawer ignorar este botao
|
||||||
|
na deteccao de "clicou fora pra fechar" (senao fecha aqui
|
||||||
|
e o @click reabre). Em mobile (<md) some, vai pro popup. -->
|
||||||
|
<button
|
||||||
|
class="glass-btn w-10 h-10 hidden md:grid place-items-center"
|
||||||
|
:class="{ 'glass-btn--active': ajudaDrawerOpen }"
|
||||||
|
v-tooltip.top="ajudaDrawerOpen ? 'Fechar ajuda' : 'Ajuda'"
|
||||||
|
data-ajuda-toggle
|
||||||
|
@click="toggleAjuda"
|
||||||
|
>
|
||||||
|
<i class="pi pi-question-circle text-white/90 text-base" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Cog (settings popover abre pra CIMA — vide MelissaSettingsPanel).
|
||||||
|
Em mobile (<md) some, vai pro popup. -->
|
||||||
|
<button
|
||||||
|
ref="cogBtnEl"
|
||||||
|
class="glass-btn w-10 h-10 hidden md:grid place-items-center"
|
||||||
|
v-tooltip.top="'Personalizar'"
|
||||||
|
@click="settingsOpen = !settingsOpen"
|
||||||
|
>
|
||||||
|
<i class="pi pi-cog text-white/90 text-base" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Transition name="settings-pop">
|
||||||
|
<MelissaSettingsPanel
|
||||||
|
v-if="settingsOpen"
|
||||||
|
@close="settingsOpen = false"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- "More" — so em mobile (<md). Collapse de plan-DEV+bell+help+
|
||||||
|
cog num popup vertical. Dot vermelho aparece se houver
|
||||||
|
notificacoes nao-lidas (preserva o sinal visual da bell). -->
|
||||||
|
<button
|
||||||
|
ref="trayMoreBtnEl"
|
||||||
|
class="glass-btn w-10 h-10 grid md:hidden place-items-center relative"
|
||||||
|
:class="{ 'glass-btn--active': trayMoreOpen }"
|
||||||
|
v-tooltip.top="trayMoreOpen ? 'Fechar' : 'Mais'"
|
||||||
|
aria-label="Mais opcoes"
|
||||||
|
@click="trayMoreOpen = !trayMoreOpen"
|
||||||
|
>
|
||||||
|
<i class="pi pi-ellipsis-v text-white/90 text-base" />
|
||||||
|
<span
|
||||||
|
v-if="notificationStore.unreadCount > 0"
|
||||||
|
class="melissa-tray__more-dot"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Transition name="settings-pop">
|
||||||
|
<div v-if="trayMoreOpen" class="melissa-tray__more-panel glass-panel">
|
||||||
|
<!-- Notificacoes -->
|
||||||
|
<button
|
||||||
|
class="melissa-tray__more-item"
|
||||||
|
@click="notificationStore.drawerOpen = true; trayMoreOpen = false"
|
||||||
|
>
|
||||||
|
<i class="pi pi-bell" />
|
||||||
|
<span class="flex-1 text-left">Notificações</span>
|
||||||
|
<span
|
||||||
|
v-if="notificationStore.unreadCount > 0"
|
||||||
|
class="m-topbar-badge"
|
||||||
|
style="position: static; transform: none;"
|
||||||
|
>{{ notificationStore.unreadCount > 99 ? '99+' : notificationStore.unreadCount }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Ajuda -->
|
||||||
|
<button
|
||||||
|
class="melissa-tray__more-item"
|
||||||
|
:class="{ 'is-active': ajudaDrawerOpen }"
|
||||||
|
data-ajuda-toggle
|
||||||
|
@click="toggleAjuda(); trayMoreOpen = false"
|
||||||
|
>
|
||||||
|
<i class="pi pi-question-circle" />
|
||||||
|
<span class="flex-1 text-left">{{ ajudaDrawerOpen ? 'Fechar ajuda' : 'Ajuda' }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Personalizar (cog) -->
|
||||||
|
<button
|
||||||
|
class="melissa-tray__more-item"
|
||||||
|
@click="settingsOpen = !settingsOpen; trayMoreOpen = false"
|
||||||
|
>
|
||||||
|
<i class="pi pi-cog" />
|
||||||
|
<span class="flex-1 text-left">Personalizar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Plano DEV (so se flag) -->
|
||||||
|
<button
|
||||||
|
v-if="showPlanDevMenu"
|
||||||
|
class="melissa-tray__more-item"
|
||||||
|
:disabled="planMenuLoading || trocandoPlano"
|
||||||
|
@click="openPlanMenu($event); trayMoreOpen = false"
|
||||||
|
>
|
||||||
|
<i :class="planMenuLoading || trocandoPlano ? 'pi pi-spin pi-spinner' : 'pi pi-sliders-h'" />
|
||||||
|
<span class="flex-1 text-left">Plano (DEV)</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════ -->
|
<!-- ═══════════════════════════════════════════════════════ -->
|
||||||
<!-- DOCK (taskbar Win11-style sem chrome) — receptáculo pra -->
|
<!-- DOCK (taskbar Win11-style sem chrome) — receptáculo pra -->
|
||||||
<!-- elementos minimizados (cronômetro, futuros) + atalhos -->
|
<!-- elementos minimizados (cronômetro, futuros) + atalhos -->
|
||||||
<!-- pinned (Agenda, WhatsApp). Transparent, só os items são -->
|
<!-- pinned (Agenda, Pacientes, WhatsApp, Financeiro). Transp.-->
|
||||||
<!-- clicáveis. ψ vive ao lado (absolute, bottom-left). -->
|
<!-- só os items sao clicaveis. ψ vive ao lado (absolute, -->
|
||||||
|
<!-- bottom-left). -->
|
||||||
<!-- ═══════════════════════════════════════════════════════ -->
|
<!-- ═══════════════════════════════════════════════════════ -->
|
||||||
<div class="melissa-dock">
|
<div class="melissa-dock">
|
||||||
<!-- Pinned: atalhos diretos pras seções mais usadas.
|
<!-- Pinned builtins: 4 atalhos pras secoes principais.
|
||||||
Tamanho menor que ψ (44px vs 56px) + cantos arredondados
|
Tamanho menor que ψ (44px vs 56px) + cantos arredondados
|
||||||
mas não full-circle, pra hierarquia visual ficar óbvia. -->
|
mas não full-circle, pra hierarquia visual ficar óbvia. -->
|
||||||
<button
|
<button
|
||||||
@@ -2632,6 +2842,15 @@ function onKeydown(e) {
|
|||||||
>
|
>
|
||||||
<i class="pi pi-calendar" />
|
<i class="pi pi-calendar" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dock-pin"
|
||||||
|
v-tooltip.top="'Pacientes'"
|
||||||
|
:class="{ 'dock-pin--active': secaoAberta === 'pacientes' }"
|
||||||
|
@click="abrirSecao('pacientes')"
|
||||||
|
>
|
||||||
|
<i class="pi pi-users" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="dock-pin"
|
class="dock-pin"
|
||||||
@@ -2646,12 +2865,24 @@ function onKeydown(e) {
|
|||||||
:title="`${whatsappPendente.count} mensagens não lidas`"
|
:title="`${whatsappPendente.count} mensagens não lidas`"
|
||||||
>{{ whatsappPendente.count > 99 ? '99+' : whatsappPendente.count }}</span>
|
>{{ whatsappPendente.count > 99 ? '99+' : whatsappPendente.count }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dock-pin"
|
||||||
|
v-tooltip.top="'Financeiro'"
|
||||||
|
:class="{ 'dock-pin--active': secaoAberta === 'financeiro' }"
|
||||||
|
@click="abrirSecao('financeiro')"
|
||||||
|
>
|
||||||
|
<i class="pi pi-wallet" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Divisor entre builtins e pins dinâmicos. Só aparece se
|
<!-- Divisor entre builtins e pins dinâmicos. Só aparece se
|
||||||
o user tem pelo menos 1 pin (fixo ou recente). -->
|
o user tem pelo menos 1 pin (fixo ou recente).
|
||||||
|
Em mobile (<md), se so houver MRU (que e oculto), o
|
||||||
|
divisor tambem some pra nao "soltar" no fim do dock. -->
|
||||||
<div
|
<div
|
||||||
v-if="dockPins.pinned.value.length || dockPins.recent.value.length"
|
v-if="dockPins.pinned.value.length || dockPins.recent.value.length"
|
||||||
class="dock-divider"
|
class="dock-divider"
|
||||||
|
:class="{ 'hidden md:block': !dockPins.pinned.value.length }"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -2671,7 +2902,11 @@ function onKeydown(e) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Pins MRU (max 3) — empurrados pelas últimas seções abertas.
|
<!-- Pins MRU (max 3) — empurrados pelas últimas seções abertas.
|
||||||
Visual mais leve (opacity menor) pra destacar dos fixos. -->
|
Visual mais leve (opacity menor) pra destacar dos fixos.
|
||||||
|
Em mobile (<md=768px) sao ocultos via media query no CSS
|
||||||
|
do .dock-pin--recent — utility 'hidden' do Tailwind perde
|
||||||
|
pro 'display: grid' base do .dock-pin (mesma specificity,
|
||||||
|
ordem de carga). -->
|
||||||
<button
|
<button
|
||||||
v-for="slug in dockPins.recent.value" :key="`r-${slug}`"
|
v-for="slug in dockPins.recent.value" :key="`r-${slug}`"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -3856,7 +4091,7 @@ function onKeydown(e) {
|
|||||||
.settings-pop-enter-from,
|
.settings-pop-enter-from,
|
||||||
.settings-pop-leave-to {
|
.settings-pop-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-6px) scale(0.98);
|
transform: translateY(6px) scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* line-clamp util (caso Tailwind não tenha) */
|
/* line-clamp util (caso Tailwind não tenha) */
|
||||||
@@ -4011,24 +4246,88 @@ function onKeydown(e) {
|
|||||||
--m-hero-text-border: rgba(255, 255, 255, 0.12);
|
--m-hero-text-border: rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Faixa de fundo do topbar (canto sup. direito) ──────────────
|
/* ─── Tray Melissa (system tray win11-style, canto inferior direito)
|
||||||
Gradiente horizontal: cor solida na direita (onde os icones vivem)
|
Posiciona o grupo de icones globais (plan-DEV, notificacoes, ajuda,
|
||||||
e fade pra transparente na esquerda. z-index abaixo do topbar
|
cog) alinhado verticalmente com os pins do dock. z-index acima do
|
||||||
(z-30) e acima do conteudo principal. */
|
dock (65) pra ficar no mesmo plano interativo. */
|
||||||
.melissa-topbar-band {
|
.melissa-tray {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
bottom: 0;
|
||||||
left: 0;
|
right: 1.25rem;
|
||||||
right: 0;
|
height: var(--m-dock-h, 76px);
|
||||||
height: 80px;
|
z-index: 66;
|
||||||
z-index: 25;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dot vermelho no botao "More" da tray (mobile) — sinaliza que ha
|
||||||
|
notificacoes nao-lidas escondidas dentro do popup. */
|
||||||
|
.melissa-tray__more-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: rgb(239, 68, 68);
|
||||||
|
border: 1.5px solid var(--m-bg-medium, rgba(0, 0, 0, 0.4));
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: linear-gradient(
|
}
|
||||||
to left,
|
|
||||||
var(--m-band) 0%,
|
/* Popup vertical do "More" — abre acima do botao trigger, mesmo padrao
|
||||||
var(--m-band) 25%,
|
visual do MelissaSettingsPanel mas mais compacto. */
|
||||||
transparent 75%
|
.melissa-tray__more-panel {
|
||||||
);
|
position: absolute;
|
||||||
|
bottom: calc(var(--m-dock-h, 76px) - 8px);
|
||||||
|
right: 0;
|
||||||
|
min-width: 220px;
|
||||||
|
padding: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
border-radius: 14px;
|
||||||
|
z-index: 67;
|
||||||
|
}
|
||||||
|
|
||||||
|
.melissa-tray__more-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--m-text);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
text-align: left;
|
||||||
|
transition: background-color 140ms ease, color 140ms ease;
|
||||||
|
}
|
||||||
|
.melissa-tray__more-item:hover:not(:disabled),
|
||||||
|
.melissa-tray__more-item:focus-visible {
|
||||||
|
background: var(--m-bg-soft-hover);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.melissa-tray__more-item.is-active {
|
||||||
|
background: var(--m-bg-soft-hover);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.melissa-tray__more-item:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.melissa-tray__more-item > i {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
width: 16px;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.melissa-tray__more-item:hover > i,
|
||||||
|
.melissa-tray__more-item.is-active > i {
|
||||||
|
color: var(--m-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Dock (global pra atravessar Teleport + evitar perda de scoped
|
/* ─── Dock (global pra atravessar Teleport + evitar perda de scoped
|
||||||
@@ -4165,6 +4464,13 @@ html:not(.app-dark) .dock-divider {
|
|||||||
.dock-pin--recent.dock-pin--active {
|
.dock-pin--recent.dock-pin--active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
/* Em mobile (<md=768px) os pins MRU somem pra economizar largura
|
||||||
|
(4 builtins + user-fixed pins ja saturam o dock). Media query no
|
||||||
|
bloco do .dock-pin--recent ganha do utility 'hidden' do Tailwind
|
||||||
|
por ordem de carga (mesma specificity). */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.dock-pin--recent { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Skeleton loading utilitário (global pra atravessar scoped CSS dos
|
/* ─── Skeleton loading utilitário (global pra atravessar scoped CSS dos
|
||||||
componentes filhos). Use a classe .melissa-skeleton em qualquer
|
componentes filhos). Use a classe .melissa-skeleton em qualquer
|
||||||
|
|||||||
Reference in New Issue
Block a user