Files
agenciapsilmno/src/layout/configuracoes/ConfiguracoesAgendadorPage.vue
T
Leonardo 9966b5f175 Melissa: paginas nativas cfg-* + temas + textos com fundo + drawer WA
CHROME COMPARTILHADO + 18 PAGINAS NATIVAS
- MelissaConfigPage: chrome unico (header, drawer mobile, sidebar com Configuracoes
  + FAQ slot, main com Suspense). Replica fake-dialog right rule e fica flush
  com o config-aside global.
- 18 wrappers finos (~25 linhas cada): cfg-precificacao, cfg-descontos,
  cfg-excecoes, cfg-convenios, cfg-wa, cfg-wa-pessoal, cfg-wa-oficial,
  cfg-wa-templates, cfg-conversas-tags/autoreply/optouts/sla/bots,
  cfg-lembretes, cfg-creditos-wa, cfg-sms, cfg-email-templates,
  cfg-recursos-extras, cfg-recursos-extras-extrato, cfg-auditoria.
- Cada wrapper usa defineAsyncComponent + Suspense pra evitar race com
  tenantStore no boot (loading travado em alguns chooser-style pages).
- MelissaLayout: imports + SECOES + MELISSA_NON_CONFIG_SLUGS + render
  conditions atualizados pra cobrir os 18 slugs.

PAGINAS LEGADAS DETECTAM CONTEXTO MELISSA
- ConfiguracoesWhatsappChooserPage, WhatsappPage, TwilioWhatsappPage,
  SmsPage, RecursosExtrasPage, EmailTemplatesPage, AddonsExtratoPage,
  AgendadorPage, ConversasAutoreplyPage: route.startsWith('/melissa')
  decide se router.push vai pro slug Melissa ou /configuracoes legado.
- Anchors <a href="/configuracoes/..."> (que recarregavam pagina e
  vazavam o usuario do Melissa) trocados por RouterLink context-aware.
- MelissaAgenda.goSettings agora vai pra /melissa/agenda-config.

PERSONALIZAR > TEMAS
- melissaThemes.js: catalogo Freud/Klein/Jung (wallpaper + cor primaria
  + preset Lara/Nora + surface).
- Toggle de tema aplica tudo de uma vez; persistido em melissa_prefs.themeName.
- Boot resolve themeName -> imagem via fetch + data URL (sem guardar
  data URL gigante no DB).
- onCustomFileChange/onClearBg invalidam themeName quando user mexe no bg.

PERSONALIZAR > FUNDO NOS TEXTOS
- Pref textBgEnabled em melissa_prefs.
- MelissaHeroClock: prop textBg envolve relogio/data/saudacao/resumo
  em <span class="hero-text"> que ganha bg branco/preto 60% + borda
  + padding + radius quando o toggle esta on.
- Vars --m-hero-text-bg / --m-hero-text-border flipam com light/dark.

TOP + DOCK COM GRADIENT HORIZONTAL
- Var --m-band: preto 80% (dark) / branco 80% (light).
- .melissa-topbar-band: gradiente cor->transparente (right->left) atras
  dos botoes do topo.
- .melissa-dock: gradiente cor->transparente (left->right) atras dos pins.

MELISSANEGOCIO ABSORVE MINHA EMPRESA
- Adiciona logo upload + preview "cartao de visita" (computeds
  enderecoLinhas/redesValidas/temDados/logoDisplay + redeIcon helper).
- Normaliza dados legados do cfg-empresa: redes_sociais.{rede} virou {name}.
- Preview teleporta entre 3 destinos baseado no viewport:
  mobile -> drawer; mid-desktop -> sidebar; wide-desktop (>=1340px) ->
  painel flutuante FORA do fake dialog (ancora no right edge + 14px gap,
  altura segue conteudo, header alinhado com header do dialog).
- Remove cfg-empresa de melissaConfigGrupos.js + COMPONENT_MAP do
  MelissaConfiguracoes; grupo "Empresa & Plataforma" -> "Plataforma".

CRONOMETRO -> SESSAO AGENDADA
- MelissaCronometro emite session-end ao parar com paciente selecionado
  (threshold 5s pra ignorar start/stop acidental).
