import React, { useState, useEffect, useCallback, useRef } from 'react'; import { TrendingUp, Users, Calendar, DollarSign, ArrowUpRight, ArrowDownRight, Printer, Filter, Loader2, Download, BarChart3, PieChart, Activity, Wallet, Clock, AlertTriangle, Search, X, Stethoscope, CreditCard } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { useHybridBackend } from '../hooks/useHybridBackend.ts'; import { PageHeader } from '../components/PageHeader.tsx'; const fmtBRL = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 }); // ─── Relatório Financeiro (H3.1 período + H3.2 paciente/profissional) ───────── const RelatorioFinanceiro: React.FC<{ inicio: string; fim: string }> = ({ inicio, fim }) => { const [dados, setDados] = useState(null); const [loading, setLoading] = useState(false); const [dentistas, setDentistas] = useState([]); const [profissionalId, setProfissionalId] = useState(''); const [paciente, setPaciente] = useState<{ id: string; nome: string } | null>(null); const [busca, setBusca] = useState(''); const [resultados, setResultados] = useState([]); const [buscaAberta, setBuscaAberta] = useState(false); const buscaRef = useRef(null); useEffect(() => { HybridBackend.getDentistas().then(d => setDentistas(Array.isArray(d) ? d : [])); }, []); // busca de paciente (debounce simples) useEffect(() => { if (!busca) { setResultados([]); return; } const t = setTimeout(() => { HybridBackend.getPacientes(busca).then(r => setResultados(r.data || [])); }, 350); return () => clearTimeout(t); }, [busca]); const carregar = useCallback(() => { setLoading(true); HybridBackend.getFinanceiroRelatorio({ inicio, fim, profissionalId: profissionalId || undefined, pacienteId: paciente?.id }) .then(setDados).catch(() => setDados(null)).finally(() => setLoading(false)); }, [inicio, fim, profissionalId, paciente?.id]); useEffect(() => { carregar(); }, [carregar]); const r = dados?.resumo; const maxMes = Math.max(1, ...(dados?.porMes || []).map((m: any) => Number(m.recebido) + Number(m.a_receber))); const exportCSV = () => { const linhas = dados?.lancamentos || []; let csv = 'Data;Descricao;Paciente;Tipo;Status;Forma;Valor;Origem;SemComissao\n'; linhas.forEach((l: any) => { csv += `${(l.datavencimento || '').slice(0, 10)};${(l.descricao || '').replace(/;/g, ',')};${(l.pacientenome || '').replace(/;/g, ',')};${l.tipo};${l.status};${l.formapagamento || ''};${Number(l.valor).toFixed(2)};${l.origem || ''};${l.sem_comissao ? 'sim' : 'nao'}\n`; }); const url = window.URL.createObjectURL(new Blob([csv], { type: 'text/csv' })); const a = document.createElement('a'); a.href = url; a.download = `relatorio_financeiro_${inicio}_${fim}.csv`; a.click(); }; const KPI: React.FC<{ titulo: string; valor: number; cor: string; Icon: any; sub?: string }> = ({ titulo, valor, cor, Icon, sub }) => (

{titulo}

{fmtBRL(valor)}
{sub &&

{sub}

}
); return (

RELATÓRIO FINANCEIRO

A receber / recebido · por paciente e profissional

{/* Filtros H3.2 */}
{paciente ? (
{paciente.nome}
) : (
setBusca(e.target.value)} onFocus={() => setBuscaAberta(true)} onBlur={() => setTimeout(() => setBuscaAberta(false), 200)} placeholder="Buscar paciente..." className="bg-white border border-gray-200 rounded-xl pl-9 pr-3 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400 min-w-[200px]" /> {buscaAberta && busca && (
    {resultados.map(p => (
  • { setPaciente({ id: p.id, nome: p.nome }); setBusca(''); }} className="px-3 py-2 hover:bg-gray-50 cursor-pointer text-sm font-semibold text-gray-700 uppercase">{p.nome}
  • ))} {resultados.length === 0 &&
  • Nenhum paciente.
  • }
)}
)}

Período herda os filtros de data acima

