-
- Sua agenda está livre hoje.
-
-
- Hoje há
-
- , e
- .
-
+
+
+
+ Sua agenda está livre hoje.
+
+
+ Hoje há
+
+ , e
+ .
+
+
@@ -128,4 +134,28 @@ const emit = defineEmits(['cronometro', 'toggle-filtro']);
font-weight: 500;
border-bottom: 1px solid var(--m-text);
}
+
+/* ─── Modo "fundo nos textos soltos" ──────────────────────────
+ Toggle no painel Personalizar. Cada bloco de texto (.hero-text)
+ ganha um fundo solido translucido + borda + padding pra ficar
+ legivel sobre wallpapers com pouca transparencia. Cor flipa
+ com light/dark via --m-hero-text-bg / --m-hero-text-border. */
+.has-text-bg .hero-text {
+ display: inline-block;
+ background: var(--m-hero-text-bg, rgba(0, 0, 0, 0.6));
+ border: 1px solid var(--m-hero-text-border, rgba(255, 255, 255, 0.12));
+ border-radius: 12px;
+ padding: 4px 14px;
+ backdrop-filter: blur(6px);
+ -webkit-backdrop-filter: blur(6px);
+}
+/* Relogio: padding maior porque o texto e' gigante */
+.has-text-bg .clock-display .hero-text {
+ padding: 6px 22px;
+ border-radius: 18px;
+}
+/* Linhas inferiores (data, saudacao, resumo) ganham margem extra
+ pra o fundo nao colar visualmente no de cima. */
+.has-text-bg .hero-line { margin-top: 12px; }
+.has-text-bg .hero-line:first-of-type { margin-top: 8px; }
diff --git a/src/layout/melissa/MelissaLayout.vue b/src/layout/melissa/MelissaLayout.vue
index 59108bb..f22826e 100644
--- a/src/layout/melissa/MelissaLayout.vue
+++ b/src/layout/melissa/MelissaLayout.vue
@@ -20,6 +20,7 @@ import { useRouter, useRoute } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { useLayout } from '@/layout/composables/layout';
import { applyThemeEngine, surfaces as THEME_SURFACES, presetOptions as THEME_PRESETS } from '@/theme/theme.options';
+import { MELISSA_THEME_NAMES, findMelissaTheme } from './melissaThemes';
import { useUserSettingsPersistence } from '@/composables/useUserSettingsPersistence';
import MelissaCronometro from './MelissaCronometro.vue';
import MelissaCard from './MelissaCard.vue';
@@ -46,6 +47,26 @@ import MelissaBloqueios from './MelissaBloqueios.vue';
import MelissaAgendador from './MelissaAgendador.vue';
import MelissaAgendaConfig from './MelissaAgendaConfig.vue';
import MelissaPagamento from './MelissaPagamento.vue';
+import MelissaPrecificacao from './MelissaPrecificacao.vue';
+import MelissaDescontos from './MelissaDescontos.vue';
+import MelissaExcecoes from './MelissaExcecoes.vue';
+import MelissaConvenios from './MelissaConvenios.vue';
+import MelissaCfgWa from './MelissaCfgWa.vue';
+import MelissaCfgWaPessoal from './MelissaCfgWaPessoal.vue';
+import MelissaCfgWaOficial from './MelissaCfgWaOficial.vue';
+import MelissaCfgWaTemplates from './MelissaCfgWaTemplates.vue';
+import MelissaCfgConversasTags from './MelissaCfgConversasTags.vue';
+import MelissaCfgConversasAutoreply from './MelissaCfgConversasAutoreply.vue';
+import MelissaCfgConversasOptouts from './MelissaCfgConversasOptouts.vue';
+import MelissaCfgConversasSla from './MelissaCfgConversasSla.vue';
+import MelissaCfgConversasBots from './MelissaCfgConversasBots.vue';
+import MelissaCfgLembretes from './MelissaCfgLembretes.vue';
+import MelissaCfgCreditosWa from './MelissaCfgCreditosWa.vue';
+import MelissaCfgSms from './MelissaCfgSms.vue';
+import MelissaCfgEmailTemplates from './MelissaCfgEmailTemplates.vue';
+import MelissaCfgRecursosExtras from './MelissaCfgRecursosExtras.vue';
+import MelissaCfgRecursosExtrasExtrato from './MelissaCfgRecursosExtrasExtrato.vue';
+import MelissaCfgAuditoria from './MelissaCfgAuditoria.vue';
// Sidebar global de configs removido — substituido por botao + popover
// (MelissaConfigPopover) dentro de cada pagina de config. Resolveu lag
// de scroll que o overlay sempre visivel causava em mobile.
@@ -68,6 +89,7 @@ import { useMelissaWhatsapp } from './composables/useMelissaWhatsapp';
import { useMelissaAgenda, MELISSA_AGENDA_KEY } from './composables/useMelissaAgenda';
import { useMelissaDockPins } from './composables/useMelissaDockPins';
import { supabase } from '@/lib/supabase/client';
+import { useTenantStore } from '@/stores/tenantStore';
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
import ConversationDrawer from '@/components/conversations/ConversationDrawer.vue';
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
@@ -193,6 +215,27 @@ const SECOES = {
'agenda-config': { label: 'Configurações da Agenda', icon: 'pi pi-calendar', descricao: 'Jornada (dias e horários), ritmo das sessões e agendamento online.' },
// Pagina nativa de formas de pagamento (MelissaPagamento) — saiu do MelissaConfiguracoes
pagamento: { label: 'Formas de Pagamento', icon: 'pi pi-wallet', descricao: 'Pix, depósito, dinheiro, cartão e convênio.' },
+ // Paginas nativas de configuracoes financeiras — saidas do MelissaConfiguracoes
+ 'cfg-precificacao': { label: 'Precificação', icon: 'pi pi-tag', descricao: 'Valor padrão da sessão e preços por tipo de compromisso.' },
+ 'cfg-descontos': { label: 'Descontos por Paciente', icon: 'pi pi-percentage', descricao: 'Descontos recorrentes aplicados automaticamente.' },
+ 'cfg-excecoes': { label: 'Exceções Financeiras', icon: 'pi pi-exclamation-triangle', descricao: 'O que cobrar em faltas, cancelamentos e situações excepcionais.' },
+ 'cfg-convenios': { label: 'Convênios', icon: 'pi pi-id-card', descricao: 'Cadastre os convênios que você atende e seus valores.' },
+ 'cfg-wa': { label: 'Canal WhatsApp', icon: 'pi pi-whatsapp', descricao: 'Escolha o canal de envio: oficial AgenciaPSI ou pessoal.' },
+ 'cfg-wa-pessoal': { label: 'WhatsApp Pessoal', icon: 'pi pi-mobile', descricao: 'Conecte seu próprio número via QR code.' },
+ 'cfg-wa-oficial': { label: 'WhatsApp Oficial', icon: 'pi pi-verified', descricao: 'Número provisionado pela AgenciaPSI via API oficial Meta.' },
+ 'cfg-wa-templates': { label: 'Templates WhatsApp', icon: 'pi pi-file-edit', descricao: 'Personalize os textos enviados ou volte ao padrão.' },
+ 'cfg-conversas-tags': { label: 'Tags de Conversa', icon: 'pi pi-tag', descricao: 'Etiquetas custom pra classificar threads no CRM.' },
+ 'cfg-conversas-autoreply': { label: 'Auto-reply WhatsApp', icon: 'pi pi-reply', descricao: 'Resposta automática quando paciente escreve fora do horário.' },
+ 'cfg-conversas-optouts': { label: 'Opt-outs (LGPD)', icon: 'pi pi-ban', descricao: 'Números que pediram pra não receber mensagens. LGPD Art. 18.' },
+ 'cfg-conversas-sla': { label: 'SLA de resposta', icon: 'pi pi-stopwatch', descricao: 'Tempo máximo pra responder. Alerta quando estourar.' },
+ 'cfg-conversas-bots': { label: 'Bot de triagem', icon: 'pi pi-android', descricao: 'Coleta nome e motivo via WhatsApp antes do humano.' },
+ 'cfg-lembretes': { label: 'Lembretes de Sessão', icon: 'pi pi-bell', descricao: 'WhatsApp automático antes das sessões agendadas.' },
+ 'cfg-creditos-wa': { label: 'Créditos WhatsApp', icon: 'pi pi-credit-card', descricao: 'Compre pacotes de mensagens, veja saldo e extrato.' },
+ 'cfg-sms': { label: 'SMS', icon: 'pi pi-comment', descricao: 'Backup quando WhatsApp falha. Gerencie créditos SMS.' },
+ 'cfg-email-templates': { label: 'Templates de E-mail', icon: 'pi pi-envelope', descricao: 'Personalize os e-mails enviados aos pacientes.' },
+ 'cfg-recursos-extras': { label: 'Recursos Extras', icon: 'pi pi-box', descricao: 'Amplie as funcionalidades com recursos adicionais.' },
+ 'cfg-recursos-extras-extrato': { label: 'Extrato de Recursos Extras', icon: 'pi pi-list', descricao: 'Histórico de débitos e créditos exportável.' },
+ 'cfg-auditoria': { label: 'Auditoria', icon: 'pi pi-shield', descricao: 'Registro imutável de operações (LGPD Art. 37).' },
// Pagina nativa de alterar plano (MelissaAlterarPlano) — substitui /therapist/upgrade
'alterar-plano': { label: 'Alterar Plano', icon: 'pi pi-arrow-up-right', descricao: 'Escolha um plano pessoal pra ativar todos os recursos.' },
// Onda 1 — pages embedadas via MelissaEmbed (1-coluna, hero glass)
@@ -221,6 +264,12 @@ const MELISSA_NON_CONFIG_SLUGS = new Set([
'documentos', 'documentos-templates', 'relatorios',
'perfil', 'plano', 'negocio', 'seguranca', 'bloqueios', 'alterar-plano',
'online-scheduling', 'agenda-config', 'pagamento',
+ 'cfg-precificacao', 'cfg-descontos', 'cfg-excecoes', 'cfg-convenios',
+ 'cfg-wa', 'cfg-wa-pessoal', 'cfg-wa-oficial', 'cfg-wa-templates',
+ 'cfg-conversas-tags', 'cfg-conversas-autoreply', 'cfg-conversas-optouts',
+ 'cfg-conversas-sla', 'cfg-conversas-bots',
+ 'cfg-lembretes', 'cfg-creditos-wa', 'cfg-sms',
+ 'cfg-email-templates', 'cfg-recursos-extras', 'cfg-recursos-extras-extrato', 'cfg-auditoria',
...MELISSA_EMBED_KEYS
]);
// Aliases "bonitos" + INLINE_KEYS reconhecidos pelo MelissaConfiguracoes.
@@ -283,6 +332,14 @@ function fecharSecao() {
router.push({ name: 'Melissa', params: {} });
}
+// Click "Cores do Tema" no menu principal: fecha qualquer fake dialog
+// aberto (perfil/plano/negocio/seguranca/pagamento/agendador/cfg-*) e
+// abre o painel Personalizar (cog top-right).
+function onMenuOpenSettings() {
+ fecharSecao();
+ settingsOpen.value = true;
+}
+
// ── Pins dinâmicos do dock (híbrido: 4 fixos + 3 MRU) ──────────
const dockPins = useMelissaDockPins();
const pinContextMenu = ref(null);
@@ -423,6 +480,21 @@ const {
clearBg
} = useMelissaWallpaper();
+// Nome do tema selecionado em "Personalizar > Temas" (Freud/Klein/Jung).
+// E o ID persistido — wallpaper e' resolvido a partir dele no boot
+// (sem guardar a data URL gigante no DB). null = wallpaper custom ou padrao.
+const themeName = ref(null);
+function setThemeName(name) {
+ if (name && !MELISSA_THEME_NAMES.has(name)) name = null;
+ themeName.value = name || null;
+}
+
+// Quando ON, os textos do hero (relogio, saudacao, resumo) ganham um
+// fundo solido translucido + borda + padding. Util pra wallpapers
+// com pouca transparencia onde o text-shadow nao da legibilidade.
+const textBgEnabled = ref(false);
+function setTextBgEnabled(v) { textBgEnabled.value = !!v; }
+
// ───────────────────────────────────────────────────────────────
// Tema (dark/light + cor primária) — usa a infra existente do app
// ───────────────────────────────────────────────────────────────
@@ -531,6 +603,7 @@ const eventoSelecionado = ref(null);
const eventoBusy = ref(false); // bloqueia botões enquanto UPDATE roda
const melissaAgendaRef = ref(null); // pra chamar openProntuario + setView
const toast = useToast();
+const tenantStore = useTenantStore();
const conversationDrawerStore = useConversationDrawerStore();
// ───────────────────────────────────────────────────────────────
@@ -881,6 +954,73 @@ function fecharCronometro() {
cronoRef.value?.fechar();
}
+// ── Cronometro: salvar tempo na sessao agendada ──────────────
+// Quando o user para o cronometro com paciente selecionado, busca a
+// sessao agendada do dia desse paciente em agenda_eventos e grava o
+// tempo cronometrado em extra_fields.cronometro_*. Se nao encontrar
+// sessao agendada hoje, avisa e nao falha silencioso.
+async function onCronometroSessionEnd({ pacienteId, elapsedSec, stoppedAt }) {
+ if (!pacienteId || !Number.isFinite(elapsedSec) || elapsedSec <= 0) return;
+ const tenantId = tenantStore?.activeTenantId || tenantStore?.tenantId;
+ if (!tenantId) {
+ toast.add({ severity: 'warn', summary: 'Sem tenant ativo', detail: 'Não foi possível salvar o tempo.', life: 3500 });
+ return;
+ }
+ // Janela do dia em ISO (00:00 ate 23:59:59 local).
+ const now = new Date();
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0).toISOString();
+ const tomorrowStart = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0).toISOString();
+
+ try {
+ const { data: sessao, error } = await supabase
+ .from('agenda_eventos')
+ .select('id, extra_fields, inicio_em')
+ .eq('tenant_id', tenantId)
+ .eq('patient_id', pacienteId)
+ .eq('tipo', 'sessao')
+ .gte('inicio_em', todayStart)
+ .lt('inicio_em', tomorrowStart)
+ .order('inicio_em', { ascending: false })
+ .limit(1)
+ .maybeSingle();
+ if (error) throw error;
+ if (!sessao) {
+ toast.add({
+ severity: 'warn',
+ summary: 'Sessão não encontrada',
+ detail: 'Não há sessão agendada hoje pra este paciente — tempo não foi salvo.',
+ life: 4500
+ });
+ return;
+ }
+ const newExtra = {
+ ...(sessao.extra_fields && typeof sessao.extra_fields === 'object' ? sessao.extra_fields : {}),
+ cronometro_duracao_seg: Math.round(elapsedSec),
+ cronometro_parado_em: stoppedAt
+ };
+ const { error: upErr } = await supabase
+ .from('agenda_eventos')
+ .update({ extra_fields: newExtra })
+ .eq('id', sessao.id);
+ if (upErr) throw upErr;
+
+ const min = Math.round(elapsedSec / 60);
+ toast.add({
+ severity: 'success',
+ summary: 'Tempo registrado',
+ detail: `${min} min cronometrados salvos na sessão.`,
+ life: 3000
+ });
+ } catch (e) {
+ toast.add({
+ severity: 'error',
+ summary: 'Falha ao salvar tempo',
+ detail: e?.message || 'Tente novamente.',
+ life: 4500
+ });
+ }
+}
+
// Provide das prefs/refs pro MelissaConfiguracoes (página interna de
// configs). Posicionado aqui pra que TODAS as refs/funções referenciadas
// já estejam definidas no momento do setup. A página lê/escreve direto
@@ -907,7 +1047,13 @@ provide('melissaSettings', {
use24h,
// cronômetro
toqueTermino,
- testarToque
+ testarToque,
+ // tema (bundle wallpaper + cores) — Freud/Klein/Jung
+ themeName,
+ setThemeName,
+ // fundo nos textos do hero (relogio, saudacao, resumo)
+ textBgEnabled,
+ setTextBgEnabled
});
// ───────────────────────────────────────────────────────────────
@@ -948,6 +1094,14 @@ function applyPrefsPayload(prefs) {
if (prefs.cardsLayout === 'linha-unica' || prefs.cardsLayout === 'duas-linhas') {
cardsLayout.value = prefs.cardsLayout;
}
+ if (typeof prefs.themeName === 'string' && MELISSA_THEME_NAMES.has(prefs.themeName)) {
+ themeName.value = prefs.themeName;
+ } else if (prefs.themeName === null) {
+ themeName.value = null;
+ }
+ if (typeof prefs.textBgEnabled === 'boolean') {
+ textBgEnabled.value = prefs.textBgEnabled;
+ }
}
function currentPrefsSnapshot() {
@@ -957,7 +1111,9 @@ function currentPrefsSnapshot() {
bgImageOpacity: bgImageOpacity.value,
use24h: use24h.value,
cardsAtivos: cardsAtivos.value,
- cardsLayout: cardsLayout.value
+ cardsLayout: cardsLayout.value,
+ themeName: themeName.value,
+ textBgEnabled: textBgEnabled.value
};
}
@@ -1036,6 +1192,29 @@ async function saveDbPrefs() {
}
}
+// Quando themeName carregou mas bgUrl ainda esta vazio (ex: 1a vez em outro
+// device), resolve a imagem do tema e gera a data URL. Idempotente: se ja
+// tem bgUrl, nao mexe (custom upload ou data URL ja restaurada do storage).
+async function resolveThemeWallpaperIfNeeded() {
+ if (!themeName.value) return;
+ if (bgUrl.value) return;
+ const t = findMelissaTheme(themeName.value);
+ if (!t) return;
+ try {
+ const res = await fetch(t.image);
+ const blob = await res.blob();
+ const dataUrl = await new Promise((resolve, reject) => {
+ const r = new FileReader();
+ r.onload = () => resolve(r.result);
+ r.onerror = reject;
+ r.readAsDataURL(blob);
+ });
+ bgUrl.value = dataUrl;
+ } catch {
+ bgUrl.value = t.image; // fallback: URL direta funciona na sessao
+ }
+}
+
function queueDbSave() {
if (dbSaveTimer) clearTimeout(dbSaveTimer);
dbSaveTimer = setTimeout(saveDbPrefs, 600);
@@ -1043,7 +1222,7 @@ function queueDbSave() {
// Salva em qualquer mudança das prefs (deep no array de cardsAtivos pra pegar splice/push)
watch(
- [toqueTermino, overlayOpacity, bgImageOpacity, use24h, bgUrl, cardsAtivos, cardsLayout],
+ [toqueTermino, overlayOpacity, bgImageOpacity, use24h, bgUrl, cardsAtivos, cardsLayout, themeName, textBgEnabled],
() => {
saveLayoutPrefs();
queueDbSave();
@@ -1055,6 +1234,9 @@ watch(
onMounted(async () => {
loadLocalPrefs(); // sync: paint imediato com valores cached
await loadDbPrefs(); // async: sobrescreve com valores autoritativos do DB
+ // Se o user logou em outro device, themeName vem do DB mas bgUrl
+ // (data URL) nao — resolvemos a imagem do tema agora.
+ await resolveThemeWallpaperIfNeeded();
});
// Auto-scroll inicial + ResizeObserver da timeline migrou pro
@@ -1128,6 +1310,12 @@ function onKeydown(e) {
+
+
+
+
+
+
+
+
+
+
+
+
+
Pré-visualização
+
Como seu negócio aparece
+
+
+
+
+
+
+
Preencha os dados
pra ver o preview
+
+
+
+
![Logo]()
+
+
+
+
+
+
{{ form.nome_fantasia }}
+
{{ form.razao_social }}
+
{{ businessTypes.find((t) => t.value === form.tipo_empresa)?.label || form.tipo_empresa }}
+
+
+
+
+
+ CNPJ
+ {{ form.cnpj }}
+
+
+ IE
+ {{ form.ie }}
+
+
+ IM
+ {{ form.im }}
+
+
+
+
+
+
+
+
+
+ {{ form.email }}
+
+
+
+ {{ form.telefone }}
+
+
+
+ {{ form.site }}
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/melissa/MelissaPagamento.vue b/src/layout/melissa/MelissaPagamento.vue
index 0912338..a01e9ab 100644
--- a/src/layout/melissa/MelissaPagamento.vue
+++ b/src/layout/melissa/MelissaPagamento.vue
@@ -14,6 +14,7 @@
* Logica espelhada do ConfiguracoesPagamentoPage (tabela payment_settings).
*/
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
+import MelissaConfigList from './MelissaConfigList.vue';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
@@ -35,6 +36,11 @@ function _onMqMobileChange(e) {
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
function fecharDrawer() { drawerOpen.value = false; }
+// Toggle entre cards (default) e lista de configs (alterna inline na sidebar)
+const cfgOpen = ref(false);
+function toggleCfg() { cfgOpen.value = !cfgOpen.value; }
+function fecharCfg() { cfgOpen.value = false; }
+
// ── Estado ─────────────────────────────────────────────────
const loading = ref(true);
const ownerId = ref(null);
@@ -253,7 +259,15 @@ onBeforeUnmount(() => {