feat(orders): implement orders pages, update routes, and add order-related API constants
This commit is contained in:
parent
d8b6f2a54f
commit
d216a886d0
10
src/App.tsx
10
src/App.tsx
|
|
@ -15,7 +15,6 @@ import { Layout } from './components/layout/Layout';
|
||||||
const Login = lazy(() => import('./pages/Login').then(module => ({ default: module.Login })));
|
const Login = lazy(() => import('./pages/Login').then(module => ({ default: module.Login })));
|
||||||
const Dashboard = lazy(() => import('./pages/Dashboard').then(module => ({ default: module.Dashboard })));
|
const Dashboard = lazy(() => import('./pages/Dashboard').then(module => ({ default: module.Dashboard })));
|
||||||
const Users = lazy(() => import('./pages/Users').then(module => ({ default: module.Users })));
|
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 Reports = lazy(() => import('./pages/Reports').then(module => ({ default: module.Reports })));
|
||||||
const Notifications = lazy(() => import('./pages/Notifications').then(module => ({ default: module.Notifications })));
|
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 DiscountCodesListPage = lazy(() => import('./pages/discount-codes/discount-codes-list/DiscountCodesListPage'));
|
||||||
const DiscountCodeFormPage = lazy(() => import('./pages/discount-codes/discount-code-form/DiscountCodeFormPage'));
|
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
|
// Products Pages
|
||||||
const ProductsListPage = lazy(() => import('./pages/products/products-list/ProductsListPage'));
|
const ProductsListPage = lazy(() => import('./pages/products/products-list/ProductsListPage'));
|
||||||
const ProductFormPage = lazy(() => import('./pages/products/product-form/ProductFormPage'));
|
const ProductFormPage = lazy(() => import('./pages/products/product-form/ProductFormPage'));
|
||||||
|
|
@ -81,7 +84,6 @@ const AppRoutes = () => {
|
||||||
<Route index element={<Dashboard />} />
|
<Route index element={<Dashboard />} />
|
||||||
<Route path="users" element={<Users />} />
|
<Route path="users" element={<Users />} />
|
||||||
<Route path="products" element={<ProductsListPage />} />
|
<Route path="products" element={<ProductsListPage />} />
|
||||||
<Route path="orders" element={<Orders />} />
|
|
||||||
<Route path="reports" element={<Reports />} />
|
<Route path="reports" element={<Reports />} />
|
||||||
<Route path="notifications" element={<Notifications />} />
|
<Route path="notifications" element={<Notifications />} />
|
||||||
|
|
||||||
|
|
@ -118,6 +120,10 @@ const AppRoutes = () => {
|
||||||
<Route path="discount-codes/create" element={<DiscountCodeFormPage />} />
|
<Route path="discount-codes/create" element={<DiscountCodeFormPage />} />
|
||||||
<Route path="discount-codes/:id/edit" 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 */}
|
{/* Landing Hero Route */}
|
||||||
<Route path="landing-hero" element={<HeroSliderPage />} />
|
<Route path="landing-hero" element={<HeroSliderPage />} />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Sliders,
|
Sliders,
|
||||||
BadgePercent,
|
BadgePercent,
|
||||||
|
ShoppingCart,
|
||||||
X
|
X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
|
@ -33,6 +34,11 @@ const menuItems: MenuItem[] = [
|
||||||
icon: Home,
|
icon: Home,
|
||||||
path: '/',
|
path: '/',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'سفارشات',
|
||||||
|
icon: ShoppingCart,
|
||||||
|
path: '/orders',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'مدیریت محصولات',
|
title: 'مدیریت محصولات',
|
||||||
icon: Package,
|
icon: Package,
|
||||||
|
|
|
||||||
|
|
@ -87,4 +87,9 @@ export const API_ROUTES = {
|
||||||
CREATE_DISCOUNT_CODE: "api/v1/admin/discount/",
|
CREATE_DISCOUNT_CODE: "api/v1/admin/discount/",
|
||||||
UPDATE_DISCOUNT_CODE: (id: string) => `api/v1/admin/discount/${id}/`,
|
UPDATE_DISCOUNT_CODE: (id: string) => `api/v1/admin/discount/${id}/`,
|
||||||
DELETE_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`,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,16 +1,22 @@
|
||||||
export type DiscountCodeType = "percentage" | "fixed";
|
export type DiscountCodeType = "percentage" | "fixed" | "fee_percentage";
|
||||||
|
|
||||||
export type DiscountApplicationLevel =
|
export type DiscountApplicationLevel =
|
||||||
| "invoice"
|
| "invoice"
|
||||||
| "category"
|
| "category"
|
||||||
| "product"
|
| "product"
|
||||||
| "shipping";
|
| "shipping"
|
||||||
|
| "product_fee";
|
||||||
|
|
||||||
export type DiscountStatus = "active" | "inactive";
|
export type DiscountStatus = "active" | "inactive";
|
||||||
|
|
||||||
|
export type UserGroup = "new" | "loyal" | "all";
|
||||||
|
|
||||||
export interface DiscountUserRestrictions {
|
export interface DiscountUserRestrictions {
|
||||||
user_ids?: number[];
|
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;
|
new_users_only?: boolean;
|
||||||
loyal_users_only?: boolean;
|
loyal_users_only?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -19,6 +25,15 @@ export interface DiscountMeta {
|
||||||
[key: string]: string | number | boolean | null;
|
[key: string]: string | number | boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SteppedDiscountStep {
|
||||||
|
min_amount: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SteppedDiscount {
|
||||||
|
steps: SteppedDiscountStep[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface DiscountCode {
|
export interface DiscountCode {
|
||||||
id: number;
|
id: number;
|
||||||
code: string;
|
code: string;
|
||||||
|
|
@ -32,10 +47,11 @@ export interface DiscountCode {
|
||||||
max_discount_amount?: number;
|
max_discount_amount?: number;
|
||||||
usage_limit?: number;
|
usage_limit?: number;
|
||||||
user_usage_limit?: number;
|
user_usage_limit?: number;
|
||||||
single_use?: boolean;
|
single_use: boolean;
|
||||||
valid_from?: string;
|
valid_from?: string;
|
||||||
valid_to?: string;
|
valid_to?: string;
|
||||||
user_restrictions?: DiscountUserRestrictions;
|
user_restrictions?: DiscountUserRestrictions;
|
||||||
|
stepped_discount?: SteppedDiscount;
|
||||||
meta?: DiscountMeta;
|
meta?: DiscountMeta;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
|
@ -63,10 +79,11 @@ export interface CreateDiscountCodeRequest {
|
||||||
max_discount_amount?: number;
|
max_discount_amount?: number;
|
||||||
usage_limit?: number;
|
usage_limit?: number;
|
||||||
user_usage_limit?: number;
|
user_usage_limit?: number;
|
||||||
single_use?: boolean;
|
single_use: boolean;
|
||||||
valid_from?: string;
|
valid_from?: string;
|
||||||
valid_to?: string;
|
valid_to?: string;
|
||||||
user_restrictions?: DiscountUserRestrictions;
|
user_restrictions?: DiscountUserRestrictions;
|
||||||
|
stepped_discount?: SteppedDiscount;
|
||||||
meta?: DiscountMeta;
|
meta?: DiscountMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,18 @@ import { FormHeader, PageContainer, Label, SectionTitle } from '../../../compone
|
||||||
import { ArrowRight, BadgePercent, Calendar, Settings, Users, Tag, Info } from 'lucide-react';
|
import { ArrowRight, BadgePercent, Calendar, Settings, Users, Tag, Info } from 'lucide-react';
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
code: yup.string().required('کد الزامی است'),
|
code: yup.string().min(3, 'کد باید حداقل ۳ کاراکتر باشد').max(50, 'کد نباید بیشتر از ۵۰ کاراکتر باشد').required('کد الزامی است'),
|
||||||
name: yup.string().required('نام الزامی است'),
|
name: yup.string().min(1, 'نام الزامی است').max(100, 'نام نباید بیشتر از ۱۰۰ کاراکتر باشد').required('نام الزامی است'),
|
||||||
description: yup.string().nullable(),
|
description: yup.string().max(500, 'توضیحات نباید بیشتر از ۵۰۰ کاراکتر باشد').nullable(),
|
||||||
type: yup.mixed<'percentage' | 'fixed'>().oneOf(['percentage', 'fixed']).required('نوع الزامی است'),
|
type: yup.mixed<'percentage' | 'fixed' | 'fee_percentage'>().oneOf(['percentage', 'fixed', 'fee_percentage']).required('نوع الزامی است'),
|
||||||
value: yup.number().typeError('مقدار نامعتبر است').required('مقدار الزامی است').min(0),
|
value: yup.number().typeError('مقدار نامعتبر است').required('مقدار الزامی است').min(0.01, 'مقدار باید بیشتر از صفر باشد'),
|
||||||
status: yup.mixed<'active' | 'inactive'>().oneOf(['active', 'inactive']).required('وضعیت الزامی است'),
|
status: yup.mixed<'active' | 'inactive'>().oneOf(['active', 'inactive']).required('وضعیت الزامی است'),
|
||||||
application_level: yup.mixed<'invoice' | 'category' | 'product' | 'shipping'>().oneOf(['invoice', 'category', 'product', 'shipping']).required('سطح اعمال الزامی است'),
|
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).nullable(),
|
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).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).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).nullable(),
|
user_usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).min(1, 'حداقل ۱ بار استفاده').nullable(),
|
||||||
single_use: yup.boolean().default(false),
|
single_use: yup.boolean().required('این فیلد الزامی است'),
|
||||||
valid_from: yup.string().nullable(),
|
valid_from: yup.string().nullable(),
|
||||||
valid_to: 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')}>
|
<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="percentage">درصدی</option>
|
||||||
<option value="fixed">مبلغ ثابت</option>
|
<option value="fixed">مبلغ ثابت</option>
|
||||||
|
<option value="fee_percentage">درصد کارمزد</option>
|
||||||
</select>
|
</select>
|
||||||
{errors.type && <p className="text-sm text-red-600 dark:text-red-400">{errors.type.message as string}</p>}
|
{errors.type && <p className="text-sm text-red-600 dark:text-red-400">{errors.type.message as string}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -192,6 +193,7 @@ const DiscountCodeFormPage = () => {
|
||||||
<option value="category">دستهبندی خاص</option>
|
<option value="category">دستهبندی خاص</option>
|
||||||
<option value="product">محصول خاص</option>
|
<option value="product">محصول خاص</option>
|
||||||
<option value="shipping">هزینه ارسال</option>
|
<option value="shipping">هزینه ارسال</option>
|
||||||
|
<option value="product_fee">کارمزد محصول</option>
|
||||||
</select>
|
</select>
|
||||||
{errors.application_level && <p className="text-sm text-red-600 dark:text-red-400">{errors.application_level.message as string}</p>}
|
{errors.application_level && <p className="text-sm text-red-600 dark:text-red-400">{errors.application_level.message as string}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 || "خطا در بهروزرسانی وضعیت سفارش");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -78,4 +78,9 @@ export const QUERY_KEYS = {
|
||||||
CREATE_DISCOUNT_CODE: "create_discount_code",
|
CREATE_DISCOUNT_CODE: "create_discount_code",
|
||||||
UPDATE_DISCOUNT_CODE: "update_discount_code",
|
UPDATE_DISCOUNT_CODE: "update_discount_code",
|
||||||
DELETE_DISCOUNT_CODE: "delete_discount_code",
|
DELETE_DISCOUNT_CODE: "delete_discount_code",
|
||||||
|
|
||||||
|
// Orders
|
||||||
|
GET_ORDERS: "get_orders",
|
||||||
|
GET_ORDER: "get_order",
|
||||||
|
UPDATE_ORDER_STATUS: "update_order_status",
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue