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:
Leonardo
2026-05-06 09:38:18 -03:00
parent 97b0ec1ec5
commit ee084c2918
6 changed files with 862 additions and 28 deletions
@@ -34,6 +34,7 @@ import { useRoute, useRouter } from 'vue-router';
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
import PatientActionMenu from '@/components/patients/PatientActionMenu.vue';
import { usePatientLifecycle } from '@/composables/usePatientLifecycle';
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
@@ -104,6 +105,36 @@ const conversationDrawerStore = useConversationDrawerStore();
const route = useRoute();
const toast = useToast();
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 ───────────────────────────────────────────
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 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 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" />
</div>
</template>
@@ -1146,6 +1178,7 @@ function isRecent(row) {
<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-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" />
</div>
</div>
@@ -95,21 +95,9 @@ export async function softDeletePatient(id, { tenantId } = {}) {
if (error) throw error;
}
/**
* Restaura um paciente arquivado — volta status pra 'Ativo'.
* Inverso explícito do softDeletePatient. Uso: botão "Restaurar"
* 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;
}
// Pra restaurar um paciente arquivado, use `reactivatePatient` do
// composable `usePatientLifecycle` — fonte única de verdade pra toda
// transição de status (Inativo/Arquivado/Alta/Encaminhado → Ativo).
// ─────────────────────────────────────────────────────────────────────────
// Groups