first commit
This commit is contained in:
148
src/pages/AuthLogin.tsx
Normal file
148
src/pages/AuthLogin.tsx
Normal 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
182
src/pages/AuthRegister.tsx
Normal 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}>Já 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
298
src/pages/Booking.tsx
Normal 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
167
src/pages/Cart.tsx
Normal 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
541
src/pages/Dashboard.tsx
Normal 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
109
src/pages/Explore.tsx
Normal 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
119
src/pages/Landing.tsx
Normal 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
165
src/pages/Profile.tsx
Normal 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
184
src/pages/ShopDetails.tsx
Normal 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%',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user