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>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
@@ -25,9 +25,16 @@ import { downloadExtratoCSV, downloadExtratoPDF, formatters } from '@/utils/addo
|
||||
import { downloadExcel } from '@/utils/excelExport';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const tenantStore = useTenantStore();
|
||||
|
||||
// "Voltar" preserva o layout (Melissa vs /configuracoes).
|
||||
const inMelissa = computed(() => String(route.path || '').startsWith('/melissa'));
|
||||
function goBack() {
|
||||
router.push(inMelissa.value ? '/melissa/cfg-recursos-extras' : '/configuracoes/recursos-extras');
|
||||
}
|
||||
|
||||
const { rows, balances, filters, dateRange, loading, summary, load, loadBalances } = useAddonExtrato();
|
||||
|
||||
const tenantName = ref('');
|
||||
@@ -193,7 +200,7 @@ watch(
|
||||
<Card>
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button icon="pi pi-arrow-left" text rounded severity="secondary" class="!w-8 !h-8" @click="router.push({ name: 'ConfiguracoesRecursosExtras' })" v-tooltip.bottom="'Voltar'" />
|
||||
<Button icon="pi pi-arrow-left" text rounded severity="secondary" class="!w-8 !h-8" @click="goBack" v-tooltip.bottom="'Voltar'" />
|
||||
<i class="pi pi-list text-xl" />
|
||||
Extrato de Recursos Extras
|
||||
</div>
|
||||
@@ -205,27 +212,27 @@ watch(
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-3">
|
||||
<div class="md:col-span-3">
|
||||
<div :class="inMelissa ? 'md:col-span-6' : 'md:col-span-3'">
|
||||
<label class="text-xs font-medium text-surface-500 mb-1 block">Período</label>
|
||||
<Select v-model="filters.periodPreset" :options="periodOptions" optionLabel="label" optionValue="value" class="w-full" @change="onFilterChange" />
|
||||
</div>
|
||||
|
||||
<div v-if="filters.periodPreset === 'custom'" class="md:col-span-4">
|
||||
<div v-if="filters.periodPreset === 'custom'" :class="inMelissa ? 'md:col-span-6' : 'md:col-span-4'">
|
||||
<label class="text-xs font-medium text-surface-500 mb-1 block">Intervalo personalizado</label>
|
||||
<DatePicker v-model="filters.customRange" selectionMode="range" dateFormat="dd/mm/yy" :manualInput="false" showIcon class="w-full" @update:model-value="onFilterChange" />
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<div :class="inMelissa ? 'md:col-span-6' : 'md:col-span-3'">
|
||||
<label class="text-xs font-medium text-surface-500 mb-1 block">Tipo de recurso</label>
|
||||
<MultiSelect v-model="filters.addonTypes" :options="addonTypeOptions" optionLabel="label" optionValue="value" placeholder="Todos" class="w-full" display="chip" :maxSelectedLabels="2" @change="onFilterChange" />
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<div :class="inMelissa ? 'md:col-span-6' : 'md:col-span-3'">
|
||||
<label class="text-xs font-medium text-surface-500 mb-1 block">Tipo de movimento</label>
|
||||
<MultiSelect v-model="filters.movementTypes" :options="movementOptions" optionLabel="label" optionValue="value" placeholder="Todos" class="w-full" display="chip" :maxSelectedLabels="2" @change="onFilterChange" />
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-6">
|
||||
<div :class="inMelissa ? 'md:col-span-6' : 'md:col-span-12'">
|
||||
<label class="text-xs font-medium text-surface-500 mb-1 block">Buscar por referência / descrição / método</label>
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-search" />
|
||||
@@ -233,18 +240,20 @@ watch(
|
||||
</IconField>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-6 flex items-end gap-2">
|
||||
<Button label="Atualizar" icon="pi pi-refresh" severity="secondary" outlined @click="reload" :loading="loading" />
|
||||
<Button label="Exportar CSV" icon="pi pi-file" severity="secondary" @click="onExportCSV" :disabled="!rows.length" />
|
||||
<Button label="Exportar Excel" icon="pi pi-file-excel" severity="secondary" @click="onExportExcel" :disabled="!rows.length" />
|
||||
<Button label="Exportar PDF" icon="pi pi-file-pdf" severity="secondary" @click="onExportPDF" :disabled="!rows.length" :loading="exportingPdf" />
|
||||
<div class="md:col-span-12 flex items-end gap-2 flex-wrap md:flex-nowrap">
|
||||
<Button class="md:flex-1" label="Atualizar" icon="pi pi-refresh" severity="secondary" outlined @click="reload" :loading="loading" />
|
||||
<Button class="md:flex-1" label="Exportar CSV" icon="pi pi-file" severity="secondary" @click="onExportCSV" :disabled="!rows.length" />
|
||||
<Button class="md:flex-1" label="Exportar Excel" icon="pi pi-file-excel" severity="secondary" @click="onExportExcel" :disabled="!rows.length" />
|
||||
<Button class="md:flex-1" label="Exportar PDF" icon="pi pi-file-pdf" severity="secondary" @click="onExportPDF" :disabled="!rows.length" :loading="exportingPdf" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<!-- Resumo -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<!-- Resumo: em /melissa teleporta pro drawer (#cfg-page-side) empilhado em
|
||||
1 coluna; em /configuracoes fica inline em 4 colunas (md+). -->
|
||||
<Teleport to="#cfg-page-side" :disabled="!inMelissa" defer>
|
||||
<div :class="inMelissa ? 'flex flex-col gap-3' : 'grid grid-cols-2 md:grid-cols-4 gap-3'">
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="flex flex-col">
|
||||
@@ -278,6 +287,7 @@ watch(
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Tabela -->
|
||||
<Card>
|
||||
@@ -293,6 +303,7 @@ watch(
|
||||
sortField="created_at"
|
||||
:sortOrder="-1"
|
||||
emptyMessage="Nenhuma transação encontrada no período. Ajuste os filtros ou selecione um intervalo mais amplo."
|
||||
scrollable
|
||||
>
|
||||
<Column field="created_at" header="Data" style="min-width: 150px" sortable>
|
||||
<template #body="{ data }">{{ formatters.fmtDate(data.created_at) }}</template>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { RouterLink, useRoute } from 'vue-router';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore';
|
||||
@@ -25,10 +25,15 @@ 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'));
|
||||
|
||||
@@ -900,11 +905,11 @@ onMounted(load);
|
||||
<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="/configuracoes/pagamento" class="underline">Configurações › Pagamento</RouterLink>.</p>
|
||||
<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="/configuracoes/pagamento" class="underline font-medium ml-1">Configurar agora</RouterLink>
|
||||
<RouterLink :to="pagamentoPath" class="underline font-medium ml-1">Configurar agora</RouterLink>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
@@ -938,7 +943,7 @@ onMounted(load);
|
||||
<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="/configuracoes/pagamento" class="underline">Formas de Pagamento</RouterLink>
|
||||
<RouterLink :to="pagamentoPath" class="underline">Formas de Pagamento</RouterLink>
|
||||
({{ paymentSettings.pix_chave }}).
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -8,14 +8,25 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useAutoReplySettings } from '@/composables/useAutoReplySettings';
|
||||
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const tenantStore = useTenantStore();
|
||||
const api = useAutoReplySettings();
|
||||
|
||||
// Em /melissa o card vive numa coluna mais estreita — quebra os 7 dias
|
||||
// em 4+3 em vez de espremer 7 colunas. /configuracoes mantem 7 colunas.
|
||||
const inMelissa = computed(() => String(route.path || '').startsWith('/melissa'));
|
||||
|
||||
// Links pra outras paginas de config preservam o layout em que estao
|
||||
// (Melissa vs /configuracoes), evitando "vazar" o usuario do Melissa.
|
||||
const agendaPath = computed(() => (inMelissa.value ? '/melissa/agenda-config' : '/configuracoes/agenda'));
|
||||
const whatsappPath = computed(() => (inMelissa.value ? '/melissa/cfg-wa' : '/configuracoes/whatsapp'));
|
||||
|
||||
const DIAS = [
|
||||
{ dow: 0, label: 'Dom' },
|
||||
{ dow: 1, label: 'Seg' },
|
||||
@@ -298,9 +309,9 @@ onBeforeUnmount(() => { if (_tick) clearInterval(_tick); });
|
||||
<!-- Preview das janelas do modo 'agenda' (read-only) -->
|
||||
<div v-if="api.settings.value.schedule_mode === 'agenda'" class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)] p-3">
|
||||
<div v-if="!agendaWindows.length" class="text-xs text-[var(--text-color-secondary)] italic">
|
||||
Nenhuma regra semanal ativa encontrada na agenda. Configure em <a href="/configuracoes/agenda" class="text-[var(--primary-color)] underline">Configurações → Agenda</a>.
|
||||
Nenhuma regra semanal ativa encontrada na agenda. Configure em <RouterLink :to="agendaPath" class="text-[var(--primary-color)] underline">{{ inMelissa ? 'Agenda' : 'Configurações → Agenda' }}</RouterLink>.
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-7 gap-1">
|
||||
<div v-else class="grid gap-1" :class="inMelissa ? 'grid-cols-4' : 'grid-cols-7'">
|
||||
<div v-for="d in DIAS" :key="d.dow" class="flex flex-col items-center gap-1">
|
||||
<div class="text-[0.7rem] font-semibold text-[var(--text-color-secondary)] uppercase">{{ d.label }}</div>
|
||||
<div class="flex flex-col gap-0.5 w-full">
|
||||
@@ -341,7 +352,10 @@ onBeforeUnmount(() => { if (_tick) clearInterval(_tick); });
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 gap-1.5 border border-[var(--surface-border)] rounded-md bg-[var(--surface-ground)] p-2">
|
||||
<div
|
||||
class="grid gap-1.5 border border-[var(--surface-border)] rounded-md bg-[var(--surface-ground)] p-2"
|
||||
:class="inMelissa ? 'grid-cols-4' : 'grid-cols-7'"
|
||||
>
|
||||
<div v-for="d in DIAS" :key="d.dow" class="flex flex-col gap-1 min-h-[80px]">
|
||||
<div class="text-[0.7rem] font-bold text-[var(--text-color-secondary)] uppercase text-center">{{ d.label }}</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
@@ -390,7 +404,7 @@ onBeforeUnmount(() => { if (_tick) clearInterval(_tick); });
|
||||
<i class="pi pi-info-circle text-sky-500 mt-0.5" />
|
||||
<div>
|
||||
<strong>Pré-requisito:</strong> o WhatsApp do tenant precisa estar conectado em
|
||||
<a href="/configuracoes/whatsapp" class="text-[var(--primary-color)] underline">Configurações → WhatsApp</a>.
|
||||
<RouterLink :to="whatsappPath" class="text-[var(--primary-color)] underline">{{ inMelissa ? 'Canal WhatsApp' : 'Configurações → WhatsApp' }}</RouterLink>.
|
||||
O auto-reply é enviado via o mesmo canal.
|
||||
<br/>
|
||||
Todas as datas/horas são tratadas no fuso <strong>America/Sao_Paulo</strong>.
|
||||
|
||||
@@ -9,15 +9,26 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch, reactive } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useWhatsappCredits } from '@/composables/useWhatsappCredits';
|
||||
import { isValidCPF, fmtCPF, isValidCNPJ, fmtCNPJ } from '@/utils/validators';
|
||||
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const tenantStore = useTenantStore();
|
||||
const api = useWhatsappCredits();
|
||||
|
||||
// Em /melissa o card vive numa coluna mais estreita — limita a loja de
|
||||
// pacotes a 2 por linha (1 no mobile). /configuracoes mantem 4 por linha.
|
||||
const inMelissa = computed(() => String(route.path || '').startsWith('/melissa'));
|
||||
const pkgGridClass = computed(() => (
|
||||
inMelissa.value
|
||||
? 'grid grid-cols-1 md:grid-cols-2 gap-3'
|
||||
: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3'
|
||||
));
|
||||
|
||||
// Dialog de confirmação (coleta CPF/CNPJ antes de criar a cobrança)
|
||||
const confirmDlg = reactive({
|
||||
open: false,
|
||||
@@ -233,7 +244,7 @@ watch(() => tenantStore.activeTenantId, () => { api.loadAll(); });
|
||||
<span class="text-sm font-bold uppercase tracking-wide">Comprar créditos</span>
|
||||
</div>
|
||||
|
||||
<div v-if="api.loading.value && !api.packages.value.length" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div v-if="api.loading.value && !api.packages.value.length" :class="pkgGridClass">
|
||||
<Skeleton v-for="n in 4" :key="n" height="12rem" class="rounded-md" />
|
||||
</div>
|
||||
|
||||
@@ -241,7 +252,7 @@ watch(() => tenantStore.activeTenantId, () => { api.loadAll(); });
|
||||
Nenhum pacote disponível no momento. Contate o suporte.
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div v-else :class="pkgGridClass">
|
||||
<div
|
||||
v-for="pkg in api.packages.value"
|
||||
:key="pkg.id"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import JoditEmailEditor from '@/components/ui/JoditEmailEditor.vue';
|
||||
@@ -27,6 +27,14 @@ import { MOCK_DATA, TEMPLATE_DOMAINS } from '@/lib/email/emailTemplateConstants'
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Em /melissa o "Minha Empresa" foi unificado com /melissa/negocio (mesma
|
||||
// tabela company_profiles). Preserva o layout em que a pagina foi aberta.
|
||||
const inMelissa = computed(() => String(route.path || '').startsWith('/melissa'));
|
||||
function goEmpresa() {
|
||||
router.push(inMelissa.value ? '/melissa/negocio' : '/configuracoes/empresa');
|
||||
}
|
||||
|
||||
// ── Contexto ──────────────────────────────────────────────────
|
||||
const tenantId = ref(null);
|
||||
@@ -365,7 +373,7 @@ onMounted(async () => {
|
||||
<div class="flex items-center gap-3 px-3 py-2.5 rounded-lg border border-[var(--surface-border)] bg-[var(--surface-ground)]">
|
||||
<i class="pi pi-image text-[var(--text-color-secondary)] text-sm opacity-60" />
|
||||
<span class="text-sm flex-1 text-[var(--text-color-secondary)]"> Para trocar sua logo, acesse <strong>Minha Empresa</strong>. </span>
|
||||
<Button label="Minha Empresa" icon="pi pi-building" size="small" severity="secondary" outlined class="shrink-0" @click="router.push('/configuracoes/empresa')" />
|
||||
<Button :label="inMelissa ? 'Meu Negócio' : 'Minha Empresa'" icon="pi pi-building" size="small" severity="secondary" outlined class="shrink-0" @click="goEmpresa" />
|
||||
</div>
|
||||
|
||||
<!-- ── HEADER ──────────────────────────────────────── -->
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
@@ -24,6 +24,25 @@ import { useTenantStore } from '@/stores/tenantStore';
|
||||
const toast = useToast();
|
||||
const tenantStore = useTenantStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Em /melissa o card vive numa coluna mais estreita — limita os pacotes
|
||||
// a 2/linha (1 no mobile) e os "Em breve" a 1/linha. /configuracoes mantem o original.
|
||||
const inMelissa = computed(() => String(route.path || '').startsWith('/melissa'));
|
||||
const productsGridClass = computed(() => (
|
||||
inMelissa.value
|
||||
? 'grid grid-cols-1 md:grid-cols-2 gap-4 mb-4'
|
||||
: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4'
|
||||
));
|
||||
const futureGridClass = computed(() => (
|
||||
inMelissa.value
|
||||
? 'grid grid-cols-1 gap-4'
|
||||
: 'grid grid-cols-1 md:grid-cols-3 gap-4'
|
||||
));
|
||||
|
||||
function goExtrato() {
|
||||
router.push(inMelissa.value ? '/melissa/cfg-recursos-extras-extrato' : '/configuracoes/recursos-extras/extrato');
|
||||
}
|
||||
|
||||
// ── Contexto ──────────────────────────────────────────────────
|
||||
const userId = ref(null);
|
||||
@@ -145,7 +164,7 @@ onMounted(async () => {
|
||||
<i class="pi pi-box text-xl" />
|
||||
Recursos Extras
|
||||
</div>
|
||||
<Button label="Ver extrato" icon="pi pi-list" severity="secondary" outlined size="small" @click="router.push({ name: 'ConfiguracoesRecursosExtrasExtrato' })" />
|
||||
<Button label="Ver extrato" icon="pi pi-list" severity="secondary" outlined size="small" @click="goExtrato" />
|
||||
</div>
|
||||
</template>
|
||||
<template #subtitle>Amplie as funcionalidades da sua clínica com recursos adicionais.</template>
|
||||
@@ -164,7 +183,7 @@ onMounted(async () => {
|
||||
<Tag v-else value="Sem créditos" severity="secondary" class="ml-2" />
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
<div :class="productsGridClass">
|
||||
<Card v-for="product in items" :key="product.id" class="shadow-sm hover:shadow-md transition-shadow">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2 text-base">
|
||||
@@ -195,7 +214,7 @@ onMounted(async () => {
|
||||
Em breve
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div :class="futureGridClass">
|
||||
<Card v-for="addon in futureAddons" :key="addon.type" class="opacity-60">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2 text-base">
|
||||
|
||||
@@ -18,15 +18,19 @@
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const tenantStore = useTenantStore();
|
||||
|
||||
// Preserva o layout em que a pagina esta aberta (Melissa vs /configuracoes).
|
||||
const inMelissa = computed(() => String(route.path || '').startsWith('/melissa'));
|
||||
|
||||
// ── Contexto ──────────────────────────────────────────────────
|
||||
const userId = ref(null);
|
||||
const tenantId = ref(null);
|
||||
@@ -302,7 +306,7 @@ function formatDate(iso) {
|
||||
}
|
||||
|
||||
function goToRecursosExtras() {
|
||||
router.push('/configuracoes/recursos-extras');
|
||||
router.push(inMelissa.value ? '/melissa/cfg-recursos-extras' : '/configuracoes/recursos-extras');
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -16,16 +16,22 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { useTwilioWhatsappStore } from '@/stores/twilioWhatsappStore';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const store = useTwilioWhatsappStore();
|
||||
const tenant = useTenantStore();
|
||||
|
||||
// "Trocar canal" preserva o layout (Melissa vs /configuracoes).
|
||||
const inMelissa = computed(() => String(route.path || '').startsWith('/melissa'));
|
||||
const chooserPath = computed(() => (inMelissa.value ? '/melissa/cfg-wa' : '/configuracoes/whatsapp'));
|
||||
|
||||
// ── Computed ───────────────────────────────────────────────────────────────
|
||||
const tenantId = computed(() => tenant.tenantId);
|
||||
|
||||
@@ -171,7 +177,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<Teleport to="#cfg-page-actions" defer>
|
||||
<router-link to="/configuracoes/whatsapp">
|
||||
<router-link :to="chooserPath">
|
||||
<Button label="Trocar canal" icon="pi pi-arrow-left" severity="secondary" outlined size="small" class="rounded-full" />
|
||||
</router-link>
|
||||
</Teleport>
|
||||
|
||||
@@ -7,17 +7,25 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const tenantStore = useTenantStore();
|
||||
|
||||
// Detecta se o chooser foi aberto dentro do layout Melissa pra rotear o
|
||||
// setup pras paginas nativas (cfg-wa-pessoal/cfg-wa-oficial) em vez das
|
||||
// paginas de /configuracoes (que usam o layout antigo).
|
||||
const inMelissa = computed(() => String(route.path || '').startsWith('/melissa'));
|
||||
const pessoalPath = computed(() => (inMelissa.value ? '/melissa/cfg-wa-pessoal' : '/configuracoes/whatsapp-pessoal'));
|
||||
const oficialPath = computed(() => (inMelissa.value ? '/melissa/cfg-wa-oficial' : '/configuracoes/whatsapp-oficial'));
|
||||
|
||||
const loading = ref(true);
|
||||
const switching = ref(false);
|
||||
const activeChannel = ref(null); // row de notification_channels (provider, is_active, etc) ou null
|
||||
@@ -114,8 +122,8 @@ async function reactivateProvider(provider) {
|
||||
}
|
||||
|
||||
function goSetup(provider) {
|
||||
if (provider === 'evolution') router.push('/configuracoes/whatsapp-pessoal');
|
||||
else if (provider === 'twilio') router.push('/configuracoes/whatsapp-oficial');
|
||||
if (provider === 'evolution') router.push(pessoalPath.value);
|
||||
else if (provider === 'twilio') router.push(oficialPath.value);
|
||||
}
|
||||
|
||||
async function handleChoose(provider) {
|
||||
@@ -282,7 +290,7 @@ watch(() => tenantStore.activeTenantId, () => { loadChannel(); });
|
||||
Escolha outro canal abaixo pra trocar, ou clique em "Gerenciar" pra configurar o atual.
|
||||
</div>
|
||||
</div>
|
||||
<router-link :to="activeProvider === 'twilio' ? '/configuracoes/whatsapp-oficial' : '/configuracoes/whatsapp-pessoal'">
|
||||
<router-link :to="activeProvider === 'twilio' ? oficialPath : pessoalPath">
|
||||
<Button
|
||||
label="Gerenciar"
|
||||
icon="pi pi-cog"
|
||||
|
||||
@@ -16,17 +16,23 @@
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const tenantStore = useTenantStore();
|
||||
|
||||
// "Trocar canal" volta pro chooser. Mantem o usuario no layout em que esta:
|
||||
// /melissa/cfg-wa se aberto via Melissa, /configuracoes/whatsapp caso contrario.
|
||||
const inMelissa = computed(() => String(route.path || '').startsWith('/melissa'));
|
||||
const chooserPath = computed(() => (inMelissa.value ? '/melissa/cfg-wa' : '/configuracoes/whatsapp'));
|
||||
|
||||
// Detecta area (admin clinica vs therapist) pra direcionar corretamente
|
||||
function goConversas() {
|
||||
const role = tenantStore.activeRole || tenantStore.role || '';
|
||||
@@ -770,7 +776,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<Teleport to="#cfg-page-actions" defer>
|
||||
<router-link to="/configuracoes/whatsapp">
|
||||
<router-link :to="chooserPath">
|
||||
<Button label="Trocar canal" icon="pi pi-arrow-left" severity="secondary" outlined size="small" class="rounded-full" />
|
||||
</router-link>
|
||||
</Teleport>
|
||||
@@ -940,8 +946,8 @@ onBeforeUnmount(() => {
|
||||
<TabPanel :value="1">
|
||||
<div class="flex gap-4 pt-3 items-start">
|
||||
|
||||
<!-- ── Coluna esquerda: cards de templates (65%) ── -->
|
||||
<div class="flex flex-col gap-3 min-w-0" style="flex: 0 0 65%;">
|
||||
<!-- ── Coluna esquerda: cards de templates (65% no /configuracoes; 100% no /melissa pq a guia vai pro drawer) ── -->
|
||||
<div class="flex flex-col gap-3 min-w-0" :style="inMelissa ? 'flex: 1;' : 'flex: 0 0 65%;'">
|
||||
<!-- Skeleton loading -->
|
||||
<template v-if="templatesLoading">
|
||||
<div v-for="n in 4" :key="n" class="border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] p-4">
|
||||
@@ -984,8 +990,16 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Coluna direita: guia de formatação (35%) ── -->
|
||||
<div class="flex flex-col gap-3 sticky top-4" style="flex: 0 0 35%;">
|
||||
<!-- ── Coluna direita: guia de formatacao.
|
||||
Em /configuracoes: fica inline (35%).
|
||||
Em /melissa: teleporta pro drawer (#cfg-page-side). -->
|
||||
<Teleport to="#cfg-page-side" :disabled="!inMelissa" defer>
|
||||
<div
|
||||
v-show="!inMelissa || activeTab === 1"
|
||||
class="flex flex-col gap-3"
|
||||
:class="inMelissa ? '' : 'sticky top-4'"
|
||||
:style="inMelissa ? '' : 'flex: 0 0 35%;'"
|
||||
>
|
||||
<div class="border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] p-4 flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-book text-[var(--primary-color)]" />
|
||||
@@ -1083,6 +1097,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
Reference in New Issue
Block a user