Files
agenciapsilmno/src/layout/configuracoes/ConfiguracoesAgendadorPage.vue
2026-03-17 21:08:14 -03:00

1362 lines
58 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 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 ( 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>