// Acompanhamento em Tempo Real de UMA acao — orcamento 310, item 4.
//
// Ja existia o PainelPromotores (Freelancers > Acompanhamento), mas ele e
// global: mostra as ultimas 100 escalas de todos os eventos. No dia da campanha
// a producao nao quer isso — quer UMA acao na tela, sabendo quem chegou, quem
// nao chegou e o que ja foi registrado. E esta a promessa do orcamento 310.
//
// Atualiza sozinho por realtime (mesmo canal da migration 028): quando a
// promotora da check-in no celular, a linha muda aqui sem ninguem apertar F5.
const ACOMP_STATUS_CORES = {
'Convidado': { bg: '#F1F5F9', fg: '#475569', rotulo: 'Aguardando' },
'Confirmado': { bg: '#EFF6FF', fg: '#1D4ED8', rotulo: 'Confirmado' },
'Recusado': { bg: '#FEF2F2', fg: '#B91C1C', rotulo: 'Recusou' },
'Check-in': { bg: '#F0FDF4', fg: '#15803D', rotulo: 'Presente' },
'Concluído': { bg: '#F0FDF4', fg: '#166534', rotulo: 'Concluiu' },
'Falta': { bg: '#FEF2F2', fg: '#B91C1C', rotulo: 'Falta' },
};
const fmtAcompHora = iso => iso
? new Date(iso).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
: null;
function TileAcomp({ rotulo, valor, cor, destaque }) {
return (
{valor}
{rotulo}
);
}
function AcompanhamentoEvento({ event }) {
const [escalas, setEscalas] = React.useState([]);
const [fotos, setFotos] = React.useState([]);
const [historico, setHistorico] = React.useState({});
const [loading, setLoading] = React.useState(true);
const [ocorrencias, setOcorrencias] = React.useState([]);
const [fotoAberta, setFotoAberta] = React.useState(null);
const [respostas, setRespostas] = React.useState({});
const carregar = React.useCallback(async () => {
if (!event || !event.id) return;
const [{ data: es, error: erEs }, { data: fo, error: erFo }, { data: oc }] = await Promise.all([
sb.from('event_escalas')
.select('*, freelancers(id, name, phone, photo_url)')
.eq('event_id', event.id)
.order('created_at'),
sb.from('escala_fotos')
.select('*, freelancers(name)')
.eq('event_id', event.id)
.order('enviada_em', { ascending: false }),
sb.from('escala_ocorrencias')
.select('*, freelancers(name)')
.eq('event_id', event.id)
.order('criada_em', { ascending: false }),
]);
if (erEs) console.error('[AcompanhamentoEvento.escalas] Supabase error:', erEs);
if (erFo) console.error('[AcompanhamentoEvento.fotos] Supabase error:', erFo);
setEscalas(es || []);
setFotos(fo || []);
setOcorrencias(oc || []);
setLoading(false);
// Historico basico por profissional (item 4 do orcamento): quantas acoes ja
// concluiu e quantas faltou. Uma consulta so para a equipe inteira.
const ids = (es || []).map(e => e.freelancer_id);
if (ids.length) {
const { data: hist } = await sb.from('event_escalas')
.select('freelancer_id, status')
.in('freelancer_id', ids)
.neq('event_id', event.id);
const mapa = {};
(hist || []).forEach(h => {
if (!mapa[h.freelancer_id]) mapa[h.freelancer_id] = { concluidas: 0, faltas: 0 };
if (h.status === 'Concluído') mapa[h.freelancer_id].concluidas++;
if (h.status === 'Falta' || h.status === 'Recusado') mapa[h.freelancer_id].faltas++;
});
setHistorico(mapa);
}
}, [event && event.id]);
React.useEffect(() => {
carregar();
if (!event || !event.id) return;
const canal = sb.channel('acomp-evento-' + event.id)
.on('postgres_changes', { event: '*', schema: 'public', table: 'event_escalas', filter: 'event_id=eq.' + event.id }, carregar)
.on('postgres_changes', { event: '*', schema: 'public', table: 'escala_fotos', filter: 'event_id=eq.' + event.id }, carregar)
.on('postgres_changes', { event: '*', schema: 'public', table: 'escala_ocorrencias', filter: 'event_id=eq.' + event.id }, carregar)
.subscribe();
return () => { sb.removeChannel(canal); };
}, [carregar, event && event.id]);
const resumo = React.useMemo(() => {
const r = { total: escalas.length, presentes: 0, semCheckin: 0, recusas: 0, concluidos: 0 };
escalas.forEach(e => {
if (e.status === 'Recusado') r.recusas++;
else if (e.check_in_em) { r.presentes++; if (e.check_out_em) r.concluidos++; }
else r.semCheckin++;
});
return r;
}, [escalas]);
// Consolida os indicadores da acao somando o que cada profissional registrou.
const campos = (event && event.resultado_campos) || [];
const totaisResultados = React.useMemo(() => {
const t = {};
campos.forEach(c => { t[c.chave] = 0; });
escalas.forEach(e => {
const r = e.resultados || {};
campos.forEach(c => {
const v = parseFloat(r[c.chave]);
if (Number.isFinite(v)) t[c.chave] += v;
});
});
return t;
}, [escalas, campos]);
const ocorrenciasAbertas = ocorrencias.filter(o => !o.resolvida_em);
// Responder e fechar em um passo so: a producao esta no meio da operacao,
// nao vai abrir uma tela separada para cada problema.
const resolver = async (oc) => {
const texto = (respostas[oc.id] || '').trim();
const { error } = await sb.from('escala_ocorrencias').update({
resolvida_em: new Date().toISOString(),
resposta: texto || null,
}).eq('id', oc.id);
if (error) { console.error('[AcompanhamentoEvento.resolver] Supabase error:', error); return; }
carregar();
};
if (loading) return ;
if (!escalas.length) {
return ;
}
return (
{/* Resumo ao vivo */}
{/* Indicadores da acao */}
{campos.length > 0 && (