diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 69cf707..2f02afe 100644 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/admin/src/app/api/recommendations/route.ts b/apps/admin/src/app/api/recommendations/route.ts index 84141d8..0d38f50 100644 --- a/apps/admin/src/app/api/recommendations/route.ts +++ b/apps/admin/src/app/api/recommendations/route.ts @@ -28,16 +28,22 @@ export async function GET(request: Request) { } // Check permissions: Users can view their own, Admins/Trainers can view anyone's - if (currentUserId !== targetUserId) { - const currentUser = await db.getUserById(currentUserId) - const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer' + const currentUser = await db.getUserById(currentUserId) + const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer' + if (currentUserId !== targetUserId) { if (!isStaff) { return new NextResponse('Forbidden', { status: 403 }) } } - const recommendations = await db.getRecommendationsByUserId(targetUserId) + let recommendations = await db.getRecommendationsByUserId(targetUserId) + + // Non-staff users should only see approved recommendations + if (!isStaff) { + recommendations = recommendations.filter((rec: any) => rec.status === 'approved') + } + return NextResponse.json(recommendations) } catch (error) { console.error('Error fetching recommendations:', error) diff --git a/apps/mobile/src/app/(tabs)/recommendations.tsx b/apps/mobile/src/app/(tabs)/recommendations.tsx new file mode 100644 index 0000000..ecedc89 --- /dev/null +++ b/apps/mobile/src/app/(tabs)/recommendations.tsx @@ -0,0 +1,201 @@ +import { useEffect, useState } from "react"; +import { View, Text, FlatList, ActivityIndicator, StyleSheet, RefreshControl } from "react-native"; +import { useAuth } from "@clerk/clerk-expo"; +import { API_BASE_URL, API_ENDPOINTS } from "../../config/api"; + +interface Recommendation { + id: string; + userId: string; + content: string; + activityPlan?: string; + dietPlan?: string; + status: string; + createdAt: string; +} + +export default function RecommendationsScreen() { + const { getToken, userId } = useAuth(); + const [recommendations, setRecommendations] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + const fetchRecommendations = async () => { + try { + if (!userId) { + console.error('No userId available'); + return; + } + + const token = await getToken(); + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const url = `${API_BASE_URL}${API_ENDPOINTS.RECOMMENDATIONS}?userId=${userId}`; + console.log('Fetching recommendations from:', url); + + const res = await fetch(url, { headers }); + + if (!res.ok) { + const errorText = await res.text(); + console.error('API Error:', res.status, errorText); + throw new Error(`Network response was not ok: ${res.status}`); + } + + const data = await res.json(); + console.log('Recommendations data:', data); + setRecommendations(data.recommendations || data || []); + } catch (e) { + console.error('Failed to load recommendations', e); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + fetchRecommendations(); + }, []); + + const onRefresh = () => { + setRefreshing(true); + fetchRecommendations(); + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + AI Recommendations + item.id} + refreshControl={} + ListEmptyComponent={ + + No recommendations available yet. + Pull down to refresh + + } + renderItem={({ item }) => ( + + + {item.status.toUpperCase()} + {new Date(item.createdAt).toLocaleDateString()} + + + Daily Advice + {item.content} + + {item.activityPlan && ( + <> + Activity Plan + {item.activityPlan} + + )} + + {item.dietPlan && ( + <> + Diet Plan + {item.dietPlan} + + )} + + )} + /> + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + header: { + fontSize: 28, + fontWeight: 'bold', + padding: 20, + paddingBottom: 12, + color: '#1a1a1a', + }, + centered: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#f5f5f5', + }, + card: { + backgroundColor: '#fff', + padding: 16, + marginHorizontal: 16, + marginBottom: 12, + borderRadius: 12, + shadowColor: '#000', + shadowOpacity: 0.1, + shadowRadius: 4, + shadowOffset: { width: 0, height: 2 }, + elevation: 3, + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + paddingBottom: 12, + borderBottomWidth: 1, + borderBottomColor: '#e0e0e0', + }, + status: { + fontSize: 12, + fontWeight: '600', + color: '#2e7d32', + backgroundColor: '#e8f5e9', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + }, + date: { + fontSize: 12, + color: '#666', + }, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + color: '#1a1a1a', + marginTop: 12, + marginBottom: 6, + }, + content: { + fontSize: 14, + color: '#333', + lineHeight: 20, + }, + emptyContainer: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 60, + }, + empty: { + textAlign: 'center', + fontSize: 16, + color: '#666', + fontWeight: '500', + }, + emptySub: { + textAlign: 'center', + fontSize: 14, + color: '#999', + marginTop: 8, + }, +});