From d216a886d0503fa7fe80fc6912ab6eac77cd4af2 Mon Sep 17 00:00:00 2001 From: hossein taromi Date: Sat, 30 Aug 2025 17:38:19 +0330 Subject: [PATCH] feat(orders): implement orders pages, update routes, and add order-related API constants --- src/App.tsx | 10 +- src/components/layout/Sidebar.tsx | 6 + src/constant/routes.ts | 5 + src/pages/Orders.tsx | 192 --------- src/pages/discount-codes/core/_models.ts | 27 +- .../DiscountCodeFormPage.tsx | 24 +- src/pages/orders/core/_hooks.ts | 58 +++ src/pages/orders/core/_models.ts | 126 ++++++ src/pages/orders/core/_requests.ts | 84 ++++ .../orders/order-detail/OrderDetailPage.tsx | 395 ++++++++++++++++++ .../orders/orders-list/OrdersListPage.tsx | 355 ++++++++++++++++ src/utils/query-key.ts | 5 + 12 files changed, 1077 insertions(+), 210 deletions(-) delete mode 100644 src/pages/Orders.tsx create mode 100644 src/pages/orders/core/_hooks.ts create mode 100644 src/pages/orders/core/_models.ts create mode 100644 src/pages/orders/core/_requests.ts create mode 100644 src/pages/orders/order-detail/OrderDetailPage.tsx create mode 100644 src/pages/orders/orders-list/OrdersListPage.tsx 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.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", };