dashboard

This commit is contained in:
hamid zarghami
2025-10-08 16:58:40 +03:30
parent a3537e5a66
commit 26f1a057be
14 changed files with 1055 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
import { type FC } from 'react'
import { useGetDashboard, useGetOrderChart } from './hooks/useDashboardData'
import { useLocation } from 'react-router-dom'
import PageLoading from '../../components/PageLoading'
import Error from '../../components/Error'
import DashboardOverview from './components/DashboardOverview'
import ProductsCountSection from './components/ProductsCountSection'
import OrdersCountSection from './components/OrdersCountSection'
import UnreadCommentsSection from './components/UnreadCommentsSection'
import UsersCountSection from './components/UsersCountSection'
import OrdersChartSection from './components/OrdersChartSection'
const Dashboard: FC = () => {
const location = useLocation()
const { data, isLoading, error } = useGetDashboard()
const { data: orderChartData } = useGetOrderChart()
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<PageLoading />
</div>
)
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<Error errorText="خطا در بارگذاری اطلاعات داشبورد" />
</div>
)
}
const renderDashboardContent = () => {
const path = location.pathname
if (path.includes('products-count')) {
return <ProductsCountSection data={data?.results} />
} else if (path.includes('orders-count')) {
return <OrdersCountSection data={data?.results} />
} else if (path.includes('unread-comments')) {
return <UnreadCommentsSection data={data?.results} />
} else if (path.includes('users-count')) {
return <UsersCountSection data={data?.results} />
} else if (path.includes('orders-chart')) {
return <OrdersChartSection
dashboardData={data?.results}
orderChartData={orderChartData?.results}
/>
} else {
// نمایش داشبورد کلی
return <DashboardOverview data={data?.results} />
}
}
return (
<div className="p-6">
{renderDashboardContent()}
</div>
)
}
export default Dashboard
@@ -0,0 +1,87 @@
import { type FC } from 'react'
import TitleLine from '../../../components/TitleLine'
import { type DashboardData } from '../types/Types'
import { Box, ShoppingCart, Message, User } from 'iconsax-react'
import StatCard from './StatCard'
const DashboardOverview: FC<{ data?: DashboardData }> = ({ data }) => {
if (!data) return null
return (
<div className="space-y-6">
{/* دیباگ داده‌های API */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h4 className="text-sm font-medium text-blue-800 mb-2"> وضعیت اتصال به API</h4>
<div className="text-xs text-blue-700 space-y-1">
<div><strong>دادههای واقعی API:</strong> {Array.isArray(data?.FiveDaysAgoSales) ? `${data.FiveDaysAgoSales.length} آیتم دریافت شد` : '❌ داده‌ای وجود ندارد'}</div>
<div><strong>نمودار:</strong> {Array.isArray(data?.FiveDaysAgoSales) && data.FiveDaysAgoSales.length > 0 ? '✅ نمایش داده‌های واقعی API' : '⏳ در انتظار داده‌های API'}</div>
<div><strong>منطق نمایش:</strong> فقط از دادههای واقعی API استفاده میشود</div>
</div>
{Array.isArray(data?.FiveDaysAgoSales) && data.FiveDaysAgoSales.length > 0 && (
<div className="mt-3 p-2 bg-white rounded text-xs">
<div className="font-medium mb-1">نمونه داده واقعی API:</div>
<pre className="text-gray-600 overflow-x-auto">
{JSON.stringify(data.FiveDaysAgoSales[0], null, 2)}
</pre>
</div>
)}
<div className="mt-3 p-2 bg-green-50 rounded text-xs">
<div className="font-medium mb-1 text-green-800">🎯 نحوه عملکرد نمودار:</div>
<div className="text-green-700">
۱. فقط از دادههای واقعی API استفاده میشود<br />
۲. اگر داده وجود نداشته باشد، نمودار خالی نمایش داده میشود<br />
۳. هیچ داده نمونهای استفاده نمیشود<br />
۴. ساختار دادههای مورد انتظار: totalSales, totalOrders
</div>
</div>
</div>
<TitleLine title="نمای کلی داشبورد" />
{/* آمار کلی */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard
title="تعداد محصولات"
value={data.productCount}
icon={<Box size={24} color="#3B82F6" />}
/>
<StatCard
title="تعداد سفارشات"
value={data.proccessingOrders}
icon={<ShoppingCart size={24} color="#10B981" />}
/>
<StatCard
title="کامنت های خوانده نشده"
value={data.pendingComments}
icon={<Message size={24} color="#F59E0B" />}
/>
<StatCard
title="تعداد کاربران"
value={data.usersCount}
icon={<User size={24} color="#8B5CF6" />}
/>
</div>
{/* آمار فروش روزانه */}
<div className="bg-white rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-medium mb-4">آمار فروش روزانه</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">{data.dailySales.totalOrders}</div>
<div className="text-sm text-gray-600">تعداد سفارشات</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">{data.dailySales.totalSales.toLocaleString()} تومان</div>
<div className="text-sm text-gray-600">مجموع فروش</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-600">{data.dailySales.itemsSold}</div>
<div className="text-sm text-gray-600">تعداد آیتم فروش رفته</div>
</div>
</div>
</div>
</div>
)
}
export default DashboardOverview
@@ -0,0 +1,240 @@
import { type FC } from 'react'
import TitleLine from '../../../components/TitleLine'
import { type DashboardData, type OrderChartData } from '../types/Types'
import { Chart } from 'iconsax-react'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'
export interface OrdersChartSectionProps {
dashboardData?: DashboardData
orderChartData?: OrderChartData
}
const OrdersChartSection: FC<OrdersChartSectionProps> = ({
dashboardData,
orderChartData
}) => {
// پردازش داده‌های Order Chart API
const processOrderChartData = (chartData?: OrderChartData) => {
if (chartData && chartData.xAxis && chartData.yAxis) {
return chartData.xAxis.map((xValue, index) => ({
name: xValue,
value: chartData.yAxis[index] || 0,
sales: chartData.yAxis[index] || 0,
}))
}
return []
}
// پردازش داده‌های ۵ روز گذشته برای نمودار (fallback)
const processDashboardChartData = (rawData: unknown) => {
if (Array.isArray(rawData) && rawData.length > 0) {
return (rawData as Array<Record<string, unknown>>).slice(-5).map((item, index: number) => ({
name: `${5 - index} روز پیش`,
sales: (item.totalSales as number) || (item.sales as number) || 0,
orders: (item.totalOrders as number) || (item.orders as number) || 0,
date: item.date || item.created_at || `روز ${5 - index}`,
}))
}
return []
}
// اولویت با داده‌های Order Chart API
const orderChartProcessed = processOrderChartData(orderChartData)
const dashboardChartProcessed = processDashboardChartData(dashboardData?.FiveDaysAgoSales)
// استفاده از داده‌های Order Chart اگر موجود باشد، در غیر این صورت از Dashboard استفاده کن
const chartData = orderChartProcessed.length > 0 ? orderChartProcessed : dashboardChartProcessed
return (
<div className="space-y-6">
<TitleLine title="نمودار سفارشات" />
{/* نمودار فروش ۵ روز گذشته */}
<div className="bg-white rounded-lg p-6 shadow-sm">
<div className="flex items-center gap-3 mb-6">
<Chart size={24} color="#3B82F6" />
<h3 className="text-lg font-medium">آمار فروش ۵ روز گذشته</h3>
</div>
<div className="h-80 w-full">
{chartData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="name"
tick={{ fontSize: 12 }}
axisLine={false}
tickLine={false}
/>
<YAxis
tick={{ fontSize: 12 }}
axisLine={false}
tickLine={false}
tickFormatter={(value) => orderChartData ? value.toLocaleString() : `${(value / 1000000).toFixed(1)}M`}
/>
<Tooltip
formatter={(value: number, name: string) => {
if (orderChartData) {
return [
`${value.toLocaleString()} ${orderChartData.unit || 'تومان'}`,
orderChartData.title || 'مقدار'
]
} else {
return [
name === 'sales' ? `${value.toLocaleString()} تومان` : value,
name === 'sales' ? 'فروش' : 'سفارشات'
]
}
}}
labelStyle={{ color: '#374151' }}
contentStyle={{
backgroundColor: '#ffffff',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)'
}}
/>
<Line
type="monotone"
dataKey="sales"
stroke="#3B82F6"
strokeWidth={3}
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#3B82F6', strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Chart size={48} color="#9CA3AF" className="mx-auto mb-4" />
<div className="text-gray-500 text-sm">
دادهای برای نمایش نمودار وجود ندارد
</div>
<div className="text-gray-400 text-xs mt-1">
منتظر دریافت داده از API...
</div>
</div>
</div>
)}
</div>
</div>
{/* نمودار مقایسه فروش و سفارشات */}
<div className="bg-white rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-medium mb-6">مقایسه فروش و تعداد سفارشات</h3>
<div className="h-80 w-full">
{chartData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="name"
tick={{ fontSize: 12 }}
axisLine={false}
tickLine={false}
/>
<YAxis
yAxisId="left"
tick={{ fontSize: 12 }}
axisLine={false}
tickLine={false}
tickFormatter={(value) => orderChartData ? value.toLocaleString() : `${(value / 1000000).toFixed(1)}M`}
/>
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 12 }}
axisLine={false}
tickLine={false}
/>
<Tooltip
formatter={(value: number, name: string) => {
if (orderChartData) {
return [
`${value.toLocaleString()} ${orderChartData.unit || 'تومان'}`,
orderChartData.title || 'مقدار'
]
} else {
return [
name === 'sales' ? `${value.toLocaleString()} تومان` : value,
name === 'sales' ? 'فروش' : 'سفارشات'
]
}
}}
labelStyle={{ color: '#374151' }}
contentStyle={{
backgroundColor: '#ffffff',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)'
}}
/>
<Bar
yAxisId="left"
dataKey="sales"
fill="#10B981"
radius={[4, 4, 0, 0]}
/>
<Bar
yAxisId="right"
dataKey="orders"
fill="#8B5CF6"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Chart size={48} color="#9CA3AF" className="mx-auto mb-4" />
<div className="text-gray-500 text-sm">
دادهای برای نمایش نمودار مقایسه وجود ندارد
</div>
<div className="text-gray-400 text-xs mt-1">
منتظر دریافت داده از API...
</div>
</div>
</div>
)}
</div>
{chartData.length > 0 && (
<div className="flex justify-center gap-6 mt-4">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-green-500 rounded"></div>
<span className="text-sm text-gray-600">فروش (تومان)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-purple-500 rounded"></div>
<span className="text-sm text-gray-600">تعداد سفارشات</span>
</div>
</div>
)}
</div>
{/* آمار ماهانه */}
<div className="bg-white rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-medium mb-4">آمار فروش ماهانه</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">{dashboardData?.monthlySales?.totalOrders || 0}</div>
<div className="text-sm text-gray-600">تعداد سفارشات</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">{dashboardData?.monthlySales?.totalSales?.toLocaleString() || 0} تومان</div>
<div className="text-sm text-gray-600">مجموع فروش</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-600">{dashboardData?.monthlySales?.itemsSold || 0}</div>
<div className="text-sm text-gray-600">تعداد آیتم فروش رفته</div>
</div>
</div>
</div>
</div>
)
}
export default OrdersChartSection
@@ -0,0 +1,49 @@
import { type FC } from 'react'
import TitleLine from '../../../components/TitleLine'
import { type DashboardData } from '../types/Types'
import { ShoppingCart } from 'iconsax-react'
import StatCard from './StatCard'
const OrdersCountSection: FC<{ data?: DashboardData }> = ({ data }) => {
if (!data) return null
return (
<div className="space-y-6">
<TitleLine title="آمار سفارشات" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<StatCard
title="سفارشات در حال پردازش"
value={data.proccessingOrders}
icon={<ShoppingCart size={24} color="#10B981" />}
/>
<StatCard
title="درخواست‌های برداشت وجه"
value={data.withdrawalRequests}
icon={<ShoppingCart size={24} color="#F59E0B" />}
/>
</div>
{/* آمار فروش روزانه */}
<div className="bg-white rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-medium mb-4">آمار فروش روزانه</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">{data.dailySales.totalOrders}</div>
<div className="text-sm text-gray-600">تعداد سفارشات</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">{data.dailySales.totalSales.toLocaleString()} تومان</div>
<div className="text-sm text-gray-600">مجموع فروش</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-600">{data.dailySales.itemsSold}</div>
<div className="text-sm text-gray-600">تعداد آیتم فروش رفته</div>
</div>
</div>
</div>
</div>
)
}
export default OrdersCountSection
@@ -0,0 +1,35 @@
import { type FC } from 'react'
import TitleLine from '../../../components/TitleLine'
import { type DashboardData } from '../types/Types'
import { Box, Shop } from 'iconsax-react'
import StatCard from './StatCard'
const ProductsCountSection: FC<{ data?: DashboardData }> = ({ data }) => {
if (!data) return null
return (
<div className="space-y-6">
<TitleLine title="آمار محصولات" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<StatCard
title="کل محصولات"
value={data.productCount}
icon={<Box size={24} color="#3B82F6" />}
/>
<StatCard
title="محصولات در انتظار تایید"
value={data.pendingProductCount}
icon={<Box size={24} color="#F59E0B" />}
/>
<StatCard
title="برندهای در انتظار تایید"
value={data.pendingBrandsCount}
icon={<Shop size={24} color="#8B5CF6" />}
/>
</div>
</div>
)
}
export default ProductsCountSection
@@ -0,0 +1,25 @@
import { type FC } from 'react'
interface StatCardProps {
title: string
value: number
icon: React.ReactNode
}
const StatCard: FC<StatCardProps> = ({ title, value, icon }) => {
return (
<div className="bg-white rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">{title}</p>
<p className="text-2xl font-bold text-gray-900">{value?.toLocaleString() || 0}</p>
</div>
<div className="p-3 bg-gray-50 rounded-full">
{icon}
</div>
</div>
</div>
)
}
export default StatCard
@@ -0,0 +1,30 @@
import { type FC } from 'react'
import TitleLine from '../../../components/TitleLine'
import { type DashboardData } from '../types/Types'
import { Message } from 'iconsax-react'
import StatCard from './StatCard'
const UnreadCommentsSection: FC<{ data?: DashboardData }> = ({ data }) => {
if (!data) return null
return (
<div className="space-y-6">
<TitleLine title="کامنت‌ها و پرسش‌ها" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<StatCard
title="کامنت های خوانده نشده"
value={data.pendingComments}
icon={<Message size={24} color="#F59E0B" />}
/>
<StatCard
title="پرسش های خوانده نشده"
value={data.pendingQuestions}
icon={<Message size={24} color="#8B5CF6" />}
/>
</div>
</div>
)
}
export default UnreadCommentsSection
@@ -0,0 +1,30 @@
import { type FC } from 'react'
import TitleLine from '../../../components/TitleLine'
import { type DashboardData } from '../types/Types'
import { User, Shop } from 'iconsax-react'
import StatCard from './StatCard'
const UsersCountSection: FC<{ data?: DashboardData }> = ({ data }) => {
if (!data) return null
return (
<div className="space-y-6">
<TitleLine title="آمار کاربران" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<StatCard
title="کل کاربران"
value={data.usersCount}
icon={<User size={24} color="#8B5CF6" />}
/>
<StatCard
title="تعداد فروشندگان"
value={data.sellersCount}
icon={<Shop size={24} color="#10B981" />}
/>
</div>
</div>
)
}
export default UsersCountSection
@@ -0,0 +1,16 @@
import { useQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
export const useGetDashboard = () => {
return useQuery({
queryKey: ["dashboard"],
queryFn: api.getDashboard,
});
};
export const useGetOrderChart = () => {
return useQuery({
queryKey: ["order-chart"],
queryFn: api.getOrderChart,
});
};
+12
View File
@@ -0,0 +1,12 @@
import axios from "@/config/axios";
import type { DashboardResponse, OrderChartResponse } from "../types/Types";
export const getDashboard = async (): Promise<DashboardResponse> => {
const { data } = await axios.get<DashboardResponse>(`/admin/reports`);
return data;
};
export const getOrderChart = async (): Promise<OrderChartResponse> => {
const { data } = await axios.get<OrderChartResponse>(`/admin/orders/chart`);
return data;
};
+95
View File
@@ -0,0 +1,95 @@
// تایپ‌های مربوط به فروش
interface SalesStats {
_id: string | null;
totalSales: number;
totalNetSales: number;
itemsSold: number;
totalOrders: number;
}
// تایپ‌های مربوط به فروش حمل و نقل
interface ShipmentSalesStats {
_id: string | null;
totalShipmentCost: number;
itemsSold: number;
totalOrders: number;
}
// تایپ فروشگاه
interface Shop {
_id: string;
shopName: string;
}
// تایپ جزئیات محصول
interface ProductDetails {
_id: number;
title_fa: string;
title_en: string;
model: string;
description: string;
shop: Shop;
}
// تایپ محصولات برتر
interface TopProduct {
totalSold: number;
productDetails: ProductDetails;
productId: number;
}
// تایپ فروش ۵ روز گذشته (ساختار دقیق مشخص نیست، فعلاً unknown استفاده می‌کنیم)
type FiveDaysAgoSales = unknown[];
// تایپ اصلی داشبورد
export interface DashboardData {
dailySales: SalesStats;
weeklySales: SalesStats;
monthlySales: SalesStats | null;
yearlySales: SalesStats;
dailyShipmentSales: ShipmentSalesStats;
weeklyShipmentSales: ShipmentSalesStats;
monthlyShipmentSales: ShipmentSalesStats;
yearlyShipmentSales: ShipmentSalesStats;
withdrawalRequests: number;
FiveDaysAgoSales: FiveDaysAgoSales;
topProducts: TopProduct[];
proccessingOrders: number;
pendingComments: number;
pendingQuestions: number;
sellersCount: number;
usersCount: number;
wholesaleRequestCount: number;
pendingBrandsCount: number;
pendingWarranties: number;
productCount: number;
pendingProductCount: number;
}
// تایپ داده‌های نمودار سفارش
export interface OrderChartData {
chartType: string;
title: string;
unit: string;
granularity: string;
xAxis: string[];
yAxis: number[];
totalSales: number;
_debug: {
aggregatedPeriods: number;
};
}
// تایپ پاسخ API نمودار سفارش
export interface OrderChartResponse {
status: number;
success: boolean;
results: OrderChartData;
}
// تایپ پاسخ API
export interface DashboardResponse {
status: number;
success: boolean;
results: DashboardData;
}