diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 84fa70b..cfa8659 100755 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/admin/src/app/api/activity/daily/route.ts b/apps/admin/src/app/api/activity/daily/route.ts new file mode 100644 index 0000000..07989a5 --- /dev/null +++ b/apps/admin/src/app/api/activity/daily/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; + +export async function GET(req: NextRequest) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(req.url); + const date = searchParams.get("date") || new Date().toISOString().split('T')[0]; + + // TODO: Fetch from database when activity_logs methods are added to IDatabase + console.log(`Fetching daily activity for user ${userId} on ${date}`); + + // Return mock data for now + return NextResponse.json({ + steps: 0, + calories: 0, + duration: 0, + date + }); + } catch (error) { + console.error("Error fetching daily activity:", error); + return NextResponse.json( + { error: "Failed to fetch daily activity" }, + { status: 500 } + ); + } +} diff --git a/apps/admin/src/app/api/activity/steps/route.ts b/apps/admin/src/app/api/activity/steps/route.ts new file mode 100644 index 0000000..6350555 --- /dev/null +++ b/apps/admin/src/app/api/activity/steps/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; + +export async function POST(req: NextRequest) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { steps, timestamp } = await req.json(); + + // TODO: Implement database persistence when activity_logs methods are added to IDatabase + console.log(`Syncing steps for user ${userId}: ${steps} steps at ${timestamp}`); + + return NextResponse.json({ success: true, steps }); + } catch (error) { + console.error("Error syncing steps:", error); + return NextResponse.json( + { error: "Failed to sync steps" }, + { status: 500 } + ); + } +} diff --git a/apps/admin/src/app/api/fitness-goals/recent/route.ts b/apps/admin/src/app/api/fitness-goals/recent/route.ts new file mode 100644 index 0000000..f7e6200 --- /dev/null +++ b/apps/admin/src/app/api/fitness-goals/recent/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { getDatabase } from "@/lib/database"; + +export async function GET(req: NextRequest) { + try { + const { userId } = await auth(); + + // Return empty array if not authenticated (for testing) + if (!userId) { + return NextResponse.json([]); + } + + const db = await getDatabase(); + + // Fetch recent fitness goals (last 5) + const recentGoals = await db.getFitnessGoalsByUserId(userId); + + // Take only the 5 most recent and transform to format expected by mobile app + const activities = recentGoals.slice(0, 5).map((goal) => ({ + title: goal.title, + subtitle: new Date(goal.createdAt).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }), + duration: 45, // Mock duration for now + icon: goal.goalType === 'strength_milestone' ? 'barbell' : + goal.goalType === 'endurance_target' ? 'bicycle' : + 'fitness', + gradient: goal.goalType === 'strength_milestone' ? 'primary' : + goal.goalType === 'endurance_target' ? 'success' : + 'purple', + })); + + return NextResponse.json(activities); + } catch (error) { + console.error("Error fetching recent activities:", error); + return NextResponse.json([], { status: 200 }); // Return empty array on error + } +} diff --git a/apps/admin/src/app/api/profile/fitness/route.ts b/apps/admin/src/app/api/profile/fitness/route.ts index 279170c..78a0d20 100644 --- a/apps/admin/src/app/api/profile/fitness/route.ts +++ b/apps/admin/src/app/api/profile/fitness/route.ts @@ -24,7 +24,7 @@ export async function POST(request: NextRequest) { // Check if profile already exists const existingProfile = await db.getFitnessProfileByUserId(profileData.userId) - + let profile if (existingProfile) { profile = await db.updateFitnessProfile(profileData.userId, profileData) @@ -33,7 +33,7 @@ export async function POST(request: NextRequest) { } return NextResponse.json( - { + { message: 'Fitness profile saved successfully', profile }, @@ -50,11 +50,12 @@ export async function POST(request: NextRequest) { export async function GET(request: NextRequest) { try { - const db = await getDatabase() const { searchParams } = new URL(request.url) const userId = searchParams.get('userId') + // If userId is provided, get specific profile if (userId) { + const db = await getDatabase() const profile = await db.getFitnessProfileByUserId(userId) if (!profile) { return NextResponse.json( @@ -62,11 +63,24 @@ export async function GET(request: NextRequest) { { status: 404 } ) } - return NextResponse.json({ profile }) + + // Return profile with mock activity data for now + // TODO: Fetch real activity data from activity_logs table + return NextResponse.json({ + profile, + steps: 0, + calories: 0, + duration: 0 + }) } - const profiles = await db.getAllFitnessProfiles() - return NextResponse.json({ profiles }) + // For mobile app: return activity data without userId param + // This is a simplified endpoint for the mobile ActivityWidget + return NextResponse.json({ + steps: 0, + calories: 0, + duration: 0 + }) } catch (error) { console.error('Get fitness profiles error:', error) return NextResponse.json( diff --git a/apps/mobile/app.json b/apps/mobile/app.json index 97a9138..c7ad20e 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -14,13 +14,20 @@ "assetBundlePatterns": [ "**/*" ], - "ios": { - "supportsTablet": true - }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" + }, + "permissions": [ + "android.permission.ACTIVITY_RECOGNITION" + ], + "package": "com.anonymous.fitai" + }, + "ios": { + "supportsTablet": true, + "infoPlist": { + "NSMotionUsageDescription": "This app uses the pedometer to track your steps." } }, "web": { @@ -28,8 +35,27 @@ }, "plugins": [ "expo-router", - "expo-font" + "expo-font", + [ + "react-native-health-connect", + { + "permissions": [ + { + "accessType": "read", + "recordType": "Steps" + }, + { + "accessType": "read", + "recordType": "TotalCaloriesBurned" + }, + { + "accessType": "read", + "recordType": "ExerciseSession" + } + ] + } + ] ], "scheme": "fitai" } -} +} \ No newline at end of file diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index 320fb1d..0364651 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -22,6 +22,7 @@ "expo-camera": "~17.0.0", "expo-constants": "^18.0.10", "expo-crypto": "^15.0.7", + "expo-dev-client": "^6.0.18", "expo-font": "~14.0.9", "expo-haptics": "^15.0.7", "expo-linear-gradient": "~15.0.7", @@ -29,12 +30,14 @@ "expo-notifications": "~0.32.0", "expo-router": "~6.0.14", "expo-secure-store": "~15.0.7", + "expo-sensors": "~15.0.7", "expo-status-bar": "^3.0.8", "expo-web-browser": "^15.0.9", "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.47.0", "react-native": "0.81.5", + "react-native-health-connect": "^3.5.0", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "^15.15.0", @@ -7236,6 +7239,56 @@ "expo": "*" } }, + "node_modules/expo-dev-client": { + "version": "6.0.18", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.18.tgz", + "integrity": "sha512-8QKWvhsoZpMkecAMlmWoRHnaTNiPS3aO7E42spZOMjyiaNRJMHZsnB8W2b63dt3Yg3oLyskLAoI8IOmnqVX8vA==", + "license": "MIT", + "dependencies": { + "expo-dev-launcher": "6.0.18", + "expo-dev-menu": "7.0.17", + "expo-dev-menu-interface": "2.0.0", + "expo-manifests": "~1.0.9", + "expo-updates-interface": "~2.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher": { + "version": "6.0.18", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.18.tgz", + "integrity": "sha512-JTtcIfNvHO9PTdRJLmHs+7HJILXXZjF95jxgzu6hsJrgsTg/AZDtEsIt/qa6ctEYQTqrLdsLDgDhiXVel3AoQA==", + "license": "MIT", + "dependencies": { + "expo-dev-menu": "7.0.17", + "expo-manifests": "~1.0.9" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.17.tgz", + "integrity": "sha512-NIu7TdaZf+A8+DROa6BB6lDfxjXxwaD+Q8QbNSVa0E0x6yl3P0ZJ80QbD2cCQeBzlx3Ufd3hNhczQWk4+A29HQ==", + "license": "MIT", + "dependencies": { + "expo-dev-menu-interface": "2.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz", + "integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-file-system": { "version": "19.0.19", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.19.tgz", @@ -7269,6 +7322,12 @@ "expo": "*" } }, + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "license": "MIT" + }, "node_modules/expo-linear-gradient": { "version": "15.0.7", "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-15.0.7.tgz", @@ -7294,6 +7353,19 @@ "react-native": "*" } }, + "node_modules/expo-manifests": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.9.tgz", + "integrity": "sha512-5uVgvIo0o+xBcEJiYn4uVh72QSIqyHePbYTWXYa4QamXd+AmGY/yWmtHaNqCqjsPLCwXyn4OxPr7jXJCeTWLow==", + "license": "MIT", + "dependencies": { + "@expo/config": "~12.0.10", + "expo-json-utils": "~0.15.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "3.0.22", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.22.tgz", @@ -7435,6 +7507,19 @@ "expo": "*" } }, + "node_modules/expo-sensors": { + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/expo-sensors/-/expo-sensors-15.0.7.tgz", + "integrity": "sha512-TGUxRx/Ss7KGgfWo453YF64ENucw6oYryPiu/8I3ZZuf114xQPRxAbsZohPLaVUUGuaUyWbDsb0eRsmuKUzBnQ==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-server": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.4.tgz", @@ -7457,6 +7542,15 @@ "react-native": "*" } }, + "node_modules/expo-updates-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz", + "integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-web-browser": { "version": "15.0.9", "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.9.tgz", @@ -11613,6 +11707,27 @@ } } }, + "node_modules/react-native-health-connect": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/react-native-health-connect/-/react-native-health-connect-3.5.0.tgz", + "integrity": "sha512-lIfiRps+jjDs83SApQ3AzBzU2cITwn8nPRl3v+52imWJPVw7qltAXkduioOfqFqOQJuEOwd1Wei3qtMTYbTaag==", + "license": "MIT", + "workspaces": [ + "example", + "docs" + ], + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "url": "https://github.com/matinzd/react-native-health-connect?sponsor=1" + }, + "peerDependencies": { + "@expo/config-plugins": ">= 6.0.2", + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-is-edge-to-edge": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 8f5c5d5..11a7e87 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -5,8 +5,8 @@ "private": true, "scripts": { "start": "expo start", - "android": "expo start --android", - "ios": "expo start --ios", + "android": "expo run:android", + "ios": "expo run:ios", "web": "expo start --web", "build": "expo build", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", @@ -28,6 +28,7 @@ "expo-camera": "~17.0.0", "expo-constants": "^18.0.10", "expo-crypto": "^15.0.7", + "expo-dev-client": "^6.0.18", "expo-font": "~14.0.9", "expo-haptics": "^15.0.7", "expo-linear-gradient": "~15.0.7", @@ -35,12 +36,14 @@ "expo-notifications": "~0.32.0", "expo-router": "~6.0.14", "expo-secure-store": "~15.0.7", + "expo-sensors": "~15.0.7", "expo-status-bar": "^3.0.8", "expo-web-browser": "^15.0.9", "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.47.0", "react-native": "0.81.5", + "react-native-health-connect": "^3.5.0", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "^15.15.0", diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 9cb174d..a8f3aa8 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -1,22 +1,43 @@ import { View, Text, StyleSheet, ScrollView, RefreshControl, Image } from "react-native"; -import { useUser } from "@clerk/clerk-expo"; +import { useUser, useAuth } from "@clerk/clerk-expo"; import { LinearGradient } from "expo-linear-gradient"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useEffect } from "react"; import { theme } from "../../styles/theme"; import { ActivityWidget } from "../../components/ActivityWidget"; import { QuickActionGrid } from "../../components/QuickActionGrid"; import { Ionicons } from "@expo/vector-icons"; +import { apiClient } from "../../services/apiService"; +import { useStepCount } from "../../hooks/useStepCount"; export default function HomeScreen() { const { user } = useUser(); + const { getToken } = useAuth(); const [refreshing, setRefreshing] = useState(false); + const [profile, setProfile] = useState<{ steps: number; calories: number; duration: number } | null>(null); + const [recentActivities, setRecentActivities] = useState>([]); + const { steps: deviceSteps, available } = useStepCount(); + + const loadData = useCallback(async () => { + try { + const token = await getToken(); + const fitness = await apiClient.getFitnessProfile(token); + setProfile({ steps: fitness.steps, calories: fitness.calories, duration: fitness.duration }); + const recent = await apiClient.getRecentActivities(token); + setRecentActivities(recent); + } catch (e) { + console.error(e); + } + }, [getToken]); + + // Load data on component mount + useEffect(() => { + loadData(); + }, []); const onRefresh = useCallback(() => { setRefreshing(true); - setTimeout(() => { - setRefreshing(false); - }, 2000); - }, []); + loadData().finally(() => setRefreshing(false)); + }, [loadData]); const getGreeting = () => { const hour = new Date().getHours(); @@ -25,6 +46,16 @@ export default function HomeScreen() { return "Good Evening"; }; + // Use device steps if available, otherwise use profile steps + const displayedSteps = available ? deviceSteps : profile?.steps ?? 0; + + console.log('HomeScreen Debug:', { + available, + deviceSteps, + profileSteps: profile?.steps, + displayedSteps + }); + return ( {/* Activity Widget */} - + {profile && ( + + )} {/* Quick Actions */} @@ -67,45 +96,22 @@ export default function HomeScreen() { See All - - - - - - - - - Upper Body Power - Today, 10:00 AM - - 45m - - - - - - - - - - Morning Cardio - Yesterday, 7:30 AM - - 30m - - + {recentActivities.map((act: any, idx: number) => ( + + + + + + + + + {act.title} + {act.subtitle} + + {act.duration}m + + + ))} {/* Bottom Spacer for Tab Bar */} diff --git a/apps/mobile/src/config/api.ts b/apps/mobile/src/config/api.ts index bff40aa..2ffb7f1 100644 --- a/apps/mobile/src/config/api.ts +++ b/apps/mobile/src/config/api.ts @@ -1,5 +1,5 @@ export const API_BASE_URL = __DEV__ - ? 'https://cd1b5e914b16.ngrok-free.app' + ? 'https://ef85a57e6911.ngrok-free.app' : 'https://your-production-url.com' export const API_ENDPOINTS = { diff --git a/apps/mobile/src/hooks/useStepCount.ts b/apps/mobile/src/hooks/useStepCount.ts new file mode 100644 index 0000000..ed25381 --- /dev/null +++ b/apps/mobile/src/hooks/useStepCount.ts @@ -0,0 +1,152 @@ +import { useEffect, useState, useRef } from "react"; +import { Pedometer } from "expo-sensors"; +import { Platform } from "react-native"; +import { initialize, requestPermission, readRecords } from "react-native-health-connect"; +import { apiClient } from "../services/apiService"; +import { useAuth } from "@clerk/clerk-expo"; + +export const useStepCount = () => { + const [steps, setSteps] = useState(0); + const [available, setAvailable] = useState(null); + const { getToken } = useAuth(); + const stepsRef = useRef(0); + + useEffect(() => { + let subscription: any = null; + let syncInterval: NodeJS.Timeout | null = null; + + const initPedometer = async () => { + if (Platform.OS === 'android') { + try { + // Initialize Health Connect + const isInitialized = await initialize(); + if (!isInitialized) { + console.log('Health Connect not initialized'); + setAvailable(false); + return; + } + + // Request permissions + const permissions = [ + { accessType: 'read', recordType: 'Steps' }, + ] as any[]; + + const granted = await requestPermission(permissions); + + // Check if permissions granted (simplified check) + if (granted) { + setAvailable(true); + + // Function to fetch steps + const fetchSteps = async () => { + const end = new Date(); + const start = new Date(); + start.setHours(0, 0, 0, 0); + + const result = await readRecords('Steps', { + timeRangeFilter: { + operator: 'between', + startTime: start.toISOString(), + endTime: end.toISOString(), + }, + }); + + const totalSteps = result.records.reduce((sum: number, record: any) => sum + record.count, 0); + console.log('Health Connect steps:', totalSteps); + setSteps(totalSteps); + stepsRef.current = totalSteps; + }; + + // Initial fetch + await fetchSteps(); + + // Poll for updates on Android (Health Connect doesn't have a real-time listener like Pedometer) + // We'll reuse the sync interval for this or create a separate one + syncInterval = setInterval(async () => { + await fetchSteps(); + + // Sync to backend + const currentSteps = stepsRef.current; + if (currentSteps > 0) { + try { + const token = await getToken(); + await apiClient.syncSteps(currentSteps, token); + console.log('Synced steps:', currentSteps); + } catch (error) { + console.error('Failed to sync steps:', error); + } + } + }, 5000); // Check every 5 seconds for UI updates + } else { + console.log('Health Connect permissions denied'); + setAvailable(false); + } + } catch (error) { + console.error('Error initializing Health Connect:', error); + setAvailable(false); + } + } else { + // iOS Implementation (Pedometer) + try { + const perm = await Pedometer.requestPermissionsAsync(); + if (perm.status === 'denied') { + setAvailable(false); + return; + } + + const isAvailable = await Pedometer.isAvailableAsync(); + setAvailable(isAvailable); + + if (isAvailable) { + const end = new Date(); + const start = new Date(); + start.setHours(0, 0, 0, 0); + + const pastStepCount = await Pedometer.getStepCountAsync(start, end); + if (pastStepCount) { + setSteps(pastStepCount.steps); + stepsRef.current = pastStepCount.steps; + } + + subscription = Pedometer.watchStepCount(result => { + const newSteps = stepsRef.current + result.steps; + setSteps(newSteps); + }); + + syncInterval = setInterval(async () => { + const currentSteps = stepsRef.current; + if (currentSteps > 0) { + try { + const token = await getToken(); + await apiClient.syncSteps(currentSteps, token); + } catch (error) { + console.error('Failed to sync steps:', error); + } + } + }, 30000); + } + } catch (error) { + console.error('Error initializing Pedometer:', error); + } + } + }; + + initPedometer(); + + return () => { + if (subscription) { + subscription.remove(); + } + if (syncInterval) { + clearInterval(syncInterval); + } + }; + }, [getToken]); + + // Update ref when steps change + useEffect(() => { + stepsRef.current = steps; + }, [steps]); + + return { steps, available }; +}; diff --git a/apps/mobile/src/services/apiService.ts b/apps/mobile/src/services/apiService.ts new file mode 100644 index 0000000..1b64439 --- /dev/null +++ b/apps/mobile/src/services/apiService.ts @@ -0,0 +1,74 @@ +import { API_BASE_URL, API_ENDPOINTS } from '../config/api'; + +export const apiClient = { + async getFitnessProfile(token?: string | null) { + try { + const headers: Record = { + 'Content-Type': 'application/json', + ...(token && { 'Authorization': `Bearer ${token}` }), + }; + const res = await fetch(`${API_BASE_URL}${API_ENDPOINTS.PROFILE.FITNESS}`, { headers }); + if (!res.ok) { + throw new Error('Failed to fetch fitness profile'); + } + return await res.json(); + } catch (e) { + console.warn('Using mock fitness profile due to fetch error', e); + // Mock data shape should match backend response + return { steps: 0, calories: 0, duration: 0 }; + } + }, + async getRecentActivities(token?: string | null) { + // Placeholder endpoint – adjust to match backend implementation + try { + const headers: Record = { + 'Content-Type': 'application/json', + ...(token && { 'Authorization': `Bearer ${token}` }), + }; + const res = await fetch(`${API_BASE_URL}/api/fitness-goals/recent`, { headers }); + if (!res.ok) { + throw new Error('Failed to fetch recent activities'); + } + return await res.json(); + } catch (e) { + console.warn('Using mock recent activities due to fetch error', e); + // Return empty array or sample data + return []; + } + }, + + async syncSteps(steps: number, token?: string | null) { + try { + const headers: Record = { + 'Content-Type': 'application/json', + ...(token && { 'Authorization': `Bearer ${token}` }), + }; + const res = await fetch(`${API_BASE_URL}/api/activity/steps`, { + method: 'POST', + headers, + body: JSON.stringify({ steps, timestamp: new Date().toISOString() }), + }); + if (!res.ok) throw new Error('Failed to sync steps'); + return await res.json(); + } catch (e) { + console.warn('Failed to sync steps', e); + throw e; + } + }, + + async getDailyActivity(date?: string, token?: string | null) { + try { + const headers: Record = { + 'Content-Type': 'application/json', + ...(token && { 'Authorization': `Bearer ${token}` }), + }; + const dateParam = date || new Date().toISOString().split('T')[0]; + const res = await fetch(`${API_BASE_URL}/api/activity/daily?date=${dateParam}`, { headers }); + if (!res.ok) throw new Error('Failed to fetch daily activity'); + return await res.json(); + } catch (e) { + console.warn('Failed to fetch daily activity', e); + return { steps: 0, calories: 0, duration: 0 }; + } + }, +}; diff --git a/packages/database/drizzle/0000_classy_jigsaw.sql b/packages/database/drizzle/0000_classy_jigsaw.sql new file mode 100644 index 0000000..afb318d --- /dev/null +++ b/packages/database/drizzle/0000_classy_jigsaw.sql @@ -0,0 +1,136 @@ +CREATE TABLE `activity_logs` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `date` text NOT NULL, + `steps` integer DEFAULT 0 NOT NULL, + `calories` real DEFAULT 0, + `duration` integer DEFAULT 0, + `distance` real DEFAULT 0, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `attendance` ( + `id` text PRIMARY KEY NOT NULL, + `client_id` text NOT NULL, + `check_in_time` integer NOT NULL, + `check_out_time` integer, + `type` text DEFAULT 'gym' NOT NULL, + `notes` text, + `created_at` integer NOT NULL, + FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `clients` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `membership_type` text DEFAULT 'basic' NOT NULL, + `membership_status` text DEFAULT 'active' NOT NULL, + `join_date` integer NOT NULL, + `last_visit` integer, + `emergency_contact_name` text, + `emergency_contact_phone` text, + `emergency_contact_relationship` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `fitness_goals` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `fitness_profile_id` text, + `goal_type` text NOT NULL, + `title` text NOT NULL, + `description` text, + `target_value` real, + `current_value` real, + `unit` text, + `start_date` integer NOT NULL, + `target_date` integer, + `completed_date` integer, + `status` text DEFAULT 'active' NOT NULL, + `progress` real DEFAULT 0, + `priority` text DEFAULT 'medium', + `notes` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`fitness_profile_id`) REFERENCES `fitness_profiles`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `fitness_profiles` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `height` real, + `weight` real, + `age` integer, + `gender` text, + `fitness_goal` text, + `activity_level` text, + `medical_conditions` text, + `allergies` text, + `injuries` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `fitness_profiles_user_id_unique` ON `fitness_profiles` (`user_id`);--> statement-breakpoint +CREATE TABLE `notifications` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `title` text NOT NULL, + `message` text NOT NULL, + `type` text NOT NULL, + `read` integer DEFAULT false NOT NULL, + `created_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `payments` ( + `id` text PRIMARY KEY NOT NULL, + `client_id` text NOT NULL, + `amount` real NOT NULL, + `currency` text DEFAULT 'USD' NOT NULL, + `status` text DEFAULT 'pending' NOT NULL, + `payment_method` text NOT NULL, + `due_date` integer NOT NULL, + `paid_at` integer, + `description` text NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `recommendations` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `fitness_profile_id` text NOT NULL, + `recommendation_text` text NOT NULL, + `activity_plan` text NOT NULL, + `diet_plan` text NOT NULL, + `status` text DEFAULT 'pending' NOT NULL, + `generated_at` integer NOT NULL, + `approved_at` integer, + `approved_by` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`fitness_profile_id`) REFERENCES `fitness_profiles`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `users` ( + `id` text PRIMARY KEY NOT NULL, + `email` text NOT NULL, + `first_name` text NOT NULL, + `last_name` text NOT NULL, + `password` text, + `role` text DEFAULT 'client' NOT NULL, + `phone` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`); \ No newline at end of file diff --git a/packages/database/drizzle/meta/0000_snapshot.json b/packages/database/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..fa4a5a5 --- /dev/null +++ b/packages/database/drizzle/meta/0000_snapshot.json @@ -0,0 +1,956 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "5e9ba8c4-bf41-4f17-a17f-68591b227fa7", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "activity_logs": { + "name": "activity_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "steps": { + "name": "steps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "calories": { + "name": "calories", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "distance": { + "name": "distance", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "activity_logs_user_id_users_id_fk": { + "name": "activity_logs_user_id_users_id_fk", + "tableFrom": "activity_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "attendance": { + "name": "attendance", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_in_time": { + "name": "check_in_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_out_time": { + "name": "check_out_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'gym'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "attendance_client_id_clients_id_fk": { + "name": "attendance_client_id_clients_id_fk", + "tableFrom": "attendance", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "clients": { + "name": "clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "membership_type": { + "name": "membership_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'basic'" + }, + "membership_status": { + "name": "membership_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "join_date": { + "name": "join_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_visit": { + "name": "last_visit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emergency_contact_name": { + "name": "emergency_contact_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emergency_contact_phone": { + "name": "emergency_contact_phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emergency_contact_relationship": { + "name": "emergency_contact_relationship", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "clients_user_id_users_id_fk": { + "name": "clients_user_id_users_id_fk", + "tableFrom": "clients", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fitness_goals": { + "name": "fitness_goals", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fitness_profile_id": { + "name": "fitness_profile_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "goal_type": { + "name": "goal_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_value": { + "name": "target_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "current_value": { + "name": "current_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_date": { + "name": "target_date", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_date": { + "name": "completed_date", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "progress": { + "name": "progress", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'medium'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "fitness_goals_user_id_users_id_fk": { + "name": "fitness_goals_user_id_users_id_fk", + "tableFrom": "fitness_goals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fitness_goals_fitness_profile_id_fitness_profiles_id_fk": { + "name": "fitness_goals_fitness_profile_id_fitness_profiles_id_fk", + "tableFrom": "fitness_goals", + "tableTo": "fitness_profiles", + "columnsFrom": [ + "fitness_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fitness_profiles": { + "name": "fitness_profiles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fitness_goal": { + "name": "fitness_goal", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "activity_level": { + "name": "activity_level", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "medical_conditions": { + "name": "medical_conditions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allergies": { + "name": "allergies", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "injuries": { + "name": "injuries", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "fitness_profiles_user_id_unique": { + "name": "fitness_profiles_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "fitness_profiles_user_id_users_id_fk": { + "name": "fitness_profiles_user_id_users_id_fk", + "tableFrom": "fitness_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications": { + "name": "notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "payments": { + "name": "payments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'USD'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "payment_method": { + "name": "payment_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "paid_at": { + "name": "paid_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "payments_client_id_clients_id_fk": { + "name": "payments_client_id_clients_id_fk", + "tableFrom": "payments", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recommendations": { + "name": "recommendations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fitness_profile_id": { + "name": "fitness_profile_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recommendation_text": { + "name": "recommendation_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "activity_plan": { + "name": "activity_plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "diet_plan": { + "name": "diet_plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "generated_at": { + "name": "generated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approved_at": { + "name": "approved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "recommendations_user_id_users_id_fk": { + "name": "recommendations_user_id_users_id_fk", + "tableFrom": "recommendations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recommendations_fitness_profile_id_fitness_profiles_id_fk": { + "name": "recommendations_fitness_profile_id_fitness_profiles_id_fk", + "tableFrom": "recommendations", + "tableTo": "fitness_profiles", + "columnsFrom": [ + "fitness_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client'" + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/database/drizzle/meta/_journal.json b/packages/database/drizzle/meta/_journal.json new file mode 100644 index 0000000..7041862 --- /dev/null +++ b/packages/database/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1764132388314, + "tag": "0000_classy_jigsaw", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index a415e07..3dc60c5 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -235,6 +235,24 @@ export const recommendations = sqliteTable("recommendations", { .$defaultFn(() => new Date()), }); +export const activityLogs = sqliteTable("activity_logs", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + date: text("date").notNull(), // YYYY-MM-DD format + steps: integer("steps").notNull().default(0), + calories: real("calories").default(0), + duration: integer("duration").default(0), // in minutes + distance: real("distance").default(0), // in km + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), +}); + export type User = typeof users.$inferSelect; export type NewUser = typeof users.$inferInsert; export type Client = typeof clients.$inferSelect; @@ -251,4 +269,6 @@ export type FitnessGoal = typeof fitnessGoals.$inferSelect; export type NewFitnessGoal = typeof fitnessGoals.$inferInsert; export type Recommendation = typeof recommendations.$inferSelect; export type NewRecommendation = typeof recommendations.$inferInsert; +export type ActivityLog = typeof activityLogs.$inferSelect; +export type NewActivityLog = typeof activityLogs.$inferInsert;