1362 lines
58 KiB
Vue
1362 lines
58 KiB
Vue
<!-- src/layout/configuracoes/ConfiguracoesAgendadorPage.vue -->
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { RouterLink } from 'vue-router'
|
||
import { supabase } from '@/lib/supabase/client'
|
||
import { useTenantStore } from '@/stores/tenantStore'
|
||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||
import { useToast } from 'primevue/usetoast'
|
||
|
||
const toast = useToast()
|
||
const tenantStore = useTenantStore()
|
||
const entitlements = useEntitlementsStore()
|
||
|
||
const hasAgendador = computed(() => entitlements.can('agendador.online'))
|
||
const hasLinkPersonalizado = computed(() => entitlements.can('agendador.link_personalizado'))
|
||
|
||
// ── Estado ─────────────────────────────────────────────────────
|
||
const loading = ref(true)
|
||
const ownerId = ref(null)
|
||
const expandedCard = ref(new Set())
|
||
const savingCard = ref(null)
|
||
|
||
// ── Upload de imagens ────────────────────────────────────────────
|
||
const AGENDADOR_BUCKET = 'agendador'
|
||
const uploadingField = ref(null) // 'logomarca' | 'header' | 'fundo'
|
||
const fileInputLogo = ref(null)
|
||
const fileInputHeader = ref(null)
|
||
const fileInputFundo = ref(null)
|
||
|
||
async function uploadImagem (file, field) {
|
||
if (!file || !ownerId.value) return null
|
||
uploadingField.value = field
|
||
try {
|
||
const ext = file.name.split('.').pop() || 'jpg'
|
||
const path = `${ownerId.value}/${field}-${Date.now()}.${ext}`
|
||
const { error: upErr } = await supabase.storage
|
||
.from(AGENDADOR_BUCKET)
|
||
.upload(path, file, { upsert: true, contentType: file.type })
|
||
if (upErr) throw upErr
|
||
const { data } = supabase.storage.from(AGENDADOR_BUCKET).getPublicUrl(path)
|
||
return data?.publicUrl || null
|
||
} finally {
|
||
uploadingField.value = null
|
||
}
|
||
}
|
||
|
||
async function onFileSelected (event, field) {
|
||
const file = event.target.files?.[0]
|
||
if (!file) return
|
||
try {
|
||
const url = await uploadImagem(file, field)
|
||
if (url) {
|
||
if (field === 'logomarca') cfg.value.logomarca_url = url
|
||
if (field === 'header') cfg.value.imagem_header_url = url
|
||
if (field === 'fundo') cfg.value.imagem_fundo_url = url
|
||
toast.add({ severity: 'success', summary: 'Imagem enviada', life: 2000 })
|
||
}
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro no upload', detail: e.message, life: 4000 })
|
||
}
|
||
}
|
||
|
||
// ── Expand / Collapse all ────────────────────────────────────────
|
||
const CARDS = ['identidade', 'perfil', 'fluxo', 'pagamento', 'triagem', 'textos']
|
||
function expandAll () { expandedCard.value = new Set(CARDS) }
|
||
function collapseAll () { expandedCard.value = new Set() }
|
||
|
||
// ── Defaults ───────────────────────────────────────────────────
|
||
const DEFAULT_CFG = {
|
||
ativo: false,
|
||
link_slug: '',
|
||
imagem_fundo_url: '',
|
||
imagem_header_url: '',
|
||
logomarca_url: '',
|
||
cor_primaria: '#4b6bff',
|
||
nome_exibicao: '',
|
||
endereco: '',
|
||
botao_como_chegar_ativo: true,
|
||
maps_url: '',
|
||
modo_aprovacao: 'aprovacao',
|
||
modalidade: 'presencial',
|
||
tipos_habilitados: ['primeira', 'retorno'],
|
||
duracao_sessao_min: 50,
|
||
antecedencia_minima_horas: 24,
|
||
prazo_resposta_horas: 2,
|
||
reserva_horas: 2,
|
||
pagamento_obrigatorio: false, // mantido para backward compat (derivado de pagamento_modo)
|
||
pagamento_modo: 'sem_pagamento', // 'sem_pagamento' | 'pagar_na_hora' | 'pix_antecipado'
|
||
pagamento_metodos_visiveis: [], // métodos exibidos ao paciente quando pagar_na_hora
|
||
pix_chave: '',
|
||
pix_countdown_minutos: 20,
|
||
triagem_motivo: true,
|
||
triagem_como_conheceu: false,
|
||
verificacao_email: false,
|
||
exigir_aceite_lgpd: true,
|
||
mensagem_boas_vindas: '',
|
||
texto_como_se_preparar: '',
|
||
texto_termos_lgpd: ''
|
||
}
|
||
|
||
const cfg = ref({ ...DEFAULT_CFG })
|
||
|
||
// ── Opções ─────────────────────────────────────────────────────
|
||
const tiposOptions = [
|
||
{ label: 'Primeira Entrevista', value: 'primeira' },
|
||
{ label: 'Retorno', value: 'retorno' },
|
||
{ label: 'Reagendar', value: 'reagendar'}
|
||
]
|
||
|
||
const modalidadeOptions = [
|
||
{ label: 'Presencial', value: 'presencial' },
|
||
{ label: 'Online (vídeo)', value: 'online' },
|
||
{ label: 'Ambos', value: 'ambos' }
|
||
]
|
||
|
||
const modoOptions = [
|
||
{ label: 'Aprovação manual', value: 'aprovacao' },
|
||
{ label: 'Automático', value: 'automatico' }
|
||
]
|
||
|
||
const duracoesOptions = [
|
||
{ label: '30 min', value: 30 },
|
||
{ label: '40 min', value: 40 },
|
||
{ label: '45 min', value: 45 },
|
||
{ label: '50 min', value: 50 },
|
||
{ label: '55 min', value: 55 },
|
||
{ label: '1 hora', value: 60 },
|
||
{ label: '1h 30', value: 90 },
|
||
{ label: '2 horas',value: 120 }
|
||
]
|
||
|
||
const antecedenciaOptions = [
|
||
{ label: 'Sem limite', value: 0 },
|
||
{ label: '1 hora', value: 1 },
|
||
{ label: '2 horas', value: 2 },
|
||
{ label: '6 horas', value: 6 },
|
||
{ label: '12 horas', value: 12 },
|
||
{ label: '24 horas', value: 24 },
|
||
{ label: '48 horas', value: 48 },
|
||
{ label: '72 horas', value: 72 }
|
||
]
|
||
|
||
const reservaOptions = [
|
||
{ label: '1 hora', value: 1 },
|
||
{ label: '2 horas', value: 2 },
|
||
{ label: '4 horas', value: 4 },
|
||
{ label: '6 horas', value: 6 },
|
||
{ label: '12 horas', value: 12 },
|
||
{ label: '24 horas', value: 24 },
|
||
{ label: '48 horas', value: 48 }
|
||
]
|
||
|
||
const pixCountdownOptions = [
|
||
{ label: '5 min', value: 5 },
|
||
{ label: '10 min', value: 10 },
|
||
{ label: '15 min', value: 15 },
|
||
{ label: '20 min', value: 20 },
|
||
{ label: '30 min', value: 30 },
|
||
{ label: '60 min', value: 60 }
|
||
]
|
||
|
||
const prazoRespostaOptions = [
|
||
{ label: '1 hora', value: 1 },
|
||
{ label: '2 horas', value: 2 },
|
||
{ label: '4 horas', value: 4 },
|
||
{ label: '6 horas', value: 6 },
|
||
{ label: '12 horas', value: 12 },
|
||
{ label: '24 horas', value: 24 },
|
||
{ label: '48 horas', value: 48 },
|
||
{ label: '72 horas', value: 72 }
|
||
]
|
||
|
||
// ── Link público ────────────────────────────────────────────────
|
||
const linkPublico = computed(() => {
|
||
if (!cfg.value.link_slug) return ''
|
||
return `${window.location.origin}/agendar/${cfg.value.link_slug}`
|
||
})
|
||
|
||
const linkCopied = ref(false)
|
||
let _copyTimer = null
|
||
async function copyLink () {
|
||
try {
|
||
await navigator.clipboard.writeText(linkPublico.value)
|
||
linkCopied.value = true
|
||
clearTimeout(_copyTimer)
|
||
_copyTimer = setTimeout(() => { linkCopied.value = false }, 2000)
|
||
} catch {
|
||
toast.add({ severity: 'warn', summary: 'Copie manualmente', detail: linkPublico.value, life: 5000 })
|
||
}
|
||
}
|
||
|
||
// ── Resumos dos cards ──────────────────────────────────────────
|
||
const resumoIdentidade = computed(() => {
|
||
const parts = []
|
||
if (cfg.value.nome_exibicao) parts.push(cfg.value.nome_exibicao)
|
||
if (cfg.value.cor_primaria) parts.push(cfg.value.cor_primaria)
|
||
return parts.join(' · ') || 'Não configurado'
|
||
})
|
||
|
||
const resumoPerfil = computed(() => {
|
||
const parts = []
|
||
if (cfg.value.endereco) parts.push(cfg.value.endereco.slice(0, 40) + (cfg.value.endereco.length > 40 ? '…' : ''))
|
||
if (cfg.value.botao_como_chegar_ativo) parts.push('Como chegar ativo')
|
||
return parts.join(' · ') || 'Não configurado'
|
||
})
|
||
|
||
const resumoFluxo = computed(() => {
|
||
const modo = cfg.value.modo_aprovacao === 'aprovacao' ? 'Aprovação manual' : 'Automático'
|
||
const tipos = (cfg.value.tipos_habilitados || []).length
|
||
return `${modo} · ${tipos} tipo${tipos !== 1 ? 's' : ''} · ${cfg.value.duracao_sessao_min} min`
|
||
})
|
||
|
||
const resumoPagamento = computed(() => {
|
||
const modo = cfg.value.pagamento_modo
|
||
if (modo === 'pix_antecipado') return `Pix obrigatório antes do agendamento · ${cfg.value.pix_countdown_minutos} min para pagar`
|
||
if (modo === 'pagar_na_hora') {
|
||
const ativos = cfg.value.pagamento_metodos_visiveis || []
|
||
return ativos.length ? `Pagar na hora · ${ativos.map(m => METODO_LABEL[m] || m).join(', ')}` : 'Pagar na hora da sessão'
|
||
}
|
||
return 'Sem cobrança antecipada'
|
||
})
|
||
|
||
// ── Payment Settings (lidos de payment_settings para sync) ──────
|
||
const paymentSettings = ref({})
|
||
|
||
const METODO_LABEL = {
|
||
pix: 'Pix',
|
||
deposito: 'Depósito/TED',
|
||
dinheiro: 'Dinheiro',
|
||
cartao: 'Cartão',
|
||
convenio: 'Convênio',
|
||
}
|
||
|
||
// Métodos que o terapeuta tem configurados em payment_settings
|
||
const metodosDisponiveis = computed(() => {
|
||
const ps = paymentSettings.value
|
||
const todos = [
|
||
{ key: 'pix', label: 'Pix', icon: 'pi-qrcode', ativo: ps.pix_ativo },
|
||
{ key: 'deposito', label: 'Depósito / TED', icon: 'pi-building-columns', ativo: ps.deposito_ativo },
|
||
{ key: 'dinheiro', label: 'Dinheiro', icon: 'pi-wallet', ativo: ps.dinheiro_ativo },
|
||
{ key: 'cartao', label: 'Cartão', icon: 'pi-credit-card', ativo: ps.cartao_ativo },
|
||
{ key: 'convenio', label: 'Convênio', icon: 'pi-heart', ativo: ps.convenio_ativo },
|
||
]
|
||
return todos
|
||
})
|
||
|
||
const algumMetodoConfigurado = computed(() => metodosDisponiveis.value.some(m => m.ativo))
|
||
|
||
// chave Pix sincronizada com payment_settings (fallback)
|
||
const pixChaveEfetiva = computed(() => cfg.value.pix_chave || paymentSettings.value.pix_chave || '')
|
||
|
||
async function loadPaymentSettings (uid) {
|
||
try {
|
||
const { data } = await supabase
|
||
.from('payment_settings')
|
||
.select('pix_ativo, pix_chave, pix_tipo, deposito_ativo, dinheiro_ativo, cartao_ativo, convenio_ativo')
|
||
.eq('owner_id', uid)
|
||
.maybeSingle()
|
||
paymentSettings.value = data || {}
|
||
} catch {
|
||
paymentSettings.value = {}
|
||
}
|
||
}
|
||
|
||
function toggleMetodoVisivel (key) {
|
||
const arr = [...(cfg.value.pagamento_metodos_visiveis || [])]
|
||
const idx = arr.indexOf(key)
|
||
if (idx === -1) arr.push(key)
|
||
else arr.splice(idx, 1)
|
||
cfg.value.pagamento_metodos_visiveis = arr
|
||
}
|
||
|
||
function isMetodoVisivel (key) {
|
||
return (cfg.value.pagamento_metodos_visiveis || []).includes(key)
|
||
}
|
||
|
||
const modosPagamento = [
|
||
{
|
||
value: 'sem_pagamento',
|
||
label: 'Sem cobrança antecipada',
|
||
desc: 'O horário fica reservado sem exigir pagamento.',
|
||
icon: 'pi-calendar-clock',
|
||
},
|
||
{
|
||
value: 'pagar_na_hora',
|
||
label: 'Pagar na hora da sessão',
|
||
desc: 'O paciente vê as formas de pagamento aceitas e paga no dia.',
|
||
icon: 'pi-wallet',
|
||
},
|
||
{
|
||
value: 'pix_antecipado',
|
||
label: 'Pix antecipado obrigatório',
|
||
desc: 'O paciente paga via Pix antes de o agendamento ser confirmado.',
|
||
icon: 'pi-qrcode',
|
||
},
|
||
]
|
||
|
||
const resumoTriagem = computed(() => {
|
||
const campos = []
|
||
if (cfg.value.triagem_motivo) campos.push('Motivo')
|
||
if (cfg.value.triagem_como_conheceu) campos.push('Como conheceu')
|
||
if (cfg.value.verificacao_email) campos.push('Verificação e-mail')
|
||
if (cfg.value.exigir_aceite_lgpd) campos.push('LGPD')
|
||
return campos.join(' · ') || 'Sem triagem extra'
|
||
})
|
||
|
||
const resumoTextos = computed(() => {
|
||
const bv = cfg.value.mensagem_boas_vindas?.trim()
|
||
const cp = cfg.value.texto_como_se_preparar?.trim()
|
||
if (bv || cp) return 'Textos configurados'
|
||
return 'Nenhum texto configurado'
|
||
})
|
||
|
||
// ── Auth / Tenant ──────────────────────────────────────────────
|
||
async function getOwnerId () {
|
||
const { data, error } = await supabase.auth.getUser()
|
||
if (error) throw error
|
||
return data?.user?.id
|
||
}
|
||
|
||
async function getActiveTenantId (uid) {
|
||
const fromStore = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null
|
||
if (fromStore) return fromStore
|
||
const { data } = await supabase.from('tenant_members').select('tenant_id').eq('user_id', uid).eq('status', 'active').limit(1).maybeSingle()
|
||
return data?.tenant_id || null
|
||
}
|
||
|
||
// ── Load ───────────────────────────────────────────────────────
|
||
async function load () {
|
||
loading.value = true
|
||
try {
|
||
const uid = await getOwnerId()
|
||
ownerId.value = uid
|
||
|
||
const [{ data, error }] = await Promise.all([
|
||
supabase.from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(),
|
||
loadPaymentSettings(uid),
|
||
])
|
||
|
||
if (error) throw error
|
||
|
||
if (data) {
|
||
const loaded = {
|
||
...DEFAULT_CFG,
|
||
...data,
|
||
tipos_habilitados: Array.isArray(data.tipos_habilitados) ? data.tipos_habilitados : DEFAULT_CFG.tipos_habilitados,
|
||
pagamento_metodos_visiveis: Array.isArray(data.pagamento_metodos_visiveis) ? data.pagamento_metodos_visiveis : [],
|
||
}
|
||
// backward compat: se coluna pagamento_modo não existe ainda no banco, deriva de pagamento_obrigatorio
|
||
if (!data.pagamento_modo) {
|
||
loaded.pagamento_modo = data.pagamento_obrigatorio ? 'pix_antecipado' : 'sem_pagamento'
|
||
}
|
||
cfg.value = loaded
|
||
} else {
|
||
// Seed com nome do tenant como sugestão
|
||
cfg.value = { ...DEFAULT_CFG }
|
||
if (tenantStore.tenant?.name) cfg.value.nome_exibicao = tenantStore.tenant.name
|
||
}
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro ao carregar', detail: e.message, life: 4000 })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// ── Toggle ativo ───────────────────────────────────────────────
|
||
async function toggleAtivo () {
|
||
const uid = ownerId.value
|
||
if (!uid) return
|
||
|
||
const novoAtivo = !cfg.value.ativo
|
||
cfg.value.ativo = novoAtivo
|
||
|
||
try {
|
||
const tenantId = await getActiveTenantId(uid)
|
||
|
||
await supabase.from('agendador_configuracoes').upsert(
|
||
{ owner_id: uid, tenant_id: tenantId, ativo: novoAtivo, updated_at: new Date().toISOString() },
|
||
{ onConflict: 'owner_id' }
|
||
)
|
||
|
||
toast.add({
|
||
severity: novoAtivo ? 'success' : 'info',
|
||
summary: novoAtivo ? 'Agendador ativado' : 'Agendador desativado',
|
||
life: 3000
|
||
})
|
||
if (novoAtivo) await load() // recarrega para obter o slug gerado pelo trigger
|
||
} catch (e) {
|
||
cfg.value.ativo = !novoAtivo
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e.message, life: 4000 })
|
||
}
|
||
}
|
||
|
||
// ── Save card ──────────────────────────────────────────────────
|
||
async function saveCard (cardKey) {
|
||
savingCard.value = cardKey
|
||
try {
|
||
const uid = ownerId.value
|
||
const tenantId = await getActiveTenantId(uid)
|
||
const payload = buildPayload(cardKey)
|
||
|
||
await supabase.from('agendador_configuracoes').upsert(
|
||
{ owner_id: uid, tenant_id: tenantId, ...payload, updated_at: new Date().toISOString() },
|
||
{ onConflict: 'owner_id' }
|
||
)
|
||
|
||
toast.add({ severity: 'success', summary: 'Salvo', life: 2500 })
|
||
expandedCard.value = new Set()
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro ao salvar', detail: e.message, life: 4000 })
|
||
} finally {
|
||
savingCard.value = null
|
||
}
|
||
}
|
||
|
||
function buildPayload (cardKey) {
|
||
const c = cfg.value
|
||
if (cardKey === 'identidade') {
|
||
return {
|
||
imagem_fundo_url: c.imagem_fundo_url,
|
||
imagem_header_url: c.imagem_header_url,
|
||
logomarca_url: c.logomarca_url,
|
||
cor_primaria: c.cor_primaria
|
||
}
|
||
}
|
||
if (cardKey === 'perfil') {
|
||
return {
|
||
nome_exibicao: c.nome_exibicao,
|
||
endereco: c.endereco,
|
||
botao_como_chegar_ativo: c.botao_como_chegar_ativo,
|
||
maps_url: c.maps_url
|
||
}
|
||
}
|
||
if (cardKey === 'fluxo') {
|
||
return {
|
||
modo_aprovacao: c.modo_aprovacao,
|
||
modalidade: c.modalidade,
|
||
tipos_habilitados: c.tipos_habilitados,
|
||
duracao_sessao_min: c.duracao_sessao_min,
|
||
antecedencia_minima_horas: c.antecedencia_minima_horas,
|
||
prazo_resposta_horas: c.prazo_resposta_horas
|
||
}
|
||
}
|
||
if (cardKey === 'pagamento') {
|
||
const modo = c.pagamento_modo || 'sem_pagamento'
|
||
return {
|
||
pagamento_modo: modo,
|
||
pagamento_obrigatorio: modo === 'pix_antecipado', // backward compat
|
||
pagamento_metodos_visiveis: c.pagamento_metodos_visiveis || [],
|
||
pix_chave: c.pix_chave?.trim() ?? '',
|
||
pix_countdown_minutos: c.pix_countdown_minutos,
|
||
reserva_horas: c.reserva_horas
|
||
}
|
||
}
|
||
if (cardKey === 'triagem') {
|
||
return {
|
||
triagem_motivo: c.triagem_motivo,
|
||
triagem_como_conheceu:c.triagem_como_conheceu,
|
||
verificacao_email: c.verificacao_email,
|
||
exigir_aceite_lgpd: c.exigir_aceite_lgpd
|
||
}
|
||
}
|
||
if (cardKey === 'textos') {
|
||
return {
|
||
mensagem_boas_vindas: c.mensagem_boas_vindas,
|
||
texto_como_se_preparar: c.texto_como_se_preparar,
|
||
texto_termos_lgpd: c.texto_termos_lgpd
|
||
}
|
||
}
|
||
if (cardKey === 'slug') {
|
||
return { link_slug: c.link_slug?.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-') || null }
|
||
}
|
||
return {}
|
||
}
|
||
|
||
function toggleCard (key) {
|
||
const s = new Set(expandedCard.value)
|
||
if (s.has(key)) s.delete(key)
|
||
else s.add(key)
|
||
expandedCard.value = s
|
||
}
|
||
|
||
onMounted(load)
|
||
</script>
|
||
|
||
<template>
|
||
<div class="flex flex-col gap-4">
|
||
|
||
<!-- ── LOADING ──────────────────────────────────────────────── -->
|
||
<div v-if="loading" class="flex justify-center py-16">
|
||
<ProgressSpinner style="width:40px;height:40px" />
|
||
</div>
|
||
|
||
<template v-else>
|
||
|
||
<!-- Subheader -->
|
||
<div class="cfg-subheader">
|
||
<div class="cfg-subheader__icon"><i class="pi pi-calendar-clock" /></div>
|
||
<div class="min-w-0">
|
||
<div class="cfg-subheader__title">Agendador Online</div>
|
||
<div class="cfg-subheader__sub">Personalize a aparência, fluxo e comportamento do seu agendador público</div>
|
||
</div>
|
||
<div class="cfg-subheader__actions">
|
||
<Button size="small" icon="pi pi-arrows-v" label="Expandir" severity="secondary" outlined class="rounded-full" @click="expandAll" />
|
||
<Button size="small" icon="pi pi-minus" label="Contrair" severity="secondary" outlined class="rounded-full" @click="collapseAll" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── CARD: STATUS / ATIVAR ──────────────────────────────── -->
|
||
<div class="agd-card">
|
||
<div class="flex flex-col gap-4">
|
||
|
||
<!-- Cabeçalho PRO -->
|
||
<div class="flex items-start justify-between gap-4">
|
||
<div class="flex items-center gap-3">
|
||
<div class="grid place-items-center w-11 h-11 rounded-[6px] shrink-0"
|
||
:class="cfg.ativo ? 'bg-green-100 dark:bg-green-900/30 text-green-600' : 'bg-surface-100 text-surface-400'">
|
||
<i class="pi pi-calendar-clock text-xl" />
|
||
</div>
|
||
<div>
|
||
<div class="font-bold text-lg leading-none">Agendador Online</div>
|
||
<div class="text-sm text-surface-500 mt-1">
|
||
Funcionalidade <Tag value="PRO" severity="contrast" class="text-xs ml-1" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-3 shrink-0">
|
||
<template v-if="hasAgendador">
|
||
<span class="text-sm font-medium" :class="cfg.ativo ? 'text-green-600' : 'text-surface-400'">
|
||
{{ cfg.ativo ? 'Ativo' : 'Inativo' }}
|
||
</span>
|
||
<InputSwitch :modelValue="cfg.ativo" @update:modelValue="toggleAtivo" />
|
||
</template>
|
||
<template v-else>
|
||
<Tag value="Plano não inclui" severity="warn" class="text-xs" />
|
||
<InputSwitch :modelValue="false" disabled v-tooltip.left="'Seu plano não inclui o Agendador Online'" />
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<Divider class="my-0" />
|
||
|
||
<!-- Link público -->
|
||
<div v-if="cfg.ativo">
|
||
<div class="text-sm font-semibold text-surface-600 mb-2">Link público do agendador</div>
|
||
|
||
<!-- Gerando slug (aguarda trigger do banco) -->
|
||
<div v-if="!cfg.link_slug" class="flex items-center gap-2 text-sm text-surface-400">
|
||
<i class="pi pi-spin pi-spinner text-xs" /> Gerando link...
|
||
</div>
|
||
|
||
<!-- Link disponível -->
|
||
<template v-else>
|
||
<InputGroup>
|
||
<InputGroupAddon><i class="pi pi-link" /></InputGroupAddon>
|
||
<InputText readonly :value="linkPublico" class="font-mono text-xs" />
|
||
<Button
|
||
:icon="linkCopied ? 'pi pi-check' : 'pi pi-copy'"
|
||
:severity="linkCopied ? 'success' : 'secondary'"
|
||
title="Copiar link"
|
||
@click="copyLink()"
|
||
/>
|
||
<Button
|
||
icon="pi pi-external-link"
|
||
severity="secondary"
|
||
title="Abrir no navegador"
|
||
@click="() => window.open(linkPublico, '_blank', 'noopener')"
|
||
/>
|
||
</InputGroup>
|
||
<div class="text-xs text-surface-400 mt-2">
|
||
Este link é permanente e nunca muda. O link personalizado (se ativo) é um apelido — este continua funcionando mesmo se o apelido for removido.
|
||
</div>
|
||
|
||
<!-- Link personalizado bloqueado -->
|
||
<div v-if="!hasLinkPersonalizado" class="mt-3 flex items-center gap-3 p-3 rounded-[6px] border border-dashed
|
||
border-surface-300 dark:border-surface-600 bg-surface-50 dark:bg-surface-800/50">
|
||
<div class="grid place-items-center w-9 h-9 rounded-[6px] bg-amber-100 dark:bg-amber-900/30 text-amber-500 shrink-0">
|
||
<i class="pi pi-lock text-base" />
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<div class="text-sm font-semibold leading-none">Link Personalizado</div>
|
||
<div class="text-xs text-surface-400 mt-1">
|
||
Escolha sua própria URL — <span class="font-mono">/agendar/<b>dra-ana-silva</b></span>.
|
||
Disponível em planos superiores.
|
||
</div>
|
||
</div>
|
||
<Tag value="Upgrade" severity="warn" class="shrink-0 text-xs" />
|
||
</div>
|
||
|
||
<!-- Input para slug personalizado -->
|
||
<div v-else class="mt-3 flex items-center gap-2">
|
||
<InputGroup>
|
||
<InputGroupAddon class="text-xs text-surface-400 font-mono">/agendar/</InputGroupAddon>
|
||
<InputText v-model="cfg.link_slug" placeholder="dra-ana-silva" class="font-mono" />
|
||
<Button label="Salvar" icon="pi pi-check" @click="saveCard('slug')" />
|
||
</InputGroup>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div v-else class="text-sm text-surface-500 leading-relaxed">
|
||
Ative o agendador para que seus pacientes possam solicitar horários online.
|
||
Você controla quem pode agendar e quais horários ficam disponíveis.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── CARD: IDENTIDADE VISUAL ────────────────────────────── -->
|
||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('identidade') }">
|
||
<button
|
||
type="button"
|
||
class="agd-accordion__header"
|
||
@click="toggleCard('identidade')"
|
||
>
|
||
<div class="agd-accordion__icon bg-purple-100 dark:bg-purple-900/30 text-purple-600">
|
||
<i class="pi pi-palette" />
|
||
</div>
|
||
<div class="min-w-0 flex-1 text-left">
|
||
<div class="agd-accordion__title">Identidade Visual</div>
|
||
<div v-if="!expandedCard.has('identidade')" class="agd-accordion__summary">{{ resumoIdentidade }}</div>
|
||
</div>
|
||
<i class="pi agd-accordion__chevron"
|
||
:class="expandedCard.has('identidade') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||
</button>
|
||
|
||
<div v-if="expandedCard.has('identidade')" class="agd-accordion__body">
|
||
<div class="flex flex-col gap-5">
|
||
|
||
<!-- Nome de exibição (aqui pois é parte da identidade) -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-2">Nome de exibição</label>
|
||
<InputText v-model="cfg.nome_exibicao" placeholder="Ex: Dra. Ana Silva — Psicóloga" class="w-full" />
|
||
<div class="text-xs text-surface-400 mt-1">Como aparece no topo do agendador para o paciente.</div>
|
||
</div>
|
||
|
||
<!-- Cor primária -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-2">Cor principal</label>
|
||
<div class="flex items-center gap-3">
|
||
<ColorPicker v-model="cfg.cor_primaria" format="hex" />
|
||
<InputText v-model="cfg.cor_primaria" placeholder="#4b6bff" class="w-32 font-mono" maxlength="7" />
|
||
<div class="w-10 h-10 rounded-[6px] border border-surface-200 shrink-0"
|
||
:style="{ background: cfg.cor_primaria }" />
|
||
</div>
|
||
<div class="text-xs text-surface-400 mt-1">Botões e destaques do agendador.</div>
|
||
</div>
|
||
|
||
<Divider class="my-0" />
|
||
|
||
<!-- Logomarca -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-1">Logomarca (avatar circular)</label>
|
||
<div class="text-xs text-surface-400 mb-2">Quadrada ou circular. Recomendado: 300×300 px.</div>
|
||
<div class="flex items-start gap-4">
|
||
<!-- Preview -->
|
||
<div v-if="cfg.logomarca_url" class="shrink-0">
|
||
<img :src="cfg.logomarca_url" alt="Logomarca"
|
||
class="w-16 h-16 rounded-full object-cover border-2 border-surface-200" />
|
||
</div>
|
||
<div class="flex-1 flex flex-col gap-2">
|
||
<!-- Upload arquivo -->
|
||
<div class="agd-upload-zone" @click="fileInputLogo.click()">
|
||
<i class="pi pi-upload text-surface-400" />
|
||
<span class="text-sm text-surface-500">
|
||
{{ uploadingField === 'logomarca' ? 'Enviando...' : 'Clique para enviar — PNG · JPG · WebP · máx 5 MB' }}
|
||
</span>
|
||
<input ref="fileInputLogo" type="file" accept="image/*" class="hidden"
|
||
@change="e => onFileSelected(e, 'logomarca')" />
|
||
</div>
|
||
<!-- URL direta -->
|
||
<InputText v-model="cfg.logomarca_url" placeholder="ou cole uma URL pública..." class="w-full text-xs" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Imagem do header -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-1">Imagem do header</label>
|
||
<div class="text-xs text-surface-400 mb-2">Faixa superior do agendador. Recomendado: 1400×300 px.</div>
|
||
<div class="flex flex-col gap-2">
|
||
<div class="agd-upload-zone" @click="fileInputHeader.click()">
|
||
<i class="pi pi-upload text-surface-400" />
|
||
<span class="text-sm text-surface-500">
|
||
{{ uploadingField === 'header' ? 'Enviando...' : 'Clique para enviar — PNG · JPG · WebP · máx 5 MB' }}
|
||
</span>
|
||
<input ref="fileInputHeader" type="file" accept="image/*" class="hidden"
|
||
@change="e => onFileSelected(e, 'header')" />
|
||
</div>
|
||
<InputText v-model="cfg.imagem_header_url" placeholder="ou cole uma URL pública..." class="w-full text-xs" />
|
||
<div v-if="cfg.imagem_header_url" class="rounded-[6px] overflow-hidden h-20 w-full">
|
||
<img :src="cfg.imagem_header_url" alt="Header" class="w-full h-full object-cover" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Imagem de fundo -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-1">Imagem de fundo</label>
|
||
<div class="text-xs text-surface-400 mb-2">Fundo da página do agendador. Recomendado: 1920×1080 px.</div>
|
||
<div class="flex flex-col gap-2">
|
||
<div class="agd-upload-zone" @click="fileInputFundo.click()">
|
||
<i class="pi pi-upload text-surface-400" />
|
||
<span class="text-sm text-surface-500">
|
||
{{ uploadingField === 'fundo' ? 'Enviando...' : 'Clique para enviar — PNG · JPG · WebP · máx 5 MB' }}
|
||
</span>
|
||
<input ref="fileInputFundo" type="file" accept="image/*" class="hidden"
|
||
@change="e => onFileSelected(e, 'fundo')" />
|
||
</div>
|
||
<InputText v-model="cfg.imagem_fundo_url" placeholder="ou cole uma URL pública..." class="w-full text-xs" />
|
||
<div v-if="cfg.imagem_fundo_url" class="rounded-[6px] overflow-hidden h-28 w-full">
|
||
<img :src="cfg.imagem_fundo_url" alt="Fundo" class="w-full h-full object-cover" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Salvar -->
|
||
<div class="flex justify-end">
|
||
<Button
|
||
label="Salvar identidade visual"
|
||
icon="pi pi-check"
|
||
:loading="savingCard === 'identidade'"
|
||
@click="saveCard('identidade')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── CARD: PERFIL PÚBLICO ───────────────────────────────── -->
|
||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('perfil') }">
|
||
<button
|
||
type="button"
|
||
class="agd-accordion__header"
|
||
@click="toggleCard('perfil')"
|
||
>
|
||
<div class="agd-accordion__icon bg-blue-100 dark:bg-blue-900/30 text-blue-600">
|
||
<i class="pi pi-map-marker" />
|
||
</div>
|
||
<div class="min-w-0 flex-1 text-left">
|
||
<div class="agd-accordion__title">Perfil Público</div>
|
||
<div v-if="!expandedCard.has('perfil')" class="agd-accordion__summary">{{ resumoPerfil }}</div>
|
||
</div>
|
||
<i class="pi agd-accordion__chevron"
|
||
:class="expandedCard.has('perfil') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||
</button>
|
||
|
||
<div v-if="expandedCard.has('perfil')" class="agd-accordion__body">
|
||
<div class="flex flex-col gap-5">
|
||
|
||
<!-- Endereço -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-2">Endereço</label>
|
||
<InputText v-model="cfg.endereco" placeholder="Rua das Flores, 123, Centro — São Paulo, SP" class="w-full" />
|
||
</div>
|
||
|
||
<!-- Botão Como Chegar -->
|
||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
|
||
<div>
|
||
<div class="font-medium text-sm">Botão "Como chegar"</div>
|
||
<div class="text-xs text-surface-400 mt-0.5">Exibe um botão que abre o mapa para o paciente.</div>
|
||
</div>
|
||
<InputSwitch v-model="cfg.botao_como_chegar_ativo" />
|
||
</div>
|
||
|
||
<!-- URL do mapa -->
|
||
<div v-if="cfg.botao_como_chegar_ativo">
|
||
<label class="block text-sm font-semibold mb-2">URL do Google Maps (opcional)</label>
|
||
<InputText v-model="cfg.maps_url" placeholder="https://maps.google.com/..." class="w-full" />
|
||
<div class="text-xs text-surface-400 mt-1">Se vazio, abre uma busca pelo endereço acima.</div>
|
||
</div>
|
||
|
||
<div class="flex justify-end">
|
||
<Button
|
||
label="Salvar perfil"
|
||
icon="pi pi-check"
|
||
:loading="savingCard === 'perfil'"
|
||
@click="saveCard('perfil')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── CARD: FLUXO DE AGENDAMENTO ─────────────────────────── -->
|
||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('fluxo') }">
|
||
<button
|
||
type="button"
|
||
class="agd-accordion__header"
|
||
@click="toggleCard('fluxo')"
|
||
>
|
||
<div class="agd-accordion__icon bg-cyan-100 dark:bg-cyan-900/30 text-cyan-600">
|
||
<i class="pi pi-sitemap" />
|
||
</div>
|
||
<div class="min-w-0 flex-1 text-left">
|
||
<div class="agd-accordion__title">Fluxo de Agendamento</div>
|
||
<div v-if="!expandedCard.has('fluxo')" class="agd-accordion__summary">{{ resumoFluxo }}</div>
|
||
</div>
|
||
<i class="pi agd-accordion__chevron"
|
||
:class="expandedCard.has('fluxo') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||
</button>
|
||
|
||
<div v-if="expandedCard.has('fluxo')" class="agd-accordion__body">
|
||
<div class="flex flex-col gap-5">
|
||
|
||
<!-- Modo de aprovação -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-2">Modo de aprovação</label>
|
||
<div class="flex flex-col gap-2">
|
||
<div
|
||
v-for="opt in modoOptions"
|
||
:key="opt.value"
|
||
class="flex items-center gap-3 p-3 rounded-[6px] border cursor-pointer transition"
|
||
:class="cfg.modo_aprovacao === opt.value
|
||
? 'border-primary bg-primary/5 dark:bg-primary/10'
|
||
: 'border-surface-200 dark:border-surface-700 hover:border-surface-300'"
|
||
@click="cfg.modo_aprovacao = opt.value"
|
||
>
|
||
<RadioButton :modelValue="cfg.modo_aprovacao" :value="opt.value" />
|
||
<div>
|
||
<div class="font-medium text-sm">{{ opt.label }}</div>
|
||
<div class="text-xs text-surface-400">
|
||
<template v-if="opt.value === 'aprovacao'">
|
||
Você analisa cada solicitação e autoriza manualmente. O horário fica reservado até a resposta.
|
||
</template>
|
||
<template v-else>
|
||
Agendamentos confirmados automaticamente. O evento é criado na agenda sem revisão.
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Prazo de resposta (só aparece no modo aprovação) -->
|
||
<div v-if="cfg.modo_aprovacao === 'aprovacao'">
|
||
<label class="block text-sm font-semibold mb-2">Prazo para responder a solicitação</label>
|
||
<Select
|
||
v-model="cfg.prazo_resposta_horas"
|
||
:options="prazoRespostaOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
<div class="text-xs text-surface-400 mt-1">
|
||
Se não responder no prazo, o paciente é notificado e o horário é liberado.
|
||
</div>
|
||
</div>
|
||
|
||
<Divider class="my-0" />
|
||
|
||
<!-- Modalidade -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-2">Modalidade de atendimento</label>
|
||
<SelectButton
|
||
v-model="cfg.modalidade"
|
||
:options="modalidadeOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Tipos habilitados -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-2">Tipos de agendamento disponíveis</label>
|
||
<div class="flex flex-wrap gap-2">
|
||
<div
|
||
v-for="opt in tiposOptions"
|
||
:key="opt.value"
|
||
class="flex items-center gap-2 px-3 py-2 rounded-full border cursor-pointer transition select-none text-sm font-medium"
|
||
:class="cfg.tipos_habilitados?.includes(opt.value)
|
||
? 'border-primary bg-primary text-white'
|
||
: 'border-surface-200 dark:border-surface-700 hover:border-primary/50'"
|
||
@click="() => {
|
||
const list = [...(cfg.tipos_habilitados || [])]
|
||
const idx = list.indexOf(opt.value)
|
||
if (idx >= 0) list.splice(idx, 1)
|
||
else list.push(opt.value)
|
||
cfg.tipos_habilitados = list
|
||
}"
|
||
>
|
||
<i class="pi pi-check text-xs" v-if="cfg.tipos_habilitados?.includes(opt.value)" />
|
||
{{ opt.label }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Divider class="my-0" />
|
||
|
||
<!-- Duração da sessão -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-2">Duração da sessão</label>
|
||
<Select
|
||
v-model="cfg.duracao_sessao_min"
|
||
:options="duracoesOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
<div class="text-xs text-surface-400 mt-1">
|
||
Usado para bloquear o horário correto na agenda após aprovação.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Antecedência mínima -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-2">Antecedência mínima para agendar</label>
|
||
<Select
|
||
v-model="cfg.antecedencia_minima_horas"
|
||
:options="antecedenciaOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
<div class="text-xs text-surface-400 mt-1">
|
||
Pacientes não conseguem agendar com menos de X horas de antecedência.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex justify-end">
|
||
<Button
|
||
label="Salvar fluxo"
|
||
icon="pi pi-check"
|
||
:loading="savingCard === 'fluxo'"
|
||
@click="saveCard('fluxo')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── CARD: PAGAMENTO ────────────────────────────────────── -->
|
||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('pagamento') }">
|
||
<button
|
||
type="button"
|
||
class="agd-accordion__header"
|
||
@click="toggleCard('pagamento')"
|
||
>
|
||
<div class="agd-accordion__icon bg-green-100 dark:bg-green-900/30 text-green-600">
|
||
<i class="pi pi-credit-card" />
|
||
</div>
|
||
<div class="min-w-0 flex-1 text-left">
|
||
<div class="agd-accordion__title">Pagamento</div>
|
||
<div v-if="!expandedCard.has('pagamento')" class="agd-accordion__summary">{{ resumoPagamento }}</div>
|
||
</div>
|
||
<i class="pi agd-accordion__chevron"
|
||
:class="expandedCard.has('pagamento') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||
</button>
|
||
|
||
<div v-if="expandedCard.has('pagamento')" class="agd-accordion__body">
|
||
<div class="flex flex-col gap-5">
|
||
|
||
<!-- Modo de pagamento -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-3">Como o paciente vai pagar?</label>
|
||
<div class="flex flex-col gap-2">
|
||
<button
|
||
v-for="modo in modosPagamento"
|
||
:key="modo.value"
|
||
type="button"
|
||
class="flex items-center gap-3 p-3 rounded-[6px] border text-left transition"
|
||
:class="cfg.pagamento_modo === modo.value
|
||
? 'border-primary bg-primary/5 ring-1 ring-primary/30'
|
||
: 'border-surface-border bg-surface-50 dark:bg-surface-800 hover:bg-surface-100 dark:hover:bg-surface-700'"
|
||
@click="cfg.pagamento_modo = modo.value"
|
||
>
|
||
<div class="grid place-items-center w-9 h-9 rounded-[6px] shrink-0"
|
||
:class="cfg.pagamento_modo === modo.value ? 'bg-primary/15 text-primary' : 'bg-surface-200 dark:bg-surface-700 text-surface-400'">
|
||
<i :class="['pi', modo.icon]" />
|
||
</div>
|
||
<div class="min-w-0">
|
||
<div class="font-medium text-sm leading-none">{{ modo.label }}</div>
|
||
<div class="text-xs text-surface-400 mt-1">{{ modo.desc }}</div>
|
||
</div>
|
||
<i v-if="cfg.pagamento_modo === modo.value" class="pi pi-check-circle text-primary ml-auto shrink-0" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── PAGAR NA HORA ─────────────────────────────── -->
|
||
<template v-if="cfg.pagamento_modo === 'pagar_na_hora'">
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-1">Formas de pagamento aceitas</label>
|
||
<p class="text-xs text-surface-400 mb-3">
|
||
Selecione quais formas serão exibidas ao paciente.
|
||
Configure os dados em
|
||
<RouterLink to="/configuracoes/pagamento" class="underline">Configurações › Pagamento</RouterLink>.
|
||
</p>
|
||
|
||
<div v-if="!algumMetodoConfigurado" class="rounded-[6px] border border-orange-200 bg-orange-50 dark:bg-orange-900/20 p-3 text-sm text-orange-700 dark:text-orange-300">
|
||
<i class="pi pi-exclamation-triangle mr-1" />
|
||
Nenhuma forma de pagamento configurada ainda.
|
||
<RouterLink to="/configuracoes/pagamento" class="underline font-medium ml-1">Configurar agora</RouterLink>
|
||
</div>
|
||
|
||
<div class="flex flex-col gap-2">
|
||
<label
|
||
v-for="m in metodosDisponiveis"
|
||
:key="m.key"
|
||
class="flex items-center gap-3 p-3 rounded-[6px] border cursor-pointer transition select-none"
|
||
:class="[
|
||
!m.ativo ? 'opacity-40 cursor-not-allowed border-surface-border bg-surface-50 dark:bg-surface-800' :
|
||
isMetodoVisivel(m.key) ? 'border-primary bg-primary/5 ring-1 ring-primary/20' : 'border-surface-border bg-surface-50 dark:bg-surface-800 hover:bg-surface-100 dark:hover:bg-surface-700'
|
||
]"
|
||
>
|
||
<Checkbox
|
||
:modelValue="isMetodoVisivel(m.key)"
|
||
:disabled="!m.ativo"
|
||
binary
|
||
@change="m.ativo && toggleMetodoVisivel(m.key)"
|
||
/>
|
||
<i :class="['pi', m.icon, 'text-base']" />
|
||
<span class="font-medium text-sm flex-1">{{ m.label }}</span>
|
||
<Tag v-if="!m.ativo" value="Não configurado" severity="secondary" class="text-xs" />
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ── PIX ANTECIPADO ────────────────────────────── -->
|
||
<template v-if="cfg.pagamento_modo === 'pix_antecipado'">
|
||
<!-- Chave Pix -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-2">Chave Pix</label>
|
||
<InputText
|
||
v-model="cfg.pix_chave"
|
||
:placeholder="paymentSettings.pix_chave ? `Usando: ${paymentSettings.pix_chave}` : 'CPF, e-mail, telefone ou chave aleatória'"
|
||
class="w-full"
|
||
/>
|
||
<div v-if="!cfg.pix_chave && paymentSettings.pix_chave" class="text-xs text-surface-400 mt-1">
|
||
Deixe vazio para usar a chave de
|
||
<RouterLink to="/configuracoes/pagamento" class="underline">Formas de Pagamento</RouterLink>
|
||
({{ paymentSettings.pix_chave }}).
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Countdown -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-2">Tempo para realizar o pagamento</label>
|
||
<Select
|
||
v-model="cfg.pix_countdown_minutos"
|
||
:options="pixCountdownOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
<div class="text-xs text-surface-400 mt-1">
|
||
Se o paciente não pagar no prazo, o horário é liberado automaticamente.
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Tempo de reserva — sempre visível quando não é pix_antecipado -->
|
||
<div v-if="cfg.pagamento_modo !== 'pix_antecipado'">
|
||
<label class="block text-sm font-semibold mb-2">Tempo de reserva do horário</label>
|
||
<Select
|
||
v-model="cfg.reserva_horas"
|
||
:options="reservaOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
<div class="text-xs text-surface-400 mt-1">
|
||
Enquanto a solicitação está pendente, o horário fica bloqueado por este período.
|
||
Após expirar, volta a ficar disponível.
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="cfg.pagamento_modo === 'pix_antecipado'">
|
||
<label class="block text-sm font-semibold mb-2">Tempo de reserva do horário</label>
|
||
<Select
|
||
v-model="cfg.reserva_horas"
|
||
:options="reservaOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
<div class="text-xs text-surface-400 mt-1">
|
||
Tempo em que o horário fica bloqueado aguardando o pagamento Pix.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex justify-end">
|
||
<Button
|
||
label="Salvar pagamento"
|
||
icon="pi pi-check"
|
||
:loading="savingCard === 'pagamento'"
|
||
@click="saveCard('pagamento')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── CARD: TRIAGEM & CONFORMIDADE ───────────────────────── -->
|
||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('triagem') }">
|
||
<button
|
||
type="button"
|
||
class="agd-accordion__header"
|
||
@click="toggleCard('triagem')"
|
||
>
|
||
<div class="agd-accordion__icon bg-orange-100 dark:bg-orange-900/30 text-orange-600">
|
||
<i class="pi pi-shield" />
|
||
</div>
|
||
<div class="min-w-0 flex-1 text-left">
|
||
<div class="agd-accordion__title">Triagem & Conformidade</div>
|
||
<div v-if="!expandedCard.has('triagem')" class="agd-accordion__summary">{{ resumoTriagem }}</div>
|
||
</div>
|
||
<i class="pi agd-accordion__chevron"
|
||
:class="expandedCard.has('triagem') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||
</button>
|
||
|
||
<div v-if="expandedCard.has('triagem')" class="agd-accordion__body">
|
||
<div class="flex flex-col gap-4">
|
||
|
||
<div class="text-sm font-semibold text-surface-600">Campos extras no formulário</div>
|
||
|
||
<!-- Triagem: motivo -->
|
||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
|
||
<div>
|
||
<div class="font-medium text-sm">Motivo da consulta</div>
|
||
<div class="text-xs text-surface-400 mt-0.5">Campo de texto livre opcional para o paciente informar o motivo.</div>
|
||
</div>
|
||
<InputSwitch v-model="cfg.triagem_motivo" />
|
||
</div>
|
||
|
||
<!-- Triagem: como conheceu -->
|
||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
|
||
<div>
|
||
<div class="font-medium text-sm">Como nos conheceu?</div>
|
||
<div class="text-xs text-surface-400 mt-0.5">Pergunta de origem (indicação, redes sociais, busca…).</div>
|
||
</div>
|
||
<InputSwitch v-model="cfg.triagem_como_conheceu" />
|
||
</div>
|
||
|
||
<Divider class="my-0" />
|
||
|
||
<div class="text-sm font-semibold text-surface-600">Segurança & LGPD</div>
|
||
|
||
<!-- Verificação de email -->
|
||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
|
||
<div>
|
||
<div class="font-medium text-sm">Verificação de e-mail</div>
|
||
<div class="text-xs text-surface-400 mt-0.5">
|
||
Paciente confirma o e-mail antes de concluir o agendamento.
|
||
</div>
|
||
</div>
|
||
<InputSwitch v-model="cfg.verificacao_email" />
|
||
</div>
|
||
|
||
<!-- Aceite LGPD -->
|
||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
|
||
<div>
|
||
<div class="font-medium text-sm">Aceite obrigatório de termos (LGPD)</div>
|
||
<div class="text-xs text-surface-400 mt-0.5">
|
||
Exige que o paciente marque o aceite da política de privacidade antes de finalizar.
|
||
</div>
|
||
</div>
|
||
<InputSwitch v-model="cfg.exigir_aceite_lgpd" />
|
||
</div>
|
||
|
||
<div class="flex justify-end">
|
||
<Button
|
||
label="Salvar triagem"
|
||
icon="pi pi-check"
|
||
:loading="savingCard === 'triagem'"
|
||
@click="saveCard('triagem')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── CARD: TEXTOS DA JORNADA ────────────────────────────── -->
|
||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('textos') }">
|
||
<button
|
||
type="button"
|
||
class="agd-accordion__header"
|
||
@click="toggleCard('textos')"
|
||
>
|
||
<div class="agd-accordion__icon bg-pink-100 dark:bg-pink-900/30 text-pink-600">
|
||
<i class="pi pi-file-edit" />
|
||
</div>
|
||
<div class="min-w-0 flex-1 text-left">
|
||
<div class="agd-accordion__title">Textos da Jornada</div>
|
||
<div v-if="!expandedCard.has('textos')" class="agd-accordion__summary">{{ resumoTextos }}</div>
|
||
</div>
|
||
<i class="pi agd-accordion__chevron"
|
||
:class="expandedCard.has('textos') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||
</button>
|
||
|
||
<div v-if="expandedCard.has('textos')" class="agd-accordion__body">
|
||
<div class="flex flex-col gap-5">
|
||
|
||
<!-- Mensagem de boas-vindas -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-1">Mensagem de boas-vindas</label>
|
||
<div class="text-xs text-surface-400 mb-2">
|
||
Aparece logo após o paciente informar o nome. Ex: "É um prazer tê-la aqui. Em instantes, vamos agendar sua entrevista."
|
||
</div>
|
||
<Textarea
|
||
v-model="cfg.mensagem_boas_vindas"
|
||
rows="3"
|
||
placeholder="Olá! É um prazer ter você aqui. Vamos juntos agendar o seu horário."
|
||
class="w-full"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Como se preparar -->
|
||
<div>
|
||
<label class="block text-sm font-semibold mb-1">Como se preparar para a sessão</label>
|
||
<div class="text-xs text-surface-400 mb-2">
|
||
Exibido na tela de confirmação após o agendamento. Dicas, instruções de local, etc.
|
||
</div>
|
||
<Textarea
|
||
v-model="cfg.texto_como_se_preparar"
|
||
rows="5"
|
||
placeholder="Procure chegar com pelo menos 10 minutos de antecedência..."
|
||
class="w-full"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Termos LGPD -->
|
||
<div v-if="cfg.exigir_aceite_lgpd">
|
||
<label class="block text-sm font-semibold mb-1">Texto dos termos de uso / política de privacidade</label>
|
||
<div class="text-xs text-surface-400 mb-2">
|
||
Aparece no checkbox de aceite obrigatório. Pode incluir link para documento completo.
|
||
</div>
|
||
<Textarea
|
||
v-model="cfg.texto_termos_lgpd"
|
||
rows="4"
|
||
placeholder="Li e concordo com a política de privacidade e tratamento de dados pessoais conforme a LGPD."
|
||
class="w-full"
|
||
/>
|
||
</div>
|
||
|
||
<div class="flex justify-end">
|
||
<Button
|
||
label="Salvar textos"
|
||
icon="pi pi-check"
|
||
:loading="savingCard === 'textos'"
|
||
@click="saveCard('textos')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* ── Upload zone ──────────────────────────────────── */
|
||
.agd-upload-zone {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.625rem;
|
||
padding: 0.75rem 1rem;
|
||
border: 1.5px dashed var(--surface-border);
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: border-color 0.15s, background 0.15s;
|
||
background: var(--surface-ground);
|
||
}
|
||
.agd-upload-zone:hover {
|
||
border-color: var(--primary-color, #6366f1);
|
||
background: color-mix(in srgb, var(--primary-color, #6366f1) 5%, transparent);
|
||
}
|
||
|
||
/* ── Subheader degradê ────────────────────────────── */
|
||
.cfg-subheader {
|
||
display: flex; align-items: center; gap: 0.65rem;
|
||
padding: 0.875rem 1rem; border-radius: 6px;
|
||
border: 1px solid color-mix(in srgb, var(--primary-color, #6366f1) 30%, transparent);
|
||
background: linear-gradient(135deg,
|
||
color-mix(in srgb, var(--primary-color, #6366f1) 12%, var(--surface-card)) 0%,
|
||
color-mix(in srgb, var(--primary-color, #6366f1) 4%, var(--surface-card)) 60%,
|
||
var(--surface-card) 100%);
|
||
position: relative; overflow: hidden;
|
||
}
|
||
.cfg-subheader::before {
|
||
content: ''; position: absolute;
|
||
top: -20px; right: -20px;
|
||
width: 80px; height: 80px; border-radius: 50%;
|
||
background: color-mix(in srgb, var(--primary-color, #6366f1) 15%, transparent);
|
||
filter: blur(20px); pointer-events: none;
|
||
}
|
||
.cfg-subheader__icon {
|
||
display: grid; place-items: center;
|
||
width: 2rem; height: 2rem; border-radius: 6px; flex-shrink: 0;
|
||
background: color-mix(in srgb, var(--primary-color, #6366f1) 20%, transparent);
|
||
color: var(--primary-color, #6366f1); font-size: 0.85rem;
|
||
}
|
||
.cfg-subheader__title { font-size: 0.95rem; font-weight: 700; letter-spacing: -0.01em; color: var(--primary-color, #6366f1); }
|
||
.cfg-subheader__sub { font-size: 0.75rem; color: var(--text-color-secondary); opacity: 0.85; }
|
||
.cfg-subheader__actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; flex-shrink: 0; position: relative; z-index: 1; }
|
||
|
||
/* ── Card status (sem accordion) ─────────────────── */
|
||
.agd-card {
|
||
border: 1px solid var(--surface-border);
|
||
border-radius: 6px;
|
||
background: var(--surface-card);
|
||
padding: 1rem;
|
||
}
|
||
|
||
/* ── Accordion cards ──────────────────────────────── */
|
||
.agd-accordion {
|
||
border: 1px solid var(--surface-border);
|
||
border-radius: 6px;
|
||
background: var(--surface-card);
|
||
overflow: hidden;
|
||
transition: border-color 0.15s;
|
||
}
|
||
.agd-accordion--open {
|
||
border-color: color-mix(in srgb, var(--primary-color, #6366f1) 35%, transparent);
|
||
}
|
||
|
||
.agd-accordion__header {
|
||
display: flex; align-items: center; gap: 0.75rem;
|
||
width: 100%; padding: 0.875rem 1rem;
|
||
background: transparent; border: none; cursor: pointer;
|
||
transition: background 0.12s;
|
||
text-align: left;
|
||
}
|
||
.agd-accordion__header:hover { background: var(--surface-hover); }
|
||
.agd-accordion--open .agd-accordion__header {
|
||
background: color-mix(in srgb, var(--primary-color, #6366f1) 4%, var(--surface-card));
|
||
border-bottom: 1px solid var(--surface-border);
|
||
}
|
||
|
||
.agd-accordion__icon {
|
||
display: grid; place-items: center;
|
||
width: 2rem; height: 2rem; border-radius: 6px; flex-shrink: 0;
|
||
}
|
||
.agd-accordion__title {
|
||
font-size: 0.88rem; font-weight: 700;
|
||
color: var(--text-color);
|
||
}
|
||
.agd-accordion--open .agd-accordion__title {
|
||
color: var(--primary-color, #6366f1);
|
||
}
|
||
.agd-accordion__summary {
|
||
font-size: 0.72rem; color: var(--text-color-secondary);
|
||
opacity: 0.75; margin-top: 1px;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.agd-accordion__chevron {
|
||
font-size: 0.7rem; color: var(--text-color-secondary);
|
||
opacity: 0.5; flex-shrink: 0;
|
||
transition: transform 0.2s;
|
||
}
|
||
.agd-accordion--open .agd-accordion__chevron {
|
||
color: var(--primary-color, #6366f1); opacity: 0.8;
|
||
}
|
||
|
||
.agd-accordion__body {
|
||
padding: 1rem;
|
||
display: flex; flex-direction: column; gap: 1.25rem;
|
||
}
|
||
</style> |