- MelissaLayout.onCronometroSessionEnd busca agenda_eventos do paciente
  no dia (tipo='sessao'), pega o mais recente e grava em
  extra_fields.cronometro_duracao_seg + cronometro_parado_em.
- Toast: sucesso ("X min salvos") ou warn ("sessao nao encontrada").

CONVERSATIONDRAWER WHATSAPP-LIKE
- Nova imagem whatsapp-bg.jpg (renomeada de hash random) usada como
  tile (380px) no .cd-msgs.
- Light: bege #efeae2 + multiply blend.
- Dark: #0b141a + camada 78% sobre o doodle.
- Bubbles WA-style (verde out / branco-dark in com tails) ja existiam.

EXTRATO RECURSOS EXTRAS
- Filtros 2-por-linha em Melissa (vs 1/4 no /configuracoes).
- Cards de Resumo teleportam pro #cfg-page-side em Melissa
  (1-col empilhado no drawer; 4-col inline no /configuracoes).
- Botoes de exportar com flex-1 distribuidos em uma unica linha em
  desktop, wrap no mobile.
- DataTable scrollable em ambos os layouts.

OUTROS AJUSTES MENORES
- Cfg-conversas-autoreply: dias semana 4-cols em Melissa (vs 7-cols
  no /configuracoes).
- Cfg-creditos-wa: 1/2 por linha (vs 1/2/4) em Melissa.
- Cfg-recursos-extras: pacotes 1/2 (vs 1/2/4); "Em breve" 1-col.
- WhatsAppPage aba Templates: guia de formatacao teleporta pro side
  drawer em Melissa, deixando textareas full-width.
- ConfigPage chrome agora tem #cfg-page-actions target pros Teleport
  de acoes (refresh button etc).
- Imagens renomeadas em src/assets/themes/ (freudwebp/melainewebp/
  jungwebp.webp) e src/assets/whatsapp-bg.jpg.
