phase 1-6 implemented
This commit is contained in:
1 parent
22c274bb83
commit
2c4927478a
73 files changed
+9702
-2354
No files matched your search
@@ -1,31 +1,32 @@
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getDatabase } from '@/lib/database'
|
||||
import { ensureUserSynced } from '@/lib/sync-user'
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import { ensureUserSynced } from "@/lib/sync-user";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { userId } = await auth()
|
||||
if (!userId) return new NextResponse('Unauthorized', { status: 401 })
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return new NextResponse("Unauthorized", { status: 401 });
|
||||
|
||||
const db = await getDatabase()
|
||||
const db = await getDatabase();
|
||||
|
||||
// Ensure user exists in DB (sync from Clerk if needed)
|
||||
await ensureUserSynced(userId, db)
|
||||
// Ensure user exists in DB (sync from Clerk if needed)
|
||||
await ensureUserSynced(userId, db);
|
||||
|
||||
// Check if already checked in
|
||||
const activeCheckIn = await db.getActiveCheckIn(userId)
|
||||
if (activeCheckIn) {
|
||||
return new NextResponse('Already checked in', { status: 400 })
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { type = 'gym', notes } = body
|
||||
|
||||
const attendance = await db.checkIn(userId, type, notes)
|
||||
return NextResponse.json(attendance)
|
||||
} catch (error) {
|
||||
console.error('Check-in error:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
// Check if already checked in
|
||||
const activeCheckIn = await db.getActiveCheckIn(userId);
|
||||
if (activeCheckIn) {
|
||||
return new NextResponse("Already checked in", { status: 400 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { type = "gym", notes } = body;
|
||||
|
||||
const attendance = await db.checkIn(userId, type, notes);
|
||||
return NextResponse.json(attendance);
|
||||
} catch (error) {
|
||||
log.error("Failed to check in", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getDatabase } from '@/lib/database'
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { userId } = await auth()
|
||||
if (!userId) return new NextResponse('Unauthorized', { status: 401 })
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return new NextResponse("Unauthorized", { status: 401 });
|
||||
|
||||
const db = await getDatabase()
|
||||
const db = await getDatabase();
|
||||
|
||||
const activeCheckIn = await db.getActiveCheckIn(userId)
|
||||
if (!activeCheckIn) {
|
||||
return new NextResponse('No active check-in found', { status: 404 })
|
||||
}
|
||||
|
||||
const attendance = await db.checkOut(activeCheckIn.id)
|
||||
return NextResponse.json(attendance)
|
||||
} catch (error) {
|
||||
console.error('Check-out error:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
const activeCheckIn = await db.getActiveCheckIn(userId);
|
||||
if (!activeCheckIn) {
|
||||
return new NextResponse("No active check-in found", { status: 404 });
|
||||
}
|
||||
|
||||
const attendance = await db.checkOut(activeCheckIn.id);
|
||||
return NextResponse.json(attendance);
|
||||
} catch (error) {
|
||||
log.error("Failed to check out", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,33 @@
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getDatabase } from '@/lib/database'
|
||||
import { ensureUserSynced } from '@/lib/sync-user'
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import { ensureUserSynced } from "@/lib/sync-user";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
console.log('GET /api/attendance/history called')
|
||||
console.log('Headers:', Object.fromEntries(req.headers.entries()))
|
||||
log.apiRequest("GET", "/api/attendance/history", {
|
||||
headers: Object.fromEntries(req.headers.entries()),
|
||||
});
|
||||
|
||||
try {
|
||||
const authResult = await auth()
|
||||
const { userId } = authResult
|
||||
console.log('Auth result:', JSON.stringify(authResult, null, 2))
|
||||
try {
|
||||
const authResult = await auth();
|
||||
const { userId } = authResult;
|
||||
log.debug("Auth result received", { authResult });
|
||||
|
||||
if (!userId) {
|
||||
console.log('No userId found in auth result')
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
|
||||
const db = await getDatabase()
|
||||
|
||||
// Ensure user exists in DB (sync from Clerk if needed)
|
||||
await ensureUserSynced(userId, db)
|
||||
|
||||
const history = await db.getAttendanceHistory(userId)
|
||||
return NextResponse.json(history)
|
||||
} catch (error) {
|
||||
console.error('History error:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
if (!userId) {
|
||||
log.warn("No userId found in auth result");
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
// Ensure user exists in DB (sync from Clerk if needed)
|
||||
await ensureUserSynced(userId, db);
|
||||
|
||||
const history = await db.getAttendanceHistory(userId);
|
||||
return NextResponse.json(history);
|
||||
} catch (error) {
|
||||
log.error("Failed to fetch attendance history", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,49 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { getDatabase } from '../../../../lib/database/index'
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { getDatabase } from "../../../../lib/database/index";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const db = await getDatabase()
|
||||
const { email, password } = await request.json()
|
||||
const db = await getDatabase();
|
||||
const { email, password } = await request.json();
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email and password are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
{ error: "Email and password are required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const user = await db.getUserByEmail(email)
|
||||
const user = await db.getUserByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid credentials' },
|
||||
{ status: 401 }
|
||||
)
|
||||
{ error: "Invalid credentials" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const isValidPassword = await bcrypt.compare(password, user.password)
|
||||
const isValidPassword = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isValidPassword) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid credentials' },
|
||||
{ status: 401 }
|
||||
)
|
||||
{ error: "Invalid credentials" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const { password: _, ...userWithoutPassword } = user
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Login successful',
|
||||
user: userWithoutPassword
|
||||
})
|
||||
message: "Login successful",
|
||||
user: userWithoutPassword,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
log.error("Login failed", error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { getDatabase } from '../../../../lib/database/index'
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { getDatabase } from "../../../../lib/database/index";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const db = await getDatabase()
|
||||
const { email, password, firstName, lastName, phone } = await request.json()
|
||||
const db = await getDatabase();
|
||||
const { email, password, firstName, lastName, phone } =
|
||||
await request.json();
|
||||
|
||||
if (!email || !password || !firstName || !lastName) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
)
|
||||
{ error: "Missing required fields" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const existingUser = await db.getUserByEmail(email)
|
||||
const existingUser = await db.getUserByEmail(email);
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ error: 'User already exists' },
|
||||
{ status: 409 }
|
||||
)
|
||||
{ error: "User already exists" },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const newUser = await db.createUser({
|
||||
email,
|
||||
@@ -31,45 +33,47 @@ export async function POST(request: NextRequest) {
|
||||
lastName,
|
||||
password: hashedPassword,
|
||||
phone,
|
||||
role: 'client'
|
||||
})
|
||||
role: "client",
|
||||
});
|
||||
|
||||
const newClient = await db.createClient({
|
||||
userId: newUser.id,
|
||||
membershipType: 'basic',
|
||||
membershipStatus: 'active',
|
||||
joinDate: new Date()
|
||||
})
|
||||
membershipType: "basic",
|
||||
membershipStatus: "active",
|
||||
joinDate: new Date(),
|
||||
});
|
||||
|
||||
const { password: _, ...userWithoutPassword } = newUser
|
||||
const { password: _, ...userWithoutPassword } = newUser;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'User registered successfully',
|
||||
user: { ...userWithoutPassword, client: newClient }
|
||||
{
|
||||
message: "User registered successfully",
|
||||
user: { ...userWithoutPassword, client: newClient },
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error)
|
||||
log.error("User registration failed", error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const db = await getDatabase()
|
||||
const allUsers = await db.getAllUsers()
|
||||
const usersWithoutPassword = allUsers.map(({ password: _, ...user }) => user)
|
||||
return NextResponse.json({ users: usersWithoutPassword })
|
||||
const db = await getDatabase();
|
||||
const allUsers = await db.getAllUsers();
|
||||
const usersWithoutPassword = allUsers.map(
|
||||
({ password: _, ...user }) => user,
|
||||
);
|
||||
return NextResponse.json({ users: usersWithoutPassword });
|
||||
} catch (error) {
|
||||
console.error('Get users error:', error)
|
||||
log.error("Failed to get users", error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
// Helper to add CORS headers
|
||||
function corsHeaders() {
|
||||
@@ -52,7 +53,7 @@ export async function GET(
|
||||
|
||||
return NextResponse.json(goal, { headers: corsHeaders() });
|
||||
} catch (error) {
|
||||
console.error("Error fetching fitness goal:", error);
|
||||
log.error("Failed to fetch fitness goal", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500, headers: corsHeaders() },
|
||||
@@ -103,7 +104,7 @@ export async function PUT(
|
||||
|
||||
return NextResponse.json(updatedGoal, { headers: corsHeaders() });
|
||||
} catch (error) {
|
||||
console.error("Error updating fitness goal:", error);
|
||||
log.error("Failed to update fitness goal", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500, headers: corsHeaders() },
|
||||
@@ -154,7 +155,7 @@ export async function DELETE(
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting fitness goal:", error);
|
||||
log.error("Failed to delete fitness goal", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500, headers: corsHeaders() },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import { randomBytes } from "crypto";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
// Helper to add CORS headers
|
||||
function corsHeaders() {
|
||||
@@ -33,18 +34,16 @@ export async function GET(req: NextRequest) {
|
||||
const userId = await getAuthenticatedUserId(req);
|
||||
|
||||
if (!userId) {
|
||||
console.error("[Fitness Goals API] Authentication failed");
|
||||
console.error(
|
||||
"[Fitness Goals API] Headers:",
|
||||
Object.fromEntries(req.headers.entries()),
|
||||
);
|
||||
log.error("Fitness goals GET - authentication failed", undefined, {
|
||||
headers: Object.fromEntries(req.headers.entries()),
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized" },
|
||||
{ status: 401, headers: corsHeaders() },
|
||||
);
|
||||
}
|
||||
|
||||
console.log("[Fitness Goals API] Authenticated user:", userId);
|
||||
log.debug("Fitness goals GET - authenticated user", { userId });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const targetUserId = searchParams.get("userId") || userId;
|
||||
@@ -58,12 +57,13 @@ export async function GET(req: NextRequest) {
|
||||
status && status !== "all" ? status : undefined,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[Fitness Goals API] Found ${goals.length} goals for user ${targetUserId}`,
|
||||
);
|
||||
log.debug("Fitness goals retrieved", {
|
||||
count: goals.length,
|
||||
userId: targetUserId,
|
||||
});
|
||||
return NextResponse.json(goals, { headers: corsHeaders() });
|
||||
} catch (error) {
|
||||
console.error("[Fitness Goals API] Error:", error);
|
||||
log.error("Failed to get fitness goals", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500, headers: corsHeaders() },
|
||||
@@ -77,7 +77,7 @@ export async function POST(req: NextRequest) {
|
||||
const userId = await getAuthenticatedUserId(req);
|
||||
|
||||
if (!userId) {
|
||||
console.error("[Fitness Goals API] Authentication failed for POST");
|
||||
log.error("Fitness goals POST - authentication failed");
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized" },
|
||||
{ status: 401, headers: corsHeaders() },
|
||||
@@ -127,12 +127,10 @@ export async function POST(req: NextRequest) {
|
||||
notes: notes || undefined,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[Fitness Goals API] Created goal ${goal.id} for user ${userId}`,
|
||||
);
|
||||
log.info("Fitness goal created", { goalId: goal.id, userId });
|
||||
return NextResponse.json(goal, { status: 201, headers: corsHeaders() });
|
||||
} catch (error) {
|
||||
console.error("[Fitness Goals API] Error creating goal:", error);
|
||||
log.error("Failed to create fitness goal", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500, headers: corsHeaders() },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import Database from "better-sqlite3";
|
||||
import { randomBytes } from "crypto";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
const db = new Database("./data/fitai.db");
|
||||
|
||||
@@ -15,16 +16,14 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
const profile = db
|
||||
.prepare(
|
||||
`SELECT * FROM fitness_profiles WHERE userId = ?`
|
||||
)
|
||||
.prepare(`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 || '[]');
|
||||
p.fitnessGoals = JSON.parse(p.fitnessGoals || "[]");
|
||||
} catch (e) {
|
||||
p.fitnessGoals = [];
|
||||
}
|
||||
@@ -32,10 +31,10 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({ profile: profile || null });
|
||||
} catch (error) {
|
||||
console.error("Error fetching fitness profile:", error);
|
||||
log.error("Failed to fetch fitness profile", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch fitness profile" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -61,7 +60,7 @@ export async function POST(request: NextRequest) {
|
||||
allergies,
|
||||
injuries,
|
||||
exerciseHabits,
|
||||
dietHabits
|
||||
dietHabits,
|
||||
} = body;
|
||||
|
||||
// Check if profile exists
|
||||
@@ -88,7 +87,7 @@ export async function POST(request: NextRequest) {
|
||||
exerciseHabits = ?,
|
||||
dietHabits = ?,
|
||||
updatedAt = ?
|
||||
WHERE userId = ?`
|
||||
WHERE userId = ?`,
|
||||
).run(
|
||||
height || null,
|
||||
weight || null,
|
||||
@@ -102,7 +101,7 @@ export async function POST(request: NextRequest) {
|
||||
exerciseHabits || null,
|
||||
dietHabits || null,
|
||||
now,
|
||||
userId
|
||||
userId,
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -115,7 +114,7 @@ export async function POST(request: NextRequest) {
|
||||
`INSERT INTO fitness_profiles
|
||||
(userId, height, weight, age, gender, fitnessGoals, activityLevel,
|
||||
medicalConditions, allergies, injuries, exerciseHabits, dietHabits, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
userId,
|
||||
height || null,
|
||||
@@ -130,7 +129,7 @@ export async function POST(request: NextRequest) {
|
||||
exerciseHabits || null,
|
||||
dietHabits || null,
|
||||
now,
|
||||
now
|
||||
now,
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
@@ -138,14 +137,14 @@ export async function POST(request: NextRequest) {
|
||||
message: "Fitness profile created successfully",
|
||||
userId,
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving fitness profile:", error);
|
||||
log.error("Failed to save fitness profile", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to save fitness profile" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { auth } from "@clerk/nextjs/server";
|
||||
import { eq, sql } from "@fitai/database";
|
||||
import { db, users as usersTable } from "@fitai/database";
|
||||
import { ensureUserSynced } from "@/lib/sync-user";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
async function ensureGymsTable() {
|
||||
await db.run(sql`
|
||||
@@ -29,7 +30,7 @@ export async function GET() {
|
||||
|
||||
return NextResponse.json(rows);
|
||||
} catch (error) {
|
||||
console.error("GET /gyms error:", error);
|
||||
log.error("Failed to get gyms", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -160,7 +161,7 @@ export async function POST(req: Request) {
|
||||
const created = await db.get(sql`SELECT * FROM gyms WHERE id = ${id}`);
|
||||
return NextResponse.json(created, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("POST /gyms error:", error);
|
||||
log.error("Failed to create gym", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +1,82 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getDatabase } from '../../../../lib/database/index'
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDatabase } from "../../../../lib/database/index";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const db = await getDatabase()
|
||||
const profileData = await request.json()
|
||||
const db = await getDatabase();
|
||||
const profileData = await request.json();
|
||||
|
||||
if (!profileData.userId || !profileData.height || !profileData.weight || !profileData.age) {
|
||||
if (
|
||||
!profileData.userId ||
|
||||
!profileData.height ||
|
||||
!profileData.weight ||
|
||||
!profileData.age
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
)
|
||||
{ error: "Missing required fields" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user exists
|
||||
const user = await db.getUserById(profileData.userId)
|
||||
const user = await db.getUserById(profileData.userId);
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'User not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if profile already exists
|
||||
const existingProfile = await db.getFitnessProfileByUserId(profileData.userId)
|
||||
|
||||
let profile
|
||||
const existingProfile = await db.getFitnessProfileByUserId(
|
||||
profileData.userId,
|
||||
);
|
||||
|
||||
let profile;
|
||||
if (existingProfile) {
|
||||
profile = await db.updateFitnessProfile(profileData.userId, profileData)
|
||||
profile = await db.updateFitnessProfile(profileData.userId, profileData);
|
||||
} else {
|
||||
profile = await db.createFitnessProfile(profileData)
|
||||
profile = await db.createFitnessProfile(profileData);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Fitness profile saved successfully',
|
||||
profile
|
||||
{
|
||||
message: "Fitness profile saved successfully",
|
||||
profile,
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Fitness profile error:', error)
|
||||
log.error("Failed to save fitness profile", error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const db = await getDatabase()
|
||||
const { searchParams } = new URL(request.url)
|
||||
const userId = searchParams.get('userId')
|
||||
const db = await getDatabase();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const userId = searchParams.get("userId");
|
||||
|
||||
if (userId) {
|
||||
const profile = await db.getFitnessProfileByUserId(userId)
|
||||
const profile = await db.getFitnessProfileByUserId(userId);
|
||||
if (!profile) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Profile not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
{ error: "Profile not found" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ profile })
|
||||
return NextResponse.json({ profile });
|
||||
}
|
||||
|
||||
const profiles = await db.getAllFitnessProfiles()
|
||||
return NextResponse.json({ profiles })
|
||||
const profiles = await db.getAllFitnessProfiles();
|
||||
return NextResponse.json({ profiles });
|
||||
} catch (error) {
|
||||
console.error('Get fitness profiles error:', error)
|
||||
log.error("Failed to get fitness profiles", error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,190 +2,220 @@ 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";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { userId, useExternalModel } = await req.json();
|
||||
try {
|
||||
const { userId, useExternalModel } = await req.json();
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
// Fetch fitness profile
|
||||
const profile = await db.getFitnessProfileByUserId(userId);
|
||||
|
||||
if (!profile) {
|
||||
return NextResponse.json(
|
||||
{ error: "Fitness profile not found for this user" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
if (useExternalModel) {
|
||||
// Use DeepSeek AI
|
||||
const deepseekApiKey = process.env.DEEPSEEK_API_KEY;
|
||||
|
||||
if (!deepseekApiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: "DeepSeek API key not configured" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Using DeepSeek AI model...");
|
||||
|
||||
const deepseekResponse = await fetch("https://api.deepseek.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${deepseekApiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "deepseek-chat",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "You are a professional fitness trainer and nutritionist. Always respond with valid JSON only, no markdown or code blocks."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!deepseekResponse.ok) {
|
||||
const errorText = await deepseekResponse.text();
|
||||
console.error("DeepSeek API error:", errorText);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to generate recommendation from DeepSeek AI" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const deepseekData = await deepseekResponse.json();
|
||||
console.log("Raw DeepSeek Response:", deepseekData);
|
||||
|
||||
try {
|
||||
const content = deepseekData.choices[0].message.content;
|
||||
let cleanResponse = content.trim();
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
if (cleanResponse.startsWith("```json")) {
|
||||
cleanResponse = cleanResponse.replace(/^```json\s*/, "").replace(/\s*```$/, "");
|
||||
} else if (cleanResponse.startsWith("```")) {
|
||||
cleanResponse = cleanResponse.replace(/^```\s*/, "").replace(/\s*```$/, "");
|
||||
}
|
||||
|
||||
// Find the first '{' and last '}' to extract the JSON object
|
||||
const firstBrace = cleanResponse.indexOf("{");
|
||||
const lastBrace = cleanResponse.lastIndexOf("}");
|
||||
|
||||
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
|
||||
}
|
||||
|
||||
parsedResponse = JSON.parse(cleanResponse);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse DeepSeek response:", deepseekData);
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid response format from DeepSeek AI" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Use local Ollama
|
||||
console.log("Using local Ollama model...");
|
||||
|
||||
const ollamaResponse = await fetch("http://localhost:11434/api/generate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "gemma3:latest",
|
||||
prompt: prompt,
|
||||
stream: false,
|
||||
format: "json",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!ollamaResponse.ok) {
|
||||
console.error("Ollama API error:", await ollamaResponse.text());
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to generate recommendation from Ollama" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const aiData = await ollamaResponse.json();
|
||||
console.log("Raw Ollama Response:", aiData.response);
|
||||
|
||||
try {
|
||||
let cleanResponse = aiData.response.trim();
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
if (cleanResponse.startsWith("```json")) {
|
||||
cleanResponse = cleanResponse.replace(/^```json\s*/, "").replace(/\s*```$/, "");
|
||||
} else if (cleanResponse.startsWith("```")) {
|
||||
cleanResponse = cleanResponse.replace(/^```\s*/, "").replace(/\s*```$/, "");
|
||||
}
|
||||
|
||||
// Find the first '{' and last '}' to extract the JSON object
|
||||
const firstBrace = cleanResponse.indexOf("{");
|
||||
const lastBrace = cleanResponse.lastIndexOf("}");
|
||||
|
||||
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
|
||||
}
|
||||
|
||||
parsedResponse = JSON.parse(cleanResponse);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse Ollama response:", aiData.response);
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid response format from Ollama" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const recommendation = await db.createRecommendation({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
fitnessProfileId: profile.id,
|
||||
type: 'ai_plan',
|
||||
recommendationText: parsedResponse.recommendationText,
|
||||
activityPlan: parsedResponse.activityPlan,
|
||||
dietPlan: parsedResponse.dietPlan,
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
return NextResponse.json(recommendation);
|
||||
} catch (error) {
|
||||
console.error("Error generating recommendation:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "User ID is required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
// Fetch fitness profile
|
||||
const profile = await db.getFitnessProfileByUserId(userId);
|
||||
|
||||
if (!profile) {
|
||||
return NextResponse.json(
|
||||
{ error: "Fitness profile not found for this user" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Build AI context with goals and recommendations
|
||||
let prompt: string;
|
||||
try {
|
||||
const context = await buildAIContext(userId);
|
||||
prompt = buildEnhancedPrompt(context);
|
||||
log.debug("Using enhanced AI context with goals and history");
|
||||
} catch (error) {
|
||||
// Fallback to basic prompt if context building fails
|
||||
log.warn("Failed to build AI context, using basic prompt", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
prompt = buildBasicPrompt(profile);
|
||||
}
|
||||
|
||||
let parsedResponse;
|
||||
|
||||
if (useExternalModel) {
|
||||
// Use DeepSeek AI
|
||||
const deepseekApiKey = process.env.DEEPSEEK_API_KEY;
|
||||
|
||||
if (!deepseekApiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: "DeepSeek API key not configured" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
log.debug("Using DeepSeek AI model", { userId });
|
||||
|
||||
const deepseekResponse = await fetch(
|
||||
"https://api.deepseek.com/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${deepseekApiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "deepseek-chat",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"You are a professional fitness trainer and nutritionist. Always respond with valid JSON only, no markdown or code blocks.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!deepseekResponse.ok) {
|
||||
const errorText = await deepseekResponse.text();
|
||||
log.error("DeepSeek API request failed", new Error(errorText), {
|
||||
status: deepseekResponse.status,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to generate recommendation from DeepSeek AI" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const deepseekData = await deepseekResponse.json();
|
||||
log.debug("Received DeepSeek response", { deepseekData });
|
||||
|
||||
try {
|
||||
const content = deepseekData.choices[0].message.content;
|
||||
let cleanResponse = content.trim();
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
if (cleanResponse.startsWith("```json")) {
|
||||
cleanResponse = cleanResponse
|
||||
.replace(/^```json\s*/, "")
|
||||
.replace(/\s*```$/, "");
|
||||
} else if (cleanResponse.startsWith("```")) {
|
||||
cleanResponse = cleanResponse
|
||||
.replace(/^```\s*/, "")
|
||||
.replace(/\s*```$/, "");
|
||||
}
|
||||
|
||||
// Find the first '{' and last '}' to extract the JSON object
|
||||
const firstBrace = cleanResponse.indexOf("{");
|
||||
const lastBrace = cleanResponse.lastIndexOf("}");
|
||||
|
||||
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
|
||||
}
|
||||
|
||||
parsedResponse = JSON.parse(cleanResponse);
|
||||
} catch (e) {
|
||||
log.error("Failed to parse DeepSeek response", e, {
|
||||
response: deepseekData,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid response format from DeepSeek AI" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Use local Ollama
|
||||
log.debug("Using local Ollama model", { userId });
|
||||
|
||||
const ollamaResponse = await fetch(
|
||||
"http://localhost:11434/api/generate",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "gemma3:latest",
|
||||
prompt: prompt,
|
||||
stream: false,
|
||||
format: "json",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!ollamaResponse.ok) {
|
||||
const errorText = await ollamaResponse.text();
|
||||
log.error("Ollama API request failed", new Error(errorText), {
|
||||
status: ollamaResponse.status,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to generate recommendation from Ollama" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const aiData = await ollamaResponse.json();
|
||||
log.debug("Received Ollama response", { response: aiData.response });
|
||||
|
||||
try {
|
||||
let cleanResponse = aiData.response.trim();
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
if (cleanResponse.startsWith("```json")) {
|
||||
cleanResponse = cleanResponse
|
||||
.replace(/^```json\s*/, "")
|
||||
.replace(/\s*```$/, "");
|
||||
} else if (cleanResponse.startsWith("```")) {
|
||||
cleanResponse = cleanResponse
|
||||
.replace(/^```\s*/, "")
|
||||
.replace(/\s*```$/, "");
|
||||
}
|
||||
|
||||
// Find the first '{' and last '}' to extract the JSON object
|
||||
const firstBrace = cleanResponse.indexOf("{");
|
||||
const lastBrace = cleanResponse.lastIndexOf("}");
|
||||
|
||||
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
|
||||
}
|
||||
|
||||
parsedResponse = JSON.parse(cleanResponse);
|
||||
} catch (e) {
|
||||
log.error("Failed to parse Ollama response", e, {
|
||||
response: aiData.response,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid response format from Ollama" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const recommendation = await db.createRecommendation({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
fitnessProfileId: profile.id,
|
||||
type: "ai_plan",
|
||||
recommendationText: parsedResponse.recommendationText,
|
||||
activityPlan: parsedResponse.activityPlan,
|
||||
dietPlan: parsedResponse.dietPlan,
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
return NextResponse.json(recommendation);
|
||||
} catch (error) {
|
||||
log.error("Failed to generate recommendation", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +1,157 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { getDatabase } from '@/lib/database'
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { userId: currentUserId } = await auth()
|
||||
if (!currentUserId) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const targetUserId = searchParams.get('userId')
|
||||
|
||||
const db = await getDatabase()
|
||||
|
||||
// If no userId provided, check if staff and return all recommendations
|
||||
if (!targetUserId) {
|
||||
const currentUser = await db.getUserById(currentUserId)
|
||||
const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer'
|
||||
|
||||
if (!isStaff) {
|
||||
return new NextResponse('User ID is required', { status: 400 })
|
||||
}
|
||||
|
||||
const recommendations = await db.getAllRecommendations()
|
||||
return NextResponse.json({ recommendations })
|
||||
}
|
||||
|
||||
// Check permissions: Users can view their own, Admins/Trainers can view anyone's
|
||||
const currentUser = await db.getUserById(currentUserId)
|
||||
const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer'
|
||||
|
||||
if (currentUserId !== targetUserId) {
|
||||
if (!isStaff) {
|
||||
return new NextResponse('Forbidden', { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
let recommendations = await db.getRecommendationsByUserId(targetUserId)
|
||||
|
||||
// Non-staff users should only see approved recommendations
|
||||
if (!isStaff) {
|
||||
recommendations = recommendations.filter((rec: any) => rec.status === 'approved')
|
||||
}
|
||||
|
||||
return NextResponse.json(recommendations)
|
||||
} catch (error) {
|
||||
console.error('Error fetching recommendations:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
try {
|
||||
const { userId: currentUserId } = await auth();
|
||||
if (!currentUserId) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const targetUserId = searchParams.get("userId");
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
// If no userId provided, check if staff and return all recommendations
|
||||
if (!targetUserId) {
|
||||
const currentUser = await db.getUserById(currentUserId);
|
||||
const isStaff =
|
||||
currentUser?.role === "admin" ||
|
||||
currentUser?.role === "superAdmin" ||
|
||||
currentUser?.role === "trainer";
|
||||
|
||||
if (!isStaff) {
|
||||
return new NextResponse("User ID is required", { status: 400 });
|
||||
}
|
||||
|
||||
const recommendations = await db.getAllRecommendations();
|
||||
return NextResponse.json({ recommendations });
|
||||
}
|
||||
|
||||
// Check permissions: Users can view their own, Admins/Trainers can view anyone's
|
||||
const currentUser = await db.getUserById(currentUserId);
|
||||
const isStaff =
|
||||
currentUser?.role === "admin" ||
|
||||
currentUser?.role === "superAdmin" ||
|
||||
currentUser?.role === "trainer";
|
||||
|
||||
if (currentUserId !== targetUserId) {
|
||||
if (!isStaff) {
|
||||
return new NextResponse("Forbidden", { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
let recommendations = await db.getRecommendationsByUserId(targetUserId);
|
||||
|
||||
// Non-staff users should only see approved recommendations
|
||||
if (!isStaff) {
|
||||
recommendations = recommendations.filter(
|
||||
(rec: any) => rec.status === "approved",
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(recommendations);
|
||||
} catch (error) {
|
||||
console.error("Error fetching recommendations:", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { userId: currentUserId } = await auth()
|
||||
if (!currentUserId) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
|
||||
const db = await getDatabase()
|
||||
const currentUser = await db.getUserById(currentUserId)
|
||||
const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer'
|
||||
|
||||
if (!isStaff) {
|
||||
return new NextResponse('Forbidden', { status: 403 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { userId, fitnessProfileId, recommendationText, activityPlan, dietPlan, status, type, content } = body
|
||||
|
||||
// Handle AI Plan (Legacy/Specific)
|
||||
if (recommendationText && activityPlan && dietPlan && fitnessProfileId) {
|
||||
const recommendation = await db.createRecommendation({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
fitnessProfileId,
|
||||
type: 'ai_plan',
|
||||
recommendationText: recommendationText,
|
||||
activityPlan,
|
||||
dietPlan,
|
||||
status: status || 'pending'
|
||||
})
|
||||
return NextResponse.json(recommendation)
|
||||
}
|
||||
|
||||
// Handle User Goal (Generic)
|
||||
if (type && content) {
|
||||
const recommendation = await db.createRecommendation({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
type,
|
||||
recommendationText: content,
|
||||
status: status || 'pending'
|
||||
})
|
||||
return NextResponse.json(recommendation)
|
||||
}
|
||||
|
||||
return NextResponse.json('Missing required fields', { status: 400 })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating recommendation:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
try {
|
||||
const { userId: currentUserId } = await auth();
|
||||
if (!currentUserId) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
const currentUser = await db.getUserById(currentUserId);
|
||||
const isStaff =
|
||||
currentUser?.role === "admin" ||
|
||||
currentUser?.role === "superAdmin" ||
|
||||
currentUser?.role === "trainer";
|
||||
|
||||
if (!isStaff) {
|
||||
return new NextResponse("Forbidden", { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
userId,
|
||||
fitnessProfileId,
|
||||
recommendationText,
|
||||
activityPlan,
|
||||
dietPlan,
|
||||
status,
|
||||
type,
|
||||
content,
|
||||
} = body;
|
||||
|
||||
// Handle AI Plan (Legacy/Specific)
|
||||
if (recommendationText && activityPlan && dietPlan && fitnessProfileId) {
|
||||
const recommendation = await db.createRecommendation({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
fitnessProfileId,
|
||||
type: "ai_plan",
|
||||
recommendationText: recommendationText,
|
||||
activityPlan,
|
||||
dietPlan,
|
||||
status: status || "pending",
|
||||
});
|
||||
return NextResponse.json(recommendation);
|
||||
}
|
||||
|
||||
// Handle User Goal (Generic)
|
||||
if (type && content) {
|
||||
const recommendation = await db.createRecommendation({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
type,
|
||||
recommendationText: content,
|
||||
status: status || "pending",
|
||||
});
|
||||
return NextResponse.json(recommendation);
|
||||
}
|
||||
|
||||
return NextResponse.json("Missing required fields", { status: 400 });
|
||||
} catch (error) {
|
||||
log.error("Failed to create recommendation", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const { userId: currentUserId } = await auth()
|
||||
if (!currentUserId) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { id, status, recommendationText, activityPlan, dietPlan, content } = body
|
||||
|
||||
if (!id) {
|
||||
return new NextResponse('Recommendation ID is required', { status: 400 })
|
||||
}
|
||||
|
||||
const db = await getDatabase()
|
||||
|
||||
const updated = await db.updateRecommendation(id, {
|
||||
...(status && { status }),
|
||||
...(recommendationText && { recommendationText }),
|
||||
...(content && { recommendationText: content }),
|
||||
...(activityPlan && { activityPlan }),
|
||||
...(dietPlan && { dietPlan })
|
||||
})
|
||||
|
||||
return NextResponse.json(updated)
|
||||
} catch (error) {
|
||||
console.error('Error updating recommendation:', error)
|
||||
return new NextResponse('Internal Server Error', { status: 500 })
|
||||
try {
|
||||
const { userId: currentUserId } = await auth();
|
||||
if (!currentUserId) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { id, status, recommendationText, activityPlan, dietPlan, content } =
|
||||
body;
|
||||
|
||||
if (!id) {
|
||||
return new NextResponse("Recommendation ID is required", { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
const updated = await db.updateRecommendation(id, {
|
||||
...(status && { status }),
|
||||
...(recommendationText && { recommendationText }),
|
||||
...(content && { recommendationText: content }),
|
||||
...(activityPlan && { activityPlan }),
|
||||
...(dietPlan && { dietPlan }),
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
log.error("Failed to update recommendation", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { db, users as usersTable, eq, sql } from "@fitai/database";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
/**
|
||||
* PATCH /api/users/gym
|
||||
@@ -12,7 +13,7 @@ import { db, users as usersTable, eq, sql } from "@fitai/database";
|
||||
export async function PATCH(req: Request) {
|
||||
try {
|
||||
const { userId } = await auth();
|
||||
console.log("PATCH /api/users/gym auth userId:", userId);
|
||||
log.debug("Updating user gym assignment", { userId });
|
||||
if (!userId) return new NextResponse("Unauthorized", { status: 401 });
|
||||
|
||||
const body = await req.json().catch(() => null);
|
||||
@@ -24,32 +25,32 @@ export async function PATCH(req: Request) {
|
||||
}
|
||||
|
||||
const gymId = body.gymId === null ? null : String(body.gymId);
|
||||
console.log("PATCH /api/users/gym parsed gymId from body:", gymId);
|
||||
log.debug("Parsed gym ID from request body", { gymId });
|
||||
|
||||
// Ensure user exists
|
||||
console.log("PATCH /api/users/gym fetching user by id:", userId);
|
||||
log.debug("Fetching user by ID", { userId });
|
||||
const user = await db
|
||||
.select()
|
||||
.from(usersTable)
|
||||
.where(eq(usersTable.id, userId))
|
||||
.get();
|
||||
console.log("PATCH /api/users/gym fetched user:", user);
|
||||
log.debug("User fetched", { user: user ? { id: user.id } : null });
|
||||
if (!user) return new NextResponse("User not found", { status: 404 });
|
||||
|
||||
// Validate gym when provided
|
||||
if (gymId) {
|
||||
console.log("PATCH /api/users/gym validating gym:", gymId);
|
||||
log.debug("Validating gym", { gymId });
|
||||
const rows = await db.all(
|
||||
sql`SELECT status FROM gyms WHERE id = ${gymId} LIMIT 1`,
|
||||
);
|
||||
console.log("PATCH /api/users/gym validation query result rows:", rows);
|
||||
log.debug("Gym validation query result", { rowCount: rows?.length });
|
||||
const gym = rows?.[0] as { status?: string } | undefined;
|
||||
if (!gym) {
|
||||
console.log("PATCH /api/users/gym validation: gym not found");
|
||||
log.warn("Gym not found during validation", { gymId });
|
||||
return NextResponse.json({ error: "Gym not found" }, { status: 404 });
|
||||
}
|
||||
if (gym.status !== "active") {
|
||||
console.log("PATCH /api/users/gym validation: gym not active", gym);
|
||||
log.warn("Gym is not active", { gymId, status: gym.status });
|
||||
return NextResponse.json(
|
||||
{ error: "Gym is not active" },
|
||||
{ status: 400 },
|
||||
@@ -58,24 +59,21 @@ export async function PATCH(req: Request) {
|
||||
}
|
||||
|
||||
// Update user's gym selection
|
||||
console.log("PATCH /api/users/gym updating user gym_id:", {
|
||||
userId,
|
||||
gymId,
|
||||
});
|
||||
log.debug("Updating user gym assignment in database", { userId, gymId });
|
||||
await db.run(
|
||||
sql`UPDATE users SET gym_id = ${gymId ?? null}, updated_at = ${Date.now()} WHERE id = ${userId}`,
|
||||
);
|
||||
console.log("PATCH /api/users/gym update completed");
|
||||
log.debug("User gym assignment updated");
|
||||
|
||||
const updated = await db
|
||||
.select()
|
||||
.from(usersTable)
|
||||
.where(eq(usersTable.id, userId))
|
||||
.get();
|
||||
console.log("PATCH /api/users/gym returning updated user:", updated);
|
||||
log.debug("Returning updated user", { userId });
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
console.error("PATCH /users/gym error:", error);
|
||||
log.error("Failed to update user gym assignment", error);
|
||||
return new NextResponse("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { getDatabase } from "../../../lib/database/index";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { auth, clerkClient } from "@clerk/nextjs/server";
|
||||
import { db as rawDb, sql } from "@fitai/database";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -31,28 +32,25 @@ export async function GET(request: NextRequest) {
|
||||
(g.name as string) || (g.id as string),
|
||||
]),
|
||||
);
|
||||
console.log(
|
||||
"GET /api/users: total users fetched from DB:",
|
||||
Array.isArray(users) ? users.length : 0,
|
||||
);
|
||||
log.debug("Fetched users from database", {
|
||||
totalUsers: Array.isArray(users) ? users.length : 0,
|
||||
});
|
||||
|
||||
if (role) {
|
||||
users = users.filter((user) => user.role === role);
|
||||
}
|
||||
console.log(
|
||||
"GET /api/users: role filter:",
|
||||
log.debug("Applied role filter", {
|
||||
role,
|
||||
"users after filter:",
|
||||
Array.isArray(users) ? users.length : 0,
|
||||
"sample:",
|
||||
users && users[0]
|
||||
? {
|
||||
id: users[0].id,
|
||||
role: users[0].role,
|
||||
gymId: (users as any)[0].gymId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
usersAfterFilter: Array.isArray(users) ? users.length : 0,
|
||||
sample:
|
||||
users && users[0]
|
||||
? {
|
||||
id: users[0].id,
|
||||
role: users[0].role,
|
||||
gymId: (users as any)[0].gymId,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
||||
const usersWithClients = await Promise.all(
|
||||
users.map(async (user) => {
|
||||
@@ -116,21 +114,20 @@ export async function GET(request: NextRequest) {
|
||||
}),
|
||||
);
|
||||
|
||||
console.log(
|
||||
"GET /api/users: responding users count:",
|
||||
Array.isArray(usersWithClients) ? usersWithClients.length : 0,
|
||||
"sample:",
|
||||
usersWithClients && usersWithClients[0]
|
||||
? {
|
||||
id: usersWithClients[0].id,
|
||||
role: usersWithClients[0].role,
|
||||
gymId: (usersWithClients as any)[0].gymId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
log.debug("Prepared response with user details", {
|
||||
usersCount: Array.isArray(usersWithClients) ? usersWithClients.length : 0,
|
||||
sample:
|
||||
usersWithClients && usersWithClients[0]
|
||||
? {
|
||||
id: usersWithClients[0].id,
|
||||
role: usersWithClients[0].role,
|
||||
gymId: (usersWithClients as any)[0].gymId,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
return NextResponse.json({ users: usersWithClients });
|
||||
} catch (error) {
|
||||
console.error("Get users error:", error);
|
||||
log.error("Failed to get users", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
@@ -218,7 +215,11 @@ export async function POST(request: NextRequest) {
|
||||
ignoreExisting: true, // Don't fail if invite exists
|
||||
});
|
||||
} catch (clerkError: any) {
|
||||
console.error("Clerk invitation error:", clerkError);
|
||||
log.error("Clerk invitation failed", clerkError, {
|
||||
email,
|
||||
role,
|
||||
errorCode: clerkError.errors?.[0]?.code,
|
||||
});
|
||||
// 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") {
|
||||
@@ -263,7 +264,7 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Create user error:", error);
|
||||
log.error("Failed to create user", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
@@ -276,7 +277,7 @@ export async function PUT(request: NextRequest) {
|
||||
const db = await getDatabase();
|
||||
const body = await request.json();
|
||||
const { id, email, firstName, lastName, role, phone, gymId } = body;
|
||||
console.log("PUT /api/users received body:", {
|
||||
log.debug("Updating user", {
|
||||
id,
|
||||
email,
|
||||
firstName,
|
||||
@@ -346,7 +347,7 @@ export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const client = await clerkClient();
|
||||
const publicMetadata: Record<string, unknown> = {};
|
||||
console.log("PUT /api/users preparing Clerk metadata update:", {
|
||||
log.debug("Preparing Clerk metadata update", {
|
||||
targetUserId: id,
|
||||
role,
|
||||
gymId,
|
||||
@@ -360,23 +361,20 @@ export async function PUT(request: NextRequest) {
|
||||
}
|
||||
|
||||
if (Object.keys(publicMetadata).length > 0) {
|
||||
console.log(
|
||||
"PUT /api/users calling Clerk updateUser with metadata:",
|
||||
publicMetadata,
|
||||
);
|
||||
log.debug("Updating Clerk user metadata", { publicMetadata });
|
||||
const clerkResult = await client.users.updateUser(id, {
|
||||
publicMetadata,
|
||||
});
|
||||
console.log("PUT /api/users Clerk updateUser result:", {
|
||||
log.debug("Clerk user updated successfully", {
|
||||
id: clerkResult.id,
|
||||
role: clerkResult.publicMetadata?.role,
|
||||
gymId: clerkResult.publicMetadata?.gymId,
|
||||
});
|
||||
} else {
|
||||
console.log("PUT /api/users no Clerk metadata changes requested");
|
||||
log.debug("No Clerk metadata changes requested");
|
||||
}
|
||||
} catch (clerkErr: any) {
|
||||
console.error("Clerk metadata update error:", clerkErr);
|
||||
log.error("Clerk metadata update failed", clerkErr, { userId: id });
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update role/gym in identity provider" },
|
||||
{ status: 500 },
|
||||
@@ -384,9 +382,7 @@ export async function PUT(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Update local DB for immediate UI feedback (webhook will also sync)
|
||||
console.log(
|
||||
"PUT /api/users raw SQL updating local DB user gym_id and fields",
|
||||
);
|
||||
log.debug("Updating local database user");
|
||||
await rawDb.run(
|
||||
sql`UPDATE users
|
||||
SET email = ${email ?? existingUser.email},
|
||||
@@ -402,7 +398,7 @@ export async function PUT(request: NextRequest) {
|
||||
const updatedRow = await rawDb.get(
|
||||
sql`SELECT id, email, first_name, last_name, role, phone, gym_id, created_at, updated_at FROM users WHERE id = ${id}`,
|
||||
);
|
||||
console.log("PUT /api/users raw DB row after update:", updatedRow);
|
||||
log.debug("User updated in database", { updatedRow });
|
||||
|
||||
const updatedUser = {
|
||||
...existingUser,
|
||||
@@ -418,11 +414,11 @@ export async function PUT(request: NextRequest) {
|
||||
? gymId
|
||||
: existingUser.gymId,
|
||||
};
|
||||
console.log("PUT /api/users responding with updated user:", updatedUser);
|
||||
log.debug("Sending updated user response", { updatedUser });
|
||||
|
||||
return NextResponse.json({ user: updatedUser });
|
||||
} catch (error) {
|
||||
console.error("Update user error:", error);
|
||||
log.error("Failed to update user", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
@@ -457,7 +453,7 @@ export async function DELETE(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Delete user error:", error);
|
||||
log.error("Failed to delete user(s)", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
|
||||
@@ -4,6 +4,7 @@ import { WebhookEvent } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
// Get the webhook secret from environment variables
|
||||
@@ -46,13 +47,16 @@ export async function POST(req: Request) {
|
||||
"svix-signature": svix_signature,
|
||||
}) as WebhookEvent;
|
||||
} catch (err) {
|
||||
console.error("Error verifying webhook:", err);
|
||||
log.error("Webhook verification failed", err);
|
||||
return NextResponse.json({ error: "Verification failed" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Handle the webhook
|
||||
const eventType = evt.type;
|
||||
console.log(`Received webhook with ID ${evt.data.id} and type ${eventType}`);
|
||||
log.info("Received webhook event", {
|
||||
userId: evt.data.id,
|
||||
eventType,
|
||||
});
|
||||
|
||||
try {
|
||||
// Connect to database directly for webhook operations
|
||||
@@ -70,7 +74,13 @@ export async function POST(req: Request) {
|
||||
);
|
||||
|
||||
if (!primaryEmail?.email_address) {
|
||||
console.error("No primary email found for user:", id);
|
||||
log.error(
|
||||
"No primary email found for user (user.created)",
|
||||
undefined,
|
||||
{
|
||||
userId: id,
|
||||
},
|
||||
);
|
||||
db.close();
|
||||
return NextResponse.json(
|
||||
{ error: "No primary email" },
|
||||
@@ -161,9 +171,11 @@ export async function POST(req: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ User ${id} created in database (role=${role}, gymId=${gymId ?? "null"})`,
|
||||
);
|
||||
log.info("User created in database", {
|
||||
userId: id,
|
||||
role,
|
||||
gymId: gymId ?? null,
|
||||
});
|
||||
db.close();
|
||||
break;
|
||||
}
|
||||
@@ -178,7 +190,13 @@ export async function POST(req: Request) {
|
||||
);
|
||||
|
||||
if (!primaryEmail?.email_address) {
|
||||
console.error("No primary email found for user:", id);
|
||||
log.error(
|
||||
"No primary email found for user (user.updated)",
|
||||
undefined,
|
||||
{
|
||||
userId: id,
|
||||
},
|
||||
);
|
||||
db.close();
|
||||
return NextResponse.json(
|
||||
{ error: "No primary email" },
|
||||
@@ -236,7 +254,7 @@ export async function POST(req: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ User ${id} updated in database`);
|
||||
log.info("User updated in database", { userId: id });
|
||||
db.close();
|
||||
break;
|
||||
}
|
||||
@@ -245,7 +263,7 @@ export async function POST(req: Request) {
|
||||
const { id } = evt.data;
|
||||
|
||||
if (!id) {
|
||||
console.error("No user ID provided for deletion");
|
||||
log.error("No user ID provided for deletion");
|
||||
db.close();
|
||||
return NextResponse.json({ error: "No user ID" }, { status: 400 });
|
||||
}
|
||||
@@ -254,13 +272,13 @@ export async function POST(req: Request) {
|
||||
const stmt = db.prepare("DELETE FROM users WHERE id = ?");
|
||||
stmt.run(id);
|
||||
|
||||
console.log(`✅ User ${id} deleted from database`);
|
||||
log.info("User deleted from database", { userId: id });
|
||||
db.close();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`Unhandled webhook event type: ${eventType}`);
|
||||
log.debug("Unhandled webhook event type", { eventType });
|
||||
db.close();
|
||||
}
|
||||
|
||||
@@ -269,7 +287,7 @@ export async function POST(req: Request) {
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error processing webhook:", error);
|
||||
log.error("Webhook processing failed", error);
|
||||
return NextResponse.json({ error: "Processing failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -2,259 +2,286 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface Recommendation {
|
||||
id: string;
|
||||
userId: string;
|
||||
content: string;
|
||||
activityPlan: string;
|
||||
dietPlan: string;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
id: string;
|
||||
userId: string;
|
||||
content: string;
|
||||
recommendationText: string;
|
||||
activityPlan: string;
|
||||
dietPlan: string;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export default function RecommendationsPage() {
|
||||
const { user } = useUser();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [pendingRecommendations, setPendingRecommendations] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState<string | null>(null);
|
||||
const [useExternalModel, setUseExternalModel] = useState(false);
|
||||
const { user } = useUser();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [pendingRecommendations, setPendingRecommendations] = useState<
|
||||
Recommendation[]
|
||||
>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState<string | null>(null);
|
||||
const [useExternalModel, setUseExternalModel] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch users
|
||||
const usersRes = await fetch("/api/users");
|
||||
const usersData = await usersRes.json();
|
||||
setUsers(usersData.users || []);
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch users
|
||||
const usersRes = await fetch("/api/users");
|
||||
const usersData = await usersRes.json();
|
||||
setUsers(usersData.users || []);
|
||||
|
||||
// Fetch pending recommendations
|
||||
const recsRes = await fetch("/api/recommendations");
|
||||
const recsData = await recsRes.json();
|
||||
const allRecs = recsData.recommendations || [];
|
||||
setPendingRecommendations(allRecs.filter((r: any) => r.status === 'pending'));
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
// Fetch pending recommendations
|
||||
const recsRes = await fetch("/api/recommendations");
|
||||
const recsData = await recsRes.json();
|
||||
const allRecs = recsData.recommendations || [];
|
||||
setPendingRecommendations(
|
||||
allRecs.filter((r: Recommendation) => r.status === "pending"),
|
||||
);
|
||||
} catch (error) {
|
||||
log.error("Failed to fetch data", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async (userId: string) => {
|
||||
setGenerating(userId);
|
||||
try {
|
||||
const res = await fetch("/api/recommendations/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userId, useExternalModel }),
|
||||
});
|
||||
const handleGenerate = async (userId: string) => {
|
||||
setGenerating(userId);
|
||||
try {
|
||||
const res = await fetch("/api/recommendations/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userId, useExternalModel }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
alert(`Error: ${error.error}`);
|
||||
} else {
|
||||
alert("Recommendation generated successfully!");
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("Failed to generate recommendation.");
|
||||
} finally {
|
||||
setGenerating(null);
|
||||
}
|
||||
};
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
alert(`Error: ${error.error}`);
|
||||
} else {
|
||||
alert("Recommendation generated successfully!");
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to generate recommendation", error);
|
||||
alert("Failed to generate recommendation.");
|
||||
} finally {
|
||||
setGenerating(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApprove = async (recommendationId: string, status: "approved" | "rejected") => {
|
||||
try {
|
||||
const res = await fetch("/api/recommendations/approve", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
recommendationId,
|
||||
status,
|
||||
approvedBy: user?.id || "admin",
|
||||
}),
|
||||
});
|
||||
const handleApprove = async (
|
||||
recommendationId: string,
|
||||
status: "approved" | "rejected",
|
||||
) => {
|
||||
try {
|
||||
const res = await fetch("/api/recommendations/approve", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
recommendationId,
|
||||
status,
|
||||
approvedBy: user?.id || "admin",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
alert(`Failed to update status: ${errorData.error || 'Unknown error'}`);
|
||||
} else {
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("Error processing request");
|
||||
}
|
||||
};
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
alert(`Failed to update status: ${errorData.error || "Unknown error"}`);
|
||||
} else {
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to approve recommendation", error);
|
||||
alert("Error processing request");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (rec: Recommendation) => {
|
||||
const newContent = prompt("Edit Advice:", rec.content);
|
||||
const newActivityPlan = prompt("Edit Activity Plan:", rec.activityPlan);
|
||||
const newDietPlan = prompt("Edit Diet Plan:", rec.dietPlan);
|
||||
const handleEdit = async (rec: Recommendation) => {
|
||||
const newContent = prompt("Edit Advice:", rec.content);
|
||||
const newActivityPlan = prompt("Edit Activity Plan:", rec.activityPlan);
|
||||
const newDietPlan = prompt("Edit Diet Plan:", rec.dietPlan);
|
||||
|
||||
if (newContent === null || newActivityPlan === null || newDietPlan === null) {
|
||||
// User cancelled one of the prompts
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/recommendations", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: rec.id,
|
||||
content: newContent,
|
||||
activityPlan: newActivityPlan,
|
||||
dietPlan: newDietPlan,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
alert(`Failed to update recommendation: ${errorData.error || 'Unknown error'}`);
|
||||
} else {
|
||||
alert("Recommendation updated successfully!");
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating recommendation:", error);
|
||||
alert("Failed to update recommendation.");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-xl">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
if (
|
||||
newContent === null ||
|
||||
newActivityPlan === null ||
|
||||
newDietPlan === null
|
||||
) {
|
||||
// User cancelled one of the prompts
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/recommendations", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: rec.id,
|
||||
content: newContent,
|
||||
activityPlan: newActivityPlan,
|
||||
dietPlan: newDietPlan,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
alert(
|
||||
`Failed to update recommendation: ${errorData.error || "Unknown error"}`,
|
||||
);
|
||||
} else {
|
||||
alert("Recommendation updated successfully!");
|
||||
fetchData(); // Refresh data
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to update recommendation", error);
|
||||
alert("Failed to update recommendation.");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto py-10 px-4">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">AI Recommendations</h1>
|
||||
|
||||
{/* Model Selection Toggle */}
|
||||
<div className="flex items-center gap-3 bg-white px-4 py-2 rounded-lg shadow">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{useExternalModel ? "DeepSeek AI" : "Local Ollama"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setUseExternalModel(!useExternalModel)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${useExternalModel ? "bg-blue-600" : "bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${useExternalModel ? "translate-x-6" : "translate-x-1"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-xs text-gray-500">
|
||||
{useExternalModel ? "External" : "Local"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Generate Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-4">Generate Recommendations</h2>
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<p className="mb-4 text-gray-600">
|
||||
Select a user to generate a new daily recommendation.
|
||||
</p>
|
||||
<ul className="space-y-4">
|
||||
{users.map((user) => (
|
||||
<li key={user.id} className="flex items-center justify-between border-b pb-2">
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{user.firstName} {user.lastName}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{user.email}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleGenerate(user.id)}
|
||||
disabled={generating === user.id}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{generating === user.id ? "Generating..." : "Generate"}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{users.length === 0 && (
|
||||
<p className="text-gray-500 italic">No users found.</p>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Approvals Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-4">Pending Approvals</h2>
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
{pendingRecommendations.length === 0 ? (
|
||||
<p className="text-gray-500 italic">No pending recommendations.</p>
|
||||
) : (
|
||||
<ul className="space-y-6">
|
||||
{pendingRecommendations.map((rec) => (
|
||||
<li key={rec.id} className="border rounded p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="font-bold">For: User {rec.userId}</h3>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(rec.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm mb-4">
|
||||
<div>
|
||||
<span className="font-semibold">Advice:</span> {rec.recommendationText}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Activity:</span> {rec.activityPlan}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Diet:</span> {rec.dietPlan}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => handleEdit(rec)}
|
||||
className="flex-1 bg-blue-500 text-white py-2 rounded hover:bg-blue-600"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleApprove(rec.id, "approved")}
|
||||
className="flex-1 bg-green-600 text-white py-2 rounded hover:bg-green-700"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleApprove(rec.id, "rejected")}
|
||||
className="flex-1 bg-red-600 text-white py-2 rounded hover:bg-red-700"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-xl">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10 px-4">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">AI Recommendations</h1>
|
||||
|
||||
{/* Model Selection Toggle */}
|
||||
<div className="flex items-center gap-3 bg-white px-4 py-2 rounded-lg shadow">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{useExternalModel ? "DeepSeek AI" : "Local Ollama"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setUseExternalModel(!useExternalModel)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
useExternalModel ? "bg-blue-600" : "bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
useExternalModel ? "translate-x-6" : "translate-x-1"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-xs text-gray-500">
|
||||
{useExternalModel ? "External" : "Local"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Generate Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Generate Recommendations
|
||||
</h2>
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<p className="mb-4 text-gray-600">
|
||||
Select a user to generate a new daily recommendation.
|
||||
</p>
|
||||
<ul className="space-y-4">
|
||||
{users.map((user) => (
|
||||
<li
|
||||
key={user.id}
|
||||
className="flex items-center justify-between border-b pb-2"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{user.firstName} {user.lastName}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{user.email}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleGenerate(user.id)}
|
||||
disabled={generating === user.id}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{generating === user.id ? "Generating..." : "Generate"}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{users.length === 0 && (
|
||||
<p className="text-gray-500 italic">No users found.</p>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Approvals Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-4">Pending Approvals</h2>
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
{pendingRecommendations.length === 0 ? (
|
||||
<p className="text-gray-500 italic">
|
||||
No pending recommendations.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-6">
|
||||
{pendingRecommendations.map((rec) => (
|
||||
<li key={rec.id} className="border rounded p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="font-bold">For: User {rec.userId}</h3>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(rec.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm mb-4">
|
||||
<div>
|
||||
<span className="font-semibold">Advice:</span>{" "}
|
||||
{rec.recommendationText}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Activity:</span>{" "}
|
||||
{rec.activityPlan}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Diet:</span>{" "}
|
||||
{rec.dietPlan}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => handleEdit(rec)}
|
||||
className="flex-1 bg-blue-500 text-white py-2 rounded hover:bg-blue-600"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleApprove(rec.id, "approved")}
|
||||
className="flex-1 bg-green-600 text-white py-2 rounded hover:bg-green-700"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleApprove(rec.id, "rejected")}
|
||||
className="flex-1 bg-red-600 text-white py-2 rounded hover:bg-red-700"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import log from "@/lib/logger";
|
||||
|
||||
interface Backup {
|
||||
name: string;
|
||||
@@ -56,7 +57,7 @@ export default function SettingsPage() {
|
||||
const response = await axios.get("/api/admin/backups");
|
||||
setBackups(response.data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch backups:", error);
|
||||
log.error("Failed to fetch backups", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -69,7 +70,7 @@ export default function SettingsPage() {
|
||||
const res = await axios.get("/api/gyms");
|
||||
setGyms(Array.isArray(res.data) ? res.data : []);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch gyms:", error);
|
||||
log.error("Failed to fetch gyms", error);
|
||||
setGymMessage({ type: "error", text: "Failed to load gyms" });
|
||||
} finally {
|
||||
setGymsLoading(false);
|
||||
@@ -89,7 +90,7 @@ export default function SettingsPage() {
|
||||
await fetchBackups();
|
||||
setMessage({ type: "success", text: "Backup created successfully" });
|
||||
} catch (error) {
|
||||
console.error("Failed to create backup:", error);
|
||||
log.error("Failed to create backup", error);
|
||||
setMessage({ type: "error", text: "Failed to create backup" });
|
||||
} finally {
|
||||
setCreatingBackup(false);
|
||||
@@ -112,7 +113,7 @@ export default function SettingsPage() {
|
||||
setMessage({ type: "success", text: "Database restored successfully" });
|
||||
// Optional: Refresh page or force re-login if session is invalidated
|
||||
} catch (error) {
|
||||
console.error("Failed to restore backup:", error);
|
||||
log.error("Failed to restore backup", error);
|
||||
setMessage({ type: "error", text: "Failed to restore backup" });
|
||||
} finally {
|
||||
setRestoring(null);
|
||||
@@ -144,7 +145,7 @@ export default function SettingsPage() {
|
||||
text: gymId ? "Gym selected successfully" : "Proceeding without gym",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to set gym:", error);
|
||||
log.error("Failed to set gym", error);
|
||||
setGymMessage({ type: "error", text: "Failed to set gym" });
|
||||
}
|
||||
};
|
||||
@@ -247,7 +248,7 @@ export default function SettingsPage() {
|
||||
setGymLocation("");
|
||||
fetchGyms();
|
||||
} catch (error) {
|
||||
console.error("Failed to create gym:", error);
|
||||
log.error("Failed to create gym", error);
|
||||
setGymMessage({
|
||||
type: "error",
|
||||
text: "Failed to create gym",
|
||||
|
||||
@@ -1,104 +1,135 @@
|
||||
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import { Recommendations } from "@/components/users/Recommendations";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
}>;
|
||||
params: Promise<{
|
||||
id: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function UserProfilePage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
const db = await getDatabase();
|
||||
const user = await db.getUserById(id);
|
||||
const { id } = await params;
|
||||
const db = await getDatabase();
|
||||
const user = await db.getUserById(id);
|
||||
|
||||
if (!user) {
|
||||
notFound();
|
||||
}
|
||||
if (!user) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const client = await db.getClientByUserId(user.id);
|
||||
const fitnessProfile = await db.getFitnessProfileByUserId(user.id);
|
||||
const client = await db.getClientByUserId(user.id);
|
||||
const fitnessProfile = await db.getFitnessProfileByUserId(user.id);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold">
|
||||
{user.firstName} {user.lastName}
|
||||
</h1>
|
||||
<span className="px-3 py-1 bg-gray-100 rounded-full text-sm font-medium capitalize">
|
||||
{user.role}
|
||||
</span>
|
||||
</div>
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold">
|
||||
{user.firstName} {user.lastName}
|
||||
</h1>
|
||||
<span className="px-3 py-1 bg-gray-100 rounded-full text-sm font-medium capitalize">
|
||||
{user.role}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Basic Info */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">Contact Information</h2>
|
||||
<div className="space-y-2">
|
||||
<p><span className="font-medium">Email:</span> {user.email}</p>
|
||||
<p><span className="font-medium">Phone:</span> {user.phone || "N/A"}</p>
|
||||
<p><span className="font-medium">Joined:</span> {user.createdAt.toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Client Info */}
|
||||
{client && (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">Membership Details</h2>
|
||||
<div className="space-y-2">
|
||||
<p><span className="font-medium">Type:</span> {client.membershipType}</p>
|
||||
<p><span className="font-medium">Status:</span> {client.membershipStatus}</p>
|
||||
<p><span className="font-medium">Member Since:</span> {client.joinDate.toLocaleDateString()}</p>
|
||||
<p><span className="font-medium">Last Visit:</span> {client.lastVisit?.toLocaleDateString() || "Never"}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fitness Profile */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">Fitness Profile</h2>
|
||||
{fitnessProfile ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<h3 className="font-medium mb-2 text-gray-700">Physical Stats</h3>
|
||||
<p>Height: {fitnessProfile.height} cm</p>
|
||||
<p>Weight: {fitnessProfile.weight} kg</p>
|
||||
<p>Age: {fitnessProfile.age}</p>
|
||||
<p>Gender: {fitnessProfile.gender}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-2 text-gray-700">Health & Habits</h3>
|
||||
<p>Activity Level: {fitnessProfile.activityLevel.replace('_', ' ')}</p>
|
||||
<p>Diet: {fitnessProfile.dietHabits || "N/A"}</p>
|
||||
<p>Exercise: {fitnessProfile.exerciseHabits || "N/A"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-2 text-gray-700">Medical</h3>
|
||||
<p>Conditions: {fitnessProfile.medicalConditions || "None"}</p>
|
||||
<p>Allergies: {fitnessProfile.allergies || "None"}</p>
|
||||
<p>Injuries: {fitnessProfile.injuries || "None"}</p>
|
||||
</div>
|
||||
<div className="col-span-full mt-4">
|
||||
<h3 className="font-medium mb-2 text-gray-700">Goals</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{fitnessProfile.fitnessGoals.map((goal, i) => (
|
||||
<span key={i} className="px-2 py-1 bg-blue-50 text-blue-700 rounded text-sm">
|
||||
{goal.replace('_', ' ')}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 italic">No fitness profile created yet.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recommendations Component */}
|
||||
<Recommendations userId={user.id} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Basic Info */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">Contact Information</h2>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
<span className="font-medium">Email:</span> {user.email}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Phone:</span> {user.phone || "N/A"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Joined:</span>{" "}
|
||||
{user.createdAt.toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* Client Info */}
|
||||
{client && (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">Membership Details</h2>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
<span className="font-medium">Type:</span>{" "}
|
||||
{client.membershipType}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Status:</span>{" "}
|
||||
{client.membershipStatus}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Member Since:</span>{" "}
|
||||
{client.joinDate.toLocaleDateString()}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Last Visit:</span>{" "}
|
||||
{client.lastVisit?.toLocaleDateString() || "Never"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fitness Profile */}
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">Fitness Profile</h2>
|
||||
{fitnessProfile ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<h3 className="font-medium mb-2 text-gray-700">Physical Stats</h3>
|
||||
<p>Height: {fitnessProfile.height} cm</p>
|
||||
<p>Weight: {fitnessProfile.weight} kg</p>
|
||||
<p>Age: {fitnessProfile.age}</p>
|
||||
<p>Gender: {fitnessProfile.gender}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-2 text-gray-700">
|
||||
Health & Habits
|
||||
</h3>
|
||||
<p>
|
||||
Activity Level:{" "}
|
||||
{fitnessProfile.activityLevel?.replace("_", " ") || "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-2 text-gray-700">Medical</h3>
|
||||
<p>Conditions: {fitnessProfile.medicalConditions || "None"}</p>
|
||||
<p>Allergies: {fitnessProfile.allergies || "None"}</p>
|
||||
<p>Injuries: {fitnessProfile.injuries || "None"}</p>
|
||||
</div>
|
||||
<div className="col-span-full mt-4">
|
||||
<h3 className="font-medium mb-2 text-gray-700">Goals</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{fitnessProfile.fitnessGoals &&
|
||||
fitnessProfile.fitnessGoals.length > 0 ? (
|
||||
fitnessProfile.fitnessGoals.map((goal, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-2 py-1 bg-blue-50 text-blue-700 rounded text-sm"
|
||||
>
|
||||
{goal.replace("_", " ")}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-gray-500 italic">No goals set</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 italic">
|
||||
No fitness profile created yet.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recommendations Component */}
|
||||
<Recommendations userId={user.id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user