/* ============================================================ KeS Metas — CONFIGURAÇÕES (área exclusiva da conta master) ------------------------------------------------------------ EMPRESAS = tenants SEPARADOS das equipes (coleção própria). Cada empresa nasce com um banco ZERADO e cadastro completo. O master (KeS) cria / edita / configura / entra — mas NÃO é membro de nenhuma empresa. ============================================================ */ function ConfigScreen({ state, dispatch, viewer }) { const { useState, useEffect } = React; const DB = window.KESDB; const [empresas, setEmpresas] = useState(null); // null = carregando const [modal, setModal] = useState(null); // null | {} (nova) | empresa (editar) const [excluir, setExcluir] = useState(null); const [aviso, setAviso] = useState(""); function carregar() { if (!DB || !DB.listEmpresas) { setEmpresas([]); return; } DB.listEmpresas().then((l) => { setEmpresas(l || []); // Mantém o seletor de troca rápida (topo) sempre atualizado. dispatch({ type: "SET_EMPRESAS", empresas: (l || []).map((e) => ({ id: e.id, nome: e.nome || e.id })) }); }); } useEffect(() => { carregar(); }, []); const fmtDoc = (e) => e.documento ? ((e.tipoPessoa === "fisica" ? "CPF " : "CNPJ ") + e.documento) : "sem documento"; return (

Configurações · Empresas

Cadastre e gerencie as empresas.

setModal({})}>Cadastrar empresa
{aviso &&
{aviso}
}
Empresas cadastradas{empresas ? " (" + empresas.length + ")" : ""}
{empresas === null ? (
Carregando…
) : empresas.length === 0 ? (
Nenhuma empresa ainda
Clique em "Cadastrar empresa" para criar a primeira.
) : (
{empresas.map((e) => { const cfg = e.config || {}; const matriz = e.config && e.config.matrizId ? empresas.find((x) => x.id === e.config.matrizId) : null; return (
{(e.nome || "?").slice(0, 2).toUpperCase()}
{e.nome}
{fmtDoc(e)} {e.telefone && {e.telefone}} {matriz && Filial de {matriz.nome}}
dispatch({ type: "SET_EMPRESA", empresaId: e.id, nome: e.nome })}>Entrar setModal(e)}>Editar
); })}
)}
{modal && ( !modal.id || x.id !== modal.id)} onClose={() => setModal(null)} onSalvo={(msg) => { setModal(null); carregar(); if (msg) dispatch({ type: "TOAST", msg }); }} onErro={(m) => setAviso(m)} viewer={viewer} /> )} {excluir && ( setExcluir(null)} footer={<> setExcluir(null)}>Cancelar { await DB.deleteEmpresa(excluir.id); setExcluir(null); carregar(); dispatch({ type: "TOAST", msg: "Empresa excluída." }); }}>Excluir definitivamente}>

Excluir {excluir.nome} e todos os dados dela (leads, usuários, metas)? Não pode ser desfeito.

)}
); } /* ---- Script do Google Agenda para a EMPRESA-CLIENTE ---- Mesmo contrato do Agenda-Calendar.gs da KeS (create/delete + Meet com entrada aberta e fallback), com o TOKEN da empresa já embutido. A pessoa só cola na conta Google da empresa, publica e devolve a URL /exec. ---- */ function scriptAgendaEmpresa(token, nomeEmpresa) { return '/**\n' + ' * ' + (nomeEmpresa || 'Empresa') + ' — Agenda de reuniões (KeS Metas)\n' + ' * -----------------------------------------------------------\n' + ' * Publique este script NA CONTA GOOGLE DA EMPRESA (a agenda que\n' + ' * vai receber as reuniões). O sistema KeS Metas chama este script\n' + ' * para criar/cancelar eventos — a senha do Google NUNCA sai daqui.\n' + ' * O TOKEN abaixo já está preenchido — não altere.\n' + ' */\n' + '\n' + 'var TOKEN = "' + token + '";\n' + 'var CRIAR_MEET = true;\n' + 'var MEET_ABERTO = true;\n' + '\n' + 'function doGet(e) {\n' + ' return _json({ ok: true, service: "KeS Metas - Agenda da empresa",\n' + ' calendar: CalendarApp.getDefaultCalendar().getName(), meet: CRIAR_MEET });\n' + '}\n' + '\n' + 'function doPost(e) {\n' + ' try {\n' + ' var body = {};\n' + ' if (e && e.postData && e.postData.contents) body = JSON.parse(e.postData.contents);\n' + ' if (body.token !== TOKEN) return _json({ ok: false, error: "token invalido" });\n' + '\n' + ' if (body.action === "delete") {\n' + ' if (!body.eventId) return _json({ ok: false, error: "eventId ausente" });\n' + ' try { Calendar.Events.remove("primary", body.eventId, { sendUpdates: "all" }); }\n' + ' catch (e2) { if (!/not found|deleted|410|404/i.test(String(e2))) throw e2; }\n' + ' return _json({ ok: true, deleted: body.eventId });\n' + ' }\n' + ' if (body.action !== "create") return _json({ ok: false, error: "acao desconhecida" });\n' + '\n' + ' var start = new Date(body.startISO);\n' + ' var end = body.endISO ? new Date(body.endISO)\n' + ' : new Date(start.getTime() + (parseInt(body.durationMin, 10) || 30) * 60000);\n' + ' var guests = (Array.isArray(body.guests) ? body.guests : []).filter(Boolean)\n' + ' .map(function (em) { return { email: String(em).trim() }; });\n' + '\n' + ' // Sala do Meet com ENTRADA ABERTA (se a Meet API deixar; senao cai no plano B)\n' + ' var meet = "";\n' + ' if (CRIAR_MEET && MEET_ABERTO) {\n' + ' try { meet = criarSalaMeetAberta_(); } catch (eMeet) { console.error(String(eMeet)); }\n' + ' }\n' + '\n' + ' var resource = {\n' + ' summary: body.title || "Reuniao", description: body.description || "",\n' + ' start: { dateTime: start.toISOString() }, end: { dateTime: end.toISOString() },\n' + ' attendees: guests, guestsCanInviteOthers: true, guestsCanModify: false,\n' + ' guestsCanSeeOtherGuests: true, anyoneCanAddSelf: true, visibility: "public",\n' + ' };\n' + ' var params = { sendUpdates: "all" };\n' + ' if (meet) {\n' + ' resource.location = meet;\n' + ' resource.description = (resource.description ? resource.description + "\\n\\n" : "") + "Entrar na reuniao: " + meet;\n' + ' } else if (CRIAR_MEET) {\n' + ' resource.conferenceData = { createRequest: {\n' + ' requestId: "kes-" + new Date().getTime() + "-" + Math.floor(Math.random() * 100000),\n' + ' conferenceSolutionKey: { type: "hangoutsMeet" } } };\n' + ' params.conferenceDataVersion = 1;\n' + ' }\n' + '\n' + ' var ev = Calendar.Events.insert(resource, "primary", params);\n' + ' if (!meet) {\n' + ' meet = ev.hangoutLink || "";\n' + ' if (!meet && ev.conferenceData && ev.conferenceData.entryPoints) {\n' + ' for (var i = 0; i < ev.conferenceData.entryPoints.length; i++) {\n' + ' if (ev.conferenceData.entryPoints[i].entryPointType === "video") { meet = ev.conferenceData.entryPoints[i].uri; break; }\n' + ' }\n' + ' }\n' + ' }\n' + ' return _json({ ok: true, eventId: ev.id, htmlLink: ev.htmlLink || "", meetLink: meet, calendar: "primary" });\n' + ' } catch (err) { return _json({ ok: false, error: String(err) }); }\n' + '}\n' + '\n' + 'function criarSalaMeetAberta_() {\n' + ' var token = ScriptApp.getOAuthToken();\n' + ' var res = UrlFetchApp.fetch("https://meet.googleapis.com/v2/spaces", {\n' + ' method: "post", contentType: "application/json",\n' + ' headers: { Authorization: "Bearer " + token },\n' + ' payload: JSON.stringify({ config: { accessType: "OPEN", entryPointAccess: "ALL" } }),\n' + ' muteHttpExceptions: true,\n' + ' });\n' + ' if (res.getResponseCode() >= 300) throw new Error("meet api: " + res.getResponseCode());\n' + ' var space = JSON.parse(res.getContentText());\n' + ' var link = space.meetingUri || (space.meetingCode ? "https://meet.google.com/" + space.meetingCode : "");\n' + ' if (!link) throw new Error("sala sem link");\n' + ' return link;\n' + '}\n' + '\n' + 'function _json(obj) {\n' + ' return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON);\n' + '}\n'; } /* ---- Modal de cadastro/edição da empresa (cadastro + configuração) ---- */ function EmpresaModal({ initial, outras, onClose, onSalvo, onErro, viewer }) { const { useState } = React; const DB = window.KESDB, SEED = window.KES.seed; const editando = !!initial; const cfg0 = (initial && initial.config) || {}; const [f, setF] = useState({ nome: (initial && initial.nome) || "", tipoPessoa: (initial && initial.tipoPessoa) || "juridica", documento: (initial && initial.documento) || "", telefone: (initial && initial.telefone) || "", email: (initial && initial.email) || "", endereco: (initial && initial.endereco) || "", responsavel: (initial && initial.responsavel) || "", matrizId: cfg0.matrizId || "", termoLead: cfg0.termoLead || "", modulos: Object.assign({ competicao: true, chat: true, whatsapp: true }, cfg0.modulos || {}), agenda: Object.assign({ email: "", horaInicio: "09:00", horaFim: "18:00", dias: [1, 2, 3, 4, 5], duracaoPadrao: 30, linkPadrao: "", gasEndpoint: "", gasToken: "", }, cfg0.agenda || {}), adminNome: "", adminEmail: "", adminSenha: "", // login do admin (só na criação) }); const [err, setErr] = useState(""); const [busy, setBusy] = useState(false); const [copiadoGs, setCopiadoGs] = useState(false); const set = (k, v) => setF((s) => ({ ...s, [k]: v })); const setMod = (k, v) => setF((s) => ({ ...s, modulos: { ...s.modulos, [k]: v } })); const setAg = (k, v) => setF((s) => ({ ...s, agenda: { ...s.agenda, [k]: v } })); const toggleDia = (d) => setF((s) => { const atuais = s.agenda.dias || []; const novos = atuais.indexOf(d) >= 0 ? atuais.filter((x) => x !== d) : atuais.concat([d]).sort(); return { ...s, agenda: { ...s.agenda, dias: novos } }; }); const DIAS = [[0, "D"], [1, "S"], [2, "T"], [3, "Q"], [4, "Q"], [5, "S"], [6, "S"]]; function maskDoc(v) { const d = String(v).replace(/\D/g, "").slice(0, f.tipoPessoa === "fisica" ? 11 : 14); if (f.tipoPessoa === "fisica") return d.replace(/(\d{3})(\d)/, "$1.$2").replace(/(\d{3})(\d)/, "$1.$2").replace(/(\d{3})(\d{1,2})$/, "$1-$2"); return d.replace(/(\d{2})(\d)/, "$1.$2").replace(/(\d{3})(\d)/, "$1.$2").replace(/(\d{3})(\d)/, "$1/$2").replace(/(\d{4})(\d{1,2})$/, "$1-$2"); } function maskTel(v) { let d = String(v).replace(/\D/g, "").slice(0, 11); if (d.length > 6) d = "(" + d.slice(0, 2) + ") " + d.slice(2, 7) + "-" + d.slice(7); else if (d.length > 2) d = "(" + d.slice(0, 2) + ") " + d.slice(2); else if (d.length > 0) d = "(" + d; return d; } // Telas que a empresa pode ou não enxergar. A chave é o id da tela no menu. // "Painel" fica sempre disponível (é a tela-base pra onde o sistema volta). const MODULOS = [ { k: "ganhos", nome: "Ganhos", ic: "banknote" }, { k: "tarefas", nome: "Tarefas", ic: "checkCircle" }, { k: "crm", nome: "CRM", ic: "funnel" }, { k: "agenda", nome: "Agenda", ic: "calendar" }, { k: "whatsapp", nome: "WhatsApp", ic: "whatsapp" }, { k: "email", nome: "E-mails", ic: "mail" }, { k: "competicao", nome: "Competição", ic: "trophy" }, { k: "consolidado", nome: "Equipe (consolidado)", ic: "users" }, { k: "relatorios", nome: "Relatórios", ic: "spreadsheet" }, { k: "usuarios", nome: "Usuários", ic: "user" }, { k: "metas", nome: "Metas", ic: "sliders" }, { k: "comissao", nome: "Comissionamento", ic: "banknote" }, ]; const ligados = MODULOS.filter((m) => f.modulos[m.k] !== false).length; const setTodos = (v) => setF((s) => ({ ...s, modulos: MODULOS.reduce((o, m) => { o[m.k] = v; return o; }, Object.assign({}, s.modulos)) })); async function salvar() { if (!f.nome.trim()) return setErr("Informe o nome da empresa."); if (!editando) { // Login do administrador da empresa (é a porta de entrada dela no sistema). if (!f.adminNome.trim()) return setErr("Informe o nome do administrador da empresa."); if (!/^\S+@\S+\.\S+$/.test(f.adminEmail)) return setErr("E-mail do administrador inválido."); if (f.adminSenha.length < 6) return setErr("Senha do administrador: mínimo 6 caracteres."); } setBusy(true); setErr(""); const cadastro = { nome: f.nome.trim(), tipoPessoa: f.tipoPessoa, documento: f.documento.trim(), telefone: f.telefone.trim(), email: f.email.trim(), endereco: f.endereco.trim(), responsavel: f.responsavel.trim(), config: { matrizId: f.matrizId || null, termoLead: f.termoLead.trim(), modulos: f.modulos, agenda: f.agenda }, }; let r; if (editando) { r = await DB.updateEmpresa(initial.id, cadastro); } else { // 1) conta de LOGIN do admin da empresa const A = window.KESAUTH; const cred = await A.createUser(f.adminEmail.trim(), f.adminSenha); if (!cred.ok) { setBusy(false); return setErr(A.traduzErro(cred)); } const slug = f.nome.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "empresa"; const id = "emp-" + slug + "-" + Math.random().toString(36).slice(2, 7); // 2) banco ZERADO da empresa, já com o admin DELA dentro (o master fica fora) const adminUser = { id: cred.uid, nome: f.adminNome.trim(), email: f.adminEmail.trim().toLowerCase(), papel: "admin", cargo: "lider", googleConnected: false, cor: "#E03C31", fator: 1, comissao: {}, permissoes: null }; const dados = { users: [adminUser], metas: JSON.parse(JSON.stringify(SEED.metas)), activities: [], meetings: [], stages: SEED.stages.map((s) => ({ ...s })), leads: [], comissaoEquipe: { ...SEED.comissaoEquipe }, tarefas: [], competicoes: [], }; const doc = Object.assign({ criadaEm: new Date().toISOString(), criadaPor: (viewer && viewer.email) || "master" }, cadastro, dados); r = await DB.createEmpresa(id, doc); // 3) FICHA do admin aponta pra EMPRESA → no login, ele cai direto nela if (r && r.ok && window.KESMEMBERS) { await window.KESMEMBERS.setMembership(cred.uid, { email: f.adminEmail.trim().toLowerCase(), nome: f.adminNome.trim(), papel: "admin", cargo: "lider", empresaId: id, teamId: null, }); } // 4) WHATSAPP AUTOMÁTICO: a caixa do admin já nasce pedindo o QR. // O motor (n8n) vê o pedido, cria a instância no Evolution e devolve // o QR — o admin só abre a tela WhatsApp e escaneia. if (r && r.ok && DB.waCaixaSet) { try { await DB.waCaixaSet(id, cred.uid, { canal: "evolution", status: "provisionar", auto: true, solicitadoEm: new Date().toISOString() }); } catch (e) { /* não trava o cadastro */ } } } setBusy(false); if (!r || !r.ok) { const m = "Não foi possível salvar a empresa. Tente novamente."; setErr(m); if (onErro) onErro(m); return; } onSalvo(editando ? "Empresa atualizada." : "Empresa cadastrada. ✅"); } return ( Cancelar {busy ? "Salvando…" : (editando ? "Salvar" : "Cadastrar empresa")}}>
Dados da empresa
set("nome", e.target.value)} placeholder="Ex.: Móveis Silva LTDA" />
set("documento", maskDoc(e.target.value))} placeholder={f.tipoPessoa === "fisica" ? "000.000.000-00" : "00.000.000/0000-00"} />
set("telefone", maskTel(e.target.value))} placeholder="(54) 99999-9999" /> set("email", e.target.value)} placeholder="contato@empresa.com" />
set("responsavel", e.target.value)} placeholder="Nome de quem responde pela empresa" /> set("endereco", e.target.value)} placeholder="Rua, número — Cidade/UF" /> {!editando && ( <>
Acesso da empresa (login do administrador)
set("adminNome", e.target.value)} placeholder="Quem vai gerenciar o sistema na empresa" />
set("adminEmail", e.target.value)} placeholder="admin@empresa.com" /> set("adminSenha", e.target.value)} placeholder="mín. 6 caracteres" />
)}
Estrutura
{f.matrizId && ( )}
Telas que esta empresa acessa · {ligados}/{MODULOS.length}

