diff --git a/src/layout/melissa/MelissaConfiguracoes.vue b/src/layout/melissa/MelissaConfiguracoes.vue index 57c7cde..79ca87b 100644 --- a/src/layout/melissa/MelissaConfiguracoes.vue +++ b/src/layout/melissa/MelissaConfiguracoes.vue @@ -39,7 +39,6 @@ const emit = defineEmits(['close']); // existir, senão a key como-é. const ROUTE_ALIASES = { aparencia: 'aparencia', - negocio: 'cfg-negocio', seguranca: 'cfg-seguranca', bloqueios: 'cfg-bloqueios' }; @@ -97,9 +96,9 @@ const COMPONENT_MAP = { 'cfg-recursos-extras': defineAsyncComponent(() => import('@/layout/configuracoes/ConfiguracoesRecursosExtrasPage.vue')), 'cfg-auditoria': defineAsyncComponent(() => import('@/layout/configuracoes/AuditoriaPage.vue')), // Conta (páginas pessoais que vivem em /account/*) - // 'cfg-perfil' removido — virou pagina nativa MelissaPerfil em /melissa/perfil - // 'cfg-plano' removido — virou pagina nativa MelissaPlano em /melissa/plano - 'cfg-negocio': defineAsyncComponent(() => import('@/views/pages/account/Negociopage.vue')), + // 'cfg-perfil' removido — virou pagina nativa MelissaPerfil em /melissa/perfil + // 'cfg-plano' removido — virou pagina nativa MelissaPlano em /melissa/plano + // 'cfg-negocio' removido — virou pagina nativa MelissaNegocio em /melissa/negocio 'cfg-seguranca': defineAsyncComponent(() => import('@/views/pages/auth/SecurityPage.vue')) }; @@ -149,13 +148,13 @@ const grupos = [ { key: 'conta', label: 'Conta', - desc: 'Dados do negócio e segurança.', + desc: 'Segurança da sua conta.', icon: 'pi pi-user', items: [ - // "Meu Perfil" virou pagina nativa em /melissa/perfil (MelissaPerfil) - // "Meu Plano" virou pagina nativa em /melissa/plano (MelissaPlano) - { key: 'cfg-negocio', label: 'Meu Negócio', desc: 'Dados do negócio, faturamento e branding.', icon: 'pi pi-briefcase' }, - { key: 'cfg-seguranca', label: 'Segurança', desc: 'Senha, dispositivos confiáveis e sessões ativas.', icon: 'pi pi-shield' } + // "Meu Perfil" virou pagina nativa em /melissa/perfil (MelissaPerfil) + // "Meu Plano" virou pagina nativa em /melissa/plano (MelissaPlano) + // "Meu Negócio" virou pagina nativa em /melissa/negocio (MelissaNegocio) + { key: 'cfg-seguranca', label: 'Segurança', desc: 'Senha, dispositivos confiáveis e sessões ativas.', icon: 'pi pi-shield' } ] }, { diff --git a/src/layout/melissa/MelissaLayout.vue b/src/layout/melissa/MelissaLayout.vue index aba0e9f..91138e2 100644 --- a/src/layout/melissa/MelissaLayout.vue +++ b/src/layout/melissa/MelissaLayout.vue @@ -35,6 +35,7 @@ import MelissaGrupos from './MelissaGrupos.vue'; import MelissaConfiguracoes from './MelissaConfiguracoes.vue'; import MelissaPerfil from './MelissaPerfil.vue'; import MelissaPlano from './MelissaPlano.vue'; +import MelissaNegocio from './MelissaNegocio.vue'; import MelissaEmbed from './MelissaEmbed.vue'; import MelissaCadastrosRecebidos from './MelissaCadastrosRecebidos.vue'; import MelissaAgendamentosRecebidos from './MelissaAgendamentosRecebidos.vue'; @@ -169,8 +170,8 @@ const SECOES = { perfil: { label: 'Meu Perfil', icon: 'pi pi-user', descricao: 'Identidade, contato, bio, redes — gamificacao no aside.' }, // Pagina nativa do plano (MelissaPlano) — saiu do MelissaConfiguracoes plano: { label: 'Meu Plano', icon: 'pi pi-credit-card', descricao: 'Assinatura, recursos liberados e historico de mudancas.' }, - // Atalhos de Conta restantes — montam o MelissaConfiguracoes com a seção embed pré-selecionada - negocio: { label: 'Meu Negócio', icon: 'pi pi-briefcase', descricao: 'Dados do negócio, faturamento e branding.' }, + // Pagina nativa do negocio (MelissaNegocio) — saiu do MelissaConfiguracoes + negocio: { label: 'Meu Negócio', icon: 'pi pi-briefcase', descricao: 'Identidade, fiscal, endereco, contato, redes — gamificacao no aside.' }, seguranca: { label: 'Segurança', icon: 'pi pi-shield', descricao: 'Senha, dispositivos confiáveis e sessões.' }, // Onda 1 — pages embedadas via MelissaEmbed (1-coluna, hero glass) 'financeiro': { label: 'Financeiro', icon: 'pi pi-wallet', descricao: 'Visão geral, recebíveis e indicadores.' }, @@ -197,13 +198,13 @@ const MELISSA_NON_CONFIG_SLUGS = new Set([ 'tags', 'grupos', 'cadastros-recebidos', 'medicos', 'agendamentos-recebidos', 'link-externo', 'notificacoes', 'financeiro', 'financeiro-lancamentos', 'documentos', 'documentos-templates', 'relatorios', - 'perfil', 'plano', + 'perfil', 'plano', 'negocio', ...MELISSA_EMBED_KEYS ]); // Aliases "bonitos" + INLINE_KEYS reconhecidos pelo MelissaConfiguracoes. // Mantidos sincronizados com o ROUTE_ALIASES/INLINE_KEYS de lá. const MELISSA_CONFIG_ALIASES = new Set([ - 'aparencia', 'negocio', 'seguranca', 'bloqueios', + 'aparencia', 'seguranca', 'bloqueios', 'fundo', 'relogio', 'cronometro' ]); function isMelissaConfigRoute(slug) { @@ -2222,6 +2223,11 @@ function onKeydown(e) { @close="fecharSecao" /> + + +/* + * MelissaNegocio — Pagina nativa Melissa pra "Meu Negocio". + * + * Substitui o embed cfg-negocio que vivia dentro do MelissaConfiguracoes. + * Layout 2-col (espelha MelissaPerfil): + * - COL 1 (sidebar) — Card "Sua presença" (gamificação 7 niveis + + * badges + dicas) + Card "Logomarca" (preview/upload/remover) + * + Footer (Sair sem salvar -> emit close) + * - COL 2 (main) — Identidade + Dados Fiscais + Endereço (com + * fetch ViaCEP) + Contato + Redes Sociais + * + * Logica de load/save espelhada do Negociopage.vue (tabela + * company_profiles + bucket logos), compativel com /account/negocio. + */ +import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'; +import { useToast } from 'primevue/usetoast'; +import { supabase } from '@/lib/supabase/client'; +import { useTenantStore } from '@/stores/tenantStore'; +// InputText/Select/Textarea/InputMask/Skeleton/Tag: auto via PrimeVueResolver + +const emit = defineEmits(['close']); + +const toast = useToast(); +const tenantStore = useTenantStore(); + +const LOGO_BUCKET = 'logos'; + +// ── Breakpoints + drawer ─────────────────────────────────── +const drawerOpen = ref(false); +const isMobile = ref(false); +let _mqMobile = null; +function _onMqMobileChange(e) { + isMobile.value = e.matches; + if (!e.matches) drawerOpen.value = false; +} +function toggleDrawer() { drawerOpen.value = !drawerOpen.value; } +function fecharDrawer() { drawerOpen.value = false; } + +// ── Estado ───────────────────────────────────────────────── +const loading = ref(true); +const saving = ref(false); +const dirty = ref(false); +const silentApplying = ref(true); + +const tenantId = ref(''); +const recordId = ref(''); +const logoInput = ref(null); + +const ui = reactive({ + logoPreview: '', + logoFile: null, + logoFilePreviewUrl: '' +}); + +const form = reactive({ + nome_fantasia: '', + razao_social: '', + tipo_empresa: '', + cnpj: '', + ie: '', + im: '', + cep: '', + logradouro: '', + numero: '', + complemento: '', + bairro: '', + cidade: '', + estado: '', + telefone: '', + email: '', + site: '', + logo_url: '', + redes_sociais: [] +}); + +const fieldErrors = reactive({ + nome_fantasia: '', + tipo_empresa: '' +}); + +function clearErr(field) { fieldErrors[field] = ''; } +function markDirty() { + if (silentApplying.value) return; + dirty.value = true; +} + +function validateRequired() { + fieldErrors.nome_fantasia = form.nome_fantasia?.trim() ? '' : 'Nome do negócio é obrigatório.'; + fieldErrors.tipo_empresa = form.tipo_empresa?.trim() ? '' : 'Selecione o tipo de negócio.'; + return !fieldErrors.nome_fantasia && !fieldErrors.tipo_empresa; +} + +const businessTypes = [ + { value: 'consultorio', label: 'Consultório' }, + { value: 'clinica', label: 'Clínica' }, + { value: 'instituicao', label: 'Instituição' }, + { value: 'empresa', label: 'Empresa' }, + { value: 'autonomo', label: 'Autônomo' }, + { value: 'cooperativa', label: 'Cooperativa' }, + { value: 'ong', label: 'ONG' }, + { value: 'outro', label: 'Outro' } +]; + +const ufOptions = [ + 'AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG', + 'PA','PB','PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO' +].map((uf) => ({ label: uf, value: uf })); + +const negocioInitials = computed(() => { + const name = form.nome_fantasia || ''; + const parts = String(name).trim().split(/\s+/).filter(Boolean); + const a = parts[0]?.[0] || 'N'; + const b = parts.length > 1 ? parts[1][0] : ''; + return (a + b).toUpperCase(); +}); + +// ── Gamificação / Progresso ──────────────────────────────── +const progressFields = computed(() => [ + { key: 'nome', filled: !!form.nome_fantasia?.trim(), icon: 'pi pi-building', text: 'Preencha o nome do negócio' }, + { key: 'tipo', filled: !!form.tipo_empresa?.trim(), icon: 'pi pi-briefcase', text: 'Selecione o tipo de negócio' }, + { key: 'cnpj', filled: !!form.cnpj?.replace(/[^0-9]/g, ''), icon: 'pi pi-file-edit', text: 'Informe o CNPJ' }, + { key: 'endereco', filled: !!(form.cep && form.logradouro && form.cidade), icon: 'pi pi-map-marker', text: 'Complete o endereço' }, + { key: 'contato', filled: !!(form.telefone?.replace(/[^0-9]/g, '') || form.email), icon: 'pi pi-phone', text: 'Adicione um contato' }, + { key: 'logo', filled: !!(form.logo_url?.trim() || ui.logoFile), icon: 'pi pi-image', text: 'Adicione a logomarca' }, + { key: 'redes', filled: form.redes_sociais.length > 0 || !!form.site?.trim(), icon: 'pi pi-share-alt', text: 'Adicione uma rede social' } +]); + +const progress = computed(() => { + const filled = progressFields.value.filter((f) => f.filled).length; + return Math.round((filled / progressFields.value.length) * 100); +}); + +const progressSuggestions = computed(() => progressFields.value.filter((f) => !f.filled)); + +const progressColor = computed(() => { + if (progress.value >= 80) return '#10b981'; + if (progress.value >= 50) return '#f59e0b'; + return '#ef4444'; +}); + +const bizLevels = [ + { min: 0, max: 14, label: 'Cadastro Básico', icon: '🏗️', color: '#94a3b8' }, + { min: 15, max: 28, label: 'Em Formação', icon: '📋', color: '#60a5fa' }, + { min: 29, max: 42, label: 'Estabelecido', icon: '🏢', color: '#f59e0b' }, + { min: 43, max: 57, label: 'Consolidado', icon: '📈', color: '#f97316' }, + { min: 58, max: 71, label: 'Profissional', icon: '💼', color: '#a78bfa' }, + { min: 72, max: 85, label: 'Referência', icon: '🏆', color: '#10b981' }, + { min: 86, max: 100, label: 'Excelência', icon: '⭐', color: '#eab308' } +]; + +const currentBizLevel = computed(() => + bizLevels.find((l) => progress.value >= l.min && progress.value <= l.max) || bizLevels[0] +); +const nextBizLevel = computed(() => { + const i = bizLevels.indexOf(currentBizLevel.value); + return i < bizLevels.length - 1 ? bizLevels[i + 1] : null; +}); +const xpToNext = computed(() => (nextBizLevel.value ? nextBizLevel.value.min - progress.value : 0)); + +const bizBadges = computed(() => [ + { key: 'nome', earned: !!form.nome_fantasia?.trim(), icon: '🏢', label: 'Nomeado' }, + { key: 'tipo', earned: !!form.tipo_empresa?.trim(), icon: '📌', label: 'Categorizado' }, + { key: 'cnpj', earned: !!form.cnpj?.replace(/[^0-9]/g, ''), icon: '📄', label: 'Regularizado' }, + { key: 'endereco', earned: !!(form.cep && form.logradouro && form.cidade), icon: '📍', label: 'Localizado' }, + { key: 'contato', earned: !!(form.telefone?.replace(/[^0-9]/g, '') || form.email), icon: '📞', label: 'Acessível' }, + { key: 'logo', earned: !!(form.logo_url?.trim() || ui.logoFile), icon: '🎨', label: 'Identificado' }, + { key: 'redes', earned: form.redes_sociais.length > 0 || !!form.site?.trim(), icon: '🌐', label: 'Online' } +]); + +// ── Ancoras: badge/dica leva pra sessao do form ──────────── +const SECTION_BY_FIELD = { + nome: 'identidade', + tipo: 'identidade', + cnpj: 'fiscal', + endereco: 'endereco', + contato: 'contato', + logo: 'logomarca', + redes: 'redes' +}; +function scrollToSection(field) { + const sec = SECTION_BY_FIELD[field]; + if (!sec) return; + const isLogo = sec === 'logomarca'; + if (isMobile.value && !isLogo) drawerOpen.value = false; + nextTick(() => { + const target = document.getElementById('mng-sec-' + sec); + if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); +} + +// ── Logomarca ────────────────────────────────────────────── +function pickLogo() { logoInput.value?.click(); } + +function clearLogoFile() { + ui.logoFile = null; + if (ui.logoFilePreviewUrl) { + try { URL.revokeObjectURL(ui.logoFilePreviewUrl); } catch { /* ignore */ } + } + ui.logoFilePreviewUrl = ''; + if (logoInput.value) logoInput.value.value = ''; +} + +function onLogoChange(ev) { + const file = ev?.target?.files?.[0]; + if (!file) return; + if (!file.type?.startsWith('image/')) { + toast.add({ severity: 'warn', summary: 'Arquivo inválido', detail: 'Escolha uma imagem (PNG/SVG/JPG/WebP).', life: 3500 }); + clearLogoFile(); + return; + } + if (file.size > 2 * 1024 * 1024) { + toast.add({ severity: 'warn', summary: 'Arquivo grande', detail: 'Máximo recomendado: 2 MB.', life: 3500 }); + clearLogoFile(); + return; + } + ui.logoFile = file; + if (ui.logoFilePreviewUrl) { + try { URL.revokeObjectURL(ui.logoFilePreviewUrl); } catch { /* ignore */ } + } + ui.logoFilePreviewUrl = URL.createObjectURL(file); + ui.logoPreview = ui.logoFilePreviewUrl; + markDirty(); +} + +function removeLogo() { + form.logo_url = ''; + ui.logoPreview = ''; + clearLogoFile(); + markDirty(); +} + +function extFromMime(mime) { + if (!mime) return 'png'; + if (mime.includes('svg')) return 'svg'; + if (mime.includes('png')) return 'png'; + if (mime.includes('jpeg')) return 'jpg'; + if (mime.includes('webp')) return 'webp'; + return 'png'; +} + +async function uploadLogoIfNeeded() { + if (!ui.logoFile || !tenantId.value) return null; + const file = ui.logoFile; + const ext = extFromMime(file.type); + const path = `${tenantId.value}/logo-${Date.now()}.${ext}`; + const { error: upErr } = await supabase.storage + .from(LOGO_BUCKET) + .upload(path, file, { upsert: true, contentType: file.type }); + if (upErr) throw upErr; + const { data } = supabase.storage.from(LOGO_BUCKET).getPublicUrl(path); + const url = data?.publicUrl; + if (!url) throw new Error('Upload ok, mas não consegui obter a URL pública.'); + return url; +} + +// ── CEP (ViaCEP) ─────────────────────────────────────────── +const fetchingCep = ref(false); +async function onCepBlur() { + const digits = (form.cep || '').replace(/[^0-9]/g, ''); + if (digits.length !== 8) return; + fetchingCep.value = true; + try { + const res = await fetch(`https://viacep.com.br/ws/${digits}/json/`); + const data = await res.json(); + if (data?.erro) return; + form.logradouro = data.logradouro || form.logradouro; + form.bairro = data.bairro || form.bairro; + form.cidade = data.localidade || form.cidade; + form.estado = data.uf || form.estado; + markDirty(); + } catch { /* ignore */ } + finally { fetchingCep.value = false; } +} + +// ── Redes sociais customizadas ───────────────────────────── +function addRede() { form.redes_sociais.push({ name: '', url: '' }); markDirty(); } +function removeRede(idx) { form.redes_sociais.splice(idx, 1); markDirty(); } + +// ── Carregar / Salvar ────────────────────────────────────── +async function loadNegocio() { + silentApplying.value = true; + loading.value = true; + try { + const { data: u, error: uErr } = await supabase.auth.getUser(); + if (uErr) throw uErr; + const uid = u?.user?.id; + if (!uid) throw new Error('Você precisa estar logado.'); + + const { data: member, error: mErr } = await supabase + .from('tenant_members') + .select('tenant_id') + .eq('user_id', uid) + .eq('status', 'active') + .order('created_at', { ascending: true }) + .limit(1) + .maybeSingle(); + if (mErr) throw mErr; + if (!member?.tenant_id) throw new Error('Nenhum tenant associado à sua conta.'); + + tenantId.value = member.tenant_id; + + const { data: co, error: coErr } = await supabase + .from('company_profiles') + .select('*') + .eq('tenant_id', tenantId.value) + .maybeSingle(); + if (coErr) throw coErr; + + if (co) { + recordId.value = co.id; + form.nome_fantasia = co.nome_fantasia ?? ''; + form.razao_social = co.razao_social ?? ''; + form.tipo_empresa = co.tipo_empresa ?? ''; + form.cnpj = co.cnpj ?? ''; + form.ie = co.ie ?? ''; + form.im = co.im ?? ''; + form.cep = co.cep ?? ''; + form.logradouro = co.logradouro ?? ''; + form.numero = co.numero ?? ''; + form.complemento = co.complemento ?? ''; + form.bairro = co.bairro ?? ''; + form.cidade = co.cidade ?? ''; + form.estado = co.estado ?? ''; + form.telefone = co.telefone ?? ''; + form.email = co.email ?? ''; + form.site = co.site ?? ''; + form.logo_url = co.logo_url ?? ''; + ui.logoPreview = form.logo_url; + form.redes_sociais = Array.isArray(co.redes_sociais) ? co.redes_sociais : []; + } + } catch (e) { + toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não consegui carregar os dados do negócio.', life: 6000 }); + } finally { + loading.value = false; + silentApplying.value = false; + dirty.value = false; + } +} + +async function saveAll() { + if (!validateRequired()) { + toast.add({ + severity: 'warn', + summary: 'Campos obrigatórios', + detail: 'Preencha o nome e o tipo do negócio antes de salvar.', + life: 4000 + }); + nextTick(() => { + document.getElementById('mng-sec-identidade')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + return; + } + saving.value = true; + try { + if (ui.logoFile) { + try { + const uploadedUrl = await uploadLogoIfNeeded(); + if (uploadedUrl) { + form.logo_url = uploadedUrl; + ui.logoPreview = uploadedUrl; + } + } catch (e) { + toast.add({ + severity: 'warn', + summary: 'Logo não subiu', + detail: `Não consegui enviar o arquivo (bucket "${LOGO_BUCKET}"). (${e?.message || 'erro'})`, + life: 6500 + }); + } + } + + const payload = { + tenant_id: tenantId.value, + nome_fantasia: String(form.nome_fantasia || '').trim() || null, + razao_social: String(form.razao_social || '').trim() || null, + tipo_empresa: String(form.tipo_empresa || '').trim() || null, + cnpj: String(form.cnpj || '').trim() || null, + ie: String(form.ie || '').trim() || null, + im: String(form.im || '').trim() || null, + cep: String(form.cep || '').trim() || null, + logradouro: String(form.logradouro || '').trim() || null, + numero: String(form.numero || '').trim() || null, + complemento: String(form.complemento || '').trim() || null, + bairro: String(form.bairro || '').trim() || null, + cidade: String(form.cidade || '').trim() || null, + estado: String(form.estado || '').trim() || null, + telefone: String(form.telefone || '').trim() || null, + email: String(form.email || '').trim() || null, + site: String(form.site || '').trim() || null, + logo_url: String(form.logo_url || '').trim() || null, + redes_sociais: form.redes_sociais.filter((r) => r.name || r.url), + updated_at: new Date().toISOString() + }; + + const { error } = await supabase + .from('company_profiles') + .upsert(payload, { onConflict: 'tenant_id' }); + if (error) throw error; + + clearLogoFile(); + dirty.value = false; + toast.add({ severity: 'success', summary: 'Salvo', detail: 'Dados do negócio atualizados.', life: 2500 }); + } catch (e) { + toast.add({ severity: 'error', summary: 'Erro ao salvar', detail: e?.message || 'Não consegui salvar.', life: 6000 }); + } finally { + saving.value = false; + } +} + +// ── Lifecycle ────────────────────────────────────────────── +onMounted(async () => { + if (typeof window !== 'undefined' && window.matchMedia) { + _mqMobile = window.matchMedia('(max-width: 1023px)'); + isMobile.value = _mqMobile.matches; + try { _mqMobile.addEventListener('change', _onMqMobileChange); } + catch { _mqMobile.addListener(_onMqMobileChange); } + } + await tenantStore.ensureLoaded(); + await loadNegocio(); +}); + +onBeforeUnmount(() => { + if (_mqMobile) { + try { _mqMobile.removeEventListener('change', _onMqMobileChange); } + catch { _mqMobile.removeListener(_onMqMobileChange); } + } + clearLogoFile(); +}); + + + + +