diff --git a/RELATORIO_PROJETO.md b/RELATORIO_PROJETO.md new file mode 100644 index 0000000..1583490 --- /dev/null +++ b/RELATORIO_PROJETO.md @@ -0,0 +1,423 @@ +# 📊 Relatório do Projeto Smart Agenda + +**Data:** Janeiro 2025 +**Versão:** 1.0.0 +**Status:** Em Desenvolvimento + +--- + +## 📋 Visão Geral + +O **Smart Agenda** é uma aplicação completa para gestão de agendamentos de barbearias, disponível em duas versões: +- **Mobile:** React Native/Expo (aplicação nativa) +- **Web:** React + TypeScript + Vite (aplicação web responsiva) + +--- + +## 🏗️ Arquitetura do Projeto + +### Estrutura de Pastas + +``` +SmartAgendaMobile/ +├── src/ # Aplicação Mobile (React Native/Expo) +│ ├── components/ # Componentes UI reutilizáveis +│ ├── context/ # Context API (estado global) +│ ├── data/ # Dados mock +│ ├── lib/ # Utilitários +│ ├── navigation/ # Configuração de navegação +│ └── pages/ # Páginas da aplicação +│ +└── web/ # Aplicação Web (React + Vite) + └── src/ + ├── components/ # Componentes UI + ├── context/ # Context API + ├── data/ # Dados mock + ├── lib/ # Utilitários + ├── pages/ # Páginas + └── routes.tsx # Rotas da aplicação +``` + +--- + +## 🎯 Funcionalidades Implementadas + +### 👤 Para Clientes + +1. **Exploração de Barbearias** + - Lista de barbearias disponíveis + - Visualização de detalhes (serviços, produtos, barbeiros) + - Avaliações e informações de localização + +2. **Sistema de Agendamento** + - Seleção de serviço + - Escolha de barbeiro + - Seleção de data e horário + - Validação de disponibilidade em tempo real + - Histórico de agendamentos + +3. **Carrinho de Compras** + - Adicionar produtos ao carrinho + - Agrupamento por barbearia + - Checkout e finalização de pedidos + +4. **Perfil do Usuário** + - Visualização de agendamentos + - Histórico de pedidos + - Gestão de conta + +### 🏪 Para Barbearias + +1. **Dashboard Completo** + - Métricas de faturamento + - Gráficos de receita (Recharts) + - Estatísticas de agendamentos e pedidos + - Filtros por período (hoje, semana, mês, ano) + +2. **Gestão de Agendamentos** + - Visualização de todos os agendamentos + - Alteração de status (pendente, confirmado, concluído, cancelado) + - Filtros por status + +3. **Gestão de Pedidos** + - Visualização de pedidos de produtos + - Alteração de status + - Histórico completo + +4. **CRUD de Serviços** + - Criar, editar e excluir serviços + - Definir preço e duração + - Associar barbeiros aos serviços + +5. **CRUD de Produtos** + - Criar, editar e excluir produtos + - Controlo de stock + - Alertas de stock baixo + +6. **CRUD de Barbeiros** + - Adicionar e remover barbeiros + - Definir especialidades + - Gestão de horários e disponibilidade + +--- + +## 🎨 Design e UI + +### Paleta de Cores Atual +- **Primária:** Indigo/Blue (profissional, moderno) +- **Secundária:** Slate (neutro) +- **Background:** Gradientes suaves +- **Status:** Sistema de badges coloridos + +### Componentes UI Criados + +1. **Componentes Base:** + - `Button` - Botões com variantes (solid, outline, ghost, danger) + - `Card` - Cards com hover effects + - `Input` - Inputs estilizados + - `Badge` - Badges coloridos + - `Tabs` - Sistema de abas + - `Chip` - Chips selecionáveis + - `Dialog` - Modais + +2. **Componentes Específicos:** + - `ShopCard` - Card de barbearia + - `ServiceList` - Lista de serviços + - `ProductList` - Lista de produtos + - `CartPanel` - Painel do carrinho + - `DashboardCards` - Cards do dashboard + - `Header` - Cabeçalho com navegação + - `Shell` - Layout principal + +### Landing Page + +Página inicial desenvolvida com: +- **Hero Section** - Seção principal com CTA +- **Features Grid** - 6 funcionalidades principais +- **Como Funciona** - Passo a passo em 3 etapas +- **Barbearias em Destaque** - Cards de barbearias +- **Benefícios** - Mobile-first e aumento de receita +- **Depoimentos** - Testemunhos de clientes +- **CTA Final** - Chamada para ação + +--- + +## 🔧 Tecnologias Utilizadas + +### Mobile (React Native/Expo) +- **Expo** ~54.0.27 +- **React Native** 0.81.5 +- **React Navigation** - Navegação +- **AsyncStorage** - Persistência +- **Nanoid** - Geração de IDs +- **TypeScript** - Tipagem + +### Web (React + Vite) +- **React** 18.3.1 +- **TypeScript** 5.6.3 +- **Vite** 5.4.10 +- **React Router** v6 - Roteamento +- **Tailwind CSS** 3.4.14 - Estilização +- **Recharts** 2.12.7 - Gráficos +- **Lucide React** 0.473.0 - Ícones +- **Zustand** 4.5.4 - Estado (não utilizado, Context API em uso) +- **Date-fns** 4.1.0 - Manipulação de datas +- **Nanoid** 5.0.7 - Geração de IDs + +--- + +## 💾 Gestão de Estado + +### Context API +- Estado global centralizado em `AppContext` +- Persistência automática em `localStorage` (web) +- Funções para: + - Autenticação (login, logout, registro) + - Gestão de carrinho + - Criação de agendamentos e pedidos + - CRUD completo de serviços, produtos e barbeiros + - Atualização de status + +### Dados Mock +- 2 usuários demo (cliente e barbearia) +- 2 barbearias com serviços, produtos e barbeiros +- Sistema de dados persistente + +--- + +## 🌐 Rotas da Aplicação Web + +1. `/` - Landing Page +2. `/login` - Login +3. `/registo` - Registro +4. `/explorar` - Explorar barbearias +5. `/barbearia/:id` - Detalhes da barbearia +6. `/agendar/:id` - Agendamento +7. `/carrinho` - Carrinho de compras +8. `/perfil` - Perfil do usuário +9. `/painel` - Dashboard da barbearia + +--- + +## 🔐 Sistema de Autenticação + +### Credenciais Demo +- **Cliente:** `cliente@demo.com` / `123` +- **Barbearia:** `barber@demo.com` / `123` + +### Funcionalidades +- Login com validação +- Registro de novos usuários +- Registro de barbearias (cria barbearia automaticamente) +- Logout +- Redirecionamento automático baseado em role + +--- + +## 💰 Sistema de Moeda + +- **Moeda Atual:** EUR (Euros) +- **Locale:** pt-PT +- Formatação automática em toda a aplicação +- Função `currency()` centralizada em `lib/format.ts` + +--- + +## 📱 Responsividade + +- Design **mobile-first** +- Breakpoints Tailwind (md, lg) +- Layout adaptativo +- Navegação mobile com menu hambúrguer +- Componentes responsivos + +--- + +## 📊 Funcionalidades do Dashboard + +### Métricas Principais +- Faturamento total +- Agendamentos do período +- Pedidos do período +- Produtos com stock baixo + +### Gráficos +- Gráfico de barras de receita por período +- Visualização de tendências + +### Gestão +- Tabs para navegação entre seções +- Filtros por período +- Ações rápidas (CRUD) + +--- + +## 🛠️ Melhorias Realizadas (Nesta Sessão) + +1. **Landing Page Desenvolvida** + - Página inicial completa e profissional + - Múltiplas seções com conteúdo rico + - Design moderno e atraente + +2. **Paleta de Cores Atualizada** + - Mudança de Amber para Indigo/Blue + - Atualização em todos os componentes + - Documentação de opções de cores criada + +3. **Correções Técnicas** + - Correção de erro TypeScript em `cn.ts` + - Configuração do servidor Vite para acesso externo + - Ajustes de lint + +4. **Documentação** + - Criação de `OPCOES_CORES.md` com 6 paletas alternativas + - Documentação de como aplicar novas cores + +5. **Moeda Atualizada** + - Mudança de BRL (Reais) para EUR (Euros) + - Formatação atualizada em toda a aplicação + +--- + +## 📝 Arquivos de Documentação + +1. **README.md** - Documentação principal do projeto mobile +2. **web/README.md** - Documentação da aplicação web +3. **web/OPCOES_CORES.md** - Opções de paletas de cores +4. **CONECTAR_ANDROID.md** - Instruções para conectar Android +5. **RELATORIO_PROJETO.md** - Este relatório + +--- + +## 🚀 Como Executar + +### Aplicação Web +```bash +cd web +npm install +npm run dev +# Acesse http://localhost:5173 +``` + +### Aplicação Mobile +```bash +npm install +npm start +# Escolha a plataforma (a/i/w) +``` + +--- + +## 📈 Estatísticas do Projeto + +### Arquivos Criados +- **Páginas:** 9 (Landing, Login, Register, Explore, ShopDetails, Booking, Cart, Profile, Dashboard) +- **Componentes UI:** 7 componentes base +- **Componentes Específicos:** 5 componentes +- **Context:** 1 (AppContext completo) +- **Utilitários:** 2 (format, storage) + +### Linhas de Código (Estimativa) +- Aproximadamente 3.000+ linhas de código TypeScript/TSX +- Componentes bem estruturados e reutilizáveis + +--- + +## ✅ Status das Funcionalidades + +### ✅ Completas +- [x] Autenticação (login/registro) +- [x] Exploração de barbearias +- [x] Sistema de agendamento +- [x] Carrinho de compras +- [x] Dashboard completo +- [x] CRUD de serviços +- [x] CRUD de produtos +- [x] CRUD de barbeiros +- [x] Gestão de agendamentos +- [x] Gestão de pedidos +- [x] Landing page +- [x] Design responsivo +- [x] Persistência de dados + +### 🔄 Em Desenvolvimento +- [ ] Melhorias no fluxo de agendamento (separação por etapas) +- [ ] Validações adicionais +- [ ] Testes automatizados + +### 📋 Futuras Melhorias +- [ ] Integração com API real +- [ ] Sistema de notificações +- [ ] Pagamentos online +- [ ] Sistema de avaliações +- [ ] Chat/suporte +- [ ] App mobile nativo completo + +--- + +## 🎨 Opções de Cores Disponíveis + +Documentadas em `web/OPCOES_CORES.md`: +1. Azul/Indigo (atual) ✅ +2. Âmbar/Amarelo +3. Verde/Emerald +4. Roxo/Violet +5. Vermelho/Rose +6. Ciano/Sky +7. Laranja/Orange + +--- + +## 🔍 Próximos Passos Sugeridos + +1. **Melhorar Fluxo de Agendamento** + - Separar etapas (serviço → barbeiro → data/hora) + - Melhorar UX do processo + +2. **Validações** + - Validação de formulários + - Mensagens de erro mais claras + - Validação de horários disponíveis + +3. **Testes** + - Testes unitários + - Testes de integração + - Testes E2E + +4. **Performance** + - Otimização de imagens + - Lazy loading + - Code splitting + +5. **Acessibilidade** + - ARIA labels + - Navegação por teclado + - Contraste de cores + +--- + +## 📞 Informações Técnicas + +### Portas Utilizadas +- **Web:** 5173 (Vite dev server) +- **Mobile:** Expo padrão + +### Estrutura de Dados +- Tipos TypeScript bem definidos +- Interfaces claras e documentadas +- Validação de tipos em tempo de compilação + +### Persistência +- **Web:** localStorage +- **Mobile:** AsyncStorage + +--- + +## 📄 Licença + +Projeto privado - Todos os direitos reservados + +--- + +**Última Atualização:** Janeiro 2025 +**Versão do Relatório:** 1.0 diff --git a/web/src/components/CalendarWeekView.tsx b/web/src/components/CalendarWeekView.tsx new file mode 100644 index 0000000..798fbbb --- /dev/null +++ b/web/src/components/CalendarWeekView.tsx @@ -0,0 +1,269 @@ +import { useMemo, useState, useEffect } from 'react'; +import { ChevronLeft, ChevronRight, ZoomIn, ZoomOut, RefreshCw } from 'lucide-react'; +import { Appointment, BarberShop } from '../types'; +import { Badge } from './ui/badge'; +import { Button } from './ui/button'; + +type CalendarWeekViewProps = { + week: Date; + appointments: Appointment[]; + shop: BarberShop; + onWeekChange: (date: Date) => void; + onStatusChange: (id: string, status: Appointment['status']) => void; +}; + +export const CalendarWeekView = ({ + week, + appointments, + shop, + onWeekChange, + onStatusChange, +}: CalendarWeekViewProps) => { + const [currentTime, setCurrentTime] = useState(new Date()); + + // Atualizar hora atual a cada minuto + useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(new Date()); + }, 60000); + return () => clearInterval(interval); + }, []); + + // Obter dias da semana + const weekDays = useMemo(() => { + const startOfWeek = new Date(week); + const day = startOfWeek.getDay(); + const diff = startOfWeek.getDate() - day + (day === 0 ? -6 : 1); // Segunda-feira + startOfWeek.setDate(diff); + + const days = []; + for (let i = 0; i < 7; i++) { + const date = new Date(startOfWeek); + date.setDate(startOfWeek.getDate() + i); + days.push(date); + } + return days; + }, [week]); + + // Gerar slots de horário (08:00 até 18:00, intervalos de 30 minutos) + const timeSlots = useMemo(() => { + const slots: string[] = []; + for (let hour = 8; hour <= 18; hour++) { + slots.push(`${hour.toString().padStart(2, '0')}:00`); + if (hour < 18) { + slots.push(`${hour.toString().padStart(2, '0')}:30`); + } + } + return slots; + }, []); + + // Formatar data para YYYY-MM-DD + const formatDateKey = (date: Date) => { + return date.toISOString().split('T')[0]; + }; + + // Obter agendamentos para um dia e horário específicos + const getAppointmentsAtSlot = (date: Date, timeSlot: string) => { + const dateKey = formatDateKey(date); + return appointments.filter((apt) => { + const aptDate = apt.date.split(' ')[0]; + const aptTime = apt.date.split(' ')[1]?.substring(0, 5); + return aptDate === dateKey && aptTime === timeSlot; + }); + }; + + // Navegação de semana + const goToPreviousWeek = () => { + const newDate = new Date(week); + newDate.setDate(newDate.getDate() - 7); + onWeekChange(newDate); + }; + + const goToNextWeek = () => { + const newDate = new Date(week); + newDate.setDate(newDate.getDate() + 7); + onWeekChange(newDate); + }; + + const goToToday = () => { + onWeekChange(new Date()); + }; + + // Verificar se é hoje + const isToday = (date: Date) => { + const today = new Date(); + return ( + date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear() + ); + }; + + // Obter posição da linha de hora atual + const getCurrentTimePosition = (date: Date) => { + if (!isToday(date)) return null; + const now = currentTime; + const hours = now.getHours(); + const minutes = now.getMinutes(); + const totalMinutes = hours * 60 + minutes; + const startMinutes = 8 * 60; // 08:00 + const slotIndex = Math.floor((totalMinutes - startMinutes) / 30); + if (slotIndex < 0 || slotIndex >= timeSlots.length) return null; + return slotIndex; + }; + + // Nomes dos dias da semana + const dayNames = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom']; + const dayNamesFull = ['Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado', 'Domingo']; + + return ( +
+ {/* Header com navegação */} +
+
+ + + +
+

