From 28b5b52a8f5620245c36e05144996ee6d79da4c2 Mon Sep 17 00:00:00 2001 From: echo Date: Wed, 26 Nov 2025 01:13:11 +0100 Subject: [PATCH] context extended maybe need polishing --- apps/admin/data/fitai.db | Bin 122880 -> 135168 bytes apps/admin/scripts/create-fitness-goals.js | 49 +++++ .../api/fitness-goals/[id]/complete/route.ts | 39 ++++ .../src/app/api/fitness-goals/[id]/route.ts | 119 +++++++++++++ apps/admin/src/app/api/fitness-goals/route.ts | 95 ++++++++++ .../app/api/recommendations/generate/route.ts | 33 ++-- apps/admin/src/lib/ai/ai-context.ts | 88 +++++++++ apps/admin/src/lib/ai/prompt-builder.ts | 91 ++++++++++ apps/admin/src/lib/database/sqlite.ts | 168 ++++++++++++++++++ apps/admin/src/lib/database/types.ts | 35 ++++ packages/database/src/schema.ts | 64 +++++++ 11 files changed, 761 insertions(+), 20 deletions(-) create mode 100644 apps/admin/scripts/create-fitness-goals.js create mode 100644 apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts create mode 100644 apps/admin/src/app/api/fitness-goals/[id]/route.ts create mode 100644 apps/admin/src/app/api/fitness-goals/route.ts create mode 100644 apps/admin/src/lib/ai/ai-context.ts create mode 100644 apps/admin/src/lib/ai/prompt-builder.ts diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 5572a1b5e38aea3ce09a8536ad8ec0de0ce574c7..8ead25bdc4782d121181fd47e5467a38303c9f36 100644 GIT binary patch delta 2857 zcma)8Z){sv758i0IK=gHTAI40Nx6Y({?pj;pEzlv1sW1}?UsbnI@(k;x!3Pq+m}AS z`}*#ENj)T-NgHEhgLd!36W>;8e4Kz~QnYWAhL8|Qd;zv1P*LFnlaSyuLlfsd|7pFf z8^2l@?Jo#t2fp0 zXjr|cz7u(G_h|ObNHQ_BsT6T8i_a_+(zEl0h4gH3p_Z=Z=d0-=na|J8oyp?D?1H-c z&!vgCcE5P>m(g24pFI?T5C?|d4}>R&-rt=X`_0hYk5<&^=KA`{?}uKFzQiY&me&6t zx9(rt$orn7q}m43nMn{ck;Y6;xge%6T1ePfyGG4AYB9$~md)xm;d}~R5chU|!ch&I z0&_t%BpBj^NHb8_1iUnxR0yI=P1T`>F1=vXAlMKMN^E4%8sVa4kPKQQZ4u`(vY@6z zB1b~&NeHFv0MQsy5R!W>Bfu96*IFSQcAE&u8<+bS{_9&y};;#awnVzmS=q zoxieAK0BYu=ZjYyPV7pdSo2yl%JCyFU5)r#?fK0YzWai|)!ctCJ+k*oSlJAo*%zW% z&KDMoka<3npDTVMME1;Lp^%x&<*#_xXB70>&0sJv76|N(j4A((9#tbB1=Nq#JL=o& zkJay}B^5-)gM&96pF>;%B>+ewWLOc2^+IgXV~Ba4`VBrO3UREDldOyt%L?t-hVz& zvcoT}q(XI6E+akx?q5WfAx6furq2{}El-K#BM^ zq4kESz=Et35uZkJxf#s5XjGcukqfv0q!jj@V`Ebz4_MX=a)VZ3#zo6dMcVm0w$j@` zQUVKEux31&^iipOTrkoGai^jaZJltK!Gv)D7orx{6N(WKEbHn1asf0UPgIe^w^jtEF?x zn`>nh_ZK&bqm}cel_wJrQo#M;v)>rGJCj0A0*Cu z=1)o}yiYp*7v=Cc^kov@o-17{ZLE|oBiY2zG8x$jCJkWY2T>cYlq>1p&? z$Iv%72FEVZa|9^c-62a_lcCrW!1%&LV022|JL!)(`Wy(|?${T;BTw>)Q0(h%2pM@u z$j)LIO#9296SI3}f6iS+a^d7~Y(1_7{FwZ@n>rMm^s}kt29i4hubw0F4cB||uF@JF Sm4C-}4`2M1d@=p($NvEl7@2?o delta 365 zcmZozz|nAkeS);$BL)TrIUp7SVkRJto2X;V_-JFo5`6()-Zu<<>3kFTMESY-9`Wtm zEM~Blcj5-^&2MaFHgWOk@jLJx<(_4{A5KQiRt@q zFtSZP$-^(eyNp4=Ri2R{GcP5zqPQ?;`@$QHtgI|Rn;%Z^zR&2z1`Hk^TXDE1UZc|D j)S_ZwurWSF2=lNC0tL2Bzi@+5baOJF3gh(l`;3(U4N79T diff --git a/apps/admin/scripts/create-fitness-goals.js b/apps/admin/scripts/create-fitness-goals.js new file mode 100644 index 0000000..e4741ff --- /dev/null +++ b/apps/admin/scripts/create-fitness-goals.js @@ -0,0 +1,49 @@ +const Database = require('better-sqlite3'); +const path = require('path'); + +const dbPath = path.join(__dirname, '../data/fitai.db'); +const db = new Database(dbPath); + +console.log('Creating fitness_goals table...'); + +try { + db.exec(` + CREATE TABLE IF NOT EXISTS fitness_goals ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + fitness_profile_id TEXT, + goal_type TEXT NOT NULL CHECK(goal_type IN ('weight_target', 'strength_milestone', 'endurance_target', 'flexibility_goal', 'habit_building', 'custom')), + 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 NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'abandoned', 'paused')), + progress REAL DEFAULT 0, + priority TEXT DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high')), + notes TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles(id) ON DELETE CASCADE + ) + `); + + // Create indexes for better query performance + db.exec(` + CREATE INDEX IF NOT EXISTS idx_fitness_goals_user_id ON fitness_goals(user_id); + CREATE INDEX IF NOT EXISTS idx_fitness_goals_status ON fitness_goals(status); + CREATE INDEX IF NOT EXISTS idx_fitness_goals_profile_id ON fitness_goals(fitness_profile_id); + `); + + console.log('✅ fitness_goals table created successfully'); + console.log('✅ Indexes created successfully'); +} catch (error) { + console.error('❌ Migration failed:', error); + process.exit(1); +} finally { + db.close(); +} diff --git a/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts b/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts new file mode 100644 index 0000000..6f62465 --- /dev/null +++ b/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@clerk/nextjs/server'; +import { getDatabase } from '@/lib/database'; + +// POST - Mark goal as complete +export async function POST( + req: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id } = await params; + const db = await getDatabase(); + + // Verify goal exists and user owns it + const existingGoal = await db.getFitnessGoalById(id); + if (!existingGoal) { + return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); + } + if (existingGoal.userId !== userId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + // Mark as completed + const completedGoal = await db.completeGoal(id); + + return NextResponse.json(completedGoal); + } catch (error) { + console.error('Error completing fitness goal:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/apps/admin/src/app/api/fitness-goals/[id]/route.ts b/apps/admin/src/app/api/fitness-goals/[id]/route.ts new file mode 100644 index 0000000..dc31594 --- /dev/null +++ b/apps/admin/src/app/api/fitness-goals/[id]/route.ts @@ -0,0 +1,119 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@clerk/nextjs/server'; +import { getDatabase } from '@/lib/database'; + +// GET - Get specific goal +export async function GET( + req: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id } = await params; + const db = await getDatabase(); + + const goal = await db.getFitnessGoalById(id); + + if (!goal) { + return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); + } + + // Verify ownership + if (goal.userId !== userId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + return NextResponse.json(goal); + } catch (error) { + console.error('Error fetching fitness goal:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// PUT - Update goal +export async function PUT( + req: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id } = await params; + const db = await getDatabase(); + + // Verify goal exists and user owns it + const existingGoal = await db.getFitnessGoalById(id); + if (!existingGoal) { + return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); + } + if (existingGoal.userId !== userId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + const updates = await req.json(); + + // Don't allow changing userId or id + delete updates.userId; + delete updates.id; + delete updates.createdAt; + + const updatedGoal = await db.updateFitnessGoal(id, updates); + + return NextResponse.json(updatedGoal); + } catch (error) { + console.error('Error updating fitness goal:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// DELETE - Delete goal +export async function DELETE( + req: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id } = await params; + const db = await getDatabase(); + + // Verify goal exists and user owns it + const existingGoal = await db.getFitnessGoalById(id); + if (!existingGoal) { + return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); + } + if (existingGoal.userId !== userId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + const deleted = await db.deleteFitnessGoal(id); + + if (deleted) { + return NextResponse.json({ success: true }); + } else { + return NextResponse.json({ error: 'Failed to delete goal' }, { status: 500 }); + } + } catch (error) { + console.error('Error deleting fitness goal:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/apps/admin/src/app/api/fitness-goals/route.ts b/apps/admin/src/app/api/fitness-goals/route.ts new file mode 100644 index 0000000..c82c3f3 --- /dev/null +++ b/apps/admin/src/app/api/fitness-goals/route.ts @@ -0,0 +1,95 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from '@clerk/nextjs/server'; +import { getDatabase } from '@/lib/database'; +import { randomBytes } from 'crypto'; + +// GET - List user's fitness goals +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 targetUserId = searchParams.get('userId') || userId; + const status = searchParams.get('status'); // active, completed, all + + const db = await getDatabase(); + + // Fetch goals with optional status filter + const goals = await db.getFitnessGoalsByUserId( + targetUserId, + status && status !== 'all' ? status : undefined + ); + + return NextResponse.json(goals); + } catch (error) { + console.error('Error fetching fitness goals:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// POST - Create new fitness goal +export async function POST(req: NextRequest) { + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await req.json(); + const { + goalType, + title, + description, + targetValue, + currentValue, + unit, + targetDate, + priority, + notes, + fitnessProfileId + } = body; + + // Validation + if (!goalType || !title) { + return NextResponse.json( + { error: 'goalType and title are required' }, + { status: 400 } + ); + } + + const db = await getDatabase(); + + // Create the goal + const goal = await db.createFitnessGoal({ + id: randomBytes(16).toString('hex'), + userId, + fitnessProfileId: fitnessProfileId || undefined, + goalType, + title, + description: description || undefined, + targetValue: targetValue || undefined, + currentValue: currentValue || 0, + unit: unit || undefined, + startDate: new Date(), + targetDate: targetDate ? new Date(targetDate) : undefined, + status: 'active', + progress: 0, + priority: priority || 'medium', + notes: notes || undefined + }); + + return NextResponse.json(goal, { status: 201 }); + } catch (error) { + console.error('Error creating fitness goal:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/apps/admin/src/app/api/recommendations/generate/route.ts b/apps/admin/src/app/api/recommendations/generate/route.ts index cbb6541..a1345db 100644 --- a/apps/admin/src/app/api/recommendations/generate/route.ts +++ b/apps/admin/src/app/api/recommendations/generate/route.ts @@ -1,5 +1,7 @@ import { NextResponse } from "next/server"; import { getDatabase } from "@/lib/database"; +import { buildAIContext } from "@/lib/ai/ai-context"; +import { buildEnhancedPrompt, buildBasicPrompt } from "@/lib/ai/prompt-builder"; export async function POST(req: Request) { try { @@ -21,26 +23,17 @@ export async function POST(req: Request) { ); } - // Construct prompt - const prompt = ` - You are a professional fitness trainer and nutritionist. - Generate a detailed daily recommendation for a user with the following profile: - - Height: ${profile.height} cm - - Weight: ${profile.weight} kg - - Age: ${profile.age} - - Gender: ${profile.gender} - - Goal: ${profile.fitnessGoals.join(", ")} - - Activity Level: ${profile.activityLevel} - - Medical Conditions: ${profile.medicalConditions || "None"} - - Injuries: ${profile.injuries || "None"} - - Please provide the response in the following JSON format ONLY, no other text. Do not use markdown formatting or code blocks: - { - "recommendationText": "General advice and motivation for today.", - "activityPlan": "Detailed workout or activity plan for today.", - "dietPlan": "Detailed meal plan for today." - } - `; + // Build AI context with goals and recommendations + let prompt: string; + try { + const context = await buildAIContext(userId); + prompt = buildEnhancedPrompt(context); + console.log('Using enhanced AI context with goals and history'); + } catch (error) { + // Fallback to basic prompt if context building fails + console.warn('Failed to build AI context, using basic prompt:', error); + prompt = buildBasicPrompt(profile); + } let parsedResponse; diff --git a/apps/admin/src/lib/ai/ai-context.ts b/apps/admin/src/lib/ai/ai-context.ts new file mode 100644 index 0000000..e06eb42 --- /dev/null +++ b/apps/admin/src/lib/ai/ai-context.ts @@ -0,0 +1,88 @@ +import { getDatabase } from '@/lib/database'; +import type { FitnessGoal } from '@/lib/database/types'; + +// Import types from database package +import type { + FitnessProfile as DBFitnessProfile, + Recommendation as DBRecommendation, +} from '@fitai/database'; + +export interface AIContext { + profile: DBFitnessProfile; + activeGoals: FitnessGoal[]; + completedGoals: FitnessGoal[]; + recentRecommendations: DBRecommendation[]; + progressSummary: { + goalsCompleted: number; + goalsActive: number; + averageProgress: number; + }; +} + +/** + * Build comprehensive AI context for a user + * Aggregates fitness profile, goals, and recommendation history + */ +export async function buildAIContext(userId: string): Promise { + const db = await getDatabase(); + + // Fetch fitness profile + const profile = await db.getFitnessProfileByUserId(userId); + + if (!profile) { + throw new Error(`Fitness profile not found for user ${userId}`); + } + + // Fetch all fitness goals + const allGoals = await db.getFitnessGoalsByUserId(userId); + + // Separate active and completed goals + const activeGoals = allGoals.filter((g) => g.status === 'active'); + const completedGoals = allGoals.filter((g) => g.status === 'completed'); + + // Get all recommendations + const allRecommendations = await db.getRecommendationsByUserId(userId); + + // Filter recent recommendations (last 7 days) + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + const recentRecommendations = allRecommendations + .filter((r) => new Date(r.createdAt) > sevenDaysAgo) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, 5); + + // Calculate progress summary + const progressSummary = { + goalsCompleted: completedGoals.length, + goalsActive: activeGoals.length, + averageProgress: + activeGoals.length > 0 + ? activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) / activeGoals.length + : 0, + }; + + return { + profile, + activeGoals, + completedGoals, + recentRecommendations, + progressSummary, + }; +} + +/** + * Format goal for display in AI prompt + */ +export function formatGoalForPrompt(goal: FitnessGoal): string { + const progress = goal.progress || 0; + const current = goal.currentValue || 0; + const target = goal.targetValue; + const unit = goal.unit || ''; + + if (target) { + return `${goal.title}: ${current}/${target} ${unit} (${progress.toFixed(1)}% complete)`; + } + + return `${goal.title}: ${progress.toFixed(1)}% complete`; +} diff --git a/apps/admin/src/lib/ai/prompt-builder.ts b/apps/admin/src/lib/ai/prompt-builder.ts new file mode 100644 index 0000000..735b0cd --- /dev/null +++ b/apps/admin/src/lib/ai/prompt-builder.ts @@ -0,0 +1,91 @@ +import type { AIContext } from './ai-context'; +import { formatGoalForPrompt } from './ai-context'; + +/** + * Build enhanced AI prompt with comprehensive user context + * Includes profile, goals, progress, and recommendation history + */ +export function buildEnhancedPrompt(context: AIContext): string { + const { profile, activeGoals, completedGoals, recentRecommendations, progressSummary } = context; + + // Build goals section + const activeGoalsText = + activeGoals.length > 0 + ? activeGoals.map((g) => `- ${formatGoalForPrompt(g)}`).join('\n') + : '- No active goals set'; + + const completedGoalsText = + completedGoals.length > 0 + ? completedGoals.slice(0, 3).map((g) => g.title).join(', ') + : 'None yet'; + + // Build context about recent recommendations + const recommendationContextText = + recentRecommendations.length > 0 + ? `The user has received ${recentRecommendations.length} recommendations in the past week. Consider their progress and avoid repetitive advice.` + : 'This is a new user or they haven\'t received recent recommendations. Provide comprehensive guidance.'; + + return `You are a professional fitness trainer and nutritionist with access to the user's complete fitness journey. + +## User Profile +- Height: ${profile.height || 'Not specified'} cm +- Weight: ${profile.weight || 'Not specified'} kg +- Age: ${profile.age || 'Not specified'} +- Gender: ${profile.gender || 'Not specified'} +- Primary Goal: ${profile.fitnessGoal || 'General fitness'} +- Activity Level: ${profile.activityLevel || 'Not specified'} +- Medical Conditions: ${profile.medicalConditions || 'None'} +- Allergies: ${profile.allergies || 'None'} +- Injuries: ${profile.injuries || 'None'} + +## Active Goals (${activeGoals.length}) +${activeGoalsText} + +## Recent Progress +- Goals Completed: ${progressSummary.goalsCompleted} +- Active Goals: ${progressSummary.goalsActive} +- Average Progress on Active Goals: ${progressSummary.averageProgress.toFixed(1)}% +${completedGoals.length > 0 ? `- Recently Completed: ${completedGoalsText}` : ''} + +## Recommendation History +${recommendationContextText} + +## Task +Generate a personalized daily recommendation that: +1. Acknowledges their progress on active goals +2. Provides specific, actionable steps toward goal completion +3. Builds on previous recommendations without being repetitive +4. Adapts to their current progress level +5. Takes into account their medical conditions, allergies, and injuries + +Respond in the following JSON format ONLY, no other text. Do not use markdown formatting or code blocks: +{ + "recommendationText": "Personalized motivation and progress acknowledgment (2-3 sentences)", + "activityPlan": "Specific workout plan aligned with active goals (detailed, 3-5 exercises or activities)", + "dietPlan": "Nutrition plan supporting their goals and restrictions (detailed, include meals and portions)" +}`; +} + +/** + * Build basic prompt for users without complete context + * Fallback when goals or other data is missing + */ +export function buildBasicPrompt(profile: any): string { + return `You are a professional fitness trainer and nutritionist. +Generate a detailed daily recommendation for a user with the following profile: +- Height: ${profile.height} cm +- Weight: ${profile.weight} kg +- Age: ${profile.age} +- Gender: ${profile.gender} +- Goal: ${profile.fitnessGoal} +- Activity Level: ${profile.activityLevel} +- Medical Conditions: ${profile.medicalConditions || "None"} +- Injuries: ${profile.injuries || "None"} + +Please provide the response in the following JSON format ONLY, no other text. Do not use markdown formatting or code blocks: +{ + "recommendationText": "General advice and motivation for today.", + "activityPlan": "Detailed workout or activity plan for today.", + "dietPlan": "Detailed meal plan for today." +}`; +} diff --git a/apps/admin/src/lib/database/sqlite.ts b/apps/admin/src/lib/database/sqlite.ts index 4ad39a5..06cc9ae 100644 --- a/apps/admin/src/lib/database/sqlite.ts +++ b/apps/admin/src/lib/database/sqlite.ts @@ -614,6 +614,174 @@ export class SQLiteDatabase implements IDatabase { } } + // Fitness Goals operations + async createFitnessGoal(goalData: Omit): Promise { + if (!this.db) throw new Error('Database not connected') + + const now = new Date() + const goal: import('./types').FitnessGoal = { + ...goalData, + createdAt: now, + updatedAt: now + } + + const stmt = this.db.prepare( + `INSERT INTO fitness_goals ( + id, user_id, fitness_profile_id, goal_type, title, description, + target_value, current_value, unit, start_date, target_date, + completed_date, status, progress, priority, notes, created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + + stmt.run( + goal.id, + goal.userId, + goal.fitnessProfileId || null, + goal.goalType, + goal.title, + goal.description || null, + goal.targetValue || null, + goal.currentValue || null, + goal.unit || null, + goal.startDate.toISOString(), + goal.targetDate ? goal.targetDate.toISOString() : null, + goal.completedDate ? goal.completedDate.toISOString() : null, + goal.status, + goal.progress, + goal.priority, + goal.notes || null, + goal.createdAt.toISOString(), + goal.updatedAt.toISOString() + ) + + return goal + } + + async getFitnessGoalById(id: string): Promise { + if (!this.db) throw new Error('Database not connected') + + const stmt = this.db.prepare('SELECT * FROM fitness_goals WHERE id = ?') + const row = stmt.get(id) + + return row ? this.mapRowToFitnessGoal(row) : null + } + + async getFitnessGoalsByUserId(userId: string, status?: string): Promise { + if (!this.db) throw new Error('Database not connected') + + let query = 'SELECT * FROM fitness_goals WHERE user_id = ?' + const params: any[] = [userId] + + if (status) { + query += ' AND status = ?' + params.push(status) + } + + query += ' ORDER BY created_at DESC' + + const stmt = this.db.prepare(query) + const rows = stmt.all(...params) + + return rows.map(row => this.mapRowToFitnessGoal(row)) + } + + async updateFitnessGoal(id: string, updates: Partial): Promise { + if (!this.db) throw new Error('Database not connected') + + const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'createdAt') + if (fields.length === 0) return this.getFitnessGoalById(id) + + // Map camelCase to snake_case for database columns + const columnMap: Record = { + userId: 'user_id', + fitnessProfileId: 'fitness_profile_id', + goalType: 'goal_type', + targetValue: 'target_value', + currentValue: 'current_value', + startDate: 'start_date', + targetDate: 'target_date', + completedDate: 'completed_date', + updatedAt: 'updated_at' + } + + const setClause = fields.map(field => `${columnMap[field] || field} = ?`).join(', ') + const values = fields.map(field => { + const val = (updates as any)[field] + return val instanceof Date ? val.toISOString() : val + }) + values.push(new Date().toISOString()) // updatedAt + values.push(id) + + const stmt = this.db.prepare(`UPDATE fitness_goals SET ${setClause}, updated_at = ? WHERE id = ?`) + stmt.run(values) + + return this.getFitnessGoalById(id) + } + + async deleteFitnessGoal(id: string): Promise { + if (!this.db) throw new Error('Database not connected') + + const stmt = this.db.prepare('DELETE FROM fitness_goals WHERE id = ?') + const result = stmt.run(id) + return (result.changes || 0) > 0 + } + + async updateGoalProgress(id: string, currentValue: number): Promise { + if (!this.db) throw new Error('Database not connected') + + // Get the goal to calculate progress + const goal = await this.getFitnessGoalById(id) + if (!goal) return null + + let progress = goal.progress + if (goal.targetValue && goal.targetValue > 0) { + progress = Math.min(100, (currentValue / goal.targetValue) * 100) + } + + const stmt = this.db.prepare( + 'UPDATE fitness_goals SET current_value = ?, progress = ?, updated_at = ? WHERE id = ?' + ) + stmt.run(currentValue, progress, new Date().toISOString(), id) + + return this.getFitnessGoalById(id) + } + + async completeGoal(id: string): Promise { + if (!this.db) throw new Error('Database not connected') + + const now = new Date() + const stmt = this.db.prepare( + 'UPDATE fitness_goals SET status = ?, progress = ?, completed_date = ?, updated_at = ? WHERE id = ?' + ) + stmt.run('completed', 100, now.toISOString(), now.toISOString(), id) + + return this.getFitnessGoalById(id) + } + + private mapRowToFitnessGoal(row: any): import('./types').FitnessGoal { + return { + id: row.id, + userId: row.user_id, + fitnessProfileId: row.fitness_profile_id, + goalType: row.goal_type, + title: row.title, + description: row.description, + targetValue: row.target_value, + currentValue: row.current_value, + unit: row.unit, + startDate: new Date(row.start_date), + targetDate: row.target_date ? new Date(row.target_date) : undefined, + completedDate: row.completed_date ? new Date(row.completed_date) : undefined, + status: row.status, + progress: row.progress, + priority: row.priority, + notes: row.notes, + createdAt: new Date(row.created_at), + updatedAt: new Date(row.updated_at) + } + } + async getDashboardStats(): Promise<{ totalUsers: number; activeClients: number; diff --git a/apps/admin/src/lib/database/types.ts b/apps/admin/src/lib/database/types.ts index c74a222..608ee65 100644 --- a/apps/admin/src/lib/database/types.ts +++ b/apps/admin/src/lib/database/types.ts @@ -61,6 +61,27 @@ export interface Recommendation { approvedBy?: string; } +export interface FitnessGoal { + id: string; + userId: string; + fitnessProfileId?: string; + goalType: "weight_target" | "strength_milestone" | "endurance_target" | "flexibility_goal" | "habit_building" | "custom"; + title: string; + description?: string; + targetValue?: number; + currentValue?: number; + unit?: string; + startDate: Date; + targetDate?: Date; + completedDate?: Date; + status: "active" | "completed" | "abandoned" | "paused"; + progress: number; + priority: "low" | "medium" | "high"; + notes?: string; + createdAt: Date; + updatedAt: Date; +} + // Database Interface - allows us to swap implementations export interface IDatabase { // Connection management @@ -121,6 +142,20 @@ export interface IDatabase { ): Promise; deleteRecommendation(id: string): Promise; + // Fitness Goals operations + createFitnessGoal( + goal: Omit + ): Promise; + getFitnessGoalById(id: string): Promise; + getFitnessGoalsByUserId(userId: string, status?: string): Promise; + updateFitnessGoal( + id: string, + updates: Partial + ): Promise; + deleteFitnessGoal(id: string): Promise; + updateGoalProgress(id: string, currentValue: number): Promise; + completeGoal(id: string): Promise; + // Dashboard operations getDashboardStats(): Promise<{ totalUsers: number; diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index 99465c7..a415e07 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -145,6 +145,67 @@ export const fitnessProfiles = sqliteTable("fitness_profiles", { .$defaultFn(() => new Date()), }); +export const fitnessGoals = sqliteTable("fitness_goals", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + fitnessProfileId: text("fitness_profile_id").references( + () => fitnessProfiles.id, + { onDelete: "cascade" } + ), + + // Goal details + goalType: text("goal_type", { + enum: [ + "weight_target", + "strength_milestone", + "endurance_target", + "flexibility_goal", + "habit_building", + "custom", + ], + }).notNull(), + + title: text("title").notNull(), + description: text("description"), + + // Measurable targets + targetValue: real("target_value"), // e.g., 70 (kg), 100 (kg bench press) + currentValue: real("current_value"), // Current progress + unit: text("unit"), // kg, km, reps, etc. + + // Timeline + startDate: integer("start_date", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + targetDate: integer("target_date", { mode: "timestamp" }), + completedDate: integer("completed_date", { mode: "timestamp" }), + + // Status tracking + status: text("status", { + enum: ["active", "completed", "abandoned", "paused"], + }) + .notNull() + .default("active"), + + progress: real("progress").default(0), // 0-100 percentage + + // Metadata + priority: text("priority", { + enum: ["low", "medium", "high"], + }).default("medium"), + + notes: text("notes"), + + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), +}); + export const recommendations = sqliteTable("recommendations", { id: text("id").primaryKey(), userId: text("user_id") @@ -186,5 +247,8 @@ export type Notification = typeof notifications.$inferSelect; export type NewNotification = typeof notifications.$inferInsert; export type FitnessProfile = typeof fitnessProfiles.$inferSelect; export type NewFitnessProfile = typeof fitnessProfiles.$inferInsert; +export type FitnessGoal = typeof fitnessGoals.$inferSelect; +export type NewFitnessGoal = typeof fitnessGoals.$inferInsert; export type Recommendation = typeof recommendations.$inferSelect; export type NewRecommendation = typeof recommendations.$inferInsert; +