performance improvments

This commit is contained in:
dimitar committed 2026-03-18 02:04:09 +01:00
1 parent 912981e3f3
commit 02c9681aca
13 files changed
+805 -474

No files matched your search

@@ -0,0 +1,142 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { getDatabase } from "@/lib/database";
import { ensureUserSynced } from "@/lib/sync-user";
import { successResponse } from "@/lib/api/responses";
import { db as rawDb, sql } from "@fitai/database";
interface UserGrowthPoint {
label: string;
value: number;
}
interface MembershipDistributionPoint {
label: string;
value: number;
color: string;
}
interface RevenuePoint {
label: string;
value: number;
color: string;
}
interface AnalyticsData {
userGrowth: UserGrowthPoint[];
membershipDistribution: MembershipDistributionPoint[];
revenue: RevenuePoint[];
}
export async function GET(req: NextRequest) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const database = await getDatabase();
const user = await ensureUserSynced(userId, database);
if (!user || (user.role !== "admin" && user.role !== "superAdmin")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const url = new URL(req.url);
const months = parseInt(url.searchParams.get("months") || "6");
const allUsers = await database.getAllUsers();
const allClients = await database.getAllClients();
const paymentsRaw = await rawDb.all(
sql`SELECT * FROM payments WHERE status = 'completed' AND paid_at IS NOT NULL`,
);
const payments: any[] = paymentsRaw || [];
const now = new Date();
const userGrowth: UserGrowthPoint[] = [];
for (let i = months - 1; i >= 0; i--) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
const monthLabel = date.toLocaleDateString("en-US", { month: "short" });
const usersCreatedByMonth = allUsers.filter((u: any) => {
const createdAt = new Date(u.createdAt);
return (
createdAt.getFullYear() === date.getFullYear() &&
createdAt.getMonth() === date.getMonth()
);
});
userGrowth.push({
label: monthLabel,
value: usersCreatedByMonth.length,
});
}
let runningTotal = 0;
for (const point of userGrowth) {
runningTotal += point.value;
point.value = runningTotal;
}
const membershipCounts: Record<string, number> = {
basic: 0,
premium: 0,
vip: 0,
};
for (const client of allClients) {
const membershipType = (client as any).membershipType || "basic";
if (membershipCounts[membershipType] !== undefined) {
membershipCounts[membershipType]++;
}
}
const membershipDistribution: MembershipDistributionPoint[] = [
{ label: "Basic", value: membershipCounts.basic, color: "#6b7280" },
{ label: "Premium", value: membershipCounts.premium, color: "#3b82f6" },
{ label: "VIP", value: membershipCounts.vip, color: "#f59e0b" },
];
const revenue: RevenuePoint[] = [];
for (let i = months - 1; i >= 0; i--) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
const monthLabel = date.toLocaleDateString("en-US", { month: "short" });
const nextMonthDate = new Date(
now.getFullYear(),
now.getMonth() - i + 1,
1,
);
const monthlyRevenue = payments
.filter((p: any) => {
const paidAt = p.paid_at ? new Date(p.paid_at) : null;
if (!paidAt) return false;
return paidAt >= date && paidAt < nextMonthDate;
})
.reduce((sum: number, p: any) => sum + (p.amount || 0), 0);
revenue.push({
label: monthLabel,
value: monthlyRevenue,
color: "#10b981",
});
}
const analyticsData: AnalyticsData = {
userGrowth,
membershipDistribution,
revenue,
};
return successResponse({ analytics: analyticsData });
} catch (error) {
console.error("Analytics error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { auth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import { getDatabase } from "@/lib/database";
import { ensureUserSynced } from "@/lib/sync-user";
import { successResponse } from "@/lib/api/responses";
export async function GET(req: Request) {
try {
@@ -51,7 +52,7 @@ export async function GET(req: Request) {
revenueGrowth: 0,
};
return NextResponse.json(stats);
return successResponse({ stats });
} catch (error) {
console.error("Dashboard stats error:", error);
return new NextResponse("Internal Server Error", { status: 500 });
+12 -9
View File
@@ -10,6 +10,7 @@ import {
} from "@clerk/nextjs";
import { Sidebar } from "@/components/ui/Sidebar";
import { Toaster } from "sonner";
import { QueryProvider } from "@/components/providers/QueryProvider";
const inter = Inter({ subsets: ["latin"] });
@@ -25,15 +26,17 @@ export default function RootLayout({
}) {
return (
<ClerkProvider>
<html lang="en">
<body className={inter.className}>
<div className="flex min-h-screen bg-slate-50">
<Sidebar />
<main className="flex-1 ml-20 p-8">{children}</main>
</div>
<Toaster richColors position="top-right" />
</body>
</html>
<QueryProvider>
<html lang="en">
<body className={inter.className}>
<div className="flex min-h-screen bg-slate-50">
<Sidebar />
<main className="flex-1 ml-20 p-8">{children}</main>
</div>
<Toaster richColors position="top-right" />
</body>
</html>
</QueryProvider>
</ClerkProvider>
);
}
+56 -67
View File
@@ -1,99 +1,88 @@
"use client";
import { useEffect, useState } from "react";
import { Users, CreditCard, CalendarCheck, TrendingUp } from "lucide-react";
import { StatsCard } from "@/components/ui/StatsCard";
import { StatsCardSkeleton } from "@/components/ui/skeleton";
import { UserManagement } from "@/components/users/UserManagement";
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
import axios from "axios";
interface DashboardStats {
totalUsers: number;
activeClients: number;
totalRevenue: number;
revenueGrowth: number;
}
import { useDashboardStats } from "@/hooks/use-api";
export default function Home() {
const [stats, setStats] = useState<DashboardStats>({
totalUsers: 0,
activeClients: 0,
totalRevenue: 0,
revenueGrowth: 0,
});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchStats = async () => {
try {
const response = await axios.get("/api/admin/stats");
setStats(response.data);
} catch (error) {
console.error("Failed to fetch dashboard stats:", error);
} finally {
setLoading(false);
}
};
fetchStats();
}, []);
const { data: stats, isLoading } = useDashboardStats();
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(value);
}).format(value || 0);
};
return (
<div className="space-y-8">
<div>
<h2 className="text-3xl font-bold text-slate-900">Dashboard</h2>
<p className="text-slate-500 mt-2">Welcome back, here's what's happening today.</p>
<p className="text-slate-500 mt-2">
Welcome back, here's what's happening today.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatsCard
title="Total Users"
value={loading ? "..." : stats.totalUsers}
change="+12%" // Placeholder for now as we don't track historical growth yet
trend="up"
icon={Users}
color="blue"
/>
<StatsCard
title="Active Clients"
value={loading ? "..." : stats.activeClients}
change="+5%"
trend="up"
icon={CalendarCheck}
color="green"
/>
<StatsCard
title="Revenue"
value={loading ? "..." : formatCurrency(stats.totalRevenue)}
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}%`}
trend={stats.revenueGrowth >= 0 ? "up" : "down"}
icon={CreditCard}
color="purple"
/>
<StatsCard
title="Growth"
value="24%" // Placeholder
change="-2%"
trend="down"
icon={TrendingUp}
color="orange"
/>
{isLoading ? (
<>
<StatsCardSkeleton />
<StatsCardSkeleton />
<StatsCardSkeleton />
<StatsCardSkeleton />
</>
) : (
<>
<StatsCard
title="Total Users"
value={stats?.totalUsers ?? 0}
change="+12%"
trend="up"
icon={Users}
color="blue"
/>
<StatsCard
title="Active Clients"
value={stats?.activeClients ?? 0}
change="+5%"
trend="up"
icon={CalendarCheck}
color="green"
/>
<StatsCard
title="Revenue"
value={formatCurrency(stats?.totalRevenue ?? 0)}
change={`${(stats?.revenueGrowth ?? 0) > 0 ? "+" : ""}${stats?.revenueGrowth ?? 0}%`}
trend={(stats?.revenueGrowth ?? 0) >= 0 ? "up" : "down"}
icon={CreditCard}
color="purple"
/>
<StatsCard
title="Growth"
value="24%"
change="-2%"
trend="down"
icon={TrendingUp}
color="orange"
/>
</>
)}
</div>
<div className="flex flex-col gap-8">
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<h3 className="text-xl font-bold text-slate-900 mb-6">Recent Activity</h3>
<h3 className="text-xl font-bold text-slate-900 mb-6">
Recent Activity
</h3>
<UserManagement />
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<h3 className="text-xl font-bold text-slate-900 mb-6">Quick Analytics</h3>
<h3 className="text-xl font-bold text-slate-900 mb-6">
Quick Analytics
</h3>
<AnalyticsDashboard />
</div>
</div>
+33 -107
View File
@@ -1,86 +1,40 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useUser } from "@clerk/nextjs";
import log from "@/lib/logger";
import { toast } from "@/lib/toast";
interface User {
id: string;
firstName: string;
lastName: string;
email: string;
}
interface Recommendation {
id: string;
userId: string;
content: string;
recommendationText: string;
activityPlan: string;
dietPlan: string;
status: string;
createdAt: Date;
}
import {
useUsers,
useRecommendations,
useGenerateRecommendations,
useApproveRecommendation,
useUpdateRecommendation,
type Recommendation,
} from "@/hooks/use-api";
export default function RecommendationsPage() {
const { user } = useUser();
const [users, setUsers] = useState<User[]>([]);
const [pendingRecommendations, setPendingRecommendations] = useState<
Recommendation[]
>([]);
const [loading, setLoading] = useState(true);
const [generating, setGenerating] = useState<string | null>(null);
const [useExternalModel, setUseExternalModel] = useState(false);
useEffect(() => {
fetchData();
}, []);
const { data: users = [], isLoading: usersLoading } = useUsers();
const { data: allRecommendations = [], isLoading: recsLoading } =
useRecommendations();
const pendingRecommendations = allRecommendations.filter(
(r) => r.status === "pending",
);
const fetchData = async () => {
try {
// Fetch users - API returns { success: true, data: { users: [...] }, meta: {...} }
const usersRes = await fetch("/api/users");
const usersResult = await usersRes.json();
const usersArray = usersResult.data?.users || usersResult.users || [];
setUsers(usersArray);
// Fetch pending recommendations - API returns { success: true, data: { recommendations: [...] }, meta: {...} }
const recsRes = await fetch("/api/recommendations");
const recsResult = await recsRes.json();
const allRecs =
recsResult.data?.recommendations || recsResult.recommendations || [];
setPendingRecommendations(
allRecs.filter((r: Recommendation) => r.status === "pending"),
);
} catch (error) {
log.error("Failed to fetch data", error);
} finally {
setLoading(false);
}
};
const generateRec = useGenerateRecommendations();
const approveRec = useApproveRecommendation();
const updateRec = useUpdateRecommendation();
const handleGenerate = async (userId: string) => {
setGenerating(userId);
try {
const res = await fetch("/api/recommendations/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId, useExternalModel }),
});
if (!res.ok) {
const error = await res.json();
toast.error(`Error: ${error.error}`);
} else {
toast.success("Recommendation generated successfully!");
fetchData(); // Refresh data
}
await generateRec.mutateAsync(userId);
toast.success("Recommendation generated successfully!");
} catch (error) {
log.error("Failed to generate recommendation", error);
toast.error("Failed to generate recommendation.");
} finally {
setGenerating(null);
}
};
@@ -89,25 +43,11 @@ export default function RecommendationsPage() {
status: "approved" | "rejected",
) => {
try {
const res = await fetch("/api/recommendations/approve", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
recommendationId,
status,
approvedBy: user?.id || "admin",
}),
await approveRec.mutateAsync({
recommendationId,
approved: status === "approved",
});
if (!res.ok) {
const errorData = await res.json();
toast.error(
`Failed to update status: ${errorData.error || "Unknown error"}`,
);
} else {
toast.success("Recommendation status updated");
fetchData(); // Refresh data
}
toast.success("Recommendation status updated");
} catch (error) {
log.error("Failed to approve recommendation", error);
toast.error("Error processing request");
@@ -124,38 +64,24 @@ export default function RecommendationsPage() {
newActivityPlan === null ||
newDietPlan === null
) {
// User cancelled one of the prompts
return;
}
try {
const res = await fetch("/api/recommendations", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: rec.id,
content: newContent,
activityPlan: newActivityPlan,
dietPlan: newDietPlan,
}),
await updateRec.mutateAsync({
id: rec.id,
content: newContent,
activityPlan: newActivityPlan,
dietPlan: newDietPlan,
});
if (!res.ok) {
const errorData = await res.json();
toast.error(
`Failed to update recommendation: ${errorData.error || "Unknown error"}`,
);
} else {
toast.success("Recommendation updated successfully!");
fetchData(); // Refresh data
}
toast.success("Recommendation updated successfully!");
} catch (error) {
log.error("Failed to update recommendation", error);
toast.error("Failed to update recommendation.");
}
};
if (loading) {
if (usersLoading || recsLoading) {
return (
<div className="flex items-center justify-center h-screen">
<div className="text-xl">Loading...</div>
@@ -215,10 +141,10 @@ export default function RecommendationsPage() {
</div>
<button
onClick={() => handleGenerate(user.id)}
disabled={generating === user.id}
disabled={generateRec.isPending}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{generating === user.id ? "Generating..." : "Generate"}
{generateRec.isPending ? "Generating..." : "Generate"}
</button>
</li>
))}