From dad0ff292d55ce98c25a0688372d7a31f21f2b14 Mon Sep 17 00:00:00 2001 From: hosseintaromi Date: Fri, 26 Sep 2025 12:16:58 +0330 Subject: [PATCH] feat(orders): enhance order detail and list pages with improved data handling and UI updates --- src/components/ui/Table.tsx | 4 +- src/constant/routes.ts | 109 ++++---- src/pages/categories/core/_requests.ts | 15 +- src/pages/orders/core/_models.ts | 49 ++-- src/pages/orders/core/_requests.ts | 63 +++-- .../orders/order-detail/OrderDetailPage.tsx | 238 ++++++++++++------ .../orders/orders-list/OrdersListPage.tsx | 43 ++-- src/pages/products/core/_requests.ts | 41 ++- src/pages/users-admin/core/_requests.ts | 18 +- .../user-admin-detail/UserAdminDetailPage.tsx | 62 +---- .../users-admin-list/UsersAdminListPage.tsx | 91 +++++-- src/utils/baseHttpService.ts | 12 +- src/utils/numberUtils.ts | 11 + 13 files changed, 448 insertions(+), 308 deletions(-) diff --git a/src/components/ui/Table.tsx b/src/components/ui/Table.tsx index b7100b9..8cf0f8c 100644 --- a/src/components/ui/Table.tsx +++ b/src/components/ui/Table.tsx @@ -96,9 +96,9 @@ export const Table = ({ columns, data, loading = false }: TableProps) => { column.align === 'center' && 'justify-center', (!column.align || column.align === 'right') && 'justify-end' )}> - {column.label} + {column.label} {column.sortable && ( -
+
`api/v1/discount-drafts/${id}`, GET_DRAFT_DETAIL: (id: string) => `api/v1/drafts/${id}`, // Admin Users APIs - GET_ADMIN_USERS: "api/v1/admin/admin-users", - GET_ADMIN_USER: (id: string) => `api/v1/admin/admin-users/${id}`, - CREATE_ADMIN_USER: "api/v1/admin/admin-users", - UPDATE_ADMIN_USER: (id: string) => `api/v1/admin/admin-users/${id}`, - DELETE_ADMIN_USER: (id: string) => `api/v1/admin/admin-users/${id}`, + GET_ADMIN_USERS: "admin-users", + GET_ADMIN_USER: (id: string) => `admin-users/${id}`, + CREATE_ADMIN_USER: "admin-users", + UPDATE_ADMIN_USER: (id: string) => `admin-users/${id}`, + DELETE_ADMIN_USER: (id: string) => `admin-users/${id}`, // Roles APIs - GET_ROLES: "api/v1/admin/roles", - GET_ROLE: (id: string) => `api/v1/admin/roles/${id}`, - CREATE_ROLE: "api/v1/admin/roles", - UPDATE_ROLE: (id: string) => `api/v1/admin/roles/${id}`, - DELETE_ROLE: (id: string) => `api/v1/admin/roles/${id}`, - GET_ROLE_PERMISSIONS: (id: string) => `api/v1/admin/roles/${id}/permissions`, + GET_ROLES: "roles", + GET_ROLE: (id: string) => `roles/${id}`, + CREATE_ROLE: "roles", + UPDATE_ROLE: (id: string) => `roles/${id}`, + DELETE_ROLE: (id: string) => `roles/${id}`, + GET_ROLE_PERMISSIONS: (id: string) => `roles/${id}/permissions`, ASSIGN_ROLE_PERMISSION: (roleId: string, permissionId: string) => - `api/v1/admin/roles/${roleId}/permissions/${permissionId}`, + `roles/${roleId}/permissions/${permissionId}`, REMOVE_ROLE_PERMISSION: (roleId: string, permissionId: string) => - `api/v1/admin/roles/${roleId}/permissions/${permissionId}`, + `roles/${roleId}/permissions/${permissionId}`, // Permissions APIs - GET_PERMISSIONS: "api/v1/admin/permissions", - GET_PERMISSION: (id: string) => `api/v1/admin/permissions/${id}`, - CREATE_PERMISSION: "api/v1/admin/permissions", - UPDATE_PERMISSION: (id: string) => `api/v1/admin/permissions/${id}`, - DELETE_PERMISSION: (id: string) => `api/v1/admin/permissions/${id}`, + GET_PERMISSIONS: "permissions", + GET_PERMISSION: (id: string) => `permissions/${id}`, + CREATE_PERMISSION: "permissions", + UPDATE_PERMISSION: (id: string) => `permissions/${id}`, + DELETE_PERMISSION: (id: string) => `permissions/${id}`, - // Product Options APIs + // Product Options APIs (non-admin) GET_PRODUCT_OPTIONS: "api/v1/product-options", GET_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`, CREATE_PRODUCT_OPTION: "api/v1/product-options", UPDATE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`, DELETE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`, - // Categories APIs + // Categories APIs (non-admin) GET_CATEGORIES: "api/v1/products/categories", GET_CATEGORY: (id: string) => `api/v1/products/categories/${id}`, CREATE_CATEGORY: "api/v1/products/categories", UPDATE_CATEGORY: (id: string) => `api/v1/products/categories/${id}`, DELETE_CATEGORY: (id: string) => `api/v1/products/categories/${id}`, - // Products APIs + // Products APIs (non-admin) GET_PRODUCTS: "api/v1/products", GET_PRODUCT: (id: string) => `api/v1/products/${id}`, CREATE_PRODUCT: "api/v1/products", @@ -64,54 +65,52 @@ export const API_ROUTES = { `api/v1/products/variants/${variantId}`, // Files APIs - GET_FILES: "api/v1/admin/files", - UPLOAD_FILE: "api/v1/admin/files", - GET_FILE: (id: string) => `api/v1/admin/files/${id}`, - UPDATE_FILE: (id: string) => `api/v1/admin/files/${id}`, - DELETE_FILE: (id: string) => `api/v1/admin/files/${id}`, - DOWNLOAD_FILE: (serveKey: string) => `api/v1/files/${serveKey}`, + GET_FILES: "files", + UPLOAD_FILE: "files", + GET_FILE: (id: string) => `files/${id}`, + UPDATE_FILE: (id: string) => `files/${id}`, + DELETE_FILE: (id: string) => `files/${id}`, + DOWNLOAD_FILE: (serveKey: string) => `api/v1/files/${serveKey}`, // non-admin - // Images APIs + // Images APIs (non-admin) GET_IMAGES: "api/v1/images", CREATE_IMAGE: "api/v1/images", UPDATE_IMAGE: (imageId: string) => `api/v1/products/images/${imageId}`, DELETE_IMAGE: (imageId: string) => `api/v1/products/images/${imageId}`, // Landing Hero APIs - GET_LANDING_HERO: "api/v1/settings/landing/hero", - UPDATE_LANDING_HERO: "api/v1/admin/settings/landing/hero", + GET_LANDING_HERO: "api/v1/settings/landing/hero", // non-admin + UPDATE_LANDING_HERO: "settings/landing/hero", // admin // Discount Codes APIs - GET_DISCOUNT_CODES: "api/v1/admin/discount/", - GET_DISCOUNT_CODE: (id: string) => `api/v1/admin/discount/${id}/`, - 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}/`, + GET_DISCOUNT_CODES: "discount/", + GET_DISCOUNT_CODE: (id: string) => `discount/${id}/`, + CREATE_DISCOUNT_CODE: "discount/", + UPDATE_DISCOUNT_CODE: (id: string) => `discount/${id}/`, + DELETE_DISCOUNT_CODE: (id: string) => `discount/${id}/`, // Orders APIs GET_ORDERS: "checkout/orders", GET_ORDER: (id: string) => `checkout/orders/${id}`, + GET_ORDER_STATS: "checkout/orders/stats", UPDATE_ORDER_STATUS: (id: string) => `checkout/orders/${id}/status`, // Shipping Methods APIs - GET_SHIPPING_METHODS: "api/v1/admin/checkout/shipping-methods", - GET_SHIPPING_METHOD: (id: string) => - `api/v1/admin/checkout/shipping-methods/${id}`, - CREATE_SHIPPING_METHOD: "api/v1/admin/checkout/shipping-methods", - UPDATE_SHIPPING_METHOD: (id: string) => - `api/v1/admin/checkout/shipping-methods/${id}`, - DELETE_SHIPPING_METHOD: (id: string) => - `api/v1/admin/checkout/shipping-methods/${id}`, + GET_SHIPPING_METHODS: "checkout/shipping-methods", + GET_SHIPPING_METHOD: (id: string) => `checkout/shipping-methods/${id}`, + CREATE_SHIPPING_METHOD: "checkout/shipping-methods", + UPDATE_SHIPPING_METHOD: (id: string) => `checkout/shipping-methods/${id}`, + DELETE_SHIPPING_METHOD: (id: string) => `checkout/shipping-methods/${id}`, // User Admin APIs - GET_USERS: "api/v1/admin/users", - GET_USER: (id: string) => `api/v1/admin/users/${id}`, - SEARCH_USERS: "api/v1/admin/users/search", - CREATE_USER: "api/v1/admin/users", - UPDATE_USER: (id: string) => `api/v1/admin/users/${id}`, - UPDATE_USER_PROFILE: (id: string) => `api/v1/admin/users/${id}/profile`, - UPDATE_USER_AVATAR: (id: string) => `api/v1/admin/users/${id}/avatar`, - DELETE_USER: (id: string) => `api/v1/admin/users/${id}`, - VERIFY_USER: (id: string) => `api/v1/admin/users/${id}/verify`, - UNVERIFY_USER: (id: string) => `api/v1/admin/users/${id}/unverify`, + GET_USERS: "users", + GET_USER: (id: string) => `users/${id}`, + SEARCH_USERS: "users/search", + CREATE_USER: "users", + UPDATE_USER: (id: string) => `users/${id}`, + UPDATE_USER_PROFILE: (id: string) => `users/${id}/profile`, + UPDATE_USER_AVATAR: (id: string) => `users/${id}/avatar`, + DELETE_USER: (id: string) => `users/${id}`, + VERIFY_USER: (id: string) => `users/${id}/verify`, + UNVERIFY_USER: (id: string) => `users/${id}/unverify`, }; diff --git a/src/pages/categories/core/_requests.ts b/src/pages/categories/core/_requests.ts index 563faf7..926d340 100644 --- a/src/pages/categories/core/_requests.ts +++ b/src/pages/categories/core/_requests.ts @@ -27,7 +27,7 @@ export const getCategories = async (filters?: CategoryFilters) => { if (filters?.limit) queryParams.limit = filters.limit; const response = await httpGetRequest( - APIUrlGenerator(API_ROUTES.GET_CATEGORIES, queryParams) + APIUrlGenerator(API_ROUTES.GET_CATEGORIES, queryParams, undefined, false) ); console.log("Categories API Response:", response); @@ -50,14 +50,14 @@ export const getCategories = async (filters?: CategoryFilters) => { export const getCategory = async (id: string) => { const response = await httpGetRequest( - APIUrlGenerator(API_ROUTES.GET_CATEGORY(id)) + APIUrlGenerator(API_ROUTES.GET_CATEGORY(id), undefined, undefined, false) ); return response.data.category; }; export const createCategory = async (data: CreateCategoryRequest) => { const response = await httpPostRequest( - APIUrlGenerator(API_ROUTES.CREATE_CATEGORY), + APIUrlGenerator(API_ROUTES.CREATE_CATEGORY, undefined, undefined, false), data ); return response.data.category; @@ -65,7 +65,12 @@ export const createCategory = async (data: CreateCategoryRequest) => { export const updateCategory = async (data: UpdateCategoryRequest) => { const response = await httpPutRequest( - APIUrlGenerator(API_ROUTES.UPDATE_CATEGORY(data.id.toString())), + APIUrlGenerator( + API_ROUTES.UPDATE_CATEGORY(data.id.toString()), + undefined, + undefined, + false + ), data ); return response.data.category; @@ -73,7 +78,7 @@ export const updateCategory = async (data: UpdateCategoryRequest) => { export const deleteCategory = async (id: string) => { const response = await httpDeleteRequest( - APIUrlGenerator(API_ROUTES.DELETE_CATEGORY(id)) + APIUrlGenerator(API_ROUTES.DELETE_CATEGORY(id), undefined, undefined, false) ); return response.data; }; diff --git a/src/pages/orders/core/_models.ts b/src/pages/orders/core/_models.ts index 7d5cddc..94e2602 100644 --- a/src/pages/orders/core/_models.ts +++ b/src/pages/orders/core/_models.ts @@ -62,25 +62,28 @@ export interface OrderCustomer { } 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; + 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; + final_total: number; + currency: string; + notes?: string; + tracking_number?: string; + estimated_delivery?: string; + created_at: string; + updated_at: string; + }; } export interface OrderFilters { @@ -88,7 +91,7 @@ export interface OrderFilters { page?: number; limit?: number; offset?: number; - + // Filter Parameters user_id?: number; invoice_id?: number; @@ -98,13 +101,13 @@ export interface OrderFilters { order_number?: string; transaction_id?: string; discount_code?: string; - + // Amount Range Parameters min_total?: number; max_total?: number; min_amount?: number; // legacy support max_amount?: number; // legacy support - + // Date Range Parameters created_from?: string; created_to?: string; @@ -112,7 +115,7 @@ export interface OrderFilters { updated_to?: string; date_from?: string; // legacy support date_to?: string; // legacy support - + // Search Parameter search?: string; } diff --git a/src/pages/orders/core/_requests.ts b/src/pages/orders/core/_requests.ts index dfacfdb..39739d2 100644 --- a/src/pages/orders/core/_requests.ts +++ b/src/pages/orders/core/_requests.ts @@ -17,35 +17,46 @@ export const getOrders = async (filters?: OrderFilters) => { const queryParams: Record = {}; // Pagination - if (filters?.page) queryParams.page = filters.page; if (filters?.limit) queryParams.limit = filters.limit; - if (filters?.offset) queryParams.offset = filters.offset; - + // Prefer offset; compute from page when provided + if (filters?.offset !== undefined && filters.offset !== null) { + queryParams.offset = filters.offset; + } else if (filters?.page) { + const effectiveLimit = filters?.limit || 20; + queryParams.offset = (filters.page - 1) * effectiveLimit; + } + // Filter Parameters if (filters?.user_id) queryParams.user_id = filters.user_id; if (filters?.invoice_id) queryParams.invoice_id = filters.invoice_id; if (filters?.status) queryParams.status = filters.status; - if (filters?.payment_status) queryParams.payment_status = filters.payment_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?.transaction_id) queryParams.transaction_id = filters.transaction_id; + if (filters?.transaction_id) + queryParams.transaction_id = filters.transaction_id; if (filters?.discount_code) queryParams.discount_code = filters.discount_code; - + // Amount Range Parameters (prefer new API naming) if (filters?.min_total) queryParams.min_total = filters.min_total; if (filters?.max_total) queryParams.max_total = filters.max_total; - if (filters?.min_amount && !filters?.min_total) queryParams.min_total = filters.min_amount; - if (filters?.max_amount && !filters?.max_total) queryParams.max_total = filters.max_amount; - + if (filters?.min_amount && !filters?.min_total) + queryParams.min_total = filters.min_amount; + if (filters?.max_amount && !filters?.max_total) + queryParams.max_total = filters.max_amount; + // Date Range Parameters (prefer new API naming) if (filters?.created_from) queryParams.created_from = filters.created_from; if (filters?.created_to) queryParams.created_to = filters.created_to; if (filters?.updated_from) queryParams.updated_from = filters.updated_from; if (filters?.updated_to) queryParams.updated_to = filters.updated_to; - if (filters?.date_from && !filters?.created_from) queryParams.created_from = filters.date_from; - if (filters?.date_to && !filters?.created_to) queryParams.created_to = filters.date_to; - - // Search Parameter + if (filters?.date_from && !filters?.created_from) + queryParams.created_from = filters.date_from; + if (filters?.date_to && !filters?.created_to) + queryParams.created_to = filters.date_to; + + // Search Parameter (per API spec) if (filters?.search) queryParams.search = filters.search; const response = await httpGetRequest( @@ -75,28 +86,10 @@ export const updateOrderStatus = async ( export const getOrderStats = async (): Promise => { try { - const ordersResponse = await getOrders({ limit: 20 }); - - 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; + const response = await httpGetRequest( + APIUrlGenerator(API_ROUTES.GET_ORDER_STATS) + ); + return response.data; } 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 index eb274c8..5ba7183 100644 --- a/src/pages/orders/order-detail/OrderDetailPage.tsx +++ b/src/pages/orders/order-detail/OrderDetailPage.tsx @@ -19,6 +19,7 @@ import { Mail, FileText } from 'lucide-react'; +import { englishToPersian } from '@/utils/numberUtils'; const getStatusColor = (status: OrderStatus) => { const colors = { @@ -64,9 +65,9 @@ const OrderDetailPage = () => { const [statusUpdateOpen, setStatusUpdateOpen] = useState(false); const [newStatus, setNewStatus] = useState('processing'); - const { data: order, isLoading, error } = useOrder(id || ''); + const { data, isLoading, error } = useOrder(id || ''); const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus(); - + const order = data?.order; const handleStatusUpdate = () => { if (id) { updateStatus( @@ -78,13 +79,13 @@ const OrderDetailPage = () => { const handleUpdateStatusClick = () => { if (order) { - setNewStatus(order.status); + setNewStatus(order?.status || 'pending'); setStatusUpdateOpen(true); } }; if (isLoading) return ; - + console.log(order) if (error || !order) { return ( @@ -103,9 +104,9 @@ const OrderDetailPage = () => { {/* هدر صفحه */}
- سفارش #{order.order_number} + سفارش #{order?.order_number || 'نامشخص'}

- تاریخ ثبت: {formatDate(order.created_at)} + تاریخ ثبت: {order?.created_at ? formatDate(order.created_at) : 'نامشخص'}

@@ -141,35 +142,51 @@ const OrderDetailPage = () => {
اطلاعات سفارش
- - {getStatusText(order.status)} + + {getStatusText(order?.status || 'pending')}
-
+

شماره سفارش

-

#{order.order_number}

+

#{order?.order_number || 'نامشخص'}

تاریخ ثبت

-

{formatDate(order.created_at)}

+

{order?.created_at ? formatDate(order.created_at) : 'نامشخص'}

- {order.tracking_number && ( +
+

شناسه فاکتور

+

{order?.invoice_id || 'نامشخص'}

+
+
+

شناسه کاربر

+

{order?.user_id || 'نامشخص'}

+
+
+

تاریخ آخرین بروزرسانی

+

{order?.updated_at ? formatDate(order.updated_at) : 'نامشخص'}

+
+
+

روش حمل و نقل

+

{order?.shipping_method_id || 'تعریف نشده'}

+
+ {order?.tracking_number && (

کد رهگیری

{order.tracking_number}

)} - {order.estimated_delivery && ( + {order?.estimated_delivery && (

تاریخ تحویل تخمینی

{formatDate(order.estimated_delivery)}

)}
- {order.notes && ( + {order?.notes && (

یادداشت

@@ -192,7 +209,7 @@ const OrderDetailPage = () => {

- {order.items.map((item) => ( + {order?.items && order.items.length > 0 ? order.items.map((item) => (
{item.product_image && ( { /> )}
-

{item.product_name}

- {item.variant_name && ( -

نوع: {item.variant_name}

- )} +

+ {item.product_name || `محصول شناسه: ${item.product_id}`} +

+

+ {item.product_variant_name || `واریانت شناسه: ${item.product_variant_id}`} +

+

+ شناسه آیتم: {item.id} +

تعداد: {item.quantity} - قیمت واحد: {formatCurrency(item.unit_price)} + قیمت واحد: {formatCurrency(item.unit_price || 0)} + + وزن: {item.weight || 0} کگ + + {item.final_weight && item.final_weight !== item.weight && ( + + وزن نهایی: {item.final_weight} کگ + + )}

- {formatCurrency(item.total_price)} + {formatCurrency(item.total_price || 0)}

- ))} + )) : ( +

+ محصولی در این سفارش یافت نشد +

+ )}
@@ -229,6 +263,51 @@ const OrderDetailPage = () => { {/* ستون جانبی */}
+ {/* اطلاعات کاربر سفارش‌دهنده */} +
+
+
+
+ +
+ کاربر سفارش‌دهنده +
+
+
+ {order?.user ? ( +
+
+

نام

+

+ {(order.user.first_name || 'نامشخص') + ' ' + (order.user.last_name || '')} +

+
+
+ +

{order.user.email || 'ایمیل نامشخص'}

+
+ {order.user.phone_number && ( +
+ +

+ {englishToPersian(order.user.phone_number)} +

+
+ )} +
+ + {order.user.verified ? 'تأیید شده' : 'تأیید نشده'} + +
+
+ ) : ( +

اطلاعات کاربر در دسترس نیست

+ )} +
+
{/* اطلاعات مشتری */}
@@ -240,24 +319,28 @@ const OrderDetailPage = () => {
-
-
-

نام

-

- {order.customer.first_name} {order.customer.last_name} -

-
-
- -

{order.customer.email}

-
- {order.customer.phone && ( -
- -

{order.customer.phone}

+ {order?.customer ? ( +
+
+

نام

+

+ {order.customer.first_name || 'نامشخص'} {order.customer.last_name || ''} +

- )} -
+
+ +

{order.customer.email || 'ایمیل نامشخص'}

+
+ {order.customer.phone && ( +
+ +

{order.customer.phone}

+
+ )} +
+ ) : ( +

اطلاعات مشتری در دسترس نیست

+ )}
@@ -275,23 +358,18 @@ const OrderDetailPage = () => {

آدرس ارسال

-

{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}

+

نام: {order?.shipping_address?.name || 'نام نامشخص'}

+

آدرس: {order?.shipping_address?.address || 'آدرس نامشخص'}

+

شهر: {order?.shipping_address?.city || 'شهر نامشخص'}, استان: {order?.shipping_address?.state || 'استان نامشخص'}

+

کشور: {order?.shipping_address?.country || 'کشور نامشخص'}

+

منطقه: {order?.shipping_address?.region || 'منطقه نامشخص'}

+

کد پستی: {order?.shipping_address?.postal_code || 'نامشخص'}

+ {order?.shipping_address?.plaque && ( +

پلاک: {order.shipping_address.plaque}, واحد: {order.shipping_address.unit || 'ندارد'}

+ )} + {order?.shipping_address?.receiving_address && ( +

آدرس تحویل: {order.shipping_address.receiving_address}

+ )}
@@ -310,48 +388,46 @@ const OrderDetailPage = () => {
جمع فرعی - {formatCurrency(order.subtotal)} + {formatCurrency(order?.net_total || 0)}
مالیات - {formatCurrency(order.tax_amount)} + {formatCurrency(order?.vat_total || 0)}
هزینه ارسال - {formatCurrency(order.shipping_amount)} + {formatCurrency(order?.shipping_total || 0)}
- {order.discount_amount > 0 && ( + {(order?.discount_total || 0) > 0 && (
تخفیف - -{formatCurrency(order.discount_amount)} + -{formatCurrency(order.discount_total)}
)}
- مجموع - {formatCurrency(order.total_amount)} + مجموع نهایی + {formatCurrency(order?.final_total || 0)}
-
- روش پرداخت - {order.payment.payment_method} -
-
- وضعیت پرداخت - - {order.payment.payment_status === 'paid' ? 'پرداخت شده' : 'در انتظار پرداخت'} - -
- {order.payment.transaction_id && ( -
- شماره تراکنش - {order.payment.transaction_id} +
+
+ وضعیت پرداخت + + {order?.payment_status === 'paid' ? 'پرداخت شده' : 'در انتظار پرداخت'} +
- )} + {order?.invoice_id && ( +
+ شماره فاکتور + {order.invoice_id} +
+ )} +
diff --git a/src/pages/orders/orders-list/OrdersListPage.tsx b/src/pages/orders/orders-list/OrdersListPage.tsx index a02e32b..218d8cd 100644 --- a/src/pages/orders/orders-list/OrdersListPage.tsx +++ b/src/pages/orders/orders-list/OrdersListPage.tsx @@ -1,4 +1,5 @@ import React, { useMemo, useState } from 'react'; +import { englishToPersian } from '@/utils/numberUtils'; import { useNavigate } from 'react-router-dom'; import { useOrders, useOrderStats, useUpdateOrderStatus } from '../core/_hooks'; import { Order, OrderFilters, OrderStatus } from '../core/_models'; @@ -72,29 +73,39 @@ const OrdersListPage = () => { }); const { data: ordersData, isLoading, error } = useOrders(filters); - const { data: stats, isLoading: statsLoading } = useOrderStats(!isLoading); + // Temporarily disabled stats API + // const { data: stats, isLoading: statsLoading } = useOrderStats(!isLoading); + const stats = null; + const statsLoading = false; const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus(); + const columns: TableColumn[] = useMemo(() => [ - { key: 'order_number', label: 'شماره سفارش', sortable: true, render: (v: string) => `#${v}` }, + { key: 'order_number', label: 'شماره سفارش', sortable: true, align: 'right', render: (v: string) => `#${v}` }, { key: 'customer', label: 'مشتری', + align: 'right', render: (_val, row: any) => ( -
-
{row.customer.first_name} {row.customer.last_name}
-
{row.customer.email}
+
+
+ {(row.user?.first_name || row.customer?.first_name || 'نامشخص')} {(row.user?.last_name || row.customer?.last_name || '')} +
+
+ {row.user?.phone_number ? englishToPersian(row.user.phone_number) : '-'} +
) }, - { key: 'total_amount', label: 'مبلغ', sortable: true, render: (v: number) => formatCurrency(v) }, - { key: 'status', label: 'وضعیت', render: (v: OrderStatus) => ({getStatusText(v)}) }, - { key: 'created_at', label: 'تاریخ', sortable: true, render: (v: string) => formatDate(v) }, + { key: 'final_total', label: 'مبلغ نهایی', sortable: true, align: 'right', render: (v: number, row: any) => formatCurrency(row.final_total || row.total_amount || 0) }, + { key: 'status', label: 'وضعیت', align: 'right', render: (v: OrderStatus) => ({getStatusText(v)}) }, + { key: 'created_at', label: 'تاریخ', sortable: true, align: 'right', render: (v: string) => formatDate(v) }, { key: 'actions', label: 'عملیات', + align: 'right', render: (_val, row: any) => ( -
+
@@ -184,7 +195,7 @@ const OrdersListPage = () => {

کل فروش

- {statsLoading ? '...' : formatCurrency(stats?.total_revenue || 0)} + --

@@ -198,7 +209,7 @@ const OrdersListPage = () => {

در انتظار

- {statsLoading ? '...' : (stats?.orders_by_status?.pending || 0)} + --

@@ -212,7 +223,7 @@ const OrdersListPage = () => {

میانگین سفارش

- {statsLoading ? '...' : formatCurrency(stats?.avg_order_value || 0)} + --

@@ -222,14 +233,14 @@ const OrdersListPage = () => { {/* فیلترها */}
-
- +
+ setFilters(prev => ({ ...prev, search: 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" + 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 placeholder-gray-500 dark:placeholder-gray-300" />
diff --git a/src/pages/products/core/_requests.ts b/src/pages/products/core/_requests.ts index 71f3a14..22d9a89 100644 --- a/src/pages/products/core/_requests.ts +++ b/src/pages/products/core/_requests.ts @@ -39,7 +39,7 @@ export const getProducts = async (filters?: ProductFilters) => { if (filters?.limit) queryParams.limit = filters.limit; const response = await httpGetRequest( - APIUrlGenerator(API_ROUTES.GET_PRODUCTS, queryParams) + APIUrlGenerator(API_ROUTES.GET_PRODUCTS, queryParams, undefined, false) ); console.log("Products API Response:", response); @@ -87,14 +87,14 @@ export const getProducts = async (filters?: ProductFilters) => { export const getProduct = async (id: string) => { const response = await httpGetRequest( - APIUrlGenerator(API_ROUTES.GET_PRODUCT(id)) + APIUrlGenerator(API_ROUTES.GET_PRODUCT(id), undefined, undefined, false) ); return response.data.product; }; export const createProduct = async (data: CreateProductRequest) => { const response = await httpPostRequest( - APIUrlGenerator(API_ROUTES.CREATE_PRODUCT), + APIUrlGenerator(API_ROUTES.CREATE_PRODUCT, undefined, undefined, false), data ); return response.data.product; @@ -102,7 +102,12 @@ export const createProduct = async (data: CreateProductRequest) => { export const updateProduct = async (data: UpdateProductRequest) => { const response = await httpPutRequest( - APIUrlGenerator(API_ROUTES.UPDATE_PRODUCT(data.id.toString())), + APIUrlGenerator( + API_ROUTES.UPDATE_PRODUCT(data.id.toString()), + undefined, + undefined, + false + ), data ); return response.data.product; @@ -110,7 +115,7 @@ export const updateProduct = async (data: UpdateProductRequest) => { export const deleteProduct = async (id: string) => { const response = await httpDeleteRequest( - APIUrlGenerator(API_ROUTES.DELETE_PRODUCT(id)) + APIUrlGenerator(API_ROUTES.DELETE_PRODUCT(id), undefined, undefined, false) ); return response.data; }; @@ -119,7 +124,12 @@ export const deleteProduct = async (id: string) => { export const getProductVariants = async (productId: string) => { try { const response = await httpGetRequest( - APIUrlGenerator(API_ROUTES.GET_PRODUCT_VARIANTS(productId)) + APIUrlGenerator( + API_ROUTES.GET_PRODUCT_VARIANTS(productId), + undefined, + undefined, + false + ) ); console.log("Product Variants API Response:", response); @@ -143,7 +153,10 @@ export const getProductVariants = async (productId: string) => { export const createProductVariant = async (data: CreateVariantRequest) => { const response = await httpPostRequest( APIUrlGenerator( - API_ROUTES.CREATE_PRODUCT_VARIANT(data.product_id?.toString() || "") + API_ROUTES.CREATE_PRODUCT_VARIANT(data.product_id?.toString() || ""), + undefined, + undefined, + false ), data ); @@ -152,7 +165,12 @@ export const createProductVariant = async (data: CreateVariantRequest) => { export const updateProductVariant = async (data: UpdateVariantRequest) => { const response = await httpPutRequest( - APIUrlGenerator(API_ROUTES.UPDATE_PRODUCT_VARIANT(data.id.toString())), + APIUrlGenerator( + API_ROUTES.UPDATE_PRODUCT_VARIANT(data.id.toString()), + undefined, + undefined, + false + ), data ); return response.data.variant; @@ -160,7 +178,12 @@ export const updateProductVariant = async (data: UpdateVariantRequest) => { export const deleteProductVariant = async (variantId: string) => { const response = await httpDeleteRequest( - APIUrlGenerator(API_ROUTES.DELETE_PRODUCT_VARIANT(variantId)) + APIUrlGenerator( + API_ROUTES.DELETE_PRODUCT_VARIANT(variantId), + undefined, + undefined, + false + ) ); return response.data; }; diff --git a/src/pages/users-admin/core/_requests.ts b/src/pages/users-admin/core/_requests.ts index bc3b4b2..359ce32 100644 --- a/src/pages/users-admin/core/_requests.ts +++ b/src/pages/users-admin/core/_requests.ts @@ -39,7 +39,9 @@ export const getUsers = async (filters?: UserFilters): Promise => { // Get user by ID export const getUser = async (id: string): Promise => { - const response = await httpGetRequest(API_ROUTES.GET_USER(id)); + const response = await httpGetRequest( + APIUrlGenerator(API_ROUTES.GET_USER(id)) + ); return response.data; }; @@ -70,7 +72,7 @@ export const createUser = async ( userData: CreateUserRequest ): Promise => { const response = await httpPostRequest( - API_ROUTES.CREATE_USER, + APIUrlGenerator(API_ROUTES.CREATE_USER), userData ); return response.data; @@ -82,7 +84,7 @@ export const updateUser = async ( userData: UpdateUserRequest ): Promise => { const response = await httpPutRequest( - API_ROUTES.UPDATE_USER(id), + APIUrlGenerator(API_ROUTES.UPDATE_USER(id)), userData ); return response.data; @@ -94,7 +96,7 @@ export const updateUserProfile = async ( userData: UpdateUserProfileRequest ): Promise => { const response = await httpPutRequest( - API_ROUTES.UPDATE_USER_PROFILE(id), + APIUrlGenerator(API_ROUTES.UPDATE_USER_PROFILE(id)), userData ); return response.data; @@ -106,7 +108,7 @@ export const updateUserAvatar = async ( avatarData: UpdateUserAvatarRequest ): Promise => { const response = await httpPutRequest( - API_ROUTES.UPDATE_USER_AVATAR(id), + APIUrlGenerator(API_ROUTES.UPDATE_USER_AVATAR(id)), avatarData ); return response.data; @@ -115,7 +117,7 @@ export const updateUserAvatar = async ( // Delete user export const deleteUser = async (id: string): Promise => { const response = await httpDeleteRequest( - API_ROUTES.DELETE_USER(id) + APIUrlGenerator(API_ROUTES.DELETE_USER(id)) ); return response.data; }; @@ -123,7 +125,7 @@ export const deleteUser = async (id: string): Promise => { // Verify user export const verifyUser = async (id: string): Promise => { const response = await httpPostRequest( - API_ROUTES.VERIFY_USER(id), + APIUrlGenerator(API_ROUTES.VERIFY_USER(id)), {} ); return response.data; @@ -132,7 +134,7 @@ export const verifyUser = async (id: string): Promise => { // Unverify user export const unverifyUser = async (id: string): Promise => { const response = await httpPostRequest( - API_ROUTES.UNVERIFY_USER(id), + APIUrlGenerator(API_ROUTES.UNVERIFY_USER(id)), {} ); return response.data; diff --git a/src/pages/users-admin/user-admin-detail/UserAdminDetailPage.tsx b/src/pages/users-admin/user-admin-detail/UserAdminDetailPage.tsx index ce21420..beaac25 100644 --- a/src/pages/users-admin/user-admin-detail/UserAdminDetailPage.tsx +++ b/src/pages/users-admin/user-admin-detail/UserAdminDetailPage.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { User, Edit, UserCheck, UserX, Trash2, ArrowLeft, Phone, Mail, CreditCard, Calendar } from 'lucide-react'; import { useUser, useVerifyUser, useUnverifyUser, useDeleteUser } from '../core/_hooks'; +import { englishToPersian } from '../../../utils/numberUtils'; import { PageContainer } from '../../../components/ui/Typography'; import { Button } from '../../../components/ui/Button'; import { Modal } from '../../../components/ui/Modal'; @@ -110,7 +111,7 @@ const UserAdminDetailPage: React.FC = () => {
@@ -264,57 +265,18 @@ const UserAdminDetailPage: React.FC = () => {
- {/* Actions Section */} -
-

- عملیات سریع -

-
- - -
-
+ {/* Delete Confirmation Modal */} setDeleteModal(false)} - title="حذف کاربر" + title={user.verified ? 'لغو تأیید کاربر' : 'تأیید کاربر'} >

- آیا از حذف کاربر "{user.first_name} {user.last_name}" اطمینان دارید؟ + {user.verified ? `آیا از لغو تأیید «${user.first_name} ${user.last_name}» مطمئن هستید؟` : `آیا از تأیید «${user.first_name} ${user.last_name}» مطمئن هستید؟`}

-
-

- هشدار: این عمل غیرقابل بازگشت است و تمام اطلاعات کاربر به طور کامل حذف خواهد شد. -

-
diff --git a/src/pages/users-admin/users-admin-list/UsersAdminListPage.tsx b/src/pages/users-admin/users-admin-list/UsersAdminListPage.tsx index cc1c1e5..0c8ca0a 100644 --- a/src/pages/users-admin/users-admin-list/UsersAdminListPage.tsx +++ b/src/pages/users-admin/users-admin-list/UsersAdminListPage.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Users, Plus, Search, Filter, UserCheck, UserX, Edit, Trash2, Eye, User as UserIcon } from 'lucide-react'; -import { useUsers, useUserStats, useVerifyUser, useUnverifyUser, useDeleteUser } from '../core/_hooks'; +import { useSearchUsers, useUserStats, useVerifyUser, useUnverifyUser, useDeleteUser } from '../core/_hooks'; import { User, UserFilters } from '../core/_models'; import { PageContainer } from '../../../components/ui/Typography'; import { Button } from '../../../components/ui/Button'; @@ -11,6 +11,7 @@ import { Pagination } from '../../../components/ui/Pagination'; import { StatsCard } from '../../../components/dashboard/StatsCard'; import { Table } from '../../../components/ui/Table'; import { TableColumn } from '../../../types'; +import { englishToPersian, persianToEnglish } from '../../../utils/numberUtils'; const UsersAdminListPage: React.FC = () => { const navigate = useNavigate(); @@ -26,18 +27,23 @@ const UsersAdminListPage: React.FC = () => { isOpen: false, user: null }); + const [verifyModal, setVerifyModal] = useState<{ isOpen: boolean; user: User | null; action: 'verify' | 'unverify' }>({ + isOpen: false, + user: null, + action: 'verify' + }); // Hooks - const { data: users = [], isLoading, error } = useUsers(filters); + const { data: searchResult, isLoading, error } = useSearchUsers(filters); + const users: any[] = searchResult?.users || []; + const totalMatched: number = searchResult?.total || 0; const { data: stats } = useUserStats(); const verifyUserMutation = useVerifyUser(); const unverifyUserMutation = useUnverifyUser(); const deleteUserMutation = useDeleteUser(); // Handlers - const handleCreate = () => { - navigate('/users-admin/create'); - }; + // Creation disabled per request const handleEdit = (user: User) => { navigate(`/users-admin/${user.id}/edit`); @@ -54,7 +60,7 @@ const UsersAdminListPage: React.FC = () => { }; if (searchTerm.trim()) { - newFilters.search_text = searchTerm.trim(); + newFilters.search_text = persianToEnglish(searchTerm.trim()); } else { delete newFilters.search_text; } @@ -78,10 +84,20 @@ const UsersAdminListPage: React.FC = () => { }; const handleVerifyToggle = (user: User) => { - if (user.verified) { - unverifyUserMutation.mutate(user.id.toString()); + setVerifyModal({ isOpen: true, user, action: user.verified ? 'unverify' : 'verify' }); + }; + + const handleVerifyConfirm = () => { + if (!verifyModal.user) return; + const user = verifyModal.user; + if (verifyModal.action === 'unverify') { + unverifyUserMutation.mutate(user.id.toString(), { + onSettled: () => setVerifyModal({ isOpen: false, user: null, action: 'verify' }) + }); } else { - verifyUserMutation.mutate(user.id.toString()); + verifyUserMutation.mutate(user.id.toString(), { + onSettled: () => setVerifyModal({ isOpen: false, user: null, action: 'verify' }) + }); } }; @@ -133,7 +149,15 @@ const UsersAdminListPage: React.FC = () => { ) }, - { key: 'phone_number', label: 'شماره تلفن', align: 'left' }, + { + key: 'phone_number', + label: 'شماره تلفن', + align: 'left', + render: (v: string) => { + const display = v ? englishToPersian(v) : '-'; + return {display}; + } + }, { key: 'email', label: 'ایمیل', align: 'left', render: (v: string) => v || '-' }, { key: 'verified', @@ -216,14 +240,7 @@ const UsersAdminListPage: React.FC = () => {

مشاهده و مدیریت کاربران سیستم

- + {/* Stats Cards */} @@ -309,9 +326,9 @@ const UsersAdminListPage: React.FC = () => { هیچ کاربری یافت نشد

- برای شروع یک کاربر ایجاد کنید + نتیجه‌ای یافت نشد

- + ) : ( @@ -322,10 +339,10 @@ const UsersAdminListPage: React.FC = () => { {users.length > 0 && ( handlePageChange((page - 1) * (filters.limit || 20))} itemsPerPage={filters.limit || 20} - totalItems={stats?.total_users || users.length} + totalItems={totalMatched || users.length} /> )} @@ -359,6 +376,36 @@ const UsersAdminListPage: React.FC = () => { + + {/* Verify/Unverify Confirmation Modal */} + setVerifyModal({ isOpen: false, user: null, action: 'verify' })} + title={verifyModal.action === 'unverify' ? 'لغو تأیید کاربر' : 'تأیید کاربر'} + > +
+

+ {verifyModal.action === 'unverify' + ? `آیا از لغو تأیید «${verifyModal.user?.first_name} ${verifyModal.user?.last_name}» مطمئن هستید؟` + : `آیا از تأیید «${verifyModal.user?.first_name} ${verifyModal.user?.last_name}» مطمئن هستید؟`} +

+
+ + +
+
+
); diff --git a/src/utils/baseHttpService.ts b/src/utils/baseHttpService.ts index fde8701..5a1d1c2 100644 --- a/src/utils/baseHttpService.ts +++ b/src/utils/baseHttpService.ts @@ -1,5 +1,9 @@ import axios, { AxiosRequestConfig } from "axios"; -import { API_GATE_WAY, REQUEST_TIMEOUT } from "@/constant/routes"; +import { + API_GATE_WAY, + ADMIN_API_PREFIX, + REQUEST_TIMEOUT, +} from "@/constant/routes"; import { getAuth } from "@/pages/auth"; import { pageSize } from "@/constant/generalVariables"; import Cookies from "js-cookie"; @@ -144,11 +148,13 @@ export const calculateTotalPages = (totalItemsCount: number | undefined) => { export function APIUrlGenerator( route: string, qry?: Record, - baseUrl?: string + baseUrl?: string, + useAdminPrefix: boolean = true ): string { const query = qry || {}; const queryKeys = Object.keys(query); - let apiUrl = `${baseUrl || API_GATE_WAY}/${route}`; + const prefix = useAdminPrefix ? `${ADMIN_API_PREFIX}/` : ""; + let apiUrl = `${baseUrl || API_GATE_WAY}/${prefix}${route}`; queryKeys.forEach((item, index) => { if (index === 0) { apiUrl += "?"; diff --git a/src/utils/numberUtils.ts b/src/utils/numberUtils.ts index 1d34a05..163d0dc 100644 --- a/src/utils/numberUtils.ts +++ b/src/utils/numberUtils.ts @@ -102,3 +102,14 @@ export const parseFormattedNumber = (value: any): number | undefined => { const num = Number(cleaned); return isNaN(num) ? undefined : num; }; + +export const englishToPersian = (str: string | number): string => { + if (str === null || str === undefined) return ""; + const en = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; + const fa = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"]; + let s = String(str); + for (let i = 0; i < 10; i++) { + s = s.replace(new RegExp(en[i], "g"), fa[i]); + } + return s; +};