From 5b866a161e839fec0667290aee24f080282726ac Mon Sep 17 00:00:00 2001 From: 230417 <230417@epvc.pt> Date: Thu, 26 Feb 2026 10:38:13 +0000 Subject: [PATCH] docs: Add JSDoc and inline comments to enhance code readability and explain functionality across multiple pages and components. --- src/context/AppContext.tsx | 54 ++++++++++- src/pages/AuthLogin.tsx | 20 ++++ src/pages/AuthRegister.tsx | 24 +++++ src/pages/Booking.tsx | 69 ++++++++++++-- src/pages/Cart.tsx | 38 ++++++++ src/pages/Dashboard.tsx | 34 +++++++ src/pages/Explore.tsx | 18 ++++ src/pages/Landing.tsx | 11 +++ src/pages/Profile.tsx | 22 +++++ src/pages/ShopDetails.tsx | 27 ++++++ web/src/components/CalendarWeekView.tsx | 16 ++-- web/src/context/AppContext.tsx | 26 ++++- web/src/data/mock.ts | 35 +++++++ web/src/lib/auth.ts | 6 ++ web/src/lib/events.ts | 13 ++- web/src/pages/AuthLogin.tsx | 25 ++++- web/src/pages/AuthRegister.tsx | 46 ++++++--- web/src/pages/Booking.tsx | 78 +++++++++------ web/src/pages/Cart.tsx | 6 ++ web/src/pages/Dashboard.tsx | 88 +++++++++-------- web/src/pages/EventsCreate.tsx | 11 ++- web/src/pages/Explore.tsx | 19 ++++ web/src/pages/Landing.tsx | 122 +++++++++++++----------- web/src/pages/Profile.tsx | 35 ++++--- web/src/pages/ShopDetails.tsx | 6 ++ 25 files changed, 675 insertions(+), 174 deletions(-) diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx index 0dee7ce..37259b8 100644 --- a/src/context/AppContext.tsx +++ b/src/context/AppContext.tsx @@ -1,7 +1,14 @@ +/** + * @file AppContext.tsx + * @description Contexto global principal ("State Manager") da aplicação. + * Efetua a ligação direta e centralizada com a base de dados Supabase + * lidando com Auth, Consultas (Shops/Services) e CRUD (Criar/Ler/Atualizar/Apagar). + */ import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types'; import { supabase } from '../lib/supabase'; +// Tipo interno determinando as propriedades globais partilhadas (Estados e Funções) type State = { user?: User; shops: BarberShop[]; @@ -26,9 +33,14 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const [user, setUser] = useState(undefined); const [loading, setLoading] = useState(true); - // 🔹 Carregar utilizador autenticado + /** + * Hook executado no carregamento (mount) inicial. + * Valida através de `supabase.auth.getUser()` se existe um token de sessão válido + * (identificando o utilizador sem necessidade de o cliente refazer login ativamente). + */ useEffect(() => { const loadUser = async () => { + // Pedido restrito à API de autenticação do Supabase const { data } = await supabase.auth.getUser(); if (data.user) { setUser({ @@ -41,10 +53,16 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { loadUser(); }, []); - // 🔹 Buscar shops + services + /** + * Consulta mestra (Query) - Refresca todo o ecossistema de dados das barbearias. + * Faz 2 queries (`supabase.from('shops').select('*')` e `services`) e depois + * executa um JOIN manual via Javascript para injetar os serviços dentro + * dos objetos da barbearia respetiva. + */ const refreshShops = async () => { console.log("A buscar shops..."); + // Query 1: Obtém a listagem completa (tabela 'shops') const { data: shopsData, error: shopsError } = await supabase .from('shops') .select('*'); @@ -54,6 +72,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { return; } + // Query 2: Obtém a listagem associada globalmente (tabela 'services') const { data: servicesData, error: servicesError } = await supabase .from('services') .select('*'); @@ -63,9 +82,10 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { return; } - // Associar serviços às respetivas shops + // Associar serviços às respetivas shops, simulando um INNER JOIN nativo do SQL const shopsWithServices = shopsData.map((shop) => ({ ...shop, + // Relaciona a 'foreign key' (shop_id) com o resgistro primário (shop.id) services: servicesData.filter((s) => s.shop_id === shop.id), products: [], barbers: [], @@ -76,6 +96,11 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { setShops(shopsWithServices); }; + /** + * Hook de Inicialização Master. + * Aciona a função de preenchimento do Contexto assincronamente e liberta + * a interface UI da view de Loading (`setLoading(false)`). + */ useEffect(() => { const init = async () => { await refreshShops(); @@ -84,11 +109,16 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { init(); }, []); + /** + * Encerra a sessão JWT ativa com o Supabase Auth. + * Limpa integralmente a interface do User local (estado React vazio). + */ const logout = async () => { await supabase.auth.signOut(); setUser(undefined); }; + // Funções elementares do fluxo transacional não persistido (Estado do Carrinho transitório/local) const addToCart = (item: CartItem) => { setCart((prev) => [...prev, item]); }; @@ -97,7 +127,13 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { // 🔹 CRUD SERVICES (SUPABASE REAL) + /** + * Executa um INSERT na BD (via API REST gerada) protegendo interações com a tabela estrita 'services'. + * @param {string} shopId - A foreign key relacionando o estabelecimento. + * @param {Omit} service - O DTO (Data Transfer Object) sem a primary key autonumerável. + */ const addService = async (shopId: string, service: Omit) => { + // Insere os campos exatos formatados estritamente na query Supabase const { error } = await supabase.from('services').insert([ { shop_id: shopId, @@ -112,9 +148,13 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { return; } + // Para manter integridade reativa pura, força refetch dos dados pós-mutação await refreshShops(); }; + /** + * Executa um UPDATE num tuplo específico filtrando analiticamente pela primary key `(eq('id', service.id))`. + */ const updateService = async (shopId: string, service: Service) => { const { error } = await supabase .from('services') @@ -123,7 +163,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { price: service.price, duration: service.duration, }) - .eq('id', service.id); + .eq('id', service.id); // Identificador vital do update if (error) { console.error("Erro ao atualizar serviço:", error); @@ -133,6 +173,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { await refreshShops(); }; + /** + * Executa uma instrução SQL DELETE remota rígida, baseada no ID unívoco do tuplo. + */ const deleteService = async (shopId: string, serviceId: string) => { const { error } = await supabase .from('services') @@ -147,6 +190,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { await refreshShops(); }; + // Empacotamento em objeto estabilizado memoizado face renderizações espúrias (React Context Pattern) const value: AppContextValue = useMemo( () => ({ user, @@ -163,11 +207,13 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { [user, shops, cart] ); + // Loading Shield evita quebra generalizada se o app renderizar sem BD disponível if (loading) return null; return {children}; }; +// Hook prático de acesso central sem import múltiplo do 'useContext' em toda aplicação export const useApp = () => { const ctx = useContext(AppContext); if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider'); diff --git a/src/pages/AuthLogin.tsx b/src/pages/AuthLogin.tsx index 9525b3e..daa9a01 100644 --- a/src/pages/AuthLogin.tsx +++ b/src/pages/AuthLogin.tsx @@ -1,3 +1,8 @@ +/** + * @file AuthLogin.tsx + * @description Página de autenticação da aplicação. Permite o login de utilizadores + * (clientes ou barbearias) utilizando o contexto global que faz a interface com a base de dados (Supabase). + */ import React, { useState } from 'react'; import { View, Text, StyleSheet, ScrollView, Alert } from 'react-native'; import { useNavigation } from '@react-navigation/native'; @@ -13,30 +18,42 @@ export default function AuthLogin() { const [password, setPassword] = useState('123'); const [error, setError] = useState(''); + /** + * Lida com a submissão do formulário de login. + * Valida as credenciais do utilizador através do contexto (AppContext). + * O contexto encapsula a chamada ao Supabase (ex: `supabase.auth.signInWithPassword`) e devolve um objeto tipado do utilizador. + * Dependendo do 'role' retornado, reencaminha para o Dashboard (barbearia) ou Explore (cliente). + */ const handleSubmit = () => { setError(''); + // O retorno 'user' respeita a nossa tipagem/interfaces baseadas nas tabelas da base de dados. const user = login(email, password); if (!user) { setError('Credenciais inválidas'); Alert.alert('Erro', 'Credenciais inválidas'); } else { + // Redirecionamento condicional com base no papel (identificando a entidade via BD). const target = user.role === 'barbearia' ? 'Dashboard' : 'Explore'; navigation.navigate(target as never); } }; return ( + // Componente estrutural que permite "scroll" vertical do conteúdo na vista + {/* O componente Card encapsula de forma visual os inputs de login */} Bem-vindo de volta Entre na sua conta para continuar + {/* Bloco temporário para dados demo */} 💡 Conta demo: Cliente: cliente@demo.com / 123 Barbearia: barber@demo.com / 123 + {/* Input controlado pelo estadoReact "email"; atualiza à medida que se digita */} + {/* Input controlado para a password; secureTextEntry oculta os caracteres */} + {/* Botão de ação que dispara a função handleSubmit para processar as credenciais */} + {/* Estrutura de rodapé para navegação alternativa (redirecionar para o ecrã de registo) */} Não tem conta? ('cliente'); const [shopName, setShopName] = useState(''); const [error, setError] = useState(''); + /** + * Valida e submete o formulário de registo. + * Chama a função `register` do AppContext, que por sua vez utiliza a lógica de base de dados + * (ex: `supabase.auth.signUp`) e insere o novo registo (ex: `supabase.from('profiles').insert(...)`). + */ const handleSubmit = () => { setError(''); + // Criamos o objeto de registo de acordo com os tipos/interfaces da base de dados e API const user = register({ name, email, password, role, shopName }); if (!user) { setError('Email já registado'); Alert.alert('Erro', 'Email já registado'); } else { + // Redireciona para um fluxo distinto consoante o tipo ('role') do novo utilizador criado const target = user.role === 'barbearia' ? 'Dashboard' : 'Explore'; navigation.navigate(target as never); } }; return ( + // Componente estrutural que assegura scroll de conteúdo para ecrãs menores + {/* O Card agrupa a interface de registo */} Criar conta Escolha o tipo de acesso + {/* Grupo de botões de seleção de papel. O CSS ativo altera com base no estado `role`. */} + {/* Input simples para o nome, que atualiza a variável React de estado "name" */} + {/* Input específico de email - autoCapitalize=none evita letras maiúsculas automáticas */} + {/* Input para password mascarada com secureTextEntry para esconder os carateres */} + {/* Renderização condicional do JSX: o campo shopName só é renderizado + se o papel na base de dados (e estado) for 'barbearia' */} {role === 'barbearia' && ( )} + {/* Botão de chamada à ação principal que aciona a submissão dos dados (handleSubmit) */} + {/* Seção rodapé simples com reencaminhamento de navegação para a página de Login */} Já tem conta? shops.find((s) => s.id === shopId), [shops, shopId]); - + + // Gestão de estados para armazenar as seleções temporárias do cliente no formulário const [serviceId, setService] = useState(''); const [barberId, setBarber] = useState(''); const [date, setDate] = useState(''); const [slot, setSlot] = useState(''); + // Fallback visual caso ocorra um erro a obter o ID requisitado if (!shop) { return ( @@ -28,9 +41,15 @@ export default function Booking() { ); } + // Prepara as entidades interligadas baseadas na escolha para uso no interface (renderizar nome/preço) const selectedService = shop.services.find((s) => s.id === serviceId); const selectedBarber = shop.barbers.find((b) => b.id === barberId); + /** + * Função utilitária local para gerar slots de horário padrão (09:00 - 18:00). + * Auxilia no preenchimento visual caso não haja regras específicas na BD. + * @returns {string[]} Lista de horários padrão. + */ const generateDefaultSlots = (): string[] => { const slots: string[] = []; for (let hour = 9; hour <= 18; hour++) { @@ -39,38 +58,65 @@ export default function Booking() { return slots; }; + /** + * Este hook processa de forma otimizada os horários disponíveis ao cruzar informações. + * Lógica do negócio e tipagem de dados: + * 1. Consulta as horas operacionais do 'barbeiro' a trabalhar nesse dia. + * 2. Cruza com as 'appointments' da base de dados (onde `status` não está cancelado). + * 3. Subtrai os slots já ocupados para garantir a consistência das marcações. + */ const availableSlots = useMemo(() => { if (!selectedBarber || !date) return []; + // Busca a array de schedule (tipo específico) baseada em JSON / relação const specificSchedule = selectedBarber.schedule.find((s) => s.day === date); - let slots = specificSchedule && specificSchedule.slots.length > 0 - ? [...specificSchedule.slots] + let slots = specificSchedule && specificSchedule.slots.length > 0 + ? [...specificSchedule.slots] : generateDefaultSlots(); - + + // Obtém as marcações validadas (equivalente a `supabase.from('appointments').select(*).eq(...)`) const bookedSlots = appointments - .filter((apt) => - apt.barberId === barberId && + .filter((apt) => + apt.barberId === barberId && apt.status !== 'cancelado' && apt.date.startsWith(date) ) .map((apt) => { + // Separa a data completa num timestamp (ex: "2023-10-10 14:00" -> "14:00") const parts = apt.date.split(' '); return parts.length > 1 ? parts[1] : ''; }) .filter(Boolean); - + + // Filtro devolvendo assim uma array de horários finais para o UI renderizar return slots.filter((slot) => !bookedSlots.includes(slot)); }, [selectedBarber, date, barberId, appointments]); + // Booleano derivável auxiliar, controla o bloqueio ou liberação do botão submit const canSubmit = serviceId && barberId && date && slot; + /** + * Submissão do agendamento. + * Desencadeia o pedido assíncrono à API para materializar o DTO na tabela 'appointments'. + * Verifica se o token de Auth está válido (`!user`). + */ const submit = () => { if (!user) { + // Impede requisições anónimas, delegando a sessão para o sistema de autenticação Alert.alert('Login necessário', 'Faça login para agendar'); navigation.navigate('Login' as never); return; } if (!canSubmit) return; - const appt = createAppointment({ shopId: shop.id, serviceId, barberId, customerId: user.id, date: `${date} ${slot}` }); + + // Cria o agendamento fornecendo as 'Foreign Keys' vitais (shopId, serviceId, etc...) + const appt = createAppointment({ + shopId: shop.id, + serviceId, + barberId, + customerId: user.id, // Auth User UID + date: `${date} ${slot}` + }); + if (appt) { Alert.alert('Sucesso', 'Agendamento criado com sucesso!'); navigation.navigate('Profile' as never); @@ -80,11 +126,13 @@ export default function Booking() { }; return ( + // Área de conteúdo com scroll adaptativa Agendar em {shop.name} 1. Escolha o serviço + {/* Renderiza um botão (bloco flexível) por cada serviço (ex: Corte, Barba) vindos do mapeamento DB */} {shop.services.map((s) => ( 2. Escolha o barbeiro + {/* Renderiza os profissionais, normalmente provindo dum JOIN na base de dados (tabela barbeiros + barbearia) */} {shop.barbers.map((b) => ( 3. Escolha a data + {/* Componente simples de input que deverá mapear para a inserção final do timestamp Postgres */} 4. Escolha o horário + {/* Lista mapeada e computada: Apenas slots `available` (que passaram pela query preventiva de duplicação) */} {availableSlots.length > 0 ? ( availableSlots.map((h) => ( @@ -137,6 +188,7 @@ export default function Booking() { )} + {/* Quadro resumo: Apenas mostrado se o estado interno conter todas as variáveis relacionais */} {canSubmit && selectedService && ( Resumo @@ -147,6 +199,7 @@ export default function Booking() { )} + {/* Botão para concretizar o INSERT na base de dados com as validações pré-acionadas */} diff --git a/src/pages/Cart.tsx b/src/pages/Cart.tsx index b8eda3a..28a002b 100644 --- a/src/pages/Cart.tsx +++ b/src/pages/Cart.tsx @@ -1,3 +1,8 @@ +/** + * @file Cart.tsx + * @description Página do carrinho de compras. Permite visualizar e finalizar pedidos + * de serviços (e possivelmente de produtos vinculados) acumulados para diferentes barbearias. + */ import React from 'react'; import { View, Text, StyleSheet, ScrollView, Alert } from 'react-native'; import { useNavigation } from '@react-navigation/native'; @@ -9,8 +14,10 @@ import { currency } from '../lib/format'; export default function Cart() { const navigation = useNavigation(); + // Obtém o estado global do carrinho, e as funções comunicadoras do AppContext (interface BD) const { cart, shops, removeFromCart, placeOrder, user } = useApp(); + // Renderiza um estado/view vazia intercetiva, caso o array "cart" esteja vazio if (!cart.length) { return ( @@ -21,18 +28,32 @@ export default function Cart() { ); } + /** + * Lógica de Agrupamento de itens no Carrinho. + * A nível de negócio, como as encomendas (orders) são feitas por Barbearia (shopId), + * agrupamos para construir a interface dividida por Lojas caso de adicione itens mistos. + * @returns {Record} Dicionário indexado pelo shopId + */ const grouped = cart.reduce>((acc, item) => { acc[item.shopId] = acc[item.shopId] || []; acc[item.shopId].push(item); return acc; }, {}); + /** + * Acionado ao clicar em 'Finalizar pedido' para uma dada loja no JSX. + * Interliga através da função do Contexto à base de dados para materializar + * o agrupo de serviços selecionados e criar uma tupla na tabela de Pedidos/Marcações da db. + * @param {string} shopId - ID da loja que receberá o pedido final. + */ const handleCheckout = (shopId: string) => { + // Verificamos de forma segura pelo objeto user se o authState (sessão Supabase) existe if (!user) { Alert.alert('Login necessário', 'Faça login para finalizar o pedido'); navigation.navigate('Login' as never); return; } + // Gera a inserção na API (insert em tabelas de orders / ordem e dependentes) const order = placeOrder(user.id, shopId); if (order) { Alert.alert('Sucesso', 'Pedido criado com sucesso!'); @@ -40,10 +61,16 @@ export default function Cart() { }; return ( + // A página permite visibilidade escalonada num conteúdo flexível (ScrollView) Carrinho + + {/* Renderiza dinamicamente 1 Card de Checkout por Loja agrupada no objeto `grouped` */} {Object.entries(grouped).map(([shopId, items]) => { + // Mapeia o mock do objeto de loja baseado na primmary key `shopId` const shop = shops.find((s) => s.id === shopId); + + // Agregador quantitativo do array reduzindo o total financeiro calculado pelos preços da BD local const total = items.reduce((sum, i) => { const price = i.type === 'service' @@ -53,15 +80,21 @@ export default function Cart() { }, 0); return ( + // Engloba os items duma só loja + {/* Consome o nome e morada do registo principal (Profile > Shop) na UI */} {shop?.name ?? 'Barbearia'} {shop?.address} + {/* Apresenta o custo transformado visualmente (ex: R$ / €) */} {currency(total)} + + {/* Listagem linha a linha dos items (relacionados por foreign key 'refId') */} {items.map((i) => { + // JOIN via frontend para resgatar o nome natural referenciado no menu original da Lojas const ref = i.type === 'service' ? shop?.services.find((s) => s.id === i.refId) @@ -69,9 +102,12 @@ export default function Cart() { return ( + {/* Condicionamento estruturado na UI, mostra Serviço vs Produto perante a tipagem DB iterada */} {i.type === 'service' ? 'Serviço: ' : 'Produto: '} {ref?.name ?? 'Item'} x{i.qty} + + {/* Elimina de forma independente o registo não guardado da persistência AppContext/State */} + + {/* Redirecionamento direto com foreign key injetada para a view de Agendamentos */} + + {/* Botão nativo focado à inserção de utilizadores - Cria sessão no ecositema de Auth/BD */} Agendamentos + {/* Renderiza a lista se existirem marcações no percurso deste utilizador */} {myAppointments.length > 0 ? ( myAppointments.map((a) => { + // Resolve a associação relacional (a.shopId) obtendo os detalhes da barbearia const shop = shops.find((s) => s.id === a.shopId); return ( + {/* Nome exibido pós JOIN de array em memória */} {shop?.name} + + {/* O status (persistido na BD) influencia a cor devolvida ao Badge */} {a.status} {a.date} @@ -64,6 +84,7 @@ export default function Profile() { )} Pedidos + {/* Renderiza o histórico de compras de retalho/produtos usando idêntica lógica */} {myOrders.length > 0 ? ( myOrders.map((o) => { const shop = shops.find((s) => s.id === o.shopId); @@ -74,6 +95,7 @@ export default function Profile() { {o.status} + {/* Formatação Timestamp temporal da BD (createdAt) para modo visual PT */} {new Date(o.createdAt).toLocaleString('pt-BR')} {currency(o.total)} diff --git a/src/pages/ShopDetails.tsx b/src/pages/ShopDetails.tsx index cfd7027..e0accd7 100644 --- a/src/pages/ShopDetails.tsx +++ b/src/pages/ShopDetails.tsx @@ -1,3 +1,9 @@ +/** + * @file ShopDetails.tsx + * @description Página de detalhes de uma barbearia específica. + * Mostra a listagem de catálogos dividida por abas (Serviços / Produtos) e + * permite adicionar itens ao carrinho de compras ou transitar para Agendamento. + */ import React, { useState, useMemo } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; import { useRoute, useNavigation, RouteProp } from '@react-navigation/native'; @@ -10,13 +16,21 @@ import { currency } from '../lib/format'; import { RootStackParamList } from '../navigation/types'; export default function ShopDetails() { + // Apanha o parâmetro 'shopId' passado por rotas anteriores (ex: via Explore) const route = useRoute>(); const navigation = useNavigation>(); const { shopId } = route.params; + + // Instancia ferramentas de dados partilhadas do State (a simular uma API de comunicação) const { shops, addToCart } = useApp(); + + // Otimiza o re-render filtrando e selecionando apenas a entidade visualizada na DB const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]); + + // Estado local contidiano para gerir as abas visíveis (Tabs de Conteúdos) const [tab, setTab] = useState<'servicos' | 'produtos'>('servicos'); + // Fallback visual de navegação inválida para o caso da barbearia não constar if (!shop) { return ( @@ -26,10 +40,13 @@ export default function ShopDetails() { } return ( + // Contentor em Scroll adaptável horizontal/vertical em smartphones reduzidos + {/* Cabeçalho superior: Informações imutáveis populadas pelos profiles preenchidos */} {shop.name} {shop.address} + {/* Call to action de elevado destaque que incia form Booking */} + {/* Controladores de abas visuais iterando o estado (setTab) */} + {/* Renderização Condicionada pela Tab ativa */} {tab === 'servicos' ? ( + // Apresenta o catálogo relacionado com 'serviços' da tabela/coleção na BD da referida loja {shop.services.map((service) => ( @@ -62,6 +82,8 @@ export default function ShopDetails() { {currency(service.price)} Duração: {service.duration} min + + {/* Função addToCart despacha dados para Context agregador permitindo checkout posterior */} diff --git a/web/src/pages/Cart.tsx b/web/src/pages/Cart.tsx index 35e233d..b7000ed 100644 --- a/web/src/pages/Cart.tsx +++ b/web/src/pages/Cart.tsx @@ -1,9 +1,15 @@ +/** + * @file Cart.tsx + * @description Rota principal da view do Carrinho. Atua meramente + * como _wrapper_ injetando um componente funcional isolado 'CartPanel'. + */ import { CartPanel } from '../components/CartPanel'; export default function Cart() { return (

Carrinho

+ {/* Componente que gere a lógica do Array de cart itens, e envio para backend */}
); diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index 6b49c47..6f7b546 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -1,3 +1,9 @@ +/** + * @file Dashboard.tsx + * @description Painel principal de controlo ('Dashboard') para barbearias na web. + * Consolida a visualização de Agendamentos, Histórico, Pedidos (produtos), + * Gestão de Serviços/Produtos e Perfis de Barbeiros numa única view complexa. + */ import { useMemo, useState } from 'react'; import { Card } from '../components/ui/card'; import { Button } from '../components/ui/button'; @@ -64,6 +70,7 @@ const parseDate = (value: string) => new Date(value.replace(' ', 'T')); type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers'; export default function Dashboard() { + // Importa estado global interligado com Supabase (ex: updateAppointmentStatus reflete-se na BD) const { user, users, @@ -106,10 +113,14 @@ export default function Dashboard() { if (!shop) return
Barbearia não encontrada.
; const periodMatch = periods[period]; + + // Agendamentos filtrados pela barbearia logada e pela janela de tempo selecionada const allShopAppointments = appointments.filter((a) => a.shopId === shop.id && periodMatch(parseDate(a.date))); - // Agendamentos ativos (não concluídos) + + // Agendamentos ativos (não concluídos e não cancelados) const shopAppointments = allShopAppointments.filter((a) => a.status !== 'concluido'); - // Agendamentos concluídos (histórico) + + // Agendamentos concluídos (histórico passado) const completedAppointments = allShopAppointments.filter((a) => a.status === 'concluido'); // Estatísticas para lista de marcações (do dia selecionado) @@ -124,14 +135,14 @@ export default function Dashboard() { }); 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; @@ -145,7 +156,7 @@ export default function Dashboard() { // Filtrar agendamentos para lista const filteredAppointments = useMemo(() => { let filtered = selectedDateAppointments; - + if (!includeCancelled) { filtered = filtered.filter((a) => a.status !== 'cancelado'); } @@ -288,11 +299,10 @@ export default function Dashboard() { @@ -380,7 +390,7 @@ export default function Dashboard() { const aptDate = new Date(a.date.replace(' ', 'T')); return aptDate.toDateString() === today.toDateString(); }); - + if (todayAppts.length === 0) { return (
@@ -393,7 +403,7 @@ export default function Dashboard() {
); } - + return (
{todayAppts.slice(0, 3).map(a => { @@ -402,7 +412,7 @@ export default function Dashboard() { 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 (
@@ -517,7 +527,7 @@ export default function Dashboard() { 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 (
@@ -548,22 +558,20 @@ export default function Dashboard() {
- {selectedDate.toLocaleDateString('pt-PT', { - weekday: 'long', - day: 'numeric', - month: 'numeric', - year: 'numeric' + {selectedDate.toLocaleDateString('pt-PT', { + weekday: 'long', + day: 'numeric', + month: 'numeric', + year: 'numeric' })}
@@ -776,10 +783,10 @@ export default function Dashboard() { {s === 'pendente' ? 'Pendente' : s === 'confirmado' - ? 'Confirmado' - : s === 'concluido' - ? 'Concluído' - : 'Cancelado'} + ? 'Confirmado' + : s === 'concluido' + ? 'Concluído' + : 'Cancelado'} ))} @@ -972,9 +979,8 @@ export default function Dashboard() { {shop.products.map((p) => (
diff --git a/web/src/pages/EventsCreate.tsx b/web/src/pages/EventsCreate.tsx index 52d3e12..6a8dc54 100644 --- a/web/src/pages/EventsCreate.tsx +++ b/web/src/pages/EventsCreate.tsx @@ -1,4 +1,9 @@ -// src/pages/EventsCreate.tsx +/** + * @file EventsCreate.tsx + * @description Formulário para a criação de novos Eventos customizados na aplicação Web. + * Interage diretamente com a função `createEvent` da biblioteca do Supabase, formatando e guardando + * registos de calendário associados ao Utilizador em sessão. + */ import { FormEvent, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Card } from '../components/ui/card' @@ -16,6 +21,10 @@ export default function EventsCreate() { const [saving, setSaving] = useState(false) const [error, setError] = useState('') + /** + * Valida e submete o formulário, criando uma Data ISO string a partir do input nativo do browser. + * Aciona pedido à Base de Dados via Supabase Client (em '../lib/events'). + */ async function onSubmit(e: FormEvent) { e.preventDefault() setError('') diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 15f1588..1da73a6 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -1,3 +1,9 @@ +/** + * @file Explore.tsx + * @description View de Exploração de barbearias na versão Web. + * Consome do estado global (`useApp`) a lista consolidada das "shops" populadas + * pela base de dados e aplica filtros do lado do cliente baseados na query, tipo e ordenação. + */ import { useMemo, useState } from 'react'; import { ShopCard } from '../components/ShopCard'; import { Card } from '../components/ui/card'; @@ -8,15 +14,24 @@ import { Search } from 'lucide-react'; export default function Explore() { const { shops } = useApp(); + + // Estados para manter as seleções de filtragem const [query, setQuery] = useState(''); const [filter, setFilter] = useState<'todas' | 'top' | 'produtos' | 'barbeiros' | 'servicos'>('todas'); const [sortBy, setSortBy] = useState<'relevancia' | 'avaliacao' | 'preco' | 'servicos'>('relevancia'); + /** + * Deriva a lista de Shops tratada a partir do conjunto mestre global. + * Só recalcula quando os raw `shops` ou os critérios de pesquisa se alteram. + */ const filtered = useMemo(() => { const normalized = query.trim().toLowerCase(); + + // Regra 1: Combinação livre por correspondência parcial textual (Nome/Morada) const matchesQuery = (name: string, address: string) => !normalized || name.toLowerCase().includes(normalized) || address.toLowerCase().includes(normalized); + // Regra 2: Restrições de Chip const passesFilter = (shop: (typeof shops)[number]) => { if (filter === 'top') return shop.rating >= 4.7; if (filter === 'produtos') return shop.products.length > 0; @@ -25,6 +40,7 @@ export default function Explore() { return true; }; + // Aplicação condicional com Sort const sorted = [...shops] .filter((shop) => matchesQuery(shop.name, shop.address)) .filter(passesFilter) @@ -32,10 +48,13 @@ export default function Explore() { if (sortBy === 'avaliacao') return b.rating - a.rating; if (sortBy === 'servicos') return b.services.length - a.services.length; if (sortBy === 'preco') { + // Extrai o preço mínimo nos serviços oferecidos e compara const aMin = Math.min(...a.services.map((s) => s.price)); const bMin = Math.min(...b.services.map((s) => s.price)); return aMin - bMin; } + + // Critério por defeito ou quebra de empate: Avaliação descendente if (b.rating !== a.rating) return b.rating - a.rating; return b.services.length - a.services.length; }); diff --git a/web/src/pages/Landing.tsx b/web/src/pages/Landing.tsx index 874478a..ad5396a 100644 --- a/web/src/pages/Landing.tsx +++ b/web/src/pages/Landing.tsx @@ -1,9 +1,15 @@ +/** + * @file Landing.tsx + * @description Página de destino (Landing Page) da aplicação web. + * Serve como vitrine promocional e porta de entrada (Login/Registo - Call to Actions). + * Redireciona autonomamente utilizadores já autenticados para as suas áreas reservadas. + */ import { Link, useNavigate } from 'react-router-dom'; import { Button } from '../components/ui/button'; import { Card } from '../components/ui/card'; import { ShopCard } from '../components/ShopCard'; -import { - Calendar, ShoppingBag, BarChart3, Sparkles, +import { + Calendar, ShoppingBag, BarChart3, Sparkles, Users, Clock, Shield, TrendingUp, CheckCircle2, ArrowRight, Star, Quote, Scissors, MapPin, Zap, Smartphone, Globe @@ -31,23 +37,23 @@ export default function Landing() {
- +
Revolucione sua barbearia
- +

Agendamentos, produtos e gestão em um{' '} único lugar

- +

- Experiência mobile-first para clientes e painel completo para barbearias. + Experiência mobile-first para clientes e painel completo para barbearias. Simplifique a gestão do seu negócio e aumente sua receita.

- +
- +
- {[ - { - icon: Calendar, - title: 'Agendamentos Inteligentes', + {[ + { + 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' - }, - { - icon: Users, - title: 'Gestão de Barbeiros', + color: 'from-purple-500 to-purple-600' + }, + { + icon: Users, + title: 'Gestão de Barbeiros', desc: 'Gerencie horários, especialidades e disponibilidade de cada barbeiro. Calendário integrado.', color: 'from-indigo-500 to-indigo-600' }, - { - icon: Clock, - title: 'Horários Flexíveis', + { + icon: Clock, + title: 'Horários Flexíveis', desc: 'Configure horários de funcionamento, intervalos e disponibilidade. Sistema automático de bloqueio.', color: 'from-orange-500 to-orange-600' }, - { - icon: Shield, - title: 'Seguro e Confiável', + { + icon: Shield, + title: 'Seguro e Confiável', desc: 'Dados protegidos, pagamentos seguros e backup automático. Conformidade com LGPD.', color: 'from-rose-500 to-rose-600' }, - ].map((feature) => ( - -
- -
-
-

{feature.title}

-

{feature.desc}

-
-
- ))} + ].map((feature) => ( + +
+ +
+
+

{feature.title}

+

{feature.desc}

+
+
+ ))}
@@ -151,7 +157,7 @@ export default function Landing() { Simples, rápido e eficiente em 3 passos

- +
{[ { step: '1', title: 'Explore', desc: 'Navegue pelas barbearias disponíveis, veja avaliações e serviços oferecidos.' }, @@ -187,13 +193,13 @@ export default function Landing() {
- +
{featuredShops.map((shop) => ( ))}
- +
- +
{[ - { - name: 'João Silva', + { + name: 'João Silva', role: 'Cliente', text: 'Facilita muito agendar meu corte. Interface simples e rápida. Recomendo!', rating: 5 }, - { - name: 'Carlos Mendes', + { + name: 'Carlos Mendes', role: 'Proprietário', text: 'O painel é completo e me ajuda muito na gestão. Aumentou minha organização.', rating: 5 }, - { - name: 'Miguel Santos', + { + name: 'Miguel Santos', role: 'Cliente', text: 'Nunca mais perco horário. As notificações são muito úteis.', rating: 5 @@ -299,13 +305,13 @@ export default function Landing() {
- +

Pronto para começar?

- Junte-se a centenas de barbearias que já estão usando a Smart Agenda + Junte-se a centenas de barbearias que já estão usando a Smart Agenda para revolucionar seus negócios.

diff --git a/web/src/pages/Profile.tsx b/web/src/pages/Profile.tsx index b430829..b3aa1b5 100644 --- a/web/src/pages/Profile.tsx +++ b/web/src/pages/Profile.tsx @@ -1,4 +1,8 @@ -// src/pages/Profile.tsx +/** + * @file Profile.tsx + * @description Página do Perfil do utilizador web. Carrega a sessão atual a partir do Supabase, + * interceta eventos customizados, agendamentos associados ao `customerId` e pedidos de produtos (Orders). + */ import { useEffect, useMemo, useState } from 'react' import { Link, useLocation, useNavigate } from 'react-router-dom' import { Card } from '../components/ui/card' @@ -38,23 +42,28 @@ export default function Profile() { const [loadingEvents, setLoadingEvents] = useState(true) const [eventsError, setEventsError] = useState('') + /** + * Obtém de forma segura o objeto de "getUser" fornecido pelo Supabase Auth. + * Não despoleta "flash" de erros se o utilizador não tiver credenciais, mas + * em caso positivo armazena Auth Id e Email globalmente na View. + */ useEffect(() => { let mounted = true - ;(async () => { - const { data, error } = await supabase.auth.getUser() - if (!mounted) return + ; (async () => { + const { data, error } = await supabase.auth.getUser() + if (!mounted) return - if (error || !data.user) { - setAuthEmail('') - setAuthId('') - } else { - setAuthEmail(data.user.email ?? '') - setAuthId(data.user.id) - } + if (error || !data.user) { + setAuthEmail('') + setAuthId('') + } else { + setAuthEmail(data.user.email ?? '') + setAuthId(data.user.id) + } - setLoadingAuth(false) - })() + setLoadingAuth(false) + })() return () => { mounted = false diff --git a/web/src/pages/ShopDetails.tsx b/web/src/pages/ShopDetails.tsx index 0a5a29f..55256ce 100644 --- a/web/src/pages/ShopDetails.tsx +++ b/web/src/pages/ShopDetails.tsx @@ -1,3 +1,9 @@ +/** + * @file ShopDetails.tsx + * @description Vista de Detalhes de uma Barbearia na Web. + * Apresenta informações do estabelecimento (foto, avaliação, mapa), + * listagens de Serviços e Produtos disponíveis em Tabs. + */ import { Link, useParams } from 'react-router-dom'; import { useMemo, useState } from 'react'; import { Tabs } from '../components/ui/tabs';