/* ============================================================ KeS Metas — App raiz: estado, navegação, papéis O estado vive em memória (useReducer) e é PERSISTIDO no Firebase quando configurado (ver firebase.js + PASSO-A-PASSO-FIREBASE.md). ============================================================ */ const { useReducer, useEffect, useRef } = React; // Fatias do estado que são salvas no banco de dados (Firebase). // As SENHAS nunca vão para o banco — ficam no cofre do Firebase Authentication. function semSenha(u) { const c = { ...u }; delete c.senha; return c; } function dbSlice(s) { return { users: s.users.map(semSenha), metas: s.metas, activities: s.activities, meetings: s.meetings, stages: s.stages, leads: s.leads }; } const NEW_COLORS = ["#E03C31", "#3E8EDE", "#2BB673", "#F2A93B", "#9B6BD6", "#E0689A", "#48B3C7"]; function initState() { const s = window.KES.seed; return { users: s.users.map((u) => ({ ...u })), metas: JSON.parse(JSON.stringify(s.metas)), activities: s.activities.map((a) => ({ ...a })), meetings: s.meetings.map((m) => ({ ...m })), stages: s.stages.map((st) => ({ ...st })), leads: s.leads.map((l) => ({ ...l, tags: [...(l.tags || [])] })), google: { connected: true, account: window.KES.GoogleCalendarService.account }, agendaPrefill: null, session: null, authEmail: null, // e-mail logado no Firebase (login seguro) view: "dashboard", scope: "all", // contexto do admin: 'all' (equipe) ou userId period: window.buildPreset("mes"), toast: null, }; } function reducer(state, action) { switch (action.type) { case "LOGIN": return { ...state, session: action.user.id, view: "dashboard", scope: "all", period: window.buildPreset("mes") }; case "SET_AUTH_EMAIL": { // Login seguro do Firebase resolveu: casa o e-mail com o usuário do sistema. if (!action.email) return { ...state, authEmail: null, session: null }; const u = state.users.find((x) => x.email.toLowerCase() === action.email.toLowerCase()); return { ...state, authEmail: action.email, session: u ? u.id : null, view: "dashboard", scope: "all", period: window.buildPreset("mes") }; } case "LOGOUT": return { ...state, session: null, authEmail: null }; case "SET_VIEW": return { ...state, view: action.view }; case "SET_PERIOD": return { ...state, period: action.period }; case "SET_SCOPE": return { ...state, scope: action.scope }; case "OPEN_USER": return { ...state, scope: action.userId, view: "dashboard" }; case "TOAST": return { ...state, toast: { msg: action.msg, id: Date.now() } }; case "CLEAR_TOAST": return { ...state, toast: null }; case "UPSERT_ACTIVITY": { const idx = state.activities.findIndex((a) => a.userId === action.userId && a.date === action.date); const activities = state.activities.slice(); if (idx >= 0) activities[idx] = { ...activities[idx], ...action.patch }; else activities.push({ id: "a" + Date.now(), userId: action.userId, date: action.date, ligacoes: 0, conexoes: 0, reunioesRealizadas: 0, reunioesQualificadas: 0, vendas: 0, valorVendas: 0, ...action.patch }); return { ...state, activities }; } case "ADD_MEETING": { // // AQUI: integrar com backend/banco de dados (persistir agendamento) let leads = state.leads; if (action.meeting.leadId) { const reuniao = state.stages.find((s) => /reuni/i.test(s.nome) && s.tipo === "open"); if (reuniao) leads = state.leads.map((l) => l.id === action.meeting.leadId ? { ...l, stageId: reuniao.id } : l); } return { ...state, meetings: [...state.meetings, action.meeting], leads }; } case "DELETE_MEETING": return { ...state, meetings: state.meetings.filter((m) => m.id !== action.id) }; /* ---------- CRM ---------- */ case "ADD_LEAD": { const lead = { id: "l" + Date.now(), createdAt: window.KES.iso(window.KES.SIM_TODAY), tags: [], ...action.lead }; return { ...state, leads: [lead, ...state.leads] }; } case "UPDATE_LEAD": return { ...state, leads: state.leads.map((l) => l.id === action.id ? { ...l, ...action.patch } : l) }; case "DELETE_LEAD": return { ...state, leads: state.leads.filter((l) => l.id !== action.id) }; case "MOVE_LEAD": return { ...state, leads: state.leads.map((l) => l.id === action.id ? { ...l, stageId: action.stageId } : l) }; case "IMPORT_LEADS": // // AQUI: integrar com backend/banco de dados (gravar leads importados) return { ...state, leads: [...action.leads, ...state.leads] }; case "SET_STAGES": { const validIds = new Set(action.stages.map((s) => s.id)); const fallback = action.stages[0].id; const leads = state.leads.map((l) => validIds.has(l.stageId) ? l : { ...l, stageId: fallback }); return { ...state, stages: action.stages, leads }; } case "AGENDAR_LEAD": return { ...state, agendaPrefill: action.lead, view: "agenda" }; case "CLEAR_PREFILL": return { ...state, agendaPrefill: null }; case "ADD_USER": { const id = action.user.id || ("u" + Date.now()); const cor = NEW_COLORS[state.users.length % NEW_COLORS.length]; // Sem senha: a credencial fica no Firebase Authentication. const u = { id, nome: action.user.nome.trim(), email: action.user.email.trim(), papel: action.user.papel, googleConnected: false, cor, fator: 1 }; return { ...state, users: [...state.users, u] }; } case "UPDATE_USER": { const users = state.users.map((u) => { if (u.id !== action.id) return u; const patch = { ...action.patch }; if (!patch.senha) delete patch.senha; // mantém senha se vazio return { ...u, ...patch }; }); return { ...state, users }; } case "DELETE_USER": return { ...state, users: state.users.filter((u) => u.id !== action.id), activities: state.activities.filter((a) => a.userId !== action.id), meetings: state.meetings.filter((m) => m.userId !== action.id), leads: state.leads.filter((l) => l.responsavelId !== action.id), scope: state.scope === action.id ? "all" : state.scope, }; case "SET_GOOGLE": return { ...state, google: { ...state.google, connected: action.connected } }; case "SET_METAS": return { ...state, metas: action.metas }; case "HYDRATE": { // Substitui os dados pelos vindos do banco (Firebase), preservando a sessão/UI. const next = { ...state, ...action.data }; // Se já há um e-mail logado, recalcula a sessão pelos usuários recém-carregados. if (state.authEmail) { const u = next.users.find((x) => x.email.toLowerCase() === state.authEmail.toLowerCase()); next.session = u ? u.id : null; } return next; } default: return state; } } const NAV = [ { id: "dashboard", label: "Painel", icon: "dashboard" }, { id: "crm", label: "CRM", icon: "funnel" }, { id: "agenda", label: "Agenda", icon: "calendar" }, { id: "consolidado", label: "Equipe", icon: "users", adminOnly: true }, { id: "usuarios", label: "Usuários", icon: "user", adminOnly: true }, { id: "metas", label: "Metas", icon: "sliders", adminOnly: true }, ]; function getSubject(state, viewer) { if (viewer.papel === "comum") return { isTeam: false, user: viewer, userIds: [viewer.id] }; if (state.scope === "all") return { isTeam: true, userIds: state.users.map((u) => u.id), label: "Equipe" }; const u = state.users.find((x) => x.id === state.scope) || viewer; return { isTeam: false, user: u, userIds: [u.id] }; } function App() { const [state, dispatch] = useReducer(reducer, undefined, initState); // ---- Persistência no Firebase (quando configurado) ---- const dbReady = useRef(false); // já recebeu os dados do banco? const lastSync = useRef(""); // último JSON salvo/recebido (evita laço) useEffect(() => { const DB = window.KESDB; if (!DB || !DB.configured() || !DB.init()) return; // sem Firebase → modo memória const unsub = DB.subscribe((data, pending) => { if (pending) return; // ignora o "eco" da nossa própria gravação if (!data) { // Primeiro uso: banco vazio → grava o estado inicial (admin + etapas). // Sem senha: a credencial do admin fica no Firebase Authentication. const seed = { users: window.KES.seed.users.map(semSenha), metas: window.KES.seed.metas, activities: [], meetings: [], stages: window.KES.seed.stages, leads: [] }; lastSync.current = JSON.stringify(seed); DB.save(seed); dbReady.current = true; dispatch({ type: "HYDRATE", data: seed }); return; } lastSync.current = JSON.stringify(dbSlice(data)); dbReady.current = true; dispatch({ type: "HYDRATE", data: { users: data.users || [], metas: data.metas || window.KES.seed.metas, activities: data.activities || [], meetings: data.meetings || [], stages: data.stages || window.KES.seed.stages, leads: data.leads || [], } }); }); return unsub; }, []); // Salva no banco sempre que os dados mudarem (após a 1ª carga). useEffect(() => { const DB = window.KESDB; if (!DB || !DB.configured() || !dbReady.current) return; const json = JSON.stringify(dbSlice(state)); if (json === lastSync.current) return; // nada mudou de fato lastSync.current = json; DB.save(dbSlice(state)); }, [state.users, state.metas, state.activities, state.meetings, state.stages, state.leads]); // ---- Login seguro do Firebase (Authentication) ---- // Escuta quem está logado e casa o e-mail com o usuário do sistema. useEffect(() => { const A = window.KESAUTH; if (!A || !A.configured() || !A.init()) return; // sem Firebase → login local (fallback) const unsub = A.onAuth((user) => { dispatch({ type: "SET_AUTH_EMAIL", email: user ? user.email : null }); }); return unsub; }, []); function logout() { if (window.KESAUTH && window.KESAUTH.configured()) window.KESAUTH.signOut(); dispatch({ type: "LOGOUT" }); } useEffect(() => { if (!state.toast) return; const t = setTimeout(() => dispatch({ type: "CLEAR_TOAST" }), 3200); return () => clearTimeout(t); }, [state.toast && state.toast.id]); if (!state.session) { return (<> dispatch({ type: "LOGIN", user: u })} />); } const viewer = state.users.find((u) => u.id === state.session); if (!viewer) { dispatch({ type: "LOGOUT" }); return null; } const isAdmin = viewer.papel === "admin"; const nav = NAV.filter((n) => !n.adminOnly || isAdmin); // garante que comum nunca fique numa view de admin const view = (!isAdmin && ["consolidado", "usuarios", "metas"].includes(state.view)) ? "dashboard" : state.view; const subject = getSubject(state, viewer); const scopeSelect = isAdmin ? ( ) : null; let screen = null; if (view === "dashboard") screen = ; else if (view === "crm") screen = ; else if (view === "agenda") screen = ; else if (view === "consolidado") screen = ; else if (view === "usuarios") screen = ; else if (view === "metas") screen = ; return (
{/* Sidebar (desktop) */} {/* Conteúdo */}
{/* App bar mobile */}
KeS Metas
{screen}
{/* Bottom nav mobile */}
); } ReactDOM.createRoot(document.getElementById("root")).render();