feat: redesign client profile layout and update booking redirect
This commit is contained in:
@@ -64,7 +64,7 @@ export default function Booking() {
|
||||
});
|
||||
if (appt) {
|
||||
Alert.alert('Sucesso', 'O seu agendamento foi confirmado.');
|
||||
navigation.navigate('ProfileTab' as never);
|
||||
navigation.navigate('ExploreTab' as never);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -199,168 +199,181 @@ export default function Profile() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Dynamic Tab Bar */}
|
||||
<View style={styles.tabBarContainer}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.tabBar}>
|
||||
{(isBarber ? [
|
||||
['agenda_pessoal', 'Agenda Global'],
|
||||
['reviews', 'Avaliações'],
|
||||
['estatisticas', 'Resumo'],
|
||||
] : [
|
||||
['agenda', 'Minha Agenda'],
|
||||
['favoritos', 'Favoritos'],
|
||||
['pedidos', 'Compras'],
|
||||
]).map(([id, label]) => (
|
||||
<TouchableOpacity
|
||||
key={id}
|
||||
onPress={() => setActiveTab(id)}
|
||||
style={[styles.tabItem, activeTab === id && styles.tabActive]}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[styles.tabText, activeTab === id && styles.tabTextActive]}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
{/* Dynamic Tab Bar (Only for Barber) */}
|
||||
{isBarber && (
|
||||
<View style={styles.tabBarContainer}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.tabBar}>
|
||||
{[
|
||||
['agenda_pessoal', 'Agenda Global'],
|
||||
['reviews', 'Avaliações'],
|
||||
['estatisticas', 'Resumo'],
|
||||
].map(([id, label]) => (
|
||||
<TouchableOpacity
|
||||
key={id}
|
||||
onPress={() => setActiveTab(id)}
|
||||
style={[styles.tabItem, activeTab === id && styles.tabActive]}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[styles.tabText, activeTab === id && styles.tabTextActive]}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Tab Content Area */}
|
||||
<View style={styles.tabContent}>
|
||||
|
||||
{/* CLIENTE TABS */}
|
||||
{activeTab === 'agenda' && (
|
||||
<View style={styles.listContainer}>
|
||||
{myAppointments.length > 0 ? (
|
||||
myAppointments.map((appt) => {
|
||||
const shop = shops.find((s) => s.id === appt.shopId);
|
||||
const canReview = appt.status === 'concluido' && !reviewedAppointments.has(appt.id);
|
||||
const dateParts = appt.date.split(' ');
|
||||
const day = dateParts[0].split('-')[2];
|
||||
const month = new Date(dateParts[0]).toLocaleString('pt-PT', { month: 'short' }).toUpperCase();
|
||||
|
||||
return (
|
||||
<Card key={appt.id} style={styles.agendaCard}>
|
||||
<View style={styles.agendaTop}>
|
||||
{/* tear-off style calendar block */}
|
||||
<View style={styles.calendarBlock}>
|
||||
<View style={styles.calendarHeader} />
|
||||
<Text style={styles.dateDay}>{day}</Text>
|
||||
<Text style={styles.dateMonth}>{month}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.agendaMain}>
|
||||
<Text style={styles.agendaShop} numberOfLines={1}>{shop?.name || 'Barbearia'}</Text>
|
||||
<View style={styles.agendaMetaRow}>
|
||||
<Ionicons name="time-outline" size={13} color={colors.textMuted} />
|
||||
<Text style={styles.agendaTime}>{dateParts[1]} • {currency(appt.total)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.statusTag, { backgroundColor: statusColor[appt.status] + '14' }]}>
|
||||
<Ionicons
|
||||
name={statusIconName[appt.status]}
|
||||
size={11}
|
||||
color={statusColor[appt.status]}
|
||||
/>
|
||||
<Text style={[styles.statusText, { color: statusColor[appt.status] }]}>{statusLabel[appt.status]}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* CLIENTE VERTICAL LAYOUT */}
|
||||
{!isBarber && (
|
||||
<View style={{ gap: 32 }}>
|
||||
|
||||
{/* Minha Agenda Section */}
|
||||
<View>
|
||||
<View style={styles.clientSectionHeader}>
|
||||
<Ionicons name="calendar" size={20} color={colors.primary} />
|
||||
<Text style={styles.clientSectionTitle}>As Minhas Marcações</Text>
|
||||
</View>
|
||||
<View style={styles.listContainer}>
|
||||
{myAppointments.length > 0 ? (
|
||||
myAppointments.map((appt) => {
|
||||
const shop = shops.find((s) => s.id === appt.shopId);
|
||||
const canReview = appt.status === 'concluido' && !reviewedAppointments.has(appt.id);
|
||||
const dateParts = appt.date.split(' ');
|
||||
const day = dateParts[0].split('-')[2];
|
||||
const month = new Date(dateParts[0]).toLocaleString('pt-PT', { month: 'short' }).toUpperCase();
|
||||
|
||||
{canReview && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
style={styles.reviewBtn}
|
||||
onPress={() => setReviewTarget({ appointmentId: appt.id, shopId: appt.shopId, shopName: shop?.name || 'Barbearia' })}
|
||||
>
|
||||
Avaliar Serviço ⭐
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<View style={styles.emptyBlock}>
|
||||
<Ionicons name="calendar-outline" size={36} color={colors.textSubtle} />
|
||||
<Text style={styles.emptyTxt}>Sem marcações agendadas.</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activeTab === 'favoritos' && (
|
||||
<View style={styles.listContainer}>
|
||||
{favoriteShops.length > 0 ? (
|
||||
favoriteShops.map((s) => (
|
||||
<TouchableOpacity
|
||||
key={s.id}
|
||||
onPress={() => navigation.navigate('ShopDetails', { shopId: s.id })}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<Card style={styles.shopCard}>
|
||||
<View style={styles.shopIcon}>
|
||||
<Text style={styles.shopIconText}>{s.name.charAt(0).toUpperCase()}</Text>
|
||||
</View>
|
||||
<View style={styles.shopCardBody}>
|
||||
<Text style={styles.shopName} numberOfLines={1}>{s.name}</Text>
|
||||
<View style={styles.shopLocationRow}>
|
||||
<Ionicons name="location-outline" size={12} color={colors.textMuted} />
|
||||
<Text style={styles.shopAddr} numberOfLines={1}>
|
||||
{s.address && s.address !== 'Endereço a definir' ? s.address : 'Morada por definir'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.shopRatingBadge}>
|
||||
<Ionicons name="star" size={12} color={colors.star} />
|
||||
<Text style={styles.shopRatingText}>{(s.rating || 0).toFixed(1)}</Text>
|
||||
</View>
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<View style={styles.emptyBlock}>
|
||||
<Ionicons name="heart-outline" size={36} color={colors.textSubtle} />
|
||||
<Text style={styles.emptyTxt}>Ainda não tens favoritos.</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activeTab === 'pedidos' && (
|
||||
<View style={styles.listContainer}>
|
||||
{myOrders.length > 0 ? (
|
||||
myOrders.map((order) => {
|
||||
const shop = shops.find((s) => s.id === order.shopId);
|
||||
return (
|
||||
<Card key={order.id} style={styles.orderCard}>
|
||||
<View style={styles.orderTop}>
|
||||
<View style={{ flex: 1, gap: 2 }}>
|
||||
<Text style={styles.orderShop} numberOfLines={1}>{shop?.name || 'Barbearia'}</Text>
|
||||
<View style={styles.orderMetaRow}>
|
||||
<Ionicons name="calendar-outline" size={12} color={colors.textSubtle} />
|
||||
<Text style={styles.orderMeta}>
|
||||
{new Date(order.createdAt).toLocaleDateString('pt-PT')} • {order.items.length} item(s)
|
||||
</Text>
|
||||
return (
|
||||
<Card key={appt.id} style={styles.agendaCardPremium}>
|
||||
<View style={styles.agendaTop}>
|
||||
<View style={styles.calendarBlockPremium}>
|
||||
<Text style={styles.dateMonthPremium}>{month}</Text>
|
||||
<Text style={styles.dateDayPremium}>{day}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.agendaMain}>
|
||||
<Text style={styles.agendaShopPremium} numberOfLines={1}>{shop?.name || 'Barbearia'}</Text>
|
||||
<View style={styles.agendaMetaRow}>
|
||||
<Ionicons name="time-outline" size={14} color={colors.textMuted} />
|
||||
<Text style={styles.agendaTimePremium}>{dateParts[1]} • {currency(appt.total)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.statusTagPremium, { backgroundColor: statusColor[appt.status] + '1A' }]}>
|
||||
<Text style={[styles.statusTextPremium, { color: statusColor[appt.status] }]}>{statusLabel[appt.status]}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.orderTotal}>{currency(order.total)}</Text>
|
||||
|
||||
{canReview && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
style={styles.reviewBtn}
|
||||
onPress={() => setReviewTarget({ appointmentId: appt.id, shopId: appt.shopId, shopName: shop?.name || 'Barbearia' })}
|
||||
>
|
||||
Avaliar Serviço ⭐
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<View style={styles.emptyBlockPremium}>
|
||||
<View style={styles.emptyIconCircle}>
|
||||
<Ionicons name="calendar-outline" size={28} color={colors.primary} />
|
||||
</View>
|
||||
<View style={[styles.statusTag, { backgroundColor: statusColor[order.status] + '14', alignSelf: 'flex-start' }]}>
|
||||
<Ionicons
|
||||
name={statusIconName[order.status]}
|
||||
size={11}
|
||||
color={statusColor[order.status]}
|
||||
/>
|
||||
<Text style={[styles.statusText, { color: statusColor[order.status] }]}>{statusLabel[order.status]}</Text>
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<View style={styles.emptyBlock}>
|
||||
<Ionicons name="basket-outline" size={36} color={colors.textSubtle} />
|
||||
<Text style={styles.emptyTxt}>Ainda não tens compras.</Text>
|
||||
<Text style={styles.emptyTxtPremium}>Sem marcações agendadas.</Text>
|
||||
<Button size="sm" variant="ghost" onPress={() => navigation.navigate('ExploreTab' as never)}>Procurar Barbearias</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Favoritos Section */}
|
||||
<View>
|
||||
<View style={styles.clientSectionHeader}>
|
||||
<Ionicons name="heart" size={20} color={colors.danger} />
|
||||
<Text style={styles.clientSectionTitle}>Espaços Favoritos</Text>
|
||||
</View>
|
||||
<View style={styles.listContainer}>
|
||||
{favoriteShops.length > 0 ? (
|
||||
favoriteShops.map((s) => (
|
||||
<TouchableOpacity
|
||||
key={s.id}
|
||||
onPress={() => navigation.navigate('ShopDetails', { shopId: s.id })}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<Card style={styles.shopCardPremium}>
|
||||
<Image source={{ uri: s.imageUrl || 'https://via.placeholder.com/100' }} style={styles.shopImageMini} />
|
||||
<View style={styles.shopCardBodyPremium}>
|
||||
<Text style={styles.shopNamePremium} numberOfLines={1}>{s.name}</Text>
|
||||
<View style={styles.shopLocationRow}>
|
||||
<Ionicons name="location-outline" size={13} color={colors.textMuted} />
|
||||
<Text style={styles.shopAddrPremium} numberOfLines={1}>
|
||||
{s.address && s.address !== 'Endereço a definir' ? s.address : 'Morada por definir'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.shopRatingBadgePremium}>
|
||||
<Ionicons name="star" size={14} color={colors.star} />
|
||||
<Text style={styles.shopRatingTextPremium}>{(s.rating || 0).toFixed(1)}</Text>
|
||||
</View>
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<View style={styles.emptyBlockPremium}>
|
||||
<View style={styles.emptyIconCircle}>
|
||||
<Ionicons name="heart-outline" size={28} color={colors.textSubtle} />
|
||||
</View>
|
||||
<Text style={styles.emptyTxtPremium}>Ainda não tens favoritos.</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Compras Section */}
|
||||
<View>
|
||||
<View style={styles.clientSectionHeader}>
|
||||
<Ionicons name="bag-handle" size={20} color={colors.primaryDark} />
|
||||
<Text style={styles.clientSectionTitle}>As Minhas Compras</Text>
|
||||
</View>
|
||||
<View style={styles.listContainer}>
|
||||
{myOrders.length > 0 ? (
|
||||
myOrders.map((order) => {
|
||||
const shop = shops.find((s) => s.id === order.shopId);
|
||||
return (
|
||||
<Card key={order.id} style={styles.orderCardPremium}>
|
||||
<View style={styles.orderTopPremium}>
|
||||
<View style={{ flex: 1, gap: 4 }}>
|
||||
<Text style={styles.orderShopPremium} numberOfLines={1}>{shop?.name || 'Barbearia'}</Text>
|
||||
<View style={styles.orderMetaRow}>
|
||||
<Ionicons name="calendar-outline" size={14} color={colors.textSubtle} />
|
||||
<Text style={styles.orderMetaPremium}>
|
||||
{new Date(order.createdAt).toLocaleDateString('pt-PT')} • {order.items.length} item(s)
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ alignItems: 'flex-end', gap: 6 }}>
|
||||
<Text style={styles.orderTotalPremium}>{currency(order.total)}</Text>
|
||||
<View style={[styles.statusTagPremium, { backgroundColor: statusColor[order.status] + '1A' }]}>
|
||||
<Text style={[styles.statusTextPremium, { color: statusColor[order.status] }]}>{statusLabel[order.status]}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<View style={styles.emptyBlockPremium}>
|
||||
<View style={styles.emptyIconCircle}>
|
||||
<Ionicons name="bag-handle-outline" size={28} color={colors.textSubtle} />
|
||||
</View>
|
||||
<Text style={styles.emptyTxtPremium}>Ainda não tens compras efetuadas.</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1086,6 +1099,169 @@ const styles = StyleSheet.create({
|
||||
borderTopColor: 'rgba(15,118,110,0.08)',
|
||||
alignItems: 'center',
|
||||
},
|
||||
/* ── Premium Client Layout Styles ── */
|
||||
clientSectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
clientSectionTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -0.3,
|
||||
},
|
||||
agendaCardPremium: {
|
||||
padding: 16,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
gap: 16,
|
||||
...shadows.soft,
|
||||
},
|
||||
calendarBlockPremium: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
backgroundColor: colors.primarySoft,
|
||||
borderRadius: radius.md,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.1)',
|
||||
},
|
||||
dateMonthPremium: {
|
||||
color: colors.primaryDark,
|
||||
fontSize: 11,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
dateDayPremium: {
|
||||
color: colors.primary,
|
||||
fontSize: 22,
|
||||
fontWeight: '900',
|
||||
marginTop: -2,
|
||||
},
|
||||
agendaShopPremium: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
},
|
||||
agendaTimePremium: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
statusTagPremium: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: radius.pill,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
statusTextPremium: {
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
emptyBlockPremium: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 32,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.04)',
|
||||
gap: 12,
|
||||
},
|
||||
emptyIconCircle: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: colors.background,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
emptyTxtPremium: {
|
||||
color: colors.textMuted,
|
||||
fontWeight: '600',
|
||||
fontSize: 14,
|
||||
marginBottom: 4,
|
||||
},
|
||||
shopCardPremium: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 12,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
gap: 12,
|
||||
},
|
||||
shopImageMini: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: radius.md,
|
||||
backgroundColor: colors.surfaceMuted,
|
||||
},
|
||||
shopCardBodyPremium: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
shopNamePremium: {
|
||||
color: colors.text,
|
||||
fontSize: 15,
|
||||
fontWeight: '800',
|
||||
marginBottom: 2,
|
||||
},
|
||||
shopAddrPremium: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
flex: 1,
|
||||
},
|
||||
shopRatingBadgePremium: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.accentSoft,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: radius.pill,
|
||||
gap: 4,
|
||||
},
|
||||
shopRatingTextPremium: {
|
||||
color: '#7a4310',
|
||||
fontSize: 12,
|
||||
fontWeight: '900',
|
||||
},
|
||||
orderCardPremium: {
|
||||
padding: 16,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
},
|
||||
orderTopPremium: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
orderShopPremium: {
|
||||
color: colors.text,
|
||||
fontSize: 15,
|
||||
fontWeight: '800',
|
||||
},
|
||||
orderMetaPremium: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
},
|
||||
orderTotalPremium: {
|
||||
color: colors.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: '900',
|
||||
},
|
||||
logoutButtonPremium: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
Reference in New Issue
Block a user