Merge branch 'adminUI'

This commit is contained in:
dimitar committed 2026-03-18 03:57:25 +01:00
commit cc2dbc3423
16 files changed
+988 -435

No files matched your search

@@ -1,27 +1,26 @@
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
import { getDatabase } from '@/lib/database'
import { ensureUserSynced } from '@/lib/sync-user'
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 {
const { userId } = await auth()
if (!userId) return new NextResponse('Unauthorized', { status: 401 })
try {
const { userId } = await auth();
if (!userId) return new NextResponse("Unauthorized", { status: 401 });
const db = await getDatabase()
const db = await getDatabase();
// Ensure user is synced (handles seed script ID mismatch)
// We need to import ensureUserSynced
const user = await ensureUserSynced(userId, db)
const user = await ensureUserSynced(userId, db);
if (!user || (user.role !== 'admin' && user.role !== 'superAdmin')) {
return new NextResponse('Forbidden', { status: 403 })
}
const attendance = await db.getAllAttendance()
return NextResponse.json(attendance)
} catch (error) {
console.error('Admin attendance error:', error)
return new NextResponse('Internal Server Error', { status: 500 })
if (!user || (user.role !== "admin" && user.role !== "superAdmin")) {
return new NextResponse("Forbidden", { status: 403 });
}
const attendance = await db.getAllAttendance();
return successResponse({ records: attendance });
} catch (error) {
console.error("Admin attendance error:", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}
+95 -89
View File
@@ -1,96 +1,102 @@
'use client'
"use client";
import { useState, useEffect } from 'react'
import { format } from 'date-fns'
interface Attendance {
id: string
clientId: string
checkInTime: string
checkOutTime?: string
type: string
notes?: string
}
import { useAttendance } from "@/hooks/use-api";
import { PageHeader } from "@/components/ui/PageHeader";
import { Badge } from "@/components/ui/badge";
export default function AttendancePage() {
const [attendance, setAttendance] = useState<Attendance[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchAttendance = async () => {
try {
const res = await fetch('/api/admin/attendance')
if (res.ok) {
const data = await res.json()
setAttendance(data)
}
} catch (error) {
console.error('Error fetching attendance:', error)
} finally {
setLoading(false)
}
}
fetchAttendance()
}, [])
if (loading) {
return <div className="p-8">Loading...</div>
}
const { data: attendance = [], isLoading } = useAttendance();
if (isLoading) {
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-6">Attendance Monitoring</h1>
<div className="bg-white rounded-lg shadow overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Client ID
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Check In
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Check Out
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{attendance.map((record) => (
<tr key={record.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{record.clientId}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 capitalize">
{record.type}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{format(new Date(record.checkInTime), 'PP p')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{record.checkOutTime ? format(new Date(record.checkOutTime), 'PP p') : '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${record.checkOutTime
? 'bg-gray-100 text-gray-800'
: 'bg-green-100 text-green-800'
}`}>
{record.checkOutTime ? 'Completed' : 'Active'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="space-y-8">
<PageHeader
title="Attendance"
description="Monitor gym check-ins and check-outs"
breadcrumbs={[{ label: "Attendance" }]}
/>
<div className="card-modern">
<div className="animate-pulse space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-12 bg-muted rounded" />
))}
</div>
</div>
)
</div>
);
}
return (
<div className="space-y-8">
<PageHeader
title="Attendance"
description="Monitor gym check-ins and check-outs"
breadcrumbs={[{ label: "Attendance" }]}
/>
<div className="card-modern">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground">
Client
</th>
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground">
Type
</th>
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground">
Check In
</th>
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground">
Check Out
</th>
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground">
Status
</th>
</tr>
</thead>
<tbody>
{attendance.map((record) => (
<tr
key={record.id}
className="border-b border-border/50 hover:bg-muted/50 transition-colors"
>
<td className="py-3 px-4 text-sm">
{record.userId.substring(0, 8)}...
</td>
<td className="py-3 px-4 text-sm capitalize text-muted-foreground">
{record.type}
</td>
<td className="py-3 px-4 text-sm text-muted-foreground">
{new Date(record.checkIn).toLocaleString()}
</td>
<td className="py-3 px-4 text-sm text-muted-foreground">
{record.checkOut
? new Date(record.checkOut).toLocaleString()
: "-"}
</td>
<td className="py-3 px-4">
<Badge variant={record.checkOut ? "gray" : "success"}>
{record.checkOut ? "Completed" : "Active"}
</Badge>
</td>
</tr>
))}
{attendance.length === 0 && (
<tr>
<td
colSpan={5}
className="py-8 text-center text-muted-foreground"
>
No attendance records found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
}
+201 -29
View File
@@ -1,71 +1,107 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--background: 0 0% 98%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--success: 142.1 76.2% 36.3%;
--success-foreground: 355.7 100% 97.3%;
--warning: 37.7 93.1% 50.2%;
--warning-foreground: 222.2 47.4% 11.2%;
--info: 199.4 86.4% 48.4%;
--info-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--ring: 221.2 83.2% 53.3%;
--radius: 0.625rem;
--sidebar-background: 222.2 47.4% 7.8%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 221.2 83.2% 53.3%;
--sidebar-primary-foreground: 210 40% 98%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 217.2 91.6% 59.8%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card: 222.2 47.4% 10.2%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover: 222.2 47.4% 10.2%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--success: 142.1 76.2% 36.3%;
--success-foreground: 355.7 100% 97.3%;
--warning: 37.7 93.1% 50.2%;
--warning-foreground: 222.2 47.4% 11.2%;
--info: 199.4 86.4% 48.4%;
--info-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--ring: 224.3 76.3% 48%;
--sidebar-background: 222.2 47.4% 7.8%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 217.2 91.2% 59.8%;
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 217.2 91.6% 59.8%;
}
}
@layer base {
* {
@apply border-border;
@@ -73,4 +109,140 @@
body {
@apply bg-background text-foreground antialiased;
}
}
}
@layer utilities {
.gradient-mesh {
background:
radial-gradient(
at 40% 20%,
hsla(221, 83%, 53%, 0.1) 0px,
transparent 50%
),
radial-gradient(at 80% 0%, hsla(189, 97%, 66%, 0.1) 0px, transparent 50%),
radial-gradient(at 0% 50%, hsla(355, 85%, 93%, 0.3) 0px, transparent 50%),
radial-gradient(
at 80% 50%,
hsla(240, 75%, 98%, 0.3) 0px,
transparent 50%
),
radial-gradient(at 0% 100%, hsla(22, 100%, 92%, 0.2) 0px, transparent 50%);
}
.glass {
@apply backdrop-blur-xl bg-white/70 dark:bg-black/70;
}
.text-gradient {
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/60;
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
.animate-slide-up {
animation: slideUp 0.5s ease-out forwards;
}
.animate-scale-in {
animation: scaleIn 0.3s ease-out forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.stagger-1 {
animation-delay: 0.1s;
}
.stagger-2 {
animation-delay: 0.2s;
}
.stagger-3 {
animation-delay: 0.3s;
}
.stagger-4 {
animation-delay: 0.4s;
}
.stagger-5 {
animation-delay: 0.5s;
}
}
@layer components {
.btn-primary {
@apply inline-flex items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-all duration-200 hover:bg-primary/90 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
}
.btn-secondary {
@apply inline-flex items-center justify-center rounded-lg bg-secondary px-4 py-2 text-sm font-medium text-secondary-foreground shadow-sm transition-all duration-200 hover:bg-secondary/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
}
.btn-ghost {
@apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-muted-foreground transition-all duration-200 hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
}
.btn-danger {
@apply inline-flex items-center justify-center rounded-lg bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground shadow-sm transition-all duration-200 hover:bg-destructive/90 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
}
.card-modern {
@apply rounded-xl border border-border/50 bg-card p-6 shadow-sm transition-all duration-200 hover:shadow-md;
}
.card-elevated {
@apply rounded-xl border border-border/50 bg-card p-6 shadow-lg transition-all duration-200 hover:shadow-xl hover:-translate-y-0.5;
}
.input-modern {
@apply flex h-10 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200;
}
.badge-success {
@apply inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900/30 dark:text-green-400;
}
.badge-warning {
@apply inline-flex items-center rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-medium text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400;
}
.badge-error {
@apply inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400;
}
.badge-info {
@apply inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900/30 dark:text-blue-400;
}
.badge-default {
@apply inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-800 dark:text-gray-300;
}
}
+4 -2
View File
@@ -29,9 +29,11 @@ export default function RootLayout({
<QueryProvider>
<html lang="en">
<body className={inter.className}>
<div className="flex min-h-screen bg-slate-50">
<div className="flex min-h-screen bg-background">
<Sidebar />
<main className="flex-1 ml-20 p-8">{children}</main>
<main className="flex-1 p-6 lg:p-8 min-h-screen">
<div className="max-w-7xl mx-auto space-y-8">{children}</div>
</main>
</div>
<Toaster richColors position="top-right" />
</body>
+66 -23
View File
@@ -1,32 +1,62 @@
"use client";
import { Users, CreditCard, CalendarCheck, TrendingUp } from "lucide-react";
import { StatsCard } from "@/components/ui/StatsCard";
import { StatsCardSkeleton } from "@/components/ui/skeleton";
import {
Users,
CreditCard,
CalendarCheck,
TrendingUp,
RefreshCw,
} from "lucide-react";
import { StatsCard, StatsCardSkeleton } from "@/components/ui/StatsCard";
import { PageHeader } from "@/components/ui/PageHeader";
import { UserManagement } from "@/components/users/UserManagement";
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
import { useDashboardStats } from "@/hooks/use-api";
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
export default function Home() {
const { data: stats, isLoading } = useDashboardStats();
const { data: stats, isLoading, refetch, isFetching } = useDashboardStats();
const queryClient = useQueryClient();
const handleRefresh = () => {
queryClient.invalidateQueries({ queryKey: ["dashboard-stats"] });
queryClient.invalidateQueries({ queryKey: ["analytics"] });
refetch();
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).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>
</div>
<PageHeader
title="Dashboard"
description="Welcome back! Here's what's happening with your gym today."
actions={
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={isFetching}
className="gap-2"
>
<RefreshCw
className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`}
/>
Refresh
</Button>
}
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{isLoading ? (
<>
<StatsCardSkeleton />
@@ -72,18 +102,31 @@ export default function Home() {
)}
</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>
<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>
<AnalyticsDashboard />
{/* Main Content Grid */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* <div className="xl:col-span-2"> */}
{/* <div className="card-modern"> */}
{/* <div className="mb-6"> */}
{/* <h3 className="text-lg font-semibold">Recent Activity</h3> */}
{/* <p className="text-sm text-muted-foreground"> */}
{/* Manage and view your users */}
{/* </p> */}
{/* </div> */}
{/* <UserManagement /> */}
{/* </div> */}
{/* </div> */}
{/* Analytics Sidebar */}
<div className="xl:col-span-3">
<div className="card-modern">
<div className="mb-6">
<h3 className="text-lg font-semibold">Quick Analytics</h3>
<p className="text-sm text-muted-foreground">
Overview of your gym metrics
</p>
</div>
<AnalyticsDashboard />
</div>
</div>
</div>
</div>
+1 -1
View File
@@ -55,7 +55,7 @@ export default function ProfilePage() {
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Profile</h1>
<Button
variant={isEditing ? "primary" : "secondary"}
variant={isEditing ? "default" : "secondary"}
onClick={() => setIsEditing(!isEditing)}
>
{isEditing ? "Cancel" : "Edit Profile"}
+99 -115
View File
@@ -12,6 +12,7 @@ import {
useUpdateRecommendation,
type Recommendation,
} from "@/hooks/use-api";
import { PageHeader } from "@/components/ui/PageHeader";
export default function RecommendationsPage() {
const { user } = useUser();
@@ -83,135 +84,118 @@ export default function RecommendationsPage() {
if (usersLoading || recsLoading) {
return (
<div className="flex items-center justify-center h-screen">
<div className="text-xl">Loading...</div>
<div className="flex items-center justify-center h-64">
<div className="text-lg">Loading...</div>
</div>
);
}
return (
<div className="container mx-auto py-10 px-4">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">AI Recommendations</h1>
<div className="space-y-8">
<PageHeader
title="AI Recommendations"
description="Generate and manage AI-powered fitness recommendations"
breadcrumbs={[{ label: "AI Recommendations" }]}
/>
{/* Model Selection Toggle */}
<div className="flex items-center gap-3 bg-white px-4 py-2 rounded-lg shadow">
<span className="text-sm font-medium text-gray-700">
{useExternalModel ? "DeepSeek AI" : "Local Ollama"}
</span>
<button
onClick={() => setUseExternalModel(!useExternalModel)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
useExternalModel ? "bg-blue-600" : "bg-gray-300"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
useExternalModel ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
<span className="text-xs text-gray-500">
{useExternalModel ? "External" : "Local"}
</span>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Generate Section */}
<div>
<h2 className="text-2xl font-semibold mb-4">
Generate Recommendations
</h2>
<div className="bg-white shadow rounded-lg p-6">
<p className="mb-4 text-gray-600">
<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>
<ul className="space-y-4">
{users.map((user) => (
<li
key={user.id}
className="flex items-center justify-between border-b pb-2"
>
<div>
<p className="font-medium">
{user.firstName} {user.lastName}
</p>
<p className="text-sm text-gray-500">{user.email}</p>
</div>
<button
onClick={() => handleGenerate(user.id)}
disabled={generateRec.isPending}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{generateRec.isPending ? "Generating..." : "Generate"}
</button>
</li>
))}
{users.length === 0 && (
<p className="text-gray-500 italic">No users found.</p>
)}
</ul>
</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>
<h2 className="text-2xl font-semibold mb-4">Pending Approvals</h2>
<div className="bg-white shadow rounded-lg p-6">
{pendingRecommendations.length === 0 ? (
<p className="text-gray-500 italic">
No pending recommendations.
</p>
) : (
<ul className="space-y-6">
{pendingRecommendations.map((rec) => (
<li key={rec.id} className="border rounded p-4">
<div className="flex justify-between items-start mb-2">
<h3 className="font-bold">For: User {rec.userId}</h3>
<span className="text-xs text-gray-500">
{new Date(rec.createdAt).toLocaleDateString()}
</span>
</div>
<div className="space-y-2 text-sm mb-4">
<div>
<span className="font-semibold">Advice:</span>{" "}
{rec.recommendationText}
</div>
<div>
<span className="font-semibold">Activity:</span>{" "}
{rec.activityPlan}
</div>
<div>
<span className="font-semibold">Diet:</span>{" "}
{rec.dietPlan}
</div>
</div>
<div className="flex space-x-2">
<button
onClick={() => handleEdit(rec)}
className="flex-1 bg-blue-500 text-white py-2 rounded hover:bg-blue-600"
>
Edit
</button>
<button
onClick={() => handleApprove(rec.id, "approved")}
className="flex-1 bg-green-600 text-white py-2 rounded hover:bg-green-700"
>
Approve
</button>
<button
onClick={() => handleApprove(rec.id, "rejected")}
className="flex-1 bg-red-600 text-white py-2 rounded hover:bg-red-700"
>
Reject
</button>
</div>
</li>
))}
</ul>
)}
<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>
+13 -6
View File
@@ -1,11 +1,18 @@
import { UserManagement } from '@/components/users/UserManagement'
import { UserManagement } from "@/components/users/UserManagement";
import { PageHeader } from "@/components/ui/PageHeader";
export default function UsersPage() {
return (
<main className="min-h-screen bg-gray-50">
<div className="container mx-auto px-4 py-8">
<div className="space-y-8">
<PageHeader
title="Users"
description="Manage your gym members, trainers, and administrators"
breadcrumbs={[{ label: "Users", href: "/users" }]}
/>
<div className="card-modern">
<UserManagement />
</div>
</main>
)
}
</div>
);
}