204 lines
6.7 KiB
TypeScript
204 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useUser } from "@clerk/nextjs";
|
|
import log from "@/lib/logger";
|
|
import { toast } from "@/lib/toast";
|
|
import {
|
|
useUsers,
|
|
useRecommendations,
|
|
useGenerateRecommendations,
|
|
useApproveRecommendation,
|
|
useUpdateRecommendation,
|
|
type Recommendation,
|
|
} from "@/hooks/use-api";
|
|
import { PageHeader } from "@/components/ui/PageHeader";
|
|
|
|
export default function RecommendationsPage() {
|
|
const { user } = useUser();
|
|
const [useExternalModel, setUseExternalModel] = useState(false);
|
|
|
|
const { data: users = [], isLoading: usersLoading } = useUsers();
|
|
const { data: allRecommendations = [], isLoading: recsLoading } =
|
|
useRecommendations();
|
|
const pendingRecommendations = allRecommendations.filter(
|
|
(r) => r.status === "pending",
|
|
);
|
|
|
|
const generateRec = useGenerateRecommendations();
|
|
const approveRec = useApproveRecommendation();
|
|
const updateRec = useUpdateRecommendation();
|
|
|
|
const handleGenerate = async (userId: string) => {
|
|
try {
|
|
await generateRec.mutateAsync(userId);
|
|
toast.success("Recommendation generated successfully!");
|
|
} catch (error) {
|
|
log.error("Failed to generate recommendation", error);
|
|
toast.error("Failed to generate recommendation.");
|
|
}
|
|
};
|
|
|
|
const handleApprove = async (
|
|
recommendationId: string,
|
|
status: "approved" | "rejected",
|
|
) => {
|
|
try {
|
|
await approveRec.mutateAsync({
|
|
recommendationId,
|
|
approved: status === "approved",
|
|
});
|
|
toast.success("Recommendation status updated");
|
|
} catch (error) {
|
|
log.error("Failed to approve recommendation", error);
|
|
toast.error("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);
|
|
|
|
if (
|
|
newContent === null ||
|
|
newActivityPlan === null ||
|
|
newDietPlan === null
|
|
) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await updateRec.mutateAsync({
|
|
id: rec.id,
|
|
content: newContent,
|
|
activityPlan: newActivityPlan,
|
|
dietPlan: newDietPlan,
|
|
});
|
|
toast.success("Recommendation updated successfully!");
|
|
} catch (error) {
|
|
log.error("Failed to update recommendation", error);
|
|
toast.error("Failed to update recommendation.");
|
|
}
|
|
};
|
|
|
|
if (usersLoading || recsLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-lg">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<PageHeader
|
|
title="AI Recommendations"
|
|
description="Generate and manage AI-powered fitness recommendations"
|
|
breadcrumbs={[{ label: "AI Recommendations" }]}
|
|
/>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Generate Section */}
|
|
<div className="card-modern">
|
|
<div className="mb-6">
|
|
<h3 className="text-lg font-semibold">Generate Recommendations</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Select a user to generate a new daily recommendation.
|
|
</p>
|
|
</div>
|
|
<ul className="space-y-4">
|
|
{users.map((user) => (
|
|
<li
|
|
key={user.id}
|
|
className="flex items-center justify-between border-b border-border/50 pb-4 last:border-0 last:pb-0"
|
|
>
|
|
<div>
|
|
<p className="font-medium">
|
|
{user.firstName} {user.lastName}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">{user.email}</p>
|
|
</div>
|
|
<button
|
|
onClick={() => handleGenerate(user.id)}
|
|
disabled={generateRec.isPending}
|
|
className="btn-primary"
|
|
>
|
|
{generateRec.isPending ? "Generating..." : "Generate"}
|
|
</button>
|
|
</li>
|
|
))}
|
|
{users.length === 0 && (
|
|
<p className="text-muted-foreground italic">No users found.</p>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
|
|
{/* Pending Approvals Section */}
|
|
<div className="card-modern">
|
|
<div className="mb-6">
|
|
<h3 className="text-lg font-semibold">Pending Approvals</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Review and approve AI-generated recommendations
|
|
</p>
|
|
</div>
|
|
{pendingRecommendations.length === 0 ? (
|
|
<p className="text-muted-foreground italic">
|
|
No pending recommendations.
|
|
</p>
|
|
) : (
|
|
<ul className="space-y-4">
|
|
{pendingRecommendations.map((rec) => (
|
|
<li
|
|
key={rec.id}
|
|
className="border border-border rounded-lg p-4"
|
|
>
|
|
<div className="flex justify-between items-start mb-3">
|
|
<h4 className="font-semibold">For: User {rec.userId}</h4>
|
|
<span className="text-xs text-muted-foreground">
|
|
{new Date(rec.createdAt).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
<div className="space-y-2 text-sm mb-4">
|
|
<div>
|
|
<span className="font-medium">Advice:</span>{" "}
|
|
{rec.recommendationText}
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">Activity:</span>{" "}
|
|
{rec.activityPlan}
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">Diet:</span> {rec.dietPlan}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleEdit(rec)}
|
|
className="btn-secondary flex-1"
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
onClick={() => handleApprove(rec.id, "approved")}
|
|
className="btn-primary flex-1 bg-emerald-600 hover:bg-emerald-700"
|
|
>
|
|
Approve
|
|
</button>
|
|
<button
|
|
onClick={() => handleApprove(rec.id, "rejected")}
|
|
className="btn-danger flex-1"
|
|
>
|
|
Reject
|
|
</button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|