agendapro/src/components/Dashboard.tsx

327 lines
14 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Calendar, Users, DollarSign, TrendingUp, Clock, ArrowUpRight, Play, CheckSquare, Loader } from 'lucide-react';
interface DashboardProps {
onNavigate: (page: string) => void;
}
const statusMap: Record<string, { label: string; cls: string }> = {
confirmed: { label: 'Confirmado', cls: 'badge-info' },
pending: { label: 'Pendente', cls: 'badge-warning' },
completed: { label: 'Concluído', cls: 'badge-success' },
cancelled: { label: 'Cancelado', cls: 'badge-danger' },
serving: { label: 'Em Atendimento', cls: 'badge-purple' },
in_progress: { label: 'Em Atendimento', cls: 'badge-purple' },
};
export default function Dashboard({ onNavigate }: DashboardProps) {
const [appointments, setAppointments] = useState<any[]>([]);
const [kpis, setKpis] = useState({ appointmentsToday: 0, revenueToday: 0, totalClients: 0, occupancy: 0, weekChange: 0 });
const [weeklyPerformance, setWeeklyPerformance] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [currentServingId, setCurrentServingId] = useState<string | null>(null);
useEffect(() => {
const fetchDashboard = async () => {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
try {
const res = await fetch(`/api/tenant/dashboard?slug=${slug}`);
if (res.ok) {
const data = await res.json();
setAppointments(data.appointments || []);
setKpis(data.kpis || { appointmentsToday: 0, revenueToday: 0, totalClients: 0, occupancy: 0, weekChange: 0 });
setWeeklyPerformance(data.weeklyPerformance || []);
}
} catch (err) {
console.error('Erro ao carregar dashboard:', err);
} finally {
setLoading(false);
}
};
fetchDashboard();
}, []);
const handleStartServing = async (id: string) => {
setCurrentServingId(id);
setAppointments(prev => prev.map(a => a.id === id ? { ...a, status: 'in_progress' } : a));
// Atualiza no banco
try {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
await fetch('/api/tenant/agenda', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status: 'in_progress', slug })
});
} catch (err) {
console.error('Erro ao iniciar atendimento:', err);
}
};
const handleFinishServing = async (id: string) => {
setCurrentServingId(null);
setAppointments(prev => prev.map(a => a.id === id ? { ...a, status: 'completed' } : a));
// Atualiza no banco
try {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
await fetch('/api/tenant/agenda', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status: 'completed', slug })
});
} catch (err) {
console.error('Erro ao finalizar atendimento:', err);
}
};
const servingApt = appointments.find(a => a.id === currentServingId || a.status === 'in_progress');
const activeQueue = appointments.filter(a => a.status !== 'completed' && a.status !== 'cancelled' && a.id !== servingApt?.id);
const nextInQueue = activeQueue[0];
const weekChangeLabel = kpis.weekChange > 0 ? `${kpis.weekChange}% vs semana anterior` : kpis.weekChange < 0 ? `${Math.abs(kpis.weekChange)}% vs semana anterior` : 'Sem dados da semana anterior';
if (loading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '300px', color: 'var(--text-muted)' }}>
<Loader size={24} className="spin" style={{ marginRight: 8 }} /> Carregando dashboard...
</div>
);
}
return (
<div className="slide-up">
<style>{`
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
.pulse {
animation: pulse 1.5s infinite !important;
background: #a855f7 !important;
color: white !important;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`}</style>
{/* Painel de Fila de Atendimento */}
<div className="card mb-6" style={{ background: 'var(--bg-secondary)', borderLeft: '4px solid var(--accent)', padding: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '20px', flexWrap: 'wrap' }}>
<div>
<span className="badge badge-purple" style={{ marginBottom: '8px', fontSize: '11px', fontWeight: 700 }}>Fila de Atendimento</span>
{servingApt ? (
<div>
<h3 style={{ fontSize: '18px', fontWeight: 800, color: 'var(--text-primary)' }}>
Atendendo agora: <span style={{ color: 'var(--accent)' }}>{servingApt.client}</span>
</h3>
<p style={{ fontSize: '13px', color: 'var(--text-secondary)', marginTop: '4px' }}>
{servingApt.service} com {servingApt.professional} R$ {Number(servingApt.price).toFixed(2)}
</p>
</div>
) : nextInQueue ? (
<div>
<h3 style={{ fontSize: '18px', fontWeight: 800, color: 'var(--text-primary)' }}>
Próximo da Fila: <span style={{ color: 'var(--accent)' }}>{nextInQueue.client}</span>
</h3>
<p style={{ fontSize: '13px', color: 'var(--text-secondary)', marginTop: '4px' }}>
Agendado para {nextInQueue.time} ({nextInQueue.service})
</p>
</div>
) : (
<div>
<h3 style={{ fontSize: '16px', fontWeight: 700, color: 'var(--text-muted)' }}>
{appointments.length === 0 ? 'Nenhum agendamento para hoje' : 'Todos os atendimentos do dia foram concluídos'}
</h3>
<p style={{ fontSize: '13px', color: 'var(--text-muted)', marginTop: '4px' }}>
{appointments.length === 0 ? 'Acesse a Agenda para criar novos agendamentos.' : 'Parabéns! Todos os clientes foram atendidos.'}
</p>
</div>
)}
</div>
<div>
{servingApt ? (
<button className="btn btn-danger" style={{ gap: '8px' }} onClick={() => handleFinishServing(servingApt.id)}>
<CheckSquare size={16} /> Finalizar Atendimento
</button>
) : nextInQueue ? (
<button className="btn btn-primary" style={{ gap: '8px' }} onClick={() => handleStartServing(nextInQueue.id)}>
<Play size={16} /> Iniciar Atendimento
</button>
) : null}
</div>
</div>
{servingApt && (
<div style={{ marginTop: '16px', paddingTop: '12px', borderTop: '1px solid var(--border-color)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '13px', flexWrap: 'wrap', gap: '10px' }}>
<span style={{ color: 'var(--text-secondary)' }}>
<strong>Próximo da Fila:</strong> {activeQueue[0] ? `${activeQueue[0].client} (às ${activeQueue[0].time})` : 'Ninguém aguardando'}
</span>
{activeQueue[0] && (
<button className="btn btn-ghost btn-sm" style={{ padding: '2px 8px', fontSize: '12px', color: 'var(--accent)', height: 'auto', minHeight: 'unset' }} onClick={() => {
handleFinishServing(servingApt.id);
handleStartServing(activeQueue[0].id);
}}>
Chamar Próximo
</button>
)}
</div>
)}
</div>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon purple"><Calendar size={24} /></div>
<div>
<div className="stat-value">{kpis.appointmentsToday}</div>
<div className="stat-label">Agendamentos Hoje</div>
<div className="stat-change up">{weekChangeLabel}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon green"><DollarSign size={24} /></div>
<div>
<div className="stat-value">R$ {kpis.revenueToday.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</div>
<div className="stat-label">Receita do Dia</div>
<div className="stat-change up">Valores confirmados</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon orange"><Users size={24} /></div>
<div>
<div className="stat-value">{kpis.totalClients}</div>
<div className="stat-label">Clientes Cadastrados</div>
<div className="stat-change up">Total ativo</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon blue"><TrendingUp size={24} /></div>
<div>
<div className="stat-value">{kpis.occupancy}%</div>
<div className="stat-label">Taxa de Ocupação</div>
<div className="stat-change up">Hoje</div>
</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 360px', gap: '24px' }}>
<div className="card">
<div className="card-header">
<h2 className="card-title" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Clock size={18} /> Agendamentos de Hoje
</h2>
<button className="btn btn-sm btn-secondary" onClick={() => onNavigate('agenda')}>
Ver Agenda <ArrowUpRight size={14} />
</button>
</div>
<div className="table-container">
<table>
<thead>
<tr>
<th>Horário</th>
<th>Cliente</th>
<th>Serviço</th>
<th>Profissional</th>
<th>Status</th>
<th>Valor</th>
</tr>
</thead>
<tbody>
{appointments.map(apt => {
const isServing = apt.status === 'in_progress';
const isCompleted = apt.status === 'completed';
const st = statusMap[apt.status] || { label: apt.status, cls: 'badge-secondary' };
return (
<tr key={apt.id} style={{ opacity: isCompleted ? 0.5 : 1, transition: 'opacity 0.3s' }}>
<td style={{ fontWeight: 600 }}>{apt.time}</td>
<td>
<div className="flex-gap gap-sm">
<div className="avatar" style={{ width: 28, height: 28, fontSize: 11 }}>
{apt.client.split(' ').map((n: string) => n[0]).join('').substring(0, 2)}
</div>
{apt.client}
</div>
</td>
<td>{apt.service}</td>
<td>{apt.professional}</td>
<td>
<span className={`badge ${st.cls} ${isServing ? 'pulse' : ''}`}>
{st.label}
</span>
</td>
<td style={{ fontWeight: 600 }}>R$ {Number(apt.price).toFixed(2)}</td>
</tr>
);
})}
{appointments.length === 0 && (
<tr>
<td colSpan={6} style={{ textAlign: 'center', padding: '40px', color: 'var(--text-muted)' }}>
Nenhum agendamento para hoje. Crie um novo na Agenda.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div className="card">
<h3 className="card-title mb-4">Desempenho Semanal</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{weeklyPerformance.length > 0 ? weeklyPerformance.map((d: any) => (
<div key={d.label} style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span style={{ width: '30px', fontSize: '12px', color: 'var(--text-secondary)' }}>{d.label}</span>
<div style={{ flex: 1, height: '8px', background: 'var(--bg-elevated)', borderRadius: '4px', overflow: 'hidden' }}>
<div style={{
width: `${d.value}%`, height: '100%',
background: 'var(--gradient-primary)',
borderRadius: '4px',
transition: 'width 1s ease'
}} />
</div>
<span style={{ fontSize: '12px', fontWeight: 600, width: '35px' }}>{d.count || 0}</span>
</div>
)) : (
<p style={{ fontSize: '13px', color: 'var(--text-muted)', textAlign: 'center' }}>Sem dados suficientes</p>
)}
</div>
</div>
<div className="card">
<h3 className="card-title mb-4">Ações Rápidas</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{[
{ label: 'Novo Agendamento', desc: 'Agendar um novo cliente', page: 'agenda' },
{ label: 'Cadastrar Cliente', desc: 'Adicionar cliente à base', page: 'clients' },
{ label: 'Novo Serviço', desc: 'Cadastrar um novo serviço', page: 'services' },
].map((action, i) => (
<div key={i} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '10px 14px', background: 'var(--bg-primary)', borderRadius: 'var(--radius-sm)',
border: '1px solid var(--border-color)'
}}>
<div>
<div style={{ fontSize: '14px', fontWeight: 600 }}>{action.label}</div>
<div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>{action.desc}</div>
</div>
<button className="btn btn-sm btn-primary" onClick={() => onNavigate(action.page)}>Ir</button>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}