528 lines
26 KiB
TypeScript
528 lines
26 KiB
TypeScript
import React, { useMemo, useState } from 'react';
|
||
import { englishToPersian, persianToEnglish, formatWithThousands, parseFormattedNumber } from '@/utils/numberUtils';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useOrders, useOrderStats, useUpdateOrderStatus } from '../core/_hooks';
|
||
import { OrderFilters, OrderStatus } from '../core/_models';
|
||
import { Button } from "@/components/ui/Button";
|
||
import { Modal } from "@/components/ui/Modal";
|
||
import { Pagination } from "@/components/ui/Pagination";
|
||
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,
|
||
DollarSign,
|
||
Clock,
|
||
Search,
|
||
Filter,
|
||
Eye,
|
||
Edit3,
|
||
TrendingUp
|
||
} from 'lucide-react';
|
||
|
||
const getStatusColor = (status: OrderStatus) => {
|
||
const colors = {
|
||
pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||
processing: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||
shipped: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
||
delivered: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||
cancelled: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||
refunded: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
||
};
|
||
return colors[status] || colors.pending;
|
||
};
|
||
|
||
const getStatusText = (status: OrderStatus) => {
|
||
const text = {
|
||
pending: 'در انتظار',
|
||
processing: 'در حال پردازش',
|
||
shipped: 'ارسال شده',
|
||
delivered: 'تحویل شده',
|
||
cancelled: 'لغو شده',
|
||
refunded: 'مرجوع شده',
|
||
};
|
||
return text[status] || status;
|
||
};
|
||
|
||
const formatCurrency = (amount: number) => {
|
||
return new Intl.NumberFormat('fa-IR').format(amount) + ' تومان';
|
||
};
|
||
|
||
const formatDate = (dateString: string) => {
|
||
return new Date(dateString).toLocaleDateString('fa-IR');
|
||
};
|
||
|
||
const ListSkeleton = () => (
|
||
<Table columns={[]} data={[]} loading={true} />
|
||
);
|
||
|
||
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<string | null>(null);
|
||
const [newStatus, setNewStatus] = useState<OrderStatus>('processing');
|
||
const [filters, setFilters] = useState<OrderFilters>(getDefaultFilters());
|
||
|
||
const { data: ordersData, isLoading, error } = useOrders(filters);
|
||
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}` },
|
||
{
|
||
key: 'customer',
|
||
label: 'مشتری',
|
||
align: 'right',
|
||
render: (_val, row: any) => (
|
||
<div className="text-right">
|
||
<div className="font-medium">
|
||
{(row.user?.first_name || row.customer?.first_name || 'نامشخص')} {(row.user?.last_name || row.customer?.last_name || '')}
|
||
</div>
|
||
<div className="text-gray-500 dark:text-gray-400" dir="ltr" style={{ direction: 'ltr' }}>
|
||
{row.user?.phone_number ? englishToPersian(row.user.phone_number) : '-'}
|
||
</div>
|
||
</div>
|
||
)
|
||
},
|
||
{ key: 'final_total', label: 'مبلغ نهایی', sortable: true, align: 'right', render: (v: number, row: any) => formatCurrency(row.final_total || row.total_amount || 0) },
|
||
{ key: 'status', label: 'وضعیت', align: 'right', render: (v: OrderStatus) => (<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(v)}`}>{getStatusText(v)}</span>) },
|
||
{ key: 'created_at', label: 'تاریخ', sortable: true, align: 'right', render: (v: string) => formatDate(v) },
|
||
{
|
||
key: 'actions',
|
||
label: 'عملیات',
|
||
align: 'right',
|
||
render: (_val, row: any) => (
|
||
<div className="flex items-center gap-2 justify-end">
|
||
<button
|
||
onClick={() => handleViewOrder(row.id)}
|
||
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||
title="مشاهده جزئیات"
|
||
>
|
||
<Eye className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleUpdateStatus(row.id, row.status)}
|
||
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
|
||
title="تغییر وضعیت"
|
||
>
|
||
<Edit3 className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
)
|
||
},
|
||
], []);
|
||
|
||
const handleStatusUpdate = () => {
|
||
if (statusUpdateId) {
|
||
updateStatus(
|
||
{ id: statusUpdateId, payload: { status: newStatus } },
|
||
{ onSuccess: () => setStatusUpdateId(null) }
|
||
);
|
||
}
|
||
};
|
||
|
||
const handleViewOrder = (id: number) => {
|
||
navigate(`/orders/${id}`);
|
||
};
|
||
|
||
const handleUpdateStatus = (id: number, currentStatus: OrderStatus) => {
|
||
setStatusUpdateId(id.toString());
|
||
setNewStatus(currentStatus);
|
||
};
|
||
|
||
const handlePageChange = (page: number) => {
|
||
setFilters(prev => ({ ...prev, page }));
|
||
};
|
||
|
||
if (error) {
|
||
return (
|
||
<PageContainer>
|
||
<div className="text-center py-12">
|
||
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری سفارشات</p>
|
||
</div>
|
||
</PageContainer>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<PageContainer>
|
||
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
||
<div>
|
||
<PageTitle className="flex items-center gap-2">
|
||
<ShoppingCart className="h-6 w-6" />
|
||
مدیریت سفارشات
|
||
</PageTitle>
|
||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||
{ordersData?.total || 0} سفارش یافت شد
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<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" />
|
||
))}
|
||
</>
|
||
) : (
|
||
statsItems.map((stat, index) => (
|
||
<StatsCard key={index} {...stat} />
|
||
))
|
||
)}
|
||
</div>
|
||
{statsError && (
|
||
<div className="mt-2 text-sm text-red-600 dark:text-red-400">
|
||
خطا در دریافت آمار سفارشات
|
||
</div>
|
||
)}
|
||
|
||
{/* فیلترها */}
|
||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-6">
|
||
<div className="space-y-4">
|
||
<div className="relative">
|
||
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-gray-300" />
|
||
<input
|
||
type="text"
|
||
placeholder="جستجو عمومی (شماره سفارش، کد تراکنش، نام، تلفن، کالا، کد تخفیف)"
|
||
value={filters.search || ''}
|
||
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value, page: 1 }))}
|
||
className="w-full pr-10 px-3 py-2.5 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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">وضعیت سفارش</label>
|
||
<select
|
||
value={filters.status || ''}
|
||
onChange={(e) => setFilters(prev => ({ ...prev, status: e.target.value as OrderStatus || undefined, page: 1 }))}
|
||
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
||
>
|
||
<option value="">همه وضعیتها</option>
|
||
<option value="pending">در انتظار</option>
|
||
<option value="processing">در حال پردازش</option>
|
||
<option value="shipped">ارسال شده</option>
|
||
<option value="delivered">تحویل شده</option>
|
||
<option value="cancelled">لغو شده</option>
|
||
<option value="refunded">مرجوع شده</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">شناسه کاربر</label>
|
||
<input
|
||
type="text"
|
||
inputMode="numeric"
|
||
value={filters.user_id ? String(filters.user_id) : ''}
|
||
onChange={(e) => handleIdFilterChange('user_id', e.target.value)}
|
||
placeholder="مثلاً ۱۰۲۴"
|
||
className="w-full px-3 py-2.5 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-400"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">وضعیت پرداخت</label>
|
||
<select
|
||
value={filters.payment_status || ''}
|
||
onChange={(e) => setFilters(prev => ({ ...prev, payment_status: e.target.value as any || undefined, page: 1 }))}
|
||
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
||
>
|
||
<option value="">همه وضعیتهای پرداخت</option>
|
||
<option value="pending">در انتظار پرداخت</option>
|
||
<option value="paid">پرداخت شده</option>
|
||
<option value="failed">پرداخت ناموفق</option>
|
||
<option value="refunded">بازپرداخت شده</option>
|
||
<option value="cancelled">لغو شده</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">کد تخفیف</label>
|
||
<input
|
||
type="text"
|
||
value={filters.discount_code ?? ''}
|
||
onChange={(e) => handleTextFilterChange('discount_code', e.target.value)}
|
||
placeholder="مثلاً SPRING2025"
|
||
className="w-full px-3 py-2.5 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-400"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">شناسه فاکتور</label>
|
||
<input
|
||
type="text"
|
||
inputMode="numeric"
|
||
value={filters.invoice_id ? String(filters.invoice_id) : ''}
|
||
onChange={(e) => handleIdFilterChange('invoice_id', e.target.value)}
|
||
placeholder="مثلاً ۱۲۳۴۵"
|
||
className="w-full px-3 py-2.5 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-400"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">حداقل مبلغ</label>
|
||
<input
|
||
type="text"
|
||
inputMode="numeric"
|
||
value={formatNumberDisplay(filters.min_total)}
|
||
onChange={(e) => handleAmountFilterChange('min_total', e.target.value)}
|
||
placeholder="مثلاً ۳,۰۰۰,۰۰۰"
|
||
className="w-full px-3 py-2.5 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-400"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">حداکثر مبلغ</label>
|
||
<input
|
||
type="text"
|
||
inputMode="numeric"
|
||
value={formatNumberDisplay(filters.max_total)}
|
||
onChange={(e) => handleAmountFilterChange('max_total', e.target.value)}
|
||
placeholder="مثلاً ۹,۰۰۰,۰۰۰"
|
||
className="w-full px-3 py-2.5 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-400"
|
||
/>
|
||
</div>
|
||
|
||
<div className="lg:col-span-2">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">بازه تاریخ ایجاد</label>
|
||
<DatePicker
|
||
value={buildRangeValue(filters.created_from, filters.created_to)}
|
||
onChange={(range) => 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-2.5 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="از تاریخ / تا تاریخ"
|
||
/>
|
||
</div>
|
||
|
||
<div className="lg:col-span-2">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">بازه تاریخ بروزرسانی</label>
|
||
<DatePicker
|
||
value={buildRangeValue(filters.updated_from, filters.updated_to)}
|
||
onChange={(range) => 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-2.5 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="از تاریخ / تا تاریخ"
|
||
/>
|
||
</div>
|
||
|
||
<div className="md:col-span-2 lg:col-span-4 flex justify-end">
|
||
<Button
|
||
variant="secondary"
|
||
onClick={() => setFilters(getDefaultFilters())}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<Filter className="h-4 w-4" />
|
||
پاک کردن فیلترها
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* جدول سفارشات */}
|
||
{isLoading ? (
|
||
<ListSkeleton />
|
||
) : !ordersData?.orders || ordersData.orders.length === 0 ? (
|
||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
||
<div className="text-center py-12">
|
||
<ShoppingCart className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">هیچ سفارشی یافت نشد</h3>
|
||
<p className="text-gray-600 dark:text-gray-400">با تغییر فیلترها جستجو کنید</p>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<Table columns={columns} data={ordersData.orders as any[]} />
|
||
<Pagination
|
||
currentPage={filters.page || 1}
|
||
totalPages={Math.ceil((ordersData.total || 0) / (filters.limit || 20))}
|
||
onPageChange={handlePageChange}
|
||
itemsPerPage={filters.limit || 20}
|
||
totalItems={ordersData.total || 0}
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
{/* مودال تغییر وضعیت */}
|
||
<Modal isOpen={!!statusUpdateId} onClose={() => setStatusUpdateId(null)} title="تغییر وضعیت سفارش">
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
وضعیت جدید
|
||
</label>
|
||
<select
|
||
value={newStatus}
|
||
onChange={(e) => setNewStatus(e.target.value as OrderStatus)}
|
||
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
||
>
|
||
<option value="pending">در انتظار</option>
|
||
<option value="processing">در حال پردازش</option>
|
||
<option value="shipped">ارسال شده</option>
|
||
<option value="delivered">تحویل شده</option>
|
||
<option value="cancelled">لغو شده</option>
|
||
<option value="refunded">مرجوع شده</option>
|
||
</select>
|
||
</div>
|
||
<div className="flex justify-end space-x-2 space-x-reverse">
|
||
<Button variant="secondary" onClick={() => setStatusUpdateId(null)} disabled={isUpdating}>
|
||
انصراف
|
||
</Button>
|
||
<Button variant="primary" onClick={handleStatusUpdate} loading={isUpdating}>
|
||
بهروزرسانی
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
</PageContainer>
|
||
);
|
||
};
|
||
|
||
export default OrdersListPage;
|