-
- وضعیت پرداخت
-
+ وضعیت پرداخت
+
@@ -405,32 +518,31 @@ const OrderDetailPage = () => {
{Array.isArray((data as any)?.payments) && (data as any)?.payments.length > 0 && (
-
-
روش پرداخت
-
{formatPaymentType((data as any).payments[0].payment_type)}
+
+ روش پرداخت
+ {formatPaymentType((data as any).payments[0].payment_type)}
)}
{order?.invoice_id && (
-
-
شماره فاکتور
-
{order.invoice_id}
+
+ شماره فاکتور
+ {order.invoice_id}
)}
{Array.isArray((data as any)?.payments) && (data as any)?.payments[0]?.transaction_id && (
-
-
شناسه تراکنش
-
{(data as any).payments[0].transaction_id}
+
+ شناسه تراکنش
+ {(data as any).payments[0].transaction_id}
)}
{Array.isArray((data as any)?.payments) && (data as any)?.payments[0]?.image_urls?.length > 0 && (
-
-
رسید پرداخت
+
+
رسید پرداخت
)}
-
diff --git a/src/pages/orders/orders-list/OrdersListPage.tsx b/src/pages/orders/orders-list/OrdersListPage.tsx
index 6450923..7b28ce1 100644
--- a/src/pages/orders/orders-list/OrdersListPage.tsx
+++ b/src/pages/orders/orders-list/OrdersListPage.tsx
@@ -1,26 +1,28 @@
import React, { useMemo, useState } from 'react';
-import { englishToPersian } from '@/utils/numberUtils';
+import { englishToPersian, persianToEnglish, formatWithThousands, parseFormattedNumber } from '@/utils/numberUtils';
import { useNavigate } from 'react-router-dom';
import { useOrders, useOrderStats, useUpdateOrderStatus } from '../core/_hooks';
-import { Order, OrderFilters, OrderStatus } from '../core/_models';
+import { OrderFilters, OrderStatus } from '../core/_models';
import { Button } from "@/components/ui/Button";
-import { Input } from "@/components/ui/Input";
import { Modal } from "@/components/ui/Modal";
import { Pagination } from "@/components/ui/Pagination";
-import { PageContainer, PageTitle, SectionTitle } from "@/components/ui/Typography";
+import { PageContainer, PageTitle } from "@/components/ui/Typography";
import { Table } from "@/components/ui/Table";
import { TableColumn } from "@/types";
+import { StatsCard } from '@/components/dashboard/StatsCard';
+import DatePicker from 'react-multi-date-picker';
+import persian from 'react-date-object/calendars/persian';
+import persian_fa from 'react-date-object/locales/persian_fa';
+import DateObject from 'react-date-object';
import {
ShoppingCart,
- Package,
DollarSign,
Clock,
Search,
Filter,
Eye,
Edit3,
- TrendingUp,
- Calendar
+ TrendingUp
} from 'lucide-react';
const getStatusColor = (status: OrderStatus) => {
@@ -59,26 +61,145 @@ const ListSkeleton = () => (
);
+const getDefaultFilters = (): OrderFilters => ({
+ page: 1,
+ limit: 20,
+ status: 'pending',
+ payment_status: undefined,
+ search: '',
+ user_id: undefined,
+ invoice_id: undefined,
+ discount_code: undefined,
+ created_from: undefined,
+ created_to: undefined,
+ updated_from: undefined,
+ updated_to: undefined,
+ min_total: undefined,
+ max_total: undefined,
+});
+
+const toIsoDate = (date?: DateObject | null) => {
+ if (!date) return undefined;
+ try {
+ const g = date.convert(undefined);
+ const yyyy = g.year.toString().padStart(4, '0');
+ const mm = g.month.toString().padStart(2, '0');
+ const dd = g.day.toString().padStart(2, '0');
+ return `${yyyy}-${mm}-${dd}`;
+ } catch {
+ return undefined;
+ }
+};
+
+const fromIsoDate = (value?: string) => {
+ if (!value) return undefined;
+ try {
+ const d = new Date(value);
+ if (isNaN(d.getTime())) return undefined;
+ return new DateObject(d).convert(persian, persian_fa);
+ } catch {
+ return undefined;
+ }
+};
+
+const buildRangeValue = (from?: string, to?: string) => {
+ const start = fromIsoDate(from);
+ const end = fromIsoDate(to);
+ if (start && end) return [start, end];
+ if (start) return [start];
+ if (end) return [end];
+ return [];
+};
+
const OrdersListPage = () => {
const navigate = useNavigate();
const [statusUpdateId, setStatusUpdateId] = useState
(null);
const [newStatus, setNewStatus] = useState('processing');
- const [filters, setFilters] = useState({
- page: 1,
- limit: 20,
- order_number: '',
- status: 'pending',
- payment_status: undefined,
- search: '',
- });
+ const [filters, setFilters] = useState(getDefaultFilters());
const { data: ordersData, isLoading, error } = useOrders(filters);
- // Temporarily disabled stats API
- // const { data: stats, isLoading: statsLoading } = useOrderStats(!isLoading);
- const stats = null;
- const statsLoading = false;
+ const { data: stats, isLoading: statsLoading, error: statsError } = useOrderStats(true);
const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus();
+ const handleIdFilterChange = (key: keyof OrderFilters, raw: string) => {
+ const converted = persianToEnglish(raw).replace(/[^\d]/g, '');
+ const numeric = converted ? Number(converted) : undefined;
+ setFilters(prev => ({
+ ...prev,
+ [key]: numeric,
+ page: 1,
+ }));
+ };
+
+ const handleTextFilterChange = (key: keyof OrderFilters, value: string) => {
+ setFilters(prev => ({
+ ...prev,
+ [key]: value || undefined,
+ page: 1,
+ }));
+ };
+
+ const formatNumberDisplay = (val?: number) => {
+ if (val === undefined || val === null || Number.isNaN(val)) return '';
+ return formatWithThousands(val);
+ };
+
+ const handleAmountFilterChange = (key: keyof OrderFilters, raw: string) => {
+ const converted = persianToEnglish(raw);
+ const numeric = parseFormattedNumber(converted);
+ setFilters(prev => ({
+ ...prev,
+ [key]: numeric,
+ page: 1,
+ }));
+ };
+
+ const handleDateRangeChange = (startKey: keyof OrderFilters, endKey: keyof OrderFilters, range: (DateObject | null)[] | DateObject | null) => {
+ if (Array.isArray(range)) {
+ const [start, end] = range;
+ setFilters(prev => ({
+ ...prev,
+ [startKey]: toIsoDate(start),
+ [endKey]: toIsoDate(end),
+ page: 1,
+ }));
+ return;
+ }
+ setFilters(prev => ({
+ ...prev,
+ [startKey]: toIsoDate(range as DateObject | null),
+ [endKey]: undefined,
+ page: 1,
+ }));
+ };
+
+ const statsItems = useMemo(() => ([
+ {
+ title: 'کل سفارشات',
+ value: stats?.total_orders_count ?? 0,
+ icon: ShoppingCart,
+ color: 'yellow' as const,
+ },
+ {
+ title: 'مجموع فروش',
+ value: stats?.total_amount_of_sale ?? 0,
+ icon: DollarSign,
+ color: 'green' as const,
+ },
+ {
+ title: 'سفارشهای در انتظار',
+ value: stats?.total_order_pending ?? 0,
+ icon: Clock,
+ color: 'blue' as const,
+ },
+ {
+ title: 'میانگین سفارش',
+ value: stats?.order_avg ?? 0,
+ icon: TrendingUp,
+ color: 'purple' as const,
+ },
+ ]), [stats]);
+
const columns: TableColumn[] = useMemo(() => [
{ key: 'order_number', label: 'شماره سفارش', sortable: true, align: 'right', render: (v: string) => `#${v}` },
@@ -171,64 +292,24 @@ const OrdersListPage = () => {
-
-
-
-
-
-
-
-
-
-
-
-
-
میانگین سفارش
-
- --
-
-
-
-
+
+ {statsLoading ? (
+ <>
+ {[...Array(4)].map((_, idx) => (
+
+ ))}
+ >
+ ) : (
+ statsItems.map((stat, index) => (
+
+ ))
+ )}
+ {statsError && (
+
+ خطا در دریافت آمار سفارشات
+
+ )}
{/* فیلترها */}
@@ -237,7 +318,7 @@ const OrdersListPage = () => {
setFilters(prev => ({ ...prev, search: e.target.value, page: 1 }))}
className="w-full pr-10 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-300"
@@ -245,6 +326,42 @@ const OrdersListPage = () => {
+
+ handleIdFilterChange('user_id', e.target.value)}
+ placeholder="مثلا 1024"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
+ />
+
+
+
+
+ handleIdFilterChange('invoice_id', e.target.value)}
+ placeholder="invoice_id"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
+ />
+
+
+
+
+ handleTextFilterChange('discount_code', e.target.value)}
+ placeholder="مثلا SPRING2025"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
+ />
+
+
+
+
+
+
+
+ handleAmountFilterChange('min_total', e.target.value)}
+ placeholder="مثلا 3000000"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
+ />
+
+
+
+
+ handleAmountFilterChange('max_total', e.target.value)}
+ placeholder="مثلا 9000000"
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
+ />
+
+
+
+
+ handleDateRangeChange('created_from', 'created_to', range as any)}
+ format="YYYY/MM/DD"
+ range
+ calendar={persian}
+ locale={persian_fa}
+ calendarPosition="bottom-center"
+ inputClass="w-full px-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500"
+ containerClassName="w-full"
+ editable={false}
+ placeholder="از تاریخ / تا تاریخ"
+ />
+
+
+
+
+ handleDateRangeChange('updated_from', 'updated_to', range as any)}
+ format="YYYY/MM/DD"
+ range
+ calendar={persian}
+ locale={persian_fa}
+ calendarPosition="bottom-center"
+ inputClass="w-full px-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500"
+ containerClassName="w-full"
+ editable={false}
+ placeholder="از تاریخ / تا تاریخ"
+ />
+
+
- تنها تصاویر مربعی پذیرفته میشوند و میتوانید ویدیو نیز اضافه کنید.
-