+ {weekDays[0].toLocaleDateString('pt-PT', { day: 'numeric', month: 'long' })} -{' '} + {weekDays[6].toLocaleDateString('pt-PT', { day: 'numeric', month: 'long' })} +

+
+
+
+ + + +
+
+ + {/* Calendário */} +
+ {/* Cabeçalho com dias da semana */} +
+
+ {weekDays.map((day, idx) => ( +
+
{dayNames[idx]}
+
+ {day.getDate()} +
+
+ ))} +
+ + {/* Grade de horários */} +
+
+ {/* Coluna de horários */} +
+ {timeSlots.map((slot) => ( +
+ {slot} +
+ ))} +
+ + {/* Colunas dos dias */} + {weekDays.map((day, dayIdx) => { + const currentTimePos = getCurrentTimePosition(day); + return ( +
+ {timeSlots.map((slot, slotIdx) => { + const slotAppointments = getAppointmentsAtSlot(day, slot); + const isCurrentTimeSlot = currentTimePos === slotIdx; + + return ( +
+ {/* Linha de hora atual */} + {isCurrentTimeSlot && ( +
+
+
+ )} + + {/* Agendamentos */} + {slotAppointments.map((apt, aptIdx) => { + const service = shop.services.find((s) => s.id === apt.serviceId); + const barber = shop.barbers.find((b) => b.id === apt.barberId); + const statusColors = { + pendente: 'bg-amber-100 border-amber-300 text-amber-800', + confirmado: 'bg-green-100 border-green-300 text-green-800', + concluido: 'bg-blue-100 border-blue-300 text-blue-800', + cancelado: 'bg-red-100 border-red-300 text-red-800', + }; + + return ( +
+
{service?.name || 'Serviço'}
+
{barber?.name || 'Barbeiro'}
+
+ {apt.date.split(' ')[1]?.substring(0, 5)} +
+
+ ); + })} +
+ ); + })} +
+ ); + })} +
+
+
+ + {/* Legenda */} +
+
+
+ Pendente +
+
+
+ Confirmado +
+
+
+ Concluído +
+
+
+ Cancelado +
+
+
+ ); +}; diff --git a/web/src/lib/cn.ts b/web/src/lib/cn.ts index 80da165..a317026 100644 --- a/web/src/lib/cn.ts +++ b/web/src/lib/cn.ts @@ -1,6 +1,6 @@ -import clsx, { ClassValue } from 'classnames'; +import clsx from 'classnames'; -export const cn = (...inputs: ClassValue[]) => clsx(inputs); +export const cn = (...inputs: (string | undefined | null | boolean)[]) => clsx(inputs); diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts index e843310..aeab84f 100644 --- a/web/src/lib/format.ts +++ b/web/src/lib/format.ts @@ -1,4 +1,4 @@ -export const currency = (v: number) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); +export const currency = (v: number) => v.toLocaleString('pt-PT', { style: 'currency', currency: 'EUR' }); diff --git a/web/src/pages/Booking.tsx b/web/src/pages/Booking.tsx index c9e442e..ea720d7 100644 --- a/web/src/pages/Booking.tsx +++ b/web/src/pages/Booking.tsx @@ -75,13 +75,11 @@ export default function Booking() { } }; - // Determinar qual etapa mostrar - const currentStep = !serviceId ? 1 : !barberId ? 2 : 3; - const steps = [ - { id: 1, label: 'Serviço', icon: Scissors, completed: !!serviceId, current: currentStep === 1 }, - { id: 2, label: 'Barbeiro', icon: User, completed: !!barberId, current: currentStep === 2 }, - { id: 3, label: 'Data & Hora', icon: Calendar, completed: !!date && !!slot, current: currentStep === 3 }, + { id: 1, label: 'Serviço', icon: Scissors, completed: !!serviceId }, + { id: 2, label: 'Barbeiro', icon: User, completed: !!barberId }, + { id: 3, label: 'Data', icon: Calendar, completed: !!date }, + { id: 4, label: 'Horário', icon: Clock, completed: !!slot }, ]; return ( @@ -99,165 +97,119 @@ export default function Booking() {
{step.completed ? : }
- + {step.label} {idx < steps.length - 1 && ( -
+
)}
))}
- {/* Step 1: Service - Só aparece se não tiver serviço selecionado */} - {currentStep === 1 && ( -
-
- -

1. Escolha o serviço

-
-
- {shop.services.map((s) => ( - - ))} -
+ {/* Step 1: Service */} +
+
+ +

1. Escolha o serviço

- )} - - {/* Step 2: Barber - Só aparece se tiver serviço mas não barbeiro */} - {currentStep === 2 && ( -
-
+
+ {shop.services.map((s) => ( -
+ ))} +
+
+ + {/* Step 2: Barber */} +
+
+ +

2. Escolha o barbeiro

+
+
+ {shop.barbers.map((b) => ( + + ))} +
+
+ + {/* Step 3 & 4: Date & Time */} +
+
- -

2. Escolha o barbeiro

+ +

3. Escolha a data

+
+ setDate(e.target.value)} + min={new Date().toISOString().split('T')[0]} + /> +
+
+
+ +

4. Escolha o horário

- {selectedService && ( -
-
Serviço selecionado:
-
{selectedService.name} - {currency(selectedService.price)}
-
- )}
- {shop.barbers.map((b) => ( - - ))} + {!barberId || !date ? ( +

Escolha primeiro o barbeiro e a data.

+ ) : availableSlots.length > 0 ? ( + availableSlots.map((h) => ( + + )) + ) : ( +

Nenhum horário disponível para esta data.

+ )}
- )} - - {/* Step 3: Date & Time - Só aparece se tiver serviço e barbeiro */} - {currentStep === 3 && ( -
-
- -
-
- -

3. Escolha a data e horário

-
- - {selectedService && selectedBarber && ( -
-
Serviço: {selectedService.name}
-
Barbeiro: {selectedBarber.name}
-
- )} - -
-
-
- -

Data

-
- setDate(e.target.value)} - min={new Date().toISOString().split('T')[0]} - /> -
-
-
- -

Horário

-
-
- {!date ? ( -

Escolha primeiro a data.

- ) : availableSlots.length > 0 ? ( - availableSlots.map((h) => ( - - )) - ) : ( -

Nenhum horário disponível para esta data.

- )} -
-
-
-
- )} +
{/* Summary */} {canSubmit && selectedService && ( @@ -280,7 +232,7 @@ export default function Booking() {
Total: - {currency(selectedService.price)} + {currency(selectedService.price)}
diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index e191a2a..b8d5c07 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -8,6 +8,7 @@ import { currency } from '../lib/format'; import { useApp } from '../context/AppContext'; import { Product } from '../types'; import { BarChart, Bar, CartesianGrid, ResponsiveContainer, Tooltip, XAxis } from 'recharts'; +import { CalendarWeekView } from '../components/CalendarWeekView'; import { BarChart3, Calendar, @@ -22,6 +23,18 @@ import { Minus, Plus as PlusIcon, History, + ChevronLeft, + ChevronRight, + List, + ZoomIn, + ZoomOut, + RefreshCw, + Search, + Printer, + ChevronDown, + UserPlus, + Globe, + TrendingUp, } from 'lucide-react'; const periods: Record boolean> = { @@ -48,6 +61,7 @@ type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | ' export default function Dashboard() { const { user, + users, shops, appointments, orders, @@ -65,6 +79,11 @@ export default function Dashboard() { const [activeTab, setActiveTab] = useState('overview'); const [period, setPeriod] = useState('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(''); @@ -87,6 +106,58 @@ export default function Dashboard() { const shopAppointments = allShopAppointments.filter((a) => a.status !== 'concluido'); // 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) => { + 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() + ); + }); + + const totalBookingsToday = todayAppointments.filter((a) => includeCancelled || a.status !== 'cancelado').length; + const newClientsToday = useMemo(() => { + const clientIds = new Set(todayAppointments.map((a) => a.customerId)); + return clientIds.size; + }, [todayAppointments]); + const onlineBookingsToday = todayAppointments.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; + return Math.round((bookedSlots / totalSlots) * 100); + }, [todayAppointments]); + + // Filtrar agendamentos para lista + const filteredAppointments = useMemo(() => { + let filtered = shopAppointments; + + 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; + }, [shopAppointments, 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') @@ -331,50 +402,258 @@ export default function Dashboard() { )} {activeTab === 'appointments' && ( - -
-

Agendamentos

+
+ {/* View Toggle Buttons */} +
+
+ + +
{shopAppointments.length} no período
-
- {shopAppointments.length > 0 ? ( - shopAppointments.map((a) => { - const svc = shop.services.find((s) => s.id === a.serviceId); - const barber = shop.barbers.find((b) => b.id === a.barberId); - return ( -
-
-
-

{svc?.name ?? 'Serviço'}

- - {a.status} - -
-

{barber?.name ?? 'Barbeiro'} · {a.date}

-

{currency(a.total)}

+ + {/* List View */} + {appointmentView === 'list' && ( +
+ {/* Cards de Estatísticas */} +
+ +
+
+
- + 0%
- ); - }) - ) : ( -
- -

Nenhum agendamento no período

+

Total de marcações

+

{totalBookingsToday}

+ + + +
+
+ +
+ 0% +
+

Novos clientes

+

{newClientsToday}

+
+ + +
+
+ +
+ 0% +
+

Marcações online

+

{onlineBookingsToday}

+
+ + +
+
+ +
+ 0% +
+

Ocupação

+

{occupancyRate}%

+
- )} -
- + + {/* Barra de Pesquisa e Filtros */} +
+
+
+ + 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" + /> +
+
+
+ + +
+
+ + {/* Navegação de Data */} +
+ +
+ +
+ {selectedDate.toLocaleDateString('pt-PT', { + weekday: 'long', + day: 'numeric', + month: 'numeric', + year: 'numeric' + })} +
+ +
+
+ + {/* Lista de Agendamentos */} + + {filteredAppointments.length > 0 ? ( +
+ {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 ( +
+
+
+

Cliente

+

{customer?.name || 'Cliente'}

+

{customer?.email || ''}

+
+
+

Serviço

+

{svc?.name ?? 'Serviço'}

+

{barber?.name ?? 'Barbeiro'}

+
+
+

Data e Hora

+

{dateStr}

+

{timeStr}

+
+
+

Status

+
+ + {a.status === 'pendente' + ? 'Pendente' + : a.status === 'confirmado' + ? 'Confirmado' + : a.status === 'concluido' + ? 'Concluído' + : 'Cancelado'} + +

{currency(a.total)}

+
+
+
+
+ +
+
+ ); + })} +
+ ) : ( +
+ +

Sem reservas

+

+ Ambas as suas reservas online e manuais aparecerão aqui +

+
+ )} +
+
+ )} + + {/* Calendar View */} + {appointmentView === 'calendar' && ( + + + + )} +
)} {activeTab === 'history' && (