This commit is contained in:
“kirukib” 2026-02-27 19:03:47 +03:00
parent c7447f68ad
commit e35defe48a
7 changed files with 571 additions and 387 deletions

2
.env
View File

@ -1,2 +1,2 @@
VITE_API_BASE_URL= https://api.yimaru.yaltopia.com/ VITE_API_BASE_URL= https://api.yimaru.yaltopia.com/api/v1
VITE_GOOGLE_CLIENT_ID=google_client_id VITE_GOOGLE_CLIENT_ID=google_client_id

View File

@ -27,7 +27,7 @@ import {
} from "recharts" } from "recharts"
import { StatCard } from "../components/dashboard/StatCard" import { StatCard } from "../components/dashboard/StatCard"
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"
// import { cn } from "../lib/utils" import { cn } from "../lib/utils"
import { getTeamMemberById } from "../api/team.api" import { getTeamMemberById } from "../api/team.api"
import { getDashboard } from "../api/analytics.api" import { getDashboard } from "../api/analytics.api"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
@ -44,6 +44,7 @@ export function DashboardPage() {
const [userFirstName, setUserFirstName] = useState<string>("") const [userFirstName, setUserFirstName] = useState<string>("")
const [dashboard, setDashboard] = useState<DashboardData | null>(null) const [dashboard, setDashboard] = useState<DashboardData | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [activeStatTab, setActiveStatTab] = useState<"primary" | "secondary">("primary")
useEffect(() => { useEffect(() => {
const fetchUser = async () => { const fetchUser = async () => {
@ -115,69 +116,109 @@ export function DashboardPage() {
<div className="flex items-center justify-center py-20 text-destructive">Failed to load dashboard data.</div> <div className="flex items-center justify-center py-20 text-destructive">Failed to load dashboard data.</div>
) : ( ) : (
<> <>
{/* Stat Cards */} {/* Stat tabs */}
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4"> <div className="mb-3 border-b border-grayScale-200">
<StatCard <div className="-mb-px flex gap-6">
icon={Users} <button
label="Total Users" type="button"
value={dashboard.users.total_users.toLocaleString()} onClick={() => setActiveStatTab("primary")}
deltaLabel={`+${dashboard.users.new_month} this month`} className={cn(
deltaPositive={dashboard.users.new_month > 0} "relative px-1 pb-3.5 pt-1 text-sm font-semibold transition-all",
/> activeStatTab === "primary"
<StatCard ? "text-brand-600"
icon={BadgeCheck} : "text-grayScale-400 hover:text-grayScale-700",
label="Active Subscribers" )}
value={dashboard.subscriptions.active_subscriptions.toLocaleString()} >
deltaLabel={`+${dashboard.subscriptions.new_month} this month`} Overview
deltaPositive={dashboard.subscriptions.new_month > 0} {activeStatTab === "primary" && (
/> <span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-brand-500" />
<StatCard )}
icon={DollarSign} </button>
label="Total Revenue (ETB)" <button
value={dashboard.payments.total_revenue.toLocaleString()} type="button"
deltaLabel={`${dashboard.payments.total_payments} payments`} onClick={() => setActiveStatTab("secondary")}
deltaPositive={dashboard.payments.total_revenue > 0} className={cn(
/> "relative px-1 pb-3.5 pt-1 text-sm font-semibold transition-all",
<StatCard activeStatTab === "secondary"
icon={TicketCheck} ? "text-brand-600"
label="Issues" : "text-grayScale-400 hover:text-grayScale-700",
value={`${dashboard.issues.resolved_issues}/${dashboard.issues.total_issues}`} )}
deltaLabel={`${(dashboard.issues.resolution_rate * 100).toFixed(1)}% resolved`} >
deltaPositive={dashboard.issues.resolution_rate > 0.5} More metrics
/> {activeStatTab === "secondary" && (
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-brand-500" />
)}
</button>
</div>
</div> </div>
{/* Stat Cards */}
{activeStatTab === "primary" && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatCard
icon={Users}
label="Total Users"
value={dashboard.users.total_users.toLocaleString()}
deltaLabel={`+${dashboard.users.new_month} this month`}
deltaPositive={dashboard.users.new_month > 0}
/>
<StatCard
icon={BadgeCheck}
label="Active Subscribers"
value={dashboard.subscriptions.active_subscriptions.toLocaleString()}
deltaLabel={`+${dashboard.subscriptions.new_month} this month`}
deltaPositive={dashboard.subscriptions.new_month > 0}
/>
<StatCard
icon={DollarSign}
label="Total Revenue (ETB)"
value={dashboard.payments.total_revenue.toLocaleString()}
deltaLabel={`${dashboard.payments.total_payments} payments`}
deltaPositive={dashboard.payments.total_revenue > 0}
/>
<StatCard
icon={TicketCheck}
label="Issues"
value={`${dashboard.issues.resolved_issues}/${dashboard.issues.total_issues}`}
deltaLabel={`${(dashboard.issues.resolution_rate * 100).toFixed(1)}% resolved`}
deltaPositive={dashboard.issues.resolution_rate > 0.5}
/>
</div>
)}
{/* Secondary Stats */} {/* Secondary Stats */}
<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-4"> {activeStatTab === "secondary" && (
<StatCard <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
icon={BookOpen} <StatCard
label="Courses" icon={BookOpen}
value={dashboard.courses.total_courses.toLocaleString()} label="Courses"
deltaLabel={`${dashboard.courses.total_sub_courses} sub-courses, ${dashboard.courses.total_videos} videos`} value={dashboard.courses.total_courses.toLocaleString()}
deltaPositive deltaLabel={`${dashboard.courses.total_sub_courses} sub-courses, ${dashboard.courses.total_videos} videos`}
/> deltaPositive
<StatCard />
icon={HelpCircle} <StatCard
label="Questions" icon={HelpCircle}
value={dashboard.content.total_questions.toLocaleString()} label="Questions"
deltaLabel={`${dashboard.content.total_question_sets} question sets`} value={dashboard.content.total_questions.toLocaleString()}
deltaPositive deltaLabel={`${dashboard.content.total_question_sets} question sets`}
/> deltaPositive
<StatCard />
icon={Bell} <StatCard
label="Notifications" icon={Bell}
value={dashboard.notifications.total_sent.toLocaleString()} label="Notifications"
deltaLabel={`${dashboard.notifications.unread_count} unread`} value={dashboard.notifications.total_sent.toLocaleString()}
deltaPositive={dashboard.notifications.unread_count === 0} deltaLabel={`${dashboard.notifications.unread_count} unread`}
/> deltaPositive={dashboard.notifications.unread_count === 0}
<StatCard />
icon={UsersRound} <StatCard
label="Team Members" icon={UsersRound}
value={dashboard.team.total_members.toLocaleString()} label="Team Members"
deltaLabel={`${dashboard.team.by_role.length} roles`} value={dashboard.team.total_members.toLocaleString()}
deltaPositive deltaLabel={`${dashboard.team.by_role.length} roles`}
/> deltaPositive
</div> />
</div>
)}
{/* User Registrations Chart */} {/* User Registrations Chart */}
<div className="mt-5 grid gap-4"> <div className="mt-5 grid gap-4">

View File

@ -44,7 +44,7 @@ function formatDateTime(dateStr: string | null | undefined): string {
function LoadingSkeleton() { function LoadingSkeleton() {
return ( return (
<div className="mx-auto w-full max-w-5xl space-y-8 px-4 py-10 sm:px-6"> <div className="mx-auto w-full max-w-6xl space-y-8 px-4 py-10 sm:px-6">
<div className="animate-pulse space-y-8"> <div className="animate-pulse space-y-8">
{/* Hero skeleton */} {/* Hero skeleton */}
<div className="overflow-hidden rounded-2xl border border-grayScale-100"> <div className="overflow-hidden rounded-2xl border border-grayScale-100">
@ -93,15 +93,15 @@ function InfoRow({
extra?: React.ReactNode; extra?: React.ReactNode;
}) { }) {
return ( return (
<div className="group flex items-center justify-between rounded-lg px-3 py-3 transition-colors hover:bg-grayScale-100/60"> <div className="group flex flex-col gap-1 rounded-lg px-3 py-3 transition-colors hover:bg-grayScale-100/60 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3 text-sm text-grayScale-400"> <div className="flex items-center gap-3 text-sm text-grayScale-400">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-grayScale-100 text-grayScale-400 transition-colors group-hover:bg-brand-100 group-hover:text-brand-500"> <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-grayScale-100 text-grayScale-400 transition-colors group-hover:bg-brand-100 group-hover:text-brand-500">
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
</div> </div>
<span className="font-medium">{label}</span> <span className="font-medium">{label}</span>
</div> </div>
<div className="flex items-center gap-2 text-sm font-medium text-grayScale-600"> <div className="flex items-center gap-2 text-sm font-medium text-grayScale-600 sm:justify-end min-w-0">
<span className="text-right">{value || "—"}</span> <span className="truncate text-right sm:text-left">{value || "—"}</span>
{extra} {extra}
</div> </div>
</div> </div>
@ -121,13 +121,13 @@ function VerifiedIcon({ verified }: { verified: boolean }) {
} }
function ProgressRing({ percent }: { percent: number }) { function ProgressRing({ percent }: { percent: number }) {
const radius = 18; const radius = 14;
const circumference = 2 * Math.PI * radius; const circumference = 2 * Math.PI * radius;
const offset = circumference - (percent / 100) * circumference; const offset = circumference - (percent / 100) * circumference;
return ( return (
<div className="relative inline-flex items-center justify-center"> <div className="relative inline-flex items-center justify-center">
<svg className="h-11 w-11 -rotate-90" viewBox="0 0 44 44"> <svg className="h-8 w-8 -rotate-90" viewBox="0 0 44 44">
<circle <circle
cx="22" cx="22"
cy="22" cy="22"
@ -150,7 +150,7 @@ function ProgressRing({ percent }: { percent: number }) {
className="text-brand-500 transition-all duration-700" className="text-brand-500 transition-all duration-700"
/> />
</svg> </svg>
<span className="absolute text-[10px] font-bold text-brand-600">{percent}%</span> <span className="absolute text-[9px] font-bold text-brand-600">{percent}%</span>
</div> </div>
); );
} }
@ -179,7 +179,7 @@ export function ProfilePage() {
if (error || !profile) { if (error || !profile) {
return ( return (
<div className="mx-auto w-full max-w-5xl px-4 py-16 sm:px-6"> <div className="mx-auto w-full max-w-6xl px-4 py-16 sm:px-6">
<Card className="border-dashed"> <Card className="border-dashed">
<CardContent className="flex flex-col items-center gap-5 p-12"> <CardContent className="flex flex-col items-center gap-5 p-12">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-grayScale-100"> <div className="flex h-20 w-20 items-center justify-center rounded-full bg-grayScale-100">
@ -203,227 +203,310 @@ export function ProfilePage() {
const initials = `${profile.first_name?.[0] ?? ""}${profile.last_name?.[0] ?? ""}`.toUpperCase(); const initials = `${profile.first_name?.[0] ?? ""}${profile.last_name?.[0] ?? ""}`.toUpperCase();
const completionPct = profile.profile_completion_percentage ?? 0; const completionPct = profile.profile_completion_percentage ?? 0;
const sectionCardIcons: Record<string, { icon: typeof User; color: string }> = {
personal: { icon: User, color: "from-brand-500 to-brand-600" },
contact: { icon: Mail, color: "from-brand-400 to-brand-500" },
account: { icon: Shield, color: "from-brand-600 to-brand-500" },
};
return ( return (
<div className="mx-auto w-full max-w-5xl space-y-8 px-4 py-8 sm:px-6"> <div className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6">
{/* Hero Card */} {/* Page header (no tabs) */}
<Card className="overflow-hidden border-0 shadow-lg"> <div className="mb-5">
{/* Banner gradient */} <p className="text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">My Info</p>
<div className="relative h-36 bg-gradient-to-br from-brand-600 via-brand-500 to-brand-400 sm:h-40"> <h1 className="mt-1 text-2xl font-semibold tracking-tight text-grayScale-800">Profile</h1>
{/* Decorative pattern overlay */} </div>
<div className="absolute inset-0 opacity-10">
<div {/* Main profile layout card */}
className="h-full w-full" <div className="rounded-2xl border border-grayScale-100 bg-white shadow-sm">
style={{ {/* Header strip */}
backgroundImage: <div className="border-b border-grayScale-100 px-6 py-4 sm:px-8">
"radial-gradient(circle at 25% 50%, white 1px, transparent 1px), radial-gradient(circle at 75% 50%, white 1px, transparent 1px)", <div className="flex items-center justify-between">
backgroundSize: "40px 40px", <div>
}} <p className="text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">Overview</p>
/> <p className="mt-1 text-sm text-grayScale-500">
Personal, job and account details for this team member.
</p>
</div>
</div> </div>
{/* Bottom fade */}
<div className="absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-white/20 to-transparent" />
</div> </div>
<CardContent className="-mt-16 px-6 pb-8 pt-0 sm:px-10"> <div className="px-6 py-6 sm:px-8 sm:py-7">
<div className="flex flex-col items-center text-center"> <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1.8fr)_minmax(0,1.2fr)]">
{/* Avatar */} {/* Left column: About & details */}
<Avatar className="h-28 w-28 ring-4 ring-white shadow-lg"> <div className="space-y-6">
<AvatarImage src={profile.profile_picture_url || undefined} alt={fullName} /> {/* Identity */}
<AvatarFallback className="bg-gradient-to-br from-brand-100 to-brand-200 text-2xl font-bold text-brand-600"> <div className="flex flex-col gap-4 sm:flex-row">
{initials} <Avatar className="h-16 w-16 sm:h-18 sm:w-18">
</AvatarFallback> <AvatarImage src={profile.profile_picture_url || undefined} alt={fullName} />
</Avatar> <AvatarFallback className="bg-grayScale-100 text-base font-semibold text-grayScale-600">
{initials}
{/* Name */} </AvatarFallback>
<h1 className="mt-4 text-2xl font-bold tracking-tight text-grayScale-600 sm:text-3xl"> </Avatar>
{fullName} <div className="min-w-0">
</h1> <div className="flex flex-wrap items-center gap-2">
<h2 className="text-lg font-semibold tracking-tight text-grayScale-800">{fullName}</h2>
{/* Role badge */} <span className="rounded-full bg-grayScale-50 px-2.5 py-0.5 text-xs font-medium text-grayScale-500">
<Badge #{profile.id}
className={cn( </span>
"mt-2.5 px-3 py-1", </div>
profile.role === "ADMIN" <div className="mt-1 flex flex-wrap items-center gap-2">
? "bg-brand-500/10 text-brand-600 border border-brand-500/20" <Badge
: "bg-grayScale-100 text-grayScale-600 border border-grayScale-200" className={cn(
)} "px-2.5 py-0.5 text-xs font-semibold",
> profile.role === "ADMIN"
<Shield className="h-3 w-3 mr-1.5" /> ? "bg-brand-500/10 text-brand-600 border border-brand-500/20"
{profile.role} : "bg-grayScale-50 text-grayScale-600 border border-grayScale-200"
</Badge> )}
>
{/* Status pills */} <Shield className="mr-1 h-3 w-3" />
<div className="mt-6 flex flex-wrap items-center justify-center gap-2.5"> {profile.role}
{/* Active status */} </Badge>
<div <span className="inline-flex items-center gap-1.5 rounded-full bg-grayScale-50 px-2.5 py-0.5 text-xs font-medium text-grayScale-500">
className={cn( <Calendar className="h-3 w-3" />
"flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-xs font-semibold transition-colors", Joined {formatDate(profile.created_at)}
profile.status === "ACTIVE" </span>
? "border-mint-300 bg-mint-100/60 text-mint-500" </div>
: "border-destructive/20 bg-destructive/10 text-destructive" <div className="mt-3 flex flex-wrap items-center gap-2">
)} <span
> className={cn(
<span "inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold",
className={cn( profile.status === "ACTIVE"
"h-2 w-2 rounded-full", ? "bg-mint-50 text-mint-600"
profile.status === "ACTIVE" ? "bg-mint-500 animate-pulse" : "bg-destructive" : "bg-destructive/10 text-destructive"
)} )}
/> >
{profile.status} <span
className={cn(
"h-1.5 w-1.5 rounded-full",
profile.status === "ACTIVE" ? "bg-mint-500" : "bg-destructive"
)}
/>
{profile.status}
</span>
<div className="inline-flex items-center gap-2 rounded-full border border-brand-100 bg-brand-50/60 px-2.5 py-0.5 text-xs font-semibold text-brand-600">
<ProgressRing percent={completionPct} />
<span>Profile complete</span>
</div>
</div>
</div>
</div> </div>
{/* Email verification */} {/* About / Contact */}
<div <div>
className={cn( <h3 className="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">
"flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-xs font-semibold transition-colors", About
profile.email_verified </h3>
? "border-mint-300 bg-mint-100/60 text-mint-500" <div className="space-y-1.5 rounded-xl border border-grayScale-100 bg-grayScale-50/60 px-3 py-3">
: "border-grayScale-200 bg-grayScale-100/60 text-grayScale-400" <InfoRow icon={Phone} label="Phone" value={profile.phone_number} extra={<VerifiedIcon verified={profile.phone_verified} />} />
)} <InfoRow icon={Mail} label="Email" value={profile.email} extra={<VerifiedIcon verified={profile.email_verified} />} />
> <InfoRow
{profile.email_verified ? ( icon={MapPin}
<CheckCircle2 className="h-3 w-3" /> label="Location"
) : ( value={[profile.region, profile.country].filter(Boolean).join(", ") || "—"}
<XCircle className="h-3 w-3" /> />
)} </div>
Email {profile.email_verified ? "Verified" : "Unverified"}
</div> </div>
{/* Phone verification */} {/* Employee details */}
<div <div>
className={cn( <h3 className="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">
"flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-xs font-semibold transition-colors", Employee details
profile.phone_verified </h3>
? "border-mint-300 bg-mint-100/60 text-mint-500" <dl className="grid grid-cols-2 gap-x-6 gap-y-2 text-xs sm:text-sm text-grayScale-500">
: "border-grayScale-200 bg-grayScale-100/60 text-grayScale-400" <div>
)} <dt className="text-grayScale-400">Date of birth</dt>
> <dd className="mt-0.5 font-medium text-grayScale-700">
{profile.phone_verified ? ( {formatDate(profile.birth_day)}
<CheckCircle2 className="h-3 w-3" /> </dd>
) : ( </div>
<XCircle className="h-3 w-3" /> <div>
)} <dt className="text-grayScale-400">Age</dt>
Phone {profile.phone_verified ? "Verified" : "Unverified"} <dd className="mt-0.5 font-medium text-grayScale-700">
{profile.age ? `${profile.age} years` : "—"}
</dd>
</div>
<div>
<dt className="text-grayScale-400">Gender</dt>
<dd className="mt-0.5 font-medium text-grayScale-700">
{profile.gender || "Not specified"}
</dd>
</div>
<div>
<dt className="text-grayScale-400">Age group</dt>
<dd className="mt-0.5 font-medium text-grayScale-700">
{profile.age_group || "—"}
</dd>
</div>
<div>
<dt className="text-grayScale-400">Occupation</dt>
<dd className="mt-0.5 font-medium text-grayScale-700">
{profile.occupation || "—"}
</dd>
</div>
<div>
<dt className="text-grayScale-400">Preferred language</dt>
<dd className="mt-0.5 font-medium text-grayScale-700">
{profile.preferred_language || "—"}
</dd>
</div>
</dl>
</div>
</div>
{/* Middle column: Job information */}
<div className="space-y-6">
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">
Job information
</h3>
<div className="overflow-x-auto rounded-xl border border-grayScale-100">
<table className="w-full min-w-[600px] border-collapse text-sm">
<thead className="bg-grayScale-50 text-xs font-medium uppercase tracking-[0.12em] text-grayScale-400">
<tr>
<th className="px-4 py-2 text-left">Title</th>
<th className="px-4 py-2 text-left">Team</th>
<th className="px-4 py-2 text-left">Division</th>
<th className="px-4 py-2 text-left">Manager</th>
<th className="px-4 py-2 text-left">Hire date</th>
<th className="px-4 py-2 text-left">Location</th>
</tr>
</thead>
<tbody className="divide-y divide-grayScale-100 text-grayScale-700">
<tr>
<td className="px-4 py-3">{profile.occupation || profile.role}</td>
<td className="px-4 py-3">{profile.role}</td>
<td className="px-4 py-3">{profile.preferred_language || "—"}</td>
<td className="px-4 py-3"></td>
<td className="px-4 py-3">{formatDate(profile.created_at)}</td>
<td className="px-4 py-3">
{[profile.region, profile.country].filter(Boolean).join(", ") || "—"}
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
{/* Profile completion ring */} {/* Learning & goals */}
<div className="flex items-center gap-2 rounded-full border border-brand-200 bg-brand-100/30 px-3 py-1 text-xs font-semibold text-brand-600"> <div className="grid gap-4 md:grid-cols-2">
<ProgressRing percent={completionPct} /> <Card className="shadow-none border-grayScale-100">
<span>Profile Complete</span> <CardHeader className="pb-2">
<CardTitle className="text-sm font-semibold text-grayScale-700">
Learning goal
</CardTitle>
</CardHeader>
<CardContent className="pt-1">
<p className="text-sm text-grayScale-500">
{profile.learning_goal || "No learning goal specified."}
</p>
</CardContent>
</Card>
<Card className="shadow-none border-grayScale-100">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-semibold text-grayScale-700">
Language goal
</CardTitle>
</CardHeader>
<CardContent className="pt-1">
<p className="text-sm text-grayScale-500">
{profile.language_goal || "No language goal specified."}
</p>
</CardContent>
</Card>
</div>
</div>
{/* Right column: Activity & account summary */}
<div className="space-y-6">
{/* Activity */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">
Activity
</h3>
<Card className="shadow-none border-grayScale-100">
<CardContent className="space-y-4 p-4">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-brand-50 text-brand-600">
<Clock className="h-4 w-4" />
</div>
<div>
<p className="text-sm font-medium text-grayScale-700">
Last login
</p>
<p className="text-xs text-grayScale-400">
{formatDateTime(profile.last_login)}
</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-grayScale-50 text-grayScale-500">
<User className="h-4 w-4" />
</div>
<div>
<p className="text-sm font-medium text-grayScale-700">
Account created
</p>
<p className="text-xs text-grayScale-400">
{formatDateTime(profile.created_at)}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Account summary */}
<div>
<h3 className="mb-3 text-xs font-semibold uppercase tracking-[0.14em] text-grayScale-400">
Account
</h3>
<Card className="shadow-none border-grayScale-100">
<CardContent className="space-y-3 p-4">
<div className="flex items-center justify-between text-sm">
<span className="text-grayScale-400">Role</span>
<span className="font-medium text-grayScale-700">{profile.role}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-grayScale-400">Status</span>
<span
className={cn(
"inline-flex items-center gap-1.5 text-xs font-semibold",
profile.status === "ACTIVE"
? "text-mint-600"
: "text-destructive"
)}
>
<span
className={cn(
"h-1.5 w-1.5 rounded-full",
profile.status === "ACTIVE" ? "bg-mint-500" : "bg-destructive"
)}
/>
{profile.status}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-grayScale-400">Email</span>
<span className="flex items-center gap-1 text-grayScale-700">
<span className="truncate max-w-[140px] text-right text-xs sm:text-sm">
{profile.email}
</span>
<VerifiedIcon verified={profile.email_verified} />
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-grayScale-400">Phone</span>
<span className="flex items-center gap-1 text-grayScale-700">
<span className="truncate max-w-[120px] text-right text-xs sm:text-sm">
{profile.phone_number || "—"}
</span>
<VerifiedIcon verified={profile.phone_verified} />
</span>
</div>
</CardContent>
</Card>
</div> </div>
</div> </div>
</div> </div>
</CardContent> </div>
</Card>
{/* Info Cards */}
<div className="grid gap-6 md:grid-cols-3">
{/* Personal Information */}
<Card className="group overflow-hidden border border-grayScale-100 transition-all duration-200 hover:shadow-lg">
<div className="h-1 w-full bg-gradient-to-r from-brand-500 to-brand-600" />
<CardHeader className="pb-3">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-brand-500 to-brand-600 text-white shadow-sm">
<User className="h-4 w-4" />
</div>
<CardTitle className="text-base font-semibold tracking-tight text-grayScale-600">
Personal Information
</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-0.5 px-3 pb-4">
<InfoRow icon={User} label="Full Name" value={fullName} />
<InfoRow icon={User} label="Gender" value={profile.gender || "Not specified"} />
<InfoRow icon={Calendar} label="Birthday" value={formatDate(profile.birth_day)} />
<InfoRow icon={User} label="Age Group" value={profile.age_group || "—"} />
<InfoRow icon={Briefcase} label="Occupation" value={profile.occupation || "—"} />
</CardContent>
</Card>
{/* Contact & Location */}
<Card className="group overflow-hidden border border-grayScale-100 transition-all duration-200 hover:shadow-lg">
<div className="h-1 w-full bg-gradient-to-r from-brand-400 to-brand-500" />
<CardHeader className="pb-3">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-brand-400 to-brand-500 text-white shadow-sm">
<Mail className="h-4 w-4" />
</div>
<CardTitle className="text-base font-semibold tracking-tight text-grayScale-600">
Contact & Location
</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-0.5 px-3 pb-4">
<InfoRow
icon={Mail}
label="Email"
value={profile.email}
extra={<VerifiedIcon verified={profile.email_verified} />}
/>
<InfoRow
icon={Phone}
label="Phone"
value={profile.phone_number}
extra={<VerifiedIcon verified={profile.phone_verified} />}
/>
<InfoRow icon={Globe} label="Country" value={profile.country || "—"} />
<InfoRow icon={MapPin} label="Region" value={profile.region || "—"} />
</CardContent>
</Card>
{/* Account Details */}
<Card className="group overflow-hidden border border-grayScale-100 transition-all duration-200 hover:shadow-lg">
<div className="h-1 w-full bg-gradient-to-r from-brand-600 to-brand-500" />
<CardHeader className="pb-3">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-brand-600 to-brand-500 text-white shadow-sm">
<Shield className="h-4 w-4" />
</div>
<CardTitle className="text-base font-semibold tracking-tight text-grayScale-600">
Account Details
</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-0.5 px-3 pb-4">
<InfoRow icon={Shield} label="Role" value={profile.role} />
<InfoRow
icon={Languages}
label="Language"
value={profile.preferred_language || "—"}
/>
<InfoRow
icon={Clock}
label="Last Login"
value={formatDateTime(profile.last_login)}
/>
<InfoRow
icon={Calendar}
label="Member Since"
value={formatDate(profile.created_at)}
/>
<InfoRow
icon={CheckCircle2}
label="Status"
value={profile.status}
extra={
<span
className={cn(
"h-2.5 w-2.5 rounded-full ring-2",
profile.status === "ACTIVE"
? "bg-mint-500 ring-mint-100"
: "bg-destructive ring-destructive/20"
)}
/>
}
/>
</CardContent>
</Card>
</div> </div>
</div> </div>
); );

View File

@ -282,6 +282,7 @@ export function AnalyticsPage() {
const [dashboard, setDashboard] = useState<DashboardData | null>(null) const [dashboard, setDashboard] = useState<DashboardData | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState(false) const [error, setError] = useState(false)
const [activeSummaryTab, setActiveSummaryTab] = useState<"key" | "content" | "operations">("key")
const fetchData = async () => { const fetchData = async () => {
setLoading(true) setLoading(true)
@ -384,107 +385,166 @@ export function AnalyticsPage() {
</div> </div>
</div> </div>
{/* Summary Tabs */}
<div className="mb-4 border-b border-grayScale-200">
<div className="-mb-px flex gap-6">
<button
onClick={() => setActiveSummaryTab("key")}
className={cn(
"relative px-1 pb-3.5 pt-1 text-sm font-semibold transition-all",
activeSummaryTab === "key" ? "text-brand-600" : "text-grayScale-400 hover:text-grayScale-700",
)}
>
Key Metrics
{activeSummaryTab === "key" && (
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-brand-500" />
)}
</button>
<button
onClick={() => setActiveSummaryTab("content")}
className={cn(
"relative px-1 pb-3.5 pt-1 text-sm font-semibold transition-all",
activeSummaryTab === "content" ? "text-brand-600" : "text-grayScale-400 hover:text-grayScale-700",
)}
>
Content &amp; Platform
{activeSummaryTab === "content" && (
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-brand-500" />
)}
</button>
<button
onClick={() => setActiveSummaryTab("operations")}
className={cn(
"relative px-1 pb-3.5 pt-1 text-sm font-semibold transition-all",
activeSummaryTab === "operations" ? "text-brand-600" : "text-grayScale-400 hover:text-grayScale-700",
)}
>
Operations
{activeSummaryTab === "operations" && (
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-brand-500" />
)}
</button>
</div>
</div>
<div className="space-y-4"> <div className="space-y-4">
{/* ─── Key Metrics ─── */} {activeSummaryTab === "key" && (
<Section title="Key Metrics" icon={TrendingUp} defaultOpen> <>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> {/* ─── Key Metrics ─── */}
<KpiCard <Section title="Key Metrics" icon={TrendingUp} defaultOpen>
icon={Users} <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
label="Total Users" <KpiCard
value={formatNumber(users.total_users)} icon={Users}
sub={`+${users.new_today} today · +${users.new_week} this week · +${users.new_month} this month`} label="Total Users"
trend={users.new_month > 0 ? "up" : "neutral"} value={formatNumber(users.total_users)}
/> sub={`+${users.new_today} today · +${users.new_week} this week · +${users.new_month} this month`}
<KpiCard trend={users.new_month > 0 ? "up" : "neutral"}
icon={BadgeCheck} />
label="Active Subscriptions" <KpiCard
value={formatNumber(subscriptions.active_subscriptions)} icon={BadgeCheck}
sub={`${subscriptions.total_subscriptions} total · +${subscriptions.new_month} this month`} label="Active Subscriptions"
trend={subscriptions.new_month > 0 ? "up" : "neutral"} value={formatNumber(subscriptions.active_subscriptions)}
/> sub={`${subscriptions.total_subscriptions} total · +${subscriptions.new_month} this month`}
<KpiCard trend={subscriptions.new_month > 0 ? "up" : "neutral"}
icon={DollarSign} />
label="Total Revenue" <KpiCard
value={`ETB ${formatNumber(payments.total_revenue)}`} icon={DollarSign}
sub={`${payments.successful_payments}/${payments.total_payments} successful · Avg ETB ${payments.avg_transaction_value.toLocaleString()}`} label="Total Revenue"
trend={payments.total_revenue > 0 ? "up" : "neutral"} value={`ETB ${formatNumber(payments.total_revenue)}`}
/> sub={`${payments.successful_payments}/${payments.total_payments} successful · Avg ETB ${payments.avg_transaction_value.toLocaleString()}`}
<KpiCard trend={payments.total_revenue > 0 ? "up" : "neutral"}
icon={TicketCheck} />
label="Issue Resolution" <KpiCard
value={`${(issues.resolution_rate * 100).toFixed(1)}%`} icon={TicketCheck}
sub={`${issues.resolved_issues} resolved of ${issues.total_issues} total`} label="Issue Resolution"
trend={issues.resolution_rate >= 0.5 ? "up" : "down"} value={`${(issues.resolution_rate * 100).toFixed(1)}%`}
/> sub={`${issues.resolved_issues} resolved of ${issues.total_issues} total`}
</div> trend={issues.resolution_rate >= 0.5 ? "up" : "down"}
</Section> />
</div>
</Section>
</>
)}
{/* ─── Content & Platform ─── */} {activeSummaryTab === "content" && (
<Section title="Content & Platform" icon={BookOpen} count={courses.total_courses + content.total_questions} defaultOpen> <>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> {/* ─── Content & Platform ─── */}
<KpiCard <Section
icon={FolderOpen} title="Content & Platform"
label="Categories"
value={courses.total_categories.toLocaleString()}
sub={`${courses.total_courses} courses`}
trend="neutral"
/>
<KpiCard
icon={BookOpen} icon={BookOpen}
label="Sub-Courses" count={courses.total_courses + content.total_questions}
value={courses.total_sub_courses.toLocaleString()} defaultOpen
sub={`across ${courses.total_courses} courses`} >
trend="neutral" <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
/> <KpiCard
<KpiCard icon={FolderOpen}
icon={Video} label="Categories"
label="Videos" value={courses.total_categories.toLocaleString()}
value={courses.total_videos.toLocaleString()} sub={`${courses.total_courses} courses`}
trend="neutral" trend="neutral"
/> />
<KpiCard <KpiCard
icon={HelpCircle} icon={BookOpen}
label="Questions" label="Sub-Courses"
value={content.total_questions.toLocaleString()} value={courses.total_sub_courses.toLocaleString()}
sub={`${content.total_question_sets} question sets`} sub={`across ${courses.total_courses} courses`}
trend="neutral" trend="neutral"
/> />
</div> <KpiCard
</Section> icon={Video}
label="Videos"
value={courses.total_videos.toLocaleString()}
trend="neutral"
/>
<KpiCard
icon={HelpCircle}
label="Questions"
value={content.total_questions.toLocaleString()}
sub={`${content.total_question_sets} question sets`}
trend="neutral"
/>
</div>
</Section>
</>
)}
{/* ─── Operations ─── */} {activeSummaryTab === "operations" && (
<Section title="Operations" icon={Bell} defaultOpen> <>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> {/* ─── Operations ─── */}
<KpiCard <Section title="Operations" icon={Bell} defaultOpen>
icon={Bell} <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
label="Notifications Sent" <KpiCard
value={formatNumber(notifications.total_sent)} icon={Bell}
sub={`${notifications.read_count} read · ${notifications.unread_count} unread`} label="Notifications Sent"
trend={notifications.unread_count === 0 ? "up" : "neutral"} value={formatNumber(notifications.total_sent)}
/> sub={`${notifications.read_count} read · ${notifications.unread_count} unread`}
<KpiCard trend={notifications.unread_count === 0 ? "up" : "neutral"}
icon={UsersRound} />
label="Team Members" <KpiCard
value={team.total_members.toLocaleString()} icon={UsersRound}
sub={`${team.by_role.length} roles`} label="Team Members"
trend="neutral" value={team.total_members.toLocaleString()}
/> sub={`${team.by_role.length} roles`}
<KpiCard trend="neutral"
icon={CreditCard} />
label="Payments" <KpiCard
value={payments.total_payments.toLocaleString()} icon={CreditCard}
sub={`${payments.successful_payments} successful`} label="Payments"
trend={payments.successful_payments > 0 ? "up" : "neutral"} value={payments.total_payments.toLocaleString()}
/> sub={`${payments.successful_payments} successful`}
<KpiCard trend={payments.successful_payments > 0 ? "up" : "neutral"}
icon={Layers} />
label="Question Sets" <KpiCard
value={content.total_question_sets.toLocaleString()} icon={Layers}
sub={content.question_sets_by_type.map((q) => `${q.count} ${q.label.toLowerCase()}`).join(" · ")} label="Question Sets"
trend="neutral" value={content.total_question_sets.toLocaleString()}
/> sub={content.question_sets_by_type.map((q) => `${q.count} ${q.label.toLowerCase()}`).join(" · ")}
</div> trend="neutral"
</Section> />
</div>
</Section>
</>
)}
{/* ─── User Analytics ─── */} {/* ─── User Analytics ─── */}
<Section title="User Analytics" icon={Users} count={users.total_users} defaultOpen> <Section title="User Analytics" icon={Users} count={users.total_users} defaultOpen>

View File

@ -512,7 +512,7 @@ export function IssuesPage() {
<TableRow key={issue.id} className="group"> <TableRow key={issue.id} className="group">
<TableCell> <TableCell>
<div className="flex items-start gap-3 max-w-[300px]"> <div className="flex items-start gap-3 max-w-[300px]">
<div className="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-grayScale-50 text-grayScale-400 group-hover:bg-brand-50 group-hover:text-brand-500 transition-colors"> <div className="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-grayScale-50 text-grayScale-400 group-hover:bg-brand-500 group-hover:text-white transition-colors">
<TypeIcon className="h-4 w-4" /> <TypeIcon className="h-4 w-4" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">

View File

@ -404,7 +404,7 @@ export function UserLogPage() {
<TableRow key={log.id} className="group"> <TableRow key={log.id} className="group">
<TableCell> <TableCell>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-grayScale-50 text-grayScale-400 group-hover:bg-brand-50 group-hover:text-brand-500 transition-colors"> <div className="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-grayScale-50 text-grayScale-400 group-hover:bg-brand-500 group-hover:text-white transition-colors">
<ActionIcon className="h-4 w-4" /> <ActionIcon className="h-4 w-4" />
</div> </div>
<span <span

View File

@ -29,32 +29,32 @@ export function UserManagementDashboard() {
<Users className="h-6 w-6" /> <Users className="h-6 w-6" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-medium text-grayScale-400">Total Users</p> <p className="text-sm font-medium text-white/80">Total Users</p>
<p className="text-2xl font-bold text-grayScale-600">1,248</p> <p className="text-2xl font-bold text-white">1,248</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border-none bg-mint-50 shadow-sm"> <Card className="border-none bg-brand-50 shadow-sm">
<CardContent className="flex items-center gap-4 p-5"> <CardContent className="flex items-center gap-4 p-5">
<div className="grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-mint-100 text-mint-600"> <div className="grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-brand-100 text-brand-600">
<UserCheck className="h-6 w-6" /> <UserCheck className="h-6 w-6" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-medium text-grayScale-400">Active Users</p> <p className="text-sm font-medium text-white/80">Active Users</p>
<p className="text-2xl font-bold text-grayScale-600">1,180</p> <p className="text-2xl font-bold text-white">1,180</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border-none bg-gold-50 shadow-sm sm:col-span-2 lg:col-span-1"> <Card className="border-none bg-brand-50 shadow-sm sm:col-span-2 lg:col-span-1">
<CardContent className="flex items-center gap-4 p-5"> <CardContent className="flex items-center gap-4 p-5">
<div className="grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-gold-100 text-gold-600"> <div className="grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-brand-100 text-brand-600">
<TrendingUp className="h-6 w-6" /> <TrendingUp className="h-6 w-6" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-medium text-grayScale-400">New This Month</p> <p className="text-sm font-medium text-white/80">New This Month</p>
<p className="text-2xl font-bold text-grayScale-600">64</p> <p className="text-2xl font-bold text-white">64</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>