import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { TrashIcon, PlusIcon, PencilIcon, TagIcon, MapPinIcon, BuildingOffice2Icon, HeartIcon, ShieldCheckIcon, WrenchScrewdriverIcon, CheckCircleIcon, XCircleIcon, PhotoIcon, XMarkIcon, } from '@heroicons/react/24/outline'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; import type { Category, CategoryBundle, Representation } from '../types'; import { useAuthStore } from '../stores/authStore'; import PageHeader from '../components/ui/PageHeader'; import DataTable, { Column } from '../components/ui/DataTable'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import Modal from '../components/ui/Modal'; import SearchableSelect from '../components/ui/SearchableSelect'; // ── Tab config ────────────────────────────────────────────────────────────── interface TabConfig { key: CategoryBundle; label: string; icon: React.ElementType; color: string; description: string; hasParent: boolean; parentBundle?: CategoryBundle; parentLabel?: string; hasLogo: boolean; hasCityFields: boolean; hasWeight: boolean; } const TABS: TabConfig[] = [ { key: 'state', label: 'استان‌ها', icon: MapPinIcon, color: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-400/10', description: 'مدیریت استان‌ها', hasParent: false, hasLogo: false, hasCityFields: false, hasWeight: true, }, { key: 'city', label: 'شهرها', icon: BuildingOffice2Icon, color: 'text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-400/10', description: 'مدیریت شهرها و اطلاعات تفصیلی هر شهر', hasParent: true, parentBundle: 'state', parentLabel: 'استان', hasLogo: false, hasCityFields: true, hasWeight: true, }, { key: 'specially_doctor', label: 'تخصص‌های پزشکی', icon: HeartIcon, color: 'text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-400/10', description: 'تخصص‌ها و زیرتخصص‌های پزشکی', hasParent: true, parentBundle: 'specially_doctor', parentLabel: 'تخصص والد', hasLogo: false, hasCityFields: false, hasWeight: true, }, { key: 'doctor_services', label: 'خدمات پزشک', icon: WrenchScrewdriverIcon, color: 'text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-400/10', description: 'خدمات قابل ارائه توسط پزشکان', hasParent: false, hasLogo: false, hasCityFields: false, hasWeight: true, }, { key: 'insurance_type', label: 'بیمه پایه', icon: ShieldCheckIcon, color: 'text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-400/10', description: 'انواع بیمه‌های پایه درمانی', hasParent: false, hasLogo: true, hasCityFields: false, hasWeight: false, }, { key: 'supplementary_insurance', label: 'بیمه تکمیلی', icon: ShieldCheckIcon, color: 'text-cyan-600 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-400/10', description: 'انواع بیمه‌های تکمیلی درمانی', hasParent: false, hasLogo: true, hasCityFields: false, hasWeight: false, }, { key: 'tag', label: 'تگ‌های بلاگ', icon: TagIcon, color: 'text-violet-600 dark:text-violet-400 bg-violet-50 dark:bg-violet-400/10', description: 'تگ‌های مورد استفاده در مطالب بلاگ', hasParent: false, hasLogo: false, hasCityFields: false, hasWeight: true, }, ]; // ── Zod schema ────────────────────────────────────────────────────────────── const baseSchema = z.object({ label: z.string().min(1, 'نام الزامی است'), status: z.string().optional(), weight: z.string().optional(), parent_id: z.number().nullable().optional(), representation_id: z.number().nullable().optional(), title: z.string().optional(), contact_phone: z.string().optional(), email: z.string().optional(), description: z.string().optional(), slogan: z.string().optional(), domain: z.string().optional(), keywords: z.string().optional(), footer_description: z.string().optional(), footer_disclaimer: z.string().optional(), }); type FormData = z.infer; // ── Logo uploader ──────────────────────────────────────────────────────────── function LogoUploadField({ value, onChange, }: { value: string | null; onChange: (url: string | null) => void; }) { const [uploading, setUploading] = useState(false); const token = useAuthStore((s) => s.token); const handleFile = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setUploading(true); try { const res = await fetch('/api/v1/admin/category/upload-logo', { method: 'POST', headers: { 'Content-Disposition': `attachment; filename="${file.name}"`, 'Content-Type': 'application/octet-stream', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: file, }); const json = await res.json(); if (json.success) { onChange(json.data.url); toast.success('لگو آپلود شد'); } else { toast.error(json.errors?.[0]?.message ?? 'خطا در آپلود'); } } catch { toast.error('خطا در آپلود تصویر'); } finally { setUploading(false); e.target.value = ''; } }; return (
{value ? (
logo
) : (
)}

JPG، PNG یا WebP — حداکثر ۵ مگابایت

); } // ── Helper to load parent list ────────────────────────────────────────────── function useBundleList(bundle: CategoryBundle | undefined) { return useQuery({ queryKey: ['categories', bundle], queryFn: () => api.get>(`/api/v1/categorys/${bundle}`), enabled: !!bundle, staleTime: 60_000, }); } // ── Dynamic form fields ───────────────────────────────────────────────────── interface FormFieldsProps { register: ReturnType>['register']; control: ReturnType>['control']; errors: ReturnType>['formState']['errors']; tab: TabConfig; parentOptions: { value: number; label: string }[]; representationOptions: { value: number; label: string }[]; parentLoading: boolean; representationLoading: boolean; logoUrl: string | null; onLogoChange: (url: string | null) => void; } function CategoryFormFields({ register, control, errors, tab, parentOptions, representationOptions, parentLoading, representationLoading, logoUrl, onLogoChange, }: FormFieldsProps) { return (
{/* Logo upload (insurance bundles) */} {tab.hasLogo && (
)} {/* Label */}
{errors.label &&

{errors.label.message}

}
{/* Parent selector */} {tab.hasParent && (
( field.onChange(val as number | null)} placeholder="-- بدون والد --" isClearable isLoading={parentLoading} noOptionsMessage="موردی یافت نشد" /> )} />
)} {/* Representation selector (city only) */} {tab.key === 'city' && (
( field.onChange(val as number | null)} placeholder="انتخاب نماینده..." isClearable isLoading={representationLoading} noOptionsMessage="هیچ نماینده‌ای یافت نشد" /> )} />
)} {/* Title (city) */} {tab.key === 'city' && (
)} {/* City-specific fields */} {tab.hasCityFields && ( <>