feat(orders): enhance order detail and list pages with improved data handling and UI updates

This commit is contained in:
hosseintaromi 2025-09-26 12:16:58 +03:30
parent 07fd4e1d2d
commit dad0ff292d
13 changed files with 448 additions and 308 deletions

View File

@ -96,9 +96,9 @@ export const Table = ({ columns, data, loading = false }: TableProps) => {
column.align === 'center' && 'justify-center', column.align === 'center' && 'justify-center',
(!column.align || column.align === 'right') && 'justify-end' (!column.align || column.align === 'right') && 'justify-end'
)}> )}>
<span>{column.label}</span> <span style={{ width: '100%', textAlign: 'right' }}>{column.label}</span>
{column.sortable && ( {column.sortable && (
<div className="flex flex-col"> <div className="flex flex-col ml-1">
<ChevronUp <ChevronUp
className={clsx( className={clsx(
'h-3 w-3', 'h-3 w-3',

View File

@ -1,56 +1,57 @@
export const API_GATE_WAY = "https://apimznstg.aireview.ir"; export const API_GATE_WAY = "https://apimznstg.aireview.ir";
export const ADMIN_API_PREFIX = "api/v1/admin";
export const REQUEST_TIMEOUT = 30000; export const REQUEST_TIMEOUT = 30000;
export const API_ROUTES = { export const API_ROUTES = {
// Auth APIs // Auth APIs
ADMIN_LOGIN: "api/v1/admin/auth/login", ADMIN_LOGIN: "auth/login",
// Draft APIs // Draft APIs (non-admin)
GET_DISCOUNT_DETAIL: (id: string) => `api/v1/discount-drafts/${id}`, GET_DISCOUNT_DETAIL: (id: string) => `api/v1/discount-drafts/${id}`,
GET_DRAFT_DETAIL: (id: string) => `api/v1/drafts/${id}`, GET_DRAFT_DETAIL: (id: string) => `api/v1/drafts/${id}`,
// Admin Users APIs // Admin Users APIs
GET_ADMIN_USERS: "api/v1/admin/admin-users", GET_ADMIN_USERS: "admin-users",
GET_ADMIN_USER: (id: string) => `api/v1/admin/admin-users/${id}`, GET_ADMIN_USER: (id: string) => `admin-users/${id}`,
CREATE_ADMIN_USER: "api/v1/admin/admin-users", CREATE_ADMIN_USER: "admin-users",
UPDATE_ADMIN_USER: (id: string) => `api/v1/admin/admin-users/${id}`, UPDATE_ADMIN_USER: (id: string) => `admin-users/${id}`,
DELETE_ADMIN_USER: (id: string) => `api/v1/admin/admin-users/${id}`, DELETE_ADMIN_USER: (id: string) => `admin-users/${id}`,
// Roles APIs // Roles APIs
GET_ROLES: "api/v1/admin/roles", GET_ROLES: "roles",
GET_ROLE: (id: string) => `api/v1/admin/roles/${id}`, GET_ROLE: (id: string) => `roles/${id}`,
CREATE_ROLE: "api/v1/admin/roles", CREATE_ROLE: "roles",
UPDATE_ROLE: (id: string) => `api/v1/admin/roles/${id}`, UPDATE_ROLE: (id: string) => `roles/${id}`,
DELETE_ROLE: (id: string) => `api/v1/admin/roles/${id}`, DELETE_ROLE: (id: string) => `roles/${id}`,
GET_ROLE_PERMISSIONS: (id: string) => `api/v1/admin/roles/${id}/permissions`, GET_ROLE_PERMISSIONS: (id: string) => `roles/${id}/permissions`,
ASSIGN_ROLE_PERMISSION: (roleId: string, permissionId: string) => 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) => REMOVE_ROLE_PERMISSION: (roleId: string, permissionId: string) =>
`api/v1/admin/roles/${roleId}/permissions/${permissionId}`, `roles/${roleId}/permissions/${permissionId}`,
// Permissions APIs // Permissions APIs
GET_PERMISSIONS: "api/v1/admin/permissions", GET_PERMISSIONS: "permissions",
GET_PERMISSION: (id: string) => `api/v1/admin/permissions/${id}`, GET_PERMISSION: (id: string) => `permissions/${id}`,
CREATE_PERMISSION: "api/v1/admin/permissions", CREATE_PERMISSION: "permissions",
UPDATE_PERMISSION: (id: string) => `api/v1/admin/permissions/${id}`, UPDATE_PERMISSION: (id: string) => `permissions/${id}`,
DELETE_PERMISSION: (id: string) => `api/v1/admin/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_OPTIONS: "api/v1/product-options",
GET_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`, GET_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
CREATE_PRODUCT_OPTION: "api/v1/product-options", CREATE_PRODUCT_OPTION: "api/v1/product-options",
UPDATE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`, UPDATE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
DELETE_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_CATEGORIES: "api/v1/products/categories",
GET_CATEGORY: (id: string) => `api/v1/products/categories/${id}`, GET_CATEGORY: (id: string) => `api/v1/products/categories/${id}`,
CREATE_CATEGORY: "api/v1/products/categories", CREATE_CATEGORY: "api/v1/products/categories",
UPDATE_CATEGORY: (id: string) => `api/v1/products/categories/${id}`, UPDATE_CATEGORY: (id: string) => `api/v1/products/categories/${id}`,
DELETE_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_PRODUCTS: "api/v1/products",
GET_PRODUCT: (id: string) => `api/v1/products/${id}`, GET_PRODUCT: (id: string) => `api/v1/products/${id}`,
CREATE_PRODUCT: "api/v1/products", CREATE_PRODUCT: "api/v1/products",
@ -64,54 +65,52 @@ export const API_ROUTES = {
`api/v1/products/variants/${variantId}`, `api/v1/products/variants/${variantId}`,
// Files APIs // Files APIs
GET_FILES: "api/v1/admin/files", GET_FILES: "files",
UPLOAD_FILE: "api/v1/admin/files", UPLOAD_FILE: "files",
GET_FILE: (id: string) => `api/v1/admin/files/${id}`, GET_FILE: (id: string) => `files/${id}`,
UPDATE_FILE: (id: string) => `api/v1/admin/files/${id}`, UPDATE_FILE: (id: string) => `files/${id}`,
DELETE_FILE: (id: string) => `api/v1/admin/files/${id}`, DELETE_FILE: (id: string) => `files/${id}`,
DOWNLOAD_FILE: (serveKey: string) => `api/v1/files/${serveKey}`, DOWNLOAD_FILE: (serveKey: string) => `api/v1/files/${serveKey}`, // non-admin
// Images APIs // Images APIs (non-admin)
GET_IMAGES: "api/v1/images", GET_IMAGES: "api/v1/images",
CREATE_IMAGE: "api/v1/images", CREATE_IMAGE: "api/v1/images",
UPDATE_IMAGE: (imageId: string) => `api/v1/products/images/${imageId}`, UPDATE_IMAGE: (imageId: string) => `api/v1/products/images/${imageId}`,
DELETE_IMAGE: (imageId: string) => `api/v1/products/images/${imageId}`, DELETE_IMAGE: (imageId: string) => `api/v1/products/images/${imageId}`,
// Landing Hero APIs // Landing Hero APIs
GET_LANDING_HERO: "api/v1/settings/landing/hero", GET_LANDING_HERO: "api/v1/settings/landing/hero", // non-admin
UPDATE_LANDING_HERO: "api/v1/admin/settings/landing/hero", UPDATE_LANDING_HERO: "settings/landing/hero", // admin
// Discount Codes APIs // Discount Codes APIs
GET_DISCOUNT_CODES: "api/v1/admin/discount/", GET_DISCOUNT_CODES: "discount/",
GET_DISCOUNT_CODE: (id: string) => `api/v1/admin/discount/${id}/`, GET_DISCOUNT_CODE: (id: string) => `discount/${id}/`,
CREATE_DISCOUNT_CODE: "api/v1/admin/discount/", CREATE_DISCOUNT_CODE: "discount/",
UPDATE_DISCOUNT_CODE: (id: string) => `api/v1/admin/discount/${id}/`, UPDATE_DISCOUNT_CODE: (id: string) => `discount/${id}/`,
DELETE_DISCOUNT_CODE: (id: string) => `api/v1/admin/discount/${id}/`, DELETE_DISCOUNT_CODE: (id: string) => `discount/${id}/`,
// Orders APIs // Orders APIs
GET_ORDERS: "checkout/orders", GET_ORDERS: "checkout/orders",
GET_ORDER: (id: string) => `checkout/orders/${id}`, GET_ORDER: (id: string) => `checkout/orders/${id}`,
GET_ORDER_STATS: "checkout/orders/stats",
UPDATE_ORDER_STATUS: (id: string) => `checkout/orders/${id}/status`, UPDATE_ORDER_STATUS: (id: string) => `checkout/orders/${id}/status`,
// Shipping Methods APIs // Shipping Methods APIs
GET_SHIPPING_METHODS: "api/v1/admin/checkout/shipping-methods", GET_SHIPPING_METHODS: "checkout/shipping-methods",
GET_SHIPPING_METHOD: (id: string) => GET_SHIPPING_METHOD: (id: string) => `checkout/shipping-methods/${id}`,
`api/v1/admin/checkout/shipping-methods/${id}`, CREATE_SHIPPING_METHOD: "checkout/shipping-methods",
CREATE_SHIPPING_METHOD: "api/v1/admin/checkout/shipping-methods", UPDATE_SHIPPING_METHOD: (id: string) => `checkout/shipping-methods/${id}`,
UPDATE_SHIPPING_METHOD: (id: string) => DELETE_SHIPPING_METHOD: (id: string) => `checkout/shipping-methods/${id}`,
`api/v1/admin/checkout/shipping-methods/${id}`,
DELETE_SHIPPING_METHOD: (id: string) =>
`api/v1/admin/checkout/shipping-methods/${id}`,
// User Admin APIs // User Admin APIs
GET_USERS: "api/v1/admin/users", GET_USERS: "users",
GET_USER: (id: string) => `api/v1/admin/users/${id}`, GET_USER: (id: string) => `users/${id}`,
SEARCH_USERS: "api/v1/admin/users/search", SEARCH_USERS: "users/search",
CREATE_USER: "api/v1/admin/users", CREATE_USER: "users",
UPDATE_USER: (id: string) => `api/v1/admin/users/${id}`, UPDATE_USER: (id: string) => `users/${id}`,
UPDATE_USER_PROFILE: (id: string) => `api/v1/admin/users/${id}/profile`, UPDATE_USER_PROFILE: (id: string) => `users/${id}/profile`,
UPDATE_USER_AVATAR: (id: string) => `api/v1/admin/users/${id}/avatar`, UPDATE_USER_AVATAR: (id: string) => `users/${id}/avatar`,
DELETE_USER: (id: string) => `api/v1/admin/users/${id}`, DELETE_USER: (id: string) => `users/${id}`,
VERIFY_USER: (id: string) => `api/v1/admin/users/${id}/verify`, VERIFY_USER: (id: string) => `users/${id}/verify`,
UNVERIFY_USER: (id: string) => `api/v1/admin/users/${id}/unverify`, UNVERIFY_USER: (id: string) => `users/${id}/unverify`,
}; };

View File

@ -27,7 +27,7 @@ export const getCategories = async (filters?: CategoryFilters) => {
if (filters?.limit) queryParams.limit = filters.limit; if (filters?.limit) queryParams.limit = filters.limit;
const response = await httpGetRequest<CategoriesResponse>( const response = await httpGetRequest<CategoriesResponse>(
APIUrlGenerator(API_ROUTES.GET_CATEGORIES, queryParams) APIUrlGenerator(API_ROUTES.GET_CATEGORIES, queryParams, undefined, false)
); );
console.log("Categories API Response:", response); console.log("Categories API Response:", response);
@ -50,14 +50,14 @@ export const getCategories = async (filters?: CategoryFilters) => {
export const getCategory = async (id: string) => { export const getCategory = async (id: string) => {
const response = await httpGetRequest<CategoryResponse>( const response = await httpGetRequest<CategoryResponse>(
APIUrlGenerator(API_ROUTES.GET_CATEGORY(id)) APIUrlGenerator(API_ROUTES.GET_CATEGORY(id), undefined, undefined, false)
); );
return response.data.category; return response.data.category;
}; };
export const createCategory = async (data: CreateCategoryRequest) => { export const createCategory = async (data: CreateCategoryRequest) => {
const response = await httpPostRequest<CreateCategoryResponse>( const response = await httpPostRequest<CreateCategoryResponse>(
APIUrlGenerator(API_ROUTES.CREATE_CATEGORY), APIUrlGenerator(API_ROUTES.CREATE_CATEGORY, undefined, undefined, false),
data data
); );
return response.data.category; return response.data.category;
@ -65,7 +65,12 @@ export const createCategory = async (data: CreateCategoryRequest) => {
export const updateCategory = async (data: UpdateCategoryRequest) => { export const updateCategory = async (data: UpdateCategoryRequest) => {
const response = await httpPutRequest<UpdateCategoryResponse>( const response = await httpPutRequest<UpdateCategoryResponse>(
APIUrlGenerator(API_ROUTES.UPDATE_CATEGORY(data.id.toString())), APIUrlGenerator(
API_ROUTES.UPDATE_CATEGORY(data.id.toString()),
undefined,
undefined,
false
),
data data
); );
return response.data.category; return response.data.category;
@ -73,7 +78,7 @@ export const updateCategory = async (data: UpdateCategoryRequest) => {
export const deleteCategory = async (id: string) => { export const deleteCategory = async (id: string) => {
const response = await httpDeleteRequest<DeleteCategoryResponse>( const response = await httpDeleteRequest<DeleteCategoryResponse>(
APIUrlGenerator(API_ROUTES.DELETE_CATEGORY(id)) APIUrlGenerator(API_ROUTES.DELETE_CATEGORY(id), undefined, undefined, false)
); );
return response.data; return response.data;
}; };

View File

@ -62,25 +62,28 @@ export interface OrderCustomer {
} }
export interface Order { export interface Order {
id: number; order: {
order_number: string; id: number;
customer: OrderCustomer; order_number: string;
status: OrderStatus; customer: OrderCustomer;
items: OrderItem[]; status: OrderStatus;
billing_address: OrderAddress; items: OrderItem[];
shipping_address: OrderAddress; billing_address: OrderAddress;
payment: OrderPayment; shipping_address: OrderAddress;
subtotal: number; payment: OrderPayment;
tax_amount: number; subtotal: number;
shipping_amount: number; tax_amount: number;
discount_amount: number; shipping_amount: number;
total_amount: number; discount_amount: number;
currency: string; total_amount: number;
notes?: string; final_total: number;
tracking_number?: string; currency: string;
estimated_delivery?: string; notes?: string;
created_at: string; tracking_number?: string;
updated_at: string; estimated_delivery?: string;
created_at: string;
updated_at: string;
};
} }
export interface OrderFilters { export interface OrderFilters {
@ -88,7 +91,7 @@ export interface OrderFilters {
page?: number; page?: number;
limit?: number; limit?: number;
offset?: number; offset?: number;
// Filter Parameters // Filter Parameters
user_id?: number; user_id?: number;
invoice_id?: number; invoice_id?: number;
@ -98,13 +101,13 @@ export interface OrderFilters {
order_number?: string; order_number?: string;
transaction_id?: string; transaction_id?: string;
discount_code?: string; discount_code?: string;
// Amount Range Parameters // Amount Range Parameters
min_total?: number; min_total?: number;
max_total?: number; max_total?: number;
min_amount?: number; // legacy support min_amount?: number; // legacy support
max_amount?: number; // legacy support max_amount?: number; // legacy support
// Date Range Parameters // Date Range Parameters
created_from?: string; created_from?: string;
created_to?: string; created_to?: string;
@ -112,7 +115,7 @@ export interface OrderFilters {
updated_to?: string; updated_to?: string;
date_from?: string; // legacy support date_from?: string; // legacy support
date_to?: string; // legacy support date_to?: string; // legacy support
// Search Parameter // Search Parameter
search?: string; search?: string;
} }

View File

@ -17,35 +17,46 @@ export const getOrders = async (filters?: OrderFilters) => {
const queryParams: Record<string, string | number | null> = {}; const queryParams: Record<string, string | number | null> = {};
// Pagination // Pagination
if (filters?.page) queryParams.page = filters.page;
if (filters?.limit) queryParams.limit = filters.limit; 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 // Filter Parameters
if (filters?.user_id) queryParams.user_id = filters.user_id; if (filters?.user_id) queryParams.user_id = filters.user_id;
if (filters?.invoice_id) queryParams.invoice_id = filters.invoice_id; if (filters?.invoice_id) queryParams.invoice_id = filters.invoice_id;
if (filters?.status) queryParams.status = filters.status; 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?.customer_id) queryParams.customer_id = filters.customer_id;
if (filters?.order_number) queryParams.order_number = filters.order_number; 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; if (filters?.discount_code) queryParams.discount_code = filters.discount_code;
// Amount Range Parameters (prefer new API naming) // Amount Range Parameters (prefer new API naming)
if (filters?.min_total) queryParams.min_total = filters.min_total; if (filters?.min_total) queryParams.min_total = filters.min_total;
if (filters?.max_total) queryParams.max_total = filters.max_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?.min_amount && !filters?.min_total)
if (filters?.max_amount && !filters?.max_total) queryParams.max_total = filters.max_amount; 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) // Date Range Parameters (prefer new API naming)
if (filters?.created_from) queryParams.created_from = filters.created_from; if (filters?.created_from) queryParams.created_from = filters.created_from;
if (filters?.created_to) queryParams.created_to = filters.created_to; if (filters?.created_to) queryParams.created_to = filters.created_to;
if (filters?.updated_from) queryParams.updated_from = filters.updated_from; if (filters?.updated_from) queryParams.updated_from = filters.updated_from;
if (filters?.updated_to) queryParams.updated_to = filters.updated_to; 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_from && !filters?.created_from)
if (filters?.date_to && !filters?.created_to) queryParams.created_to = filters.date_to; queryParams.created_from = filters.date_from;
if (filters?.date_to && !filters?.created_to)
// Search Parameter queryParams.created_to = filters.date_to;
// Search Parameter (per API spec)
if (filters?.search) queryParams.search = filters.search; if (filters?.search) queryParams.search = filters.search;
const response = await httpGetRequest<PaginatedOrdersResponse>( const response = await httpGetRequest<PaginatedOrdersResponse>(
@ -75,28 +86,10 @@ export const updateOrderStatus = async (
export const getOrderStats = async (): Promise<OrderStats> => { export const getOrderStats = async (): Promise<OrderStats> => {
try { try {
const ordersResponse = await getOrders({ limit: 20 }); const response = await httpGetRequest<OrderStats>(
APIUrlGenerator(API_ROUTES.GET_ORDER_STATS)
const stats: OrderStats = { );
total_orders: ordersResponse.total, return response.data;
total_revenue: ordersResponse.orders.reduce(
(sum, order) => sum + order.total_amount,
0
),
orders_by_status: ordersResponse.orders.reduce((acc, order) => {
acc[order.status] = (acc[order.status] || 0) + 1;
return acc;
}, {} as Record<OrderStatus, number>),
avg_order_value:
ordersResponse.orders.length > 0
? ordersResponse.orders.reduce(
(sum, order) => sum + order.total_amount,
0
) / ordersResponse.orders.length
: 0,
};
return stats;
} catch (error) { } catch (error) {
console.error("Error fetching order stats:", error); console.error("Error fetching order stats:", error);
throw error; throw error;

View File

@ -19,6 +19,7 @@ import {
Mail, Mail,
FileText FileText
} from 'lucide-react'; } from 'lucide-react';
import { englishToPersian } from '@/utils/numberUtils';
const getStatusColor = (status: OrderStatus) => { const getStatusColor = (status: OrderStatus) => {
const colors = { const colors = {
@ -64,9 +65,9 @@ const OrderDetailPage = () => {
const [statusUpdateOpen, setStatusUpdateOpen] = useState(false); const [statusUpdateOpen, setStatusUpdateOpen] = useState(false);
const [newStatus, setNewStatus] = useState<OrderStatus>('processing'); const [newStatus, setNewStatus] = useState<OrderStatus>('processing');
const { data: order, isLoading, error } = useOrder(id || ''); const { data, isLoading, error } = useOrder(id || '');
const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus(); const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus();
const order = data?.order;
const handleStatusUpdate = () => { const handleStatusUpdate = () => {
if (id) { if (id) {
updateStatus( updateStatus(
@ -78,13 +79,13 @@ const OrderDetailPage = () => {
const handleUpdateStatusClick = () => { const handleUpdateStatusClick = () => {
if (order) { if (order) {
setNewStatus(order.status); setNewStatus(order?.status || 'pending');
setStatusUpdateOpen(true); setStatusUpdateOpen(true);
} }
}; };
if (isLoading) return <LoadingSpinner />; if (isLoading) return <LoadingSpinner />;
console.log(order)
if (error || !order) { if (error || !order) {
return ( return (
<PageContainer> <PageContainer>
@ -103,9 +104,9 @@ const OrderDetailPage = () => {
{/* هدر صفحه */} {/* هدر صفحه */}
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0"> <div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div> <div>
<PageTitle>سفارش #{order.order_number}</PageTitle> <PageTitle>سفارش #{order?.order_number || 'نامشخص'}</PageTitle>
<p className="text-gray-600 dark:text-gray-400 mt-1"> <p className="text-gray-600 dark:text-gray-400 mt-1">
تاریخ ثبت: {formatDate(order.created_at)} تاریخ ثبت: {order?.created_at ? formatDate(order.created_at) : 'نامشخص'}
</p> </p>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
@ -141,35 +142,51 @@ const OrderDetailPage = () => {
</div> </div>
<SectionTitle>اطلاعات سفارش</SectionTitle> <SectionTitle>اطلاعات سفارش</SectionTitle>
</div> </div>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(order.status)}`}> <span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(order?.status || 'pending')}`}>
{getStatusText(order.status)} {getStatusText(order?.status || 'pending')}
</span> </span>
</div> </div>
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">شماره سفارش</h4> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">شماره سفارش</h4>
<p className="text-gray-600 dark:text-gray-400">#{order.order_number}</p> <p className="text-gray-600 dark:text-gray-400">#{order?.order_number || 'نامشخص'}</p>
</div> </div>
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ ثبت</h4> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ ثبت</h4>
<p className="text-gray-600 dark:text-gray-400">{formatDate(order.created_at)}</p> <p className="text-gray-600 dark:text-gray-400">{order?.created_at ? formatDate(order.created_at) : 'نامشخص'}</p>
</div> </div>
{order.tracking_number && ( <div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">شناسه فاکتور</h4>
<p className="text-gray-600 dark:text-gray-400 font-mono">{order?.invoice_id || 'نامشخص'}</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">شناسه کاربر</h4>
<p className="text-gray-600 dark:text-gray-400">{order?.user_id || 'نامشخص'}</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ آخرین بروزرسانی</h4>
<p className="text-gray-600 dark:text-gray-400">{order?.updated_at ? formatDate(order.updated_at) : 'نامشخص'}</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">روش حمل و نقل</h4>
<p className="text-gray-600 dark:text-gray-400">{order?.shipping_method_id || 'تعریف نشده'}</p>
</div>
{order?.tracking_number && (
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">کد رهگیری</h4> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">کد رهگیری</h4>
<p className="text-gray-600 dark:text-gray-400 font-mono">{order.tracking_number}</p> <p className="text-gray-600 dark:text-gray-400 font-mono">{order.tracking_number}</p>
</div> </div>
)} )}
{order.estimated_delivery && ( {order?.estimated_delivery && (
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ تحویل تخمینی</h4> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ تحویل تخمینی</h4>
<p className="text-gray-600 dark:text-gray-400">{formatDate(order.estimated_delivery)}</p> <p className="text-gray-600 dark:text-gray-400">{formatDate(order.estimated_delivery)}</p>
</div> </div>
)} )}
</div> </div>
{order.notes && ( {order?.notes && (
<div className="mt-6"> <div className="mt-6">
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">یادداشت</h4> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">یادداشت</h4>
<p className="text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700 p-3 rounded-lg"> <p className="text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700 p-3 rounded-lg">
@ -192,7 +209,7 @@ const OrderDetailPage = () => {
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="space-y-4"> <div className="space-y-4">
{order.items.map((item) => ( {order?.items && order.items.length > 0 ? order.items.map((item) => (
<div key={item.id} className="flex items-center gap-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg"> <div key={item.id} className="flex items-center gap-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
{item.product_image && ( {item.product_image && (
<img <img
@ -202,26 +219,43 @@ const OrderDetailPage = () => {
/> />
)} )}
<div className="flex-1"> <div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-gray-100">{item.product_name}</h4> <h4 className="font-medium text-gray-900 dark:text-gray-100">
{item.variant_name && ( {item.product_name || `محصول شناسه: ${item.product_id}`}
<p className="text-sm text-gray-500 dark:text-gray-400">نوع: {item.variant_name}</p> </h4>
)} <p className="text-sm text-gray-500 dark:text-gray-400">
{item.product_variant_name || `واریانت شناسه: ${item.product_variant_id}`}
</p>
<p className="text-xs text-gray-400 dark:text-gray-500">
شناسه آیتم: {item.id}
</p>
<div className="flex items-center gap-4 mt-2"> <div className="flex items-center gap-4 mt-2">
<span className="text-sm text-gray-600 dark:text-gray-400"> <span className="text-sm text-gray-600 dark:text-gray-400">
تعداد: {item.quantity} تعداد: {item.quantity}
</span> </span>
<span className="text-sm text-gray-600 dark:text-gray-400"> <span className="text-sm text-gray-600 dark:text-gray-400">
قیمت واحد: {formatCurrency(item.unit_price)} قیمت واحد: {formatCurrency(item.unit_price || 0)}
</span> </span>
<span className="text-sm text-gray-600 dark:text-gray-400">
وزن: {item.weight || 0} کگ
</span>
{item.final_weight && item.final_weight !== item.weight && (
<span className="text-sm text-gray-600 dark:text-gray-400">
وزن نهایی: {item.final_weight} کگ
</span>
)}
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="font-medium text-gray-900 dark:text-gray-100"> <p className="font-medium text-gray-900 dark:text-gray-100">
{formatCurrency(item.total_price)} {formatCurrency(item.total_price || 0)}
</p> </p>
</div> </div>
</div> </div>
))} )) : (
<p className="text-gray-500 dark:text-gray-400 text-center py-4">
محصولی در این سفارش یافت نشد
</p>
)}
</div> </div>
</div> </div>
</div> </div>
@ -229,6 +263,51 @@ const OrderDetailPage = () => {
{/* ستون جانبی */} {/* ستون جانبی */}
<div className="space-y-8"> <div className="space-y-8">
{/* اطلاعات کاربر سفارش‌دهنده */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-indigo-50 to-blue-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-100 dark:bg-indigo-900 rounded-lg">
<User className="h-5 w-5 text-indigo-600 dark:text-indigo-300" />
</div>
<SectionTitle>کاربر سفارشدهنده</SectionTitle>
</div>
</div>
<div className="p-6">
{order?.user ? (
<div className="space-y-4">
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-1">نام</h4>
<p className="text-gray-600 dark:text-gray-400">
{(order.user.first_name || 'نامشخص') + ' ' + (order.user.last_name || '')}
</p>
</div>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400">{order.user.email || 'ایمیل نامشخص'}</p>
</div>
{order.user.phone_number && (
<div className="flex items-center gap-2">
<Phone className="h-4 w-4 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400 font-mono" dir="ltr" style={{ direction: 'ltr' }}>
{englishToPersian(order.user.phone_number)}
</p>
</div>
)}
<div className="flex items-center gap-2">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${order.user.verified
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'}`}
>
{order.user.verified ? 'تأیید شده' : 'تأیید نشده'}
</span>
</div>
</div>
) : (
<p className="text-gray-500 dark:text-gray-400">اطلاعات کاربر در دسترس نیست</p>
)}
</div>
</div>
{/* اطلاعات مشتری */} {/* اطلاعات مشتری */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-purple-50 to-pink-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700"> <div className="bg-gradient-to-r from-purple-50 to-pink-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
@ -240,24 +319,28 @@ const OrderDetailPage = () => {
</div> </div>
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="space-y-4"> {order?.customer ? (
<div> <div className="space-y-4">
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-1">نام</h4> <div>
<p className="text-gray-600 dark:text-gray-400"> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-1">نام</h4>
{order.customer.first_name} {order.customer.last_name} <p className="text-gray-600 dark:text-gray-400">
</p> {order.customer.first_name || 'نامشخص'} {order.customer.last_name || ''}
</div> </p>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400">{order.customer.email}</p>
</div>
{order.customer.phone && (
<div className="flex items-center gap-2">
<Phone className="h-4 w-4 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400" dir="ltr">{order.customer.phone}</p>
</div> </div>
)} <div className="flex items-center gap-2">
</div> <Mail className="h-4 w-4 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400">{order.customer.email || 'ایمیل نامشخص'}</p>
</div>
{order.customer.phone && (
<div className="flex items-center gap-2">
<Phone className="h-4 w-4 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400" dir="ltr">{order.customer.phone}</p>
</div>
)}
</div>
) : (
<p className="text-gray-500 dark:text-gray-400">اطلاعات مشتری در دسترس نیست</p>
)}
</div> </div>
</div> </div>
@ -275,23 +358,18 @@ const OrderDetailPage = () => {
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">آدرس ارسال</h4> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">آدرس ارسال</h4>
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-1"> <div className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<p>{order.shipping_address.first_name} {order.shipping_address.last_name}</p> <p><strong>نام:</strong> {order?.shipping_address?.name || 'نام نامشخص'}</p>
<p>{order.shipping_address.address_line_1}</p> <p><strong>آدرس:</strong> {order?.shipping_address?.address || 'آدرس نامشخص'}</p>
{order.shipping_address.address_line_2 && <p>{order.shipping_address.address_line_2}</p>} <p><strong>شهر:</strong> {order?.shipping_address?.city || 'شهر نامشخص'}, <strong>استان:</strong> {order?.shipping_address?.state || 'استان نامشخص'}</p>
<p>{order.shipping_address.city}, {order.shipping_address.state}</p> <p><strong>کشور:</strong> {order?.shipping_address?.country || 'کشور نامشخص'}</p>
<p>کد پستی: {order.shipping_address.postal_code}</p> <p><strong>منطقه:</strong> {order?.shipping_address?.region || 'منطقه نامشخص'}</p>
{order.shipping_address.phone && <p>تلفن: {order.shipping_address.phone}</p>} <p><strong>کد پستی:</strong> {order?.shipping_address?.postal_code || 'نامشخص'}</p>
</div> {order?.shipping_address?.plaque && (
</div> <p><strong>پلاک:</strong> {order.shipping_address.plaque}, <strong>واحد:</strong> {order.shipping_address.unit || 'ندارد'}</p>
<hr className="border-gray-200 dark:border-gray-700" /> )}
<div> {order?.shipping_address?.receiving_address && (
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">آدرس صورتحساب</h4> <p><strong>آدرس تحویل:</strong> {order.shipping_address.receiving_address}</p>
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-1"> )}
<p>{order.billing_address.first_name} {order.billing_address.last_name}</p>
<p>{order.billing_address.address_line_1}</p>
{order.billing_address.address_line_2 && <p>{order.billing_address.address_line_2}</p>}
<p>{order.billing_address.city}, {order.billing_address.state}</p>
<p>کد پستی: {order.billing_address.postal_code}</p>
</div> </div>
</div> </div>
</div> </div>
@ -310,48 +388,46 @@ const OrderDetailPage = () => {
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">جمع فرعی</span> <span className="text-gray-600 dark:text-gray-400">جمع فرعی</span>
<span className="font-medium">{formatCurrency(order.subtotal)}</span> <span className="font-medium">{formatCurrency(order?.net_total || 0)}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">مالیات</span> <span className="text-gray-600 dark:text-gray-400">مالیات</span>
<span className="font-medium">{formatCurrency(order.tax_amount)}</span> <span className="font-medium">{formatCurrency(order?.vat_total || 0)}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">هزینه ارسال</span> <span className="text-gray-600 dark:text-gray-400">هزینه ارسال</span>
<span className="font-medium">{formatCurrency(order.shipping_amount)}</span> <span className="font-medium">{formatCurrency(order?.shipping_total || 0)}</span>
</div> </div>
{order.discount_amount > 0 && ( {(order?.discount_total || 0) > 0 && (
<div className="flex justify-between text-green-600 dark:text-green-400"> <div className="flex justify-between text-green-600 dark:text-green-400">
<span>تخفیف</span> <span>تخفیف</span>
<span className="font-medium">-{formatCurrency(order.discount_amount)}</span> <span className="font-medium">-{formatCurrency(order.discount_total)}</span>
</div> </div>
)} )}
<hr className="border-gray-200 dark:border-gray-700" /> <hr className="border-gray-200 dark:border-gray-700" />
<div className="flex justify-between text-lg font-bold"> <div className="flex justify-between text-lg font-bold">
<span>مجموع</span> <span>مجموع نهایی</span>
<span>{formatCurrency(order.total_amount)}</span> <span>{formatCurrency(order?.final_total || 0)}</span>
</div> </div>
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700"> <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex justify-between mb-2"> <div className="space-y-2">
<span className="text-sm text-gray-600 dark:text-gray-400">روش پرداخت</span> <div className="flex justify-between">
<span className="text-sm font-medium">{order.payment.payment_method}</span> <span className="text-sm text-gray-600 dark:text-gray-400">وضعیت پرداخت</span>
</div> <span className={`text-sm font-medium px-2 py-1 rounded-full ${order?.payment_status === 'paid'
<div className="flex justify-between"> ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
<span className="text-sm text-gray-600 dark:text-gray-400">وضعیت پرداخت</span> : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
<span className={`text-sm font-medium ${order.payment.payment_status === 'paid' }`}>
? 'text-green-600 dark:text-green-400' {order?.payment_status === 'paid' ? 'پرداخت شده' : 'در انتظار پرداخت'}
: 'text-yellow-600 dark:text-yellow-400' </span>
}`}>
{order.payment.payment_status === 'paid' ? 'پرداخت شده' : 'در انتظار پرداخت'}
</span>
</div>
{order.payment.transaction_id && (
<div className="flex justify-between mt-2">
<span className="text-sm text-gray-600 dark:text-gray-400">شماره تراکنش</span>
<span className="text-sm font-mono">{order.payment.transaction_id}</span>
</div> </div>
)} {order?.invoice_id && (
<div className="flex justify-between">
<span className="text-sm text-gray-600 dark:text-gray-400">شماره فاکتور</span>
<span className="text-sm font-mono text-gray-900 dark:text-gray-100">{order.invoice_id}</span>
</div>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,5 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { englishToPersian } from '@/utils/numberUtils';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useOrders, useOrderStats, useUpdateOrderStatus } from '../core/_hooks'; import { useOrders, useOrderStats, useUpdateOrderStatus } from '../core/_hooks';
import { Order, OrderFilters, OrderStatus } from '../core/_models'; import { Order, OrderFilters, OrderStatus } from '../core/_models';
@ -72,29 +73,39 @@ const OrdersListPage = () => {
}); });
const { data: ordersData, isLoading, error } = useOrders(filters); 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 { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus();
const columns: TableColumn[] = useMemo(() => [ 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', key: 'customer',
label: 'مشتری', label: 'مشتری',
align: 'right',
render: (_val, row: any) => ( render: (_val, row: any) => (
<div> <div className="text-right">
<div className="font-medium">{row.customer.first_name} {row.customer.last_name}</div> <div className="font-medium">
<div className="text-gray-500 dark:text-gray-400">{row.customer.email}</div> {(row.user?.first_name || row.customer?.first_name || 'نامشخص')} {(row.user?.last_name || row.customer?.last_name || '')}
</div>
<div className="text-gray-500 dark:text-gray-400" dir="ltr" style={{ direction: 'ltr' }}>
{row.user?.phone_number ? englishToPersian(row.user.phone_number) : '-'}
</div>
</div> </div>
) )
}, },
{ key: 'total_amount', label: 'مبلغ', sortable: true, render: (v: number) => formatCurrency(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: 'وضعیت', render: (v: OrderStatus) => (<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(v)}`}>{getStatusText(v)}</span>) }, { key: 'status', label: 'وضعیت', align: 'right', render: (v: OrderStatus) => (<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(v)}`}>{getStatusText(v)}</span>) },
{ key: 'created_at', label: 'تاریخ', sortable: true, render: (v: string) => formatDate(v) }, { key: 'created_at', label: 'تاریخ', sortable: true, align: 'right', render: (v: string) => formatDate(v) },
{ {
key: 'actions', key: 'actions',
label: 'عملیات', label: 'عملیات',
align: 'right',
render: (_val, row: any) => ( render: (_val, row: any) => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 justify-end">
<button <button
onClick={() => handleViewOrder(row.id)} onClick={() => handleViewOrder(row.id)}
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300" className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
@ -170,7 +181,7 @@ const OrdersListPage = () => {
<div className="mr-4"> <div className="mr-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">کل سفارشات</p> <p className="text-sm font-medium text-gray-600 dark:text-gray-400">کل سفارشات</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100"> <p className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{statsLoading ? '...' : stats?.total_orders?.toLocaleString('fa-IR') || '0'} --
</p> </p>
</div> </div>
</div> </div>
@ -184,7 +195,7 @@ const OrdersListPage = () => {
<div className="mr-4"> <div className="mr-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">کل فروش</p> <p className="text-sm font-medium text-gray-600 dark:text-gray-400">کل فروش</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100"> <p className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{statsLoading ? '...' : formatCurrency(stats?.total_revenue || 0)} --
</p> </p>
</div> </div>
</div> </div>
@ -198,7 +209,7 @@ const OrdersListPage = () => {
<div className="mr-4"> <div className="mr-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">در انتظار</p> <p className="text-sm font-medium text-gray-600 dark:text-gray-400">در انتظار</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100"> <p className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{statsLoading ? '...' : (stats?.orders_by_status?.pending || 0)} --
</p> </p>
</div> </div>
</div> </div>
@ -212,7 +223,7 @@ const OrdersListPage = () => {
<div className="mr-4"> <div className="mr-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">میانگین سفارش</p> <p className="text-sm font-medium text-gray-600 dark:text-gray-400">میانگین سفارش</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100"> <p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{statsLoading ? '...' : formatCurrency(stats?.avg_order_value || 0)} --
</p> </p>
</div> </div>
</div> </div>
@ -222,14 +233,14 @@ const OrdersListPage = () => {
{/* فیلترها */} {/* فیلترها */}
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-6"> <div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="relative"> <div className="relative col-span-1 md:col-span-2 lg:col-span-4 order-first">
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" /> <Search className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-gray-300" />
<input <input
type="text" type="text"
placeholder="جستجو عمومی (شماره سفارش، کد تراکنش، کد تخفیف)..." placeholder="جستجو عمومی (شماره سفارش، کد تراکنش، کد تخفیف)..."
value={filters.search || ''} value={filters.search || ''}
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value, page: 1 }))} onChange={(e) => 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"
/> />
</div> </div>

View File

@ -39,7 +39,7 @@ export const getProducts = async (filters?: ProductFilters) => {
if (filters?.limit) queryParams.limit = filters.limit; if (filters?.limit) queryParams.limit = filters.limit;
const response = await httpGetRequest<ProductsResponse>( const response = await httpGetRequest<ProductsResponse>(
APIUrlGenerator(API_ROUTES.GET_PRODUCTS, queryParams) APIUrlGenerator(API_ROUTES.GET_PRODUCTS, queryParams, undefined, false)
); );
console.log("Products API Response:", response); console.log("Products API Response:", response);
@ -87,14 +87,14 @@ export const getProducts = async (filters?: ProductFilters) => {
export const getProduct = async (id: string) => { export const getProduct = async (id: string) => {
const response = await httpGetRequest<ProductResponse>( const response = await httpGetRequest<ProductResponse>(
APIUrlGenerator(API_ROUTES.GET_PRODUCT(id)) APIUrlGenerator(API_ROUTES.GET_PRODUCT(id), undefined, undefined, false)
); );
return response.data.product; return response.data.product;
}; };
export const createProduct = async (data: CreateProductRequest) => { export const createProduct = async (data: CreateProductRequest) => {
const response = await httpPostRequest<CreateProductResponse>( const response = await httpPostRequest<CreateProductResponse>(
APIUrlGenerator(API_ROUTES.CREATE_PRODUCT), APIUrlGenerator(API_ROUTES.CREATE_PRODUCT, undefined, undefined, false),
data data
); );
return response.data.product; return response.data.product;
@ -102,7 +102,12 @@ export const createProduct = async (data: CreateProductRequest) => {
export const updateProduct = async (data: UpdateProductRequest) => { export const updateProduct = async (data: UpdateProductRequest) => {
const response = await httpPutRequest<UpdateProductResponse>( const response = await httpPutRequest<UpdateProductResponse>(
APIUrlGenerator(API_ROUTES.UPDATE_PRODUCT(data.id.toString())), APIUrlGenerator(
API_ROUTES.UPDATE_PRODUCT(data.id.toString()),
undefined,
undefined,
false
),
data data
); );
return response.data.product; return response.data.product;
@ -110,7 +115,7 @@ export const updateProduct = async (data: UpdateProductRequest) => {
export const deleteProduct = async (id: string) => { export const deleteProduct = async (id: string) => {
const response = await httpDeleteRequest<DeleteProductResponse>( const response = await httpDeleteRequest<DeleteProductResponse>(
APIUrlGenerator(API_ROUTES.DELETE_PRODUCT(id)) APIUrlGenerator(API_ROUTES.DELETE_PRODUCT(id), undefined, undefined, false)
); );
return response.data; return response.data;
}; };
@ -119,7 +124,12 @@ export const deleteProduct = async (id: string) => {
export const getProductVariants = async (productId: string) => { export const getProductVariants = async (productId: string) => {
try { try {
const response = await httpGetRequest<ProductVariantsResponse>( const response = await httpGetRequest<ProductVariantsResponse>(
APIUrlGenerator(API_ROUTES.GET_PRODUCT_VARIANTS(productId)) APIUrlGenerator(
API_ROUTES.GET_PRODUCT_VARIANTS(productId),
undefined,
undefined,
false
)
); );
console.log("Product Variants API Response:", response); console.log("Product Variants API Response:", response);
@ -143,7 +153,10 @@ export const getProductVariants = async (productId: string) => {
export const createProductVariant = async (data: CreateVariantRequest) => { export const createProductVariant = async (data: CreateVariantRequest) => {
const response = await httpPostRequest<CreateVariantResponse>( const response = await httpPostRequest<CreateVariantResponse>(
APIUrlGenerator( APIUrlGenerator(
API_ROUTES.CREATE_PRODUCT_VARIANT(data.product_id?.toString() || "") API_ROUTES.CREATE_PRODUCT_VARIANT(data.product_id?.toString() || ""),
undefined,
undefined,
false
), ),
data data
); );
@ -152,7 +165,12 @@ export const createProductVariant = async (data: CreateVariantRequest) => {
export const updateProductVariant = async (data: UpdateVariantRequest) => { export const updateProductVariant = async (data: UpdateVariantRequest) => {
const response = await httpPutRequest<UpdateVariantResponse>( const response = await httpPutRequest<UpdateVariantResponse>(
APIUrlGenerator(API_ROUTES.UPDATE_PRODUCT_VARIANT(data.id.toString())), APIUrlGenerator(
API_ROUTES.UPDATE_PRODUCT_VARIANT(data.id.toString()),
undefined,
undefined,
false
),
data data
); );
return response.data.variant; return response.data.variant;
@ -160,7 +178,12 @@ export const updateProductVariant = async (data: UpdateVariantRequest) => {
export const deleteProductVariant = async (variantId: string) => { export const deleteProductVariant = async (variantId: string) => {
const response = await httpDeleteRequest<DeleteVariantResponse>( const response = await httpDeleteRequest<DeleteVariantResponse>(
APIUrlGenerator(API_ROUTES.DELETE_PRODUCT_VARIANT(variantId)) APIUrlGenerator(
API_ROUTES.DELETE_PRODUCT_VARIANT(variantId),
undefined,
undefined,
false
)
); );
return response.data; return response.data;
}; };

View File

@ -39,7 +39,9 @@ export const getUsers = async (filters?: UserFilters): Promise<User[]> => {
// Get user by ID // Get user by ID
export const getUser = async (id: string): Promise<User> => { export const getUser = async (id: string): Promise<User> => {
const response = await httpGetRequest<User>(API_ROUTES.GET_USER(id)); const response = await httpGetRequest<User>(
APIUrlGenerator(API_ROUTES.GET_USER(id))
);
return response.data; return response.data;
}; };
@ -70,7 +72,7 @@ export const createUser = async (
userData: CreateUserRequest userData: CreateUserRequest
): Promise<User> => { ): Promise<User> => {
const response = await httpPostRequest<User>( const response = await httpPostRequest<User>(
API_ROUTES.CREATE_USER, APIUrlGenerator(API_ROUTES.CREATE_USER),
userData userData
); );
return response.data; return response.data;
@ -82,7 +84,7 @@ export const updateUser = async (
userData: UpdateUserRequest userData: UpdateUserRequest
): Promise<User> => { ): Promise<User> => {
const response = await httpPutRequest<User>( const response = await httpPutRequest<User>(
API_ROUTES.UPDATE_USER(id), APIUrlGenerator(API_ROUTES.UPDATE_USER(id)),
userData userData
); );
return response.data; return response.data;
@ -94,7 +96,7 @@ export const updateUserProfile = async (
userData: UpdateUserProfileRequest userData: UpdateUserProfileRequest
): Promise<User> => { ): Promise<User> => {
const response = await httpPutRequest<User>( const response = await httpPutRequest<User>(
API_ROUTES.UPDATE_USER_PROFILE(id), APIUrlGenerator(API_ROUTES.UPDATE_USER_PROFILE(id)),
userData userData
); );
return response.data; return response.data;
@ -106,7 +108,7 @@ export const updateUserAvatar = async (
avatarData: UpdateUserAvatarRequest avatarData: UpdateUserAvatarRequest
): Promise<UserActionResponse> => { ): Promise<UserActionResponse> => {
const response = await httpPutRequest<UserActionResponse>( const response = await httpPutRequest<UserActionResponse>(
API_ROUTES.UPDATE_USER_AVATAR(id), APIUrlGenerator(API_ROUTES.UPDATE_USER_AVATAR(id)),
avatarData avatarData
); );
return response.data; return response.data;
@ -115,7 +117,7 @@ export const updateUserAvatar = async (
// Delete user // Delete user
export const deleteUser = async (id: string): Promise<UserActionResponse> => { export const deleteUser = async (id: string): Promise<UserActionResponse> => {
const response = await httpDeleteRequest<UserActionResponse>( const response = await httpDeleteRequest<UserActionResponse>(
API_ROUTES.DELETE_USER(id) APIUrlGenerator(API_ROUTES.DELETE_USER(id))
); );
return response.data; return response.data;
}; };
@ -123,7 +125,7 @@ export const deleteUser = async (id: string): Promise<UserActionResponse> => {
// Verify user // Verify user
export const verifyUser = async (id: string): Promise<UserActionResponse> => { export const verifyUser = async (id: string): Promise<UserActionResponse> => {
const response = await httpPostRequest<UserActionResponse>( const response = await httpPostRequest<UserActionResponse>(
API_ROUTES.VERIFY_USER(id), APIUrlGenerator(API_ROUTES.VERIFY_USER(id)),
{} {}
); );
return response.data; return response.data;
@ -132,7 +134,7 @@ export const verifyUser = async (id: string): Promise<UserActionResponse> => {
// Unverify user // Unverify user
export const unverifyUser = async (id: string): Promise<UserActionResponse> => { export const unverifyUser = async (id: string): Promise<UserActionResponse> => {
const response = await httpPostRequest<UserActionResponse>( const response = await httpPostRequest<UserActionResponse>(
API_ROUTES.UNVERIFY_USER(id), APIUrlGenerator(API_ROUTES.UNVERIFY_USER(id)),
{} {}
); );
return response.data; return response.data;

View File

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { User, Edit, UserCheck, UserX, Trash2, ArrowLeft, Phone, Mail, CreditCard, Calendar } from 'lucide-react'; import { User, Edit, UserCheck, UserX, Trash2, ArrowLeft, Phone, Mail, CreditCard, Calendar } from 'lucide-react';
import { useUser, useVerifyUser, useUnverifyUser, useDeleteUser } from '../core/_hooks'; import { useUser, useVerifyUser, useUnverifyUser, useDeleteUser } from '../core/_hooks';
import { englishToPersian } from '../../../utils/numberUtils';
import { PageContainer } from '../../../components/ui/Typography'; import { PageContainer } from '../../../components/ui/Typography';
import { Button } from '../../../components/ui/Button'; import { Button } from '../../../components/ui/Button';
import { Modal } from '../../../components/ui/Modal'; import { Modal } from '../../../components/ui/Modal';
@ -110,7 +111,7 @@ const UserAdminDetailPage: React.FC = () => {
</Button> </Button>
<Button <Button
variant={user.verified ? "secondary" : "primary"} variant={user.verified ? "secondary" : "primary"}
onClick={handleVerifyToggle} onClick={() => setDeleteModal(true)}
loading={verifyUserMutation.isPending || unverifyUserMutation.isPending} loading={verifyUserMutation.isPending || unverifyUserMutation.isPending}
className="flex items-center gap-2" className="flex items-center gap-2"
data-testid="verify-toggle-button" data-testid="verify-toggle-button"
@ -195,7 +196,7 @@ const UserAdminDetailPage: React.FC = () => {
<Phone className="h-5 w-5 text-gray-400" /> <Phone className="h-5 w-5 text-gray-400" />
<div> <div>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">شماره تلفن</p> <p className="text-sm font-medium text-gray-500 dark:text-gray-400">شماره تلفن</p>
<p className="text-gray-900 dark:text-gray-100">{user.phone_number}</p> <p className="text-gray-900 dark:text-gray-100" dir="ltr" style={{ direction: 'ltr' }}>{englishToPersian(user.phone_number)}</p>
</div> </div>
</div> </div>
@ -264,57 +265,18 @@ const UserAdminDetailPage: React.FC = () => {
</div> </div>
</div> </div>
{/* Actions Section */}
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
عملیات سریع
</h3>
<div className="flex flex-wrap gap-3">
<Button
onClick={handleEdit}
className="flex items-center gap-2"
data-testid="quick-edit-button"
>
<Edit className="h-4 w-4" />
ویرایش اطلاعات
</Button>
<Button
variant={user.verified ? "secondary" : "primary"}
onClick={handleVerifyToggle}
loading={verifyUserMutation.isPending || unverifyUserMutation.isPending}
className="flex items-center gap-2"
data-testid="quick-verify-button"
>
{user.verified ? (
<>
<UserX className="h-4 w-4" />
لغو تأیید حساب
</>
) : (
<>
<UserCheck className="h-4 w-4" />
تأیید حساب کاربر
</>
)}
</Button>
</div>
</div>
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
<Modal <Modal
isOpen={deleteModal} isOpen={deleteModal}
onClose={() => setDeleteModal(false)} onClose={() => setDeleteModal(false)}
title="حذف کاربر" title={user.verified ? 'لغو تأیید کاربر' : 'تأیید کاربر'}
> >
<div className="space-y-4"> <div className="space-y-4">
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
آیا از حذف کاربر "{user.first_name} {user.last_name}" اطمینان دارید؟ {user.verified ? `آیا از لغو تأیید «${user.first_name} ${user.last_name}» مطمئن هستید؟` : `آیا از تأیید «${user.first_name} ${user.last_name}» مطمئن هستید؟`}
</p> </p>
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<p className="text-sm text-red-600 dark:text-red-400">
<strong>هشدار:</strong> این عمل غیرقابل بازگشت است و تمام اطلاعات کاربر به طور کامل حذف خواهد شد.
</p>
</div>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<Button <Button
variant="secondary" variant="secondary"
@ -324,12 +286,14 @@ const UserAdminDetailPage: React.FC = () => {
انصراف انصراف
</Button> </Button>
<Button <Button
variant="danger" variant={user.verified ? 'danger' : 'primary'}
onClick={handleDeleteConfirm} onClick={() => {
loading={deleteUserMutation.isPending} handleVerifyToggle();
data-testid="confirm-delete-button" setDeleteModal(false);
}}
loading={verifyUserMutation.isPending || unverifyUserMutation.isPending}
> >
حذف کاربر تایید
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Users, Plus, Search, Filter, UserCheck, UserX, Edit, Trash2, Eye, User as UserIcon } from 'lucide-react'; 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 { User, UserFilters } from '../core/_models';
import { PageContainer } from '../../../components/ui/Typography'; import { PageContainer } from '../../../components/ui/Typography';
import { Button } from '../../../components/ui/Button'; import { Button } from '../../../components/ui/Button';
@ -11,6 +11,7 @@ import { Pagination } from '../../../components/ui/Pagination';
import { StatsCard } from '../../../components/dashboard/StatsCard'; import { StatsCard } from '../../../components/dashboard/StatsCard';
import { Table } from '../../../components/ui/Table'; import { Table } from '../../../components/ui/Table';
import { TableColumn } from '../../../types'; import { TableColumn } from '../../../types';
import { englishToPersian, persianToEnglish } from '../../../utils/numberUtils';
const UsersAdminListPage: React.FC = () => { const UsersAdminListPage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -26,18 +27,23 @@ const UsersAdminListPage: React.FC = () => {
isOpen: false, isOpen: false,
user: null user: null
}); });
const [verifyModal, setVerifyModal] = useState<{ isOpen: boolean; user: User | null; action: 'verify' | 'unverify' }>({
isOpen: false,
user: null,
action: 'verify'
});
// Hooks // 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 { data: stats } = useUserStats();
const verifyUserMutation = useVerifyUser(); const verifyUserMutation = useVerifyUser();
const unverifyUserMutation = useUnverifyUser(); const unverifyUserMutation = useUnverifyUser();
const deleteUserMutation = useDeleteUser(); const deleteUserMutation = useDeleteUser();
// Handlers // Handlers
const handleCreate = () => { // Creation disabled per request
navigate('/users-admin/create');
};
const handleEdit = (user: User) => { const handleEdit = (user: User) => {
navigate(`/users-admin/${user.id}/edit`); navigate(`/users-admin/${user.id}/edit`);
@ -54,7 +60,7 @@ const UsersAdminListPage: React.FC = () => {
}; };
if (searchTerm.trim()) { if (searchTerm.trim()) {
newFilters.search_text = searchTerm.trim(); newFilters.search_text = persianToEnglish(searchTerm.trim());
} else { } else {
delete newFilters.search_text; delete newFilters.search_text;
} }
@ -78,10 +84,20 @@ const UsersAdminListPage: React.FC = () => {
}; };
const handleVerifyToggle = (user: User) => { const handleVerifyToggle = (user: User) => {
if (user.verified) { setVerifyModal({ isOpen: true, user, action: user.verified ? 'unverify' : 'verify' });
unverifyUserMutation.mutate(user.id.toString()); };
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 { } 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 = () => {
</div> </div>
) )
}, },
{ key: 'phone_number', label: 'شماره تلفن', align: 'left' }, {
key: 'phone_number',
label: 'شماره تلفن',
align: 'left',
render: (v: string) => {
const display = v ? englishToPersian(v) : '-';
return <span dir="ltr" style={{ direction: 'ltr' }}>{display}</span>;
}
},
{ key: 'email', label: 'ایمیل', align: 'left', render: (v: string) => v || '-' }, { key: 'email', label: 'ایمیل', align: 'left', render: (v: string) => v || '-' },
{ {
key: 'verified', key: 'verified',
@ -216,14 +240,7 @@ const UsersAdminListPage: React.FC = () => {
</h1> </h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">مشاهده و مدیریت کاربران سیستم</p> <p className="text-gray-600 dark:text-gray-400 mt-1">مشاهده و مدیریت کاربران سیستم</p>
</div> </div>
<Button
onClick={handleCreate}
className="flex items-center gap-2"
data-testid="create-user-button"
>
<Plus className="h-4 w-4" />
کاربر جدید
</Button>
</div> </div>
{/* Stats Cards */} {/* Stats Cards */}
@ -309,9 +326,9 @@ const UsersAdminListPage: React.FC = () => {
هیچ کاربری یافت نشد هیچ کاربری یافت نشد
</h3> </h3>
<p className="text-gray-600 dark:text-gray-400 mb-4"> <p className="text-gray-600 dark:text-gray-400 mb-4">
برای شروع یک کاربر ایجاد کنید نتیجهای یافت نشد
</p> </p>
<Button onClick={handleCreate}>ایجاد کاربر جدید</Button>
</div> </div>
) : ( ) : (
<Table columns={columns} data={users as any[]} /> <Table columns={columns} data={users as any[]} />
@ -322,10 +339,10 @@ const UsersAdminListPage: React.FC = () => {
{users.length > 0 && ( {users.length > 0 && (
<Pagination <Pagination
currentPage={Math.floor((filters.offset || 0) / (filters.limit || 20)) + 1} currentPage={Math.floor((filters.offset || 0) / (filters.limit || 20)) + 1}
totalPages={Math.ceil((stats?.total_users || 0) / (filters.limit || 20))} totalPages={Math.ceil((totalMatched || 0) / (filters.limit || 20))}
onPageChange={(page) => handlePageChange((page - 1) * (filters.limit || 20))} onPageChange={(page) => handlePageChange((page - 1) * (filters.limit || 20))}
itemsPerPage={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 = () => {
</div> </div>
</div> </div>
</Modal> </Modal>
{/* Verify/Unverify Confirmation Modal */}
<Modal
isOpen={verifyModal.isOpen}
onClose={() => setVerifyModal({ isOpen: false, user: null, action: 'verify' })}
title={verifyModal.action === 'unverify' ? 'لغو تأیید کاربر' : 'تأیید کاربر'}
>
<div className="space-y-4">
<p className="text-gray-600 dark:text-gray-400">
{verifyModal.action === 'unverify'
? `آیا از لغو تأیید «${verifyModal.user?.first_name} ${verifyModal.user?.last_name}» مطمئن هستید؟`
: `آیا از تأیید «${verifyModal.user?.first_name} ${verifyModal.user?.last_name}» مطمئن هستید؟`}
</p>
<div className="flex justify-end gap-3">
<Button
variant="secondary"
onClick={() => setVerifyModal({ isOpen: false, user: null, action: 'verify' })}
>
انصراف
</Button>
<Button
variant={verifyModal.action === 'unverify' ? 'danger' : 'primary'}
onClick={handleVerifyConfirm}
loading={verifyUserMutation.isPending || unverifyUserMutation.isPending}
>
تایید
</Button>
</div>
</div>
</Modal>
</div> </div>
</PageContainer> </PageContainer>
); );

View File

@ -1,5 +1,9 @@
import axios, { AxiosRequestConfig } from "axios"; 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 { getAuth } from "@/pages/auth";
import { pageSize } from "@/constant/generalVariables"; import { pageSize } from "@/constant/generalVariables";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
@ -144,11 +148,13 @@ export const calculateTotalPages = (totalItemsCount: number | undefined) => {
export function APIUrlGenerator( export function APIUrlGenerator(
route: string, route: string,
qry?: Record<string, string | number | null>, qry?: Record<string, string | number | null>,
baseUrl?: string baseUrl?: string,
useAdminPrefix: boolean = true
): string { ): string {
const query = qry || {}; const query = qry || {};
const queryKeys = Object.keys(query); 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) => { queryKeys.forEach((item, index) => {
if (index === 0) { if (index === 0) {
apiUrl += "?"; apiUrl += "?";

View File

@ -102,3 +102,14 @@ export const parseFormattedNumber = (value: any): number | undefined => {
const num = Number(cleaned); const num = Number(cleaned);
return isNaN(num) ? undefined : num; 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;
};