refactor: implement role-based profile dashboard with custom tab navigation and barber-specific views
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user