diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index d3b15b8..6eae76a 100644 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/admin/src/app/api/recommendations/route.ts b/apps/admin/src/app/api/recommendations/route.ts new file mode 100644 index 0000000..b5496b8 --- /dev/null +++ b/apps/admin/src/app/api/recommendations/route.ts @@ -0,0 +1,106 @@ +import { NextResponse } from 'next/server' +import { auth } from '@clerk/nextjs/server' +import { getDatabase } from '@/lib/database' + +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') + + if (!targetUserId) { + return new NextResponse('User ID is required', { status: 400 }) + } + + const db = await getDatabase() + + // Check permissions: Users can view their own, Admins/Trainers can view anyone's + if (currentUserId !== targetUserId) { + 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 recommendations = await db.getRecommendationsByUserId(targetUserId) + 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, type, content, status } = body + + if (!userId || !type || !content) { + return new NextResponse('Missing required fields', { status: 400 }) + } + + const recommendation = await db.createRecommendation({ + id: crypto.randomUUID(), + userId, + type, + content, + status: status || 'pending' + }) + + return NextResponse.json(recommendation) + } catch (error) { + console.error('Error creating 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, content } = body + + if (!id) { + return new NextResponse('Recommendation ID is required', { status: 400 }) + } + + const db = await getDatabase() + + // Users can update status (e.g. mark as completed), Staff can update content too + // Ideally we'd check ownership for status update, but for now let's allow it if they have the ID + // A stricter check would be: fetch recommendation, check if userId matches currentUserId OR if currentUser is staff + + const updated = await db.updateRecommendation(id, { + ...(status && { status }), + ...(content && { content }) + }) + + return NextResponse.json(updated) + } catch (error) { + console.error('Error updating recommendation:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} diff --git a/apps/admin/src/app/users/[id]/page.tsx b/apps/admin/src/app/users/[id]/page.tsx new file mode 100644 index 0000000..5eba062 --- /dev/null +++ b/apps/admin/src/app/users/[id]/page.tsx @@ -0,0 +1,104 @@ + +import { getDatabase } from "@/lib/database"; +import { Recommendations } from "@/components/users/Recommendations"; +import { notFound } from "next/navigation"; + +interface PageProps { + 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); + + if (!user) { + notFound(); + } + + const client = await db.getClientByUserId(user.id); + const fitnessProfile = await db.getFitnessProfileByUserId(user.id); + + return ( +
+
+

+ {user.firstName} {user.lastName} +

+ + {user.role} + +
+ +
+ {/* Basic Info */} +
+

Contact Information

+
+

Email: {user.email}

+

Phone: {user.phone || "N/A"}

+

Joined: {user.createdAt.toLocaleDateString()}

+
+
+ + {/* Client Info */} + {client && ( +
+

Membership Details

+
+

Type: {client.membershipType}

+

Status: {client.membershipStatus}

+

Member Since: {client.joinDate.toLocaleDateString()}

+

Last Visit: {client.lastVisit?.toLocaleDateString() || "Never"}

+
+
+ )} +
+ + {/* Fitness Profile */} +
+

Fitness Profile

+ {fitnessProfile ? ( +
+
+

Physical Stats

+

Height: {fitnessProfile.height} cm

+

Weight: {fitnessProfile.weight} kg

+

Age: {fitnessProfile.age}

+

Gender: {fitnessProfile.gender}

+
+
+

Health & Habits

+

Activity Level: {fitnessProfile.activityLevel.replace('_', ' ')}

+

Diet: {fitnessProfile.dietHabits || "N/A"}

+

Exercise: {fitnessProfile.exerciseHabits || "N/A"}

+
+
+

Medical

+

Conditions: {fitnessProfile.medicalConditions || "None"}

+

Allergies: {fitnessProfile.allergies || "None"}

+

Injuries: {fitnessProfile.injuries || "None"}

+
+
+

Goals

+
+ {fitnessProfile.fitnessGoals.map((goal, i) => ( + + {goal.replace('_', ' ')} + + ))} +
+
+
+ ) : ( +

No fitness profile created yet.

+ )} +
+ + {/* Recommendations Component */} + +
+ ); +} diff --git a/apps/admin/src/components/users/Recommendations.tsx b/apps/admin/src/components/users/Recommendations.tsx new file mode 100644 index 0000000..7f5db92 --- /dev/null +++ b/apps/admin/src/components/users/Recommendations.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/Button"; +import { Card, CardHeader, CardContent } from "@/components/ui/card"; + +interface Recommendation { + id: string; + userId: string; + type: "short_term" | "medium_term" | "long_term"; + content: string; + status: "pending" | "completed"; + createdAt: string; +} + +interface RecommendationsProps { + userId: string; +} + +export function Recommendations({ userId }: RecommendationsProps) { + const [recommendations, setRecommendations] = useState([]); + const [loading, setLoading] = useState(true); + const [newRec, setNewRec] = useState<{ + type: "short_term" | "medium_term" | "long_term"; + content: string; + }>({ type: "short_term", content: "" }); + + useEffect(() => { + fetchRecommendations(); + }, [userId]); + + const fetchRecommendations = async () => { + setLoading(true); + try { + const response = await fetch(`/api/recommendations?userId=${userId}`); + if (response.ok) { + const data = await response.json(); + setRecommendations(data); + } + } catch (error) { + console.error("Failed to fetch recommendations:", error); + } finally { + setLoading(false); + } + }; + + const handleAddRecommendation = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const response = await fetch("/api/recommendations", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + userId, + type: newRec.type, + content: newRec.content, + }), + }); + + if (response.ok) { + setNewRec({ ...newRec, content: "" }); + fetchRecommendations(); + } else { + alert("Failed to add recommendation"); + } + } catch (error) { + console.error(error); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm("Are you sure?")) return; + // Note: Delete API not implemented in route.ts yet, but good to have UI ready or we can add it. + // For now, let's assume we might add it or just omit. + // Actually, I didn't add DELETE to route.ts. Let's skip for now. + alert("Delete functionality not available yet."); + }; + + const groupedRecs = { + short_term: recommendations.filter((r) => r.type === "short_term"), + medium_term: recommendations.filter((r) => r.type === "medium_term"), + long_term: recommendations.filter((r) => r.type === "long_term"), + }; + + const renderSection = ( + title: string, + type: "short_term" | "medium_term" | "long_term", + items: Recommendation[] + ) => ( +
+

