diff --git a/src/App.tsx b/src/App.tsx
index d6a0d57..d3c52e4 100644
--- a/src/App.tsx
+++ b/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 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 = () => {
} />
} />
} />
- } />
} />
} />
@@ -118,6 +120,10 @@ const AppRoutes = () => {
} />
} />
+ {/* Orders Routes */}
+ } />
+ } />
+
{/* Landing Hero Route */}
} />
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index f0c016f..f31ce86 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -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,
diff --git a/src/constant/routes.ts b/src/constant/routes.ts
index 7160814..cf3a0d8 100644
--- a/src/constant/routes.ts
+++ b/src/constant/routes.ts
@@ -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`,
};
diff --git a/src/pages/Orders.tsx b/src/pages/Orders.tsx
deleted file mode 100644
index 2abea74..0000000
--- a/src/pages/Orders.tsx
+++ /dev/null
@@ -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) => (
-
- {value} تومان
-
- )
- },
- {
- key: 'status',
- label: 'وضعیت',
- render: (value) => (
-
- {value}
-
- )
- },
- { key: 'date', label: 'تاریخ سفارش', sortable: true },
- {
- key: 'actions',
- label: 'عملیات',
- render: (_, row) => (
-
-
-
-
- )
- }
- ];
-
- 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 (
-
- مدیریت سفارشات
-
- {filteredOrders.length} سفارش یافت شد
-
-
-
-
-
-
-
-
کل سفارشات
-
{allOrders.length}
-
-
-
-
-
-
-
-
-
تحویل شده
-
- {allOrders.filter(o => o.status === 'تحویل شده').length}
-
-
-
-
-
-
-
-
-
-
در انتظار
-
- {allOrders.filter(o => o.status === 'در حال پردازش').length}
-
-
-
-
-
-
-
-
-
-
کل فروش
-
- {totalRevenue.toLocaleString()} تومان
-
-
-
-
-
-
-
-
-
-
-
-
-
setSearchTerm(e.target.value)}
- className="input pr-10 max-w-md"
- />
-
-
-
-
-
-
- );
-};
\ No newline at end of file
diff --git a/src/pages/discount-codes/core/_models.ts b/src/pages/discount-codes/core/_models.ts
index a91b99f..92fbafe 100644
--- a/src/pages/discount-codes/core/_models.ts
+++ b/src/pages/discount-codes/core/_models.ts
@@ -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;
}
diff --git a/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx b/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx
index e2dfbfa..b458753 100644
--- a/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx
+++ b/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx
@@ -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 = () => {
{errors.type && {errors.type.message as string}
}
@@ -192,6 +193,7 @@ const DiscountCodeFormPage = () => {
+
{errors.application_level && {errors.application_level.message as string}
}
diff --git a/src/pages/orders/core/_hooks.ts b/src/pages/orders/core/_hooks.ts
new file mode 100644
index 0000000..3dfd395
--- /dev/null
+++ b/src/pages/orders/core/_hooks.ts
@@ -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 || "خطا در بهروزرسانی وضعیت سفارش");
+ },
+ });
+};
diff --git a/src/pages/orders/core/_models.ts b/src/pages/orders/core/_models.ts
new file mode 100644
index 0000000..f7b10be
--- /dev/null
+++ b/src/pages/orders/core/_models.ts
@@ -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;
+ 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;
+ avg_order_value: number;
+}
+
+export type Response = {
+ data: T;
+ message?: string;
+ success?: boolean;
+};
diff --git a/src/pages/orders/core/_requests.ts b/src/pages/orders/core/_requests.ts
new file mode 100644
index 0000000..6a30827
--- /dev/null
+++ b/src/pages/orders/core/_requests.ts
@@ -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 = {};
+
+ 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(
+ APIUrlGenerator(API_ROUTES.GET_ORDERS, queryParams)
+ );
+
+ return response.data;
+};
+
+export const getOrder = async (id: string) => {
+ const response = await httpGetRequest(
+ APIUrlGenerator(API_ROUTES.GET_ORDER(id))
+ );
+ return response.data;
+};
+
+export const updateOrderStatus = async (
+ id: string,
+ payload: UpdateOrderStatusRequest
+) => {
+ const response = await httpPutRequest(
+ APIUrlGenerator(API_ROUTES.UPDATE_ORDER_STATUS(id)),
+ payload
+ );
+ return response.data;
+};
+
+export const getOrderStats = async (): Promise => {
+ 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),
+ 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;
+ }
+};
diff --git a/src/pages/orders/order-detail/OrderDetailPage.tsx b/src/pages/orders/order-detail/OrderDetailPage.tsx
new file mode 100644
index 0000000..eb274c8
--- /dev/null
+++ b/src/pages/orders/order-detail/OrderDetailPage.tsx
@@ -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('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 ;
+
+ if (error || !order) {
+ return (
+
+
+
خطا در بارگذاری اطلاعات سفارش
+
+
+
+ );
+ }
+
+ return (
+
+ {/* هدر صفحه */}
+
+
+
سفارش #{order.order_number}
+
+ تاریخ ثبت: {formatDate(order.created_at)}
+
+
+
+
+
+
+
+
+
+ {/* ستون اصلی */}
+
+ {/* اطلاعات سفارش */}
+
+
+
+
+
+ {getStatusText(order.status)}
+
+
+
+
+
+
+
شماره سفارش
+
#{order.order_number}
+
+
+
تاریخ ثبت
+
{formatDate(order.created_at)}
+
+ {order.tracking_number && (
+
+
کد رهگیری
+
{order.tracking_number}
+
+ )}
+ {order.estimated_delivery && (
+
+
تاریخ تحویل تخمینی
+
{formatDate(order.estimated_delivery)}
+
+ )}
+
+ {order.notes && (
+
+
یادداشت
+
+ {order.notes}
+
+
+ )}
+
+
+
+ {/* آیتمهای سفارش */}
+
+
+
+
+ {order.items.map((item) => (
+
+ {item.product_image && (
+

+ )}
+
+
{item.product_name}
+ {item.variant_name && (
+
نوع: {item.variant_name}
+ )}
+
+
+ تعداد: {item.quantity}
+
+
+ قیمت واحد: {formatCurrency(item.unit_price)}
+
+
+
+
+
+ {formatCurrency(item.total_price)}
+
+
+
+ ))}
+
+
+
+
+
+ {/* ستون جانبی */}
+
+ {/* اطلاعات مشتری */}
+
+
+
+
+
+
نام
+
+ {order.customer.first_name} {order.customer.last_name}
+
+
+
+
+
{order.customer.email}
+
+ {order.customer.phone && (
+
+
+
{order.customer.phone}
+
+ )}
+
+
+
+
+ {/* آدرسها */}
+
+
+
+
+
آدرس ارسال
+
+
{order.shipping_address.first_name} {order.shipping_address.last_name}
+
{order.shipping_address.address_line_1}
+ {order.shipping_address.address_line_2 &&
{order.shipping_address.address_line_2}
}
+
{order.shipping_address.city}, {order.shipping_address.state}
+
کد پستی: {order.shipping_address.postal_code}
+ {order.shipping_address.phone &&
تلفن: {order.shipping_address.phone}
}
+
+
+
+
+
آدرس صورتحساب
+
+
{order.billing_address.first_name} {order.billing_address.last_name}
+
{order.billing_address.address_line_1}
+ {order.billing_address.address_line_2 &&
{order.billing_address.address_line_2}
}
+
{order.billing_address.city}, {order.billing_address.state}
+
کد پستی: {order.billing_address.postal_code}
+
+
+
+
+
+ {/* اطلاعات پرداخت */}
+
+
+
+
+ جمع فرعی
+ {formatCurrency(order.subtotal)}
+
+
+ مالیات
+ {formatCurrency(order.tax_amount)}
+
+
+ هزینه ارسال
+ {formatCurrency(order.shipping_amount)}
+
+ {order.discount_amount > 0 && (
+
+ تخفیف
+ -{formatCurrency(order.discount_amount)}
+
+ )}
+
+
+ مجموع
+ {formatCurrency(order.total_amount)}
+
+
+
+
+ روش پرداخت
+ {order.payment.payment_method}
+
+
+ وضعیت پرداخت
+
+ {order.payment.payment_status === 'paid' ? 'پرداخت شده' : 'در انتظار پرداخت'}
+
+
+ {order.payment.transaction_id && (
+
+ شماره تراکنش
+ {order.payment.transaction_id}
+
+ )}
+
+
+
+
+
+
+ {/* مودال تغییر وضعیت */}
+ setStatusUpdateOpen(false)} title="تغییر وضعیت سفارش">
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default OrderDetailPage;
diff --git a/src/pages/orders/orders-list/OrdersListPage.tsx b/src/pages/orders/orders-list/OrdersListPage.tsx
new file mode 100644
index 0000000..6c996d1
--- /dev/null
+++ b/src/pages/orders/orders-list/OrdersListPage.tsx
@@ -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 = () => (
+
+
+
+
+ {[...Array(5)].map((_, i) => (
+
+ {Array.from({ length: 7 }).map((__, j) => (
+ |
+
+ |
+ ))}
+
+ ))}
+
+
+
+
+);
+
+const OrdersListPage = () => {
+ const navigate = useNavigate();
+ const [statusUpdateId, setStatusUpdateId] = useState(null);
+ const [newStatus, setNewStatus] = useState('processing');
+ const [filters, setFilters] = useState({
+ page: 1,
+ limit: 20,
+ order_number: '',
+ status: 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 (
+
+
+
خطا در بارگذاری سفارشات
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ مدیریت سفارشات
+
+
+ {ordersData?.total || 0} سفارش یافت شد
+
+
+
+
+ {/* آمار کلی */}
+
+
+
+
+
+
+
+
کل سفارشات
+
+ {statsLoading ? '...' : stats?.total_orders?.toLocaleString('fa-IR') || '0'}
+
+
+
+
+
+
+
+
+
+
+
+
کل فروش
+
+ {statsLoading ? '...' : formatCurrency(stats?.total_revenue || 0)}
+
+
+
+
+
+
+
+
+
+
+
+
در انتظار
+
+ {statsLoading ? '...' : (stats?.orders_by_status?.pending || 0)}
+
+
+
+
+
+
+
+
+
+
+
+
میانگین سفارش
+
+ {statsLoading ? '...' : formatCurrency(stats?.avg_order_value || 0)}
+
+
+
+
+
+
+ {/* فیلترها */}
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+
+
+
+
+
+ {/* جدول سفارشات */}
+ {isLoading ? (
+
+ ) : !ordersData?.orders || ordersData.orders.length === 0 ? (
+
+
+
+
هیچ سفارشی یافت نشد
+
با تغییر فیلترها جستجو کنید
+
+
+ ) : (
+ <>
+
+
+
+
+
+ | شماره سفارش |
+ مشتری |
+ مبلغ |
+ وضعیت |
+ تاریخ |
+ عملیات |
+
+
+
+ {ordersData.orders.map((order: Order) => (
+
+ |
+ #{order.order_number}
+ |
+
+
+ {order.customer.first_name} {order.customer.last_name}
+ {order.customer.email}
+
+ |
+
+ {formatCurrency(order.total_amount)}
+ |
+
+
+ {getStatusText(order.status)}
+
+ |
+
+ {formatDate(order.created_at)}
+ |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+
+
+ {/* صفحهبندی */}
+
+ >
+ )}
+
+ {/* مودال تغییر وضعیت */}
+ setStatusUpdateId(null)} title="تغییر وضعیت سفارش">
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default OrdersListPage;
diff --git a/src/utils/query-key.ts b/src/utils/query-key.ts
index 2d211bc..df4b742 100644
--- a/src/utils/query-key.ts
+++ b/src/utils/query-key.ts
@@ -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",
};