/* ============================================================
KeS Metas — COMPETIÇÃO
Programação de competições da equipe: período (tempo), prêmio,
quem participa, ranking ao vivo e ganhadores no encerramento.
- Criar/editar/encerrar: admin e líder.
- Todos os participantes veem o ranking e os prêmios.
Os números vêm dos MESMOS indicadores do painel (KESM.userMetrics),
então conta o que o usuário lança E o que o líder lança para ele.
============================================================ */
function CompeticaoScreen({ state, dispatch, viewer }) {
const { useState } = React;
const K = window.KES, KM = window.KESM;
const hoje = K.iso(K.SIM_TODAY);
const ehGestor = viewer.papel === "admin" || viewer.cargo === "lider";
const usersById = {}; state.users.forEach((u) => { usersById[u.id] = u; });
const competicoes = state.competicoes || [];
const [modal, setModal] = useState(null); // null | {} (nova) | competicao (editar)
const [excluir, setExcluir] = useState(null); // competição a excluir
const [encerrar, setEncerrar] = useState(null); // competição a encerrar
const indicador = (key) => KM.INDICADORES.find((i) => i.key === key) || { rotulo: key, tipo: "num" };
const fmtVal = (c, v) => indicador(c.metrica).tipo === "moeda" ? K.fmtMoeda(v) : K.fmtNum(v);
function statusDe(c) {
if (c.encerrada) return "encerrada";
if (c.fim < hoje) return "encerrada"; // prazo acabou = encerrada (ganhadores pelo resultado final)
if (c.inicio > hoje) return "programada";
return "ativa";
}
function diasRestantes(c) {
const fim = K.parseISO(c.fim), h = K.parseISO(hoje);
return Math.max(0, Math.round((fim.getTime() - h.getTime()) / 86400000));
}
// Ranking da competição: indicador escolhido, no período dela, só participantes.
function rankingDe(c) {
const ids = (c.participantes && c.participantes.length ? c.participantes : state.users.filter((u) => u.cargo).map((u) => u.id));
const p = { start: c.inicio, end: c.fim };
return ids
.map((uid) => ({ user: usersById[uid], val: (KM.userMetrics(state, uid, p)[c.metrica] || {}).realizado || 0 }))
.filter((r) => r.user)
.sort((a, b) => b.val - a.val);
}
// Vencedores: os congelados no encerramento manual, senão o ranking final.
function vencedoresDe(c) {
if (c.vencedores && c.vencedores.length) {
return c.vencedores.map((v) => ({ user: usersById[v.userId] || { nome: v.nome || "—", cor: "#7C8794" }, val: v.valor }));
}
return rankingDe(c).slice(0, 3);
}
function confirmarEncerrar(c) {
const top = rankingDe(c).slice(0, 3).map((r, i) => ({ userId: r.user.id, nome: r.user.nome, valor: r.val, posicao: i + 1 }));
dispatch({ type: "UPDATE_COMPETICAO", id: c.id, patch: { encerrada: true, encerradaEm: hoje, vencedores: top } });
dispatch({ type: "TOAST", msg: "Competição encerrada! 🏆 " + (top[0] ? top[0].nome + " venceu." : "") });
setEncerrar(null);
}
const ordem = { ativa: 0, programada: 1, encerrada: 2 };
const lista = competicoes.slice().sort((a, b) =>
(ordem[statusDe(a)] - ordem[statusDe(b)]) || String(b.inicio).localeCompare(String(a.inicio)));
const MEDALHAS = ["🥇", "🥈", "🥉"];
return (
Competição
Programação de competições da equipe: período, prêmio, participantes e ranking ao vivo.
{ehGestor && (
setModal({})}>Nova competição
)}
{lista.length === 0 && (
Nenhuma competição criada
{ehGestor ? "Crie a primeira: escolha o indicador, o período, o prêmio e quem participa." : "Quando o líder criar uma competição, ela aparece aqui."}
)}
{lista.map((c) => {
const st = statusDe(c);
const rank = st === "encerrada" ? [] : rankingDe(c);
const venc = st === "encerrada" ? vencedoresDe(c) : [];
const ind = indicador(c.metrica);
const dias = diasRestantes(c);
const max = Math.max(1, ...(st === "encerrada" ? venc : rank).map((r) => r.val));
const chip = st === "ativa"
? { txt: dias === 0 ? "Último dia!" : "Em andamento · faltam " + dias + " dia" + (dias === 1 ? "" : "s"), cor: "var(--ok)", bg: "rgba(43,182,115,.14)" }
: st === "programada"
? { txt: "Começa em " + K.fmtBR(c.inicio), cor: "var(--warn)", bg: "rgba(242,169,59,.14)" }
: { txt: "Encerrada", cor: "var(--fg3)", bg: "rgba(255,255,255,.07)" };
return (
{/* Cabeçalho da competição */}
{c.nome}
{chip.txt}
{ind.rotulo}
{K.fmtBR(c.inicio)} → {K.fmtBR(c.fim)}
{(c.participantes || []).length || "toda a equipe"} participante{((c.participantes || []).length || 2) === 1 ? "" : "s"}
{ehGestor && (
{st === "ativa" && setEncerrar(c)}>Encerrar}
{st !== "encerrada" && }
)}
{/* Prêmio */}
{c.premio && (
Prêmio: {typeof c.premio === "number" ? K.fmtMoeda(c.premio) : c.premio}
)}
{/* Ganhadores (encerrada) */}
{st === "encerrada" && (
🏆 Ganhadores
{venc.length === 0 ? (
Sem lançamentos no período da competição.
) : (
{venc.map((r, i) => (
{MEDALHAS[i] || (i + 1) + "º"}
{r.user.nome}
{fmtVal(c, r.val)}
))}
)}
)}
{/* Ranking ao vivo (ativa/programada) */}
{st !== "encerrada" && (
{st === "programada" ? "Participantes" : "Ranking ao vivo"}
{rank.map((r, i) => (
{st === "ativa" ? (MEDALHAS[i] || (i + 1) + "º") : "•"}
{r.user.nome}{r.user.id === viewer.id ? " (você)" : ""}
{st === "ativa" && }
{st === "ativa" &&
{fmtVal(c, r.val)}}
))}
)}
);
})}
{/* Modal criar/editar */}
{modal && (
setModal(null)}
onSave={(data) => {
if (modal.id) {
dispatch({ type: "UPDATE_COMPETICAO", id: modal.id, patch: data });
dispatch({ type: "TOAST", msg: "Competição atualizada." });
} else {
dispatch({ type: "ADD_COMPETICAO", competicao: { id: "cp" + Date.now(), criadaPor: viewer.id, criadaEm: hoje, encerrada: false, vencedores: null, ...data } });
dispatch({ type: "TOAST", msg: "Competição criada! 🏁" });
}
setModal(null);
}} />
)}
{/* Confirmar encerramento (congela os ganhadores) */}
{encerrar && (
setEncerrar(null)}
footer={<> setEncerrar(null)}>Cancelar
confirmarEncerrar(encerrar)}>Encerrar e premiar>}>
Encerrar {encerrar.nome} agora? O ranking atual vira o resultado final e os 3 primeiros ficam registrados como ganhadores.
)}
{/* Confirmar exclusão */}
{excluir && (
setExcluir(null)}
footer={<> setExcluir(null)}>Cancelar
{ dispatch({ type: "DELETE_COMPETICAO", id: excluir.id }); dispatch({ type: "TOAST", msg: "Competição excluída." }); setExcluir(null); }}>Excluir>}>
Excluir {excluir.nome}? O histórico dela some da tela. Esta ação não pode ser desfeita.
)}
);
}
/* ---- Modal de criar/editar competição ---- */
function CompeticaoModal({ initial, users, onClose, onSave }) {
const { useState } = React;
const K = window.KES, KM = window.KESM;
const hoje = K.iso(K.SIM_TODAY);
const participaveis = users.filter((u) => u.cargo); // quem tem cargo comercial compete
const [f, setF] = useState(initial ? {
nome: initial.nome || "", metrica: initial.metrica || "vendas",
inicio: initial.inicio || hoje, fim: initial.fim || hoje,
premio: initial.premio || "", participantes: (initial.participantes || []).slice(),
} : {
nome: "", metrica: "vendas", inicio: hoje, fim: hoje, premio: "",
participantes: participaveis.map((u) => u.id), // padrão: todos com cargo
});
const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
const togglePart = (id) => set("participantes", f.participantes.includes(id)
? f.participantes.filter((x) => x !== id) : [...f.participantes, id]);
const premioNum = Math.max(0, parseFloat(f.premio) || 0);
const ok = f.nome.trim() && f.inicio && f.fim && f.fim >= f.inicio && premioNum > 0 && f.participantes.length >= 2;
return (
Cancelar
onSave({ nome: f.nome.trim(), metrica: f.metrica, inicio: f.inicio, fim: f.fim, premio: premioNum, participantes: f.participantes })}>
{initial ? "Salvar" : "Criar competição"}>}>
set("nome", e.target.value)} placeholder="Ex.: Corrida de Vendas — Julho" />
set("premio", e.target.value)} placeholder="500" />
set("inicio", e.target.value)} />
set("fim", e.target.value)} />
{participaveis.length < 2 && É preciso ter pelo menos 2 usuários com cargo comercial para competir.
}
);
}
window.CompeticaoScreen = CompeticaoScreen;