Desmarque uma tela e a empresa deixa de vê-la no menu. O Painel fica sempre disponível. Dá pra mudar isso a qualquer momento por aqui.

setTodos(true)}>Marcar todas setTodos(false)}>Desmarcar todas
{MODULOS.map((m) => { const on = f.modulos[m.k] !== false; return ( ); })}
Agenda desta empresa

Cada empresa tem a própria agenda. Sem conectar nada, as reuniões já funcionam dentro do sistema e o time recebe o convite para jogar na agenda dele (Google, Outlook ou Apple).

setAg("email", e.target.value)} placeholder="agenda@empresa.com" />
setAg("horaInicio", e.target.value)} /> setAg("horaFim", e.target.value)} />
{DIAS.map(([d, rot], i) => { const on = (f.agenda.dias || []).indexOf(d) >= 0; return ( ); })}
setAg("linkPadrao", e.target.value)} placeholder="https://meet.google.com/abc-defg-hij" />
Conectar ao Google Agenda da empresa (opcional) — script pronto aqui dentro

Com isto, as reuniões entram direto no Google Agenda da empresa, com sala do Meet. O sistema gera o script já personalizado — é copiar, publicar na conta Google da empresa e colar o link de volta. A KeS nunca pede nem guarda a senha do Google da empresa. Enquanto não conectar, vale o convite por e-mail.

