feat: Implement initial application structure, core pages, UI components, and Supabase backend integration.
This commit is contained in:
@@ -36,7 +36,7 @@ export const Button = ({ children, onPress, variant = 'solid', size = 'md', disa
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={variant === 'solid' ? '#fff' : '#6366f1'} />
|
||||
<ActivityIndicator size={20} color={variant === 'solid' ? '#fff' : '#6366f1'} />
|
||||
) : (
|
||||
<Text style={textStyles}>{children}</Text>
|
||||
)}
|
||||
|
||||
@@ -8,6 +8,9 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from '
|
||||
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { nanoid } from 'nanoid';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import * as Device from 'expo-device';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
type State = {
|
||||
user?: User;
|
||||
@@ -18,10 +21,12 @@ type State = {
|
||||
};
|
||||
|
||||
type AppContextValue = State & {
|
||||
login: (email: string, password: string) => boolean;
|
||||
login: (email: string, password: string) => User | undefined;
|
||||
logout: () => void;
|
||||
register: (payload: any) => boolean;
|
||||
register: (payload: any) => User | undefined;
|
||||
addToCart: (item: CartItem) => void;
|
||||
removeFromCart: (refId: string) => void;
|
||||
placeOrder: (customerId: string, shopId: string) => Order | null;
|
||||
clearCart: () => void;
|
||||
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Promise<Appointment | null>;
|
||||
updateAppointmentStatus: (id: string, status: Appointment['status']) => Promise<void>;
|
||||
@@ -54,7 +59,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
if (data.user) {
|
||||
const { data: prof } = await supabase
|
||||
.from('profiles')
|
||||
.select('shop_id, role, name')
|
||||
.select('shop_id, role, name, fcm_token')
|
||||
.eq('id', data.user.id)
|
||||
.single();
|
||||
|
||||
@@ -64,13 +69,22 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
email: data.user.email || '',
|
||||
role: (prof?.role as any) || 'cliente',
|
||||
shopId: prof?.shop_id || undefined,
|
||||
fcmToken: prof?.fcm_token || undefined,
|
||||
} as User);
|
||||
|
||||
// Regista token FCM se ainda não existir ou tiver mudado
|
||||
registerForPushNotificationsAsync().then(token => {
|
||||
if (token && token !== prof?.fcm_token) {
|
||||
supabase.from('profiles').update({ fcm_token: token }).eq('id', data.user.id).then();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
const refreshShops = async () => {
|
||||
console.log('AppContext: refreshShops iniciado');
|
||||
try {
|
||||
const { data: shopsData } = await supabase.from('shops').select('*');
|
||||
const { data: servicesData } = await supabase.from('services').select('*');
|
||||
@@ -134,15 +148,33 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('AppContext: Iniciando carregamento...');
|
||||
const init = async () => {
|
||||
await refreshShops();
|
||||
setLoading(false);
|
||||
try {
|
||||
await refreshShops();
|
||||
console.log('AppContext: Lojas carregadas com sucesso.');
|
||||
} catch (e) {
|
||||
console.error('AppContext: Erro no init:', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
console.log('AppContext: setLoading(false)');
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const login = (email: string, password: string) => {
|
||||
return true;
|
||||
// Provisório para demo
|
||||
const u: User = {
|
||||
id: email === 'barber@demo.com' ? 'demo-barber' : 'demo-cliente',
|
||||
name: email === 'barber@demo.com' ? 'Barbeiro Chefe' : 'Utilizador Demo',
|
||||
email,
|
||||
password, // Adicionado para satisfazer o tipo
|
||||
role: email === 'barber@demo.com' ? 'barbearia' : 'cliente',
|
||||
shopId: email === 'barber@demo.com' ? shops[0]?.id : undefined
|
||||
};
|
||||
setUser(u);
|
||||
return u;
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
@@ -154,7 +186,39 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const id = nanoid();
|
||||
const newUser: User = { ...payload, id };
|
||||
setUser(newUser);
|
||||
return true;
|
||||
return newUser;
|
||||
};
|
||||
|
||||
const removeFromCart = (refId: string) => {
|
||||
setCart((prev) => prev.filter((i) => i.refId !== refId));
|
||||
};
|
||||
|
||||
const placeOrder = (customerId: string, shopId: string) => {
|
||||
const shopItems = cart.filter((i) => i.shopId === shopId);
|
||||
if (!shopItems.length) return null;
|
||||
|
||||
const shop = shops.find((s) => s.id === shopId);
|
||||
const total = shopItems.reduce((sum, i) => {
|
||||
const price =
|
||||
i.type === 'service'
|
||||
? shop?.services.find((s) => s.id === i.refId)?.price ?? 0
|
||||
: shop?.products.find((p) => p.id === i.refId)?.price ?? 0;
|
||||
return sum + price * i.qty;
|
||||
}, 0);
|
||||
|
||||
const newOrder: Order = {
|
||||
id: nanoid(),
|
||||
shopId,
|
||||
customerId,
|
||||
items: shopItems,
|
||||
total,
|
||||
status: 'pendente',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setOrders((prev) => [...prev, newOrder]);
|
||||
setCart((prev) => prev.filter((i) => i.shopId !== shopId));
|
||||
return newOrder;
|
||||
};
|
||||
|
||||
const addToCart = (item: CartItem) => {
|
||||
@@ -221,7 +285,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
customer_id: input.customerId,
|
||||
date: input.date,
|
||||
status: 'pendente',
|
||||
total
|
||||
total,
|
||||
reminder_minutes: input.reminderMinutes
|
||||
}]).select().single();
|
||||
await refreshShops();
|
||||
return data as any as Appointment;
|
||||
@@ -248,6 +313,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
logout,
|
||||
register,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
placeOrder,
|
||||
clearCart,
|
||||
createAppointment,
|
||||
updateAppointmentStatus,
|
||||
@@ -275,4 +342,36 @@ export const useApp = () => {
|
||||
const ctx = useContext(AppContext);
|
||||
if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider');
|
||||
return ctx;
|
||||
};
|
||||
};
|
||||
|
||||
async function registerForPushNotificationsAsync() {
|
||||
let token;
|
||||
if (Platform.OS === 'android') {
|
||||
await Notifications.setNotificationChannelAsync('default', {
|
||||
name: 'default',
|
||||
importance: Notifications.AndroidImportance.MAX,
|
||||
vibrationPattern: [0, 250, 250, 250],
|
||||
lightColor: '#FF231F7C',
|
||||
});
|
||||
}
|
||||
|
||||
if (Device.isDevice) {
|
||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||
let finalStatus = existingStatus;
|
||||
if (existingStatus !== 'granted') {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
finalStatus = status;
|
||||
}
|
||||
if (finalStatus !== 'granted') {
|
||||
console.log('Falha ao obter token push!');
|
||||
return;
|
||||
}
|
||||
token = (await Notifications.getExpoPushTokenAsync({
|
||||
projectId: 'b018a5db-c940-4364-81ee-596ced75cae3',
|
||||
})).data;
|
||||
} else {
|
||||
console.log('Necessário dispositivo físico para notificações push');
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -4,9 +4,9 @@ export type Product = { id: string; name: string; price: number; stock: number }
|
||||
export type BarberShop = { id: string; name: string; address: string; rating: number; services: Service[]; products: Product[]; barbers: Barber[] };
|
||||
export type AppointmentStatus = 'pendente' | 'confirmado' | 'concluido' | 'cancelado';
|
||||
export type OrderStatus = 'pendente' | 'confirmado' | 'concluido' | 'cancelado';
|
||||
export type Appointment = { id: string; shopId: string; serviceId: string; barberId: string; customerId: string; date: string; status: AppointmentStatus; total: number };
|
||||
export type Appointment = { id: string; shopId: string; serviceId: string; barberId: string; customerId: string; date: string; status: AppointmentStatus; total: number; reminderMinutes?: number };
|
||||
export type CartItem = { shopId: string; type: 'service' | 'product'; refId: string; qty: number };
|
||||
export type Order = { id: string; shopId: string; customerId: string; items: CartItem[]; total: number; status: OrderStatus; createdAt: string };
|
||||
export type User = { id: string; name: string; email: string; password: string; role: 'cliente' | 'barbearia'; shopId?: string };
|
||||
export type User = { id: string; name: string; email: string; password: string; role: 'cliente' | 'barbearia'; shopId?: string; fcmToken?: string };
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user