// Perfil Supervisor no app do promotor — orcamento 310, item 15.
//
// Quem esta escalado como Supervisor ou Coordenador ganha, dentro da acao, a
// lista de presenca da equipe, os resultados que cada um registrou, e duas
// acoes de campo: informar ausencia e registrar substituicao.
//
// O QUE NAO APARECE AQUI, E DE PROPOSITO
// Cache e prazo de pagamento. A migration 040 removeu a policy que expunha a
// linha inteira das escalas justamente por causa disso, e a RPC da migration
// 044 devolve so as colunas seguras. Esta tela nao tem como mostrar dinheiro
// dos colegas nem que alguem queira.
//
// SUBSTITUICAO NAO TROCA A ESCALA
// Quem cobre a vaga quase nunca esta cadastrado, e cache e pagamento sao
// decisao da producao. O supervisor registra o fato e a producao formaliza —
// por isso a tela pede o nome de quem cobriu e avisa que a producao ajusta
// a escala depois.
const SUP_ESTADOS = {
concluiu: { emoji: '🟢', rotulo: 'Concluiu', cor: '#166534' },
presente: { emoji: '🟢', rotulo: 'Check-in realizado', cor: '#15803D' },
sem_checkin: { emoji: '🟡', rotulo: 'Sem check-in', cor: '#B45309' },
falta: { emoji: '🔴', rotulo: 'Faltou', cor: '#B91C1C' },
recusou: { emoji: '🔴', rotulo: 'Recusou', cor: '#B91C1C' },
};
const supHora = iso => iso
? new Date(iso).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
: null;
function supEstado(p) {
if (p.check_out_em) return 'concluiu';
if (p.check_in_em) return 'presente';
if (p.status === 'Falta') return 'falta';
if (p.status === 'Recusado') return 'recusou';
return 'sem_checkin';
}
// Modal unico para ausencia e substituicao: sao o mesmo gesto — "esta pessoa
// nao veio" — e a substituicao so acrescenta quem cobriu. Duas telas quase
// iguais dariam ao supervisor a chance de escolher a errada com pressa.
function SupervisorAusenciaModal({ open, pessoa, modo, onClose, onFeito, showToast }) {
const [substituto, setSubstituto] = React.useState('');
const [observacao, setObservacao] = React.useState('');
const [busy, setBusy] = React.useState(false);
React.useEffect(() => {
if (open) { setSubstituto(''); setObservacao(''); }
}, [open, pessoa && pessoa.escala_id, modo]);
if (!open || !pessoa) return null;
const ehSubstituicao = modo === 'substituicao';
const confirmar = async () => {
setBusy(true);
const { error } = ehSubstituicao
? await sb.rpc('supervisor_registrar_substituicao', {
p_escala_id: pessoa.escala_id,
p_substituto: substituto.trim(),
p_observacao: observacao.trim() || null,
})
: await sb.rpc('supervisor_informar_ausencia', {
p_escala_id: pessoa.escala_id,
p_observacao: observacao.trim() || null,
});
setBusy(false);
if (error) {
console.error('[SupervisorAusenciaModal] Supabase error:', error);
showToast(error.message || 'Não foi possível registrar.', 'danger');
return;
}
showToast(ehSubstituicao ? 'Substituição registrada. A produção foi avisada.'
: 'Ausência informada. A produção foi avisada.');
onFeito();
onClose();
};
const podeConfirmar = !busy && (!ehSubstituicao || substituto.trim().length >= 3);
return (
{pessoa.nome} não compareceu?
{ehSubstituicao && (
)}
{ehSubstituicao
? 'A escala e o cachê não são alterados por aqui — a produção ajusta depois com os números.'
: 'A produção é avisada na hora e a escala fica marcada como falta.'}
);
}
function EquipeDaAcaoCard({ ev, showToast }) {
const [equipe, setEquipe] = React.useState([]);
const [campos, setCampos] = React.useState([]);
const [carregando, setCarregando] = React.useState(true);
const [verResultados, setVerResultados] = React.useState(false);
const [alvo, setAlvo] = React.useState(null);
const [modo, setModo] = React.useState('ausencia');
const [avaliar, setAvaliar] = React.useState(null);
const [avaliacoes, setAvaliacoes] = React.useState({});
const carregar = React.useCallback(async () => {
if (!ev || !ev.id) return;
const [{ data, error }, { data: evRow }, { data: notas }] = await Promise.all([
sb.rpc('supervisor_equipe_da_acao', { p_event_id: ev.id }),
// Os rotulos dos indicadores vem do evento, e nao da prop: quem abre a
// acao no app monta o objeto a partir da escala e nem sempre traz
// resultado_campos junto.
sb.from('events').select('resultado_campos').eq('id', ev.id).maybeSingle(),
sb.from('avaliacoes_profissional').select('escala_id, media').eq('event_id', ev.id),
]);
if (error) console.error('[EquipeDaAcaoCard] Supabase error:', error);
setEquipe(data || []);
setCampos((evRow && evRow.resultado_campos) || []);
const mapa = {};
(notas || []).forEach(n => { mapa[n.escala_id] = n.media; });
setAvaliacoes(mapa);
setCarregando(false);
}, [ev && ev.id]);
React.useEffect(() => { carregar(); }, [carregar]);
if (carregando) return null;
// Sem linhas, quem esta olhando nao e supervisor desta acao — a RPC filtra
// no banco. A secao simplesmente nao existe para essa pessoa.
if (!equipe.length) return null;
const presentes = equipe.filter(p => p.check_in_em).length;
return (
Equipe da ação ({presentes}/{equipe.length} presentes)
{equipe.map(p => {
const est = SUP_ESTADOS[supEstado(p)];
// So faz sentido marcar ausencia de quem ainda nao chegou. Quem
// chegou e foi embora e ocorrencia, nao falta.
const podeMarcar = !p.sou_eu && !p.check_in_em && p.status !== 'Falta' && p.status !== 'Recusado';
return (
{verResultados && }
{/* Avaliar so quem trabalhou (item 27): nota para quem faltou
nao mede nada e sujaria a media com um chute. */}
{!p.sou_eu && p.check_in_em && (