feat(orders): implement orders pages, update routes, and add order-related API constants

This commit is contained in:
hossein taromi 2025-08-30 17:38:19 +03:30
parent d8b6f2a54f
commit d216a886d0
12 changed files with 1077 additions and 210 deletions

View File

@ -15,7 +15,6 @@ import { Layout } from './components/layout/Layout';
const Login = lazy(() => import('./pages/Login').then(module => ({ default: module.Login })));
const Dashboard = lazy(() => import('./pages/Dashboard').then(module => ({ default: module.Dashboard })));
const Users = lazy(() => import('./pages/Users').then(module => ({ default: module.Users })));
const Orders = lazy(() => import('./pages/Orders').then(module => ({ default: module.Orders })));
const Reports = lazy(() => import('./pages/Reports').then(module => ({ default: module.Reports })));
const Notifications = lazy(() => import('./pages/Notifications').then(module => ({ default: module.Notifications })));
@ -47,6 +46,10 @@ const CategoryFormPage = lazy(() => import('./pages/categories/category-form/Cat
const DiscountCodesListPage = lazy(() => import('./pages/discount-codes/discount-codes-list/DiscountCodesListPage'));
const DiscountCodeFormPage = lazy(() => import('./pages/discount-codes/discount-code-form/DiscountCodeFormPage'));
// Orders Pages
const OrdersListPage = lazy(() => import('./pages/orders/orders-list/OrdersListPage'));
const OrderDetailPage = lazy(() => import('./pages/orders/order-detail/OrderDetailPage'));
// Products Pages
const ProductsListPage = lazy(() => import('./pages/products/products-list/ProductsListPage'));
const ProductFormPage = lazy(() => import('./pages/products/product-form/ProductFormPage'));
@ -81,7 +84,6 @@ const AppRoutes = () => {
<Route index element={<Dashboard />} />
<Route path="users" element={<Users />} />
<Route path="products" element={<ProductsListPage />} />
<Route path="orders" element={<Orders />} />
<Route path="reports" element={<Reports />} />
<Route path="notifications" element={<Notifications />} />
@ -118,6 +120,10 @@ const AppRoutes = () => {
<Route path="discount-codes/create" element={<DiscountCodeFormPage />} />
<Route path="discount-codes/:id/edit" element={<DiscountCodeFormPage />} />
{/* Orders Routes */}
<Route path="orders" element={<OrdersListPage />} />
<Route path="orders/:id" element={<OrderDetailPage />} />
{/* Landing Hero Route */}
<Route path="landing-hero" element={<HeroSliderPage />} />

View File

@ -13,6 +13,7 @@ import {
FolderOpen,
Sliders,
BadgePercent,
ShoppingCart,
X
} from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
@ -33,6 +34,11 @@ const menuItems: MenuItem[] = [
icon: Home,
path: '/',
},
{
title: 'سفارشات',
icon: ShoppingCart,
path: '/orders',
},
{
title: 'مدیریت محصولات',
icon: Package,

View File

@ -87,4 +87,9 @@ export const API_ROUTES = {
CREATE_DISCOUNT_CODE: "api/v1/admin/discount/",
UPDATE_DISCOUNT_CODE: (id: string) => `api/v1/admin/discount/${id}/`,
DELETE_DISCOUNT_CODE: (id: string) => `api/v1/admin/discount/${id}/`,
// Orders APIs
GET_ORDERS: "checkout/orders",
GET_ORDER: (id: string) => `checkout/orders/${id}`,
UPDATE_ORDER_STATUS: (id: string) => `checkout/orders/${id}/status`,
};

View File

@ -1,192 +0,0 @@
import { useState } from 'react';
import { Plus, Search, Filter, Package, ShoppingCart, DollarSign, Clock } from 'lucide-react';
import { Table } from '../components/ui/Table';
import { Button } from '../components/ui/Button';
import { Pagination } from '../components/ui/Pagination';
import { PermissionWrapper } from '../components/common/PermissionWrapper';
import { TableColumn } from '../types';
import { PageContainer, PageTitle, StatValue } from '../components/ui/Typography';
const allOrders = [
{ id: 1001, customer: 'علی احمدی', products: '۳ محصول', amount: '۴۵,۰۰۰,۰۰۰', status: 'تحویل شده', date: '۱۴۰۲/۰۸/۱۵' },
{ id: 1002, customer: 'فاطمه حسینی', products: '۱ محصول', amount: '۲۵,۰۰۰,۰۰۰', status: 'در حال پردازش', date: '۱۴۰۲/۰۸/۱۴' },
{ id: 1003, customer: 'محمد رضایی', products: '۲ محصول', amount: '۳۲,۰۰۰,۰۰۰', status: 'ارسال شده', date: '۱۴۰۲/۰۸/۱۳' },
{ id: 1004, customer: 'زهرا کریمی', products: '۵ محصول', amount: '۱۲۰,۰۰۰,۰۰۰', status: 'تحویل شده', date: '۱۴۰۲/۰۸/۱۲' },
{ id: 1005, customer: 'حسن نوری', products: '۱ محصول', amount: '۱۸,۰۰۰,۰۰۰', status: 'لغو شده', date: '۱۴۰۲/۰۸/۱۱' },
{ id: 1006, customer: 'مریم صادقی', products: '۴ محصول', amount: '۸۵,۰۰۰,۰۰۰', status: 'در حال پردازش', date: '۱۴۰۲/۰۸/۱۰' },
{ id: 1007, customer: 'احمد قاسمی', products: '۲ محصول', amount: '۳۸,۰۰۰,۰۰۰', status: 'ارسال شده', date: '۱۴۰۲/۰۸/۰۹' },
{ id: 1008, customer: 'سارا محمدی', products: '۳ محصول', amount: '۶۲,۰۰۰,۰۰۰', status: 'تحویل شده', date: '۱۴۰۲/۰۸/۰۸' },
{ id: 1009, customer: 'رضا کریمی', products: '۱ محصول', amount: '۱۵,۰۰۰,۰۰۰', status: 'در حال پردازش', date: '۱۴۰۲/۰۸/۰۷' },
{ id: 1010, customer: 'نرگس احمدی', products: '۶ محصول', amount: '۱۴۵,۰۰۰,۰۰۰', status: 'تحویل شده', date: '۱۴۰۲/۰۸/۰۶' },
];
export const Orders = () => {
const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 6;
const columns: TableColumn[] = [
{ key: 'id', label: 'شماره سفارش', sortable: true },
{ key: 'customer', label: 'مشتری', sortable: true },
{ key: 'products', label: 'محصولات' },
{
key: 'amount',
label: 'مبلغ',
render: (value) => (
<span className="font-medium text-gray-900 dark:text-gray-100">
{value} تومان
</span>
)
},
{
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'
: value === 'ارسال شده'
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
: value === 'در حال پردازش'
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
}`}>
{value}
</span>
)
},
{ key: 'date', label: 'تاریخ سفارش', sortable: true },
{
key: 'actions',
label: 'عملیات',
render: (_, row) => (
<div className="flex space-x-2">
<Button
size="sm"
variant="secondary"
onClick={() => handleViewOrder(row)}
>
مشاهده
</Button>
<Button
size="sm"
variant="primary"
onClick={() => handleEditOrder(row)}
>
ویرایش
</Button>
</div>
)
}
];
const filteredOrders = allOrders.filter((order: any) =>
order.customer.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.id.toString().includes(searchTerm)
);
const totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const paginatedOrders = filteredOrders.slice(startIndex, startIndex + itemsPerPage);
const handleViewOrder = (order: any) => {
console.log('Viewing order:', order);
};
const handleEditOrder = (order: any) => {
console.log('Editing order:', order);
};
const totalRevenue = allOrders.reduce((sum, order) => {
const amount = parseInt(order.amount.replace(/[,]/g, ''));
return sum + amount;
}, 0);
return (
<PageContainer>
<PageTitle>مدیریت سفارشات</PageTitle>
<p className="text-gray-600 dark:text-gray-400 mt-1">
{filteredOrders.length} سفارش یافت شد
</p>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
<div className="flex items-center">
<ShoppingCart className="h-8 w-8 text-blue-600" />
<div className="mr-3">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">کل سفارشات</p>
<StatValue>{allOrders.length}</StatValue>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
<div className="flex items-center">
<Package className="h-8 w-8 text-green-600" />
<div className="mr-3">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">تحویل شده</p>
<StatValue>
{allOrders.filter(o => o.status === 'تحویل شده').length}
</StatValue>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
<div className="flex items-center">
<ShoppingCart className="h-8 w-8 text-yellow-600" />
<div className="mr-3">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">در انتظار</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{allOrders.filter(o => o.status === 'در حال پردازش').length}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow">
<div className="flex items-center">
<DollarSign className="h-8 w-8 text-purple-600" />
<div className="mr-3">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">کل فروش</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{totalRevenue.toLocaleString()} تومان
</p>
</div>
</div>
</div>
</div>
<div className="card p-6">
<div className="mb-6">
<div className="relative">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Search className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
placeholder="جستجو در سفارشات..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="input pr-10 max-w-md"
/>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg overflow-hidden">
<Table
columns={columns}
data={paginatedOrders}
loading={false}
/>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
itemsPerPage={itemsPerPage}
totalItems={filteredOrders.length}
/>
</div>
</div>
</PageContainer>
);
};

View File

@ -1,16 +1,22 @@
export type DiscountCodeType = "percentage" | "fixed";
export type DiscountCodeType = "percentage" | "fixed" | "fee_percentage";
export type DiscountApplicationLevel =
| "invoice"
| "category"
| "product"
| "shipping";
| "shipping"
| "product_fee";
export type DiscountStatus = "active" | "inactive";
export type UserGroup = "new" | "loyal" | "all";
export interface DiscountUserRestrictions {
user_ids?: number[];
user_group?: string;
user_group?: UserGroup;
min_purchase_count?: number;
max_purchase_count?: number;
referrer_user_id?: number;
new_users_only?: boolean;
loyal_users_only?: boolean;
}
@ -19,6 +25,15 @@ export interface DiscountMeta {
[key: string]: string | number | boolean | null;
}
export interface SteppedDiscountStep {
min_amount: number;
value: number;
}
export interface SteppedDiscount {
steps: SteppedDiscountStep[];
}
export interface DiscountCode {
id: number;
code: string;
@ -32,10 +47,11 @@ export interface DiscountCode {
max_discount_amount?: number;
usage_limit?: number;
user_usage_limit?: number;
single_use?: boolean;
single_use: boolean;
valid_from?: string;
valid_to?: string;
user_restrictions?: DiscountUserRestrictions;
stepped_discount?: SteppedDiscount;
meta?: DiscountMeta;
created_at?: string;
updated_at?: string;
@ -63,10 +79,11 @@ export interface CreateDiscountCodeRequest {
max_discount_amount?: number;
usage_limit?: number;
user_usage_limit?: number;
single_use?: boolean;
single_use: boolean;
valid_from?: string;
valid_to?: string;
user_restrictions?: DiscountUserRestrictions;
stepped_discount?: SteppedDiscount;
meta?: DiscountMeta;
}

View File

@ -12,18 +12,18 @@ import { FormHeader, PageContainer, Label, SectionTitle } from '../../../compone
import { ArrowRight, BadgePercent, Calendar, Settings, Users, Tag, Info } from 'lucide-react';
const schema = yup.object({
code: yup.string().required('کد الزامی است'),
name: yup.string().required('نام الزامی است'),
description: yup.string().nullable(),
type: yup.mixed<'percentage' | 'fixed'>().oneOf(['percentage', 'fixed']).required('نوع الزامی است'),
value: yup.number().typeError('مقدار نامعتبر است').required('مقدار الزامی است').min(0),
code: yup.string().min(3, 'کد باید حداقل ۳ کاراکتر باشد').max(50, 'کد نباید بیشتر از ۵۰ کاراکتر باشد').required('کد الزامی است'),
name: yup.string().min(1, 'نام الزامی است').max(100, 'نام نباید بیشتر از ۱۰۰ کاراکتر باشد').required('نام الزامی است'),
description: yup.string().max(500, 'توضیحات نباید بیشتر از ۵۰۰ کاراکتر باشد').nullable(),
type: yup.mixed<'percentage' | 'fixed' | 'fee_percentage'>().oneOf(['percentage', 'fixed', 'fee_percentage']).required('نوع الزامی است'),
value: yup.number().typeError('مقدار نامعتبر است').required('مقدار الزامی است').min(0.01, 'مقدار باید بیشتر از صفر باشد'),
status: yup.mixed<'active' | 'inactive'>().oneOf(['active', 'inactive']).required('وضعیت الزامی است'),
application_level: yup.mixed<'invoice' | 'category' | 'product' | 'shipping'>().oneOf(['invoice', 'category', 'product', 'shipping']).required('سطح اعمال الزامی است'),
min_purchase_amount: yup.number().transform((v, o) => o === '' ? undefined : v).nullable(),
max_discount_amount: yup.number().transform((v, o) => o === '' ? undefined : v).nullable(),
usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).nullable(),
user_usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).nullable(),
single_use: yup.boolean().default(false),
application_level: yup.mixed<'invoice' | 'category' | 'product' | 'shipping' | 'product_fee'>().oneOf(['invoice', 'category', 'product', 'shipping', 'product_fee']).required('سطح اعمال الزامی است'),
min_purchase_amount: yup.number().transform((v, o) => o === '' ? undefined : v).min(0.01, 'مبلغ باید بیشتر از صفر باشد').nullable(),
max_discount_amount: yup.number().transform((v, o) => o === '' ? undefined : v).min(0.01, 'مبلغ باید بیشتر از صفر باشد').nullable(),
usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).min(1, 'حداقل ۱ بار استفاده').nullable(),
user_usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).min(1, 'حداقل ۱ بار استفاده').nullable(),
single_use: yup.boolean().required('این فیلد الزامی است'),
valid_from: yup.string().nullable(),
valid_to: yup.string().nullable(),
});
@ -166,6 +166,7 @@ const DiscountCodeFormPage = () => {
<select className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100 transition-colors" {...register('type')}>
<option value="percentage">درصدی</option>
<option value="fixed">مبلغ ثابت</option>
<option value="fee_percentage">درصد کارمزد</option>
</select>
{errors.type && <p className="text-sm text-red-600 dark:text-red-400">{errors.type.message as string}</p>}
</div>
@ -192,6 +193,7 @@ const DiscountCodeFormPage = () => {
<option value="category">دستهبندی خاص</option>
<option value="product">محصول خاص</option>
<option value="shipping">هزینه ارسال</option>
<option value="product_fee">کارمزد محصول</option>
</select>
{errors.application_level && <p className="text-sm text-red-600 dark:text-red-400">{errors.application_level.message as string}</p>}
</div>

View File

@ -0,0 +1,58 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { QUERY_KEYS } from "@/utils/query-key";
import toast from "react-hot-toast";
import {
getOrders,
getOrder,
updateOrderStatus,
getOrderStats,
} from "./_requests";
import { OrderFilters, UpdateOrderStatusRequest } from "./_models";
export const useOrders = (filters?: OrderFilters) => {
return useQuery({
queryKey: [QUERY_KEYS.GET_ORDERS, filters],
queryFn: () => getOrders(filters),
});
};
export const useOrder = (id: string) => {
return useQuery({
queryKey: [QUERY_KEYS.GET_ORDER, id],
queryFn: () => getOrder(id),
enabled: !!id,
});
};
export const useOrderStats = () => {
return useQuery({
queryKey: [QUERY_KEYS.GET_ORDERS, "stats"],
queryFn: getOrderStats,
});
};
export const useUpdateOrderStatus = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
id,
payload,
}: {
id: string;
payload: UpdateOrderStatusRequest;
}) => updateOrderStatus(id, payload),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_ORDERS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_ORDER, variables.id],
});
toast.success("وضعیت سفارش با موفقیت به‌روزرسانی شد");
},
onError: (error: any) => {
toast.error(error?.message || "خطا در به‌روزرسانی وضعیت سفارش");
},
});
};

View File

@ -0,0 +1,126 @@
export type OrderStatus =
| "pending"
| "processing"
| "shipped"
| "delivered"
| "cancelled"
| "refunded";
export type PaymentStatus = "pending" | "paid" | "failed" | "refunded";
export type PaymentMethod =
| "credit_card"
| "debit_card"
| "bank_transfer"
| "cash_on_delivery"
| "wallet";
export interface OrderItem {
id: number;
product_id: number;
product_name: string;
product_image?: string;
variant_id?: number;
variant_name?: string;
quantity: number;
unit_price: number;
total_price: number;
discount_amount?: number;
}
export interface OrderAddress {
id: number;
type: "billing" | "shipping";
first_name: string;
last_name: string;
company?: string;
address_line_1: string;
address_line_2?: string;
city: string;
state: string;
postal_code: string;
country: string;
phone?: string;
}
export interface OrderPayment {
id: number;
payment_method: PaymentMethod;
payment_status: PaymentStatus;
amount: number;
transaction_id?: string;
gateway_response?: Record<string, any>;
paid_at?: string;
}
export interface OrderCustomer {
id: number;
first_name: string;
last_name: string;
email: string;
phone?: string;
}
export interface Order {
id: number;
order_number: string;
customer: OrderCustomer;
status: OrderStatus;
items: OrderItem[];
billing_address: OrderAddress;
shipping_address: OrderAddress;
payment: OrderPayment;
subtotal: number;
tax_amount: number;
shipping_amount: number;
discount_amount: number;
total_amount: number;
currency: string;
notes?: string;
tracking_number?: string;
estimated_delivery?: string;
created_at: string;
updated_at: string;
}
export interface OrderFilters {
page?: number;
limit?: number;
offset?: number;
status?: OrderStatus;
payment_status?: PaymentStatus;
customer_id?: number;
order_number?: string;
date_from?: string;
date_to?: string;
min_amount?: number;
max_amount?: number;
}
export interface PaginatedOrdersResponse {
orders: Order[];
total: number;
page: number;
limit: number;
total_pages: number;
}
export interface UpdateOrderStatusRequest {
status: OrderStatus;
notes?: string;
tracking_number?: string;
estimated_delivery?: string;
}
export interface OrderStats {
total_orders: number;
total_revenue: number;
orders_by_status: Record<OrderStatus, number>;
avg_order_value: number;
}
export type Response<T> = {
data: T;
message?: string;
success?: boolean;
};

View File

@ -0,0 +1,84 @@
import {
APIUrlGenerator,
httpGetRequest,
httpPutRequest,
} from "@/utils/baseHttpService";
import { API_ROUTES } from "@/constant/routes";
import {
Order,
OrderFilters,
PaginatedOrdersResponse,
UpdateOrderStatusRequest,
OrderStats,
} from "./_models";
export const getOrders = async (filters?: OrderFilters) => {
const queryParams: Record<string, string | number | boolean | null> = {};
if (filters?.page) queryParams.page = filters.page;
if (filters?.limit) queryParams.limit = filters.limit;
if (filters?.offset) queryParams.offset = filters.offset;
if (filters?.status) queryParams.status = filters.status;
if (filters?.payment_status)
queryParams.payment_status = filters.payment_status;
if (filters?.customer_id) queryParams.customer_id = filters.customer_id;
if (filters?.order_number) queryParams.order_number = filters.order_number;
if (filters?.date_from) queryParams.date_from = filters.date_from;
if (filters?.date_to) queryParams.date_to = filters.date_to;
if (filters?.min_amount) queryParams.min_amount = filters.min_amount;
if (filters?.max_amount) queryParams.max_amount = filters.max_amount;
const response = await httpGetRequest<PaginatedOrdersResponse>(
APIUrlGenerator(API_ROUTES.GET_ORDERS, queryParams)
);
return response.data;
};
export const getOrder = async (id: string) => {
const response = await httpGetRequest<Order>(
APIUrlGenerator(API_ROUTES.GET_ORDER(id))
);
return response.data;
};
export const updateOrderStatus = async (
id: string,
payload: UpdateOrderStatusRequest
) => {
const response = await httpPutRequest<Order>(
APIUrlGenerator(API_ROUTES.UPDATE_ORDER_STATUS(id)),
payload
);
return response.data;
};
export const getOrderStats = async (): Promise<OrderStats> => {
try {
const ordersResponse = await getOrders({ limit: 1000 });
const stats: OrderStats = {
total_orders: ordersResponse.total,
total_revenue: ordersResponse.orders.reduce(
(sum, order) => sum + order.total_amount,
0
),
orders_by_status: ordersResponse.orders.reduce((acc, order) => {
acc[order.status] = (acc[order.status] || 0) + 1;
return acc;
}, {} as Record<OrderStatus, number>),
avg_order_value:
ordersResponse.orders.length > 0
? ordersResponse.orders.reduce(
(sum, order) => sum + order.total_amount,
0
) / ordersResponse.orders.length
: 0,
};
return stats;
} catch (error) {
console.error("Error fetching order stats:", error);
throw error;
}
};

View File

@ -0,0 +1,395 @@
import React, { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useOrder, useUpdateOrderStatus } from '../core/_hooks';
import { OrderStatus } from '../core/_models';
import { Button } from "@/components/ui/Button";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { Modal } from "@/components/ui/Modal";
import { PageContainer, PageTitle, SectionTitle } from "@/components/ui/Typography";
import {
ArrowRight,
Package,
User,
CreditCard,
MapPin,
Calendar,
Truck,
Edit3,
Phone,
Mail,
FileText
} 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', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const OrderDetailPage = () => {
const navigate = useNavigate();
const { id } = useParams();
const [statusUpdateOpen, setStatusUpdateOpen] = useState(false);
const [newStatus, setNewStatus] = useState<OrderStatus>('processing');
const { data: order, isLoading, error } = useOrder(id || '');
const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus();
const handleStatusUpdate = () => {
if (id) {
updateStatus(
{ id, payload: { status: newStatus } },
{ onSuccess: () => setStatusUpdateOpen(false) }
);
}
};
const handleUpdateStatusClick = () => {
if (order) {
setNewStatus(order.status);
setStatusUpdateOpen(true);
}
};
if (isLoading) return <LoadingSpinner />;
if (error || !order) {
return (
<PageContainer>
<div className="text-center py-12">
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری اطلاعات سفارش</p>
<Button variant="secondary" onClick={() => navigate('/orders')} className="mt-4">
بازگشت به لیست سفارشات
</Button>
</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>سفارش #{order.order_number}</PageTitle>
<p className="text-gray-600 dark:text-gray-400 mt-1">
تاریخ ثبت: {formatDate(order.created_at)}
</p>
</div>
<div className="flex gap-3">
<Button
variant="secondary"
onClick={() => navigate('/orders')}
className="flex items-center gap-2"
>
<ArrowRight className="h-4 w-4" />
بازگشت
</Button>
<Button
variant="primary"
onClick={handleUpdateStatusClick}
className="flex items-center gap-2"
>
<Edit3 className="h-4 w-4" />
تغییر وضعیت
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* ستون اصلی */}
<div className="lg:col-span-2 space-y-8">
{/* اطلاعات سفارش */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
<Package className="h-5 w-5 text-blue-600 dark:text-blue-300" />
</div>
<SectionTitle>اطلاعات سفارش</SectionTitle>
</div>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(order.status)}`}>
{getStatusText(order.status)}
</span>
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">شماره سفارش</h4>
<p className="text-gray-600 dark:text-gray-400">#{order.order_number}</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ ثبت</h4>
<p className="text-gray-600 dark:text-gray-400">{formatDate(order.created_at)}</p>
</div>
{order.tracking_number && (
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">کد رهگیری</h4>
<p className="text-gray-600 dark:text-gray-400 font-mono">{order.tracking_number}</p>
</div>
)}
{order.estimated_delivery && (
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ تحویل تخمینی</h4>
<p className="text-gray-600 dark:text-gray-400">{formatDate(order.estimated_delivery)}</p>
</div>
)}
</div>
{order.notes && (
<div className="mt-6">
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">یادداشت</h4>
<p className="text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700 p-3 rounded-lg">
{order.notes}
</p>
</div>
)}
</div>
</div>
{/* آیتم‌های سفارش */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
<Package className="h-5 w-5 text-green-600 dark:text-green-300" />
</div>
<SectionTitle>محصولات سفارش</SectionTitle>
</div>
</div>
<div className="p-6">
<div className="space-y-4">
{order.items.map((item) => (
<div key={item.id} className="flex items-center gap-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
{item.product_image && (
<img
src={item.product_image}
alt={item.product_name}
className="w-16 h-16 object-cover rounded-lg"
/>
)}
<div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-gray-100">{item.product_name}</h4>
{item.variant_name && (
<p className="text-sm text-gray-500 dark:text-gray-400">نوع: {item.variant_name}</p>
)}
<div className="flex items-center gap-4 mt-2">
<span className="text-sm text-gray-600 dark:text-gray-400">
تعداد: {item.quantity}
</span>
<span className="text-sm text-gray-600 dark:text-gray-400">
قیمت واحد: {formatCurrency(item.unit_price)}
</span>
</div>
</div>
<div className="text-right">
<p className="font-medium text-gray-900 dark:text-gray-100">
{formatCurrency(item.total_price)}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* ستون جانبی */}
<div className="space-y-8">
{/* اطلاعات مشتری */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-purple-50 to-pink-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
<User className="h-5 w-5 text-purple-600 dark:text-purple-300" />
</div>
<SectionTitle>اطلاعات مشتری</SectionTitle>
</div>
</div>
<div className="p-6">
<div className="space-y-4">
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-1">نام</h4>
<p className="text-gray-600 dark:text-gray-400">
{order.customer.first_name} {order.customer.last_name}
</p>
</div>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400">{order.customer.email}</p>
</div>
{order.customer.phone && (
<div className="flex items-center gap-2">
<Phone className="h-4 w-4 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400" dir="ltr">{order.customer.phone}</p>
</div>
)}
</div>
</div>
</div>
{/* آدرس‌ها */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-orange-50 to-red-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-orange-100 dark:bg-orange-900 rounded-lg">
<MapPin className="h-5 w-5 text-orange-600 dark:text-orange-300" />
</div>
<SectionTitle>آدرسها</SectionTitle>
</div>
</div>
<div className="p-6 space-y-6">
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">آدرس ارسال</h4>
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<p>{order.shipping_address.first_name} {order.shipping_address.last_name}</p>
<p>{order.shipping_address.address_line_1}</p>
{order.shipping_address.address_line_2 && <p>{order.shipping_address.address_line_2}</p>}
<p>{order.shipping_address.city}, {order.shipping_address.state}</p>
<p>کد پستی: {order.shipping_address.postal_code}</p>
{order.shipping_address.phone && <p>تلفن: {order.shipping_address.phone}</p>}
</div>
</div>
<hr className="border-gray-200 dark:border-gray-700" />
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">آدرس صورتحساب</h4>
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<p>{order.billing_address.first_name} {order.billing_address.last_name}</p>
<p>{order.billing_address.address_line_1}</p>
{order.billing_address.address_line_2 && <p>{order.billing_address.address_line_2}</p>}
<p>{order.billing_address.city}, {order.billing_address.state}</p>
<p>کد پستی: {order.billing_address.postal_code}</p>
</div>
</div>
</div>
</div>
{/* اطلاعات پرداخت */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-teal-50 to-cyan-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-teal-100 dark:bg-teal-900 rounded-lg">
<CreditCard className="h-5 w-5 text-teal-600 dark:text-teal-300" />
</div>
<SectionTitle>پرداخت</SectionTitle>
</div>
</div>
<div className="p-6 space-y-4">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">جمع فرعی</span>
<span className="font-medium">{formatCurrency(order.subtotal)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">مالیات</span>
<span className="font-medium">{formatCurrency(order.tax_amount)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">هزینه ارسال</span>
<span className="font-medium">{formatCurrency(order.shipping_amount)}</span>
</div>
{order.discount_amount > 0 && (
<div className="flex justify-between text-green-600 dark:text-green-400">
<span>تخفیف</span>
<span className="font-medium">-{formatCurrency(order.discount_amount)}</span>
</div>
)}
<hr className="border-gray-200 dark:border-gray-700" />
<div className="flex justify-between text-lg font-bold">
<span>مجموع</span>
<span>{formatCurrency(order.total_amount)}</span>
</div>
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex justify-between mb-2">
<span className="text-sm text-gray-600 dark:text-gray-400">روش پرداخت</span>
<span className="text-sm font-medium">{order.payment.payment_method}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-600 dark:text-gray-400">وضعیت پرداخت</span>
<span className={`text-sm font-medium ${order.payment.payment_status === 'paid'
? 'text-green-600 dark:text-green-400'
: 'text-yellow-600 dark:text-yellow-400'
}`}>
{order.payment.payment_status === 'paid' ? 'پرداخت شده' : 'در انتظار پرداخت'}
</span>
</div>
{order.payment.transaction_id && (
<div className="flex justify-between mt-2">
<span className="text-sm text-gray-600 dark:text-gray-400">شماره تراکنش</span>
<span className="text-sm font-mono">{order.payment.transaction_id}</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>
{/* مودال تغییر وضعیت */}
<Modal isOpen={statusUpdateOpen} onClose={() => setStatusUpdateOpen(false)} 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-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"
>
<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={() => setStatusUpdateOpen(false)} disabled={isUpdating}>
انصراف
</Button>
<Button variant="primary" onClick={handleStatusUpdate} loading={isUpdating}>
بهروزرسانی
</Button>
</div>
</div>
</Modal>
</PageContainer>
);
};
export default OrderDetailPage;

View File

@ -0,0 +1,355 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useOrders, useOrderStats, useUpdateOrderStatus } from '../core/_hooks';
import { Order, 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 {
ShoppingCart,
Package,
DollarSign,
Clock,
Search,
Filter,
Eye,
Edit3,
TrendingUp,
Calendar
} 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 = () => (
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{[...Array(5)].map((_, i) => (
<tr key={i}>
{Array.from({ length: 7 }).map((__, j) => (
<td key={j} className="px-6 py-4 whitespace-nowrap">
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse" />
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
const OrdersListPage = () => {
const navigate = useNavigate();
const [statusUpdateId, setStatusUpdateId] = useState<string | null>(null);
const [newStatus, setNewStatus] = useState<OrderStatus>('processing');
const [filters, setFilters] = useState<OrderFilters>({
page: 1,
limit: 20,
order_number: '',
status: undefined,
});
const { data: ordersData, isLoading, error } = useOrders(filters);
const { data: stats, isLoading: statsLoading } = useOrderStats();
const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus();
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 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center">
<div className="p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
<ShoppingCart className="h-6 w-6 text-blue-600 dark:text-blue-300" />
</div>
<div className="mr-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">کل سفارشات</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{statsLoading ? '...' : stats?.total_orders?.toLocaleString('fa-IR') || '0'}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center">
<div className="p-3 bg-green-100 dark:bg-green-900 rounded-lg">
<DollarSign className="h-6 w-6 text-green-600 dark:text-green-300" />
</div>
<div className="mr-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">کل فروش</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{statsLoading ? '...' : formatCurrency(stats?.total_revenue || 0)}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center">
<div className="p-3 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
<Clock className="h-6 w-6 text-yellow-600 dark:text-yellow-300" />
</div>
<div className="mr-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">در انتظار</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{statsLoading ? '...' : (stats?.orders_by_status?.pending || 0)}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center">
<div className="p-3 bg-purple-100 dark:bg-purple-900 rounded-lg">
<TrendingUp className="h-6 w-6 text-purple-600 dark:text-purple-300" />
</div>
<div className="mr-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">میانگین سفارش</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{statsLoading ? '...' : formatCurrency(stats?.avg_order_value || 0)}
</p>
</div>
</div>
</div>
</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="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="relative">
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="شماره سفارش..."
value={filters.order_number || ''}
onChange={(e) => setFilters(prev => ({ ...prev, order_number: 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"
/>
</div>
<div>
<select
value={filters.status || ''}
onChange={(e) => setFilters(prev => ({ ...prev, status: e.target.value as OrderStatus || undefined, page: 1 }))}
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"
>
<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>
<Button
variant="secondary"
onClick={() => setFilters({ page: 1, limit: 20, order_number: '', status: undefined })}
className="flex items-center gap-2"
>
<Filter className="h-4 w-4" />
پاک کردن فیلترها
</Button>
</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>
) : (
<>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">شماره سفارش</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">مشتری</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">مبلغ</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">وضعیت</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">تاریخ</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">عملیات</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{ordersData.orders.map((order: Order) => (
<tr key={order.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
#{order.order_number}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
<div>
<div className="font-medium">{order.customer.first_name} {order.customer.last_name}</div>
<div className="text-gray-500 dark:text-gray-400">{order.customer.email}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
{formatCurrency(order.total_amount)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(order.status)}`}>
{getStatusText(order.status)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{formatDate(order.created_at)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center gap-2">
<button
onClick={() => handleViewOrder(order.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(order.id, order.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>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* صفحه‌بندی */}
<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-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"
>
<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;

View File

@ -78,4 +78,9 @@ export const QUERY_KEYS = {
CREATE_DISCOUNT_CODE: "create_discount_code",
UPDATE_DISCOUNT_CODE: "update_discount_code",
DELETE_DISCOUNT_CODE: "delete_discount_code",
// Orders
GET_ORDERS: "get_orders",
GET_ORDER: "get_order",
UPDATE_ORDER_STATUS: "update_order_status",
};