Update dependencies and enhance Dashboard and Landing pages with new features and UI improvements

This commit is contained in:
2026-01-14 09:52:19 +00:00
parent 58e5889b89
commit 4339b79455
7 changed files with 2062 additions and 997 deletions

View File

@@ -34,7 +34,12 @@ import {
ChevronDown,
UserPlus,
Globe,
TrendingUp,
HelpCircle,
MessageCircle,
Settings,
Store,
Clock,
ArrowRight,
} from 'lucide-react';
const periods: Record<string, (date: Date) => boolean> = {
@@ -107,34 +112,39 @@ export default function Dashboard() {
// Agendamentos concluídos (histórico)
const completedAppointments = allShopAppointments.filter((a) => a.status === 'concluido');
// Estatísticas para lista de marcações
const todayAppointments = appointments.filter((a) => {
// Estatísticas para lista de marcações (do dia selecionado)
const selectedDateAppointments = appointments.filter((a) => {
if (a.shopId !== shop.id) return false;
const aptDate = new Date(a.date.replace(' ', 'T'));
const today = new Date();
return (
aptDate.getDate() === today.getDate() &&
aptDate.getMonth() === today.getMonth() &&
aptDate.getFullYear() === today.getFullYear()
aptDate.getDate() === selectedDate.getDate() &&
aptDate.getMonth() === selectedDate.getMonth() &&
aptDate.getFullYear() === selectedDate.getFullYear()
);
});
const totalBookingsToday = todayAppointments.filter((a) => includeCancelled || a.status !== 'cancelado').length;
const totalBookingsToday = selectedDateAppointments.filter((a) => includeCancelled || a.status !== 'cancelado').length;
const newClientsToday = useMemo(() => {
const clientIds = new Set(todayAppointments.map((a) => a.customerId));
const clientIds = new Set(selectedDateAppointments.map((a) => a.customerId));
return clientIds.size;
}, [todayAppointments]);
const onlineBookingsToday = todayAppointments.filter((a) => a.status !== 'cancelado').length;
}, [selectedDateAppointments]);
const onlineBookingsToday = selectedDateAppointments.filter((a) => a.status !== 'cancelado').length;
const occupancyRate = useMemo(() => {
// Calcular ocupação baseada em slots disponíveis (8h-18h = 20 slots de 30min)
const totalSlots = 20;
const bookedSlots = todayAppointments.filter((a) => a.status !== 'cancelado').length;
const bookedSlots = selectedDateAppointments.filter((a) => a.status !== 'cancelado').length;
return Math.round((bookedSlots / totalSlots) * 100);
}, [todayAppointments]);
}, [selectedDateAppointments]);
// Comparação com semana passada (simplificado - sempre 0% por enquanto)
const comparisonPercent = 0;
// Filtrar agendamentos para lista
const filteredAppointments = useMemo(() => {
let filtered = shopAppointments;
let filtered = selectedDateAppointments;
if (!includeCancelled) {
filtered = filtered.filter((a) => a.status !== 'cancelado');
@@ -157,7 +167,8 @@ export default function Dashboard() {
}
return filtered;
}, [shopAppointments, includeCancelled, searchQuery, shop.services, shop.barbers, users]);
}, [selectedDateAppointments, includeCancelled, searchQuery, shop.services, shop.barbers, users]);
// Pedidos apenas com produtos (não serviços)
const shopOrders = orders.filter(
(o) => o.shopId === shop.id && periodMatch(new Date(o.createdAt)) && o.items.some((item) => item.type === 'product')
@@ -295,109 +306,238 @@ export default function Dashboard() {
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Stats Cards */}
<div className="grid md:grid-cols-4 gap-4">
<Card className="p-5 bg-gradient-to-br from-amber-50 to-white">
<div className="flex items-center justify-between mb-2">
<div className="p-2 bg-amber-500 rounded-lg text-white">
<TrendingUp size={20} />
{/* Saudação */}
<div className="space-y-1">
<h1 className="text-2xl font-bold text-slate-900">
{new Date().toLocaleDateString('pt-PT', { weekday: 'long', day: 'numeric', month: 'long' })}
</h1>
<p className="text-slate-600">
{(() => {
const hour = new Date().getHours();
if (hour < 12) return 'Bom dia';
if (hour < 18) return 'Boa tarde';
return 'Boa noite';
})()}, {user?.name}
</p>
</div>
{/* Stats Cards Principais */}
<div className="grid md:grid-cols-3 gap-4">
<Card className="p-6">
<div className="flex items-center justify-between mb-3">
<div className="p-3 bg-indigo-500 rounded-lg text-white">
<Calendar size={24} />
</div>
<Badge color="amber" variant="soft">Período</Badge>
</div>
<p className="text-sm text-slate-600 mb-1">Faturamento</p>
<p className="text-2xl font-bold text-amber-700">{currency(totalRevenue)}</p>
<p className="text-sm text-slate-600 mb-1">Total de reservas</p>
<p className="text-3xl font-bold text-slate-900">{allShopAppointments.length}</p>
<p className="text-xs text-slate-500 mt-2">Reservas da plataforma: {allShopAppointments.length}</p>
</Card>
<Card className="p-5">
<div className="flex items-center justify-between mb-2">
<div className="p-2 bg-blue-500 rounded-lg text-white">
<Calendar size={20} />
<Card className="p-6">
<div className="flex items-center justify-between mb-3">
<div className="p-3 bg-blue-500 rounded-lg text-white">
<Globe size={24} />
</div>
<Badge color="amber" variant="soft">{pendingAppts}</Badge>
</div>
<p className="text-sm text-slate-600 mb-1">Pendentes</p>
<p className="text-2xl font-bold text-slate-900">{pendingAppts}</p>
<p className="text-sm text-slate-600 mb-1">Reservas online</p>
<p className="text-3xl font-bold text-slate-900">{allShopAppointments.length}</p>
<p className="text-xs text-slate-500 mt-2">Marcações feitas pela plataforma</p>
</Card>
<Card className="p-5">
<div className="flex items-center justify-between mb-2">
<div className="p-2 bg-emerald-500 rounded-lg text-white">
<Calendar size={20} />
<Card className="p-6">
<div className="flex items-center justify-between mb-3">
<div className="p-3 bg-green-500 rounded-lg text-white">
<UserPlus size={24} />
</div>
<Badge color="green" variant="soft">{confirmedAppts}</Badge>
</div>
<p className="text-sm text-slate-600 mb-1">Confirmados</p>
<p className="text-2xl font-bold text-slate-900">{confirmedAppts}</p>
</Card>
<Card className={`p-5 ${lowStock.length > 0 ? 'bg-amber-50 border-amber-200' : ''}`}>
<div className="flex items-center justify-between mb-2">
<div className={`p-2 rounded-lg text-white ${lowStock.length > 0 ? 'bg-amber-500' : 'bg-slate-500'}`}>
<AlertTriangle size={20} />
</div>
{lowStock.length > 0 && <Badge color="amber" variant="solid">{lowStock.length}</Badge>}
</div>
<p className="text-sm text-slate-600 mb-1">Stock baixo</p>
<p className={`text-2xl font-bold ${lowStock.length > 0 ? 'text-amber-700' : 'text-slate-900'}`}>{lowStock.length}</p>
<p className="text-sm text-slate-600 mb-1">Novos clientes</p>
<p className="text-3xl font-bold text-slate-900">
{new Set(allShopAppointments.map(a => a.customerId)).size}
</p>
<p className="text-xs text-slate-500 mt-2">Clientes únicos</p>
</Card>
</div>
{/* Charts */}
<div className="grid md:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-bold text-slate-900 mb-4">Serviços vs Produtos</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={comparisonData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<Tooltip />
<Bar dataKey="value" fill="#f59e0b" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</Card>
<div className="space-y-4">
<Card className="p-5">
<h3 className="text-base font-bold text-slate-900 mb-3">Top 5 Serviços</h3>
<div className="space-y-2">
{topServices.length > 0 ? (
topServices.map((s, idx) => (
<div key={s.name} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<span className="text-slate-400 font-bold">#{idx + 1}</span>
<span className="text-slate-700">{s.name}</span>
</div>
<Badge color="amber">{s.qty} vendas</Badge>
{/* Layout em duas colunas */}
<div className="grid md:grid-cols-3 gap-6">
{/* Coluna Principal - Esquerda */}
<div className="md:col-span-2 space-y-6">
{/* Reservas de Hoje */}
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-indigo-100 rounded-lg">
<Search size={20} className="text-indigo-600" />
</div>
<div>
<h3 className="text-lg font-bold text-slate-900">Reservas de hoje</h3>
<p className="text-sm text-slate-600">Verá aqui as reservas de hoje assim que chegarem</p>
</div>
</div>
{(() => {
const today = new Date();
const todayAppts = allShopAppointments.filter(a => {
const aptDate = new Date(a.date.replace(' ', 'T'));
return aptDate.toDateString() === today.toDateString();
});
if (todayAppts.length === 0) {
return (
<div className="text-center py-8">
<Calendar size={48} className="mx-auto text-slate-300 mb-3" />
<p className="text-slate-600 font-medium mb-2">Sem reservas hoje</p>
<Button variant="outline" size="sm" onClick={() => setActiveTab('appointments')}>
Ir para o calendário
<ArrowRight size={16} className="ml-2" />
</Button>
</div>
))
) : (
<p className="text-sm text-slate-500">Sem vendas no período</p>
)}
);
}
return (
<div className="space-y-2">
{todayAppts.slice(0, 3).map(a => {
const svc = shop.services.find(s => s.id === a.serviceId);
const barber = shop.barbers.find(b => b.id === a.barberId);
const customer = users.find(u => u.id === a.customerId);
const aptDate = new Date(a.date.replace(' ', 'T'));
const timeStr = aptDate.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' });
return (
<div key={a.id} className="flex items-center justify-between p-3 border border-slate-200 rounded-lg hover:bg-slate-50">
<div className="flex-1">
<p className="font-semibold text-slate-900">{customer?.name || 'Cliente'}</p>
<p className="text-sm text-slate-600">{timeStr} · {svc?.name || 'Serviço'}</p>
</div>
<Badge color={a.status === 'pendente' ? 'amber' : a.status === 'confirmado' ? 'green' : 'red'}>
{a.status}
</Badge>
</div>
);
})}
{todayAppts.length > 3 && (
<Button variant="outline" size="sm" className="w-full" onClick={() => setActiveTab('appointments')}>
Ver todas ({todayAppts.length})
</Button>
)}
</div>
);
})()}
</Card>
</div>
{/* Coluna Lateral - Direita */}
<div className="space-y-6">
{/* Ajuda */}
<Card className="p-6">
<h3 className="text-lg font-bold text-slate-900 mb-4">Tem alguma pergunta?</h3>
<div className="space-y-3">
<button className="w-full flex items-center gap-3 p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors text-left">
<div className="p-2 bg-indigo-100 rounded-lg">
<HelpCircle size={18} className="text-indigo-600" />
</div>
<span className="font-medium text-slate-900">Centro de ajuda</span>
</button>
<button className="w-full flex items-center gap-3 p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors text-left">
<div className="p-2 bg-blue-100 rounded-lg">
<MessageCircle size={18} className="text-blue-600" />
</div>
<span className="font-medium text-slate-900">Contacte-nos</span>
</button>
</div>
</Card>
<Card className="p-5">
<h3 className="text-base font-bold text-slate-900 mb-3">Top 5 Produtos</h3>
<div className="space-y-2">
{topProducts.length > 0 ? (
topProducts.map((p, idx) => (
<div key={p.name} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<span className="text-slate-400 font-bold">#{idx + 1}</span>
<span className="text-slate-700">{p.name}</span>
</div>
<Badge color="amber">{p.qty} vendas</Badge>
</div>
))
) : (
<p className="text-sm text-slate-500">Sem vendas no período</p>
)}
{/* Atalhos */}
<Card className="p-6">
<h3 className="text-lg font-bold text-slate-900 mb-4">Atalhos</h3>
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => setActiveTab('services')}
className="flex flex-col items-center gap-2 p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors"
>
<div className="p-2 bg-indigo-100 rounded-lg">
<Scissors size={20} className="text-indigo-600" />
</div>
<span className="text-sm font-medium text-slate-900">Serviços</span>
</button>
<button
onClick={() => setActiveTab('appointments')}
className="flex flex-col items-center gap-2 p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors"
>
<div className="p-2 bg-blue-100 rounded-lg">
<Users size={20} className="text-blue-600" />
</div>
<span className="text-sm font-medium text-slate-900">Clientes</span>
</button>
<button
onClick={() => setActiveTab('appointments')}
className="flex flex-col items-center gap-2 p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors"
>
<div className="p-2 bg-green-100 rounded-lg">
<Calendar size={20} className="text-green-600" />
</div>
<span className="text-sm font-medium text-slate-900">Calendário</span>
</button>
<button className="flex flex-col items-center gap-2 p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors">
<div className="p-2 bg-purple-100 rounded-lg">
<Settings size={20} className="text-purple-600" />
</div>
<span className="text-sm font-medium text-slate-900">Definições</span>
</button>
</div>
</Card>
{/* Próximos Agendamentos */}
<Card className="p-6">
<h3 className="text-lg font-bold text-slate-900 mb-4">Seguinte</h3>
{(() => {
const upcoming = allShopAppointments
.filter(a => {
const aptDate = new Date(a.date.replace(' ', 'T'));
return aptDate > new Date() && a.status !== 'cancelado';
})
.sort((a, b) => {
const dateA = new Date(a.date.replace(' ', 'T'));
const dateB = new Date(b.date.replace(' ', 'T'));
return dateA.getTime() - dateB.getTime();
})
.slice(0, 3);
if (upcoming.length === 0) {
return (
<p className="text-sm text-slate-500 text-center py-4">Sem agendamentos futuros</p>
);
}
return (
<div className="space-y-3">
{upcoming.map(a => {
const svc = shop.services.find(s => s.id === a.serviceId);
const barber = shop.barbers.find(b => b.id === a.barberId);
const customer = users.find(u => u.id === a.customerId);
const aptDate = new Date(a.date.replace(' ', 'T'));
const timeStr = aptDate.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' });
return (
<div key={a.id} className="flex items-start gap-3 p-3 border border-slate-200 rounded-lg">
<div className="p-1.5 bg-indigo-100 rounded">
<Clock size={14} className="text-indigo-600" />
</div>
<div className="flex-1 min-w-0">
<p className="font-semibold text-slate-900 truncate">{customer?.name || 'Cliente'}</p>
<p className="text-sm text-slate-600">{timeStr}</p>
<p className="text-xs text-slate-500 truncate">{svc?.name || 'Serviço'}</p>
</div>
</div>
);
})}
</div>
);
})()}
</Card>
</div>
</div>
</div>
)}
@@ -442,10 +582,15 @@ export default function Dashboard() {
<div className="p-2 bg-indigo-500 rounded-lg text-white">
<Calendar size={20} />
</div>
<span className="text-xs text-slate-500">0%</span>
<span className={`text-xs ${comparisonPercent >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{comparisonPercent >= 0 ? '+' : ''}{comparisonPercent}%
</span>
</div>
<p className="text-sm text-slate-600 mb-1">Total de marcações</p>
<p className="text-2xl font-bold text-slate-900">{totalBookingsToday}</p>
<p className="text-xs text-slate-500 mt-1">
Comparado com {totalBookingsToday} no mesmo dia da semana passada
</p>
</Card>
<Card className="p-5">
@@ -453,10 +598,15 @@ export default function Dashboard() {
<div className="p-2 bg-green-500 rounded-lg text-white">
<UserPlus size={20} />
</div>
<span className="text-xs text-slate-500">0%</span>
<span className={`text-xs ${comparisonPercent >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{comparisonPercent >= 0 ? '+' : ''}{comparisonPercent}%
</span>
</div>
<p className="text-sm text-slate-600 mb-1">Novos clientes</p>
<p className="text-2xl font-bold text-slate-900">{newClientsToday}</p>
<p className="text-xs text-slate-500 mt-1">
Comparado com {newClientsToday} no mesmo dia da semana passada
</p>
</Card>
<Card className="p-5">
@@ -464,10 +614,15 @@ export default function Dashboard() {
<div className="p-2 bg-blue-500 rounded-lg text-white">
<Globe size={20} />
</div>
<span className="text-xs text-slate-500">0%</span>
<span className={`text-xs ${comparisonPercent >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{comparisonPercent >= 0 ? '+' : ''}{comparisonPercent}%
</span>
</div>
<p className="text-sm text-slate-600 mb-1">Marcações online</p>
<p className="text-2xl font-bold text-slate-900">{onlineBookingsToday}</p>
<p className="text-xs text-slate-500 mt-1">
Comparado com {onlineBookingsToday} no mesmo dia da semana passada
</p>
</Card>
<Card className="p-5">
@@ -475,10 +630,15 @@ export default function Dashboard() {
<div className="p-2 bg-purple-500 rounded-lg text-white">
<TrendingUp size={20} />
</div>
<span className="text-xs text-slate-500">0%</span>
<span className={`text-xs ${comparisonPercent >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{comparisonPercent >= 0 ? '+' : ''}{comparisonPercent}%
</span>
</div>
<p className="text-sm text-slate-600 mb-1">Ocupação</p>
<p className="text-2xl font-bold text-slate-900">{occupancyRate}%</p>
<p className="text-xs text-slate-500 mt-1">
Comparado com {occupancyRate}% no mesmo dia da semana passada
</p>
</Card>
</div>
@@ -489,7 +649,7 @@ export default function Dashboard() {
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
<input
type="text"
placeholder="Pesquisar por cliente, serviço ou barbeiro..."
placeholder="Pesquisar por cliente"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full rounded-lg border border-slate-300 bg-white px-10 py-2.5 text-sm text-slate-900 placeholder:text-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 transition-all"
@@ -528,7 +688,7 @@ export default function Dashboard() {
}}>
<ChevronLeft size={18} />
</Button>
<div className="text-sm font-medium text-slate-700">
<div className="text-sm font-medium text-slate-700 px-3">
{selectedDate.toLocaleDateString('pt-PT', {
weekday: 'long',
day: 'numeric',
@@ -630,7 +790,7 @@ export default function Dashboard() {
</div>
) : (
<div className="text-center py-16">
<Calendar size={64} className="mx-auto text-slate-300 mb-4" />
<Calendar size={64} className="mx-auto text-indigo-300 mb-4" />
<p className="text-lg font-semibold text-slate-900 mb-2">Sem reservas</p>
<p className="text-sm text-slate-600 max-w-md mx-auto">
Ambas as suas reservas online e manuais aparecerão aqui

View File

@@ -90,25 +90,25 @@ export default function Landing() {
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{
icon: Calendar,
{[
{
icon: Calendar,
title: 'Agendamentos Inteligentes',
desc: 'Escolha serviço, barbeiro, data e horário com validação de slots em tempo real. Notificações automáticas.',
color: 'from-blue-500 to-blue-600'
},
{
icon: ShoppingBag,
title: 'Carrinho Inteligente',
color: 'from-blue-500 to-blue-600'
},
{
icon: ShoppingBag,
title: 'Carrinho Inteligente',
desc: 'Produtos e serviços agrupados por barbearia, checkout rápido e seguro. Histórico completo de compras.',
color: 'from-emerald-500 to-emerald-600'
},
{
icon: BarChart3,
title: 'Painel Completo',
color: 'from-emerald-500 to-emerald-600'
},
{
icon: BarChart3,
title: 'Painel Completo',
desc: 'Faturamento, agendamentos, pedidos e análises detalhadas. Tudo no controle da sua barbearia.',
color: 'from-purple-500 to-purple-600'
},
color: 'from-purple-500 to-purple-600'
},
{
icon: Users,
title: 'Gestão de Barbeiros',
@@ -127,17 +127,17 @@ export default function Landing() {
desc: 'Dados protegidos, pagamentos seguros e backup automático. Conformidade com LGPD.',
color: 'from-rose-500 to-rose-600'
},
].map((feature) => (
<Card key={feature.title} hover className="p-6 space-y-4 group">
<div className={`inline-flex p-3 rounded-xl bg-gradient-to-br ${feature.color} text-white shadow-lg group-hover:scale-110 transition-transform duration-200`}>
<feature.icon size={24} />
</div>
<div>
<h3 className="text-xl font-bold text-slate-900 mb-2">{feature.title}</h3>
<p className="text-sm text-slate-600 leading-relaxed">{feature.desc}</p>
</div>
</Card>
))}
].map((feature) => (
<Card key={feature.title} hover className="p-6 space-y-4 group">
<div className={`inline-flex p-3 rounded-xl bg-gradient-to-br ${feature.color} text-white shadow-lg group-hover:scale-110 transition-transform duration-200`}>
<feature.icon size={24} />
</div>
<div>
<h3 className="text-xl font-bold text-slate-900 mb-2">{feature.title}</h3>
<p className="text-sm text-slate-600 leading-relaxed">{feature.desc}</p>
</div>
</Card>
))}
</div>
</section>