basaHoushanPanel/src/app/user/[id]/page.tsx

294 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { useParams } from "next/navigation"; // useRouter حذف شد
import { useState, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { API_ROUTES, request } from "@/services/api";
import {
Avatar,
Breadcrumb,
Button,
Card,
Descriptions,
Divider,
message as antdMessage,
Select,
Spin,
Table,
Tabs,
Tag,
Typography,
} from "antd";
import type { ColumnsType } from "antd/es/table";
import { UserOutlined, RobotOutlined, TeamOutlined,HistoryOutlined } from "@ant-design/icons";
// تعریف تایپ‌ها
interface SubUser {
id: string;
username: string;
name: string | null;
mobile_number: string | null;
created_at: string;
level: number;
}
// تعریف تایپ بات برای جلوگیری از خطای any
interface Bot {
id: number;
name: string;
description: string;
image: string | null;
}
interface UserDetail {
id: string;
username: string;
name: string | null;
mobile_number: string | null;
email: string | null;
image: string | null;
level: number;
credit: number;
income: number;
created_at: string;
sub_users: SubUser[];
bots?: Bot[]; // استفاده از تایپ Bot به جای any
}
export default function UserPage() {
const { id } = useParams();
const [messageApi, contextHolder] = antdMessage.useMessage();
const [selectedLevel, setSelectedLevel] = useState<number>(1);
// 1. دریافت اطلاعات کامل کاربر
const { data: user, isLoading, refetch } = useQuery({
queryKey: ["user", id],
queryFn: async () => {
const res = await request.get(API_ROUTES.USER.ONE(id as string));
return res.data.user as UserDetail;
},
});
// 2. سینک کردن لول انتخابی
useEffect(() => {
if (user) {
setSelectedLevel(user.level || 1);
}
}, [user]);
// 3. تابع آپدیت لول
const updateLevelMutation = useMutation({
mutationFn: async () => {
if (!user?.id) throw new Error("User ID not found");
return await request.put(`/admin/user/${user.id}/level`, {
level: selectedLevel,
});
},
onSuccess: () => {
messageApi.success("سطح کاربر با موفقیت بروزرسانی شد");
refetch();
},
onError: () => {
messageApi.error("خطا در تغییر سطح کاربر");
},
});
// ستون‌های جدول زیرمجموعه‌ها
const subUsersColumns: ColumnsType<SubUser> = [
{
title: "نام کاربری",
dataIndex: "username",
key: "username",
render: (text) => (
<Link href={`/user/${text}`} className="text-blue-600 hover:underline">
{text}
</Link>
),
},
{
title: "نام",
dataIndex: "name",
key: "name",
render: (text) => text || <span className="text-gray-400">---</span>,
},
{
title: "شماره موبایل",
dataIndex: "mobile_number",
key: "mobile_number",
render: (text) => <span dir="ltr">{text}</span>,
},
{
title: "سطح",
dataIndex: "level",
key: "level",
render: (level) => (
<Tag color={level > 1 ? "gold" : "blue"}>سطح {level}</Tag>
),
},
{
title: "تاریخ عضویت",
dataIndex: "created_at",
key: "created_at",
render: (date) => new Date(date).toLocaleDateString("fa-IR"),
},
];
if (isLoading)
return (
<div className="flex h-screen w-full items-center justify-center">
<Spin size="large" />
</div>
);
if (!user)
return (
<div className="flex h-screen w-full items-center justify-center text-red-500">
کاربر یافت نشد
</div>
);
const items = [
{
key: "1",
label: (
<span>
<RobotOutlined />
دستیارها
</span>
),
children: (
<div className="p-4">
{user.bots && user.bots.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{user.bots.map((bot: Bot) => (
<Card key={bot.id} title={bot.name} size="small" extra={<Link href={`/assistant/${bot.id}`}>مشاهده</Link>}>
<p className="truncate text-gray-500">{bot.description}</p>
</Card>
))}
</div>
) : (
<p className="text-gray-500 text-center">این کاربر دستیاری ندارد.</p>
)}
</div>
),
},
{
key: "2",
label: (
<span>
<TeamOutlined />
زیرمجموعهها ({user.sub_users ? user.sub_users.length : 0})
</span>
),
children: (
<Table
columns={subUsersColumns}
dataSource={user.sub_users || []}
rowKey="id"
pagination={{ pageSize: 5 }}
locale={{ emptyText: "زیرمجموعه‌ای یافت نشد" }}
/>
),
},
];
return (
<>
{contextHolder}
<Breadcrumb
items={[
{ title: <Link href="/dashboard">داشبورد</Link> },
{ title: <Link href="/user">کاربران</Link> },
{ title: user.username },
]}
className="mb-6"
/>
<div className="flex flex-col gap-6">
<Card bordered={false} className="shadow-sm">
<div className="flex flex-col md:flex-row gap-6">
<div className="flex flex-col items-center gap-4 border-l pl-6 md:w-1/4">
<Avatar
size={100}
src={
user.image
? `${process.env.NEXT_PUBLIC_API_BASE_URL}${user.image}`
: `${process.env.NEXT_PUBLIC_AVATAR_BASE_URL}?seed=${user.username}`
}
icon={<UserOutlined />}
/>
<div className="text-center">
<Typography.Title level={4} className="!mb-0">
{user.name || "کاربر ناشناس"}
</Typography.Title>
<Typography.Text type="secondary" copyable>@{user.username}</Typography.Text>
</div>
<Tag color="purple" className="px-3 py-1 text-sm mt-2">
سطح فعلی: {user.level}
</Tag>
{/* ۲. دکمه جدید برای مشاهده تاریخچه تراکنش‌ها */}
<Link href={`/user/${user.id}/history`} className="w-full">
<Button
block
icon={<HistoryOutlined />}
className="mt-2 text-blue-600 border-blue-200 bg-blue-50"
>
تاریخچه تراکنشها
</Button>
</Link>
</div>
<div className="flex-1">
<Descriptions title="مشخصات فردی" layout="vertical" column={{ xs: 1, sm: 2, md: 3 }}>
<Descriptions.Item label="ایمیل">{user.email || "---"}</Descriptions.Item>
<Descriptions.Item label="شماره موبایل">{user.mobile_number || "---"}</Descriptions.Item>
<Descriptions.Item label="تاریخ ثبت نام">
{new Date(user.created_at).toLocaleDateString("fa-IR")}
</Descriptions.Item>
<Descriptions.Item label="موجودی حساب">
{user.credit.toLocaleString()} سکه
</Descriptions.Item>
<Descriptions.Item label="درآمد">
{user.income.toLocaleString()} تومان
</Descriptions.Item>
</Descriptions>
<Divider />
<div className="flex items-end gap-4 bg-blue-50 p-4 rounded-lg border border-blue-100">
<div className="w-full md:w-1/3">
<label className="block text-xs text-gray-500 mb-1">تعیین سطح جدید</label>
<Select
className="w-full"
value={selectedLevel}
onChange={(val) => setSelectedLevel(val)}
options={[
{ label: "بالای 30 سال", value: 1 },
{ label: "بین 18 تا 30 سال", value: 2 },
{ label: "زیر 18 سال", value: 3 },
]}
/>
</div>
<Button
type="primary"
onClick={() => updateLevelMutation.mutate()}
loading={updateLevelMutation.isPending}
disabled={selectedLevel === user.level}
>
بروزرسانی سطح
</Button>
</div>
</div>
</div>
</Card>
<Card bordered={false} className="shadow-sm">
<Tabs defaultActiveKey="2" items={items} />
</Card>
</div>
</>
);
}