first commit

This commit is contained in:
2026-01-07 10:35:00 +00:00
parent 13745ac89e
commit 3c7190bca4
53 changed files with 5538 additions and 531 deletions

148
src/pages/AuthLogin.tsx Normal file
View File

@@ -0,0 +1,148 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, Alert } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Card } from '../components/ui/Card';
export default function AuthLogin() {
const navigation = useNavigation();
const { login } = useApp();
const [email, setEmail] = useState('cliente@demo.com');
const [password, setPassword] = useState('123');
const [error, setError] = useState('');
const handleSubmit = () => {
setError('');
const ok = login(email, password);
if (!ok) {
setError('Credenciais inválidas');
Alert.alert('Erro', 'Credenciais inválidas');
} else {
navigation.navigate('Explore' as never);
}
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Card style={styles.card}>
<Text style={styles.title}>Bem-vindo de volta</Text>
<Text style={styles.subtitle}>Entre na sua conta para continuar</Text>
<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
label="Email"
value={email}
onChangeText={(text) => {
setEmail(text);
setError('');
}}
keyboardType="email-address"
autoCapitalize="none"
placeholder="seu@email.com"
/>
<Input
label="Senha"
value={password}
onChangeText={(text) => {
setPassword(text);
setError('');
}}
secureTextEntry
placeholder="••••••••"
error={error}
/>
<Button onPress={handleSubmit} style={styles.submitButton} size="lg">
Entrar
</Button>
<View style={styles.footer}>
<Text style={styles.footerText}>Não tem conta? </Text>
<Text
style={styles.footerLink}
onPress={() => navigation.navigate('Register' as never)}
>
Criar conta
</Text>
</View>
</Card>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
justifyContent: 'center',
minHeight: '100%',
},
card: {
padding: 24,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
fontSize: 14,
color: '#64748b',
marginBottom: 24,
textAlign: 'center',
},
demoBox: {
backgroundColor: '#fef3c7',
borderWidth: 1,
borderColor: '#fbbf24',
borderRadius: 8,
padding: 12,
marginBottom: 20,
},
demoTitle: {
fontSize: 12,
fontWeight: '600',
color: '#92400e',
marginBottom: 4,
},
demoText: {
fontSize: 11,
color: '#92400e',
},
submitButton: {
width: '100%',
marginTop: 8,
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 24,
paddingTop: 24,
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
},
footerText: {
fontSize: 14,
color: '#64748b',
},
footerLink: {
fontSize: 14,
color: '#f59e0b',
fontWeight: '600',
},
});

182
src/pages/AuthRegister.tsx Normal file
View File

@@ -0,0 +1,182 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, Alert, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Card } from '../components/ui/Card';
export default function AuthRegister() {
const navigation = useNavigation();
const { register } = useApp();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [role, setRole] = useState<'cliente' | 'barbearia'>('cliente');
const [shopName, setShopName] = useState('');
const [error, setError] = useState('');
const handleSubmit = () => {
setError('');
const ok = register({ name, email, password, role, shopName });
if (!ok) {
setError('Email já registado');
Alert.alert('Erro', 'Email já registado');
} else {
navigation.navigate('Explore' as never);
}
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Card style={styles.card}>
<Text style={styles.title}>Criar conta</Text>
<Text style={styles.subtitle}>Escolha o tipo de acesso</Text>
<View style={styles.roleContainer}>
<TouchableOpacity
style={[styles.roleButton, role === 'cliente' && styles.roleButtonActive]}
onPress={() => setRole('cliente')}
>
<Text style={[styles.roleText, role === 'cliente' && styles.roleTextActive]}>
Cliente
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.roleButton, role === 'barbearia' && styles.roleButtonActive]}
onPress={() => setRole('barbearia')}
>
<Text style={[styles.roleText, role === 'barbearia' && styles.roleTextActive]}>
Barbearia
</Text>
</TouchableOpacity>
</View>
<Input
label="Nome completo"
value={name}
onChangeText={setName}
placeholder="João Silva"
/>
<Input
label="Email"
value={email}
onChangeText={(text) => {
setEmail(text);
setError('');
}}
keyboardType="email-address"
autoCapitalize="none"
placeholder="seu@email.com"
error={error}
/>
<Input
label="Senha"
value={password}
onChangeText={setPassword}
secureTextEntry
placeholder="••••••••"
/>
{role === 'barbearia' && (
<Input
label="Nome da barbearia"
value={shopName}
onChangeText={setShopName}
placeholder="Barbearia XPTO"
/>
)}
<Button onPress={handleSubmit} style={styles.submitButton} size="lg">
Criar conta
</Button>
<View style={styles.footer}>
<Text style={styles.footerText}> tem conta? </Text>
<Text
style={styles.footerLink}
onPress={() => navigation.navigate('Login' as never)}
>
Entrar
</Text>
</View>
</Card>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
card: {
padding: 24,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
fontSize: 14,
color: '#64748b',
marginBottom: 24,
textAlign: 'center',
},
roleContainer: {
flexDirection: 'row',
gap: 12,
marginBottom: 20,
},
roleButton: {
flex: 1,
padding: 16,
borderRadius: 12,
borderWidth: 2,
borderColor: '#e2e8f0',
alignItems: 'center',
},
roleButtonActive: {
borderColor: '#f59e0b',
backgroundColor: '#fef3c7',
},
roleText: {
fontSize: 14,
fontWeight: '600',
color: '#64748b',
},
roleTextActive: {
color: '#f59e0b',
},
submitButton: {
width: '100%',
marginTop: 8,
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 24,
paddingTop: 24,
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
},
footerText: {
fontSize: 14,
color: '#64748b',
},
footerLink: {
fontSize: 14,
color: '#f59e0b',
fontWeight: '600',
},
});

