fix
This commit is contained in:
parent
5b62d189f8
commit
3690a8c1f6
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="card p-3 sm:p-4 lg:p-6">
|
||||
{title && (
|
||||
<CardTitle className="mb-3 sm:mb-4">
|
||||
{title}
|
||||
</CardTitle>
|
||||
)}
|
||||
<ReactApexChart options={options} series={series} type="area" height={250} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<div className="card p-3 sm:p-4 lg:p-6">
|
||||
{title && (
|
||||
<CardTitle className="mb-3 sm:mb-4">
|
||||
{title}
|
||||
</CardTitle>
|
||||
)}
|
||||
<ReactApexChart options={options} series={series} type="bar" height={250} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<div className="card p-3 sm:p-4 lg:p-6">
|
||||
{title && (
|
||||
<CardTitle className="mb-3 sm:mb-4">
|
||||
{title}
|
||||
</CardTitle>
|
||||
)}
|
||||
<div className="w-full">
|
||||
<ResponsiveContainer width="100%" height={250} minHeight={200}>
|
||||
<RechartsAreaChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
|
||||
<defs>
|
||||
<linearGradient id={`areaFill-${color}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={color} stopOpacity={0.5} />
|
||||
<stop offset="95%" stopColor={color} stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="4 4" className="stroke-gray-200 dark:stroke-gray-700" />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
tick={{ fontSize: 11, fontFamily: 'inherit' }}
|
||||
tickFormatter={(value) => englishToPersian(value)}
|
||||
interval="preserveStartEnd"
|
||||
minTickGap={16}
|
||||
height={30}
|
||||
/>
|
||||
<YAxis
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
tick={{ fontSize: 11, fontFamily: 'inherit' }}
|
||||
tickFormatter={(value) => formatNumber(value)}
|
||||
width={72}
|
||||
tickMargin={8}
|
||||
tickCount={4}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'var(--toast-bg)',
|
||||
color: 'var(--toast-color)',
|
||||
border: 'none',
|
||||
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)}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={color}
|
||||
strokeWidth={3}
|
||||
fill={`url(#areaFill-${color})`}
|
||||
dot={false}
|
||||
activeDot={{ r: 4 }}
|
||||
/>
|
||||
</RechartsAreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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) => {
|
|||
)}
|
||||
<div className="w-full">
|
||||
<ResponsiveContainer width="100%" height={250} minHeight={200}>
|
||||
<RechartsBarChart data={data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-300 dark:stroke-gray-600" />
|
||||
<RechartsBarChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
|
||||
<defs>
|
||||
<linearGradient id="barFill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={color} stopOpacity={0.9} />
|
||||
<stop offset="95%" stopColor={color} stopOpacity={0.4} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="4 4" className="stroke-gray-200 dark:stroke-gray-700" />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
tick={{ fontSize: 10 }}
|
||||
interval={0}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
tick={{ fontSize: 11, fontFamily: 'inherit' }}
|
||||
tickFormatter={(value) => englishToPersian(value)}
|
||||
interval="preserveStartEnd"
|
||||
height={40}
|
||||
/>
|
||||
<YAxis
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
tick={{ fontSize: 10 }}
|
||||
width={40}
|
||||
tick={{ fontSize: 11, fontFamily: 'inherit' }}
|
||||
tickFormatter={(value) => formatNumber(value)}
|
||||
width={72}
|
||||
tickMargin={8}
|
||||
tickCount={4}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
|
|
@ -41,9 +57,12 @@ export const BarChart = ({ data, title, color = '#3b82f6' }: BarChartProps) => {
|
|||
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)}
|
||||
/>
|
||||
<Bar dataKey="value" fill={color} radius={[2, 2, 0, 0]} />
|
||||
<Bar dataKey="value" fill="url(#barFill)" radius={[8, 8, 0, 0]} barSize={28} />
|
||||
</RechartsBarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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) =>
|
|||
)}
|
||||
<div className="w-full">
|
||||
<ResponsiveContainer width="100%" height={250} minHeight={200}>
|
||||
<RechartsLineChart data={data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-300 dark:stroke-gray-600" />
|
||||
<RechartsLineChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
|
||||
<CartesianGrid strokeDasharray="4 4" className="stroke-gray-200 dark:stroke-gray-700" />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
tick={{ fontSize: 10 }}
|
||||
interval={0}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
tick={{ fontSize: 11, fontFamily: 'inherit' }}
|
||||
tickFormatter={(value) => englishToPersian(value)}
|
||||
interval="preserveStartEnd"
|
||||
height={40}
|
||||
/>
|
||||
<YAxis
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
tick={{ fontSize: 10 }}
|
||||
width={40}
|
||||
tick={{ fontSize: 11, fontFamily: 'inherit' }}
|
||||
tickFormatter={(value) => formatNumber(value)}
|
||||
width={72}
|
||||
tickMargin={8}
|
||||
tickCount={4}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
|
|
@ -41,14 +51,17 @@ export const LineChart = ({ data, title, color = '#10b981' }: LineChartProps) =>
|
|||
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)}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3, strokeWidth: 2 }}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
activeDot={{ r: 5 }}
|
||||
/>
|
||||
</RechartsLineChart>
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
/>
|
||||
<span className="text-xs sm:text-sm text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
||||
<span className="font-medium">{entry.value}</span>: <span className="font-bold">{Math.round(entry.payload.value)}%</span>
|
||||
<span className="font-medium">{entry.value}</span>: <span className="font-bold">{englishToPersian(Math.round(entry.payload.value))}%</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -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]}
|
||||
/>
|
||||
</RechartsPieChart>
|
||||
</ResponsiveContainer>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
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 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) => (
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${value === 'فعال'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||
}`}>
|
||||
{value}
|
||||
</span>
|
||||
)
|
||||
render: (value: any) => <StatusBadge status={value} type="order" />,
|
||||
},
|
||||
{ key: 'createdAt', label: 'تاریخ عضویت' },
|
||||
{
|
||||
key: 'actions',
|
||||
label: 'عملیات',
|
||||
render: () => (
|
||||
<div className="flex space-x-2">
|
||||
<Button size="sm" variant="secondary">
|
||||
ویرایش
|
||||
</Button>
|
||||
<PermissionWrapper permission={22}>
|
||||
<Button size="sm" variant="danger">
|
||||
حذف
|
||||
</Button>
|
||||
</PermissionWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
key: 'created_at',
|
||||
label: 'تاریخ',
|
||||
render: (value: string) => formatDate(value),
|
||||
},
|
||||
];
|
||||
|
||||
export const Dashboard = () => {
|
||||
const { items: statsData, isLoading: statsLoading, error: statsError } = useDashboardStats();
|
||||
const ordersTableData = (recentOrders?.orders || []).map((item) => item.order ?? item);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{/* Header with mobile-responsive layout */}
|
||||
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
||||
<PageTitle>داشبورد</PageTitle>
|
||||
<div className="flex justify-start gap-3">
|
||||
<button
|
||||
className="flex items-center justify-center w-12 h-12 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-full transition-colors duration-200 text-gray-600 dark:text-gray-300"
|
||||
title="گزارشگیری"
|
||||
>
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
</button>
|
||||
<PermissionWrapper permission={25}>
|
||||
<button
|
||||
className="flex items-center justify-center w-12 h-12 bg-primary-600 hover:bg-primary-700 rounded-full transition-colors duration-200 text-white shadow-lg hover:shadow-xl"
|
||||
title="اضافه کردن"
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</button>
|
||||
</PermissionWrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards - Mobile responsive grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 lg:gap-6">
|
||||
{statsLoading ? (
|
||||
<>
|
||||
{[...Array(4)].map((_, idx) => (
|
||||
<div key={idx} className="card p-6 animate-pulse bg-gray-100 dark:bg-gray-800 h-24" />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
statsData.map((stat, index) => (
|
||||
<StatsCard key={index} {...stat} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{statsError && (
|
||||
<div className="mt-2 text-sm text-red-600 dark:text-red-400">
|
||||
خطا در دریافت آمار سفارشات
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Charts - Better mobile layout */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 sm:gap-6">
|
||||
<div className="min-w-0">
|
||||
<BarChart
|
||||
data={chartData}
|
||||
<ApexBarChartCard
|
||||
data={monthlySalesData}
|
||||
title="فروش ماهانه"
|
||||
color="#3b82f6"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<Suspense fallback={<div className="card p-6 animate-pulse bg-gray-100 dark:bg-gray-800 h-64" />}>
|
||||
<LineChart
|
||||
data={chartData}
|
||||
title="روند رشد"
|
||||
<ApexAreaChartCard
|
||||
data={registrationGrowthData}
|
||||
title="روند رشد ثبتنام کاربران"
|
||||
color="#10b981"
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -170,20 +110,22 @@ export const Dashboard = () => {
|
|||
<div className="xl:col-span-2 min-w-0">
|
||||
<div className="card p-3 sm:p-4 lg:p-6">
|
||||
<CardTitle className="mb-3 sm:mb-4">
|
||||
کاربران اخیر
|
||||
آخرین سفارشات در انتظار
|
||||
</CardTitle>
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
columns={userColumns}
|
||||
data={recentUsers}
|
||||
/>
|
||||
<Table columns={orderColumns} data={ordersTableData} loading={isOrdersLoading} />
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button variant="secondary" onClick={() => navigate('/orders')}>
|
||||
مشاهده همه
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<PieChart
|
||||
data={pieData}
|
||||
title="دستگاههای کاربری"
|
||||
data={salesByCategoryData}
|
||||
title="توزیع فروش بر اساس دستهبندی"
|
||||
colors={['#3b82f6', '#10b981', '#f59e0b']}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ const DiscountCodeFormPage = () => {
|
|||
description: category.description || `دستهبندی #${category.id}`
|
||||
}));
|
||||
|
||||
const { register, handleSubmit, control, formState: { errors, isValid }, reset, watch } = useForm<CreateDiscountCodeRequest>({
|
||||
const { register, handleSubmit, control, formState: { errors, isValid }, reset, watch, setValue } = useForm<CreateDiscountCodeRequest>({
|
||||
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"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<SalesGrowthResponse> => {
|
||||
const queryParams: Record<string, string | number | null> = {};
|
||||
|
||||
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<SalesGrowthResponse>(
|
||||
APIUrlGenerator(API_ROUTES.SALES_GROWTH_REPORT, queryParams)
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getUserRegistrationGrowthReport = async (
|
||||
filters?: UserRegistrationGrowthFilters
|
||||
): Promise<UserRegistrationGrowthResponse> => {
|
||||
const queryParams: Record<string, string | number | null> = {};
|
||||
|
||||
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<UserRegistrationGrowthResponse>(
|
||||
APIUrlGenerator(API_ROUTES.USER_REGISTRATION_GROWTH_REPORT, queryParams)
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getSalesByCategoryReport = async (
|
||||
filters?: SalesByCategoryFilters
|
||||
): Promise<SalesByCategoryResponse> => {
|
||||
const queryParams: Record<string, string | number | null> = {};
|
||||
|
||||
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<SalesByCategoryResponse>(
|
||||
APIUrlGenerator(API_ROUTES.SALES_BY_CATEGORY_REPORT, queryParams)
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
|
@ -32,7 +32,7 @@ const TicketConfigPage = () => {
|
|||
const [activeTab, setActiveTab] = useState<TabKey>("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 } =
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue