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 */}
+
+
+ );
+};
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}
+
+
+
+
+ Novos clientes
+ {newClientsToday}
+
+
+
+
+ Marcações online
+ {onlineBookingsToday}
+
+
+
+
+ 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' && (