3 and 4 completed

This commit is contained in:
dimitar committed 2026-03-11 02:38:43 +01:00
1 parent 3573709d99
commit 97436c6823
21 files changed
+2168 -558

No files matched your search

@@ -9,12 +9,16 @@ import { toast } from "@/lib/toast";
interface Recommendation {
id: string;
userId: string;
type: "short_term" | "medium_term" | "long_term" | "ai_plan";
fitnessProfileId: string;
recommendationText: string;
activityPlan?: string;
dietPlan?: string;
status: "pending" | "completed" | "approved" | "rejected";
activityPlan: string;
dietPlan: string;
status: "pending" | "approved" | "rejected";
generatedAt: string;
approvedAt?: string | null;
approvedBy?: string | null;
createdAt: string;
updatedAt: string;
}
interface RecommendationsProps {
@@ -24,10 +28,7 @@ interface RecommendationsProps {
export function Recommendations({ userId }: RecommendationsProps) {
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
const [loading, setLoading] = useState(true);
const [newRec, setNewRec] = useState<{
type: "short_term" | "medium_term" | "long_term";
content: string;
}>({ type: "short_term", content: "" });
const [generating, setGenerating] = useState(false);
useEffect(() => {
fetchRecommendations();
@@ -38,8 +39,13 @@ export function Recommendations({ userId }: RecommendationsProps) {
try {
const response = await fetch(`/api/recommendations?userId=${userId}`);
if (response.ok) {
const data = await response.json();
setRecommendations(data);
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);
@@ -48,159 +54,179 @@ export function Recommendations({ userId }: RecommendationsProps) {
}
};
const handleAddRecommendation = async (e: React.FormEvent) => {
e.preventDefault();
const handleGenerateRecommendation = async () => {
setGenerating(true);
try {
const response = await fetch("/api/recommendations", {
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 }),
});
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({
userId,
type: newRec.type,
content: newRec.content,
recommendationId,
status: "rejected",
}),
});
if (response.ok) {
setNewRec({ ...newRec, content: "" });
fetchRecommendations();
toast.success("Recommendation added successfully");
toast.success("Recommendation rejected");
} else {
toast.error("Failed to add recommendation");
toast.error("Failed to reject recommendation");
}
} catch (error) {
log.error("Failed to add recommendation", error);
log.error("Failed to reject recommendation", error);
toast.error("Failed to reject recommendation");
}
};
const groupedRecs = {
ai_plan: recommendations.filter((r) => r.type === "ai_plan"),
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" | "ai_plan",
items: Recommendation[],
) => (
<div className="mb-6">
<h4 className="font-semibold text-lg mb-3 capitalize">{title}</h4>
<div className="space-y-2">
{items.length === 0 && (
<p className="text-gray-500 text-sm italic">
No recommendations yet.
</p>
)}
{items.map((rec) => (
<div
key={rec.id}
className={`p-3 rounded border flex justify-between items-start ${
rec.status === "completed" || rec.status === "approved"
? "bg-green-50 border-green-200"
: "bg-white border-gray-200"
}`}
>
<div className="w-full">
<p className="text-gray-700">{rec.recommendationText}</p>
{rec.type === "ai_plan" && (
<div className="mt-2 text-xs text-gray-600 space-y-1">
{rec.activityPlan && (
<p>
<span className="font-semibold">Activity:</span>{" "}
{rec.activityPlan}
</p>
)}
{rec.dietPlan && (
<p>
<span className="font-semibold">Diet:</span>{" "}
{rec.dietPlan}
</p>
)}
</div>
)}
<p className="text-xs text-gray-400 mt-2">
{new Date(rec.createdAt).toLocaleDateString()} -{" "}
<span
className={
rec.status === "completed" || rec.status === "approved"
? "text-green-600 font-medium"
: "text-yellow-600"
}
>
{rec.status === "completed"
? "Completed"
: rec.status === "approved"
? "Approved"
: "Pending"}
</span>
</p>
</div>
</div>
))}
</div>
{type !== "ai_plan" && (
<form onSubmit={handleAddRecommendation} className="mt-3 flex gap-2">
<input
type="hidden"
value={type}
onChange={() => setNewRec({ ...newRec, type: type as any })}
/>
{newRec.type === type && (
<>
<input
type="text"
placeholder={`Add ${title.toLowerCase()}...`}
className="flex-1 border rounded px-3 py-1 text-sm"
value={newRec.content}
onChange={(e) =>
setNewRec({ ...newRec, content: e.target.value })
}
required
/>
<Button type="submit" variant="secondary">
Add
</Button>
</>
)}
{newRec.type !== type && (
<Button
type="button"
variant="secondary"
onClick={() => setNewRec({ type: type as any, content: "" })}
className="text-xs text-gray-500"
>
+ Add New
</Button>
)}
</form>
)}
</div>
);
if (loading) return <div>Loading recommendations...</div>;
return (
<Card>
<CardHeader>
<h3 className="text-xl font-bold">Fitness Recommendations</h3>
<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>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{renderSection("Daily AI Plan", "ai_plan", groupedRecs.ai_plan)}
{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)}
</div>
{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>
);