471 lines
12 KiB
TypeScript
471 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Pressable,
|
|
Image,
|
|
Alert,
|
|
FlatList,
|
|
} from 'react-native';
|
|
import { useRoute, useNavigation } from '@react-navigation/native';
|
|
import { StackNavigationProp } from '@react-navigation/stack';
|
|
import { RootStackParamList } from '../navigation/types';
|
|
import { supabase, isSupabaseConfigured } from '../services/supabase';
|
|
import { COLORS, SIZES, FONTS, SHADOWS } from '../constants/theme';
|
|
import { Barber, Review, Booking } from '../types';
|
|
import ReviewCard from '../components/ReviewCard';
|
|
import BookingCard from '../components/BookingCard';
|
|
|
|
const BarberDetailScreen: React.FC = () => {
|
|
const route = useRoute();
|
|
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
|
|
const { barberId } = route.params as { barberId: string };
|
|
|
|
const [barber, setBarber] = useState<Barber | null>(null);
|
|
const [reviews, setReviews] = useState<Review[]>([]);
|
|
const [bookings, setBookings] = useState<Booking[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activeTab, setActiveTab] = useState<'reviews' | 'availability'>('reviews');
|
|
|
|
useEffect(() => {
|
|
loadBarberDetails();
|
|
}, [barberId]);
|
|
|
|
const loadBarberDetails = async () => {
|
|
try {
|
|
if (!isSupabaseConfigured) {
|
|
// Demo mode - use mock data
|
|
const { mockBarbers, mockReviews } = await import('../data/mockData');
|
|
const barber = mockBarbers.find(b => b.id === barberId);
|
|
if (barber) {
|
|
setBarber(barber);
|
|
// Load barber-specific reviews
|
|
const barberReviews = mockReviews.filter(r => r.barber_id === barberId);
|
|
setReviews(barberReviews);
|
|
// Mock bookings for demo
|
|
setBookings([]);
|
|
}
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const [barberRes, reviewsRes, bookingsRes] = await Promise.all([
|
|
supabase
|
|
.from('barbers')
|
|
.select(`
|
|
*,
|
|
user:users(name, photo, email, phone)
|
|
`)
|
|
.eq('id', barberId)
|
|
.single(),
|
|
supabase
|
|
.from('reviews')
|
|
.select(`
|
|
*,
|
|
user:users(name, photo)
|
|
`)
|
|
.eq('barber_id', barberId)
|
|
.order('created_at', { ascending: false }),
|
|
supabase
|
|
.from('bookings')
|
|
.select(`
|
|
*,
|
|
service:services(name, price),
|
|
customer:users(name)
|
|
`)
|
|
.eq('barber_id', barberId)
|
|
.eq('status', 'completed')
|
|
.order('booking_date', { ascending: false })
|
|
.limit(10)
|
|
]);
|
|
|
|
if (barberRes.data) setBarber(barberRes.data);
|
|
if (reviewsRes.data) setReviews(reviewsRes.data);
|
|
if (bookingsRes.data) setBookings(bookingsRes.data);
|
|
} catch (error) {
|
|
console.error('Erro ao carregar detalhes do barbeiro:', error);
|
|
Alert.alert('Erro', 'Falha ao carregar detalhes do barbeiro');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleBookNow = () => {
|
|
Alert.alert(
|
|
'👨💼 Marcar com Barbeiro',
|
|
`Vou marcar com: ${barber?.user?.name}`,
|
|
[
|
|
{
|
|
text: 'Cancelar',
|
|
style: 'cancel',
|
|
},
|
|
{
|
|
text: 'Confirmar',
|
|
onPress: () => {
|
|
Alert.alert(
|
|
'✅ Barbeiro Selecionado',
|
|
`${barber?.user?.name} foi selecionado para a sua marcação.\n\nContinue para escolher serviço, data e horário.`,
|
|
[
|
|
{
|
|
text: 'OK',
|
|
onPress: () => navigation.navigate('Booking'),
|
|
},
|
|
]
|
|
);
|
|
},
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
const renderStars = (rating: number) => {
|
|
const stars = [];
|
|
const fullStars = Math.floor(rating);
|
|
|
|
for (let i = 0; i < fullStars; i++) {
|
|
stars.push(
|
|
<Text key={i} style={styles.star}>★</Text>
|
|
);
|
|
}
|
|
|
|
const emptyStars = 5 - fullStars;
|
|
for (let i = 0; i < emptyStars; i++) {
|
|
stars.push(
|
|
<Text key={`empty-${i}`} style={[styles.star, styles.emptyStar]}>☆</Text>
|
|
);
|
|
}
|
|
|
|
return stars;
|
|
};
|
|
|
|
const renderAvailability = () => {
|
|
if (!barber?.availability) return null;
|
|
|
|
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
|
|
return (
|
|
<View style={styles.availabilityContainer}>
|
|
{days.map((day) => {
|
|
const timeSlots = barber.availability[day.toLowerCase()] || [];
|
|
return (
|
|
<View key={day} style={styles.dayRow}>
|
|
<Text style={styles.dayText}>{day}</Text>
|
|
<View style={styles.timeSlots}>
|
|
{timeSlots.length > 0 ? (
|
|
timeSlots.map((time, index) => (
|
|
<Text key={index} style={styles.timeSlot}>
|
|
{time}
|
|
</Text>
|
|
))
|
|
) : (
|
|
<Text style={styles.closedText}>Fechado</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<View style={styles.loadingContainer}>
|
|
<Text style={styles.loadingText}>A carregar...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (!barber) {
|
|
return (
|
|
<View style={styles.errorContainer}>
|
|
<Text style={styles.errorText}>Barbeiro não encontrado</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScrollView style={styles.container}>
|
|
{/* Barber Header */}
|
|
<View style={styles.header}>
|
|
<Image
|
|
source={{ uri: barber.user?.photo || 'https://via.placeholder.com/150' }}
|
|
style={styles.avatar}
|
|
/>
|
|
<View style={styles.barberInfo}>
|
|
<Text style={styles.name}>{barber.user?.name}</Text>
|
|
<Text style={styles.specialty}>{barber.specialty}</Text>
|
|
<View style={styles.ratingContainer}>
|
|
<View style={styles.stars}>
|
|
{renderStars(barber.rating)}
|
|
</View>
|
|
<Text style={styles.rating}>{barber.rating.toFixed(1)}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Bio */}
|
|
<View style={styles.bioSection}>
|
|
<Text style={styles.sectionTitle}>Sobre</Text>
|
|
<Text style={styles.bio}>{barber.bio}</Text>
|
|
</View>
|
|
|
|
{/* Contact Info */}
|
|
<View style={styles.contactSection}>
|
|
<Text style={styles.sectionTitle}>Contacto</Text>
|
|
<Text style={styles.contactText}>📧 {barber.user?.email}</Text>
|
|
<Text style={styles.contactText}>📱 {barber.user?.phone}</Text>
|
|
</View>
|
|
|
|
{/* Book Button */}
|
|
<Pressable
|
|
style={({ pressed }) => [styles.bookButton, pressed && styles.bookButtonPressed]}
|
|
onPress={handleBookNow}
|
|
>
|
|
<Text style={styles.bookButtonText}>Marcar Horário</Text>
|
|
</Pressable>
|
|
|
|
{/* Tabs */}
|
|
<View style={styles.tabsContainer}>
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.tab,
|
|
activeTab === 'reviews' && styles.activeTab,
|
|
pressed && styles.tabPressed
|
|
]}
|
|
onPress={() => setActiveTab('reviews')}
|
|
>
|
|
<Text style={[styles.tabText, activeTab === 'reviews' && styles.activeTabText]}>
|
|
Avaliações ({reviews.length})
|
|
</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.tab,
|
|
activeTab === 'availability' && styles.activeTab,
|
|
pressed && styles.tabPressed
|
|
]}
|
|
onPress={() => setActiveTab('availability')}
|
|
>
|
|
<Text style={[styles.tabText, activeTab === 'availability' && styles.activeTabText]}>
|
|
Disponibilidade
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
|
|
{/* Tab Content */}
|
|
<View style={styles.tabContent}>
|
|
{activeTab === 'reviews' ? (
|
|
<>
|
|
{reviews.length === 0 ? (
|
|
<Text style={styles.noReviewsText}>Sem avaliações</Text>
|
|
) : (
|
|
reviews.map((review) => (
|
|
<ReviewCard key={review.id} review={review} />
|
|
))
|
|
)}
|
|
</>
|
|
) : (
|
|
renderAvailability()
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: COLORS.background,
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
loadingText: {
|
|
...FONTS.body,
|
|
color: COLORS.textSecondary,
|
|
},
|
|
errorContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
errorText: {
|
|
...FONTS.body,
|
|
color: COLORS.error,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
padding: SIZES.padding,
|
|
backgroundColor: COLORS.surface,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: COLORS.border,
|
|
},
|
|
avatar: {
|
|
width: 100,
|
|
height: 100,
|
|
borderRadius: 50,
|
|
marginRight: SIZES.margin,
|
|
},
|
|
barberInfo: {
|
|
flex: 1,
|
|
},
|
|
name: {
|
|
...FONTS.h1,
|
|
color: COLORS.text,
|
|
marginBottom: SIZES.base / 2,
|
|
},
|
|
specialty: {
|
|
...FONTS.h3,
|
|
color: COLORS.primary,
|
|
marginBottom: SIZES.base,
|
|
},
|
|
ratingContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
stars: {
|
|
flexDirection: 'row',
|
|
marginRight: SIZES.base,
|
|
},
|
|
star: {
|
|
color: COLORS.primary,
|
|
fontSize: 18,
|
|
},
|
|
emptyStar: {
|
|
color: COLORS.border,
|
|
},
|
|
rating: {
|
|
...FONTS.h3,
|
|
color: COLORS.text,
|
|
},
|
|
bioSection: {
|
|
padding: SIZES.padding,
|
|
},
|
|
sectionTitle: {
|
|
...FONTS.h2,
|
|
color: COLORS.text,
|
|
marginBottom: SIZES.margin,
|
|
},
|
|
bio: {
|
|
...FONTS.body,
|
|
color: COLORS.textSecondary,
|
|
lineHeight: 24,
|
|
},
|
|
contactSection: {
|
|
padding: SIZES.padding,
|
|
backgroundColor: COLORS.surface,
|
|
margin: SIZES.margin,
|
|
borderRadius: SIZES.radius,
|
|
...SHADOWS.light,
|
|
},
|
|
contactText: {
|
|
...FONTS.body,
|
|
color: COLORS.text,
|
|
marginBottom: SIZES.base,
|
|
},
|
|
bookButton: {
|
|
backgroundColor: COLORS.primary,
|
|
borderRadius: SIZES.radius,
|
|
padding: SIZES.padding,
|
|
alignItems: 'center',
|
|
margin: SIZES.margin,
|
|
...SHADOWS.medium,
|
|
cursor: 'pointer',
|
|
},
|
|
bookButtonPressed: {
|
|
opacity: 0.9,
|
|
},
|
|
bookButtonText: {
|
|
...FONTS.h3,
|
|
color: COLORS.background,
|
|
fontWeight: 'bold',
|
|
},
|
|
tabsContainer: {
|
|
flexDirection: 'row',
|
|
marginHorizontal: SIZES.margin,
|
|
marginBottom: SIZES.margin,
|
|
backgroundColor: COLORS.surface,
|
|
borderRadius: SIZES.radius,
|
|
padding: SIZES.base,
|
|
...SHADOWS.light,
|
|
},
|
|
tab: {
|
|
flex: 1,
|
|
paddingVertical: SIZES.base,
|
|
alignItems: 'center',
|
|
borderRadius: SIZES.base,
|
|
cursor: 'pointer',
|
|
},
|
|
tabPressed: {
|
|
opacity: 0.7,
|
|
},
|
|
activeTab: {
|
|
backgroundColor: COLORS.primary,
|
|
},
|
|
tabText: {
|
|
...FONTS.body,
|
|
color: COLORS.textSecondary,
|
|
},
|
|
activeTabText: {
|
|
color: COLORS.background,
|
|
fontWeight: 'bold',
|
|
},
|
|
tabContent: {
|
|
marginHorizontal: SIZES.margin,
|
|
marginBottom: SIZES.margin * 2,
|
|
},
|
|
noReviewsText: {
|
|
...FONTS.body,
|
|
color: COLORS.textSecondary,
|
|
textAlign: 'center',
|
|
paddingVertical: SIZES.padding,
|
|
},
|
|
availabilityContainer: {
|
|
backgroundColor: COLORS.surface,
|
|
borderRadius: SIZES.radius,
|
|
padding: SIZES.padding,
|
|
...SHADOWS.light,
|
|
},
|
|
dayRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingVertical: SIZES.base,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: COLORS.border,
|
|
},
|
|
dayText: {
|
|
...FONTS.body,
|
|
color: COLORS.text,
|
|
flex: 1,
|
|
},
|
|
timeSlots: {
|
|
flex: 2,
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
justifyContent: 'flex-end',
|
|
},
|
|
timeSlot: {
|
|
...FONTS.caption,
|
|
color: COLORS.primary,
|
|
backgroundColor: COLORS.background,
|
|
paddingHorizontal: SIZES.base,
|
|
paddingVertical: SIZES.base / 2,
|
|
borderRadius: SIZES.base / 2,
|
|
marginLeft: SIZES.base / 2,
|
|
marginBottom: SIZES.base / 2,
|
|
},
|
|
closedText: {
|
|
...FONTS.caption,
|
|
color: COLORS.textSecondary,
|
|
fontStyle: 'italic',
|
|
},
|
|
});
|
|
|
|
export default BarberDetailScreen;
|