298
src/pages/Booking.tsx Normal file
View File

@@ -0,0 +1,298 @@
import React, { useState, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
export default function Booking() {
const route = useRoute();
const navigation = useNavigation();
const { shopId } = route.params as { shopId: string };
const { shops, createAppointment, user, appointments } = useApp();
const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]);
const [serviceId, setService] = useState('');
const [barberId, setBarber] = useState('');
const [date, setDate] = useState('');
const [slot, setSlot] = useState('');
if (!shop) {
return (
<View style={styles.container}>
<Text>Barbearia não encontrada</Text>
</View>
);
}
const selectedService = shop.services.find((s) => s.id === serviceId);
const selectedBarber = shop.barbers.find((b) => b.id === barberId);
const generateDefaultSlots = (): string[] => {
const slots: string[] = [];
for (let hour = 9; hour <= 18; hour++) {
slots.push(`${hour.toString().padStart(2, '0')}:00`);
}
return slots;
};
const availableSlots = useMemo(() => {
if (!selectedBarber || !date) return [];
const specificSchedule = selectedBarber.schedule.find((s) => s.day === date);
let slots = specificSchedule && specificSchedule.slots.length > 0
? [...specificSchedule.slots]
: generateDefaultSlots();
const bookedSlots = appointments
.filter((apt) =>
apt.barberId === barberId &&
apt.status !== 'cancelado' &&
apt.date.startsWith(date)
)
.map((apt) => {
const parts = apt.date.split(' ');
return parts.length > 1 ? parts[1] : '';
})
.filter(Boolean);
return slots.filter((slot) => !bookedSlots.includes(slot));
}, [selectedBarber, date, barberId, appointments]);
const canSubmit = serviceId && barberId && date && slot;
const submit = () => {
if (!user) {
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}` });
if (appt) {
Alert.alert('Sucesso', 'Agendamento criado com sucesso!');
navigation.navigate('Profile' as never);
} else {
Alert.alert('Erro', 'Horário indisponível');
}
};
return (
<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>
<View style={styles.grid}>
{shop.services.map((s) => (
<TouchableOpacity
key={s.id}
style={[styles.serviceButton, serviceId === s.id && styles.serviceButtonActive]}
onPress={() => setService(s.id)}
>
<Text style={[styles.serviceText, serviceId === s.id && styles.serviceTextActive]}>{s.name}</Text>
<Text style={styles.servicePrice}>{currency(s.price)}</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.sectionTitle}>2. Escolha o barbeiro</Text>
<View style={styles.barberContainer}>
{shop.barbers.map((b) => (
<TouchableOpacity
key={b.id}
style={[styles.barberButton, barberId === b.id && styles.barberButtonActive]}
onPress={() => setBarber(b.id)}
>
<Text style={[styles.barberText, barberId === b.id && styles.barberTextActive]}>
{b.name}
</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.sectionTitle}>3. Escolha a data</Text>
<Input
value={date}
onChangeText={setDate}
placeholder="YYYY-MM-DD"
/>
<Text style={styles.sectionTitle}>4. Escolha o horário</Text>
<View style={styles.slotsContainer}>
{availableSlots.length > 0 ? (
availableSlots.map((h) => (
<TouchableOpacity
key={h}
style={[styles.slotButton, slot === h && styles.slotButtonActive]}
onPress={() => setSlot(h)}
>
<Text style={[styles.slotText, slot === h && styles.slotTextActive]}>{h}</Text>
</TouchableOpacity>
))
) : (
<Text style={styles.noSlots}>Escolha primeiro o barbeiro e a data</Text>
)}
</View>
{canSubmit && selectedService && (
<View style={styles.summary}>
<Text style={styles.summaryTitle}>Resumo</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>
<Text style={styles.summaryTotal}>Total: {currency(selectedService.price)}</Text>
</View>
)}
<Button onPress={submit} disabled={!canSubmit} style={styles.submitButton} size="lg">
{user ? 'Confirmar agendamento' : 'Entrar para agendar'}
</Button>
</Card>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 16,
},
card: {
padding: 20,
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
marginTop: 16,
marginBottom: 12,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 16,
},
serviceButton: {
flex: 1,
minWidth: '45%',
padding: 16,
borderRadius: 8,
borderWidth: 2,
borderColor: '#e2e8f0',
marginBottom: 8,
},
serviceButtonActive: {
borderColor: '#f59e0b',
backgroundColor: '#fef3c7',
},
serviceText: {
fontSize: 14,
fontWeight: '600',
color: '#0f172a',
marginBottom: 4,
},
serviceTextActive: {
color: '#f59e0b',
},
servicePrice: {
fontSize: 12,
color: '#64748b',
},
barberContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 16,
},
barberButton: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
borderWidth: 2,
borderColor: '#e2e8f0',
},
barberButtonActive: {
borderColor: '#f59e0b',
backgroundColor: '#f59e0b',
},
barberText: {
fontSize: 14,
fontWeight: '600',
color: '#64748b',
},
barberTextActive: {
color: '#fff',
},
slotsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 16,
},
slotButton: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 8,
borderWidth: 2,
borderColor: '#e2e8f0',
},
slotButtonActive: {
borderColor: '#f59e0b',
backgroundColor: '#f59e0b',
},
slotText: {
fontSize: 14,
fontWeight: '600',
color: '#64748b',
},
slotTextActive: {
color: '#fff',
},
noSlots: {
fontSize: 14,
color: '#94a3b8',
fontStyle: 'italic',
},
summary: {
backgroundColor: '#f1f5f9',
padding: 16,
borderRadius: 8,
marginBottom: 16,
},
summaryTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 8,
},
summaryText: {
fontSize: 14,
color: '#64748b',
marginBottom: 4,
},
summaryTotal: {
fontSize: 18,
fontWeight: 'bold',
color: '#f59e0b',
marginTop: 8,
},
submitButton: {
width: '100%',
marginTop: 16,
},
});

167
src/pages/Cart.tsx Normal file
View File

@@ -0,0 +1,167 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, Alert } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
export default function Cart() {
const navigation = useNavigation();
const { cart, shops, removeFromCart, placeOrder, user } = useApp();
if (!cart.length) {
return (
<View style={styles.container}>
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Carrinho vazio</Text>
</Card>
</View>
);
}
const grouped = cart.reduce<Record<string, typeof cart>>((acc, item) => {
acc[item.shopId] = acc[item.shopId] || [];
acc[item.shopId].push(item);
return acc;
}, {});
const handleCheckout = (shopId: string) => {
if (!user) {
Alert.alert('Login necessário', 'Faça login para finalizar o pedido');
navigation.navigate('Login' as never);
return;
}
const order = placeOrder(user.id, shopId);
if (order) {
Alert.alert('Sucesso', 'Pedido criado com sucesso!');
}
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Carrinho</Text>
{Object.entries(grouped).map(([shopId, items]) => {
const shop = shops.find((s) => s.id === shopId);
const total = items.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);
return (
<Card key={shopId} style={styles.shopCard}>
<View style={styles.shopHeader}>
<View>
<Text style={styles.shopName}>{shop?.name ?? 'Barbearia'}</Text>
<Text style={styles.shopAddress}>{shop?.address}</Text>
</View>
<Text style={styles.total}>{currency(total)}</Text>
</View>
{items.map((i) => {
const ref =
i.type === 'service'
? shop?.services.find((s) => s.id === i.refId)
: shop?.products.find((p) => p.id === i.refId);
return (
<View key={i.refId} style={styles.item}>
<Text style={styles.itemText}>
{i.type === 'service' ? 'Serviço: ' : 'Produto: '}
{ref?.name ?? 'Item'} x{i.qty}
</Text>
<Button
onPress={() => removeFromCart(i.refId)}
variant="ghost"
size="sm"
>
Remover
</Button>
</View>
);
})}
{user ? (
<Button onPress={() => handleCheckout(shopId)} style={styles.checkoutButton}>
Finalizar pedido
</Button>
) : (
<Button
onPress={() => navigation.navigate('Login' as never)}
style={styles.checkoutButton}
>
Entrar para finalizar
</Button>
)}
</Card>
);
})}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 16,
},
emptyCard: {
padding: 32,
alignItems: 'center',
},
emptyText: {
fontSize: 16,
color: '#64748b',
},
shopCard: {
marginBottom: 16,
},
shopHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
},
shopName: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
},
shopAddress: {
fontSize: 12,
color: '#64748b',
},
total: {
fontSize: 18,
fontWeight: 'bold',
color: '#f59e0b',
},
item: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
itemText: {
fontSize: 14,
color: '#64748b',
flex: 1,
},
checkoutButton: {
width: '100%',
marginTop: 12,
},
});

541
src/pages/Dashboard.tsx Normal file
View File

@@ -0,0 +1,541 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
export default function Dashboard() {
const navigation = useNavigation();
const {
user,
shops,
appointments,
orders,
updateAppointmentStatus,
updateOrderStatus,
addService,
addProduct,
addBarber,
updateProduct,
deleteProduct,
deleteService,
deleteBarber,
logout,
} = useApp();
const shop = shops.find((s) => s.id === user?.shopId);
const [activeTab, setActiveTab] = useState<'overview' | 'appointments' | 'orders' | 'services' | 'products' | 'barbers'>('overview');
const [svcName, setSvcName] = useState('');
const [svcPrice, setSvcPrice] = useState('50');
const [svcDuration, setSvcDuration] = useState('30');
const [prodName, setProdName] = useState('');
const [prodPrice, setProdPrice] = useState('30');
const [prodStock, setProdStock] = useState('10');
const [barberName, setBarberName] = useState('');
const [barberSpecs, setBarberSpecs] = useState('');
if (!user || user.role !== 'barbearia') {
return (
<View style={styles.container}>
<Text>Área exclusiva para barbearias</Text>
</View>
);
}
if (!shop) {
return (
<View style={styles.container}>
<Text>Barbearia não encontrada</Text>
</View>
);
}
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'));
const totalRevenue = shopOrders.reduce((s, o) => s + o.total, 0);
const lowStock = shop.products.filter((p) => p.stock <= 3);
const addNewService = () => {
if (!svcName.trim()) return;
addService(shop.id, { name: svcName, price: Number(svcPrice) || 0, duration: Number(svcDuration) || 30, barberIds: [] });
setSvcName('');
setSvcPrice('50');
setSvcDuration('30');
Alert.alert('Sucesso', 'Serviço adicionado');
};
const addNewProduct = () => {
if (!prodName.trim()) return;
addProduct(shop.id, { name: prodName, price: Number(prodPrice) || 0, stock: Number(prodStock) || 0 });
setProdName('');
setProdPrice('30');
setProdStock('10');
Alert.alert('Sucesso', 'Produto adicionado');
};
const addNewBarber = () => {
if (!barberName.trim()) return;
addBarber(shop.id, {
name: barberName,
specialties: barberSpecs.split(',').map((s) => s.trim()).filter(Boolean),
schedule: [],
});
setBarberName('');
setBarberSpecs('');
Alert.alert('Sucesso', 'Barbeiro adicionado');
};
const updateProductStock = (productId: string, delta: number) => {
const product = shop.products.find((p) => p.id === productId);
if (!product) return;
const next = { ...product, stock: Math.max(0, product.stock + delta) };
updateProduct(shop.id, next);
};
const tabs = [
{ id: 'overview', label: 'Visão Geral' },
{ id: 'appointments', label: 'Agendamentos' },
{ id: 'orders', label: 'Pedidos' },
{ id: 'services', label: 'Serviços' },
{ id: 'products', label: 'Produtos' },
{ id: 'barbers', label: 'Barbeiros' },
];
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>{shop.name}</Text>
<Button onPress={logout} variant="ghost" size="sm">
Sair
</Button>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsContainer}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.id}
style={[styles.tab, activeTab === tab.id && styles.tabActive]}
onPress={() => setActiveTab(tab.id as any)}
>
<Text style={[styles.tabText, activeTab === tab.id && styles.tabTextActive]}>
{tab.label}
</Text>
</TouchableOpacity>
))}
</ScrollView>
<ScrollView style={styles.content} contentContainerStyle={styles.contentInner}>
{activeTab === 'overview' && (
<View>
<View style={styles.statsGrid}>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Faturamento</Text>
<Text style={styles.statValue}>{currency(totalRevenue)}</Text>
</Card>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Pendentes</Text>
<Text style={styles.statValue}>{activeAppointments.length}</Text>
</Card>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Concluídos</Text>
<Text style={styles.statValue}>{completedAppointments.length}</Text>
</Card>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Stock baixo</Text>
<Text style={[styles.statValue, lowStock.length > 0 && styles.statValueWarning]}>
{lowStock.length}
</Text>
</Card>
</View>
</View>
)}
{activeTab === 'appointments' && (
<View>
{activeAppointments.length > 0 ? (
activeAppointments.map((a) => {
const svc = shop.services.find((s) => s.id === a.serviceId);
const barber = shop.barbers.find((b) => b.id === a.barberId);
return (
<Card key={a.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{svc?.name ?? 'Serviço'}</Text>
<Text style={styles.itemDesc}>{barber?.name} · {a.date}</Text>
</View>
<Badge color={a.status === 'pendente' ? 'amber' : a.status === 'confirmado' ? 'green' : 'red'}>
{a.status}
</Badge>
</View>
<View style={styles.statusSelector}>
<Text style={styles.selectorLabel}>Alterar status:</Text>
<View style={styles.statusButtons}>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<Button
key={s}
onPress={() => updateAppointmentStatus(a.id, s as any)}
variant={a.status === s ? 'solid' : 'outline'}
size="sm"
style={styles.statusButton}
>
{s}
</Button>
))}
</View>
</View>
</Card>
);
})
) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Nenhum agendamento ativo</Text>
</Card>
)}
</View>
)}
{activeTab === 'orders' && (
<View>
{productOrders.length > 0 ? (
productOrders.map((o) => (
<Card key={o.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{currency(o.total)}</Text>
<Text style={styles.itemDesc}>{new Date(o.createdAt).toLocaleString('pt-BR')}</Text>
</View>
<Badge color={o.status === 'pendente' ? 'amber' : o.status === 'confirmado' ? 'green' : 'red'}>
{o.status}
</Badge>
</View>
<View style={styles.statusButtons}>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<Button
key={s}
onPress={() => updateOrderStatus(o.id, s as any)}
variant={o.status === s ? 'solid' : 'outline'}
size="sm"
style={styles.statusButton}
>
{s}
</Button>
))}
</View>
</Card>
))
) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Nenhum pedido de produtos</Text>
</Card>
)}
</View>
)}
{activeTab === 'services' && (
<View>
{shop.services.map((s) => (
<Card key={s.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{s.name}</Text>
<Text style={styles.itemDesc}>Duração: {s.duration} min</Text>
</View>
<Text style={styles.itemPrice}>{currency(s.price)}</Text>
</View>
<Button
onPress={() => {
Alert.alert('Confirmar', 'Deseja remover este serviço?', [
{ text: 'Cancelar', style: 'cancel' },
{ text: 'Remover', style: 'destructive', onPress: () => deleteService(shop.id, s.id) },
]);
}}
variant="outline"
size="sm"
style={styles.deleteButton}
>
Remover
</Button>
</Card>
))}
<Card style={styles.formCard}>
<Text style={styles.formTitle}>Adicionar serviço</Text>
<Input label="Nome" value={svcName} onChangeText={setSvcName} placeholder="Ex: Corte Fade" />
<Input label="Preço" value={svcPrice} onChangeText={setSvcPrice} keyboardType="numeric" placeholder="50" />
<Input label="Duração (min)" value={svcDuration} onChangeText={setSvcDuration} keyboardType="numeric" placeholder="30" />
<Button onPress={addNewService} style={styles.addButton}>
Adicionar
</Button>
</Card>
</View>
)}
{activeTab === 'products' && (
<View>
{lowStock.length > 0 && (
<Card style={styles.alertCard}>
<Text style={styles.alertText}>
Atenção: {lowStock.length} {lowStock.length === 1 ? 'produto com stock baixo' : 'produtos com stock baixo'}
</Text>
</Card>
)}
{shop.products.map((p) => (
<Card key={p.id} style={[styles.itemCard, p.stock <= 3 && styles.itemCardWarning]}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{p.name}</Text>
<Text style={styles.itemDesc}>Stock: {p.stock} unidades</Text>
</View>
<Text style={styles.itemPrice}>{currency(p.price)}</Text>
</View>
<View style={styles.stockControls}>
<Button onPress={() => updateProductStock(p.id, -1)} variant="outline" size="sm" style={styles.stockButton}>
-1
</Button>
<Button onPress={() => updateProductStock(p.id, 1)} variant="outline" size="sm" style={styles.stockButton}>
+1
</Button>
<Button
onPress={() => {
Alert.alert('Confirmar', 'Deseja remover este produto?', [
{ text: 'Cancelar', style: 'cancel' },
{ text: 'Remover', style: 'destructive', onPress: () => deleteProduct(shop.id, p.id) },
]);
}}
variant="outline"
size="sm"
style={styles.stockButton}
>
Remover
</Button>
</View>
</Card>
))}
<Card style={styles.formCard}>
<Text style={styles.formTitle}>Adicionar produto</Text>
<Input label="Nome" value={prodName} onChangeText={setProdName} placeholder="Ex: Pomada" />
<Input label="Preço" value={prodPrice} onChangeText={setProdPrice} keyboardType="numeric" placeholder="30" />
<Input label="Stock inicial" value={prodStock} onChangeText={setProdStock} keyboardType="numeric" placeholder="10" />
<Button onPress={addNewProduct} style={styles.addButton}>
Adicionar
</Button>
</Card>
</View>
)}
{activeTab === 'barbers' && (
<View>
{shop.barbers.map((b) => (
<Card key={b.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{b.name}</Text>
<Text style={styles.itemDesc}>
Especialidades: {b.specialties.length > 0 ? b.specialties.join(', ') : 'Nenhuma'}
</Text>
</View>
</View>
<Button
onPress={() => {
Alert.alert('Confirmar', 'Deseja remover este barbeiro?', [
{ text: 'Cancelar', style: 'cancel' },
{ text: 'Remover', style: 'destructive', onPress: () => deleteBarber(shop.id, b.id) },
]);
}}
variant="outline"
size="sm"
style={styles.deleteButton}
>
Remover
</Button>
</Card>
))}
<Card style={styles.formCard}>
<Text style={styles.formTitle}>Adicionar barbeiro</Text>
<Input label="Nome" value={barberName} onChangeText={setBarberName} placeholder="Ex: João Silva" />
<Input label="Especialidades" value={barberSpecs} onChangeText={setBarberSpecs} placeholder="Fade, Navalha, Barba" />
<Button onPress={addNewBarber} style={styles.addButton}>
Adicionar
</Button>
</Card>
</View>
)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#0f172a',
},
tabsContainer: {
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
tab: {
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 2,
borderBottomColor: 'transparent',
},
tabActive: {
borderBottomColor: '#f59e0b',
},
tabText: {
fontSize: 14,
fontWeight: '600',
color: '#64748b',
},
tabTextActive: {
color: '#f59e0b',
},
content: {
flex: 1,
},
contentInner: {
padding: 16,
},
statsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
marginBottom: 16,
},
statCard: {
flex: 1,
minWidth: '45%',
padding: 16,
},
statLabel: {
fontSize: 12,
color: '#64748b',
marginBottom: 4,
},
statValue: {
fontSize: 20,
fontWeight: 'bold',
color: '#0f172a',
},
statValueWarning: {
color: '#f59e0b',
},
itemCard: {
marginBottom: 12,
padding: 16,
},
itemCardWarning: {
borderColor: '#fbbf24',
backgroundColor: '#fef3c7',
},
itemHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 12,
},
itemName: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
flex: 1,
},
itemDesc: {
fontSize: 14,
color: '#64748b',
marginTop: 4,
},
itemPrice: {
fontSize: 18,
fontWeight: 'bold',
color: '#f59e0b',
},
statusSelector: {
marginTop: 8,
},
selectorLabel: {
fontSize: 12,
color: '#64748b',
marginBottom: 8,
},
statusButtons: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
statusButton: {
flex: 1,
minWidth: '22%',
},
emptyCard: {
padding: 32,
alignItems: 'center',
},
emptyText: {
fontSize: 14,
color: '#64748b',
},
alertCard: {
backgroundColor: '#fef3c7',
borderColor: '#fbbf24',
marginBottom: 16,
padding: 16,
},
alertText: {
fontSize: 14,
fontWeight: '600',
color: '#92400e',
},
formCard: {
marginTop: 16,
padding: 16,
},
formTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 16,
},
addButton: {
width: '100%',
marginTop: 8,
},
deleteButton: {
width: '100%',
marginTop: 8,
},
stockControls: {
flexDirection: 'row',
gap: 8,
marginTop: 8,
},
stockButton: {
flex: 1,
},
});

109
src/pages/Explore.tsx Normal file
View File

@@ -0,0 +1,109 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, FlatList } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
export default function Explore() {
const navigation = useNavigation();
const { shops } = useApp();
return (
<View style={styles.container}>
<Text style={styles.title}>Explorar barbearias</Text>
<FlatList
data={shops}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
renderItem={({ item: shop }) => (
<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}>
<Text style={styles.shopInfoText}>{shop.services.length} serviços</Text>
<Text style={styles.shopInfoText}></Text>
<Text style={styles.shopInfoText}>{shop.barbers.length} barbeiros</Text>
</View>
<View style={styles.buttons}>
<Button
onPress={() => navigation.navigate('ShopDetails' as never, { shopId: shop.id } as never)}
variant="outline"
style={styles.button}
>
Ver detalhes
</Button>
<Button
onPress={() => navigation.navigate('Booking' as never, { shopId: shop.id } as never)}
style={styles.button}
>
Agendar
</Button>
</View>
</Card>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 16,
},
list: {
gap: 16,
},
shopCard: {
marginBottom: 12,
},
shopHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
shopName: {
fontSize: 18,
fontWeight: 'bold',
color: '#0f172a',
flex: 1,
},
shopAddress: {
fontSize: 14,
color: '#64748b',
marginBottom: 8,
},
shopInfo: {
flexDirection: 'row',
gap: 8,
marginBottom: 12,
},
shopInfoText: {
fontSize: 12,
color: '#94a3b8',
},
buttons: {
flexDirection: 'row',
gap: 8,
marginTop: 8,
},
button: {
flex: 1,
},
});

119
src/pages/Landing.tsx Normal file
View File

@@ -0,0 +1,119 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { Button } from '../components/ui/Button';
import { Card } from '../components/ui/Card';
export default function Landing() {
const navigation = useNavigation();
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<View style={styles.hero}>
<Text style={styles.heroTitle}>Smart Agenda</Text>
<Text style={styles.heroSubtitle}>
Agendamentos, produtos e gestão em um único lugar.
</Text>
<Text style={styles.heroDesc}>
Experiência mobile-first para clientes e painel completo para barbearias.
</Text>
<View style={styles.buttons}>
<Button
onPress={() => navigation.navigate('Explore' as never)}
style={styles.button}
size="lg"
>
Explorar barbearias
</Button>
<Button
onPress={() => navigation.navigate('Register' as never)}
variant="outline"
style={styles.button}
size="lg"
>
Criar conta
</Button>
</View>
</View>
<View style={styles.features}>
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Agendamentos</Text>
<Text style={styles.featureDesc}>
Escolha serviço, barbeiro, data e horário com validação de slots.
</Text>
</Card>
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Carrinho</Text>
<Text style={styles.featureDesc}>
Produtos e serviços agrupados por barbearia, pagamento rápido.
</Text>
</Card>
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Painel</Text>
<Text style={styles.featureDesc}>
Faturamento, agendamentos, pedidos, barbearia no controle.
</Text>
</Card>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
hero: {
backgroundColor: '#f59e0b',
borderRadius: 16,
padding: 24,
marginBottom: 24,
},
heroTitle: {
fontSize: 28,
fontWeight: 'bold',
color: '#fff',
marginBottom: 8,
},
heroSubtitle: {
fontSize: 20,
fontWeight: '600',
color: '#fff',
marginBottom: 8,
},
heroDesc: {
fontSize: 16,
color: '#fef3c7',
marginBottom: 20,
},
buttons: {
gap: 12,
},
button: {
width: '100%',
},
features: {
gap: 16,
},
featureCard: {
marginBottom: 12,
},
featureTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 8,
},
featureDesc: {
fontSize: 14,
color: '#64748b',
lineHeight: 20,
},
});

165
src/pages/Profile.tsx Normal file
View File

@@ -0,0 +1,165 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Badge } from '../components/ui/Badge';
import { Button } from '../components/ui/Button';
import { currency } from '../lib/format';
const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
pendente: 'amber',
confirmado: 'green',
concluido: 'green',
cancelado: 'red',
};
export default function Profile() {
const navigation = useNavigation();
const { user, appointments, orders, shops, logout } = useApp();
if (!user) {
return (
<View style={styles.container}>
<Text>Faça login para ver o perfil</Text>
</View>
);
}
const myAppointments = appointments.filter((a) => a.customerId === user.id);
const myOrders = orders.filter((o) => o.customerId === user.id);
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Card style={styles.profileCard}>
<Text style={styles.profileName}>Olá, {user.name}</Text>
<Text style={styles.profileEmail}>{user.email}</Text>
<Badge color="amber" style={styles.roleBadge}>
{user.role === 'cliente' ? 'Cliente' : 'Barbearia'}
</Badge>
<Button onPress={logout} variant="outline" style={styles.logoutButton}>
Sair
</Button>
</Card>
<Text style={styles.sectionTitle}>Agendamentos</Text>
{myAppointments.length > 0 ? (
myAppointments.map((a) => {
const shop = shops.find((s) => s.id === a.shopId);
return (
<Card key={a.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<Text style={styles.itemName}>{shop?.name}</Text>
<Badge color={statusColor[a.status]}>{a.status}</Badge>
</View>
<Text style={styles.itemDate}>{a.date}</Text>
<Text style={styles.itemTotal}>{currency(a.total)}</Text>
</Card>
);
})
) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Nenhum agendamento ainda</Text>
</Card>
)}
<Text style={styles.sectionTitle}>Pedidos</Text>
{myOrders.length > 0 ? (
myOrders.map((o) => {
const shop = shops.find((s) => s.id === o.shopId);
return (
<Card key={o.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<Text style={styles.itemName}>{shop?.name}</Text>
<Badge color={statusColor[o.status]}>{o.status}</Badge>
</View>
<Text style={styles.itemDate}>
{new Date(o.createdAt).toLocaleString('pt-BR')}
</Text>
<Text style={styles.itemTotal}>{currency(o.total)}</Text>
</Card>
);
})
) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Nenhum pedido ainda</Text>
</Card>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
profileCard: {
marginBottom: 24,
padding: 20,
},
profileName: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 4,
},
profileEmail: {
fontSize: 14,
color: '#64748b',
marginBottom: 12,
},
roleBadge: {
alignSelf: 'flex-start',
marginBottom: 16,
},
logoutButton: {
width: '100%',
},
sectionTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 12,
marginTop: 8,
},
itemCard: {
marginBottom: 12,
padding: 16,
},
itemHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
itemName: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
flex: 1,
},
itemDate: {
fontSize: 14,
color: '#64748b',
marginBottom: 4,
},
itemTotal: {
fontSize: 16,
fontWeight: 'bold',
color: '#f59e0b',
},
emptyCard: {
padding: 32,
alignItems: 'center',
},
emptyText: {
fontSize: 14,
color: '#64748b',
},
});

184
src/pages/ShopDetails.tsx Normal file
View File

@@ -0,0 +1,184 @@
import React, { useState, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
export default function ShopDetails() {
const route = useRoute();
const navigation = useNavigation();
const { shopId } = route.params as { shopId: string };
const { shops, addToCart } = useApp();
const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]);
const [tab, setTab] = useState<'servicos' | 'produtos'>('servicos');
if (!shop) {
return (
<View style={styles.container}>
<Text>Barbearia não encontrada</Text>
</View>
);
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<View style={styles.header}>
<Text style={styles.title}>{shop.name}</Text>
<Text style={styles.address}>{shop.address}</Text>
<Button
onPress={() => navigation.navigate('Booking' as never, { shopId: shop.id } as never)}
style={styles.bookButton}
>
Agendar
</Button>
</View>
<View style={styles.tabs}>
<TouchableOpacity
style={[styles.tab, tab === 'servicos' && styles.tabActive]}
onPress={() => setTab('servicos')}
>
<Text style={[styles.tabText, tab === 'servicos' && styles.tabTextActive]}>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>
</TouchableOpacity>
</View>
{tab === 'servicos' ? (
<View style={styles.list}>
{shop.services.map((service) => (
<Card key={service.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<Text style={styles.itemName}>{service.name}</Text>
<Text style={styles.itemPrice}>{currency(service.price)}</Text>
</View>
<Text style={styles.itemDesc}>Duração: {service.duration} min</Text>
<Button
onPress={() => addToCart({ shopId: shop.id, type: 'service', refId: service.id, qty: 1 })}
size="sm"
style={styles.addButton}
>
Adicionar ao carrinho
</Button>
</Card>
))}
</View>
) : (
<View style={styles.list}>
{shop.products.map((product) => (
<Card key={product.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<Text style={styles.itemName}>{product.name}</Text>
<Text style={styles.itemPrice}>{currency(product.price)}</Text>
</View>
<Text style={styles.itemDesc}>Stock: {product.stock} unidades</Text>
{product.stock <= 3 && <Badge color="amber" style={styles.stockBadge}>Stock baixo</Badge>}
<Button
onPress={() => addToCart({ shopId: shop.id, type: 'product', refId: product.id, qty: 1 })}
size="sm"
style={styles.addButton}
disabled={product.stock <= 0}
>
{product.stock > 0 ? 'Adicionar ao carrinho' : 'Sem stock'}
</Button>
</Card>
))}
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
header: {
marginBottom: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 8,
},
address: {
fontSize: 14,
color: '#64748b',
marginBottom: 16,
},
bookButton: {
width: '100%',
},
tabs: {
flexDirection: 'row',
gap: 8,
marginBottom: 16,
},
tab: {
flex: 1,
padding: 12,
borderRadius: 8,
borderWidth: 1,
borderColor: '#e2e8f0',
alignItems: 'center',
},
tabActive: {
borderColor: '#f59e0b',
backgroundColor: '#fef3c7',
},
tabText: {
fontSize: 14,
fontWeight: '600',
color: '#64748b',
},
tabTextActive: {
color: '#f59e0b',
},
list: {
gap: 12,
},
itemCard: {
marginBottom: 12,
},
itemHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
itemName: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
flex: 1,
},
itemPrice: {
fontSize: 16,
fontWeight: 'bold',
color: '#f59e0b',
},
itemDesc: {
fontSize: 14,
color: '#64748b',
marginBottom: 12,
},
stockBadge: {
marginBottom: 8,
},
addButton: {
width: '100%',
},
});