import { NextResponse } from "next/server"; import { auth } from "@clerk/nextjs/server"; import { getDatabase } from "@/lib/database"; import log from "@/lib/logger"; import { ensureUserSynced } from "@/lib/sync-user"; const AI_LINK_PREFIX = "[AI_LINKED]"; type GoalType = | "weight_target" | "strength_milestone" | "endurance_target" | "flexibility_goal" | "habit_building" | "custom"; interface ParsedPlanItem { id: string; title: string; description: string; goalType: GoalType; } function inferGoalType(text: string): GoalType { const normalized = text.toLowerCase(); if ( normalized.includes("strength") || normalized.includes("bench") || normalized.includes("squat") || normalized.includes("deadlift") || normalized.includes("weights") ) { return "strength_milestone"; } if ( normalized.includes("run") || normalized.includes("cardio") || normalized.includes("endurance") || normalized.includes("cycle") ) { return "endurance_target"; } if ( normalized.includes("stretch") || normalized.includes("mobility") || normalized.includes("yoga") || normalized.includes("flexibility") ) { return "flexibility_goal"; } if ( normalized.includes("daily") || normalized.includes("routine") || normalized.includes("habit") ) { return "habit_building"; } return "custom"; } function parseActivityPlanToItems(activityPlan: string): ParsedPlanItem[] { const lines = activityPlan .replace(/\r\n/g, "\n") .split(/\n+/) .flatMap((line) => line.split(/(?<=[.!?])\s+(?=[A-Z0-9])/g)) .map((line) => line.trim()) .filter(Boolean) .map((line) => line.replace(/^[-*•\d.)\s]+/, "").trim()) .filter((line) => line.length > 10) .slice(0, 8); const uniqueLines = Array.from(new Set(lines)); return uniqueLines.map((line) => ({ id: crypto.randomUUID(), title: line.length > 72 ? `${line.slice(0, 69)}...` : line, description: line, goalType: inferGoalType(line), })); } function getDefaultPlanItems(): ParsedPlanItem[] { const defaults = [ "Complete 3 strength sessions this week with progressive overload.", "Add 2 cardio sessions of 25-30 minutes for endurance.", "Do a 10-minute mobility routine daily after training.", ]; return defaults.map((line) => ({ id: crypto.randomUUID(), title: line.length > 72 ? `${line.slice(0, 69)}...` : line, description: line, goalType: inferGoalType(line), })); } export async function POST(req: Request) { try { const { userId: clerkUserId } = await auth(); if (!clerkUserId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const body = await req.json(); log.debug("Approve recommendation request body", { body }); const { recommendationId, status } = body; if (!recommendationId || !status) { log.error("Missing required fields", { recommendationId, status, receivedBody: body, }); return NextResponse.json( { error: "Recommendation ID and status are required" }, { status: 400 }, ); } const db = await getDatabase(); const currentUser = await ensureUserSynced(clerkUserId, db); if (!currentUser) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const canApproveRecommendations = currentUser.role === "superAdmin" || currentUser.role === "admin" || currentUser.role === "trainer"; if (!canApproveRecommendations) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const existingRecommendation = (await db.getAllRecommendations()).find( (recommendation) => recommendation.id === recommendationId, ); if (!existingRecommendation) { return NextResponse.json( { error: "Recommendation not found" }, { status: 404 }, ); } if (currentUser.role !== "superAdmin") { const targetUser = await db.getUserById(existingRecommendation.userId); if ( !currentUser.gymId || !targetUser || targetUser.gymId !== currentUser.gymId ) { return NextResponse.json( { error: "Forbidden - Cannot access users from other gyms" }, { status: 403 }, ); } } // Update recommendation status const updates: any = { status, approvedAt: status === "approved" ? new Date() : undefined, approvedBy: status === "approved" ? clerkUserId : undefined, }; // Remove undefined keys Object.keys(updates).forEach( (key) => updates[key] === undefined && delete updates[key], ); const updatedRecommendation = await db.updateRecommendation( recommendationId, updates, ); if (!updatedRecommendation) { return NextResponse.json( { error: "Recommendation not found" }, { status: 404 }, ); } let pausedGoalsCount = 0; let createdGoalsCount = 0; // If approved, regenerate linked AI goals and create a notification for the user if (status === "approved") { try { const existingActiveGoals = await db.getFitnessGoalsByUserId( updatedRecommendation.userId, "active", ); const linkedGoals = existingActiveGoals.filter((goal) => goal.notes?.startsWith(AI_LINK_PREFIX), ); pausedGoalsCount = linkedGoals.length; await Promise.all( linkedGoals.map((goal) => db.updateFitnessGoal(goal.id, { status: "paused", notes: `${goal.notes || ""}\nPaused due to recommendation approval on ${new Date().toISOString()}`, }), ), ); let planItems = parseActivityPlanToItems( updatedRecommendation.activityPlan || "", ); if ( planItems.length === 0 && updatedRecommendation.recommendationText ) { planItems = parseActivityPlanToItems( updatedRecommendation.recommendationText, ); } if (planItems.length === 0) { planItems = getDefaultPlanItems(); } const fitnessProfileId = updatedRecommendation.fitnessProfileId || (await db.getFitnessProfileByUserId(updatedRecommendation.userId)) ?.id; if (!fitnessProfileId) { log.warn("No fitness profile available for AI goal creation", { recommendationId, userId: updatedRecommendation.userId, }); } else { const createdGoals = await Promise.all( planItems.map((item) => db.createFitnessGoal({ id: crypto.randomUUID(), userId: updatedRecommendation.userId, fitnessProfileId, goalType: item.goalType, title: item.title, description: item.description, targetValue: undefined, currentValue: 0, unit: undefined, startDate: new Date(), targetDate: undefined, completedDate: undefined, status: "active", progress: 0, priority: "medium", notes: `${AI_LINK_PREFIX} recommendationId=${updatedRecommendation.id}; itemId=${item.id}`, }), ), ); createdGoalsCount = createdGoals.length; } log.info("Regenerated linked AI goals from approved recommendation", { recommendationId: updatedRecommendation.id, userId: updatedRecommendation.userId, pausedGoals: pausedGoalsCount, createdGoals: createdGoalsCount, }); } catch (goalConversionError) { log.error( "Failed to regenerate linked goals for approved recommendation", goalConversionError, { recommendationId, userId: updatedRecommendation.userId, }, ); } try { await db.createNotification({ id: crypto.randomUUID(), userId: updatedRecommendation.userId, title: "Recommendation Approved! 🎉", message: "Your AI-powered fitness recommendation has been approved by your trainer. Check it out now!", type: "system", read: false, }); log.info("Notification created for approved recommendation", { recommendationId, userId: updatedRecommendation.userId, }); } catch (notificationError) { // Log error but don't fail the approval log.error("Failed to create notification", notificationError); } } return NextResponse.json({ success: true, data: updatedRecommendation, meta: { timestamp: new Date().toISOString(), pausedGoals: pausedGoalsCount, createdGoals: createdGoalsCount, }, }); } catch (error) { log.error("Error approving recommendation", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, ); } }