Compare commits

...

2 Commits

Author SHA1 Message Date
parsa aghaei
81a2837c04 feat: restore profile pets dropdown, fix logout handler, cleanup task titles, fix maintenance guard, add catalog settings & shipping fee calculation
Some checks are pending
Deploy Canina / deploy (push) Waiting to run
2026-08-05 14:17:13 +03:30
parsa aghaei
c79b0fa394 docs: add tasks 25.1 to 25.14 to backlog 2026-08-05 14:10:42 +03:30
6 changed files with 211 additions and 111 deletions

View File

@ -177,76 +177,20 @@
## 📍 بخش ۲۴: آیکون راهنما و Tooltip کدهای تخفیف (Discount Code Field Info Tooltip) ## 📍 بخش ۲۴: آیکون راهنما و Tooltip کدهای تخفیف (Discount Code Field Info Tooltip)
- [x] **TASK-24.1 [Admin Panel & Frontend UI]:** اضافه کردن آیکون راهنما (Info Tooltip) به فیلدهای کد تخفیف با توضیحات شفاف درباره کارکرد فیلد و تغییرات ناشی از ویرایش آن. - [x] **TASK-24.1 [Admin Panel & Frontend UI]:** اضافه کردن آیکون راهنما (Info Tooltip) به فیلدهای کد تخفیف با توضیحات شفاف درباره کارکرد فیلد و تغییرات ناشی از ویرایش آن.
--- ---
## 📍 بخش ۲۵: بازگرداندن Dropdown منوی پروفایل کاربر در هدر (User Profile Pet Dropdown Header) ## 📍 بخش ۲۵: بازبینی، اصلاح خطاها و توسعه قابلیت‌های درخواستی جدید (Batch Tasks 25.1 - 25.14)
- [ ] **TASK-25.1 [Frontend Site UI]:** بازگرداندن منوی کشویی پروفایل کاربر در هدر سایت شامل نمایش لیست پت‌های ثبت‌شده، دسترسی سریع به مدیریت پت‌ها و خروج از حساب. - [ ] **TASK-25.1 [Frontend Header Dropdown]:** بازگرداندن Dropdown سریع اطلاعات کاربری و لیست پت‌ها در هدر اصلی فرانت‌اند (Header Profile Dropdown with Pets List).
- [ ] **TASK-25.2 [Admin Panel Topbar]:** اصلاح کارکرد دکمه «خروج از حساب» در Dropdown پروفایل ادمین پنل (Fix Logout Handler in Admin Topbar).
--- - [ ] **TASK-25.3 [Admin Panel Users Page]:** تکمیل مدیریت کامل کاربران (مشاهده تمامی فیلدها، ویرایش اطلاعات کاربر، تغییر نقش، مسدودی و...).
- [ ] **TASK-25.4 [Health Calculator UI]:** اصلاح فیلدهای گام دوم پایش فیزیکی در چک‌آپ سلامت جهت امکان تایپ مستقیم عددی علاوه بر دکمه‌های کم و زیاد.
## 📍 بخش ۲۶: اصلاح خروج از حساب کاربری در پنل ادمین (Admin Panel Logout & Profile Dropdown) - [ ] **TASK-25.5 [Health Supplement Tracking Logic]:** پیاده‌سازی کامل منطق و الگوریتم پایش موجودی مکمل‌ها براساس تاریخ مصرف و سفارشات واقعی پت.
- [ ] **TASK-26.1 [Admin Panel UI]:** برطرف کردن خطای منوی کشویی پروفایل ادمین در پنل مدیریت و فعال‌سازی کامل دکمه «خروج از حساب». - [ ] **TASK-25.6 [Backend Exception & Error Handling System]:** اصلاح منطق پاسخ‌دهی خطاهای بهینه‌شده به تفکیک پارامترها و جلوگیری از نمایش پیام‌های کلی غیرمنتظره و توست‌های تکراری.
- [ ] **TASK-25.7 [Wallet Checkout Error Handling]:** اصلاح هندلینگ خطای ورودی هنگام پرداخت با کیف پول و نمایش دقیق فیلد یا دلیل خطای پرداخت.
--- - [ ] **TASK-25.8 [Address Add & Refresh Bug Fix]:** رفع مشکل خطای اضافه کردن آدرس، جلوگیری از نمایش توست‌های تکراری و بروزرسانی خودکار لیست آدرس‌های کاربر.
- [ ] **TASK-25.9 [Shipping Fee Calculation Fix]:** برطرف کردن خطای محاسبه هزینه ثابت ارسال در فاکتور و فاز تسویه‌حساب براساس تنظیمات پنل ادمین.
## 📍 بخش ۲۷: مشاهده و ویرایش کامل اطلاعات کاربر در پنل ادمین (Admin Full User Profile View & Edit) - [ ] **TASK-25.10 [Admin Panel UI Cleanup]:** پاکسازی پسوند کد تپ‌ک‌ها یا کدهای درون پرانتز (مانند TASK-x) از عناوین صفحات و منوهای پنل ادمین.
- [ ] **TASK-27.1 [Admin Panel & Backend]:** فراهم کردن امکان مشاهده کلیه اطلاعات کاربر (مشخصات، شماره تماس، آدرس‌ها، پت‌ها) و ویرایش کامل آن‌ها در پنل مدیریت به‌جای تغییر صرف نقش. - [ ] **TASK-25.11 [Maintenance Mode Protection Guard]:** اصلاح و فعال‌سازی عملکرد گارد «حالت تعمیرات» (Maintenance Mode) در فرانت‌اند برای مسدودسازی دسترسی عمومی.
- [ ] **TASK-25.12 [Flexible Catalog-Only Mode Settings]:** توسعه تنظیمات چندحالته «حالت کاتالوگ» (نمایش/عدم نمایش قیمت، فعال/غیرفعال‌سازی سبد خرید و فاکتور، تبدیل به پیش‌خرید).
--- - [ ] **TASK-25.13 [Product Pre-order System]:** ایجاد امکان پیش‌خرید کالا در پنل ادمین (رایگان یا با بیعانه) و ثبت نهایی بعد از موجود شدن.
- [ ] **TASK-25.14 [Stock Alert Queue & SMS Notification]:** ایجاد قابلیت «موجود شد اطلاع بده» برای کالاهای ناموجود، صف خودکار اطلاع‌رسانی و ارسال پیامک به محض شارژ موجودی.
## 📍 بخش ۲۸: ورود تایپی و مستقیم اعداد در پایش فیزیکی (Direct Numeric Input in Physical Monitoring)
- [ ] **TASK-28.1 [Frontend Site UI]:** اضافه کردن قابلیت ورود تایپی و مستقیم عدد به تمام اینپوت‌های گام دوم پایش فیزیکی در کنار دکمه‌های + و -.
---
## 📍 بخش ۲۹: منطق واقعی و کاربردی پایش هوشمند موجودی مکمل‌ها (Smart Supplement Consumption Tracking Logic)
- [ ] **TASK-29.1 [Frontend & Backend]:** پیاده‌سازی منطق واقعی محاسبه میزان مصرف روزانه براساس سفارشات ثبت‌شده، دوز مصرفی و تعداد باقی‌مانده مکمل کاربر و امکان ثبت دستی مکمل جهت پایش.
---
## 📍 بخش ۳۰: شفاف‌سازی و هندلینگ استاندارد خطاهای پردازش پرداخت (Detailed Checkout Payment Error Toast Fix)
- [ ] **TASK-30.1 [Frontend Site]:** رفع دوبار نمایش داده شدن Toast خطا در زمان ثبت سفارش، دریافت دقیق پیام خطا از پاسخ backend و نمایش پیام شفاف و کاربردی.
---
## 📍 بخش ۳۱: اصلاح عیب‌یابی پرداخت با کیف پول (Wallet Payment Validation & Error Handling)
- [ ] **TASK-31.1 [Backend & Frontend]:** بررسی دقیق پارامترهای پردازش پرداخت کیف پول و نمایش دقیق فیلد یا علت نامعتبر بودن درخواست به کاربر.
---
## 📍 بخش ۳۲: اصلاح فرآیند ذخیره و نمایش آدرس جدید (Address Creation Double Toast & List Sync Fix)
- [ ] **TASK-32.1 [Frontend Site]:** برطرف کردن خطای `property addresses should not exist` در ثبت آدرس، حذف Toastهای تکراری و به‌روزرسانی آنی لیست آدرس‌های منتخب کاربر در تسویه‌حساب.
---
## 📍 بخش ۳۳: اعمال هزینه واقعی ارسال سفارشی و پیک در تسویه‌حساب (Shipping Cost Admin Sync)
- [ ] **TASK-33.1 [Backend & Frontend]:** خواندن هزینه ارسال تنظیم‌شده در پنل ادمین (مثلاً ۱۰۰,۰۰۰ تومان) و محاسبه دقیق آن در فاکتور نهایی تسویه‌حساب به‌جای رایگان فرض کردن آن.
---
## 📍 بخش ۳۴: پاک‌سازی عبارت‌های فنی (TASK-X) از عنوان‌های پنل ادمین (Admin Titles Clean-up)
- [ ] **TASK-34.1 [Admin Panel UI]:** حذف کدهای شناسه تاسک (`TASK-2.15` و غیره) از تمام عنوان‌ها و صفحات پنل مدیریت ادمین.
---
## 📍 بخش ۳۵: اصلاح عملکرد واقعی حالت تعمیرات (Real Maintenance Mode Enforcement)
- [ ] **TASK-35.1 [Frontend Site]:** اصلاح بررسی حالت تعمیرات (Maintenance Mode) در `ClientLayout` به‌طوری‌که حتی کاربران لاگین‌شده معمولی نیز صفحه Coming Soon را مشاهده کنند و صرفاً ادمین مجاز به عبور باشد.
---
## 📍 بخش ۳۶: تفکیک دقیق گزینه‌های حالت کاتالوگ و پیش‌خرید (Granular Catalog Mode Sub-options & Pre-order Button)
- [ ] **TASK-36.1 [Admin Panel & Frontend]:** پیاده‌سازی زیرگزینه‌های کاتالوگ‌مود (غیرفعال‌سازی ثبت سفارش، جایگزینی دکمه پیش‌خرید به‌جای خرید، غیرفعال‌سازی قیمت و سبد) در تنظیمات ادمین و اعمال آن در فرانت‌اند.
---
## 📍 بخش ۳۷: سیستم پیش‌خرید محصولات رایگان یا بیعانه‌ای (Pre-order System with SMS Notification)
- [ ] **TASK-37.1 [DB, Backend & Frontend]:** ساخت سیستم پیش‌خرید محصولات Coming Soon (رایگان یا با بیعانه) و ارسال پیامک خودکار به کاربر هنگام موجود شدن و قیمت‌گذاری کالا جهت تکمیل سفارش.
---
## 📍 بخش ۳۸: صف خودکار «موجود شد مطلعم کن» با ارسال پیامک (Out-of-Stock Notification Queue & SMS)
- [ ] **TASK-38.1 [DB, Backend & Frontend]:** ایجاد دکمه «موجود شد خبرم کن» برای کالاهای ناموجود، ثبت کاربر در صف (Queue) و ارسال خودکار پیامک هنگام افزایش موجودی کالا به بالای ۰.

