Melissa: deep-link via URL + Pacientes (WIP) + cronometro reset

Roteamento por URL (substitui o ref local secaoAberta):
- routes.misc.js: rota vira /preview/melissa/:secao? — param opcional
- MelissaLayout.vue: secaoAberta agora e computed do route.params.secao,
  validado contra SECOES (chave invalida -> null). abrirSecao/fecharSecao
  fazem router.push em vez de mutar ref. Habilita back/forward, refresh
  e deep-link tipo /preview/melissa/agenda.

Pagina Pacientes (WIP, ainda nao wireada no slot do Layout):
- src/layout/melissa/MelissaPacientes.vue (novo, ~? linhas) — fullscreen
  3-col espelhando MelissaAgenda: aside esquerda com filtros (status /
  grupos / tags), lista central com cards + busca, quick view direita
  com KPIs do paciente selecionado + acoes.
- Carrega pacientes (todos os status), grupos/tags do tenant, vinculos
  patient_groups + patient_tags + session counts em paralelo.
- Integra PatientProntuario (overlay), PatientCadastroDialog,
  PatientCreatePopover + ComponentCadastroRapido, e
  conversationDrawerStore (acao WhatsApp da quick view).

useMelissaPacientes ganha opcao { onlyActive }:
- default true (compat com cards do resumo / cronometro / eventos hoje
  — so faz sentido com ativos)
- false retorna Ativo + Inativo + Arquivado, pra uso na pagina nova
- select agora inclui data_nascimento (necessario pros KPIs da quick view)

