fix(mobile): resolve profile navigation crash, organize profile screen in tabs, clean explore page and inspect user role

This commit is contained in:
2026-06-23 15:37:48 +01:00
parent 7b8e8d6150
commit d875e5689c
3 changed files with 181 additions and 149 deletions

View File

@@ -67,7 +67,13 @@ export default function Explore() {
<TouchableOpacity
style={styles.avatarButton}
activeOpacity={0.85}
onPress={() => navigation.navigate('Profile' as any)}
onPress={() => {
if (user) {
navigation.navigate('ProfileTab' as any);
} else {
navigation.navigate('Login' as any);
}
}}
>
<View style={styles.avatarGradient}>
<Text style={styles.avatarText}>
@@ -114,7 +120,8 @@ export default function Explore() {
</ScrollView>
</View>
{/* Categories Quick Grid */}
{/* Categories Quick Grid (hidden for now) */}
{false && (
<View style={styles.categoriesSection}>
<Text style={styles.sectionTitle}>Categorias Populares</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.categoriesScroll}>
@@ -140,8 +147,10 @@ export default function Explore() {
})}
</ScrollView>
</View>
)}
{/* Promo Hero Banner */}
{/* Promo Hero Banner (hidden for now) */}
{false && (
<View style={styles.promoBanner}>
<View style={styles.promoContent}>
<View style={styles.promoBadge}>
@@ -161,6 +170,7 @@ export default function Explore() {
<Ionicons name="sparkles" size={80} color="rgba(255,255,255,0.08)" style={styles.promoBackgroundIcon} />
</View>
</View>
)}
{!shopsReady ? (
<View style={styles.emptyState}>

View File

@@ -48,6 +48,11 @@ export default function Profile() {
} = useApp();
const [activeTab, setActiveTab] = useState<string>(user?.role === 'barbearia' ? 'agenda_pessoal' : 'agenda');
useEffect(() => {
if (user) {
setActiveTab(user.role === 'barbearia' ? 'agenda_pessoal' : 'agenda');
}
}, [user?.role]);
const [reviewedAppointments, setReviewedAppointments] = useState<Set<string>>(new Set());
const [reviewTarget, setReviewTarget] = useState<{ appointmentId: string; shopId: string; shopName: string } | null>(null);
const [rating, setRating] = useState(0);
@@ -199,11 +204,11 @@ export default function Profile() {
</View>
)}
{/* Dynamic Tab Bar (Only for Barber) */}
{isBarber && (
<View style={styles.tabBarContainer}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.tabBar}>
{[
{/* Dynamic Tab Bar */}
<View style={styles.tabBarContainer}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.tabBar}>
{isBarber ? (
[
['agenda_pessoal', 'Agenda Global'],
['reviews', 'Avaliações'],
['estatisticas', 'Resumo'],
@@ -216,164 +221,181 @@ export default function Profile() {
>
<Text style={[styles.tabText, activeTab === id && styles.tabTextActive]}>{label}</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
)}
))
) : (
[
['agenda', 'Marcações'],
['favoritos', 'Favoritos'],
['compras', '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>
{/* Tab Content Area */}
<View style={styles.tabContent}>
{/* CLIENTE VERTICAL LAYOUT */}
{/* CLIENTE TABS */}
{!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();
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 style={{ gap: 20 }}>
{activeTab === 'agenda' && (
<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();
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 style={[styles.statusTagPremium, { backgroundColor: statusColor[appt.status] + '1A' }]}>
<Text style={[styles.statusTextPremium, { color: statusColor[appt.status] }]}>{statusLabel[appt.status]}</Text>
</View>
</View>
{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} />
{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>
<Text style={styles.emptyTxtPremium}>Sem marcações agendadas.</Text>
<Button size="sm" variant="ghost" onPress={() => navigation.navigate('ExploreTab' as never)}>Procurar Barbearias</Button>
</View>
<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>
</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)
{activeTab === 'favoritos' && (
<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={{ 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 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>
)}
{activeTab === 'compras' && (
<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>
</View>
</Card>
);
})
) : (
<View style={styles.emptyBlockPremium}>
<View style={styles.emptyIconCircle}>
<Ionicons name="bag-handle-outline" size={28} color={colors.textSubtle} />
</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>
<Text style={styles.emptyTxtPremium}>Ainda não tens compras efetuadas.</Text>
</View>
)}
)}
</View>
</View>
</View>
)}
</View>
)}

View File

@@ -70,7 +70,7 @@ export default function ShopDetails() {
Alert.alert('Sucesso', 'Produto adicionado ao carrinho.');
};
const schedule = (shop.schedule || defaultSchedule).filter(s => !s.isException);
const schedule = (shop.schedule || defaultSchedule).filter((s: any) => !s.isException);
const currentDayIndex = new Date().getDay() === 0 ? 6 : new Date().getDay() - 1;
const isFav = isFavorite(shop.id);