View File

@ -190,7 +190,10 @@ export default function Topbar({ toggleMenu }: TopbarProps) {
</button> </button>
<div className="border-t border-gray-100 my-1"></div> <div className="border-t border-gray-100 my-1"></div>
<button <button
onClick={handleLogout} onClick={() => {
setShowUserMenu(false);
handleLogout();
}}
className="w-full text-right px-3 py-2 text-xs font-bold text-red-600 hover:bg-red-50 rounded-xl transition-colors flex items-center justify-between" className="w-full text-right px-3 py-2 text-xs font-bold text-red-600 hover:bg-red-50 rounded-xl transition-colors flex items-center justify-between"
> >
<span>خروج از حساب</span> <span>خروج از حساب</span>

View File

@ -11,6 +11,10 @@ export default function Settings() {
B2B_DISCOUNT_PERCENT: '', B2B_DISCOUNT_PERCENT: '',
MAINTENANCE_MODE: 'false', MAINTENANCE_MODE: 'false',
CATALOG_ONLY_MODE: 'false', CATALOG_ONLY_MODE: 'false',
CATALOG_SHOW_PRICES: 'true',
CATALOG_ALLOW_CART: 'false',
CATALOG_ALLOW_CHECKOUT: 'false',
CATALOG_PREORDER_BTN: 'false',
CHARITY_ROUND_STEP: '10000', CHARITY_ROUND_STEP: '10000',
PAY_GATEWAY_CARD_ENABLE: 'true', PAY_GATEWAY_CARD_ENABLE: 'true',
PAY_GATEWAY_WALLET_ENABLE: 'true', PAY_GATEWAY_WALLET_ENABLE: 'true',
@ -41,6 +45,10 @@ export default function Settings() {
B2B_DISCOUNT_PERCENT: response.data.data.B2B_DISCOUNT_PERCENT || '0', B2B_DISCOUNT_PERCENT: response.data.data.B2B_DISCOUNT_PERCENT || '0',
MAINTENANCE_MODE: response.data.data.MAINTENANCE_MODE || 'false', MAINTENANCE_MODE: response.data.data.MAINTENANCE_MODE || 'false',
CATALOG_ONLY_MODE: response.data.data.CATALOG_ONLY_MODE || 'false', CATALOG_ONLY_MODE: response.data.data.CATALOG_ONLY_MODE || 'false',
CATALOG_SHOW_PRICES: response.data.data.CATALOG_SHOW_PRICES || 'true',
CATALOG_ALLOW_CART: response.data.data.CATALOG_ALLOW_CART || 'false',
CATALOG_ALLOW_CHECKOUT: response.data.data.CATALOG_ALLOW_CHECKOUT || 'false',
CATALOG_PREORDER_BTN: response.data.data.CATALOG_PREORDER_BTN || 'false',
CHARITY_ROUND_STEP: response.data.data.CHARITY_ROUND_STEP || '10000', CHARITY_ROUND_STEP: response.data.data.CHARITY_ROUND_STEP || '10000',
PAY_GATEWAY_CARD_ENABLE: response.data.data.PAY_GATEWAY_CARD_ENABLE || 'true', PAY_GATEWAY_CARD_ENABLE: response.data.data.PAY_GATEWAY_CARD_ENABLE || 'true',
PAY_GATEWAY_WALLET_ENABLE: response.data.data.PAY_GATEWAY_WALLET_ENABLE || 'true', PAY_GATEWAY_WALLET_ENABLE: response.data.data.PAY_GATEWAY_WALLET_ENABLE || 'true',
@ -320,11 +328,11 @@ export default function Settings() {
</div> </div>
</div> </div>
{/* Brand Identity & Theme Settings (TASK-2.15) */} {/* Brand Identity & Theme Settings */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6 lg:col-span-2"> <div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6 lg:col-span-2">
<h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2"> <h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2">
<SettingsIcon className="w-5 h-5 text-purple-600" /> <SettingsIcon className="w-5 h-5 text-purple-600" />
مدیریت هویت برند، نمادها و ظاهر سایت (TASK-2.15) مدیریت هویت برند، نمادها و ظاهر سایت
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@ -386,11 +394,11 @@ export default function Settings() {
</div> </div>
</div> </div>
{/* System Settings (TASK-2.14) */} {/* System Settings */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6 lg:col-span-2"> <div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6 lg:col-span-2">
<h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2"> <h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2">
<ShieldAlert className="w-5 h-5 text-orange-500" /> <ShieldAlert className="w-5 h-5 text-orange-500" />
وضعیت سیستم و کنترل حالت کاتالوگ (TASK-2.14) وضعیت سیستم و کنترل حالت کاتالوگ
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
@ -413,12 +421,13 @@ export default function Settings() {
</label> </label>
</div> </div>
<div className="flex items-center justify-between p-4 bg-purple-50 rounded-xl border border-purple-100"> <div className="p-4 bg-purple-50 rounded-xl border border-purple-100 space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<AlertCircle className="w-6 h-6 text-purple-600" /> <AlertCircle className="w-6 h-6 text-purple-600" />
<div> <div>
<h4 className="font-bold text-gray-900 text-sm">حالت کاتالوگ بدون خرید (Catalog Only Mode)</h4> <h4 className="font-bold text-gray-900 text-sm">حالت کاتالوگ بدون خرید (Catalog Only Mode)</h4>
<p className="text-xs text-gray-600 mt-0.5">با فعال‌سازی این گزینه، دکمه‌های افزودن به سبد خرید غیرفعال شده و سایت به کاتالوگ آنلاین تبدیل می‌شود.</p> <p className="text-xs text-gray-600 mt-0.5">با فعال‌سازی این گزینه، فروش مستقیم کالا غیرفعال شده و سایت به کاتالوگ هوشمند آنلاین تبدیل می‌شود.</p>
</div> </div>
</div> </div>
<label className="relative inline-flex items-center cursor-pointer"> <label className="relative inline-flex items-center cursor-pointer">
@ -431,6 +440,51 @@ export default function Settings() {
<div className="w-14 h-7 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-purple-600"></div> <div className="w-14 h-7 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-purple-600"></div>
</label> </label>
</div> </div>
{settings.CATALOG_ONLY_MODE === 'true' && (
<div className="pt-3 border-t border-purple-200/60 grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<label className="flex items-center gap-2 font-bold text-gray-800 cursor-pointer">
<input
type="checkbox"
checked={settings.CATALOG_SHOW_PRICES === 'true'}
onChange={(e) => setSettings({ ...settings, CATALOG_SHOW_PRICES: e.target.checked ? 'true' : 'false' })}
className="rounded text-purple-600 focus:ring-purple-500"
/>
<span>نمایش قیمت محصولات در حالت کاتالوگ</span>
</label>
<label className="flex items-center gap-2 font-bold text-gray-800 cursor-pointer">
<input
type="checkbox"
checked={settings.CATALOG_ALLOW_CART === 'true'}
onChange={(e) => setSettings({ ...settings, CATALOG_ALLOW_CART: e.target.checked ? 'true' : 'false' })}
className="rounded text-purple-600 focus:ring-purple-500"
/>
<span>امکان افزودن به سبد خرید</span>
</label>
<label className="flex items-center gap-2 font-bold text-gray-800 cursor-pointer">
<input
type="checkbox"
checked={settings.CATALOG_ALLOW_CHECKOUT === 'true'}
onChange={(e) => setSettings({ ...settings, CATALOG_ALLOW_CHECKOUT: e.target.checked ? 'true' : 'false' })}
className="rounded text-purple-600 focus:ring-purple-500"
/>
<span>امکان ورود به صفحه تسویه‌حساب (ثبت فاکتور)</span>
</label>
<label className="flex items-center gap-2 font-bold text-gray-800 cursor-pointer">
<input
type="checkbox"
checked={settings.CATALOG_PREORDER_BTN === 'true'}
onChange={(e) => setSettings({ ...settings, CATALOG_PREORDER_BTN: e.target.checked ? 'true' : 'false' })}
className="rounded text-purple-600 focus:ring-purple-500"
/>
<span>نمایش دکمه «ثبت پیش‌خرید» به جای ثبت سفارش</span>
</label>
</div>
)}
</div>
</div> </div>
</div> </div>

View File

@ -42,7 +42,7 @@ export default function ClientLayout({ children }: { children: React.ReactNode }
}, [fetchProfile, fetchSettings]); }, [fetchProfile, fetchSettings]);
// Check Maintenance Mode (Admin / Partner bypass) // Check Maintenance Mode (Admin / Partner bypass)
const isMaintenanceMode = getText('maintenance_mode', 'false') === 'true'; const isMaintenanceMode = getText('MAINTENANCE_MODE', 'false') === 'true' || getText('maintenance_mode', 'false') === 'true';
const isAdmin = role === 'User_Partner' || (typeof window !== 'undefined' && Boolean(localStorage.getItem('adminToken'))); const isAdmin = role === 'User_Partner' || (typeof window !== 'undefined' && Boolean(localStorage.getItem('adminToken')));
if (isMaintenanceMode && !isAdmin) { if (isMaintenanceMode && !isAdmin) {

View File

@ -76,6 +76,8 @@ export default function CheckoutPage() {
}, [profile, isLoggedIn]); }, [profile, isLoggedIn]);
const roundStep = Number(useSettingsStore((state) => state.getText('CHARITY_ROUND_STEP', '10000'))) || 10000; const roundStep = Number(useSettingsStore((state) => state.getText('CHARITY_ROUND_STEP', '10000'))) || 10000;
const shippingFeeSetting = useSettingsStore((state) => state.getText('shipping_fee', '0'));
const shippingFee = Number(shippingFeeSetting) || 0;
const subtotal = getSubtotal(); const subtotal = getSubtotal();
const remainder = subtotal % roundStep; const remainder = subtotal % roundStep;
const roundUpDiff = remainder === 0 ? roundStep : roundStep - remainder; const roundUpDiff = remainder === 0 ? roundStep : roundStep - remainder;
@ -91,7 +93,7 @@ export default function CheckoutPage() {
}; };
const handleFinalize = async () => { const handleFinalize = async () => {
const totalAmount = getTotal(); const totalAmount = getTotal() + shippingFee;
setLoading(true); setLoading(true);
const activePet = getActivePet(); const activePet = getActivePet();
@ -618,12 +620,16 @@ export default function CheckoutPage() {
)} )}
<div className="flex justify-between text-sm font-bold text-white/80"> <div className="flex justify-between text-sm font-bold text-white/80">
<span>هزینه ارسال</span> <span>هزینه ارسال</span>
{shippingFee > 0 ? (
<span className="font-vazir font-bold text-white">{toPersian(shippingFee.toLocaleString())} تومان</span>
) : (
<span className="text-green-400 uppercase tracking-widest text-[10px] font-black">رایگان</span> <span className="text-green-400 uppercase tracking-widest text-[10px] font-black">رایگان</span>
)}
</div> </div>
<div className="flex justify-between items-end pt-4"> <div className="flex justify-between items-end pt-4">
<span className="text-xl font-black italic text-white">مبلغ نهایی</span> <span className="text-xl font-black italic text-white">مبلغ نهایی</span>
<div className="text-left"> <div className="text-left">
<p className="text-3xl font-black text-canina-gold leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p> <p className="text-3xl font-black text-canina-gold leading-none font-vazir">{toPersian((getTotal() + shippingFee).toLocaleString())}</p>
<p className="text-[10px] text-white/50 font-bold mt-1">تومان</p> <p className="text-[10px] text-white/50 font-bold mt-1">تومان</p>
</div> </div>
</div> </div>
@ -728,13 +734,17 @@ export default function CheckoutPage() {
<AddressModal <AddressModal
isOpen={showAddressModal} isOpen={showAddressModal}
onClose={() => setShowAddressModal(false)} onClose={() => setShowAddressModal(false)}
onSave={(newAddr) => { onSave={async (newAddr) => {
const currentAddresses = profile.addresses || []; try {
const updatedAddresses = [newAddr, ...currentAddresses]; await useUserStore.getState().addAddress(newAddr);
updateProfile({ addresses: updatedAddresses }); const updatedProfile = useUserStore.getState().profile;
setSelectedAddressId(newAddr.id); if (updatedProfile.addresses && updatedProfile.addresses.length > 0) {
setSelectedAddressId(updatedProfile.addresses[0].id);
}
setShowAddressModal(false); setShowAddressModal(false);
toast.success('آدرس جدید اضافه شد و به عنوان آدرس تحویل انتخاب گردید.'); } catch (e) {
console.error("Failed to add address", e);
}
}} }}
/> />
</div> </div>

View File

@ -43,6 +43,8 @@ export default function Header({
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false); const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
const [isPrescriptionModalOpen, setIsPrescriptionModalOpen] = useState(false); const [isPrescriptionModalOpen, setIsPrescriptionModalOpen] = useState(false);
const [isPetSwitcherOpen, setIsPetSwitcherOpen] = useState(false); const [isPetSwitcherOpen, setIsPetSwitcherOpen] = useState(false);
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
const userProfileRef = useRef<HTMLDivElement>(null);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
@ -114,6 +116,9 @@ export default function Header({
if (petMenuRef.current && !petMenuRef.current.contains(event.target as Node)) { if (petMenuRef.current && !petMenuRef.current.contains(event.target as Node)) {
setIsPetSwitcherOpen(false); setIsPetSwitcherOpen(false);
} }
if (userProfileRef.current && !userProfileRef.current.contains(event.target as Node)) {
setIsProfileMenuOpen(false);
}
} }
document.addEventListener("mousedown", handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside);
@ -193,16 +198,100 @@ export default function Header({
<span>ثبت نسخه دامپزشک</span> <span>ثبت نسخه دامپزشک</span>
</button> </button>
{/* User Account Button */} {/* User Account Button with Dropdown & Pets List */}
{isLoggedIn ? ( {isLoggedIn ? (
<Link <div className="relative" ref={userProfileRef}>
href="/dashboard" <button
className="flex items-center justify-center p-2.5 sm:px-3 sm:py-2 bg-medical-gray-50 hover:bg-medical-gray-100 border border-medical-gray-200 rounded-xl text-xs font-black text-medical-gray-900 transition-all" onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
title="حساب کاربری" className="flex items-center gap-1.5 p-2 sm:px-3 sm:py-2 bg-medical-gray-50 hover:bg-medical-gray-100 border border-medical-gray-200 rounded-xl text-xs font-black text-medical-gray-900 transition-all cursor-pointer"
> >
<User className="w-4 h-4 text-canina-blue" /> <User className="w-4 h-4 text-canina-blue" />
<span className="hidden sm:inline">{profile.firstName || 'حساب کاربری'}</span> <span className="hidden sm:inline">{profile.firstName || 'حساب کاربری'}</span>
<ChevronDown className={cn("w-3.5 h-3.5 text-medical-gray-500 transition-transform duration-200", isProfileMenuOpen && "rotate-180")} />
</button>
<AnimatePresence>
{isProfileMenuOpen && (
<motion.div
initial={{ opacity: 0, y: 8, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 8, scale: 0.96 }}
className="absolute left-0 mt-2 w-64 bg-white rounded-2xl shadow-xl border border-medical-gray-100 overflow-hidden z-50 p-2 text-xs font-vazir"
>
{/* User info header */}
<div className="px-3 py-2.5 bg-medical-gray-50 rounded-xl mb-1">
<p className="font-black text-medical-gray-900 truncate">
{profile.firstName ? `${profile.firstName} ${profile.lastName || ''}` : 'کاربر عزیز'}
</p>
<p className="text-[10px] text-medical-gray-500 font-mono mt-0.5 truncate">{profile.mobile || profile.email}</p>
</div>
{/* Pets List Section */}
<div className="py-1">
<div className="flex items-center justify-between px-3 py-1.5 text-[11px] font-black text-medical-gray-500">
<span className="flex items-center gap-1">
<HeartPulse className="w-3.5 h-3.5 text-canina-blue" />
پت‌های من ({pets.length})
</span>
<Link href="/dashboard" onClick={() => setIsProfileMenuOpen(false)} className="text-canina-blue hover:underline text-[10px]">
مدیریت
</Link> </Link>
</div>
{pets.length > 0 ? (
<div className="space-y-1 max-h-32 overflow-y-auto">
{pets.map((p) => (
<button
key={p.id}
onClick={() => {
setActivePet(p.id);
setIsProfileMenuOpen(false);
toast.success(`پت فعال به ${p.name} تغییر یافت`);
}}
className={cn(
"w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-right transition-colors",
p.id === activePetId ? "bg-canina-blue/10 text-canina-blue font-bold" : "hover:bg-medical-gray-50 text-medical-gray-700"
)}
>
<div className="flex items-center gap-2 truncate">
<span className="text-sm">{(p.type === 'سگ' || p.type === ('dog' as any)) ? '🐶' : '🐱'}</span>
<span className="truncate">{p.name}</span>
</div>
{p.id === activePetId && <Check className="w-3.5 h-3.5 shrink-0" />}
</button>
))}
</div>
) : (
<p className="px-3 py-2 text-[10px] text-medical-gray-400 text-center">پتی ثبت نشده است</p>
)}
</div>
<div className="border-t border-medical-gray-100 my-1" />
{/* Direct Navigation Links */}
<Link
href="/dashboard"
onClick={() => setIsProfileMenuOpen(false)}
className="flex items-center gap-2 px-3 py-2 hover:bg-medical-gray-50 text-medical-gray-700 rounded-xl font-bold transition-colors"
>
<User className="w-4 h-4 text-canina-blue" />
<span>داشبورد کاربری</span>
</Link>
<button
onClick={() => {
logout();
setIsProfileMenuOpen(false);
toast.success('از حساب کاربری خارج شدید');
}}
className="w-full flex items-center gap-2 px-3 py-2 hover:bg-rose-50 text-rose-600 rounded-xl font-bold transition-colors text-right"
>
<LogOut className="w-4 h-4" />
<span>خروج از حساب</span>
</button>
</motion.div>
)}
</AnimatePresence>
</div>
) : ( ) : (
<button <button
onClick={() => setIsAuthModalOpen(true)} onClick={() => setIsAuthModalOpen(true)}