From 118efad70f5b88e16a3773ef917b1ad89d3d8458 Mon Sep 17 00:00:00 2001 From: echo Date: Thu, 20 Nov 2025 18:36:40 +0100 Subject: [PATCH] invite and saving fitness profile added, need polishing --- apps/admin/data/fitai.db | Bin 73728 -> 73728 bytes apps/admin/package-lock.json | 58 +++++++++------- apps/admin/package.json | 1 + apps/admin/scripts/migrate-fitness-profile.js | 33 +++++++++ .../src/app/api/fitness-profile/route.ts | 56 +++++++++------ apps/admin/src/app/api/users/route.ts | 45 +++++++++--- .../src/components/users/UserManagement.tsx | 64 ++++++++++++++---- apps/admin/src/lib/database/sqlite.ts | 13 ++-- apps/admin/src/lib/database/types.ts | 2 + apps/mobile/src/config/api.ts | 2 +- 10 files changed, 202 insertions(+), 72 deletions(-) create mode 100644 apps/admin/scripts/migrate-fitness-profile.js diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 4bb1adc90b722cb7db6799b3ab245e6be5797e2c..d3b15b81f7c4ff8d7590482e7b57ce6d7e62805b 100644 GIT binary patch delta 1041 zcmb7DUr19?7(aLSs+;cao0;61)}3Szg}dH+=iTPyqLibAT%j|<3c2dq=C-@qbhQPk zh(Si6TrAWFAqXl8!aejLf)7T9L{H&Efh6_NQxCqDc2@akp%20NIDEhJJKy*FzVDpD z1!{1C8Y?fZM+hCjX23>bGmlr>OX?AK=LBu2L?(hkz-$d z%};cha^pABNWlbxS(t#&u#Djm&=wmi4pA0TA`~?gR=Q#Vk#7t{l~9E1jz!7LGJA{% zk@pD>H}ArWv)A#N&2xF?d;*3M4C5eiunZ9d&6X73BA|sSEiGk>acQ!QkS1(c&k8P{ z<6RQROL&he5r4wp<5&BXckrySKAlR}QoNl|_ZFg|aT1Urz_@goN>>y>$P-8#9 zLgOrY;}U{N5KYUb9{MN!fUYqvVNEZM0l!|)va(!)B9tCrEW2fmC$AE18=454wYT7)P)I>o5mjVFq5nTNuYOQedJ}c*Jj@P z|B%-)aRt-nFnQ)LS({m<3Cvl|a>@{Vfj96B?!z@`0|%7hfCN-lkTS8K7=89oyFG8w zTEEX4DSC=9RY9ZaH9dvzj?x1|1yms`SF#e#$a1nPlyJpWN3e^-LkTAowMU6J#`^mr zu~;}e{T;L`qNM%FL^#y!z0mCQGREx zcMi0wjmd$ch8l($%0nr#kd>Gci7u06H4#jwc`>2x^&q#fy>xU(LikzzBgF3JMb0Tm zxkB8v4!0<9l)&3Nw8qUv{4b^vJcUp23Z`+Z?twKsx9tv5S#MhySw*%Gns?Ls@FNh% L|NBpA%dGSZ-k=`q delta 652 zcmZoTz|wGlWrDPz5d#B*5D;?$F*6YJP1G?KHe%5065-`kWZ+|d#K8QB_aA5e#G<8} zCvb8z+Be(svy02iGq!n_BqrsgmKLWL702hLmctnW&Oxq@A+8D`j!r(V3UFZs4K4*B zm^_h3b@LLw7{<+ynC)3vcw!ioCwKD7PIltdLmjC2tL4{8K$@9;rLgYRJ#jo?9 z1j?`FpAVJ050W!u;D5q@oPQO6FMk1lFuxhlPzC=10" }, @@ -9910,8 +9913,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/promise-inflight": { "version": "1.0.1", @@ -10043,6 +10045,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10052,6 +10055,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10064,6 +10068,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -10087,6 +10092,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -10185,7 +10191,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -11638,6 +11645,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11857,6 +11865,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12443,6 +12452,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/apps/admin/package.json b/apps/admin/package.json index 54cff00..d2a096a 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -25,6 +25,7 @@ "autoprefixer": "^10.4.16", "axios": "^1.13.2", "bcryptjs": "^3.0.3", + "better-sqlite3": "^11.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/apps/admin/scripts/migrate-fitness-profile.js b/apps/admin/scripts/migrate-fitness-profile.js new file mode 100644 index 0000000..434d292 --- /dev/null +++ b/apps/admin/scripts/migrate-fitness-profile.js @@ -0,0 +1,33 @@ +const Database = require('better-sqlite3'); +const path = require('path'); + +const dbPath = path.join(__dirname, '../../../data/fitai.db'); +const db = new Database(dbPath); + +console.log('Migrating fitness_profiles table...'); + +try { + // Check if columns exist + const tableInfo = db.prepare('PRAGMA table_info(fitness_profiles)').all(); + const columns = tableInfo.map(c => c.name); + + if (!columns.includes('allergies')) { + console.log('Adding allergies column...'); + db.prepare('ALTER TABLE fitness_profiles ADD COLUMN allergies TEXT').run(); + } else { + console.log('allergies column already exists.'); + } + + if (!columns.includes('injuries')) { + console.log('Adding injuries column...'); + db.prepare('ALTER TABLE fitness_profiles ADD COLUMN injuries TEXT').run(); + } else { + console.log('injuries column already exists.'); + } + + console.log('Migration completed successfully.'); +} catch (error) { + console.error('Migration failed:', error); +} finally { + db.close(); +} diff --git a/apps/admin/src/app/api/fitness-profile/route.ts b/apps/admin/src/app/api/fitness-profile/route.ts index 7b53af3..b5991e3 100644 --- a/apps/admin/src/app/api/fitness-profile/route.ts +++ b/apps/admin/src/app/api/fitness-profile/route.ts @@ -16,10 +16,20 @@ export async function GET(request: NextRequest) { const profile = db .prepare( - `SELECT * FROM fitness_profiles WHERE user_id = ?` + `SELECT * FROM fitness_profiles WHERE userId = ?` ) .get(userId); + if (profile) { + const p = profile as any; + // Parse JSON fields + try { + p.fitnessGoals = JSON.parse(p.fitnessGoals || '[]'); + } catch (e) { + p.fitnessGoals = []; + } + } + return NextResponse.json({ profile: profile || null }); } catch (error) { console.error("Error fetching fitness profile:", error); @@ -45,19 +55,22 @@ export async function POST(request: NextRequest) { weight, age, gender, - fitnessGoal, + fitnessGoals, // Changed from fitnessGoal activityLevel, medicalConditions, allergies, injuries, + exerciseHabits, + dietHabits } = body; // Check if profile exists const existingProfile = db - .prepare(`SELECT id FROM fitness_profiles WHERE user_id = ?`) - .get(userId) as { id: string } | undefined; + .prepare(`SELECT userId FROM fitness_profiles WHERE userId = ?`) + .get(userId) as { userId: string } | undefined; - const now = Date.now(); + const now = new Date().toISOString(); + const fitnessGoalsJson = JSON.stringify(fitnessGoals || []); if (existingProfile) { // Update existing profile @@ -67,52 +80,55 @@ export async function POST(request: NextRequest) { weight = ?, age = ?, gender = ?, - fitness_goal = ?, - activity_level = ?, - medical_conditions = ?, + fitnessGoals = ?, + activityLevel = ?, + medicalConditions = ?, allergies = ?, injuries = ?, - updated_at = ? - WHERE user_id = ?` + exerciseHabits = ?, + dietHabits = ?, + updatedAt = ? + WHERE userId = ?` ).run( height || null, weight || null, age || null, gender || null, - fitnessGoal || null, + fitnessGoalsJson, activityLevel || null, medicalConditions || null, allergies || null, injuries || null, + exerciseHabits || null, + dietHabits || null, now, userId ); return NextResponse.json({ message: "Fitness profile updated successfully", - profileId: existingProfile.id, + userId: userId, }); } else { // Create new profile - const profileId = `fp_${randomBytes(16).toString("hex")}`; - db.prepare( `INSERT INTO fitness_profiles - (id, user_id, height, weight, age, gender, fitness_goal, activity_level, - medical_conditions, allergies, injuries, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + (userId, height, weight, age, gender, fitnessGoals, activityLevel, + medicalConditions, allergies, injuries, exerciseHabits, dietHabits, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run( - profileId, userId, height || null, weight || null, age || null, gender || null, - fitnessGoal || null, + fitnessGoalsJson, activityLevel || null, medicalConditions || null, allergies || null, injuries || null, + exerciseHabits || null, + dietHabits || null, now, now ); @@ -120,7 +136,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { message: "Fitness profile created successfully", - profileId, + userId, }, { status: 201 } ); diff --git a/apps/admin/src/app/api/users/route.ts b/apps/admin/src/app/api/users/route.ts index aa56c85..c92c772 100644 --- a/apps/admin/src/app/api/users/route.ts +++ b/apps/admin/src/app/api/users/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getDatabase } from "../../../lib/database/index"; import bcrypt from "bcryptjs"; -import { auth } from "@clerk/nextjs/server"; +import { auth, clerkClient } from "@clerk/nextjs/server"; export async function GET(request: NextRequest) { try { @@ -64,9 +64,9 @@ export async function POST(request: NextRequest) { } const body = await request.json(); - const { email, password, firstName, lastName, role, phone } = body; + const { email, firstName, lastName, role, phone } = body; - if (!email || !password || !firstName || !lastName || !role) { + if (!email || !firstName || !lastName || !role) { return NextResponse.json( { error: "Missing required fields" }, { status: 400 }, @@ -75,7 +75,7 @@ export async function POST(request: NextRequest) { // Enforce Hierarchy const allowed = { - superAdmin: ["admin", "trainer", "client"], // Super Admin can create anyone (except maybe another superAdmin via this UI?) + superAdmin: ["admin", "trainer", "client"], admin: ["trainer", "client"], trainer: ["client"], client: [] @@ -89,7 +89,7 @@ export async function POST(request: NextRequest) { ); } - // Check if user already exists + // Check if user already exists locally const existingUser = await db.getUserByEmail(email); if (existingUser) { return NextResponse.json( @@ -98,13 +98,38 @@ export async function POST(request: NextRequest) { ); } - // Hash password - const hashedPassword = await bcrypt.hash(password, 12); + // Create Clerk Invitation + // Note: We pass the role in publicMetadata so it persists when they sign up + try { + const client = await clerkClient(); + await client.invitations.createInvitation({ + emailAddress: email, + publicMetadata: { + role, + }, + ignoreExisting: true // Don't fail if invite exists + }); + } catch (clerkError: any) { + console.error("Clerk invitation error:", clerkError); + // If user already exists in Clerk, we might want to handle it. + // But for now, let's proceed to create local record if invite sent or if they exist. + if (clerkError.errors?.[0]?.code === 'form_identifier_exists') { + return NextResponse.json( + { error: "User already exists in Clerk system" }, + { status: 409 }, + ); + } + return NextResponse.json( + { error: "Failed to send invitation: " + (clerkError.message || "Unknown error") }, + { status: 500 }, + ); + } - // Create user + // Create user in local DB with temporary ID (will be migrated on first login) + // We set a placeholder password since it's required by schema but won't be used const newUserId = await db.createUser({ email, - password: hashedPassword, + password: "INVITED_USER_PENDING", firstName, lastName, role, @@ -121,7 +146,7 @@ export async function POST(request: NextRequest) { }); } - return NextResponse.json({ userId: newUserId.id }, { status: 201 }); + return NextResponse.json({ userId: newUserId.id, message: "Invitation sent" }, { status: 201 }); } catch (error) { console.error("Create user error:", error); return NextResponse.json( diff --git a/apps/admin/src/components/users/UserManagement.tsx b/apps/admin/src/components/users/UserManagement.tsx index 7aa62b4..f4fd447 100644 --- a/apps/admin/src/components/users/UserManagement.tsx +++ b/apps/admin/src/components/users/UserManagement.tsx @@ -139,23 +139,44 @@ export function UserManagement() { }; const handleSaveEdit = async () => { - if (!editForm || !selectedUser) return; + if (!editForm) return; try { - const response = await fetch("/api/users", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ id: selectedUser.id, ...editForm }), - }); - if (response.ok) { - setIsEditing(false); - setEditForm(null); - fetchUsers(); + if (selectedUser) { + // Update existing user + const response = await fetch("/api/users", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: selectedUser.id, ...editForm }), + }); + if (response.ok) { + setIsEditing(false); + setEditForm(null); + fetchUsers(); + } else { + alert("Error updating user"); + } } else { - alert("Error updating user"); + // Create (Invite) new user + const response = await fetch("/api/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(editForm), + }); + + if (response.ok) { + setIsEditing(false); + setEditForm(null); + fetchUsers(); + alert("Invitation sent successfully!"); + } else { + const errorData = await response.json(); + alert(`Error sending invitation: ${errorData.error}`); + } } } catch (error) { console.error(error); + alert("An unexpected error occurred"); } }; @@ -196,6 +217,22 @@ export function UserManagement() { > Edit User + diff --git a/apps/admin/src/lib/database/sqlite.ts b/apps/admin/src/lib/database/sqlite.ts index ca64116..f7ed088 100644 --- a/apps/admin/src/lib/database/sqlite.ts +++ b/apps/admin/src/lib/database/sqlite.ts @@ -91,6 +91,8 @@ export class SQLiteDatabase implements IDatabase { exerciseHabits TEXT, dietHabits TEXT, medicalConditions TEXT, + allergies TEXT, + injuries TEXT, createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL, FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE @@ -198,6 +200,7 @@ export class SQLiteDatabase implements IDatabase { async deleteUser(id: string): Promise { if (!this.db) throw new Error('Database not connected') + const stmt = this.db.prepare('DELETE FROM users WHERE id = ?') const result = stmt.run(id) return (result.changes || 0) > 0 } @@ -298,15 +301,15 @@ export class SQLiteDatabase implements IDatabase { const stmt = this.db.prepare( `INSERT INTO fitness_profiles (userId, height, weight, age, gender, activityLevel, fitnessGoals, - exerciseHabits, dietHabits, medicalConditions, createdAt, updatedAt) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + exerciseHabits, dietHabits, medicalConditions, allergies, injuries, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) stmt.run( profile.userId, profile.height, profile.weight, profile.age, profile.gender, profile.activityLevel, JSON.stringify(profile.fitnessGoals), profile.exerciseHabits, - profile.dietHabits, profile.medicalConditions, profile.createdAt.toISOString(), - profile.updatedAt.toISOString() + profile.dietHabits, profile.medicalConditions, profile.allergies, profile.injuries, + profile.createdAt.toISOString(), profile.updatedAt.toISOString() ) return profile @@ -468,6 +471,8 @@ export class SQLiteDatabase implements IDatabase { exerciseHabits: row.exerciseHabits, dietHabits: row.dietHabits, medicalConditions: row.medicalConditions, + allergies: row.allergies, + injuries: row.injuries, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt) } diff --git a/apps/admin/src/lib/database/types.ts b/apps/admin/src/lib/database/types.ts index d97db62..4c66185 100644 --- a/apps/admin/src/lib/database/types.ts +++ b/apps/admin/src/lib/database/types.ts @@ -31,6 +31,8 @@ export interface FitnessProfile { exerciseHabits: string; dietHabits: string; medicalConditions: string; + allergies?: string; + injuries?: string; createdAt: Date; updatedAt: Date; } diff --git a/apps/mobile/src/config/api.ts b/apps/mobile/src/config/api.ts index 0ecc78c..646a78f 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://5cb23f31d8c1.ngrok-free.app' + ? 'https://390dfd6ece05.ngrok-free.app' : 'https://your-production-url.com' export const API_ENDPOINTS = {