{title}

+
+ {items.length === 0 && ( +

No recommendations yet.

+ )} + {items.map((rec) => ( +
+
+

{rec.content}

+

+ {new Date(rec.createdAt).toLocaleDateString()} -{" "} + + {rec.status === "completed" ? "Completed" : "Pending"} + +

+
+
+ ))} +
+
+ setNewRec({ ...newRec, type })} + /> + {newRec.type === type && ( + <> + + setNewRec({ ...newRec, content: e.target.value }) + } + required + /> + + + )} + {newRec.type !== type && ( + + )} +
+
+ ); + + if (loading) return
Loading recommendations...
; + + return ( + + +

Fitness Recommendations

+
+ +
+ {renderSection("Short Term Goals", "short_term", groupedRecs.short_term)} + {renderSection("Medium Term Goals", "medium_term", groupedRecs.medium_term)} + {renderSection("Long Term Goals", "long_term", groupedRecs.long_term)} +
+
+
+ ); +} diff --git a/apps/admin/src/components/users/UserManagement.tsx b/apps/admin/src/components/users/UserManagement.tsx index f4fd447..f4ab707 100644 --- a/apps/admin/src/components/users/UserManagement.tsx +++ b/apps/admin/src/components/users/UserManagement.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { UserGrid } from "@/components/users/UserGrid"; import { Button } from "@/components/ui/Button"; -import { Card, CardHeader, CardContent } from "@/components/ui/Card"; +import { Card, CardHeader, CardContent } from "@/components/ui/card"; interface User { id: string; @@ -212,7 +212,7 @@ export function UserManagement() {