canina/frontend/admin-panel/src/pages/Products.tsx

1013 lines
60 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react';
import { Search, Filter, Box, Plus, Edit2, Trash2, Image as ImageIcon, X, HelpCircle } from 'lucide-react';
import { toast } from 'react-hot-toast';
import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination';
import MediaSelector from '../components/ui/MediaSelector';
import ConfirmModal from '../components/ui/ConfirmModal';
export interface Category {
id: string;
nameFa: string;
nameEn?: string;
name?: string;
}
export interface Product {
name?: string;
category?: Category | string;
id: string;
artNo: string;
nameFa: string;
nameEn?: string;
scientificTagline?: string;
description?: string;
shortDescription?: string;
categoryId?: string;
priceValue?: number;
wholesalePrice?: number;
priceDisplay?: string;
unit?: string;
packageSize?: number;
dosageLogic?: string;
suitableFor?: string;
imageUrl?: string;
metaTitle?: string;
metaDescription?: string;
keywords?: string;
canonicalUrl?: string;
slug?: string;
images?: string[];
podcastUrl?: string;
videoUrl?: string;
pdfUrl?: string;
symptoms?: Array<{ symptom: string } | string>;
}
export default function Products() {
const [products, setProducts] = useState<Product[]>([]);
const [categories, setCategories] = useState<Category[]>([]);
const [isLoading, setIsLoading] = useState(true);
// URL and String Helper Functions
const getProductImageUrl = (url: string) => {
if (!url) return '';
if (url.startsWith('http')) {
const path = url.replace(/^https?:\/\/[^/]+/, '');
return `${BASE_DOMAIN}${path}`;
}
return url.startsWith('/') ? `${BASE_DOMAIN}${url}` : `${BASE_DOMAIN}/${url}`;
};
const getStoreProductUrl = (slug: string) => {
const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
if (isDev) {
return `http://localhost:4000/shop/${slug}`;
}
// Production fallback based on actual URL
const storeOrigin = window.location.origin.replace('admin', 'www');
return `${storeOrigin}/shop/${slug}`;
};
const slugify = (text: string) => {
return text
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
};
// Filters
const [search, setSearch] = useState('');
const [categoryFilter, setCategoryFilter] = useState('');
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
const [mediaImageError, setMediaImageError] = useState(false);
const [availableSymptoms, setAvailableSymptoms] = useState<string[]>([
"درد مفاصل", "سختی در بلند شدن", "لنگیدن", "رشد سریع توله‌سگ", "پاهای پرانتزی", "ضعف تاندون",
"اسهال", "یبوست", "اسهال مزمن", "بی‌اشتهایی", "ضعف بعد از بیماری", "ریزش مو", "خشکی پوست", "خارش", "جرم دندان"
]);
const [symptomInput, setSymptomInput] = useState('');
const [showSymptomSuggestions, setShowSymptomSuggestions] = useState(false);
const limit = 10;
// Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
const [mediaTargetField, setMediaTargetField] = useState<'imageUrl' | 'podcastUrl' | 'videoUrl' | 'pdfUrl'>('imageUrl');
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState('general');
const [formData, setFormData] = useState({
artNo: '',
nameFa: '',
nameEn: '',
scientificTagline: '',
description: '',
shortDescription: '',
categoryId: '',
priceValue: 0,
wholesalePrice: '' as number | string,
priceDisplay: '',
unit: '',
packageSize: 0,
dosageLogic: '',
suitableFor: 'سگ و گربه',
imageUrl: '',
metaTitle: '',
metaDescription: '',
keywords: '',
canonicalUrl: '',
slug: '',
images: [] as string[],
podcastUrl: '',
videoUrl: '',
pdfUrl: '',
symptoms: [] as string[]
});
const fetchData = useCallback(async () => {
try {
// Fetch products first
const prodRes = await api.get('/admin/products', { params: { page, limit, search, categoryId: categoryFilter } });
if (prodRes.data?.data) {
setProducts(prodRes.data.data);
setTotalPages(prodRes.data.meta?.lastPage || 1);
}
// Fetch categories separately so it doesn't break products
try {
const catRes = await api.get('/admin/categories');
if (catRes.data?.data) {
setCategories(catRes.data.data);
} else if (Array.isArray(catRes.data)) {
setCategories(catRes.data);
}
} catch (err) {
console.warn('Could not fetch categories', err);
}
} catch (err: unknown) {
console.error(err);
toast.error('خطا در دریافت لیست محصولات');
} finally {
setIsLoading(false);
}
}, [page, search, categoryFilter]);
useEffect(() => {
const timer = setTimeout(() => {
fetchData();
}, 500);
return () => clearTimeout(timer);
}, [fetchData]);
const openModal = (product: Product | null = null) => {
setMediaImageError(false);
if (product) {
setEditingProduct(product);
setFormData({
artNo: product.artNo,
nameFa: product.nameFa || '',
nameEn: product.nameEn || '',
scientificTagline: product.scientificTagline || '',
description: product.description || '',
shortDescription: product.shortDescription || '',
categoryId: product.categoryId || '',
priceValue: Number(product.priceValue),
wholesalePrice: product.wholesalePrice ? Number(product.wholesalePrice) : '',
priceDisplay: product.priceDisplay || '',
unit: product.unit || '',
packageSize: Number(product.packageSize),
dosageLogic: product.dosageLogic || '',
suitableFor: product.suitableFor || 'سگ و گربه',
imageUrl: product.imageUrl || '',
metaTitle: product.metaTitle || '',
metaDescription: product.metaDescription || '',
keywords: product.keywords || '',
canonicalUrl: product.canonicalUrl || '',
slug: product.slug || '',
images: Array.isArray(product.images) ? product.images : [],
podcastUrl: product.podcastUrl || '',
videoUrl: product.videoUrl || '',
pdfUrl: product.pdfUrl || '',
symptoms: product.symptoms ? product.symptoms.map((s: { symptom: string } | string) => typeof s === 'string' ? s : s.symptom) : []
});
} else {
setEditingProduct(null);
setFormData({
artNo: '', nameFa: '', nameEn: '', scientificTagline: '', description: '', shortDescription: '', categoryId: '',
priceValue: 0, wholesalePrice: '', priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه',
imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '',
images: [], podcastUrl: '', videoUrl: '', pdfUrl: '',
symptoms: []
});
}
setActiveTab('general');
setIsModalOpen(true);
};
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
try {
const derivedSlug = formData.slug.trim() || slugify(formData.nameEn);
const finalPayload = {
...formData,
slug: derivedSlug,
metaTitle: formData.metaTitle.trim() || `${formData.nameFa || 'نام محصول'} | خرید و قیمت مکمل کانینا`,
metaDescription: formData.metaDescription.trim() || formData.shortDescription || 'خرید آنلاین مکمل اصل کانینا آلمان با بالاترین کیفیت بالینی...',
canonicalUrl: formData.canonicalUrl.trim() || `/shop/${derivedSlug}`
};
if (editingProduct) {
await api.put(`/admin/products/${editingProduct.id}`, finalPayload);
toast.success('محصول با موفقیت ویرایش شد');
} else {
await api.post('/admin/products', finalPayload);
toast.success('محصول با موفقیت ایجاد شد');
}
setIsModalOpen(false);
setImageErrors({});
fetchData();
} catch (error) {
console.error('Save failed', error);
toast.error('خطا در ذخیره محصول');
}
};
const confirmDelete = async () => {
if (!deleteTargetId) return;
try {
await api.delete(`/admin/products/${deleteTargetId}`);
toast.success('محصول با موفقیت حذف شد');
setImageErrors({});
fetchData();
} catch (error) {
console.error('Delete failed', error);
toast.error('خطا در حذف محصول');
} finally {
setDeleteTargetId(null);
}
};
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row justify-between gap-4">
<div>
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
<Box className="w-6 h-6 text-purple-600" />
مدیریت محصولات
</h2>
<p className="text-gray-500 font-medium mt-1">افزودن و ویرایش پیشرفته محصولات با تنظیمات سئو</p>
</div>
<button
onClick={() => openModal()}
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
>
<Plus className="w-5 h-5" />
محصول جدید
</button>
</div>
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="جستجو در نام و کد محصول..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all"
/>
</div>
<div className="relative w-full sm:w-64">
<Filter className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
<select
value={categoryFilter}
onChange={(e) => { setCategoryFilter(e.target.value); setPage(1); }}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all appearance-none"
>
<option value="">همه دسته‌بندی‌ها</option>
{categories.map(c => <option key={c.id} value={c.id}>{typeof c === 'string' ? c : (c.nameFa || c.name)}</option>)}
</select>
</div>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-right">
<thead className="bg-gray-50 border-b border-gray-100">
<tr>
<th className="py-4 px-6 text-sm font-bold text-gray-500">تصویر</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">کد (Art No)</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">نام محصول</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">دسته‌بندی</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">موجودی / قیمت</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500 w-24">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{isLoading ? (
<tr>
<td colSpan={6} className="py-12 text-center">
<Spinner size="lg" className="mx-auto text-purple-600" />
</td>
</tr>
) : products.length === 0 ? (
<tr>
<td colSpan={6} className="py-12 text-center text-gray-500 font-medium">محصولی یافت نشد</td>
</tr>
) : (
products.map((product) => (
<tr key={product.id} className="hover:bg-gray-50/50 transition-colors">
<td className="py-3 px-6">
{product.imageUrl && !imageErrors[product.id] ? (
<div className="w-12 h-12 rounded-xl bg-gray-50 border border-gray-200 p-1 flex items-center justify-center overflow-hidden">
<img
src={getProductImageUrl(product.imageUrl)}
alt=""
onError={() => setImageErrors(prev => ({ ...prev, [product.id]: true }))}
className="max-w-full max-h-full object-contain object-center"
/>
</div>
) : (
<div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
<ImageIcon className="w-5 h-5" />
</div>
)}
</td>
<td className="py-4 px-6 text-gray-500 font-mono text-sm">{product.artNo}</td>
<td className="py-4 px-6">
<div className="flex flex-col gap-0.5">
<span className="font-bold text-gray-900" dir="rtl">{product.nameFa || product.name || '-'}</span>
{product.nameEn && <span className="text-xs text-gray-400 font-mono" dir="ltr">{product.nameEn}</span>}
</div>
</td>
<td className="py-4 px-6 text-gray-600 text-sm">{product.category?.name || 'بدون دسته'}</td>
<td className="py-4 px-6">
<div className="flex flex-col">
<span className="font-bold text-gray-900">{Number(product.priceValue).toLocaleString()} تومان</span>
<span className="text-xs text-gray-500">{product.packageSize} موجودی</span>
</div>
</td>
<td className="py-4 px-6">
<div className="flex items-center gap-2">
<button onClick={() => openModal(product)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
<Edit2 className="w-4 h-4" />
</button>
<button onClick={() => setDeleteTargetId(product.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{!isLoading && totalPages > 1 && (
<div className="p-4 border-t border-gray-100 flex justify-center bg-gray-50/50">
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
</div>
)}
</div>
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-full max-w-4xl max-h-[90vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<div className="flex items-center gap-3">
<h3 className="text-xl font-bold text-gray-900">
{editingProduct ? 'ویرایش پیشرفته محصول' : 'افزودن محصول جدید'}
</h3>
{editingProduct && formData.slug && (
<a
href={getStoreProductUrl(formData.slug)}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-purple-50 border border-purple-100 text-purple-600 hover:bg-purple-100 transition-all cursor-pointer"
>
مشاهده آنلاین در سایت 🔗
</a>
)}
</div>
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<X className="w-6 h-6" />
</button>
</div>
<div className="flex border-b border-gray-100 px-6 pt-2 gap-4">
{[
{ id: 'general', label: 'اطلاعات پایه' },
{ id: 'pricing', label: 'موجودی و قیمت' },
{ id: 'seo', label: 'سئو (SEO)' },
{ id: 'media', label: 'تصاویر' }
].map(tab => (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={`pb-3 px-2 text-sm font-bold border-b-2 transition-colors ${activeTab === tab.id ? 'border-purple-600 text-purple-700' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
>
{tab.label}
</button>
))}
</div>
<div className="p-6 overflow-y-auto h-[550px] min-h-[550px] max-h-[550px] bg-gray-50/30">
<form id="productForm" onSubmit={handleSave} className="space-y-6">
{/* General Tab */}
<div className={activeTab === 'general' ? '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 flex items-center gap-1.5">
<span>کد محصول (Art No) *</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
کد کاتالوگ انحصاری کارخانه Canina آلمان (مانند 123490).
</div>
</div>
</label>
<input required type="text" value={formData.artNo} onChange={(e) => setFormData({ ...formData, artNo: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono" dir="ltr" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>نام فارسی محصول *</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
عنوان فارسی محصول که در صفحات و کاتالوگ فروشگاه نمایش داده می‌شود.
</div>
</div>
</label>
<input required type="text" value={formData.nameFa} onChange={(e) => setFormData({ ...formData, nameFa: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="rtl" placeholder="مثال: ژل دندان میکروسیلور" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>نام انگلیسی / آلمانی (nameEn) *</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
نام برند و وزن کالا به انگلیسی جهت استفاده در آدرس، فاکتورها و سئو.
</div>
</div>
</label>
<input required type="text" value={formData.nameEn} onChange={(e) => setFormData({ ...formData, nameEn: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono" dir="ltr" placeholder="e.g. Canina Mikrosilber Zahngel - 50ml" />
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>شعار علمی (Tagline)</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
شعار کلیدی یا علمی کالا که در بالای صفحه محصول نمایش داده می‌شود.
</div>
</div>
</label>
<input type="text" value={formData.scientificTagline} onChange={(e) => setFormData({ ...formData, scientificTagline: 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 flex items-center gap-1.5">
<span>دسته‌بندی *</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
دسته‌بندی اصلی محصول جهت تقسیم‌بندی در منوها و سیستم ناوبری سایت.
</div>
</div>
</label>
<select required value={formData.categoryId} onChange={(e) => setFormData({ ...formData, categoryId: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none bg-white">
<option value="">انتخاب کنید</option>
{categories.map(c => <option key={c.id} value={c.id}>{typeof c === 'string' ? c : (c.nameFa || c.name)}</option>)}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>مناسب برای</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
تعیین این که کالا برای سگ، گربه یا هر دو گونه مناسب است.
</div>
</div>
</label>
<select value={formData.suitableFor} onChange={(e) => setFormData({ ...formData, suitableFor: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none bg-white">
<option value="سگ">سگ</option>
<option value="گربه">گربه</option>
<option value="سگ و گربه">سگ و گربه</option>
</select>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>توضیح کوتاه</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
خلاصه ویژگی‌های کلیدی کالا که در کارت‌های فروشگاه و زیر عنوان محصول نمایش داده می‌شود.
</div>
</div>
</label>
<textarea rows={2} value={formData.shortDescription} onChange={(e) => setFormData({ ...formData, shortDescription: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>توضیحات کامل</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
توضیحات کامل علمی، نحوه عملکرد مکمل و جزئیات ترکیبات محصول.
</div>
</div>
</label>
<textarea rows={5} value={formData.description} onChange={(e) => setFormData({ ...formData, description: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>منطق دوز مصرفی (Dosage Logic)</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
فرمول نحوه مصرف روزانه کالا بر اساس وزن پت (مثال: ۲ میلی‌لیتر به ازای هر ۵ کیلوگرم وزن).
</div>
</div>
</label>
<textarea rows={2} value={formData.dosageLogic} onChange={(e) => setFormData({ ...formData, dosageLogic: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>علائم درمانی مرتبط (Symptom Tags)</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
علائم متصل به این دارو جهت فیلترینگ و سرچ پویای کاربران (مثال: اسهال، خارش، ریزش مو).
</div>
</div>
</label>
<div className="flex flex-wrap gap-2 p-3 bg-white border border-gray-200 rounded-xl min-h-[50px] items-center focus-within:border-purple-500 focus-within:ring-2 focus-within:ring-purple-200 transition-all">
{formData.symptoms && formData.symptoms.map((symptom, idx) => (
<span key={idx} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-purple-50 border border-purple-200 text-purple-700">
{symptom}
<button
type="button"
onClick={() => {
const newSymptoms = formData.symptoms.filter((_, i) => i !== idx);
setFormData({ ...formData, symptoms: newSymptoms });
}}
className="text-purple-400 hover:text-purple-600 focus:outline-none transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</span>
))}
<div className="relative flex-1 min-w-[200px]">
<input
type="text"
placeholder="جستجو یا تایپ علامت جدید..."
value={symptomInput}
onChange={(e) => {
setSymptomInput(e.target.value);
setShowSymptomSuggestions(true);
}}
onFocus={() => setShowSymptomSuggestions(true)}
className="w-full bg-transparent border-none outline-none text-sm text-right font-vazir"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
const val = symptomInput.trim();
if (val && !formData.symptoms.includes(val)) {
setFormData({
...formData,
symptoms: [...(formData.symptoms || []), val]
});
if (!availableSymptoms.includes(val)) {
setAvailableSymptoms([...availableSymptoms, val]);
}
setSymptomInput('');
setShowSymptomSuggestions(false);
}
}
}}
/>
{/* Autocomplete Suggestions Popup */}
{showSymptomSuggestions && availableSymptoms.filter(s => s.includes(symptomInput) && !formData.symptoms.includes(s)).length > 0 && (
<div className="absolute top-full right-0 z-50 mt-1 w-64 max-h-48 overflow-y-auto bg-white border border-gray-200 rounded-xl shadow-xl p-1">
{availableSymptoms
.filter(s => s.includes(symptomInput) && !formData.symptoms.includes(s))
.map((s) => (
<button
type="button"
key={s}
onMouseDown={(e) => {
e.preventDefault(); // Prevent blur before click executes
setFormData({
...formData,
symptoms: [...(formData.symptoms || []), s]
});
setSymptomInput('');
setShowSymptomSuggestions(false);
}}
className="w-full text-right px-3 py-1.5 hover:bg-purple-50 hover:text-purple-700 text-xs font-bold rounded-lg transition-colors flex items-center justify-between"
>
<span>{s}</span>
<span className="text-[10px] text-gray-400">+ افزودن</span>
</button>
))}
</div>
)}
</div>
</div>
<p className="text-[11px] text-gray-400 font-medium">از منوی پیشنهادی انتخاب کنید یا علامت جدید بنویسید و کلید Enter را بزنید.</p>
</div>
</div>
</div>
{/* Pricing Tab */}
<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) => {
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">
<span>قیمت اختصاصی B2B (تومان)</span>
<span className="text-[10px] font-normal text-gray-400">(اختیاری)</span>
</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="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" />
</div>
<div className="space-y-2 col-span-1 md:col-span-2">
<label className="text-sm font-bold text-gray-700 flex items-center justify-between">
<span>منطق و دوز دقیق مصرفی (Dosage Calculator Fields)</span>
<span className="text-xs text-purple-600 font-bold">فرمت JSON یا راهنمای متنی</span>
</label>
<textarea
rows={3}
value={formData.dosageLogic}
onChange={(e) => setFormData({ ...formData, dosageLogic: e.target.value })}
placeholder='{"baseDosage": 1, "perKg": 10, "unit": "قرص", "maxPerDay": 3}'
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-xs"
/>
<p className="text-[11px] text-gray-400">
این مقادیر توسط محاسبه‌گر هوشمند دوز مکمل در فرانت‌اند بخوانده می‌شود.
</p>
</div>
</div>
</div>
{/* SEO Tab */}
<div className={activeTab === 'seo' ? 'block' : 'hidden'}>
{(() => {
const derivedSlug = formData.slug || slugify(formData.nameEn);
const derivedMetaTitle = formData.metaTitle || `${formData.nameFa || 'نام محصول'} | خرید و قیمت مکمل کانینا`;
const derivedMetaDescription = formData.metaDescription || (formData.shortDescription || 'خرید آنلاین مکمل اصل کانینا آلمان با بالاترین کیفیت بالینی و ضمانت اصالت فیزیکی...');
const derivedCanonicalUrl = formData.canonicalUrl || `/shop/${derivedSlug}`;
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
{/* Right Column: SEO Inputs */}
<div className="lg:col-span-7 space-y-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>نامک (Slug) محصول</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
آدرس مستقیم محصول در سایت. در صورت خالی بودن، به طور خودکار از روی نام انگلیسی ساخته می‌شود.
</div>
</div>
</label>
<input
type="text"
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
dir="ltr"
placeholder={derivedSlug}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>عنوان متا (Meta Title)</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
عنوان اصلی صفحه در گوگل (حداکثر ۶۰ کاراکتر). در صورت خالی بودن، از ساختار پیش‌فرض استفاده می‌شود.
</div>
</div>
</label>
<input
type="text"
value={formData.metaTitle}
onChange={(e) => setFormData({ ...formData, metaTitle: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
placeholder={derivedMetaTitle}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>توضیحات متا (Meta Description)</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
خلاصه توضیحات صفحه در نتایج جستجو (حداکثر ۱۵۵ کاراکتر). در صورت خالی بودن، از توضیحات کوتاه محصول استفاده می‌شود.
</div>
</div>
</label>
<textarea
rows={3}
value={formData.metaDescription}
onChange={(e) => setFormData({ ...formData, metaDescription: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
placeholder={derivedMetaDescription}
></textarea>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>کلمات کلیدی سئو (Keywords)</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
کلمات کلیدی مرتبط با محصول که با کاما (,) از هم جدا شده‌اند.
</div>
</div>
</label>
<input
type="text"
value={formData.keywords}
onChange={(e) => setFormData({ ...formData, keywords: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
placeholder="مثال: مکمل سگ, تقویت مفاصل, کنهیدروکس"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>آدرس کانونی (Canonical URL)</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
آدرس اصلی محصول جهت جلوگیری از جریمه محتوای تکراری در گوگل.
</div>
</div>
</label>
<input
type="text"
value={formData.canonicalUrl}
onChange={(e) => setFormData({ ...formData, canonicalUrl: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm"
dir="ltr"
placeholder={derivedCanonicalUrl}
/>
</div>
</div>
{/* Left Column: Sticky Google SERP Preview */}
<div className="lg:col-span-5 lg:sticky lg:top-4 bg-gray-100 border border-gray-200 rounded-2xl p-5 space-y-3">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest block">پیش‌نمایش نتایج در گوگل (Google SERP Preview)</span>
<div className="bg-white rounded-xl p-4 border border-gray-100 shadow-sm text-right font-sans" dir="rtl">
{/* Domain / Breadcrumb */}
<div className="flex items-center gap-1 text-xs text-[#202124] mb-1">
<span className="font-medium text-gray-800">caninoiran.ir</span>
<span className="text-gray-400">›</span>
<span className="text-gray-500">shop</span>
<span className="text-gray-400">›</span>
<span className="text-gray-500 max-w-[150px] truncate">{derivedSlug}</span>
</div>
{/* Title */}
<h4 className="text-[19px] text-[#1a0dab] hover:underline cursor-pointer leading-tight font-medium font-sans mb-1 select-none">
{derivedMetaTitle}
</h4>
{/* Description */}
<p className="text-sm text-[#4d5156] leading-relaxed font-sans select-none break-words">
{derivedMetaDescription}
</p>
</div>
<p className="text-[10px] text-gray-400 font-medium">تغییرات فیلدهای سمت راست، بلافاصله در پیش‌نمایش بالا منعکس می‌شود.</p>
</div>
</div>
);
})()}
</div>
{/* Media Tab */}
<div className={activeTab === 'media' ? 'block space-y-6' : 'hidden'}>
{/* Direct Image URL Input Box */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-3">
<label className="text-sm font-bold text-gray-700 flex items-center justify-between">
<span className="flex items-center gap-2">
<ImageIcon className="w-4 h-4 text-purple-600" />
لینک مستقیم آدرس تصویر (External / Direct URL)
</span>
<span className="text-[11px] text-gray-400 font-normal">امکان چسباندن (Ctrl+V) مستقیم آدرس اینترنتی تصویر</span>
</label>
<div className="flex gap-2">
<input
type="text"
value={formData.imageUrl}
onChange={(e) => {
setFormData({ ...formData, imageUrl: e.target.value });
setMediaImageError(false);
}}
placeholder="https://example.com/images/product.png یا /uploads/photo.jpg"
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm"
dir="ltr"
/>
<button
type="button"
onClick={() => setIsMediaSelectorOpen(true)}
className="bg-purple-100 text-purple-700 hover:bg-purple-200 px-4 py-2.5 rounded-xl text-xs font-bold transition-colors whitespace-nowrap"
>
گالری سرور
</button>
</div>
</div>
{/* Preview Container */}
<div className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-gray-200 rounded-2xl bg-white">
{formData.imageUrl && !mediaImageError ? (
<div className="w-full flex flex-col items-center">
<div className="relative group">
<img
src={getProductImageUrl(formData.imageUrl)}
alt="پیش‌نمایش تصویر محصول"
onError={() => setMediaImageError(true)}
className="max-w-xs h-48 object-contain rounded-xl shadow-md border border-gray-100"
/>
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl flex items-center justify-center gap-3">
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="bg-white text-gray-800 px-4 py-2 rounded-lg font-bold text-sm">انتخاب از گالری</button>
<button type="button" onClick={() => setFormData({ ...formData, imageUrl: '' })} className="bg-red-500 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-red-600">حذف عکس</button>
</div>
</div>
<div className="mt-4 text-center" dir="ltr">
<p className="text-xs text-gray-500 font-mono bg-gray-100 px-4 py-2 rounded-lg inline-block max-w-md truncate">
{formData.imageUrl}
</p>
</div>
<p className="text-[11px] text-gray-400 mt-2">تصویر با موفقیت بارگذاری و تایید شد.</p>
</div>
) : (
<>
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center text-gray-400 mb-4">
<ImageIcon className="w-8 h-8 text-purple-400" />
</div>
<p className="text-gray-600 font-bold mb-2">آدرس یا فایلی برای تصویر انتخاب نشده است</p>
<p className="text-xs text-gray-400 mb-4 text-center leading-relaxed">
می‌توانید لینک اینترنتی تصویر را در کادر بالا وارد کنید، عکس جدید آپلود نمایید<br />
یا از کلیدهای <kbd className="px-1.5 py-0.5 bg-gray-100 rounded text-gray-700 font-mono">Ctrl+V</kbd> برای چسباندن مستقیم فایل یا لینک استفاده کنید.
</p>
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="bg-purple-600 text-white hover:bg-purple-700 px-6 py-2.5 rounded-xl font-bold transition-colors shadow-sm text-sm">
انتخاب یا آپلود در گالری
</button>
</>
)}
</div>
{/* Multimedia Attachments (TASK-2.3 & TASK-2.4) */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
<h4 className="font-bold text-gray-800 text-sm border-b pb-2">پیوست‌های مالتی‌مدیا و مستندات علمی</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 block">لینک فصلی پادکست (Podcast URL)</label>
<div className="flex gap-1.5">
<input
type="text"
value={formData.podcastUrl}
onChange={(e) => setFormData({ ...formData, podcastUrl: e.target.value })}
placeholder="https://... MP3 or Soundcloud"
className="w-full px-3 py-2 border rounded-xl text-xs font-mono"
dir="ltr"
/>
<button
type="button"
onClick={() => {
setMediaTargetField('podcastUrl');
setIsMediaSelectorOpen(true);
}}
className="px-2 py-1 bg-purple-100 text-purple-700 hover:bg-purple-200 rounded-lg text-xs font-bold whitespace-nowrap"
>
انتخاب
</button>
</div>
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 block">لینک یا کدفریم ویدیو (Video URL)</label>
<div className="flex gap-1.5">
<input
type="text"
value={formData.videoUrl}
onChange={(e) => setFormData({ ...formData, videoUrl: e.target.value })}
placeholder="https://... MP4 or Aparat/YouTube iframe"
className="w-full px-3 py-2 border rounded-xl text-xs font-mono"
dir="ltr"
/>
<button
type="button"
onClick={() => {
setMediaTargetField('videoUrl');
setIsMediaSelectorOpen(true);
}}
className="px-2 py-1 bg-purple-100 text-purple-700 hover:bg-purple-200 rounded-lg text-xs font-bold whitespace-nowrap"
>
انتخاب
</button>
</div>
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 block">فایل کاتالوگ / PDF علمی (PDF URL)</label>
<div className="flex gap-1.5">
<input
type="text"
value={formData.pdfUrl}
onChange={(e) => setFormData({ ...formData, pdfUrl: e.target.value })}
placeholder="https://... PDF catalog"
className="w-full px-3 py-2 border rounded-xl text-xs font-mono"
dir="ltr"
/>
<button
type="button"
onClick={() => {
setMediaTargetField('pdfUrl');
setIsMediaSelectorOpen(true);
}}
className="px-2 py-1 bg-purple-100 text-purple-700 hover:bg-purple-200 rounded-lg text-xs font-bold whitespace-nowrap"
>
انتخاب
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">
انصراف
</button>
<button type="submit" form="productForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">
ذخیره محصول
</button>
</div>
</div>
</div>
)}
{/* Media Selector Modal */}
<MediaSelector
isOpen={isMediaSelectorOpen}
onClose={() => setIsMediaSelectorOpen(false)}
onSelect={(url) => {
setFormData({ ...formData, [mediaTargetField]: url });
if (mediaTargetField === 'imageUrl') setMediaImageError(false);
}}
selectedUrl={formData[mediaTargetField]?.startsWith('http') ? formData[mediaTargetField] : formData[mediaTargetField] ? `${BASE_DOMAIN}${formData[mediaTargetField]}` : undefined}
/>
<ConfirmModal
isOpen={!!deleteTargetId}
title="حذف محصول"
message="آیا از حذف این محصول مطمئن هستید؟ این عملیات قابل بازگشت نیست."
onConfirm={confirmDelete}
onCancel={() => setDeleteTargetId(null)}
/>
</div>
);
}