- JoditTextEditor.vue novo (wrapper Jodit generico, sem features de email).
- MelissaConfigList.vue novo (lista compartilhada de configs pro drawer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:48:18 -03:00

1212 lines
65 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.
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/ConfiguracoesAgendadorPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted } from 'vue';
import { RouterLink, useRoute } from 'vue-router';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
import { useEntitlementsStore } from '@/stores/entitlementsStore';
import { useToast } from 'primevue/usetoast';
import AgendadorPreview from '@/components/agendador/AgendadorPreview.vue';
import Editor from 'primevue/editor';
const route = useRoute();
const toast = useToast();
const tenantStore = useTenantStore();
const entitlements = useEntitlementsStore();
// Em /melissa o "Pagamento" vive em /melissa/pagamento. Preserva o layout.
const inMelissa = computed(() => String(route.path || '').startsWith('/melissa'));
const pagamentoPath = computed(() => (inMelissa.value ? '/melissa/pagamento' : '/configuracoes/pagamento'));
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;
// Persiste imediatamente no banco sem fechar o accordion
const uid = ownerId.value;
const tenantId = await getActiveTenantId(uid);
await supabase.from('agendador_configuracoes').upsert({ owner_id: uid, tenant_id: tenantId, ...buildPayload('identidade'), updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
toast.add({ severity: 'success', summary: 'Imagem salva', 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">
<!-- SKELETON -->
<template v-if="loading">
<div class="flex flex-col xl:flex-row gap-4">
<!-- Coluna esquerda: cards skeleton -->
<div class="flex flex-col gap-4 min-w-0 xl:flex-1">
<!-- Subheader skeleton -->
<div class="cfg-subheader">
<AppLoadingPhrases class="w-full" />
</div>
<!-- Card status skeleton -->
<div class="agd-card flex flex-col gap-4">
<div class="flex items-center gap-3">
<Skeleton width="44px" height="44px" border-radius="6px" />
<div class="flex flex-col gap-2 flex-1">
<Skeleton width="180px" height="16px" />
<Skeleton width="120px" height="12px" />
</div>
<Skeleton width="48px" height="24px" border-radius="12px" />
</div>
<Skeleton height="1px" />
<Skeleton width="260px" height="36px" border-radius="6px" />
</div>
<!-- Section skeletons -->
<div v-for="n in 5" :key="n" class="agd-card flex flex-col gap-3 p-0 overflow-hidden">
<div class="flex items-center gap-3 px-4 py-3 bg-surface-50 dark:bg-surface-800 border-b border-surface-border">
<Skeleton width="32px" height="32px" border-radius="6px" />
<Skeleton :width="n % 2 === 0 ? '160px' : '140px'" height="14px" />
</div>
<div class="px-4 pb-4 flex flex-col gap-2">
<Skeleton :width="n % 3 === 0 ? '90%' : '80%'" height="11px" />
<Skeleton width="60%" height="11px" />
</div>
</div>
</div>
<!-- Coluna direita: phone frame skeleton -->
<div class="xl:w-[280px] xl:self-start xl:sticky xl:top-4">
<Skeleton width="260px" height="500px" border-radius="2.5rem" class="mx-auto" />
</div>
</div>
</template>
<template v-else>
<!-- 2 COLUNAS: seções (esq) + preview (dir) -->
<div class="flex flex-col xl:flex-row gap-4 items-start">
<!-- Coluna esquerda: todos os cards -->
<div class="flex flex-col gap-4 min-w-0 xl:flex-1">
<!-- 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>
<ToggleSwitch :modelValue="cfg.ativo" @update:modelValue="toggleAtivo" />
</template>
<template v-else>
<Tag value="Plano não inclui" severity="warn" class="text-xs" />
<ToggleSwitch :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>
<!-- SEÇÃO: IDENTIDADE VISUAL -->
<div class="agd-section">
<div class="agd-section__head">
<div class="agd-section__icon bg-purple-100 dark:bg-purple-900/30 text-purple-600">
<i class="pi pi-palette" />
</div>
<span class="agd-section__title">Identidade Visual</span>
</div>
<div class="agd-section__body">
<!-- Aparência -->
<div class="agd-group-title">Aparência</div>
<div class="px-1 grid grid-cols-2 gap-4">
<div class="flex flex-col gap-1">
<label class="cfg-label">Nome de exibição</label>
<InputText v-model="cfg.nome_exibicao" placeholder="Ex: Dra. Ana Silva — Psicóloga" class="w-full" />
<span class="cfg-hint">Como aparece no topo do agendador.</span>
</div>
<div class="flex flex-col gap-1">
<label class="cfg-label">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>
<span class="cfg-hint">Botões e destaques do agendador.</span>
</div>
</div>
<!-- Imagens -->
<Divider class="my-0" />
<div class="agd-group-title">Imagens</div>
<div class="px-1 grid grid-cols-1 md:grid-cols-3 gap-3">
<!-- Logomarca -->
<div class="cfg-card">
<div class="cfg-card__head">
<i class="pi pi-user-circle text-xs opacity-50" />
<span>Logomarca</span>
</div>
<div class="p-3 flex flex-col gap-2">
<div class="cfg-hint opacity-60">Avatar circular 300×300 px</div>
<div v-if="cfg.logomarca_url" class="flex justify-center">
<img :src="cfg.logomarca_url" alt="Logomarca" class="w-16 h-16 rounded-full object-cover border-2 border-surface-200" />
</div>
<div class="agd-upload-zone flex-col text-center" @click="fileInputLogo.click()">
<i class="pi pi-upload text-surface-400" />
<span>{{ uploadingField === 'logomarca' ? 'Enviando...' : 'Clique para enviar' }}</span>
<input ref="fileInputLogo" type="file" accept="image/*" class="hidden" @change="(e) => onFileSelected(e, 'logomarca')" />
</div>
<InputText v-model="cfg.logomarca_url" placeholder="ou cole uma URL..." class="w-full" />
</div>
</div>
<!-- Imagem do header -->
<div class="cfg-card">
<div class="cfg-card__head">
<i class="pi pi-image text-xs opacity-50" />
<span>Header</span>
</div>
<div class="p-3 flex flex-col gap-2">
<div class="cfg-hint opacity-60">Faixa superior 1400×300 px</div>
<div v-if="cfg.imagem_header_url" class="rounded-[6px] overflow-hidden h-16 w-full">
<img :src="cfg.imagem_header_url" alt="Header" class="w-full h-full object-cover" />
</div>
<div class="agd-upload-zone flex-col text-center" @click="fileInputHeader.click()">
<i class="pi pi-upload text-surface-400" />
<span>{{ uploadingField === 'header' ? 'Enviando...' : 'Clique para enviar' }}</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..." class="w-full" />
</div>
</div>
<!-- Imagem de fundo -->
<div class="cfg-card">
<div class="cfg-card__head">
<i class="pi pi-stop text-xs opacity-50" />
<span>Fundo</span>
</div>
<div class="p-3 flex flex-col gap-2">
<div class="cfg-hint opacity-60">Fundo da página 1920×1080 px</div>
<div v-if="cfg.imagem_fundo_url" class="rounded-[6px] overflow-hidden h-16 w-full">
<img :src="cfg.imagem_fundo_url" alt="Fundo" class="w-full h-full object-cover" />
</div>
<div class="agd-upload-zone flex-col text-center" @click="fileInputFundo.click()">
<i class="pi pi-upload text-surface-400" />
<span>{{ uploadingField === 'fundo' ? 'Enviando...' : 'Clique para enviar' }}</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..." class="w-full" />
</div>
</div>
</div>
<div class="flex justify-end">
<Button label="Salvar identidade visual" icon="pi pi-check" :loading="savingCard === 'identidade'" @click="saveCard('identidade')" />
</div>
</div>
</div>
<!-- SEÇÃO: PERFIL PÚBLICO -->
<div class="agd-section">
<div class="agd-section__head">
<div class="agd-section__icon bg-blue-100 dark:bg-blue-900/30 text-blue-600">
<i class="pi pi-map-marker" />
</div>
<span class="agd-section__title">Perfil Público</span>
</div>
<div class="agd-section__body">
<!-- Localização -->
<div class="agd-group-title">Localização</div>
<div class="px-1 flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label class="cfg-label">Endereço</label>
<InputText v-model="cfg.endereco" placeholder="Rua das Flores, 123, Centro — São Paulo, SP" class="w-full" />
</div>
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
<div>
<div class="font-semibold">Botão "Como chegar"</div>
<div class="text-surface-400 mt-0.5">Exibe um botão que abre o mapa para o paciente.</div>
</div>
<ToggleSwitch v-model="cfg.botao_como_chegar_ativo" />
</div>
<div v-if="cfg.botao_como_chegar_ativo" class="flex flex-col gap-1">
<label class="cfg-label">URL do Google Maps <span class="font-normal opacity-60">(opcional)</span></label>
<InputText v-model="cfg.maps_url" placeholder="https://maps.google.com/..." class="w-full" />
<span class="cfg-hint">Se vazio, abre uma busca pelo endereço acima.</span>
</div>
</div>
<div class="flex justify-end">
<Button label="Salvar perfil" icon="pi pi-check" :loading="savingCard === 'perfil'" @click="saveCard('perfil')" />
</div>
</div>
</div>
<!-- SEÇÃO: FLUXO DE AGENDAMENTO -->
<div class="agd-section">
<div class="agd-section__head">
<div class="agd-section__icon bg-cyan-100 dark:bg-cyan-900/30 text-cyan-600">
<i class="pi pi-sitemap" />
</div>
<span class="agd-section__title">Fluxo de Agendamento</span>
</div>
<div class="agd-section__body">
<!-- Aprovação -->
<div class="agd-group-title">Aprovação</div>
<div class="px-1 flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label class="cfg-label">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">{{ opt.label }}</div>
<div class="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>
<div v-if="cfg.modo_aprovacao === 'aprovacao'" class="flex flex-col gap-1">
<label class="cfg-label">Prazo para responder a solicitação</label>
<Select v-model="cfg.prazo_resposta_horas" :options="prazoRespostaOptions" optionLabel="label" optionValue="value" class="w-full" />
<span class="cfg-hint">Se não responder no prazo, o paciente é notificado e o horário é liberado.</span>
</div>
</div>
<Divider class="my-0" />
<!-- Atendimento -->
<div class="agd-group-title">Atendimento</div>
<div class="px-1 flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label class="cfg-label">Modalidade</label>
<SelectButton v-model="cfg.modalidade" :options="modalidadeOptions" optionLabel="label" optionValue="value" class="w-full" />
</div>
<div class="flex flex-col gap-1">
<label class="cfg-label">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 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" v-if="cfg.tipos_habilitados?.includes(opt.value)" />
{{ opt.label }}
</div>
</div>
</div>
</div>
<Divider class="my-0" />
<!-- Horários -->
<div class="agd-group-title">Horários</div>
<div class="px-1 grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex flex-col gap-1">
<label class="cfg-label">Duração da sessão</label>
<Select v-model="cfg.duracao_sessao_min" :options="duracoesOptions" optionLabel="label" optionValue="value" class="w-full" />
<span class="cfg-hint">Usado para bloquear o horário correto na agenda após aprovação.</span>
</div>
<div class="flex flex-col gap-1">
<label class="cfg-label">Antecedência mínima para agendar</label>
<Select v-model="cfg.antecedencia_minima_horas" :options="antecedenciaOptions" optionLabel="label" optionValue="value" class="w-full" />
<span class="cfg-hint">Pacientes não conseguem agendar com menos de X horas de antecedência.</span>
</div>
</div>
<div class="flex justify-end">
<Button label="Salvar fluxo" icon="pi pi-check" :loading="savingCard === 'fluxo'" @click="saveCard('fluxo')" />
</div>
</div>
</div>
<!-- SEÇÃO: PAGAMENTO -->
<div class="agd-section">
<div class="agd-section__head">
<div class="agd-section__icon bg-green-100 dark:bg-green-900/30 text-green-600">
<i class="pi pi-credit-card" />
</div>
<span class="agd-section__title">Pagamento</span>
</div>
<div class="agd-section__body">
<!-- Como o paciente vai pagar -->
<div class="agd-group-title">Como o paciente vai pagar?</div>
<div class="px-1 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-semibold">{{ modo.label }}</div>
<div class="text-surface-400">{{ 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>
<!-- Pagar na hora (condicional) -->
<template v-if="cfg.pagamento_modo === 'pagar_na_hora'">
<Divider class="my-0" />
<div class="agd-group-title">Formas de pagamento aceitas</div>
<div class="px-1 flex flex-col gap-3">
<p class="text-surface-400 m-0">Selecione quais formas serão exibidas ao paciente. Configure os dados em <RouterLink :to="pagamentoPath" class="underline">{{ inMelissa ? 'Pagamento' : '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-orange-700 dark:text-orange-300">
<i class="pi pi-exclamation-triangle mr-1" />
Nenhuma forma de pagamento configurada ainda.
<RouterLink :to="pagamentoPath" 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]" />
<span class="font-medium flex-1">{{ m.label }}</span>
<Tag v-if="!m.ativo" value="Não configurado" severity="secondary" />
</label>
</div>
</div>
</template>
<!-- Pix antecipado (condicional) -->
<template v-if="cfg.pagamento_modo === 'pix_antecipado'">
<Divider class="my-0" />
<div class="agd-group-title">Configurações do Pix</div>
<div class="px-1 grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex flex-col gap-1">
<label class="cfg-label">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" />
<span v-if="!cfg.pix_chave && paymentSettings.pix_chave" class="cfg-hint">
Deixe vazio para usar a chave de
<RouterLink :to="pagamentoPath" class="underline">Formas de Pagamento</RouterLink>
({{ paymentSettings.pix_chave }}).
</span>
</div>
<div class="flex flex-col gap-1">
<label class="cfg-label">Tempo para realizar o pagamento</label>
<Select v-model="cfg.pix_countdown_minutos" :options="pixCountdownOptions" optionLabel="label" optionValue="value" class="w-full" />
<span class="cfg-hint">Se o paciente não pagar no prazo, o horário é liberado automaticamente.</span>
</div>
</div>
</template>
<Divider class="my-0" />
<!-- Reserva do horário -->
<div class="agd-group-title">Reserva do horário</div>
<div class="px-1 flex flex-col gap-1">
<label class="cfg-label">Tempo de reserva</label>
<Select v-model="cfg.reserva_horas" :options="reservaOptions" optionLabel="label" optionValue="value" class="w-full" />
<span class="cfg-hint">
<template v-if="cfg.pagamento_modo === 'pix_antecipado'"> Tempo em que o horário fica bloqueado aguardando o pagamento Pix. </template>
<template v-else> Enquanto a solicitação está pendente, o horário fica bloqueado por este período. Após expirar, volta a ficar disponível. </template>
</span>
</div>
<div class="flex justify-end">
<Button label="Salvar pagamento" icon="pi pi-check" :loading="savingCard === 'pagamento'" @click="saveCard('pagamento')" />
</div>
</div>
</div>
<!-- SEÇÃO: TRIAGEM & CONFORMIDADE -->
<div class="agd-section">
<div class="agd-section__head">
<div class="agd-section__icon bg-orange-100 dark:bg-orange-900/30 text-orange-600">
<i class="pi pi-shield" />
</div>
<span class="agd-section__title">Triagem & Conformidade</span>
</div>
<div class="agd-section__body">
<!-- Campos extras no formulário -->
<div class="agd-group-title">Campos extras no formulário</div>
<div class="px-1 flex flex-col gap-2">
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
<div>
<div class="font-semibold">Motivo da consulta</div>
<div class="text-surface-400 mt-0.5">Campo de texto livre opcional para o paciente informar o motivo.</div>
</div>
<ToggleSwitch v-model="cfg.triagem_motivo" />
</div>
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
<div>
<div class="font-semibold">Como nos conheceu?</div>
<div class="text-surface-400 mt-0.5">Pergunta de origem (indicação, redes sociais, busca).</div>
</div>
<ToggleSwitch v-model="cfg.triagem_como_conheceu" />
</div>
</div>
<Divider class="my-0" />
<!-- Segurança & LGPD -->
<div class="agd-group-title">Segurança & LGPD</div>
<div class="px-1 flex flex-col gap-2">
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
<div>
<div class="font-semibold">Verificação de e-mail</div>
<div class="text-surface-400 mt-0.5">Paciente confirma o e-mail antes de concluir o agendamento.</div>
</div>
<ToggleSwitch v-model="cfg.verificacao_email" />
</div>
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
<div>
<div class="font-semibold">Aceite obrigatório de termos (LGPD)</div>
<div class="text-surface-400 mt-0.5">Exige que o paciente marque o aceite da política de privacidade antes de finalizar.</div>
</div>
<ToggleSwitch v-model="cfg.exigir_aceite_lgpd" />
</div>
</div>
<div class="flex justify-end">
<Button label="Salvar triagem" icon="pi pi-check" :loading="savingCard === 'triagem'" @click="saveCard('triagem')" />
</div>
</div>
</div>
<!-- SEÇÃO: TEXTOS DA JORNADA -->
<div class="agd-section">
<div class="agd-section__head">
<div class="agd-section__icon bg-pink-100 dark:bg-pink-900/30 text-pink-600">
<i class="pi pi-file-edit" />
</div>
<span class="agd-section__title">Textos da Jornada</span>
</div>
<div class="agd-section__body">
<!-- Mensagens ao paciente -->
<div class="agd-group-title">Mensagens ao paciente</div>
<div class="px-1 flex flex-col gap-5">
<div class="flex flex-col gap-1">
<label class="cfg-label">Mensagem de boas-vindas</label>
<span class="cfg-hint">Aparece logo após o paciente informar o nome.</span>
<Editor v-model="cfg.mensagem_boas_vindas" style="margin-top: 0.25rem" editorStyle="min-height: 80px; font-size: 1rem;">
<template #toolbar>
<span class="ql-formats"> <button class="ql-bold" /><button class="ql-italic" /><button class="ql-underline" /> </span>
<span class="ql-formats"> <button class="ql-list" value="ordered" /><button class="ql-list" value="bullet" /> </span>
<span class="ql-formats">
<button class="ql-link" />
</span>
</template>
</Editor>
</div>
<div class="flex flex-col gap-1">
<label class="cfg-label">Como se preparar para a sessão</label>
<span class="cfg-hint">Exibido na tela de confirmação após o agendamento. Dicas, instruções de local, etc.</span>
<Editor v-model="cfg.texto_como_se_preparar" style="margin-top: 0.25rem" editorStyle="min-height: 120px; font-size: 1rem;">
<template #toolbar>
<span class="ql-formats"> <button class="ql-bold" /><button class="ql-italic" /><button class="ql-underline" /> </span>
<span class="ql-formats"> <button class="ql-list" value="ordered" /><button class="ql-list" value="bullet" /> </span>
<span class="ql-formats">
<button class="ql-link" />
</span>
</template>
</Editor>
</div>
</div>
<!-- Termos (condicional) -->
<template v-if="cfg.exigir_aceite_lgpd">
<Divider class="my-0" />
<div class="agd-group-title">Termos de uso / Política de privacidade</div>
<div class="px-1 flex flex-col gap-1">
<span class="cfg-hint">Aparece no checkbox de aceite obrigatório. Pode incluir link para documento completo.</span>
<Editor v-model="cfg.texto_termos_lgpd" style="margin-top: 0.25rem" editorStyle="min-height: 120px; font-size: 1rem;">
<template #toolbar>
<span class="ql-formats"> <button class="ql-bold" /><button class="ql-italic" /><button class="ql-underline" /> </span>
<span class="ql-formats"> <button class="ql-list" value="ordered" /><button class="ql-list" value="bullet" /> </span>
<span class="ql-formats">
<button class="ql-link" />
</span>
</template>
</Editor>
</div>
</template>
<div class="flex justify-end">
<Button label="Salvar textos" icon="pi pi-check" :loading="savingCard === 'textos'" @click="saveCard('textos')" />
</div>
</div>
</div>
<!-- /textos -->
<!-- LoadedPhraseBlock abaixo do último card -->
<LoadedPhraseBlock />
</div>
<!-- /coluna esquerda -->
<!-- Coluna direita: preview do agendador -->
<div class="xl:w-[280px] xl:self-start xl:sticky xl:top-4">
<AgendadorPreview :cfg="cfg" />
</div>
</div>
<!-- /2 colunas -->
</template>
</div>
</template>
<style scoped>
/* ── Upload zone ──────────────────────────────────── */
.agd-upload-zone {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
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);
}
/* ── Card status (sem accordion) ─────────────────── */
.agd-card {
border: 1px solid var(--surface-border);
border-radius: 6px;
background: var(--surface-card);
padding: 1rem;
}
/* ── Seções sempre abertas (estilo Minha Empresa) ──── */
.agd-section {
border: 1px solid var(--surface-border);
border-radius: 8px;
background: var(--surface-card);
overflow: hidden;
}
.agd-section__head {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--surface-border);
background: var(--surface-ground);
}
.agd-section__icon {
display: grid;
place-items: center;
width: 2rem;
height: 2rem;
border-radius: 6px;
flex-shrink: 0;
}
.agd-section__title {
font-size: 1rem;
font-weight: 700;
color: var(--text-color);
}
.agd-section__body {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* ── Sub-seção cards (mesmo padrão que MinhaEmpresa) ─── */
.cfg-card {
border: 1px solid var(--surface-border);
border-radius: 8px;
background: var(--surface-card);
overflow: hidden;
}
.cfg-card__head {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 1rem;
border-bottom: 1px solid var(--surface-border);
background: var(--surface-ground);
font-size: 0.8rem;
font-weight: 700;
}
.cfg-label {
font-size: 1rem;
font-weight: 600;
color: var(--text-color-secondary);
}
.cfg-hint {
font-size: 1rem;
color: var(--text-color-secondary);
opacity: 0.8;
}
.agd-group-title {
font-size: 1rem;
font-weight: 700;
color: var(--text-color-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
opacity: 0.65;
}
</style>