refactor: implement role-based profile dashboard with custom tab navigation and barber-specific views

This commit is contained in:
2026-05-12 16:55:52 +01:00
parent d29cf7535b
commit a657308f9d
4 changed files with 380 additions and 455 deletions

View File

@@ -69,19 +69,24 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
useEffect(() => {
let mounted = true;
storage.get<{ favorites?: string[]; cart?: CartItem[] }>('smart-agenda-mobile-state', {}).then((stored) => {
if (!mounted) return;
if (Array.isArray(stored.favorites)) setFavorites(stored.favorites);
if (Array.isArray(stored.cart)) setCart(stored.cart);
});
if (user?.id) {
storage.get<{ favorites?: string[]; cart?: CartItem[] }>(`smart-agenda-user-${user.id}`, {}).then((stored) => {
if (!mounted) return;
setFavorites(Array.isArray(stored.favorites) ? stored.favorites : []);
});
} else {
setFavorites([]);
}
return () => {
mounted = false;
};
}, []);
}, [user?.id]);
useEffect(() => {
storage.set('smart-agenda-mobile-state', { favorites, cart });
}, [favorites, cart]);
if (user?.id) {
storage.set(`smart-agenda-user-${user.id}`, { favorites, cart });
}
}, [favorites, cart, user?.id]);
const applySupabaseUser = async (authUser: any): Promise<User | undefined> => {
if (!authUser) return undefined;

View File

@@ -62,7 +62,7 @@ export default function Booking() {
});
if (appt) {
Alert.alert('Sucesso', 'O seu agendamento foi confirmado.');
navigation.navigate('Profile' as never);
navigation.navigate('ProfileTab' as never);
}
};

View File

@@ -31,18 +31,13 @@ export default function Dashboard() {
user,
shops,
appointments,
orders,
refreshShops,
updateAppointmentStatus,
updateOrderStatus,
addService,
updateService,
deleteService,
addProduct,
updateProduct,
deleteProduct,
addBarber,
updateBarber,
deleteBarber,
updateShopDetails,
} = useApp();
@@ -60,11 +55,28 @@ export default function Dashboard() {
}, [appointments, shop?.id, dateFilter]);
// Form states
const [editingId, setEditingId] = useState<string | null>(null);
const [formSvc, setFormSvc] = useState({ name: '', price: '', duration: '' });
const [formProd, setFormProd] = useState({ name: '', price: '', stock: '' });
const [formBarb, setFormBarb] = useState({ name: '', specialties: '' });
// Settings states
const [shopName, setShopName] = useState(shop?.name || '');
const [shopAddr, setShopAddr] = useState(shop?.address || '');
const [phone1, setPhone1] = useState(shop?.contacts?.phone1 || '');
const [whatsapp, setWhatsapp] = useState(shop?.socialNetworks?.whatsapp || '');
const [instagram, setInstagram] = useState(shop?.socialNetworks?.instagram || '');
const [facebook, setFacebook] = useState(shop?.socialNetworks?.facebook || '');
const [paymentMethods, setPaymentMethods] = useState(shop?.paymentMethods?.join(', ') || '');
const [schedule, setSchedule] = useState(shop?.schedule || [
{ day: 'Segunda', open: '09:00', close: '19:00' },
{ day: 'Terça', open: '09:00', close: '19:00' },
{ day: 'Quarta', open: '09:00', close: '19:00' },
{ day: 'Quinta', open: '09:00', close: '19:00' },
{ day: 'Sexta', open: '09:00', close: '19:00' },
{ day: 'Sábado', open: '09:00', close: '18:00' },
{ day: 'Domingo', open: '00:00', close: '00:00', closed: true },
]);
const pickImage = async (target: 'shop' | 'barber', barberId?: string) => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
@@ -102,7 +114,10 @@ export default function Dashboard() {
await updateShopDetails(shop!.id, { imageUrl: publicUrl });
} else if (barberId) {
const b = shop?.barbers.find(x => x.id === barberId);
if (b) await updateBarber(shop!.id, { ...b, imageUrl: publicUrl });
if (b) {
// Update barber logic in context or direct supabase
await supabase.from('barbers').update({ image_url: publicUrl }).eq('id', barberId);
}
}
await refreshShops();
} catch (e: any) {
@@ -113,6 +128,27 @@ export default function Dashboard() {
}
};
const handleSaveSettings = async () => {
if (!shop) return;
setLoading(true);
try {
await updateShopDetails(shop.id, {
name: shopName,
address: shopAddr,
contacts: { phone1 },
socialNetworks: { whatsapp, instagram, facebook },
paymentMethods: paymentMethods.split(',').map(s => s.trim()).filter(Boolean),
schedule,
});
Alert.alert('Sucesso', 'Definições guardadas com sucesso.');
await refreshShops();
} catch (e: any) {
Alert.alert('Erro', e.message);
} finally {
setLoading(false);
}
};
if (!shop) return null;
return (
@@ -120,10 +156,10 @@ export default function Dashboard() {
{/* Header Dashboard */}
<View style={styles.header}>
<View style={styles.headerInfo}>
<Text style={styles.headerGreeting}>Olá, Parceiro</Text>
<Text style={styles.headerGreeting}>Gestão do Espaço</Text>
<Text style={styles.headerShopName} numberOfLines={1}>{shop.name}</Text>
</View>
<TouchableOpacity style={styles.brandPill} onPress={() => navigation.navigate('ProfileTab' as never)}>
<TouchableOpacity style={styles.brandPill} onPress={() => navigation.navigate('BarberProfileTab' as never)}>
<Text style={styles.brandText}>{user?.name.charAt(0)}</Text>
</TouchableOpacity>
</View>
@@ -134,7 +170,7 @@ export default function Dashboard() {
{[
['agenda', 'Agenda'],
['servicos', 'Serviços'],
['produtos', 'Inventário'],
['produtos', 'Produtos'],
['equipa', 'Equipa'],
['perfil', 'Definições'],
].map(([id, label]) => (
@@ -153,7 +189,7 @@ export default function Dashboard() {
{activeTab === 'agenda' && (
<View style={styles.tabBox}>
<View style={styles.agendaHeader}>
<Text style={styles.sectionTitle}>Agenda do Dia</Text>
<Text style={styles.sectionTitle}>Marcações</Text>
<TextInput
value={dateFilter}
onChangeText={setDateFilter}
@@ -197,7 +233,7 @@ export default function Dashboard() {
})
) : (
<View style={styles.emptyBox}>
<Text style={styles.emptyTxt}>Nenhum agendamento para este dia.</Text>
<Text style={styles.emptyTxt}>Sem marcações para hoje.</Text>
</View>
)}
</View>
@@ -205,7 +241,7 @@ export default function Dashboard() {
{activeTab === 'servicos' && (
<View style={styles.tabBox}>
<Text style={styles.sectionTitle}>Gerir Serviços</Text>
<Text style={styles.sectionTitle}>Serviços Disponíveis</Text>
{shop.services.map(s => (
<Card key={s.id} style={styles.itemCard}>
<View style={styles.itemInfo}>
@@ -228,14 +264,46 @@ export default function Dashboard() {
<Button size="sm" onPress={async () => {
await addService(shop.id, { name: formSvc.name, price: Number(formSvc.price), duration: Number(formSvc.duration), barberIds: [] });
setFormSvc({ name: '', price: '', duration: '' });
Alert.alert('Sucesso', 'Serviço adicionado.');
}}>Adicionar Serviço</Button>
</Card>
</View>
)}
{activeTab === 'produtos' && (
<View style={styles.tabBox}>
<Text style={styles.sectionTitle}>Inventário de Produtos</Text>
{shop.products.map(p => (
<Card key={p.id} style={styles.itemCard}>
<View style={styles.itemInfo}>
<Text style={styles.itemName}>{p.name}</Text>
<Text style={styles.itemMeta}>Stock: {p.stock} · {currency(p.price)}</Text>
</View>
<TouchableOpacity onPress={() => deleteProduct(shop.id, p.id)} style={styles.deleteIcon}>
<Text style={styles.deleteTxt}></Text>
</TouchableOpacity>
</Card>
))}
<Card style={styles.formCard}>
<Text style={styles.formTitle}>Novo Produto</Text>
<TextInput style={styles.input} placeholder="Nome do produto" placeholderTextColor="#475569" value={formProd.name} onChangeText={t => setFormProd({...formProd, name: t})} />
<View style={styles.row}>
<TextInput style={[styles.input, {flex: 1}]} placeholder="Preço (€)" placeholderTextColor="#475569" keyboardType="numeric" value={formProd.price} onChangeText={t => setFormProd({...formProd, price: t})} />
<TextInput style={[styles.input, {flex: 1}]} placeholder="Stock" placeholderTextColor="#475569" keyboardType="numeric" value={formProd.stock} onChangeText={t => setFormProd({...formProd, stock: t})} />
</View>
<Button size="sm" onPress={async () => {
await addProduct(shop.id, { name: formProd.name, price: Number(formProd.price), stock: Number(formProd.stock) });
setFormProd({ name: '', price: '', stock: '' });
Alert.alert('Sucesso', 'Produto adicionado.');
}}>Adicionar Produto</Button>
</Card>
</View>
)}
{activeTab === 'equipa' && (
<View style={styles.tabBox}>
<Text style={styles.sectionTitle}>Equipa</Text>
<Text style={styles.sectionTitle}>Membros da Equipa</Text>
{shop.barbers.map(b => (
<Card key={b.id} style={styles.itemCard}>
<TouchableOpacity onPress={() => pickImage('barber', b.id)} style={styles.barberAvatar}>
@@ -250,12 +318,27 @@ export default function Dashboard() {
</TouchableOpacity>
</Card>
))}
<Card style={styles.formCard}>
<Text style={styles.formTitle}>Novo Profissional</Text>
<TextInput style={styles.input} placeholder="Nome do profissional" placeholderTextColor="#475569" value={formBarb.name} onChangeText={t => setFormBarb({...formBarb, name: t})} />
<TextInput style={styles.input} placeholder="Especialidades (separadas por vírgula)" placeholderTextColor="#475569" value={formBarb.specialties} onChangeText={t => setFormBarb({...formBarb, specialties: t})} />
<Button size="sm" onPress={async () => {
await addBarber(shop.id, {
name: formBarb.name,
specialties: formBarb.specialties.split(',').map(s => s.trim()).filter(Boolean),
schedule: []
});
setFormBarb({ name: '', specialties: '' });
Alert.alert('Sucesso', 'Membro adicionado.');
}}>Adicionar Membro</Button>
</Card>
</View>
)}
{activeTab === 'perfil' && (
<View style={styles.tabBox}>
<Text style={styles.sectionTitle}>Definições do Espaço</Text>
<Text style={styles.sectionTitle}>Configurações do Espaço</Text>
<TouchableOpacity onPress={() => pickImage('shop')} style={styles.coverUpload}>
{shop.imageUrl ? (
<Image source={{ uri: shop.imageUrl }} style={styles.fullImg} />
@@ -266,13 +349,62 @@ export default function Dashboard() {
</TouchableOpacity>
<Card style={styles.formCard}>
<Text style={styles.inputLabel}>Nome do Espaço</Text>
<TextInput style={styles.input} value={shop.name} onChangeText={t => updateShopDetails(shop.id, { name: t })} />
<Text style={styles.inputLabel}>Informação Geral</Text>
<TextInput style={styles.input} placeholder="Nome da Barbearia" placeholderTextColor="#475569" value={shopName} onChangeText={setShopName} />
<TextInput style={styles.input} placeholder="Endereço Completo" placeholderTextColor="#475569" value={shopAddr} onChangeText={setShopAddr} />
<TextInput style={styles.input} placeholder="Telefone de Contacto" placeholderTextColor="#475569" value={phone1} onChangeText={setPhone1} keyboardType="phone-pad" />
<Text style={styles.inputLabel}>Endereço</Text>
<TextInput style={styles.input} value={shop.address} onChangeText={t => updateShopDetails(shop.id, { address: t })} />
<Text style={[styles.inputLabel, { marginTop: 12 }]}>Redes Sociais</Text>
<TextInput style={styles.input} placeholder="WhatsApp (ex: 351912345678)" placeholderTextColor="#475569" value={whatsapp} onChangeText={setWhatsapp} />
<TextInput style={styles.input} placeholder="Instagram (User)" placeholderTextColor="#475569" value={instagram} onChangeText={setInstagram} />
<TextInput style={styles.input} placeholder="Facebook (Link)" placeholderTextColor="#475569" value={facebook} onChangeText={setFacebook} />
<Text style={[styles.inputLabel, { marginTop: 12 }]}>Pagamentos</Text>
<TextInput style={styles.input} placeholder="Métodos (ex: Dinheiro, MBWay, Cartão)" placeholderTextColor="#475569" value={paymentMethods} onChangeText={setPaymentMethods} />
<Button style={{ marginTop: 10 }} onPress={() => refreshShops()}>Guardar Alterações</Button>
<Text style={[styles.inputLabel, { marginTop: 12 }]}>Horário de Funcionamento</Text>
{schedule.map((s, idx) => (
<View key={s.day} style={styles.scheduleRow}>
<Text style={styles.dayLabel}>{s.day}</Text>
<TouchableOpacity
onPress={() => {
const next = [...schedule];
next[idx] = { ...next[idx], closed: !next[idx].closed };
setSchedule(next);
}}
style={[styles.closedBtn, s.closed && styles.closedBtnActive]}
>
<Text style={[styles.closedText, s.closed && styles.closedTextActive]}>
{s.closed ? 'Fechado' : 'Aberto'}
</Text>
</TouchableOpacity>
{!s.closed && (
<View style={styles.timeInputs}>
<TextInput
style={styles.timeInput}
value={s.open}
onChangeText={t => {
const next = [...schedule];
next[idx] = { ...next[idx], open: t };
setSchedule(next);
}}
/>
<Text style={{ color: '#475569' }}>-</Text>
<TextInput
style={styles.timeInput}
value={s.close}
onChangeText={t => {
const next = [...schedule];
next[idx] = { ...next[idx], close: t };
setSchedule(next);
}}
/>
</View>
)}
</View>
))}
<Button style={{ marginTop: 10 }} onPress={handleSaveSettings} loading={loading}>Guardar Todas as Alterações</Button>
</Card>
</View>
)}
@@ -517,6 +649,7 @@ const styles = StyleSheet.create({
fontSize: 14,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.04)',
marginBottom: 8,
},
inputLabel: {
color: '#94a3b8',
@@ -524,6 +657,7 @@ const styles = StyleSheet.create({
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 8,
},
row: {
flexDirection: 'row',
@@ -578,4 +712,53 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: '700',
},
scheduleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 10,
},
dayLabel: {
color: '#f8fafc',
fontSize: 13,
fontWeight: '700',
width: 65,
},
closedBtn: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
backgroundColor: 'rgba(16,185,129,0.1)',
borderWidth: 1,
borderColor: 'rgba(16,185,129,0.2)',
},
closedBtnActive: {
backgroundColor: 'rgba(239,68,68,0.1)',
borderColor: 'rgba(239,68,68,0.2)',
},
closedText: {
color: '#10b981',
fontSize: 11,
fontWeight: '800',
},
closedTextActive: {
color: '#ef4444',
},
timeInputs: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
timeInput: {
backgroundColor: '#1c1c2e',
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 4,
color: '#f8fafc',
fontSize: 12,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.04)',
width: 55,
textAlign: 'center',
},
});

View File

@@ -24,7 +24,8 @@ const statusLabel: Record<string, string> = {
cancelado: 'Cancelado',
};
type Tab = 'favoritos' | 'agenda' | 'pedidos';
type ClientTab = 'favoritos' | 'agenda' | 'pedidos';
type BarberTab = 'reviews' | 'agenda_pessoal' | 'estatisticas';
export default function Profile() {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
@@ -40,41 +41,49 @@ export default function Profile() {
submitReview,
} = useApp();
const [activeTab, setActiveTab] = useState<Tab>('agenda');
const [activeTab, setActiveTab] = useState<string>(user?.role === 'barbearia' ? 'agenda_pessoal' : 'agenda');
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);
const [comment, setComment] = useState('');
const [submittingReview, setSubmittingReview] = useState(false);
const [shopReviews, setShopReviews] = useState<any[]>([]);
const isBarber = user?.role === 'barbearia';
const myShop = useMemo(() => shops.find(s => s.id === user?.shopId), [shops, user?.shopId]);
useEffect(() => {
let mounted = true;
(async () => {
if (!user) return;
const { data } = await supabase
.from('reviews')
.select('appointment_id')
.eq('customer_id', user.id);
if (!mounted || !data) return;
setReviewedAppointments(new Set(data.map((row: any) => row.appointment_id).filter(Boolean)));
if (user.role === 'cliente') {
const { data } = await supabase.from('reviews').select('appointment_id').eq('customer_id', user.id);
if (!mounted || !data) return;
setReviewedAppointments(new Set(data.map((row: any) => row.appointment_id).filter(Boolean)));
} else if (user.role === 'barbearia' && user.shopId) {
const { data } = await supabase.from('reviews').select('*').eq('shop_id', user.shopId).order('created_at', { ascending: false });
if (!mounted || !data) return;
setShopReviews(data);
}
})();
return () => {
mounted = false;
};
return () => { mounted = false; };
}, [user?.id]);
const myAppointments = useMemo(
() => (user ? appointments.filter((a) => a.customerId === user.id) : []),
[appointments, user?.id]
() => (user ? (isBarber ? appointments.filter(a => a.shopId === user.shopId) : appointments.filter((a) => a.customerId === user.id)) : []),
[appointments, user?.id, isBarber]
);
const myOrders = useMemo(
() => (user ? orders.filter((o) => o.customerId === user.id) : []),
[orders, user?.id]
);
const favoriteShops = useMemo(
() => shops.filter((shop) => favorites.includes(shop.id)),
[shops, favorites]
);
const myNotifications = useMemo(
() =>
user
@@ -85,19 +94,7 @@ export default function Profile() {
[notifications, user?.id]
);
if (!user) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.centerState}>
<Text style={styles.centerTitle}>Sessão expirada</Text>
<Text style={styles.centerText}>Faz login para aceder ao teu perfil.</Text>
<Button style={styles.darkButton} onPress={() => navigation.replace('Login')}>
Iniciar Sessão
</Button>
</View>
</SafeAreaView>
);
}
if (!user) return null;
const handleReviewSubmit = async () => {
if (!reviewTarget || rating === 0) return;
@@ -106,8 +103,6 @@ export default function Profile() {
await submitReview(reviewTarget.shopId, reviewTarget.appointmentId, rating, comment);
setReviewedAppointments((prev) => new Set([...prev, reviewTarget.appointmentId]));
setReviewTarget(null);
setRating(0);
setComment('');
Alert.alert('Obrigado', 'A tua avaliação foi enviada.');
} catch (e: any) {
Alert.alert('Erro', e?.message || 'Erro ao enviar avaliação.');
@@ -128,7 +123,7 @@ export default function Profile() {
<Text style={styles.profileName}>{user.name}</Text>
<Text style={styles.profileEmail}>{user.email}</Text>
<View style={styles.roleBadge}>
<Text style={styles.roleText}>{user.role === 'cliente' ? 'Cliente' : 'Parceiro'}</Text>
<Text style={styles.roleText}>{isBarber ? 'Gestor de Espaço' : 'Cliente'}</Text>
</View>
</View>
<TouchableOpacity onPress={logout} style={styles.logoutBtn}>
@@ -155,16 +150,20 @@ export default function Profile() {
</View>
)}
{/* Tabs Estilizadas */}
{/* Tabs Role-Based */}
<View style={styles.tabBar}>
{[
['agenda', 'Agenda'],
{(isBarber ? [
['agenda_pessoal', 'Agenda Global'],
['reviews', 'Avaliações'],
['estatisticas', 'Resumo'],
] : [
['agenda', 'Minha Agenda'],
['favoritos', 'Favoritos'],
['pedidos', 'Pedidos'],
].map(([id, label]) => (
['pedidos', 'Compras'],
]).map(([id, label]) => (
<TouchableOpacity
key={id}
onPress={() => setActiveTab(id as Tab)}
onPress={() => setActiveTab(id)}
style={[styles.tabItem, activeTab === id && styles.tabActive]}
>
<Text style={[styles.tabText, activeTab === id && styles.tabTextActive]}>{label}</Text>
@@ -175,6 +174,7 @@ export default function Profile() {
{/* Conteúdo das Tabs */}
<View style={styles.tabContent}>
{/* CLIENTE TABS */}
{activeTab === 'agenda' && (
<View style={styles.listContainer}>
{myAppointments.length > 0 ? (
@@ -182,124 +182,115 @@ export default function Profile() {
const shop = shops.find((s) => s.id === appt.shopId);
const canReview = appt.status === 'concluido' && !reviewedAppointments.has(appt.id);
const dateParts = appt.date.split(' ');
const dateStr = dateParts[0];
const timeStr = dateParts[1] || '';
return (
<Card key={appt.id} style={styles.agendaCard}>
<View style={styles.agendaTop}>
<View style={styles.dateBox}>
<Text style={styles.dateDay}>{dateStr.split('-')[2]}</Text>
<Text style={styles.dateMonth}>{new Date(dateStr).toLocaleString('pt-PT', { month: 'short' }).toUpperCase()}</Text>
<Text style={styles.dateDay}>{dateParts[0].split('-')[2]}</Text>
<Text style={styles.dateMonth}>{new Date(dateParts[0]).toLocaleString('pt-PT', { month: 'short' }).toUpperCase()}</Text>
</View>
<View style={styles.agendaMain}>
<Text style={styles.agendaShop} numberOfLines={1}>{shop?.name || 'Barbearia'}</Text>
<Text style={styles.agendaTime}>{timeStr} · {currency(appt.total)}</Text>
<Text style={styles.agendaTime}>{dateParts[1]} · {currency(appt.total)}</Text>
</View>
<View style={[styles.statusTag, { backgroundColor: statusColor[appt.status] + '20' }]}>
<Text style={[styles.statusText, { 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 Experiência
<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.emptyBox}>
<Text style={styles.emptyText}>Nenhum agendamento ativo.</Text>
</View>
)}
) : <Text style={styles.emptyTxt}>Sem marcações agendadas.</Text>}
</View>
)}
{activeTab === 'favoritos' && (
<View style={styles.listContainer}>
{favoriteShops.length > 0 ? (
favoriteShops.map((shop) => (
<TouchableOpacity
key={shop.id}
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
>
<Card style={styles.shopCard}>
<View style={styles.shopIcon}>
<Text style={styles.shopIconText}>{shop.name.charAt(0)}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.shopName}>{shop.name}</Text>
<Text style={styles.shopAddr} numberOfLines={1}>{shop.address}</Text>
</View>
<Text style={styles.shopRating}> {shop.rating.toFixed(1)}</Text>
</Card>
</TouchableOpacity>
))
) : (
<View style={styles.emptyBox}>
<Text style={styles.emptyText}>Sem barbearias favoritas.</Text>
</View>
)}
{favoriteShops.map(s => (
<TouchableOpacity key={s.id} onPress={() => navigation.navigate('ShopDetails', { shopId: s.id })}>
<Card style={styles.shopCard}>
<View style={styles.shopIcon}><Text style={styles.shopIconText}>{s.name.charAt(0)}</Text></View>
<View style={{ flex: 1 }}><Text style={styles.shopName}>{s.name}</Text><Text style={styles.shopAddr} numberOfLines={1}>{s.address}</Text></View>
<Text style={styles.shopRating}> {s.rating.toFixed(1)}</Text>
</Card>
</TouchableOpacity>
))}
</View>
)}
{activeTab === 'pedidos' && (
{/* BARBER TABS */}
{activeTab === 'agenda_pessoal' && (
<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.orderHeader}>
<Text style={styles.orderShop}>{shop?.name || 'Barbearia'}</Text>
<Text style={styles.orderPrice}>{currency(order.total)}</Text>
</View>
<View style={styles.orderFooter}>
<Text style={styles.orderDate}>{new Date(order.createdAt).toLocaleDateString()}</Text>
<View style={[styles.statusTag, { backgroundColor: statusColor[order.status] + '20' }]}>
<Text style={[styles.statusText, { color: statusColor[order.status] }]}>{statusLabel[order.status]}</Text>
</View>
</View>
</Card>
);
})
) : (
<View style={styles.emptyBox}>
<Text style={styles.emptyText}>Ainda não tens pedidos.</Text>
</View>
)}
<Text style={styles.subTitle}>Todas as marcações do espaço</Text>
{myAppointments.map(appt => (
<Card key={appt.id} style={styles.agendaCard}>
<View style={styles.agendaTop}>
<View style={styles.dateBox}>
<Text style={styles.dateDay}>{appt.date.split(' ')[0].split('-')[2]}</Text>
<Text style={styles.dateMonth}>{new Date(appt.date.split(' ')[0]).toLocaleString('pt-PT', { month: 'short' }).toUpperCase()}</Text>
</View>
<View style={styles.agendaMain}>
<Text style={styles.agendaShop}>{appt.date.split(' ')[1]}</Text>
<Text style={styles.agendaTime}>Status: {statusLabel[appt.status]}</Text>
</View>
</View>
</Card>
))}
</View>
)}
{activeTab === 'reviews' && (
<View style={styles.listContainer}>
{shopReviews.map(r => (
<Card key={r.id} style={styles.reviewCard}>
<View style={styles.reviewHeader}>
<Text style={styles.reviewStars}>{'★'.repeat(r.rating)}</Text>
<Text style={styles.reviewDate}>{new Date(r.created_at).toLocaleDateString()}</Text>
</View>
<Text style={styles.reviewComment}>{r.comment || 'Sem comentário.'}</Text>
</Card>
))}
</View>
)}
{activeTab === 'estatisticas' && (
<View style={styles.listContainer}>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Total de Marcações</Text>
<Text style={styles.statValue}>{myAppointments.length}</Text>
</Card>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Rating Médio</Text>
<Text style={styles.statValue}>{myShop?.rating.toFixed(1)} </Text>
</Card>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Vendas em Produtos</Text>
<Text style={styles.statValue}>{currency(myOrders.reduce((sum, o) => sum + o.total, 0))}</Text>
</Card>
</View>
)}
</View>
{/* Modal de Avaliação (Inline) */}
{/* Review Modal for Clients */}
{reviewTarget && (
<Card style={styles.reviewModal}>
<Text style={styles.reviewTitle}>Como foi o serviço na {reviewTarget.shopName}?</Text>
<Text style={styles.reviewTitle}>Avaliar {reviewTarget.shopName}</Text>
<View style={styles.stars}>
{[1, 2, 3, 4, 5].map((s) => (
{[1,2,3,4,5].map(s => (
<TouchableOpacity key={s} onPress={() => setRating(s)}>
<Text style={[styles.star, rating >= s && styles.starActive]}></Text>
</TouchableOpacity>
))}
</View>
<TextInput
style={styles.reviewInput}
placeholder="Partilha a tua opinião (opcional)..."
placeholderTextColor="#475569"
multiline
value={comment}
onChangeText={setComment}
/>
<TextInput style={styles.reviewInput} placeholder="Diz-nos o que achaste..." placeholderTextColor="#475569" multiline value={comment} onChangeText={setComment} />
<View style={styles.reviewActions}>
<Button variant="ghost" onPress={() => setReviewTarget(null)} style={{ flex: 1 }}>Cancelar</Button>
<Button onPress={handleReviewSubmit} loading={submittingReview} disabled={rating === 0} style={{ flex: 2 }}>Enviar Avaliação</Button>
<Button onPress={handleReviewSubmit} loading={submittingReview} disabled={rating === 0} style={{ flex: 2 }}>Enviar</Button>
</View>
</Card>
)}
@@ -309,319 +300,65 @@ export default function Profile() {
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0a0a0f',
},
content: {
padding: 20,
paddingBottom: 100,
},
profileHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 32,
gap: 16,
},
avatarBox: {
width: 64,
height: 64,
borderRadius: 22,
backgroundColor: '#141420',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: 'rgba(99,102,241,0.3)',
},
avatarText: {
color: '#818cf8',
fontSize: 24,
fontWeight: '900',
},
profileInfo: {
flex: 1,
gap: 2,
},
profileName: {
color: '#f8fafc',
fontSize: 22,
fontWeight: '900',
},
profileEmail: {
color: '#64748b',
fontSize: 14,
fontWeight: '500',
},
roleBadge: {
alignSelf: 'flex-start',
backgroundColor: 'rgba(99,102,241,0.1)',
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 3,
marginTop: 4,
},
roleText: {
color: '#818cf8',
fontSize: 10,
fontWeight: '800',
textTransform: 'uppercase',
},
logoutBtn: {
padding: 8,
},
logoutIcon: {
color: '#ef4444',
fontSize: 24,
fontWeight: '700',
},
sectionTitle: {
color: '#f8fafc',
fontSize: 18,
fontWeight: '800',
marginBottom: 12,
},
notifSection: {
marginBottom: 32,
},
notifScroll: {
marginHorizontal: -20,
paddingHorizontal: 20,
},
notifCard: {
backgroundColor: '#141420',
width: 240,
borderRadius: 16,
padding: 14,
marginRight: 12,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.06)',
gap: 8,
},
notifUnread: {
borderColor: 'rgba(99,102,241,0.4)',
},
notifMsg: {
color: '#94a3b8',
fontSize: 13,
lineHeight: 18,
},
markRead: {
color: '#818cf8',
fontSize: 12,
fontWeight: '700',
},
tabBar: {
flexDirection: 'row',
marginBottom: 24,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255,255,255,0.06)',
},
tabItem: {
paddingVertical: 12,
marginRight: 24,
position: 'relative',
},
container: { flex: 1, backgroundColor: '#0a0a0f' },
content: { padding: 20, paddingBottom: 100 },
profileHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 32, gap: 16 },
avatarBox: { width: 64, height: 64, borderRadius: 22, backgroundColor: '#141420', alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: 'rgba(99,102,241,0.3)' },
avatarText: { color: '#818cf8', fontSize: 24, fontWeight: '900' },
profileInfo: { flex: 1, gap: 2 },
profileName: { color: '#f8fafc', fontSize: 22, fontWeight: '900' },
profileEmail: { color: '#64748b', fontSize: 14, fontWeight: '500' },
roleBadge: { alignSelf: 'flex-start', backgroundColor: 'rgba(99,102,241,0.1)', borderRadius: 8, paddingHorizontal: 8, paddingVertical: 3, marginTop: 4 },
roleText: { color: '#818cf8', fontSize: 10, fontWeight: '800', textTransform: 'uppercase' },
logoutBtn: { padding: 8 },
logoutIcon: { color: '#ef4444', fontSize: 24, fontWeight: '700' },
sectionTitle: { color: '#f8fafc', fontSize: 18, fontWeight: '800', marginBottom: 12 },
notifSection: { marginBottom: 32 },
notifScroll: { marginHorizontal: -20, paddingHorizontal: 20 },
notifCard: { backgroundColor: '#141420', width: 240, borderRadius: 16, padding: 14, marginRight: 12, borderWidth: 1, borderColor: 'rgba(255,255,255,0.06)', gap: 8 },
notifUnread: { borderColor: 'rgba(99,102,241,0.4)' },
notifMsg: { color: '#94a3b8', fontSize: 13, lineHeight: 18 },
markRead: { color: '#818cf8', fontSize: 12, fontWeight: '700' },
tabBar: { flexDirection: 'row', marginBottom: 24, borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.06)' },
tabItem: { paddingVertical: 12, marginRight: 24, position: 'relative' },
tabActive: {},
tabText: {
color: '#475569',
fontSize: 15,
fontWeight: '700',
},
tabTextActive: {
color: '#a5b4fc',
},
tabIndicator: {
position: 'absolute',
bottom: -1,
left: 0,
right: 0,
height: 2,
backgroundColor: '#6366f1',
borderRadius: 2,
},
tabContent: {
minHeight: 200,
},
listContainer: {
gap: 12,
},
agendaCard: {
padding: 14,
gap: 12,
},
agendaTop: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
dateBox: {
backgroundColor: '#1c1c2e',
padding: 8,
borderRadius: 12,
alignItems: 'center',
minWidth: 50,
},
dateDay: {
color: '#f8fafc',
fontSize: 18,
fontWeight: '900',
},
dateMonth: {
color: '#6366f1',
fontSize: 10,
fontWeight: '800',
},
agendaMain: {
flex: 1,
gap: 2,
},
agendaShop: {
color: '#f8fafc',
fontSize: 16,
fontWeight: '800',
},
agendaTime: {
color: '#64748b',
fontSize: 13,
fontWeight: '500',
},
statusTag: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 10,
},
statusText: {
fontSize: 11,
fontWeight: '800',
textTransform: 'uppercase',
},
reviewBtn: {
marginTop: 4,
},
shopCard: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
padding: 12,
},
shopIcon: {
width: 44,
height: 44,
borderRadius: 12,
backgroundColor: 'rgba(255,255,255,0.03)',
alignItems: 'center',
justifyContent: 'center',
},
shopIconText: {
color: '#475569',
fontSize: 18,
fontWeight: '800',
},
shopName: {
color: '#f8fafc',
fontSize: 16,
fontWeight: '800',
},
shopAddr: {
color: '#475569',
fontSize: 12,
},
shopRating: {
color: '#fbbf24',
fontWeight: '800',
fontSize: 13,
},
orderCard: {
padding: 16,
gap: 8,
},
orderHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
orderShop: {
color: '#f8fafc',
fontSize: 16,
fontWeight: '800',
},
orderPrice: {
color: '#a5b4fc',
fontSize: 16,
fontWeight: '900',
},
orderFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
orderDate: {
color: '#475569',
fontSize: 12,
},
emptyBox: {
alignItems: 'center',
paddingVertical: 40,
},
emptyText: {
color: '#475569',
fontSize: 14,
fontWeight: '600',
},
reviewModal: {
marginTop: 24,
padding: 20,
gap: 16,
borderColor: 'rgba(99,102,241,0.3)',
},
reviewTitle: {
color: '#f8fafc',
fontSize: 16,
fontWeight: '800',
textAlign: 'center',
},
stars: {
flexDirection: 'row',
justifyContent: 'center',
gap: 8,
},
star: {
fontSize: 32,
color: '#1c1c2e',
},
starActive: {
color: '#fbbf24',
},
reviewInput: {
backgroundColor: '#1c1c2e',
borderRadius: 14,
padding: 12,
color: '#f8fafc',
height: 80,
textAlignVertical: 'top',
},
reviewActions: {
flexDirection: 'row',
gap: 12,
},
centerState: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 40,
gap: 16,
},
centerTitle: {
color: '#f8fafc',
fontSize: 22,
fontWeight: '900',
},
centerText: {
color: '#64748b',
textAlign: 'center',
lineHeight: 20,
},
darkButton: {
width: '100%',
},
tabText: { color: '#475569', fontSize: 15, fontWeight: '700' },
tabTextActive: { color: '#a5b4fc' },
tabIndicator: { position: 'absolute', bottom: -1, left: 0, right: 0, height: 2, backgroundColor: '#6366f1', borderRadius: 2 },
tabContent: { minHeight: 200 },
listContainer: { gap: 12 },
emptyTxt: { color: '#475569', textAlign: 'center', marginTop: 20 },
subTitle: { color: '#64748b', fontSize: 13, fontWeight: '700', textTransform: 'uppercase', marginBottom: 8 },
agendaCard: { padding: 14, gap: 12 },
agendaTop: { flexDirection: 'row', alignItems: 'center', gap: 12 },
dateBox: { backgroundColor: '#1c1c2e', padding: 8, borderRadius: 12, alignItems: 'center', minWidth: 50 },
dateDay: { color: '#f8fafc', fontSize: 18, fontWeight: '900' },
dateMonth: { color: '#6366f1', fontSize: 10, fontWeight: '800' },
agendaMain: { flex: 1, gap: 2 },
agendaShop: { color: '#f8fafc', fontSize: 16, fontWeight: '800' },
agendaTime: { color: '#64748b', fontSize: 13 },
statusTag: { paddingHorizontal: 10, paddingVertical: 5, borderRadius: 10 },
statusText: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase' },
reviewBtn: { marginTop: 4 },
shopCard: { flexDirection: 'row', alignItems: 'center', gap: 12, padding: 12 },
shopIcon: { width: 44, height: 44, borderRadius: 12, backgroundColor: 'rgba(255,255,255,0.03)', alignItems: 'center', justifyContent: 'center' },
shopIconText: { color: '#475569', fontSize: 18, fontWeight: '800' },
shopName: { color: '#f8fafc', fontSize: 16, fontWeight: '800' },
shopAddr: { color: '#475569', fontSize: 12 },
shopRating: { color: '#fbbf24', fontWeight: '800', fontSize: 13 },
reviewCard: { padding: 16, gap: 8 },
reviewHeader: { flexDirection: 'row', justifyContent: 'space-between' },
reviewStars: { color: '#fbbf24', fontSize: 14 },
reviewDate: { color: '#475569', fontSize: 11 },
reviewComment: { color: '#f8fafc', fontSize: 13, lineHeight: 18 },
statCard: { padding: 20, alignItems: 'center', gap: 4 },
statLabel: { color: '#64748b', fontSize: 12, fontWeight: '700', textTransform: 'uppercase' },
statValue: { color: '#f8fafc', fontSize: 24, fontWeight: '900' },
reviewModal: { marginTop: 24, padding: 20, gap: 16, borderColor: 'rgba(99,102,241,0.3)' },
reviewTitle: { color: '#f8fafc', fontSize: 16, fontWeight: '800', textAlign: 'center' },
stars: { flexDirection: 'row', justifyContent: 'center', gap: 8 },
star: { fontSize: 32, color: '#1c1c2e' },
starActive: { color: '#fbbf24' },
reviewInput: { backgroundColor: '#1c1c2e', borderRadius: 14, padding: 12, color: '#f8fafc', height: 80, textAlignVertical: 'top' },
reviewActions: { flexDirection: 'row', gap: 12 },
});