pedometar

problem with expogo, should be sorted before build
This commit is contained in:
echo 2025-11-28 18:08:52 +01:00
parent b0d751540e
commit 24ed0b6190
16 changed files with 1676 additions and 64 deletions

Binary file not shown.

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -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
}
}

View File

@ -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(

View File

@ -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"
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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<Array<any>>([]);
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 (
<View style={styles.container}>
<ScrollView
@ -51,11 +82,9 @@ export default function HomeScreen() {
</View>
{/* Activity Widget */}
<ActivityWidget
steps={8432}
calories={640}
duration={45}
/>
{profile && (
<ActivityWidget steps={displayedSteps} calories={profile.calories} duration={profile.duration} />
)}
{/* Quick Actions */}
<QuickActionGrid />
@ -67,45 +96,22 @@ export default function HomeScreen() {
<Text style={styles.seeAll}>See All</Text>
</View>
<View style={styles.activityCard}>
<LinearGradient
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.5)']}
style={[styles.recentItem, theme.shadows.subtle]}
>
<View style={styles.recentIconContainer}>
<LinearGradient
colors={theme.gradients.primary}
style={styles.recentIcon}
>
<Ionicons name="barbell" size={20} color="#fff" />
</LinearGradient>
</View>
<View style={styles.recentInfo}>
<Text style={styles.recentTitle}>Upper Body Power</Text>
<Text style={styles.recentSubtitle}>Today, 10:00 AM</Text>
</View>
<Text style={styles.recentValue}>45m</Text>
</LinearGradient>
<LinearGradient
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.5)']}
style={[styles.recentItem, theme.shadows.subtle]}
>
<View style={styles.recentIconContainer}>
<LinearGradient
colors={theme.gradients.success}
style={styles.recentIcon}
>
<Ionicons name="bicycle" size={20} color="#fff" />
</LinearGradient>
</View>
<View style={styles.recentInfo}>
<Text style={styles.recentTitle}>Morning Cardio</Text>
<Text style={styles.recentSubtitle}>Yesterday, 7:30 AM</Text>
</View>
<Text style={styles.recentValue}>30m</Text>
</LinearGradient>
</View>
{recentActivities.map((act: any, idx: number) => (
<View key={idx} style={styles.recentItem}>
<LinearGradient colors={['rgba(255,255,255,0.8)', 'rgba(255,255,255,0.5)']} style={[styles.recentItem, theme.shadows.subtle]}>
<View style={styles.recentIconContainer}>
<LinearGradient colors={(theme.gradients as any)[act.gradient || 'primary']} style={styles.recentIcon}>
<Ionicons name={act.icon || 'barbell'} size={20} color="#fff" />
</LinearGradient>
</View>
<View style={styles.recentInfo}>
<Text style={styles.recentTitle}>{act.title}</Text>
<Text style={styles.recentSubtitle}>{act.subtitle}</Text>
</View>
<Text style={styles.recentValue}>{act.duration}m</Text>
</LinearGradient>
</View>
))}
</View>
{/* Bottom Spacer for Tab Bar */}

View File

@ -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 = {

View File

@ -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<boolean | null>(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 };
};

View File

@ -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<string, string> = {
'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<string, string> = {
'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<string, string> = {
'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<string, string> = {
'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 };
}
},
};

View File

@ -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`);

View File

@ -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": {}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1764132388314,
"tag": "0000_classy_jigsaw",
"breakpoints": true
}
]
}

View File

@ -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;