invite and saving fitness profile
added, need polishing
This commit is contained in:
parent
daaf5aa2dd
commit
118efad70f
Binary file not shown.
58
apps/admin/package-lock.json
generated
58
apps/admin/package-lock.json
generated
@ -22,6 +22,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",
|
||||
@ -112,6 +113,7 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@ -2396,7 +2398,6 @@
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@ -2417,7 +2418,6 @@
|
||||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
@ -2427,8 +2427,7 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom": {
|
||||
"version": "6.9.1",
|
||||
@ -2504,8 +2503,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@ -2689,6 +2687,7 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@ -2699,6 +2698,7 @@
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@ -2789,6 +2789,7 @@
|
||||
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.3",
|
||||
"@typescript-eslint/types": "8.46.3",
|
||||
@ -3333,6 +3334,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -3988,6 +3990,17 @@
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
||||
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@ -4062,6 +4075,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@ -5296,6 +5310,7 @@
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@ -5481,6 +5496,7 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@ -8324,18 +8340,6 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
@ -8563,7 +8567,6 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@ -9076,6 +9079,7 @@
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.1.tgz",
|
||||
"integrity": "sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@next/env": "16.0.1",
|
||||
"@swc/helpers": "0.5.15",
|
||||
@ -9744,6 +9748,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
@ -9881,7 +9886,6 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@ -9897,7 +9901,6 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=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"
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
33
apps/admin/scripts/migrate-fitness-profile.js
Normal file
33
apps/admin/scripts/migrate-fitness-profile.js
Normal file
@ -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();
|
||||
}
|
||||
@ -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 }
|
||||
);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setEditForm({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
role: "client",
|
||||
phone: "",
|
||||
});
|
||||
setSelectedUser(null);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
>
|
||||
Invite User
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleDeleteUser}
|
||||
@ -265,7 +302,7 @@ export function UserManagement() {
|
||||
{isEditing && editForm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white p-6 rounded-lg shadow-lg max-w-md w-full">
|
||||
<h3 className="text-lg font-semibold mb-4">Edit User</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">{selectedUser ? 'Edit User' : 'Invite New User'}</h3>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@ -310,6 +347,7 @@ export function UserManagement() {
|
||||
}
|
||||
className="w-full border border-gray-300 rounded px-3 py-2"
|
||||
required
|
||||
disabled={!!selectedUser} // Disable email edit for existing users if desired, or keep enabled
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
@ -360,7 +398,7 @@ export function UserManagement() {
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Save
|
||||
{selectedUser ? 'Save Changes' : 'Send Invitation'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -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<boolean> {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -31,6 +31,8 @@ export interface FitnessProfile {
|
||||
exerciseHabits: string;
|
||||
dietHabits: string;
|
||||
medicalConditions: string;
|
||||
allergies?: string;
|
||||
injuries?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user