{loading ? (
) : (
{/* KPIs */}
{Number(r?.glosa) > 0 ? : }
{/* Série por mês */}

Por competência (mês)

{(dados?.porMes || []).length === 0 ?

Sem dados no período.

: (
{dados.porMes.map((m: any) => { const total = Number(m.recebido) + Number(m.a_receber); return (
{m.mes}{fmtBRL(total)}
); })}
Recebido A receber
)}
{/* Por forma de pagamento */}

Recebido por forma de pagamento

{(dados?.porForma || []).length === 0 ?

Nenhum recebimento no período.

: (
{dados.porForma.map((f: any) => (
{f.forma} · {f.qtd}x {fmtBRL(f.total)}
))}
)}
{/* Receitas por convênio */} {(dados?.porConvenio || []).length > 0 && (

Receitas por convênio

{['Convênio', 'Lanç.', 'Recebido', 'A receber', 'Glosa', 'Total'].map(h => )} {dados.porConvenio.map((c: any) => ( ))}
{h}
{c.convenio} {c.qtd} {fmtBRL(c.recebido)} {fmtBRL(c.a_receber)} {Number(c.glosa) > 0 ? fmtBRL(c.glosa) : '—'} {fmtBRL(c.total)}
)} {/* Despesas por centro de custo (H3.3) */} {(dados?.porCategoria || []).length > 0 && (

Despesas por centro de custo

{dados.porCategoria.map((c: any) => (
{c.centro_custo} · {c.qtd}x {fmtBRL(c.total)}
))}
)} {/* Lançamentos */}

Lançamentos ({(dados?.lancamentos || []).length})

{['Venc.', 'Descrição', 'Paciente', 'Status', 'Forma', 'Valor'].map(h => )} {(dados?.lancamentos || []).slice(0, 100).map((l: any) => ( ))} {(dados?.lancamentos || []).length === 0 && ( )}
{h}
{(l.datavencimento || '').slice(0, 10).split('-').reverse().join('/')} {l.descricao}{l.sem_comissao && Sem comissão} {l.pacientenome || '—'} {l.status} {l.formapagamento || '—'} {l.tipo === 'DESPESA' ? '-' : ''}{fmtBRL(l.valor)}
Nenhum lançamento no período/filtro.
)}
); }; interface ReportData { financeiro: { receita: number; despesa: number; totalTransacoes: number; }; producao: { dentista: string; totalAgendamentos: number; concluidos: number; faltas: number; }[]; } export const ReportsView: React.FC = () => { const activeWorkspace = HybridBackend.getActiveWorkspace(); const [startDate, setStartDate] = useState(new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0]); const [endDate, setEndDate] = useState(new Date().toISOString().split('T')[0]); const { data, isLoading, error, refresh } = useHybridBackend(() => activeWorkspace ? HybridBackend.getProductivityReport(activeWorkspace.id, startDate, endDate) : Promise.resolve(null), [activeWorkspace?.id, startDate, endDate] ); const receita = data?.financeiro?.receita || 0; const despesa = data?.financeiro?.despesa || 0; const saldo = receita - despesa; const exportToCSV = () => { if (!data) return; let csv = "Dentista,Total,Concluidos,Faltas,Taxa Ocupacao\n"; data.producao.forEach(p => { const taxa = p.totalAgendamentos > 0 ? ((p.concluidos / p.totalAgendamentos) * 100).toFixed(1) : 0; csv += `${p.dentista},${p.totalAgendamentos},${p.concluidos},${p.faltas},${taxa}%\n`; }); const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `relatorio_producao_${startDate}_${endDate}.csv`; a.click(); }; return (
{/* Filters */}
setStartDate(e.target.value)} className="bg-gray-50 border border-gray-200 rounded-xl px-4 py-2.5 text-sm font-bold text-gray-700 focus:ring-2 focus:ring-teal-100 outline-none transition-all" />
setEndDate(e.target.value)} className="bg-gray-50 border border-gray-200 rounded-xl px-4 py-2.5 text-sm font-bold text-gray-700 focus:ring-2 focus:ring-teal-100 outline-none transition-all" />
{isLoading ? (

Processando dados clínicos...

) : (
{/* Financial Summary */}

RECEITA NO PERÍODO

R$ {receita.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} +2.4%

DESPESA NO PERÍODO

R$ {despesa.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} -1.2%

SALDO OPERACIONAL

= 0 ? 'text-blue-600' : 'text-red-600'}`}> R$ {saldo.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}

LIQUIDEZ ATUAL DA UNIDADE

TRANSAÇÕES

{data?.financeiro?.totalTransacoes || 0}

MOVIMENTAÇÕES FINANCEIRAS

{/* Productivity Table */}

PRODUTIVIDADE POR DENTISTA

DESEMPENHO CLÍNICO NO PERÍODO

Visão por Especialista
{(data?.producao || []).map((p, idx) => { const occupancy = p.totalAgendamentos > 0 ? (p.concluidos / p.totalAgendamentos) * 100 : 0; return ( ); })}
ESPECIALISTA TOTAL APPTS EFETIVADOS FALTAS TAXA OCUPAÇÃO STATUS
{p.dentista.substring(0, 2).toUpperCase()}
{p.dentista}
{p.totalAgendamentos} {p.concluidos} {p.faltas}
70 ? 'bg-green-500' : occupancy > 40 ? 'bg-teal-500' : 'bg-teal-400'}`} style={{ width: `${occupancy}%` }} >
{occupancy.toFixed(0)}%
{occupancy > 75 ? ( Alta Performance ) : occupancy > 30 ? ( Estável ) : ( Baixa Ocupação )}
{/* Relatório financeiro (H3.1 + H3.2) */}
)}
); };