feat: Implement initial application structure, core pages, UI components, and Supabase backend integration.
This commit is contained in:
@@ -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%',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user