الملفات
back_end_oudelaa/oudelaa_dashboard/app/(dashboard)/users/page.tsx
boutmoun123 8863f61d00
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
Add Oudelaa dashboard API integration
2026-05-25 20:36:52 +03:00

876 أسطر
32 KiB
TypeScript
خام اللوم التاريخ

هذا الملف يحتوي على أحرف Unicode غامضة

هذا الملف يحتوي على أحرف Unicode قد تُخلط مع أحرف أخرى. إذا كنت تعتقد أن هذا مقصود، يمكنك تجاهل هذا التحذير بأمان. استخدم زر الهروب للكشف عنها.

"use client";
import Image from "next/image";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Search, ShieldPlus, UserMinus, UserPlus, UserX } from "lucide-react";
import { NoPermissionState } from "@/components/auth/no-permission-state";
import { useSuperAdminSession } from "@/components/auth/session-context";
import { PageHeader } from "@/components/dashboard/page-header";
import { PaginationControls } from "@/components/dashboard/pagination-controls";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Drawer } from "@/components/ui/drawer";
import { EmptyState } from "@/components/ui/empty-state";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/components/ui/toast";
import {
createAdminUser,
deleteAdminUser,
deletePlatformAdmin,
getAdminUserById,
getProfileOverviewForSuperAdmin,
searchAdminUsers,
searchPlatformAdmins,
setAdminUserRole,
updateAdminUser,
updatePlatformAdmin,
} from "@/lib/api/admin-users";
import { getItems, getPagination } from "@/lib/api/core";
import { updateSuperAdminUserStatus } from "@/lib/api/superadmin";
import { formatDateTime } from "@/lib/format";
import { resolveMediaUrl } from "@/lib/media-url";
import { SUPERADMIN_PERMISSIONS, hasPermission } from "@/lib/permissions";
import { cn } from "@/lib/utils";
import type {
AdminUpdatePayload,
AdminCreatePayload,
ApiRole,
ApiUser,
PaginatedResponse,
ProfileOverviewResponse,
} from "@/types/api";
type ScopeFilter = "all-users" | "admins";
const emptyCreateState: AdminCreatePayload = {
name: "",
username: "",
email: "",
password: "",
confirmPassword: "",
};
function buildEditPayload(user: ApiUser): Partial<ApiUser> {
return {
name: user.name ?? "",
username: user.username ?? "",
email: user.email ?? "",
role: user.role ?? "user",
stageName: user.stageName ?? "",
bio: user.bio ?? "",
location: user.location ?? "",
isPrivate: user.isPrivate ?? false,
isVerified: user.isVerified ?? false,
};
}
function normalizeComparableString(value: string | null | undefined) {
return (value ?? "").trim();
}
function buildUserUpdatePayload(
selectedUser: ApiUser,
editPayload: Partial<ApiUser>,
): AdminUpdatePayload {
const payload: AdminUpdatePayload = {};
const nextName = normalizeComparableString(
typeof editPayload.name === "string" ? editPayload.name : selectedUser.name,
);
const nextUsername = normalizeComparableString(
typeof editPayload.username === "string" ? editPayload.username : selectedUser.username,
);
const nextEmail = normalizeComparableString(
typeof editPayload.email === "string" ? editPayload.email : selectedUser.email,
);
const currentName = normalizeComparableString(selectedUser.name);
const currentUsername = normalizeComparableString(selectedUser.username);
const currentEmail = normalizeComparableString(selectedUser.email);
if (nextName !== currentName) {
if (!nextName) {
throw new Error("Name cannot be empty.");
}
payload.name = nextName;
}
if (nextUsername !== currentUsername) {
if (!nextUsername) {
throw new Error("Username cannot be empty.");
}
payload.username = nextUsername;
}
if (nextEmail !== currentEmail) {
if (!nextEmail) {
throw new Error("Email cannot be empty.");
}
payload.email = nextEmail;
}
const nextStageName = normalizeComparableString(
typeof editPayload.stageName === "string" ? editPayload.stageName : selectedUser.stageName,
);
const currentStageName = normalizeComparableString(selectedUser.stageName);
if (nextStageName !== currentStageName) {
payload.stageName = nextStageName;
}
const nextBio = normalizeComparableString(
typeof editPayload.bio === "string" ? editPayload.bio : selectedUser.bio,
);
const currentBio = normalizeComparableString(selectedUser.bio);
if (nextBio !== currentBio) {
payload.bio = nextBio;
}
const nextLocation = normalizeComparableString(
typeof editPayload.location === "string" ? editPayload.location : selectedUser.location,
);
const currentLocation = normalizeComparableString(selectedUser.location);
if (nextLocation !== currentLocation) {
payload.location = nextLocation;
}
const nextIsPrivate = Boolean(editPayload.isPrivate ?? selectedUser.isPrivate ?? false);
const currentIsPrivate = Boolean(selectedUser.isPrivate ?? false);
if (nextIsPrivate !== currentIsPrivate) {
payload.isPrivate = nextIsPrivate;
}
const nextIsVerified = Boolean(editPayload.isVerified ?? selectedUser.isVerified ?? false);
const currentIsVerified = Boolean(selectedUser.isVerified ?? false);
if (nextIsVerified !== currentIsVerified) {
payload.isVerified = nextIsVerified;
}
return payload;
}
function FieldRow({
label,
value,
valueDir,
valueClassName,
}: {
label: string;
value: string | number | boolean | null | undefined;
valueDir?: "auto" | "rtl" | "ltr";
valueClassName?: string;
}) {
const normalized =
value === null || value === undefined || value === ""
? "-"
: typeof value === "boolean"
? value
? "نعم"
: "لا"
: String(value);
const isTechnicalValue =
typeof normalized === "string" &&
(normalized.includes("@") || normalized.includes("_") || normalized.includes("-") || normalized.includes("."));
const resolvedDir = valueDir ?? (isTechnicalValue ? "ltr" : "auto");
return (
<div className="min-w-0 rounded-lg border border-border/60 bg-background/30 p-3">
<div className="text-xs text-muted-foreground">{label}</div>
<div
dir={resolvedDir}
className={cn(
"mt-2 min-w-0 break-words text-sm leading-relaxed text-foreground [overflow-wrap:anywhere]",
isTechnicalValue && "text-left font-mono text-xs sm:text-sm",
valueClassName,
)}
>
{normalized}
</div>
</div>
);
}
export default function UsersPage() {
const { permissions } = useSuperAdminSession();
const [scope, setScope] = useState<ScopeFilter>("all-users");
const [search, setSearch] = useState("");
const [verifiedFilter, setVerifiedFilter] = useState("all");
const [page, setPage] = useState(1);
const [response, setResponse] = useState<PaginatedResponse<ApiUser> | null>(null);
const [loading, setLoading] = useState(true);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [selectedUser, setSelectedUser] = useState<ApiUser | null>(null);
const [avatarLoadFailed, setAvatarLoadFailed] = useState(false);
const [overview, setOverview] = useState<ProfileOverviewResponse | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
const [editPayload, setEditPayload] = useState<Partial<ApiUser>>({});
const [createOpen, setCreateOpen] = useState(false);
const [createPayload, setCreatePayload] = useState<AdminCreatePayload>(emptyCreateState);
const { toast } = useToast();
const filtersRef = useRef({ search, verifiedFilter });
const canReadUsers = hasPermission(permissions, SUPERADMIN_PERMISSIONS.USERS_READ);
const canManageUsers = hasPermission(permissions, SUPERADMIN_PERMISSIONS.USERS_MANAGE);
filtersRef.current = { search, verifiedFilter };
useEffect(() => {
setAvatarLoadFailed(false);
}, [selectedUser?.avatar]);
const loadUsers = useCallback(async () => {
if (!canReadUsers) {
setLoading(false);
return;
}
setLoading(true);
try {
const { search: currentSearch, verifiedFilter: currentVerifiedFilter } = filtersRef.current;
const isVerified =
currentVerifiedFilter === "all"
? undefined
: currentVerifiedFilter === "verified"
? true
: false;
const nextResponse =
scope === "admins"
? await searchPlatformAdmins({
page,
limit: 12,
q: currentSearch || undefined,
isVerified,
sortBy: "createdAt",
sortOrder: "desc",
})
: await searchAdminUsers({
page,
limit: 12,
q: currentSearch || undefined,
isVerified,
sortBy: "createdAt",
sortOrder: "desc",
});
setResponse(nextResponse);
} catch (error) {
const message = String(error);
toast({ title: "تعذر تحميل المستخدمين", description: message, variant: "danger" });
} finally {
setLoading(false);
}
}, [canReadUsers, page, scope, toast]);
useEffect(() => {
void loadUsers();
}, [loadUsers]);
useEffect(() => {
if (!selectedUserId || !canReadUsers) return;
let active = true;
const loadDetails = async () => {
setDetailLoading(true);
try {
const [user, userOverview] = await Promise.all([
getAdminUserById(selectedUserId),
getProfileOverviewForSuperAdmin(selectedUserId),
]);
if (!active) return;
setSelectedUser(user);
setOverview(userOverview);
setEditPayload(buildEditPayload(user));
} catch (error) {
if (!active) return;
toast({ title: "تعذر تحميل التفاصيل", description: String(error), variant: "danger" });
} finally {
if (active) setDetailLoading(false);
}
};
void loadDetails();
return () => {
active = false;
};
}, [canReadUsers, selectedUserId, toast]);
const users = getItems(response);
const stats = useMemo(() => {
const disabled = users.filter((item) => item.isDisabled).length;
const admins = users.filter((item) => item.role === "admin").length;
return { total: response?.pagination?.total ?? users.length, disabled, admins };
}, [response, users]);
const syncUpdatedUser = (userId: string, patch: Partial<ApiUser>) => {
setResponse((prev) => {
if (!prev) return prev;
const source = getItems(prev);
const updatedItems = source.map((item) => (item._id === userId ? { ...item, ...patch } : item));
return {
...prev,
items: updatedItems,
data: updatedItems,
};
});
setSelectedUser((prev) => (prev && prev._id === userId ? { ...prev, ...patch } : prev));
};
const handleDisable = async (userId: string) => {
const reason =
typeof window === "undefined"
? "Disabled by SuperAdmin dashboard"
: window.prompt("Reason for disabling this user", "Disabled by SuperAdmin dashboard");
if (reason === null) {
return;
}
try {
const updated = await updateSuperAdminUserStatus(
userId,
true,
reason.trim() || "Disabled by SuperAdmin dashboard",
);
syncUpdatedUser(userId, { isDisabled: true, disabledReason: updated.disabledReason });
toast({ title: "تم التعطيل", description: userId, variant: "warning" });
} catch (error) {
toast({ title: "فشل التعطيل", description: String(error), variant: "danger" });
}
};
const handleEnable = async (userId: string) => {
try {
const updated = await updateSuperAdminUserStatus(userId, false);
syncUpdatedUser(userId, { isDisabled: false, disabledReason: updated.disabledReason ?? "" });
toast({ title: "تم التفعيل", description: userId, variant: "success" });
} catch (error) {
toast({ title: "فشل التفعيل", description: String(error), variant: "danger" });
}
};
const handleDelete = async (userId: string) => {
try {
if (scope === "admins") {
await deletePlatformAdmin(userId);
} else {
await deleteAdminUser(userId);
}
if (selectedUserId === userId) {
setSelectedUserId(null);
setSelectedUser(null);
setOverview(null);
}
await loadUsers();
toast({ title: "تم الحذف", description: userId, variant: "warning" });
} catch (error) {
toast({ title: "فشل الحذف", description: String(error), variant: "danger" });
}
};
const handleSave = async () => {
if (!selectedUserId || !selectedUser || !canManageUsers) return;
try {
const updater = scope === "admins" ? updatePlatformAdmin : updateAdminUser;
const updatePayload = buildUserUpdatePayload(selectedUser, editPayload);
const currentRole = selectedUser.role ?? "user";
const nextRole = (editPayload.role as ApiRole | undefined) ?? currentRole;
const roleChanged = nextRole !== currentRole;
const hasProfileChanges = Object.keys(updatePayload).length > 0;
if (!hasProfileChanges && !roleChanged) {
toast({
title: "No changes to save",
description: "Update one or more fields before saving.",
variant: "default",
});
return;
}
if (hasProfileChanges) {
await updater(selectedUserId, updatePayload);
}
if (roleChanged) {
await setAdminUserRole(selectedUserId, nextRole);
}
const [freshUser, freshOverview] = await Promise.all([
getAdminUserById(selectedUserId),
getProfileOverviewForSuperAdmin(selectedUserId),
]);
syncUpdatedUser(selectedUserId, freshUser);
setSelectedUser(freshUser);
setOverview(freshOverview);
setEditPayload(buildEditPayload(freshUser));
await loadUsers();
toast({
title: "تم حفظ التعديلات",
description: "تم تحديث بيانات المستخدم.",
variant: "success",
});
} catch (error) {
toast({ title: "فشل التحديث", description: String(error), variant: "danger" });
}
};
const handleCreate = async () => {
if (!canManageUsers) return;
try {
await createAdminUser(createPayload);
setCreateOpen(false);
setCreatePayload(emptyCreateState);
setScope("admins");
setPage(1);
await loadUsers();
toast({ title: "تم إنشاء الأدمن", description: createPayload.email, variant: "success" });
} catch (error) {
toast({ title: "فشل إنشاء الأدمن", description: String(error), variant: "danger" });
}
};
if (!canReadUsers) {
return (
<div className="space-y-5 pb-8">
<PageHeader
title="User management"
subtitle="Search, review, and manage platform users and admins from one place."
/>
<NoPermissionState description="This page needs the users.read permission." />
</div>
);
}
return (
<div className="space-y-5 pb-8">
<PageHeader
title="إدارة المستخدمين"
subtitle="بحث وترقيم حقيقيان من الخادم مع تفاصيل Profile Overview لكل حساب."
actions={canManageUsers ? (
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogTrigger asChild>
<Button>
<ShieldPlus className="h-4 w-4" />
إضافة Admin
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>إنشاء حساب إداري</DialogTitle>
<DialogDescription>
هذا الطلب مرتبط بمسار SuperAdmin المباشر لإنشاء Admin جديد.
</DialogDescription>
</DialogHeader>
<div className="grid gap-3">
<Input
placeholder="الاسم"
value={createPayload.name ?? ""}
onChange={(event) =>
setCreatePayload((prev) => ({ ...prev, name: event.target.value }))
}
/>
<Input
placeholder="اسم المستخدم"
value={createPayload.username}
onChange={(event) =>
setCreatePayload((prev) => ({ ...prev, username: event.target.value }))
}
/>
<Input
placeholder="البريد الإلكتروني"
type="email"
value={createPayload.email}
onChange={(event) =>
setCreatePayload((prev) => ({ ...prev, email: event.target.value }))
}
/>
<Input
placeholder="كلمة المرور"
type="password"
value={createPayload.password}
onChange={(event) =>
setCreatePayload((prev) => ({
...prev,
password: event.target.value,
confirmPassword: event.target.value,
}))
}
/>
<Button onClick={handleCreate}>إنشاء الأدمن</Button>
</div>
</DialogContent>
</Dialog>
) : null}
/>
<section className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>إجمالي النتائج</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold text-foreground">{stats.total}</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>أدمنز في الصفحة</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold text-foreground">{stats.admins}</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>حسابات معطلة في الصفحة</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold text-foreground">{stats.disabled}</CardContent>
</Card>
</section>
<Card>
<CardHeader>
<CardTitle>الفلاتر</CardTitle>
</CardHeader>
<CardContent className="grid gap-3 md:grid-cols-4">
<Select
value={scope}
onValueChange={(value) => {
setScope(value as ScopeFilter);
setPage(1);
}}
>
<SelectTrigger>
<SelectValue placeholder="النطاق" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all-users">كل المستخدمين</SelectItem>
<SelectItem value="admins">الأدمنز فقط</SelectItem>
</SelectContent>
</Select>
<div className="relative">
<Search className="pointer-events-none absolute right-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="ابحث بالاسم أو البريد أو اليوزر"
className="pr-9"
/>
</div>
<Select value={verifiedFilter} onValueChange={setVerifiedFilter}>
<SelectTrigger>
<SelectValue placeholder="التحقق" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">الكل</SelectItem>
<SelectItem value="verified">موثق</SelectItem>
<SelectItem value="unverified">غير موثق</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
onClick={() => {
setPage(1);
void loadUsers();
}}
>
تطبيق
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>النتائج</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{!users.length && !loading ? (
<EmptyState
title="لا توجد نتائج"
description="لم يرجع الخادم أي مستخدمين بهذه الفلاتر."
/>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>الاسم</TableHead>
<TableHead>البريد</TableHead>
<TableHead>الدور</TableHead>
<TableHead>موثق</TableHead>
<TableHead>الحالة</TableHead>
<TableHead>إجراءات</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow
key={user._id}
className="cursor-pointer"
onClick={() => setSelectedUserId(user._id)}
>
<TableCell>{user.name ?? user.username ?? "-"}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role ?? "user"}</TableCell>
<TableCell>
<Badge variant={user.isVerified ? "success" : "muted"}>
{user.isVerified ? "موثق" : "غير موثق"}
</Badge>
</TableCell>
<TableCell>
<Badge variant={user.isDisabled ? "danger" : "success"}>
{user.isDisabled ? "معطل" : "نشط"}
</Badge>
</TableCell>
<TableCell className="flex flex-wrap gap-2">
{user.isDisabled ? (
<Button
size="sm"
variant="outline"
disabled={!canManageUsers}
onClick={(event) => {
event.stopPropagation();
void handleEnable(user._id);
}}
>
<UserPlus className="h-4 w-4" />
تفعيل
</Button>
) : (
<Button
size="sm"
variant="outline"
disabled={!canManageUsers}
onClick={(event) => {
event.stopPropagation();
void handleDisable(user._id);
}}
>
<UserMinus className="h-4 w-4" />
تعطيل
</Button>
)}
<Button
size="sm"
variant="danger"
disabled={!canManageUsers}
onClick={(event) => {
event.stopPropagation();
void handleDelete(user._id);
}}
>
<UserX className="h-4 w-4" />
حذف
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
<PaginationControls pagination={getPagination(response)} loading={loading} onPageChange={setPage} />
</CardContent>
</Card>
<Drawer
open={Boolean(selectedUserId)}
onClose={() => {
setSelectedUserId(null);
setSelectedUser(null);
setOverview(null);
setEditPayload({});
}}
title="User details"
description="Review the account profile overview, safety state, and editable admin fields."
side="right"
widthClassName="w-full sm:w-[92vw] sm:max-w-2xl"
>
{detailLoading ? (
<div className="text-sm text-muted-foreground">Loading details...</div>
) : selectedUser ? (
<div className="space-y-4">
<Card className="border-border/70 bg-secondary/20">
<CardContent className="grid gap-3 p-5 md:grid-cols-[auto,1fr,auto] md:items-center">
<div className="flex h-16 w-16 items-center justify-center overflow-hidden rounded-full border border-border/70 bg-background/30">
{selectedUser.avatar && !avatarLoadFailed ? (
<Image
src={resolveMediaUrl(selectedUser.avatar)}
alt={selectedUser.name ?? selectedUser.username ?? "user"}
width={64}
height={64}
unoptimized
loading="lazy"
onError={() => setAvatarLoadFailed(true)}
className="h-full w-full object-cover"
/>
) : (
<span className="text-xl font-semibold text-muted-foreground">
{(selectedUser.name ?? selectedUser.username ?? "U").charAt(0).toUpperCase()}
</span>
)}
</div>
<div className="min-w-0 space-y-1">
<div className="text-lg font-semibold text-foreground">
{selectedUser.name ?? selectedUser.username ?? "-"}
</div>
<div dir="ltr" className="truncate text-sm text-muted-foreground">
{selectedUser.email}
</div>
</div>
<div className="flex flex-wrap gap-2">
<Badge variant={selectedUser.isVerified ? "success" : "muted"}>
{selectedUser.isVerified ? "Verified" : "Unverified"}
</Badge>
<Badge variant={selectedUser.isDisabled ? "danger" : "success"}>
{selectedUser.isDisabled ? "Disabled" : "Active"}
</Badge>
</div>
</CardContent>
</Card>
<Card className="border-border/70 bg-secondary/20">
<CardHeader className="pb-2">
<CardTitle className="text-base">Quick edit</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid gap-3 md:grid-cols-2">
<Input
placeholder="Name"
value={String(editPayload.name ?? "")}
onChange={(event) =>
setEditPayload((prev) => ({ ...prev, name: event.target.value }))
}
/>
<Input
placeholder="Username"
value={String(editPayload.username ?? "")}
onChange={(event) =>
setEditPayload((prev) => ({ ...prev, username: event.target.value }))
}
/>
</div>
<Input
placeholder="Email"
value={String(editPayload.email ?? "")}
onChange={(event) =>
setEditPayload((prev) => ({ ...prev, email: event.target.value }))
}
/>
<div className="grid gap-3 md:grid-cols-2">
<Input
placeholder="Stage name"
value={String(editPayload.stageName ?? "")}
onChange={(event) =>
setEditPayload((prev) => ({ ...prev, stageName: event.target.value }))
}
/>
<Input
placeholder="Location"
value={String(editPayload.location ?? "")}
onChange={(event) =>
setEditPayload((prev) => ({ ...prev, location: event.target.value }))
}
/>
</div>
<Select
disabled={!canManageUsers}
value={String(editPayload.role ?? selectedUser.role ?? "user")}
onValueChange={(value) =>
setEditPayload((prev) => ({ ...prev, role: value as ApiRole }))
}
>
<SelectTrigger>
<SelectValue placeholder="Role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">user</SelectItem>
<SelectItem value="admin">admin</SelectItem>
</SelectContent>
</Select>
<div className="grid gap-3 md:grid-cols-2">
<label className="flex items-center justify-between rounded-lg border border-input bg-background/40 px-3 py-2 text-sm text-foreground">
<span>Verified by super admin</span>
<Switch
checked={Boolean(editPayload.isVerified ?? selectedUser.isVerified)}
onCheckedChange={(value) =>
setEditPayload((prev) => ({ ...prev, isVerified: value }))
}
/>
</label>
<label className="flex items-center justify-between rounded-lg border border-input bg-background/40 px-3 py-2 text-sm text-foreground">
<span>Private account</span>
<Switch
checked={Boolean(editPayload.isPrivate ?? selectedUser.isPrivate)}
onCheckedChange={(value) =>
setEditPayload((prev) => ({ ...prev, isPrivate: value }))
}
/>
</label>
</div>
<Textarea
placeholder="Bio"
value={String(editPayload.bio ?? "")}
onChange={(event) =>
setEditPayload((prev) => ({ ...prev, bio: event.target.value }))
}
/>
<Button disabled={!canManageUsers} onClick={() => void handleSave()}>
Save changes
</Button>
</CardContent>
</Card>
<Card className="border-border/70 bg-secondary/20">
<CardHeader className="pb-2">
<CardTitle className="text-base">Profile Overview</CardTitle>
</CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2">
<FieldRow label="Followers" value={overview?.stats.followersCount} />
<FieldRow label="Following" value={overview?.stats.followingCount} />
<FieldRow label="Posts" value={overview?.stats.postsCount} />
<FieldRow label="Collaborations" value={overview?.stats.collaborationsCount} />
<FieldRow label="Audio" value={overview?.contentCounts.audio} />
<FieldRow label="Reels" value={overview?.contentCounts.reels} />
</CardContent>
</Card>
<Card className="border-border/70 bg-secondary/20">
<CardHeader className="pb-2">
<CardTitle className="text-base">General information</CardTitle>
</CardHeader>
<CardContent className="grid gap-3 md:grid-cols-2">
<FieldRow label="ID" value={selectedUser._id} valueDir="ltr" />
<FieldRow label="Name" value={selectedUser.name} />
<FieldRow label="Username" value={selectedUser.username} valueDir="ltr" />
<FieldRow label="Role" value={selectedUser.role} />
<FieldRow label="Email" value={selectedUser.email} valueDir="ltr" />
<FieldRow label="Location" value={selectedUser.location} />
<FieldRow label="Verified" value={selectedUser.isVerified} />
<FieldRow label="Disabled" value={selectedUser.isDisabled} />
<FieldRow label="Created at" value={formatDateTime(selectedUser.createdAt)} />
<FieldRow label="Updated at" value={formatDateTime(selectedUser.updatedAt)} />
</CardContent>
</Card>
</div>
) : (
<div className="text-sm text-muted-foreground">No details available.</div>
)}
</Drawer>
</div>
);
}