237 lines
7.8 KiB
TypeScript
237 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
|
import log from "@/lib/logger";
|
|
import { toast } from "@/lib/toast";
|
|
|
|
interface Recommendation {
|
|
id: string;
|
|
userId: string;
|
|
fitnessProfileId: string;
|
|
recommendationText: string;
|
|
activityPlan: string;
|
|
dietPlan: string;
|
|
status: "pending" | "approved" | "rejected";
|
|
generatedAt: string;
|
|
approvedAt?: string | null;
|
|
approvedBy?: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
interface RecommendationsProps {
|
|
userId: string;
|
|
}
|
|
|
|
export function Recommendations({ userId }: RecommendationsProps) {
|
|
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [generating, setGenerating] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchRecommendations();
|
|
}, [userId]);
|
|
|
|
const fetchRecommendations = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/recommendations?userId=${userId}`);
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
// API returns { success: true, data: [...], meta: {...} }
|
|
// Extract the recommendations array from the data field
|
|
const recsArray = Array.isArray(result.data)
|
|
? result.data
|
|
: result.data?.recommendations || [];
|
|
setRecommendations(recsArray);
|
|
}
|
|
} catch (error) {
|
|
log.error("Failed to fetch recommendations", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleGenerateRecommendation = async () => {
|
|
setGenerating(true);
|
|
try {
|
|
const response = await fetch("/api/recommendations/generate", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ userId }),
|
|
});
|
|
|
|
if (response.ok) {
|
|
fetchRecommendations();
|
|
toast.success("AI recommendation generated successfully");
|
|
} else {
|
|
const error = await response.json();
|
|
toast.error(error.error || "Failed to generate recommendation");
|
|
}
|
|
} catch (error) {
|
|
log.error("Failed to generate recommendation", error);
|
|
toast.error("Failed to generate recommendation");
|
|
} finally {
|
|
setGenerating(false);
|
|
}
|
|
};
|
|
|
|
const handleApproveRecommendation = async (recommendationId: string) => {
|
|
try {
|
|
const response = await fetch("/api/recommendations/approve", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
recommendationId,
|
|
status: "approved",
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
fetchRecommendations();
|
|
toast.success("Recommendation approved");
|
|
} else {
|
|
toast.error("Failed to approve recommendation");
|
|
}
|
|
} catch (error) {
|
|
log.error("Failed to approve recommendation", error);
|
|
toast.error("Failed to approve recommendation");
|
|
}
|
|
};
|
|
|
|
const handleRejectRecommendation = async (recommendationId: string) => {
|
|
try {
|
|
const response = await fetch("/api/recommendations", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
recommendationId,
|
|
status: "rejected",
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
fetchRecommendations();
|
|
toast.success("Recommendation rejected");
|
|
} else {
|
|
toast.error("Failed to reject recommendation");
|
|
}
|
|
} catch (error) {
|
|
log.error("Failed to reject recommendation", error);
|
|
toast.error("Failed to reject recommendation");
|
|
}
|
|
};
|
|
|
|
if (loading) return <div>Loading recommendations...</div>;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<h3 className="text-xl font-bold">AI Fitness Recommendations</h3>
|
|
<Button onClick={handleGenerateRecommendation} disabled={generating}>
|
|
{generating ? "Generating..." : "Generate New Recommendation"}
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{recommendations.length === 0 ? (
|
|
<p className="text-gray-500 text-center py-8">
|
|
No recommendations yet. Click "Generate New Recommendation" to
|
|
create one.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{recommendations.map((rec) => (
|
|
<div
|
|
key={rec.id}
|
|
className={`p-4 rounded-lg border ${
|
|
rec.status === "approved"
|
|
? "bg-green-50 border-green-200"
|
|
: rec.status === "rejected"
|
|
? "bg-red-50 border-red-200"
|
|
: "bg-yellow-50 border-yellow-200"
|
|
}`}
|
|
>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<p className="font-semibold text-gray-900 mb-1">
|
|
Recommendation
|
|
</p>
|
|
<p className="text-gray-700">{rec.recommendationText}</p>
|
|
</div>
|
|
|
|
{rec.activityPlan && (
|
|
<div>
|
|
<p className="font-semibold text-gray-900 mb-1">
|
|
Activity Plan
|
|
</p>
|
|
<p className="text-gray-700 text-sm">
|
|
{rec.activityPlan}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{rec.dietPlan && (
|
|
<div>
|
|
<p className="font-semibold text-gray-900 mb-1">
|
|
Diet Plan
|
|
</p>
|
|
<p className="text-gray-700 text-sm">{rec.dietPlan}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between pt-2 border-t">
|
|
<div className="text-xs text-gray-500">
|
|
Generated: {new Date(rec.generatedAt).toLocaleString()}
|
|
{rec.approvedAt && (
|
|
<>
|
|
{" "}
|
|
• Approved:{" "}
|
|
{new Date(rec.approvedAt).toLocaleString()}
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`text-xs font-medium px-2 py-1 rounded ${
|
|
rec.status === "approved"
|
|
? "bg-green-600 text-white"
|
|
: rec.status === "rejected"
|
|
? "bg-red-600 text-white"
|
|
: "bg-yellow-600 text-white"
|
|
}`}
|
|
>
|
|
{rec.status.charAt(0).toUpperCase() +
|
|
rec.status.slice(1)}
|
|
</span>
|
|
{rec.status === "pending" && (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleApproveRecommendation(rec.id)}
|
|
className="bg-green-600 hover:bg-green-700"
|
|
>
|
|
Approve
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="destructive"
|
|
onClick={() => handleRejectRecommendation(rec.id)}
|
|
>
|
|
Reject
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|