alteração dashboard
This commit is contained in:
@@ -118,11 +118,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
|
||||
|
||||
const [activeTab, setActiveTab] = useState<TabId>('overview');
|
||||
const [period, setPeriod] = useState<keyof typeof periods>('semana');
|
||||
const [appointmentView, setAppointmentView] = useState<'list' | 'calendar'>('list');
|
||||
const [currentWeek, setCurrentWeek] = useState(new Date());
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [includeCancelled, setIncludeCancelled] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||
|
||||
// Form states
|
||||
const [svcName, setSvcName] = useState('');
|
||||
@@ -148,67 +144,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
|
||||
const allShopAppointments = appointments.filter((a) => a.shopId === shop.id && periodMatch(parseDate(a.date)));
|
||||
|
||||
// Agendamentos ativos (não concluídos e não cancelados)
|
||||
const shopAppointments = allShopAppointments.filter((a) => a.status !== 'concluido');
|
||||
|
||||
// Agendamentos concluídos (histórico passado)
|
||||
const completedAppointments = allShopAppointments.filter((a) => a.status === 'concluido');
|
||||
|
||||
// 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'));
|
||||
return (
|
||||
aptDate.getDate() === selectedDate.getDate() &&
|
||||
aptDate.getMonth() === selectedDate.getMonth() &&
|
||||
aptDate.getFullYear() === selectedDate.getFullYear()
|
||||
);
|
||||
});
|
||||
|
||||
const totalBookingsToday = selectedDateAppointments.filter((a) => includeCancelled || a.status !== 'cancelado').length;
|
||||
|
||||
const newClientsToday = useMemo(() => {
|
||||
const clientIds = new Set(selectedDateAppointments.map((a) => a.customerId));
|
||||
return clientIds.size;
|
||||
}, [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 = selectedDateAppointments.filter((a) => a.status !== 'cancelado').length;
|
||||
return Math.round((bookedSlots / totalSlots) * 100);
|
||||
}, [selectedDateAppointments]);
|
||||
|
||||
// Comparação com semana passada (simplificado - sempre 0% por enquanto)
|
||||
const comparisonPercent = 0;
|
||||
|
||||
// Filtrar agendamentos para lista
|
||||
const filteredAppointments = useMemo(() => {
|
||||
let filtered = selectedDateAppointments;
|
||||
|
||||
if (!includeCancelled) {
|
||||
filtered = filtered.filter((a) => a.status !== 'cancelado');
|
||||
}
|
||||
|
||||
if (searchQuery) {
|
||||
filtered = filtered.filter((a) => {
|
||||
const service = 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 searchLower = searchQuery.toLowerCase();
|
||||
return (
|
||||
service?.name.toLowerCase().includes(searchLower) ||
|
||||
barber?.name.toLowerCase().includes(searchLower) ||
|
||||
customer?.name.toLowerCase().includes(searchLower) ||
|
||||
customer?.email.toLowerCase().includes(searchLower) ||
|
||||
a.date.toLowerCase().includes(searchLower)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [selectedDateAppointments, includeCancelled, searchQuery, shop.services, shop.barbers, users]);
|
||||
const shopAppointments = allShopAppointments.filter((a) => a.status !== 'concluido' && a.status !== 'cancelado');
|
||||
|
||||
// Pedidos apenas com produtos (não serviços)
|
||||
const shopOrders = orders.filter(
|
||||
@@ -454,66 +390,32 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{/* Coluna Principal - Esquerda */}
|
||||
<div className="md:col-span-2 space-y-6">
|
||||
{/* Reservas de Hoje */}
|
||||
{/* Calendário de Visão Geral */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-indigo-100 rounded-lg">
|
||||
<Calendar size={20} className="text-indigo-600" />
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-slate-900">Calendário de Reservas</h3>
|
||||
<p className="text-sm text-slate-600">Visão panorâmica da semana atual</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => setActiveTab('appointments')}>
|
||||
Gerir Pedidos
|
||||
<ArrowRight size={16} className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Reutiliza o Componente de Calendário Inteiro na aba Overview */}
|
||||
<CalendarWeekView
|
||||
week={currentWeek}
|
||||
appointments={shopAppointments}
|
||||
shop={shop}
|
||||
onWeekChange={setCurrentWeek}
|
||||
onStatusChange={updateAppointmentStatus}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -635,311 +537,190 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
|
||||
)}
|
||||
|
||||
{activeTab === 'appointments' && (
|
||||
<div className="space-y-4">
|
||||
{/* View Toggle Buttons */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setAppointmentView('list')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${appointmentView === 'list'
|
||||
? 'border-indigo-500 bg-indigo-50 text-indigo-700 font-semibold'
|
||||
: 'border-slate-200 text-slate-700 hover:border-indigo-300 hover:bg-indigo-50/50'
|
||||
}`}
|
||||
>
|
||||
<List size={18} />
|
||||
<span>Lista de marcações</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setAppointmentView('calendar')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${appointmentView === 'calendar'
|
||||
? 'border-indigo-500 bg-indigo-50 text-indigo-700 font-semibold'
|
||||
: 'border-slate-200 text-slate-700 hover:border-indigo-300 hover:bg-indigo-50/50'
|
||||
}`}
|
||||
>
|
||||
<Calendar size={18} />
|
||||
<span>Calendário</span>
|
||||
</button>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900">Caixa de Pedidos</h2>
|
||||
<p className="text-sm text-slate-600">Aprove ou recuse os pedidos pendentes e conclua os serviços de hoje.</p>
|
||||
</div>
|
||||
<Badge color="slate" variant="soft">{shopAppointments.length} no período</Badge>
|
||||
<Badge color="amber" variant="soft">{pendingAppts} Novos Pedidos</Badge>
|
||||
</div>
|
||||
|
||||
{/* List View */}
|
||||
{appointmentView === 'list' && (
|
||||
<div className="space-y-6">
|
||||
{/* Cards de Estatísticas */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="p-2 bg-indigo-500 rounded-lg text-white">
|
||||
<Calendar size={20} />
|
||||
</div>
|
||||
<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">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="p-2 bg-green-500 rounded-lg text-white">
|
||||
<UserPlus size={20} />
|
||||
</div>
|
||||
<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">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="p-2 bg-blue-500 rounded-lg text-white">
|
||||
<Globe size={20} />
|
||||
</div>
|
||||
<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">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="p-2 bg-purple-500 rounded-lg text-white">
|
||||
<TrendingUp size={20} />
|
||||
</div>
|
||||
<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>
|
||||
|
||||
{/* Barra de Pesquisa e Filtros */}
|
||||
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between">
|
||||
<div className="flex-1 max-w-md">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setIncludeCancelled(!includeCancelled)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${includeCancelled
|
||||
? 'border-indigo-500 bg-indigo-50 text-indigo-700'
|
||||
: 'border-slate-200 text-slate-700 hover:border-indigo-300'
|
||||
}`}
|
||||
>
|
||||
<span>Incluir cancelamentos</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Printer size={18} className="mr-2" />
|
||||
Imprimir
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navegação de Data */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" size="sm" onClick={() => setSelectedDate(new Date())}>
|
||||
Hoje
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => {
|
||||
const newDate = new Date(selectedDate);
|
||||
newDate.setDate(newDate.getDate() - 1);
|
||||
setSelectedDate(newDate);
|
||||
}}>
|
||||
<ChevronLeft size={18} />
|
||||
</Button>
|
||||
<div className="text-sm font-medium text-slate-700 px-3">
|
||||
{selectedDate.toLocaleDateString('pt-PT', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'numeric',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => {
|
||||
const newDate = new Date(selectedDate);
|
||||
newDate.setDate(newDate.getDate() + 1);
|
||||
setSelectedDate(newDate);
|
||||
}}>
|
||||
<ChevronRight size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lista de Agendamentos */}
|
||||
<Card className="p-6">
|
||||
{filteredAppointments.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{filteredAppointments.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' });
|
||||
const dateStr = aptDate.toLocaleDateString('pt-PT', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
|
||||
return (
|
||||
<div
|
||||
key={a.id}
|
||||
className="flex items-center justify-between p-4 border border-slate-200 rounded-lg hover:border-indigo-300 hover:shadow-sm transition-all"
|
||||
>
|
||||
<div className="flex-1 grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">Cliente</p>
|
||||
<p className="font-semibold text-slate-900">{customer?.name || 'Cliente'}</p>
|
||||
<p className="text-xs text-slate-500">{customer?.email || ''}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">Serviço</p>
|
||||
<p className="font-semibold text-slate-900">{svc?.name ?? 'Serviço'}</p>
|
||||
<p className="text-xs text-slate-500">{barber?.name ?? 'Barbeiro'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">Data e Hora</p>
|
||||
<p className="font-semibold text-slate-900">{dateStr}</p>
|
||||
<p className="text-xs text-slate-500">{timeStr}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">Status</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
color={
|
||||
a.status === 'pendente'
|
||||
? 'amber'
|
||||
: a.status === 'confirmado'
|
||||
? 'green'
|
||||
: a.status === 'concluido'
|
||||
? 'green'
|
||||
: 'red'
|
||||
}
|
||||
>
|
||||
{a.status === 'pendente'
|
||||
? 'Pendente'
|
||||
: a.status === 'confirmado'
|
||||
? 'Confirmado'
|
||||
: a.status === 'concluido'
|
||||
? 'Concluído'
|
||||
: 'Cancelado'}
|
||||
</Badge>
|
||||
<p className="text-sm font-semibold text-slate-900">{currency(a.total)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<select
|
||||
className="text-sm border border-slate-300 rounded-lg px-3 py-2 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30"
|
||||
value={a.status}
|
||||
onChange={(e) => updateAppointmentStatus(a.id, e.target.value as any)}
|
||||
>
|
||||
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{s === 'pendente'
|
||||
? 'Pendente'
|
||||
: s === 'confirmado'
|
||||
? 'Confirmado'
|
||||
: s === 'concluido'
|
||||
? 'Concluído'
|
||||
: 'Cancelado'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
{/* Secção de Pedidos Pendentes */}
|
||||
<Card className="p-6 border-amber-200">
|
||||
<div className="flex items-center gap-2 mb-4 pb-4 border-b border-slate-100">
|
||||
<Clock className="text-amber-500" size={20} />
|
||||
<h3 className="font-bold text-slate-900">Aguardam Aprovação</h3>
|
||||
<Badge color="slate" variant="soft" className="ml-auto">{pendingAppts}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{shopAppointments.filter(a => a.status === 'pendente').length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{shopAppointments.filter(a => a.status === 'pendente').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'));
|
||||
|
||||
{/* Calendar View */}
|
||||
{appointmentView === 'calendar' && (
|
||||
<Card className="p-6">
|
||||
<CalendarWeekView
|
||||
week={currentWeek}
|
||||
appointments={shopAppointments}
|
||||
shop={shop}
|
||||
onWeekChange={setCurrentWeek}
|
||||
onStatusChange={updateAppointmentStatus}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
return (
|
||||
<div key={a.id} className="flex flex-col sm:flex-row sm:items-center justify-between p-4 bg-amber-50/50 border border-amber-100 rounded-lg gap-4">
|
||||
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">Cliente</p>
|
||||
<p className="font-semibold text-slate-900">{customer?.name || 'Cliente'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">Serviço</p>
|
||||
<p className="font-semibold text-slate-900">{svc?.name ?? 'Serviço'}</p>
|
||||
<p className="text-xs text-slate-500">com {barber?.name ?? 'Barbeiro'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">Data / Hora</p>
|
||||
<p className="font-semibold text-amber-700">
|
||||
{aptDate.toLocaleDateString('pt-PT', { day: 'numeric', month: 'short' })} às {aptDate.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2 sm:mt-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-red-600 border-red-200 hover:bg-red-50 hover:border-red-300"
|
||||
onClick={() => updateAppointmentStatus(a.id, 'cancelado')}
|
||||
>
|
||||
Recusar
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-amber-500 hover:bg-amber-600 text-white"
|
||||
onClick={() => updateAppointmentStatus(a.id, 'confirmado')}
|
||||
>
|
||||
Aceitar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<CheckCircle2 size={40} className="mx-auto text-slate-300 mb-2" />
|
||||
<p className="text-slate-500 text-sm">Não há pedidos pendentes no momento.</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Secção de Pedidos Confirmados */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-2 mb-4 pb-4 border-b border-slate-100">
|
||||
<Calendar className="text-green-500" size={20} />
|
||||
<h3 className="font-bold text-slate-900">Agendamentos Aprovados</h3>
|
||||
<Badge color="slate" variant="soft" className="ml-auto">{confirmedAppts}</Badge>
|
||||
</div>
|
||||
{shopAppointments.filter(a => a.status === 'confirmado').length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{shopAppointments.filter(a => a.status === 'confirmado').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'));
|
||||
|
||||
return (
|
||||
<div key={a.id} className="flex flex-col sm:flex-row sm:items-center justify-between p-4 border border-slate-200 rounded-lg gap-4">
|
||||
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">Cliente</p>
|
||||
<p className="font-semibold text-slate-900">{customer?.name || 'Cliente'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">Serviço</p>
|
||||
<p className="font-semibold text-slate-900">{svc?.name ?? 'Serviço'}</p>
|
||||
<p className="text-xs text-slate-500">com {barber?.name ?? 'Barbeiro'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">Data / Hora</p>
|
||||
<p className="font-semibold text-slate-900">
|
||||
{aptDate.toLocaleDateString('pt-PT', { day: 'numeric', month: 'short' })} às {aptDate.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2 sm:mt-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-red-600 border-red-200 hover:bg-red-50 hover:border-red-300"
|
||||
onClick={() => {
|
||||
if (window.confirm('Tem a certeza que deseja cancelar esta marcação?')) {
|
||||
updateAppointmentStatus(a.id, 'cancelado');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-green-600 hover:bg-green-700 text-white"
|
||||
onClick={() => updateAppointmentStatus(a.id, 'concluido')}
|
||||
>
|
||||
Concluir Serviço
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<Calendar size={40} className="mx-auto text-slate-300 mb-2" />
|
||||
<p className="text-slate-500 text-sm">Próximos serviços irão aparecer aqui.</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-slate-900">Histórico de Agendamentos</h2>
|
||||
<Badge color="slate" variant="soft">{completedAppointments.length} concluídos</Badge>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{completedAppointments.length > 0 ? (
|
||||
completedAppointments.map((a) => {
|
||||
const svc = shop.services.find((s) => s.id === a.serviceId);
|
||||
const barber = shop.barbers.find((b) => b.id === a.barberId);
|
||||
return (
|
||||
<div key={a.id} className="flex items-center justify-between p-4 border border-slate-200 rounded-lg bg-slate-50/50">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="font-bold text-slate-900">{svc?.name ?? 'Serviço'}</p>
|
||||
<Badge color="green" variant="soft">Concluído</Badge>
|
||||
{activeTab === 'history' && (() => {
|
||||
// O histórico agora compreende marcacões permanentemente finalizadas (Cancelados e Concluídos)
|
||||
const historyAppointments = allShopAppointments.filter(a => a.status === 'concluido' || a.status === 'cancelado');
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-slate-900">Histórico de Agendamentos</h2>
|
||||
<Badge color="slate" variant="soft">{historyAppointments.length} registos</Badge>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{historyAppointments.length > 0 ? (
|
||||
historyAppointments.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'));
|
||||
|
||||
return (
|
||||
<div key={a.id} className="flex items-center justify-between p-4 border border-slate-200 rounded-lg bg-slate-50/50">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="font-bold text-slate-900">{svc?.name ?? 'Serviço'}</p>
|
||||
<Badge color={a.status === 'concluido' ? 'green' : 'red'} variant="soft">
|
||||
{a.status === 'concluido' ? 'Concluído' : 'Cancelado'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600">
|
||||
{customer?.name || 'Cliente'} c/ {barber?.name ?? 'Barbeiro'} ·
|
||||
{aptDate.toLocaleDateString('pt-PT', { day: 'numeric', month: 'short' })}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">{currency(a.total)}</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600">{barber?.name ?? 'Barbeiro'} · {a.date}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">{currency(a.total)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<History size={48} className="mx-auto text-slate-300 mb-3" />
|
||||
<p className="text-slate-600 font-medium">Nenhum agendamento concluído no período</p>
|
||||
<p className="text-sm text-slate-500 mt-1">Os agendamentos concluídos aparecerão aqui</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<History size={48} className="mx-auto text-slate-300 mb-3" />
|
||||
<p className="text-slate-600 font-medium">Nenhum registo no período selecionado</p>
|
||||
<p className="text-sm text-slate-500 mt-1">Os agendamentos cancelados ou concluídos aparecerão aqui.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})()}
|
||||
|
||||
{activeTab === 'orders' && (
|
||||
<Card className="p-6">
|
||||
|
||||
Reference in New Issue
Block a user