fix(admin): cleanup price display input, upgrade pagination component and extend roles guard for CMS & B2B
This commit is contained in:
parent
2aea3bded4
commit
2ce24b0473
@ -26,7 +26,8 @@ export class RolesGuard implements CanActivate {
|
||||
throw new ForbiddenException('شما دسترسی لازم برای این بخش را ندارید');
|
||||
}
|
||||
|
||||
const hasRole = requiredRoles.includes(user.role);
|
||||
const userRoleLower = user.role.toLowerCase();
|
||||
const hasRole = requiredRoles.some(r => r.toLowerCase() === userRoleLower || (userRoleLower.includes('admin') && r.toLowerCase().includes('admin')));
|
||||
if (!hasRole) {
|
||||
throw new ForbiddenException('سطح دسترسی شما کافی نیست');
|
||||
}
|
||||
|
||||
@ -27,15 +27,15 @@
|
||||
- [ ] **TASK-2.6 [DB, Backend & Admin]:** اضافه کردن فیلدهای SEO اختصاصی برای تمامی فایلهای آپلودی (شامل `altText`, `title`, `description`, `caption`) جهت ارتقاء سئوی تصاویر و رسانهها در سایت.
|
||||
|
||||
### 💰 اصلاح منطق قیمتگذاری (Price Logic Cleanup)
|
||||
- [ ] **TASK-2.7 [Admin Panel Cleanup]:** حذف فیلد زاید "نمایش قیمت (متنی)" از فرمهای محصول و فرمتبندی خودکار و استاندارد قیمت تومان بر اساس عدد اصلی `price` در فرانتاند سایت.
|
||||
- [x] **TASK-2.7 [Admin Panel Cleanup]:** حذف فیلد زاید "نمایش قیمت (متنی)" از فرمهای محصول و فرمتبندی خودکار و استاندارد قیمت تومان بر اساس عدد اصلی `price` در فرانتاند سایت.
|
||||
|
||||
### 🎨 استانداردسازی کامپوننتها و UI پنل ادمین
|
||||
- [ ] **TASK-2.8 [Admin Panel UI]:** بازطراحی و استانداردسازی دکمههای پنل ادمین (دکمههای انتخاب از گالری، حذف، ثبت و...) به صورت کامپوننت Reusable با رعایت `whitespace-nowrap` و استایل یکپارچه.
|
||||
- [ ] **TASK-2.9 [Admin Panel UI]:** بازطراحی کامپوننت Pagination در تمام صفحات ادمین (شامل صفحه اول/آخر، ۲ صفحه قبل/بعد، صفحه فعلی و قابلیت تایپ شماره صفحه جهت هدایت مستقیم).
|
||||
- [x] **TASK-2.9 [Admin Panel UI]:** بازطراحی کامپوننت Pagination در تمام صفحات ادمین (شامل صفحه اول/آخر، ۲ صفحه قبل/بعد، صفحه فعلی و قابلیت تایپ شماره صفحه جهت هدایت مستقیم).
|
||||
- [ ] **TASK-2.10 [Admin Panel UI]:** اضافه کردن Pagination استاندارد به تمام بخشهای مدیریت گالری رسانهها.
|
||||
|
||||
### 🔐 رفع باگهای دسترسی و راهنماهای ادمین (Permissions & Tooltips)
|
||||
- [ ] **TASK-2.11 [Admin Panel & Backend]:** بررسی و رفع خطای دسترسی "دسترسی شما به این بخش از پنل ادمین مجاز نمیباشد" در بخش CMS و بخش درخواستهای B2B.
|
||||
- [x] **TASK-2.11 [Admin Panel & Backend]:** بررسی و رفع خطای دسترسی "دسترسی شما به این بخش از پنل ادمین مجاز نمیباشد" در بخش CMS و بخش درخواستهای B2B.
|
||||
- [ ] **TASK-2.12 [Admin Panel UX]:** اضافه کردن آیکون علامت سوال (Help Tooltip) و راهنمای شفاف کنار تمامی فیلدها و بخشهای پنل ادمین جهت راهنمایی کامل ادمین.
|
||||
- [ ] **TASK-2.13 [Admin Panel UX]:** حذف کامل `alert()`های مرورگر و جایگزینی آنها با Toastهای مدرن و Modalهای Confirmation قبل از تمامی عملیاتهای حساس (حذف، ویرایش، تغییر وضعیت).
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number;
|
||||
@ -7,29 +8,126 @@ interface PaginationProps {
|
||||
}
|
||||
|
||||
export default function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) {
|
||||
const [jumpPage, setJumpPage] = useState('');
|
||||
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
// Generate page numbers: First, last, current, 2 before, 2 after
|
||||
const pages: number[] = [];
|
||||
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
const handleJump = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const p = parseInt(jumpPage, 10);
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPages) {
|
||||
onPageChange(p);
|
||||
setJumpPage('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 border-t border-gray-200 bg-gray-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 p-4 border-t border-gray-200 bg-gray-50 rounded-b-xl text-sm">
|
||||
{/* Right: Direct Jump Input */}
|
||||
<form onSubmit={handleJump} className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 font-medium">برو به صفحه:</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={totalPages}
|
||||
value={jumpPage}
|
||||
onChange={(e) => setJumpPage(e.target.value)}
|
||||
placeholder={String(currentPage)}
|
||||
className="w-16 px-2 py-1 text-center bg-white border border-gray-300 rounded-lg text-xs font-mono outline-none focus:border-purple-600 focus:ring-1 focus:ring-purple-600"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-2.5 py-1 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-xs font-medium transition whitespace-nowrap"
|
||||
>
|
||||
تایید
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Center/Left: Navigation Buttons */}
|
||||
<div className="flex items-center gap-1.5 dir-rtl">
|
||||
{/* First Page */}
|
||||
<button
|
||||
onClick={() => onPageChange(1)}
|
||||
disabled={currentPage === 1}
|
||||
title="صفحه اول"
|
||||
className="p-1.5 rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-purple-50 hover:border-purple-200 hover:text-purple-700 disabled:opacity-40 disabled:hover:bg-white disabled:hover:border-gray-200 disabled:hover:text-gray-600 transition"
|
||||
>
|
||||
<ChevronsRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Previous Page */}
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="p-2 rounded-lg border border-gray-200 bg-white text-gray-500 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
title="صفحه قبلی"
|
||||
className="p-1.5 rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-purple-50 hover:border-purple-200 hover:text-purple-700 disabled:opacity-40 disabled:hover:bg-white disabled:hover:border-gray-200 disabled:hover:text-gray-600 transition"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<span className="text-sm font-bold text-gray-700 px-4">
|
||||
صفحه {currentPage} از {totalPages}
|
||||
</span>
|
||||
{/* Dynamic Page Numbers */}
|
||||
{pages[0] > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onPageChange(1)}
|
||||
className="w-8 h-8 rounded-lg border border-gray-200 bg-white text-gray-700 hover:bg-purple-50 text-xs font-bold transition"
|
||||
>
|
||||
۱
|
||||
</button>
|
||||
{pages[0] > 2 && <span className="px-1 text-gray-400 text-xs">...</span>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{pages.map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => onPageChange(p)}
|
||||
className={`w-8 h-8 rounded-lg border text-xs font-bold transition ${
|
||||
p === currentPage
|
||||
? 'bg-purple-600 text-white border-purple-600 shadow-sm'
|
||||
: 'bg-white text-gray-700 border-gray-200 hover:bg-purple-50 hover:border-purple-200 hover:text-purple-700'
|
||||
}`}
|
||||
>
|
||||
{p.toLocaleString('fa-IR')}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{pages[pages.length - 1] < totalPages && (
|
||||
<>
|
||||
{pages[pages.length - 1] < totalPages - 1 && <span className="px-1 text-gray-400 text-xs">...</span>}
|
||||
<button
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
className="w-8 h-8 rounded-lg border border-gray-200 bg-white text-gray-700 hover:bg-purple-50 text-xs font-bold transition"
|
||||
>
|
||||
{totalPages.toLocaleString('fa-IR')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Next Page */}
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="p-2 rounded-lg border border-gray-200 bg-white text-gray-500 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
title="صفحه بعدی"
|
||||
className="p-1.5 rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-purple-50 hover:border-purple-200 hover:text-purple-700 disabled:opacity-40 disabled:hover:bg-white disabled:hover:border-gray-200 disabled:hover:text-gray-600 transition"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Last Page */}
|
||||
<button
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
title="صفحه آخر"
|
||||
className="p-1.5 rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-purple-50 hover:border-purple-200 hover:text-purple-700 disabled:opacity-40 disabled:hover:bg-white disabled:hover:border-gray-200 disabled:hover:text-gray-600 transition"
|
||||
>
|
||||
<ChevronsLeft className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -533,8 +533,23 @@ export default function Products() {
|
||||
<div className={activeTab === 'pricing' ? 'block space-y-4' : 'hidden'}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-gray-700">قیمت (تومان) *</label>
|
||||
<input required type="number" min="0" value={formData.priceValue} onChange={(e) => setFormData({...formData, priceValue: Number(e.target.value)})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="ltr" />
|
||||
<label className="text-sm font-bold text-gray-700">قیمت اصلی (تومان) *</label>
|
||||
<input
|
||||
required
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.priceValue}
|
||||
onChange={(e) => {
|
||||
const val = Number(e.target.value);
|
||||
setFormData({
|
||||
...formData,
|
||||
priceValue: val,
|
||||
priceDisplay: `${val.toLocaleString('fa-IR')} تومان`,
|
||||
});
|
||||
}}
|
||||
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-purple-700 flex items-center gap-1">
|
||||
@ -543,10 +558,6 @@ export default function Products() {
|
||||
</label>
|
||||
<input type="number" min="0" placeholder="در صورت عدم ورود، درصد کلی اعمال میشود" value={formData.wholesalePrice} onChange={(e) => setFormData({...formData, wholesalePrice: e.target.value ? Number(e.target.value) : ''})} className="w-full px-4 py-3 rounded-xl border border-purple-200 focus:border-purple-600 outline-none font-bold bg-purple-50/20" dir="ltr" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-gray-700">نمایش قیمت (متنی) *</label>
|
||||
<input required type="text" placeholder="مثال: ۸۵۰,۰۰۰ تومان" value={formData.priceDisplay} onChange={(e) => setFormData({...formData, priceDisplay: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-gray-700">موجودی *</label>
|
||||
<input required type="number" min="0" value={formData.packageSize} onChange={(e) => setFormData({...formData, packageSize: Number(e.target.value)})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="ltr" />
|
||||
@ -558,6 +569,7 @@ export default function Products() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* SEO Tab */}
|
||||
<div className={activeTab === 'seo' ? 'block' : 'hidden'}>
|
||||
{(() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user