docs: Add JSDoc and inline comments to enhance code readability and explain functionality across multiple pages and components.

This commit is contained in:
2026-02-26 10:38:13 +00:00
parent c4164e50a0
commit 5b866a161e
25 changed files with 675 additions and 174 deletions

View File

@@ -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
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* O componente Card encapsula de forma visual os inputs de login */}
<Card style={styles.card}>
<Text style={styles.title}>Bem-vindo de volta</Text>
<Text style={styles.subtitle}>Entre na sua conta para continuar</Text>
{/* Bloco temporário para dados demo */}
<View style={styles.demoBox}>
<Text style={styles.demoTitle}>💡 Conta demo:</Text>
<Text style={styles.demoText}>Cliente: cliente@demo.com / 123</Text>
<Text style={styles.demoText}>Barbearia: barber@demo.com / 123</Text>
</View>
{/* Input controlado pelo estadoReact "email"; atualiza à medida que se digita */}
<Input
label="Email"
value={email}
@@ -49,6 +66,7 @@ export default function AuthLogin() {
placeholder="seu@email.com"
/>
{/* Input controlado para a password; secureTextEntry oculta os caracteres */}
<Input
label="Senha"
value={password}
@@ -61,10 +79,12 @@ export default function AuthLogin() {
error={error}
/>
{/* Botão de ação que dispara a função handleSubmit para processar as credenciais */}
<Button onPress={handleSubmit} style={styles.submitButton} size="lg">
Entrar
</Button>
{/* Estrutura de rodapé para navegação alternativa (redirecionar para o ecrã de registo) */}
<View style={styles.footer}>
<Text style={styles.footerText}>Não tem conta? </Text>
<Text

View File

@@ -1,3 +1,8 @@
/**
* @file AuthRegister.tsx
* @description Página de registo da aplicação. Permite criar novas contas para clientes
* ou barbearias comunicando com o contexto de autenticação global e a base de dados.
*/
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, Alert, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
@@ -9,31 +14,43 @@ import { Card } from '../components/ui/Card';
export default function AuthRegister() {
const navigation = useNavigation();
const { register } = useApp();
// Estados locais para controlar os campos de introdução de dados
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// O tipo de utilizador é estritamente tipado (cliente ou barbearia) para garantir a integridade dos dados
const [role, setRole] = useState<'cliente' | 'barbearia'>('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
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* O Card agrupa a interface de registo */}
<Card style={styles.card}>
<Text style={styles.title}>Criar conta</Text>
<Text style={styles.subtitle}>Escolha o tipo de acesso</Text>
{/* Grupo de botões de seleção de papel. O CSS ativo altera com base no estado `role`. */}
<View style={styles.roleContainer}>
<TouchableOpacity
style={[styles.roleButton, role === 'cliente' && styles.roleButtonActive]}
@@ -53,6 +70,7 @@ export default function AuthRegister() {
</TouchableOpacity>
</View>
{/* Input simples para o nome, que atualiza a variável React de estado "name" */}
<Input
label="Nome completo"
value={name}
@@ -60,6 +78,7 @@ export default function AuthRegister() {
placeholder="João Silva"
/>
{/* Input específico de email - autoCapitalize=none evita letras maiúsculas automáticas */}
<Input
label="Email"
value={email}
@@ -73,6 +92,7 @@ export default function AuthRegister() {
error={error}
/>
{/* Input para password mascarada com secureTextEntry para esconder os carateres */}
<Input
label="Senha"
value={password}
@@ -81,6 +101,8 @@ export default function AuthRegister() {
placeholder="••••••••"
/>
{/* Renderização condicional do JSX: o campo shopName só é renderizado
se o papel na base de dados (e estado) for 'barbearia' */}
{role === 'barbearia' && (
<Input
label="Nome da barbearia"
@@ -90,10 +112,12 @@ export default function AuthRegister() {
/>
)}
{/* Botão de chamada à ação principal que aciona a submissão dos dados (handleSubmit) */}
<Button onPress={handleSubmit} style={styles.submitButton} size="lg">
Criar conta
</Button>
{/* Seção rodapé simples com reencaminhamento de navegação para a página de Login */}
<View style={styles.footer}>
<Text style={styles.footerText}> tem conta? </Text>
<Text

View File

@@ -1,3 +1,9 @@
/**
* @file Booking.tsx
* @description Página de agendamento de serviços. Permite ao utilizador (cliente)
* interagir visualmente com as disponibilidades, horários e serviços de uma barbearia
* consumindo as informações do estado/BD da aplicação.
*/
import React, { useState, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
@@ -11,15 +17,22 @@ import { currency } from '../lib/format';
export default function Booking() {
const route = useRoute();
const navigation = useNavigation();
// Recebe o ID da barbearia a partir dos parâmetros de navegação
const { shopId } = route.params as { shopId: string };
// Extrai as entidades e os métodos de interação com a base de dados (através do AppContext)
const { shops, createAppointment, user, appointments } = useApp();
// Encontra a barbearia correspondente tipada via query local na array 'shops' (similar a um SELECT * WHERE id = shopId)
const shop = useMemo(() => 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 (
<View style={styles.container}>
@@ -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
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Agendar em {shop.name}</Text>
<Card style={styles.card}>
<Text style={styles.sectionTitle}>1. Escolha o serviço</Text>
{/* Renderiza um botão (bloco flexível) por cada serviço (ex: Corte, Barba) vindos do mapeamento DB */}
<View style={styles.grid}>
{shop.services.map((s) => (
<TouchableOpacity
@@ -99,6 +147,7 @@ export default function Booking() {
</View>
<Text style={styles.sectionTitle}>2. Escolha o barbeiro</Text>
{/* Renderiza os profissionais, normalmente provindo dum JOIN na base de dados (tabela barbeiros + barbearia) */}
<View style={styles.barberContainer}>
{shop.barbers.map((b) => (
<TouchableOpacity
@@ -114,6 +163,7 @@ export default function Booking() {
</View>
<Text style={styles.sectionTitle}>3. Escolha a data</Text>
{/* Componente simples de input que deverá mapear para a inserção final do timestamp Postgres */}
<Input
value={date}
onChangeText={setDate}
@@ -121,6 +171,7 @@ export default function Booking() {
/>
<Text style={styles.sectionTitle}>4. Escolha o horário</Text>
{/* Lista mapeada e computada: Apenas slots `available` (que passaram pela query preventiva de duplicação) */}
<View style={styles.slotsContainer}>
{availableSlots.length > 0 ? (
availableSlots.map((h) => (
@@ -137,6 +188,7 @@ export default function Booking() {
)}
</View>
{/* Quadro resumo: Apenas mostrado se o estado interno conter todas as variáveis relacionais */}
{canSubmit && selectedService && (
<View style={styles.summary}>
<Text style={styles.summaryTitle}>Resumo</Text>
@@ -147,6 +199,7 @@ export default function Booking() {
</View>
)}
{/* Botão para concretizar o INSERT na base de dados com as validações pré-acionadas */}
<Button onPress={submit} disabled={!canSubmit} style={styles.submitButton} size="lg">
{user ? 'Confirmar agendamento' : 'Entrar para agendar'}
</Button>

View File

@@ -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 (
<View style={styles.container}>
@@ -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<string, CartItem[]>} Dicionário indexado pelo shopId
*/
const grouped = cart.reduce<Record<string, typeof cart>>((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)
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Carrinho</Text>
{/* 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
<Card key={shopId} style={styles.shopCard}>
<View style={styles.shopHeader}>
<View>
{/* Consome o nome e morada do registo principal (Profile > Shop) na UI */}
<Text style={styles.shopName}>{shop?.name ?? 'Barbearia'}</Text>
<Text style={styles.shopAddress}>{shop?.address}</Text>
</View>
{/* Apresenta o custo transformado visualmente (ex: R$ / €) */}
<Text style={styles.total}>{currency(total)}</Text>
</View>
{/* 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 (
<View key={i.refId} style={styles.item}>
<Text style={styles.itemText}>
{/* 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}
</Text>
{/* Elimina de forma independente o registo não guardado da persistência AppContext/State */}
<Button
onPress={() => removeFromCart(i.refId)}
variant="ghost"
@@ -82,6 +118,8 @@ export default function Cart() {
</View>
);
})}
{/* Renderização condicional no React para encaminhar fluxo para login se anónimo */}
{user ? (
<Button onPress={() => handleCheckout(shopId)} style={styles.checkoutButton}>
Finalizar pedido

View File

@@ -1,3 +1,9 @@
/**
* @file Dashboard.tsx
* @description Painel principal (Dashboard) destinado exclusivamente a utilizadores
* do tipo 'barbearia'. Permite a gestão integral do negócio: marcações, pedidos,
* serviços prestados, produtos e equipa (barbeiros).
*/
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { useNavigation } from '@react-navigation/native';
@@ -10,6 +16,7 @@ import { currency } from '../lib/format';
export default function Dashboard() {
const navigation = useNavigation();
// Resgata variáveis e ações modificadoras do Contexto (ponte para o backend/Supabase)
const {
user,
shops,
@@ -27,9 +34,11 @@ export default function Dashboard() {
logout,
} = useApp();
// Garante a entidade da barbearia atual baseada na Foreign Key armazenada no utilizador logado
const shop = shops.find((s) => s.id === user?.shopId);
const [activeTab, setActiveTab] = useState<'overview' | 'appointments' | 'orders' | 'services' | 'products' | 'barbers'>('overview');
// Estados locais dos subformulários na página para Adicionar entidades
const [svcName, setSvcName] = useState('');
const [svcPrice, setSvcPrice] = useState('50');
const [svcDuration, setSvcDuration] = useState('30');
@@ -39,6 +48,7 @@ export default function Dashboard() {
const [barberName, setBarberName] = useState('');
const [barberSpecs, setBarberSpecs] = useState('');
// Segurança de Bloqueio - Validação estrita do role do utilziador no componente
if (!user || user.role !== 'barbearia') {
return (
<View style={styles.container}>
@@ -47,6 +57,7 @@ export default function Dashboard() {
);
}
// Fallback no caso da loja ainda não estar populada no join do utilizador
if (!shop) {
return (
<View style={styles.container}>
@@ -55,24 +66,36 @@ export default function Dashboard() {
);
}
// Consultas de agregação de uso local sobre as variações gerais do state (como um SELECT com WHERE)
const shopAppointments = appointments.filter((a) => a.shopId === shop.id);
const shopOrders = orders.filter((o) => o.shopId === shop.id);
const completedAppointments = shopAppointments.filter((a) => a.status === 'concluido');
const activeAppointments = shopAppointments.filter((a) => a.status !== 'concluido');
const productOrders = shopOrders.filter((o) => o.items.some((i) => i.type === 'product'));
// Métricas agregadas globais calculadas dinamicamente
const totalRevenue = shopOrders.reduce((s, o) => s + o.total, 0);
const lowStock = shop.products.filter((p) => p.stock <= 3);
/**
* Adiciona um novo serviço disponível na barbearia.
* Invoca "addService", efetuando em backend uma lógica de `supabase.from('services').insert(...)`
* relacionando com o `shop.id`.
*/
const addNewService = () => {
if (!svcName.trim()) return;
addService(shop.id, { name: svcName, price: Number(svcPrice) || 0, duration: Number(svcDuration) || 30, barberIds: [] });
// Limpeza formulário
setSvcName('');
setSvcPrice('50');
setSvcDuration('30');
Alert.alert('Sucesso', 'Serviço adicionado');
};
/**
* Adiciona um novo produto de venda.
* Interliga e armazena informando a Foreign Key da loja `shop.id`.
*/
const addNewProduct = () => {
if (!prodName.trim()) return;
addProduct(shop.id, { name: prodName, price: Number(prodPrice) || 0, stock: Number(prodStock) || 0 });
@@ -82,6 +105,11 @@ export default function Dashboard() {
Alert.alert('Sucesso', 'Produto adicionado');
};
/**
* Adiciona um membro (barbeiro) à gestão da equipa.
* Modifica a base de dados (ex: `supabase.from('barbers').insert({...})`)
* inicializando com estrutura base de interface.
*/
const addNewBarber = () => {
if (!barberName.trim()) return;
addBarber(shop.id, {
@@ -94,6 +122,12 @@ export default function Dashboard() {
Alert.alert('Sucesso', 'Barbeiro adicionado');
};
/**
* Atualiza a quantidade de inventário de um produto iterando a variação (+1/-1).
* Desencadeia um update para a tabela products (ex: `supabase.from('products').update(...)`) evitando stocks negativos.
* @param {string} productId - O identificador único do produto afetado.
* @param {number} delta - A quantidade exata matemática a variar da realidade material.
*/
const updateProductStock = (productId: string, delta: number) => {
const product = shop.products.find((p) => p.id === productId);
if (!product) return;

View File

@@ -1,3 +1,9 @@
/**
* @file Explore.tsx
* @description Página de exploração. Apresenta uma listagem geral de barbearias
* disponíveis na plataforma onde os clientes podem visualizar detalhes e
* iniciar o fluxo de agendamento (consome dados das Lojas da BD).
*/
import React from 'react';
import { View, Text, StyleSheet, ScrollView, FlatList } from 'react-native';
import { useNavigation } from '@react-navigation/native';
@@ -11,28 +17,38 @@ import { RootStackParamList } from '../navigation/types';
export default function Explore() {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
// Obtém a lista completa de barbearias carregadas no estado global (equivalente a "SELECT * FROM shops")
const { shops } = useApp();
return (
// Componente raiz do ecã de exploração
<View style={styles.container}>
<Text style={styles.title}>Explorar barbearias</Text>
{/* FlatList é o componente nativo otimizado para renderizar grandes arrays de dados provenientes da BD */}
<FlatList
data={shops}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
renderItem={({ item: shop }) => (
// Cada Card rege a UI de uma barbearia com dados mapeados reais/mock
<Card style={styles.shopCard}>
<View style={styles.shopHeader}>
<Text style={styles.shopName}>{shop.name}</Text>
<Badge color="amber">{shop.rating.toFixed(1)} </Badge>
</View>
<Text style={styles.shopAddress}>{shop.address}</Text>
<View style={styles.shopInfo}>
{/* O atributo length do array simula as rows de relacionamentos 1-para-N (serviços) */}
<Text style={styles.shopInfoText}>{shop.services.length} serviços</Text>
<Text style={styles.shopInfoText}></Text>
{/* Mapeamento de dimensão da relation 'barbers' */}
<Text style={styles.shopInfoText}>{shop.barbers.length} barbeiros</Text>
</View>
<View style={styles.buttons}>
{/* Botão direciona para as informações, passando o ID primário (shop.id) pelo React Navigation */}
<Button
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
variant="outline"
@@ -40,6 +56,8 @@ export default function Explore() {
>
Ver detalhes
</Button>
{/* Redirecionamento direto com foreign key injetada para a view de Agendamentos */}
<Button
onPress={() => navigation.navigate('Booking', { shopId: shop.id })}
style={styles.button}

View File

@@ -1,3 +1,8 @@
/**
* @file Landing.tsx
* @description Página principal introdutória (Landing Page) não protegida. Serve
* como "home" e apresentação da aplicação, reencaminhando para Login/Explorar.
*/
import React from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
@@ -8,7 +13,9 @@ export default function Landing() {
const navigation = useNavigation();
return (
// Interface principal agrupando o "hero" e "features"
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Hero section: elemento apelativo para captar valor (exibição de marca) */}
<View style={styles.hero}>
<Text style={styles.heroTitle}>Smart Agenda</Text>
<Text style={styles.heroSubtitle}>
@@ -18,6 +25,7 @@ export default function Landing() {
Experiência mobile-first para clientes e painel completo para barbearias.
</Text>
<View style={styles.buttons}>
{/* Este fluxo permite utilizadores visitarem dados públicos da plataforma via Explore */}
<Button
onPress={() => navigation.navigate('Explore' as never)}
style={styles.button}
@@ -25,6 +33,8 @@ export default function Landing() {
>
Explorar barbearias
</Button>
{/* Botão nativo focado à inserção de utilizadores - Cria sessão no ecositema de Auth/BD */}
<Button
onPress={() => navigation.navigate('Register' as never)}
variant="outline"
@@ -37,6 +47,7 @@ export default function Landing() {
</View>
<View style={styles.features}>
{/* Componentes estáticos descritivos sobre as features que mapeiam para funcionalidades da BD */}
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Agendamentos</Text>
<Text style={styles.featureDesc}>

View File

@@ -1,3 +1,8 @@
/**
* @file Profile.tsx
* @description Página do perfil de utilizador logado. Exibe os dados do cliente
* ou barbearia, bem como o histórico de agendamentos e pedidos vinculados à conta.
*/
import React from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
@@ -7,6 +12,7 @@ import { Badge } from '../components/ui/Badge';
import { Button } from '../components/ui/Button';
import { currency } from '../lib/format';
// Mapeamento visual estático das strings de estado do Postgres/State para cores da UI
const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
pendente: 'amber',
confirmado: 'green',
@@ -16,8 +22,10 @@ const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
export default function Profile() {
const navigation = useNavigation();
// Obtém sessão do utilizador (auth) e listas globais da BD (appointments e orders)
const { user, appointments, orders, shops, logout } = useApp();
// Guarda/Bloqueio protetor para forçar navegação ou alertar utilizadores anónimos
if (!user) {
return (
<View style={styles.container}>
@@ -26,30 +34,42 @@ export default function Profile() {
);
}
// Filtragem (equivalente a queries com cláusula WHERE customerId = ?) para obter o histórico individual
const myAppointments = appointments.filter((a) => a.customerId === user.id);
const myOrders = orders.filter((o) => o.customerId === user.id);
return (
// Contentor com ScrollView adaptável (evita cortes em ecrãs pequenos)
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Bloco de identificação do Utilizador validado pela API (Supabase Auth) */}
<Card style={styles.profileCard}>
<Text style={styles.profileName}>Olá, {user.name}</Text>
<Text style={styles.profileEmail}>{user.email}</Text>
{/* Distanciamento visual e lógica dos tipos de perfil 'role' presentes na BD */}
<Badge color="amber" style={styles.roleBadge}>
{user.role === 'cliente' ? 'Cliente' : 'Barbearia'}
</Badge>
{/* Limpa a sessão ativa e tokens memorizados de Login */}
<Button onPress={logout} variant="outline" style={styles.logoutButton}>
Sair
</Button>
</Card>
<Text style={styles.sectionTitle}>Agendamentos</Text>
{/* 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 (
<Card key={a.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
{/* Nome exibido pós JOIN de array em memória */}
<Text style={styles.itemName}>{shop?.name}</Text>
{/* O status (persistido na BD) influencia a cor devolvida ao Badge */}
<Badge color={statusColor[a.status]}>{a.status}</Badge>
</View>
<Text style={styles.itemDate}>{a.date}</Text>
@@ -64,6 +84,7 @@ export default function Profile() {
)}
<Text style={styles.sectionTitle}>Pedidos</Text>
{/* 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() {
<Badge color={statusColor[o.status]}>{o.status}</Badge>
</View>
<Text style={styles.itemDate}>
{/* Formatação Timestamp temporal da BD (createdAt) para modo visual PT */}
{new Date(o.createdAt).toLocaleString('pt-BR')}
</Text>
<Text style={styles.itemTotal}>{currency(o.total)}</Text>

View File

@@ -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<RouteProp<RootStackParamList, 'ShopDetails'>>();
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
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 (
<View style={styles.container}>
@@ -26,10 +40,13 @@ export default function ShopDetails() {
}
return (
// Contentor em Scroll adaptável horizontal/vertical em smartphones reduzidos
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Cabeçalho superior: Informações imutáveis populadas pelos profiles preenchidos */}
<View style={styles.header}>
<Text style={styles.title}>{shop.name}</Text>
<Text style={styles.address}>{shop.address}</Text>
{/* Call to action de elevado destaque que incia form Booking */}
<Button
onPress={() => navigation.navigate('Booking', { shopId: shop.id })}
style={styles.bookButton}
@@ -38,6 +55,7 @@ export default function ShopDetails() {
</Button>
</View>
{/* Controladores de abas visuais iterando o estado (setTab) */}
<View style={styles.tabs}>
<TouchableOpacity
style={[styles.tab, tab === 'servicos' && styles.tabActive]}
@@ -53,7 +71,9 @@ export default function ShopDetails() {
</TouchableOpacity>
</View>
{/* 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
<View style={styles.list}>
{shop.services.map((service) => (
<Card key={service.id} style={styles.itemCard}>
@@ -62,6 +82,8 @@ export default function ShopDetails() {
<Text style={styles.itemPrice}>{currency(service.price)}</Text>
</View>
<Text style={styles.itemDesc}>Duração: {service.duration} min</Text>
{/* Função addToCart despacha dados para Context agregador permitindo checkout posterior */}
<Button
onPress={() => addToCart({ shopId: shop.id, type: 'service', refId: service.id, qty: 1 })}
size="sm"
@@ -73,6 +95,7 @@ export default function ShopDetails() {
))}
</View>
) : (
// Retorno divergente: inventário global visivelmente iterado em componentes
<View style={styles.list}>
{shop.products.map((product) => (
<Card key={product.id} style={styles.itemCard}>
@@ -81,7 +104,11 @@ export default function ShopDetails() {
<Text style={styles.itemPrice}>{currency(product.price)}</Text>
</View>
<Text style={styles.itemDesc}>Stock: {product.stock} unidades</Text>
{/* Alerta de urgência de reposição assente numa regra simples de negócios matemática */}
{product.stock <= 3 && <Badge color="amber" style={styles.stockBadge}>Stock baixo</Badge>}
{/* Botão em React é afetado logicamente face à impossibilidade material de encomenda */}
<Button
onPress={() => addToCart({ shopId: shop.id, type: 'product', refId: product.id, qty: 1 })}
size="sm"