fix(admin): cleanup price display input, upgrade pagination component and extend roles guard for CMS & B2B

This commit is contained in:
parsa aghaei 2026-08-01 18:30:14 +03:30
parent 2aea3bded4
commit 2ce24b0473
4 changed files with 132 additions and 21 deletions

View File

@ -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('سطح دسترسی شما کافی نیست');
}

View File

@ -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 قبل از تمامی عملیات‌های حساس (حذف، ویرایش، تغییر وضعیت).

View File

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

View File

@ -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'}>
{(() => {