/* ============================================================ 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.map((u) => ( ))}
{participaveis.length < 2 &&

É preciso ter pelo menos 2 usuários com cargo comercial para competir.

}
); } window.CompeticaoScreen = CompeticaoScreen;