Passo a passo (~5 min, uma vez só)
  1. Clique em “Copiar script pronto” abaixo (o código já vai com a palavra-chave desta empresa).
  2. Entre na conta Google da empresa (a agenda que receberá as reuniões) e abra script.google.com → “Novo projeto”.
  3. Apague o conteúdo do editor, cole o script (Ctrl+V) e salve (Ctrl+S).
  4. No menu esquerdo, em Serviços +, adicione “Google Calendar API”.
  5. Implantar → Nova implantação → App da Web · Executar como: Eu · Acesso: Qualquer pessoa → Implantar (autorize a conta quando pedir).
  6. Copie a URL que termina em /exec e cole no campo “Link do script” abaixo. Salve a empresa. ✅
{ let tok = (f.agenda.gasToken || "").trim(); if (!tok) { tok = "agenda-" + (f.nome || "empresa").toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "").slice(0, 14) + "-" + Math.random().toString(36).slice(2, 8); setAg("gasToken", tok); } const texto = scriptAgendaEmpresa(tok, f.nome || "Empresa"); const done = () => { setCopiadoGs(true); setTimeout(() => setCopiadoGs(false), 2500); }; try { navigator.clipboard.writeText(texto).then(done); } catch (e) { const ta = document.createElement("textarea"); ta.value = texto; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); ta.remove(); done(); } }}>{copiadoGs ? "Copiado! ✅ agora é só colar no Apps Script" : "Copiar script pronto (Apps Script)"}
setAg("gasEndpoint", e.target.value.trim())} placeholder="https://script.google.com/macros/s/…/exec" /> setAg("gasToken", e.target.value.trim())} placeholder="gerada ao copiar o script" />
Termos (opcional)
set("termoLead", e.target.value)} placeholder="Lead" /> {err &&
{err}
}
); } window.ConfigScreen = ConfigScreen;