+
+
Dashboard
+
Welcome back, here's what's happening today.
+
+
+
+
+
+ 0 ? "+" : ""}${stats.revenueGrowth}%`}
+ trend={stats.revenueGrowth >= 0 ? "up" : "down"}
+ icon={CreditCard}
+ color="purple"
+ />
+
+
+
+
+
+
Recent Activity
+
-
-
-
Client Management
-
- Manage fitness clients and their profiles
-
-
-
-
Payment Tracking
-
Monitor payments and subscriptions
-
-
-
Attendance
-
Track client attendance and habits
-
-
-
-
-
-
- Recent User Activity
-
-
-
-
-
-
-
+
-
+
);
}
diff --git a/apps/admin/src/components/ui/Sidebar.tsx b/apps/admin/src/components/ui/Sidebar.tsx
new file mode 100644
index 0000000..005b8cf
--- /dev/null
+++ b/apps/admin/src/components/ui/Sidebar.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import {
+ LayoutDashboard,
+ Users,
+ CalendarCheck,
+ CreditCard,
+ Settings,
+ LogOut
+} from "lucide-react";
+import { UserButton, useUser } from "@clerk/nextjs";
+
+const menuItems = [
+ { icon: LayoutDashboard, label: "Dashboard", href: "/" },
+ { icon: Users, label: "Users", href: "/users" },
+ { icon: CalendarCheck, label: "Attendance", href: "/attendance" },
+ { icon: CreditCard, label: "Payments", href: "/payments" },
+ { icon: Settings, label: "Settings", href: "/settings" },
+];
+
+export function Sidebar() {
+ const pathname = usePathname();
+ const { user } = useUser();
+
+ return (
+
+ );
+}
diff --git a/apps/admin/src/components/ui/StatsCard.tsx b/apps/admin/src/components/ui/StatsCard.tsx
new file mode 100644
index 0000000..76cb699
--- /dev/null
+++ b/apps/admin/src/components/ui/StatsCard.tsx
@@ -0,0 +1,51 @@
+import { LucideIcon } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+interface StatsCardProps {
+ title: string;
+ value: string | number;
+ change?: string;
+ trend?: "up" | "down" | "neutral";
+ icon: LucideIcon;
+ color?: "blue" | "green" | "purple" | "orange";
+}
+
+export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
+ const colorStyles = {
+ blue: "bg-blue-50 text-blue-600",
+ green: "bg-green-50 text-green-600",
+ purple: "bg-purple-50 text-purple-600",
+ orange: "bg-orange-50 text-orange-600",
+ };
+
+ return (
+
+
+
+ {title}
+
+
+
+
+
+
+ {value}
+ {change && (
+
+
+ {change}
+ {" "}
+ vs last month
+
+ )}
+
+
+ );
+}
diff --git a/apps/admin/src/components/ui/card.tsx b/apps/admin/src/components/ui/card.tsx
new file mode 100644
index 0000000..59a6010
--- /dev/null
+++ b/apps/admin/src/components/ui/card.tsx
@@ -0,0 +1,76 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/apps/admin/src/components/users/UserGrid.tsx b/apps/admin/src/components/users/UserGrid.tsx
index 969f284..f29e4cb 100644
--- a/apps/admin/src/components/users/UserGrid.tsx
+++ b/apps/admin/src/components/users/UserGrid.tsx
@@ -69,6 +69,7 @@ export function UserGrid({
filter: "agTextColumnFilter",
sortable: true,
cellRenderer: (params: any) => {
+ if (!params.value) return null;
const roleColors = {
admin: "bg-purple-100 text-purple-800",
trainer: "bg-blue-100 text-blue-800",
@@ -77,7 +78,14 @@ export function UserGrid({
const colorClass =
roleColors[params.value as keyof typeof roleColors] ||
"bg-gray-100 text-gray-800";
- return `${params.value}`;
+
+ const label = params.value.charAt(0).toUpperCase() + params.value.slice(1);
+
+ return (
+
+ {label}
+
+ );
},
minWidth: 120,
},
@@ -87,6 +95,7 @@ export function UserGrid({
filter: "agTextColumnFilter",
sortable: true,
minWidth: 130,
+ valueFormatter: (params: any) => params.value || "N/A",
},
{
headerName: "Membership",
@@ -94,17 +103,24 @@ export function UserGrid({
filter: "agTextColumnFilter",
sortable: true,
cellRenderer: (params: any) => {
- if (!params.value || params.value === "N/A") return "N/A";
+ if (!params.value || params.value === "N/A") return N/A;
const membershipColors = {
- vip: "bg-yellow-100 text-yellow-800",
- premium: "bg-blue-100 text-blue-800",
- basic: "bg-gray-100 text-gray-800",
+ vip: "bg-yellow-100 text-yellow-800 border-yellow-200",
+ premium: "bg-blue-100 text-blue-800 border-blue-200",
+ basic: "bg-slate-100 text-slate-800 border-slate-200",
};
const colorClass =
membershipColors[params.value as keyof typeof membershipColors] ||
"bg-gray-100 text-gray-800";
- return `${params.value}`;
+
+ const label = params.value.charAt(0).toUpperCase() + params.value.slice(1);
+
+ return (
+
+ {label}
+
+ );
},
minWidth: 120,
},
@@ -114,17 +130,25 @@ export function UserGrid({
filter: "agTextColumnFilter",
sortable: true,
cellRenderer: (params: any) => {
- if (!params.value || params.value === "N/A") return "N/A";
+ if (!params.value || params.value === "N/A") return N/A;
const statusColors = {
active: "bg-green-100 text-green-800",
inactive: "bg-red-100 text-red-800",
- suspended: "bg-yellow-100 text-yellow-800",
+ suspended: "bg-orange-100 text-orange-800",
+ expired: "bg-gray-100 text-gray-800",
};
const colorClass =
statusColors[params.value as keyof typeof statusColors] ||
"bg-gray-100 text-gray-800";
- return `${params.value}`;
+
+ const label = params.value.charAt(0).toUpperCase() + params.value.slice(1);
+
+ return (
+
+ {label}
+
+ );
},
minWidth: 120,
},
diff --git a/apps/admin/src/lib/database/sqlite.ts b/apps/admin/src/lib/database/sqlite.ts
index b416180..9877336 100644
--- a/apps/admin/src/lib/database/sqlite.ts
+++ b/apps/admin/src/lib/database/sqlite.ts
@@ -370,6 +370,12 @@ export class SQLiteDatabase implements IDatabase {
attendance.type, attendance.notes, attendance.createdAt.toISOString()
)
+ // Update client last visit
+ this.db.prepare('UPDATE clients SET lastVisit = ? WHERE id = ?').run(
+ now.toISOString(),
+ clientId
+ )
+
return attendance
}
@@ -432,7 +438,8 @@ export class SQLiteDatabase implements IDatabase {
userId: row.userId,
membershipType: row.membershipType,
membershipStatus: row.membershipStatus,
- joinDate: new Date(row.joinDate)
+ joinDate: new Date(row.joinDate),
+ lastVisit: row.lastVisit ? new Date(row.lastVisit) : undefined
}
}
@@ -464,4 +471,43 @@ export class SQLiteDatabase implements IDatabase {
createdAt: new Date(row.createdAt)
}
}
+
+ async getDashboardStats(): Promise<{
+ totalUsers: number;
+ activeClients: number;
+ totalRevenue: number;
+ revenueGrowth: number;
+ }> {
+ if (!this.db) throw new Error('Database not connected')
+
+ // Total Users
+ const userCountStmt = this.db.prepare('SELECT COUNT(*) as count FROM users')
+ const userCount = (userCountStmt.get() as any).count
+
+ // Active Clients
+ const activeClientCountStmt = this.db.prepare("SELECT COUNT(*) as count FROM clients WHERE membershipStatus = 'active'")
+ const activeClientCount = (activeClientCountStmt.get() as any).count
+
+ // Total Revenue (assuming payments table exists, handling if it's empty)
+ // Note: We need to create the payments table first if it doesn't exist in createTables
+ // For now, returning 0 if table doesn't exist or is empty
+ let totalRevenue = 0
+ let revenueGrowth = 0
+
+ try {
+ const revenueStmt = this.db.prepare('SELECT SUM(amount) as total FROM payments WHERE status = "completed"')
+ const revenueResult = revenueStmt.get() as any
+ totalRevenue = revenueResult?.total || 0
+ } catch (e) {
+ // Table might not exist yet
+ console.warn('Payments table query failed, returning 0 revenue')
+ }
+
+ return {
+ totalUsers: userCount,
+ activeClients: activeClientCount,
+ totalRevenue,
+ revenueGrowth
+ }
+ }
}
\ No newline at end of file
diff --git a/apps/admin/src/lib/database/types.ts b/apps/admin/src/lib/database/types.ts
index d8e7bf2..16de2b5 100644
--- a/apps/admin/src/lib/database/types.ts
+++ b/apps/admin/src/lib/database/types.ts
@@ -17,6 +17,7 @@ export interface Client {
membershipType: "basic" | "premium" | "vip";
membershipStatus: "active" | "inactive" | "expired";
joinDate: Date;
+ lastVisit?: Date;
}
export interface FitnessProfile {
@@ -88,6 +89,14 @@ export interface IDatabase {
getAttendanceHistory(clientId: string): Promise;
getAllAttendance(): Promise;
getActiveCheckIn(clientId: string): Promise;
+
+ // Dashboard operations
+ getDashboardStats(): Promise<{
+ totalUsers: number;
+ activeClients: number;
+ totalRevenue: number;
+ revenueGrowth: number; // Percentage vs last month
+ }>;
}
// Database configuration
diff --git a/apps/admin/src/lib/utils.ts b/apps/admin/src/lib/utils.ts
index ae40506..41044eb 100644
--- a/apps/admin/src/lib/utils.ts
+++ b/apps/admin/src/lib/utils.ts
@@ -1,26 +1,14 @@
-export const formatDate = (date: Date): string => {
- return new Intl.DateTimeFormat('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- }).format(date)
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
}
-export const formatCurrency = (
- amount: number,
- currency: string = 'USD'
-): string => {
- return new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency,
- }).format(amount)
-}
-
-export const calculateDaysBetween = (startDate: Date, endDate: Date): number => {
- const timeDiff = endDate.getTime() - startDate.getTime()
- return Math.ceil(timeDiff / (1000 * 3600 * 24))
-}
-
-export const generateId = (): string => {
- return Math.random().toString(36).substr(2, 9)
+export function formatDate(date: string | Date) {
+ return new Date(date).toLocaleDateString("en-US", {
+ month: "long",
+ day: "numeric",
+ year: "numeric",
+ })
}
\ No newline at end of file
diff --git a/apps/admin/tailwind.config.js b/apps/admin/tailwind.config.js
index 905d512..0122a7d 100644
--- a/apps/admin/tailwind.config.js
+++ b/apps/admin/tailwind.config.js
@@ -1,21 +1,76 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
+ darkMode: ["class"],
content: [
- './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
- './src/components/**/*.{js,ts,jsx,tsx,mdx}',
- './src/app/**/*.{js,ts,jsx,tsx,mdx}',
+ './pages/**/*.{ts,tsx}',
+ './components/**/*.{ts,tsx}',
+ './app/**/*.{ts,tsx}',
+ './src/**/*.{ts,tsx}',
],
theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
extend: {
colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
primary: {
- 50: '#eff6ff',
- 500: '#3b82f6',
- 600: '#2563eb',
- 700: '#1d4ed8',
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
},
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: 0 },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: 0 },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
},
},
},
- plugins: [],
+ plugins: [require("tailwindcss-animate")],
}
\ No newline at end of file
diff --git a/apps/admin/userFlow.md b/apps/admin/userFlow.md
new file mode 100644
index 0000000..ce60cea
--- /dev/null
+++ b/apps/admin/userFlow.md
@@ -0,0 +1,3 @@
+## user flow
+
+superAdmin -> localAdmin/Gym -> localAdmin -> creates trainers -> can add users