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:
hosseintaromi 2026-01-08 17:46:40 +03:30
parent bfd1ea72a5
commit 8538d4282e
21 changed files with 1346 additions and 919 deletions

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
); );
}; };

View File

@ -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>
); );
}; };

View File

@ -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>
) : ( ) : (
<> <>

View File

@ -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>

View File

@ -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>
); );

View File

@ -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">

View File

@ -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>
); );
}; };

View File

@ -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';

View File

@ -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

72
src/utils/formatters.ts Normal file
View File

@ -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);
};

185
src/utils/statusUtils.ts Normal file
View File

@ -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 });
};