diff --git a/apps/admin/src/lib/database/sqlite.ts b/apps/admin/src/lib/database/sqlite.ts
index f7ed088..d0b15ba 100644
--- a/apps/admin/src/lib/database/sqlite.ts
+++ b/apps/admin/src/lib/database/sqlite.ts
@@ -1,7 +1,7 @@
import Database from 'better-sqlite3'
import path from 'path'
import fs from 'fs'
-import { IDatabase, User, Client, FitnessProfile, Attendance, DatabaseConfig } from './types'
+import { IDatabase, User, Client, FitnessProfile, Attendance, Recommendation, DatabaseConfig } from './types'
export class SQLiteDatabase implements IDatabase {
private db: Database.Database | null = null
@@ -120,6 +120,24 @@ export class SQLiteDatabase implements IDatabase {
)
`)
+ // Recommendations table
+ this.db.exec(`
+ CREATE TABLE IF NOT EXISTS recommendations (
+ id TEXT PRIMARY KEY,
+ userId TEXT NOT NULL,
+ type TEXT NOT NULL CHECK (type IN ('short_term', 'medium_term', 'long_term')),
+ content TEXT NOT NULL,
+ status TEXT NOT NULL CHECK (status IN ('pending', 'completed')),
+ createdAt DATETIME NOT NULL,
+ updatedAt DATETIME NOT NULL,
+ FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE
+ )
+ `)
+
+ this.db.exec(`
+ CREATE INDEX IF NOT EXISTS idx_recommendations_userId ON recommendations(userId);
+ `)
+
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_attendance_clientId ON attendance(clientId);
CREATE INDEX IF NOT EXISTS idx_attendance_checkInTime ON attendance(checkInTime);
@@ -141,8 +159,8 @@ export class SQLiteDatabase implements IDatabase {
}
const stmt = this.db.prepare(
- `INSERT INTO users (id, email, firstName, lastName, password, phone, role, createdAt, updatedAt)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
+ `INSERT INTO users(id, email, firstName, lastName, password, phone, role, createdAt, updatedAt)
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
stmt.run(
@@ -191,7 +209,7 @@ export class SQLiteDatabase implements IDatabase {
values.push(new Date().toISOString()) // updatedAt
values.push(id)
- const stmt = this.db.prepare(`UPDATE users SET ${setClause}, updatedAt = ? WHERE id = ?`)
+ const stmt = this.db.prepare(`UPDATE users SET ${setClause}, updatedAt = ? WHERE id = ? `)
stmt.run(values)
return this.getUserById(id)
@@ -227,8 +245,8 @@ export class SQLiteDatabase implements IDatabase {
const client: Client = { id, ...clientData }
const stmt = this.db.prepare(
- `INSERT INTO clients (id, userId, membershipType, membershipStatus, joinDate)
- VALUES (?, ?, ?, ?, ?)`
+ `INSERT INTO clients(id, userId, membershipType, membershipStatus, joinDate)
+ VALUES(?, ?, ?, ?, ?)`
)
stmt.run(
@@ -273,7 +291,7 @@ export class SQLiteDatabase implements IDatabase {
const values = fields.map(field => (updates as any)[field])
values.push(id)
- const stmt = this.db.prepare(`UPDATE clients SET ${setClause} WHERE id = ?`)
+ const stmt = this.db.prepare(`UPDATE clients SET ${setClause} WHERE id = ? `)
stmt.run(values)
return this.getClientById(id)
@@ -299,10 +317,10 @@ export class SQLiteDatabase implements IDatabase {
}
const stmt = this.db.prepare(
- `INSERT INTO fitness_profiles
- (userId, height, weight, age, gender, activityLevel, fitnessGoals,
+ `INSERT INTO fitness_profiles
+ (userId, height, weight, age, gender, activityLevel, fitnessGoals,
exerciseHabits, dietHabits, medicalConditions, allergies, injuries, createdAt, updatedAt)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
stmt.run(
@@ -346,7 +364,7 @@ export class SQLiteDatabase implements IDatabase {
values.push(new Date().toISOString()) // updatedAt
values.push(userId)
- const stmt = this.db.prepare(`UPDATE fitness_profiles SET ${setClause}, updatedAt = ? WHERE userId = ?`)
+ const stmt = this.db.prepare(`UPDATE fitness_profiles SET ${setClause}, updatedAt = ? WHERE userId = ? `)
stmt.run(values)
return this.getFitnessProfileByUserId(userId)
@@ -377,8 +395,8 @@ export class SQLiteDatabase implements IDatabase {
}
const stmt = this.db.prepare(
- `INSERT INTO attendance (id, clientId, checkInTime, type, notes, createdAt)
- VALUES (?, ?, ?, ?, ?, ?)`
+ `INSERT INTO attendance(id, clientId, checkInTime, type, notes, createdAt)
+ VALUES(?, ?, ?, ?, ?, ?)`
)
stmt.run(
@@ -490,6 +508,83 @@ export class SQLiteDatabase implements IDatabase {
}
}
+ // Recommendation operations
+ async createRecommendation(data: Omit
): Promise {
+ if (!this.db) throw new Error('Database not connected')
+
+ const now = new Date()
+ const recommendation: Recommendation = {
+ ...data,
+ createdAt: now,
+ updatedAt: now
+ }
+
+ const stmt = this.db.prepare(
+ `INSERT INTO recommendations (id, userId, type, content, status, createdAt, updatedAt)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
+ )
+
+ stmt.run(
+ recommendation.id, recommendation.userId, recommendation.type,
+ recommendation.content, recommendation.status,
+ recommendation.createdAt.toISOString(), recommendation.updatedAt.toISOString()
+ )
+
+ return recommendation
+ }
+
+ async getRecommendationsByUserId(userId: string): Promise {
+ if (!this.db) throw new Error('Database not connected')
+
+ const stmt = this.db.prepare('SELECT * FROM recommendations WHERE userId = ? ORDER BY createdAt DESC')
+ const rows = stmt.all(userId)
+
+ return rows.map(row => this.mapRowToRecommendation(row))
+ }
+
+ async updateRecommendation(id: string, updates: Partial): Promise {
+ if (!this.db) throw new Error('Database not connected')
+
+ const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'userId')
+ if (fields.length === 0) {
+ const stmt = this.db.prepare('SELECT * FROM recommendations WHERE id = ?')
+ const row = stmt.get(id)
+ return row ? this.mapRowToRecommendation(row) : null
+ }
+
+ const setClause = fields.map(field => `${field} = ?`).join(', ')
+ const values = fields.map(field => (updates as any)[field])
+ values.push(new Date().toISOString()) // updatedAt
+ values.push(id)
+
+ const stmt = this.db.prepare(`UPDATE recommendations SET ${setClause}, updatedAt = ? WHERE id = ?`)
+ stmt.run(values)
+
+ const getStmt = this.db.prepare('SELECT * FROM recommendations WHERE id = ?')
+ const row = getStmt.get(id)
+ return row ? this.mapRowToRecommendation(row) : null
+ }
+
+ async deleteRecommendation(id: string): Promise {
+ if (!this.db) throw new Error('Database not connected')
+
+ const stmt = this.db.prepare('DELETE FROM recommendations WHERE id = ?')
+ const result = stmt.run(id)
+ return (result.changes || 0) > 0
+ }
+
+ private mapRowToRecommendation(row: any): Recommendation {
+ return {
+ id: row.id,
+ userId: row.userId,
+ type: row.type,
+ content: row.content,
+ status: row.status,
+ createdAt: new Date(row.createdAt),
+ updatedAt: new Date(row.updatedAt)
+ }
+ }
+
async getDashboardStats(): Promise<{
totalUsers: number;
activeClients: number;
diff --git a/apps/admin/src/lib/database/types.ts b/apps/admin/src/lib/database/types.ts
index 4c66185..5d05532 100644
--- a/apps/admin/src/lib/database/types.ts
+++ b/apps/admin/src/lib/database/types.ts
@@ -47,6 +47,16 @@ export interface Attendance {
createdAt: Date;
}
+export interface Recommendation {
+ id: string;
+ userId: string;
+ type: "short_term" | "medium_term" | "long_term";
+ content: string;
+ status: "pending" | "completed";
+ createdAt: Date;
+ updatedAt: Date;
+}
+
// Database Interface - allows us to swap implementations
export interface IDatabase {
// Connection management
@@ -93,6 +103,17 @@ export interface IDatabase {
getAllAttendance(): Promise;
getActiveCheckIn(clientId: string): Promise;
+ // Recommendation operations
+ createRecommendation(
+ recommendation: Omit,
+ ): Promise;
+ getRecommendationsByUserId(userId: string): Promise;
+ updateRecommendation(
+ id: string,
+ updates: Partial,
+ ): Promise;
+ deleteRecommendation(id: string): Promise;
+
// Dashboard operations
getDashboardStats(): Promise<{
totalUsers: number;
diff --git a/apps/mobile/src/app/(tabs)/_layout.tsx b/apps/mobile/src/app/(tabs)/_layout.tsx
index aed5aa7..4c9aa08 100644
--- a/apps/mobile/src/app/(tabs)/_layout.tsx
+++ b/apps/mobile/src/app/(tabs)/_layout.tsx
@@ -67,6 +67,16 @@ export default function TabLayout() {
),
}}
/>
+ (
+
+ ),
+ }}
+ />
);
}
diff --git a/apps/mobile/src/app/(tabs)/goals.tsx b/apps/mobile/src/app/(tabs)/goals.tsx
new file mode 100644
index 0000000..d50737b
--- /dev/null
+++ b/apps/mobile/src/app/(tabs)/goals.tsx
@@ -0,0 +1,272 @@
+import { useState, useEffect } from "react";
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ ActivityIndicator,
+ RefreshControl,
+ Alert,
+} from "react-native";
+import { useAuth } from "@clerk/clerk-expo";
+import { Ionicons } from "@expo/vector-icons";
+import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
+
+interface Recommendation {
+ id: string;
+ userId: string;
+ type: "short_term" | "medium_term" | "long_term";
+ content: string;
+ status: "pending" | "completed";
+ createdAt: string;
+}
+
+export default function GoalsScreen() {
+ const { userId, getToken } = useAuth();
+ const [recommendations, setRecommendations] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
+
+ const fetchRecommendations = async () => {
+ if (!userId) return;
+ try {
+ const token = await getToken();
+ const response = await fetch(
+ `${API_BASE_URL}${API_ENDPOINTS.RECOMMENDATIONS}?userId=${userId}`,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ setRecommendations(data);
+ } else {
+ console.error("Failed to fetch recommendations");
+ }
+ } catch (error) {
+ console.error("Error fetching recommendations:", error);
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchRecommendations();
+ }, [userId]);
+
+ const onRefresh = () => {
+ setRefreshing(true);
+ fetchRecommendations();
+ };
+
+ const toggleStatus = async (id: string, currentStatus: string) => {
+ const newStatus = currentStatus === "pending" ? "completed" : "pending";
+
+ // Optimistic update
+ setRecommendations(prev =>
+ prev.map(r => r.id === id ? { ...r, status: newStatus } : r)
+ );
+
+ try {
+ const token = await getToken();
+ const response = await fetch(`${API_BASE_URL}${API_ENDPOINTS.RECOMMENDATIONS}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ id,
+ status: newStatus,
+ }),
+ });
+
+ if (!response.ok) {
+ // Revert on failure
+ setRecommendations(prev =>
+ prev.map(r => r.id === id ? { ...r, status: currentStatus as "pending" | "completed" } : r)
+ );
+ Alert.alert("Error", "Failed to update status");
+ }
+ } catch (error) {
+ console.error(error);
+ // Revert on error
+ setRecommendations(prev =>
+ prev.map(r => r.id === id ? { ...r, status: currentStatus as "pending" | "completed" } : r)
+ );
+ Alert.alert("Error", "Failed to update status");
+ }
+ };
+
+ const renderSection = (
+ title: string,
+ type: "short_term" | "medium_term" | "long_term"
+ ) => {
+ const items = recommendations.filter((r) => r.type === type);
+
+ return (
+
+ {title}
+ {items.length === 0 ? (
+ No goals set yet.
+ ) : (
+ items.map((rec) => (
+ toggleStatus(rec.id, rec.status)}
+ >
+
+ {rec.status === "completed" && (
+
+ )}
+
+
+
+ {rec.content}
+
+
+ {new Date(rec.createdAt).toLocaleDateString()}
+
+
+
+ ))
+ )}
+
+ );
+ };
+
+ if (loading && !refreshing) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ }
+ >
+
+ My Goals
+
+ Track your fitness journey progress
+
+
+
+ {renderSection("Short Term Goals", "short_term")}
+ {renderSection("Medium Term Goals", "medium_term")}
+ {renderSection("Long Term Goals", "long_term")}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: "#f3f4f6",
+ },
+ center: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ header: {
+ padding: 20,
+ backgroundColor: "#fff",
+ marginBottom: 10,
+ },
+ headerTitle: {
+ fontSize: 28,
+ fontWeight: "bold",
+ color: "#111827",
+ },
+ headerSubtitle: {
+ fontSize: 16,
+ color: "#6b7280",
+ marginTop: 4,
+ },
+ section: {
+ padding: 20,
+ paddingTop: 10,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: "600",
+ color: "#374151",
+ marginBottom: 12,
+ },
+ emptyText: {
+ fontStyle: "italic",
+ color: "#9ca3af",
+ },
+ card: {
+ backgroundColor: "#fff",
+ padding: 16,
+ borderRadius: 12,
+ marginBottom: 12,
+ flexDirection: "row",
+ alignItems: "flex-start",
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.05,
+ shadowRadius: 2,
+ elevation: 2,
+ },
+ cardCompleted: {
+ backgroundColor: "#f0fdf4", // light green
+ borderColor: "#bbf7d0",
+ borderWidth: 1,
+ },
+ checkbox: {
+ width: 24,
+ height: 24,
+ borderRadius: 6,
+ borderWidth: 2,
+ borderColor: "#d1d5db",
+ marginRight: 12,
+ marginTop: 2,
+ justifyContent: "center",
+ alignItems: "center",
+ backgroundColor: "#fff",
+ },
+ cardContent: {
+ flex: 1,
+ },
+ cardText: {
+ fontSize: 16,
+ color: "#1f2937",
+ lineHeight: 24,
+ },
+ cardTextCompleted: {
+ textDecorationLine: "line-through",
+ color: "#9ca3af",
+ },
+ dateText: {
+ fontSize: 12,
+ color: "#9ca3af",
+ marginTop: 4,
+ },
+ footer: {
+ height: 40,
+ },
+});
diff --git a/apps/mobile/src/config/api.ts b/apps/mobile/src/config/api.ts
index 646a78f..7085cac 100644
--- a/apps/mobile/src/config/api.ts
+++ b/apps/mobile/src/config/api.ts
@@ -17,4 +17,5 @@ export const API_ENDPOINTS = {
CHECK_OUT: '/api/attendance/check-out',
HISTORY: '/api/attendance/history',
},
+ RECOMMENDATIONS: '/api/recommendations',
}
\ No newline at end of file