Files
agenciapsilmno/src/views/pages/public/PatientsExternalLinkPage.vue
T

180 lines
5.0 KiB
Vue

<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/public/PatientsExternalLinkPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<div class="p-4">
<!-- HEADER -->
<div class="flex flex-column md:flex-row md:align-items-center md:justify-content-between gap-3">
<div>
<div class="text-900 text-2xl font-semibold">Cadastro Externo</div>
<div class="text-600 mt-1">
Gere um link para o paciente preencher o pré-cadastro.
</div>
</div>
<div class="flex gap-2">
<Button
label="Gerar novo link"
icon="pi pi-refresh"
severity="secondary"
outlined
:loading="rotating"
@click="rotateLink"
/>
</div>
</div>
<!-- CARD -->
<Card class="mt-4">
<template #title>Seu link</template>
<template #subtitle>Envie este link ao paciente.</template>
<template #content>
<div class="flex flex-column gap-3">
<div class="p-inputgroup">
<InputText
readonly
:value="publicUrl"
placeholder="Gerando seu link…"
/>
<Button
icon="pi pi-copy"
severity="secondary"
outlined
:disabled="!publicUrl"
@click="copyLink"
v-tooltip.bottom="'Copiar'"
/>
<Button
icon="pi pi-external-link"
severity="secondary"
outlined
:disabled="!publicUrl"
@click="openLink"
v-tooltip.bottom="'Abrir'"
/>
</div>
<Message v-if="!inviteToken" severity="info" :closable="false">
Gerando seu link...
</Message>
</div>
</template>
</Card>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import Message from 'primevue/message'
import { useToast } from 'primevue/usetoast'
import { supabase } from '@/lib/supabase/client' // ajuste se seu caminho for diferente
const toast = useToast()
const inviteToken = ref('')
const rotating = ref(false)
const origin = computed(() => window.location.origin)
const publicUrl = computed(() => {
if (!inviteToken.value) return ''
return `${origin.value}/cadastro/paciente?t=${inviteToken.value}`
})
function newToken () {
// browsers modernos
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID()
// fallback simples
return 'tok_' + Math.random().toString(36).slice(2) + Date.now().toString(36)
}
async function requireUserId () {
const { data, error } = await supabase.auth.getUser()
if (error) throw error
const uid = data?.user?.id
if (!uid) throw new Error('Usuário não autenticado')
return uid
}
async function loadOrCreateInvite () {
const uid = await requireUserId()
const { data, error } = await supabase
.from('patient_invites')
.select('token, active')
.eq('owner_id', uid)
.eq('active', true)
.order('created_at', { ascending: false })
.limit(1)
if (error) throw error
const token = data?.[0]?.token
if (token) {
inviteToken.value = token
return
}
const t = newToken()
const { error: insErr } = await supabase
.from('patient_invites')
.insert({ owner_id: uid, token: t, active: true })
if (insErr) throw insErr
inviteToken.value = t
}
async function rotateLink () {
rotating.value = true
try {
const t = newToken()
const { error } = await supabase.rpc('rotate_patient_invite_token', { p_new_token: t })
if (error) throw error
inviteToken.value = t
toast.add({ severity: 'success', summary: 'Pronto', detail: 'Novo link gerado.', life: 2000 })
} catch (err) {
toast.add({ severity: 'error', summary: 'Erro', detail: err?.message || 'Falha ao gerar novo link.', life: 3500 })
} finally {
rotating.value = false
}
}
async function copyLink () {
try {
if (!publicUrl.value) return
await navigator.clipboard.writeText(publicUrl.value)
toast.add({ severity: 'success', summary: 'Copiado', detail: 'Link copiado.', life: 1500 })
} catch {
toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Não foi possível copiar automaticamente.', life: 2500 })
}
}
function openLink () {
if (!publicUrl.value) return
window.open(publicUrl.value, '_blank', 'noopener')
}
onMounted(async () => {
try {
await loadOrCreateInvite()
} catch (err) {
toast.add({ severity: 'error', summary: 'Erro', detail: err?.message || 'Falha ao carregar link.', life: 3500 })
}
})
</script>