refactor: کامپوننتسازی پروژه و حذف کدهای تکراری
- ایجاد کامپوننتهای قابل استفاده مجدد: PageHeader, FiltersSection, TableSkeleton, EmptyState, DeleteConfirmModal, ActionButtons, StatusBadge - ایجاد کامپوننتهای فرم: FormSection, FormActions - ایجاد utility functions: formatters.ts, statusUtils.ts - Refactor صفحات لیست: ProductsListPage, CategoriesListPage, DiscountCodesListPage, UsersAdminListPage, RolesListPage, OrdersListPage - Refactor صفحات فرم: ProductFormPage - بهبود maintainability و کاهش code duplication
This commit is contained in:
parent
bfd1ea72a5
commit
8538d4282e
16
src/App.tsx
16
src/App.tsx
|
|
@ -88,16 +88,12 @@ const ShipmentsByMethodReportPage = lazy(() => import('./pages/reports/shipment-
|
||||||
// Product Comments Page
|
// Product Comments Page
|
||||||
const ProductCommentsListPage = lazy(() => import('./pages/products/comments/comments-list/ProductCommentsListPage'));
|
const ProductCommentsListPage = lazy(() => import('./pages/products/comments/comments-list/ProductCommentsListPage'));
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }: { children: any }) => {
|
const ProtectedRoute = ({ children }: { children: React.ReactElement }) => {
|
||||||
const { user, isLoading } = useAuth();
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout />
|
||||||
<div className="flex items-center justify-center py-12">
|
|
||||||
<LoadingSpinner />
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,11 +204,9 @@ const App = () => {
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Router>
|
<Router>
|
||||||
<Suspense fallback={
|
<Suspense fallback={
|
||||||
<Layout>
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="flex items-center justify-center py-12">
|
<LoadingSpinner />
|
||||||
<LoadingSpinner />
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
}>
|
}>
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Eye, Edit3, Trash2, LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ActionButtonsProps {
|
||||||
|
onView?: () => void;
|
||||||
|
onEdit?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
viewTitle?: string;
|
||||||
|
editTitle?: string;
|
||||||
|
deleteTitle?: string;
|
||||||
|
className?: string;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
showLabels?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSizeClasses = (size: 'sm' | 'md' | 'lg') => {
|
||||||
|
switch (size) {
|
||||||
|
case 'sm':
|
||||||
|
return 'h-3 w-3';
|
||||||
|
case 'md':
|
||||||
|
return 'h-4 w-4';
|
||||||
|
case 'lg':
|
||||||
|
return 'h-5 w-5';
|
||||||
|
default:
|
||||||
|
return 'h-4 w-4';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTextSizeClasses = (size: 'sm' | 'md' | 'lg') => {
|
||||||
|
switch (size) {
|
||||||
|
case 'sm':
|
||||||
|
return 'text-xs';
|
||||||
|
case 'md':
|
||||||
|
return 'text-xs';
|
||||||
|
case 'lg':
|
||||||
|
return 'text-sm';
|
||||||
|
default:
|
||||||
|
return 'text-xs';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActionButtons: React.FC<ActionButtonsProps> = ({
|
||||||
|
onView,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
viewTitle = 'مشاهده',
|
||||||
|
editTitle = 'ویرایش',
|
||||||
|
deleteTitle = 'حذف',
|
||||||
|
className = '',
|
||||||
|
size = 'md',
|
||||||
|
showLabels = false,
|
||||||
|
}) => {
|
||||||
|
const iconSize = getSizeClasses(size);
|
||||||
|
const textSize = getTextSizeClasses(size);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center gap-2 ${className}`}>
|
||||||
|
{onView && (
|
||||||
|
<button
|
||||||
|
onClick={onView}
|
||||||
|
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 flex items-center gap-1"
|
||||||
|
title={viewTitle}
|
||||||
|
>
|
||||||
|
<Eye className={iconSize} />
|
||||||
|
{showLabels && <span className={textSize}>{viewTitle}</span>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onEdit && (
|
||||||
|
<button
|
||||||
|
onClick={onEdit}
|
||||||
|
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 flex items-center gap-1"
|
||||||
|
title={editTitle}
|
||||||
|
>
|
||||||
|
<Edit3 className={iconSize} />
|
||||||
|
{showLabels && <span className={textSize}>{editTitle}</span>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onDelete && (
|
||||||
|
<button
|
||||||
|
onClick={onDelete}
|
||||||
|
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 flex items-center gap-1"
|
||||||
|
title={deleteTitle}
|
||||||
|
>
|
||||||
|
<Trash2 className={iconSize} />
|
||||||
|
{showLabels && <span className={textSize}>{deleteTitle}</span>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal } from '../ui/Modal';
|
||||||
|
import { Button } from '../ui/Button';
|
||||||
|
|
||||||
|
interface DeleteConfirmModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
warningMessage?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
itemName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteConfirmModal: React.FC<DeleteConfirmModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
title = 'حذف',
|
||||||
|
message,
|
||||||
|
warningMessage,
|
||||||
|
isLoading = false,
|
||||||
|
itemName,
|
||||||
|
}) => {
|
||||||
|
const defaultMessage = itemName
|
||||||
|
? `آیا از حذف "${itemName}" اطمینان دارید؟ این عمل قابل بازگشت نیست.`
|
||||||
|
: 'آیا از حذف این مورد اطمینان دارید؟ این عمل قابل بازگشت نیست.';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} title={title}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
{message || defaultMessage}
|
||||||
|
</p>
|
||||||
|
{warningMessage && (
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{warningMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end space-x-2 space-x-reverse">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={onConfirm}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
import { Button } from '../ui/Button';
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
icon?: LucideIcon;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
actionLabel?: ReactNode;
|
||||||
|
onAction?: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||||
|
icon: Icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actionLabel,
|
||||||
|
onAction,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={`text-center py-12 ${className}`}>
|
||||||
|
{Icon && (
|
||||||
|
<Icon className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||||
|
)}
|
||||||
|
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{actionLabel && onAction && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button onClick={onAction} className="flex items-center gap-2 mx-auto">
|
||||||
|
{actionLabel}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface FiltersSectionProps {
|
||||||
|
children: ReactNode;
|
||||||
|
isLoading?: boolean;
|
||||||
|
columns?: 1 | 2 | 3 | 4;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FiltersSection: React.FC<FiltersSectionProps> = ({
|
||||||
|
children,
|
||||||
|
isLoading = false,
|
||||||
|
columns = 4,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const gridCols = {
|
||||||
|
1: 'grid-cols-1',
|
||||||
|
2: 'grid-cols-1 md:grid-cols-2',
|
||||||
|
3: 'grid-cols-1 md:grid-cols-3',
|
||||||
|
4: 'grid-cols-1 md:grid-cols-4',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4 ${className}`}>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className={`grid ${gridCols[columns]} gap-4 animate-pulse`}>
|
||||||
|
{[...Array(columns)].map((_, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2"></div>
|
||||||
|
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={`grid ${gridCols[columns]} gap-4`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface TableSkeletonProps {
|
||||||
|
columns?: number;
|
||||||
|
rows?: number;
|
||||||
|
showMobileCards?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableSkeleton: React.FC<TableSkeletonProps> = ({
|
||||||
|
columns = 5,
|
||||||
|
rows = 5,
|
||||||
|
showMobileCards = true,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={`bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden ${className}`}>
|
||||||
|
{/* Desktop Table Skeleton */}
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
{[...Array(columns)].map((_, i) => (
|
||||||
|
<th
|
||||||
|
key={i}
|
||||||
|
className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded w-24 animate-pulse"></div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{[...Array(rows)].map((_, rowIndex) => (
|
||||||
|
<tr key={rowIndex}>
|
||||||
|
{[...Array(columns)].map((_, colIndex) => (
|
||||||
|
<td key={colIndex} className="px-6 py-4 whitespace-nowrap">
|
||||||
|
{colIndex === columns - 1 ? (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="h-8 w-8 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse w-32"></div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Cards Skeleton */}
|
||||||
|
{showMobileCards && (
|
||||||
|
<div className="md:hidden p-4 space-y-4">
|
||||||
|
{[...Array(Math.min(rows, 3))].map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 animate-pulse"
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="h-5 bg-gray-300 dark:bg-gray-600 rounded w-3/4"></div>
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-full"></div>
|
||||||
|
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-1/3"></div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '../ui/Button';
|
||||||
|
|
||||||
|
interface FormActionsProps {
|
||||||
|
onCancel?: () => void;
|
||||||
|
cancelLabel?: string;
|
||||||
|
submitLabel?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormActions: React.FC<FormActionsProps> = ({
|
||||||
|
onCancel,
|
||||||
|
cancelLabel = 'انصراف',
|
||||||
|
submitLabel = 'ذخیره',
|
||||||
|
isLoading = false,
|
||||||
|
isDisabled = false,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={`flex justify-end space-x-4 space-x-reverse pt-6 border-t border-gray-200 dark:border-gray-600 ${className}`}>
|
||||||
|
{onCancel && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{cancelLabel}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={isDisabled || isLoading}
|
||||||
|
>
|
||||||
|
{submitLabel}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { SectionTitle } from '../ui/Typography';
|
||||||
|
|
||||||
|
interface FormSectionProps {
|
||||||
|
title: string;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
titleClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormSection: React.FC<FormSectionProps> = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
titleClassName = '',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<SectionTitle className={`mb-4 ${titleClassName}`}>
|
||||||
|
{title}
|
||||||
|
</SectionTitle>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PageHeaderProps {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
actions?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageHeader: React.FC<PageHeaderProps> = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
icon: Icon,
|
||||||
|
actions,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 ${className}`}>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||||
|
{Icon && <Icon className="h-6 w-6" />}
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{actions && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export type StatusType = 'product' | 'order' | 'user' | 'discount' | 'comment' | 'generic';
|
||||||
|
|
||||||
|
export type ProductStatus = 'active' | 'inactive' | 'draft';
|
||||||
|
export type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded';
|
||||||
|
export type UserStatus = 'verified' | 'unverified' | boolean;
|
||||||
|
export type DiscountStatus = 'active' | 'inactive';
|
||||||
|
export type CommentStatus = 'approved' | 'rejected' | 'pending';
|
||||||
|
|
||||||
|
export type StatusValue = ProductStatus | OrderStatus | UserStatus | DiscountStatus | CommentStatus | string;
|
||||||
|
|
||||||
|
interface StatusBadgeProps {
|
||||||
|
status: StatusValue;
|
||||||
|
type?: StatusType;
|
||||||
|
className?: string;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusConfig = (status: StatusValue, type?: StatusType) => {
|
||||||
|
// Handle boolean status (for verified/unverified)
|
||||||
|
if (typeof status === 'boolean') {
|
||||||
|
return {
|
||||||
|
color: status
|
||||||
|
? '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',
|
||||||
|
text: status ? 'تأیید شده' : 'تأیید نشده',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusStr = String(status).toLowerCase();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'product':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'active':
|
||||||
|
return {
|
||||||
|
color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
|
text: 'فعال',
|
||||||
|
};
|
||||||
|
case 'inactive':
|
||||||
|
return {
|
||||||
|
color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||||
|
text: 'غیرفعال',
|
||||||
|
};
|
||||||
|
case 'draft':
|
||||||
|
return {
|
||||||
|
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
||||||
|
text: 'پیشنویس',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
||||||
|
text: statusStr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'order':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'pending':
|
||||||
|
return {
|
||||||
|
color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
|
text: 'در انتظار',
|
||||||
|
};
|
||||||
|
case 'processing':
|
||||||
|
return {
|
||||||
|
color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||||
|
text: 'در حال پردازش',
|
||||||
|
};
|
||||||
|
case 'shipped':
|
||||||
|
return {
|
||||||
|
color: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
||||||
|
text: 'ارسال شده',
|
||||||
|
};
|
||||||
|
case 'delivered':
|
||||||
|
return {
|
||||||
|
color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
|
text: 'تحویل شده',
|
||||||
|
};
|
||||||
|
case 'cancelled':
|
||||||
|
return {
|
||||||
|
color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||||
|
text: 'لغو شده',
|
||||||
|
};
|
||||||
|
case 'refunded':
|
||||||
|
return {
|
||||||
|
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
||||||
|
text: 'مرجوع شده',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
||||||
|
text: statusStr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'user':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'verified':
|
||||||
|
case 'true':
|
||||||
|
return {
|
||||||
|
color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
|
text: 'تأیید شده',
|
||||||
|
};
|
||||||
|
case 'unverified':
|
||||||
|
case 'false':
|
||||||
|
return {
|
||||||
|
color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
|
text: 'تأیید نشده',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
||||||
|
text: statusStr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'discount':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'active':
|
||||||
|
return {
|
||||||
|
color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
|
text: 'فعال',
|
||||||
|
};
|
||||||
|
case 'inactive':
|
||||||
|
return {
|
||||||
|
color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||||
|
text: 'غیرفعال',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
||||||
|
text: statusStr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'comment':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'approved':
|
||||||
|
return {
|
||||||
|
color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
|
text: 'تأیید شده',
|
||||||
|
};
|
||||||
|
case 'rejected':
|
||||||
|
return {
|
||||||
|
color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||||
|
text: 'رد شده',
|
||||||
|
};
|
||||||
|
case 'pending':
|
||||||
|
return {
|
||||||
|
color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
|
text: 'در انتظار',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
||||||
|
text: statusStr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Generic status handling
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'active':
|
||||||
|
case 'true':
|
||||||
|
return {
|
||||||
|
color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||||
|
text: 'فعال',
|
||||||
|
};
|
||||||
|
case 'inactive':
|
||||||
|
case 'false':
|
||||||
|
return {
|
||||||
|
color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||||
|
text: 'غیرفعال',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
color: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
||||||
|
text: statusStr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSizeClasses = (size: 'sm' | 'md' | 'lg') => {
|
||||||
|
switch (size) {
|
||||||
|
case 'sm':
|
||||||
|
return 'px-2 py-0.5 text-xs';
|
||||||
|
case 'md':
|
||||||
|
return 'px-2.5 py-0.5 text-xs';
|
||||||
|
case 'lg':
|
||||||
|
return 'px-3 py-1 text-sm';
|
||||||
|
default:
|
||||||
|
return 'px-2.5 py-0.5 text-xs';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatusBadge: React.FC<StatusBadgeProps> = ({
|
||||||
|
status,
|
||||||
|
type = 'generic',
|
||||||
|
className = '',
|
||||||
|
size = 'md',
|
||||||
|
}) => {
|
||||||
|
const config = getStatusConfig(status, type);
|
||||||
|
const sizeClasses = getSizeClasses(size);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center rounded-full font-medium ${config.color} ${sizeClasses} ${className}`}
|
||||||
|
>
|
||||||
|
{config.text}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -3,58 +3,15 @@ import { useNavigate } from 'react-router-dom';
|
||||||
import { useCategories, useDeleteCategory } from '../core/_hooks';
|
import { useCategories, useDeleteCategory } from '../core/_hooks';
|
||||||
import { Category } from '../core/_models';
|
import { Category } from '../core/_models';
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import { Plus, FolderOpen, Folder } from "lucide-react";
|
||||||
import { Trash2, Edit3, Plus, FolderOpen, Folder } from "lucide-react";
|
import { PageContainer } from "../../../components/ui/Typography";
|
||||||
import { Modal } from "@/components/ui/Modal";
|
import { PageHeader } from "@/components/layout/PageHeader";
|
||||||
import { PageContainer, PageTitle, SectionSubtitle } from "../../../components/ui/Typography";
|
import { FiltersSection } from "@/components/common/FiltersSection";
|
||||||
|
import { TableSkeleton } from "@/components/common/TableSkeleton";
|
||||||
const CategoriesTableSkeleton = () => (
|
import { EmptyState } from "@/components/common/EmptyState";
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
import { DeleteConfirmModal } from "@/components/common/DeleteConfirmModal";
|
||||||
<div className="hidden md:block">
|
import { ActionButtons } from "@/components/common/ActionButtons";
|
||||||
<div className="overflow-x-auto">
|
import { formatDate } from "@/utils/formatters";
|
||||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
نام دستهبندی
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
توضیحات
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
تاریخ ایجاد
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
عملیات
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="h-8 w-8 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
<div className="h-8 w-8 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const CategoriesListPage = () => {
|
const CategoriesListPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -98,48 +55,57 @@ const CategoriesListPage = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createButton = (
|
||||||
|
<button
|
||||||
|
onClick={handleCreate}
|
||||||
|
className="flex items-center justify-center w-12 h-12 bg-primary-600 hover:bg-primary-700 rounded-full transition-colors duration-200 text-white shadow-lg hover:shadow-xl"
|
||||||
|
title="دستهبندی جدید"
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
{/* Header */}
|
<PageHeader
|
||||||
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
title="مدیریت دستهبندیها"
|
||||||
|
subtitle="مدیریت دستهبندیهای محصولات"
|
||||||
|
icon={FolderOpen}
|
||||||
|
actions={createButton}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FiltersSection isLoading={isLoading} columns={2}>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
<FolderOpen className="h-6 w-6" />
|
جستجو
|
||||||
<PageTitle>مدیریت دستهبندیها</PageTitle>
|
</label>
|
||||||
</div>
|
<input
|
||||||
<p className="text-gray-600 dark:text-gray-400">مدیریت دستهبندیهای محصولات</p>
|
type="text"
|
||||||
|
placeholder="جستجو در نام دستهبندی..."
|
||||||
|
value={filters.search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</FiltersSection>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleCreate}
|
|
||||||
className="flex items-center justify-center w-12 h-12 bg-primary-600 hover:bg-primary-700 rounded-full transition-colors duration-200 text-white shadow-lg hover:shadow-xl"
|
|
||||||
title="دستهبندی جدید"
|
|
||||||
>
|
|
||||||
<Plus className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
جستجو
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="جستجو در نام دستهبندی..."
|
|
||||||
value={filters.search}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Categories Table */}
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<CategoriesTableSkeleton />
|
<TableSkeleton columns={4} rows={5} />
|
||||||
|
) : (!categories || categories.length === 0) ? (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
<EmptyState
|
||||||
|
icon={FolderOpen}
|
||||||
|
title="دستهبندیای موجود نیست"
|
||||||
|
description="برای شروع، اولین دستهبندی محصولات خود را ایجاد کنید."
|
||||||
|
actionLabel={
|
||||||
|
<>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
ایجاد دستهبندی جدید
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onAction={handleCreate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
{/* Desktop Table */}
|
{/* Desktop Table */}
|
||||||
|
|
@ -177,25 +143,13 @@ const CategoriesListPage = () => {
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
{new Date(category.created_at).toLocaleDateString('fa-IR')}
|
{formatDate(category.created_at)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<div className="flex items-center gap-2">
|
<ActionButtons
|
||||||
<button
|
onEdit={() => handleEdit(category.id)}
|
||||||
onClick={() => handleEdit(category.id)}
|
onDelete={() => setDeleteCategoryId(category.id.toString())}
|
||||||
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
/>
|
||||||
title="ویرایش"
|
|
||||||
>
|
|
||||||
<Edit3 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteCategoryId(category.id.toString())}
|
|
||||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
|
||||||
title="حذف"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
@ -220,77 +174,28 @@ const CategoriesListPage = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||||
تاریخ ایجاد: {new Date(category.created_at).toLocaleDateString('fa-IR')}
|
تاریخ ایجاد: {formatDate(category.created_at)}
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(category.id)}
|
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
|
||||||
>
|
|
||||||
<Edit3 className="h-3 w-3" />
|
|
||||||
ویرایش
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteCategoryId(category.id.toString())}
|
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
حذف
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ActionButtons
|
||||||
|
onEdit={() => handleEdit(category.id)}
|
||||||
|
onDelete={() => setDeleteCategoryId(category.id.toString())}
|
||||||
|
showLabels={true}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Empty State */}
|
|
||||||
{(!categories || categories.length === 0) && !isLoading && (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<FolderOpen className="mx-auto h-12 w-12 text-gray-400" />
|
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
دستهبندیای موجود نیست
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
برای شروع، اولین دستهبندی محصولات خود را ایجاد کنید.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<Button onClick={handleCreate} className="flex items-center gap-2 mx-auto">
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
ایجاد دستهبندی جدید
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
<DeleteConfirmModal
|
||||||
<Modal
|
|
||||||
isOpen={!!deleteCategoryId}
|
isOpen={!!deleteCategoryId}
|
||||||
onClose={() => setDeleteCategoryId(null)}
|
onClose={() => setDeleteCategoryId(null)}
|
||||||
|
onConfirm={handleDeleteConfirm}
|
||||||
title="حذف دستهبندی"
|
title="حذف دستهبندی"
|
||||||
>
|
message="آیا از حذف این دستهبندی اطمینان دارید؟ این عمل قابل بازگشت نیست و ممکن است بر محصولاتی که در این دستهبندی قرار دارند تأثیر بگذارد."
|
||||||
<div className="space-y-4">
|
isLoading={isDeleting}
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
/>
|
||||||
آیا از حذف این دستهبندی اطمینان دارید؟ این عمل قابل بازگشت نیست و ممکن است بر محصولاتی که در این دستهبندی قرار دارند تأثیر بگذارد.
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-end space-x-2 space-x-reverse">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setDeleteCategoryId(null)}
|
|
||||||
disabled={isDeleting}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
onClick={handleDeleteConfirm}
|
|
||||||
loading={isDeleting}
|
|
||||||
>
|
|
||||||
حذف
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,17 @@ import React, { useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useDiscountCodes, useDeleteDiscountCode } from '../core/_hooks';
|
import { useDiscountCodes, useDeleteDiscountCode } from '../core/_hooks';
|
||||||
import { DiscountCode } from '../core/_models';
|
import { DiscountCode } from '../core/_models';
|
||||||
import { Button } from "@/components/ui/Button";
|
|
||||||
import { Modal } from "@/components/ui/Modal";
|
|
||||||
import { Table } from "@/components/ui/Table";
|
import { Table } from "@/components/ui/Table";
|
||||||
import { TableColumn } from "@/types";
|
import { TableColumn } from "@/types";
|
||||||
import { Percent, BadgePercent, Trash2, Edit3, Plus, Ticket } from 'lucide-react';
|
import { BadgePercent, Plus, Ticket } from 'lucide-react';
|
||||||
|
import { PageContainer } from "@/components/ui/Typography";
|
||||||
|
import { PageHeader } from "@/components/layout/PageHeader";
|
||||||
|
import { FiltersSection } from "@/components/common/FiltersSection";
|
||||||
|
import { EmptyState } from "@/components/common/EmptyState";
|
||||||
|
import { DeleteConfirmModal } from "@/components/common/DeleteConfirmModal";
|
||||||
|
import { ActionButtons } from "@/components/common/ActionButtons";
|
||||||
|
import { StatusBadge } from "@/components/ui/StatusBadge";
|
||||||
|
import { formatDate } from "@/utils/formatters";
|
||||||
|
|
||||||
const DiscountCodesListPage = () => {
|
const DiscountCodesListPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -41,18 +47,14 @@ const DiscountCodesListPage = () => {
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
label: 'وضعیت',
|
label: 'وضعیت',
|
||||||
render: (val: string) => (
|
render: (val: string) => <StatusBadge status={val} type="discount" />
|
||||||
<span className={`px-2 py-1 rounded-full text-xs ${val === 'active' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}`}>
|
|
||||||
{val === 'active' ? 'فعال' : 'غیرفعال'}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'period',
|
key: 'period',
|
||||||
label: 'بازه زمانی',
|
label: 'بازه زمانی',
|
||||||
render: (_val, row: any) => (
|
render: (_val, row: any) => (
|
||||||
<span>
|
<span>
|
||||||
{row.valid_from ? new Date(row.valid_from).toLocaleDateString('fa-IR') : '-'} تا {row.valid_to ? new Date(row.valid_to).toLocaleDateString('fa-IR') : '-'}
|
{row.valid_from ? formatDate(row.valid_from) : '-'} تا {row.valid_to ? formatDate(row.valid_to) : '-'}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -60,22 +62,10 @@ const DiscountCodesListPage = () => {
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
label: 'عملیات',
|
label: 'عملیات',
|
||||||
render: (_val, row: any) => (
|
render: (_val, row: any) => (
|
||||||
<div className="flex items-center gap-2">
|
<ActionButtons
|
||||||
<button
|
onEdit={() => handleEdit(row.id)}
|
||||||
onClick={() => handleEdit(row.id)}
|
onDelete={() => setDeleteId(row.id.toString())}
|
||||||
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
/>
|
||||||
title="ویرایش"
|
|
||||||
>
|
|
||||||
<Edit3 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteId(row.id.toString())}
|
|
||||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
|
||||||
title="حذف"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
], [navigate]);
|
], [navigate]);
|
||||||
|
|
@ -90,28 +80,28 @@ const DiscountCodesListPage = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const createButton = (
|
||||||
<div className="p-6 space-y-6">
|
<button
|
||||||
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
onClick={handleCreate}
|
||||||
<div>
|
className="flex items-center justify-center w-12 h-12 bg-primary-600 hover:bg-primary-700 rounded-full transition-colors duration-200 text-white shadow-lg hover:shadow-xl"
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
title="کد تخفیف جدید"
|
||||||
<BadgePercent className="h-6 w-6" />
|
data-testid="create-discount-button"
|
||||||
مدیریت کدهای تخفیف
|
>
|
||||||
</h1>
|
<Plus className="h-5 w-5" />
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">ایجاد و مدیریت کدهای تخفیف</p>
|
</button>
|
||||||
</div>
|
);
|
||||||
<button
|
|
||||||
onClick={handleCreate}
|
|
||||||
className="flex items-center justify-center w-12 h-12 bg-primary-600 hover:bg-primary-700 rounded-full transition-colors duration-200 text-white shadow-lg hover:shadow-xl"
|
|
||||||
title="کد تخفیف جدید"
|
|
||||||
data-testid="create-discount-button"
|
|
||||||
>
|
|
||||||
<Plus className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<PageContainer>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<PageHeader
|
||||||
|
title="مدیریت کدهای تخفیف"
|
||||||
|
subtitle="ایجاد و مدیریت کدهای تخفیف"
|
||||||
|
icon={BadgePercent}
|
||||||
|
actions={createButton}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FiltersSection isLoading={isLoading} columns={3}>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">کد</label>
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">کد</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -122,37 +112,39 @@ const DiscountCodesListPage = () => {
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FiltersSection>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Table columns={columns} data={Array.isArray(discountCodes) ? (discountCodes as any[]) : []} loading={true} />
|
||||||
|
) : !discountCodes || discountCodes.length === 0 ? (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
|
<EmptyState
|
||||||
|
icon={Ticket}
|
||||||
|
title="هیچ کد تخفیفی یافت نشد"
|
||||||
|
description="برای شروع یک کد تخفیف ایجاد کنید"
|
||||||
|
actionLabel={
|
||||||
|
<>
|
||||||
|
<Plus className="h-4 w-4 ml-2" />
|
||||||
|
ایجاد کد تخفیف
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onAction={handleCreate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table columns={columns} data={discountCodes as any[]} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DeleteConfirmModal
|
||||||
|
isOpen={!!deleteId}
|
||||||
|
onClose={() => setDeleteId(null)}
|
||||||
|
onConfirm={handleDeleteConfirm}
|
||||||
|
title="حذف کد تخفیف"
|
||||||
|
message="آیا از حذف این کد تخفیف اطمینان دارید؟ این عمل قابل بازگشت نیست."
|
||||||
|
isLoading={isDeleting}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</PageContainer>
|
||||||
{isLoading ? (
|
|
||||||
<Table columns={columns} data={Array.isArray(discountCodes) ? (discountCodes as any[]) : []} loading={true} />
|
|
||||||
) : !discountCodes || discountCodes.length === 0 ? (
|
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<Ticket className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">هیچ کد تخفیفی یافت نشد</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">برای شروع یک کد تخفیف ایجاد کنید</p>
|
|
||||||
<Button onClick={handleCreate} className="flex items-center gap-2">
|
|
||||||
<Plus className="h-4 w-4 ml-2" />
|
|
||||||
ایجاد کد تخفیف
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Table columns={columns} data={discountCodes as any[]} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Modal isOpen={!!deleteId} onClose={() => setDeleteId(null)} title="حذف کد تخفیف">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-gray-600 dark:text-gray-400">آیا از حذف این کد تخفیف اطمینان دارید؟ این عمل قابل بازگشت نیست.</p>
|
|
||||||
<div className="flex justify-end space-x-2 space-x-reverse">
|
|
||||||
<Button variant="secondary" onClick={() => setDeleteId(null)} disabled={isDeleting}>انصراف</Button>
|
|
||||||
<Button variant="danger" onClick={handleDeleteConfirm} loading={isDeleting}>حذف</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { OrderFilters, OrderStatus } from '../core/_models';
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { Modal } from "@/components/ui/Modal";
|
import { Modal } from "@/components/ui/Modal";
|
||||||
import { Pagination } from "@/components/ui/Pagination";
|
import { Pagination } from "@/components/ui/Pagination";
|
||||||
import { PageContainer, PageTitle } from "@/components/ui/Typography";
|
import { PageContainer } from "@/components/ui/Typography";
|
||||||
import { Table } from "@/components/ui/Table";
|
import { Table } from "@/components/ui/Table";
|
||||||
import { TableColumn } from "@/types";
|
import { TableColumn } from "@/types";
|
||||||
import { StatsCard } from '@/components/dashboard/StatsCard';
|
import { StatsCard } from '@/components/dashboard/StatsCard';
|
||||||
|
|
@ -20,46 +20,16 @@ import {
|
||||||
Clock,
|
Clock,
|
||||||
Search,
|
Search,
|
||||||
Filter,
|
Filter,
|
||||||
Eye,
|
|
||||||
Edit3,
|
Edit3,
|
||||||
TrendingUp
|
TrendingUp
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { PageHeader } from '@/components/layout/PageHeader';
|
||||||
|
import { FiltersSection } from '@/components/common/FiltersSection';
|
||||||
|
import { EmptyState } from '@/components/common/EmptyState';
|
||||||
|
import { ActionButtons } from '@/components/common/ActionButtons';
|
||||||
|
import { StatusBadge } from '@/components/ui/StatusBadge';
|
||||||
|
import { formatCurrency, formatDate } from '@/utils/formatters';
|
||||||
|
|
||||||
const getStatusColor = (status: OrderStatus) => {
|
|
||||||
const colors = {
|
|
||||||
pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
|
||||||
processing: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
|
||||||
shipped: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
|
||||||
delivered: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
|
||||||
cancelled: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
|
||||||
refunded: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
|
||||||
};
|
|
||||||
return colors[status] || colors.pending;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status: OrderStatus) => {
|
|
||||||
const text = {
|
|
||||||
pending: 'در انتظار',
|
|
||||||
processing: 'در حال پردازش',
|
|
||||||
shipped: 'ارسال شده',
|
|
||||||
delivered: 'تحویل شده',
|
|
||||||
cancelled: 'لغو شده',
|
|
||||||
refunded: 'مرجوع شده',
|
|
||||||
};
|
|
||||||
return text[status] || status;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCurrency = (amount: number) => {
|
|
||||||
return new Intl.NumberFormat('fa-IR').format(amount) + ' تومان';
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
return new Date(dateString).toLocaleDateString('fa-IR');
|
|
||||||
};
|
|
||||||
|
|
||||||
const ListSkeleton = () => (
|
|
||||||
<Table columns={[]} data={[]} loading={true} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const getDefaultFilters = (): OrderFilters => ({
|
const getDefaultFilters = (): OrderFilters => ({
|
||||||
page: 1,
|
page: 1,
|
||||||
|
|
@ -219,7 +189,7 @@ const OrdersListPage = () => {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ key: 'final_total', label: 'مبلغ نهایی', sortable: true, align: 'right', render: (v: number, row: any) => formatCurrency(row.final_total || row.total_amount || 0) },
|
{ 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) => (<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) => <StatusBadge status={v} type="order" /> },
|
||||||
{ key: 'created_at', label: 'تاریخ', sortable: true, align: 'right', render: (v: string) => formatDate(v) },
|
{ key: 'created_at', label: 'تاریخ', sortable: true, align: 'right', render: (v: string) => formatDate(v) },
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
|
|
@ -227,13 +197,9 @@ const OrdersListPage = () => {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (_val, row: any) => (
|
render: (_val, row: any) => (
|
||||||
<div className="flex items-center gap-2 justify-end">
|
<div className="flex items-center gap-2 justify-end">
|
||||||
<button
|
<ActionButtons
|
||||||
onClick={() => handleViewOrder(row.id)}
|
onView={() => handleViewOrder(row.id)}
|
||||||
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
/>
|
||||||
title="مشاهده جزئیات"
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleUpdateStatus(row.id, row.status)}
|
onClick={() => handleUpdateStatus(row.id, row.status)}
|
||||||
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
|
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
|
@ -280,17 +246,11 @@ const OrdersListPage = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
<PageHeader
|
||||||
<div>
|
title="مدیریت سفارشات"
|
||||||
<PageTitle className="flex items-center gap-2">
|
subtitle={`${ordersData?.total || 0} سفارش یافت شد`}
|
||||||
<ShoppingCart className="h-6 w-6" />
|
icon={ShoppingCart}
|
||||||
مدیریت سفارشات
|
/>
|
||||||
</PageTitle>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
{ordersData?.total || 0} سفارش یافت شد
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 lg:gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 lg:gap-6">
|
||||||
{statsLoading ? (
|
{statsLoading ? (
|
||||||
|
|
@ -468,14 +428,14 @@ const OrdersListPage = () => {
|
||||||
|
|
||||||
{/* جدول سفارشات */}
|
{/* جدول سفارشات */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ListSkeleton />
|
<Table columns={columns} data={[]} loading={true} />
|
||||||
) : !ordersData?.orders || ordersData.orders.length === 0 ? (
|
) : !ordersData?.orders || ordersData.orders.length === 0 ? (
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
<div className="text-center py-12">
|
<EmptyState
|
||||||
<ShoppingCart className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
|
icon={ShoppingCart}
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">هیچ سفارشی یافت نشد</h3>
|
title="هیچ سفارشی یافت نشد"
|
||||||
<p className="text-gray-600 dark:text-gray-400">با تغییر فیلترها جستجو کنید</p>
|
description="با تغییر فیلترها جستجو کنید"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ import { Input } from "@/components/ui/Input";
|
||||||
import { FileUploader } from "@/components/ui/FileUploader";
|
import { FileUploader } from "@/components/ui/FileUploader";
|
||||||
import { VariantManager } from "@/components/ui/VariantManager";
|
import { VariantManager } from "@/components/ui/VariantManager";
|
||||||
import { ArrowRight, X } from "lucide-react";
|
import { ArrowRight, X } from "lucide-react";
|
||||||
import { FormHeader, PageContainer, SectionTitle, Label } from '../../../components/ui/Typography';
|
import { FormHeader, PageContainer, Label } from '../../../components/ui/Typography';
|
||||||
|
import { FormSection } from '@/components/forms/FormSection';
|
||||||
|
import { FormActions } from '@/components/forms/FormActions';
|
||||||
import { createNumberTransform, createOptionalNumberTransform, convertPersianNumbersInObject } from '../../../utils/numberUtils';
|
import { createNumberTransform, createOptionalNumberTransform, convertPersianNumbersInObject } from '../../../utils/numberUtils';
|
||||||
import { API_GATE_WAY, API_ROUTES } from '@/constant/routes';
|
import { API_GATE_WAY, API_ROUTES } from '@/constant/routes';
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
|
@ -471,11 +473,7 @@ const ProductFormPage = () => {
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<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">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
|
||||||
{/* Basic Information */}
|
<FormSection title="اطلاعات پایه">
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
اطلاعات پایه
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -536,13 +534,9 @@ const ProductFormPage = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormSection>
|
||||||
|
|
||||||
{/* Categories and Product Options */}
|
<FormSection title="دستهبندی و گزینهها">
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
دستهبندی و گزینهها
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<MultiSelectAutocomplete
|
<MultiSelectAutocomplete
|
||||||
label="دستهبندیها"
|
label="دستهبندیها"
|
||||||
|
|
@ -582,13 +576,9 @@ const ProductFormPage = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormSection>
|
||||||
|
|
||||||
{/* Images */}
|
<FormSection title="تصاویر محصول">
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
تصاویر محصول
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<FileUploader
|
<FileUploader
|
||||||
onUpload={handleFileUpload}
|
onUpload={handleFileUpload}
|
||||||
|
|
@ -641,12 +631,9 @@ const ProductFormPage = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</FormSection>
|
||||||
|
|
||||||
<div>
|
<FormSection title="فایلهای Explorer">
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
فایلهای Explorer
|
|
||||||
</h3>
|
|
||||||
<FileUploader
|
<FileUploader
|
||||||
onUpload={handleExplorerUpload}
|
onUpload={handleExplorerUpload}
|
||||||
onRemove={handleExplorerRemove}
|
onRemove={handleExplorerRemove}
|
||||||
|
|
@ -709,19 +696,16 @@ const ProductFormPage = () => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</FormSection>
|
||||||
|
|
||||||
{/* Variants Management */}
|
<FormSection title="مدیریت Variants">
|
||||||
<div>
|
|
||||||
<VariantManager
|
<VariantManager
|
||||||
variants={watch('variants') || []}
|
variants={watch('variants') || []}
|
||||||
onChange={(variants) => setValue('variants', variants, { shouldValidate: true, shouldDirty: true })}
|
onChange={(variants) => setValue('variants', variants, { shouldValidate: true, shouldDirty: true })}
|
||||||
productOptions={productOptionOptions}
|
productOptions={productOptionOptions}
|
||||||
variantAttributeName={watch('variant_attribute_name')}
|
variantAttributeName={watch('variant_attribute_name')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormSection>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Preview */}
|
{/* Preview */}
|
||||||
{formValues.name && (
|
{formValues.name && (
|
||||||
|
|
@ -797,24 +781,13 @@ const ProductFormPage = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Submit Buttons */}
|
<FormActions
|
||||||
<div className="flex justify-end space-x-4 space-x-reverse pt-6 border-t border-gray-200 dark:border-gray-600">
|
onCancel={handleBack}
|
||||||
<Button
|
cancelLabel="انصراف"
|
||||||
type="button"
|
submitLabel={isEdit ? 'بهروزرسانی' : 'ایجاد محصول'}
|
||||||
variant="secondary"
|
isLoading={isLoading}
|
||||||
onClick={handleBack}
|
isDisabled={!isValid || isLoading || isUploading || isExplorerUploading}
|
||||||
disabled={isLoading}
|
/>
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
loading={isLoading}
|
|
||||||
disabled={!isValid || isLoading || isUploading || isExplorerUploading}
|
|
||||||
>
|
|
||||||
{isEdit ? 'بهروزرسانی' : 'ایجاد محصول'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,64 +5,17 @@ import { useCategories } from '../../categories/core/_hooks';
|
||||||
import { Product } from '../core/_models';
|
import { Product } from '../core/_models';
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { PageContainer } from "@/components/ui/Typography";
|
import { PageContainer } from "@/components/ui/Typography";
|
||||||
import { Trash2, Edit3, Plus, Package, Eye, Image } from "lucide-react";
|
import { Plus, Package, Image } from "lucide-react";
|
||||||
import { Modal } from "@/components/ui/Modal";
|
|
||||||
import { persianToEnglish } from '../../../utils/numberUtils';
|
import { persianToEnglish } from '../../../utils/numberUtils';
|
||||||
import { Pagination } from "@/components/ui/Pagination";
|
import { Pagination } from "@/components/ui/Pagination";
|
||||||
|
import { PageHeader } from "@/components/layout/PageHeader";
|
||||||
const ProductsTableSkeleton = () => (
|
import { FiltersSection } from "@/components/common/FiltersSection";
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
import { TableSkeleton } from "@/components/common/TableSkeleton";
|
||||||
<div className="hidden md:block">
|
import { EmptyState } from "@/components/common/EmptyState";
|
||||||
<div className="overflow-x-auto">
|
import { DeleteConfirmModal } from "@/components/common/DeleteConfirmModal";
|
||||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
import { ActionButtons } from "@/components/common/ActionButtons";
|
||||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
import { StatusBadge } from "@/components/ui/StatusBadge";
|
||||||
<tr>
|
import { formatPrice } from "@/utils/formatters";
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
محصول
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
قیمت
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
دستهبندی
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
وضعیت
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
عملیات
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="h-8 w-8 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
<div className="h-8 w-8 bg-gray-200 dark:bg-gray-600 rounded animate-pulse"></div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ProductsListPage = () => {
|
const ProductsListPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -122,29 +75,12 @@ const ProductsListPage = () => {
|
||||||
setFilters(prev => ({ ...prev, status: e.target.value, page: 1 }));
|
setFilters(prev => ({ ...prev, status: e.target.value, page: 1 }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatPrice = (price: number) => {
|
|
||||||
return new Intl.NumberFormat('fa-IR').format(price) + ' تومان';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFirstImageUrl = (p: any): string | null => {
|
const getFirstImageUrl = (p: any): string | null => {
|
||||||
if (p.file_ids && p.file_ids.length > 0) return p.file_ids[0].url;
|
if (p.file_ids && p.file_ids.length > 0) return p.file_ids[0].url;
|
||||||
if (p.files && p.files.length > 0) return p.files[0].url;
|
if (p.files && p.files.length > 0) return p.files[0].url;
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'active':
|
|
||||||
return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">فعال</span>;
|
|
||||||
case 'inactive':
|
|
||||||
return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">غیرفعال</span>;
|
|
||||||
case 'draft':
|
|
||||||
return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200">پیشنویس</span>;
|
|
||||||
default:
|
|
||||||
return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200">{status}</span>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const total = productsData?.total || 0;
|
const total = productsData?.total || 0;
|
||||||
const currentPage = productsData?.page || filters.page;
|
const currentPage = productsData?.page || filters.page;
|
||||||
const perPage = productsData?.per_page || filters.limit;
|
const perPage = productsData?.per_page || filters.limit;
|
||||||
|
|
@ -160,129 +96,119 @@ const ProductsListPage = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createButton = (
|
||||||
|
<button
|
||||||
|
onClick={handleCreate}
|
||||||
|
className="flex items-center justify-center w-12 h-12 bg-primary-600 hover:bg-primary-700 rounded-full transition-colors duration-200 text-white shadow-lg hover:shadow-xl"
|
||||||
|
title="محصول جدید"
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
<PageHeader
|
||||||
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
title="مدیریت محصولات"
|
||||||
|
subtitle="مدیریت محصولات، قیمتها و موجودی"
|
||||||
|
icon={Package}
|
||||||
|
actions={createButton}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FiltersSection isLoading={isLoading} columns={4}>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
<Package className="h-6 w-6" />
|
جستجو
|
||||||
مدیریت محصولات
|
</label>
|
||||||
</h1>
|
<input
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
type="text"
|
||||||
مدیریت محصولات، قیمتها و موجودی
|
placeholder="جستجو در نام محصول..."
|
||||||
</p>
|
value={filters.search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div>
|
||||||
onClick={handleCreate}
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
className="flex items-center justify-center w-12 h-12 bg-primary-600 hover:bg-primary-700 rounded-full transition-colors duration-200 text-white shadow-lg hover:shadow-xl"
|
دستهبندی
|
||||||
title="محصول جدید"
|
</label>
|
||||||
>
|
<select
|
||||||
<Plus className="h-5 w-5" />
|
value={filters.category_id}
|
||||||
</button>
|
onChange={handleCategoryChange}
|
||||||
</div>
|
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
||||||
|
>
|
||||||
{/* Filters */}
|
<option value="">همه دستهبندیها</option>
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
{(categories || []).map((category) => (
|
||||||
{isLoading ? (
|
<option key={category.id} value={category.id}>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 animate-pulse">
|
{category.name}
|
||||||
{[...Array(4)].map((_, i) => (
|
</option>
|
||||||
<div key={i}>
|
|
||||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2"></div>
|
|
||||||
{i === 3 ? (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded-lg flex-1"></div>
|
|
||||||
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded-lg flex-1"></div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
وضعیت
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filters.status}
|
||||||
|
onChange={handleStatusChange}
|
||||||
|
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<option value="">همه وضعیتها</option>
|
||||||
|
<option value="active">فعال</option>
|
||||||
|
<option value="inactive">غیرفعال</option>
|
||||||
|
<option value="draft">پیشنویس</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
محدوده قیمت
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
placeholder="حداقل (مثال: ۱۰۰۰۰)"
|
||||||
|
value={filters.min_price}
|
||||||
|
onChange={(e) => {
|
||||||
|
const converted = persianToEnglish(e.target.value);
|
||||||
|
setFilters(prev => ({ ...prev, min_price: converted, page: 1 }));
|
||||||
|
}}
|
||||||
|
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
placeholder="حداکثر (مثال: ۵۰۰۰۰۰)"
|
||||||
|
value={filters.max_price}
|
||||||
|
onChange={(e) => {
|
||||||
|
const converted = persianToEnglish(e.target.value);
|
||||||
|
setFilters(prev => ({ ...prev, max_price: converted, page: 1 }));
|
||||||
|
}}
|
||||||
|
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
</FiltersSection>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
جستجو
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="جستجو در نام محصول..."
|
|
||||||
value={filters.search}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
دستهبندی
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filters.category_id}
|
|
||||||
onChange={handleCategoryChange}
|
|
||||||
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
|
||||||
>
|
|
||||||
<option value="">همه دستهبندیها</option>
|
|
||||||
{(categories || []).map((category) => (
|
|
||||||
<option key={category.id} value={category.id}>
|
|
||||||
{category.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
وضعیت
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filters.status}
|
|
||||||
onChange={handleStatusChange}
|
|
||||||
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
|
||||||
>
|
|
||||||
<option value="">همه وضعیتها</option>
|
|
||||||
<option value="active">فعال</option>
|
|
||||||
<option value="inactive">غیرفعال</option>
|
|
||||||
<option value="draft">پیشنویس</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
محدوده قیمت
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
placeholder="حداقل (مثال: ۱۰۰۰۰)"
|
|
||||||
value={filters.min_price}
|
|
||||||
onChange={(e) => {
|
|
||||||
const converted = persianToEnglish(e.target.value);
|
|
||||||
setFilters(prev => ({ ...prev, min_price: converted, page: 1 }));
|
|
||||||
}}
|
|
||||||
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
placeholder="حداکثر (مثال: ۵۰۰۰۰۰)"
|
|
||||||
value={filters.max_price}
|
|
||||||
onChange={(e) => {
|
|
||||||
const converted = persianToEnglish(e.target.value);
|
|
||||||
setFilters(prev => ({ ...prev, max_price: converted, page: 1 }));
|
|
||||||
}}
|
|
||||||
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Products Table */}
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ProductsTableSkeleton />
|
<TableSkeleton columns={5} rows={5} />
|
||||||
|
) : products.length === 0 ? (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
<EmptyState
|
||||||
|
icon={Package}
|
||||||
|
title="محصولی موجود نیست"
|
||||||
|
description="برای شروع، اولین محصول خود را ایجاد کنید."
|
||||||
|
actionLabel={
|
||||||
|
<>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
ایجاد محصول جدید
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onAction={handleCreate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
{/* Desktop Table */}
|
{/* Desktop Table */}
|
||||||
|
|
@ -345,32 +271,14 @@ const ProductsListPage = () => {
|
||||||
{product.category?.name || 'بدون دستهبندی'}
|
{product.category?.name || 'بدون دستهبندی'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
{getStatusBadge(product.status || '')}
|
<StatusBadge status={product.status || ''} type="product" />
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<div className="flex items-center gap-2">
|
<ActionButtons
|
||||||
<button
|
onView={() => handleView(product.id)}
|
||||||
onClick={() => handleView(product.id)}
|
onEdit={() => handleEdit(product.id)}
|
||||||
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
|
onDelete={() => setDeleteProductId(product.id.toString())}
|
||||||
title="مشاهده"
|
/>
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(product.id)}
|
|
||||||
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
|
||||||
title="ویرایش"
|
|
||||||
>
|
|
||||||
<Edit3 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteProductId(product.id.toString())}
|
|
||||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
|
||||||
title="حذف"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
@ -405,7 +313,7 @@ const ProductsListPage = () => {
|
||||||
{formatPrice(product.price || 0)}
|
{formatPrice(product.price || 0)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
{getStatusBadge(product.status || '')}
|
<StatusBadge status={product.status || ''} type="product" size="sm" />
|
||||||
{product.category && (
|
{product.category && (
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
{product.category.name}
|
{product.category.name}
|
||||||
|
|
@ -414,51 +322,16 @@ const ProductsListPage = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<ActionButtons
|
||||||
<button
|
onView={() => handleView(product.id)}
|
||||||
onClick={() => handleView(product.id)}
|
onEdit={() => handleEdit(product.id)}
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
|
onDelete={() => setDeleteProductId(product.id.toString())}
|
||||||
>
|
showLabels={true}
|
||||||
<Eye className="h-3 w-3" />
|
size="sm"
|
||||||
مشاهده
|
/>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(product.id)}
|
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
|
||||||
>
|
|
||||||
<Edit3 className="h-3 w-3" />
|
|
||||||
ویرایش
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteProductId(product.id.toString())}
|
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
حذف
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Empty State */}
|
|
||||||
{(!products || products.length === 0) && !isLoading && (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<Package className="mx-auto h-12 w-12 text-gray-400" />
|
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
محصولی موجود نیست
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
برای شروع، اولین محصول خود را ایجاد کنید.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<Button onClick={handleCreate} className="flex items-center gap-2 mx-auto">
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
ایجاد محصول جدید
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -471,34 +344,14 @@ const ProductsListPage = () => {
|
||||||
onPageChange={(page) => setFilters(prev => ({ ...prev, page }))}
|
onPageChange={(page) => setFilters(prev => ({ ...prev, page }))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
<DeleteConfirmModal
|
||||||
<Modal
|
|
||||||
isOpen={!!deleteProductId}
|
isOpen={!!deleteProductId}
|
||||||
onClose={() => setDeleteProductId(null)}
|
onClose={() => setDeleteProductId(null)}
|
||||||
|
onConfirm={handleDeleteConfirm}
|
||||||
title="حذف محصول"
|
title="حذف محصول"
|
||||||
>
|
message="آیا از حذف این محصول اطمینان دارید؟ این عمل قابل بازگشت نیست و تمام اطلاعات مربوط به محصول از جمله نسخهها و تصاویر حذف خواهد شد."
|
||||||
<div className="space-y-4">
|
isLoading={isDeleting}
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
/>
|
||||||
آیا از حذف این محصول اطمینان دارید؟ این عمل قابل بازگشت نیست و تمام اطلاعات مربوط به محصول از جمله نسخهها و تصاویر حذف خواهد شد.
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-end space-x-2 space-x-reverse">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setDeleteProductId(null)}
|
|
||||||
disabled={isDeleting}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
onClick={handleDeleteConfirm}
|
|
||||||
loading={isDeleting}
|
|
||||||
>
|
|
||||||
حذف
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ const RoleDetailPage = () => {
|
||||||
if (!role) return <div>نقش یافت نشد</div>;
|
if (!role) return <div>نقش یافت نشد</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<PageContainer>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
|
|
||||||
|
|
@ -3,79 +3,15 @@ import { useNavigate } from 'react-router-dom';
|
||||||
import { useRoles, useDeleteRole } from '../core/_hooks';
|
import { useRoles, useDeleteRole } from '../core/_hooks';
|
||||||
import { Role } from '@/types/auth';
|
import { Role } from '@/types/auth';
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import { Plus, Shield, Eye, Settings } from "lucide-react";
|
||||||
import { Trash2, Edit3, Plus, UserCog, Shield, Eye, Settings } from "lucide-react";
|
import { PageContainer } from '../../../components/ui/Typography';
|
||||||
import { Modal } from "@/components/ui/Modal";
|
import { PageHeader } from '@/components/layout/PageHeader';
|
||||||
import { PageContainer, PageTitle, SectionSubtitle } from '../../../components/ui/Typography';
|
import { FiltersSection } from '@/components/common/FiltersSection';
|
||||||
|
import { TableSkeleton } from '@/components/common/TableSkeleton';
|
||||||
// Skeleton Loading Component
|
import { EmptyState } from '@/components/common/EmptyState';
|
||||||
const RolesTableSkeleton = () => (
|
import { DeleteConfirmModal } from '@/components/common/DeleteConfirmModal';
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
import { ActionButtons } from '@/components/common/ActionButtons';
|
||||||
{/* Desktop Table Skeleton */}
|
import { formatDate } from '@/utils/formatters';
|
||||||
<div className="hidden md:block">
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
نام نقش
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
توضیحات
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
تاریخ ایجاد
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
عملیات
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{[...Array(5)].map((_, index) => (
|
|
||||||
<tr key={index} className="animate-pulse">
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-32"></div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-48"></div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-20"></div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
|
||||||
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
|
||||||
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Cards Skeleton */}
|
|
||||||
<div className="md:hidden p-4 space-y-4">
|
|
||||||
{[...Array(3)].map((_, index) => (
|
|
||||||
<div key={index} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 animate-pulse">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="h-5 bg-gray-300 dark:bg-gray-600 rounded w-3/4"></div>
|
|
||||||
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-full"></div>
|
|
||||||
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-1/3"></div>
|
|
||||||
<div className="flex gap-2 pt-2">
|
|
||||||
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
|
||||||
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
|
||||||
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const RolesListPage = () => {
|
const RolesListPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -135,66 +71,58 @@ const RolesListPage = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createButton = (
|
||||||
|
<button
|
||||||
|
onClick={handleCreate}
|
||||||
|
className="flex items-center justify-center w-12 h-12 bg-primary-600 hover:bg-primary-700 rounded-full transition-colors duration-200 text-white shadow-lg hover:shadow-xl"
|
||||||
|
title="نقش جدید"
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
{/* Header */}
|
<PageHeader
|
||||||
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
title="مدیریت نقشها"
|
||||||
|
subtitle="مدیریت نقشها و دسترسیهای سیستم"
|
||||||
|
icon={Shield}
|
||||||
|
actions={createButton}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FiltersSection isLoading={isLoading} columns={2}>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
<UserCog className="h-6 w-6" />
|
جستجو
|
||||||
<PageTitle>مدیریت نقشها</PageTitle>
|
</label>
|
||||||
</div>
|
<input
|
||||||
<p className="text-gray-600 dark:text-gray-400">مدیریت نقشها و دسترسیهای سیستم</p>
|
type="text"
|
||||||
|
placeholder="جستجو در نام یا توضیحات نقش..."
|
||||||
|
value={filters.search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</FiltersSection>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleCreate}
|
|
||||||
className="flex items-center justify-center w-12 h-12 bg-primary-600 hover:bg-primary-700 rounded-full transition-colors duration-200 text-white shadow-lg hover:shadow-xl"
|
|
||||||
title="نقش جدید"
|
|
||||||
>
|
|
||||||
<Plus className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
جستجو
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="جستجو در نام یا توضیحات نقش..."
|
|
||||||
value={filters.search}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Roles Table */}
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<RolesTableSkeleton />
|
<TableSkeleton columns={4} rows={5} />
|
||||||
) : (roles || []).length === 0 ? (
|
) : (roles || []).length === 0 ? (
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
<div className="text-center py-12">
|
<EmptyState
|
||||||
<UserCog className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
|
icon={Shield}
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
title="هیچ نقش یافت نشد"
|
||||||
هیچ نقش یافت نشد
|
description={filters.search
|
||||||
</h3>
|
? "نتیجهای برای جستجوی شما یافت نشد"
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
: "شما هنوز هیچ نقش ایجاد نکردهاید"}
|
||||||
{filters.search
|
actionLabel={
|
||||||
? "نتیجهای برای جستجوی شما یافت نشد"
|
<>
|
||||||
: "شما هنوز هیچ نقش ایجاد نکردهاید"
|
<Plus className="h-4 w-4 ml-2" />
|
||||||
}
|
اولین نقش را ایجاد کنید
|
||||||
</p>
|
</>
|
||||||
<Button onClick={handleCreate} className="flex items-center gap-2">
|
}
|
||||||
<Plus className="h-4 w-4 ml-2" />
|
onAction={handleCreate}
|
||||||
اولین نقش را ایجاد کنید
|
/>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
|
@ -228,24 +156,15 @@ const RolesListPage = () => {
|
||||||
{role.description}
|
{role.description}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
{new Date(role.created_at).toLocaleDateString('fa-IR')}
|
{formatDate(role.created_at)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<ActionButtons
|
||||||
onClick={() => handleView(role.id)}
|
onView={() => handleView(role.id)}
|
||||||
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
|
onEdit={() => handleEdit(role.id)}
|
||||||
title="مشاهده"
|
onDelete={() => setDeleteRoleId(role.id.toString())}
|
||||||
>
|
/>
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(role.id)}
|
|
||||||
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
|
||||||
title="ویرایش"
|
|
||||||
>
|
|
||||||
<Edit3 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePermissions(role.id)}
|
onClick={() => handlePermissions(role.id)}
|
||||||
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
|
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
|
@ -253,13 +172,6 @@ const RolesListPage = () => {
|
||||||
>
|
>
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setDeleteRoleId(role.id.toString())}
|
|
||||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
|
||||||
title="حذف"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -284,23 +196,16 @@ const RolesListPage = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||||
تاریخ ایجاد: {new Date(role.created_at).toLocaleDateString('fa-IR')}
|
تاریخ ایجاد: {formatDate(role.created_at)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<ActionButtons
|
||||||
onClick={() => handleView(role.id)}
|
onView={() => handleView(role.id)}
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
|
onEdit={() => handleEdit(role.id)}
|
||||||
>
|
onDelete={() => setDeleteRoleId(role.id.toString())}
|
||||||
<Eye className="h-3 w-3" />
|
showLabels={true}
|
||||||
مشاهده
|
size="sm"
|
||||||
</button>
|
/>
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(role.id)}
|
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
|
||||||
>
|
|
||||||
<Edit3 className="h-3 w-3" />
|
|
||||||
ویرایش
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePermissions(role.id)}
|
onClick={() => handlePermissions(role.id)}
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
|
className="flex items-center gap-1 px-2 py-1 text-xs text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
|
@ -308,13 +213,6 @@ const RolesListPage = () => {
|
||||||
<Settings className="h-3 w-3" />
|
<Settings className="h-3 w-3" />
|
||||||
دسترسیها
|
دسترسیها
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setDeleteRoleId(role.id.toString())}
|
|
||||||
className="flex items-center gap-1 px-2 py-1 text-xs text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
حذف
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -322,31 +220,14 @@ const RolesListPage = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
<DeleteConfirmModal
|
||||||
<Modal
|
|
||||||
isOpen={!!deleteRoleId}
|
isOpen={!!deleteRoleId}
|
||||||
onClose={cancelDelete}
|
onClose={cancelDelete}
|
||||||
|
onConfirm={handleDeleteConfirm}
|
||||||
title="تأیید حذف"
|
title="تأیید حذف"
|
||||||
size="sm"
|
message="آیا از حذف این نقش اطمینان دارید؟ این عمل قابل بازگشت نیست."
|
||||||
>
|
isLoading={isDeleting}
|
||||||
<div className="space-y-4">
|
/>
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
آیا از حذف این نقش اطمینان دارید؟ این عمل قابل بازگشت نیست.
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button variant="secondary" onClick={cancelDelete}>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
onClick={handleDeleteConfirm}
|
|
||||||
loading={isDeleting}
|
|
||||||
>
|
|
||||||
حذف
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { ShippingOpenHour } from '../core/_models';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { TagInput } from '@/components/ui/TagInput';
|
import { TagInput } from '@/components/ui/TagInput';
|
||||||
|
import { PageContainer } from '@/components/ui/Typography';
|
||||||
import { Truck } from 'lucide-react';
|
import { Truck } from 'lucide-react';
|
||||||
import { formatWithThousands, parseFormattedNumber } from '@/utils/numberUtils';
|
import { formatWithThousands, parseFormattedNumber } from '@/utils/numberUtils';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
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, Eye, Edit, Trash2, User as UserIcon } from 'lucide-react';
|
||||||
import { useSearchUsers, 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';
|
||||||
|
|
@ -12,6 +12,10 @@ 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';
|
import { englishToPersian, persianToEnglish } from '../../../utils/numberUtils';
|
||||||
|
import { PageHeader } from '@/components/layout/PageHeader';
|
||||||
|
import { FiltersSection } from '@/components/common/FiltersSection';
|
||||||
|
import { DeleteConfirmModal } from '@/components/common/DeleteConfirmModal';
|
||||||
|
import { StatusBadge } from '@/components/ui/StatusBadge';
|
||||||
|
|
||||||
const UsersAdminListPage: React.FC = () => {
|
const UsersAdminListPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -163,14 +167,7 @@ const UsersAdminListPage: React.FC = () => {
|
||||||
key: 'verified',
|
key: 'verified',
|
||||||
label: 'وضعیت',
|
label: 'وضعیت',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (v: boolean) => (
|
render: (v: boolean) => <StatusBadge status={v} type="user" />
|
||||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${v
|
|
||||||
? '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'
|
|
||||||
}`}>
|
|
||||||
{v ? 'تأیید شده' : 'تأیید نشده'}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
|
|
@ -231,17 +228,11 @@ const UsersAdminListPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
<PageHeader
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
title="مدیریت کاربران"
|
||||||
<div>
|
subtitle="مشاهده و مدیریت کاربران سیستم"
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
icon={Users}
|
||||||
<Users className="h-6 w-6" />
|
/>
|
||||||
مدیریت کاربران
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">مشاهده و مدیریت کاربران سیستم</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
{stats && (
|
{stats && (
|
||||||
|
|
@ -273,47 +264,44 @@ const UsersAdminListPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filters */}
|
<FiltersSection isLoading={false} columns={3}>
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
<Input
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
placeholder="جستجو بر اساس نام، شماره تلفن یا ایمیل..."
|
||||||
<Input
|
value={searchTerm}
|
||||||
placeholder="جستجو بر اساس نام، شماره تلفن یا ایمیل..."
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
value={searchTerm}
|
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
data-testid="search-users-input"
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
/>
|
||||||
data-testid="search-users-input"
|
<select
|
||||||
/>
|
value={selectedStatus}
|
||||||
<select
|
onChange={(e) => setSelectedStatus(e.target.value as any)}
|
||||||
value={selectedStatus}
|
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
||||||
onChange={(e) => setSelectedStatus(e.target.value as any)}
|
data-testid="status-filter-select"
|
||||||
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
|
>
|
||||||
data-testid="status-filter-select"
|
<option value="all">همه کاربران</option>
|
||||||
|
<option value="verified">تأیید شده</option>
|
||||||
|
<option value="unverified">تأیید نشده</option>
|
||||||
|
</select>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleSearch}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
data-testid="search-button"
|
||||||
>
|
>
|
||||||
<option value="all">همه کاربران</option>
|
<Search className="h-4 w-4" />
|
||||||
<option value="verified">تأیید شده</option>
|
جستجو
|
||||||
<option value="unverified">تأیید نشده</option>
|
</Button>
|
||||||
</select>
|
<Button
|
||||||
<div className="flex gap-2">
|
variant="secondary"
|
||||||
<Button
|
onClick={handleClearFilters}
|
||||||
onClick={handleSearch}
|
className="flex items-center gap-2"
|
||||||
className="flex items-center gap-2"
|
data-testid="clear-filters-button"
|
||||||
data-testid="search-button"
|
>
|
||||||
>
|
<Filter className="h-4 w-4" />
|
||||||
<Search className="h-4 w-4" />
|
پاک کردن فیلترها
|
||||||
جستجو
|
</Button>
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={handleClearFilters}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
data-testid="clear-filters-button"
|
|
||||||
>
|
|
||||||
<Filter className="h-4 w-4" />
|
|
||||||
پاک کردن فیلترها
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FiltersSection>
|
||||||
|
|
||||||
{/* Users Table */}
|
{/* Users Table */}
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
|
@ -346,36 +334,15 @@ const UsersAdminListPage: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
<DeleteConfirmModal
|
||||||
<Modal
|
|
||||||
isOpen={deleteModal.isOpen}
|
isOpen={deleteModal.isOpen}
|
||||||
onClose={() => setDeleteModal({ isOpen: false, user: null })}
|
onClose={() => setDeleteModal({ isOpen: false, user: null })}
|
||||||
|
onConfirm={handleDeleteConfirm}
|
||||||
title="حذف کاربر"
|
title="حذف کاربر"
|
||||||
>
|
message={`آیا از حذف کاربر "${deleteModal.user?.first_name} ${deleteModal.user?.last_name}" اطمینان دارید؟`}
|
||||||
<div className="space-y-4">
|
warningMessage="این عمل غیرقابل بازگشت است."
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
isLoading={deleteUserMutation.isPending}
|
||||||
آیا از حذف کاربر "{deleteModal.user?.first_name} {deleteModal.user?.last_name}" اطمینان دارید؟
|
/>
|
||||||
</p>
|
|
||||||
<p className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
این عمل غیرقابل بازگشت است.
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setDeleteModal({ isOpen: false, user: null })}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
onClick={handleDeleteConfirm}
|
|
||||||
loading={deleteUserMutation.isPending}
|
|
||||||
>
|
|
||||||
حذف
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Verify/Unverify Confirmation Modal */}
|
{/* Verify/Unverify Confirmation Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
* Utility functions for formatting data
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format price with Persian number formatting and تومان suffix
|
||||||
|
*/
|
||||||
|
export const formatPrice = (price: number): string => {
|
||||||
|
if (price === null || price === undefined || isNaN(price)) {
|
||||||
|
return '0 تومان';
|
||||||
|
}
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(price) + ' تومان';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format currency amount with Persian number formatting and تومان suffix
|
||||||
|
*/
|
||||||
|
export const formatCurrency = (amount: number): string => {
|
||||||
|
if (amount === null || amount === undefined || isNaN(amount)) {
|
||||||
|
return '0 تومان';
|
||||||
|
}
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(amount) + ' تومان';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date string to Persian locale
|
||||||
|
*/
|
||||||
|
export const formatDate = (dateString: string | Date): string => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = typeof dateString === 'string' ? new Date(dateString) : dateString;
|
||||||
|
if (isNaN(date.getTime())) return '-';
|
||||||
|
|
||||||
|
return date.toLocaleDateString('fa-IR');
|
||||||
|
} catch {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date and time string to Persian locale
|
||||||
|
*/
|
||||||
|
export const formatDateTime = (dateString: string | Date): string => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = typeof dateString === 'string' ? new Date(dateString) : dateString;
|
||||||
|
if (isNaN(date.getTime())) return '-';
|
||||||
|
|
||||||
|
return date.toLocaleDateString('fa-IR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format number with thousands separator (Persian)
|
||||||
|
*/
|
||||||
|
export const formatNumber = (num: number): string => {
|
||||||
|
if (num === null || num === undefined || isNaN(num)) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { StatusBadge as StatusBadgeComponent, StatusType, StatusValue } from '../components/ui/StatusBadge';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status badge color classes
|
||||||
|
*/
|
||||||
|
export const getStatusColor = (status: string, type?: StatusType): string => {
|
||||||
|
const statusStr = String(status).toLowerCase();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'order':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'pending':
|
||||||
|
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
|
||||||
|
case 'processing':
|
||||||
|
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
||||||
|
case 'shipped':
|
||||||
|
return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200';
|
||||||
|
case 'delivered':
|
||||||
|
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||||
|
case 'cancelled':
|
||||||
|
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||||
|
case 'refunded':
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'product':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'active':
|
||||||
|
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||||
|
case 'inactive':
|
||||||
|
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||||
|
case 'draft':
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'user':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'verified':
|
||||||
|
case 'true':
|
||||||
|
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||||
|
case 'unverified':
|
||||||
|
case 'false':
|
||||||
|
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'discount':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'active':
|
||||||
|
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||||
|
case 'inactive':
|
||||||
|
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'comment':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'approved':
|
||||||
|
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||||
|
case 'rejected':
|
||||||
|
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||||
|
case 'pending':
|
||||||
|
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'active':
|
||||||
|
case 'true':
|
||||||
|
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||||
|
case 'inactive':
|
||||||
|
case 'false':
|
||||||
|
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status text in Persian
|
||||||
|
*/
|
||||||
|
export const getStatusText = (status: string, type?: StatusType): string => {
|
||||||
|
const statusStr = String(status).toLowerCase();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'order':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'pending':
|
||||||
|
return 'در انتظار';
|
||||||
|
case 'processing':
|
||||||
|
return 'در حال پردازش';
|
||||||
|
case 'shipped':
|
||||||
|
return 'ارسال شده';
|
||||||
|
case 'delivered':
|
||||||
|
return 'تحویل شده';
|
||||||
|
case 'cancelled':
|
||||||
|
return 'لغو شده';
|
||||||
|
case 'refunded':
|
||||||
|
return 'مرجوع شده';
|
||||||
|
default:
|
||||||
|
return statusStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'product':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'active':
|
||||||
|
return 'فعال';
|
||||||
|
case 'inactive':
|
||||||
|
return 'غیرفعال';
|
||||||
|
case 'draft':
|
||||||
|
return 'پیشنویس';
|
||||||
|
default:
|
||||||
|
return statusStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'user':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'verified':
|
||||||
|
case 'true':
|
||||||
|
return 'تأیید شده';
|
||||||
|
case 'unverified':
|
||||||
|
case 'false':
|
||||||
|
return 'تأیید نشده';
|
||||||
|
default:
|
||||||
|
return statusStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'discount':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'active':
|
||||||
|
return 'فعال';
|
||||||
|
case 'inactive':
|
||||||
|
return 'غیرفعال';
|
||||||
|
default:
|
||||||
|
return statusStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'comment':
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'approved':
|
||||||
|
return 'تأیید شده';
|
||||||
|
case 'rejected':
|
||||||
|
return 'رد شده';
|
||||||
|
case 'pending':
|
||||||
|
return 'در انتظار';
|
||||||
|
default:
|
||||||
|
return statusStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
switch (statusStr) {
|
||||||
|
case 'active':
|
||||||
|
case 'true':
|
||||||
|
return 'فعال';
|
||||||
|
case 'inactive':
|
||||||
|
case 'false':
|
||||||
|
return 'غیرفعال';
|
||||||
|
default:
|
||||||
|
return statusStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status badge React component
|
||||||
|
*/
|
||||||
|
export const getStatusBadge = (
|
||||||
|
status: StatusValue,
|
||||||
|
type?: StatusType,
|
||||||
|
className?: string,
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
): React.ReactElement => {
|
||||||
|
return React.createElement(StatusBadgeComponent, { status, type, className, size });
|
||||||
|
};
|
||||||
|
|
||||||
Loading…
Reference in New Issue