Compare commits
No commits in common. "ce30ade80880d98da8809563fd01d59dd328178e" and "81a2837c04b820e93355831133bf035e1697efa1" have entirely different histories.
ce30ade808
...
81a2837c04
@ -180,17 +180,17 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 📍 بخش ۲۵: بازبینی، اصلاح خطاها و توسعه قابلیتهای درخواستی جدید (Batch Tasks 25.1 - 25.14)
|
## 📍 بخش ۲۵: بازبینی، اصلاح خطاها و توسعه قابلیتهای درخواستی جدید (Batch Tasks 25.1 - 25.14)
|
||||||
- [x] **TASK-25.1 [Frontend Header Dropdown]:** بازگرداندن Dropdown سریع اطلاعات کاربری و لیست پتها در هدر اصلی فرانتاند (Header Profile Dropdown with Pets List).
|
- [ ] **TASK-25.1 [Frontend Header Dropdown]:** بازگرداندن Dropdown سریع اطلاعات کاربری و لیست پتها در هدر اصلی فرانتاند (Header Profile Dropdown with Pets List).
|
||||||
- [x] **TASK-25.2 [Admin Panel Topbar]:** اصلاح کارکرد دکمه «خروج از حساب» در Dropdown پروفایل ادمین پنل (Fix Logout Handler in Admin Topbar).
|
- [ ] **TASK-25.2 [Admin Panel Topbar]:** اصلاح کارکرد دکمه «خروج از حساب» در Dropdown پروفایل ادمین پنل (Fix Logout Handler in Admin Topbar).
|
||||||
- [x] **TASK-25.3 [Admin Panel Users Page]:** تکمیل مدیریت کامل کاربران (مشاهده تمامی فیلدها، ویرایش اطلاعات کاربر، تغییر نقش، مسدودی و...).
|
- [ ] **TASK-25.3 [Admin Panel Users Page]:** تکمیل مدیریت کامل کاربران (مشاهده تمامی فیلدها، ویرایش اطلاعات کاربر، تغییر نقش، مسدودی و...).
|
||||||
- [x] **TASK-25.4 [Health Calculator UI]:** اصلاح فیلدهای گام دوم پایش فیزیکی در چکآپ سلامت جهت امکان تایپ مستقیم عددی علاوه بر دکمههای کم و زیاد.
|
- [ ] **TASK-25.4 [Health Calculator UI]:** اصلاح فیلدهای گام دوم پایش فیزیکی در چکآپ سلامت جهت امکان تایپ مستقیم عددی علاوه بر دکمههای کم و زیاد.
|
||||||
- [x] **TASK-25.5 [Health Supplement Tracking Logic]:** پیادهسازی کامل منطق و الگوریتم پایش موجودی مکملها براساس تاریخ مصرف و سفارشات واقعی پت.
|
- [ ] **TASK-25.5 [Health Supplement Tracking Logic]:** پیادهسازی کامل منطق و الگوریتم پایش موجودی مکملها براساس تاریخ مصرف و سفارشات واقعی پت.
|
||||||
- [x] **TASK-25.6 [Backend Exception & Error Handling System]:** اصلاح منطق پاسخدهی خطاهای بهینهشده به تفکیک پارامترها و جلوگیری از نمایش پیامهای کلی غیرمنتظره و توستهای تکراری.
|
- [ ] **TASK-25.6 [Backend Exception & Error Handling System]:** اصلاح منطق پاسخدهی خطاهای بهینهشده به تفکیک پارامترها و جلوگیری از نمایش پیامهای کلی غیرمنتظره و توستهای تکراری.
|
||||||
- [x] **TASK-25.7 [Wallet Checkout Error Handling]:** اصلاح هندلینگ خطای ورودی هنگام پرداخت با کیف پول و نمایش دقیق فیلد یا دلیل خطای پرداخت.
|
- [ ] **TASK-25.7 [Wallet Checkout Error Handling]:** اصلاح هندلینگ خطای ورودی هنگام پرداخت با کیف پول و نمایش دقیق فیلد یا دلیل خطای پرداخت.
|
||||||
- [x] **TASK-25.8 [Address Add & Refresh Bug Fix]:** رفع مشکل خطای اضافه کردن آدرس، جلوگیری از نمایش توستهای تکراری و بروزرسانی خودکار لیست آدرسهای کاربر.
|
- [ ] **TASK-25.8 [Address Add & Refresh Bug Fix]:** رفع مشکل خطای اضافه کردن آدرس، جلوگیری از نمایش توستهای تکراری و بروزرسانی خودکار لیست آدرسهای کاربر.
|
||||||
- [x] **TASK-25.9 [Shipping Fee Calculation Fix]:** برطرف کردن خطای محاسبه هزینه ثابت ارسال در فاکتور و فاز تسویهحساب براساس تنظیمات پنل ادمین.
|
- [ ] **TASK-25.9 [Shipping Fee Calculation Fix]:** برطرف کردن خطای محاسبه هزینه ثابت ارسال در فاکتور و فاز تسویهحساب براساس تنظیمات پنل ادمین.
|
||||||
- [x] **TASK-25.10 [Admin Panel UI Cleanup]:** پاکسازی پسوند کد تپکها یا کدهای درون پرانتز (مانند TASK-x) از عناوین صفحات و منوهای پنل ادمین.
|
- [ ] **TASK-25.10 [Admin Panel UI Cleanup]:** پاکسازی پسوند کد تپکها یا کدهای درون پرانتز (مانند TASK-x) از عناوین صفحات و منوهای پنل ادمین.
|
||||||
- [x] **TASK-25.11 [Maintenance Mode Protection Guard]:** اصلاح و فعالسازی عملکرد گارد «حالت تعمیرات» (Maintenance Mode) در فرانتاند برای مسدودسازی دسترسی عمومی.
|
- [ ] **TASK-25.11 [Maintenance Mode Protection Guard]:** اصلاح و فعالسازی عملکرد گارد «حالت تعمیرات» (Maintenance Mode) در فرانتاند برای مسدودسازی دسترسی عمومی.
|
||||||
- [x] **TASK-25.12 [Flexible Catalog-Only Mode Settings]:** توسعه تنظیمات چندحالته «حالت کاتالوگ» (نمایش/عدم نمایش قیمت، فعال/غیرفعالسازی سبد خرید و فاکتور، تبدیل به پیشخرید).
|
- [ ] **TASK-25.12 [Flexible Catalog-Only Mode Settings]:** توسعه تنظیمات چندحالته «حالت کاتالوگ» (نمایش/عدم نمایش قیمت، فعال/غیرفعالسازی سبد خرید و فاکتور، تبدیل به پیشخرید).
|
||||||
- [x] **TASK-25.13 [Product Pre-order System]:** ایجاد امکان پیشخرید کالا در پنل ادمین (رایگان یا با بیعانه) و ثبت نهایی بعد از موجود شدن.
|
- [ ] **TASK-25.13 [Product Pre-order System]:** ایجاد امکان پیشخرید کالا در پنل ادمین (رایگان یا با بیعانه) و ثبت نهایی بعد از موجود شدن.
|
||||||
- [x] **TASK-25.14 [Stock Alert Queue & SMS Notification]:** ایجاد قابلیت «موجود شد اطلاع بده» برای کالاهای ناموجود، صف خودکار اطلاعرسانی و ارسال پیامک به محض شارژ موجودی.
|
- [ ] **TASK-25.14 [Stock Alert Queue & SMS Notification]:** ایجاد قابلیت «موجود شد اطلاع بده» برای کالاهای ناموجود، صف خودکار اطلاعرسانی و ارسال پیامک به محض شارژ موجودی.
|
||||||
|
|||||||
@ -134,9 +134,7 @@ export default function Products() {
|
|||||||
podcastUrl: '',
|
podcastUrl: '',
|
||||||
videoUrl: '',
|
videoUrl: '',
|
||||||
pdfUrl: '',
|
pdfUrl: '',
|
||||||
symptoms: [] as string[],
|
symptoms: [] as string[]
|
||||||
isPreorder: false,
|
|
||||||
preorderDeposit: '' as number | string
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
@ -213,9 +211,7 @@ export default function Products() {
|
|||||||
podcastUrl: product.podcastUrl || '',
|
podcastUrl: product.podcastUrl || '',
|
||||||
videoUrl: product.videoUrl || '',
|
videoUrl: product.videoUrl || '',
|
||||||
pdfUrl: product.pdfUrl || '',
|
pdfUrl: product.pdfUrl || '',
|
||||||
symptoms: product.symptoms ? product.symptoms.map((s: { symptom: string } | string) => typeof s === 'string' ? s : s.symptom) : [],
|
symptoms: product.symptoms ? product.symptoms.map((s: { symptom: string } | string) => typeof s === 'string' ? s : s.symptom) : []
|
||||||
isPreorder: Boolean((product as any).isPreorder),
|
|
||||||
preorderDeposit: (product as any).preorderDeposit || ''
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setEditingProduct(null);
|
setEditingProduct(null);
|
||||||
@ -225,7 +221,7 @@ export default function Products() {
|
|||||||
priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه',
|
priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه',
|
||||||
imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '',
|
imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '',
|
||||||
images: [], podcastUrl: '', videoUrl: '', pdfUrl: '',
|
images: [], podcastUrl: '', videoUrl: '', pdfUrl: '',
|
||||||
symptoms: [], isPreorder: false, preorderDeposit: ''
|
symptoms: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setActiveTab('general');
|
setActiveTab('general');
|
||||||
@ -891,40 +887,6 @@ export default function Products() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pre-order Configuration (TASK-25.13) */}
|
|
||||||
<div className="p-4 bg-purple-50 rounded-2xl border border-purple-100 space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-bold text-purple-900 text-sm">قابلیت پیشخرید کالا (Pre-Order Mode)</h4>
|
|
||||||
<p className="text-xs text-purple-700 mt-0.5">امکان پیشخرید کالا قبل از موجود شدن رسمی با بیعانه یا ثبت رایگان.</p>
|
|
||||||
</div>
|
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="sr-only peer"
|
|
||||||
checked={formData.isPreorder}
|
|
||||||
onChange={(e) => setFormData({ ...formData, isPreorder: e.target.checked })}
|
|
||||||
/>
|
|
||||||
<div className="w-12 h-6 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-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{formData.isPreorder && (
|
|
||||||
<div className="pt-2 border-t border-purple-200/60">
|
|
||||||
<label className="block text-xs font-bold text-purple-900 mb-1">مبلغ بیعانه پیشخرید (تومان - ۰ برای ثبت رایگان)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
placeholder="مثلاً: 200000 یا 0 برای پیشخرید بدون بیعانه"
|
|
||||||
value={formData.preorderDeposit}
|
|
||||||
onChange={(e) => setFormData({ ...formData, preorderDeposit: e.target.value ? Number(e.target.value) : '' })}
|
|
||||||
className="w-full px-4 py-2.5 rounded-xl border border-purple-200 bg-white font-mono text-xs outline-none focus:ring-2 focus:ring-purple-500"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 col-span-1 md:col-span-2 pt-2">
|
<div className="space-y-2 col-span-1 md:col-span-2 pt-2">
|
||||||
<label className="text-sm font-bold text-gray-700 flex items-center justify-between">
|
<label className="text-sm font-bold text-gray-700 flex items-center justify-between">
|
||||||
<span>منطق و دوز دقیق مصرفی (Dosage Calculator Fields)</span>
|
<span>منطق و دوز دقیق مصرفی (Dosage Calculator Fields)</span>
|
||||||
|
|||||||
@ -1,23 +1,19 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Users as UsersIcon, CheckCircle, Search, Edit3, Check, X, Eye, ShieldAlert, UserCheck, UserX, Mail, Phone, Calendar, Shield } from 'lucide-react';
|
import { Users as UsersIcon, CheckCircle, Search, Edit3, Check, X } from 'lucide-react';
|
||||||
import { toast } from 'react-hot-toast';
|
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import Skeleton from '../components/ui/Skeleton';
|
import Skeleton from '../components/ui/Skeleton';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
|
|
||||||
export interface UserRecord {
|
export interface UserRecord {
|
||||||
|
[x: string]: string;
|
||||||
id: string;
|
id: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
mobile?: string;
|
mobile?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
status?: string;
|
|
||||||
walletBalance?: number;
|
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
addresses?: any[];
|
|
||||||
pets?: any[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Users() {
|
export default function Users() {
|
||||||
@ -30,17 +26,9 @@ export default function Users() {
|
|||||||
const [role, setRole] = useState('');
|
const [role, setRole] = useState('');
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
|
||||||
// Modals & Edit State
|
// Edit State
|
||||||
const [viewUser, setViewUser] = useState<UserRecord | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [editUser, setEditUser] = useState<UserRecord | null>(null);
|
const [editRole, setEditRole] = useState<string>('');
|
||||||
const [editFormData, setEditFormData] = useState({
|
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
email: '',
|
|
||||||
mobile: '',
|
|
||||||
role: 'User_PetOwner',
|
|
||||||
status: 'active'
|
|
||||||
});
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
const fetchUsers = useCallback(async () => {
|
const fetchUsers = useCallback(async () => {
|
||||||
@ -71,91 +59,66 @@ export default function Users() {
|
|||||||
return () => clearTimeout(delayDebounceFn);
|
return () => clearTimeout(delayDebounceFn);
|
||||||
}, [fetchUsers]);
|
}, [fetchUsers]);
|
||||||
|
|
||||||
const handleOpenEdit = (user: UserRecord) => {
|
const handleEditClick = (user: UserRecord) => {
|
||||||
setEditUser(user);
|
setEditingId(user.id);
|
||||||
setEditFormData({
|
setEditRole(user.role || 'User_PetOwner');
|
||||||
firstName: user.firstName || '',
|
|
||||||
lastName: user.lastName || '',
|
|
||||||
email: user.email || '',
|
|
||||||
mobile: user.mobile || '',
|
|
||||||
role: user.role || 'User_PetOwner',
|
|
||||||
status: user.status || 'active'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveUser = async (e: React.FormEvent) => {
|
const handleSaveEdit = async (id: string) => {
|
||||||
e.preventDefault();
|
|
||||||
if (!editUser) return;
|
|
||||||
try {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
await api.put(`/admin/users/${editUser.id}`, editFormData);
|
await api.put(`/admin/users/${id}/role`, { role: editRole });
|
||||||
toast.success('اطلاعات کاربر با موفقیت بروزرسانی شد');
|
setEditingId(null);
|
||||||
setEditUser(null);
|
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update user', err);
|
console.error('Failed to update user role', err);
|
||||||
toast.error('خطا در بروزرسانی اطلاعات کاربر');
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleUserStatus = async (user: UserRecord) => {
|
|
||||||
const newStatus = user.status === 'active' ? 'blocked' : 'active';
|
|
||||||
try {
|
|
||||||
await api.put(`/admin/users/${user.id}`, { status: newStatus });
|
|
||||||
toast.success(`وضعیت کاربر به ${newStatus === 'active' ? 'فعال' : 'مسدود'} تغییر یافت`);
|
|
||||||
fetchUsers();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Status change failed', err);
|
|
||||||
toast.error('خطا در تغییر وضعیت کاربر');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
|
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
|
||||||
<UsersIcon className="w-6 h-6 text-purple-600" />
|
<UsersIcon className="w-6 h-6 text-purple-600" />
|
||||||
مدیریت کامل کاربران
|
مدیریت کاربران
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-500 font-medium mt-1">مشاهده تمامی اطلاعات، تغییر نقش، ویرایش پروفایل و مدیریت دسترسی</p>
|
<p className="text-gray-500 font-medium mt-1">مشاهده و تایید حسابهای کاربری و کلینیکها</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center gap-3 w-full sm:w-auto">
|
<div className="flex flex-col sm:flex-row items-center gap-3 w-full sm:w-auto">
|
||||||
<select
|
<select
|
||||||
className="bg-white border border-gray-200 text-gray-700 px-4 py-2 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto text-xs"
|
className="bg-white border border-gray-200 text-gray-700 px-4 py-2 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto"
|
||||||
value={role}
|
value={role}
|
||||||
onChange={(e) => { setRole(e.target.value); setPage(1); }}
|
onChange={(e) => { setRole(e.target.value); setPage(1); }}
|
||||||
>
|
>
|
||||||
<option value="">همه نقشها</option>
|
<option value="">همه نقشها</option>
|
||||||
|
<option value="B2B">همکار (B2B)</option>
|
||||||
<option value="User_PetOwner">مشتری عادی</option>
|
<option value="User_PetOwner">مشتری عادی</option>
|
||||||
<option value="User_Wholesale">خریدار عمده</option>
|
|
||||||
<option value="User_B2B">همکار (B2B)</option>
|
|
||||||
<option value="ADMIN">ادمین</option>
|
<option value="ADMIN">ادمین</option>
|
||||||
</select>
|
</select>
|
||||||
<div className="relative w-full sm:w-64">
|
<div className="relative w-full sm:w-64">
|
||||||
<Search className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
|
<Search className="w-5 h-5 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="جستجو (نام، ایمیل، موبایل)..."
|
placeholder="جستجو کاربر (نام، ایمیل، موبایل)..."
|
||||||
className="pl-4 pr-9 py-2 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-purple-500 w-full font-medium text-xs"
|
className="pl-4 pr-10 py-2 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-purple-500 w-full font-medium"
|
||||||
dir="rtl"
|
dir="rtl"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-right font-vazir text-xs">
|
<table className="w-full text-right font-vazir">
|
||||||
<thead className="bg-gray-50 text-gray-500 border-b border-gray-200">
|
<thead className="bg-gray-50 text-gray-500 text-sm border-b border-gray-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="py-4 px-6 font-bold">نام کاربر / کلینیک</th>
|
<th className="py-4 px-6 font-bold">نام کاربر / کلینیک</th>
|
||||||
<th className="py-4 px-6 font-bold">شماره موبایل</th>
|
|
||||||
<th className="py-4 px-6 font-bold">ایمیل</th>
|
<th className="py-4 px-6 font-bold">ایمیل</th>
|
||||||
<th className="py-4 px-6 font-bold">نوع حساب</th>
|
<th className="py-4 px-6 font-bold">نوع حساب</th>
|
||||||
<th className="py-4 px-6 font-bold">تاریخ عضویت</th>
|
<th className="py-4 px-6 font-bold">تاریخ عضویت</th>
|
||||||
@ -168,253 +131,83 @@ export default function Users() {
|
|||||||
Array.from({ length: 4 }).map((_, i) => (
|
Array.from({ length: 4 }).map((_, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<td className="py-4 px-6"><Skeleton className="h-6 w-32" /></td>
|
<td className="py-4 px-6"><Skeleton className="h-6 w-32" /></td>
|
||||||
<td className="py-4 px-6"><Skeleton className="h-6 w-28" /></td>
|
|
||||||
<td className="py-4 px-6"><Skeleton className="h-6 w-40" /></td>
|
<td className="py-4 px-6"><Skeleton className="h-6 w-40" /></td>
|
||||||
<td className="py-4 px-6"><Skeleton className="h-6 w-24" /></td>
|
<td className="py-4 px-6"><Skeleton className="h-6 w-24" /></td>
|
||||||
<td className="py-4 px-6"><Skeleton className="h-6 w-20" /></td>
|
<td className="py-4 px-6"><Skeleton className="h-6 w-20" /></td>
|
||||||
<td className="py-4 px-6"><Skeleton className="h-6 w-24" /></td>
|
<td className="py-4 px-6"><Skeleton className="h-6 w-24" /></td>
|
||||||
<td className="py-4 px-6"><Skeleton className="h-8 w-24 mx-auto" /></td>
|
<td className="py-4 px-6"><Skeleton className="h-8 w-20 mx-auto" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
users.map((user) => (
|
users.map((user) => {
|
||||||
|
const isEditing = editingId === user.id;
|
||||||
|
return (
|
||||||
<tr key={user.id} className="hover:bg-gray-50/50 transition-colors">
|
<tr key={user.id} className="hover:bg-gray-50/50 transition-colors">
|
||||||
<td className="py-4 px-6 font-bold text-gray-900">{user.firstName || user.lastName ? `${user.firstName || ''} ${user.lastName || ''}`.trim() : 'بدون نام'}</td>
|
<td className="py-4 px-6 font-bold text-gray-900">{user.firstName} {user.lastName}</td>
|
||||||
<td className="py-4 px-6 font-mono text-gray-700" dir="ltr">{user.mobile || '---'}</td>
|
<td className="py-4 px-6 text-gray-500" dir="ltr">{user.email}</td>
|
||||||
<td className="py-4 px-6 text-gray-500 font-mono" dir="ltr">{user.email || '---'}</td>
|
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<span className={`px-3 py-1 rounded-full text-[11px] font-bold ${
|
{isEditing ? (
|
||||||
user.role === 'User_Wholesale' ? 'bg-amber-100 text-amber-800' :
|
<select
|
||||||
user.role?.includes('B2B') ? 'bg-blue-100 text-blue-700' :
|
className="border border-purple-300 rounded p-1 text-xs font-bold"
|
||||||
user.role === 'ADMIN' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-700'
|
value={editRole}
|
||||||
|
onChange={(e) => setEditRole(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="User_PetOwner">مشتری عادی</option>
|
||||||
|
<option value="User_Wholesale">خریدار عمده (تاییدشده)</option>
|
||||||
|
<option value="User_B2B">همکار (B2B)</option>
|
||||||
|
<option value="ADMIN">ادمین</option>
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-bold ${user.role === 'User_Wholesale' ? 'bg-amber-100 text-amber-800' :
|
||||||
|
user.role?.includes('B2B') ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
|
||||||
}`}>
|
}`}>
|
||||||
{user.role === 'User_Wholesale' ? 'خریدار عمده' :
|
{user.role === 'User_Wholesale' ? 'خریدار عمده' :
|
||||||
user.role?.includes('B2B') ? 'همکار (B2B)' :
|
user.role?.includes('B2B') ? 'همکار (B2B)' :
|
||||||
user.role === 'ADMIN' ? 'مدیر سیستم' : 'مشتری عادی'}
|
(user.role === 'ADMIN' ? 'مدیر سیستم' : 'مشتری عادی')}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6 text-gray-500 text-xs font-medium">
|
<td className="py-4 px-6 text-gray-500 text-sm font-medium">{new Date(user.createdAt).toLocaleDateString('fa-IR')}</td>
|
||||||
{user.createdAt ? new Date(user.createdAt).toLocaleDateString('fa-IR') : '---'}
|
|
||||||
</td>
|
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
{user.status === 'blocked' ? (
|
{user.status === 'active' ? (
|
||||||
<span className="inline-flex items-center gap-1 text-red-600 font-bold bg-red-50 px-2.5 py-1 rounded-full border border-red-100">
|
<span className="flex items-center gap-1.5 text-green-600 font-bold text-sm">
|
||||||
<UserX className="w-3.5 h-3.5" />
|
<CheckCircle className="w-4 h-4" />
|
||||||
مسدود شده
|
تایید شده
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-1 text-green-600 font-bold bg-green-50 px-2.5 py-1 rounded-full border border-green-100">
|
<span className="flex items-center gap-1.5 text-orange-500 font-bold text-sm">
|
||||||
<UserCheck className="w-3.5 h-3.5" />
|
<div className="w-2 h-2 rounded-full bg-orange-500"></div>
|
||||||
فعال
|
در انتظار تایید
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6 text-center">
|
<td className="py-4 px-6 text-center">
|
||||||
<div className="flex justify-center items-center gap-1.5">
|
{isEditing ? (
|
||||||
<button
|
<div className="flex justify-center gap-2">
|
||||||
onClick={() => setViewUser(user)}
|
<button onClick={() => handleSaveEdit(user.id)} className="text-green-600 hover:bg-green-50 p-1.5 rounded-lg transition-colors" disabled={isSaving}>
|
||||||
className="p-1.5 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
|
{isSaving ? <Spinner size="sm" /> : <Check className="w-5 h-5" />}
|
||||||
title="مشاهده جزئیات کامل"
|
|
||||||
>
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => setEditingId(null)} className="text-gray-500 hover:bg-gray-50 p-1.5 rounded-lg transition-colors" disabled={isSaving}>
|
||||||
onClick={() => handleOpenEdit(user)}
|
<X className="w-5 h-5" />
|
||||||
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
|
||||||
title="ویرایش کاربر"
|
|
||||||
>
|
|
||||||
<Edit3 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => toggleUserStatus(user)}
|
|
||||||
className={`p-1.5 rounded-lg transition-colors ${
|
|
||||||
user.status === 'blocked' ? 'text-green-600 hover:bg-green-50' : 'text-red-500 hover:bg-red-50'
|
|
||||||
}`}
|
|
||||||
title={user.status === 'blocked' ? 'رفع مسدودی' : 'مسدود کردن'}
|
|
||||||
>
|
|
||||||
<ShieldAlert className="w-4 h-4" />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center gap-2">
|
||||||
|
<button onClick={() => handleEditClick(user)} className="text-blue-600 hover:bg-blue-50 p-1.5 rounded-lg transition-colors" title="ویرایش نقش">
|
||||||
|
<Edit3 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
|
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* View User Modal */}
|
|
||||||
{viewUser && (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
|
|
||||||
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl p-6 space-y-6 font-vazir animate-in fade-in zoom-in duration-150">
|
|
||||||
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
|
||||||
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
|
||||||
<UsersIcon className="w-5 h-5 text-purple-600" />
|
|
||||||
مشاهده جزئیات پروفایل کاربر
|
|
||||||
</h3>
|
|
||||||
<button onClick={() => setViewUser(null)} className="text-gray-400 hover:text-red-500 transition-colors">
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4 text-xs">
|
|
||||||
<div className="grid grid-cols-2 gap-4 bg-gray-50 p-4 rounded-xl border border-gray-100">
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-400 font-bold block mb-1">نام و نام خانوادگی:</span>
|
|
||||||
<span className="font-bold text-gray-900 text-sm">{viewUser.firstName} {viewUser.lastName}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-400 font-bold block mb-1">نوع نقش:</span>
|
|
||||||
<span className="font-bold text-purple-700">{viewUser.role}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-400 font-bold block mb-1">موبایل:</span>
|
|
||||||
<span className="font-mono text-gray-800" dir="ltr">{viewUser.mobile || '---'}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-400 font-bold block mb-1">ایمیل:</span>
|
|
||||||
<span className="font-mono text-gray-800" dir="ltr">{viewUser.email || '---'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-purple-50/60 p-4 rounded-xl border border-purple-100 flex items-center justify-between">
|
|
||||||
<span className="font-bold text-purple-900">موجودی کیف پول کاربر:</span>
|
|
||||||
<span className="font-mono font-black text-purple-700 text-base">
|
|
||||||
{(viewUser.walletBalance || 0).toLocaleString('fa-IR')} تومان
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-bold text-gray-900 mb-2">آدرسهای ثبتشده ({viewUser.addresses?.length || 0})</h4>
|
|
||||||
{viewUser.addresses && viewUser.addresses.length > 0 ? (
|
|
||||||
<div className="space-y-2 max-h-32 overflow-y-auto">
|
|
||||||
{viewUser.addresses.map((a: any, idx: number) => (
|
|
||||||
<div key={idx} className="p-2.5 bg-gray-50 rounded-lg border border-gray-200/60">
|
|
||||||
<p className="font-bold text-gray-800">{a.title} - {a.receptorName}</p>
|
|
||||||
<p className="text-gray-500 mt-0.5">{a.province}، {a.city}، {a.detail}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-400 text-center py-2 bg-gray-50 rounded-lg">هیچ آدرسی ثبت نشده است.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-100 flex justify-end">
|
|
||||||
<button
|
|
||||||
onClick={() => setViewUser(null)}
|
|
||||||
className="px-5 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-xl transition-colors text-xs"
|
|
||||||
>
|
|
||||||
بستن پنجره
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Edit User Modal */}
|
|
||||||
{editUser && (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
|
|
||||||
<form onSubmit={handleSaveUser} className="bg-white rounded-2xl w-full max-w-lg shadow-2xl p-6 space-y-6 font-vazir animate-in fade-in zoom-in duration-150">
|
|
||||||
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
|
||||||
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
|
||||||
<Edit3 className="w-5 h-5 text-blue-600" />
|
|
||||||
ویرایش اطلاعات کاربر
|
|
||||||
</h3>
|
|
||||||
<button type="button" onClick={() => setEditUser(null)} className="text-gray-400 hover:text-red-500 transition-colors">
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">نام</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={editFormData.firstName}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, firstName: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">نام خانوادگی</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={editFormData.lastName}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, lastName: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">شماره موبایل</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={editFormData.mobile}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, mobile: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">ایمیل</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={editFormData.email}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">نقش دسترسی</label>
|
|
||||||
<select
|
|
||||||
value={editFormData.role}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, role: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold"
|
|
||||||
>
|
|
||||||
<option value="User_PetOwner">مشتری عادی</option>
|
|
||||||
<option value="User_Wholesale">خریدار عمده (تاییدشده)</option>
|
|
||||||
<option value="User_B2B">همکار (B2B)</option>
|
|
||||||
<option value="ADMIN">ادمین ارشد</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-gray-700 font-bold mb-1">وضعیت حساب</label>
|
|
||||||
<select
|
|
||||||
value={editFormData.status}
|
|
||||||
onChange={(e) => setEditFormData({ ...editFormData, status: e.target.value })}
|
|
||||||
className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold"
|
|
||||||
>
|
|
||||||
<option value="active">فعال</option>
|
|
||||||
<option value="blocked">مسدود شده</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-100 flex justify-end gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setEditUser(null)}
|
|
||||||
className="px-5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-xl transition-colors text-xs"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSaving}
|
|
||||||
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-bold rounded-xl transition-colors text-xs flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isSaving ? <Spinner size="sm" /> : <Check className="w-4 h-4" />}
|
|
||||||
ذخیره تغییرات
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -130,20 +130,6 @@ export default function CheckoutPage() {
|
|||||||
shippingAddressStr = `${provCity ? provCity + '، ' : ''}${manualAddress}${manualZipCode ? ` (کد پستی: ${manualZipCode})` : ''}${manualName ? ` (گیرنده: ${manualName}${manualPhone ? ` - ${toPersian(manualPhone)}` : ''})` : ''}`;
|
shippingAddressStr = `${provCity ? provCity + '، ' : ''}${manualAddress}${manualZipCode ? ` (کد پستی: ${manualZipCode})` : ''}${manualName ? ` (گیرنده: ${manualName}${manualPhone ? ` - ${toPersian(manualPhone)}` : ''})` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check wallet balance if payment method is wallet
|
|
||||||
if (paymentMethod === 'wallet' && (profile.walletBalance || 0) < totalAmount) {
|
|
||||||
toast.error(`موجودی کیف پول شما (${(profile.walletBalance || 0).toLocaleString('fa-IR')} تومان) کمتر از مبلغ نهایی سفارش (${totalAmount.toLocaleString('fa-IR')} تومان) است. لطفاً ابتدا کیف پول خود را شارژ کنید.`);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check shipping address requirement
|
|
||||||
if (!selectedAddressId && !manualAddress) {
|
|
||||||
toast.error("لطفاً یک آدرس برای ارسال سفارش انتخاب کرده یا آدرس جدید وارد کنید.");
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderId = await addOrder({
|
const orderId = await addOrder({
|
||||||
items: [...items],
|
items: [...items],
|
||||||
total: totalAmount,
|
total: totalAmount,
|
||||||
@ -158,7 +144,7 @@ export default function CheckoutPage() {
|
|||||||
const { fetchProfile } = useUserStore.getState();
|
const { fetchProfile } = useUserStore.getState();
|
||||||
await fetchProfile();
|
await fetchProfile();
|
||||||
|
|
||||||
// Update pet consumptions and charity contributions for active pet
|
// 4. Update pet consumptions and charity contributions for active pet
|
||||||
const targetPet = activePet || usePetStore.getState().pets[0];
|
const targetPet = activePet || usePetStore.getState().pets[0];
|
||||||
if (targetPet) {
|
if (targetPet) {
|
||||||
const newConsumptions = [...(targetPet.consumptions || [])];
|
const newConsumptions = [...(targetPet.consumptions || [])];
|
||||||
@ -186,10 +172,7 @@ export default function CheckoutPage() {
|
|||||||
toast.success("سفارش شما با موفقیت ثبت شد!");
|
toast.success("سفارش شما با موفقیت ثبت شد!");
|
||||||
router.push(`/checkout/success/${orderId}`);
|
router.push(`/checkout/success/${orderId}`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const backendMessage = err.response?.data?.message || err.response?.data?.details?.[0]?.message || err.message;
|
toast.error(err.message || "خطا در ثبت سفارش. لطفاً مجدداً تلاش کنید.");
|
||||||
if (backendMessage && !backendMessage.includes("Request failed")) {
|
|
||||||
toast.error(`خطا در ثبت سفارش: ${backendMessage}`);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -740,30 +740,9 @@ export default function PetProfile({ initialView, advisorNeed }: {
|
|||||||
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{(activePet?.consumptions || []).length === 0 ? (
|
{(activePet?.consumptions || []).length === 0 ? (
|
||||||
<div className="col-span-full py-12 text-center bg-medical-gray-50 rounded-[2.5rem] border border-dashed border-medical-gray-200 p-6 space-y-4">
|
<div className="col-span-full py-12 text-center bg-medical-gray-50 rounded-[2.5rem] border border-dashed border-medical-gray-200">
|
||||||
<div>
|
<p className="text-medical-gray-400 font-bold italic">هنوز محصولی برای پایش موجودی ثبت نشده است.</p>
|
||||||
<p className="text-medical-gray-700 font-black text-sm">هنوز محصولی برای پایش موجودی این پت ثبت نشده است.</p>
|
<p className="text-[10px] text-medical-gray-400 mt-2">با ثبت اولین سفارش، پایش هوشمند فعال میشود.</p>
|
||||||
<p className="text-xs text-medical-gray-500 mt-1">با ثبت سفارش جدید، الگوریتم پایش هوشمند کالا به صورت خودکار فعال میشود. همچنین میتوانید محصول مورد مصرف فعلی را انتخاب کنید:</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap justify-center gap-2 max-w-xl mx-auto pt-2">
|
|
||||||
{products.slice(0, 5).map(p => (
|
|
||||||
<button
|
|
||||||
key={p.id}
|
|
||||||
onClick={() => {
|
|
||||||
const newConsumptions = [...(activePet.consumptions || []), {
|
|
||||||
productId: p.id,
|
|
||||||
packageSize: p.packageSize || 60,
|
|
||||||
remaining: p.packageSize || 60
|
|
||||||
}];
|
|
||||||
updatePet(activePet.id, { consumptions: newConsumptions });
|
|
||||||
toast.success(`مکمل ${p.name} به لیست پایش اضافه شد`);
|
|
||||||
}}
|
|
||||||
className="px-3.5 py-2 bg-white border border-medical-gray-200 hover:border-canina-blue hover:bg-canina-blue/5 text-medical-gray-800 text-xs font-bold rounded-xl transition-all flex items-center gap-1.5 shadow-xs"
|
|
||||||
>
|
|
||||||
<span>+ افزودن {p.name}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
activePet.consumptions.map((consumption) => {
|
activePet.consumptions.map((consumption) => {
|
||||||
|
|||||||
@ -80,8 +80,6 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
const [showRefillModal, setShowRefillModal] = useState(false);
|
const [showRefillModal, setShowRefillModal] = useState(false);
|
||||||
const [itemQuantity, setItemQuantity] = useState(1);
|
const [itemQuantity, setItemQuantity] = useState(1);
|
||||||
const [allProducts, setAllProducts] = useState<Product[]>([]);
|
const [allProducts, setAllProducts] = useState<Product[]>([]);
|
||||||
const [showNotifier, setShowNotifier] = useState(false);
|
|
||||||
const [notifierPhone, setNotifierPhone] = useState('');
|
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -674,27 +672,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add to Cart / Preorder / Stock Notify Button - 60% Width */}
|
{/* Add to Cart Button - 60% Width */}
|
||||||
{(product as any).isPreorder ? (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
addItem(product, itemQuantity, { quantity: calculation.dailyDose, unit: calculation.unit });
|
|
||||||
toast.success(`پیشخرید ${toPersian(itemQuantity)} عدد ${product.name} به سبد شما اضافه شد`);
|
|
||||||
}}
|
|
||||||
className="w-[60%] flex items-center justify-center gap-1.5 px-4 bg-purple-700 text-white rounded-2xl h-16 font-black text-sm hover:bg-purple-800 transition-all shadow-xl shadow-purple-900/20 font-vazir whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<Sparkles className="w-5 h-5 mb-1 text-amber-300" />
|
|
||||||
{(product as any).preorderDeposit > 0 ? `پیشخرید (بیعانه: ${((product as any).preorderDeposit).toLocaleString('fa-IR')} تومان)` : 'ثبت پیشخرید (رایگان)'}
|
|
||||||
</button>
|
|
||||||
) : (product.packageSize || 0) <= 0 ? (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowNotifier(true)}
|
|
||||||
className="w-[60%] flex items-center justify-center gap-1.5 px-4 bg-amber-500 text-white rounded-2xl h-16 font-black text-sm hover:bg-amber-600 transition-all shadow-xl shadow-amber-500/20 font-vazir whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<Bell className="w-5 h-5 mb-1" />
|
|
||||||
موجود شد اطلاع بده
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addItem(product, itemQuantity, { quantity: calculation.dailyDose, unit: calculation.unit });
|
addItem(product, itemQuantity, { quantity: calculation.dailyDose, unit: calculation.unit });
|
||||||
@ -705,7 +683,6 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
<ShoppingBag className="w-5 h-5 mb-1" />
|
<ShoppingBag className="w-5 h-5 mb-1" />
|
||||||
افزودن به سبد خرید
|
افزودن به سبد خرید
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex flex-col gap-3 border-t border-medical-gray-50 pt-6">
|
<div className="mt-6 flex flex-col gap-3 border-t border-medical-gray-50 pt-6">
|
||||||
|
|||||||
@ -382,38 +382,18 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
|||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<label className="block text-center text-sm font-black text-medical-gray-800 font-vazir">{getText('advisor_age_label', "سن حیوان (سال)")}</label>
|
<label className="block text-center text-sm font-black text-medical-gray-800 font-vazir">{getText('advisor_age_label', "سن حیوان (سال)")}</label>
|
||||||
<div className="flex items-center gap-2 bg-white rounded-3xl p-2 border-2 border-medical-gray-200">
|
<div className="flex items-center gap-4 bg-white rounded-3xl p-2 border-2 border-medical-gray-200">
|
||||||
<button type="button" onClick={() => setAge(Math.max(0, age - 1))} className="w-10 h-10 shrink-0 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white transition-all shadow-sm font-black text-lg">-</button>
|
<button onClick={() => setAge(Math.max(0, age - 1))} className="w-12 h-12 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white focus-visible:ring-2 focus-visible:ring-canina-blue focus-visible:outline-none transition-all shadow-sm font-black text-lg">-</button>
|
||||||
<div className="flex-1 flex items-center justify-center gap-1">
|
<span className="flex-1 text-center text-2xl font-black text-canina-blue font-vazir">{toPersian(age.toString())} <span className="text-sm font-bold text-medical-gray-400">سال</span></span>
|
||||||
<input
|
<button onClick={() => setAge(age + 1)} className="w-12 h-12 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white focus-visible:ring-2 focus-visible:ring-canina-blue focus-visible:outline-none transition-all shadow-sm font-black text-lg">+</button>
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="30"
|
|
||||||
value={age === 0 ? '' : age}
|
|
||||||
onChange={(e) => setAge(Math.max(0, parseInt(e.target.value) || 0))}
|
|
||||||
className="w-12 text-center text-xl font-black text-canina-blue font-vazir outline-none bg-transparent"
|
|
||||||
/>
|
|
||||||
<span className="text-xs font-bold text-medical-gray-400">سال</span>
|
|
||||||
</div>
|
|
||||||
<button type="button" onClick={() => setAge(age + 1)} className="w-10 h-10 shrink-0 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white transition-all shadow-sm font-black text-lg">+</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<label className="block text-center text-sm font-black text-medical-gray-800 font-vazir">{getText('advisor_weight_label', "وزن حیوان (کیلوگرم)")}</label>
|
<label className="block text-center text-sm font-black text-medical-gray-800 font-vazir">{getText('advisor_weight_label', "وزن حیوان (کیلوگرم)")}</label>
|
||||||
<div className="flex items-center gap-2 bg-white rounded-3xl p-2 border-2 border-medical-gray-200">
|
<div className="flex items-center gap-4 bg-white rounded-3xl p-2 border-2 border-medical-gray-200">
|
||||||
<button type="button" onClick={() => setWeight(Math.max(1, weight - 1))} className="w-10 h-10 shrink-0 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white transition-all shadow-sm font-black text-lg">-</button>
|
<button onClick={() => setWeight(Math.max(1, weight - 1))} className="w-12 h-12 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white focus-visible:ring-2 focus-visible:ring-canina-blue focus-visible:outline-none transition-all shadow-sm font-black text-lg">-</button>
|
||||||
<div className="flex-1 flex items-center justify-center gap-1">
|
<span className="flex-1 text-center text-2xl font-black text-canina-blue font-vazir">{toPersian(weight.toString())} <span className="text-sm font-bold text-medical-gray-400">kg</span></span>
|
||||||
<input
|
<button onClick={() => setWeight(weight + 1)} className="w-12 h-12 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white focus-visible:ring-2 focus-visible:ring-canina-blue focus-visible:outline-none transition-all shadow-sm font-black text-lg">+</button>
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="120"
|
|
||||||
value={weight === 0 ? '' : weight}
|
|
||||||
onChange={(e) => setWeight(Math.max(1, parseInt(e.target.value) || 1))}
|
|
||||||
className="w-16 text-center text-xl font-black text-canina-blue font-vazir outline-none bg-transparent"
|
|
||||||
/>
|
|
||||||
<span className="text-xs font-bold text-medical-gray-400">kg</span>
|
|
||||||
</div>
|
|
||||||
<button type="button" onClick={() => setWeight(weight + 1)} className="w-10 h-10 shrink-0 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white transition-all shadow-sm font-black text-lg">+</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user