enhancment complete with phase 10

This commit is contained in:
dimitar committed 2026-03-10 05:37:22 +01:00
1 parent 2c4927478a
commit 6adb541967
44 files changed
+7820 -845

No files matched your search

@@ -20,81 +20,47 @@ export function MembershipDistributionChart({
title = "Membership Distribution",
}: MembershipDistributionChartProps) {
const chartOptions: AgChartOptions = useMemo(
() => ({
title: {
text: title,
fontSize: 18,
fontWeight: "bold",
},
data,
series: [
{
type: "pie",
calloutLabelKey: "label",
angleKey: "value",
sectorLabelKey: "label",
fills: data.map((item) => item.color || "#3b82f6"),
strokes: ["#ffffff"],
strokeWidth: 2,
calloutLabel: {
enabled: true,
fontSize: 12,
fontWeight: "bold",
},
sectorLabel: {
enabled: true,
fontSize: 14,
fontWeight: "bold",
color: "#ffffff",
formatter: (params: any) => {
const percentage = (
(params.datum.value /
data.reduce((sum, item) => sum + item.value, 0)) *
100
).toFixed(1);
return `${params.datum.label}: ${percentage}%`;
},
},
highlightStyle: {
item: {
fillOpacity: 0.8,
stroke: "#000000",
strokeWidth: 2,
},
},
tooltip: {
enabled: true,
renderer: (params: any) => {
const percentage = (
(params.datum.value /
data.reduce((sum, item) => sum + item.value, 0)) *
100
).toFixed(1);
return `<div class="bg-white p-2 rounded shadow-lg border">
<div class="font-bold">${params.datum.label}</div>
<div class="text-sm">Count: ${params.datum.value}</div>
<div class="text-sm">Percentage: ${percentage}%</div>
</div>`;
},
},
() =>
({
title: {
text: title,
},
],
legend: {
enabled: true,
position: "right",
fontSize: 12,
marker: {
shape: "square",
size: 12,
data,
series: [
{
type: "pie",
calloutLabelKey: "label",
angleKey: "value",
sectorLabelKey: "label",
fills: data.map((item) => item.color || "#3b82f6"),
calloutLabel: {
enabled: true,
},
sectorLabel: {
enabled: true,
color: "#ffffff",
formatter: (params: any) => {
const percentage = (
(params.datum.value /
data.reduce((sum, item) => sum + item.value, 0)) *
100
).toFixed(1);
return `${percentage}%`;
},
},
tooltip: {
renderer: (params: any) => ({
title: params.datum.label,
content: `${params.datum.value} (${((params.datum.value / data.reduce((sum, item) => sum + item.value, 0)) * 100).toFixed(1)}%)`,
}),
},
},
],
legend: {
enabled: true,
position: "right",
},
},
padding: {
top: 20,
right: 20,
bottom: 20,
left: 20,
},
}),
}) as any,
[data, title],
);
@@ -20,84 +20,52 @@ export function RevenueChart({
title = "Monthly Revenue",
}: RevenueChartProps) {
const chartOptions: AgChartOptions = useMemo(
() => ({
title: {
text: title,
fontSize: 18,
fontWeight: "bold",
},
data,
series: [
{
type: "bar",
xKey: "category",
yKey: "value",
fills: data.map((item) => item.color || "#10b981"),
strokes: ["#ffffff"],
strokeWidth: 2,
cornerRadius: 4,
highlightStyle: {
item: {
fill: "#059669",
stroke: "#ffffff",
strokeWidth: 2,
() =>
({
title: {
text: title,
},
data,
series: [
{
type: "bar",
xKey: "category",
yKey: "value",
fill: "#10b981",
stroke: "#ffffff",
strokeWidth: 2,
cornerRadius: 4,
highlightStyle: {
item: {
fill: "#059669",
},
},
label: {
placement: "outside",
formatter: (params: any) => `$${params.value.toLocaleString()}`,
},
tooltip: {
renderer: (params: any) => ({
title: params.datum.category,
content: `Revenue: $${params.datum.value.toLocaleString()}`,
}),
},
},
label: {
enabled: true,
position: "top",
fontSize: 12,
fontWeight: "bold",
color: "#374151",
formatter: (params: any) => `$${params.value.toLocaleString()}`,
],
axes: [
{
type: "category",
position: "bottom",
},
tooltip: {
enabled: true,
renderer: (params: any) => {
return `<div class="bg-white p-2 rounded shadow-lg border">
<div class="font-bold">${params.datum.category}</div>
<div class="text-sm">Revenue: $${params.datum.value.toLocaleString()}</div>
</div>`;
{
type: "number",
position: "left",
label: {
formatter: (params: any) => `$${params.value.toLocaleString()}`,
},
},
},
],
axes: [
{
type: "category",
position: "bottom",
title: {
text: "Month",
fontSize: 14,
},
label: {
fontSize: 12,
rotation: 45,
},
},
{
type: "number",
position: "left",
title: {
text: "Revenue ($)",
fontSize: 14,
},
label: {
fontSize: 12,
formatter: (params: any) => `$${params.value.toLocaleString()}`,
},
},
],
legend: {
enabled: false,
},
padding: {
top: 20,
right: 20,
bottom: 60,
left: 80,
},
}),
],
}) as any,
[data, title],
);
@@ -20,63 +20,38 @@ export function UserGrowthChart({
title = "User Growth",
}: UserGrowthChartProps) {
const chartOptions: AgChartOptions = useMemo(
() => ({
title: {
text: title,
fontSize: 18,
fontWeight: "bold",
},
data,
series: [
{
type: "line",
xKey: "label",
yKey: "value",
stroke: "#3b82f6",
strokeWidth: 3,
marker: {
size: 6,
fill: "#3b82f6",
stroke: "#ffffff",
strokeWidth: 2,
},
highlightStyle: {
item: {
fill: "#1d4ed8",
() =>
({
title: {
text: title,
},
data,
series: [
{
type: "line",
xKey: "label",
yKey: "value",
stroke: "#3b82f6",
strokeWidth: 3,
marker: {
size: 6,
fill: "#3b82f6",
stroke: "#ffffff",
strokeWidth: 2,
},
},
},
],
axes: [
{
type: "category",
position: "bottom",
title: {
text: "Time Period",
fontSize: 14,
],
axes: [
{
type: "category",
position: "bottom",
},
},
{
type: "number",
position: "left",
title: {
text: "Number of Users",
fontSize: 14,
{
type: "number",
position: "left",
},
},
],
legend: {
enabled: false,
},
padding: {
top: 20,
right: 20,
bottom: 20,
left: 20,
},
}),
],
}) as any,
[data, title],
);
+112 -97
View File
@@ -4,113 +4,128 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import {
LayoutDashboard,
Users,
CalendarCheck,
CreditCard,
Settings,
LogOut,
Brain,
LayoutDashboard,
Users,
CalendarCheck,
CreditCard,
Settings,
LogOut,
Brain,
} from "lucide-react";
import { UserButton, useUser } from "@clerk/nextjs";
interface Recommendation {
id: string;
status: string;
id: string;
status: string;
}
export function Sidebar() {
const pathname = usePathname();
const { user } = useUser();
const [pendingCount, setPendingCount] = useState(0);
const pathname = usePathname();
const { user } = useUser();
const [pendingCount, setPendingCount] = useState(0);
const [mounted, setMounted] = useState(false);
useEffect(() => {
const fetchPending = async () => {
try {
const res = await fetch("/api/recommendations");
if (res.ok) {
const data = await res.json();
const pending = (data.recommendations || []).filter(
(r: Recommendation) => r.status === "pending"
);
setPendingCount(pending.length);
}
} catch (e) {
console.error("Failed to fetch pending recommendations", e);
}
};
fetchPending();
}, []);
useEffect(() => {
setMounted(true);
}, []);
const menuItems = [
{ icon: LayoutDashboard, label: "Dashboard", href: "/" },
{ icon: Users, label: "Users", href: "/users" },
{
icon: Brain,
label: (
<span className="flex items-center">
AI Recommendations
{pendingCount > 0 && (
<span className="ml-2 inline-block bg-red-600 text-xs rounded-full px-2 py-0.5">
{pendingCount}
</span>
)}
</span>
),
href: "/recommendations",
},
{ icon: CalendarCheck, label: "Attendance", href: "/attendance" },
{ icon: CreditCard, label: "Payments", href: "/payments" },
{ icon: Settings, label: "Settings", href: "/settings" },
];
useEffect(() => {
const fetchPending = async () => {
try {
const res = await fetch("/api/recommendations");
if (res.ok) {
const data = await res.json();
const pending = (data.recommendations || []).filter(
(r: Recommendation) => r.status === "pending",
);
setPendingCount(pending.length);
}
} catch (e) {
console.error("Failed to fetch pending recommendations", e);
}
};
fetchPending();
}, []);
return (
<aside className="w-20 hover:w-64 bg-slate-900 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800 transition-all duration-300 z-50 group overflow-hidden">
<div className="p-6 border-b border-slate-800 flex items-center overflow-hidden whitespace-nowrap">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
FitAI Admin
</h1>
</div>
const menuItems = [
{ icon: LayoutDashboard, label: "Dashboard", href: "/" },
{ icon: Users, label: "Users", href: "/users" },
{
icon: Brain,
label: (
<span className="flex items-center">
AI Recommendations
{pendingCount > 0 && (
<span className="ml-2 inline-block bg-red-600 text-xs rounded-full px-2 py-0.5">
{pendingCount}
</span>
)}
</span>
),
href: "/recommendations",
},
{ icon: CalendarCheck, label: "Attendance", href: "/attendance" },
{ icon: CreditCard, label: "Payments", href: "/payments" },
{ icon: Settings, label: "Settings", href: "/settings" },
];
<nav className="flex-1 p-4 space-y-2">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
const label = typeof item.label === "string" ? item.label : item.label;
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 ${isActive
? "bg-blue-600 text-white shadow-lg shadow-blue-900/20"
: "text-slate-400 hover:bg-slate-800 hover:text-white"}`}
>
<div className="min-w-[20px]">
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-white"} />
</div>
<span className="font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-300 whitespace-nowrap overflow-hidden">
{label}
</span>
</Link>
);
})}
</nav>
return (
<aside className="w-20 hover:w-64 bg-slate-900 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800 transition-all duration-300 z-50 group overflow-hidden">
<div className="p-6 border-b border-slate-800 flex items-center overflow-hidden whitespace-nowrap">
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
FitAI Admin
</h1>
</div>
<div className="p-4 border-t border-slate-800">
<div className="flex items-center gap-3 px-2 py-3 rounded-lg bg-slate-800/50 overflow-hidden whitespace-nowrap">
<div className="min-w-[32px]">
<UserButton afterSignOutUrl="/" />
</div>
<div className="flex-1 min-w-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<p className="text-sm font-medium text-white truncate">
{user?.fullName || "Admin User"}
</p>
<p className="text-xs text-slate-400 truncate">
{user?.primaryEmailAddress?.emailAddress}
</p>
</div>
</div>
</div>
</aside>
);
<nav className="flex-1 p-4 space-y-2">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
const label =
typeof item.label === "string" ? item.label : item.label;
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 ${
isActive
? "bg-blue-600 text-white shadow-lg shadow-blue-900/20"
: "text-slate-400 hover:bg-slate-800 hover:text-white"
}`}
>
<div className="min-w-[20px]">
<Icon
size={20}
className={
isActive
? "text-white"
: "text-slate-500 group-hover:text-white"
}
/>
</div>
<span className="font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-300 whitespace-nowrap overflow-hidden">
{label}
</span>
</Link>
);
})}
</nav>
<div className="p-4 border-t border-slate-800">
<div className="flex items-center gap-3 px-2 py-3 rounded-lg bg-slate-800/50 overflow-hidden whitespace-nowrap">
<div className="min-w-[32px]">
{mounted && <UserButton afterSignOutUrl="/" />}
</div>
<div className="flex-1 min-w-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<p className="text-sm font-medium text-white truncate">
{user?.fullName || "Admin User"}
</p>
<p className="text-xs text-slate-400 truncate">
{user?.primaryEmailAddress?.emailAddress}
</p>
</div>
</div>
</div>
</aside>
);
}
+5 -3
View File
@@ -272,7 +272,7 @@ export function UserGrid({
flex: 1,
resizable: true,
floatingFilter: true,
suppressMenu: true,
suppressHeaderMenuButton: true,
}),
[],
);
@@ -284,7 +284,10 @@ export function UserGrid({
columnDefs,
defaultColDef,
rowData: users,
rowSelection: "multiple" as const,
rowSelection: {
mode: "multiRow" as const,
enableClickSelection: true,
},
onSelectionChanged: () => {
const selectedNodes = gridRef.current?.api.getSelectedNodes();
const selectedData =
@@ -295,7 +298,6 @@ export function UserGrid({
onUserSelect(selectedData[0]);
}
},
suppressRowClickSelection: false,
animateRows: true,
loading: loading,
pagination: true,
@@ -91,7 +91,8 @@ export function UserManagement() {
ok: response.ok,
status: response.status,
});
const data = await response.json();
const responseData = await response.json();
const data = responseData.data || responseData; // Handle both old and new API response formats
log.debug("Received users data", {
count: Array.isArray(data.users) ? data.users.length : 0,
sample: