refactor: replace duplicate components and utilities across pages
- Replace duplicate skeleton components with TableSkeleton in AdminUsersListPage, ProductOptionsListPage, PermissionsListPage - Replace duplicate format functions with formatters.ts utilities in OrderDetailPage - Replace duplicate page headers with PageHeader component - Replace duplicate empty states with EmptyState component - Replace duplicate action buttons with ActionButtons component - Replace duplicate delete modals with DeleteConfirmModal component - Improve code consistency and maintainability
This commit is contained in:
parent
8538d4282e
commit
6dd2429920
|
|
@ -4,84 +4,14 @@ import { useAdminUsers, useDeleteAdminUser } from '../core/_hooks';
|
|||
import { AdminUserInfo } from '../core/_models';
|
||||
import { Button } from "@/components/ui/Button";
|
||||
|
||||
import { Trash2, Edit3, Plus, Eye, Users, UserPlus } from "lucide-react";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
import { PageContainer, PageTitle, SectionSubtitle } from '../../../components/ui/Typography';
|
||||
|
||||
// Skeleton Loading Component
|
||||
const AdminUserTableSkeleton = () => (
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
{/* 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>
|
||||
<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((_, 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-24"></div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded-full w-16"></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-1/2"></div>
|
||||
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded-full w-16"></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>
|
||||
);
|
||||
import { Users, UserPlus, Plus } from "lucide-react";
|
||||
import { PageContainer, SectionSubtitle } from '../../../components/ui/Typography';
|
||||
import { TableSkeleton } from '@/components/common/TableSkeleton';
|
||||
import { PageHeader } from '@/components/layout/PageHeader';
|
||||
import { EmptyState } from '@/components/common/EmptyState';
|
||||
import { ActionButtons } from '@/components/common/ActionButtons';
|
||||
import { DeleteConfirmModal } from '@/components/common/DeleteConfirmModal';
|
||||
import { formatDate } from '@/utils/formatters';
|
||||
|
||||
const AdminUsersListPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -136,24 +66,20 @@ const AdminUsersListPage = () => {
|
|||
|
||||
return (
|
||||
<PageContainer>
|
||||
{/* Header */}
|
||||
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Users className="h-6 w-6" />
|
||||
<PageTitle>مدیریت کاربران ادمین</PageTitle>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400">مدیریت کاربران دسترسی به پنل ادمین</p>
|
||||
</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="کاربر ادمین جدید"
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<PageHeader
|
||||
title="مدیریت کاربران ادمین"
|
||||
subtitle="مدیریت کاربران دسترسی به پنل ادمین"
|
||||
icon={Users}
|
||||
actions={
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Filters */}
|
||||
<SectionSubtitle>فیلترها</SectionSubtitle>
|
||||
|
|
@ -190,25 +116,24 @@ const AdminUsersListPage = () => {
|
|||
|
||||
{/* Users Table */}
|
||||
{isLoading ? (
|
||||
<AdminUserTableSkeleton />
|
||||
<TableSkeleton columns={5} rows={5} />
|
||||
) : (users || []).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">
|
||||
<Users 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">
|
||||
{filters.search || filters.status
|
||||
? "نتیجهای برای جستجوی شما یافت نشد"
|
||||
: "شما هنوز هیچ کاربر ادمین ایجاد نکردهاید"
|
||||
}
|
||||
</p>
|
||||
<Button onClick={handleCreate}>
|
||||
<UserPlus className="h-4 w-4 ml-2" />
|
||||
اولین کاربر ادمین را ایجاد کنید
|
||||
</Button>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title="هیچ کاربر ادمین یافت نشد"
|
||||
description={filters.search || filters.status
|
||||
? "نتیجهای برای جستجوی شما یافت نشد"
|
||||
: "شما هنوز هیچ کاربر ادمین ایجاد نکردهاید"
|
||||
}
|
||||
actionLabel={
|
||||
<>
|
||||
<UserPlus className="h-4 w-4 ml-2" />
|
||||
اولین کاربر ادمین را ایجاد کنید
|
||||
</>
|
||||
}
|
||||
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">
|
||||
|
|
@ -253,32 +178,14 @@ const AdminUsersListPage = () => {
|
|||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{new Date(user.created_at).toLocaleDateString('fa-IR')}
|
||||
{formatDate(user.created_at)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleView(user.id)}
|
||||
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
title="مشاهده"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(user.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={() => setDeleteUserId(user.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>
|
||||
<ActionButtons
|
||||
onView={() => handleView(user.id)}
|
||||
onEdit={() => handleEdit(user.id)}
|
||||
onDelete={() => setDeleteUserId(user.id.toString())}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -308,65 +215,27 @@ const AdminUsersListPage = () => {
|
|||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
تاریخ ایجاد: {new Date(user.created_at).toLocaleDateString('fa-IR')}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleView(user.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"
|
||||
>
|
||||
<Eye className="h-3 w-3" />
|
||||
مشاهده
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(user.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={() => setDeleteUserId(user.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>
|
||||
تاریخ ایجاد: {formatDate(user.created_at)}
|
||||
</div>
|
||||
<ActionButtons
|
||||
onView={() => handleView(user.id)}
|
||||
onEdit={() => handleEdit(user.id)}
|
||||
onDelete={() => setDeleteUserId(user.id.toString())}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<Modal
|
||||
<DeleteConfirmModal
|
||||
isOpen={!!deleteUserId}
|
||||
onClose={() => setDeleteUserId(null)}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
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={() => setDeleteUserId(null)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleDeleteConfirm}
|
||||
loading={isDeleting}
|
||||
>
|
||||
حذف
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
message="آیا از حذف این کاربر ادمین اطمینان دارید؟ این عمل قابل بازگشت نیست."
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from 'lucide-react';
|
||||
import { englishToPersian } from '@/utils/numberUtils';
|
||||
import { API_GATE_WAY } from '@/constant/routes';
|
||||
import { formatCurrency, formatDateTime } from '@/utils/formatters';
|
||||
|
||||
const resolveImageUrl = (imageUrl?: string): string => {
|
||||
if (!imageUrl) return '';
|
||||
|
|
@ -55,19 +56,6 @@ const getStatusText = (status: OrderStatus) => {
|
|||
return text[status] || status;
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('fa-IR').format(amount) + ' تومان';
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('fa-IR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const formatPaymentType = (type?: string) => {
|
||||
if (!type) return '';
|
||||
|
|
@ -171,7 +159,7 @@ const OrderDetailPage = () => {
|
|||
<div>
|
||||
<PageTitle>سفارش #{order?.order_number || 'نامشخص'}</PageTitle>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
تاریخ ثبت: {order?.created_at ? formatDate(order.created_at) : 'نامشخص'}
|
||||
تاریخ ثبت: {order?.created_at ? formatDateTime(order.created_at) : 'نامشخص'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
|
|
@ -367,7 +355,7 @@ const OrderDetailPage = () => {
|
|||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ ثبت</h4>
|
||||
<p className="text-gray-600 dark:text-gray-400">{order?.created_at ? formatDate(order.created_at) : 'نامشخص'}</p>
|
||||
<p className="text-gray-600 dark:text-gray-400">{order?.created_at ? formatDateTime(order.created_at) : 'نامشخص'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">شناسه فاکتور</h4>
|
||||
|
|
@ -379,7 +367,7 @@ const OrderDetailPage = () => {
|
|||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ آخرین بروزرسانی</h4>
|
||||
<p className="text-gray-600 dark:text-gray-400">{order?.updated_at ? formatDate(order.updated_at) : 'نامشخص'}</p>
|
||||
<p className="text-gray-600 dark:text-gray-400">{order?.updated_at ? formatDateTime(order.updated_at) : 'نامشخص'}</p>
|
||||
</div>
|
||||
{/* روش حمل و نقل در دادههای فعلی وجود ندارد */}
|
||||
{order?.tracking_number && (
|
||||
|
|
@ -391,7 +379,7 @@ const OrderDetailPage = () => {
|
|||
{order?.estimated_delivery && (
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ تحویل تخمینی</h4>
|
||||
<p className="text-gray-600 dark:text-gray-400">{formatDate(order.estimated_delivery)}</p>
|
||||
<p className="text-gray-600 dark:text-gray-400">{formatDateTime(order.estimated_delivery)}</p>
|
||||
</div>
|
||||
)}
|
||||
{order?.shipping_method_id !== undefined && order?.shipping_method_id !== null && (
|
||||
|
|
|
|||
|
|
@ -1,72 +1,12 @@
|
|||
import React, { useState } from 'react';
|
||||
import { usePermissions } from '../core/_hooks';
|
||||
import { Permission } from '../core/_models';
|
||||
|
||||
import { Shield, Plus } from "lucide-react";
|
||||
|
||||
// Skeleton Loading Component
|
||||
const PermissionsTableSkeleton = () => (
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
{/* 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>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
import { Shield } from "lucide-react";
|
||||
import { TableSkeleton } from '@/components/common/TableSkeleton';
|
||||
import { PageHeader } from '@/components/layout/PageHeader';
|
||||
import { EmptyState } from '@/components/common/EmptyState';
|
||||
import { FiltersSection } from '@/components/common/FiltersSection';
|
||||
import { formatDate } from '@/utils/formatters';
|
||||
|
||||
const PermissionsListPage = () => {
|
||||
const [filters, setFilters] = useState({
|
||||
|
|
@ -91,54 +31,40 @@ const PermissionsListPage = () => {
|
|||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<Shield className="h-6 w-6" />
|
||||
لیست دسترسیها
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
نمایش دسترسیهای سیستم
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader
|
||||
title="لیست دسترسیها"
|
||||
subtitle="نمایش دسترسیهای سیستم"
|
||||
icon={Shield}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
<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-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>
|
||||
|
||||
{/* Permissions Table */}
|
||||
{isLoading ? (
|
||||
<PermissionsTableSkeleton />
|
||||
<TableSkeleton columns={4} rows={5} />
|
||||
) : (permissions || []).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">
|
||||
<Shield 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">
|
||||
{filters.search
|
||||
? "نتیجهای برای جستجوی شما یافت نشد"
|
||||
: "دسترسیهای سیستم در اینجا نمایش داده میشوند"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Shield}
|
||||
title="هیچ دسترسی یافت نشد"
|
||||
description={filters.search
|
||||
? "نتیجهای برای جستجوی شما یافت نشد"
|
||||
: "دسترسیهای سیستم در اینجا نمایش داده میشوند"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
|
|
@ -172,7 +98,7 @@ const PermissionsListPage = () => {
|
|||
{permission.description}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{new Date(permission.created_at).toLocaleDateString('fa-IR')}
|
||||
{formatDate(permission.created_at)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -196,7 +122,7 @@ const PermissionsListPage = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
تاریخ ایجاد: {new Date(permission.created_at).toLocaleDateString('fa-IR')}
|
||||
تاریخ ایجاد: {formatDate(permission.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -2,58 +2,14 @@ import React, { useState } from 'react';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { useProductOptions, useDeleteProductOption } from '../core/_hooks';
|
||||
import { ProductOption } from '../core/_models';
|
||||
import { Button } from "@/components/ui/Button";
|
||||
|
||||
import { Trash2, Edit3, Plus, Settings, Tag } from "lucide-react";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
|
||||
const ProductOptionsTableSkeleton = () => (
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<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((_, 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>
|
||||
);
|
||||
import { Settings, Tag, Plus } from "lucide-react";
|
||||
import { TableSkeleton } from '@/components/common/TableSkeleton';
|
||||
import { PageHeader } from '@/components/layout/PageHeader';
|
||||
import { EmptyState } from '@/components/common/EmptyState';
|
||||
import { ActionButtons } from '@/components/common/ActionButtons';
|
||||
import { DeleteConfirmModal } from '@/components/common/DeleteConfirmModal';
|
||||
import { FiltersSection } from '@/components/common/FiltersSection';
|
||||
import { formatDate } from '@/utils/formatters';
|
||||
|
||||
const ProductOptionsListPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -99,47 +55,39 @@ const ProductOptionsListPage = () => {
|
|||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<Settings className="h-6 w-6" />
|
||||
مدیریت گزینههای محصول
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
تنظیمات گزینههای قابل انتخاب برای محصولات
|
||||
</p>
|
||||
</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="گزینه محصول جدید"
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<PageHeader
|
||||
title="مدیریت گزینههای محصول"
|
||||
subtitle="تنظیمات گزینههای قابل انتخاب برای محصولات"
|
||||
icon={Settings}
|
||||
actions={
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
<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-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>
|
||||
|
||||
{/* Product Options Table */}
|
||||
{isLoading ? (
|
||||
<ProductOptionsTableSkeleton />
|
||||
<TableSkeleton columns={4} rows={5} />
|
||||
) : (
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
{/* Desktop Table */}
|
||||
|
|
@ -187,25 +135,13 @@ const ProductOptionsListPage = () => {
|
|||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{new Date(option.created_at).toLocaleDateString('fa-IR')}
|
||||
{formatDate(option.created_at)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(option.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={() => setDeleteOptionId(option.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>
|
||||
<ActionButtons
|
||||
onEdit={() => handleEdit(option.id)}
|
||||
onDelete={() => setDeleteOptionId(option.id.toString())}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -242,77 +178,26 @@ const ProductOptionsListPage = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
تاریخ ایجاد: {new Date(option.created_at).toLocaleDateString('fa-IR')}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(option.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={() => setDeleteOptionId(option.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>
|
||||
تاریخ ایجاد: {formatDate(option.created_at)}
|
||||
</div>
|
||||
<ActionButtons
|
||||
onEdit={() => handleEdit(option.id)}
|
||||
onDelete={() => setDeleteOptionId(option.id.toString())}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
{(!productOptions || productOptions.length === 0) && !isLoading && (
|
||||
<div className="text-center py-12">
|
||||
<Settings 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>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<Modal
|
||||
<DeleteConfirmModal
|
||||
isOpen={!!deleteOptionId}
|
||||
onClose={() => setDeleteOptionId(null)}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
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={() => setDeleteOptionId(null)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleDeleteConfirm}
|
||||
loading={isDeleting}
|
||||
>
|
||||
حذف
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
message="آیا از حذف این گزینه محصول اطمینان دارید؟ این عمل قابل بازگشت نیست و ممکن است بر محصولاتی که از این گزینه استفاده میکنند تأثیر بگذارد."
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue