Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
+61 -51
View File
@@ -14,74 +14,84 @@
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<Transition name="fade-up" appear>
<div class="flex flex-col items-center justify-center gap-6" :class="containerClass">
<!-- Motivação -->
<div class="flex flex-col items-center gap-2 text-center px-6">
<span class="text-base font-semibold text-[var(--text-color,#1e293b)] leading-snug max-w-[320px]">
{{ motivation || '...' }}
</span>
<span class="text-sm text-[var(--text-color-secondary,#64748b)]">
{{ action }}
</span>
</div>
<!-- Progress bar -->
<div class="w-[220px] h-[3px] rounded-full bg-[var(--surface-border,#e2e8f0)] overflow-hidden">
<div class="progress-bar h-full rounded-full bg-[var(--primary-color,#6366f1)]" />
</div>
</div>
</Transition>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted } from 'vue';
defineProps({
action: { type: String, default: 'Carregando...' },
containerClass: { type: String, default: 'py-24' },
})
action: { type: String, default: 'Carregando...' },
containerClass: { type: String, default: 'py-24' }
});
const motivation = ref(null)
const motivation = ref(null);
onMounted(async () => {
try {
const res = await fetch('/loading-phrases.json')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = await res.json()
const list = json.motivations || []
motivation.value = list.length
? list[Math.floor(Math.random() * list.length)]
: 'Carregando...'
} catch (e) {
console.warn('[AppLoadingPhrases] fetch falhou:', e)
motivation.value = 'Carregando...'
}
})
try {
const res = await fetch('/loading-phrases.json');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
const list = json.motivations || [];
motivation.value = list.length ? list[Math.floor(Math.random() * list.length)] : 'Carregando...';
} catch (e) {
console.warn('[AppLoadingPhrases] fetch falhou:', e);
motivation.value = 'Carregando...';
}
});
</script>
<template>
<Transition name="fade-up" appear>
<div class="flex flex-col items-center justify-center gap-6" :class="containerClass">
<!-- Motivação -->
<div class="flex flex-col items-center gap-2 text-center px-6">
<span class="text-base font-semibold text-[var(--text-color,#1e293b)] leading-snug max-w-[320px]">
{{ motivation || '...' }}
</span>
<span class="text-sm text-[var(--text-color-secondary,#64748b)]">
{{ action }}
</span>
</div>
<!-- Progress bar -->
<div class="w-[220px] h-[3px] rounded-full bg-[var(--surface-border,#e2e8f0)] overflow-hidden">
<div class="progress-bar h-full rounded-full bg-[var(--primary-color,#6366f1)]" />
</div>
</div>
</Transition>
</template>
<style scoped>
/* Entrada do componente inteiro */
.fade-up-enter-active {
transition: opacity 0.45s ease, transform 0.45s ease;
transition:
opacity 0.45s ease,
transform 0.45s ease;
}
.fade-up-enter-from {
opacity: 0;
transform: translateY(16px);
opacity: 0;
transform: translateY(16px);
}
/* Progress bar — vai de 0% a 85% em ~2.5s, para não "completar" antes do loading acabar */
.progress-bar {
animation: progress-indeterminate 1.6s ease-in-out infinite;
transform-origin: left;
animation: progress-indeterminate 1.6s ease-in-out infinite;
transform-origin: left;
}
@keyframes progress-indeterminate {
0% { margin-left: 0%; width: 0%; }
30% { margin-left: 0%; width: 60%; }
70% { margin-left: 40%; width: 60%; }
100% { margin-left: 100%; width: 0%; }
0% {
margin-left: 0%;
width: 0%;
}
30% {
margin-left: 0%;
width: 60%;
}
70% {
margin-left: 40%;
width: 60%;
}
100% {
margin-left: 100%;
width: 0%;
}
}
</style>
</style>
+23 -23
View File
@@ -14,31 +14,31 @@
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<template>
<Transition name="loaded-phrase-in" appear>
<div v-if="phrase" class="loaded-phrase-block">
<div class="loaded-phrase-block__header">
<i class="pi pi-check-circle loaded-phrase-block__icon" />
<span class="loaded-phrase-block__title">Ambiente carregado!</span>
</div>
<p class="loaded-phrase-block__text">{{ phrase }}</p>
</div>
</Transition>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted } from 'vue';
const phrase = ref(null)
const phrase = ref(null);
onMounted(async () => {
try {
const res = await fetch('/loading-phrases.json')
const json = await res.json()
const list = json.motivations || []
phrase.value = list.length ? list[Math.floor(Math.random() * list.length)] : null
} catch {
phrase.value = null
}
})
try {
const res = await fetch('/loading-phrases.json');
const json = await res.json();
const list = json.motivations || [];
phrase.value = list.length ? list[Math.floor(Math.random() * list.length)] : null;
} catch {
phrase.value = null;
}
});
</script>
<template>
<Transition name="loaded-phrase-in" appear>
<div v-if="phrase" class="loaded-phrase-block">
<div class="loaded-phrase-block__header">
<i class="pi pi-check-circle loaded-phrase-block__icon" />
<span class="loaded-phrase-block__title">Ambiente carregado!</span>
</div>
<p class="loaded-phrase-block__text">{{ phrase }}</p>
</div>
</Transition>
</template>
+125 -151
View File
@@ -26,172 +26,146 @@
| created paciente criado ou atualizado com sucesso
|--------------------------------------------------------------------------
-->
<template>
<Dialog
v-model:visible="isOpen"
modal
:draggable="false"
:closable="false"
:dismissableMask="false"
:maximizable="false"
:style="{ width: '90vw', maxWidth: '1100px', height: maximized ? '100vh' : '90vh' }"
:contentStyle="{ padding: 0, overflow: 'auto', height: '100%' }"
pt:mask:class="backdrop-blur-xs"
>
<!-- Header -->
<template #header>
<div class="flex items-center justify-between w-full gap-3">
<!-- Título -->
<span class="text-base font-semibold text-[var(--text-color)] leading-tight">
{{ patientId ? 'Editar Paciente' : 'Cadastro de Paciente' }}
</span>
<!-- Botões à direita -->
<div class="flex items-center gap-1 ml-auto">
<!-- Preencher tudo ( testMODE) -->
<Button
v-if="pageRef?.canSee?.('testMODE')"
label="Preencher tudo"
icon="pi pi-bolt"
severity="secondary"
outlined
size="small"
class="rounded-full"
:disabled="pageRef?.saving?.value || pageRef?.deleting?.value"
@click="pageRef?.fillRandomPatient?.()"
/>
<!-- Excluir ( em edição) -->
<Button
v-if="patientId"
icon="pi pi-trash"
severity="danger"
outlined
size="small"
class="rounded-full"
:loading="pageRef?.deleting?.value"
:disabled="pageRef?.saving?.value || pageRef?.deleting?.value"
title="Excluir paciente"
@click="pageRef?.confirmDelete?.()"
/>
<!-- Maximizar -->
<button
class="w-8 h-8 rounded-lg border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-sm transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
:title="maximized ? 'Restaurar' : 'Maximizar'"
@click="maximized = !maximized"
>
<i :class="maximized ? 'pi pi-window-minimize' : 'pi pi-window-maximize'" />
</button>
<!-- Fechar -->
<button
class="w-8 h-8 rounded-lg border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-sm transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
title="Fechar"
@click="isOpen = false"
>
<i class="pi pi-times" />
</button>
</div>
</div>
</template>
<!-- Conteúdo -->
<PatientsCadastroPage
ref="pageRef"
:dialog-mode="true"
:patient-id="patientId"
@cancel="isOpen = false"
@created="onCreated"
/>
<!-- Footer -->
<template #footer>
<div class="flex justify-end gap-2">
<Button
label="Cancelar"
severity="secondary"
text
:disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value"
@click="isOpen = false"
/>
<!-- Na rota de pacientes: "Salvar" -->
<Button
v-if="isOnPatientsPage"
label="Salvar"
:loading="!!pageRef?.saving?.value"
:disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value"
@click="submitWith('only')"
/>
<!-- Fora da rota de pacientes: "Salvar e fechar" + "Salvar e ver pacientes" -->
<template v-else>
<Button
label="Salvar e fechar"
severity="secondary"
outlined
:loading="pendingMode === 'only' && !!pageRef?.saving?.value"
:disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value"
@click="submitWith('only')"
/>
<Button
label="Salvar e ver pacientes"
:loading="pendingMode === 'view' && !!pageRef?.saving?.value"
:disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value"
@click="submitWith('view')"
/>
</template>
</div>
</template>
</Dialog>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PatientsCadastroPage from '@/features/patients/cadastro/PatientsCadastroPage.vue'
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import PatientsCadastroPage from '@/features/patients/cadastro/PatientsCadastroPage.vue';
const props = defineProps({
modelValue: { type: Boolean, default: false },
patientId: { type: String, default: null }
})
const emit = defineEmits(['update:modelValue', 'created'])
modelValue: { type: Boolean, default: false },
patientId: { type: String, default: null }
});
const emit = defineEmits(['update:modelValue', 'created']);
const isOpen = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
});
// Reset maximized when dialog opens
watch(() => props.modelValue, (v) => { if (!v) maximized.value = false })
watch(
() => props.modelValue,
(v) => {
if (!v) maximized.value = false;
}
);
const maximized = ref(false)
const pageRef = ref(null)
const pendingMode = ref('only')
const maximized = ref(false);
const pageRef = ref(null);
const pendingMode = ref('only');
const route = useRoute()
const router = useRouter()
const route = useRoute();
const router = useRouter();
const isOnPatientsPage = computed(() => {
const p = String(route.path || '')
return p.includes('/patients') || p.includes('/pacientes')
})
const p = String(route.path || '');
return p.includes('/patients') || p.includes('/pacientes');
});
function patientsListRoute () {
const p = String(route.path || '')
return p.startsWith('/therapist') ? '/therapist/patients' : '/admin/pacientes'
function patientsListRoute() {
const p = String(route.path || '');
return p.startsWith('/therapist') ? '/therapist/patients' : '/admin/pacientes';
}
function submitWith (mode) {
pendingMode.value = mode
pageRef.value?.onSubmit()
function submitWith(mode) {
pendingMode.value = mode;
pageRef.value?.onSubmit();
}
async function onCreated (data) {
isOpen.value = false
emit('created', data)
if (pendingMode.value === 'view') {
await router.push(patientsListRoute())
}
async function onCreated(data) {
isOpen.value = false;
emit('created', data);
if (pendingMode.value === 'view') {
await router.push(patientsListRoute());
}
}
</script>
<template>
<Dialog
v-model:visible="isOpen"
modal
:draggable="false"
:closable="false"
:dismissableMask="false"
:maximizable="false"
:style="{ width: '90vw', maxWidth: '1100px', height: maximized ? '100vh' : '90vh' }"
:contentStyle="{ padding: 0, overflow: 'auto', height: '100%' }"
pt:mask:class="backdrop-blur-xs"
>
<!-- Header -->
<template #header>
<div class="flex items-center justify-between w-full gap-3">
<!-- Título -->
<span class="text-base font-semibold text-[var(--text-color)] leading-tight">
{{ patientId ? 'Editar Paciente' : 'Cadastro de Paciente' }}
</span>
<!-- Botões à direita -->
<div class="flex items-center gap-1 ml-auto">
<!-- Preencher tudo ( testMODE) -->
<Button
v-if="pageRef?.canSee?.('testMODE')"
label="Preencher tudo"
icon="pi pi-bolt"
severity="secondary"
outlined
size="small"
class="rounded-full"
:disabled="pageRef?.saving?.value || pageRef?.deleting?.value"
@click="pageRef?.fillRandomPatient?.()"
/>
<!-- Excluir ( em edição) -->
<Button
v-if="patientId"
icon="pi pi-trash"
severity="danger"
outlined
size="small"
class="rounded-full"
:loading="pageRef?.deleting?.value"
:disabled="pageRef?.saving?.value || pageRef?.deleting?.value"
title="Excluir paciente"
@click="pageRef?.confirmDelete?.()"
/>
<!-- Maximizar -->
<button
class="w-8 h-8 rounded-lg border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-sm transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
:title="maximized ? 'Restaurar' : 'Maximizar'"
@click="maximized = !maximized"
>
<i :class="maximized ? 'pi pi-window-minimize' : 'pi pi-window-maximize'" />
</button>
<!-- Fechar -->
<button
class="w-8 h-8 rounded-lg border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-sm transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
title="Fechar"
@click="isOpen = false"
>
<i class="pi pi-times" />
</button>
</div>
</div>
</template>
<!-- Conteúdo -->
<PatientsCadastroPage ref="pageRef" :dialog-mode="true" :patient-id="patientId" @cancel="isOpen = false" @created="onCreated" />
<!-- Footer -->
<template #footer>
<div class="flex justify-end gap-2">
<Button label="Cancelar" severity="secondary" text :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="isOpen = false" />
<!-- Na rota de pacientes: "Salvar" -->
<Button v-if="isOnPatientsPage" label="Salvar" :loading="!!pageRef?.saving?.value" :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="submitWith('only')" />
<!-- Fora da rota de pacientes: "Salvar e fechar" + "Salvar e ver pacientes" -->
<template v-else>
<Button label="Salvar e fechar" severity="secondary" outlined :loading="pendingMode === 'only' && !!pageRef?.saving?.value" :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="submitWith('only')" />
<Button label="Salvar e ver pacientes" :loading="pendingMode === 'view' && !!pageRef?.saving?.value" :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="submitWith('view')" />
</template>
</div>
</template>
</Dialog>
</template>
+131 -160
View File
@@ -26,183 +26,154 @@
| toggle(event) abre/fecha o Popover
|--------------------------------------------------------------------------
-->
<template>
<PatientCadastroDialog v-model="showCadastroDialog" />
<Popover ref="popRef">
<div class="flex flex-col min-w-[230px]">
<!-- Cadastro rápido -->
<button
class="flex items-center gap-2.5 px-3 py-2.5 rounded-md cursor-pointer border-0 bg-transparent text-left w-full transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)]"
@click="onQuickCreate"
>
<div class="w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 bg-indigo-500/10 text-indigo-500">
<i class="pi pi-bolt text-sm" />
</div>
<div>
<div class="text-sm font-semibold text-[var(--text-color)]">Cadastro Rápido</div>
<div class="text-[0.7rem] text-[var(--text-color-secondary)]">Nome, e-mail e telefone</div>
</div>
</button>
<!-- Cadastro completo -->
<button
class="flex items-center gap-2.5 px-3 py-2.5 rounded-md cursor-pointer border-0 bg-transparent text-left w-full transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)]"
@click="onGoComplete"
>
<div class="w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 bg-emerald-500/10 text-emerald-600">
<i class="pi pi-user-plus text-sm" />
</div>
<div>
<div class="text-sm font-semibold text-[var(--text-color)]">Cadastro Completo</div>
<div class="text-[0.7rem] text-[var(--text-color-secondary)]">Formulário detalhado</div>
</div>
</button>
<!-- Divisor -->
<div class="mx-3 my-1.5 border-t border-[var(--surface-border,#e2e8f0)]" />
<!-- Link de cadastro -->
<div class="px-3 pb-3">
<div class="flex items-center gap-1.5 text-[0.68rem] font-bold uppercase tracking-wider text-[var(--text-color-secondary)] opacity-60 mb-2">
<i class="pi pi-link text-[0.6rem]" />
Link de cadastro
</div>
<!-- Carregando token -->
<div v-if="loadingToken" class="flex items-center gap-1.5 text-xs text-[var(--text-color-secondary)] py-1">
<i class="pi pi-spin pi-spinner text-[0.7rem]" /> Carregando link
</div>
<!-- Sem token ainda -->
<div v-else-if="!inviteToken" class="text-[0.7rem] text-[var(--text-color-secondary)] opacity-60 py-1">
Nenhum link ativo.
<button class="underline cursor-pointer border-0 bg-transparent text-[var(--primary-color,#6366f1)]" @click="loadToken">Tentar novamente</button>
</div>
<!-- URL + ações -->
<template v-else>
<InputGroup class="w-full">
<InputText
:value="publicUrl"
readonly
class="text-[0.68rem] font-mono"
style="min-width: 0"
/>
<InputGroupAddon
class="cursor-pointer hover:bg-[var(--surface-hover)] transition-colors"
title="Copiar link"
@click="copyLink"
>
<i class="pi pi-copy text-sm" />
</InputGroupAddon>
</InputGroup>
<div class="flex gap-1 mt-2">
<Button
label="Copiar mensagem"
icon="pi pi-comment"
text
size="small"
class="flex-1 text-xs rounded-full"
@click="copyMessage"
/>
<Button
icon="pi pi-external-link"
text
size="small"
class="rounded-full"
v-tooltip.top="'Abrir no navegador'"
@click="openLink"
/>
</div>
</template>
</div>
</div>
</Popover>
</template>
<script setup>
import { ref, computed } from 'vue'
import Popover from 'primevue/popover'
import { useToast } from 'primevue/usetoast'
import { supabase } from '@/lib/supabase/client'
import PatientCadastroDialog from './PatientCadastroDialog.vue'
import { ref, computed } from 'vue';
import Popover from 'primevue/popover';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import PatientCadastroDialog from './PatientCadastroDialog.vue';
const emit = defineEmits(['quick-create'])
const showCadastroDialog = ref(false)
const toast = useToast()
const emit = defineEmits(['quick-create']);
const showCadastroDialog = ref(false);
const toast = useToast();
const popRef = ref(null)
const inviteToken = ref('')
const loadingToken = ref(false)
let tokenLoaded = false
const popRef = ref(null);
const inviteToken = ref('');
const loadingToken = ref(false);
let tokenLoaded = false;
const publicUrl = computed(() => {
if (!inviteToken.value) return ''
return `${window.location.origin}/cadastro/paciente?t=${encodeURIComponent(inviteToken.value)}`
})
if (!inviteToken.value) return '';
return `${window.location.origin}/cadastro/paciente?t=${encodeURIComponent(inviteToken.value)}`;
});
async function loadToken () {
if (tokenLoaded || loadingToken.value) return
loadingToken.value = true
try {
const { data: authData } = await supabase.auth.getUser()
const uid = authData?.user?.id
if (!uid) return
const { data } = await supabase
.from('patient_invites')
.select('token')
.eq('owner_id', uid)
.eq('active', true)
.order('created_at', { ascending: false })
.limit(1)
if (data?.[0]?.token) {
inviteToken.value = data[0].token
tokenLoaded = true
async function loadToken() {
if (tokenLoaded || loadingToken.value) return;
loadingToken.value = true;
try {
const { data: authData } = await supabase.auth.getUser();
const uid = authData?.user?.id;
if (!uid) return;
const { data } = await supabase.from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
if (data?.[0]?.token) {
inviteToken.value = data[0].token;
tokenLoaded = true;
}
} catch {
/* silencioso */
} finally {
loadingToken.value = false;
}
} catch { /* silencioso */ } finally {
loadingToken.value = false
}
}
function toggle (event) {
popRef.value?.toggle(event)
loadToken()
function toggle(event) {
popRef.value?.toggle(event);
loadToken();
}
function close () {
try { popRef.value?.hide() } catch {}
function close() {
try {
popRef.value?.hide();
} catch {}
}
function onQuickCreate () { close(); emit('quick-create') }
function onGoComplete () { close(); showCadastroDialog.value = true }
async function copyLink () {
if (!publicUrl.value) return
try {
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 onQuickCreate() {
close();
emit('quick-create');
}
function onGoComplete() {
close();
showCadastroDialog.value = true;
}
async function copyMessage () {
if (!publicUrl.value) return
try {
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 {}
async function copyLink() {
if (!publicUrl.value) return;
try {
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 copyMessage() {
if (!publicUrl.value) return;
try {
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 {}
}
defineExpose({ toggle, close })
function openLink() {
if (!publicUrl.value) return;
window.open(publicUrl.value, '_blank', 'noopener');
}
defineExpose({ toggle, close });
</script>
<template>
<PatientCadastroDialog v-model="showCadastroDialog" />
<Popover ref="popRef">
<div class="flex flex-col min-w-[230px]">
<!-- Cadastro rápido -->
<button class="flex items-center gap-2.5 px-3 py-2.5 rounded-md cursor-pointer border-0 bg-transparent text-left w-full transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)]" @click="onQuickCreate">
<div class="w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 bg-indigo-500/10 text-indigo-500">
<i class="pi pi-bolt text-sm" />
</div>
<div>
<div class="text-sm font-semibold text-[var(--text-color)]">Cadastro Rápido</div>
<div class="text-[0.7rem] text-[var(--text-color-secondary)]">Nome, e-mail e telefone</div>
</div>
</button>
<!-- Cadastro completo -->
<button class="flex items-center gap-2.5 px-3 py-2.5 rounded-md cursor-pointer border-0 bg-transparent text-left w-full transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)]" @click="onGoComplete">
<div class="w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 bg-emerald-500/10 text-emerald-600">
<i class="pi pi-user-plus text-sm" />
</div>
<div>
<div class="text-sm font-semibold text-[var(--text-color)]">Cadastro Completo</div>
<div class="text-[0.7rem] text-[var(--text-color-secondary)]">Formulário detalhado</div>
</div>
</button>
<!-- Divisor -->
<div class="mx-3 my-1.5 border-t border-[var(--surface-border,#e2e8f0)]" />
<!-- Link de cadastro -->
<div class="px-3 pb-3">
<div class="flex items-center gap-1.5 text-[0.68rem] font-bold uppercase tracking-wider text-[var(--text-color-secondary)] opacity-60 mb-2">
<i class="pi pi-link text-[0.6rem]" />
Link de cadastro
</div>
<!-- Carregando token -->
<div v-if="loadingToken" class="flex items-center gap-1.5 text-xs text-[var(--text-color-secondary)] py-1"><i class="pi pi-spin pi-spinner text-[0.7rem]" /> Carregando link</div>
<!-- Sem token ainda -->
<div v-else-if="!inviteToken" class="text-[0.7rem] text-[var(--text-color-secondary)] opacity-60 py-1">
Nenhum link ativo.
<button class="underline cursor-pointer border-0 bg-transparent text-[var(--primary-color,#6366f1)]" @click="loadToken">Tentar novamente</button>
</div>
<!-- URL + ações -->
<template v-else>
<InputGroup class="w-full">
<InputText :value="publicUrl" readonly class="text-[0.68rem] font-mono" style="min-width: 0" />
<InputGroupAddon class="cursor-pointer hover:bg-[var(--surface-hover)] transition-colors" title="Copiar link" @click="copyLink">
<i class="pi pi-copy text-sm" />
</InputGroupAddon>
</InputGroup>
<div class="flex gap-1 mt-2">
<Button label="Copiar mensagem" icon="pi pi-comment" text size="small" class="flex-1 text-xs rounded-full" @click="copyMessage" />
<Button icon="pi pi-external-link" text size="small" class="rounded-full" v-tooltip.top="'Abrir no navegador'" @click="openLink" />
</div>
</template>
</div>
</div>
</Popover>
</template>