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: {