// Historico e ranking do profissional — orcamento 310, itens 26 e 28. // // Nenhum numero daqui e guardado. Tudo e conta feita na hora sobre as escalas // e as avaliacoes que ja existem (migration 047). Uma tabela de estatisticas // do freelancer seria um segundo lugar para a mesma verdade, e bastaria um // gatilho falhar para o ranking passar a mentir sobre a operacao. // // "USO APENAS INTERNO" (item 28) // As duas funcoes do banco so devolvem linha para quem e da equipe. O // promotor nao descobre a propria posicao nem a dos colegas, e o cliente // nao tem caminho nenhum ate aqui. const FD_DATA_BR = iso => iso ? String(iso).split('-').reverse().join('/') : '—'; function FDNumero({ valor, rotulo, cor, dica }) { return (
{valor}
{rotulo}
); } // ================================================= // ITEM 26 — historico dentro do cadastro do freelancer // ================================================= function HistoricoProfissionalCard({ freelancerId }) { const [h, setH] = React.useState(null); const [carregando, setCarregando] = React.useState(true); React.useEffect(() => { if (!freelancerId) return; let vivo = true; sb.rpc('historico_do_profissional', { p_freelancer_id: freelancerId }).then(({ data, error }) => { if (!vivo) return; if (error) console.error('[HistoricoProfissionalCard] Supabase error:', error); setH((data && data[0]) || null); setCarregando(false); }); return () => { vivo = false; }; }, [freelancerId]); if (carregando || !h) return null; const pct = v => (v === null || v === undefined ? '—' : Number(v) + '%'); return (
Histórico de ações
0 ? '#B91C1C' : undefined} /> 0 ? '#B45309' : undefined} dica="Check-in mais de 15 minutos depois do horário da ação" /> 0 ? h.avaliacoes + (h.avaliacoes === 1 ? ' avaliação' : ' avaliações') : 'sem avaliação'} />
{h.ultima_acao_nome ? <>Última ação: {h.ultima_acao_nome} em {FD_DATA_BR(h.ultima_acao_data)}. : 'Ainda não participou de nenhuma ação.'} {h.recusas > 0 ? ' ' + h.recusas + (h.recusas === 1 ? ' recusa registrada.' : ' recusas registradas.') : ''}
); } // ================================================= // ITEM 28 — ranking interno // ================================================= // Os cinco criterios que o orcamento pede, cada um como uma ordenacao da // mesma lista. Cinco telas separadas mostrariam os mesmos numeros com cinco // oportunidades de discordarem entre si. const FD_CRITERIOS = [ { id: 'realizadas', rotulo: 'Mais ações realizadas', campo: 'acoes_realizadas', sufixo: '' }, { id: 'ativos', rotulo: 'Mais ativos', campo: 'acoes_escaladas', sufixo: '' }, { id: 'avaliacao', rotulo: 'Melhor avaliação', campo: 'avaliacao_media', sufixo: '' }, { id: 'pontualidade', rotulo: 'Melhor pontualidade', campo: 'taxa_pontualidade', sufixo: '%' }, { id: 'confirmacao', rotulo: 'Maior confirmação', campo: 'taxa_confirmacao', sufixo: '%' }, ]; const FD_PERIODOS = [ { id: '90', rotulo: 'Últimos 90 dias', dias: 90 }, { id: '365', rotulo: 'Último ano', dias: 365 }, { id: 'tudo', rotulo: 'Tudo', dias: null }, ]; function RankingProfissionais() { const [linhas, setLinhas] = React.useState([]); const [criterio, setCriterio] = React.useState('realizadas'); const [periodo, setPeriodo] = React.useState('90'); const [carregando, setCarregando] = React.useState(true); React.useEffect(() => { let vivo = true; setCarregando(true); const p = FD_PERIODOS.find(x => x.id === periodo); let desde = null; if (p && p.dias) { const d = new Date(); d.setDate(d.getDate() - p.dias); desde = d.toISOString().slice(0, 10); } sb.rpc('ranking_profissionais', { p_desde: desde }).then(({ data, error }) => { if (!vivo) return; if (error) console.error('[RankingProfissionais] Supabase error:', error); setLinhas(data || []); setCarregando(false); }); return () => { vivo = false; }; }, [periodo]); const def = FD_CRITERIOS.find(c => c.id === criterio) || FD_CRITERIOS[0]; const ordenadas = React.useMemo(() => { const copia = linhas.slice(); copia.sort((a, b) => { const va = a[def.campo], vb = b[def.campo]; // Quem nao tem o numero vai para o fim, e nao para o topo com um zero // que nunca teve. Sem isto, "melhor avaliacao" comecaria por quem nunca // foi avaliado. if (va === null && vb === null) return 0; if (va === null) return 1; if (vb === null) return -1; return Number(vb) - Number(va); }); return copia; }, [linhas, def]); const valorDe = l => { const v = l[def.campo]; if (v === null || v === undefined) return '—'; return (def.campo === 'avaliacao_media' ? '⭐ ' + Number(v).toFixed(2) : Number(v) + def.sufixo); }; const visiveis = ordenadas.slice(0, 15); return (
Ranking interno
Uso interno — não aparece para o cliente nem para a equipe de campo.
{FD_PERIODOS.map(p => ( ))}
{FD_CRITERIOS.map(c => ( ))}
{carregando ? : visiveis.length === 0 ? (
Nenhuma escala no período escolhido.
) : (
{visiveis.map((l, i) => (
{i + 1}º {l.nome} {l.acoes_realizadas} {l.acoes_realizadas === 1 ? 'ação' : 'ações'} {l.faltas > 0 ? ' · ' + l.faltas + (l.faltas === 1 ? ' falta' : ' faltas') : ''} {valorDe(l)}
))} {ordenadas.length > visiveis.length && (
Mostrando os 15 primeiros de {ordenadas.length}.
)}
)}
); } Object.assign(window, { HistoricoProfissionalCard, RankingProfissionais });