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

1716 lines
98 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 { useSearchParams } from 'react-router-dom';
import { Search, Filter, Box, Plus, Edit2, Trash2, Image as ImageIcon, X, HelpCircle, ArrowUpDown, ArrowUp, ArrowDown } 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';
import PriceInput from '../components/ui/PriceInput';
import Button from '../components/ui/Button';
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;
buyPrice?: number;
priceDisplay?: string;
unit?: string;
packageSize?: number;
dosageLogic?: string;
suitableFor?: string;
imageUrl?: string;
images?: string[];
metaTitle?: string;
metaDescription?: string;
keywords?: string;
canonicalUrl?: string;
slug?: string;
podcastUrl?: string;
podcastTitle?: string;
podcastDescription?: string;
podcastCover?: string;
videoUrl?: string;
videoTitle?: string;
videoDescription?: string;
videoCover?: string;
pdfUrl?: string;
pdfTitle?: string;
pdfDescription?: string;
pdfCover?: string;
symptoms?: Array<{ symptom: string } | string>;
isPreorder?: boolean;
preorderDeposit?: string | number;
}
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, '');
};
// URL Query Params Management
const [searchParams, setSearchParams] = useSearchParams();
// Filters initialized from URL
const [search, setSearch] = useState(() => searchParams.get('search') || '');
const [categoryFilter, setCategoryFilter] = useState(() => searchParams.get('category') || '');
const [suitableForFilter, setSuitableForFilter] = useState(() => searchParams.get('suitableFor') || '');
const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1);
const [sortBy, setSortBy] = useState(() => searchParams.get('sortBy') || 'createdAt');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(() => (searchParams.get('sortOrder') as 'asc' | 'desc') || 'desc');
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;
// Sync state changes to URL search params
const updateUrlParams = useCallback((paramsObj: Record<string, string | number | undefined | null>) => {
const current = Object.fromEntries(searchParams.entries());
const merged = { ...current, ...paramsObj };
// Remove empty/default values to keep URL clean
const cleaned: Record<string, string> = {};
Object.entries(merged).forEach(([key, val]) => {
if (val !== undefined && val !== null && String(val).trim() !== '' && !(key === 'page' && String(val) === '1') && !(key === 'sortBy' && String(val) === 'createdAt') && !(key === 'sortOrder' && String(val) === 'desc')) {
cleaned[key] = String(val);
}
});
setSearchParams(cleaned, { replace: true });
}, [searchParams, setSearchParams]);
const handleSort = (field: string) => {
let newOrder: 'asc' | 'desc' = 'asc';
if (sortBy === field) {
newOrder = sortOrder === 'asc' ? 'desc' : 'asc';
}
setSortBy(field);
setSortOrder(newOrder);
setPage(1);
updateUrlParams({ sortBy: field, sortOrder: newOrder, page: 1 });
};
// Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
const [mediaTargetField, setMediaTargetField] = useState<'imageUrl' | 'podcastUrl' | 'podcastCover' | 'videoUrl' | 'videoCover' | 'pdfUrl' | 'pdfCover' | 'gallery'>('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: '',
buyPrice: '' as number | string,
priceValue: 0,
wholesalePrice: '' as number | string,
priceValueMarginPercent: '' as number | string,
wholesaleMarginPercent: '' as number | string,
priceDisplay: '',
unit: '',
packageSize: 0,
dosageLogic: '',
suitableFor: 'سگ و گربه',
imageUrl: '',
metaTitle: '',
metaDescription: '',
keywords: '',
canonicalUrl: '',
slug: '',
images: [] as string[],
podcastUrl: '',
podcastTitle: '',
podcastDescription: '',
podcastCover: '',
videoUrl: '',
videoTitle: '',
videoDescription: '',
videoCover: '',
pdfUrl: '',
pdfTitle: '',
pdfDescription: '',
pdfCover: '',
symptoms: [] as string[],
isPreorder: false,
preorderDeposit: '' as number | string
});
const fetchData = useCallback(async () => {
try {
setIsLoading(true);
// Fetch products with full query params
const prodRes = await api.get('/admin/products', {
params: {
page,
limit,
search: search || undefined,
categoryId: categoryFilter || undefined,
suitableFor: suitableForFilter || undefined,
sortBy,
sortOrder
}
});
if (prodRes.data?.data) {
setProducts(prodRes.data.data);
setTotalPages(prodRes.data.meta?.lastPage || 1);
}
// Fetch categories separately
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, suitableForFilter, sortBy, sortOrder]);
useEffect(() => {
const timer = setTimeout(() => {
fetchData();
}, 400);
return () => clearTimeout(timer);
}, [fetchData]);
const openModal = (product: Product | null = null) => {
setMediaImageError(false);
if (product) {
setEditingProduct(product);
const bPrice = product.buyPrice ? Number(product.buyPrice) : '';
const pPrice = Number(product.priceValue || 0);
const wPrice = product.wholesalePrice ? Number(product.wholesalePrice) : '';
const pMargin = bPrice && pPrice ? Math.round(((pPrice - Number(bPrice)) / Number(bPrice)) * 100) : '';
const wMargin = bPrice && wPrice ? Math.round(((wPrice - Number(bPrice)) / Number(bPrice)) * 100) : '';
setFormData({
artNo: product.artNo || '',
nameFa: product.nameFa || '',
nameEn: product.nameEn || '',
scientificTagline: product.scientificTagline || '',
description: product.description || '',
shortDescription: product.shortDescription || '',
categoryId: product.categoryId || '',
buyPrice: bPrice,
priceValue: pPrice,
wholesalePrice: wPrice,
priceValueMarginPercent: pMargin,
wholesaleMarginPercent: wMargin,
priceDisplay: product.priceDisplay || '',
unit: product.unit || '',
packageSize: Number(product.packageSize) || 0,
dosageLogic: product.dosageLogic || '',
suitableFor: product.suitableFor || 'سگ و گربه',
imageUrl: product.imageUrl || '',
slug: product.slug || '',
metaTitle: product.metaTitle || '',
metaDescription: product.metaDescription || '',
keywords: product.keywords || '',
canonicalUrl: product.canonicalUrl || '',
images: Array.isArray(product.images) ? product.images : [],
podcastUrl: product.podcastUrl || '',
podcastTitle: product.podcastTitle || '',
podcastDescription: product.podcastDescription || '',
podcastCover: product.podcastCover || '',
videoUrl: product.videoUrl || '',
videoTitle: product.videoTitle || '',
videoDescription: product.videoDescription || '',
videoCover: product.videoCover || '',
pdfUrl: product.pdfUrl || '',
pdfTitle: product.pdfTitle || '',
pdfDescription: product.pdfDescription || '',
pdfCover: product.pdfCover || '',
symptoms: product.symptoms ? product.symptoms.map((s: { symptom: string } | string) => typeof s === 'string' ? s : s.symptom) : [],
isPreorder: Boolean(product.isPreorder),
preorderDeposit: product.preorderDeposit || ''
});
} else {
setEditingProduct(null);
setFormData({
artNo: '', nameFa: '', nameEn: '', scientificTagline: '', description: '', shortDescription: '', categoryId: '',
buyPrice: '', priceValue: 0, wholesalePrice: '', priceValueMarginPercent: '', wholesaleMarginPercent: '',
priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه',
imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '',
images: [],
podcastUrl: '', podcastTitle: '', podcastDescription: '', podcastCover: '',
videoUrl: '', videoTitle: '', videoDescription: '', videoCover: '',
pdfUrl: '', pdfTitle: '', pdfDescription: '', pdfCover: '',
symptoms: [], isPreorder: false, preorderDeposit: ''
});
}
setActiveTab('general');
setIsModalOpen(true);
};
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
// Explicit Tab-Aware Client-Side Validation
if (!formData.artNo?.trim()) {
setActiveTab('general');
toast.error('لطفاً کد محصول (Art No) را وارد نمایید');
return;
}
if (!formData.nameFa?.trim()) {
setActiveTab('general');
toast.error('لطفاً نام فارسی محصول را وارد نمایید');
return;
}
if (!formData.nameEn?.trim()) {
setActiveTab('general');
toast.error('لطفاً نام انگلیسی محصول را وارد نمایید');
return;
}
if (!formData.categoryId?.trim()) {
setActiveTab('general');
toast.error('لطفاً دسته‌بندی محصول را انتخاب نمایید');
return;
}
if (formData.priceValue === undefined || formData.priceValue === null || isNaN(Number(formData.priceValue))) {
setActiveTab('pricing');
toast.error('لطفاً قیمت فروش محصول را وارد نمایید');
return;
}
try {
const derivedSlug = (formData.slug || '').trim() || slugify(formData.nameEn || '');
const finalPayload: Record<string, unknown> = {
artNo: (formData.artNo || '').trim(),
nameFa: (formData.nameFa || '').trim(),
nameEn: (formData.nameEn || '').trim(),
scientificTagline: (formData.scientificTagline || '').trim() || undefined,
description: (formData.description || '').trim() || undefined,
shortDescription: (formData.shortDescription || '').trim() || undefined,
categoryId: (formData.categoryId || '').trim(),
buyPrice: formData.buyPrice ? Number(formData.buyPrice) : 0,
priceValue: Number(formData.priceValue || 0),
wholesalePrice: formData.wholesalePrice ? Number(formData.wholesalePrice) : undefined,
priceValueMarginPercent: formData.priceValueMarginPercent !== '' ? Number(formData.priceValueMarginPercent) : undefined,
wholesaleMarginPercent: formData.wholesaleMarginPercent !== '' ? Number(formData.wholesaleMarginPercent) : undefined,
priceDisplay: formData.priceDisplay || `${Number(formData.priceValue || 0).toLocaleString('fa-IR')} تومان`,
unit: (formData.unit || '').trim() || undefined,
packageSize: Number(formData.packageSize) || 0,
dosageLogic: (formData.dosageLogic || '').trim() || undefined,
suitableFor: formData.suitableFor || 'سگ و گربه',
imageUrl: (formData.imageUrl || '').trim() || undefined,
images: formData.images || [],
podcastUrl: (formData.podcastUrl || '').trim() || undefined,
podcastTitle: (formData.podcastTitle || '').trim() || undefined,
podcastDescription: (formData.podcastDescription || '').trim() || undefined,
podcastCover: (formData.podcastCover || '').trim() || undefined,
videoUrl: (formData.videoUrl || '').trim() || undefined,
videoTitle: (formData.videoTitle || '').trim() || undefined,
videoDescription: (formData.videoDescription || '').trim() || undefined,
videoCover: (formData.videoCover || '').trim() || undefined,
pdfUrl: (formData.pdfUrl || '').trim() || undefined,
pdfTitle: (formData.pdfTitle || '').trim() || undefined,
pdfDescription: (formData.pdfDescription || '').trim() || undefined,
pdfCover: (formData.pdfCover || '').trim() || undefined,
metaTitle: (formData.metaTitle || '').trim() || `${formData.nameFa || 'نام محصول'} | خرید و قیمت مکمل کنینا`,
metaDescription: (formData.metaDescription || '').trim() || formData.shortDescription || 'خرید آنلاین مکمل اصل کنینا آلمان با بالاترین کیفیت بالینی...',
keywords: (formData.keywords || '').trim() || undefined,
canonicalUrl: (formData.canonicalUrl || '').trim() || `/shop/${derivedSlug}`,
slug: derivedSlug,
symptoms: formData.symptoms || [],
isPreorder: Boolean(formData.isPreorder),
preorderDeposit: formData.preorderDeposit ? String(formData.preorderDeposit) : undefined
};
if (editingProduct) {
const res = await api.put(`/admin/products/${editingProduct.id}`, finalPayload);
const updated = res.data?.data || res.data;
if (updated && updated.id) {
setProducts(prev => prev.map(p => p.id === editingProduct.id ? { ...p, ...updated } : p));
}
toast.success('محصول با موفقیت ویرایش شد');
} else {
const res = await api.post('/admin/products', finalPayload);
const created = res.data?.data || res.data;
if (created && created.id) {
setProducts(prev => [created, ...prev]);
}
toast.success('محصول با موفقیت ایجاد شد');
}
setIsModalOpen(false);
setImageErrors({});
await fetchData();
} catch (error: unknown) {
console.error('Save failed', error);
const axiosErr = error as { response?: { data?: { message?: string } }; message?: string; _toastShown?: boolean };
const serverMsg = axiosErr.response?.data?.message || axiosErr.message;
if (serverMsg && !axiosErr._toastShown) {
toast.error(`خطا در ذخیره محصول: ${serverMsg}`);
}
}
};
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
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()}
>
محصول جدید
</Button>
</div>
{/* Filters Bar */}
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col md: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="جستجو حرفه‌ای در نام فارسی، نام انگلیسی (Latin)، کد محصول، بارکد، ترکیبات و علائم..."
value={search}
onChange={(e) => {
const val = e.target.value;
setSearch(val);
setPage(1);
updateUrlParams({ search: val, page: 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 text-sm"
/>
</div>
<div className="flex flex-wrap sm:flex-nowrap gap-3">
<div className="relative w-full sm:w-56">
<Filter className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
<select
value={categoryFilter}
onChange={(e) => {
const val = e.target.value;
setCategoryFilter(val);
setPage(1);
updateUrlParams({ category: val, page: 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 bg-white text-sm"
>
<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="relative w-full sm:w-44">
<select
value={suitableForFilter}
onChange={(e) => {
const val = e.target.value;
setSuitableForFilter(val);
setPage(1);
updateUrlParams({ suitableFor: val, page: 1 });
}}
className="w-full px-4 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 bg-white text-sm"
>
<option value="">همه گونه‌ها</option>
<option value="سگ">مخصوص سگ</option>
<option value="گربه">مخصوص گربه</option>
<option value="سگ و گربه">سگ و گربه</option>
</select>
</div>
</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 select-none">
<tr>
<th className="py-4 px-6 text-sm font-bold text-gray-500">تصویر</th>
<th
onClick={() => handleSort('artNo')}
className="py-4 px-6 text-sm font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
>
<div className="flex items-center gap-1.5">
<span>کد (Art No)</span>
{sortBy === 'artNo' ? (
sortOrder === 'asc' ? <ArrowUp className="w-4 h-4 text-purple-600" /> : <ArrowDown className="w-4 h-4 text-purple-600" />
) : (
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
)}
</div>
</th>
<th
onClick={() => handleSort('nameFa')}
className="py-4 px-6 text-sm font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
>
<div className="flex items-center gap-1.5">
<span>نام محصول</span>
{sortBy === 'nameFa' ? (
sortOrder === 'asc' ? <ArrowUp className="w-4 h-4 text-purple-600" /> : <ArrowDown className="w-4 h-4 text-purple-600" />
) : (
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
)}
</div>
</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">دسته‌بندی</th>
<th
onClick={() => handleSort('priceValue')}
className="py-4 px-6 text-sm font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
>
<div className="flex items-center gap-1.5">
<span>موجودی / قیمت</span>
{sortBy === 'priceValue' ? (
sortOrder === 'asc' ? <ArrowUp className="w-4 h-4 text-purple-600" /> : <ArrowDown className="w-4 h-4 text-purple-600" />
) : (
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
)}
</div>
</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">
{typeof product.category === 'object' && product.category !== null
? (product.category.nameFa || product.category.name || 'بدون دسته')
: (typeof product.category === 'string' ? product.category : 'بدون دسته')}
</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-3 sm:p-4 bg-gray-900/60 backdrop-blur-xs font-vazir animate-in fade-in duration-200" dir="rtl">
<div className="bg-white rounded-3xl w-full max-w-4xl max-h-[92dvh] sm:max-h-[88vh] flex flex-col shadow-2xl overflow-hidden border border-gray-100 animate-in zoom-in-95 duration-200">
<div className="px-5 sm:px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-white shrink-0">
<div className="flex items-center gap-3">
<h3 className="text-base sm:text-lg font-black 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-2.5 py-1 rounded-full text-[11px] font-bold bg-purple-50 border border-purple-100 text-purple-600 hover:bg-purple-100 transition-all cursor-pointer"
>
مشاهده در سایت 🔗
</a>
)}
</div>
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="w-8 h-8 rounded-xl bg-gray-50 text-gray-400 hover:text-gray-700 hover:bg-gray-100 flex items-center justify-center transition-colors cursor-pointer"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="flex border-b border-gray-100 px-5 sm:px-6 pt-2 gap-3 sm:gap-4 overflow-x-auto shrink-0 bg-gray-50/50">
{[
{ 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-xs sm:text-sm font-bold border-b-2 whitespace-nowrap transition-colors cursor-pointer ${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-4 sm:p-6 overflow-y-auto flex-1 overscroll-contain 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>
{/* Dosage Calculator Field in General Tab */}
<div className="space-y-2 pt-2 border-t border-gray-100">
<label className="text-sm font-bold text-gray-700 flex items-center justify-between">
<span className="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-64 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
دستور مصرف روان و فارسی کالا (مانند: روزانه ۱ قرص به ازای هر ۱۰ کیلوگرم وزن بدن). این متن در کاتالوگ آنلاین و صفحه مشخصات کالا به صورت برجسته نمایش داده می‌شود.
</div>
</div>
</span>
<span className="text-xs text-purple-600 font-bold">متن راهنمای بالینی</span>
</label>
<textarea
rows={3}
value={formData.dosageLogic}
onChange={(e) => setFormData({ ...formData, dosageLogic: e.target.value })}
placeholder="مثال: روزانه ۱ قرص به ازای هر ۱۰ کیلوگرم وزن بدن، همراه با وعده غذایی مصرف شود."
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-vazir"
/>
<p className="text-[11px] text-gray-400">
این متن در بخش مشخصات علمی کالا، ماشین‌حساب دوز و کاتالوگ آنلاین برای پزشکان و خریداران نمایش داده می‌شود.
</p>
</div>
</div>
{/* Pricing Tab */}
<div className={activeTab === 'pricing' ? 'block space-y-4' : 'hidden'}>
<div className="bg-amber-50/60 border border-amber-200 p-4 rounded-xl text-xs text-amber-800 flex items-start gap-2">
<HelpCircle className="w-4 h-4 text-amber-600 shrink-0 mt-0.5" />
<div>
<p className="font-bold mb-1">امنیت و حریم خصوصی قیمت خرید (سود و زیان)</p>
<p className="leading-relaxed text-[11px]">
قیمت خرید و درصد‌های سود به هیچ عنوان در پاسخ‌های API عمومی فرانت‌اند سایت منتشر نمی‌شوند و فقط جهت محاسبات حسابداری و گزارش‌گیری سود/زیان در پنل ادمین استفاده می‌شوند.
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Buy Price */}
<div className="space-y-2">
<label className="text-sm font-bold text-amber-900 flex items-center gap-1.5">
<span>قیمت خرید ما (تومان)</span>
<div className="group relative inline-block">
<HelpCircle className="w-3.5 h-3.5 text-amber-500 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>
<PriceInput
placeholder="مثال: ۵۰۰,۰۰۰"
value={formData.buyPrice}
onChange={(rawVal) => {
const bPrice = rawVal ? Number(rawVal) : '';
let pPrice = formData.priceValue;
let wPrice = formData.wholesalePrice;
let pMargin = formData.priceValueMarginPercent;
let wMargin = formData.wholesaleMarginPercent;
if (bPrice && pMargin !== '') {
pPrice = Math.round(Number(bPrice) * (1 + Number(pMargin) / 100));
} else if (bPrice && pPrice) {
pMargin = Math.round(((pPrice - Number(bPrice)) / Number(bPrice)) * 100);
}
if (bPrice && wMargin !== '') {
wPrice = Math.round(Number(bPrice) * (1 + Number(wMargin) / 100));
} else if (bPrice && wPrice) {
wMargin = Math.round(((Number(wPrice) - Number(bPrice)) / Number(bPrice)) * 100);
}
setFormData({
...formData,
buyPrice: bPrice,
priceValue: pPrice,
wholesalePrice: wPrice,
priceValueMarginPercent: pMargin,
wholesaleMarginPercent: wMargin,
priceDisplay: `${Number(pPrice || 0).toLocaleString('fa-IR')} تومان`,
});
}}
className="px-4 py-3 rounded-xl border border-amber-300 focus:border-amber-500 bg-amber-50/20 text-sm"
/>
</div>
{/* Retail Price & Margin */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-sm font-bold text-gray-700">قیمت فروش تک‌فروشی (تومان) *</label>
<span className="text-[10px] text-purple-600 font-bold">یا سود ٪ تک‌فروشی</span>
</div>
<div className="grid grid-cols-5 gap-2">
<div className="col-span-3">
<PriceInput
required
value={formData.priceValue}
onChange={(rawVal) => {
const val = Number(rawVal || 0);
const bPrice = formData.buyPrice ? Number(formData.buyPrice) : null;
const pMargin = bPrice && val ? Math.round(((val - bPrice) / bPrice) * 100) : '';
setFormData({
...formData,
priceValue: val,
priceValueMarginPercent: pMargin,
priceDisplay: `${val.toLocaleString('fa-IR')} تومان`,
});
}}
className="px-3 py-3 rounded-xl border border-gray-200 focus:border-purple-500 text-sm"
/>
</div>
<div className="col-span-2 relative">
<input
type="number"
placeholder="٪ سود"
value={formData.priceValueMarginPercent}
onChange={(e) => {
const margin = e.target.value !== '' ? Number(e.target.value) : '';
const bPrice = formData.buyPrice ? Number(formData.buyPrice) : null;
let pPrice = formData.priceValue;
if (bPrice && margin !== '') {
pPrice = Math.round(bPrice * (1 + Number(margin) / 100));
}
setFormData({
...formData,
priceValueMarginPercent: margin,
priceValue: pPrice,
priceDisplay: `${Number(pPrice || 0).toLocaleString('fa-IR')} تومان`,
});
}}
className="w-full px-2 py-3 rounded-xl border border-purple-200 focus:border-purple-500 text-center font-bold text-xs bg-purple-50/30 outline-none"
dir="ltr"
/>
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-xs pointer-events-none">٪</span>
</div>
</div>
</div>
{/* Wholesale B2B Price & Margin */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-sm font-bold text-purple-700">قیمت اختصاصی B2B (تومان)</label>
<span className="text-[10px] text-purple-600 font-bold">یا سود ٪ B2B</span>
</div>
<div className="grid grid-cols-5 gap-2">
<div className="col-span-3">
<PriceInput
placeholder="قیمت B2B"
value={formData.wholesalePrice}
onChange={(rawVal) => {
const val = rawVal ? Number(rawVal) : '';
const bPrice = formData.buyPrice ? Number(formData.buyPrice) : null;
const wMargin = bPrice && val ? Math.round(((Number(val) - bPrice) / bPrice) * 100) : '';
setFormData({
...formData,
wholesalePrice: val,
wholesaleMarginPercent: wMargin,
});
}}
className="px-3 py-3 rounded-xl border border-purple-200 focus:border-purple-600 bg-purple-50/20 text-sm"
/>
</div>
<div className="col-span-2 relative">
<input
type="number"
placeholder="٪ سود"
value={formData.wholesaleMarginPercent}
onChange={(e) => {
const margin = e.target.value !== '' ? Number(e.target.value) : '';
const bPrice = formData.buyPrice ? Number(formData.buyPrice) : null;
let wPrice = formData.wholesalePrice;
if (bPrice && margin !== '') {
wPrice = Math.round(bPrice * (1 + Number(margin) / 100));
}
setFormData({
...formData,
wholesaleMarginPercent: margin,
wholesalePrice: wPrice,
});
}}
className="w-full px-2 py-3 rounded-xl border border-purple-200 focus:border-purple-500 text-center font-bold text-xs bg-purple-50/30 outline-none"
dir="ltr"
/>
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-xs pointer-events-none">٪</span>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
<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">
<label className="text-sm font-bold text-gray-700">واحد سنجش</label>
<input type="text" placeholder="مثال: عدد / بسته" value={formData.unit} onChange={(e) => setFormData({ ...formData, unit: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</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">
<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>
{/* 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'}>
{/* Main Product Image Section */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
<div className="flex items-center justify-between border-b pb-2">
<span className="text-sm font-bold text-gray-800 flex items-center gap-2">
<ImageIcon className="w-4 h-4 text-purple-600" />
تصویر اصلی و کاور محصول (Main Cover Image)
</span>
<button
type="button"
onClick={() => {
setMediaTargetField('imageUrl');
setIsMediaSelectorOpen(true);
}}
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-1.5 rounded-xl text-xs font-bold transition-all cursor-pointer flex items-center gap-1.5"
>
<ImageIcon className="w-3.5 h-3.5" />
انتخاب تصویر اصلی
</button>
</div>
<div className="flex flex-col sm:flex-row gap-4 items-center">
<input
type="text"
value={formData.imageUrl}
onChange={(e) => {
setFormData({ ...formData, imageUrl: e.target.value });
setMediaImageError(false);
}}
placeholder="آدرس اینترنتی یا مسیر عکس اصلی (/uploads/cover.jpg)"
className="flex-1 w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-xs"
dir="ltr"
/>
{formData.imageUrl && (
<div className="relative group w-16 h-16 shrink-0 rounded-xl border border-gray-200 overflow-hidden bg-gray-50 flex items-center justify-center">
{mediaImageError ? (
<ImageIcon className="w-6 h-6 text-gray-300" />
) : (
<img
src={getProductImageUrl(formData.imageUrl)}
alt="Cover"
className="w-full h-full object-contain"
onError={() => setMediaImageError(true)}
/>
)}
</div>
)}
</div>
</div>
{/* Product Gallery Images (Multiple Angles & Views) */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
<div className="flex items-center justify-between border-b pb-2">
<div>
<h4 className="font-bold text-gray-800 text-sm flex items-center gap-2">
<ImageIcon className="w-4 h-4 text-blue-600" />
گالری تصاویر جانبی محصول (Gallery Images)
</h4>
<p className="text-[11px] text-gray-400 font-medium mt-0.5">تصاویر زوایای مختلف، پشت بسته‌بندی، جدول ارزش غذایی و مشخصات فنی</p>
</div>
<button
type="button"
onClick={() => {
setMediaTargetField('gallery');
setIsMediaSelectorOpen(true);
}}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 rounded-xl text-xs font-bold transition-all cursor-pointer flex items-center gap-1.5"
>
<Plus className="w-3.5 h-3.5" />
افزودن عکس به گالری
</button>
</div>
{formData.images && formData.images.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 gap-3">
{formData.images.map((imgUrl, idx) => (
<div key={idx} className="relative group rounded-xl border border-gray-200 overflow-hidden bg-gray-50 aspect-square flex items-center justify-center p-2 shadow-xs">
<img
src={getProductImageUrl(imgUrl)}
alt={`Gallery ${idx + 1}`}
className="max-h-full max-w-full object-contain"
/>
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1.5 p-1">
<button
type="button"
title="حذف از گالری"
onClick={() => {
const updated = formData.images.filter((_, i) => i !== idx);
setFormData({ ...formData, images: updated });
}}
className="w-7 h-7 rounded-lg bg-red-600 hover:bg-red-700 text-white flex items-center justify-center transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
<span className="absolute bottom-1 right-1 bg-black/60 text-white text-[9px] px-1.5 py-0.5 rounded font-mono">
#{idx + 1}
</span>
</div>
))}
</div>
) : (
<div className="text-center py-6 text-gray-400 bg-gray-50/50 rounded-xl border border-dashed border-gray-200">
<ImageIcon className="w-8 h-8 mx-auto mb-2 opacity-30 text-gray-400" />
<p className="text-xs font-bold text-gray-500">هنوز عکس جانبی به گالری محصول اضافه نشده است.</p>
<p className="text-[10px] text-gray-400 mt-1">با زدن دکمه «افزودن عکس به گالری» می‌توانید چند عکس اضافه نمایید.</p>
</div>
)}
</div>
{/* Multimedia Attachments */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-6">
<div>
<h4 className="font-bold text-gray-800 text-sm border-b pb-2">پیوست‌های مالتی‌مدیا و مستندات علمی تخصصی</h4>
<p className="text-[11px] text-gray-500 mt-1">مدیریت فایل‌های صوتی، تصویری و کاتالوگ‌های اختصاصی این محصول همراه با عنوان، توضیحات و کاور اختصاصی.</p>
</div>
{/* Section 1: Podcast Management Card */}
<div className="bg-purple-50/40 border border-purple-100 rounded-2xl p-4 space-y-3">
<div className="flex items-center justify-between border-b border-purple-100/80 pb-2">
<span className="text-xs font-black text-purple-900 flex items-center gap-1.5">
🎧 تنظیمات پادکست و بررسی صوتی بالینی
</span>
{formData.podcastUrl && (
<span className="text-[10px] bg-purple-200 text-purple-800 font-bold px-2 py-0.5 rounded-full">
فایل صوتی متصل است
</span>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">لینک یا فایل پادکست (Audio/MP3 URL)</label>
<div className="flex gap-1.5">
<input
type="text"
value={formData.podcastUrl}
onChange={(e) => setFormData({ ...formData, podcastUrl: e.target.value })}
placeholder="https://... /uploads/audio.mp3"
className="w-full px-3 py-2 border rounded-xl text-xs font-mono bg-white"
dir="ltr"
/>
<button
type="button"
onClick={() => {
setMediaTargetField('podcastUrl');
setIsMediaSelectorOpen(true);
}}
className="px-2.5 py-1 bg-purple-600 text-white hover:bg-purple-700 rounded-lg text-xs font-bold whitespace-nowrap cursor-pointer"
>
انتخاب فایل
</button>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">تصویر کاور اختصاصی پادکست (اختیاری)</label>
<div className="flex gap-1.5">
<input
type="text"
value={formData.podcastCover}
onChange={(e) => setFormData({ ...formData, podcastCover: e.target.value })}
placeholder="در صورت خالی بودن، تصویر اصلی محصول استفاده می‌شود"
className="w-full px-3 py-2 border rounded-xl text-xs font-mono bg-white"
dir="ltr"
/>
<button
type="button"
onClick={() => {
setMediaTargetField('podcastCover');
setIsMediaSelectorOpen(true);
}}
className="px-2.5 py-1 bg-purple-100 text-purple-700 hover:bg-purple-200 rounded-lg text-xs font-bold whitespace-nowrap cursor-pointer"
>
انتخاب کاور
</button>
</div>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">عنوان پادکست</label>
<input
type="text"
value={formData.podcastTitle}
onChange={(e) => setFormData({ ...formData, podcastTitle: e.target.value })}
placeholder="مثلاً: بررسی بالینی اثر مکمل بر سلامت مفاصل سگ‌ها"
className="w-full px-3 py-2 border rounded-xl text-xs bg-white font-medium"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">توضیحات کوتاه پادکست</label>
<textarea
rows={2}
value={formData.podcastDescription}
onChange={(e) => setFormData({ ...formData, podcastDescription: e.target.value })}
placeholder="توضیحات و نکات مطرح شده در پادکست تخصصی..."
className="w-full px-3 py-2 border rounded-xl text-xs bg-white font-medium resize-none"
/>
</div>
</div>
{/* Section 2: Video Management Card */}
<div className="bg-blue-50/40 border border-blue-100 rounded-2xl p-4 space-y-3">
<div className="flex items-center justify-between border-b border-blue-100/80 pb-2">
<span className="text-xs font-black text-blue-900 flex items-center gap-1.5">
🎬 تنظیمات ویدیو و راهنمای مصرف
</span>
{formData.videoUrl && (
<span className="text-[10px] bg-blue-200 text-blue-800 font-bold px-2 py-0.5 rounded-full">
ویدیو متصل است
</span>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">لینک مستقیم یا کدفریم ویدیو (Video URL / Embed)</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 iframe"
className="w-full px-3 py-2 border rounded-xl text-xs font-mono bg-white"
dir="ltr"
/>
<button
type="button"
onClick={() => {
setMediaTargetField('videoUrl');
setIsMediaSelectorOpen(true);
}}
className="px-2.5 py-1 bg-blue-600 text-white hover:bg-blue-700 rounded-lg text-xs font-bold whitespace-nowrap cursor-pointer"
>
انتخاب فایل
</button>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">تصویر پوستر / کاور ویدیو (اختیاری)</label>
<div className="flex gap-1.5">
<input
type="text"
value={formData.videoCover}
onChange={(e) => setFormData({ ...formData, videoCover: e.target.value })}
placeholder="در صورت خالی بودن، تصویر محصول استفاده می‌شود"
className="w-full px-3 py-2 border rounded-xl text-xs font-mono bg-white"
dir="ltr"
/>
<button
type="button"
onClick={() => {
setMediaTargetField('videoCover');
setIsMediaSelectorOpen(true);
}}
className="px-2.5 py-1 bg-blue-100 text-blue-700 hover:bg-blue-200 rounded-lg text-xs font-bold whitespace-nowrap cursor-pointer"
>
انتخاب کاور
</button>
</div>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">عنوان ویدیو</label>
<input
type="text"
value={formData.videoTitle}
onChange={(e) => setFormData({ ...formData, videoTitle: e.target.value })}
placeholder="مثلاً: راهنمای نحوه خوراندن و دوز مصرف مکمل"
className="w-full px-3 py-2 border rounded-xl text-xs bg-white font-medium"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">توضیحات کوتاه ویدیو</label>
<textarea
rows={2}
value={formData.videoDescription}
onChange={(e) => setFormData({ ...formData, videoDescription: e.target.value })}
placeholder="توضیحات ویدیوی آموزشی و نکات کلیدی..."
className="w-full px-3 py-2 border rounded-xl text-xs bg-white font-medium resize-none"
/>
</div>
</div>
{/* Section 3: PDF / Document Management Card */}
<div className="bg-amber-50/40 border border-amber-100 rounded-2xl p-4 space-y-3">
<div className="flex items-center justify-between border-b border-amber-100/80 pb-2">
<span className="text-xs font-black text-amber-900 flex items-center gap-1.5">
📄 تنظیمات کاتالوگ و مستندات PDF
</span>
{formData.pdfUrl && (
<span className="text-[10px] bg-amber-200 text-amber-800 font-bold px-2 py-0.5 rounded-full">
فایل PDF متصل است
</span>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">لینک یا فایل کاتالوگ (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://... /uploads/catalog.pdf"
className="w-full px-3 py-2 border rounded-xl text-xs font-mono bg-white"
dir="ltr"
/>
<button
type="button"
onClick={() => {
setMediaTargetField('pdfUrl');
setIsMediaSelectorOpen(true);
}}
className="px-2.5 py-1 bg-amber-600 text-white hover:bg-amber-700 rounded-lg text-xs font-bold whitespace-nowrap cursor-pointer"
>
انتخاب فایل
</button>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">تصویر پیش‌نمایش کاتالوگ (اختیاری)</label>
<div className="flex gap-1.5">
<input
type="text"
value={formData.pdfCover}
onChange={(e) => setFormData({ ...formData, pdfCover: e.target.value })}
placeholder="در صورت خالی بودن، تصویر محصول استفاده می‌شود"
className="w-full px-3 py-2 border rounded-xl text-xs font-mono bg-white"
dir="ltr"
/>
<button
type="button"
onClick={() => {
setMediaTargetField('pdfCover');
setIsMediaSelectorOpen(true);
}}
className="px-2.5 py-1 bg-amber-100 text-amber-800 hover:bg-amber-200 rounded-lg text-xs font-bold whitespace-nowrap cursor-pointer"
>
انتخاب کاور
</button>
</div>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">عنوان فایل PDF</label>
<input
type="text"
value={formData.pdfTitle}
onChange={(e) => setFormData({ ...formData, pdfTitle: e.target.value })}
placeholder="مثلاً: کاتالوگ جامع مشخصات فنی و مطالعات بالینی"
className="w-full px-3 py-2 border rounded-xl text-xs bg-white font-medium"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-700 block">توضیحات فایل PDF</label>
<textarea
rows={2}
value={formData.pdfDescription}
onChange={(e) => setFormData({ ...formData, pdfDescription: e.target.value })}
placeholder="توضیحات و محتویات کاتالوگ و بروشور رسمی..."
className="w-full px-3 py-2 border rounded-xl text-xs bg-white font-medium resize-none"
/>
</div>
</div>
</div>
</div>
</form>
</div>
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-2.5 rounded-b-2xl">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="productForm"
>
ذخیره محصول
</Button>
</div>
</div>
</div>
)}
{/* Media Selector Modal */}
<MediaSelector
isOpen={isMediaSelectorOpen}
onClose={() => setIsMediaSelectorOpen(false)}
onSelect={(url) => {
if ((mediaTargetField as string) === 'gallery') {
if (!formData.images.includes(url)) {
setFormData({ ...formData, images: [...formData.images, url] });
}
} else {
setFormData({ ...formData, [mediaTargetField]: url });
if (mediaTargetField === 'imageUrl') setMediaImageError(false);
}
}}
selectedUrl={
(mediaTargetField as string) === 'gallery'
? undefined
: formData[mediaTargetField]?.startsWith('http')
? formData[mediaTargetField]
: formData[mediaTargetField]
? `${BASE_DOMAIN}${formData[mediaTargetField]}`
: undefined
}
/>
<ConfirmModal
isOpen={!!deleteTargetId}
title="حذف محصول"
message="آیا از حذف این محصول مطمئن هستید؟ این عملیات قابل بازگشت نیست."
onConfirm={confirmDelete}
onCancel={() => setDeleteTargetId(null)}
/>
</div>
);
}