feat: Implement initial application structure, core pages, UI components, and Supabase backend integration.

This commit is contained in:
Rodrigo Lopes dos Santos
2026-03-16 01:30:28 +00:00
parent 8ece90a37e
commit 0270a6cbdf
49 changed files with 2122 additions and 797 deletions

View File

@@ -43,8 +43,8 @@ export default function AuthLogin() {
<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>
<Text style={styles.title}>Bem-vindo</Text>
<Text style={styles.subtitle}>Aceda à sua conta</Text>
{/* Bloco temporário para dados demo */}
<View style={styles.demoBox}>
@@ -91,7 +91,7 @@ export default function AuthLogin() {
style={styles.footerLink}
onPress={() => navigation.navigate('Register' as never)}
>
Criar conta
Criar Conta
</Text>
</View>
</Card>
@@ -126,9 +126,9 @@ const styles = StyleSheet.create({
textAlign: 'center',
},
demoBox: {
backgroundColor: '#fef3c7',
backgroundColor: '#e0e7ff',
borderWidth: 1,
borderColor: '#fbbf24',
borderColor: '#6366f1',
borderRadius: 8,
padding: 12,
marginBottom: 20,
@@ -136,12 +136,12 @@ const styles = StyleSheet.create({
demoTitle: {
fontSize: 12,
fontWeight: '600',
color: '#92400e',
color: '#4338ca',
marginBottom: 4,
},
demoText: {
fontSize: 11,
color: '#92400e',
color: '#4338ca',
},
submitButton: {
width: '100%',

View File

@@ -31,6 +31,14 @@ export default function Booking() {
const [barberId, setBarber] = useState('');
const [date, setDate] = useState('');
const [slot, setSlot] = useState('');
const [reminderMinutes, setReminderMinutes] = useState(1440); // 24h por padrão
const reminderOptions = [
{ label: '10 min', value: 10 },
{ label: '30 min', value: 30 },
{ label: '1 hora', value: 60 },
{ label: '24 horas', value: 1440 },
];
// Fallback visual caso ocorra um erro a obter o ID requisitado
if (!shop) {
@@ -99,7 +107,7 @@ export default function Booking() {
* 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 = () => {
const submit = async () => {
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');
@@ -109,12 +117,13 @@ export default function Booking() {
if (!canSubmit) return;
// Cria o agendamento fornecendo as 'Foreign Keys' vitais (shopId, serviceId, etc...)
const appt = createAppointment({
const appt = await createAppointment({
shopId: shop.id,
serviceId,
barberId,
customerId: user.id, // Auth User UID
date: `${date} ${slot}`
date: `${date} ${slot}`,
reminderMinutes
});
if (appt) {
@@ -131,7 +140,7 @@ export default function Booking() {
<Text style={styles.title}>Agendar em {shop.name}</Text>
<Card style={styles.card}>
<Text style={styles.sectionTitle}>1. Escolha o serviço</Text>
<Text style={styles.sectionTitle}>1. Seleção de 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) => (
@@ -146,7 +155,7 @@ export default function Booking() {
))}
</View>
<Text style={styles.sectionTitle}>2. Escolha o barbeiro</Text>
<Text style={styles.sectionTitle}>2. Barbeiro</Text>
{/* Renderiza os profissionais, normalmente provindo dum JOIN na base de dados (tabela barbeiros + barbearia) */}
<View style={styles.barberContainer}>
{shop.barbers.map((b) => (
@@ -162,7 +171,7 @@ export default function Booking() {
))}
</View>
<Text style={styles.sectionTitle}>3. Escolha a data</Text>
<Text style={styles.sectionTitle}>3. Data de Preferência</Text>
{/* Componente simples de input que deverá mapear para a inserção final do timestamp Postgres */}
<Input
value={date}
@@ -170,8 +179,7 @@ export default function Booking() {
placeholder="YYYY-MM-DD"
/>
<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) */}
<Text style={styles.sectionTitle}>4. Horário</Text>
<View style={styles.slotsContainer}>
{availableSlots.length > 0 ? (
availableSlots.map((h) => (
@@ -184,14 +192,29 @@ export default function Booking() {
</TouchableOpacity>
))
) : (
<Text style={styles.noSlots}>Escolha primeiro o barbeiro e a data</Text>
<Text style={styles.noSlots}>Selecione primeiro o mestre e a data</Text>
)}
</View>
<Text style={styles.sectionTitle}>5. Receber Lembrete</Text>
<View style={styles.reminderContainer}>
{reminderOptions.map((opt) => (
<TouchableOpacity
key={opt.value}
style={[styles.reminderButton, reminderMinutes === opt.value && styles.reminderButtonActive]}
onPress={() => setReminderMinutes(opt.value)}
>
<Text style={[styles.reminderText, reminderMinutes === opt.value && styles.reminderTextActive]}>
{opt.label} antes
</Text>
</TouchableOpacity>
))}
</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>
<Text style={styles.summaryTitle}>Resumo do Agendamento</Text>
<Text style={styles.summaryText}>Serviço: {selectedService.name}</Text>
<Text style={styles.summaryText}>Barbeiro: {selectedBarber?.name}</Text>
<Text style={styles.summaryText}>Data: {date} às {slot}</Text>
@@ -201,7 +224,7 @@ export default function Booking() {
{/* 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'}
{user ? 'Confirmar Reserva' : 'Entrar para Reservar'}
</Button>
</Card>
</ScrollView>
@@ -319,6 +342,31 @@ const styles = StyleSheet.create({
color: '#94a3b8',
fontStyle: 'italic',
},
reminderContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 16,
},
reminderButton: {
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
borderWidth: 1,
borderColor: '#e2e8f0',
},
reminderButtonActive: {
borderColor: '#6366f1',
backgroundColor: '#e0e7ff',
},
reminderText: {
fontSize: 12,
color: '#64748b',
},
reminderTextActive: {
color: '#6366f1',
fontWeight: 'bold',
},
summary: {
backgroundColor: '#f1f5f9',
padding: 16,

View File

@@ -22,7 +22,7 @@ export default function Cart() {
return (
<View style={styles.container}>
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Carrinho vazio</Text>
<Text style={styles.emptyText}>Sua Seleção está Deserta</Text>
</Card>
</View>
);
@@ -49,7 +49,7 @@ export default function Cart() {
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');
Alert.alert('Sessão Necessária', 'Inicie sessão para confirmar o seu pedido');
navigation.navigate('Login' as never);
return;
}
@@ -63,7 +63,7 @@ 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>
<Text style={styles.title}>Minha Seleção</Text>
{/* Renderiza dinamicamente 1 Card de Checkout por Loja agrupada no objeto `grouped` */}
{Object.entries(grouped).map(([shopId, items]) => {
@@ -122,14 +122,14 @@ export default function Cart() {
{/* Renderização condicional no React para encaminhar fluxo para login se anónimo */}
{user ? (
<Button onPress={() => handleCheckout(shopId)} style={styles.checkoutButton}>
Finalizar pedido
Finalizar Aquisição
</Button>
) : (
<Button
onPress={() => navigation.navigate('Login' as never)}
style={styles.checkoutButton}
>
Entrar para finalizar
Entrar para Adquirir
</Button>
)}
</Card>

View File

@@ -136,12 +136,12 @@ export default function Dashboard() {
};
const tabs = [
{ id: 'overview', label: 'Visão Geral' },
{ id: 'appointments', label: 'Agendamentos' },
{ id: 'orders', label: 'Pedidos' },
{ id: 'overview', label: 'Estatísticas' },
{ id: 'appointments', label: 'Reservas' },
{ id: 'orders', label: 'Pedidos Boutique' },
{ id: 'services', label: 'Serviços' },
{ id: 'products', label: 'Produtos' },
{ id: 'barbers', label: 'Barbeiros' },
{ id: 'barbers', label: 'Equipa' },
];
return (
@@ -172,7 +172,7 @@ export default function Dashboard() {
<View>
<View style={styles.statsGrid}>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Faturamento</Text>
<Text style={styles.statLabel}>Receita Total</Text>
<Text style={styles.statValue}>{currency(totalRevenue)}</Text>
</Card>
<Card style={styles.statCard}>

View File

@@ -23,7 +23,7 @@ export default function Explore() {
return (
// Componente raiz do ecã de exploração
<View style={styles.container}>
<Text style={styles.title}>Explorar barbearias</Text>
<Text style={styles.title}>Barbearias</Text>
{/* FlatList é o componente nativo otimizado para renderizar grandes arrays de dados provenientes da BD */}
<FlatList
@@ -54,7 +54,7 @@ export default function Explore() {
variant="outline"
style={styles.button}
>
Ver detalhes
Ver Barbearia
</Button>
{/* Redirecionamento direto com foreign key injetada para a view de Agendamentos */}
@@ -62,7 +62,7 @@ export default function Explore() {
onPress={() => navigation.navigate('Booking', { shopId: shop.id })}
style={styles.button}
>
Agendar
Reservar
</Button>
</View>
</Card>

View File

@@ -19,51 +19,48 @@ export default function Landing() {
<View style={styles.hero}>
<Text style={styles.heroTitle}>Smart Agenda</Text>
<Text style={styles.heroSubtitle}>
Agendamentos, produtos e gestão em um único lugar.
Agendamento e Gestão de Barbearias.
</Text>
<Text style={styles.heroDesc}>
Experiência mobile-first para clientes e painel completo para barbearias.
A sua solução completa para o dia-a-dia da barbearia.
</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}
size="lg"
>
Explorar barbearias
Ver Barbearias
</Button>
{/* Botão focado no registo de novos utilizadores */}
<Button
onPress={() => navigation.navigate('Register' as never)}
variant="outline"
style={styles.button}
size="lg"
>
Criar conta
Criar Conta
</Button>
</View>
</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.featureTitle}>Reservas Rápidas</Text>
<Text style={styles.featureDesc}>
Escolha serviço, barbeiro, data e horário com validação de slots.
Selecione o seu barbeiro e o horário ideal em poucos segundos.
</Text>
</Card>
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Carrinho</Text>
<Text style={styles.featureTitle}>Produtos</Text>
<Text style={styles.featureDesc}>
Produtos e serviços agrupados por barbearia, pagamento rápido.
Produtos de cuidado masculino selecionados para si.
</Text>
</Card>
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Painel</Text>
<Text style={styles.featureTitle}>Gestão de Barbearia</Text>
<Text style={styles.featureDesc}>
Faturamento, agendamentos, pedidos, barbearia no controle.
Controlo total sobre o faturamento e operação da sua barbearia.
</Text>
</Card>
</View>

View File

@@ -13,8 +13,8 @@ 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',
const statusColor: Record<string, 'indigo' | 'green' | 'slate' | 'red'> = {
pendente: 'indigo',
confirmado: 'green',
concluido: 'green',
cancelado: 'red',
@@ -47,7 +47,7 @@ export default function Profile() {
<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}>
<Badge color="indigo" style={styles.roleBadge}>
{user.role === 'cliente' ? 'Cliente' : 'Barbearia'}
</Badge>
@@ -57,7 +57,7 @@ export default function Profile() {
</Button>
</Card>
<Text style={styles.sectionTitle}>Agendamentos</Text>
<Text style={styles.sectionTitle}>As Minhas Reservas</Text>
{/* Renderiza a lista se existirem marcações no percurso deste utilizador */}
{myAppointments.length > 0 ? (
myAppointments.map((a) => {
@@ -83,7 +83,7 @@ export default function Profile() {
</Card>
)}
<Text style={styles.sectionTitle}>Pedidos</Text>
<Text style={styles.sectionTitle}>As Minhas Compras</Text>
{/* Renderiza o histórico de compras de retalho/produtos usando idêntica lógica */}
{myOrders.length > 0 ? (
myOrders.map((o) => {

View File

@@ -51,7 +51,7 @@ export default function ShopDetails() {
onPress={() => navigation.navigate('Booking', { shopId: shop.id })}
style={styles.bookButton}
>
Agendar
Reservar Experiência
</Button>
</View>
@@ -61,13 +61,13 @@ export default function ShopDetails() {
style={[styles.tab, tab === 'servicos' && styles.tabActive]}
onPress={() => setTab('servicos')}
>
<Text style={[styles.tabText, tab === 'servicos' && styles.tabTextActive]}>Serviços</Text>
<Text style={[styles.tabText, tab === 'servicos' && styles.tabTextActive]}>Menu de Serviços</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, tab === 'produtos' && styles.tabActive]}
onPress={() => setTab('produtos')}
>
<Text style={[styles.tabText, tab === 'produtos' && styles.tabTextActive]}>Produtos</Text>
<Text style={[styles.tabText, tab === 'produtos' && styles.tabTextActive]}>Boutique</Text>
</TouchableOpacity>
</View>
@@ -89,7 +89,7 @@ export default function ShopDetails() {
size="sm"
style={styles.addButton}
>
Adicionar ao carrinho
Adicionar à Seleção
</Button>
</Card>
))}
@@ -106,7 +106,7 @@ export default function ShopDetails() {
<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>}
{product.stock <= 3 && <Badge color="indigo" style={styles.stockBadge}>Últimas unidades</Badge>}
{/* Botão em React é afetado logicamente face à impossibilidade material de encomenda */}
<Button
@@ -115,7 +115,7 @@ export default function ShopDetails() {
style={styles.addButton}
disabled={product.stock <= 0}
>
{product.stock > 0 ? 'Adicionar ao carrinho' : 'Sem stock'}
{product.stock > 0 ? 'Adicionar à Seleção' : 'Indisponível'}
</Button>
</Card>
))}
@@ -165,7 +165,7 @@ const styles = StyleSheet.create({
},
tabActive: {
borderColor: '#6366f1',
backgroundColor: '#fef3c7',
backgroundColor: '#e0e7ff',
},
tabText: {
fontSize: 14,