Pacientes restore unificado + MelissaLinkExterno nativa
Trabalho de continuidade pós-blueprint: A) Botao "Restaurar" visivel direto na linha da PatientsListPage (layout Rail) quando paciente.status === 'Arquivado' — atalho pra usuarios que filtram por arquivados sem precisar abrir o menu de "..." (que ja tinha "Reativar" via PatientActionMenu). Icone pi-undo + label "Restaurar" + tooltip + click chama reactivatePatient do usePatientLifecycle. Aplicado tanto no DataTable desktop quanto nos cards mobile. B) Consolidacao: removido restorePatient do patientsRepository (era duplicado com reactivatePatient do usePatientLifecycle). MelissaPacientes agora consome reactivatePatient direto, fonte unica de verdade pra toda transicao de status pra 'Ativo'. C) MelissaLinkExterno (nova pagina nativa Melissa). Substitui o embed via MelissaEmbed que duplicava 3 headers (layout + embed + hero sticky da pagina interna). Lógica preservada (RPC issue_patient_invite + rotate_patient_invite_token_v2 + copy/openLink), so o chrome muda pra casar com o blueprint Melissa: 1 header com status pill (Link ativo/Gerando) + botao "Gerar novo link" + Recarregar + Voltar; subheader explicativo; body 2-col (esquerda card "Seu link publico" com InputGroup + 2 CTAs grandes + card "Mensagem pronta"; direita cards "Como funciona" + "Boas praticas"); mobile vira 1-col. PatientsExternalLinkPage continua intacta — segue funcionando no layout Rail. Wire-up no MelissaLayout: import + render block + 'link-externo' literal em NON_CONFIG_SLUGS; removido de MELISSA_EMBED_KEYS. Entry removido do EMBED_MAP no MelissaEmbed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ import { useRoute, useRouter } from 'vue-router';
|
|||||||
|
|
||||||
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
|
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
|
||||||
import PatientActionMenu from '@/components/patients/PatientActionMenu.vue';
|
import PatientActionMenu from '@/components/patients/PatientActionMenu.vue';
|
||||||
|
import { usePatientLifecycle } from '@/composables/usePatientLifecycle';
|
||||||
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
||||||
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
|
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
|
||||||
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
|
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
|
||||||
@@ -104,6 +105,36 @@ const conversationDrawerStore = useConversationDrawerStore();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
const { reactivatePatient } = usePatientLifecycle();
|
||||||
|
|
||||||
|
// Restaurar paciente arquivado — atalho visível direto na linha (em vez
|
||||||
|
// de exigir abrir o menu de "..."). Compartilha reactivatePatient com
|
||||||
|
// PatientActionMenu pra manter fonte única de verdade.
|
||||||
|
function restaurarPaciente(p) {
|
||||||
|
if (!p?.id) return;
|
||||||
|
confirm.require({
|
||||||
|
message: `Restaurar "${p.nome_completo}" pra lista de pacientes ativos?`,
|
||||||
|
header: 'Restaurar paciente',
|
||||||
|
icon: 'pi pi-undo',
|
||||||
|
acceptLabel: 'Restaurar',
|
||||||
|
rejectLabel: 'Cancelar',
|
||||||
|
accept: async () => {
|
||||||
|
try {
|
||||||
|
const result = await reactivatePatient(p.id);
|
||||||
|
if (!result?.ok) throw result?.error || new Error('Falha ao restaurar.');
|
||||||
|
toast.add({ severity: 'success', summary: 'Restaurado', detail: p.nome_completo, life: 2200 });
|
||||||
|
await fetchAll();
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Falha ao restaurar',
|
||||||
|
detail: e?.message || 'Tente novamente.',
|
||||||
|
life: 4000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Hero sticky ───────────────────────────────────────────
|
// ── Hero sticky ───────────────────────────────────────────
|
||||||
const headerEl = ref(null);
|
const headerEl = ref(null);
|
||||||
@@ -1079,6 +1110,7 @@ function isRecent(row) {
|
|||||||
<Button label="Prontuário" icon="pi pi-file" size="small" @click="openProntuario(data)" />
|
<Button label="Prontuário" icon="pi pi-file" size="small" @click="openProntuario(data)" />
|
||||||
<Button icon="pi pi-whatsapp" severity="success" outlined size="small" v-tooltip.top="'Conversar no WhatsApp'" @click="goConversation(data)" />
|
<Button icon="pi pi-whatsapp" severity="success" outlined size="small" v-tooltip.top="'Conversar no WhatsApp'" @click="goConversation(data)" />
|
||||||
<Button icon="pi pi-pencil" severity="secondary" outlined size="small" v-tooltip.top="'Editar'" @click="goEdit(data)" />
|
<Button icon="pi pi-pencil" severity="secondary" outlined size="small" v-tooltip.top="'Editar'" @click="goEdit(data)" />
|
||||||
|
<Button v-if="data.status === 'Arquivado'" icon="pi pi-undo" label="Restaurar" size="small" severity="primary" outlined v-tooltip.top="'Voltar paciente pra ativos'" @click="restaurarPaciente(data)" />
|
||||||
<PatientActionMenu :patient="data" :hasHistory="historySet.has(data.id)" @updated="fetchAll" />
|
<PatientActionMenu :patient="data" :hasHistory="historySet.has(data.id)" @updated="fetchAll" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1146,6 +1178,7 @@ function isRecent(row) {
|
|||||||
<Button label="Prontuário" icon="pi pi-file" size="small" @click="openProntuario(pat)" />
|
<Button label="Prontuário" icon="pi pi-file" size="small" @click="openProntuario(pat)" />
|
||||||
<Button icon="pi pi-whatsapp" severity="success" outlined size="small" v-tooltip.top="'Conversar'" @click="goConversation(pat)" />
|
<Button icon="pi pi-whatsapp" severity="success" outlined size="small" v-tooltip.top="'Conversar'" @click="goConversation(pat)" />
|
||||||
<Button icon="pi pi-pencil" severity="secondary" outlined size="small" @click="goEdit(pat)" />
|
<Button icon="pi pi-pencil" severity="secondary" outlined size="small" @click="goEdit(pat)" />
|
||||||
|
<Button v-if="pat.status === 'Arquivado'" icon="pi pi-undo" label="Restaurar" size="small" severity="primary" outlined v-tooltip.top="'Voltar paciente pra ativos'" @click="restaurarPaciente(pat)" />
|
||||||
<PatientActionMenu :patient="pat" :hasHistory="historySet.has(pat.id)" @updated="fetchAll" />
|
<PatientActionMenu :patient="pat" :hasHistory="historySet.has(pat.id)" @updated="fetchAll" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -95,21 +95,9 @@ export async function softDeletePatient(id, { tenantId } = {}) {
|
|||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Pra restaurar um paciente arquivado, use `reactivatePatient` do
|
||||||
* Restaura um paciente arquivado — volta status pra 'Ativo'.
|
// composable `usePatientLifecycle` — fonte única de verdade pra toda
|
||||||
* Inverso explícito do softDeletePatient. Uso: botão "Restaurar"
|
// transição de status (Inativo/Arquivado/Alta/Encaminhado → Ativo).
|
||||||
* que aparece nas ações quando p.status === 'Arquivado'.
|
|
||||||
*/
|
|
||||||
export async function restorePatient(id, { tenantId } = {}) {
|
|
||||||
if (!id) throw new Error('id obrigatório');
|
|
||||||
assertTenantId(tenantId);
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('patients')
|
|
||||||
.update({ status: 'Ativo' })
|
|
||||||
.eq('id', id)
|
|
||||||
.eq('tenant_id', tenantId);
|
|
||||||
if (error) throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// Groups
|
// Groups
|
||||||
|
|||||||
@@ -74,12 +74,9 @@ const EMBED_MAP = {
|
|||||||
icon: 'pi pi-bell',
|
icon: 'pi pi-bell',
|
||||||
comp: defineAsyncComponent(() => import('@/views/pages/therapist/NotificationsHistoryPage.vue'))
|
comp: defineAsyncComponent(() => import('@/views/pages/therapist/NotificationsHistoryPage.vue'))
|
||||||
},
|
},
|
||||||
'link-externo': {
|
// 'link-externo' foi promovido pra página nativa MelissaLinkExterno
|
||||||
label: 'Link externo de cadastro',
|
// (eliminado o triplo header). Wire-up agora no MelissaLayout.vue,
|
||||||
desc: 'Link público pra pacientes preencherem o cadastro online.',
|
// não passa mais por aqui.
|
||||||
icon: 'pi pi-share-alt',
|
|
||||||
comp: defineAsyncComponent(() => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue'))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const info = computed(() => EMBED_MAP[props.secaoRota] || null);
|
const info = computed(() => EMBED_MAP[props.secaoRota] || null);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import MelissaConfiguracoes from './MelissaConfiguracoes.vue';
|
|||||||
import MelissaEmbed from './MelissaEmbed.vue';
|
import MelissaEmbed from './MelissaEmbed.vue';
|
||||||
import MelissaCadastrosRecebidos from './MelissaCadastrosRecebidos.vue';
|
import MelissaCadastrosRecebidos from './MelissaCadastrosRecebidos.vue';
|
||||||
import MelissaAgendamentosRecebidos from './MelissaAgendamentosRecebidos.vue';
|
import MelissaAgendamentosRecebidos from './MelissaAgendamentosRecebidos.vue';
|
||||||
|
import MelissaLinkExterno from './MelissaLinkExterno.vue';
|
||||||
import MelissaMedicos from './MelissaMedicos.vue';
|
import MelissaMedicos from './MelissaMedicos.vue';
|
||||||
import MelissaEventoPanel from './MelissaEventoPanel.vue';
|
import MelissaEventoPanel from './MelissaEventoPanel.vue';
|
||||||
import { TOQUES, playToque } from './melissaToques';
|
import { TOQUES, playToque } from './melissaToques';
|
||||||
@@ -173,8 +174,10 @@ const SECOES = {
|
|||||||
'link-externo': { label: 'Link externo de cadastro', icon: 'pi pi-share-alt', descricao: 'Link público pra pacientes se cadastrarem.' }
|
'link-externo': { label: 'Link externo de cadastro', icon: 'pi pi-share-alt', descricao: 'Link público pra pacientes se cadastrarem.' }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set de keys que renderizam via MelissaEmbed (Onda 1 — pages 1-coluna)
|
// Set de keys que renderizam via MelissaEmbed (Onda 1 — pages 1-coluna).
|
||||||
const MELISSA_EMBED_KEYS = ['financeiro', 'financeiro-lancamentos', 'documentos', 'documentos-templates', 'online-scheduling', 'relatorios', 'notificacoes', 'link-externo'];
|
// 'link-externo' foi promovido pra página nativa (MelissaLinkExterno) pra
|
||||||
|
// remover o triplo-header que aparecia no embed.
|
||||||
|
const MELISSA_EMBED_KEYS = ['financeiro', 'financeiro-lancamentos', 'documentos', 'documentos-templates', 'online-scheduling', 'relatorios', 'notificacoes'];
|
||||||
|
|
||||||
// Slugs reservados pra páginas dedicadas (não-config) — agenda, pacientes,
|
// Slugs reservados pra páginas dedicadas (não-config) — agenda, pacientes,
|
||||||
// conversas etc. + as MELISSA_EMBED_KEYS. Usado no isConfigRoute pra
|
// conversas etc. + as MELISSA_EMBED_KEYS. Usado no isConfigRoute pra
|
||||||
@@ -182,6 +185,7 @@ const MELISSA_EMBED_KEYS = ['financeiro', 'financeiro-lancamentos', 'documentos'
|
|||||||
const MELISSA_NON_CONFIG_SLUGS = new Set([
|
const MELISSA_NON_CONFIG_SLUGS = new Set([
|
||||||
'agenda', 'pacientes', 'compromissos', 'recorrencias', 'conversas',
|
'agenda', 'pacientes', 'compromissos', 'recorrencias', 'conversas',
|
||||||
'tags', 'grupos', 'cadastros-recebidos', 'medicos', 'agendamentos-recebidos',
|
'tags', 'grupos', 'cadastros-recebidos', 'medicos', 'agendamentos-recebidos',
|
||||||
|
'link-externo',
|
||||||
...MELISSA_EMBED_KEYS
|
...MELISSA_EMBED_KEYS
|
||||||
]);
|
]);
|
||||||
// Aliases "bonitos" + INLINE_KEYS reconhecidos pelo MelissaConfiguracoes.
|
// Aliases "bonitos" + INLINE_KEYS reconhecidos pelo MelissaConfiguracoes.
|
||||||
@@ -2161,6 +2165,11 @@ function onKeydown(e) {
|
|||||||
@close="fecharSecao"
|
@close="fecharSecao"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<MelissaLinkExterno
|
||||||
|
v-if="layoutReady && secaoAberta === 'link-externo'"
|
||||||
|
@close="fecharSecao"
|
||||||
|
/>
|
||||||
|
|
||||||
<MelissaConfiguracoes
|
<MelissaConfiguracoes
|
||||||
v-if="layoutReady && isMelissaConfigRoute(secaoAberta)"
|
v-if="layoutReady && isMelissaConfigRoute(secaoAberta)"
|
||||||
:secao-rota="secaoAberta"
|
:secao-rota="secaoAberta"
|
||||||
|
|||||||
@@ -0,0 +1,805 @@
|
|||||||
|
<script setup>
|
||||||
|
/*
|
||||||
|
* MelissaLinkExterno — Página nativa Melissa pra "Link externo de cadastro"
|
||||||
|
* (substitui o embed via MelissaEmbed que duplicava headers).
|
||||||
|
*
|
||||||
|
* Layout 2-col:
|
||||||
|
* - COL 1 — Esquerda (flex 1): card "Seu link público" + card
|
||||||
|
* "Mensagem pronta"
|
||||||
|
* - COL 2 — Direita (~280px): card "Como funciona" + card "Boas práticas"
|
||||||
|
*
|
||||||
|
* Lógica idêntica à PatientsExternalLinkPage (RPC issue_patient_invite +
|
||||||
|
* rotate_patient_invite_token_v2 + copy/openLink). Só o chrome muda pra
|
||||||
|
* casar com o blueprint Melissa (1 header só, sem hero sticky redundante).
|
||||||
|
*/
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { supabase } from '@/lib/supabase/client';
|
||||||
|
// Button/InputText/InputGroup/InputGroupAddon/Message: auto via PrimeVueResolver
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// ── Estado ─────────────────────────────────────────────────
|
||||||
|
const inviteToken = ref('');
|
||||||
|
const rotating = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// ── Conteúdo estático ─────────────────────────────────────
|
||||||
|
const howItWorks = [
|
||||||
|
{ n: 1, title: 'Você envia o link', desc: 'Por WhatsApp, e-mail ou mensagem direta.' },
|
||||||
|
{ n: 2, title: 'O paciente preenche', desc: 'Campos opcionais podem ficar em branco. Menos fricção, mais adesão.' },
|
||||||
|
{ n: 3, title: 'Você recebe e converte', desc: 'O cadastro aparece em "Cadastros recebidos". Revise e converta em paciente quando quiser.' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const goodPractices = [
|
||||||
|
'Gere um novo link se suspeitar que ele foi repassado indevidamente.',
|
||||||
|
'Informe o paciente que campos opcionais podem ficar em branco.',
|
||||||
|
'Evite divulgar em público; é um link para compartilhamento individual.'
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── URL base ──────────────────────────────────────────────
|
||||||
|
const origin = computed(() => {
|
||||||
|
return typeof window !== 'undefined' ? window.location.origin : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicUrl = computed(() => {
|
||||||
|
if (!inviteToken.value) return '';
|
||||||
|
return `${origin.value}/cadastro/paciente?t=${encodeURIComponent(inviteToken.value)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── RPC: issue + rotate ───────────────────────────────────
|
||||||
|
// Tokens são gerados no SERVIDOR via gen_random_uuid() — cliente nunca
|
||||||
|
// gera (segurança: elimina fallback Math.random).
|
||||||
|
async function loadOrCreateInvite() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.rpc('issue_patient_invite');
|
||||||
|
if (error) throw error;
|
||||||
|
inviteToken.value = data;
|
||||||
|
} catch (err) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Erro', detail: err?.message || 'Falha ao carregar link.', life: 3500 });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rotateLink() {
|
||||||
|
rotating.value = true;
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.rpc('rotate_patient_invite_token_v2');
|
||||||
|
if (error) throw error;
|
||||||
|
inviteToken.value = data;
|
||||||
|
toast.add({ severity: 'success', summary: 'Pronto', detail: 'Novo link gerado. O anterior foi revogado.', life: 2500 });
|
||||||
|
} catch (err) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Erro', detail: err?.message || 'Falha ao gerar novo link.', life: 3500 });
|
||||||
|
} finally {
|
||||||
|
rotating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Copy/open helpers ─────────────────────────────────────
|
||||||
|
async function copyLink() {
|
||||||
|
try {
|
||||||
|
if (!publicUrl.value) return;
|
||||||
|
await navigator.clipboard.writeText(publicUrl.value);
|
||||||
|
toast.add({ severity: 'success', summary: 'Copiado', detail: 'Link copiado para a área de transferência.', life: 1500 });
|
||||||
|
} catch {
|
||||||
|
window.prompt('Copie o link:', publicUrl.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLink() {
|
||||||
|
if (!publicUrl.value) return;
|
||||||
|
window.open(publicUrl.value, '_blank', 'noopener');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyInviteMessage() {
|
||||||
|
try {
|
||||||
|
if (!publicUrl.value) return;
|
||||||
|
const msg = `Olá! Segue o link para seu pré-cadastro. Preencha com calma — campos opcionais podem ficar em branco:\n${publicUrl.value}`;
|
||||||
|
await navigator.clipboard.writeText(msg);
|
||||||
|
toast.add({ severity: 'success', summary: 'Copiado', detail: 'Mensagem copiada para a área de transferência.', life: 1500 });
|
||||||
|
} catch {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Não foi possível copiar automaticamente.', life: 2500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ──────────────────────────────────────────────
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadOrCreateInvite();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="ml-page">
|
||||||
|
<!-- Header único (sem hero sticky — só o chrome Melissa) -->
|
||||||
|
<header class="ml-page__head">
|
||||||
|
<div class="ml-page__title">
|
||||||
|
<i class="pi pi-share-alt ml-page__title-icon" />
|
||||||
|
<span>Link externo de cadastro</span>
|
||||||
|
<span
|
||||||
|
class="ml-page__status"
|
||||||
|
:class="inviteToken ? 'is-active' : 'is-loading'"
|
||||||
|
>
|
||||||
|
<span class="ml-page__status-dot" />
|
||||||
|
{{ inviteToken ? 'Link ativo' : 'Gerando…' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-page__actions">
|
||||||
|
<button
|
||||||
|
class="ml-act-btn ml-act-btn--primary"
|
||||||
|
v-tooltip.bottom="'Gera um novo token e revoga o anterior'"
|
||||||
|
:disabled="rotating || !inviteToken"
|
||||||
|
@click="rotateLink"
|
||||||
|
>
|
||||||
|
<i :class="rotating ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
|
||||||
|
<span>Gerar novo link</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ml-head-btn"
|
||||||
|
v-tooltip.bottom="'Recarregar'"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="loadOrCreateInvite"
|
||||||
|
>
|
||||||
|
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
|
||||||
|
</button>
|
||||||
|
<button class="ml-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
|
||||||
|
<i class="pi pi-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Subheader explicativo -->
|
||||||
|
<div class="ml-subheader">
|
||||||
|
<i class="pi pi-info-circle ml-subheader__icon" />
|
||||||
|
<span class="ml-subheader__text">
|
||||||
|
Link público pra pacientes preencherem o pré-cadastro online.
|
||||||
|
<strong>Compartilhe</strong> por WhatsApp/email; o cadastro chega em
|
||||||
|
<strong>Cadastros Recebidos</strong> pra você converter em paciente.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body 2-col -->
|
||||||
|
<div class="ml-body">
|
||||||
|
<!-- ═══ COL 1: Link + Mensagem ═══ -->
|
||||||
|
<div class="ml-main">
|
||||||
|
<!-- Card: Seu link público -->
|
||||||
|
<div class="ml-card">
|
||||||
|
<div class="ml-card__head">
|
||||||
|
<div class="ml-card__title">
|
||||||
|
<span class="ml-card__title-text">Seu link público</span>
|
||||||
|
<span class="ml-card__sub">Envie ao paciente por WhatsApp, e-mail ou mensagem direta</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="ml-card__status"
|
||||||
|
:class="inviteToken ? 'is-active' : 'is-loading'"
|
||||||
|
>
|
||||||
|
<span class="ml-card__status-dot" />
|
||||||
|
{{ inviteToken ? 'Ativo' : 'Gerando…' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ml-card__body">
|
||||||
|
<!-- Skeleton enquanto carrega -->
|
||||||
|
<div v-if="!inviteToken" class="ml-skel">
|
||||||
|
<div class="ml-skel__bar" />
|
||||||
|
<div class="ml-skel__bar" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col gap-4">
|
||||||
|
<!-- InputGroup do link -->
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon><i class="pi pi-link" /></InputGroupAddon>
|
||||||
|
<InputText
|
||||||
|
readonly
|
||||||
|
:value="publicUrl"
|
||||||
|
class="font-mono text-[0.78rem]"
|
||||||
|
/>
|
||||||
|
<Button icon="pi pi-copy" severity="secondary" v-tooltip.top="'Copiar'" @click="copyLink" />
|
||||||
|
<Button icon="pi pi-external-link" severity="secondary" v-tooltip.top="'Abrir'" @click="openLink" />
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<div class="ml-token-line">
|
||||||
|
Token: <span class="font-mono select-all opacity-60">{{ inviteToken }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2 CTAs grandes -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
|
||||||
|
<button class="ml-cta" @click="copyLink">
|
||||||
|
<div class="ml-cta__icon ml-cta__icon--primary">
|
||||||
|
<i class="pi pi-copy" />
|
||||||
|
</div>
|
||||||
|
<div class="ml-cta__text">
|
||||||
|
<div class="ml-cta__title">Copiar link</div>
|
||||||
|
<div class="ml-cta__sub">Cole no WhatsApp ou e-mail</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="ml-cta" @click="copyInviteMessage">
|
||||||
|
<div class="ml-cta__icon ml-cta__icon--success">
|
||||||
|
<i class="pi pi-comment" />
|
||||||
|
</div>
|
||||||
|
<div class="ml-cta__text">
|
||||||
|
<div class="ml-cta__title">Copiar mensagem pronta</div>
|
||||||
|
<div class="ml-cta__sub">Texto formatado com o link incluso</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aviso -->
|
||||||
|
<Message severity="warn" :closable="false">
|
||||||
|
<b>Dica:</b> ao gerar um novo link, o anterior é revogado.
|
||||||
|
Use isso quando quiser invalidar um link já compartilhado.
|
||||||
|
</Message>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card: Mensagem pronta -->
|
||||||
|
<div class="ml-card">
|
||||||
|
<div class="ml-card__head ml-card__head--simple">
|
||||||
|
<div class="ml-card__title">
|
||||||
|
<span class="ml-card__title-text">
|
||||||
|
<i class="pi pi-comment ml-card__title-icon" />
|
||||||
|
Mensagem pronta para envio
|
||||||
|
</span>
|
||||||
|
<span class="ml-card__sub">Copie e cole ao enviar o link ao paciente</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-card__body">
|
||||||
|
<div class="ml-msg-preview">
|
||||||
|
Olá! Segue o link para seu pré-cadastro. Preencha com calma —
|
||||||
|
campos opcionais podem ficar em branco:
|
||||||
|
<span class="ml-msg-preview__link">{{ publicUrl || '…aguardando link…' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-copy"
|
||||||
|
label="Copiar mensagem"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
class="rounded-full"
|
||||||
|
:disabled="!publicUrl"
|
||||||
|
@click="copyInviteMessage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ COL 2: Instruções ═══ -->
|
||||||
|
<aside class="ml-side">
|
||||||
|
<!-- Como funciona -->
|
||||||
|
<div class="ml-card">
|
||||||
|
<div class="ml-card__head ml-card__head--simple">
|
||||||
|
<div class="ml-info-head">
|
||||||
|
<span class="ml-info-head__icon ml-info-head__icon--primary">
|
||||||
|
<i class="pi pi-list-check" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="ml-info-head__title">Como funciona</div>
|
||||||
|
<div class="ml-info-head__sub">Simples e sem fricção pro paciente</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ol class="ml-steps">
|
||||||
|
<li v-for="step in howItWorks" :key="step.n" class="ml-steps__item">
|
||||||
|
<div class="ml-steps__num">{{ step.n }}</div>
|
||||||
|
<div class="ml-steps__text">
|
||||||
|
<div class="ml-steps__title">{{ step.title }}</div>
|
||||||
|
<div class="ml-steps__desc">{{ step.desc }}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Boas práticas -->
|
||||||
|
<div class="ml-card">
|
||||||
|
<div class="ml-card__head ml-card__head--simple">
|
||||||
|
<div class="ml-info-head">
|
||||||
|
<span class="ml-info-head__icon ml-info-head__icon--success">
|
||||||
|
<i class="pi pi-shield" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="ml-info-head__title">Boas práticas</div>
|
||||||
|
<div class="ml-info-head__sub">Segurança e privacidade</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="ml-tips">
|
||||||
|
<li v-for="tip in goodPractices" :key="tip" class="ml-tips__item">
|
||||||
|
<i class="pi pi-check ml-tips__check" />
|
||||||
|
<span>{{ tip }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ─── Page chrome (espelha demais Melissa Pages) ─── */
|
||||||
|
.ml-page {
|
||||||
|
position: absolute;
|
||||||
|
inset: 6px 6px calc(var(--m-dock-h, 76px) + 6px) 6px;
|
||||||
|
z-index: 40;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--m-bg-medium);
|
||||||
|
backdrop-filter: blur(32px) saturate(160%);
|
||||||
|
-webkit-backdrop-filter: blur(32px) saturate(160%);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||||
|
color: var(--m-text);
|
||||||
|
animation: ml-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||||||
|
}
|
||||||
|
@keyframes ml-page-enter {
|
||||||
|
from { opacity: 0; transform: scale(0.985); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ml-page__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-bottom: 1px solid var(--m-border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.ml-page__title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.ml-page__title-icon {
|
||||||
|
color: var(--p-primary-color);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
.ml-page__title > span:not(.ml-page__status) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status pill no header (Link ativo / Gerando) */
|
||||||
|
.ml-page__status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
.ml-page__status.is-active {
|
||||||
|
color: rgb(22, 163, 74);
|
||||||
|
background: rgba(22, 163, 74, 0.10);
|
||||||
|
border-color: rgba(22, 163, 74, 0.35);
|
||||||
|
}
|
||||||
|
.ml-page__status.is-loading {
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
background: var(--m-bg-soft);
|
||||||
|
border-color: var(--m-border);
|
||||||
|
}
|
||||||
|
.ml-page__status-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
.ml-page__status.is-active .ml-page__status-dot {
|
||||||
|
animation: ml-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes ml-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.45; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ml-page__actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
.ml-close, .ml-head-btn {
|
||||||
|
width: 32px; height: 32px;
|
||||||
|
display: grid; place-items: center;
|
||||||
|
background: var(--m-bg-soft);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
color: var(--m-text);
|
||||||
|
border-radius: 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background-color 140ms ease;
|
||||||
|
}
|
||||||
|
.ml-close:hover, .ml-head-btn:hover { background: var(--m-bg-soft-hover); }
|
||||||
|
.ml-head-btn > i { font-size: 0.85rem; }
|
||||||
|
.ml-head-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.ml-act-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 140ms ease, transform 140ms ease;
|
||||||
|
background: var(--m-bg-soft);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
color: var(--m-text);
|
||||||
|
}
|
||||||
|
.ml-act-btn--primary {
|
||||||
|
background: var(--m-accent);
|
||||||
|
border-color: var(--m-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.ml-act-btn--primary:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--m-accent) 88%, white);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.ml-act-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
||||||
|
.ml-act-btn > i { font-size: 0.78rem; }
|
||||||
|
|
||||||
|
/* Subheader (blueprint §9) */
|
||||||
|
.ml-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;
|
||||||
|
}
|
||||||
|
.ml-subheader__icon {
|
||||||
|
color: var(--p-primary-color);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.ml-subheader__text { flex: 1; min-width: 0; }
|
||||||
|
.ml-subheader__text strong { color: var(--m-text); font-weight: 600; }
|
||||||
|
|
||||||
|
/* Body 2-col */
|
||||||
|
.ml-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Col 1: Main (link + mensagem) ─── */
|
||||||
|
.ml-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Col 2: Aside (instruções) ─── */
|
||||||
|
.ml-side {
|
||||||
|
width: 280px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card-base */
|
||||||
|
.ml-card {
|
||||||
|
background: var(--m-bg-soft);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ml-card__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px 12px;
|
||||||
|
border-bottom: 1px solid var(--m-border);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.ml-card__head--simple { border-bottom: 1px solid var(--m-border); }
|
||||||
|
.ml-card__title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.ml-card__title-text {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--m-text);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.ml-card__title-icon {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
}
|
||||||
|
.ml-card__sub {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
}
|
||||||
|
.ml-card__body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ml-card__status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ml-card__status.is-active {
|
||||||
|
color: rgb(22, 163, 74);
|
||||||
|
background: rgba(22, 163, 74, 0.10);
|
||||||
|
border-color: rgba(22, 163, 74, 0.35);
|
||||||
|
}
|
||||||
|
.ml-card__status.is-loading {
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
background: var(--m-bg-medium);
|
||||||
|
border-color: var(--m-border);
|
||||||
|
}
|
||||||
|
.ml-card__status-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
.ml-card__status.is-active .ml-card__status-dot {
|
||||||
|
animation: ml-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skeleton */
|
||||||
|
.ml-skel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.ml-skel__bar {
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
var(--m-bg-medium) 0%,
|
||||||
|
var(--m-bg-soft-hover) 50%,
|
||||||
|
var(--m-bg-medium) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: ml-skel-shimmer 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes ml-skel-shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* InputGroup tweaks */
|
||||||
|
.ml-card :deep(.p-inputgroup) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Token line */
|
||||||
|
.ml-token-line {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTAs grandes */
|
||||||
|
.ml-cta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: var(--m-bg-medium);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--m-text);
|
||||||
|
transition: background-color 140ms ease, border-color 140ms ease, transform 140ms ease, box-shadow 140ms ease;
|
||||||
|
}
|
||||||
|
.ml-cta:hover {
|
||||||
|
background: var(--m-bg-soft-hover);
|
||||||
|
border-color: var(--m-border-strong);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
.ml-cta__icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 9px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.ml-cta__icon--primary {
|
||||||
|
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
|
||||||
|
color: var(--p-primary-color);
|
||||||
|
}
|
||||||
|
.ml-cta__icon--success {
|
||||||
|
background: rgba(22, 163, 74, 0.15);
|
||||||
|
color: rgb(22, 163, 74);
|
||||||
|
}
|
||||||
|
.ml-cta__text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.ml-cta__title {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--m-text);
|
||||||
|
}
|
||||||
|
.ml-cta__sub {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mensagem preview */
|
||||||
|
.ml-msg-preview {
|
||||||
|
background: var(--m-bg-medium);
|
||||||
|
border: 1px solid var(--m-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--m-text);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.ml-msg-preview__link {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Side: Instructions cards */
|
||||||
|
.ml-info-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.ml-info-head__icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
.ml-info-head__icon--primary {
|
||||||
|
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
|
||||||
|
color: var(--p-primary-color);
|
||||||
|
}
|
||||||
|
.ml-info-head__icon--success {
|
||||||
|
background: rgba(22, 163, 74, 0.15);
|
||||||
|
color: rgb(22, 163, 74);
|
||||||
|
}
|
||||||
|
.ml-info-head__title {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--m-text);
|
||||||
|
}
|
||||||
|
.ml-info-head__sub {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Steps (Como funciona) */
|
||||||
|
.ml-steps {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.ml-steps__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--m-border);
|
||||||
|
}
|
||||||
|
.ml-steps__item:last-child { border-bottom: none; }
|
||||||
|
.ml-steps__num {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
|
||||||
|
color: var(--p-primary-color);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.ml-steps__text { flex: 1; min-width: 0; }
|
||||||
|
.ml-steps__title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--m-text);
|
||||||
|
}
|
||||||
|
.ml-steps__desc {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
margin-top: 2px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tips (Boas práticas) */
|
||||||
|
.ml-tips {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.ml-tips__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid var(--m-border);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--m-text-muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.ml-tips__item:last-child { border-bottom: none; }
|
||||||
|
.ml-tips__check {
|
||||||
|
color: rgb(22, 163, 74);
|
||||||
|
margin-top: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile: 2-col vira 1-col, instruções vão pra baixo */
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.ml-body {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.ml-side {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ml-page__title > span:not(.ml-page__title-icon):not(.ml-page__status) {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
.ml-page__status { display: none; }
|
||||||
|
.ml-act-btn--primary span { display: none; }
|
||||||
|
.ml-act-btn--primary { width: 32px; padding: 0; justify-content: center; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -37,9 +37,9 @@ import {
|
|||||||
listGroupsByPatient,
|
listGroupsByPatient,
|
||||||
listTagsByPatient,
|
listTagsByPatient,
|
||||||
getSessionCounts,
|
getSessionCounts,
|
||||||
softDeletePatient,
|
softDeletePatient
|
||||||
restorePatient
|
|
||||||
} from '@/features/patients/services/patientsRepository';
|
} from '@/features/patients/services/patientsRepository';
|
||||||
|
import { usePatientLifecycle } from '@/composables/usePatientLifecycle';
|
||||||
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
|
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
|
||||||
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
|
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
|
||||||
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
||||||
@@ -52,6 +52,7 @@ const emit = defineEmits(['close', 'patient-created', 'goto-agenda', 'goto-grupo
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const tenantStore = useTenantStore();
|
const tenantStore = useTenantStore();
|
||||||
|
const { reactivatePatient } = usePatientLifecycle();
|
||||||
const conversationDrawerStore = useConversationDrawerStore();
|
const conversationDrawerStore = useConversationDrawerStore();
|
||||||
|
|
||||||
// ── Pacientes (todos os status) ───────────────────────────────
|
// ── Pacientes (todos os status) ───────────────────────────────
|
||||||
@@ -605,7 +606,8 @@ function isArquivado(p) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restaurar paciente arquivado — volta status pra 'Ativo' via
|
// Restaurar paciente arquivado — volta status pra 'Ativo' via
|
||||||
// restorePatient (inverso semântico do softDeletePatient).
|
// reactivatePatient (compartilhado com PatientActionMenu/PatientsListPage,
|
||||||
|
// fonte única em usePatientLifecycle).
|
||||||
function restaurarPaciente(p) {
|
function restaurarPaciente(p) {
|
||||||
if (!p?.id) return;
|
if (!p?.id) return;
|
||||||
confirm.require({
|
confirm.require({
|
||||||
@@ -616,8 +618,8 @@ function restaurarPaciente(p) {
|
|||||||
rejectLabel: 'Cancelar',
|
rejectLabel: 'Cancelar',
|
||||||
accept: async () => {
|
accept: async () => {
|
||||||
try {
|
try {
|
||||||
const tenantId = tenantStore.activeTenantId || tenantStore.tenantId;
|
const result = await reactivatePatient(p.id);
|
||||||
await restorePatient(p.id, { tenantId });
|
if (!result?.ok) throw result?.error || new Error('Falha ao restaurar.');
|
||||||
toast.add({ severity: 'success', summary: 'Restaurado', detail: p.nome, life: 2200 });
|
toast.add({ severity: 'success', summary: 'Restaurado', detail: p.nome, life: 2200 });
|
||||||
await refetchTudo();
|
await refetchTudo();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user