Yimaru-Admin/src/pages/team/TeamMemberDetailPage.tsx

334 lines
11 KiB
TypeScript

import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import {
ArrowLeft,
Briefcase,
Building2,
Calendar,
CheckCircle2,
Clock,
Globe,
KeyRound,
Mail,
Phone,
Shield,
User,
XCircle,
} from "lucide-react";
import { Badge } from "../../components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card";
import { Separator } from "../../components/ui/separator";
import { Avatar, AvatarFallback, AvatarImage } from "../../components/ui/avatar";
import { cn } from "../../lib/utils";
import { getTeamMemberById } from "../../api/team.api";
import type { TeamMember } from "../../types/team.types";
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return "—";
return new Date(dateStr).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
}
function formatDateTime(dateStr: string | null | undefined): string {
if (!dateStr) return "—";
return new Date(dateStr).toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function formatRoleLabel(role: string): string {
return role
.split("_")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
}
function formatEmploymentType(type: string): string {
return type
.split("_")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
}
function getRoleBadgeClasses(role: string): string {
switch (role) {
case "super_admin":
return "bg-brand-500/15 text-brand-600 border border-brand-500/25";
case "admin":
return "bg-brand-100 text-brand-600 border border-brand-200";
case "content_manager":
return "bg-mint-100 text-mint-500 border border-mint-300/40";
case "instructor":
return "bg-gold-100 text-gold-600 border border-gold-300/40";
case "support_agent":
return "bg-orange-100 text-orange-600 border border-orange-200";
case "finance":
return "bg-sky-100 text-sky-600 border border-sky-200";
case "hr":
return "bg-pink-100 text-pink-600 border border-pink-200";
case "analyst":
return "bg-violet-100 text-violet-600 border border-violet-200";
default:
return "bg-grayScale-100 text-grayScale-600 border border-grayScale-200";
}
}
function LoadingSkeleton() {
return (
<div className="space-y-6">
<div className="h-5 w-32 animate-pulse rounded bg-grayScale-100" />
<div className="animate-pulse">
<div className="rounded-2xl bg-grayScale-100 h-64" />
<div className="mt-6 grid gap-6 lg:grid-cols-3">
<div className="rounded-2xl bg-grayScale-100 h-52" />
<div className="rounded-2xl bg-grayScale-100 h-52" />
<div className="rounded-2xl bg-grayScale-100 h-52" />
</div>
</div>
</div>
);
}
export function TeamMemberDetailPage() {
const { id } = useParams();
const [member, setMember] = useState<TeamMember | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!id) return;
const fetchMember = async () => {
try {
const res = await getTeamMemberById(Number(id));
setMember(res.data.data);
} catch (err) {
console.error("Failed to fetch team member", err);
setError("Team member not found.");
} finally {
setLoading(false);
}
};
fetchMember();
}, [id]);
if (loading) return <LoadingSkeleton />;
if (error || !member) {
return (
<div className="space-y-6">
<Link
to="/team"
className="inline-flex items-center gap-2 text-sm font-medium text-grayScale-500 transition-colors hover:text-brand-600"
>
<ArrowLeft className="h-4 w-4" />
Back to Team
</Link>
<Card>
<CardContent className="flex flex-col items-center gap-4 p-10">
<div className="h-16 w-16 rounded-full bg-grayScale-100 flex items-center justify-center">
<User className="h-8 w-8 text-grayScale-300" />
</div>
<div className="text-lg font-semibold text-grayScale-600">
{error || "Member not found"}
</div>
</CardContent>
</Card>
</div>
);
}
const fullName = `${member.first_name} ${member.last_name}`;
const initials = `${member.first_name?.[0] ?? ""}${member.last_name?.[0] ?? ""}`.toUpperCase();
return (
<div className="space-y-6">
<Link
to="/team"
className="inline-flex items-center gap-2 text-sm font-medium text-grayScale-500 transition-colors hover:text-brand-600"
>
<ArrowLeft className="h-4 w-4" />
Back to Team
</Link>
<Card className="overflow-hidden">
<div className="h-28 bg-gradient-to-r from-brand-600 via-brand-400 to-mint-500" />
<CardContent className="-mt-12 px-4 sm:px-8 pb-4 sm:pb-8 pt-0">
<div className="flex flex-col items-start gap-5 sm:flex-row sm:items-end">
<Avatar className="h-24 w-24 ring-4 ring-white shadow-soft">
<AvatarImage src={undefined} alt={fullName} />
<AvatarFallback className="bg-brand-100 text-brand-600 text-2xl font-bold">
{initials}
</AvatarFallback>
</Avatar>
<div className="flex-1 pb-1">
<h1 className="text-2xl font-bold text-grayScale-600">{fullName}</h1>
<p className="mt-0.5 text-sm text-grayScale-400">{member.job_title} · {member.department}</p>
<div className="mt-2 flex flex-wrap items-center gap-2">
<span
className={cn(
"inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-semibold",
getRoleBadgeClasses(member.team_role)
)}
>
<Shield className="h-3 w-3" />
{formatRoleLabel(member.team_role)}
</span>
<span
className={cn(
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium",
member.status === "active"
? "bg-mint-100 text-mint-500"
: "bg-destructive/10 text-destructive"
)}
>
<span
className={cn(
"h-1.5 w-1.5 rounded-full",
member.status === "active" ? "bg-mint-500" : "bg-destructive"
)}
/>
{member.status === "active" ? "Active" : "Inactive"}
</span>
<span className="inline-flex items-center gap-1 rounded-full bg-grayScale-100 px-2.5 py-0.5 text-xs font-medium text-grayScale-500">
{formatEmploymentType(member.employment_type)}
</span>
</div>
</div>
</div>
{member.bio && (
<div className="mt-5 rounded-xl bg-grayScale-100 p-4 text-sm leading-relaxed text-grayScale-600">
{member.bio}
</div>
)}
</CardContent>
</Card>
<div className="grid gap-6 lg:grid-cols-3">
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-brand-100/50">
<User className="h-4 w-4 text-brand-600" />
</div>
<CardTitle>Work Details</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-0">
<DetailRow icon={Briefcase} label="Job Title" value={member.job_title} />
<Separator />
<DetailRow icon={Building2} label="Department" value={member.department} />
<Separator />
<DetailRow icon={Globe} label="Employment" value={formatEmploymentType(member.employment_type)} />
<Separator />
<DetailRow icon={Calendar} label="Hire Date" value={formatDate(member.hire_date)} />
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-mint-100/60">
<Mail className="h-4 w-4 text-mint-500" />
</div>
<CardTitle>Contact</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-0">
<DetailRow
icon={Mail}
label="Email"
value={member.email}
extra={
member.email_verified ? (
<CheckCircle2 className="h-4 w-4 text-mint-500" />
) : (
<XCircle className="h-4 w-4 text-grayScale-300" />
)
}
/>
<Separator />
<DetailRow icon={Phone} label="Phone" value={member.phone_number} />
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gold-100/60">
<Shield className="h-4 w-4 text-gold-600" />
</div>
<CardTitle>Account</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-0">
<DetailRow icon={Shield} label="Role" value={formatRoleLabel(member.team_role)} />
<Separator />
<DetailRow icon={Clock} label="Last Login" value={formatDateTime(member.last_login)} />
<Separator />
<DetailRow icon={Calendar} label="Member Since" value={formatDate(member.created_at)} />
</CardContent>
</Card>
</div>
{member.permissions.length > 0 && (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-brand-100/50">
<KeyRound className="h-4 w-4 text-brand-600" />
</div>
<CardTitle>Permissions</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{member.permissions.map((perm) => (
<Badge
key={perm}
className="bg-grayScale-100 text-grayScale-600 border border-grayScale-200 font-mono text-xs"
>
{perm}
</Badge>
))}
</div>
</CardContent>
</Card>
)}
</div>
);
}
function DetailRow({
icon: Icon,
label,
value,
extra,
}: {
icon: typeof User;
label: string;
value: string;
extra?: React.ReactNode;
}) {
return (
<div className="flex items-center justify-between py-3">
<div className="flex items-center gap-3 text-sm text-grayScale-400">
<Icon className="h-4 w-4" />
<span>{label}</span>
</div>
<div className="flex items-center gap-2 text-sm font-medium text-grayScale-600">
<span>{value || "—"}</span>
{extra}
</div>
</div>
);
}