From 0896dd46d1c70a1855575a55cbe929b5600037f6 Mon Sep 17 00:00:00 2001 From: echo Date: Mon, 24 Nov 2025 19:08:58 +0100 Subject: [PATCH] ai rec refinement --- apps/admin/data/fitai.db | Bin 94208 -> 98304 bytes .../src/app/api/recommendations/route.ts | 14 +- .../mobile/src/app/(tabs)/recommendations.tsx | 201 ++++++++++++++++++ 3 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 apps/mobile/src/app/(tabs)/recommendations.tsx diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 69cf70713423965b3547bf78c9508cb1484b63fe..2f02afee6bccd73320dcc7c942d3174278770167 100644 GIT binary patch delta 2456 zcma)8O>7&-73NT;B+?RBu$@@8D)6asYl_w`xyv7s3}nMm8mE;hwY8|gyu`!ZA-U%M zQ2Qf=y;y0}HmBCc23e#ikQN9GA6qbh(KbMHDRQiDO@RV+4K%(4fr}Qs6j1wylvN}~ z00ShrocZRx_kHhsL+;&=?d^@b^!;UB`M z!o$JB%Oj^W11$>7uM>{L8?Gm^Y198Eq*{$?bx$Ld5b7c0uLS`cgptBAC$w3>=s(#kC*&Vl)sR+Mvy(Q@Twv4Drf%5sj1%UU^KtW=tmwX{hN z44+rYnL*;h!DiX7$q_T z$^GP?lkeY60J)tE_a$_y8%&)A} z?*8Hmxj0p(y`E$5FzxfoD}}03k@C6n7n@b878EI;mmA@BmcZLNa%3_ZrIF~PD8)qe zX#Vx7$*FoRvXG5LQY(?j)A%ls_hH@b*pBzA#~jmO9yq=QE!%-2n7XCfW)By?SR?A2 zFH3o)*!bD>#E{&Nt_G*Vb3=0I;@e+Ju2Rh_l3Xf1NiJWJ%9ZjVxmYarC=z>w%sz>g z-kOe&vc!L4-a0jhN1e4a0x*MgW6+! zI-ldx`yO)_GnqsNGMQJXV~Tzc(!-_N?2C*vV6>L%}gc)mqu!#gdK46^zNJB4DvAZE2wgMV;W@%whs+0CZ=MJn9Rd^5sI>5n4$0k6 zsj%;^KdoRLpO`H+GH zS9ESzJdfmDF!U~iT{@zAP$zP+96WBaCD3fHfs4PAj zWW`~#&_dWR?6<@39#LZ!`l5v$b4Q$YOKbTCFv|NG{_6;0Tt`g zocuI4b$dA>P8oj@`H;Q>;>jva`!OUB8X@jpEVEG zF$hXScMrZt6Pcj5G#91}GM8~M)HpaKG zN2;&-Cf2lsGiXxJUVHKW*wR~`#68db(uxt%-AX}XC_I8!61P}5MX;n3~X$aiOf&%-J;Y-|f`as|BM zbcX(Qk_yQ>Ieo*VeXtmBsm49Z-Pyz&MdM~d%ji2gzmfpC^lWt0A|J7J+hbMO?4afF zMZu-RX~Di&5^PaS=HV6QLYMW2vt`&;TjW0sm;{R*x*1+VBSTBZ9`LKr-K9Nnbu<(z kH*zhmnvbsuW?i@Y{2G)p;on{*$EV8GoXr2$$b~}Vf0`Kdwg3PC delta 417 zcmZo@U~725IzgKCJOcxR_(TPJ#`7B!mdFdR@V@~HFo^U2=Kse3k^jwRMS&GeoA)Sb z^RlK@MkN_dexonJ!Or)HfuEc2(PjaI2EK_4!X{6!W7+)7j$3fD+I#*@0W1m3d|dpK z**C}nb-(7^%)g75X|m)lE#~yf+{yXxBsM4RSQzU;~D4?u$Me3Sj&32m-@*UHGT zi-DDwhmlWZGuHrNB=WnO-#wn%$t0{Uv%=Sf85fB zmLUcvR>tO5hUR)k1{P63A9LI5i;Fk1GHgEj&t8#@o$nt5-@omA3XIEu9`u{OMUPQs zyOKWR69tw&2IfAX16r7cX5_Q+hx+VcQB(Y&-91&jKb6R x#53}6@d8a}WY%P0)&y#kXBL_n!VcEf7CBjRx;9wbGdq>-YRQaEFm3saJOHpcfhYg~ 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, + }, +});