232 lines
16 KiB
TypeScript
232 lines
16 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { TrendingUp, Users, Calendar, DollarSign, ArrowUpRight, ArrowDownRight, Printer, Filter, Loader2, Download, BarChart3, PieChart, Activity } from 'lucide-react';
|
|
import { HybridBackend } from '../services/backend.ts';
|
|
import { useHybridBackend } from '../hooks/useHybridBackend.ts';
|
|
import { PageHeader } from '../components/PageHeader.tsx';
|
|
|
|
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<ReportData>(() =>
|
|
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 (
|
|
<div className="space-y-8 animate-in fade-in duration-500 pb-10">
|
|
<PageHeader title="RELATÓRIOS & PERFORMANCE">
|
|
<div className="flex items-center gap-3">
|
|
<button onClick={() => window.print()} className="bg-white border border-gray-200 text-gray-600 p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2 text-xs font-bold transition-all shadow-sm">
|
|
<Printer size={16} /> <span className="hidden sm:inline">IMPRIMIR</span>
|
|
</button>
|
|
<button onClick={exportToCSV} className="bg-white border border-gray-200 text-gray-600 p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2 text-xs font-bold transition-all shadow-sm">
|
|
<Download size={16} /> <span className="hidden sm:inline">EXPORTAR</span>
|
|
</button>
|
|
</div>
|
|
</PageHeader>
|
|
|
|
{/* Filters */}
|
|
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 flex flex-wrap items-end gap-6">
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest flex items-center gap-2">
|
|
<Calendar size={12} /> INÍCIO DO PERÍODO
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={startDate}
|
|
onChange={(e) => 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-blue-100 outline-none transition-all"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest flex items-center gap-2">
|
|
<Calendar size={12} /> FIM DO PERÍODO
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={endDate}
|
|
onChange={(e) => 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-blue-100 outline-none transition-all"
|
|
/>
|
|
</div>
|
|
<button onClick={refresh} className="bg-blue-600 text-white px-6 py-3 rounded-xl font-black text-xs uppercase hover:bg-blue-700 transition-all shadow-lg shadow-blue-200 flex items-center gap-2">
|
|
<Filter size={16} /> FILTRAR
|
|
</button>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="flex flex-col items-center justify-center py-20 gap-4">
|
|
<Loader2 className="animate-spin text-blue-600" size={40} />
|
|
<p className="text-xs font-black text-gray-400 uppercase tracking-widest">Processando dados clínicos...</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-8">
|
|
{/* Financial Summary */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<div className="bg-white p-6 rounded-2xl border border-gray-100 shadow-sm overflow-hidden relative group">
|
|
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:scale-110 transition-transform">
|
|
<DollarSign size={80} className="text-green-600" />
|
|
</div>
|
|
<h4 className="text-[10px] font-black text-gray-400 uppercase tracking-widest">RECEITA NO PERÍODO</h4>
|
|
<div className="mt-2 flex items-baseline gap-2">
|
|
<span className="text-2xl font-black text-gray-900">R$ {receita.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</span>
|
|
<span className="text-green-500 text-[10px] font-black flex items-center"><ArrowUpRight size={12} /> +2.4%</span>
|
|
</div>
|
|
<div className="mt-4 w-full bg-gray-50 h-1.5 rounded-full overflow-hidden">
|
|
<div className="bg-green-500 h-full w-[70%]"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-6 rounded-2xl border border-gray-100 shadow-sm overflow-hidden relative group">
|
|
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:scale-110 transition-transform">
|
|
<TrendingUp size={80} className="text-red-600" />
|
|
</div>
|
|
<h4 className="text-[10px] font-black text-gray-400 uppercase tracking-widest">DESPESA NO PERÍODO</h4>
|
|
<div className="mt-2 flex items-baseline gap-2">
|
|
<span className="text-2xl font-black text-gray-900">R$ {despesa.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</span>
|
|
<span className="text-red-500 text-[10px] font-black flex items-center"><ArrowDownRight size={12} /> -1.2%</span>
|
|
</div>
|
|
<div className="mt-4 w-full bg-gray-50 h-1.5 rounded-full overflow-hidden">
|
|
<div className="bg-red-500 h-full w-[30%]"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-6 rounded-2xl border border-gray-100 shadow-sm overflow-hidden relative group">
|
|
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:scale-110 transition-transform">
|
|
<Activity size={80} className="text-blue-600" />
|
|
</div>
|
|
<h4 className="text-[10px] font-black text-gray-400 uppercase tracking-widest">SALDO OPERACIONAL</h4>
|
|
<div className="mt-2">
|
|
<span className={`text-2xl font-black ${saldo >= 0 ? 'text-blue-600' : 'text-red-600'}`}>
|
|
R$ {saldo.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
|
</span>
|
|
</div>
|
|
<p className="mt-4 text-[10px] font-bold text-gray-400">LIQUIDEZ ATUAL DA UNIDADE</p>
|
|
</div>
|
|
|
|
<div className="bg-white p-6 rounded-2xl border border-gray-100 shadow-sm overflow-hidden relative group">
|
|
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:scale-110 transition-transform">
|
|
<Users size={80} className="text-purple-600" />
|
|
</div>
|
|
<h4 className="text-[10px] font-black text-gray-400 uppercase tracking-widest">TRANSAÇÕES</h4>
|
|
<div className="mt-2">
|
|
<span className="text-2xl font-black text-gray-900">{data?.financeiro?.totalTransacoes || 0}</span>
|
|
</div>
|
|
<p className="mt-4 text-[10px] font-bold text-gray-400">MOVIMENTAÇÕES FINANCEIRAS</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Productivity Table */}
|
|
<div className="bg-white rounded-3xl border border-gray-100 shadow-xl overflow-hidden">
|
|
<div className="px-8 py-6 bg-gradient-to-r from-gray-50 to-white border-b border-gray-100 flex justify-between items-center">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg shadow-blue-200">
|
|
<BarChart3 size={20} />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-black text-gray-900 uppercase">PRODUTIVIDADE POR DENTISTA</h3>
|
|
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest">DESEMPENHO CLÍNICO NO PERÍODO</p>
|
|
</div>
|
|
</div>
|
|
<div className="hidden sm:flex items-center gap-2 px-4 py-2 bg-blue-50 rounded-full">
|
|
<PieChart size={16} className="text-blue-600" />
|
|
<span className="text-[10px] font-black text-blue-600 uppercase">Visão por Especialista</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left">
|
|
<thead className="bg-gray-50/50">
|
|
<tr>
|
|
<th className="px-8 py-5 text-[10px] font-black text-gray-400 uppercase tracking-widest">ESPECIALISTA</th>
|
|
<th className="px-8 py-5 text-[10px] font-black text-gray-400 uppercase tracking-widest">TOTAL APPTS</th>
|
|
<th className="px-8 py-5 text-[10px] font-black text-gray-400 uppercase tracking-widest">EFETIVADOS</th>
|
|
<th className="px-8 py-5 text-[10px] font-black text-gray-400 uppercase tracking-widest">FALTAS</th>
|
|
<th className="px-8 py-5 text-[10px] font-black text-gray-400 uppercase tracking-widest">TAXA OCUPAÇÃO</th>
|
|
<th className="px-8 py-5 text-[10px] font-black text-gray-400 uppercase tracking-widest">STATUS</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{(data?.producao || []).map((p, idx) => {
|
|
const occupancy = p.totalAgendamentos > 0 ? (p.concluidos / p.totalAgendamentos) * 100 : 0;
|
|
return (
|
|
<tr key={idx} className="hover:bg-blue-50/30 transition-colors group">
|
|
<td className="px-8 py-5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center text-gray-400 font-black text-xs group-hover:bg-blue-600 group-hover:text-white transition-all">
|
|
{p.dentista.substring(0, 2).toUpperCase()}
|
|
</div>
|
|
<span className="font-bold text-gray-900 uppercase text-xs">{p.dentista}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-8 py-5 font-bold text-gray-600 text-xs">{p.totalAgendamentos}</td>
|
|
<td className="px-8 py-5 font-bold text-green-600 text-xs">{p.concluidos}</td>
|
|
<td className="px-8 py-5 font-bold text-red-500 text-xs">{p.faltas}</td>
|
|
<td className="px-8 py-5">
|
|
<div className="flex items-center gap-3 min-w-[120px]">
|
|
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden shadow-inner">
|
|
<div
|
|
className={`h-full transition-all duration-1000 ${occupancy > 70 ? 'bg-green-500' : occupancy > 40 ? 'bg-blue-500' : 'bg-orange-400'}`}
|
|
style={{ width: `${occupancy}%` }}
|
|
></div>
|
|
</div>
|
|
<span className="text-[10px] font-black text-gray-400">{occupancy.toFixed(0)}%</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-8 py-5">
|
|
{occupancy > 75 ? (
|
|
<span className="px-2 py-1 bg-green-50 text-green-600 text-[9px] font-black rounded-lg uppercase border border-green-100">Alta Performance</span>
|
|
) : occupancy > 30 ? (
|
|
<span className="px-2 py-1 bg-blue-50 text-blue-600 text-[9px] font-black rounded-lg uppercase border border-blue-100">Estável</span>
|
|
) : (
|
|
<span className="px-2 py-1 bg-gray-50 text-gray-400 text-[9px] font-black rounded-lg uppercase border border-gray-100">Baixa Ocupação</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|