diff --git a/package-lock.json b/package-lock.json
index 5e67e45..c731b1c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,11 +13,13 @@
"@tanstack/react-query": "^5.80.6",
"@tanstack/react-query-devtools": "^5.80.6",
"@types/js-cookie": "^3.0.6",
+ "apexcharts": "^5.3.6",
"axios": "^1.9.0",
"clsx": "^2.0.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.263.1",
"react": "^19.2.3",
+ "react-apexcharts": "^1.9.0",
"react-date-object": "2.1.9",
"react-dom": "^19.2.3",
"react-hook-form": "^7.57.0",
@@ -1344,6 +1346,62 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
+ "node_modules/@svgdotjs/svg.draggable.js": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
+ "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@svgdotjs/svg.js": "^3.2.4"
+ }
+ },
+ "node_modules/@svgdotjs/svg.filter.js": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz",
+ "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@svgdotjs/svg.js": "^3.2.4"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/@svgdotjs/svg.js": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz",
+ "integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Fuzzyma"
+ }
+ },
+ "node_modules/@svgdotjs/svg.resize.js": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz",
+ "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18"
+ },
+ "peerDependencies": {
+ "@svgdotjs/svg.js": "^3.2.4",
+ "@svgdotjs/svg.select.js": "^4.0.1"
+ }
+ },
+ "node_modules/@svgdotjs/svg.select.js": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
+ "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18"
+ },
+ "peerDependencies": {
+ "@svgdotjs/svg.js": "^3.2.4"
+ }
+ },
"node_modules/@tanstack/query-core": {
"version": "5.80.6",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.6.tgz",
@@ -1833,6 +1891,12 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
}
},
+ "node_modules/@yr/monotone-cubic-spline": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
+ "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
+ "license": "MIT"
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -1973,6 +2037,20 @@
"node": ">= 8"
}
},
+ "node_modules/apexcharts": {
+ "version": "5.3.6",
+ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.3.6.tgz",
+ "integrity": "sha512-sVEPw+J0Gp0IHQabKu8cfdsxlfME0e36Wid7RIaPclGM2OUt+O7O4+6mfAmTUYhy5bDk8cNHzEhPfVtLCIXEJA==",
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@svgdotjs/svg.draggable.js": "^3.0.4",
+ "@svgdotjs/svg.filter.js": "^3.0.8",
+ "@svgdotjs/svg.js": "^3.2.4",
+ "@svgdotjs/svg.resize.js": "^2.0.2",
+ "@svgdotjs/svg.select.js": "^4.0.1",
+ "@yr/monotone-cubic-spline": "^1.0.3"
+ }
+ },
"node_modules/arch": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
@@ -5787,6 +5865,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-apexcharts": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.9.0.tgz",
+ "integrity": "sha512-DDBzQFuKdwyCEZnji1yIcjlnV8hRr4VDabS5Y3iuem/WcTq6n4VbjWPzbPm3aOwW4I+rf/gA3zWqhws4z9CwLw==",
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "apexcharts": ">=4.0.0",
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/react-date-object": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz",
diff --git a/package.json b/package.json
index cfe77f7..68a0e9d 100644
--- a/package.json
+++ b/package.json
@@ -21,11 +21,13 @@
"@tanstack/react-query": "^5.80.6",
"@tanstack/react-query-devtools": "^5.80.6",
"@types/js-cookie": "^3.0.6",
+ "apexcharts": "^5.3.6",
"axios": "^1.9.0",
"clsx": "^2.0.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.263.1",
"react": "^19.2.3",
+ "react-apexcharts": "^1.9.0",
"react-date-object": "2.1.9",
"react-dom": "^19.2.3",
"react-hook-form": "^7.57.0",
diff --git a/src/components/charts/ApexAreaChartCard.tsx b/src/components/charts/ApexAreaChartCard.tsx
new file mode 100644
index 0000000..9ace606
--- /dev/null
+++ b/src/components/charts/ApexAreaChartCard.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import ReactApexChart from 'react-apexcharts';
+import type { ApexOptions } from 'apexcharts';
+import { CardTitle } from '../ui/Typography';
+import { englishToPersian, formatWithThousands } from '@/utils/numberUtils';
+
+interface ApexAreaChartCardProps {
+ data: { name: string; value: number }[];
+ title?: string;
+ color?: string;
+}
+
+const formatNumber = (value: number | string) => {
+ const formatted = formatWithThousands(value);
+ return englishToPersian(formatted);
+};
+
+export const ApexAreaChartCard = ({ data, title, color = '#3b82f6' }: ApexAreaChartCardProps) => {
+ const categories = data.map((item) => item.name);
+ const series = [
+ {
+ name: title || '',
+ data: data.map((item) => item.value),
+ },
+ ];
+
+ const options: ApexOptions = {
+ chart: {
+ type: 'area',
+ height: 250,
+ toolbar: { show: false },
+ zoom: { enabled: false },
+ fontFamily: 'inherit',
+ },
+ colors: [color],
+ dataLabels: { enabled: false },
+ stroke: {
+ curve: 'smooth',
+ width: 3,
+ },
+ fill: {
+ type: 'gradient',
+ gradient: {
+ shadeIntensity: 1,
+ opacityFrom: 0.45,
+ opacityTo: 0.05,
+ stops: [0, 90, 100],
+ },
+ },
+ grid: {
+ strokeDashArray: 4,
+ padding: { left: 12, right: 12 },
+ },
+ xaxis: {
+ categories,
+ labels: {
+ style: { fontSize: '11px' },
+ formatter: (value) => englishToPersian(value),
+ },
+ axisBorder: { show: true },
+ axisTicks: { show: true },
+ },
+ yaxis: {
+ labels: {
+ style: { fontSize: '11px' },
+ formatter: (value) => formatNumber(value),
+ minWidth: 70,
+ align: 'right',
+ offsetX: 0,
+ },
+ },
+ tooltip: {
+ y: {
+ formatter: (value) => formatNumber(value),
+ },
+ x: {
+ formatter: (value) => englishToPersian(value),
+ },
+ },
+ };
+
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+
+ );
+};
diff --git a/src/components/charts/ApexBarChartCard.tsx b/src/components/charts/ApexBarChartCard.tsx
new file mode 100644
index 0000000..c58b208
--- /dev/null
+++ b/src/components/charts/ApexBarChartCard.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import ReactApexChart from 'react-apexcharts';
+import type { ApexOptions } from 'apexcharts';
+import { CardTitle } from '../ui/Typography';
+import { englishToPersian, formatWithThousands } from '@/utils/numberUtils';
+
+interface ApexBarChartCardProps {
+ data: { name: string; value: number }[];
+ title?: string;
+ color?: string;
+}
+
+const formatNumber = (value: number | string) => {
+ const formatted = formatWithThousands(value);
+ return englishToPersian(formatted);
+};
+
+export const ApexBarChartCard = ({ data, title, color = '#3b82f6' }: ApexBarChartCardProps) => {
+ const categories = data.map((item) => item.name);
+ const series = [
+ {
+ name: title || '',
+ data: data.map((item) => item.value),
+ },
+ ];
+
+ const options: ApexOptions = {
+ chart: {
+ type: 'bar',
+ height: 250,
+ toolbar: { show: false },
+ fontFamily: 'inherit',
+ },
+ colors: [color],
+ plotOptions: {
+ bar: {
+ borderRadius: 6,
+ columnWidth: '40%',
+ },
+ },
+ dataLabels: { enabled: false },
+ grid: {
+ strokeDashArray: 4,
+ padding: { left: 12, right: 12 },
+ },
+ xaxis: {
+ categories,
+ labels: {
+ style: { fontSize: '11px' },
+ formatter: (value) => englishToPersian(value),
+ },
+ },
+ yaxis: {
+ labels: {
+ style: { fontSize: '11px' },
+ formatter: (value) => formatNumber(value),
+ minWidth: 70,
+ align: 'right',
+ offsetX: 0,
+ },
+ },
+ tooltip: {
+ y: {
+ formatter: (value) => formatNumber(value),
+ },
+ x: {
+ formatter: (value) => englishToPersian(value),
+ },
+ },
+ };
+
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+
+ );
+};
diff --git a/src/components/charts/AreaChartCard.tsx b/src/components/charts/AreaChartCard.tsx
new file mode 100644
index 0000000..dec6224
--- /dev/null
+++ b/src/components/charts/AreaChartCard.tsx
@@ -0,0 +1,79 @@
+import { AreaChart as RechartsAreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
+import { CardTitle } from '../ui/Typography';
+import { englishToPersian, formatWithThousands } from '@/utils/numberUtils';
+
+interface AreaChartCardProps {
+ data: any[];
+ title?: string;
+ color?: string;
+}
+
+const formatNumber = (value: number | string) => {
+ const formatted = formatWithThousands(value);
+ return englishToPersian(formatted);
+};
+
+export const AreaChartCard = ({ data, title, color = '#3b82f6' }: AreaChartCardProps) => {
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ englishToPersian(value)}
+ interval="preserveStartEnd"
+ minTickGap={16}
+ height={30}
+ />
+ formatNumber(value)}
+ width={72}
+ tickMargin={8}
+ tickCount={4}
+ allowDecimals={false}
+ />
+ formatNumber(value)}
+ labelFormatter={(label: any) => englishToPersian(label)}
+ />
+
+
+
+
+
+ );
+};
diff --git a/src/components/charts/BarChart.tsx b/src/components/charts/BarChart.tsx
index 798798c..334b7b9 100644
--- a/src/components/charts/BarChart.tsx
+++ b/src/components/charts/BarChart.tsx
@@ -1,5 +1,12 @@
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { CardTitle } from '../ui/Typography';
+import { englishToPersian, formatWithThousands } from '@/utils/numberUtils';
+
+const formatNumber = (value: number | string) => {
+ const formatted = formatWithThousands(value);
+ return englishToPersian(formatted);
+};
+
interface BarChartProps {
data: any[];
@@ -17,21 +24,30 @@ export const BarChart = ({ data, title, color = '#3b82f6' }: BarChartProps) => {
)}
-
-
+
+
+
+
+
+
+
+
englishToPersian(value)}
+ interval="preserveStartEnd"
+ height={40}
/>
formatNumber(value)}
+ width={72}
+ tickMargin={8}
+ tickCount={4}
+ allowDecimals={false}
/>
{
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
fontSize: '12px',
+ fontFamily: 'inherit',
}}
+ formatter={(value: any) => formatNumber(value)}
+ labelFormatter={(label: any) => englishToPersian(label)}
/>
-
+
diff --git a/src/components/charts/LineChart.tsx b/src/components/charts/LineChart.tsx
index 2290ddd..5f9a513 100644
--- a/src/components/charts/LineChart.tsx
+++ b/src/components/charts/LineChart.tsx
@@ -1,5 +1,12 @@
import { LineChart as RechartsLineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { CardTitle } from '../ui/Typography';
+import { englishToPersian, formatWithThousands } from '@/utils/numberUtils';
+
+const formatNumber = (value: number | string) => {
+ const formatted = formatWithThousands(value);
+ return englishToPersian(formatted);
+};
+
interface LineChartProps {
data: any[];
@@ -17,21 +24,24 @@ export const LineChart = ({ data, title, color = '#10b981' }: LineChartProps) =>
)}
-
-
+
+
englishToPersian(value)}
+ interval="preserveStartEnd"
+ height={40}
/>
formatNumber(value)}
+ width={72}
+ tickMargin={8}
+ tickCount={4}
+ allowDecimals={false}
/>
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
fontSize: '12px',
+ fontFamily: 'inherit',
}}
+ formatter={(value: any) => formatNumber(value)}
+ labelFormatter={(label: any) => englishToPersian(label)}
/>
diff --git a/src/components/charts/PieChart.tsx b/src/components/charts/PieChart.tsx
index 2632122..290dcd2 100644
--- a/src/components/charts/PieChart.tsx
+++ b/src/components/charts/PieChart.tsx
@@ -1,5 +1,6 @@
import { PieChart as RechartsPieChart, Pie, Cell, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { CardTitle } from '../ui/Typography';
+import { englishToPersian, formatWithThousands } from '@/utils/numberUtils';
interface PieChartProps {
data: any[];
@@ -22,7 +23,7 @@ export const PieChart = ({ data, title, colors = DEFAULT_COLORS }: PieChartProps
style={{ backgroundColor: entry.color }}
/>
- {entry.value}: {Math.round(entry.payload.value)}%
+ {entry.value}: {englishToPersian(Math.round(entry.payload.value))}%
))}
@@ -77,8 +78,9 @@ export const PieChart = ({ data, title, colors = DEFAULT_COLORS }: PieChartProps
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
fontSize: '14px',
fontWeight: '500',
+ fontFamily: 'inherit',
}}
- formatter={(value: any, name: any) => [`${Math.round(value)}%`, name]}
+ formatter={(value: any, name: any) => [`${englishToPersian(Math.round(value))}%`, name]}
/>
diff --git a/src/constant/routes.ts b/src/constant/routes.ts
index 792f6ab..5efc491 100644
--- a/src/constant/routes.ts
+++ b/src/constant/routes.ts
@@ -157,6 +157,9 @@ export const API_ROUTES = {
PAYMENT_METHODS_REPORT: "reports/payments/methods",
PAYMENT_TRANSACTIONS_REPORT: "reports/payments/transactions",
SHIPMENTS_BY_METHOD_REPORT: "reports/shipments/by-method",
+ SALES_GROWTH_REPORT: "reports/sales/growth",
+ USER_REGISTRATION_GROWTH_REPORT: "reports/user-registration/growth",
+ SALES_BY_CATEGORY_REPORT: "reports/sales/by-category",
// Product Comments APIs
GET_PRODUCT_COMMENTS: "products/comments",
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx
index 1719ae7..5c16e30 100644
--- a/src/pages/Dashboard.tsx
+++ b/src/pages/Dashboard.tsx
@@ -1,167 +1,107 @@
-import { Users, ShoppingBag, DollarSign, TrendingUp, BarChart3, Plus, Clock } from 'lucide-react';
-import { StatsCard } from '../components/dashboard/StatsCard';
-import { BarChart } from '../components/charts/BarChart';
-import { lazy, Suspense } from 'react';
-import { useOrderStats } from './orders/core/_hooks';
+import { ApexAreaChartCard } from '../components/charts/ApexAreaChartCard';
+import { ApexBarChartCard } from '../components/charts/ApexBarChartCard';
+import { useMemo } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useSalesGrowthReport, useUserRegistrationGrowthReport, useSalesByCategoryReport } from './reports/sales-statistics/core/_hooks';
+import { useOrders } from './orders/core/_hooks';
+import { StatusBadge } from '../components/ui/StatusBadge';
+import { formatCurrency, formatDate } from '../utils/formatters';
-const LineChart = lazy(() => import('../components/charts/LineChart').then(module => ({ default: module.LineChart })));
import { PieChart } from '../components/charts/PieChart';
import { Table } from '../components/ui/Table';
import { Button } from '../components/ui/Button';
-import { PermissionWrapper } from '../components/common/PermissionWrapper';
import { PageContainer, PageTitle, CardTitle } from '../components/ui/Typography';
import { ChartData, TableColumn } from '../types';
-const useDashboardStats = () => {
- const { data, isLoading, error } = useOrderStats(true);
- const items = [
+export const Dashboard = () => {
+ const navigate = useNavigate();
+ const { data: salesGrowthReport } = useSalesGrowthReport({ group_by: 'month' });
+ const { data: registrationGrowthReport } = useUserRegistrationGrowthReport({ group_by: 'month' });
+ const { data: salesByCategoryReport } = useSalesByCategoryReport();
+ const recentOrdersFilters = useMemo(() => ({
+ page: 1,
+ limit: 5,
+ status: 'pending' as const,
+ }), []);
+ const { data: recentOrders, isLoading: isOrdersLoading } = useOrders(recentOrdersFilters);
+
+ const monthlySalesData: ChartData[] = useMemo(() => {
+ return (salesGrowthReport?.sales || []).map((item) => ({
+ name: item.month_name || `${item.year}/${item.month}`,
+ value: item.total_sales,
+ }));
+ }, [salesGrowthReport]);
+
+ const registrationGrowthData: ChartData[] = useMemo(() => {
+ return (registrationGrowthReport?.registrations || []).map((item) => ({
+ name: item.month_name || `${item.year}/${item.month}`,
+ value: item.total_users,
+ }));
+ }, [registrationGrowthReport]);
+
+ const salesByCategoryData: ChartData[] = useMemo(() => {
+ return (salesByCategoryReport?.categories || []).map((item) => ({
+ name: item.category_name,
+ value: item.percentage,
+ }));
+ }, [salesByCategoryReport]);
+
+ const orderColumns: TableColumn[] = [
{
- title: 'کل سفارشات',
- value: data?.total_orders_count ?? 0,
- icon: ShoppingBag,
- color: 'yellow' as const,
+ key: 'order_number',
+ label: 'شماره سفارش',
+ render: (value: string) => `#${value}`,
},
{
- title: 'مجموع فروش',
- value: data?.total_amount_of_sale ?? 0,
- icon: DollarSign,
- color: 'green' as const,
+ key: 'customer',
+ label: 'مشتری',
+ render: (_value, row: any) => {
+ const customer = row.user || row.customer;
+ const name = `${customer?.first_name || ''} ${customer?.last_name || ''}`.trim();
+ return name || 'نامشخص';
+ },
},
{
- title: 'سفارشهای در انتظار',
- value: data?.total_order_pending ?? 0,
- icon: Clock,
- color: 'blue' as const,
+ key: 'final_total',
+ label: 'مبلغ',
+ render: (_value, row: any) => formatCurrency(row.final_total || row.total_amount || 0),
},
{
- title: 'میانگین سفارش',
- value: data?.order_avg ?? 0,
- icon: TrendingUp,
- color: 'purple' as const,
+ key: 'status',
+ label: 'وضعیت',
+ render: (value: any) => ,
+ },
+ {
+ key: 'created_at',
+ label: 'تاریخ',
+ render: (value: string) => formatDate(value),
},
];
- return { items, isLoading, error };
-};
-const chartData: ChartData[] = [
- { name: 'فروردین', value: 4000 },
- { name: 'اردیبهشت', value: 3000 },
- { name: 'خرداد', value: 5000 },
- { name: 'تیر', value: 4500 },
- { name: 'مرداد', value: 6000 },
- { name: 'شهریور', value: 5500 },
-];
+ const ordersTableData = (recentOrders?.orders || []).map((item) => item.order ?? item);
-const pieData: ChartData[] = [
- { name: 'دسکتاپ', value: 45 },
- { name: 'موبایل', value: 35 },
- { name: 'تبلت', value: 20 },
-];
-
-const recentUsers = [
- { id: 1, name: 'علی احمدی', email: 'ali@example.com', role: 'کاربر', status: 'فعال', createdAt: '۱۴۰۲/۰۸/۱۵' },
- { id: 2, name: 'فاطمه حسینی', email: 'fateme@example.com', role: 'مدیر', status: 'فعال', createdAt: '۱۴۰۲/۰۸/۱۴' },
- { id: 3, name: 'محمد رضایی', email: 'mohammad@example.com', role: 'کاربر', status: 'غیرفعال', createdAt: '۱۴۰۲/۰۸/۱۳' },
- { id: 4, name: 'زهرا کریمی', email: 'zahra@example.com', role: 'کاربر', status: 'فعال', createdAt: '۱۴۰۲/۰۸/۱۲' },
-];
-
-const userColumns: TableColumn[] = [
- { key: 'name', label: 'نام', sortable: true },
- { key: 'email', label: 'ایمیل' },
- { key: 'role', label: 'نقش' },
- {
- key: 'status',
- label: 'وضعیت',
- render: (value) => (
-
- {value}
-
- )
- },
- { key: 'createdAt', label: 'تاریخ عضویت' },
- {
- key: 'actions',
- label: 'عملیات',
- render: () => (
-
-
-
-
-
-
- )
- }
-];
-
-export const Dashboard = () => {
- const { items: statsData, isLoading: statsLoading, error: statsError } = useDashboardStats();
return (
{/* Header with mobile-responsive layout */}
- {/* Stats Cards - Mobile responsive grid */}
-
- {statsLoading ? (
- <>
- {[...Array(4)].map((_, idx) => (
-
- ))}
- >
- ) : (
- statsData.map((stat, index) => (
-
- ))
- )}
-
- {statsError && (
-
- خطا در دریافت آمار سفارشات
-
- )}
-
{/* Charts - Better mobile layout */}
@@ -170,20 +110,22 @@ export const Dashboard = () => {
- کاربران اخیر
+ آخرین سفارشات در انتظار
+
+
diff --git a/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx b/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx
index c6db9f2..9f70d41 100644
--- a/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx
+++ b/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx
@@ -163,7 +163,7 @@ const DiscountCodeFormPage = () => {
description: category.description || `دستهبندی #${category.id}`
}));
- const { register, handleSubmit, control, formState: { errors, isValid }, reset, watch } = useForm({
+ const { register, handleSubmit, control, formState: { errors, isValid }, reset, watch, setValue } = useForm({
resolver: yupResolver(schema as any),
mode: 'onChange',
defaultValues: { status: 'active', type: 'percentage', application_level: 'invoice', single_use: false }
@@ -171,6 +171,23 @@ const DiscountCodeFormPage = () => {
const applicationLevel = watch('application_level');
+ const handleApplicationLevelChange = (value: string) => {
+ setSelectedApplicationLevel(value);
+ setValue('application_level', value as any, { shouldDirty: true, shouldValidate: true });
+ if (value !== 'product') {
+ setSelectedProductIds([]);
+ }
+ if (value !== 'category') {
+ setSelectedCategoryIds([]);
+ }
+ };
+
+ useEffect(() => {
+ if (applicationLevel && applicationLevel !== selectedApplicationLevel) {
+ handleApplicationLevelChange(applicationLevel);
+ }
+ }, [applicationLevel]);
+
useEffect(() => {
if (isEdit && dc) {
reset({
@@ -231,8 +248,8 @@ const DiscountCodeFormPage = () => {
...cleanRestrictions,
user_ids: [selectedUserId],
} : cleanRestrictions.user_group ? cleanRestrictions : undefined,
- product_ids: selectedProductIds.length > 0 ? selectedProductIds : undefined,
- category_ids: selectedCategoryIds.length > 0 ? selectedCategoryIds : undefined,
+ product_ids: selectedApplicationLevel === 'product' && selectedProductIds.length > 0 ? selectedProductIds : undefined,
+ category_ids: selectedApplicationLevel === 'category' && selectedCategoryIds.length > 0 ? selectedCategoryIds : undefined,
};
if (isEdit && id) {
@@ -472,7 +489,7 @@ const DiscountCodeFormPage = () => {
name="application_level"
value={option.value}
checked={isSelected}
- onChange={() => setSelectedApplicationLevel(option.value)}
+ onChange={() => handleApplicationLevelChange(option.value)}
className="sr-only"
/>
diff --git a/src/pages/reports/sales-statistics/core/_hooks.ts b/src/pages/reports/sales-statistics/core/_hooks.ts
new file mode 100644
index 0000000..8e8f472
--- /dev/null
+++ b/src/pages/reports/sales-statistics/core/_hooks.ts
@@ -0,0 +1,35 @@
+import { useQuery } from "@tanstack/react-query";
+import { QUERY_KEYS } from "@/utils/query-key";
+import {
+ getSalesGrowthReport,
+ getUserRegistrationGrowthReport,
+ getSalesByCategoryReport,
+} from "./_requests";
+import {
+ SalesGrowthFilters,
+ UserRegistrationGrowthFilters,
+ SalesByCategoryFilters,
+} from "./_models";
+
+export const useSalesGrowthReport = (filters?: SalesGrowthFilters) => {
+ return useQuery({
+ queryKey: [QUERY_KEYS.GET_SALES_GROWTH_REPORT, filters],
+ queryFn: () => getSalesGrowthReport(filters),
+ });
+};
+
+export const useUserRegistrationGrowthReport = (
+ filters?: UserRegistrationGrowthFilters
+) => {
+ return useQuery({
+ queryKey: [QUERY_KEYS.GET_USER_REGISTRATION_GROWTH_REPORT, filters],
+ queryFn: () => getUserRegistrationGrowthReport(filters),
+ });
+};
+
+export const useSalesByCategoryReport = (filters?: SalesByCategoryFilters) => {
+ return useQuery({
+ queryKey: [QUERY_KEYS.GET_SALES_BY_CATEGORY_REPORT, filters],
+ queryFn: () => getSalesByCategoryReport(filters),
+ });
+};
diff --git a/src/pages/reports/sales-statistics/core/_models.ts b/src/pages/reports/sales-statistics/core/_models.ts
new file mode 100644
index 0000000..1334fbc
--- /dev/null
+++ b/src/pages/reports/sales-statistics/core/_models.ts
@@ -0,0 +1,89 @@
+export interface SalesGrowthFilters {
+ group_by?: "month" | "year";
+ status?: string;
+ from?: string; // ISO 8601
+ to?: string; // ISO 8601
+}
+
+export interface SalesGrowthEntry {
+ year: number;
+ month: number;
+ month_name: string;
+ total_sales: number; // تومان
+ total_orders: number;
+ avg_per_order: number;
+}
+
+export interface SalesGrowthSummary {
+ total_sales: number;
+ total_orders: number;
+ average_per_month: number;
+ growth_rate: number;
+ peak_month: string;
+ peak_month_sales: number;
+}
+
+export interface SalesGrowthResponse {
+ sales: SalesGrowthEntry[];
+ summary: SalesGrowthSummary;
+}
+
+export interface UserRegistrationGrowthFilters {
+ group_by?: "month" | "year";
+ from?: string; // ISO 8601
+ to?: string; // ISO 8601
+}
+
+export interface UserRegistrationEntry {
+ year: number;
+ month: number;
+ month_name: string;
+ total_users: number;
+ verified_users: number;
+ unverified_users: number;
+}
+
+export interface UserRegistrationSummary {
+ total_users: number;
+ total_verified: number;
+ total_unverified: number;
+ average_per_month: number;
+ growth_rate: number;
+ peak_month: string;
+ peak_month_count: number;
+ verification_rate: number;
+}
+
+export interface UserRegistrationGrowthResponse {
+ registrations: UserRegistrationEntry[];
+ summary: UserRegistrationSummary;
+}
+
+export interface SalesByCategoryFilters {
+ status?: string;
+ from?: string; // ISO 8601
+ to?: string; // ISO 8601
+}
+
+export interface SalesByCategoryEntry {
+ category_id: number;
+ category_name: string;
+ total_sales: number; // تومان
+ total_orders: number;
+ total_products: number;
+ percentage: number;
+ average_per_order: number;
+}
+
+export interface SalesByCategorySummary {
+ total_sales: number;
+ total_orders: number;
+ total_categories: number;
+ top_category: string;
+ top_category_perc: number;
+}
+
+export interface SalesByCategoryResponse {
+ categories: SalesByCategoryEntry[];
+ summary: SalesByCategorySummary;
+}
diff --git a/src/pages/reports/sales-statistics/core/_requests.ts b/src/pages/reports/sales-statistics/core/_requests.ts
new file mode 100644
index 0000000..eae3ad0
--- /dev/null
+++ b/src/pages/reports/sales-statistics/core/_requests.ts
@@ -0,0 +1,56 @@
+import { httpGetRequest, APIUrlGenerator } from "@/utils/baseHttpService";
+import { API_ROUTES } from "@/constant/routes";
+import {
+ SalesGrowthFilters,
+ SalesGrowthResponse,
+ UserRegistrationGrowthFilters,
+ UserRegistrationGrowthResponse,
+ SalesByCategoryFilters,
+ SalesByCategoryResponse,
+} from "./_models";
+
+export const getSalesGrowthReport = async (
+ filters?: SalesGrowthFilters
+): Promise => {
+ const queryParams: Record = {};
+
+ if (filters?.group_by) queryParams.group_by = filters.group_by;
+ if (filters?.status) queryParams.status = filters.status;
+ if (filters?.from) queryParams.from = filters.from;
+ if (filters?.to) queryParams.to = filters.to;
+
+ const response = await httpGetRequest(
+ APIUrlGenerator(API_ROUTES.SALES_GROWTH_REPORT, queryParams)
+ );
+ return response.data;
+};
+
+export const getUserRegistrationGrowthReport = async (
+ filters?: UserRegistrationGrowthFilters
+): Promise => {
+ const queryParams: Record = {};
+
+ if (filters?.group_by) queryParams.group_by = filters.group_by;
+ if (filters?.from) queryParams.from = filters.from;
+ if (filters?.to) queryParams.to = filters.to;
+
+ const response = await httpGetRequest(
+ APIUrlGenerator(API_ROUTES.USER_REGISTRATION_GROWTH_REPORT, queryParams)
+ );
+ return response.data;
+};
+
+export const getSalesByCategoryReport = async (
+ filters?: SalesByCategoryFilters
+): Promise => {
+ const queryParams: Record = {};
+
+ if (filters?.status) queryParams.status = filters.status;
+ if (filters?.from) queryParams.from = filters.from;
+ if (filters?.to) queryParams.to = filters.to;
+
+ const response = await httpGetRequest(
+ APIUrlGenerator(API_ROUTES.SALES_BY_CATEGORY_REPORT, queryParams)
+ );
+ return response.data;
+};
diff --git a/src/pages/tickets/ticket-config/TicketConfigPage.tsx b/src/pages/tickets/ticket-config/TicketConfigPage.tsx
index e5f5c25..e429f4d 100644
--- a/src/pages/tickets/ticket-config/TicketConfigPage.tsx
+++ b/src/pages/tickets/ticket-config/TicketConfigPage.tsx
@@ -32,7 +32,7 @@ const TicketConfigPage = () => {
const [activeTab, setActiveTab] = useState("departments");
const { data: departments } = useTicketDepartments({ activeOnly: true });
- const { data: statuses } = useTicketStatuses({ activeOnly: false });
+ const { data: statuses } = useTicketStatuses();
const { data: subjects } = useTicketSubjects({ activeOnly: false });
const { mutate: createDepartment, isPending: isCreatingDepartment } =
diff --git a/src/pages/tickets/ticket-detail/TicketDetailPage.tsx b/src/pages/tickets/ticket-detail/TicketDetailPage.tsx
index 907e388..e8bf4d5 100644
--- a/src/pages/tickets/ticket-detail/TicketDetailPage.tsx
+++ b/src/pages/tickets/ticket-detail/TicketDetailPage.tsx
@@ -33,7 +33,7 @@ const TicketDetailPage = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const { data: ticket, isLoading, error } = useTicket(id);
- const { data: statuses } = useTicketStatuses({ activeOnly: false });
+ const { data: statuses } = useTicketStatuses({ activeOnly: true });
const { mutate: sendReply, isPending: isReplying } = useReplyTicket();
const { mutate: updateStatus, isPending: isUpdatingStatus } =
useUpdateTicketStatusMutation();
diff --git a/src/utils/query-key.ts b/src/utils/query-key.ts
index 07c3e1b..d3017fa 100644
--- a/src/utils/query-key.ts
+++ b/src/utils/query-key.ts
@@ -139,6 +139,11 @@ export const QUERY_KEYS = {
// Shipment Statistics
GET_SHIPMENTS_BY_METHOD_REPORT: "get_shipments_by_method_report",
+ // Sales Statistics
+ GET_SALES_GROWTH_REPORT: "get_sales_growth_report",
+ GET_USER_REGISTRATION_GROWTH_REPORT: "get_user_registration_growth_report",
+ GET_SALES_BY_CATEGORY_REPORT: "get_sales_by_category_report",
+
// Product Comments
GET_PRODUCT_COMMENTS: "get_product_comments",
UPDATE_COMMENT_STATUS: "update_comment_status",