/* ============================================================
KeS Metas — Telas administrativas
Usuários (CRUD) · Configurar Metas · Visão Consolidada
============================================================ */
const { useState } = React;
/* ============================================================
USUÁRIOS (CRUD — somente admin)
============================================================ */
function UserModal({ initial, onClose, onSave, emails, onReset }) {
const editing = !!initial;
const usaFirebase = !!(window.KESAUTH && window.KESAUTH.configured());
const [f, setF] = useState(initial || { nome: "", email: "", senha: "", papel: "comum" });
const [err, setErr] = useState("");
const [busy, setBusy] = useState(false);
const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
function save() {
if (!f.nome.trim()) return setErr("Informe o nome.");
if (!/^\S+@\S+\.\S+$/.test(f.email)) return setErr("E-mail inválido.");
const dup = emails.some((e) => e.email.toLowerCase() === f.email.toLowerCase() && e.id !== (initial && initial.id));
if (dup) return setErr("Este e-mail já está cadastrado.");
if (!editing && f.senha.length < 6) return setErr("Defina uma senha inicial (mín. 6 caracteres).");
setBusy(true);
Promise.resolve(onSave(f)).then((r) => {
setBusy(false);
if (r && r.error) setErr(r.error); else onClose();
});
}
return (
Cancelar {busy ? "Salvando…" : (editing ? "Salvar" : "Cadastrar")} >}>
set("nome", e.target.value)} placeholder="Nome e sobrenome" />
set("email", e.target.value)} placeholder="pessoa@kesassessoria.com.br" />
{!editing && (
set("senha", e.target.value)} placeholder="mín. 6 caracteres" />
set("papel", e.target.value)}>
Usuário comum
Administrador
)}
{editing && (
set("papel", e.target.value)}>
Usuário comum
Administrador
)}
{editing && usaFirebase && onReset && (
Enviar um link de redefinição de senha para o e-mail da pessoa.
onReset(f.email)}>Redefinir senha
)}
{err && {err}
}
{f.papel === "admin" ? "Administradores veem e editam os dados de toda a equipe." : "Usuários comuns veem e editam apenas os próprios dados."}
{!editing && usaFirebase ? " A conta de acesso é criada com segurança no Google." : ""}
);
}
function UsuariosScreen({ state, dispatch, viewer }) {
const [modal, setModal] = useState(null); // null | {} | user
const [del, setDel] = useState(null);
const A = window.KESAUTH;
const usaFirebase = !!(A && A.configured());
// Retorna {error} para o modal exibir, ou cria/atualiza e fecha.
async function save(f) {
if (modal && modal.id) {
dispatch({ type: "UPDATE_USER", id: modal.id, patch: { nome: f.nome, papel: f.papel } });
dispatch({ type: "TOAST", msg: "Usuário atualizado." });
setModal(null);
return {};
}
// Novo usuário: cria a conta de login no Firebase e adiciona ao sistema.
if (usaFirebase) {
const res = await A.createUser(f.email, f.senha);
if (!res.ok) return { error: A.traduzErro(res) };
dispatch({ type: "ADD_USER", user: { id: res.uid, nome: f.nome, email: f.email, papel: f.papel } });
} else {
dispatch({ type: "ADD_USER", user: { nome: f.nome, email: f.email, papel: f.papel } });
}
dispatch({ type: "TOAST", msg: "Usuário cadastrado." });
setModal(null);
return {};
}
async function resetar(email) {
if (!usaFirebase) return;
const res = await A.resetPassword(email);
dispatch({ type: "TOAST", msg: res.ok ? "Link de redefinição enviado para " + email + "." : A.traduzErro(res) });
}
return (
Usuários Cadastre, edite e remova membros da equipe. Apenas administradores têm acesso a esta tela.
setModal({})}>Novo usuário
Usuário Papel Leads atribuídos Ações
{state.users.map((u) => (
{u.nome}{u.id === viewer.id ? " (você)" : ""}
{u.email}
{u.papel === "admin" ? "Administrador" : "Comum"}
{state.leads.filter((l) => l.responsavelId === u.id).length} leads
setModal(u)}>
setDel(u)} disabled={u.id === viewer.id}
style={u.id === viewer.id ? { opacity: .35, cursor: "not-allowed" } : { color: "var(--kes-red-400)" }}>
))}
{modal &&
setModal(null)} onSave={save} onReset={resetar} />}
{del && (
setDel(null)}
footer={<> setDel(null)}>Cancelar { dispatch({ type: "DELETE_USER", id: del.id }); dispatch({ type: "TOAST", msg: "Usuário excluído." }); setDel(null); }}>Excluir >}>
Tem certeza que deseja excluir {del.nome} ? Todos os dados de atividade e agendamentos desse usuário serão removidos. Esta ação não pode ser desfeita.
)}
);
}
/* ============================================================
CONFIGURAR METAS (somente admin)
============================================================ */
function ConfigurarMetasScreen({ state, dispatch }) {
const K = window.KES;
const [m, setM] = useState(() => JSON.parse(JSON.stringify(state.metas)));
const setAlvo = (key, v) => setM((s) => ({ ...s, [key]: { ...s[key], alvo: v } }));
const dirty = JSON.stringify(m) !== JSON.stringify(state.metas);
function save() {
const clean = JSON.parse(JSON.stringify(m));
Object.keys(clean).forEach((k) => { clean[k].alvo = Math.max(0, parseInt(clean[k].alvo, 10) || 0); });
dispatch({ type: "SET_METAS", metas: clean });
dispatch({ type: "TOAST", msg: "Metas atualizadas para toda a equipe." });
}
const rows = [
{ key: "ligacoes", icon: "phone", desc: "Quantidade de ligações esperada por dia útil." },
{ key: "conexoes", icon: "link", desc: "Conexões/contatos efetivos por dia útil." },
{ key: "reunioesAgendadas", icon: "calendarCheck", desc: "Reuniões marcadas por dia útil." },
{ key: "reunioesRealizadas", icon: "handshake", desc: "Reuniões efetivamente realizadas por dia útil." },
{ key: "reunioesQualificadas", icon: "checkCircle", desc: "Reuniões qualificadas (lead com fit) por dia útil." },
{ key: "vendas", icon: "trophy", desc: "Vendas fechadas por mês, por pessoa." },
{ key: "valorVendas", icon: "banknote", desc: "Valor (R$) em vendas por mês, por pessoa." },
];
return (
Configurar Metas Defina os alvos da equipe. Valem para todos os usuários e recalculam automaticamente conforme o período filtrado.
{dirty ? "Salvar metas" : "Tudo salvo"}
{rows.map((r, i) => (
{state.metas[r.key].rotulo}
{r.desc}
setAlvo(r.key, e.target.value)} />
/ {m[r.key].unidade === "dia" ? "dia" : "mês"}
))}
);
}
/* ============================================================
VISÃO CONSOLIDADA (somente admin)
============================================================ */
function pctMini(pct) {
const tone = window.tonePct(pct);
return (
);
}
function ConsolidadoScreen({ state, dispatch, viewer }) {
const K = window.KES, KM = window.KESM;
const team = state.users;
const ids = team.map((u) => u.id);
const totals = KM.teamMetrics(state, ids, state.period);
const rows = team.map((u) => {
const mtr = KM.userMetrics(state, u.id, state.period);
return { u, mtr, overall: KM.overallPct(mtr) };
}).sort((a, b) => b.overall - a.overall);
const top = rows[0];
const cols = [
{ key: "ligacoes", lbl: "Ligações" },
{ key: "conexoes", lbl: "Conexões" },
{ key: "reunioesAgendadas", lbl: "Reun. agend." },
{ key: "reunioesRealizadas", lbl: "Reun. realiz." },
{ key: "reunioesQualificadas", lbl: "Reun. qualif." },
{ key: "vendas", lbl: "Vendas" },
];
return (
Visão da Equipe Comparativo de todos os usuários no período. Clique em uma linha para ver o detalhe individual.
dispatch({ type: "SET_PERIOD", period: p })} />
{/* Destaques */}
Maior atingimento
{top &&
}
{top ? top.u.nome.split(" ")[0] : "—"}
{top ? Math.round(top.overall) + "% da meta" : ""}
Vendas no período
{K.fmtNum(totals.vendas.realizado)}
meta {K.fmtNum(totals.vendas.meta)} · {Math.round(totals.vendas.pct)}%
Valor em vendas
{K.fmtMoeda(totals.valorVendas.realizado)}
meta {K.fmtMoeda(totals.valorVendas.meta)}
Comparativo por usuário
# Usuário
{cols.map((c) => {c.lbl} )}
Atingimento
{rows.map((r, i) => (
dispatch({ type: "OPEN_USER", userId: r.u.id })}>
{i + 1}º
{r.u.nome}
{r.u.papel === "admin" ? "Administrador" : "Comum"}
{cols.map((c) => (
{K.fmtNum(r.mtr[c.key].realizado)}
/ {K.fmtNum(r.mtr[c.key].meta)}
))}
{pctMini(r.overall)}
))}
Totais da equipe
{cols.map((c) => (
{K.fmtNum(totals[c.key].realizado)}
/ {K.fmtNum(totals[c.key].meta)}
))}
{pctMini(KM.overallPct(totals))}
Clique em qualquer usuário para abrir o painel individual dele.
);
}
window.UsuariosScreen = UsuariosScreen;
window.ConfigurarMetasScreen = ConfigurarMetasScreen;
window.ConsolidadoScreen = ConsolidadoScreen;