diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 62294b2..17ac7ec 100644 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/mobile/src/app/(tabs)/goals.tsx b/apps/mobile/src/app/(tabs)/goals.tsx index c6ebe43..b1cdf6e 100644 --- a/apps/mobile/src/app/(tabs)/goals.tsx +++ b/apps/mobile/src/app/(tabs)/goals.tsx @@ -258,60 +258,97 @@ export default function GoalsScreen() { )} {/* Analytics Section */} - {statistics && ( - - + {statistics && + (statistics.weeklyTrend.length > 0 || + statistics.goals.goalsByType.length > 0) && ( + setShowAnalytics(!showAnalytics)} - activeOpacity={0.7} + activeOpacity={0.85} > - - - - 📈 Progress Analytics - - - - + + + + + + + + + Progress Analytics + + + {showAnalytics ? "Tap to collapse" : "Tap to expand"} + + + + + + + - {showAnalytics && ( - - {statistics.weeklyTrend.length > 0 && ( - + {showAnalytics && ( + + {statistics.weeklyTrend.length > 0 && ( + + + Weekly Trend + + + + )} + {statistics.goals.goalsByType.length > 0 && ( + + + Goals by Type + + + + )} + )} - {statistics.goals.goalsByType.length > 0 && ( - - )} - - )} - - - )} + + + + )} {/* Active Goals */} @@ -439,6 +476,9 @@ const styles = StyleSheet.create({ paddingHorizontal: 12, borderRadius: 20, }, + analyticsCard: { + padding: 20, + }, analyticsHeader: { flexDirection: "row", justifyContent: "space-between", @@ -448,13 +488,32 @@ const styles = StyleSheet.create({ flexDirection: "row", alignItems: "center", }, + analyticsIcon: { + width: 48, + height: 48, + borderRadius: 14, + justifyContent: "center", + alignItems: "center", + marginRight: 14, + }, + analyticsToggle: { + width: 36, + height: 36, + borderRadius: 10, + justifyContent: "center", + alignItems: "center", + }, analyticsContent: { - paddingTop: 16, - marginTop: 16, + paddingTop: 24, + marginTop: 20, borderTopWidth: 1, + borderTopColor: "rgba(0,0,0,0.05)", + }, + chartSection: { + marginBottom: 20, }, goalsList: { - gap: 12, + gap: 16, }, emptyState: { alignItems: "center", diff --git a/apps/mobile/src/app/(tabs)/profile.tsx b/apps/mobile/src/app/(tabs)/profile.tsx index 2945f93..d6808a3 100644 --- a/apps/mobile/src/app/(tabs)/profile.tsx +++ b/apps/mobile/src/app/(tabs)/profile.tsx @@ -19,6 +19,7 @@ import { MinimalButton } from "../../components/MinimalButton"; import { Badge } from "../../components/Badge"; import { IconContainer } from "../../components/IconContainer"; import { API_BASE_URL, API_ENDPOINTS } from "../../config/api"; +import { fitnessProfileApi, FitnessProfile } from "../../api/fitnessProfile"; import log from "../../utils/logger"; export default function ProfileScreen() { @@ -35,6 +36,11 @@ export default function ProfileScreen() { const [selectedGymId, setSelectedGymId] = useState(null); const [currentGymId, setCurrentGymId] = useState(null); const [currentGymName, setCurrentGymName] = useState(null); + const [showGymDropdown, setShowGymDropdown] = useState(false); + const [fitnessProfile, setFitnessProfile] = useState( + null, + ); + const [profileLoading, setProfileLoading] = useState(false); useEffect(() => { const gid = @@ -49,8 +55,23 @@ export default function ProfileScreen() { useEffect(() => { loadGyms(); + loadFitnessProfile(); }, []); + const loadFitnessProfile = async () => { + try { + setProfileLoading(true); + const token = await getToken(); + if (!token) return; + const profile = await fitnessProfileApi.getFitnessProfile(token); + setFitnessProfile(profile); + } catch (error) { + log.error("Failed to load fitness profile", error); + } finally { + setProfileLoading(false); + } + }; + const loadGyms = async () => { try { setGymsLoading(true); @@ -115,8 +136,12 @@ export default function ProfileScreen() { const handleApplyGym = async () => { try { const token = await getToken(); - const url = `${API_BASE_URL}${API_ENDPOINTS.USERS}/gym`; - log.debug("Updating gym selection", { url, gymId: selectedGymId }); + const url = `${API_BASE_URL}${API_ENDPOINTS.USERS.GYM}`; + log.debug("Updating gym selection", { + url, + gymId: selectedGymId, + token: token ? "present" : "missing", + }); const res = await fetch(url, { method: "PATCH", headers: { @@ -331,29 +356,145 @@ export default function ProfileScreen() { onPress={() => router.push("/personal-details")} /> - - - - } - rightElement={ - - } + + {/* Fitness Profile Card */} + router.push("/fitness-profile")} - /> + activeOpacity={0.85} + > + + + + + + + + Fitness Profile + + + {fitnessProfile ? "Tap to edit" : "Set up your profile"} + + + + + + + + {profileLoading ? ( + + + + ) : fitnessProfile ? ( + + + + {fitnessProfile.height || "-"} + + + Height (cm) + + + + + + {fitnessProfile.weight || "-"} + + + Weight (kg) + + + + + + {fitnessProfile.age || "-"} + + + Age + + + + ) : ( + + + Complete your fitness profile to get personalized + recommendations + + router.push("/fitness-profile")} + /> + + )} + + + - - - Gym Selection - - - - Refresh - - - - - {currentGymName && ( - - - Current Gym - - + Gym Selection + + + setShowGymDropdown(!showGymDropdown)} + activeOpacity={0.85} + > + + + + + Current Gym + + + {currentGymName || "No gym selected"} + + + - {currentGymName} - + + - )} + + - {gymsLoading ? ( - - - - ) : ( - <> - + {showGymDropdown && ( + + {gymsLoading ? ( + + + + ) : ( + <> setSelectedGymId(null)} + onPress={() => { + setSelectedGymId(null); + setShowGymDropdown(false); + }} > No Gym + {selectedGymId === null && ( + + )} {gyms.map((gym) => ( setSelectedGymId(gym.id)} + onPress={() => { + setSelectedGymId(gym.id); + setShowGymDropdown(false); + }} > - - {gym.name} - + + + {gym.name} + + {gym.location && ( + + {gym.location} + + )} + + {selectedGymId === gym.id && ( + + )} ))} - - - - )} - + {selectedGymId !== currentGymId && ( + + )} + + )} + + )} {/* Support */} @@ -651,4 +825,78 @@ const styles = StyleSheet.create({ borderWidth: 1.5, marginRight: 8, }, + dropdownCard: { + padding: 16, + }, + dropdownHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + dropdownIcon: { + width: 36, + height: 36, + borderRadius: 10, + justifyContent: "center", + alignItems: "center", + }, + dropdownOptions: { + marginTop: 8, + padding: 8, + }, + dropdownOption: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingVertical: 14, + paddingHorizontal: 16, + borderRadius: 12, + marginBottom: 4, + }, + fitnessProfileCard: { + padding: 16, + }, + fitnessProfileHeader: { + flexDirection: "row", + alignItems: "center", + }, + fitnessProfileIcon: { + width: 48, + height: 48, + borderRadius: 14, + justifyContent: "center", + alignItems: "center", + }, + editButton: { + width: 32, + height: 32, + borderRadius: 8, + justifyContent: "center", + alignItems: "center", + }, + profileLoading: { + paddingVertical: 20, + alignItems: "center", + }, + fitnessProfileStats: { + flexDirection: "row", + justifyContent: "space-around", + marginTop: 20, + paddingTop: 16, + borderTopWidth: 1, + }, + profileStat: { + alignItems: "center", + flex: 1, + }, + profileStatDivider: { + width: 1, + height: 40, + }, + noProfile: { + marginTop: 16, + paddingTop: 16, + borderTopWidth: 1, + alignItems: "center", + }, }); diff --git a/apps/mobile/src/components/GoalTypeBreakdownChart.tsx b/apps/mobile/src/components/GoalTypeBreakdownChart.tsx index 22757d6..6d62a19 100644 --- a/apps/mobile/src/components/GoalTypeBreakdownChart.tsx +++ b/apps/mobile/src/components/GoalTypeBreakdownChart.tsx @@ -1,7 +1,7 @@ import React from "react"; import { View, Text, StyleSheet, Dimensions } from "react-native"; import { PieChart } from "react-native-chart-kit"; -import { theme } from "../styles/theme"; +import { useTheme } from "../contexts/ThemeContext"; interface GoalTypeData { goalType: string; @@ -17,37 +17,40 @@ export function GoalTypeBreakdownChart({ data, title = "Goals by Type", }: GoalTypeBreakdownChartProps) { + const { colors, typography } = useTheme(); const screenWidth = Dimensions.get("window").width; - // Color palette for different goal types - const colors = [ - "#3b82f6", // Blue - "#10b981", // Green - "#f59e0b", // Orange - "#8b5cf6", // Purple - "#ec4899", // Pink - "#06b6d4", // Cyan + const chartColors = [ + colors.primary, + colors.success, + colors.warning, + colors.accent, + colors.secondary, + colors.info, ]; - // Prepare chart data const chartData = data.map((item, index) => ({ - name: item.goalType, + name: item.goalType + .replace(/_/g, " ") + .replace(/\b\w/g, (l) => l.toUpperCase()), count: item.count, - color: colors[index % colors.length], - legendFontColor: theme.colors.gray600, + color: chartColors[index % chartColors.length], + legendFontColor: colors.textSecondary, legendFontSize: 12, })); - const chartConfig = { - color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`, - }; + const totalGoals = data.reduce((sum, item) => sum + item.count, 0); if (data.length === 0) { return ( - {title} + + {title} + - No goals yet + + No goals yet + ); @@ -55,46 +58,123 @@ export function GoalTypeBreakdownChart({ return ( - {title} + + {title} + + + {/* Summary Stats */} + + + + {totalGoals} + + + Total Goals + + + + + {data.length} + + + Types + + + + `rgba(0, 0, 0, ${opacity})`, + }} accessor="count" backgroundColor="transparent" paddingLeft="15" absolute /> + + {/* Legend */} + + {data.map((item, index) => ( + + + + {item.goalType + .replace(/_/g, " ") + .replace(/\b\w/g, (l) => l.toUpperCase())} + + + {item.count} + + + ))} + ); } const styles = StyleSheet.create({ container: { - backgroundColor: theme.colors.white, - borderRadius: theme.borderRadius.xl, - padding: 16, - marginBottom: 16, - ...theme.shadows.medium, + borderRadius: 16, + padding: 4, }, - title: { - fontSize: theme.typography.fontSize.lg, - fontWeight: theme.typography.fontWeight.bold, - color: theme.colors.gray700, - marginBottom: 12, + summaryRow: { + flexDirection: "row", + justifyContent: "space-around", + marginBottom: 16, + paddingVertical: 12, + }, + summaryItem: { + alignItems: "center", }, chartContainer: { alignItems: "center", + marginBottom: 16, + }, + legendGrid: { + gap: 10, + }, + legendItem: { + flexDirection: "row", + alignItems: "center", + gap: 10, + }, + legendDot: { + width: 12, + height: 12, + borderRadius: 6, }, emptyState: { - paddingVertical: 40, + paddingVertical: 32, alignItems: "center", }, - emptyText: { - fontSize: theme.typography.fontSize.sm, - color: theme.colors.gray400, - }, }); diff --git a/apps/mobile/src/components/WeeklyProgressChart.tsx b/apps/mobile/src/components/WeeklyProgressChart.tsx index 26e43c5..9d6a1a4 100644 --- a/apps/mobile/src/components/WeeklyProgressChart.tsx +++ b/apps/mobile/src/components/WeeklyProgressChart.tsx @@ -1,7 +1,7 @@ import React from "react"; import { View, Text, StyleSheet, Dimensions } from "react-native"; import { LineChart } from "react-native-chart-kit"; -import { theme } from "../styles/theme"; +import { useTheme } from "../contexts/ThemeContext"; import type { WeeklyTrendData } from "../api/types"; interface WeeklyProgressChartProps { @@ -13,28 +13,26 @@ export function WeeklyProgressChart({ weeklyData, title = "Weekly Progress", }: WeeklyProgressChartProps) { + const { colors, typography } = useTheme(); const screenWidth = Dimensions.get("window").width; - // Prepare chart data - const labels = weeklyData.map((week) => week.weekLabel); - const checkInsData = weeklyData.map((week) => week.checkIns); - const goalsCompletedData = weeklyData.map((week) => week.goalsCompleted); - const avgProgressData = weeklyData.map((week) => week.averageProgress); + const labels = weeklyData.map((week: WeeklyTrendData) => week.weekLabel); + const checkInsData = weeklyData.map((week: WeeklyTrendData) => week.checkIns); + const goalsCompletedData = weeklyData.map( + (week: WeeklyTrendData) => week.goalsCompleted, + ); const chartConfig = { - backgroundColor: theme.colors.white, - backgroundGradientFrom: theme.colors.white, - backgroundGradientTo: theme.colors.white, + backgroundColor: colors.surface, + backgroundGradientFrom: colors.surface, + backgroundGradientTo: colors.surface, decimalPlaces: 0, - color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`, - labelColor: (opacity = 1) => `rgba(107, 114, 128, ${opacity})`, - style: { - borderRadius: theme.borderRadius.lg, - }, + color: (opacity = 1) => `rgba(0, 102, 255, ${opacity})`, + labelColor: (opacity = 1) => colors.textTertiary, propsForDots: { - r: "4", + r: "5", strokeWidth: "2", - stroke: theme.colors.primary, + stroke: colors.primary, }, }; @@ -43,31 +41,40 @@ export function WeeklyProgressChart({ datasets: [ { data: checkInsData, - color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`, // Blue for check-ins - strokeWidth: 2, + color: () => colors.primary, + strokeWidth: 3, }, { data: goalsCompletedData, - color: (opacity = 1) => `rgba(16, 185, 129, ${opacity})`, // Green for goals - strokeWidth: 2, + color: () => colors.success, + strokeWidth: 3, }, ], - legend: ["Check-ins", "Goals Completed"], + legend: ["Check-ins", "Goals"], }; return ( - {title} + {title && ( + + {title} + + )} - - Check-ins + + + Check-ins + - - Goals Completed + + + Goals + @@ -89,17 +104,8 @@ export function WeeklyProgressChart({ const styles = StyleSheet.create({ container: { - backgroundColor: theme.colors.white, - borderRadius: theme.borderRadius.xl, - padding: 16, - marginBottom: 16, - ...theme.shadows.medium, - }, - title: { - fontSize: theme.typography.fontSize.lg, - fontWeight: theme.typography.fontWeight.bold, - color: theme.colors.gray700, - marginBottom: 12, + borderRadius: 16, + padding: 4, }, chartContainer: { alignItems: "center", @@ -107,26 +113,22 @@ const styles = StyleSheet.create({ }, chart: { marginVertical: 8, - borderRadius: theme.borderRadius.lg, + borderRadius: 12, }, legend: { flexDirection: "row", justifyContent: "center", - gap: 20, + gap: 24, paddingTop: 8, }, legendItem: { flexDirection: "row", alignItems: "center", - gap: 6, + gap: 8, }, legendDot: { width: 10, height: 10, borderRadius: 5, }, - legendText: { - fontSize: theme.typography.fontSize.sm, - color: theme.colors.gray500, - }, }); diff --git a/apps/mobile/src/config/api.ts b/apps/mobile/src/config/api.ts index 08c3d3a..563dbba 100644 --- a/apps/mobile/src/config/api.ts +++ b/apps/mobile/src/config/api.ts @@ -35,6 +35,7 @@ export const API_ENDPOINTS = { USERS: { LIST: "/api/users", STATISTICS: "/api/users/statistics", + GYM: "/api/users/gym", }, GYMS: "/api/gyms", ATTENDANCE: {