Cronometro: zera ao parar — terminou a sessao, fica pronto pra proxima
sem precisar reabrir o popover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-04-28 17:12:15 -03:00
parent 7b67bd083a
commit 06bce11e1c
5 changed files with 1755 additions and 8 deletions
+2
View File
@@ -112,6 +112,8 @@ function toggle() {
if (timer) clearInterval(timer); if (timer) clearInterval(timer);
timer = null; timer = null;
running.value = false; running.value = false;
// Zera ao parar — sessão acabou, deixa pronto pra próxima
seconds.value = props.duracaoMinutos * 60;
} else { } else {
if (timer) clearInterval(timer); if (timer) clearInterval(timer);
timer = setInterval(() => { timer = setInterval(() => {
+14 -3
View File
@@ -15,6 +15,7 @@
* Rota atual (sandbox): /preview/melissa * Rota atual (sandbox): /preview/melissa
*/ */
import { ref, computed, watch, onMounted, onBeforeUnmount, provide } from 'vue'; import { ref, computed, watch, onMounted, onBeforeUnmount, provide } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { useLayout } from '@/layout/composables/layout'; import { useLayout } from '@/layout/composables/layout';
import { applyThemeEngine } from '@/theme/theme.options'; import { applyThemeEngine } from '@/theme/theme.options';
@@ -91,16 +92,26 @@ const SECOES = {
medicos: { label: 'Médicos e referências', icon: 'pi pi-user-edit', descricao: 'Profissionais que encaminham ou recebem pacientes.' } medicos: { label: 'Médicos e referências', icon: 'pi pi-user-edit', descricao: 'Profissionais que encaminham ou recebem pacientes.' }
}; };
const secaoAberta = ref(null); // chave em SECOES, ou null // Seção ativa = param `:secao?` da rota. URL é a fonte da verdade pra
// permitir back/forward e deep-link (ex: /preview/melissa/agenda abre o
// overlay da agenda direto). Se a chave for inválida, vira null.
const router = useRouter();
const route = useRoute();
const secaoAberta = computed(() => {
const s = route.params?.secao;
return s && SECOES[s] ? s : null;
});
function abrirSecao(key) { function abrirSecao(key) {
// Fecha overlays paralelos pra evitar empilhamento // Fecha overlays paralelos pra evitar empilhamento
workspaceOpen.value = false; workspaceOpen.value = false;
eventoSelecionado.value = null; eventoSelecionado.value = null;
secaoAberta.value = key; if (key === secaoAberta.value) return; // no-op, evita push duplicado
router.push({ name: 'PreviewMelissa', params: { secao: key } });
} }
function fecharSecao() { function fecharSecao() {
secaoAberta.value = null; if (!secaoAberta.value) return;
router.push({ name: 'PreviewMelissa', params: {} });
} }
// Prefs de layout/UI (toque, fundo, opacidade, formato hora) // Prefs de layout/UI (toque, fundo, opacidade, formato hora)
File diff suppressed because it is too large Load Diff
@@ -27,7 +27,16 @@ function normalizeStatus(s) {
return v.charAt(0).toUpperCase() + v.slice(1); return v.charAt(0).toUpperCase() + v.slice(1);
} }
export function useMelissaPacientes() { /**
* @param {object} [opts]
* @param {boolean} [opts.onlyActive=true]
* true (default, legado) = retorna só status='Ativo' (uso original: cards do
* resumo, cronômetro, eventos hoje — só faz sentido com ativos).
* false = retorna todos (Ativo + Inativo + Arquivado), pra páginas que
* precisam mostrar/filtrar por status (ex.: MelissaPacientes).
*/
export function useMelissaPacientes(opts = {}) {
const onlyActive = opts.onlyActive !== false; // default true (compat)
const tenantStore = useTenantStore(); const tenantStore = useTenantStore();
const pacientes = ref([]); const pacientes = ref([]);
@@ -66,7 +75,7 @@ export function useMelissaPacientes() {
// ('ativo', 'Ativo', 'active', null...). Normaliza e filtra no client. // ('ativo', 'Ativo', 'active', null...). Normaliza e filtra no client.
const { data, error: err } = await supabase const { data, error: err } = await supabase
.from('patients') .from('patients')
.select('id, nome_completo, email_principal, telefone, status, avatar_url, last_attended_at, created_at') .select('id, nome_completo, email_principal, telefone, status, avatar_url, last_attended_at, created_at, data_nascimento')
.eq('owner_id', userId) .eq('owner_id', userId)
.eq('tenant_id', tid) .eq('tenant_id', tid)
.order('nome_completo', { ascending: true }) .order('nome_completo', { ascending: true })
@@ -82,10 +91,11 @@ export function useMelissaPacientes() {
avatar_url: r.avatar_url || null, avatar_url: r.avatar_url || null,
status: normalizeStatus(r.status), status: normalizeStatus(r.status),
last_attended_at: r.last_attended_at || null, last_attended_at: r.last_attended_at || null,
created_at: r.created_at || null created_at: r.created_at || null,
data_nascimento: r.data_nascimento || null
})); }));
pacientes.value = todos.filter((p) => p.status === 'Ativo'); pacientes.value = onlyActive ? todos.filter((p) => p.status === 'Ativo') : todos;
} catch (e) { } catch (e) {
error.value = e?.message || 'Erro ao carregar pacientes'; error.value = e?.message || 'Erro ao carregar pacientes';
pacientes.value = []; pacientes.value = [];
+4 -1
View File
@@ -25,8 +25,11 @@ export default {
// Sandbox do layout Melissa (Direção B — lockscreen-style) // Sandbox do layout Melissa (Direção B — lockscreen-style)
// Standalone, sem auth, sem AppLayout. Promovido de /preview/dashboard-win11. // Standalone, sem auth, sem AppLayout. Promovido de /preview/dashboard-win11.
// Param `:secao?` opcional reflete a seção aberta na URL (agenda,
// pacientes, conversas, etc.) — permite deep-link, back/forward,
// refresh preservando estado.
{ {
path: 'preview/melissa', path: 'preview/melissa/:secao?',
name: 'PreviewMelissa', name: 'PreviewMelissa',
component: () => import('@/layout/melissa/MelissaLayout.vue') component: () => import('@/layout/melissa/MelissaLayout.vue')
}, },