diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 4300f66..f3c8e1c 100644 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/admin/src/app/api/admin/attendance/route.ts b/apps/admin/src/app/api/admin/attendance/route.ts index cf78c80..c540337 100644 --- a/apps/admin/src/app/api/admin/attendance/route.ts +++ b/apps/admin/src/app/api/admin/attendance/route.ts @@ -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 }); + } } diff --git a/apps/admin/src/app/attendance/page.tsx b/apps/admin/src/app/attendance/page.tsx index 960aeba..e9795dc 100644 --- a/apps/admin/src/app/attendance/page.tsx +++ b/apps/admin/src/app/attendance/page.tsx @@ -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([]) - 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
Loading...
- } + const { data: attendance = [], isLoading } = useAttendance(); + if (isLoading) { return ( -
-

Attendance Monitoring

- -
- - - - - - - - - - - - {attendance.map((record) => ( - - - - - - - - ))} - -
- Client ID - - Type - - Check In - - Check Out - - Status -
- {record.clientId} - - {record.type} - - {format(new Date(record.checkInTime), 'PP p')} - - {record.checkOutTime ? format(new Date(record.checkOutTime), 'PP p') : '-'} - - - {record.checkOutTime ? 'Completed' : 'Active'} - -
-
+
+ +
+
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
- ) +
+ ); + } + + return ( +
+ + +
+
+ + + + + + + + + + + + {attendance.map((record) => ( + + + + + + + + ))} + {attendance.length === 0 && ( + + + + )} + +
+ Client + + Type + + Check In + + Check Out + + Status +
+ {record.userId.substring(0, 8)}... + + {record.type} + + {new Date(record.checkIn).toLocaleString()} + + {record.checkOut + ? new Date(record.checkOut).toLocaleString() + : "-"} + + + {record.checkOut ? "Completed" : "Active"} + +
+ No attendance records found +
+
+
+
+ ); } diff --git a/apps/admin/src/app/globals.css b/apps/admin/src/app/globals.css index 13259e0..d25a1d6 100644 --- a/apps/admin/src/app/globals.css +++ b/apps/admin/src/app/globals.css @@ -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; } -} \ No newline at end of file +} + +@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; + } +} diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx index b45416c..5b0e439 100644 --- a/apps/admin/src/app/layout.tsx +++ b/apps/admin/src/app/layout.tsx @@ -29,9 +29,11 @@ export default function RootLayout({ -
+
-
{children}
+
+
{children}
+
diff --git a/apps/admin/src/app/page.tsx b/apps/admin/src/app/page.tsx index e2fdaad..76278eb 100644 --- a/apps/admin/src/app/page.tsx +++ b/apps/admin/src/app/page.tsx @@ -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 (
-
-

Dashboard

-

- Welcome back, here's what's happening today. -

-
+ + + Refresh + + } + /> -
+ {/* Stats Grid */} +
{isLoading ? ( <> @@ -72,18 +102,31 @@ export default function Home() { )}
-
-
-

- Recent Activity -

- -
-
-

- Quick Analytics -

- + {/* Main Content Grid */} +
+ {/*
*/} + {/*
*/} + {/*
*/} + {/*

Recent Activity

*/} + {/*

*/} + {/* Manage and view your users */} + {/*

*/} + {/*
*/} + {/* */} + {/*
*/} + {/*
*/} + + {/* Analytics Sidebar */} +
+
+
+

Quick Analytics

+

+ Overview of your gym metrics +

+
+ +
diff --git a/apps/admin/src/app/profile/page.tsx b/apps/admin/src/app/profile/page.tsx index 43533e2..03c1a86 100644 --- a/apps/admin/src/app/profile/page.tsx +++ b/apps/admin/src/app/profile/page.tsx @@ -55,7 +55,7 @@ export default function ProfilePage() {

Profile

- - {useExternalModel ? "External" : "Local"} - -
-
- -
+
{/* Generate Section */} -
-

- Generate Recommendations -

-
-

+

+
+

Generate Recommendations

+

Select a user to generate a new daily recommendation.

-
    - {users.map((user) => ( -
  • -
    -

    - {user.firstName} {user.lastName} -

    -

    {user.email}

    -
    - -
  • - ))} - {users.length === 0 && ( -

    No users found.

    - )} -
+
    + {users.map((user) => ( +
  • +
    +

    + {user.firstName} {user.lastName} +

    +

    {user.email}

    +
    + +
  • + ))} + {users.length === 0 && ( +

    No users found.

    + )} +
{/* Pending Approvals Section */} -
-

Pending Approvals

-
- {pendingRecommendations.length === 0 ? ( -

- No pending recommendations. -

- ) : ( -
    - {pendingRecommendations.map((rec) => ( -
  • -
    -

    For: User {rec.userId}

    - - {new Date(rec.createdAt).toLocaleDateString()} - -
    -
    -
    - Advice:{" "} - {rec.recommendationText} -
    -
    - Activity:{" "} - {rec.activityPlan} -
    -
    - Diet:{" "} - {rec.dietPlan} -
    -
    -
    - - - -
    -
  • - ))} -
- )} +
+
+

Pending Approvals

+

+ Review and approve AI-generated recommendations +

+ {pendingRecommendations.length === 0 ? ( +

+ No pending recommendations. +

+ ) : ( +
    + {pendingRecommendations.map((rec) => ( +
  • +
    +

    For: User {rec.userId}

    + + {new Date(rec.createdAt).toLocaleDateString()} + +
    +
    +
    + Advice:{" "} + {rec.recommendationText} +
    +
    + Activity:{" "} + {rec.activityPlan} +
    +
    + Diet: {rec.dietPlan} +
    +
    +
    + + + +
    +
  • + ))} +
+ )}
diff --git a/apps/admin/src/app/users/page.tsx b/apps/admin/src/app/users/page.tsx index d0a6c05..f631cb4 100644 --- a/apps/admin/src/app/users/page.tsx +++ b/apps/admin/src/app/users/page.tsx @@ -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 ( -
-
+
+ + +
-
- ) -} \ No newline at end of file +
+ ); +} diff --git a/apps/admin/src/components/Navigation.tsx b/apps/admin/src/components/Navigation.tsx index 44e561f..1bf2fc9 100644 --- a/apps/admin/src/components/Navigation.tsx +++ b/apps/admin/src/components/Navigation.tsx @@ -71,11 +71,11 @@ export function Navigation(): ReactElement {
  • + + {/* Navigation */} +
  • - - {label} - - - ); - })} - + {!isCollapsed && ( + <> + {item.label} + {item.badge !== undefined && ( + + {item.badge > 99 ? "99+" : item.badge} + + )} + + )} + {isCollapsed && item.badge !== undefined && ( + + {item.badge > 9 ? "9+" : item.badge} + + )} + + ); + })} + -
    -
    -
    - {mounted && } -
    -
    -

    - {user?.fullName || "Admin User"} -

    -

    - {user?.primaryEmailAddress?.emailAddress} -

    + {/* User Section */} +
    +
    +
    + {mounted && } +
    + {!isCollapsed && ( +
    +

    + {user?.fullName || "Admin"} +

    +

    + {user?.primaryEmailAddress?.emailAddress} +

    +
    + )}
    diff --git a/apps/admin/src/components/ui/StatsCard.tsx b/apps/admin/src/components/ui/StatsCard.tsx index 76cb699..25dbd3e 100644 --- a/apps/admin/src/components/ui/StatsCard.tsx +++ b/apps/admin/src/components/ui/StatsCard.tsx @@ -1,51 +1,139 @@ import { LucideIcon } from "lucide-react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; +import { TrendingUp, TrendingDown, Minus } from "lucide-react"; interface StatsCardProps { - title: string; - value: string | number; - change?: string; - trend?: "up" | "down" | "neutral"; - icon: LucideIcon; - color?: "blue" | "green" | "purple" | "orange"; + title: string; + value: string | number; + change?: string; + trend?: "up" | "down" | "neutral"; + icon: LucideIcon; + color?: "blue" | "green" | "purple" | "orange" | "red"; + className?: string; } -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", - }; +export function StatsCard({ + title, + value, + change, + trend, + icon: Icon, + color = "blue", + className, +}: StatsCardProps) { + const colorStyles = { + blue: { + gradient: "from-blue-500/10 to-blue-600/5", + iconBg: "bg-gradient-to-br from-blue-500 to-blue-600", + iconShadow: "shadow-blue-500/25", + text: "text-blue-600", + trendUp: "text-emerald-600", + trendDown: "text-red-600", + }, + green: { + gradient: "from-emerald-500/10 to-emerald-600/5", + iconBg: "bg-gradient-to-br from-emerald-500 to-emerald-600", + iconShadow: "shadow-emerald-500/25", + text: "text-emerald-600", + trendUp: "text-emerald-600", + trendDown: "text-red-600", + }, + purple: { + gradient: "from-violet-500/10 to-violet-600/5", + iconBg: "bg-gradient-to-br from-violet-500 to-violet-600", + iconShadow: "shadow-violet-500/25", + text: "text-violet-600", + trendUp: "text-emerald-600", + trendDown: "text-red-600", + }, + orange: { + gradient: "from-amber-500/10 to-amber-600/5", + iconBg: "bg-gradient-to-br from-amber-500 to-orange-600", + iconShadow: "shadow-amber-500/25", + text: "text-amber-600", + trendUp: "text-emerald-600", + trendDown: "text-red-600", + }, + red: { + gradient: "from-rose-500/10 to-rose-600/5", + iconBg: "bg-gradient-to-br from-rose-500 to-rose-600", + iconShadow: "shadow-rose-500/25", + text: "text-rose-600", + trendUp: "text-emerald-600", + trendDown: "text-red-600", + }, + }; - return ( - - - - {title} - -
    - -
    -
    - -
    {value}
    - {change && ( -

    - - {change} - {" "} - vs last month -

    + const styles = colorStyles[color]; + const TrendIcon = + trend === "up" ? TrendingUp : trend === "down" ? TrendingDown : Minus; + + return ( +
    +
    +
    +

    {title}

    +
    + {value} +
    + {change && ( +
    + - - ); + /> + + {change} + + vs last month +
    + )} +
    +
    + +
    +
    + + {/* Decorative corner gradient */} +
    +
    + ); +} + +export function StatsCardSkeleton() { + return ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ); } diff --git a/apps/admin/src/components/ui/badge.tsx b/apps/admin/src/components/ui/badge.tsx new file mode 100644 index 0000000..4081270 --- /dev/null +++ b/apps/admin/src/components/ui/badge.tsx @@ -0,0 +1,62 @@ +import { cn } from "@/lib/utils"; +import { cva, type VariantProps } from "class-variance-authority"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "bg-primary/10 text-primary", + secondary: "bg-secondary text-secondary-foreground", + success: + "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400", + warning: + "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400", + destructive: + "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400", + info: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400", + outline: "border border-input text-foreground", + gray: "bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-300", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +export function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
    + ); +} + +export function StatusBadge({ status }: { status: string }) { + const statusConfig: Record< + string, + { variant: BadgeProps["variant"]; label: string } + > = { + active: { variant: "success", label: "Active" }, + inactive: { variant: "gray", label: "Inactive" }, + pending: { variant: "warning", label: "Pending" }, + approved: { variant: "success", label: "Approved" }, + rejected: { variant: "destructive", label: "Rejected" }, + completed: { variant: "success", label: "Completed" }, + failed: { variant: "destructive", label: "Failed" }, + suspended: { variant: "destructive", label: "Suspended" }, + basic: { variant: "default", label: "Basic" }, + premium: { variant: "info", label: "Premium" }, + vip: { variant: "warning", label: "VIP" }, + }; + + const config = statusConfig[status.toLowerCase()] || { + variant: "outline", + label: status, + }; + + return {config.label}; +} diff --git a/apps/admin/src/components/ui/button.tsx b/apps/admin/src/components/ui/button.tsx index 0b60055..36025e1 100644 --- a/apps/admin/src/components/ui/button.tsx +++ b/apps/admin/src/components/ui/button.tsx @@ -1,49 +1,60 @@ -import React from 'react' -import { cn } from '@/lib/utils' -import { Loader2 } from 'lucide-react' +import React from "react"; +import { cn } from "@/lib/utils"; +import { Loader2 } from "lucide-react"; +import { cva, type VariantProps } from "class-variance-authority"; -interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'outline' | 'default' - size?: 'default' | 'sm' | 'lg' | 'icon' - isLoading?: boolean - children: React.ReactNode +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-lg text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 hover:shadow", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + isLoading?: boolean; } -export function Button({ - variant = 'primary', - size = 'default', - isLoading = false, - children, - className = '', - disabled, - ...props -}: ButtonProps) { - const baseClasses = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50' - - const variantClasses = { - primary: 'bg-blue-600 text-white hover:bg-blue-700 shadow hover:bg-blue-700/90', - default: 'bg-blue-600 text-white hover:bg-blue-700 shadow hover:bg-blue-700/90', - secondary: 'bg-slate-100 text-slate-900 hover:bg-slate-200/80', - ghost: 'hover:bg-slate-100 hover:text-slate-900', - destructive: 'bg-red-500 text-white hover:bg-red-600/90', - outline: 'border border-input bg-transparent shadow-sm hover:bg-slate-100 hover:text-slate-900' - } - - const sizeClasses = { - default: 'h-9 px-4 py-2', - sm: 'h-8 rounded-md px-3 text-xs', - lg: 'h-10 rounded-md px-8', - icon: 'h-9 w-9' - } - - return ( - - ) -} \ No newline at end of file +export const Button = React.forwardRef( + ( + { className, variant, size, isLoading, children, disabled, ...props }, + ref, + ) => { + return ( + + ); + }, +); +Button.displayName = "Button"; diff --git a/apps/admin/src/hooks/use-api.ts b/apps/admin/src/hooks/use-api.ts index d9150d8..87be6d3 100644 --- a/apps/admin/src/hooks/use-api.ts +++ b/apps/admin/src/hooks/use-api.ts @@ -58,6 +58,7 @@ export interface AttendanceRecord { checkIn: string; checkOut?: string; date: string; + type?: string; } async function fetchApi(url: string, options?: RequestInit): Promise {