Files
scoreodonto.com/frontend/views/Dashboard.tsx
T
VPS 4 Builder 2f3c0420e3 feat(routing): switch from hash routing to HTML5 History API
Remove all window.location.hash assignments and hashchange events.
Navigate with history.pushState; listen on popstate. Fix base href to
'/' so assets resolve correctly on deep paths. Update magic link URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 04:44:53 +02:00

289 lines
12 KiB
TypeScript

import React, { useMemo } from 'react';
import {
Users,
DollarSign,
TrendingUp,
Megaphone,
Calendar,
ChevronRight,
ArrowUpRight,
ArrowDownRight,
Plus,
Clock,
ArrowRight
} from 'lucide-react';
import { PageHeader } from '../components/PageHeader.tsx';
import { useHybridBackend } from '../hooks/useHybridBackend.ts';
import { HybridBackend } from '../services/backend.ts';
import { NotificationCenter } from '../components/NotificationCenter.tsx';
const StatCard: React.FC<{
title: string;
value: string | number;
icon: React.ReactNode;
trend?: string;
isPositive?: boolean;
color: 'blue' | 'green' | 'purple' | 'amber';
onClick?: () => void;
}> = ({ title, value, icon, trend, isPositive, color, onClick }) => {
const bgLight = {
blue: 'bg-blue-50 text-blue-600',
green: 'bg-emerald-50 text-emerald-600',
purple: 'bg-indigo-50 text-indigo-600',
amber: 'bg-amber-50 text-amber-600'
};
return (
<div
onClick={onClick}
className={`bg-white rounded-[2rem] p-8 border border-gray-100 shadow-[0_10px_40px_-15px_rgba(0,0,0,0.05)] hover:shadow-[0_20px_50px_-20px_rgba(0,0,0,0.1)] transition-all duration-500 group relative overflow-hidden ${onClick ? 'cursor-pointer' : ''}`}
>
<div className="absolute top-0 right-0 w-32 h-32 opacity-[0.03] -mr-8 -mt-8 rounded-full bg-current transform group-hover:scale-110 transition-transform duration-700"></div>
<div className="flex justify-between items-start relative z-10">
<div className="space-y-4">
<div className={`w-12 h-12 ${bgLight[color]} rounded-2xl flex items-center justify-center mb-2 group-hover:scale-110 transition-transform`}>
{icon}
</div>
<div>
<p className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em]">{title}</p>
<h3 className="text-3xl font-black text-gray-900 tracking-tight mt-1">{value}</h3>
</div>
{trend && (
<div className={`flex items-center gap-1.5 text-xs font-bold ${isPositive ? 'text-emerald-600' : 'text-rose-600'}`}>
<div className={`p-0.5 rounded-full ${isPositive ? 'bg-emerald-100' : 'bg-rose-100'}`}>
{isPositive ? <ArrowUpRight size={12} /> : <ArrowDownRight size={12} />}
</div>
{trend}
</div>
)}
</div>
</div>
</div>
);
};
export const Dashboard: React.FC = () => {
const { data, isLoading } = useHybridBackend<any>(HybridBackend.getDashboardStats);
const growthData = useMemo(() => {
if (!data?.growth || data.growth.length === 0) {
return Array(6).fill(null).map((_, i) => ({
label: `MÊS ${i + 1}`,
receita: 0,
despesa: 0,
heightP: '0%',
heightR: '0%'
}));
}
const maxVal = Math.max(...data.growth.map((g: any) => Math.max(parseFloat(g.receita), parseFloat(g.despesa))), 1000);
return data.growth.map((g: any) => {
const [year, month] = g.mes.split('-');
const date = new Date(parseInt(year), parseInt(month) - 1);
const label = date.toLocaleString('pt-BR', { month: 'short' }).toUpperCase();
return {
label,
receita: parseFloat(g.receita),
despesa: parseFloat(g.despesa),
// Proporção para o gráfico
heightR: `${(parseFloat(g.receita) / maxVal) * 100}%`,
heightD: `${(parseFloat(g.despesa) / maxVal) * 100}%`
};
}).slice(-6); // Garantir últimos 6
}, [data]);
const currentRole = HybridBackend.getCurrentRole();
const showFinancials = ['admin', 'donoclinica', 'funcionario'].includes(currentRole);
if (isLoading) {
return (
<div className="flex items-center justify-center h-96">
<div className="w-12 h-12 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
</div>
);
}
const stats = data?.stats || {
totalPacientes: 0,
totalAgendamentos: 0,
totalLeads: 0,
faturamentoTotal: 0,
despesasTotal: 0
};
const recent = data?.recentAgendamentos || [];
return (
<div className="space-y-10 pb-10">
<header className="flex flex-col md:flex-row md:items-end justify-between gap-6">
<div>
<PageHeader
title="DASHBOARD ANALÍTICO"
description="Informações consolidadas em tempo real da sua clínica."
/>
</div>
<div className="flex items-center gap-3">
{(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') && (
<button
onClick={() => history.pushState({}, '', '/relatorios')}
className="bg-white text-gray-900 border border-gray-200 px-6 py-3 rounded-2xl text-xs font-black uppercase tracking-widest hover:bg-gray-50 transition-all flex items-center gap-2 shadow-sm"
>
<Clock size={16} /> Relatórios
</button>
)}
{(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'funcionario') && (
<button
onClick={() => history.pushState({}, '', '/pacientes')}
className="bg-blue-600 text-white px-6 py-3 rounded-2xl text-xs font-black uppercase tracking-widest hover:bg-blue-700 hover:shadow-xl hover:shadow-blue-200 transition-all flex items-center gap-2 shadow-lg shadow-blue-100"
>
<Plus size={16} /> Novo Paciente
</button>
)}
<NotificationCenter />
</div>
</header>
{/* Stats Grid */}
<div className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-${showFinancials ? '4' : '2'} gap-8`}>
<StatCard
title="Pacientes Ativos"
value={stats.totalPacientes}
icon={<Users size={24} />}
trend="+12% este mês"
isPositive={true}
color="blue"
onClick={() => history.pushState({}, '', '/pacientes')}
/>
{showFinancials && (
<StatCard
title="Faturamento Bruto"
value={new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(stats.faturamentoTotal)}
icon={<DollarSign size={24} />}
trend="+5.4% vs mês ant."
isPositive={true}
color="green"
onClick={() => history.pushState({}, '', '/financeiro')}
/>
)}
<StatCard
title="Agendamentos"
value={stats.totalAgendamentos}
icon={<Calendar size={24} />}
trend="82% de ocupação"
isPositive={true}
color="purple"
onClick={() => history.pushState({}, '', '/agenda')}
/>
{showFinancials && (
<StatCard
title="Novas Leads"
value={stats.totalLeads}
icon={<Megaphone size={24} />}
trend="-2% vs ontem"
isPositive={false}
color="amber"
onClick={() => history.pushState({}, '', '/leads')}
/>
)}
</div>
<div className={`grid lg:grid-cols-${showFinancials ? '3' : '1'} gap-8`}>
{/* Growth Chart Area */}
{showFinancials && (
<div className="lg:col-span-2 bg-white rounded-[2.5rem] p-10 border border-gray-100 shadow-[0_20px_50px_-15px_rgba(0,0,0,0.03)]">
<div className="flex justify-between items-center mb-10">
<div>
<h4 className="text-xl font-black text-gray-900 uppercase tracking-tighter italic">Crescimento Mensal</h4>
<p className="text-xs font-bold text-gray-400 mt-1 uppercase">Receitas vs Despesas Reais</p>
</div>
<select
id="chart-period-dashboard"
name="chart-period"
className="bg-gray-50 border-none text-[10px] font-black uppercase tracking-widest px-4 py-2 rounded-xl focus:ring-2 focus:ring-blue-100 cursor-pointer"
>
<option value="6m">Últimos 6 meses</option>
<option value="year">Este Ano</option>
</select>
</div>
<div className="h-64 flex items-end justify-between gap-4 px-4">
{growthData.map((d: any, i: number) => (
<div key={i} className="flex-1 flex flex-col items-center gap-3 group">
<div className="w-full flex justify-center gap-1 items-end h-[200px] bg-slate-50/50 rounded-2xl p-1">
{/* Barra Despesa */}
<div
title={`Despesa: R$ ${d.despesa}`}
className="w-1/3 bg-rose-200 rounded-lg group-hover:bg-rose-300 transition-all duration-500 origin-bottom"
style={{ height: d.heightD || '5%' }}
></div>
{/* Barra Receita */}
<div
title={`Receita: R$ ${d.receita}`}
className="w-1/3 bg-blue-500 rounded-lg group-hover:bg-blue-600 shadow-sm group-hover:shadow-blue-200 transition-all duration-500 origin-bottom"
style={{ height: d.heightR || '5%' }}
></div>
</div>
<span className="text-[10px] font-black text-gray-400 uppercase tracking-tighter">{d.label}</span>
</div>
))}
</div>
<div className="flex justify-center gap-6 mt-6">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-500 rounded-sm"></div>
<span className="text-[10px] font-bold text-gray-500 uppercase">Receitas</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-rose-200 rounded-sm"></div>
<span className="text-[10px] font-bold text-gray-500 uppercase">Despesas</span>
</div>
</div>
</div>
)}
{/* Recent Activity */}
<div className="bg-gray-900 rounded-[2.5rem] p-10 shadow-2xl shadow-blue-200 overflow-hidden relative">
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-600/20 blur-[80px] rounded-full"></div>
<div className="relative z-10 h-full flex flex-col">
<h4 className="text-xl font-black text-white uppercase tracking-tighter italic mb-8">Últimos Agendamentos</h4>
<div className="space-y-6 flex-1 overflow-y-auto custom-scrollbar pr-2">
{recent.length > 0 ? recent.map((app: any) => (
<div
key={app.id}
onClick={() => history.pushState({}, '', '/agenda')}
className="flex items-center gap-4 group cursor-pointer"
>
<div className="w-12 h-12 rounded-2xl bg-white/10 flex items-center justify-center text-blue-400 group-hover:bg-blue-600 group-hover:text-white transition-all duration-300 flex-shrink-0">
<Calendar size={20} />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-black text-white uppercase tracking-tight truncate">{app.pacientenome || 'Paciente'}</div>
<div className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mt-0.5 truncate">
{app.dentistaNome || 'Dr. Dentista'} {new Date(app.start_time).toLocaleDateString('pt-BR')} {new Date(app.start_time).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
<ChevronRight size={16} className="text-gray-600 group-hover:text-white group-hover:translate-x-1 transition-all" />
</div>
)) : (
<div className="flex flex-col items-center justify-center py-10 opacity-40">
<Calendar size={40} className="text-white mb-2" />
<p className="text-white text-[10px] font-bold uppercase tracking-widest">Nenhum agendamento recente.</p>
</div>
)}
</div>
<button
onClick={() => history.pushState({}, '', '/agenda')}
className="w-full mt-8 bg-white/5 border border-white/10 text-white py-4 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-white/10 transition-all flex items-center justify-center gap-2 group"
>
Ver Agenda Completa <ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
</button>
</div>
</div>
</div>
</div>
);
};